summaryrefslogtreecommitdiffstats
path: root/dom/media/ChannelMediaResource.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/ChannelMediaResource.cpp')
-rw-r--r--dom/media/ChannelMediaResource.cpp1053
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