aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsreeise <reeisesean@gmail.com>2019-07-12 06:31:27 -0400
committersreeise <reeisesean@gmail.com>2019-07-22 21:39:55 -0400
commitdc1121949619c4d04be001220059d9b5f0e70e8e (patch)
tree61690ed4bab3f269b6137557d194a9a6dc2bfd41
parent95b304b7861fc7d965f79cab31f24302c86afbff (diff)
downloadservo-dc1121949619c4d04be001220059d9b5f0e70e8e.tar.gz
servo-dc1121949619c4d04be001220059d9b5f0e70e8e.zip
Media fragment parser
-rw-r--r--components/script/dom/audiotrack.rs6
-rw-r--r--components/script/dom/htmlmediaelement.rs49
-rw-r--r--components/script/dom/mediafragmentparser.rs355
-rw-r--r--components/script/dom/mod.rs1
-rw-r--r--components/script/dom/videotrack.rs6
-rw-r--r--tests/wpt/metadata/MANIFEST.json10
-rw-r--r--tests/wpt/web-platform-tests/html/semantics/embedded-content/media-elements/media_fragment_seek.html48
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>
+