1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-05-18 00:12:54 +00:00

Compare commits

...

248 commits

Author SHA1 Message Date
Lucas f5ddfed738
Merge pull request #72 from Luro02/ci
Remove miri from ci
2021-10-01 13:56:54 +02:00
Lucas 1c31d3835f remove miri from ci 2021-10-01 13:55:36 +02:00
Lucas aa4728aaed
Merge pull request #71 from Luro02/ci
fix ci by running rustfmt on nightly
2021-10-01 13:40:05 +02:00
Lucas ddb618f0d9 fix ci by running rustfmt on nightly 2021-10-01 13:39:17 +02:00
Lucas 6c3438a68f
Merge pull request #69 from Luro02/issue64
fix issue 64
2021-10-01 13:37:19 +02:00
Lucas 9ec0687f5e
Merge pull request #68 from Luro02/clippy
fix clippy lints
2021-10-01 13:36:57 +02:00
Lucas 4ba3fcf352
Merge pull request #70 from Luro02/ci
update ci to test code
2021-10-01 13:36:39 +02:00
Lucas f7eaeea281 update ci to test code 2021-10-01 13:35:43 +02:00
Lucas 3a742a95b6 fix issue #64 2021-10-01 13:32:36 +02:00
Lucas 6c694d186d fix broken test 2021-10-01 13:32:19 +02:00
Lucas c2c94f9352 fix clippy lints 2021-10-01 13:04:57 +02:00
Takeru Ohta 8bf28b75fa
Merge pull request #67 from sile/bump-to-v0.4.1
Bump version to v0.4.1
2021-08-03 07:13:10 +09:00
Takeru Ohta 5c2852c3ce Bump version to v0.4.1 2021-08-03 07:11:16 +09:00
Lucas fc067c3293
merge pull request #66 from Luro02/master
fix compiler errors
2021-08-02 20:17:26 +02:00
Lucas 153c6e3e33 fix clippy error by implementing PartialOrd manually for UFloat and Float 2021-08-02 20:15:45 +02:00
Lucas 8ad21ec161 fix compiler errors 2021-08-02 20:05:11 +02:00
Takeru Ohta 23c799a88b
Merge pull request #63 from sile/fix-build-error-on-rustc-1.45.2
Fix build errors on rustc-1.45.2
2020-08-15 20:22:04 +09:00
Takeru Ohta eff1e6783d Fix build errors on rustc-1.45.2 2020-08-15 17:19:27 +09:00
Takeru Ohta 42b5eb531b
Merge pull request #62 from sile/bump-to-v0.3.1
Bump version to v0.4.0
2020-08-15 17:07:42 +09:00
Takeru Ohta 09227f124b Change the new version to v0.4.0 2020-08-15 16:01:19 +09:00
Takeru Ohta af92a94873 Bump version to v0.3.1 2020-08-15 13:08:21 +09:00
Lucas 68453ea54d
Merge pull request #61 from Luro02/master
Fix a lot of clippy lints
2020-08-11 11:14:13 +02:00
Luro02 e6588ab963
Fix a lot of clippy lints 2020-08-11 11:13:14 +02:00
Lucas c343858860
Merge pull request #60 from Luro02/master
Fix issue #59 related to parsing the #EXT-X-DISCONTINUITY-SEQUENCE tag
2020-08-11 10:38:33 +02:00
Luro02 f98f12fa82
Fix #59 2020-08-11 10:36:44 +02:00
Luro02 e41105afdd
Fixed broken test 2020-08-11 09:45:21 +02:00
Lucas 7907f9b1f1
Merge pull request #57 from sile/0.3.1
Update master
2020-04-25 09:55:06 +02:00
Lucas 85df5c94ad
Merge branch 'master' into 0.3.1 2020-04-25 09:54:01 +02:00
Luro02 fe54e0b456 Fix #55 2020-04-25 09:50:31 +02:00
Luro02 c138f70738 Add issues directory 2020-04-25 09:50:31 +02:00
Luro02 fa3bb30c5c Add changelog.md 2020-04-22 10:34:23 +02:00
Luro02 c4643c7083 Use Cow<'a, str> to reduce clones #52 2020-04-22 10:34:23 +02:00
Luro02 2d6a49662d Add performance feature 2020-04-22 10:34:23 +02:00
Luro02 1a4813a1d1 Minor performance improvements in AttributePairs parsing 2020-04-18 15:09:56 +02:00
Luro02 ce04223ec4 Improve Benchmarks #51 2020-04-18 15:09:56 +02:00
Luro02 096957b167 Add stable-vec #52 2020-04-18 15:09:56 +02:00
Lucas 7e6da6224d
Merge pull request #51 from dholroyd/parser-benchmark
Add a parsing benchmark
2020-04-15 07:23:46 +02:00
David Holroyd 8c4ff3e399 Add a parsing benchmark 2020-04-14 22:54:11 +01:00
Takeru Ohta 8a1cfd86d0 Fix category name. 2020-04-10 09:13:44 +09:00
Takeru Ohta 413d5263a3 Add Luro02 to the author list. 2020-04-10 09:11:58 +09:00
Luro02 6d4e6051c4
fix license badge in readme.md 2020-04-09 14:55:21 +02:00
Luro02 41d917e6f1
bump to version 0.3.0 2020-04-09 14:37:31 +02:00
Lucas 1057c905bf
Merge pull request #44 from Luro02/master 2020-04-09 14:33:15 +02:00
Luro02 a085971c42
add cargo audit to github actions 2020-04-09 14:18:53 +02:00
npajkovsky 34df16f7d6
fix EXT-X-MEDIA DEFAULT=yes wrt AUTOSELECT (#1)
* fix EXT-X-MEDIA DEFAULT=yes wrt AUTOSELECT

In 4.3.4.1. EXT-X-MEDIA section about AUTOSELECT is written

   If the AUTOSELECT attribute is present, its value MUST be YES if
   the value of the DEFAULT attribute is YES.

That means, that if DEFAULT is YES and AUTOSELECT is *not present*, it
ok.

Before the patch, incorrect error is emitted

  If `DEFAULT` is true, `AUTOSELECT` has to be true too, Default:
  Some(true), Autoselect: None!

Signed-off-by: Nikola Pajkovsky <nikola.pajkovsky@livesporttv.cz>

* update src/tags/master_playlist/media.rs

Co-authored-by: Nikola Pajkovsky <nikola.pajkovsky@livesporttv.cz>
Co-authored-by: Lucas <24826124+Luro02@users.noreply.github.com>
2020-04-09 14:12:16 +02:00
Luro02 fdc3442bb6
minor improvements to documentation 2020-04-09 10:50:41 +02:00
Luro02 ec0b5cdb21
improve rustfmt.toml 2020-04-09 09:29:29 +02:00
Luro02 7a918d31bd
document crate features 2020-04-09 09:29:29 +02:00
Luro02 f0d91c5e7c
fix rust_2018_idioms 2020-04-09 09:29:29 +02:00
Luro02 f90ea7a121
slightly improve PlaylistType 2020-04-09 09:29:26 +02:00
Luro02 25a01261f5
add features section in Cargo.toml 2020-04-09 09:29:23 +02:00
Luro02 3492c529c5
add version-sync 2020-04-09 09:29:20 +02:00
Luro02 41f81aebb3
add html_root_url 2020-04-09 09:29:17 +02:00
Lucas c6b1732c26
Merge pull request #2 from dholroyd/optional-start-date
Make START-DATE optional.
2020-04-08 15:14:42 +02:00
David Holroyd e07fb9262d Make START-DATE optional.
Although this goes against the wording of RFC8216, the spec includes
examples that omit this, and implementations also omit START-DATE when
for example signalling an 'explicit-IN' SCTE marker.
2020-04-07 21:20:52 +01:00
Luro02 fb4f6a451e
include backtrace feature in ci tests 2020-03-29 13:01:30 +02:00
Luro02 969e5bae9a
minor code improvements 2020-03-29 12:58:43 +02:00
Luro02 9eccea8a7f
automatically infer start of ByteRange 2020-03-29 12:58:32 +02:00
Luro02 47eccfdef9
improve panic messages for ByteRange 2020-03-29 12:57:43 +02:00
Luro02 8ece080cda
remove examples directory 2020-03-29 12:01:28 +02:00
Luro02 79c4f5e934
Merge remote-tracking branch 'upstream/master' 2020-03-29 11:57:16 +02:00
Luro02 3710a9c5c2
change github actions 2020-03-29 11:33:16 +02:00
Luro02 8b3517326b
fix badge in readme 2020-03-29 11:32:59 +02:00
Luro02 e174fcac9a
remove redundant deny.toml 2020-03-29 11:02:30 +02:00
Luro02 899aea7fc1
finish documentation 2020-03-28 10:51:19 +01:00
Luro02 9a2cacf024
fix some clippy lints 2020-03-28 10:48:17 +01:00
Luro02 8eb45dceb7
insert iv based on segmentnumber 2020-03-28 10:47:52 +01:00
Luro02 e187c9dc7c
improve readability 2020-03-28 10:46:07 +01:00
Luro02 20072c2695
implement missing traits 2020-03-25 16:13:40 +01:00
Luro02 6cd9fe7064
fix broken documentation 2020-03-25 15:57:43 +01:00
Luro02 24c5ad8199
fix github actions syntax 2020-03-25 14:56:46 +01:00
Luro02 cc48478b05
some minor fixes 2020-03-25 14:11:11 +01:00
Luro02 7a63c2dcf2
add builder module 2020-03-25 14:10:59 +01:00
Luro02 c268fa3a82
rewrite MediaPlaylist 2020-03-25 14:10:27 +01:00
Luro02 72c0ff9c75
rename MediaSegment::inf to MediaSegment::duration 2020-03-25 13:58:21 +01:00
Luro02 429f3f8c3d
internalize ExtXTargetDuration 2020-03-25 13:37:47 +01:00
Luro02 f48876ee07
internalize ExtXDiscontinuitySequence 2020-03-25 13:21:11 +01:00
Luro02 99b6b23acc
internalize ExtMediaSequence 2020-03-25 13:08:26 +01:00
Luro02 c56a56abe8
internalize ExtXIFramesOnly 2020-03-25 12:49:53 +01:00
Luro02 285d2eccb8
internalize ExtXEndList 2020-03-25 12:41:36 +01:00
Luro02 112c3998b8
improve ExtXDiscontinuitySequence 2020-03-25 12:27:36 +01:00
Luro02 ca302ef543
improve MediaSegment 2020-03-25 12:18:34 +01:00
Luro02 42e1afaa47
change ExtXPlaylistType to PlaylistType 2020-03-25 12:17:03 +01:00
Luro02 15cc360a2c
implement missing traits 2020-03-25 12:03:19 +01:00
Luro02 ca3ba476c3
improve ExtXProgramDateTime 2020-03-25 11:56:43 +01:00
Luro02 fc1136265c
improve DecryptionKey 2020-03-25 11:49:16 +01:00
Luro02 7c26d2f7f1
slightly improve ExtInf 2020-03-25 11:41:24 +01:00
Luro02 870a39cddd
internalize ExtXDiscontinuity 2020-03-25 11:32:48 +01:00
Luro02 d1fdb7fec1
improve ExtXDateRange 2020-03-25 11:21:20 +01:00
Luro02 b8fd4c15d5
rewrite KeyFormatVersions 2020-03-25 11:08:32 +01:00
Luro02 7025114e36
rewrite keys (ExtXKey, ExtXSessionKey) and Encrypted trait 2020-03-23 13:34:26 +01:00
Luro02 02d363daa1
slight changes to tests 2020-03-23 12:00:02 +01:00
Luro02 b2fb58559c
make chrono optional #49 2020-03-20 12:05:16 +01:00
Luro02 1b01675250
improve ExtXMap 2020-03-17 16:13:38 +01:00
Luro02 ff807940b2
add more tests 2020-03-17 15:58:59 +01:00
Luro02 a797e401ed
improve MasterPlaylist 2020-03-17 15:58:43 +01:00
Luro02 025add6dc3
improve VariantStream 2020-03-17 15:54:53 +01:00
Luro02 78edff9341
improve ExtXSessionKey 2020-03-17 15:48:02 +01:00
Luro02 4e41585cbd
improve ExtXMedia 2020-03-17 15:39:07 +01:00
Luro02 187174042d
use chars instead of bytes in the attribute parser 2020-03-16 11:17:52 +01:00
Luro02 e338f5f95f
finer grained clippy lints 2020-03-08 10:00:39 +01:00
Luro02 afd9e0437c
change name of github action 2020-02-24 16:46:57 +01:00
Luro02 a262c77c58
improvements to Value 2020-02-24 16:45:32 +01:00
Luro02 6333a80507
minor improvements 2020-02-24 16:45:10 +01:00
Luro02 6ef8182f2c
improvments to ExtXStart 2020-02-24 16:44:02 +01:00
Luro02 9273e6c16c
add must_use attributes 2020-02-24 16:30:43 +01:00
Luro02 f7d81a55c9
improve documentation of ExtXIndependentSegments 2020-02-24 16:16:40 +01:00
Luro02 dc12db9fad
improve KeyFormatVersions 2020-02-24 14:49:20 +01:00
Luro02 90783fdd9d
improve documentation of InstreamId 2020-02-24 14:32:28 +01:00
Luro02 11ac527fca
improve documentation of EncryptionMethod 2020-02-24 14:28:14 +01:00
Luro02 c7419c864f
improve StreamData 2020-02-24 14:09:26 +01:00
Luro02 0be0c7ddfb
improve documentation and tests for Resolution 2020-02-24 13:00:20 +01:00
Luro02 cdb6367dbd
improve documentation for ProtocolVersion 2020-02-24 12:41:30 +01:00
Luro02 49c5b5334c
improve documentation and tests of HdcpLevel 2020-02-24 12:36:04 +01:00
Luro02 dae826b4e5
improve (U)Float 2020-02-24 12:19:37 +01:00
Luro02 88a5fa4460
improve Channels 2020-02-23 23:19:54 +01:00
Luro02 03b0d2cf0c
remove cargo deny action 2020-02-23 19:00:03 +01:00
Luro02 e1c10d27f7
improve code coverage of (U)Float 2020-02-23 18:57:13 +01:00
Luro02 5972216323
improve documentation and tests of ByteRange 2020-02-23 18:56:41 +01:00
Luro02 651db2e18b
fix ci 2020-02-23 18:53:47 +01:00
Luro02 a8c788f4d2
improve documentation and tests of ExtXSessionData 2020-02-21 22:06:09 +01:00
Luro02 8948f9914b
improve documentation of ExtXVersion 2020-02-21 21:11:51 +01:00
Luro02 5304947885
various minor improvements 2020-02-21 20:45:23 +01:00
Luro02 f404e68d1c
add VariantStream::is_associated 2020-02-21 20:45:18 +01:00
Luro02 070a62f9ad
improvements to ClosedCaptions 2020-02-21 20:42:44 +01:00
Luro02 30e8009af1
fix Float and UFloat 2020-02-21 20:42:14 +01:00
Luro02 86bb573c97
various improvements to InStreamId 2020-02-21 20:41:31 +01:00
Luro02 c39d104137
remove DecryptionKey 2020-02-21 10:45:04 +01:00
Luro02 a96367e3fa
improve tests #25 2020-02-16 17:14:28 +01:00
Luro02 d3c238df92
impl Ord, Eq and Hash for (U)Float 2020-02-16 17:09:40 +01:00
Luro02 b54b17df73
fix key parsing and printing 2020-02-16 12:51:49 +01:00
Luro02 b2c997d04d
remove _tag suffix from MediaSegment fields 2020-02-16 12:50:52 +01:00
Luro02 8cced1ac53
some improvements 2020-02-14 13:05:18 +01:00
Luro02 25f9691c75
improve error 2020-02-14 13:01:42 +01:00
Luro02 94d85d922f
fix github actions 2020-02-10 13:54:51 +01:00
Luro02 9b61f74b9d
fix documentation 2020-02-10 13:51:37 +01:00
Luro02 3a388e3985
improvements to code 2020-02-10 13:21:48 +01:00
Luro02 e6f5091f1b
implement VariantStream 2020-02-10 13:20:39 +01:00
Luro02 90ff18e2b3
implement Float and UFloat 2020-02-10 13:13:41 +01:00
Luro02 101878a083
improvements to error 2020-02-10 12:47:01 +01:00
Luro02 9cc162ece7
format Cargo.toml 2020-02-10 12:46:22 +01:00
Luro02 acbe7e73da
disable examples 2020-02-10 12:45:58 +01:00
Luro02 ec07e6b64c
improve travis 2020-02-08 10:45:48 +01:00
Luro02 4e298f76ef
use cargo deny 2020-02-08 10:45:26 +01:00
Luro02 66c0b8dd0c
improve Iterator types 2020-02-06 17:02:44 +01:00
Luro02 5de47561b1
some minor improvements 2020-02-06 12:28:54 +01:00
Luro02 1b0eb56224
remove unnecessary allocations 2020-02-06 12:27:48 +01:00
Luro02 aae3809545
remove Copy trait from Lines
Copy should not be implemented for types that implement Iterator, 
because this would be confusing.

https://rust-lang.github.io/rust-clippy/master/#copy_iterator
2020-02-06 12:24:40 +01:00
Luro02 2471737455
update dependencies 2020-02-02 15:23:47 +01:00
Luro02 e6a1103d24
rewrite Lines to reduce allocations 2020-02-02 14:33:57 +01:00
Luro02 006f36ff47
collect unsupported tags #36
closes #36
2020-02-02 13:50:56 +01:00
Luro02 27d94faec4
use shorthand #24 2020-02-02 13:38:11 +01:00
Luro02 048f09bd14
minor improvements 2020-01-26 13:12:19 +01:00
Luro02 a777f74cfa
refactor attribute parsing to comply with #26 2020-01-26 13:11:57 +01:00
Luro02 e156f6e3fd
improve ExtXMedia 2020-01-25 12:26:20 +01:00
Lucas ae43982d0b
Merge pull request #41 from unipro/master
fix rfc8216_8-7.m3u8 parsing bug
2020-01-23 19:29:47 +01:00
Lucas 1876adbaf8
Merge branch 'master' into master 2020-01-23 19:28:30 +01:00
Luro02 ac80ac5c9d
switch error implementation #23
closes #23
2020-01-23 19:13:26 +01:00
Luro02 448c331447
fix compilation 2020-01-23 17:57:57 +01:00
Lucas aa9dbc7b71
Merge pull request #39 from Luro02/master
Lots of changes.
2020-01-23 07:05:01 +01:00
Ian Jun 91ca70687b fix rfc8216_8-7.m3u8 parsing bug 2019-11-28 06:30:12 +00:00
Luro02 73d9eb4f79 minor changes 2019-10-12 11:38:28 +02:00
Luro02 c53e9e33f1 added pretty_assertions
This will allow for better troubleshooting of failing test, because you 
don't have to search for the difference (between left and right). This 
is especially helpful for larger assertions.
2019-10-08 15:42:33 +02:00
Luro02 e75153ec5e fix backwards compatibility 2019-10-06 17:30:24 +02:00
Luro02 c8020ede8e update dependencies 2019-10-06 17:30:05 +02:00
Luro02 b1c1ea8bdc minor changes + more tests #25 2019-10-06 16:39:18 +02:00
Luro02 3dad1277ca implemented ExtXDateRange 2019-10-06 16:37:14 +02:00
Luro02 b18e6ea4fb use required_version! macro 2019-10-05 16:24:48 +02:00
Luro02 32876e1371 fix some clippy lints 2019-10-05 16:08:03 +02:00
Luro02 4ffd4350f8 improve documentation #31 2019-10-05 14:45:40 +02:00
Luro02 8d1ed6372b removed Eq implementation
f64 and f32 don't implement Eq for a reason!
For example this comparison `1.0 + 2.0 == 3.0` is false for floats, even 
though it should be true!
2019-10-05 13:23:41 +02:00
Luro02 99493446eb Infallible errors
https://doc.rust-lang.org/std/convert/enum.Infallible.html
2019-10-05 13:15:42 +02:00
Luro02 f76b223482 made master playlist smarter 2019-10-05 12:49:08 +02:00
Luro02 5b44262dc8 minor changes 2019-10-05 09:44:23 +02:00
Luro02 f96207c93e remove draft content 2019-10-04 11:19:03 +02:00
Luro02 4b4cffc248 fix #20 2019-10-04 11:02:21 +02:00
Lucas ebddb7a0e2
Merge pull request #38 from Luro02/backup
more tests #25 + better docs #31
2019-10-03 18:06:43 +02:00
Luro02 d2d2782cb0 switch rustfmt to nightly 2019-10-03 18:04:10 +02:00
Luro02 5eca073a8c added rustfmt.toml 2019-10-03 18:04:10 +02:00
Luro02 06a30d7704 added cargo-audit to travis 2019-10-03 18:04:10 +02:00
Luro02 93283f61f1 more tests #25 + better docs #31 2019-10-03 18:04:10 +02:00
Luro02 6b717f97c2 more tests #25 + better docs #31 2019-10-03 18:01:53 +02:00
Lucas b197d5fbd7
Merge pull request #34 from Luro02/master
Documentation Improvements + more tests
2019-09-22 18:02:42 +02:00
Luro02 0c4fa008e6 more documentation #31 + tests #25 2019-09-22 18:00:38 +02:00
Luro02 3240417304 replaced builder with derive_builder #14 2019-09-22 12:56:28 +02:00
Luro02 81f9a421fe added RequiredVersion trait 2019-09-22 10:57:28 +02:00
Lucas ed64ed15d3
Merge pull request #33 from Luro02/master
Some changes
2019-09-21 15:23:39 +02:00
Luro02 b2c9f2db36 Merge branch 'master' of https://github.com/Luro02/hls_m3u8 2019-09-21 15:21:10 +02:00
Luro02 d240ac5c5e remove code duplication 2019-09-21 15:20:19 +02:00
Luro02 cdab47ad35 minor changes 2019-09-21 13:24:05 +02:00
Lucas d04f9c2dc7
Merge pull request #32 from Luro02/master
Update rust.yml
2019-09-21 13:11:23 +02:00
Lucas 60ae8c5e60
Update rust.yml 2019-09-21 13:10:55 +02:00
Lucas 56f8d10f38
Merge pull request #30 from Luro02/master
remove getset and url
2019-09-21 12:32:49 +02:00
Luro02 71361ff328 remove url #21 2019-09-21 12:11:36 +02:00
Luro02 ea75128aee remove getset #19 2019-09-21 11:53:34 +02:00
Luro02 0900d7e56b improved session_data docs + tests 2019-09-21 11:04:45 +02:00
Luro02 720dc32474 fixed test 2019-09-21 08:47:52 +02:00
Lucas fb44c8803d
Merge pull request #17 from Luro02/master
Lots of changes
2019-09-17 18:50:38 +02:00
Luro02 612c3d15be minor protocol_version fixes 2019-09-17 15:40:10 +02:00
Luro02 e55113e752 readded decryption_key 2019-09-17 14:45:10 +02:00
Luro02 5486c5e830 cleanup imports 2019-09-15 19:09:48 +02:00
Luro02 b932cef71a internalize signed_decimal_floating_point #9 2019-09-15 19:01:56 +02:00
Luro02 42469275d3 internalize decimal_floating_point #9 2019-09-15 18:54:25 +02:00
Luro02 fd66f8b4ef remove decryption_key #9 2019-09-15 16:47:35 +02:00
Luro02 1d614d580a updated ExtXKey + ExtXSessionKey #9 2019-09-15 16:45:43 +02:00
Luro02 db6961d19f parse dates with chrono #9 2019-09-15 12:51:51 +02:00
Luro02 fa96a76ca9 parse Urls #9 2019-09-15 11:25:41 +02:00
Luro02 c28d6963a6 removed SingleLineString #9 2019-09-15 11:05:22 +02:00
Luro02 6ffbe50322 internalized DecimalResolution #9 2019-09-15 10:51:04 +02:00
Luro02 3acf67df6a added more tests 2019-09-15 10:40:45 +02:00
Luro02 b954ae1134 remove all occurences of ref 2019-09-14 21:42:06 +02:00
Luro02 51b66d2adf added media_segment builder 2019-09-14 21:21:44 +02:00
Luro02 dd1a40abc9 added media_playlist builder 2019-09-14 21:08:35 +02:00
Luro02 b1aa512679 added master_playlist builder 2019-09-14 13:26:16 +02:00
Luro02 a2614b5aca added test 2019-09-14 12:34:34 +02:00
Luro02 7483f49fe9 fix bug 2019-09-14 12:29:54 +02:00
Luro02 273c0990dc Rewrote Lines 2019-09-14 11:57:56 +02:00
Luro02 3721106795 Rewrote AttributePairs 2019-09-14 11:31:16 +02:00
Luro02 c8f3df1228 New Error type 2019-09-13 16:06:52 +02:00
Luro02 1a35463185 updated parser 2019-09-10 11:05:20 +02:00
Takeru Ohta de8f92508d Bump version to v0.2.1 2019-09-09 20:16:30 +09:00
Takeru Ohta f51ba2bb1c Update README.md 2019-09-09 20:16:13 +09:00
Takeru Ohta 230128bb8e
Merge pull request #16 from sile/add-apache-lisence
Add Apache 2.0 license
2019-09-09 20:14:36 +09:00
Takeru Ohta b2f836a445 Add Apache 2.0 license 2019-09-09 20:05:27 +09:00
Luro02 91c6698f16 added more tests 2019-09-08 12:49:22 +02:00
Luro02 cf97a45f60 fixed clippy warnings 2019-09-08 12:23:33 +02:00
Luro02 1966a7608d removed QuotedString 2019-09-08 11:30:52 +02:00
Takeru Ohta 861e7e4b74
Merge pull request #12 from Luro02/declutter
Added some tests and moved types + tags into separate folders
2019-09-08 11:12:39 +09:00
Luro02 fe032ee984 added tests 2019-09-06 13:46:21 +02:00
Luro02 cb27640867 rustfmt code 2019-09-06 13:21:05 +02:00
Luro02 5da2fa8104 move types into their own files 2019-09-06 13:20:40 +02:00
Luro02 3ecbbd9acb move tags into their own modules 2019-09-06 12:55:00 +02:00
Takeru Ohta 4324cb79d0 Bump version to v0.2.0 2019-08-16 06:56:13 +09:00
Takeru Ohta 5a231199d6
Merge pull request #7 from Luro02/master
fixed spelling mistake + compiler warnings
2019-08-16 06:54:52 +09:00
Luro02 211ce6e79a fixed spelling mistake + compiler warnings 2019-08-15 18:33:01 +02:00
Takeru Ohta 8034d543b8 Bump version to v0.1.4 2019-06-15 18:52:16 +09:00
Takeru Ohta a304ac0a2f
Merge pull request #5 from unipro/master
Fix EXTINF and URI displaying bug
2019-06-15 18:51:09 +09:00
Ian Jun 788138903f Fix EXTINF and URI displaying bug 2019-05-20 07:01:01 +00:00
Takeru Ohta 7767f47f21 Support up-to-date clippy 2019-03-31 19:00:02 +09:00
Takeru Ohta 02d7c80b2b Switch to 2018-edition 2019-03-31 18:58:11 +09:00
Takeru Ohta 625d037b27 Apply rustfmt-1.0.0 2019-03-31 18:54:21 +09:00
Takeru Ohta ac57417cc7 Update README.md 2018-10-11 00:41:20 +09:00
Takeru Ohta c66fcd9178 Bump version to v0.1.3 2018-10-11 00:36:55 +09:00
Takeru Ohta 3122949384 Fix the unexpected panic reported by #2 2018-10-11 00:35:24 +09:00
Takeru Ohta 807b73a701 Bump version to v0.1.2 2018-10-04 23:34:34 +09:00
Takeru Ohta ab82edf119 Add MediaPlaylistOptions 2018-10-04 23:31:15 +09:00
Takeru Ohta 24a6ff9851 Apply clippy-0.0.212 2018-10-04 22:10:53 +09:00
Takeru Ohta 8585016720 Apply rustfmt-0.99.1 2018-10-04 20:18:56 +09:00
93 changed files with 13989 additions and 4313 deletions

14
.github/workflows/audit.yml vendored Normal file
View 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
View 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
View file

@ -2,3 +2,4 @@
/target/
**/*.rs.bk
Cargo.lock
tarpaulin-report.html

View file

@ -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
View 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

View file

@ -1,20 +1,44 @@
[package]
name = "hls_m3u8"
version = "0.1.1"
authors = ["Takeru Ohta <phjgt308@gmail.com>"]
version = "0.4.1" # 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.9"
hex = "0.4"
thiserror = "1.0"
derive_more = "0.99"
shorthand = "0.1"
strum = { version = "0.17", features = ["derive"] }
stable-vec = { version = "0.4" }
[dev-dependencies]
clap = "2"
pretty_assertions = "0.6"
version-sync = "0.9"
automod = "0.2"
criterion = "0.3.1"
[[bench]]
name = "bench_main"
harness = false

202
LICENSE-APACHE Normal file
View 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.

View file

@ -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
View file

@ -0,0 +1,7 @@
use criterion::criterion_main;
mod benchmarks;
criterion_main! {
benchmarks::media_playlist::benches,
}

View 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);

View file

@ -0,0 +1 @@
pub mod media_playlist;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View 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

View file

@ -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);
}
}

View file

@ -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 {}

View file

@ -1,44 +1,145 @@
#![doc(html_root_url = "https://docs.rs/hls_m3u8/0.4.1")]
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
#![warn(
clippy::pedantic, //
clippy::nursery,
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::expect_used,
clippy::unneeded_field_pattern,
clippy::wrong_pub_self_convention
)]
// those should not be present in production code:
#![deny(
clippy::print_stdout,
clippy::todo,
clippy::unimplemented,
clippy::dbg_macro,
clippy::use_debug
)]
#![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::*;

View file

@ -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(fmt = "{}")]
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

View file

@ -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::<(), String>(())
/// ```
#[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()
);
}
}

View file

@ -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
View 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
View file

@ -0,0 +1,5 @@
pub(crate) mod m3u;
pub(crate) mod version;
pub(crate) use m3u::*;
pub use version::*;

115
src/tags/basic/version.rs Normal file
View file

@ -0,0 +1,115 @@
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)]
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 Default for ExtXVersion {
fn default() -> Self { Self(ProtocolVersion::default()) }
}
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)
);
}
}

View file

@ -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()
}
}

View file

@ -0,0 +1,811 @@
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::<(), String>(())
/// ```
#[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_eq!(ExtXMedia::try_from("").is_err(), true);
assert_eq!(ExtXMedia::try_from("garbage").is_err(), true);
assert_eq!(
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\"")
.is_err(),
true
);
assert_eq!(
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1").is_err(),
true
);
assert_eq!(
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO").is_err(),
true
);
assert_eq!(
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES").is_err(),
true
);
}
#[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
);
}
}

View 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::*;

View 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::<(), String>(())
/// ```
#[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
);
}
}

View 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.to_string())
}
}
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
);
}
}

View 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(),
));
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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("")))
);
}
}

View 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);
}
}

View 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)
}
}

View 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()
);
}
}

View 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::*;

View 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()
);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,256 @@
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 }
}
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
);
}
}

View 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::<(), String>(())
```
"#
)]
#[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::<(), String>(())
```
"#
)]
#[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
);
}
}

View 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)
}
}

View 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(|v| Cow::Borrowed(v));
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))
);
}
}

View 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::<(), String>(())
/// ```
#[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
);
}
}

View 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());
}
}

View 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::*;

View 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),
);
}
}

View file

@ -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::*;

View 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
View 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
View 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
View 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);
}
}

View file

@ -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());
}
}

688
src/types/byte_range.rs Normal file
View file

@ -0,0 +1,688 @@
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_value());
///
/// // this would cause the end to overflow
/// let nrange = range.saturating_add(1);
///
/// // but the range remains unchanged
/// assert_eq!(range, nrange);
/// ```
///
/// # Note
///
/// The length of the range will remain unchanged,
/// if the `start` is `Some`.
#[must_use]
pub fn saturating_add(mut self, num: usize) -> Self {
if let Some(start) = self.start {
// add the number to the start
if let (Some(start), Some(end)) = (start.checked_add(num), self.end.checked_add(num)) {
self.start = Some(start);
self.end = end;
} else {
// it is ensured at construction that the start will never be larger than the
// end. This clause can therefore be only reached if the end overflowed.
// -> It is only possible to add `usize::max_value() - end` to the start.
if let Some(start) = start.checked_add(usize::max_value() - self.end) {
self.start = Some(start);
self.end = usize::max_value();
} else {
// both end + start overflowed -> do not change anything
}
}
} else {
self.end = self.end.saturating_add(num);
}
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];
#[must_use]
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_value());
range += 5;
unreachable!();
}
#[test]
fn test_sub_assign() {
let mut range = ByteRange::from(10..20);
range -= 5;
assert_eq!(range, ByteRange::from(5..15));
}
#[test]
#[should_panic = "attempt to subtract with overflow"]
fn test_sub_assign_panic() {
let mut range = ByteRange::from(4..10);
range -= 5;
unreachable!();
}
#[test]
#[should_panic = "attempt to make the start (11) larger than the end (10)"]
fn test_set_start() { let _ = ByteRange::from(4..10).set_start(Some(11)); }
#[test]
#[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_value()..usize::max_value()) + 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_value()..usize::max_value()).saturating_add(1),
ByteRange::from(usize::max_value()..usize::max_value())
);
assert_eq!(
ByteRange::from(..usize::max_value()).saturating_add(1),
ByteRange::from(..usize::max_value())
);
assert_eq!(
ByteRange::from(usize::max_value() - 5..usize::max_value()).saturating_add(1),
ByteRange::from(usize::max_value() - 5..usize::max_value())
);
// overflow, but something can be added to the range:
assert_eq!(
ByteRange::from(usize::max_value() - 5..usize::max_value() - 3).saturating_add(4),
ByteRange::from(usize::max_value() - 2..usize::max_value())
);
assert_eq!(
ByteRange::from(..usize::max_value() - 3).saturating_add(4),
ByteRange::from(..usize::max_value())
);
}
#[test]
fn test_saturating_sub() {
// normal subtraction
assert_eq!(
ByteRange::from(5..10).saturating_sub(4),
ByteRange::from(1..6)
);
// subtracting 0
assert_eq!(
ByteRange::from(0..0).saturating_sub(0),
ByteRange::from(0..0)
);
assert_eq!(
ByteRange::from(2..3).saturating_sub(0),
ByteRange::from(2..3)
);
// the start underflows
assert_eq!(
ByteRange::from(0..5).saturating_sub(4),
ByteRange::from(0..5)
);
// the start underflows, but one can still subtract something from it
assert_eq!(
ByteRange::from(1..5).saturating_sub(2),
ByteRange::from(0..4)
);
// both start and end underflow
assert_eq!(
ByteRange::from(1..3).saturating_sub(5),
ByteRange::from(0..2)
);
// both start + end are 0 + underflow
assert_eq!(
ByteRange::from(0..0).saturating_sub(1),
ByteRange::from(0..0)
);
// half open ranges:
assert_eq!(ByteRange::from(..6).saturating_sub(2), ByteRange::from(..4));
assert_eq!(ByteRange::from(..5).saturating_sub(0), ByteRange::from(..5));
assert_eq!(ByteRange::from(..0).saturating_sub(0), ByteRange::from(..0));
assert_eq!(ByteRange::from(..0).saturating_sub(1), ByteRange::from(..0));
}
#[test]
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
View 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());
}
}

View 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
View 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
View 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::<(), String>(())
/// ```
#[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
);
}
}

View 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());
}
}

311
src/types/float.rs Normal file
View file

@ -0,0 +1,311 @@
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 youre using but instead of the way Hash happens
/// to be implemented for the type youre 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]
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).eq(&Float::new(1.0)), true);
assert_eq!(Float::new(1.0).eq(&Float::new(33.3)), false);
assert_eq!(Float::new(1.1), 1.1);
}
#[test]
fn test_display() {
assert_eq!(Float::new(22.0).to_string(), "22".to_string());
assert_eq!(Float::new(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(::core::f32::INFINITY); }
#[test]
#[should_panic = "float must be finite: `-inf`"]
fn test_new_neg_infinite() { let _ = Float::new(::core::f32::NEG_INFINITY); }
#[test]
#[should_panic = "float must not be `NaN`"]
fn test_new_nan() { let _ = Float::new(::core::f32::NAN); }
#[test]
fn test_as_f32() {
assert_eq!(Float::new(1.1).as_f32(), 1.1_f32);
}
#[test]
fn test_from() {
assert_eq!(Float::from(-1_i8), Float::new(-1.0));
assert_eq!(Float::from(1_u8), Float::new(1.0));
assert_eq!(Float::from(-1_i16), Float::new(-1.0));
assert_eq!(Float::from(1_u16), Float::new(1.0));
}
#[test]
fn test_try_from() {
assert_eq!(Float::try_from(1.1_f32).unwrap(), Float::new(1.1));
assert_eq!(Float::try_from(-1.1_f32).unwrap(), Float::new(-1.1));
assert!(Float::try_from(::core::f32::INFINITY).is_err());
assert!(Float::try_from(::core::f32::NAN).is_err());
assert!(Float::try_from(::core::f32::NEG_INFINITY).is_err());
}
}

41
src/types/hdcp_level.rs Normal file
View 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
View 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
];
}

View 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
View 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());
}
}

View file

@ -0,0 +1,684 @@
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]
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_eq!(KeyFormatVersions::new().is_default(), true);
assert_eq!(KeyFormatVersions::default().is_default(), true);
assert_eq!(KeyFormatVersions::from([]).is_default(), true);
assert_eq!(KeyFormatVersions::from([1]).is_default(), true);
assert_eq!(KeyFormatVersions::from([1, 2, 3]).is_default(), false);
}
#[test]
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
View 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
View 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;

View 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);
}
}

View 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
View file

@ -0,0 +1,137 @@
use std::str::FromStr;
use derive_more::Display;
use shorthand::ShortHand;
use crate::Error;
/// The number of distinct pixels in each dimension that can be displayed (e.g.
/// 1920x1080).
///
/// For example Full HD has a resolution of 1920x1080.
#[derive(ShortHand, Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
#[display(fmt = "{}x{}", width, height)]
#[shorthand(enable(must_use))]
pub struct Resolution {
/// Horizontal pixel dimension.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::Resolution;
/// let mut resolution = Resolution::new(1280, 720);
///
/// resolution.set_width(1000);
/// assert_eq!(resolution.width(), 1000);
/// ```
width: usize,
/// Vertical pixel dimension.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::Resolution;
/// let mut resolution = Resolution::new(1280, 720);
///
/// resolution.set_height(800);
/// assert_eq!(resolution.height(), 800);
/// ```
height: usize,
}
impl Resolution {
/// Constructs a new [`Resolution`].
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::Resolution;
/// let resolution = Resolution::new(1920, 1080);
/// ```
#[must_use]
pub const fn new(width: usize, height: usize) -> Self { Self { width, height } }
}
impl From<(usize, usize)> for Resolution {
fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) }
}
impl Into<(usize, usize)> for Resolution {
fn into(self) -> (usize, usize) { (self.width, self.height) }
}
impl FromStr for Resolution {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut input = input.splitn(2, 'x');
let width = input
.next()
.ok_or_else(|| Error::custom("missing width for `Resolution` or an invalid input"))
.and_then(|v| v.parse().map_err(|e| Error::parse_int(v, e)))?;
let height = input
.next()
.ok_or_else(|| Error::custom("missing height for `Resolution` or an invalid input"))
.and_then(|v| v.parse().map_err(|e| Error::parse_int(v, e)))?;
Ok(Self { width, height })
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_display() {
assert_eq!(
Resolution::new(1920, 1080).to_string(),
"1920x1080".to_string()
);
assert_eq!(
Resolution::new(1280, 720).to_string(),
"1280x720".to_string()
);
}
#[test]
fn test_parser() {
assert_eq!(
Resolution::new(1920, 1080),
"1920x1080".parse::<Resolution>().unwrap()
);
assert_eq!(
Resolution::new(1280, 720),
"1280x720".parse::<Resolution>().unwrap()
);
assert!("1280".parse::<Resolution>().is_err());
}
#[test]
fn test_width() {
assert_eq!(Resolution::new(1920, 1080).width(), 1920);
assert_eq!(Resolution::new(1920, 1080).set_width(12).width(), 12);
}
#[test]
fn test_height() {
assert_eq!(Resolution::new(1920, 1080).height(), 1080);
assert_eq!(Resolution::new(1920, 1080).set_height(12).height(), 12);
}
#[test]
fn test_from() {
assert_eq!(Resolution::from((1920, 1080)), Resolution::new(1920, 1080));
}
#[test]
fn test_into() {
assert_eq!((1920, 1080), Resolution::new(1920, 1080).into());
}
}

420
src/types/stream_data.rs Normal file
View 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());
}
}

319
src/types/ufloat.rs Normal file
View file

@ -0,0 +1,319 @@
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 youre using but instead of the way Hash happens
/// to be implemented for the type youre 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]
fn test_hash() {
let mut hasher_left = std::collections::hash_map::DefaultHasher::new();
let mut hasher_right = std::collections::hash_map::DefaultHasher::new();
assert_eq!(
UFloat::new(1.0).hash(&mut hasher_left),
UFloat::new(1.0).hash(&mut hasher_right)
);
assert_eq!(hasher_left.finish(), hasher_right.finish());
}
#[test]
fn test_ord() {
assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(1.1)), Ordering::Equal);
assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(2.1)), Ordering::Less);
assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(0.1)), Ordering::Greater);
}
#[test]
fn test_partial_ord() {
assert_eq!(
UFloat::new(1.1).partial_cmp(&UFloat::new(1.1)),
Some(Ordering::Equal)
);
assert_eq!(
UFloat::new(1.1).partial_cmp(&UFloat::new(2.1)),
Some(Ordering::Less)
);
assert_eq!(
UFloat::new(1.1).partial_cmp(&UFloat::new(0.1)),
Some(Ordering::Greater)
);
}
#[test]
fn test_partial_eq() {
assert_eq!(UFloat::new(1.0).eq(&UFloat::new(1.0)), true);
assert_eq!(UFloat::new(1.0).eq(&UFloat::new(33.3)), false);
assert_eq!(UFloat::new(1.1), 1.1);
}
#[test]
#[should_panic = "float must be positive: `-1.1`"]
fn test_new_negative() { let _ = UFloat::new(-1.1); }
#[test]
#[should_panic = "float must be positive: `-0`"]
fn test_new_negative_zero() { let _ = UFloat::new(-0.0); }
#[test]
#[should_panic = "float must be finite: `inf`"]
fn test_new_infinite() { let _ = UFloat::new(::core::f32::INFINITY); }
#[test]
#[should_panic = "float must be finite: `-inf`"]
fn test_new_neg_infinite() { let _ = UFloat::new(::core::f32::NEG_INFINITY); }
#[test]
#[should_panic = "float must not be `NaN`"]
fn test_new_nan() { let _ = UFloat::new(::core::f32::NAN); }
#[test]
fn test_as_f32() {
assert_eq!(UFloat::new(1.1).as_f32(), 1.1_f32);
}
#[test]
fn test_from() {
assert_eq!(UFloat::from(1_u8), UFloat::new(1.0));
assert_eq!(UFloat::from(1_u16), UFloat::new(1.0));
}
#[test]
fn test_try_from() {
assert_eq!(UFloat::try_from(1.1_f32).unwrap(), UFloat::new(1.1));
assert_eq!(
UFloat::try_from(-1.1_f32),
Err(Error::custom("float must be positive: `-1.1`"))
);
assert!(UFloat::try_from(::core::f32::INFINITY).is_err());
assert!(UFloat::try_from(::core::f32::NAN).is_err());
assert!(UFloat::try_from(::core::f32::NEG_INFINITY).is_err());
}
#[test]
const fn test_eq() {
struct _AssertEq
where
UFloat: Eq;
}
}

126
src/types/value.rs Normal file
View 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
View 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"
)
);
}
}

View 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"

View 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
View 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()
);
}

View 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);
}

View 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
View file

@ -0,0 +1 @@
automod::dir!("tests/issues");

131
tests/master_playlist.rs Normal file
View 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
View 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
View file

@ -0,0 +1 @@
mod issues;

630
tests/rfc8216.rs Normal file
View 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
View 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");
}