HTMLTrackElement.cpp (18343B)
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/. */ 6 7 #include "mozilla/dom/HTMLTrackElement.h" 8 9 #include "mozilla/LoadInfo.h" 10 #include "mozilla/StaticPrefs_media.h" 11 #include "mozilla/dom/Document.h" 12 #include "mozilla/dom/Element.h" 13 #include "mozilla/dom/HTMLMediaElement.h" 14 #include "mozilla/dom/HTMLTrackElementBinding.h" 15 #include "mozilla/dom/UnbindContext.h" 16 #include "mozilla/dom/WebVTTListener.h" 17 #include "nsAttrValueInlines.h" 18 #include "nsCOMPtr.h" 19 #include "nsContentUtils.h" 20 #include "nsCycleCollectionParticipant.h" 21 #include "nsGenericHTMLElement.h" 22 #include "nsGkAtoms.h" 23 #include "nsIContentPolicy.h" 24 #include "nsILoadGroup.h" 25 #include "nsIObserver.h" 26 #include "nsIObserverService.h" 27 #include "nsIScriptError.h" 28 #include "nsISupportsImpl.h" 29 #include "nsISupportsPrimitives.h" 30 #include "nsNetUtil.h" 31 #include "nsStyleConsts.h" 32 #include "nsThreadUtils.h" 33 34 extern mozilla::LazyLogModule gTextTrackLog; 35 #define LOG(msg, ...) \ 36 MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \ 37 ("TextTrackElement=%p, " msg, this, ##__VA_ARGS__)) 38 39 // Replace the usual NS_IMPL_NS_NEW_HTML_ELEMENT(Track) so 40 // we can return an UnknownElement instead when pref'd off. 41 nsGenericHTMLElement* NS_NewHTMLTrackElement( 42 already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, 43 mozilla::dom::FromParser aFromParser) { 44 RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); 45 auto* nim = nodeInfo->NodeInfoManager(); 46 return new (nim) mozilla::dom::HTMLTrackElement(nodeInfo.forget()); 47 } 48 49 namespace mozilla::dom { 50 51 // Map html attribute string values to TextTrackKind enums. 52 static constexpr nsAttrValue::EnumTableEntry kKindTable[] = { 53 {"subtitles", static_cast<int16_t>(TextTrackKind::Subtitles)}, 54 {"captions", static_cast<int16_t>(TextTrackKind::Captions)}, 55 {"descriptions", static_cast<int16_t>(TextTrackKind::Descriptions)}, 56 {"chapters", static_cast<int16_t>(TextTrackKind::Chapters)}, 57 {"metadata", static_cast<int16_t>(TextTrackKind::Metadata)}, 58 }; 59 60 // Invalid values are treated as "metadata" in ParseAttribute, but if no value 61 // at all is specified, it's treated as "subtitles" in GetKind 62 static constexpr const nsAttrValue::EnumTableEntry* 63 kKindTableInvalidValueDefault = &kKindTable[4]; 64 65 class WindowDestroyObserver final : public nsIObserver { 66 NS_DECL_ISUPPORTS 67 68 public: 69 explicit WindowDestroyObserver(HTMLTrackElement* aElement, uint64_t aWinID) 70 : mTrackElement(aElement), mInnerID(aWinID) { 71 RegisterWindowDestroyObserver(); 72 } 73 void RegisterWindowDestroyObserver() { 74 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 75 if (obs) { 76 obs->AddObserver(this, "inner-window-destroyed", false); 77 } 78 } 79 void UnRegisterWindowDestroyObserver() { 80 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 81 if (obs) { 82 obs->RemoveObserver(this, "inner-window-destroyed"); 83 } 84 mTrackElement = nullptr; 85 } 86 NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic, 87 const char16_t* aData) override { 88 MOZ_ASSERT(NS_IsMainThread()); 89 if (strcmp(aTopic, "inner-window-destroyed") == 0) { 90 nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); 91 NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); 92 uint64_t innerID; 93 nsresult rv = wrapper->GetData(&innerID); 94 NS_ENSURE_SUCCESS(rv, rv); 95 if (innerID == mInnerID) { 96 if (mTrackElement) { 97 // Window is being destroyed, cancel without checking for RFP. 98 mTrackElement->CancelChannelAndListener(false); 99 } 100 UnRegisterWindowDestroyObserver(); 101 } 102 } 103 return NS_OK; 104 } 105 106 private: 107 ~WindowDestroyObserver() = default; 108 109 HTMLTrackElement* mTrackElement; 110 uint64_t mInnerID; 111 }; 112 NS_IMPL_ISUPPORTS(WindowDestroyObserver, nsIObserver); 113 114 /** HTMLTrackElement */ 115 HTMLTrackElement::HTMLTrackElement( 116 already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) 117 : nsGenericHTMLElement(std::move(aNodeInfo)), 118 mLoadResourceDispatched(false), 119 mWindowDestroyObserver(nullptr) { 120 nsISupports* parentObject = OwnerDoc()->GetParentObject(); 121 NS_ENSURE_TRUE_VOID(parentObject); 122 nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); 123 if (window) { 124 mWindowDestroyObserver = 125 new WindowDestroyObserver(this, window->WindowID()); 126 } 127 } 128 129 HTMLTrackElement::~HTMLTrackElement() { 130 if (mWindowDestroyObserver) { 131 mWindowDestroyObserver->UnRegisterWindowDestroyObserver(); 132 } 133 // Track element is being destroyed, cancel without checking for RFP. 134 CancelChannelAndListener(false); 135 } 136 137 NS_IMPL_ELEMENT_CLONE(HTMLTrackElement) 138 139 NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTrackElement, nsGenericHTMLElement, 140 mTrack, mMediaParent, mListener) 141 142 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTrackElement, 143 nsGenericHTMLElement) 144 145 void HTMLTrackElement::GetKind(DOMString& aKind) const { 146 GetEnumAttr(nsGkAtoms::kind, kKindTable[0].tag, aKind); 147 } 148 149 void HTMLTrackElement::OnChannelRedirect(nsIChannel* aChannel, 150 nsIChannel* aNewChannel, 151 uint32_t aFlags) { 152 NS_ASSERTION(aChannel == mChannel, "Channels should match!"); 153 mChannel = aNewChannel; 154 } 155 156 JSObject* HTMLTrackElement::WrapNode(JSContext* aCx, 157 JS::Handle<JSObject*> aGivenProto) { 158 return HTMLTrackElement_Binding::Wrap(aCx, this, aGivenProto); 159 } 160 161 TextTrack* HTMLTrackElement::GetTrack() { 162 if (!mTrack) { 163 CreateTextTrack(); 164 } 165 return mTrack; 166 } 167 168 void HTMLTrackElement::CreateTextTrack() { 169 nsISupports* parentObject = OwnerDoc()->GetParentObject(); 170 nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); 171 if (!parentObject) { 172 nsContentUtils::ReportToConsole( 173 nsIScriptError::errorFlag, "Media"_ns, OwnerDoc(), 174 nsContentUtils::eDOM_PROPERTIES, 175 "Using track element in non-window context"); 176 return; 177 } 178 179 nsString label, srcLang; 180 GetSrclang(srcLang); 181 GetLabel(label); 182 183 TextTrackKind kind; 184 if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::kind)) { 185 kind = static_cast<TextTrackKind>(value->GetEnumValue()); 186 } else { 187 kind = TextTrackKind::Subtitles; 188 } 189 190 MOZ_ASSERT(!mTrack, "No need to recreate a text track!"); 191 mTrack = 192 new TextTrack(window, kind, label, srcLang, TextTrackMode::Disabled, 193 TextTrackReadyState::NotLoaded, TextTrackSource::Track); 194 mTrack->SetTrackElement(this); 195 } 196 197 bool HTMLTrackElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, 198 const nsAString& aValue, 199 nsIPrincipal* aMaybeScriptedPrincipal, 200 nsAttrValue& aResult) { 201 if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::kind) { 202 // Case-insensitive lookup, with the first element as the default. 203 return aResult.ParseEnumValue(aValue, kKindTable, false, 204 kKindTableInvalidValueDefault); 205 } 206 207 // Otherwise call the generic implementation. 208 return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, 209 aMaybeScriptedPrincipal, aResult); 210 } 211 212 void HTMLTrackElement::SetSrc(const nsAString& aSrc, ErrorResult& aError) { 213 LOG("Set src=%s", NS_ConvertUTF16toUTF8(aSrc).get()); 214 215 nsAutoString src; 216 if (GetAttr(nsGkAtoms::src, src) && src == aSrc) { 217 LOG("No need to reload for same src url"); 218 return; 219 } 220 221 SetHTMLAttr(nsGkAtoms::src, aSrc, aError); 222 SetReadyState(TextTrackReadyState::NotLoaded); 223 if (!mMediaParent) { 224 return; 225 } 226 227 // Stop WebVTTListener. 228 mListener = nullptr; 229 if (mChannel) { 230 mChannel->CancelWithReason(NS_BINDING_ABORTED, 231 "HTMLTrackElement::SetSrc"_ns); 232 mChannel = nullptr; 233 } 234 235 MaybeDispatchLoadResource(); 236 } 237 238 void HTMLTrackElement::MaybeClearAllCues() { 239 // Empty track's cue list whenever the track element's `src` attribute set, 240 // changed, or removed, 241 // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:attr-track-src 242 if (!mTrack) { 243 return; 244 } 245 mTrack->ClearAllCues(); 246 } 247 248 // This function will run partial steps from `start-the-track-processing-model` 249 // and finish the rest of steps in `LoadResource()` during the stable state. 250 // https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model 251 void HTMLTrackElement::MaybeDispatchLoadResource() { 252 MOZ_ASSERT(mTrack, "Should have already created text track!"); 253 254 // step2, if the text track's text track mode is not set to one of hidden or 255 // showing, then return. 256 bool resistFingerprinting = ShouldResistFingerprinting(RFPTarget::WebVTT); 257 if (mTrack->Mode() == TextTrackMode::Disabled && !resistFingerprinting) { 258 LOG("Do not load resource for disable track"); 259 return; 260 } 261 262 // We need to do this check in order to prevent 263 // HonorUserPreferencesForTrackSelection from loading the text track twice. 264 if (resistFingerprinting && ReadyState() == TextTrackReadyState::Loading) { 265 return; 266 } 267 268 // step3, if the text track's track element does not have a media element as a 269 // parent, return. 270 if (!mMediaParent) { 271 LOG("Do not load resource for track without media element"); 272 return; 273 } 274 275 if (ReadyState() == TextTrackReadyState::Loaded) { 276 LOG("Has already loaded resource"); 277 return; 278 } 279 280 // step5, await a stable state and run the rest of steps. 281 if (!mLoadResourceDispatched) { 282 RefPtr<WebVTTListener> listener = new WebVTTListener(this); 283 RefPtr<Runnable> r = NewRunnableMethod<RefPtr<WebVTTListener>>( 284 "dom::HTMLTrackElement::LoadResource", this, 285 &HTMLTrackElement::LoadResource, std::move(listener)); 286 nsContentUtils::RunInStableState(r.forget()); 287 mLoadResourceDispatched = true; 288 } 289 } 290 291 void HTMLTrackElement::LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener) { 292 LOG("LoadResource"); 293 mLoadResourceDispatched = false; 294 295 nsAutoString src; 296 if (!GetAttr(nsGkAtoms::src, src) || src.IsEmpty()) { 297 LOG("Fail to load because no src"); 298 SetReadyState(TextTrackReadyState::FailedToLoad); 299 return; 300 } 301 302 nsCOMPtr<nsIURI> uri; 303 nsresult rv = NewURIFromString(src, getter_AddRefs(uri)); 304 NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); 305 LOG("Trying to load from src=%s", NS_ConvertUTF16toUTF8(src).get()); 306 307 // Prevent canceling the channel and listener if RFP is enabled. 308 CancelChannelAndListener(true); 309 310 // According to 311 // https://www.w3.org/TR/html5/embedded-content-0.html#sourcing-out-of-band-text-tracks 312 // 313 // "8: If the track element's parent is a media element then let CORS mode 314 // be the state of the parent media element's crossorigin content attribute. 315 // Otherwise, let CORS mode be No CORS." 316 // 317 CORSMode corsMode = 318 mMediaParent ? AttrValueToCORSMode( 319 mMediaParent->GetParsedAttr(nsGkAtoms::crossorigin)) 320 : CORS_NONE; 321 322 // Determine the security flag based on corsMode. 323 nsSecurityFlags secFlags; 324 if (CORS_NONE == corsMode) { 325 // Same-origin is required for track element. 326 secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; 327 } else { 328 secFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; 329 if (CORS_ANONYMOUS == corsMode) { 330 secFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; 331 } else if (CORS_USE_CREDENTIALS == corsMode) { 332 secFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; 333 } else { 334 NS_WARNING("Unknown CORS mode."); 335 secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; 336 } 337 } 338 339 mListener = std::move(aWebVTTListener); 340 // This will do 6. Set the text track readiness state to loading. 341 rv = mListener->LoadResource(); 342 NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); 343 344 Document* doc = OwnerDoc(); 345 if (!doc) { 346 return; 347 } 348 349 // 9. End the synchronous section, continuing the remaining steps in parallel. 350 nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( 351 "dom::HTMLTrackElement::LoadResource", 352 [self = RefPtr<HTMLTrackElement>(this), this, uri, secFlags]() { 353 if (!mListener) { 354 // Shutdown got called, abort. 355 return; 356 } 357 nsCOMPtr<nsIChannel> channel; 358 nsCOMPtr<nsILoadGroup> loadGroup = OwnerDoc()->GetDocumentLoadGroup(); 359 nsresult rv = NS_NewChannel(getter_AddRefs(channel), uri, 360 static_cast<Element*>(this), secFlags, 361 nsIContentPolicy::TYPE_INTERNAL_TRACK, 362 nullptr, // PerformanceStorage 363 loadGroup); 364 365 if (NS_FAILED(rv)) { 366 LOG("create channel failed."); 367 SetReadyState(TextTrackReadyState::FailedToLoad); 368 return; 369 } 370 371 channel->SetNotificationCallbacks(mListener); 372 373 LOG("opening webvtt channel"); 374 rv = channel->AsyncOpen(mListener); 375 376 if (NS_FAILED(rv)) { 377 SetReadyState(TextTrackReadyState::FailedToLoad); 378 return; 379 } 380 mChannel = channel; 381 }); 382 doc->Dispatch(runnable.forget()); 383 } 384 385 nsresult HTMLTrackElement::BindToTree(BindContext& aContext, nsINode& aParent) { 386 nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); 387 NS_ENSURE_SUCCESS(rv, rv); 388 389 LOG("Track Element bound to tree."); 390 auto* parent = HTMLMediaElement::FromNode(aParent); 391 if (!parent) { 392 return NS_OK; 393 } 394 395 // Store our parent so we can look up its frame for display. 396 if (!mMediaParent) { 397 mMediaParent = parent; 398 399 // TODO: separate notification for 'alternate' tracks? 400 mMediaParent->NotifyAddedSource(); 401 LOG("Track element sent notification to parent."); 402 403 // We may already have a TextTrack at this point if GetTrack() has already 404 // been called. This happens, for instance, if script tries to get the 405 // TextTrack before its mTrackElement has been bound to the DOM tree. 406 if (!mTrack) { 407 CreateTextTrack(); 408 } 409 // As `CreateTextTrack()` might fail, so we have to check it again. 410 if (mTrack) { 411 LOG("Add text track to media parent"); 412 mMediaParent->AddTextTrack(mTrack); 413 } 414 MaybeDispatchLoadResource(); 415 } 416 417 return NS_OK; 418 } 419 420 void HTMLTrackElement::UnbindFromTree(UnbindContext& aContext) { 421 if (mMediaParent && aContext.IsUnbindRoot(this)) { 422 // mTrack can be null if HTMLTrackElement::LoadResource has never been 423 // called. 424 if (mTrack) { 425 mMediaParent->RemoveTextTrack(mTrack); 426 mMediaParent->UpdateReadyState(); 427 } 428 mMediaParent = nullptr; 429 } 430 431 nsGenericHTMLElement::UnbindFromTree(aContext); 432 } 433 434 TextTrackReadyState HTMLTrackElement::ReadyState() const { 435 if (!mTrack) { 436 return TextTrackReadyState::NotLoaded; 437 } 438 439 return mTrack->ReadyState(); 440 } 441 442 void HTMLTrackElement::SetReadyState(TextTrackReadyState aReadyState) { 443 if (ReadyState() == aReadyState) { 444 return; 445 } 446 447 if (mTrack) { 448 switch (aReadyState) { 449 case TextTrackReadyState::Loaded: 450 LOG("dispatch 'load' event"); 451 DispatchTrackRunnable(u"load"_ns); 452 break; 453 case TextTrackReadyState::FailedToLoad: 454 LOG("dispatch 'error' event"); 455 DispatchTrackRunnable(u"error"_ns); 456 break; 457 default: 458 break; 459 } 460 mTrack->SetReadyState(aReadyState); 461 } 462 } 463 464 void HTMLTrackElement::DispatchTrackRunnable(const nsString& aEventName) { 465 Document* doc = OwnerDoc(); 466 if (!doc) { 467 return; 468 } 469 nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<const nsString>( 470 "dom::HTMLTrackElement::DispatchTrustedEvent", this, 471 &HTMLTrackElement::DispatchTrustedEvent, aEventName); 472 doc->Dispatch(runnable.forget()); 473 } 474 475 void HTMLTrackElement::DispatchTrustedEvent(const nsAString& aName) { 476 Document* doc = OwnerDoc(); 477 if (!doc) { 478 return; 479 } 480 nsContentUtils::DispatchTrustedEvent(doc, this, aName, CanBubble::eNo, 481 Cancelable::eNo); 482 } 483 484 void HTMLTrackElement::CancelChannelAndListener(bool aCheckRFP) { 485 if (aCheckRFP && ShouldResistFingerprinting(RFPTarget::WebVTT)) { 486 return; 487 } 488 489 if (mChannel) { 490 mChannel->CancelWithReason(NS_BINDING_ABORTED, 491 "HTMLTrackElement::CancelChannelAndListener"_ns); 492 mChannel->SetNotificationCallbacks(nullptr); 493 mChannel = nullptr; 494 } 495 496 if (mListener) { 497 mListener->Cancel(); 498 mListener = nullptr; 499 } 500 } 501 502 bool HTMLTrackElement::ShouldResistFingerprinting(RFPTarget aRfpTarget) { 503 Document* doc = OwnerDoc(); 504 if (!doc) { 505 return nsContentUtils::ShouldResistFingerprinting("Null document", 506 aRfpTarget); 507 } 508 return doc->ShouldResistFingerprinting(aRfpTarget); 509 } 510 511 void HTMLTrackElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, 512 const nsAttrValue* aValue, 513 const nsAttrValue* aOldValue, 514 nsIPrincipal* aMaybeScriptedPrincipal, 515 bool aNotify) { 516 if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) { 517 MaybeClearAllCues(); 518 // In spec, `start the track processing model` step10, while fetching is 519 // ongoing, if the track URL changes, then we have to set the `FailedToLoad` 520 // state. 521 // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:text-track-failed-to-load-3 522 if (ReadyState() == TextTrackReadyState::Loading && aValue != aOldValue) { 523 SetReadyState(TextTrackReadyState::FailedToLoad); 524 } 525 } 526 return nsGenericHTMLElement::AfterSetAttr( 527 aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); 528 } 529 530 void HTMLTrackElement::DispatchTestEvent(const nsAString& aName) { 531 if (!StaticPrefs::media_webvtt_testing_events()) { 532 return; 533 } 534 DispatchTrustedEvent(aName); 535 } 536 537 #undef LOG 538 539 } // namespace mozilla::dom