pub trait CustomTag<'a>:
TryFrom<UnknownTag<'a>, Error = ValidationError>
+ Debug
+ PartialEq {
// Required method
fn is_known_name(name: &str) -> bool;
}Expand description
Trait to define a custom tag implementation.
The trait comes in two parts:
CustomTag::is_known_namewhich allows the library to know whether a tag line (line prefixed with#EXT) should be considered a possible instance of this implementation.TryFrom<UnknownTag>which is where the parsing into the custom tag instance is attempted.
The UnknownTag struct provides the name of the tag and the value (if it exists), split out
and wrapped in a struct that provides parsing methods for several data types defined in the HLS
specification. The concept here is that when we are converting into our known tag we have the
right context to choose the best parsing method for the tag value type we expect. If we were to
try and parse values up front, then we would run into issues, like trying to distinguish between
an integer and a float if the mantissa (fractional part) is not present. Taking a lazy approach
to parsing helps us avoid these ambiguities, and also, provdies a performance improvement as we
do not waste attempts at parsing data in an unexpected format.
§Single tag example
Suppose we have a proprietary extension of HLS where we have added the following tag:
EXT-X-JOKE
The EXT-X-JOKE tag allows a server to provide a joke to the client.
It is OPTIONAL. Its format is:
#EXT-X-JOKE:<attribute-list>
The following attributes are defined:
TYPE
The value is an enumerated-string; valid strings are DAD, PUN,
BAR, STORY, and KNOCK-KNOCK. This attribute is REQUIRED.
JOKE
The value is a quoted-string that includes the contents of the
joke. The value MUST be hilarious. Clients SHOULD reject the joke
if it does not ellicit at least a smile. If the TYPE is DAD, then
the client SHOULD groan on completion of the joke. This attribute
is REQUIRED.We may choose to model this tag as such (adding the derive attributes for convenience):
#[derive(Debug, PartialEq, Clone)]
struct JokeTag<'a> {
joke_type: JokeType,
joke: &'a str,
}
#[derive(Debug, PartialEq, Clone)]
enum JokeType {
Dad,
Pun,
Bar,
Story,
KnockKnock,
}The first step we must take is to implement the parsing logic for this tag. To do that we must
implement the TryFrom<unknown::Tag> requirement. We may do this as follows:
impl<'a> TryFrom<UnknownTag<'a>> for JokeTag<'a> {
type Error = ValidationError;
fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
// Ensure that the value of the tag corresponds to `<attribute-list>`
let list = tag
.value()
.ok_or(ParseTagValueError::UnexpectedEmpty)?
.try_as_attribute_list()?;
// Ensure that the `JOKE` attribute exists and is of the correct type.
let joke = list
.get("JOKE")
.and_then(AttributeValue::quoted)
.ok_or(ValidationError::MissingRequiredAttribute("JOKE"))?;
// Ensure that the `TYPE` attribute exists and is of the correct type. Note the
// difference that this type is `Unquoted` instead of `Quoted`, and so we use the helper
// method `unquoted` rather than `quoted`. This signifies the use of the HLS defined
// `enumerated-string` attribute value type.
let joke_type_str = list
.get("TYPE")
.and_then(AttributeValue::unquoted)
.ok_or(ValidationError::MissingRequiredAttribute("TYPE"))?
.try_as_utf_8()
.map_err(|e| ValidationError::from(
ParseAttributeValueError::Utf8 { attr_name: "TYPE", error: e }
))?;
// Translate the enumerated string value into the enum cases we support, otherwise,
// return an error.
let Some(joke_type) = (match joke_type_str {
"DAD" => Some(JokeType::Dad),
"PUN" => Some(JokeType::Pun),
"BAR" => Some(JokeType::Bar),
"STORY" => Some(JokeType::Story),
"KNOCK-KNOCK" => Some(JokeType::KnockKnock),
_ => None,
}) else {
return Err(ValidationError::InvalidEnumeratedString);
};
// Now we have our joke.
Ok(Self { joke_type, joke })
}
}Now we can simply implement the CustomTag requirement via the is_known_name method. Note
that the tag name is everything after #EXT (and before :), implying that the -X- is
included in the name:
impl<'a> CustomTag<'a> for JokeTag<'a> {
fn is_known_name(name: &str) -> bool {
name == "-X-JOKE"
}
}At this stage we are ready to use our tag, for example, as part of a crate::Reader. Below we
include an example playlist string and show parsing of the joke working. Note that we define our
custom tag with the reader using std::marker::PhantomData and the
crate::Reader::with_custom_from_str method.
const EXAMPLE: &str = r#"#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-JOKE:TYPE=DAD,JOKE="Why did the bicycle fall over? Because it was two-tired!"
#EXTINF:9.009
segment.0.ts
"#;
let mut reader = Reader::with_custom_from_str(
EXAMPLE,
ParsingOptions::default(),
PhantomData::<JokeTag>,
);
// First 3 tags as expected
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(M3u))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Targetduration::new(10)))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Version::new(3)))));
// And the big reveal
match reader.read_line() {
Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
assert_eq!(
&JokeTag {
joke_type: JokeType::Dad,
joke: "Why did the bicycle fall over? Because it was two-tired!",
},
tag.as_ref()
);
}
r => panic!("unexpected result {r:?}"),
}§Multiple tag example
The same concepts extend to defining multiple custom tags. For example, in 2018 (before the
standardization of LL-HLS), the good people at JWPlayer and hls.js proposed a new extension of
HLS to support low latency streaming. This proposal was captured in hlsjs-rfcs-0001. It added
two tags: #EXT-X-PREFETCH:<URI> and #EXT-X-PREFETCH-DISCONTINUITY. Below we make an attempt
to implement these as custom tags. We don’t break for commentary as most of this was explained
in the example above. This example was chosen as the defined tag values are not attribute-list
and so we can demonstrate different tag parsing techniques.
#[derive(Debug, PartialEq, Clone)]
enum LHlsTag<'a> {
Discontinuity,
Prefetch(&'a str),
}
impl<'a> LHlsTag<'a> {
fn try_from_discontinuity(value: Option<TagValue>) -> Result<Self, ValidationError> {
match value {
Some(_) => Err(ValidationError::from(ParseTagValueError::NotEmpty)),
None => Ok(Self::Discontinuity)
}
}
fn try_from_prefetch(value: Option<TagValue<'a>>) -> Result<Self, ValidationError> {
// Note that the `TagValue` provides methods for parsing value data as defined in the
// HLS specification, as extracted from the existing tag definitions (there is specific
// definition for possible attribute-list value types; however, for general tag values,
// this has to be inferred from what tags are defined). `TagValue` does not provide a
// `try_as_utf_8` method, since the only tag that defines a text value is the
// `EXT-X-PLAYLIST-TYPE` tag, but this is an enumerated string (`EVENT` or `VOD`), and
// so we just offer `try_as_playlist_type`. Nevertheless, the inner data of `TagValue`
// is accessible, and so we can convert to UTF-8 ourselves here, as shown below.
let unparsed = value.ok_or(ParseTagValueError::UnexpectedEmpty)?;
let Ok(uri) = std::str::from_utf8(unparsed.0) else {
return Err(ValidationError::MissingRequiredAttribute("<URI>"));
};
Ok(Self::Prefetch(uri))
}
}
impl<'a> TryFrom<UnknownTag<'a>> for LHlsTag<'a> {
type Error = ValidationError;
fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
match tag.name() {
"-X-PREFETCH-DISCONTINUITY" => Self::try_from_discontinuity(tag.value()),
"-X-PREFETCH" => Self::try_from_prefetch(tag.value()),
_ => Err(ValidationError::UnexpectedTagName),
}
}
}
impl<'a> CustomTag<'a> for LHlsTag<'a> {
fn is_known_name(name: &str) -> bool {
name == "-X-PREFETCH" || name == "-X-PREFETCH-DISCONTINUITY"
}
}
// This example is taken from the "Examples" section under the "Discontinuities" example.
const EXAMPLE: &str = r#"#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z
#EXTINF:2.000
https://foo.com/bar/0.ts
#EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z
#EXTINF:2.000
https://foo.com/bar/1.ts
#EXT-X-PREFETCH-DISCONTINUITY
#EXT-X-PREFETCH:https://foo.com/bar/5.ts
#EXT-X-PREFETCH:https://foo.com/bar/6.ts"#;
let mut reader = Reader::with_custom_from_str(
EXAMPLE,
ParsingOptions::default(),
PhantomData::<LHlsTag>,
);
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(M3u))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Version::new(3)))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Targetduration::new(2)))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(MediaSequence::new(0)))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(DiscontinuitySequence::new(0)))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::Blank)));
assert_eq!(
reader.read_line(),
Ok(Some(HlsLine::from(ProgramDateTime::new(
date_time!(2018-09-05 T 20:59:06.531)
))))
);
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Inf::new(2.0, "")))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::Uri("https://foo.com/bar/0.ts".into()))));
assert_eq!(
reader.read_line(),
Ok(Some(HlsLine::from(ProgramDateTime::new(
date_time!(2018-09-05 T 20:59:08.531)
))))
);
assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Inf::new(2.0, "")))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::Uri("https://foo.com/bar/1.ts".into()))));
assert_eq!(reader.read_line(), Ok(Some(HlsLine::Blank)));
match reader.read_line() {
Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
assert_eq!(&LHlsTag::Discontinuity, tag.as_ref());
}
r => panic!("unexpected result {r:?}"),
}
match reader.read_line() {
Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
assert_eq!(&LHlsTag::Prefetch("https://foo.com/bar/5.ts"), tag.as_ref());
}
r => panic!("unexpected result {r:?}"),
}
match reader.read_line() {
Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
assert_eq!(&LHlsTag::Prefetch("https://foo.com/bar/6.ts"), tag.as_ref());
}
r => panic!("unexpected result {r:?}"),
}
assert_eq!(reader.read_line(), Ok(None)); // end of exampleRequired Methods§
Sourcefn is_known_name(name: &str) -> bool
fn is_known_name(name: &str) -> bool
Check if the provided name is known for this custom tag implementation.
This method is called before any attempt to parse the data into a CustomTag (it is the test for whether an attempt will be made to parse to CustomTag).
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.