mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-09-28 14:31:53 +00:00
Compare commits
255 commits
Author | SHA1 | Date | |
---|---|---|---|
|
dbf26f685e | ||
|
d02edcab90 | ||
|
c8d848a51e | ||
|
4aa9f63f3b | ||
|
4f1c1fe32c | ||
|
8432c003d1 | ||
|
149c3b5c45 | ||
|
f5ddfed738 | ||
|
1c31d3835f | ||
|
aa4728aaed | ||
|
ddb618f0d9 | ||
|
6c3438a68f | ||
|
9ec0687f5e | ||
|
4ba3fcf352 | ||
|
f7eaeea281 | ||
|
3a742a95b6 | ||
|
6c694d186d | ||
|
c2c94f9352 | ||
|
8bf28b75fa | ||
|
5c2852c3ce | ||
|
fc067c3293 | ||
|
153c6e3e33 | ||
|
8ad21ec161 | ||
|
23c799a88b | ||
|
eff1e6783d | ||
|
42b5eb531b | ||
|
09227f124b | ||
|
af92a94873 | ||
|
68453ea54d | ||
|
e6588ab963 | ||
|
c343858860 | ||
|
f98f12fa82 | ||
|
e41105afdd | ||
|
7907f9b1f1 | ||
|
85df5c94ad | ||
|
fe54e0b456 | ||
|
c138f70738 | ||
|
fa3bb30c5c | ||
|
c4643c7083 | ||
|
2d6a49662d | ||
|
1a4813a1d1 | ||
|
ce04223ec4 | ||
|
096957b167 | ||
|
7e6da6224d | ||
|
8c4ff3e399 | ||
|
8a1cfd86d0 | ||
|
413d5263a3 | ||
|
6d4e6051c4 | ||
|
41d917e6f1 | ||
|
1057c905bf | ||
|
a085971c42 | ||
|
34df16f7d6 | ||
|
fdc3442bb6 | ||
|
ec0b5cdb21 | ||
|
7a918d31bd | ||
|
f0d91c5e7c | ||
|
f90ea7a121 | ||
|
25a01261f5 | ||
|
3492c529c5 | ||
|
41f81aebb3 | ||
|
c6b1732c26 | ||
|
e07fb9262d | ||
|
fb4f6a451e | ||
|
969e5bae9a | ||
|
9eccea8a7f | ||
|
47eccfdef9 | ||
|
8ece080cda | ||
|
79c4f5e934 | ||
|
3710a9c5c2 | ||
|
8b3517326b | ||
|
e174fcac9a | ||
|
899aea7fc1 | ||
|
9a2cacf024 | ||
|
8eb45dceb7 | ||
|
e187c9dc7c | ||
|
20072c2695 | ||
|
6cd9fe7064 | ||
|
24c5ad8199 | ||
|
cc48478b05 | ||
|
7a63c2dcf2 | ||
|
c268fa3a82 | ||
|
72c0ff9c75 | ||
|
429f3f8c3d | ||
|
f48876ee07 | ||
|
99b6b23acc | ||
|
c56a56abe8 | ||
|
285d2eccb8 | ||
|
112c3998b8 | ||
|
ca302ef543 | ||
|
42e1afaa47 | ||
|
15cc360a2c | ||
|
ca3ba476c3 | ||
|
fc1136265c | ||
|
7c26d2f7f1 | ||
|
870a39cddd | ||
|
d1fdb7fec1 | ||
|
b8fd4c15d5 | ||
|
7025114e36 | ||
|
02d363daa1 | ||
|
b2fb58559c | ||
|
1b01675250 | ||
|
ff807940b2 | ||
|
a797e401ed | ||
|
025add6dc3 | ||
|
78edff9341 | ||
|
4e41585cbd | ||
|
187174042d | ||
|
e338f5f95f | ||
|
afd9e0437c | ||
|
a262c77c58 | ||
|
6333a80507 | ||
|
6ef8182f2c | ||
|
9273e6c16c | ||
|
f7d81a55c9 | ||
|
dc12db9fad | ||
|
90783fdd9d | ||
|
11ac527fca | ||
|
c7419c864f | ||
|
0be0c7ddfb | ||
|
cdb6367dbd | ||
|
49c5b5334c | ||
|
dae826b4e5 | ||
|
88a5fa4460 | ||
|
03b0d2cf0c | ||
|
e1c10d27f7 | ||
|
5972216323 | ||
|
651db2e18b | ||
|
a8c788f4d2 | ||
|
8948f9914b | ||
|
5304947885 | ||
|
f404e68d1c | ||
|
070a62f9ad | ||
|
30e8009af1 | ||
|
86bb573c97 | ||
|
c39d104137 | ||
|
a96367e3fa | ||
|
d3c238df92 | ||
|
b54b17df73 | ||
|
b2c997d04d | ||
|
8cced1ac53 | ||
|
25f9691c75 | ||
|
94d85d922f | ||
|
9b61f74b9d | ||
|
3a388e3985 | ||
|
e6f5091f1b | ||
|
90ff18e2b3 | ||
|
101878a083 | ||
|
9cc162ece7 | ||
|
acbe7e73da | ||
|
ec07e6b64c | ||
|
4e298f76ef | ||
|
66c0b8dd0c | ||
|
5de47561b1 | ||
|
1b0eb56224 | ||
|
aae3809545 | ||
|
2471737455 | ||
|
e6a1103d24 | ||
|
006f36ff47 | ||
|
27d94faec4 | ||
|
048f09bd14 | ||
|
a777f74cfa | ||
|
e156f6e3fd | ||
|
ae43982d0b | ||
|
1876adbaf8 | ||
|
ac80ac5c9d | ||
|
448c331447 | ||
|
aa9dbc7b71 | ||
|
91ca70687b | ||
|
73d9eb4f79 | ||
|
c53e9e33f1 | ||
|
e75153ec5e | ||
|
c8020ede8e | ||
|
b1c1ea8bdc | ||
|
3dad1277ca | ||
|
b18e6ea4fb | ||
|
32876e1371 | ||
|
4ffd4350f8 | ||
|
8d1ed6372b | ||
|
99493446eb | ||
|
f76b223482 | ||
|
5b44262dc8 | ||
|
f96207c93e | ||
|
4b4cffc248 | ||
|
ebddb7a0e2 | ||
|
d2d2782cb0 | ||
|
5eca073a8c | ||
|
06a30d7704 | ||
|
93283f61f1 | ||
|
6b717f97c2 | ||
|
b197d5fbd7 | ||
|
0c4fa008e6 | ||
|
3240417304 | ||
|
81f9a421fe | ||
|
ed64ed15d3 | ||
|
b2c9f2db36 | ||
|
d240ac5c5e | ||
|
cdab47ad35 | ||
|
d04f9c2dc7 | ||
|
60ae8c5e60 | ||
|
56f8d10f38 | ||
|
71361ff328 | ||
|
ea75128aee | ||
|
0900d7e56b | ||
|
720dc32474 | ||
|
fb44c8803d | ||
|
612c3d15be | ||
|
e55113e752 | ||
|
5486c5e830 | ||
|
b932cef71a | ||
|
42469275d3 | ||
|
fd66f8b4ef | ||
|
1d614d580a | ||
|
db6961d19f | ||
|
fa96a76ca9 | ||
|
c28d6963a6 | ||
|
6ffbe50322 | ||
|
3acf67df6a | ||
|
b954ae1134 | ||
|
51b66d2adf | ||
|
dd1a40abc9 | ||
|
b1aa512679 | ||
|
a2614b5aca | ||
|
7483f49fe9 | ||
|
273c0990dc | ||
|
3721106795 | ||
|
c8f3df1228 | ||
|
1a35463185 | ||
|
de8f92508d | ||
|
f51ba2bb1c | ||
|
230128bb8e | ||
|
b2f836a445 | ||
|
91c6698f16 | ||
|
cf97a45f60 | ||
|
1966a7608d | ||
|
861e7e4b74 | ||
|
fe032ee984 | ||
|
cb27640867 | ||
|
5da2fa8104 | ||
|
3ecbbd9acb | ||
|
4324cb79d0 | ||
|
5a231199d6 | ||
|
211ce6e79a | ||
|
8034d543b8 | ||
|
a304ac0a2f | ||
|
788138903f | ||
|
7767f47f21 | ||
|
02d7c80b2b | ||
|
625d037b27 | ||
|
ac57417cc7 | ||
|
c66fcd9178 | ||
|
3122949384 | ||
|
807b73a701 | ||
|
ab82edf119 | ||
|
24a6ff9851 | ||
|
8585016720 |
93 changed files with 13979 additions and 4313 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 }}
|
52
.github/workflows/rust.yml
vendored
Normal file
52
.github/workflows/rust.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
on: [push, pull_request]
|
||||
|
||||
name: Continuous integration
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
fmt:
|
||||
name: rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
# rustfmt uses unstable features
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
tarpaulin-report.html
|
||||
|
|
57
.travis.yml
57
.travis.yml
|
@ -1,5 +1,18 @@
|
|||
language: rust
|
||||
sudo: required
|
||||
|
||||
cache: cargo
|
||||
|
||||
before_cache:
|
||||
- cargo install cargo-tarpaulin || echo "cargo-tarpaulin already installed"
|
||||
- cargo install cargo-update || echo "cargo-update already installed"
|
||||
- cargo install cargo-audit || echo "cargo-audit already installed"
|
||||
- cargo install-update --all
|
||||
# Travis can't cache files that are not readable by "others"
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
|
||||
# before_cache:
|
||||
# - rm -rf /home/travis/.cargo/registry
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
|
@ -8,32 +21,22 @@ matrix:
|
|||
allow_failures:
|
||||
- rust: nightly
|
||||
|
||||
env:
|
||||
global:
|
||||
- RUSTFLAGS="-C link-dead-code"
|
||||
script:
|
||||
- cargo clean
|
||||
- cargo build
|
||||
- cargo test
|
||||
- cargo test --features chrono
|
||||
- cargo test --features backtrace
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libcurl4-openssl-dev
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
- cmake
|
||||
- gcc
|
||||
- binutils-dev
|
||||
- libiberty-dev
|
||||
# it's enough to run this once:
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
|
||||
cargo audit
|
||||
fi
|
||||
|
||||
after_success: |
|
||||
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz &&
|
||||
tar xzf master.tar.gz &&
|
||||
cd kcov-master &&
|
||||
mkdir build &&
|
||||
cd build &&
|
||||
cmake .. &&
|
||||
make &&
|
||||
make install DESTDIR=../../kcov-build &&
|
||||
cd ../.. &&
|
||||
rm -rf kcov-master &&
|
||||
for file in target/debug/hls_m3u8-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done &&
|
||||
bash <(curl -s https://codecov.io/bash) &&
|
||||
echo "Uploaded code coverage"
|
||||
# this does require a -Z flag for Doctests, which is unstable!
|
||||
if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
|
||||
cargo tarpaulin -f --ignore-panics --ignore-tests --run-types Tests Doctests --out Xml
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
fi
|
||||
|
|
15
CHANGELOG.md
Normal file
15
CHANGELOG.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# hls_m3u8
|
||||
|
||||
## {next}
|
||||
|
||||
* Performance improvements:
|
||||
+ Changed `MediaPlaylist::segments` from `BTreeMap<usize, MediaSegment>`
|
||||
to `StableVec<MediaSegment>`
|
||||
+ Added `perf` feature, which can be used to improve performance in the future
|
||||
+ Changed all instances of `String` to `Cow<'a, str>` to reduce `Clone`-ing.
|
||||
|
||||
* Most structs now implement [`TryFrom<&'a str>`][TryFrom] instead of [`FromStr`][FromStr].
|
||||
|
||||
|
||||
[TryFrom]: https://doc.rust-lang.org/std/convert/trait.TryFrom.html
|
||||
[FromStr]: https://doc.rust-lang.org/std/str/trait.FromStr.html
|
44
Cargo.toml
44
Cargo.toml
|
@ -1,20 +1,50 @@
|
|||
[package]
|
||||
name = "hls_m3u8"
|
||||
version = "0.1.1"
|
||||
authors = ["Takeru Ohta <phjgt308@gmail.com>"]
|
||||
version = "0.4.2" # remember to update html_root_url
|
||||
authors = ["Takeru Ohta <phjgt308@gmail.com>", "Luro02 <24826124+Luro02@users.noreply.github.com>"]
|
||||
description = "HLS m3u8 parser/generator"
|
||||
homepage = "https://github.com/sile/hls_m3u8"
|
||||
repository = "https://github.com/sile/hls_m3u8"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["hls", "m3u8"]
|
||||
edition = "2018"
|
||||
categories = ["parser-implementations"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
perf = []
|
||||
|
||||
[badges]
|
||||
travis-ci = {repository = "sile/hls_m3u8"}
|
||||
codecov = {repository = "sile/hls_m3u8"}
|
||||
codecov = { repository = "sile/hls_m3u8" }
|
||||
travis-ci = { repository = "sile/hls_m3u8" }
|
||||
|
||||
[dependencies]
|
||||
trackable = "0.2"
|
||||
chrono = { version = "0.4", optional = true }
|
||||
backtrace = { version = "0.3", features = ["std"], optional = true }
|
||||
|
||||
derive_builder = "0.20"
|
||||
hex = "0.4"
|
||||
thiserror = "1.0"
|
||||
|
||||
derive_more = { version = "1", features = [
|
||||
"display",
|
||||
"as_ref",
|
||||
"from",
|
||||
"deref",
|
||||
"deref_mut",
|
||||
] }
|
||||
shorthand = "0.1"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
|
||||
stable-vec = { version = "0.4" }
|
||||
|
||||
[dev-dependencies]
|
||||
clap = "2"
|
||||
pretty_assertions = "1.4.0"
|
||||
version-sync = "0.9"
|
||||
automod = "1.0.14"
|
||||
criterion = "0.5.1"
|
||||
|
||||
[[bench]]
|
||||
name = "bench_main"
|
||||
harness = false
|
||||
|
|
202
LICENSE-APACHE
Normal file
202
LICENSE-APACHE
Normal file
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2019 Takeru Ohta <phjgt308@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
21
README.md
21
README.md
|
@ -1,11 +1,11 @@
|
|||
hls_m3u8
|
||||
=========
|
||||
|
||||
[![Crates.io: hls_m3u8](http://meritbadge.herokuapp.com/hls_m3u8)](https://crates.io/crates/hls_m3u8)
|
||||
[![Crates.io: hls_m3u8](https://img.shields.io/crates/v/hls_m3u8.svg)](https://crates.io/crates/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)
|
||||
[![Code Coverage](https://codecov.io/gh/sile/hls_m3u8/branch/master/graph/badge.svg)](https://codecov.io/gh/sile/hls_m3u8/branch/master)
|
||||
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
|
||||
![License](https://img.shields.io/crates/l/hls_m3u8)
|
||||
|
||||
[HLS] m3u8 parser/generator.
|
||||
|
||||
|
@ -32,3 +32,20 @@ http://media.example.com/third.ts
|
|||
|
||||
assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
- Apache License, Version 2.0
|
||||
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license
|
||||
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
## Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
|
|
7
benches/bench_main.rs
Normal file
7
benches/bench_main.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use criterion::criterion_main;
|
||||
|
||||
mod benchmarks;
|
||||
|
||||
criterion_main! {
|
||||
benchmarks::media_playlist::benches,
|
||||
}
|
90
benches/benchmarks/media_playlist.rs
Normal file
90
benches/benchmarks/media_playlist.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{black_box, criterion_group, Criterion, Throughput};
|
||||
|
||||
use hls_m3u8::tags::{ExtXDateRange, ExtXProgramDateTime};
|
||||
use hls_m3u8::types::Value;
|
||||
use hls_m3u8::{MediaPlaylist, MediaSegment};
|
||||
|
||||
fn create_manifest_data() -> Vec<u8> {
|
||||
let mut builder = MediaPlaylist::builder();
|
||||
|
||||
builder.media_sequence(826176645);
|
||||
builder.has_independent_segments(true);
|
||||
builder.target_duration(Duration::from_secs(2));
|
||||
|
||||
for i in 0..4000 {
|
||||
let mut seg = MediaSegment::builder();
|
||||
seg.duration(Duration::from_secs_f64(1.92)).uri(format!(
|
||||
"avc_unencrypted_global-video=3000000-{}.ts?variant=italy",
|
||||
826176659 + i
|
||||
));
|
||||
|
||||
if i == 0 {
|
||||
seg.program_date_time(ExtXProgramDateTime::new("2020-04-07T11:32:38Z"));
|
||||
}
|
||||
|
||||
if i % 100 == 0 {
|
||||
seg.date_range(
|
||||
ExtXDateRange::builder()
|
||||
.id(format!("date_id_{}", i / 100))
|
||||
.start_date("2020-04-07T11:40:02.040000Z")
|
||||
.duration(Duration::from_secs_f64(65.2))
|
||||
.insert_client_attribute(
|
||||
"SCTE35-OUT",
|
||||
Value::Hex(
|
||||
hex::decode(concat!(
|
||||
"FC30250000",
|
||||
"0000000000",
|
||||
"FFF0140500",
|
||||
"001C207FEF",
|
||||
"FE0030E3A0",
|
||||
"FE005989E0",
|
||||
"0001000000",
|
||||
"0070BA5ABF"
|
||||
))
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
builder.push_segment(seg.build().unwrap());
|
||||
}
|
||||
|
||||
builder.build().unwrap().to_string().into_bytes()
|
||||
}
|
||||
|
||||
fn media_playlist_from_str(c: &mut Criterion) {
|
||||
let data = String::from_utf8(create_manifest_data()).unwrap();
|
||||
|
||||
let mut group = c.benchmark_group("MediaPlaylist::from_str");
|
||||
|
||||
group.throughput(Throughput::Bytes(data.len() as u64));
|
||||
|
||||
group.bench_function("MediaPlaylist::from_str", |b| {
|
||||
b.iter(|| MediaPlaylist::from_str(black_box(&data)).unwrap());
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn media_playlist_try_from(c: &mut Criterion) {
|
||||
let data = String::from_utf8(create_manifest_data()).unwrap();
|
||||
|
||||
let mut group = c.benchmark_group("MediaPlaylist::try_from");
|
||||
|
||||
group.throughput(Throughput::Bytes(data.len() as u64));
|
||||
|
||||
group.bench_function("MediaPlaylist::try_from", |b| {
|
||||
b.iter(|| MediaPlaylist::try_from(black_box(data.as_str())).unwrap());
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, media_playlist_from_str, media_playlist_try_from);
|
1
benches/benchmarks/mod.rs
Normal file
1
benches/benchmarks/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod media_playlist;
|
|
@ -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,39 +0,0 @@
|
|||
extern crate clap;
|
||||
extern crate hls_m3u8;
|
||||
#[macro_use]
|
||||
extern crate trackable;
|
||||
|
||||
use std::io::{self, Read};
|
||||
use clap::{App, Arg};
|
||||
use hls_m3u8::{MasterPlaylist, MediaPlaylist};
|
||||
use trackable::error::Failure;
|
||||
|
||||
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();
|
||||
track_try_unwrap!(
|
||||
io::stdin()
|
||||
.read_to_string(&mut m3u8)
|
||||
.map_err(Failure::from_error)
|
||||
);
|
||||
|
||||
match matches.value_of("M3U8_TYPE").unwrap() {
|
||||
"media" => {
|
||||
let playlist: MediaPlaylist = track_try_unwrap!(m3u8.parse());
|
||||
println!("{}", playlist);
|
||||
}
|
||||
"master" => {
|
||||
let playlist: MasterPlaylist = track_try_unwrap!(m3u8.parse());
|
||||
println!("{}", playlist);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
15
rustfmt.toml
Normal file
15
rustfmt.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
error_on_unformatted = true
|
||||
edition = "2018"
|
||||
fn_single_line = true
|
||||
force_multiline_blocks = true
|
||||
|
||||
format_code_in_doc_comments = true
|
||||
format_macro_matchers = true
|
||||
format_macro_bodies = true
|
||||
|
||||
match_arm_blocks = true
|
||||
reorder_impl_items = true
|
||||
use_field_init_shorthand = true
|
||||
wrap_comments = true
|
||||
condense_wildcard_suffixes = true
|
||||
unstable_features = true
|
265
src/attribute.rs
265
src/attribute.rs
|
@ -1,104 +1,201 @@
|
|||
use std::collections::HashSet;
|
||||
use std::str;
|
||||
use core::iter::FusedIterator;
|
||||
|
||||
use {ErrorKind, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AttributePairs<'a> {
|
||||
input: &'a str,
|
||||
visited_keys: HashSet<&'a str>,
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AttributePairs<'a> {
|
||||
string: &'a str,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> AttributePairs<'a> {
|
||||
pub fn parse(input: &'a str) -> Self {
|
||||
AttributePairs {
|
||||
input,
|
||||
visited_keys: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_name(&mut self) -> Result<&'a str> {
|
||||
for i in 0..self.input.len() {
|
||||
match self.input.as_bytes()[i] {
|
||||
b'=' => {
|
||||
let (key, _) = self.input.split_at(i);
|
||||
let (_, rest) = self.input.split_at(i + 1);
|
||||
self.input = rest;
|
||||
return Ok(key);
|
||||
}
|
||||
b'A'...b'Z' | b'0'...b'9' | b'-' => {}
|
||||
_ => track_panic!(
|
||||
ErrorKind::InvalidInput,
|
||||
"Malformed attribute name: {:?}",
|
||||
self.input
|
||||
),
|
||||
}
|
||||
}
|
||||
track_panic!(
|
||||
ErrorKind::InvalidInput,
|
||||
"No attribute value: {:?}",
|
||||
self.input
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_raw_value(&mut self) -> &'a str {
|
||||
let mut in_quote = false;
|
||||
let mut value_end = self.input.len();
|
||||
let mut next = self.input.len();
|
||||
for (i, c) in self.input.bytes().enumerate() {
|
||||
match c {
|
||||
b'"' => {
|
||||
in_quote = !in_quote;
|
||||
}
|
||||
b',' if !in_quote => {
|
||||
value_end = i;
|
||||
next = i + 1;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let (value, _) = self.input.split_at(value_end);
|
||||
let (_, rest) = self.input.split_at(next);
|
||||
self.input = rest;
|
||||
value
|
||||
}
|
||||
pub const fn new(string: &'a str) -> Self { Self { string, index: 0 } }
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AttributePairs<'a> {
|
||||
type Item = Result<(&'a str, &'a str)>;
|
||||
type Item = (&'a str, &'a str);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.input.is_empty() {
|
||||
return None;
|
||||
// return `None`, if there are no more bytes
|
||||
self.string.as_bytes().get(self.index + 1)?;
|
||||
|
||||
let key = {
|
||||
// the position in the string:
|
||||
let start = self.index;
|
||||
// the key ends at an `=`:
|
||||
let end = self.string[self.index..]
|
||||
.char_indices()
|
||||
.find_map(|(i, c)| if c == '=' { Some(i) } else { None })?
|
||||
+ self.index;
|
||||
|
||||
// advance the index to the char after the end of the key (to skip the `=`)
|
||||
// NOTE: it is okay to add 1 to the index, because an `=` is exactly 1 byte.
|
||||
self.index = end + 1;
|
||||
|
||||
// NOTE: See https://github.com/sile/hls_m3u8/issues/64
|
||||
self.string[start..end].trim()
|
||||
};
|
||||
|
||||
let value = {
|
||||
let start = self.index;
|
||||
|
||||
// find the end of the value by searching for `,`.
|
||||
// it should ignore `,` that are inside double quotes.
|
||||
let mut inside_quotes = false;
|
||||
|
||||
let end = {
|
||||
let mut result = self.string.len();
|
||||
|
||||
for (i, c) in self.string[self.index..].char_indices() {
|
||||
// if a quote is encountered
|
||||
if c == '"' {
|
||||
// update variable
|
||||
inside_quotes = !inside_quotes;
|
||||
// terminate if a comma is encountered, which is not in a
|
||||
// quote
|
||||
} else if c == ',' && !inside_quotes {
|
||||
// move the index past the comma
|
||||
self.index += 1;
|
||||
// the result is the index of the comma (comma is not included in the
|
||||
// resulting string)
|
||||
result = i + self.index - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
|
||||
self.index += end;
|
||||
self.index -= start;
|
||||
|
||||
// NOTE: See https://github.com/sile/hls_m3u8/issues/64
|
||||
self.string[start..end].trim()
|
||||
};
|
||||
|
||||
Some((key, value))
|
||||
}
|
||||
|
||||
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[self.index..].char_indices() {
|
||||
if c == '=' && !inside_quotes {
|
||||
remaining += 1;
|
||||
} else if c == '"' {
|
||||
inside_quotes = !inside_quotes;
|
||||
}
|
||||
}
|
||||
|
||||
let result = || -> Result<(&'a str, &'a str)> {
|
||||
let key = track!(self.parse_name())?;
|
||||
track_assert!(
|
||||
self.visited_keys.insert(key),
|
||||
ErrorKind::InvalidInput,
|
||||
"Duplicate attribute key: {:?}",
|
||||
key
|
||||
);
|
||||
|
||||
let value = self.parse_raw_value();
|
||||
Ok((key, value))
|
||||
}();
|
||||
Some(result)
|
||||
(remaining, Some(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ExactSizeIterator for AttributePairs<'a> {}
|
||||
impl<'a> FusedIterator for AttributePairs<'a> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let mut pairs = AttributePairs::parse("FOO=BAR,BAR=\"baz,qux\",ABC=12.3");
|
||||
assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("FOO", "BAR"))));
|
||||
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]
|
||||
fn test_parser() {
|
||||
let mut pairs = AttributePairs::new("FOO=BAR,BAR=\"baz,qux\",ABC=12.3");
|
||||
|
||||
assert_eq!(pairs.next(), Some(("FOO", "BAR")));
|
||||
assert_eq!(pairs.next(), Some(("BAR", "\"baz,qux\"")));
|
||||
assert_eq!(pairs.next(), Some(("ABC", "12.3")));
|
||||
assert_eq!(pairs.next(), None);
|
||||
|
||||
// stress test with foreign input
|
||||
// got it from https://generator.lorem-ipsum.info/_chinese
|
||||
|
||||
let mut pairs = AttributePairs::new(concat!(
|
||||
"載抗留囲軽来実基供全必式覧領意度振。=著地内方満職控努作期投綱研本模,",
|
||||
"後文図様改表宮能本園半参裁報作神掲索=\"針支年得率新賞現報発援白少動面。矢拉年世掲注索政平定他込\",",
|
||||
"ध्वनि स्थिति और्४५० नीचे =देखने लाभो द्वारा करके(विशेष"
|
||||
));
|
||||
|
||||
assert_eq!((3, Some(3)), pairs.size_hint());
|
||||
assert_eq!(
|
||||
pairs.next().map(|x| x.ok()),
|
||||
Some(Some(("BAR", "\"baz,qux\"")))
|
||||
pairs.next(),
|
||||
Some((
|
||||
"載抗留囲軽来実基供全必式覧領意度振。",
|
||||
"著地内方満職控努作期投綱研本模"
|
||||
))
|
||||
);
|
||||
assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("ABC", "12.3"))));
|
||||
assert_eq!(pairs.next().map(|x| x.ok()), None)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
241
src/error.rs
241
src/error.rs
|
@ -1,14 +1,235 @@
|
|||
use trackable::error::{ErrorKind as TrackableErrorKind, TrackableError};
|
||||
use std::fmt;
|
||||
|
||||
/// This crate specific `Error` type.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Error(TrackableError<ErrorKind>);
|
||||
derive_traits_for_trackable_error_newtype!(Error, ErrorKind);
|
||||
#[cfg(feature = "backtrace")]
|
||||
use backtrace::Backtrace;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Possible error kinds.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ErrorKind {
|
||||
//use crate::types::ProtocolVersion;
|
||||
|
||||
/// This crate specific `Result` type.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
enum ErrorKind {
|
||||
#[error("a value is missing for the attribute {value}")]
|
||||
MissingValue { value: String },
|
||||
|
||||
#[error("invalid input")]
|
||||
InvalidInput,
|
||||
|
||||
#[error("{source}: {input:?}")]
|
||||
ParseIntError {
|
||||
input: String,
|
||||
source: ::std::num::ParseIntError,
|
||||
},
|
||||
|
||||
#[error("{source}: {input:?}")]
|
||||
ParseFloatError {
|
||||
input: String,
|
||||
source: ::std::num::ParseFloatError,
|
||||
},
|
||||
|
||||
#[error("expected `{tag}` at the start of {input:?}")]
|
||||
MissingTag {
|
||||
/// The required tag.
|
||||
tag: String,
|
||||
/// The unparsed input data.
|
||||
input: String,
|
||||
},
|
||||
|
||||
#[error("{0}")]
|
||||
Custom(String),
|
||||
|
||||
#[error("unmatched group: {0:?}")]
|
||||
UnmatchedGroup(String),
|
||||
|
||||
#[error("unknown protocol version {0:?}")]
|
||||
UnknownProtocolVersion(String),
|
||||
|
||||
// #[error("required_version: {:?}, specified_version: {:?}", _0, _1)]
|
||||
// VersionError(ProtocolVersion, ProtocolVersion),
|
||||
#[error("missing attribute: {attribute:?}")]
|
||||
MissingAttribute { attribute: String },
|
||||
|
||||
#[error("unexpected attribute: {attribute:?}")]
|
||||
UnexpectedAttribute { attribute: String },
|
||||
|
||||
#[error("unexpected tag: {tag:?}")]
|
||||
UnexpectedTag { tag: String },
|
||||
|
||||
#[error("{source}")]
|
||||
#[cfg(feature = "chrono")]
|
||||
Chrono { source: chrono::ParseError },
|
||||
|
||||
#[error("builder error: {message}")]
|
||||
Builder { message: String },
|
||||
|
||||
#[error("{source}")]
|
||||
Hex { source: hex::FromHexError },
|
||||
}
|
||||
|
||||
/// The Error type of this library.
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
inner: ErrorKind,
|
||||
#[cfg(feature = "backtrace")]
|
||||
backtrace: Backtrace,
|
||||
}
|
||||
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool { self.inner == other.inner }
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) }
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
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 {
|
||||
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 {
|
||||
Self::new(ErrorKind::UnexpectedAttribute {
|
||||
attribute: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unexpected_tag<T: ToString>(value: T) -> Self {
|
||||
Self::new(ErrorKind::UnexpectedTag {
|
||||
tag: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_input() -> Self { Self::new(ErrorKind::InvalidInput) }
|
||||
|
||||
pub(crate) fn parse_int<T: fmt::Display>(input: T, source: ::std::num::ParseIntError) -> Self {
|
||||
Self::new(ErrorKind::ParseIntError {
|
||||
input: input.to_string(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_float<T: fmt::Display>(
|
||||
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
|
||||
where
|
||||
T: ToString,
|
||||
U: ToString,
|
||||
{
|
||||
Self::new(ErrorKind::MissingTag {
|
||||
tag: tag.to_string(),
|
||||
input: input.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unmatched_group<T: ToString>(value: T) -> Self {
|
||||
Self::new(ErrorKind::UnmatchedGroup(value.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn unknown_protocol_version<T: ToString>(value: T) -> Self {
|
||||
Self::new(ErrorKind::UnknownProtocolVersion(value.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn builder<T: ToString>(value: T) -> Self {
|
||||
Self::new(ErrorKind::Builder {
|
||||
message: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn missing_attribute<T: ToString>(value: T) -> Self {
|
||||
Self::new(ErrorKind::MissingAttribute {
|
||||
attribute: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unexpected_data(value: &str) -> Self {
|
||||
Self::custom(format!("Unexpected data in the line: {:?}", value))
|
||||
}
|
||||
|
||||
// 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()))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl From<::strum::ParseError> for Error {
|
||||
fn from(value: ::strum::ParseError) -> Self { Self::strum(value) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parse_float_error() {
|
||||
assert_eq!(
|
||||
Error::parse_float(
|
||||
"1.x234",
|
||||
"1.x234"
|
||||
.parse::<f32>()
|
||||
.expect_err("this should not parse as a float!")
|
||||
)
|
||||
.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()
|
||||
);
|
||||
}
|
||||
}
|
||||
impl TrackableErrorKind for ErrorKind {}
|
||||
|
|
142
src/lib.rs
142
src/lib.rs
|
@ -1,44 +1,140 @@
|
|||
#![doc(html_root_url = "https://docs.rs/hls_m3u8/0.4.2")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
#![warn(clippy::cargo, clippy::inline_always)]
|
||||
#![allow(
|
||||
clippy::non_ascii_literal,
|
||||
clippy::redundant_pub_crate,
|
||||
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::unneeded_field_pattern,
|
||||
clippy::wrong_self_convention
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::expect_used))]
|
||||
// those should not be present in production code:
|
||||
#![deny(
|
||||
clippy::print_stdout,
|
||||
clippy::todo,
|
||||
clippy::unimplemented,
|
||||
clippy::dbg_macro,
|
||||
clippy::use_debug
|
||||
)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
missing_copy_implementations,
|
||||
missing_debug_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts
|
||||
)]
|
||||
//! [HLS] m3u8 parser/generator.
|
||||
//!
|
||||
//! [HLS]: https://tools.ietf.org/html/rfc8216
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! use hls_m3u8::MediaPlaylist;
|
||||
//! use std::convert::TryFrom;
|
||||
//!
|
||||
//! let m3u8 = "#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";
|
||||
//! let m3u8 = MediaPlaylist::try_from(concat!(
|
||||
//! "#EXTM3U\n",
|
||||
//! "#EXT-X-TARGETDURATION:10\n",
|
||||
//! "#EXT-X-VERSION:3\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",
|
||||
//! ));
|
||||
//!
|
||||
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
||||
//! assert!(m3u8.is_ok());
|
||||
//! ```
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(const_static_lifetime))]
|
||||
#[macro_use]
|
||||
extern crate trackable;
|
||||
//!
|
||||
//! ## 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, ErrorKind};
|
||||
pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder};
|
||||
pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder};
|
||||
pub use media_segment::{MediaSegment, MediaSegmentBuilder};
|
||||
pub use error::Error;
|
||||
pub use master_playlist::MasterPlaylist;
|
||||
pub use media_playlist::MediaPlaylist;
|
||||
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 types;
|
||||
|
||||
#[macro_use]
|
||||
mod utils;
|
||||
mod attribute;
|
||||
mod error;
|
||||
mod line;
|
||||
mod master_playlist;
|
||||
mod media_playlist;
|
||||
mod media_segment;
|
||||
mod traits;
|
||||
|
||||
/// This crate specific `Result` type.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub use error::Result;
|
||||
pub use stable_vec;
|
||||
pub use traits::*;
|
||||
|
|
270
src/line.rs
270
src/line.rs
|
@ -1,189 +1,135 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use core::convert::TryFrom;
|
||||
use core::iter::FusedIterator;
|
||||
|
||||
use {Error, ErrorKind, Result};
|
||||
use tags;
|
||||
use types::SingleLineString;
|
||||
use derive_more::Display;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Lines<'a> {
|
||||
input: &'a str,
|
||||
use crate::tags;
|
||||
use crate::types::PlaylistType;
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Lines<'a> {
|
||||
lines: ::core::iter::FilterMap<::core::str::Lines<'a>, fn(&'a str) -> Option<&'a str>>,
|
||||
}
|
||||
impl<'a> Lines<'a> {
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Lines { input }
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Result<Line<'a>> {
|
||||
let mut end = self.input.len();
|
||||
let mut next_start = self.input.len();
|
||||
let mut adjust = 0;
|
||||
let mut next_line_of_ext_x_stream_inf = false;
|
||||
for (i, c) in self.input.char_indices() {
|
||||
match c {
|
||||
'\n' => {
|
||||
if !next_line_of_ext_x_stream_inf
|
||||
&& self.input.starts_with(tags::ExtXStreamInf::PREFIX)
|
||||
{
|
||||
next_line_of_ext_x_stream_inf = true;
|
||||
adjust = 0;
|
||||
continue;
|
||||
}
|
||||
next_start = i + 1;
|
||||
end = i - adjust;
|
||||
break;
|
||||
}
|
||||
'\r' => {
|
||||
adjust = 1;
|
||||
}
|
||||
_ => {
|
||||
track_assert!(!c.is_control(), ErrorKind::InvalidInput);
|
||||
adjust = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
let raw_line = &self.input[..end];
|
||||
let line = if raw_line.is_empty() {
|
||||
Line::Blank
|
||||
} else if raw_line.starts_with("#EXT") {
|
||||
Line::Tag(track!(raw_line.parse())?)
|
||||
} else if raw_line.starts_with('#') {
|
||||
Line::Comment(raw_line)
|
||||
} else {
|
||||
let uri = track!(SingleLineString::new(raw_line))?;
|
||||
Line::Uri(uri)
|
||||
};
|
||||
self.input = &self.input[next_start..];
|
||||
Ok(line)
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for Lines<'a> {
|
||||
type Item = Result<Line<'a>>;
|
||||
type Item = crate::Result<Line<'a>>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match track!(self.read_line()) {
|
||||
Err(e) => Some(Err(e)),
|
||||
Ok(line) => Some(Ok(line)),
|
||||
let line = self.lines.next()?;
|
||||
|
||||
if line.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) {
|
||||
let uri = self.lines.next()?;
|
||||
|
||||
Some(
|
||||
tags::VariantStream::try_from(format!("{}\n{}", line, uri).as_str())
|
||||
.map(tags::VariantStream::into_owned)
|
||||
.map(|v| Line::Tag(Tag::VariantStream(v))),
|
||||
)
|
||||
} else if line.starts_with("#EXT") {
|
||||
Some(Tag::try_from(line).map(Line::Tag))
|
||||
} else if line.starts_with('#') {
|
||||
Some(Ok(Line::Comment(line)))
|
||||
} else {
|
||||
Some(Ok(Line::Uri(line)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(large_enum_variant))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Line<'a> {
|
||||
Blank,
|
||||
Comment(&'a str),
|
||||
Tag(Tag),
|
||||
Uri(SingleLineString),
|
||||
impl<'a> FusedIterator for Lines<'a> {}
|
||||
|
||||
impl<'a> From<&'a str> for Lines<'a> {
|
||||
fn from(buffer: &'a str) -> Self {
|
||||
Self {
|
||||
lines: buffer
|
||||
.lines()
|
||||
.filter_map(|line| Some(line.trim()).filter(|v| !v.is_empty())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(large_enum_variant))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Tag {
|
||||
ExtM3u(tags::ExtM3u),
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Line<'a> {
|
||||
Tag(Tag<'a>),
|
||||
Comment(&'a str),
|
||||
Uri(&'a str),
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, PartialEq, Display)]
|
||||
#[display("{_variant}")]
|
||||
pub(crate) enum Tag<'a> {
|
||||
ExtXVersion(tags::ExtXVersion),
|
||||
ExtInf(tags::ExtInf),
|
||||
ExtInf(tags::ExtInf<'a>),
|
||||
ExtXByteRange(tags::ExtXByteRange),
|
||||
ExtXDiscontinuity(tags::ExtXDiscontinuity),
|
||||
ExtXKey(tags::ExtXKey),
|
||||
ExtXMap(tags::ExtXMap),
|
||||
ExtXProgramDateTime(tags::ExtXProgramDateTime),
|
||||
ExtXDateRange(tags::ExtXDateRange),
|
||||
ExtXKey(tags::ExtXKey<'a>),
|
||||
ExtXMap(tags::ExtXMap<'a>),
|
||||
ExtXProgramDateTime(tags::ExtXProgramDateTime<'a>),
|
||||
ExtXDateRange(tags::ExtXDateRange<'a>),
|
||||
ExtXTargetDuration(tags::ExtXTargetDuration),
|
||||
ExtXMediaSequence(tags::ExtXMediaSequence),
|
||||
ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence),
|
||||
ExtXEndList(tags::ExtXEndList),
|
||||
ExtXPlaylistType(tags::ExtXPlaylistType),
|
||||
PlaylistType(PlaylistType),
|
||||
ExtXIFramesOnly(tags::ExtXIFramesOnly),
|
||||
ExtXMedia(tags::ExtXMedia),
|
||||
ExtXStreamInf(tags::ExtXStreamInf),
|
||||
ExtXIFrameStreamInf(tags::ExtXIFrameStreamInf),
|
||||
ExtXSessionData(tags::ExtXSessionData),
|
||||
ExtXSessionKey(tags::ExtXSessionKey),
|
||||
ExtXMedia(tags::ExtXMedia<'a>),
|
||||
ExtXSessionData(tags::ExtXSessionData<'a>),
|
||||
ExtXSessionKey(tags::ExtXSessionKey<'a>),
|
||||
ExtXIndependentSegments(tags::ExtXIndependentSegments),
|
||||
ExtXStart(tags::ExtXStart),
|
||||
Unknown(SingleLineString),
|
||||
VariantStream(tags::VariantStream<'a>),
|
||||
Unknown(&'a str),
|
||||
}
|
||||
impl fmt::Display for Tag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Tag::ExtM3u(ref t) => t.fmt(f),
|
||||
Tag::ExtXVersion(ref t) => t.fmt(f),
|
||||
Tag::ExtInf(ref t) => t.fmt(f),
|
||||
Tag::ExtXByteRange(ref t) => t.fmt(f),
|
||||
Tag::ExtXDiscontinuity(ref t) => t.fmt(f),
|
||||
Tag::ExtXKey(ref t) => t.fmt(f),
|
||||
Tag::ExtXMap(ref t) => t.fmt(f),
|
||||
Tag::ExtXProgramDateTime(ref t) => t.fmt(f),
|
||||
Tag::ExtXDateRange(ref t) => t.fmt(f),
|
||||
Tag::ExtXTargetDuration(ref t) => t.fmt(f),
|
||||
Tag::ExtXMediaSequence(ref t) => t.fmt(f),
|
||||
Tag::ExtXDiscontinuitySequence(ref t) => t.fmt(f),
|
||||
Tag::ExtXEndList(ref t) => t.fmt(f),
|
||||
Tag::ExtXPlaylistType(ref t) => t.fmt(f),
|
||||
Tag::ExtXIFramesOnly(ref t) => t.fmt(f),
|
||||
Tag::ExtXMedia(ref t) => t.fmt(f),
|
||||
Tag::ExtXStreamInf(ref t) => t.fmt(f),
|
||||
Tag::ExtXIFrameStreamInf(ref t) => t.fmt(f),
|
||||
Tag::ExtXSessionData(ref t) => t.fmt(f),
|
||||
Tag::ExtXSessionKey(ref t) => t.fmt(f),
|
||||
Tag::ExtXIndependentSegments(ref t) => t.fmt(f),
|
||||
Tag::ExtXStart(ref t) => t.fmt(f),
|
||||
Tag::Unknown(ref t) => t.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for Tag {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
if s.starts_with(tags::ExtM3u::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtM3u))
|
||||
} else if s.starts_with(tags::ExtXVersion::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXVersion))
|
||||
} else if s.starts_with(tags::ExtInf::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtInf))
|
||||
} else if s.starts_with(tags::ExtXByteRange::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXByteRange))
|
||||
} else if s.starts_with(tags::ExtXDiscontinuity::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXDiscontinuity))
|
||||
} else if s.starts_with(tags::ExtXKey::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXKey))
|
||||
} else if s.starts_with(tags::ExtXMap::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXMap))
|
||||
} else if s.starts_with(tags::ExtXProgramDateTime::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXProgramDateTime))
|
||||
} else if s.starts_with(tags::ExtXTargetDuration::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXTargetDuration))
|
||||
} else if s.starts_with(tags::ExtXDateRange::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXDateRange))
|
||||
} else if s.starts_with(tags::ExtXMediaSequence::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXMediaSequence))
|
||||
} else if s.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXDiscontinuitySequence))
|
||||
} else if s.starts_with(tags::ExtXEndList::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXEndList))
|
||||
} else if s.starts_with(tags::ExtXPlaylistType::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXPlaylistType))
|
||||
} else if s.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXIFramesOnly))
|
||||
} else if s.starts_with(tags::ExtXMedia::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXMedia))
|
||||
} else if s.starts_with(tags::ExtXStreamInf::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXStreamInf))
|
||||
} else if s.starts_with(tags::ExtXIFrameStreamInf::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXIFrameStreamInf))
|
||||
} else if s.starts_with(tags::ExtXSessionData::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXSessionData))
|
||||
} else if s.starts_with(tags::ExtXSessionKey::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXSessionKey))
|
||||
} else if s.starts_with(tags::ExtXIndependentSegments::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXIndependentSegments))
|
||||
} else if s.starts_with(tags::ExtXStart::PREFIX) {
|
||||
track!(s.parse().map(Tag::ExtXStart))
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Tag<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
if input.starts_with(tags::ExtXVersion::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXVersion)
|
||||
} else if input.starts_with(tags::ExtInf::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtInf)
|
||||
} else if input.starts_with(tags::ExtXByteRange::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXByteRange)
|
||||
} else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXDiscontinuitySequence)
|
||||
} else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXDiscontinuity)
|
||||
} else if input.starts_with(tags::ExtXKey::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXKey)
|
||||
} else if input.starts_with(tags::ExtXMap::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXMap)
|
||||
} else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXProgramDateTime)
|
||||
} else if input.starts_with(tags::ExtXTargetDuration::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXTargetDuration)
|
||||
} else if input.starts_with(tags::ExtXDateRange::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXDateRange)
|
||||
} else if input.starts_with(tags::ExtXMediaSequence::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXMediaSequence)
|
||||
} else if input.starts_with(tags::ExtXEndList::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXEndList)
|
||||
} else if input.starts_with(PlaylistType::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::PlaylistType)
|
||||
} else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXIFramesOnly)
|
||||
} else if input.starts_with(tags::ExtXMedia::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXMedia)
|
||||
} else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME)
|
||||
|| input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF)
|
||||
{
|
||||
TryFrom::try_from(input).map(Self::VariantStream)
|
||||
} else if input.starts_with(tags::ExtXSessionData::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXSessionData)
|
||||
} else if input.starts_with(tags::ExtXSessionKey::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXSessionKey)
|
||||
} else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXIndependentSegments)
|
||||
} else if input.starts_with(tags::ExtXStart::PREFIX) {
|
||||
TryFrom::try_from(input).map(Self::ExtXStart)
|
||||
} else {
|
||||
track!(SingleLineString::new(s)).map(Tag::Unknown)
|
||||
Ok(Self::Unknown(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,173 +1,313 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::iter;
|
||||
|
||||
use {ErrorKind, Result};
|
||||
use tags::{ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap,
|
||||
ExtXProgramDateTime, MediaSegmentTag};
|
||||
use types::{ProtocolVersion, SingleLineString};
|
||||
use derive_builder::Builder;
|
||||
use shorthand::ShortHand;
|
||||
|
||||
/// Media segment builder.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaSegmentBuilder {
|
||||
key_tags: Vec<ExtXKey>,
|
||||
map_tag: Option<ExtXMap>,
|
||||
byte_range_tag: Option<ExtXByteRange>,
|
||||
date_range_tag: Option<ExtXDateRange>,
|
||||
discontinuity_tag: Option<ExtXDiscontinuity>,
|
||||
program_date_time_tag: Option<ExtXProgramDateTime>,
|
||||
inf_tag: Option<ExtInf>,
|
||||
uri: Option<SingleLineString>,
|
||||
use crate::tags::{
|
||||
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
||||
};
|
||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||
use crate::{Decryptable, RequiredVersion};
|
||||
|
||||
/// A video is split into smaller chunks called [`MediaSegment`]s, which are
|
||||
/// specified by a uri and optionally a byte range.
|
||||
///
|
||||
/// 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<'a> {
|
||||
/// 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<'a>>,
|
||||
/// 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)]
|
||||
pub map: Option<ExtXMap<'a>>,
|
||||
/// 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)]
|
||||
pub date_range: Option<ExtXDateRange<'a>>,
|
||||
/// 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)]
|
||||
pub has_discontinuity: bool,
|
||||
/// This field associates the first sample of a media segment with an
|
||||
/// absolute date and/or time.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(default)]
|
||||
pub program_date_time: Option<ExtXProgramDateTime<'a>>,
|
||||
/// This field indicates the duration of a media segment.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is required.
|
||||
#[builder(setter(into))]
|
||||
pub duration: ExtInf<'a>,
|
||||
/// The URI of a media segment.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is required.
|
||||
#[builder(setter(into))]
|
||||
#[shorthand(enable(into), disable(skip))]
|
||||
uri: Cow<'a, str>,
|
||||
}
|
||||
impl MediaSegmentBuilder {
|
||||
/// Makes a new `MediaSegmentBuilder` instance.
|
||||
pub fn new() -> Self {
|
||||
MediaSegmentBuilder {
|
||||
key_tags: Vec::new(),
|
||||
map_tag: None,
|
||||
byte_range_tag: None,
|
||||
date_range_tag: None,
|
||||
discontinuity_tag: None,
|
||||
program_date_time_tag: None,
|
||||
inf_tag: None,
|
||||
uri: None,
|
||||
|
||||
impl<'a> MediaSegment<'a> {
|
||||
/// Returns a builder for a [`MediaSegment`].
|
||||
///
|
||||
/// # 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::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn builder() -> MediaSegmentBuilder<'static> { MediaSegmentBuilder::default() }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
pub fn into_owned(self) -> MediaSegment<'static> {
|
||||
MediaSegment {
|
||||
number: self.number,
|
||||
explicit_number: self.explicit_number,
|
||||
keys: self.keys.into_iter().map(|k| k.into_owned()).collect(),
|
||||
map: self.map.map(|v| v.into_owned()),
|
||||
byte_range: self.byte_range,
|
||||
date_range: self.date_range.map(|v| v.into_owned()),
|
||||
has_discontinuity: self.has_discontinuity,
|
||||
program_date_time: self.program_date_time.map(|v| v.into_owned()),
|
||||
duration: self.duration.into_owned(),
|
||||
uri: Cow::Owned(self.uri.into_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MediaSegmentBuilder<'a> {
|
||||
/// Pushes an [`ExtXKey`] tag.
|
||||
pub fn push_key<VALUE: Into<ExtXKey<'a>>>(&mut self, value: VALUE) -> &mut Self {
|
||||
if let Some(keys) = &mut self.keys {
|
||||
keys.push(value.into());
|
||||
} else {
|
||||
self.keys = Some(vec![value.into()]);
|
||||
}
|
||||
|
||||
/// Sets the URI of the resulting media segment.
|
||||
pub fn uri(&mut self, uri: SingleLineString) -> &mut Self {
|
||||
self.uri = Some(uri);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the given tag to the resulting media segment.
|
||||
pub fn tag<T: Into<MediaSegmentTag>>(&mut self, tag: T) -> &mut Self {
|
||||
match tag.into() {
|
||||
MediaSegmentTag::ExtInf(t) => self.inf_tag = Some(t),
|
||||
MediaSegmentTag::ExtXByteRange(t) => self.byte_range_tag = Some(t),
|
||||
MediaSegmentTag::ExtXDateRange(t) => self.date_range_tag = Some(t),
|
||||
MediaSegmentTag::ExtXDiscontinuity(t) => self.discontinuity_tag = Some(t),
|
||||
MediaSegmentTag::ExtXKey(t) => self.key_tags.push(t),
|
||||
MediaSegmentTag::ExtXMap(t) => self.map_tag = Some(t),
|
||||
MediaSegmentTag::ExtXProgramDateTime(t) => self.program_date_time_tag = Some(t),
|
||||
}
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Builds a `MediaSegment` instance.
|
||||
pub fn finish(self) -> Result<MediaSegment> {
|
||||
let uri = track_assert_some!(self.uri, ErrorKind::InvalidInput);
|
||||
let inf_tag = track_assert_some!(self.inf_tag, ErrorKind::InvalidInput);
|
||||
Ok(MediaSegment {
|
||||
key_tags: self.key_tags,
|
||||
map_tag: self.map_tag,
|
||||
byte_range_tag: self.byte_range_tag,
|
||||
date_range_tag: self.date_range_tag,
|
||||
discontinuity_tag: self.discontinuity_tag,
|
||||
program_date_time_tag: self.program_date_time_tag,
|
||||
inf_tag,
|
||||
uri,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Default for MediaSegmentBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Media segment.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaSegment {
|
||||
key_tags: Vec<ExtXKey>,
|
||||
map_tag: Option<ExtXMap>,
|
||||
byte_range_tag: Option<ExtXByteRange>,
|
||||
date_range_tag: Option<ExtXDateRange>,
|
||||
discontinuity_tag: Option<ExtXDiscontinuity>,
|
||||
program_date_time_tag: Option<ExtXProgramDateTime>,
|
||||
inf_tag: ExtInf,
|
||||
uri: SingleLineString,
|
||||
}
|
||||
impl fmt::Display for MediaSegment {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for t in &self.key_tags {
|
||||
writeln!(f, "{}", t)?;
|
||||
impl<'a> fmt::Display for MediaSegment<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// NOTE: self.keys will be printed by the `MediaPlaylist` to prevent redundance.
|
||||
|
||||
if let Some(value) = &self.map {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
if let Some(ref t) = self.map_tag {
|
||||
writeln!(f, "{}", t)?;
|
||||
|
||||
if let Some(value) = &self.byte_range {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
if let Some(ref t) = self.byte_range_tag {
|
||||
writeln!(f, "{}", t)?;
|
||||
|
||||
if let Some(value) = &self.date_range {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
if let Some(ref t) = self.date_range_tag {
|
||||
writeln!(f, "{}", t)?;
|
||||
|
||||
if self.has_discontinuity {
|
||||
writeln!(f, "{}", ExtXDiscontinuity)?;
|
||||
}
|
||||
if let Some(ref t) = self.discontinuity_tag {
|
||||
writeln!(f, "{}", t)?;
|
||||
|
||||
if let Some(value) = &self.program_date_time {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
if let Some(ref t) = self.program_date_time_tag {
|
||||
writeln!(f, "{}", t)?;
|
||||
}
|
||||
writeln!(f, "{}", self.inf_tag)?;
|
||||
|
||||
writeln!(f, "{}", self.duration)?;
|
||||
writeln!(f, "{}", self.uri)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl MediaSegment {
|
||||
/// Returns the URI of the media segment.
|
||||
pub fn uri(&self) -> &SingleLineString {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-INF` tag associated with the media segment.
|
||||
pub fn inf_tag(&self) -> &ExtInf {
|
||||
&self.inf_tag
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-BYTERANGE` tag associated with the media segment.
|
||||
pub fn byte_range_tag(&self) -> Option<ExtXByteRange> {
|
||||
self.byte_range_tag
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-DATERANGE` tag associated with the media segment.
|
||||
pub fn date_range_tag(&self) -> Option<&ExtXDateRange> {
|
||||
self.date_range_tag.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment.
|
||||
pub fn discontinuity_tag(&self) -> Option<ExtXDiscontinuity> {
|
||||
self.discontinuity_tag
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-PROGRAM-DATE-TIME` tag associated with the media segment.
|
||||
pub fn program_date_time_tag(&self) -> Option<&ExtXProgramDateTime> {
|
||||
self.program_date_time_tag.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-MAP` tag associated with the media segment.
|
||||
pub fn map_tag(&self) -> Option<&ExtXMap> {
|
||||
self.map_tag.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-KEY` tags associated with the media segment.
|
||||
pub fn key_tags(&self) -> &[ExtXKey] {
|
||||
&self.key_tags
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this segment requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
iter::empty()
|
||||
.chain(self.key_tags.iter().map(|t| t.requires_version()))
|
||||
.chain(self.map_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(self.byte_range_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(self.date_range_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(self.discontinuity_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(
|
||||
self.program_date_time_tag
|
||||
.iter()
|
||||
.map(|t| t.requires_version()),
|
||||
)
|
||||
.chain(iter::once(self.inf_tag.requires_version()))
|
||||
.max()
|
||||
.expect("Never fails")
|
||||
impl<'a> RequiredVersion for MediaSegment<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
required_version![
|
||||
self.keys,
|
||||
self.map,
|
||||
self.byte_range,
|
||||
self.date_range,
|
||||
{
|
||||
if self.has_discontinuity {
|
||||
Some(ExtXDiscontinuity)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
self.program_date_time,
|
||||
self.duration
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decryptable<'a> for MediaSegment<'a> {
|
||||
fn keys(&self) -> Vec<&DecryptionKey<'a>> {
|
||||
//
|
||||
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
MediaSegment::builder()
|
||||
.map(ExtXMap::new("https://www.example.com/"))
|
||||
.byte_range(ExtXByteRange::from(5..25))
|
||||
.has_discontinuity(true)
|
||||
.duration(ExtInf::new(Duration::from_secs(4)))
|
||||
.uri("http://www.uri.com/")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
concat!(
|
||||
"#EXT-X-MAP:URI=\"https://www.example.com/\"\n",
|
||||
"#EXT-X-BYTERANGE:20@5\n",
|
||||
"#EXT-X-DISCONTINUITY\n",
|
||||
"#EXTINF:4,\n",
|
||||
"http://www.uri.com/\n"
|
||||
)
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use {Error, ErrorKind, Result};
|
||||
use types::ProtocolVersion;
|
||||
|
||||
/// [4.3.1.1. EXTM3U]
|
||||
///
|
||||
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtM3u;
|
||||
impl ExtM3u {
|
||||
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtM3u {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Self::PREFIX.fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtM3u {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||
Ok(ExtM3u)
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.1.2. EXT-X-VERSION]
|
||||
///
|
||||
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXVersion {
|
||||
version: ProtocolVersion,
|
||||
}
|
||||
impl ExtXVersion {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:";
|
||||
|
||||
/// Makes a new `ExtXVersion` tag.
|
||||
pub fn new(version: ProtocolVersion) -> Self {
|
||||
ExtXVersion { version }
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version of the playlist containing this tag.
|
||||
pub fn version(&self) -> ProtocolVersion {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.version)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXVersion {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
||||
let version = track!(suffix.parse())?;
|
||||
Ok(ExtXVersion { version })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extm3u() {
|
||||
assert_eq!("#EXTM3U".parse::<ExtM3u>().ok(), Some(ExtM3u));
|
||||
assert_eq!(ExtM3u.to_string(), "#EXTM3U");
|
||||
assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_version() {
|
||||
let tag = ExtXVersion::new(ProtocolVersion::V6);
|
||||
assert_eq!("#EXT-X-VERSION:6".parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), "#EXT-X-VERSION:6");
|
||||
assert_eq!(tag.version(), ProtocolVersion::V6);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
}
|
60
src/tags/basic/m3u.rs
Normal file
60
src/tags/basic/m3u.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]**
|
||||
/// Playlist file.
|
||||
/// It is the at the start of every [`MediaPlaylist`] and [`MasterPlaylist`].
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
/// [`M3U`]: https://en.wikipedia.org/wiki/M3U
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub(crate) struct ExtM3u;
|
||||
|
||||
impl ExtM3u {
|
||||
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtM3u {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtM3u {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX) }
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtM3u {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(ExtM3u.to_string(), "#EXTM3U".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(ExtM3u::try_from("#EXTM3U").unwrap(), ExtM3u);
|
||||
assert!(ExtM3u::try_from("#EXTM2U").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtM3u.required_version(), ProtocolVersion::V1);
|
||||
}
|
||||
}
|
5
src/tags/basic/mod.rs
Normal file
5
src/tags/basic/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub(crate) mod m3u;
|
||||
pub(crate) mod version;
|
||||
|
||||
pub(crate) use m3u::*;
|
||||
pub use version::*;
|
111
src/tags/basic/version.rs
Normal file
111
src/tags/basic/version.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The compatibility version of a playlist.
|
||||
///
|
||||
/// It applies to the entire [`MasterPlaylist`] or [`MediaPlaylist`].
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub struct ExtXVersion(ProtocolVersion);
|
||||
|
||||
impl ExtXVersion {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:";
|
||||
|
||||
/// Makes a new [`ExtXVersion`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXVersion;
|
||||
/// use hls_m3u8::types::ProtocolVersion;
|
||||
///
|
||||
/// let version = ExtXVersion::new(ProtocolVersion::V2);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn new(version: ProtocolVersion) -> Self { Self(version) }
|
||||
|
||||
/// Returns the underlying [`ProtocolVersion`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXVersion;
|
||||
/// use hls_m3u8::types::ProtocolVersion;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ExtXVersion::new(ProtocolVersion::V6).version(),
|
||||
/// ProtocolVersion::V6
|
||||
/// );
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn version(self) -> ProtocolVersion { self.0 }
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXVersion {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
//
|
||||
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtocolVersion> for ExtXVersion {
|
||||
fn from(value: ProtocolVersion) -> Self { Self(value) }
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXVersion {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
let version = tag(input, Self::PREFIX)?.parse()?;
|
||||
Ok(Self::new(version))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXVersion::new(ProtocolVersion::V6).to_string(),
|
||||
"#EXT-X-VERSION:6"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXVersion::try_from("#EXT-X-VERSION:6").unwrap(),
|
||||
ExtXVersion::new(ProtocolVersion::V6)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXVersion::new(ProtocolVersion::V6).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_and_from() {
|
||||
assert_eq!(
|
||||
ExtXVersion::default(),
|
||||
ExtXVersion::from(ProtocolVersion::V1)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,917 +0,0 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use {Error, ErrorKind, Result};
|
||||
use attribute::AttributePairs;
|
||||
use types::{ClosedCaptions, DecimalFloatingPoint, DecimalResolution, DecryptionKey, HdcpLevel,
|
||||
InStreamId, MediaType, ProtocolVersion, QuotedString, SessionData, SingleLineString};
|
||||
use super::{parse_yes_or_no, parse_u64};
|
||||
|
||||
/// `ExtXMedia` builder.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtXMediaBuilder {
|
||||
media_type: Option<MediaType>,
|
||||
uri: Option<QuotedString>,
|
||||
group_id: Option<QuotedString>,
|
||||
language: Option<QuotedString>,
|
||||
assoc_language: Option<QuotedString>,
|
||||
name: Option<QuotedString>,
|
||||
default: bool,
|
||||
autoselect: Option<bool>,
|
||||
forced: Option<bool>,
|
||||
instream_id: Option<InStreamId>,
|
||||
characteristics: Option<QuotedString>,
|
||||
channels: Option<QuotedString>,
|
||||
}
|
||||
impl ExtXMediaBuilder {
|
||||
/// Makes a `ExtXMediaBuilder` instance.
|
||||
pub fn new() -> Self {
|
||||
ExtXMediaBuilder {
|
||||
media_type: None,
|
||||
uri: None,
|
||||
group_id: None,
|
||||
language: None,
|
||||
assoc_language: None,
|
||||
name: None,
|
||||
default: false,
|
||||
autoselect: None,
|
||||
forced: None,
|
||||
instream_id: None,
|
||||
characteristics: None,
|
||||
channels: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the media type of the rendition.
|
||||
pub fn media_type(&mut self, media_type: MediaType) -> &mut Self {
|
||||
self.media_type = Some(media_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the identifier that specifies the group to which the rendition belongs.
|
||||
pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self {
|
||||
self.group_id = Some(group_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a human-readable description of the rendition.
|
||||
pub fn name(&mut self, name: QuotedString) -> &mut Self {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the URI that identifies the media playlist.
|
||||
pub fn uri(&mut self, uri: QuotedString) -> &mut Self {
|
||||
self.uri = Some(uri);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of the primary language used in the rendition.
|
||||
pub fn language(&mut self, language: QuotedString) -> &mut Self {
|
||||
self.language = Some(language);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of a language associated with the rendition.
|
||||
pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self {
|
||||
self.assoc_language = Some(language);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the `default` flag.
|
||||
pub fn default(&mut self, b: bool) -> &mut Self {
|
||||
self.default = b;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the `autoselect` flag.
|
||||
pub fn autoselect(&mut self, b: bool) -> &mut Self {
|
||||
self.autoselect = Some(b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the `forced` flag.
|
||||
pub fn forced(&mut self, b: bool) -> &mut Self {
|
||||
self.forced = Some(b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the identifier that specifies a rendition within the segments in the media playlist.
|
||||
pub fn instream_id(&mut self, id: InStreamId) -> &mut Self {
|
||||
self.instream_id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the string that represents uniform type identifiers (UTI).
|
||||
pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self {
|
||||
self.characteristics = Some(characteristics);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the string that represents the parameters of the rendition.
|
||||
pub fn channels(&mut self, channels: QuotedString) -> &mut Self {
|
||||
self.channels = Some(channels);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a `ExtXMedia` instance.
|
||||
pub fn finish(self) -> Result<ExtXMedia> {
|
||||
let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput);
|
||||
let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput);
|
||||
let name = track_assert_some!(self.name, ErrorKind::InvalidInput);
|
||||
if MediaType::ClosedCaptions == media_type {
|
||||
track_assert_ne!(self.uri, None, ErrorKind::InvalidInput);
|
||||
track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput);
|
||||
} else {
|
||||
track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput);
|
||||
}
|
||||
if self.default && self.autoselect.is_some() {
|
||||
track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput);
|
||||
}
|
||||
if MediaType::Subtitles != media_type {
|
||||
track_assert_eq!(self.forced, None, ErrorKind::InvalidInput);
|
||||
}
|
||||
Ok(ExtXMedia {
|
||||
media_type,
|
||||
uri: self.uri,
|
||||
group_id,
|
||||
language: self.language,
|
||||
assoc_language: self.assoc_language,
|
||||
name,
|
||||
default: self.default,
|
||||
autoselect: self.autoselect.unwrap_or(false),
|
||||
forced: self.forced.unwrap_or(false),
|
||||
instream_id: self.instream_id,
|
||||
characteristics: self.characteristics,
|
||||
channels: self.channels,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Default for ExtXMediaBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.4.1. EXT-X-MEDIA]
|
||||
///
|
||||
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXMedia {
|
||||
media_type: MediaType,
|
||||
uri: Option<QuotedString>,
|
||||
group_id: QuotedString,
|
||||
language: Option<QuotedString>,
|
||||
assoc_language: Option<QuotedString>,
|
||||
name: QuotedString,
|
||||
default: bool,
|
||||
autoselect: bool,
|
||||
forced: bool,
|
||||
instream_id: Option<InStreamId>,
|
||||
characteristics: Option<QuotedString>,
|
||||
channels: Option<QuotedString>,
|
||||
}
|
||||
impl ExtXMedia {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
||||
|
||||
/// Makes a new `ExtXMedia` tag.
|
||||
pub fn new(media_type: MediaType, group_id: QuotedString, name: QuotedString) -> Self {
|
||||
ExtXMedia {
|
||||
media_type,
|
||||
uri: None,
|
||||
group_id,
|
||||
language: None,
|
||||
assoc_language: None,
|
||||
name,
|
||||
default: false,
|
||||
autoselect: false,
|
||||
forced: false,
|
||||
instream_id: None,
|
||||
characteristics: None,
|
||||
channels: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the type of the media associated with this tag.
|
||||
pub fn media_type(&self) -> MediaType {
|
||||
self.media_type
|
||||
}
|
||||
|
||||
/// Returns the identifier that specifies the group to which the rendition belongs.
|
||||
pub fn group_id(&self) -> &QuotedString {
|
||||
&self.group_id
|
||||
}
|
||||
|
||||
/// Returns a human-readable description of the rendition.
|
||||
pub fn name(&self) -> &QuotedString {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns the URI that identifies the media playlist.
|
||||
pub fn uri(&self) -> Option<&QuotedString> {
|
||||
self.uri.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the name of the primary language used in the rendition.
|
||||
pub fn language(&self) -> Option<&QuotedString> {
|
||||
self.language.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the name of a language associated with the rendition.
|
||||
pub fn assoc_language(&self) -> Option<&QuotedString> {
|
||||
self.assoc_language.as_ref()
|
||||
}
|
||||
|
||||
/// Returns whether this is the default rendition.
|
||||
pub fn default(&self) -> bool {
|
||||
self.default
|
||||
}
|
||||
|
||||
/// Returns whether the client may choose to
|
||||
/// play this rendition in the absence of explicit user preference.
|
||||
pub fn autoselect(&self) -> bool {
|
||||
self.autoselect
|
||||
}
|
||||
|
||||
/// Returns whether the rendition contains content that is considered essential to play.
|
||||
pub fn forced(&self) -> bool {
|
||||
self.forced
|
||||
}
|
||||
|
||||
/// Returns the identifier that specifies a rendition within the segments in the media playlist.
|
||||
pub fn instream_id(&self) -> Option<InStreamId> {
|
||||
self.instream_id
|
||||
}
|
||||
|
||||
/// Returns a string that represents uniform type identifiers (UTI).
|
||||
///
|
||||
/// Each UTI indicates an individual characteristic of the rendition.
|
||||
pub fn characteristics(&self) -> Option<&QuotedString> {
|
||||
self.characteristics.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a string that represents the parameters of the rendition.
|
||||
pub fn channels(&self) -> Option<&QuotedString> {
|
||||
self.channels.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
match self.instream_id {
|
||||
None
|
||||
| Some(InStreamId::Cc1)
|
||||
| Some(InStreamId::Cc2)
|
||||
| Some(InStreamId::Cc3)
|
||||
| Some(InStreamId::Cc4) => ProtocolVersion::V1,
|
||||
_ => ProtocolVersion::V7,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXMedia {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "TYPE={}", self.media_type)?;
|
||||
if let Some(ref x) = self.uri {
|
||||
write!(f, ",URI={}", x)?;
|
||||
}
|
||||
write!(f, ",GROUP-ID={}", self.group_id)?;
|
||||
if let Some(ref x) = self.language {
|
||||
write!(f, ",LANGUAGE={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.assoc_language {
|
||||
write!(f, ",ASSOC-LANGUAGE={}", x)?;
|
||||
}
|
||||
write!(f, ",NAME={}", self.name)?;
|
||||
if self.default {
|
||||
write!(f, ",DEFAULT=YES")?;
|
||||
}
|
||||
if self.autoselect {
|
||||
write!(f, ",AUTOSELECT=YES")?;
|
||||
}
|
||||
if self.forced {
|
||||
write!(f, ",FORCED=YES")?;
|
||||
}
|
||||
if let Some(ref x) = self.instream_id {
|
||||
write!(f, ",INSTREAM-ID=\"{}\"", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.characteristics {
|
||||
write!(f, ",CHARACTERISTICS={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.channels {
|
||||
write!(f, ",CHANNELS={}", x)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXMedia {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
|
||||
let mut builder = ExtXMediaBuilder::new();
|
||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"TYPE" => {
|
||||
builder.media_type(track!(value.parse())?);
|
||||
}
|
||||
"URI" => {
|
||||
builder.uri(track!(value.parse())?);
|
||||
}
|
||||
"GROUP-ID" => {
|
||||
builder.group_id(track!(value.parse())?);
|
||||
}
|
||||
"LANGUAGE" => {
|
||||
builder.language(track!(value.parse())?);
|
||||
}
|
||||
"ASSOC-LANGUAGE" => {
|
||||
builder.assoc_language(track!(value.parse())?);
|
||||
}
|
||||
"NAME" => {
|
||||
builder.name(track!(value.parse())?);
|
||||
}
|
||||
"DEFAULT" => {
|
||||
builder.default(track!(parse_yes_or_no(value))?);
|
||||
}
|
||||
"AUTOSELECT" => {
|
||||
builder.autoselect(track!(parse_yes_or_no(value))?);
|
||||
}
|
||||
"FORCED" => {
|
||||
builder.forced(track!(parse_yes_or_no(value))?);
|
||||
}
|
||||
"INSTREAM-ID" => {
|
||||
let s: QuotedString = track!(value.parse())?;
|
||||
builder.instream_id(track!(s.parse())?);
|
||||
}
|
||||
"CHARACTERISTICS" => {
|
||||
builder.characteristics(track!(value.parse())?);
|
||||
}
|
||||
"CHANNELS" => {
|
||||
builder.channels(track!(value.parse())?);
|
||||
}
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
track!(builder.finish())
|
||||
}
|
||||
}
|
||||
|
||||
/// [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(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExtXStreamInf {
|
||||
uri: SingleLineString,
|
||||
bandwidth: u64,
|
||||
average_bandwidth: Option<u64>,
|
||||
codecs: Option<QuotedString>,
|
||||
resolution: Option<DecimalResolution>,
|
||||
frame_rate: Option<DecimalFloatingPoint>,
|
||||
hdcp_level: Option<HdcpLevel>,
|
||||
audio: Option<QuotedString>,
|
||||
video: Option<QuotedString>,
|
||||
subtitles: Option<QuotedString>,
|
||||
closed_captions: Option<ClosedCaptions>,
|
||||
}
|
||||
impl ExtXStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
||||
|
||||
/// Makes a new `ExtXStreamInf` tag.
|
||||
pub fn new(uri: SingleLineString, bandwidth: u64) -> Self {
|
||||
ExtXStreamInf {
|
||||
uri,
|
||||
bandwidth,
|
||||
average_bandwidth: None,
|
||||
codecs: None,
|
||||
resolution: None,
|
||||
frame_rate: None,
|
||||
hdcp_level: None,
|
||||
audio: None,
|
||||
video: None,
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the URI that identifies the associated media playlist.
|
||||
pub fn uri(&self) -> &SingleLineString {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Returns the peak segment bit rate of the variant stream.
|
||||
pub fn bandwidth(&self) -> u64 {
|
||||
self.bandwidth
|
||||
}
|
||||
|
||||
/// Returns the average segment bit rate of the variant stream.
|
||||
pub fn average_bandwidth(&self) -> Option<u64> {
|
||||
self.average_bandwidth
|
||||
}
|
||||
|
||||
/// Returns a string that represents the list of codec types contained the variant stream.
|
||||
pub fn codecs(&self) -> Option<&QuotedString> {
|
||||
self.codecs.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the optimal pixel resolution at which to display all the video in the variant stream.
|
||||
pub fn resolution(&self) -> Option<DecimalResolution> {
|
||||
self.resolution
|
||||
}
|
||||
|
||||
/// Returns the maximum frame rate for all the video in the variant stream.
|
||||
pub fn frame_rate(&self) -> Option<DecimalFloatingPoint> {
|
||||
self.frame_rate
|
||||
}
|
||||
|
||||
/// Returns the HDCP level of the variant stream.
|
||||
pub fn hdcp_level(&self) -> Option<HdcpLevel> {
|
||||
self.hdcp_level
|
||||
}
|
||||
|
||||
/// Returns the group identifier for the audio in the variant stream.
|
||||
pub fn audio(&self) -> Option<&QuotedString> {
|
||||
self.audio.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the group identifier for the video in the variant stream.
|
||||
pub fn video(&self) -> Option<&QuotedString> {
|
||||
self.video.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the group identifier for the subtitles in the variant stream.
|
||||
pub fn subtitles(&self) -> Option<&QuotedString> {
|
||||
self.subtitles.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the value of `CLOSED-CAPTIONS` attribute.
|
||||
pub fn closed_captions(&self) -> Option<&ClosedCaptions> {
|
||||
self.closed_captions.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXStreamInf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
||||
if let Some(ref x) = self.average_bandwidth {
|
||||
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.codecs {
|
||||
write!(f, ",CODECS={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.resolution {
|
||||
write!(f, ",RESOLUTION={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.frame_rate {
|
||||
write!(f, ",FRAME-RATE={:.3}", x.as_f64())?;
|
||||
}
|
||||
if let Some(ref x) = self.hdcp_level {
|
||||
write!(f, ",HDCP-LEVEL={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.audio {
|
||||
write!(f, ",AUDIO={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.video {
|
||||
write!(f, ",VIDEO={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.subtitles {
|
||||
write!(f, ",SUBTITLES={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.closed_captions {
|
||||
write!(f, ",CLOSED-CAPTIONS={}", x)?;
|
||||
}
|
||||
write!(f, "\n{}", self.uri)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXStreamInf {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut lines = s.splitn(2, '\n');
|
||||
let first_line = lines.next().expect("Never fails").trim_right_matches('\r');
|
||||
let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput);
|
||||
|
||||
track_assert!(
|
||||
first_line.starts_with(Self::PREFIX),
|
||||
ErrorKind::InvalidInput
|
||||
);
|
||||
let uri = track!(SingleLineString::new(second_line))?;
|
||||
let mut bandwidth = None;
|
||||
let mut average_bandwidth = None;
|
||||
let mut codecs = None;
|
||||
let mut resolution = None;
|
||||
let mut frame_rate = None;
|
||||
let mut hdcp_level = None;
|
||||
let mut audio = None;
|
||||
let mut video = None;
|
||||
let mut subtitles = None;
|
||||
let mut closed_captions = None;
|
||||
let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?),
|
||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
||||
"CODECS" => codecs = Some(track!(value.parse())?),
|
||||
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
||||
"FRAME-RATE" => frame_rate = Some(track!(value.parse())?),
|
||||
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
||||
"AUDIO" => audio = Some(track!(value.parse())?),
|
||||
"VIDEO" => video = Some(track!(value.parse())?),
|
||||
"SUBTITLES" => subtitles = Some(track!(value.parse())?),
|
||||
"CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?),
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
||||
Ok(ExtXStreamInf {
|
||||
uri,
|
||||
bandwidth,
|
||||
average_bandwidth,
|
||||
codecs,
|
||||
resolution,
|
||||
frame_rate,
|
||||
hdcp_level,
|
||||
audio,
|
||||
video,
|
||||
subtitles,
|
||||
closed_captions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
||||
///
|
||||
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXIFrameStreamInf {
|
||||
uri: QuotedString,
|
||||
bandwidth: u64,
|
||||
average_bandwidth: Option<u64>,
|
||||
codecs: Option<QuotedString>,
|
||||
resolution: Option<DecimalResolution>,
|
||||
hdcp_level: Option<HdcpLevel>,
|
||||
video: Option<QuotedString>,
|
||||
}
|
||||
impl ExtXIFrameStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||
|
||||
/// Makes a new `ExtXIFrameStreamInf` tag.
|
||||
pub fn new(uri: QuotedString, bandwidth: u64) -> Self {
|
||||
ExtXIFrameStreamInf {
|
||||
uri,
|
||||
bandwidth,
|
||||
average_bandwidth: None,
|
||||
codecs: None,
|
||||
resolution: None,
|
||||
hdcp_level: None,
|
||||
video: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the URI that identifies the associated media playlist.
|
||||
pub fn uri(&self) -> &QuotedString {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Returns the peak segment bit rate of the variant stream.
|
||||
pub fn bandwidth(&self) -> u64 {
|
||||
self.bandwidth
|
||||
}
|
||||
|
||||
/// Returns the average segment bit rate of the variant stream.
|
||||
pub fn average_bandwidth(&self) -> Option<u64> {
|
||||
self.average_bandwidth
|
||||
}
|
||||
|
||||
/// Returns a string that represents the list of codec types contained the variant stream.
|
||||
pub fn codecs(&self) -> Option<&QuotedString> {
|
||||
self.codecs.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the optimal pixel resolution at which to display all the video in the variant stream.
|
||||
pub fn resolution(&self) -> Option<DecimalResolution> {
|
||||
self.resolution
|
||||
}
|
||||
|
||||
/// Returns the HDCP level of the variant stream.
|
||||
pub fn hdcp_level(&self) -> Option<HdcpLevel> {
|
||||
self.hdcp_level
|
||||
}
|
||||
|
||||
/// Returns the group identifier for the video in the variant stream.
|
||||
pub fn video(&self) -> Option<&QuotedString> {
|
||||
self.video.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_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={}", self.uri)?;
|
||||
write!(f, ",BANDWIDTH={}", self.bandwidth)?;
|
||||
if let Some(ref x) = self.average_bandwidth {
|
||||
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.codecs {
|
||||
write!(f, ",CODECS={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.resolution {
|
||||
write!(f, ",RESOLUTION={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.hdcp_level {
|
||||
write!(f, ",HDCP-LEVEL={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.video {
|
||||
write!(f, ",VIDEO={}", x)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXIFrameStreamInf {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
|
||||
let mut uri = None;
|
||||
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;
|
||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"URI" => uri = Some(track!(value.parse())?),
|
||||
"BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?),
|
||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
||||
"CODECS" => codecs = Some(track!(value.parse())?),
|
||||
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
||||
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
||||
"VIDEO" => video = Some(track!(value.parse())?),
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
||||
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
||||
Ok(ExtXIFrameStreamInf {
|
||||
uri,
|
||||
bandwidth,
|
||||
average_bandwidth,
|
||||
codecs,
|
||||
resolution,
|
||||
hdcp_level,
|
||||
video,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.4.4. EXT-X-SESSION-DATA]
|
||||
///
|
||||
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXSessionData {
|
||||
data_id: QuotedString,
|
||||
data: SessionData,
|
||||
language: Option<QuotedString>,
|
||||
}
|
||||
impl ExtXSessionData {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
||||
|
||||
/// Makes a new `ExtXSessionData` tag.
|
||||
pub fn new(data_id: QuotedString, data: SessionData) -> Self {
|
||||
ExtXSessionData {
|
||||
data_id,
|
||||
data,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new `ExtXSessionData` with the given language.
|
||||
pub fn with_language(data_id: QuotedString, data: SessionData, language: QuotedString) -> Self {
|
||||
ExtXSessionData {
|
||||
data_id,
|
||||
data,
|
||||
language: Some(language),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the identifier of the data.
|
||||
pub fn data_id(&self) -> &QuotedString {
|
||||
&self.data_id
|
||||
}
|
||||
|
||||
/// Returns the session data.
|
||||
pub fn data(&self) -> &SessionData {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Returns the language of the data.
|
||||
pub fn language(&self) -> Option<&QuotedString> {
|
||||
self.language.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXSessionData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "DATA-ID={}", self.data_id)?;
|
||||
match self.data {
|
||||
SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?,
|
||||
SessionData::Uri(ref x) => write!(f, ",URI={}", x)?,
|
||||
}
|
||||
if let Some(ref x) = self.language {
|
||||
write!(f, ",LANGUAGE={}", x)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXSessionData {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
|
||||
let mut data_id = None;
|
||||
let mut session_value = None;
|
||||
let mut uri = None;
|
||||
let mut language = None;
|
||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"DATA-ID" => data_id = Some(track!(value.parse())?),
|
||||
"VALUE" => session_value = Some(track!(value.parse())?),
|
||||
"URI" => uri = Some(track!(value.parse())?),
|
||||
"LANGUAGE" => language = Some(track!(value.parse())?),
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput);
|
||||
let data = if let Some(value) = session_value {
|
||||
track_assert_eq!(uri, None, ErrorKind::InvalidInput);
|
||||
SessionData::Value(value)
|
||||
} else if let Some(uri) = uri {
|
||||
SessionData::Uri(uri)
|
||||
} else {
|
||||
track_panic!(ErrorKind::InvalidInput);
|
||||
};
|
||||
Ok(ExtXSessionData {
|
||||
data_id,
|
||||
data,
|
||||
language,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.4.5. EXT-X-SESSION-KEY]
|
||||
///
|
||||
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXSessionKey {
|
||||
key: DecryptionKey,
|
||||
}
|
||||
impl ExtXSessionKey {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||
|
||||
/// Makes a new `ExtXSessionKey` tag.
|
||||
pub fn new(key: DecryptionKey) -> Self {
|
||||
ExtXSessionKey { key }
|
||||
}
|
||||
|
||||
/// Returns a decryption key for the playlist.
|
||||
pub fn key(&self) -> &DecryptionKey {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
self.key.requires_version()
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXSessionKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.key)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXSessionKey {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
||||
let key = track!(suffix.parse())?;
|
||||
Ok(ExtXSessionKey { key })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use types::{EncryptionMethod, InitializationVector};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_media() {
|
||||
let tag = ExtXMedia::new(MediaType::Audio, quoted_string("foo"), quoted_string("bar"));
|
||||
let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_stream_inf() {
|
||||
let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000);
|
||||
let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo";
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_i_frame_stream_inf() {
|
||||
let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), 1000);
|
||||
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_session_data() {
|
||||
let tag = ExtXSessionData::new(
|
||||
quoted_string("foo"),
|
||||
SessionData::Value(quoted_string("bar")),
|
||||
);
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
|
||||
let tag =
|
||||
ExtXSessionData::new(quoted_string("foo"), SessionData::Uri(quoted_string("bar")));
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
|
||||
let tag = ExtXSessionData::with_language(
|
||||
quoted_string("foo"),
|
||||
SessionData::Value(quoted_string("bar")),
|
||||
quoted_string("baz"),
|
||||
);
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_session_key() {
|
||||
let tag = ExtXSessionKey::new(DecryptionKey {
|
||||
method: EncryptionMethod::Aes128,
|
||||
uri: quoted_string("foo"),
|
||||
iv: Some(InitializationVector([
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
|
||||
])),
|
||||
key_format: None,
|
||||
key_format_versions: None,
|
||||
});
|
||||
let text =
|
||||
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V2);
|
||||
}
|
||||
|
||||
fn quoted_string(s: &str) -> QuotedString {
|
||||
QuotedString::new(s).unwrap()
|
||||
}
|
||||
}
|
801
src/tags/master_playlist/media.rs
Normal file
801
src/tags/master_playlist/media.rs
Normal file
|
@ -0,0 +1,801 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion};
|
||||
use crate::utils::{parse_yes_or_no, quote, tag, unquote};
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// An [`ExtXMedia`] tag is an alternative rendition of a [`VariantStream`].
|
||||
///
|
||||
/// For example an [`ExtXMedia`] tag can be used to specify different audio
|
||||
/// languages (e.g. english is the default and there also exists an
|
||||
/// [`ExtXMedia`] stream with a german audio).
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
/// [`VariantStream`]: crate::tags::VariantStream
|
||||
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[shorthand(enable(must_use, into))]
|
||||
#[builder(setter(into))]
|
||||
#[builder(build_fn(validate = "Self::validate"))]
|
||||
pub struct ExtXMedia<'a> {
|
||||
/// The [`MediaType`] associated with this tag.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is required.
|
||||
#[shorthand(enable(skip))]
|
||||
pub media_type: MediaType,
|
||||
/// An `URI` to a [`MediaPlaylist`].
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// - This field is required, if the [`ExtXMedia::media_type`] is
|
||||
/// [`MediaType::Subtitles`].
|
||||
/// - This field is not allowed, if the [`ExtXMedia::media_type`] is
|
||||
/// [`MediaType::ClosedCaptions`].
|
||||
///
|
||||
/// An absent value indicates that the media data for this rendition is
|
||||
/// included in the [`MediaPlaylist`] of any
|
||||
/// [`VariantStream::ExtXStreamInf`] tag with the same `group_id` of
|
||||
/// this [`ExtXMedia`] instance.
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
/// [`VariantStream::ExtXStreamInf`]:
|
||||
/// crate::tags::VariantStream::ExtXStreamInf
|
||||
#[builder(setter(strip_option), default)]
|
||||
uri: Option<Cow<'a, str>>,
|
||||
/// The identifier that specifies the group to which the rendition
|
||||
/// belongs.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is required.
|
||||
group_id: Cow<'a, str>,
|
||||
/// The name of the primary language used in the rendition.
|
||||
/// The value has to conform to [`RFC5646`].
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is optional.
|
||||
///
|
||||
/// [`RFC5646`]: https://tools.ietf.org/html/rfc5646
|
||||
#[builder(setter(strip_option), default)]
|
||||
language: Option<Cow<'a, str>>,
|
||||
/// The name of a language associated with the rendition.
|
||||
/// An associated language is often used in a different role, than the
|
||||
/// language specified by the [`language`] field (e.g., written versus
|
||||
/// spoken, or a fallback dialect).
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is optional.
|
||||
///
|
||||
/// [`language`]: #method.language
|
||||
#[builder(setter(strip_option), default)]
|
||||
assoc_language: Option<Cow<'a, str>>,
|
||||
/// A human-readable description of the rendition.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is required.
|
||||
///
|
||||
/// If the [`language`] field is present, this field should be in
|
||||
/// that language.
|
||||
///
|
||||
/// [`language`]: #method.language
|
||||
name: Cow<'a, str>,
|
||||
/// The value of the `default` flag.
|
||||
/// A value of `true` indicates, that the client should play
|
||||
/// this rendition of the content in the absence of information
|
||||
/// from the user indicating a different choice.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is optional, its absence indicates an implicit value
|
||||
/// of `false`.
|
||||
#[builder(default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub is_default: bool,
|
||||
/// Whether the client may choose to play this rendition in the absence of
|
||||
/// explicit user preference.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is optional, its absence indicates an implicit value
|
||||
/// of `false`.
|
||||
#[builder(default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub is_autoselect: bool,
|
||||
/// Whether the rendition contains content that is considered
|
||||
/// essential to play.
|
||||
#[builder(default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub is_forced: bool,
|
||||
/// An [`InStreamId`] identifies a rendition within the
|
||||
/// [`MediaSegment`]s in a [`MediaPlaylist`].
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is required, if the media type is
|
||||
/// [`MediaType::ClosedCaptions`]. For all other media types the
|
||||
/// [`InStreamId`] must not be specified!
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
#[builder(setter(strip_option), default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub instream_id: Option<InStreamId>,
|
||||
/// The characteristics field contains one or more Uniform Type
|
||||
/// Identifiers ([`UTI`]) separated by a comma.
|
||||
/// Each [`UTI`] indicates an individual characteristic of the Rendition.
|
||||
///
|
||||
/// An `ExtXMedia` instance with [`MediaType::Subtitles`] may include the
|
||||
/// following characteristics:
|
||||
/// - `"public.accessibility.transcribes-spoken-dialog"`,
|
||||
/// - `"public.accessibility.describes-music-and-sound"`, and
|
||||
/// - `"public.easy-to-read"` (which indicates that the subtitles have been
|
||||
/// edited for ease of reading).
|
||||
///
|
||||
/// An `ExtXMedia` instance with [`MediaType::Audio`] may include the
|
||||
/// following characteristic:
|
||||
/// - `"public.accessibility.describes-video"`
|
||||
///
|
||||
/// The characteristics field may include private UTIs.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is optional.
|
||||
///
|
||||
/// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI
|
||||
#[builder(setter(strip_option), default)]
|
||||
characteristics: Option<Cow<'a, str>>,
|
||||
/// A count of audio channels indicating the maximum number of independent,
|
||||
/// simultaneous audio channels present in any [`MediaSegment`] in the
|
||||
/// rendition.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This field is optional, but every instance of [`ExtXMedia`] with
|
||||
/// [`MediaType::Audio`] should have this field. If the [`MasterPlaylist`]
|
||||
/// contains two renditions with the same codec, but a different number of
|
||||
/// channels, then the channels field is required.
|
||||
///
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
#[builder(setter(strip_option), default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub channels: Option<Channels>,
|
||||
}
|
||||
|
||||
impl<'a> ExtXMediaBuilder<'a> {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
// A MediaType is always required!
|
||||
let media_type = self
|
||||
.media_type
|
||||
.ok_or_else(|| Error::missing_attribute("MEDIA-TYPE").to_string())?;
|
||||
|
||||
if media_type == MediaType::Subtitles && self.uri.is_none() {
|
||||
return Err(Error::missing_attribute("URI").to_string());
|
||||
}
|
||||
|
||||
if media_type == MediaType::ClosedCaptions {
|
||||
if self.uri.is_some() {
|
||||
return Err(Error::unexpected_attribute("URI").to_string());
|
||||
}
|
||||
if self.instream_id.is_none() {
|
||||
return Err(Error::missing_attribute("INSTREAM-ID").to_string());
|
||||
}
|
||||
} else if self.instream_id.is_some() {
|
||||
return Err(Error::custom(
|
||||
"InStreamId should only be specified for an ExtXMedia tag with `MediaType::ClosedCaptions`"
|
||||
).to_string());
|
||||
}
|
||||
|
||||
if self.is_default.unwrap_or(false) && self.is_autoselect.map_or(false, |b| !b) {
|
||||
return Err(Error::custom(format!(
|
||||
"If `DEFAULT` is true, `AUTOSELECT` has to be true too, if present. Default: {:?}, Autoselect: {:?}!",
|
||||
self.is_default, self.is_autoselect
|
||||
))
|
||||
.to_string());
|
||||
}
|
||||
|
||||
if media_type != MediaType::Subtitles && self.is_forced.unwrap_or(false) {
|
||||
return Err(Error::custom(format!(
|
||||
concat!(
|
||||
"the forced attribute must not be present, ",
|
||||
"unless the media_type is `MediaType::Subtitles`: ",
|
||||
"media_type: {:?}, is_forced: {:?}"
|
||||
),
|
||||
media_type, self.is_forced
|
||||
))
|
||||
.to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ExtXMedia<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
||||
|
||||
/// Makes a new [`ExtXMedia`] tag with the associated [`MediaType`], the
|
||||
/// identifier that specifies the group to which the rendition belongs
|
||||
/// (group id) and a human-readable description of the rendition. If the
|
||||
/// [`language`] is specified it should be in that language.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let media = ExtXMedia::new(MediaType::Video, "vg1", "1080p video stream");
|
||||
/// ```
|
||||
///
|
||||
/// [`language`]: #method.language
|
||||
#[must_use]
|
||||
pub fn new<T, K>(media_type: MediaType, group_id: T, name: K) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
K: Into<Cow<'a, str>>,
|
||||
{
|
||||
Self {
|
||||
media_type,
|
||||
uri: None,
|
||||
group_id: group_id.into(),
|
||||
language: None,
|
||||
assoc_language: None,
|
||||
name: name.into(),
|
||||
is_default: false,
|
||||
is_autoselect: false,
|
||||
is_forced: false,
|
||||
instream_id: None,
|
||||
characteristics: None,
|
||||
channels: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXMedia`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let media = ExtXMedia::builder()
|
||||
/// .media_type(MediaType::Subtitles)
|
||||
/// .uri("french/ed.ttml")
|
||||
/// .group_id("subs")
|
||||
/// .language("fra")
|
||||
/// .assoc_language("fra")
|
||||
/// .name("French")
|
||||
/// .is_autoselect(true)
|
||||
/// .is_forced(true)
|
||||
/// // concat! joins multiple `&'static str`
|
||||
/// .characteristics(concat!(
|
||||
/// "public.accessibility.transcribes-spoken-dialog,",
|
||||
/// "public.accessibility.describes-music-and-sound"
|
||||
/// ))
|
||||
/// .build()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn builder() -> ExtXMediaBuilder<'a> { ExtXMediaBuilder::default() }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ExtXMedia<'static> {
|
||||
ExtXMedia {
|
||||
media_type: self.media_type,
|
||||
uri: self.uri.map(|v| Cow::Owned(v.into_owned())),
|
||||
group_id: Cow::Owned(self.group_id.into_owned()),
|
||||
language: self.language.map(|v| Cow::Owned(v.into_owned())),
|
||||
assoc_language: self.assoc_language.map(|v| Cow::Owned(v.into_owned())),
|
||||
name: Cow::Owned(self.name.into_owned()),
|
||||
is_default: self.is_default,
|
||||
is_autoselect: self.is_autoselect,
|
||||
is_forced: self.is_forced,
|
||||
instream_id: self.instream_id,
|
||||
characteristics: self.characteristics.map(|v| Cow::Owned(v.into_owned())),
|
||||
channels: self.channels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires either `ProtocolVersion::V1` or if there is an
|
||||
/// `instream_id` it requires it's version.
|
||||
impl<'a> RequiredVersion for ExtXMedia<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.instream_id
|
||||
.map_or(ProtocolVersion::V1, |i| i.required_version())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtXMedia<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "TYPE={}", self.media_type)?;
|
||||
|
||||
if let Some(value) = &self.uri {
|
||||
write!(f, ",URI={}", quote(value))?;
|
||||
}
|
||||
|
||||
write!(f, ",GROUP-ID={}", quote(&self.group_id))?;
|
||||
|
||||
if let Some(value) = &self.language {
|
||||
write!(f, ",LANGUAGE={}", quote(value))?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.assoc_language {
|
||||
write!(f, ",ASSOC-LANGUAGE={}", quote(value))?;
|
||||
}
|
||||
|
||||
write!(f, ",NAME={}", quote(&self.name))?;
|
||||
|
||||
if self.is_default {
|
||||
write!(f, ",DEFAULT=YES")?;
|
||||
}
|
||||
|
||||
if self.is_autoselect {
|
||||
write!(f, ",AUTOSELECT=YES")?;
|
||||
}
|
||||
|
||||
if self.is_forced {
|
||||
write!(f, ",FORCED=YES")?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.instream_id {
|
||||
write!(f, ",INSTREAM-ID={}", quote(value))?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.characteristics {
|
||||
write!(f, ",CHARACTERISTICS={}", quote(value))?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.channels {
|
||||
write!(f, ",CHANNELS={}", quote(value))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtXMedia<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let mut builder = Self::builder();
|
||||
|
||||
for (key, value) in AttributePairs::new(input) {
|
||||
match key {
|
||||
"TYPE" => {
|
||||
builder.media_type(value.parse::<MediaType>()?);
|
||||
}
|
||||
"URI" => {
|
||||
builder.uri(unquote(value));
|
||||
}
|
||||
"GROUP-ID" => {
|
||||
builder.group_id(unquote(value));
|
||||
}
|
||||
"LANGUAGE" => {
|
||||
builder.language(unquote(value));
|
||||
}
|
||||
"ASSOC-LANGUAGE" => {
|
||||
builder.assoc_language(unquote(value));
|
||||
}
|
||||
"NAME" => {
|
||||
builder.name(unquote(value));
|
||||
}
|
||||
"DEFAULT" => {
|
||||
builder.is_default(parse_yes_or_no(value)?);
|
||||
}
|
||||
"AUTOSELECT" => {
|
||||
builder.is_autoselect(parse_yes_or_no(value)?);
|
||||
}
|
||||
"FORCED" => {
|
||||
builder.is_forced(parse_yes_or_no(value)?);
|
||||
}
|
||||
"INSTREAM-ID" => {
|
||||
builder.instream_id(unquote(value).parse::<InStreamId>()?);
|
||||
}
|
||||
"CHARACTERISTICS" => {
|
||||
builder.characteristics(unquote(value));
|
||||
}
|
||||
"CHANNELS" => {
|
||||
builder.channels(unquote(value).parse::<Channels>()?);
|
||||
}
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized
|
||||
// AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.build().map_err(Error::builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
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, TryFrom::try_from($str).unwrap());
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_default(true)
|
||||
.uri("eng/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"eng/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio\",",
|
||||
"LANGUAGE=\"eng\",",
|
||||
"NAME=\"English\",",
|
||||
"DEFAULT=YES",
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("eng/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"eng/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio\",",
|
||||
"LANGUAGE=\"eng\",",
|
||||
"NAME=\"English\",",
|
||||
"DEFAULT=YES,",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.uri("fre/prog_index.m3u8")
|
||||
.group_id("audio")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_default(false)
|
||||
.is_autoselect(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"fre/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio\",",
|
||||
"LANGUAGE=\"fre\",",
|
||||
"NAME=\"Français\",",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("sp")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("sp/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"sp/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio\",",
|
||||
"LANGUAGE=\"sp\",",
|
||||
"NAME=\"Espanol\",",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("englo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"englo/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio-lo\",",
|
||||
"LANGUAGE=\"eng\",",
|
||||
"NAME=\"English\",",
|
||||
"DEFAULT=YES,",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("frelo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"frelo/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio-lo\",",
|
||||
"LANGUAGE=\"fre\",",
|
||||
"NAME=\"Français\",",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("es")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("splo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"splo/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio-lo\",",
|
||||
"LANGUAGE=\"es\",",
|
||||
"NAME=\"Espanol\",",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("eng/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"eng/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio-hi\",",
|
||||
"LANGUAGE=\"eng\",",
|
||||
"NAME=\"English\",",
|
||||
"DEFAULT=YES,",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("fre/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"fre/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio-hi\",",
|
||||
"LANGUAGE=\"fre\",",
|
||||
"NAME=\"Français\",",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("es")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("sp/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"sp/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio-hi\",",
|
||||
"LANGUAGE=\"es\",",
|
||||
"NAME=\"Espanol\",",
|
||||
"AUTOSELECT=YES"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-aacl-312")
|
||||
.language("en")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.channels(Channels::new(2))
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"GROUP-ID=\"audio-aacl-312\",",
|
||||
"LANGUAGE=\"en\",",
|
||||
"NAME=\"English\",",
|
||||
"DEFAULT=YES,",
|
||||
"AUTOSELECT=YES,",
|
||||
"CHANNELS=\"2\""
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Subtitles)
|
||||
.uri("french/ed.ttml")
|
||||
.group_id("subs")
|
||||
.language("fra")
|
||||
.assoc_language("fra")
|
||||
.name("French")
|
||||
.is_autoselect(true)
|
||||
.is_forced(true)
|
||||
.characteristics("public.accessibility.transcribes-spoken\
|
||||
-dialog,public.accessibility.describes-music-and-sound")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=SUBTITLES,",
|
||||
"URI=\"french/ed.ttml\",",
|
||||
"GROUP-ID=\"subs\",",
|
||||
"LANGUAGE=\"fra\",",
|
||||
"ASSOC-LANGUAGE=\"fra\",",
|
||||
"NAME=\"French\",",
|
||||
"AUTOSELECT=YES,",
|
||||
"FORCED=YES,",
|
||||
"CHARACTERISTICS=\"",
|
||||
"public.accessibility.transcribes-spoken-dialog,",
|
||||
"public.accessibility.describes-music-and-sound",
|
||||
"\""
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::ClosedCaptions)
|
||||
.group_id("cc")
|
||||
.language("sp")
|
||||
.name("CC2")
|
||||
.instream_id(InStreamId::Cc2)
|
||||
.is_autoselect(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=CLOSED-CAPTIONS,",
|
||||
"GROUP-ID=\"cc\",",
|
||||
"LANGUAGE=\"sp\",",
|
||||
"NAME=\"CC2\",",
|
||||
"AUTOSELECT=YES,",
|
||||
"INSTREAM-ID=\"CC2\""
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXMedia::new(MediaType::Audio, "foo", "bar"),
|
||||
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\""
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_error() {
|
||||
assert!(ExtXMedia::try_from("").is_err());
|
||||
assert!(ExtXMedia::try_from("garbage").is_err());
|
||||
|
||||
assert!(ExtXMedia::try_from(
|
||||
"#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\""
|
||||
)
|
||||
.is_err());
|
||||
assert!(ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1").is_err());
|
||||
|
||||
assert!(ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO").is_err());
|
||||
|
||||
assert!(ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
macro_rules! gen_required_version {
|
||||
( $( $id:expr => $output:expr, )* ) => {
|
||||
$(
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::ClosedCaptions)
|
||||
.group_id("audio")
|
||||
.name("English")
|
||||
.instream_id($id)
|
||||
.build()
|
||||
.unwrap()
|
||||
.required_version(),
|
||||
$output
|
||||
);
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
gen_required_version![
|
||||
InStreamId::Cc1 => ProtocolVersion::V1,
|
||||
InStreamId::Cc2 => ProtocolVersion::V1,
|
||||
InStreamId::Cc3 => ProtocolVersion::V1,
|
||||
InStreamId::Cc4 => ProtocolVersion::V1,
|
||||
InStreamId::Service1 => ProtocolVersion::V7,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.name("English")
|
||||
.build()
|
||||
.unwrap()
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
}
|
9
src/tags/master_playlist/mod.rs
Normal file
9
src/tags/master_playlist/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
pub(crate) mod media;
|
||||
pub(crate) mod session_data;
|
||||
pub(crate) mod session_key;
|
||||
pub(crate) mod variant_stream;
|
||||
|
||||
pub use media::ExtXMedia;
|
||||
pub use session_data::{ExtXSessionData, SessionData};
|
||||
pub use session_key::*;
|
||||
pub use variant_stream::*;
|
338
src/tags/master_playlist/session_data.rs
Normal file
338
src/tags/master_playlist/session_data.rs
Normal file
|
@ -0,0 +1,338 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The data of [`ExtXSessionData`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SessionData<'a> {
|
||||
/// Contains the data identified by the [`ExtXSessionData::data_id`].
|
||||
///
|
||||
/// If a [`language`] is specified, this variant should contain a
|
||||
/// human-readable string written in the specified language.
|
||||
///
|
||||
/// [`data_id`]: ExtXSessionData::data_id
|
||||
/// [`language`]: ExtXSessionData::language
|
||||
Value(Cow<'a, str>),
|
||||
/// An [`URI`], which points to a [`json`] file.
|
||||
///
|
||||
/// [`json`]: https://tools.ietf.org/html/rfc8259
|
||||
/// [`URI`]: https://tools.ietf.org/html/rfc3986
|
||||
Uri(Cow<'a, str>),
|
||||
}
|
||||
|
||||
impl<'a> SessionData<'a> {
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> SessionData<'static> {
|
||||
match self {
|
||||
Self::Value(v) => SessionData::Value(Cow::Owned(v.into_owned())),
|
||||
Self::Uri(v) => SessionData::Uri(Cow::Owned(v.into_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows arbitrary session data to be carried in a [`MasterPlaylist`].
|
||||
///
|
||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
#[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
||||
#[builder(setter(into))]
|
||||
#[shorthand(enable(must_use, into))]
|
||||
pub struct ExtXSessionData<'a> {
|
||||
/// This 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.
|
||||
///
|
||||
/// This field is required.
|
||||
///
|
||||
/// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
|
||||
data_id: Cow<'a, str>,
|
||||
/// The [`SessionData`] associated with the
|
||||
/// [`data_id`](ExtXSessionData::data_id).
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This field is required.
|
||||
#[shorthand(enable(skip))]
|
||||
pub data: SessionData<'a>,
|
||||
/// 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(strip_option), default)]
|
||||
language: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> ExtXSessionData<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
||||
|
||||
/// Makes a new [`ExtXSessionData`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXSessionData;
|
||||
/// use hls_m3u8::tags::SessionData;
|
||||
///
|
||||
/// let session_data = ExtXSessionData::new(
|
||||
/// "com.example.movie.title",
|
||||
/// SessionData::Uri("https://www.example.com/".into()),
|
||||
/// );
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn new<T: Into<Cow<'a, str>>>(data_id: T, data: SessionData<'a>) -> Self {
|
||||
Self {
|
||||
data_id: data_id.into(),
|
||||
data,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXSessionData`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXSessionData;
|
||||
/// use hls_m3u8::tags::SessionData;
|
||||
///
|
||||
/// let session_data = ExtXSessionData::builder()
|
||||
/// .data_id("com.example.movie.title")
|
||||
/// .data(SessionData::Value("some data".into()))
|
||||
/// .language("en")
|
||||
/// .build()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn builder() -> ExtXSessionDataBuilder<'a> { ExtXSessionDataBuilder::default() }
|
||||
|
||||
/// Makes a new [`ExtXSessionData`] tag, with the given language.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXSessionData;
|
||||
/// use hls_m3u8::tags::SessionData;
|
||||
///
|
||||
/// let session_data = ExtXSessionData::with_language(
|
||||
/// "com.example.movie.title",
|
||||
/// SessionData::Value("some data".into()),
|
||||
/// "en",
|
||||
/// );
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn with_language<T, K>(data_id: T, data: SessionData<'a>, language: K) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
K: Into<Cow<'a, str>>,
|
||||
{
|
||||
Self {
|
||||
data_id: data_id.into(),
|
||||
data,
|
||||
language: Some(language.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ExtXSessionData<'static> {
|
||||
ExtXSessionData {
|
||||
data_id: Cow::Owned(self.data_id.into_owned()),
|
||||
data: self.data.into_owned(),
|
||||
language: self.language.map(|v| Cow::Owned(v.into_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl<'a> RequiredVersion for ExtXSessionData<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtXSessionData<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "DATA-ID={}", quote(&self.data_id))?;
|
||||
|
||||
match &self.data {
|
||||
SessionData::Value(value) => write!(f, ",VALUE={}", quote(value))?,
|
||||
SessionData::Uri(value) => write!(f, ",URI={}", quote(value))?,
|
||||
}
|
||||
|
||||
if let Some(value) = &self.language {
|
||||
write!(f, ",LANGUAGE={}", quote(value))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtXSessionData<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let mut data_id = None;
|
||||
let mut session_value = None;
|
||||
let mut uri = None;
|
||||
let mut language = None;
|
||||
|
||||
for (key, value) in AttributePairs::new(input) {
|
||||
match key {
|
||||
"DATA-ID" => data_id = Some(unquote(value)),
|
||||
"VALUE" => session_value = Some(unquote(value)),
|
||||
"URI" => uri = Some(unquote(value)),
|
||||
"LANGUAGE" => language = Some(unquote(value)),
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized
|
||||
// AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data_id = data_id.ok_or_else(|| Error::missing_value("EXT-X-DATA-ID"))?;
|
||||
|
||||
let data = {
|
||||
if let Some(value) = session_value {
|
||||
if uri.is_some() {
|
||||
return Err(Error::custom("unexpected URI"));
|
||||
}
|
||||
|
||||
SessionData::Value(value)
|
||||
} else if let Some(uri) = uri {
|
||||
SessionData::Uri(uri)
|
||||
} else {
|
||||
return Err(Error::custom(
|
||||
"expected either `SessionData::Uri` or `SessionData::Value`",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
data_id,
|
||||
data,
|
||||
language,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
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, TryFrom::try_from($str).unwrap());
|
||||
)+
|
||||
|
||||
assert!(
|
||||
ExtXSessionData::try_from(concat!(
|
||||
"#EXT-X-SESSION-DATA:",
|
||||
"DATA-ID=\"foo\",",
|
||||
"LANGUAGE=\"baz\""
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
ExtXSessionData::try_from(concat!(
|
||||
"#EXT-X-SESSION-DATA:",
|
||||
"DATA-ID=\"foo\",",
|
||||
"LANGUAGE=\"baz\",",
|
||||
"VALUE=\"VALUE\",",
|
||||
"URI=\"https://www.example.com/\""
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
{
|
||||
ExtXSessionData::new(
|
||||
"com.example.lyrics",
|
||||
SessionData::Uri("lyrics.json".into())
|
||||
),
|
||||
concat!(
|
||||
"#EXT-X-SESSION-DATA:",
|
||||
"DATA-ID=\"com.example.lyrics\",",
|
||||
"URI=\"lyrics.json\""
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXSessionData::with_language(
|
||||
"com.example.title",
|
||||
SessionData::Value("This is an example".into()),
|
||||
"en"
|
||||
),
|
||||
concat!(
|
||||
"#EXT-X-SESSION-DATA:",
|
||||
"DATA-ID=\"com.example.title\",",
|
||||
"VALUE=\"This is an example\",",
|
||||
"LANGUAGE=\"en\""
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXSessionData::with_language(
|
||||
"com.example.title",
|
||||
SessionData::Value("Este es un ejemplo".into()),
|
||||
"es"
|
||||
),
|
||||
concat!(
|
||||
"#EXT-X-SESSION-DATA:",
|
||||
"DATA-ID=\"com.example.title\",",
|
||||
"VALUE=\"Este es un ejemplo\",",
|
||||
"LANGUAGE=\"es\""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXSessionData::new("com.example.lyrics", SessionData::Uri("lyrics.json".into()))
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
}
|
166
src/tags/master_playlist/session_key.rs
Normal file
166
src/tags/master_playlist/session_key.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use core::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use derive_more::{AsMut, AsRef, From};
|
||||
|
||||
use crate::tags::ExtXKey;
|
||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The [`ExtXSessionKey`] tag allows encryption keys from [`MediaPlaylist`]s
|
||||
/// to be specified in a [`MasterPlaylist`]. This allows the client to
|
||||
/// preload these keys without having to read the [`MediaPlaylist`]s
|
||||
/// first.
|
||||
///
|
||||
/// If an [`ExtXSessionKey`] is used, the values of [`DecryptionKey::method`],
|
||||
/// [`DecryptionKey::format`] and [`DecryptionKey::versions`] must match any
|
||||
/// [`ExtXKey`] with the same uri field.
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
/// [`ExtXKey`]: crate::tags::ExtXKey
|
||||
#[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExtXSessionKey<'a>(pub DecryptionKey<'a>);
|
||||
|
||||
impl<'a> ExtXSessionKey<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||
|
||||
/// Makes a new [`ExtXSessionKey`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXSessionKey;
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
///
|
||||
/// let session_key = ExtXSessionKey::new(DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "https://www.example.com/",
|
||||
/// ));
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn new(inner: DecryptionKey<'a>) -> Self { Self(inner) }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
///
|
||||
/// [`Cow`]: std::borrow::Cow
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ExtXSessionKey<'static> { ExtXSessionKey(self.0.into_owned()) }
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<ExtXKey<'a>> for ExtXSessionKey<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: ExtXKey<'a>) -> 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<'a> RequiredVersion for ExtXSessionKey<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtXSessionKey<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtXSessionKey<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
Ok(Self(DecryptionKey::try_from(tag(input, Self::PREFIX)?)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::types::{EncryptionMethod, KeyFormat};
|
||||
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, TryFrom::try_from($str).unwrap());
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
{
|
||||
ExtXSessionKey::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-SESSION-KEY:",
|
||||
"METHOD=AES-128,",
|
||||
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXSessionKey::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)
|
||||
.build()
|
||||
.unwrap(),
|
||||
),
|
||||
concat!(
|
||||
"#EXT-X-SESSION-KEY:",
|
||||
"METHOD=AES-128,",
|
||||
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
||||
"KEYFORMAT=\"identity\"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXSessionKey::new(DecryptionKey::new(
|
||||
EncryptionMethod::Aes128,
|
||||
"https://www.example.com/"
|
||||
))
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
}
|
509
src/tags/master_playlist/variant_stream.rs
Normal file
509
src/tags/master_playlist/variant_stream.rs
Normal file
|
@ -0,0 +1,509 @@
|
|||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
use core::ops::Deref;
|
||||
use std::borrow::Cow;
|
||||
|
||||
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<'a> {
|
||||
/// 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: Cow<'a, str>,
|
||||
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
||||
/// [`VariantStream::ExtXIFrame`].
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This field is optional.
|
||||
stream_data: StreamData<'a>,
|
||||
},
|
||||
/// [`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: Cow<'a, str>,
|
||||
/// 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<Cow<'a, str>>,
|
||||
/// 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<Cow<'a, str>>,
|
||||
/// 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<'a>>,
|
||||
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
||||
/// [`VariantStream::ExtXIFrame`].
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This field is optional.
|
||||
stream_data: StreamData<'a>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> VariantStream<'a> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> VariantStream<'static> {
|
||||
match self {
|
||||
VariantStream::ExtXIFrame { uri, stream_data } => {
|
||||
VariantStream::ExtXIFrame {
|
||||
uri: Cow::Owned(uri.into_owned()),
|
||||
stream_data: stream_data.into_owned(),
|
||||
}
|
||||
}
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri,
|
||||
frame_rate,
|
||||
audio,
|
||||
subtitles,
|
||||
closed_captions,
|
||||
stream_data,
|
||||
} => {
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: Cow::Owned(uri.into_owned()),
|
||||
frame_rate,
|
||||
audio: audio.map(|v| Cow::Owned(v.into_owned())),
|
||||
subtitles: subtitles.map(|v| Cow::Owned(v.into_owned())),
|
||||
closed_captions: closed_captions.map(ClosedCaptions::into_owned),
|
||||
stream_data: stream_data.into_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl<'a> RequiredVersion for VariantStream<'a> {
|
||||
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<'a> fmt::Display for VariantStream<'a> {
|
||||
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<'a> TryFrom<&'a str> for VariantStream<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) {
|
||||
let uri = AttributePairs::new(input)
|
||||
.find_map(|(key, value)| (key == "URI").then(|| unquote(value)))
|
||||
.ok_or_else(|| Error::missing_value("URI"))?;
|
||||
|
||||
Ok(Self::ExtXIFrame {
|
||||
uri,
|
||||
stream_data: StreamData::try_from(input)?,
|
||||
})
|
||||
} 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(ClosedCaptions::try_from(value).unwrap());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self::ExtXStreamInf {
|
||||
uri: Cow::Borrowed(uri),
|
||||
frame_rate,
|
||||
audio,
|
||||
subtitles,
|
||||
closed_captions,
|
||||
stream_data: StreamData::try_from(first_line)?,
|
||||
})
|
||||
} 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<'a> Deref for VariantStream<'a> {
|
||||
type Target = StreamData<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self {
|
||||
Self::ExtXIFrame { stream_data, .. } | Self::ExtXStreamInf { stream_data, .. } => {
|
||||
stream_data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<&VariantStream<'a>> for VariantStream<'a> {
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use {Error, ErrorKind, Result};
|
||||
use attribute::AttributePairs;
|
||||
use types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
||||
use super::parse_yes_or_no;
|
||||
|
||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
|
||||
///
|
||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXIndependentSegments;
|
||||
impl ExtXIndependentSegments {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXIndependentSegments {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Self::PREFIX.fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXIndependentSegments {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||
Ok(ExtXIndependentSegments)
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.5.2. EXT-X-START]
|
||||
///
|
||||
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ExtXStart {
|
||||
time_offset: SignedDecimalFloatingPoint,
|
||||
precise: bool,
|
||||
}
|
||||
impl ExtXStart {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
|
||||
|
||||
/// Makes a new `ExtXStart` tag.
|
||||
pub fn new(time_offset: SignedDecimalFloatingPoint) -> Self {
|
||||
ExtXStart {
|
||||
time_offset,
|
||||
precise: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new `ExtXStart` tag with the given `precise` flag.
|
||||
pub fn with_precise(time_offset: SignedDecimalFloatingPoint, precise: bool) -> Self {
|
||||
ExtXStart {
|
||||
time_offset,
|
||||
precise,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the time offset of the media segments in the playlist.
|
||||
pub fn time_offset(&self) -> SignedDecimalFloatingPoint {
|
||||
self.time_offset
|
||||
}
|
||||
|
||||
/// Returns whether clients should not render media stream whose presentation times are
|
||||
/// prior to the specified time offset.
|
||||
pub fn precise(&self) -> bool {
|
||||
self.precise
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXStart {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "TIME-OFFSET={}", self.time_offset)?;
|
||||
if self.precise {
|
||||
write!(f, ",PRECISE=YES")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXStart {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
|
||||
let mut time_offset = None;
|
||||
let mut precise = false;
|
||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"TIME-OFFSET" => time_offset = Some(track!(value.parse())?),
|
||||
"PRECISE" => precise = track!(parse_yes_or_no(value))?,
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let time_offset = track_assert_some!(time_offset, ErrorKind::InvalidInput);
|
||||
Ok(ExtXStart {
|
||||
time_offset,
|
||||
precise,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_independent_segments() {
|
||||
let tag = ExtXIndependentSegments;
|
||||
let text = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_start() {
|
||||
let tag = ExtXStart::new(SignedDecimalFloatingPoint::new(-1.23).unwrap());
|
||||
let text = "#EXT-X-START:TIME-OFFSET=-1.23";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
|
||||
let tag = ExtXStart::with_precise(SignedDecimalFloatingPoint::new(1.23).unwrap(), true);
|
||||
let text = "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
}
|
|
@ -1,280 +0,0 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use trackable::error::ErrorKindExt;
|
||||
|
||||
use {Error, ErrorKind, Result};
|
||||
use types::{PlaylistType, ProtocolVersion};
|
||||
|
||||
/// [4.3.3.1. EXT-X-TARGETDURATION]
|
||||
///
|
||||
/// [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)]
|
||||
pub struct ExtXTargetDuration {
|
||||
duration: Duration,
|
||||
}
|
||||
impl ExtXTargetDuration {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
|
||||
|
||||
/// Makes a new `ExtXTargetduration` tag.
|
||||
///
|
||||
/// Note that the nanoseconds part of the `duration` will be discarded.
|
||||
pub fn new(duration: Duration) -> Self {
|
||||
let duration = Duration::from_secs(duration.as_secs());
|
||||
ExtXTargetDuration { duration }
|
||||
}
|
||||
|
||||
/// Returns the maximum media segment duration in the associated playlist.
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXTargetDuration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.duration.as_secs())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXTargetDuration {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||
Ok(ExtXTargetDuration {
|
||||
duration: Duration::from_secs(duration),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]
|
||||
///
|
||||
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXMediaSequence {
|
||||
seq_num: u64,
|
||||
}
|
||||
impl ExtXMediaSequence {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
||||
|
||||
/// Makes a new `ExtXMediaSequence` tag.
|
||||
pub fn new(seq_num: u64) -> Self {
|
||||
ExtXMediaSequence { seq_num }
|
||||
}
|
||||
|
||||
/// Returns the sequence number of the first media segment that appears in the associated playlist.
|
||||
pub fn seq_num(&self) -> u64 {
|
||||
self.seq_num
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXMediaSequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXMediaSequence {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||
Ok(ExtXMediaSequence { seq_num })
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
||||
///
|
||||
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.3
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXDiscontinuitySequence {
|
||||
seq_num: u64,
|
||||
}
|
||||
impl ExtXDiscontinuitySequence {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
||||
|
||||
/// Makes a new `ExtXDiscontinuitySequence` tag.
|
||||
pub fn new(seq_num: u64) -> Self {
|
||||
ExtXDiscontinuitySequence { seq_num }
|
||||
}
|
||||
|
||||
/// Returns the discontinuity sequence number of
|
||||
/// the first media segment that appears in the associated playlist.
|
||||
pub fn seq_num(&self) -> u64 {
|
||||
self.seq_num
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXDiscontinuitySequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXDiscontinuitySequence {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||
Ok(ExtXDiscontinuitySequence { seq_num })
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.3.4. EXT-X-ENDLIST]
|
||||
///
|
||||
/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXEndList;
|
||||
impl ExtXEndList {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXEndList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Self::PREFIX.fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXEndList {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||
Ok(ExtXEndList)
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
||||
///
|
||||
/// [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)]
|
||||
pub struct ExtXPlaylistType {
|
||||
playlist_type: PlaylistType,
|
||||
}
|
||||
impl ExtXPlaylistType {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||
|
||||
/// Makes a new `ExtXPlaylistType` tag.
|
||||
pub fn new(playlist_type: PlaylistType) -> Self {
|
||||
ExtXPlaylistType { playlist_type }
|
||||
}
|
||||
|
||||
/// Returns the type of the associated media playlist.
|
||||
pub fn playlist_type(&self) -> PlaylistType {
|
||||
self.playlist_type
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXPlaylistType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.playlist_type)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXPlaylistType {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||
Ok(ExtXPlaylistType { playlist_type })
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]
|
||||
///
|
||||
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXIFramesOnly;
|
||||
impl ExtXIFramesOnly {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V4
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXIFramesOnly {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Self::PREFIX.fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXIFramesOnly {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||
Ok(ExtXIFramesOnly)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_targetduration() {
|
||||
let tag = ExtXTargetDuration::new(Duration::from_secs(5));
|
||||
let text = "#EXT-X-TARGETDURATION:5";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_media_sequence() {
|
||||
let tag = ExtXMediaSequence::new(123);
|
||||
let text = "#EXT-X-MEDIA-SEQUENCE:123";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_discontinuity_sequence() {
|
||||
let tag = ExtXDiscontinuitySequence::new(123);
|
||||
let text = "#EXT-X-DISCONTINUITY-SEQUENCE:123";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_endlist() {
|
||||
let tag = ExtXEndList;
|
||||
let text = "#EXT-X-ENDLIST";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_playlist_type() {
|
||||
let tag = ExtXPlaylistType::new(PlaylistType::Vod);
|
||||
let text = "#EXT-X-PLAYLIST-TYPE:VOD";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_i_frames_only() {
|
||||
let tag = ExtXIFramesOnly;
|
||||
let text = "#EXT-X-I-FRAMES-ONLY";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
||||
}
|
||||
}
|
75
src/tags/media_playlist/discontinuity_sequence.rs
Normal file
75
src/tags/media_playlist/discontinuity_sequence.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Allows synchronization between different renditions of the same
|
||||
/// [`VariantStream`].
|
||||
///
|
||||
/// [`VariantStream`]: crate::tags::VariantStream
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
|
||||
pub(crate) struct ExtXDiscontinuitySequence(pub usize);
|
||||
|
||||
impl ExtXDiscontinuitySequence {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXDiscontinuitySequence {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXDiscontinuitySequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
//
|
||||
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXDiscontinuitySequence {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
||||
|
||||
Ok(Self(seq_num))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuitySequence(123).to_string(),
|
||||
"#EXT-X-DISCONTINUITY-SEQUENCE:123".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuitySequence(123).required_version(),
|
||||
ProtocolVersion::V1
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuitySequence(123),
|
||||
ExtXDiscontinuitySequence::try_from("#EXT-X-DISCONTINUITY-SEQUENCE:123").unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXDiscontinuitySequence::try_from("#EXT-X-DISCONTINUITY-SEQUENCE:12A"),
|
||||
Err(Error::parse_int("12A", "12A".parse::<u64>().expect_err("")))
|
||||
);
|
||||
}
|
||||
}
|
60
src/tags/media_playlist/end_list.rs
Normal file
60
src/tags/media_playlist/end_list.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Indicates that no more [`MediaSegment`]s will be added to the
|
||||
/// [`MediaPlaylist`] file.
|
||||
///
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub(crate) struct ExtXEndList;
|
||||
|
||||
impl ExtXEndList {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXEndList {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXEndList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXEndList {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(ExtXEndList.to_string(), "#EXT-X-ENDLIST".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXEndList,
|
||||
ExtXEndList::try_from("#EXT-X-ENDLIST").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXEndList.required_version(), ProtocolVersion::V1);
|
||||
}
|
||||
}
|
58
src/tags/media_playlist/i_frames_only.rs
Normal file
58
src/tags/media_playlist/i_frames_only.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub(crate) struct ExtXIFramesOnly;
|
||||
|
||||
impl ExtXIFramesOnly {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V4`].
|
||||
impl RequiredVersion for ExtXIFramesOnly {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXIFramesOnly {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXIFramesOnly {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXIFramesOnly.to_string(),
|
||||
"#EXT-X-I-FRAMES-ONLY".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXIFramesOnly,
|
||||
ExtXIFramesOnly::try_from("#EXT-X-I-FRAMES-ONLY").unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXIFramesOnly.required_version(), ProtocolVersion::V4)
|
||||
}
|
||||
}
|
68
src/tags/media_playlist/media_sequence.rs
Normal file
68
src/tags/media_playlist/media_sequence.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Indicates the Media Sequence Number of the first `MediaSegment` that
|
||||
/// appears in a `MediaPlaylist`.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub(crate) struct ExtXMediaSequence(pub usize);
|
||||
|
||||
impl ExtXMediaSequence {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXMediaSequence {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXMediaSequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
//
|
||||
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXMediaSequence {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
||||
|
||||
Ok(Self(seq_num))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXMediaSequence(123).to_string(),
|
||||
"#EXT-X-MEDIA-SEQUENCE:123".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXMediaSequence(123).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXMediaSequence(123),
|
||||
ExtXMediaSequence::try_from("#EXT-X-MEDIA-SEQUENCE:123").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
11
src/tags/media_playlist/mod.rs
Normal file
11
src/tags/media_playlist/mod.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
pub(crate) mod discontinuity_sequence;
|
||||
pub(crate) mod end_list;
|
||||
pub(crate) mod i_frames_only;
|
||||
pub(crate) mod media_sequence;
|
||||
pub(crate) mod target_duration;
|
||||
|
||||
pub(crate) use discontinuity_sequence::*;
|
||||
pub(crate) use end_list::*;
|
||||
pub(crate) use i_frames_only::*;
|
||||
pub(crate) use media_sequence::*;
|
||||
pub(crate) use target_duration::*;
|
68
src/tags/media_playlist/target_duration.rs
Normal file
68
src/tags/media_playlist/target_duration.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Specifies the maximum `MediaSegment` duration.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
|
||||
pub(crate) struct ExtXTargetDuration(pub Duration);
|
||||
|
||||
impl ExtXTargetDuration {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXTargetDuration {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXTargetDuration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.0.as_secs())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXTargetDuration {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?
|
||||
.parse()
|
||||
.map_err(|e| Error::parse_int(input, e))?;
|
||||
|
||||
Ok(Self(Duration::from_secs(input)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXTargetDuration(Duration::from_secs(5)).to_string(),
|
||||
"#EXT-X-TARGETDURATION:5".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXTargetDuration(Duration::from_secs(5)).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXTargetDuration(Duration::from_secs(5)),
|
||||
ExtXTargetDuration::try_from("#EXT-X-TARGETDURATION:5").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,614 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use trackable::error::ErrorKindExt;
|
||||
|
||||
use {Error, ErrorKind, Result};
|
||||
use attribute::AttributePairs;
|
||||
use types::{ByteRange, DecimalFloatingPoint, DecryptionKey, ProtocolVersion, QuotedString,
|
||||
SingleLineString};
|
||||
|
||||
/// [4.3.2.1. EXTINF]
|
||||
///
|
||||
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtInf {
|
||||
duration: Duration,
|
||||
title: Option<SingleLineString>,
|
||||
}
|
||||
impl ExtInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
||||
|
||||
/// Makes a new `ExtInf` tag.
|
||||
pub fn new(duration: Duration) -> Self {
|
||||
ExtInf {
|
||||
duration,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new `ExtInf` tag with the given title.
|
||||
pub fn with_title(duration: Duration, title: SingleLineString) -> Self {
|
||||
ExtInf {
|
||||
duration,
|
||||
title: Some(title),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration of the associated media segment.
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Returns the title of the associated media segment.
|
||||
pub fn title(&self) -> Option<&SingleLineString> {
|
||||
self.title.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
if self.duration.subsec_nanos() == 0 {
|
||||
ProtocolVersion::V1
|
||||
} else {
|
||||
ProtocolVersion::V3
|
||||
}
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtInf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
|
||||
let duration = (self.duration.as_secs() as f64)
|
||||
+ (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0);
|
||||
write!(f, "{}", duration)?;
|
||||
|
||||
if let Some(ref title) = self.title {
|
||||
write!(f, ",{}", title)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtInf {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ',');
|
||||
|
||||
let seconds: DecimalFloatingPoint =
|
||||
may_invalid!(tokens.next().expect("Never fails").parse())?;
|
||||
let duration = seconds.to_duration();
|
||||
|
||||
let title = if let Some(title) = tokens.next() {
|
||||
Some(track!(SingleLineString::new(title))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(ExtInf { duration, title })
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.2.2. EXT-X-BYTERANGE]
|
||||
///
|
||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXByteRange {
|
||||
range: ByteRange,
|
||||
}
|
||||
impl ExtXByteRange {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||
|
||||
/// Makes a new `ExtXByteRange` tag.
|
||||
pub fn new(range: ByteRange) -> Self {
|
||||
ExtXByteRange { range }
|
||||
}
|
||||
|
||||
/// Returns the range of the associated media segment.
|
||||
pub fn range(&self) -> ByteRange {
|
||||
self.range
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V4
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXByteRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.range)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXByteRange {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||
Ok(ExtXByteRange { range })
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]
|
||||
///
|
||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXDiscontinuity;
|
||||
impl ExtXDiscontinuity {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXDiscontinuity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Self::PREFIX.fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXDiscontinuity {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||
Ok(ExtXDiscontinuity)
|
||||
}
|
||||
}
|
||||
|
||||
/// [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
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXKey {
|
||||
key: Option<DecryptionKey>,
|
||||
}
|
||||
impl ExtXKey {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||
|
||||
/// Makes a new `ExtXKey` tag.
|
||||
pub fn new(key: DecryptionKey) -> Self {
|
||||
ExtXKey { key: Some(key) }
|
||||
}
|
||||
|
||||
/// Makes a new `ExtXKey` tag without a decryption key.
|
||||
///
|
||||
/// This tag has the `METHDO=NONE` attribute.
|
||||
pub fn new_without_key() -> Self {
|
||||
ExtXKey { key: None }
|
||||
}
|
||||
|
||||
/// Returns the decryption key for the following media segments and media initialization sections.
|
||||
pub fn key(&self) -> Option<&DecryptionKey> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
self.key
|
||||
.as_ref()
|
||||
.map_or(ProtocolVersion::V1, |k| k.requires_version())
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
if let Some(ref key) = self.key {
|
||||
write!(f, "{}", key)?;
|
||||
} else {
|
||||
write!(f, "METHOD=NONE")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXKey {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
||||
|
||||
if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) {
|
||||
for attr in AttributePairs::parse(suffix) {
|
||||
let (key, _) = track!(attr)?;
|
||||
track_assert_ne!(key, "URI", ErrorKind::InvalidInput);
|
||||
track_assert_ne!(key, "IV", ErrorKind::InvalidInput);
|
||||
track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput);
|
||||
track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput);
|
||||
}
|
||||
Ok(ExtXKey { key: None })
|
||||
} else {
|
||||
let key = track!(suffix.parse())?;
|
||||
Ok(ExtXKey { key: Some(key) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.2.5. EXT-X-MAP]
|
||||
///
|
||||
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXMap {
|
||||
uri: QuotedString,
|
||||
range: Option<ByteRange>,
|
||||
}
|
||||
impl ExtXMap {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
||||
|
||||
/// Makes a new `ExtXMap` tag.
|
||||
pub fn new(uri: QuotedString) -> Self {
|
||||
ExtXMap { uri, range: None }
|
||||
}
|
||||
|
||||
/// Makes a new `ExtXMap` tag with the given range.
|
||||
pub fn with_range(uri: QuotedString, range: ByteRange) -> Self {
|
||||
ExtXMap {
|
||||
uri,
|
||||
range: Some(range),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the URI that identifies a resource that contains the media initialization section.
|
||||
pub fn uri(&self) -> &QuotedString {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Returns the range of the media initialization section.
|
||||
pub fn range(&self) -> Option<ByteRange> {
|
||||
self.range
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V6
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXMap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "URI={}", self.uri)?;
|
||||
if let Some(ref x) = self.range {
|
||||
write!(f, ",BYTERANGE=\"{}\"", x)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXMap {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
|
||||
let mut uri = None;
|
||||
let mut range = None;
|
||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"URI" => uri = Some(track!(value.parse())?),
|
||||
"BYTERANGE" => {
|
||||
let s: QuotedString = track!(value.parse())?;
|
||||
range = Some(track!(s.parse())?);
|
||||
}
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
||||
Ok(ExtXMap { uri, range })
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
||||
///
|
||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXProgramDateTime {
|
||||
date_time: SingleLineString,
|
||||
}
|
||||
impl ExtXProgramDateTime {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||
|
||||
/// Makes a new `ExtXProgramDateTime` tag.
|
||||
pub fn new(date_time: SingleLineString) -> Self {
|
||||
ExtXProgramDateTime { date_time }
|
||||
}
|
||||
|
||||
/// Returns the date-time of the first sample of the associated media segment.
|
||||
pub fn date_time(&self) -> &SingleLineString {
|
||||
&self.date_time
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXProgramDateTime {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.date_time)
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXProgramDateTime {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
||||
Ok(ExtXProgramDateTime {
|
||||
date_time: track!(SingleLineString::new(suffix))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// [4.3.2.7. EXT-X-DATERANGE]
|
||||
///
|
||||
/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
||||
///
|
||||
/// TODO: Implement properly
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXDateRange {
|
||||
pub id: QuotedString,
|
||||
pub class: Option<QuotedString>,
|
||||
pub start_date: QuotedString,
|
||||
pub end_date: Option<QuotedString>,
|
||||
pub duration: Option<Duration>,
|
||||
pub planned_duration: Option<Duration>,
|
||||
pub scte35_cmd: Option<QuotedString>,
|
||||
pub scte35_out: Option<QuotedString>,
|
||||
pub scte35_in: Option<QuotedString>,
|
||||
pub end_on_next: bool,
|
||||
pub client_attributes: BTreeMap<String, String>,
|
||||
}
|
||||
impl ExtXDateRange {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ExtXDateRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "ID={}", self.id)?;
|
||||
if let Some(ref x) = self.class {
|
||||
write!(f, ",CLASS={}", x)?;
|
||||
}
|
||||
write!(f, ",START-DATE={}", self.start_date)?;
|
||||
if let Some(ref x) = self.end_date {
|
||||
write!(f, ",END-DATE={}", x)?;
|
||||
}
|
||||
if let Some(x) = self.duration {
|
||||
write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?;
|
||||
}
|
||||
if let Some(x) = self.planned_duration {
|
||||
write!(
|
||||
f,
|
||||
",PLANNED-DURATION={}",
|
||||
DecimalFloatingPoint::from_duration(x)
|
||||
)?;
|
||||
}
|
||||
if let Some(ref x) = self.scte35_cmd {
|
||||
write!(f, ",SCTE35-CMD={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.scte35_out {
|
||||
write!(f, ",SCTE35-OUT={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.scte35_in {
|
||||
write!(f, ",SCTE35-IN={}", x)?;
|
||||
}
|
||||
if self.end_on_next {
|
||||
write!(f, ",END-ON-NEXT=YES",)?;
|
||||
}
|
||||
for (k, v) in &self.client_attributes {
|
||||
write!(f, ",{}={}", k, v)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ExtXDateRange {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||
|
||||
let mut id = None;
|
||||
let mut class = None;
|
||||
let mut start_date = None;
|
||||
let mut end_date = None;
|
||||
let mut duration = None;
|
||||
let mut planned_duration = None;
|
||||
let mut scte35_cmd = None;
|
||||
let mut scte35_out = None;
|
||||
let mut scte35_in = None;
|
||||
let mut end_on_next = false;
|
||||
let mut client_attributes = BTreeMap::new();
|
||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"ID" => id = Some(track!(value.parse())?),
|
||||
"CLASS" => class = Some(track!(value.parse())?),
|
||||
"START-DATE" => start_date = Some(track!(value.parse())?),
|
||||
"END-DATE" => end_date = Some(track!(value.parse())?),
|
||||
"DURATION" => {
|
||||
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
||||
duration = Some(seconds.to_duration());
|
||||
}
|
||||
"PLANNED-DURATION" => {
|
||||
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
||||
planned_duration = Some(seconds.to_duration());
|
||||
}
|
||||
"SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?),
|
||||
"SCTE35-OUT" => scte35_out = Some(track!(value.parse())?),
|
||||
"SCTE35-IN" => scte35_in = Some(track!(value.parse())?),
|
||||
"END-ON-NEXT" => {
|
||||
track_assert_eq!(value, "YES", ErrorKind::InvalidInput);
|
||||
end_on_next = true;
|
||||
}
|
||||
_ => {
|
||||
if key.starts_with("X-") {
|
||||
client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned());
|
||||
} else {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = track_assert_some!(id, ErrorKind::InvalidInput);
|
||||
let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput);
|
||||
if end_on_next {
|
||||
track_assert!(class.is_some(), ErrorKind::InvalidInput);
|
||||
}
|
||||
Ok(ExtXDateRange {
|
||||
id,
|
||||
class,
|
||||
start_date,
|
||||
end_date,
|
||||
duration,
|
||||
planned_duration,
|
||||
scte35_cmd,
|
||||
scte35_out,
|
||||
scte35_in,
|
||||
end_on_next,
|
||||
client_attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::Duration;
|
||||
|
||||
use types::{EncryptionMethod, InitializationVector};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extinf() {
|
||||
let tag = ExtInf::new(Duration::from_secs(5));
|
||||
assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), "#EXTINF:5");
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
|
||||
let tag = ExtInf::with_title(
|
||||
Duration::from_secs(5),
|
||||
SingleLineString::new("foo").unwrap(),
|
||||
);
|
||||
assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), "#EXTINF:5,foo");
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
|
||||
let tag = ExtInf::new(Duration::from_millis(1234));
|
||||
assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), "#EXTINF:1.234");
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_byterange() {
|
||||
let tag = ExtXByteRange::new(ByteRange {
|
||||
length: 3,
|
||||
start: None,
|
||||
});
|
||||
assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3");
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
||||
|
||||
let tag = ExtXByteRange::new(ByteRange {
|
||||
length: 3,
|
||||
start: Some(5),
|
||||
});
|
||||
assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5");
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_discontinuity() {
|
||||
let tag = ExtXDiscontinuity;
|
||||
assert_eq!("#EXT-X-DISCONTINUITY".parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), "#EXT-X-DISCONTINUITY");
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_key() {
|
||||
let tag = ExtXKey::new_without_key();
|
||||
let text = "#EXT-X-KEY:METHOD=NONE";
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
|
||||
let tag = ExtXKey::new(DecryptionKey {
|
||||
method: EncryptionMethod::Aes128,
|
||||
uri: QuotedString::new("foo").unwrap(),
|
||||
iv: None,
|
||||
key_format: None,
|
||||
key_format_versions: None,
|
||||
});
|
||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
|
||||
let tag = ExtXKey::new(DecryptionKey {
|
||||
method: EncryptionMethod::Aes128,
|
||||
uri: QuotedString::new("foo").unwrap(),
|
||||
iv: Some(InitializationVector([
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
|
||||
])),
|
||||
key_format: None,
|
||||
key_format_versions: None,
|
||||
});
|
||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V2);
|
||||
|
||||
let tag = ExtXKey::new(DecryptionKey {
|
||||
method: EncryptionMethod::Aes128,
|
||||
uri: QuotedString::new("foo").unwrap(),
|
||||
iv: Some(InitializationVector([
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
|
||||
])),
|
||||
key_format: Some(QuotedString::new("baz").unwrap()),
|
||||
key_format_versions: None,
|
||||
});
|
||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_map() {
|
||||
let tag = ExtXMap::new(QuotedString::new("foo").unwrap());
|
||||
let text = r#"#EXT-X-MAP:URI="foo""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||
|
||||
let tag = ExtXMap::with_range(
|
||||
QuotedString::new("foo").unwrap(),
|
||||
ByteRange {
|
||||
length: 9,
|
||||
start: Some(2),
|
||||
},
|
||||
);
|
||||
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
||||
track_try_unwrap!(ExtXMap::from_str(text));
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ext_x_program_date_time() {
|
||||
let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00";
|
||||
assert!(text.parse::<ExtXProgramDateTime>().is_ok());
|
||||
|
||||
let tag = text.parse::<ExtXProgramDateTime>().unwrap();
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
}
|
||||
}
|
257
src/tags/media_segment/byte_range.rs
Normal file
257
src/tags/media_segment/byte_range.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
use derive_more::{AsMut, AsRef, Deref, DerefMut, From};
|
||||
|
||||
use crate::types::{ByteRange, ProtocolVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Indicates that a [`MediaSegment`] is a sub-range of the resource identified
|
||||
/// by its `URI`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Constructing an [`ExtXByteRange`]:
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||
/// assert_eq!(ExtXByteRange::from(22..55), ExtXByteRange::from(22..=54));
|
||||
/// ```
|
||||
///
|
||||
/// It is also possible to omit the start, in which case it assumes that the
|
||||
/// [`ExtXByteRange`] starts at the byte after the end of the previous
|
||||
/// [`ExtXByteRange`] or 0 if there is no previous one.
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||
/// assert_eq!(ExtXByteRange::from(..55), ExtXByteRange::from(..=54));
|
||||
/// ```
|
||||
///
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
#[derive(
|
||||
AsRef, AsMut, From, Deref, DerefMut, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord,
|
||||
)]
|
||||
#[from(forward)]
|
||||
pub struct ExtXByteRange(ByteRange);
|
||||
|
||||
impl ExtXByteRange {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||
|
||||
/// Adds `num` to the `start` and `end` of the range.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||
/// 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));
|
||||
/// ```
|
||||
///
|
||||
/// # 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)) }
|
||||
|
||||
/// Subtracts `num` from the `start` and `end` of the range.
|
||||
///
|
||||
/// # 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::types::ByteRange;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ExtXByteRange::from(2..11).as_byte_range(),
|
||||
/// &ByteRange::from(2..11)
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn as_byte_range(&self) -> &ByteRange { &self.0 }
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V4`].
|
||||
impl RequiredVersion for ExtXByteRange {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 }
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)] // Some magic `From` blanket impl is going on that means this can't be done.
|
||||
impl Into<ByteRange> for ExtXByteRange {
|
||||
fn into(self) -> ByteRange { self.0 }
|
||||
}
|
||||
|
||||
impl<T> Sub<T> for ExtXByteRange
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "{}", self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXByteRange {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
Ok(Self(ByteRange::try_from(input)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXByteRange::from(2..15).to_string(),
|
||||
"#EXT-X-BYTERANGE:13@2".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXByteRange::from(..22).to_string(),
|
||||
"#EXT-X-BYTERANGE:22".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXByteRange::from(2..15),
|
||||
ExtXByteRange::try_from("#EXT-X-BYTERANGE:13@2").unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXByteRange::from(..22),
|
||||
ExtXByteRange::try_from("#EXT-X-BYTERANGE:22").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deref() {
|
||||
let byte_range = ExtXByteRange::from(0..22);
|
||||
|
||||
assert_eq!(byte_range.len(), 22);
|
||||
assert_eq!(byte_range.start(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deref_mut() {
|
||||
let mut byte_range = ExtXByteRange::from(10..110);
|
||||
|
||||
byte_range.set_start(Some(50));
|
||||
|
||||
assert_eq!(byte_range.len(), 60);
|
||||
assert_eq!(byte_range.start(), Some(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXByteRange::from(5..20).required_version(),
|
||||
ProtocolVersion::V4
|
||||
);
|
||||
}
|
||||
}
|
699
src/tags/media_segment/date_range.rs
Normal file
699
src/tags/media_segment/date_range.rs
Normal file
|
@ -0,0 +1,699 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
||||
use derive_builder::Builder;
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, Value};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The [`ExtXDateRange`] tag associates a date range (i.e., a range of time
|
||||
/// defined by a starting and ending date) with a set of attribute/value pairs.
|
||||
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
#[builder(setter(into))]
|
||||
#[shorthand(enable(must_use, into))]
|
||||
pub struct ExtXDateRange<'a> {
|
||||
/// A string that uniquely identifies an [`ExtXDateRange`] in the playlist.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is required.
|
||||
id: Cow<'a, str>,
|
||||
/// A client-defined string that specifies some set of attributes and their
|
||||
/// associated value semantics. All [`ExtXDateRange`]s with the same class
|
||||
/// attribute value must adhere to these semantics.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(setter(strip_option), default)]
|
||||
class: Option<Cow<'a, str>>,
|
||||
/// The date at which the [`ExtXDateRange`] begins.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is required by the spec wording, but optional in examples
|
||||
/// elsewhere in the same document. Some implementations omit it in
|
||||
/// practise (e.g. for SCTE 'explicit-IN' markers) so it is optional
|
||||
/// here.
|
||||
#[cfg(feature = "chrono")]
|
||||
#[shorthand(enable(copy), disable(into))]
|
||||
#[builder(setter(strip_option), default)]
|
||||
start_date: Option<DateTime<FixedOffset>>,
|
||||
/// The date at which the [`ExtXDateRange`] begins.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is required by the spec wording, but optional in examples
|
||||
/// elsewhere in the same document. Some implementations omit it in
|
||||
/// practise (e.g. for SCTE 'explicit-IN' markers) so it is optional
|
||||
/// here.
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
#[builder(setter(strip_option), default)]
|
||||
start_date: Option<Cow<'a, str>>,
|
||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||
/// later than the value of the [`start-date`] attribute.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
///
|
||||
/// [`start-date`]: #method.start_date
|
||||
#[cfg(feature = "chrono")]
|
||||
#[shorthand(enable(copy), disable(into))]
|
||||
#[builder(setter(strip_option), default)]
|
||||
end_date: Option<DateTime<FixedOffset>>,
|
||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||
/// later than the value of the start-date field.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
///
|
||||
/// [`start-date`]: #method.start_date
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
#[builder(setter(strip_option), default)]
|
||||
end_date: Option<Cow<'a, str>>,
|
||||
/// The duration of the [`ExtXDateRange`]. A single instant in time (e.g.,
|
||||
/// crossing a finish line) should be represented with a duration of 0.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(setter(strip_option), default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub duration: Option<Duration>,
|
||||
/// This field indicates the expected duration of an [`ExtXDateRange`],
|
||||
/// whose actual duration is not yet known.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(setter(strip_option), default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub planned_duration: Option<Duration>,
|
||||
/// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and
|
||||
/// Telecommunications Engineers standard that describes the inline
|
||||
/// insertion of cue tones in mpeg-ts streams.
|
||||
///
|
||||
/// SCTE-35 was originally used in the US to signal a local ad insertion
|
||||
/// opportunity in the transport streams, and in Europe to insert local TV
|
||||
/// programs (e.g. local news transmissions). It is now used to signal all
|
||||
/// kinds of program and ad events in linear transport streams and in newer
|
||||
/// ABR delivery formats such as HLS and DASH.
|
||||
///
|
||||
/// <https://en.wikipedia.org/wiki/SCTE-35>
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(setter(strip_option), default)]
|
||||
scte35_cmd: Option<Cow<'a, str>>,
|
||||
/// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and
|
||||
/// Telecommunications Engineers standard that describes the inline
|
||||
/// insertion of cue tones in mpeg-ts streams.
|
||||
///
|
||||
/// SCTE-35 was originally used in the US to signal a local ad insertion
|
||||
/// opportunity in the transport streams, and in Europe to insert local TV
|
||||
/// programs (e.g. local news transmissions). It is now used to signal all
|
||||
/// kinds of program and ad events in linear transport streams and in newer
|
||||
/// ABR delivery formats such as HLS and DASH.
|
||||
///
|
||||
/// <https://en.wikipedia.org/wiki/SCTE-35>
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(setter(strip_option), default)]
|
||||
scte35_out: Option<Cow<'a, str>>,
|
||||
/// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and
|
||||
/// Telecommunications Engineers standard that describes the inline
|
||||
/// insertion of cue tones in mpeg-ts streams.
|
||||
///
|
||||
/// SCTE-35 was originally used in the US to signal a local ad insertion
|
||||
/// opportunity in the transport streams, and in Europe to insert local TV
|
||||
/// programs (e.g. local news transmissions). It is now used to signal all
|
||||
/// kinds of program and ad events in linear transport streams and in newer
|
||||
/// ABR delivery formats such as HLS and DASH.
|
||||
///
|
||||
/// <https://en.wikipedia.org/wiki/SCTE-35>
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(setter(strip_option), default)]
|
||||
scte35_in: Option<Cow<'a, str>>,
|
||||
/// This field indicates that the [`ExtXDateRange::end_date`] is equal to
|
||||
/// the [`ExtXDateRange::start_date`] of the following range.
|
||||
///
|
||||
/// The following range is the [`ExtXDateRange`] with the same class, that
|
||||
/// has the earliest start date after the start date of the range in
|
||||
/// question.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(default)]
|
||||
#[shorthand(enable(skip))]
|
||||
pub end_on_next: bool,
|
||||
/// The `"X-"` prefix defines a namespace reserved for client-defined
|
||||
/// attributes.
|
||||
///
|
||||
/// A client-attribute can only consist of uppercase characters (A-Z),
|
||||
/// numbers (0-9) and `-`.
|
||||
///
|
||||
/// Clients should use a reverse-dns naming scheme, when defining
|
||||
/// their own attribute names to avoid collisions.
|
||||
///
|
||||
/// An example of a client-defined attribute is
|
||||
/// `X-COM-EXAMPLE-AD-ID="XYZ123"`.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(default)]
|
||||
#[shorthand(enable(collection_magic), disable(set, get))]
|
||||
pub client_attributes: BTreeMap<Cow<'a, str>, Value<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> ExtXDateRangeBuilder<'a> {
|
||||
/// Inserts a key value pair.
|
||||
pub fn insert_client_attribute<K: Into<Cow<'a, str>>, V: Into<Value<'a>>>(
|
||||
&mut self,
|
||||
key: K,
|
||||
value: V,
|
||||
) -> &mut Self {
|
||||
let attrs = self.client_attributes.get_or_insert_with(BTreeMap::new);
|
||||
|
||||
attrs.insert(key.into(), value.into());
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ExtXDateRange<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
||||
|
||||
/// Makes a new [`ExtXDateRange`] tag.
|
||||
///
|
||||
/// # Example
|
||||
#[cfg_attr(
|
||||
feature = "chrono",
|
||||
doc = r#"
|
||||
```
|
||||
# use hls_m3u8::tags::ExtXDateRange;
|
||||
use chrono::offset::TimeZone;
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
|
||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
|
||||
let date_range = ExtXDateRange::new(
|
||||
"id",
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
);
|
||||
```
|
||||
"#
|
||||
)]
|
||||
#[cfg_attr(
|
||||
not(feature = "chrono"),
|
||||
doc = r#"
|
||||
```
|
||||
# use hls_m3u8::tags::ExtXDateRange;
|
||||
let date_range = ExtXDateRange::new("id", "2010-02-19T14:54:23.031+08:00");
|
||||
```
|
||||
"#
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn new<T: Into<Cow<'a, str>>, #[cfg(not(feature = "chrono"))] I: Into<Cow<'a, str>>>(
|
||||
id: T,
|
||||
#[cfg(feature = "chrono")] start_date: DateTime<FixedOffset>,
|
||||
#[cfg(not(feature = "chrono"))] start_date: I,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
class: None,
|
||||
#[cfg(feature = "chrono")]
|
||||
start_date: Some(start_date),
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
start_date: Some(start_date.into()),
|
||||
end_date: None,
|
||||
duration: None,
|
||||
planned_duration: None,
|
||||
scte35_cmd: None,
|
||||
scte35_out: None,
|
||||
scte35_in: None,
|
||||
end_on_next: false,
|
||||
client_attributes: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXDateRange`].
|
||||
///
|
||||
/// # Example
|
||||
#[cfg_attr(
|
||||
feature = "chrono",
|
||||
doc = r#"
|
||||
```
|
||||
# use hls_m3u8::tags::ExtXDateRange;
|
||||
use std::time::Duration;
|
||||
use chrono::{FixedOffset, TimeZone};
|
||||
use hls_m3u8::types::Float;
|
||||
|
||||
let date_range = ExtXDateRange::builder()
|
||||
.id("test_id")
|
||||
.class("test_class")
|
||||
.start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0))
|
||||
.end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0))
|
||||
.duration(Duration::from_secs_f64(60.1))
|
||||
.planned_duration(Duration::from_secs_f64(59.993))
|
||||
.insert_client_attribute("X-CUSTOM", Float::new(45.3))
|
||||
.scte35_cmd("0xFC002F0000000000FF2")
|
||||
.scte35_out("0xFC002F0000000000FF0")
|
||||
.scte35_in("0xFC002F0000000000FF1")
|
||||
.end_on_next(true)
|
||||
.build()?;
|
||||
# Ok::<(), Box<dyn std::error::Error>>(())
|
||||
```
|
||||
"#
|
||||
)]
|
||||
#[cfg_attr(
|
||||
not(feature = "chrono"),
|
||||
doc = r#"
|
||||
```
|
||||
# use hls_m3u8::tags::ExtXDateRange;
|
||||
use std::time::Duration;
|
||||
use hls_m3u8::types::Float;
|
||||
|
||||
let date_range = ExtXDateRange::builder()
|
||||
.id("test_id")
|
||||
.class("test_class")
|
||||
.start_date("2014-03-05T11:15:00Z")
|
||||
.end_date("2014-03-05T11:16:00Z")
|
||||
.duration(Duration::from_secs_f64(60.1))
|
||||
.planned_duration(Duration::from_secs_f64(59.993))
|
||||
.insert_client_attribute("X-CUSTOM", Float::new(45.3))
|
||||
.scte35_cmd("0xFC002F0000000000FF2")
|
||||
.scte35_out("0xFC002F0000000000FF0")
|
||||
.scte35_in("0xFC002F0000000000FF1")
|
||||
.end_on_next(true)
|
||||
.build()?;
|
||||
# Ok::<(), Box<dyn std::error::Error>>(())
|
||||
```
|
||||
"#
|
||||
)]
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn builder() -> ExtXDateRangeBuilder<'a> { ExtXDateRangeBuilder::default() }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ExtXDateRange<'static> {
|
||||
ExtXDateRange {
|
||||
id: Cow::Owned(self.id.into_owned()),
|
||||
class: self.class.map(|v| Cow::Owned(v.into_owned())),
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
start_date: self.start_date.map(|v| Cow::Owned(v.into_owned())),
|
||||
#[cfg(feature = "chrono")]
|
||||
start_date: self.start_date,
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
end_date: self.end_date.map(|v| Cow::Owned(v.into_owned())),
|
||||
#[cfg(feature = "chrono")]
|
||||
end_date: self.end_date,
|
||||
scte35_cmd: self.scte35_cmd.map(|v| Cow::Owned(v.into_owned())),
|
||||
scte35_out: self.scte35_out.map(|v| Cow::Owned(v.into_owned())),
|
||||
scte35_in: self.scte35_in.map(|v| Cow::Owned(v.into_owned())),
|
||||
client_attributes: {
|
||||
self.client_attributes
|
||||
.into_iter()
|
||||
.map(|(k, v)| (Cow::Owned(k.into_owned()), v.into_owned()))
|
||||
.collect()
|
||||
},
|
||||
duration: self.duration,
|
||||
end_on_next: self.end_on_next,
|
||||
planned_duration: self.planned_duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl<'a> RequiredVersion for ExtXDateRange<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtXDateRange<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let mut id = None;
|
||||
let mut class = None;
|
||||
let mut start_date = None;
|
||||
let mut end_date = None;
|
||||
let mut duration = None;
|
||||
let mut planned_duration = None;
|
||||
let mut scte35_cmd = None;
|
||||
let mut scte35_out = None;
|
||||
let mut scte35_in = None;
|
||||
let mut end_on_next = false;
|
||||
|
||||
let mut client_attributes = BTreeMap::new();
|
||||
|
||||
for (key, value) in AttributePairs::new(input) {
|
||||
match key {
|
||||
"ID" => id = Some(unquote(value)),
|
||||
"CLASS" => class = Some(unquote(value)),
|
||||
"START-DATE" => {
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
start_date = Some(unquote(value).parse().map_err(Error::chrono)?);
|
||||
}
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
start_date = Some(unquote(value));
|
||||
}
|
||||
}
|
||||
"END-DATE" => {
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
end_date = Some(unquote(value).parse().map_err(Error::chrono)?);
|
||||
}
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
end_date = Some(unquote(value));
|
||||
}
|
||||
}
|
||||
"DURATION" => {
|
||||
duration = Some(Duration::from_secs_f64(
|
||||
value.parse().map_err(|e| Error::parse_float(value, e))?,
|
||||
));
|
||||
}
|
||||
"PLANNED-DURATION" => {
|
||||
planned_duration = Some(Duration::from_secs_f64(
|
||||
value.parse().map_err(|e| Error::parse_float(value, e))?,
|
||||
));
|
||||
}
|
||||
"SCTE35-CMD" => scte35_cmd = Some(unquote(value)),
|
||||
"SCTE35-OUT" => scte35_out = Some(unquote(value)),
|
||||
"SCTE35-IN" => scte35_in = Some(unquote(value)),
|
||||
"END-ON-NEXT" => {
|
||||
if value != "YES" {
|
||||
return Err(Error::custom("`END-ON-NEXT` must be `YES`"));
|
||||
}
|
||||
end_on_next = true;
|
||||
}
|
||||
_ => {
|
||||
if key.starts_with("X-") {
|
||||
if key.chars().any(|c| {
|
||||
c.is_ascii_lowercase()
|
||||
|| !c.is_ascii()
|
||||
|| !(c.is_alphanumeric() || c == '-')
|
||||
}) {
|
||||
return Err(Error::custom(
|
||||
"a client attribute can only consist of uppercase ascii characters, numbers or `-`",
|
||||
));
|
||||
}
|
||||
|
||||
client_attributes.insert(Cow::Borrowed(key), Value::try_from(value)?);
|
||||
} else {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an
|
||||
// unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = id.ok_or_else(|| Error::missing_value("ID"))?;
|
||||
|
||||
if end_on_next && class.is_none() {
|
||||
return Err(Error::missing_attribute("CLASS"));
|
||||
} else if end_on_next && duration.is_some() {
|
||||
return Err(Error::unexpected_attribute("DURATION"));
|
||||
} else if end_on_next && end_date.is_some() {
|
||||
return Err(Error::unexpected_attribute("END-DATE"));
|
||||
}
|
||||
|
||||
// TODO: verify this without chrono?
|
||||
// https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
if let (Some(start_date), Some(Ok(duration)), Some(end_date)) = (
|
||||
start_date,
|
||||
duration.map(chrono::Duration::from_std),
|
||||
&end_date,
|
||||
) {
|
||||
if start_date + duration != *end_date {
|
||||
return Err(Error::custom(
|
||||
"end_date must be equal to start_date + duration",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
class,
|
||||
start_date,
|
||||
end_date,
|
||||
duration,
|
||||
planned_duration,
|
||||
scte35_cmd,
|
||||
scte35_out,
|
||||
scte35_in,
|
||||
end_on_next,
|
||||
client_attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtXDateRange<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "ID={}", quote(&self.id))?;
|
||||
|
||||
if let Some(value) = &self.class {
|
||||
write!(f, ",CLASS={}", quote(value))?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.start_date {
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
",START-DATE={}",
|
||||
quote(&value.to_rfc3339_opts(SecondsFormat::AutoSi, true))
|
||||
)?;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
write!(f, ",START-DATE={}", quote(value))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = &self.end_date {
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
",END-DATE={}",
|
||||
quote(&value.to_rfc3339_opts(SecondsFormat::AutoSi, true))
|
||||
)?;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
write!(f, ",END-DATE={}", quote(value))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = &self.duration {
|
||||
write!(f, ",DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.planned_duration {
|
||||
write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.scte35_cmd {
|
||||
write!(f, ",SCTE35-CMD={}", value)?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.scte35_out {
|
||||
write!(f, ",SCTE35-OUT={}", value)?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.scte35_in {
|
||||
write!(f, ",SCTE35-IN={}", value)?;
|
||||
}
|
||||
|
||||
for (k, v) in &self.client_attributes {
|
||||
write!(f, ",{}={}", k, v)?;
|
||||
}
|
||||
|
||||
if self.end_on_next {
|
||||
write!(f, ",END-ON-NEXT=YES",)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::types::Float;
|
||||
#[cfg(feature = "chrono")]
|
||||
use chrono::offset::TimeZone;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
|
||||
macro_rules! generate_tests {
|
||||
( $( { $left:expr, $right:expr } ),* $(,)* ) => {
|
||||
#[test]
|
||||
fn test_display() {
|
||||
$(
|
||||
assert_eq!($left.to_string(), $right.to_string());
|
||||
)*
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
$(
|
||||
assert_eq!($left, TryFrom::try_from($right).unwrap());
|
||||
)*
|
||||
assert!(ExtXDateRange::try_from("#EXT-X-DATERANGE:END-ON-NEXT=NO")
|
||||
.is_err());
|
||||
|
||||
assert!(ExtXDateRange::try_from("garbage").is_err());
|
||||
assert!(ExtXDateRange::try_from("").is_err());
|
||||
|
||||
assert!(ExtXDateRange::try_from(concat!(
|
||||
"#EXT-X-DATERANGE:",
|
||||
"ID=\"test_id\",",
|
||||
"START-DATE=\"2014-03-05T11:15:00Z\",",
|
||||
"END-ON-NEXT=YES"
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
{
|
||||
ExtXDateRange::builder()
|
||||
.id("splice-6FFFFFF0")
|
||||
.start_date({
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)
|
||||
}
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
"2014-03-05T11:15:00Z"
|
||||
}
|
||||
})
|
||||
.planned_duration(Duration::from_secs_f64(59.993))
|
||||
.scte35_out(concat!(
|
||||
"0xFC002F0000000000FF00001",
|
||||
"4056FFFFFF000E011622DCAFF0",
|
||||
"00052636200000000000A00080",
|
||||
"29896F50000008700000000"
|
||||
))
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-DATERANGE:",
|
||||
"ID=\"splice-6FFFFFF0\",",
|
||||
"START-DATE=\"2014-03-05T11:15:00Z\",",
|
||||
"PLANNED-DURATION=59.993,",
|
||||
"SCTE35-OUT=0xFC002F0000000000FF000014056F",
|
||||
"FFFFF000E011622DCAFF000052636200000000000",
|
||||
"A0008029896F50000008700000000"
|
||||
)
|
||||
},
|
||||
{
|
||||
ExtXDateRange::builder()
|
||||
.id("test_id")
|
||||
.class("test_class")
|
||||
.start_date({
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)
|
||||
}
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
"2014-03-05T11:15:00Z"
|
||||
}
|
||||
})
|
||||
.end_date({
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
FixedOffset::east(0).ymd(2014, 3, 5).and_hms_milli(11, 16, 0, 100)
|
||||
}
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
"2014-03-05T11:16:00.100Z"
|
||||
}
|
||||
})
|
||||
.duration(Duration::from_secs_f64(60.1))
|
||||
.planned_duration(Duration::from_secs_f64(59.993))
|
||||
.insert_client_attribute("X-CUSTOM", Float::new(45.3))
|
||||
.scte35_cmd("0xFC002F0000000000FF2")
|
||||
.scte35_out("0xFC002F0000000000FF0")
|
||||
.scte35_in("0xFC002F0000000000FF1")
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXT-X-DATERANGE:",
|
||||
"ID=\"test_id\",",
|
||||
"CLASS=\"test_class\",",
|
||||
"START-DATE=\"2014-03-05T11:15:00Z\",",
|
||||
"END-DATE=\"2014-03-05T11:16:00.100Z\",",
|
||||
"DURATION=60.1,",
|
||||
"PLANNED-DURATION=59.993,",
|
||||
"SCTE35-CMD=0xFC002F0000000000FF2,",
|
||||
"SCTE35-OUT=0xFC002F0000000000FF0,",
|
||||
"SCTE35-IN=0xFC002F0000000000FF1,",
|
||||
"X-CUSTOM=45.3",
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXDateRange::new("id", {
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
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(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
}
|
66
src/tags/media_segment/discontinuity.rs
Normal file
66
src/tags/media_segment/discontinuity.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The `ExtXDiscontinuity` tag indicates a discontinuity between the
|
||||
/// `MediaSegment` that follows it and the one that preceded it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub(crate) struct ExtXDiscontinuity;
|
||||
|
||||
impl ExtXDiscontinuity {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXDiscontinuity {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXDiscontinuity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXDiscontinuity {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
// the parser assumes that only a single line is passed as input,
|
||||
// which should be "#EXT-X-DISCONTINUITY"
|
||||
if input == Self::PREFIX {
|
||||
Ok(Self)
|
||||
} else {
|
||||
Err(Error::unexpected_data(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuity.to_string(),
|
||||
"#EXT-X-DISCONTINUITY".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuity,
|
||||
ExtXDiscontinuity::try_from("#EXT-X-DISCONTINUITY").unwrap()
|
||||
);
|
||||
|
||||
assert!(ExtXDiscontinuity::try_from("#EXT-X-DISCONTINUITY:0").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXDiscontinuity.required_version(), ProtocolVersion::V1)
|
||||
}
|
||||
}
|
280
src/tags/media_segment/inf.rs
Normal file
280
src/tags/media_segment/inf.rs
Normal file
|
@ -0,0 +1,280 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
use derive_more::AsRef;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Specifies the duration of a [`Media Segment`].
|
||||
///
|
||||
/// [`Media Segment`]: crate::media_segment::MediaSegment
|
||||
#[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtInf<'a> {
|
||||
#[as_ref]
|
||||
duration: Duration,
|
||||
title: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> ExtInf<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
||||
|
||||
/// Makes a new [`ExtInf`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtInf;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let ext_inf = ExtInf::new(Duration::from_secs(5));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn new(duration: Duration) -> Self {
|
||||
Self {
|
||||
duration,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new [`ExtInf`] tag with the given title.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtInf;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn with_title<T: Into<Cow<'a, str>>>(duration: Duration, title: T) -> Self {
|
||||
Self {
|
||||
duration,
|
||||
title: Some(title.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration of the associated media segment.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtInf;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let ext_inf = ExtInf::new(Duration::from_secs(5));
|
||||
///
|
||||
/// assert_eq!(ext_inf.duration(), Duration::from_secs(5));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn duration(&self) -> Duration { self.duration }
|
||||
|
||||
/// Sets the duration of the associated media segment.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtInf;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let mut ext_inf = ExtInf::new(Duration::from_secs(5));
|
||||
///
|
||||
/// ext_inf.set_duration(Duration::from_secs(10));
|
||||
///
|
||||
/// assert_eq!(ext_inf.duration(), Duration::from_secs(10));
|
||||
/// ```
|
||||
pub fn set_duration(&mut self, value: Duration) -> &mut Self {
|
||||
self.duration = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the title of the associated media segment.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtInf;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
||||
///
|
||||
/// assert_eq!(ext_inf.title(), &Some("title".into()));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn title(&self) -> &Option<Cow<'a, str>> { &self.title }
|
||||
|
||||
/// Sets the title of the associated media segment.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtInf;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let mut ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
||||
///
|
||||
/// ext_inf.set_title(Some("better title"));
|
||||
///
|
||||
/// assert_eq!(ext_inf.title(), &Some("better title".into()));
|
||||
/// ```
|
||||
pub fn set_title<T: Into<Cow<'a, str>>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.title = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ExtInf<'static> {
|
||||
ExtInf {
|
||||
duration: self.duration,
|
||||
title: self.title.map(|v| Cow::Owned(v.into_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`], if the duration does not have
|
||||
/// nanoseconds, otherwise it requires [`ProtocolVersion::V3`].
|
||||
impl<'a> RequiredVersion for ExtInf<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
if self.duration.subsec_nanos() == 0 {
|
||||
ProtocolVersion::V1
|
||||
} else {
|
||||
ProtocolVersion::V3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtInf<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "{},", self.duration.as_secs_f64())?;
|
||||
|
||||
if let Some(value) = &self.title {
|
||||
write!(f, "{}", value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtInf<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let mut input = tag(input, Self::PREFIX)?.splitn(2, ',');
|
||||
|
||||
let duration = input.next().unwrap();
|
||||
let duration = Duration::from_secs_f64(
|
||||
duration
|
||||
.parse()
|
||||
.map_err(|e| Error::parse_float(duration, e))?,
|
||||
);
|
||||
|
||||
let title = input
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(Cow::Borrowed);
|
||||
|
||||
Ok(Self { duration, title })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Duration> for ExtInf<'a> {
|
||||
fn from(value: Duration) -> Self { Self::new(value) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
"#EXTINF:5,".to_string(),
|
||||
ExtInf::new(Duration::from_secs(5)).to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
"#EXTINF:5.5,".to_string(),
|
||||
ExtInf::new(Duration::from_millis(5500)).to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
"#EXTINF:5.5,title".to_string(),
|
||||
ExtInf::with_title(Duration::from_millis(5500), "title").to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
"#EXTINF:5,title".to_string(),
|
||||
ExtInf::with_title(Duration::from_secs(5), "title").to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
// #EXTINF:<duration>,[<title>]
|
||||
assert_eq!(
|
||||
ExtInf::try_from("#EXTINF:5").unwrap(),
|
||||
ExtInf::new(Duration::from_secs(5))
|
||||
);
|
||||
assert_eq!(
|
||||
ExtInf::try_from("#EXTINF:5,").unwrap(),
|
||||
ExtInf::new(Duration::from_secs(5))
|
||||
);
|
||||
assert_eq!(
|
||||
ExtInf::try_from("#EXTINF:5.5").unwrap(),
|
||||
ExtInf::new(Duration::from_millis(5500))
|
||||
);
|
||||
assert_eq!(
|
||||
ExtInf::try_from("#EXTINF:5.5,").unwrap(),
|
||||
ExtInf::new(Duration::from_millis(5500))
|
||||
);
|
||||
assert_eq!(
|
||||
ExtInf::try_from("#EXTINF:5.5,title").unwrap(),
|
||||
ExtInf::with_title(Duration::from_millis(5500), "title")
|
||||
);
|
||||
assert_eq!(
|
||||
ExtInf::try_from("#EXTINF:5,title").unwrap(),
|
||||
ExtInf::with_title(Duration::from_secs(5), "title")
|
||||
);
|
||||
|
||||
assert!(ExtInf::try_from("#EXTINF:").is_err());
|
||||
assert!(ExtInf::try_from("#EXTINF:garbage").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_title() {
|
||||
assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None);
|
||||
assert_eq!(
|
||||
ExtInf::with_title(Duration::from_secs(5), "title").title(),
|
||||
&Some("title".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtInf::new(Duration::from_secs(4)).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
assert_eq!(
|
||||
ExtInf::new(Duration::from_millis(4400)).required_version(),
|
||||
ProtocolVersion::V3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(
|
||||
ExtInf::from(Duration::from_secs(1)),
|
||||
ExtInf::new(Duration::from_secs(1))
|
||||
);
|
||||
}
|
||||
}
|
362
src/tags/media_segment/key.rs
Normal file
362
src/tags/media_segment/key.rs
Normal file
|
@ -0,0 +1,362 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Specifies how to decrypt encrypted data from the server.
|
||||
///
|
||||
/// An unencrypted segment should be marked with [`ExtXKey::empty`].
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct ExtXKey<'a>(pub Option<DecryptionKey<'a>>);
|
||||
|
||||
impl<'a> ExtXKey<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||
|
||||
/// Constructs an [`ExtXKey`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXKey;
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod, KeyFormat};
|
||||
///
|
||||
/// 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::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn new(inner: DecryptionKey<'a>) -> 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!(
|
||||
/// k.unwrap(),
|
||||
/// DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.url")
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ```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<'a> {
|
||||
match self.0 {
|
||||
Some(v) => v,
|
||||
None => panic!("called `ExtXKey::unwrap()` on an empty key"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying [`DecryptionKey`].
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn as_ref(&self) -> Option<&DecryptionKey<'a>> { self.0.as_ref() }
|
||||
|
||||
/// Converts an [`ExtXKey`] into an `Option<DecryptionKey>`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXKey;
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
///
|
||||
/// assert_eq!(ExtXKey::empty().into_option(), None);
|
||||
///
|
||||
/// 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]
|
||||
#[inline]
|
||||
pub fn into_option(self) -> Option<DecryptionKey<'a>> { self.0 }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
///
|
||||
/// [`Cow`]: std::borrow::Cow
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn into_owned(self) -> ExtXKey<'static> { ExtXKey(self.0.map(DecryptionKey::into_owned)) }
|
||||
}
|
||||
|
||||
/// 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<'a> RequiredVersion for ExtXKey<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map_or(ProtocolVersion::V1, |i| i.required_version())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtXKey<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
if input.trim() == "METHOD=NONE" {
|
||||
Ok(Self(None))
|
||||
} else {
|
||||
Ok(DecryptionKey::try_from(input)?.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Option<DecryptionKey<'a>>> for ExtXKey<'a> {
|
||||
fn from(value: Option<DecryptionKey<'a>>) -> Self { Self(value) }
|
||||
}
|
||||
|
||||
impl<'a> From<DecryptionKey<'a>> for ExtXKey<'a> {
|
||||
fn from(value: DecryptionKey<'a>) -> Self { Self(Some(value)) }
|
||||
}
|
||||
|
||||
impl<'a> From<crate::tags::ExtXSessionKey<'a>> for ExtXKey<'a> {
|
||||
fn from(value: crate::tags::ExtXSessionKey<'a>) -> Self { Self(Some(value.0)) }
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtXKey<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
|
||||
if let Some(value) = &self.0 {
|
||||
write!(f, "{}", value)
|
||||
} else {
|
||||
write!(f, "METHOD=NONE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::types::{EncryptionMethod, KeyFormat};
|
||||
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, TryFrom::try_from($str).unwrap());
|
||||
)+
|
||||
|
||||
assert_eq!(
|
||||
ExtXKey::new(
|
||||
DecryptionKey::new(
|
||||
EncryptionMethod::Aes128,
|
||||
"http://www.example.com"
|
||||
)
|
||||
),
|
||||
ExtXKey::try_from(concat!(
|
||||
"#EXT-X-KEY:",
|
||||
"METHOD=AES-128,",
|
||||
"URI=\"http://www.example.com\",",
|
||||
"UNKNOWNTAG=abcd"
|
||||
)).unwrap(),
|
||||
);
|
||||
assert!(ExtXKey::try_from("#EXT-X-KEY:METHOD=AES-128,URI=").is_err());
|
||||
assert!(ExtXKey::try_from("garbage").is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
{
|
||||
ExtXKey::empty(),
|
||||
"#EXT-X-KEY:METHOD=NONE"
|
||||
},
|
||||
{
|
||||
ExtXKey::new(DecryptionKey::new(
|
||||
EncryptionMethod::Aes128,
|
||||
"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
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXKey::new(
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
222
src/tags/media_segment/map.rs
Normal file
222
src/tags/media_segment/map.rs
Normal file
|
@ -0,0 +1,222 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::tags::ExtXKey;
|
||||
use crate::types::{ByteRange, DecryptionKey, ProtocolVersion};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::{Decryptable, Error, RequiredVersion};
|
||||
|
||||
/// The [`ExtXMap`] tag specifies how to obtain the [Media Initialization
|
||||
/// Section], required to parse the applicable [`MediaSegment`]s.
|
||||
///
|
||||
/// It applies to every [`MediaSegment`] that appears after it in the playlist
|
||||
/// 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
|
||||
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||
/// [`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<'a> {
|
||||
/// The `URI` that identifies a resource, that contains the media
|
||||
/// initialization section.
|
||||
uri: Cow<'a, str>,
|
||||
/// The range of the media initialization section.
|
||||
#[shorthand(enable(copy))]
|
||||
range: Option<ByteRange>,
|
||||
#[shorthand(enable(skip))]
|
||||
pub(crate) keys: Vec<ExtXKey<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> ExtXMap<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
||||
|
||||
/// Makes a new [`ExtXMap`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn new<T: Into<Cow<'a, str>>>(uri: T) -> Self {
|
||||
Self {
|
||||
uri: uri.into(),
|
||||
range: None,
|
||||
keys: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new [`ExtXMap`] tag with the given range.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// use hls_m3u8::types::ByteRange;
|
||||
///
|
||||
/// let map = ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn with_range<I: Into<Cow<'a, str>>, B: Into<ByteRange>>(uri: I, range: B) -> Self {
|
||||
Self {
|
||||
uri: uri.into(),
|
||||
range: Some(range.into()),
|
||||
keys: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ExtXMap<'static> {
|
||||
ExtXMap {
|
||||
uri: Cow::Owned(self.uri.into_owned()),
|
||||
range: self.range,
|
||||
keys: self.keys.into_iter().map(ExtXKey::into_owned).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decryptable<'a> for ExtXMap<'a> {
|
||||
fn keys(&self) -> Vec<&DecryptionKey<'a>> {
|
||||
//
|
||||
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that contains the
|
||||
/// [`ExtXIFramesOnly`] tag requires [`ProtocolVersion::V5`] or
|
||||
/// greater. Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that does not
|
||||
/// contain the [`ExtXIFramesOnly`] tag requires [`ProtocolVersion::V6`] or
|
||||
/// greater.
|
||||
///
|
||||
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
impl<'a> RequiredVersion for ExtXMap<'a> {
|
||||
// this should return ProtocolVersion::V5, if it does not contain an
|
||||
// EXT-X-I-FRAMES-ONLY!
|
||||
// http://alexzambelli.com/blog/2016/05/04/understanding-hls-versions-and-client-compatibility/
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V6 }
|
||||
|
||||
fn introduced_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtXMap<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "URI={}", quote(&self.uri))?;
|
||||
|
||||
if let Some(value) = &self.range {
|
||||
write!(f, ",BYTERANGE={}", quote(value))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtXMap<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let mut uri = None;
|
||||
let mut range = None;
|
||||
|
||||
for (key, value) in AttributePairs::new(input) {
|
||||
match key {
|
||||
"URI" => uri = Some(unquote(value)),
|
||||
"BYTERANGE" => {
|
||||
range = Some(unquote(value).try_into()?);
|
||||
}
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized
|
||||
// AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let uri = uri.ok_or_else(|| Error::missing_value("URI"))?;
|
||||
|
||||
Ok(Self {
|
||||
uri,
|
||||
range,
|
||||
keys: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXMap::new("foo").to_string(),
|
||||
"#EXT-X-MAP:URI=\"foo\"".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::from(2..11)).to_string(),
|
||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXMap::new("foo"),
|
||||
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\"").unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
||||
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"").unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
||||
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXMap::new("foo").required_version(), ProtocolVersion::V6);
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::from(2..11)).required_version(),
|
||||
ProtocolVersion::V6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decryptable() {
|
||||
assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey<'_>>::new());
|
||||
}
|
||||
}
|
15
src/tags/media_segment/mod.rs
Normal file
15
src/tags/media_segment/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
pub(crate) mod byte_range;
|
||||
pub(crate) mod date_range;
|
||||
pub(crate) mod discontinuity;
|
||||
pub(crate) mod inf;
|
||||
pub(crate) mod key;
|
||||
pub(crate) mod map;
|
||||
pub(crate) mod program_date_time;
|
||||
|
||||
pub use byte_range::*;
|
||||
pub use date_range::ExtXDateRange;
|
||||
pub(crate) use discontinuity::*;
|
||||
pub use inf::*;
|
||||
pub use key::ExtXKey;
|
||||
pub use map::*;
|
||||
pub use program_date_time::*;
|
247
src/tags/media_segment/program_date_time.rs
Normal file
247
src/tags/media_segment/program_date_time.rs
Normal file
|
@ -0,0 +1,247 @@
|
|||
#[cfg(not(feature = "chrono"))]
|
||||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
||||
#[cfg(feature = "chrono")]
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Associates the first sample of a [`MediaSegment`] with an absolute date
|
||||
/// and/or time.
|
||||
///
|
||||
/// ## Features
|
||||
///
|
||||
/// By enabling the `chrono` feature the `date_time`-field will change from
|
||||
/// `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<'a> {
|
||||
/// 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: Cow<'a, str>,
|
||||
_p: PhantomData<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> ExtXProgramDateTime<'a> {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||
|
||||
/// Makes a new [`ExtXProgramDateTime`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
||||
/// use chrono::{FixedOffset, TimeZone};
|
||||
///
|
||||
/// 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),
|
||||
/// );
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[cfg(feature = "chrono")]
|
||||
pub const fn new(date_time: DateTime<FixedOffset>) -> Self {
|
||||
Self {
|
||||
date_time,
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new [`ExtXProgramDateTime`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
||||
/// let program_date_time = ExtXProgramDateTime::new("2010-02-19T14:54:23.031+08:00");
|
||||
/// ```
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
pub fn new<T: Into<Cow<'a, str>>>(date_time: T) -> Self {
|
||||
Self {
|
||||
date_time: date_time.into(),
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ExtXProgramDateTime<'static> {
|
||||
ExtXProgramDateTime {
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
date_time: Cow::Owned(self.date_time.into_owned()),
|
||||
#[cfg(feature = "chrono")]
|
||||
date_time: self.date_time,
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl<'a> RequiredVersion for ExtXProgramDateTime<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ExtXProgramDateTime<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ExtXProgramDateTime<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
Ok(Self::new({
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
DateTime::parse_from_rfc3339(input).map_err(Error::chrono)?
|
||||
}
|
||||
#[cfg(not(feature = "chrono"))]
|
||||
{
|
||||
input
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[cfg(feature = "chrono")]
|
||||
use chrono::{Datelike, TimeZone};
|
||||
#[cfg(feature = "chrono")]
|
||||
use core::ops::DerefMut;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new({
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
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(),
|
||||
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new({
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
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"
|
||||
}
|
||||
}),
|
||||
ExtXProgramDateTime::try_from("#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00")
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new({
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
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(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "chrono")]
|
||||
fn test_deref() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new(
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
)
|
||||
.year(),
|
||||
2010
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "chrono")]
|
||||
fn test_deref_mut() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new(
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
)
|
||||
.deref_mut(),
|
||||
&mut FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
);
|
||||
}
|
||||
}
|
131
src/tags/mod.rs
131
src/tags/mod.rs
|
@ -1,126 +1,15 @@
|
|||
//! [4.3. Playlist Tags]
|
||||
//!
|
||||
//! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3
|
||||
use trackable::error::ErrorKindExt;
|
||||
|
||||
use {ErrorKind, Result};
|
||||
pub(crate) mod basic;
|
||||
pub(crate) mod master_playlist;
|
||||
pub(crate) mod media_playlist;
|
||||
pub(crate) mod media_segment;
|
||||
pub(crate) mod shared;
|
||||
|
||||
macro_rules! may_invalid {
|
||||
($expr:expr) => {
|
||||
$expr.map_err(|e| track!(Error::from(ErrorKind::InvalidInput.cause(e))))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($to:ident, $from:ident) => {
|
||||
impl From<$from> for $to {
|
||||
fn from(f: $from) -> Self {
|
||||
$to::$from(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use self::basic::{ExtM3u, ExtXVersion};
|
||||
pub use self::master_playlist::{ExtXIFrameStreamInf, ExtXMedia, ExtXSessionData, ExtXSessionKey,
|
||||
ExtXStreamInf};
|
||||
pub use self::media_or_master_playlist::{ExtXIndependentSegments, ExtXStart};
|
||||
pub use self::media_playlist::{ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly,
|
||||
ExtXMediaSequence, ExtXPlaylistType, ExtXTargetDuration};
|
||||
pub use self::media_segment::{ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey,
|
||||
ExtXMap, ExtXProgramDateTime};
|
||||
|
||||
mod basic;
|
||||
mod master_playlist;
|
||||
mod media_or_master_playlist;
|
||||
mod media_playlist;
|
||||
mod media_segment;
|
||||
|
||||
/// [4.3.4. Master Playlist Tags]
|
||||
///
|
||||
/// See also [4.3.5. Media or Master Playlist Tags]
|
||||
///
|
||||
/// [4.3.4. Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.4
|
||||
/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5
|
||||
#[allow(missing_docs)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(large_enum_variant))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MasterPlaylistTag {
|
||||
ExtXMedia(ExtXMedia),
|
||||
ExtXStreamInf(ExtXStreamInf),
|
||||
ExtXIFrameStreamInf(ExtXIFrameStreamInf),
|
||||
ExtXSessionData(ExtXSessionData),
|
||||
ExtXSessionKey(ExtXSessionKey),
|
||||
ExtXIndependentSegments(ExtXIndependentSegments),
|
||||
ExtXStart(ExtXStart),
|
||||
}
|
||||
impl_from!(MasterPlaylistTag, ExtXMedia);
|
||||
impl_from!(MasterPlaylistTag, ExtXStreamInf);
|
||||
impl_from!(MasterPlaylistTag, ExtXIFrameStreamInf);
|
||||
impl_from!(MasterPlaylistTag, ExtXSessionData);
|
||||
impl_from!(MasterPlaylistTag, ExtXSessionKey);
|
||||
impl_from!(MasterPlaylistTag, ExtXIndependentSegments);
|
||||
impl_from!(MasterPlaylistTag, ExtXStart);
|
||||
|
||||
/// [4.3.3. Media Playlist Tags]
|
||||
///
|
||||
/// See also [4.3.5. Media or Master Playlist Tags]
|
||||
///
|
||||
/// [4.3.3. Media Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.3
|
||||
/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MediaPlaylistTag {
|
||||
ExtXTargetDuration(ExtXTargetDuration),
|
||||
ExtXMediaSequence(ExtXMediaSequence),
|
||||
ExtXDiscontinuitySequence(ExtXDiscontinuitySequence),
|
||||
ExtXEndList(ExtXEndList),
|
||||
ExtXPlaylistType(ExtXPlaylistType),
|
||||
ExtXIFramesOnly(ExtXIFramesOnly),
|
||||
ExtXIndependentSegments(ExtXIndependentSegments),
|
||||
ExtXStart(ExtXStart),
|
||||
}
|
||||
impl_from!(MediaPlaylistTag, ExtXTargetDuration);
|
||||
impl_from!(MediaPlaylistTag, ExtXMediaSequence);
|
||||
impl_from!(MediaPlaylistTag, ExtXDiscontinuitySequence);
|
||||
impl_from!(MediaPlaylistTag, ExtXEndList);
|
||||
impl_from!(MediaPlaylistTag, ExtXPlaylistType);
|
||||
impl_from!(MediaPlaylistTag, ExtXIFramesOnly);
|
||||
impl_from!(MediaPlaylistTag, ExtXIndependentSegments);
|
||||
impl_from!(MediaPlaylistTag, ExtXStart);
|
||||
|
||||
/// [4.3.2. Media Segment Tags]
|
||||
///
|
||||
/// [4.3.2. Media Segment Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.2
|
||||
#[allow(missing_docs)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(large_enum_variant))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MediaSegmentTag {
|
||||
ExtInf(ExtInf),
|
||||
ExtXByteRange(ExtXByteRange),
|
||||
ExtXDateRange(ExtXDateRange),
|
||||
ExtXDiscontinuity(ExtXDiscontinuity),
|
||||
ExtXKey(ExtXKey),
|
||||
ExtXMap(ExtXMap),
|
||||
ExtXProgramDateTime(ExtXProgramDateTime),
|
||||
}
|
||||
impl_from!(MediaSegmentTag, ExtInf);
|
||||
impl_from!(MediaSegmentTag, ExtXByteRange);
|
||||
impl_from!(MediaSegmentTag, ExtXDateRange);
|
||||
impl_from!(MediaSegmentTag, ExtXDiscontinuity);
|
||||
impl_from!(MediaSegmentTag, ExtXKey);
|
||||
impl_from!(MediaSegmentTag, ExtXMap);
|
||||
impl_from!(MediaSegmentTag, ExtXProgramDateTime);
|
||||
|
||||
fn parse_yes_or_no(s: &str) -> Result<bool> {
|
||||
match s {
|
||||
"YES" => Ok(true),
|
||||
"NO" => Ok(false),
|
||||
_ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_u64(s: &str) -> Result<u64> {
|
||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||
Ok(n)
|
||||
}
|
||||
pub use basic::*;
|
||||
pub use master_playlist::*;
|
||||
pub(crate) use media_playlist::*;
|
||||
pub use media_segment::*;
|
||||
pub use shared::*;
|
||||
|
|
65
src/tags/shared/independent_segments.rs
Normal file
65
src/tags/shared/independent_segments.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Signals that all media samples in a [`MediaSegment`] can be decoded without
|
||||
/// information from other segments.
|
||||
///
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub(crate) struct ExtXIndependentSegments;
|
||||
|
||||
impl ExtXIndependentSegments {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXIndependentSegments {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXIndependentSegments {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXIndependentSegments {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXIndependentSegments.to_string(),
|
||||
"#EXT-X-INDEPENDENT-SEGMENTS".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXIndependentSegments,
|
||||
ExtXIndependentSegments::try_from("#EXT-X-INDEPENDENT-SEGMENTS").unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXIndependentSegments.required_version(),
|
||||
ProtocolVersion::V1
|
||||
)
|
||||
}
|
||||
}
|
5
src/tags/shared/mod.rs
Normal file
5
src/tags/shared/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub(crate) mod independent_segments;
|
||||
pub(crate) mod start;
|
||||
|
||||
pub(crate) use independent_segments::ExtXIndependentSegments;
|
||||
pub use start::*;
|
192
src/tags/shared/start.rs
Normal file
192
src/tags/shared/start.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{Float, ProtocolVersion};
|
||||
use crate::utils::{parse_yes_or_no, tag};
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// This tag 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.
|
||||
#[derive(ShortHand, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Ord, Hash)]
|
||||
#[shorthand(enable(must_use))]
|
||||
pub struct ExtXStart {
|
||||
/// The time offset of the [`MediaSegment`]s in the playlist.
|
||||
///
|
||||
/// # 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 {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
|
||||
|
||||
/// Makes a new [`ExtXStart`] tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXStart;
|
||||
/// use hls_m3u8::types::Float;
|
||||
///
|
||||
/// let start = ExtXStart::new(Float::new(20.123456));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn new(time_offset: Float) -> Self {
|
||||
Self {
|
||||
time_offset,
|
||||
is_precise: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new [`ExtXStart`] tag with the given `precise` flag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXStart;
|
||||
/// use hls_m3u8::types::Float;
|
||||
///
|
||||
/// let start = ExtXStart::with_precise(Float::new(20.123456), true);
|
||||
/// assert_eq!(start.is_precise(), true);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn with_precise(time_offset: Float, is_precise: bool) -> Self {
|
||||
Self {
|
||||
time_offset,
|
||||
is_precise,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXStart {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXStart {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "TIME-OFFSET={}", self.time_offset)?;
|
||||
|
||||
if self.is_precise {
|
||||
write!(f, ",PRECISE=YES")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtXStart {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let mut time_offset = None;
|
||||
let mut is_precise = false;
|
||||
|
||||
for (key, value) in AttributePairs::new(input) {
|
||||
match key {
|
||||
"TIME-OFFSET" => time_offset = Some(value.parse()?),
|
||||
"PRECISE" => is_precise = parse_yes_or_no(value)?,
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized
|
||||
// AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let time_offset = time_offset.ok_or_else(|| Error::missing_value("TIME-OFFSET"))?;
|
||||
|
||||
Ok(Self {
|
||||
time_offset,
|
||||
is_precise,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXStart::new(Float::new(-1.23)).to_string(),
|
||||
"#EXT-X-START:TIME-OFFSET=-1.23".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXStart::with_precise(Float::new(1.23), true).to_string(),
|
||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXStart::new(Float::new(-1.23)).required_version(),
|
||||
ProtocolVersion::V1,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXStart::with_precise(Float::new(1.23), true).required_version(),
|
||||
ProtocolVersion::V1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXStart::new(Float::new(-1.23)),
|
||||
ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=-1.23").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXStart::with_precise(Float::new(1.23), true),
|
||||
ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXStart::with_precise(Float::new(1.23), true),
|
||||
ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG").unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
139
src/traits.rs
Normal file
139
src/traits.rs
Normal file
|
@ -0,0 +1,139 @@
|
|||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use stable_vec::StableVec;
|
||||
|
||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
impl<'a> Sealed for crate::MediaSegment<'a> {}
|
||||
impl<'a> Sealed for crate::tags::ExtXMap<'a> {}
|
||||
}
|
||||
|
||||
/// Signals that a type or some of the asssociated data might need to be
|
||||
/// decrypted.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// You are not supposed to implement this trait, therefore it is "sealed".
|
||||
pub trait Decryptable<'a>: private::Sealed {
|
||||
/// Returns all keys, associated with the type.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXMap;
|
||||
/// use hls_m3u8::types::{ByteRange, EncryptionMethod};
|
||||
/// use hls_m3u8::Decryptable;
|
||||
///
|
||||
/// 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<'a>>;
|
||||
|
||||
/// 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<'a>> {
|
||||
<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 {
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is for the latest working [`ProtocolVersion`] and a client, that
|
||||
/// only supports an older version would break.
|
||||
#[must_use]
|
||||
fn required_version(&self) -> ProtocolVersion;
|
||||
|
||||
/// The protocol version, in which the tag has been introduced.
|
||||
#[must_use]
|
||||
fn introduced_version(&self) -> ProtocolVersion { self.required_version() }
|
||||
}
|
||||
|
||||
impl<T: RequiredVersion> RequiredVersion for Vec<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(RequiredVersion::required_version)
|
||||
.max()
|
||||
// return ProtocolVersion::V1, if the iterator is empty:
|
||||
.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> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(RequiredVersion::required_version)
|
||||
.max()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RequiredVersion> RequiredVersion for StableVec<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.values()
|
||||
.map(RequiredVersion::required_version)
|
||||
.max()
|
||||
// return ProtocolVersion::V1, if the iterator is empty:
|
||||
.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);
|
||||
}
|
||||
}
|
841
src/types.rs
841
src/types.rs
|
@ -1,841 +0,0 @@
|
|||
//! Miscellaneous types.
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::str::{self, FromStr};
|
||||
use std::time::Duration;
|
||||
use trackable::error::ErrorKindExt;
|
||||
|
||||
use {Error, ErrorKind, Result};
|
||||
use attribute::AttributePairs;
|
||||
|
||||
/// String that represents a single line in a playlist file.
|
||||
///
|
||||
/// See: [4.1. Definition of a Playlist]
|
||||
///
|
||||
/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct SingleLineString(String);
|
||||
impl SingleLineString {
|
||||
/// Makes a new `SingleLineString` instance.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the given string contains any control characters,
|
||||
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
||||
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
||||
let s = s.into();
|
||||
track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput);
|
||||
Ok(SingleLineString(s))
|
||||
}
|
||||
}
|
||||
impl Deref for SingleLineString {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for SingleLineString {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl fmt::Display for SingleLineString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Quoted string.
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct QuotedString(String);
|
||||
impl QuotedString {
|
||||
/// Makes a new `QuotedString` instance.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the given string contains any control characters or double-quote character,
|
||||
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
||||
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
||||
let s = s.into();
|
||||
track_assert!(
|
||||
!s.chars().any(|c| c.is_control() || c == '"'),
|
||||
ErrorKind::InvalidInput
|
||||
);
|
||||
Ok(QuotedString(s))
|
||||
}
|
||||
}
|
||||
impl Deref for QuotedString {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for QuotedString {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl fmt::Display for QuotedString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
impl FromStr for QuotedString {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let len = s.len();
|
||||
let bytes = s.as_bytes();
|
||||
track_assert!(len >= 2, ErrorKind::InvalidInput);
|
||||
track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput);
|
||||
track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput);
|
||||
|
||||
let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) };
|
||||
track!(QuotedString::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
/// Decimal resolution.
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DecimalResolution {
|
||||
/// Horizontal pixel dimension.
|
||||
pub width: usize,
|
||||
|
||||
/// Vertical pixel dimension.
|
||||
pub height: usize,
|
||||
}
|
||||
impl fmt::Display for DecimalResolution {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.width, self.height)
|
||||
}
|
||||
}
|
||||
impl FromStr for DecimalResolution {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut tokens = s.splitn(2, 'x');
|
||||
let width = tokens.next().expect("Never fails");
|
||||
let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput);
|
||||
Ok(DecimalResolution {
|
||||
width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
||||
height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-negative decimal floating-point number.
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub 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(n: f64) -> Result<Self> {
|
||||
track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput);
|
||||
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
||||
Ok(DecimalFloatingPoint(n))
|
||||
}
|
||||
|
||||
/// Converts `DecimalFloatingPoint` to `f64`.
|
||||
pub fn as_f64(&self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub(crate) fn to_duration(&self) -> Duration {
|
||||
let secs = self.0 as u64;
|
||||
let nanos = (self.0.fract() * 1_000_000_000.0) as u32;
|
||||
Duration::new(secs, nanos)
|
||||
}
|
||||
|
||||
pub(crate) fn from_duration(duration: Duration) -> Self {
|
||||
let n =
|
||||
(duration.as_secs() as f64) + (f64::from(duration.subsec_nanos()) / 1_000_000_000.0);
|
||||
DecimalFloatingPoint(n)
|
||||
}
|
||||
}
|
||||
impl From<u32> for DecimalFloatingPoint {
|
||||
fn from(f: u32) -> Self {
|
||||
DecimalFloatingPoint(f64::from(f))
|
||||
}
|
||||
}
|
||||
impl Eq for DecimalFloatingPoint {}
|
||||
impl fmt::Display for DecimalFloatingPoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for DecimalFloatingPoint {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(
|
||||
s.chars().all(|c| c.is_digit(10) || c == '.'),
|
||||
ErrorKind::InvalidInput
|
||||
);
|
||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||
Ok(DecimalFloatingPoint(n))
|
||||
}
|
||||
}
|
||||
|
||||
/// Signed decimal floating-point number.
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub struct SignedDecimalFloatingPoint(f64);
|
||||
impl SignedDecimalFloatingPoint {
|
||||
/// Makes a new `SignedDecimalFloatingPoint` instance.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The given value must be finite,
|
||||
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
||||
pub fn new(n: f64) -> Result<Self> {
|
||||
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
||||
Ok(SignedDecimalFloatingPoint(n))
|
||||
}
|
||||
|
||||
/// Converts `DecimalFloatingPoint` to `f64`.
|
||||
pub fn as_f64(&self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
impl From<i32> for SignedDecimalFloatingPoint {
|
||||
fn from(f: i32) -> Self {
|
||||
SignedDecimalFloatingPoint(f64::from(f))
|
||||
}
|
||||
}
|
||||
impl Eq for SignedDecimalFloatingPoint {}
|
||||
impl fmt::Display for SignedDecimalFloatingPoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for SignedDecimalFloatingPoint {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(
|
||||
s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'),
|
||||
ErrorKind::InvalidInput
|
||||
);
|
||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||
Ok(SignedDecimalFloatingPoint(n))
|
||||
}
|
||||
}
|
||||
|
||||
/// Hexadecimal sequence.
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct HexadecimalSequence(Vec<u8>);
|
||||
impl HexadecimalSequence {
|
||||
/// Makes a new `HexadecimalSequence` instance.
|
||||
pub fn new<T: Into<Vec<u8>>>(v: T) -> Self {
|
||||
HexadecimalSequence(v.into())
|
||||
}
|
||||
|
||||
/// Converts into the underlying byte sequence.
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
impl Deref for HexadecimalSequence {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl AsRef<[u8]> for HexadecimalSequence {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl fmt::Display for HexadecimalSequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "0x")?;
|
||||
for b in &self.0 {
|
||||
write!(f, "{:02x}", b)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for HexadecimalSequence {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(
|
||||
s.starts_with("0x") || s.starts_with("0X"),
|
||||
ErrorKind::InvalidInput
|
||||
);
|
||||
track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput);
|
||||
|
||||
let mut v = Vec::with_capacity(s.len() / 2 - 1);
|
||||
for c in s.as_bytes().chunks(2).skip(1) {
|
||||
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||
let b =
|
||||
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||
v.push(b);
|
||||
}
|
||||
Ok(HexadecimalSequence(v))
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialization vector.
|
||||
///
|
||||
/// 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
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct InitializationVector(pub [u8; 16]);
|
||||
impl Deref for InitializationVector {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl AsRef<[u8]> for InitializationVector {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl fmt::Display for InitializationVector {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "0x")?;
|
||||
for b in &self.0 {
|
||||
write!(f, "{:02x}", b)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for InitializationVector {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
track_assert!(
|
||||
s.starts_with("0x") || s.starts_with("0X"),
|
||||
ErrorKind::InvalidInput
|
||||
);
|
||||
track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput);
|
||||
|
||||
let mut v = [0; 16];
|
||||
for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
|
||||
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||
let b =
|
||||
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||
v[i] = b;
|
||||
}
|
||||
Ok(InitializationVector(v))
|
||||
}
|
||||
}
|
||||
|
||||
/// [7. Protocol Version Compatibility]
|
||||
///
|
||||
/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ProtocolVersion {
|
||||
V1,
|
||||
V2,
|
||||
V3,
|
||||
V4,
|
||||
V5,
|
||||
V6,
|
||||
V7,
|
||||
}
|
||||
impl fmt::Display for ProtocolVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let n = match *self {
|
||||
ProtocolVersion::V1 => 1,
|
||||
ProtocolVersion::V2 => 2,
|
||||
ProtocolVersion::V3 => 3,
|
||||
ProtocolVersion::V4 => 4,
|
||||
ProtocolVersion::V5 => 5,
|
||||
ProtocolVersion::V6 => 6,
|
||||
ProtocolVersion::V7 => 7,
|
||||
};
|
||||
write!(f, "{}", n)
|
||||
}
|
||||
}
|
||||
impl FromStr for ProtocolVersion {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Ok(match s {
|
||||
"1" => ProtocolVersion::V1,
|
||||
"2" => ProtocolVersion::V2,
|
||||
"3" => ProtocolVersion::V3,
|
||||
"4" => ProtocolVersion::V4,
|
||||
"5" => ProtocolVersion::V5,
|
||||
"6" => ProtocolVersion::V6,
|
||||
"7" => ProtocolVersion::V7,
|
||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte range.
|
||||
///
|
||||
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
||||
///
|
||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ByteRange {
|
||||
pub length: usize,
|
||||
pub start: Option<usize>,
|
||||
}
|
||||
impl fmt::Display for ByteRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.length)?;
|
||||
if let Some(x) = self.start {
|
||||
write!(f, "@{}", x)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for ByteRange {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut tokens = s.splitn(2, '@');
|
||||
let length = tokens.next().expect("Never fails");
|
||||
let start = if let Some(start) = tokens.next() {
|
||||
Some(track!(
|
||||
start.parse().map_err(|e| ErrorKind::InvalidInput.cause(e))
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(ByteRange {
|
||||
length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
||||
start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Decryption key.
|
||||
///
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct DecryptionKey {
|
||||
pub method: EncryptionMethod,
|
||||
pub uri: QuotedString,
|
||||
pub iv: Option<InitializationVector>,
|
||||
pub key_format: Option<QuotedString>,
|
||||
pub key_format_versions: Option<QuotedString>,
|
||||
}
|
||||
impl DecryptionKey {
|
||||
pub(crate) fn requires_version(&self) -> ProtocolVersion {
|
||||
if self.key_format.is_some() | self.key_format_versions.is_some() {
|
||||
ProtocolVersion::V5
|
||||
} else if self.iv.is_some() {
|
||||
ProtocolVersion::V2
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
}
|
||||
impl fmt::Display for DecryptionKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "METHOD={}", self.method)?;
|
||||
write!(f, ",URI={}", self.uri)?;
|
||||
if let Some(ref x) = self.iv {
|
||||
write!(f, ",IV={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.key_format {
|
||||
write!(f, ",KEYFORMAT={}", x)?;
|
||||
}
|
||||
if let Some(ref x) = self.key_format_versions {
|
||||
write!(f, ",KEYFORMATVERSIONS={}", x)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FromStr for DecryptionKey {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut method = None;
|
||||
let mut uri = None;
|
||||
let mut iv = None;
|
||||
let mut key_format = None;
|
||||
let mut key_format_versions = None;
|
||||
let attrs = AttributePairs::parse(s);
|
||||
for attr in attrs {
|
||||
let (key, value) = track!(attr)?;
|
||||
match key {
|
||||
"METHOD" => method = Some(track!(value.parse())?),
|
||||
"URI" => uri = Some(track!(value.parse())?),
|
||||
"IV" => iv = Some(track!(value.parse())?),
|
||||
"KEYFORMAT" => key_format = Some(track!(value.parse())?),
|
||||
"KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?),
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
let method = track_assert_some!(method, ErrorKind::InvalidInput);
|
||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
||||
Ok(DecryptionKey {
|
||||
method,
|
||||
uri,
|
||||
iv,
|
||||
key_format,
|
||||
key_format_versions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Encryption method.
|
||||
///
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum EncryptionMethod {
|
||||
Aes128,
|
||||
SampleAes,
|
||||
}
|
||||
impl fmt::Display for EncryptionMethod {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
EncryptionMethod::Aes128 => "AES-128".fmt(f),
|
||||
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for EncryptionMethod {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"AES-128" => Ok(EncryptionMethod::Aes128),
|
||||
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
|
||||
_ => track_panic!(
|
||||
ErrorKind::InvalidInput,
|
||||
"Unknown encryption method: {:?}",
|
||||
s
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Playlist type.
|
||||
///
|
||||
/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
||||
///
|
||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum PlaylistType {
|
||||
Event,
|
||||
Vod,
|
||||
}
|
||||
impl fmt::Display for PlaylistType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
PlaylistType::Event => write!(f, "EVENT"),
|
||||
PlaylistType::Vod => write!(f, "VOD"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for PlaylistType {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"EVENT" => Ok(PlaylistType::Event),
|
||||
"VOD" => Ok(PlaylistType::Vod),
|
||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Media type.
|
||||
///
|
||||
/// See: [4.3.4.1. EXT-X-MEDIA]
|
||||
///
|
||||
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum MediaType {
|
||||
Audio,
|
||||
Video,
|
||||
Subtitles,
|
||||
ClosedCaptions,
|
||||
}
|
||||
impl fmt::Display for MediaType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
MediaType::Audio => "AUDIO".fmt(f),
|
||||
MediaType::Video => "VIDEO".fmt(f),
|
||||
MediaType::Subtitles => "SUBTITLES".fmt(f),
|
||||
MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for MediaType {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Ok(match s {
|
||||
"AUDIO" => MediaType::Audio,
|
||||
"VIDEO" => MediaType::Video,
|
||||
"SUBTITLES" => MediaType::Subtitles,
|
||||
"CLOSED-CAPTIONS" => MediaType::ClosedCaptions,
|
||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifier of a rendition within the segments in a media playlist.
|
||||
///
|
||||
/// See: [4.3.4.1. EXT-X-MEDIA]
|
||||
///
|
||||
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum InStreamId {
|
||||
Cc1,
|
||||
Cc2,
|
||||
Cc3,
|
||||
Cc4,
|
||||
Service1,
|
||||
Service2,
|
||||
Service3,
|
||||
Service4,
|
||||
Service5,
|
||||
Service6,
|
||||
Service7,
|
||||
Service8,
|
||||
Service9,
|
||||
Service10,
|
||||
Service11,
|
||||
Service12,
|
||||
Service13,
|
||||
Service14,
|
||||
Service15,
|
||||
Service16,
|
||||
Service17,
|
||||
Service18,
|
||||
Service19,
|
||||
Service20,
|
||||
Service21,
|
||||
Service22,
|
||||
Service23,
|
||||
Service24,
|
||||
Service25,
|
||||
Service26,
|
||||
Service27,
|
||||
Service28,
|
||||
Service29,
|
||||
Service30,
|
||||
Service31,
|
||||
Service32,
|
||||
Service33,
|
||||
Service34,
|
||||
Service35,
|
||||
Service36,
|
||||
Service37,
|
||||
Service38,
|
||||
Service39,
|
||||
Service40,
|
||||
Service41,
|
||||
Service42,
|
||||
Service43,
|
||||
Service44,
|
||||
Service45,
|
||||
Service46,
|
||||
Service47,
|
||||
Service48,
|
||||
Service49,
|
||||
Service50,
|
||||
Service51,
|
||||
Service52,
|
||||
Service53,
|
||||
Service54,
|
||||
Service55,
|
||||
Service56,
|
||||
Service57,
|
||||
Service58,
|
||||
Service59,
|
||||
Service60,
|
||||
Service61,
|
||||
Service62,
|
||||
Service63,
|
||||
}
|
||||
impl fmt::Display for InStreamId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
format!("{:?}", self).to_uppercase().fmt(f)
|
||||
}
|
||||
}
|
||||
impl FromStr for InStreamId {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Ok(match s {
|
||||
"CC1" => InStreamId::Cc1,
|
||||
"CC2" => InStreamId::Cc2,
|
||||
"CC3" => InStreamId::Cc3,
|
||||
"CC4" => InStreamId::Cc4,
|
||||
"SERVICE1" => InStreamId::Service1,
|
||||
"SERVICE2" => InStreamId::Service2,
|
||||
"SERVICE3" => InStreamId::Service3,
|
||||
"SERVICE4" => InStreamId::Service4,
|
||||
"SERVICE5" => InStreamId::Service5,
|
||||
"SERVICE6" => InStreamId::Service6,
|
||||
"SERVICE7" => InStreamId::Service7,
|
||||
"SERVICE8" => InStreamId::Service8,
|
||||
"SERVICE9" => InStreamId::Service9,
|
||||
"SERVICE10" => InStreamId::Service10,
|
||||
"SERVICE11" => InStreamId::Service11,
|
||||
"SERVICE12" => InStreamId::Service12,
|
||||
"SERVICE13" => InStreamId::Service13,
|
||||
"SERVICE14" => InStreamId::Service14,
|
||||
"SERVICE15" => InStreamId::Service15,
|
||||
"SERVICE16" => InStreamId::Service16,
|
||||
"SERVICE17" => InStreamId::Service17,
|
||||
"SERVICE18" => InStreamId::Service18,
|
||||
"SERVICE19" => InStreamId::Service19,
|
||||
"SERVICE20" => InStreamId::Service20,
|
||||
"SERVICE21" => InStreamId::Service21,
|
||||
"SERVICE22" => InStreamId::Service22,
|
||||
"SERVICE23" => InStreamId::Service23,
|
||||
"SERVICE24" => InStreamId::Service24,
|
||||
"SERVICE25" => InStreamId::Service25,
|
||||
"SERVICE26" => InStreamId::Service26,
|
||||
"SERVICE27" => InStreamId::Service27,
|
||||
"SERVICE28" => InStreamId::Service28,
|
||||
"SERVICE29" => InStreamId::Service29,
|
||||
"SERVICE30" => InStreamId::Service30,
|
||||
"SERVICE31" => InStreamId::Service31,
|
||||
"SERVICE32" => InStreamId::Service32,
|
||||
"SERVICE33" => InStreamId::Service33,
|
||||
"SERVICE34" => InStreamId::Service34,
|
||||
"SERVICE35" => InStreamId::Service35,
|
||||
"SERVICE36" => InStreamId::Service36,
|
||||
"SERVICE37" => InStreamId::Service37,
|
||||
"SERVICE38" => InStreamId::Service38,
|
||||
"SERVICE39" => InStreamId::Service39,
|
||||
"SERVICE40" => InStreamId::Service40,
|
||||
"SERVICE41" => InStreamId::Service41,
|
||||
"SERVICE42" => InStreamId::Service42,
|
||||
"SERVICE43" => InStreamId::Service43,
|
||||
"SERVICE44" => InStreamId::Service44,
|
||||
"SERVICE45" => InStreamId::Service45,
|
||||
"SERVICE46" => InStreamId::Service46,
|
||||
"SERVICE47" => InStreamId::Service47,
|
||||
"SERVICE48" => InStreamId::Service48,
|
||||
"SERVICE49" => InStreamId::Service49,
|
||||
"SERVICE50" => InStreamId::Service50,
|
||||
"SERVICE51" => InStreamId::Service51,
|
||||
"SERVICE52" => InStreamId::Service52,
|
||||
"SERVICE53" => InStreamId::Service53,
|
||||
"SERVICE54" => InStreamId::Service54,
|
||||
"SERVICE55" => InStreamId::Service55,
|
||||
"SERVICE56" => InStreamId::Service56,
|
||||
"SERVICE57" => InStreamId::Service57,
|
||||
"SERVICE58" => InStreamId::Service58,
|
||||
"SERVICE59" => InStreamId::Service59,
|
||||
"SERVICE60" => InStreamId::Service60,
|
||||
"SERVICE61" => InStreamId::Service61,
|
||||
"SERVICE62" => InStreamId::Service62,
|
||||
"SERVICE63" => InStreamId::Service63,
|
||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown instream id: {:?}", s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// HDCP level.
|
||||
///
|
||||
/// 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(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum HdcpLevel {
|
||||
Type0,
|
||||
None,
|
||||
}
|
||||
impl fmt::Display for HdcpLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
HdcpLevel::Type0 => "TYPE-0".fmt(f),
|
||||
HdcpLevel::None => "NONE".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for HdcpLevel {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"TYPE-0" => Ok(HdcpLevel::Type0),
|
||||
"NONE" => Ok(HdcpLevel::None),
|
||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The identifier of a closed captions group or its absence.
|
||||
///
|
||||
/// 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(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ClosedCaptions {
|
||||
GroupId(QuotedString),
|
||||
None,
|
||||
}
|
||||
impl fmt::Display for ClosedCaptions {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ClosedCaptions::GroupId(ref x) => x.fmt(f),
|
||||
ClosedCaptions::None => "NONE".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for ClosedCaptions {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
if s == "NONE" {
|
||||
Ok(ClosedCaptions::None)
|
||||
} else {
|
||||
Ok(ClosedCaptions::GroupId(track!(s.parse())?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Session data.
|
||||
///
|
||||
/// See: [4.3.4.4. EXT-X-SESSION-DATA]
|
||||
///
|
||||
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SessionData {
|
||||
Value(QuotedString),
|
||||
Uri(QuotedString),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_line_string() {
|
||||
assert!(SingleLineString::new("foo").is_ok());
|
||||
assert!(SingleLineString::new("b\rar").is_err());
|
||||
}
|
||||
}
|
687
src/types/byte_range.rs
Normal file
687
src/types/byte_range.rs
Normal file
|
@ -0,0 +1,687 @@
|
|||
use core::convert::{TryFrom, TryInto};
|
||||
use core::fmt;
|
||||
use core::ops::{
|
||||
Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub,
|
||||
SubAssign,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// A range of bytes, which can be seen as either `..end` or `start..end`.
|
||||
///
|
||||
/// It can be constructed from `..end` and `start..end`:
|
||||
///
|
||||
/// ```
|
||||
/// 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 {
|
||||
/// 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>,
|
||||
/// 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 {
|
||||
/// Changes the length of the [`ByteRange`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::ByteRange;
|
||||
/// 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));
|
||||
/// ```
|
||||
///
|
||||
/// # 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());
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the `start` of the [`ByteRange`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::ByteRange;
|
||||
/// assert_eq!(ByteRange::from(0..5).set_start(Some(5)).start(), Some(5));
|
||||
/// assert_eq!(ByteRange::from(..5).set_start(Some(2)).start(), Some(2));
|
||||
/// ```
|
||||
///
|
||||
/// # 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;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds `num` to the `start` and `end` of the range.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::ByteRange;
|
||||
/// let range = ByteRange::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));
|
||||
/// ```
|
||||
///
|
||||
/// # 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);
|
||||
///
|
||||
/// // 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 - end` to the start.
|
||||
if let Some(start) = start.checked_add(usize::MAX - self.end) {
|
||||
self.start = Some(start);
|
||||
self.end = usize::MAX;
|
||||
} else {
|
||||
// both end + start overflowed -> do not change anything
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.end = self.end.saturating_add(num);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Subtracts `num` from the `start` and `end` of the range.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::ByteRange;
|
||||
/// let range = ByteRange::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::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
|
||||
}
|
||||
|
||||
/// 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];
|
||||
|
||||
impl RangeBounds<usize> for ByteRange {
|
||||
fn start_bound(&self) -> Bound<&usize> {
|
||||
self.start
|
||||
.as_ref()
|
||||
.map_or(Bound::Unbounded, Bound::Included)
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.len())?;
|
||||
|
||||
if let Some(value) = self.start {
|
||||
write!(f, "@{}", value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ByteRange {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
let mut input = input.splitn(2, '@');
|
||||
|
||||
let length = input.next().unwrap();
|
||||
let length = length
|
||||
.parse::<usize>()
|
||||
.map_err(|e| Error::parse_int(length, e))?;
|
||||
|
||||
let start = input
|
||||
.next()
|
||||
.map(|v| v.parse::<usize>().map_err(|e| Error::parse_int(v, e)))
|
||||
.transpose()?;
|
||||
|
||||
Ok(Self {
|
||||
start,
|
||||
end: start.unwrap_or(0) + length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<Cow<'a, str>> for ByteRange {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: Cow<'a, str>) -> Result<Self, Self::Error> {
|
||||
//
|
||||
Self::try_from(input.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[should_panic = "the range start (6) must be smaller than the end (0)"]
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
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)"]
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
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);
|
||||
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]
|
||||
#[allow(clippy::identity_op)]
|
||||
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..usize::MAX) + 1; }
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::identity_op)]
|
||||
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..usize::MAX).saturating_add(1),
|
||||
ByteRange::from(usize::MAX..usize::MAX)
|
||||
);
|
||||
assert_eq!(
|
||||
ByteRange::from(..usize::MAX).saturating_add(1),
|
||||
ByteRange::from(..usize::MAX)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ByteRange::from(usize::MAX - 5..usize::MAX).saturating_add(1),
|
||||
ByteRange::from(usize::MAX - 5..usize::MAX)
|
||||
);
|
||||
|
||||
// overflow, but something can be added to the range:
|
||||
assert_eq!(
|
||||
ByteRange::from(usize::MAX - 5..usize::MAX - 3).saturating_add(4),
|
||||
ByteRange::from(usize::MAX - 2..usize::MAX)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ByteRange::from(..usize::MAX - 3).saturating_add(4),
|
||||
ByteRange::from(..usize::MAX)
|
||||
);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test_display() {
|
||||
assert_eq!(ByteRange::from(0..5).to_string(), "5@0".to_string());
|
||||
|
||||
assert_eq!(
|
||||
ByteRange::from(2..100_001).to_string(),
|
||||
"99999@2".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(ByteRange::from(..99999).to_string(), "99999".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(ByteRange::from(2..22), ByteRange::try_from("20@2").unwrap());
|
||||
|
||||
assert_eq!(ByteRange::from(..300), ByteRange::try_from("300").unwrap());
|
||||
|
||||
assert_eq!(
|
||||
ByteRange::try_from("a"),
|
||||
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ByteRange::try_from("1@a"),
|
||||
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
||||
);
|
||||
|
||||
assert!(ByteRange::try_from("").is_err());
|
||||
}
|
||||
}
|
87
src/types/channels.rs
Normal file
87
src/types/channels.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// 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 maximum channel number of
|
||||
/// 6.
|
||||
///
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
#[derive(ShortHand, Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[shorthand(enable(must_use))]
|
||||
pub struct Channels {
|
||||
/// The maximum number of independent simultaneous audio channels.
|
||||
///
|
||||
/// # 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 {
|
||||
/// Makes a new [`Channels`] struct.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::Channels;
|
||||
/// let channels = Channels::new(6);
|
||||
///
|
||||
/// println!("CHANNELS=\"{}\"", channels);
|
||||
/// # assert_eq!(format!("CHANNELS=\"{}\"", channels), "CHANNELS=\"6\"".to_string());
|
||||
/// ```
|
||||
//#[inline]
|
||||
#[must_use]
|
||||
pub const fn new(number: u64) -> Self { Self { number } }
|
||||
}
|
||||
|
||||
impl FromStr for Channels {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::new(
|
||||
input.parse().map_err(|e| Error::parse_int(input, e))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Channels {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.number)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(Channels::new(6).to_string(), "6".to_string());
|
||||
|
||||
assert_eq!(Channels::new(7).to_string(), "7".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(Channels::new(6), Channels::from_str("6").unwrap());
|
||||
|
||||
assert!(Channels::from_str("garbage").is_err());
|
||||
assert!(Channels::from_str("").is_err());
|
||||
}
|
||||
}
|
129
src/types/closed_captions.rs
Normal file
129
src/types/closed_captions.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use core::convert::{Infallible, TryFrom};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
use crate::utils::{quote, unquote};
|
||||
|
||||
/// The identifier of a closed captions group or its absence.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub enum ClosedCaptions<'a> {
|
||||
/// 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(Cow<'a, str>),
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl<'a> ClosedCaptions<'a> {
|
||||
/// Creates a [`ClosedCaptions::GroupId`] with the provided [`String`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hls_m3u8::types::ClosedCaptions;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ClosedCaptions::group_id("vg1"),
|
||||
/// ClosedCaptions::GroupId("vg1".into())
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn group_id<I: Into<Cow<'a, str>>>(value: I) -> Self {
|
||||
//
|
||||
Self::GroupId(value.into())
|
||||
}
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> ClosedCaptions<'static> {
|
||||
match self {
|
||||
Self::GroupId(id) => ClosedCaptions::GroupId(Cow::Owned(id.into_owned())),
|
||||
Self::None => ClosedCaptions::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: PartialEq<str>> PartialEq<T> for ClosedCaptions<'a> {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
match &self {
|
||||
Self::GroupId(value) => other.eq(value),
|
||||
Self::None => other.eq("NONE"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ClosedCaptions<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::GroupId(value) => write!(f, "{}", quote(value)),
|
||||
Self::None => write!(f, "NONE"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for ClosedCaptions<'a> {
|
||||
type Error = Infallible;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
if input.trim() == "NONE" {
|
||||
Ok(Self::None)
|
||||
} else {
|
||||
Ok(Self::GroupId(unquote(input)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(ClosedCaptions::None.to_string(), "NONE".to_string());
|
||||
|
||||
assert_eq!(
|
||||
ClosedCaptions::GroupId("value".into()).to_string(),
|
||||
"\"value\"".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ClosedCaptions::None,
|
||||
ClosedCaptions::try_from("NONE").unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ClosedCaptions::GroupId("value".into()),
|
||||
ClosedCaptions::try_from("\"value\"").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
181
src/types/codecs.rs
Normal file
181
src/types/codecs.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
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<'a> {
|
||||
list: Vec<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> Codecs<'a> {
|
||||
/// 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() } }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> Codecs<'static> {
|
||||
Codecs {
|
||||
list: self
|
||||
.list
|
||||
.into_iter()
|
||||
.map(|v| Cow::Owned(v.into_owned()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<Vec<T>> for Codecs<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
fn from(value: Vec<T>) -> Self {
|
||||
Self {
|
||||
list: value.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this should be implemented with const generics in the future!
|
||||
macro_rules! implement_from {
|
||||
($($size:expr),*) => {
|
||||
$(
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
impl<'a> From<[&'a str; $size]> for Codecs<'a> {
|
||||
fn from(value: [&'a str; $size]) -> Self {
|
||||
Self {
|
||||
list: {
|
||||
let mut result = Vec::with_capacity($size);
|
||||
|
||||
for i in 0..$size {
|
||||
result.push(Cow::Borrowed(value[i]))
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
impl<'a> From<&[&'a str; $size]> for Codecs<'a> {
|
||||
fn from(value: &[&'a str; $size]) -> Self {
|
||||
Self {
|
||||
list: {
|
||||
let mut result = Vec::with_capacity($size);
|
||||
|
||||
for i in 0..$size {
|
||||
result.push(Cow::Borrowed(value[i]))
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
implement_from!(
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||
0x20
|
||||
);
|
||||
|
||||
impl<'a> fmt::Display for Codecs<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(codec) = self.list.first() {
|
||||
write!(f, "{}", codec)?;
|
||||
|
||||
for codec in self.list.iter().skip(1) {
|
||||
write!(f, ",{}", codec)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Codecs<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
list: input.split(',').map(|s| s.into()).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<Cow<'a, str>> for Codecs<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: Cow<'a, str>) -> Result<Self, Self::Error> {
|
||||
match input {
|
||||
Cow::Owned(o) => Ok(Codecs::try_from(o.as_str())?.into_owned()),
|
||||
Cow::Borrowed(b) => Self::try_from(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(["mp4a.40.2", "avc1.4d401e"]).to_string(),
|
||||
"mp4a.40.2,avc1.4d401e".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
Codecs::try_from("mp4a.40.2,avc1.4d401e").unwrap(),
|
||||
Codecs::from(["mp4a.40.2", "avc1.4d401e"])
|
||||
);
|
||||
}
|
||||
}
|
384
src/types/decryption_key.rs
Normal file
384
src/types/decryption_key.rs
Normal file
|
@ -0,0 +1,384 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use shorthand::ShortHand;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{
|
||||
EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion,
|
||||
};
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// 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"))]
|
||||
#[shorthand(enable(skip, must_use, into))]
|
||||
#[non_exhaustive]
|
||||
pub struct DecryptionKey<'a> {
|
||||
/// The encryption method, which has been used to encrypt the data.
|
||||
///
|
||||
/// 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)]
|
||||
#[shorthand(disable(skip))]
|
||||
pub(crate) uri: Cow<'a, str>,
|
||||
/// 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)]
|
||||
pub iv: 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)]
|
||||
pub format: Option<KeyFormat>,
|
||||
/// 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
|
||||
/// [`KeyFormat`] is defined.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This field is optional.
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
pub versions: Option<KeyFormatVersions>,
|
||||
}
|
||||
|
||||
impl<'a> DecryptionKey<'a> {
|
||||
/// Creates a new `DecryptionKey` from an uri pointing to the key data and
|
||||
/// an `EncryptionMethod`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.uri/key");
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn new<I: Into<Cow<'a, str>>>(method: EncryptionMethod, uri: I) -> Self {
|
||||
Self {
|
||||
method,
|
||||
uri: uri.into(),
|
||||
iv: InitializationVector::default(),
|
||||
format: None,
|
||||
versions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for a `DecryptionKey`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::{EncryptionMethod, KeyFormat};
|
||||
///
|
||||
/// let key = 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(&[1, 2, 3, 4, 5])
|
||||
/// .build()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn builder() -> DecryptionKeyBuilder<'a> { DecryptionKeyBuilder::default() }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> DecryptionKey<'static> {
|
||||
DecryptionKey {
|
||||
method: self.method,
|
||||
uri: Cow::Owned(self.uri.into_owned()),
|
||||
iv: self.iv,
|
||||
format: self.format,
|
||||
versions: self.versions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<'a> RequiredVersion for DecryptionKey<'a> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
if self.format.is_some() || self.versions.is_some() {
|
||||
ProtocolVersion::V5
|
||||
} else if self.iv.is_some() {
|
||||
ProtocolVersion::V2
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for DecryptionKey<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
let mut method = None;
|
||||
let mut uri = None;
|
||||
let mut iv = None;
|
||||
let mut format = None;
|
||||
let mut versions = None;
|
||||
|
||||
for (key, value) in AttributePairs::new(input) {
|
||||
match key {
|
||||
"METHOD" => method = Some(value.parse().map_err(Error::strum)?),
|
||||
"URI" => {
|
||||
let unquoted_uri = unquote(value);
|
||||
|
||||
if !unquoted_uri.trim().is_empty() {
|
||||
uri = Some(unquoted_uri);
|
||||
}
|
||||
}
|
||||
"IV" => iv = Some(value.parse()?),
|
||||
"KEYFORMAT" => format = Some(value.parse()?),
|
||||
"KEYFORMATVERSIONS" => versions = Some(value.parse()?),
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized
|
||||
// AttributeName.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let method = method.ok_or_else(|| Error::missing_value("METHOD"))?;
|
||||
let uri = uri.ok_or_else(|| Error::missing_value("URI"))?;
|
||||
let iv = iv.unwrap_or_default();
|
||||
|
||||
Ok(Self {
|
||||
method,
|
||||
uri,
|
||||
iv,
|
||||
format,
|
||||
versions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for DecryptionKey<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?;
|
||||
|
||||
if let InitializationVector::Aes128(_) = &self.iv {
|
||||
write!(f, ",IV={}", &self.iv)?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.format {
|
||||
write!(f, ",KEYFORMAT={}", quote(value))?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.versions {
|
||||
if !value.is_default() {
|
||||
write!(f, ",KEYFORMATVERSIONS={}", value)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DecryptionKeyBuilder<'a> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::types::{EncryptionMethod, KeyFormat};
|
||||
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, TryFrom::try_from($str).unwrap());
|
||||
)+
|
||||
|
||||
assert_eq!(
|
||||
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"),
|
||||
DecryptionKey::try_from(concat!(
|
||||
"METHOD=AES-128,",
|
||||
"URI=\"http://www.example.com\",",
|
||||
"UNKNOWNTAG=abcd"
|
||||
)).unwrap(),
|
||||
);
|
||||
assert!(DecryptionKey::try_from("METHOD=AES-128,URI=").is_err());
|
||||
assert!(DecryptionKey::try_from("garbage").is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder() {
|
||||
let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
||||
key.iv = [
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
]
|
||||
.into();
|
||||
key.format = Some(KeyFormat::Identity);
|
||||
key.versions = Some(vec![1, 2, 3, 4, 5].into());
|
||||
|
||||
assert_eq!(
|
||||
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()
|
||||
.unwrap(),
|
||||
key
|
||||
);
|
||||
|
||||
assert!(DecryptionKey::builder().build().is_err());
|
||||
assert!(DecryptionKey::builder()
|
||||
.method(EncryptionMethod::Aes128)
|
||||
.build()
|
||||
.is_err());
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
{
|
||||
DecryptionKey::new(
|
||||
EncryptionMethod::Aes128,
|
||||
"https://priv.example.com/key.php?r=52"
|
||||
),
|
||||
concat!(
|
||||
"METHOD=AES-128,",
|
||||
"URI=\"https://priv.example.com/key.php?r=52\""
|
||||
)
|
||||
},
|
||||
{
|
||||
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!(
|
||||
"METHOD=AES-128,",
|
||||
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||
)
|
||||
},
|
||||
{
|
||||
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!(
|
||||
"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!(
|
||||
DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/")
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
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!(
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
80
src/types/encryption_method.rs
Normal file
80
src/types/encryption_method.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use strum::{Display, EnumString};
|
||||
|
||||
/// The encryption method.
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum EncryptionMethod {
|
||||
/// 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
|
||||
/// Initialization Vector (IV) or the Media Sequence Number as the IV
|
||||
///
|
||||
/// ```
|
||||
/// # 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
|
||||
#[strum(serialize = "AES-128")]
|
||||
Aes128,
|
||||
/// The [`MediaSegment`]s contain media samples, such as audio or video,
|
||||
/// that are encrypted 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` [`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
|
||||
/// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264
|
||||
/// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496
|
||||
/// [AC-3]: https://tools.ietf.org/html/rfc8216#ref-AC_3
|
||||
/// [HTTP Live Streaming (HLS) SampleEncryption specification]:
|
||||
/// https://tools.ietf.org/html/rfc8216#ref-SampleEnc
|
||||
SampleAes,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(EncryptionMethod::Aes128.to_string(), "AES-128".to_string());
|
||||
assert_eq!(
|
||||
EncryptionMethod::SampleAes.to_string(),
|
||||
"SAMPLE-AES".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
EncryptionMethod::Aes128,
|
||||
"AES-128".parse::<EncryptionMethod>().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
EncryptionMethod::SampleAes,
|
||||
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
||||
);
|
||||
|
||||
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)]
|
||||
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 PartialOrd for Float {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
#[allow(clippy::all, clippy::unreadable_literal)]
|
||||
const PI: f32 = 3.14159265359;
|
||||
|
||||
#[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]
|
||||
#[allow(clippy::unit_cmp)] // fucked 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]
|
||||
const fn test_eq() {
|
||||
struct _AssertEq
|
||||
where
|
||||
Float: Eq;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_eq() {
|
||||
assert_eq!(Float::new(1.0), Float::new(1.0));
|
||||
assert_ne!(Float::new(1.0), Float::new(33.3));
|
||||
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(PI).to_string(), "3.1415927".to_string());
|
||||
assert_eq!(Float::new(-PI).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(PI), 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(f32::INFINITY); }
|
||||
|
||||
#[test]
|
||||
#[should_panic = "float must be finite: `-inf`"]
|
||||
fn test_new_neg_infinite() { let _ = Float::new(f32::NEG_INFINITY); }
|
||||
|
||||
#[test]
|
||||
#[should_panic = "float must not be `NaN`"]
|
||||
fn test_new_nan() { let _ = Float::new(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(f32::INFINITY).is_err());
|
||||
assert!(Float::try_from(f32::NAN).is_err());
|
||||
assert!(Float::try_from(f32::NEG_INFINITY).is_err());
|
||||
}
|
||||
}
|
41
src/types/hdcp_level.rs
Normal file
41
src/types/hdcp_level.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use strum::{Display, EnumString};
|
||||
|
||||
/// HDCP ([`High-bandwidth Digital Content Protection`]) level.
|
||||
///
|
||||
/// [`High-bandwidth Digital Content Protection`]:
|
||||
/// https://www.digital-cp.com/sites/default/files/specifications/HDCP%20on%20HDMI%20Specification%20Rev2_2_Final1.pdf
|
||||
#[non_exhaustive]
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||
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")]
|
||||
Type0,
|
||||
/// The content does not require output copy protection.
|
||||
None,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(HdcpLevel::Type0.to_string(), "TYPE-0".to_string());
|
||||
assert_eq!(HdcpLevel::None.to_string(), "NONE".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(HdcpLevel::Type0, "TYPE-0".parse().unwrap());
|
||||
assert_eq!(HdcpLevel::None, "NONE".parse().unwrap());
|
||||
|
||||
assert!("unk".parse::<HdcpLevel>().is_err());
|
||||
}
|
||||
}
|
199
src/types/in_stream_id.rs
Normal file
199
src/types/in_stream_id.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::traits::RequiredVersion;
|
||||
use crate::types::ProtocolVersion;
|
||||
|
||||
/// Identifier of a rendition within the [`MediaSegment`]s in a
|
||||
/// [`MediaPlaylist`].
|
||||
///
|
||||
/// The variants [`InStreamId::Cc1`], [`InStreamId::Cc2`], [`InStreamId::Cc3`],
|
||||
/// and [`InStreamId::Cc4`] identify a Line 21 Data Services channel ([CEA608]).
|
||||
///
|
||||
/// 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)]
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
pub enum InStreamId {
|
||||
Cc1,
|
||||
Cc2,
|
||||
Cc3,
|
||||
Cc4,
|
||||
Service1,
|
||||
Service2,
|
||||
Service3,
|
||||
Service4,
|
||||
Service5,
|
||||
Service6,
|
||||
Service7,
|
||||
Service8,
|
||||
Service9,
|
||||
Service10,
|
||||
Service11,
|
||||
Service12,
|
||||
Service13,
|
||||
Service14,
|
||||
Service15,
|
||||
Service16,
|
||||
Service17,
|
||||
Service18,
|
||||
Service19,
|
||||
Service20,
|
||||
Service21,
|
||||
Service22,
|
||||
Service23,
|
||||
Service24,
|
||||
Service25,
|
||||
Service26,
|
||||
Service27,
|
||||
Service28,
|
||||
Service29,
|
||||
Service30,
|
||||
Service31,
|
||||
Service32,
|
||||
Service33,
|
||||
Service34,
|
||||
Service35,
|
||||
Service36,
|
||||
Service37,
|
||||
Service38,
|
||||
Service39,
|
||||
Service40,
|
||||
Service41,
|
||||
Service42,
|
||||
Service43,
|
||||
Service44,
|
||||
Service45,
|
||||
Service46,
|
||||
Service47,
|
||||
Service48,
|
||||
Service49,
|
||||
Service50,
|
||||
Service51,
|
||||
Service52,
|
||||
Service53,
|
||||
Service54,
|
||||
Service55,
|
||||
Service56,
|
||||
Service57,
|
||||
Service58,
|
||||
Service59,
|
||||
Service60,
|
||||
Service61,
|
||||
Service62,
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! gen_tests {
|
||||
( $($string:expr => $enum:expr),* ) => {
|
||||
#[test]
|
||||
fn test_display() {
|
||||
$(
|
||||
assert_eq!($enum.to_string(), $string.to_string());
|
||||
)*
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
$(
|
||||
assert_eq!($enum, $string.parse::<InStreamId>().unwrap());
|
||||
)*
|
||||
assert!("invalid_input".parse::<InStreamId>().is_err());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
gen_tests![
|
||||
"CC1" => InStreamId::Cc1,
|
||||
"CC2" => InStreamId::Cc2,
|
||||
"CC3" => InStreamId::Cc3,
|
||||
"CC4" => InStreamId::Cc4,
|
||||
"SERVICE1" => InStreamId::Service1,
|
||||
"SERVICE2" => InStreamId::Service2,
|
||||
"SERVICE3" => InStreamId::Service3,
|
||||
"SERVICE4" => InStreamId::Service4,
|
||||
"SERVICE5" => InStreamId::Service5,
|
||||
"SERVICE6" => InStreamId::Service6,
|
||||
"SERVICE7" => InStreamId::Service7,
|
||||
"SERVICE8" => InStreamId::Service8,
|
||||
"SERVICE9" => InStreamId::Service9,
|
||||
"SERVICE10" => InStreamId::Service10,
|
||||
"SERVICE11" => InStreamId::Service11,
|
||||
"SERVICE12" => InStreamId::Service12,
|
||||
"SERVICE13" => InStreamId::Service13,
|
||||
"SERVICE14" => InStreamId::Service14,
|
||||
"SERVICE15" => InStreamId::Service15,
|
||||
"SERVICE16" => InStreamId::Service16,
|
||||
"SERVICE17" => InStreamId::Service17,
|
||||
"SERVICE18" => InStreamId::Service18,
|
||||
"SERVICE19" => InStreamId::Service19,
|
||||
"SERVICE20" => InStreamId::Service20,
|
||||
"SERVICE21" => InStreamId::Service21,
|
||||
"SERVICE22" => InStreamId::Service22,
|
||||
"SERVICE23" => InStreamId::Service23,
|
||||
"SERVICE24" => InStreamId::Service24,
|
||||
"SERVICE25" => InStreamId::Service25,
|
||||
"SERVICE26" => InStreamId::Service26,
|
||||
"SERVICE27" => InStreamId::Service27,
|
||||
"SERVICE28" => InStreamId::Service28,
|
||||
"SERVICE29" => InStreamId::Service29,
|
||||
"SERVICE30" => InStreamId::Service30,
|
||||
"SERVICE31" => InStreamId::Service31,
|
||||
"SERVICE32" => InStreamId::Service32,
|
||||
"SERVICE33" => InStreamId::Service33,
|
||||
"SERVICE34" => InStreamId::Service34,
|
||||
"SERVICE35" => InStreamId::Service35,
|
||||
"SERVICE36" => InStreamId::Service36,
|
||||
"SERVICE37" => InStreamId::Service37,
|
||||
"SERVICE38" => InStreamId::Service38,
|
||||
"SERVICE39" => InStreamId::Service39,
|
||||
"SERVICE40" => InStreamId::Service40,
|
||||
"SERVICE41" => InStreamId::Service41,
|
||||
"SERVICE42" => InStreamId::Service42,
|
||||
"SERVICE43" => InStreamId::Service43,
|
||||
"SERVICE44" => InStreamId::Service44,
|
||||
"SERVICE45" => InStreamId::Service45,
|
||||
"SERVICE46" => InStreamId::Service46,
|
||||
"SERVICE47" => InStreamId::Service47,
|
||||
"SERVICE48" => InStreamId::Service48,
|
||||
"SERVICE49" => InStreamId::Service49,
|
||||
"SERVICE50" => InStreamId::Service50,
|
||||
"SERVICE51" => InStreamId::Service51,
|
||||
"SERVICE52" => InStreamId::Service52,
|
||||
"SERVICE53" => InStreamId::Service53,
|
||||
"SERVICE54" => InStreamId::Service54,
|
||||
"SERVICE55" => InStreamId::Service55,
|
||||
"SERVICE56" => InStreamId::Service56,
|
||||
"SERVICE57" => InStreamId::Service57,
|
||||
"SERVICE58" => InStreamId::Service58,
|
||||
"SERVICE59" => InStreamId::Service59,
|
||||
"SERVICE60" => InStreamId::Service60,
|
||||
"SERVICE61" => InStreamId::Service61,
|
||||
"SERVICE62" => InStreamId::Service62,
|
||||
"SERVICE63" => InStreamId::Service63
|
||||
];
|
||||
}
|
305
src/types/initialization_vector.rs
Normal file
305
src/types/initialization_vector.rs
Normal file
|
@ -0,0 +1,305 @@
|
|||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// An initialization vector (IV) is a fixed size input that can be used along
|
||||
/// with a secret key for data encryption.
|
||||
///
|
||||
/// 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
|
||||
/// 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)]
|
||||
#[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 {
|
||||
/// Returns the IV as an [`u128`]. `None` is returned for
|
||||
/// [`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 Default for InitializationVector {
|
||||
fn default() -> Self { Self::Missing }
|
||||
}
|
||||
|
||||
impl From<[u8; 0x10]> for InitializationVector {
|
||||
fn from(value: [u8; 0x10]) -> Self { Self::Aes128(value) }
|
||||
}
|
||||
|
||||
impl From<Option<[u8; 0x10]>> for InitializationVector {
|
||||
fn from(value: Option<[u8; 0x10]>) -> Self {
|
||||
match value {
|
||||
Some(v) => Self::Aes128(v),
|
||||
None => Self::Missing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InitializationVector {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::Aes128(buffer) => {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for InitializationVector {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
if !(input.starts_with("0x") || input.starts_with("0X")) {
|
||||
return Err(Error::custom("An IV should either start with `0x` or `0X`"));
|
||||
}
|
||||
|
||||
if input.len() - 2 != 32 {
|
||||
return Err(Error::custom(
|
||||
"An IV must be 32 bytes long + 2 bytes for 0x/0X",
|
||||
));
|
||||
}
|
||||
|
||||
let mut result = [0; 16];
|
||||
|
||||
::hex::decode_to_slice(&input.as_bytes()[2..], &mut result).map_err(Error::hex)?;
|
||||
|
||||
Ok(Self::Aes128(result))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
assert_eq!(
|
||||
InitializationVector::default(),
|
||||
InitializationVector::Missing
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(
|
||||
InitializationVector::from([
|
||||
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]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
InitializationVector::Aes128([
|
||||
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());
|
||||
}
|
||||
}
|
72
src/types/key_format.rs
Normal file
72
src/types/key_format.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
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)]
|
||||
pub enum KeyFormat {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Default for KeyFormat {
|
||||
fn default() -> Self { Self::Identity }
|
||||
}
|
||||
|
||||
impl FromStr for KeyFormat {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
tag(&unquote(input), "identity")?; // currently only KeyFormat::Identity exists!
|
||||
|
||||
Ok(Self::Identity)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyFormat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", quote("identity")) }
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V5`].
|
||||
impl RequiredVersion for KeyFormat {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(KeyFormat::Identity.to_string(), quote("identity"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap());
|
||||
|
||||
assert_eq!(KeyFormat::Identity, "identity".parse().unwrap());
|
||||
|
||||
assert!("garbage".parse::<KeyFormat>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(KeyFormat::Identity.required_version(), ProtocolVersion::V5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
assert_eq!(KeyFormat::Identity, KeyFormat::default());
|
||||
}
|
||||
}
|
685
src/types/key_format_versions.rs
Normal file
685
src/types/key_format_versions.rs
Normal file
|
@ -0,0 +1,685 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::{Extend, FromIterator};
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::slice::SliceIndex;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
use crate::RequiredVersion;
|
||||
|
||||
/// 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
|
||||
/// [`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
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
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 {
|
||||
/// 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() }
|
||||
|
||||
/// Add a value to the end of [`KeyFormatVersions`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// 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 {
|
||||
self.len -= 1;
|
||||
Some(self.buffer[self.len()])
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true`, if it is either empty or has a length of 1 and the first
|
||||
/// element is 1.
|
||||
///
|
||||
/// # 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 {
|
||||
iter.into_iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V5`].
|
||||
impl RequiredVersion for KeyFormatVersions {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
||||
}
|
||||
|
||||
impl FromStr for KeyFormatVersions {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut result = Self::default();
|
||||
|
||||
for item in unquote(input)
|
||||
.split('/')
|
||||
.map(|v| v.parse().map_err(|e| Error::parse_int(v, e)))
|
||||
{
|
||||
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() {
|
||||
result.push(1);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyFormatVersions {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.is_default() || self.is_empty() {
|
||||
return write!(f, "{}", quote("1"));
|
||||
}
|
||||
|
||||
write!(f, "\"{}", self.buffer[0])?;
|
||||
|
||||
for item in &self.buffer[1..self.len()] {
|
||||
write!(f, "/{}", item)?;
|
||||
}
|
||||
|
||||
write!(f, "\"")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[usize]>> From<T> for KeyFormatVersions {
|
||||
fn from(value: T) -> Self { value.as_ref().iter().map(|i| *i as u8).collect() }
|
||||
}
|
||||
|
||||
/// `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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unit_cmp)] // fucked 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]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
KeyFormatVersions::from([1, 2, 3, 4, 5]).to_string(),
|
||||
quote("1/2/3/4/5")
|
||||
);
|
||||
|
||||
assert_eq!(KeyFormatVersions::from([]).to_string(), quote("1"));
|
||||
assert_eq!(KeyFormatVersions::new().to_string(), quote("1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
KeyFormatVersions::from([1, 2, 3, 4, 5]),
|
||||
quote("1/2/3/4/5").parse().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(KeyFormatVersions::from([1]), "1".parse().unwrap());
|
||||
assert_eq!(KeyFormatVersions::from([1, 2]), "1/2".parse().unwrap());
|
||||
|
||||
assert!("1/b".parse::<KeyFormatVersions>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
KeyFormatVersions::new().required_version(),
|
||||
ProtocolVersion::V5
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_default() {
|
||||
assert!(KeyFormatVersions::new().is_default());
|
||||
assert!(KeyFormatVersions::default().is_default());
|
||||
|
||||
assert!(KeyFormatVersions::from([]).is_default());
|
||||
assert!(KeyFormatVersions::from([1]).is_default());
|
||||
|
||||
assert!(!KeyFormatVersions::from([1, 2, 3]).is_default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push() {
|
||||
let mut key_format_versions = KeyFormatVersions::new();
|
||||
key_format_versions.push(2);
|
||||
|
||||
assert_eq!(KeyFormatVersions::from([2]), key_format_versions);
|
||||
}
|
||||
}
|
41
src/types/media_type.rs
Normal file
41
src/types/media_type.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use strum::{Display, EnumString};
|
||||
|
||||
/// Specifies the media type.
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Ord, PartialOrd, Display, EnumString, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum MediaType {
|
||||
Audio,
|
||||
Video,
|
||||
Subtitles,
|
||||
ClosedCaptions,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(MediaType::Audio, "AUDIO".parse().unwrap());
|
||||
assert_eq!(MediaType::Video, "VIDEO".parse().unwrap());
|
||||
assert_eq!(MediaType::Subtitles, "SUBTITLES".parse().unwrap());
|
||||
assert_eq!(
|
||||
MediaType::ClosedCaptions,
|
||||
"CLOSED-CAPTIONS".parse().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(MediaType::Audio.to_string(), "AUDIO".to_string());
|
||||
assert_eq!(MediaType::Video.to_string(), "VIDEO".to_string());
|
||||
assert_eq!(MediaType::Subtitles.to_string(), "SUBTITLES".to_string());
|
||||
assert_eq!(
|
||||
MediaType::ClosedCaptions.to_string(),
|
||||
"CLOSED-CAPTIONS".to_string()
|
||||
);
|
||||
}
|
||||
}
|
42
src/types/mod.rs
Normal file
42
src/types/mod.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
//! Miscellaneous types.
|
||||
pub(crate) mod byte_range;
|
||||
pub(crate) mod channels;
|
||||
pub(crate) mod closed_captions;
|
||||
pub(crate) mod codecs;
|
||||
pub(crate) mod decryption_key;
|
||||
pub(crate) mod encryption_method;
|
||||
pub(crate) mod hdcp_level;
|
||||
pub(crate) mod in_stream_id;
|
||||
pub(crate) mod initialization_vector;
|
||||
pub(crate) mod key_format;
|
||||
pub(crate) mod key_format_versions;
|
||||
pub(crate) mod media_type;
|
||||
pub(crate) mod playlist_type;
|
||||
pub(crate) mod protocol_version;
|
||||
pub(crate) mod resolution;
|
||||
pub(crate) mod stream_data;
|
||||
pub(crate) mod value;
|
||||
|
||||
pub(crate) mod float;
|
||||
pub(crate) mod ufloat;
|
||||
|
||||
pub use byte_range::*;
|
||||
pub use channels::*;
|
||||
pub use closed_captions::*;
|
||||
pub use codecs::*;
|
||||
pub use decryption_key::DecryptionKey;
|
||||
pub use encryption_method::*;
|
||||
pub use hdcp_level::*;
|
||||
pub use in_stream_id::*;
|
||||
pub use initialization_vector::*;
|
||||
pub use key_format::*;
|
||||
pub use key_format_versions::*;
|
||||
pub use media_type::*;
|
||||
pub use playlist_type::*;
|
||||
pub use protocol_version::*;
|
||||
pub use resolution::*;
|
||||
pub use stream_data::StreamData;
|
||||
pub use value::*;
|
||||
|
||||
pub use float::Float;
|
||||
pub use ufloat::UFloat;
|
99
src/types/playlist_type.rs
Normal file
99
src/types/playlist_type.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
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 TryFrom<&str> for PlaylistType {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||
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!(
|
||||
PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:VOD").unwrap(),
|
||||
PlaylistType::Vod,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:EVENT").unwrap(),
|
||||
PlaylistType::Event,
|
||||
);
|
||||
|
||||
assert!(PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:H").is_err());
|
||||
|
||||
assert!(PlaylistType::try_from("garbage").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);
|
||||
}
|
||||
}
|
113
src/types/protocol_version.rs
Normal file
113
src/types/protocol_version.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// The [`ProtocolVersion`] specifies which `m3u8` revision is required, to
|
||||
/// parse a certain tag correctly.
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ProtocolVersion {
|
||||
V1,
|
||||
V2,
|
||||
V3,
|
||||
V4,
|
||||
V5,
|
||||
V6,
|
||||
V7,
|
||||
}
|
||||
|
||||
impl ProtocolVersion {
|
||||
/// Returns the latest [`ProtocolVersion`] that is supported by
|
||||
/// this library.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::ProtocolVersion;
|
||||
/// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn latest() -> Self { Self::V7 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ProtocolVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::V1 => write!(f, "1"),
|
||||
Self::V2 => write!(f, "2"),
|
||||
Self::V3 => write!(f, "3"),
|
||||
Self::V4 => write!(f, "4"),
|
||||
Self::V5 => write!(f, "5"),
|
||||
Self::V6 => write!(f, "6"),
|
||||
Self::V7 => write!(f, "7"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ProtocolVersion {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
Ok({
|
||||
match input.trim() {
|
||||
"1" => Self::V1,
|
||||
"2" => Self::V2,
|
||||
"3" => Self::V3,
|
||||
"4" => Self::V4,
|
||||
"5" => Self::V5,
|
||||
"6" => Self::V6,
|
||||
"7" => Self::V7,
|
||||
_ => return Err(Error::unknown_protocol_version(input)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The default is [`ProtocolVersion::V1`].
|
||||
impl Default for ProtocolVersion {
|
||||
fn default() -> Self { Self::V1 }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(ProtocolVersion::V1.to_string(), "1".to_string());
|
||||
assert_eq!(ProtocolVersion::V2.to_string(), "2".to_string());
|
||||
assert_eq!(ProtocolVersion::V3.to_string(), "3".to_string());
|
||||
assert_eq!(ProtocolVersion::V4.to_string(), "4".to_string());
|
||||
assert_eq!(ProtocolVersion::V5.to_string(), "5".to_string());
|
||||
assert_eq!(ProtocolVersion::V6.to_string(), "6".to_string());
|
||||
assert_eq!(ProtocolVersion::V7.to_string(), "7".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(ProtocolVersion::V1, "1".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V2, "2".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V3, "3".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V4, "4".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V5, "5".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V6, "6".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V7, "7".parse().unwrap());
|
||||
|
||||
assert_eq!(ProtocolVersion::V7, " 7 ".parse().unwrap());
|
||||
assert!("garbage".parse::<ProtocolVersion>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
assert_eq!(ProtocolVersion::default(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latest() {
|
||||
assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
|
||||
}
|
||||
}
|
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("{}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 From<Resolution> for (usize, usize) {
|
||||
fn from(val: Resolution) -> Self { (val.width, val.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());
|
||||
}
|
||||
}
|
420
src/types/stream_data.rs
Normal file
420
src/types/stream_data.rs
Normal file
|
@ -0,0 +1,420 @@
|
|||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
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<'a> {
|
||||
/// 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<'a>>,
|
||||
/// 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".into()));
|
||||
/// ```
|
||||
///
|
||||
/// # 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<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> StreamData<'a> {
|
||||
/// 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<'a> { StreamDataBuilder::default() }
|
||||
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> StreamData<'static> {
|
||||
StreamData {
|
||||
bandwidth: self.bandwidth,
|
||||
average_bandwidth: self.average_bandwidth,
|
||||
codecs: self.codecs.map(Codecs::into_owned),
|
||||
resolution: self.resolution,
|
||||
hdcp_level: self.hdcp_level,
|
||||
video: self.video.map(|v| Cow::Owned(v.into_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for StreamData<'a> {
|
||||
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<'a> TryFrom<&'a str> for StreamData<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
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(TryFrom::try_from(unquote(value))?),
|
||||
"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<'a> RequiredVersion for StreamData<'a> {
|
||||
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,
|
||||
StreamData::try_from(concat!(
|
||||
"BANDWIDTH=200,",
|
||||
"AVERAGE-BANDWIDTH=15,",
|
||||
"CODECS=\"mp4a.40.2,avc1.4d401e\",",
|
||||
"RESOLUTION=1920x1080,",
|
||||
"HDCP-LEVEL=TYPE-0,",
|
||||
"VIDEO=\"video\""
|
||||
))
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert!(StreamData::try_from("garbage").is_err());
|
||||
}
|
||||
}
|
320
src/types/ufloat.rs
Normal file
320
src/types/ufloat.rs
Normal file
|
@ -0,0 +1,320 @@
|
|||
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, 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 PartialOrd for UFloat {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
#[allow(clippy::all, clippy::unreadable_literal)]
|
||||
const PI: f32 = 3.14159265359;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(UFloat::new(22.0).to_string(), "22".to_string());
|
||||
assert_eq!(UFloat::new(PI).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(PI), 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]
|
||||
#[allow(clippy::unit_cmp)] // fucked 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), UFloat::new(1.0));
|
||||
assert_ne!(UFloat::new(1.0), UFloat::new(33.3));
|
||||
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(f32::INFINITY); }
|
||||
|
||||
#[test]
|
||||
#[should_panic = "float must be finite: `-inf`"]
|
||||
fn test_new_neg_infinite() { let _ = UFloat::new(f32::NEG_INFINITY); }
|
||||
|
||||
#[test]
|
||||
#[should_panic = "float must not be `NaN`"]
|
||||
fn test_new_nan() { let _ = UFloat::new(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(f32::INFINITY).is_err());
|
||||
assert!(UFloat::try_from(f32::NAN).is_err());
|
||||
assert!(UFloat::try_from(f32::NEG_INFINITY).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
const fn test_eq() {
|
||||
struct _AssertEq
|
||||
where
|
||||
UFloat: Eq;
|
||||
}
|
||||
}
|
126
src/types/value.rs
Normal file
126
src/types/value.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use crate::types::Float;
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
|
||||
/// A `Value`.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
pub enum Value<'a> {
|
||||
/// A `String`.
|
||||
String(Cow<'a, str>),
|
||||
/// A sequence of bytes.
|
||||
Hex(Vec<u8>),
|
||||
/// A floating point number, that's neither NaN nor infinite.
|
||||
Float(Float),
|
||||
}
|
||||
|
||||
impl<'a> Value<'a> {
|
||||
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||
/// internal [`Cow`]s.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a relatively expensive operation.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> Value<'static> {
|
||||
match self {
|
||||
Self::String(value) => Value::String(Cow::Owned(value.into_owned())),
|
||||
Self::Hex(value) => Value::Hex(value),
|
||||
Self::Float(value) => Value::Float(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Value<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::String(value) => write!(f, "{}", quote(value)),
|
||||
Self::Hex(value) => write!(f, "0x{}", hex::encode_upper(value)),
|
||||
Self::Float(value) => write!(f, "{}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Value<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||
if input.starts_with("0x") || input.starts_with("0X") {
|
||||
Ok(Self::Hex(
|
||||
hex::decode(input.trim_start_matches("0x").trim_start_matches("0X"))
|
||||
.map_err(Error::hex)?,
|
||||
))
|
||||
} else {
|
||||
match input.parse() {
|
||||
Ok(value) => Ok(Self::Float(value)),
|
||||
Err(_) => Ok(Self::String(unquote(input))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Float>> From<T> for Value<'static> {
|
||||
fn from(value: T) -> Self { Self::Float(value.into()) }
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Value<'static> {
|
||||
fn from(value: Vec<u8>) -> Self { Self::Hex(value) }
|
||||
}
|
||||
|
||||
impl From<String> for Value<'static> {
|
||||
fn from(value: String) -> Self { Self::String(Cow::Owned(unquote(&value).into_owned())) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(Value::Float(Float::new(1.1)).to_string(), "1.1".to_string());
|
||||
assert_eq!(
|
||||
Value::String("&str".into()).to_string(),
|
||||
"\"&str\"".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Value::Hex(vec![1, 2, 3]).to_string(),
|
||||
"0x010203".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
Value::Float(Float::new(1.1)),
|
||||
Value::try_from("1.1").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Value::String("&str".into()),
|
||||
Value::try_from("\"&str\"").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Value::Hex(vec![1, 2, 3]),
|
||||
Value::try_from("0x010203").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Value::Hex(vec![1, 2, 3]),
|
||||
Value::try_from("0X010203").unwrap()
|
||||
);
|
||||
assert!(Value::try_from("0x010203Z").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(Value::from(1_u8), Value::Float(Float::new(1.0)));
|
||||
assert_eq!(
|
||||
Value::from("&str".to_string()),
|
||||
Value::String("&str".into())
|
||||
);
|
||||
assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3]));
|
||||
}
|
||||
}
|
186
src/utils.rs
Normal file
186
src/utils.rs
Normal file
|
@ -0,0 +1,186 @@
|
|||
use core::iter;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// 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 {
|
||||
( $( $tag:expr ),* ) => {
|
||||
::core::iter::empty()
|
||||
$(
|
||||
.chain(::core::iter::once($tag.required_version()))
|
||||
)*
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
||||
match s.as_ref() {
|
||||
"YES" => Ok(true),
|
||||
"NO" => Ok(false),
|
||||
_ => Err(Error::invalid_input()),
|
||||
}
|
||||
}
|
||||
|
||||
/// According to the documentation the following characters are forbidden
|
||||
/// inside a quoted string:
|
||||
/// - carriage return (`\r`)
|
||||
/// - new line (`\n`)
|
||||
/// - double quotes (`"`)
|
||||
///
|
||||
/// 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)
|
||||
pub(crate) fn unquote(value: &str) -> Cow<'_, str> {
|
||||
if value.starts_with('"') && value.ends_with('"') {
|
||||
let result = Cow::Borrowed(&value[1..value.len() - 1]);
|
||||
|
||||
if !result.chars().any(|c| c == '"' || c == '\n' || c == '\r') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Cow::Owned(
|
||||
value
|
||||
.chars()
|
||||
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Puts a string inside quotes.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn quote<T: ToString>(value: T) -> String {
|
||||
// the replace is for the case, that quote is called on an already quoted
|
||||
// string, which could cause problems!
|
||||
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,
|
||||
/// it will remove it and return the rest of the input.
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
/// This function will return `Error::MissingTag`, if the input doesn't start
|
||||
/// with the tag, that has been passed to this function.
|
||||
pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
if !input.trim().starts_with(tag.as_ref()) {
|
||||
return Err(Error::missing_tag(tag.as_ref(), input));
|
||||
}
|
||||
|
||||
Ok(input.trim().split_at(tag.as_ref().len()).1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parse_yes_or_no() {
|
||||
assert!(parse_yes_or_no("YES").unwrap());
|
||||
assert!(!parse_yes_or_no("NO").unwrap());
|
||||
assert!(parse_yes_or_no("garbage").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unquote() {
|
||||
assert_eq!(unquote("\"TestValue\""), "TestValue".to_string());
|
||||
assert_eq!(unquote("\"TestValue\n\""), "TestValue".to_string());
|
||||
assert_eq!(unquote("\"TestValue\n\r\""), "TestValue".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quote() {
|
||||
assert_eq!(quote("value"), "\"value\"".to_string());
|
||||
assert_eq!(quote("\"value\""), "\"value\"".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag() {
|
||||
let input = "HelloMyFriendThisIsASampleString";
|
||||
|
||||
let input = tag(input, "Hello").unwrap();
|
||||
assert_eq!(input, "MyFriendThisIsASampleString");
|
||||
|
||||
let input = tag(input, "My").unwrap();
|
||||
assert_eq!(input, "FriendThisIsASampleString");
|
||||
|
||||
let input = tag(input, "FriendThisIs").unwrap();
|
||||
assert_eq!(input, "ASampleString");
|
||||
|
||||
let input = tag(input, "A").unwrap();
|
||||
assert_eq!(input, "SampleString");
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
30
tests/issues/assets/issue_00055.m3u8
Normal file
30
tests/issues/assets/issue_00055.m3u8
Normal file
|
@ -0,0 +1,30 @@
|
|||
#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_aac_1",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="https://www.example.com/file_01.m3u8",FORCED=NO
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_aac_2",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="https://www.example.com/file_02.m3u8",FORCED=NO
|
||||
#EXT-X-STREAM-INF:RESOLUTION=426x240,CODECS="avc1.4D401F,mp4a.40.2",BANDWIDTH=609683,AVERAGE-BANDWIDTH=337111,FRAME-RATE=24.000,AUDIO="audio_aac_1"
|
||||
https://www.example.com/file_03.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=426x240,CODECS="avc1.4D401F,mp4a.40.2",BANDWIDTH=672828,AVERAGE-BANDWIDTH=401121,FRAME-RATE=24.000,AUDIO="audio_aac_2"
|
||||
https://www.example.com/file_04.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=640x360,CODECS="avc1.4D401F,mp4a.40.2",BANDWIDTH=963123,AVERAGE-BANDWIDTH=498553,FRAME-RATE=24.000,AUDIO="audio_aac_1"
|
||||
https://www.example.com/file_05.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=640x360,CODECS="avc1.4D401F,mp4a.40.2",BANDWIDTH=1026268,AVERAGE-BANDWIDTH=562563,FRAME-RATE=24.000,AUDIO="audio_aac_2"
|
||||
https://www.example.com/file_06.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=852x480,CODECS="avc1.4D401F,mp4a.40.2",BANDWIDTH=1365255,AVERAGE-BANDWIDTH=652779,FRAME-RATE=24.000,AUDIO="audio_aac_1"
|
||||
https://www.example.com/file_07.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=852x480,CODECS="avc1.4D401F,mp4a.40.2",BANDWIDTH=1428400,AVERAGE-BANDWIDTH=716789,FRAME-RATE=24.000,AUDIO="audio_aac_2"
|
||||
https://www.example.com/file_08.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=1280x720,CODECS="avc1.4D4020,mp4a.40.2",BANDWIDTH=2342667,AVERAGE-BANDWIDTH=1030774,FRAME-RATE=24.000,AUDIO="audio_aac_1"
|
||||
https://www.example.com/file_09.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=1280x720,CODECS="avc1.4D4020,mp4a.40.2",BANDWIDTH=2405812,AVERAGE-BANDWIDTH=1094784,FRAME-RATE=24.000,AUDIO="audio_aac_2"
|
||||
https://www.example.com/file_10.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=1920x1080,CODECS="avc1.64002A,mp4a.40.2",BANDWIDTH=4635327,AVERAGE-BANDWIDTH=1687626,FRAME-RATE=24.000,AUDIO="audio_aac_1"
|
||||
https://www.example.com/file_11.m3u8
|
||||
#EXT-X-STREAM-INF:RESOLUTION=1920x1080,CODECS="avc1.64002A,mp4a.40.2",BANDWIDTH=4698472,AVERAGE-BANDWIDTH=1751636,FRAME-RATE=24.000,AUDIO="audio_aac_2"
|
||||
https://www.example.com/file_12.m3u8
|
||||
#EXT-X-I-FRAME-STREAM-INF:RESOLUTION=426x240,CODECS="avc1.4D401F",BANDWIDTH=92496,AVERAGE-BANDWIDTH=31745,URI="https://www.example.com/file_13.m3u8"
|
||||
#EXT-X-I-FRAME-STREAM-INF:RESOLUTION=640x360,CODECS="avc1.4D401F",BANDWIDTH=252672,AVERAGE-BANDWIDTH=53787,URI="https://www.example.com/file_14.m3u8"
|
||||
#EXT-X-I-FRAME-STREAM-INF:RESOLUTION=852x480,CODECS="avc1.4D401F",BANDWIDTH=392544,AVERAGE-BANDWIDTH=72767,URI="https://www.example.com/file_15.m3u8"
|
||||
#EXT-X-I-FRAME-STREAM-INF:RESOLUTION=1280x720,CODECS="avc1.4D4020",BANDWIDTH=649728,AVERAGE-BANDWIDTH=108944,URI="https://www.example.com/file_16.m3u8"
|
||||
#EXT-X-I-FRAME-STREAM-INF:RESOLUTION=1920x1080,CODECS="avc1.64002A",BANDWIDTH=1328784,AVERAGE-BANDWIDTH=161039,URI="https://www.example.com/file_17.m3u8"
|
3
tests/issues/assets/issue_00064.m3u8
Normal file
3
tests/issues/assets/issue_00064.m3u8
Normal file
|
@ -0,0 +1,3 @@
|
|||
#EXTM3U
|
||||
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=10000000
|
||||
https://995107575.cloudvdn.com/a.m3u8?cdn=cn-gotcha03&domain=d1--cn-gotcha103.bilivideo.com&expires=1614619920&len=0&oi=1891753406&order=1&player=70YAALwcl0b9RGgW&pt=h5&ptype=0&qn=10000&secondToken=secondToken%3ACZ4ggpPHomuwcnT8XWDjJUp9eh8&sign=325afc8bc3b01ccbadeac084004ece64&sigparams=cdn%2Cexpires%2Clen%2Coi%2Cpt%2Cqn%2Ctrid&sl=1&src=4&streamid=live-qn%3Alive-qn%2Flive_402401719_42665292&trid=20d9f245179b4ef3a7e3635afaaa87ea&v3=1
|
236
tests/issues/issue_00055.rs
Normal file
236
tests/issues/issue_00055.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
// The relevant issue:
|
||||
// https://github.com/sile/hls_m3u8/issues/55
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use hls_m3u8::tags::{ExtXMedia, VariantStream};
|
||||
use hls_m3u8::types::{MediaType, StreamData, UFloat};
|
||||
use hls_m3u8::MasterPlaylist;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let file = include_str!("assets/issue_00055.m3u8");
|
||||
|
||||
assert_eq!(
|
||||
MasterPlaylist::try_from(file).unwrap(),
|
||||
MasterPlaylist::builder()
|
||||
.has_independent_segments(true)
|
||||
.media(vec![
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio_aac_1")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("https://www.example.com/file_01.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio_aac_2")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("https://www.example.com/file_02.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.variant_streams(vec![
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_03.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_1".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(609683)
|
||||
.average_bandwidth(337111)
|
||||
.resolution((426, 240))
|
||||
.codecs(vec!["avc1.4D401F", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_04.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_2".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(672828)
|
||||
.average_bandwidth(401121)
|
||||
.resolution((426, 240))
|
||||
.codecs(vec!["avc1.4D401F", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_05.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_1".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(963123)
|
||||
.average_bandwidth(498553)
|
||||
.resolution((640, 360))
|
||||
.codecs(vec!["avc1.4D401F", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_06.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_2".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(1026268)
|
||||
.average_bandwidth(562563)
|
||||
.resolution((640, 360))
|
||||
.codecs(vec!["avc1.4D401F", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_07.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_1".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(1365255)
|
||||
.average_bandwidth(652779)
|
||||
.resolution((852, 480))
|
||||
.codecs(vec!["avc1.4D401F", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_08.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_2".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(1428400)
|
||||
.average_bandwidth(716789)
|
||||
.resolution((852, 480))
|
||||
.codecs(vec!["avc1.4D401F", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_09.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_1".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(2342667)
|
||||
.average_bandwidth(1030774)
|
||||
.resolution((1280, 720))
|
||||
.codecs(vec!["avc1.4D4020", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_10.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_2".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(2405812)
|
||||
.average_bandwidth(1094784)
|
||||
.resolution((1280, 720))
|
||||
.codecs(vec!["avc1.4D4020", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_11.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_1".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(4635327)
|
||||
.average_bandwidth(1687626)
|
||||
.resolution((1920, 1080))
|
||||
.codecs(vec!["avc1.64002A", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://www.example.com/file_12.m3u8".into(),
|
||||
frame_rate: Some(UFloat::new(24.000)),
|
||||
audio: Some("audio_aac_2".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(4698472)
|
||||
.average_bandwidth(1751636)
|
||||
.resolution((1920, 1080))
|
||||
.codecs(vec!["avc1.64002A", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXIFrame {
|
||||
uri: "https://www.example.com/file_13.m3u8".into(),
|
||||
stream_data: StreamData::builder()
|
||||
.resolution((426, 240))
|
||||
.codecs(vec!["avc1.4D401F"])
|
||||
.bandwidth(92496)
|
||||
.average_bandwidth(31745)
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXIFrame {
|
||||
uri: "https://www.example.com/file_14.m3u8".into(),
|
||||
stream_data: StreamData::builder()
|
||||
.resolution((640, 360))
|
||||
.codecs(vec!["avc1.4D401F"])
|
||||
.bandwidth(252672)
|
||||
.average_bandwidth(53787)
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXIFrame {
|
||||
uri: "https://www.example.com/file_15.m3u8".into(),
|
||||
stream_data: StreamData::builder()
|
||||
.resolution((852, 480))
|
||||
.codecs(vec!["avc1.4D401F"])
|
||||
.bandwidth(392544)
|
||||
.average_bandwidth(72767)
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXIFrame {
|
||||
uri: "https://www.example.com/file_16.m3u8".into(),
|
||||
stream_data: StreamData::builder()
|
||||
.resolution((1280, 720))
|
||||
.codecs(vec!["avc1.4D4020"])
|
||||
.bandwidth(649728)
|
||||
.average_bandwidth(108944)
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXIFrame {
|
||||
uri: "https://www.example.com/file_17.m3u8".into(),
|
||||
stream_data: StreamData::builder()
|
||||
.resolution((1920, 1080))
|
||||
.codecs(vec!["avc1.64002A"])
|
||||
.bandwidth(1328784)
|
||||
.average_bandwidth(161039)
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
])
|
||||
.build()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
27
tests/issues/issue_00059.rs
Normal file
27
tests/issues/issue_00059.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// The relevant issue:
|
||||
// https://github.com/sile/hls_m3u8/issues/59
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use hls_m3u8::MediaPlaylist;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let playlist = concat!(
|
||||
"#EXTM3U\n",
|
||||
"#EXT-X-DISCONTINUITY-SEQUENCE:1\n",
|
||||
"#EXT-X-TARGETDURATION:10\n",
|
||||
"#EXT-X-VERSION:3\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"
|
||||
);
|
||||
|
||||
let playlist = MediaPlaylist::try_from(playlist).unwrap();
|
||||
assert_eq!(playlist.discontinuity_sequence, 1);
|
||||
}
|
34
tests/issues/issue_00064.rs
Normal file
34
tests/issues/issue_00064.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// The relevant issue:
|
||||
// https://github.com/sile/hls_m3u8/issues/55
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use hls_m3u8::tags::VariantStream;
|
||||
use hls_m3u8::types::StreamData;
|
||||
use hls_m3u8::MasterPlaylist;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let file = include_str!("assets/issue_00064.m3u8");
|
||||
|
||||
assert_eq!(
|
||||
MasterPlaylist::try_from(file).unwrap(),
|
||||
MasterPlaylist::builder()
|
||||
.variant_streams(vec![
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "https://995107575.cloudvdn.com/a.m3u8?cdn=cn-gotcha03&domain=d1--cn-gotcha103.bilivideo.com&expires=1614619920&len=0&oi=1891753406&order=1&player=70YAALwcl0b9RGgW&pt=h5&ptype=0&qn=10000&secondToken=secondToken%3ACZ4ggpPHomuwcnT8XWDjJUp9eh8&sign=325afc8bc3b01ccbadeac084004ece64&sigparams=cdn%2Cexpires%2Clen%2Coi%2Cpt%2Cqn%2Ctrid&sl=1&src=4&streamid=live-qn%3Alive-qn%2Flive_402401719_42665292&trid=20d9f245179b4ef3a7e3635afaaa87ea&v3=1".into(),
|
||||
frame_rate: None,
|
||||
audio: None,
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(10000000)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
])
|
||||
.build()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
1
tests/issues/mod.rs
Normal file
1
tests/issues/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
automod::dir!("tests/issues");
|
131
tests/master_playlist.rs
Normal file
131
tests/master_playlist.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use hls_m3u8::tags::{ExtXMedia, VariantStream};
|
||||
use hls_m3u8::types::{MediaType, StreamData};
|
||||
use hls_m3u8::MasterPlaylist;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! generate_tests {
|
||||
( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $fnname() {
|
||||
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||
|
||||
assert_eq!($struct.to_string(), $str.to_string());
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
test_alternate_audio => {
|
||||
MasterPlaylist::builder()
|
||||
.media(vec![
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("eng/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("fre/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("sp")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("sp/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.variant_streams(vec![
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "lo/prog_index.m3u8".into(),
|
||||
frame_rate: None,
|
||||
audio: Some("audio".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(195023)
|
||||
.codecs(["avc1.42e00a", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
},
|
||||
VariantStream::ExtXStreamInf {
|
||||
uri: "hi/prog_index.m3u8".into(),
|
||||
frame_rate: None,
|
||||
audio: Some("audio".into()),
|
||||
subtitles: None,
|
||||
closed_captions: None,
|
||||
stream_data: StreamData::builder()
|
||||
.bandwidth(591680)
|
||||
.codecs(["avc1.42e01e", "mp4a.40.2"])
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"#EXTM3U\n",
|
||||
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"eng/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio\",",
|
||||
"LANGUAGE=\"eng\",",
|
||||
"NAME=\"English\",",
|
||||
"DEFAULT=YES,",
|
||||
"AUTOSELECT=YES",
|
||||
"\n",
|
||||
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"fre/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio\",",
|
||||
"LANGUAGE=\"fre\",",
|
||||
"NAME=\"Français\",",
|
||||
"AUTOSELECT=YES",
|
||||
"\n",
|
||||
|
||||
"#EXT-X-MEDIA:",
|
||||
"TYPE=AUDIO,",
|
||||
"URI=\"sp/prog_index.m3u8\",",
|
||||
"GROUP-ID=\"audio\",",
|
||||
"LANGUAGE=\"sp\",",
|
||||
"NAME=\"Espanol\",",
|
||||
"AUTOSELECT=YES",
|
||||
"\n",
|
||||
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"BANDWIDTH=195023,",
|
||||
"CODECS=\"avc1.42e00a,mp4a.40.2\",",
|
||||
"AUDIO=\"audio\"",
|
||||
"\n",
|
||||
"lo/prog_index.m3u8\n",
|
||||
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"BANDWIDTH=591680,",
|
||||
"CODECS=\"avc1.42e01e,mp4a.40.2\",",
|
||||
"AUDIO=\"audio\"",
|
||||
"\n",
|
||||
"hi/prog_index.m3u8\n"
|
||||
)
|
||||
}
|
||||
}
|
318
tests/media_playlist.rs
Normal file
318
tests/media_playlist.rs
Normal file
|
@ -0,0 +1,318 @@
|
|||
//! 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::convert::TryFrom;
|
||||
use std::time::Duration;
|
||||
|
||||
use hls_m3u8::tags::{ExtInf, ExtXByteRange};
|
||||
use hls_m3u8::types::PlaylistType;
|
||||
use hls_m3u8::{MediaPlaylist, MediaSegment};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! generate_tests {
|
||||
( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $fnname() {
|
||||
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||
|
||||
assert_eq!($struct.to_string(), $str.to_string());
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
generate_tests! {
|
||||
test_media_playlist_with_byterange => {
|
||||
MediaPlaylist::builder()
|
||||
.media_sequence(1)
|
||||
.target_duration(Duration::from_secs(10))
|
||||
.segments(vec![
|
||||
MediaSegment::builder()
|
||||
.duration(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||
.byte_range(ExtXByteRange::from(0..75232))
|
||||
.uri("video.ts")
|
||||
.build()
|
||||
.unwrap(),
|
||||
MediaSegment::builder()
|
||||
.duration(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||
.byte_range(ExtXByteRange::from(752321..82112 + 752321))
|
||||
.uri("video.ts")
|
||||
.build()
|
||||
.unwrap(),
|
||||
MediaSegment::builder()
|
||||
.duration(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||
// 834433..904297
|
||||
.byte_range(ExtXByteRange::from(..69864))
|
||||
.uri("video.ts")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
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
tests/mod.rs
Normal file
1
tests/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
mod issues;
|
630
tests/rfc8216.rs
Normal file
630
tests/rfc8216.rs
Normal file
|
@ -0,0 +1,630 @@
|
|||
// https://tools.ietf.org/html/rfc8216#section-8
|
||||
use std::convert::TryFrom;
|
||||
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, TryFrom::try_from($str).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