1use 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 video_width: Cell<Option<u32>>,
52 video_height: Cell<Option<u32>>,
54 generation_id: Cell<u32>,
56 load_blocker: DomRefCell<Option<LoadBlocker>>,
59 #[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 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 Some(Snapshot::cleared(size.cast()))
144 }
145 },
146 None => None,
147 }
148 }
149
150 fn update_poster_frame(&self, poster_url: Option<&str>, can_gc: CanGc) {
152 self.generation_id.set(self.generation_id.get() + 1);
156
157 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 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 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 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 fn do_fetch_poster_frame(&self, poster_url: ServoUrl, id: PendingImageId, can_gc: CanGc) {
224 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 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 fn process_image_response(&self, response: ImageResponse, can_gc: CanGc) {
268 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 ImageResponse::FailedToLoadOrDecode => {
282 self.htmlmediaelement.set_poster_frame(None);
283 LoadBlocker::terminate(&self.load_blocker, can_gc);
285 },
286 }
287 }
288
289 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 make_dimension_uint_getter!(Width, "width");
309
310 make_dimension_uint_setter!(SetWidth, "width");
312
313 make_dimension_uint_getter!(Height, "height");
315
316 make_dimension_uint_setter!(SetHeight, "height");
318
319 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 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 make_getter!(Poster, "poster");
337
338 make_setter!(SetPoster, "poster");
340
341 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 image_cache: Arc<dyn ImageCache>,
391 elem: Trusted<HTMLVideoElement>,
393 id: PendingImageId,
395 cancelled: bool,
397 url: ServoUrl,
399 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 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 let current_frame = video.htmlmediaelement.get_current_frame_to_present();
515
516 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}