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