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