mirror of
https://github.com/servo/servo.git
synced 2025-07-22 06:43:40 +01:00
Improve how intrinsic sizes work for videos (#31746)
* feat: patch for video layout sizes added rebase from main 2024/10/05 Co-authored-by: Josh Matthews <josh@joshmatthews.net> Signed-off-by: eri <epazos@igalia.com> * feat: take width and height parameters if provided Signed-off-by: eri <epazos@igalia.com> * chore: tidy the code and update test expectations Signed-off-by: eri <epazos@igalia.com> * feat: handle removing poster Signed-off-by: eri <epazos@igalia.com> * chore: update test expectations and remove debug code Signed-off-by: eri <epazos@igalia.com> * fix: issues after rebasing to main Signed-off-by: eri <epazos@igalia.com> * feat: pass src remove test and tidy Signed-off-by: eri <epazos@igalia.com> * chore: clippy fixes Signed-off-by: eri <epazos@igalia.com> * chore: update passing test expectations Signed-off-by: eri <epazos@igalia.com> * fix object-position-svg test Signed-off-by: eri <epazos@igalia.com> * fix unintentional override of video size and resize events Signed-off-by: eri <epazos@igalia.com> * change how resize events are sent to better match the spec Signed-off-by: eri <epazos@igalia.com> * simplify poster mutation handling Co-authored-by: Oriol Brufau <obrufau@igalia.com> Signed-off-by: eri <eri@inventati.org> * improved handling of intrinsic sizes - differentiate between natural size and css size - presentational attributes - fallback ratio for video element - handle more cases where the src/poster are added/removed - aspect ratio hints Signed-off-by: eri <epazos@igalia.com> * update test expectations Signed-off-by: eri <epazos@igalia.com> * fix cleaning current frame Signed-off-by: eri <epazos@igalia.com> * update test expectations Signed-off-by: eri <epazos@igalia.com> * Apply suggestions from code review Co-authored-by: Oriol Brufau <obrufau@igalia.com> Signed-off-by: eri <eri@inventati.org> * More code review suggestions Signed-off-by: eri <epazos@igalia.com> * Prevent aspect-ratio:auto from pulling the ratio from the default object size As resolved in https://github.com/w3c/csswg-drafts/issues/7524#issuecomment-1204462924 Signed-off-by: Oriol Brufau <obrufau@igalia.com> --------- Signed-off-by: eri <epazos@igalia.com> Signed-off-by: eri <eri@inventati.org> Signed-off-by: Oriol Brufau <obrufau@igalia.com> Co-authored-by: Josh Matthews <josh@joshmatthews.net> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
parent
43d1601016
commit
01820e2a8a
25 changed files with 290 additions and 360 deletions
|
@ -1919,14 +1919,14 @@ impl Fragment {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SpecificFragmentInfo::Media(ref fragment_info) => {
|
SpecificFragmentInfo::Media(ref fragment_info) => {
|
||||||
if let Some((ref image_key, _, _)) = fragment_info.current_frame {
|
if let Some(ref media_frame) = fragment_info.current_frame {
|
||||||
let base = create_base_display_item(state);
|
let base = create_base_display_item(state);
|
||||||
state.add_image_item(
|
state.add_image_item(
|
||||||
base,
|
base,
|
||||||
webrender_api::ImageDisplayItem {
|
webrender_api::ImageDisplayItem {
|
||||||
bounds: stacking_relative_content_box.to_layout(),
|
bounds: stacking_relative_content_box.to_layout(),
|
||||||
common: items::empty_common_item_properties(),
|
common: items::empty_common_item_properties(),
|
||||||
image_key: *image_key,
|
image_key: media_frame.image_key,
|
||||||
image_rendering: ImageRendering::Auto,
|
image_rendering: ImageRendering::Auto,
|
||||||
alpha_type: webrender_api::AlphaType::PremultipliedAlpha,
|
alpha_type: webrender_api::AlphaType::PremultipliedAlpha,
|
||||||
color: webrender_api::ColorF::WHITE,
|
color: webrender_api::ColorF::WHITE,
|
||||||
|
|
|
@ -26,7 +26,9 @@ use range::*;
|
||||||
use script_layout_interface::wrapper_traits::{
|
use script_layout_interface::wrapper_traits::{
|
||||||
PseudoElementType, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
|
PseudoElementType, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
|
||||||
};
|
};
|
||||||
use script_layout_interface::{HTMLCanvasData, HTMLCanvasDataSource, HTMLMediaData, SVGSVGData};
|
use script_layout_interface::{
|
||||||
|
HTMLCanvasData, HTMLCanvasDataSource, HTMLMediaData, MediaFrame, SVGSVGData,
|
||||||
|
};
|
||||||
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
||||||
use servo_url::ServoUrl;
|
use servo_url::ServoUrl;
|
||||||
use style::computed_values::border_collapse::T as BorderCollapse;
|
use style::computed_values::border_collapse::T as BorderCollapse;
|
||||||
|
@ -381,7 +383,7 @@ impl CanvasFragmentInfo {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MediaFragmentInfo {
|
pub struct MediaFragmentInfo {
|
||||||
pub current_frame: Option<(webrender_api::ImageKey, i32, i32)>,
|
pub current_frame: Option<MediaFrame>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaFragmentInfo {
|
impl MediaFragmentInfo {
|
||||||
|
@ -1036,13 +1038,9 @@ impl Fragment {
|
||||||
Au(0)
|
Au(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SpecificFragmentInfo::Media(ref info) => {
|
SpecificFragmentInfo::Media(ref info) => info
|
||||||
if let Some((_, width, _)) = info.current_frame {
|
.current_frame
|
||||||
Au::from_px(width)
|
.map_or(Au(0), |frame| Au::from_px(frame.width)),
|
||||||
} else {
|
|
||||||
Au(0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SpecificFragmentInfo::Canvas(ref info) => info.dom_width,
|
SpecificFragmentInfo::Canvas(ref info) => info.dom_width,
|
||||||
SpecificFragmentInfo::Svg(ref info) => info.dom_width,
|
SpecificFragmentInfo::Svg(ref info) => info.dom_width,
|
||||||
// Note: Currently for replaced element with no intrinsic size,
|
// Note: Currently for replaced element with no intrinsic size,
|
||||||
|
@ -1066,13 +1064,9 @@ impl Fragment {
|
||||||
Au(0)
|
Au(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SpecificFragmentInfo::Media(ref info) => {
|
SpecificFragmentInfo::Media(ref info) => info
|
||||||
if let Some((_, _, height)) = info.current_frame {
|
.current_frame
|
||||||
Au::from_px(height)
|
.map_or(Au(0), |frame| Au::from_px(frame.height)),
|
||||||
} else {
|
|
||||||
Au(0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SpecificFragmentInfo::Canvas(ref info) => info.dom_height,
|
SpecificFragmentInfo::Canvas(ref info) => info.dom_height,
|
||||||
SpecificFragmentInfo::Svg(ref info) => info.dom_height,
|
SpecificFragmentInfo::Svg(ref info) => info.dom_height,
|
||||||
SpecificFragmentInfo::Iframe(_) => Au::from_px(DEFAULT_REPLACED_HEIGHT),
|
SpecificFragmentInfo::Iframe(_) => Au::from_px(DEFAULT_REPLACED_HEIGHT),
|
||||||
|
|
|
@ -291,14 +291,16 @@ impl Fragment {
|
||||||
.to_webrender();
|
.to_webrender();
|
||||||
let common = builder.common_properties(clip, &image.style);
|
let common = builder.common_properties(clip, &image.style);
|
||||||
|
|
||||||
builder.wr().push_image(
|
if let Some(image_key) = image.image_key {
|
||||||
&common,
|
builder.wr().push_image(
|
||||||
rect,
|
&common,
|
||||||
image_rendering,
|
rect,
|
||||||
wr::AlphaType::PremultipliedAlpha,
|
image_rendering,
|
||||||
image.image_key,
|
wr::AlphaType::PremultipliedAlpha,
|
||||||
wr::ColorF::WHITE,
|
image_key,
|
||||||
);
|
wr::ColorF::WHITE,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Visibility::Hidden => (),
|
Visibility::Hidden => (),
|
||||||
Visibility::Collapse => (),
|
Visibility::Collapse => (),
|
||||||
|
|
|
@ -102,7 +102,7 @@ pub(crate) trait NodeExt<'dom>: 'dom + LayoutNode<'dom> {
|
||||||
fn as_image(self) -> Option<(Option<Arc<Image>>, PhysicalSize<f64>)>;
|
fn as_image(self) -> Option<(Option<Arc<Image>>, PhysicalSize<f64>)>;
|
||||||
fn as_canvas(self) -> Option<(CanvasInfo, PhysicalSize<f64>)>;
|
fn as_canvas(self) -> Option<(CanvasInfo, PhysicalSize<f64>)>;
|
||||||
fn as_iframe(self) -> Option<(PipelineId, BrowsingContextId)>;
|
fn as_iframe(self) -> Option<(PipelineId, BrowsingContextId)>;
|
||||||
fn as_video(self) -> Option<(webrender_api::ImageKey, PhysicalSize<f64>)>;
|
fn as_video(self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)>;
|
||||||
fn as_typeless_object_with_data_attribute(self) -> Option<String>;
|
fn as_typeless_object_with_data_attribute(self) -> Option<String>;
|
||||||
fn style(self, context: &LayoutContext) -> ServoArc<ComputedValues>;
|
fn style(self, context: &LayoutContext) -> ServoArc<ComputedValues>;
|
||||||
|
|
||||||
|
@ -136,11 +136,19 @@ where
|
||||||
Some((resource, PhysicalSize::new(width, height)))
|
Some((resource, PhysicalSize::new(width, height)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_video(self) -> Option<(webrender_api::ImageKey, PhysicalSize<f64>)> {
|
fn as_video(self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)> {
|
||||||
let node = self.to_threadsafe();
|
let node = self.to_threadsafe();
|
||||||
let frame_data = node.media_data()?.current_frame?;
|
let data = node.media_data()?;
|
||||||
let (width, height) = (frame_data.1 as f64, frame_data.2 as f64);
|
let natural_size = if let Some(frame) = data.current_frame {
|
||||||
Some((frame_data.0, PhysicalSize::new(width, height)))
|
Some(PhysicalSize::new(frame.width.into(), frame.height.into()))
|
||||||
|
} else {
|
||||||
|
data.metadata
|
||||||
|
.map(|meta| PhysicalSize::new(meta.width.into(), meta.height.into()))
|
||||||
|
};
|
||||||
|
Some((
|
||||||
|
data.current_frame.map(|frame| frame.image_key),
|
||||||
|
natural_size,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_canvas(self) -> Option<(CanvasInfo, PhysicalSize<f64>)> {
|
fn as_canvas(self) -> Option<(CanvasInfo, PhysicalSize<f64>)> {
|
||||||
|
|
|
@ -85,7 +85,7 @@ pub(crate) struct ImageFragment {
|
||||||
pub rect: PhysicalRect<Au>,
|
pub rect: PhysicalRect<Au>,
|
||||||
pub clip: PhysicalRect<Au>,
|
pub clip: PhysicalRect<Au>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub image_key: ImageKey,
|
pub image_key: Option<ImageKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
|
@ -137,7 +137,7 @@ pub(crate) enum ReplacedContentKind {
|
||||||
Image(Option<Arc<Image>>),
|
Image(Option<Arc<Image>>),
|
||||||
IFrame(IFrameInfo),
|
IFrame(IFrameInfo),
|
||||||
Canvas(CanvasInfo),
|
Canvas(CanvasInfo),
|
||||||
Video(VideoInfo),
|
Video(Option<VideoInfo>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReplacedContent {
|
impl ReplacedContent {
|
||||||
|
@ -173,24 +173,25 @@ impl ReplacedContent {
|
||||||
)
|
)
|
||||||
} else if let Some((image_key, natural_size_in_dots)) = element.as_video() {
|
} else if let Some((image_key, natural_size_in_dots)) = element.as_video() {
|
||||||
(
|
(
|
||||||
ReplacedContentKind::Video(VideoInfo { image_key }),
|
ReplacedContentKind::Video(image_key.map(|key| VideoInfo { image_key: key })),
|
||||||
Some(natural_size_in_dots),
|
natural_size_in_dots,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let natural_size =
|
let natural_size = if let Some(naturalc_size_in_dots) = natural_size_in_dots {
|
||||||
natural_size_in_dots.map_or_else(NaturalSizes::empty, |naturalc_size_in_dots| {
|
// FIXME: should 'image-resolution' (when implemented) be used *instead* of
|
||||||
// FIXME: should 'image-resolution' (when implemented) be used *instead* of
|
// `script::dom::htmlimageelement::ImageRequest::current_pixel_density`?
|
||||||
// `script::dom::htmlimageelement::ImageRequest::current_pixel_density`?
|
// https://drafts.csswg.org/css-images-4/#the-image-resolution
|
||||||
// https://drafts.csswg.org/css-images-4/#the-image-resolution
|
let dppx = 1.0;
|
||||||
let dppx = 1.0;
|
let width = (naturalc_size_in_dots.width as CSSFloat) / dppx;
|
||||||
let width = (naturalc_size_in_dots.width as CSSFloat) / dppx;
|
let height = (naturalc_size_in_dots.height as CSSFloat) / dppx;
|
||||||
let height = (naturalc_size_in_dots.height as CSSFloat) / dppx;
|
NaturalSizes::from_width_and_height(width, height)
|
||||||
NaturalSizes::from_width_and_height(width, height)
|
} else {
|
||||||
});
|
NaturalSizes::empty()
|
||||||
|
};
|
||||||
|
|
||||||
let base_fragment_info = BaseFragmentInfo::new_for_node(element.opaque());
|
let base_fragment_info = BaseFragmentInfo::new_for_node(element.opaque());
|
||||||
Some(Self {
|
Some(Self {
|
||||||
|
@ -354,7 +355,7 @@ impl ReplacedContent {
|
||||||
style: style.clone(),
|
style: style.clone(),
|
||||||
rect,
|
rect,
|
||||||
clip,
|
clip,
|
||||||
image_key,
|
image_key: Some(image_key),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -364,7 +365,7 @@ impl ReplacedContent {
|
||||||
style: style.clone(),
|
style: style.clone(),
|
||||||
rect,
|
rect,
|
||||||
clip,
|
clip,
|
||||||
image_key: video.image_key,
|
image_key: video.as_ref().map(|video| video.image_key),
|
||||||
})],
|
})],
|
||||||
ReplacedContentKind::IFrame(iframe) => {
|
ReplacedContentKind::IFrame(iframe) => {
|
||||||
vec![Fragment::IFrame(IFrameFragment {
|
vec![Fragment::IFrame(IFrameFragment {
|
||||||
|
@ -403,7 +404,7 @@ impl ReplacedContent {
|
||||||
style: style.clone(),
|
style: style.clone(),
|
||||||
rect,
|
rect,
|
||||||
clip,
|
clip,
|
||||||
image_key,
|
image_key: Some(image_key),
|
||||||
})]
|
})]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -449,6 +450,16 @@ impl ReplacedContent {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_object_size() -> PhysicalSize<Au> {
|
||||||
|
// FIXME:
|
||||||
|
// https://drafts.csswg.org/css-images/#default-object-size
|
||||||
|
// “If 300px is too wide to fit the device, UAs should use the width of
|
||||||
|
// the largest rectangle that has a 2:1 ratio and fits the device instead.”
|
||||||
|
// “height of the largest rectangle that has a 2:1 ratio, has a height not greater
|
||||||
|
// than 150px, and has a width not greater than the device width.”
|
||||||
|
PhysicalSize::new(Au::from_px(300), Au::from_px(150))
|
||||||
|
}
|
||||||
|
|
||||||
/// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-width>
|
/// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-width>
|
||||||
/// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-height>
|
/// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-height>
|
||||||
///
|
///
|
||||||
|
@ -464,20 +475,19 @@ impl ReplacedContent {
|
||||||
) -> LogicalVec2<Au> {
|
) -> LogicalVec2<Au> {
|
||||||
let mode = style.writing_mode;
|
let mode = style.writing_mode;
|
||||||
let intrinsic_size = self.flow_relative_intrinsic_size(style);
|
let intrinsic_size = self.flow_relative_intrinsic_size(style);
|
||||||
let intrinsic_ratio = self.preferred_aspect_ratio(&containing_block.into(), style);
|
let intrinsic_ratio = self
|
||||||
|
.preferred_aspect_ratio(&containing_block.into(), style)
|
||||||
|
.or_else(|| {
|
||||||
|
matches!(self.kind, ReplacedContentKind::Video(_)).then(|| {
|
||||||
|
let size = Self::default_object_size();
|
||||||
|
AspectRatio::from_content_ratio(
|
||||||
|
size.width.to_f32_px() / size.height.to_f32_px(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let default_object_size = || {
|
let default_object_size =
|
||||||
// FIXME:
|
|| LogicalVec2::from_physical_size(&Self::default_object_size(), mode);
|
||||||
// https://drafts.csswg.org/css-images/#default-object-size
|
|
||||||
// “If 300px is too wide to fit the device, UAs should use the width of
|
|
||||||
// the largest rectangle that has a 2:1 ratio and fits the device instead.”
|
|
||||||
// “height of the largest rectangle that has a 2:1 ratio, has a height not greater
|
|
||||||
// than 150px, and has a width not greater than the device width.”
|
|
||||||
LogicalVec2::from_physical_size(
|
|
||||||
&PhysicalSize::new(Au::from_px(300), Au::from_px(150)),
|
|
||||||
mode,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let get_tentative_size = |LogicalVec2 { inline, block }| -> LogicalVec2<Au> {
|
let get_tentative_size = |LogicalVec2 { inline, block }| -> LogicalVec2<Au> {
|
||||||
match (inline, block) {
|
match (inline, block) {
|
||||||
|
|
|
@ -177,6 +177,13 @@ impl AspectRatio {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_content_ratio(i_over_b: CSSFloat) -> Self {
|
||||||
|
Self {
|
||||||
|
box_sizing_adjustment: LogicalVec2::zero(),
|
||||||
|
i_over_b,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -905,25 +912,18 @@ impl ComputedValuesExt for ComputedValues {
|
||||||
// ratio; otherwise the box has no preferred aspect ratio. Size
|
// ratio; otherwise the box has no preferred aspect ratio. Size
|
||||||
// calculations involving the aspect ratio work with the content box
|
// calculations involving the aspect ratio work with the content box
|
||||||
// dimensions always."
|
// dimensions always."
|
||||||
(_, PreferredRatio::None) => natural_aspect_ratio.map(|natural_ratio| AspectRatio {
|
(_, PreferredRatio::None) => natural_aspect_ratio.map(AspectRatio::from_content_ratio),
|
||||||
i_over_b: natural_ratio,
|
|
||||||
box_sizing_adjustment: LogicalVec2::zero(),
|
|
||||||
}),
|
|
||||||
// "If both auto and a <ratio> are specified together, the preferred
|
// "If both auto and a <ratio> are specified together, the preferred
|
||||||
// aspect ratio is the specified ratio of width / height unless it
|
// aspect ratio is the specified ratio of width / height unless it
|
||||||
// is a replaced element with a natural aspect ratio, in which case
|
// is a replaced element with a natural aspect ratio, in which case
|
||||||
// that aspect ratio is used instead. In all cases, size
|
// that aspect ratio is used instead. In all cases, size
|
||||||
// calculations involving the aspect ratio work with the content box
|
// calculations involving the aspect ratio work with the content box
|
||||||
// dimensions always."
|
// dimensions always."
|
||||||
(true, PreferredRatio::Ratio(preferred_ratio)) => match natural_aspect_ratio {
|
(true, PreferredRatio::Ratio(preferred_ratio)) => {
|
||||||
Some(natural_ratio) => Some(AspectRatio {
|
Some(AspectRatio::from_content_ratio(
|
||||||
i_over_b: natural_ratio,
|
natural_aspect_ratio
|
||||||
box_sizing_adjustment: LogicalVec2::zero(),
|
.unwrap_or_else(|| (preferred_ratio.0).0 / (preferred_ratio.1).0),
|
||||||
}),
|
))
|
||||||
None => Some(AspectRatio {
|
|
||||||
i_over_b: (preferred_ratio.0).0 / (preferred_ratio.1).0,
|
|
||||||
box_sizing_adjustment: LogicalVec2::zero(),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// "The box’s preferred aspect ratio is the specified ratio of width
|
// "The box’s preferred aspect ratio is the specified ratio of width
|
||||||
|
|
|
@ -55,6 +55,8 @@ use style::selector_parser::{
|
||||||
use style::shared_lock::{Locked, SharedRwLock};
|
use style::shared_lock::{Locked, SharedRwLock};
|
||||||
use style::stylesheets::layer_rule::LayerOrder;
|
use style::stylesheets::layer_rule::LayerOrder;
|
||||||
use style::stylesheets::{CssRuleType, UrlExtraData};
|
use style::stylesheets::{CssRuleType, UrlExtraData};
|
||||||
|
use style::values::generics::position::PreferredRatio;
|
||||||
|
use style::values::generics::ratio::Ratio;
|
||||||
use style::values::generics::NonNegative;
|
use style::values::generics::NonNegative;
|
||||||
use style::values::{computed, specified, AtomIdent, AtomString, CSSFloat};
|
use style::values::{computed, specified, AtomIdent, AtomString, CSSFloat};
|
||||||
use style::{dom_apis, thread_state, ArcSlice, CaseSensitivityExt};
|
use style::{dom_apis, thread_state, ArcSlice, CaseSensitivityExt};
|
||||||
|
@ -132,6 +134,7 @@ use crate::dom::htmltablesectionelement::{
|
||||||
};
|
};
|
||||||
use crate::dom::htmltemplateelement::HTMLTemplateElement;
|
use crate::dom::htmltemplateelement::HTMLTemplateElement;
|
||||||
use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers};
|
use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers};
|
||||||
|
use crate::dom::htmlvideoelement::{HTMLVideoElement, LayoutHTMLVideoElementHelpers};
|
||||||
use crate::dom::mutationobserver::{Mutation, MutationObserver};
|
use crate::dom::mutationobserver::{Mutation, MutationObserver};
|
||||||
use crate::dom::namednodemap::NamedNodeMap;
|
use crate::dom::namednodemap::NamedNodeMap;
|
||||||
use crate::dom::node::{
|
use crate::dom::node::{
|
||||||
|
@ -849,6 +852,8 @@ impl<'dom> LayoutElementHelpers<'dom> for LayoutDom<'dom, Element> {
|
||||||
this.get_width()
|
this.get_width()
|
||||||
} else if let Some(this) = self.downcast::<HTMLImageElement>() {
|
} else if let Some(this) = self.downcast::<HTMLImageElement>() {
|
||||||
this.get_width()
|
this.get_width()
|
||||||
|
} else if let Some(this) = self.downcast::<HTMLVideoElement>() {
|
||||||
|
this.get_width()
|
||||||
} else if let Some(this) = self.downcast::<HTMLTableElement>() {
|
} else if let Some(this) = self.downcast::<HTMLTableElement>() {
|
||||||
this.get_width()
|
this.get_width()
|
||||||
} else if let Some(this) = self.downcast::<HTMLTableCellElement>() {
|
} else if let Some(this) = self.downcast::<HTMLTableCellElement>() {
|
||||||
|
@ -891,6 +896,8 @@ impl<'dom> LayoutElementHelpers<'dom> for LayoutDom<'dom, Element> {
|
||||||
this.get_height()
|
this.get_height()
|
||||||
} else if let Some(this) = self.downcast::<HTMLImageElement>() {
|
} else if let Some(this) = self.downcast::<HTMLImageElement>() {
|
||||||
this.get_height()
|
this.get_height()
|
||||||
|
} else if let Some(this) = self.downcast::<HTMLVideoElement>() {
|
||||||
|
this.get_height()
|
||||||
} else if let Some(this) = self.downcast::<HTMLTableElement>() {
|
} else if let Some(this) = self.downcast::<HTMLTableElement>() {
|
||||||
this.get_height()
|
this.get_height()
|
||||||
} else if let Some(this) = self.downcast::<HTMLTableCellElement>() {
|
} else if let Some(this) = self.downcast::<HTMLTableCellElement>() {
|
||||||
|
@ -927,6 +934,27 @@ impl<'dom> LayoutElementHelpers<'dom> for LayoutDom<'dom, Element> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aspect ratio when providing both width and height.
|
||||||
|
// https://html.spec.whatwg.org/multipage/#attributes-for-embedded-content-and-images
|
||||||
|
if self.downcast::<HTMLImageElement>().is_some() ||
|
||||||
|
self.downcast::<HTMLVideoElement>().is_some()
|
||||||
|
{
|
||||||
|
if let LengthOrPercentageOrAuto::Length(width) = width {
|
||||||
|
if let LengthOrPercentageOrAuto::Length(height) = height {
|
||||||
|
let width_value = NonNegative(specified::Number::new(width.to_f32_px()));
|
||||||
|
let height_value = NonNegative(specified::Number::new(height.to_f32_px()));
|
||||||
|
let aspect_ratio = specified::position::AspectRatio {
|
||||||
|
auto: true,
|
||||||
|
ratio: PreferredRatio::Ratio(Ratio(width_value, height_value)),
|
||||||
|
};
|
||||||
|
hints.push(from_declaration(
|
||||||
|
shared_lock,
|
||||||
|
PropertyDeclaration::AspectRatio(aspect_ratio),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let cols = if let Some(this) = self.downcast::<HTMLTextAreaElement>() {
|
let cols = if let Some(this) = self.downcast::<HTMLTextAreaElement>() {
|
||||||
match this.get_cols() {
|
match this.get_cols() {
|
||||||
0 => None,
|
0 => None,
|
||||||
|
|
|
@ -27,7 +27,7 @@ use net_traits::{
|
||||||
ResourceTimingType,
|
ResourceTimingType,
|
||||||
};
|
};
|
||||||
use pixels::Image;
|
use pixels::Image;
|
||||||
use script_layout_interface::HTMLMediaData;
|
use script_layout_interface::MediaFrame;
|
||||||
use servo_config::pref;
|
use servo_config::pref;
|
||||||
use servo_media::player::audio::AudioRenderer;
|
use servo_media::player::audio::AudioRenderer;
|
||||||
use servo_media::player::video::{VideoFrame, VideoFrameRenderer};
|
use servo_media::player::video::{VideoFrame, VideoFrameRenderer};
|
||||||
|
@ -68,7 +68,7 @@ use crate::dom::bindings::inheritance::Castable;
|
||||||
use crate::dom::bindings::num::Finite;
|
use crate::dom::bindings::num::Finite;
|
||||||
use crate::dom::bindings::refcounted::Trusted;
|
use crate::dom::bindings::refcounted::Trusted;
|
||||||
use crate::dom::bindings::reflector::DomObject;
|
use crate::dom::bindings::reflector::DomObject;
|
||||||
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
|
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
|
||||||
use crate::dom::bindings::str::{DOMString, USVString};
|
use crate::dom::bindings::str::{DOMString, USVString};
|
||||||
use crate::dom::blob::Blob;
|
use crate::dom::blob::Blob;
|
||||||
use crate::dom::document::Document;
|
use crate::dom::document::Document;
|
||||||
|
@ -158,7 +158,7 @@ impl FrameHolder {
|
||||||
pub struct MediaFrameRenderer {
|
pub struct MediaFrameRenderer {
|
||||||
player_id: Option<u64>,
|
player_id: Option<u64>,
|
||||||
compositor_api: CrossProcessCompositorApi,
|
compositor_api: CrossProcessCompositorApi,
|
||||||
current_frame: Option<(ImageKey, i32, i32)>,
|
current_frame: Option<MediaFrame>,
|
||||||
old_frame: Option<ImageKey>,
|
old_frame: Option<ImageKey>,
|
||||||
very_old_frame: Option<ImageKey>,
|
very_old_frame: Option<ImageKey>,
|
||||||
current_frame_holder: Option<FrameHolder>,
|
current_frame_holder: Option<FrameHolder>,
|
||||||
|
@ -179,8 +179,12 @@ impl MediaFrameRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_poster_frame(&mut self, image: Arc<Image>) {
|
fn render_poster_frame(&mut self, image: Arc<Image>) {
|
||||||
if let Some(image_id) = image.id {
|
if let Some(image_key) = image.id {
|
||||||
self.current_frame = Some((image_id, image.width as i32, image.height as i32));
|
self.current_frame = Some(MediaFrame {
|
||||||
|
image_key,
|
||||||
|
width: image.width as i32,
|
||||||
|
height: image.height as i32,
|
||||||
|
});
|
||||||
self.show_poster = true;
|
self.show_poster = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,13 +210,14 @@ impl VideoFrameRenderer for MediaFrameRenderer {
|
||||||
ImageDescriptorFlags::empty(),
|
ImageDescriptorFlags::empty(),
|
||||||
);
|
);
|
||||||
|
|
||||||
match self.current_frame {
|
match &mut self.current_frame {
|
||||||
Some((ref image_key, ref mut width, ref mut height))
|
Some(ref mut current_frame)
|
||||||
if *width == frame.get_width() && *height == frame.get_height() =>
|
if current_frame.width == frame.get_width() &&
|
||||||
|
current_frame.height == frame.get_height() =>
|
||||||
{
|
{
|
||||||
if !frame.is_gl_texture() {
|
if !frame.is_gl_texture() {
|
||||||
updates.push(ImageUpdate::UpdateImage(
|
updates.push(ImageUpdate::UpdateImage(
|
||||||
*image_key,
|
current_frame.image_key,
|
||||||
descriptor,
|
descriptor,
|
||||||
SerializableImageData::Raw(IpcSharedMemory::from_bytes(&frame.get_data())),
|
SerializableImageData::Raw(IpcSharedMemory::from_bytes(&frame.get_data())),
|
||||||
));
|
));
|
||||||
|
@ -226,17 +231,17 @@ impl VideoFrameRenderer for MediaFrameRenderer {
|
||||||
updates.push(ImageUpdate::DeleteImage(old_image_key));
|
updates.push(ImageUpdate::DeleteImage(old_image_key));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some((ref mut image_key, ref mut width, ref mut height)) => {
|
Some(ref mut current_frame) => {
|
||||||
self.old_frame = Some(*image_key);
|
self.old_frame = Some(current_frame.image_key);
|
||||||
|
|
||||||
let Some(new_image_key) = self.compositor_api.generate_image_key() else {
|
let Some(new_image_key) = self.compositor_api.generate_image_key() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* update current_frame */
|
/* update current_frame */
|
||||||
*image_key = new_image_key;
|
current_frame.image_key = new_image_key;
|
||||||
*width = frame.get_width();
|
current_frame.width = frame.get_width();
|
||||||
*height = frame.get_height();
|
current_frame.height = frame.get_height();
|
||||||
|
|
||||||
let image_data = if frame.is_gl_texture() && self.player_id.is_some() {
|
let image_data = if frame.is_gl_texture() && self.player_id.is_some() {
|
||||||
let texture_target = if frame.is_external_oes() {
|
let texture_target = if frame.is_external_oes() {
|
||||||
|
@ -264,7 +269,12 @@ impl VideoFrameRenderer for MediaFrameRenderer {
|
||||||
let Some(image_key) = self.compositor_api.generate_image_key() else {
|
let Some(image_key) = self.compositor_api.generate_image_key() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.current_frame = Some((image_key, frame.get_width(), frame.get_height()));
|
|
||||||
|
self.current_frame = Some(MediaFrame {
|
||||||
|
image_key,
|
||||||
|
width: frame.get_width(),
|
||||||
|
height: frame.get_height(),
|
||||||
|
});
|
||||||
|
|
||||||
let image_data = if frame.is_gl_texture() && self.player_id.is_some() {
|
let image_data = if frame.is_gl_texture() && self.player_id.is_some() {
|
||||||
let texture_target = if frame.is_external_oes() {
|
let texture_target = if frame.is_external_oes() {
|
||||||
|
@ -1320,6 +1330,7 @@ impl HTMLMediaElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6.
|
// Step 6.
|
||||||
|
self.handle_resize(Some(image.width), Some(image.height));
|
||||||
self.video_renderer
|
self.video_renderer
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -1584,6 +1595,10 @@ impl HTMLMediaElement {
|
||||||
},
|
},
|
||||||
PlayerEvent::VideoFrameUpdated => {
|
PlayerEvent::VideoFrameUpdated => {
|
||||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||||
|
// Check if the frame was resized
|
||||||
|
if let Some(frame) = self.video_renderer.lock().unwrap().current_frame {
|
||||||
|
self.handle_resize(Some(frame.width as u32), Some(frame.height as u32));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
PlayerEvent::MetadataUpdated(ref metadata) => {
|
PlayerEvent::MetadataUpdated(ref metadata) => {
|
||||||
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
||||||
|
@ -1727,18 +1742,7 @@ impl HTMLMediaElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5.
|
// Step 5.
|
||||||
if self.is::<HTMLVideoElement>() {
|
self.handle_resize(Some(metadata.width), Some(metadata.height));
|
||||||
let video_elem = self.downcast::<HTMLVideoElement>().unwrap();
|
|
||||||
if video_elem.get_video_width() != metadata.width ||
|
|
||||||
video_elem.get_video_height() != metadata.height
|
|
||||||
{
|
|
||||||
video_elem.set_video_width(metadata.width);
|
|
||||||
video_elem.set_video_height(metadata.height);
|
|
||||||
let window = window_from_node(self);
|
|
||||||
let task_source = window.task_manager().media_element_task_source();
|
|
||||||
task_source.queue_simple_event(self.upcast(), atom!("resize"), &window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6.
|
// Step 6.
|
||||||
self.change_ready_state(ReadyState::HaveMetadata);
|
self.change_ready_state(ReadyState::HaveMetadata);
|
||||||
|
@ -1971,6 +1975,21 @@ impl HTMLMediaElement {
|
||||||
.map(|holder| holder.get_frame())
|
.map(|holder| holder.get_frame())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_current_frame_data(&self) -> Option<MediaFrame> {
|
||||||
|
self.video_renderer.lock().unwrap().current_frame
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_current_frame_data(&self) {
|
||||||
|
self.handle_resize(None, None);
|
||||||
|
self.video_renderer.lock().unwrap().current_frame = None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_resize(&self, width: Option<u32>, height: Option<u32>) {
|
||||||
|
if let Some(video_elem) = self.downcast::<HTMLVideoElement>() {
|
||||||
|
video_elem.resize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// By default the audio is rendered through the audio sink automatically
|
/// By default the audio is rendered through the audio sink automatically
|
||||||
/// selected by the servo-media Player instance. However, in some cases, like
|
/// selected by the servo-media Player instance. However, in some cases, like
|
||||||
/// the WebAudio MediaElementAudioSourceNode, we need to set a custom audio
|
/// the WebAudio MediaElementAudioSourceNode, we need to set a custom audio
|
||||||
|
@ -1998,13 +2017,11 @@ impl HTMLMediaElement {
|
||||||
self.duration.set(duration);
|
self.duration.set(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a new value for the show_poster propperty. If the poster is being hidden
|
/// Sets a new value for the show_poster propperty. Updates video_rederer
|
||||||
/// because new frames should render, updates video_renderer to allow it.
|
/// with the new value.
|
||||||
fn set_show_poster(&self, show_poster: bool) {
|
pub fn set_show_poster(&self, show_poster: bool) {
|
||||||
self.show_poster.set(show_poster);
|
self.show_poster.set(show_poster);
|
||||||
if !show_poster {
|
self.video_renderer.lock().unwrap().show_poster = show_poster;
|
||||||
self.video_renderer.lock().unwrap().show_poster = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&self) {
|
pub fn reset(&self) {
|
||||||
|
@ -2478,6 +2495,7 @@ impl VirtualMethods for HTMLMediaElement {
|
||||||
},
|
},
|
||||||
local_name!("src") => {
|
local_name!("src") => {
|
||||||
if mutation.new_value(attr).is_none() {
|
if mutation.new_value(attr).is_none() {
|
||||||
|
self.clear_current_frame_data();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.media_element_load_algorithm(CanGc::note());
|
self.media_element_load_algorithm(CanGc::note());
|
||||||
|
@ -2508,23 +2526,6 @@ impl VirtualMethods for HTMLMediaElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait LayoutHTMLMediaElementHelpers {
|
|
||||||
fn data(self) -> HTMLMediaData;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutHTMLMediaElementHelpers for LayoutDom<'_, HTMLMediaElement> {
|
|
||||||
fn data(self) -> HTMLMediaData {
|
|
||||||
HTMLMediaData {
|
|
||||||
current_frame: self
|
|
||||||
.unsafe_get()
|
|
||||||
.video_renderer
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.current_frame,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(JSTraceable, MallocSizeOf)]
|
#[derive(JSTraceable, MallocSizeOf)]
|
||||||
pub enum MediaElementMicrotask {
|
pub enum MediaElementMicrotask {
|
||||||
ResourceSelection {
|
ResourceSelection {
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use dom_struct::dom_struct;
|
use dom_struct::dom_struct;
|
||||||
use euclid::default::Size2D;
|
use euclid::default::Size2D;
|
||||||
use html5ever::{local_name, LocalName, Prefix};
|
use html5ever::{local_name, namespace_url, ns, LocalName, Prefix};
|
||||||
use ipc_channel::ipc;
|
use ipc_channel::ipc;
|
||||||
use js::rust::HandleObject;
|
use js::rust::HandleObject;
|
||||||
use net_traits::image_cache::{
|
use net_traits::image_cache::{
|
||||||
|
@ -19,8 +19,10 @@ use net_traits::{
|
||||||
FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ResourceFetchTiming,
|
FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ResourceFetchTiming,
|
||||||
ResourceTimingType,
|
ResourceTimingType,
|
||||||
};
|
};
|
||||||
|
use script_layout_interface::{HTMLMediaData, MediaMetadata};
|
||||||
use servo_media::player::video::VideoFrame;
|
use servo_media::player::video::VideoFrame;
|
||||||
use servo_url::ServoUrl;
|
use servo_url::ServoUrl;
|
||||||
|
use style::attr::{AttrValue, LengthOrPercentageOrAuto};
|
||||||
|
|
||||||
use crate::document_loader::{LoadBlocker, LoadType};
|
use crate::document_loader::{LoadBlocker, LoadType};
|
||||||
use crate::dom::attr::Attr;
|
use crate::dom::attr::Attr;
|
||||||
|
@ -29,10 +31,10 @@ use crate::dom::bindings::codegen::Bindings::HTMLVideoElementBinding::HTMLVideoE
|
||||||
use crate::dom::bindings::inheritance::Castable;
|
use crate::dom::bindings::inheritance::Castable;
|
||||||
use crate::dom::bindings::refcounted::Trusted;
|
use crate::dom::bindings::refcounted::Trusted;
|
||||||
use crate::dom::bindings::reflector::DomObject;
|
use crate::dom::bindings::reflector::DomObject;
|
||||||
use crate::dom::bindings::root::DomRoot;
|
use crate::dom::bindings::root::{DomRoot, LayoutDom};
|
||||||
use crate::dom::bindings::str::DOMString;
|
use crate::dom::bindings::str::DOMString;
|
||||||
use crate::dom::document::Document;
|
use crate::dom::document::Document;
|
||||||
use crate::dom::element::{AttributeMutation, Element};
|
use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers};
|
||||||
use crate::dom::globalscope::GlobalScope;
|
use crate::dom::globalscope::GlobalScope;
|
||||||
use crate::dom::htmlmediaelement::{HTMLMediaElement, ReadyState};
|
use crate::dom::htmlmediaelement::{HTMLMediaElement, ReadyState};
|
||||||
use crate::dom::node::{document_from_node, window_from_node, Node};
|
use crate::dom::node::{document_from_node, window_from_node, Node};
|
||||||
|
@ -43,16 +45,13 @@ use crate::image_listener::{generate_cache_listener_for_element, ImageCacheListe
|
||||||
use crate::network_listener::{self, PreInvoke, ResourceTimingListener};
|
use crate::network_listener::{self, PreInvoke, ResourceTimingListener};
|
||||||
use crate::script_runtime::CanGc;
|
use crate::script_runtime::CanGc;
|
||||||
|
|
||||||
const DEFAULT_WIDTH: u32 = 300;
|
|
||||||
const DEFAULT_HEIGHT: u32 = 150;
|
|
||||||
|
|
||||||
#[dom_struct]
|
#[dom_struct]
|
||||||
pub struct HTMLVideoElement {
|
pub struct HTMLVideoElement {
|
||||||
htmlmediaelement: HTMLMediaElement,
|
htmlmediaelement: HTMLMediaElement,
|
||||||
/// <https://html.spec.whatwg.org/multipage/#dom-video-videowidth>
|
/// <https://html.spec.whatwg.org/multipage/#dom-video-videowidth>
|
||||||
video_width: Cell<u32>,
|
video_width: Cell<Option<u32>>,
|
||||||
/// <https://html.spec.whatwg.org/multipage/#dom-video-videoheight>
|
/// <https://html.spec.whatwg.org/multipage/#dom-video-videoheight>
|
||||||
video_height: Cell<u32>,
|
video_height: Cell<Option<u32>>,
|
||||||
/// Incremented whenever tasks associated with this element are cancelled.
|
/// Incremented whenever tasks associated with this element are cancelled.
|
||||||
generation_id: Cell<u32>,
|
generation_id: Cell<u32>,
|
||||||
/// Poster frame fetch request canceller.
|
/// Poster frame fetch request canceller.
|
||||||
|
@ -64,6 +63,8 @@ pub struct HTMLVideoElement {
|
||||||
#[ignore_malloc_size_of = "VideoFrame"]
|
#[ignore_malloc_size_of = "VideoFrame"]
|
||||||
#[no_trace]
|
#[no_trace]
|
||||||
last_frame: DomRefCell<Option<VideoFrame>>,
|
last_frame: DomRefCell<Option<VideoFrame>>,
|
||||||
|
/// Indicates if it has already sent a resize event for a given size
|
||||||
|
sent_resize: Cell<Option<(u32, u32)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HTMLVideoElement {
|
impl HTMLVideoElement {
|
||||||
|
@ -74,12 +75,13 @@ impl HTMLVideoElement {
|
||||||
) -> HTMLVideoElement {
|
) -> HTMLVideoElement {
|
||||||
HTMLVideoElement {
|
HTMLVideoElement {
|
||||||
htmlmediaelement: HTMLMediaElement::new_inherited(local_name, prefix, document),
|
htmlmediaelement: HTMLMediaElement::new_inherited(local_name, prefix, document),
|
||||||
video_width: Cell::new(DEFAULT_WIDTH),
|
video_width: Cell::new(None),
|
||||||
video_height: Cell::new(DEFAULT_HEIGHT),
|
video_height: Cell::new(None),
|
||||||
generation_id: Cell::new(0),
|
generation_id: Cell::new(0),
|
||||||
poster_frame_canceller: DomRefCell::new(Default::default()),
|
poster_frame_canceller: DomRefCell::new(Default::default()),
|
||||||
load_blocker: Default::default(),
|
load_blocker: Default::default(),
|
||||||
last_frame: Default::default(),
|
last_frame: Default::default(),
|
||||||
|
sent_resize: Cell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,20 +103,36 @@ impl HTMLVideoElement {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_video_width(&self) -> u32 {
|
pub fn get_video_width(&self) -> Option<u32> {
|
||||||
self.video_width.get()
|
self.video_width.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_video_width(&self, width: u32) {
|
pub fn get_video_height(&self) -> Option<u32> {
|
||||||
self.video_width.set(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_video_height(&self) -> u32 {
|
|
||||||
self.video_height.get()
|
self.video_height.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_video_height(&self, height: u32) {
|
/// <https://html.spec.whatwg.org/multipage#event-media-resize>
|
||||||
|
pub fn resize(&self, width: Option<u32>, height: Option<u32>) -> Option<(u32, u32)> {
|
||||||
|
self.video_width.set(width);
|
||||||
self.video_height.set(height);
|
self.video_height.set(height);
|
||||||
|
|
||||||
|
let width = width?;
|
||||||
|
let height = height?;
|
||||||
|
if self.sent_resize.get() == Some((width, height)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sent_resize = if self.htmlmediaelement.get_ready_state() == ReadyState::HaveNothing {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let window = window_from_node(self);
|
||||||
|
let task_source = window.task_manager().media_element_task_source();
|
||||||
|
task_source.queue_simple_event(self.upcast(), atom!("resize"), &window);
|
||||||
|
Some((width, height))
|
||||||
|
};
|
||||||
|
|
||||||
|
self.sent_resize.set(sent_resize);
|
||||||
|
sent_resize
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_frame_data(&self) -> Option<(Option<ipc::IpcSharedMemory>, Size2D<u32>)> {
|
pub fn get_current_frame_data(&self) -> Option<(Option<ipc::IpcSharedMemory>, Size2D<u32>)> {
|
||||||
|
@ -228,7 +246,7 @@ impl HTMLVideoElementMethods for HTMLVideoElement {
|
||||||
if self.htmlmediaelement.get_ready_state() == ReadyState::HaveNothing {
|
if self.htmlmediaelement.get_ready_state() == ReadyState::HaveNothing {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
self.video_width.get()
|
self.video_width.get().unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-video-videoheight
|
// https://html.spec.whatwg.org/multipage/#dom-video-videoheight
|
||||||
|
@ -236,7 +254,7 @@ impl HTMLVideoElementMethods for HTMLVideoElement {
|
||||||
if self.htmlmediaelement.get_ready_state() == ReadyState::HaveNothing {
|
if self.htmlmediaelement.get_ready_state() == ReadyState::HaveNothing {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
self.video_height.get()
|
self.video_height.get().unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-video-poster
|
// https://html.spec.whatwg.org/multipage/#dom-video-poster
|
||||||
|
@ -258,10 +276,25 @@ impl VirtualMethods for HTMLVideoElement {
|
||||||
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
|
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
|
||||||
self.super_type().unwrap().attribute_mutated(attr, mutation);
|
self.super_type().unwrap().attribute_mutated(attr, mutation);
|
||||||
|
|
||||||
if let Some(new_value) = mutation.new_value(attr) {
|
if attr.local_name() == &local_name!("poster") {
|
||||||
if attr.local_name() == &local_name!("poster") {
|
if let Some(new_value) = mutation.new_value(attr) {
|
||||||
self.fetch_poster_frame(&new_value, CanGc::note());
|
self.fetch_poster_frame(&new_value, CanGc::note())
|
||||||
|
} else {
|
||||||
|
self.htmlmediaelement.clear_current_frame_data();
|
||||||
|
self.htmlmediaelement.set_show_poster(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
|
||||||
|
match name {
|
||||||
|
&local_name!("width") | &local_name!("height") => {
|
||||||
|
AttrValue::from_dimension(value.into())
|
||||||
|
},
|
||||||
|
_ => self
|
||||||
|
.super_type()
|
||||||
|
.unwrap()
|
||||||
|
.parse_plain_attribute(name, value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -408,3 +441,55 @@ impl PosterFrameFetchContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait LayoutHTMLVideoElementHelpers {
|
||||||
|
fn data(self) -> HTMLMediaData;
|
||||||
|
fn get_width(self) -> LengthOrPercentageOrAuto;
|
||||||
|
fn get_height(self) -> LengthOrPercentageOrAuto;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutDom<'_, HTMLVideoElement> {
|
||||||
|
fn width_attr(self) -> Option<LengthOrPercentageOrAuto> {
|
||||||
|
self.upcast::<Element>()
|
||||||
|
.get_attr_for_layout(&ns!(), &local_name!("width"))
|
||||||
|
.map(AttrValue::as_dimension)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height_attr(self) -> Option<LengthOrPercentageOrAuto> {
|
||||||
|
self.upcast::<Element>()
|
||||||
|
.get_attr_for_layout(&ns!(), &local_name!("height"))
|
||||||
|
.map(AttrValue::as_dimension)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutHTMLVideoElementHelpers for LayoutDom<'_, HTMLVideoElement> {
|
||||||
|
fn data(self) -> HTMLMediaData {
|
||||||
|
let video = self.unsafe_get();
|
||||||
|
|
||||||
|
// Get the current frame being rendered.
|
||||||
|
let current_frame = video.htmlmediaelement.get_current_frame_data();
|
||||||
|
|
||||||
|
// This value represents the natural width and height of the video.
|
||||||
|
// It may exist even if there is no current frame (for example, after the
|
||||||
|
// metadata of the video is loaded).
|
||||||
|
let metadata = video
|
||||||
|
.get_video_width()
|
||||||
|
.zip(video.get_video_height())
|
||||||
|
.map(|(width, height)| MediaMetadata { width, height });
|
||||||
|
|
||||||
|
HTMLMediaData {
|
||||||
|
current_frame,
|
||||||
|
metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_width(self) -> LengthOrPercentageOrAuto {
|
||||||
|
self.width_attr().unwrap_or(LengthOrPercentageOrAuto::Auto)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_height(self) -> LengthOrPercentageOrAuto {
|
||||||
|
self.height_attr().unwrap_or(LengthOrPercentageOrAuto::Auto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -88,9 +88,9 @@ use crate::dom::htmliframeelement::{HTMLIFrameElement, HTMLIFrameElementLayoutMe
|
||||||
use crate::dom::htmlimageelement::{HTMLImageElement, LayoutHTMLImageElementHelpers};
|
use crate::dom::htmlimageelement::{HTMLImageElement, LayoutHTMLImageElementHelpers};
|
||||||
use crate::dom::htmlinputelement::{HTMLInputElement, LayoutHTMLInputElementHelpers};
|
use crate::dom::htmlinputelement::{HTMLInputElement, LayoutHTMLInputElementHelpers};
|
||||||
use crate::dom::htmllinkelement::HTMLLinkElement;
|
use crate::dom::htmllinkelement::HTMLLinkElement;
|
||||||
use crate::dom::htmlmediaelement::{HTMLMediaElement, LayoutHTMLMediaElementHelpers};
|
|
||||||
use crate::dom::htmlstyleelement::HTMLStyleElement;
|
use crate::dom::htmlstyleelement::HTMLStyleElement;
|
||||||
use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers};
|
use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers};
|
||||||
|
use crate::dom::htmlvideoelement::{HTMLVideoElement, LayoutHTMLVideoElementHelpers};
|
||||||
use crate::dom::mouseevent::MouseEvent;
|
use crate::dom::mouseevent::MouseEvent;
|
||||||
use crate::dom::mutationobserver::{Mutation, MutationObserver, RegisteredObserver};
|
use crate::dom::mutationobserver::{Mutation, MutationObserver, RegisteredObserver};
|
||||||
use crate::dom::nodelist::NodeList;
|
use crate::dom::nodelist::NodeList;
|
||||||
|
@ -1553,7 +1553,7 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn media_data(self) -> Option<HTMLMediaData> {
|
fn media_data(self) -> Option<HTMLMediaData> {
|
||||||
self.downcast::<HTMLMediaElement>()
|
self.downcast::<HTMLVideoElement>()
|
||||||
.map(|media| media.data())
|
.map(|media| media.data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -163,8 +163,21 @@ pub struct PendingImage {
|
||||||
pub origin: ImmutableOrigin,
|
pub origin: ImmutableOrigin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct MediaFrame {
|
||||||
|
pub image_key: webrender_api::ImageKey,
|
||||||
|
pub width: i32,
|
||||||
|
pub height: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MediaMetadata {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct HTMLMediaData {
|
pub struct HTMLMediaData {
|
||||||
pub current_frame: Option<(ImageKey, i32, i32)>,
|
pub current_frame: Option<MediaFrame>,
|
||||||
|
pub metadata: Option<MediaMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LayoutConfig {
|
pub struct LayoutConfig {
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[element-replacement-on-replaced-element.tentative.html]
|
|
||||||
expected: FAIL
|
|
|
@ -1,2 +0,0 @@
|
||||||
[overflow-video.html]
|
|
||||||
expected: FAIL
|
|
|
@ -1,16 +1,10 @@
|
||||||
[intrinsic-size-fallback-replaced.html]
|
[intrinsic-size-fallback-replaced.html]
|
||||||
[.test 2]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[.test 3: undefined]
|
[.test 3: undefined]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[.test 4]
|
[.test 4]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[.test 6]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[.test 7: undefined]
|
[.test 7: undefined]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -227,120 +227,6 @@
|
||||||
[<marquee height="0px"> mapping to <marquee> height property]
|
[<marquee height="0px"> mapping to <marquee> height property]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[<video width="200"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="1007"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width=" 00523 "> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200.25"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200.7"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200."> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200in"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200.25in"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200 %"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200 abc"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200%"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200%abc"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200.25%"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="200.%"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="20.25e2"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="20.25E2"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="0"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="0%"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video width="0px"> mapping to <video> width property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="1007"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height=" 00523 "> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200.25"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200.7"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200."> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200in"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200.25in"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200 %"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200 abc"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200%"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200%abc"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200.25%"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="200.%"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="20.25e2"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="20.25E2"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="0"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="0%"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<video height="0px"> mapping to <video> height property]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[<object width="200"> mapping to <object> width property]
|
[<object width="200"> mapping to <object> width property]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[img-aspect-ratio-lazy.html]
|
|
||||||
[Image width and height attributes are used to infer aspect-ratio for lazy-loaded images]
|
|
||||||
expected: FAIL
|
|
|
@ -2,50 +2,20 @@
|
||||||
[Computed style]
|
[Computed style]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Create, append and test immediately: <img> with attributes width=250, height=100]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Create, append and test immediately: <img> with attributes width=0.8, height=0.2]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Create, append and test immediately: <img> with invalid trailing attributes width=50pp height=25xx]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: img with {"width":"10","height":"20"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: input with {"type":"image","width":"10","height":"20"}]
|
[Computed style test: input with {"type":"image","width":"10","height":"20"}]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Computed style test: img with {"width":"0","height":"1"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: input with {"type":"image","width":"0","height":"1"}]
|
[Computed style test: input with {"type":"image","width":"0","height":"1"}]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Computed style test: img with {"width":"1","height":"0"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: input with {"type":"image","width":"1","height":"0"}]
|
[Computed style test: input with {"type":"image","width":"1","height":"0"}]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Computed style test: img with {"width":"0","height":"0"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: input with {"type":"image","width":"0","height":"0"}]
|
[Computed style test: input with {"type":"image","width":"0","height":"0"}]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Computed style test: img with {"width":"0.5","height":"1.5"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: input with {"type":"image","width":"0.5","height":"1.5"}]
|
[Computed style test: input with {"type":"image","width":"0.5","height":"1.5"}]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Loaded images test: <img> with width, height and empty src attributes]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Loaded images test: Error image with width and height attributes]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Loaded images test: Error image with width, height and alt attributes]
|
[Loaded images test: Error image with width, height and alt attributes]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
|
@ -8,15 +8,9 @@
|
||||||
[Source width/height should take precedence over img attributes.]
|
[Source width/height should take precedence over img attributes.]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Make sure style gets invalidated correctly when the source gets removed.]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[If the <source> has only one of width/height, we don't get an aspect ratio, even if the <img> has both.]
|
[If the <source> has only one of width/height, we don't get an aspect ratio, even if the <img> has both.]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[If we don't have width/height on the source, we fall back to width/height on the <img>.]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[If we only have one width/height attribute, we should get that attribute mapped but no aspect ratio, even if <img> has attributes.]
|
[If we only have one width/height attribute, we should get that attribute mapped but no aspect ratio, even if <img> has attributes.]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
[video-aspect-ratio.html]
|
|
||||||
[Video width and height attributes are not used to infer aspect-ratio]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: video with {"width":"10","height":"20"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: video with {"width":"0.5","height":"1.5"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: video with {"width":"0","height":"1"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: video with {"width":"1","height":"0"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Computed style test: video with {"width":"0","height":"0"}]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Video width and height attributes are used to infer aspect-ratio]
|
|
||||||
expected: FAIL
|
|
|
@ -1,2 +0,0 @@
|
||||||
[image-loading-lazy-slow-aspect-ratio.html]
|
|
||||||
expected: FAIL
|
|
|
@ -1,15 +0,0 @@
|
||||||
[intrinsic_sizes.htm]
|
|
||||||
[default object size is 300x150]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[default height is half the width]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[default width is twice the height]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[default object size after src is removed]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[default object size after poster is removed]
|
|
||||||
expected: FAIL
|
|
|
@ -1,6 +1,3 @@
|
||||||
[resize-during-playback.html]
|
[resize-during-playback.html]
|
||||||
[webm video]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[mp4 video]
|
[mp4 video]
|
||||||
expected: PRECONDITION_FAILED
|
expected: PRECONDITION_FAILED
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[video_content_image.htm]
|
|
||||||
expected: FAIL
|
|
|
@ -1,2 +0,0 @@
|
||||||
[video_content_text.htm]
|
|
||||||
expected: FAIL
|
|
Loading…
Add table
Add a link
Reference in a new issue