michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et cindent: */ 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 "MediaRecorder.h" michael@0: #include "GeneratedEvents.h" michael@0: #include "MediaEncoder.h" michael@0: #include "mozilla/DOMEventTargetHelper.h" michael@0: #include "nsError.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsIDOMRecordErrorEvent.h" michael@0: #include "nsTArray.h" michael@0: #include "DOMMediaStream.h" michael@0: #include "EncodedBufferCache.h" michael@0: #include "nsIDOMFile.h" michael@0: #include "mozilla/dom/BlobEvent.h" michael@0: #include "nsIPrincipal.h" michael@0: #include "nsMimeTypes.h" michael@0: michael@0: #include "mozilla/dom/AudioStreamTrack.h" michael@0: #include "mozilla/dom/VideoStreamTrack.h" michael@0: michael@0: #ifdef PR_LOGGING michael@0: PRLogModuleInfo* gMediaRecorderLog; michael@0: #define LOG(type, msg) PR_LOG(gMediaRecorderLog, type, msg) michael@0: #else michael@0: #define LOG(type, msg) michael@0: #endif michael@0: michael@0: namespace mozilla { michael@0: michael@0: namespace dom { michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaRecorder, DOMEventTargetHelper, michael@0: mStream) michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder) michael@0: NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) michael@0: michael@0: NS_IMPL_ADDREF_INHERITED(MediaRecorder, DOMEventTargetHelper) michael@0: NS_IMPL_RELEASE_INHERITED(MediaRecorder, DOMEventTargetHelper) michael@0: michael@0: /** michael@0: * Session is an object to represent a single recording event. michael@0: * In original design, all recording context is stored in MediaRecorder, which causes michael@0: * a problem if someone calls MediaRecoder::Stop and MedaiRecorder::Start quickly. michael@0: * To prevent blocking main thread, media encoding is executed in a second thread, michael@0: * named as Read Thread. For the same reason, we do not wait Read Thread shutdown in michael@0: * MediaRecorder::Stop. If someone call MediaRecoder::Start before Read Thread shutdown, michael@0: * the same recording context in MediaRecoder might be access by two Reading Threads, michael@0: * which cause a problem. michael@0: * In the new design, we put recording context into Session object, including Read michael@0: * Thread. Each Session has its own recording context and Read Thread, problem is been michael@0: * resolved. michael@0: * michael@0: * Life cycle of a Session object. michael@0: * 1) Initialization Stage (in main thread) michael@0: * Setup media streams in MSG, and bind MediaEncoder with Source Stream when mStream is available. michael@0: * Resource allocation, such as encoded data cache buffer and MediaEncoder. michael@0: * Create read thread. michael@0: * Automatically switch to Extract stage in the end of this stage. michael@0: * 2) Extract Stage (in Read Thread) michael@0: * Pull encoded A/V frames from MediaEncoder, dispatch to OnDataAvailable handler. michael@0: * Unless a client calls Session::Stop, Session object keeps stay in this stage. michael@0: * 3) Destroy Stage (in main thread) michael@0: * Switch from Extract stage to Destroy stage by calling Session::Stop. michael@0: * Release session resource and remove associated streams from MSG. michael@0: * michael@0: * Lifetime of a Session object. michael@0: * 1) MediaRecorder creates a Session in MediaRecorder::Start function. michael@0: * 2) A Session is destroyed in DestroyRunnable after MediaRecorder::Stop being called michael@0: * _and_ all encoded media data been passed to OnDataAvailable handler. michael@0: */ michael@0: class MediaRecorder::Session: public nsIObserver michael@0: { michael@0: NS_DECL_THREADSAFE_ISUPPORTS michael@0: michael@0: // Main thread task. michael@0: // Create a blob event and send back to client. michael@0: class PushBlobRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: PushBlobRunnable(Session* aSession) michael@0: : mSession(aSession) michael@0: { } michael@0: michael@0: NS_IMETHODIMP Run() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.PushBlobRunnable s=(%p)", mSession.get())); michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: nsRefPtr recorder = mSession->mRecorder; michael@0: if (!recorder) { michael@0: return NS_OK; michael@0: } michael@0: recorder->SetMimeType(mSession->mMimeType); michael@0: if (mSession->IsEncoderError()) { michael@0: recorder->NotifyError(NS_ERROR_UNEXPECTED); michael@0: } michael@0: nsresult rv = recorder->CreateAndDispatchBlobEvent(mSession->GetEncodedData()); michael@0: if (NS_FAILED(rv)) { michael@0: recorder->NotifyError(rv); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mSession; michael@0: }; michael@0: michael@0: // Record thread task and it run in Media Encoder thread. michael@0: // Fetch encoded Audio/Video data from MediaEncoder. michael@0: class ExtractRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: ExtractRunnable(Session *aSession) michael@0: : mSession(aSession) {} michael@0: michael@0: NS_IMETHODIMP Run() michael@0: { michael@0: MOZ_ASSERT(NS_GetCurrentThread() == mSession->mReadThread); michael@0: michael@0: mSession->Extract(); michael@0: LOG(PR_LOG_DEBUG, ("Session.ExtractRunnable shutdown = %d", mSession->mEncoder->IsShutdown())); michael@0: if (!mSession->mEncoder->IsShutdown()) { michael@0: NS_DispatchToCurrentThread(new ExtractRunnable(mSession)); michael@0: } else { michael@0: // Flush out remainding encoded data. michael@0: NS_DispatchToMainThread(new PushBlobRunnable(mSession)); michael@0: // Destroy this Session object in main thread. michael@0: NS_DispatchToMainThread(new DestroyRunnable(already_AddRefed(mSession))); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mSession; michael@0: }; michael@0: michael@0: // For Ensure recorder has tracks to record. michael@0: class TracksAvailableCallback : public DOMMediaStream::OnTracksAvailableCallback michael@0: { michael@0: public: michael@0: TracksAvailableCallback(Session *aSession) michael@0: : mSession(aSession) {} michael@0: virtual void NotifyTracksAvailable(DOMMediaStream* aStream) michael@0: { michael@0: uint8_t trackType = aStream->GetHintContents(); michael@0: // ToDo: GetHintContents return 0 when recording media tags. michael@0: if (trackType == 0) { michael@0: nsTArray > audioTracks; michael@0: aStream->GetAudioTracks(audioTracks); michael@0: nsTArray > videoTracks; michael@0: aStream->GetVideoTracks(videoTracks); michael@0: // What is inside the track michael@0: if (videoTracks.Length() > 0) { michael@0: trackType |= DOMMediaStream::HINT_CONTENTS_VIDEO; michael@0: } michael@0: if (audioTracks.Length() > 0) { michael@0: trackType |= DOMMediaStream::HINT_CONTENTS_AUDIO; michael@0: } michael@0: } michael@0: LOG(PR_LOG_DEBUG, ("Session.NotifyTracksAvailable track type = (%d)", trackType)); michael@0: mSession->AfterTracksAdded(trackType); michael@0: } michael@0: private: michael@0: nsRefPtr mSession; michael@0: }; michael@0: // Main thread task. michael@0: // To delete RecordingSession object. michael@0: class DestroyRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: DestroyRunnable(already_AddRefed&& aSession) michael@0: : mSession(aSession) {} michael@0: michael@0: NS_IMETHODIMP Run() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.DestroyRunnable session refcnt = (%d) stopIssued %d s=(%p)", michael@0: (int)mSession->mRefCnt, mSession->mStopIssued, mSession.get())); michael@0: MOZ_ASSERT(NS_IsMainThread() && mSession.get()); michael@0: nsRefPtr recorder = mSession->mRecorder; michael@0: if (!recorder) { michael@0: return NS_OK; michael@0: } michael@0: // SourceMediaStream is ended, and send out TRACK_EVENT_END notification. michael@0: // Read Thread will be terminate soon. michael@0: // We need to switch MediaRecorder to "Stop" state first to make sure michael@0: // MediaRecorder is not associated with this Session anymore, then, it's michael@0: // safe to delete this Session. michael@0: // Also avoid to run if this session already call stop before michael@0: if (!mSession->mStopIssued) { michael@0: ErrorResult result; michael@0: mSession->mStopIssued = true; michael@0: recorder->Stop(result); michael@0: NS_DispatchToMainThread(new DestroyRunnable(mSession.forget())); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Dispatch stop event and clear MIME type. michael@0: mSession->mMimeType = NS_LITERAL_STRING(""); michael@0: recorder->SetMimeType(mSession->mMimeType); michael@0: recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop")); michael@0: recorder->RemoveSession(mSession); michael@0: mSession->mRecorder = nullptr; michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: // Call mSession::Release automatically while DestroyRunnable be destroy. michael@0: nsRefPtr mSession; michael@0: }; michael@0: michael@0: friend class PushBlobRunnable; michael@0: friend class ExtractRunnable; michael@0: friend class DestroyRunnable; michael@0: friend class TracksAvailableCallback; michael@0: michael@0: public: michael@0: Session(MediaRecorder* aRecorder, int32_t aTimeSlice) michael@0: : mRecorder(aRecorder), michael@0: mTimeSlice(aTimeSlice), michael@0: mStopIssued(false) michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: AddRef(); michael@0: mEncodedBufferCache = new EncodedBufferCache(MAX_ALLOW_MEMORY_BUFFER); michael@0: mLastBlobTimeStamp = TimeStamp::Now(); michael@0: } michael@0: michael@0: // Only DestroyRunnable is allowed to delete Session object. michael@0: virtual ~Session() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.~Session (%p)", this)); michael@0: CleanupStreams(); michael@0: } michael@0: michael@0: void Start() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.Start %p", this)); michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: SetupStreams(); michael@0: } michael@0: michael@0: void Stop() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.Stop %p", this)); michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: mStopIssued = true; michael@0: CleanupStreams(); michael@0: nsContentUtils::UnregisterShutdownObserver(this); michael@0: } michael@0: michael@0: nsresult Pause() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.Pause")); michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); michael@0: mTrackUnionStream->ChangeExplicitBlockerCount(-1); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult Resume() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.Resume")); michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); michael@0: mTrackUnionStream->ChangeExplicitBlockerCount(1); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: already_AddRefed GetEncodedData() michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: return mEncodedBufferCache->ExtractBlob(mMimeType); michael@0: } michael@0: michael@0: bool IsEncoderError() michael@0: { michael@0: if (mEncoder && mEncoder->HasError()) { michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: void ForgetMediaRecorder() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.ForgetMediaRecorder (%p)", mRecorder)); michael@0: mRecorder = nullptr; michael@0: } michael@0: private: michael@0: michael@0: // Pull encoded meida data from MediaEncoder and put into EncodedBufferCache. michael@0: // Destroy this session object in the end of this function. michael@0: void Extract() michael@0: { michael@0: MOZ_ASSERT(NS_GetCurrentThread() == mReadThread); michael@0: LOG(PR_LOG_DEBUG, ("Session.Extract %p", this)); michael@0: // Whether push encoded data back to onDataAvailable automatically. michael@0: const bool pushBlob = (mTimeSlice > 0) ? true : false; michael@0: michael@0: // Pull encoded media data from MediaEncoder michael@0: nsTArray > encodedBuf; michael@0: mEncoder->GetEncodedData(&encodedBuf, mMimeType); michael@0: michael@0: // Append pulled data into cache buffer. michael@0: for (uint32_t i = 0; i < encodedBuf.Length(); i++) { michael@0: mEncodedBufferCache->AppendBuffer(encodedBuf[i]); michael@0: } michael@0: michael@0: if (pushBlob) { michael@0: if ((TimeStamp::Now() - mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice) { michael@0: NS_DispatchToMainThread(new PushBlobRunnable(this)); michael@0: mLastBlobTimeStamp = TimeStamp::Now(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Bind media source with MediaEncoder to receive raw media data. michael@0: void SetupStreams() michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: // Create a Track Union Stream michael@0: MediaStreamGraph* gm = mRecorder->mStream->GetStream()->Graph(); michael@0: mTrackUnionStream = gm->CreateTrackUnionStream(nullptr); michael@0: MOZ_ASSERT(mTrackUnionStream, "CreateTrackUnionStream failed"); michael@0: michael@0: mTrackUnionStream->SetAutofinish(true); michael@0: michael@0: // Bind this Track Union Stream with Source Media michael@0: mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT); michael@0: michael@0: // Allocate encoder and bind with the Track Union Stream. michael@0: TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(mRecorder->mSessions.LastElement()); michael@0: mRecorder->mStream->OnTracksAvailable(tracksAvailableCallback); michael@0: } michael@0: michael@0: void AfterTracksAdded(uint8_t aTrackTypes) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("Session.AfterTracksAdded %p", this)); michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: // Allocate encoder and bind with union stream. michael@0: // At this stage, the API doesn't allow UA to choose the output mimeType format. michael@0: michael@0: nsCOMPtr doc = mRecorder->GetOwner()->GetExtantDoc(); michael@0: uint16_t appStatus = nsIPrincipal::APP_STATUS_NOT_INSTALLED; michael@0: if (doc) { michael@0: doc->NodePrincipal()->GetAppStatus(&appStatus); michael@0: } michael@0: // Only allow certificated application can assign AUDIO_3GPP michael@0: if (appStatus == nsIPrincipal::APP_STATUS_CERTIFIED && michael@0: mRecorder->mMimeType.EqualsLiteral(AUDIO_3GPP)) { michael@0: mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(AUDIO_3GPP), aTrackTypes); michael@0: } else { michael@0: mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""), aTrackTypes); michael@0: } michael@0: michael@0: if (!mEncoder) { michael@0: DoSessionEndTask(NS_ERROR_ABORT); michael@0: return; michael@0: } michael@0: michael@0: // Media stream is ready but UA issues a stop method follow by start method. michael@0: // The Session::stop would clean the mTrackUnionStream. If the AfterTracksAdded michael@0: // comes after stop command, this function would crash. michael@0: if (!mTrackUnionStream) { michael@0: DoSessionEndTask(NS_OK); michael@0: return; michael@0: } michael@0: mTrackUnionStream->AddListener(mEncoder); michael@0: // Create a thread to read encode media data from MediaEncoder. michael@0: if (!mReadThread) { michael@0: nsresult rv = NS_NewNamedThread("Media Encoder", getter_AddRefs(mReadThread)); michael@0: if (NS_FAILED(rv)) { michael@0: DoSessionEndTask(rv); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // In case source media stream does not notify track end, recieve michael@0: // shutdown notification and stop Read Thread. michael@0: nsContentUtils::RegisterShutdownObserver(this); michael@0: michael@0: mReadThread->Dispatch(new ExtractRunnable(this), NS_DISPATCH_NORMAL); michael@0: } michael@0: // application should get blob and onstop event michael@0: void DoSessionEndTask(nsresult rv) michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: if (NS_FAILED(rv)) { michael@0: mRecorder->NotifyError(rv); michael@0: } michael@0: michael@0: CleanupStreams(); michael@0: // Destroy this session object in main thread. michael@0: NS_DispatchToMainThread(new PushBlobRunnable(this)); michael@0: NS_DispatchToMainThread(new DestroyRunnable(already_AddRefed(this))); michael@0: } michael@0: void CleanupStreams() michael@0: { michael@0: if (mInputPort.get()) { michael@0: mInputPort->Destroy(); michael@0: mInputPort = nullptr; michael@0: } michael@0: michael@0: if (mTrackUnionStream.get()) { michael@0: mTrackUnionStream->Destroy(); michael@0: mTrackUnionStream = nullptr; michael@0: } michael@0: } michael@0: michael@0: NS_IMETHODIMP Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: LOG(PR_LOG_DEBUG, ("Session.Observe XPCOM_SHUTDOWN %p", this)); michael@0: if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { michael@0: // Force stop Session to terminate Read Thread. michael@0: mEncoder->Cancel(); michael@0: if (mReadThread) { michael@0: mReadThread->Shutdown(); michael@0: mReadThread = nullptr; michael@0: } michael@0: if (mRecorder) { michael@0: mRecorder->RemoveSession(this); michael@0: mRecorder = nullptr; michael@0: } michael@0: Stop(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: // Hold weak a reference to MediaRecoder and can be accessed ONLY on main thread. michael@0: MediaRecorder* mRecorder; michael@0: michael@0: // Receive track data from source and dispatch to Encoder. michael@0: // Pause/ Resume controller. michael@0: nsRefPtr mTrackUnionStream; michael@0: nsRefPtr mInputPort; michael@0: michael@0: // Runnable thread for read data from MediaEncode. michael@0: nsCOMPtr mReadThread; michael@0: // MediaEncoder pipeline. michael@0: nsRefPtr mEncoder; michael@0: // A buffer to cache encoded meda data. michael@0: nsAutoPtr mEncodedBufferCache; michael@0: // Current session mimeType michael@0: nsString mMimeType; michael@0: // Timestamp of the last fired dataavailable event. michael@0: TimeStamp mLastBlobTimeStamp; michael@0: // The interval of passing encoded data from EncodedBufferCache to onDataAvailable michael@0: // handler. "mTimeSlice < 0" means Session object does not push encoded data to michael@0: // onDataAvailable, instead, it passive wait the client side pull encoded data michael@0: // by calling requestData API. michael@0: const int32_t mTimeSlice; michael@0: // Indicate this session's stop has been called. michael@0: bool mStopIssued; michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(MediaRecorder::Session, nsIObserver) michael@0: michael@0: MediaRecorder::~MediaRecorder() michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("~MediaRecorder (%p)", this)); michael@0: for (uint32_t i = 0; i < mSessions.Length(); i ++) { michael@0: if (mSessions[i]) { michael@0: mSessions[i]->ForgetMediaRecorder(); michael@0: mSessions[i]->Stop(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: MediaRecorder::MediaRecorder(DOMMediaStream& aStream, nsPIDOMWindow* aOwnerWindow) michael@0: : DOMEventTargetHelper(aOwnerWindow), michael@0: mState(RecordingState::Inactive), michael@0: mMutex("Session.Data.Mutex") michael@0: { michael@0: MOZ_ASSERT(aOwnerWindow); michael@0: MOZ_ASSERT(aOwnerWindow->IsInnerWindow()); michael@0: mStream = &aStream; michael@0: #ifdef PR_LOGGING michael@0: if (!gMediaRecorderLog) { michael@0: gMediaRecorderLog = PR_NewLogModule("MediaRecorder"); michael@0: } michael@0: #endif michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::SetMimeType(const nsString &aMimeType) michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: mMimeType = aMimeType; michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::GetMimeType(nsString &aMimeType) michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: aMimeType = mMimeType; michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::Start(const Optional& aTimeSlice, ErrorResult& aResult) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("MediaRecorder.Start %p", this)); michael@0: if (mState != RecordingState::Inactive) { michael@0: aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: michael@0: if (mStream->GetStream()->IsFinished() || mStream->GetStream()->IsDestroyed()) { michael@0: aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: michael@0: if (!mStream->GetPrincipal()) { michael@0: aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: michael@0: if (!CheckPrincipal()) { michael@0: aResult.Throw(NS_ERROR_DOM_SECURITY_ERR); michael@0: return; michael@0: } michael@0: michael@0: int32_t timeSlice = 0; michael@0: if (aTimeSlice.WasPassed()) { michael@0: if (aTimeSlice.Value() < 0) { michael@0: aResult.Throw(NS_ERROR_INVALID_ARG); michael@0: return; michael@0: } michael@0: michael@0: timeSlice = aTimeSlice.Value(); michael@0: } michael@0: michael@0: mState = RecordingState::Recording; michael@0: // Start a session michael@0: michael@0: mSessions.AppendElement(); michael@0: mSessions.LastElement() = new Session(this, timeSlice); michael@0: mSessions.LastElement()->Start(); michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::Stop(ErrorResult& aResult) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("MediaRecorder.Stop %p", this)); michael@0: if (mState == RecordingState::Inactive) { michael@0: aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: mState = RecordingState::Inactive; michael@0: if (mSessions.Length() > 0) { michael@0: mSessions.LastElement()->Stop(); michael@0: } michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::Pause(ErrorResult& aResult) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("MediaRecorder.Pause")); michael@0: if (mState != RecordingState::Recording) { michael@0: aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: michael@0: MOZ_ASSERT(mSessions.Length() > 0); michael@0: nsresult rv = mSessions.LastElement()->Pause(); michael@0: if (NS_FAILED(rv)) { michael@0: NotifyError(rv); michael@0: return; michael@0: } michael@0: mState = RecordingState::Paused; michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::Resume(ErrorResult& aResult) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("MediaRecorder.Resume")); michael@0: if (mState != RecordingState::Paused) { michael@0: aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: michael@0: MOZ_ASSERT(mSessions.Length() > 0); michael@0: nsresult rv = mSessions.LastElement()->Resume(); michael@0: if (NS_FAILED(rv)) { michael@0: NotifyError(rv); michael@0: return; michael@0: } michael@0: mState = RecordingState::Recording; michael@0: } michael@0: michael@0: class CreateAndDispatchBlobEventRunnable : public nsRunnable { michael@0: nsCOMPtr mBlob; michael@0: nsRefPtr mRecorder; michael@0: public: michael@0: CreateAndDispatchBlobEventRunnable(already_AddRefed&& aBlob, michael@0: MediaRecorder* aRecorder) michael@0: : mBlob(aBlob), mRecorder(aRecorder) michael@0: { } michael@0: michael@0: NS_IMETHOD michael@0: Run(); michael@0: }; michael@0: michael@0: NS_IMETHODIMP michael@0: CreateAndDispatchBlobEventRunnable::Run() michael@0: { michael@0: return mRecorder->CreateAndDispatchBlobEvent(mBlob.forget()); michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::RequestData(ErrorResult& aResult) michael@0: { michael@0: if (mState != RecordingState::Recording) { michael@0: aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); michael@0: return; michael@0: } michael@0: michael@0: NS_DispatchToMainThread( michael@0: new CreateAndDispatchBlobEventRunnable(mSessions.LastElement()->GetEncodedData(), michael@0: this), michael@0: NS_DISPATCH_NORMAL); michael@0: } michael@0: michael@0: JSObject* michael@0: MediaRecorder::WrapObject(JSContext* aCx) michael@0: { michael@0: return MediaRecorderBinding::Wrap(aCx, this); michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: MediaRecorder::Constructor(const GlobalObject& aGlobal, michael@0: DOMMediaStream& aStream, michael@0: const MediaRecorderOptions& aInitDict, michael@0: ErrorResult& aRv) michael@0: { michael@0: nsCOMPtr sgo = do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!sgo) { michael@0: aRv.Throw(NS_ERROR_FAILURE); michael@0: return nullptr; michael@0: } michael@0: michael@0: nsCOMPtr ownerWindow = do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!ownerWindow) { michael@0: aRv.Throw(NS_ERROR_FAILURE); michael@0: return nullptr; michael@0: } michael@0: michael@0: nsRefPtr object = new MediaRecorder(aStream, ownerWindow); michael@0: object->SetMimeType(aInitDict.mMimeType); michael@0: return object.forget(); michael@0: } michael@0: michael@0: nsresult michael@0: MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed&& aBlob) michael@0: { michael@0: NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); michael@0: if (!CheckPrincipal()) { michael@0: // Media is not same-origin, don't allow the data out. michael@0: nsRefPtr blob = aBlob; michael@0: return NS_ERROR_DOM_SECURITY_ERR; michael@0: } michael@0: BlobEventInit init; michael@0: init.mBubbles = false; michael@0: init.mCancelable = false; michael@0: init.mData = aBlob; michael@0: nsRefPtr event = michael@0: BlobEvent::Constructor(this, michael@0: NS_LITERAL_STRING("dataavailable"), michael@0: init); michael@0: event->SetTrusted(true); michael@0: return DispatchDOMEvent(nullptr, event, nullptr, nullptr); michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::DispatchSimpleEvent(const nsAString & aStr) michael@0: { michael@0: NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); michael@0: nsresult rv = CheckInnerWindowCorrectness(); michael@0: if (NS_FAILED(rv)) { michael@0: return; michael@0: } michael@0: michael@0: nsCOMPtr event; michael@0: rv = NS_NewDOMEvent(getter_AddRefs(event), this, nullptr, nullptr); michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Failed to create the error event!!!"); michael@0: return; michael@0: } michael@0: rv = event->InitEvent(aStr, false, false); michael@0: michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Failed to init the error event!!!"); michael@0: return; michael@0: } michael@0: michael@0: event->SetTrusted(true); michael@0: michael@0: rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr); michael@0: if (NS_FAILED(rv)) { michael@0: NS_ERROR("Failed to dispatch the event!!!"); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::NotifyError(nsresult aRv) michael@0: { michael@0: NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); michael@0: nsresult rv = CheckInnerWindowCorrectness(); michael@0: if (NS_FAILED(rv)) { michael@0: return; michael@0: } michael@0: nsString errorMsg; michael@0: switch (aRv) { michael@0: case NS_ERROR_DOM_SECURITY_ERR: michael@0: errorMsg = NS_LITERAL_STRING("SecurityError"); michael@0: break; michael@0: case NS_ERROR_OUT_OF_MEMORY: michael@0: errorMsg = NS_LITERAL_STRING("OutOfMemoryError"); michael@0: break; michael@0: default: michael@0: errorMsg = NS_LITERAL_STRING("GenericError"); michael@0: } michael@0: michael@0: nsCOMPtr event; michael@0: rv = NS_NewDOMRecordErrorEvent(getter_AddRefs(event), this, nullptr, nullptr); michael@0: michael@0: nsCOMPtr errorEvent = do_QueryInterface(event); michael@0: rv = errorEvent->InitRecordErrorEvent(NS_LITERAL_STRING("error"), michael@0: false, false, errorMsg); michael@0: michael@0: event->SetTrusted(true); michael@0: rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr); michael@0: if (NS_FAILED(rv)) { michael@0: NS_ERROR("Failed to dispatch the error event!!!"); michael@0: return; michael@0: } michael@0: return; michael@0: } michael@0: michael@0: bool MediaRecorder::CheckPrincipal() michael@0: { michael@0: NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); michael@0: if (!mStream) { michael@0: return false; michael@0: } michael@0: nsCOMPtr principal = mStream->GetPrincipal(); michael@0: if (!GetOwner()) michael@0: return false; michael@0: nsCOMPtr doc = GetOwner()->GetExtantDoc(); michael@0: if (!doc || !principal) michael@0: return false; michael@0: michael@0: bool subsumes; michael@0: if (NS_FAILED(doc->NodePrincipal()->Subsumes(principal, &subsumes))) michael@0: return false; michael@0: michael@0: return subsumes; michael@0: } michael@0: michael@0: void michael@0: MediaRecorder::RemoveSession(Session* aSession) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("MediaRecorder.RemoveSession (%p)", aSession)); michael@0: mSessions.RemoveElement(aSession); michael@0: } michael@0: michael@0: } michael@0: }