/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "SourceBuffer.h" #include "AsyncEventRunner.h" #include "MediaSourceUtils.h" #include "TrackBuffer.h" #include "mozilla/ErrorResult.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Preferences.h" #include "mozilla/dom/MediaSourceBinding.h" #include "mozilla/dom/TimeRanges.h" #include "nsError.h" #include "nsIEventTarget.h" #include "nsIRunnable.h" #include "nsThreadUtils.h" #include "prlog.h" struct JSContext; class JSObject; #ifdef PR_LOGGING extern PRLogModuleInfo* GetMediaSourceLog(); extern PRLogModuleInfo* GetMediaSourceAPILog(); #define MSE_DEBUG(...) PR_LOG(GetMediaSourceLog(), PR_LOG_DEBUG, (__VA_ARGS__)) #define MSE_DEBUGV(...) PR_LOG(GetMediaSourceLog(), PR_LOG_DEBUG+1, (__VA_ARGS__)) #define MSE_API(...) PR_LOG(GetMediaSourceAPILog(), PR_LOG_DEBUG, (__VA_ARGS__)) #else #define MSE_DEBUG(...) #define MSE_DEBUGV(...) #define MSE_API(...) #endif namespace mozilla { namespace dom { void SourceBuffer::SetMode(SourceBufferAppendMode aMode, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::SetMode(aMode=%d)", this, aMode); if (!IsAttached() || mUpdating) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (aMode == SourceBufferAppendMode::Sequence) { aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return; } MOZ_ASSERT(mMediaSource->ReadyState() != MediaSourceReadyState::Closed); if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) { mMediaSource->SetReadyState(MediaSourceReadyState::Open); } // TODO: Test append state. // TODO: If aMode is "sequence", set sequence start time. mAppendMode = aMode; } void SourceBuffer::SetTimestampOffset(double aTimestampOffset, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::SetTimestampOffset(aTimestampOffset=%f)", this, aTimestampOffset); if (!IsAttached() || mUpdating) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } MOZ_ASSERT(mMediaSource->ReadyState() != MediaSourceReadyState::Closed); if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) { mMediaSource->SetReadyState(MediaSourceReadyState::Open); } // TODO: Test append state. // TODO: If aMode is "sequence", set sequence start time. mTimestampOffset = aTimestampOffset; } already_AddRefed SourceBuffer::GetBuffered(ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); if (!IsAttached()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } nsRefPtr ranges = new TimeRanges(); double highestEndTime = mTrackBuffer->Buffered(ranges); if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) { // Set the end time on the last range to highestEndTime by adding a // new range spanning the current end time to highestEndTime, which // Normalize() will then merge with the old last range. ranges->Add(ranges->GetEndTime(), highestEndTime); ranges->Normalize(); } MSE_DEBUGV("SourceBuffer(%p)::GetBuffered ranges=%s", this, DumpTimeRanges(ranges).get()); return ranges.forget(); } void SourceBuffer::SetAppendWindowStart(double aAppendWindowStart, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::SetAppendWindowStart(aAppendWindowStart=%f)", this, aAppendWindowStart); if (!IsAttached() || mUpdating) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (aAppendWindowStart < 0 || aAppendWindowStart >= mAppendWindowEnd) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } mAppendWindowStart = aAppendWindowStart; } void SourceBuffer::SetAppendWindowEnd(double aAppendWindowEnd, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::SetAppendWindowEnd(aAppendWindowEnd=%f)", this, aAppendWindowEnd); if (!IsAttached() || mUpdating) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (IsNaN(aAppendWindowEnd) || aAppendWindowEnd <= mAppendWindowStart) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } mAppendWindowEnd = aAppendWindowEnd; } void SourceBuffer::AppendBuffer(const ArrayBuffer& aData, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::AppendBuffer(ArrayBuffer)", this); aData.ComputeLengthAndData(); AppendData(aData.Data(), aData.Length(), aRv); } void SourceBuffer::AppendBuffer(const ArrayBufferView& aData, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::AppendBuffer(ArrayBufferView)", this); aData.ComputeLengthAndData(); AppendData(aData.Data(), aData.Length(), aRv); } void SourceBuffer::Abort(ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::Abort()", this); if (!IsAttached()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (mMediaSource->ReadyState() != MediaSourceReadyState::Open) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (mUpdating) { // TODO: Abort segment parser loop, buffer append, and stream append loop algorithms. AbortUpdating(); } // TODO: Run reset parser algorithm. mAppendWindowStart = 0; mAppendWindowEnd = PositiveInfinity(); MSE_DEBUG("SourceBuffer(%p)::Abort() Discarding decoder", this); mTrackBuffer->DiscardDecoder(); } void SourceBuffer::Remove(double aStart, double aEnd, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p)::Remove(aStart=%f, aEnd=%f)", this, aStart, aEnd); if (!IsAttached()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (IsNaN(mMediaSource->Duration()) || aStart < 0 || aStart > mMediaSource->Duration() || aEnd <= aStart || IsNaN(aEnd)) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } if (mUpdating) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) { mMediaSource->SetReadyState(MediaSourceReadyState::Open); } RangeRemoval(aStart, aEnd); } void SourceBuffer::RangeRemoval(double aStart, double aEnd) { StartUpdating(); /// TODO: Run coded frame removal algorithm. // Run the final step of the coded frame removal algorithm asynchronously // to ensure the SourceBuffer's updating flag transition behaves as // required by the spec. nsCOMPtr event = NS_NewRunnableMethod(this, &SourceBuffer::StopUpdating); NS_DispatchToMainThread(event); } void SourceBuffer::Detach() { MOZ_ASSERT(NS_IsMainThread()); MSE_DEBUG("SourceBuffer(%p)::Detach", this); if (mTrackBuffer) { mTrackBuffer->Detach(); } mTrackBuffer = nullptr; mMediaSource = nullptr; } void SourceBuffer::Ended() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(IsAttached()); MSE_DEBUG("SourceBuffer(%p)::Ended", this); mTrackBuffer->DiscardDecoder(); } SourceBuffer::SourceBuffer(MediaSource* aMediaSource, const nsACString& aType) : DOMEventTargetHelper(aMediaSource->GetParentObject()) , mMediaSource(aMediaSource) , mAppendWindowStart(0) , mAppendWindowEnd(PositiveInfinity()) , mTimestampOffset(0) , mAppendMode(SourceBufferAppendMode::Segments) , mUpdating(false) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aMediaSource); mEvictionThreshold = Preferences::GetUint("media.mediasource.eviction_threshold", 75 * (1 << 20)); mTrackBuffer = new TrackBuffer(aMediaSource->GetDecoder(), aType); MSE_DEBUG("SourceBuffer(%p)::SourceBuffer: Create mTrackBuffer=%p", this, mTrackBuffer.get()); } SourceBuffer::~SourceBuffer() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!mMediaSource); MSE_DEBUG("SourceBuffer(%p)::~SourceBuffer", this); } MediaSource* SourceBuffer::GetParentObject() const { return mMediaSource; } JSObject* SourceBuffer::WrapObject(JSContext* aCx) { return SourceBufferBinding::Wrap(aCx, this); } void SourceBuffer::DispatchSimpleEvent(const char* aName) { MOZ_ASSERT(NS_IsMainThread()); MSE_API("SourceBuffer(%p) Dispatch event '%s'", this, aName); DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName)); } void SourceBuffer::QueueAsyncSimpleEvent(const char* aName) { MSE_DEBUG("SourceBuffer(%p) Queuing event '%s'", this, aName); nsCOMPtr event = new AsyncEventRunner(this, aName); NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); } void SourceBuffer::StartUpdating() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!mUpdating); mUpdating = true; QueueAsyncSimpleEvent("updatestart"); } void SourceBuffer::StopUpdating() { MOZ_ASSERT(NS_IsMainThread()); if (!mUpdating) { // The buffer append algorithm has been interrupted by abort(). // // If the sequence appendBuffer(), abort(), appendBuffer() occurs before // the first StopUpdating() runnable runs, then a second StopUpdating() // runnable will be scheduled, but still only one (the first) will queue // events. return; } mUpdating = false; QueueAsyncSimpleEvent("update"); QueueAsyncSimpleEvent("updateend"); } void SourceBuffer::AbortUpdating() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mUpdating); mUpdating = false; QueueAsyncSimpleEvent("abort"); QueueAsyncSimpleEvent("updateend"); } void SourceBuffer::CheckEndTime() { // Check if we need to update mMediaSource duration double endTime = GetBufferedEnd(); if (endTime > mMediaSource->Duration()) { mMediaSource->SetDuration(endTime); } } void SourceBuffer::AppendData(const uint8_t* aData, uint32_t aLength, ErrorResult& aRv) { MSE_DEBUG("SourceBuffer(%p)::AppendData(aLength=%u)", this, aLength); if (!PrepareAppend(aRv)) { return; } StartUpdating(); MOZ_ASSERT(mAppendMode == SourceBufferAppendMode::Segments, "We don't handle timestampOffset for sequence mode yet"); if (aLength) { if (!mTrackBuffer->AppendData(aData, aLength, mTimestampOffset * USECS_PER_S)) { Optional decodeError(MediaSourceEndOfStreamError::Decode); ErrorResult dummy; mMediaSource->EndOfStream(decodeError, dummy); aRv.Throw(NS_ERROR_FAILURE); return; } if (mTrackBuffer->HasInitSegment()) { mMediaSource->QueueInitializationEvent(); } CheckEndTime(); } // Run the final step of the buffer append algorithm asynchronously to // ensure the SourceBuffer's updating flag transition behaves as required // by the spec. nsCOMPtr event = NS_NewRunnableMethod(this, &SourceBuffer::StopUpdating); NS_DispatchToMainThread(event); } bool SourceBuffer::PrepareAppend(ErrorResult& aRv) { if (!IsAttached() || mUpdating) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return false; } if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) { mMediaSource->SetReadyState(MediaSourceReadyState::Open); } // Eviction uses a byte threshold. If the buffer is greater than the // number of bytes then data is evicted. The time range for this // eviction is reported back to the media source. It will then // evict data before that range across all SourceBuffers it knows // about. // TODO: Make the eviction threshold smaller for audio-only streams. // TODO: Drive evictions off memory pressure notifications. // TODO: Consider a global eviction threshold rather than per TrackBuffer. double newBufferStartTime = 0.0; bool evicted = mTrackBuffer->EvictData(mMediaSource->GetDecoder()->GetCurrentTime(), mEvictionThreshold, &newBufferStartTime); if (evicted) { MSE_DEBUG("SourceBuffer(%p)::AppendData Evict; current buffered start=%f", this, GetBufferedStart()); // We notify that we've evicted from the time range 0 through to // the current start point. mMediaSource->NotifyEvicted(0.0, newBufferStartTime); } // TODO: Test buffer full flag. return true; } double SourceBuffer::GetBufferedStart() { MOZ_ASSERT(NS_IsMainThread()); ErrorResult dummy; nsRefPtr ranges = GetBuffered(dummy); return ranges->Length() > 0 ? ranges->GetStartTime() : 0; } double SourceBuffer::GetBufferedEnd() { MOZ_ASSERT(NS_IsMainThread()); ErrorResult dummy; nsRefPtr ranges = GetBuffered(dummy); return ranges->Length() > 0 ? ranges->GetEndTime() : 0; } void SourceBuffer::Evict(double aStart, double aEnd) { MOZ_ASSERT(NS_IsMainThread()); MSE_DEBUG("SourceBuffer(%p)::Evict(aStart=%f, aEnd=%f)", this, aStart, aEnd); double currentTime = mMediaSource->GetDecoder()->GetCurrentTime(); double evictTime = aEnd; const double safety_threshold = 5; if (currentTime + safety_threshold >= evictTime) { evictTime -= safety_threshold; } mTrackBuffer->EvictBefore(evictTime); } #if defined(DEBUG) void SourceBuffer::Dump(const char* aPath) { if (mTrackBuffer) { mTrackBuffer->Dump(aPath); } } #endif NS_IMPL_CYCLE_COLLECTION_CLASS(SourceBuffer) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SourceBuffer) // Tell the TrackBuffer to end its current SourceBufferResource. TrackBuffer* track = tmp->mTrackBuffer; if (track) { track->Detach(); } NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource) NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SourceBuffer, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_ADDREF_INHERITED(SourceBuffer, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(SourceBuffer, DOMEventTargetHelper) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(SourceBuffer) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) } // namespace dom } // namespace mozilla