1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-06-02 08:39:41 +00:00
hls_m3u8/src/tags/master_playlist/media.rs

798 lines
25 KiB
Rust
Raw Normal View History

2019-09-13 14:06:52 +00:00
use std::fmt;
use std::str::FromStr;
use derive_builder::Builder;
2020-02-02 12:38:11 +00:00
use shorthand::ShortHand;
2019-09-06 10:55:00 +00:00
use crate::attribute::AttributePairs;
2019-10-04 09:02:21 +00:00
use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion};
2019-09-10 09:05:20 +00:00
use crate::utils::{parse_yes_or_no, quote, tag, unquote};
2019-10-04 09:02:21 +00:00
use crate::{Error, RequiredVersion};
2019-09-06 10:55:00 +00:00
2020-03-17 14:39:07 +00:00
/// An [`ExtXMedia`] tag is an alternative rendition of a [`VariantStream`].
2019-10-05 14:08:03 +00:00
///
2020-03-17 14:39:07 +00:00
/// For example an [`ExtXMedia`] tag can be used to specify different audio
/// languages (e.g. english is the default and there also exists an
/// [`ExtXMedia`] stream with a german audio).
2019-09-22 16:00:38 +00:00
///
2020-02-14 12:05:18 +00:00
/// [`MediaPlaylist`]: crate::MediaPlaylist
2020-03-17 14:39:07 +00:00
/// [`VariantStream`]: crate::tags::VariantStream
2020-03-25 15:13:40 +00:00
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2020-02-02 12:38:11 +00:00
#[shorthand(enable(must_use, into))]
#[builder(setter(into))]
#[builder(build_fn(validate = "Self::validate"))]
pub struct ExtXMedia {
2020-03-17 14:39:07 +00:00
/// The [`MediaType`] associated with this tag.
2019-10-03 14:23:27 +00:00
///
2020-03-17 14:39:07 +00:00
/// ### Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is required.
#[shorthand(enable(skip))]
pub media_type: MediaType,
2020-02-14 12:05:18 +00:00
/// An `URI` to a [`MediaPlaylist`].
2019-10-03 14:23:27 +00:00
///
2020-03-17 14:39:07 +00:00
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXMedia;
/// use hls_m3u8::types::MediaType;
///
/// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel");
/// # assert_eq!(media.uri(), None);
///
/// media.set_uri(Some("https://www.example.com/stream1.m3u8"));
///
/// assert_eq!(
/// media.uri(),
/// Some(&"https://www.example.com/stream1.m3u8".to_string())
/// );
/// ```
///
2019-10-03 14:23:27 +00:00
/// # Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// - This field is required, if the [`ExtXMedia::media_type`] is
2019-10-03 15:01:15 +00:00
/// [`MediaType::Subtitles`].
2020-03-17 14:39:07 +00:00
/// - This field is not allowed, if the [`ExtXMedia::media_type`] is
2019-10-03 14:23:27 +00:00
/// [`MediaType::ClosedCaptions`].
///
2020-03-17 14:39:07 +00:00
/// An absent value indicates that the media data for this rendition is
/// included in the [`MediaPlaylist`] of any
/// [`VariantStream::ExtXStreamInf`] tag with the same `group_id` of
/// this [`ExtXMedia`] instance.
///
2020-02-14 12:05:18 +00:00
/// [`MediaPlaylist`]: crate::MediaPlaylist
2020-03-17 14:39:07 +00:00
/// [`VariantStream::ExtXStreamInf`]:
/// crate::tags::VariantStream::ExtXStreamInf
2020-01-25 11:26:20 +00:00
#[builder(setter(strip_option), default)]
uri: Option<String>,
2020-02-02 12:38:11 +00:00
/// The identifier that specifies the group to which the rendition
2019-10-03 15:01:15 +00:00
/// belongs.
2019-10-03 14:23:27 +00:00
///
2020-03-17 14:39:07 +00:00
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXMedia;
/// use hls_m3u8::types::MediaType;
///
/// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel");
///
/// media.set_group_id("ag2");
///
/// assert_eq!(media.group_id(), &"ag2".to_string());
/// ```
///
2019-10-03 14:23:27 +00:00
/// # Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is required.
group_id: String,
2020-02-02 12:38:11 +00:00
/// The name of the primary language used in the rendition.
2019-10-03 14:23:27 +00:00
/// The value has to conform to [`RFC5646`].
///
2020-03-17 14:39:07 +00:00
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXMedia;
/// use hls_m3u8::types::MediaType;
///
/// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel");
///
/// media.set_language(Some("en"));
///
/// assert_eq!(media.language(), Some(&"en".to_string()));
/// ```
///
2019-10-03 14:23:27 +00:00
/// # Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is optional.
2019-10-03 14:23:27 +00:00
///
/// [`RFC5646`]: https://tools.ietf.org/html/rfc5646
2019-10-06 14:39:18 +00:00
#[builder(setter(strip_option), default)]
2020-01-25 11:26:20 +00:00
language: Option<String>,
2020-02-02 12:38:11 +00:00
/// The name of a language associated with the rendition.
/// An associated language is often used in a different role, than the
2020-03-17 14:39:07 +00:00
/// language specified by the [`language`] field (e.g., written versus
2020-02-02 12:38:11 +00:00
/// spoken, or a fallback dialect).
2019-10-03 14:23:27 +00:00
///
/// # Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is optional.
2019-10-03 14:23:27 +00:00
///
/// [`language`]: #method.language
2020-01-25 11:26:20 +00:00
#[builder(setter(strip_option), default)]
assoc_language: Option<String>,
2020-02-02 12:38:11 +00:00
/// A human-readable description of the rendition.
2019-10-03 14:23:27 +00:00
///
/// # Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is required.
2019-10-03 14:23:27 +00:00
///
2020-03-17 14:39:07 +00:00
/// If the [`language`] field is present, this field should be in
2019-10-03 15:01:15 +00:00
/// that language.
2019-10-03 14:23:27 +00:00
///
/// [`language`]: #method.language
name: String,
2020-02-02 12:38:11 +00:00
/// The value of the `default` flag.
/// A value of `true` indicates, that the client should play
/// this rendition of the content in the absence of information
/// from the user indicating a different choice.
2019-10-03 14:23:27 +00:00
///
2020-03-17 14:39:07 +00:00
/// ### Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is optional, its absence indicates an implicit value
2019-10-03 15:01:15 +00:00
/// of `false`.
#[builder(default)]
2020-03-17 14:39:07 +00:00
#[shorthand(enable(skip))]
pub is_default: bool,
2020-02-02 12:38:11 +00:00
/// Whether the client may choose to play this rendition in the absence of
/// explicit user preference.
2019-10-03 14:23:27 +00:00
///
2020-03-17 14:39:07 +00:00
/// ### Note
2020-01-25 11:26:20 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is optional, its absence indicates an implicit value
2019-10-03 15:01:15 +00:00
/// of `false`.
#[builder(default)]
2020-03-17 14:39:07 +00:00
#[shorthand(enable(skip))]
pub is_autoselect: bool,
2020-02-02 12:38:11 +00:00
/// Whether the rendition contains content that is considered
/// essential to play.
2020-01-25 11:26:20 +00:00
#[builder(default)]
2020-03-17 14:39:07 +00:00
#[shorthand(enable(skip))]
pub is_forced: bool,
2020-02-21 19:41:31 +00:00
/// An [`InStreamId`] identifies a rendition within the
/// [`MediaSegment`]s in a [`MediaPlaylist`].
///
2020-03-17 14:39:07 +00:00
/// ### Note
2020-02-21 19:41:31 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is required, if the media type is
/// [`MediaType::ClosedCaptions`]. For all other media types the
/// [`InStreamId`] must not be specified!
2020-02-02 12:38:11 +00:00
///
2020-02-14 12:05:18 +00:00
/// [`MediaPlaylist`]: crate::MediaPlaylist
2020-02-21 19:41:31 +00:00
/// [`MediaSegment`]: crate::MediaSegment
2019-10-06 14:39:18 +00:00
#[builder(setter(strip_option), default)]
2020-03-17 14:39:07 +00:00
#[shorthand(enable(skip))]
pub instream_id: Option<InStreamId>,
/// The characteristics field contains one or more Uniform Type
/// Identifiers ([`UTI`]) separated by a comma.
2020-02-02 12:38:11 +00:00
/// Each [`UTI`] indicates an individual characteristic of the Rendition.
///
2020-03-17 14:39:07 +00:00
/// An `ExtXMedia` instance with [`MediaType::Subtitles`] may include the
/// following characteristics:
/// - `"public.accessibility.transcribes-spoken-dialog"`,
/// - `"public.accessibility.describes-music-and-sound"`, and
/// - `"public.easy-to-read"` (which indicates that the subtitles have
2020-02-02 12:38:11 +00:00
/// been edited for ease of reading).
///
2020-03-17 14:39:07 +00:00
/// An `ExtXMedia` instance with [`MediaType::Audio`] may include the
/// following characteristic:
/// - `"public.accessibility.describes-video"`
///
/// The characteristics field may include private UTIs.
///
/// # Note
2020-02-02 12:38:11 +00:00
///
2020-03-17 14:39:07 +00:00
/// This field is optional.
2020-02-02 12:38:11 +00:00
///
/// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI
2019-10-06 14:39:18 +00:00
#[builder(setter(strip_option), default)]
2020-01-25 11:26:20 +00:00
characteristics: Option<String>,
2020-03-17 14:39:07 +00:00
/// A count of audio channels indicating the maximum number of independent,
/// simultaneous audio channels present in any [`MediaSegment`] in the
/// rendition.
///
/// ### Note
///
/// This field is optional, but every instance of [`ExtXMedia`] with
/// [`MediaType::Audio`] should have this field. If the [`MasterPlaylist`]
/// contains two renditions with the same codec, but a different number of
/// channels, then the channels field is required.
///
/// [`MediaSegment`]: crate::MediaSegment
/// [`MasterPlaylist`]: crate::MasterPlaylist
2020-01-25 11:26:20 +00:00
#[builder(setter(strip_option), default)]
2020-03-17 14:39:07 +00:00
#[shorthand(enable(skip))]
pub channels: Option<Channels>,
}
2019-09-06 10:55:00 +00:00
impl ExtXMediaBuilder {
fn validate(&self) -> Result<(), String> {
2019-10-03 14:23:27 +00:00
// A MediaType is always required!
2019-09-13 14:06:52 +00:00
let media_type = self
.media_type
2019-09-22 18:33:40 +00:00
.ok_or_else(|| Error::missing_attribute("MEDIA-TYPE").to_string())?;
2019-09-13 14:06:52 +00:00
2019-10-03 14:23:27 +00:00
if media_type == MediaType::Subtitles && self.uri.is_none() {
return Err(Error::missing_attribute("URI").to_string());
}
if media_type == MediaType::ClosedCaptions {
2019-09-22 16:00:38 +00:00
if self.uri.is_some() {
2019-10-03 14:23:27 +00:00
return Err(Error::unexpected_attribute("URI").to_string());
}
if self.instream_id.is_none() {
return Err(Error::missing_attribute("INSTREAM-ID").to_string());
2019-09-13 14:06:52 +00:00
}
2019-09-22 18:33:40 +00:00
} else if self.instream_id.is_some() {
2020-03-17 14:39:07 +00:00
return Err(Error::custom(
"InStreamId should only be specified for an ExtXMedia tag with `MediaType::ClosedCaptions`"
).to_string());
2019-09-06 10:55:00 +00:00
}
2019-09-13 14:06:52 +00:00
2019-09-22 16:00:38 +00:00
if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) {
2019-10-12 09:38:28 +00:00
return Err(Error::custom(format!(
"If `DEFAULT` is true, `AUTOSELECT` has to be true too, Default: {:?}, Autoselect: {:?}!",
self.is_default, self.is_autoselect
))
.to_string());
2019-09-06 10:55:00 +00:00
}
2019-09-13 14:06:52 +00:00
2019-10-03 14:23:27 +00:00
if media_type != MediaType::Subtitles && self.is_forced.is_some() {
2019-09-22 18:33:40 +00:00
return Err(Error::invalid_input().to_string());
2019-09-06 10:55:00 +00:00
}
2019-09-13 14:06:52 +00:00
Ok(())
2019-09-06 10:55:00 +00:00
}
}
impl ExtXMedia {
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
2020-03-17 14:39:07 +00:00
/// Makes a new [`ExtXMedia`] tag with the associated [`MediaType`], the
/// identifier that specifies the group to which the rendition belongs
/// (group id) and a human-readable description of the rendition. If the
/// [`language`] is specified it should be in that language.
///
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXMedia;
/// use hls_m3u8::types::MediaType;
///
/// let media = ExtXMedia::new(MediaType::Video, "vg1", "1080p video stream");
/// ```
///
/// [`language`]: #method.language
2020-02-24 15:30:43 +00:00
#[must_use]
2020-02-10 12:21:48 +00:00
pub fn new<T, K>(media_type: MediaType, group_id: T, name: K) -> Self
where
T: Into<String>,
K: Into<String>,
{
2019-09-22 18:33:40 +00:00
Self {
2019-09-06 10:55:00 +00:00
media_type,
uri: None,
2020-02-10 12:21:48 +00:00
group_id: group_id.into(),
2019-09-06 10:55:00 +00:00
language: None,
assoc_language: None,
2020-02-10 12:21:48 +00:00
name: name.into(),
is_default: false,
is_autoselect: false,
is_forced: false,
2019-09-06 10:55:00 +00:00
instream_id: None,
characteristics: None,
channels: None,
}
}
2019-10-03 14:23:27 +00:00
/// Returns a builder for [`ExtXMedia`].
2020-03-17 14:39:07 +00:00
///
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXMedia;
/// use hls_m3u8::types::MediaType;
///
/// let media = ExtXMedia::builder()
/// .media_type(MediaType::Subtitles)
/// .uri("french/ed.ttml")
/// .group_id("subs")
/// .language("fra")
/// .assoc_language("fra")
/// .name("French")
/// .is_autoselect(true)
/// .is_forced(true)
/// // concat! joins multiple `&'static str`
/// .characteristics(concat!(
/// "public.accessibility.transcribes-spoken-dialog,",
/// "public.accessibility.describes-music-and-sound"
/// ))
/// .build()?;
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
/// ```
2020-02-24 15:30:43 +00:00
#[must_use]
2019-10-03 15:01:15 +00:00
pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() }
2019-09-22 08:57:28 +00:00
}
2019-09-06 10:55:00 +00:00
2020-03-17 14:39:07 +00:00
/// This tag requires either `ProtocolVersion::V1` or if there is an
/// `instream_id` it requires it's version.
2019-09-22 08:57:28 +00:00
impl RequiredVersion for ExtXMedia {
fn required_version(&self) -> ProtocolVersion {
2020-02-24 15:45:10 +00:00
self.instream_id
.map_or(ProtocolVersion::V1, |i| i.required_version())
2019-09-06 10:55:00 +00:00
}
}
impl fmt::Display for ExtXMedia {
2020-04-09 06:43:13 +00:00
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2019-09-06 10:55:00 +00:00
write!(f, "{}", Self::PREFIX)?;
write!(f, "TYPE={}", self.media_type)?;
2020-01-25 11:26:20 +00:00
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.uri {
write!(f, ",URI={}", quote(value))?;
2019-09-06 10:55:00 +00:00
}
2020-01-25 11:26:20 +00:00
2019-09-08 09:30:52 +00:00
write!(f, ",GROUP-ID={}", quote(&self.group_id))?;
2020-01-25 11:26:20 +00:00
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.language {
write!(f, ",LANGUAGE={}", quote(value))?;
2019-09-06 10:55:00 +00:00
}
2020-01-25 11:26:20 +00:00
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.assoc_language {
write!(f, ",ASSOC-LANGUAGE={}", quote(value))?;
2019-09-06 10:55:00 +00:00
}
2020-01-25 11:26:20 +00:00
2019-09-08 09:30:52 +00:00
write!(f, ",NAME={}", quote(&self.name))?;
2020-01-25 11:26:20 +00:00
if self.is_default {
2019-09-06 10:55:00 +00:00
write!(f, ",DEFAULT=YES")?;
}
2020-01-25 11:26:20 +00:00
if self.is_autoselect {
2019-09-06 10:55:00 +00:00
write!(f, ",AUTOSELECT=YES")?;
}
2020-01-25 11:26:20 +00:00
if self.is_forced {
2019-09-06 10:55:00 +00:00
write!(f, ",FORCED=YES")?;
}
2020-01-25 11:26:20 +00:00
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.instream_id {
write!(f, ",INSTREAM-ID={}", quote(value))?;
2019-09-06 10:55:00 +00:00
}
2020-01-25 11:26:20 +00:00
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.characteristics {
write!(f, ",CHARACTERISTICS={}", quote(value))?;
2019-09-06 10:55:00 +00:00
}
2020-01-25 11:26:20 +00:00
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.channels {
write!(f, ",CHANNELS={}", quote(value))?;
2019-09-06 10:55:00 +00:00
}
Ok(())
}
}
impl FromStr for ExtXMedia {
type Err = Error;
2019-09-08 10:23:33 +00:00
2019-09-10 09:05:20 +00:00
fn from_str(input: &str) -> Result<Self, Self::Err> {
let input = tag(input, Self::PREFIX)?;
2019-09-06 10:55:00 +00:00
2019-09-22 18:33:40 +00:00
let mut builder = Self::builder();
2019-09-14 09:31:16 +00:00
for (key, value) in AttributePairs::new(input) {
match key {
2019-09-06 10:55:00 +00:00
"TYPE" => {
builder.media_type(value.parse::<MediaType>()?);
2019-09-06 10:55:00 +00:00
}
"URI" => {
2019-09-08 09:30:52 +00:00
builder.uri(unquote(value));
2019-09-06 10:55:00 +00:00
}
"GROUP-ID" => {
2019-09-08 09:30:52 +00:00
builder.group_id(unquote(value));
2019-09-06 10:55:00 +00:00
}
"LANGUAGE" => {
2019-09-08 09:30:52 +00:00
builder.language(unquote(value));
2019-09-06 10:55:00 +00:00
}
"ASSOC-LANGUAGE" => {
2019-09-08 09:30:52 +00:00
builder.assoc_language(unquote(value));
2019-09-06 10:55:00 +00:00
}
"NAME" => {
2019-09-08 09:30:52 +00:00
builder.name(unquote(value));
2019-09-06 10:55:00 +00:00
}
"DEFAULT" => {
builder.is_default(parse_yes_or_no(value)?);
2019-09-06 10:55:00 +00:00
}
"AUTOSELECT" => {
builder.is_autoselect(parse_yes_or_no(value)?);
2019-09-06 10:55:00 +00:00
}
"FORCED" => {
builder.is_forced(parse_yes_or_no(value)?);
2019-09-06 10:55:00 +00:00
}
"INSTREAM-ID" => {
builder.instream_id(unquote(value).parse::<InStreamId>()?);
2019-09-06 10:55:00 +00:00
}
"CHARACTERISTICS" => {
2019-09-08 09:30:52 +00:00
builder.characteristics(unquote(value));
2019-09-06 10:55:00 +00:00
}
"CHANNELS" => {
2019-10-03 14:23:27 +00:00
builder.channels(unquote(value).parse::<Channels>()?);
2019-09-06 10:55:00 +00:00
}
_ => {
// [6.3.1. General Client Responsibilities]
2019-10-03 15:01:15 +00:00
// > ignore any attribute/value pair with an unrecognized
// AttributeName.
2019-09-06 10:55:00 +00:00
}
}
}
2019-10-03 14:23:27 +00:00
builder.build().map_err(Error::builder)
2019-09-06 10:55:00 +00:00
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
2019-09-06 10:55:00 +00:00
2020-03-23 11:00:02 +00:00
macro_rules! generate_tests {
( $( { $struct:expr, $str:expr } ),+ $(,)* ) => {
#[test]
fn test_display() {
2020-01-25 11:26:20 +00:00
$(
2020-03-23 11:00:02 +00:00
assert_eq!($struct.to_string(), $str.to_string());
)+
}
2019-09-22 18:33:40 +00:00
2020-03-23 11:00:02 +00:00
#[test]
fn test_parser() {
$(
assert_eq!($struct, $str.parse().unwrap());
)+
2020-01-25 11:26:20 +00:00
}
}
2020-03-23 11:00:02 +00:00
}
2019-09-22 18:33:40 +00:00
2020-03-23 11:00:02 +00:00
generate_tests! {
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio")
.language("eng")
.name("English")
.is_autoselect(true)
.is_default(true)
.uri("eng/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"eng/prog_index.m3u8\",",
"GROUP-ID=\"audio\",",
"LANGUAGE=\"eng\",",
"NAME=\"English\",",
"DEFAULT=YES,",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.uri("fre/prog_index.m3u8")
.group_id("audio")
.language("fre")
.name("Français")
.is_default(false)
.is_autoselect(true)
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"fre/prog_index.m3u8\",",
"GROUP-ID=\"audio\",",
"LANGUAGE=\"fre\",",
"NAME=\"Français\",",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio")
.language("sp")
.name("Espanol")
.is_autoselect(true)
.is_default(false)
.uri("sp/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"sp/prog_index.m3u8\",",
"GROUP-ID=\"audio\",",
"LANGUAGE=\"sp\",",
"NAME=\"Espanol\",",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio-lo")
.language("eng")
.name("English")
.is_autoselect(true)
.is_default(true)
.uri("englo/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"englo/prog_index.m3u8\",",
"GROUP-ID=\"audio-lo\",",
"LANGUAGE=\"eng\",",
"NAME=\"English\",",
"DEFAULT=YES,",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio-lo")
.language("fre")
.name("Français")
.is_autoselect(true)
.is_default(false)
.uri("frelo/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"frelo/prog_index.m3u8\",",
"GROUP-ID=\"audio-lo\",",
"LANGUAGE=\"fre\",",
"NAME=\"Français\",",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio-lo")
.language("es")
.name("Espanol")
.is_autoselect(true)
.is_default(false)
.uri("splo/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"splo/prog_index.m3u8\",",
"GROUP-ID=\"audio-lo\",",
"LANGUAGE=\"es\",",
"NAME=\"Espanol\",",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio-hi")
.language("eng")
.name("English")
.is_autoselect(true)
.is_default(true)
.uri("eng/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"eng/prog_index.m3u8\",",
"GROUP-ID=\"audio-hi\",",
"LANGUAGE=\"eng\",",
"NAME=\"English\",",
"DEFAULT=YES,",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio-hi")
.language("fre")
.name("Français")
.is_autoselect(true)
.is_default(false)
.uri("fre/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"fre/prog_index.m3u8\",",
"GROUP-ID=\"audio-hi\",",
"LANGUAGE=\"fre\",",
"NAME=\"Français\",",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio-hi")
.language("es")
.name("Espanol")
.is_autoselect(true)
.is_default(false)
.uri("sp/prog_index.m3u8")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"URI=\"sp/prog_index.m3u8\",",
"GROUP-ID=\"audio-hi\",",
"LANGUAGE=\"es\",",
"NAME=\"Espanol\",",
"AUTOSELECT=YES"
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio-aacl-312")
.language("en")
.name("English")
.is_autoselect(true)
.is_default(true)
.channels(Channels::new(2))
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=AUDIO,",
"GROUP-ID=\"audio-aacl-312\",",
"LANGUAGE=\"en\",",
"NAME=\"English\",",
"DEFAULT=YES,",
"AUTOSELECT=YES,",
"CHANNELS=\"2\""
)
},
{
ExtXMedia::builder()
.media_type(MediaType::Subtitles)
.uri("french/ed.ttml")
.group_id("subs")
.language("fra")
.assoc_language("fra")
.name("French")
.is_autoselect(true)
.is_forced(true)
.characteristics("public.accessibility.transcribes-spoken\
-dialog,public.accessibility.describes-music-and-sound")
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=SUBTITLES,",
"URI=\"french/ed.ttml\",",
"GROUP-ID=\"subs\",",
"LANGUAGE=\"fra\",",
"ASSOC-LANGUAGE=\"fra\",",
"NAME=\"French\",",
"AUTOSELECT=YES,",
"FORCED=YES,",
"CHARACTERISTICS=\"",
"public.accessibility.transcribes-spoken-dialog,",
"public.accessibility.describes-music-and-sound",
"\""
)
},
{
ExtXMedia::builder()
.media_type(MediaType::ClosedCaptions)
.group_id("cc")
.language("sp")
.name("CC2")
.instream_id(InStreamId::Cc2)
.is_autoselect(true)
.build()
.unwrap(),
concat!(
"#EXT-X-MEDIA:",
"TYPE=CLOSED-CAPTIONS,",
"GROUP-ID=\"cc\",",
"LANGUAGE=\"sp\",",
"NAME=\"CC2\",",
"AUTOSELECT=YES,",
"INSTREAM-ID=\"CC2\""
)
},
{
ExtXMedia::new(MediaType::Audio, "foo", "bar"),
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\""
},
2019-10-03 14:23:27 +00:00
}
#[test]
fn test_parser_error() {
assert!("".parse::<ExtXMedia>().is_err());
assert!("garbage".parse::<ExtXMedia>().is_err());
assert!(
"#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\""
.parse::<ExtXMedia>()
.is_err()
);
assert!("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1"
.parse::<ExtXMedia>()
.is_err());
assert!("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO"
.parse::<ExtXMedia>()
.is_err());
assert!("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES"
.parse::<ExtXMedia>()
.is_err());
2019-09-22 08:57:28 +00:00
}
#[test]
fn test_required_version() {
2019-09-22 18:33:40 +00:00
macro_rules! gen_required_version {
( $( $id:expr => $output:expr, )* ) => {
$(
assert_eq!(
ExtXMedia::builder()
.media_type(MediaType::ClosedCaptions)
.group_id("audio")
.name("English")
.instream_id($id)
.build()
.unwrap()
.required_version(),
$output
);
)*
}
}
gen_required_version![
InStreamId::Cc1 => ProtocolVersion::V1,
InStreamId::Cc2 => ProtocolVersion::V1,
InStreamId::Cc3 => ProtocolVersion::V1,
InStreamId::Cc4 => ProtocolVersion::V1,
InStreamId::Service1 => ProtocolVersion::V7,
];
2019-09-22 08:57:28 +00:00
assert_eq!(
2019-09-22 18:33:40 +00:00
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("audio")
.name("English")
.build()
.unwrap()
.required_version(),
2019-09-22 08:57:28 +00:00
ProtocolVersion::V1
2019-09-22 18:33:40 +00:00
);
2019-09-06 10:55:00 +00:00
}
}