script/dom/html/embedded_content/htmltrackelement.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6
7use bytes::Bytes;
8use content_security_policy::Destination;
9use dom_struct::dom_struct;
10use html5ever::{LocalName, Prefix, local_name};
11use js::context::{JSContext, NoGC};
12use js::rust::HandleObject;
13use net_traits::request::RequestId;
14use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
15use script_bindings::cell::DomRefCell;
16use servo_url::ServoUrl;
17use servo_webvtt::{IncrementalWebVTTParser, WebVttCue, WebVttParserSink};
18
19use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::HTMLMediaElementMethods;
20use crate::dom::bindings::codegen::Bindings::HTMLTrackElementBinding::{
21 HTMLTrackElementConstants, HTMLTrackElementMethods,
22};
23use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
24use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackMethods, TextTrackMode};
25use crate::dom::bindings::inheritance::Castable;
26use crate::dom::bindings::refcounted::Trusted;
27use crate::dom::bindings::reflector::DomGlobal;
28use crate::dom::bindings::root::{Dom, DomRoot, UnrootedDom};
29use crate::dom::bindings::str::{DOMString, USVString};
30use crate::dom::csp::Violation;
31use crate::dom::document::Document;
32use crate::dom::element::Element;
33use crate::dom::element::storage::AttrRef;
34use crate::dom::eventtarget::EventTarget;
35use crate::dom::globalscope::GlobalScope;
36use crate::dom::html::htmlelement::HTMLElement;
37use crate::dom::html::htmlmediaelement::HTMLMediaElement;
38use crate::dom::node::node::NodeTraits;
39use crate::dom::node::{BindContext, MoveContext, Node, UnbindContext};
40use crate::dom::performanceresourcetiming::InitiatorType;
41use crate::dom::security::csp::GlobalCspReporting;
42use crate::dom::texttrack::TextTrack;
43use crate::dom::virtualmethods::VirtualMethods;
44use crate::dom::webvtt::vttcue::VTTCue;
45use crate::dom::{AttributeMutation, cors_setting_for_element};
46use crate::event_loop::script_thread::ScriptThread;
47use crate::fetch::fetch::{RequestWithGlobalScope, create_a_potential_cors_request};
48use crate::fetch::network_listener::{self, FetchResponseListener, ResourceTimingListener};
49use crate::realms::enter_auto_realm;
50use crate::runtime::job_queue::MicrotaskRunnable;
51
52#[derive(Clone, Copy, Default, JSTraceable, MallocSizeOf, PartialEq)]
53#[repr(u16)]
54/// <https://html.spec.whatwg.org/multipage/#text-track-readiness-state>
55pub(crate) enum TextTrackReadinessState {
56 /// <https://html.spec.whatwg.org/multipage/#text-track-not-loaded>
57 #[default]
58 None = HTMLTrackElementConstants::NONE,
59 /// <https://html.spec.whatwg.org/multipage/#text-track-loading>
60 Loading = HTMLTrackElementConstants::LOADING,
61 /// <https://html.spec.whatwg.org/multipage/#text-track-loaded>
62 Loaded = HTMLTrackElementConstants::LOADED,
63 /// <https://html.spec.whatwg.org/multipage/#text-track-failed-to-load>
64 FailedToLoad = HTMLTrackElementConstants::ERROR,
65}
66
67#[dom_struct]
68pub(crate) struct HTMLTrackElement {
69 htmlelement: HTMLElement,
70 /// <https://html.spec.whatwg.org/multipage/#text-track-readiness-state>
71 readiness_state: Cell<TextTrackReadinessState>,
72 /// <https://html.spec.whatwg.org/multipage/#text-track>
73 track: Dom<TextTrack>,
74 /// <https://html.spec.whatwg.org/multipage/#track-url>
75 #[no_trace]
76 track_url: DomRefCell<Option<ServoUrl>>,
77 /// The track_url used for the last load that was successful.
78 #[no_trace]
79 last_successful_load: DomRefCell<Option<ServoUrl>>,
80 /// Used as part of
81 /// <https://html.spec.whatwg.org/multipage/#start-the-track-processing-model>
82 /// whether the algorithm is running or not.
83 is_running_processing_model_algorithm: Cell<bool>,
84}
85
86impl HTMLTrackElement {
87 fn new_inherited(
88 local_name: LocalName,
89 prefix: Option<Prefix>,
90 document: &Document,
91 track: &TextTrack,
92 ) -> HTMLTrackElement {
93 HTMLTrackElement {
94 htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
95 readiness_state: Default::default(),
96 track: Dom::from_ref(track),
97 track_url: Default::default(),
98 last_successful_load: Default::default(),
99 is_running_processing_model_algorithm: Default::default(),
100 }
101 }
102
103 pub(crate) fn new(
104 cx: &mut JSContext,
105 local_name: LocalName,
106 prefix: Option<Prefix>,
107 document: &Document,
108 proto: Option<HandleObject>,
109 ) -> DomRoot<HTMLTrackElement> {
110 // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks
111 // > When a track element is created, it must be associated
112 // > with a new text track (with its value set as defined below).
113 let track = TextTrack::new(
114 cx,
115 document.window(),
116 Default::default(),
117 Default::default(),
118 Default::default(),
119 Default::default(),
120 Default::default(),
121 None,
122 );
123 let track_element = Node::reflect_node_with_proto(
124 cx,
125 Box::new(HTMLTrackElement::new_inherited(
126 local_name, prefix, document, &track,
127 )),
128 document,
129 proto,
130 );
131 track.set_associated_track(&track_element);
132 track_element
133 }
134
135 /// <https://html.spec.whatwg.org/multipage/#start-the-track-processing-model>
136 pub(crate) fn start_the_track_processing_model(&self, cx: &mut JSContext) {
137 // Step 1. If another occurrence of this algorithm is already running
138 // for this text track and its track element, return,
139 // letting that other algorithm take care of this element.
140 if self.is_running_processing_model_algorithm.get() {
141 return;
142 }
143 // Step 2. If the text track's text track mode is not set to one of hidden or showing, then return.
144 if !matches!(
145 self.track.Mode(),
146 TextTrackMode::Hidden | TextTrackMode::Showing
147 ) {
148 return;
149 }
150 // Step 3. If the text track's track element does not have a media element as a parent, return.
151 if self
152 .upcast::<Node>()
153 .GetParentElement()
154 .is_none_or(|parent| !parent.is::<HTMLMediaElement>())
155 {
156 return;
157 };
158 // Step 4. Run the remainder of these steps in parallel, allowing whatever caused these steps to run to continue.
159 // Step 5. Top: Await a stable state.
160 let task = TrackElementMicrotask::ProcessingModel {
161 elem: Dom::from_ref(self),
162 };
163 self.is_running_processing_model_algorithm.set(true);
164
165 ScriptThread::await_stable_state(cx, Box::new(task));
166 }
167
168 fn check_if_track_parent_element_changed(&self, cx: &mut JSContext) {
169 if let Some(parent) = self
170 .upcast::<Node>()
171 .GetParentNode()
172 .and_then(DomRoot::downcast::<HTMLMediaElement>)
173 {
174 // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks
175 // > When a track element's parent element changes and the new parent is a media element,
176 // > then the user agent must add the track element's corresponding text track to
177 // > the media element's list of text tracks, and then queue a media element task
178 // > given the media element to fire an event named addtrack at the media element's
179 // > textTracks attribute's TextTrackList object, using TrackEvent,
180 // > with the track attribute initialized to the text track's TextTrack object.
181 parent.TextTracks(cx).add(cx, &self.track);
182
183 // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks:start-the-track-processing-model
184 // > The track element's parent element changes and the new parent is a media element.
185 self.start_the_track_processing_model(cx);
186 }
187 }
188
189 pub(crate) fn track<'a>(&self, no_gc: &'a NoGC) -> UnrootedDom<'a, TextTrack> {
190 self.track.as_unrooted(no_gc)
191 }
192}
193
194impl HTMLTrackElementMethods<crate::DomTypeHolder> for HTMLTrackElement {
195 /// <https://html.spec.whatwg.org/multipage/#dom-track-kind>
196 fn Kind(&self) -> DOMString {
197 let element = self.upcast::<Element>();
198 // Get the value of "kind" and transform all uppercase
199 // chars into lowercase.
200 let kind = element
201 .get_string_attribute(&local_name!("kind"))
202 .to_lowercase();
203 match &*kind {
204 "subtitles" | "captions" | "descriptions" | "chapters" | "metadata" => {
205 // The value of "kind" is valid. Return the lowercase version
206 // of it.
207 DOMString::from(kind)
208 },
209 _ if kind.is_empty() => {
210 // The default value should be "subtitles". If "kind" has not
211 // been set, the real value for "kind" is "subtitles"
212 DOMString::from_static("subtitles")
213 },
214 _ => {
215 // If "kind" has been set but it is not one of the valid
216 // values, return the default invalid value of "metadata"
217 DOMString::from_static("metadata")
218 },
219 }
220 }
221
222 // https://html.spec.whatwg.org/multipage/#dom-track-kind
223 // Do no transformations on the value of "kind" when setting it.
224 // All transformations should be done in the get method.
225 make_setter!(SetKind, "kind");
226
227 // https://html.spec.whatwg.org/multipage/#dom-track-src
228 make_url_getter!(Src, "src");
229 // https://html.spec.whatwg.org/multipage/#dom-track-src
230 make_url_setter!(SetSrc, "src");
231
232 // https://html.spec.whatwg.org/multipage/#dom-track-srclang
233 make_getter!(Srclang, "srclang");
234 // https://html.spec.whatwg.org/multipage/#dom-track-srclang
235 make_setter!(SetSrclang, "srclang");
236
237 // https://html.spec.whatwg.org/multipage/#dom-track-label
238 make_getter!(Label, "label");
239 // https://html.spec.whatwg.org/multipage/#dom-track-label
240 make_setter!(SetLabel, "label");
241
242 // https://html.spec.whatwg.org/multipage/#dom-track-default
243 make_bool_getter!(Default, "default");
244 // https://html.spec.whatwg.org/multipage/#dom-track-default
245 make_bool_setter!(SetDefault, "default");
246
247 /// <https://html.spec.whatwg.org/multipage/#dom-track-readystate>
248 fn ReadyState(&self) -> u16 {
249 self.readiness_state.get() as u16
250 }
251
252 /// <https://html.spec.whatwg.org/multipage/#dom-track-track>
253 fn Track(&self) -> DomRoot<TextTrack> {
254 DomRoot::from_ref(&*self.track)
255 }
256}
257
258impl VirtualMethods for HTMLTrackElement {
259 fn super_type(&self) -> Option<&dyn VirtualMethods> {
260 Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
261 }
262
263 fn attribute_mutated(
264 &self,
265 cx: &mut JSContext,
266 attr: AttrRef<'_>,
267 mutation: AttributeMutation,
268 ) {
269 self.super_type()
270 .unwrap()
271 .attribute_mutated(cx, attr, mutation);
272 match *attr.local_name() {
273 local_name!("src") => {
274 // https://html.spec.whatwg.org/multipage/#attr-track-src
275 // > When the element's src attribute is set, run these steps:
276 if matches!(mutation, AttributeMutation::Set(..)) {
277 // Step 2. Let value be the element's src attribute value.
278 let value = &**attr.value();
279 // Step 1. Let trackURL be failure.
280 // Step 3. If value is not the empty string,
281 // then set trackURL to the result of encoding-parsing-and-serializing
282 // a URL given value, relative to the element's node document.
283 // Step 4. Set the element's track URL to trackURL if it is not failure;
284 // otherwise to the empty string.
285 *self.track_url.borrow_mut() = if !value.is_empty() {
286 self.owner_document().encoding_parse_a_url(value).ok()
287 } else {
288 None
289 };
290 }
291 // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks
292 // > Whenever a track element has its src attribute set, changed, or removed,
293 // > the user agent must immediately empty the element's text track's text track list of cues.
294 // > (This also causes the algorithm above to stop adding cues from the resource
295 // > being obtained using the previously given URL, if any.)
296 self.track.empty_cue_list();
297 },
298 local_name!("kind") |
299 local_name!("label") |
300 local_name!("srclang") |
301 local_name!("id") => {
302 // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks
303 // > As the kind, label, srclang, and id attributes are set, changed,
304 // > or removed, the text track must update accordingly, as per the definitions above.
305 self.track.update_attributes_from_track_element(self);
306 },
307 _ => {},
308 }
309 }
310
311 fn moving_steps(&self, cx: &mut JSContext, context: &MoveContext) {
312 if let Some(super_type) = self.super_type() {
313 super_type.moving_steps(cx, context);
314 }
315
316 if let Some(parent) = context
317 .old_parent
318 .and_then(|node| node.downcast::<HTMLMediaElement>())
319 {
320 // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks
321 // > When a track element's parent element changes and the old parent was a media element,
322 // > then the user agent must remove the track element's corresponding text track from
323 // > the media element's list of text tracks, and then queue a media element task
324 // > given the media element to fire an event named removetrack at the media element's
325 // > textTracks attribute's TextTrackList object, using TrackEvent,
326 // > with the track attribute initialized to the text track's TextTrack object.
327 parent.TextTracks(cx).remove(&self.track);
328 }
329
330 self.check_if_track_parent_element_changed(cx);
331 }
332
333 fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
334 if let Some(super_type) = self.super_type() {
335 super_type.bind_to_tree(cx, context);
336 }
337
338 self.check_if_track_parent_element_changed(cx);
339 }
340
341 fn unbind_from_tree(&self, cx: &mut JSContext, context: &UnbindContext) {
342 if let Some(s) = self.super_type() {
343 s.unbind_from_tree(cx, context);
344 }
345
346 if let Some(parent) = context.parent.downcast::<HTMLMediaElement>() {
347 // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks
348 // > When a track element's parent element changes and the old parent was a media element,
349 // > then the user agent must remove the track element's corresponding text track from
350 // > the media element's list of text tracks, and then queue a media element task
351 // > given the media element to fire an event named removetrack at the media element's
352 // > textTracks attribute's TextTrackList object, using TrackEvent,
353 // > with the track attribute initialized to the text track's TextTrack object.
354 parent.TextTracks(cx).remove(&self.track);
355 }
356 }
357}
358
359#[derive(JSTraceable, MallocSizeOf)]
360pub(crate) enum TrackElementMicrotask {
361 ProcessingModel { elem: Dom<HTMLTrackElement> },
362}
363
364impl MicrotaskRunnable for TrackElementMicrotask {
365 fn handler(&self, cx: &mut JSContext) {
366 let _realm = match self {
367 TrackElementMicrotask::ProcessingModel { elem, .. } => enter_auto_realm(cx, &**elem),
368 };
369 match self {
370 // https://html.spec.whatwg.org/multipage/#start-the-track-processing-model
371 TrackElementMicrotask::ProcessingModel { elem } => {
372 // Not specced, but required for browser compatibility:
373 // https://github.com/whatwg/html/issues/12796
374 if elem.readiness_state.get() == TextTrackReadinessState::Loaded &&
375 *elem.track_url.borrow() == *elem.last_successful_load.borrow()
376 {
377 elem.is_running_processing_model_algorithm.set(false);
378 return;
379 }
380
381 let media_parent = elem
382 .upcast::<Node>()
383 .GetParentNode()
384 .and_then(DomRoot::downcast::<HTMLMediaElement>);
385
386 // The synchronous section consists of the following steps.
387 // (The steps in the synchronous section are marked with ⌛.)
388 // Step 6. ⌛ Set the text track readiness state to loading.
389 elem.readiness_state.set(TextTrackReadinessState::Loading);
390 // Step 7. ⌛ Let URL be the track URL of the track element.
391 let url = elem.track_url.borrow().clone();
392 // Step 8. ⌛ If the track element's parent is a media element,
393 // then let corsAttributeState be the state of the parent media element's
394 // crossorigin content attribute. Otherwise, let corsAttributeState be No CORS.
395 let cors_attribute_state =
396 media_parent.and_then(|parent| cors_setting_for_element(parent.upcast()));
397 // Step 9. End the synchronous section, continuing the remaining steps in parallel.
398 // TODO
399 // Step 10. If URL is not the empty string:
400 if let Some(url) = url {
401 // Step 10.1. Let request be the result of creating a potential-CORS request given URL,
402 // "track", and corsAttributeState, and with the same-origin fallback flag set.
403 let global = elem.global();
404 let document = elem.owner_document();
405 let request = create_a_potential_cors_request(
406 Some(document.webview_id()),
407 url.clone(),
408 Destination::Track,
409 cors_attribute_state,
410 Some(true),
411 global.get_referrer(),
412 )
413 // Step 10.2. Set request's client to the track element's node document's relevant
414 // settings object.
415 .with_global_scope(&global);
416 // Step 10.3. Set request's initiator type to "track".
417 //
418 // Set in listener
419
420 // Step 10.4. Fetch request.
421 let listener = HTMLTrackElementFetchListener {
422 element: Trusted::new(elem),
423 url,
424 payload: vec![],
425 };
426 document.fetch_background(request, listener);
427 } else {
428 elem.is_running_processing_model_algorithm.set(false);
429 }
430 // Step 11. Wait until the text track readiness state is no longer set to loading.
431 // TODO
432 // Step 12. Wait until the track URL is no longer equal to URL,
433 // at the same time as the text track mode is set to hidden or showing.
434 // TODO
435 // Step 13. Jump to the step labeled top.
436 // TODO
437 },
438 }
439 }
440}
441
442struct TextTrackCueSink {
443 track_element: Trusted<HTMLTrackElement>,
444}
445
446impl WebVttParserSink<JSContext> for TextTrackCueSink {
447 fn consume_cue(&self, cx: &mut JSContext, cue: WebVttCue) {
448 let element = self.track_element.root();
449 let global = element.global();
450 let text_track = &element.track;
451
452 let cue = VTTCue::create_from_vtt(cx, cue, global.as_window(), Some(text_track));
453 text_track.get_text_track_cue_list(cx).add(cx, cue.upcast());
454 }
455}
456
457struct HTMLTrackElementFetchListener {
458 /// The element that initiated the request.
459 element: Trusted<HTMLTrackElement>,
460 /// URL for the resource.
461 url: ServoUrl,
462 /// The payload received
463 payload: Vec<u8>,
464}
465
466impl FetchResponseListener for HTMLTrackElementFetchListener {
467 fn process_request_body(&mut self, _: RequestId) {}
468
469 fn process_response(
470 &mut self,
471 _: &mut JSContext,
472 _: RequestId,
473 _: Result<FetchMetadata, NetworkError>,
474 ) {
475 }
476
477 fn process_response_chunk(&mut self, _: &mut JSContext, _: RequestId, payload: Bytes) {
478 self.payload.extend_from_slice(&payload);
479 }
480
481 /// Step 10.4 of <https://html.spec.whatwg.org/multipage/#start-the-track-processing-model>
482 fn process_response_eof(
483 self,
484 cx: &mut JSContext,
485 _: RequestId,
486 status: Result<(), NetworkError>,
487 timing: ResourceFetchTiming,
488 ) {
489 let track = self.element.clone();
490 let element = self.element.root();
491 if status.is_err() {
492 // > If fetching fails for any reason (network error, the server returns an error code, CORS fails, etc.),
493 // > or if URL is the empty string, then queue an element task on the DOM manipulation task source
494 // > given the media element to first change the text track readiness state to failed to load
495 // > and then fire an event named error at the track element.
496 element
497 .global()
498 .task_manager()
499 .dom_manipulation_task_source()
500 .queue(task!(failed_to_load: move |cx| {
501 let track = track.root();
502 track.readiness_state.set(TextTrackReadinessState::FailedToLoad);
503 track.upcast::<EventTarget>().fire_event(cx, atom!("error"));
504 }));
505 } else {
506 // > The tasks queued by the fetching algorithm on the networking task source to
507 // > process the data as it is being fetched must determine the type of the resource.
508 // > If the type of the resource is not a supported text track format, the load will fail,
509 // > as described below. Otherwise, the resource's data must be passed to the appropriate parser
510 // > (e.g., the WebVTT parser) as it is received, with the text track list of cues
511 // > being used for that parser's output. [WEBVTT]
512 let result = str::from_utf8(&self.payload)
513 .map_err(|str_error| debug!("WebVTT file contains non-utf8 data: {str_error}"))
514 .and_then(|payload| {
515 let sink = TextTrackCueSink {
516 track_element: track.clone(),
517 };
518 IncrementalWebVTTParser::new(sink)
519 .parse_sync(cx, payload)
520 .map_err(|parser_error| {
521 debug!("Failed to parse WEBVTT file: {parser_error}")
522 })
523 });
524 if result.is_ok() {
525 // > If fetching does not fail, and the file was successfully processed,
526 // > then the final task that is queued by the networking task source,
527 // > after it has finished parsing the data, must change the text track readiness state to loaded,
528 // > and fire an event named load at the track element.
529 let url = self.url.clone();
530 element
531 .global()
532 .task_manager()
533 .networking_task_source()
534 .queue(task!(successfully_loaded: move |cx| {
535 let track = track.root();
536 *track.last_successful_load.borrow_mut() = Some(url);
537 track.readiness_state.set(TextTrackReadinessState::Loaded);
538 track.upcast::<EventTarget>().fire_event(cx, atom!("load"));
539 }));
540 } else {
541 // > If fetching does not fail, but the type of the resource is not a supported text track format,
542 // > or the file was not successfully processed (e.g., the format in question is an XML format
543 // > and the file contained a well-formedness error that XML requires be detected
544 // > and reported to the application), then the task that is queued on the networking task source
545 // > in which the aforementioned problem is found must change the text track readiness state
546 // > to failed to load and fire an event named error at the track element.
547 element
548 .global()
549 .task_manager()
550 .networking_task_source()
551 .queue(task!(failed_to_parse: move |cx| {
552 let track = track.root();
553 track.readiness_state.set(TextTrackReadinessState::FailedToLoad);
554 track.upcast::<EventTarget>().fire_event(cx, atom!("error"));
555 }));
556 }
557 }
558 element.is_running_processing_model_algorithm.set(false);
559 network_listener::submit_timing(cx, &self, &status, &timing);
560 }
561
562 fn process_csp_violations(
563 &mut self,
564 cx: &mut JSContext,
565 _: RequestId,
566 violations: Vec<Violation>,
567 ) {
568 let global = &self.resource_timing_global();
569 global.report_csp_violations(cx, violations, None, None);
570 }
571
572 fn should_invoke(&self) -> bool {
573 true
574 }
575}
576
577impl ResourceTimingListener for HTMLTrackElementFetchListener {
578 /// Step 10.3. of <https://html.spec.whatwg.org/multipage/#start-the-track-processing-model>
579 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
580 (InitiatorType::Track, self.url.clone())
581 }
582
583 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
584 self.element.root().owner_document().global()
585 }
586}