diff options
author | sreeise <reeisesean@gmail.com> | 2019-07-12 06:31:27 -0400 |
---|---|---|
committer | sreeise <reeisesean@gmail.com> | 2019-07-22 21:39:55 -0400 |
commit | dc1121949619c4d04be001220059d9b5f0e70e8e (patch) | |
tree | 61690ed4bab3f269b6137557d194a9a6dc2bfd41 | |
parent | 95b304b7861fc7d965f79cab31f24302c86afbff (diff) | |
download | servo-dc1121949619c4d04be001220059d9b5f0e70e8e.tar.gz servo-dc1121949619c4d04be001220059d9b5f0e70e8e.zip |
Media fragment parser
-rw-r--r-- | components/script/dom/audiotrack.rs | 6 | ||||
-rw-r--r-- | components/script/dom/htmlmediaelement.rs | 49 | ||||
-rw-r--r-- | components/script/dom/mediafragmentparser.rs | 355 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 1 | ||||
-rw-r--r-- | components/script/dom/videotrack.rs | 6 | ||||
-rw-r--r-- | tests/wpt/metadata/MANIFEST.json | 10 | ||||
-rw-r--r-- | tests/wpt/web-platform-tests/html/semantics/embedded-content/media-elements/media_fragment_seek.html | 48 |
7 files changed, 464 insertions, 11 deletions
diff --git a/components/script/dom/audiotrack.rs b/components/script/dom/audiotrack.rs index 9233b1d9c93..6a43eed64b2 100644 --- a/components/script/dom/audiotrack.rs +++ b/components/script/dom/audiotrack.rs @@ -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 diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index 9ba07fe0c09..4abd9d6823a 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -41,6 +41,7 @@ use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlsourceelement::HTMLSourceElement; 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; @@ -1511,8 +1512,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() { @@ -1554,8 +1567,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() { @@ -1617,7 +1640,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. { @@ -1625,16 +1648,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. }, diff --git a/components/script/dom/mediafragmentparser.rs b/components/script/dom/mediafragmentparser.rs new file mode 100644 index 00000000000..ddd3f029df4 --- /dev/null +++ b/components/script/dom/mediafragmentparser.rs @@ -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) +} diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index d20b68c92b7..8ae0047d9df 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -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; diff --git a/components/script/dom/videotrack.rs b/components/script/dom/videotrack.rs index fd337db5e8a..5dc13c09645 100644 --- a/components/script/dom/videotrack.rs +++ b/components/script/dom/videotrack.rs @@ -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 diff --git a/tests/wpt/metadata/MANIFEST.json b/tests/wpt/metadata/MANIFEST.json index ea546a88006..1264bd701bd 100644 --- a/tests/wpt/metadata/MANIFEST.json +++ b/tests/wpt/metadata/MANIFEST.json @@ -348626,6 +348626,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", @@ -626132,6 +626138,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" diff --git a/tests/wpt/web-platform-tests/html/semantics/embedded-content/media-elements/media_fragment_seek.html b/tests/wpt/web-platform-tests/html/semantics/embedded-content/media-elements/media_fragment_seek.html new file mode 100644 index 00000000000..d6f6e6c30bf --- /dev/null +++ b/tests/wpt/web-platform-tests/html/semantics/embedded-content/media-elements/media_fragment_seek.html @@ -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> + |