commit ef4b1a3076f60571c3f0ebcd504bfbf889809483
Author: Jim Stichnoth <jstichnoth@mythtv.org>
Date:   Wed Apr 15 07:45:15 2015 -0700

    Add utility marks for preroll amount and latest playback position.
    
    ==== Preroll:
    
    The recorder writes a mark of the new type MARK_UTIL_PREROLL as soon
    as it encounters a keyframe close to the recording's scheduled start
    time.
    
    The frontend starts playback from this keyframe if the user selects
    Play and there is no explicit bookmark (but not if the user selects
    one of the "Play from..." menu items).
    
    ==== Playback position:
    
    During playback, a timer event auto-saves the current playback frame
    with the new MARK_UTIL_PLAYPOS mark type, every 30 seconds. The actual
    saving is done in a background thread to avoid playback glitches.
    Saving is done only during active playback, i.e. not while paused.
    The MARK_UTIL_PLAYPOS mark is tidied up (cleared) if the user saves a
    bookmark while exiting playback.
    
    The "Play from..." menu gets a new item (a new translatable string):
        Play from last played position
    
    This item is added to the menu only when the recording markup actually
    contains a MARK_UTIL_PLAYPOS mark.
    
    The motivation of MARK_UTIL_PLAYPOS is that if you accidentally exit
    playback without setting a bookmark (or the frontend crashes, or the
    OS crashes, or ...), this allows you to resume roughly where you left
    off.  Because of the 30-second timer, if you start playing normally
    and realize you've made a mistake, you have 30 seconds to exit
    playback and instead "Play from last played position".
    
    Note that different frontends can fight over setting this mark, same
    as with a regular bookmark.
    
    Still to do:
    1. Preview generator can do less guessing about preroll.
    2. Provide MythVideo launcher with similar preroll/playpos options.

diff --git a/mythtv/libs/libmyth/programinfo.cpp b/mythtv/libs/libmyth/programinfo.cpp
index 8764134..f4b5a77 100644
--- a/mythtv/libs/libmyth/programinfo.cpp
+++ b/mythtv/libs/libmyth/programinfo.cpp
@@ -2695,6 +2695,40 @@ uint64_t ProgramInfo::QueryBookmark(uint chanid, const QDateTime &recstartts)
     return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
 }
 
