├── .gitignore ├── scripts ├── id_rsa.enc └── travis-doc-upload.cfg ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── resize.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /scripts/id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-rs/y4m/HEAD/scripts/id_rsa.enc -------------------------------------------------------------------------------- /scripts/travis-doc-upload.cfg: -------------------------------------------------------------------------------- 1 | PROJECT_NAME=y4m 2 | DOCS_REPO=PistonDevelopers/docs.git 3 | SSH_KEY_TRAVIS_ID=5d3a3cfaa652 4 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ '**' ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "y4m" 3 | version = "0.8.0" 4 | authors = ["Kagami Hiiragi "] 5 | description = "YUV4MPEG2 (.y4m) Encoder/Decoder." 6 | keywords = ["y4m", "YUV4MPEG2", "video", "decoder", "encoder"] 7 | license = "MIT" 8 | readme = "README.md" 9 | homepage = "https://github.com/image-rs/y4m" 10 | repository = "https://github.com/image-rs/y4m.git" 11 | documentation = "https://docs.rs/y4m" 12 | 13 | [dev-dependencies] 14 | resize = "0.8" 15 | rgb = "0.8" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2019 PistonDevelopers 4 | Copyright (c) 2019 image-rs contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # y4m [![Build Status](https://github.com/image-rs/y4m/actions/workflows/rust.yml/badge.svg)](https://github.com/image-rs/y4m/actions?query=branch%3Amaster) [![crates.io](https://img.shields.io/crates/v/y4m.svg)](https://crates.io/crates/y4m) 2 | 3 | YUV4MPEG2 (.y4m) Encoder/Decoder. [Format specification](https://wiki.multimedia.cx/index.php?title=YUV4MPEG2). 4 | 5 | ## Usage 6 | 7 | Simple stream copying: 8 | 9 | ```rust 10 | extern crate y4m; 11 | use std::io; 12 | 13 | let mut infh = io::stdin(); 14 | let mut outfh = io::stdout(); 15 | let mut dec = y4m::decode(&mut infh).unwrap(); 16 | let mut enc = y4m::encode(dec.get_width(), dec.get_height(), dec.get_framerate()) 17 | .with_colorspace(dec.get_colorspace()) 18 | .write_header(&mut outfh) 19 | .unwrap(); 20 | loop { 21 | match dec.read_frame() { 22 | Ok(frame) => if enc.write_frame(&frame).is_err() { break }, 23 | _ => break, 24 | } 25 | } 26 | ``` 27 | 28 | See [API documentation](https://docs.rs/y4m) for overview of all available methods. See also [this example](examples/resize.rs) on how to resize input y4m into grayscale y4m of different resolution: 29 | 30 | ```bash 31 | cargo build --release --example resize 32 | ffmpeg -i in.mkv -f yuv4mpegpipe - | target/release/examples/resize - 640x360 - | mpv - 33 | ``` 34 | 35 | ## License 36 | 37 | Library is licensed under [MIT](LICENSE). 38 | -------------------------------------------------------------------------------- /examples/resize.rs: -------------------------------------------------------------------------------- 1 | extern crate resize; 2 | extern crate rgb; 3 | extern crate y4m; 4 | 5 | use resize::Pixel::Gray8; 6 | use resize::Type::Triangle; 7 | use rgb::FromSlice; 8 | use std::env; 9 | use std::fs::File; 10 | use std::io; 11 | 12 | fn main() { 13 | let args: Vec<_> = env::args().collect(); 14 | if args.len() != 4 { 15 | return println!("Usage: {} in.y4m WxH out.y4m", args[0]); 16 | } 17 | 18 | let mut infh: Box = if args[1] == "-" { 19 | Box::new(io::stdin()) 20 | } else { 21 | Box::new(File::open(&args[1]).unwrap()) 22 | }; 23 | let mut decoder = y4m::decode(&mut infh).unwrap(); 24 | 25 | if decoder.get_bit_depth() != 8 { 26 | panic!( 27 | "Unsupported bit depth {}, this example only supports 8.", 28 | decoder.get_bit_depth() 29 | ); 30 | } 31 | let (w1, h1) = (decoder.get_width(), decoder.get_height()); 32 | let dst_dims: Vec<_> = args[2].split("x").map(|s| s.parse().unwrap()).collect(); 33 | let (w2, h2) = (dst_dims[0], dst_dims[1]); 34 | let mut resizer = resize::new(w1, h1, w2, h2, Gray8, Triangle).unwrap(); 35 | let mut dst = vec![0; w2 * h2]; 36 | 37 | let mut outfh: Box = if args[3] == "-" { 38 | Box::new(io::stdout()) 39 | } else { 40 | Box::new(File::create(&args[3]).unwrap()) 41 | }; 42 | let mut encoder = y4m::encode(w2, h2, decoder.get_framerate()) 43 | .with_colorspace(y4m::Colorspace::Cmono) 44 | .write_header(&mut outfh) 45 | .unwrap(); 46 | 47 | loop { 48 | match decoder.read_frame() { 49 | Ok(frame) => { 50 | resizer 51 | .resize(frame.get_y_plane().as_gray(), dst.as_gray_mut()) 52 | .unwrap(); 53 | let out_frame = y4m::Frame::new([&dst, &[], &[]], None); 54 | if encoder.write_frame(&out_frame).is_err() { 55 | break; 56 | } 57 | } 58 | _ => break, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # YUV4MPEG2 (.y4m) Encoder/Decoder 2 | #![deny(missing_docs)] 3 | 4 | use std::fmt; 5 | use std::io; 6 | use std::io::Read; 7 | use std::io::Write; 8 | use std::num; 9 | use std::str; 10 | 11 | const MAX_PARAMS_SIZE: usize = 1024; 12 | const FILE_MAGICK: &[u8] = b"YUV4MPEG2 "; 13 | const FRAME_MAGICK: &[u8] = b"FRAME"; 14 | const TERMINATOR: u8 = 0x0A; 15 | const FIELD_SEP: u8 = b' '; 16 | const RATIO_SEP: u8 = b':'; 17 | 18 | /// Both encoding and decoding errors. 19 | #[derive(Debug)] 20 | pub enum Error { 21 | /// End of the file. Technically not an error, but it's easier to process 22 | /// that way. 23 | EOF, 24 | /// Bad input parameters provided. 25 | BadInput, 26 | /// Unknown colorspace (possibly just unimplemented). 27 | UnknownColorspace, 28 | /// Error while parsing the file/frame header. 29 | // TODO(Kagami): Better granularity of parse errors. 30 | ParseError(ParseError), 31 | /// Error while reading/writing the file. 32 | IoError(io::Error), 33 | /// Out of memory (limits exceeded). 34 | OutOfMemory, 35 | } 36 | 37 | impl std::error::Error for crate::Error { 38 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 39 | match *self { 40 | Error::EOF => None, 41 | Error::BadInput => None, 42 | Error::UnknownColorspace => None, 43 | Error::ParseError(ref err) => Some(err), 44 | Error::IoError(ref err) => Some(err), 45 | Error::OutOfMemory => None, 46 | } 47 | } 48 | } 49 | 50 | impl fmt::Display for crate::Error { 51 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 52 | match *self { 53 | Error::EOF => write!(f, "End of file"), 54 | Error::BadInput => write!(f, "Bad input parameters provided"), 55 | Error::UnknownColorspace => write!(f, "Bad input parameters provided"), 56 | Error::ParseError(ref err) => err.fmt(f), 57 | Error::IoError(ref err) => err.fmt(f), 58 | Error::OutOfMemory => write!(f, "Out of memory (limits exceeded)"), 59 | } 60 | } 61 | } 62 | 63 | /// Granular ParseError Definiations 64 | pub enum ParseError { 65 | /// Error reading y4m header 66 | InvalidY4M, 67 | /// Error parsing int 68 | Int, 69 | /// Error parsing UTF8 70 | Utf8, 71 | /// General Parsing Error 72 | General, 73 | } 74 | 75 | impl std::error::Error for crate::ParseError { 76 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 77 | match *self { 78 | ParseError::InvalidY4M => None, 79 | ParseError::Int => None, 80 | ParseError::Utf8 => None, 81 | ParseError::General => None, 82 | } 83 | } 84 | } 85 | 86 | impl fmt::Display for ParseError { 87 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 88 | match self { 89 | ParseError::InvalidY4M => write!(f, "Error parsing y4m header"), 90 | ParseError::Int => write!(f, "Error parsing Int"), 91 | ParseError::Utf8 => write!(f, "Error parsing UTF8"), 92 | ParseError::General => write!(f, "General parsing error"), 93 | } 94 | } 95 | } 96 | 97 | impl fmt::Debug for ParseError { 98 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 99 | match self { 100 | ParseError::InvalidY4M => write!(f, "Error parsing y4m header"), 101 | ParseError::Int => write!(f, "Error parsing Int"), 102 | ParseError::Utf8 => write!(f, "Error parsing UTF8"), 103 | ParseError::General => write!(f, "General parsing error"), 104 | } 105 | } 106 | } 107 | 108 | macro_rules! parse_error { 109 | ($p:expr) => { 110 | return Err(Error::ParseError($p)) 111 | } 112 | } 113 | 114 | impl From for Error { 115 | fn from(err: io::Error) -> Error { 116 | match err.kind() { 117 | io::ErrorKind::UnexpectedEof => Error::EOF, 118 | _ => Error::IoError(err), 119 | } 120 | } 121 | } 122 | 123 | impl From for Error { 124 | fn from(_: num::ParseIntError) -> Error { 125 | Error::ParseError(ParseError::Int) 126 | } 127 | } 128 | 129 | impl From for Error { 130 | fn from(_: str::Utf8Error) -> Error { 131 | Error::ParseError(ParseError::Utf8) 132 | } 133 | } 134 | 135 | trait EnhancedRead { 136 | fn read_until(&mut self, ch: u8, buf: &mut [u8]) -> Result; 137 | } 138 | 139 | impl EnhancedRead for R { 140 | // Current implementation does one `read` call per byte. This might be a 141 | // bit slow for long headers but it simplifies things: we don't need to 142 | // check whether start of the next frame is already read and so on. 143 | fn read_until(&mut self, ch: u8, buf: &mut [u8]) -> Result { 144 | let mut collected = 0; 145 | while collected < buf.len() { 146 | let chunk_size = self.read(&mut buf[collected..=collected])?; 147 | if chunk_size == 0 { 148 | return Err(Error::EOF); 149 | } 150 | if buf[collected] == ch { 151 | return Ok(collected); 152 | } 153 | collected += chunk_size; 154 | } 155 | parse_error!(ParseError::General) 156 | } 157 | } 158 | 159 | fn parse_bytes(buf: &[u8]) -> Result { 160 | // A bit kludgy but seems like there is no other way. 161 | Ok(str::from_utf8(buf)?.parse()?) 162 | } 163 | 164 | /// A newtype wrapper around Vec to ensure validity as a vendor extension. 165 | #[derive(Debug, Clone)] 166 | pub struct VendorExtensionString(Vec); 167 | 168 | impl VendorExtensionString { 169 | /// Create a new vendor extension string. 170 | /// 171 | /// For example, setting to `b"COLORRANGE=FULL"` sets the interpretation of 172 | /// the YUV values to cover the full range (rather a limited "studio swing" 173 | /// range). 174 | /// 175 | /// The argument `x_option` must not contain a space (b' ') character, 176 | /// otherwise [Error::BadInput] is returned. 177 | pub fn new(value: Vec) -> Result { 178 | if value.contains(&b' ') { 179 | return Err(Error::BadInput); 180 | } 181 | Ok(VendorExtensionString(value)) 182 | } 183 | /// Get the vendor extension string. 184 | pub fn value(&self) -> &[u8] { 185 | self.0.as_slice() 186 | } 187 | } 188 | 189 | /// Simple ratio structure since stdlib lacks one. 190 | #[derive(Debug, Clone, Copy)] 191 | pub struct Ratio { 192 | /// Numerator. 193 | pub num: usize, 194 | /// Denominator. 195 | pub den: usize, 196 | } 197 | 198 | impl Ratio { 199 | /// Create a new ratio. 200 | pub fn new(num: usize, den: usize) -> Ratio { 201 | Ratio { num, den } 202 | } 203 | 204 | /// Parse a ratio from a byte slice. 205 | pub fn parse(value: &[u8]) -> Result { 206 | let parts: Vec<_> = value.splitn(2, |&b| b == RATIO_SEP).collect(); 207 | if parts.len() != 2 { 208 | parse_error!(ParseError::General) 209 | } 210 | let num = parse_bytes(parts[0])?; 211 | let den = parse_bytes(parts[1])?; 212 | Ok(Ratio::new(num, den)) 213 | } 214 | } 215 | 216 | impl fmt::Display for Ratio { 217 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 218 | write!(f, "{}:{}", self.num, self.den) 219 | } 220 | } 221 | 222 | /// Colorspace (color model/pixel format). Only subset of them is supported. 223 | /// 224 | /// From libavformat/yuv4mpegenc.c: 225 | /// 226 | /// > yuv4mpeg can only handle yuv444p, yuv422p, yuv420p, yuv411p and gray8 227 | /// pixel formats. And using 'strict -1' also yuv444p9, yuv422p9, yuv420p9, 228 | /// yuv444p10, yuv422p10, yuv420p10, yuv444p12, yuv422p12, yuv420p12, 229 | /// yuv444p14, yuv422p14, yuv420p14, yuv444p16, yuv422p16, yuv420p16, gray9, 230 | /// gray10, gray12 and gray16 pixel formats. 231 | #[derive(Debug, Clone, Copy)] 232 | #[non_exhaustive] 233 | pub enum Colorspace { 234 | /// Grayscale only, 8-bit. 235 | Cmono, 236 | /// Grayscale only, 12-bit. 237 | Cmono12, 238 | /// 4:2:0 with coincident chroma planes, 8-bit. 239 | C420, 240 | /// 4:2:0 with coincident chroma planes, 10-bit. 241 | C420p10, 242 | /// 4:2:0 with coincident chroma planes, 12-bit. 243 | C420p12, 244 | /// 4:2:0 with biaxially-displaced chroma planes, 8-bit. 245 | C420jpeg, 246 | /// 4:2:0 with coincident Cb and vertically-displaced Cr, 8-bit. 247 | C420paldv, 248 | /// 4:2:0 with vertically-displaced chroma planes, 8-bit. 249 | C420mpeg2, 250 | /// 4:2:2, 8-bit. 251 | C422, 252 | /// 4:2:2, 10-bit. 253 | C422p10, 254 | /// 4:2:2, 12-bit. 255 | C422p12, 256 | /// 4:4:4, 8-bit. 257 | C444, 258 | /// 4:4:4, 10-bit. 259 | C444p10, 260 | /// 4:4:4, 12-bit. 261 | C444p12, 262 | } 263 | 264 | impl Colorspace { 265 | /// Return the bit depth per sample 266 | #[inline] 267 | pub fn get_bit_depth(self) -> usize { 268 | match self { 269 | Colorspace::Cmono 270 | | Colorspace::C420 271 | | Colorspace::C422 272 | | Colorspace::C444 273 | | Colorspace::C420jpeg 274 | | Colorspace::C420paldv 275 | | Colorspace::C420mpeg2 => 8, 276 | Colorspace::C420p10 | Colorspace::C422p10 | Colorspace::C444p10 => 10, 277 | Colorspace::Cmono12 278 | | Colorspace::C420p12 279 | | Colorspace::C422p12 280 | | Colorspace::C444p12 => 12, 281 | } 282 | } 283 | 284 | /// Return the number of bytes in a sample 285 | #[inline] 286 | pub fn get_bytes_per_sample(self) -> usize { 287 | if self.get_bit_depth() <= 8 { 288 | 1 289 | } else { 290 | 2 291 | } 292 | } 293 | } 294 | 295 | fn get_plane_sizes(width: usize, height: usize, colorspace: Colorspace) -> (usize, usize, usize) { 296 | let y_plane_size = width * height * colorspace.get_bytes_per_sample(); 297 | 298 | let c420_chroma_size = 299 | ((width + 1) / 2) * ((height + 1) / 2) * colorspace.get_bytes_per_sample(); 300 | let c422_chroma_size = ((width + 1) / 2) * height * colorspace.get_bytes_per_sample(); 301 | 302 | let c420_sizes = (y_plane_size, c420_chroma_size, c420_chroma_size); 303 | let c422_sizes = (y_plane_size, c422_chroma_size, c422_chroma_size); 304 | let c444_sizes = (y_plane_size, y_plane_size, y_plane_size); 305 | 306 | match colorspace { 307 | Colorspace::Cmono | Colorspace::Cmono12 => (y_plane_size, 0, 0), 308 | Colorspace::C420 309 | | Colorspace::C420p10 310 | | Colorspace::C420p12 311 | | Colorspace::C420jpeg 312 | | Colorspace::C420paldv 313 | | Colorspace::C420mpeg2 => c420_sizes, 314 | Colorspace::C422 | Colorspace::C422p10 | Colorspace::C422p12 => c422_sizes, 315 | Colorspace::C444 | Colorspace::C444p10 | Colorspace::C444p12 => c444_sizes, 316 | } 317 | } 318 | 319 | /// Limits on the resources `Decoder` is allowed to use. 320 | #[derive(Clone, Copy, Debug)] 321 | pub struct Limits { 322 | /// Maximum allowed size of frame buffer, default is 1 GiB. 323 | pub bytes: usize, 324 | } 325 | 326 | impl Default for Limits { 327 | fn default() -> Limits { 328 | Limits { 329 | bytes: 1024 * 1024 * 1024, 330 | } 331 | } 332 | } 333 | 334 | /// YUV4MPEG2 decoder. 335 | pub struct Decoder { 336 | reader: R, 337 | params_buf: Vec, 338 | frame_buf: Vec, 339 | raw_params: Vec, 340 | width: usize, 341 | height: usize, 342 | framerate: Ratio, 343 | pixel_aspect: Ratio, 344 | colorspace: Colorspace, 345 | y_len: usize, 346 | u_len: usize, 347 | } 348 | 349 | impl Decoder { 350 | /// Create a new decoder instance. 351 | pub fn new(reader: R) -> Result, Error> { 352 | Decoder::new_with_limits(reader, Limits::default()) 353 | } 354 | 355 | /// Create a new decoder instance with custom limits. 356 | pub fn new_with_limits(mut reader: R, limits: Limits) -> Result, Error> { 357 | let mut params_buf = vec![0; MAX_PARAMS_SIZE]; 358 | let end_params_pos = reader.read_until(TERMINATOR, &mut params_buf)?; 359 | if end_params_pos < FILE_MAGICK.len() || !params_buf.starts_with(FILE_MAGICK) { 360 | parse_error!(ParseError::InvalidY4M) 361 | } 362 | let raw_params = params_buf[FILE_MAGICK.len()..end_params_pos].to_owned(); 363 | let mut width = 0; 364 | let mut height = 0; 365 | // Framerate is actually required per spec, but let's be a bit more 366 | // permissive as per ffmpeg behavior. 367 | let mut framerate = Ratio::new(25, 1); 368 | let mut pixel_aspect = Ratio::new(1, 1); 369 | let mut colorspace = None; 370 | // We shouldn't convert it to string because encoding is unspecified. 371 | for param in raw_params.split(|&b| b == FIELD_SEP) { 372 | if param.is_empty() { 373 | continue; 374 | } 375 | let (name, value) = (param[0], ¶m[1..]); 376 | // TODO(Kagami): interlacing, comment. 377 | match name { 378 | b'W' => width = parse_bytes(value)?, 379 | b'H' => height = parse_bytes(value)?, 380 | b'F' => framerate = Ratio::parse(value)?, 381 | b'A' => pixel_aspect = Ratio::parse(value)?, 382 | b'C' => { 383 | colorspace = match value { 384 | b"mono" => Some(Colorspace::Cmono), 385 | b"mono12" => Some(Colorspace::Cmono12), 386 | b"420" => Some(Colorspace::C420), 387 | b"420p10" => Some(Colorspace::C420p10), 388 | b"420p12" => Some(Colorspace::C420p12), 389 | b"422" => Some(Colorspace::C422), 390 | b"422p10" => Some(Colorspace::C422p10), 391 | b"422p12" => Some(Colorspace::C422p12), 392 | b"444" => Some(Colorspace::C444), 393 | b"444p10" => Some(Colorspace::C444p10), 394 | b"444p12" => Some(Colorspace::C444p12), 395 | b"420jpeg" => Some(Colorspace::C420jpeg), 396 | b"420paldv" => Some(Colorspace::C420paldv), 397 | b"420mpeg2" => Some(Colorspace::C420mpeg2), 398 | _ => return Err(Error::UnknownColorspace), 399 | } 400 | } 401 | _ => {} 402 | } 403 | } 404 | let colorspace = colorspace.unwrap_or(Colorspace::C420); 405 | if width == 0 || height == 0 { 406 | parse_error!(ParseError::General) 407 | } 408 | let (y_len, u_len, v_len) = get_plane_sizes(width, height, colorspace); 409 | let frame_size = y_len + u_len + v_len; 410 | if frame_size > limits.bytes { 411 | return Err(Error::OutOfMemory); 412 | } 413 | let frame_buf = vec![0; frame_size]; 414 | Ok(Decoder { 415 | reader, 416 | params_buf, 417 | frame_buf, 418 | raw_params, 419 | width, 420 | height, 421 | framerate, 422 | pixel_aspect, 423 | colorspace, 424 | y_len, 425 | u_len, 426 | }) 427 | } 428 | 429 | /// Iterate over frames. End of input is indicated by `Error::EOF`. 430 | pub fn read_frame(&mut self) -> Result { 431 | let end_params_pos = self.reader.read_until(TERMINATOR, &mut self.params_buf)?; 432 | if end_params_pos < FRAME_MAGICK.len() || !self.params_buf.starts_with(FRAME_MAGICK) { 433 | parse_error!(ParseError::InvalidY4M) 434 | } 435 | // We don't parse frame params currently but user has access to them. 436 | let start_params_pos = FRAME_MAGICK.len(); 437 | let raw_params = if end_params_pos - start_params_pos > 0 { 438 | // Check for extra space. 439 | if self.params_buf[start_params_pos] != FIELD_SEP { 440 | parse_error!(ParseError::InvalidY4M) 441 | } 442 | Some(self.params_buf[start_params_pos + 1..end_params_pos].to_owned()) 443 | } else { 444 | None 445 | }; 446 | self.reader.read_exact(&mut self.frame_buf)?; 447 | Ok(Frame::new( 448 | [ 449 | &self.frame_buf[0..self.y_len], 450 | &self.frame_buf[self.y_len..self.y_len + self.u_len], 451 | &self.frame_buf[self.y_len + self.u_len..], 452 | ], 453 | raw_params, 454 | )) 455 | } 456 | 457 | /// Return file width. 458 | #[inline] 459 | pub fn get_width(&self) -> usize { 460 | self.width 461 | } 462 | /// Return file height. 463 | #[inline] 464 | pub fn get_height(&self) -> usize { 465 | self.height 466 | } 467 | /// Return file framerate. 468 | #[inline] 469 | pub fn get_framerate(&self) -> Ratio { 470 | self.framerate 471 | } 472 | /// Return file pixel aspect. 473 | #[inline] 474 | pub fn get_pixel_aspect(&self) -> Ratio { 475 | self.pixel_aspect 476 | } 477 | /// Return file colorspace. 478 | /// 479 | /// **NOTE:** normally all .y4m should have colorspace param, but there are 480 | /// files encoded without that tag and it's unclear what should we do in 481 | /// that case. Currently C420 is implied by default as per ffmpeg behavior. 482 | #[inline] 483 | pub fn get_colorspace(&self) -> Colorspace { 484 | self.colorspace 485 | } 486 | /// Return file raw parameters. 487 | #[inline] 488 | pub fn get_raw_params(&self) -> &[u8] { 489 | &self.raw_params 490 | } 491 | /// Return the bit depth per sample 492 | #[inline] 493 | pub fn get_bit_depth(&self) -> usize { 494 | self.colorspace.get_bit_depth() 495 | } 496 | /// Return the number of bytes in a sample 497 | #[inline] 498 | pub fn get_bytes_per_sample(&self) -> usize { 499 | self.colorspace.get_bytes_per_sample() 500 | } 501 | } 502 | 503 | /// A single frame. 504 | #[derive(Debug)] 505 | pub struct Frame<'f> { 506 | planes: [&'f [u8]; 3], 507 | raw_params: Option>, 508 | } 509 | 510 | impl<'f> Frame<'f> { 511 | /// Create a new frame with optional parameters. 512 | /// No heap allocations are made. 513 | pub fn new(planes: [&'f [u8]; 3], raw_params: Option>) -> Frame<'f> { 514 | Frame { planes, raw_params } 515 | } 516 | 517 | /// Create a new frame from data in 16-bit format. 518 | pub fn from_u16(planes: [&'f [u16]; 3], raw_params: Option>) -> Frame<'f> { 519 | Frame::new( 520 | [ 521 | unsafe { 522 | std::slice::from_raw_parts::( 523 | planes[0].as_ptr() as *const u8, 524 | planes[0].len() * 2, 525 | ) 526 | }, 527 | unsafe { 528 | std::slice::from_raw_parts::( 529 | planes[1].as_ptr() as *const u8, 530 | planes[1].len() * 2, 531 | ) 532 | }, 533 | unsafe { 534 | std::slice::from_raw_parts::( 535 | planes[2].as_ptr() as *const u8, 536 | planes[2].len() * 2, 537 | ) 538 | }, 539 | ], 540 | raw_params, 541 | ) 542 | } 543 | 544 | /// Return Y (first) plane. 545 | #[inline] 546 | pub fn get_y_plane(&self) -> &[u8] { 547 | self.planes[0] 548 | } 549 | /// Return U (second) plane. Empty in case of grayscale. 550 | #[inline] 551 | pub fn get_u_plane(&self) -> &[u8] { 552 | self.planes[1] 553 | } 554 | /// Return V (third) plane. Empty in case of grayscale. 555 | #[inline] 556 | pub fn get_v_plane(&self) -> &[u8] { 557 | self.planes[2] 558 | } 559 | /// Return frame raw parameters if any. 560 | #[inline] 561 | pub fn get_raw_params(&self) -> Option<&[u8]> { 562 | self.raw_params.as_ref().map(|v| &v[..]) 563 | } 564 | } 565 | 566 | /// Encoder builder. Allows to set y4m file parameters using builder pattern. 567 | // TODO(Kagami): Accept all known tags and raw params. 568 | #[derive(Debug)] 569 | pub struct EncoderBuilder { 570 | width: usize, 571 | height: usize, 572 | framerate: Ratio, 573 | pixel_aspect: Ratio, 574 | colorspace: Colorspace, 575 | vendor_extensions: Vec>, 576 | } 577 | 578 | impl EncoderBuilder { 579 | /// Create a new encoder builder. 580 | pub fn new(width: usize, height: usize, framerate: Ratio) -> EncoderBuilder { 581 | EncoderBuilder { 582 | width, 583 | height, 584 | framerate, 585 | pixel_aspect: Ratio::new(1, 1), 586 | colorspace: Colorspace::C420, 587 | vendor_extensions: vec![], 588 | } 589 | } 590 | 591 | /// Specify file colorspace. 592 | pub fn with_colorspace(mut self, colorspace: Colorspace) -> Self { 593 | self.colorspace = colorspace; 594 | self 595 | } 596 | 597 | /// Specify file pixel aspect. 598 | pub fn with_pixel_aspect(mut self, pixel_aspect: Ratio) -> Self { 599 | self.pixel_aspect = pixel_aspect; 600 | self 601 | } 602 | 603 | /// Add vendor extension. 604 | pub fn append_vendor_extension(mut self, x_option: VendorExtensionString) -> Self { 605 | self.vendor_extensions.push(x_option.0); 606 | self 607 | } 608 | 609 | /// Write header to the stream and create encoder instance. 610 | pub fn write_header(self, mut writer: W) -> Result, Error> { 611 | // XXX(Kagami): Beware that FILE_MAGICK already contains space. 612 | writer.write_all(FILE_MAGICK)?; 613 | write!( 614 | writer, 615 | "W{} H{} F{}", 616 | self.width, self.height, self.framerate 617 | )?; 618 | if self.pixel_aspect.num != 1 || self.pixel_aspect.den != 1 { 619 | write!(writer, " A{}", self.pixel_aspect)?; 620 | } 621 | for x_option in self.vendor_extensions.iter() { 622 | write!(writer, " X")?; 623 | writer.write_all(x_option)?; 624 | } 625 | write!(writer, " {:?}", self.colorspace)?; 626 | writer.write_all(&[TERMINATOR])?; 627 | let (y_len, u_len, v_len) = get_plane_sizes(self.width, self.height, self.colorspace); 628 | Ok(Encoder { 629 | writer, 630 | y_len, 631 | u_len, 632 | v_len, 633 | }) 634 | } 635 | } 636 | 637 | /// YUV4MPEG2 encoder. 638 | pub struct Encoder { 639 | writer: W, 640 | y_len: usize, 641 | u_len: usize, 642 | v_len: usize, 643 | } 644 | 645 | impl Encoder { 646 | /// Write next frame to the stream. 647 | pub fn write_frame(&mut self, frame: &Frame) -> Result<(), Error> { 648 | if frame.get_y_plane().len() != self.y_len 649 | || frame.get_u_plane().len() != self.u_len 650 | || frame.get_v_plane().len() != self.v_len 651 | { 652 | return Err(Error::BadInput); 653 | } 654 | self.writer.write_all(FRAME_MAGICK)?; 655 | if let Some(params) = frame.get_raw_params() { 656 | self.writer.write_all(&[FIELD_SEP])?; 657 | self.writer.write_all(params)?; 658 | } 659 | self.writer.write_all(&[TERMINATOR])?; 660 | self.writer.write_all(frame.get_y_plane())?; 661 | self.writer.write_all(frame.get_u_plane())?; 662 | self.writer.write_all(frame.get_v_plane())?; 663 | Ok(()) 664 | } 665 | 666 | /// Flush the underlying writer. 667 | pub fn flush(&mut self) -> Result<(), Error> { 668 | self.writer.flush().map_err(|e| Error::IoError(e)) 669 | } 670 | 671 | /// Return the underlying writer. 672 | pub fn into_inner(self) -> W { 673 | self.writer 674 | } 675 | } 676 | 677 | /// Create a new decoder instance. Alias for `Decoder::new`. 678 | pub fn decode(reader: R) -> Result, Error> { 679 | Decoder::new(reader) 680 | } 681 | 682 | /// Create a new encoder builder. Alias for `EncoderBuilder::new`. 683 | pub fn encode(width: usize, height: usize, framerate: Ratio) -> EncoderBuilder { 684 | EncoderBuilder::new(width, height, framerate) 685 | } 686 | --------------------------------------------------------------------------------