//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // The use and distribution terms for this software are covered by the // Microsoft Limited Public License (Ms-LPL) // which can be found in the file MS-LPL.txt at the root of this distribution. // By using this software in any fashion, you are agreeing to be bound by // the terms of this license. // // The software is licensed “as-is.” // // You must not remove this notice, or any other, from this software. // // // // Demux code for Windows CE // //------------------------------------------------------------------------- //====================================================================== // Demux code for Windows CE //====================================================================== /*++ Module Name: clock.h Abstract: This module contains the IReferenceClock declarations, as well as all the push-clock declarations. Revision History: 23-Jan-2000 created 17-Apr-2000 CMonotonicClock added; refined the PCR clock subordinating algorithm to be adaptive Notes: skews WaitFor_ milliseconds only; does not skew requested advise notification times because it is assumed that they are already scaled i.e. a media sample has been received by a renderer with a PTS-derived timestamp on it (therefore subordinated to the PCR clock already), and the rendere wants to be notified when the PTS time is. ------------------------------------------------------------------ PCR clock subordinating ----------------------- The demux exposes a clock that is subordinated to a PCR stream. The PCR is a sampled value from the headend clock which is also used to generate the PES PTS. Since the demux produces dshow presentation timestamps which are non-morphed but scaled values of the PES PTS values, keeping a graph clock in sync with the headend clock ensures that buffers will never underflow/overflow. The demux subordinates to the PCRs by sampling the QPC counter every time a PCR is received. Over time, a delta which is relatively free of on-host jitter such as batched media sample deliveries, interrupts between buffer reception and buffer processing, etc..., is built up for each clock. The slope PCR(delta) / QPC(delta) would be 1.0 if they are identical. Most likely, they are not, in which case the slope provides a direct scaling value to skew the QPC clock values to the PCR clock. To return a time, we keep track of the following tuples: 1. QPC when we were last called; QPC(last) 2. QPC_derived time we last returned; QPC(last)_derived we then do as follows - 1. sample the QPC clock to get QPC(now) 2. QPC(delta) = QPC(now) - QPC(last) 3. QPC(now)_derived = QPC(last)_derived + PCR_QPCUsed * QPC(delta) ** PCR_QPCUsed obtained as described in the algorithm below PCR subordinating algorithm: ---------------------------- Receive (PCR) { QPC(now) = QueryPerformanceCounter () QPC(total) += QPC(now) PCR(total) += PCR PCR_QPCSlopeNew = PCR(total) / QPC(total) if (PCR_QPCSlopeNew > PCR_QPCUsed + AllowableError) { // above allowable bounds; // ADJUST up Diff = PCR_QPCSlopeNew - PCR_QPCUsed SlopeStep = MIN (Diff, MaxSlopeStepValue) PCR_QPCUsed += SlopeStep ; // allowable error is initialized to a smaller and smaller // value as we converge AllowableError = MIN (MaxAllowableErrorBracket, Diff) } else if (PCR_QPCSlopeNew < PCR_QPCUsed - AllowableError) { // below allowable bounds; // ADJUST down Diff = PCR_QPCUsed - PCR_QPCSlopeNew SlopeStep = MIN (Diff, MaxSlopeStepValue) PCR_QPCUsed -= SlopeStep // allowable error is initialized to a smaller and smaller // value as we converge AllowableError = MIN (MaxAllowableErrorBracket, Diff) } else { // within allowable bounds // DEGRADE AllowableError AllowableError -= ErrorBracketDegradation } } Initial Values: --------------- Initial values are as follows: // we immediately start correcting AllowableError = 0 ; // macro defined in mp2const.h MaxSlopeStepValue = MAX_SLOPE_STEP_VALUE () ; // expect perfect PCR_QPCUsed = 1 ; // macro defined in mp2const.h ErrorBracketDegradation = ERROR_BRACKET_DEGRADATION () ; // macro defined in mp2const.h MaxAllowableErrorBracket = MAX_ALLOWABLE_ERROR_BRACKET () ; ** see mp2const.h for more details Improvement ideas: ------------------ 1. Set an averaging window vs. averaging over the lifetime of the demux. The current averaging window may well be too big and not flex sufficiently to effectively subordinate to headend jitter after a long time as passed. Instead, a 2-3 minute subordination window could be used, with slope values older than the window getting dropped from the average. --*/ // forward declarations class CMPEG2Demultiplexer ; class CMPEG2PushClock ; class CMonotonicClock ; // ============================================================================ // ============================================================================ template class CTIGetTime { public : virtual T SampleClock () = 0 ; // can wrap virtual T GetFreq () = 0 ; // in ticks/sec; cannot change per run } ; // ============================================================================ // ============================================================================ template < class HostClock, // host clock; ratio subordinates this clock to the master clock class MasterClock // master clock; we'll have a ratio that is host-subordinated to this > class CTClockSubordinate { enum { ALLOC_QUANTUM = 30 } ; struct CLOCK_SUBORDINATE_BRACKET { DWORD TicksStart ; MasterClock Start_Master ; HostClock Start_Host ; } ; CTDynQueue m_BracketQueue ; ObjectPool m_BracketPool ; CTIGetTime * m_pICTGetTime ; MasterClock m_MasterFreq ; HostClock m_HostFreq ; double m_dClockSubordinateRatio ; DWORD m_dwMinSamplingWinTicks ; DWORD m_dwMaxHistoryMillis ; double m_dMinSubordinatable ; double m_dMaxSubordinatable ; CMpeg2Stats * m_pStats ; CCritSec m_cs ; CLOCK_SUBORDINATE_BRACKET m_Last ; void TrimQueue_ ( IN DWORD dwTicksNow ) ; void ComputeNewScalingVal_ ( ) ; DWORD QueueNewBracket_ ( IN MasterClock MasterTime, IN HostClock HostTime, IN DWORD dwTicksNow ) ; double ObservedBracketScalingVal_ ( IN CLOCK_SUBORDINATE_BRACKET * pBracket ) ; double ObservedTailScalingVal_ ( ) ; double ObservedHeadScalingVal_ ( ) ; public : CTClockSubordinate ( IN CTIGetTime * pICTGetTime, IN MasterClock MasterClockFreq, // ticks per second IN DWORD dwMinSamplingWinTicks, // we measure clock tuples of min delta of this IN DWORD dwMaxHistoryMillis, // we maintain a history up to this max; default = 10 minutes IN DWORD MinSubordinatable, // min % we consider "Subordinatable" IN DWORD MaxSubordinatable, // max % we consider "Subordinatable" IN CMpeg2Stats * pStats ) ; ~CTClockSubordinate ( ) ; void OnMasterTime ( IN MasterClock MasterTime ) ; void Reset ( IN BOOL fSubordinate = TRUE ) ; BOOL IsWithinBounds (IN double dRatio) { return (dRatio >= m_dMinSubordinatable && dRatio <= m_dMaxSubordinatable) ; } double ObservedScalingVal () { return ObservedHeadScalingVal_ () ; } double ScalingVal () { return m_dClockSubordinateRatio ; } } ; // ============================================================================ // ============================================================================ class CMonotonicClock /*++ given an incoming clock tick, guarantees that that clock tick will be normalized to monotonically increase from 0 (first), etc...; incoming ticks can increase or decrease, tick to tick, as long as they don't exceed the specified threshold m_dwMaxNonSpanningMillis however protects against sucking out legitimate spans. For example, successive calls to this object might be far apart. We'll detect a discontinuity and smooth the clock. However, if the time since the last packet was processed is non "instantaneous" smoothing the clock isn't a discontinuity as much as it is a spanned carry. The timestamps should reflect the spanned time. We'll still smooth in that case but also notify that the call spanned & exceeded a specified spanning threshold. Typically the demultiplexer will be reset in that case so timestamps can be reinitialized correctly. --*/ { LONGLONG m_llPosDiscontinuityThreshold ; LONGLONG m_llNegDiscontinuityThreshold ; LONGLONG m_llNormalizingTicks ; LONGLONG m_llLastTicksNormalized ; LONGLONG m_llLastGoodPosDelta ; LONGLONG m_llLastGoodNegDelta ; LONGLONG m_llInitialJitterPadding ; DWORD m_dwSysClockTicksLast ; DWORD m_dwSysClockMaxNonSpanningMillis ; CMpeg2Stats * m_pStats ; public : CMonotonicClock ( IN LONGLONG llPosDiscontinuityThreshold, // packet - packet threshold IN LONGLONG llNegDiscontinuityThreshold, // packet - packet threshold IN LONGLONG llInitialJitterPadding, // gives first few packets a bit of // breathing room in case there's a // a negative PTS delta early on IN DWORD dwMaxSysClockNonSpanningMillis, // spanning time threshold IN CMpeg2Stats * pStats // stats ) ; virtual ~CMonotonicClock ( ) ; // guaranteed to return a monotonically increasing value; even // if it means artificially setting the return to 0 LONGLONG Normalize ( IN LONGLONG llTicks, // clock value sample OUT BOOL * pfDiscontinuity, // TRUE/FALSE if a time discontinuity was detected OUT BOOL * pfSpanningTimeExceeded // TRUE/FALSE if max timespanning was not exceeded ) ; void Reset ( ) ; } ; class CStreamMonotonicClock : public CMonotonicClock { DWORD m_dwStreamIdentifier ; LONGLONG m_llTimelineOffset ; // offset between this stream's // timestamps the master timeline // (PCRs) public : LIST_ENTRY m_ListEntry ; CStreamMonotonicClock ( IN LONGLONG llPosDiscontinuityThreshold, // packet - packet threshold IN LONGLONG llNegDiscontinuityThreshold, // packet - packet threshold IN LONGLONG llInitialJitterPadding, // gives first few packets a bit of // breathing room in case there's a // a negative PTS delta early on IN DWORD dwMaxSysClockNonSpanningMillis, // spanning time threshold IN CMpeg2Stats * pStats, // stats IN DWORD dwStreamIdentifier ) : CMonotonicClock ( llPosDiscontinuityThreshold, llNegDiscontinuityThreshold, llInitialJitterPadding, dwMaxSysClockNonSpanningMillis, pStats ), m_dwStreamIdentifier (dwStreamIdentifier), m_llTimelineOffset (UNDEFINED) {} DWORD GetStreamIdentifier () { return m_dwStreamIdentifier ; } LONGLONG GetStreamTimelineOffset () { return m_llTimelineOffset ; } void SetStreamTimelineOffset (IN LONGLONG ll) { m_llTimelineOffset = ll ; } void Reset ( ) { m_llTimelineOffset = UNDEFINED ; CMonotonicClock::Reset () ; } } ; class CMPEG2PushClock : public IReferenceClock, // reference clock interface public CIPCRValueNotify, // PCR value notifications public CTIGetTime , // get the time public CIPTSConvert // PTS -> REFERENCE_TIME conversion { // smoothing window sizes, etc... enum { // allowable PTS to scaledSCR discrepency; we allow for 2 seconds // worth of PTS to SCR diff SCR_PTS_ALLOWABLE_DISPARITY = PTS_HZ * 2, } ; // events enum { EVENT_STOP, // thread stop EVENT_NEW_ADVISE, // new advise received EVENT_COUNT // always last } ; struct ADVISE_NODE { SINGLE_LIST_ENTRY SListEntry ; // link HANDLE hSignal ; // semaphore or event REFERENCE_TIME rtAdviseTime ; // when to advise REFERENCE_TIME rtPeriodTime ; // what the period is; 0 in non-periodic advise nodes } ; CTClockSubordinate m_ClockSubordinate ; // clock subordinate object REFERENCE_TIME m_rtGraphStart ; // graph start time; ::GetTime() returns 0 if earlier than this LIST_ENTRY m_MonotonicClockListHead ; // list head for the PTS clock ticks SINGLE_LIST_ENTRY * m_pAdviseListHead ; // list head to the advise list nodes SINGLE_LIST_ENTRY * m_pAdviseNodeFreePool ; // list head to free pool of list nodes CMpeg2Stats * m_pStats ; // stats CMPEG2Demultiplexer * m_pMPEG2Demux ; // our controlling unknown (always the filter) HANDLE m_hThread ; // adviser thread HANDLE m_rhEvents [EVENT_COUNT] ; // array of events to signal the thread CRITICAL_SECTION m_crtIRefConfig ; // IReferenceClock configuration lock CRITICAL_SECTION m_crtGetTime ; // IReferenceClock::GetTime () lock BOOL m_fActive ; // TRUE/FALSE depending whether the hosting filter // will implement IReferenceClock; LONGLONG m_qpcCurrentBufferTime ; // qpc time that the last byte of the current // input buffer was received; the current input buffer // contains the multiplexed transport packets LONGLONG m_QPCTicksPerSecond ; // set at initialization to scale the qpc-raw numbers LONGLONG m_ullTotalContInputBufferBytes ; // total input buffer bytes without discontinuities LONGLONG m_llLastReturnedIRefTime ; // value for the last time we returned LONGLONG m_llBaseQPCForLastReturnedTime ; // QPC time that formed the basis for the last returned time double m_dCarriedError ; // fractional error that gets rounded out for each call, but which // must be carried from call-call UINT m_uiTimerResolution ; // for timeBeginPeriod and timeEndPeriod calls IReferenceClock * m_pIReferenceClock ; // graph reference clock; may/may not be us LONGLONG m_llQPCNormalizer ; // sampled when object is instantiated #define NORMALIZED_QPC(qpc) ((qpc) + m_llQPCNormalizer) LONGLONG m_llBasePTS ; // timeline base PTS; morphed PCR value (90khz) LONGLONG m_llLastPCR ; // last-processed PCR (90khz) REFERENCE_TIME m_rtStartToFirstChannelPCR ; // time from graph start to first PCR for current stream CIAVStreamNotify * m_pIAVStreamNotify ; CStreamMonotonicClock * m_pPCRMonotonicClock ; // object ensures that clock ticks always increase monotononically double m_dPlaybackRate ; // playback rate; if we must, we can generate timestamps that // reflect the playback rate; playback rate can only be set when // we are playing back from file i.e. are operating in pull mode; // furthermore, this should be done in conjunction with resetting // the PCR stream, before any of the new timestamps are generated REFERENCE_TIME m_rtMinReturnedTime ; // use this value to make sure our clock never runs backwards; // must watch for this at starup when the time is padded by FG REFERENCE_TIME m_rtMinDownstreamBuffering ; // min downstream buffering; this is our threshold REFERENCE_TIME m_rtBaselinePTSPadding ; // amount of padding on PTSs to ensure they are padded to the // min downtream buffering; set when the timeline is first constructed REFERENCE_TIME m_rtOverPad ; // we overpad when we detect a downstream starvation CTSmoothingFilter m_RefClockShift ; // we dynamically detect if we're underflowing downstream and shift // the refclock values back; we've observed shifts on some PCR values // that don't change the rate of advance except across the one discontinuity // for which we can only account by shifting with them DWORD m_dwStartTicks ; BOOL m_fSettling ; DWORD m_cSettlingTicks ; void LockIRefConfig_ () { EnterCriticalSection (& m_crtIRefConfig) ; } void UnlockIRefConfig_ () { LeaveCriticalSection (& m_crtIRefConfig) ; } void LockGetTime_ () { EnterCriticalSection (& m_crtGetTime) ; } void UnlockGetTime_ () { LeaveCriticalSection (& m_crtGetTime) ; } void ResetClockSubordinatingCounters_ ( ) { m_ullTotalContInputBufferBytes = 0 ; // reset continuous byte count m_qpcCurrentBufferTime = UNDEFINED ; // current buffer time is undefined m_ClockSubordinate.Reset () ; } void ResetMonotonicClocks_ ( ) ; HRESULT ConfirmAdviseThreadRunning_ ( ) { DWORD dw ; TIMECAPS tc ; MMRESULT mmRes ; if (m_hThread == NULL) { mmRes = timeGetDevCaps (& tc, sizeof tc) ; if (mmRes == TIMERR_NOERROR) { m_uiTimerResolution = tc.wPeriodMin ; } else { m_uiTimerResolution = 1 ; } timeBeginPeriod (m_uiTimerResolution) ; m_hThread = CreateThread ( NULL, // security 0, // calling thread's stack size CMPEG2PushClock::ThreadEntry, // entry point (LPVOID) this, // parameter NULL, // flags & dw // id; required on win9x ) ; if (m_hThread == NULL) { // failure dw = GetLastError () ; return HRESULT_FROM_WIN32 (dw) ; } SetThreadPriority (m_hThread, THREAD_PRIORITY_TIME_CRITICAL) ; } ASSERT (m_rhEvents [EVENT_STOP] != NULL) ; // thread is running return S_OK ; } // returns the milliseconds until the next timeout DWORD ProcessNotificationTimeoutLocked_ ( IN REFERENCE_TIME rtNow ) ; // returns the milliseconds until the next timeout DWORD ResetWaitTimeLocked_ ( IN REFERENCE_TIME rtNow ) ; HRESULT AdvisePeriodicLocked_ ( IN REFERENCE_TIME rtStartTime, IN REFERENCE_TIME rtPeriodTime, IN HSEMAPHORE hSemaphore, OUT DWORD_PTR * pdwpContext ) ; HRESULT AdviseTimeLocked_ ( IN REFERENCE_TIME rtBaseTime, IN REFERENCE_TIME rtStreamTime, IN HEVENT hEvent, OUT DWORD_PTR * pdwpContext ) ; void QueueAdviseTimeout_ ( IN ADVISE_NODE * pAdviseNode ) ; HRESULT CancelAdviseTimeout_ ( IN ADVISE_NODE * pAdviseNode ) ; ADVISE_NODE * GetAdviseNode_ ( ) // locks held: the list lock { ADVISE_NODE * pAdviseNode ; SINGLE_LIST_ENTRY * pSListEntry ; if (m_pAdviseNodeFreePool != NULL) { pSListEntry = m_pAdviseNodeFreePool ; m_pAdviseNodeFreePool = m_pAdviseNodeFreePool -> Next ; pAdviseNode = CONTAINING_RECORD (pSListEntry, ADVISE_NODE, SListEntry) ; ZeroMemory (pAdviseNode, sizeof ADVISE_NODE) ; } else { pAdviseNode = new ADVISE_NODE ; if (pAdviseNode) { ZeroMemory (pAdviseNode, sizeof ADVISE_NODE) ; } } return pAdviseNode ; } void RecycleAdviseNode_ ( IN ADVISE_NODE * pAdviseNode ) // locks held: the list lock { ASSERT (pAdviseNode) ; pAdviseNode -> SListEntry.Next = m_pAdviseNodeFreePool ; m_pAdviseNodeFreePool = & pAdviseNode -> SListEntry ; } // scales the wait milliseconds to the PCR clock so we wait an // amount that is synced with the headend clock; might increase // or decrease the number of milliseconds we actually wait (based // on the host's clock) DWORD SkewedWaitMilliseconds_ ( IN DWORD dwMilliseconds ) { return (DWORD) (m_ClockSubordinate.ScalingVal () * (double) dwMilliseconds) ; } public : CMPEG2PushClock ( IN BOOL fActive, // hosting filter may not wish to implement IReferenceClock IN HKEY hkey, IN CMpeg2Stats * pStats, IN CMPEG2Demultiplexer * pMPEG2Demux, // always the filter IN DWORD dwMinSamplingWinTicks, IN DWORD dwMaxHistoryMillis, IN DWORD MinSubordinatable, IN DWORD MaxSubordinatable, IN DWORD MaxGlitchesPerHour, OUT HRESULT * pHr ) ; ~CMPEG2PushClock ( ) ; // ------------------------------------------------------------------- // clock sampling interface; used by the clock subordinating module virtual LONGLONG SampleClock ( ) ; virtual LONGLONG GetFreq ( ) ; // ------------------------------------------------------------------- void LogTransportStreamDiscontinuity ( ) { ResetClockSubordinatingCounters_ () ; } void SetGraphClock ( IN IReferenceClock * pIRefClock ) { // only release if we have a reference clock and it's not us RELEASE_AND_CLEAR (m_pIReferenceClock) ; if (pIRefClock != NULL) { m_pIReferenceClock = pIRefClock ; m_pIReferenceClock -> AddRef () ; } } void SetIAVStreamNotify (IN CIAVStreamNotify * pIAVStreamNotify) { m_pIAVStreamNotify = pIAVStreamNotify ; } // ------------------------------------------------------------------- // called by the content manager to set and unset a PCR value // object (receives the PCR values) // called by the content manager to notify the clock that a new PCR // source will be providing PCRs and that is must reset its historical // QPC - PCR data. HRESULT ResetPCRSource ( ) ; void SetPlaybackRate (IN double d) { ASSERT (d != 0.0) ; m_dPlaybackRate = d ; } double GetPlaybackRate () { return m_dPlaybackRate ; } // ------------------------------------------------------------------- // IPTSConvert implementation virtual HRESULT GetNewPTSContext ( IN DWORD dwStreamIdentifier, OUT DWORD_PTR * pdwContext ) ; virtual void RecyclePTSContext ( IN DWORD_PTR dwContext ) ; virtual BOOL FirstPCRProcessed ( IN DWORD_PTR dwContext ) ; virtual void MediaSampleTimesFromPTS ( IN DWORD_PTR dwContext, // PTS context IN DWORD stream_id, // PES stream_id IN LONGLONG llPES_PTS, // PES value IN CTStickyVal * pReset, OUT REFERENCE_TIME * pStartTime, // return start time OUT REFERENCE_TIME * pStopTime, // return stop time OUT BOOL * pfPaddingAdjust ) ; // ------------------------------------------------------------------- // IPCRValueNotify implementation virtual HRESULT NewPCRValue ( IN LONGLONG llPCR, // PCR value IN BOOL fDiscontinuity, // TRUE if this PCR follows a discontinuity IN int iLastPCRByteOffset, // input buffer offset IN CTStickyVal * pReset ) ; // ------------------------------------------------------------------- // called by the controller whenever a new buffer is received void LogInputBufferQPCTime ( IN LONGLONG qpcInputBufferTime, IN LONGLONG qpcLocalTime, IN int iInputBufferLengthBytes ) ; // ------------------------------------------------------------------- // called by the filter when there's a graph state transition void FilterStateChanged ( IN FILTER_STATE NewFilterState, IN REFERENCE_TIME rtStart // 0 if not start run ) ; // ------------------------------------------------------------------- // Advise thread entry point and worker method void AdviseThreadBody ( ) ; static DWORD WINAPI ThreadEntry ( IN LPVOID pv ) { (reinterpret_cast (pv)) -> AdviseThreadBody () ; return EXIT_SUCCESS ; } // ------------------------------------------------------------------- // IReferenceClock methods, including IUnknown STDMETHODIMP QueryInterface ( IN REFIID riid, OUT void ** ppv ) ; STDMETHODIMP_(ULONG) AddRef ( ) ; STDMETHODIMP_(ULONG) Release ( ) ; STDMETHODIMP AdvisePeriodic ( IN REFERENCE_TIME rtStartTime, IN REFERENCE_TIME rtPeriodTime, IN HSEMAPHORE hSemaphore, OUT DWORD_PTR * pdwpContext ) ; STDMETHODIMP AdviseTime ( IN REFERENCE_TIME rtBaseTime, IN REFERENCE_TIME rtStreamTime, IN HEVENT hEvent, OUT DWORD_PTR * pdwpContext ) ; STDMETHODIMP GetTime ( OUT REFERENCE_TIME * pTime ) ; STDMETHODIMP Unadvise ( IN DWORD_PTR dwpContext ) ; } ;