+/** \brief Gets any preroll position in database,
+ *         unless the ignore preroll flag is set.
+ *
+ *  \return Preroll position in frames if the query is executed
+ *          and succeeds, zero otherwise.
+ */
+uint64_t ProgramInfo::QueryPreroll(void) const
+{
+    if (programflags & FL_IGNOREPREROLL)
+        return 0;
+
+    frm_dir_map_t bookmarkmap;
+    QueryMarkupMap(bookmarkmap, MARK_UTIL_PREROLL);
+
+    return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
+}
+
+/** \brief Gets any playpos position in database,
+ *         unless the ignore playpos flag is set.
+ *
+ *  \return PlayPos position in frames if the query is executed
+ *          and succeeds, zero otherwise.
+ */
+uint64_t ProgramInfo::QueryPlayPos(void) const
+{
+    if (programflags & FL_IGNOREPLAYPOS)
+        return 0;
+
+    frm_dir_map_t bookmarkmap;
+    QueryMarkupMap(bookmarkmap, MARK_UTIL_PLAYPOS);
+
+    return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
+}
+
 /** \brief Queries "dvdbookmark" table for bookmarking DVD serial
  *         number. Deletes old dvd bookmarks if "delbookmark" is set.
  *
diff --git a/mythtv/libs/libmyth/programinfo.h b/mythtv/libs/libmyth/programinfo.h
index ce8159d..b90e609 100644
--- a/mythtv/libs/libmyth/programinfo.h
+++ b/mythtv/libs/libmyth/programinfo.h
@@ -452,7 +452,7 @@ class MPUBLIC ProgramInfo
 
     uint32_t GetProgramFlags(void)        const { return programflags; }
     ProgramInfoType GetProgramInfoType(void) const
-        { return (ProgramInfoType)((programflags & FL_TYPEMASK) >> 16); }
+        { return (ProgramInfoType)((programflags & FL_TYPEMASK) >> 20); }
     bool IsGeneric(void) const;
     bool IsInUsePlaying(void)   const { return programflags & FL_INUSEPLAYING;}
     bool IsCommercialFree(void) const { return programflags & FL_CHANCOMMFREE;}
@@ -489,7 +489,7 @@ class MPUBLIC ProgramInfo
     // Quick sets
     void SetTitle(const QString &t) { title = t; title.detach(); }
     void SetProgramInfoType(ProgramInfoType t)
-        { programflags &= ~FL_TYPEMASK; programflags |= ((uint32_t)t<<16); }
+        { programflags &= ~FL_TYPEMASK; programflags |= ((uint32_t)t<<20); }
     void SetPathname(const QString&) const;
     void SetChanID(uint _chanid) { chanid = _chanid; }
     void SetScheduledStartTime(const QDateTime &dt) { startts      = dt;    }
@@ -532,6 +532,20 @@ class MPUBLIC ProgramInfo
         programflags &= ~FL_IGNOREBOOKMARK;
         programflags |= (ignore) ? FL_IGNOREBOOKMARK : 0;
     }
+    /// \brief If "ignore" is true QueryPreroll() will return 0, otherwise
+    ///        QueryPreroll() will return the preroll position if it exists.
+    void SetIgnorePreroll(bool ignore)
+    {
+        programflags &= ~FL_IGNOREPREROLL;
+        programflags |= (ignore) ? FL_IGNOREPREROLL : 0;
+    }
+    /// \brief If "ignore" is true QueryPlayPos() will return 0, otherwise
+    ///        QueryPlayPos() will return the playback position if it exists.
+    void SetIgnorePlayPos(bool ignore)
+    {
+        programflags &= ~FL_IGNOREPLAYPOS;
+        programflags |= (ignore) ? FL_IGNOREPLAYPOS : 0;
+    }
     virtual void SetRecordingID(uint _recordedid) { recordedid = _recordedid; }
     void SetRecordingStatus(RecStatus::Type status) { recstatus = status; }
     void SetRecordingRuleType(RecordingType type) { rectype   = type;   }
@@ -544,6 +558,8 @@ class MPUBLIC ProgramInfo
     uint        QueryMplexID(void) const;
     QDateTime   QueryBookmarkTimeStamp(void) const;
     uint64_t    QueryBookmark(void) const;
+    uint64_t    QueryPreroll(void) const;
+    uint64_t    QueryPlayPos(void) const;
     CategoryType QueryCategoryType(void) const;
     QStringList QueryDVDBookmark(const QString &serialid) const;
     bool        QueryIsEditing(void) const;
@@ -658,7 +674,6 @@ class MPUBLIC ProgramInfo
     static QMap<QString,bool> QueryJobsRunning(int type);
     static QStringList LoadFromScheduler(const QString &altTable, int recordid);
 
-  protected:
     // Flagging map support methods
     void QueryMarkupMap(frm_dir_map_t&, MarkTypes type,
                         bool merge = false) const;
@@ -667,6 +682,7 @@ class MPUBLIC ProgramInfo
     void ClearMarkupMap(MarkTypes type = MARK_ALL,
                         int64_t min_frm = -1, int64_t max_frm = -1) const;
 
+  protected:
     // Creates a basename from the start and end times
     QString CreateRecordBasename(const QString &ext) const;
 
diff --git a/mythtv/libs/libmyth/programtypes.cpp b/mythtv/libs/libmyth/programtypes.cpp
index 26b5357..475484b 100644
--- a/mythtv/libs/libmyth/programtypes.cpp
+++ b/mythtv/libs/libmyth/programtypes.cpp
@@ -53,6 +53,8 @@ QString toString(MarkTypes type)
         case MARK_VIDEO_RATE:   return "VIDEO_RATE";
         case MARK_DURATION_MS:  return "DURATION_MS";
         case MARK_TOTAL_FRAMES: return "TOTAL_FRAMES";
+        case MARK_UTIL_PREROLL: return "UTIL_PREROLL";
+        case MARK_UTIL_PLAYPOS: return "UTIL_PLAYPOS";
     }
 
     return "unknown";
diff --git a/mythtv/libs/libmyth/programtypes.h b/mythtv/libs/libmyth/programtypes.h
index d85ee94..974adb1 100644
--- a/mythtv/libs/libmyth/programtypes.h
+++ b/mythtv/libs/libmyth/programtypes.h
@@ -74,6 +74,8 @@ typedef enum {
     MARK_VIDEO_RATE    = 32,
     MARK_DURATION_MS   = 33,
     MARK_TOTAL_FRAMES  = 34,
+    MARK_UTIL_PREROLL  = 40,
+    MARK_UTIL_PLAYPOS  = 41,
 } MarkTypes;
 MPUBLIC QString toString(MarkTypes type);
 
@@ -145,11 +147,13 @@ typedef enum FlagMask {
     FL_DUPLICATE      = 0x00002000,
     FL_REACTIVATE     = 0x00004000,
     FL_IGNOREBOOKMARK = 0x00008000,
+    FL_IGNOREPREROLL  = 0x00010000,
+    FL_IGNOREPLAYPOS  = 0x00020000,
     // if you move the type mask please edit {Set,Get}ProgramInfoType()
-    FL_TYPEMASK       = 0x000F0000,
-    FL_INUSERECORDING = 0x00100000,
-    FL_INUSEPLAYING   = 0x00200000,
-    FL_INUSEOTHER     = 0x00400000,
+    FL_TYPEMASK       = 0x00F00000,
+    FL_INUSERECORDING = 0x01000000,
+    FL_INUSEPLAYING   = 0x02000000,
+    FL_INUSEOTHER     = 0x04000000,
 } ProgramFlag;
 
 typedef enum ProgramInfoType {
diff --git a/mythtv/libs/libmythtv/mythplayer.cpp b/mythtv/libs/libmythtv/mythplayer.cpp
index b62b86c..2ce4a75 100644
--- a/mythtv/libs/libmythtv/mythplayer.cpp
+++ b/mythtv/libs/libmythtv/mythplayer.cpp
@@ -2870,7 +2870,14 @@ void MythPlayer::EventStart(void)
     player_ctx->LockPlayingInfo(__FILE__, __LINE__);
     {
         if (player_ctx->playingInfo)
+        {
+            // When initial playback gets underway, we override the ProgramInfo
+            // flags such that future calls to GetBookmark() will consider only
+            // an actual bookmark and not preroll or playpos information.
             player_ctx->playingInfo->SetIgnoreBookmark(false);
+            player_ctx->playingInfo->SetIgnorePreroll(true);
+            player_ctx->playingInfo->SetIgnorePlayPos(true);
+        }
     }
     player_ctx->UnlockPlayingInfo(__FILE__, __LINE__);
     commBreakMap.LoadMap(player_ctx, framesPlayed);
@@ -3601,7 +3608,13 @@ uint64_t MythPlayer::GetBookmark(void)
     {
         player_ctx->LockPlayingInfo(__FILE__, __LINE__);
         if (player_ctx->playingInfo)
+        {
             bookmark = player_ctx->playingInfo->QueryBookmark();
+            if (bookmark == 0)
+                bookmark = player_ctx->playingInfo->QueryPreroll();
+            if (bookmark == 0)
+                bookmark = player_ctx->playingInfo->QueryPlayPos();
+        }
         player_ctx->UnlockPlayingInfo(__FILE__, __LINE__);
     }
 
diff --git a/mythtv/libs/libmythtv/recorders/recorderbase.cpp b/mythtv/libs/libmythtv/recorders/recorderbase.cpp
index 669c872..8654562 100644
--- a/mythtv/libs/libmythtv/recorders/recorderbase.cpp
+++ b/mythtv/libs/libmythtv/recorders/recorderbase.cpp
@@ -56,7 +56,8 @@ RecorderBase::RecorderBase(TVRec *rec)
       request_pause(false),     paused(false),
       request_recording(false), recording(false),
       nextRingBuffer(NULL),     nextRecording(NULL),
-      positionMapType(MARK_GOP_BYFRAME)
+      positionMapType(MARK_GOP_BYFRAME),
+      estimatedPrerollMS(0), lastSavedKeyframe(0), lastSavedDuration(0)
 {
     ClearStatistics();
     QMutexLocker locker(avcodeclock);
@@ -114,6 +115,12 @@ void RecorderBase::SetRecording(const RecordingInfo *pginfo)
         //       instance which may lead to the possibility that changes made
         //       in the database by one are overwritten by the other
         curRecording = new RecordingInfo(*pginfo);
+        // Compute an estimate of the actual preroll for setting the
+        // MARK_UTIL_PREROLL mark.  We can't reliably use
+        // curRecording->GetRecordingStartTime() because the scheduler rounds it
+        // to the nearest minute, so we use the current time instead.
+        estimatedPrerollMS =
+            MythDate::current().msecsTo(curRecording->GetScheduledStartTime());
         RecordingFile *recFile = curRecording->GetRecordingFile();
         recFile->m_containerFormat = m_containerFormat;
         recFile->Save();
@@ -579,15 +586,15 @@ void RecorderBase::SavePositionMap(bool force, bool finished)
     bool needToSave = force;
     positionMapLock.lock();
 
-    uint delta_size = positionMapDelta.size();
+    bool has_delta = !positionMapDelta.empty();
     // set pm_elapsed to a fake large value if the timer hasn't yet started
     uint pm_elapsed = (positionMapTimer.isRunning()) ?
         positionMapTimer.elapsed() : ~0;
     // save on every 1.5 seconds if in the first few frames of a recording
     needToSave |= (positionMap.size() < 30) &&
-        (delta_size >= 1) && (pm_elapsed >= 1500);
+        has_delta && (pm_elapsed >= 1500);
     // save every 10 seconds later on
-    needToSave |= (delta_size >= 1) && (pm_elapsed >= 10000);
+    needToSave |= has_delta && (pm_elapsed >= 10000);
     // Assume that durationMapDelta is the same size as
     // positionMapDelta and implicitly use the same logic about when
     // to same durationMapDelta.
@@ -595,7 +602,7 @@ void RecorderBase::SavePositionMap(bool force, bool finished)
     if (curRecording && needToSave)
     {
         positionMapTimer.start();
-        if (delta_size)
+        if (has_delta)
         {
             // copy the delta map because most times we are called it will be in
             // another thread and we don't want to lock the main recorder thread
@@ -609,6 +616,8 @@ void RecorderBase::SavePositionMap(bool force, bool finished)
             curRecording->SavePositionMapDelta(deltaCopy, positionMapType);
             curRecording->SavePositionMapDelta(durationDeltaCopy,
                                                MARK_DURATION_MS);
+
+            TryWritePrerollMark(durationDeltaCopy);
         }
         else
         {
@@ -626,6 +635,81 @@ void RecorderBase::SavePositionMap(bool force, bool finished)
     }
 }
 
+void RecorderBase::TryWritePrerollMark(const frm_pos_map_t &durationDeltaCopy)
+{
+    // Note: all log strings contain "preroll bookmark" for searching.
+    if (estimatedPrerollMS <= 0)
+    {
+        // Do nothing because no preroll bookmark is needed.
+        LOG(VB_RECORD, LOG_DEBUG,
+            QString("No preroll bookmark needed because delta=%1")
+            .arg(estimatedPrerollMS));
+        return;
+    }
+    frm_pos_map_t::const_iterator last_it = durationDeltaCopy.end();
+    --last_it;
+    long long bookmarkFrame = 0;
+    LOG(VB_RECORD, LOG_DEBUG,
+        QString("durationDeltaCopy.begin() = (%1,%2)")
+        .arg(durationDeltaCopy.begin().key())
+        .arg(durationDeltaCopy.begin().value()));
+    if (estimatedPrerollMS > *last_it)
+    {
+        // Do nothing because we haven't reached recstartts yet.
+        LOG(VB_RECORD, LOG_DEBUG,
+            QString("No preroll bookmark yet because estimatedPrerollMS=%1 "
+                    "and *last_it=%2")
+            .arg(estimatedPrerollMS).arg(*last_it));
+    }
+    else if (lastSavedDuration <= estimatedPrerollMS &&
+             estimatedPrerollMS < *durationDeltaCopy.begin())
+    {
+        // Set preroll bookmark @ lastSavedKeyframe
+        LOG(VB_RECORD, LOG_DEBUG,
+            QString("Set preroll bookmark=%1 because %2<=%3<%4")
+            .arg(lastSavedKeyframe).arg(lastSavedDuration)
+            .arg(estimatedPrerollMS).arg(*durationDeltaCopy.begin()));
+        bookmarkFrame = lastSavedKeyframe;
+    }
+    else if (*durationDeltaCopy.begin() <= estimatedPrerollMS &&
+             estimatedPrerollMS < *last_it)
+    {
+        frm_pos_map_t::const_iterator upper_it = durationDeltaCopy.begin();
+        for (; upper_it != durationDeltaCopy.end(); ++upper_it)
+        {
+            if (*upper_it > estimatedPrerollMS)
+            {
+                --upper_it;
+                // Set preroll bookmark @ upper_it.key()
+                LOG(VB_RECORD, LOG_DEBUG,
+                    QString("Set preroll bookmark=%1 because "
+                            "estimatedPrerollMS=%2 and upper_it.value()=%3")
+                    .arg(upper_it.key()).arg(estimatedPrerollMS)
+                    .arg(upper_it.value()));
+                bookmarkFrame = upper_it.key();
+                break;
+            }
+        }
+    }
+    else
+    {
+        // do nothing
+        LOG(VB_RECORD, LOG_DEBUG, "No preroll bookmark due to fallthrough");
+    }
+    if (bookmarkFrame)
+    {
+        frm_dir_map_t prerollMap;
+        prerollMap[bookmarkFrame] = MARK_UTIL_PREROLL;
+        curRecording->SaveMarkupMap(prerollMap, MARK_UTIL_PREROLL);
+    }
+    lastSavedKeyframe = last_it.key();
+    lastSavedDuration = last_it.value();
+    LOG(VB_RECORD, LOG_DEBUG,
+        QString("Setting lastSavedKeyframe=%1 lastSavedDuration=%2 "
+                "for preroll bookmark calculations")
+        .arg(lastSavedKeyframe).arg(lastSavedDuration));
+}
+
 void RecorderBase::AspectChange(uint aspect, long long frame)
 {
     MarkTypes mark = MARK_ASPECT_4_3;
diff --git a/mythtv/libs/libmythtv/recorders/recorderbase.h b/mythtv/libs/libmythtv/recorders/recorderbase.h
index 8d00836..88e88a7 100644
--- a/mythtv/libs/libmythtv/recorders/recorderbase.h
+++ b/mythtv/libs/libmythtv/recorders/recorderbase.h
@@ -292,6 +292,8 @@ class MTV_PUBLIC RecorderBase : public QRunnable
      */
     void SetTotalFrames(uint64_t total_frames);
 
