script/dom/html/
htmlvideoelement.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;
6use std::sync::Arc;
7
8use dom_struct::dom_struct;
9use euclid::default::Size2D;
10use html5ever::{LocalName, Prefix, local_name, ns};
11use js::rust::HandleObject;
12use layout_api::{HTMLMediaData, MediaMetadata};
13use net_traits::image_cache::{
14    ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable, ImageResponse,
15    PendingImageId,
16};
17use net_traits::request::{CredentialsMode, Destination, RequestBuilder, RequestId};
18use net_traits::{
19    CoreResourceThread, FetchMetadata, FetchResponseMsg, NetworkError, ResourceFetchTiming,
20};
21use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
22use servo_media::player::video::VideoFrame;
23use servo_url::ServoUrl;
24use style::attr::{AttrValue, LengthOrPercentageOrAuto};
25
26use crate::document_loader::{LoadBlocker, LoadType};
27use crate::dom::attr::Attr;
28use crate::dom::bindings::cell::DomRefCell;
29use crate::dom::bindings::codegen::Bindings::HTMLVideoElementBinding::HTMLVideoElementMethods;
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::refcounted::Trusted;
32use crate::dom::bindings::reflector::DomGlobal;
33use crate::dom::bindings::root::{DomRoot, LayoutDom};
34use crate::dom::bindings::str::DOMString;
35use crate::dom::csp::{GlobalCspReporting, Violation};
36use crate::dom::document::Document;
37use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers};
38use crate::dom::globalscope::GlobalScope;
39use crate::dom::html::htmlmediaelement::{HTMLMediaElement, NetworkState, ReadyState};
40use crate::dom::node::{Node, NodeDamage, NodeTraits};
41use crate::dom::performance::performanceresourcetiming::InitiatorType;
42use crate::dom::virtualmethods::VirtualMethods;
43use crate::fetch::{FetchCanceller, RequestWithGlobalScope};
44use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
45use crate::script_runtime::CanGc;
46
47#[dom_struct]
48pub(crate) struct HTMLVideoElement {
49    htmlmediaelement: HTMLMediaElement,
50    /// <https://html.spec.whatwg.org/multipage/#dom-video-videowidth>
51    video_width: Cell<Option<u32>>,
52    /// <https://html.spec.whatwg.org/multipage/#dom-video-videoheight>
53    video_height: Cell<Option<u32>>,
54    /// Incremented whenever tasks associated with this element are cancelled.
55    generation_id: Cell<u32>,
56    /// Load event blocker. Will block the load event while the poster frame
57    /// is being fetched.
58    load_blocker: DomRefCell<Option<LoadBlocker>>,
59    /// A copy of the last frame
60    #[ignore_malloc_size_of = "VideoFrame"]
61    #[no_trace]
62    last_frame: DomRefCell<Option<VideoFrame>>,
63}
64
65impl HTMLVideoElement {
66    fn new_inherited(
67        local_name: LocalName,
68        prefix: Option<Prefix>,
69        document: &Document,
70    ) -> HTMLVideoElement {
71        HTMLVideoElement {
72            htmlmediaelement: HTMLMediaElement::new_inherited(local_name, prefix, document),
73            video_width: Cell::new(None),
74            video_height: Cell::new(None),
75            generation_id: Cell::new(0),
76            load_blocker: Default::default(),
77            last_frame: Default::default(),
78        }
79    }
80
81    pub(crate) fn new(
82        local_name: LocalName,
83        prefix: Option<Prefix>,
84        document: &Document,
85        proto: Option<HandleObject>,
86        can_gc: CanGc,
87    ) -> DomRoot<HTMLVideoElement> {
88        Node::reflect_node_with_proto(
89            Box::new(HTMLVideoElement::new_inherited(
90                local_name, prefix, document,
91            )),
92            document,
93            proto,
94            can_gc,
95        )
96    }
97
98    pub(crate) fn get_video_width(&self) -> Option<u32> {
99        self.video_width.get()
100    }
101
102    pub(crate) fn get_video_height(&self) -> Option<u32> {
103        self.video_height.get()
104    }
105
106    pub(crate) fn set_natural_dimensions(&self, width: Option<u32>, height: Option<u32>) -> bool {
107        if self.video_width.get() == width && self.video_height.get() == height {
108            return false;
109        }
110
111        self.video_width.set(width);
112        self.video_height.set(height);
113
114        self.upcast::<Node>().dirty(NodeDamage::Other);
115        true
116    }
117
118    /// Gets the copy of the video frame at the current playback position,
119    /// if that is available, or else (e.g. when the video is seeking or buffering)
120    /// its previous appearance, if any.
121    pub(crate) fn get_current_frame_data(&self) -> Option<Snapshot> {
122        let frame = self.htmlmediaelement.get_current_frame();
123        if frame.is_some() {
124            *self.last_frame.borrow_mut() = frame;
125        }
126
127        match self.last_frame.borrow().as_ref() {
128            Some(frame) => {
129                let size = Size2D::new(frame.get_width() as u32, frame.get_height() as u32);
130                if !frame.is_gl_texture() {
131                    let alpha_mode = SnapshotAlphaMode::Transparent {
132                        premultiplied: false,
133                    };
134
135                    Some(Snapshot::from_vec(
136                        size.cast(),
137                        SnapshotPixelFormat::BGRA,
138                        alpha_mode,
139                        frame.get_data().to_vec(),
140                    ))
141                } else {
142                    // XXX(victor): here we only have the GL texture ID.
143                    Some(Snapshot::cleared(size.cast()))
144                }
145            },
146            None => None,
147        }
148    }
149
150    /// <https://html.spec.whatwg.org/multipage/#poster-frame>
151    fn update_poster_frame(&self, poster_url: Option<&str>, can_gc: CanGc) {
152        // Step 1. If there is an existing instance of this algorithm running
153        // for this video element, abort that instance of this algorithm without
154        // changing the poster frame.
155        self.generation_id.set(self.generation_id.get() + 1);
156
157        // Step 2. If the poster attribute's value is the empty string or
158        // if the attribute is absent, then there is no poster frame; return.
159        let Some(poster_url) = poster_url.filter(|poster_url| !poster_url.is_empty()) else {
160            self.htmlmediaelement.set_poster_frame(None);
161            return;
162        };
163
164        // Step 3. Let url be the result of encoding-parsing a URL given
165        // the poster attribute's value, relative to the element's node
166        // document.
167        // Step 4. If url is failure, then return. There is no poster frame.
168        let poster_url = match self.owner_document().encoding_parse_a_url(poster_url) {
169            Ok(url) => url,
170            Err(_) => {
171                self.htmlmediaelement.set_poster_frame(None);
172                return;
173            },
174        };
175
176        // We use the image cache for poster frames so we save as much
177        // network activity as possible.
178        let window = self.owner_window();
179        let image_cache = window.image_cache();
180        let cache_result = image_cache.get_cached_image_status(
181            poster_url.clone(),
182            window.origin().immutable().clone(),
183            None,
184        );
185
186        let id = match cache_result {
187            ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable {
188                image,
189                url,
190                ..
191            }) => {
192                self.process_image_response(ImageResponse::Loaded(image, url), can_gc);
193                return;
194            },
195            ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(_, id)) => id,
196            ImageCacheResult::ReadyForRequest(id) => {
197                self.do_fetch_poster_frame(poster_url, id, can_gc);
198                id
199            },
200            ImageCacheResult::FailedToLoadOrDecode => {
201                self.process_image_response(ImageResponse::FailedToLoadOrDecode, can_gc);
202                return;
203            },
204            ImageCacheResult::Pending(id) => id,
205        };
206
207        let trusted_node = Trusted::new(self);
208        let generation = self.generation_id();
209        let callback = window.register_image_cache_listener(id, move |response| {
210            let element = trusted_node.root();
211
212            // Ignore any image response for a previous request that has been discarded.
213            if generation != element.generation_id() {
214                return;
215            }
216            element.process_image_response(response.response, CanGc::note());
217        });
218
219        image_cache.add_listener(ImageLoadListener::new(callback, window.pipeline_id(), id));
220    }
221
222    /// <https://html.spec.whatwg.org/multipage/#poster-frame>
223    fn do_fetch_poster_frame(&self, poster_url: ServoUrl, id: PendingImageId, can_gc: CanGc) {
224        // Step 5. Let request be a new request whose URL is url, client is the element's node
225        // document's relevant settings object, destination is "image", initiator type is "video",
226        // credentials mode is "include", and whose use-URL-credentials flag is set.
227        let document = self.owner_document();
228        let global = self.owner_global();
229        let request = RequestBuilder::new(
230            Some(document.webview_id()),
231            poster_url.clone(),
232            global.get_referrer(),
233        )
234        .destination(Destination::Image)
235        .credentials_mode(CredentialsMode::Include)
236        .use_url_credentials(true)
237        .with_global_scope(&global);
238
239        // Step 6. Fetch request. This must delay the load event of the element's node document.
240        // This delay must be independent from the ones created by HTMLMediaElement during
241        // its media load algorithm, otherwise a code like
242        // <video poster="poster.png"></video>
243        // (which triggers no media load algorithm unless a explicit call to .load() is done)
244        // will block the document's load event forever.
245        let blocker = &self.load_blocker;
246        LoadBlocker::terminate(blocker, can_gc);
247        *blocker.borrow_mut() = Some(LoadBlocker::new(
248            &self.owner_document(),
249            LoadType::Image(poster_url.clone()),
250        ));
251
252        let context = PosterFrameFetchContext::new(
253            self,
254            poster_url,
255            id,
256            request.id,
257            self.global().core_resource_thread(),
258        );
259        self.owner_document().fetch_background(request, context);
260    }
261
262    fn generation_id(&self) -> u32 {
263        self.generation_id.get()
264    }
265
266    /// <https://html.spec.whatwg.org/multipage/#poster-frame>
267    fn process_image_response(&self, response: ImageResponse, can_gc: CanGc) {
268        // Step 7. If an image is thus obtained, the poster frame is that image.
269        // Otherwise, there is no poster frame.
270        match response {
271            ImageResponse::Loaded(image, url) => {
272                debug!("Loaded poster image for video element: {:?}", url);
273                match image.as_raster_image() {
274                    Some(image) => self.htmlmediaelement.set_poster_frame(Some(image)),
275                    None => warn!("Vector images are not yet supported in video poster"),
276                }
277                LoadBlocker::terminate(&self.load_blocker, can_gc);
278            },
279            ImageResponse::MetadataLoaded(..) => {},
280            // The image cache may have loaded a placeholder for an invalid poster url
281            ImageResponse::FailedToLoadOrDecode => {
282                self.htmlmediaelement.set_poster_frame(None);
283                // A failed load should unblock the document load.
284                LoadBlocker::terminate(&self.load_blocker, can_gc);
285            },
286        }
287    }
288
289    /// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
290    pub(crate) fn is_usable(&self) -> bool {
291        !matches!(
292            self.htmlmediaelement.get_ready_state(),
293            ReadyState::HaveNothing | ReadyState::HaveMetadata
294        )
295    }
296
297    pub(crate) fn origin_is_clean(&self) -> bool {
298        self.htmlmediaelement.origin_is_clean()
299    }
300
301    pub(crate) fn is_network_state_empty(&self) -> bool {
302        self.htmlmediaelement.network_state() == NetworkState::Empty
303    }
304}
305
306impl HTMLVideoElementMethods<crate::DomTypeHolder> for HTMLVideoElement {
307    // <https://html.spec.whatwg.org/multipage/#dom-video-width>
308    make_dimension_uint_getter!(Width, "width");
309
310    // <https://html.spec.whatwg.org/multipage/#dom-video-width>
311    make_dimension_uint_setter!(SetWidth, "width");
312
313    // <https://html.spec.whatwg.org/multipage/#dom-video-height>
314    make_dimension_uint_getter!(Height, "height");
315
316    // <https://html.spec.whatwg.org/multipage/#dom-video-height>
317    make_dimension_uint_setter!(SetHeight, "height");
318
319    /// <https://html.spec.whatwg.org/multipage/#dom-video-videowidth>
320    fn VideoWidth(&self) -> u32 {
321        if self.htmlmediaelement.get_ready_state() == ReadyState::HaveNothing {
322            return 0;
323        }
324        self.video_width.get().unwrap_or(0)
325    }
326
327    /// <https://html.spec.whatwg.org/multipage/#dom-video-videoheight>
328    fn VideoHeight(&self) -> u32 {
329        if self.htmlmediaelement.get_ready_state() == ReadyState::HaveNothing {
330            return 0;
331        }
332        self.video_height.get().unwrap_or(0)
333    }
334
335    // https://html.spec.whatwg.org/multipage/#dom-video-poster
336    make_getter!(Poster, "poster");
337
338    // https://html.spec.whatwg.org/multipage/#dom-video-poster
339    make_setter!(SetPoster, "poster");
340
341    // For testing purposes only. This is not an event from
342    // https://html.spec.whatwg.org/multipage/#dom-video-poster
343    event_handler!(postershown, GetOnpostershown, SetOnpostershown);
344}
345
346impl VirtualMethods for HTMLVideoElement {
347    fn super_type(&self) -> Option<&dyn VirtualMethods> {
348        Some(self.upcast::<HTMLMediaElement>() as &dyn VirtualMethods)
349    }
350
351    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
352        self.super_type()
353            .unwrap()
354            .attribute_mutated(attr, mutation, can_gc);
355
356        if attr.local_name() == &local_name!("poster") {
357            if let Some(new_value) = mutation.new_value(attr) {
358                self.update_poster_frame(Some(&new_value), CanGc::note())
359            } else {
360                self.update_poster_frame(None, CanGc::note())
361            }
362        };
363    }
364
365    fn attribute_affects_presentational_hints(&self, attr: &Attr) -> bool {
366        match attr.local_name() {
367            &local_name!("width") | &local_name!("height") => true,
368            _ => self
369                .super_type()
370                .unwrap()
371                .attribute_affects_presentational_hints(attr),
372        }
373    }
374
375    fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
376        match name {
377            &local_name!("width") | &local_name!("height") => {
378                AttrValue::from_dimension(value.into())
379            },
380            _ => self
381                .super_type()
382                .unwrap()
383                .parse_plain_attribute(name, value),
384        }
385    }
386}
387
388struct PosterFrameFetchContext {
389    /// Reference to the script thread image cache.
390    image_cache: Arc<dyn ImageCache>,
391    /// The element that initiated the request.
392    elem: Trusted<HTMLVideoElement>,
393    /// The cache ID for this request.
394    id: PendingImageId,
395    /// True if this response is invalid and should be ignored.
396    cancelled: bool,
397    /// Url for the resource
398    url: ServoUrl,
399    /// A [`FetchCanceller`] for this request.
400    fetch_canceller: FetchCanceller,
401}
402
403impl FetchResponseListener for PosterFrameFetchContext {
404    fn process_request_body(&mut self, _: RequestId) {}
405
406    fn process_request_eof(&mut self, _: RequestId) {
407        self.fetch_canceller.ignore()
408    }
409
410    fn process_response(
411        &mut self,
412        request_id: RequestId,
413        metadata: Result<FetchMetadata, NetworkError>,
414    ) {
415        self.image_cache.notify_pending_response(
416            self.id,
417            FetchResponseMsg::ProcessResponse(request_id, metadata.clone()),
418        );
419
420        let metadata = metadata.ok().map(|meta| match meta {
421            FetchMetadata::Unfiltered(m) => m,
422            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
423        });
424
425        let status_is_ok = metadata
426            .as_ref()
427            .map_or(true, |m| m.status.in_range(200..300));
428
429        if !status_is_ok {
430            self.cancelled = true;
431            self.fetch_canceller.abort();
432        }
433    }
434
435    fn process_response_chunk(&mut self, request_id: RequestId, payload: Vec<u8>) {
436        if self.cancelled {
437            // An error was received previously, skip processing the payload.
438            return;
439        }
440
441        self.image_cache.notify_pending_response(
442            self.id,
443            FetchResponseMsg::ProcessResponseChunk(request_id, payload.into()),
444        );
445    }
446
447    fn process_response_eof(
448        self,
449        request_id: RequestId,
450        response: Result<(), NetworkError>,
451        timing: ResourceFetchTiming,
452    ) {
453        self.image_cache.notify_pending_response(
454            self.id,
455            FetchResponseMsg::ProcessResponseEOF(request_id, response.clone(), timing.clone()),
456        );
457        network_listener::submit_timing(&self, &response, &timing, CanGc::note());
458    }
459
460    fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
461        let global = &self.resource_timing_global();
462        global.report_csp_violations(violations, None, None);
463    }
464}
465
466impl ResourceTimingListener for PosterFrameFetchContext {
467    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
468        let initiator_type = InitiatorType::LocalName(
469            self.elem
470                .root()
471                .upcast::<Element>()
472                .local_name()
473                .to_string(),
474        );
475        (initiator_type, self.url.clone())
476    }
477
478    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
479        self.elem.root().owner_document().global()
480    }
481}
482
483impl PosterFrameFetchContext {
484    fn new(
485        elem: &HTMLVideoElement,
486        url: ServoUrl,
487        id: PendingImageId,
488        request_id: RequestId,
489        core_resource_thread: CoreResourceThread,
490    ) -> PosterFrameFetchContext {
491        let window = elem.owner_window();
492        PosterFrameFetchContext {
493            image_cache: window.image_cache(),
494            elem: Trusted::new(elem),
495            id,
496            cancelled: false,
497            url,
498            fetch_canceller: FetchCanceller::new(request_id, false, core_resource_thread),
499        }
500    }
501}
502
503pub(crate) trait LayoutHTMLVideoElementHelpers {
504    fn data(self) -> HTMLMediaData;
505    fn get_width(self) -> LengthOrPercentageOrAuto;
506    fn get_height(self) -> LengthOrPercentageOrAuto;
507}
508
509impl LayoutHTMLVideoElementHelpers for LayoutDom<'_, HTMLVideoElement> {
510    fn data(self) -> HTMLMediaData {
511        let video = self.unsafe_get();
512
513        // Get the current frame being rendered.
514        let current_frame = video.htmlmediaelement.get_current_frame_to_present();
515
516        // This value represents the natural width and height of the video.
517        // It may exist even if there is no current frame (for example, after the
518        // metadata of the video is loaded).
519        let metadata = video
520            .get_video_width()
521            .zip(video.get_video_height())
522            .map(|(width, height)| MediaMetadata { width, height });
523
524        HTMLMediaData {
525            current_frame,
526            metadata,
527        }
528    }
529
530    fn get_width(self) -> LengthOrPercentageOrAuto {
531        self.upcast::<Element>()
532            .get_attr_for_layout(&ns!(), &local_name!("width"))
533            .map(AttrValue::as_dimension)
534            .cloned()
535            .unwrap_or(LengthOrPercentageOrAuto::Auto)
536    }
537
538    fn get_height(self) -> LengthOrPercentageOrAuto {
539        self.upcast::<Element>()
540            .get_attr_for_layout(&ns!(), &local_name!("height"))
541            .map(AttrValue::as_dimension)
542            .cloned()
543            .unwrap_or(LengthOrPercentageOrAuto::Auto)
544    }
545}