/* -*- 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/Element.h" #include "mozilla/dom/HTMLMediaElement.h" #include "mozilla/dom/HTMLTrackElement.h" #include "mozilla/dom/HTMLTrackElementBinding.h" #include "mozilla/dom/HTMLUnknownElement.h" #include "nsIContentPolicy.h" #include "mozilla/LoadInfo.h" #include "WebVTTListener.h" #include "nsAttrValueInlines.h" #include "nsCOMPtr.h" #include "nsContentPolicyUtils.h" #include "nsContentUtils.h" #include "nsCycleCollectionParticipant.h" #include "nsGenericHTMLElement.h" #include "nsGkAtoms.h" #include "nsIAsyncVerifyRedirectCallback.h" #include "nsICachingChannel.h" #include "nsIChannelEventSink.h" #include "nsIContentPolicy.h" #include "nsIContentSecurityPolicy.h" #include "nsIDocument.h" #include "nsIHttpChannel.h" #include "nsIInterfaceRequestor.h" #include "nsILoadGroup.h" #include "nsIObserver.h" #include "nsIStreamListener.h" #include "nsISupportsImpl.h" #include "nsISupportsPrimitives.h" #include "nsMappedAttributes.h" #include "nsNetUtil.h" #include "nsStyleConsts.h" #include "nsThreadUtils.h" #include "nsVideoFrame.h" static mozilla::LazyLogModule gTrackElementLog("nsTrackElement"); #define LOG(type, msg) MOZ_LOG(gTrackElementLog, type, msg) // 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&& aNodeInfo, mozilla::dom::FromParser aFromParser) { return new mozilla::dom::HTMLTrackElement(aNodeInfo); } namespace mozilla { namespace dom { // Map html attribute string values to TextTrackKind enums. static constexpr nsAttrValue::EnumTable kKindTable[] = { { "subtitles", static_cast(TextTrackKind::Subtitles) }, { "captions", static_cast(TextTrackKind::Captions) }, { "descriptions", static_cast(TextTrackKind::Descriptions) }, { "chapters", static_cast(TextTrackKind::Chapters) }, { "metadata", static_cast(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 const nsAttrValue::EnumTable* const 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 obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(this, "inner-window-destroyed", false); } } void UnRegisterWindowDestroyObserver() { nsCOMPtr 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 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->NotifyShutdown(); } UnRegisterWindowDestroyObserver(); } } return NS_OK; } private: ~WindowDestroyObserver() {}; HTMLTrackElement* mTrackElement; uint64_t mInnerID; }; NS_IMPL_ISUPPORTS(WindowDestroyObserver, nsIObserver); /** HTMLTrackElement */ HTMLTrackElement::HTMLTrackElement(already_AddRefed& aNodeInfo) : nsGenericHTMLElement(aNodeInfo) , mLoadResourceDispatched(false) , mWindowDestroyObserver(nullptr) { nsISupports* parentObject = OwnerDoc()->GetParentObject(); NS_ENSURE_TRUE_VOID(parentObject); nsCOMPtr window = do_QueryInterface(parentObject); if (window) { mWindowDestroyObserver = new WindowDestroyObserver(this, window->WindowID()); } } HTMLTrackElement::~HTMLTrackElement() { if (mWindowDestroyObserver) { mWindowDestroyObserver->UnRegisterWindowDestroyObserver(); } NotifyShutdown(); } 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 aGivenProto) { return HTMLTrackElement_Binding::Wrap(aCx, this, aGivenProto); } TextTrack* HTMLTrackElement::GetTrack() { if (!mTrack) { CreateTextTrack(); } return mTrack; } void HTMLTrackElement::CreateTextTrack() { nsString label, srcLang; GetSrclang(srcLang); GetLabel(label); TextTrackKind kind; if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::kind)) { kind = static_cast(value->GetEnumValue()); } else { kind = TextTrackKind::Subtitles; } nsISupports* parentObject = OwnerDoc()->GetParentObject(); NS_ENSURE_TRUE_VOID(parentObject); nsCOMPtr window = do_QueryInterface(parentObject); mTrack = new TextTrack(window, kind, label, srcLang, TextTrackMode::Disabled, TextTrackReadyState::NotLoaded, TextTrackSource::Track); mTrack->SetTrackElement(this); if (mMediaParent) { mMediaParent->AddTextTrack(mTrack); } } 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) { SetHTMLAttr(nsGkAtoms::src, aSrc, aError); uint16_t oldReadyState = ReadyState(); SetReadyState(TextTrackReadyState::NotLoaded); if (!mMediaParent) { return; } if (mTrack && (oldReadyState != TextTrackReadyState::NotLoaded)) { // Remove all the cues in MediaElement. mMediaParent->RemoveTextTrack(mTrack); // Recreate mTrack. CreateTextTrack(); } // Stop WebVTTListener. mListener = nullptr; if (mChannel) { mChannel->Cancel(NS_BINDING_ABORTED); mChannel = nullptr; } DispatchLoadResource(); } void HTMLTrackElement::DispatchLoadResource() { if (!mLoadResourceDispatched) { RefPtr r = NewRunnableMethod("dom::HTMLTrackElement::LoadResource", this, &HTMLTrackElement::LoadResource); nsContentUtils::RunInStableState(r.forget()); mLoadResourceDispatched = true; } } void HTMLTrackElement::LoadResource() { mLoadResourceDispatched = false; // Find our 'src' url nsAutoString src; if (!GetAttr(kNameSpaceID_None, nsGkAtoms::src, src)) { return; } nsCOMPtr uri; nsresult rv = NewURIFromString(src, getter_AddRefs(uri)); NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); LOG(LogLevel::Info, ("%p Trying to load from src=%s", this, NS_ConvertUTF16toUTF8(src).get())); if (mChannel) { mChannel->Cancel(NS_BINDING_ABORTED); mChannel = nullptr; } // 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 ? mMediaParent->GetCORSMode() : 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_DATA_INHERITS; } else { secFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS; 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_DATA_INHERITS; } } nsCOMPtr channel; nsCOMPtr loadGroup = OwnerDoc()->GetDocumentLoadGroup(); rv = NS_NewChannel(getter_AddRefs(channel), uri, static_cast(this), secFlags, nsIContentPolicy::TYPE_INTERNAL_TRACK, nullptr, // PerformanceStorage loadGroup, nullptr, // aCallbacks nsIRequest::LOAD_NORMAL | nsIChannel::LOAD_CLASSIFY_URI); NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); mListener = new WebVTTListener(this); rv = mListener->LoadResource(); NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); channel->SetNotificationCallbacks(mListener); LOG(LogLevel::Debug, ("opening webvtt channel")); rv = channel->AsyncOpen2(mListener); if (NS_FAILED(rv)) { SetReadyState(TextTrackReadyState::FailedToLoad); return; } mChannel = channel; } nsresult HTMLTrackElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent, nsIContent* aBindingParent) { nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent, aBindingParent); NS_ENSURE_SUCCESS(rv, rv); LOG(LogLevel::Debug, ("Track Element bound to tree.")); auto* parent = HTMLMediaElement::FromNodeOrNull(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(LogLevel::Debug, ("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(); } DispatchLoadResource(); } return NS_OK; } void HTMLTrackElement::UnbindFromTree(bool aDeep, 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(aDeep, aNullParent); } uint16_t HTMLTrackElement::ReadyState() const { if (!mTrack) { return TextTrackReadyState::NotLoaded; } return mTrack->ReadyState(); } void HTMLTrackElement::SetReadyState(uint16_t aReadyState) { if (ReadyState() == aReadyState) { return; } if (mTrack) { switch (aReadyState) { case TextTrackReadyState::Loaded: DispatchTrackRunnable(NS_LITERAL_STRING("load")); break; case TextTrackReadyState::FailedToLoad: DispatchTrackRunnable(NS_LITERAL_STRING("error")); break; } mTrack->SetReadyState(aReadyState); } } void HTMLTrackElement::DispatchTrackRunnable(const nsString& aEventName) { nsIDocument* doc = OwnerDoc(); if (!doc) { return; } nsCOMPtr runnable = NewRunnableMethod( "dom::HTMLTrackElement::DispatchTrustedEvent", this, &HTMLTrackElement::DispatchTrustedEvent, aEventName); doc->Dispatch(TaskCategory::Other, runnable.forget()); } void HTMLTrackElement::DispatchTrustedEvent(const nsAString& aName) { nsIDocument* doc = OwnerDoc(); if (!doc) { return; } nsContentUtils::DispatchTrustedEvent(doc, static_cast(this), aName, CanBubble::eNo, Cancelable::eNo); } void HTMLTrackElement::DropChannel() { mChannel = nullptr; } void HTMLTrackElement::NotifyShutdown() { if (mChannel) { mChannel->Cancel(NS_BINDING_ABORTED); } mChannel = nullptr; mListener = nullptr; } } // namespace dom } // namespace mozilla