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