/* 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 "ChannelMediaResource.h" #include "mozilla/Preferences.h" #include "mozilla/dom/HTMLMediaElement.h" #include "mozilla/net/OpaqueResponseUtils.h" #include "nsIAsyncVerifyRedirectCallback.h" #include "nsICachingChannel.h" #include "nsIClassOfService.h" #include "nsIHttpChannel.h" #include "nsIInputStream.h" #include "nsIThreadRetargetableRequest.h" #include "nsITimedChannel.h" #include "nsHttp.h" #include "nsNetUtil.h" static const uint32_t HTTP_PARTIAL_RESPONSE_CODE = 206; static const uint32_t HTTP_OK_CODE = 200; static const uint32_t HTTP_REQUESTED_RANGE_NOT_SATISFIABLE_CODE = 416; mozilla::LazyLogModule gMediaResourceLog("MediaResource"); // Debug logging macro with object pointer and class name. #define LOG(msg, ...) \ DDMOZ_LOG(gMediaResourceLog, mozilla::LogLevel::Debug, msg, ##__VA_ARGS__) namespace mozilla { ChannelMediaResource::ChannelMediaResource(MediaResourceCallback* aCallback, nsIChannel* aChannel, nsIURI* aURI, int64_t aStreamLength, bool aIsPrivateBrowsing) : BaseMediaResource(aCallback, aChannel, aURI), mCacheStream(this, aIsPrivateBrowsing), mSuspendAgent(mCacheStream), mKnownStreamLength(aStreamLength) {} ChannelMediaResource::~ChannelMediaResource() { MOZ_ASSERT(mClosed); MOZ_ASSERT(!mChannel); MOZ_ASSERT(!mListener); if (mSharedInfo) { mSharedInfo->mResources.RemoveElement(this); } } // ChannelMediaResource::Listener just observes the channel and // forwards notifications to the ChannelMediaResource. We use multiple // listener objects so that when we open a new stream for a seek we can // disconnect the old listener from the ChannelMediaResource and hook up // a new listener, so notifications from the old channel are discarded // and don't confuse us. NS_IMPL_ISUPPORTS(ChannelMediaResource::Listener, nsIRequestObserver, nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor, nsIThreadRetargetableStreamListener) nsresult ChannelMediaResource::Listener::OnStartRequest(nsIRequest* aRequest) { MOZ_ASSERT(NS_IsMainThread()); if (!mResource) return NS_OK; return mResource->OnStartRequest(aRequest, mOffset); } nsresult ChannelMediaResource::Listener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { MOZ_ASSERT(NS_IsMainThread()); if (!mResource) return NS_OK; return mResource->OnStopRequest(aRequest, aStatus); } nsresult ChannelMediaResource::Listener::OnDataAvailable( nsIRequest* aRequest, nsIInputStream* aStream, uint64_t aOffset, uint32_t aCount) { // This might happen off the main thread. RefPtr res; { MutexAutoLock lock(mMutex); res = mResource; } // Note Rekove() might happen at the same time to reset mResource. We check // the load ID to determine if the data is from an old channel. return res ? res->OnDataAvailable(mLoadID, aStream, aCount) : NS_OK; } nsresult ChannelMediaResource::Listener::AsyncOnChannelRedirect( nsIChannel* aOld, nsIChannel* aNew, uint32_t aFlags, nsIAsyncVerifyRedirectCallback* cb) { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = NS_OK; if (mResource) { rv = mResource->OnChannelRedirect(aOld, aNew, aFlags, mOffset); } if (NS_FAILED(rv)) { return rv; } cb->OnRedirectVerifyCallback(NS_OK); return NS_OK; } nsresult ChannelMediaResource::Listener::CheckListenerChain() { return NS_OK; } nsresult ChannelMediaResource::Listener::GetInterface(const nsIID& aIID, void** aResult) { return QueryInterface(aIID, aResult); } void ChannelMediaResource::Listener::Revoke() { MOZ_ASSERT(NS_IsMainThread()); MutexAutoLock lock(mMutex); mResource = nullptr; } static bool IsPayloadCompressed(nsIHttpChannel* aChannel) { nsAutoCString encoding; Unused << aChannel->GetResponseHeader("Content-Encoding"_ns, encoding); return encoding.Length() > 0; } nsresult ChannelMediaResource::OnStartRequest(nsIRequest* aRequest, int64_t aRequestOffset) { NS_ASSERTION(mChannel.get() == aRequest, "Wrong channel!"); MOZ_DIAGNOSTIC_ASSERT(!mClosed); MediaDecoderOwner* owner = mCallback->GetMediaOwner(); MOZ_DIAGNOSTIC_ASSERT(owner); dom::HTMLMediaElement* element = owner->GetMediaElement(); MOZ_DIAGNOSTIC_ASSERT(element); nsresult status; nsresult rv = aRequest->GetStatus(&status); NS_ENSURE_SUCCESS(rv, rv); if (status == NS_BINDING_ABORTED) { // Request was aborted before we had a chance to receive any data, or // even an OnStartRequest(). Close the channel. This is important, as // we don't want to mess up our state, as if we're cloned that would // cause the clone to copy incorrect metadata (like whether we're // infinite for example). CloseChannel(); return status; } if (element->ShouldCheckAllowOrigin()) { // If the request was cancelled by nsCORSListenerProxy due to failing // the CORS security check, send an error through to the media element. if (status == NS_ERROR_DOM_BAD_URI) { mCallback->NotifyNetworkError(MediaResult(status, "CORS not allowed")); return NS_ERROR_DOM_BAD_URI; } } nsCOMPtr hc = do_QueryInterface(aRequest); bool seekable = false; int64_t length = -1; int64_t startOffset = aRequestOffset; if (hc) { uint32_t responseStatus = 0; Unused << hc->GetResponseStatus(&responseStatus); bool succeeded = false; Unused << hc->GetRequestSucceeded(&succeeded); if (!succeeded && NS_SUCCEEDED(status)) { // HTTP-level error (e.g. 4xx); treat this as a fatal network-level error. // We might get this on a seek. // (Note that lower-level errors indicated by NS_FAILED(status) are // handled in OnStopRequest.) // A 416 error should treated as EOF here... it's possible // that we don't get Content-Length, we read N bytes, then we // suspend and resume, the resume reopens the channel and we seek to // offset N, but there are no more bytes, so we get a 416 // "Requested Range Not Satisfiable". if (responseStatus == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE_CODE) { // OnStopRequest will not be fired, so we need to do some of its // work here. Note we need to pass the load ID first so the following // NotifyDataEnded() can pass the ID check. mCacheStream.NotifyLoadID(mLoadID); mCacheStream.NotifyDataEnded(mLoadID, status); } else { mCallback->NotifyNetworkError( MediaResult(NS_ERROR_FAILURE, "HTTP error")); } // This disconnects our listener so we don't get any more data. We // certainly don't want an error page to end up in our cache! CloseChannel(); return NS_OK; } nsAutoCString ranges; Unused << hc->GetResponseHeader("Accept-Ranges"_ns, ranges); bool acceptsRanges = net::nsHttp::FindToken(ranges.get(), "bytes", HTTP_HEADER_VALUE_SEPS); int64_t contentLength = -1; const bool isCompressed = IsPayloadCompressed(hc); if (!isCompressed) { hc->GetContentLength(&contentLength); } // Check response code for byte-range requests (seeking, chunk requests). // We don't expect to get a 206 response for a compressed stream, but // double check just to be sure. if (!isCompressed && responseStatus == HTTP_PARTIAL_RESPONSE_CODE) { // Parse Content-Range header. int64_t rangeStart = 0; int64_t rangeEnd = 0; int64_t rangeTotal = 0; rv = ParseContentRangeHeader(hc, rangeStart, rangeEnd, rangeTotal); // We received 'Content-Range', so the server accepts range requests. bool gotRangeHeader = NS_SUCCEEDED(rv); if (gotRangeHeader) { startOffset = rangeStart; // We received 'Content-Range', so the server accepts range requests. // Notify media cache about the length and start offset of data // received. Note: If aRangeTotal == -1, then the total bytes is unknown // at this stage. // For now, tell the decoder that the stream is infinite. if (rangeTotal != -1) { contentLength = std::max(contentLength, rangeTotal); } } acceptsRanges = gotRangeHeader; } else if (responseStatus == HTTP_OK_CODE) { // HTTP_OK_CODE means data will be sent from the start of the stream. startOffset = 0; if (aRequestOffset > 0) { // If HTTP_OK_CODE is responded for a non-zero range request, we have // to assume seeking doesn't work. acceptsRanges = false; } } if (aRequestOffset == 0 && contentLength >= 0 && (responseStatus == HTTP_OK_CODE || responseStatus == HTTP_PARTIAL_RESPONSE_CODE)) { length = contentLength; } // XXX we probably should examine the Content-Range header in case // the server gave us a range which is not quite what we asked for // If we get an HTTP_OK_CODE response to our byte range request, // and the server isn't sending Accept-Ranges:bytes then we don't // support seeking. We also can't seek in compressed streams. seekable = !isCompressed && acceptsRanges; } else { // Not an HTTP channel. Assume data will be sent from position zero. startOffset = 0; } // Update principals before OnDataAvailable() putting the data in the cache. // This is important, we want to make sure all principals are updated before // any consumer can see the new data. UpdatePrincipal(); if (owner->HasError()) { // Updating the principal resulted in an error. Abort the load. CloseChannel(); return NS_OK; } mCacheStream.NotifyDataStarted(mLoadID, startOffset, seekable, length); mIsTransportSeekable = seekable; if (mFirstReadLength < 0) { mFirstReadLength = length; } mSuspendAgent.Delegate(mChannel); // Fires an initial progress event. owner->DownloadProgressed(); nsCOMPtr retarget; if (Preferences::GetBool("media.omt_data_delivery.enabled", false) && (retarget = do_QueryInterface(aRequest))) { // Note this will not always succeed. We need to handle the case where // all resources sharing the same cache might run their data callbacks // on different threads. retarget->RetargetDeliveryTo(mCacheStream.OwnerThread()); } return NS_OK; } bool ChannelMediaResource::IsTransportSeekable() { MOZ_ASSERT(NS_IsMainThread()); // We Report the transport as seekable if we know we will never seek into // the underlying transport. As the MediaCache reads content by block of // BLOCK_SIZE bytes, so the content length is less it will always be fully // read from offset = 0 and we can then always successfully seek within this // buffered content. return mIsTransportSeekable || (mFirstReadLength > 0 && mFirstReadLength < MediaCacheStream::BLOCK_SIZE); } nsresult ChannelMediaResource::ParseContentRangeHeader( nsIHttpChannel* aHttpChan, int64_t& aRangeStart, int64_t& aRangeEnd, int64_t& aRangeTotal) const { NS_ENSURE_ARG(aHttpChan); nsAutoCString rangeStr; nsresult rv = aHttpChan->GetResponseHeader("Content-Range"_ns, rangeStr); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_FALSE(rangeStr.IsEmpty(), NS_ERROR_ILLEGAL_VALUE); auto rangeOrErr = net::ParseContentRangeHeaderString(rangeStr); NS_ENSURE_FALSE(rangeOrErr.isErr(), rangeOrErr.unwrapErr()); aRangeStart = std::get<0>(rangeOrErr.inspect()); aRangeEnd = std::get<1>(rangeOrErr.inspect()); aRangeTotal = std::get<2>(rangeOrErr.inspect()); LOG("Received bytes [%" PRId64 "] to [%" PRId64 "] of [%" PRId64 "] for decoder[%p]", aRangeStart, aRangeEnd, aRangeTotal, mCallback.get()); return NS_OK; } nsresult ChannelMediaResource::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { NS_ASSERTION(mChannel.get() == aRequest, "Wrong channel!"); NS_ASSERTION(!mSuspendAgent.IsSuspended(), "How can OnStopRequest fire while we're suspended?"); MOZ_DIAGNOSTIC_ASSERT(!mClosed); // Move this request back into the foreground. This is necessary for // requests owned by video documents to ensure the load group fires // OnStopRequest when restoring from session history. nsLoadFlags loadFlags; DebugOnly rv = mChannel->GetLoadFlags(&loadFlags); NS_ASSERTION(NS_SUCCEEDED(rv), "GetLoadFlags() failed!"); if (loadFlags & nsIRequest::LOAD_BACKGROUND) { Unused << NS_WARN_IF( NS_FAILED(ModifyLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND))); } // Note that aStatus might have succeeded --- this might be a normal close // --- even in situations where the server cut us off because we were // suspended. It is also possible that the server sends us fewer bytes than // requested. So we need to "reopen on error" in that case too. The only // cases where we don't need to reopen are when *we* closed the stream. // But don't reopen if we need to seek and we don't think we can... that would // cause us to just re-read the stream, which would be really bad. /* * | length | offset | reopen | * +--------+-----------+----------+ * | -1 | 0 | yes | * +--------+-----------+----------+ * | -1 | > 0 | seekable | * +--------+-----------+----------+ * | 0 | X | no | * +--------+-----------+----------+ * | > 0 | 0 | yes | * +--------+-----------+----------+ * | > 0 | != length | seekable | * +--------+-----------+----------+ * | > 0 | == length | no | */ if (aStatus != NS_ERROR_PARSED_DATA_CACHED && aStatus != NS_BINDING_ABORTED) { auto lengthAndOffset = mCacheStream.GetLengthAndOffset(); int64_t length = lengthAndOffset.mLength; int64_t offset = lengthAndOffset.mOffset; if ((offset == 0 || mIsTransportSeekable) && offset != length) { // If the stream did close normally, restart the channel if we're either // at the start of the resource, or if the server is seekable and we're // not at the end of stream. We don't restart the stream if we're at the // end because not all web servers handle this case consistently; see: // https://bugzilla.mozilla.org/show_bug.cgi?id=1373618#c36 nsresult rv = Seek(offset, false); if (NS_SUCCEEDED(rv)) { return rv; } // Close the streams that failed due to error. This will cause all // client Read and Seek operations on those streams to fail. Blocked // Reads will also be woken up. Close(); } } mCacheStream.NotifyDataEnded(mLoadID, aStatus); return NS_OK; } nsresult ChannelMediaResource::OnChannelRedirect(nsIChannel* aOld, nsIChannel* aNew, uint32_t aFlags, int64_t aOffset) { // OnChannelRedirect() is followed by OnStartRequest() where we will // call mSuspendAgent.Delegate(). mChannel = aNew; return SetupChannelHeaders(aOffset); } nsresult ChannelMediaResource::CopySegmentToCache( nsIInputStream* aInStream, void* aClosure, const char* aFromSegment, uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) { *aWriteCount = aCount; Closure* closure = static_cast(aClosure); MediaCacheStream* cacheStream = &closure->mResource->mCacheStream; if (cacheStream->OwnerThread()->IsOnCurrentThread()) { cacheStream->NotifyDataReceived( closure->mLoadID, aCount, reinterpret_cast(aFromSegment)); return NS_OK; } RefPtr self = closure->mResource; uint32_t loadID = closure->mLoadID; UniquePtr data = MakeUnique(aCount); memcpy(data.get(), aFromSegment, aCount); cacheStream->OwnerThread()->Dispatch(NS_NewRunnableFunction( "MediaCacheStream::NotifyDataReceived", [self, loadID, data = std::move(data), aCount]() { self->mCacheStream.NotifyDataReceived(loadID, aCount, data.get()); })); return NS_OK; } nsresult ChannelMediaResource::OnDataAvailable(uint32_t aLoadID, nsIInputStream* aStream, uint32_t aCount) { // This might happen off the main thread. Closure closure{aLoadID, this}; uint32_t count = aCount; while (count > 0) { uint32_t read; nsresult rv = aStream->ReadSegments(CopySegmentToCache, &closure, count, &read); if (NS_FAILED(rv)) return rv; NS_ASSERTION(read > 0, "Read 0 bytes while data was available?"); count -= read; } return NS_OK; } int64_t ChannelMediaResource::CalculateStreamLength() const { if (!mChannel) { return -1; } nsCOMPtr hc = do_QueryInterface(mChannel); if (!hc) { return -1; } bool succeeded = false; Unused << hc->GetRequestSucceeded(&succeeded); if (!succeeded) { return -1; } // We can't determine the length of uncompressed payload. const bool isCompressed = IsPayloadCompressed(hc); if (isCompressed) { return -1; } int64_t contentLength = -1; if (NS_FAILED(hc->GetContentLength(&contentLength))) { return -1; } uint32_t responseStatus = 0; Unused << hc->GetResponseStatus(&responseStatus); if (responseStatus != HTTP_PARTIAL_RESPONSE_CODE) { return contentLength; } // We have an HTTP Byte Range response. The Content-Length is the length // of the response, not the resource. We need to parse the Content-Range // header and extract the range total in order to get the stream length. int64_t rangeStart = 0; int64_t rangeEnd = 0; int64_t rangeTotal = 0; bool gotRangeHeader = NS_SUCCEEDED( ParseContentRangeHeader(hc, rangeStart, rangeEnd, rangeTotal)); if (gotRangeHeader && rangeTotal != -1) { contentLength = std::max(contentLength, rangeTotal); } return contentLength; } nsresult ChannelMediaResource::Open(nsIStreamListener** aStreamListener) { NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); MOZ_ASSERT(aStreamListener); MOZ_ASSERT(mChannel); int64_t streamLength = mKnownStreamLength < 0 ? CalculateStreamLength() : mKnownStreamLength; nsresult rv = mCacheStream.Init(streamLength); if (NS_FAILED(rv)) { return rv; } mSharedInfo = new SharedInfo; mSharedInfo->mResources.AppendElement(this); mIsLiveStream = streamLength < 0; mListener = new Listener(this, 0, ++mLoadID); *aStreamListener = mListener; NS_ADDREF(*aStreamListener); return NS_OK; } dom::HTMLMediaElement* ChannelMediaResource::MediaElement() const { MOZ_ASSERT(NS_IsMainThread()); MediaDecoderOwner* owner = mCallback->GetMediaOwner(); MOZ_DIAGNOSTIC_ASSERT(owner); dom::HTMLMediaElement* element = owner->GetMediaElement(); MOZ_DIAGNOSTIC_ASSERT(element); return element; } nsresult ChannelMediaResource::OpenChannel(int64_t aOffset) { MOZ_ASSERT(NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(!mClosed); MOZ_ASSERT(mChannel); MOZ_ASSERT(!mListener, "Listener should have been removed by now"); mListener = new Listener(this, aOffset, ++mLoadID); nsresult rv = mChannel->SetNotificationCallbacks(mListener.get()); NS_ENSURE_SUCCESS(rv, rv); rv = SetupChannelHeaders(aOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mChannel->AsyncOpen(mListener); NS_ENSURE_SUCCESS(rv, rv); // Tell the media element that we are fetching data from a channel. MediaElement()->DownloadResumed(); return NS_OK; } nsresult ChannelMediaResource::SetupChannelHeaders(int64_t aOffset) { MOZ_ASSERT(NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(!mClosed); // Always use a byte range request even if we're reading from the start // of the resource. // This enables us to detect if the stream supports byte range // requests, and therefore seeking, early. nsCOMPtr hc = do_QueryInterface(mChannel); if (hc) { // Use |mOffset| if seeking in a complete file download. nsAutoCString rangeString("bytes="); rangeString.AppendInt(aOffset); rangeString.Append('-'); nsresult rv = hc->SetRequestHeader("Range"_ns, rangeString, false); NS_ENSURE_SUCCESS(rv, rv); // Send Accept header for video and audio types only (Bug 489071) MediaElement()->SetRequestHeaders(hc); } else { NS_ASSERTION(aOffset == 0, "Don't know how to seek on this channel type"); return NS_ERROR_FAILURE; } return NS_OK; } RefPtr ChannelMediaResource::Close() { NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); if (!mClosed) { CloseChannel(); mClosed = true; return mCacheStream.Close(); } return GenericPromise::CreateAndResolve(true, __func__); } already_AddRefed ChannelMediaResource::GetCurrentPrincipal() { MOZ_ASSERT(NS_IsMainThread()); return do_AddRef(mSharedInfo->mPrincipal); } bool ChannelMediaResource::HadCrossOriginRedirects() { MOZ_ASSERT(NS_IsMainThread()); return mSharedInfo->mHadCrossOriginRedirects; } bool ChannelMediaResource::CanClone() { return !mClosed && mCacheStream.IsAvailableForSharing(); } already_AddRefed ChannelMediaResource::CloneData( MediaResourceCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(CanClone(), "Stream can't be cloned"); RefPtr resource = new ChannelMediaResource(aCallback, nullptr, mURI, mKnownStreamLength); resource->mIsLiveStream = mIsLiveStream; resource->mIsTransportSeekable = mIsTransportSeekable; resource->mSharedInfo = mSharedInfo; mSharedInfo->mResources.AppendElement(resource.get()); // Initially the clone is treated as suspended by the cache, because // we don't have a channel. If the cache needs to read data from the clone // it will call CacheClientResume (or CacheClientSeek with aResume true) // which will recreate the channel. This way, if all of the media data // is already in the cache we don't create an unnecessary HTTP channel // and perform a useless HTTP transaction. resource->mCacheStream.InitAsClone(&mCacheStream); return resource.forget(); } void ChannelMediaResource::CloseChannel() { NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); // Revoking listener should be done before canceling the channel, because // canceling the channel might cause the input stream to release its buffer. // If we don't do revoke first, it's possible that `OnDataAvailable` would be // called later and then incorrectly access that released buffer. if (mListener) { mListener->Revoke(); mListener = nullptr; } if (mChannel) { mSuspendAgent.Revoke(); // The status we use here won't be passed to the decoder, since // we've already revoked the listener. It can however be passed // to nsDocumentViewer::LoadComplete if our channel is the one // that kicked off creation of a video document. We don't want that // document load to think there was an error. // NS_ERROR_PARSED_DATA_CACHED is the best thing we have for that // at the moment. mChannel->Cancel(NS_ERROR_PARSED_DATA_CACHED); mChannel = nullptr; } } nsresult ChannelMediaResource::ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount) { return mCacheStream.ReadFromCache(aBuffer, aOffset, aCount); } nsresult ChannelMediaResource::ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes) { NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread"); return mCacheStream.ReadAt(aOffset, aBuffer, aCount, aBytes); } void ChannelMediaResource::ThrottleReadahead(bool bThrottle) { mCacheStream.ThrottleReadahead(bThrottle); } nsresult ChannelMediaResource::GetCachedRanges(MediaByteRangeSet& aRanges) { return mCacheStream.GetCachedRanges(aRanges); } void ChannelMediaResource::Suspend(bool aCloseImmediately) { NS_ASSERTION(NS_IsMainThread(), "Don't call on non-main thread"); if (mClosed) { // Nothing to do when we are closed. return; } dom::HTMLMediaElement* element = MediaElement(); if (mChannel && aCloseImmediately && mIsTransportSeekable) { CloseChannel(); } if (mSuspendAgent.Suspend()) { element->DownloadSuspended(); } } void ChannelMediaResource::Resume() { NS_ASSERTION(NS_IsMainThread(), "Don't call on non-main thread"); if (mClosed) { // Nothing to do when we are closed. return; } dom::HTMLMediaElement* element = MediaElement(); if (mSuspendAgent.Resume()) { if (mChannel) { // Just wake up our existing channel element->DownloadResumed(); } else { mCacheStream.NotifyResume(); } } } nsresult ChannelMediaResource::RecreateChannel() { MOZ_DIAGNOSTIC_ASSERT(!mClosed); nsLoadFlags loadFlags = nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY | (mLoadInBackground ? nsIRequest::LOAD_BACKGROUND : 0); dom::HTMLMediaElement* element = MediaElement(); nsCOMPtr loadGroup = element->GetDocumentLoadGroup(); NS_ENSURE_TRUE(loadGroup, NS_ERROR_NULL_POINTER); nsSecurityFlags securityFlags = element->ShouldCheckAllowOrigin() ? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; if (element->GetCORSMode() == CORS_USE_CREDENTIALS) { securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; } MOZ_ASSERT(element->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video)); nsContentPolicyType contentPolicyType = element->IsHTMLElement(nsGkAtoms::audio) ? nsIContentPolicy::TYPE_INTERNAL_AUDIO : nsIContentPolicy::TYPE_INTERNAL_VIDEO; // If element has 'triggeringprincipal' attribute, we will use the value as // triggeringPrincipal for the channel, otherwise it will default to use // aElement->NodePrincipal(). // This function returns true when element has 'triggeringprincipal', so if // setAttrs is true we will override the origin attributes on the channel // later. nsCOMPtr triggeringPrincipal; bool setAttrs = nsContentUtils::QueryTriggeringPrincipal( element, getter_AddRefs(triggeringPrincipal)); nsresult rv = NS_NewChannelWithTriggeringPrincipal( getter_AddRefs(mChannel), mURI, element, triggeringPrincipal, securityFlags, contentPolicyType, nullptr, // aPerformanceStorage loadGroup, nullptr, // aCallbacks loadFlags); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr loadInfo = mChannel->LoadInfo(); if (setAttrs) { // The function simply returns NS_OK, so we ignore the return value. Unused << loadInfo->SetOriginAttributes( triggeringPrincipal->OriginAttributesRef()); } Unused << loadInfo->SetIsMediaRequest(true); nsCOMPtr cos(do_QueryInterface(mChannel)); if (cos) { // Unconditionally disable throttling since we want the media to fluently // play even when we switch the tab to background. cos->AddClassFlags(nsIClassOfService::DontThrottle); } return rv; } void ChannelMediaResource::CacheClientNotifyDataReceived() { mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod( "MediaResourceCallback::NotifyDataArrived", mCallback.get(), &MediaResourceCallback::NotifyDataArrived)); } void ChannelMediaResource::CacheClientNotifyDataEnded(nsresult aStatus) { mCallback->AbstractMainThread()->Dispatch(NS_NewRunnableFunction( "ChannelMediaResource::CacheClientNotifyDataEnded", [self = RefPtr(this), aStatus]() { if (NS_SUCCEEDED(aStatus)) { self->mIsLiveStream = false; } self->mCallback->NotifyDataEnded(aStatus); })); } void ChannelMediaResource::CacheClientNotifyPrincipalChanged() { NS_ASSERTION(NS_IsMainThread(), "Don't call on non-main thread"); mCallback->NotifyPrincipalChanged(); } void ChannelMediaResource::UpdatePrincipal() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mChannel); nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); if (!secMan) { return; } bool hadData = mSharedInfo->mPrincipal != nullptr; // Channels created from a media element (in RecreateChannel() or // HTMLMediaElement::ChannelLoader) do not have SANDBOXED_ORIGIN set in the // LoadInfo. Document loads for a sandboxed iframe, however, may have // SANDBOXED_ORIGIN set. Ignore sandboxing so that on such loads the result // principal is not replaced with a null principal but describes the source // of the data and is the same as would be obtained from a load from the // media host element. nsCOMPtr principal; secMan->GetChannelResultPrincipalIfNotSandboxed(mChannel, getter_AddRefs(principal)); if (nsContentUtils::CombineResourcePrincipals(&mSharedInfo->mPrincipal, principal)) { for (auto* r : mSharedInfo->mResources) { r->CacheClientNotifyPrincipalChanged(); } if (!mChannel) { // Sometimes cleared during NotifyPrincipalChanged() return; } } nsCOMPtr loadInfo = mChannel->LoadInfo(); auto mode = loadInfo->GetSecurityMode(); if (mode != nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) { MOZ_ASSERT( mode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT || mode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, "no-cors request"); MOZ_ASSERT(!hadData || !mChannel->IsDocument(), "Only the initial load may be a document load"); bool finalResponseIsOpaque = // NS_GetFinalChannelURI() and GetChannelResultPrincipal() return the // original request URI for null-origin Responses from ServiceWorker, // in which case the URI does not necessarily indicate the real source // of data. Such null-origin Responses have Basic LoadTainting, and // so can be distinguished from true cross-origin responses when the // channel is not a document load. // // When the channel is a document load, LoadTainting indicates opacity // wrt the parent document and so does not indicate whether the // response is cross-origin wrt to the media element. However, // ServiceWorkers for document loads are always same-origin with the // channel URI and so there is no need to distinguish null-origin // ServiceWorker responses to document loads. // // CORS filtered Responses from ServiceWorker also cannot be mixed // with no-cors cross-origin responses. (mChannel->IsDocument() || loadInfo->GetTainting() == LoadTainting::Opaque) && // Although intermediate cross-origin redirects back to URIs with // loadingPrincipal will have LoadTainting::Opaque and will taint the // media element, they are not considered opaque when verifying // network responses; they can be mixed with non-opaque responses from // subsequent loads on the same-origin finalURI. !nsContentUtils::CheckMayLoad(MediaElement()->NodePrincipal(), mChannel, /*allowIfInheritsPrincipal*/ true); if (!hadData) { // First response with data mSharedInfo->mFinalResponsesAreOpaque = finalResponseIsOpaque; } else if (mSharedInfo->mFinalResponsesAreOpaque != finalResponseIsOpaque) { for (auto* r : mSharedInfo->mResources) { r->mCallback->NotifyNetworkError(MediaResult( NS_ERROR_CONTENT_BLOCKED, "opaque and non-opaque responses")); } // Our caller, OnStartRequest() will CloseChannel() on discovering the // error, so no data will be read from the channel. return; } } // ChannelMediaResource can recreate the channel. When this happens, we don't // want to overwrite mHadCrossOriginRedirects because the new channel could // skip intermediate redirects. if (!mSharedInfo->mHadCrossOriginRedirects) { nsCOMPtr timedChannel = do_QueryInterface(mChannel); if (timedChannel) { bool allRedirectsSameOrigin = false; mSharedInfo->mHadCrossOriginRedirects = NS_SUCCEEDED(timedChannel->GetAllRedirectsSameOrigin( &allRedirectsSameOrigin)) && !allRedirectsSameOrigin; } } } void ChannelMediaResource::CacheClientNotifySuspendedStatusChanged( bool aSuspended) { mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod( "MediaResourceCallback::NotifySuspendedStatusChanged", mCallback.get(), &MediaResourceCallback::NotifySuspendedStatusChanged, aSuspended)); } nsresult ChannelMediaResource::Seek(int64_t aOffset, bool aResume) { MOZ_ASSERT(NS_IsMainThread()); if (mClosed) { // Nothing to do when we are closed. return NS_OK; } LOG("Seek requested for aOffset [%" PRId64 "]", aOffset); CloseChannel(); if (aResume) { mSuspendAgent.Resume(); } // Don't create a new channel if we are still suspended. The channel will // be recreated when we are resumed. if (mSuspendAgent.IsSuspended()) { return NS_OK; } nsresult rv = RecreateChannel(); NS_ENSURE_SUCCESS(rv, rv); return OpenChannel(aOffset); } void ChannelMediaResource::CacheClientSeek(int64_t aOffset, bool aResume) { RefPtr self = this; nsCOMPtr r = NS_NewRunnableFunction( "ChannelMediaResource::Seek", [self, aOffset, aResume]() { nsresult rv = self->Seek(aOffset, aResume); if (NS_FAILED(rv)) { // Close the streams that failed due to error. This will cause all // client Read and Seek operations on those streams to fail. Blocked // Reads will also be woken up. self->Close(); } }); mCallback->AbstractMainThread()->Dispatch(r.forget()); } void ChannelMediaResource::CacheClientSuspend() { mCallback->AbstractMainThread()->Dispatch( NewRunnableMethod("ChannelMediaResource::Suspend", this, &ChannelMediaResource::Suspend, false)); } void ChannelMediaResource::CacheClientResume() { mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod( "ChannelMediaResource::Resume", this, &ChannelMediaResource::Resume)); } int64_t ChannelMediaResource::GetNextCachedData(int64_t aOffset) { return mCacheStream.GetNextCachedData(aOffset); } int64_t ChannelMediaResource::GetCachedDataEnd(int64_t aOffset) { return mCacheStream.GetCachedDataEnd(aOffset); } bool ChannelMediaResource::IsDataCachedToEndOfResource(int64_t aOffset) { return mCacheStream.IsDataCachedToEndOfStream(aOffset); } bool ChannelMediaResource::IsSuspended() { return mSuspendAgent.IsSuspended(); } void ChannelMediaResource::SetReadMode(MediaCacheStream::ReadMode aMode) { mCacheStream.SetReadMode(aMode); } void ChannelMediaResource::SetPlaybackRate(uint32_t aBytesPerSecond) { mCacheStream.SetPlaybackRate(aBytesPerSecond); } void ChannelMediaResource::Pin() { mCacheStream.Pin(); } void ChannelMediaResource::Unpin() { mCacheStream.Unpin(); } double ChannelMediaResource::GetDownloadRate(bool* aIsReliable) { return mCacheStream.GetDownloadRate(aIsReliable); } int64_t ChannelMediaResource::GetLength() { return mCacheStream.GetLength(); } void ChannelMediaResource::GetDebugInfo(dom::MediaResourceDebugInfo& aInfo) { mCacheStream.GetDebugInfo(aInfo.mCacheStream); } // ChannelSuspendAgent bool ChannelSuspendAgent::Suspend() { MOZ_ASSERT(NS_IsMainThread()); SuspendInternal(); if (++mSuspendCount == 1) { mCacheStream.NotifyClientSuspended(true); return true; } return false; } void ChannelSuspendAgent::SuspendInternal() { MOZ_ASSERT(NS_IsMainThread()); if (mChannel) { bool isPending = false; nsresult rv = mChannel->IsPending(&isPending); if (NS_SUCCEEDED(rv) && isPending && !mIsChannelSuspended) { mChannel->Suspend(); mIsChannelSuspended = true; } } } bool ChannelSuspendAgent::Resume() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(IsSuspended(), "Resume without suspend!"); if (--mSuspendCount == 0) { if (mChannel && mIsChannelSuspended) { mChannel->Resume(); mIsChannelSuspended = false; } mCacheStream.NotifyClientSuspended(false); return true; } return false; } void ChannelSuspendAgent::Delegate(nsIChannel* aChannel) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aChannel); MOZ_ASSERT(!mChannel, "The previous channel not closed."); MOZ_ASSERT(!mIsChannelSuspended); mChannel = aChannel; // Ensure the suspend status of the channel matches our suspend count. if (IsSuspended()) { SuspendInternal(); } } void ChannelSuspendAgent::Revoke() { MOZ_ASSERT(NS_IsMainThread()); if (!mChannel) { // Channel already revoked. Nothing to do. return; } // Before closing the channel, it needs to be resumed to make sure its // internal state is correct. Besides, We need to suspend the channel after // recreating. if (mIsChannelSuspended) { mChannel->Resume(); mIsChannelSuspended = false; } mChannel = nullptr; } bool ChannelSuspendAgent::IsSuspended() { MOZ_ASSERT(NS_IsMainThread()); return (mSuspendCount > 0); } } // namespace mozilla