Fri, 16 Jan 2015 04:50:19 +0100
Replace accessor implementation with direct member state manipulation, by
request https://trac.torproject.org/projects/tor/ticket/9701#comment:32
michael@0 | 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim:set ts=2 sw=2 sts=2 et cindent: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | #include "MediaRecorder.h" |
michael@0 | 8 | #include "GeneratedEvents.h" |
michael@0 | 9 | #include "MediaEncoder.h" |
michael@0 | 10 | #include "mozilla/DOMEventTargetHelper.h" |
michael@0 | 11 | #include "nsError.h" |
michael@0 | 12 | #include "nsIDocument.h" |
michael@0 | 13 | #include "nsIDOMRecordErrorEvent.h" |
michael@0 | 14 | #include "nsTArray.h" |
michael@0 | 15 | #include "DOMMediaStream.h" |
michael@0 | 16 | #include "EncodedBufferCache.h" |
michael@0 | 17 | #include "nsIDOMFile.h" |
michael@0 | 18 | #include "mozilla/dom/BlobEvent.h" |
michael@0 | 19 | #include "nsIPrincipal.h" |
michael@0 | 20 | #include "nsMimeTypes.h" |
michael@0 | 21 | |
michael@0 | 22 | #include "mozilla/dom/AudioStreamTrack.h" |
michael@0 | 23 | #include "mozilla/dom/VideoStreamTrack.h" |
michael@0 | 24 | |
michael@0 | 25 | #ifdef PR_LOGGING |
michael@0 | 26 | PRLogModuleInfo* gMediaRecorderLog; |
michael@0 | 27 | #define LOG(type, msg) PR_LOG(gMediaRecorderLog, type, msg) |
michael@0 | 28 | #else |
michael@0 | 29 | #define LOG(type, msg) |
michael@0 | 30 | #endif |
michael@0 | 31 | |
michael@0 | 32 | namespace mozilla { |
michael@0 | 33 | |
michael@0 | 34 | namespace dom { |
michael@0 | 35 | |
michael@0 | 36 | NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaRecorder, DOMEventTargetHelper, |
michael@0 | 37 | mStream) |
michael@0 | 38 | |
michael@0 | 39 | NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder) |
michael@0 | 40 | NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) |
michael@0 | 41 | |
michael@0 | 42 | NS_IMPL_ADDREF_INHERITED(MediaRecorder, DOMEventTargetHelper) |
michael@0 | 43 | NS_IMPL_RELEASE_INHERITED(MediaRecorder, DOMEventTargetHelper) |
michael@0 | 44 | |
michael@0 | 45 | /** |
michael@0 | 46 | * Session is an object to represent a single recording event. |
michael@0 | 47 | * In original design, all recording context is stored in MediaRecorder, which causes |
michael@0 | 48 | * a problem if someone calls MediaRecoder::Stop and MedaiRecorder::Start quickly. |
michael@0 | 49 | * To prevent blocking main thread, media encoding is executed in a second thread, |
michael@0 | 50 | * named as Read Thread. For the same reason, we do not wait Read Thread shutdown in |
michael@0 | 51 | * MediaRecorder::Stop. If someone call MediaRecoder::Start before Read Thread shutdown, |
michael@0 | 52 | * the same recording context in MediaRecoder might be access by two Reading Threads, |
michael@0 | 53 | * which cause a problem. |
michael@0 | 54 | * In the new design, we put recording context into Session object, including Read |
michael@0 | 55 | * Thread. Each Session has its own recording context and Read Thread, problem is been |
michael@0 | 56 | * resolved. |
michael@0 | 57 | * |
michael@0 | 58 | * Life cycle of a Session object. |
michael@0 | 59 | * 1) Initialization Stage (in main thread) |
michael@0 | 60 | * Setup media streams in MSG, and bind MediaEncoder with Source Stream when mStream is available. |
michael@0 | 61 | * Resource allocation, such as encoded data cache buffer and MediaEncoder. |
michael@0 | 62 | * Create read thread. |
michael@0 | 63 | * Automatically switch to Extract stage in the end of this stage. |
michael@0 | 64 | * 2) Extract Stage (in Read Thread) |
michael@0 | 65 | * Pull encoded A/V frames from MediaEncoder, dispatch to OnDataAvailable handler. |
michael@0 | 66 | * Unless a client calls Session::Stop, Session object keeps stay in this stage. |
michael@0 | 67 | * 3) Destroy Stage (in main thread) |
michael@0 | 68 | * Switch from Extract stage to Destroy stage by calling Session::Stop. |
michael@0 | 69 | * Release session resource and remove associated streams from MSG. |
michael@0 | 70 | * |
michael@0 | 71 | * Lifetime of a Session object. |
michael@0 | 72 | * 1) MediaRecorder creates a Session in MediaRecorder::Start function. |
michael@0 | 73 | * 2) A Session is destroyed in DestroyRunnable after MediaRecorder::Stop being called |
michael@0 | 74 | * _and_ all encoded media data been passed to OnDataAvailable handler. |
michael@0 | 75 | */ |
michael@0 | 76 | class MediaRecorder::Session: public nsIObserver |
michael@0 | 77 | { |
michael@0 | 78 | NS_DECL_THREADSAFE_ISUPPORTS |
michael@0 | 79 | |
michael@0 | 80 | // Main thread task. |
michael@0 | 81 | // Create a blob event and send back to client. |
michael@0 | 82 | class PushBlobRunnable : public nsRunnable |
michael@0 | 83 | { |
michael@0 | 84 | public: |
michael@0 | 85 | PushBlobRunnable(Session* aSession) |
michael@0 | 86 | : mSession(aSession) |
michael@0 | 87 | { } |
michael@0 | 88 | |
michael@0 | 89 | NS_IMETHODIMP Run() |
michael@0 | 90 | { |
michael@0 | 91 | LOG(PR_LOG_DEBUG, ("Session.PushBlobRunnable s=(%p)", mSession.get())); |
michael@0 | 92 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 93 | |
michael@0 | 94 | nsRefPtr<MediaRecorder> recorder = mSession->mRecorder; |
michael@0 | 95 | if (!recorder) { |
michael@0 | 96 | return NS_OK; |
michael@0 | 97 | } |
michael@0 | 98 | recorder->SetMimeType(mSession->mMimeType); |
michael@0 | 99 | if (mSession->IsEncoderError()) { |
michael@0 | 100 | recorder->NotifyError(NS_ERROR_UNEXPECTED); |
michael@0 | 101 | } |
michael@0 | 102 | nsresult rv = recorder->CreateAndDispatchBlobEvent(mSession->GetEncodedData()); |
michael@0 | 103 | if (NS_FAILED(rv)) { |
michael@0 | 104 | recorder->NotifyError(rv); |
michael@0 | 105 | } |
michael@0 | 106 | |
michael@0 | 107 | return NS_OK; |
michael@0 | 108 | } |
michael@0 | 109 | |
michael@0 | 110 | private: |
michael@0 | 111 | nsRefPtr<Session> mSession; |
michael@0 | 112 | }; |
michael@0 | 113 | |
michael@0 | 114 | // Record thread task and it run in Media Encoder thread. |
michael@0 | 115 | // Fetch encoded Audio/Video data from MediaEncoder. |
michael@0 | 116 | class ExtractRunnable : public nsRunnable |
michael@0 | 117 | { |
michael@0 | 118 | public: |
michael@0 | 119 | ExtractRunnable(Session *aSession) |
michael@0 | 120 | : mSession(aSession) {} |
michael@0 | 121 | |
michael@0 | 122 | NS_IMETHODIMP Run() |
michael@0 | 123 | { |
michael@0 | 124 | MOZ_ASSERT(NS_GetCurrentThread() == mSession->mReadThread); |
michael@0 | 125 | |
michael@0 | 126 | mSession->Extract(); |
michael@0 | 127 | LOG(PR_LOG_DEBUG, ("Session.ExtractRunnable shutdown = %d", mSession->mEncoder->IsShutdown())); |
michael@0 | 128 | if (!mSession->mEncoder->IsShutdown()) { |
michael@0 | 129 | NS_DispatchToCurrentThread(new ExtractRunnable(mSession)); |
michael@0 | 130 | } else { |
michael@0 | 131 | // Flush out remainding encoded data. |
michael@0 | 132 | NS_DispatchToMainThread(new PushBlobRunnable(mSession)); |
michael@0 | 133 | // Destroy this Session object in main thread. |
michael@0 | 134 | NS_DispatchToMainThread(new DestroyRunnable(already_AddRefed<Session>(mSession))); |
michael@0 | 135 | } |
michael@0 | 136 | return NS_OK; |
michael@0 | 137 | } |
michael@0 | 138 | |
michael@0 | 139 | private: |
michael@0 | 140 | nsRefPtr<Session> mSession; |
michael@0 | 141 | }; |
michael@0 | 142 | |
michael@0 | 143 | // For Ensure recorder has tracks to record. |
michael@0 | 144 | class TracksAvailableCallback : public DOMMediaStream::OnTracksAvailableCallback |
michael@0 | 145 | { |
michael@0 | 146 | public: |
michael@0 | 147 | TracksAvailableCallback(Session *aSession) |
michael@0 | 148 | : mSession(aSession) {} |
michael@0 | 149 | virtual void NotifyTracksAvailable(DOMMediaStream* aStream) |
michael@0 | 150 | { |
michael@0 | 151 | uint8_t trackType = aStream->GetHintContents(); |
michael@0 | 152 | // ToDo: GetHintContents return 0 when recording media tags. |
michael@0 | 153 | if (trackType == 0) { |
michael@0 | 154 | nsTArray<nsRefPtr<mozilla::dom::AudioStreamTrack> > audioTracks; |
michael@0 | 155 | aStream->GetAudioTracks(audioTracks); |
michael@0 | 156 | nsTArray<nsRefPtr<mozilla::dom::VideoStreamTrack> > videoTracks; |
michael@0 | 157 | aStream->GetVideoTracks(videoTracks); |
michael@0 | 158 | // What is inside the track |
michael@0 | 159 | if (videoTracks.Length() > 0) { |
michael@0 | 160 | trackType |= DOMMediaStream::HINT_CONTENTS_VIDEO; |
michael@0 | 161 | } |
michael@0 | 162 | if (audioTracks.Length() > 0) { |
michael@0 | 163 | trackType |= DOMMediaStream::HINT_CONTENTS_AUDIO; |
michael@0 | 164 | } |
michael@0 | 165 | } |
michael@0 | 166 | LOG(PR_LOG_DEBUG, ("Session.NotifyTracksAvailable track type = (%d)", trackType)); |
michael@0 | 167 | mSession->AfterTracksAdded(trackType); |
michael@0 | 168 | } |
michael@0 | 169 | private: |
michael@0 | 170 | nsRefPtr<Session> mSession; |
michael@0 | 171 | }; |
michael@0 | 172 | // Main thread task. |
michael@0 | 173 | // To delete RecordingSession object. |
michael@0 | 174 | class DestroyRunnable : public nsRunnable |
michael@0 | 175 | { |
michael@0 | 176 | public: |
michael@0 | 177 | DestroyRunnable(already_AddRefed<Session>&& aSession) |
michael@0 | 178 | : mSession(aSession) {} |
michael@0 | 179 | |
michael@0 | 180 | NS_IMETHODIMP Run() |
michael@0 | 181 | { |
michael@0 | 182 | LOG(PR_LOG_DEBUG, ("Session.DestroyRunnable session refcnt = (%d) stopIssued %d s=(%p)", |
michael@0 | 183 | (int)mSession->mRefCnt, mSession->mStopIssued, mSession.get())); |
michael@0 | 184 | MOZ_ASSERT(NS_IsMainThread() && mSession.get()); |
michael@0 | 185 | nsRefPtr<MediaRecorder> recorder = mSession->mRecorder; |
michael@0 | 186 | if (!recorder) { |
michael@0 | 187 | return NS_OK; |
michael@0 | 188 | } |
michael@0 | 189 | // SourceMediaStream is ended, and send out TRACK_EVENT_END notification. |
michael@0 | 190 | // Read Thread will be terminate soon. |
michael@0 | 191 | // We need to switch MediaRecorder to "Stop" state first to make sure |
michael@0 | 192 | // MediaRecorder is not associated with this Session anymore, then, it's |
michael@0 | 193 | // safe to delete this Session. |
michael@0 | 194 | // Also avoid to run if this session already call stop before |
michael@0 | 195 | if (!mSession->mStopIssued) { |
michael@0 | 196 | ErrorResult result; |
michael@0 | 197 | mSession->mStopIssued = true; |
michael@0 | 198 | recorder->Stop(result); |
michael@0 | 199 | NS_DispatchToMainThread(new DestroyRunnable(mSession.forget())); |
michael@0 | 200 | return NS_OK; |
michael@0 | 201 | } |
michael@0 | 202 | |
michael@0 | 203 | // Dispatch stop event and clear MIME type. |
michael@0 | 204 | mSession->mMimeType = NS_LITERAL_STRING(""); |
michael@0 | 205 | recorder->SetMimeType(mSession->mMimeType); |
michael@0 | 206 | recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop")); |
michael@0 | 207 | recorder->RemoveSession(mSession); |
michael@0 | 208 | mSession->mRecorder = nullptr; |
michael@0 | 209 | return NS_OK; |
michael@0 | 210 | } |
michael@0 | 211 | |
michael@0 | 212 | private: |
michael@0 | 213 | // Call mSession::Release automatically while DestroyRunnable be destroy. |
michael@0 | 214 | nsRefPtr<Session> mSession; |
michael@0 | 215 | }; |
michael@0 | 216 | |
michael@0 | 217 | friend class PushBlobRunnable; |
michael@0 | 218 | friend class ExtractRunnable; |
michael@0 | 219 | friend class DestroyRunnable; |
michael@0 | 220 | friend class TracksAvailableCallback; |
michael@0 | 221 | |
michael@0 | 222 | public: |
michael@0 | 223 | Session(MediaRecorder* aRecorder, int32_t aTimeSlice) |
michael@0 | 224 | : mRecorder(aRecorder), |
michael@0 | 225 | mTimeSlice(aTimeSlice), |
michael@0 | 226 | mStopIssued(false) |
michael@0 | 227 | { |
michael@0 | 228 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 229 | |
michael@0 | 230 | AddRef(); |
michael@0 | 231 | mEncodedBufferCache = new EncodedBufferCache(MAX_ALLOW_MEMORY_BUFFER); |
michael@0 | 232 | mLastBlobTimeStamp = TimeStamp::Now(); |
michael@0 | 233 | } |
michael@0 | 234 | |
michael@0 | 235 | // Only DestroyRunnable is allowed to delete Session object. |
michael@0 | 236 | virtual ~Session() |
michael@0 | 237 | { |
michael@0 | 238 | LOG(PR_LOG_DEBUG, ("Session.~Session (%p)", this)); |
michael@0 | 239 | CleanupStreams(); |
michael@0 | 240 | } |
michael@0 | 241 | |
michael@0 | 242 | void Start() |
michael@0 | 243 | { |
michael@0 | 244 | LOG(PR_LOG_DEBUG, ("Session.Start %p", this)); |
michael@0 | 245 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 246 | |
michael@0 | 247 | SetupStreams(); |
michael@0 | 248 | } |
michael@0 | 249 | |
michael@0 | 250 | void Stop() |
michael@0 | 251 | { |
michael@0 | 252 | LOG(PR_LOG_DEBUG, ("Session.Stop %p", this)); |
michael@0 | 253 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 254 | mStopIssued = true; |
michael@0 | 255 | CleanupStreams(); |
michael@0 | 256 | nsContentUtils::UnregisterShutdownObserver(this); |
michael@0 | 257 | } |
michael@0 | 258 | |
michael@0 | 259 | nsresult Pause() |
michael@0 | 260 | { |
michael@0 | 261 | LOG(PR_LOG_DEBUG, ("Session.Pause")); |
michael@0 | 262 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 263 | |
michael@0 | 264 | NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); |
michael@0 | 265 | mTrackUnionStream->ChangeExplicitBlockerCount(-1); |
michael@0 | 266 | |
michael@0 | 267 | return NS_OK; |
michael@0 | 268 | } |
michael@0 | 269 | |
michael@0 | 270 | nsresult Resume() |
michael@0 | 271 | { |
michael@0 | 272 | LOG(PR_LOG_DEBUG, ("Session.Resume")); |
michael@0 | 273 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 274 | |
michael@0 | 275 | NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); |
michael@0 | 276 | mTrackUnionStream->ChangeExplicitBlockerCount(1); |
michael@0 | 277 | |
michael@0 | 278 | return NS_OK; |
michael@0 | 279 | } |
michael@0 | 280 | |
michael@0 | 281 | already_AddRefed<nsIDOMBlob> GetEncodedData() |
michael@0 | 282 | { |
michael@0 | 283 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 284 | return mEncodedBufferCache->ExtractBlob(mMimeType); |
michael@0 | 285 | } |
michael@0 | 286 | |
michael@0 | 287 | bool IsEncoderError() |
michael@0 | 288 | { |
michael@0 | 289 | if (mEncoder && mEncoder->HasError()) { |
michael@0 | 290 | return true; |
michael@0 | 291 | } |
michael@0 | 292 | return false; |
michael@0 | 293 | } |
michael@0 | 294 | void ForgetMediaRecorder() |
michael@0 | 295 | { |
michael@0 | 296 | LOG(PR_LOG_DEBUG, ("Session.ForgetMediaRecorder (%p)", mRecorder)); |
michael@0 | 297 | mRecorder = nullptr; |
michael@0 | 298 | } |
michael@0 | 299 | private: |
michael@0 | 300 | |
michael@0 | 301 | // Pull encoded meida data from MediaEncoder and put into EncodedBufferCache. |
michael@0 | 302 | // Destroy this session object in the end of this function. |
michael@0 | 303 | void Extract() |
michael@0 | 304 | { |
michael@0 | 305 | MOZ_ASSERT(NS_GetCurrentThread() == mReadThread); |
michael@0 | 306 | LOG(PR_LOG_DEBUG, ("Session.Extract %p", this)); |
michael@0 | 307 | // Whether push encoded data back to onDataAvailable automatically. |
michael@0 | 308 | const bool pushBlob = (mTimeSlice > 0) ? true : false; |
michael@0 | 309 | |
michael@0 | 310 | // Pull encoded media data from MediaEncoder |
michael@0 | 311 | nsTArray<nsTArray<uint8_t> > encodedBuf; |
michael@0 | 312 | mEncoder->GetEncodedData(&encodedBuf, mMimeType); |
michael@0 | 313 | |
michael@0 | 314 | // Append pulled data into cache buffer. |
michael@0 | 315 | for (uint32_t i = 0; i < encodedBuf.Length(); i++) { |
michael@0 | 316 | mEncodedBufferCache->AppendBuffer(encodedBuf[i]); |
michael@0 | 317 | } |
michael@0 | 318 | |
michael@0 | 319 | if (pushBlob) { |
michael@0 | 320 | if ((TimeStamp::Now() - mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice) { |
michael@0 | 321 | NS_DispatchToMainThread(new PushBlobRunnable(this)); |
michael@0 | 322 | mLastBlobTimeStamp = TimeStamp::Now(); |
michael@0 | 323 | } |
michael@0 | 324 | } |
michael@0 | 325 | } |
michael@0 | 326 | |
michael@0 | 327 | // Bind media source with MediaEncoder to receive raw media data. |
michael@0 | 328 | void SetupStreams() |
michael@0 | 329 | { |
michael@0 | 330 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 331 | |
michael@0 | 332 | // Create a Track Union Stream |
michael@0 | 333 | MediaStreamGraph* gm = mRecorder->mStream->GetStream()->Graph(); |
michael@0 | 334 | mTrackUnionStream = gm->CreateTrackUnionStream(nullptr); |
michael@0 | 335 | MOZ_ASSERT(mTrackUnionStream, "CreateTrackUnionStream failed"); |
michael@0 | 336 | |
michael@0 | 337 | mTrackUnionStream->SetAutofinish(true); |
michael@0 | 338 | |
michael@0 | 339 | // Bind this Track Union Stream with Source Media |
michael@0 | 340 | mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT); |
michael@0 | 341 | |
michael@0 | 342 | // Allocate encoder and bind with the Track Union Stream. |
michael@0 | 343 | TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(mRecorder->mSessions.LastElement()); |
michael@0 | 344 | mRecorder->mStream->OnTracksAvailable(tracksAvailableCallback); |
michael@0 | 345 | } |
michael@0 | 346 | |
michael@0 | 347 | void AfterTracksAdded(uint8_t aTrackTypes) |
michael@0 | 348 | { |
michael@0 | 349 | LOG(PR_LOG_DEBUG, ("Session.AfterTracksAdded %p", this)); |
michael@0 | 350 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 351 | |
michael@0 | 352 | // Allocate encoder and bind with union stream. |
michael@0 | 353 | // At this stage, the API doesn't allow UA to choose the output mimeType format. |
michael@0 | 354 | |
michael@0 | 355 | nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc(); |
michael@0 | 356 | uint16_t appStatus = nsIPrincipal::APP_STATUS_NOT_INSTALLED; |
michael@0 | 357 | if (doc) { |
michael@0 | 358 | doc->NodePrincipal()->GetAppStatus(&appStatus); |
michael@0 | 359 | } |
michael@0 | 360 | // Only allow certificated application can assign AUDIO_3GPP |
michael@0 | 361 | if (appStatus == nsIPrincipal::APP_STATUS_CERTIFIED && |
michael@0 | 362 | mRecorder->mMimeType.EqualsLiteral(AUDIO_3GPP)) { |
michael@0 | 363 | mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(AUDIO_3GPP), aTrackTypes); |
michael@0 | 364 | } else { |
michael@0 | 365 | mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""), aTrackTypes); |
michael@0 | 366 | } |
michael@0 | 367 | |
michael@0 | 368 | if (!mEncoder) { |
michael@0 | 369 | DoSessionEndTask(NS_ERROR_ABORT); |
michael@0 | 370 | return; |
michael@0 | 371 | } |
michael@0 | 372 | |
michael@0 | 373 | // Media stream is ready but UA issues a stop method follow by start method. |
michael@0 | 374 | // The Session::stop would clean the mTrackUnionStream. If the AfterTracksAdded |
michael@0 | 375 | // comes after stop command, this function would crash. |
michael@0 | 376 | if (!mTrackUnionStream) { |
michael@0 | 377 | DoSessionEndTask(NS_OK); |
michael@0 | 378 | return; |
michael@0 | 379 | } |
michael@0 | 380 | mTrackUnionStream->AddListener(mEncoder); |
michael@0 | 381 | // Create a thread to read encode media data from MediaEncoder. |
michael@0 | 382 | if (!mReadThread) { |
michael@0 | 383 | nsresult rv = NS_NewNamedThread("Media Encoder", getter_AddRefs(mReadThread)); |
michael@0 | 384 | if (NS_FAILED(rv)) { |
michael@0 | 385 | DoSessionEndTask(rv); |
michael@0 | 386 | return; |
michael@0 | 387 | } |
michael@0 | 388 | } |
michael@0 | 389 | |
michael@0 | 390 | // In case source media stream does not notify track end, recieve |
michael@0 | 391 | // shutdown notification and stop Read Thread. |
michael@0 | 392 | nsContentUtils::RegisterShutdownObserver(this); |
michael@0 | 393 | |
michael@0 | 394 | mReadThread->Dispatch(new ExtractRunnable(this), NS_DISPATCH_NORMAL); |
michael@0 | 395 | } |
michael@0 | 396 | // application should get blob and onstop event |
michael@0 | 397 | void DoSessionEndTask(nsresult rv) |
michael@0 | 398 | { |
michael@0 | 399 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 400 | if (NS_FAILED(rv)) { |
michael@0 | 401 | mRecorder->NotifyError(rv); |
michael@0 | 402 | } |
michael@0 | 403 | |
michael@0 | 404 | CleanupStreams(); |
michael@0 | 405 | // Destroy this session object in main thread. |
michael@0 | 406 | NS_DispatchToMainThread(new PushBlobRunnable(this)); |
michael@0 | 407 | NS_DispatchToMainThread(new DestroyRunnable(already_AddRefed<Session>(this))); |
michael@0 | 408 | } |
michael@0 | 409 | void CleanupStreams() |
michael@0 | 410 | { |
michael@0 | 411 | if (mInputPort.get()) { |
michael@0 | 412 | mInputPort->Destroy(); |
michael@0 | 413 | mInputPort = nullptr; |
michael@0 | 414 | } |
michael@0 | 415 | |
michael@0 | 416 | if (mTrackUnionStream.get()) { |
michael@0 | 417 | mTrackUnionStream->Destroy(); |
michael@0 | 418 | mTrackUnionStream = nullptr; |
michael@0 | 419 | } |
michael@0 | 420 | } |
michael@0 | 421 | |
michael@0 | 422 | NS_IMETHODIMP Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) |
michael@0 | 423 | { |
michael@0 | 424 | MOZ_ASSERT(NS_IsMainThread()); |
michael@0 | 425 | LOG(PR_LOG_DEBUG, ("Session.Observe XPCOM_SHUTDOWN %p", this)); |
michael@0 | 426 | if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { |
michael@0 | 427 | // Force stop Session to terminate Read Thread. |
michael@0 | 428 | mEncoder->Cancel(); |
michael@0 | 429 | if (mReadThread) { |
michael@0 | 430 | mReadThread->Shutdown(); |
michael@0 | 431 | mReadThread = nullptr; |
michael@0 | 432 | } |
michael@0 | 433 | if (mRecorder) { |
michael@0 | 434 | mRecorder->RemoveSession(this); |
michael@0 | 435 | mRecorder = nullptr; |
michael@0 | 436 | } |
michael@0 | 437 | Stop(); |
michael@0 | 438 | } |
michael@0 | 439 | |
michael@0 | 440 | return NS_OK; |
michael@0 | 441 | } |
michael@0 | 442 | |
michael@0 | 443 | private: |
michael@0 | 444 | // Hold weak a reference to MediaRecoder and can be accessed ONLY on main thread. |
michael@0 | 445 | MediaRecorder* mRecorder; |
michael@0 | 446 | |
michael@0 | 447 | // Receive track data from source and dispatch to Encoder. |
michael@0 | 448 | // Pause/ Resume controller. |
michael@0 | 449 | nsRefPtr<ProcessedMediaStream> mTrackUnionStream; |
michael@0 | 450 | nsRefPtr<MediaInputPort> mInputPort; |
michael@0 | 451 | |
michael@0 | 452 | // Runnable thread for read data from MediaEncode. |
michael@0 | 453 | nsCOMPtr<nsIThread> mReadThread; |
michael@0 | 454 | // MediaEncoder pipeline. |
michael@0 | 455 | nsRefPtr<MediaEncoder> mEncoder; |
michael@0 | 456 | // A buffer to cache encoded meda data. |
michael@0 | 457 | nsAutoPtr<EncodedBufferCache> mEncodedBufferCache; |
michael@0 | 458 | // Current session mimeType |
michael@0 | 459 | nsString mMimeType; |
michael@0 | 460 | // Timestamp of the last fired dataavailable event. |
michael@0 | 461 | TimeStamp mLastBlobTimeStamp; |
michael@0 | 462 | // The interval of passing encoded data from EncodedBufferCache to onDataAvailable |
michael@0 | 463 | // handler. "mTimeSlice < 0" means Session object does not push encoded data to |
michael@0 | 464 | // onDataAvailable, instead, it passive wait the client side pull encoded data |
michael@0 | 465 | // by calling requestData API. |
michael@0 | 466 | const int32_t mTimeSlice; |
michael@0 | 467 | // Indicate this session's stop has been called. |
michael@0 | 468 | bool mStopIssued; |
michael@0 | 469 | }; |
michael@0 | 470 | |
michael@0 | 471 | NS_IMPL_ISUPPORTS(MediaRecorder::Session, nsIObserver) |
michael@0 | 472 | |
michael@0 | 473 | MediaRecorder::~MediaRecorder() |
michael@0 | 474 | { |
michael@0 | 475 | LOG(PR_LOG_DEBUG, ("~MediaRecorder (%p)", this)); |
michael@0 | 476 | for (uint32_t i = 0; i < mSessions.Length(); i ++) { |
michael@0 | 477 | if (mSessions[i]) { |
michael@0 | 478 | mSessions[i]->ForgetMediaRecorder(); |
michael@0 | 479 | mSessions[i]->Stop(); |
michael@0 | 480 | } |
michael@0 | 481 | } |
michael@0 | 482 | } |
michael@0 | 483 | |
michael@0 | 484 | MediaRecorder::MediaRecorder(DOMMediaStream& aStream, nsPIDOMWindow* aOwnerWindow) |
michael@0 | 485 | : DOMEventTargetHelper(aOwnerWindow), |
michael@0 | 486 | mState(RecordingState::Inactive), |
michael@0 | 487 | mMutex("Session.Data.Mutex") |
michael@0 | 488 | { |
michael@0 | 489 | MOZ_ASSERT(aOwnerWindow); |
michael@0 | 490 | MOZ_ASSERT(aOwnerWindow->IsInnerWindow()); |
michael@0 | 491 | mStream = &aStream; |
michael@0 | 492 | #ifdef PR_LOGGING |
michael@0 | 493 | if (!gMediaRecorderLog) { |
michael@0 | 494 | gMediaRecorderLog = PR_NewLogModule("MediaRecorder"); |
michael@0 | 495 | } |
michael@0 | 496 | #endif |
michael@0 | 497 | } |
michael@0 | 498 | |
michael@0 | 499 | void |
michael@0 | 500 | MediaRecorder::SetMimeType(const nsString &aMimeType) |
michael@0 | 501 | { |
michael@0 | 502 | MutexAutoLock lock(mMutex); |
michael@0 | 503 | mMimeType = aMimeType; |
michael@0 | 504 | } |
michael@0 | 505 | |
michael@0 | 506 | void |
michael@0 | 507 | MediaRecorder::GetMimeType(nsString &aMimeType) |
michael@0 | 508 | { |
michael@0 | 509 | MutexAutoLock lock(mMutex); |
michael@0 | 510 | aMimeType = mMimeType; |
michael@0 | 511 | } |
michael@0 | 512 | |
michael@0 | 513 | void |
michael@0 | 514 | MediaRecorder::Start(const Optional<int32_t>& aTimeSlice, ErrorResult& aResult) |
michael@0 | 515 | { |
michael@0 | 516 | LOG(PR_LOG_DEBUG, ("MediaRecorder.Start %p", this)); |
michael@0 | 517 | if (mState != RecordingState::Inactive) { |
michael@0 | 518 | aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
michael@0 | 519 | return; |
michael@0 | 520 | } |
michael@0 | 521 | |
michael@0 | 522 | if (mStream->GetStream()->IsFinished() || mStream->GetStream()->IsDestroyed()) { |
michael@0 | 523 | aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
michael@0 | 524 | return; |
michael@0 | 525 | } |
michael@0 | 526 | |
michael@0 | 527 | if (!mStream->GetPrincipal()) { |
michael@0 | 528 | aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
michael@0 | 529 | return; |
michael@0 | 530 | } |
michael@0 | 531 | |
michael@0 | 532 | if (!CheckPrincipal()) { |
michael@0 | 533 | aResult.Throw(NS_ERROR_DOM_SECURITY_ERR); |
michael@0 | 534 | return; |
michael@0 | 535 | } |
michael@0 | 536 | |
michael@0 | 537 | int32_t timeSlice = 0; |
michael@0 | 538 | if (aTimeSlice.WasPassed()) { |
michael@0 | 539 | if (aTimeSlice.Value() < 0) { |
michael@0 | 540 | aResult.Throw(NS_ERROR_INVALID_ARG); |
michael@0 | 541 | return; |
michael@0 | 542 | } |
michael@0 | 543 | |
michael@0 | 544 | timeSlice = aTimeSlice.Value(); |
michael@0 | 545 | } |
michael@0 | 546 | |
michael@0 | 547 | mState = RecordingState::Recording; |
michael@0 | 548 | // Start a session |
michael@0 | 549 | |
michael@0 | 550 | mSessions.AppendElement(); |
michael@0 | 551 | mSessions.LastElement() = new Session(this, timeSlice); |
michael@0 | 552 | mSessions.LastElement()->Start(); |
michael@0 | 553 | } |
michael@0 | 554 | |
michael@0 | 555 | void |
michael@0 | 556 | MediaRecorder::Stop(ErrorResult& aResult) |
michael@0 | 557 | { |
michael@0 | 558 | LOG(PR_LOG_DEBUG, ("MediaRecorder.Stop %p", this)); |
michael@0 | 559 | if (mState == RecordingState::Inactive) { |
michael@0 | 560 | aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
michael@0 | 561 | return; |
michael@0 | 562 | } |
michael@0 | 563 | mState = RecordingState::Inactive; |
michael@0 | 564 | if (mSessions.Length() > 0) { |
michael@0 | 565 | mSessions.LastElement()->Stop(); |
michael@0 | 566 | } |
michael@0 | 567 | } |
michael@0 | 568 | |
michael@0 | 569 | void |
michael@0 | 570 | MediaRecorder::Pause(ErrorResult& aResult) |
michael@0 | 571 | { |
michael@0 | 572 | LOG(PR_LOG_DEBUG, ("MediaRecorder.Pause")); |
michael@0 | 573 | if (mState != RecordingState::Recording) { |
michael@0 | 574 | aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
michael@0 | 575 | return; |
michael@0 | 576 | } |
michael@0 | 577 | |
michael@0 | 578 | MOZ_ASSERT(mSessions.Length() > 0); |
michael@0 | 579 | nsresult rv = mSessions.LastElement()->Pause(); |
michael@0 | 580 | if (NS_FAILED(rv)) { |
michael@0 | 581 | NotifyError(rv); |
michael@0 | 582 | return; |
michael@0 | 583 | } |
michael@0 | 584 | mState = RecordingState::Paused; |
michael@0 | 585 | } |
michael@0 | 586 | |
michael@0 | 587 | void |
michael@0 | 588 | MediaRecorder::Resume(ErrorResult& aResult) |
michael@0 | 589 | { |
michael@0 | 590 | LOG(PR_LOG_DEBUG, ("MediaRecorder.Resume")); |
michael@0 | 591 | if (mState != RecordingState::Paused) { |
michael@0 | 592 | aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
michael@0 | 593 | return; |
michael@0 | 594 | } |
michael@0 | 595 | |
michael@0 | 596 | MOZ_ASSERT(mSessions.Length() > 0); |
michael@0 | 597 | nsresult rv = mSessions.LastElement()->Resume(); |
michael@0 | 598 | if (NS_FAILED(rv)) { |
michael@0 | 599 | NotifyError(rv); |
michael@0 | 600 | return; |
michael@0 | 601 | } |
michael@0 | 602 | mState = RecordingState::Recording; |
michael@0 | 603 | } |
michael@0 | 604 | |
michael@0 | 605 | class CreateAndDispatchBlobEventRunnable : public nsRunnable { |
michael@0 | 606 | nsCOMPtr<nsIDOMBlob> mBlob; |
michael@0 | 607 | nsRefPtr<MediaRecorder> mRecorder; |
michael@0 | 608 | public: |
michael@0 | 609 | CreateAndDispatchBlobEventRunnable(already_AddRefed<nsIDOMBlob>&& aBlob, |
michael@0 | 610 | MediaRecorder* aRecorder) |
michael@0 | 611 | : mBlob(aBlob), mRecorder(aRecorder) |
michael@0 | 612 | { } |
michael@0 | 613 | |
michael@0 | 614 | NS_IMETHOD |
michael@0 | 615 | Run(); |
michael@0 | 616 | }; |
michael@0 | 617 | |
michael@0 | 618 | NS_IMETHODIMP |
michael@0 | 619 | CreateAndDispatchBlobEventRunnable::Run() |
michael@0 | 620 | { |
michael@0 | 621 | return mRecorder->CreateAndDispatchBlobEvent(mBlob.forget()); |
michael@0 | 622 | } |
michael@0 | 623 | |
michael@0 | 624 | void |
michael@0 | 625 | MediaRecorder::RequestData(ErrorResult& aResult) |
michael@0 | 626 | { |
michael@0 | 627 | if (mState != RecordingState::Recording) { |
michael@0 | 628 | aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
michael@0 | 629 | return; |
michael@0 | 630 | } |
michael@0 | 631 | |
michael@0 | 632 | NS_DispatchToMainThread( |
michael@0 | 633 | new CreateAndDispatchBlobEventRunnable(mSessions.LastElement()->GetEncodedData(), |
michael@0 | 634 | this), |
michael@0 | 635 | NS_DISPATCH_NORMAL); |
michael@0 | 636 | } |
michael@0 | 637 | |
michael@0 | 638 | JSObject* |
michael@0 | 639 | MediaRecorder::WrapObject(JSContext* aCx) |
michael@0 | 640 | { |
michael@0 | 641 | return MediaRecorderBinding::Wrap(aCx, this); |
michael@0 | 642 | } |
michael@0 | 643 | |
michael@0 | 644 | /* static */ already_AddRefed<MediaRecorder> |
michael@0 | 645 | MediaRecorder::Constructor(const GlobalObject& aGlobal, |
michael@0 | 646 | DOMMediaStream& aStream, |
michael@0 | 647 | const MediaRecorderOptions& aInitDict, |
michael@0 | 648 | ErrorResult& aRv) |
michael@0 | 649 | { |
michael@0 | 650 | nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(aGlobal.GetAsSupports()); |
michael@0 | 651 | if (!sgo) { |
michael@0 | 652 | aRv.Throw(NS_ERROR_FAILURE); |
michael@0 | 653 | return nullptr; |
michael@0 | 654 | } |
michael@0 | 655 | |
michael@0 | 656 | nsCOMPtr<nsPIDOMWindow> ownerWindow = do_QueryInterface(aGlobal.GetAsSupports()); |
michael@0 | 657 | if (!ownerWindow) { |
michael@0 | 658 | aRv.Throw(NS_ERROR_FAILURE); |
michael@0 | 659 | return nullptr; |
michael@0 | 660 | } |
michael@0 | 661 | |
michael@0 | 662 | nsRefPtr<MediaRecorder> object = new MediaRecorder(aStream, ownerWindow); |
michael@0 | 663 | object->SetMimeType(aInitDict.mMimeType); |
michael@0 | 664 | return object.forget(); |
michael@0 | 665 | } |
michael@0 | 666 | |
michael@0 | 667 | nsresult |
michael@0 | 668 | MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob) |
michael@0 | 669 | { |
michael@0 | 670 | NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); |
michael@0 | 671 | if (!CheckPrincipal()) { |
michael@0 | 672 | // Media is not same-origin, don't allow the data out. |
michael@0 | 673 | nsRefPtr<nsIDOMBlob> blob = aBlob; |
michael@0 | 674 | return NS_ERROR_DOM_SECURITY_ERR; |
michael@0 | 675 | } |
michael@0 | 676 | BlobEventInit init; |
michael@0 | 677 | init.mBubbles = false; |
michael@0 | 678 | init.mCancelable = false; |
michael@0 | 679 | init.mData = aBlob; |
michael@0 | 680 | nsRefPtr<BlobEvent> event = |
michael@0 | 681 | BlobEvent::Constructor(this, |
michael@0 | 682 | NS_LITERAL_STRING("dataavailable"), |
michael@0 | 683 | init); |
michael@0 | 684 | event->SetTrusted(true); |
michael@0 | 685 | return DispatchDOMEvent(nullptr, event, nullptr, nullptr); |
michael@0 | 686 | } |
michael@0 | 687 | |
michael@0 | 688 | void |
michael@0 | 689 | MediaRecorder::DispatchSimpleEvent(const nsAString & aStr) |
michael@0 | 690 | { |
michael@0 | 691 | NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); |
michael@0 | 692 | nsresult rv = CheckInnerWindowCorrectness(); |
michael@0 | 693 | if (NS_FAILED(rv)) { |
michael@0 | 694 | return; |
michael@0 | 695 | } |
michael@0 | 696 | |
michael@0 | 697 | nsCOMPtr<nsIDOMEvent> event; |
michael@0 | 698 | rv = NS_NewDOMEvent(getter_AddRefs(event), this, nullptr, nullptr); |
michael@0 | 699 | if (NS_FAILED(rv)) { |
michael@0 | 700 | NS_WARNING("Failed to create the error event!!!"); |
michael@0 | 701 | return; |
michael@0 | 702 | } |
michael@0 | 703 | rv = event->InitEvent(aStr, false, false); |
michael@0 | 704 | |
michael@0 | 705 | if (NS_FAILED(rv)) { |
michael@0 | 706 | NS_WARNING("Failed to init the error event!!!"); |
michael@0 | 707 | return; |
michael@0 | 708 | } |
michael@0 | 709 | |
michael@0 | 710 | event->SetTrusted(true); |
michael@0 | 711 | |
michael@0 | 712 | rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr); |
michael@0 | 713 | if (NS_FAILED(rv)) { |
michael@0 | 714 | NS_ERROR("Failed to dispatch the event!!!"); |
michael@0 | 715 | return; |
michael@0 | 716 | } |
michael@0 | 717 | } |
michael@0 | 718 | |
michael@0 | 719 | void |
michael@0 | 720 | MediaRecorder::NotifyError(nsresult aRv) |
michael@0 | 721 | { |
michael@0 | 722 | NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); |
michael@0 | 723 | nsresult rv = CheckInnerWindowCorrectness(); |
michael@0 | 724 | if (NS_FAILED(rv)) { |
michael@0 | 725 | return; |
michael@0 | 726 | } |
michael@0 | 727 | nsString errorMsg; |
michael@0 | 728 | switch (aRv) { |
michael@0 | 729 | case NS_ERROR_DOM_SECURITY_ERR: |
michael@0 | 730 | errorMsg = NS_LITERAL_STRING("SecurityError"); |
michael@0 | 731 | break; |
michael@0 | 732 | case NS_ERROR_OUT_OF_MEMORY: |
michael@0 | 733 | errorMsg = NS_LITERAL_STRING("OutOfMemoryError"); |
michael@0 | 734 | break; |
michael@0 | 735 | default: |
michael@0 | 736 | errorMsg = NS_LITERAL_STRING("GenericError"); |
michael@0 | 737 | } |
michael@0 | 738 | |
michael@0 | 739 | nsCOMPtr<nsIDOMEvent> event; |
michael@0 | 740 | rv = NS_NewDOMRecordErrorEvent(getter_AddRefs(event), this, nullptr, nullptr); |
michael@0 | 741 | |
michael@0 | 742 | nsCOMPtr<nsIDOMRecordErrorEvent> errorEvent = do_QueryInterface(event); |
michael@0 | 743 | rv = errorEvent->InitRecordErrorEvent(NS_LITERAL_STRING("error"), |
michael@0 | 744 | false, false, errorMsg); |
michael@0 | 745 | |
michael@0 | 746 | event->SetTrusted(true); |
michael@0 | 747 | rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr); |
michael@0 | 748 | if (NS_FAILED(rv)) { |
michael@0 | 749 | NS_ERROR("Failed to dispatch the error event!!!"); |
michael@0 | 750 | return; |
michael@0 | 751 | } |
michael@0 | 752 | return; |
michael@0 | 753 | } |
michael@0 | 754 | |
michael@0 | 755 | bool MediaRecorder::CheckPrincipal() |
michael@0 | 756 | { |
michael@0 | 757 | NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); |
michael@0 | 758 | if (!mStream) { |
michael@0 | 759 | return false; |
michael@0 | 760 | } |
michael@0 | 761 | nsCOMPtr<nsIPrincipal> principal = mStream->GetPrincipal(); |
michael@0 | 762 | if (!GetOwner()) |
michael@0 | 763 | return false; |
michael@0 | 764 | nsCOMPtr<nsIDocument> doc = GetOwner()->GetExtantDoc(); |
michael@0 | 765 | if (!doc || !principal) |
michael@0 | 766 | return false; |
michael@0 | 767 | |
michael@0 | 768 | bool subsumes; |
michael@0 | 769 | if (NS_FAILED(doc->NodePrincipal()->Subsumes(principal, &subsumes))) |
michael@0 | 770 | return false; |
michael@0 | 771 | |
michael@0 | 772 | return subsumes; |
michael@0 | 773 | } |
michael@0 | 774 | |
michael@0 | 775 | void |
michael@0 | 776 | MediaRecorder::RemoveSession(Session* aSession) |
michael@0 | 777 | { |
michael@0 | 778 | LOG(PR_LOG_DEBUG, ("MediaRecorder.RemoveSession (%p)", aSession)); |
michael@0 | 779 | mSessions.RemoveElement(aSession); |
michael@0 | 780 | } |
michael@0 | 781 | |
michael@0 | 782 | } |
michael@0 | 783 | } |