diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/media/ChannelMediaResource.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/ChannelMediaResource.cpp')
-rw-r--r-- | dom/media/ChannelMediaResource.cpp | 1053 |
1 files changed, 1053 insertions, 0 deletions
diff --git a/dom/media/ChannelMediaResource.cpp b/dom/media/ChannelMediaResource.cpp new file mode 100644 index 0000000000..e249ec36db --- /dev/null +++ b/dom/media/ChannelMediaResource.cpp @@ -0,0 +1,1053 @@ +/* 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<ChannelMediaResource> 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<nsIHttpChannel> 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<nsIThreadRetargetableRequest> 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<nsresult> 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<Closure*>(aClosure); + MediaCacheStream* cacheStream = &closure->mResource->mCacheStream; + if (cacheStream->OwnerThread()->IsOnCurrentThread()) { + cacheStream->NotifyDataReceived( + closure->mLoadID, aCount, + reinterpret_cast<const uint8_t*>(aFromSegment)); + return NS_OK; + } + + RefPtr<ChannelMediaResource> self = closure->mResource; + uint32_t loadID = closure->mLoadID; + UniquePtr<uint8_t[]> data = MakeUnique<uint8_t[]>(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<nsIHttpChannel> 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<nsIHttpChannel> 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<GenericPromise> 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<nsIPrincipal> 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<BaseMediaResource> ChannelMediaResource::CloneData( + MediaResourceCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(CanClone(), "Stream can't be cloned"); + + RefPtr<ChannelMediaResource> 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<nsILoadGroup> 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<nsIPrincipal> 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<nsILoadInfo> 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<nsIClassOfService> 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<ChannelMediaResource>(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<nsIPrincipal> 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<nsILoadInfo> 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<nsITimedChannel> 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<bool>( + "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<ChannelMediaResource> self = this; + nsCOMPtr<nsIRunnable> 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<bool>("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 |