+    void TryWritePrerollMark(const frm_pos_map_t &durationDeltaCopy);
+
     TVRec         *tvrec;
     RingBuffer    *ringBuffer;
     bool           weMadeBuffer;
@@ -341,6 +343,11 @@ class MTV_PUBLIC RecorderBase : public QRunnable
     frm_pos_map_t  durationMapDelta;
     MythTimer      positionMapTimer;
 
+    // Preroll bookmark support
+    qint64         estimatedPrerollMS;
+    long long      lastSavedKeyframe;
+    long long      lastSavedDuration;
+
     // Statistics
     // Note: Once we enter RecorderBase::run(), only that thread can
     // update these values safely. These values are read in that thread
diff --git a/mythtv/libs/libmythtv/tv_play.cpp b/mythtv/libs/libmythtv/tv_play.cpp
index ed338fa..55f9afb 100644
--- a/mythtv/libs/libmythtv/tv_play.cpp
+++ b/mythtv/libs/libmythtv/tv_play.cpp
@@ -138,6 +138,7 @@ const uint TV::kEndOfPlaybackFirstCheckTimer = 60000;
 #else
 const uint TV::kEndOfPlaybackFirstCheckTimer = 5000;
 #endif
+const uint TV::kSavePlayPosTimeout           = 30000;
 
 /**
  * \brief stores last program info. maintains info so long as
@@ -335,6 +336,8 @@ bool TV::StartTV(ProgramInfo *tvrec, uint flags,
     {
         curProgram = new ProgramInfo(*tvrec);
         curProgram->SetIgnoreBookmark(flags & kStartTVIgnoreBookmark);
+        curProgram->SetIgnorePreroll(flags & kStartTVIgnorePreroll);
+        curProgram->SetIgnorePlayPos(flags & kStartTVIgnorePlayPos);
     }
 
     PlaySettings settings(curProgram ? curProgram->GetPlaybackGroup() : "Default");
@@ -1070,7 +1073,8 @@ TV::TV(PlaySettings *settings)
       endOfPlaybackTimerId(0),      embedCheckTimerId(0),
       endOfRecPromptTimerId(0),     videoExitDialogTimerId(0),
       pseudoChangeChanTimerId(0),   speedChangeTimerId(0),
-      errorRecoveryTimerId(0),      exitPlayerTimerId(0)
+      errorRecoveryTimerId(0),      exitPlayerTimerId(0),
+      savePlayPosTimerId(0)
 {
     LOG(VB_GENERAL, LOG_INFO, LOC + "Creating TV object");
     ctorTime.start();
@@ -1333,6 +1337,7 @@ bool TV::Init(bool createWindow)
     errorRecoveryTimerId = StartTimer(kErrorRecoveryCheckFrequency, __LINE__);
     lcdTimerId           = StartTimer(1, __LINE__);
     speedChangeTimerId   = StartTimer(kSpeedChangeCheckFrequency, __LINE__);
+    savePlayPosTimerId   = StartTimer(kSavePlayPosTimeout, __LINE__);
 
     LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- end");
     return true;
@@ -2786,6 +2791,8 @@ void TV::timerEvent(QTimerEvent *te)
         HandleSpeedChangeTimerEvent();
     else if (timer_id == pipChangeTimerId)
         HandlePxPTimerEvent();
+    else if (timer_id == savePlayPosTimerId)
+        HandleSavePlayPosEvent();
     else
         handled = false;
 
@@ -3330,10 +3337,18 @@ void TV::PrepareToExitPlayer(PlayerContext *ctx, int line, BookmarkAction bookma
             // Don't consider ourselves at the end if the recording is
             // in-progress.
             at_end &= !StateIsRecording(GetState(ctx));
+            bool clear_playpos = true;
             if (at_end && allow_clear_at_end)
                 SetBookmark(ctx, true);
-            if (!at_end && allow_set_before_end)
+            else if (!at_end && allow_set_before_end)
                 SetBookmark(ctx, false);
+            else
+                clear_playpos = false;
+            // If we are setting a bookmark upon exit (or equivalently clearing
+            // it due to exiting at the end), we clean up the unnecessary
+            // playpos mark.
+            if (clear_playpos && ctx->playingInfo)
+                ctx->playingInfo->ClearMarkupMap(MARK_UTIL_PLAYPOS);
         }
         if (db_auto_set_watched)
             ctx->player->SetWatched();
@@ -13392,6 +13407,53 @@ bool TV::HandleOSDVideoExit(PlayerContext *ctx, QString action)
     return hide;
 }
 
+void TV::HandleSavePlayPosEvent(void)
+{
+    // Helper class to save the latest playback position (in a background thread
+    // to avoid playback glitches).  The ctor makes a copy of the ProgramInfo
+    // struct to avoid race conditions if playback ends and deletes objects
+    // before or while the background thread runs.
+    class PositionSaver : public QRunnable
+    {
+    public:
+        PositionSaver(const ProgramInfo &pginfo, uint64_t frame) :
+            m_pginfo(pginfo), m_frame(frame) {}
+        virtual void run(void)
+        {
+            LOG(VB_PLAYBACK, LOG_DEBUG,
+                QString("PositionSaver frame=%1").arg(m_frame));
+            frm_dir_map_t playPosMap;
+            playPosMap[m_frame] = MARK_UTIL_PLAYPOS;
+            m_pginfo.ClearMarkupMap(MARK_UTIL_PLAYPOS);
+            m_pginfo.SaveMarkupMap(playPosMap, MARK_UTIL_PLAYPOS);
+        }
+    private:
+        const ProgramInfo m_pginfo;
+        const uint64_t m_frame;
+    };
+
+    PlayerContext *mctx = GetPlayerReadLock(0, __FILE__, __LINE__);
+    for (uint i = 0; mctx && i < player.size(); ++i)
+    {
+        PlayerContext *ctx = GetPlayer(mctx, i);
+        ctx->LockDeletePlayer(__FILE__, __LINE__);
+        bool playing = ctx->player && !ctx->player->IsPaused();
+        if (playing) // Don't bother saving playpos while paused
+        {
+            uint64_t framesPlayed = ctx->player->GetFramesPlayed();
+            MThreadPool::globalInstance()->
+                start(new PositionSaver(*ctx->playingInfo, framesPlayed),
+                      "PositionSaver");
+        }
+        ReturnPlayerLock(ctx);
+    }
+    ReturnPlayerLock(mctx);
+
+    QMutexLocker locker(&timerIdLock);
+    KillTimer(savePlayPosTimerId);
+    savePlayPosTimerId = StartTimer(kSavePlayPosTimeout, __LINE__);
+}
+
 void TV::SetLastProgram(const ProgramInfo *rcinfo)
 {
     QMutexLocker locker(&lastProgramLock);
diff --git a/mythtv/libs/libmythtv/tv_play.h b/mythtv/libs/libmythtv/tv_play.h
index c55bb3a..39c31c9 100644
--- a/mythtv/libs/libmythtv/tv_play.h
+++ b/mythtv/libs/libmythtv/tv_play.h
@@ -116,6 +116,8 @@ enum {
     kStartTVInPlayList       = 0x02,
     kStartTVByNetworkCommand = 0x04,
     kStartTVIgnoreBookmark   = 0x08,
+    kStartTVIgnorePreroll    = 0x10,
+    kStartTVIgnorePlayPos    = 0x20,
 };
 
 class AskProgramInfo
@@ -410,6 +412,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     bool HandlePxPTimerEvent(void);
     bool HandleLCDTimerEvent(void);
     void HandleLCDVolumeTimerEvent(void);
+    void HandleSavePlayPosEvent();
 
     // Commands used by frontend UI screens (PlaybackBox, GuideGrid etc)
     void EditSchedule(const PlayerContext*,
@@ -980,6 +983,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     volatile int         speedChangeTimerId;
     volatile int         errorRecoveryTimerId;
     mutable volatile int exitPlayerTimerId;
+    volatile int         savePlayPosTimerId;
     TimerContextMap      stateChangeTimerId;
     TimerContextMap      signalMonitorTimerId;
 
@@ -1096,6 +1100,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     static const uint kErrorRecoveryCheckFrequency;
     static const uint kEndOfRecPromptCheckFrequency;
     static const uint kEndOfPlaybackFirstCheckTimer;
+    static const uint kSavePlayPosTimeout;
 };
 
 #endif
diff --git a/mythtv/programs/mythfrontend/playbackbox.cpp b/mythtv/programs/mythfrontend/playbackbox.cpp
index 1050b4a..116b1bf 100644
--- a/mythtv/programs/mythfrontend/playbackbox.cpp
+++ b/mythtv/programs/mythfrontend/playbackbox.cpp
@@ -556,7 +556,7 @@ bool PlaybackBox::Create()
     connect(m_recordingList, SIGNAL(itemSelected(MythUIButtonListItem*)),
             SLOT(ItemSelected(MythUIButtonListItem*)));
     connect(m_recordingList, SIGNAL(itemClicked(MythUIButtonListItem*)),
-            SLOT(PlayFromBookmark(MythUIButtonListItem*)));
+            SLOT(PlayFromBookmarkOrPreroll(MythUIButtonListItem*)));
     connect(m_recordingList, SIGNAL(itemVisible(MythUIButtonListItem*)),
             SLOT(ItemVisible(MythUIButtonListItem*)));
     connect(m_recordingList, SIGNAL(itemLoaded(MythUIButtonListItem*)),
@@ -2197,6 +2197,25 @@ void PlaybackBox::playSelectedPlaylist(bool _random)
         this, new MythEvent("PLAY_PLAYLIST"));
 }
 
+void PlaybackBox::PlayFromBookmarkOrPreroll(MythUIButtonListItem *item)
+{
+    if (!item)
+        item = m_recordingList->GetItemCurrent();
+
+    if (!item)
+        return;
+
+    ProgramInfo *pginfo = item->GetData().value<ProgramInfo *>();
+
+    const bool ignoreBookmark = false;
+    const bool ignorePreroll = false;
+    const bool ignorePlayPos = true;
+    const bool underNetworkControl = false;
+    if (pginfo)
+        PlayX(*pginfo, ignoreBookmark, ignorePreroll, ignorePlayPos,
+              underNetworkControl);
+}
+
 void PlaybackBox::PlayFromBookmark(MythUIButtonListItem *item)
 {
     if (!item)
@@ -2207,8 +2226,13 @@ void PlaybackBox::PlayFromBookmark(MythUIButtonListItem *item)
 
     ProgramInfo *pginfo = item->GetData().value<ProgramInfo *>();
 
+    const bool ignoreBookmark = false;
+    const bool ignorePreroll = true;
+    const bool ignorePlayPos = true;
+    const bool underNetworkControl = false;
     if (pginfo)
-        PlayX(*pginfo, false, false);
+        PlayX(*pginfo, ignoreBookmark, ignorePreroll, ignorePlayPos,
+              underNetworkControl);
 }
 
 void PlaybackBox::PlayFromBeginning(MythUIButtonListItem *item)
@@ -2221,17 +2245,44 @@ void PlaybackBox::PlayFromBeginning(MythUIButtonListItem *item)
 
     ProgramInfo *pginfo = item->GetData().value<ProgramInfo *>();
 
+    const bool ignoreBookmark = true;
+    const bool ignorePreroll = true;
+    const bool ignorePlayPos = true;
+    const bool underNetworkControl = false;
+    if (pginfo)
+        PlayX(*pginfo, ignoreBookmark, ignorePreroll, ignorePlayPos,
+              underNetworkControl);
+}
+
+void PlaybackBox::PlayFromPlayPos(MythUIButtonListItem *item)
+{
+    if (!item)
+        item = m_recordingList->GetItemCurrent();
+
+    if (!item)
+        return;
+
+    ProgramInfo *pginfo = item->GetData().value<ProgramInfo *>();
+
+    const bool ignoreBookmark = true;
+    const bool ignorePreroll = true;
+    const bool ignorePlayPos = false;
+    const bool underNetworkControl = false;
     if (pginfo)
-        PlayX(*pginfo, true, false);
+        PlayX(*pginfo, ignoreBookmark, ignorePreroll, ignorePlayPos,
+              underNetworkControl);
 }
 
 void PlaybackBox::PlayX(const ProgramInfo &pginfo,
                         bool ignoreBookmark,
+                        bool ignorePreroll,
+                        bool ignorePlayPos,
                         bool underNetworkControl)
 {
     if (!m_player)
     {
-        Play(pginfo, false, ignoreBookmark, underNetworkControl);
+        Play(pginfo, false, ignoreBookmark, ignorePreroll, ignorePlayPos,
+             underNetworkControl);
         return;
     }
 
@@ -2242,6 +2293,7 @@ void PlaybackBox::PlayX(const ProgramInfo &pginfo,
             ignoreBookmark ? "1" : "0");
         m_player_selected_new_show.push_back(
             underNetworkControl ? "1" : "0");
+        // XXX add anything for ignorePreroll and ignorePlayPos?
     }
     Close();
 }
@@ -2324,7 +2376,7 @@ void PlaybackBox::selected(MythUIButtonListItem *item)
     if (!item)
         return;
 
-    PlayFromBookmark(item);
+    PlayFromBookmarkOrPreroll(item);
 }
 
 void PlaybackBox::popupClosed(QString which, int result)
@@ -2412,7 +2464,8 @@ void PlaybackBox::ShowGroupPopup()
 
 bool PlaybackBox::Play(
     const ProgramInfo &rec,
-    bool inPlaylist, bool ignoreBookmark, bool underNetworkControl)
+    bool inPlaylist, bool ignoreBookmark, bool ignorePreroll,
+    bool ignorePlayPos, bool underNetworkControl)
 {
     bool playCompleted = false;
 
@@ -2444,6 +2497,8 @@ bool PlaybackBox::Play(
     uint flags =
         (inPlaylist          ? kStartTVInPlayList       : kStartTVNoFlags) |
         (underNetworkControl ? kStartTVByNetworkCommand : kStartTVNoFlags) |
+        (ignorePlayPos       ? kStartTVIgnorePlayPos    : kStartTVNoFlags) |
+        (ignorePreroll       ? kStartTVIgnorePreroll    : kStartTVNoFlags) |
         (ignoreBookmark      ? kStartTVIgnoreBookmark   : kStartTVNoFlags);
 
     playCompleted = TV::StartTV(&tvrec, flags);
@@ -2912,8 +2967,12 @@ MythMenu* PlaybackBox::createPlayFromMenu()
 
     MythMenu *menu = new MythMenu(title, this, "slotmenu");
 
-    menu->AddItem(tr("Play from bookmark"),  SLOT(PlayFromBookmark()));
+    if (pginfo->IsBookmarkSet())
+        menu->AddItem(tr("Play from bookmark"), SLOT(PlayFromBookmark()));
     menu->AddItem(tr("Play from beginning"), SLOT(PlayFromBeginning()));
+    if (pginfo->QueryPlayPos())
+        menu->AddItem(tr("Play from last played position"),
+                      SLOT(PlayFromPlayPos()));
 
     return menu;
 }
@@ -3149,10 +3208,10 @@ void PlaybackBox::ShowActionPopup(const ProgramInfo &pginfo)
 
     if (!sameProgram)
     {
-        if (pginfo.IsBookmarkSet())
+        if (pginfo.IsBookmarkSet() || pginfo.QueryPlayPos())
             m_popupMenu->AddItem(tr("Play from..."), NULL, createPlayFromMenu());
         else
-            m_popupMenu->AddItem(tr("Play"), SLOT(PlayFromBookmark()));
+            m_popupMenu->AddItem(tr("Play"), SLOT(PlayFromBookmarkOrPreroll()));
     }
 
     if (!m_player)
@@ -3743,8 +3802,12 @@ void PlaybackBox::processNetworkControlCommand(const QString &command)
 
                 pginfo.SetPathname(pginfo.GetPlaybackURL());
 
-                bool ignoreBookmark = (tokens[1] == "PLAY");
-                PlayX(pginfo, ignoreBookmark, true);
+                const bool ignoreBookmark = (tokens[1] == "PLAY");
+                const bool ignorePreroll = true;
+                const bool ignorePlayPos = true;
+                const bool underNetworkControl = true;
+                PlayX(pginfo, ignoreBookmark, ignorePreroll, ignorePlayPos,
+                      underNetworkControl);
             }
             else
             {
@@ -3877,7 +3940,7 @@ bool PlaybackBox::keyPressEvent(QKeyEvent *event)
             if (action == "DELETE")
                 deleteSelected(m_recordingList->GetItemCurrent());
             else if (action == ACTION_PLAYBACK)
-                PlayFromBookmark();
+                PlayFromBookmarkOrPreroll();
             else if (action == "DETAILS" || action == "INFO")
                 ShowDetails();
             else if (action == "CUSTOMEDIT")
@@ -4154,7 +4217,13 @@ void PlaybackBox::customEvent(QEvent *event)
                 else if (pginfo)
                 {
                     playnext = false;
-                    Play(*pginfo, kCheckForPlaylistAction == cat, false, false);
+                    const bool ignoreBookmark = false;
+                    const bool ignorePreroll = false;
+                    const bool ignorePlayPos = true;
+                    const bool underNetworkControl = false;
+                    Play(*pginfo, kCheckForPlaylistAction == cat,
+                         ignoreBookmark, ignorePreroll, ignorePlayPos,
+                         underNetworkControl);
                 }
             }
 
@@ -4183,8 +4252,13 @@ void PlaybackBox::customEvent(QEvent *event)
             }
 
             ProgramInfo *pginfo = FindProgramInUILists(recordingID);
+            const bool ignoreBookmark = false;
+            const bool ignorePreroll = true;
+            const bool ignorePlayPos = true;
+            const bool underNetworkControl = false;
             if (pginfo)
-                Play(*pginfo, true, false, false);
+                Play(*pginfo, true, ignoreBookmark, ignorePreroll,
+                     ignorePlayPos, underNetworkControl);
         }
         else if ((message == "SET_PLAYBACK_URL") && (me->ExtraDataCount() == 2))
         {
diff --git a/mythtv/programs/mythfrontend/playbackbox.h b/mythtv/programs/mythfrontend/playbackbox.h
index 18ae480..4415113 100644
--- a/mythtv/programs/mythfrontend/playbackbox.h
+++ b/mythtv/programs/mythfrontend/playbackbox.h
@@ -140,8 +140,10 @@ class PlaybackBox : public ScheduleCommon
     void ItemVisible(MythUIButtonListItem *item);
     void ItemLoaded(MythUIButtonListItem *item);
     void selected(MythUIButtonListItem *item);
+    void PlayFromBookmarkOrPreroll(MythUIButtonListItem *item = NULL);
     void PlayFromBookmark(MythUIButtonListItem *item = NULL);
     void PlayFromBeginning(MythUIButtonListItem *item = NULL);
+    void PlayFromPlayPos(MythUIButtonListItem *item = NULL);
     void deleteSelected(MythUIButtonListItem *item);
 
     void SwitchList(void);
@@ -270,11 +272,15 @@ class PlaybackBox : public ScheduleCommon
 
     void PlayX(const ProgramInfo &rec,
                bool ignoreBookmark,
+               bool ignorePreroll,
+               bool ignorePlayPos,
                bool underNetworkControl);
 
     bool Play(const ProgramInfo &rec,
               bool inPlaylist,
               bool ignoreBookmark,
+              bool ignorePreroll,
+              bool ignorePlayPos,
               bool underNetworkControl);
 
     virtual ProgramInfo *GetCurrentProgram(void) const;
