diff options
Diffstat (limited to 'dom/html/HTMLTrackElement.cpp')
-rw-r--r-- | dom/html/HTMLTrackElement.cpp | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/dom/html/HTMLTrackElement.cpp b/dom/html/HTMLTrackElement.cpp new file mode 100644 index 0000000000..e5b06f4a1d --- /dev/null +++ b/dom/html/HTMLTrackElement.cpp @@ -0,0 +1,517 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/dom/HTMLTrackElement.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/WebVTTListener.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/HTMLTrackElementBinding.h" +#include "mozilla/dom/HTMLUnknownElement.h" +#include "nsAttrValueInlines.h" +#include "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsIContentPolicy.h" +#include "mozilla/dom/Document.h" +#include "nsILoadGroup.h" +#include "nsIObserver.h" +#include "nsIScriptError.h" +#include "nsISupportsImpl.h" +#include "nsISupportsPrimitives.h" +#include "nsMappedAttributes.h" +#include "nsNetUtil.h" +#include "nsStyleConsts.h" +#include "nsThreadUtils.h" +#include "nsVideoFrame.h" + +extern mozilla::LazyLogModule gTextTrackLog; +#define LOG(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \ + ("TextTrackElement=%p, " msg, this, ##__VA_ARGS__)) + +// Replace the usual NS_IMPL_NS_NEW_HTML_ELEMENT(Track) so +// we can return an UnknownElement instead when pref'd off. +nsGenericHTMLElement* NS_NewHTMLTrackElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) mozilla::dom::HTMLTrackElement(nodeInfo.forget()); +} + +namespace mozilla::dom { + +// Map html attribute string values to TextTrackKind enums. +static constexpr nsAttrValue::EnumTable kKindTable[] = { + {"subtitles", static_cast<int16_t>(TextTrackKind::Subtitles)}, + {"captions", static_cast<int16_t>(TextTrackKind::Captions)}, + {"descriptions", static_cast<int16_t>(TextTrackKind::Descriptions)}, + {"chapters", static_cast<int16_t>(TextTrackKind::Chapters)}, + {"metadata", static_cast<int16_t>(TextTrackKind::Metadata)}, + {nullptr, 0}}; + +// Invalid values are treated as "metadata" in ParseAttribute, but if no value +// at all is specified, it's treated as "subtitles" in GetKind +static constexpr const nsAttrValue::EnumTable* kKindTableInvalidValueDefault = + &kKindTable[4]; + +class WindowDestroyObserver final : public nsIObserver { + NS_DECL_ISUPPORTS + + public: + explicit WindowDestroyObserver(HTMLTrackElement* aElement, uint64_t aWinID) + : mTrackElement(aElement), mInnerID(aWinID) { + RegisterWindowDestroyObserver(); + } + void RegisterWindowDestroyObserver() { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, "inner-window-destroyed", false); + } + } + void UnRegisterWindowDestroyObserver() { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "inner-window-destroyed"); + } + mTrackElement = nullptr; + } + NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + MOZ_ASSERT(NS_IsMainThread()); + if (strcmp(aTopic, "inner-window-destroyed") == 0) { + nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + NS_ENSURE_SUCCESS(rv, rv); + if (innerID == mInnerID) { + if (mTrackElement) { + mTrackElement->CancelChannelAndListener(); + } + UnRegisterWindowDestroyObserver(); + } + } + return NS_OK; + } + + private: + ~WindowDestroyObserver() = default; + + HTMLTrackElement* mTrackElement; + uint64_t mInnerID; +}; +NS_IMPL_ISUPPORTS(WindowDestroyObserver, nsIObserver); + +/** HTMLTrackElement */ +HTMLTrackElement::HTMLTrackElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mLoadResourceDispatched(false), + mWindowDestroyObserver(nullptr) { + nsISupports* parentObject = OwnerDoc()->GetParentObject(); + NS_ENSURE_TRUE_VOID(parentObject); + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); + if (window) { + mWindowDestroyObserver = + new WindowDestroyObserver(this, window->WindowID()); + } +} + +HTMLTrackElement::~HTMLTrackElement() { + if (mWindowDestroyObserver) { + mWindowDestroyObserver->UnRegisterWindowDestroyObserver(); + } + CancelChannelAndListener(); +} + +NS_IMPL_ELEMENT_CLONE(HTMLTrackElement) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTrackElement, nsGenericHTMLElement, + mTrack, mMediaParent, mListener) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTrackElement, + nsGenericHTMLElement) + +void HTMLTrackElement::GetKind(DOMString& aKind) const { + GetEnumAttr(nsGkAtoms::kind, kKindTable[0].tag, aKind); +} + +void HTMLTrackElement::OnChannelRedirect(nsIChannel* aChannel, + nsIChannel* aNewChannel, + uint32_t aFlags) { + NS_ASSERTION(aChannel == mChannel, "Channels should match!"); + mChannel = aNewChannel; +} + +JSObject* HTMLTrackElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTrackElement_Binding::Wrap(aCx, this, aGivenProto); +} + +TextTrack* HTMLTrackElement::GetTrack() { + if (!mTrack) { + CreateTextTrack(); + } + return mTrack; +} + +void HTMLTrackElement::CreateTextTrack() { + nsISupports* parentObject = OwnerDoc()->GetParentObject(); + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); + if (!parentObject) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, "Media"_ns, OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, + "Using track element in non-window context"); + return; + } + + nsString label, srcLang; + GetSrclang(srcLang); + GetLabel(label); + + TextTrackKind kind; + if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::kind)) { + kind = static_cast<TextTrackKind>(value->GetEnumValue()); + } else { + kind = TextTrackKind::Subtitles; + } + + MOZ_ASSERT(!mTrack, "No need to recreate a text track!"); + mTrack = + new TextTrack(window, kind, label, srcLang, TextTrackMode::Disabled, + TextTrackReadyState::NotLoaded, TextTrackSource::Track); + mTrack->SetTrackElement(this); +} + +bool HTMLTrackElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::kind) { + // Case-insensitive lookup, with the first element as the default. + return aResult.ParseEnumValue(aValue, kKindTable, false, + kKindTableInvalidValueDefault); + } + + // Otherwise call the generic implementation. + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTrackElement::SetSrc(const nsAString& aSrc, ErrorResult& aError) { + LOG("Set src=%s", NS_ConvertUTF16toUTF8(aSrc).get()); + + nsAutoString src; + if (GetAttr(kNameSpaceID_None, nsGkAtoms::src, src) && src == aSrc) { + LOG("No need to reload for same src url"); + return; + } + + SetHTMLAttr(nsGkAtoms::src, aSrc, aError); + SetReadyState(TextTrackReadyState::NotLoaded); + if (!mMediaParent) { + return; + } + + // Stop WebVTTListener. + mListener = nullptr; + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "HTMLTrackElement::SetSrc"_ns); + mChannel = nullptr; + } + + MaybeDispatchLoadResource(); +} + +void HTMLTrackElement::MaybeClearAllCues() { + // Empty track's cue list whenever the track element's `src` attribute set, + // changed, or removed, + // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:attr-track-src + if (!mTrack) { + return; + } + mTrack->ClearAllCues(); +} + +// This function will run partial steps from `start-the-track-processing-model` +// and finish the rest of steps in `LoadResource()` during the stable state. +// https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model +void HTMLTrackElement::MaybeDispatchLoadResource() { + MOZ_ASSERT(mTrack, "Should have already created text track!"); + + // step2, if the text track's text track mode is not set to one of hidden or + // showing, then return. + if (mTrack->Mode() == TextTrackMode::Disabled) { + LOG("Do not load resource for disable track"); + return; + } + + // step3, if the text track's track element does not have a media element as a + // parent, return. + if (!mMediaParent) { + LOG("Do not load resource for track without media element"); + return; + } + + if (ReadyState() == TextTrackReadyState::Loaded) { + LOG("Has already loaded resource"); + return; + } + + // step5, await a stable state and run the rest of steps. + if (!mLoadResourceDispatched) { + RefPtr<WebVTTListener> listener = new WebVTTListener(this); + RefPtr<Runnable> r = NewRunnableMethod<RefPtr<WebVTTListener>>( + "dom::HTMLTrackElement::LoadResource", this, + &HTMLTrackElement::LoadResource, std::move(listener)); + nsContentUtils::RunInStableState(r.forget()); + mLoadResourceDispatched = true; + } +} + +void HTMLTrackElement::LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener) { + LOG("LoadResource"); + mLoadResourceDispatched = false; + + nsAutoString src; + if (!GetAttr(kNameSpaceID_None, nsGkAtoms::src, src) || src.IsEmpty()) { + LOG("Fail to load because no src"); + SetReadyState(TextTrackReadyState::FailedToLoad); + return; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NewURIFromString(src, getter_AddRefs(uri)); + NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); + LOG("Trying to load from src=%s", NS_ConvertUTF16toUTF8(src).get()); + + CancelChannelAndListener(); + + // According to + // https://www.w3.org/TR/html5/embedded-content-0.html#sourcing-out-of-band-text-tracks + // + // "8: If the track element's parent is a media element then let CORS mode + // be the state of the parent media element's crossorigin content attribute. + // Otherwise, let CORS mode be No CORS." + // + CORSMode corsMode = + mMediaParent ? AttrValueToCORSMode( + mMediaParent->GetParsedAttr(nsGkAtoms::crossorigin)) + : CORS_NONE; + + // Determine the security flag based on corsMode. + nsSecurityFlags secFlags; + if (CORS_NONE == corsMode) { + // Same-origin is required for track element. + secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; + } else { + secFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + if (CORS_ANONYMOUS == corsMode) { + secFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else if (CORS_USE_CREDENTIALS == corsMode) { + secFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } else { + NS_WARNING("Unknown CORS mode."); + secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; + } + } + + mListener = std::move(aWebVTTListener); + // This will do 6. Set the text track readiness state to loading. + rv = mListener->LoadResource(); + NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); + + Document* doc = OwnerDoc(); + if (!doc) { + return; + } + + // 9. End the synchronous section, continuing the remaining steps in parallel. + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + "dom::HTMLTrackElement::LoadResource", + [self = RefPtr<HTMLTrackElement>(this), this, uri, secFlags]() { + if (!mListener) { + // Shutdown got called, abort. + return; + } + nsCOMPtr<nsIChannel> channel; + nsCOMPtr<nsILoadGroup> loadGroup = OwnerDoc()->GetDocumentLoadGroup(); + nsresult rv = NS_NewChannel(getter_AddRefs(channel), uri, + static_cast<Element*>(this), secFlags, + nsIContentPolicy::TYPE_INTERNAL_TRACK, + nullptr, // PerformanceStorage + loadGroup); + + if (NS_FAILED(rv)) { + LOG("create channel failed."); + SetReadyState(TextTrackReadyState::FailedToLoad); + return; + } + + channel->SetNotificationCallbacks(mListener); + + LOG("opening webvtt channel"); + rv = channel->AsyncOpen(mListener); + + if (NS_FAILED(rv)) { + SetReadyState(TextTrackReadyState::FailedToLoad); + return; + } + mChannel = channel; + }); + doc->Dispatch(TaskCategory::Other, runnable.forget()); +} + +nsresult HTMLTrackElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + LOG("Track Element bound to tree."); + auto* parent = HTMLMediaElement::FromNode(aParent); + if (!parent) { + return NS_OK; + } + + // Store our parent so we can look up its frame for display. + if (!mMediaParent) { + mMediaParent = parent; + + // TODO: separate notification for 'alternate' tracks? + mMediaParent->NotifyAddedSource(); + LOG("Track element sent notification to parent."); + + // We may already have a TextTrack at this point if GetTrack() has already + // been called. This happens, for instance, if script tries to get the + // TextTrack before its mTrackElement has been bound to the DOM tree. + if (!mTrack) { + CreateTextTrack(); + } + // As `CreateTextTrack()` might fail, so we have to check it again. + if (mTrack) { + LOG("Add text track to media parent"); + mMediaParent->AddTextTrack(mTrack); + } + MaybeDispatchLoadResource(); + } + + return NS_OK; +} + +void HTMLTrackElement::UnbindFromTree(bool aNullParent) { + if (mMediaParent && aNullParent) { + // mTrack can be null if HTMLTrackElement::LoadResource has never been + // called. + if (mTrack) { + mMediaParent->RemoveTextTrack(mTrack); + mMediaParent->UpdateReadyState(); + } + mMediaParent = nullptr; + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +TextTrackReadyState HTMLTrackElement::ReadyState() const { + if (!mTrack) { + return TextTrackReadyState::NotLoaded; + } + + return mTrack->ReadyState(); +} + +void HTMLTrackElement::SetReadyState(TextTrackReadyState aReadyState) { + if (ReadyState() == aReadyState) { + return; + } + + if (mTrack) { + switch (aReadyState) { + case TextTrackReadyState::Loaded: + LOG("dispatch 'load' event"); + DispatchTrackRunnable(u"load"_ns); + break; + case TextTrackReadyState::FailedToLoad: + LOG("dispatch 'error' event"); + DispatchTrackRunnable(u"error"_ns); + break; + default: + break; + } + mTrack->SetReadyState(aReadyState); + } +} + +void HTMLTrackElement::DispatchTrackRunnable(const nsString& aEventName) { + Document* doc = OwnerDoc(); + if (!doc) { + return; + } + nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<const nsString>( + "dom::HTMLTrackElement::DispatchTrustedEvent", this, + &HTMLTrackElement::DispatchTrustedEvent, aEventName); + doc->Dispatch(TaskCategory::Other, runnable.forget()); +} + +void HTMLTrackElement::DispatchTrustedEvent(const nsAString& aName) { + Document* doc = OwnerDoc(); + if (!doc) { + return; + } + nsContentUtils::DispatchTrustedEvent(doc, static_cast<nsIContent*>(this), + aName, CanBubble::eNo, Cancelable::eNo); +} + +void HTMLTrackElement::CancelChannelAndListener() { + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "HTMLTrackElement::CancelChannelAndListener"_ns); + mChannel->SetNotificationCallbacks(nullptr); + mChannel = nullptr; + } + + if (mListener) { + mListener->Cancel(); + mListener = nullptr; + } +} + +nsresult HTMLTrackElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) { + MaybeClearAllCues(); + // In spec, `start the track processing model` step10, while fetching is + // ongoing, if the track URL changes, then we have to set the `FailedToLoad` + // state. + // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:text-track-failed-to-load-3 + if (ReadyState() == TextTrackReadyState::Loading && aValue != aOldValue) { + SetReadyState(TextTrackReadyState::FailedToLoad); + } + } + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLTrackElement::DispatchTestEvent(const nsAString& aName) { + if (!StaticPrefs::media_webvtt_testing_events()) { + return; + } + DispatchTrustedEvent(aName); +} + +#undef LOG + +} // namespace mozilla::dom |