gecko-dev/dom/media/mediasource/SourceBuffer.cpp

591 lines
16 KiB
C++
Raw Normal View History

/* -*- 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 "MediaData.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"
#include <time.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 {
class AppendDataRunnable : public nsRunnable {
public:
AppendDataRunnable(SourceBuffer* aSourceBuffer,
LargeDataBuffer* aData,
double aTimestampOffset)
: mSourceBuffer(aSourceBuffer)
, mData(aData)
, mTimestampOffset(aTimestampOffset)
{
}
NS_IMETHOD Run() MOZ_OVERRIDE MOZ_FINAL {
mSourceBuffer->AppendData(mData, mTimestampOffset);
return NS_OK;
}
private:
nsRefPtr<SourceBuffer> mSourceBuffer;
nsRefPtr<LargeDataBuffer> mData;
double mTimestampOffset;
};
class RangeRemovalRunnable : public nsRunnable {
public:
RangeRemovalRunnable(SourceBuffer* aSourceBuffer,
double aStart,
double aEnd)
: mSourceBuffer(aSourceBuffer)
, mStart(aStart)
, mEnd(aEnd)
{ }
NS_IMETHOD Run() MOZ_OVERRIDE MOZ_FINAL {
mSourceBuffer->DoRangeRemoval(mStart, mEnd);
return NS_OK;
}
private:
nsRefPtr<SourceBuffer> mSourceBuffer;
double mStart;
double mEnd;
};
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<TimeRanges>
SourceBuffer::GetBuffered(ErrorResult& aRv)
{
MOZ_ASSERT(NS_IsMainThread());
if (!IsAttached()) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return nullptr;
}
nsRefPtr<TimeRanges> 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;
}
Abort();
mTrackBuffer->ResetParserState();
mAppendWindowStart = 0;
mAppendWindowEnd = PositiveInfinity<double>();
MSE_DEBUG("SourceBuffer(%p)::Abort() Discarding decoder", this);
mTrackBuffer->DiscardDecoder();
}
void
SourceBuffer::Abort()
{
if (mUpdating) {
// TODO: Abort segment parser loop, buffer append, and stream append loop algorithms.
AbortUpdating();
}
}
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();
nsRefPtr<nsIRunnable> task = new RangeRemovalRunnable(this, aStart, aEnd);
NS_DispatchToMainThread(task);
}
void
SourceBuffer::DoRangeRemoval(double aStart, double aEnd)
{
if (!mUpdating) {
// abort was called in between.
return;
}
if (mTrackBuffer && !IsInfinite(aStart)) {
int64_t start = aStart * USECS_PER_S;
int64_t end = IsInfinite(aEnd) ? INT64_MAX : (int64_t)(aEnd * USECS_PER_S);
mTrackBuffer->RangeRemoval(start, end);
}
StopUpdating();
}
void
SourceBuffer::Detach()
{
MOZ_ASSERT(NS_IsMainThread());
MSE_DEBUG("SourceBuffer(%p)::Detach", this);
Abort();
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->EndCurrentDecoder();
}
SourceBuffer::SourceBuffer(MediaSource* aMediaSource, const nsACString& aType)
: DOMEventTargetHelper(aMediaSource->GetParentObject())
, mMediaSource(aMediaSource)
, mAppendWindowStart(0)
, mAppendWindowEnd(PositiveInfinity<double>())
, 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<nsIRunnable> event = new AsyncEventRunner<SourceBuffer>(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, MSRangeRemovalAction::SKIP);
}
}
void
SourceBuffer::AppendData(const uint8_t* aData, uint32_t aLength, ErrorResult& aRv)
{
MSE_DEBUG("SourceBuffer(%p)::AppendData(aLength=%u)", this, aLength);
nsRefPtr<LargeDataBuffer> data = PrepareAppend(aData, aLength, aRv);
if (!data) {
return;
}
StartUpdating();
MOZ_ASSERT(mAppendMode == SourceBufferAppendMode::Segments,
"We don't handle timestampOffset for sequence mode yet");
nsRefPtr<nsIRunnable> task =
new AppendDataRunnable(this, data, mTimestampOffset);
NS_DispatchToMainThread(task);
}
void
SourceBuffer::AppendData(LargeDataBuffer* aData, double aTimestampOffset)
{
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;
}
MOZ_ASSERT(mMediaSource);
if (aData->Length()) {
if (!mTrackBuffer->AppendData(aData, aTimestampOffset * USECS_PER_S)) {
AppendError(true);
return;
}
if (mTrackBuffer->HasInitSegment()) {
mMediaSource->QueueInitializationEvent();
}
CheckEndTime();
}
StopUpdating();
}
void
SourceBuffer::AppendError(bool aDecoderError)
{
MOZ_ASSERT(NS_IsMainThread());
if (!mUpdating) {
// The buffer append algorithm has been interrupted by abort().
return;
}
mTrackBuffer->ResetParserState();
mUpdating = false;
QueueAsyncSimpleEvent("error");
QueueAsyncSimpleEvent("updateend");
if (aDecoderError) {
Optional<MediaSourceEndOfStreamError> decodeError(
MediaSourceEndOfStreamError::Decode);
ErrorResult dummy;
mMediaSource->EndOfStream(decodeError, dummy);
}
}
already_AddRefed<LargeDataBuffer>
SourceBuffer::PrepareAppend(const uint8_t* aData, uint32_t aLength, ErrorResult& aRv)
{
if (!IsAttached() || mUpdating) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return nullptr;
}
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.
Bug 1055904 - Improve MSE eviction calculation - r=jya Fixes a bug in the SourceBufferResource eviction code where it was using the mOffset of the resource as the min bound for what to evict. This offset is almost always zero though due to ReadFromCache being used which never updates the offset. This prevented eviction from happening in most cases. Moves the code to remove old decoders so that it does this during the same loop as that which remove data from existing decoders. This more aggressively prunes old decoders and is more likely to keep data in the current playing decoder around for seeking, etc. Prevent removing any decoder that the MediaSourceReader is currently using for playback to prevent RemoveDecoder crashes. Add a threshold to subtract from the current time when working out the time bound to evict before to make it less likely to evict current data that is needed for current playback. Remove all data from evicted decoders in the initial iteration then iterate after to remove empty decoders to put the RemoveDecoder logic in one place. Iterate decoders in order that they were added rather than sorted by time so the logic that removes entire decoders can do it only to those old decoders that existed before the existing one was created. Keeps track of the time that was evicted from the current decoder and uses that as the time to EvictBefore for all decoders in the track buffer when doing MediaSource::NotifyEvict. --HG-- extra : rebase_source : f7b4fe263a8041b3882585caea389742b2a1a9b3
2015-01-16 03:14:56 +00:00
double newBufferStartTime = 0.0;
// Attempt to evict the amount of data we are about to add by lowering the
// threshold.
uint32_t toEvict =
(mEvictionThreshold > aLength) ? mEvictionThreshold - aLength : aLength;
Bug 1055904 - Improve MSE eviction calculation - r=jya Fixes a bug in the SourceBufferResource eviction code where it was using the mOffset of the resource as the min bound for what to evict. This offset is almost always zero though due to ReadFromCache being used which never updates the offset. This prevented eviction from happening in most cases. Moves the code to remove old decoders so that it does this during the same loop as that which remove data from existing decoders. This more aggressively prunes old decoders and is more likely to keep data in the current playing decoder around for seeking, etc. Prevent removing any decoder that the MediaSourceReader is currently using for playback to prevent RemoveDecoder crashes. Add a threshold to subtract from the current time when working out the time bound to evict before to make it less likely to evict current data that is needed for current playback. Remove all data from evicted decoders in the initial iteration then iterate after to remove empty decoders to put the RemoveDecoder logic in one place. Iterate decoders in order that they were added rather than sorted by time so the logic that removes entire decoders can do it only to those old decoders that existed before the existing one was created. Keeps track of the time that was evicted from the current decoder and uses that as the time to EvictBefore for all decoders in the track buffer when doing MediaSource::NotifyEvict. --HG-- extra : rebase_source : f7b4fe263a8041b3882585caea389742b2a1a9b3
2015-01-16 03:14:56 +00:00
bool evicted =
mTrackBuffer->EvictData(mMediaSource->GetDecoder()->GetCurrentTime(),
toEvict, &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.
Bug 1055904 - Improve MSE eviction calculation - r=jya Fixes a bug in the SourceBufferResource eviction code where it was using the mOffset of the resource as the min bound for what to evict. This offset is almost always zero though due to ReadFromCache being used which never updates the offset. This prevented eviction from happening in most cases. Moves the code to remove old decoders so that it does this during the same loop as that which remove data from existing decoders. This more aggressively prunes old decoders and is more likely to keep data in the current playing decoder around for seeking, etc. Prevent removing any decoder that the MediaSourceReader is currently using for playback to prevent RemoveDecoder crashes. Add a threshold to subtract from the current time when working out the time bound to evict before to make it less likely to evict current data that is needed for current playback. Remove all data from evicted decoders in the initial iteration then iterate after to remove empty decoders to put the RemoveDecoder logic in one place. Iterate decoders in order that they were added rather than sorted by time so the logic that removes entire decoders can do it only to those old decoders that existed before the existing one was created. Keeps track of the time that was evicted from the current decoder and uses that as the time to EvictBefore for all decoders in the track buffer when doing MediaSource::NotifyEvict. --HG-- extra : rebase_source : f7b4fe263a8041b3882585caea389742b2a1a9b3
2015-01-16 03:14:56 +00:00
mMediaSource->NotifyEvicted(0.0, newBufferStartTime);
}
nsRefPtr<LargeDataBuffer> data = new LargeDataBuffer();
if (!data->AppendElements(aData, aLength)) {
aRv.Throw(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR);
return nullptr;
}
// TODO: Test buffer full flag.
return data.forget();
}
double
SourceBuffer::GetBufferedStart()
{
MOZ_ASSERT(NS_IsMainThread());
ErrorResult dummy;
nsRefPtr<TimeRanges> ranges = GetBuffered(dummy);
return ranges->Length() > 0 ? ranges->GetStartTime() : 0;
}
double
SourceBuffer::GetBufferedEnd()
{
MOZ_ASSERT(NS_IsMainThread());
ErrorResult dummy;
nsRefPtr<TimeRanges> 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