2020-04-22 07:54:48 +00:00
|
|
|
use core::convert::TryFrom;
|
2020-02-10 12:20:39 +00:00
|
|
|
use core::fmt;
|
|
|
|
use core::ops::Deref;
|
2020-04-22 07:54:48 +00:00
|
|
|
use std::borrow::Cow;
|
2020-02-10 12:20:39 +00:00
|
|
|
|
|
|
|
use crate::attribute::AttributePairs;
|
2020-02-21 19:43:12 +00:00
|
|
|
use crate::tags::ExtXMedia;
|
2020-02-10 12:20:39 +00:00
|
|
|
use crate::traits::RequiredVersion;
|
2020-02-21 19:43:12 +00:00
|
|
|
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, StreamData, UFloat};
|
2020-02-10 12:20:39 +00:00
|
|
|
use crate::utils::{quote, tag, unquote};
|
|
|
|
use crate::Error;
|
|
|
|
|
2020-02-10 12:51:37 +00:00
|
|
|
/// A server may offer multiple [`MediaPlaylist`] files to provide different
|
2020-04-09 08:50:41 +00:00
|
|
|
/// encodings of the same presentation.
|
|
|
|
///
|
|
|
|
/// If it does so, it should provide
|
2020-02-10 12:51:37 +00:00
|
|
|
/// a [`MasterPlaylist`] that lists each [`VariantStream`] to allow
|
2020-02-10 12:20:39 +00:00
|
|
|
/// clients to switch between encodings dynamically.
|
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// The server must meet the following constraints when producing
|
|
|
|
/// [`VariantStream`]s in order to allow clients to switch between them
|
|
|
|
/// seamlessly:
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// - Each [`VariantStream`] must present the same content.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// - Matching content in [`VariantStream`]s must have matching timestamps. This
|
|
|
|
/// allows clients to synchronize the media.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// - Matching content in [`VariantStream`]s must have matching
|
|
|
|
/// [`ExtXDiscontinuitySequence`].
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// - Each [`MediaPlaylist`] in each [`VariantStream`] must have the same target
|
|
|
|
/// duration. The only exceptions are subtitle renditions and
|
|
|
|
/// [`MediaPlaylist`]s containing an [`ExtXIFramesOnly`] tag, which may have
|
2020-03-25 14:57:43 +00:00
|
|
|
/// different target durations if they have [`PlaylistType::Vod`].
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// - Content that appears in a [`MediaPlaylist`] of one [`VariantStream`] but
|
|
|
|
/// not in another must appear either at the beginning or at the end of the
|
|
|
|
/// [`MediaPlaylist`] and must not be longer than the target duration.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-03-25 14:57:43 +00:00
|
|
|
/// - If any [`MediaPlaylist`]s have an [`PlaylistType`] tag, all
|
|
|
|
/// [`MediaPlaylist`]s must have an [`PlaylistType`] tag with the same value.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-03-25 14:57:43 +00:00
|
|
|
/// - If the Playlist contains an [`PlaylistType`] tag with the value of VOD,
|
|
|
|
/// the first segment of every [`MediaPlaylist`] in every [`VariantStream`]
|
|
|
|
/// must start at the same media timestamp.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// - If any [`MediaPlaylist`] in a [`MasterPlaylist`] contains an
|
|
|
|
/// [`ExtXProgramDateTime`] tag, then all [`MediaPlaylist`]s in that
|
|
|
|
/// [`MasterPlaylist`] must contain [`ExtXProgramDateTime`] tags with
|
|
|
|
/// consistent mappings of date and time to media timestamps.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// - Each [`VariantStream`] must contain the same set of Date Ranges, each one
|
|
|
|
/// identified by an [`ExtXDateRange`] tag(s) with the same ID attribute value
|
|
|
|
/// and containing the same set of attribute/value pairs.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
2020-02-10 12:51:37 +00:00
|
|
|
/// In addition, for broadest compatibility, [`VariantStream`]s should
|
|
|
|
/// contain the same encoded audio bitstream. This allows clients to
|
|
|
|
/// switch between [`VariantStream`]s without audible glitching.
|
2020-02-10 12:20:39 +00:00
|
|
|
///
|
|
|
|
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
|
2020-02-10 12:51:37 +00:00
|
|
|
/// [`ExtXDiscontinuitySequence`]: crate::tags::ExtXDiscontinuitySequence
|
2020-03-25 14:57:43 +00:00
|
|
|
/// [`PlaylistType::Vod`]: crate::types::PlaylistType::Vod
|
2020-02-10 12:51:37 +00:00
|
|
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
|
|
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
|
|
|
/// [`ExtXDateRange`]: crate::tags::ExtXDateRange
|
|
|
|
/// [`ExtXProgramDateTime`]: crate::tags::ExtXProgramDateTime
|
2020-03-25 14:57:43 +00:00
|
|
|
/// [`PlaylistType`]: crate::types::PlaylistType
|
2020-02-10 12:51:37 +00:00
|
|
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
2020-02-21 09:45:04 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
2020-04-22 07:54:48 +00:00
|
|
|
pub enum VariantStream<'a> {
|
2020-02-10 12:51:37 +00:00
|
|
|
/// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`]
|
|
|
|
/// file containing the I-frames of a multimedia presentation.
|
|
|
|
/// It stands alone, in that it does not apply to a particular URI in the
|
|
|
|
/// [`MasterPlaylist`].
|
|
|
|
///
|
|
|
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
|
|
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
2020-02-10 12:20:39 +00:00
|
|
|
ExtXIFrame {
|
|
|
|
/// The URI identifies the I-frame [`MediaPlaylist`] file.
|
|
|
|
/// That Playlist file must contain an [`ExtXIFramesOnly`] tag.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is required.
|
|
|
|
///
|
|
|
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
2020-02-10 12:51:37 +00:00
|
|
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
2020-04-22 07:54:48 +00:00
|
|
|
uri: Cow<'a, str>,
|
2020-02-10 12:20:39 +00:00
|
|
|
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
|
|
|
/// [`VariantStream::ExtXIFrame`].
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2020-04-22 07:54:48 +00:00
|
|
|
stream_data: StreamData<'a>,
|
2020-02-10 12:20:39 +00:00
|
|
|
},
|
2020-02-10 12:51:37 +00:00
|
|
|
/// [`VariantStream::ExtXStreamInf`] specifies a [`VariantStream`], which is
|
|
|
|
/// a set of renditions that can be combined to play the presentation.
|
2020-02-10 12:20:39 +00:00
|
|
|
ExtXStreamInf {
|
|
|
|
/// The URI specifies a [`MediaPlaylist`] that carries a rendition of
|
|
|
|
/// the [`VariantStream`]. Clients that do not support multiple video
|
|
|
|
/// renditions should play this rendition.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is required.
|
|
|
|
///
|
|
|
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
2020-04-22 07:54:48 +00:00
|
|
|
uri: Cow<'a, str>,
|
2020-02-10 12:20:39 +00:00
|
|
|
/// The value is an unsigned float describing the maximum frame
|
|
|
|
/// rate for all the video in the [`VariantStream`].
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// Specifying the frame rate is optional, but is recommended if the
|
|
|
|
/// [`VariantStream`] includes video. It should be specified if any
|
|
|
|
/// video exceeds 30 frames per second.
|
|
|
|
frame_rate: Option<UFloat>,
|
|
|
|
/// It indicates the set of audio renditions that should be used when
|
|
|
|
/// playing the presentation.
|
|
|
|
///
|
|
|
|
/// It must match the value of the [`ExtXMedia::group_id`] of an
|
|
|
|
/// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose
|
|
|
|
/// [`ExtXMedia::media_type`] is [`MediaType::Audio`].
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
|
|
|
///
|
|
|
|
/// [`ExtXMedia`]: crate::tags::ExtXMedia
|
|
|
|
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
|
|
|
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
|
|
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
|
|
|
/// [`MediaType::Audio`]: crate::types::MediaType::Audio
|
2020-04-22 07:54:48 +00:00
|
|
|
audio: Option<Cow<'a, str>>,
|
2020-02-10 12:20:39 +00:00
|
|
|
/// It indicates the set of subtitle renditions that can be used when
|
|
|
|
/// playing the presentation.
|
|
|
|
///
|
|
|
|
/// It must match the value of the [`ExtXMedia::group_id`] of an
|
|
|
|
/// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose
|
|
|
|
/// [`ExtXMedia::media_type`] is [`MediaType::Subtitles`].
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
|
|
|
///
|
|
|
|
/// [`ExtXMedia`]: crate::tags::ExtXMedia
|
|
|
|
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
|
|
|
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
|
|
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
|
|
|
/// [`MediaType::Subtitles`]: crate::types::MediaType::Subtitles
|
2020-04-22 07:54:48 +00:00
|
|
|
subtitles: Option<Cow<'a, str>>,
|
2020-02-10 12:20:39 +00:00
|
|
|
/// It indicates the set of closed-caption renditions that can be used
|
|
|
|
/// when playing the presentation.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2020-04-22 07:54:48 +00:00
|
|
|
closed_captions: Option<ClosedCaptions<'a>>,
|
2020-02-10 12:20:39 +00:00
|
|
|
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
|
|
|
/// [`VariantStream::ExtXIFrame`].
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2020-04-22 07:54:48 +00:00
|
|
|
stream_data: StreamData<'a>,
|
2020-02-10 12:20:39 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-04-22 07:54:48 +00:00
|
|
|
impl<'a> VariantStream<'a> {
|
2020-02-10 12:20:39 +00:00
|
|
|
pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
|
|
|
pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:";
|
2020-02-21 19:43:12 +00:00
|
|
|
|
|
|
|
/// Checks if a [`VariantStream`] and an [`ExtXMedia`] element are
|
|
|
|
/// associated.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use hls_m3u8::tags::{ExtXMedia, VariantStream};
|
|
|
|
/// use hls_m3u8::types::{ClosedCaptions, MediaType, StreamData};
|
|
|
|
///
|
|
|
|
/// let variant_stream = VariantStream::ExtXStreamInf {
|
|
|
|
/// uri: "https://www.example.com/init.bin".into(),
|
|
|
|
/// frame_rate: None,
|
|
|
|
/// audio: Some("ag1".into()),
|
|
|
|
/// subtitles: Some("sg1".into()),
|
|
|
|
/// closed_captions: Some(ClosedCaptions::group_id("cc1")),
|
|
|
|
/// stream_data: StreamData::builder()
|
|
|
|
/// .bandwidth(1_110_000)
|
|
|
|
/// .video("vg1")
|
|
|
|
/// .build()
|
|
|
|
/// .unwrap(),
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// assert!(variant_stream.is_associated(
|
|
|
|
/// &ExtXMedia::builder()
|
|
|
|
/// .media_type(MediaType::Audio)
|
|
|
|
/// .group_id("ag1")
|
|
|
|
/// .name("audio example")
|
|
|
|
/// .build()
|
|
|
|
/// .unwrap(),
|
|
|
|
/// ));
|
|
|
|
/// ```
|
2020-02-24 15:30:43 +00:00
|
|
|
#[must_use]
|
2020-04-22 07:54:48 +00:00
|
|
|
pub fn is_associated(&self, media: &ExtXMedia<'_>) -> bool {
|
2020-02-21 19:43:12 +00:00
|
|
|
match &self {
|
|
|
|
Self::ExtXIFrame { stream_data, .. } => {
|
2020-03-17 14:39:07 +00:00
|
|
|
if let MediaType::Video = media.media_type {
|
2020-02-21 19:43:12 +00:00
|
|
|
if let Some(value) = stream_data.video() {
|
|
|
|
return value == media.group_id();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|
|
|
|
Self::ExtXStreamInf {
|
|
|
|
audio,
|
|
|
|
subtitles,
|
|
|
|
closed_captions,
|
|
|
|
stream_data,
|
|
|
|
..
|
|
|
|
} => {
|
2020-03-17 14:39:07 +00:00
|
|
|
match media.media_type {
|
2020-02-21 19:43:12 +00:00
|
|
|
MediaType::Audio => audio.as_ref().map_or(false, |v| v == media.group_id()),
|
|
|
|
MediaType::Video => {
|
|
|
|
stream_data.video().map_or(false, |v| v == media.group_id())
|
|
|
|
}
|
|
|
|
MediaType::Subtitles => {
|
|
|
|
subtitles.as_ref().map_or(false, |v| v == media.group_id())
|
|
|
|
}
|
|
|
|
MediaType::ClosedCaptions => {
|
|
|
|
closed_captions
|
|
|
|
.as_ref()
|
|
|
|
.map_or(false, |v| v == media.group_id())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-22 07:54:48 +00:00
|
|
|
|
|
|
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
|
|
|
/// internal [`Cow`]s.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This is a relatively expensive operation.
|
|
|
|
#[must_use]
|
|
|
|
pub fn into_owned(self) -> VariantStream<'static> {
|
|
|
|
match self {
|
|
|
|
VariantStream::ExtXIFrame { uri, stream_data } => {
|
|
|
|
VariantStream::ExtXIFrame {
|
|
|
|
uri: Cow::Owned(uri.into_owned()),
|
|
|
|
stream_data: stream_data.into_owned(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VariantStream::ExtXStreamInf {
|
|
|
|
uri,
|
|
|
|
frame_rate,
|
|
|
|
audio,
|
|
|
|
subtitles,
|
|
|
|
closed_captions,
|
|
|
|
stream_data,
|
|
|
|
} => {
|
|
|
|
VariantStream::ExtXStreamInf {
|
|
|
|
uri: Cow::Owned(uri.into_owned()),
|
|
|
|
frame_rate,
|
|
|
|
audio: audio.map(|v| Cow::Owned(v.into_owned())),
|
|
|
|
subtitles: subtitles.map(|v| Cow::Owned(v.into_owned())),
|
2020-08-11 09:13:14 +00:00
|
|
|
closed_captions: closed_captions.map(ClosedCaptions::into_owned),
|
2020-04-22 07:54:48 +00:00
|
|
|
stream_data: stream_data.into_owned(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-02-10 12:20:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// This tag requires [`ProtocolVersion::V1`].
|
2020-04-22 07:54:48 +00:00
|
|
|
impl<'a> RequiredVersion for VariantStream<'a> {
|
2020-02-10 12:20:39 +00:00
|
|
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
2020-02-21 19:43:12 +00:00
|
|
|
|
|
|
|
fn introduced_version(&self) -> ProtocolVersion {
|
|
|
|
match &self {
|
|
|
|
Self::ExtXStreamInf {
|
|
|
|
audio,
|
|
|
|
subtitles,
|
|
|
|
stream_data,
|
|
|
|
..
|
|
|
|
} => {
|
|
|
|
if stream_data.introduced_version() >= ProtocolVersion::V4 {
|
|
|
|
stream_data.introduced_version()
|
|
|
|
} else if audio.is_some() || subtitles.is_some() {
|
|
|
|
ProtocolVersion::V4
|
|
|
|
} else {
|
|
|
|
ProtocolVersion::V1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Self::ExtXIFrame { stream_data, .. } => stream_data.introduced_version(),
|
|
|
|
}
|
|
|
|
}
|
2020-02-10 12:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-04-22 07:54:48 +00:00
|
|
|
impl<'a> fmt::Display for VariantStream<'a> {
|
2020-04-09 06:43:13 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2020-02-10 12:20:39 +00:00
|
|
|
match &self {
|
|
|
|
Self::ExtXIFrame { uri, stream_data } => {
|
|
|
|
write!(f, "{}", Self::PREFIX_EXTXIFRAME)?;
|
|
|
|
write!(f, "URI={},{}", quote(uri), stream_data)?;
|
|
|
|
}
|
|
|
|
Self::ExtXStreamInf {
|
|
|
|
uri,
|
|
|
|
frame_rate,
|
|
|
|
audio,
|
|
|
|
subtitles,
|
|
|
|
closed_captions,
|
|
|
|
stream_data,
|
|
|
|
} => {
|
|
|
|
write!(f, "{}{}", Self::PREFIX_EXTXSTREAMINF, stream_data)?;
|
|
|
|
|
|
|
|
if let Some(value) = frame_rate {
|
|
|
|
write!(f, ",FRAME-RATE={:.3}", value.as_f32())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(value) = audio {
|
|
|
|
write!(f, ",AUDIO={}", quote(value))?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(value) = subtitles {
|
|
|
|
write!(f, ",SUBTITLES={}", quote(value))?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(value) = closed_captions {
|
|
|
|
write!(f, ",CLOSED-CAPTIONS={}", value)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
write!(f, "\n{}", uri)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-22 07:54:48 +00:00
|
|
|
impl<'a> TryFrom<&'a str> for VariantStream<'a> {
|
|
|
|
type Error = Error;
|
2020-02-10 12:20:39 +00:00
|
|
|
|
2020-04-22 07:54:48 +00:00
|
|
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) {
|
|
|
|
let uri = AttributePairs::new(input)
|
2021-10-01 11:32:36 +00:00
|
|
|
.find_map(|(key, value)| (key == "URI").then(|| unquote(value)))
|
2020-02-10 12:20:39 +00:00
|
|
|
.ok_or_else(|| Error::missing_value("URI"))?;
|
|
|
|
|
|
|
|
Ok(Self::ExtXIFrame {
|
|
|
|
uri,
|
2020-04-22 07:54:48 +00:00
|
|
|
stream_data: StreamData::try_from(input)?,
|
2020-02-10 12:20:39 +00:00
|
|
|
})
|
|
|
|
} else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) {
|
|
|
|
let mut lines = input.lines();
|
|
|
|
let first_line = lines
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| Error::missing_value("first_line"))?;
|
|
|
|
let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?;
|
|
|
|
|
|
|
|
let mut frame_rate = None;
|
|
|
|
let mut audio = None;
|
|
|
|
let mut subtitles = None;
|
|
|
|
let mut closed_captions = None;
|
|
|
|
|
|
|
|
for (key, value) in AttributePairs::new(first_line) {
|
|
|
|
match key {
|
|
|
|
"FRAME-RATE" => frame_rate = Some(value.parse()?),
|
|
|
|
"AUDIO" => audio = Some(unquote(value)),
|
|
|
|
"SUBTITLES" => subtitles = Some(unquote(value)),
|
2020-04-22 07:54:48 +00:00
|
|
|
"CLOSED-CAPTIONS" => {
|
2021-10-01 11:04:57 +00:00
|
|
|
closed_captions = Some(ClosedCaptions::try_from(value).unwrap());
|
2020-04-22 07:54:48 +00:00
|
|
|
}
|
2020-02-10 12:20:39 +00:00
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Self::ExtXStreamInf {
|
2020-04-22 07:54:48 +00:00
|
|
|
uri: Cow::Borrowed(uri),
|
2020-02-10 12:20:39 +00:00
|
|
|
frame_rate,
|
|
|
|
audio,
|
|
|
|
subtitles,
|
|
|
|
closed_captions,
|
2020-04-22 07:54:48 +00:00
|
|
|
stream_data: StreamData::try_from(first_line)?,
|
2020-02-10 12:20:39 +00:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// TODO: custom error type? + attach input data
|
|
|
|
Err(Error::custom(format!(
|
|
|
|
"invalid start of input, expected either {:?} or {:?}",
|
|
|
|
Self::PREFIX_EXTXIFRAME,
|
|
|
|
Self::PREFIX_EXTXSTREAMINF
|
|
|
|
)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-22 07:54:48 +00:00
|
|
|
impl<'a> Deref for VariantStream<'a> {
|
|
|
|
type Target = StreamData<'a>;
|
2020-02-10 12:20:39 +00:00
|
|
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
match &self {
|
|
|
|
Self::ExtXIFrame { stream_data, .. } | Self::ExtXStreamInf { stream_data, .. } => {
|
|
|
|
stream_data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-02-21 19:43:12 +00:00
|
|
|
|
2020-04-22 07:54:48 +00:00
|
|
|
impl<'a> PartialEq<&VariantStream<'a>> for VariantStream<'a> {
|
2020-03-17 14:54:53 +00:00
|
|
|
fn eq(&self, other: &&Self) -> bool { self.eq(*other) }
|
|
|
|
}
|
|
|
|
|
2020-02-21 19:43:12 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::types::InStreamId;
|
2020-04-22 07:54:48 +00:00
|
|
|
use pretty_assertions::assert_eq;
|
2020-02-21 19:43:12 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_required_version() {
|
|
|
|
assert_eq!(
|
|
|
|
VariantStream::ExtXStreamInf {
|
|
|
|
uri: "https://www.example.com/init.bin".into(),
|
|
|
|
frame_rate: None,
|
|
|
|
audio: None,
|
|
|
|
subtitles: None,
|
|
|
|
closed_captions: None,
|
|
|
|
stream_data: StreamData::new(1_110_000)
|
|
|
|
}
|
|
|
|
.required_version(),
|
|
|
|
ProtocolVersion::V1
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_is_associated() {
|
|
|
|
let mut variant_stream = VariantStream::ExtXStreamInf {
|
|
|
|
uri: "https://www.example.com/init.bin".into(),
|
|
|
|
frame_rate: None,
|
|
|
|
audio: Some("ag1".into()),
|
|
|
|
subtitles: Some("sg1".into()),
|
|
|
|
closed_captions: Some(ClosedCaptions::group_id("cc1")),
|
|
|
|
stream_data: StreamData::builder()
|
|
|
|
.bandwidth(1_110_000)
|
|
|
|
.video("vg1")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
};
|
|
|
|
|
|
|
|
assert!(variant_stream.is_associated(
|
|
|
|
&ExtXMedia::builder()
|
|
|
|
.media_type(MediaType::Audio)
|
|
|
|
.group_id("ag1")
|
|
|
|
.name("audio example")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
));
|
|
|
|
|
|
|
|
assert!(variant_stream.is_associated(
|
|
|
|
&ExtXMedia::builder()
|
|
|
|
.media_type(MediaType::Subtitles)
|
|
|
|
.uri("https://www.example.com/sg1.ssa")
|
|
|
|
.group_id("sg1")
|
|
|
|
.name("subtitle example")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
));
|
|
|
|
|
|
|
|
assert!(variant_stream.is_associated(
|
|
|
|
&ExtXMedia::builder()
|
|
|
|
.media_type(MediaType::ClosedCaptions)
|
|
|
|
.group_id("cc1")
|
|
|
|
.name("closed captions example")
|
|
|
|
.instream_id(InStreamId::Cc1)
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
));
|
|
|
|
|
|
|
|
if let VariantStream::ExtXStreamInf {
|
|
|
|
closed_captions, ..
|
|
|
|
} = &mut variant_stream
|
|
|
|
{
|
|
|
|
*closed_captions = Some(ClosedCaptions::None);
|
|
|
|
}
|
|
|
|
|
|
|
|
assert!(variant_stream.is_associated(
|
|
|
|
&ExtXMedia::builder()
|
|
|
|
.media_type(MediaType::ClosedCaptions)
|
|
|
|
.group_id("NONE")
|
|
|
|
.name("closed captions example")
|
|
|
|
.instream_id(InStreamId::Cc1)
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
));
|
|
|
|
|
|
|
|
assert!(variant_stream.is_associated(
|
|
|
|
&ExtXMedia::builder()
|
|
|
|
.media_type(MediaType::Video)
|
|
|
|
.group_id("vg1")
|
|
|
|
.name("video example")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|