Compare commits

...

140 commits

Author SHA1 Message Date
rutgersc 381ac7732f
Merge pull request #72 from ant1eicher/master
Support AWS Elemental MediaConvert decimal format.
2024-02-14 18:59:35 +01:00
Anton Eicher e3b6390186 EXTINF tags need to be in floating-point format to work with AWS Elemental MediaConvert
AWS Elemental MediaConvert rejects playlists with EXTINF tags that are not in floating point format. When m3u8 MediaSegment self.duration is an exact number without trailing decimals, writeln cuts off the decimal places and prints it like an integer.

This change adds support for fixed length floating point numbers.
2024-02-14 16:29:47 +02:00
rutgersc 7f322675eb
Merge pull request #73 from rutgersc/fix/targetduration
#EXT-X-TARGETDURATION:<s> is supposed to be a decimal-integer
2024-01-30 20:57:38 +01:00
Rutger Schoorstra c5cceeb4f6 Update version to 6.0.0 2024-01-26 18:56:34 +01:00
Rutger Schoorstra 5109753b96 #EXT-X-TARGETDURATION:<s> is supposed to be a decimal-integer
https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.1
2024-01-26 18:55:39 +01:00
Sebastian Dröge 487d63da4d Udpate version to 5.0.5 2023-12-05 08:33:58 +02:00
Vadim Getmanshchuk f6af8acbfe ran rustfmt 2023-12-04 21:25:16 +02:00
Vadim Getmanshchuk 46622345d1 added test 2023-12-04 21:25:16 +02:00
Vadim Getmanshchuk d8e0283ddb allowing empty comments 2023-12-04 21:25:16 +02:00
Sebastian Dröge b9cf88b7ec Update version to 5.0.4 2023-05-08 10:24:06 +03:00
clitic a1970192ff Write BYTERANGE inside quotes 2023-04-12 10:39:44 +03:00
clitic ae31a2741f Parse #EXT-X-MAP BYTERANGE attr from quoted string 2023-04-12 10:39:44 +03:00
mmason e7a6cf943c allow millisecond accuracy for EXT-X-PROGRAM-DATE-TIME 2023-04-11 18:05:54 +03:00
Sebastian Dröge 48e416cd69 Update version to 5.0.3 2022-12-02 10:46:10 +02:00
Sebastian Dröge 015b05f26c Fix some minor clippy warnings 2022-12-02 10:46:10 +02:00
Vadim Getmanshchuk b0a9fe2625 When a manifest is incomplete or damaged, the contains_master_tag may go into infinite loop if is_master_playlist_tag_line detection hasn't had a chance to succeed.
Clippy recommendation is implemented to use `.is_none()` instead of comparing to literal `Option::None`.
Test added
2022-12-02 10:18:40 +02:00
Sebastian Dröge 14d24b94c8 Release 5.0.2 2022-09-23 20:16:57 +03:00
Shell Turner 2a76fa549c Remove unnecessary features from chrono, removing vulnerable time dependency 2022-09-23 18:56:20 +03:00
Sebastian Dröge 18739b59ac Release 5.0.1 2022-09-12 11:06:52 +03:00
Sebastian Dröge e41c47a8f8 Map unknown strings to already existing Other variants of enums
Fixes https://github.com/rutgersc/m3u8-rs/issues/52
2022-09-12 10:12:44 +03:00
Rutger Schoorstra 31b31fd958 Update version to 5.0.0 2022-07-30 13:05:09 +02:00
Vadim Getmanshchuk d2881fef08 Improve #EXT-X-INDEPENDENT-SEGMENTS placement
While it can be anywhere in the playlist as it applies to, this one usually is at the top of the manifest, right after the VERSION tag.
2022-07-21 10:34:57 +03:00
Sebastian Dröge bd7cce75e9 Parse PROGRAM-DATE-TIME and DATERANGE start/end as proper datetimes instead of strings 2022-07-21 10:34:57 +03:00
Vadim Getmanshchuk 7247e02ee5 Use more specific types for playlist fields and improve parsing/writing
Added
* DateRange struct and implementations
* Parsing DateRange (wasn't parsed due to a typo)
* fn daterange() to parse DateRange
* `write_some_other_attributes` macro
* resolved `FIXME required unless None` for method and iv

Changed / fixed:
* trim_matches() potentially too greedy, replaced with strip_suffix/prefix
* removed a bunch of "Unnecessary qualification", eg `fmt::` for `Display`
* changed `fn extmap()` to use `QuotedOrUnquoted` and `other_attributes`
* `fmt` for `QuotedOrUnquoted` are now printing quoted strings in `"`
* `bool_default_false` macro renamed to `is_yes`
* qualified error message for `quoted_string_parse` and `unquoted_string_parse`
* `other_attributes` now `Option<>`
* `ClosedCaptionGroupId::Other(s)` variant processing added to `write_to`
* `HDCPLevel::Other(s)` variant processing added to `fmt`
* `AlternativeMediaType::Other(s)` variant processing added to `fmt`
* `InstreamId::Other(s)` variant processing added to `fmt`
* `MediaPlaylistType::Other(s)` variant processing added to `fmt`
* `KeyMethod::Other(s)` variant processing added to `fmt`
* included `DateRange` to tests
* included `other_attributes` to tests

Minor:
Typos corrections
rustfmt applied
2022-07-21 10:34:57 +03:00
Sebastian Dröge 6559e45b49 Store VariantStream attributes in more specific types and validate them 2022-07-21 10:34:57 +03:00
Sebastian Dröge 0789098d7d Add some convenience API to QuotedOrUnquoted 2022-07-21 10:34:57 +03:00
Sebastian Dröge b692ac0808 Remove additional newline after "#EXTM3U" added in previous commit
Co-authored-by: Vadim Getmanshchuk <vagetman@users.noreply.github.com>
2022-04-19 18:20:32 +03:00
Sebastian Dröge 85141f6a51
Merge pull request #50 from vagetman/patch-3
`EXT-X-VERSION` tag may be absent
2022-04-17 23:45:33 +03:00
Vadim Getmanshchuk d941541be8 EXT-X-VERSION tag may be absent 2022-04-17 01:08:07 -07:00
Sebastian Dröge 7173c26015
Merge pull request #45 from vagetman/patch-2
A fix for `CLOSED-CAPTIONS=NONE` case and a few minor  fixes
2022-04-17 09:03:34 +03:00
Vadim Getmanshchuk ac0f881eef * added QuotedOrUnquoted enum
* implemented `Default`, `From`, `Display` traits
* updated `VariantStream`, `AlternativeMedia`, `SessionData`, `Key`, `Start` emums
* updated `from_hashmap` methods for each enum
* fixed tests
2022-04-15 23:06:40 -07:00
rutgersc 212a485687
Merge pull request #47 from rutgersc/bump
Update version to 4.0.0
2022-04-13 23:14:49 +02:00
Rutger Schoorstra dc352b7ef3 Update version to 4.0.0 2022-04-13 22:57:56 +02:00
rutgersc c28cb7f7d6
Merge pull request #46 from rutgersc/readme
Update readme, v3.0.1
2022-04-09 13:03:13 +02:00
Rutger Schoorstra f606063330 Update version to 3.0.1 2022-04-09 12:59:07 +02:00
Rutger Schoorstra 210af70f72 Update readme 2022-04-09 12:59:07 +02:00
Vadim Getmanshchuk 5c842fd9f6 minor cargo clippy suggestions 2022-04-08 16:31:37 -07:00
Vadim Getmanshchuk 2f92e3ae8c #EXT-X-ENDLIST is moved to be the last part of the media manifest. Theoretically it can appear anywhere in manifest, so the current placement is not breaking the standard, but not usually is what found in the wild and I believe will help with readability. As any placement is possible, the placement at the end is completely legal. 2022-04-08 16:27:04 -07:00
Vadim Getmanshchuk 3c8368f9a3 A fix for CLOSED-CAPTIONS=NONE case and a few minor fixes
The PR includes 
* a hotfix for issue #44. I'm not entirely sure and it's possible to
* `#EXT-X-ENDLIST` is moved to be the last part of the media manifest. Theoretically it can appear anywhere in manifest, so the current placement is not breaking the standard, but not usually is what found in the wild and I believe will help with readability. As any placement is possible, the placement at the end is completely legal.  
* minor `cargo clippy` suggestions
2022-04-08 16:23:27 -07:00
rutgersc 39b52a1d4b
Merge pull request #42 from sdroege/u64-instead-of-i32
Use `u64` instead of `i32` for byte ranges and sequence numbers
2022-02-19 13:38:06 +01:00
rutgersc bc8ccf0f5d
Merge pull request #41 from sdroege/derive-more-traits
Derive some more traits for the public types where it makes sense
2022-02-19 13:37:41 +01:00
rutgersc 5ee1273f7c
Merge pull request #40 from sdroege/type-closed-captions
The `TYPE` attribute uses `CLOSED-CAPTIONS` and not `CLOSEDCAPTIONS`
2022-02-19 13:37:26 +01:00
Sebastian Dröge 44aa097c90 Use u64 instead of i32 for byte ranges and sequence numbers
Fixes https://github.com/rutgersc/m3u8-rs/issues/39
2022-01-07 12:47:40 +02:00
Sebastian Dröge 1bfad5df01 Derive some more traits for the public types where it makes sense
This makes them easier to use.
2022-01-07 12:44:56 +02:00
Sebastian Dröge cca02b239d The TYPE attribute uses CLOSED-CAPTIONS and not CLOSEDCAPTIONS
See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-10#section-4.4.6.1
2022-01-05 19:11:42 +02:00
rutgersc 836ef1caaf
Merge pull request #37 from sdroege/is-master-playlist-early-return
Don't bother parsing as a playlist or detecting if it's a master/medi…
2021-11-20 19:06:41 +01:00
Sebastian Dröge 2fae1d8f20 Don't bother parsing as a playlist or detecting if it's a master/media playlist if it doesn't start with #EXTM3U 2021-11-19 11:29:46 +02:00
rutgersc 53e9439660
Merge pull request #36 from sdroege/nom-7
Port to nom 7
2021-11-18 17:15:50 +01:00
Sebastian Dröge 472618e1aa Update version to 3.0.0 2021-11-18 15:05:32 +02:00
Sebastian Dröge 2432846064 Move the crate docs to the root of the crate so they actually show up
And also fix all the broken links while we're at it.
2021-11-18 15:04:17 +02:00
Sebastian Dröge 51fcb70113 Re-export all types from the crate root and remove the playlist sub-module
There's not much else in this crate and having it behind another module
decreases visibility.
2021-11-18 15:00:01 +02:00
Sebastian Dröge 3edf5d1c0f Fix various minor clippy warnings 2021-11-18 14:54:46 +02:00
Sebastian Dröge 7e62854e20 Use unwrap_or_default() instead of unwrap_or_else(Default::default) 2021-11-18 14:52:38 +02:00
Sebastian Dröge 336f11e1ba Remove useless fn main() from documentation examples 2021-11-18 14:52:03 +02:00
Sebastian Dröge a5d8358379 Make most internal parser functions private
And move parser internals tests into a test submodule of the parser.

Also add actual assertions to various tests so they test something.
2021-11-18 14:50:14 +02:00
Sebastian Dröge 5500166f74 Fix confusing #[path] usage and re-exports in lib.rs
This has effectively the same behaviour now with fewer lines, less
confusion and fewer compiler warnings about unused code.
2021-11-17 19:32:57 +02:00
Sebastian Dröge 4e6ac58d0c Add tests for parsing non-playlist text and binary data
These should fail to parse (and not panic), but previously the
non-playlist text succeeded.
2021-11-17 19:22:28 +02:00
Sebastian Dröge 65c295ee02 Require each M3U8 playlist to start with the #EXTM3U8 tag
The RFC requires this to be the very first line of every master/media
playlist, and without this we would be parsing arbitrary text files as
playlist without erroring out.

See https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.1.1

Fixes https://github.com/rutgersc/m3u8-rs/issues/27
2021-11-17 19:14:58 +02:00
Sebastian Dröge a44c2a1a72 Run parser through cargo fmt
Now that we don't use the nom macros anymore this works properly.
2021-11-17 16:00:25 +02:00
Sebastian Dröge 0ed0ce51f8 Migrate to Rust 2018
Cleans up some noise.
2021-11-17 16:00:23 +02:00
Sebastian Dröge 81398f86cd Port to nom 7
Fixes https://github.com/rutgersc/m3u8-rs/issues/35
2021-11-17 16:00:08 +02:00
rutgersc f104d431d9
Merge pull request #34 from rutgersc/ci-check-formatting
CI check formatting
2021-10-19 19:50:26 +02:00
Rutger Schoorstra 0a3fb0e671 Version 2.1.0 2021-10-19 19:48:26 +02:00
Rutger Schoorstra 1287975af4 Check cargo fmt on CI 2021-10-19 19:48:26 +02:00
Rutger Schoorstra 6ee1b52c01 Update readme 2021-10-19 19:48:26 +02:00
rutgersc 303d0ecfce
Merge pull request #33 from rafaelcaricio/apply-fmt-and-clippy
Apply cargo fmt and clippy suggestions
2021-10-19 18:54:21 +02:00
Rafael Caricio 3d5599fa28
Apply clippy suggestions 2021-10-18 11:48:30 +02:00
Rafael Caricio 39aab3a2ac
Apply cargo fmt 2021-10-18 11:41:28 +02:00
rutgersc 359695a25c
Merge pull request #30 from rafaelcaricio/support-segment-unknown-tags
Support parsing of unknown tags on segments
2021-10-16 21:09:02 +02:00
Rafael Caricio 677027e22c
Update readme with new attribute 2021-10-14 21:35:35 +02:00
Rafael Caricio dc352301a3
Allow unknown tags at the master playlist level 2021-10-14 21:21:03 +02:00
Rafael Caricio c1ff2b3730
Support parsing of unknown tags on segments 2021-10-12 23:06:47 +02:00
Rutger Schoorstra 06162a8554 Version 2.0.0 2021-04-24 19:06:57 +02:00
rutgersc 46922bdab3
Merge pull request #23 from rutgersc/use-features
Use features to split parser and types
2021-04-24 19:03:49 +02:00
Rutger Schoorstra dc576c8e3c Add parser as default feature 2021-04-24 18:43:57 +02:00
Rutger Schoorstra c3ef5bc16e Split code into parser/types 2021-04-24 18:39:25 +02:00
rutgersc 5a72e1e875
Merge pull request #24 from thaytan/20-multiple-session-data-keys
Fixes for session data and keys handling
2021-04-24 17:29:44 +02:00
Jan Schmidt 05669cab68 Support multiple session data and key tags.
Collect Vecs of session_data and session_key tags to
allow for multiples of each. Doesn't do any validation
as to disallowed duplicated values.

Fixes #20
2021-04-25 00:44:08 +10:00
Jan Schmidt 870ca830d3 SessionData: Must have either VALUE or URI, but not both.
If SessionData has a value, don't write the URI and vice-versa.

As per https://tools.ietf.org/html/rfc8216#section-4.3.4.4
EXT-X-SESSION-DATA must have one or the other, not both.
2021-04-25 00:44:08 +10:00
rutgersc 85b0826103
Merge pull request #26 from thaytan/update-to-rfc8216
Add HDCP-LEVEL and CHANNELS fields.
2021-04-21 18:20:16 +02:00
Jan Schmidt 5fe3fc309c Add HDCP-LEVEL and CHANNELS fields.
Add support for parsing / generating HDCP-LEVEL in a VariantStream
and CHANNELS in AlternativeMedia. The fields were added after
draft-pantos-http-live-streaming-19.txt and brings things up to date
with RFC 8216.
2021-04-21 13:44:17 +10:00
rutgersc b75379437d
Merge pull request #16 from rafaelcaricio/master
Expose unknown tags
2021-04-20 19:35:58 +02:00
Rafael Caricio 3e74f7787f Expose unknown tags 2021-04-20 19:29:04 +02:00
rutgersc 302ff22f31
Merge pull request #21 from rutgersc/fix/alternatives
Move alternatives to MasterPlaylist
2021-04-20 18:47:33 +02:00
Rutger Schoorstra b44f6518c8 Version 1.0.8 2021-04-18 12:51:43 +02:00
rutgersc 37acdb304d
Merge pull request #22 from rutgersc/fix/line-ending
Fix "Last URL in m3u8 file not found unless a new line is found"
2021-04-18 12:07:38 +02:00
Rutger Schoorstra 087c47bddd Fix consume_line not returning line when line doesn't end with a newline 2021-04-18 11:59:31 +02:00
Rutger Schoorstra 57d60ba438 Move alternatives to MasterPlaylist 2021-04-16 21:20:30 +02:00
rutgersc cd9402051e
Merge pull request #18 from thaytan/17-fix-master-playlists
Handle blank lines when checking for master playlist tags.
2021-04-16 19:05:22 +02:00
Rutger Schoorstra 64de5e4d9b Version 1.0.7 2021-03-17 17:16:40 +01:00
Jan Schmidt 978e6a7e58 Handle blank lines when checking for master playlist tags.
Fix a bug where a blank line in a master playlist before the first
master playlist tag will make the parser think it's a media playlist.

Fixes #17
2021-03-17 06:41:35 +11:00
Rutger Schoorstra 76aab26b20 Create rust.yml 2020-07-01 17:13:26 +02:00
rutgersc 1b18c7902c
Create rust-windows.yml 2020-07-01 17:08:06 +02:00
rutgersc e2822e4521
Merge pull request #11 from vagetman/vagetman-nom-5.1.0
Upgraded macros to Nom 5
2020-07-01 16:43:30 +02:00
Vadim Getmanshchuk 7a882e5df0
fixed warning: unused std::result::Result
```
warning: unused `std::result::Result` that must be used
```

I forgot `?` for the error propagation
2020-03-21 21:23:03 -07:00
Vadim Getmanshchuk a081b462d1 Stops #X-EXT-KEY and #X-EXT-MAP tags replication 2020-03-15 11:02:35 +01:00
Vadim Getmanshchuk 3fee7b9983
Merge pull request #1 from rutgersc/vaget/vagetman-nom-5.1.0-extra
Updated docs, tests to nom 5.1.0
2020-03-08 12:15:03 -07:00
rutgersc ca9c41d823
Merge pull request #10 from vagetman/patch-1
Version update in README to match the current
2020-03-07 10:43:42 +01:00
Rutger Schoorstra 100a57078a Fix failed test on CLRF 2020-03-07 10:23:06 +01:00
Rutger Schoorstra ab9c554eb4 Upgraded tests to Nom 5 2020-03-07 10:23:06 +01:00
Rutger Schoorstra 13405a09eb Upgraded docs to Nom 5 2020-03-07 10:23:06 +01:00
Vadim Getmanshchuk b810687652
fixed issue with #EXTINF without titles
in #EXTINF tag, when comma `,` after segment duration immediately follows by `\n` the `title` is not getting populated and is not printed with `writeln!`

The problem is created by the nom 5 conversion, where `take_until_either_and_consume!` macro was replaced with `is_not!` + `take!(1)`. However, in case when `opt!(take_until_either_and_consume!` is used, everything gets way too hairy. I couldn't to expressed that in a fairly elegant manner and instead fixed the data presentation, when a new manifest is produced. While storing `\n` in a form of `title` is a nice hack, I'm also convinced this is a better approach to data handling.
2020-03-06 21:52:22 -08:00
Vadim Getmanshchuk 4ed378772b
added forgotten use for map 2020-03-04 20:12:50 -08:00
Vadim Getmanshchuk 350109e29a
map cleanup, leftovers 2020-02-26 22:57:05 -08:00
Vadim Getmanshchuk f7587aa264
cleanup, fn map renamed
Local `fn map` -> `fn extmap` that allows to use map from Nom directly, without `nom::combinator::`
2020-02-26 22:52:20 -08:00
Vadim Getmanshchuk e4e1717b0a
Back ported 1.0.6 release 2020-02-26 17:17:13 -08:00
Vadim Getmanshchuk b9d377bfa4
Upgraded macros to Nom 5
Changes:

IResult::Done - IResult::Ok
chain! -> do_parse!
   slightly different syntax with `?` ->  opt!
digit -> digit1
space? -> space0
multispace? -> multispace0
many0!() -> many0!(complete!())
take_until_either! -> is_not!
take_until_and_consume! -> take_until! + take!(1)
take_until_either_and_consume! -> is_not! + take!(1)
2020-02-26 17:05:14 -08:00
Vadim Getmanshchuk fc9f45dd18
Fix tag duplication
The fix is addressing duplication for #EXT-X-KEY and #EXT-X-MAP tags in a media manifest produced
2020-02-26 16:21:06 -08:00
Vadim Getmanshchuk ed0d35b3a3
Update README.md 2020-02-26 16:15:27 -08:00
Vadim Getmanshchuk e982070d59
Version update in README to match the current
The current is 1.0.6. It should match one with the README
2020-02-26 12:17:45 -08:00
Rutger Schoorstra 43c7321fb2 Version 1.0.6 2020-02-15 16:02:58 +01:00
Rutger Schoorstra 31e78801f9 CODECS should be optional
https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/creating_a_master_playlist
> While the CODECS parameter is optional, every EXT-X-STREAM-INF tag should include the attribute.
2020-02-15 16:02:45 +01:00
Rutger Schoorstra 03ec1a4544 Added quick parse roundtrip test 2020-02-15 16:01:30 +01:00
Rutger Schoorstra b2150f26e5 Remove unneeded macro_use 2020-02-15 16:00:10 +01:00
rutgersc e594ec5792
Merge pull request #7 from vagetman/master
A comma added before the URI for fn write_to
2020-02-15 15:47:45 +01:00
Vadim Getmanshchuk af15863688
Done -> Ok in examples 2020-02-12 16:35:31 -08:00
Rogier 'DocWilco' Mulhuijzen 67eebd17a0 Done is now Ok, map works a little different 2020-02-12 14:38:47 -08:00
Vadim Getmanshchuk ca07767eb4
Update Cargo.toml
upped nom dependency to 5.1.0
2020-02-12 12:19:40 -08:00
Vadim Getmanshchuk 22571a3404
A stub on updating nom to 5.1.0 version 2020-02-12 12:04:20 -08:00
vagetman 279fefef72
A comma added before the URI for fn write_to 2020-02-11 12:43:06 -08:00
Rutger Schoorstra 9d81ce7194 Version Version 1.0.5 2019-04-26 19:49:46 +02:00
rutgersc 5389f25f6f
Merge pull request #5 from gurry/master
Fixed a bug where #EXT-X-MEDIA-SEQUENCE tag was being interpreted as #EXT-X-MEDIA
2019-04-26 19:43:36 +02:00
Gurinder Singh ccf25fefcf Fixed a bug where #EXT-X-MEDIA-SEQUENCE tag was being interpreted as #EXT-X-MEDIA 2019-03-08 11:22:34 +05:30
rutgersc 3c8268eace
Merge pull request #3 from gurry/master
Implemented Clone on all structs
2018-11-29 19:06:59 +01:00
Gurinder Singh 759809b2b4 Implemented Clone on all structs 2018-11-26 07:13:42 +05:30
rutgersc b3bdaa133c
Merge pull request #2 from philn/master
Make tests run on non-windows machines
2017-12-02 15:24:41 +01:00
Philippe Normand b5b950150f Make tests run on non-windows machines
Hardcoding the windows path separator isn't a good idea for portability :)
2017-11-11 13:02:04 +01:00
Rutger e18e1d5cd0 Version 1.0.4 2017-04-19 09:32:53 +02:00
Rutger 6032301143 Delete println! 2017-04-19 09:05:13 +02:00
Rutger 7583aaccba Fix doc tests 2017-02-17 17:22:09 +01:00
Rutger e3d0cb8cff Added example of how to create & write a playlist 2017-02-17 15:16:08 +01:00
Rutger a900b5471f Version 1.0.3 2017-02-17 14:53:02 +01:00
Rutger c336b89981 Added feature: writing playlists back to file 2017-02-17 14:50:50 +01:00
Rutger 6286cddc04 Remove unwrap from version tag 2017-02-16 11:17:02 +01:00
Rutger 333ac3ec46 Version 1.0.2 2016-09-10 13:27:23 +02:00
Rutger d7c452fe78 Fixed a bug where the parser fails when the playlist does not end with a newline. 2016-09-10 13:22:13 +02:00
Rutger fe2ceb87dd fixed README 2016-06-03 21:46:36 +02:00
Rutger f08d22d030 Added documentation links 2016-06-03 21:39:36 +02:00
Rutger a81a5e582b LICENSE 2016-06-03 21:25:34 +02:00
Rutger 0828c51e71 extra crates.io metadata 2016-06-03 21:12:53 +02:00
21 changed files with 2695 additions and 1156 deletions

27
.github/workflows/rust-windows.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Rust Windows
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run fmt
run: cargo fmt --all -- --check
- name: Run tests
run: cargo test --verbose

27
.github/workflows/rust.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Rust
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run fmt
run: cargo fmt --all -- --check
- name: Run tests
run: cargo test --verbose

View file

@ -1,7 +1,19 @@
[package]
name = "m3u8-rs"
version = "1.0.0"
version = "6.0.0"
authors = ["Rutger"]
readme = "README.md"
repository = "https://github.com/rutgersc/m3u8-rs"
description = "A library for parsing m3u8 files (Apple's HTTP Live Streaming (HLS) protocol)."
documentation = "https://rutgersc.github.io/doc/m3u8_rs/index.html"
license = "MIT"
edition = "2018"
[dependencies]
nom = "^1.2.3"
nom = { version = "7", optional = true }
chrono = { version = "0.4", default-features = false, features = [ "std" ] }
[features]
default = ["parser"]
parser = ["nom"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Rutger Schoorstra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

138
README.md
View file

@ -1,136 +1,10 @@
# m3u8-rs
A Rust library for parsing m3u8 playlists (HTTP Live Streaming) [link](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19).
# m3u8-rs
![crates.io](https://img.shields.io/crates/v/m3u8-rs.svg)
[![API](https://docs.rs/m3u8-rs/badge.svg)](https://docs.rs/m3u8-rs)
A Rust library for parsing m3u8 playlists (HTTP Live Streaming) [link](https://datatracker.ietf.org/doc/html/rfc8216).
Uses the [`nom` library](https://github.com/Geal/nom) for all of the parsing.
# Installation
To use this library, add the following dependency to `Cargo.toml`:
```toml
[dependencies]
m3u8-rs = "1.0.0"
```
And add the crate to `lib.rs`
```rust
extern crate m3u8_rs;
```
Also available on [crates.io]()
# Documentation
Available [here]()
# Examples
A simple example of parsing a playlist:
```rust
use m3u8_rs::playlist::Playlist;
use std::io::Read;
let mut file = std::fs::File::open("playlist.m3u8").unwrap();
let mut bytes: Vec<u8> = Vec::new();
file.read_to_end(&mut bytes).unwrap();
let parsed = m3u8_rs::parse_playlist_res(&bytes);
match parsed {
Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
Err(e) => println!("Error: {:?}", e)
}
```
In the example above, `parse_playlist_res(&bytes)` returns a `Result<Playlist, IResult>`. It uses
the output of `parse_playlist(&bytes)` behind the scenes and just converts the `IResult` to a `Result`.
Here is an example of using the `parse_playlist(&bytes)` with `IResult` directly:
```rust
use m3u8_rs::playlist::Playlist;
use std::io::Read;
use nom::IResult;
let mut file = std::fs::File::open("playlist.m3u8").unwrap();
let mut bytes: Vec<u8> = Vec::new();
file.read_to_end(&mut bytes).unwrap();
let parsed = m3u8::parse_playlist(&bytes);
match parsed {
IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
IResult::Error(e) => panic!("Parsing error: \n{}", e),
IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
}
```
Currently the parser will succeed even if REQUIRED attributes/tags are missing from a playlist (such as the `#EXT-X-VERSION` tag).
The option to abort parsing when attributes/tags are missing may be something to add later on.
# Structure Summary
All of the details about the structs are taken from https://tools.ietf.org/html/draft-pantos-http-live-streaming-19.
```rust
// Short summary of the important structs in playlist.rs:
//
pub enum Playlist {
MasterPlaylist(MasterPlaylist),
MediaPlaylist(MediaPlaylist),
}
pub struct MasterPlaylist {
pub version: usize,
pub variants: Vec<VariantStream>,
pub session_data: Option<SessionData>,
pub session_key: Option<SessionKey>,
pub start: Option<Start>,
pub independent_segments: bool,
}
pub struct MediaPlaylist {
pub version: usize,
pub target_duration: f32,
pub media_sequence: i32,
pub segments: Vec<MediaSegment>,
pub discontinuity_sequence: i32,
pub end_list: bool,
pub playlist_type: MediaPlaylistType,
pub i_frames_only: bool,
pub start: Option<Start>,
pub independent_segments: bool,
}
pub struct VariantStream {
pub is_i_frame: bool,
pub uri: String,
pub bandwidth: String,
pub average_bandwidth: Option<String>,
pub codecs: String,
pub resolution: Option<String>,
pub frame_rate: Option<String>,
pub audio: Option<String>,
pub video: Option<String>,
pub subtitles: Option<String>,
pub closed_captions: Option<String>,
pub alternatives: Vec<AlternativeMedia>,
}
pub struct MediaSegment {
pub uri: String,
pub duration: f32,
pub title: Option<String>,
pub byte_range: Option<ByteRange>,
pub discontinuity: bool,
pub key: Option<Key>,
pub map: Option<Map>,
pub program_date_time: Option<String>,
pub daterange: Option<String>,
}
```
Examples can be found in the `examples` folder.

View file

@ -1,7 +1,4 @@
extern crate nom;
extern crate m3u8_rs;
use m3u8_rs::playlist::{Playlist};
use m3u8_rs::Playlist;
use std::io::Read;
fn main() {
@ -12,8 +9,8 @@ fn main() {
let parsed = m3u8_rs::parse_playlist_res(&bytes);
match parsed {
Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
Err(e) => println!("Error: {:?}", e)
Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl),
Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl),
Err(e) => println!("Error: {:?}", e),
}
}

View file

@ -1,9 +1,5 @@
extern crate nom;
extern crate m3u8_rs;
use m3u8_rs::playlist::{Playlist};
use m3u8_rs::Playlist;
use std::io::Read;
use nom::IResult;
fn main() {
let mut file = std::fs::File::open("playlist.m3u8").unwrap();
@ -13,17 +9,17 @@ fn main() {
let parsed = m3u8_rs::parse_playlist(&bytes);
let playlist = match parsed {
IResult::Done(i, playlist) => playlist,
IResult::Error(e) => panic!("Parsing error: \n{}", e),
IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
Result::Ok((_i, playlist)) => playlist,
Result::Err(e) => panic!("Parsing error: \n{}", e),
};
match playlist {
Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{}", pl),
Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{}", pl),
Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{:?}", pl),
Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{:?}", pl),
}
}
#[allow(unused)]
fn main_alt() {
let mut file = std::fs::File::open("playlist.m3u8").unwrap();
let mut bytes: Vec<u8> = Vec::new();
@ -32,9 +28,8 @@ fn main_alt() {
let parsed = m3u8_rs::parse_playlist(&bytes);
match parsed {
IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
IResult::Error(e) => panic!("Parsing error: \n{}", e),
IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
Result::Ok((_i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl),
Result::Ok((_i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl),
Result::Err(e) => panic!("Parsing error: \n{}", e),
}
}

View file

@ -0,0 +1,23 @@
#EXTM3U
#EXT-X-TWITCH-INFO:NODE="video-edge-346990.cph01",MANIFEST-NODE-TYPE="weaver_cluster",MANIFEST-NODE="video-weaver.cph01",SUPPRESS="false",SERVER-TIME="1603666310.03",TRANSCODESTACK="2017TranscodeX264_V2",USER-IP="81.228.244.140",SERVING-ID="2e8913ca31ab4d54988d8dd87a66d3eb",CLUSTER="cph01",ABS="false",VIDEO-SESSION-ID="709300939666498958",BROADCAST-ID="40219312190",STREAM-TIME="10225.032456",B="false",USER-COUNTRY="SE",MANIFEST-CLUSTER="cph01",ORIGIN="sjc02",C="aHR0cHM6Ly92aWRlby1lZGdlLTNlN2UyOC5wZHgwMS5hYnMuaGxzLnR0dm53Lm5ldC92MS9zZWdtZW50L0NtSVk5WlFDUWp1eTFjMmo0WFB0LUM0eDhSWWZpdlRnRjl6dDJQT083WlRPdzYybC14bkpnYnJxanlBTl82eExfWlcxeS1PS1NyUFBQZnpaSUI0VGxYS3k4YmFvMEpUVjQ0X1Q2X215b1dKZ3NDaVBWaU5CMjIyTlljd2VpQVpaTUxHUElER1ExY1dla1hvejc3LTRPU1BqRVhpUmNDVnM5ZmFnMkVLblk5UTZjaldDaTZwSldVTjl2N01femVRZ1VsSU5YNDNIdFh4RU1ORU5sT2hZX1RXRjExRmJqRVdjMW9OYkRva191c3g0WHdwb29ETVFFZ2Z1NlJFV0pRRERFdGhTTVJDN3J3SFVFb3c4aG9PN0VLQmFOVG1zTGpIUjZ6S3R3RS1nblptRk1HSWFSQ2V6NWtzTlVOREhidW45RXlOOFdIeWJaVG1rZlpfQm9MRjZScXBpbnNUYkJGT0I1VWJUYnFjbU50RnBFcGs5ejFBU29zZHNvM0ZJNGplWWZUU3lIYTFSQU11a0YwQXBJZjNmNHZDaVpNdFN3amh6aFl1Z2dsNnM0a0ZZSWdKUVBPMUhGWktlMjFtak1BOHBycFJoV2NqazN1Z2VvUnI2VGt0S3dzSmdBVjVrWmlRUm9TZ1V5ZE5UUmZJa1hIay1nM1JBaW1FcGpiWnVUYnd0UVRFSjhpNUhBdGtTd25ZUm01WUhrTlpVVnU0TlJyZDdiaElDTEZVWW1xSlRLR3hSU0NXXzRteXlmc0JYeXZFQWFhVFNTLTBtb0dSTmpGZEl5TlotOVN2U3pKZ0UxRkJiSk9pdTVGS1phTDlzNGl3NHYySXJITUF3dDkwbkxWN1QzNTVQQTVIN0pRTlk3MXZMTUNtZGNMMkY4RG1BTE1JNl9lbEZEeml5dThhRFlieXZjbi1LXzJWZmk2azBONjNGSnBKU0F3bDZyaG01Um80bVJ2NFc0TDl3V3JacTNhblNxR2wxWlZJejBUNnZlcFRYU3l5UVl6dzRPYmZ1S0l6OVNGdFNnb0JvSmFiLnRz",D="false"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="1080p60 (source)",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=9061127,RESOLUTION=1920x1080,CODECS="avc1.64002A,mp4a.40.2",VIDEO="chunked"
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/Cu0DdhlwnpcIE03_B0CE88V9Oqeqod_rQRgGhkHtC_a8kMVJczhYuLCbGc04CMESYcRvucOJJccCANfsfli1hJPoqruJD7u9ETSxMIcNUfT54D52gA55ePJXxM5IUddHwZ5lw1vPC4IFC0IPVeUvRFw5g8HbCgRAJD-YV2W90VVcbXoCnNoLtvk_IJYwVk59XGECSzWh9lRRXTuqHgOWaehnTOvBcNLGl_BovpgqnLcMjeJTLhIO9Sna8JqM3ppnl19M8AE9RqrI6P4qsQRPcOmwa19Xx8n7yhBcrkMHVJhatHhUIVnOJ99fmgTul9NPBtRhcn59AVSCcNHrjrMwK0LTWmkD-8rmZaG2ezrLVbC6TzR9LfPsYVPokmAKDyUdOdai0gEMoWI8MvD17gl5bgoWrUHNk458cbtN8PaNrrbRbTC5fweA3Qo4mo4QNv1qOwi2inwKV3Jq81y6A0WG9gq-xQlQQaTk9r_XPnkqmuAFW_SlXHC7fdT8ZQHr3LxvzsQgjBrTo935EK269CMU3uuzOiQIphR0N-NJormgedjw5sZ6WT1N-1UWSliEqJ2CP1F89fPGDfB0Zxi8RUWNY7FJDJBWaofXumtBkObhmUqaPQtm5yq2rto89e0mOFx8pOzFCH556zu0vuHZ4ua4OxIQ3gR52zVBOaNMWs8PGJ7UARoMYEeMrBQEwEssMiG4.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p60",NAME="720p60",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=3430252,RESOLUTION=1280x720,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="720p60"
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDfH81q-R4CeLFl4jpTunvTt4JycqLEY4s_ysCUBub4j9pBZ-_6XcUIrWcfAgyaxFDNk7U6OkUYiD2jLTsvR4fzLGLzjnCyi604bDe_hek8719_WGgzVYEH6NuqauU_5C0DN6_h79PjTHkdu2Ty3WVlO4wg1nONk1m79n5DyEuxZOWuSQVZ5EIXxRWTMWbJxRNnW-jONMCAogfwrhIckgLOJIHjALdyBKEj7RFLGTsInfK59l7q81FsWFZztgrpIRjOteUdwJpWgnMpSK17rA-Rpy_KaU0oZKVslYMA7ZN2SLh808PKg28A6pqECZYu99Mpi6u_vo9WLU77igUi-P1UWEtuWEBXSPLQ1e5CSlBl7N_O13davOU7VndT8DZXgRwSlv_l3PmUQHG93BNdjxUpxzuc1lUKyde-M0xjhIBqQaYiwZ8UJeEWhuGbG1lsrCEzrsWMwo-FgBIakfVCMVCyQJk2w7E5bscPSQBuXLt4wlYUJZE5oSCGIj58xYZGmXd9ziUho5hH2KX5QA53nwcj4pbwfc-a4kE3ei12XuzCdsnjOgN1MxfCH2lfqHzGk9qxkPAF78e3nXU3U-wud8ZqMpYMXuGtf3ieA8bcXbrT_TftnA2KF1OaZ_Cn0Sv2qp6LHWkcAPPBe1KxkUSELv_6WHWmQDFpHKMYFMXTqgaDMDj0TLeNq1d00BBTw.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p30",NAME="720p",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=2380252,RESOLUTION=1280x720,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="720p30"
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDrxckkPKJOGpcsyTyuBoy-pTZVCqHuHQzm7N0fmyFc165xORl03bTfVbTY694T2U5WFAir8n62naHRMRgqJGJbvWyPx3lSetz6nKH-mY3TeDv-irBc6AEWfUbMmd5Nfh22CFBg__4iQxgvhru8TDlSDI2jR2pGkWCkN72bGN7iT6AXVcXTg_LVl1Tv1L7Sk8DFXm4jNHUPZo6utaYQ1jzlkEESTUE9PnXqrRRhIsm1HsCLsiUWZuBXyqgSbvJRl9LA1lFSoWEl-oGksc9BtFC5J25589KAc2vbvJpRvkiROlJaSwhD0m2LeOM9MiYU4HsFx94g8EC6WZrIQjWAVn7bcGAey0nsMZbWwBh38nCfRuk4Zh5AR2OvAmYMgFhT8s2kLxUhcrcMzJJpKVF15gRx1xwB4caFMVjtRNulVutR7acOJTVpftf-b2A8nWUiB_EdKTFjhOagYDbMFKUEEhWh159xir1YaVG4bu8qqsxroiToAqiB2MdbpxZnAWE8-ByJ_TgYBTFF7mu2WxTxT58cslGVR1B5EXlg-9RuzJFnE8uxgcwzWyN_lQK6DbIEV9Ge1rQp8HczxZ6Ly1W7OOZ1t8TNaljH0Aq_ZAvuZY-9L5N_pnfzLyk4rhLOsw-W3dk8y2R4jKAMch8QS4SEPqbvo6S7IXCMcEvS2qlWXYaDMVPpybtHPQRXtOUGw.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="480p30",NAME="480p",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=1435252,RESOLUTION=852x480,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="480p30"
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDzVCKhT87Cu678cooYX0w6oTbF6DmpnQ2XeBqjM2ru-PpHq7qOeI-ETldFN3cOQBpLWXv_s0wwNIqXei9MeK-teHn4NBK-U6hAFZjYqhdf2iDMAk8aUhPSXOrU9rXIbnYE1MgJsR0TUTqltBn14ZJAzdkMEfZx1PtaCh2E0HjeR8N4-N8wOP0QV7_rj8MMHKlmhHgPJURRUhza3RlIzrbzLyGAgCV64RxpyEsRFrysYxHph9X1yWygwYUyUz-ER6Byj6Ko55qcIUMEDLoR9IhV87V9pFFPhhk0YBYnpQYOdsmMCkRn5ZsRElAld22IuIVAPVEMhOfSQX_Pu2exBCcUIdQ_Sglj_nfWP6_FiGSCvN3LPtD3ZNgU8TXED9PxElHZzqUBU2iSoxxLBoE8C7Vd6BpDeepDuH1jkBxb-WwzMUs7pIdnrB98BNKQgYhMEUiQZARhpyG8NeGhOoMIfXcnLDQw6XmD7PZTM6HXOSGNtIlM8jdZUiDCEBwYMwfDXOoDvGuY9MBDXaIw_uFiWa5CiU44awF1v3-EHmloFUGVQSTS1xHUjywTtC5KUsGPVPjHoShKCWUSRRLaiHVVbXLo0oGXFxuSxiIFzLUUeKa7TNtbcHpyl1aIVgpe0iVPjsCE4duOcWfN0bJeqcSEFSTU15DhJrDLlqrysEI3ssaDLrB_bSkpe3nv55Saw.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="360p30",NAME="360p",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=630000,RESOLUTION=640x360,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="360p30"
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDTjryGQpOcbZo5efDAoWz0JI__T4n7owyQCkQkw5otrJdA4MdYnrWx43dC6zmWGnNxTDEoSOJ3d4kKtPyccQc5fZCRIM0IjSJfK4fvD4VwPBf02eCrEREXqchqhiLhN_QBD163OqCAfoEbvifAqef-C9Czhy7Wy3H8kJkHXko3Yho8Mv1u5ENyjGlRaKBECbZavuTxjbkgzp_mbk60h8sYWbMFnT0xAVb3GEI59NRdd-0bvnu6eEuxJKWaEpqSrJ_m0G77afZ6yFr-aEUf92aEn_azpaFcUDs3Tt_6yub-zrQqtKluRlo25g14YZ18qJhab8FFoaRVKVzZaNZYVBLlf-5Bh8btnpNu8dg9SQe5IbHi6Wp4YSFhWK0BMO49FVdVUDSzxNFKHwiFFLr6kRMC5maVOxmvDY1bhLQjqge1yjVps82ByVasINeq9bxjkWILMRIWbOp8U_0wGXJAN_B7HE1mPYRQ7sfFaK5tQd0Qf_JVWdYZRzd9TvGJv2xgNEUCnbmNuzF2i52zwHTYoigE3YAJkT_zZb3PzWreyEGgoCd30GSfh9FOmeMnNPnb4EkSTxSYyDgDNQhSzmY1tpT9sGhDA_7biNX4vSUVka8zwfd79dTtv-jfuw3BrKxDARbW7aCSahXb7XCRU8SEOeSFUU-PulEu72Hg37qca8aDFFmPsBvds0OR-h-ig.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="160p30",NAME="160p",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=230000,RESOLUTION=284x160,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="160p30"
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDgryHl1FRa03tGi6OqC7QbH2IjFzL5P73F5_wjyigOYsZl9IQeDTCNcb3KdPYLWViVYOkblpTiWSZIurBjsLybPM5CyG8ajiQdJmcokxSWuF0JCxKW3B9k4Jzd7kjAcTG2m6Q6AgAVUl-12pmDLPiqJdS9hOmHBBz8Jfqb-nI2CQn96nHXOAv0tn9HDToE_070JGD8IdSMXM1ha8bhASa7O5ykIYR_YsGgcQ8ifWvTikPOWbRel7XAo1ABjSTbhPFhwuhdBuS2R3CcDHNqbmHLbEdQkmi9K2ibgQ9Yq7XkjziTNpFHxh2FuwWcRSgPupd82pVSJIJHAwz9Bz2rSgNnHeZBcCuJ-xa7H2DPyTrWiE915Gr4Sb5xbNMPAf2kIBP-406j3bri43AMTNJIvOOv7E33Gl93iRGdXNE18zi5Bl_mwDAX4_3SE7m10pNRYqduvqgSYBRwSwt2JRPpjzwqL8oLeOeNSGs_N6BFfbkWXT7QZ5oJgZjUFW2RBxVwPnMXuRr1kzw6BD1LrxAZbEQpqcQGKf11AfaAshyT6zKxrpyKh_oC_wl7yu4VOgYeehEezQiSgMyFLkarfOeM6I-o7Usx3jFtChwVQ0QMXq2hGWmy6LMX0d-LrUASBMkxs3Ul7uX2T3gpzqoEFgSEEddVKslKF4gxBbMMRvroOsaDKTtGcbLwO10mWlpbw.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="audio_only",NAME="audio_only",AUTOSELECT=NO,DEFAULT=NO
#EXT-X-STREAM-INF:BANDWIDTH=166907,CODECS="mp4a.40.2",VIDEO="audio_only"
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CvADyY7WIxbtDSJ0njAsWODtSlU3th3xEoqA4xy0hB37DMPbQ-UOcK9ipvr4PSV5JZdacS0zHw1B7M7qMxNkGTz-4GresX5omdJK25JFJ3R57TdES2QqOYr6s9Njiov9VfYboWl93cTLeDC4bOk6jWa7CROjMQOdzZQFs7ItRvKWiIOWHJWmp20pkeGJGzKn7djFb1QYrmhEWpel36muLe62rV1iXDeAw2d6wzNH0cXF-Ub09fIawnTwECXvHaQAYMK6Ij4hqdDchOVewvLJQj7vxDLMQhus6bKPbgulojTO0CLvxHdrGydswSbiYYTib_5nIIuMXTNlZbTQ1vyPJgFZlOUHPDy8znoQanX4YQnTdx-9jQ35YA2AhdKJ6edHF3oKy3c-CDxBJIJavZqsMj2Zc_o6oEUQXJfrKAlmWUaYz0FFm6j8PDRGKxpyaiUYokYZ-W0wx_sNJDKOr_Fd_MN8sRCxwiTFRWJ1ht7tumnfpP177P3n3prplM__3cgleAdVwzULqx_5bjVJYbHkcVxCE0iGz21n2UaOWkW1CaFR9QxQunEfDuiTh_w_Ver3M9kWnFpU8S0LxYtjRNlvqU8o1ds4_vgaPNFXrMaN7fAnoN1ETawu5I0yDAWIX6PclLW6K5fyjSaCiCpk3M8O5KYCTRIQqSaiIAjvvYr_qGRuzVDHrRoMJSflyQR53h9-9ZMn.m3u8

View file

@ -0,0 +1,16 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="Audio1",NAME="mp4a.40.2_96K_Spanish",LANGUAGE="spa",DEFAULT=YES,AUTOSELECT=YES,URI="A1.m3u8"
A1.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=395000,CODECS="avc1.4d001f,mp4a.40.2",AUDIO="Audio1",RESOLUTION=320x240
01.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=394000,CODECS="avc1.4d001f,mp4a.40.2",URI="01_iframe_index.m3u8"
01_iframe_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=963000,CODECS="avc1.4d001f,mp4a.40.2",AUDIO="Audio1",RESOLUTION=448x336
02.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=962000,CODECS="avc1.4d001f,mp4a.40.2",URI="02_iframe_index.m3u8"
02_iframe_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1695000,CODECS="avc1.4d001f,mp4a.40.2",AUDIO="Audio1",RESOLUTION=640x480
03.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1694000,CODECS="avc1.4d001f,mp4a.40.2",URI="03_iframe_index.m3u8"
03_iframe_index.m3u8

View file

@ -0,0 +1,10 @@
#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

View file

@ -1,20 +0,0 @@
#EXTM3U
#EXT-X-TWITCH-INFO:NODE="video47.lhr02",MANIFEST-NODE="video47.lhr02",SERVER-TIME="1463829370.34",USER-IP="145.87.245.122",CLUSTER="lhr02",STREAM-TIME="39299.3432069",MANIFEST-CLUSTER="lhr02"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="Source",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3960281,RESOLUTION=1280x720,CODECS="avc1.4D4029,mp4a.40.2",VIDEO="chunked"
http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/chunked/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=chunked&sig=2491212c28bdc97e74c8755d6cc18f8bdd971f30
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="high",NAME="High",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1760000,RESOLUTION=1280x720,CODECS="avc1.66.31,mp4a.40.2",VIDEO="high"
http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/high/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=high&sig=be34da4d70ab7cabe773f12a37d863565efe2b46
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="medium",NAME="Medium",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=928000,RESOLUTION=852x480,CODECS="avc1.66.31,mp4a.40.2",VIDEO="medium"
http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/medium/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=medium&sig=88ee9e7c5416096599e0b46017d7de82f876b8ae
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Low",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=596000,RESOLUTION=640x360,CODECS="avc1.66.31,mp4a.40.2",VIDEO="low"
http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/low/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=low&sig=79398c8f2e8636720d59aa2310e19eff6ab42d46
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mobile",NAME="Mobile",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=164000,RESOLUTION=400x226,CODECS="avc1.66.31,mp4a.40.2",VIDEO="mobile"
http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/mobile/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=mobile&sig=cc9f889823f25969c8b8d911ea9193dc33ce41dc
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="audio_only",NAME="Audio Only",AUTOSELECT=NO,DEFAULT=NO
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,CODECS="mp4a.40.2",VIDEO="audio_only"
http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/audio_only/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=audio_only&sig=e3179b7cacb9530df951f8d683ff02a493100849

View file

@ -0,0 +1,35 @@
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 2",AUTOSELECT=NO,DEFAULT=NO,URI="alternate_audio_aac_sinewave/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語 (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs"
gear1/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,CODECS="avc1.4d400d",URI="gear1/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs"
gear2/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,CODECS="avc1.4d401e",URI="gear2/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs"
gear3/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,CODECS="avc1.4d401f",URI="gear3/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs"
gear4/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,CODECS="avc1.4d401f",URI="gear4/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs"
gear5/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,CODECS="avc1.4d401f",URI="gear5/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs"
gear0/prog_index.m3u8

View file

@ -0,0 +1,35 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:338559
#EXT-X-KEY:METHOD=AES-128,URI="https://secure.domain.com",IV=0xb059217aa2649ce170b734
#EXTINF:2.002,338559
20140311T113819-01-338559live.ts
#EXTINF:2.002,338560
20140311T113819-01-338560live.ts
#EXTINF:2.002,338561
20140311T113819-01-338561live.ts
#EXTINF:2.002,338562
20140311T113819-01-338562live.ts
#EXTINF:2.002,338563
20140311T113819-01-338563live.ts
#EXTINF:2.002,338564
20140311T113819-01-338564live.ts
#EXTINF:2.002,338565
20140311T113819-01-338565live.ts
#EXTINF:2.002,338566
20140311T113819-01-338566live.ts
#EXTINF:2.002,338567
20140311T113819-01-338567live.ts
#EXTINF:2.002,338568
20140311T113819-01-338568live.ts
#EXTINF:2.002,338569
20140311T113819-01-338569live.ts
#EXTINF:2.002,338570
20140311T113819-01-338570live.ts
#EXTINF:2.002,338571
20140311T113819-01-338571live.ts
#EXTINF:2.002,338572
20140311T113819-01-338572live.ts
#EXTINF:2.002,338573
20140311T113819-01-338573live.ts

View file

@ -0,0 +1,25 @@
#EXTM3U
# Borrowed from https://github.com/grafov/m3u8/pull/83
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:6
#EXTINF:6.000,
1.ts
#EXT-X-DATERANGE:ID="20",START-DATE="2020-06-03T14:56:00Z",PLANNED-DURATION=19,SCTE35-OUT=0xFC302000000000000000FFF00F05000000147FFFFE001A17B0C0000000000061DFD67D
#EXT-X-CUE-OUT:19.0
#EXT-X-PROGRAM-DATE-TIME:2020-06-03T14:56:00Z
#EXTINF:6.000,
2.ts
#EXTINF:6.000,
3.ts
#EXTINF:6.000,
4.ts
#EXT-X-CUE-IN
#EXTINF:6.000,
5.ts
#EXTINF:6.000,
6.ts
#EXTINF:6.000,
7.ts
#EXTINF:6.000,
8.ts

View file

@ -0,0 +1,21 @@
#EXTM3U
#EXTINF:10,
http://media.example.com/fileSequence7796.ts
#EXTINF:6,
http://media.example.com/fileSequence7797.ts
#EXT-X-CUE-OUT:DURATION=30
#EXTINF:4,
http://media.example.com/fileSequence7798.ts
#EXTINF:10,
http://media.example.com/fileSequence7799.ts
#EXTINF:10,
http://media.example.com/fileSequence7800.ts
#EXTINF:6,
http://media.example.com/fileSequence7801.ts
#EXT-X-CUE-IN
#EXTINF:4,
http://media.example.com/fileSequence7802.ts
#EXTINF:10,
http://media.example.com/fileSequence7803.ts
#EXTINF:3,
http://media.example.com/fileSequence7804.ts

View file

@ -1,5 +1,3 @@
# https://developer.apple.com/library/ios/technotes/tn2288/_index.html
#
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
@ -12,4 +10,4 @@ ad1.ts
#EXTINF:10.0,
movieA.ts
#EXTINF:10.0,
movieB.ts
movieB.ts

View file

@ -0,0 +1,30 @@
#EXTM3U
#EXT-X-TARGETDURATION:11
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:9.00000,
#EXT-X-BYTERANGE:86920@0
main.aac
#EXTINF:10.00000,
#EXT-X-BYTERANGE:136595@86920
main.aac
#EXTINF:9.00000,
#EXT-X-BYTERANGE:136567@223515
main.aac
#EXTINF:10.00000,
#EXT-X-BYTERANGE:136954@360082
main.aac
#EXTINF:10.00000,
#EXT-X-BYTERANGE:137116@497036
main.aac
#EXTINF:9.00000,
#EXT-X-BYTERANGE:136770@634152
main.aac
#EXTINF:10.00000,
#EXT-X-BYTERANGE:137219@770922
main.aac
#EXTINF:10.00000,
#EXT-X-BYTERANGE:137132@908141
main.acc
#EXT-X-ENDLIST

View file

@ -1,14 +1,11 @@
//! A library to parse m3u8 playlists (HTTP Live Streaming) [link]
//! (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19).
//! A library to parse m3u8 playlists [HTTP Live Streaming](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19).
//!
//! #Examples
//! # Examples
//!
//! Parsing a playlist and let the parser figure out if it's a media or master playlist.
//!
//! ```
//! extern crate m3u8_rs;
//! extern crate nom;
//! use m3u8_rs::playlist::Playlist;
//! use m3u8_rs::Playlist;
//! use nom::IResult;
//! use std::io::Read;
//!
@ -16,27 +13,16 @@
//! let mut bytes: Vec<u8> = Vec::new();
//! file.read_to_end(&mut bytes).unwrap();
//!
//! // Option 1: fn parse_playlist_res(input) -> Result<Playlist, _>
//! match m3u8_rs::parse_playlist_res(&bytes) {
//! Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
//! Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
//! Err(e) => println!("Error: {:?}", e)
//! }
//!
//! // Option 2: fn parse_playlist(input) -> IResult<_, Playlist, _>
//! match m3u8_rs::parse_playlist(&bytes) {
//! IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
//! IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
//! IResult::Error(e) => panic!("Parsing error: \n{}", e),
//! IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl),
//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl),
//! Result::Err(e) => panic!("Parsing error: \n{}", e),
//! }
//! ```
//!
//! Parsing a master playlist directly
//!
//! ```
//! extern crate m3u8_rs;
//! extern crate nom;
//! use std::io::Read;
//! use nom::IResult;
//!
@ -44,442 +30,72 @@
//! let mut bytes: Vec<u8> = Vec::new();
//! file.read_to_end(&mut bytes).unwrap();
//!
//! if let IResult::Done(_, pl) = m3u8_rs::parse_master_playlist(&bytes) {
//! println!("{}", pl);
//! if let Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) {
//! println!("{:?}", pl);
//! }
//! ```
//!
//! Creating a playlist and writing it back to a vec/file
//!
//! ```
//! use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment};
//!
//! let playlist = MediaPlaylist {
//! version: Some(6),
//! target_duration: 3,
//! media_sequence: 338559,
//! discontinuity_sequence: 1234,
//! end_list: true,
//! playlist_type: Some(MediaPlaylistType::Vod),
//! segments: vec![
//! MediaSegment {
//! uri: "20140311T113819-01-338559live.ts".into(),
//! duration: 2.002,
//! title: Some("title".into()),
//! ..Default::default()
//! },
//! ],
//! ..Default::default()
//! };
//!
//! //let mut v: Vec<u8> = Vec::new();
//! //playlist.write_to(&mut v).unwrap();
//!
//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap();
//! //playlist.write_to(&mut file).unwrap();
//! ```
//!
//! Controlling the output precision for floats, such as #EXTINF (default is unset)
//!
//! ```
//! use std::sync::atomic::Ordering;
//! use m3u8_rs::{WRITE_OPT_FLOAT_PRECISION, MediaPlaylist, MediaSegment};
//!
//! WRITE_OPT_FLOAT_PRECISION.store(5, Ordering::Relaxed);
//!
//! let playlist = MediaPlaylist {
//! target_duration: 3,
//! segments: vec![
//! MediaSegment {
//! duration: 2.9,
//! title: Some("title".into()),
//! ..Default::default()
//! },
//! ],
//! ..Default::default()
//! };
//!
//! let mut v: Vec<u8> = Vec::new();
//!
//! playlist.write_to(&mut v).unwrap();
//! let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
//! assert!(m3u8_str.contains("#EXTINF:2.90000,title"));
#[macro_use]
extern crate nom;
mod playlist;
pub use playlist::*;
pub mod playlist;
#[cfg(feature = "parser")]
mod parser;
use nom::*;
use std::str;
use std::f32;
use std::string;
use std::str::FromStr;
use std::result::Result;
use std::collections::HashMap;
use playlist::*;
// -----------------------------------------------------------------------------------------------
// Playlist parser
// -----------------------------------------------------------------------------------------------
/// Parse a m3u8 playlist.
///
/// #Examples
///
/// let mut file = std::fs::File::open("playlist.m3u8").unwrap();
/// let mut bytes: Vec<u8> = Vec::new();
/// file.read_to_end(&mut bytes).unwrap();
///
/// let parsed = m3u8_rs::parse_playlist(&bytes);
///
/// let playlist = match parsed {
/// IResult::Done(i, playlist) => playlist,
/// IResult::Error(e) => panic!("Parsing error: \n{}", e),
/// IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
/// };
///
/// match playlist {
/// Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{}", pl),
/// Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{}", pl),
/// }
pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> {
match is_master_playlist(input) {
true => parse_master_playlist(input).map(Playlist::MasterPlaylist),
false => parse_media_playlist(input).map(Playlist::MediaPlaylist),
}
}
/// Parse a m3u8 playlist just like `parse_playlist`. This returns a Result<PLaylist,_>.
///
/// #Examples
///
/// ```
/// use m3u8_rs::playlist::{Playlist};
/// use std::io::Read;
///
/// let mut file = std::fs::File::open("playlist.m3u8").unwrap();
/// let mut bytes: Vec<u8> = Vec::new();
/// file.read_to_end(&mut bytes).unwrap();
///
/// let parsed = m3u8_rs::parse_playlist_res(&bytes);
///
/// match parsed {
/// Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
/// Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
/// Err(e) => println!("Error: {:?}", e)
/// }
/// ```
pub fn parse_playlist_res(input: &[u8]) -> Result<Playlist, IResult<&[u8], Playlist>> {
let parse_result = parse_playlist(input);
match parse_result {
IResult::Done(_, playlist) => Ok(playlist),
_ => Err(parse_result),
}
}
/// Parse input as a master playlist
pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> {
parse_master_playlist_tags(input).map(MasterPlaylist::from_tags)
}
/// Parse input as a media playlist
pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> {
parse_media_playlist_tags(input).map(MediaPlaylist::from_tags)
}
/// When a media tag or no master tag is found, this returns false.
pub fn is_master_playlist(input: &[u8]) -> bool {
// Assume it's not a master playlist
contains_master_tag(input).map(|t| t.0).unwrap_or(false)
}
/// Scans input looking for either a master or media `#EXT` tag.
///
/// Returns `Some(true/false)` when a master/media tag is found. Otherwise returns `None`.
///
/// - None: Unkown tag or empty line
/// - Some(true, tagstring): Line contains a master playlist tag
/// - Some(false, tagstring): Line contains a media playlist tag
pub fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> {
let mut is_master_opt = None;
let mut current_input: &[u8] = input;
while is_master_opt == None {
match is_master_playlist_tag_line(current_input) {
IResult::Done(rest, result) => {
current_input = rest;
is_master_opt = result; // result can be None (no media or master tag found)
}
_ => break, // Parser error encountered, can't read any more lines.
}
}
is_master_opt
}
named!(pub is_master_playlist_tag_line(&[u8]) -> Option<(bool, String)>,
chain!(
tag: opt!(alt!(
map!(tag!("#EXT-X-STREAM-INF"), |t| (true, t))
| map!(tag!("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t))
| map!(tag!("#EXT-X-MEDIA"), |t| (true, t))
| map!(tag!("#EXT-X-SESSION-KEY"), |t| (true, t))
| map!(tag!("#EXT-X-SESSION-DATA"), |t| (true, t))
| map!(tag!("#EXT-X-TARGETDURATION"), |t| (false, t))
| map!(tag!("#EXT-X-MEDIA-SEQUENCE"), |t| (false, t))
| map!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE"), |t| (false, t))
| map!(tag!("#EXT-X-ENDLIST"), |t| (false, t))
| map!(tag!("#EXT-X-PLAYLIST-TYPE"), |t| (false, t))
| map!(tag!("#EXT-X-I-FRAMES-ONLY"), |t| (false, t))
| map!(tag!("#EXTINF"), |t| (false, t))
| map!(tag!("#EXT-X-BYTERANGE"), |t| (false, t))
| map!(tag!("#EXT-X-DISCONTINUITY"), |t| (false, t))
| map!(tag!("#EXT-X-KEY"), |t| (false, t))
| map!(tag!("#EXT-X-MAP"), |t| (false, t))
| map!(tag!("#EXT-X-PROGRAM-DATE-TIME"), |t| (false, t))
| map!(tag!("#EXT-X-DATERANGE"), |t| (false, t))
))
~ consume_line
, || {
tag.map(|(a,b)| (a, from_utf8_slice(b).unwrap()))
}
)
);
// -----------------------------------------------------------------------------------------------
// Master Playlist Tags
// -----------------------------------------------------------------------------------------------
pub fn parse_master_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec<MasterPlaylistTag>> {
chain!(input,
mut tags: many0!(chain!(m:master_playlist_tag ~ multispace?, || m)) ~ eof,
|| { tags.reverse(); tags }
)
}
/// Contains all the tags required to parse a master playlist.
#[derive(Debug)]
pub enum MasterPlaylistTag {
M3U(String),
Version(usize),
VariantStream(VariantStream),
AlternativeMedia(AlternativeMedia),
SessionData(SessionData),
SessionKey(SessionKey),
Start(Start),
IndependentSegments,
Unknown(ExtTag),
Comment(String),
Uri(String),
}
pub fn master_playlist_tag(input: &[u8]) -> IResult<&[u8], MasterPlaylistTag> {
alt!(input,
map!(m3u_tag, MasterPlaylistTag::M3U)
| map!(version_tag, MasterPlaylistTag::Version)
| map!(variant_stream_tag, MasterPlaylistTag::VariantStream)
| map!(variant_i_frame_stream_tag, MasterPlaylistTag::VariantStream)
| map!(alternative_media_tag, MasterPlaylistTag::AlternativeMedia)
| map!(session_data_tag, MasterPlaylistTag::SessionData)
| map!(session_key_tag, MasterPlaylistTag::SessionKey)
| map!(start_tag, MasterPlaylistTag::Start)
| map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MasterPlaylistTag::IndependentSegments)
| map!(ext_tag, MasterPlaylistTag::Unknown)
| map!(comment_tag, MasterPlaylistTag::Comment)
| map!(consume_line, MasterPlaylistTag::Uri)
)
}
named!(pub variant_stream_tag<VariantStream>,
chain!(tag!("#EXT-X-STREAM-INF:") ~ attributes: key_value_pairs,
|| VariantStream::from_hashmap(attributes, false))
);
named!(pub variant_i_frame_stream_tag<VariantStream>,
chain!( tag!("#EXT-X-I-FRAME-STREAM-INF:") ~ attributes: key_value_pairs,
|| VariantStream::from_hashmap(attributes, true))
);
named!(pub alternative_media_tag<AlternativeMedia>,
chain!( tag!("#EXT-X-MEDIA:") ~ attributes: key_value_pairs,
|| AlternativeMedia::from_hashmap(attributes))
);
named!(pub session_data_tag<SessionData>,
chain!( tag!("#EXT-X-SESSION-DATA:") ~ attributes: key_value_pairs,
|| SessionData::from_hashmap(attributes))
);
named!(pub session_key_tag<SessionKey>,
chain!( tag!("#EXT-X-SESSION-KEY:") ~ session_key: map!(key, SessionKey),
|| session_key)
);
// -----------------------------------------------------------------------------------------------
// Media Playlist
// -----------------------------------------------------------------------------------------------
pub fn parse_media_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec<MediaPlaylistTag>> {
chain!(input,
mut tags: many0!(chain!(m:media_playlist_tag ~ multispace?, || m)) ~ eof,
|| { tags.reverse(); tags }
)
}
/// Contains all the tags required to parse a media playlist.
#[derive(Debug)]
pub enum MediaPlaylistTag {
M3U(String),
Version(usize),
Segment(SegmentTag),
TargetDuration(f32),
MediaSequence(i32),
DiscontinuitySequence(i32),
EndList,
PlaylistType(MediaPlaylistType),
IFramesOnly,
Start(Start),
IndependentSegments,
}
pub fn media_playlist_tag(input: &[u8]) -> IResult<&[u8], MediaPlaylistTag> {
alt!(input,
map!(m3u_tag, MediaPlaylistTag::M3U)
| map!(version_tag, MediaPlaylistTag::Version)
| map!(chain!(tag!("#EXT-X-TARGETDURATION:") ~ n:float,||n), MediaPlaylistTag::TargetDuration)
| map!(chain!(tag!("#EXT-X-MEDIA-SEQUENCE:") ~ n:number,||n), MediaPlaylistTag::MediaSequence)
| map!(chain!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE:") ~ n:number,||n), MediaPlaylistTag::DiscontinuitySequence)
| map!(playlist_type_tag, MediaPlaylistTag::PlaylistType)
| map!(tag!("#EXT-X-I-FRAMES-ONLY"), |_| MediaPlaylistTag::IFramesOnly)
| map!(start_tag, MediaPlaylistTag::Start)
| map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MediaPlaylistTag::IndependentSegments)
| map!(media_segment_tag, MediaPlaylistTag::Segment)
)
}
named!(pub playlist_type_tag<MediaPlaylistType>,
map_res!(
map_res!(tag!("#EXT-X-PLAYLIST-TYPE:"), str::from_utf8),
MediaPlaylistType::from_str)
);
// -----------------------------------------------------------------------------------------------
// Media Segment
// -----------------------------------------------------------------------------------------------
/// All possible media segment tags.
#[derive(Debug)]
pub enum SegmentTag {
Extinf(f32, Option<String>),
ByteRange(ByteRange),
Discontinuity,
Key(Key),
Map(Map),
ProgramDateTime(String),
DateRange(String),
Unknown(ExtTag),
Comment(String),
Uri(String),
}
pub fn media_segment_tag(input: &[u8]) -> IResult<&[u8], SegmentTag> {
alt!(input,
map!(chain!(tag!("#EXTINF:") ~ e:duration_title_tag,||e), |(a,b)| SegmentTag::Extinf(a,b))
| map!(chain!(tag!("#EXT-X-BYTERANGE:") ~ r:byterange_val, || r), SegmentTag::ByteRange)
| map!(tag!("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity)
| map!(chain!(tag!("#EXT-X-KEY:") ~ k:key, || k), SegmentTag::Key)
| map!(chain!(tag!("#EXT-X-MAP:") ~ m:map, || m), SegmentTag::Map)
| map!(chain!(tag!("#EXT-X-PROGRAM-DATE-TIME:") ~ t:consume_line, || t), SegmentTag::ProgramDateTime)
| map!(chain!(tag!("#EXT-X-DATE-RANGE:") ~ t:consume_line, || t), SegmentTag::DateRange)
| map!(ext_tag, SegmentTag::Unknown)
| map!(comment_tag, SegmentTag::Comment)
| map!(consume_line, SegmentTag::Uri)
)
}
named!(pub duration_title_tag<(f32, Option<String>)>,
chain!(
duration: float
~ tag!(",")?
~ title: opt!(map_res!(take_until_and_consume!("\r\n"), from_utf8_slice))
~ tag!(",")?
,
|| (duration, title)
)
);
named!(pub key<Key>, map!(key_value_pairs, Key::from_hashmap));
named!(pub map<Map>,
chain!(
uri: quoted ~ range: opt!(chain!(char!(',') ~ b:byterange_val,||b )),
|| Map { uri: uri, byterange: range }
)
);
// -----------------------------------------------------------------------------------------------
// Basic tags
// -----------------------------------------------------------------------------------------------
named!(pub m3u_tag<String>,
map_res!(tag!("#EXTM3U"), from_utf8_slice)
);
named!(pub version_tag<usize>,
chain!(
tag!("#EXT-X-VERSION:") ~ version: map_res!(digit, str::from_utf8),
|| version.parse().unwrap() //TODO: or return a default value?
)
);
named!(pub start_tag<Start>,
chain!(tag!("#EXT-X-START:") ~ attributes:key_value_pairs, || Start::from_hashmap(attributes))
);
named!(pub ext_tag<ExtTag>,
chain!(
tag!("#EXT-")
~ tag: map_res!(take_until_and_consume!(":"), from_utf8_slice)
~ rest: map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice)
,
|| ExtTag { tag: tag, rest: rest }
)
);
named!(pub comment_tag<String>,
chain!(
tag!("#") ~ text: map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice),
|| text
)
);
// -----------------------------------------------------------------------------------------------
// Util
// -----------------------------------------------------------------------------------------------
named!(pub key_value_pairs(&[u8]) -> HashMap<String, String>,
map!(
many0!(chain!(space? ~ k:key_value_pair,|| k))
,
|pairs: Vec<(String, String)>| {
pairs.into_iter().collect()
}
)
);
named!(pub key_value_pair(&[u8]) -> (String, String),
chain!(
peek!(none_of!("\r\n"))
~ left: map_res!(take_until_and_consume!("="), from_utf8_slice)
~ right: alt!(quoted | unquoted)
~ char!(',')?
,
|| (left, right)
)
);
named!(pub quoted<String>,
delimited!(char!('\"'), map_res!(is_not!("\""), from_utf8_slice), char!('\"'))
);
named!(pub unquoted<String>,
map_res!(take_until_either!(",\r\n"), from_utf8_slice)
);
named!(pub consume_line<String>,
map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice)
);
named!(pub number<i32>,
map_res!(map_res!(digit, str::from_utf8), str::FromStr::from_str)
);
named!(pub byterange_val<ByteRange>,
chain!(
n: number
~ o: opt!(chain!(char!('@') ~ n:number,||n))
,
|| ByteRange { length: n, offset: o }
)
);
named!(pub float<f32>,
chain!(
left: map_res!(digit, str::from_utf8)
~ right_opt: opt!(chain!(char!('.') ~ d:map_res!(digit, str::from_utf8),|| d )),
||
match right_opt {
Some(right) => {
let mut num = String::from(left);
num.push('.');
num.push_str(right);
num.parse().unwrap()
},
None => left.parse().unwrap(),
}
)
);
pub fn from_utf8_slice(s: &[u8]) -> Result<String, string::FromUtf8Error> {
String::from_utf8(s.to_vec())
}
pub fn from_utf8_slice2(s: &[u8]) -> Result<String, str::Utf8Error> {
str::from_utf8(s).map(String::from)
}
#[cfg(feature = "parser")]
pub use self::parser::*;

1004
src/parser.rs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,20 @@
#![allow(unused_variables, unused_imports, dead_code)]
#[macro_use]
extern crate nom;
extern crate m3u8_rs;
use std::fs;
use std::path;
use chrono::prelude::*;
use m3u8_rs::QuotedOrUnquoted::Quoted;
use m3u8_rs::*;
use std::io::Read;
use nom::*;
use nom::AsBytes;
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path;
use std::sync::atomic::Ordering;
use std::{fs, io};
fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
fs::read_dir("sample-playlists\\").unwrap()
let path: path::PathBuf = ["sample-playlists"].iter().collect();
fs::read_dir(path.to_str().unwrap())
.unwrap()
.filter_map(Result::ok)
.map(|dir| dir.path())
.filter(|path| path.extension().map_or(false, |ext| ext == "m3u8"))
@ -21,51 +23,55 @@ fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
fn getm3u(path: &str) -> String {
let mut buf = String::new();
let mut file = fs::File::open(path).expect("Can't find m3u8.");
let mut file = File::open(path).unwrap_or_else(|_| panic!("Can't find m3u8: {}", path));
let u = file.read_to_string(&mut buf).expect("Can't read file");
buf
}
fn get_sample_playlist(name: &str) -> String {
getm3u(&(String::from("sample-playlists\\") + name))
let path: path::PathBuf = ["sample-playlists", name].iter().collect();
getm3u(path.to_str().unwrap())
}
// -----------------------------------------------------------------------------------------------
// Playlist
fn print_parse_playlist_test(playlist_name: &str) -> bool {
let input = get_sample_playlist(playlist_name);
let input: String = get_sample_playlist(playlist_name);
println!("Parsing playlist file: {:?}", playlist_name);
let parsed = parse_playlist(input.as_bytes());
if let IResult::Done(i,o) = parsed {
println!("{}", o);
if let Ok((i, o)) = &parsed {
println!("{:?}", o);
true
}
else {
} else {
println!("Parsing failed:\n {:?}", parsed);
false
}
}
#[test]
fn playlis_master_usher_result() {
assert!(print_parse_playlist_test("master-usher_result.m3u8"));
}
#[test]
fn playlist_master_with_alternatives() {
assert!(print_parse_playlist_test("master-with-alternatives.m3u8"));
}
#[test]
fn playlist_master_with_alternatives_2_3() {
assert!(print_parse_playlist_test("master-with-alternatives-2.m3u8"));
}
#[test]
fn playlist_master_with_i_frame_stream_inf() {
assert!(print_parse_playlist_test("master-with-i-frame-stream-inf.m3u8"));
assert!(print_parse_playlist_test(
"master-with-i-frame-stream-inf.m3u8"
));
}
#[test]
fn playlist_master_with_multiple_codecs() {
assert!(print_parse_playlist_test("master-with-multiple-codecs.m3u8"));
assert!(print_parse_playlist_test(
"master-with-multiple-codecs.m3u8"
));
}
// -- Media playlists
@ -77,7 +83,55 @@ fn playlist_media_standard() {
#[test]
fn playlist_media_without_segments() {
assert!(print_parse_playlist_test("media-playlist-without-segments.m3u8"));
assert!(print_parse_playlist_test(
"media-playlist-without-segments.m3u8"
));
}
#[test]
fn playlist_media_with_cues() {
assert!(print_parse_playlist_test("media-playlist-with-cues.m3u8"));
}
#[test]
fn playlist_media_with_cues1() {
assert!(print_parse_playlist_test("media-playlist-with-cues-1.m3u8"));
}
#[test]
fn playlist_media_with_scte35() {
assert!(print_parse_playlist_test("media-playlist-with-scte35.m3u8"));
}
#[test]
fn playlist_media_with_scte35_1() {
assert!(print_parse_playlist_test(
"media-playlist-with-scte35-1.m3u8"
));
}
// -----------------------------------------------------------------------------------------------
// Playlist with no newline end
#[test]
fn playlist_not_ending_in_newline_master() {
assert!(print_parse_playlist_test(
"master-not-ending-in-newline.m3u8"
));
}
#[test]
fn playlist_not_ending_in_newline_master1() {
assert!(print_parse_playlist_test(
"master-not-ending-in-newline-1.m3u8"
));
}
#[test]
fn playlist_not_ending_in_newline_media() {
assert!(print_parse_playlist_test(
"media-not-ending-in-newline.m3u8"
));
}
// -----------------------------------------------------------------------------------------------
@ -87,16 +141,16 @@ fn playlist_media_without_segments() {
fn playlist_type_is_master() {
let input = get_sample_playlist("master.m3u8");
let result = is_master_playlist(input.as_bytes());
assert_eq!(true, result);
assert!(result);
}
#[test]
fn playlist_type_with_unkown_tag() {
let input = get_sample_playlist("master-usher_result.m3u8");
let result = is_master_playlist(input.as_bytes());
println!("Playlist_type_with_unkown_tag is master playlist: {:?}", result);
assert_eq!(true, result);
}
// #[test]
// fn playlist_type_with_unknown_tag() {
// let input = get_sample_playlist("!!");
// let result = is_master_playlist(input.as_bytes());
// println!("Playlist_type_with_unknown_tag is master playlist: {:?}", result);
// assert_eq!(true, result);
// }
#[test]
fn playlist_types() {
@ -105,115 +159,314 @@ fn playlist_types() {
let input = getm3u(path);
let is_master = is_master_playlist(input.as_bytes());
assert!(path.to_lowercase().contains("master") == is_master);
println!("{:?} = {:?}", path, is_master);
assert_eq!(path.to_lowercase().contains("master"), is_master);
}
}
// -----------------------------------------------------------------------------------------------
// Variant
// Creating playlists
#[test]
fn variant_stream() {
let input = b"#EXT-X-STREAM-INF:BANDWIDTH=300000,CODECS=\"xxx\"\n";
let result = variant_stream_tag(input);
println!("{:?}", result);
}
fn print_create_and_parse_playlist(playlist_original: &mut Playlist) -> Playlist {
let mut utf8: Vec<u8> = Vec::new();
playlist_original.write_to(&mut utf8).unwrap();
let m3u8_str: &str = std::str::from_utf8(&utf8).unwrap();
// -----------------------------------------------------------------------------------------------
// Other
let playlist_parsed = match *playlist_original {
Playlist::MasterPlaylist(_) => {
Playlist::MasterPlaylist(parse_master_playlist_res(m3u8_str.as_bytes()).unwrap())
}
Playlist::MediaPlaylist(_) => {
Playlist::MediaPlaylist(parse_media_playlist_res(m3u8_str.as_bytes()).unwrap())
}
};
#[test]
fn test_key_value_pairs_trailing_equals() {
let res = key_value_pairs(b"BANDWIDTH=395000,CODECS=\"avc1.4d001f,mp4a.40.2\"\r\nrest=");
println!("{:?}\n\n", res);
print!("\n\n---- utf8 result\n\n{}", m3u8_str);
print!("\n---- Original\n\n{:?}", playlist_original);
print!("\n\n---- Parsed\n\n{:?}\n\n", playlist_parsed);
playlist_parsed
}
#[test]
fn test_key_value_pairs_multiple_quoted_values() {
assert_eq!(
key_value_pairs(b"BANDWIDTH=86000,URI=\"low/iframe.m3u8\",PROGRAM-ID=1,RESOLUTION=\"1x1\",VIDEO=1\nrest"),
IResult::Done(
"\nrest".as_bytes(),
vec![
("BANDWIDTH".to_string(), "86000".to_string()),
("URI".to_string(), "low/iframe.m3u8".to_string()),
("PROGRAM-ID".to_string(), "1".to_string()),
("RESOLUTION".to_string(), "1x1".to_string()),
("VIDEO".to_string(), "1".to_string())
].into_iter().collect::<HashMap<String,String>>()
)
);
fn create_and_parse_master_playlist_empty() {
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
..Default::default()
});
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
assert_eq!(playlist_original, playlist_parsed);
}
#[test]
fn test_key_value_pairs_quotes() {
let res = key_value_pairs(b"BANDWIDTH=300000,CODECS=\"avc1.42c015,mp4a.40.2\"\r\nrest");
println!("{:?}\n\n", res);
fn create_segment_float_inf() {
let playlist = Playlist::MediaPlaylist(MediaPlaylist {
version: Some(6),
target_duration: 3,
media_sequence: 338559,
discontinuity_sequence: 1234,
end_list: true,
playlist_type: Some(MediaPlaylistType::Vod),
segments: vec![MediaSegment {
uri: "20140311T113819-01-338559live.ts".into(),
duration: 2.000f32,
title: Some("title".into()),
..Default::default()
}],
..Default::default()
});
let mut v: Vec<u8> = Vec::new();
playlist.write_to(&mut v).unwrap();
let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
assert!(m3u8_str.contains("#EXTINF:2,title"));
WRITE_OPT_FLOAT_PRECISION.store(5, Ordering::Relaxed);
playlist.write_to(&mut v).unwrap();
let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
assert!(m3u8_str.contains("#EXTINF:2.00000,title"));
}
#[test]
fn test_key_value_pairs() {
let res = key_value_pairs(b"BANDWIDTH=300000,RESOLUTION=22x22,VIDEO=1\r\nrest=");
println!("{:?}\n\n", res);
}
#[test]
fn test_key_value_pair() {
assert_eq!(
key_value_pair(b"PROGRAM-ID=1,rest"),
IResult::Done(
"rest".as_bytes(),
("PROGRAM-ID".to_string(), "1".to_string())
)
);
fn create_and_parse_master_playlist_full() {
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
version: Some(6),
alternatives: vec![
AlternativeMedia {
media_type: AlternativeMediaType::Audio,
uri: Some("alt-media-uri".into()),
group_id: "group-id".into(),
language: Some("language".into()),
assoc_language: Some("assoc-language".into()),
name: "Xmedia".into(),
default: true, // Its absence indicates an implicit value of NO
autoselect: true, // Its absence indicates an implicit value of NO
forced: false, // Its absence indicates an implicit value of NO
instream_id: None,
characteristics: Some("characteristics".into()),
channels: Some("channels".into()),
other_attributes: Default::default(),
},
AlternativeMedia {
media_type: AlternativeMediaType::Subtitles,
uri: Some("alt-media-uri".into()),
group_id: "group-id".into(),
language: Some("language".into()),
assoc_language: Some("assoc-language".into()),
name: "Xmedia".into(),
default: true, // Its absence indicates an implicit value of NO
autoselect: true, // Its absence indicates an implicit value of NO
forced: true, // Its absence indicates an implicit value of NO
instream_id: None,
characteristics: Some("characteristics".into()),
channels: Some("channels".into()),
other_attributes: Default::default(),
},
AlternativeMedia {
media_type: AlternativeMediaType::ClosedCaptions,
uri: None,
group_id: "group-id".into(),
language: Some("language".into()),
assoc_language: Some("assoc-language".into()),
name: "Xmedia".into(),
default: true, // Its absence indicates an implicit value of NO
autoselect: true, // Its absence indicates an implicit value of NO
forced: false, // Its absence indicates an implicit value of NO
instream_id: Some(InstreamId::CC(1)),
characteristics: Some("characteristics".into()),
channels: Some("channels".into()),
other_attributes: Default::default(),
},
],
variants: vec![VariantStream {
is_i_frame: false,
uri: "masterplaylist-uri".into(),
bandwidth: 10010010,
average_bandwidth: Some(10010010),
codecs: Some("TheCODEC".into()),
resolution: Some(Resolution {
width: 1000,
height: 3000,
}),
frame_rate: Some(60.0),
hdcp_level: Some(HDCPLevel::None),
audio: Some("audio".into()),
video: Some("video".into()),
subtitles: Some("subtitles".into()),
closed_captions: Some(ClosedCaptionGroupId::GroupId("closed_captions".into())),
other_attributes: Default::default(),
}],
session_data: vec![SessionData {
data_id: "****".into(),
field: SessionDataField::Value("%%%%".to_string()),
language: Some("SessionDataLanguage".into()),
other_attributes: Default::default(),
}],
session_key: vec![SessionKey(Key {
method: KeyMethod::AES128,
uri: Some("https://secure.domain.com".into()),
iv: Some("0xb059217aa2649ce170b734".into()),
keyformat: Some("xXkeyformatXx".into()),
keyformatversions: Some("xXFormatVers".into()),
})],
start: Some(Start {
time_offset: "123123123".parse().unwrap(),
precise: Some(true),
other_attributes: Default::default(),
}),
independent_segments: true,
unknown_tags: vec![],
});
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
assert_eq!(playlist_original, playlist_parsed);
}
#[test]
fn comment() {
assert_eq!(
comment_tag(b"#Hello\nxxx"),
IResult::Done("xxx".as_bytes(), "Hello".to_string())
);
fn create_and_parse_media_playlist_empty() {
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
..Default::default()
});
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
assert_eq!(playlist_original, playlist_parsed);
}
#[test]
fn quotes() {
assert_eq!(
quoted(b"\"value\"rest"),
IResult::Done("rest".as_bytes(), "value".to_string())
);
fn create_and_parse_media_playlist_single_segment() {
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
target_duration: 2,
segments: vec![MediaSegment {
uri: "20140311T113819-01-338559live.ts".into(),
duration: 2.002,
title: Some("hey".into()),
..Default::default()
}],
..Default::default()
});
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
assert_eq!(playlist_original, playlist_parsed);
}
#[test]
fn consume_empty_line() {
let line = consume_line(b"\r\nrest");
println!("{:?}", line);
fn create_and_parse_media_playlist_full() {
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
version: Some(4),
target_duration: 3,
media_sequence: 338559,
discontinuity_sequence: 1234,
end_list: true,
playlist_type: Some(MediaPlaylistType::Vod),
i_frames_only: true,
start: Some(Start {
time_offset: "9999".parse().unwrap(),
precise: Some(true),
other_attributes: Default::default(),
}),
independent_segments: true,
segments: vec![MediaSegment {
uri: "20140311T113819-01-338559live.ts".into(),
duration: 2.002,
title: Some("338559".into()),
byte_range: Some(ByteRange {
length: 137116,
offset: Some(4559),
}),
discontinuity: true,
key: Some(Key {
method: KeyMethod::None,
uri: Some("https://secure.domain.com".into()),
iv: Some("0xb059217aa2649ce170b734".into()),
keyformat: Some("xXkeyformatXx".into()),
keyformatversions: Some("xXFormatVers".into()),
}),
map: Some(Map {
uri: "www.map-uri.com".into(),
byte_range: Some(ByteRange {
length: 137116,
offset: Some(4559),
}),
other_attributes: Default::default(),
}),
program_date_time: Some(
chrono::FixedOffset::east(8 * 3600)
.ymd(2010, 2, 19)
.and_hms_milli(14, 54, 23, 31),
),
daterange: Some(DateRange {
id: "9999".into(),
class: Some("class".into()),
start_date: chrono::FixedOffset::east(8 * 3600)
.ymd(2010, 2, 19)
.and_hms_milli(14, 54, 23, 31),
end_date: None,
duration: None,
planned_duration: Some("40.000".parse().unwrap()),
x_prefixed: Some(HashMap::from([(
"X-client-attribute".into(),
"whatever".into(),
)])),
end_on_next: false,
other_attributes: Default::default(),
}),
unknown_tags: vec![ExtTag {
tag: "X-CUE-OUT".into(),
rest: Some("DURATION=2.002".into()),
}],
..Default::default()
}],
unknown_tags: vec![],
});
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
assert_eq!(playlist_original, playlist_parsed);
}
//
// Roundtrip
#[test]
fn parsing_write_to_should_produce_the_same_structure() {
for playlist in all_sample_m3u_playlists() {
let input = getm3u(playlist.to_str().unwrap());
let expected = parse_playlist_res(input.as_bytes()).unwrap();
let mut written: Vec<u8> = Vec::new();
expected.write_to(&mut written).unwrap();
let actual = parse_playlist_res(&written).unwrap();
assert_eq!(
expected,
actual,
"\n\nFailed parser input:\n\n{}\n\nOriginal input:\n\n{}",
std::str::from_utf8(&written).unwrap(),
input
);
}
}
// Failure on arbitrary text files that don't start with #EXTM3U8
#[test]
fn parsing_text_file_should_fail() {
let s = "
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
";
let res = parse_master_playlist_res(s.as_bytes());
assert!(res.is_err());
}
#[test]
fn float_() {
assert_eq!(
float(b"33.22rest"),
IResult::Done("rest".as_bytes(), 33.22f32)
);
}
fn parsing_binary_data_should_fail_cleanly() {
let data = (0..1024).map(|i| (i % 255) as u8).collect::<Vec<u8>>();
let res = parse_master_playlist_res(&data);
#[test]
fn float_no_decimal() {
assert_eq!(
float(b"33rest"),
IResult::Done("rest".as_bytes(), 33f32)
);
}
#[test]
fn float_should_ignore_trailing_dot() {
assert_eq!(
float(b"33.rest"),
IResult::Done(".rest".as_bytes(), 33f32)
);
assert!(res.is_err());
}