diff --git a/dom/media/MediaTimer.cpp b/dom/media/MediaTimer.cpp new file mode 100644 index 000000000000..fa8452c68c72 --- /dev/null +++ b/dom/media/MediaTimer.cpp @@ -0,0 +1,165 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MediaTimer.h" + +#include + +#include "nsComponentManagerUtils.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +NS_IMPL_ADDREF(MediaTimer) +NS_IMPL_RELEASE_WITH_DESTROY(MediaTimer, DispatchDestroy()) + +MediaTimer::MediaTimer() + : mMonitor("MediaTimer Monitor") + , mTimer(do_CreateInstance("@mozilla.org/timer;1")) + , mUpdateScheduled(false) +{ + // Use the SharedThreadPool to create an nsIThreadPool with a maximum of one + // thread, which is equivalent to an nsIThread for our purposes. + RefPtr threadPool( + SharedThreadPool::Get(NS_LITERAL_CSTRING("MediaTimer"), 1)); + mThread = threadPool.get(); + mTimer->SetTarget(mThread); +} + +void +MediaTimer::DispatchDestroy() +{ + nsCOMPtr task = NS_NewNonOwningRunnableMethod(this, &MediaTimer::Destroy); + nsresult rv = mThread->Dispatch(task, NS_DISPATCH_NORMAL); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + (void) rv; +} + +void +MediaTimer::Destroy() +{ + MOZ_ASSERT(OnMediaTimerThread()); + + // Reject any outstanding entries. There's no need to acquire the monitor + // here, because we're on the timer thread and all other references to us + // must be gone. + while (!mEntries.empty()) { + mEntries.top().mPromise->Reject(false, __func__); + mEntries.pop(); + } + + // Cancel the timer if necessary. + CancelTimerIfArmed(); + + delete this; +} + +bool +MediaTimer::OnMediaTimerThread() +{ + bool rv = false; + mThread->IsOnCurrentThread(&rv); + return rv; +} + +nsRefPtr +MediaTimer::WaitUntil(const TimeStamp& aTimeStamp, const char* aCallSite) +{ + MonitorAutoLock mon(mMonitor); + Entry e(aTimeStamp, aCallSite); + nsRefPtr p = e.mPromise.get(); + mEntries.push(e); + ScheduleUpdate(); + return p; +} + +void +MediaTimer::ScheduleUpdate() +{ + mMonitor.AssertCurrentThreadOwns(); + if (mUpdateScheduled) { + return; + } + mUpdateScheduled = true; + + nsCOMPtr task = NS_NewRunnableMethod(this, &MediaTimer::Update); + nsresult rv = mThread->Dispatch(task, NS_DISPATCH_NORMAL); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + (void) rv; +} + +void +MediaTimer::Update() +{ + MonitorAutoLock mon(mMonitor); + UpdateLocked(); +} + +void +MediaTimer::UpdateLocked() +{ + MOZ_ASSERT(OnMediaTimerThread()); + mMonitor.AssertCurrentThreadOwns(); + mUpdateScheduled = false; + + // Resolve all the promises whose time is up. + TimeStamp now = TimeStamp::Now(); + while (!mEntries.empty() && mEntries.top().mTimeStamp < now) { + mEntries.top().mPromise->Resolve(true, __func__); + mEntries.pop(); + } + + // If we've got no more entries, cancel any pending timer and bail out. + if (mEntries.empty()) { + CancelTimerIfArmed(); + return; + } + + // We've got more entries - (re)arm the timer for the soonest one. + if (!TimerIsArmed() || mEntries.top().mTimeStamp < mCurrentTimerTarget) { + CancelTimerIfArmed(); + ArmTimer(mEntries.top().mTimeStamp, now); + } +} + +/* + * We use a callback function, rather than a callback method, to ensure that + * the nsITimer does not artifically keep the refcount of the MediaTimer above + * zero. When the MediaTimer is destroyed, it safely cancels the nsITimer so that + * we never fire against a dangling closure. + */ + +/* static */ void +MediaTimer::TimerCallback(nsITimer* aTimer, void* aClosure) +{ + static_cast(aClosure)->TimerFired(); +} + +void +MediaTimer::TimerFired() +{ + MonitorAutoLock mon(mMonitor); + MOZ_ASSERT(OnMediaTimerThread()); + mCurrentTimerTarget = TimeStamp(); + UpdateLocked(); +} + +void +MediaTimer::ArmTimer(const TimeStamp& aTarget, const TimeStamp& aNow) +{ + MOZ_DIAGNOSTIC_ASSERT(!TimerIsArmed()); + MOZ_DIAGNOSTIC_ASSERT(aTarget > aNow); + + // XPCOM timer resolution is in milliseconds. It's important to never resolve + // a timer when mTarget might compare < now (even if very close), so round up. + unsigned long delay = std::ceil((aTarget - aNow).ToMilliseconds()); + mCurrentTimerTarget = aTarget; + nsresult rv = mTimer->InitWithFuncCallback(&TimerCallback, this, delay, nsITimer::TYPE_ONE_SHOT); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + (void) rv; +} + +} // namespace mozilla diff --git a/dom/media/MediaTimer.h b/dom/media/MediaTimer.h new file mode 100644 index 000000000000..7acda7635a09 --- /dev/null +++ b/dom/media/MediaTimer.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#if !defined(MediaTimer_h_) +#define MediaTimer_h_ + +#include "MediaPromise.h" + +#include + +#include "nsITimer.h" +#include "nsRefPtr.h" + +#include "mozilla/Monitor.h" +#include "mozilla/TimeStamp.h" + +namespace mozilla { + +// This promise type is only exclusive because so far there isn't a reason for +// it not to be. Feel free to change that. +typedef MediaPromise MediaTimerPromise; + +// Timers only know how to fire at a given thread, which creates an impedence +// mismatch with code that operates with MediaTaskQueues. This class solves +// that mismatch with a dedicated (but shared) thread and a nice MediaPromise-y +// interface. +class MediaTimer +{ +public: + MediaTimer(); + + // We use a release with a custom Destroy(). + NS_IMETHOD_(MozExternalRefCountType) AddRef(void); + NS_IMETHOD_(MozExternalRefCountType) Release(void); + + nsRefPtr WaitUntil(const TimeStamp& aTimeStamp, const char* aCallSite); + +private: + virtual ~MediaTimer() { MOZ_ASSERT(OnMediaTimerThread()); } + + void DispatchDestroy(); // Invoked by Release on an arbitrary thread. + void Destroy(); // Runs on the timer thread. + + bool OnMediaTimerThread(); + void ScheduleUpdate(); + void Update(); + void UpdateLocked(); + + static void TimerCallback(nsITimer* aTimer, void* aClosure); + void TimerFired(); + void ArmTimer(const TimeStamp& aTarget, const TimeStamp& aNow); + + bool TimerIsArmed() + { + return !mCurrentTimerTarget.IsNull(); + } + + void CancelTimerIfArmed() + { + MOZ_ASSERT(OnMediaTimerThread()); + if (TimerIsArmed()) { + mTimer->Cancel(); + mCurrentTimerTarget = TimeStamp(); + } + } + + + struct Entry + { + TimeStamp mTimeStamp; + nsRefPtr mPromise; + + explicit Entry(const TimeStamp& aTimeStamp, const char* aCallSite) + : mTimeStamp(aTimeStamp) + , mPromise(new MediaTimerPromise::Private(aCallSite)) + {} + + bool operator<(const Entry& aOther) const + { + return mTimeStamp < aOther.mTimeStamp; + } + }; + + ThreadSafeAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + nsCOMPtr mThread; + std::priority_queue mEntries; + Monitor mMonitor; + nsCOMPtr mTimer; + TimeStamp mCurrentTimerTarget; + bool mUpdateScheduled; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/moz.build b/dom/media/moz.build index ab4b3a05d712..a686d44c4925 100644 --- a/dom/media/moz.build +++ b/dom/media/moz.build @@ -192,6 +192,7 @@ UNIFIED_SOURCES += [ 'MediaStreamGraph.cpp', 'MediaStreamTrack.cpp', 'MediaTaskQueue.cpp', + 'MediaTimer.cpp', 'MediaTrack.cpp', 'MediaTrackList.cpp', 'MP3FrameParser.cpp',