I have been trying for two weeks to get seeking to work properly, and have just come to the conclusion that it does not work.
I would be happy if someone could point me to an updated player sample that actually works correctly. (Able to single step frames, and seek while paused or stopped).
Since I noticed in the Chromium source code that they play videos by interacting with a topology directly (e.g. use a timer to create playback, not using the Session, I am thinking that might be the way to go).
So, what have I done? First: I have reviewed the following references exhaustively:
MSDN Samples:
- Seeking, FastForward and Reverse Play: Describes a PlayerSeeking object, where the "CommitRateChange" method encodes a number of rules regarding how seeking works (I do not find these rules documented anywhere else).
- <g class="gr_ gr_648 gr-alert gr_gramm gr_disable_anim_appear Punctuation only-del replaceWithoutSep" data-gr-id="648" id="648">MFPlayerSample</g><g
class="gr_ gr_648 gr-alert gr_gramm gr_disable_anim_appear Punctuation only-del replaceWithoutSep" data-gr-id="648" id="648">:</g> contains a slightly modified version of the player seeking object referenced above.
This sample does work while paused: If you open a file, then click in the seek bar, it seeks, but then begins playback. - How to Perform Scrubbing: basic scrub operation.
Relevant Posts from this Forum:
- Seeking in <g class="gr_ gr_335 gr-alert gr_gramm gr_disable_anim_appear Grammar only-ins doubleReplace replaceWithoutSep" data-gr-id="335" id="335">video</g> while keeping playback Paused: Says to look at MFPlayer Sample. Does NOT work correctly.
- AVI Source Not Frame Accurate: Documents that you will have problems seeking near the end of an AVI file.
Concepts in the PlayerSeeking Object:
The player seeking object is confusing to me. It conflates 'commands' with 'state', and uses a 'request' and a 'pending' variable.
Nearly as I can tell:
- "Pending" means we have started an async operation and are waiting for its completion event.
- "request" means an operation that will be completed after the pending completes.
- For the actual seek (m_pSession->Start(null, &varStart) where varStart contains the MFTIME index) we receive MESessionStarted, MESessionScrubSampleComplete, and the session delivers a frame (ONCE and ONLY ONCE )
What Events Do I See Now:
The first attempt to seek succeeds, and goes through the following sequence:
- call SetPosition(x) -> Scrub(True) which calls SetRate(0) which calls CommitRate: performs m_pRate->SetRate(false, 0.0)
- MESessionRateChanged event = Ok: next performs pending seek = m_pSession->Start(null, &varStart) with correct timestamp.
- MESessionStart event occurs
- MESessionScrubSampleEvent occurs
- Frame is delivered to session
Subsequent attempts to seek do not deliver a frame, regardless of whether I leave the session started, stop, or pause before attempting to seek to the next frame. I have tried various permutations of what to do after the first frame is delivered:
- Scrub -> Seek(Rate = 0), Start(position) -> processFrame -> Scrub(false) -> SetRate(0) -> Stop()
- Scrub -> Seek(Rate = 0), Start(position) -> processFrame -> Scrub(false) -> SetRate(0) -> Pause()
- Scrub -> Seek(Rate = 0), Start(position) -> processFrame -> Scrub(false)
- Scrub -> Seek(Rate = 0), Start(position) -> processFrame
In each of the above I have tried Scrub(false) in varioius places:
- In OnSessionStarted event
- In ScrubSampleCompleteEvent
- After Processing frame.
I have been banging my head for two weeks and it just doesn't work.
Attempts to fix the Player Seeking Object:
I ended up modifying the player seeking object, replacing the 'pending' state variable with a queue.
Here is my code, it uses a Qt Queue. The queue generally works as designed, although there are currently quite a few problems with it, it gets' into states where it doesn't handle the "updatePendingCommands" code properly (Mostly due to the fact that the frames are never delivered, leaving pending operations on the queue).
Major Modifications:
- modified SetPosition to call Scrub(true) to handle 'single-click' on toolbar slider.
- Moved code handling rate changed into OnSessionRateChanged
- Replaced all direct settings of m_state and m_pending with Setters so that I can do debug logging.
- If we are in a pending state, additional operations are pushed onto a queue instead of just put into a single variable. Original code handled the scrubbing in a sliderPressed event, which apparently allowed them to get by with a variable instead of a queue: The scrub(true) pending event was able to complete before the position setting, which seems to work by accident, or because of the way the slider in the sample delivers <g class="gr_ gr_4552 gr-alert gr_spell gr_disable_anim_appear ContextualSpelling" data-gr-id="4552" id="4552">it's</g> events.
- Modified UpdatePendingCommands to use the queue
- Modifications to CommitRateChange, and SessionEvent.
- Added code in various places to detect if we have a pending call and work around it. These are pretty hacky.
Really, the complicated parts of this object are CommitRateChange, SessionEvent, and UpdatePendingCommands interaction.
Header File: MFPlayerSeeking.h:
//------------------------------------------------------------------------------------------------- /// Microsoft Sample Code (MODIFIED) /// This is Microsoft Sample code related to media foundation seeking of video playback. /// See Windows Dev Center article "Seeking, Fast Forward, and Reverse Play in Media Foundation /// programming guide: /// https://msdn.microsoft.com/en-us/library/windows/desktop/ee892373?f=255&MSPPError=-2147217396 /// /// \file Backends\MediaFoundation\MFPlayerSeeking /// \author Microsoft /// \date 10/21/2016 /// \brief Microsoft Media Foundation - Video Playback Seeking, Fast Forward and Reverse Play //------------------------------------------------------------------------------------------------- #pragma once #ifdef _WINDOWS #include "ZCritSect.h" #include <mfapi.h> #include <mfidl.h> // Forward declarations //struct IMFMediaSession; //struct IMFMediaType; //struct IMFTopology; // #include <QQueue> namespace Video { //------------------------------------------------------------------------------------------------- /// \class MFPlayerSeeking /// \brief Implements seeking and rate control functionality. /// /// \li Call SetTopology when you get the MF_TOPOSTATUS_READY session event. /// \li Call SessionEvent for each session event. /// \li Call Clear before closing the Media Session. /// \li To coordinate rate-change requests with transport state, delegate all stop, pause, and play commands to the PlayerSeeking class. //------------------------------------------------------------------------------------------------- class MFPlayerSeeking { public: MFPlayerSeeking(); virtual ~MFPlayerSeeking(); HRESULT SetTopology(IMFMediaSession *pSession, IMFTopology *pTopology); HRESULT Clear(); HRESULT SessionEvent(MediaEventType type, HRESULT hrStatus, IMFMediaEvent *pEvent); HRESULT CanSeek(BOOL *pbCanSeek); HRESULT GetDuration(MFTIME *phnsDuration); HRESULT GetCurrentPosition(MFTIME *phnsPosition); HRESULT SetPosition(MFTIME hnsPosition); HRESULT CanScrub(BOOL *pbCanScrub); BOOL IsScrubbing(); HRESULT Scrub(BOOL bScrub); void ProcessFrameEvent(); HRESULT CanFastForward(BOOL *pbCanFF); HRESULT CanRewind(BOOL *pbCanRewind); HRESULT SetRate(float fRate); HRESULT FastForward(); HRESULT Rewind(); HRESULT Start(); HRESULT Pause(); HRESULT Stop(); private: static const int RATE_INVALID = -100.0; static const int TIME_INVALID = -1; enum Command { CmdNone = 0, CmdStop, CmdStart, CmdPause, CmdSeek, // Transitory state! CmdRate // Transitory state! }; // Bit flags! enum PendState { PendNone = 0, PendCmd = 0x01, PendSeek = 0x02 // PendRate = 0x04 // Defined but never referenced in original sample Why?? }; HRESULT SetPositionInternal(const MFTIME &hnsPosition); HRESULT CommitRateChange(float fRate, BOOL bThin); float GetNominalRate(); HRESULT OnSessionStart(HRESULT hr); HRESULT OnSessionStop(HRESULT hr); HRESULT OnSessionPause(HRESULT hr); HRESULT OnSessionEnded(HRESULT hr); HRESULT OnSessionRateChanged(HRESULT hr, IMFMediaEvent* pEvent); HRESULT UpdatePendingCommands(); private: /// \class CCmd /// \brief Describes the current or requested state, with respect to seeking and playback rate. struct CCmd { public: CCmd() : cmd(CmdNone), fRate(0.0), bThin(false), hnsStart(-1) { } CCmd(Command c) : cmd(c), fRate(0.0), bThin(false), hnsStart(-1) { } CCmd(Command c, float f, BOOL th, MFTIME t) : cmd(c), fRate(f), bThin(th), hnsStart(t) { } ~CCmd() {} CCmd(const CCmd& other) { cmd = other.cmd; fRate = other.fRate; bThin = other.bThin; hnsStart = other.hnsStart; } CCmd& operator=(const CCmd& other) = default; Command cmd; float fRate; // Playback rate BOOL bThin; // Thinned playback? MFTIME hnsStart; // Start position }; QString CmdToString(const CCmd& cmd); void SetPending(CCmd& cmd); void SetState(CCmd& cmd); // Command pending means we have started but not yet completed an async operation. // This is different than whether there are commands on the queue bool IsCommandPending() const { return (m_pending.cmd != CmdNone); } QQueue<CCmd>mQueue; CCmd m_state; // Current nominal state. CCmd m_pending; // Pending State ZCritSect m_critsec; // Protects the seeking and rate-change states. DWORD m_caps; // Session caps. BOOL m_bCanScrub; // Does the current session support rate = 0. BOOL m_scrubbing; MFTIME m_hnsDuration; // Duration of the current presentation. float m_fPrevRate; IMFMediaSession *m_pSession; IMFRateControl *m_pRate; IMFRateSupport *m_pRateSupport; IMFPresentationClock *m_pClock; }; } // End Name Space Pasco Video #endif // WINDOWS
//------------------------------------------------------------------------------------------------- /// Microsoft Sample Code /// This is Microsoft Sample code related to media foundation seeking of video playback. /// See Windows Dev Center article "Seeking, Fast Forward, and Reverse Play in Media Foundation /// programming guide: /// https://msdn.microsoft.com/en-us/library/windows/desktop/ee892373?f=255&MSPPError=-2147217396 /// /// \file Backends\MediaFoundation\MFPlayerSeeking.cpp /// \author Microsoft /// \date 10/21/2016 /// \brief Microsoft Media Foundation - Video Playback Seeking, Fast Forward and Reverse Play //------------------------------------------------------------------------------------------------- #include "_VideoLibPre.h" // basically includes windows.h, debug heap, some common definitions #include "_Tracing.h" // #defines for debugging. #include "MFPlayerSeeking.h" #include "MFHelper.h" // for MFTime conversions #ifdef TRACE_VIDEO_EVENTS #include <QString> #include <QDebug> #endif #ifdef _WINDOWS #include <assert.h> #include <mfapi.h> #include <mfidl.h> #include <mferror.h> namespace Video { template <class T> void SafeRelease(T **ppT) { if (*ppT) { (*ppT)->Release(); *ppT = NULL; } } //------------------------------------------------------------------------------------------------- // Internal Methods //------------------------------------------------------------------------------------------------- /// \brief Given a topology, returns a pointer to the presentation descriptor. HRESULT GetPresentationDescriptorFromTopology( IMFTopology *pTopology, IMFPresentationDescriptor **ppPD ) { HRESULT hr = S_OK; IMFCollection *pCollection = NULL; IUnknown *pUnk = NULL; IMFTopologyNode *pNode = NULL; IMFPresentationDescriptor *pPD = NULL; // Get the collection of source nodes from the topology. hr = pTopology->GetSourceNodeCollection(&pCollection); if (FAILED(hr)) { goto done; } // Any of the source nodes should have the PD, so take the first // object in the collection. hr = pCollection->GetElement(0, &pUnk); if (FAILED(hr)) { goto done; } hr = pUnk->QueryInterface(IID_PPV_ARGS(&pNode)); if (FAILED(hr)) { goto done; } // Get the PD, which is stored as an attribute. hr = pNode->GetUnknown( MF_TOPONODE_PRESENTATION_DESCRIPTOR, IID_PPV_ARGS(&pPD)); if (FAILED(hr)) { goto done; } *ppPD = pPD; (*ppPD)->AddRef(); done: SafeRelease(&pCollection); SafeRelease(&pUnk); SafeRelease(&pNode); SafeRelease(&pPD); return hr; } //------------------------------------------------------------------------------------------------- // ctor //------------------------------------------------------------------------------------------------- MFPlayerSeeking::MFPlayerSeeking() : m_pClock(NULL), m_pSession(NULL), m_pRate(NULL), m_pRateSupport(NULL), m_state(CCmd(CmdNone)), m_pending(CCmd(CmdNone)), m_caps(0), m_bCanScrub(false), m_scrubbing(false), m_hnsDuration(0), m_fPrevRate(0.0) { Clear(); } //------------------------------------------------------------------------------------------------- // dtor //------------------------------------------------------------------------------------------------- MFPlayerSeeking::~MFPlayerSeeking() { SafeRelease(&m_pClock); SafeRelease(&m_pSession); SafeRelease(&m_pRate); SafeRelease(&m_pRateSupport); } //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::Clear() { m_caps = 0; m_bCanScrub = FALSE; m_hnsDuration = 0; m_fPrevRate = 1.0f; SafeRelease(&m_pClock); SafeRelease(&m_pSession); SafeRelease(&m_pRate); SafeRelease(&m_pRateSupport); // Note that set state and set pending only set the .cmd variable for CmdNone! SetState(CCmd(CmdNone)); m_state.fRate = 1.0; m_state.bThin = false; m_state.hnsStart = 0; SetPending(CCmd(CmdNone, 1.0, false, 0)); m_pending.fRate = 1.0; m_pending.bThin = false; m_pending.hnsStart = 0; while (!mQueue.isEmpty()) mQueue.dequeue(); return S_OK; } //------------------------------------------------------------------------------ // SetTopology // // Called when the full playback topology is ready. (MeSessonTopologyStatus ready) //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::SetTopology(IMFMediaSession *pSession, IMFTopology *pTopology) { HRESULT hr = S_OK; HRESULT hrTmp = S_OK; // For non-critical failures. IMFClock *pClock = NULL; IMFPresentationDescriptor *pPD = NULL; Clear(); // We have just received a new topology: transition from state 'none' to 'stopped' SetState(CCmd(CmdStop)); // Get the session capabilities. hr = pSession->GetSessionCapabilities(&m_caps); if (FAILED(hr)) { goto done; } // Get the presentation descriptor from the topology. hr = GetPresentationDescriptorFromTopology(pTopology, &pPD); if (FAILED(hr)) { goto done; } // Get the duration from the presentation descriptor (optional) (void)pPD->GetUINT64(MF_PD_DURATION, (UINT64*)&m_hnsDuration); // Get the presentation clock (optional) hrTmp = pSession->GetClock(&pClock); if (SUCCEEDED(hrTmp)) { hr = pClock->QueryInterface(IID_PPV_ARGS(&m_pClock)); if (FAILED(hr)) { goto done; } } // Get the rate control interface (optional) hrTmp = MFGetService( pSession, MF_RATE_CONTROL_SERVICE, IID_PPV_ARGS(&m_pRate)); // Get the rate support interface (optional) if (SUCCEEDED(hrTmp)) { hrTmp = MFGetService( pSession, MF_RATE_CONTROL_SERVICE, IID_PPV_ARGS(&m_pRateSupport)); } if (SUCCEEDED(hrTmp)) { // Check if rate 0 (scrubbing) is supported. hrTmp = m_pRateSupport->IsRateSupported(TRUE, 0, NULL); } if (SUCCEEDED(hrTmp)) { m_bCanScrub = TRUE; } // if m_pRate is NULL, m_bCanScrub must be FALSE. assert(m_pRate || !m_bCanScrub); // Cache a pointer to the session. m_pSession = pSession; m_pSession->AddRef(); // Seek to position 0 to display first frame of movie. //SetPosition(0); done: SafeRelease(&pPD); SafeRelease(&pClock); return hr; } //------------------------------------------------------------------------------ // SessionEvent // // Called when media session fires an event. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::SessionEvent( MediaEventType type, HRESULT hrStatus, IMFMediaEvent *pEvent ) { HRESULT hr = S_OK; bool CheckPend = false; switch (type) { case MESessionStarted: // if pending is command seek, we do nothing, // and wait for ScrubComplete if (m_pending.cmd == CmdStart) { hr = OnSessionStart(hrStatus); CheckPend = true; } break; case MESessionStopped: hr = OnSessionStop(hrStatus); CheckPend = true; break; case MESessionPaused: hr = OnSessionPause(hrStatus); CheckPend = true; break; case MESessionRateChanged: hr = OnSessionRateChanged(hrStatus, pEvent); CheckPend = true; break; case MESessionEnded: hr = OnSessionEnded(hrStatus); CheckPend = true; break; case MESessionCapabilitiesChanged: // The session capabilities changed. Get the updated capabilities. m_caps = MFGetAttributeUINT32(pEvent, MF_EVENT_SESSIONCAPS, m_caps); CheckPend = false; break; case MESessionScrubSampleComplete: // We have Started session and waited for scrub sample to complete // Now we wait for ProcessFrameEvent // if (m_pending.cmd == CmdSeek) // { // OnSessionStart(S_OK); // SetPending(CCmd(CmdNone)); // Scrub(false); qDebug() << DNTs("Scrub complete"); // CheckPend = true; // } break; } if (CheckPend) { UpdatePendingCommands(); } return S_OK; } //------------------------------------------------------------------------------ // Start // // Starts playback. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::Start() { HRESULT hr = S_OK; if (m_state.cmd == CmdStart) { UpdatePendingCommands(); return S_OK; } ZAutoLock lock(m_critsec); // If another operation is pending, cache the request. // Otherwise, start the media session. if (IsCommandPending()) { mQueue.enqueue(CCmd(CmdStart)); } else { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("Async: Start Media Session"); #endif PROPVARIANT varStart; PropVariantInit(&varStart); hr = m_pSession->Start(NULL, &varStart); SetPending(CCmd(CmdStart,RATE_INVALID, false, 0)); } return hr; } //------------------------------------------------------------------------------ // Pause // // Pauses playback. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::Pause() { HRESULT hr = S_OK; if (m_state.cmd == CmdPause) { UpdatePendingCommands(); return S_OK; } // HACK ALERT: We cannot transition from Stop to Pause, so we do this haaaacckkk! ACK else if (m_state.cmd == CmdStop) { Start(); mQueue.enqueue(CCmd(CmdPause)); } ZAutoLock lock(m_critsec); // If another operation is pending, cache the request. // Otherwise, pause the media session. if (IsCommandPending()) { mQueue.enqueue(CCmd(CmdPause)); } else { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("Async: Pause Media Session"); #endif hr = m_pSession->Pause(); SetPending(CCmd(CmdPause)); } return hr; } //------------------------------------------------------------------------------ // Stop // // Stops playback. // Note: The Player class delegates Stop calls to the PlayerSeeking class. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::Stop() { HRESULT hr = S_OK; if (m_state.cmd == CmdStop) { UpdatePendingCommands(); return S_OK; } if (m_pending.cmd == CmdStop) { return S_OK; } ZAutoLock lock(m_critsec); // If another operation is pending, cache the request. // Otherwise, stop the media session. if (IsCommandPending()) { mQueue.enqueue(CCmd(CmdStop)); } else { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("Async: Stop Media Session"); #endif hr = m_pSession->Stop(); SetPending(CCmd(CmdStop)); } return hr; } //------------------------------------------------------------------------------ // CanSeek // // Queries whether the current session supports seeking. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::CanSeek(BOOL *pbCanSeek) { if (pbCanSeek == NULL) { return E_POINTER; } // Note: The MFSESSIONCAP_SEEK flag is sufficient for seeking. However, to // implement a seek bar, an application also needs the duration (to get // the valid range) and a presentation clock (to get the current position). *pbCanSeek = ( ((m_caps & MFSESSIONCAP_SEEK) == MFSESSIONCAP_SEEK) && (m_hnsDuration > 0) && (m_pClock != NULL) ); return S_OK; } //------------------------------------------------------------------------------ // GetDuration // // Gets the duration of the current presentation. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::GetDuration(MFTIME *phnsDuration) { if (phnsDuration == NULL) { return E_POINTER; } *phnsDuration = m_hnsDuration; if (m_hnsDuration == 0) { return MF_E_NO_DURATION; } else { return S_OK; } } //------------------------------------------------------------------------------ // GetCurrentPosition // // Gets the current playback position. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::GetCurrentPosition(MFTIME *phnsPosition) { if (phnsPosition == NULL) { return E_POINTER; } HRESULT hr = S_OK; ZAutoLock lock(m_critsec); if (m_pClock == NULL) { return MF_E_NO_CLOCK; } // Return, in order: // 1. Queued seek position (nominal position). // 2. Pending seek operation (nominal position). // 3. Presentation time (actual position). if (mQueue.size() != 0 && mQueue[0].cmd == CmdSeek) { *phnsPosition = mQueue[0].hnsStart; } else if (IsCommandPending() && m_pending.cmd == CmdSeek) { *phnsPosition = m_pending.hnsStart; } else { hr = m_pClock->GetTime(phnsPosition); if (SUCCEEDED(hr)) m_state.hnsStart = *phnsPosition; } return hr; } //------------------------------------------------------------------------------ // SetPosition // // Sets the current playback position. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::SetPosition(MFTIME hnsPosition) { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("====================================================="); qDebug() << QString(DNTs("Set Position %1")).arg(MFHelper::MFTimeToReal(hnsPosition), 0, 'f', 3); #endif ZAutoLock lock(m_critsec); HRESULT hr = S_OK; SetPending(CCmd(CmdNone)); while (mQueue.size() != 0) { mQueue.dequeue(); } // Command currentState = m_state.cmd; BOOL resetState = (mQueue.isEmpty() && !IsCommandPending()); BOOL canScrub = false; hr = CanScrub(&canScrub); if (canScrub) { Scrub(true); } // Currently seeking or changing rates, so cache this request. if (IsCommandPending()) { if (mQueue.size() != 0 && mQueue[0].cmd == CmdSeek) mQueue[0] = CCmd(CmdSeek, RATE_INVALID, false, hnsPosition); else mQueue.enqueue(CCmd(CmdSeek, RATE_INVALID, false, hnsPosition)); } else { hr = SetPositionInternal(hnsPosition); } // if (resetState) // mQueue.enqueue(CCmd(currentState)); return hr; } //------------------------------------------------------------------------------ // CanScrub // // Queries whether the current session supports scrubbing. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::CanScrub(BOOL *pbCanScrub) { if (pbCanScrub == NULL) { return E_POINTER; } if (IsScrubbing() ) *pbCanScrub = false; else *pbCanScrub = m_bCanScrub; return S_OK; } //------------------------------------------------------------------------------ // IsScrubbbing // // Queries whether the current session is scrubbing //------------------------------------------------------------------------------ BOOL MFPlayerSeeking::IsScrubbing() { BOOL result = m_scrubbing; if (m_state.fRate == 0.0) result = true; return result; } // ------------------------------------------------------------------------------ // Scrub // // Enables or disables scrubbing. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::Scrub(BOOL bScrub) { // Scrubbing is implemented as rate = 0. ZAutoLock lock(m_critsec); if (!m_pRate) { return MF_E_INVALIDREQUEST; } if (!m_bCanScrub) { return MF_E_INVALIDREQUEST; } HRESULT hr = S_OK; float newRate = 0.0; if (bScrub) { // Enter scrubbing mode. Cache the current rate. if (GetNominalRate() != 0) { m_fPrevRate = m_state.fRate; } newRate = 0.0; hr = SetRate(newRate); } else { // Leaving scrubbing mode. Restore the old rate. if (GetNominalRate() == 0) { newRate = m_fPrevRate; hr = SetRate(m_fPrevRate); } } #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << QString(DNTs("Scrubbing %1, new rate is %2")).arg(bScrub ? DNTs("enabled") : DNTs("disabled")).arg(newRate, 0, 'f', 3); #endif m_scrubbing = bScrub; return hr; } //------------------------------------------------------------------------------ void MFPlayerSeeking::ProcessFrameEvent() { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS if (IsScrubbing()) { qDebug() << DNTs(" "); qDebug() << DNTs("Process Frame Event"); qDebug() << DNTs(" "); } #endif if (m_pending.cmd == CmdSeek) { OnSessionStart(S_OK); SetPending(CCmd(CmdNone)); // We just finished a pending command: we know scrub(false) will immediately begin rate change) // if (mQueue.empty()) { // Scrub(false); // Pause(); Stop(); // mQueue.enqueue(CCmd(CmdPause)); // } // UpdatePendingCommands(); } #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS if (IsScrubbing()) qDebug() << DNTs("======================================"); #endif } //------------------------------------------------------------------------------ // CanFastForward // // Queries whether the current session supports fast forward //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::CanFastForward(BOOL *pbCanFF) { if (pbCanFF == NULL) { return E_POINTER; } *pbCanFF = ((m_caps & MFSESSIONCAP_RATE_FORWARD) == MFSESSIONCAP_RATE_FORWARD); return S_OK; } //------------------------------------------------------------------------------ // CanRewind // // Queries whether the current session supports rewind (reverse play). //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::CanRewind(BOOL *pbCanRewind) { if (pbCanRewind == NULL) { return E_POINTER; } *pbCanRewind = ((m_caps & MFSESSIONCAP_RATE_REVERSE) == MFSESSIONCAP_RATE_REVERSE); return S_OK; } //------------------------------------------------------------------------------ // FastForward // // Switches to fast-forward playback, as follows: // - If the current rate is < 0 (reverse play), switch to 1x speed. // - Otherwise, double the current playback rate. // // Note: This method is just for convenience; the app could call SetRate(). //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::FastForward() { if (!m_pRate) { return MF_E_INVALIDREQUEST; } HRESULT hr = S_OK; float fTarget = GetNominalRate() * 2; if (fTarget <= 0.0f) { fTarget = 1.0f; } hr = SetRate(fTarget); return hr; } //------------------------------------------------------------------------------ // Rewind // // Switches to reverse playback, as follows: // - If the current rate is > 0 (forward playback), switch to -1x speed. // - Otherwise, double the current (reverse) playback rate. // // Note: This method is just for convenience; the app could call SetRate(). //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::Rewind() { if (!m_pRate) { return MF_E_INVALIDREQUEST; } HRESULT hr = S_OK; float fTarget = GetNominalRate() * 2; if (fTarget >= 0.0f) { fTarget = -1.0f; } hr = SetRate(fTarget); return hr; } //------------------------------------------------------------------------------ // SetRate // // Sets the playback rate. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::SetRate(float fRate) { HRESULT hr = S_OK; BOOL bThin = FALSE; ZAutoLock lock(m_critsec); if (fRate == GetNominalRate()) { UpdatePendingCommands(); return S_OK; // no-op } if (m_pRateSupport == NULL) { return MF_E_INVALIDREQUEST; } // Check if this rate is supported. Try non-thinned playback first, // then fall back to thinned playback. hr = m_pRateSupport->IsRateSupported(FALSE, fRate, NULL); if (FAILED(hr)) { bThin = TRUE; hr = m_pRateSupport->IsRateSupported(TRUE, fRate, NULL); } if (FAILED(hr)) { // Unsupported rate. return hr; } // If there is an operation pending, cache the request. if (IsCommandPending()) { mQueue.enqueue(CCmd(CmdRate, fRate, bThin, TIME_INVALID)); // Remember the current transport state (play, paused, etc), so that we // can restore it after the rate change, if necessary. However, if // another command is already pending, that one takes precedent. if (m_state.cmd == CmdPause || m_state.cmd == CmdStart || m_state.cmd == CmdStop) { mQueue.enqueue(CCmd(m_state.cmd)); } } else { // No pending operation. Commit the new rate. hr = CommitRateChange(fRate, bThin); } return hr; } /// Protected methods //------------------------------------------------------------------------------ // SetPositionInternal // // Sets the playback position. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::SetPositionInternal(const MFTIME &hnsPosition) { assert(m_pending.cmd == CmdNone); // Caller holds the lock. #ifdef TRACE_VIDEO if (m_state.hnsStart == hnsPosition) { qDebug() << "Duplicate SetPosition? "; } #endif if (m_pSession == NULL) { return MF_E_INVALIDREQUEST; } #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << QString(DNTs("Async: Start Media Session moving to position %1, rate is:%2")).arg(hnsPosition).arg(m_state.fRate, 0, 'f', 3); #endif HRESULT hr = S_OK; PROPVARIANT varStart; varStart.vt = VT_I8; varStart.hVal.QuadPart = hnsPosition; hr = m_pSession->Start(NULL, &varStart); if (SUCCEEDED(hr)) { SetPending(CCmd(CmdSeek, RATE_INVALID, false, hnsPosition)); } return hr; } //------------------------------------------------------------------------------ // CommitRateChange // // Sets the playback rate. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::CommitRateChange(float fRate, BOOL bThin) { assert(m_pending.cmd == CmdNone); // Caller holds the lock. HRESULT hr = S_OK; MFTIME hnsSystemTime = 0; MFTIME hnsClockTime = 0; Command currentState = m_state.cmd; IMFClock *pClock = NULL; // Allowed rate transitions: // // Positive <-> negative: Stopped // Negative <-> zero: Stopped // Postive <-> zero: Paused or stopped // Positive or Zero to Negative or Vice-Versa if ((fRate > 0 && m_state.fRate <= 0) || (fRate < 0 && m_state.fRate >= 0)) { // Playing: Transition to Stop. if (currentState == CmdStart) { // Get the current clock position. This will be the restart time. hr = m_pSession->GetClock(&pClock); if (FAILED(hr)) goto done; (void)pClock->GetCorrelatedTime(0, &hnsClockTime, &hnsSystemTime); assert(hnsSystemTime != 0); #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("Async: Stop, Commit Rate change"); // Was Stop, then Seek"); #endif !IsCommandPending() ? Stop() : mQueue.enqueue(CmdStop); mQueue.enqueue(CCmd(CmdRate, fRate, false, TIME_INVALID)); // TODO: Handle Rate change while playing... // mQueue.enqueue(CCmd(CmdPlay, RATE_INVALID, false, hnsClockTime)); goto done; } else if (currentState == CmdPause) { // The current state is paused. // For this rate change, the session must be stopped. However, the // session cannot transition back from stopped to paused. // Therefore, this rate transition is not supported while paused. //qDebug() << DNTs("Error: Commit Rate change: fail while paused!!!"); //hr = MF_E_UNSUPPORTED_STATE_TRANSITION; //// For this rate change, the session must be stopped. !IsCommandPending() ? Start() : mQueue.enqueue(CCmd(CmdStart)); mQueue.enqueue(CCmd(CmdPause)); mQueue.enqueue(CCmd(CmdRate, fRate, false, TIME_INVALID)); // TODO: Handle Rate change while playing... // mQueue.enqueue(CCmd(currentState)); goto done; } } // Else transition Non-Zero rate to Zero else if (fRate == 0 && m_state.fRate != 0) { // This transition requires the paused state. if (currentState == CmdStart) { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("Async: Rate change: Playing: Pause, rate change"); #endif // Pause and set the rate. if (! IsCommandPending()) Pause(); else mQueue.enqueue(CmdPause); mQueue.enqueue(CCmd(CmdRate, fRate, false, TIME_INVALID)); // TODO: Handle Rate change while playing... // mQueue.enqueue(CCmd(currentState)); goto done; } } // Set the rate. #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << QString(DNTs("Async: Commit Rate change: Set Rate %1")).arg(fRate, 0, 'f', 3); #endif hr = m_pRate->SetRate(bThin, fRate); if (FAILED(hr)) { goto done; } // Adjust our requested rate. SetPending(CCmd(CmdRate, fRate, false, 0)); done: SafeRelease(&pClock); return hr; } //------------------------------------------------------------------------------ // GetNominalRate // // Returns the nominal playback rate. //------------------------------------------------------------------------------ float MFPlayerSeeking::GetNominalRate() { return m_state.fRate; } //------------------------------------------------------------------------------ // OnSessionStart // // Called when playback starts or restarts. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::OnSessionStart(HRESULT hrStatus) { HRESULT hr = S_OK; if (FAILED(hrStatus)) { SetPending(CCmd(CmdNone)); return hrStatus; } // We can start as a result of a seek command, where we moved to the pending start // position, or as a result of a "play" (Start) command, where there is no position // info: we keep our current position in that case. CCmd startedCmd = CCmd(CmdStart); if (m_pending.cmd == CmdSeek && m_pending.hnsStart != 0) { startedCmd.hnsStart = m_pending.hnsStart; // Leave pending command intact until ScrubCompleted event } else { // Restarted from current startedCmd.hnsStart = m_state.hnsStart; SetPending(CCmd(CmdNone)); } SetState(startedCmd); return hr; } //------------------------------------------------------------------------------ // OnSessionStop // // Called when playback stops. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::OnSessionStop(HRESULT hrStatus) { SetPending(CCmd(CmdNone)); if (FAILED(hrStatus)) { return hrStatus; } SetState(CCmd(CmdStop)); // The Media Session completed a transition to stopped. This might occur // because we are changing playback direction (forward/rewind). Check if // there is a pending rate-change request. return S_OK; } //------------------------------------------------------------------------------ // OnSessionPause // // Called when playback pauses. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::OnSessionPause(HRESULT hrStatus) { SetPending(CCmd(CmdNone)); if (FAILED(hrStatus)) { return hrStatus; } SetState(CCmd(CmdPause)); return S_OK; } //------------------------------------------------------------------------------ // OnSessionEnded // // Called when the session ends. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::OnSessionEnded(HRESULT hr) { SetState(CCmd(CmdStop)); SetPending(CCmd(CmdNone)); // After the session ends, playback starts from position zero. But if the // current playback rate is reversed, playback would end immediately // (reversing from position 0). Therefore, reset the rate to 1x. if (GetNominalRate() < 0.0f) { hr = CommitRateChange(1.0f, FALSE); } return hr; } //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::OnSessionRateChanged(HRESULT hr, IMFMediaEvent* pEvent) { // Don't clear pending status at beginning because we need to get the new rate out of it. // If the rate change succeeded, we've already got the rate cached in m_pending // If it failed, try to get the actual rate. // we will update the rate of our current state. CCmd updatedState = m_state; updatedState.cmd = CmdRate; if (FAILED(hr)) { PROPVARIANT var; PropVariantInit(&var); hr = pEvent->GetValue(&var); if (SUCCEEDED(hr) && (var.vt == VT_R4)) { updatedState.fRate = var.fltVal; } } else { updatedState.fRate = m_pending.fRate; } #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << QString(DNTs("Player Seeking rate change %1, new rate is %2")) .arg("succeeded") // .arg(SUCCEEDED(hr) ? DNTs("succeeded") : DNTs("failed")) .arg(updatedState.fRate, 0, 'f', 3); #endif SetState(updatedState); SetPending(CCmd(CmdNone)); return hr; } //--------------------------------------------- // Provides a central place to save pending commands, allowing us to do logging. QString MFPlayerSeeking::CmdToString(const CCmd& cmd) { QString cmdStr; switch (cmd.cmd) { case CmdNone: cmdStr = DNTs("None"); break; case CmdStop: cmdStr = DNTs("Stop"); break; case CmdStart: cmdStr = QString(DNTs("Start, fRate=%1, pos=%2")).arg(cmd.fRate,0, 'f', 3).arg(((qreal)cmd.hnsStart/10000000.0), 0, 'f', 3); break; case CmdPause: cmdStr = DNTs("Pause"); break; case CmdSeek: cmdStr = QString(DNTs("Seek, pos=%1")).arg(((qreal)cmd.hnsStart / 10000000.0), 0, 'f', 3); break; case CmdRate: cmdStr = QString(DNTs("Rate, fRate=%1")).arg(cmd.fRate, 0, 'f', 3); break; } return cmdStr; } //--------------------------------------------- void MFPlayerSeeking::SetPending(CCmd& cmd) { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs(" Pend:") << CmdToString(cmd); #endif m_pending = cmd; } //--------------------------------------------- void MFPlayerSeeking::SetState(CCmd& cmd) { switch (cmd.cmd) { case CmdNone: m_state.cmd = cmd.cmd; break; case CmdStop: m_state.cmd = cmd.cmd; break; case CmdStart: { m_state.cmd = cmd.cmd; if (cmd.hnsStart != 0) // Start from 0 means restart from current position m_state.hnsStart = cmd.hnsStart; // so only update if starting from non-zero } break; case CmdPause: m_state.cmd = cmd.cmd; break; case CmdSeek: m_state.cmd = cmd.cmd; m_state.hnsStart = cmd.hnsStart; break; case CmdRate: m_state.fRate = cmd.fRate; } #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs(" State:") << CmdToString(cmd); #endif } //------------------------------------------------------------------------------ // UpdatePendingCommands // // Called after an operation completes. // This method executes any cached requests. //------------------------------------------------------------------------------ HRESULT MFPlayerSeeking::UpdatePendingCommands() { HRESULT hr = S_OK; if (mQueue.isEmpty()) { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("UpdatePendingCommands: queue empty"); #endif return S_OK; } if (IsCommandPending()) { #ifdef TRACE_VIDEO_PLAYER_TIMESTAMPS qDebug() << DNTs("UpdatePendingCommands: already Pending"); #endif return S_OK; } PROPVARIANT varStart; PropVariantInit(&varStart); ZAutoLock lock(m_critsec); CCmd cmd = mQueue.dequeue(); #ifdef TRACE_VIDEO_EVENTS qDebug() << QString(DNTs("UpdatePendingCommands (cmd = %1 / rate = %2, thin=%3, pos=%4), count=%5")).arg(CmdToString(cmd.cmd)).arg(cmd.fRate, 0,'f',3).arg(cmd.bThin).arg(cmd.hnsStart).arg(mQueue.size()); #endif // The current pending command has completed. switch (cmd.cmd) { case CmdNone: // Nothing to do. break; case CmdStart: Start(); break; case CmdPause: Pause(); break; case CmdStop: Stop(); break; case CmdSeek: if (cmd.fRate != m_state.fRate) { ZAutoLock lock(m_critsec); SetPositionInternal(cmd.hnsStart); } break; case CmdRate: if (cmd.fRate != m_state.fRate) { ZAutoLock lock(m_critsec); CommitRateChange(cmd.fRate, cmd.bThin); } break; } return hr; } } // End Name space Pasco Video #endif // Windows