mirror of
https://github.com/sile/hls_m3u8.git
synced 2025-02-01 22:22:19 +00:00
Merge pull request #44 from Luro02/master
This commit is contained in:
commit
1057c905bf
81 changed files with 8902 additions and 7093 deletions
14
.github/workflows/audit.yml
vendored
Normal file
14
.github/workflows/audit.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: Security audit
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
jobs:
|
||||||
|
security_audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/audit-check@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
55
.github/workflows/rust.yml
vendored
55
.github/workflows/rust.yml
vendored
|
@ -1,31 +1,36 @@
|
||||||
name: Rust
|
name: rust
|
||||||
|
|
||||||
# Trigger the workflow on push or pull request
|
# Trigger the workflow on push or pull request
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rustfmt:
|
rustfmt:
|
||||||
runs-on: ubuntu-latest
|
name: rustfmt
|
||||||
steps:
|
runs-on: ubuntu-latest
|
||||||
- uses: actions/checkout@v1
|
steps:
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions/checkout@v2
|
||||||
with:
|
- uses: actions-rs/toolchain@v1
|
||||||
toolchain: nightly
|
with:
|
||||||
- run: rustup component add rustfmt
|
profile: minimal
|
||||||
- uses: actions-rs/cargo@v1
|
toolchain: nightly
|
||||||
with:
|
override: true
|
||||||
command: fmt
|
components: rustfmt
|
||||||
args: --all -- --check
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
runs-on: ubuntu-latest
|
name: clippy
|
||||||
steps:
|
runs-on: ubuntu-latest
|
||||||
- uses: actions/checkout@v1
|
steps:
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions/checkout@v2
|
||||||
with:
|
- uses: actions-rs/toolchain@v1
|
||||||
toolchain: stable
|
with:
|
||||||
- run: rustup component add clippy
|
profile: minimal
|
||||||
- uses: actions-rs/cargo@v1
|
toolchain: nightly
|
||||||
with:
|
override: true
|
||||||
command: clippy
|
components: clippy
|
||||||
# args: -- -D warnings
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
|
31
.travis.yml
31
.travis.yml
|
@ -2,11 +2,13 @@ language: rust
|
||||||
|
|
||||||
cache: cargo
|
cache: cargo
|
||||||
|
|
||||||
before_cache: |
|
before_cache:
|
||||||
cargo install cargo-tarpaulin || echo "cargo-tarpaulin already installed"
|
- cargo install cargo-tarpaulin || echo "cargo-tarpaulin already installed"
|
||||||
cargo install cargo-update || echo "cargo-update already installed"
|
- cargo install cargo-update || echo "cargo-update already installed"
|
||||||
cargo install cargo-audit || echo "cargo-audit already installed"
|
- cargo install cargo-audit || echo "cargo-audit already installed"
|
||||||
cargo install-update --all
|
- cargo install-update --all
|
||||||
|
# Travis can't cache files that are not readable by "others"
|
||||||
|
- chmod -R a+r $HOME/.cargo
|
||||||
|
|
||||||
# before_cache:
|
# before_cache:
|
||||||
# - rm -rf /home/travis/.cargo/registry
|
# - rm -rf /home/travis/.cargo/registry
|
||||||
|
@ -19,19 +21,22 @@ matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- rust: nightly
|
- rust: nightly
|
||||||
|
|
||||||
script: |
|
script:
|
||||||
cargo clean
|
- cargo clean
|
||||||
cargo build
|
- cargo build
|
||||||
cargo test
|
- cargo test
|
||||||
|
- cargo test --features chrono
|
||||||
|
- cargo test --features backtrace
|
||||||
|
|
||||||
# it's enough to run this once:
|
# it's enough to run this once:
|
||||||
if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
|
- |
|
||||||
cargo audit
|
if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
|
||||||
fi
|
cargo audit
|
||||||
|
fi
|
||||||
|
|
||||||
after_success: |
|
after_success: |
|
||||||
# this does require a -Z flag for Doctests, which is unstable!
|
# this does require a -Z flag for Doctests, which is unstable!
|
||||||
if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
|
if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
|
||||||
cargo tarpaulin --run-types Tests Doctests --out Xml
|
cargo tarpaulin -f --ignore-panics --ignore-tests --run-types Tests Doctests --out Xml
|
||||||
bash <(curl -s https://codecov.io/bash)
|
bash <(curl -s https://codecov.io/bash)
|
||||||
fi
|
fi
|
||||||
|
|
26
Cargo.toml
26
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "hls_m3u8"
|
name = "hls_m3u8"
|
||||||
version = "0.2.1"
|
version = "0.2.1" # remember to update html_root_url
|
||||||
authors = ["Takeru Ohta <phjgt308@gmail.com>"]
|
authors = ["Takeru Ohta <phjgt308@gmail.com>"]
|
||||||
description = "HLS m3u8 parser/generator"
|
description = "HLS m3u8 parser/generator"
|
||||||
homepage = "https://github.com/sile/hls_m3u8"
|
homepage = "https://github.com/sile/hls_m3u8"
|
||||||
|
@ -11,18 +11,24 @@ keywords = ["hls", "m3u8"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
categories = ["parser"]
|
categories = ["parser"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
[badges]
|
[badges]
|
||||||
travis-ci = { repository = "sile/hls_m3u8" }
|
|
||||||
codecov = { repository = "sile/hls_m3u8" }
|
codecov = { repository = "sile/hls_m3u8" }
|
||||||
|
travis-ci = { repository = "sile/hls_m3u8" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
failure = "0.1.5"
|
chrono = { version = "0.4", optional = true }
|
||||||
derive_builder = "0.8.0"
|
backtrace = { version = "0.3", features = ["std"], optional = true }
|
||||||
chrono = "0.4.9"
|
|
||||||
strum = { version = "0.16.0", features = ["derive"] }
|
derive_builder = "0.9"
|
||||||
derive_more = "0.15.0"
|
hex = "0.4"
|
||||||
hex = "0.4.0"
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
derive_more = "0.99"
|
||||||
|
shorthand = "0.1"
|
||||||
|
strum = { version = "0.17", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clap = "2.33.0"
|
pretty_assertions = "0.6"
|
||||||
pretty_assertions = "0.6.1"
|
version-sync = "0.9"
|
||||||
|
|
|
@ -5,9 +5,7 @@ hls_m3u8
|
||||||
[![Documentation](https://docs.rs/hls_m3u8/badge.svg)](https://docs.rs/hls_m3u8)
|
[![Documentation](https://docs.rs/hls_m3u8/badge.svg)](https://docs.rs/hls_m3u8)
|
||||||
[![Build Status](https://travis-ci.org/sile/hls_m3u8.svg?branch=master)](https://travis-ci.org/sile/hls_m3u8)
|
[![Build Status](https://travis-ci.org/sile/hls_m3u8.svg?branch=master)](https://travis-ci.org/sile/hls_m3u8)
|
||||||
[![Code Coverage](https://codecov.io/gh/sile/hls_m3u8/branch/master/graph/badge.svg)](https://codecov.io/gh/sile/hls_m3u8/branch/master)
|
[![Code Coverage](https://codecov.io/gh/sile/hls_m3u8/branch/master/graph/badge.svg)](https://codecov.io/gh/sile/hls_m3u8/branch/master)
|
||||||
[![License: Apache](https://img.shields.io/badge/License-Apache%202.0-red.svg)](LICENSE-APACHE)
|
![Crates.io](https://img.shields.io/crates/l/hls_m3u8)
|
||||||
OR
|
|
||||||
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
|
|
||||||
|
|
||||||
[HLS] m3u8 parser/generator.
|
[HLS] m3u8 parser/generator.
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-TARGETDURATION:10
|
|
||||||
#EXT-X-VERSION:3
|
|
||||||
#EXTINF:9.009,
|
|
||||||
http://media.example.com/first.ts
|
|
||||||
#EXTINF:9.009,
|
|
||||||
http://media.example.com/second.ts
|
|
||||||
#EXTINF:3.003,
|
|
||||||
http://media.example.com/third.ts
|
|
||||||
#EXT-X-ENDLIST
|
|
||||||
|
|
||||||
# 8.1. Simple Media Playlist
|
|
|
@ -1,13 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-VERSION:3
|
|
||||||
#EXT-X-TARGETDURATION:8
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:2680
|
|
||||||
|
|
||||||
#EXTINF:7.975,
|
|
||||||
https://priv.example.com/fileSequence2680.ts
|
|
||||||
#EXTINF:7.941,
|
|
||||||
https://priv.example.com/fileSequence2681.ts
|
|
||||||
#EXTINF:7.975,
|
|
||||||
https://priv.example.com/fileSequence2682.ts
|
|
||||||
|
|
||||||
# 8.2. Live Media Playlist Using HTTPS
|
|
|
@ -1,20 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-VERSION:3
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:7794
|
|
||||||
#EXT-X-TARGETDURATION:15
|
|
||||||
|
|
||||||
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"
|
|
||||||
|
|
||||||
#EXTINF:2.833,
|
|
||||||
http://media.example.com/fileSequence52-A.ts
|
|
||||||
#EXTINF:15.0,
|
|
||||||
http://media.example.com/fileSequence52-B.ts
|
|
||||||
#EXTINF:13.333,
|
|
||||||
http://media.example.com/fileSequence52-C.ts
|
|
||||||
|
|
||||||
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53"
|
|
||||||
|
|
||||||
#EXTINF:15.0,
|
|
||||||
http://media.example.com/fileSequence53-A.ts
|
|
||||||
|
|
||||||
# 8.3. Playlist with Encrypted Media Segments
|
|
|
@ -1,11 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000
|
|
||||||
http://example.com/low.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000
|
|
||||||
http://example.com/mid.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000
|
|
||||||
http://example.com/hi.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
|
|
||||||
http://example.com/audio-only.m3u8
|
|
||||||
|
|
||||||
# 8.4. Master Playlist
|
|
|
@ -1,14 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000
|
|
||||||
low/audio-video.m3u8
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8"
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000
|
|
||||||
mid/audio-video.m3u8
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8"
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000
|
|
||||||
hi/audio-video.m3u8
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8"
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
|
|
||||||
audio-only.m3u8
|
|
||||||
|
|
||||||
# 8.5. Master Playlist with I-Frames
|
|
|
@ -1,14 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="main/english-audio.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",URI="main/german-audio.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",URI="commentary/audio-only.m3u8"
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",AUDIO="aac"
|
|
||||||
low/video-only.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",AUDIO="aac"
|
|
||||||
mid/video-only.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",AUDIO="aac"
|
|
||||||
hi/video-only.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac"
|
|
||||||
main/english-audio.m3u8
|
|
||||||
|
|
||||||
# 8.6. Master Playlist with Alternative Audio
|
|
|
@ -1,23 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8"
|
|
||||||
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",VIDEO="low"
|
|
||||||
low/main/audio-video.m3u8
|
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=YES,URI="mid/main/audio-video.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=NO,URI="mid/centerfield/audio-video.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8"
|
|
||||||
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",VIDEO="mid"
|
|
||||||
mid/main/audio-video.m3u8
|
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8"
|
|
||||||
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",VIDEO="hi"
|
|
||||||
hi/main/audio-video.m3u8
|
|
||||||
|
|
||||||
# 8.7. Master Playlist with Alternative Video
|
|
|
@ -1,32 +0,0 @@
|
||||||
extern crate clap;
|
|
||||||
extern crate hls_m3u8;
|
|
||||||
|
|
||||||
use clap::{App, Arg};
|
|
||||||
use hls_m3u8::{MasterPlaylist, MediaPlaylist};
|
|
||||||
use std::io::{self, Read};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let matches = App::new("parse")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("M3U8_TYPE")
|
|
||||||
.long("m3u8-type")
|
|
||||||
.takes_value(true)
|
|
||||||
.default_value("media")
|
|
||||||
.possible_values(&["media", "master"]),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
let mut m3u8 = String::new();
|
|
||||||
io::stdin().read_to_string(&mut m3u8).unwrap();
|
|
||||||
|
|
||||||
match matches.value_of("M3U8_TYPE").unwrap() {
|
|
||||||
"media" => {
|
|
||||||
let playlist: MediaPlaylist = m3u8.parse().unwrap();
|
|
||||||
println!("{}", playlist);
|
|
||||||
}
|
|
||||||
"master" => {
|
|
||||||
let playlist: MasterPlaylist = m3u8.parse().unwrap();
|
|
||||||
println!("{}", playlist);
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,9 +2,14 @@ error_on_unformatted = true
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
fn_single_line = true
|
fn_single_line = true
|
||||||
force_multiline_blocks = true
|
force_multiline_blocks = true
|
||||||
|
|
||||||
format_code_in_doc_comments = true
|
format_code_in_doc_comments = true
|
||||||
format_macro_matchers = true
|
format_macro_matchers = true
|
||||||
|
format_macro_bodies = true
|
||||||
|
|
||||||
match_arm_blocks = true
|
match_arm_blocks = true
|
||||||
reorder_impl_items = true
|
reorder_impl_items = true
|
||||||
use_field_init_shorthand = true
|
use_field_init_shorthand = true
|
||||||
wrap_comments = true
|
wrap_comments = true
|
||||||
|
condense_wildcard_suffixes = true
|
||||||
|
unstable_features = true
|
||||||
|
|
288
src/attribute.rs
288
src/attribute.rs
|
@ -1,148 +1,208 @@
|
||||||
use std::collections::HashMap;
|
use core::iter::FusedIterator;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::Error;
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct AttributePairs<'a> {
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
string: &'a str,
|
||||||
pub struct AttributePairs(HashMap<String, String>);
|
index: usize,
|
||||||
|
|
||||||
impl AttributePairs {
|
|
||||||
pub fn new() -> Self { Self::default() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for AttributePairs {
|
impl<'a> AttributePairs<'a> {
|
||||||
type Target = HashMap<String, String>;
|
pub const fn new(string: &'a str) -> Self { Self { string, index: 0 } }
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for AttributePairs {
|
impl<'a> Iterator for AttributePairs<'a> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
type Item = (&'a str, &'a str);
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for AttributePairs {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
type IntoIter = ::std::collections::hash_map::IntoIter<String, String>;
|
// return `None`, if there are no more bytes
|
||||||
type Item = (String, String);
|
self.string.as_bytes().get(self.index + 1)?;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
|
let key = {
|
||||||
}
|
// the position in the string:
|
||||||
|
let start = self.index;
|
||||||
|
// the key ends at an `=`:
|
||||||
|
let end = self
|
||||||
|
.string
|
||||||
|
.char_indices()
|
||||||
|
.skip_while(|(i, _)| *i < self.index)
|
||||||
|
.find_map(|(i, c)| if c == '=' { Some(i) } else { None })?;
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a AttributePairs {
|
// advance the index to the char after the end of the key (to skip the `=`)
|
||||||
type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>;
|
// NOTE: it is okay to add 1 to the index, because an `=` is exactly 1 byte.
|
||||||
type Item = (&'a String, &'a String);
|
self.index = end + 1;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter { self.0.iter() }
|
::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap()
|
||||||
}
|
};
|
||||||
|
|
||||||
impl FromStr for AttributePairs {
|
let value = {
|
||||||
type Err = Error;
|
let start = self.index;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
// find the end of the value by searching for `,`.
|
||||||
let mut result = Self::new();
|
// it should ignore `,` that are inside double quotes.
|
||||||
|
let mut inside_quotes = false;
|
||||||
|
|
||||||
for line in split(input, ',') {
|
let end = {
|
||||||
let pair = split(line.trim(), '=');
|
let mut result = self.string.len();
|
||||||
|
|
||||||
if pair.len() < 2 {
|
for (i, c) in self
|
||||||
continue;
|
.string
|
||||||
}
|
.char_indices()
|
||||||
|
.skip_while(|(i, _)| *i < self.index)
|
||||||
let key = pair[0].trim().to_uppercase();
|
{
|
||||||
let value = pair[1].trim().to_string();
|
// if a quote is encountered
|
||||||
if value.is_empty() {
|
if c == '"' {
|
||||||
continue;
|
// update variable
|
||||||
}
|
inside_quotes = !inside_quotes;
|
||||||
|
// terminate if a comma is encountered, which is not in a
|
||||||
result.insert(key.trim().to_string(), value.trim().to_string());
|
// quote
|
||||||
}
|
} else if c == ',' && !inside_quotes {
|
||||||
|
// move the index past the comma
|
||||||
#[cfg(test)] // this is very useful, when a test fails!
|
self.index += 1;
|
||||||
dbg!(&result);
|
// the result is the index of the comma (comma is not included in the
|
||||||
Ok(result)
|
// resulting string)
|
||||||
}
|
result = i;
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
fn split(value: &str, terminator: char) -> Vec<String> {
|
|
||||||
let mut result = vec![];
|
|
||||||
|
|
||||||
let mut inside_quotes = false;
|
|
||||||
let mut temp_string = String::with_capacity(1024);
|
|
||||||
|
|
||||||
for c in value.chars() {
|
|
||||||
match c {
|
|
||||||
'"' => {
|
|
||||||
inside_quotes = !inside_quotes;
|
|
||||||
temp_string.push(c);
|
|
||||||
}
|
|
||||||
k if (k == terminator) => {
|
|
||||||
if inside_quotes {
|
|
||||||
temp_string.push(c);
|
|
||||||
} else {
|
|
||||||
result.push(temp_string);
|
|
||||||
temp_string = String::with_capacity(1024);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {
|
result
|
||||||
temp_string.push(c);
|
};
|
||||||
|
|
||||||
|
self.index += end;
|
||||||
|
self.index -= start;
|
||||||
|
|
||||||
|
::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((key.trim(), value.trim()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let mut remaining = 0;
|
||||||
|
|
||||||
|
// each `=` in the remaining str is an iteration
|
||||||
|
// this also ignores `=` inside quotes!
|
||||||
|
let mut inside_quotes = false;
|
||||||
|
|
||||||
|
for (_, c) in self
|
||||||
|
.string
|
||||||
|
.char_indices()
|
||||||
|
.skip_while(|(i, _)| *i < self.index)
|
||||||
|
{
|
||||||
|
if c == '=' && !inside_quotes {
|
||||||
|
remaining += 1;
|
||||||
|
} else if c == '"' {
|
||||||
|
inside_quotes = !inside_quotes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
result.push(temp_string);
|
|
||||||
|
|
||||||
result
|
(remaining, Some(remaining))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> ExactSizeIterator for AttributePairs<'a> {}
|
||||||
|
impl<'a> FusedIterator for AttributePairs<'a> {}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_attributes() {
|
||||||
|
let mut attributes = AttributePairs::new("KEY=VALUE,PAIR=YES");
|
||||||
|
|
||||||
|
assert_eq!((2, Some(2)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), Some(("KEY", "VALUE")));
|
||||||
|
|
||||||
|
assert_eq!((1, Some(1)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), Some(("PAIR", "YES")));
|
||||||
|
|
||||||
|
assert_eq!((0, Some(0)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), None);
|
||||||
|
|
||||||
|
let mut attributes = AttributePairs::new("garbage");
|
||||||
|
assert_eq!((0, Some(0)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), None);
|
||||||
|
|
||||||
|
let mut attributes = AttributePairs::new("KEY=,=VALUE,=,");
|
||||||
|
|
||||||
|
assert_eq!((3, Some(3)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), Some(("KEY", "")));
|
||||||
|
|
||||||
|
assert_eq!((2, Some(2)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), Some(("", "VALUE")));
|
||||||
|
|
||||||
|
assert_eq!((1, Some(1)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), Some(("", "")));
|
||||||
|
|
||||||
|
assert_eq!((0, Some(0)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), None);
|
||||||
|
|
||||||
|
// test quotes:
|
||||||
|
let mut attributes = AttributePairs::new("KEY=\"VALUE,\",");
|
||||||
|
|
||||||
|
assert_eq!((1, Some(1)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), Some(("KEY", "\"VALUE,\"")));
|
||||||
|
|
||||||
|
assert_eq!((0, Some(0)), attributes.size_hint());
|
||||||
|
assert_eq!(attributes.next(), None);
|
||||||
|
|
||||||
|
// test with chars, that are larger, than 1 byte
|
||||||
|
let mut attributes = AttributePairs::new(concat!(
|
||||||
|
"LANGUAGE=\"fre\",",
|
||||||
|
"NAME=\"Français\",",
|
||||||
|
"AUTOSELECT=YES"
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(attributes.next(), Some(("LANGUAGE", "\"fre\"")));
|
||||||
|
assert_eq!(attributes.next(), Some(("NAME", "\"Français\"")));
|
||||||
|
assert_eq!(attributes.next(), Some(("AUTOSELECT", "YES")));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
let pairs = "FOO=BAR,BAR=\"baz,qux\",ABC=12.3"
|
let mut pairs = AttributePairs::new("FOO=BAR,BAR=\"baz,qux\",ABC=12.3");
|
||||||
.parse::<AttributePairs>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut iterator = pairs.iter();
|
assert_eq!(pairs.next(), Some(("FOO", "BAR")));
|
||||||
assert!(iterator.any(|(k, v)| k == "FOO" && "BAR" == v));
|
assert_eq!(pairs.next(), Some(("BAR", "\"baz,qux\"")));
|
||||||
|
assert_eq!(pairs.next(), Some(("ABC", "12.3")));
|
||||||
|
assert_eq!(pairs.next(), None);
|
||||||
|
|
||||||
let mut iterator = pairs.iter();
|
// stress test with foreign input
|
||||||
assert!(iterator.any(|(k, v)| k == "BAR" && v == "\"baz,qux\""));
|
// got it from https://generator.lorem-ipsum.info/_chinese
|
||||||
|
|
||||||
let mut iterator = pairs.iter();
|
let mut pairs = AttributePairs::new(concat!(
|
||||||
assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3"));
|
"載抗留囲軽来実基供全必式覧領意度振。=著地内方満職控努作期投綱研本模,",
|
||||||
|
"後文図様改表宮能本園半参裁報作神掲索=\"針支年得率新賞現報発援白少動面。矢拉年世掲注索政平定他込\",",
|
||||||
let mut pairs = AttributePairs::new();
|
"ध्वनि स्थिति और्४५० नीचे =देखने लाभो द्वारा करके(विशेष"
|
||||||
pairs.insert("FOO".to_string(), "BAR".to_string());
|
));
|
||||||
|
|
||||||
assert_eq!("FOO=BAR,VAL".parse::<AttributePairs>().unwrap(), pairs);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_iterator() {
|
|
||||||
let mut attrs = AttributePairs::new();
|
|
||||||
attrs.insert("key_01".to_string(), "value_01".to_string());
|
|
||||||
attrs.insert("key_02".to_string(), "value_02".to_string());
|
|
||||||
|
|
||||||
let mut iterator = attrs.iter();
|
|
||||||
assert!(iterator.any(|(k, v)| k == "key_01" && v == "value_01"));
|
|
||||||
|
|
||||||
let mut iterator = attrs.iter();
|
|
||||||
assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_into_iter() {
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
map.insert("k".to_string(), "v".to_string());
|
|
||||||
|
|
||||||
let mut attrs = AttributePairs::new();
|
|
||||||
attrs.insert("k".to_string(), "v".to_string());
|
|
||||||
|
|
||||||
|
assert_eq!((3, Some(3)), pairs.size_hint());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
attrs.into_iter().collect::<Vec<_>>(),
|
pairs.next(),
|
||||||
map.into_iter().collect::<Vec<_>>()
|
Some((
|
||||||
|
"載抗留囲軽来実基供全必式覧領意度振。",
|
||||||
|
"著地内方満職控努作期投綱研本模"
|
||||||
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!((2, Some(2)), pairs.size_hint());
|
||||||
|
assert_eq!(
|
||||||
|
pairs.next(),
|
||||||
|
Some((
|
||||||
|
"後文図様改表宮能本園半参裁報作神掲索",
|
||||||
|
"\"針支年得率新賞現報発援白少動面。矢拉年世掲注索政平定他込\""
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!((1, Some(1)), pairs.size_hint());
|
||||||
|
assert_eq!(
|
||||||
|
pairs.next(),
|
||||||
|
Some(("ध्वनि स्थिति और्४५० नीचे", "देखने लाभो द्वारा करके(विशेष"))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!((0, Some(0)), pairs.size_hint());
|
||||||
|
assert_eq!(pairs.next(), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
282
src/error.rs
282
src/error.rs
|
@ -1,39 +1,36 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use failure::{Backtrace, Context, Fail};
|
#[cfg(feature = "backtrace")]
|
||||||
|
use backtrace::Backtrace;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
//use crate::types::ProtocolVersion;
|
||||||
|
|
||||||
/// This crate specific `Result` type.
|
/// This crate specific `Result` type.
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
/// The [`ErrorKind`].
|
#[derive(Debug, Error, Clone, PartialEq)]
|
||||||
#[derive(Debug, Fail, Clone, PartialEq, Eq)]
|
#[non_exhaustive]
|
||||||
pub enum ErrorKind {
|
enum ErrorKind {
|
||||||
#[fail(display = "ChronoParseError: {}", _0)]
|
#[error("a value is missing for the attribute {value}")]
|
||||||
/// An error from the [Chrono](chrono) crate.
|
MissingValue { value: String },
|
||||||
ChronoParseError(String),
|
|
||||||
|
|
||||||
#[fail(display = "UnknownError: {}", _0)]
|
#[error("invalid input")]
|
||||||
/// An unknown error occured.
|
|
||||||
UnknownError(String),
|
|
||||||
|
|
||||||
#[fail(display = "A value is missing for the attribute {}", _0)]
|
|
||||||
/// A required value is missing.
|
|
||||||
MissingValue(String),
|
|
||||||
|
|
||||||
#[fail(display = "Invalid Input")]
|
|
||||||
/// Error for anything.
|
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
|
|
||||||
#[fail(display = "ParseIntError: {}", _0)]
|
#[error("{source}: {input:?}")]
|
||||||
/// Failed to parse a String to int.
|
ParseIntError {
|
||||||
ParseIntError(String),
|
input: String,
|
||||||
|
source: ::std::num::ParseIntError,
|
||||||
|
},
|
||||||
|
|
||||||
#[fail(display = "ParseFloatError: {}", _0)]
|
#[error("{source}: {input:?}")]
|
||||||
/// Failed to parse a String to float.
|
ParseFloatError {
|
||||||
ParseFloatError(String),
|
input: String,
|
||||||
|
source: ::std::num::ParseFloatError,
|
||||||
|
},
|
||||||
|
|
||||||
#[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)]
|
#[error("expected `{tag}` at the start of {input:?}")]
|
||||||
/// A tag is missing, that is required at the start of the input.
|
|
||||||
MissingTag {
|
MissingTag {
|
||||||
/// The required tag.
|
/// The required tag.
|
||||||
tag: String,
|
tag: String,
|
||||||
|
@ -41,100 +38,111 @@ pub enum ErrorKind {
|
||||||
input: String,
|
input: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[fail(display = "CustomError: {}", _0)]
|
#[error("{0}")]
|
||||||
/// A custom error.
|
|
||||||
Custom(String),
|
Custom(String),
|
||||||
|
|
||||||
#[fail(display = "Unmatched Group: {:?}", _0)]
|
#[error("unmatched group: {0:?}")]
|
||||||
/// Unmatched Group
|
|
||||||
UnmatchedGroup(String),
|
UnmatchedGroup(String),
|
||||||
|
|
||||||
#[fail(display = "Unknown Protocol version: {:?}", _0)]
|
#[error("unknown protocol version {0:?}")]
|
||||||
/// Unknown m3u8 version. This library supports up to ProtocolVersion 7.
|
|
||||||
UnknownProtocolVersion(String),
|
UnknownProtocolVersion(String),
|
||||||
|
|
||||||
#[fail(display = "IoError: {}", _0)]
|
// #[error("required_version: {:?}, specified_version: {:?}", _0, _1)]
|
||||||
/// Some io error
|
// VersionError(ProtocolVersion, ProtocolVersion),
|
||||||
Io(String),
|
#[error("missing attribute: {attribute:?}")]
|
||||||
|
MissingAttribute { attribute: String },
|
||||||
|
|
||||||
#[fail(
|
#[error("unexpected attribute: {attribute:?}")]
|
||||||
display = "VersionError: required_version: {:?}, specified_version: {:?}",
|
UnexpectedAttribute { attribute: String },
|
||||||
_0, _1
|
|
||||||
)]
|
|
||||||
/// This error occurs, if there is a ProtocolVersion mismatch.
|
|
||||||
VersionError(String, String),
|
|
||||||
|
|
||||||
#[fail(display = "BuilderError: {}", _0)]
|
#[error("unexpected tag: {tag:?}")]
|
||||||
/// An Error from a Builder.
|
UnexpectedTag { tag: String },
|
||||||
BuilderError(String),
|
|
||||||
|
|
||||||
#[fail(display = "Missing Attribute: {}", _0)]
|
#[error("{source}")]
|
||||||
/// An attribute is missing.
|
#[cfg(feature = "chrono")]
|
||||||
MissingAttribute(String),
|
Chrono { source: chrono::ParseError },
|
||||||
|
|
||||||
#[fail(display = "Unexpected Attribute: {:?}", _0)]
|
#[error("builder error: {message}")]
|
||||||
/// An unexpected value.
|
Builder { message: String },
|
||||||
UnexpectedAttribute(String),
|
|
||||||
|
|
||||||
#[fail(display = "Unexpected Tag: {:?}", _0)]
|
#[error("{source}")]
|
||||||
/// An unexpected tag.
|
Hex { source: hex::FromHexError },
|
||||||
UnexpectedTag(String),
|
|
||||||
|
|
||||||
/// Hints that destructuring should not be exhaustive.
|
|
||||||
///
|
|
||||||
/// This enum may grow additional variants, so this makes sure clients
|
|
||||||
/// don't count on exhaustive matching. (Otherwise, adding a new variant
|
|
||||||
/// could break existing code.)
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[fail(display = "Invalid error")]
|
|
||||||
__Nonexhaustive,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// The Error type of this library.
|
/// The Error type of this library.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
inner: Context<ErrorKind>,
|
inner: ErrorKind,
|
||||||
|
#[cfg(feature = "backtrace")]
|
||||||
|
backtrace: Backtrace,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fail for Error {
|
impl PartialEq for Error {
|
||||||
fn cause(&self) -> Option<&dyn Fail> { self.inner.cause() }
|
fn eq(&self, other: &Self) -> bool { self.inner == other.inner }
|
||||||
|
|
||||||
fn backtrace(&self) -> Option<&Backtrace> { self.inner.backtrace() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) }
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ErrorKind> for Error {
|
|
||||||
fn from(kind: ErrorKind) -> Self { Self::from(Context::new(kind)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Context<ErrorKind>> for Error {
|
|
||||||
fn from(inner: Context<ErrorKind>) -> Self { Self { inner } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
impl Error {
|
impl Error {
|
||||||
|
fn new(inner: ErrorKind) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
#[cfg(feature = "backtrace")]
|
||||||
|
backtrace: Backtrace::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn custom<T: fmt::Display>(value: T) -> Self {
|
||||||
|
Self::new(ErrorKind::Custom(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn missing_value<T: ToString>(value: T) -> Self {
|
pub(crate) fn missing_value<T: ToString>(value: T) -> Self {
|
||||||
Self::from(ErrorKind::MissingValue(value.to_string()))
|
Self::new(ErrorKind::MissingValue {
|
||||||
|
value: value.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn missing_field<T: fmt::Display, D: fmt::Display>(strct: D, field: T) -> Self {
|
||||||
|
Self::new(ErrorKind::Custom(format!(
|
||||||
|
"the field `{}` is missing for `{}`",
|
||||||
|
field, strct
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unexpected_attribute<T: ToString>(value: T) -> Self {
|
pub(crate) fn unexpected_attribute<T: ToString>(value: T) -> Self {
|
||||||
Self::from(ErrorKind::UnexpectedAttribute(value.to_string()))
|
Self::new(ErrorKind::UnexpectedAttribute {
|
||||||
|
attribute: value.to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unexpected_tag<T: ToString>(value: T) -> Self {
|
pub(crate) fn unexpected_tag<T: ToString>(value: T) -> Self {
|
||||||
Self::from(ErrorKind::UnexpectedTag(value.to_string()))
|
Self::new(ErrorKind::UnexpectedTag {
|
||||||
|
tag: value.to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn invalid_input() -> Self { Self::from(ErrorKind::InvalidInput) }
|
pub(crate) fn invalid_input() -> Self { Self::new(ErrorKind::InvalidInput) }
|
||||||
|
|
||||||
pub(crate) fn parse_int_error<T: ToString>(value: T) -> Self {
|
pub(crate) fn parse_int<T: fmt::Display>(input: T, source: ::std::num::ParseIntError) -> Self {
|
||||||
Self::from(ErrorKind::ParseIntError(value.to_string()))
|
Self::new(ErrorKind::ParseIntError {
|
||||||
|
input: input.to_string(),
|
||||||
|
source,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_float_error<T: ToString>(value: T) -> Self {
|
pub(crate) fn parse_float<T: fmt::Display>(
|
||||||
Self::from(ErrorKind::ParseFloatError(value.to_string()))
|
input: T,
|
||||||
|
source: ::std::num::ParseFloatError,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(ErrorKind::ParseFloatError {
|
||||||
|
input: input.to_string(),
|
||||||
|
source,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn missing_tag<T, U>(tag: T, input: U) -> Self
|
pub(crate) fn missing_tag<T, U>(tag: T, input: U) -> Self
|
||||||
|
@ -142,76 +150,82 @@ impl Error {
|
||||||
T: ToString,
|
T: ToString,
|
||||||
U: ToString,
|
U: ToString,
|
||||||
{
|
{
|
||||||
Self::from(ErrorKind::MissingTag {
|
Self::new(ErrorKind::MissingTag {
|
||||||
tag: tag.to_string(),
|
tag: tag.to_string(),
|
||||||
input: input.to_string(),
|
input: input.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unmatched_group<T: ToString>(value: T) -> Self {
|
pub(crate) fn unmatched_group<T: ToString>(value: T) -> Self {
|
||||||
Self::from(ErrorKind::UnmatchedGroup(value.to_string()))
|
Self::new(ErrorKind::UnmatchedGroup(value.to_string()))
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn custom<T>(value: T) -> Self
|
|
||||||
where
|
|
||||||
T: fmt::Display,
|
|
||||||
{
|
|
||||||
Self::from(ErrorKind::Custom(value.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unknown_protocol_version<T: ToString>(value: T) -> Self {
|
pub(crate) fn unknown_protocol_version<T: ToString>(value: T) -> Self {
|
||||||
Self::from(ErrorKind::UnknownProtocolVersion(value.to_string()))
|
Self::new(ErrorKind::UnknownProtocolVersion(value.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn io<T: ToString>(value: T) -> Self { Self::from(ErrorKind::Io(value.to_string())) }
|
pub(crate) fn builder<T: ToString>(value: T) -> Self {
|
||||||
|
Self::new(ErrorKind::Builder {
|
||||||
pub(crate) fn builder_error<T: ToString>(value: T) -> Self {
|
message: value.to_string(),
|
||||||
Self::from(ErrorKind::BuilderError(value.to_string()))
|
})
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn chrono<T: ToString>(value: T) -> Self {
|
|
||||||
Self::from(ErrorKind::ChronoParseError(value.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn missing_attribute<T: ToString>(value: T) -> Self {
|
pub(crate) fn missing_attribute<T: ToString>(value: T) -> Self {
|
||||||
Self::from(ErrorKind::MissingAttribute(value.to_string()))
|
Self::new(ErrorKind::MissingAttribute {
|
||||||
|
attribute: value.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// third party crates:
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
pub(crate) fn chrono(source: chrono::format::ParseError) -> Self {
|
||||||
|
Self::new(ErrorKind::Chrono { source })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hex(source: hex::FromHexError) -> Self {
|
||||||
|
//
|
||||||
|
Self::new(ErrorKind::Hex { source })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn strum(value: strum::ParseError) -> Self {
|
||||||
|
Self::new(ErrorKind::Custom(value.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<::std::num::ParseIntError> for Error {
|
#[doc(hidden)]
|
||||||
fn from(value: ::std::num::ParseIntError) -> Self { Self::parse_int_error(value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<::std::num::ParseFloatError> for Error {
|
|
||||||
fn from(value: ::std::num::ParseFloatError) -> Self { Self::parse_float_error(value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<::std::io::Error> for Error {
|
|
||||||
fn from(value: ::std::io::Error) -> Self { Self::io(value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<::chrono::ParseError> for Error {
|
|
||||||
fn from(value: ::chrono::ParseError) -> Self { Self::chrono(value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<::strum::ParseError> for Error {
|
impl From<::strum::ParseError> for Error {
|
||||||
fn from(value: ::strum::ParseError) -> Self {
|
fn from(value: ::strum::ParseError) -> Self { Self::strum(value) }
|
||||||
Self::custom(value) // TODO!
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Error {
|
#[cfg(test)]
|
||||||
fn from(value: String) -> Self { Self::custom(value) }
|
mod tests {
|
||||||
}
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
impl From<::core::convert::Infallible> for Error {
|
#[test]
|
||||||
fn from(_: ::core::convert::Infallible) -> Self {
|
fn test_parse_float_error() {
|
||||||
Self::custom("An Infallible error has been returned! (this should never happen!)")
|
assert_eq!(
|
||||||
}
|
Error::parse_float(
|
||||||
}
|
"1.x234",
|
||||||
|
"1.x234"
|
||||||
impl From<::hex::FromHexError> for Error {
|
.parse::<f32>()
|
||||||
fn from(value: ::hex::FromHexError) -> Self {
|
.expect_err("this should not parse as a float!")
|
||||||
Self::custom(value) // TODO!
|
)
|
||||||
|
.to_string(),
|
||||||
|
"invalid float literal: \"1.x234\"".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_int_error() {
|
||||||
|
assert_eq!(
|
||||||
|
Error::parse_int(
|
||||||
|
"1x",
|
||||||
|
"1x".parse::<usize>()
|
||||||
|
.expect_err("this should not parse as an usize!")
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
"invalid digit found in string: \"1x\"".to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
95
src/lib.rs
95
src/lib.rs
|
@ -1,22 +1,43 @@
|
||||||
|
#![doc(html_root_url = "https://docs.rs/hls_m3u8/0.2.1")]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![feature(option_flattening)]
|
#![warn(rust_2018_idioms)]
|
||||||
#![warn(
|
#![warn(
|
||||||
//clippy::pedantic,
|
clippy::pedantic, //
|
||||||
clippy::nursery,
|
clippy::nursery,
|
||||||
clippy::cargo
|
clippy::cargo,
|
||||||
|
clippy::inline_always,
|
||||||
|
)]
|
||||||
|
#![allow(
|
||||||
|
clippy::multiple_crate_versions,
|
||||||
|
clippy::module_name_repetitions,
|
||||||
|
clippy::default_trait_access,
|
||||||
|
clippy::unnecessary_operation // temporary until derive-builder uses #[allow(clippy::all)]
|
||||||
|
)]
|
||||||
|
#![warn(
|
||||||
|
clippy::clone_on_ref_ptr,
|
||||||
|
clippy::decimal_literal_representation,
|
||||||
|
clippy::get_unwrap,
|
||||||
|
clippy::option_expect_used,
|
||||||
|
clippy::unneeded_field_pattern,
|
||||||
|
clippy::wrong_pub_self_convention
|
||||||
|
)]
|
||||||
|
// those should not be present in production code:
|
||||||
|
#![deny(
|
||||||
|
clippy::print_stdout,
|
||||||
|
clippy::todo,
|
||||||
|
clippy::unimplemented,
|
||||||
|
clippy::dbg_macro,
|
||||||
|
clippy::use_debug
|
||||||
)]
|
)]
|
||||||
#![allow(clippy::multiple_crate_versions)]
|
|
||||||
#![warn(
|
#![warn(
|
||||||
missing_docs,
|
missing_docs,
|
||||||
missing_copy_implementations,
|
missing_copy_implementations,
|
||||||
missing_debug_implementations,
|
missing_debug_implementations,
|
||||||
trivial_casts, // TODO (needed?)
|
trivial_casts,
|
||||||
trivial_numeric_casts
|
trivial_numeric_casts
|
||||||
)]
|
)]
|
||||||
//! [HLS] m3u8 parser/generator.
|
//! [HLS] m3u8 parser/generator.
|
||||||
//!
|
//!
|
||||||
//! [HLS]: https://tools.ietf.org/html/rfc8216
|
|
||||||
//!
|
|
||||||
//! # Examples
|
//! # Examples
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
|
@ -35,12 +56,72 @@
|
||||||
//!
|
//!
|
||||||
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
||||||
//! ```
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Crate Feature Flags
|
||||||
|
//!
|
||||||
|
//! The following crate feature flags are available:
|
||||||
|
//!
|
||||||
|
//! - [`backtrace`] (optional)
|
||||||
|
//! - Enables the backtrace feature for the `Error` type.
|
||||||
|
//! - This feature depends on the following dependencies:
|
||||||
|
//! - [`backtrace`]
|
||||||
|
//! - [`chrono`] (optional)
|
||||||
|
//! - Enables parsing dates and verifying them.
|
||||||
|
//! - This feature depends on the following dependencies:
|
||||||
|
//! - [`chrono`]
|
||||||
|
//! - The following things will change:
|
||||||
|
//! - [`ExtXProgramDateTime::date_time`] will change from [`String`] to
|
||||||
|
//! `DateTime<FixedOffset>`
|
||||||
|
//! - [`ExtXDateRange::start_date`] will change from [`String`] to
|
||||||
|
//! `DateTime<FixedOffset>`
|
||||||
|
//! - [`ExtXDateRange::end_date`] will change from [`String`] to
|
||||||
|
//! `DateTime<FixedOffset>`
|
||||||
|
//!
|
||||||
|
//! They are configured in your `Cargo.toml` and can be enabled like this
|
||||||
|
//!
|
||||||
|
//! ```toml
|
||||||
|
//! hls_m3u8 = { version = "0.3", features = ["chrono", "backtrace"] }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! [`ExtXProgramDateTime::date_time`]:
|
||||||
|
//! crate::tags::ExtXProgramDateTime::date_time
|
||||||
|
//! [`ExtXDateRange::start_date`]:
|
||||||
|
//! crate::tags::ExtXDateRange::start_date
|
||||||
|
//! [`ExtXDateRange::end_date`]:
|
||||||
|
//! crate::tags::ExtXDateRange::end_date
|
||||||
|
//! [`chrono`]: https://github.com/chronotope/chrono
|
||||||
|
//! [`backtrace`]: https://github.com/rust-lang/backtrace-rs
|
||||||
|
//! [HLS]: https://tools.ietf.org/html/rfc8216
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use master_playlist::MasterPlaylist;
|
pub use master_playlist::MasterPlaylist;
|
||||||
pub use media_playlist::MediaPlaylist;
|
pub use media_playlist::MediaPlaylist;
|
||||||
pub use media_segment::MediaSegment;
|
pub use media_segment::MediaSegment;
|
||||||
|
|
||||||
|
/// Builder structs
|
||||||
|
pub mod builder {
|
||||||
|
pub use crate::master_playlist::MasterPlaylistBuilder;
|
||||||
|
pub use crate::media_playlist::MediaPlaylistBuilder;
|
||||||
|
pub use crate::media_segment::MediaSegmentBuilder;
|
||||||
|
|
||||||
|
/// Builder structs for tags
|
||||||
|
pub mod tags {
|
||||||
|
// master playlist
|
||||||
|
pub use crate::tags::master_playlist::media::ExtXMediaBuilder;
|
||||||
|
pub use crate::tags::master_playlist::session_data::ExtXSessionDataBuilder;
|
||||||
|
|
||||||
|
// media segment
|
||||||
|
pub use crate::tags::media_segment::date_range::ExtXDateRangeBuilder;
|
||||||
|
|
||||||
|
// media playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder structs for types
|
||||||
|
pub mod types {
|
||||||
|
pub use crate::types::decryption_key::DecryptionKeyBuilder;
|
||||||
|
pub use crate::types::stream_data::StreamDataBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
|
|
175
src/line.rs
175
src/line.rs
|
@ -1,94 +1,64 @@
|
||||||
use std::fmt;
|
use core::convert::TryFrom;
|
||||||
use std::ops::{Deref, DerefMut};
|
use core::iter::FusedIterator;
|
||||||
use std::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use derive_more::Display;
|
||||||
|
|
||||||
use crate::tags;
|
use crate::tags;
|
||||||
|
use crate::types::PlaylistType;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Lines(Vec<Line>);
|
pub(crate) struct Lines<'a> {
|
||||||
|
lines: ::core::iter::FilterMap<::core::str::Lines<'a>, fn(&'a str) -> Option<&'a str>>,
|
||||||
impl Lines {
|
|
||||||
pub fn new() -> Self { Self::default() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Lines {
|
impl<'a> Iterator for Lines<'a> {
|
||||||
type Err = Error;
|
type Item = crate::Result<Line<'a>>;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let mut result = Self::new();
|
let line = self.lines.next()?;
|
||||||
|
|
||||||
let mut stream_inf = false;
|
if line.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) {
|
||||||
let mut stream_inf_line = None;
|
let uri = self.lines.next()?;
|
||||||
|
|
||||||
for l in input.lines() {
|
Some(
|
||||||
let raw_line = l.trim();
|
tags::VariantStream::from_str(&format!("{}\n{}", line, uri))
|
||||||
|
.map(|v| Line::Tag(Tag::VariantStream(v))),
|
||||||
if raw_line.is_empty() {
|
)
|
||||||
continue;
|
} else if line.starts_with("#EXT") {
|
||||||
}
|
Some(Tag::try_from(line).map(Line::Tag))
|
||||||
|
} else if line.starts_with('#') {
|
||||||
let line = {
|
Some(Ok(Line::Comment(line)))
|
||||||
if raw_line.starts_with(tags::ExtXStreamInf::PREFIX) {
|
} else {
|
||||||
stream_inf = true;
|
Some(Ok(Line::Uri(line)))
|
||||||
stream_inf_line = Some(raw_line);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else if raw_line.starts_with("#EXT") {
|
|
||||||
Line::Tag(raw_line.parse()?)
|
|
||||||
} else if raw_line.starts_with('#') {
|
|
||||||
continue; // ignore comments
|
|
||||||
} else {
|
|
||||||
// stream inf line needs special treatment
|
|
||||||
if stream_inf {
|
|
||||||
stream_inf = false;
|
|
||||||
if let Some(first_line) = stream_inf_line {
|
|
||||||
let res = Line::Tag(format!("{}\n{}", first_line, raw_line).parse()?);
|
|
||||||
stream_inf_line = None;
|
|
||||||
res
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Line::Uri(raw_line.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
result.push(line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoIterator for Lines {
|
impl<'a> FusedIterator for Lines<'a> {}
|
||||||
type IntoIter = ::std::vec::IntoIter<Line>;
|
|
||||||
type Item = Line;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
|
impl<'a> From<&'a str> for Lines<'a> {
|
||||||
}
|
fn from(buffer: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
impl Deref for Lines {
|
lines: buffer
|
||||||
type Target = Vec<Line>;
|
.lines()
|
||||||
|
.filter_map(|line| Some(line.trim()).filter(|v| !v.is_empty())),
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for Lines {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Line {
|
pub(crate) enum Line<'a> {
|
||||||
Tag(Tag),
|
Tag(Tag<'a>),
|
||||||
Uri(String),
|
Comment(&'a str),
|
||||||
|
Uri(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Display)]
|
||||||
pub enum Tag {
|
#[display(fmt = "{}")]
|
||||||
ExtM3u(tags::ExtM3u),
|
pub(crate) enum Tag<'a> {
|
||||||
ExtXVersion(tags::ExtXVersion),
|
ExtXVersion(tags::ExtXVersion),
|
||||||
ExtInf(tags::ExtInf),
|
ExtInf(tags::ExtInf),
|
||||||
ExtXByteRange(tags::ExtXByteRange),
|
ExtXByteRange(tags::ExtXByteRange),
|
||||||
|
@ -101,55 +71,22 @@ pub enum Tag {
|
||||||
ExtXMediaSequence(tags::ExtXMediaSequence),
|
ExtXMediaSequence(tags::ExtXMediaSequence),
|
||||||
ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence),
|
ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence),
|
||||||
ExtXEndList(tags::ExtXEndList),
|
ExtXEndList(tags::ExtXEndList),
|
||||||
ExtXPlaylistType(tags::ExtXPlaylistType),
|
PlaylistType(PlaylistType),
|
||||||
ExtXIFramesOnly(tags::ExtXIFramesOnly),
|
ExtXIFramesOnly(tags::ExtXIFramesOnly),
|
||||||
ExtXMedia(tags::ExtXMedia),
|
ExtXMedia(tags::ExtXMedia),
|
||||||
ExtXStreamInf(tags::ExtXStreamInf),
|
|
||||||
ExtXIFrameStreamInf(tags::ExtXIFrameStreamInf),
|
|
||||||
ExtXSessionData(tags::ExtXSessionData),
|
ExtXSessionData(tags::ExtXSessionData),
|
||||||
ExtXSessionKey(tags::ExtXSessionKey),
|
ExtXSessionKey(tags::ExtXSessionKey),
|
||||||
ExtXIndependentSegments(tags::ExtXIndependentSegments),
|
ExtXIndependentSegments(tags::ExtXIndependentSegments),
|
||||||
ExtXStart(tags::ExtXStart),
|
ExtXStart(tags::ExtXStart),
|
||||||
Unknown(String),
|
VariantStream(tags::VariantStream),
|
||||||
|
Unknown(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Tag {
|
impl<'a> TryFrom<&'a str> for Tag<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
type Error = Error;
|
||||||
match &self {
|
|
||||||
Self::ExtM3u(value) => value.fmt(f),
|
|
||||||
Self::ExtXVersion(value) => value.fmt(f),
|
|
||||||
Self::ExtInf(value) => value.fmt(f),
|
|
||||||
Self::ExtXByteRange(value) => value.fmt(f),
|
|
||||||
Self::ExtXDiscontinuity(value) => value.fmt(f),
|
|
||||||
Self::ExtXKey(value) => value.fmt(f),
|
|
||||||
Self::ExtXMap(value) => value.fmt(f),
|
|
||||||
Self::ExtXProgramDateTime(value) => value.fmt(f),
|
|
||||||
Self::ExtXDateRange(value) => value.fmt(f),
|
|
||||||
Self::ExtXTargetDuration(value) => value.fmt(f),
|
|
||||||
Self::ExtXMediaSequence(value) => value.fmt(f),
|
|
||||||
Self::ExtXDiscontinuitySequence(value) => value.fmt(f),
|
|
||||||
Self::ExtXEndList(value) => value.fmt(f),
|
|
||||||
Self::ExtXPlaylistType(value) => value.fmt(f),
|
|
||||||
Self::ExtXIFramesOnly(value) => value.fmt(f),
|
|
||||||
Self::ExtXMedia(value) => value.fmt(f),
|
|
||||||
Self::ExtXStreamInf(value) => value.fmt(f),
|
|
||||||
Self::ExtXIFrameStreamInf(value) => value.fmt(f),
|
|
||||||
Self::ExtXSessionData(value) => value.fmt(f),
|
|
||||||
Self::ExtXSessionKey(value) => value.fmt(f),
|
|
||||||
Self::ExtXIndependentSegments(value) => value.fmt(f),
|
|
||||||
Self::ExtXStart(value) => value.fmt(f),
|
|
||||||
Self::Unknown(value) => value.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Tag {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
type Err = Error;
|
if input.starts_with(tags::ExtXVersion::PREFIX) {
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
if input.starts_with(tags::ExtM3u::PREFIX) {
|
|
||||||
input.parse().map(Self::ExtM3u)
|
|
||||||
} else if input.starts_with(tags::ExtXVersion::PREFIX) {
|
|
||||||
input.parse().map(Self::ExtXVersion)
|
input.parse().map(Self::ExtXVersion)
|
||||||
} else if input.starts_with(tags::ExtInf::PREFIX) {
|
} else if input.starts_with(tags::ExtInf::PREFIX) {
|
||||||
input.parse().map(Self::ExtInf)
|
input.parse().map(Self::ExtInf)
|
||||||
|
@ -173,16 +110,16 @@ impl FromStr for Tag {
|
||||||
input.parse().map(Self::ExtXDiscontinuitySequence)
|
input.parse().map(Self::ExtXDiscontinuitySequence)
|
||||||
} else if input.starts_with(tags::ExtXEndList::PREFIX) {
|
} else if input.starts_with(tags::ExtXEndList::PREFIX) {
|
||||||
input.parse().map(Self::ExtXEndList)
|
input.parse().map(Self::ExtXEndList)
|
||||||
} else if input.starts_with(tags::ExtXPlaylistType::PREFIX) {
|
} else if input.starts_with(PlaylistType::PREFIX) {
|
||||||
input.parse().map(Self::ExtXPlaylistType)
|
input.parse().map(Self::PlaylistType)
|
||||||
} else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
} else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
||||||
input.parse().map(Self::ExtXIFramesOnly)
|
input.parse().map(Self::ExtXIFramesOnly)
|
||||||
} else if input.starts_with(tags::ExtXMedia::PREFIX) {
|
} else if input.starts_with(tags::ExtXMedia::PREFIX) {
|
||||||
input.parse().map(Self::ExtXMedia).map_err(Error::custom)
|
input.parse().map(Self::ExtXMedia)
|
||||||
} else if input.starts_with(tags::ExtXStreamInf::PREFIX) {
|
} else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME)
|
||||||
input.parse().map(Self::ExtXStreamInf)
|
|| input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF)
|
||||||
} else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) {
|
{
|
||||||
input.parse().map(Self::ExtXIFrameStreamInf)
|
input.parse().map(Self::VariantStream)
|
||||||
} else if input.starts_with(tags::ExtXSessionData::PREFIX) {
|
} else if input.starts_with(tags::ExtXSessionData::PREFIX) {
|
||||||
input.parse().map(Self::ExtXSessionData)
|
input.parse().map(Self::ExtXSessionData)
|
||||||
} else if input.starts_with(tags::ExtXSessionKey::PREFIX) {
|
} else if input.starts_with(tags::ExtXSessionKey::PREFIX) {
|
||||||
|
@ -192,7 +129,7 @@ impl FromStr for Tag {
|
||||||
} else if input.starts_with(tags::ExtXStart::PREFIX) {
|
} else if input.starts_with(tags::ExtXStart::PREFIX) {
|
||||||
input.parse().map(Self::ExtXStart)
|
input.parse().map(Self::ExtXStart)
|
||||||
} else {
|
} else {
|
||||||
Ok(Self::Unknown(input.to_string()))
|
Ok(Self::Unknown(input))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -7,59 +8,133 @@ use derive_builder::Builder;
|
||||||
use crate::line::{Line, Lines, Tag};
|
use crate::line::{Line, Lines, Tag};
|
||||||
use crate::media_segment::MediaSegment;
|
use crate::media_segment::MediaSegment;
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
|
ExtM3u, ExtXByteRange, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly,
|
||||||
ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion,
|
ExtXIndependentSegments, ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration,
|
||||||
|
ExtXVersion,
|
||||||
};
|
};
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::{
|
||||||
use crate::{Encrypted, Error, RequiredVersion};
|
DecryptionKey, EncryptionMethod, InitializationVector, KeyFormat, PlaylistType, ProtocolVersion,
|
||||||
|
};
|
||||||
|
use crate::utils::{tag, BoolExt};
|
||||||
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// Media playlist.
|
/// Media playlist.
|
||||||
#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)]
|
#[derive(Builder, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[builder(build_fn(validate = "Self::validate"))]
|
#[builder(build_fn(skip), setter(strip_option))]
|
||||||
#[builder(setter(into, strip_option))]
|
#[non_exhaustive]
|
||||||
pub struct MediaPlaylist {
|
pub struct MediaPlaylist {
|
||||||
/// Sets the [`ExtXTargetDuration`] tag.
|
/// Specifies the maximum [`MediaSegment::duration`]. A typical target
|
||||||
target_duration_tag: ExtXTargetDuration,
|
/// duration is 10 seconds.
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is required.
|
||||||
|
pub target_duration: Duration,
|
||||||
|
/// The [`MediaSegment::number`] of the first [`MediaSegment`] that
|
||||||
|
/// appears in a [`MediaPlaylist`].
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional and by default a value of 0 is assumed.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets the [`ExtXMediaSequence`] tag.
|
pub media_sequence: usize,
|
||||||
media_sequence_tag: Option<ExtXMediaSequence>,
|
/// Allows synchronization between different renditions of the same
|
||||||
|
/// [`VariantStream`].
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional and by default a vaule of 0 is assumed.
|
||||||
|
///
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets the [`ExtXDiscontinuitySequence`] tag.
|
pub discontinuity_sequence: usize,
|
||||||
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
|
/// Provides mutability information about a [`MediaPlaylist`].
|
||||||
|
///
|
||||||
|
/// - [`PlaylistType::Vod`] indicates that the playlist must not change.
|
||||||
|
///
|
||||||
|
/// - [`PlaylistType::Event`] indicates that the server does not change or
|
||||||
|
/// delete any part of the playlist, but may append new lines to it.
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
pub playlist_type: Option<PlaylistType>,
|
||||||
|
/// Indicates that each [`MediaSegment`] in the playlist describes a single
|
||||||
|
/// I-frame. I-frames are encoded video frames, whose decoding does not
|
||||||
|
/// depend on any other frame. I-frame Playlists can be used for trick
|
||||||
|
/// play, such as fast forward, rapid reverse, and scrubbing.
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets the [`ExtXPlaylistType`] tag.
|
pub has_i_frames_only: bool,
|
||||||
playlist_type_tag: Option<ExtXPlaylistType>,
|
/// This indicates that all media samples in a [`MediaSegment`] can be
|
||||||
|
/// decoded without information from other segments.
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional and by default `false`. If the value is `true` it
|
||||||
|
/// applies to every [`MediaSegment`] in this [`MediaPlaylist`].
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets the [`ExtXIFramesOnly`] tag.
|
pub has_independent_segments: bool,
|
||||||
i_frames_only_tag: Option<ExtXIFramesOnly>,
|
/// Indicates a preferred point at which to start playing a playlist. By
|
||||||
|
/// default, clients should start playback at this point when beginning a
|
||||||
|
/// playback session.
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
pub start: Option<ExtXStart>,
|
||||||
|
/// Indicates that no more [`MediaSegment`]s will be added to the
|
||||||
|
/// [`MediaPlaylist`] file.
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional and by default `false`.
|
||||||
|
/// A `false` indicates that the client should reload the [`MediaPlaylist`]
|
||||||
|
/// from the server, until a playlist is encountered, where this field is
|
||||||
|
/// `true`.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets the [`ExtXIndependentSegments`] tag.
|
pub has_end_list: bool,
|
||||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
/// A list of all [`MediaSegment`]s.
|
||||||
#[builder(default)]
|
///
|
||||||
/// Sets the [`ExtXStart`] tag.
|
/// ### Note
|
||||||
start_tag: Option<ExtXStart>,
|
///
|
||||||
#[builder(default)]
|
/// This field is required.
|
||||||
/// Sets the [`ExtXEndList`] tag.
|
#[builder(setter(custom))]
|
||||||
end_list_tag: Option<ExtXEndList>,
|
pub segments: BTreeMap<usize, MediaSegment>,
|
||||||
/// Sets all [`MediaSegment`]s.
|
/// The allowable excess duration of each media segment in the
|
||||||
segments: Vec<MediaSegment>,
|
|
||||||
/// Sets the allowable excess duration of each media segment in the
|
|
||||||
/// associated playlist.
|
/// associated playlist.
|
||||||
///
|
///
|
||||||
/// # Error
|
/// ### Error
|
||||||
|
///
|
||||||
/// If there is a media segment of which duration exceeds
|
/// If there is a media segment of which duration exceeds
|
||||||
/// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
|
/// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
|
||||||
/// the invocation of `MediaPlaylistBuilder::build()` method will fail.
|
/// the invocation of `MediaPlaylistBuilder::build()` method will fail.
|
||||||
///
|
///
|
||||||
/// The default value is `Duration::from_secs(0)`.
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional and the default value is
|
||||||
|
/// `Duration::from_secs(0)`.
|
||||||
#[builder(default = "Duration::from_secs(0)")]
|
#[builder(default = "Duration::from_secs(0)")]
|
||||||
allowable_excess_duration: Duration,
|
pub allowable_excess_duration: Duration,
|
||||||
|
/// A list of unknown tags.
|
||||||
|
///
|
||||||
|
/// ### Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
pub unknown: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaPlaylistBuilder {
|
impl MediaPlaylistBuilder {
|
||||||
fn validate(&self) -> Result<(), String> {
|
fn validate(&self) -> Result<(), String> {
|
||||||
if let Some(target_duration) = &self.target_duration_tag {
|
if let Some(target_duration) = &self.target_duration {
|
||||||
self.validate_media_segments(target_duration.duration())
|
self.validate_media_segments(*target_duration)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,25 +143,55 @@ impl MediaPlaylistBuilder {
|
||||||
|
|
||||||
fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> {
|
fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> {
|
||||||
let mut last_range_uri = None;
|
let mut last_range_uri = None;
|
||||||
if let Some(segments) = &self.segments {
|
|
||||||
for s in segments {
|
|
||||||
// CHECK: `#EXT-X-TARGETDURATION`
|
|
||||||
let segment_duration = s.inf_tag().duration();
|
|
||||||
let rounded_segment_duration = {
|
|
||||||
if segment_duration.subsec_nanos() < 500_000_000 {
|
|
||||||
Duration::from_secs(segment_duration.as_secs())
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(segment_duration.as_secs() + 1)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let max_segment_duration = {
|
if let Some(segments) = &self.segments {
|
||||||
if let Some(value) = &self.allowable_excess_duration {
|
// verify the independent segments
|
||||||
target_duration + *value
|
if self.has_independent_segments.unwrap_or(false) {
|
||||||
} else {
|
// If the encryption METHOD is AES-128 and the Playlist contains an EXT-
|
||||||
target_duration
|
// X-I-FRAMES-ONLY tag, the entire resource MUST be encrypted using
|
||||||
|
// AES-128 CBC with PKCS7 padding [RFC5652].
|
||||||
|
//
|
||||||
|
// from the rfc: https://tools.ietf.org/html/rfc8216#section-6.2.3
|
||||||
|
|
||||||
|
let is_aes128 = segments
|
||||||
|
.values()
|
||||||
|
// convert iterator of segments to iterator of keys
|
||||||
|
.flat_map(|s| s.keys.iter())
|
||||||
|
// filter out all empty keys
|
||||||
|
.filter_map(ExtXKey::as_ref)
|
||||||
|
.any(|k| k.method == EncryptionMethod::Aes128);
|
||||||
|
|
||||||
|
if is_aes128 {
|
||||||
|
for key in segments.values().flat_map(|s| s.keys.iter()) {
|
||||||
|
if let ExtXKey(Some(key)) = key {
|
||||||
|
if key.method != EncryptionMethod::Aes128 {
|
||||||
|
return Err(Error::custom(concat!(
|
||||||
|
"if any independent segment is encrypted with Aes128,",
|
||||||
|
" all must be encrypted with Aes128"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::custom(concat!(
|
||||||
|
"if any independent segment is encrypted with Aes128,",
|
||||||
|
" all must be encrypted with Aes128"
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for segment in segments.values() {
|
||||||
|
// CHECK: `#EXT-X-TARGETDURATION`
|
||||||
|
let segment_duration = segment.duration.duration();
|
||||||
|
|
||||||
|
// round the duration if it is .5s
|
||||||
|
let rounded_segment_duration =
|
||||||
|
Duration::from_secs(segment_duration.as_secs_f64().round() as u64);
|
||||||
|
|
||||||
|
let max_segment_duration = self
|
||||||
|
.allowable_excess_duration
|
||||||
|
.as_ref()
|
||||||
|
.map_or(target_duration, |value| target_duration + *value);
|
||||||
|
|
||||||
if rounded_segment_duration > max_segment_duration {
|
if rounded_segment_duration > max_segment_duration {
|
||||||
return Err(Error::custom(format!(
|
return Err(Error::custom(format!(
|
||||||
|
@ -94,35 +199,43 @@ impl MediaPlaylistBuilder {
|
||||||
segment_duration,
|
segment_duration,
|
||||||
max_segment_duration,
|
max_segment_duration,
|
||||||
target_duration,
|
target_duration,
|
||||||
s.uri()
|
segment.uri()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHECK: `#EXT-X-BYTE-RANGE`
|
// CHECK: `#EXT-X-BYTE-RANGE`
|
||||||
if let Some(tag) = s.byte_range_tag() {
|
if let Some(range) = &segment.byte_range {
|
||||||
if tag.to_range().start().is_none() {
|
if range.start().is_none() {
|
||||||
let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?;
|
// TODO: error messages
|
||||||
if last_uri != s.uri() {
|
if last_range_uri.ok_or_else(Error::invalid_input)? != segment.uri() {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::invalid_input());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
last_range_uri = Some(s.uri());
|
last_range_uri = Some(segment.uri());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
last_range_uri = None;
|
last_range_uri = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a media segment to the resulting playlist.
|
/// Adds a media segment to the resulting playlist and assigns the next free
|
||||||
pub fn push_segment<VALUE: Into<MediaSegment>>(&mut self, value: VALUE) -> &mut Self {
|
/// [`MediaSegment::number`] to the segment.
|
||||||
if let Some(segments) = &mut self.segments {
|
pub fn push_segment(&mut self, segment: MediaSegment) -> &mut Self {
|
||||||
segments.push(value.into());
|
let segments = self.segments.get_or_insert_with(BTreeMap::new);
|
||||||
} else {
|
|
||||||
self.segments = Some(vec![value.into()]);
|
let number = {
|
||||||
}
|
if segment.explicit_number {
|
||||||
|
segment.number
|
||||||
|
} else {
|
||||||
|
segments.keys().last().copied().unwrap_or(0) + 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
segments.insert(number, segment);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,19 +243,157 @@ impl MediaPlaylistBuilder {
|
||||||
pub fn parse(&mut self, input: &str) -> crate::Result<MediaPlaylist> {
|
pub fn parse(&mut self, input: &str) -> crate::Result<MediaPlaylist> {
|
||||||
parse_media_playlist(input, self)
|
parse_media_playlist(input, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds segments to the resulting playlist and assigns a
|
||||||
|
/// [`MediaSegment::number`] to each segment.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// The [`MediaSegment::number`] will be assigned based on the order of the
|
||||||
|
/// input (e.g. the first element will be 0, second element 1, ..) or if a
|
||||||
|
/// number has been set explicitly. This function assumes, that all segments
|
||||||
|
/// will be present in the final media playlist and the following is only
|
||||||
|
/// possible if the segment is marked with `ExtXDiscontinuity`.
|
||||||
|
pub fn segments(&mut self, segments: Vec<MediaSegment>) -> &mut Self {
|
||||||
|
// media segments are numbered starting at either 0 or the discontinuity
|
||||||
|
// sequence, but it might not be available at the moment.
|
||||||
|
//
|
||||||
|
// -> final numbering will be applied in the build function
|
||||||
|
self.segments = Some(segments.into_iter().enumerate().collect());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a new `MediaPlaylist`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If a required field has not been initialized.
|
||||||
|
pub fn build(&self) -> Result<MediaPlaylist, String> {
|
||||||
|
// validate builder
|
||||||
|
self.validate()?;
|
||||||
|
|
||||||
|
let sequence_number = self.media_sequence.unwrap_or(0);
|
||||||
|
|
||||||
|
let segments = self
|
||||||
|
.segments
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| "missing field `segments`".to_string())?;
|
||||||
|
|
||||||
|
// insert all explictly numbered segments into the result
|
||||||
|
let mut result_segments = segments
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, s)| s.explicit_number.athen(|| (s.number, s.clone())))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
// no segment should exist before the sequence_number
|
||||||
|
if let Some(first_segment) = result_segments.keys().min() {
|
||||||
|
if sequence_number > *first_segment {
|
||||||
|
return Err(format!(
|
||||||
|
"there should be no segment ({}) before the sequence_number ({})",
|
||||||
|
first_segment, sequence_number,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut position = sequence_number;
|
||||||
|
let mut previous_range: Option<ExtXByteRange> = None;
|
||||||
|
|
||||||
|
for segment in segments
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, s)| if s.explicit_number { None } else { Some(s) })
|
||||||
|
{
|
||||||
|
while result_segments.contains_key(&position) {
|
||||||
|
position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut segment = segment.clone();
|
||||||
|
segment.number = position;
|
||||||
|
|
||||||
|
// add the segment number as iv, if the iv is missing:
|
||||||
|
for key in &mut segment.keys {
|
||||||
|
if let ExtXKey(Some(DecryptionKey {
|
||||||
|
method, iv, format, ..
|
||||||
|
})) = key
|
||||||
|
{
|
||||||
|
if *method == EncryptionMethod::Aes128 && *iv == InitializationVector::Missing {
|
||||||
|
if format.is_none() {
|
||||||
|
*iv = InitializationVector::Number(segment.number as u128);
|
||||||
|
} else if let Some(KeyFormat::Identity) = format {
|
||||||
|
*iv = InitializationVector::Number(segment.number as u128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the lower bound to the byterange automatically
|
||||||
|
if let Some(range) = &mut segment.byte_range {
|
||||||
|
if range.start().is_none() {
|
||||||
|
if let Some(previous_range) = previous_range {
|
||||||
|
// the end of the previous_range is the start of the next range
|
||||||
|
*range = range.saturating_add(previous_range.end());
|
||||||
|
range.set_start(Some(previous_range.end()));
|
||||||
|
} else {
|
||||||
|
// assume that the byte range starts at zero
|
||||||
|
range.set_start(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_range = segment.byte_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
result_segments.insert(segment.number, segment);
|
||||||
|
position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut previous_n = None;
|
||||||
|
|
||||||
|
for n in result_segments.keys() {
|
||||||
|
if let Some(previous_n) = previous_n {
|
||||||
|
if previous_n + 1 != *n {
|
||||||
|
return Err(format!("missing segment ({})", previous_n + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_n = Some(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(MediaPlaylist {
|
||||||
|
target_duration: self
|
||||||
|
.target_duration
|
||||||
|
.ok_or_else(|| "missing field `target_duration`".to_string())?,
|
||||||
|
media_sequence: self.media_sequence.unwrap_or(0),
|
||||||
|
discontinuity_sequence: self.discontinuity_sequence.unwrap_or(0),
|
||||||
|
playlist_type: self.playlist_type.unwrap_or(None),
|
||||||
|
has_i_frames_only: self.has_i_frames_only.unwrap_or(false),
|
||||||
|
has_independent_segments: self.has_independent_segments.unwrap_or(false),
|
||||||
|
start: self.start.unwrap_or(None),
|
||||||
|
has_end_list: self.has_end_list.unwrap_or(false),
|
||||||
|
segments: result_segments,
|
||||||
|
allowable_excess_duration: self
|
||||||
|
.allowable_excess_duration
|
||||||
|
.unwrap_or_else(|| Duration::from_secs(0)),
|
||||||
|
unknown: self.unknown.clone().unwrap_or_else(Vec::new),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiredVersion for MediaPlaylistBuilder {
|
impl RequiredVersion for MediaPlaylistBuilder {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
required_version![
|
required_version![
|
||||||
self.target_duration_tag,
|
self.target_duration.map(ExtXTargetDuration),
|
||||||
self.media_sequence_tag,
|
(self.media_sequence.unwrap_or(0) != 0)
|
||||||
self.discontinuity_sequence_tag,
|
.athen(|| ExtXMediaSequence(self.media_sequence.unwrap_or(0))),
|
||||||
self.playlist_type_tag,
|
(self.discontinuity_sequence.unwrap_or(0) != 0)
|
||||||
self.i_frames_only_tag,
|
.athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence.unwrap_or(0))),
|
||||||
self.independent_segments_tag,
|
self.playlist_type,
|
||||||
self.start_tag,
|
self.has_i_frames_only
|
||||||
self.end_list_tag,
|
.unwrap_or(false)
|
||||||
|
.athen_some(ExtXIFramesOnly),
|
||||||
|
self.has_independent_segments
|
||||||
|
.unwrap_or(false)
|
||||||
|
.athen_some(ExtXIndependentSegments),
|
||||||
|
self.start,
|
||||||
|
self.has_end_list.unwrap_or(false).athen_some(ExtXEndList),
|
||||||
self.segments
|
self.segments
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -150,88 +401,137 @@ impl RequiredVersion for MediaPlaylistBuilder {
|
||||||
|
|
||||||
impl MediaPlaylist {
|
impl MediaPlaylist {
|
||||||
/// Returns a builder for [`MediaPlaylist`].
|
/// Returns a builder for [`MediaPlaylist`].
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() }
|
pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() }
|
||||||
|
|
||||||
/// Returns the [`ExtXTargetDuration`] tag contained in the playlist.
|
/// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment
|
||||||
pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag }
|
/// duration together.
|
||||||
|
#[must_use]
|
||||||
/// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist.
|
pub fn duration(&self) -> Duration {
|
||||||
pub const fn media_sequence_tag(&self) -> Option<ExtXMediaSequence> { self.media_sequence_tag }
|
self.segments.values().map(|s| s.duration.duration()).sum()
|
||||||
|
|
||||||
/// Returns the [`ExtXDiscontinuitySequence`] tag contained in the
|
|
||||||
/// playlist.
|
|
||||||
pub const fn discontinuity_sequence_tag(&self) -> Option<ExtXDiscontinuitySequence> {
|
|
||||||
self.discontinuity_sequence_tag
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`ExtXPlaylistType`] tag contained in the playlist.
|
|
||||||
pub const fn playlist_type_tag(&self) -> Option<ExtXPlaylistType> { self.playlist_type_tag }
|
|
||||||
|
|
||||||
/// Returns the [`ExtXIFramesOnly`] tag contained in the playlist.
|
|
||||||
pub const fn i_frames_only_tag(&self) -> Option<ExtXIFramesOnly> { self.i_frames_only_tag }
|
|
||||||
|
|
||||||
/// Returns the [`ExtXIndependentSegments`] tag contained in the playlist.
|
|
||||||
pub const fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
|
||||||
self.independent_segments_tag
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`ExtXStart`] tag contained in the playlist.
|
|
||||||
pub const fn start_tag(&self) -> Option<ExtXStart> { self.start_tag }
|
|
||||||
|
|
||||||
/// Returns the [`ExtXEndList`] tag contained in the playlist.
|
|
||||||
pub const fn end_list_tag(&self) -> Option<ExtXEndList> { self.end_list_tag }
|
|
||||||
|
|
||||||
/// Returns the [`MediaSegment`]s contained in the playlist.
|
|
||||||
pub const fn segments(&self) -> &Vec<MediaSegment> { &self.segments }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiredVersion for MediaPlaylist {
|
impl RequiredVersion for MediaPlaylist {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
required_version![
|
required_version![
|
||||||
self.target_duration_tag,
|
ExtXTargetDuration(self.target_duration),
|
||||||
self.media_sequence_tag,
|
(self.media_sequence != 0).athen(|| ExtXMediaSequence(self.media_sequence)),
|
||||||
self.discontinuity_sequence_tag,
|
(self.discontinuity_sequence != 0)
|
||||||
self.playlist_type_tag,
|
.athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence)),
|
||||||
self.i_frames_only_tag,
|
self.playlist_type,
|
||||||
self.independent_segments_tag,
|
self.has_i_frames_only.athen_some(ExtXIFramesOnly),
|
||||||
self.start_tag,
|
self.has_independent_segments
|
||||||
self.end_list_tag,
|
.athen_some(ExtXIndependentSegments),
|
||||||
|
self.start,
|
||||||
|
self.has_end_list.athen_some(ExtXEndList),
|
||||||
self.segments
|
self.segments
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MediaPlaylist {
|
impl fmt::Display for MediaPlaylist {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "{}", ExtM3u)?;
|
writeln!(f, "{}", ExtM3u)?;
|
||||||
|
|
||||||
if self.required_version() != ProtocolVersion::V1 {
|
if self.required_version() != ProtocolVersion::V1 {
|
||||||
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
|
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
|
||||||
}
|
}
|
||||||
writeln!(f, "{}", self.target_duration_tag)?;
|
|
||||||
if let Some(value) = &self.media_sequence_tag {
|
writeln!(f, "{}", ExtXTargetDuration(self.target_duration))?;
|
||||||
|
|
||||||
|
if self.media_sequence != 0 {
|
||||||
|
writeln!(f, "{}", ExtXMediaSequence(self.media_sequence))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.discontinuity_sequence != 0 {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
ExtXDiscontinuitySequence(self.discontinuity_sequence)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = &self.playlist_type {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(value) = &self.discontinuity_sequence_tag {
|
|
||||||
|
if self.has_i_frames_only {
|
||||||
|
writeln!(f, "{}", ExtXIFramesOnly)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.has_independent_segments {
|
||||||
|
writeln!(f, "{}", ExtXIndependentSegments)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = &self.start {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(value) = &self.playlist_type_tag {
|
|
||||||
writeln!(f, "{}", value)?;
|
let mut available_keys = HashSet::<ExtXKey>::new();
|
||||||
}
|
|
||||||
if let Some(value) = &self.i_frames_only_tag {
|
for segment in self.segments.values() {
|
||||||
writeln!(f, "{}", value)?;
|
for key in &segment.keys {
|
||||||
}
|
if let ExtXKey(Some(decryption_key)) = key {
|
||||||
if let Some(value) = &self.independent_segments_tag {
|
// next segment will be encrypted, so the segment can not have an empty key
|
||||||
writeln!(f, "{}", value)?;
|
available_keys.remove(&ExtXKey::empty());
|
||||||
}
|
|
||||||
if let Some(value) = &self.start_tag {
|
let mut decryption_key = decryption_key.clone();
|
||||||
writeln!(f, "{}", value)?;
|
let key = {
|
||||||
}
|
if let InitializationVector::Number(_) = decryption_key.iv {
|
||||||
for segment in &self.segments {
|
// set the iv from a segment number to missing
|
||||||
|
// this does reduce the output size and the correct iv
|
||||||
|
// is automatically set, when parsing.
|
||||||
|
decryption_key.iv = InitializationVector::Missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtXKey(Some(decryption_key.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
// only do something if a key has been overwritten
|
||||||
|
if available_keys.insert(key.clone()) {
|
||||||
|
let mut remove_key = None;
|
||||||
|
|
||||||
|
// an old key might be removed:
|
||||||
|
for k in &available_keys {
|
||||||
|
if let ExtXKey(Some(dk)) = k {
|
||||||
|
if dk.format == decryption_key.format && key != *k {
|
||||||
|
remove_key = Some(k.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!("empty keys should not exist in `available_keys`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(k) = remove_key {
|
||||||
|
// this should always be true:
|
||||||
|
let res = available_keys.remove(&k);
|
||||||
|
debug_assert!(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f, "{}", key)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the next segment is not encrypted, so remove all available keys
|
||||||
|
available_keys.clear();
|
||||||
|
available_keys.insert(ExtXKey::empty());
|
||||||
|
writeln!(f, "{}", key)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
write!(f, "{}", segment)?;
|
write!(f, "{}", segment)?;
|
||||||
}
|
}
|
||||||
if let Some(value) = &self.end_list_tag {
|
|
||||||
|
for value in &self.unknown {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.has_end_list {
|
||||||
|
writeln!(f, "{}", ExtXEndList)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,132 +540,154 @@ fn parse_media_playlist(
|
||||||
input: &str,
|
input: &str,
|
||||||
builder: &mut MediaPlaylistBuilder,
|
builder: &mut MediaPlaylistBuilder,
|
||||||
) -> crate::Result<MediaPlaylist> {
|
) -> crate::Result<MediaPlaylist> {
|
||||||
|
let input = tag(input, "#EXTM3U")?;
|
||||||
|
|
||||||
let mut segment = MediaSegment::builder();
|
let mut segment = MediaSegment::builder();
|
||||||
let mut segments = vec![];
|
let mut segments = vec![];
|
||||||
|
|
||||||
let mut has_partial_segment = false;
|
let mut has_partial_segment = false;
|
||||||
let mut has_discontinuity_tag = false;
|
let mut has_discontinuity_tag = false;
|
||||||
|
let mut unknown = vec![];
|
||||||
|
let mut available_keys = HashSet::new();
|
||||||
|
|
||||||
let mut available_key_tags: Vec<crate::tags::ExtXKey> = vec![];
|
for line in Lines::from(input) {
|
||||||
|
match line? {
|
||||||
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
|
|
||||||
match line {
|
|
||||||
Line::Tag(tag) => {
|
Line::Tag(tag) => {
|
||||||
if i == 0 {
|
|
||||||
if tag != Tag::ExtM3u(ExtM3u) {
|
|
||||||
return Err(Error::custom("m3u8 doesn't start with #EXTM3U"));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match tag {
|
match tag {
|
||||||
Tag::ExtM3u(_) => return Err(Error::invalid_input()),
|
|
||||||
Tag::ExtInf(t) => {
|
Tag::ExtInf(t) => {
|
||||||
has_partial_segment = true;
|
has_partial_segment = true;
|
||||||
segment.inf_tag(t);
|
segment.duration(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXByteRange(t) => {
|
Tag::ExtXByteRange(t) => {
|
||||||
has_partial_segment = true;
|
has_partial_segment = true;
|
||||||
segment.byte_range_tag(t);
|
segment.byte_range(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXDiscontinuity(t) => {
|
Tag::ExtXDiscontinuity(_) => {
|
||||||
has_discontinuity_tag = true;
|
has_discontinuity_tag = true;
|
||||||
has_partial_segment = true;
|
has_partial_segment = true;
|
||||||
segment.discontinuity_tag(t);
|
segment.has_discontinuity(true);
|
||||||
}
|
}
|
||||||
Tag::ExtXKey(t) => {
|
Tag::ExtXKey(key) => {
|
||||||
has_partial_segment = true;
|
has_partial_segment = true;
|
||||||
if available_key_tags.is_empty() {
|
|
||||||
// An ExtXKey applies to every MediaSegment and to every Media
|
// An ExtXKey applies to every MediaSegment and to every Media
|
||||||
// Initialization Section declared by an EXT-X-MAP tag, that appears
|
// Initialization Section declared by an ExtXMap tag, that appears
|
||||||
// between it and the next EXT-X-KEY tag in the Playlist file with the
|
// between it and the next ExtXKey tag in the Playlist file with the
|
||||||
// same KEYFORMAT attribute (or the end of the Playlist file).
|
// same KEYFORMAT attribute (or the end of the Playlist file).
|
||||||
available_key_tags = available_key_tags
|
|
||||||
.into_iter()
|
let mut is_new_key = true;
|
||||||
.map(|k| {
|
let mut remove = None;
|
||||||
if t.key_format() == k.key_format() {
|
|
||||||
t.clone()
|
if let ExtXKey(Some(decryption_key)) = &key {
|
||||||
} else {
|
for old_key in &available_keys {
|
||||||
k
|
if let ExtXKey(Some(old_decryption_key)) = &old_key {
|
||||||
|
if old_decryption_key.format == decryption_key.format {
|
||||||
|
// remove the old key
|
||||||
|
remove = Some(old_key.clone());
|
||||||
|
|
||||||
|
// there are no keys with the same format in
|
||||||
|
// available_keys so the loop can stop here:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.collect();
|
// remove an empty key
|
||||||
|
remove = Some(ExtXKey::empty());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
available_key_tags.push(t);
|
available_keys.clear();
|
||||||
|
available_keys.insert(ExtXKey::empty());
|
||||||
|
is_new_key = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = &remove {
|
||||||
|
available_keys.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_new_key {
|
||||||
|
available_keys.insert(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tag::ExtXMap(mut t) => {
|
Tag::ExtXMap(mut t) => {
|
||||||
has_partial_segment = true;
|
has_partial_segment = true;
|
||||||
|
|
||||||
t.set_keys(available_key_tags.clone());
|
t.keys = available_keys.iter().cloned().collect();
|
||||||
segment.map_tag(t);
|
segment.map(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXProgramDateTime(t) => {
|
Tag::ExtXProgramDateTime(t) => {
|
||||||
has_partial_segment = true;
|
has_partial_segment = true;
|
||||||
segment.program_date_time_tag(t);
|
segment.program_date_time(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXDateRange(t) => {
|
Tag::ExtXDateRange(t) => {
|
||||||
has_partial_segment = true;
|
has_partial_segment = true;
|
||||||
segment.date_range_tag(t);
|
segment.date_range(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXTargetDuration(t) => {
|
Tag::ExtXTargetDuration(t) => {
|
||||||
builder.target_duration_tag(t);
|
builder.target_duration(t.0);
|
||||||
}
|
}
|
||||||
Tag::ExtXMediaSequence(t) => {
|
Tag::ExtXMediaSequence(t) => {
|
||||||
builder.media_sequence_tag(t);
|
builder.media_sequence(t.0);
|
||||||
}
|
}
|
||||||
Tag::ExtXDiscontinuitySequence(t) => {
|
Tag::ExtXDiscontinuitySequence(t) => {
|
||||||
if segments.is_empty() {
|
if segments.is_empty() {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::invalid_input());
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_discontinuity_tag {
|
if has_discontinuity_tag {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::invalid_input());
|
||||||
}
|
}
|
||||||
builder.discontinuity_sequence_tag(t);
|
|
||||||
|
builder.discontinuity_sequence(t.0);
|
||||||
}
|
}
|
||||||
Tag::ExtXEndList(t) => {
|
Tag::ExtXEndList(_) => {
|
||||||
builder.end_list_tag(t);
|
builder.has_end_list(true);
|
||||||
}
|
}
|
||||||
Tag::ExtXPlaylistType(t) => {
|
Tag::PlaylistType(t) => {
|
||||||
builder.playlist_type_tag(t);
|
builder.playlist_type(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXIFramesOnly(t) => {
|
Tag::ExtXIFramesOnly(_) => {
|
||||||
builder.i_frames_only_tag(t);
|
builder.has_i_frames_only(true);
|
||||||
}
|
}
|
||||||
Tag::ExtXMedia(_)
|
Tag::ExtXMedia(_)
|
||||||
| Tag::ExtXStreamInf(_)
|
| Tag::VariantStream(_)
|
||||||
| Tag::ExtXIFrameStreamInf(_)
|
|
||||||
| Tag::ExtXSessionData(_)
|
| Tag::ExtXSessionData(_)
|
||||||
| Tag::ExtXSessionKey(_) => {
|
| Tag::ExtXSessionKey(_) => {
|
||||||
return Err(Error::unexpected_tag(tag));
|
return Err(Error::unexpected_tag(tag));
|
||||||
}
|
}
|
||||||
Tag::ExtXIndependentSegments(t) => {
|
Tag::ExtXIndependentSegments(_) => {
|
||||||
builder.independent_segments_tag(t);
|
builder.has_independent_segments(true);
|
||||||
}
|
}
|
||||||
Tag::ExtXStart(t) => {
|
Tag::ExtXStart(t) => {
|
||||||
builder.start_tag(t);
|
builder.start(t);
|
||||||
}
|
}
|
||||||
Tag::Unknown(_) | Tag::ExtXVersion(_) => {
|
Tag::ExtXVersion(_) => {}
|
||||||
|
Tag::Unknown(_) => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any unrecognized tags.
|
// > ignore any unrecognized tags.
|
||||||
|
unknown.push(tag.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Line::Uri(uri) => {
|
Line::Uri(uri) => {
|
||||||
segment.uri(uri);
|
segment.uri(uri);
|
||||||
segment.keys(available_key_tags.clone());
|
segment.keys(available_keys.iter().cloned().collect::<Vec<_>>());
|
||||||
segments.push(segment.build().map_err(Error::builder_error)?);
|
segments.push(segment.build().map_err(Error::builder)?);
|
||||||
|
|
||||||
segment = MediaSegment::builder();
|
segment = MediaSegment::builder();
|
||||||
has_partial_segment = false;
|
has_partial_segment = false;
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_partial_segment {
|
if has_partial_segment {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::custom("Missing URI for the last `MediaSegment`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.unknown(unknown);
|
||||||
builder.segments(segments);
|
builder.segments(segments);
|
||||||
builder.build().map_err(Error::builder_error)
|
builder.build().map_err(Error::builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for MediaPlaylist {
|
impl FromStr for MediaPlaylist {
|
||||||
|
@ -383,17 +705,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn too_large_segment_duration_test() {
|
fn too_large_segment_duration_test() {
|
||||||
let playlist = r#"
|
let playlist = concat!(
|
||||||
#EXTM3U
|
"#EXTM3U\n",
|
||||||
#EXT-X-TARGETDURATION:8
|
"#EXT-X-TARGETDURATION:8\n",
|
||||||
#EXT-X-VERSION:3
|
"#EXT-X-VERSION:3\n",
|
||||||
#EXTINF:9.009,
|
"#EXTINF:9.009,\n",
|
||||||
http://media.example.com/first.ts
|
"http://media.example.com/first.ts\n",
|
||||||
#EXTINF:9.509,
|
"#EXTINF:9.509,\n",
|
||||||
http://media.example.com/second.ts
|
"http://media.example.com/second.ts\n",
|
||||||
#EXTINF:3.003,
|
"#EXTINF:3.003,\n",
|
||||||
http://media.example.com/third.ts
|
"http://media.example.com/third.ts\n",
|
||||||
#EXT-X-ENDLIST"#;
|
"#EXT-X-ENDLIST\n"
|
||||||
|
);
|
||||||
|
|
||||||
// Error (allowable segment duration = target duration = 8)
|
// Error (allowable segment duration = target duration = 8)
|
||||||
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
||||||
|
@ -405,10 +728,98 @@ mod tests {
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
// Ok (allowable segment duration = 10)
|
// Ok (allowable segment duration = 10)
|
||||||
MediaPlaylist::builder()
|
assert_eq!(
|
||||||
|
MediaPlaylist::builder()
|
||||||
|
.allowable_excess_duration(Duration::from_secs(2))
|
||||||
|
.parse(playlist)
|
||||||
|
.unwrap(),
|
||||||
|
MediaPlaylist::builder()
|
||||||
|
.allowable_excess_duration(Duration::from_secs(2))
|
||||||
|
.target_duration(Duration::from_secs(8))
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(9.009))
|
||||||
|
.uri("http://media.example.com/first.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(9.509))
|
||||||
|
.uri("http://media.example.com/second.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(3.003))
|
||||||
|
.uri("http://media.example.com/third.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.has_end_list(true)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_segment_number_simple() {
|
||||||
|
let playlist = MediaPlaylist::builder()
|
||||||
.allowable_excess_duration(Duration::from_secs(2))
|
.allowable_excess_duration(Duration::from_secs(2))
|
||||||
.parse(playlist)
|
.target_duration(Duration::from_secs(8))
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(9.009))
|
||||||
|
.uri("http://media.example.com/first.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(9.509))
|
||||||
|
.uri("http://media.example.com/second.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(3.003))
|
||||||
|
.uri("http://media.example.com/third.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
|
||||||
|
assert_eq!(segments.next(), Some((0, 0)));
|
||||||
|
assert_eq!(segments.next(), Some((1, 1)));
|
||||||
|
assert_eq!(segments.next(), Some((2, 2)));
|
||||||
|
assert_eq!(segments.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_segment_number_sequence() {
|
||||||
|
let playlist = MediaPlaylist::builder()
|
||||||
|
.target_duration(Duration::from_secs(8))
|
||||||
|
.media_sequence(2680)
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(7.975))
|
||||||
|
.uri("https://priv.example.com/fileSequence2680.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(7.941))
|
||||||
|
.uri("https://priv.example.com/fileSequence2681.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(Duration::from_secs_f64(7.975))
|
||||||
|
.uri("https://priv.example.com/fileSequence2682.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
|
||||||
|
assert_eq!(segments.next(), Some((2680, 2680)));
|
||||||
|
assert_eq!(segments.next(), Some((2681, 2681)));
|
||||||
|
assert_eq!(segments.next(), Some((2682, 2682)));
|
||||||
|
assert_eq!(segments.next(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,169 +1,231 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
||||||
};
|
};
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
use crate::{Encrypted, RequiredVersion};
|
use crate::{Decryptable, RequiredVersion};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)]
|
/// A video is split into smaller chunks called [`MediaSegment`]s, which are
|
||||||
#[builder(setter(into, strip_option))]
|
/// specified by a uri and optionally a byte range.
|
||||||
/// Media segment.
|
///
|
||||||
|
/// Each `MediaSegment` must carry the continuation of the encoded bitstream
|
||||||
|
/// from the end of the segment with the previous [`MediaSegment::number`],
|
||||||
|
/// where values in a series such as timestamps and continuity counters must
|
||||||
|
/// continue uninterrupted. The only exceptions are the first [`MediaSegment`]
|
||||||
|
/// ever to appear in a [`MediaPlaylist`] and [`MediaSegment`]s that are
|
||||||
|
/// explicitly signaled as discontinuities.
|
||||||
|
/// Unmarked media discontinuities can trigger playback errors.
|
||||||
|
///
|
||||||
|
/// Any `MediaSegment` that contains video should include enough information
|
||||||
|
/// to initialize a video decoder and decode a continuous set of frames that
|
||||||
|
/// includes the final frame in the segment; network efficiency is optimized if
|
||||||
|
/// there is enough information in the segment to decode all frames in the
|
||||||
|
/// segment.
|
||||||
|
///
|
||||||
|
/// For example, any `MediaSegment` containing H.264 video should
|
||||||
|
/// contain an Instantaneous Decoding Refresh (IDR); frames prior to the first
|
||||||
|
/// IDR will be downloaded but possibly discarded.
|
||||||
|
///
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[builder(setter(strip_option))]
|
||||||
|
#[shorthand(enable(must_use, skip))]
|
||||||
pub struct MediaSegment {
|
pub struct MediaSegment {
|
||||||
|
/// Each [`MediaSegment`] has a number, which allows synchronization between
|
||||||
|
/// different variants.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This number must not be specified, because it will be assigned
|
||||||
|
/// automatically by [`MediaPlaylistBuilder::segments`]. The first
|
||||||
|
/// [`MediaSegment::number`] in a [`MediaPlaylist`] will either be 0 or the
|
||||||
|
/// number returned by the [`ExtXDiscontinuitySequence`] if one is
|
||||||
|
/// provided.
|
||||||
|
/// The following segments will be the previous segment number + 1.
|
||||||
|
///
|
||||||
|
/// [`MediaPlaylistBuilder::segments`]:
|
||||||
|
/// crate::builder::MediaPlaylistBuilder::segments
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
/// [`ExtXMediaSequence`]: crate::tags::ExtXMediaSequence
|
||||||
|
/// [`ExtXDiscontinuitySequence`]: crate::tags::ExtXDiscontinuitySequence
|
||||||
|
#[builder(default, setter(custom))]
|
||||||
|
#[shorthand(disable(set, skip))]
|
||||||
|
pub(crate) number: usize,
|
||||||
|
#[builder(default, setter(custom))]
|
||||||
|
pub(crate) explicit_number: bool,
|
||||||
|
/// This field specifies how to decrypt a [`MediaSegment`], which can only
|
||||||
|
/// be encrypted with one [`EncryptionMethod`], using one [`DecryptionKey`]
|
||||||
|
/// and [`DecryptionKey::iv`].
|
||||||
|
///
|
||||||
|
/// However, a server may offer multiple ways to retrieve that key by
|
||||||
|
/// providing multiple keys with different [`DecryptionKey::format`]s.
|
||||||
|
///
|
||||||
|
/// Any unencrypted segment that is preceded by an encrypted segment must
|
||||||
|
/// have an [`ExtXKey::empty`]. Otherwise, the client will misinterpret
|
||||||
|
/// those segments as encrypted.
|
||||||
|
///
|
||||||
|
/// The server may set the HTTP Expires header in the key response to
|
||||||
|
/// indicate the duration for which the key can be cached.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional and a missing value or an [`ExtXKey::empty()`]
|
||||||
|
/// indicates an unencrypted media segment.
|
||||||
|
///
|
||||||
|
/// [`ExtXMap`]: crate::tags::ExtXMap
|
||||||
|
/// [`KeyFormat`]: crate::types::KeyFormat
|
||||||
|
/// [`EncryptionMethod`]: crate::types::EncryptionMethod
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
pub keys: Vec<ExtXKey>,
|
||||||
|
/// This field specifies how to obtain the Media Initialization Section
|
||||||
|
/// required to parse the applicable `MediaSegment`s.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional, but should be specified for media segments in
|
||||||
|
/// playlists with an [`ExtXIFramesOnly`] tag when the first `MediaSegment`
|
||||||
|
/// in the playlist (or the first segment following a segment marked with
|
||||||
|
/// [`MediaSegment::has_discontinuity`]) does not immediately follow the
|
||||||
|
/// Media Initialization Section at the beginning of its resource.
|
||||||
|
///
|
||||||
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets all [`ExtXKey`] tags.
|
pub map: Option<ExtXMap>,
|
||||||
keys: Vec<ExtXKey>,
|
/// This field indicates that a `MediaSegment` is a sub-range of the
|
||||||
|
/// resource identified by its URI.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
pub byte_range: Option<ExtXByteRange>,
|
||||||
|
/// This field associates a date-range (i.e., a range of time defined by a
|
||||||
|
/// starting and ending date) with a set of attribute/value pairs.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets an [`ExtXMap`] tag.
|
pub date_range: Option<ExtXDateRange>,
|
||||||
map_tag: Option<ExtXMap>,
|
/// This field indicates a discontinuity between the `MediaSegment` that
|
||||||
|
/// follows it and the one that preceded it.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is required if any of the following characteristics change:
|
||||||
|
/// - file format
|
||||||
|
/// - number, type, and identifiers of tracks
|
||||||
|
/// - timestamp, sequence
|
||||||
|
///
|
||||||
|
/// This field should be present if any of the following characteristics
|
||||||
|
/// change:
|
||||||
|
/// - encoding parameters
|
||||||
|
/// - encoding sequence
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets an [`ExtXByteRange`] tag.
|
pub has_discontinuity: bool,
|
||||||
byte_range_tag: Option<ExtXByteRange>,
|
/// This field associates the first sample of a media segment with an
|
||||||
|
/// absolute date and/or time.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
/// Sets an [`ExtXDateRange`] tag.
|
pub program_date_time: Option<ExtXProgramDateTime>,
|
||||||
date_range_tag: Option<ExtXDateRange>,
|
/// This field indicates the duration of a media segment.
|
||||||
#[builder(default)]
|
///
|
||||||
/// Sets an [`ExtXDiscontinuity`] tag.
|
/// ## Note
|
||||||
discontinuity_tag: Option<ExtXDiscontinuity>,
|
///
|
||||||
#[builder(default)]
|
/// This field is required.
|
||||||
/// Sets an [`ExtXProgramDateTime`] tag.
|
#[builder(setter(into))]
|
||||||
program_date_time_tag: Option<ExtXProgramDateTime>,
|
pub duration: ExtInf,
|
||||||
/// Sets an [`ExtInf`] tag.
|
/// The URI of a media segment.
|
||||||
inf_tag: ExtInf,
|
///
|
||||||
/// Sets an `URI`.
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is required.
|
||||||
|
#[builder(setter(into))]
|
||||||
|
#[shorthand(enable(into), disable(skip))]
|
||||||
uri: String,
|
uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaSegment {
|
impl MediaSegment {
|
||||||
/// Returns a Builder for a [`MasterPlaylist`].
|
/// Returns a builder for a [`MediaSegment`].
|
||||||
///
|
///
|
||||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::MediaSegment;
|
||||||
|
/// use hls_m3u8::tags::ExtXMap;
|
||||||
|
/// use std::time::Duration;
|
||||||
|
///
|
||||||
|
/// let segment = MediaSegment::builder()
|
||||||
|
/// .map(ExtXMap::new("https://www.example.com/"))
|
||||||
|
/// .byte_range(5..25)
|
||||||
|
/// .has_discontinuity(true)
|
||||||
|
/// .duration(Duration::from_secs(4))
|
||||||
|
/// .uri("http://www.uri.com/")
|
||||||
|
/// .build()?;
|
||||||
|
/// # Ok::<(), String>(())
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() }
|
pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() }
|
||||||
|
|
||||||
/// Returns the `URI` of the media segment.
|
|
||||||
pub const fn uri(&self) -> &String { &self.uri }
|
|
||||||
|
|
||||||
/// Sets the `URI` of the media segment.
|
|
||||||
pub fn set_uri<T>(&mut self, value: T) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<String>,
|
|
||||||
{
|
|
||||||
self.uri = value.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`ExtInf`] tag associated with the media segment.
|
|
||||||
pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag }
|
|
||||||
|
|
||||||
/// Sets the [`ExtInf`] tag associated with the media segment.
|
|
||||||
pub fn set_inf_tag<T>(&mut self, value: T) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<ExtInf>,
|
|
||||||
{
|
|
||||||
self.inf_tag = value.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`ExtXByteRange`] tag associated with the media segment.
|
|
||||||
pub const fn byte_range_tag(&self) -> Option<ExtXByteRange> { self.byte_range_tag }
|
|
||||||
|
|
||||||
/// Sets the [`ExtXByteRange`] tag associated with the media segment.
|
|
||||||
pub fn set_byte_range_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<ExtXByteRange>,
|
|
||||||
{
|
|
||||||
self.byte_range_tag = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`ExtXDateRange`] tag associated with the media segment.
|
|
||||||
pub const fn date_range_tag(&self) -> &Option<ExtXDateRange> { &self.date_range_tag }
|
|
||||||
|
|
||||||
/// Sets the [`ExtXDateRange`] tag associated with the media segment.
|
|
||||||
pub fn set_date_range_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<ExtXDateRange>,
|
|
||||||
{
|
|
||||||
self.date_range_tag = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`ExtXDiscontinuity`] tag associated with the media segment.
|
|
||||||
pub const fn discontinuity_tag(&self) -> Option<ExtXDiscontinuity> { self.discontinuity_tag }
|
|
||||||
|
|
||||||
/// Sets the [`ExtXDiscontinuity`] tag associated with the media segment.
|
|
||||||
pub fn set_discontinuity_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<ExtXDiscontinuity>,
|
|
||||||
{
|
|
||||||
self.discontinuity_tag = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`ExtXProgramDateTime`] tag associated with the media
|
|
||||||
/// segment.
|
|
||||||
pub const fn program_date_time_tag(&self) -> Option<ExtXProgramDateTime> {
|
|
||||||
self.program_date_time_tag
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the [`ExtXProgramDateTime`] tag associated with the media
|
|
||||||
/// segment.
|
|
||||||
pub fn set_program_date_time_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<ExtXProgramDateTime>,
|
|
||||||
{
|
|
||||||
self.program_date_time_tag = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`ExtXMap`] tag associated with the media segment.
|
|
||||||
pub const fn map_tag(&self) -> &Option<ExtXMap> { &self.map_tag }
|
|
||||||
|
|
||||||
/// Sets the [`ExtXMap`] tag associated with the media segment.
|
|
||||||
pub fn set_map_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<ExtXMap>,
|
|
||||||
{
|
|
||||||
self.map_tag = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaSegmentBuilder {
|
impl MediaSegmentBuilder {
|
||||||
/// Pushes an [`ExtXKey`] tag.
|
/// Pushes an [`ExtXKey`] tag.
|
||||||
pub fn push_key_tag<VALUE: Into<ExtXKey>>(&mut self, value: VALUE) -> &mut Self {
|
pub fn push_key<VALUE: Into<ExtXKey>>(&mut self, value: VALUE) -> &mut Self {
|
||||||
if let Some(key_tags) = &mut self.keys {
|
if let Some(keys) = &mut self.keys {
|
||||||
key_tags.push(value.into());
|
keys.push(value.into());
|
||||||
} else {
|
} else {
|
||||||
self.keys = Some(vec![value.into()]);
|
self.keys = Some(vec![value.into()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of a [`MediaSegment`]. Normally this should not be set
|
||||||
|
/// explicitly, because the [`MediaPlaylist::builder`] will automatically
|
||||||
|
/// apply the correct number.
|
||||||
|
///
|
||||||
|
/// [`MediaPlaylist::builder`]: crate::MediaPlaylist::builder
|
||||||
|
pub fn number(&mut self, value: Option<usize>) -> &mut Self {
|
||||||
|
self.number = value;
|
||||||
|
self.explicit_number = Some(value.is_some());
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MediaSegment {
|
impl fmt::Display for MediaSegment {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
for value in &self.keys {
|
// NOTE: self.keys will be printed by the `MediaPlaylist` to prevent redundance.
|
||||||
|
|
||||||
|
if let Some(value) = &self.map {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(value) = &self.map_tag {
|
|
||||||
|
if let Some(value) = &self.byte_range {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(value) = &self.byte_range_tag {
|
|
||||||
|
if let Some(value) = &self.date_range {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(value) = &self.date_range_tag {
|
|
||||||
|
if self.has_discontinuity {
|
||||||
|
writeln!(f, "{}", ExtXDiscontinuity)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = &self.program_date_time {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(value) = &self.discontinuity_tag {
|
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", self.duration)?;
|
||||||
}
|
|
||||||
if let Some(value) = &self.program_date_time_tag {
|
|
||||||
writeln!(f, "{}", value)?;
|
|
||||||
}
|
|
||||||
writeln!(f, "{}", self.inf_tag)?; // TODO: there might be a `,` missing
|
|
||||||
writeln!(f, "{}", self.uri)?;
|
writeln!(f, "{}", self.uri)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -173,20 +235,27 @@ impl RequiredVersion for MediaSegment {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
required_version![
|
required_version![
|
||||||
self.keys,
|
self.keys,
|
||||||
self.map_tag,
|
self.map,
|
||||||
self.byte_range_tag,
|
self.byte_range,
|
||||||
self.date_range_tag,
|
self.date_range,
|
||||||
self.discontinuity_tag,
|
{
|
||||||
self.program_date_time_tag,
|
if self.has_discontinuity {
|
||||||
self.inf_tag
|
Some(ExtXDiscontinuity)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
self.program_date_time,
|
||||||
|
self.duration
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encrypted for MediaSegment {
|
impl Decryptable for MediaSegment {
|
||||||
fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
fn keys(&self) -> Vec<&DecryptionKey> {
|
||||||
|
//
|
||||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -199,23 +268,22 @@ mod tests {
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.keys(vec![ExtXKey::empty()])
|
.map(ExtXMap::new("https://www.example.com/"))
|
||||||
.map_tag(ExtXMap::new("https://www.example.com/"))
|
.byte_range(ExtXByteRange::from(5..25))
|
||||||
.byte_range_tag(ExtXByteRange::new(20, Some(5)))
|
.has_discontinuity(true)
|
||||||
//.date_range_tag() // TODO!
|
.duration(ExtInf::new(Duration::from_secs(4)))
|
||||||
.discontinuity_tag(ExtXDiscontinuity)
|
|
||||||
.inf_tag(ExtInf::new(Duration::from_secs(4)))
|
|
||||||
.uri("http://www.uri.com/")
|
.uri("http://www.uri.com/")
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
"#EXT-X-KEY:METHOD=NONE\n\
|
concat!(
|
||||||
#EXT-X-MAP:URI=\"https://www.example.com/\"\n\
|
"#EXT-X-MAP:URI=\"https://www.example.com/\"\n",
|
||||||
#EXT-X-BYTERANGE:20@5\n\
|
"#EXT-X-BYTERANGE:20@5\n",
|
||||||
#EXT-X-DISCONTINUITY\n\
|
"#EXT-X-DISCONTINUITY\n",
|
||||||
#EXTINF:4,\n\
|
"#EXTINF:4,\n",
|
||||||
http://www.uri.com/\n"
|
"http://www.uri.com/\n"
|
||||||
.to_string()
|
)
|
||||||
|
.to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,37 +5,15 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.1.1. EXTM3U]
|
|
||||||
///
|
|
||||||
/// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]**
|
/// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]**
|
||||||
/// Playlist file.
|
/// Playlist file.
|
||||||
/// It is the at the start of every [`Media Playlist`] and [`Master Playlist`].
|
/// It is the at the start of every [`MediaPlaylist`] and [`MasterPlaylist`].
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
/// Parsing from a [`str`]:
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// ```
|
|
||||||
/// # use failure::Error;
|
|
||||||
/// # use hls_m3u8::tags::ExtM3u;
|
|
||||||
/// #
|
|
||||||
/// # fn main() -> Result<(), Error> {
|
|
||||||
/// assert_eq!("#EXTM3U".parse::<ExtM3u>()?, ExtM3u);
|
|
||||||
/// #
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
/// Converting to a [`str`]:
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtM3u;
|
|
||||||
/// #
|
|
||||||
/// assert_eq!("#EXTM3U".to_string(), ExtM3u.to_string());
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
|
||||||
/// [`M3U`]: https://en.wikipedia.org/wiki/M3U
|
/// [`M3U`]: https://en.wikipedia.org/wiki/M3U
|
||||||
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||||
pub struct ExtM3u;
|
pub(crate) struct ExtM3u;
|
||||||
|
|
||||||
impl ExtM3u {
|
impl ExtM3u {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
||||||
|
@ -47,7 +25,7 @@ impl RequiredVersion for ExtM3u {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtM3u {
|
impl fmt::Display for ExtM3u {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtM3u {
|
impl FromStr for ExtM3u {
|
||||||
|
@ -72,6 +50,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!("#EXTM3U".parse::<ExtM3u>().unwrap(), ExtM3u);
|
assert_eq!("#EXTM3U".parse::<ExtM3u>().unwrap(), ExtM3u);
|
||||||
|
assert!("#EXTM2U".parse::<ExtM3u>().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mod m3u;
|
pub(crate) mod m3u;
|
||||||
mod version;
|
pub(crate) mod version;
|
||||||
|
|
||||||
pub use m3u::*;
|
pub(crate) use m3u::*;
|
||||||
pub use version::*;
|
pub use version::*;
|
||||||
|
|
|
@ -5,44 +5,12 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.1.2. EXT-X-VERSION]
|
/// The compatibility version of a playlist.
|
||||||
///
|
///
|
||||||
/// The [`ExtXVersion`] tag indicates the compatibility version of the
|
/// It applies to the entire [`MasterPlaylist`] or [`MediaPlaylist`].
|
||||||
/// [`Master Playlist`] or [`Media Playlist`] file.
|
|
||||||
/// It applies to the entire Playlist.
|
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
/// Parsing from a [`str`]:
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// ```
|
|
||||||
/// # use failure::Error;
|
|
||||||
/// # use hls_m3u8::tags::ExtXVersion;
|
|
||||||
/// #
|
|
||||||
/// # fn main() -> Result<(), Error> {
|
|
||||||
/// use hls_m3u8::types::ProtocolVersion;
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// "#EXT-X-VERSION:5".parse::<ExtXVersion>()?,
|
|
||||||
/// ExtXVersion::new(ProtocolVersion::V5)
|
|
||||||
/// );
|
|
||||||
/// #
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
/// Converting to a [`str`]:
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXVersion;
|
|
||||||
/// #
|
|
||||||
/// use hls_m3u8::types::ProtocolVersion;
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// "#EXT-X-VERSION:5".to_string(),
|
|
||||||
/// ExtXVersion::new(ProtocolVersion::V5).to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
|
||||||
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub struct ExtXVersion(ProtocolVersion);
|
pub struct ExtXVersion(ProtocolVersion);
|
||||||
|
|
||||||
|
@ -52,17 +20,20 @@ impl ExtXVersion {
|
||||||
/// Makes a new [`ExtXVersion`] tag.
|
/// Makes a new [`ExtXVersion`] tag.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXVersion;
|
/// # use hls_m3u8::tags::ExtXVersion;
|
||||||
/// use hls_m3u8::types::ProtocolVersion;
|
/// use hls_m3u8::types::ProtocolVersion;
|
||||||
///
|
///
|
||||||
/// let version = ExtXVersion::new(ProtocolVersion::V2);
|
/// let version = ExtXVersion::new(ProtocolVersion::V2);
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub const fn new(version: ProtocolVersion) -> Self { Self(version) }
|
pub const fn new(version: ProtocolVersion) -> Self { Self(version) }
|
||||||
|
|
||||||
/// Returns the [`ProtocolVersion`] of the playlist, containing this tag.
|
/// Returns the underlying [`ProtocolVersion`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXVersion;
|
/// # use hls_m3u8::tags::ExtXVersion;
|
||||||
/// use hls_m3u8::types::ProtocolVersion;
|
/// use hls_m3u8::types::ProtocolVersion;
|
||||||
|
@ -72,6 +43,7 @@ impl ExtXVersion {
|
||||||
/// ProtocolVersion::V6
|
/// ProtocolVersion::V6
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub const fn version(self) -> ProtocolVersion { self.0 }
|
pub const fn version(self) -> ProtocolVersion { self.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,11 +53,14 @@ impl RequiredVersion for ExtXVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXVersion {
|
impl fmt::Display for ExtXVersion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
//
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ExtXVersion {
|
impl Default for ExtXVersion {
|
||||||
fn default() -> Self { Self(ProtocolVersion::V1) }
|
fn default() -> Self { Self(ProtocolVersion::default()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ProtocolVersion> for ExtXVersion {
|
impl From<ProtocolVersion> for ExtXVersion {
|
||||||
|
|
|
@ -1,267 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder};
|
|
||||||
use crate::utils::{quote, tag, unquote};
|
|
||||||
use crate::{Error, RequiredVersion};
|
|
||||||
|
|
||||||
/// # [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]
|
|
||||||
///
|
|
||||||
/// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file,
|
|
||||||
/// containing the I-frames of a multimedia presentation.
|
|
||||||
///
|
|
||||||
/// I-frames are encoded video frames, whose decoding
|
|
||||||
/// does not depend on any other frame.
|
|
||||||
///
|
|
||||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
/// [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
|
||||||
#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXIFrameStreamInf {
|
|
||||||
uri: String,
|
|
||||||
stream_inf: StreamInf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq)]
|
|
||||||
/// Builder for [`ExtXIFrameStreamInf`].
|
|
||||||
pub struct ExtXIFrameStreamInfBuilder {
|
|
||||||
uri: Option<String>,
|
|
||||||
stream_inf: StreamInfBuilder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXIFrameStreamInfBuilder {
|
|
||||||
/// An `URI` to the [`MediaPlaylist`] file.
|
|
||||||
///
|
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
|
||||||
pub fn uri<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.uri = Some(value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The maximum bandwidth of the stream.
|
|
||||||
pub fn bandwidth(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.stream_inf.bandwidth(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The average bandwidth of the stream.
|
|
||||||
pub fn average_bandwidth(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.stream_inf.average_bandwidth(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every media format in any of the renditions specified by the Variant
|
|
||||||
/// Stream.
|
|
||||||
pub fn codecs<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.stream_inf.codecs(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The resolution of the stream.
|
|
||||||
pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self {
|
|
||||||
self.stream_inf.resolution(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// High-bandwidth Digital Content Protection
|
|
||||||
pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self {
|
|
||||||
self.stream_inf.hdcp_level(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// It indicates the set of video renditions, that should be used when
|
|
||||||
/// playing the presentation.
|
|
||||||
pub fn video<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.stream_inf.video(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build an [`ExtXIFrameStreamInf`].
|
|
||||||
pub fn build(&self) -> crate::Result<ExtXIFrameStreamInf> {
|
|
||||||
Ok(ExtXIFrameStreamInf {
|
|
||||||
uri: self
|
|
||||||
.uri
|
|
||||||
.clone()
|
|
||||||
.ok_or_else(|| Error::missing_value("frame rate"))?,
|
|
||||||
stream_inf: self.stream_inf.build().map_err(Error::builder_error)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXIFrameStreamInf {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
|
||||||
|
|
||||||
/// Makes a new [`ExtXIFrameStreamInf`] tag.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXIFrameStreamInf;
|
|
||||||
/// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
|
|
||||||
/// ```
|
|
||||||
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
uri: uri.to_string(),
|
|
||||||
stream_inf: StreamInf::new(bandwidth),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a builder for [`ExtXIFrameStreamInf`].
|
|
||||||
pub fn builder() -> ExtXIFrameStreamInfBuilder { ExtXIFrameStreamInfBuilder::default() }
|
|
||||||
|
|
||||||
/// Returns the `URI`, that identifies the associated [`media playlist`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXIFrameStreamInf;
|
|
||||||
/// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
|
|
||||||
/// assert_eq!(stream.uri(), &"https://www.example.com".to_string());
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`media playlist`]: crate::MediaPlaylist
|
|
||||||
pub const fn uri(&self) -> &String { &self.uri }
|
|
||||||
|
|
||||||
/// Sets the `URI`, that identifies the associated [`media playlist`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXIFrameStreamInf;
|
|
||||||
/// #
|
|
||||||
/// let mut stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
|
|
||||||
///
|
|
||||||
/// stream.set_uri("../new/uri");
|
|
||||||
/// assert_eq!(stream.uri(), &"../new/uri".to_string());
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`media playlist`]: crate::MediaPlaylist
|
|
||||||
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.uri = value.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
|
||||||
impl RequiredVersion for ExtXIFrameStreamInf {
|
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ExtXIFrameStreamInf {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "URI={},{}", quote(&self.uri), self.stream_inf)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ExtXIFrameStreamInf {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
let input = tag(input, Self::PREFIX)?;
|
|
||||||
|
|
||||||
let mut uri = None;
|
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
|
||||||
if let "URI" = key.as_str() {
|
|
||||||
uri = Some(unquote(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let uri = uri.ok_or_else(|| Error::missing_value("URI"))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
uri,
|
|
||||||
stream_inf: input.parse()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for ExtXIFrameStreamInf {
|
|
||||||
type Target = StreamInf;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.stream_inf }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for ExtXIFrameStreamInf {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream_inf }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_builder() {
|
|
||||||
let mut i_frame_stream_inf =
|
|
||||||
ExtXIFrameStreamInf::new("http://example.com/audio-only.m3u8", 200_000);
|
|
||||||
|
|
||||||
i_frame_stream_inf
|
|
||||||
.set_average_bandwidth(Some(100_000))
|
|
||||||
.set_codecs(Some("mp4a.40.5"))
|
|
||||||
.set_resolution(1920, 1080)
|
|
||||||
.set_hdcp_level(Some(HdcpLevel::None))
|
|
||||||
.set_video(Some("video"));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ExtXIFrameStreamInf::builder()
|
|
||||||
.uri("http://example.com/audio-only.m3u8")
|
|
||||||
.bandwidth(200_000)
|
|
||||||
.average_bandwidth(100_000)
|
|
||||||
.codecs("mp4a.40.5")
|
|
||||||
.resolution((1920, 1080))
|
|
||||||
.hdcp_level(HdcpLevel::None)
|
|
||||||
.video("video")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
i_frame_stream_inf
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_display() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXIFrameStreamInf::new("foo", 1000).to_string(),
|
|
||||||
"#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000"
|
|
||||||
.parse::<ExtXIFrameStreamInf>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXIFrameStreamInf::new("foo", 1000)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("garbage".parse::<ExtXIFrameStreamInf>().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_required_version() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXIFrameStreamInf::new("foo", 1000).required_version(),
|
|
||||||
ProtocolVersion::V1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXIFrameStreamInf::new("https://www.example.com", 20).average_bandwidth(),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref_mut() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXIFrameStreamInf::new("https://www.example.com", 20)
|
|
||||||
.set_average_bandwidth(Some(4))
|
|
||||||
.average_bandwidth(),
|
|
||||||
Some(4)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,9 @@
|
||||||
mod i_frame_stream_inf;
|
pub(crate) mod media;
|
||||||
mod media;
|
pub(crate) mod session_data;
|
||||||
mod session_data;
|
pub(crate) mod session_key;
|
||||||
mod session_key;
|
pub(crate) mod variant_stream;
|
||||||
mod stream_inf;
|
|
||||||
|
|
||||||
pub use i_frame_stream_inf::*;
|
pub use media::ExtXMedia;
|
||||||
pub use media::*;
|
pub use session_data::{ExtXSessionData, SessionData};
|
||||||
pub use session_data::*;
|
|
||||||
pub use session_key::*;
|
pub use session_key::*;
|
||||||
pub use stream_inf::*;
|
pub use variant_stream::*;
|
||||||
|
|
|
@ -2,58 +2,66 @@ use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::{quote, tag, unquote};
|
use crate::utils::{quote, tag, unquote};
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// The data of an [`ExtXSessionData`] tag.
|
/// The data of [`ExtXSessionData`].
|
||||||
#[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum SessionData {
|
pub enum SessionData {
|
||||||
/// A String, that contains the data identified by
|
/// Contains the data identified by the [`ExtXSessionData::data_id`].
|
||||||
/// [`data_id`].
|
///
|
||||||
/// If a [`language`] is specified, the value
|
/// If a [`language`] is specified, this variant should contain a
|
||||||
/// should contain a human-readable string written in the specified
|
/// human-readable string written in the specified language.
|
||||||
/// language.
|
|
||||||
///
|
///
|
||||||
/// [`data_id`]: ExtXSessionData::data_id
|
/// [`data_id`]: ExtXSessionData::data_id
|
||||||
/// [`language`]: ExtXSessionData::language
|
/// [`language`]: ExtXSessionData::language
|
||||||
Value(String),
|
Value(String),
|
||||||
/// An [`uri`], which points to a [`json`].
|
/// An [`URI`], which points to a [`json`] file.
|
||||||
///
|
///
|
||||||
/// [`json`]: https://tools.ietf.org/html/rfc8259
|
/// [`json`]: https://tools.ietf.org/html/rfc8259
|
||||||
/// [`uri`]: https://tools.ietf.org/html/rfc3986
|
/// [`URI`]: https://tools.ietf.org/html/rfc3986
|
||||||
Uri(String),
|
Uri(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # [4.3.4.4. EXT-X-SESSION-DATA]
|
/// Allows arbitrary session data to be carried in a [`MasterPlaylist`].
|
||||||
///
|
///
|
||||||
/// The [`ExtXSessionData`] tag allows arbitrary session data to be
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// carried in a [`Master Playlist`].
|
#[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
||||||
///
|
|
||||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
|
||||||
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
|
||||||
#[derive(Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
|
#[shorthand(enable(must_use, into))]
|
||||||
pub struct ExtXSessionData {
|
pub struct ExtXSessionData {
|
||||||
/// The identifier of the data.
|
/// This should conform to a [reverse DNS] naming convention, such as
|
||||||
/// For more information look [`here`].
|
/// `com.example.movie.title`.
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
|
///
|
||||||
|
/// There is no central registration authority, so a value
|
||||||
|
/// should be choosen, that is unlikely to collide with others.
|
||||||
|
///
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
///
|
///
|
||||||
/// [`here`]: ExtXSessionData::set_data_id
|
/// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
|
||||||
data_id: String,
|
data_id: String,
|
||||||
/// The data associated with the [`data_id`].
|
/// The [`SessionData`] associated with the
|
||||||
/// For more information look [`here`](SessionData).
|
/// [`data_id`](ExtXSessionData::data_id).
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
/// This field is required.
|
|
||||||
///
|
///
|
||||||
/// [`data_id`]: ExtXSessionDataBuilder::data_id
|
/// This field is required.
|
||||||
data: SessionData,
|
#[shorthand(enable(skip))]
|
||||||
/// The language of the [`data`](ExtXSessionDataBuilder::data).
|
pub data: SessionData,
|
||||||
|
/// The `language` attribute identifies the language of the [`SessionData`].
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional and the provided value should conform to
|
||||||
|
/// [RFC5646].
|
||||||
|
///
|
||||||
|
/// [RFC5646]: https://tools.ietf.org/html/rfc5646
|
||||||
#[builder(setter(into, strip_option), default)]
|
#[builder(setter(into, strip_option), default)]
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -64,199 +72,78 @@ impl ExtXSessionData {
|
||||||
/// Makes a new [`ExtXSessionData`] tag.
|
/// Makes a new [`ExtXSessionData`] tag.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
|
||||||
/// use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
|
||||||
///
|
///
|
||||||
/// ExtXSessionData::new(
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXSessionData;
|
||||||
|
/// use hls_m3u8::tags::SessionData;
|
||||||
|
///
|
||||||
|
/// let session_data = ExtXSessionData::new(
|
||||||
/// "com.example.movie.title",
|
/// "com.example.movie.title",
|
||||||
/// SessionData::Uri("https://www.example.com/".to_string()),
|
/// SessionData::Uri("https://www.example.com/".into()),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: ToString>(data_id: T, data: SessionData) -> Self {
|
#[must_use]
|
||||||
|
pub fn new<T: Into<String>>(data_id: T, data: SessionData) -> Self {
|
||||||
Self {
|
Self {
|
||||||
data_id: data_id.to_string(),
|
data_id: data_id.into(),
|
||||||
data,
|
data,
|
||||||
language: None,
|
language: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new Builder for [`ExtXSessionData`].
|
/// Returns a builder for [`ExtXSessionData`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
/// # use hls_m3u8::tags::ExtXSessionData;
|
||||||
|
/// use hls_m3u8::tags::SessionData;
|
||||||
///
|
///
|
||||||
/// let session_data = ExtXSessionData::builder()
|
/// let session_data = ExtXSessionData::builder()
|
||||||
/// .data_id("com.example.movie.title")
|
/// .data_id("com.example.movie.title")
|
||||||
/// .data(SessionData::Value("some data".to_string()))
|
/// .data(SessionData::Value("some data".into()))
|
||||||
/// .language("english")
|
/// .language("en")
|
||||||
/// .build()
|
/// .build()?;
|
||||||
/// .expect("Failed to build an ExtXSessionData tag.");
|
/// # Ok::<(), String>(())
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// session_data,
|
|
||||||
/// ExtXSessionData::with_language(
|
|
||||||
/// "com.example.movie.title",
|
|
||||||
/// SessionData::Value("some data".to_string()),
|
|
||||||
/// "english"
|
|
||||||
/// )
|
|
||||||
/// );
|
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() }
|
pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() }
|
||||||
|
|
||||||
/// Makes a new [`ExtXSessionData`] tag, with the given language.
|
/// Makes a new [`ExtXSessionData`] tag, with the given language.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
/// # use hls_m3u8::tags::ExtXSessionData;
|
||||||
|
/// use hls_m3u8::tags::SessionData;
|
||||||
///
|
///
|
||||||
/// let session_data = ExtXSessionData::with_language(
|
/// let session_data = ExtXSessionData::with_language(
|
||||||
/// "com.example.movie.title",
|
/// "com.example.movie.title",
|
||||||
/// SessionData::Value("some data".to_string()),
|
/// SessionData::Value("some data".into()),
|
||||||
/// "english",
|
/// "en",
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub fn with_language<T: ToString>(data_id: T, data: SessionData, language: T) -> Self {
|
#[must_use]
|
||||||
|
pub fn with_language<T, K>(data_id: T, data: SessionData, language: K) -> Self
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
K: Into<String>,
|
||||||
|
{
|
||||||
Self {
|
Self {
|
||||||
data_id: data_id.to_string(),
|
data_id: data_id.into(),
|
||||||
data,
|
data,
|
||||||
language: Some(language.to_string()),
|
language: Some(language.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `data_id`, that identifies a `data_value`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
|
||||||
/// #
|
|
||||||
/// let data = ExtXSessionData::new(
|
|
||||||
/// "com.example.movie.title",
|
|
||||||
/// SessionData::Value("some data".to_string())
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// data.data_id(),
|
|
||||||
/// &"com.example.movie.title".to_string()
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
pub const fn data_id(&self) -> &String { &self.data_id }
|
|
||||||
|
|
||||||
/// Returns the `data`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
|
||||||
/// #
|
|
||||||
/// let data = ExtXSessionData::new(
|
|
||||||
/// "com.example.movie.title",
|
|
||||||
/// SessionData::Value("some data".to_string())
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// data.data(),
|
|
||||||
/// &SessionData::Value("some data".to_string())
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
pub const fn data(&self) -> &SessionData { &self.data }
|
|
||||||
|
|
||||||
/// Returns the `language` tag, that identifies the language of
|
|
||||||
/// [`SessionData`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
|
||||||
/// #
|
|
||||||
/// let data = ExtXSessionData::with_language(
|
|
||||||
/// "com.example.movie.title",
|
|
||||||
/// SessionData::Value("some data".to_string()),
|
|
||||||
/// "english"
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// data.language(),
|
|
||||||
/// &Some("english".to_string())
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
pub const fn language(&self) -> &Option<String> { &self.language }
|
|
||||||
|
|
||||||
/// Sets the `language` attribute, that identifies the language of
|
|
||||||
/// [`SessionData`]. See [rfc5646](https://tools.ietf.org/html/rfc5646).
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
|
||||||
/// #
|
|
||||||
/// let mut data = ExtXSessionData::new(
|
|
||||||
/// "com.example.movie.title",
|
|
||||||
/// SessionData::Value("some data".to_string()),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(data.language(), &None);
|
|
||||||
///
|
|
||||||
/// data.set_language(Some("english"));
|
|
||||||
/// assert_eq!(data.language(), &Some("english".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn set_language<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.language = value.map(|v| v.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the `data_id` attribute, that should conform to a [reverse DNS]
|
|
||||||
/// naming convention, such as `com.example.movie.title`.
|
|
||||||
///
|
|
||||||
/// # Note:
|
|
||||||
/// There is no central registration authority, so a value
|
|
||||||
/// should be choosen, that is unlikely to collide with others.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
|
||||||
/// #
|
|
||||||
/// let mut data = ExtXSessionData::new(
|
|
||||||
/// "com.example.movie.title",
|
|
||||||
/// SessionData::Value("some data".to_string()),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(data.data_id(), &"com.example.movie.title".to_string());
|
|
||||||
///
|
|
||||||
/// data.set_data_id("com.other.movie.title");
|
|
||||||
/// assert_eq!(data.data_id(), &"com.other.movie.title".to_string());
|
|
||||||
/// ```
|
|
||||||
/// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
|
|
||||||
pub fn set_data_id<T: ToString>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.data_id = value.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the [`data`](ExtXSessionData::data) of this tag.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
|
||||||
/// #
|
|
||||||
/// let mut data = ExtXSessionData::new(
|
|
||||||
/// "com.example.movie.title",
|
|
||||||
/// SessionData::Value("some data".to_string()),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(data.data(), &SessionData::Value("some data".to_string()));
|
|
||||||
///
|
|
||||||
/// data.set_data(SessionData::Value("new data".to_string()));
|
|
||||||
/// assert_eq!(data.data(), &SessionData::Value("new data".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn set_data(&mut self, value: SessionData) -> &mut Self {
|
|
||||||
self.data = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXSessionData {
|
impl RequiredVersion for ExtXSessionData {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXSessionData {
|
impl fmt::Display for ExtXSessionData {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "DATA-ID={}", quote(&self.data_id))?;
|
write!(f, "DATA-ID={}", quote(&self.data_id))?;
|
||||||
|
|
||||||
|
@ -284,8 +171,8 @@ impl FromStr for ExtXSessionData {
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut language = None;
|
let mut language = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"DATA-ID" => data_id = Some(unquote(value)),
|
"DATA-ID" => data_id = Some(unquote(value)),
|
||||||
"VALUE" => session_value = Some(unquote(value)),
|
"VALUE" => session_value = Some(unquote(value)),
|
||||||
"URI" => uri = Some(unquote(value)),
|
"URI" => uri = Some(unquote(value)),
|
||||||
|
@ -299,17 +186,20 @@ impl FromStr for ExtXSessionData {
|
||||||
}
|
}
|
||||||
|
|
||||||
let data_id = data_id.ok_or_else(|| Error::missing_value("EXT-X-DATA-ID"))?;
|
let data_id = data_id.ok_or_else(|| Error::missing_value("EXT-X-DATA-ID"))?;
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
if let Some(value) = session_value {
|
if let Some(value) = session_value {
|
||||||
if uri.is_some() {
|
if uri.is_some() {
|
||||||
return Err(Error::custom("Unexpected URI"));
|
return Err(Error::custom("unexpected URI"));
|
||||||
} else {
|
} else {
|
||||||
SessionData::Value(value)
|
SessionData::Value(value)
|
||||||
}
|
}
|
||||||
} else if let Some(uri) = uri {
|
} else if let Some(uri) = uri {
|
||||||
SessionData::Uri(uri)
|
SessionData::Uri(uri)
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::custom(
|
||||||
|
"expected either `SessionData::Uri` or `SessionData::Value`",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -326,159 +216,85 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
macro_rules! generate_tests {
|
||||||
fn test_display() {
|
( $( { $struct:expr, $str:expr } ),+ $(,)* ) => {
|
||||||
assert_eq!(
|
#[test]
|
||||||
"#EXT-X-SESSION-DATA:\
|
fn test_display() {
|
||||||
DATA-ID=\"com.example.lyrics\",\
|
$(
|
||||||
URI=\"lyrics.json\""
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
.to_string(),
|
)+
|
||||||
ExtXSessionData::new(
|
}
|
||||||
"com.example.lyrics",
|
|
||||||
SessionData::Uri("lyrics.json".to_string())
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
#[test]
|
||||||
"#EXT-X-SESSION-DATA:\
|
fn test_parser() {
|
||||||
DATA-ID=\"com.example.title\",\
|
$(
|
||||||
VALUE=\"This is an example\",\
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
LANGUAGE=\"en\""
|
)+
|
||||||
.to_string(),
|
|
||||||
ExtXSessionData::with_language(
|
|
||||||
"com.example.title",
|
|
||||||
SessionData::Value("This is an example".to_string()),
|
|
||||||
"en"
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert!(
|
||||||
"#EXT-X-SESSION-DATA:\
|
concat!(
|
||||||
DATA-ID=\"com.example.title\",\
|
"#EXT-X-SESSION-DATA:",
|
||||||
VALUE=\"Este es un ejemplo\",\
|
"DATA-ID=\"foo\",",
|
||||||
LANGUAGE=\"es\""
|
"LANGUAGE=\"baz\""
|
||||||
.to_string(),
|
)
|
||||||
ExtXSessionData::with_language(
|
.parse::<ExtXSessionData>()
|
||||||
"com.example.title",
|
.is_err()
|
||||||
SessionData::Value("Este es un ejemplo".to_string()),
|
);
|
||||||
"es"
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert!(
|
||||||
"#EXT-X-SESSION-DATA:\
|
concat!(
|
||||||
DATA-ID=\"foo\",\
|
"#EXT-X-SESSION-DATA:",
|
||||||
VALUE=\"bar\""
|
"DATA-ID=\"foo\",",
|
||||||
.to_string(),
|
"LANGUAGE=\"baz\",",
|
||||||
ExtXSessionData::new("foo", SessionData::Value("bar".into())).to_string()
|
"VALUE=\"VALUE\",",
|
||||||
);
|
"URI=\"https://www.example.com/\""
|
||||||
|
)
|
||||||
|
.parse::<ExtXSessionData>()
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(
|
}
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"foo\",\
|
|
||||||
URI=\"bar\""
|
|
||||||
.to_string(),
|
|
||||||
ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"foo\",\
|
|
||||||
VALUE=\"bar\",\
|
|
||||||
LANGUAGE=\"baz\""
|
|
||||||
.to_string(),
|
|
||||||
ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz")
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
generate_tests! {
|
||||||
fn test_parser() {
|
{
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"com.example.lyrics\",\
|
|
||||||
URI=\"lyrics.json\""
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXSessionData::new(
|
ExtXSessionData::new(
|
||||||
"com.example.lyrics",
|
"com.example.lyrics",
|
||||||
SessionData::Uri("lyrics.json".to_string())
|
SessionData::Uri("lyrics.json".into())
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-SESSION-DATA:",
|
||||||
|
"DATA-ID=\"com.example.lyrics\",",
|
||||||
|
"URI=\"lyrics.json\""
|
||||||
)
|
)
|
||||||
);
|
},
|
||||||
|
{
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"com.example.title\",\
|
|
||||||
LANGUAGE=\"en\",\
|
|
||||||
VALUE=\"This is an example\""
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXSessionData::with_language(
|
ExtXSessionData::with_language(
|
||||||
"com.example.title",
|
"com.example.title",
|
||||||
SessionData::Value("This is an example".to_string()),
|
SessionData::Value("This is an example".into()),
|
||||||
"en"
|
"en"
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-SESSION-DATA:",
|
||||||
|
"DATA-ID=\"com.example.title\",",
|
||||||
|
"VALUE=\"This is an example\",",
|
||||||
|
"LANGUAGE=\"en\""
|
||||||
)
|
)
|
||||||
);
|
},
|
||||||
|
{
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"com.example.title\",\
|
|
||||||
LANGUAGE=\"es\",\
|
|
||||||
VALUE=\"Este es un ejemplo\""
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXSessionData::with_language(
|
ExtXSessionData::with_language(
|
||||||
"com.example.title",
|
"com.example.title",
|
||||||
SessionData::Value("Este es un ejemplo".to_string()),
|
SessionData::Value("Este es un ejemplo".into()),
|
||||||
"es"
|
"es"
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-SESSION-DATA:",
|
||||||
|
"DATA-ID=\"com.example.title\",",
|
||||||
|
"VALUE=\"Este es un ejemplo\",",
|
||||||
|
"LANGUAGE=\"es\""
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"foo\",\
|
|
||||||
VALUE=\"bar\""
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXSessionData::new("foo", SessionData::Value("bar".into()))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"foo\",\
|
|
||||||
URI=\"bar\""
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXSessionData::new("foo", SessionData::Uri("bar".into()))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"foo\",\
|
|
||||||
VALUE=\"bar\",\
|
|
||||||
LANGUAGE=\"baz\",\
|
|
||||||
UNKNOWN=TAG"
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"foo\",\
|
|
||||||
LANGUAGE=\"baz\""
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
assert!("#EXT-X-SESSION-DATA:\
|
|
||||||
DATA-ID=\"foo\",\
|
|
||||||
LANGUAGE=\"baz\",\
|
|
||||||
VALUE=\"VALUE\",\
|
|
||||||
URI=\"https://www.example.com/\""
|
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.is_err());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,65 +1,71 @@
|
||||||
|
use core::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion};
|
use derive_more::{AsMut, AsRef, From};
|
||||||
|
|
||||||
|
use crate::tags::ExtXKey;
|
||||||
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.4.5. EXT-X-SESSION-KEY]
|
/// The [`ExtXSessionKey`] tag allows encryption keys from [`MediaPlaylist`]s
|
||||||
/// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s
|
/// to be specified in a [`MasterPlaylist`]. This allows the client to
|
||||||
/// to be specified in a [`Master Playlist`]. This allows the client to
|
/// preload these keys without having to read the [`MediaPlaylist`]s
|
||||||
/// preload these keys without having to read the [`Media Playlist`]s
|
|
||||||
/// first.
|
/// first.
|
||||||
///
|
///
|
||||||
/// Its format is:
|
/// If an [`ExtXSessionKey`] is used, the values of [`DecryptionKey::method`],
|
||||||
/// ```text
|
/// [`DecryptionKey::format`] and [`DecryptionKey::versions`] must match any
|
||||||
/// #EXT-X-SESSION-KEY:<attribute-list>
|
/// [`ExtXKey`] with the same uri field.
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
/// [`ExtXKey`]: crate::tags::ExtXKey
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct ExtXSessionKey(DecryptionKey);
|
pub struct ExtXSessionKey(pub DecryptionKey);
|
||||||
|
|
||||||
impl ExtXSessionKey {
|
impl ExtXSessionKey {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXSessionKey`] tag.
|
/// Makes a new [`ExtXSessionKey`] tag.
|
||||||
///
|
///
|
||||||
/// # Panic
|
|
||||||
/// An [`ExtXSessionKey`] should only be used,
|
|
||||||
/// if the segments of the stream are encrypted.
|
|
||||||
/// Therefore this function will panic,
|
|
||||||
/// if the `method` is [`EncryptionMethod::None`].
|
|
||||||
///
|
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXSessionKey;
|
/// # use hls_m3u8::tags::ExtXSessionKey;
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
///
|
///
|
||||||
/// let session_key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
/// let session_key = ExtXSessionKey::new(DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com/",
|
||||||
|
/// ));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: ToString>(method: EncryptionMethod, uri: T) -> Self {
|
#[must_use]
|
||||||
if method == EncryptionMethod::None {
|
#[inline]
|
||||||
panic!("The EncryptionMethod is not allowed to be None");
|
pub const fn new(inner: DecryptionKey) -> Self { Self(inner) }
|
||||||
}
|
}
|
||||||
|
|
||||||
Self(DecryptionKey::new(method, uri))
|
impl TryFrom<ExtXKey> for ExtXSessionKey {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: ExtXKey) -> Result<Self, Self::Error> {
|
||||||
|
if let ExtXKey(Some(inner)) = value {
|
||||||
|
Ok(Self(inner))
|
||||||
|
} else {
|
||||||
|
Err(Error::custom("missing decryption key"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires the same [`ProtocolVersion`] that is returned by
|
||||||
|
/// `DecryptionKey::required_version`.
|
||||||
impl RequiredVersion for ExtXSessionKey {
|
impl RequiredVersion for ExtXSessionKey {
|
||||||
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXSessionKey {
|
impl fmt::Display for ExtXSessionKey {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if self.0.method == EncryptionMethod::None {
|
write!(f, "{}{}", Self::PREFIX, self.0.to_string())
|
||||||
return Err(fmt::Error);
|
|
||||||
}
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,125 +73,84 @@ impl FromStr for ExtXSessionKey {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
Ok(Self(DecryptionKey::from_str(tag(input, Self::PREFIX)?)?))
|
||||||
Ok(Self(input.parse()?))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for ExtXSessionKey {
|
|
||||||
type Target = DecryptionKey;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for ExtXSessionKey {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::{EncryptionMethod, KeyFormat};
|
use crate::types::{EncryptionMethod, KeyFormat};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
macro_rules! generate_tests {
|
||||||
fn test_display() {
|
( $( { $struct:expr, $str:expr } ),+ $(,)* ) => {
|
||||||
let mut key = ExtXSessionKey::new(
|
#[test]
|
||||||
EncryptionMethod::Aes128,
|
fn test_display() {
|
||||||
"https://www.example.com/hls-key/key.bin",
|
$(
|
||||||
);
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
key.set_iv(Some([
|
)+
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
}
|
||||||
]));
|
|
||||||
|
|
||||||
assert_eq!(
|
#[test]
|
||||||
key.to_string(),
|
fn test_parser() {
|
||||||
"#EXT-X-SESSION-KEY:METHOD=AES-128,\
|
$(
|
||||||
URI=\"https://www.example.com/hls-key/key.bin\",\
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
IV=0x10ef8f758ca555115584bb5b3c687f52"
|
)+
|
||||||
.to_string()
|
}
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
generate_tests! {
|
||||||
fn test_parser() {
|
{
|
||||||
assert_eq!(
|
|
||||||
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""#
|
|
||||||
.parse::<ExtXSessionKey>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXSessionKey::new(
|
ExtXSessionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
DecryptionKey::builder()
|
||||||
"https://priv.example.com/key.php?r=52"
|
.method(EncryptionMethod::Aes128)
|
||||||
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
|
.iv([
|
||||||
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-SESSION-KEY:",
|
||||||
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
|
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||||
)
|
)
|
||||||
);
|
},
|
||||||
|
{
|
||||||
let mut key = ExtXSessionKey::new(
|
ExtXSessionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
DecryptionKey::builder()
|
||||||
"https://www.example.com/hls-key/key.bin",
|
.method(EncryptionMethod::Aes128)
|
||||||
);
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
key.set_iv(Some([
|
.iv([
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
]));
|
])
|
||||||
|
.format(KeyFormat::Identity)
|
||||||
assert_eq!(
|
.build()
|
||||||
"#EXT-X-SESSION-KEY:METHOD=AES-128,\
|
.unwrap(),
|
||||||
URI=\"https://www.example.com/hls-key/key.bin\",\
|
),
|
||||||
IV=0X10ef8f758ca555115584bb5b3c687f52"
|
concat!(
|
||||||
.parse::<ExtXSessionKey>()
|
"#EXT-X-SESSION-KEY:",
|
||||||
.unwrap(),
|
"METHOD=AES-128,",
|
||||||
key
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
);
|
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
||||||
|
"KEYFORMAT=\"identity\"",
|
||||||
key.set_key_format(Some(KeyFormat::Identity));
|
)
|
||||||
|
}
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-SESSION-KEY:\
|
|
||||||
METHOD=AES-128,\
|
|
||||||
URI=\"https://www.example.com/hls-key/key.bin\",\
|
|
||||||
IV=0x10ef8f758ca555115584bb5b3c687f52,\
|
|
||||||
KEYFORMAT=\"identity\""
|
|
||||||
.parse::<ExtXSessionKey>()
|
|
||||||
.unwrap(),
|
|
||||||
key
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/")
|
ExtXSessionKey::new(DecryptionKey::new(
|
||||||
.required_version(),
|
EncryptionMethod::Aes128,
|
||||||
|
"https://www.example.com/"
|
||||||
|
))
|
||||||
|
.required_version(),
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
// ExtXSessionKey::new should panic, if the provided
|
|
||||||
// EncryptionMethod is None!
|
|
||||||
fn test_new_panic() { ExtXSessionKey::new(EncryptionMethod::None, ""); }
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_display_err() {
|
|
||||||
ExtXSessionKey(DecryptionKey::new(EncryptionMethod::None, "")).to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref() {
|
|
||||||
let key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
|
|
||||||
assert_eq!(key.method(), EncryptionMethod::Aes128);
|
|
||||||
assert_eq!(key.uri(), &Some("https://www.example.com/".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref_mut() {
|
|
||||||
let mut key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
|
|
||||||
key.set_method(EncryptionMethod::None);
|
|
||||||
assert_eq!(key.method(), EncryptionMethod::None);
|
|
||||||
key.set_uri(Some("https://www.github.com/"));
|
|
||||||
assert_eq!(key.uri(), &Some("https://www.github.com/".into()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,426 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{
|
|
||||||
ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder,
|
|
||||||
};
|
|
||||||
use crate::utils::{quote, tag, unquote};
|
|
||||||
use crate::{Error, RequiredVersion};
|
|
||||||
|
|
||||||
/// # [4.3.4.2. EXT-X-STREAM-INF]
|
|
||||||
///
|
|
||||||
/// The [`ExtXStreamInf`] tag specifies a Variant Stream, which is a set
|
|
||||||
/// of Renditions that can be combined to play the presentation. The
|
|
||||||
/// attributes of the tag provide information about the Variant Stream.
|
|
||||||
///
|
|
||||||
/// The URI line that follows the [`ExtXStreamInf`] tag specifies a Media
|
|
||||||
/// Playlist that carries a rendition of the Variant Stream. The URI
|
|
||||||
/// line is REQUIRED. Clients that do not support multiple video
|
|
||||||
/// Renditions SHOULD play this Rendition.
|
|
||||||
///
|
|
||||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
|
||||||
#[derive(PartialOrd, Debug, Clone, PartialEq)]
|
|
||||||
pub struct ExtXStreamInf {
|
|
||||||
uri: String,
|
|
||||||
frame_rate: Option<DecimalFloatingPoint>,
|
|
||||||
audio: Option<String>,
|
|
||||||
subtitles: Option<String>,
|
|
||||||
closed_captions: Option<ClosedCaptions>,
|
|
||||||
stream_inf: StreamInf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
|
||||||
/// Builder for [`ExtXStreamInf`].
|
|
||||||
pub struct ExtXStreamInfBuilder {
|
|
||||||
uri: Option<String>,
|
|
||||||
frame_rate: Option<DecimalFloatingPoint>,
|
|
||||||
audio: Option<String>,
|
|
||||||
subtitles: Option<String>,
|
|
||||||
closed_captions: Option<ClosedCaptions>,
|
|
||||||
stream_inf: StreamInfBuilder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXStreamInfBuilder {
|
|
||||||
/// An `URI` to the [`MediaPlaylist`] file.
|
|
||||||
///
|
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
|
||||||
pub fn uri<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.uri = Some(value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maximum frame rate for all the video in the variant stream.
|
|
||||||
pub fn frame_rate(&mut self, value: f64) -> &mut Self {
|
|
||||||
self.frame_rate = Some(value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The group identifier for the audio in the variant stream.
|
|
||||||
pub fn audio<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.audio = Some(value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The group identifier for the subtitles in the variant stream.
|
|
||||||
pub fn subtitles<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.subtitles = Some(value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The value of [`ClosedCaptions`] attribute.
|
|
||||||
pub fn closed_captions<T: Into<ClosedCaptions>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.closed_captions = Some(value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The maximum bandwidth of the stream.
|
|
||||||
pub fn bandwidth(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.stream_inf.bandwidth(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The average bandwidth of the stream.
|
|
||||||
pub fn average_bandwidth(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.stream_inf.average_bandwidth(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every media format in any of the renditions specified by the Variant
|
|
||||||
/// Stream.
|
|
||||||
pub fn codecs<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.stream_inf.codecs(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The resolution of the stream.
|
|
||||||
pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self {
|
|
||||||
self.stream_inf.resolution(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// High-bandwidth Digital Content Protection
|
|
||||||
pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self {
|
|
||||||
self.stream_inf.hdcp_level(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// It indicates the set of video renditions, that should be used when
|
|
||||||
/// playing the presentation.
|
|
||||||
pub fn video<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.stream_inf.video(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build an [`ExtXStreamInf`].
|
|
||||||
pub fn build(&self) -> crate::Result<ExtXStreamInf> {
|
|
||||||
Ok(ExtXStreamInf {
|
|
||||||
uri: self
|
|
||||||
.uri
|
|
||||||
.clone()
|
|
||||||
.ok_or_else(|| Error::missing_value("frame rate"))?,
|
|
||||||
frame_rate: self.frame_rate,
|
|
||||||
audio: self.audio.clone(),
|
|
||||||
subtitles: self.subtitles.clone(),
|
|
||||||
closed_captions: self.closed_captions.clone(),
|
|
||||||
stream_inf: self.stream_inf.build().map_err(Error::builder_error)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXStreamInf {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
|
||||||
|
|
||||||
/// Creates a new [`ExtXStreamInf`] tag.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// ```
|
|
||||||
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
uri: uri.to_string(),
|
|
||||||
frame_rate: None,
|
|
||||||
audio: None,
|
|
||||||
subtitles: None,
|
|
||||||
closed_captions: None,
|
|
||||||
stream_inf: StreamInf::new(bandwidth),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a builder for [`ExtXStreamInf`].
|
|
||||||
pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() }
|
|
||||||
|
|
||||||
/// Returns the `URI` that identifies the associated media playlist.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
///
|
|
||||||
/// assert_eq!(stream.uri(), &"https://www.example.com/".to_string());
|
|
||||||
/// ```
|
|
||||||
pub const fn uri(&self) -> &String { &self.uri }
|
|
||||||
|
|
||||||
/// Sets the `URI` that identifies the associated media playlist.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
///
|
|
||||||
/// stream.set_uri("https://www.google.com/");
|
|
||||||
/// assert_eq!(stream.uri(), &"https://www.google.com/".to_string());
|
|
||||||
/// ```
|
|
||||||
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.uri = value.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the maximum frame rate for all the video in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.frame_rate(), None);
|
|
||||||
///
|
|
||||||
/// stream.set_frame_rate(Some(59.9));
|
|
||||||
/// assert_eq!(stream.frame_rate(), Some(59.9));
|
|
||||||
/// ```
|
|
||||||
pub fn set_frame_rate(&mut self, value: Option<f64>) -> &mut Self {
|
|
||||||
self.frame_rate = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the maximum frame rate for all the video in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.frame_rate(), None);
|
|
||||||
///
|
|
||||||
/// stream.set_frame_rate(Some(59.9));
|
|
||||||
/// assert_eq!(stream.frame_rate(), Some(59.9));
|
|
||||||
/// ```
|
|
||||||
pub fn frame_rate(&self) -> Option<f64> { self.frame_rate.map(|v| v.as_f64()) }
|
|
||||||
|
|
||||||
/// Returns the group identifier for the audio in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.audio(), &None);
|
|
||||||
///
|
|
||||||
/// stream.set_audio(Some("audio"));
|
|
||||||
/// assert_eq!(stream.audio(), &Some("audio".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub const fn audio(&self) -> &Option<String> { &self.audio }
|
|
||||||
|
|
||||||
/// Sets the group identifier for the audio in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.audio(), &None);
|
|
||||||
///
|
|
||||||
/// stream.set_audio(Some("audio"));
|
|
||||||
/// assert_eq!(stream.audio(), &Some("audio".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn set_audio<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.audio = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the group identifier for the subtitles in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.subtitles(), &None);
|
|
||||||
///
|
|
||||||
/// stream.set_subtitles(Some("subs"));
|
|
||||||
/// assert_eq!(stream.subtitles(), &Some("subs".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub const fn subtitles(&self) -> &Option<String> { &self.subtitles }
|
|
||||||
|
|
||||||
/// Sets the group identifier for the subtitles in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.subtitles(), &None);
|
|
||||||
///
|
|
||||||
/// stream.set_subtitles(Some("subs"));
|
|
||||||
/// assert_eq!(stream.subtitles(), &Some("subs".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn set_subtitles<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.subtitles = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the value of [`ClosedCaptions`] attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// use hls_m3u8::types::ClosedCaptions;
|
|
||||||
///
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.closed_captions(), &None);
|
|
||||||
///
|
|
||||||
/// stream.set_closed_captions(Some(ClosedCaptions::None));
|
|
||||||
/// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None));
|
|
||||||
/// ```
|
|
||||||
pub const fn closed_captions(&self) -> &Option<ClosedCaptions> { &self.closed_captions }
|
|
||||||
|
|
||||||
/// Sets the value of [`ClosedCaptions`] attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
|
||||||
/// use hls_m3u8::types::ClosedCaptions;
|
|
||||||
///
|
|
||||||
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
|
|
||||||
/// # assert_eq!(stream.closed_captions(), &None);
|
|
||||||
///
|
|
||||||
/// stream.set_closed_captions(Some(ClosedCaptions::None));
|
|
||||||
/// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None));
|
|
||||||
/// ```
|
|
||||||
pub fn set_closed_captions(&mut self, value: Option<ClosedCaptions>) -> &mut Self {
|
|
||||||
self.closed_captions = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
|
||||||
impl RequiredVersion for ExtXStreamInf {
|
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ExtXStreamInf {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.stream_inf)?;
|
|
||||||
if let Some(value) = &self.frame_rate {
|
|
||||||
write!(f, ",FRAME-RATE={:.3}", value.as_f64())?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.audio {
|
|
||||||
write!(f, ",AUDIO={}", quote(value))?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.subtitles {
|
|
||||||
write!(f, ",SUBTITLES={}", quote(value))?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.closed_captions {
|
|
||||||
write!(f, ",CLOSED-CAPTIONS={}", value)?;
|
|
||||||
}
|
|
||||||
write!(f, "\n{}", self.uri)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ExtXStreamInf {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
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 input = tag(first_line, Self::PREFIX)?;
|
|
||||||
|
|
||||||
let mut frame_rate = None;
|
|
||||||
let mut audio = None;
|
|
||||||
let mut subtitles = None;
|
|
||||||
let mut closed_captions = None;
|
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
|
||||||
match key.as_str() {
|
|
||||||
"FRAME-RATE" => frame_rate = Some((value.parse())?),
|
|
||||||
"AUDIO" => audio = Some(unquote(value)),
|
|
||||||
"SUBTITLES" => subtitles = Some(unquote(value)),
|
|
||||||
"CLOSED-CAPTIONS" => closed_captions = Some(value.parse()?),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
uri: uri.to_string(),
|
|
||||||
frame_rate,
|
|
||||||
audio,
|
|
||||||
subtitles,
|
|
||||||
closed_captions,
|
|
||||||
stream_inf: input.parse()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for ExtXStreamInf {
|
|
||||||
type Target = StreamInf;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.stream_inf }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for ExtXStreamInf {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream_inf }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com"
|
|
||||||
.parse::<ExtXStreamInf>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
stream_inf,
|
|
||||||
ExtXStreamInf::new("http://www.example.com", 1000)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_display() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXStreamInf::new("http://www.example.com/", 1000).to_string(),
|
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_required_version() {
|
|
||||||
assert_eq!(
|
|
||||||
ProtocolVersion::V1,
|
|
||||||
ExtXStreamInf::new("http://www.example.com", 1000).required_version()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXStreamInf::new("http://www.example.com", 1000).bandwidth(),
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref_mut() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXStreamInf::new("http://www.example.com", 1000)
|
|
||||||
.set_bandwidth(1)
|
|
||||||
.bandwidth(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
477
src/tags/master_playlist/variant_stream.rs
Normal file
477
src/tags/master_playlist/variant_stream.rs
Normal file
|
@ -0,0 +1,477 @@
|
||||||
|
use core::fmt;
|
||||||
|
use core::ops::Deref;
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::tags::ExtXMedia;
|
||||||
|
use crate::traits::RequiredVersion;
|
||||||
|
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, StreamData, UFloat};
|
||||||
|
use crate::utils::{quote, tag, unquote};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// A server may offer multiple [`MediaPlaylist`] files to provide different
|
||||||
|
/// encodings of the same presentation.
|
||||||
|
///
|
||||||
|
/// If it does so, it should provide
|
||||||
|
/// a [`MasterPlaylist`] that lists each [`VariantStream`] to allow
|
||||||
|
/// clients to switch between encodings dynamically.
|
||||||
|
///
|
||||||
|
/// The server must meet the following constraints when producing
|
||||||
|
/// [`VariantStream`]s in order to allow clients to switch between them
|
||||||
|
/// seamlessly:
|
||||||
|
///
|
||||||
|
/// - Each [`VariantStream`] must present the same content.
|
||||||
|
///
|
||||||
|
/// - Matching content in [`VariantStream`]s must have matching timestamps. This
|
||||||
|
/// allows clients to synchronize the media.
|
||||||
|
///
|
||||||
|
/// - Matching content in [`VariantStream`]s must have matching
|
||||||
|
/// [`ExtXDiscontinuitySequence`].
|
||||||
|
///
|
||||||
|
/// - 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
|
||||||
|
/// different target durations if they have [`PlaylistType::Vod`].
|
||||||
|
///
|
||||||
|
/// - 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.
|
||||||
|
///
|
||||||
|
/// - If any [`MediaPlaylist`]s have an [`PlaylistType`] tag, all
|
||||||
|
/// [`MediaPlaylist`]s must have an [`PlaylistType`] tag with the same value.
|
||||||
|
///
|
||||||
|
/// - 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.
|
||||||
|
///
|
||||||
|
/// - 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.
|
||||||
|
///
|
||||||
|
/// - 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.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
|
||||||
|
/// [`ExtXDiscontinuitySequence`]: crate::tags::ExtXDiscontinuitySequence
|
||||||
|
/// [`PlaylistType::Vod`]: crate::types::PlaylistType::Vod
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
|
/// [`ExtXDateRange`]: crate::tags::ExtXDateRange
|
||||||
|
/// [`ExtXProgramDateTime`]: crate::tags::ExtXProgramDateTime
|
||||||
|
/// [`PlaylistType`]: crate::types::PlaylistType
|
||||||
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
|
pub enum VariantStream {
|
||||||
|
/// 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
|
||||||
|
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
|
||||||
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
|
uri: String,
|
||||||
|
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
||||||
|
/// [`VariantStream::ExtXIFrame`].
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
stream_data: StreamData,
|
||||||
|
},
|
||||||
|
/// [`VariantStream::ExtXStreamInf`] specifies a [`VariantStream`], which is
|
||||||
|
/// a set of renditions that can be combined to play the presentation.
|
||||||
|
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
|
||||||
|
uri: String,
|
||||||
|
/// 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
|
||||||
|
audio: Option<String>,
|
||||||
|
/// 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
|
||||||
|
subtitles: Option<String>,
|
||||||
|
/// It indicates the set of closed-caption renditions that can be used
|
||||||
|
/// when playing the presentation.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
closed_captions: Option<ClosedCaptions>,
|
||||||
|
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
||||||
|
/// [`VariantStream::ExtXIFrame`].
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
stream_data: StreamData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VariantStream {
|
||||||
|
pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||||
|
pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:";
|
||||||
|
|
||||||
|
/// 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(),
|
||||||
|
/// ));
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_associated(&self, media: &ExtXMedia) -> bool {
|
||||||
|
match &self {
|
||||||
|
Self::ExtXIFrame { stream_data, .. } => {
|
||||||
|
if let MediaType::Video = media.media_type {
|
||||||
|
if let Some(value) = stream_data.video() {
|
||||||
|
return value == media.group_id();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Self::ExtXStreamInf {
|
||||||
|
audio,
|
||||||
|
subtitles,
|
||||||
|
closed_captions,
|
||||||
|
stream_data,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
match media.media_type {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
|
impl RequiredVersion for VariantStream {
|
||||||
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for VariantStream {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for VariantStream {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) {
|
||||||
|
let uri = AttributePairs::new(input)
|
||||||
|
.find_map(|(key, value)| {
|
||||||
|
if key == "URI" {
|
||||||
|
Some(unquote(value))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or_else(|| Error::missing_value("URI"))?;
|
||||||
|
|
||||||
|
Ok(Self::ExtXIFrame {
|
||||||
|
uri,
|
||||||
|
stream_data: input.parse()?,
|
||||||
|
})
|
||||||
|
} 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)),
|
||||||
|
"CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self::ExtXStreamInf {
|
||||||
|
uri: uri.to_string(),
|
||||||
|
frame_rate,
|
||||||
|
audio,
|
||||||
|
subtitles,
|
||||||
|
closed_captions,
|
||||||
|
stream_data: first_line.parse()?,
|
||||||
|
})
|
||||||
|
} 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
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for VariantStream {
|
||||||
|
type Target = StreamData;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match &self {
|
||||||
|
Self::ExtXIFrame { stream_data, .. } | Self::ExtXStreamInf { stream_data, .. } => {
|
||||||
|
stream_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<&VariantStream> for VariantStream {
|
||||||
|
fn eq(&self, other: &&Self) -> bool { self.eq(*other) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::InStreamId;
|
||||||
|
//use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[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(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,81 +3,39 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::RequiredVersion;
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
/// Allows synchronization between different renditions of the same
|
||||||
|
/// [`VariantStream`].
|
||||||
///
|
///
|
||||||
/// The [`ExtXDiscontinuitySequence`] tag allows synchronization between
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
/// different Renditions of the same Variant Stream or different Variant
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
|
||||||
/// Streams that have [`ExtXDiscontinuity`] tags in their [`Media Playlist`]s.
|
pub(crate) struct ExtXDiscontinuitySequence(pub usize);
|
||||||
///
|
|
||||||
/// Its format is:
|
|
||||||
/// ```text
|
|
||||||
/// #EXT-X-DISCONTINUITY-SEQUENCE:<number>
|
|
||||||
/// ```
|
|
||||||
/// where `number` is a [u64].
|
|
||||||
///
|
|
||||||
/// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
/// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]:
|
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3
|
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
|
||||||
pub struct ExtXDiscontinuitySequence(u64);
|
|
||||||
|
|
||||||
impl ExtXDiscontinuitySequence {
|
impl ExtXDiscontinuitySequence {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
||||||
|
|
||||||
/// Makes a new [ExtXDiscontinuitySequence] tag.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXDiscontinuitySequence;
|
|
||||||
/// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5);
|
|
||||||
/// ```
|
|
||||||
pub const fn new(seq_num: u64) -> Self { Self(seq_num) }
|
|
||||||
|
|
||||||
/// Returns the discontinuity sequence number of
|
|
||||||
/// the first media segment that appears in the associated playlist.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXDiscontinuitySequence;
|
|
||||||
/// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5);
|
|
||||||
///
|
|
||||||
/// assert_eq!(discontinuity_sequence.seq_num(), 5);
|
|
||||||
/// ```
|
|
||||||
pub const fn seq_num(self) -> u64 { self.0 }
|
|
||||||
|
|
||||||
/// Sets the sequence number.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXDiscontinuitySequence;
|
|
||||||
/// let mut discontinuity_sequence = ExtXDiscontinuitySequence::new(5);
|
|
||||||
///
|
|
||||||
/// discontinuity_sequence.set_seq_num(10);
|
|
||||||
/// assert_eq!(discontinuity_sequence.seq_num(), 10);
|
|
||||||
/// ```
|
|
||||||
pub fn set_seq_num(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.0 = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXDiscontinuitySequence {
|
impl RequiredVersion for ExtXDiscontinuitySequence {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXDiscontinuitySequence {
|
impl fmt::Display for ExtXDiscontinuitySequence {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
//
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuitySequence {
|
impl FromStr for ExtXDiscontinuitySequence {
|
||||||
type Err = crate::Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let seq_num = tag(input, Self::PREFIX)?.parse()?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
Ok(Self::new(seq_num))
|
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
||||||
|
|
||||||
|
Ok(Self(seq_num))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +47,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXDiscontinuitySequence::new(123).to_string(),
|
ExtXDiscontinuitySequence(123).to_string(),
|
||||||
"#EXT-X-DISCONTINUITY-SEQUENCE:123".to_string()
|
"#EXT-X-DISCONTINUITY-SEQUENCE:123".to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -97,7 +55,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXDiscontinuitySequence::new(123).required_version(),
|
ExtXDiscontinuitySequence(123).required_version(),
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -105,16 +63,13 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXDiscontinuitySequence::new(123),
|
ExtXDiscontinuitySequence(123),
|
||||||
"#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap()
|
"#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap()
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
assert_eq!(
|
||||||
fn test_seq_num() {
|
ExtXDiscontinuitySequence::from_str("#EXT-X-DISCONTINUITY-SEQUENCE:12A"),
|
||||||
let mut sequence = ExtXDiscontinuitySequence::new(123);
|
Err(Error::parse_int("12A", "12A".parse::<u64>().expect_err("")))
|
||||||
assert_eq!(sequence.seq_num(), 123);
|
);
|
||||||
sequence.set_seq_num(1);
|
|
||||||
assert_eq!(sequence.seq_num(), 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,32 +5,25 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.4.3.4. EXT-X-ENDLIST]
|
/// Indicates that no more [`MediaSegment`]s will be added to the
|
||||||
/// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be
|
/// [`MediaPlaylist`] file.
|
||||||
/// added to the [`Media Playlist`] file.
|
|
||||||
///
|
///
|
||||||
/// Its format is:
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
/// ```text
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
/// #EXT-X-ENDLIST
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`Media Segment`]: crate::MediaSegment
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
/// [4.4.3.4. EXT-X-ENDLIST]:
|
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ExtXEndList;
|
pub(crate) struct ExtXEndList;
|
||||||
|
|
||||||
impl ExtXEndList {
|
impl ExtXEndList {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXEndList {
|
impl RequiredVersion for ExtXEndList {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXEndList {
|
impl fmt::Display for ExtXEndList {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXEndList {
|
impl FromStr for ExtXEndList {
|
||||||
|
|
|
@ -5,34 +5,20 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.4.3.6. EXT-X-I-FRAMES-ONLY]
|
|
||||||
/// The [`ExtXIFramesOnly`] tag indicates that each [`Media Segment`] in the
|
|
||||||
/// Playlist describes a single I-frame. I-frames are encoded video
|
|
||||||
/// frames, whose decoding does not depend on any other frame. I-frame
|
|
||||||
/// Playlists can be used for trick play, such as fast forward, rapid
|
|
||||||
/// reverse, and scrubbing.
|
|
||||||
///
|
|
||||||
/// Its format is:
|
|
||||||
/// ```text
|
|
||||||
/// #EXT-X-I-FRAMES-ONLY
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`Media Segment`]: crate::MediaSegment
|
|
||||||
/// [4.4.3.6. EXT-X-I-FRAMES-ONLY]:
|
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.6
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ExtXIFramesOnly;
|
pub(crate) struct ExtXIFramesOnly;
|
||||||
|
|
||||||
impl ExtXIFramesOnly {
|
impl ExtXIFramesOnly {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V4`].
|
||||||
impl RequiredVersion for ExtXIFramesOnly {
|
impl RequiredVersion for ExtXIFramesOnly {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXIFramesOnly {
|
impl fmt::Display for ExtXIFramesOnly {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXIFramesOnly {
|
impl FromStr for ExtXIFramesOnly {
|
||||||
|
|
|
@ -5,54 +5,13 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.4.3.2. EXT-X-MEDIA-SEQUENCE]
|
/// Indicates the Media Sequence Number of the first `MediaSegment` that
|
||||||
/// The [`ExtXMediaSequence`] tag indicates the Media Sequence Number of
|
/// appears in a `MediaPlaylist`.
|
||||||
/// the first [`Media Segment`] that appears in a Playlist file.
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
///
|
pub(crate) struct ExtXMediaSequence(pub usize);
|
||||||
/// [Media Segment]: crate::MediaSegment
|
|
||||||
/// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]:
|
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2
|
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
|
||||||
pub struct ExtXMediaSequence(u64);
|
|
||||||
|
|
||||||
impl ExtXMediaSequence {
|
impl ExtXMediaSequence {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXMediaSequence`] tag.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMediaSequence;
|
|
||||||
/// let media_sequence = ExtXMediaSequence::new(5);
|
|
||||||
/// ```
|
|
||||||
pub const fn new(seq_num: u64) -> Self { Self(seq_num) }
|
|
||||||
|
|
||||||
/// Returns the sequence number of the first media segment,
|
|
||||||
/// that appears in the associated playlist.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMediaSequence;
|
|
||||||
/// let media_sequence = ExtXMediaSequence::new(5);
|
|
||||||
///
|
|
||||||
/// assert_eq!(media_sequence.seq_num(), 5);
|
|
||||||
/// ```
|
|
||||||
pub const fn seq_num(self) -> u64 { self.0 }
|
|
||||||
|
|
||||||
/// Sets the sequence number.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMediaSequence;
|
|
||||||
/// let mut media_sequence = ExtXMediaSequence::new(5);
|
|
||||||
///
|
|
||||||
/// media_sequence.set_seq_num(10);
|
|
||||||
/// assert_eq!(media_sequence.seq_num(), 10);
|
|
||||||
/// ```
|
|
||||||
pub fn set_seq_num(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.0 = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
|
@ -61,15 +20,20 @@ impl RequiredVersion for ExtXMediaSequence {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXMediaSequence {
|
impl fmt::Display for ExtXMediaSequence {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
//
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXMediaSequence {
|
impl FromStr for ExtXMediaSequence {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let seq_num = tag(input, Self::PREFIX)?.parse()?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
Ok(Self::new(seq_num))
|
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
||||||
|
|
||||||
|
Ok(Self(seq_num))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +45,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMediaSequence::new(123).to_string(),
|
ExtXMediaSequence(123).to_string(),
|
||||||
"#EXT-X-MEDIA-SEQUENCE:123".to_string()
|
"#EXT-X-MEDIA-SEQUENCE:123".to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -89,7 +53,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMediaSequence::new(123).required_version(),
|
ExtXMediaSequence(123).required_version(),
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -97,16 +61,8 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMediaSequence::new(123),
|
ExtXMediaSequence(123),
|
||||||
"#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap()
|
"#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_seq_num() {
|
|
||||||
let mut sequence = ExtXMediaSequence::new(123);
|
|
||||||
assert_eq!(sequence.seq_num(), 123);
|
|
||||||
sequence.set_seq_num(1);
|
|
||||||
assert_eq!(sequence.seq_num(), 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
mod discontinuity_sequence;
|
pub(crate) mod discontinuity_sequence;
|
||||||
mod end_list;
|
pub(crate) mod end_list;
|
||||||
mod i_frames_only;
|
pub(crate) mod i_frames_only;
|
||||||
mod media_sequence;
|
pub(crate) mod media_sequence;
|
||||||
mod playlist_type;
|
pub(crate) mod target_duration;
|
||||||
mod target_duration;
|
|
||||||
|
|
||||||
pub use discontinuity_sequence::*;
|
pub(crate) use discontinuity_sequence::*;
|
||||||
pub use end_list::*;
|
pub(crate) use end_list::*;
|
||||||
pub use i_frames_only::*;
|
pub(crate) use i_frames_only::*;
|
||||||
pub use media_sequence::*;
|
pub(crate) use media_sequence::*;
|
||||||
pub use playlist_type::*;
|
pub(crate) use target_duration::*;
|
||||||
pub use target_duration::*;
|
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::utils::tag;
|
|
||||||
use crate::{Error, RequiredVersion};
|
|
||||||
|
|
||||||
/// # [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
|
||||||
///
|
|
||||||
/// The [`ExtXPlaylistType`] tag provides mutability information about the
|
|
||||||
/// [`Media Playlist`]. It applies to the entire [`Media Playlist`].
|
|
||||||
///
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub enum ExtXPlaylistType {
|
|
||||||
/// If the [`ExtXPlaylistType`] is Event, [`Media Segment`]s
|
|
||||||
/// can only be added to the end of the [`Media Playlist`].
|
|
||||||
///
|
|
||||||
/// [`Media Segment`]: crate::MediaSegment
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
Event,
|
|
||||||
/// If the [`ExtXPlaylistType`] is Video On Demand (Vod),
|
|
||||||
/// the [`Media Playlist`] cannot change.
|
|
||||||
///
|
|
||||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
|
||||||
Vod,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXPlaylistType {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
|
||||||
impl RequiredVersion for ExtXPlaylistType {
|
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ExtXPlaylistType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match &self {
|
|
||||||
Self::Event => write!(f, "{}EVENT", Self::PREFIX),
|
|
||||||
Self::Vod => write!(f, "{}VOD", Self::PREFIX),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ExtXPlaylistType {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
let input = tag(input, Self::PREFIX)?;
|
|
||||||
match input {
|
|
||||||
"EVENT" => Ok(Self::Event),
|
|
||||||
"VOD" => Ok(Self::Vod),
|
|
||||||
_ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-PLAYLIST-TYPE:VOD"
|
|
||||||
.parse::<ExtXPlaylistType>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXPlaylistType::Vod,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-PLAYLIST-TYPE:EVENT"
|
|
||||||
.parse::<ExtXPlaylistType>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXPlaylistType::Event,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("#EXT-X-PLAYLIST-TYPE:H"
|
|
||||||
.parse::<ExtXPlaylistType>()
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
assert!("garbage".parse::<ExtXPlaylistType>().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_display() {
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-PLAYLIST-TYPE:VOD".to_string(),
|
|
||||||
ExtXPlaylistType::Vod.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"#EXT-X-PLAYLIST-TYPE:EVENT".to_string(),
|
|
||||||
ExtXPlaylistType::Event.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_required_version() {
|
|
||||||
assert_eq!(
|
|
||||||
ExtXPlaylistType::Vod.required_version(),
|
|
||||||
ProtocolVersion::V1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ExtXPlaylistType::Event.required_version(),
|
|
||||||
ProtocolVersion::V1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Deref;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -7,44 +6,12 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.3.1. EXT-X-TARGETDURATION]
|
/// Specifies the maximum `MediaSegment` duration.
|
||||||
/// The [`ExtXTargetDuration`] tag specifies the maximum [`MediaSegment`]
|
|
||||||
/// duration.
|
|
||||||
///
|
|
||||||
/// [`MediaSegment`]: crate::MediaSegment
|
|
||||||
/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
|
||||||
pub struct ExtXTargetDuration(Duration);
|
pub(crate) struct ExtXTargetDuration(pub Duration);
|
||||||
|
|
||||||
impl ExtXTargetDuration {
|
impl ExtXTargetDuration {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXTargetDuration`] tag.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXTargetDuration;
|
|
||||||
/// use std::time::Duration;
|
|
||||||
///
|
|
||||||
/// let target_duration = ExtXTargetDuration::new(Duration::from_secs(20));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
/// The nanoseconds part of the [`Duration`] will be discarded.
|
|
||||||
pub const fn new(duration: Duration) -> Self { Self(Duration::from_secs(duration.as_secs())) }
|
|
||||||
|
|
||||||
/// Returns the maximum media segment duration.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXTargetDuration;
|
|
||||||
/// use std::time::Duration;
|
|
||||||
///
|
|
||||||
/// let target_duration = ExtXTargetDuration::new(Duration::from_nanos(2_000_000_000));
|
|
||||||
///
|
|
||||||
/// assert_eq!(target_duration.duration(), Duration::from_secs(2));
|
|
||||||
/// ```
|
|
||||||
pub const fn duration(&self) -> Duration { self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
|
@ -52,14 +19,8 @@ impl RequiredVersion for ExtXTargetDuration {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for ExtXTargetDuration {
|
|
||||||
type Target = Duration;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ExtXTargetDuration {
|
impl fmt::Display for ExtXTargetDuration {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.0.as_secs())
|
write!(f, "{}{}", Self::PREFIX, self.0.as_secs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,8 +29,11 @@ impl FromStr for ExtXTargetDuration {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let input = tag(input, Self::PREFIX)?.parse()?;
|
let input = tag(input, Self::PREFIX)?
|
||||||
Ok(Self::new(Duration::from_secs(input)))
|
.parse()
|
||||||
|
.map_err(|e| Error::parse_int(input, e))?;
|
||||||
|
|
||||||
|
Ok(Self(Duration::from_secs(input)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +45,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXTargetDuration::new(Duration::from_secs(5)).to_string(),
|
ExtXTargetDuration(Duration::from_secs(5)).to_string(),
|
||||||
"#EXT-X-TARGETDURATION:5".to_string()
|
"#EXT-X-TARGETDURATION:5".to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -89,7 +53,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXTargetDuration::new(Duration::from_secs(5)).required_version(),
|
ExtXTargetDuration(Duration::from_secs(5)).required_version(),
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -97,13 +61,8 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXTargetDuration::new(Duration::from_secs(5)),
|
ExtXTargetDuration(Duration::from_secs(5)),
|
||||||
"#EXT-X-TARGETDURATION:5".parse().unwrap()
|
"#EXT-X-TARGETDURATION:5".parse().unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref() {
|
|
||||||
assert_eq!(ExtXTargetDuration::new(Duration::from_secs(5)).as_secs(), 5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +1,186 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
||||||
|
|
||||||
|
use derive_more::{AsMut, AsRef, Deref, DerefMut, From};
|
||||||
|
|
||||||
use crate::types::{ByteRange, ProtocolVersion};
|
use crate::types::{ByteRange, ProtocolVersion};
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.4.2.2. EXT-X-BYTERANGE]
|
/// Indicates that a [`MediaSegment`] is a sub-range of the resource identified
|
||||||
|
/// by its `URI`.
|
||||||
///
|
///
|
||||||
/// The [`ExtXByteRange`] tag indicates that a [`Media Segment`] is a sub-range
|
/// # Example
|
||||||
/// of the resource identified by its `URI`.
|
|
||||||
///
|
///
|
||||||
/// Its format is:
|
/// Constructing an [`ExtXByteRange`]:
|
||||||
/// ```text
|
///
|
||||||
/// #EXT-X-BYTERANGE:<n>[@<o>]
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// assert_eq!(ExtXByteRange::from(22..55), ExtXByteRange::from(22..=54));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// where `n` is a [usize] indicating the length of the sub-range in bytes.
|
/// It is also possible to omit the start, in which case it assumes that the
|
||||||
/// If present, `o` is a [usize] indicating the start of the sub-range,
|
/// [`ExtXByteRange`] starts at the byte after the end of the previous
|
||||||
/// as a byte offset from the beginning of the resource.
|
/// [`ExtXByteRange`] or 0 if there is no previous one.
|
||||||
///
|
///
|
||||||
/// [`Media Segment`]: crate::MediaSegment
|
/// ```
|
||||||
/// [4.4.2.2. EXT-X-BYTERANGE]:
|
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.2
|
/// assert_eq!(ExtXByteRange::from(..55), ExtXByteRange::from(..=54));
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
#[derive(
|
||||||
|
AsRef, AsMut, From, Deref, DerefMut, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord,
|
||||||
|
)]
|
||||||
|
#[from(forward)]
|
||||||
pub struct ExtXByteRange(ByteRange);
|
pub struct ExtXByteRange(ByteRange);
|
||||||
|
|
||||||
impl ExtXByteRange {
|
impl ExtXByteRange {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXByteRange`] tag.
|
/// Adds `num` to the `start` and `end` of the range.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXByteRange;
|
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||||
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
/// let range = ExtXByteRange::from(10..22);
|
||||||
|
/// let nrange = range.saturating_add(5);
|
||||||
|
///
|
||||||
|
/// assert_eq!(nrange.len(), range.len());
|
||||||
|
/// assert_eq!(nrange.start(), range.start().map(|c| c + 5));
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn new(length: usize, start: Option<usize>) -> Self {
|
///
|
||||||
Self(ByteRange::new(length, start))
|
/// # Overflow
|
||||||
}
|
///
|
||||||
|
/// If the range is saturated it will not overflow and instead
|
||||||
|
/// stay at it's current value.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// let range = ExtXByteRange::from(5..usize::max_value());
|
||||||
|
///
|
||||||
|
/// // this would cause the end to overflow
|
||||||
|
/// let nrange = range.saturating_add(1);
|
||||||
|
///
|
||||||
|
/// // but the range remains unchanged
|
||||||
|
/// assert_eq!(range, nrange);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// The length of the range will remain unchanged,
|
||||||
|
/// if the `start` is `Some`.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn saturating_add(self, num: usize) -> Self { Self(self.0.saturating_add(num)) }
|
||||||
|
|
||||||
/// Converts the [`ExtXByteRange`] to a [`ByteRange`].
|
/// Subtracts `num` from the `start` and `end` of the range.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// let range = ExtXByteRange::from(10..22);
|
||||||
|
/// let nrange = range.saturating_sub(5);
|
||||||
|
///
|
||||||
|
/// assert_eq!(nrange.len(), range.len());
|
||||||
|
/// assert_eq!(nrange.start(), range.start().map(|c| c - 5));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Underflow
|
||||||
|
///
|
||||||
|
/// If the range is saturated it will not underflow and instead stay
|
||||||
|
/// at it's current value.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// let range = ExtXByteRange::from(0..10);
|
||||||
|
///
|
||||||
|
/// // this would cause the start to underflow
|
||||||
|
/// let nrange = range.saturating_sub(1);
|
||||||
|
///
|
||||||
|
/// // but the range remains unchanged
|
||||||
|
/// assert_eq!(range, nrange);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// The length of the range will remain unchanged,
|
||||||
|
/// if the `start` is `Some`.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn saturating_sub(self, num: usize) -> Self { Self(self.0.saturating_sub(num)) }
|
||||||
|
|
||||||
|
/// Returns a shared reference to the underlying [`ByteRange`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXByteRange;
|
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||||
/// use hls_m3u8::types::ByteRange;
|
/// use hls_m3u8::types::ByteRange;
|
||||||
///
|
///
|
||||||
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
/// assert_eq!(
|
||||||
/// let range: ByteRange = byte_range.to_range();
|
/// ExtXByteRange::from(2..11).as_byte_range(),
|
||||||
|
/// &ByteRange::from(2..11)
|
||||||
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn to_range(&self) -> ByteRange { self.0 }
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_byte_range(&self) -> &ByteRange { &self.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V4`].
|
||||||
impl RequiredVersion for ExtXByteRange {
|
impl RequiredVersion for ExtXByteRange {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for ExtXByteRange {
|
impl Into<ByteRange> for ExtXByteRange {
|
||||||
type Target = ByteRange;
|
fn into(self) -> ByteRange { self.0 }
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for ExtXByteRange {
|
impl<T> Sub<T> for ExtXByteRange
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
where
|
||||||
|
ByteRange: Sub<T, Output = ByteRange>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
fn sub(self, rhs: T) -> Self::Output { Self(self.0.sub(rhs)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SubAssign<T> for ExtXByteRange
|
||||||
|
where
|
||||||
|
ByteRange: SubAssign<T>,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn sub_assign(&mut self, other: T) { self.0.sub_assign(other); }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Add<T> for ExtXByteRange
|
||||||
|
where
|
||||||
|
ByteRange: Add<T, Output = ByteRange>,
|
||||||
|
{
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
fn add(self, rhs: T) -> Self::Output { Self(self.0.add(rhs)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> AddAssign<T> for ExtXByteRange
|
||||||
|
where
|
||||||
|
ByteRange: AddAssign<T>,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn add_assign(&mut self, other: T) { self.0.add_assign(other); }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXByteRange {
|
impl fmt::Display for ExtXByteRange {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "{}", self.0)?;
|
write!(f, "{}", self.0)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -81,23 +193,7 @@ impl FromStr for ExtXByteRange {
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let tokens = input.splitn(2, '@').collect::<Vec<_>>();
|
Ok(Self(ByteRange::from_str(input)?))
|
||||||
|
|
||||||
if tokens.is_empty() {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = tokens[0].parse()?;
|
|
||||||
|
|
||||||
let start = {
|
|
||||||
if tokens.len() == 2 {
|
|
||||||
Some(tokens[1].parse()?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self::new(length, start))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,57 +204,52 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let byte_range = ExtXByteRange::new(0, Some(5));
|
|
||||||
assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:0@5".to_string());
|
|
||||||
|
|
||||||
let byte_range = ExtXByteRange::new(99999, Some(2));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
byte_range.to_string(),
|
ExtXByteRange::from(2..15).to_string(),
|
||||||
"#EXT-X-BYTERANGE:99999@2".to_string()
|
"#EXT-X-BYTERANGE:13@2".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
let byte_range = ExtXByteRange::new(99999, None);
|
assert_eq!(
|
||||||
assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:99999".to_string());
|
ExtXByteRange::from(..22).to_string(),
|
||||||
|
"#EXT-X-BYTERANGE:22".to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
let byte_range = ExtXByteRange::new(99999, Some(2));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
byte_range,
|
ExtXByteRange::from(2..15),
|
||||||
"#EXT-X-BYTERANGE:99999@2".parse::<ExtXByteRange>().unwrap()
|
"#EXT-X-BYTERANGE:13@2".parse().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
let byte_range = ExtXByteRange::new(99999, None);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
byte_range,
|
ExtXByteRange::from(..22),
|
||||||
"#EXT-X-BYTERANGE:99999".parse::<ExtXByteRange>().unwrap()
|
"#EXT-X-BYTERANGE:22".parse().unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deref() {
|
fn test_deref() {
|
||||||
let byte_range = ExtXByteRange::new(0, Some(22));
|
let byte_range = ExtXByteRange::from(0..22);
|
||||||
|
|
||||||
assert_eq!(byte_range.length(), 0);
|
assert_eq!(byte_range.len(), 22);
|
||||||
assert_eq!(byte_range.start(), Some(22));
|
assert_eq!(byte_range.start(), Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deref_mut() {
|
fn test_deref_mut() {
|
||||||
let mut byte_range = ExtXByteRange::new(0, Some(22));
|
let mut byte_range = ExtXByteRange::from(10..110);
|
||||||
|
|
||||||
byte_range.set_length(100);
|
|
||||||
byte_range.set_start(Some(50));
|
byte_range.set_start(Some(50));
|
||||||
|
|
||||||
assert_eq!(byte_range.length(), 100);
|
assert_eq!(byte_range.len(), 60);
|
||||||
assert_eq!(byte_range.start(), Some(50));
|
assert_eq!(byte_range.start(), Some(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXByteRange::new(20, Some(5)).required_version(),
|
ExtXByteRange::from(5..20).required_version(),
|
||||||
ProtocolVersion::V4
|
ProtocolVersion::V4
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,31 +5,22 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.4.2.3. EXT-X-DISCONTINUITY]
|
/// The `ExtXDiscontinuity` tag indicates a discontinuity between the
|
||||||
/// The [`ExtXDiscontinuity`] tag indicates a discontinuity between the
|
/// `MediaSegment` that follows it and the one that preceded it.
|
||||||
/// [`Media Segment`] that follows it and the one that preceded it.
|
|
||||||
///
|
|
||||||
/// Its format is:
|
|
||||||
/// ```text
|
|
||||||
/// #EXT-X-DISCONTINUITY
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`Media Segment`]: crate::MediaSegment
|
|
||||||
/// [4.4.2.3. EXT-X-DISCONTINUITY]:
|
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ExtXDiscontinuity;
|
pub(crate) struct ExtXDiscontinuity;
|
||||||
|
|
||||||
impl ExtXDiscontinuity {
|
impl ExtXDiscontinuity {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXDiscontinuity {
|
impl RequiredVersion for ExtXDiscontinuity {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXDiscontinuity {
|
impl fmt::Display for ExtXDiscontinuity {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuity {
|
impl FromStr for ExtXDiscontinuity {
|
||||||
|
|
|
@ -2,19 +2,18 @@ use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use derive_more::AsRef;
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.2.1. EXTINF]
|
/// Specifies the duration of a [`Media Segment`].
|
||||||
///
|
|
||||||
/// The [`ExtInf`] tag specifies the duration of a [`Media Segment`]. It applies
|
|
||||||
/// only to the next [`Media Segment`].
|
|
||||||
///
|
///
|
||||||
/// [`Media Segment`]: crate::media_segment::MediaSegment
|
/// [`Media Segment`]: crate::media_segment::MediaSegment
|
||||||
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
|
#[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct ExtInf {
|
pub struct ExtInf {
|
||||||
|
#[as_ref]
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -25,12 +24,14 @@ impl ExtInf {
|
||||||
/// Makes a new [`ExtInf`] tag.
|
/// Makes a new [`ExtInf`] tag.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtInf;
|
/// # use hls_m3u8::tags::ExtInf;
|
||||||
/// use std::time::Duration;
|
/// use std::time::Duration;
|
||||||
///
|
///
|
||||||
/// let ext_inf = ExtInf::new(Duration::from_secs(5));
|
/// let ext_inf = ExtInf::new(Duration::from_secs(5));
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub const fn new(duration: Duration) -> Self {
|
pub const fn new(duration: Duration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
duration,
|
duration,
|
||||||
|
@ -41,22 +42,25 @@ impl ExtInf {
|
||||||
/// Makes a new [`ExtInf`] tag with the given title.
|
/// Makes a new [`ExtInf`] tag with the given title.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtInf;
|
/// # use hls_m3u8::tags::ExtInf;
|
||||||
/// use std::time::Duration;
|
/// use std::time::Duration;
|
||||||
///
|
///
|
||||||
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self {
|
#[must_use]
|
||||||
|
pub fn with_title<T: Into<String>>(duration: Duration, title: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
duration,
|
duration,
|
||||||
title: Some(title.to_string()),
|
title: Some(title.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the duration of the associated media segment.
|
/// Returns the duration of the associated media segment.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtInf;
|
/// # use hls_m3u8::tags::ExtInf;
|
||||||
/// use std::time::Duration;
|
/// use std::time::Duration;
|
||||||
|
@ -65,11 +69,13 @@ impl ExtInf {
|
||||||
///
|
///
|
||||||
/// assert_eq!(ext_inf.duration(), Duration::from_secs(5));
|
/// assert_eq!(ext_inf.duration(), Duration::from_secs(5));
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub const fn duration(&self) -> Duration { self.duration }
|
pub const fn duration(&self) -> Duration { self.duration }
|
||||||
|
|
||||||
/// Sets the duration of the associated media segment.
|
/// Sets the duration of the associated media segment.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtInf;
|
/// # use hls_m3u8::tags::ExtInf;
|
||||||
/// use std::time::Duration;
|
/// use std::time::Duration;
|
||||||
|
@ -88,6 +94,7 @@ impl ExtInf {
|
||||||
/// Returns the title of the associated media segment.
|
/// Returns the title of the associated media segment.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtInf;
|
/// # use hls_m3u8::tags::ExtInf;
|
||||||
/// use std::time::Duration;
|
/// use std::time::Duration;
|
||||||
|
@ -96,11 +103,13 @@ impl ExtInf {
|
||||||
///
|
///
|
||||||
/// assert_eq!(ext_inf.title(), &Some("title".to_string()));
|
/// assert_eq!(ext_inf.title(), &Some("title".to_string()));
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub const fn title(&self) -> &Option<String> { &self.title }
|
pub const fn title(&self) -> &Option<String> { &self.title }
|
||||||
|
|
||||||
/// Sets the title of the associated media segment.
|
/// Sets the title of the associated media segment.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtInf;
|
/// # use hls_m3u8::tags::ExtInf;
|
||||||
/// use std::time::Duration;
|
/// use std::time::Duration;
|
||||||
|
@ -117,6 +126,8 @@ impl ExtInf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`], if the duration does not have
|
||||||
|
/// nanoseconds, otherwise it requires [`ProtocolVersion::V3`].
|
||||||
impl RequiredVersion for ExtInf {
|
impl RequiredVersion for ExtInf {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
if self.duration.subsec_nanos() == 0 {
|
if self.duration.subsec_nanos() == 0 {
|
||||||
|
@ -128,7 +139,7 @@ impl RequiredVersion for ExtInf {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtInf {
|
impl fmt::Display for ExtInf {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "{},", self.duration.as_secs_f64())?;
|
write!(f, "{},", self.duration.as_secs_f64())?;
|
||||||
|
|
||||||
|
@ -143,29 +154,20 @@ impl FromStr for ExtInf {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let mut input = tag(input, Self::PREFIX)?.splitn(2, ',');
|
||||||
let tokens = input.splitn(2, ',').collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if tokens.is_empty() {
|
let duration = input.next().unwrap();
|
||||||
return Err(Error::custom(format!(
|
let duration = Duration::from_secs_f64(
|
||||||
"failed to parse #EXTINF tag, couldn't split input: {:?}",
|
duration
|
||||||
input
|
.parse()
|
||||||
)));
|
.map_err(|e| Error::parse_float(duration, e))?,
|
||||||
}
|
);
|
||||||
|
|
||||||
let duration = Duration::from_secs_f64(tokens[0].parse()?);
|
let title = input
|
||||||
|
.next()
|
||||||
let title = {
|
.map(str::trim)
|
||||||
if tokens.len() >= 2 {
|
.filter(|value| !value.is_empty())
|
||||||
if tokens[1].trim().is_empty() {
|
.map(ToString::to_string);
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(tokens[1].to_string())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self { duration, title })
|
Ok(Self { duration, title })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +1,179 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion};
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.2.4. EXT-X-KEY]
|
/// Specifies how to decrypt encrypted data from the server.
|
||||||
///
|
///
|
||||||
/// [`Media Segment`]s may be encrypted. The [`ExtXKey`] tag specifies how to
|
/// An unencrypted segment should be marked with [`ExtXKey::empty`].
|
||||||
/// decrypt them. It applies to every [`Media Segment`] and to every Media
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||||
/// Initialization Section declared by an [`ExtXMap`] tag, that appears
|
pub struct ExtXKey(pub Option<DecryptionKey>);
|
||||||
/// between it and the next [`ExtXKey`] tag in the Playlist file with the
|
|
||||||
/// same [`KeyFormat`] attribute (or the end of the Playlist file).
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
/// In case of an empty key ([`EncryptionMethod::None`]),
|
|
||||||
/// all attributes will be ignored.
|
|
||||||
///
|
|
||||||
/// [`KeyFormat`]: crate::types::KeyFormat
|
|
||||||
/// [`ExtXMap`]: crate::tags::ExtXMap
|
|
||||||
/// [`Media Segment`]: crate::MediaSegment
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct ExtXKey(DecryptionKey);
|
|
||||||
|
|
||||||
impl ExtXKey {
|
impl ExtXKey {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXKey`] tag.
|
/// Constructs an [`ExtXKey`] tag.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXKey;
|
/// # use hls_m3u8::tags::ExtXKey;
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod, KeyFormat};
|
||||||
///
|
///
|
||||||
/// let key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
/// let key = ExtXKey::new(
|
||||||
|
/// DecryptionKey::builder()
|
||||||
|
/// .method(EncryptionMethod::Aes128)
|
||||||
|
/// .uri("https://www.example.com/")
|
||||||
|
/// .iv([
|
||||||
|
/// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
/// ])
|
||||||
|
/// .format(KeyFormat::Identity)
|
||||||
|
/// .versions(vec![1, 2, 3, 4, 5])
|
||||||
|
/// .build()?,
|
||||||
|
/// );
|
||||||
|
/// # Ok::<(), String>(())
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub const fn new(inner: DecryptionKey) -> Self { Self(Some(inner)) }
|
||||||
|
|
||||||
|
/// Constructs an empty [`ExtXKey`], which signals that a segment is
|
||||||
|
/// unencrypted.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXKey;
|
||||||
|
/// assert_eq!(ExtXKey::empty(), ExtXKey(None));
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub const fn empty() -> Self { Self(None) }
|
||||||
|
|
||||||
|
/// Returns `true` if the key is not empty.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXKey;
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let k = ExtXKey::new(DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.url",
|
||||||
|
/// ));
|
||||||
|
/// assert_eq!(k.is_some(), true);
|
||||||
|
///
|
||||||
|
/// let k = ExtXKey::empty();
|
||||||
|
/// assert_eq!(k.is_some(), false);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub fn is_some(&self) -> bool { self.0.is_some() }
|
||||||
|
|
||||||
|
/// Returns `true` if the key is empty.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXKey;
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let k = ExtXKey::new(DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.url",
|
||||||
|
/// ));
|
||||||
|
/// assert_eq!(k.is_none(), false);
|
||||||
|
///
|
||||||
|
/// let k = ExtXKey::empty();
|
||||||
|
/// assert_eq!(k.is_none(), true);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub fn is_none(&self) -> bool { self.0.is_none() }
|
||||||
|
|
||||||
|
/// Returns the underlying [`DecryptionKey`], if there is one.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if there is no underlying decryption key.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXKey;
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let k = ExtXKey::new(DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.url",
|
||||||
|
/// ));
|
||||||
///
|
///
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// key.to_string(),
|
/// k.unwrap(),
|
||||||
/// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\""
|
/// DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.url")
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: ToString>(method: EncryptionMethod, uri: T) -> Self {
|
///
|
||||||
Self(DecryptionKey::new(method, uri))
|
/// ```{.should_panic}
|
||||||
|
/// # use hls_m3u8::tags::ExtXKey;
|
||||||
|
/// use hls_m3u8::types::DecryptionKey;
|
||||||
|
///
|
||||||
|
/// let decryption_key: DecryptionKey = ExtXKey::empty().unwrap(); // panics
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn unwrap(self) -> DecryptionKey {
|
||||||
|
match self.0 {
|
||||||
|
Some(v) => v,
|
||||||
|
None => panic!("called `ExtXKey::unwrap()` on an empty key"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new [`ExtXKey`] tag without a decryption key.
|
/// Returns a reference to the underlying [`DecryptionKey`].
|
||||||
///
|
#[must_use]
|
||||||
/// # Example
|
#[inline]
|
||||||
/// ```
|
pub fn as_ref(&self) -> Option<&DecryptionKey> { self.0.as_ref() }
|
||||||
/// # use hls_m3u8::tags::ExtXKey;
|
|
||||||
/// let key = ExtXKey::empty();
|
|
||||||
///
|
|
||||||
/// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE");
|
|
||||||
/// ```
|
|
||||||
pub const fn empty() -> Self {
|
|
||||||
Self(DecryptionKey {
|
|
||||||
method: EncryptionMethod::None,
|
|
||||||
uri: None,
|
|
||||||
iv: None,
|
|
||||||
key_format: None,
|
|
||||||
key_format_versions: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the [`EncryptionMethod`] is
|
/// Converts an [`ExtXKey`] into an `Option<DecryptionKey>`.
|
||||||
/// [`None`].
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXKey;
|
/// # use hls_m3u8::tags::ExtXKey;
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
///
|
///
|
||||||
/// let key = ExtXKey::empty();
|
/// assert_eq!(ExtXKey::empty().into_option(), None);
|
||||||
///
|
///
|
||||||
/// assert_eq!(key.method() == EncryptionMethod::None, key.is_empty());
|
/// assert_eq!(
|
||||||
|
/// ExtXKey::new(DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.url"
|
||||||
|
/// ))
|
||||||
|
/// .into_option(),
|
||||||
|
/// Some(DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.url"
|
||||||
|
/// ))
|
||||||
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
///
|
#[must_use]
|
||||||
/// [`None`]: EncryptionMethod::None
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None }
|
pub fn into_option(self) -> Option<DecryptionKey> { self.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
||||||
|
/// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is
|
||||||
|
/// specified.
|
||||||
|
///
|
||||||
|
/// Otherwise [`ProtocolVersion::V1`] is required.
|
||||||
impl RequiredVersion for ExtXKey {
|
impl RequiredVersion for ExtXKey {
|
||||||
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.map_or(ProtocolVersion::V1, |i| i.required_version())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXKey {
|
impl FromStr for ExtXKey {
|
||||||
|
@ -91,22 +181,37 @@ impl FromStr for ExtXKey {
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
Ok(Self(input.parse()?))
|
|
||||||
|
if input.trim() == "METHOD=NONE" {
|
||||||
|
Ok(Self(None))
|
||||||
|
} else {
|
||||||
|
Ok(DecryptionKey::from_str(input)?.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Option<DecryptionKey>> for ExtXKey {
|
||||||
|
fn from(value: Option<DecryptionKey>) -> Self { Self(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DecryptionKey> for ExtXKey {
|
||||||
|
fn from(value: DecryptionKey) -> Self { Self(Some(value)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::tags::ExtXSessionKey> for ExtXKey {
|
||||||
|
fn from(value: crate::tags::ExtXSessionKey) -> Self { Self(Some(value.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXKey {
|
impl fmt::Display for ExtXKey {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
}
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
|
||||||
impl Deref for ExtXKey {
|
if let Some(value) = &self.0 {
|
||||||
type Target = DecryptionKey;
|
write!(f, "{}", value)
|
||||||
|
} else {
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
write!(f, "METHOD=NONE")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl DerefMut for ExtXKey {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -115,45 +220,131 @@ mod test {
|
||||||
use crate::types::{EncryptionMethod, KeyFormat};
|
use crate::types::{EncryptionMethod, KeyFormat};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
macro_rules! generate_tests {
|
||||||
fn test_display() {
|
( $( { $struct:expr, $str:expr } ),+ $(,)* ) => {
|
||||||
assert_eq!(
|
#[test]
|
||||||
ExtXKey::empty().to_string(),
|
fn test_display() {
|
||||||
"#EXT-X-KEY:METHOD=NONE".to_string()
|
$(
|
||||||
);
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
|
||||||
let mut key = ExtXKey::empty();
|
#[test]
|
||||||
// it is expected, that all attributes will be ignored for an empty key!
|
fn test_parser() {
|
||||||
key.set_key_format(Some(KeyFormat::Identity));
|
$(
|
||||||
key.set_iv(Some([
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
)+
|
||||||
]));
|
|
||||||
key.set_uri(Some("https://www.example.com"));
|
|
||||||
key.set_key_format_versions(Some(vec![1, 2, 3]));
|
|
||||||
|
|
||||||
assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string());
|
assert_eq!(
|
||||||
|
ExtXKey::new(
|
||||||
|
DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"http://www.example.com"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-KEY:",
|
||||||
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"http://www.example.com\",",
|
||||||
|
"UNKNOWNTAG=abcd"
|
||||||
|
).parse().unwrap(),
|
||||||
|
);
|
||||||
|
assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::<ExtXKey>().is_err());
|
||||||
|
assert!("garbage".parse::<ExtXKey>().is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
generate_tests! {
|
||||||
fn test_parser() {
|
{
|
||||||
assert_eq!(
|
ExtXKey::empty(),
|
||||||
"#EXT-X-KEY:\
|
"#EXT-X-KEY:METHOD=NONE"
|
||||||
METHOD=AES-128,\
|
},
|
||||||
URI=\"https://priv.example.com/key.php?r=52\""
|
{
|
||||||
.parse::<ExtXKey>()
|
ExtXKey::new(DecryptionKey::new(
|
||||||
.unwrap(),
|
|
||||||
ExtXKey::new(
|
|
||||||
EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
"https://priv.example.com/key.php?r=52"
|
"https://priv.example.com/key.php?r=52"
|
||||||
|
)),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-KEY:",
|
||||||
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"https://priv.example.com/key.php?r=52\""
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExtXKey::new(
|
||||||
|
DecryptionKey::builder()
|
||||||
|
.method(EncryptionMethod::Aes128)
|
||||||
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
|
.iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-KEY:",
|
||||||
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
|
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExtXKey::new(
|
||||||
|
DecryptionKey::builder()
|
||||||
|
.method(EncryptionMethod::Aes128)
|
||||||
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
|
.iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82])
|
||||||
|
.format(KeyFormat::Identity)
|
||||||
|
.versions(vec![1, 2, 3])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"#EXT-X-KEY:",
|
||||||
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
|
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
||||||
|
"KEYFORMAT=\"identity\",",
|
||||||
|
"KEYFORMATVERSIONS=\"1/2/3\""
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_required_version() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXKey::new(DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://www.example.com/"
|
||||||
|
))
|
||||||
|
.required_version(),
|
||||||
|
ProtocolVersion::V1
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut key = ExtXKey::new(
|
assert_eq!(
|
||||||
EncryptionMethod::Aes128,
|
ExtXKey::new(
|
||||||
"https://www.example.com/hls-key/key.bin",
|
DecryptionKey::builder()
|
||||||
|
.method(EncryptionMethod::Aes128)
|
||||||
|
.uri("https://www.example.com/")
|
||||||
|
.format(KeyFormat::Identity)
|
||||||
|
.versions(vec![1, 2, 3])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
.required_version(),
|
||||||
|
ProtocolVersion::V5
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ExtXKey::new(
|
||||||
|
DecryptionKey::builder()
|
||||||
|
.method(EncryptionMethod::Aes128)
|
||||||
|
.uri("https://www.example.com/")
|
||||||
|
.iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
.required_version(),
|
||||||
|
ProtocolVersion::V2
|
||||||
);
|
);
|
||||||
key.set_iv(Some([
|
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,47 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::tags::ExtXKey;
|
use crate::tags::ExtXKey;
|
||||||
use crate::types::{ByteRange, ProtocolVersion};
|
use crate::types::{ByteRange, DecryptionKey, ProtocolVersion};
|
||||||
use crate::utils::{quote, tag, unquote};
|
use crate::utils::{quote, tag, unquote};
|
||||||
use crate::{Encrypted, Error, RequiredVersion};
|
use crate::{Decryptable, Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.2.5. EXT-X-MAP]
|
/// The [`ExtXMap`] tag specifies how to obtain the [Media Initialization
|
||||||
|
/// Section], required to parse the applicable [`MediaSegment`]s.
|
||||||
///
|
///
|
||||||
/// The [`ExtXMap`] tag specifies how to obtain the Media Initialization
|
/// It applies to every [`MediaSegment`] that appears after it in the playlist
|
||||||
/// Section, required to parse the applicable [`MediaSegment`]s.
|
/// until the next [`ExtXMap`] tag or until the end of the playlist.
|
||||||
///
|
///
|
||||||
|
/// An [`ExtXMap`] tag should be supplied for [`MediaSegment`]s in playlists
|
||||||
|
/// with the [`ExtXIFramesOnly`] tag when the first [`MediaSegment`] (i.e.,
|
||||||
|
/// I-frame) in the playlist (or the first segment following an
|
||||||
|
/// [`ExtXDiscontinuity`] tag) does not immediately follow the Media
|
||||||
|
/// Initialization Section at the beginning of its resource.
|
||||||
|
///
|
||||||
|
/// If the Media Initialization Section declared by an [`ExtXMap`] tag is
|
||||||
|
/// encrypted with [`EncryptionMethod::Aes128`], the IV attribute of
|
||||||
|
/// the [`ExtXKey`] tag that applies to the [`ExtXMap`] is required.
|
||||||
|
///
|
||||||
|
/// [Media Initialization Section]: https://tools.ietf.org/html/rfc8216#section-3
|
||||||
/// [`MediaSegment`]: crate::MediaSegment
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
/// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity
|
||||||
|
/// [`EncryptionMethod::Aes128`]: crate::types::EncryptionMethod::Aes128
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
#[derive(ShortHand, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
#[shorthand(enable(must_use, into))]
|
||||||
pub struct ExtXMap {
|
pub struct ExtXMap {
|
||||||
|
/// The `URI` that identifies a resource, that contains the media
|
||||||
|
/// initialization section.
|
||||||
uri: String,
|
uri: String,
|
||||||
|
/// The range of the media initialization section.
|
||||||
|
#[shorthand(enable(copy))]
|
||||||
range: Option<ByteRange>,
|
range: Option<ByteRange>,
|
||||||
keys: Vec<ExtXKey>,
|
#[shorthand(enable(skip))]
|
||||||
|
pub(crate) keys: Vec<ExtXKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXMap {
|
impl ExtXMap {
|
||||||
|
@ -27,13 +50,14 @@ impl ExtXMap {
|
||||||
/// Makes a new [`ExtXMap`] tag.
|
/// Makes a new [`ExtXMap`] tag.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
/// # use hls_m3u8::tags::ExtXMap;
|
||||||
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: ToString>(uri: T) -> Self {
|
pub fn new<T: Into<String>>(uri: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uri: uri.to_string(),
|
uri: uri.into(),
|
||||||
range: None,
|
range: None,
|
||||||
keys: vec![],
|
keys: vec![],
|
||||||
}
|
}
|
||||||
|
@ -42,101 +66,37 @@ impl ExtXMap {
|
||||||
/// Makes a new [`ExtXMap`] tag with the given range.
|
/// Makes a new [`ExtXMap`] tag with the given range.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
/// # use hls_m3u8::tags::ExtXMap;
|
||||||
/// use hls_m3u8::types::ByteRange;
|
/// use hls_m3u8::types::ByteRange;
|
||||||
///
|
///
|
||||||
/// let map = ExtXMap::with_range(
|
/// ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11);
|
||||||
/// "https://prod.mediaspace.com/init.bin",
|
|
||||||
/// ByteRange::new(9, Some(2)),
|
|
||||||
/// );
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn with_range<T: ToString>(uri: T, range: ByteRange) -> Self {
|
pub fn with_range<I: Into<String>, B: Into<ByteRange>>(uri: I, range: B) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uri: uri.to_string(),
|
uri: uri.into(),
|
||||||
range: Some(range),
|
range: Some(range.into()),
|
||||||
keys: vec![],
|
keys: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the `URI` that identifies a resource, that contains the media
|
impl Decryptable for ExtXMap {
|
||||||
/// initialization section.
|
fn keys(&self) -> Vec<&DecryptionKey> {
|
||||||
///
|
//
|
||||||
/// # Example
|
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
|
||||||
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// map.uri(),
|
|
||||||
/// &"https://prod.mediaspace.com/init.bin".to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub const fn uri(&self) -> &String { &self.uri }
|
|
||||||
|
|
||||||
/// Sets the `URI` that identifies a resource, that contains the media
|
|
||||||
/// initialization section.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
|
||||||
/// let mut map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
|
||||||
///
|
|
||||||
/// map.set_uri("https://dev.mediaspace.com/init.bin");
|
|
||||||
/// assert_eq!(
|
|
||||||
/// map.uri(),
|
|
||||||
/// &"https://dev.mediaspace.com/init.bin".to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
|
|
||||||
self.uri = value.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the range of the media initialization section.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
|
||||||
/// use hls_m3u8::types::ByteRange;
|
|
||||||
///
|
|
||||||
/// let map = ExtXMap::with_range(
|
|
||||||
/// "https://prod.mediaspace.com/init.bin",
|
|
||||||
/// ByteRange::new(9, Some(2)),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(map.range(), Some(ByteRange::new(9, Some(2))));
|
|
||||||
/// ```
|
|
||||||
pub const fn range(&self) -> Option<ByteRange> { self.range }
|
|
||||||
|
|
||||||
/// Sets the range of the media initialization section.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
|
||||||
/// use hls_m3u8::types::ByteRange;
|
|
||||||
///
|
|
||||||
/// let mut map = ExtXMap::with_range(
|
|
||||||
/// "https://prod.mediaspace.com/init.bin",
|
|
||||||
/// ByteRange::new(9, Some(2)),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// map.set_range(Some(ByteRange::new(1, None)));
|
|
||||||
/// assert_eq!(map.range(), Some(ByteRange::new(1, None)));
|
|
||||||
/// ```
|
|
||||||
pub fn set_range(&mut self, value: Option<ByteRange>) -> &mut Self {
|
|
||||||
self.range = value;
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encrypted for ExtXMap {
|
/// Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that contains the
|
||||||
fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
/// [`ExtXIFramesOnly`] tag requires [`ProtocolVersion::V5`] or
|
||||||
|
/// greater. Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that does not
|
||||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
/// contain the [`ExtXIFramesOnly`] tag requires [`ProtocolVersion::V6`] or
|
||||||
}
|
/// greater.
|
||||||
|
///
|
||||||
/// This tag requires [`ProtocolVersion::V6`].
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
impl RequiredVersion for ExtXMap {
|
impl RequiredVersion for ExtXMap {
|
||||||
// this should return ProtocolVersion::V5, if it does not contain an
|
// this should return ProtocolVersion::V5, if it does not contain an
|
||||||
// EXT-X-I-FRAMES-ONLY!
|
// EXT-X-I-FRAMES-ONLY!
|
||||||
|
@ -147,7 +107,7 @@ impl RequiredVersion for ExtXMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXMap {
|
impl fmt::Display for ExtXMap {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "URI={}", quote(&self.uri))?;
|
write!(f, "URI={}", quote(&self.uri))?;
|
||||||
|
|
||||||
|
@ -168,8 +128,8 @@ impl FromStr for ExtXMap {
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut range = None;
|
let mut range = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"URI" => uri = Some(unquote(value)),
|
"URI" => uri = Some(unquote(value)),
|
||||||
"BYTERANGE" => {
|
"BYTERANGE" => {
|
||||||
range = Some(unquote(value).parse()?);
|
range = Some(unquote(value).parse()?);
|
||||||
|
@ -182,7 +142,8 @@ impl FromStr for ExtXMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = uri.ok_or_else(|| Error::missing_value("EXT-X-URI"))?;
|
let uri = uri.ok_or_else(|| Error::missing_value("URI"))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
uri,
|
uri,
|
||||||
range,
|
range,
|
||||||
|
@ -204,7 +165,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).to_string(),
|
ExtXMap::with_range("foo", ByteRange::from(2..11)).to_string(),
|
||||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".to_string(),
|
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -217,11 +178,11 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))),
|
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
||||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap()
|
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))),
|
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
||||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED"
|
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -232,14 +193,13 @@ mod test {
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(ExtXMap::new("foo").required_version(), ProtocolVersion::V6);
|
assert_eq!(ExtXMap::new("foo").required_version(), ProtocolVersion::V6);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).required_version(),
|
ExtXMap::with_range("foo", ByteRange::from(2..11)).required_version(),
|
||||||
ProtocolVersion::V6
|
ProtocolVersion::V6
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypted() {
|
fn test_decryptable() {
|
||||||
assert_eq!(ExtXMap::new("foo").keys(), &vec![]);
|
assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey>::new());
|
||||||
assert_eq!(ExtXMap::new("foo").keys_mut(), &mut vec![]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
mod byte_range;
|
pub(crate) mod byte_range;
|
||||||
mod date_range;
|
pub(crate) mod date_range;
|
||||||
mod discontinuity;
|
pub(crate) mod discontinuity;
|
||||||
mod inf;
|
pub(crate) mod inf;
|
||||||
mod key;
|
pub(crate) mod key;
|
||||||
mod map;
|
pub(crate) mod map;
|
||||||
mod program_date_time;
|
pub(crate) mod program_date_time;
|
||||||
|
|
||||||
pub use byte_range::*;
|
pub use byte_range::*;
|
||||||
pub use date_range::*;
|
pub use date_range::ExtXDateRange;
|
||||||
pub use discontinuity::*;
|
pub(crate) use discontinuity::*;
|
||||||
pub use inf::*;
|
pub use inf::*;
|
||||||
pub use key::*;
|
pub use key::ExtXKey;
|
||||||
pub use map::*;
|
pub use map::*;
|
||||||
pub use program_date_time::*;
|
pub use program_date_time::*;
|
||||||
|
|
|
@ -1,21 +1,41 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
/// Associates the first sample of a [`MediaSegment`] with an absolute date
|
||||||
/// The [`ExtXProgramDateTime`] tag associates the first sample of a
|
/// and/or time.
|
||||||
/// [`Media Segment`] with an absolute date and/or time.
|
|
||||||
///
|
///
|
||||||
/// [`Media Segment`]: crate::MediaSegment
|
/// ## Features
|
||||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6
|
///
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
/// By enabling the `chrono` feature the `date_time`-field will change from
|
||||||
pub struct ExtXProgramDateTime(DateTime<FixedOffset>);
|
/// `String` to `DateTime<FixedOffset>` and the traits
|
||||||
|
/// - `Deref<Target=DateTime<FixedOffset>>`,
|
||||||
|
/// - `DerefMut<Target=DateTime<FixedOffset>>`
|
||||||
|
/// - and `Copy`
|
||||||
|
///
|
||||||
|
/// will be derived.
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
#[cfg_attr(feature = "chrono", derive(Deref, DerefMut, Copy))]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct ExtXProgramDateTime {
|
||||||
|
/// The date-time of the first sample of the associated media segment.
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
#[cfg_attr(feature = "chrono", deref_mut, deref)]
|
||||||
|
pub date_time: DateTime<FixedOffset>,
|
||||||
|
/// The date-time of the first sample of the associated media segment.
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
pub date_time: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl ExtXProgramDateTime {
|
impl ExtXProgramDateTime {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||||
|
@ -23,6 +43,7 @@ impl ExtXProgramDateTime {
|
||||||
/// Makes a new [`ExtXProgramDateTime`] tag.
|
/// Makes a new [`ExtXProgramDateTime`] tag.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
||||||
/// use chrono::{FixedOffset, TimeZone};
|
/// use chrono::{FixedOffset, TimeZone};
|
||||||
|
@ -35,64 +56,23 @@ impl ExtXProgramDateTime {
|
||||||
/// .and_hms_milli(14, 54, 23, 31),
|
/// .and_hms_milli(14, 54, 23, 31),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn new(date_time: DateTime<FixedOffset>) -> Self { Self(date_time) }
|
#[must_use]
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
pub const fn new(date_time: DateTime<FixedOffset>) -> Self { Self { date_time } }
|
||||||
|
|
||||||
/// Returns the date-time of the first sample of the associated media
|
/// Makes a new [`ExtXProgramDateTime`] tag.
|
||||||
/// segment.
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
||||||
/// use chrono::{FixedOffset, TimeZone};
|
/// let program_date_time = ExtXProgramDateTime::new("2010-02-19T14:54:23.031+08:00");
|
||||||
///
|
|
||||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
|
||||||
///
|
|
||||||
/// let program_date_time = ExtXProgramDateTime::new(
|
|
||||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
|
||||||
/// .ymd(2010, 2, 19)
|
|
||||||
/// .and_hms_milli(14, 54, 23, 31),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// program_date_time.date_time(),
|
|
||||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
|
||||||
/// .ymd(2010, 2, 19)
|
|
||||||
/// .and_hms_milli(14, 54, 23, 31)
|
|
||||||
/// );
|
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn date_time(&self) -> DateTime<FixedOffset> { self.0 }
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
pub fn new<T: Into<String>>(date_time: T) -> Self {
|
||||||
/// Sets the date-time of the first sample of the associated media segment.
|
Self {
|
||||||
///
|
date_time: date_time.into(),
|
||||||
/// # Example
|
}
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
|
||||||
/// use chrono::{FixedOffset, TimeZone};
|
|
||||||
///
|
|
||||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
|
||||||
///
|
|
||||||
/// let mut program_date_time = ExtXProgramDateTime::new(
|
|
||||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
|
||||||
/// .ymd(2010, 2, 19)
|
|
||||||
/// .and_hms_milli(14, 54, 23, 31),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// program_date_time.set_date_time(
|
|
||||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
|
||||||
/// .ymd(2010, 10, 10)
|
|
||||||
/// .and_hms_milli(10, 10, 10, 10),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// program_date_time.date_time(),
|
|
||||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
|
||||||
/// .ymd(2010, 10, 10)
|
|
||||||
/// .and_hms_milli(10, 10, 10, 10)
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn set_date_time(&mut self, value: DateTime<FixedOffset>) -> &mut Self {
|
|
||||||
self.0 = value;
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,8 +82,17 @@ impl RequiredVersion for ExtXProgramDateTime {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXProgramDateTime {
|
impl fmt::Display for ExtXProgramDateTime {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let date_time = self.0.to_rfc3339_opts(SecondsFormat::Millis, true);
|
let date_time = {
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
{
|
||||||
|
self.date_time.to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
{
|
||||||
|
&self.date_time
|
||||||
|
}
|
||||||
|
};
|
||||||
write!(f, "{}{}", Self::PREFIX, date_time)
|
write!(f, "{}{}", Self::PREFIX, date_time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,37 +103,46 @@ impl FromStr for ExtXProgramDateTime {
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let date_time = DateTime::parse_from_rfc3339(input)?;
|
Ok(Self::new({
|
||||||
Ok(Self::new(date_time))
|
#[cfg(feature = "chrono")]
|
||||||
|
{
|
||||||
|
DateTime::parse_from_rfc3339(input).map_err(Error::chrono)?
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
{
|
||||||
|
input
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for ExtXProgramDateTime {
|
|
||||||
type Target = DateTime<FixedOffset>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for ExtXProgramDateTime {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
use chrono::{Datelike, TimeZone};
|
use chrono::{Datelike, TimeZone};
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
use core::ops::DerefMut;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXProgramDateTime::new(
|
ExtXProgramDateTime::new({
|
||||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
#[cfg(feature = "chrono")]
|
||||||
.ymd(2010, 2, 19)
|
{
|
||||||
.and_hms_milli(14, 54, 23, 31)
|
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||||
)
|
.ymd(2010, 2, 19)
|
||||||
|
.and_hms_milli(14, 54, 23, 31)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
{
|
||||||
|
"2010-02-19T14:54:23.031+08:00"
|
||||||
|
}
|
||||||
|
})
|
||||||
.to_string(),
|
.to_string(),
|
||||||
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string()
|
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string()
|
||||||
);
|
);
|
||||||
|
@ -153,11 +151,18 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXProgramDateTime::new(
|
ExtXProgramDateTime::new({
|
||||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
#[cfg(feature = "chrono")]
|
||||||
.ymd(2010, 2, 19)
|
{
|
||||||
.and_hms_milli(14, 54, 23, 31)
|
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||||
),
|
.ymd(2010, 2, 19)
|
||||||
|
.and_hms_milli(14, 54, 23, 31)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
{
|
||||||
|
"2010-02-19T14:54:23.031+08:00"
|
||||||
|
}
|
||||||
|
}),
|
||||||
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"
|
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"
|
||||||
.parse::<ExtXProgramDateTime>()
|
.parse::<ExtXProgramDateTime>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -167,17 +172,25 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXProgramDateTime::new(
|
ExtXProgramDateTime::new({
|
||||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
#[cfg(feature = "chrono")]
|
||||||
.ymd(2010, 2, 19)
|
{
|
||||||
.and_hms_milli(14, 54, 23, 31),
|
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||||
)
|
.ymd(2010, 2, 19)
|
||||||
|
.and_hms_milli(14, 54, 23, 31)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
{
|
||||||
|
"2010-02-19T14:54:23.031+08:00"
|
||||||
|
}
|
||||||
|
})
|
||||||
.required_version(),
|
.required_version(),
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
fn test_deref() {
|
fn test_deref() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXProgramDateTime::new(
|
ExtXProgramDateTime::new(
|
||||||
|
@ -191,6 +204,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
fn test_deref_mut() {
|
fn test_deref_mut() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXProgramDateTime::new(
|
ExtXProgramDateTime::new(
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
//!
|
//!
|
||||||
//! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3
|
//! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3
|
||||||
|
|
||||||
mod basic;
|
pub(crate) mod basic;
|
||||||
mod master_playlist;
|
pub(crate) mod master_playlist;
|
||||||
mod media_playlist;
|
pub(crate) mod media_playlist;
|
||||||
mod media_segment;
|
pub(crate) mod media_segment;
|
||||||
mod shared;
|
pub(crate) mod shared;
|
||||||
|
|
||||||
pub use basic::*;
|
pub use basic::*;
|
||||||
pub use master_playlist::*;
|
pub use master_playlist::*;
|
||||||
pub use media_playlist::*;
|
pub(crate) use media_playlist::*;
|
||||||
pub use media_segment::*;
|
pub use media_segment::*;
|
||||||
pub use shared::*;
|
pub use shared::*;
|
||||||
|
|
|
@ -5,22 +5,24 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
|
/// Signals that all media samples in a [`MediaSegment`] can be decoded without
|
||||||
|
/// information from other segments.
|
||||||
///
|
///
|
||||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||||
pub struct ExtXIndependentSegments;
|
pub(crate) struct ExtXIndependentSegments;
|
||||||
|
|
||||||
impl ExtXIndependentSegments {
|
impl ExtXIndependentSegments {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXIndependentSegments {
|
impl RequiredVersion for ExtXIndependentSegments {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXIndependentSegments {
|
impl fmt::Display for ExtXIndependentSegments {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXIndependentSegments {
|
impl FromStr for ExtXIndependentSegments {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mod independent_segments;
|
pub(crate) mod independent_segments;
|
||||||
mod start;
|
pub(crate) mod start;
|
||||||
|
|
||||||
pub use independent_segments::*;
|
pub(crate) use independent_segments::ExtXIndependentSegments;
|
||||||
pub use start::*;
|
pub use start::*;
|
||||||
|
|
|
@ -1,18 +1,55 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
use crate::types::{Float, ProtocolVersion};
|
||||||
use crate::utils::{parse_yes_or_no, tag};
|
use crate::utils::{parse_yes_or_no, tag};
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// [4.3.5.2. EXT-X-START]
|
/// This tag indicates a preferred point at which to start
|
||||||
|
/// playing a Playlist.
|
||||||
///
|
///
|
||||||
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
/// By default, clients should start playback at this point when beginning a
|
||||||
#[derive(PartialOrd, Debug, Clone, Copy, PartialEq)]
|
/// playback session.
|
||||||
|
#[derive(ShortHand, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Ord, Hash)]
|
||||||
|
#[shorthand(enable(must_use))]
|
||||||
pub struct ExtXStart {
|
pub struct ExtXStart {
|
||||||
time_offset: SignedDecimalFloatingPoint,
|
/// The time offset of the [`MediaSegment`]s in the playlist.
|
||||||
precise: bool,
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXStart;
|
||||||
|
/// use hls_m3u8::types::Float;
|
||||||
|
///
|
||||||
|
/// let mut start = ExtXStart::new(Float::new(20.123456));
|
||||||
|
/// # assert_eq!(start.time_offset(), Float::new(20.123456));
|
||||||
|
///
|
||||||
|
/// start.set_time_offset(Float::new(1.0));
|
||||||
|
/// assert_eq!(start.time_offset(), Float::new(1.0));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
#[shorthand(enable(copy))]
|
||||||
|
time_offset: Float,
|
||||||
|
/// Whether clients should not render media stream whose presentation times
|
||||||
|
/// are prior to the specified time offset.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::tags::ExtXStart;
|
||||||
|
/// use hls_m3u8::types::Float;
|
||||||
|
///
|
||||||
|
/// let mut start = ExtXStart::new(Float::new(20.123456));
|
||||||
|
/// # assert_eq!(start.is_precise(), false);
|
||||||
|
/// start.set_is_precise(true);
|
||||||
|
///
|
||||||
|
/// assert_eq!(start.is_precise(), true);
|
||||||
|
/// ```
|
||||||
|
is_precise: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXStart {
|
impl ExtXStart {
|
||||||
|
@ -20,106 +57,56 @@ impl ExtXStart {
|
||||||
|
|
||||||
/// Makes a new [`ExtXStart`] tag.
|
/// Makes a new [`ExtXStart`] tag.
|
||||||
///
|
///
|
||||||
/// # Panic
|
|
||||||
/// Panics if the time_offset value is infinite.
|
|
||||||
///
|
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXStart;
|
/// # use hls_m3u8::tags::ExtXStart;
|
||||||
/// let start = ExtXStart::new(20.123456);
|
/// use hls_m3u8::types::Float;
|
||||||
|
///
|
||||||
|
/// let start = ExtXStart::new(Float::new(20.123456));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new(time_offset: f64) -> Self {
|
#[must_use]
|
||||||
|
pub const fn new(time_offset: Float) -> Self {
|
||||||
Self {
|
Self {
|
||||||
time_offset: SignedDecimalFloatingPoint::new(time_offset),
|
time_offset,
|
||||||
precise: false,
|
is_precise: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new [`ExtXStart`] tag with the given `precise` flag.
|
/// Makes a new [`ExtXStart`] tag with the given `precise` flag.
|
||||||
///
|
///
|
||||||
/// # Panic
|
|
||||||
/// Panics if the time_offset value is infinite.
|
|
||||||
///
|
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXStart;
|
/// # use hls_m3u8::tags::ExtXStart;
|
||||||
/// let start = ExtXStart::with_precise(20.123456, true);
|
/// use hls_m3u8::types::Float;
|
||||||
/// assert_eq!(start.precise(), true);
|
///
|
||||||
|
/// let start = ExtXStart::with_precise(Float::new(20.123456), true);
|
||||||
|
/// assert_eq!(start.is_precise(), true);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn with_precise(time_offset: f64, precise: bool) -> Self {
|
#[must_use]
|
||||||
|
pub const fn with_precise(time_offset: Float, is_precise: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
time_offset: SignedDecimalFloatingPoint::new(time_offset),
|
time_offset,
|
||||||
precise,
|
is_precise,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the time offset of the media segments in the playlist.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStart;
|
|
||||||
/// let start = ExtXStart::new(20.123456);
|
|
||||||
/// assert_eq!(start.time_offset(), 20.123456);
|
|
||||||
/// ```
|
|
||||||
pub const fn time_offset(&self) -> f64 { self.time_offset.as_f64() }
|
|
||||||
|
|
||||||
/// Sets the time offset of the media segments in the playlist.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStart;
|
|
||||||
/// let mut start = ExtXStart::new(20.123456);
|
|
||||||
/// # assert_eq!(start.time_offset(), 20.123456);
|
|
||||||
///
|
|
||||||
/// start.set_time_offset(1.0);
|
|
||||||
///
|
|
||||||
/// assert_eq!(start.time_offset(), 1.0);
|
|
||||||
/// ```
|
|
||||||
pub fn set_time_offset(&mut self, value: f64) -> &mut Self {
|
|
||||||
self.time_offset = SignedDecimalFloatingPoint::new(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether clients should not render media stream whose
|
|
||||||
/// presentation times are prior to the specified time offset.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStart;
|
|
||||||
/// let start = ExtXStart::with_precise(20.123456, true);
|
|
||||||
/// assert_eq!(start.precise(), true);
|
|
||||||
/// ```
|
|
||||||
pub const fn precise(&self) -> bool { self.precise }
|
|
||||||
|
|
||||||
/// Sets the `precise` flag.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXStart;
|
|
||||||
/// let mut start = ExtXStart::new(20.123456);
|
|
||||||
/// # assert_eq!(start.precise(), false);
|
|
||||||
///
|
|
||||||
/// start.set_precise(true);
|
|
||||||
///
|
|
||||||
/// assert_eq!(start.precise(), true);
|
|
||||||
/// ```
|
|
||||||
pub fn set_precise(&mut self, value: bool) -> &mut Self {
|
|
||||||
self.precise = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXStart {
|
impl RequiredVersion for ExtXStart {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXStart {
|
impl fmt::Display for ExtXStart {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "TIME-OFFSET={}", self.time_offset)?;
|
write!(f, "TIME-OFFSET={}", self.time_offset)?;
|
||||||
if self.precise {
|
|
||||||
|
if self.is_precise {
|
||||||
write!(f, ",PRECISE=YES")?;
|
write!(f, ",PRECISE=YES")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,12 +118,12 @@ impl FromStr for ExtXStart {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut time_offset = None;
|
let mut time_offset = None;
|
||||||
let mut precise = false;
|
let mut is_precise = false;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"TIME-OFFSET" => time_offset = Some((value.parse())?),
|
"TIME-OFFSET" => time_offset = Some(value.parse()?),
|
||||||
"PRECISE" => precise = (parse_yes_or_no(value))?,
|
"PRECISE" => is_precise = parse_yes_or_no(value)?,
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized
|
// > ignore any attribute/value pair with an unrecognized
|
||||||
|
@ -145,11 +132,11 @@ impl FromStr for ExtXStart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let time_offset = time_offset.ok_or_else(|| Error::missing_value("EXT-X-TIME-OFFSET"))?;
|
let time_offset = time_offset.ok_or_else(|| Error::missing_value("TIME-OFFSET"))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
time_offset,
|
time_offset,
|
||||||
precise,
|
is_precise,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,12 +149,12 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::new(-1.23).to_string(),
|
ExtXStart::new(Float::new(-1.23)).to_string(),
|
||||||
"#EXT-X-START:TIME-OFFSET=-1.23".to_string(),
|
"#EXT-X-START:TIME-OFFSET=-1.23".to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::with_precise(1.23, true).to_string(),
|
ExtXStart::with_precise(Float::new(1.23), true).to_string(),
|
||||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".to_string(),
|
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -175,12 +162,12 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::new(-1.23).required_version(),
|
ExtXStart::new(Float::new(-1.23)).required_version(),
|
||||||
ProtocolVersion::V1,
|
ProtocolVersion::V1,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::with_precise(1.23, true).required_version(),
|
ExtXStart::with_precise(Float::new(1.23), true).required_version(),
|
||||||
ProtocolVersion::V1,
|
ProtocolVersion::V1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -188,17 +175,17 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::new(-1.23),
|
ExtXStart::new(Float::new(-1.23)),
|
||||||
"#EXT-X-START:TIME-OFFSET=-1.23".parse().unwrap(),
|
"#EXT-X-START:TIME-OFFSET=-1.23".parse().unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::with_precise(1.23, true),
|
ExtXStart::with_precise(Float::new(1.23), true),
|
||||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(),
|
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::with_precise(1.23, true),
|
ExtXStart::with_precise(Float::new(1.23), true),
|
||||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG"
|
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
209
src/traits.rs
209
src/traits.rs
|
@ -1,150 +1,127 @@
|
||||||
use crate::tags::ExtXKey;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use crate::types::{EncryptionMethod, ProtocolVersion};
|
|
||||||
|
|
||||||
/// A trait, that is implemented on all tags, that could be encrypted.
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use hls_m3u8::tags::ExtXKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
/// use hls_m3u8::Encrypted;
|
|
||||||
///
|
|
||||||
/// struct ExampleTag {
|
|
||||||
/// keys: Vec<ExtXKey>,
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// // Implementing the trait is very simple:
|
|
||||||
/// // Simply expose the internal buffer, that contains all the keys.
|
|
||||||
/// impl Encrypted for ExampleTag {
|
|
||||||
/// fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
|
||||||
///
|
|
||||||
/// fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let mut example_tag = ExampleTag { keys: vec![] };
|
|
||||||
///
|
|
||||||
/// // adding new keys:
|
|
||||||
/// example_tag.set_keys(vec![ExtXKey::empty()]);
|
|
||||||
/// example_tag.push_key(ExtXKey::new(
|
|
||||||
/// EncryptionMethod::Aes128,
|
|
||||||
/// "http://www.example.com/data.bin",
|
|
||||||
/// ));
|
|
||||||
///
|
|
||||||
/// // getting the keys:
|
|
||||||
/// assert_eq!(
|
|
||||||
/// example_tag.keys(),
|
|
||||||
/// &vec![
|
|
||||||
/// ExtXKey::empty(),
|
|
||||||
/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",)
|
|
||||||
/// ]
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// example_tag.keys_mut(),
|
|
||||||
/// &mut vec![
|
|
||||||
/// ExtXKey::empty(),
|
|
||||||
/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",)
|
|
||||||
/// ]
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// assert!(example_tag.is_encrypted());
|
|
||||||
/// assert!(!example_tag.is_not_encrypted());
|
|
||||||
/// ```
|
|
||||||
pub trait Encrypted {
|
|
||||||
/// Returns a shared reference to all keys, that can be used to decrypt this
|
|
||||||
/// tag.
|
|
||||||
fn keys(&self) -> &Vec<ExtXKey>;
|
|
||||||
|
|
||||||
/// Returns an exclusive reference to all keys, that can be used to decrypt
|
mod private {
|
||||||
/// this tag.
|
pub trait Sealed {}
|
||||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey>;
|
impl Sealed for crate::MediaSegment {}
|
||||||
|
impl Sealed for crate::tags::ExtXMap {}
|
||||||
/// Sets all keys, that can be used to decrypt this tag.
|
|
||||||
fn set_keys(&mut self, value: Vec<ExtXKey>) -> &mut Self {
|
|
||||||
let keys = self.keys_mut();
|
|
||||||
*keys = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a single key to the list of keys, that can be used to decrypt this
|
|
||||||
/// tag.
|
|
||||||
fn push_key(&mut self, value: ExtXKey) -> &mut Self {
|
|
||||||
self.keys_mut().push(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true`, if the tag is encrypted.
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
/// This will return `true`, if any of the keys satisfies
|
|
||||||
/// ```text
|
|
||||||
/// key.method() != EncryptionMethod::None
|
|
||||||
/// ```
|
|
||||||
fn is_encrypted(&self) -> bool {
|
|
||||||
if self.keys().is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.keys()
|
|
||||||
.iter()
|
|
||||||
.any(|k| k.method() != EncryptionMethod::None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `false`, if the tag is not encrypted.
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
/// This is the inverse of [`is_encrypted`].
|
|
||||||
///
|
|
||||||
/// [`is_encrypted`]: #method.is_encrypted
|
|
||||||
fn is_not_encrypted(&self) -> bool { !self.is_encrypted() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Example
|
/// Signals that a type or some of the asssociated data might need to be
|
||||||
/// Implementing it:
|
/// decrypted.
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::RequiredVersion;
|
|
||||||
/// use hls_m3u8::types::ProtocolVersion;
|
|
||||||
///
|
///
|
||||||
/// struct ExampleTag(u64);
|
/// # Note
|
||||||
///
|
///
|
||||||
/// impl RequiredVersion for ExampleTag {
|
/// You are not supposed to implement this trait, therefore it is "sealed".
|
||||||
/// fn required_version(&self) -> ProtocolVersion {
|
pub trait Decryptable: private::Sealed {
|
||||||
/// if self.0 == 5 {
|
/// Returns all keys, associated with the type.
|
||||||
/// ProtocolVersion::V4
|
///
|
||||||
/// } else {
|
/// # Example
|
||||||
/// ProtocolVersion::V1
|
///
|
||||||
/// }
|
/// ```
|
||||||
/// }
|
/// use hls_m3u8::tags::ExtXMap;
|
||||||
/// }
|
/// use hls_m3u8::types::{ByteRange, EncryptionMethod};
|
||||||
/// assert_eq!(ExampleTag(5).required_version(), ProtocolVersion::V4);
|
/// use hls_m3u8::Decryptable;
|
||||||
/// assert_eq!(ExampleTag(2).required_version(), ProtocolVersion::V1);
|
///
|
||||||
/// ```
|
/// let map = ExtXMap::with_range("https://www.example.url/", ByteRange::from(2..11));
|
||||||
|
///
|
||||||
|
/// for key in map.keys() {
|
||||||
|
/// if key.method == EncryptionMethod::Aes128 {
|
||||||
|
/// // fetch content with the uri and decrypt the result
|
||||||
|
/// break;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
fn keys(&self) -> Vec<&DecryptionKey>;
|
||||||
|
|
||||||
|
/// Most of the time only a single key is provided, so instead of iterating
|
||||||
|
/// through all keys, one might as well just get the first key.
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
fn first_key(&self) -> Option<&DecryptionKey> {
|
||||||
|
<Self as Decryptable>::keys(self).first().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of keys.
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize { <Self as Decryptable>::keys(self).len() }
|
||||||
|
|
||||||
|
/// Returns `true`, if the number of keys is zero.
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
fn is_empty(&self) -> bool { <Self as Decryptable>::len(self) == 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub trait RequiredVersion {
|
pub trait RequiredVersion {
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
|
///
|
||||||
/// This is for the latest working [`ProtocolVersion`] and a client, that
|
/// This is for the latest working [`ProtocolVersion`] and a client, that
|
||||||
/// only supports an older version would break.
|
/// only supports an older version would break.
|
||||||
|
#[must_use]
|
||||||
fn required_version(&self) -> ProtocolVersion;
|
fn required_version(&self) -> ProtocolVersion;
|
||||||
|
|
||||||
/// The protocol version, in which the tag has been introduced.
|
/// The protocol version, in which the tag has been introduced.
|
||||||
|
#[must_use]
|
||||||
fn introduced_version(&self) -> ProtocolVersion { self.required_version() }
|
fn introduced_version(&self) -> ProtocolVersion { self.required_version() }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: RequiredVersion> RequiredVersion for Vec<T> {
|
impl<T: RequiredVersion> RequiredVersion for Vec<T> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
self.iter()
|
self.iter()
|
||||||
.map(|v| v.required_version())
|
.map(RequiredVersion::required_version)
|
||||||
.max()
|
.max()
|
||||||
// return ProtocolVersion::V1, if the iterator is empty:
|
// return ProtocolVersion::V1, if the iterator is empty:
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<K, V: RequiredVersion> RequiredVersion for BTreeMap<K, V> {
|
||||||
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
|
self.values()
|
||||||
|
.map(RequiredVersion::required_version)
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: RequiredVersion> RequiredVersion for Option<T> {
|
impl<T: RequiredVersion> RequiredVersion for Option<T> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
self.iter()
|
self.iter()
|
||||||
.map(|v| v.required_version())
|
.map(RequiredVersion::required_version)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<K, V: RequiredVersion, S> RequiredVersion for HashMap<K, V, S> {
|
||||||
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
|
self.values()
|
||||||
|
.map(RequiredVersion::required_version)
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_required_version_trait() {
|
||||||
|
struct Example;
|
||||||
|
|
||||||
|
impl RequiredVersion for Example {
|
||||||
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V3 }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(Example.required_version(), ProtocolVersion::V3);
|
||||||
|
assert_eq!(Example.introduced_version(), ProtocolVersion::V3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,90 +1,409 @@
|
||||||
use std::fmt;
|
use core::convert::TryInto;
|
||||||
use std::str::FromStr;
|
use core::fmt;
|
||||||
|
use core::ops::{
|
||||||
|
Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub,
|
||||||
|
SubAssign,
|
||||||
|
};
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// Byte range.
|
/// A range of bytes, which can be seen as either `..end` or `start..end`.
|
||||||
///
|
///
|
||||||
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
/// It can be constructed from `..end` and `start..end`:
|
||||||
///
|
///
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
/// ```
|
||||||
#[derive(Copy, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
/// use hls_m3u8::types::ByteRange;
|
||||||
|
///
|
||||||
|
/// let range = ByteRange::from(10..20);
|
||||||
|
/// let range = ByteRange::from(..20);
|
||||||
|
/// ```
|
||||||
|
#[derive(ShortHand, Copy, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
||||||
|
#[shorthand(enable(must_use, copy), disable(option_as_ref, set))]
|
||||||
pub struct ByteRange {
|
pub struct ByteRange {
|
||||||
length: usize,
|
/// Returns the `start` of the [`ByteRange`], if there is one.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
|
/// assert_eq!(ByteRange::from(0..5).start(), Some(0));
|
||||||
|
/// assert_eq!(ByteRange::from(..5).start(), None);
|
||||||
|
/// ```
|
||||||
start: Option<usize>,
|
start: Option<usize>,
|
||||||
|
/// Returns the `end` of the [`ByteRange`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
|
/// assert_eq!(ByteRange::from(0..5).end(), 5);
|
||||||
|
/// assert_eq!(ByteRange::from(..=5).end(), 6);
|
||||||
|
/// ```
|
||||||
|
end: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ByteRange {
|
impl ByteRange {
|
||||||
/// Creates a new [`ByteRange`].
|
/// Changes the length of the [`ByteRange`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::ByteRange;
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
/// ByteRange::new(22, Some(12));
|
/// let mut range = ByteRange::from(0..5);
|
||||||
|
/// range.set_len(2);
|
||||||
|
///
|
||||||
|
/// assert_eq!(range, ByteRange::from(0..2));
|
||||||
|
///
|
||||||
|
/// range.set_len(200);
|
||||||
|
/// assert_eq!(range, ByteRange::from(0..200));
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn new(length: usize, start: Option<usize>) -> Self { Self { length, start } }
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// The `start` will not be changed.
|
||||||
|
pub fn set_len(&mut self, new_len: usize) {
|
||||||
|
// the new_len can be either greater or smaller than `self.len()`.
|
||||||
|
// if new_len is larger `checked_sub` will return `None`
|
||||||
|
if let Some(value) = self.len().checked_sub(new_len) {
|
||||||
|
self.end -= value;
|
||||||
|
} else {
|
||||||
|
self.end += new_len.saturating_sub(self.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the length of the range.
|
/// Sets the `start` of the [`ByteRange`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::ByteRange;
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
/// #
|
/// assert_eq!(ByteRange::from(0..5).set_start(Some(5)).start(), Some(5));
|
||||||
/// assert_eq!(ByteRange::new(20, Some(3)).length(), 20);
|
/// assert_eq!(ByteRange::from(..5).set_start(Some(2)).start(), Some(2));
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn length(&self) -> usize { self.length }
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function will panic, if the `new_start` is larger, than the
|
||||||
|
/// [`end`](ByteRange::end).
|
||||||
|
pub fn set_start(&mut self, new_start: Option<usize>) -> &mut Self {
|
||||||
|
if new_start.map_or(false, |s| s > self.end) {
|
||||||
|
panic!(
|
||||||
|
"attempt to make the start ({}) larger than the end ({})",
|
||||||
|
new_start.unwrap(),
|
||||||
|
self.end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.start = new_start;
|
||||||
|
|
||||||
/// Sets the length of the range.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::ByteRange;
|
|
||||||
/// #
|
|
||||||
/// let mut range = ByteRange::new(20, Some(3));
|
|
||||||
///
|
|
||||||
/// # assert_eq!(range.length(), 20);
|
|
||||||
/// range.set_length(10);
|
|
||||||
/// assert_eq!(range.length(), 10);
|
|
||||||
/// ```
|
|
||||||
pub fn set_length(&mut self, value: usize) -> &mut Self {
|
|
||||||
self.length = value;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the start of the range.
|
/// Adds `num` to the `start` and `end` of the range.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::ByteRange;
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
/// #
|
/// let range = ByteRange::from(10..22);
|
||||||
/// assert_eq!(ByteRange::new(20, Some(3)).start(), Some(3));
|
/// let nrange = range.saturating_add(5);
|
||||||
|
///
|
||||||
|
/// assert_eq!(nrange.len(), range.len());
|
||||||
|
/// assert_eq!(nrange.start(), range.start().map(|c| c + 5));
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn start(&self) -> Option<usize> { self.start }
|
///
|
||||||
|
/// # Overflow
|
||||||
|
///
|
||||||
|
/// If the range is saturated it will not overflow and instead stay
|
||||||
|
/// at it's current value.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
|
/// let range = ByteRange::from(5..usize::max_value());
|
||||||
|
///
|
||||||
|
/// // this would cause the end to overflow
|
||||||
|
/// let nrange = range.saturating_add(1);
|
||||||
|
///
|
||||||
|
/// // but the range remains unchanged
|
||||||
|
/// assert_eq!(range, nrange);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// The length of the range will remain unchanged,
|
||||||
|
/// if the `start` is `Some`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn saturating_add(mut self, num: usize) -> Self {
|
||||||
|
if let Some(start) = self.start {
|
||||||
|
// add the number to the start
|
||||||
|
if let (Some(start), Some(end)) = (start.checked_add(num), self.end.checked_add(num)) {
|
||||||
|
self.start = Some(start);
|
||||||
|
self.end = end;
|
||||||
|
} else {
|
||||||
|
// it is ensured at construction that the start will never be larger than the
|
||||||
|
// end. This clause can therefore be only reached if the end overflowed.
|
||||||
|
// -> It is only possible to add `usize::max_value() - end` to the start.
|
||||||
|
if let Some(start) = start.checked_add(usize::max_value() - self.end) {
|
||||||
|
self.start = Some(start);
|
||||||
|
self.end = usize::max_value();
|
||||||
|
} else {
|
||||||
|
// both end + start overflowed -> do not change anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.end = self.end.saturating_add(num);
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the start of the range.
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtracts `num` from the `start` and `end` of the range.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::ByteRange;
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
/// #
|
/// let range = ByteRange::from(10..22);
|
||||||
/// let mut range = ByteRange::new(20, None);
|
/// let nrange = range.saturating_sub(5);
|
||||||
///
|
///
|
||||||
/// # assert_eq!(range.start(), None);
|
/// assert_eq!(nrange.len(), range.len());
|
||||||
/// range.set_start(Some(3));
|
/// assert_eq!(nrange.start(), range.start().map(|c| c - 5));
|
||||||
/// assert_eq!(range.start(), Some(3));
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn set_start(&mut self, value: Option<usize>) -> &mut Self {
|
///
|
||||||
self.start = value;
|
/// # Underflow
|
||||||
|
///
|
||||||
|
/// If the range is saturated it will not underflow and instead stay
|
||||||
|
/// at it's current value.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
|
/// let range = ByteRange::from(0..10);
|
||||||
|
///
|
||||||
|
/// // this would cause the start to underflow
|
||||||
|
/// let nrange = range.saturating_sub(1);
|
||||||
|
///
|
||||||
|
/// // but the range remains unchanged
|
||||||
|
/// assert_eq!(range, nrange);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// The length of the range will remain unchanged,
|
||||||
|
/// if the `start` is `Some`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn saturating_sub(mut self, num: usize) -> Self {
|
||||||
|
if let Some(start) = self.start {
|
||||||
|
// subtract the number from the start
|
||||||
|
if let (Some(start), Some(end)) = (start.checked_sub(num), self.end.checked_sub(num)) {
|
||||||
|
self.start = Some(start);
|
||||||
|
self.end = end;
|
||||||
|
} else {
|
||||||
|
// it is ensured at construction that the start will never be larger, than the
|
||||||
|
// end so this clause will only be reached, if the start underflowed.
|
||||||
|
// -> can at most subtract `start` from `end`
|
||||||
|
if let Some(end) = self.end.checked_sub(start) {
|
||||||
|
self.start = Some(0);
|
||||||
|
self.end = end;
|
||||||
|
} else {
|
||||||
|
// both end + start underflowed
|
||||||
|
// -> do not change anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.end = self.end.saturating_sub(num);
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the length, which is calculated by subtracting the `end` from
|
||||||
|
/// the `start`. If the `start` is `None` a 0 is assumed.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
|
/// let range = ByteRange::from(1..16);
|
||||||
|
///
|
||||||
|
/// assert_eq!(range.len(), 15);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn len(&self) -> usize { self.end.saturating_sub(self.start.unwrap_or(0)) }
|
||||||
|
|
||||||
|
/// Returns `true` if the length is zero.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::ByteRange;
|
||||||
|
/// let range = ByteRange::from(12..12);
|
||||||
|
///
|
||||||
|
/// assert_eq!(range.is_empty(), true);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_empty(&self) -> bool { self.len() == 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub<usize> for ByteRange {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
fn sub(self, rhs: usize) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
start: self.start.map(|lhs| lhs - rhs),
|
||||||
|
end: self.end - rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubAssign<usize> for ByteRange {
|
||||||
|
#[inline]
|
||||||
|
fn sub_assign(&mut self, other: usize) { *self = <Self as Sub<usize>>::sub(*self, other); }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add<usize> for ByteRange {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
fn add(self, rhs: usize) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
start: self.start.map(|lhs| lhs + rhs),
|
||||||
|
end: self.end + rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAssign<usize> for ByteRange {
|
||||||
|
#[inline]
|
||||||
|
fn add_assign(&mut self, other: usize) { *self = <Self as Add<usize>>::add(*self, other); }
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_from_ranges {
|
||||||
|
( $( $type:tt ),* ) => {
|
||||||
|
$(
|
||||||
|
#[allow(trivial_numeric_casts, clippy::fallible_impl_from)]
|
||||||
|
impl From<Range<$type>> for ByteRange {
|
||||||
|
fn from(range: Range<$type>) -> Self {
|
||||||
|
if range.start > range.end {
|
||||||
|
panic!(
|
||||||
|
"the range start ({}) must be smaller than the end ({})",
|
||||||
|
range.start, range.end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
start: Some(range.start as usize),
|
||||||
|
end: range.end as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts, clippy::fallible_impl_from)]
|
||||||
|
impl From<RangeInclusive<$type>> for ByteRange {
|
||||||
|
fn from(range: RangeInclusive<$type>) -> Self {
|
||||||
|
let (start, end) = range.into_inner();
|
||||||
|
|
||||||
|
if start > end {
|
||||||
|
panic!(
|
||||||
|
"the range start ({}) must be smaller than the end ({}+1)",
|
||||||
|
start, end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
start: Some(start as usize),
|
||||||
|
end: (end as usize).saturating_add(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts, clippy::fallible_impl_from)]
|
||||||
|
impl From<RangeTo<$type>> for ByteRange {
|
||||||
|
fn from(range: RangeTo<$type>) -> Self {
|
||||||
|
Self {
|
||||||
|
start: None,
|
||||||
|
end: range.end as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts, clippy::fallible_impl_from)]
|
||||||
|
impl From<RangeToInclusive<$type>> for ByteRange {
|
||||||
|
fn from(range: RangeToInclusive<$type>) -> Self {
|
||||||
|
Self {
|
||||||
|
start: None,
|
||||||
|
end: (range.end as usize).saturating_add(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with generics as soon as overlapping trait implementations are
|
||||||
|
// stable (`Into<i64> for usize` is reserved for upstream crates ._.)
|
||||||
|
impl_from_ranges![u64, u32, u16, u8, usize, i32];
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
impl RangeBounds<usize> for ByteRange {
|
||||||
|
fn start_bound(&self) -> Bound<&usize> {
|
||||||
|
if let Some(start) = &self.start {
|
||||||
|
Bound::Included(start)
|
||||||
|
} else {
|
||||||
|
Bound::Unbounded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn end_bound(&self) -> Bound<&usize> { Bound::Excluded(&self.end) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This conversion will fail if the start of the [`ByteRange`] is `Some`.
|
||||||
|
impl TryInto<RangeTo<usize>> for ByteRange {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<RangeTo<usize>, Self::Error> {
|
||||||
|
if self.start.is_some() {
|
||||||
|
return Err(Error::custom("a `RangeTo` (`..end`) does not have a start"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RangeTo { end: self.end })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This conversion will fail if the start of the [`ByteRange`] is `None`.
|
||||||
|
impl TryInto<Range<usize>> for ByteRange {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<Range<usize>, Self::Error> {
|
||||||
|
if self.start.is_none() {
|
||||||
|
return Err(Error::custom(
|
||||||
|
"a `Range` (`start..end`) has to have a start.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Range {
|
||||||
|
start: self.start.unwrap(),
|
||||||
|
end: self.end,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ByteRange {
|
impl fmt::Display for ByteRange {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", self.length)?;
|
write!(f, "{}", self.len())?;
|
||||||
if let Some(x) = self.start {
|
|
||||||
write!(f, "@{}", x)?;
|
if let Some(value) = self.start {
|
||||||
|
write!(f, "@{}", value)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,22 +411,23 @@ impl fmt::Display for ByteRange {
|
||||||
impl FromStr for ByteRange {
|
impl FromStr for ByteRange {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let tokens = s.splitn(2, '@').collect::<Vec<_>>();
|
let mut input = input.splitn(2, '@');
|
||||||
if tokens.is_empty() {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = tokens[0].parse()?;
|
let length = input.next().unwrap();
|
||||||
|
let length = length
|
||||||
|
.parse::<usize>()
|
||||||
|
.map_err(|e| Error::parse_int(length, e))?;
|
||||||
|
|
||||||
let start = {
|
let start = input
|
||||||
if tokens.len() == 2 {
|
.next()
|
||||||
Some(tokens[1].parse()?)
|
.map(|v| v.parse::<usize>().map_err(|e| Error::parse_int(v, e)))
|
||||||
} else {
|
.transpose()?;
|
||||||
None
|
|
||||||
}
|
Ok(Self {
|
||||||
};
|
start,
|
||||||
Ok(Self::new(length, start))
|
end: start.unwrap_or(0) + length,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,51 +436,240 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "the range start (6) must be smaller than the end (0)"]
|
||||||
|
fn test_from_range_panic() { let _ = ByteRange::from(6..0); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "the range start (6) must be smaller than the end (0+1)"]
|
||||||
|
fn test_from_range_inclusive_panic() { let _ = ByteRange::from(6..=0); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_ranges() {
|
||||||
|
assert_eq!(ByteRange::from(1..10), ByteRange::from(1..=9));
|
||||||
|
assert_eq!(ByteRange::from(..10), ByteRange::from(..=9));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_range_bounds() {
|
||||||
|
assert_eq!(ByteRange::from(0..10).start_bound(), Bound::Included(&0));
|
||||||
|
assert_eq!(ByteRange::from(..10).start_bound(), Bound::Unbounded);
|
||||||
|
|
||||||
|
assert_eq!(ByteRange::from(0..10).end_bound(), Bound::Excluded(&10));
|
||||||
|
assert_eq!(ByteRange::from(..10).end_bound(), Bound::Excluded(&10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_try_into() {
|
||||||
|
assert_eq!(ByteRange::from(1..4).try_into(), Ok(1..4));
|
||||||
|
assert_eq!(ByteRange::from(..4).try_into(), Ok(..4));
|
||||||
|
|
||||||
|
assert!(TryInto::<RangeTo<usize>>::try_into(ByteRange::from(1..4)).is_err());
|
||||||
|
assert!(TryInto::<Range<usize>>::try_into(ByteRange::from(..4)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_assign() {
|
||||||
|
let mut range = ByteRange::from(5..10);
|
||||||
|
range += 5;
|
||||||
|
|
||||||
|
assert_eq!(range, ByteRange::from(10..15));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "attempt to add with overflow"]
|
||||||
|
fn test_add_assign_panic() {
|
||||||
|
let mut range = ByteRange::from(4..usize::max_value());
|
||||||
|
range += 5;
|
||||||
|
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub_assign() {
|
||||||
|
let mut range = ByteRange::from(10..20);
|
||||||
|
range -= 5;
|
||||||
|
|
||||||
|
assert_eq!(range, ByteRange::from(5..15));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "attempt to subtract with overflow"]
|
||||||
|
fn test_sub_assign_panic() {
|
||||||
|
let mut range = ByteRange::from(4..10);
|
||||||
|
range -= 5;
|
||||||
|
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "attempt to make the start (11) larger than the end (10)"]
|
||||||
|
fn test_set_start() { let _ = ByteRange::from(4..10).set_start(Some(11)); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
// normal addition
|
||||||
|
assert_eq!(ByteRange::from(5..10) + 5, ByteRange::from(10..15));
|
||||||
|
assert_eq!(ByteRange::from(..10) + 5, ByteRange::from(..15));
|
||||||
|
|
||||||
|
// adding 0
|
||||||
|
assert_eq!(ByteRange::from(5..10) + 0, ByteRange::from(5..10));
|
||||||
|
assert_eq!(ByteRange::from(..10) + 0, ByteRange::from(..10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "attempt to add with overflow"]
|
||||||
|
fn test_add_panic() { let _ = ByteRange::from(usize::max_value()..usize::max_value()) + 1; }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub() {
|
||||||
|
// normal subtraction
|
||||||
|
assert_eq!(ByteRange::from(5..10) - 4, ByteRange::from(1..6));
|
||||||
|
assert_eq!(ByteRange::from(..10) - 4, ByteRange::from(..6));
|
||||||
|
|
||||||
|
// subtracting 0
|
||||||
|
assert_eq!(ByteRange::from(0..0) - 0, ByteRange::from(0..0));
|
||||||
|
assert_eq!(ByteRange::from(2..3) - 0, ByteRange::from(2..3));
|
||||||
|
|
||||||
|
assert_eq!(ByteRange::from(..0) - 0, ByteRange::from(..0));
|
||||||
|
assert_eq!(ByteRange::from(..3) - 0, ByteRange::from(..3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "attempt to subtract with overflow"]
|
||||||
|
fn test_sub_panic() { let _ = ByteRange::from(0..0) - 1; }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_saturating_add() {
|
||||||
|
// normal addition
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(5..10).saturating_add(5),
|
||||||
|
ByteRange::from(10..15)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(..10).saturating_add(5),
|
||||||
|
ByteRange::from(..15)
|
||||||
|
);
|
||||||
|
|
||||||
|
// adding 0
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(6..11).saturating_add(0),
|
||||||
|
ByteRange::from(6..11)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(..11).saturating_add(0),
|
||||||
|
ByteRange::from(..11)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(0..0).saturating_add(0),
|
||||||
|
ByteRange::from(0..0)
|
||||||
|
);
|
||||||
|
assert_eq!(ByteRange::from(..0).saturating_add(0), ByteRange::from(..0));
|
||||||
|
|
||||||
|
// overflow
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(usize::max_value()..usize::max_value()).saturating_add(1),
|
||||||
|
ByteRange::from(usize::max_value()..usize::max_value())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(..usize::max_value()).saturating_add(1),
|
||||||
|
ByteRange::from(..usize::max_value())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(usize::max_value() - 5..usize::max_value()).saturating_add(1),
|
||||||
|
ByteRange::from(usize::max_value() - 5..usize::max_value())
|
||||||
|
);
|
||||||
|
|
||||||
|
// overflow, but something can be added to the range:
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(usize::max_value() - 5..usize::max_value() - 3).saturating_add(4),
|
||||||
|
ByteRange::from(usize::max_value() - 2..usize::max_value())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(..usize::max_value() - 3).saturating_add(4),
|
||||||
|
ByteRange::from(..usize::max_value())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_saturating_sub() {
|
||||||
|
// normal subtraction
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(5..10).saturating_sub(4),
|
||||||
|
ByteRange::from(1..6)
|
||||||
|
);
|
||||||
|
|
||||||
|
// subtracting 0
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(0..0).saturating_sub(0),
|
||||||
|
ByteRange::from(0..0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(2..3).saturating_sub(0),
|
||||||
|
ByteRange::from(2..3)
|
||||||
|
);
|
||||||
|
|
||||||
|
// the start underflows
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(0..5).saturating_sub(4),
|
||||||
|
ByteRange::from(0..5)
|
||||||
|
);
|
||||||
|
|
||||||
|
// the start underflows, but one can still subtract something from it
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(1..5).saturating_sub(2),
|
||||||
|
ByteRange::from(0..4)
|
||||||
|
);
|
||||||
|
|
||||||
|
// both start and end underflow
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(1..3).saturating_sub(5),
|
||||||
|
ByteRange::from(0..2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// both start + end are 0 + underflow
|
||||||
|
assert_eq!(
|
||||||
|
ByteRange::from(0..0).saturating_sub(1),
|
||||||
|
ByteRange::from(0..0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// half open ranges:
|
||||||
|
assert_eq!(ByteRange::from(..6).saturating_sub(2), ByteRange::from(..4));
|
||||||
|
assert_eq!(ByteRange::from(..5).saturating_sub(0), ByteRange::from(..5));
|
||||||
|
assert_eq!(ByteRange::from(..0).saturating_sub(0), ByteRange::from(..0));
|
||||||
|
|
||||||
|
assert_eq!(ByteRange::from(..0).saturating_sub(1), ByteRange::from(..0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let byte_range = ByteRange {
|
assert_eq!(ByteRange::from(0..5).to_string(), "5@0".to_string());
|
||||||
length: 0,
|
|
||||||
start: Some(5),
|
|
||||||
};
|
|
||||||
assert_eq!(byte_range.to_string(), "0@5".to_string());
|
|
||||||
|
|
||||||
let byte_range = ByteRange {
|
assert_eq!(
|
||||||
length: 99999,
|
ByteRange::from(2..100001).to_string(),
|
||||||
start: Some(2),
|
"99999@2".to_string()
|
||||||
};
|
);
|
||||||
assert_eq!(byte_range.to_string(), "99999@2".to_string());
|
|
||||||
|
|
||||||
let byte_range = ByteRange {
|
assert_eq!(ByteRange::from(..99999).to_string(), "99999".to_string());
|
||||||
length: 99999,
|
|
||||||
start: None,
|
|
||||||
};
|
|
||||||
assert_eq!(byte_range.to_string(), "99999".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
|
assert_eq!(ByteRange::from(2..22), "20@2".parse().unwrap());
|
||||||
|
|
||||||
|
assert_eq!(ByteRange::from(..300), "300".parse().unwrap());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ByteRange {
|
ByteRange::from_str("a"),
|
||||||
length: 99999,
|
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
||||||
start: Some(2),
|
|
||||||
},
|
|
||||||
"99999@2".parse::<ByteRange>().unwrap()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ByteRange {
|
ByteRange::from_str("1@a"),
|
||||||
length: 99999,
|
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
||||||
start: Some(2),
|
|
||||||
},
|
|
||||||
"99999@2".parse::<ByteRange>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ByteRange {
|
|
||||||
length: 99999,
|
|
||||||
start: None,
|
|
||||||
},
|
|
||||||
"99999".parse::<ByteRange>().unwrap()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!("".parse::<ByteRange>().is_err());
|
assert!("".parse::<ByteRange>().is_err());
|
||||||
|
|
|
@ -1,100 +1,65 @@
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// Specifies a list of parameters.
|
/// The maximum number of independent, simultaneous audio channels present in
|
||||||
|
/// any [`MediaSegment`] in the rendition.
|
||||||
///
|
///
|
||||||
/// # `MediaType::Audio`
|
/// For example, an `AC-3 5.1` rendition would have a maximum channel number of
|
||||||
/// The first parameter is a count of audio channels expressed as a [`u64`],
|
/// 6.
|
||||||
/// indicating the maximum number of independent, simultaneous audio channels
|
|
||||||
/// present in any [`MediaSegment`] in the rendition. For example, an
|
|
||||||
/// `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// Creating a `CHANNELS="6"` attribute
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::Channels;
|
|
||||||
/// let mut channels = Channels::new(6);
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// format!("CHANNELS=\"{}\"", channels),
|
|
||||||
/// "CHANNELS=\"6\"".to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// [`MediaSegment`]: crate::MediaSegment
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(ShortHand, Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
#[shorthand(enable(must_use))]
|
||||||
pub struct Channels {
|
pub struct Channels {
|
||||||
channel_number: u64,
|
/// The maximum number of independent simultaneous audio channels.
|
||||||
unknown: Vec<String>,
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Channels;
|
||||||
|
/// let mut channels = Channels::new(6);
|
||||||
|
/// # assert_eq!(channels.number(), 6);
|
||||||
|
///
|
||||||
|
/// channels.set_number(5);
|
||||||
|
/// assert_eq!(channels.number(), 5);
|
||||||
|
/// ```
|
||||||
|
number: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channels {
|
impl Channels {
|
||||||
/// Makes a new [`Channels`] struct.
|
/// Makes a new [`Channels`] struct.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::Channels;
|
/// # use hls_m3u8::types::Channels;
|
||||||
/// let mut channels = Channels::new(6);
|
/// let channels = Channels::new(6);
|
||||||
/// ```
|
|
||||||
pub fn new(value: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
channel_number: value,
|
|
||||||
unknown: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the channel number.
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// println!("CHANNELS=\"{}\"", channels);
|
||||||
|
/// # assert_eq!(format!("CHANNELS=\"{}\"", channels), "CHANNELS=\"6\"".to_string());
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::Channels;
|
//#[inline]
|
||||||
/// let mut channels = Channels::new(6);
|
#[must_use]
|
||||||
///
|
pub const fn new(number: u64) -> Self { Self { number } }
|
||||||
/// assert_eq!(channels.channel_number(), 6);
|
|
||||||
/// ```
|
|
||||||
pub const fn channel_number(&self) -> u64 { self.channel_number }
|
|
||||||
|
|
||||||
/// Sets the channel number.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::Channels;
|
|
||||||
/// let mut channels = Channels::new(3);
|
|
||||||
///
|
|
||||||
/// channels.set_channel_number(6);
|
|
||||||
/// assert_eq!(channels.channel_number(), 6)
|
|
||||||
/// ```
|
|
||||||
pub fn set_channel_number(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.channel_number = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Channels {
|
impl FromStr for Channels {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let parameters = input.split('/').collect::<Vec<_>>();
|
Ok(Self::new(
|
||||||
let channel_number = parameters
|
input.parse().map_err(|e| Error::parse_int(input, e))?,
|
||||||
.first()
|
))
|
||||||
.ok_or_else(|| Error::missing_attribute("First parameter of channels!"))?
|
|
||||||
.parse()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
channel_number,
|
|
||||||
unknown: parameters[1..].iter().map(|v| v.to_string()).collect(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Channels {
|
impl fmt::Display for Channels {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", self.channel_number)?;
|
write!(f, "{}", self.number)?;
|
||||||
if !self.unknown.is_empty() {
|
|
||||||
write!(f, "{}", self.unknown.join(","))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -107,17 +72,16 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let mut channels = Channels::new(6);
|
assert_eq!(Channels::new(6).to_string(), "6".to_string());
|
||||||
assert_eq!(channels.to_string(), "6".to_string());
|
|
||||||
|
|
||||||
channels.set_channel_number(7);
|
assert_eq!(Channels::new(7).to_string(), "7".to_string());
|
||||||
assert_eq!(channels.to_string(), "7".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!("6".parse::<Channels>().unwrap(), Channels::new(6));
|
assert_eq!(Channels::new(6), Channels::from_str("6").unwrap());
|
||||||
assert!("garbage".parse::<Channels>().is_err());
|
|
||||||
assert!("".parse::<Channels>().is_err());
|
assert!(Channels::from_str("garbage").is_err());
|
||||||
|
assert!(Channels::from_str("").is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,65 @@ use std::str::FromStr;
|
||||||
use crate::utils::{quote, unquote};
|
use crate::utils::{quote, unquote};
|
||||||
|
|
||||||
/// The identifier of a closed captions group or its absence.
|
/// The identifier of a closed captions group or its absence.
|
||||||
///
|
#[non_exhaustive]
|
||||||
/// See: [4.3.4.2. EXT-X-STREAM-INF]
|
|
||||||
///
|
|
||||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub enum ClosedCaptions {
|
pub enum ClosedCaptions {
|
||||||
|
/// It indicates the set of closed-caption renditions that can be used when
|
||||||
|
/// playing the presentation.
|
||||||
|
///
|
||||||
|
/// The [`String`] must match [`ExtXMedia::group_id`] elsewhere in the
|
||||||
|
/// Playlist and it's [`ExtXMedia::media_type`] must be
|
||||||
|
/// [`MediaType::ClosedCaptions`].
|
||||||
|
///
|
||||||
|
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
|
||||||
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
||||||
|
/// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions
|
||||||
GroupId(String),
|
GroupId(String),
|
||||||
|
/// This variant indicates that there are no closed captions in
|
||||||
|
/// any [`VariantStream`] in the [`MasterPlaylist`], therefore all
|
||||||
|
/// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a
|
||||||
|
/// value of [`ClosedCaptions::None`].
|
||||||
|
///
|
||||||
|
/// Having [`ClosedCaptions`] in one [`VariantStream`] but not in another
|
||||||
|
/// can trigger playback inconsistencies.
|
||||||
|
///
|
||||||
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
/// [`VariantStream::ExtXStreamInf`]:
|
||||||
|
/// crate::tags::VariantStream::ExtXStreamInf
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ClosedCaptions {
|
||||||
|
/// Creates a [`ClosedCaptions::GroupId`] with the provided [`String`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::ClosedCaptions;
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// ClosedCaptions::group_id("vg1"),
|
||||||
|
/// ClosedCaptions::GroupId("vg1".into())
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn group_id<I: Into<String>>(value: I) -> Self {
|
||||||
|
//
|
||||||
|
Self::GroupId(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: PartialEq<str>> PartialEq<T> for ClosedCaptions {
|
||||||
|
fn eq(&self, other: &T) -> bool {
|
||||||
|
match &self {
|
||||||
|
Self::GroupId(value) => other.eq(value),
|
||||||
|
Self::None => other.eq("NONE"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ClosedCaptions {
|
impl fmt::Display for ClosedCaptions {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::GroupId(value) => write!(f, "{}", quote(value)),
|
Self::GroupId(value) => write!(f, "{}", quote(value)),
|
||||||
Self::None => write!(f, "NONE"),
|
Self::None => write!(f, "NONE"),
|
||||||
|
|
101
src/types/codecs.rs
Normal file
101
src/types/codecs.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
use core::fmt;
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use derive_more::{AsMut, AsRef, Deref, DerefMut};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// A list of formats, where each format specifies a media sample type that is
|
||||||
|
/// present in one or more renditions specified by the [`VariantStream`].
|
||||||
|
///
|
||||||
|
/// Valid format identifiers are those in the ISO Base Media File Format Name
|
||||||
|
/// Space defined by "The 'Codecs' and 'Profiles' Parameters for "Bucket" Media
|
||||||
|
/// Types" ([RFC6381]).
|
||||||
|
///
|
||||||
|
/// For example, a stream containing AAC low complexity (AAC-LC) audio and H.264
|
||||||
|
/// Main Profile Level 3.0 video would be
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Codecs;
|
||||||
|
/// let codecs = Codecs::from(&["mp4a.40.2", "avc1.4d401e"]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
#[derive(
|
||||||
|
AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
|
||||||
|
)]
|
||||||
|
pub struct Codecs {
|
||||||
|
list: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Codecs {
|
||||||
|
/// Makes a new (empty) [`Codecs`] struct.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Codecs;
|
||||||
|
/// let codecs = Codecs::new();
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self { Self { list: Vec::new() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Codecs {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
if let Some(codec) = self.list.iter().next() {
|
||||||
|
write!(f, "{}", codec)?;
|
||||||
|
|
||||||
|
for codec in self.list.iter().skip(1) {
|
||||||
|
write!(f, ",{}", codec)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for Codecs {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self {
|
||||||
|
list: input.split(',').map(|s| s.into()).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>, I: IntoIterator<Item = T>> From<I> for Codecs {
|
||||||
|
fn from(value: I) -> Self {
|
||||||
|
Self {
|
||||||
|
list: value.into_iter().map(|s| s.as_ref().to_string()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from() {
|
||||||
|
assert_eq!(Codecs::from(Vec::<&str>::new()), Codecs::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(
|
||||||
|
Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"]).to_string(),
|
||||||
|
"mp4a.40.2,avc1.4d401e".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
Codecs::from_str("mp4a.40.2,avc1.4d401e").unwrap(),
|
||||||
|
Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,142 +0,0 @@
|
||||||
use core::ops::Deref;
|
|
||||||
use core::str::FromStr;
|
|
||||||
|
|
||||||
use derive_more::Display;
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
/// Non-negative decimal floating-point number.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]:
|
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.2
|
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display)]
|
|
||||||
pub(crate) struct DecimalFloatingPoint(f64);
|
|
||||||
|
|
||||||
impl DecimalFloatingPoint {
|
|
||||||
/// Makes a new [`DecimalFloatingPoint`] instance.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// The given value must have a positive sign and be finite,
|
|
||||||
/// otherwise this function will return an error that has the kind
|
|
||||||
/// `ErrorKind::InvalidInput`.
|
|
||||||
pub fn new(value: f64) -> crate::Result<Self> {
|
|
||||||
if value.is_sign_negative() || value.is_infinite() || value.is_nan() {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
Ok(Self(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn from_f64_unchecked(value: f64) -> Self { Self(value) }
|
|
||||||
|
|
||||||
/// Converts [`DecimalFloatingPoint`] to [`f64`].
|
|
||||||
pub const fn as_f64(self) -> f64 { self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// this trait is implemented manually, so it doesn't construct a
|
|
||||||
// [`DecimalFloatingPoint`], with a negative value.
|
|
||||||
impl FromStr for DecimalFloatingPoint {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> { Self::new(input.parse()?) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for DecimalFloatingPoint {
|
|
||||||
type Target = f64;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<f64> for DecimalFloatingPoint {
|
|
||||||
fn from(value: f64) -> Self {
|
|
||||||
let mut result = value;
|
|
||||||
|
|
||||||
// guard against the unlikely case of an infinite value...
|
|
||||||
if result.is_infinite() || result.is_nan() {
|
|
||||||
result = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self(result.abs())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<f32> for DecimalFloatingPoint {
|
|
||||||
fn from(value: f32) -> Self { f64::from(value).into() }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
macro_rules! test_from {
|
|
||||||
( $($input:expr),* ) => {
|
|
||||||
use ::core::convert::From;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from() {
|
|
||||||
$(
|
|
||||||
assert_eq!(
|
|
||||||
DecimalFloatingPoint::from($input),
|
|
||||||
DecimalFloatingPoint::new(1.0).unwrap(),
|
|
||||||
);
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_from![1_u8, 1_u16, 1_u32, 1.0_f32, -1.0_f32, 1.0_f64, -1.0_f64];
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_display() {
|
|
||||||
let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap();
|
|
||||||
assert_eq!(decimal_floating_point.to_string(), "22".to_string());
|
|
||||||
|
|
||||||
let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap();
|
|
||||||
assert_eq!(decimal_floating_point.to_string(), "4.1".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_parser() {
|
|
||||||
let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
decimal_floating_point,
|
|
||||||
"22".parse::<DecimalFloatingPoint>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
decimal_floating_point,
|
|
||||||
"4.1".parse::<DecimalFloatingPoint>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("1#".parse::<DecimalFloatingPoint>().is_err());
|
|
||||||
assert!("-1.0".parse::<DecimalFloatingPoint>().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new() {
|
|
||||||
assert!(DecimalFloatingPoint::new(::std::f64::INFINITY).is_err());
|
|
||||||
assert!(DecimalFloatingPoint::new(-1.0).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_f64() {
|
|
||||||
assert_eq!(DecimalFloatingPoint::new(1.0).unwrap().as_f64(), 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from_inf() {
|
|
||||||
assert_eq!(
|
|
||||||
DecimalFloatingPoint::from(::std::f64::INFINITY),
|
|
||||||
DecimalFloatingPoint::new(0.0).unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref() {
|
|
||||||
assert_eq!(DecimalFloatingPoint::from(0.1).floor(), 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use derive_more::Display;
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
/// This is a simple wrapper type for the display resolution. (1920x1080,
|
|
||||||
/// 1280x720, ...).
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
|
|
||||||
#[display(fmt = "{}x{}", width, height)]
|
|
||||||
pub struct DecimalResolution {
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DecimalResolution {
|
|
||||||
/// Creates a new [`DecimalResolution`].
|
|
||||||
pub const fn new(width: usize, height: usize) -> Self { Self { width, height } }
|
|
||||||
|
|
||||||
/// Horizontal pixel dimension.
|
|
||||||
pub const fn width(&self) -> usize { self.width }
|
|
||||||
|
|
||||||
/// Sets Horizontal pixel dimension.
|
|
||||||
pub fn set_width(&mut self, value: usize) -> &mut Self {
|
|
||||||
self.width = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vertical pixel dimension.
|
|
||||||
pub const fn height(&self) -> usize { self.height }
|
|
||||||
|
|
||||||
/// Sets Vertical pixel dimension.
|
|
||||||
pub fn set_height(&mut self, value: usize) -> &mut Self {
|
|
||||||
self.height = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [`DecimalResolution`] can be constructed from a tuple; `(width, height)`.
|
|
||||||
impl From<(usize, usize)> for DecimalResolution {
|
|
||||||
fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for DecimalResolution {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
let tokens = input.splitn(2, 'x').collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if tokens.len() != 2 {
|
|
||||||
return Err(Error::custom(format!(
|
|
||||||
"InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}",
|
|
||||||
input,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
width: tokens[0].parse()?,
|
|
||||||
height: tokens[1].parse()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_display() {
|
|
||||||
assert_eq!(
|
|
||||||
DecimalResolution::new(1920, 1080).to_string(),
|
|
||||||
"1920x1080".to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DecimalResolution::new(1280, 720).to_string(),
|
|
||||||
"1280x720".to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
assert_eq!(
|
|
||||||
DecimalResolution::new(1920, 1080),
|
|
||||||
"1920x1080".parse::<DecimalResolution>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DecimalResolution::new(1280, 720),
|
|
||||||
"1280x720".parse::<DecimalResolution>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("1280".parse::<DecimalResolution>().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_width() {
|
|
||||||
assert_eq!(DecimalResolution::new(1920, 1080).width(), 1920);
|
|
||||||
assert_eq!(DecimalResolution::new(1920, 1080).set_width(12).width(), 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_height() {
|
|
||||||
assert_eq!(DecimalResolution::new(1920, 1080).height(), 1080);
|
|
||||||
assert_eq!(
|
|
||||||
DecimalResolution::new(1920, 1080).set_height(12).height(),
|
|
||||||
12
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from() {
|
|
||||||
assert_eq!(
|
|
||||||
DecimalResolution::from((1920, 1080)),
|
|
||||||
DecimalResolution::new(1920, 1080)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
|
@ -10,270 +11,131 @@ use crate::types::{
|
||||||
use crate::utils::{quote, unquote};
|
use crate::utils::{quote, unquote};
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
/// Specifies how to decrypt encrypted data from the server.
|
||||||
|
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
#[builder(setter(into), build_fn(validate = "Self::validate"))]
|
#[builder(setter(into), build_fn(validate = "Self::validate"))]
|
||||||
/// [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`]
|
#[shorthand(enable(skip, must_use, into))]
|
||||||
/// and [`ExtXKey`].
|
#[non_exhaustive]
|
||||||
///
|
|
||||||
/// [`ExtXSessionKey`]: crate::tags::ExtXSessionKey
|
|
||||||
/// [`ExtXKey`]: crate::tags::ExtXKey
|
|
||||||
pub struct DecryptionKey {
|
pub struct DecryptionKey {
|
||||||
/// The [EncryptionMethod].
|
/// The encryption method, which has been used to encrypt the data.
|
||||||
pub(crate) method: EncryptionMethod,
|
///
|
||||||
|
/// An [`EncryptionMethod::Aes128`] signals that the data is encrypted using
|
||||||
|
/// the Advanced Encryption Standard (AES) with a 128-bit key, Cipher Block
|
||||||
|
/// Chaining (CBC), and Public-Key Cryptography Standards #7 (PKCS7)
|
||||||
|
/// padding. CBC is restarted on each segment boundary, using either the
|
||||||
|
/// [`DecryptionKey::iv`] field or the [`MediaSegment::number`] as the IV.
|
||||||
|
///
|
||||||
|
/// An [`EncryptionMethod::SampleAes`] means that the [`MediaSegment`]s
|
||||||
|
/// contain media samples, such as audio or video, that are encrypted using
|
||||||
|
/// the Advanced Encryption Standard (Aes128). How these media streams are
|
||||||
|
/// encrypted and encapsulated in a segment depends on the media encoding
|
||||||
|
/// and the media format of the segment.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is required.
|
||||||
|
///
|
||||||
|
/// [`MediaSegment::number`]: crate::MediaSegment::number
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
pub method: EncryptionMethod,
|
||||||
|
/// This uri points to a key file, which contains the cipher key.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is required.
|
||||||
#[builder(setter(into, strip_option), default)]
|
#[builder(setter(into, strip_option), default)]
|
||||||
/// An `URI`, that specifies how to obtain the key.
|
#[shorthand(disable(skip))]
|
||||||
pub(crate) uri: Option<String>,
|
pub(crate) uri: String,
|
||||||
|
/// An initialization vector (IV) is a fixed size input that can be used
|
||||||
|
/// along with a secret key for data encryption.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional and an absent value indicates that
|
||||||
|
/// [`MediaSegment::number`] should be used instead.
|
||||||
|
///
|
||||||
|
/// [`MediaSegment::number`]: crate::MediaSegment::number
|
||||||
#[builder(setter(into, strip_option), default)]
|
#[builder(setter(into, strip_option), default)]
|
||||||
/// The IV (Initialization Vector) attribute.
|
pub iv: InitializationVector,
|
||||||
pub(crate) iv: Option<InitializationVector>,
|
/// A server may offer multiple ways to retrieve a key by providing multiple
|
||||||
|
/// [`DecryptionKey`]s with different [`KeyFormat`] values.
|
||||||
|
///
|
||||||
|
/// An [`EncryptionMethod::Aes128`] uses 16-octet (16 byte/128 bit) keys. If
|
||||||
|
/// the format is [`KeyFormat::Identity`], the key file is a single packed
|
||||||
|
/// array of 16 octets (16 byte/128 bit) in binary format.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
#[builder(setter(into, strip_option), default)]
|
#[builder(setter(into, strip_option), default)]
|
||||||
/// A string that specifies how the key is
|
pub format: Option<KeyFormat>,
|
||||||
/// represented in the resource identified by the `URI`.
|
/// A list of numbers that can be used to indicate which version(s)
|
||||||
pub(crate) key_format: Option<KeyFormat>,
|
/// this instance complies with, if more than one version of a particular
|
||||||
|
/// [`KeyFormat`] is defined.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
#[builder(setter(into, strip_option), default)]
|
#[builder(setter(into, strip_option), default)]
|
||||||
/// The [KeyFormatVersions] attribute.
|
pub versions: Option<KeyFormatVersions>,
|
||||||
pub(crate) key_format_versions: Option<KeyFormatVersions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DecryptionKeyBuilder {
|
|
||||||
fn validate(&self) -> Result<(), String> {
|
|
||||||
if self.method != Some(EncryptionMethod::None) && self.uri.is_none() {
|
|
||||||
return Err(Error::custom("Missing URL").to_string());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecryptionKey {
|
impl DecryptionKey {
|
||||||
/// Makes a new [`DecryptionKey`].
|
/// Creates a new `DecryptionKey` from an uri pointing to the key data and
|
||||||
|
/// an `EncryptionMethod`.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
/// # use hls_m3u8::types::DecryptionKey;
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
/// use hls_m3u8::types::EncryptionMethod;
|
||||||
///
|
///
|
||||||
/// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
/// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.uri/key");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: ToString>(method: EncryptionMethod, uri: T) -> Self {
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub fn new<I: Into<String>>(method: EncryptionMethod, uri: I) -> Self {
|
||||||
Self {
|
Self {
|
||||||
method,
|
method,
|
||||||
uri: Some(uri.to_string()),
|
uri: uri.into(),
|
||||||
iv: None,
|
iv: InitializationVector::default(),
|
||||||
key_format: None,
|
format: None,
|
||||||
key_format_versions: None,
|
versions: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`EncryptionMethod`].
|
/// Returns a builder for a `DecryptionKey`.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
/// # use hls_m3u8::types::DecryptionKey;
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
/// use hls_m3u8::types::{EncryptionMethod, KeyFormat};
|
||||||
///
|
///
|
||||||
/// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
/// let key = DecryptionKey::builder()
|
||||||
///
|
/// .method(EncryptionMethod::Aes128)
|
||||||
/// assert_eq!(key.method(), EncryptionMethod::Aes128);
|
/// .uri("https://www.example.com/")
|
||||||
|
/// .iv([
|
||||||
|
/// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
/// ])
|
||||||
|
/// .format(KeyFormat::Identity)
|
||||||
|
/// .versions(&[1, 2, 3, 4, 5])
|
||||||
|
/// .build()?;
|
||||||
|
/// # Ok::<(), String>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn method(&self) -> EncryptionMethod { self.method }
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
/// Returns a Builder to build a [DecryptionKey].
|
|
||||||
pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() }
|
pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() }
|
||||||
|
|
||||||
/// Sets the [`EncryptionMethod`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// key.set_method(EncryptionMethod::SampleAes);
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// key.to_string(),
|
|
||||||
/// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self {
|
|
||||||
self.method = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an `URI`, that specifies how to obtain the key.
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
/// This attribute is required, if the [EncryptionMethod] is not `None`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
///
|
|
||||||
/// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// assert_eq!(key.uri(), &Some("https://www.example.com/".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub const fn uri(&self) -> &Option<String> { &self.uri }
|
|
||||||
|
|
||||||
/// Sets the `URI` attribute.
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
/// This attribute is required, if the [`EncryptionMethod`] is not `None`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// key.set_uri(Some("http://www.google.com/"));
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// key.to_string(),
|
|
||||||
/// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn set_uri<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.uri = value.map(|v| v.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the IV (Initialization Vector) attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// # assert_eq!(key.iv(), None);
|
|
||||||
/// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]));
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// key.iv(),
|
|
||||||
/// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn iv(&self) -> Option<[u8; 16]> {
|
|
||||||
if let Some(iv) = &self.iv {
|
|
||||||
Some(iv.to_slice())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the `IV` attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]));
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// key.to_string(),
|
|
||||||
/// "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607"
|
|
||||||
/// .to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn set_iv<T>(&mut self, value: Option<T>) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<[u8; 16]>,
|
|
||||||
{
|
|
||||||
self.iv = value.map(|v| InitializationVector(v.into()));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a string that specifies how the key is
|
|
||||||
/// represented in the resource identified by the `URI`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::{EncryptionMethod, KeyFormat};
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// key.set_key_format(Some(KeyFormat::Identity));
|
|
||||||
///
|
|
||||||
/// assert_eq!(key.key_format(), Some(KeyFormat::Identity));
|
|
||||||
/// ```
|
|
||||||
pub const fn key_format(&self) -> Option<KeyFormat> { self.key_format }
|
|
||||||
|
|
||||||
/// Sets the [`KeyFormat`] attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::{EncryptionMethod, KeyFormat};
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// key.set_key_format(Some(KeyFormat::Identity));
|
|
||||||
///
|
|
||||||
/// assert_eq!(key.key_format(), Some(KeyFormat::Identity));
|
|
||||||
/// ```
|
|
||||||
pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.key_format = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`KeyFormatVersions`] attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions};
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5]));
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// key.key_format_versions(),
|
|
||||||
/// &Some(KeyFormatVersions::from(vec![1, 2, 3, 4, 5]))
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub const fn key_format_versions(&self) -> &Option<KeyFormatVersions> {
|
|
||||||
&self.key_format_versions
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the [`KeyFormatVersions`] attribute.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::DecryptionKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
///
|
|
||||||
/// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
|
||||||
///
|
|
||||||
/// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5]));
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// key.to_string(),
|
|
||||||
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\""
|
|
||||||
/// .to_string()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn set_key_format_versions<T: Into<KeyFormatVersions>>(
|
|
||||||
&mut self,
|
|
||||||
value: Option<T>,
|
|
||||||
) -> &mut Self {
|
|
||||||
self.key_format_versions = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
||||||
|
/// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is
|
||||||
|
/// specified.
|
||||||
|
///
|
||||||
|
/// Otherwise [`ProtocolVersion::V1`] is required.
|
||||||
impl RequiredVersion for DecryptionKey {
|
impl RequiredVersion for DecryptionKey {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
if self.key_format.is_some() || self.key_format_versions.is_some() {
|
if self.format.is_some() || self.versions.is_some() {
|
||||||
ProtocolVersion::V5
|
ProtocolVersion::V5
|
||||||
} else if self.iv.is_some() {
|
} else if self.iv.is_some() {
|
||||||
ProtocolVersion::V2
|
ProtocolVersion::V2
|
||||||
|
@ -290,16 +152,22 @@ impl FromStr for DecryptionKey {
|
||||||
let mut method = None;
|
let mut method = None;
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut iv = None;
|
let mut iv = None;
|
||||||
let mut key_format = None;
|
let mut format = None;
|
||||||
let mut key_format_versions = None;
|
let mut versions = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"METHOD" => method = Some(value.parse()?),
|
"METHOD" => method = Some(value.parse().map_err(Error::strum)?),
|
||||||
"URI" => uri = Some(unquote(value)),
|
"URI" => {
|
||||||
|
let unquoted_uri = unquote(value);
|
||||||
|
|
||||||
|
if !unquoted_uri.trim().is_empty() {
|
||||||
|
uri = Some(unquoted_uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
"IV" => iv = Some(value.parse()?),
|
"IV" => iv = Some(value.parse()?),
|
||||||
"KEYFORMAT" => key_format = Some(value.parse()?),
|
"KEYFORMAT" => format = Some(value.parse()?),
|
||||||
"KEYFORMATVERSIONS" => key_format_versions = Some(value.parse()?),
|
"KEYFORMATVERSIONS" => versions = Some(value.parse()?),
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized
|
// > ignore any attribute/value pair with an unrecognized
|
||||||
|
@ -309,42 +177,50 @@ impl FromStr for DecryptionKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
let method = method.ok_or_else(|| Error::missing_value("METHOD"))?;
|
let method = method.ok_or_else(|| Error::missing_value("METHOD"))?;
|
||||||
if method != EncryptionMethod::None && uri.is_none() {
|
let uri = uri.ok_or_else(|| Error::missing_value("URI"))?;
|
||||||
return Err(Error::missing_value("URI"));
|
let iv = iv.unwrap_or_default();
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
method,
|
method,
|
||||||
uri,
|
uri,
|
||||||
iv,
|
iv,
|
||||||
key_format,
|
format,
|
||||||
key_format_versions,
|
versions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DecryptionKey {
|
impl fmt::Display for DecryptionKey {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "METHOD={}", self.method)?;
|
write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?;
|
||||||
|
|
||||||
if self.method == EncryptionMethod::None {
|
if let InitializationVector::Aes128(_) = &self.iv {
|
||||||
return Ok(());
|
write!(f, ",IV={}", &self.iv)?;
|
||||||
}
|
}
|
||||||
if let Some(uri) = &self.uri {
|
|
||||||
write!(f, ",URI={}", quote(uri))?;
|
if let Some(value) = &self.format {
|
||||||
}
|
|
||||||
if let Some(value) = &self.iv {
|
|
||||||
write!(f, ",IV={}", value)?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.key_format {
|
|
||||||
write!(f, ",KEYFORMAT={}", quote(value))?;
|
write!(f, ",KEYFORMAT={}", quote(value))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(key_format_versions) = &self.key_format_versions {
|
if let Some(value) = &self.versions {
|
||||||
if !key_format_versions.is_default() {
|
if !value.is_default() {
|
||||||
write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?;
|
write!(f, ",KEYFORMATVERSIONS={}", value)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecryptionKeyBuilder {
|
||||||
|
fn validate(&self) -> Result<(), String> {
|
||||||
|
// a decryption key must contain a uri and a method
|
||||||
|
if self.method.is_none() {
|
||||||
|
return Err(Error::missing_field("DecryptionKey", "method").to_string());
|
||||||
|
} else if self.uri.is_none() {
|
||||||
|
return Err(Error::missing_field("DecryptionKey", "uri").to_string());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -352,31 +228,58 @@ impl fmt::Display for DecryptionKey {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::EncryptionMethod;
|
use crate::types::{EncryptionMethod, KeyFormat};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
macro_rules! generate_tests {
|
||||||
|
( $( { $struct:expr, $str:expr } ),+ $(,)* ) => {
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
$(
|
||||||
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
$(
|
||||||
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
|
)+
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"),
|
||||||
|
concat!(
|
||||||
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"http://www.example.com\",",
|
||||||
|
"UNKNOWNTAG=abcd"
|
||||||
|
).parse().unwrap(),
|
||||||
|
);
|
||||||
|
assert!("METHOD=AES-128,URI=".parse::<DecryptionKey>().is_err());
|
||||||
|
assert!("garbage".parse::<DecryptionKey>().is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_builder() {
|
fn test_builder() {
|
||||||
let key = DecryptionKey::builder()
|
let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
||||||
.method(EncryptionMethod::Aes128)
|
key.iv = [
|
||||||
.uri("https://www.example.com/")
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
.iv([
|
]
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
.into();
|
||||||
])
|
key.format = Some(KeyFormat::Identity);
|
||||||
.key_format(KeyFormat::Identity)
|
key.versions = Some(vec![1, 2, 3, 4, 5].into());
|
||||||
.key_format_versions(vec![1, 2, 3, 4, 5])
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
key.to_string(),
|
DecryptionKey::builder()
|
||||||
"METHOD=AES-128,\
|
.method(EncryptionMethod::Aes128)
|
||||||
URI=\"https://www.example.com/\",\
|
.uri("https://www.example.com/")
|
||||||
IV=0x10ef8f758ca555115584bb5b3c687f52,\
|
.iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82])
|
||||||
KEYFORMAT=\"identity\",\
|
.format(KeyFormat::Identity)
|
||||||
KEYFORMATVERSIONS=\"1/2/3/4/5\"\
|
.versions(vec![1, 2, 3, 4, 5])
|
||||||
"
|
.build()
|
||||||
.to_string()
|
.unwrap(),
|
||||||
|
key
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(DecryptionKey::builder().build().is_err());
|
assert!(DecryptionKey::builder().build().is_err());
|
||||||
|
@ -386,93 +289,47 @@ mod test {
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
generate_tests! {
|
||||||
fn test_display() {
|
{
|
||||||
let mut key = DecryptionKey::new(
|
|
||||||
EncryptionMethod::Aes128,
|
|
||||||
"https://www.example.com/hls-key/key.bin",
|
|
||||||
);
|
|
||||||
key.set_iv(Some([
|
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
|
||||||
]));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
key.to_string(),
|
|
||||||
"METHOD=AES-128,\
|
|
||||||
URI=\"https://www.example.com/hls-key/key.bin\",\
|
|
||||||
IV=0x10ef8f758ca555115584bb5b3c687f52"
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
assert_eq!(
|
|
||||||
"METHOD=AES-128,\
|
|
||||||
URI=\"https://priv.example.com/key.php?r=52\""
|
|
||||||
.parse::<DecryptionKey>()
|
|
||||||
.unwrap(),
|
|
||||||
DecryptionKey::new(
|
DecryptionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
"https://priv.example.com/key.php?r=52"
|
"https://priv.example.com/key.php?r=52"
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"https://priv.example.com/key.php?r=52\""
|
||||||
)
|
)
|
||||||
);
|
},
|
||||||
|
{
|
||||||
let mut key = DecryptionKey::new(
|
DecryptionKey::builder()
|
||||||
EncryptionMethod::Aes128,
|
.method(EncryptionMethod::Aes128)
|
||||||
"https://www.example.com/hls-key/key.bin",
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
);
|
.iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82])
|
||||||
key.set_iv(Some([
|
.build()
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
|
||||||
]));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"METHOD=AES-128,\
|
|
||||||
URI=\"https://www.example.com/hls-key/key.bin\",\
|
|
||||||
IV=0X10ef8f758ca555115584bb5b3c687f52"
|
|
||||||
.parse::<DecryptionKey>()
|
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
key
|
concat!(
|
||||||
);
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com");
|
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||||
key.set_iv(Some([
|
)
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
},
|
||||||
]));
|
{
|
||||||
key.set_key_format(Some(KeyFormat::Identity));
|
DecryptionKey::builder()
|
||||||
|
.method(EncryptionMethod::Aes128)
|
||||||
assert_eq!(
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
"METHOD=AES-128,\
|
.iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82])
|
||||||
URI=\"http://www.example.com\",\
|
.format(KeyFormat::Identity)
|
||||||
IV=0x10ef8f758ca555115584bb5b3c687f52,\
|
.versions(vec![1, 2, 3])
|
||||||
KEYFORMAT=\"identity\""
|
.build()
|
||||||
.parse::<DecryptionKey>()
|
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
key
|
concat!(
|
||||||
);
|
"METHOD=AES-128,",
|
||||||
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
key.set_key_format_versions(Some(vec![1, 2, 3]));
|
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
||||||
assert_eq!(
|
"KEYFORMAT=\"identity\",",
|
||||||
"METHOD=AES-128,\
|
"KEYFORMATVERSIONS=\"1/2/3\""
|
||||||
URI=\"http://www.example.com\",\
|
)
|
||||||
IV=0x10ef8f758ca555115584bb5b3c687f52,\
|
},
|
||||||
KEYFORMAT=\"identity\",\
|
|
||||||
KEYFORMATVERSIONS=\"1/2/3\""
|
|
||||||
.parse::<DecryptionKey>()
|
|
||||||
.unwrap(),
|
|
||||||
key
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"METHOD=AES-128,\
|
|
||||||
URI=\"http://www.example.com\",\
|
|
||||||
UNKNOWNTAG=abcd"
|
|
||||||
.parse::<DecryptionKey>()
|
|
||||||
.unwrap(),
|
|
||||||
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com")
|
|
||||||
);
|
|
||||||
assert!("METHOD=AES-128,URI=".parse::<DecryptionKey>().is_err());
|
|
||||||
assert!("garbage".parse::<DecryptionKey>().is_err());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -487,8 +344,8 @@ mod test {
|
||||||
DecryptionKey::builder()
|
DecryptionKey::builder()
|
||||||
.method(EncryptionMethod::Aes128)
|
.method(EncryptionMethod::Aes128)
|
||||||
.uri("https://www.example.com/")
|
.uri("https://www.example.com/")
|
||||||
.key_format(KeyFormat::Identity)
|
.format(KeyFormat::Identity)
|
||||||
.key_format_versions(vec![1, 2, 3])
|
.versions(vec![1, 2, 3])
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.required_version(),
|
.required_version(),
|
||||||
|
|
|
@ -1,49 +1,51 @@
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
/// Encryption method.
|
/// The encryption method.
|
||||||
///
|
#[non_exhaustive]
|
||||||
/// See: [4.3.2.4. EXT-X-KEY]
|
|
||||||
///
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||||
pub enum EncryptionMethod {
|
pub enum EncryptionMethod {
|
||||||
/// `None` means that [MediaSegment]s are not encrypted.
|
/// The [`MediaSegment`]s are completely encrypted using the Advanced
|
||||||
|
/// Encryption Standard ([AES-128]) with a 128-bit key, Cipher Block
|
||||||
|
/// Chaining (CBC), and [Public-Key Cryptography Standards #7 (PKCS7)]
|
||||||
|
/// padding.
|
||||||
///
|
///
|
||||||
/// [MediaSegment]: crate::MediaSegment
|
|
||||||
None,
|
|
||||||
/// `Aes128` signals that the [MediaSegment]s are completely encrypted
|
|
||||||
/// using the Advanced Encryption Standard ([AES_128]) with a 128-bit
|
|
||||||
/// key, Cipher Block Chaining (CBC), and
|
|
||||||
/// [Public-Key Cryptography Standards #7 (PKCS7)] padding.
|
|
||||||
/// CBC is restarted on each segment boundary, using either the
|
/// CBC is restarted on each segment boundary, using either the
|
||||||
/// Initialization Vector (IV) attribute value or the Media Sequence
|
/// Initialization Vector (IV) or the Media Sequence Number as the IV
|
||||||
/// Number as the IV.
|
|
||||||
///
|
///
|
||||||
/// [MediaSegment]: crate::MediaSegment
|
/// ```
|
||||||
/// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
|
/// # let media_sequence_number = 5;
|
||||||
|
/// # assert_eq!(
|
||||||
|
/// format!("0x{:032x}", media_sequence_number)
|
||||||
|
/// # , "0x00000000000000000000000000000005".to_string());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
/// [AES-128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
|
||||||
/// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652
|
/// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652
|
||||||
#[strum(serialize = "AES-128")]
|
#[strum(serialize = "AES-128")]
|
||||||
Aes128,
|
Aes128,
|
||||||
/// `SampleAes` means that the [MediaSegment]s
|
/// The [`MediaSegment`]s contain media samples, such as audio or video,
|
||||||
/// contain media samples, such as audio or video, that are encrypted
|
/// that are encrypted using the Advanced Encryption Standard ([`AES-128`]).
|
||||||
/// using the Advanced Encryption Standard ([AES_128]). How these media
|
|
||||||
/// streams are encrypted and encapsulated in a segment depends on the
|
|
||||||
/// media encoding and the media format of the segment. fMP4 Media
|
|
||||||
/// Segments are encrypted using the 'cbcs' scheme of
|
|
||||||
/// [Common Encryption]. Encryption of other Media Segment
|
|
||||||
/// formats containing [H.264], [AAC], [AC-3],
|
|
||||||
/// and Enhanced [AC-3] media streams is described in the HTTP
|
|
||||||
/// Live Streaming (HLS) [SampleEncryption specification].
|
|
||||||
///
|
///
|
||||||
/// [MediaSegment]: crate::MediaSegment
|
/// How these media streams are encrypted and encapsulated in a segment
|
||||||
/// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
|
/// depends on the media encoding and the media format of the segment.
|
||||||
|
///
|
||||||
|
/// `fMP4` [`MediaSegment`]s are encrypted using the `cbcs` scheme of
|
||||||
|
/// [Common Encryption].
|
||||||
|
/// Encryption of other [`MediaSegment`] formats containing [H.264], [AAC],
|
||||||
|
/// [AC-3], and Enhanced [AC-3] media streams is described in the
|
||||||
|
/// [HTTP Live Streaming (HLS) SampleEncryption specification].
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
/// [`AES-128`]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
|
||||||
/// [Common Encryption]: https://tools.ietf.org/html/rfc8216#ref-COMMON_ENC
|
/// [Common Encryption]: https://tools.ietf.org/html/rfc8216#ref-COMMON_ENC
|
||||||
/// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264
|
/// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264
|
||||||
/// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496
|
/// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496
|
||||||
/// [AC-3]: https://tools.ietf.org/html/rfc8216#ref-AC_3
|
/// [AC-3]: https://tools.ietf.org/html/rfc8216#ref-AC_3
|
||||||
/// [SampleEncryption specification]: https://tools.ietf.org/html/rfc8216#ref-SampleEnc
|
/// [HTTP Live Streaming (HLS) SampleEncryption specification]:
|
||||||
|
/// https://tools.ietf.org/html/rfc8216#ref-SampleEnc
|
||||||
SampleAes,
|
SampleAes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +61,6 @@ mod tests {
|
||||||
EncryptionMethod::SampleAes.to_string(),
|
EncryptionMethod::SampleAes.to_string(),
|
||||||
"SAMPLE-AES".to_string()
|
"SAMPLE-AES".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(EncryptionMethod::None.to_string(), "NONE".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -74,11 +75,6 @@ mod tests {
|
||||||
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
EncryptionMethod::None,
|
|
||||||
"NONE".parse::<EncryptionMethod>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("unknown".parse::<EncryptionMethod>().is_err());
|
assert!("unknown".parse::<EncryptionMethod>().is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
312
src/types/float.rs
Normal file
312
src/types/float.rs
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use derive_more::{AsRef, Deref, Display};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// A wrapper type around an [`f32`] that can not be constructed
|
||||||
|
/// with [`NaN`], [`INFINITY`] or [`NEG_INFINITY`].
|
||||||
|
///
|
||||||
|
/// [`NaN`]: core::f32::NAN
|
||||||
|
/// [`INFINITY`]: core::f32::INFINITY
|
||||||
|
/// [`NEG_INFINITY`]: core::f32::NEG_INFINITY
|
||||||
|
#[derive(AsRef, Deref, Default, Debug, Copy, Clone, Display, PartialOrd)]
|
||||||
|
pub struct Float(f32);
|
||||||
|
|
||||||
|
impl Float {
|
||||||
|
/// Makes a new [`Float`] from an [`f32`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If the given float is infinite or [`NaN`].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Float;
|
||||||
|
/// let float = Float::new(1.0);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This would panic:
|
||||||
|
///
|
||||||
|
/// ```should_panic
|
||||||
|
/// # use hls_m3u8::types::Float;
|
||||||
|
/// use core::f32::NAN;
|
||||||
|
///
|
||||||
|
/// let float = Float::new(NAN);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`NaN`]: core::f32::NAN
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(float: f32) -> Self {
|
||||||
|
if float.is_infinite() {
|
||||||
|
panic!("float must be finite: `{}`", float);
|
||||||
|
}
|
||||||
|
|
||||||
|
if float.is_nan() {
|
||||||
|
panic!("float must not be `NaN`");
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(float)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the underlying [`f32`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Float;
|
||||||
|
/// assert_eq!(Float::new(1.1_f32).as_f32(), 1.1_f32);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_f32(self) -> f32 { self.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Float {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let float = f32::from_str(input).map_err(|e| Error::parse_float(input, e))?;
|
||||||
|
Self::try_from(float)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<f32> for Float {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(float: f32) -> Result<Self, Self::Error> {
|
||||||
|
if float.is_infinite() {
|
||||||
|
return Err(Error::custom(format!("float must be finite: `{}`", float)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if float.is_nan() {
|
||||||
|
return Err(Error::custom("float must not be `NaN`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(float))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! implement_from {
|
||||||
|
( $( $type:tt ),+ ) => {
|
||||||
|
$(
|
||||||
|
impl ::core::convert::From<$type> for Float {
|
||||||
|
fn from(value: $type) -> Self {
|
||||||
|
Self(value as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implement_from!(i16, u16, i8, u8);
|
||||||
|
|
||||||
|
impl PartialEq for Float {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenience implementation to compare f32 with a Float.
|
||||||
|
impl PartialEq<f32> for Float {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &f32) -> bool { &self.0 == other }
|
||||||
|
}
|
||||||
|
|
||||||
|
// In order to implement `Eq` a struct has to satisfy
|
||||||
|
// the following requirements:
|
||||||
|
// - reflexive: a == a;
|
||||||
|
// - symmetric: a == b implies b == a; and
|
||||||
|
// - transitive: a == b and b == c implies a == c.
|
||||||
|
//
|
||||||
|
// The symmetric and transitive parts are already satisfied
|
||||||
|
// through `PartialEq`. The reflexive part is not satisfied for f32,
|
||||||
|
// because `f32::NAN` never equals `f32::NAN`. (`assert!(f32::NAN, f32::NAN)`)
|
||||||
|
//
|
||||||
|
// It is ensured, that this struct can not be constructed
|
||||||
|
// with NaN so all of the above requirements are satisfied and therefore Eq can
|
||||||
|
// be soundly implemented.
|
||||||
|
impl Eq for Float {}
|
||||||
|
|
||||||
|
impl Ord for Float {
|
||||||
|
#[inline]
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
if self.0 < other.0 {
|
||||||
|
Ordering::Less
|
||||||
|
} else if self == other {
|
||||||
|
Ordering::Equal
|
||||||
|
} else {
|
||||||
|
Ordering::Greater
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The output of Hash cannot be relied upon to be stable. The same version of
|
||||||
|
/// rust can return different values in different architectures. This is not a
|
||||||
|
/// property of the Hasher that you’re using but instead of the way Hash happens
|
||||||
|
/// to be implemented for the type you’re using (e.g., the current
|
||||||
|
/// implementation of Hash for slices of integers returns different values in
|
||||||
|
/// big and little-endian architectures).
|
||||||
|
///
|
||||||
|
/// See <https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/33>
|
||||||
|
#[doc(hidden)]
|
||||||
|
impl ::core::hash::Hash for Float {
|
||||||
|
fn hash<H>(&self, state: &mut H)
|
||||||
|
where
|
||||||
|
H: ::core::hash::Hasher,
|
||||||
|
{
|
||||||
|
// this implementation assumes, that the internal float is:
|
||||||
|
// - not NaN
|
||||||
|
// - neither negative nor positive infinity
|
||||||
|
|
||||||
|
// to validate those assumptions debug_assertions are here
|
||||||
|
// (those will be removed in a release build)
|
||||||
|
debug_assert!(self.0.is_finite());
|
||||||
|
debug_assert!(!self.0.is_nan());
|
||||||
|
|
||||||
|
// this implementation is based on
|
||||||
|
// https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/33
|
||||||
|
//
|
||||||
|
// The important points are:
|
||||||
|
// - NaN == NaN (Float does not allow NaN, so this should be satisfied)
|
||||||
|
// - +0 == -0
|
||||||
|
|
||||||
|
if self.0 == 0.0 || self.0 == -0.0 {
|
||||||
|
state.write(&0.0_f32.to_be_bytes());
|
||||||
|
} else {
|
||||||
|
// I do not think it matters to differentiate between architectures, that use
|
||||||
|
// big endian by default and those, that use little endian.
|
||||||
|
state.write(&self.to_be_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use core::hash::{Hash, Hasher};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ord() {
|
||||||
|
assert_eq!(Float::new(1.1).cmp(&Float::new(1.1)), Ordering::Equal);
|
||||||
|
assert_eq!(Float::new(1.1).cmp(&Float::new(2.1)), Ordering::Less);
|
||||||
|
assert_eq!(Float::new(1.1).cmp(&Float::new(0.1)), Ordering::Greater);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_ord() {
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(1.1).partial_cmp(&Float::new(1.1)),
|
||||||
|
Some(Ordering::Equal)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(1.1).partial_cmp(&Float::new(2.1)),
|
||||||
|
Some(Ordering::Less)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(1.1).partial_cmp(&Float::new(0.1)),
|
||||||
|
Some(Ordering::Greater)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash() {
|
||||||
|
let mut hasher_left = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
let mut hasher_right = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(0.0).hash(&mut hasher_left),
|
||||||
|
Float::new(-0.0).hash(&mut hasher_right)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(hasher_left.finish(), hasher_right.finish());
|
||||||
|
|
||||||
|
let mut hasher_left = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
let mut hasher_right = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(1.0).hash(&mut hasher_left),
|
||||||
|
Float::new(1.0).hash(&mut hasher_right)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(hasher_left.finish(), hasher_right.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_eq() {
|
||||||
|
struct _AssertEq
|
||||||
|
where
|
||||||
|
Float: Eq;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_eq() {
|
||||||
|
assert_eq!(Float::new(1.0).eq(&Float::new(1.0)), true);
|
||||||
|
assert_eq!(Float::new(1.0).eq(&Float::new(33.3)), false);
|
||||||
|
assert_eq!(Float::new(1.1), 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(Float::new(22.0).to_string(), "22".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(3.14159265359).to_string(),
|
||||||
|
"3.1415927".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(-3.14159265359).to_string(),
|
||||||
|
"-3.1415927".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
assert_eq!(Float::new(22.0), Float::from_str("22").unwrap());
|
||||||
|
assert_eq!(Float::new(-22.0), Float::from_str("-22").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
Float::new(3.14159265359),
|
||||||
|
Float::from_str("3.14159265359").unwrap()
|
||||||
|
);
|
||||||
|
assert!(Float::from_str("1#").is_err());
|
||||||
|
assert!(Float::from_str("NaN").is_err());
|
||||||
|
assert!(Float::from_str("inf").is_err());
|
||||||
|
assert!(Float::from_str("-inf").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must be finite: `inf`"]
|
||||||
|
fn test_new_infinite() { let _ = Float::new(::core::f32::INFINITY); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must be finite: `-inf`"]
|
||||||
|
fn test_new_neg_infinite() { let _ = Float::new(::core::f32::NEG_INFINITY); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must not be `NaN`"]
|
||||||
|
fn test_new_nan() { let _ = Float::new(::core::f32::NAN); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_as_f32() {
|
||||||
|
assert_eq!(Float::new(1.1).as_f32(), 1.1_f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from() {
|
||||||
|
assert_eq!(Float::from(-1_i8), Float::new(-1.0));
|
||||||
|
assert_eq!(Float::from(1_u8), Float::new(1.0));
|
||||||
|
assert_eq!(Float::from(-1_i16), Float::new(-1.0));
|
||||||
|
assert_eq!(Float::from(1_u16), Float::new(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_try_from() {
|
||||||
|
assert_eq!(Float::try_from(1.1_f32).unwrap(), Float::new(1.1));
|
||||||
|
assert_eq!(Float::try_from(-1.1_f32).unwrap(), Float::new(-1.1));
|
||||||
|
|
||||||
|
assert!(Float::try_from(::core::f32::INFINITY).is_err());
|
||||||
|
assert!(Float::try_from(::core::f32::NAN).is_err());
|
||||||
|
assert!(Float::try_from(::core::f32::NEG_INFINITY).is_err());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,22 @@
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
/// HDCP level.
|
/// HDCP ([`High-bandwidth Digital Content Protection`]) level.
|
||||||
///
|
///
|
||||||
/// See: [4.3.4.2. EXT-X-STREAM-INF]
|
/// [`High-bandwidth Digital Content Protection`]:
|
||||||
///
|
/// https://www.digital-cp.com/sites/default/files/specifications/HDCP%20on%20HDMI%20Specification%20Rev2_2_Final1.pdf
|
||||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
#[non_exhaustive]
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||||
pub enum HdcpLevel {
|
pub enum HdcpLevel {
|
||||||
|
/// The associated [`VariantStream`] could fail to play unless the output is
|
||||||
|
/// protected by High-bandwidth Digital Content Protection ([`HDCP`]) Type 0
|
||||||
|
/// or equivalent.
|
||||||
|
///
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
/// [`HDCP`]: https://www.digital-cp.com/sites/default/files/specifications/HDCP%20on%20HDMI%20Specification%20Rev2_2_Final1.pdf
|
||||||
#[strum(serialize = "TYPE-0")]
|
#[strum(serialize = "TYPE-0")]
|
||||||
Type0,
|
Type0,
|
||||||
|
/// The content does not require output copy protection.
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,20 +27,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let level = HdcpLevel::Type0;
|
assert_eq!(HdcpLevel::Type0.to_string(), "TYPE-0".to_string());
|
||||||
assert_eq!(level.to_string(), "TYPE-0".to_string());
|
assert_eq!(HdcpLevel::None.to_string(), "NONE".to_string());
|
||||||
|
|
||||||
let level = HdcpLevel::None;
|
|
||||||
assert_eq!(level.to_string(), "NONE".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
let level = HdcpLevel::Type0;
|
assert_eq!(HdcpLevel::Type0, "TYPE-0".parse().unwrap());
|
||||||
assert_eq!(level, "TYPE-0".parse::<HdcpLevel>().unwrap());
|
assert_eq!(HdcpLevel::None, "NONE".parse().unwrap());
|
||||||
|
|
||||||
let level = HdcpLevel::None;
|
|
||||||
assert_eq!(level, "NONE".parse::<HdcpLevel>().unwrap());
|
|
||||||
|
|
||||||
assert!("unk".parse::<HdcpLevel>().is_err());
|
assert!("unk".parse::<HdcpLevel>().is_err());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
/// Identifier of a rendition within the segments in a media playlist.
|
use crate::traits::RequiredVersion;
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
|
||||||
|
/// Identifier of a rendition within the [`MediaSegment`]s in a
|
||||||
|
/// [`MediaPlaylist`].
|
||||||
///
|
///
|
||||||
/// See: [4.3.4.1. EXT-X-MEDIA]
|
/// The variants [`InStreamId::Cc1`], [`InStreamId::Cc2`], [`InStreamId::Cc3`],
|
||||||
|
/// and [`InStreamId::Cc4`] identify a Line 21 Data Services channel ([CEA608]).
|
||||||
///
|
///
|
||||||
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
/// The `Service` variants identify a Digital Television Closed Captioning
|
||||||
|
/// ([CEA708]) service block number. The `Service` variants range from
|
||||||
|
/// [`InStreamId::Service1`] to [`InStreamId::Service63`].
|
||||||
|
///
|
||||||
|
/// [CEA608]: https://tools.ietf.org/html/rfc8216#ref-CEA608
|
||||||
|
/// [CEA708]: https://tools.ietf.org/html/rfc8216#ref-CEA708
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
#[non_exhaustive]
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
|
||||||
#[strum(serialize_all = "UPPERCASE")]
|
#[strum(serialize_all = "UPPERCASE")]
|
||||||
|
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||||
pub enum InStreamId {
|
pub enum InStreamId {
|
||||||
Cc1,
|
Cc1,
|
||||||
Cc2,
|
Cc2,
|
||||||
|
@ -78,6 +91,18 @@ pub enum InStreamId {
|
||||||
Service63,
|
Service63,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The variants [`InStreamId::Cc1`], [`InStreamId::Cc2`], [`InStreamId::Cc3`]
|
||||||
|
/// and [`InStreamId::Cc4`] require [`ProtocolVersion::V1`], the other
|
||||||
|
/// [`ProtocolVersion::V7`].
|
||||||
|
impl RequiredVersion for InStreamId {
|
||||||
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
|
match &self {
|
||||||
|
Self::Cc1 | Self::Cc2 | Self::Cc3 | Self::Cc4 => ProtocolVersion::V1,
|
||||||
|
_ => ProtocolVersion::V7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,42 +1,187 @@
|
||||||
use std::fmt;
|
use core::fmt;
|
||||||
use std::ops::Deref;
|
use core::str::FromStr;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// Initialization vector.
|
/// An initialization vector (IV) is a fixed size input that can be used along
|
||||||
|
/// with a secret key for data encryption.
|
||||||
///
|
///
|
||||||
/// See: [4.3.2.4. EXT-X-KEY]
|
/// The use of an IV prevents repetition in encrypted data, making it more
|
||||||
///
|
/// difficult for a hacker using a dictionary attack to find patterns and break
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
/// a cipher. For example, a sequence might appear twice or more within the body
|
||||||
|
/// of a message. If there are repeated sequences in encrypted data, an attacker
|
||||||
|
/// could assume that the corresponding sequences in the message were also
|
||||||
|
/// identical. The IV prevents the appearance of corresponding duplicate
|
||||||
|
/// character sequences in the ciphertext.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct InitializationVector(pub [u8; 16]);
|
#[non_exhaustive]
|
||||||
|
pub enum InitializationVector {
|
||||||
|
/// An IV for use with Aes128.
|
||||||
|
Aes128([u8; 0x10]),
|
||||||
|
/// An [`ExtXKey`] tag with [`KeyFormat::Identity`] that does not have an IV
|
||||||
|
/// field indicates that the [`MediaSegment::number`] is to be used as the
|
||||||
|
/// IV when decrypting a `MediaSegment`.
|
||||||
|
///
|
||||||
|
/// [`ExtXKey`]: crate::tags::ExtXKey
|
||||||
|
/// [`KeyFormat::Identity`]: crate::types::KeyFormat::Identity
|
||||||
|
/// [`MediaSegment::number`]: crate::MediaSegment::number
|
||||||
|
Number(u128),
|
||||||
|
/// Signals that an IV is missing.
|
||||||
|
Missing,
|
||||||
|
}
|
||||||
|
|
||||||
impl InitializationVector {
|
impl InitializationVector {
|
||||||
/// Converts the [InitializationVector] to a slice.
|
/// Returns the IV as an [`u128`]. `None` is returned for
|
||||||
pub const fn to_slice(&self) -> [u8; 16] { self.0 }
|
/// [`InitializationVector::Missing`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::InitializationVector;
|
||||||
|
/// assert_eq!(
|
||||||
|
/// InitializationVector::Aes128([
|
||||||
|
/// 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78,
|
||||||
|
/// 0x90, 0x12
|
||||||
|
/// ])
|
||||||
|
/// .to_u128(),
|
||||||
|
/// Some(0x12345678901234567890123456789012)
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(InitializationVector::Number(0x10).to_u128(), Some(0x10));
|
||||||
|
///
|
||||||
|
/// assert_eq!(InitializationVector::Missing.to_u128(), None);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_u128(&self) -> Option<u128> {
|
||||||
|
match *self {
|
||||||
|
Self::Aes128(v) => Some(u128::from_be_bytes(v)),
|
||||||
|
Self::Number(n) => Some(n),
|
||||||
|
Self::Missing => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the IV as a slice, which can be used to for example decrypt
|
||||||
|
/// a [`MediaSegment`]. `None` is returned for
|
||||||
|
/// [`InitializationVector::Missing`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::InitializationVector;
|
||||||
|
/// assert_eq!(
|
||||||
|
/// InitializationVector::Aes128([
|
||||||
|
/// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||||
|
/// 0x0F, 0x10,
|
||||||
|
/// ])
|
||||||
|
/// .to_slice(),
|
||||||
|
/// Some([
|
||||||
|
/// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||||
|
/// 0x0F, 0x10,
|
||||||
|
/// ])
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// InitializationVector::Number(0x12345678901234567890123456789012).to_slice(),
|
||||||
|
/// Some([
|
||||||
|
/// 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78,
|
||||||
|
/// 0x90, 0x12
|
||||||
|
/// ])
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(InitializationVector::Missing.to_slice(), None);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_slice(&self) -> Option<[u8; 0x10]> {
|
||||||
|
match &self {
|
||||||
|
Self::Aes128(v) => Some(*v),
|
||||||
|
Self::Number(v) => Some(v.to_be_bytes()),
|
||||||
|
Self::Missing => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the initialization vector is not missing.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::InitializationVector;
|
||||||
|
/// assert_eq!(
|
||||||
|
/// InitializationVector::Aes128([
|
||||||
|
/// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||||
|
/// 0x0F, 0x10,
|
||||||
|
/// ])
|
||||||
|
/// .is_some(),
|
||||||
|
/// true
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(InitializationVector::Number(4).is_some(), true);
|
||||||
|
///
|
||||||
|
/// assert_eq!(InitializationVector::Missing.is_some(), false);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub fn is_some(&self) -> bool { *self != Self::Missing }
|
||||||
|
|
||||||
|
/// Returns `true` if the initialization vector is missing.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::InitializationVector;
|
||||||
|
/// assert_eq!(
|
||||||
|
/// InitializationVector::Aes128([
|
||||||
|
/// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||||
|
/// 0x0F, 0x10,
|
||||||
|
/// ])
|
||||||
|
/// .is_none(),
|
||||||
|
/// false
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(InitializationVector::Number(4).is_none(), false);
|
||||||
|
///
|
||||||
|
/// assert_eq!(InitializationVector::Missing.is_none(), true);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub fn is_none(&self) -> bool { *self == Self::Missing }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<[u8; 16]> for InitializationVector {
|
impl Default for InitializationVector {
|
||||||
fn from(value: [u8; 16]) -> Self { Self(value) }
|
fn default() -> Self { Self::Missing }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for InitializationVector {
|
impl From<[u8; 0x10]> for InitializationVector {
|
||||||
type Target = [u8];
|
fn from(value: [u8; 0x10]) -> Self { Self::Aes128(value) }
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<[u8]> for InitializationVector {
|
impl From<Option<[u8; 0x10]>> for InitializationVector {
|
||||||
fn as_ref(&self) -> &[u8] { &self.0 }
|
fn from(value: Option<[u8; 0x10]>) -> Self {
|
||||||
|
match value {
|
||||||
|
Some(v) => Self::Aes128(v),
|
||||||
|
None => Self::Missing,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for InitializationVector {
|
impl fmt::Display for InitializationVector {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "0x")?;
|
match &self {
|
||||||
for b in &self.0 {
|
Self::Aes128(buffer) => {
|
||||||
write!(f, "{:02x}", b)?;
|
let mut result = [0; 0x10 * 2];
|
||||||
|
::hex::encode_to_slice(buffer, &mut result).unwrap();
|
||||||
|
|
||||||
|
write!(f, "0x{}", ::core::str::from_utf8(&result).unwrap())?;
|
||||||
|
}
|
||||||
|
Self::Number(num) => {
|
||||||
|
write!(f, "InitializationVector::Number({})", num)?;
|
||||||
|
}
|
||||||
|
Self::Missing => {
|
||||||
|
write!(f, "InitializationVector::Missing")?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,20 +191,20 @@ impl FromStr for InitializationVector {
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
if !(input.starts_with("0x") || input.starts_with("0X")) {
|
if !(input.starts_with("0x") || input.starts_with("0X")) {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::custom("An IV should either start with `0x` or `0X`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.len() - 2 != 32 {
|
if input.len() - 2 != 32 {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::custom(
|
||||||
|
"An IV must be 32 bytes long + 2 bytes for 0x/0X",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result = [0; 16];
|
let mut result = [0; 16];
|
||||||
for (i, c) in input.as_bytes().chunks(2).skip(1).enumerate() {
|
|
||||||
let d = std::str::from_utf8(c).map_err(Error::custom)?;
|
|
||||||
let b = u8::from_str_radix(d, 16).map_err(Error::custom)?;
|
|
||||||
result[i] = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self(result))
|
::hex::decode_to_slice(&input.as_bytes()[2..], &mut result).map_err(Error::hex)?;
|
||||||
|
|
||||||
|
Ok(Self::Aes128(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,82 +214,92 @@ mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_default() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"0x10ef8f758ca555115584bb5b3c687f52".to_string(),
|
InitializationVector::default(),
|
||||||
InitializationVector([
|
InitializationVector::Missing
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
|
||||||
])
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
assert_eq!(
|
|
||||||
"0x10ef8f758ca555115584bb5b3c687f52"
|
|
||||||
.parse::<InitializationVector>()
|
|
||||||
.unwrap(),
|
|
||||||
InitializationVector([
|
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"0X10ef8f758ca555115584bb5b3c687f52"
|
|
||||||
.parse::<InitializationVector>()
|
|
||||||
.unwrap(),
|
|
||||||
InitializationVector([
|
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"0X10EF8F758CA555115584BB5B3C687F52"
|
|
||||||
.parse::<InitializationVector>()
|
|
||||||
.unwrap(),
|
|
||||||
InitializationVector([
|
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("garbage".parse::<InitializationVector>().is_err());
|
|
||||||
assert!("0xgarbage".parse::<InitializationVector>().is_err());
|
|
||||||
assert!("0x12".parse::<InitializationVector>().is_err());
|
|
||||||
assert!("0X10EF8F758CA555115584BB5B3C687F5Z"
|
|
||||||
.parse::<InitializationVector>()
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_ref() {
|
|
||||||
assert_eq!(
|
|
||||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).as_ref(),
|
|
||||||
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref() {
|
|
||||||
assert_eq!(
|
|
||||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).deref(),
|
|
||||||
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from() {
|
fn test_from() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
InitializationVector::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
InitializationVector::from([
|
||||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
]),
|
||||||
|
InitializationVector::Aes128([
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
InitializationVector::from(None),
|
||||||
|
InitializationVector::Missing
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
InitializationVector::from(Some([
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
])),
|
||||||
|
InitializationVector::Aes128([
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(
|
||||||
|
InitializationVector::Aes128([
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
])
|
||||||
|
.to_string(),
|
||||||
|
"0xffffffffffffffffffffffffffffffff".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
InitializationVector::Number(5).to_string(),
|
||||||
|
"InitializationVector::Number(5)".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
InitializationVector::Missing.to_string(),
|
||||||
|
"InitializationVector::Missing".to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_slice() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).to_slice(),
|
InitializationVector::Aes128([
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
]),
|
||||||
|
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".parse().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
InitializationVector::Aes128([
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
]),
|
||||||
|
"0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".parse().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// missing `0x` at the start:
|
||||||
|
assert!("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
|
||||||
|
.parse::<InitializationVector>()
|
||||||
|
.is_err());
|
||||||
|
// too small:
|
||||||
|
assert!("0xFF".parse::<InitializationVector>().is_err());
|
||||||
|
// too large:
|
||||||
|
assert!("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
|
||||||
|
.parse::<InitializationVector>()
|
||||||
|
.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,16 @@ use crate::types::ProtocolVersion;
|
||||||
use crate::utils::{quote, tag, unquote};
|
use crate::utils::{quote, tag, unquote};
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
|
/// Specifies how the key is represented in the resource identified by the
|
||||||
|
/// `URI`.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
/// [`KeyFormat`] specifies, how the key is represented in the
|
|
||||||
/// resource identified by the `URI`.
|
|
||||||
pub enum KeyFormat {
|
pub enum KeyFormat {
|
||||||
/// The key is a single packed array of 16 octets in binary format.
|
/// An [`EncryptionMethod::Aes128`] uses 16-octet (16 byte/128 bit) keys. If
|
||||||
|
/// the format is [`KeyFormat::Identity`], the key file is a single packed
|
||||||
|
/// array of 16 octets (16 byte/128 bit) in binary format.
|
||||||
|
///
|
||||||
|
/// [`EncryptionMethod::Aes128`]: crate::types::EncryptionMethod::Aes128
|
||||||
Identity,
|
Identity,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +33,7 @@ impl FromStr for KeyFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for KeyFormat {
|
impl fmt::Display for KeyFormat {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote("identity")) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", quote(&"identity")) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V5`].
|
/// This tag requires [`ProtocolVersion::V5`].
|
||||||
|
|
|
@ -1,50 +1,363 @@
|
||||||
use std::convert::Infallible;
|
use std::cmp::Ordering;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::iter::{Extend, FromIterator};
|
||||||
|
use std::ops::{Index, IndexMut};
|
||||||
|
use std::slice::SliceIndex;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::{quote, unquote};
|
use crate::utils::{quote, unquote};
|
||||||
|
use crate::Error;
|
||||||
use crate::RequiredVersion;
|
use crate::RequiredVersion;
|
||||||
|
|
||||||
/// A list of [`usize`], that can be used to indicate which version(s)
|
/// A list of numbers that can be used to indicate which version(s)
|
||||||
/// this instance complies with, if more than one version of a particular
|
/// this instance complies with, if more than one version of a particular
|
||||||
/// [`KeyFormat`] is defined.
|
/// [`KeyFormat`] is defined.
|
||||||
///
|
///
|
||||||
|
/// ## Note on maximum size
|
||||||
|
///
|
||||||
|
/// To reduce the memory usage and to make this struct implement [`Copy`], a
|
||||||
|
/// fixed size array is used internally (`[u8; 9]`), which can store a maximum
|
||||||
|
/// number of 9 `u8` numbers.
|
||||||
|
///
|
||||||
|
/// If you encounter any m3u8 file, which fails to parse, because the buffer is
|
||||||
|
/// too small, feel free to [make an issue](https://github.com/sile/hls_m3u8/issues).
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// KeyFormatVersions::from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).to_string(),
|
||||||
|
/// "\"255/255/255/255/255/255/255/255/255\"".to_string()
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// [`KeyFormat`]: crate::types::KeyFormat
|
/// [`KeyFormat`]: crate::types::KeyFormat
|
||||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct KeyFormatVersions(Vec<usize>);
|
pub struct KeyFormatVersions {
|
||||||
|
// NOTE(Luro02): if the current array is not big enough one can easily increase
|
||||||
|
// the number of elements or change the type to something bigger,
|
||||||
|
// but it would be kinda wasteful to use a `Vec` here, which requires
|
||||||
|
// allocations and has a size of at least 24 bytes
|
||||||
|
// (::std::mem::size_of::<Vec<u8>>() = 24).
|
||||||
|
buffer: [u8; 9],
|
||||||
|
// Indicates the number of used items in the array.
|
||||||
|
len: u8,
|
||||||
|
}
|
||||||
|
|
||||||
impl KeyFormatVersions {
|
impl KeyFormatVersions {
|
||||||
/// Makes a new [`KeyFormatVersions`].
|
/// Constructs an empty [`KeyFormatVersions`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// assert_eq!(versions, KeyFormatVersions::default());
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self { Self::default() }
|
pub fn new() -> Self { Self::default() }
|
||||||
|
|
||||||
/// Add a value to the [`KeyFormatVersions`].
|
/// Add a value to the end of [`KeyFormatVersions`].
|
||||||
pub fn push(&mut self, value: usize) {
|
///
|
||||||
if self.is_default() {
|
/// # Panics
|
||||||
self.0 = vec![value];
|
///
|
||||||
|
/// This function panics, if you try to push more elements, than
|
||||||
|
/// [`KeyFormatVersions::remaining`] returns.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// versions.push(1);
|
||||||
|
/// assert_eq!(versions, KeyFormatVersions::from([1]));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This will panic, because it exceeded the maximum number of elements:
|
||||||
|
///
|
||||||
|
/// ```{.should_panic}
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// for _ in 0..=versions.capacity() {
|
||||||
|
/// versions.push(1); // <- panics
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn push(&mut self, value: u8) {
|
||||||
|
if self.len as usize == self.buffer.len() {
|
||||||
|
panic!("reached maximum number of elements in KeyFormatVersions");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer[self.len()] = value;
|
||||||
|
self.len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `KeyFormatVersions` has a limited capacity and this function returns how
|
||||||
|
/// many elements can be pushed, until it panics.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// assert_eq!(versions.remaining(), versions.capacity());
|
||||||
|
///
|
||||||
|
/// versions.push(1);
|
||||||
|
/// versions.push(2);
|
||||||
|
/// versions.push(3);
|
||||||
|
/// assert_eq!(versions.remaining(), 6);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn remaining(&self) -> usize { self.capacity().saturating_sub(self.len()) }
|
||||||
|
|
||||||
|
/// Returns the number of elements.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// assert_eq!(versions.len(), 0);
|
||||||
|
///
|
||||||
|
/// versions.push(2);
|
||||||
|
/// assert_eq!(versions.len(), 1);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn len(&self) -> usize { self.len as usize }
|
||||||
|
|
||||||
|
/// Returns the total number of elements that can be stored.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// It should not be relied on that this function will always return 9. In
|
||||||
|
/// the future this number might increase.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn capacity(&self) -> usize { self.buffer.len() }
|
||||||
|
|
||||||
|
/// Shortens the internal array to the provided length.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// If `len` is greater than the current length, this has no effect.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::from([1, 2, 3, 4, 5, 6]);
|
||||||
|
/// versions.truncate(3);
|
||||||
|
///
|
||||||
|
/// assert_eq!(versions, KeyFormatVersions::from([1, 2, 3]));
|
||||||
|
/// ```
|
||||||
|
pub fn truncate(&mut self, len: usize) {
|
||||||
|
if len > self.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.len = len as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if there are no elements.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// assert_eq!(versions.is_empty(), true);
|
||||||
|
///
|
||||||
|
/// versions.push(2);
|
||||||
|
/// assert_eq!(versions.is_empty(), false);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_empty(&self) -> bool { self.len() == 0 }
|
||||||
|
|
||||||
|
/// Removes the last element and returns it, or `None` if it is empty.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// assert_eq!(versions.pop(), None);
|
||||||
|
///
|
||||||
|
/// versions.push(2);
|
||||||
|
/// assert_eq!(versions.pop(), Some(2));
|
||||||
|
/// assert_eq!(versions.is_empty(), true);
|
||||||
|
/// ```
|
||||||
|
pub fn pop(&mut self) -> Option<u8> {
|
||||||
|
if self.is_empty() {
|
||||||
|
None
|
||||||
} else {
|
} else {
|
||||||
self.0.push(value);
|
self.len -= 1;
|
||||||
|
Some(self.buffer[self.len()])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true`, if [`KeyFormatVersions`] has the default value of
|
/// Returns `true`, if it is either empty or has a length of 1 and the first
|
||||||
/// `vec![1]`.
|
/// element is 1.
|
||||||
pub fn is_default(&self) -> bool { self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() }
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::KeyFormatVersions;
|
||||||
|
/// let mut versions = KeyFormatVersions::new();
|
||||||
|
///
|
||||||
|
/// assert_eq!(versions.is_default(), true);
|
||||||
|
///
|
||||||
|
/// versions.push(1);
|
||||||
|
/// assert_eq!(versions.is_default(), true);
|
||||||
|
///
|
||||||
|
/// assert_eq!(KeyFormatVersions::default().is_default(), true);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_default(&self) -> bool {
|
||||||
|
self.is_empty() || (self.buffer[self.len().saturating_sub(1)] == 1 && self.len() == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for KeyFormatVersions {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
if self.len() == other.len() {
|
||||||
|
// only compare the parts in the buffer, that are used:
|
||||||
|
self.as_ref() == self.as_ref()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for KeyFormatVersions {}
|
||||||
|
|
||||||
|
impl PartialOrd for KeyFormatVersions {
|
||||||
|
#[inline]
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(<Self as Ord>::cmp(self, other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for KeyFormatVersions {
|
||||||
|
#[inline]
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering { self.as_ref().cmp(other.as_ref()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for KeyFormatVersions {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
state.write_usize(self.len());
|
||||||
|
self.as_ref().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for KeyFormatVersions {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
fn as_ref(&self) -> &[u8] { &self.buffer[..self.len()] }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsMut<[u8]> for KeyFormatVersions {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
fn as_mut(&mut self) -> &mut [u8] {
|
||||||
|
// this temporary variable is required, because the compiler does not resolve
|
||||||
|
// the borrow to it's value immediately, so there is a shared borrow and
|
||||||
|
// therefore no exclusive borrow can be made.
|
||||||
|
let len = self.len();
|
||||||
|
&mut self.buffer[..len]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extend<u8> for KeyFormatVersions {
|
||||||
|
fn extend<I: IntoIterator<Item = u8>>(&mut self, iter: I) {
|
||||||
|
for element in iter {
|
||||||
|
if self.remaining() == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Extend<&'a u8> for KeyFormatVersions {
|
||||||
|
fn extend<I: IntoIterator<Item = &'a u8>>(&mut self, iter: I) {
|
||||||
|
<Self as Extend<u8>>::extend(self, iter.into_iter().copied())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: SliceIndex<[u8]>> Index<I> for KeyFormatVersions {
|
||||||
|
type Output = I::Output;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn index(&self, index: I) -> &Self::Output { self.as_ref().index(index) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: SliceIndex<[u8]>> IndexMut<I> for KeyFormatVersions {
|
||||||
|
#[inline]
|
||||||
|
fn index_mut(&mut self, index: I) -> &mut Self::Output { self.as_mut().index_mut(index) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for KeyFormatVersions {
|
||||||
|
type IntoIter = IntoIter<u8>;
|
||||||
|
type Item = u8;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter { self.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<u8> for KeyFormatVersions {
|
||||||
|
fn from_iter<I: IntoIterator<Item = u8>>(iter: I) -> Self {
|
||||||
|
let mut result = Self::default();
|
||||||
|
// an array like [0; 9] as empty
|
||||||
|
let mut is_empty = true;
|
||||||
|
|
||||||
|
for item in iter {
|
||||||
|
if item != 0 {
|
||||||
|
is_empty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.remaining() == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_empty {
|
||||||
|
return Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FromIterator<&'a u8> for KeyFormatVersions {
|
||||||
|
fn from_iter<I: IntoIterator<Item = &'a u8>>(iter: I) -> Self {
|
||||||
|
<Self as FromIterator<u8>>::from_iter(iter.into_iter().copied())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for KeyFormatVersions {
|
impl Default for KeyFormatVersions {
|
||||||
fn default() -> Self { Self(vec![1]) }
|
#[inline]
|
||||||
}
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
impl Deref for KeyFormatVersions {
|
buffer: [0; 9],
|
||||||
type Target = Vec<usize>;
|
len: 0,
|
||||||
|
}
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for KeyFormatVersions {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V5`].
|
/// This tag requires [`ProtocolVersion::V5`].
|
||||||
|
@ -53,45 +366,101 @@ impl RequiredVersion for KeyFormatVersions {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for KeyFormatVersions {
|
impl FromStr for KeyFormatVersions {
|
||||||
type Err = Infallible;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let mut result = unquote(input)
|
let mut result = Self::default();
|
||||||
|
|
||||||
|
for item in unquote(input)
|
||||||
.split('/')
|
.split('/')
|
||||||
.filter_map(|v| v.parse().ok())
|
.map(|v| v.parse().map_err(|e| Error::parse_int(v, e)))
|
||||||
.collect::<Vec<_>>();
|
{
|
||||||
|
let item = item?;
|
||||||
|
|
||||||
|
if result.remaining() == 0 {
|
||||||
|
return Err(Error::custom(
|
||||||
|
"reached maximum number of elements in KeyFormatVersions",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
if result.is_empty() {
|
if result.is_empty() {
|
||||||
result.push(1);
|
result.push(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(result))
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for KeyFormatVersions {
|
impl fmt::Display for KeyFormatVersions {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if self.is_default() {
|
if self.is_default() || self.is_empty() {
|
||||||
return write!(f, "{}", quote("1"));
|
return write!(f, "{}", quote("1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(
|
write!(f, "\"{}", self.buffer[0])?;
|
||||||
f,
|
|
||||||
"{}",
|
for item in &self.buffer[1..self.len()] {
|
||||||
quote(
|
write!(f, "/{}", item)?;
|
||||||
// vec![1, 2, 3] -> "1/2/3"
|
}
|
||||||
self.0
|
|
||||||
.iter()
|
write!(f, "\"")?;
|
||||||
.map(|v| v.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
Ok(())
|
||||||
.join("/")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Into<Vec<usize>>> From<T> for KeyFormatVersions {
|
impl<T: AsRef<[usize]>> From<T> for KeyFormatVersions {
|
||||||
fn from(value: T) -> Self { Self(value.into()) }
|
fn from(value: T) -> Self { Self::from_iter(value.as_ref().iter().map(|i| *i as u8)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Iterator` for [`KeyFormatVersions`].
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct IntoIter<T> {
|
||||||
|
buffer: [T; 9],
|
||||||
|
position: usize,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KeyFormatVersions> for IntoIter<u8> {
|
||||||
|
fn from(value: KeyFormatVersions) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer: value.buffer,
|
||||||
|
position: 0,
|
||||||
|
len: value.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a KeyFormatVersions> for IntoIter<u8> {
|
||||||
|
fn from(value: &'a KeyFormatVersions) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer: value.buffer,
|
||||||
|
position: 0,
|
||||||
|
len: value.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Copy> ExactSizeIterator for IntoIter<T> {
|
||||||
|
fn len(&self) -> usize { self.len.saturating_sub(self.position) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Copy> ::core::iter::FusedIterator for IntoIter<T> {}
|
||||||
|
|
||||||
|
impl<T: Copy> Iterator for IntoIter<T> {
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.position == self.len {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.position += 1;
|
||||||
|
Some(self.buffer[self.position - 1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -99,28 +468,201 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash() {
|
||||||
|
let mut hasher_left = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
let mut hasher_right = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
KeyFormatVersions::from([1, 2, 3]).hash(&mut hasher_left),
|
||||||
|
KeyFormatVersions::from([1, 2, 3]).hash(&mut hasher_right)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(hasher_left.finish(), hasher_right.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ord() {
|
||||||
|
assert_eq!(
|
||||||
|
KeyFormatVersions::from([1, 2]).cmp(&KeyFormatVersions::from([1, 2])),
|
||||||
|
Ordering::Equal
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
KeyFormatVersions::from([2]).cmp(&KeyFormatVersions::from([1, 2])),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
KeyFormatVersions::from([2, 3]).cmp(&KeyFormatVersions::from([1, 2])),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
KeyFormatVersions::from([]).cmp(&KeyFormatVersions::from([1, 2])),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_eq() {
|
||||||
|
let mut versions = KeyFormatVersions::from([1, 2, 3, 4, 5, 6]);
|
||||||
|
versions.truncate(3);
|
||||||
|
|
||||||
|
assert_eq!(versions, KeyFormatVersions::from([1, 2, 3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_as_ref() {
|
||||||
|
assert_eq!(KeyFormatVersions::new().as_ref(), &[]);
|
||||||
|
assert_eq!(KeyFormatVersions::from([1, 2, 3]).as_ref(), &[1, 2, 3]);
|
||||||
|
assert_eq!(KeyFormatVersions::from([]).as_ref(), &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_as_mut() {
|
||||||
|
assert_eq!(KeyFormatVersions::new().as_mut(), &mut []);
|
||||||
|
assert_eq!(KeyFormatVersions::from([1, 2, 3]).as_mut(), &mut [1, 2, 3]);
|
||||||
|
assert_eq!(KeyFormatVersions::from([]).as_mut(), &mut []);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_index() {
|
||||||
|
// test index
|
||||||
|
assert_eq!(&KeyFormatVersions::new()[..], &[]);
|
||||||
|
assert_eq!(&KeyFormatVersions::from([1, 2, 3])[..2], &[1, 2]);
|
||||||
|
assert_eq!(&KeyFormatVersions::from([1, 2, 3])[1..2], &[2]);
|
||||||
|
assert_eq!(&KeyFormatVersions::from([1, 2, 3])[..], &[1, 2, 3]);
|
||||||
|
|
||||||
|
// test index_mut
|
||||||
|
assert_eq!(&mut KeyFormatVersions::new()[..], &mut []);
|
||||||
|
assert_eq!(&mut KeyFormatVersions::from([1, 2, 3])[..2], &mut [1, 2]);
|
||||||
|
assert_eq!(&mut KeyFormatVersions::from([1, 2, 3])[1..2], &mut [2]);
|
||||||
|
assert_eq!(&mut KeyFormatVersions::from([1, 2, 3])[..], &mut [1, 2, 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extend() {
|
||||||
|
let mut versions = KeyFormatVersions::new();
|
||||||
|
versions.extend(&[1, 2, 3]);
|
||||||
|
|
||||||
|
assert_eq!(versions, KeyFormatVersions::from([1, 2, 3]));
|
||||||
|
|
||||||
|
versions.extend(&[1, 2, 3]);
|
||||||
|
assert_eq!(versions, KeyFormatVersions::from([1, 2, 3, 1, 2, 3]));
|
||||||
|
|
||||||
|
versions.extend(&[1, 2, 3, 4]);
|
||||||
|
assert_eq!(
|
||||||
|
versions,
|
||||||
|
KeyFormatVersions::from([1, 2, 3, 1, 2, 3, 1, 2, 3])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default() {
|
||||||
|
assert_eq!(KeyFormatVersions::default(), KeyFormatVersions::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into_iter() {
|
||||||
|
assert_eq!(KeyFormatVersions::new().into_iter().next(), None);
|
||||||
|
assert_eq!(KeyFormatVersions::new().into_iter().len(), 0);
|
||||||
|
|
||||||
|
let mut iterator = KeyFormatVersions::from([1, 2, 3, 4, 5]).into_iter();
|
||||||
|
|
||||||
|
assert_eq!(iterator.len(), 5);
|
||||||
|
assert_eq!(iterator.next(), Some(1));
|
||||||
|
|
||||||
|
assert_eq!(iterator.len(), 4);
|
||||||
|
assert_eq!(iterator.next(), Some(2));
|
||||||
|
|
||||||
|
assert_eq!(iterator.len(), 3);
|
||||||
|
assert_eq!(iterator.next(), Some(3));
|
||||||
|
|
||||||
|
assert_eq!(iterator.len(), 2);
|
||||||
|
assert_eq!(iterator.next(), Some(4));
|
||||||
|
|
||||||
|
assert_eq!(iterator.len(), 1);
|
||||||
|
assert_eq!(iterator.next(), Some(5));
|
||||||
|
|
||||||
|
assert_eq!(iterator.len(), 0);
|
||||||
|
assert_eq!(iterator.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_iter() {
|
||||||
|
assert_eq!(
|
||||||
|
{
|
||||||
|
let mut result = KeyFormatVersions::new();
|
||||||
|
result.push(1);
|
||||||
|
result.push(2);
|
||||||
|
result.push(3);
|
||||||
|
result.push(4);
|
||||||
|
result
|
||||||
|
},
|
||||||
|
KeyFormatVersions::from_iter(&[1, 2, 3, 4])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
{
|
||||||
|
let mut result = KeyFormatVersions::new();
|
||||||
|
result.push(0);
|
||||||
|
result.push(1);
|
||||||
|
result.push(2);
|
||||||
|
result.push(3);
|
||||||
|
result.push(4);
|
||||||
|
result
|
||||||
|
},
|
||||||
|
KeyFormatVersions::from_iter(&[0, 1, 2, 3, 4])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(KeyFormatVersions::new(), KeyFormatVersions::from_iter(&[]));
|
||||||
|
|
||||||
|
assert_eq!(KeyFormatVersions::new(), KeyFormatVersions::from_iter(&[0]));
|
||||||
|
assert_eq!(
|
||||||
|
KeyFormatVersions::new(),
|
||||||
|
KeyFormatVersions::from_iter(&[0, 0])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
{
|
||||||
|
let mut result = KeyFormatVersions::new();
|
||||||
|
result.push(0);
|
||||||
|
result.push(1);
|
||||||
|
result.push(2);
|
||||||
|
result.push(3);
|
||||||
|
result.push(4);
|
||||||
|
result.push(5);
|
||||||
|
result.push(6);
|
||||||
|
result.push(7);
|
||||||
|
result.push(8);
|
||||||
|
result
|
||||||
|
},
|
||||||
|
KeyFormatVersions::from_iter(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KeyFormatVersions::from(vec![1, 2, 3, 4, 5]).to_string(),
|
KeyFormatVersions::from([1, 2, 3, 4, 5]).to_string(),
|
||||||
quote("1/2/3/4/5")
|
quote("1/2/3/4/5")
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(KeyFormatVersions::from(vec![]).to_string(), quote("1"));
|
assert_eq!(KeyFormatVersions::from([]).to_string(), quote("1"));
|
||||||
|
|
||||||
assert_eq!(KeyFormatVersions::new().to_string(), quote("1"));
|
assert_eq!(KeyFormatVersions::new().to_string(), quote("1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KeyFormatVersions::from(vec![1, 2, 3, 4, 5]),
|
KeyFormatVersions::from([1, 2, 3, 4, 5]),
|
||||||
quote("1/2/3/4/5").parse().unwrap()
|
quote("1/2/3/4/5").parse().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(KeyFormatVersions::from(vec![1]), "1".parse().unwrap());
|
assert_eq!(KeyFormatVersions::from([1]), "1".parse().unwrap());
|
||||||
|
assert_eq!(KeyFormatVersions::from([1, 2]), "1/2".parse().unwrap());
|
||||||
|
|
||||||
assert_eq!(KeyFormatVersions::from(vec![1, 2]), "1/2".parse().unwrap());
|
assert!("1/b".parse::<KeyFormatVersions>().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -133,28 +675,20 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_default() {
|
fn test_is_default() {
|
||||||
assert!(KeyFormatVersions::new().is_default());
|
assert_eq!(KeyFormatVersions::new().is_default(), true);
|
||||||
assert!(KeyFormatVersions::from(vec![]).is_default());
|
assert_eq!(KeyFormatVersions::default().is_default(), true);
|
||||||
assert!(!KeyFormatVersions::from(vec![1, 2, 3]).is_default());
|
|
||||||
|
assert_eq!(KeyFormatVersions::from([]).is_default(), true);
|
||||||
|
assert_eq!(KeyFormatVersions::from([1]).is_default(), true);
|
||||||
|
|
||||||
|
assert_eq!(KeyFormatVersions::from([1, 2, 3]).is_default(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_push() {
|
fn test_push() {
|
||||||
let mut key_format_versions = KeyFormatVersions::from(vec![]);
|
let mut key_format_versions = KeyFormatVersions::new();
|
||||||
|
|
||||||
key_format_versions.push(2);
|
key_format_versions.push(2);
|
||||||
assert_eq!(KeyFormatVersions::from(vec![2]), key_format_versions);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
assert_eq!(KeyFormatVersions::from([2]), key_format_versions);
|
||||||
fn test_deref() {
|
|
||||||
assert!(!KeyFormatVersions::new().is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref_mut() {
|
|
||||||
let mut key_format_versions = KeyFormatVersions::from(vec![1, 2, 3]);
|
|
||||||
key_format_versions.pop();
|
|
||||||
assert_eq!(key_format_versions, KeyFormatVersions::from(vec![1, 2]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
/// Specifies the media type.
|
/// Specifies the media type.
|
||||||
|
#[non_exhaustive]
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Ord, PartialOrd, Display, EnumString, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Ord, PartialOrd, Display, EnumString, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||||
|
|
|
@ -1,28 +1,30 @@
|
||||||
//! Miscellaneous types.
|
//! Miscellaneous types.
|
||||||
mod byte_range;
|
pub(crate) mod byte_range;
|
||||||
mod channels;
|
pub(crate) mod channels;
|
||||||
mod closed_captions;
|
pub(crate) mod closed_captions;
|
||||||
mod decimal_floating_point;
|
pub(crate) mod codecs;
|
||||||
mod decimal_resolution;
|
pub(crate) mod decryption_key;
|
||||||
mod decryption_key;
|
pub(crate) mod encryption_method;
|
||||||
mod encryption_method;
|
pub(crate) mod hdcp_level;
|
||||||
mod hdcp_level;
|
pub(crate) mod in_stream_id;
|
||||||
mod in_stream_id;
|
pub(crate) mod initialization_vector;
|
||||||
mod initialization_vector;
|
pub(crate) mod key_format;
|
||||||
mod key_format;
|
pub(crate) mod key_format_versions;
|
||||||
mod key_format_versions;
|
pub(crate) mod media_type;
|
||||||
mod media_type;
|
pub(crate) mod playlist_type;
|
||||||
mod protocol_version;
|
pub(crate) mod protocol_version;
|
||||||
mod signed_decimal_floating_point;
|
pub(crate) mod resolution;
|
||||||
mod stream_inf;
|
pub(crate) mod stream_data;
|
||||||
mod value;
|
pub(crate) mod value;
|
||||||
|
|
||||||
|
pub(crate) mod float;
|
||||||
|
pub(crate) mod ufloat;
|
||||||
|
|
||||||
pub use byte_range::*;
|
pub use byte_range::*;
|
||||||
pub use channels::*;
|
pub use channels::*;
|
||||||
pub use closed_captions::*;
|
pub use closed_captions::*;
|
||||||
pub(crate) use decimal_floating_point::*;
|
pub use codecs::*;
|
||||||
pub(crate) use decimal_resolution::*;
|
pub use decryption_key::DecryptionKey;
|
||||||
pub use decryption_key::*;
|
|
||||||
pub use encryption_method::*;
|
pub use encryption_method::*;
|
||||||
pub use hdcp_level::*;
|
pub use hdcp_level::*;
|
||||||
pub use in_stream_id::*;
|
pub use in_stream_id::*;
|
||||||
|
@ -30,7 +32,11 @@ pub use initialization_vector::*;
|
||||||
pub use key_format::*;
|
pub use key_format::*;
|
||||||
pub use key_format_versions::*;
|
pub use key_format_versions::*;
|
||||||
pub use media_type::*;
|
pub use media_type::*;
|
||||||
|
pub use playlist_type::*;
|
||||||
pub use protocol_version::*;
|
pub use protocol_version::*;
|
||||||
pub(crate) use signed_decimal_floating_point::*;
|
pub use resolution::*;
|
||||||
pub use stream_inf::*;
|
pub use stream_data::StreamData;
|
||||||
pub use value::*;
|
pub use value::*;
|
||||||
|
|
||||||
|
pub use float::Float;
|
||||||
|
pub use ufloat::UFloat;
|
||||||
|
|
101
src/types/playlist_type.rs
Normal file
101
src/types/playlist_type.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
|
/// Provides mutability information about the [`MediaPlaylist`].
|
||||||
|
///
|
||||||
|
/// It applies to the entire [`MediaPlaylist`].
|
||||||
|
///
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub enum PlaylistType {
|
||||||
|
/// If the [`PlaylistType`] is Event, [`MediaSegment`]s
|
||||||
|
/// can only be added to the end of the [`MediaPlaylist`].
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
Event,
|
||||||
|
/// If the [`PlaylistType`] is Video On Demand (Vod),
|
||||||
|
/// the [`MediaPlaylist`] cannot change.
|
||||||
|
///
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
Vod,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaylistType {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
|
impl RequiredVersion for PlaylistType {
|
||||||
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PlaylistType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match &self {
|
||||||
|
Self::Event => write!(f, "{}EVENT", Self::PREFIX),
|
||||||
|
Self::Vod => write!(f, "{}VOD", Self::PREFIX),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PlaylistType {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
match input {
|
||||||
|
"EVENT" => Ok(Self::Event),
|
||||||
|
"VOD" => Ok(Self::Vod),
|
||||||
|
_ => Err(Error::custom(format!("unknown playlist type: {:?}", input))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:VOD".parse::<PlaylistType>().unwrap(),
|
||||||
|
PlaylistType::Vod,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:EVENT"
|
||||||
|
.parse::<PlaylistType>()
|
||||||
|
.unwrap(),
|
||||||
|
PlaylistType::Event,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!("#EXT-X-PLAYLIST-TYPE:H".parse::<PlaylistType>().is_err());
|
||||||
|
|
||||||
|
assert!("garbage".parse::<PlaylistType>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:VOD".to_string(),
|
||||||
|
PlaylistType::Vod.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:EVENT".to_string(),
|
||||||
|
PlaylistType::Event.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_required_version() {
|
||||||
|
assert_eq!(PlaylistType::Vod.required_version(), ProtocolVersion::V1);
|
||||||
|
assert_eq!(PlaylistType::Event.required_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,9 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// # [7. Protocol Version Compatibility]
|
/// The [`ProtocolVersion`] specifies which `m3u8` revision is required, to
|
||||||
/// The [`ProtocolVersion`] specifies, which m3u8 revision is required, to parse
|
/// parse a certain tag correctly.
|
||||||
/// a certain tag correctly.
|
#[non_exhaustive]
|
||||||
///
|
|
||||||
/// [7. Protocol Version Compatibility]:
|
|
||||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-7
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum ProtocolVersion {
|
pub enum ProtocolVersion {
|
||||||
|
@ -22,19 +19,22 @@ pub enum ProtocolVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProtocolVersion {
|
impl ProtocolVersion {
|
||||||
/// Returns the newest [`ProtocolVersion`], that is supported by
|
/// Returns the latest [`ProtocolVersion`] that is supported by
|
||||||
/// this library.
|
/// this library.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::types::ProtocolVersion;
|
/// # use hls_m3u8::types::ProtocolVersion;
|
||||||
/// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
|
/// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
pub const fn latest() -> Self { Self::V7 }
|
pub const fn latest() -> Self { Self::V7 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ProtocolVersion {
|
impl fmt::Display for ProtocolVersion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::V1 => write!(f, "1"),
|
Self::V1 => write!(f, "1"),
|
||||||
Self::V2 => write!(f, "2"),
|
Self::V2 => write!(f, "2"),
|
||||||
|
@ -66,6 +66,7 @@ impl FromStr for ProtocolVersion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The default is [`ProtocolVersion::V1`].
|
||||||
impl Default for ProtocolVersion {
|
impl Default for ProtocolVersion {
|
||||||
fn default() -> Self { Self::V1 }
|
fn default() -> Self { Self::V1 }
|
||||||
}
|
}
|
||||||
|
|
137
src/types/resolution.rs
Normal file
137
src/types/resolution.rs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use derive_more::Display;
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// The number of distinct pixels in each dimension that can be displayed (e.g.
|
||||||
|
/// 1920x1080).
|
||||||
|
///
|
||||||
|
/// For example Full HD has a resolution of 1920x1080.
|
||||||
|
#[derive(ShortHand, Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
|
||||||
|
#[display(fmt = "{}x{}", width, height)]
|
||||||
|
#[shorthand(enable(must_use))]
|
||||||
|
pub struct Resolution {
|
||||||
|
/// Horizontal pixel dimension.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Resolution;
|
||||||
|
/// let mut resolution = Resolution::new(1280, 720);
|
||||||
|
///
|
||||||
|
/// resolution.set_width(1000);
|
||||||
|
/// assert_eq!(resolution.width(), 1000);
|
||||||
|
/// ```
|
||||||
|
width: usize,
|
||||||
|
/// Vertical pixel dimension.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Resolution;
|
||||||
|
/// let mut resolution = Resolution::new(1280, 720);
|
||||||
|
///
|
||||||
|
/// resolution.set_height(800);
|
||||||
|
/// assert_eq!(resolution.height(), 800);
|
||||||
|
/// ```
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resolution {
|
||||||
|
/// Constructs a new [`Resolution`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Resolution;
|
||||||
|
/// let resolution = Resolution::new(1920, 1080);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(width: usize, height: usize) -> Self { Self { width, height } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(usize, usize)> for Resolution {
|
||||||
|
fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<(usize, usize)> for Resolution {
|
||||||
|
fn into(self) -> (usize, usize) { (self.width, self.height) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Resolution {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut input = input.splitn(2, 'x');
|
||||||
|
|
||||||
|
let width = input
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::custom("missing width for `Resolution` or an invalid input"))
|
||||||
|
.and_then(|v| v.parse().map_err(|e| Error::parse_int(v, e)))?;
|
||||||
|
|
||||||
|
let height = input
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::custom("missing height for `Resolution` or an invalid input"))
|
||||||
|
.and_then(|v| v.parse().map_err(|e| Error::parse_int(v, e)))?;
|
||||||
|
|
||||||
|
Ok(Self { width, height })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(
|
||||||
|
Resolution::new(1920, 1080).to_string(),
|
||||||
|
"1920x1080".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Resolution::new(1280, 720).to_string(),
|
||||||
|
"1280x720".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
Resolution::new(1920, 1080),
|
||||||
|
"1920x1080".parse::<Resolution>().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Resolution::new(1280, 720),
|
||||||
|
"1280x720".parse::<Resolution>().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!("1280".parse::<Resolution>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_width() {
|
||||||
|
assert_eq!(Resolution::new(1920, 1080).width(), 1920);
|
||||||
|
assert_eq!(Resolution::new(1920, 1080).set_width(12).width(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_height() {
|
||||||
|
assert_eq!(Resolution::new(1920, 1080).height(), 1080);
|
||||||
|
assert_eq!(Resolution::new(1920, 1080).set_height(12).height(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from() {
|
||||||
|
assert_eq!(Resolution::from((1920, 1080)), Resolution::new(1920, 1080));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into() {
|
||||||
|
assert_eq!((1920, 1080), Resolution::new(1920, 1080).into());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,99 +0,0 @@
|
||||||
use core::ops::Deref;
|
|
||||||
use derive_more::{Display, FromStr};
|
|
||||||
|
|
||||||
/// Signed decimal floating-point number.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display, FromStr)]
|
|
||||||
pub(crate) struct SignedDecimalFloatingPoint(f64);
|
|
||||||
|
|
||||||
impl SignedDecimalFloatingPoint {
|
|
||||||
/// Makes a new [`SignedDecimalFloatingPoint`] instance.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// The given value must be finite, otherwise this function will panic!
|
|
||||||
pub fn new(value: f64) -> Self {
|
|
||||||
if value.is_infinite() || value.is_nan() {
|
|
||||||
panic!("Floating point value must be finite and not NaN!");
|
|
||||||
}
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn from_f64_unchecked(value: f64) -> Self { Self(value) }
|
|
||||||
|
|
||||||
/// Converts [`DecimalFloatingPoint`] to [`f64`].
|
|
||||||
pub const fn as_f64(self) -> f64 { self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for SignedDecimalFloatingPoint {
|
|
||||||
type Target = f64;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
macro_rules! test_from {
|
|
||||||
( $( $input:expr => $output:expr ),* ) => {
|
|
||||||
use ::core::convert::From;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from() {
|
|
||||||
$(
|
|
||||||
assert_eq!(
|
|
||||||
$input,
|
|
||||||
$output,
|
|
||||||
);
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_from![
|
|
||||||
SignedDecimalFloatingPoint::from(1_u8) => SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
SignedDecimalFloatingPoint::from(1_i8) => SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
SignedDecimalFloatingPoint::from(1_u16) => SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
SignedDecimalFloatingPoint::from(1_i16) => SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
SignedDecimalFloatingPoint::from(1_u32) => SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
SignedDecimalFloatingPoint::from(1_i32) => SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
SignedDecimalFloatingPoint::from(1.0_f32) => SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
SignedDecimalFloatingPoint::from(1.0_f64) => SignedDecimalFloatingPoint::new(1.0)
|
|
||||||
];
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_display() {
|
|
||||||
assert_eq!(
|
|
||||||
SignedDecimalFloatingPoint::new(1.0).to_string(),
|
|
||||||
1.0_f64.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_new_panic() { SignedDecimalFloatingPoint::new(::std::f64::INFINITY); }
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
assert_eq!(
|
|
||||||
SignedDecimalFloatingPoint::new(1.0),
|
|
||||||
"1.0".parse::<SignedDecimalFloatingPoint>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("garbage".parse::<SignedDecimalFloatingPoint>().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_f64() {
|
|
||||||
assert_eq!(SignedDecimalFloatingPoint::new(1.0).as_f64(), 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deref() {
|
|
||||||
assert_eq!(SignedDecimalFloatingPoint::from(0.1).floor(), 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
402
src/types/stream_data.rs
Normal file
402
src/types/stream_data.rs
Normal file
|
@ -0,0 +1,402 @@
|
||||||
|
use core::fmt;
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use derive_builder::Builder;
|
||||||
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{Codecs, HdcpLevel, ProtocolVersion, Resolution};
|
||||||
|
use crate::utils::{quote, unquote};
|
||||||
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
|
/// The [`StreamData`] struct contains the data that is shared between both
|
||||||
|
/// variants of the [`VariantStream`].
|
||||||
|
///
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
#[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)]
|
||||||
|
#[builder(setter(strip_option))]
|
||||||
|
#[builder(derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash))]
|
||||||
|
#[shorthand(enable(must_use, into))]
|
||||||
|
pub struct StreamData {
|
||||||
|
/// The peak segment bitrate of the [`VariantStream`] in bits per second.
|
||||||
|
///
|
||||||
|
/// If all the [`MediaSegment`]s in a [`VariantStream`] have already been
|
||||||
|
/// created, the bandwidth value must be the largest sum of peak segment
|
||||||
|
/// bitrates that is produced by any playable combination of renditions.
|
||||||
|
///
|
||||||
|
/// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just
|
||||||
|
/// the peak segment bit rate of that [`MediaPlaylist`].)
|
||||||
|
///
|
||||||
|
/// An inaccurate value can cause playback stalls or prevent clients from
|
||||||
|
/// playing the variant. If the [`MasterPlaylist`] is to be made available
|
||||||
|
/// before all [`MediaSegment`]s in the presentation have been encoded, the
|
||||||
|
/// bandwidth value should be the bandwidth value of a representative
|
||||||
|
/// period of similar content, encoded using the same settings.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::StreamData;
|
||||||
|
/// #
|
||||||
|
/// let mut stream = StreamData::new(20);
|
||||||
|
///
|
||||||
|
/// stream.set_bandwidth(5);
|
||||||
|
/// assert_eq!(stream.bandwidth(), 5);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is required.
|
||||||
|
///
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
#[shorthand(disable(into))]
|
||||||
|
bandwidth: u64,
|
||||||
|
/// The average bandwidth of the stream in bits per second.
|
||||||
|
///
|
||||||
|
/// It represents the average segment bitrate of the [`VariantStream`]. If
|
||||||
|
/// all the [`MediaSegment`]s in a [`VariantStream`] have already been
|
||||||
|
/// created, the average bandwidth must be the largest sum of average
|
||||||
|
/// segment bitrates that is produced by any playable combination of
|
||||||
|
/// renditions.
|
||||||
|
///
|
||||||
|
/// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just
|
||||||
|
/// the average segment bitrate of that [`MediaPlaylist`].)
|
||||||
|
///
|
||||||
|
/// An inaccurate value can cause playback stalls or prevent clients from
|
||||||
|
/// playing the variant. If the [`MasterPlaylist`] is to be made available
|
||||||
|
/// before all [`MediaSegment`]s in the presentation have been encoded, the
|
||||||
|
/// average bandwidth should be the average bandwidth of a representative
|
||||||
|
/// period of similar content, encoded using the same settings.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::StreamData;
|
||||||
|
/// #
|
||||||
|
/// let mut stream = StreamData::new(20);
|
||||||
|
///
|
||||||
|
/// stream.set_average_bandwidth(Some(300));
|
||||||
|
/// assert_eq!(stream.average_bandwidth(), Some(300));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
///
|
||||||
|
/// [`MediaSegment`]: crate::MediaSegment
|
||||||
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
#[builder(default)]
|
||||||
|
#[shorthand(enable(copy), disable(into, option_as_ref))]
|
||||||
|
average_bandwidth: Option<u64>,
|
||||||
|
/// A list of formats, where each format specifies a media sample type that
|
||||||
|
/// is present in one or more renditions specified by the [`VariantStream`].
|
||||||
|
///
|
||||||
|
/// Valid format identifiers are those in the ISO Base Media File Format
|
||||||
|
/// Name Space defined by "The 'Codecs' and 'Profiles' Parameters for
|
||||||
|
/// "Bucket" Media Types" ([RFC6381]).
|
||||||
|
///
|
||||||
|
/// For example, a stream containing AAC low complexity (AAC-LC) audio and
|
||||||
|
/// H.264 Main Profile Level 3.0 video would be
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::Codecs;
|
||||||
|
/// let codecs = Codecs::from(&["mp4a.40.2", "avc1.4d401e"]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::StreamData;
|
||||||
|
/// use hls_m3u8::types::Codecs;
|
||||||
|
///
|
||||||
|
/// let mut stream = StreamData::new(20);
|
||||||
|
///
|
||||||
|
/// stream.set_codecs(Some(&["mp4a.40.2", "avc1.4d401e"]));
|
||||||
|
/// assert_eq!(
|
||||||
|
/// stream.codecs(),
|
||||||
|
/// Some(&Codecs::from(&["mp4a.40.2", "avc1.4d401e"]))
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional, but every instance of
|
||||||
|
/// [`VariantStream::ExtXStreamInf`] should include a codecs attribute.
|
||||||
|
///
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
/// [`VariantStream::ExtXStreamInf`]:
|
||||||
|
/// crate::tags::VariantStream::ExtXStreamInf
|
||||||
|
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
codecs: Option<Codecs>,
|
||||||
|
/// The resolution of the stream.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::StreamData;
|
||||||
|
/// use hls_m3u8::types::Resolution;
|
||||||
|
///
|
||||||
|
/// let mut stream = StreamData::new(20);
|
||||||
|
///
|
||||||
|
/// stream.set_resolution(Some((1920, 1080)));
|
||||||
|
/// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080)));
|
||||||
|
/// # stream.set_resolution(Some((1280, 10)));
|
||||||
|
/// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10)));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional, but it is recommended if the [`VariantStream`]
|
||||||
|
/// includes video.
|
||||||
|
///
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
#[shorthand(enable(copy))]
|
||||||
|
resolution: Option<Resolution>,
|
||||||
|
/// High-bandwidth Digital Content Protection level of the
|
||||||
|
/// [`VariantStream`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::StreamData;
|
||||||
|
/// use hls_m3u8::types::HdcpLevel;
|
||||||
|
/// #
|
||||||
|
/// let mut stream = StreamData::new(20);
|
||||||
|
///
|
||||||
|
/// stream.set_hdcp_level(Some(HdcpLevel::None));
|
||||||
|
/// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
///
|
||||||
|
/// [`VariantStream`]: crate::tags::VariantStream
|
||||||
|
#[builder(default)]
|
||||||
|
#[shorthand(enable(copy), disable(into))]
|
||||||
|
hdcp_level: Option<HdcpLevel>,
|
||||||
|
/// It indicates the set of video renditions, that should be used when
|
||||||
|
/// playing the presentation.
|
||||||
|
///
|
||||||
|
/// It must match the value of the [`ExtXMedia::group_id`] attribute
|
||||||
|
/// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose
|
||||||
|
/// [`ExtXMedia::media_type`] attribute is video. It indicates the set of
|
||||||
|
/// video renditions that should be used when playing the presentation.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::StreamData;
|
||||||
|
/// #
|
||||||
|
/// let mut stream = StreamData::new(20);
|
||||||
|
///
|
||||||
|
/// stream.set_video(Some("video_01"));
|
||||||
|
/// assert_eq!(stream.video(), Some(&"video_01".to_string()));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
///
|
||||||
|
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
|
||||||
|
/// [`ExtXMedia`]: crate::tags::ExtXMedia
|
||||||
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
video: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamData {
|
||||||
|
/// Creates a new [`StreamData`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::StreamData;
|
||||||
|
/// #
|
||||||
|
/// let stream = StreamData::new(20);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(bandwidth: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
bandwidth,
|
||||||
|
average_bandwidth: None,
|
||||||
|
codecs: None,
|
||||||
|
resolution: None,
|
||||||
|
hdcp_level: None,
|
||||||
|
video: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a builder for [`StreamData`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{HdcpLevel, StreamData};
|
||||||
|
///
|
||||||
|
/// StreamData::builder()
|
||||||
|
/// .bandwidth(200)
|
||||||
|
/// .average_bandwidth(15)
|
||||||
|
/// .codecs(&["mp4a.40.2", "avc1.4d401e"])
|
||||||
|
/// .resolution((1920, 1080))
|
||||||
|
/// .hdcp_level(HdcpLevel::Type0)
|
||||||
|
/// .video("video_01")
|
||||||
|
/// .build()?;
|
||||||
|
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn builder() -> StreamDataBuilder { StreamDataBuilder::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for StreamData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
||||||
|
|
||||||
|
if let Some(value) = &self.average_bandwidth {
|
||||||
|
write!(f, ",AVERAGE-BANDWIDTH={}", value)?;
|
||||||
|
}
|
||||||
|
if let Some(value) = &self.codecs {
|
||||||
|
write!(f, ",CODECS={}", quote(value))?;
|
||||||
|
}
|
||||||
|
if let Some(value) = &self.resolution {
|
||||||
|
write!(f, ",RESOLUTION={}", value)?;
|
||||||
|
}
|
||||||
|
if let Some(value) = &self.hdcp_level {
|
||||||
|
write!(f, ",HDCP-LEVEL={}", value)?;
|
||||||
|
}
|
||||||
|
if let Some(value) = &self.video {
|
||||||
|
write!(f, ",VIDEO={}", quote(value))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for StreamData {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut bandwidth = None;
|
||||||
|
let mut average_bandwidth = None;
|
||||||
|
let mut codecs = None;
|
||||||
|
let mut resolution = None;
|
||||||
|
let mut hdcp_level = None;
|
||||||
|
let mut video = None;
|
||||||
|
|
||||||
|
for (key, value) in AttributePairs::new(input) {
|
||||||
|
match key {
|
||||||
|
"BANDWIDTH" => {
|
||||||
|
bandwidth = Some(
|
||||||
|
value
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(|e| Error::parse_int(value, e))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"AVERAGE-BANDWIDTH" => {
|
||||||
|
average_bandwidth = Some(
|
||||||
|
value
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(|e| Error::parse_int(value, e))?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"CODECS" => codecs = Some(unquote(value).parse()?),
|
||||||
|
"RESOLUTION" => resolution = Some(value.parse()?),
|
||||||
|
"HDCP-LEVEL" => {
|
||||||
|
hdcp_level = Some(value.parse::<HdcpLevel>().map_err(Error::strum)?)
|
||||||
|
}
|
||||||
|
"VIDEO" => video = Some(unquote(value)),
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized
|
||||||
|
// AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
bandwidth,
|
||||||
|
average_bandwidth,
|
||||||
|
codecs,
|
||||||
|
resolution,
|
||||||
|
hdcp_level,
|
||||||
|
video,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct requires [`ProtocolVersion::V1`].
|
||||||
|
impl RequiredVersion for StreamData {
|
||||||
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
|
|
||||||
|
fn introduced_version(&self) -> ProtocolVersion {
|
||||||
|
if self.video.is_some() {
|
||||||
|
ProtocolVersion::V4
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let mut stream_data = StreamData::new(200);
|
||||||
|
stream_data.set_average_bandwidth(Some(15));
|
||||||
|
stream_data.set_codecs(Some(&["mp4a.40.2", "avc1.4d401e"]));
|
||||||
|
stream_data.set_resolution(Some((1920, 1080)));
|
||||||
|
stream_data.set_hdcp_level(Some(HdcpLevel::Type0));
|
||||||
|
stream_data.set_video(Some("video"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
stream_data.to_string(),
|
||||||
|
concat!(
|
||||||
|
"BANDWIDTH=200,",
|
||||||
|
"AVERAGE-BANDWIDTH=15,",
|
||||||
|
"CODECS=\"mp4a.40.2,avc1.4d401e\",",
|
||||||
|
"RESOLUTION=1920x1080,",
|
||||||
|
"HDCP-LEVEL=TYPE-0,",
|
||||||
|
"VIDEO=\"video\""
|
||||||
|
)
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
let mut stream_data = StreamData::new(200);
|
||||||
|
stream_data.set_average_bandwidth(Some(15));
|
||||||
|
stream_data.set_codecs(Some(&["mp4a.40.2", "avc1.4d401e"]));
|
||||||
|
stream_data.set_resolution(Some((1920, 1080)));
|
||||||
|
stream_data.set_hdcp_level(Some(HdcpLevel::Type0));
|
||||||
|
stream_data.set_video(Some("video"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
stream_data,
|
||||||
|
concat!(
|
||||||
|
"BANDWIDTH=200,",
|
||||||
|
"AVERAGE-BANDWIDTH=15,",
|
||||||
|
"CODECS=\"mp4a.40.2,avc1.4d401e\",",
|
||||||
|
"RESOLUTION=1920x1080,",
|
||||||
|
"HDCP-LEVEL=TYPE-0,",
|
||||||
|
"VIDEO=\"video\""
|
||||||
|
)
|
||||||
|
.parse()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!("garbage".parse::<StreamData>().is_err());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,362 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use derive_builder::Builder;
|
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{DecimalResolution, HdcpLevel};
|
|
||||||
use crate::utils::{quote, unquote};
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
/// # [4.3.4.2. EXT-X-STREAM-INF]
|
|
||||||
///
|
|
||||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
|
||||||
#[derive(Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)]
|
|
||||||
#[builder(setter(into, strip_option))]
|
|
||||||
#[builder(derive(Debug, PartialEq))]
|
|
||||||
pub struct StreamInf {
|
|
||||||
/// The maximum bandwidth of the stream.
|
|
||||||
bandwidth: u64,
|
|
||||||
#[builder(default)]
|
|
||||||
/// The average bandwidth of the stream.
|
|
||||||
average_bandwidth: Option<u64>,
|
|
||||||
#[builder(default)]
|
|
||||||
/// Every media format in any of the renditions specified by the Variant
|
|
||||||
/// Stream.
|
|
||||||
codecs: Option<String>,
|
|
||||||
#[builder(default)]
|
|
||||||
/// The resolution of the stream.
|
|
||||||
resolution: Option<DecimalResolution>,
|
|
||||||
#[builder(default)]
|
|
||||||
/// High-bandwidth Digital Content Protection
|
|
||||||
hdcp_level: Option<HdcpLevel>,
|
|
||||||
#[builder(default)]
|
|
||||||
/// It indicates the set of video renditions, that should be used when
|
|
||||||
/// playing the presentation.
|
|
||||||
video: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StreamInf {
|
|
||||||
/// Creates a new [`StreamInf`].
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let stream = StreamInf::new(20);
|
|
||||||
/// ```
|
|
||||||
pub const fn new(bandwidth: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
bandwidth,
|
|
||||||
average_bandwidth: None,
|
|
||||||
codecs: None,
|
|
||||||
resolution: None,
|
|
||||||
hdcp_level: None,
|
|
||||||
video: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the peak segment bit rate of the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let stream = StreamInf::new(20);
|
|
||||||
/// assert_eq!(stream.bandwidth(), 20);
|
|
||||||
/// ```
|
|
||||||
pub const fn bandwidth(&self) -> u64 { self.bandwidth }
|
|
||||||
|
|
||||||
/// Sets the peak segment bit rate of the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let mut stream = StreamInf::new(20);
|
|
||||||
///
|
|
||||||
/// stream.set_bandwidth(5);
|
|
||||||
/// assert_eq!(stream.bandwidth(), 5);
|
|
||||||
/// ```
|
|
||||||
pub fn set_bandwidth(&mut self, value: u64) -> &mut Self {
|
|
||||||
self.bandwidth = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the group identifier for the video in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let stream = StreamInf::new(20);
|
|
||||||
/// assert_eq!(stream.video(), &None);
|
|
||||||
/// ```
|
|
||||||
pub const fn video(&self) -> &Option<String> { &self.video }
|
|
||||||
|
|
||||||
/// Sets the group identifier for the video in the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let mut stream = StreamInf::new(20);
|
|
||||||
///
|
|
||||||
/// stream.set_video(Some("video"));
|
|
||||||
/// assert_eq!(stream.video(), &Some("video".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn set_video<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.video = value.map(|v| v.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the average segment bit rate of the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let stream = StreamInf::new(20);
|
|
||||||
/// assert_eq!(stream.average_bandwidth(), None);
|
|
||||||
/// ```
|
|
||||||
pub const fn average_bandwidth(&self) -> Option<u64> { self.average_bandwidth }
|
|
||||||
|
|
||||||
/// Sets the average segment bit rate of the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let mut stream = StreamInf::new(20);
|
|
||||||
///
|
|
||||||
/// stream.set_average_bandwidth(Some(300));
|
|
||||||
/// assert_eq!(stream.average_bandwidth(), Some(300));
|
|
||||||
/// ```
|
|
||||||
pub fn set_average_bandwidth(&mut self, value: Option<u64>) -> &mut Self {
|
|
||||||
self.average_bandwidth = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string that represents the list of codec types contained the variant
|
|
||||||
/// stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let stream = StreamInf::new(20);
|
|
||||||
/// assert_eq!(stream.codecs(), &None);
|
|
||||||
/// ```
|
|
||||||
pub const fn codecs(&self) -> &Option<String> { &self.codecs }
|
|
||||||
|
|
||||||
/// A string that represents the list of codec types contained the variant
|
|
||||||
/// stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let mut stream = StreamInf::new(20);
|
|
||||||
///
|
|
||||||
/// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
|
|
||||||
/// assert_eq!(stream.codecs(), &Some("mp4a.40.2,avc1.4d401e".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn set_codecs<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.codecs = value.map(|v| v.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the resolution of the stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let stream = StreamInf::new(20);
|
|
||||||
/// assert_eq!(stream.resolution(), None);
|
|
||||||
/// ```
|
|
||||||
pub fn resolution(&self) -> Option<(usize, usize)> {
|
|
||||||
if let Some(res) = &self.resolution {
|
|
||||||
Some((res.width(), res.height()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the resolution of the stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let mut stream = StreamInf::new(20);
|
|
||||||
///
|
|
||||||
/// stream.set_resolution(1920, 1080);
|
|
||||||
/// assert_eq!(stream.resolution(), Some((1920, 1080)));
|
|
||||||
/// # stream.set_resolution(1280, 10);
|
|
||||||
/// # assert_eq!(stream.resolution(), Some((1280, 10)));
|
|
||||||
/// ```
|
|
||||||
pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self {
|
|
||||||
if let Some(res) = &mut self.resolution {
|
|
||||||
res.set_width(width);
|
|
||||||
res.set_height(height);
|
|
||||||
} else {
|
|
||||||
self.resolution = Some(DecimalResolution::new(width, height));
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The HDCP level of the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::StreamInf;
|
|
||||||
/// #
|
|
||||||
/// let stream = StreamInf::new(20);
|
|
||||||
/// assert_eq!(stream.hdcp_level(), None);
|
|
||||||
/// ```
|
|
||||||
pub const fn hdcp_level(&self) -> Option<HdcpLevel> { self.hdcp_level }
|
|
||||||
|
|
||||||
/// The HDCP level of the variant stream.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::types::{HdcpLevel, StreamInf};
|
|
||||||
/// #
|
|
||||||
/// let mut stream = StreamInf::new(20);
|
|
||||||
///
|
|
||||||
/// stream.set_hdcp_level(Some(HdcpLevel::None));
|
|
||||||
/// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None));
|
|
||||||
/// ```
|
|
||||||
pub fn set_hdcp_level<T: Into<HdcpLevel>>(&mut self, value: Option<T>) -> &mut Self {
|
|
||||||
self.hdcp_level = value.map(Into::into);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for StreamInf {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
|
||||||
|
|
||||||
if let Some(value) = &self.average_bandwidth {
|
|
||||||
write!(f, ",AVERAGE-BANDWIDTH={}", value)?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.codecs {
|
|
||||||
write!(f, ",CODECS={}", quote(value))?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.resolution {
|
|
||||||
write!(f, ",RESOLUTION={}", value)?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.hdcp_level {
|
|
||||||
write!(f, ",HDCP-LEVEL={}", value)?;
|
|
||||||
}
|
|
||||||
if let Some(value) = &self.video {
|
|
||||||
write!(f, ",VIDEO={}", quote(value))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for StreamInf {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut bandwidth = None;
|
|
||||||
let mut average_bandwidth = None;
|
|
||||||
let mut codecs = None;
|
|
||||||
let mut resolution = None;
|
|
||||||
let mut hdcp_level = None;
|
|
||||||
let mut video = None;
|
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
|
||||||
match key.as_str() {
|
|
||||||
"BANDWIDTH" => bandwidth = Some(value.parse::<u64>()?),
|
|
||||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(value.parse::<u64>()?),
|
|
||||||
"CODECS" => codecs = Some(unquote(value)),
|
|
||||||
"RESOLUTION" => resolution = Some(value.parse()?),
|
|
||||||
"HDCP-LEVEL" => hdcp_level = Some(value.parse()?),
|
|
||||||
"VIDEO" => video = Some(unquote(value)),
|
|
||||||
_ => {
|
|
||||||
// [6.3.1. General Client Responsibilities]
|
|
||||||
// > ignore any attribute/value pair with an unrecognized
|
|
||||||
// AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
bandwidth,
|
|
||||||
average_bandwidth,
|
|
||||||
codecs,
|
|
||||||
resolution,
|
|
||||||
hdcp_level,
|
|
||||||
video,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_display() {
|
|
||||||
let mut stream_inf = StreamInf::new(200);
|
|
||||||
stream_inf.set_average_bandwidth(Some(15));
|
|
||||||
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
|
|
||||||
stream_inf.set_resolution(1920, 1080);
|
|
||||||
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
|
|
||||||
stream_inf.set_video(Some("video"));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
stream_inf.to_string(),
|
|
||||||
"BANDWIDTH=200,\
|
|
||||||
AVERAGE-BANDWIDTH=15,\
|
|
||||||
CODECS=\"mp4a.40.2,avc1.4d401e\",\
|
|
||||||
RESOLUTION=1920x1080,\
|
|
||||||
HDCP-LEVEL=TYPE-0,\
|
|
||||||
VIDEO=\"video\""
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
let mut stream_inf = StreamInf::new(200);
|
|
||||||
stream_inf.set_average_bandwidth(Some(15));
|
|
||||||
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
|
|
||||||
stream_inf.set_resolution(1920, 1080);
|
|
||||||
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
|
|
||||||
stream_inf.set_video(Some("video"));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
stream_inf,
|
|
||||||
"BANDWIDTH=200,\
|
|
||||||
AVERAGE-BANDWIDTH=15,\
|
|
||||||
CODECS=\"mp4a.40.2,avc1.4d401e\",\
|
|
||||||
RESOLUTION=1920x1080,\
|
|
||||||
HDCP-LEVEL=TYPE-0,\
|
|
||||||
VIDEO=\"video\""
|
|
||||||
.parse()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
stream_inf,
|
|
||||||
"BANDWIDTH=200,\
|
|
||||||
AVERAGE-BANDWIDTH=15,\
|
|
||||||
CODECS=\"mp4a.40.2,avc1.4d401e\",\
|
|
||||||
RESOLUTION=1920x1080,\
|
|
||||||
HDCP-LEVEL=TYPE-0,\
|
|
||||||
VIDEO=\"video\",\
|
|
||||||
UNKNOWN=\"value\""
|
|
||||||
.parse()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("garbage".parse::<StreamInf>().is_err());
|
|
||||||
}
|
|
||||||
}
|
|
317
src/types/ufloat.rs
Normal file
317
src/types/ufloat.rs
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use derive_more::{AsRef, Deref, Display};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// A wrapper type around an [`f32`], that can not be constructed
|
||||||
|
/// with a negative float (e.g. `-1.1`), [`NaN`], [`INFINITY`] or
|
||||||
|
/// [`NEG_INFINITY`].
|
||||||
|
///
|
||||||
|
/// [`NaN`]: core::f32::NAN
|
||||||
|
/// [`INFINITY`]: core::f32::INFINITY
|
||||||
|
/// [`NEG_INFINITY`]: core::f32::NEG_INFINITY
|
||||||
|
#[derive(AsRef, Deref, Default, Debug, Copy, Clone, PartialOrd, Display)]
|
||||||
|
pub struct UFloat(f32);
|
||||||
|
|
||||||
|
impl UFloat {
|
||||||
|
/// Makes a new [`UFloat`] from an [`f32`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If the given float is negative, infinite or [`NaN`].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::UFloat;
|
||||||
|
/// let float = UFloat::new(1.0);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This would panic:
|
||||||
|
///
|
||||||
|
/// ```should_panic
|
||||||
|
/// # use hls_m3u8::types::UFloat;
|
||||||
|
/// let float = UFloat::new(-1.0);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`NaN`]: core::f32::NAN
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(float: f32) -> Self {
|
||||||
|
if float.is_infinite() {
|
||||||
|
panic!("float must be finite: `{}`", float);
|
||||||
|
}
|
||||||
|
|
||||||
|
if float.is_nan() {
|
||||||
|
panic!("float must not be `NaN`");
|
||||||
|
}
|
||||||
|
|
||||||
|
if float.is_sign_negative() {
|
||||||
|
panic!("float must be positive: `{}`", float);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(float)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the underlying [`f32`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hls_m3u8::types::UFloat;
|
||||||
|
/// assert_eq!(UFloat::new(1.1_f32).as_f32(), 1.1_f32);
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_f32(self) -> f32 { self.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for UFloat {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let float = f32::from_str(input).map_err(|e| Error::parse_float(input, e))?;
|
||||||
|
Self::try_from(float)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<f32> for UFloat {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(float: f32) -> Result<Self, Self::Error> {
|
||||||
|
if float.is_infinite() {
|
||||||
|
return Err(Error::custom(format!("float must be finite: `{}`", float)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if float.is_nan() {
|
||||||
|
return Err(Error::custom("float must not be `NaN`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if float.is_sign_negative() {
|
||||||
|
return Err(Error::custom(format!(
|
||||||
|
"float must be positive: `{}`",
|
||||||
|
float
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(float))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! implement_from {
|
||||||
|
( $( $type:tt ),+ ) => {
|
||||||
|
$(
|
||||||
|
impl ::core::convert::From<$type> for UFloat {
|
||||||
|
fn from(value: $type) -> Self {
|
||||||
|
Self(value as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implement_from!(u16, u8);
|
||||||
|
|
||||||
|
// This has to be implemented explicitly, because `Hash` is also implemented
|
||||||
|
// manually and both implementations have to agree according to clippy.
|
||||||
|
impl PartialEq for UFloat {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenience implementation to compare f32 with a Float.
|
||||||
|
impl PartialEq<f32> for UFloat {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &f32) -> bool { &self.0 == other }
|
||||||
|
}
|
||||||
|
|
||||||
|
// In order to implement `Eq` a struct has to satisfy
|
||||||
|
// the following requirements:
|
||||||
|
// - reflexive: a == a;
|
||||||
|
// - symmetric: a == b implies b == a; and
|
||||||
|
// - transitive: a == b and b == c implies a == c.
|
||||||
|
//
|
||||||
|
// The symmetric and transitive parts are already satisfied
|
||||||
|
// through `PartialEq`. The reflexive part is not satisfied for f32,
|
||||||
|
// because `f32::NAN` never equals `f32::NAN`. (`assert!(f32::NAN, f32::NAN)`)
|
||||||
|
//
|
||||||
|
// It is ensured, that this struct can not be constructed
|
||||||
|
// with NaN so all of the above requirements are satisfied and therefore Eq can
|
||||||
|
// be soundly implemented.
|
||||||
|
impl Eq for UFloat {}
|
||||||
|
|
||||||
|
impl Ord for UFloat {
|
||||||
|
#[inline]
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
if self.0 < other.0 {
|
||||||
|
Ordering::Less
|
||||||
|
} else if self == other {
|
||||||
|
Ordering::Equal
|
||||||
|
} else {
|
||||||
|
Ordering::Greater
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The output of Hash cannot be relied upon to be stable. The same version of
|
||||||
|
/// rust can return different values in different architectures. This is not a
|
||||||
|
/// property of the Hasher that you’re using but instead of the way Hash happens
|
||||||
|
/// to be implemented for the type you’re using (e.g., the current
|
||||||
|
/// implementation of Hash for slices of integers returns different values in
|
||||||
|
/// big and little-endian architectures).
|
||||||
|
///
|
||||||
|
/// See <https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/33>
|
||||||
|
#[doc(hidden)]
|
||||||
|
impl ::core::hash::Hash for UFloat {
|
||||||
|
fn hash<H>(&self, state: &mut H)
|
||||||
|
where
|
||||||
|
H: ::core::hash::Hasher,
|
||||||
|
{
|
||||||
|
// this implementation assumes, that the internal float is:
|
||||||
|
// - positive
|
||||||
|
// - not NaN
|
||||||
|
// - neither negative nor positive infinity
|
||||||
|
|
||||||
|
// to validate those assumptions debug_assertions are here
|
||||||
|
// (those will be removed in a release build)
|
||||||
|
debug_assert!(self.0.is_sign_positive());
|
||||||
|
debug_assert!(self.0.is_finite());
|
||||||
|
debug_assert!(!self.0.is_nan());
|
||||||
|
|
||||||
|
// this implementation is based on
|
||||||
|
// https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/33
|
||||||
|
//
|
||||||
|
// The important points are:
|
||||||
|
// - NaN == NaN (UFloat does not allow NaN, so this should be satisfied)
|
||||||
|
// - +0 != -0 (UFloat does not allow negative numbers, so this is fine too)
|
||||||
|
|
||||||
|
// I do not think it matters to differentiate between architectures, that use
|
||||||
|
// big endian by default and those, that use little endian.
|
||||||
|
state.write(&self.to_be_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use core::hash::{Hash, Hasher};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(UFloat::new(22.0).to_string(), "22".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
UFloat::new(3.14159265359).to_string(),
|
||||||
|
"3.1415927".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
assert_eq!(UFloat::new(22.0), UFloat::from_str("22").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
UFloat::new(3.14159265359),
|
||||||
|
UFloat::from_str("3.14159265359").unwrap()
|
||||||
|
);
|
||||||
|
assert!(UFloat::from_str("1#").is_err());
|
||||||
|
assert!(UFloat::from_str("-1.0").is_err());
|
||||||
|
assert!(UFloat::from_str("NaN").is_err());
|
||||||
|
assert!(UFloat::from_str("inf").is_err());
|
||||||
|
assert!(UFloat::from_str("-inf").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash() {
|
||||||
|
let mut hasher_left = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
let mut hasher_right = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
UFloat::new(1.0).hash(&mut hasher_left),
|
||||||
|
UFloat::new(1.0).hash(&mut hasher_right)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(hasher_left.finish(), hasher_right.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ord() {
|
||||||
|
assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(1.1)), Ordering::Equal);
|
||||||
|
assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(2.1)), Ordering::Less);
|
||||||
|
assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(0.1)), Ordering::Greater);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_ord() {
|
||||||
|
assert_eq!(
|
||||||
|
UFloat::new(1.1).partial_cmp(&UFloat::new(1.1)),
|
||||||
|
Some(Ordering::Equal)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UFloat::new(1.1).partial_cmp(&UFloat::new(2.1)),
|
||||||
|
Some(Ordering::Less)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UFloat::new(1.1).partial_cmp(&UFloat::new(0.1)),
|
||||||
|
Some(Ordering::Greater)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_eq() {
|
||||||
|
assert_eq!(UFloat::new(1.0).eq(&UFloat::new(1.0)), true);
|
||||||
|
assert_eq!(UFloat::new(1.0).eq(&UFloat::new(33.3)), false);
|
||||||
|
assert_eq!(UFloat::new(1.1), 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must be positive: `-1.1`"]
|
||||||
|
fn test_new_negative() { let _ = UFloat::new(-1.1); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must be positive: `0`"]
|
||||||
|
fn test_new_negative_zero() { let _ = UFloat::new(-0.0); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must be finite: `inf`"]
|
||||||
|
fn test_new_infinite() { let _ = UFloat::new(::core::f32::INFINITY); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must be finite: `-inf`"]
|
||||||
|
fn test_new_neg_infinite() { let _ = UFloat::new(::core::f32::NEG_INFINITY); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "float must not be `NaN`"]
|
||||||
|
fn test_new_nan() { let _ = UFloat::new(::core::f32::NAN); }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_as_f32() {
|
||||||
|
assert_eq!(UFloat::new(1.1).as_f32(), 1.1_f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from() {
|
||||||
|
assert_eq!(UFloat::from(1_u8), UFloat::new(1.0));
|
||||||
|
assert_eq!(UFloat::from(1_u16), UFloat::new(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_try_from() {
|
||||||
|
assert_eq!(UFloat::try_from(1.1_f32).unwrap(), UFloat::new(1.1));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
UFloat::try_from(-1.1_f32),
|
||||||
|
Err(Error::custom("float must be positive: `-1.1`"))
|
||||||
|
);
|
||||||
|
assert!(UFloat::try_from(::core::f32::INFINITY).is_err());
|
||||||
|
assert!(UFloat::try_from(::core::f32::NAN).is_err());
|
||||||
|
assert!(UFloat::try_from(::core::f32::NEG_INFINITY).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_eq() {
|
||||||
|
struct _AssertEq
|
||||||
|
where
|
||||||
|
UFloat: Eq;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,24 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use hex;
|
use crate::types::Float;
|
||||||
|
|
||||||
use crate::utils::{quote, unquote};
|
use crate::utils::{quote, unquote};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, PartialOrd)]
|
/// A `Value`.
|
||||||
/// A [`Value`].
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
/// A [`String`].
|
/// A `String`.
|
||||||
String(String),
|
String(String),
|
||||||
/// A sequence of bytes.
|
/// A sequence of bytes.
|
||||||
Hex(Vec<u8>),
|
Hex(Vec<u8>),
|
||||||
/// A floating point number, that's neither NaN nor infinite!
|
/// A floating point number, that's neither NaN nor infinite.
|
||||||
Float(f64),
|
Float(Float),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Value {
|
impl fmt::Display for Value {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::String(value) => write!(f, "{}", quote(value)),
|
Self::String(value) => write!(f, "{}", quote(value)),
|
||||||
Self::Hex(value) => write!(f, "0x{}", hex::encode_upper(value)),
|
Self::Hex(value) => write!(f, "0x{}", hex::encode_upper(value)),
|
||||||
|
@ -32,9 +32,10 @@ impl FromStr for Value {
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
if input.starts_with("0x") || input.starts_with("0X") {
|
if input.starts_with("0x") || input.starts_with("0X") {
|
||||||
Ok(Self::Hex(hex::decode(
|
Ok(Self::Hex(
|
||||||
input.trim_start_matches("0x").trim_start_matches("0X"),
|
hex::decode(input.trim_start_matches("0x").trim_start_matches("0X"))
|
||||||
)?))
|
.map_err(Error::hex)?,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
match input.parse() {
|
match input.parse() {
|
||||||
Ok(value) => Ok(Self::Float(value)),
|
Ok(value) => Ok(Self::Float(value)),
|
||||||
|
@ -44,8 +45,8 @@ impl FromStr for Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<f64> for Value {
|
impl<T: Into<Float>> From<T> for Value {
|
||||||
fn from(value: f64) -> Self { Self::Float(value) }
|
fn from(value: T) -> Self { Self::Float(value.into()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<u8>> for Value {
|
impl From<Vec<u8>> for Value {
|
||||||
|
@ -60,10 +61,6 @@ impl From<&str> for Value {
|
||||||
fn from(value: &str) -> Self { Self::String(unquote(value)) }
|
fn from(value: &str) -> Self { Self::String(unquote(value)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl<T: AsRef<[u8]>> From<T> for Value {
|
|
||||||
// fn from(value: T) -> Self { Self::Hex(value.as_ref().into()) }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -71,7 +68,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(Value::Float(1.1).to_string(), "1.1".to_string());
|
assert_eq!(Value::Float(Float::new(1.1)).to_string(), "1.1".to_string());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Value::String("&str".to_string()).to_string(),
|
Value::String("&str".to_string()).to_string(),
|
||||||
"\"&str\"".to_string()
|
"\"&str\"".to_string()
|
||||||
|
@ -84,7 +81,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(Value::Float(1.1), "1.1".parse().unwrap());
|
assert_eq!(Value::Float(Float::new(1.1)), "1.1".parse().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Value::String("&str".to_string()),
|
Value::String("&str".to_string()),
|
||||||
"\"&str\"".parse().unwrap()
|
"\"&str\"".parse().unwrap()
|
||||||
|
@ -96,7 +93,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from() {
|
fn test_from() {
|
||||||
assert_eq!(Value::from(1.0_f64), Value::Float(1.0));
|
assert_eq!(Value::from(1_u8), Value::Float(Float::new(1.0)));
|
||||||
assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string()));
|
assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Value::from("&str".to_string()),
|
Value::from("&str".to_string()),
|
||||||
|
|
107
src/utils.rs
107
src/utils.rs
|
@ -1,4 +1,44 @@
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
use core::iter;
|
||||||
|
|
||||||
|
/// This is an extension trait that adds the below method to `bool`.
|
||||||
|
/// Those methods are already planned for the standard library, but are not
|
||||||
|
/// stable at the time of writing this comment.
|
||||||
|
///
|
||||||
|
/// The current status can be seen here:
|
||||||
|
/// <https://github.com/rust-lang/rust/issues/64260>
|
||||||
|
///
|
||||||
|
/// This trait exists to allow publishing a new version (requires stable
|
||||||
|
/// release) and the functions are prefixed with an `a` to prevent naming
|
||||||
|
/// conflicts with the coming std functions.
|
||||||
|
// TODO: replace this trait with std version as soon as it is stabilized
|
||||||
|
pub(crate) trait BoolExt {
|
||||||
|
#[must_use]
|
||||||
|
fn athen_some<T>(self, t: T) -> Option<T>;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn athen<T, F: FnOnce() -> T>(self, f: F) -> Option<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoolExt for bool {
|
||||||
|
#[inline]
|
||||||
|
fn athen_some<T>(self, t: T) -> Option<T> {
|
||||||
|
if self {
|
||||||
|
Some(t)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn athen<T, F: FnOnce() -> T>(self, f: F) -> Option<T> {
|
||||||
|
if self {
|
||||||
|
Some(f())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! required_version {
|
macro_rules! required_version {
|
||||||
( $( $tag:expr ),* ) => {
|
( $( $tag:expr ),* ) => {
|
||||||
|
@ -11,27 +51,6 @@ macro_rules! required_version {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_from {
|
|
||||||
( $($( $type:tt ),* => $target:path ),* ) => {
|
|
||||||
use ::core::convert::From;
|
|
||||||
|
|
||||||
$( // repeat $target
|
|
||||||
$( // repeat $type
|
|
||||||
impl From<$type> for $target {
|
|
||||||
fn from(value: $type) -> Self {
|
|
||||||
Self::from_f64_unchecked(value.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_from![
|
|
||||||
u8, u16, u32 => crate::types::DecimalFloatingPoint,
|
|
||||||
u8, i8, u16, i16, u32, i32, f32, f64 => crate::types::SignedDecimalFloatingPoint
|
|
||||||
];
|
|
||||||
|
|
||||||
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
||||||
match s.as_ref() {
|
match s.as_ref() {
|
||||||
"YES" => Ok(true),
|
"YES" => Ok(true),
|
||||||
|
@ -48,25 +67,30 @@ pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
||||||
///
|
///
|
||||||
/// Therefore it is safe to simply remove any occurence of those characters.
|
/// Therefore it is safe to simply remove any occurence of those characters.
|
||||||
/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2)
|
/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2)
|
||||||
pub(crate) fn unquote<T: ToString>(value: T) -> String {
|
pub(crate) fn unquote<T: AsRef<str>>(value: T) -> String {
|
||||||
value
|
value
|
||||||
.to_string()
|
.as_ref()
|
||||||
.replace("\"", "")
|
.chars()
|
||||||
.replace("\n", "")
|
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
|
||||||
.replace("\r", "")
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puts a string inside quotes.
|
/// Puts a string inside quotes.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub(crate) fn quote<T: ToString>(value: T) -> String {
|
pub(crate) fn quote<T: ToString>(value: T) -> String {
|
||||||
// the replace is for the case, that quote is called on an already quoted
|
// the replace is for the case, that quote is called on an already quoted
|
||||||
// string, which could cause problems!
|
// string, which could cause problems!
|
||||||
format!("\"{}\"", value.to_string().replace("\"", ""))
|
iter::once('"')
|
||||||
|
.chain(value.to_string().chars().filter(|c| *c != '"'))
|
||||||
|
.chain(iter::once('"'))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks, if the given tag is at the start of the input. If this is the case,
|
/// Checks, if the given tag is at the start of the input. If this is the case,
|
||||||
/// it will remove it and return the rest of the input.
|
/// it will remove it and return the rest of the input.
|
||||||
///
|
///
|
||||||
/// # Error
|
/// # Error
|
||||||
|
///
|
||||||
/// This function will return `Error::MissingTag`, if the input doesn't start
|
/// This function will return `Error::MissingTag`, if the input doesn't start
|
||||||
/// with the tag, that has been passed to this function.
|
/// with the tag, that has been passed to this function.
|
||||||
pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str>
|
pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str>
|
||||||
|
@ -76,8 +100,8 @@ where
|
||||||
if !input.trim().starts_with(tag.as_ref()) {
|
if !input.trim().starts_with(tag.as_ref()) {
|
||||||
return Err(Error::missing_tag(tag.as_ref(), input));
|
return Err(Error::missing_tag(tag.as_ref(), input));
|
||||||
}
|
}
|
||||||
let result = input.split_at(tag.as_ref().len()).1;
|
|
||||||
Ok(result)
|
Ok(input.trim().split_at(tag.as_ref().len()).1)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -122,5 +146,30 @@ mod tests {
|
||||||
assert_eq!(input, "SampleString");
|
assert_eq!(input, "SampleString");
|
||||||
|
|
||||||
assert!(tag(input, "B").is_err());
|
assert!(tag(input, "B").is_err());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
tag(
|
||||||
|
concat!(
|
||||||
|
"\n #EXTM3U\n",
|
||||||
|
" #EXT-X-TARGETDURATION:5220\n",
|
||||||
|
" #EXTINF:0,\n",
|
||||||
|
" http://media.example.com/entire1.ts\n",
|
||||||
|
" #EXTINF:5220,\n",
|
||||||
|
" http://media.example.com/entire2.ts\n",
|
||||||
|
" #EXT-X-ENDLIST"
|
||||||
|
),
|
||||||
|
"#EXTM3U"
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"\n",
|
||||||
|
" #EXT-X-TARGETDURATION:5220\n",
|
||||||
|
" #EXTINF:0,\n",
|
||||||
|
" http://media.example.com/entire1.ts\n",
|
||||||
|
" #EXTINF:5220,\n",
|
||||||
|
" http://media.example.com/entire2.ts\n",
|
||||||
|
" #EXT-X-ENDLIST"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,371 +1,129 @@
|
||||||
use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXMedia, ExtXStreamInf};
|
use hls_m3u8::tags::{ExtXMedia, VariantStream};
|
||||||
use hls_m3u8::types::MediaType;
|
use hls_m3u8::types::{MediaType, StreamData};
|
||||||
use hls_m3u8::MasterPlaylist;
|
use hls_m3u8::MasterPlaylist;
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
macro_rules! generate_tests {
|
||||||
fn test_master_playlist() {
|
( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => {
|
||||||
// https://tools.ietf.org/html/rfc8216#section-8.4
|
$(
|
||||||
let master_playlist = "#EXTM3U\n\
|
#[test]
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n\
|
fn $fnname() {
|
||||||
http://example.com/low.m3u8\n\
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n\
|
|
||||||
http://example.com/mid.m3u8\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n\
|
|
||||||
http://example.com/hi.m3u8\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\
|
|
||||||
http://example.com/audio-only.m3u8"
|
|
||||||
.parse::<MasterPlaylist>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
MasterPlaylist::builder()
|
}
|
||||||
.stream_inf_tags(vec![
|
)+
|
||||||
ExtXStreamInf::builder()
|
}
|
||||||
.bandwidth(1280000)
|
|
||||||
.average_bandwidth(1000000)
|
|
||||||
.uri("http://example.com/low.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(2560000)
|
|
||||||
.average_bandwidth(2000000)
|
|
||||||
.uri("http://example.com/mid.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(7680000)
|
|
||||||
.average_bandwidth(6000000)
|
|
||||||
.uri("http://example.com/hi.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(65000)
|
|
||||||
.codecs("mp4a.40.5")
|
|
||||||
.uri("http://example.com/audio-only.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
])
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
master_playlist
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
generate_tests! {
|
||||||
fn test_master_playlist_with_i_frames() {
|
test_alternate_audio => {
|
||||||
// https://tools.ietf.org/html/rfc8216#section-8.5
|
|
||||||
let master_playlist = "#EXTM3U\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000\n\
|
|
||||||
low/audio-video.m3u8\n\
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000\n\
|
|
||||||
mid/audio-video.m3u8\n\
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000\n\
|
|
||||||
hi/audio-video.m3u8\n\
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\
|
|
||||||
audio-only.m3u8"
|
|
||||||
.parse::<MasterPlaylist>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
MasterPlaylist::builder()
|
MasterPlaylist::builder()
|
||||||
.stream_inf_tags(vec![
|
.media(vec![
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(1280000)
|
|
||||||
.uri("low/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(2560000)
|
|
||||||
.uri("mid/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(7680000)
|
|
||||||
.uri("hi/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(65000)
|
|
||||||
.codecs("mp4a.40.5")
|
|
||||||
.uri("audio-only.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
])
|
|
||||||
.i_frame_stream_inf_tags(vec![
|
|
||||||
ExtXIFrameStreamInf::builder()
|
|
||||||
.bandwidth(86000)
|
|
||||||
.uri("low/iframe.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXIFrameStreamInf::builder()
|
|
||||||
.bandwidth(150000)
|
|
||||||
.uri("mid/iframe.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXIFrameStreamInf::builder()
|
|
||||||
.bandwidth(550000)
|
|
||||||
.uri("hi/iframe.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
])
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
master_playlist
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_master_playlist_with_alternative_audio() {
|
|
||||||
// https://tools.ietf.org/html/rfc8216#section-8.6
|
|
||||||
// TODO: I think the CODECS=\"..." have to be replaced.
|
|
||||||
let master_playlist = "#EXTM3U\n\
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", \
|
|
||||||
DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", \
|
|
||||||
URI=\"main/english-audio.m3u8\"\n\
|
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", \
|
|
||||||
DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", \
|
|
||||||
URI=\"main/german-audio.m3u8\"\n\
|
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", \
|
|
||||||
DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", \
|
|
||||||
URI=\"commentary/audio-only.m3u8\"\n\
|
|
||||||
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n\
|
|
||||||
low/video-only.m3u8\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n\
|
|
||||||
mid/video-only.m3u8\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n\
|
|
||||||
hi/video-only.m3u8\n\
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n\
|
|
||||||
main/english-audio.m3u8"
|
|
||||||
.parse::<MasterPlaylist>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
MasterPlaylist::builder()
|
|
||||||
.media_tags(vec![
|
|
||||||
ExtXMedia::builder()
|
ExtXMedia::builder()
|
||||||
.media_type(MediaType::Audio)
|
.media_type(MediaType::Audio)
|
||||||
.group_id("aac")
|
.group_id("audio")
|
||||||
|
.language("eng")
|
||||||
.name("English")
|
.name("English")
|
||||||
.is_default(true)
|
|
||||||
.is_autoselect(true)
|
.is_autoselect(true)
|
||||||
.language("en")
|
.is_default(true)
|
||||||
.uri("main/english-audio.m3u8")
|
.uri("eng/prog_index.m3u8")
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
ExtXMedia::builder()
|
ExtXMedia::builder()
|
||||||
.media_type(MediaType::Audio)
|
.media_type(MediaType::Audio)
|
||||||
.group_id("aac")
|
.group_id("audio")
|
||||||
.name("Deutsch")
|
.language("fre")
|
||||||
.is_default(false)
|
.name("Français")
|
||||||
.is_autoselect(true)
|
.is_autoselect(true)
|
||||||
.language("de")
|
.is_default(false)
|
||||||
.uri("main/german-audio.m3u8")
|
.uri("fre/prog_index.m3u8")
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
ExtXMedia::builder()
|
ExtXMedia::builder()
|
||||||
.media_type(MediaType::Audio)
|
.media_type(MediaType::Audio)
|
||||||
.group_id("aac")
|
.group_id("audio")
|
||||||
.name("Commentary")
|
.language("sp")
|
||||||
|
.name("Espanol")
|
||||||
|
.is_autoselect(true)
|
||||||
.is_default(false)
|
.is_default(false)
|
||||||
.is_autoselect(false)
|
.uri("sp/prog_index.m3u8")
|
||||||
.language("en")
|
|
||||||
.uri("commentary/audio-only.m3u8")
|
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
])
|
])
|
||||||
.stream_inf_tags(vec![
|
.variant_streams(vec![
|
||||||
ExtXStreamInf::builder()
|
VariantStream::ExtXStreamInf {
|
||||||
.bandwidth(1280000)
|
uri: "lo/prog_index.m3u8".into(),
|
||||||
.codecs("...")
|
frame_rate: None,
|
||||||
.audio("aac")
|
audio: Some("audio".into()),
|
||||||
.uri("low/video-only.m3u8")
|
subtitles: None,
|
||||||
.build()
|
closed_captions: None,
|
||||||
.unwrap(),
|
stream_data: StreamData::builder()
|
||||||
ExtXStreamInf::builder()
|
.bandwidth(195023)
|
||||||
.bandwidth(2560000)
|
.codecs(&["avc1.42e00a", "mp4a.40.2"])
|
||||||
.codecs("...")
|
.build()
|
||||||
.audio("aac")
|
.unwrap()
|
||||||
.uri("mid/video-only.m3u8")
|
},
|
||||||
.build()
|
VariantStream::ExtXStreamInf {
|
||||||
.unwrap(),
|
uri: "hi/prog_index.m3u8".into(),
|
||||||
ExtXStreamInf::builder()
|
frame_rate: None,
|
||||||
.bandwidth(7680000)
|
audio: Some("audio".into()),
|
||||||
.codecs("...")
|
subtitles: None,
|
||||||
.audio("aac")
|
closed_captions: None,
|
||||||
.uri("hi/video-only.m3u8")
|
stream_data: StreamData::builder()
|
||||||
.build()
|
.bandwidth(591680)
|
||||||
.unwrap(),
|
.codecs(&["avc1.42e01e", "mp4a.40.2"])
|
||||||
ExtXStreamInf::builder()
|
.build()
|
||||||
.bandwidth(65000)
|
.unwrap()
|
||||||
.codecs("mp4a.40.5")
|
}
|
||||||
.audio("aac")
|
|
||||||
.uri("main/english-audio.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
])
|
])
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
master_playlist
|
concat!(
|
||||||
);
|
"#EXTM3U\n",
|
||||||
}
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
#[test]
|
"TYPE=AUDIO,",
|
||||||
fn test_master_playlist_with_alternative_video() {
|
"URI=\"eng/prog_index.m3u8\",",
|
||||||
// https://tools.ietf.org/html/rfc8216#section-8.7
|
"GROUP-ID=\"audio\",",
|
||||||
let master_playlist = "#EXTM3U\n\
|
"LANGUAGE=\"eng\",",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", \
|
"NAME=\"English\",",
|
||||||
AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n\
|
"DEFAULT=YES,",
|
||||||
|
"AUTOSELECT=YES",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", \
|
"\n",
|
||||||
DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n\
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", \
|
"TYPE=AUDIO,",
|
||||||
DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n\
|
"URI=\"fre/prog_index.m3u8\",",
|
||||||
|
"GROUP-ID=\"audio\",",
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n\
|
"LANGUAGE=\"fre\",",
|
||||||
low/main/audio-video.m3u8\n\
|
"NAME=\"Français\",",
|
||||||
|
"AUTOSELECT=YES",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", \
|
"\n",
|
||||||
AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n\
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", \
|
"TYPE=AUDIO,",
|
||||||
DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n\
|
"URI=\"sp/prog_index.m3u8\",",
|
||||||
|
"GROUP-ID=\"audio\",",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", \
|
"LANGUAGE=\"sp\",",
|
||||||
DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n\
|
"NAME=\"Espanol\",",
|
||||||
|
"AUTOSELECT=YES",
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n\
|
"\n",
|
||||||
mid/main/audio-video.m3u8\n\
|
|
||||||
|
"#EXT-X-STREAM-INF:",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\", \
|
"BANDWIDTH=195023,",
|
||||||
AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n\
|
"CODECS=\"avc1.42e00a,mp4a.40.2\",",
|
||||||
|
"AUDIO=\"audio\"",
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", \
|
"\n",
|
||||||
DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n\
|
"lo/prog_index.m3u8\n",
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", \
|
"#EXT-X-STREAM-INF:",
|
||||||
DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n\
|
"BANDWIDTH=591680,",
|
||||||
|
"CODECS=\"avc1.42e01e,mp4a.40.2\",",
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\"
|
"AUDIO=\"audio\"",
|
||||||
hi/main/audio-video.m3u8"
|
"\n",
|
||||||
.parse::<MasterPlaylist>()
|
"hi/prog_index.m3u8\n"
|
||||||
.unwrap();
|
)
|
||||||
|
}
|
||||||
assert_eq!(
|
|
||||||
MasterPlaylist::builder()
|
|
||||||
.media_tags(vec![
|
|
||||||
// low
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("low")
|
|
||||||
.name("Main")
|
|
||||||
.is_default(true)
|
|
||||||
.is_autoselect(true)
|
|
||||||
.uri("low/main/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("low")
|
|
||||||
.name("Centerfield")
|
|
||||||
.is_default(false)
|
|
||||||
.uri("low/centerfield/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("low")
|
|
||||||
.name("Dugout")
|
|
||||||
.is_default(false)
|
|
||||||
.uri("low/dugout/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
// mid
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("mid")
|
|
||||||
.name("Main")
|
|
||||||
.is_default(true)
|
|
||||||
.is_autoselect(true)
|
|
||||||
.uri("mid/main/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("mid")
|
|
||||||
.name("Centerfield")
|
|
||||||
.is_default(false)
|
|
||||||
.uri("mid/centerfield/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("mid")
|
|
||||||
.name("Dugout")
|
|
||||||
.is_default(false)
|
|
||||||
.uri("mid/dugout/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
// hi
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("hi")
|
|
||||||
.name("Main")
|
|
||||||
.is_default(true)
|
|
||||||
.is_autoselect(true)
|
|
||||||
.uri("hi/main/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("hi")
|
|
||||||
.name("Centerfield")
|
|
||||||
.is_default(false)
|
|
||||||
.uri("hi/centerfield/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXMedia::builder()
|
|
||||||
.media_type(MediaType::Video)
|
|
||||||
.group_id("hi")
|
|
||||||
.name("Dugout")
|
|
||||||
.is_default(false)
|
|
||||||
.uri("hi/dugout/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
])
|
|
||||||
.stream_inf_tags(vec![
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(1280000)
|
|
||||||
.codecs("...")
|
|
||||||
.video("low")
|
|
||||||
.uri("low/main/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(2560000)
|
|
||||||
.codecs("...")
|
|
||||||
.video("mid")
|
|
||||||
.uri("mid/main/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXStreamInf::builder()
|
|
||||||
.bandwidth(7680000)
|
|
||||||
.codecs("...")
|
|
||||||
.video("hi")
|
|
||||||
.uri("hi/main/audio-video.m3u8")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
])
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
master_playlist
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +1,317 @@
|
||||||
|
//! Some tests of this file are from
|
||||||
|
//! <https://github.com/videojs/m3u8-parser/tree/master/test/fixtures/m3u8>
|
||||||
|
//!
|
||||||
|
//! TODO: the rest of the tests
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXMediaSequence, ExtXTargetDuration};
|
use hls_m3u8::tags::{ExtInf, ExtXByteRange};
|
||||||
|
use hls_m3u8::types::PlaylistType;
|
||||||
use hls_m3u8::{MediaPlaylist, MediaSegment};
|
use hls_m3u8::{MediaPlaylist, MediaSegment};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
macro_rules! generate_tests {
|
||||||
fn test_media_playlist_with_byterange() {
|
( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => {
|
||||||
let media_playlist = "#EXTM3U\n\
|
$(
|
||||||
#EXT-X-TARGETDURATION:10\n\
|
#[test]
|
||||||
#EXT-X-VERSION:4\n\
|
fn $fnname() {
|
||||||
#EXT-X-MEDIA-SEQUENCE:0\n\
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
#EXTINF:10.0,\n\
|
|
||||||
#EXT-X-BYTERANGE:75232@0\n\
|
|
||||||
video.ts\n\
|
|
||||||
#EXT-X-BYTERANGE:82112@752321\n\
|
|
||||||
#EXTINF:10.0,\n\
|
|
||||||
video.ts\n\
|
|
||||||
#EXTINF:10.0,\n\
|
|
||||||
#EXT-X-BYTERANGE:69864\n\
|
|
||||||
video.ts"
|
|
||||||
.parse::<MediaPlaylist>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_tests! {
|
||||||
|
test_media_playlist_with_byterange => {
|
||||||
MediaPlaylist::builder()
|
MediaPlaylist::builder()
|
||||||
.target_duration_tag(ExtXTargetDuration::new(Duration::from_secs(10)))
|
.media_sequence(1)
|
||||||
.media_sequence_tag(ExtXMediaSequence::new(0))
|
.target_duration(Duration::from_secs(10))
|
||||||
.segments(vec![
|
.segments(vec![
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.inf_tag(ExtInf::new(Duration::from_secs_f64(10.0)))
|
.duration(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||||
.byte_range_tag(ExtXByteRange::new(75232, Some(0)))
|
.byte_range(ExtXByteRange::from(0..75232))
|
||||||
.uri("video.ts")
|
.uri("video.ts")
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.inf_tag(ExtInf::new(Duration::from_secs_f64(10.0)))
|
.duration(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||||
.byte_range_tag(ExtXByteRange::new(82112, Some(752321)))
|
.byte_range(ExtXByteRange::from(752321..82112 + 752321))
|
||||||
.uri("video.ts")
|
.uri("video.ts")
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.inf_tag(ExtInf::new(Duration::from_secs_f64(10.0)))
|
.duration(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||||
.byte_range_tag(ExtXByteRange::new(69864, None))
|
// 834433..904297
|
||||||
|
.byte_range(ExtXByteRange::from(..69864))
|
||||||
.uri("video.ts")
|
.uri("video.ts")
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
])
|
])
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
media_playlist
|
concat!(
|
||||||
)
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-VERSION:4\n",
|
||||||
|
"#EXT-X-TARGETDURATION:10\n",
|
||||||
|
"#EXT-X-MEDIA-SEQUENCE:1\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:75232@0\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:82112@752321\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:69864@834433\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"video.ts\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_absolute_uris => {
|
||||||
|
MediaPlaylist::builder()
|
||||||
|
.playlist_type(PlaylistType::Vod)
|
||||||
|
.target_duration(Duration::from_secs(10))
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.uri("http://example.com/00001.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.uri("https://example.com/00002.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.uri("//example.com/00003.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.uri("http://example.com/00004.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
// TODO: currently this is treated as a comment
|
||||||
|
// .unknown(vec![
|
||||||
|
// "#ZEN-TOTAL-DURATION:57.9911".into()
|
||||||
|
// ])
|
||||||
|
.has_end_list(true)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-TARGETDURATION:10\n",
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:VOD\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"http://example.com/00001.ts\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"https://example.com/00002.ts\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"//example.com/00003.ts\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"http://example.com/00004.ts\n",
|
||||||
|
//"#ZEN-TOTAL-DURATION:57.9911\n",
|
||||||
|
"#EXT-X-ENDLIST\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_allow_cache => {
|
||||||
|
MediaPlaylist::builder()
|
||||||
|
.target_duration(Duration::from_secs(10))
|
||||||
|
.media_sequence(1)
|
||||||
|
.playlist_type(PlaylistType::Vod)
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.byte_range(0..522_828)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(522_828..1_110_328)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(1_110_328..1_823_412)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(1_823_412..2_299_992)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(2_299_992..2_835_604)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(2_835_604..3_042_780)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(3_042_780..3_498_680)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(3_498_680..4_155_928)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(4_155_928..4_727_636)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(4_727_636..5_212_676)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(5_212_676..5_921_812)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(5_921_812..6_651_816)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(6_651_816..7_108_092)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(7_108_092..7_576_776)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(7_576_776..8_021_772)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs(10)))
|
||||||
|
.byte_range(8_021_772..8_353_216)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(1.4167)))
|
||||||
|
.byte_range(8_353_216..8_397_772)
|
||||||
|
.uri("hls_450k_video.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.has_end_list(true)
|
||||||
|
.unknown(vec![
|
||||||
|
// deprecated tag:
|
||||||
|
"#EXT-X-ALLOW-CACHE:YES".into()
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-VERSION:4\n",
|
||||||
|
"#EXT-X-TARGETDURATION:10\n",
|
||||||
|
"#EXT-X-MEDIA-SEQUENCE:1\n",
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:VOD\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:522828@0\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:587500@522828\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:713084@1110328\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:476580@1823412\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:535612@2299992\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:207176@2835604\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:455900@3042780\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:657248@3498680\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:571708@4155928\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:485040@4727636\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:709136@5212676\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:730004@5921812\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:456276@6651816\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:468684@7108092\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:444996@7576776\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:331444@8021772\n",
|
||||||
|
"#EXTINF:10,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-BYTERANGE:44556@8353216\n",
|
||||||
|
"#EXTINF:1.4167,\n",
|
||||||
|
"hls_450k_video.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-ALLOW-CACHE:YES\n",
|
||||||
|
"#EXT-X-ENDLIST\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
//! Credits go to
|
|
||||||
//! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py
|
|
||||||
use hls_m3u8::tags::*;
|
|
||||||
use hls_m3u8::MediaPlaylist;
|
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_playlist() {
|
|
||||||
let playlist = r#"
|
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-TARGETDURATION:5220
|
|
||||||
#EXTINF:0,
|
|
||||||
http://media.example.com/entire1.ts
|
|
||||||
#EXTINF:5220,
|
|
||||||
http://media.example.com/entire2.ts
|
|
||||||
#EXT-X-ENDLIST"#;
|
|
||||||
|
|
||||||
let media_playlist = playlist.parse::<MediaPlaylist>().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
media_playlist.target_duration_tag(),
|
|
||||||
ExtXTargetDuration::new(Duration::from_secs(5220))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(media_playlist.segments().len(), 2);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
media_playlist.segments()[0].inf_tag(),
|
|
||||||
&ExtInf::new(Duration::from_secs(0))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
media_playlist.segments()[1].inf_tag(),
|
|
||||||
&ExtInf::new(Duration::from_secs(5220))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
media_playlist.segments()[0].uri(),
|
|
||||||
&"http://media.example.com/entire1.ts".to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
media_playlist.segments()[1].uri(),
|
|
||||||
&"http://media.example.com/entire2.ts".to_string()
|
|
||||||
);
|
|
||||||
}
|
|
629
tests/rfc8216.rs
Normal file
629
tests/rfc8216.rs
Normal file
|
@ -0,0 +1,629 @@
|
||||||
|
// https://tools.ietf.org/html/rfc8216#section-8
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, VariantStream};
|
||||||
|
use hls_m3u8::types::{DecryptionKey, EncryptionMethod, MediaType, StreamData};
|
||||||
|
use hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
macro_rules! generate_tests {
|
||||||
|
( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => {
|
||||||
|
$(
|
||||||
|
#[test]
|
||||||
|
fn $fnname() {
|
||||||
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
|
|
||||||
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_tests! {
|
||||||
|
test_simple_playlist => {
|
||||||
|
MediaPlaylist::builder()
|
||||||
|
.target_duration(Duration::from_secs(10))
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(9.009)))
|
||||||
|
.uri("http://media.example.com/first.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(9.009)))
|
||||||
|
.uri("http://media.example.com/second.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(3.003)))
|
||||||
|
.uri("http://media.example.com/third.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.has_end_list(true)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-VERSION:3\n",
|
||||||
|
"#EXT-X-TARGETDURATION:10\n",
|
||||||
|
"#EXTINF:9.009,\n",
|
||||||
|
"http://media.example.com/first.ts\n",
|
||||||
|
"#EXTINF:9.009,\n",
|
||||||
|
"http://media.example.com/second.ts\n",
|
||||||
|
"#EXTINF:3.003,\n",
|
||||||
|
"http://media.example.com/third.ts\n",
|
||||||
|
"#EXT-X-ENDLIST\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_live_media_playlist_using_https => {
|
||||||
|
MediaPlaylist::builder()
|
||||||
|
.target_duration(Duration::from_secs(8))
|
||||||
|
.media_sequence(2680)
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(7.975)))
|
||||||
|
.uri("https://priv.example.com/fileSequence2680.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(7.941)))
|
||||||
|
.uri("https://priv.example.com/fileSequence2681.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(7.975)))
|
||||||
|
.uri("https://priv.example.com/fileSequence2682.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-VERSION:3\n",
|
||||||
|
"#EXT-X-TARGETDURATION:8\n",
|
||||||
|
"#EXT-X-MEDIA-SEQUENCE:2680\n",
|
||||||
|
"#EXTINF:7.975,\n",
|
||||||
|
"https://priv.example.com/fileSequence2680.ts\n",
|
||||||
|
"#EXTINF:7.941,\n",
|
||||||
|
"https://priv.example.com/fileSequence2681.ts\n",
|
||||||
|
"#EXTINF:7.975,\n",
|
||||||
|
"https://priv.example.com/fileSequence2682.ts\n",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_media_playlist_with_encrypted_segments => {
|
||||||
|
MediaPlaylist::builder()
|
||||||
|
.target_duration(Duration::from_secs(15))
|
||||||
|
.media_sequence(7794)
|
||||||
|
.segments(vec![
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(2.833)))
|
||||||
|
.keys(vec![
|
||||||
|
ExtXKey::new(DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://priv.example.com/key.php?r=52"
|
||||||
|
))
|
||||||
|
])
|
||||||
|
.uri("http://media.example.com/fileSequence52-A.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(15.0)))
|
||||||
|
.keys(vec![
|
||||||
|
ExtXKey::new(DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://priv.example.com/key.php?r=52"
|
||||||
|
))
|
||||||
|
])
|
||||||
|
.uri("http://media.example.com/fileSequence52-B.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(13.333)))
|
||||||
|
.keys(vec![
|
||||||
|
ExtXKey::new(DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://priv.example.com/key.php?r=52"
|
||||||
|
))
|
||||||
|
])
|
||||||
|
.uri("http://media.example.com/fileSequence52-C.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
MediaSegment::builder()
|
||||||
|
.duration(ExtInf::new(Duration::from_secs_f64(15.0)))
|
||||||
|
.keys(vec![
|
||||||
|
ExtXKey::new(DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://priv.example.com/key.php?r=53"
|
||||||
|
))
|
||||||
|
])
|
||||||
|
.uri("http://media.example.com/fileSequence53-A.ts")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-VERSION:3\n",
|
||||||
|
"#EXT-X-TARGETDURATION:15\n",
|
||||||
|
"#EXT-X-MEDIA-SEQUENCE:7794\n",
|
||||||
|
|
||||||
|
"#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=52\"\n",
|
||||||
|
|
||||||
|
"#EXTINF:2.833,\n",
|
||||||
|
"http://media.example.com/fileSequence52-A.ts\n",
|
||||||
|
"#EXTINF:15,\n",
|
||||||
|
"http://media.example.com/fileSequence52-B.ts\n",
|
||||||
|
"#EXTINF:13.333,\n",
|
||||||
|
"http://media.example.com/fileSequence52-C.ts\n",
|
||||||
|
|
||||||
|
"#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=53\"\n",
|
||||||
|
|
||||||
|
"#EXTINF:15,\n",
|
||||||
|
"http://media.example.com/fileSequence53-A.ts\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_master_playlist => {
|
||||||
|
MasterPlaylist::builder()
|
||||||
|
.variant_streams(vec![
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "http://example.com/low.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(1280000)
|
||||||
|
.average_bandwidth(1000000)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "http://example.com/mid.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(2560000)
|
||||||
|
.average_bandwidth(2000000)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "http://example.com/hi.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(7680000)
|
||||||
|
.average_bandwidth(6000000)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "http://example.com/audio-only.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(65000)
|
||||||
|
.codecs(&["mp4a.40.5"])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n",
|
||||||
|
"http://example.com/low.m3u8\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n",
|
||||||
|
"http://example.com/mid.m3u8\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n",
|
||||||
|
"http://example.com/hi.m3u8\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n",
|
||||||
|
"http://example.com/audio-only.m3u8\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_master_playlist_with_i_frames => {
|
||||||
|
MasterPlaylist::builder()
|
||||||
|
.variant_streams(vec![
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "low/audio-video.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::new(1280000)
|
||||||
|
},
|
||||||
|
VariantStream::ExtXIFrame {
|
||||||
|
uri: "low/iframe.m3u8".into(),
|
||||||
|
stream_data: StreamData::new(86000),
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "mid/audio-video.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::new(2560000)
|
||||||
|
},
|
||||||
|
VariantStream::ExtXIFrame {
|
||||||
|
uri: "mid/iframe.m3u8".into(),
|
||||||
|
stream_data: StreamData::new(150000),
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "hi/audio-video.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::new(7680000)
|
||||||
|
},
|
||||||
|
VariantStream::ExtXIFrame {
|
||||||
|
uri: "hi/iframe.m3u8".into(),
|
||||||
|
stream_data: StreamData::new(550000),
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "audio-only.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(65000)
|
||||||
|
.codecs(&["mp4a.40.5"])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=1280000\n",
|
||||||
|
"low/audio-video.m3u8\n",
|
||||||
|
"#EXT-X-I-FRAME-STREAM-INF:URI=\"low/iframe.m3u8\",BANDWIDTH=86000\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=2560000\n",
|
||||||
|
"mid/audio-video.m3u8\n",
|
||||||
|
"#EXT-X-I-FRAME-STREAM-INF:URI=\"mid/iframe.m3u8\",BANDWIDTH=150000\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=7680000\n",
|
||||||
|
"hi/audio-video.m3u8\n",
|
||||||
|
"#EXT-X-I-FRAME-STREAM-INF:URI=\"hi/iframe.m3u8\",BANDWIDTH=550000\n",
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n",
|
||||||
|
"audio-only.m3u8\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_master_playlist_with_alternative_audio => {
|
||||||
|
MasterPlaylist::builder()
|
||||||
|
.media(vec![
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Audio)
|
||||||
|
.group_id("aac")
|
||||||
|
.name("English")
|
||||||
|
.is_default(true)
|
||||||
|
.is_autoselect(true)
|
||||||
|
.language("en")
|
||||||
|
.uri("main/english-audio.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Audio)
|
||||||
|
.group_id("aac")
|
||||||
|
.name("Deutsch")
|
||||||
|
.is_default(false)
|
||||||
|
.is_autoselect(true)
|
||||||
|
.language("de")
|
||||||
|
.uri("main/german-audio.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Audio)
|
||||||
|
.group_id("aac")
|
||||||
|
.name("Commentary")
|
||||||
|
.is_default(false)
|
||||||
|
.is_autoselect(false)
|
||||||
|
.language("en")
|
||||||
|
.uri("commentary/audio-only.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.variant_streams(vec![
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "low/video-only.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: Some("aac".into()),
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(1280000)
|
||||||
|
.codecs(&["..."])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "mid/video-only.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: Some("aac".into()),
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(2560000)
|
||||||
|
.codecs(&["..."])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "hi/video-only.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: Some("aac".into()),
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(7680000)
|
||||||
|
.codecs(&["..."])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "main/english-audio.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: Some("aac".into()),
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(65000)
|
||||||
|
.codecs(&["mp4a.40.5"])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=AUDIO,",
|
||||||
|
"URI=\"main/english-audio.m3u8\",",
|
||||||
|
"GROUP-ID=\"aac\",",
|
||||||
|
"LANGUAGE=\"en\",",
|
||||||
|
"NAME=\"English\",",
|
||||||
|
"DEFAULT=YES,",
|
||||||
|
"AUTOSELECT=YES\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=AUDIO,",
|
||||||
|
"URI=\"main/german-audio.m3u8\",",
|
||||||
|
"GROUP-ID=\"aac\",",
|
||||||
|
"LANGUAGE=\"de\",",
|
||||||
|
"NAME=\"Deutsch\",",
|
||||||
|
"AUTOSELECT=YES\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=AUDIO,",
|
||||||
|
"URI=\"commentary/audio-only.m3u8\",",
|
||||||
|
"GROUP-ID=\"aac\",",
|
||||||
|
"LANGUAGE=\"en\",",
|
||||||
|
"NAME=\"Commentary\"\n",
|
||||||
|
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n",
|
||||||
|
"low/video-only.m3u8\n",
|
||||||
|
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n",
|
||||||
|
"mid/video-only.m3u8\n",
|
||||||
|
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n",
|
||||||
|
"hi/video-only.m3u8\n",
|
||||||
|
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n",
|
||||||
|
"main/english-audio.m3u8\n"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_master_playlist_with_alternative_video => {
|
||||||
|
MasterPlaylist::builder()
|
||||||
|
.media(vec![
|
||||||
|
// low
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("low")
|
||||||
|
.name("Main")
|
||||||
|
.is_default(true)
|
||||||
|
.is_autoselect(true)
|
||||||
|
.uri("low/main/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("low")
|
||||||
|
.name("Centerfield")
|
||||||
|
.is_default(false)
|
||||||
|
.uri("low/centerfield/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("low")
|
||||||
|
.name("Dugout")
|
||||||
|
.is_default(false)
|
||||||
|
.uri("low/dugout/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
// mid
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("mid")
|
||||||
|
.name("Main")
|
||||||
|
.is_default(true)
|
||||||
|
.is_autoselect(true)
|
||||||
|
.uri("mid/main/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("mid")
|
||||||
|
.name("Centerfield")
|
||||||
|
.is_default(false)
|
||||||
|
.uri("mid/centerfield/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("mid")
|
||||||
|
.name("Dugout")
|
||||||
|
.is_default(false)
|
||||||
|
.uri("mid/dugout/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
// hi
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("hi")
|
||||||
|
.name("Main")
|
||||||
|
.is_default(true)
|
||||||
|
.is_autoselect(true)
|
||||||
|
.uri("hi/main/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("hi")
|
||||||
|
.name("Centerfield")
|
||||||
|
.is_default(false)
|
||||||
|
.uri("hi/centerfield/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXMedia::builder()
|
||||||
|
.media_type(MediaType::Video)
|
||||||
|
.group_id("hi")
|
||||||
|
.name("Dugout")
|
||||||
|
.is_default(false)
|
||||||
|
.uri("hi/dugout/audio-video.m3u8")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.variant_streams(vec![
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "low/main/audio-video.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(1280000)
|
||||||
|
.codecs(&["..."])
|
||||||
|
.video("low")
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "mid/main/audio-video.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(2560000)
|
||||||
|
.codecs(&["..."])
|
||||||
|
.video("mid")
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: "hi/main/audio-video.m3u8".into(),
|
||||||
|
frame_rate: None,
|
||||||
|
audio: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
stream_data: StreamData::builder()
|
||||||
|
.bandwidth(7680000)
|
||||||
|
.codecs(&["..."])
|
||||||
|
.video("hi")
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
concat!(
|
||||||
|
"#EXTM3U\n",
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"low/main/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"low\",",
|
||||||
|
"NAME=\"Main\",",
|
||||||
|
"DEFAULT=YES,",
|
||||||
|
"AUTOSELECT=YES",
|
||||||
|
"\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"low/centerfield/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"low\",",
|
||||||
|
"NAME=\"Centerfield\"",
|
||||||
|
"\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"low/dugout/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"low\",",
|
||||||
|
"NAME=\"Dugout\"",
|
||||||
|
"\n",
|
||||||
|
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"mid/main/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"mid\",",
|
||||||
|
"NAME=\"Main\",",
|
||||||
|
"DEFAULT=YES,",
|
||||||
|
"AUTOSELECT=YES\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"mid/centerfield/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"mid\",",
|
||||||
|
"NAME=\"Centerfield\"\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"mid/dugout/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"mid\",",
|
||||||
|
"NAME=\"Dugout\"\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"hi/main/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"hi\",",
|
||||||
|
"NAME=\"Main\",",
|
||||||
|
"DEFAULT=YES,",
|
||||||
|
"AUTOSELECT=YES\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"hi/centerfield/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"hi\",",
|
||||||
|
"NAME=\"Centerfield\"\n",
|
||||||
|
|
||||||
|
"#EXT-X-MEDIA:",
|
||||||
|
"TYPE=VIDEO,",
|
||||||
|
"URI=\"hi/dugout/audio-video.m3u8\",",
|
||||||
|
"GROUP-ID=\"hi\",",
|
||||||
|
"NAME=\"Dugout\"\n",
|
||||||
|
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n",
|
||||||
|
"low/main/audio-video.m3u8\n",
|
||||||
|
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n",
|
||||||
|
"mid/main/audio-video.m3u8\n",
|
||||||
|
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\"\n",
|
||||||
|
"hi/main/audio-video.m3u8\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
9
tests/version-number.rs
Normal file
9
tests/version-number.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#[test]
|
||||||
|
fn test_readme_deps() {
|
||||||
|
version_sync::assert_markdown_deps_updated!("README.md");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_html_root_url() {
|
||||||
|
version_sync::assert_html_root_url_updated!("src/lib.rs");
|
||||||
|
}
|
Loading…
Reference in a new issue