diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/media/MediaRecorder.cpp | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/media/MediaRecorder.cpp')
-rw-r--r-- | dom/media/MediaRecorder.cpp | 1535 |
1 files changed, 1535 insertions, 0 deletions
diff --git a/dom/media/MediaRecorder.cpp b/dom/media/MediaRecorder.cpp new file mode 100644 index 000000000..e2d72b112 --- /dev/null +++ b/dom/media/MediaRecorder.cpp @@ -0,0 +1,1535 @@ +/* -*- 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 "MediaRecorder.h" +#include "AudioNodeEngine.h" +#include "AudioNodeStream.h" +#include "DOMMediaStream.h" +#include "EncodedBufferCache.h" +#include "MediaDecoder.h" +#include "MediaEncoder.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/AudioStreamTrack.h" +#include "mozilla/dom/BlobEvent.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/RecordErrorEvent.h" +#include "mozilla/dom/VideoStreamTrack.h" +#include "nsAutoPtr.h" +#include "nsContentUtils.h" +#include "nsError.h" +#include "nsIDocument.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsIScriptError.h" +#include "nsMimeTypes.h" +#include "nsProxyRelease.h" +#include "nsTArray.h" +#include "GeckoProfiler.h" +#include "nsContentTypeParser.h" +#include "nsCharSeparatedTokenizer.h" + +#ifdef LOG +#undef LOG +#endif + +mozilla::LazyLogModule gMediaRecorderLog("MediaRecorder"); +#define LOG(type, msg) MOZ_LOG(gMediaRecorderLog, type, msg) + +namespace mozilla { + +namespace dom { + +/** ++ * MediaRecorderReporter measures memory being used by the Media Recorder. ++ * ++ * It is a singleton reporter and the single class object lives as long as at ++ * least one Recorder is registered. In MediaRecorder, the reporter is unregistered ++ * when it is destroyed. ++ */ +class MediaRecorderReporter final : public nsIMemoryReporter +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + MediaRecorderReporter() {}; + static MediaRecorderReporter* UniqueInstance(); + void InitMemoryReporter(); + + static void AddMediaRecorder(MediaRecorder *aRecorder) + { + GetRecorders().AppendElement(aRecorder); + } + + static void RemoveMediaRecorder(MediaRecorder *aRecorder) + { + RecordersArray& recorders = GetRecorders(); + recorders.RemoveElement(aRecorder); + if (recorders.IsEmpty()) { + sUniqueInstance = nullptr; + } + } + + NS_IMETHOD + CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) override + { + int64_t amount = 0; + RecordersArray& recorders = GetRecorders(); + for (size_t i = 0; i < recorders.Length(); ++i) { + amount += recorders[i]->SizeOfExcludingThis(MallocSizeOf); + } + + MOZ_COLLECT_REPORT( + "explicit/media/recorder", KIND_HEAP, UNITS_BYTES, amount, + "Memory used by media recorder."); + + return NS_OK; + } + +private: + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + virtual ~MediaRecorderReporter(); + static StaticRefPtr<MediaRecorderReporter> sUniqueInstance; + typedef nsTArray<MediaRecorder*> RecordersArray; + static RecordersArray& GetRecorders() + { + return UniqueInstance()->mRecorders; + } + RecordersArray mRecorders; +}; +NS_IMPL_ISUPPORTS(MediaRecorderReporter, nsIMemoryReporter); + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaRecorder) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaRecorder, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaRecorder, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioNode) + tmp->UnRegisterActivityObserver(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder) + NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(MediaRecorder, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(MediaRecorder, DOMEventTargetHelper) + +/** + * Session is an object to represent a single recording event. + * In original design, all recording context is stored in MediaRecorder, which causes + * a problem if someone calls MediaRecoder::Stop and MedaiRecorder::Start quickly. + * To prevent blocking main thread, media encoding is executed in a second thread, + * named as Read Thread. For the same reason, we do not wait Read Thread shutdown in + * MediaRecorder::Stop. If someone call MediaRecoder::Start before Read Thread shutdown, + * the same recording context in MediaRecoder might be access by two Reading Threads, + * which cause a problem. + * In the new design, we put recording context into Session object, including Read + * Thread. Each Session has its own recording context and Read Thread, problem is been + * resolved. + * + * Life cycle of a Session object. + * 1) Initialization Stage (in main thread) + * Setup media streams in MSG, and bind MediaEncoder with Source Stream when mStream is available. + * Resource allocation, such as encoded data cache buffer and MediaEncoder. + * Create read thread. + * Automatically switch to Extract stage in the end of this stage. + * 2) Extract Stage (in Read Thread) + * Pull encoded A/V frames from MediaEncoder, dispatch to OnDataAvailable handler. + * Unless a client calls Session::Stop, Session object keeps stay in this stage. + * 3) Destroy Stage (in main thread) + * Switch from Extract stage to Destroy stage by calling Session::Stop. + * Release session resource and remove associated streams from MSG. + * + * Lifetime of MediaRecorder and Session objects. + * 1) MediaRecorder creates a Session in MediaRecorder::Start function and holds + * a reference to Session. Then the Session registers itself to + * ShutdownObserver and also holds a reference to MediaRecorder. + * Therefore, the reference dependency in gecko is: + * ShutdownObserver -> Session <-> MediaRecorder, note that there is a cycle + * reference between Session and MediaRecorder. + * 2) A Session is destroyed in DestroyRunnable after MediaRecorder::Stop being called + * _and_ all encoded media data been passed to OnDataAvailable handler. + * 3) MediaRecorder::Stop is called by user or the document is going to + * inactive or invisible. + */ +class MediaRecorder::Session: public nsIObserver, + public PrincipalChangeObserver<MediaStreamTrack>, + public DOMMediaStream::TrackListener +{ + NS_DECL_THREADSAFE_ISUPPORTS + + // Main thread task. + // Create a blob event and send back to client. + class PushBlobRunnable : public Runnable + { + public: + explicit PushBlobRunnable(Session* aSession) + : mSession(aSession) + { } + + NS_IMETHOD Run() override + { + LOG(LogLevel::Debug, ("Session.PushBlobRunnable s=(%p)", mSession.get())); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<MediaRecorder> recorder = mSession->mRecorder; + if (!recorder) { + return NS_OK; + } + + nsresult rv = recorder->CreateAndDispatchBlobEvent(mSession->GetEncodedData()); + if (NS_FAILED(rv)) { + recorder->NotifyError(rv); + } + + return NS_OK; + } + + private: + RefPtr<Session> mSession; + }; + + // Notify encoder error, run in main thread task. (Bug 1095381) + class EncoderErrorNotifierRunnable : public Runnable + { + public: + explicit EncoderErrorNotifierRunnable(Session* aSession) + : mSession(aSession) + { } + + NS_IMETHOD Run() override + { + LOG(LogLevel::Debug, ("Session.ErrorNotifyRunnable s=(%p)", mSession.get())); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<MediaRecorder> recorder = mSession->mRecorder; + if (!recorder) { + return NS_OK; + } + + if (mSession->IsEncoderError()) { + recorder->NotifyError(NS_ERROR_UNEXPECTED); + } + return NS_OK; + } + + private: + RefPtr<Session> mSession; + }; + + // Fire start event and set mimeType, run in main thread task. + class DispatchStartEventRunnable : public Runnable + { + public: + DispatchStartEventRunnable(Session* aSession, const nsAString & aEventName) + : mSession(aSession) + , mEventName(aEventName) + { } + + NS_IMETHOD Run() override + { + LOG(LogLevel::Debug, ("Session.DispatchStartEventRunnable s=(%p)", mSession.get())); + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_TRUE(mSession->mRecorder, NS_OK); + RefPtr<MediaRecorder> recorder = mSession->mRecorder; + + recorder->SetMimeType(mSession->mMimeType); + recorder->DispatchSimpleEvent(mEventName); + + return NS_OK; + } + + private: + RefPtr<Session> mSession; + nsString mEventName; + }; + + // Record thread task and it run in Media Encoder thread. + // Fetch encoded Audio/Video data from MediaEncoder. + class ExtractRunnable : public Runnable + { + public: + explicit ExtractRunnable(Session* aSession) + : mSession(aSession) {} + + ~ExtractRunnable() + {} + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_GetCurrentThread() == mSession->mReadThread); + + LOG(LogLevel::Debug, ("Session.ExtractRunnable shutdown = %d", mSession->mEncoder->IsShutdown())); + if (!mSession->mEncoder->IsShutdown()) { + mSession->Extract(false); + if (NS_FAILED(NS_DispatchToCurrentThread(this))) { + NS_WARNING("Failed to dispatch ExtractRunnable to encoder thread"); + } + } else { + // Flush out remaining encoded data. + mSession->Extract(true); + if (mSession->mIsRegisterProfiler) + profiler_unregister_thread(); + if (NS_FAILED(NS_DispatchToMainThread( + new DestroyRunnable(mSession.forget())))) { + MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed"); + } + } + return NS_OK; + } + + private: + RefPtr<Session> mSession; + }; + + // For Ensure recorder has tracks to record. + class TracksAvailableCallback : public OnTracksAvailableCallback + { + public: + explicit TracksAvailableCallback(Session *aSession, TrackRate aTrackRate) + : mSession(aSession) + , mTrackRate(aTrackRate) {} + + virtual void NotifyTracksAvailable(DOMMediaStream* aStream) + { + if (mSession->mStopIssued) { + return; + } + + MOZ_RELEASE_ASSERT(aStream); + mSession->MediaStreamReady(*aStream); + + uint8_t trackTypes = 0; + nsTArray<RefPtr<mozilla::dom::AudioStreamTrack>> audioTracks; + aStream->GetAudioTracks(audioTracks); + if (!audioTracks.IsEmpty()) { + trackTypes |= ContainerWriter::CREATE_AUDIO_TRACK; + mSession->ConnectMediaStreamTrack(*audioTracks[0]); + } + + nsTArray<RefPtr<mozilla::dom::VideoStreamTrack>> videoTracks; + aStream->GetVideoTracks(videoTracks); + if (!videoTracks.IsEmpty()) { + trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK; + mSession->ConnectMediaStreamTrack(*videoTracks[0]); + } + + if (audioTracks.Length() > 1 || + videoTracks.Length() > 1) { + // When MediaRecorder supports multiple tracks, we should set up a single + // MediaInputPort from the input stream, and let main thread check + // track principals async later. + nsPIDOMWindowInner* window = mSession->mRecorder->GetParentObject(); + nsIDocument* document = window ? window->GetExtantDoc() : nullptr; + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Media"), + document, + nsContentUtils::eDOM_PROPERTIES, + "MediaRecorderMultiTracksNotSupported"); + mSession->DoSessionEndTask(NS_ERROR_ABORT); + return; + } + + NS_ASSERTION(trackTypes != 0, "TracksAvailableCallback without any tracks available"); + + // Check that we may access the tracks' content. + if (!mSession->MediaStreamTracksPrincipalSubsumes()) { + LOG(LogLevel::Warning, ("Session.NotifyTracksAvailable MediaStreamTracks principal check failed")); + mSession->DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + LOG(LogLevel::Debug, ("Session.NotifyTracksAvailable track type = (%d)", trackTypes)); + mSession->InitEncoder(trackTypes, mTrackRate); + } + private: + RefPtr<Session> mSession; + TrackRate mTrackRate; + }; + // Main thread task. + // To delete RecordingSession object. + class DestroyRunnable : public Runnable + { + public: + explicit DestroyRunnable(Session* aSession) + : mSession(aSession) {} + + explicit DestroyRunnable(already_AddRefed<Session> aSession) + : mSession(aSession) {} + + NS_IMETHOD Run() override + { + LOG(LogLevel::Debug, ("Session.DestroyRunnable session refcnt = (%d) stopIssued %d s=(%p)", + (int)mSession->mRefCnt, mSession->mStopIssued, mSession.get())); + MOZ_ASSERT(NS_IsMainThread() && mSession); + RefPtr<MediaRecorder> recorder = mSession->mRecorder; + if (!recorder) { + return NS_OK; + } + // SourceMediaStream is ended, and send out TRACK_EVENT_END notification. + // Read Thread will be terminate soon. + // We need to switch MediaRecorder to "Stop" state first to make sure + // MediaRecorder is not associated with this Session anymore, then, it's + // safe to delete this Session. + // Also avoid to run if this session already call stop before + if (!mSession->mStopIssued) { + ErrorResult result; + mSession->mStopIssued = true; + recorder->Stop(result); + if (NS_FAILED(NS_DispatchToMainThread(new DestroyRunnable(mSession.forget())))) { + MOZ_ASSERT(false, "NS_DispatchToMainThread failed"); + } + return NS_OK; + } + + // Dispatch stop event and clear MIME type. + mSession->mMimeType = NS_LITERAL_STRING(""); + recorder->SetMimeType(mSession->mMimeType); + recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop")); + mSession->BreakCycle(); + return NS_OK; + } + + private: + // Call mSession::Release automatically while DestroyRunnable be destroy. + RefPtr<Session> mSession; + }; + + friend class EncoderErrorNotifierRunnable; + friend class PushBlobRunnable; + friend class ExtractRunnable; + friend class DestroyRunnable; + friend class TracksAvailableCallback; + +public: + Session(MediaRecorder* aRecorder, int32_t aTimeSlice) + : mRecorder(aRecorder) + , mTimeSlice(aTimeSlice) + , mStopIssued(false) + , mIsStartEventFired(false) + , mIsRegisterProfiler(false) + , mNeedSessionEndTask(true) + , mSelectedVideoTrackID(TRACK_NONE) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_COUNT_CTOR(MediaRecorder::Session); + + uint32_t maxMem = Preferences::GetUint("media.recorder.max_memory", + MAX_ALLOW_MEMORY_BUFFER); + mEncodedBufferCache = new EncodedBufferCache(maxMem); + mLastBlobTimeStamp = TimeStamp::Now(); + } + + void PrincipalChanged(MediaStreamTrack* aTrack) override + { + NS_ASSERTION(mMediaStreamTracks.Contains(aTrack), + "Principal changed for unrecorded track"); + if (!MediaStreamTracksPrincipalSubsumes()) { + DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR); + } + } + + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override + { + LOG(LogLevel::Warning, ("Session.NotifyTrackAdded %p Raising error due to track set change", this)); + DoSessionEndTask(NS_ERROR_ABORT); + } + + void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override + { + // Handle possible early removal of direct video listener + if (mEncoder) { + RefPtr<VideoStreamTrack> videoTrack = aTrack->AsVideoStreamTrack(); + if (videoTrack) { + videoTrack->RemoveDirectListener(mEncoder->GetVideoSink()); + } + } + + RefPtr<MediaInputPort> foundInputPort; + for (RefPtr<MediaInputPort> inputPort : mInputPorts) { + if (aTrack->IsForwardedThrough(inputPort)) { + foundInputPort = inputPort; + break; + } + } + + if (foundInputPort) { + // A recorded track was removed or ended. End it in the recording. + // Don't raise an error. + foundInputPort->Destroy(); + DebugOnly<bool> removed = mInputPorts.RemoveElement(foundInputPort); + MOZ_ASSERT(removed); + return; + } + + LOG(LogLevel::Warning, ("Session.NotifyTrackRemoved %p Raising error due to track set change", this)); + DoSessionEndTask(NS_ERROR_ABORT); + } + + void Start() + { + LOG(LogLevel::Debug, ("Session.Start %p", this)); + MOZ_ASSERT(NS_IsMainThread()); + + // Create a Track Union Stream + MediaStreamGraph* gm = mRecorder->GetSourceMediaStream()->Graph(); + TrackRate trackRate = gm->GraphRate(); + mTrackUnionStream = gm->CreateTrackUnionStream(); + MOZ_ASSERT(mTrackUnionStream, "CreateTrackUnionStream failed"); + + mTrackUnionStream->SetAutofinish(true); + + DOMMediaStream* domStream = mRecorder->Stream(); + if (domStream) { + // Get the available tracks from the DOMMediaStream. + // The callback will report back tracks that we have to connect to + // mTrackUnionStream and listen to principal changes on. + TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(this, trackRate); + domStream->OnTracksAvailable(tracksAvailableCallback); + } else { + // Check that we may access the audio node's content. + if (!AudioNodePrincipalSubsumes()) { + LOG(LogLevel::Warning, ("Session.Start AudioNode principal check failed")); + DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR); + return; + } + // Bind this Track Union Stream with Source Media. + RefPtr<MediaInputPort> inputPort = + mTrackUnionStream->AllocateInputPort(mRecorder->GetSourceMediaStream()); + mInputPorts.AppendElement(inputPort.forget()); + MOZ_ASSERT(mInputPorts[mInputPorts.Length()-1]); + + // Web Audio node has only audio. + InitEncoder(ContainerWriter::CREATE_AUDIO_TRACK, trackRate); + } + } + + void Stop() + { + LOG(LogLevel::Debug, ("Session.Stop %p", this)); + MOZ_ASSERT(NS_IsMainThread()); + mStopIssued = true; + CleanupStreams(); + if (mNeedSessionEndTask) { + LOG(LogLevel::Debug, ("Session.Stop mNeedSessionEndTask %p", this)); + // End the Session directly if there is no ExtractRunnable. + DoSessionEndTask(NS_OK); + } + // If we don't do this, the Session will be purged only when the navigator exit + // by the ShutdownObserver and the memory and number of threads will quickly + // grows with each couple stop/start. + nsContentUtils::UnregisterShutdownObserver(this); + } + + nsresult Pause() + { + LOG(LogLevel::Debug, ("Session.Pause")); + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); + mTrackUnionStream->Suspend(); + if (mEncoder) { + mEncoder->Suspend(); + } + + return NS_OK; + } + + nsresult Resume() + { + LOG(LogLevel::Debug, ("Session.Resume")); + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); + if (mEncoder) { + mEncoder->Resume(); + } + mTrackUnionStream->Resume(); + + return NS_OK; + } + + nsresult RequestData() + { + LOG(LogLevel::Debug, ("Session.RequestData")); + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(NS_DispatchToMainThread(new EncoderErrorNotifierRunnable(this))) || + NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) { + MOZ_ASSERT(false, "RequestData NS_DispatchToMainThread failed"); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + already_AddRefed<nsIDOMBlob> GetEncodedData() + { + MOZ_ASSERT(NS_IsMainThread()); + return mEncodedBufferCache->ExtractBlob(mRecorder->GetParentObject(), + mMimeType); + } + + bool IsEncoderError() + { + if (mEncoder && mEncoder->HasError()) { + return true; + } + return false; + } + + size_t + SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const + { + return (mEncoder ? mEncoder->SizeOfExcludingThis(aMallocSizeOf) : 0); + } + + +private: + // Only DestroyRunnable is allowed to delete Session object. + virtual ~Session() + { + MOZ_COUNT_DTOR(MediaRecorder::Session); + LOG(LogLevel::Debug, ("Session.~Session (%p)", this)); + CleanupStreams(); + if (mReadThread) { + mReadThread->Shutdown(); + mReadThread = nullptr; + // Inside the if() so that if we delete after xpcom-shutdown's Observe(), we + // won't try to remove it after the observer service is shut down. + // Unregistering for safety in case Stop() was never called + nsContentUtils::UnregisterShutdownObserver(this); + } + } + // Pull encoded media data from MediaEncoder and put into EncodedBufferCache. + // Destroy this session object in the end of this function. + // If the bool aForceFlush is true, we will force to dispatch a + // PushBlobRunnable to main thread. + void Extract(bool aForceFlush) + { + MOZ_ASSERT(NS_GetCurrentThread() == mReadThread); + LOG(LogLevel::Debug, ("Session.Extract %p", this)); + + if (!mIsRegisterProfiler) { + char aLocal; + profiler_register_thread("Media_Encoder", &aLocal); + mIsRegisterProfiler = true; + } + + PROFILER_LABEL("MediaRecorder", "Session Extract", + js::ProfileEntry::Category::OTHER); + + // Pull encoded media data from MediaEncoder + nsTArray<nsTArray<uint8_t> > encodedBuf; + mEncoder->GetEncodedData(&encodedBuf, mMimeType); + + // Append pulled data into cache buffer. + for (uint32_t i = 0; i < encodedBuf.Length(); i++) { + if (!encodedBuf[i].IsEmpty()) { + mEncodedBufferCache->AppendBuffer(encodedBuf[i]); + // Fire the start event when encoded data is available. + if (!mIsStartEventFired) { + NS_DispatchToMainThread( + new DispatchStartEventRunnable(this, NS_LITERAL_STRING("start"))); + mIsStartEventFired = true; + } + } + } + + // Whether push encoded data back to onDataAvailable automatically or we + // need a flush. + bool pushBlob = false; + if ((mTimeSlice > 0) && + ((TimeStamp::Now()-mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice)) { + pushBlob = true; + } + if (pushBlob || aForceFlush) { + // Fire the start event before the blob. + if (!mIsStartEventFired) { + NS_DispatchToMainThread( + new DispatchStartEventRunnable(this, NS_LITERAL_STRING("start"))); + mIsStartEventFired = true; + } + if (NS_FAILED(NS_DispatchToMainThread(new EncoderErrorNotifierRunnable(this)))) { + MOZ_ASSERT(false, "NS_DispatchToMainThread EncoderErrorNotifierRunnable failed"); + } + if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) { + MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed"); + } else { + mLastBlobTimeStamp = TimeStamp::Now(); + } + } + } + + void MediaStreamReady(DOMMediaStream& aStream) { + mMediaStream = &aStream; + aStream.RegisterTrackListener(this); + } + + void ConnectMediaStreamTrack(MediaStreamTrack& aTrack) + { + mMediaStreamTracks.AppendElement(&aTrack); + aTrack.AddPrincipalChangeObserver(this); + RefPtr<MediaInputPort> inputPort = + aTrack.ForwardTrackContentsTo(mTrackUnionStream); + MOZ_ASSERT(inputPort); + mInputPorts.AppendElement(inputPort.forget()); + MOZ_ASSERT(mInputPorts[mInputPorts.Length()-1]); + } + + bool PrincipalSubsumes(nsIPrincipal* aPrincipal) + { + if (!mRecorder->GetOwner()) + return false; + nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc(); + if (!doc) { + return false; + } + if (!aPrincipal) { + return false; + } + bool subsumes; + if (NS_FAILED(doc->NodePrincipal()->Subsumes(aPrincipal, &subsumes))) { + return false; + } + return subsumes; + } + + bool MediaStreamTracksPrincipalSubsumes() + { + MOZ_ASSERT(mRecorder->mDOMStream); + nsCOMPtr<nsIPrincipal> principal = nullptr; + for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) { + nsContentUtils::CombineResourcePrincipals(&principal, track->GetPrincipal()); + } + return PrincipalSubsumes(principal); + } + + bool AudioNodePrincipalSubsumes() + { + MOZ_ASSERT(mRecorder->mAudioNode != nullptr); + nsIDocument* doc = mRecorder->mAudioNode->GetOwner() + ? mRecorder->mAudioNode->GetOwner()->GetExtantDoc() + : nullptr; + nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr; + return PrincipalSubsumes(principal); + } + + bool CheckPermission(const char* aType) + { + if (!mRecorder || !mRecorder->GetOwner()) { + return false; + } + + nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc(); + if (!doc) { + return false; + } + + // Certified applications can always assign AUDIO_3GPP + if (doc->NodePrincipal()->GetAppStatus() == + nsIPrincipal::APP_STATUS_CERTIFIED) { + return true; + } + + nsCOMPtr<nsIPermissionManager> pm = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID); + + if (!pm) { + return false; + } + + uint32_t perm = nsIPermissionManager::DENY_ACTION; + pm->TestExactPermissionFromPrincipal(doc->NodePrincipal(), aType, &perm); + return perm == nsIPermissionManager::ALLOW_ACTION; + } + + void InitEncoder(uint8_t aTrackTypes, TrackRate aTrackRate) + { + LOG(LogLevel::Debug, ("Session.InitEncoder %p", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mRecorder) { + LOG(LogLevel::Debug, ("Session.InitEncoder failure, mRecorder is null %p", this)); + return; + } + // Allocate encoder and bind with union stream. + // At this stage, the API doesn't allow UA to choose the output mimeType format. + + // Make sure the application has permission to assign AUDIO_3GPP + if (mRecorder->mMimeType.EqualsLiteral(AUDIO_3GPP) && CheckPermission("audio-capture:3gpp")) { + mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(AUDIO_3GPP), + mRecorder->GetAudioBitrate(), + mRecorder->GetVideoBitrate(), + mRecorder->GetBitrate(), + aTrackTypes, aTrackRate); + } else if (mRecorder->mMimeType.EqualsLiteral(AUDIO_3GPP2) && CheckPermission("audio-capture:3gpp2")) { + mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(AUDIO_3GPP2), + mRecorder->GetAudioBitrate(), + mRecorder->GetVideoBitrate(), + mRecorder->GetBitrate(), + aTrackTypes, aTrackRate); + } else { + mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""), + mRecorder->GetAudioBitrate(), + mRecorder->GetVideoBitrate(), + mRecorder->GetBitrate(), + aTrackTypes, aTrackRate); + } + + if (!mEncoder) { + LOG(LogLevel::Debug, ("Session.InitEncoder !mEncoder %p", this)); + DoSessionEndTask(NS_ERROR_ABORT); + return; + } + + // Media stream is ready but UA issues a stop method follow by start method. + // The Session::stop would clean the mTrackUnionStream. If the AfterTracksAdded + // comes after stop command, this function would crash. + if (!mTrackUnionStream) { + LOG(LogLevel::Debug, ("Session.InitEncoder !mTrackUnionStream %p", this)); + DoSessionEndTask(NS_OK); + return; + } + mTrackUnionStream->AddListener(mEncoder.get()); + + nsTArray<RefPtr<mozilla::dom::VideoStreamTrack>> videoTracks; + DOMMediaStream* domStream = mRecorder->Stream(); + if (domStream) { + domStream->GetVideoTracks(videoTracks); + if (!videoTracks.IsEmpty()) { + // Right now, the MediaRecorder hasn't dealt with multiple video track + // issues. So we just bind with the first video track. Bug 1276928 is + // the following. + videoTracks[0]->AddDirectListener(mEncoder->GetVideoSink()); + } + } + + // Try to use direct listeners if possible + if (domStream && domStream->GetInputStream()) { + mInputStream = domStream->GetInputStream()->AsSourceStream(); + if (mInputStream) { + mInputStream->AddDirectListener(mEncoder.get()); + mEncoder->SetDirectConnect(true); + } + } + + // Create a thread to read encode media data from MediaEncoder. + if (!mReadThread) { + nsresult rv = NS_NewNamedThread("Media_Encoder", getter_AddRefs(mReadThread)); + if (NS_FAILED(rv)) { + LOG(LogLevel::Debug, ("Session.InitEncoder !mReadThread %p", this)); + DoSessionEndTask(rv); + return; + } + } + + // In case source media stream does not notify track end, receive + // shutdown notification and stop Read Thread. + nsContentUtils::RegisterShutdownObserver(this); + + nsCOMPtr<nsIRunnable> event = new ExtractRunnable(this); + if (NS_FAILED(mReadThread->Dispatch(event, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch ExtractRunnable at beginning"); + LOG(LogLevel::Debug, ("Session.InitEncoder !ReadThread->Dispatch %p", this)); + DoSessionEndTask(NS_ERROR_ABORT); + } + // Set mNeedSessionEndTask to false because the + // ExtractRunnable/DestroyRunnable will take the response to + // end the session. + mNeedSessionEndTask = false; + } + // application should get blob and onstop event + void DoSessionEndTask(nsresult rv) + { + MOZ_ASSERT(NS_IsMainThread()); + CleanupStreams(); + NS_DispatchToMainThread( + new DispatchStartEventRunnable(this, NS_LITERAL_STRING("start"))); + + if (NS_FAILED(rv)) { + NS_DispatchToMainThread(NewRunnableMethod<nsresult>(mRecorder, + &MediaRecorder::NotifyError, rv)); + } + if (NS_FAILED(NS_DispatchToMainThread(new EncoderErrorNotifierRunnable(this)))) { + MOZ_ASSERT(false, "NS_DispatchToMainThread EncoderErrorNotifierRunnable failed"); + } + if (rv != NS_ERROR_DOM_SECURITY_ERR) { + // Don't push a blob if there was a security error. + if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) { + MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed"); + } + } + if (NS_FAILED(NS_DispatchToMainThread(new DestroyRunnable(this)))) { + MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed"); + } + mNeedSessionEndTask = false; + } + void CleanupStreams() + { + if (mInputStream) { + if (mEncoder) { + mInputStream->RemoveDirectListener(mEncoder.get()); + } + mInputStream = nullptr; + } + for (RefPtr<MediaInputPort>& inputPort : mInputPorts) { + MOZ_ASSERT(inputPort); + inputPort->Destroy(); + } + mInputPorts.Clear(); + + if (mTrackUnionStream) { + if (mEncoder) { + nsTArray<RefPtr<mozilla::dom::VideoStreamTrack>> videoTracks; + DOMMediaStream* domStream = mRecorder->Stream(); + if (domStream) { + domStream->GetVideoTracks(videoTracks); + if (!videoTracks.IsEmpty()) { + videoTracks[0]->RemoveDirectListener(mEncoder->GetVideoSink()); + } + } + } + + // Sometimes the MediaEncoder might be initialized fail and go to + // |CleanupStreams|. So the mEncoder might be a nullptr in this case. + if (mEncoder && mSelectedVideoTrackID != TRACK_NONE) { + mTrackUnionStream->RemoveVideoOutput(mEncoder->GetVideoSink(), mSelectedVideoTrackID); + } + if (mEncoder) { + mTrackUnionStream->RemoveListener(mEncoder.get()); + } + mTrackUnionStream->Destroy(); + mTrackUnionStream = nullptr; + } + + if (mMediaStream) { + mMediaStream->UnregisterTrackListener(this); + mMediaStream = nullptr; + } + + for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) { + track->RemovePrincipalChangeObserver(this); + } + mMediaStreamTracks.Clear(); + } + + NS_IMETHOD Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) override + { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Debug, ("Session.Observe XPCOM_SHUTDOWN %p", this)); + if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + // Force stop Session to terminate Read Thread. + mEncoder->Cancel(); + if (mReadThread) { + mReadThread->Shutdown(); + mReadThread = nullptr; + } + nsContentUtils::UnregisterShutdownObserver(this); + BreakCycle(); + Stop(); + } + + return NS_OK; + } + + // Break the cycle reference between Session and MediaRecorder. + void BreakCycle() + { + MOZ_ASSERT(NS_IsMainThread()); + if (mRecorder) { + mRecorder->RemoveSession(this); + mRecorder = nullptr; + } + } + +private: + // Hold reference to MediaRecoder that ensure MediaRecorder is alive + // if there is an active session. Access ONLY on main thread. + RefPtr<MediaRecorder> mRecorder; + + // Receive track data from source and dispatch to Encoder. + // Pause/ Resume controller. + RefPtr<ProcessedMediaStream> mTrackUnionStream; + RefPtr<SourceMediaStream> mInputStream; + nsTArray<RefPtr<MediaInputPort>> mInputPorts; + + // Stream currently recorded. + RefPtr<DOMMediaStream> mMediaStream; + + // Tracks currently recorded. This should be a subset of mMediaStream's track + // set. + nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks; + + // Runnable thread for read data from MediaEncode. + nsCOMPtr<nsIThread> mReadThread; + // MediaEncoder pipeline. + RefPtr<MediaEncoder> mEncoder; + // A buffer to cache encoded meda data. + nsAutoPtr<EncodedBufferCache> mEncodedBufferCache; + // Current session mimeType + nsString mMimeType; + // Timestamp of the last fired dataavailable event. + TimeStamp mLastBlobTimeStamp; + // The interval of passing encoded data from EncodedBufferCache to onDataAvailable + // handler. "mTimeSlice < 0" means Session object does not push encoded data to + // onDataAvailable, instead, it passive wait the client side pull encoded data + // by calling requestData API. + const int32_t mTimeSlice; + // Indicate this session's stop has been called. + bool mStopIssued; + // Indicate the session had fire start event. Encoding thread only. + bool mIsStartEventFired; + // The register flag for "Media_Encoder" thread to profiler + bool mIsRegisterProfiler; + // False if the InitEncoder called successfully, ensure the + // ExtractRunnable/DestroyRunnable will end the session. + // Main thread only. + bool mNeedSessionEndTask; + TrackID mSelectedVideoTrackID; +}; + +NS_IMPL_ISUPPORTS(MediaRecorder::Session, nsIObserver) + +MediaRecorder::~MediaRecorder() +{ + if (mPipeStream != nullptr) { + mInputPort->Destroy(); + mPipeStream->Destroy(); + } + LOG(LogLevel::Debug, ("~MediaRecorder (%p)", this)); + UnRegisterActivityObserver(); +} + +MediaRecorder::MediaRecorder(DOMMediaStream& aSourceMediaStream, + nsPIDOMWindowInner* aOwnerWindow) + : DOMEventTargetHelper(aOwnerWindow) + , mState(RecordingState::Inactive) +{ + MOZ_ASSERT(aOwnerWindow); + MOZ_ASSERT(aOwnerWindow->IsInnerWindow()); + mDOMStream = &aSourceMediaStream; + + RegisterActivityObserver(); +} + +MediaRecorder::MediaRecorder(AudioNode& aSrcAudioNode, + uint32_t aSrcOutput, + nsPIDOMWindowInner* aOwnerWindow) + : DOMEventTargetHelper(aOwnerWindow) + , mState(RecordingState::Inactive) +{ + MOZ_ASSERT(aOwnerWindow); + MOZ_ASSERT(aOwnerWindow->IsInnerWindow()); + + // Only AudioNodeStream of kind EXTERNAL_STREAM stores output audio data in + // the track (see AudioNodeStream::AdvanceOutputSegment()). That means track + // union stream in recorder session won't be able to copy data from the + // stream of non-destination node. Create a pipe stream in this case. + if (aSrcAudioNode.NumberOfOutputs() > 0) { + AudioContext* ctx = aSrcAudioNode.Context(); + AudioNodeEngine* engine = new AudioNodeEngine(nullptr); + AudioNodeStream::Flags flags = + AudioNodeStream::EXTERNAL_OUTPUT | + AudioNodeStream::NEED_MAIN_THREAD_FINISHED; + mPipeStream = AudioNodeStream::Create(ctx, engine, flags, ctx->Graph()); + AudioNodeStream* ns = aSrcAudioNode.GetStream(); + if (ns) { + mInputPort = + mPipeStream->AllocateInputPort(aSrcAudioNode.GetStream(), + TRACK_ANY, TRACK_ANY, + 0, aSrcOutput); + } + } + mAudioNode = &aSrcAudioNode; + + RegisterActivityObserver(); +} + +void +MediaRecorder::RegisterActivityObserver() +{ + if (nsPIDOMWindowInner* window = GetOwner()) { + mDocument = window->GetExtantDoc(); + if (mDocument) { + mDocument->RegisterActivityObserver( + NS_ISUPPORTS_CAST(nsIDocumentActivity*, this)); + } + } +} + +void +MediaRecorder::UnRegisterActivityObserver() +{ + if (mDocument) { + mDocument->UnregisterActivityObserver( + NS_ISUPPORTS_CAST(nsIDocumentActivity*, this)); + } +} + +void +MediaRecorder::SetMimeType(const nsString &aMimeType) +{ + mMimeType = aMimeType; +} + +void +MediaRecorder::GetMimeType(nsString &aMimeType) +{ + aMimeType = mMimeType; +} + +void +MediaRecorder::Start(const Optional<int32_t>& aTimeSlice, ErrorResult& aResult) +{ + LOG(LogLevel::Debug, ("MediaRecorder.Start %p", this)); + if (mState != RecordingState::Inactive) { + aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (GetSourceMediaStream()->IsFinished() || GetSourceMediaStream()->IsDestroyed()) { + aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsTArray<RefPtr<MediaStreamTrack>> tracks; + if (mDOMStream) { + mDOMStream->GetTracks(tracks); + } + if (!tracks.IsEmpty()) { + // If there are tracks already available that we're not allowed + // to record, we should throw a security error. + bool subsumes = false; + nsPIDOMWindowInner* window; + nsIDocument* doc; + if (!(window = GetOwner()) || + !(doc = window->GetExtantDoc()) || + NS_FAILED(doc->NodePrincipal()->Subsumes( + mDOMStream->GetPrincipal(), &subsumes)) || + !subsumes) { + aResult.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + int32_t timeSlice = 0; + if (aTimeSlice.WasPassed()) { + if (aTimeSlice.Value() < 0) { + aResult.Throw(NS_ERROR_INVALID_ARG); + return; + } + + timeSlice = aTimeSlice.Value(); + } + MediaRecorderReporter::AddMediaRecorder(this); + mState = RecordingState::Recording; + // Start a session. + mSessions.AppendElement(); + mSessions.LastElement() = new Session(this, timeSlice); + mSessions.LastElement()->Start(); +} + +void +MediaRecorder::Stop(ErrorResult& aResult) +{ + LOG(LogLevel::Debug, ("MediaRecorder.Stop %p", this)); + MediaRecorderReporter::RemoveMediaRecorder(this); + if (mState == RecordingState::Inactive) { + aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + mState = RecordingState::Inactive; + MOZ_ASSERT(mSessions.Length() > 0); + mSessions.LastElement()->Stop(); +} + +void +MediaRecorder::Pause(ErrorResult& aResult) +{ + LOG(LogLevel::Debug, ("MediaRecorder.Pause")); + if (mState != RecordingState::Recording) { + aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + MOZ_ASSERT(mSessions.Length() > 0); + nsresult rv = mSessions.LastElement()->Pause(); + if (NS_FAILED(rv)) { + NotifyError(rv); + return; + } + mState = RecordingState::Paused; +} + +void +MediaRecorder::Resume(ErrorResult& aResult) +{ + LOG(LogLevel::Debug, ("MediaRecorder.Resume")); + if (mState != RecordingState::Paused) { + aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + MOZ_ASSERT(mSessions.Length() > 0); + nsresult rv = mSessions.LastElement()->Resume(); + if (NS_FAILED(rv)) { + NotifyError(rv); + return; + } + mState = RecordingState::Recording; +} + +void +MediaRecorder::RequestData(ErrorResult& aResult) +{ + if (mState != RecordingState::Recording) { + aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + MOZ_ASSERT(mSessions.Length() > 0); + nsresult rv = mSessions.LastElement()->RequestData(); + if (NS_FAILED(rv)) { + NotifyError(rv); + } +} + +JSObject* +MediaRecorder::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaRecorderBinding::Wrap(aCx, this, aGivenProto); +} + +/* static */ already_AddRefed<MediaRecorder> +MediaRecorder::Constructor(const GlobalObject& aGlobal, + DOMMediaStream& aStream, + const MediaRecorderOptions& aInitDict, + ErrorResult& aRv) +{ + nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(aGlobal.GetAsSupports()); + if (!ownerWindow) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + if (!IsTypeSupported(aInitDict.mMimeType)) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + RefPtr<MediaRecorder> object = new MediaRecorder(aStream, ownerWindow); + object->SetOptions(aInitDict); + return object.forget(); +} + +/* static */ already_AddRefed<MediaRecorder> +MediaRecorder::Constructor(const GlobalObject& aGlobal, + AudioNode& aSrcAudioNode, + uint32_t aSrcOutput, + const MediaRecorderOptions& aInitDict, + ErrorResult& aRv) +{ + // Allow recording from audio node only when pref is on. + if (!Preferences::GetBool("media.recorder.audio_node.enabled", false)) { + // Pretending that this constructor is not defined. + NS_NAMED_LITERAL_STRING(argStr, "Argument 1 of MediaRecorder.constructor"); + NS_NAMED_LITERAL_STRING(typeStr, "MediaStream"); + aRv.ThrowTypeError<MSG_DOES_NOT_IMPLEMENT_INTERFACE>(argStr, typeStr); + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(aGlobal.GetAsSupports()); + if (!ownerWindow) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // aSrcOutput doesn't matter to destination node because it has no output. + if (aSrcAudioNode.NumberOfOutputs() > 0 && + aSrcOutput >= aSrcAudioNode.NumberOfOutputs()) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + if (!IsTypeSupported(aInitDict.mMimeType)) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + RefPtr<MediaRecorder> object = new MediaRecorder(aSrcAudioNode, + aSrcOutput, + ownerWindow); + object->SetOptions(aInitDict); + return object.forget(); +} + +void +MediaRecorder::SetOptions(const MediaRecorderOptions& aInitDict) +{ + SetMimeType(aInitDict.mMimeType); + mAudioBitsPerSecond = aInitDict.mAudioBitsPerSecond.WasPassed() ? + aInitDict.mAudioBitsPerSecond.Value() : 0; + mVideoBitsPerSecond = aInitDict.mVideoBitsPerSecond.WasPassed() ? + aInitDict.mVideoBitsPerSecond.Value() : 0; + mBitsPerSecond = aInitDict.mBitsPerSecond.WasPassed() ? + aInitDict.mBitsPerSecond.Value() : 0; + // We're not handling dynamic changes yet. Eventually we'll handle + // setting audio, video and/or total -- and anything that isn't set, + // we'll derive. Calculated versions require querying bitrates after + // the encoder is Init()ed. This happens only after data is + // available and thus requires dynamic changes. + // + // Until dynamic changes are supported, I prefer to be safe and err + // slightly high + if (aInitDict.mBitsPerSecond.WasPassed() && !aInitDict.mVideoBitsPerSecond.WasPassed()) { + mVideoBitsPerSecond = mBitsPerSecond; + } +} + +static char const *const gWebMAudioEncoderCodecs[2] = { + "opus", + // no VP9 yet + nullptr, +}; +static char const *const gWebMVideoEncoderCodecs[4] = { + "opus", + "vp8", + "vp8.0", + // no VP9 yet + nullptr, +}; +static char const *const gOggAudioEncoderCodecs[2] = { + "opus", + // we could support vorbis here too, but don't + nullptr, +}; + +template <class String> +static bool +CodecListContains(char const *const * aCodecs, const String& aCodec) +{ + for (int32_t i = 0; aCodecs[i]; ++i) { + if (aCodec.EqualsASCII(aCodecs[i])) + return true; + } + return false; +} + +/* static */ +bool +MediaRecorder::IsTypeSupported(GlobalObject& aGlobal, const nsAString& aMIMEType) +{ + return IsTypeSupported(aMIMEType); +} + +/* static */ +bool +MediaRecorder::IsTypeSupported(const nsAString& aMIMEType) +{ + char const* const* codeclist = nullptr; + + if (aMIMEType.IsEmpty()) { + return true; + } + + nsContentTypeParser parser(aMIMEType); + nsAutoString mimeType; + nsresult rv = parser.GetType(mimeType); + if (NS_FAILED(rv)) { + return false; + } + + // effectively a 'switch (mimeType) {' + if (mimeType.EqualsLiteral(AUDIO_OGG)) { + if (MediaDecoder::IsOggEnabled() && MediaDecoder::IsOpusEnabled()) { + codeclist = gOggAudioEncoderCodecs; + } + } +#ifdef MOZ_WEBM_ENCODER + else if (mimeType.EqualsLiteral(VIDEO_WEBM) && + MediaEncoder::IsWebMEncoderEnabled()) { + codeclist = gWebMVideoEncoderCodecs; + } +#endif + + // codecs don't matter if we don't support the container + if (!codeclist) { + return false; + } + // now filter on codecs, and if needed rescind support + nsAutoString codecstring; + rv = parser.GetParameter("codecs", codecstring); + + nsTArray<nsString> codecs; + if (!ParseCodecsString(codecstring, codecs)) { + return false; + } + for (const nsString& codec : codecs) { + if (!CodecListContains(codeclist, codec)) { + // Totally unsupported codec + return false; + } + } + + return true; +} + +nsresult +MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob) +{ + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + + BlobEventInit init; + init.mBubbles = false; + init.mCancelable = false; + + nsCOMPtr<nsIDOMBlob> blob = aBlob; + init.mData = static_cast<Blob*>(blob.get()); + + RefPtr<BlobEvent> event = + BlobEvent::Constructor(this, + NS_LITERAL_STRING("dataavailable"), + init); + event->SetTrusted(true); + return DispatchDOMEvent(nullptr, event, nullptr, nullptr); +} + +void +MediaRecorder::DispatchSimpleEvent(const nsAString & aStr) +{ + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + nsresult rv = CheckInnerWindowCorrectness(); + if (NS_FAILED(rv)) { + return; + } + + RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr); + event->InitEvent(aStr, false, false); + event->SetTrusted(true); + + rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr); + if (NS_FAILED(rv)) { + NS_ERROR("Failed to dispatch the event!!!"); + return; + } +} + +void +MediaRecorder::NotifyError(nsresult aRv) +{ + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + nsresult rv = CheckInnerWindowCorrectness(); + if (NS_FAILED(rv)) { + return; + } + nsString errorMsg; + switch (aRv) { + case NS_ERROR_DOM_SECURITY_ERR: + errorMsg = NS_LITERAL_STRING("SecurityError"); + break; + case NS_ERROR_OUT_OF_MEMORY: + errorMsg = NS_LITERAL_STRING("OutOfMemoryError"); + break; + default: + errorMsg = NS_LITERAL_STRING("GenericError"); + } + + RecordErrorEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mName = errorMsg; + + RefPtr<RecordErrorEvent> event = + RecordErrorEvent::Constructor(this, NS_LITERAL_STRING("error"), init); + event->SetTrusted(true); + + rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr); + if (NS_FAILED(rv)) { + NS_ERROR("Failed to dispatch the error event!!!"); + return; + } + return; +} + +void +MediaRecorder::RemoveSession(Session* aSession) +{ + LOG(LogLevel::Debug, ("MediaRecorder.RemoveSession (%p)", aSession)); + mSessions.RemoveElement(aSession); +} + +void +MediaRecorder::NotifyOwnerDocumentActivityChanged() +{ + nsPIDOMWindowInner* window = GetOwner(); + NS_ENSURE_TRUE_VOID(window); + nsIDocument* doc = window->GetExtantDoc(); + NS_ENSURE_TRUE_VOID(doc); + + LOG(LogLevel::Debug, ("MediaRecorder %p document IsActive %d isVisible %d\n", + this, doc->IsActive(), doc->IsVisible())); + if (!doc->IsActive() || !doc->IsVisible()) { + // Stop the session. + ErrorResult result; + Stop(result); + result.SuppressException(); + } +} + +MediaStream* +MediaRecorder::GetSourceMediaStream() +{ + if (mDOMStream != nullptr) { + return mDOMStream->GetPlaybackStream(); + } + MOZ_ASSERT(mAudioNode != nullptr); + return mPipeStream ? mPipeStream.get() : mAudioNode->GetStream(); +} + +size_t +MediaRecorder::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + size_t amount = 42; + for (size_t i = 0; i < mSessions.Length(); ++i) { + amount += mSessions[i]->SizeOfExcludingThis(aMallocSizeOf); + } + return amount; +} + +StaticRefPtr<MediaRecorderReporter> MediaRecorderReporter::sUniqueInstance; + +MediaRecorderReporter* MediaRecorderReporter::UniqueInstance() +{ + if (!sUniqueInstance) { + sUniqueInstance = new MediaRecorderReporter(); + sUniqueInstance->InitMemoryReporter(); + } + return sUniqueInstance; + } + +void MediaRecorderReporter::InitMemoryReporter() +{ + RegisterWeakMemoryReporter(this); +} + +MediaRecorderReporter::~MediaRecorderReporter() +{ + UnregisterWeakMemoryReporter(this); +} + +} // namespace dom +} // namespace mozilla |