Skip to main content

script/dom/webvtt/
texttrack.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, Ref};
6
7use dom_struct::dom_struct;
8use js::context::{JSContext, NoGC};
9use script_bindings::cell::DomRefCell;
10use script_bindings::inheritance::Castable;
11use script_bindings::reflector::reflect_dom_object;
12
13use crate::dom::bindings::codegen::Bindings::HTMLTrackElementBinding::HTMLTrackElementMethods;
14use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{
15    TextTrackKind, TextTrackMethods, TextTrackMode,
16};
17use crate::dom::bindings::error::{Error, ErrorResult};
18use crate::dom::bindings::reflector::DomGlobal;
19use crate::dom::bindings::root::{DomRoot, MutNullableDom, UnrootedDom};
20use crate::dom::bindings::str::DOMString;
21use crate::dom::element::Element;
22use crate::dom::eventtarget::EventTarget;
23use crate::dom::html::htmltrackelement::HTMLTrackElement;
24use crate::dom::texttrackcue::TextTrackCue;
25use crate::dom::texttrackcuelist::TextTrackCueList;
26use crate::dom::texttracklist::TextTrackList;
27use crate::dom::webvtt::rules_for_rendering::RulesForUpdatingTheTextTrackRendering;
28use crate::dom::window::Window;
29
30#[dom_struct]
31pub(crate) struct TextTrack {
32    eventtarget: EventTarget,
33    /// <https://html.spec.whatwg.org/multipage/#text-track-kind>
34    kind: Cell<TextTrackKind>,
35    /// <https://html.spec.whatwg.org/multipage/#text-track-label>
36    label: DomRefCell<DOMString>,
37    /// <https://html.spec.whatwg.org/multipage/#text-track-language>
38    language: DomRefCell<DOMString>,
39    /// <https://html.spec.whatwg.org/multipage/#text-track-identifier>
40    id: DomRefCell<DOMString>,
41    /// <https://html.spec.whatwg.org/multipage/#text-track-mode>
42    mode: Cell<TextTrackMode>,
43    /// <https://html.spec.whatwg.org/multipage/#text-track-list-of-cues>
44    cue_list: MutNullableDom<TextTrackCueList>,
45    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-activecues>
46    active_cue_list: MutNullableDom<TextTrackCueList>,
47    track_list: MutNullableDom<TextTrackList>,
48    associated_track: MutNullableDom<HTMLTrackElement>,
49    /// <https://html.spec.whatwg.org/multipage/#rules-for-updating-the-text-track-rendering>
50    rules_for_updating_the_text_track_rendering:
51        Cell<Option<RulesForUpdatingTheTextTrackRendering>>,
52}
53
54impl TextTrack {
55    pub(crate) fn new_inherited(
56        id: DOMString,
57        kind: TextTrackKind,
58        label: DOMString,
59        language: DOMString,
60        mode: TextTrackMode,
61        track_list: Option<&TextTrackList>,
62    ) -> TextTrack {
63        TextTrack {
64            eventtarget: EventTarget::new_inherited(),
65            kind: Cell::new(kind),
66            label: DomRefCell::new(label),
67            language: DomRefCell::new(language),
68            id: DomRefCell::new(id),
69            mode: Cell::new(mode),
70            cue_list: Default::default(),
71            active_cue_list: Default::default(),
72            track_list: MutNullableDom::new(track_list),
73            associated_track: Default::default(),
74            rules_for_updating_the_text_track_rendering: Default::default(),
75        }
76    }
77
78    #[expect(clippy::too_many_arguments)]
79    pub(crate) fn new(
80        cx: &mut JSContext,
81        window: &Window,
82        id: DOMString,
83        kind: TextTrackKind,
84        label: DOMString,
85        language: DOMString,
86        mode: TextTrackMode,
87        track_list: Option<&TextTrackList>,
88    ) -> DomRoot<TextTrack> {
89        reflect_dom_object(
90            cx,
91            Box::new(TextTrack::new_inherited(
92                id, kind, label, language, mode, track_list,
93            )),
94            window,
95        )
96    }
97
98    pub(crate) fn cues<'no_gc>(
99        &self,
100        no_gc: &'no_gc NoGC,
101    ) -> Vec<UnrootedDom<'no_gc, TextTrackCue>> {
102        self.cue_list
103            .get_unrooted(no_gc)
104            .map(|list| list.cues(no_gc))
105            .unwrap_or_default()
106    }
107
108    pub(crate) fn text_track_cue_list(&self, cx: &mut JSContext) -> DomRoot<TextTrackCueList> {
109        self.cue_list
110            .or_init(|| TextTrackCueList::new(cx, self, self.global().as_window()))
111    }
112
113    fn active_text_track_cue_list(&self, cx: &mut JSContext) -> DomRoot<TextTrackCueList> {
114        let active_cue_list = self
115            .active_cue_list
116            .or_init(|| TextTrackCueList::new(cx, self, self.global().as_window()));
117        if let Some(cue_list) = self.cue_list.get_unrooted(cx.no_gc()) {
118            active_cue_list.refresh_active_cues(cx.no_gc(), cue_list);
119        }
120        active_cue_list
121    }
122
123    pub(crate) fn id(&self) -> Ref<'_, DOMString> {
124        self.id.borrow()
125    }
126
127    pub(crate) fn track_list(&self) -> Option<DomRoot<TextTrackList>> {
128        self.track_list.get()
129    }
130
131    pub(crate) fn add_track_list(&self, track_list: &TextTrackList) {
132        self.track_list.set(Some(track_list));
133    }
134
135    pub(crate) fn remove_track_list(&self) {
136        self.track_list.set(None);
137    }
138
139    pub(crate) fn associated_track(&self) -> Option<DomRoot<HTMLTrackElement>> {
140        self.associated_track.get()
141    }
142
143    /// <https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks>
144    pub(crate) fn set_associated_track(&self, track_element: &HTMLTrackElement) {
145        self.associated_track.set(Some(track_element));
146        // > When a track element is created, it must be associated with
147        // > a new text track (with its value set as defined below).
148        self.update_attributes_from_track_element(track_element);
149    }
150
151    /// <https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks>
152    pub(crate) fn update_attributes_from_track_element(&self, track_element: &HTMLTrackElement) {
153        // > The text track kind is determined from the state of the
154        // > element's kind attribute according to the following table;
155        // > for a state given in a cell of the first column,
156        // > the kind is the string given in the second column:
157        self.kind.set(match track_element.Kind().str().as_ref() {
158            "subtitles" => TextTrackKind::Subtitles,
159            "captions" => TextTrackKind::Captions,
160            "descriptions" => TextTrackKind::Descriptions,
161            "chapters" => TextTrackKind::Chapters,
162            "metadata" => TextTrackKind::Metadata,
163            _ => unreachable!("Must always have these specific kind states"),
164        });
165        // > The text track label is the element's track label.
166        *self.label.borrow_mut() = track_element.Label();
167        // > The text track language is the element's track language,
168        // > if any; otherwise the empty string.
169        *self.language.borrow_mut() = track_element.Srclang();
170        // > The text track identifier is the element's id attribute value,
171        // > if any; otherwise the empty string.
172        *self.id.borrow_mut() = track_element
173            .upcast::<Element>()
174            .get_id()
175            .map(|value| DOMString::from(&*value))
176            .unwrap_or_default();
177    }
178
179    pub(crate) fn empty_cue_list(&self) {
180        if let Some(cue_list) = self.cue_list.get() {
181            cue_list.empty();
182        }
183    }
184
185    pub(crate) fn sort_cue_list(&self) {
186        if let Some(cue_list) = self.cue_list.get() {
187            cue_list.sort();
188        }
189    }
190
191    pub(crate) fn set_text_track_mode(&self, cx: &mut JSContext, value: TextTrackMode) {
192        if self.mode.get() == value {
193            return;
194        }
195        self.mode.set(value);
196        // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks:start-the-track-processing-model
197        // > The text track has its text track mode changed.
198        if let Some(track_element) = self.associated_track.get() {
199            track_element.start_the_track_processing_model(cx);
200        }
201    }
202
203    /// <https://html.spec.whatwg.org/multipage/#rules-for-updating-the-text-track-rendering>
204    pub(crate) fn rules_for_updating_the_text_track_rendering(
205        &self,
206    ) -> Option<RulesForUpdatingTheTextTrackRendering> {
207        self.rules_for_updating_the_text_track_rendering.get()
208    }
209
210    /// <https://html.spec.whatwg.org/multipage/#rules-for-updating-the-text-track-rendering>
211    pub(crate) fn set_rules_for_updating_the_text_track_rendering(
212        &self,
213        rules: RulesForUpdatingTheTextTrackRendering,
214    ) {
215        debug_assert!(
216            self.rules_for_updating_the_text_track_rendering
217                .get()
218                .is_none()
219        );
220        self.rules_for_updating_the_text_track_rendering
221            .set(Some(rules));
222    }
223}
224
225impl TextTrackMethods<crate::DomTypeHolder> for TextTrack {
226    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-kind>
227    fn Kind(&self) -> TextTrackKind {
228        self.kind.get()
229    }
230
231    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-label>
232    fn Label(&self) -> DOMString {
233        self.label.borrow().clone()
234    }
235
236    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-language>
237    fn Language(&self) -> DOMString {
238        self.language.borrow().clone()
239    }
240
241    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-id>
242    fn Id(&self) -> DOMString {
243        self.id.borrow().clone()
244    }
245
246    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-mode>
247    fn Mode(&self) -> TextTrackMode {
248        self.mode.get()
249    }
250
251    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-mode>
252    fn SetMode(&self, cx: &mut JSContext, value: TextTrackMode) {
253        self.set_text_track_mode(cx, value)
254    }
255
256    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-cues>
257    fn GetCues(&self, cx: &mut JSContext) -> Option<DomRoot<TextTrackCueList>> {
258        match self.Mode() {
259            TextTrackMode::Disabled => None,
260            _ => Some(self.text_track_cue_list(cx)),
261        }
262    }
263
264    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-activecues>
265    fn GetActiveCues(&self, cx: &mut JSContext) -> Option<DomRoot<TextTrackCueList>> {
266        // Step 1. If this's mode is the text track disabled mode, then return null.
267        if self.mode.get() == TextTrackMode::Disabled {
268            return None;
269        }
270        // Step 2. Return a live TextTrackCueList object that represents
271        // the subset of this's text track list of cues whose active flag
272        // was set when the script started, in text track cue order.
273        Some(self.active_text_track_cue_list(cx))
274    }
275
276    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-addcue>
277    fn AddCue(&self, cx: &mut JSContext, cue: &TextTrackCue) -> ErrorResult {
278        // Step 1. Let list be this's text track list of cues.
279        //
280        // We store the rules of rendering in `TextTrack` rather than in `TextTrackCueList`
281
282        if let Some(rules_for_updating_the_text_track_rendering) =
283            self.rules_for_updating_the_text_track_rendering()
284        {
285            // Step 3. If list's associated rules for updating the text track rendering are not
286            // the same rules for updating the text track rendering as appropriate for cue,
287            // then throw an "InvalidStateError" DOMException.
288            if rules_for_updating_the_text_track_rendering !=
289                cue.rules_for_updating_the_text_track_rendering()
290            {
291                return Err(Error::InvalidState(Some(
292                    "Text cue rules of rendering do not match text track rules of rendering".into(),
293                )));
294            }
295        } else {
296            // Step 2. If list does not yet have any associated rules for updating the text track rendering,
297            // then associate list with the rules for updating the text track rendering appropriate to cue.
298            self.set_rules_for_updating_the_text_track_rendering(
299                cue.rules_for_updating_the_text_track_rendering(),
300            );
301        }
302
303        // Step 4. If the given cue is in a text track list of cues,
304        // then remove cue from that text track list of cues.
305        if let Some(old_track) = cue.get_text_track() {
306            // gecko calls RemoveCue when the given cue
307            // has an associated track, but doesn't return
308            // the error from it, so we wont either.
309            if old_track.RemoveCue(cx, cue).is_err() {
310                warn!("Failed to remove cues for the added cue's text track");
311            }
312        }
313        // Step 5. Add cue to list.
314        cue.set_text_track(Some(self));
315        self.text_track_cue_list(cx).add(cx, cue);
316        Ok(())
317    }
318
319    /// <https://html.spec.whatwg.org/multipage/#dom-texttrack-removecue>
320    fn RemoveCue(&self, cx: &mut JSContext, cue: &TextTrackCue) -> ErrorResult {
321        // Step 1
322        let cues = self.text_track_cue_list(cx);
323        let index = match cues.find(cue) {
324            Some(i) => Ok(i),
325            None => Err(Error::NotFound(None)),
326        }?;
327        // Step 2
328        cue.set_text_track(None);
329        cues.remove(index);
330        Ok(())
331    }
332
333    // https://html.spec.whatwg.org/multipage/#handler-texttrack-oncuechange
334    event_handler!(cuechange, GetOncuechange, SetOncuechange);
335}