Skip to main content

CustomTag

Trait CustomTag 

Source
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:

  1. CustomTag::is_known_name which allows the library to know whether a tag line (line prefixed with #EXT) should be considered a possible instance of this implementation.
  2. 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 example

Required Methods§

Source

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.

Implementors§