1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/HTMLTrackElement.h"
8 #include "mozilla/dom/Element.h"
9 #include "mozilla/dom/HTMLMediaElement.h"
10 #include "mozilla/dom/WebVTTListener.h"
11 #include "mozilla/LoadInfo.h"
12 #include "mozilla/StaticPrefs_media.h"
13 #include "mozilla/dom/HTMLTrackElementBinding.h"
14 #include "mozilla/dom/UnbindContext.h"
15 #include "nsAttrValueInlines.h"
17 #include "nsContentUtils.h"
18 #include "nsCycleCollectionParticipant.h"
19 #include "nsGenericHTMLElement.h"
20 #include "nsGkAtoms.h"
21 #include "nsIContentPolicy.h"
22 #include "mozilla/dom/Document.h"
23 #include "nsILoadGroup.h"
24 #include "nsIObserver.h"
25 #include "nsIObserverService.h"
26 #include "nsIScriptError.h"
27 #include "nsISupportsImpl.h"
28 #include "nsISupportsPrimitives.h"
29 #include "nsNetUtil.h"
30 #include "nsStyleConsts.h"
31 #include "nsThreadUtils.h"
33 extern mozilla::LazyLogModule gTextTrackLog
;
34 #define LOG(msg, ...) \
35 MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \
36 ("TextTrackElement=%p, " msg, this, ##__VA_ARGS__))
38 // Replace the usual NS_IMPL_NS_NEW_HTML_ELEMENT(Track) so
39 // we can return an UnknownElement instead when pref'd off.
40 nsGenericHTMLElement
* NS_NewHTMLTrackElement(
41 already_AddRefed
<mozilla::dom::NodeInfo
>&& aNodeInfo
,
42 mozilla::dom::FromParser aFromParser
) {
43 RefPtr
<mozilla::dom::NodeInfo
> nodeInfo(aNodeInfo
);
44 auto* nim
= nodeInfo
->NodeInfoManager();
45 return new (nim
) mozilla::dom::HTMLTrackElement(nodeInfo
.forget());
48 namespace mozilla::dom
{
50 // Map html attribute string values to TextTrackKind enums.
51 static constexpr nsAttrValue::EnumTable kKindTable
[] = {
52 {"subtitles", static_cast<int16_t>(TextTrackKind::Subtitles
)},
53 {"captions", static_cast<int16_t>(TextTrackKind::Captions
)},
54 {"descriptions", static_cast<int16_t>(TextTrackKind::Descriptions
)},
55 {"chapters", static_cast<int16_t>(TextTrackKind::Chapters
)},
56 {"metadata", static_cast<int16_t>(TextTrackKind::Metadata
)},
59 // Invalid values are treated as "metadata" in ParseAttribute, but if no value
60 // at all is specified, it's treated as "subtitles" in GetKind
61 static constexpr const nsAttrValue::EnumTable
* kKindTableInvalidValueDefault
=
64 class WindowDestroyObserver final
: public nsIObserver
{
68 explicit WindowDestroyObserver(HTMLTrackElement
* aElement
, uint64_t aWinID
)
69 : mTrackElement(aElement
), mInnerID(aWinID
) {
70 RegisterWindowDestroyObserver();
72 void RegisterWindowDestroyObserver() {
73 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
75 obs
->AddObserver(this, "inner-window-destroyed", false);
78 void UnRegisterWindowDestroyObserver() {
79 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
81 obs
->RemoveObserver(this, "inner-window-destroyed");
83 mTrackElement
= nullptr;
85 NS_IMETHODIMP
Observe(nsISupports
* aSubject
, const char* aTopic
,
86 const char16_t
* aData
) override
{
87 MOZ_ASSERT(NS_IsMainThread());
88 if (strcmp(aTopic
, "inner-window-destroyed") == 0) {
89 nsCOMPtr
<nsISupportsPRUint64
> wrapper
= do_QueryInterface(aSubject
);
90 NS_ENSURE_TRUE(wrapper
, NS_ERROR_FAILURE
);
92 nsresult rv
= wrapper
->GetData(&innerID
);
93 NS_ENSURE_SUCCESS(rv
, rv
);
94 if (innerID
== mInnerID
) {
96 mTrackElement
->CancelChannelAndListener();
98 UnRegisterWindowDestroyObserver();
105 ~WindowDestroyObserver() = default;
107 HTMLTrackElement
* mTrackElement
;
110 NS_IMPL_ISUPPORTS(WindowDestroyObserver
, nsIObserver
);
112 /** HTMLTrackElement */
113 HTMLTrackElement::HTMLTrackElement(
114 already_AddRefed
<mozilla::dom::NodeInfo
>&& aNodeInfo
)
115 : nsGenericHTMLElement(std::move(aNodeInfo
)),
116 mLoadResourceDispatched(false),
117 mWindowDestroyObserver(nullptr) {
118 nsISupports
* parentObject
= OwnerDoc()->GetParentObject();
119 NS_ENSURE_TRUE_VOID(parentObject
);
120 nsCOMPtr
<nsPIDOMWindowInner
> window
= do_QueryInterface(parentObject
);
122 mWindowDestroyObserver
=
123 new WindowDestroyObserver(this, window
->WindowID());
127 HTMLTrackElement::~HTMLTrackElement() {
128 if (mWindowDestroyObserver
) {
129 mWindowDestroyObserver
->UnRegisterWindowDestroyObserver();
131 CancelChannelAndListener();
134 NS_IMPL_ELEMENT_CLONE(HTMLTrackElement
)
136 NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTrackElement
, nsGenericHTMLElement
,
137 mTrack
, mMediaParent
, mListener
)
139 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTrackElement
,
140 nsGenericHTMLElement
)
142 void HTMLTrackElement::GetKind(DOMString
& aKind
) const {
143 GetEnumAttr(nsGkAtoms::kind
, kKindTable
[0].tag
, aKind
);
146 void HTMLTrackElement::OnChannelRedirect(nsIChannel
* aChannel
,
147 nsIChannel
* aNewChannel
,
149 NS_ASSERTION(aChannel
== mChannel
, "Channels should match!");
150 mChannel
= aNewChannel
;
153 JSObject
* HTMLTrackElement::WrapNode(JSContext
* aCx
,
154 JS::Handle
<JSObject
*> aGivenProto
) {
155 return HTMLTrackElement_Binding::Wrap(aCx
, this, aGivenProto
);
158 TextTrack
* HTMLTrackElement::GetTrack() {
165 void HTMLTrackElement::CreateTextTrack() {
166 nsISupports
* parentObject
= OwnerDoc()->GetParentObject();
167 nsCOMPtr
<nsPIDOMWindowInner
> window
= do_QueryInterface(parentObject
);
169 nsContentUtils::ReportToConsole(
170 nsIScriptError::errorFlag
, "Media"_ns
, OwnerDoc(),
171 nsContentUtils::eDOM_PROPERTIES
,
172 "Using track element in non-window context");
176 nsString label
, srcLang
;
181 if (const nsAttrValue
* value
= GetParsedAttr(nsGkAtoms::kind
)) {
182 kind
= static_cast<TextTrackKind
>(value
->GetEnumValue());
184 kind
= TextTrackKind::Subtitles
;
187 MOZ_ASSERT(!mTrack
, "No need to recreate a text track!");
189 new TextTrack(window
, kind
, label
, srcLang
, TextTrackMode::Disabled
,
190 TextTrackReadyState::NotLoaded
, TextTrackSource::Track
);
191 mTrack
->SetTrackElement(this);
194 bool HTMLTrackElement::ParseAttribute(int32_t aNamespaceID
, nsAtom
* aAttribute
,
195 const nsAString
& aValue
,
196 nsIPrincipal
* aMaybeScriptedPrincipal
,
197 nsAttrValue
& aResult
) {
198 if (aNamespaceID
== kNameSpaceID_None
&& aAttribute
== nsGkAtoms::kind
) {
199 // Case-insensitive lookup, with the first element as the default.
200 return aResult
.ParseEnumValue(aValue
, kKindTable
, false,
201 kKindTableInvalidValueDefault
);
204 // Otherwise call the generic implementation.
205 return nsGenericHTMLElement::ParseAttribute(aNamespaceID
, aAttribute
, aValue
,
206 aMaybeScriptedPrincipal
, aResult
);
209 void HTMLTrackElement::SetSrc(const nsAString
& aSrc
, ErrorResult
& aError
) {
210 LOG("Set src=%s", NS_ConvertUTF16toUTF8(aSrc
).get());
213 if (GetAttr(nsGkAtoms::src
, src
) && src
== aSrc
) {
214 LOG("No need to reload for same src url");
218 SetHTMLAttr(nsGkAtoms::src
, aSrc
, aError
);
219 SetReadyState(TextTrackReadyState::NotLoaded
);
224 // Stop WebVTTListener.
227 mChannel
->CancelWithReason(NS_BINDING_ABORTED
,
228 "HTMLTrackElement::SetSrc"_ns
);
232 MaybeDispatchLoadResource();
235 void HTMLTrackElement::MaybeClearAllCues() {
236 // Empty track's cue list whenever the track element's `src` attribute set,
237 // changed, or removed,
238 // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:attr-track-src
242 mTrack
->ClearAllCues();
245 // This function will run partial steps from `start-the-track-processing-model`
246 // and finish the rest of steps in `LoadResource()` during the stable state.
247 // https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model
248 void HTMLTrackElement::MaybeDispatchLoadResource() {
249 MOZ_ASSERT(mTrack
, "Should have already created text track!");
251 // step2, if the text track's text track mode is not set to one of hidden or
252 // showing, then return.
253 if (mTrack
->Mode() == TextTrackMode::Disabled
) {
254 LOG("Do not load resource for disable track");
258 // step3, if the text track's track element does not have a media element as a
261 LOG("Do not load resource for track without media element");
265 if (ReadyState() == TextTrackReadyState::Loaded
) {
266 LOG("Has already loaded resource");
270 // step5, await a stable state and run the rest of steps.
271 if (!mLoadResourceDispatched
) {
272 RefPtr
<WebVTTListener
> listener
= new WebVTTListener(this);
273 RefPtr
<Runnable
> r
= NewRunnableMethod
<RefPtr
<WebVTTListener
>>(
274 "dom::HTMLTrackElement::LoadResource", this,
275 &HTMLTrackElement::LoadResource
, std::move(listener
));
276 nsContentUtils::RunInStableState(r
.forget());
277 mLoadResourceDispatched
= true;
281 void HTMLTrackElement::LoadResource(RefPtr
<WebVTTListener
>&& aWebVTTListener
) {
283 mLoadResourceDispatched
= false;
286 if (!GetAttr(nsGkAtoms::src
, src
) || src
.IsEmpty()) {
287 LOG("Fail to load because no src");
288 SetReadyState(TextTrackReadyState::FailedToLoad
);
292 nsCOMPtr
<nsIURI
> uri
;
293 nsresult rv
= NewURIFromString(src
, getter_AddRefs(uri
));
294 NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv
));
295 LOG("Trying to load from src=%s", NS_ConvertUTF16toUTF8(src
).get());
297 CancelChannelAndListener();
300 // https://www.w3.org/TR/html5/embedded-content-0.html#sourcing-out-of-band-text-tracks
302 // "8: If the track element's parent is a media element then let CORS mode
303 // be the state of the parent media element's crossorigin content attribute.
304 // Otherwise, let CORS mode be No CORS."
307 mMediaParent
? AttrValueToCORSMode(
308 mMediaParent
->GetParsedAttr(nsGkAtoms::crossorigin
))
311 // Determine the security flag based on corsMode.
312 nsSecurityFlags secFlags
;
313 if (CORS_NONE
== corsMode
) {
314 // Same-origin is required for track element.
315 secFlags
= nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT
;
317 secFlags
= nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT
;
318 if (CORS_ANONYMOUS
== corsMode
) {
319 secFlags
|= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN
;
320 } else if (CORS_USE_CREDENTIALS
== corsMode
) {
321 secFlags
|= nsILoadInfo::SEC_COOKIES_INCLUDE
;
323 NS_WARNING("Unknown CORS mode.");
324 secFlags
= nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT
;
328 mListener
= std::move(aWebVTTListener
);
329 // This will do 6. Set the text track readiness state to loading.
330 rv
= mListener
->LoadResource();
331 NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv
));
333 Document
* doc
= OwnerDoc();
338 // 9. End the synchronous section, continuing the remaining steps in parallel.
339 nsCOMPtr
<nsIRunnable
> runnable
= NS_NewRunnableFunction(
340 "dom::HTMLTrackElement::LoadResource",
341 [self
= RefPtr
<HTMLTrackElement
>(this), this, uri
, secFlags
]() {
343 // Shutdown got called, abort.
346 nsCOMPtr
<nsIChannel
> channel
;
347 nsCOMPtr
<nsILoadGroup
> loadGroup
= OwnerDoc()->GetDocumentLoadGroup();
348 nsresult rv
= NS_NewChannel(getter_AddRefs(channel
), uri
,
349 static_cast<Element
*>(this), secFlags
,
350 nsIContentPolicy::TYPE_INTERNAL_TRACK
,
351 nullptr, // PerformanceStorage
355 LOG("create channel failed.");
356 SetReadyState(TextTrackReadyState::FailedToLoad
);
360 channel
->SetNotificationCallbacks(mListener
);
362 LOG("opening webvtt channel");
363 rv
= channel
->AsyncOpen(mListener
);
366 SetReadyState(TextTrackReadyState::FailedToLoad
);
371 doc
->Dispatch(runnable
.forget());
374 nsresult
HTMLTrackElement::BindToTree(BindContext
& aContext
, nsINode
& aParent
) {
375 nsresult rv
= nsGenericHTMLElement::BindToTree(aContext
, aParent
);
376 NS_ENSURE_SUCCESS(rv
, rv
);
378 LOG("Track Element bound to tree.");
379 auto* parent
= HTMLMediaElement::FromNode(aParent
);
384 // Store our parent so we can look up its frame for display.
386 mMediaParent
= parent
;
388 // TODO: separate notification for 'alternate' tracks?
389 mMediaParent
->NotifyAddedSource();
390 LOG("Track element sent notification to parent.");
392 // We may already have a TextTrack at this point if GetTrack() has already
393 // been called. This happens, for instance, if script tries to get the
394 // TextTrack before its mTrackElement has been bound to the DOM tree.
398 // As `CreateTextTrack()` might fail, so we have to check it again.
400 LOG("Add text track to media parent");
401 mMediaParent
->AddTextTrack(mTrack
);
403 MaybeDispatchLoadResource();
409 void HTMLTrackElement::UnbindFromTree(UnbindContext
& aContext
) {
410 if (mMediaParent
&& aContext
.IsUnbindRoot(this)) {
411 // mTrack can be null if HTMLTrackElement::LoadResource has never been
414 mMediaParent
->RemoveTextTrack(mTrack
);
415 mMediaParent
->UpdateReadyState();
417 mMediaParent
= nullptr;
420 nsGenericHTMLElement::UnbindFromTree(aContext
);
423 TextTrackReadyState
HTMLTrackElement::ReadyState() const {
425 return TextTrackReadyState::NotLoaded
;
428 return mTrack
->ReadyState();
431 void HTMLTrackElement::SetReadyState(TextTrackReadyState aReadyState
) {
432 if (ReadyState() == aReadyState
) {
437 switch (aReadyState
) {
438 case TextTrackReadyState::Loaded
:
439 LOG("dispatch 'load' event");
440 DispatchTrackRunnable(u
"load"_ns
);
442 case TextTrackReadyState::FailedToLoad
:
443 LOG("dispatch 'error' event");
444 DispatchTrackRunnable(u
"error"_ns
);
449 mTrack
->SetReadyState(aReadyState
);
453 void HTMLTrackElement::DispatchTrackRunnable(const nsString
& aEventName
) {
454 Document
* doc
= OwnerDoc();
458 nsCOMPtr
<nsIRunnable
> runnable
= NewRunnableMethod
<const nsString
>(
459 "dom::HTMLTrackElement::DispatchTrustedEvent", this,
460 &HTMLTrackElement::DispatchTrustedEvent
, aEventName
);
461 doc
->Dispatch(runnable
.forget());
464 void HTMLTrackElement::DispatchTrustedEvent(const nsAString
& aName
) {
465 Document
* doc
= OwnerDoc();
469 nsContentUtils::DispatchTrustedEvent(doc
, this, aName
, CanBubble::eNo
,
473 void HTMLTrackElement::CancelChannelAndListener() {
475 mChannel
->CancelWithReason(NS_BINDING_ABORTED
,
476 "HTMLTrackElement::CancelChannelAndListener"_ns
);
477 mChannel
->SetNotificationCallbacks(nullptr);
487 void HTMLTrackElement::AfterSetAttr(int32_t aNameSpaceID
, nsAtom
* aName
,
488 const nsAttrValue
* aValue
,
489 const nsAttrValue
* aOldValue
,
490 nsIPrincipal
* aMaybeScriptedPrincipal
,
492 if (aNameSpaceID
== kNameSpaceID_None
&& aName
== nsGkAtoms::src
) {
494 // In spec, `start the track processing model` step10, while fetching is
495 // ongoing, if the track URL changes, then we have to set the `FailedToLoad`
497 // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:text-track-failed-to-load-3
498 if (ReadyState() == TextTrackReadyState::Loading
&& aValue
!= aOldValue
) {
499 SetReadyState(TextTrackReadyState::FailedToLoad
);
502 return nsGenericHTMLElement::AfterSetAttr(
503 aNameSpaceID
, aName
, aValue
, aOldValue
, aMaybeScriptedPrincipal
, aNotify
);
506 void HTMLTrackElement::DispatchTestEvent(const nsAString
& aName
) {
507 if (!StaticPrefs::media_webvtt_testing_events()) {
510 DispatchTrustedEvent(aName
);
515 } // namespace mozilla::dom