Skip to main content

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}