├── .github └── workflows │ └── CI.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── manifest_modifier.png ├── cliff.toml ├── examples ├── api.http └── app │ └── Dockerfile ├── manifest-filter ├── Cargo.toml ├── manifests │ ├── master.m3u8 │ └── media.m3u8 └── src │ └── lib.rs ├── manifest-server ├── Cargo.toml └── src │ └── main.rs └── release.toml /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | 20 | - name: Run cargo check 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | 25 | test: 26 | name: Test Suite 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout sources 30 | uses: actions/checkout@v2 31 | 32 | - name: Install stable toolchain 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: stable 37 | override: true 38 | 39 | - name: Run cargo test 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: test 43 | 44 | lints: 45 | name: Lints 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout sources 49 | uses: actions/checkout@v2 50 | 51 | - name: Install stable toolchain 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | profile: minimal 55 | toolchain: stable 56 | override: true 57 | components: rustfmt, clippy 58 | 59 | - name: Run cargo fmt 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: fmt 63 | args: --all -- --check 64 | 65 | - name: Run cargo clippy 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: clippy 69 | args: -- -D warnings 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [manifest-server-v0.1.2] - 2022-10-14 6 | ### Documentation 7 | 8 | - How to install and use the server 9 | - Document server and its params 10 | 11 | ### Features 12 | 13 | - Handle shutdown signal 14 | 15 | ### Miscellaneous Tasks 16 | 17 | - Change how diff is generated 18 | 19 | ## [manifest-server-v0.1.1] - 2022-10-13 20 | 21 | ### Bug Fixes 22 | 23 | - Check for possible out of bounds when choosing initial variant 24 | 25 | ### Documentation 26 | 27 | - Add crates.io badge 28 | - Add docs badge 29 | - Label badges for each project 30 | - Add a changelog file 31 | 32 | ### Miscellaneous Tasks 33 | 34 | - Upgrade manifest-filter dependency for manifest-server 35 | - Fix Cargo.toml in order to make it installable 36 | - Automatic changelog 37 | 38 | ### Refactor 39 | 40 | - Reuse test functions to build master/media playlists 41 | - Replace structs with Option values 42 | 43 | ### Styling 44 | 45 | - Format code 46 | 47 | 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "manifest-server", 4 | "manifest-filter", 5 | ] 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Maurício Antunes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![test](https://github.com/mauricioabreu/manifest-modifier/actions/workflows/CI.yml/badge.svg)](https://github.com/mauricioabreu/manifest-modifier/actions/workflows/CI.yml) 2 | [![Crates.io](https://img.shields.io/crates/v/manifest-filter?label=manifest-filter)](https://crates.io/crates/manifest-filter) 3 | [![Crates.io](https://img.shields.io/crates/v/manifest-server?label=manifest-server)](https://crates.io/crates/manifest-server) 4 | [![manifest-filter docs](https://docs.rs/manifest-filter/badge.svg)](https://docs.rs/manifest-filter) 5 | 6 | # Manifest modifier 7 | 8 | *Manifest Modifier* is a work-in-progress project to modify video manifests. 9 | 10 | Why? Video is a bit complex. Some manifests won't run on some devices because of the frame rate, or the bitrate, or other tags that may affect playback. 11 | 12 | ![manifest_modifier](/assets/manifest_modifier.png) 13 | 14 | The image above is a perfect example that describes an usual problem: some devices can't play 60fps video. In order to solve this situation, we can use `manifest-modifier` to rewrite the manifest right before sending it to the users. 15 | 16 | There are two ways to use this project, either as a lib or a server. This project is dividied into two crates: `manifest-filter` and `manifest-server`. `manifest-server` is a server built on top of [axum](https://github.com/tokio-rs/axum) and [m3u8-rs](https://github.com/rutgersc/m3u8-rs). It can be used without requiring advanced knowledge of the Rust programming language. 17 | 18 | `manifest-filter` is the Rust code behind `manifest-server`. If you running your own server and can't use the `manifest-server`, no worries, you can use the same features by calling Rust code directly: 19 | 20 | ```rust 21 | use manifest_filter::Master; 22 | use std::io::Read; 23 | 24 | let mut file = std::fs::File::open("manifests/master.m3u8").unwrap(); 25 | let mut content: Vec = Vec::new(); 26 | file.read_to_end(&mut content).unwrap(); 27 | 28 | let (_, master_playlist) = m3u8_rs::parse_master_playlist(&content).unwrap(); 29 | let mut master = Master { 30 | playlist: master_playlist, 31 | }; 32 | master.filter_fps(Some(30.0)); 33 | ``` 34 | 35 | The result should be something like this 36 | 37 | ``` 38 | #EXTM3U 39 | #EXT-X-VERSION:4 40 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2" 41 | #EXT-X-STREAM-INF:BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=384x216,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 42 | variant-audio_1=96000-video=249984.m3u8 43 | #EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=800000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=768x432,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 44 | variant-audio_1=96000-video=1320960.m3u8 45 | ``` 46 | 47 | ## Installation 48 | 49 | The binary for `manifest-server` is `manifest_server` 50 | 51 | You must have `cargo` to install the manifest server: 52 | 53 | ``` 54 | $ cargo install manifest-server 55 | ``` 56 | 57 | There is no other way to install the server right now. 58 | 59 | ## Usage 60 | 61 | For the server to work, you need to export a variable `LISTEN_ADDRESS` 62 | 63 | ``` 64 | $ LISTEN_ADDRESS=127.0.0.1:3000 manifest_server 65 | ``` 66 | 67 | ## Features 68 | 69 | ### Master playlist 70 | 71 | **Bandwidth** - filter variants based on min and max values. 72 | 73 | Request: 74 | 75 | ``` 76 | curl --request POST \ 77 | --url 'http://localhost:3000/master?min_bitrate=800000&max_bitrate=2000000' \ 78 | --header 'content-type: text/html; charset=UTF-8' \ 79 | --header 'user-agent: vscode-restclient' \ 80 | --data '< ../manifest-filter/manifests/master.m3u8' 81 | ``` 82 | 83 | Response: 84 | 85 | ``` 86 | #EXTM3U 87 | #EXT-X-VERSION:4 88 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2" 89 | #EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=800000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=768x432,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 90 | variant-audio_1=96000-video=1320960.m3u8 91 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=1280x720,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 92 | variant-audio_1=96000-video=3092992.m3u8 93 | #EXT-X-STREAM-INF:BANDWIDTH=2000000,AVERAGE-BANDWIDTH=2000000,CODECS="mp4a.40.5,avc1.640029",RESOLUTION=1920x1080,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 94 | variant-audio_1=96000-video=4686976.m3u8 95 | ``` 96 | 97 |
98 | Original playlist 99 | 100 | As you can see, the original playlist was slightly different: 101 | 102 | ``` 103 | #EXTM3U 104 | #EXT-X-VERSION:4 105 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2" 106 | #EXT-X-STREAM-INF:BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=384x216,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 107 | variant-audio_1=96000-video=249984.m3u8 108 | #EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=800000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=768x432,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 109 | variant-audio_1=96000-video=1320960.m3u8 110 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=1280x720,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 111 | variant-audio_1=96000-video=3092992.m3u8 112 | #EXT-X-STREAM-INF:BANDWIDTH=2000000,AVERAGE-BANDWIDTH=2000000,CODECS="mp4a.40.5,avc1.640029",RESOLUTION=1920x1080,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 113 | variant-audio_1=96000-video=4686976.m3u8 114 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=37000,CODECS="avc1.64001F",RESOLUTION=384x216,URI="keyframes/variant-video=249984.m3u8" 115 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=193000,CODECS="avc1.64001F",RESOLUTION=768x432,URI="keyframes/variant-video=1320960.m3u8" 116 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=296000,CODECS="avc1.64001F",RESOLUTION=1280x720,URI="keyframes/variant-video=2029952.m3u8" 117 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=684000,CODECS="avc1.640029",RESOLUTION=1920x1080,URI="keyframes/variant-video=4686976.m3u8" 118 | ``` 119 |
120 | 121 | **Frame rate** - filter variants based on a predefined *fps*: 122 | 123 | Request: 124 | 125 | ``` 126 | curl --request POST \ 127 | --url 'http://localhost:3000/master?rate=60' \ 128 | --header 'content-type: text/html; charset=UTF-8' \ 129 | --header 'user-agent: vscode-restclient' \ 130 | --data '< ../manifest-filter/manifests/master.m3u8' 131 | ``` 132 | 133 | Response: 134 | 135 | ``` 136 | #EXTM3U 137 | #EXT-X-VERSION:4 138 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2" 139 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=1280x720,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 140 | variant-audio_1=96000-video=3092992.m3u8 141 | #EXT-X-STREAM-INF:BANDWIDTH=2000000,AVERAGE-BANDWIDTH=2000000,CODECS="mp4a.40.5,avc1.640029",RESOLUTION=1920x1080,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 142 | variant-audio_1=96000-video=4686976.m3u8 143 | ``` 144 | 145 |
146 | Original playlist 147 | 148 | ``` 149 | #EXTM3U 150 | #EXT-X-VERSION:4 151 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2" 152 | #EXT-X-STREAM-INF:BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=384x216,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 153 | variant-audio_1=96000-video=249984.m3u8 154 | #EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=800000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=768x432,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 155 | variant-audio_1=96000-video=1320960.m3u8 156 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=1280x720,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 157 | variant-audio_1=96000-video=3092992.m3u8 158 | #EXT-X-STREAM-INF:BANDWIDTH=2000000,AVERAGE-BANDWIDTH=2000000,CODECS="mp4a.40.5,avc1.640029",RESOLUTION=1920x1080,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 159 | variant-audio_1=96000-video=4686976.m3u8 160 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=37000,CODECS="avc1.64001F",RESOLUTION=384x216,URI="keyframes/variant-video=249984.m3u8" 161 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=193000,CODECS="avc1.64001F",RESOLUTION=768x432,URI="keyframes/variant-video=1320960.m3u8" 162 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=296000,CODECS="avc1.64001F",RESOLUTION=1280x720,URI="keyframes/variant-video=2029952.m3u8" 163 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=684000,CODECS="avc1.640029",RESOLUTION=1920x1080,URI="keyframes/variant-video=4686976.m3u8" 164 | ``` 165 |
166 | 167 | ### Media playlist 168 | 169 | **DVR** - remove segments (backwards) based on the duration (seconds) 170 | 171 | ``` 172 | curl --request POST \ 173 | --url 'http://localhost:3000/media?dvr=15' \ 174 | --header 'content-type: text/html; charset=UTF-8' \ 175 | --header 'user-agent: vscode-restclient' \ 176 | --data '< ../manifest-filter/manifests/media.m3u8' 177 | ``` 178 | 179 | Response: 180 | 181 | ``` 182 | #EXTM3U 183 | #EXT-X-VERSION:4 184 | #EXT-X-INDEPENDENT-SEGMENTS 185 | #EXT-X-TARGETDURATION:8 186 | #EXT-X-MEDIA-SEQUENCE:320035373 187 | #EXTINF:5, no desc 188 | variant-audio_1=96000-video=249984-320035703.ts 189 | #EXTINF:5, no desc 190 | variant-audio_1=96000-video=249984-320035702.ts 191 | #EXT-X-PROGRAM-DATE-TIME:2020-09-15T14:01:39.133333+00:00 192 | #EXT-X-CUE-IN 193 | #EXTINF:5.8666, no desc 194 | variant-audio_1=96000-video=249984-320035701.ts 195 | ``` 196 | 197 |
198 | Original playlist 199 | 200 | ``` 201 | #EXTM3U 202 | #EXT-X-VERSION:4 203 | #EXT-X-MEDIA-SEQUENCE:320035356 204 | #EXT-X-INDEPENDENT-SEGMENTS 205 | #EXT-X-TARGETDURATION:8 206 | #EXT-X-PROGRAM-DATE-TIME:2020-09-15T13:32:55Z 207 | #EXTINF:5, no desc 208 | variant-audio_1=96000-video=249984-320035684.ts 209 | #EXTINF:5, no desc 210 | variant-audio_1=96000-video=249984-320035685.ts 211 | #EXTINF:5, no desc 212 | variant-audio_1=96000-video=249984-320035686.ts 213 | #EXTINF:5, no desc 214 | variant-audio_1=96000-video=249984-320035687.ts 215 | #EXTINF:4.1333, no desc 216 | variant-audio_1=96000-video=249984-320035688.ts 217 | #EXT-X-DATERANGE:ID="4026531847",START-DATE="2020-09-15T14:00:39.133333Z",PLANNED-DURATION=60,SCTE35-OUT=0xFC3025000000000BB800FFF01405F00000077FEFFE0AF311F0FE005265C0000101010000817C918E 218 | #EXT-X-CUE-OUT:60 219 | #EXT-X-PROGRAM-DATE-TIME:2020-09-15T14:00:39.133333Z 220 | #EXTINF:5.8666, no desc 221 | variant-audio_1=96000-video=249984-320035689.ts 222 | #EXTINF:5, no desc 223 | variant-audio_1=96000-video=249984-320035690.ts 224 | #EXTINF:5, no desc 225 | variant-audio_1=96000-video=249984-320035691.ts 226 | #EXTINF:5, no desc 227 | variant-audio_1=96000-video=249984-320035692.ts 228 | #EXTINF:5, no desc 229 | variant-audio_1=96000-video=249984-320035693.ts 230 | #EXTINF:5, no desc 231 | variant-audio_1=96000-video=249984-320035694.ts 232 | #EXTINF:5, no desc 233 | variant-audio_1=96000-video=249984-320035695.ts 234 | #EXTINF:5, no desc 235 | variant-audio_1=96000-video=249984-320035696.ts 236 | #EXTINF:5, no desc 237 | variant-audio_1=96000-video=249984-320035697.ts 238 | #EXTINF:5, no desc 239 | variant-audio_1=96000-video=249984-320035698.ts 240 | #EXTINF:5, no desc 241 | variant-audio_1=96000-video=249984-320035699.ts 242 | #EXTINF:4.1333, no desc 243 | variant-audio_1=96000-video=249984-320035700.ts 244 | #EXT-X-CUE-IN 245 | #EXT-X-PROGRAM-DATE-TIME:2020-09-15T14:01:39.133333Z 246 | #EXTINF:5.8666, no desc 247 | variant-audio_1=96000-video=249984-320035701.ts 248 | #EXTINF:5, no desc 249 | variant-audio_1=96000-video=249984-320035702.ts 250 | #EXTINF:5, no desc 251 | variant-audio_1=96000-video=249984-320035703.ts 252 | ``` 253 |
254 | 255 | ## Tests 256 | 257 | ``` 258 | cargo test 259 | ``` 260 | 261 | ## Lint 262 | 263 | ``` 264 | cargo clippy 265 | ``` 266 | -------------------------------------------------------------------------------- /assets/manifest_modifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricioabreu/manifest-modifier/3147a50558c5c14b2b5f840134545677f20cd077/assets/manifest_modifier.png -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog\n 7 | All notable changes to this project will be documented in this file.\n 8 | """ 9 | # template for the changelog body 10 | # https://tera.netlify.app/docs/#introduction 11 | body = """ 12 | {% if version %}\ 13 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 14 | {% else %}\ 15 | ## [unreleased] 16 | {% endif %}\ 17 | {% for group, commits in commits | group_by(attribute="group") %} 18 | ### {{ group | upper_first }} 19 | {% for commit in commits %} 20 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 21 | {% endfor %} 22 | {% endfor %}\n 23 | """ 24 | # remove the leading and trailing whitespace from the template 25 | trim = true 26 | # changelog footer 27 | footer = """ 28 | 29 | """ 30 | 31 | [git] 32 | # parse the commits based on https://www.conventionalcommits.org 33 | conventional_commits = true 34 | # filter out the commits that are not conventional 35 | filter_unconventional = true 36 | # process each line of a commit as an individual commit 37 | split_commits = false 38 | # regex for preprocessing the commit messages 39 | commit_preprocessors = [ 40 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, 41 | ] 42 | # regex for parsing and grouping commits 43 | commit_parsers = [ 44 | { message = "^feat", group = "Features"}, 45 | { message = "^fix", group = "Bug Fixes"}, 46 | { message = "^doc", group = "Documentation"}, 47 | { message = "^perf", group = "Performance"}, 48 | { message = "^refactor", group = "Refactor"}, 49 | { message = "^style", group = "Styling"}, 50 | { message = "^test", group = "Testing"}, 51 | { message = "^chore\\(release\\): prepare for", skip = true}, 52 | { message = "^chore", group = "Miscellaneous Tasks"}, 53 | { body = ".*security", group = "Security"}, 54 | ] 55 | # filter out the commits that are not matched by commit parsers 56 | filter_commits = false 57 | # glob pattern for matching git tags 58 | tag_pattern = "manifest-*-v[0-9]*" 59 | # regex for skipping tags 60 | skip_tags = "manifest-*-v0.1.0-beta.1" 61 | # regex for ignoring tags 62 | ignore_tags = "" 63 | # sort the tags chronologically 64 | date_order = false 65 | # sort the commits inside sections by oldest/newest order 66 | sort_commits = "oldest" 67 | -------------------------------------------------------------------------------- /examples/api.http: -------------------------------------------------------------------------------- 1 | ### Bandwidth filter 2 | POST http://localhost:3000/master?min_bitrate=800000&max_bitrate=2000000 3 | Content-Type: text/plain; charset=UTF-8 4 | 5 | < ../manifest-filter/manifests/master.m3u8 6 | 7 | ### Frame rate filter 8 | POST http://localhost:3000/master?rate=60 9 | Content-Type: text/plain; charset=UTF-8 10 | 11 | < ../manifest-filter/manifests/master.m3u8 12 | 13 | ### Mixing two filters (bandwidth and frame rate) 14 | POST http://localhost:3000/master?rate=30&min_bitrate=800000&max_bitrate=2000000 15 | Content-Type: text/plain; charset=UTF-8 16 | 17 | < ../manifest-filter/manifests/master.m3u8 18 | 19 | ### Set first variant by index 20 | POST http://localhost:3000/master?variant_index=1 21 | Content-Type: text/plain; charset=UTF-8 22 | 23 | < ../manifest-filter/manifests/master.m3u8 24 | 25 | ### Set first variant by index 26 | POST http://localhost:3000/master?closest_bandwidth=1500000 27 | Content-Type: text/plain; charset=UTF-8 28 | 29 | < ../manifest-filter/manifests/master.m3u8 30 | 31 | ### Filter DVR 32 | POST http://localhost:3000/media?dvr=15 33 | Content-Type: text/plain; charset=UTF-8 34 | 35 | < ../manifest-filter/manifests/media.m3u8 36 | -------------------------------------------------------------------------------- /examples/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.64 2 | 3 | RUN cargo install manifest-server 4 | 5 | ENV LISTEN_ADDRESS 0.0.0.0:3000 6 | 7 | CMD ["manifest_server"] 8 | -------------------------------------------------------------------------------- /manifest-filter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manifest-filter" 3 | description = "lib to modify video manifests" 4 | version = "0.1.1" 5 | edition = "2021" 6 | authors = ["Maurício Antunes "] 7 | license = "MIT" 8 | homepage = "https://github.com/mauricioabreu/manifest-modifier" 9 | repository = "https://github.com/mauricioabreu/manifest-modifier" 10 | readme = "../README.md" 11 | keywords = ["video", "hls"] 12 | categories = ["multimedia"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | m3u8-rs = "5.0.2" 18 | -------------------------------------------------------------------------------- /manifest-filter/manifests/master.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2" 4 | #EXT-X-STREAM-INF:BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=384x216,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 5 | variant-audio_1=96000-video=249984.m3u8 6 | #EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=800000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=768x432,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 7 | variant-audio_1=96000-video=1320960.m3u8 8 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=1280x720,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 9 | variant-audio_1=96000-video=3092992.m3u8 10 | #EXT-X-STREAM-INF:BANDWIDTH=2000000,AVERAGE-BANDWIDTH=2000000,CODECS="mp4a.40.5,avc1.640029",RESOLUTION=1920x1080,FRAME-RATE=60,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 11 | variant-audio_1=96000-video=4686976.m3u8 12 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=37000,CODECS="avc1.64001F",RESOLUTION=384x216,URI="keyframes/variant-video=249984.m3u8" 13 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=193000,CODECS="avc1.64001F",RESOLUTION=768x432,URI="keyframes/variant-video=1320960.m3u8" 14 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=296000,CODECS="avc1.64001F",RESOLUTION=1280x720,URI="keyframes/variant-video=2029952.m3u8" 15 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=684000,CODECS="avc1.640029",RESOLUTION=1920x1080,URI="keyframes/variant-video=4686976.m3u8" 16 | -------------------------------------------------------------------------------- /manifest-filter/manifests/media.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-MEDIA-SEQUENCE:320035356 4 | #EXT-X-INDEPENDENT-SEGMENTS 5 | #EXT-X-TARGETDURATION:8 6 | #EXT-X-PROGRAM-DATE-TIME:2020-09-15T13:32:55Z 7 | #EXTINF:5, no desc 8 | variant-audio_1=96000-video=249984-320035684.ts 9 | #EXTINF:5, no desc 10 | variant-audio_1=96000-video=249984-320035685.ts 11 | #EXTINF:5, no desc 12 | variant-audio_1=96000-video=249984-320035686.ts 13 | #EXTINF:5, no desc 14 | variant-audio_1=96000-video=249984-320035687.ts 15 | #EXTINF:4.1333, no desc 16 | variant-audio_1=96000-video=249984-320035688.ts 17 | #EXT-X-DATERANGE:ID="4026531847",START-DATE="2020-09-15T14:00:39.133333Z",PLANNED-DURATION=60,SCTE35-OUT=0xFC3025000000000BB800FFF01405F00000077FEFFE0AF311F0FE005265C0000101010000817C918E 18 | #EXT-X-CUE-OUT:60 19 | #EXT-X-PROGRAM-DATE-TIME:2020-09-15T14:00:39.133333Z 20 | #EXTINF:5.8666, no desc 21 | variant-audio_1=96000-video=249984-320035689.ts 22 | #EXTINF:5, no desc 23 | variant-audio_1=96000-video=249984-320035690.ts 24 | #EXTINF:5, no desc 25 | variant-audio_1=96000-video=249984-320035691.ts 26 | #EXTINF:5, no desc 27 | variant-audio_1=96000-video=249984-320035692.ts 28 | #EXTINF:5, no desc 29 | variant-audio_1=96000-video=249984-320035693.ts 30 | #EXTINF:5, no desc 31 | variant-audio_1=96000-video=249984-320035694.ts 32 | #EXTINF:5, no desc 33 | variant-audio_1=96000-video=249984-320035695.ts 34 | #EXTINF:5, no desc 35 | variant-audio_1=96000-video=249984-320035696.ts 36 | #EXTINF:5, no desc 37 | variant-audio_1=96000-video=249984-320035697.ts 38 | #EXTINF:5, no desc 39 | variant-audio_1=96000-video=249984-320035698.ts 40 | #EXTINF:5, no desc 41 | variant-audio_1=96000-video=249984-320035699.ts 42 | #EXTINF:4.1333, no desc 43 | variant-audio_1=96000-video=249984-320035700.ts 44 | #EXT-X-CUE-IN 45 | #EXT-X-PROGRAM-DATE-TIME:2020-09-15T14:01:39.133333Z 46 | #EXTINF:5.8666, no desc 47 | variant-audio_1=96000-video=249984-320035701.ts 48 | #EXTINF:5, no desc 49 | variant-audio_1=96000-video=249984-320035702.ts 50 | #EXTINF:5, no desc 51 | variant-audio_1=96000-video=249984-320035703.ts 52 | -------------------------------------------------------------------------------- /manifest-filter/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! manifest-filter is a lib used to modify video manifests. 2 | //! 3 | //! # Table of contents 4 | //! 5 | //! - [Features](#features) 6 | //! - [Examples](#examples) 7 | //! 8 | //! # Features 9 | //! 10 | //! - Modify master playlists (filter variants by bandwidth, fps, etc) 11 | //! - Modify media playlists (DVR, trim segments) 12 | //! 13 | //! More features are coming soon. 14 | //! 15 | //! # Examples 16 | //! 17 | //! You can try the example below, used to filter only the variants that are 30fps. 18 | //! 19 | //! ```rust 20 | //! use manifest_filter::Master; 21 | //! use std::io::Read; 22 | //! 23 | //! let mut file = std::fs::File::open("manifests/master.m3u8").unwrap(); 24 | //! let mut content: Vec = Vec::new(); 25 | //! file.read_to_end(&mut content).unwrap(); 26 | //! 27 | //! let (_, master_playlist) = m3u8_rs::parse_master_playlist(&content).unwrap(); 28 | //! let mut master = Master { 29 | //! playlist: master_playlist, 30 | //! }; 31 | //! master.filter_fps(Some(30.0)); 32 | //! ``` 33 | //! 34 | //! The result should be something like this 35 | //! 36 | //! ```not_rust 37 | //! #EXTM3U 38 | //! #EXT-X-VERSION:4 39 | //! #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2" 40 | //! #EXT-X-STREAM-INF:BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=384x216,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 41 | //! variant-audio_1=96000-video=249984.m3u8 42 | //! #EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=800000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=768x432,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE 43 | //! variant-audio_1=96000-video=1320960.m3u8 44 | //! ``` 45 | //! 46 | //! All functions can be chained. Call `filter_fps` to first remove variants with 47 | //! a frame rate different of the one choosen and call `filter_bandwith` right after to 48 | //! remove variants thare don't fit into the max/min range expected. 49 | //! The sampe applies for `Media`. 50 | 51 | use m3u8_rs::{MasterPlaylist, MediaPlaylist, Playlist}; 52 | 53 | pub fn load_master(content: &[u8]) -> Result { 54 | match m3u8_rs::parse_playlist(content) { 55 | Result::Ok((_, Playlist::MasterPlaylist(pl))) => Ok(pl), 56 | Result::Ok((_, Playlist::MediaPlaylist(_))) => Err("must be a master playlist".to_string()), 57 | Result::Err(e) => Err(e.to_string()), 58 | } 59 | } 60 | 61 | pub fn load_media(content: &[u8]) -> Result { 62 | match m3u8_rs::parse_playlist(content) { 63 | Result::Ok((_, Playlist::MediaPlaylist(pl))) => Ok(pl), 64 | Result::Ok((_, Playlist::MasterPlaylist(_))) => Err("must be a media playlist".to_string()), 65 | Result::Err(e) => Err(e.to_string()), 66 | } 67 | } 68 | 69 | /// `Master` holds a reference to the master playlist. All 70 | /// functions implemented by this struct can be chained. 71 | pub struct Master { 72 | pub playlist: MasterPlaylist, 73 | } 74 | 75 | /// `Media` holds a reference to the media playlist. All 76 | /// functions implemented by this struct can be chained. 77 | pub struct Media { 78 | pub playlist: MediaPlaylist, 79 | } 80 | 81 | impl Master { 82 | /// Filter variants from a master playlist based on the frame rate passed. 83 | pub fn filter_fps(&mut self, rate: Option) -> &mut Self { 84 | if let Some(r) = rate { 85 | self.playlist.variants.retain(|v| v.frame_rate == Some(r)); 86 | } 87 | self 88 | } 89 | 90 | /// Filter variants from a master playlist based on the bandwidh passed. 91 | /// 92 | /// Variants can be filtered using `min` and `max` values for bandwidth. 93 | /// 94 | /// There's no need to pass a `min` value if you don't need to. The 95 | /// same happens for `max` value. For `min` we will set to zero by default 96 | /// and for the `max` we'll use the `u64::MAX` value. 97 | pub fn filter_bandwidth(&mut self, min: Option, max: Option) -> &mut Self { 98 | let min = min.unwrap_or(0); 99 | let max = max.unwrap_or(u64::MAX); 100 | 101 | self.playlist 102 | .variants 103 | .retain(|v| v.bandwidth >= min && v.bandwidth <= max); 104 | self 105 | } 106 | 107 | /// Set the first variant by index to appear in the playlist for the one that 108 | /// best suites the device needs. Most of the times such feature will 109 | /// be used to skip the initial variant (too low for some devices). 110 | /// 111 | /// If the `index` passed in could cause "out of bounds" error, the playlist 112 | /// will keep untouched. 113 | /// 114 | /// # Arguments 115 | /// * `index` - an Option containing the index you want to be the first variant. Variants will be swapped. 116 | pub fn first_variant_by_index(&mut self, index: Option) -> &mut Self { 117 | if let Some(i) = index { 118 | if i as usize <= self.playlist.variants.len() { 119 | self.playlist.variants.swap(0, i.try_into().unwrap()); 120 | } 121 | } 122 | self 123 | } 124 | 125 | /// Set the first variant by closes bandwidth to appear in the playlist for the one that 126 | /// best suites the device needs. Most of the times such feature will 127 | /// be used to skip the initial variant (too low for some devices). 128 | /// 129 | /// # Arguments 130 | /// * `closest_bandwidth` - an Option containing an approximate bandwidth value you want for the first variant. 131 | pub fn first_variant_by_closest_bandwidth( 132 | &mut self, 133 | closest_bandwidth: Option, 134 | ) -> &mut Self { 135 | if let Some(c) = closest_bandwidth { 136 | let (idx, _) = self 137 | .playlist 138 | .variants 139 | .iter() 140 | .enumerate() 141 | .min_by_key(|(_, v)| (c as i64 - v.bandwidth as i64).abs()) 142 | .unwrap(); 143 | let fv = self.playlist.variants.remove(idx); 144 | self.playlist.variants.insert(0, fv); 145 | } 146 | self 147 | } 148 | } 149 | 150 | impl Media { 151 | /// Remove segments backwards from the media playlist, based on the duration 152 | /// set. The duration is in seconds. 153 | /// Media sequence will be affected: `` 154 | pub fn filter_dvr(&mut self, seconds: Option) -> &mut Self { 155 | let mut acc = 0; 156 | let total_segments = self.playlist.segments.len(); 157 | 158 | if let Some(s) = seconds { 159 | self.playlist.segments = self 160 | .playlist 161 | .segments 162 | .iter() 163 | .rev() 164 | .take_while(|segment| { 165 | acc += segment.duration as u64; 166 | acc <= s 167 | }) 168 | .cloned() 169 | .collect(); 170 | self.playlist.media_sequence += (total_segments - self.playlist.segments.len()) as u64; 171 | } 172 | self 173 | } 174 | 175 | /// Remove segments from the media playlist, based on the start/end passed. 176 | /// Media sequence will be affected: `` 177 | pub fn trim(&mut self, start: Option, end: Option) -> &mut Self { 178 | let start = start.unwrap_or(0); 179 | let end = end.unwrap_or_else(|| self.playlist.segments.len().try_into().unwrap()); 180 | 181 | let segments = &self.playlist.segments[start as usize..end as usize]; 182 | let total_segments = self.playlist.segments.len(); 183 | self.playlist.segments = segments.to_vec(); 184 | self.playlist.media_sequence += (total_segments - self.playlist.segments.len()) as u64; 185 | self 186 | } 187 | } 188 | 189 | #[cfg(test)] 190 | mod tests { 191 | use super::*; 192 | use std::io::Read; 193 | 194 | fn build_master() -> Master { 195 | let mut file = std::fs::File::open("manifests/master.m3u8").unwrap(); 196 | let mut content: Vec = Vec::new(); 197 | file.read_to_end(&mut content).unwrap(); 198 | 199 | let (_, master_playlist) = m3u8_rs::parse_master_playlist(&content).unwrap(); 200 | Master { 201 | playlist: master_playlist, 202 | } 203 | } 204 | 205 | fn build_media() -> Media { 206 | let mut file = std::fs::File::open("manifests/media.m3u8").unwrap(); 207 | let mut content: Vec = Vec::new(); 208 | file.read_to_end(&mut content).unwrap(); 209 | 210 | let (_, media_playlist) = m3u8_rs::parse_media_playlist(&content).unwrap(); 211 | Media { 212 | playlist: media_playlist, 213 | } 214 | } 215 | 216 | #[test] 217 | fn filter_60_fps() { 218 | let mut master = build_master(); 219 | master.filter_fps(Some(60.0)); 220 | 221 | assert_eq!(master.playlist.variants.len(), 2); 222 | } 223 | 224 | #[test] 225 | fn filter_min_bandwidth() { 226 | let mut master = build_master(); 227 | 228 | master.filter_bandwidth(Some(800000), None); 229 | 230 | assert_eq!(master.playlist.variants.len(), 3); 231 | } 232 | 233 | #[test] 234 | fn filter_max_bandwidth() { 235 | let mut master = build_master(); 236 | 237 | master.filter_bandwidth(None, Some(800000)); 238 | 239 | assert_eq!(master.playlist.variants.len(), 6); 240 | } 241 | 242 | #[test] 243 | fn filter_min_and_max_bandwidth() { 244 | let mut master = build_master(); 245 | 246 | master.filter_bandwidth(Some(800000), Some(2000000)); 247 | 248 | assert_eq!(master.playlist.variants.len(), 3); 249 | } 250 | 251 | #[test] 252 | fn set_first_variant_by_index() { 253 | let mut master = build_master(); 254 | 255 | master.first_variant_by_index(Some(1)); 256 | 257 | assert_eq!(master.playlist.variants[0].bandwidth, 800000); 258 | assert_eq!(master.playlist.variants[1].bandwidth, 600000); 259 | } 260 | 261 | #[test] 262 | fn set_first_variant_by_out_of_bounds_index() { 263 | let mut master = build_master(); 264 | 265 | master.first_variant_by_index(Some(100)); 266 | 267 | assert_eq!(master.playlist.variants[0].bandwidth, 600000); 268 | assert_eq!(master.playlist.variants[1].bandwidth, 800000); 269 | } 270 | 271 | #[test] 272 | fn set_first_variant_by_closest_bandwidth() { 273 | let mut master = build_master(); 274 | 275 | master.first_variant_by_closest_bandwidth(Some(1650000)); 276 | assert_eq!(master.playlist.variants[0].bandwidth, 1500000); 277 | assert_eq!(master.playlist.variants[1].bandwidth, 600000); 278 | } 279 | 280 | #[test] 281 | fn filter_dvr_with_short_duration() { 282 | let mut media = build_media(); 283 | 284 | media.filter_dvr(Some(15)); 285 | 286 | assert_eq!(media.playlist.segments.len(), 3); 287 | assert_eq!(media.playlist.media_sequence, 320035373); 288 | } 289 | 290 | #[test] 291 | fn filter_dvr_with_long_duration() { 292 | let mut media = build_media(); 293 | 294 | media.filter_dvr(Some(u64::MAX)); 295 | 296 | assert_eq!(media.playlist.segments.len(), 20); 297 | assert_eq!(media.playlist.media_sequence, 320035356); 298 | } 299 | 300 | #[test] 301 | fn trim_media_playlist_with_start_only() { 302 | let mut media = build_media(); 303 | 304 | media.trim(Some(5), None); 305 | 306 | assert_eq!(media.playlist.segments.len(), 15); 307 | assert_eq!(media.playlist.media_sequence, 320035361); 308 | } 309 | 310 | #[test] 311 | fn trim_media_playlist_with_end_only() { 312 | let mut media = build_media(); 313 | 314 | media.trim(None, Some(5)); 315 | 316 | assert_eq!(media.playlist.segments.len(), 5); 317 | assert_eq!(media.playlist.media_sequence, 320035371); 318 | } 319 | 320 | #[test] 321 | fn trim_media_playlist_with_start_and_end() { 322 | let mut media = build_media(); 323 | 324 | media.trim(Some(5), Some(18)); 325 | 326 | assert_eq!(media.playlist.segments.len(), 13); 327 | assert_eq!(media.playlist.media_sequence, 320035363); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /manifest-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manifest-server" 3 | description = "HTTP server that modifies video manifests" 4 | version = "0.1.2" 5 | edition = "2021" 6 | authors = ["Maurício Antunes "] 7 | license = "MIT" 8 | homepage = "https://github.com/mauricioabreu/manifest-modifier" 9 | repository = "https://github.com/mauricioabreu/manifest-modifier" 10 | readme = "../README.md" 11 | keywords = ["video", "hls"] 12 | categories = ["multimedia"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | axum = "0.5.16" 18 | tokio = { version = "1.21.2", features = ["full"] } 19 | tracing = "0.1.36" 20 | tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } 21 | serde = { version = "1.0.145", features = ["derive"] } 22 | manifest-filter = { path = "../manifest-filter", version = "0.1.1-beta.3" } 23 | 24 | [[bin]] 25 | bench = false 26 | path = "src/main.rs" 27 | name = "manifest_server" 28 | -------------------------------------------------------------------------------- /manifest-server/src/main.rs: -------------------------------------------------------------------------------- 1 | //! manifest-server is an HTTP server you can use to modify video manifests. 2 | //! It is built on top of axum and relies on manifest handlers provided by `manifest-filter`. 3 | //! 4 | //! There are two routes: `/master` and `/media` 5 | //! 6 | //! [Params] describe the available parameters you can use to modify video manifests. 7 | //! Parameters are all optional and they are passed as query parameters. Example: 8 | //! 9 | //! ```not_rust 10 | //! /master?min_bitrate=800000&max_bitrate=2000000 11 | //! ``` 12 | 13 | use axum::{ 14 | body::Bytes, extract::Query, http::StatusCode, response::IntoResponse, routing::post, Router, 15 | }; 16 | use serde::Deserialize; 17 | use std::env; 18 | use std::net::SocketAddr; 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | tracing_subscriber::fmt::init(); 23 | 24 | let app = Router::new() 25 | .route("/master", post(modify_master)) 26 | .route("/media", post(modify_media)); 27 | let addr = env::var("LISTEN_ADDRESS").expect("env var LISTEN_ADDRESS is not set"); 28 | let socket_addr = addr 29 | .parse::() 30 | .expect("value for LISTEN_ADDRESS must be like 127.0.0.1:3000"); 31 | tracing::debug!("listening on {}", socket_addr); 32 | axum::Server::bind(&socket_addr) 33 | .serve(app.into_make_service()) 34 | .with_graceful_shutdown(shutdown_signal()) 35 | .await 36 | .unwrap(); 37 | } 38 | 39 | /// Tokio signal handler that will wait for a user to press CTRL+C 40 | async fn shutdown_signal() { 41 | tokio::signal::ctrl_c() 42 | .await 43 | .expect("Expect shutdown signal handler"); 44 | println!("signal shutdown"); 45 | } 46 | 47 | async fn modify_master(params: Query, body: Bytes) -> impl IntoResponse { 48 | match manifest_filter::load_master(&body) { 49 | Ok(pl) => { 50 | let mut master = manifest_filter::Master { playlist: pl }; 51 | master 52 | .filter_bandwidth(params.min_bitrate, params.max_bitrate) 53 | .filter_fps(params.rate) 54 | .first_variant_by_index(params.variant_index) 55 | .first_variant_by_closest_bandwidth(params.closest_bandwidth); 56 | 57 | let mut v: Vec = Vec::new(); 58 | master.playlist.write_to(&mut v).unwrap(); 59 | 60 | (StatusCode::OK, String::from_utf8(v).unwrap()) 61 | } 62 | Err(e) => (StatusCode::BAD_REQUEST, e), 63 | } 64 | } 65 | 66 | async fn modify_media(params: Query, body: Bytes) -> impl IntoResponse { 67 | match manifest_filter::load_media(&body) { 68 | Ok(pl) => { 69 | let mut media = manifest_filter::Media { playlist: pl }; 70 | media 71 | .filter_dvr(params.dvr) 72 | .trim(params.trim_start, params.trim_end); 73 | 74 | let mut v: Vec = Vec::new(); 75 | media.playlist.write_to(&mut v).unwrap(); 76 | 77 | (StatusCode::OK, String::from_utf8(v).unwrap()) 78 | } 79 | Err(e) => (StatusCode::BAD_REQUEST, e), 80 | } 81 | } 82 | 83 | /// Query string params used to modify master and media manifests 84 | #[derive(Debug, Deserialize, Default)] 85 | struct Params { 86 | /// Min bitrate present in the master manifest 87 | min_bitrate: Option, 88 | /// Max bitrate present in the master manifest 89 | max_bitrate: Option, 90 | /// Frame rate allowed to be present in the master manifest 91 | rate: Option, 92 | /// DVR in seconds 93 | dvr: Option, 94 | /// Trim start 95 | trim_start: Option, 96 | /// Trim end 97 | trim_end: Option, 98 | /// Which index must be set as the first media manifest 99 | variant_index: Option, 100 | /// Which bandwidth must be set as the first media manifest 101 | closest_bandwidth: Option, 102 | } 103 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-commit-message = "chore: release {{crate_name}} {{version}}" 2 | pre-release-hook = ["git-cliff", "-vv", "--unreleased", "--workdir", "..", "--tag", "{{version}}"] 3 | --------------------------------------------------------------------------------