├── .gitignore ├── elevator.png ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── src ├── ivf.rs ├── obu.rs ├── level.rs └── main.rs ├── LICENSE.txt ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /elevator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vimeo/elevator/HEAD/elevator.png -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build 13 | run: cargo build --verbose 14 | - name: Run tests 15 | run: cargo test --verbose 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "elevator" 3 | version = "1.1.1" 4 | authors = ["Raphaël Zumer "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | av1parser = { git = "https://github.com/yohhoy/av1parser", rev = "21180d82e488c42d4e7c23d12e03dc222d984a54" } 11 | clap = "~2.33" 12 | -------------------------------------------------------------------------------- /src/ivf.rs: -------------------------------------------------------------------------------- 1 | use av1parser; 2 | use std::io; 3 | 4 | // Adapted from av1parser. TODO: clean up/refactor/rewrite 5 | pub fn parse_ivf_header( 6 | mut reader: R, 7 | fname: &str, 8 | ) -> io::Result { 9 | let mut ivf_header = [0; av1parser::ivf::IVF_HEADER_SIZE]; 10 | reader.read_exact(&mut ivf_header)?; 11 | 12 | match av1parser::ivf::parse_ivf_header(&ivf_header) { 13 | Ok(header) => { 14 | if header.codec != av1parser::FCC_AV01 { 15 | panic!("{}: unsupport codec", fname); 16 | } 17 | 18 | Ok(header) 19 | } 20 | Err(msg) => { 21 | panic!("{}: {}", fname, msg); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Raphaël Zumer 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /src/obu.rs: -------------------------------------------------------------------------------- 1 | use av1parser::*; 2 | use std::io; 3 | 4 | // Adapted from av1parser. TODO: clean up/refactor/rewrite 5 | pub fn process_obu(reader: &mut R, seq: &mut av1::Sequence, obu: &obu::Obu) { 6 | let reader = &mut io::Read::take(reader, u64::from(obu.obu_size)); 7 | match obu.obu_type { 8 | obu::OBU_SEQUENCE_HEADER => { 9 | if let Some(sh) = obu::parse_sequence_header(reader) { 10 | seq.sh = Some(sh); 11 | } 12 | } 13 | obu::OBU_FRAME_HEADER | obu::OBU_FRAME => { 14 | if seq.sh.is_none() { 15 | return; 16 | } 17 | if let Some(fh) = 18 | obu::parse_frame_header(reader, seq.sh.as_ref().unwrap(), &mut seq.rfman) 19 | { 20 | // decode_frame_wrapup(): Decode frame wrapup process 21 | if fh.show_frame || fh.show_existing_frame { 22 | seq.rfman.output_process(&fh); 23 | } 24 | if obu.obu_type == obu::OBU_FRAME { 25 | seq.rfman.update_process(&fh); 26 | } 27 | } 28 | } 29 | _ => {} 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elevator 2 | 3 | ![Logo](elevator.png) 4 | 5 | This is a CLI application for validating and correcting the level syntax element in the sequence header(s) of AV1 streams. 6 | 7 | Encoders tend to do a poor job of setting the level accurately, because headers are usually written before the rest of the stream. 8 | Without enforcing constraints to keep encoded streams below a given level, estimating the correct one in advance is difficult to impossible. 9 | 10 | Elevator parses a fully-encoded stream, calculates all the necessary parameters and determines the minimum acceptable level that will allow a spec-conformant decoder to decode it. It can then output this level to the command line, or patch it, either in place or to a new file. 11 | 12 | ## Restrictions 13 | - Only IVF file input is supported 14 | - Only one operating point is supported 15 | - Some parameters are parsed from the first sequence header only, and are assumed to be consistent across sequences 16 | - Some uncommon AV1 features, like scalability and super resolution, are untested and may produce incorrect output 17 | 18 | ## Usage 19 | ``` 20 | elevator [FLAGS] [OPTIONS] 21 | 22 | FLAGS: 23 | -h, --help Prints help information 24 | --inplace Patch file in place 25 | -V, --version Prints version information 26 | -v, --verbose Display verbose output, which may be helpful for debugging 27 | 28 | OPTIONS: 29 | -f, --forcedlevel Force a level instead of calculating it [possible values: 0, 1, 4, 5, 8, 9, 12, 30 | 13, 14, 15, 16, 17, 18, 19, 31] 31 | -o, --output Output filename 32 | 33 | ARGS: 34 | Input filename 35 | ``` 36 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.11.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 19 | dependencies = [ 20 | "hermit-abi", 21 | "libc", 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "av1parser" 27 | version = "0.1.0" 28 | source = "git+https://github.com/yohhoy/av1parser?rev=21180d82e488c42d4e7c23d12e03dc222d984a54#21180d82e488c42d4e7c23d12e03dc222d984a54" 29 | dependencies = [ 30 | "byteorder", 31 | "clap", 32 | "hex", 33 | ] 34 | 35 | [[package]] 36 | name = "bitflags" 37 | version = "1.3.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 40 | 41 | [[package]] 42 | name = "byteorder" 43 | version = "1.4.3" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 46 | 47 | [[package]] 48 | name = "clap" 49 | version = "2.33.3" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 52 | dependencies = [ 53 | "ansi_term", 54 | "atty", 55 | "bitflags", 56 | "strsim", 57 | "textwrap", 58 | "unicode-width", 59 | "vec_map", 60 | ] 61 | 62 | [[package]] 63 | name = "elevator" 64 | version = "1.1.1" 65 | dependencies = [ 66 | "av1parser", 67 | "clap", 68 | ] 69 | 70 | [[package]] 71 | name = "hermit-abi" 72 | version = "0.1.19" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 75 | dependencies = [ 76 | "libc", 77 | ] 78 | 79 | [[package]] 80 | name = "hex" 81 | version = "0.3.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" 84 | 85 | [[package]] 86 | name = "libc" 87 | version = "0.2.102" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" 90 | 91 | [[package]] 92 | name = "strsim" 93 | version = "0.8.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 96 | 97 | [[package]] 98 | name = "textwrap" 99 | version = "0.11.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 102 | dependencies = [ 103 | "unicode-width", 104 | ] 105 | 106 | [[package]] 107 | name = "unicode-width" 108 | version = "0.1.9" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 111 | 112 | [[package]] 113 | name = "vec_map" 114 | version = "0.8.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 117 | 118 | [[package]] 119 | name = "winapi" 120 | version = "0.3.9" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 123 | dependencies = [ 124 | "winapi-i686-pc-windows-gnu", 125 | "winapi-x86_64-pc-windows-gnu", 126 | ] 127 | 128 | [[package]] 129 | name = "winapi-i686-pc-windows-gnu" 130 | version = "0.4.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 133 | 134 | [[package]] 135 | name = "winapi-x86_64-pc-windows-gnu" 136 | version = "0.4.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 139 | -------------------------------------------------------------------------------- /src/level.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum Tier { 5 | Main, 6 | High, 7 | } 8 | 9 | impl Default for Tier { 10 | fn default() -> Self { 11 | Tier::Main 12 | } 13 | } 14 | 15 | /// Describes the maximum parameters relevant to level restrictions 16 | /// encountered in a sequence. 17 | #[derive(Default)] 18 | pub struct SequenceContext { 19 | pub tier: Tier, 20 | pub pic_size: (u16, u16), // (width, height) 21 | pub display_rate: u64, 22 | pub decode_rate: u64, 23 | pub header_rate: u16, 24 | pub mbps: f64, 25 | pub tiles: u8, 26 | pub tile_cols: u8, 27 | } 28 | 29 | impl Display for SequenceContext { 30 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 31 | writeln!(f, "Tier: {:?}", self.tier)?; 32 | writeln!(f, "Picture Size: {}x{}", self.pic_size.0, self.pic_size.1)?; 33 | writeln!( 34 | f, 35 | "Display/Decode/Header Rates: {}/{}/{}", 36 | self.display_rate, self.decode_rate, self.header_rate 37 | )?; 38 | writeln!(f, "Mbps: {:.3}", self.mbps)?; 39 | writeln!(f, "Tiles/Tile Columns: {}/{}", self.tiles, self.tile_cols)?; 40 | 41 | Ok(()) 42 | } 43 | } 44 | 45 | #[derive(Copy, Clone)] 46 | struct LevelLimits { 47 | max_pic_size: u32, 48 | max_h_size: u16, 49 | max_v_size: u16, 50 | max_display_rate: u64, 51 | max_decode_rate: u64, 52 | max_header_rate: u16, 53 | main_mbps: f64, 54 | high_mbps: f64, 55 | main_cr: u8, 56 | high_cr: u8, 57 | max_tiles: u8, 58 | max_tile_cols: u8, 59 | } 60 | 61 | #[derive(Copy, Clone)] 62 | pub struct Level(pub u8, Option); 63 | 64 | impl Level { 65 | pub fn is_valid(&self) -> bool { 66 | self.1.is_some() 67 | } 68 | } 69 | 70 | impl Display for Level { 71 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 72 | let index = self.0; 73 | 74 | if index == 31 { 75 | write!(f, "Maximum parameters") 76 | } else if index >= 24 { 77 | write!(f, "Reserved") 78 | } else { 79 | let x = 2 + (index >> 2); 80 | let y = index & 3; 81 | 82 | write!(f, "{}.{} ({})", x, y, self.0) 83 | } 84 | } 85 | } 86 | 87 | macro_rules! level { 88 | ($level: expr, $limits: expr) => { 89 | Level($level, Some($limits)) 90 | }; 91 | ($level: expr) => { 92 | Level($level, None) 93 | }; 94 | } 95 | 96 | pub const LEVELS: [Level; 32] = [ 97 | level!( 98 | 0, 99 | LevelLimits { 100 | max_pic_size: 147_456, 101 | max_h_size: 2048, 102 | max_v_size: 1152, 103 | max_display_rate: 4_423_680, 104 | max_decode_rate: 5_529_600, 105 | max_header_rate: 150, 106 | main_mbps: 1.5, 107 | high_mbps: 0.0, 108 | main_cr: 2, 109 | high_cr: 0, 110 | max_tiles: 8, 111 | max_tile_cols: 4, 112 | } 113 | ), 114 | level!( 115 | 1, 116 | LevelLimits { 117 | max_pic_size: 278_784, 118 | max_h_size: 2816, 119 | max_v_size: 1584, 120 | max_display_rate: 8_363_520, 121 | max_decode_rate: 10_454_400, 122 | max_header_rate: 150, 123 | main_mbps: 3.0, 124 | high_mbps: 0.0, 125 | main_cr: 2, 126 | high_cr: 0, 127 | max_tiles: 8, 128 | max_tile_cols: 4, 129 | } 130 | ), 131 | level!(2), 132 | level!(3), 133 | level!( 134 | 4, 135 | LevelLimits { 136 | max_pic_size: 665_856, 137 | max_h_size: 4352, 138 | max_v_size: 2448, 139 | max_display_rate: 19_975_680, 140 | max_decode_rate: 24_969_600, 141 | max_header_rate: 150, 142 | main_mbps: 6.0, 143 | high_mbps: 0.0, 144 | main_cr: 2, 145 | high_cr: 0, 146 | max_tiles: 16, 147 | max_tile_cols: 6, 148 | } 149 | ), 150 | level!( 151 | 5, 152 | LevelLimits { 153 | max_pic_size: 1_065_024, 154 | max_h_size: 5504, 155 | max_v_size: 3096, 156 | max_display_rate: 31_950_720, 157 | max_decode_rate: 39_938_400, 158 | max_header_rate: 150, 159 | main_mbps: 10.0, 160 | high_mbps: 0.0, 161 | main_cr: 2, 162 | high_cr: 0, 163 | max_tiles: 8, 164 | max_tile_cols: 4, 165 | } 166 | ), 167 | level!(6), 168 | level!(7), 169 | level!( 170 | 8, 171 | LevelLimits { 172 | max_pic_size: 2_359_296, 173 | max_h_size: 6144, 174 | max_v_size: 3456, 175 | max_display_rate: 70_778_880, 176 | max_decode_rate: 77_856_768, 177 | max_header_rate: 300, 178 | main_mbps: 12.0, 179 | high_mbps: 30.0, 180 | main_cr: 4, 181 | high_cr: 4, 182 | max_tiles: 32, 183 | max_tile_cols: 8, 184 | } 185 | ), 186 | level!( 187 | 9, 188 | LevelLimits { 189 | max_pic_size: 2_359_296, 190 | max_h_size: 6144, 191 | max_v_size: 3456, 192 | max_display_rate: 141_557_760, 193 | max_decode_rate: 155_713_536, 194 | max_header_rate: 300, 195 | main_mbps: 20.0, 196 | high_mbps: 50.0, 197 | main_cr: 4, 198 | high_cr: 4, 199 | max_tiles: 32, 200 | max_tile_cols: 8, 201 | } 202 | ), 203 | level!(10), 204 | level!(11), 205 | level!( 206 | 12, 207 | LevelLimits { 208 | max_pic_size: 8_912_896, 209 | max_h_size: 8192, 210 | max_v_size: 4352, 211 | max_display_rate: 267_386_880, 212 | max_decode_rate: 273_715_200, 213 | max_header_rate: 300, 214 | main_mbps: 30.0, 215 | high_mbps: 100.0, 216 | main_cr: 6, 217 | high_cr: 4, 218 | max_tiles: 64, 219 | max_tile_cols: 8, 220 | } 221 | ), 222 | level!( 223 | 13, 224 | LevelLimits { 225 | max_pic_size: 8_912_896, 226 | max_h_size: 8192, 227 | max_v_size: 4352, 228 | max_display_rate: 534_773_760, 229 | max_decode_rate: 547_430_400, 230 | max_header_rate: 300, 231 | main_mbps: 40.0, 232 | high_mbps: 160.0, 233 | main_cr: 8, 234 | high_cr: 4, 235 | max_tiles: 64, 236 | max_tile_cols: 8, 237 | } 238 | ), 239 | level!( 240 | 14, 241 | LevelLimits { 242 | max_pic_size: 8_912_896, 243 | max_h_size: 8192, 244 | max_v_size: 4352, 245 | max_display_rate: 1_069_547_520, 246 | max_decode_rate: 1_094_860_800, 247 | max_header_rate: 300, 248 | main_mbps: 60.0, 249 | high_mbps: 240.0, 250 | main_cr: 8, 251 | high_cr: 4, 252 | max_tiles: 64, 253 | max_tile_cols: 8, 254 | } 255 | ), 256 | level!( 257 | 15, 258 | LevelLimits { 259 | max_pic_size: 8_912_896, 260 | max_h_size: 8192, 261 | max_v_size: 4352, 262 | max_display_rate: 1_069_547_520, 263 | max_decode_rate: 1_176_502_272, 264 | max_header_rate: 300, 265 | main_mbps: 60.0, 266 | high_mbps: 240.0, 267 | main_cr: 8, 268 | high_cr: 4, 269 | max_tiles: 64, 270 | max_tile_cols: 8, 271 | } 272 | ), 273 | level!( 274 | 16, 275 | LevelLimits { 276 | max_pic_size: 35_651_584, 277 | max_h_size: 16384, 278 | max_v_size: 8704, 279 | max_display_rate: 1_069_547_520, 280 | max_decode_rate: 1_176_502_272, 281 | max_header_rate: 300, 282 | main_mbps: 60.0, 283 | high_mbps: 240.0, 284 | main_cr: 8, 285 | high_cr: 4, 286 | max_tiles: 128, 287 | max_tile_cols: 16, 288 | } 289 | ), 290 | level!( 291 | 17, 292 | LevelLimits { 293 | max_pic_size: 35_651_584, 294 | max_h_size: 16384, 295 | max_v_size: 8704, 296 | max_display_rate: 2_139_095_040, 297 | max_decode_rate: 2_189_721_600, 298 | max_header_rate: 300, 299 | main_mbps: 100.0, 300 | high_mbps: 480.0, 301 | main_cr: 8, 302 | high_cr: 4, 303 | max_tiles: 128, 304 | max_tile_cols: 16, 305 | } 306 | ), 307 | level!( 308 | 18, 309 | LevelLimits { 310 | max_pic_size: 35_651_584, 311 | max_h_size: 16384, 312 | max_v_size: 8704, 313 | max_display_rate: 4_278_190_080, 314 | max_decode_rate: 4_379_443_200, 315 | max_header_rate: 300, 316 | main_mbps: 160.0, 317 | high_mbps: 800.0, 318 | main_cr: 8, 319 | high_cr: 4, 320 | max_tiles: 128, 321 | max_tile_cols: 16, 322 | } 323 | ), 324 | level!( 325 | 19, 326 | LevelLimits { 327 | max_pic_size: 35_651_584, 328 | max_h_size: 16384, 329 | max_v_size: 8704, 330 | max_display_rate: 4_278_190_080, 331 | max_decode_rate: 4_706_009_088, 332 | max_header_rate: 300, 333 | main_mbps: 160.0, 334 | high_mbps: 800.0, 335 | main_cr: 8, 336 | high_cr: 4, 337 | max_tiles: 128, 338 | max_tile_cols: 16, 339 | } 340 | ), 341 | level!(20), 342 | level!(21), 343 | level!(22), 344 | level!(23), 345 | level!(24), 346 | level!(25), 347 | level!(26), 348 | level!(27), 349 | level!(28), 350 | level!(29), 351 | level!(30), 352 | level!( 353 | 31, 354 | LevelLimits { 355 | max_pic_size: std::u32::MAX, 356 | max_h_size: std::u16::MAX, 357 | max_v_size: std::u16::MAX, 358 | max_display_rate: std::u64::MAX, 359 | max_decode_rate: std::u64::MAX, 360 | max_header_rate: std::u16::MAX, 361 | main_mbps: std::f64::MAX, 362 | high_mbps: std::f64::MAX, 363 | main_cr: std::u8::MAX, 364 | high_cr: std::u8::MAX, 365 | max_tiles: std::u8::MAX, 366 | max_tile_cols: std::u8::MAX, 367 | } 368 | ), 369 | ]; 370 | 371 | pub fn calculate_min_pic_compress_ratio(tier: Tier, display_rate: f64) -> [f64; 32] { 372 | let mut min_pic_compress_ratio = [0_f64; 32]; 373 | 374 | for i in 0..32 { 375 | if let Some(limits) = LEVELS[i].1 { 376 | let speed_adjustment = display_rate / limits.max_display_rate as f64; 377 | let min_comp_basis = if tier == Tier::Main || i <= 7 { 378 | limits.main_cr 379 | } else { 380 | limits.high_cr 381 | }; 382 | 383 | // assuming still_picture is equal to 0 384 | min_pic_compress_ratio[i] = 0.8_f64.max(f64::from(min_comp_basis) * speed_adjustment); 385 | } 386 | } 387 | 388 | min_pic_compress_ratio 389 | } 390 | 391 | pub fn calculate_level(context: &SequenceContext) -> Level { 392 | for level in LEVELS.iter() { 393 | if let Some(limits) = level.1 { 394 | // Only Main tier exists for low levels. 395 | let mbps_valid = if context.tier == Tier::Main || level.0 <= 7 { 396 | limits.main_mbps >= context.mbps 397 | } else { 398 | limits.high_mbps >= context.mbps 399 | }; 400 | 401 | if limits.max_pic_size >= u32::from(context.pic_size.0) * u32::from(context.pic_size.1) 402 | && limits.max_h_size >= context.pic_size.0 403 | && limits.max_v_size >= context.pic_size.1 404 | && limits.max_display_rate >= context.display_rate 405 | && limits.max_decode_rate >= context.decode_rate 406 | && limits.max_header_rate >= context.header_rate 407 | && mbps_valid 408 | && limits.max_tiles >= context.tiles 409 | && limits.max_tile_cols >= context.tile_cols 410 | { 411 | return *level; 412 | } 413 | } 414 | } 415 | 416 | unreachable!("no suitable level found"); 417 | } 418 | 419 | #[cfg(test)] 420 | mod tests { 421 | use super::*; 422 | 423 | #[test] 424 | fn test_calculate_level_minimum_parameters() { 425 | let seq_ctx_min = SequenceContext::default(); 426 | 427 | assert_eq!(0, calculate_level(&seq_ctx_min).0); 428 | } 429 | 430 | #[test] 431 | fn test_calculate_level_maximum_parameters() { 432 | let seq_ctx_max = SequenceContext { 433 | tier: Tier::High, 434 | pic_size: (std::u16::MAX, std::u16::MAX), 435 | display_rate: std::u64::MAX, 436 | decode_rate: std::u64::MAX, 437 | header_rate: std::u16::MAX, 438 | mbps: std::f64::MAX, 439 | tiles: std::u8::MAX, 440 | tile_cols: std::u8::MAX, 441 | }; 442 | 443 | assert_eq!(31, calculate_level(&seq_ctx_max).0); 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate av1parser; 2 | extern crate clap; 3 | 4 | mod ivf; 5 | mod level; 6 | mod obu; 7 | 8 | use av1parser as av1p; 9 | use clap::{App, Arg}; 10 | use level::*; 11 | use std::collections::VecDeque; 12 | use std::fmt; 13 | use std::fmt::{Display, Formatter}; 14 | use std::fs::{File, OpenOptions}; 15 | use std::io; 16 | use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; 17 | use std::vec::Vec; 18 | 19 | #[derive(PartialEq)] 20 | enum Output<'a> { 21 | InPlace, 22 | File(&'a str), 23 | CommandLine, 24 | } 25 | 26 | /// Configuration parameters received via CLI 27 | struct AppConfig<'a> { 28 | verbose: bool, 29 | input: &'a str, 30 | output: Output<'a>, 31 | forced_level: Option, 32 | } 33 | 34 | /// Container-level stream metadata 35 | struct ContainerMetadata { 36 | /// Temporal resolution, such that `time_scale` units represent one second of real time 37 | /// Represented as a rational (numerator, denominator) 38 | time_scale: (u32, u32), 39 | /// Frame width and height in pixels 40 | resolution: (u16, u16), 41 | } 42 | 43 | impl ContainerMetadata { 44 | /// Provides the time base in floating point form 45 | fn time_scale(&self) -> f64 { 46 | f64::from(self.time_scale.0) / f64::from(self.time_scale.1) 47 | } 48 | } 49 | 50 | impl Display for ContainerMetadata { 51 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 52 | writeln!( 53 | f, 54 | "Time scale: {:.3} ({}/{})", 55 | self.time_scale(), 56 | self.time_scale.0, 57 | self.time_scale.1 58 | )?; 59 | writeln!(f, "Resolution: {}x{}", self.resolution.0, self.resolution.1)?; 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | /// Container-level frame metadata 66 | struct ContainerFrameMetadata { 67 | /// Size of the frame in bytes 68 | size: u32, 69 | /// Display timestamp of the frame at the time scale of the stream 70 | display_timestamp: u64, 71 | } 72 | 73 | impl Display for ContainerFrameMetadata { 74 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 75 | writeln!(f, "Frame @ {}: {} bytes", self.display_timestamp, self.size) 76 | } 77 | } 78 | 79 | fn main() -> io::Result<()> { 80 | /// Shortcut for fetching a Cargo environment variable. 81 | macro_rules! cargo_env { 82 | ($name: expr) => { 83 | env!(concat!("CARGO_PKG_", $name)) 84 | }; 85 | } 86 | 87 | // Generate a list of valid levels to validate the `forcedlevel` argument. 88 | let level_strings = LEVELS 89 | .iter() 90 | .filter(|&l| l.is_valid()) 91 | .map(|&l| l.0.to_string()) 92 | .collect::>(); 93 | 94 | // Define the command line interface. 95 | let matches = App::new(cargo_env!("NAME")) 96 | .version(cargo_env!("VERSION")) 97 | .author(cargo_env!("AUTHORS")) 98 | .about(cargo_env!("DESCRIPTION")) 99 | .arg( 100 | Arg::with_name("input") 101 | .short("i") 102 | .long("input") 103 | .value_name("INPUT_FILE") 104 | .help("Input filename") 105 | .required(true) 106 | .index(1), 107 | ) 108 | .arg( 109 | Arg::with_name("output") 110 | .short("o") 111 | .long("output") 112 | .value_name("OUTPUT_FILE") 113 | .help("Output filename"), 114 | ) 115 | .arg( 116 | Arg::with_name("inplace") 117 | .long("inplace") 118 | .help("Patch file in place"), 119 | ) 120 | .arg( 121 | Arg::with_name("forcedlevel") 122 | .short("f") 123 | .long("forcedlevel") 124 | .value_name("FORCED_LEVEL") 125 | .help("Force a level instead of calculating it") 126 | .possible_values(&level_strings.iter().map(|l| &**l).collect::>()), 127 | ) 128 | .arg( 129 | Arg::with_name("verbose") 130 | .short("v") 131 | .long("verbose") 132 | .help("Display verbose output, which may be helpful for debugging"), 133 | ) 134 | .get_matches(); 135 | 136 | // Parse command line input. 137 | if matches.is_present("output") && matches.is_present("inplace") { 138 | panic!("cannot specify an output file and in place at the same time"); 139 | } 140 | 141 | let config = AppConfig { 142 | verbose: matches.is_present("verbose"), 143 | input: matches.value_of("input").unwrap(), 144 | output: if matches.is_present("output") { 145 | Output::File(matches.value_of("output").unwrap()) 146 | } else if matches.is_present("inplace") { 147 | Output::InPlace 148 | } else { 149 | Output::CommandLine 150 | }, 151 | forced_level: if matches.is_present("forcedlevel") { 152 | Some( 153 | LEVELS[matches 154 | .value_of("forcedlevel") 155 | .unwrap() 156 | .parse::() 157 | .unwrap()], 158 | ) 159 | } else { 160 | None 161 | }, 162 | }; 163 | 164 | process_input(&config)?; 165 | 166 | Ok(()) 167 | } 168 | 169 | // TODO: split this function into smaller parts 170 | #[allow(clippy::cognitive_complexity)] 171 | fn process_input(config: &AppConfig) -> io::Result<()> { 172 | // Open the specified input file using a buffered reader. 173 | let input_file = OpenOptions::new() 174 | .read(true) 175 | .write(config.output == Output::InPlace) 176 | .open(config.input) 177 | .expect("could not open the specified input file"); 178 | let output_file: File; 179 | 180 | let mut reader = BufReader::new(input_file); 181 | let mut writer: BufWriter; 182 | 183 | let fmt = av1p::probe_fileformat(&mut reader).expect("could not probe the input file format"); 184 | reader.seek(SeekFrom::Start(0))?; 185 | 186 | let mut seq = av1p::av1::Sequence::new(); 187 | let mut seq_positions = Vec::new(); 188 | let mut seq_sizes = Vec::new(); 189 | 190 | let (mut max_tile_cols, mut max_tiles) = (0, 0); // the maximum tile parameters 191 | let mut max_display_rate = 0_f64; // max number of shown frames in a temporal unit (i.e. number of frame headers with show_frame or show_existing_frame) 192 | let mut max_decode_rate = 0_f64; // max number of decoded frames in a temporal unit (i.e. number of frame headers without show_existing_frame) 193 | let mut max_header_rate = 0_f64; // max number of frame and frame header (excluding show_existing_frame) OBUs in a temporal unit 194 | let mut min_cr_level_idx = 0; // minimum level index required to support the compressed ratio bound 195 | let mut max_mbps = 0_f64; // max bitrate in megabits per second 196 | let mut max_tile_list_bitrate = 0; // max bitrate for tile lists 197 | let mut max_tile_decode_rate = 0_f64; // max decode rate for tile lists 198 | 199 | let metadata = match fmt { 200 | av1p::FileFormat::IVF => { 201 | let header = ivf::parse_ivf_header(&mut reader, config.input)?; 202 | 203 | ContainerMetadata { 204 | // Note: the `framerate` field name (from av1parser) is inaccurate 205 | time_scale: (header.framerate, header.timescale), 206 | resolution: (header.width, header.height), 207 | } 208 | } 209 | _ => unimplemented!("non-IVF input not currently supported"), 210 | }; 211 | 212 | let time_scale = metadata.time_scale(); 213 | let picture_size = usize::from(metadata.resolution.0) * usize::from(metadata.resolution.1); 214 | 215 | if config.verbose { 216 | println!("Container metadata:"); 217 | println!("{}", metadata); 218 | } 219 | 220 | // TODO: do not parse the whole stream if setting a level manually 221 | let mut show_count = 0; // shown frame count for the current temporal unit 222 | let mut frame_count = 0; // decoded frame count for the current temporal unit 223 | let mut header_count = 0; // header count for the current temporal unit 224 | let mut last_tu_time = 0; // timestamp for the first frame of the last temporal unit 225 | let mut cur_tu_time = 0; // timestamp for the first frame of the current temporal unit 226 | let mut frame_size = 0_i64; // total compressed size for the current frame (includes frame, frame header, metadata, and tile group OBUs) 227 | let mut tu_size = 0; // total size of frames in the current temporal unit 228 | let mut tu_sizes = VecDeque::::new(); // one-second buffer for bitrate calculation per temporal unit 229 | let mut tu_times = VecDeque::::new(); // one-second buffer for time scale units taken per temporal unit 230 | let mut header_counts = VecDeque::::new(); // one-second buffer for number of headers per temporal unit 231 | let mut seen_frame_header = false; // refreshed with each temporal unit 232 | let mut min_compressed_ratio = std::f64::MAX; // min compression ratio for a single frame 233 | let mut tile_info = av1p::obu::TileInfo::default(); // last seen tile information 234 | 235 | let mut total_show_count = 0; // total number of displayed frames 236 | 237 | fn get_container_frame( 238 | reader: &mut R, 239 | fmt: &av1p::FileFormat, 240 | ) -> Option { 241 | match fmt { 242 | av1p::FileFormat::IVF => { 243 | if let Ok(frame) = av1p::ivf::parse_ivf_frame(reader) { 244 | ContainerFrameMetadata { 245 | size: frame.size, 246 | display_timestamp: frame.pts, 247 | } 248 | .into() 249 | } else { 250 | None 251 | } 252 | } 253 | _ => { 254 | unreachable!(); 255 | } 256 | } 257 | } 258 | 259 | // Read one frame from the container at a time. 260 | while let Some(frame) = get_container_frame(&mut reader, &fmt) { 261 | let mut sz = frame.size; 262 | let pts = frame.display_timestamp; 263 | 264 | let pos = reader.seek(SeekFrom::Current(0))?; 265 | 266 | // Read all AV1 OBUs in the container frame. 267 | while sz > 0 { 268 | let obu = av1p::obu::parse_obu_header(&mut reader, sz)?; 269 | 270 | sz -= obu.header_len + obu.obu_size; 271 | let pos = reader.seek(SeekFrom::Current(0))?; 272 | 273 | match obu.obu_type { 274 | av1p::obu::OBU_TEMPORAL_DELIMITER => { 275 | if pts == cur_tu_time { 276 | // duplicate temporal delimiter? 277 | continue; 278 | } 279 | 280 | let delta_time = (pts - cur_tu_time) as f64 / time_scale; 281 | 282 | let display_rate = f64::from(show_count) / delta_time; 283 | max_display_rate = max_display_rate.max(display_rate); 284 | max_decode_rate = max_decode_rate.max(f64::from(frame_count) / delta_time); 285 | //max_header_rate = max_header_rate.max(header_count as f64 / delta_time); 286 | 287 | // Calculate bitrate and header rate, windowed over one second (sampled every frame). 288 | // We assume that header rate is computed over one-second windows. 289 | // This is not clear in the specification, but seems implied. 290 | header_counts.push_back(header_count); 291 | tu_sizes.push_back(tu_size); 292 | tu_times.push_back(pts - cur_tu_time); 293 | 294 | let mut tu_times_sum = tu_times.iter().sum::() as f64; 295 | 296 | if tu_times_sum >= time_scale.round() { 297 | while tu_times_sum > time_scale.round() { 298 | header_counts.pop_front(); 299 | tu_sizes.pop_front(); 300 | tu_times.pop_front(); 301 | 302 | tu_times_sum = tu_times.iter().sum::() as f64 303 | } 304 | 305 | let factor = time_scale / tu_times_sum; // adjustment to measure rates per second 306 | 307 | let header_rate = f64::from(header_counts.iter().sum::()) * factor; 308 | max_header_rate = max_header_rate.max(header_rate); 309 | 310 | let mbps = 311 | f64::from(tu_sizes.iter().sum::()) * factor * 8.0 / 1_000_000.0; 312 | max_mbps = max_mbps.max(mbps); 313 | } 314 | 315 | if let Some(sh) = seq.sh { 316 | let tier = if sh.op[0].seq_tier == 0 { 317 | Tier::Main 318 | } else { 319 | Tier::High 320 | }; 321 | let min_pic_compressed_ratio = 322 | calculate_min_pic_compress_ratio(tier, display_rate); 323 | 324 | for (level_idx, compressed_ratio) in 325 | min_pic_compressed_ratio.iter().enumerate() 326 | { 327 | if min_compressed_ratio >= *compressed_ratio { 328 | min_cr_level_idx = min_cr_level_idx.max(level_idx); 329 | break; 330 | } 331 | } 332 | } 333 | 334 | total_show_count += show_count; 335 | 336 | show_count = 0; 337 | frame_count = 0; 338 | header_count = 0; 339 | tu_size = 0; 340 | min_compressed_ratio = std::f64::MAX; 341 | seen_frame_header = false; 342 | 343 | obu::process_obu(&mut reader, &mut seq, &obu); 344 | } 345 | av1p::obu::OBU_FRAME_HEADER | av1p::obu::OBU_FRAME => { 346 | if let Some(sh) = seq.sh { 347 | if obu.obu_type == av1p::obu::OBU_FRAME_HEADER { 348 | if frame_size > 0 { 349 | let profile_factor = match sh.seq_profile { 350 | 0 => 15, 351 | 1 => 30, 352 | _ => 36, 353 | }; 354 | let uncompressed_size = (picture_size * profile_factor) >> 3; // this assumes a fixed picture size} 355 | min_compressed_ratio = min_compressed_ratio 356 | .min(uncompressed_size as f64 / frame_size as f64); 357 | } 358 | 359 | frame_size = i64::from(obu.obu_size) - 128; // this assumes one frame header per frame, coming before other OBUs for this frame 360 | tu_size += obu.obu_size; 361 | } else { 362 | frame_size += i64::from(obu.obu_size); 363 | tu_size += obu.obu_size; 364 | } 365 | 366 | if let Some(fh) = av1p::obu::parse_frame_header( 367 | &mut reader, 368 | seq.sh.as_ref().unwrap(), 369 | &mut seq.rfman, 370 | ) { 371 | if !seen_frame_header { 372 | last_tu_time = cur_tu_time; 373 | cur_tu_time = pts; 374 | } 375 | seen_frame_header = true; 376 | 377 | if fh.show_frame || fh.show_existing_frame { 378 | show_count += 1; 379 | 380 | seq.rfman.output_process(&fh); 381 | } 382 | 383 | if !fh.show_existing_frame { 384 | header_count += 1; // TODO: detect and do not count duplicate frame headers 385 | frame_count += 1; 386 | seq.rfman.update_process(&fh); 387 | } 388 | 389 | tile_info = fh.tile_info; 390 | max_tile_cols = max_tile_cols.max(fh.tile_info.tile_cols); 391 | max_tiles = 392 | max_tiles.max(fh.tile_info.tile_cols * fh.tile_info.tile_rows); 393 | } 394 | } else { 395 | panic!("frame header found before sequence header"); 396 | } 397 | } 398 | av1p::obu::OBU_METADATA | av1p::obu::OBU_TILE_GROUP => { 399 | frame_size += i64::from(obu.obu_size); 400 | tu_size += obu.obu_size; 401 | } 402 | av1p::obu::OBU_TILE_LIST => { 403 | if let Some(tile_list) = av1p::obu::parse_tile_list(&mut reader) { 404 | let mut bytes_per_tile_list = 0; 405 | 406 | for entry in tile_list.tile_list_entries { 407 | bytes_per_tile_list += entry.tile_data_size_minus_1 + 1; 408 | } 409 | 410 | max_tile_list_bitrate = 411 | max_tile_list_bitrate.max(bytes_per_tile_list * 8 * 180); 412 | max_tile_decode_rate = max_tile_decode_rate.max( 413 | f64::from(metadata.resolution.0) / f64::from(tile_info.tile_cols) 414 | * f64::from(metadata.resolution.1) 415 | / f64::from(tile_info.tile_rows) 416 | * f64::from(tile_list.tile_count_minus_1 + 1) 417 | * 180.0, 418 | ); 419 | } 420 | } 421 | av1p::obu::OBU_SEQUENCE_HEADER => { 422 | // Track the start location and size of the sequence header OBU for patching. 423 | seq_positions.push(pos); 424 | obu::process_obu(&mut reader, &mut seq, &obu); 425 | seq_sizes.push(obu.obu_size); 426 | } 427 | _ => { 428 | obu::process_obu(&mut reader, &mut seq, &obu); 429 | } 430 | } 431 | 432 | reader.seek(SeekFrom::Start(pos + u64::from(obu.obu_size)))?; 433 | } 434 | 435 | reader.seek(SeekFrom::Start(pos + u64::from(frame.size)))?; 436 | } 437 | 438 | // Do the final updates for header/display/show rates. 439 | 440 | // Single frame clips don't move forward in time, so set a minimum delta of the framerate's inverse. 441 | let delta_time = ((cur_tu_time - last_tu_time) as f64 / time_scale) 442 | .max(1.0 / time_scale * cur_tu_time as f64); 443 | let display_rate = f64::from(show_count) / delta_time; 444 | max_display_rate = max_display_rate.max(display_rate); 445 | max_decode_rate = max_decode_rate 446 | .max(f64::from(frame_count) / delta_time) 447 | // Tile decode rate is restricted to the level's maximum decode rate halved, so double the input to achieve that effect. 448 | .max(max_tile_decode_rate * 2.0); 449 | 450 | header_counts.push_back(header_count); 451 | tu_sizes.push_back(tu_size); 452 | tu_times.push_back(cur_tu_time - last_tu_time); 453 | 454 | let mut tu_times_sum = tu_times.iter().sum::() as f64; 455 | 456 | // We do not want to interpolate for short clips, since their effective rate per second is the same as their total rate. 457 | // However, for clips that fill the one-second buffers, interpolation should occur for the last frame as well. 458 | let factor = if tu_times_sum >= time_scale.round() { 459 | time_scale / tu_times_sum 460 | } else { 461 | 1.0 462 | }; 463 | 464 | while tu_times_sum > time_scale.round() { 465 | header_counts.pop_front(); 466 | tu_sizes.pop_front(); 467 | tu_times.pop_front(); 468 | 469 | tu_times_sum = tu_times.iter().sum::() as f64 470 | } 471 | 472 | let header_rate = f64::from(header_counts.iter().sum::()) * factor; 473 | max_header_rate = max_header_rate.max(header_rate); 474 | 475 | let mbps = f64::from(tu_sizes.iter().sum::()) * factor * 8.0 / 1_000_000.0; 476 | max_mbps = max_mbps.max(mbps); 477 | 478 | let sh = seq.sh.unwrap(); // sequence header 479 | let tier = if sh.op[0].seq_tier == 0 { 480 | Tier::Main 481 | } else { 482 | Tier::High 483 | }; 484 | let min_pic_compressed_ratio = calculate_min_pic_compress_ratio(tier, display_rate); 485 | 486 | for (level_idx, compressed_ratio) in min_pic_compressed_ratio.iter().enumerate() { 487 | if min_compressed_ratio >= *compressed_ratio { 488 | min_cr_level_idx = min_cr_level_idx.max(level_idx); 489 | break; 490 | } 491 | } 492 | 493 | total_show_count += show_count; 494 | 495 | if sh.operating_points_cnt > 1 { 496 | unimplemented!("streams with multiple operating points not yet supported"); 497 | } 498 | 499 | if config.verbose { 500 | println!("Number of displayed frames: {}", total_show_count); 501 | 502 | println!( 503 | "Maximum header, display, and decode rates in a single temporal unit: {:.3}, {:.3}, {:.3}", 504 | max_header_rate, max_display_rate, max_decode_rate 505 | ); 506 | 507 | println!( 508 | "Minimum level required to satisfy compressed ratio constraint: {}", 509 | LEVELS[min_cr_level_idx] 510 | ); 511 | 512 | println!("Maximum bitrate: {:.3} Mbps", max_mbps); 513 | 514 | println!( 515 | "Maximum number of tiles and tile columns found: {}, {}", 516 | max_tiles, max_tile_cols 517 | ); 518 | } 519 | 520 | // Determine the output level. 521 | let level: Level = if config.forced_level.is_some() { 522 | config.forced_level.unwrap() 523 | } else { 524 | // Generate a SequenceContext using the parsed data. 525 | let seq_ctx = SequenceContext { 526 | tier: if sh.op[0].seq_tier == 0 { 527 | Tier::Main 528 | } else { 529 | Tier::High 530 | }, 531 | pic_size: (sh.max_frame_width as u16, sh.max_frame_height as u16), // (width, height) 532 | display_rate: (max_display_rate * picture_size as f64).ceil() as u64, 533 | decode_rate: (max_decode_rate * picture_size as f64).ceil() as u64, 534 | header_rate: max_header_rate.ceil() as u16, 535 | mbps: max_mbps, 536 | tiles: max_tiles as u8, 537 | tile_cols: max_tile_cols as u8, 538 | }; 539 | 540 | if config.verbose { 541 | println!(); 542 | println!("Sequence context:"); 543 | println!("{}", seq_ctx); 544 | } 545 | LEVELS[usize::from(calculate_level(&seq_ctx).0).max(min_cr_level_idx)] 546 | }; 547 | 548 | let old_level = &LEVELS[usize::from(sh.op[0].seq_level_idx)]; 549 | 550 | // Replace the level, if the output is to a file. 551 | if config.output != Output::CommandLine { 552 | // Copy the file contents from input to output if needed. 553 | let output_fname = match config.output { 554 | Output::InPlace => config.input, 555 | Output::File(fname) => fname, 556 | _ => unreachable!(), 557 | }; 558 | 559 | if config.output == Output::File(output_fname) { 560 | std::fs::copy(config.input, output_fname)?; 561 | } 562 | 563 | // Locate the first level byte by simply counting the bits that come before it. 564 | // This is only valid for single operating point sequences. 565 | // TODO: properly offset timing and decoder model info and any other missing data that is not decoded by av1parser 566 | // TODO: Maybe we shouldn't assume all sequence headers in a file match (making this valid to do out-of-loop)? 567 | let lv_bit_offset_in_seq = if sh.reduced_still_picture_header { 568 | 5 569 | } else { 570 | // When timing info is present, there may be more nested header data to skip, 571 | // but it is not currently handled by av1parser or coded by rav1e. 572 | 24 + if sh.timing_info_present_flag { 573 | unimplemented!() 574 | } else { 575 | 0 576 | } 577 | }; 578 | 579 | output_file = OpenOptions::new() 580 | .write(true) 581 | .open(output_fname) 582 | .expect("could not open the specified output file"); 583 | writer = BufWriter::new(output_file); 584 | 585 | // Basic sanity check 586 | assert_eq!( 587 | seq_positions.len(), 588 | seq_sizes.len(), 589 | "different amount of sequence header obu positions and sizes" 590 | ); 591 | 592 | for i in 0..seq_positions.len() { 593 | let seq_pos = seq_positions[i]; 594 | let seq_sz = seq_sizes[i]; 595 | // Both the reader and writer should point to the first byte which contains level bits. 596 | let lv_byte_offset = seq_pos + lv_bit_offset_in_seq / 8; 597 | reader.seek(SeekFrom::Start(lv_byte_offset))?; 598 | writer.seek(SeekFrom::Start(lv_byte_offset))?; 599 | 600 | // Determine the number of bits preceding the level in the byte. 601 | let lv_bit_offset_in_byte = lv_bit_offset_in_seq % 8; 602 | 603 | // Generate a bitstream-aligned two-byte sequence containing the level bits. 604 | let level_aligned = 605 | ((u32::from(level.0) << 11 >> lv_bit_offset_in_byte) as u16).to_be_bytes(); 606 | // Generate a two-byte mask to filter out the non-level bits. 607 | let level_bit_mask = 608 | (((0b0001_1111_u32) << 11 >> lv_bit_offset_in_byte) as u16).to_be_bytes(); 609 | // Generate a single bit mask to identify the tier bit, which immediately follows the level bits. 610 | let tier_bit_mask = 611 | (((0b0000_0001_u32) << 11 >> lv_bit_offset_in_byte) as u16 >> 1).to_be_bytes(); 612 | let post_tier_bit_mask = 613 | (((0b1111_1111_1111_1111) << 3 >> lv_bit_offset_in_byte >> 8 >> 1) as u16) 614 | .to_be_bytes(); 615 | 616 | if config.verbose { 617 | println!( 618 | "Patching sequence header #{} with offset {}", 619 | i, lv_bit_offset_in_byte 620 | ); 621 | 622 | if i == 0 { 623 | println!( 624 | "Level bits: {:#010b}, {:#010b}", 625 | level_aligned[0], level_aligned[1] 626 | ); 627 | println!( 628 | "Level/tier/post-tier bit masks: {:#018b}/{:#018b}/{:#018b}", 629 | u16::from_be_bytes(level_bit_mask), 630 | u16::from_be_bytes(tier_bit_mask), 631 | u16::from_be_bytes(post_tier_bit_mask) 632 | ); 633 | } 634 | } 635 | 636 | let mut byte_buf = [0_u8; 2]; 637 | reader 638 | .read_exact(&mut byte_buf) 639 | .expect("could not read the level byte(s)"); 640 | 641 | // Ensure that the bytes read from the input file correspond to the level parsed earlier. 642 | assert_eq!( 643 | old_level.0, 644 | (u32::from(u16::from_be_bytes(byte_buf)) >> 11 << lv_bit_offset_in_byte) as u8, 645 | "level at the location seeked to patch does not match the parsed value" 646 | ); 647 | 648 | if config.verbose { 649 | print!( 650 | "input/output bytes: {:#010b}, {:#010b} / ", 651 | byte_buf[0], byte_buf[1] 652 | ); 653 | } 654 | 655 | // Modify the input bytes such that the level bits match the target level. 656 | byte_buf[0] = byte_buf[0] & !level_bit_mask[0] | level_aligned[0]; 657 | byte_buf[1] = byte_buf[1] & !level_bit_mask[1] | level_aligned[1]; 658 | 659 | let tier_adjusted_bits: [u8; 2]; 660 | let mut next_input_byte = [0_u8; 1]; // when removing a tier bit (reader runs ahead) 661 | let mut carry_bit = 0_u8; // used when adding a tier bit (reader runs behind) 662 | 663 | if old_level.0 > 7 && level.0 <= 7 { 664 | // The tier bit must be removed. 665 | // In that case, ensure that the tier bit is 0 (Main tier). 666 | if byte_buf[0] & tier_bit_mask[0] > 0 || byte_buf[1] & tier_bit_mask[1] > 0 { 667 | panic!("cannot reduce level below 4.0 when High tier is specified"); 668 | } 669 | 670 | // Read one byte ahead, to shift the second byte in the current two-byte sequence. 671 | reader 672 | .read_exact(&mut next_input_byte) 673 | .expect("could not read the post-tier byte"); 674 | 675 | tier_adjusted_bits = [ 676 | (byte_buf[0] << 1) | (byte_buf[1] >> 7) & post_tier_bit_mask[0], 677 | (byte_buf[1] << 1 | (next_input_byte[0] >> 7) & post_tier_bit_mask[1]), 678 | ]; 679 | } else if old_level.0 <= 7 && level.0 > 7 { 680 | // The tier bit must be added. 681 | tier_adjusted_bits = [ 682 | (byte_buf[0] >> 1) & !tier_bit_mask[0], 683 | (byte_buf[1] >> 1) & !tier_bit_mask[1] | byte_buf[0] << 7, 684 | ]; 685 | 686 | // The last bit is shifted out of the two-byte range, and must be 687 | // stored to realign the rest of the bitstream. (TODO) 688 | carry_bit = byte_buf[1] << 7; 689 | } else { 690 | // No adjustment is needed. 691 | tier_adjusted_bits = byte_buf; 692 | } 693 | 694 | byte_buf[0] = level_aligned[0] 695 | | (tier_adjusted_bits[0] & (tier_bit_mask[0] | post_tier_bit_mask[0])); 696 | byte_buf[1] = level_aligned[1] 697 | | (tier_adjusted_bits[1] & (tier_bit_mask[1] | post_tier_bit_mask[1])); 698 | 699 | if config.verbose { 700 | println!("{:#010b}, {:#010b}", byte_buf[0], byte_buf[1]); 701 | } 702 | 703 | writer 704 | .write_all(&byte_buf) 705 | .expect("could not write the level byte(s)"); 706 | 707 | // Realign the rest of the sequence header OBU if needed (i.e. if a tier bit is added/removed). 708 | let mut pos_in_seq = lv_bit_offset_in_seq / 8 + 2; // writer's position within the sequence header 709 | let mut next_output_byte: u8; 710 | 711 | while pos_in_seq < seq_sz.into() { 712 | if old_level.0 > 7 && level.0 <= 7 { 713 | // Due to the earlier shifting, the reader is always one byte ahead. 714 | let prev_input_byte = next_input_byte; 715 | 716 | reader 717 | .read_exact(&mut next_input_byte) 718 | .expect("could not read sequence header OBU byte"); 719 | 720 | next_output_byte = (prev_input_byte[0] << 1) | (next_input_byte[0] >> 7); 721 | } else if old_level.0 <= 7 && level.0 > 7 { 722 | reader 723 | .read_exact(&mut next_input_byte) 724 | .expect("could not read sequence header OBU byte"); 725 | 726 | next_output_byte = next_input_byte[0] >> 1 | carry_bit; 727 | carry_bit = next_input_byte[0] << 7; 728 | } else { 729 | break; 730 | } 731 | 732 | writer 733 | .write_all(&[next_output_byte]) 734 | .expect("could not write sequence header OBU byte"); 735 | 736 | pos_in_seq += 1; 737 | } 738 | 739 | writer.flush()?; 740 | } 741 | } 742 | 743 | println!("Level: {} -> {}", old_level, level); 744 | 745 | Ok(()) 746 | } 747 | --------------------------------------------------------------------------------