michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=8 sts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "MediaSource.h" michael@0: michael@0: #include "AsyncEventRunner.h" michael@0: #include "DecoderTraits.h" michael@0: #include "SourceBuffer.h" michael@0: #include "SourceBufferList.h" michael@0: #include "mozilla/ErrorResult.h" michael@0: #include "mozilla/FloatingPoint.h" michael@0: #include "mozilla/Preferences.h" michael@0: #include "mozilla/dom/BindingDeclarations.h" michael@0: #include "mozilla/dom/HTMLMediaElement.h" michael@0: #include "mozilla/mozalloc.h" michael@0: #include "nsContentTypeParser.h" michael@0: #include "nsDebug.h" michael@0: #include "nsError.h" michael@0: #include "nsIEventTarget.h" michael@0: #include "nsIRunnable.h" michael@0: #include "nsPIDOMWindow.h" michael@0: #include "nsStringGlue.h" michael@0: #include "nsThreadUtils.h" michael@0: #include "prlog.h" michael@0: michael@0: struct JSContext; michael@0: class JSObject; michael@0: michael@0: #ifdef PR_LOGGING michael@0: PRLogModuleInfo* gMediaSourceLog; michael@0: #define MSE_DEBUG(...) PR_LOG(gMediaSourceLog, PR_LOG_DEBUG, (__VA_ARGS__)) michael@0: #else michael@0: #define MSE_DEBUG(...) michael@0: #endif michael@0: michael@0: // Arbitrary limit. michael@0: static const unsigned int MAX_SOURCE_BUFFERS = 16; michael@0: michael@0: namespace mozilla { michael@0: michael@0: static const char* const gMediaSourceTypes[6] = { michael@0: "video/webm", michael@0: "audio/webm", michael@0: // XXX: Disabled other codecs temporarily to allow WebM testing. For now, set michael@0: // the developer-only media.mediasource.ignore_codecs pref to true to test other michael@0: // codecs, and expect things to be broken. michael@0: #if 0 michael@0: "video/mp4", michael@0: "audio/mp4", michael@0: "audio/mpeg", michael@0: #endif michael@0: nullptr michael@0: }; michael@0: michael@0: static nsresult michael@0: IsTypeSupported(const nsAString& aType) michael@0: { michael@0: if (aType.IsEmpty()) { michael@0: return NS_ERROR_DOM_INVALID_ACCESS_ERR; michael@0: } michael@0: // TODO: Further restrict this to formats in the spec. michael@0: nsContentTypeParser parser(aType); michael@0: nsAutoString mimeType; michael@0: nsresult rv = parser.GetType(mimeType); michael@0: if (NS_FAILED(rv)) { michael@0: return NS_ERROR_DOM_NOT_SUPPORTED_ERR; michael@0: } michael@0: bool found = false; michael@0: for (uint32_t i = 0; gMediaSourceTypes[i]; ++i) { michael@0: if (mimeType.EqualsASCII(gMediaSourceTypes[i])) { michael@0: found = true; michael@0: break; michael@0: } michael@0: } michael@0: if (!found) { michael@0: return NS_ERROR_DOM_NOT_SUPPORTED_ERR; michael@0: } michael@0: if (Preferences::GetBool("media.mediasource.ignore_codecs", false)) { michael@0: return NS_OK; michael@0: } michael@0: // Check aType against HTMLMediaElement list of MIME types. Since we've michael@0: // already restricted the container format, this acts as a specific check michael@0: // of any specified "codecs" parameter of aType. michael@0: if (dom::HTMLMediaElement::GetCanPlay(aType) == CANPLAY_NO) { michael@0: return NS_ERROR_DOM_NOT_SUPPORTED_ERR; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: namespace dom { michael@0: michael@0: /* static */ already_AddRefed michael@0: MediaSource::Constructor(const GlobalObject& aGlobal, michael@0: ErrorResult& aRv) michael@0: { michael@0: nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!window) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: nsRefPtr mediaSource = new MediaSource(window); michael@0: return mediaSource.forget(); michael@0: } michael@0: michael@0: SourceBufferList* michael@0: MediaSource::SourceBuffers() michael@0: { michael@0: MOZ_ASSERT_IF(mReadyState == MediaSourceReadyState::Closed, mSourceBuffers->IsEmpty()); michael@0: return mSourceBuffers; michael@0: } michael@0: michael@0: SourceBufferList* michael@0: MediaSource::ActiveSourceBuffers() michael@0: { michael@0: MOZ_ASSERT_IF(mReadyState == MediaSourceReadyState::Closed, mActiveSourceBuffers->IsEmpty()); michael@0: return mActiveSourceBuffers; michael@0: } michael@0: michael@0: MediaSourceReadyState michael@0: MediaSource::ReadyState() michael@0: { michael@0: return mReadyState; michael@0: } michael@0: michael@0: double michael@0: MediaSource::Duration() michael@0: { michael@0: if (mReadyState == MediaSourceReadyState::Closed) { michael@0: return UnspecifiedNaN(); michael@0: } michael@0: return mDuration; michael@0: } michael@0: michael@0: void michael@0: MediaSource::SetDuration(double aDuration, ErrorResult& aRv) michael@0: { michael@0: if (aDuration < 0 || IsNaN(aDuration)) { michael@0: aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); michael@0: return; michael@0: } michael@0: if (mReadyState != MediaSourceReadyState::Open || michael@0: mSourceBuffers->AnyUpdating()) { michael@0: aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: DurationChange(aDuration, aRv); michael@0: } michael@0: michael@0: already_AddRefed michael@0: MediaSource::AddSourceBuffer(const nsAString& aType, ErrorResult& aRv) michael@0: { michael@0: nsresult rv = mozilla::IsTypeSupported(aType); michael@0: MSE_DEBUG("MediaSource::AddSourceBuffer(Type=%s) -> %x", NS_ConvertUTF16toUTF8(aType).get(), rv); michael@0: if (NS_FAILED(rv)) { michael@0: aRv.Throw(rv); michael@0: return nullptr; michael@0: } michael@0: if (mSourceBuffers->Length() >= MAX_SOURCE_BUFFERS) { michael@0: aRv.Throw(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR); michael@0: return nullptr; michael@0: } michael@0: if (mReadyState != MediaSourceReadyState::Open) { michael@0: aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return nullptr; michael@0: } michael@0: nsContentTypeParser parser(aType); michael@0: nsAutoString mimeType; michael@0: rv = parser.GetType(mimeType); michael@0: if (NS_FAILED(rv)) { michael@0: aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); michael@0: return nullptr; michael@0: } michael@0: nsRefPtr sourceBuffer = SourceBuffer::Create(this, NS_ConvertUTF16toUTF8(mimeType)); michael@0: if (!sourceBuffer) { michael@0: aRv.Throw(NS_ERROR_FAILURE); // XXX need a better error here michael@0: return nullptr; michael@0: } michael@0: mSourceBuffers->Append(sourceBuffer); michael@0: mActiveSourceBuffers->Append(sourceBuffer); michael@0: MSE_DEBUG("%p AddSourceBuffer(Type=%s) -> %p", this, michael@0: NS_ConvertUTF16toUTF8(mimeType).get(), sourceBuffer.get()); michael@0: return sourceBuffer.forget(); michael@0: } michael@0: michael@0: void michael@0: MediaSource::RemoveSourceBuffer(SourceBuffer& aSourceBuffer, ErrorResult& aRv) michael@0: { michael@0: SourceBuffer* sourceBuffer = &aSourceBuffer; michael@0: MSE_DEBUG("%p RemoveSourceBuffer(Buffer=%p)", this, sourceBuffer); michael@0: if (!mSourceBuffers->Contains(sourceBuffer)) { michael@0: aRv.Throw(NS_ERROR_DOM_NOT_FOUND_ERR); michael@0: return; michael@0: } michael@0: if (sourceBuffer->Updating()) { michael@0: // TODO: michael@0: // abort stream append loop (if running) michael@0: // set updating to false michael@0: // fire "abort" at sourceBuffer michael@0: // fire "updateend" at sourceBuffer michael@0: } michael@0: // TODO: michael@0: // For all sourceBuffer audioTracks, videoTracks, textTracks: michael@0: // set sourceBuffer to null michael@0: // remove sourceBuffer video, audio, text Tracks from MediaElement tracks michael@0: // remove sourceBuffer video, audio, text Tracks and fire "removetrack" at affected lists michael@0: // fire "removetrack" at modified MediaElement track lists michael@0: // If removed enabled/selected, fire "change" at affected MediaElement list. michael@0: if (mActiveSourceBuffers->Contains(sourceBuffer)) { michael@0: mActiveSourceBuffers->Remove(sourceBuffer); michael@0: } michael@0: mSourceBuffers->Remove(sourceBuffer); michael@0: // TODO: Free all resources associated with sourceBuffer michael@0: } michael@0: michael@0: void michael@0: MediaSource::EndOfStream(const Optional& aError, ErrorResult& aRv) michael@0: { michael@0: MSE_DEBUG("%p EndOfStream(Error=%u)", this, aError.WasPassed() ? uint32_t(aError.Value()) : 0); michael@0: if (mReadyState != MediaSourceReadyState::Open || michael@0: mSourceBuffers->AnyUpdating()) { michael@0: aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: michael@0: SetReadyState(MediaSourceReadyState::Ended); michael@0: mSourceBuffers->Ended(); michael@0: if (!aError.WasPassed()) { michael@0: // TODO: michael@0: // Run duration change algorithm. michael@0: // DurationChange(highestDurationOfSourceBuffers, aRv); michael@0: // if (aRv.Failed()) { michael@0: // return; michael@0: // } michael@0: // Notify media element that all data is now available. michael@0: return; michael@0: } michael@0: switch (aError.Value()) { michael@0: case MediaSourceEndOfStreamError::Network: michael@0: // TODO: If media element has a readyState of: michael@0: // HAVE_NOTHING -> run resource fetch algorithm michael@0: // > HAVE_NOTHING -> run "interrupted" steps of resource fetch michael@0: break; michael@0: case MediaSourceEndOfStreamError::Decode: michael@0: // TODO: If media element has a readyState of: michael@0: // HAVE_NOTHING -> run "unsupported" steps of resource fetch michael@0: // > HAVE_NOTHING -> run "corrupted" steps of resource fetch michael@0: break; michael@0: default: michael@0: aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); michael@0: } michael@0: } michael@0: michael@0: /* static */ bool michael@0: MediaSource::IsTypeSupported(const GlobalObject&, const nsAString& aType) michael@0: { michael@0: #ifdef PR_LOGGING michael@0: if (!gMediaSourceLog) { michael@0: gMediaSourceLog = PR_NewLogModule("MediaSource"); michael@0: } michael@0: #endif michael@0: nsresult rv = mozilla::IsTypeSupported(aType); michael@0: MSE_DEBUG("MediaSource::IsTypeSupported(Type=%s) -> %x", NS_ConvertUTF16toUTF8(aType).get(), rv); michael@0: return NS_SUCCEEDED(rv); michael@0: } michael@0: michael@0: bool michael@0: MediaSource::Attach(MediaSourceDecoder* aDecoder) michael@0: { michael@0: MSE_DEBUG("%p Attaching decoder %p owner %p", this, aDecoder, aDecoder->GetOwner()); michael@0: MOZ_ASSERT(aDecoder); michael@0: if (mReadyState != MediaSourceReadyState::Closed) { michael@0: return false; michael@0: } michael@0: mDecoder = aDecoder; michael@0: mDecoder->AttachMediaSource(this); michael@0: SetReadyState(MediaSourceReadyState::Open); michael@0: return true; michael@0: } michael@0: michael@0: void michael@0: MediaSource::Detach() michael@0: { michael@0: MSE_DEBUG("%p Detaching decoder %p owner %p", this, mDecoder.get(), mDecoder->GetOwner()); michael@0: MOZ_ASSERT(mDecoder); michael@0: mDecoder->DetachMediaSource(); michael@0: mDecoder = nullptr; michael@0: mDuration = UnspecifiedNaN(); michael@0: mActiveSourceBuffers->Clear(); michael@0: mSourceBuffers->Clear(); michael@0: SetReadyState(MediaSourceReadyState::Closed); michael@0: } michael@0: michael@0: MediaSource::MediaSource(nsPIDOMWindow* aWindow) michael@0: : DOMEventTargetHelper(aWindow) michael@0: , mDuration(UnspecifiedNaN()) michael@0: , mDecoder(nullptr) michael@0: , mReadyState(MediaSourceReadyState::Closed) michael@0: { michael@0: mSourceBuffers = new SourceBufferList(this); michael@0: mActiveSourceBuffers = new SourceBufferList(this); michael@0: michael@0: #ifdef PR_LOGGING michael@0: if (!gMediaSourceLog) { michael@0: gMediaSourceLog = PR_NewLogModule("MediaSource"); michael@0: } michael@0: #endif michael@0: } michael@0: michael@0: void michael@0: MediaSource::SetReadyState(MediaSourceReadyState aState) michael@0: { michael@0: MOZ_ASSERT(aState != mReadyState); michael@0: MSE_DEBUG("%p SetReadyState old=%d new=%d", this, mReadyState, aState); michael@0: michael@0: MediaSourceReadyState oldState = mReadyState; michael@0: mReadyState = aState; michael@0: michael@0: if (mReadyState == MediaSourceReadyState::Open && michael@0: (oldState == MediaSourceReadyState::Closed || michael@0: oldState == MediaSourceReadyState::Ended)) { michael@0: QueueAsyncSimpleEvent("sourceopen"); michael@0: return; michael@0: } michael@0: michael@0: if (mReadyState == MediaSourceReadyState::Ended && michael@0: oldState == MediaSourceReadyState::Open) { michael@0: QueueAsyncSimpleEvent("sourceended"); michael@0: return; michael@0: } michael@0: michael@0: if (mReadyState == MediaSourceReadyState::Closed && michael@0: (oldState == MediaSourceReadyState::Open || michael@0: oldState == MediaSourceReadyState::Ended)) { michael@0: QueueAsyncSimpleEvent("sourceclose"); michael@0: return; michael@0: } michael@0: michael@0: NS_WARNING("Invalid MediaSource readyState transition"); michael@0: } michael@0: michael@0: void michael@0: MediaSource::DispatchSimpleEvent(const char* aName) michael@0: { michael@0: MSE_DEBUG("%p Dispatching event %s to MediaSource", this, aName); michael@0: DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName)); michael@0: } michael@0: michael@0: void michael@0: MediaSource::QueueAsyncSimpleEvent(const char* aName) michael@0: { michael@0: MSE_DEBUG("%p Queuing event %s to MediaSource", this, aName); michael@0: nsCOMPtr event = new AsyncEventRunner(this, aName); michael@0: NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); michael@0: } michael@0: michael@0: void michael@0: MediaSource::DurationChange(double aNewDuration, ErrorResult& aRv) michael@0: { michael@0: if (mDuration == aNewDuration) { michael@0: return; michael@0: } michael@0: double oldDuration = mDuration; michael@0: mDuration = aNewDuration; michael@0: if (aNewDuration < oldDuration) { michael@0: mSourceBuffers->Remove(aNewDuration, oldDuration, aRv); michael@0: if (aRv.Failed()) { michael@0: return; michael@0: } michael@0: } michael@0: // TODO: If partial audio frames/text cues exist, clamp duration based on mSourceBuffers. michael@0: // TODO: Update media element's duration and run element's duration change algorithm. michael@0: } michael@0: michael@0: nsPIDOMWindow* michael@0: MediaSource::GetParentObject() const michael@0: { michael@0: return GetOwner(); michael@0: } michael@0: michael@0: JSObject* michael@0: MediaSource::WrapObject(JSContext* aCx) michael@0: { michael@0: return MediaSourceBinding::Wrap(aCx, this); michael@0: } michael@0: michael@0: void michael@0: MediaSource::NotifyEvicted(double aStart, double aEnd) michael@0: { michael@0: // Cycle through all SourceBuffers and tell them to evict data in michael@0: // the given range. michael@0: mSourceBuffers->Evict(aStart, aEnd); michael@0: } michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaSource, DOMEventTargetHelper, michael@0: mSourceBuffers, mActiveSourceBuffers) michael@0: michael@0: NS_IMPL_ADDREF_INHERITED(MediaSource, DOMEventTargetHelper) michael@0: NS_IMPL_RELEASE_INHERITED(MediaSource, DOMEventTargetHelper) michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaSource) michael@0: NS_INTERFACE_MAP_ENTRY(mozilla::dom::MediaSource) michael@0: NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) michael@0: michael@0: } // namespace dom michael@0: michael@0: } // namespace mozilla