Skip to main content

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