mirror of
https://github.com/servo/servo.git
synced 2025-07-22 14:53:49 +01:00
Auto merge of #23774 - sreeise:media_fragment_parser, r=ferjm
Media fragment parser <!-- Please describe your changes on the following line: --> Media fragment parser for audio and video. --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: --> - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix #22366 (GitHub issue number if applicable) <!-- Either: --> - [X] There are tests for these changes OR - [ ] These changes do not require tests because ___ <!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.--> <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/23774) <!-- Reviewable:end -->
This commit is contained in:
commit
fdbb317d7a
7 changed files with 464 additions and 11 deletions
|
@ -55,6 +55,10 @@ impl AudioTrack {
|
|||
self.id.clone()
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> DOMString {
|
||||
self.kind.clone()
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled.get()
|
||||
}
|
||||
|
@ -72,7 +76,7 @@ impl AudioTrackMethods for AudioTrack {
|
|||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-audiotrack-kind
|
||||
fn Kind(&self) -> DOMString {
|
||||
self.kind.clone()
|
||||
self.kind()
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-audiotrack-label
|
||||
|
|
|
@ -44,6 +44,7 @@ use crate::dom::htmlsourceelement::HTMLSourceElement;
|
|||
use crate::dom::htmlstyleelement::HTMLStyleElement;
|
||||
use crate::dom::htmlvideoelement::HTMLVideoElement;
|
||||
use crate::dom::mediaerror::MediaError;
|
||||
use crate::dom::mediafragmentparser::MediaFragmentParser;
|
||||
use crate::dom::mediastream::MediaStream;
|
||||
use crate::dom::node::{document_from_node, window_from_node, Node, NodeDamage, UnbindContext};
|
||||
use crate::dom::performanceresourcetiming::InitiatorType;
|
||||
|
@ -1522,8 +1523,20 @@ impl HTMLMediaElement {
|
|||
self.AudioTracks().add(&audio_track);
|
||||
|
||||
// Step 4
|
||||
// https://www.w3.org/TR/media-frags/#media-fragment-syntax
|
||||
// https://github.com/servo/servo/issues/22366
|
||||
if let Some(servo_url) = self.resource_url.borrow().as_ref() {
|
||||
let fragment = MediaFragmentParser::from(servo_url);
|
||||
if let Some(id) = fragment.id() {
|
||||
if audio_track.id() == id {
|
||||
self.AudioTracks()
|
||||
.set_enabled(self.AudioTracks().len() - 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
if fragment.tracks().contains(&audio_track.kind()) {
|
||||
self.AudioTracks()
|
||||
.set_enabled(self.AudioTracks().len() - 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5. & 6,
|
||||
if self.AudioTracks().enabled_index().is_none() {
|
||||
|
@ -1565,8 +1578,18 @@ impl HTMLMediaElement {
|
|||
self.VideoTracks().add(&video_track);
|
||||
|
||||
// Step 4.
|
||||
// https://www.w3.org/TR/media-frags/#media-fragment-syntax
|
||||
// https://github.com/servo/servo/issues/22366
|
||||
if let Some(track) = self.VideoTracks().item(0) {
|
||||
if let Some(servo_url) = self.resource_url.borrow().as_ref() {
|
||||
let fragment = MediaFragmentParser::from(servo_url);
|
||||
if let Some(id) = fragment.id() {
|
||||
if track.id() == id {
|
||||
self.VideoTracks().set_selected(0, true);
|
||||
}
|
||||
} else if fragment.tracks().contains(&track.kind()) {
|
||||
self.VideoTracks().set_selected(0, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5. & 6.
|
||||
if self.VideoTracks().selected_index().is_none() {
|
||||
|
@ -1628,7 +1651,7 @@ impl HTMLMediaElement {
|
|||
self.change_ready_state(ReadyState::HaveMetadata);
|
||||
|
||||
// Step 7.
|
||||
let mut _jumped = false;
|
||||
let mut jumped = false;
|
||||
|
||||
// Step 8.
|
||||
if self.default_playback_start_position.get() > 0. {
|
||||
|
@ -1636,16 +1659,24 @@ impl HTMLMediaElement {
|
|||
self.default_playback_start_position.get(),
|
||||
/* approximate_for_speed*/ false,
|
||||
);
|
||||
_jumped = true;
|
||||
jumped = true;
|
||||
}
|
||||
|
||||
// Step 9.
|
||||
self.default_playback_start_position.set(0.);
|
||||
|
||||
// Steps 10 and 11.
|
||||
// XXX(ferjm) Implement parser for
|
||||
// https://www.w3.org/TR/media-frags/#media-fragment-syntax
|
||||
// https://github.com/servo/media/issues/156
|
||||
if let Some(servo_url) = self.resource_url.borrow().as_ref() {
|
||||
let fragment = MediaFragmentParser::from(servo_url);
|
||||
if let Some(start) = fragment.start() {
|
||||
if start > 0. && start < self.duration.get() {
|
||||
self.playback_position.set(start);
|
||||
if !jumped {
|
||||
self.seek(self.playback_position.get(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 12 & 13 are already handled by the earlier media track processing.
|
||||
|
||||
|
|
355
components/script/dom/mediafragmentparser.rs
Normal file
355
components/script/dom/mediafragmentparser.rs
Normal file
|
@ -0,0 +1,355 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::dom::bindings::str::DOMString;
|
||||
use chrono::NaiveDateTime;
|
||||
use servo_url::ServoUrl;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::VecDeque;
|
||||
use std::str::FromStr;
|
||||
use url::{form_urlencoded, Position, Url};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SpatialRegion {
|
||||
Pixel,
|
||||
Percent,
|
||||
}
|
||||
|
||||
impl FromStr for SpatialRegion {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"pixel" => Ok(SpatialRegion::Pixel),
|
||||
"percent" => Ok(SpatialRegion::Percent),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SpatialClipping {
|
||||
region: Option<SpatialRegion>,
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct MediaFragmentParser {
|
||||
id: Option<DOMString>,
|
||||
tracks: Vec<DOMString>,
|
||||
spatial: Option<SpatialClipping>,
|
||||
start: Option<f64>,
|
||||
end: Option<f64>,
|
||||
}
|
||||
|
||||
impl MediaFragmentParser {
|
||||
pub fn id(&self) -> Option<DOMString> {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
pub fn tracks(&self) -> &Vec<DOMString> {
|
||||
self.tracks.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Option<f64> {
|
||||
self.start
|
||||
}
|
||||
|
||||
// Parse an str of key value pairs, a URL, or a fragment.
|
||||
pub fn parse(input: &str) -> MediaFragmentParser {
|
||||
let mut parser = MediaFragmentParser::default();
|
||||
let (query, fragment) = split_url(input);
|
||||
let mut octets = decode_octets(query.as_bytes());
|
||||
octets.extend(decode_octets(fragment.as_bytes()));
|
||||
|
||||
if !octets.is_empty() {
|
||||
for (key, value) in octets.iter() {
|
||||
match key.as_bytes() {
|
||||
b"t" => {
|
||||
if let Ok((start, end)) = parser.parse_temporal(value) {
|
||||
parser.start = start;
|
||||
parser.end = end;
|
||||
}
|
||||
},
|
||||
b"xywh" => {
|
||||
if let Ok(spatial) = parser.parse_spatial(value) {
|
||||
parser.spatial = Some(spatial);
|
||||
}
|
||||
},
|
||||
b"id" => parser.id = Some(DOMString::from(value.as_ref())),
|
||||
b"track" => parser.tracks.push(DOMString::from(value.as_ref())),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
parser
|
||||
} else {
|
||||
if let Ok((start, end)) = parser.parse_temporal(input) {
|
||||
parser.start = start;
|
||||
parser.end = end;
|
||||
} else if let Ok(spatial) = parser.parse_spatial(input) {
|
||||
parser.spatial = Some(spatial);
|
||||
}
|
||||
parser
|
||||
}
|
||||
}
|
||||
|
||||
// Either NPT or UTC timestamp (real world clock time).
|
||||
fn parse_temporal(&self, input: &str) -> Result<(Option<f64>, Option<f64>), ()> {
|
||||
let (_, fragment) = split_prefix(input);
|
||||
|
||||
if fragment.ends_with('Z') || fragment.ends_with("Z-") {
|
||||
return self.parse_utc_timestamp(fragment);
|
||||
}
|
||||
|
||||
if fragment.starts_with(',') || !fragment.contains(',') {
|
||||
let sec = parse_hms(&fragment.replace(',', ""))?;
|
||||
if fragment.starts_with(',') {
|
||||
Ok((Some(0.), Some(sec)))
|
||||
} else {
|
||||
Ok((Some(sec), None))
|
||||
}
|
||||
} else {
|
||||
let mut iterator = fragment.split(',');
|
||||
let start = parse_hms(iterator.next().ok_or_else(|| ())?)?;
|
||||
let end = parse_hms(iterator.next().ok_or_else(|| ())?)?;
|
||||
|
||||
if iterator.next().is_some() || start >= end {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok((Some(start), Some(end)))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_utc_timestamp(&self, input: &str) -> Result<(Option<f64>, Option<f64>), ()> {
|
||||
if input.ends_with('-') || input.starts_with(',') || !input.contains('-') {
|
||||
let sec = parse_hms(
|
||||
NaiveDateTime::parse_from_str(
|
||||
&input.replace('-', "").replace(',', ""),
|
||||
"%Y%m%dT%H%M%S%.fZ",
|
||||
)
|
||||
.map_err(|_| ())?
|
||||
.time()
|
||||
.to_string()
|
||||
.as_ref(),
|
||||
)?;
|
||||
if input.starts_with(',') {
|
||||
Ok((Some(0.), Some(sec)))
|
||||
} else {
|
||||
Ok((Some(sec), None))
|
||||
}
|
||||
} else {
|
||||
let vec: Vec<&str> = input.split('-').collect();
|
||||
let mut hms: Vec<f64> = vec
|
||||
.iter()
|
||||
.map(|s| NaiveDateTime::parse_from_str(s, "%Y%m%dT%H%M%S%.fZ"))
|
||||
.flatten()
|
||||
.map(|s| parse_hms(&s.time().to_string()))
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let end = hms.pop().ok_or_else(|| ())?;
|
||||
let start = hms.pop().ok_or_else(|| ())?;
|
||||
|
||||
if !hms.is_empty() || start >= end {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok((Some(start), Some(end)))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_spatial(&self, input: &str) -> Result<SpatialClipping, ()> {
|
||||
let (prefix, s) = split_prefix(input);
|
||||
let vec: Vec<&str> = s.split(',').collect();
|
||||
let mut queue: VecDeque<u32> = vec.iter().map(|s| s.parse::<u32>()).flatten().collect();
|
||||
|
||||
let mut clipping = SpatialClipping {
|
||||
region: None,
|
||||
x: queue.pop_front().ok_or_else(|| ())?,
|
||||
y: queue.pop_front().ok_or_else(|| ())?,
|
||||
width: queue.pop_front().ok_or_else(|| ())?,
|
||||
height: queue.pop_front().ok_or_else(|| ())?,
|
||||
};
|
||||
|
||||
if !queue.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if let Some(s) = prefix {
|
||||
let region = SpatialRegion::from_str(s)?;
|
||||
if region.eq(&SpatialRegion::Percent) &&
|
||||
(clipping.x + clipping.width > 100 || clipping.y + clipping.height > 100)
|
||||
{
|
||||
return Err(());
|
||||
}
|
||||
clipping.region = Some(region);
|
||||
}
|
||||
|
||||
Ok(clipping)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Url> for MediaFragmentParser {
|
||||
fn from(url: &Url) -> Self {
|
||||
let input: &str = &url[Position::AfterPath..];
|
||||
MediaFragmentParser::parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ServoUrl> for MediaFragmentParser {
|
||||
fn from(servo_url: &ServoUrl) -> Self {
|
||||
let input: &str = &servo_url[Position::AfterPath..];
|
||||
MediaFragmentParser::parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
// 5.1.1 Processing name-value components.
|
||||
fn decode_octets(bytes: &[u8]) -> Vec<(Cow<str>, Cow<str>)> {
|
||||
form_urlencoded::parse(bytes)
|
||||
.filter(|(key, _)| match key.as_bytes() {
|
||||
b"t" | b"track" | b"id" | b"xywh" => true,
|
||||
_ => false,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Parse a full URL or a relative URL without a base retaining the query and/or fragment.
|
||||
fn split_url(s: &str) -> (DOMString, DOMString) {
|
||||
if s.contains('?') || s.contains('#') {
|
||||
let mut query = DOMString::new();
|
||||
let mut fragment = DOMString::new();
|
||||
|
||||
for (index, byte) in s.bytes().enumerate() {
|
||||
if byte == b'?' {
|
||||
let mut found = false;
|
||||
let partial = &s[index + 1..];
|
||||
for (i, byte) in partial.bytes().enumerate() {
|
||||
if byte == b'#' {
|
||||
found = true;
|
||||
query.push_str(&partial[..i]);
|
||||
fragment.push_str(&partial[i + 1..]);
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break;
|
||||
} else {
|
||||
query.push_str(partial);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if byte == b'#' {
|
||||
fragment.push_str(&s[index + 1..]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
(query, fragment)
|
||||
} else {
|
||||
(DOMString::new(), DOMString::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_byte_number(byte: u8) -> bool {
|
||||
match byte {
|
||||
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn split_prefix(s: &str) -> (Option<&str>, &str) {
|
||||
for (index, byte) in s.bytes().enumerate() {
|
||||
if index == 0 && is_byte_number(byte) {
|
||||
break;
|
||||
}
|
||||
|
||||
if byte == b':' {
|
||||
return (Some(&s[..index]), &s[index + 1..]);
|
||||
}
|
||||
}
|
||||
(None, s)
|
||||
}
|
||||
|
||||
fn hms_to_seconds(hour: u32, minutes: u32, seconds: f64) -> f64 {
|
||||
let mut sec: f64 = f64::from(hour) * 3600.;
|
||||
sec += f64::from(minutes) * 60.;
|
||||
sec += seconds;
|
||||
sec
|
||||
}
|
||||
|
||||
fn parse_npt_minute(s: &str) -> Result<u32, ()> {
|
||||
if s.len() > 2 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let minute = s.parse().map_err(|_| ())?;
|
||||
if minute > 59 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(minute)
|
||||
}
|
||||
|
||||
fn parse_npt_seconds(s: &str) -> Result<f64, ()> {
|
||||
if s.contains('.') {
|
||||
let mut iterator = s.split('.');
|
||||
if let Some(s) = iterator.next() {
|
||||
if s.len() > 2 {
|
||||
return Err(());
|
||||
}
|
||||
let sec = s.parse::<u32>().map_err(|_| ())?;
|
||||
if sec > 59 {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
let _ = iterator.next();
|
||||
if iterator.next().is_some() {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
s.parse().map_err(|_| ())
|
||||
}
|
||||
|
||||
fn parse_hms(s: &str) -> Result<f64, ()> {
|
||||
let mut vec: VecDeque<&str> = s.split(':').collect();
|
||||
vec.retain(|x| !x.eq(&""));
|
||||
|
||||
let result = match vec.len() {
|
||||
1 => {
|
||||
let secs = vec
|
||||
.pop_front()
|
||||
.ok_or_else(|| ())?
|
||||
.parse::<f64>()
|
||||
.map_err(|_| ())?;
|
||||
|
||||
if secs == 0. {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
hms_to_seconds(0, 0, secs)
|
||||
},
|
||||
2 => hms_to_seconds(
|
||||
0,
|
||||
parse_npt_minute(vec.pop_front().ok_or_else(|| ())?)?,
|
||||
parse_npt_seconds(vec.pop_front().ok_or_else(|| ())?)?,
|
||||
),
|
||||
3 => hms_to_seconds(
|
||||
vec.pop_front().ok_or_else(|| ())?.parse().map_err(|_| ())?,
|
||||
parse_npt_minute(vec.pop_front().ok_or_else(|| ())?)?,
|
||||
parse_npt_seconds(vec.pop_front().ok_or_else(|| ())?)?,
|
||||
),
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
if !vec.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
|
@ -396,6 +396,7 @@ pub mod keyboardevent;
|
|||
pub mod location;
|
||||
pub mod mediadevices;
|
||||
pub mod mediaerror;
|
||||
pub mod mediafragmentparser;
|
||||
pub mod medialist;
|
||||
pub mod mediaquerylist;
|
||||
pub mod mediaquerylistevent;
|
||||
|
|
|
@ -55,6 +55,10 @@ impl VideoTrack {
|
|||
self.id.clone()
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> DOMString {
|
||||
self.kind.clone()
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> bool {
|
||||
self.selected.get().clone()
|
||||
}
|
||||
|
@ -72,7 +76,7 @@ impl VideoTrackMethods for VideoTrack {
|
|||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-videotrack-kind
|
||||
fn Kind(&self) -> DOMString {
|
||||
self.kind.clone()
|
||||
self.kind()
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-videotrack-label
|
||||
|
|
|
@ -348967,6 +348967,12 @@
|
|||
{}
|
||||
]
|
||||
],
|
||||
"html/semantics/embedded-content/media-elements/media_fragment_seek.html": [
|
||||
[
|
||||
"html/semantics/embedded-content/media-elements/media_fragment_seek.html",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"html/semantics/embedded-content/media-elements/mime-types/canPlayType.html": [
|
||||
[
|
||||
"html/semantics/embedded-content/media-elements/mime-types/canPlayType.html",
|
||||
|
@ -626972,6 +626978,10 @@
|
|||
"cd1ebb9e492673feb095a227e7ca04ceae7643b9",
|
||||
"testharness"
|
||||
],
|
||||
"html/semantics/embedded-content/media-elements/media_fragment_seek.html": [
|
||||
"d6f6e6c30bf89cbb87c7fbab1529973aa69b03f6",
|
||||
"testharness"
|
||||
],
|
||||
"html/semantics/embedded-content/media-elements/mime-types/canPlayType.html": [
|
||||
"56edf25aa8fb93c66fbbad5bbfb2e9652e7297d0",
|
||||
"testharness"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Video should seek to time specified in media fragment syntax</title>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/common/media.js"></script>
|
||||
<div id="log"></div>
|
||||
<video id="video"></video>
|
||||
<script>
|
||||
async_test(function () {
|
||||
let video = document.getElementById("video");
|
||||
video.src = getVideoURI('/media/movie_5') + "#t=4,7";
|
||||
video.load();
|
||||
this.step_timeout(function () {
|
||||
assert_equals(video.currentTime, 4.0);
|
||||
|
||||
video.src = getVideoURI('/media/movie_5') + "#t=%6Ept:3";
|
||||
video.load();
|
||||
this.step_timeout(function () {
|
||||
assert_true(video.src.endsWith("t=%6Ept:3"));
|
||||
assert_equals(video.currentTime, 3.0);
|
||||
|
||||
video.src = getVideoURI('/media/movie_5') + "#t=00:00:01.00";
|
||||
video.load();
|
||||
this.step_timeout(function () {
|
||||
assert_true(video.src.endsWith("t=00:00:01.00"));
|
||||
assert_equals(video.currentTime, 1.0);
|
||||
|
||||
video.src = getVideoURI('/media/movie_5') + "#u=12&t=3";
|
||||
video.load();
|
||||
this.step_timeout(function () {
|
||||
assert_true(video.src.endsWith("#u=12&t=3"));
|
||||
assert_equals(video.currentTime, 3.0);
|
||||
|
||||
video.src = getVideoURI('/media/movie_5') + "#t=npt%3A3";
|
||||
video.load();
|
||||
this.step_timeout(function () {
|
||||
assert_true(video.src.endsWith("t=npt%3A3"));
|
||||
assert_equals(video.currentTime, 3.0);
|
||||
this.done();
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue