diff --git a/Cargo.lock b/Cargo.lock index ec899b279af0..5e16c1bb1c05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2363,6 +2363,28 @@ dependencies = [ "rustdct", ] +[[package]] +name = "nihav_codec_support" +version = "0.1.0" +source = "git+https://github.com/ruffle-rs/nihav-vp6?rev=9416fcc9fc8aab8f4681aa9093b42922214abbd3#9416fcc9fc8aab8f4681aa9093b42922214abbd3" +dependencies = [ + "nihav_core", +] + +[[package]] +name = "nihav_core" +version = "0.1.0" +source = "git+https://github.com/ruffle-rs/nihav-vp6?rev=9416fcc9fc8aab8f4681aa9093b42922214abbd3#9416fcc9fc8aab8f4681aa9093b42922214abbd3" + +[[package]] +name = "nihav_duck" +version = "0.1.0" +source = "git+https://github.com/ruffle-rs/nihav-vp6?rev=9416fcc9fc8aab8f4681aa9093b42922214abbd3#9416fcc9fc8aab8f4681aa9093b42922214abbd3" +dependencies = [ + "nihav_codec_support", + "nihav_core", +] + [[package]] name = "nix" version = "0.18.0" @@ -3070,6 +3092,9 @@ dependencies = [ "lzma-rs", "minimp3", "nellymoser-rs", + "nihav_codec_support", + "nihav_core", + "nihav_duck", "num-derive", "num-traits", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index 83773d565747..f02342052db5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,13 @@ panic = "abort" opt-level = 3 [profile.dev.package.h263-rs-yuv] -opt-level = 3 \ No newline at end of file +opt-level = 3 + +[profile.dev.package.nihav_core] +opt-level = 3 + +[profile.dev.package.nihav_codec_support] +opt-level = 3 + +[profile.dev.package.nihav_duck] +opt-level = 3 diff --git a/LICENSE.md b/LICENSE.md index 5c0f320e9280..8f8b0ba525ec 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -438,6 +438,7 @@ Ruffle depends on third-party libraries with compatible licenses. | [ndk-sys](https://github.com/rust-windowing/android-ndk-rs) | [Apache-2.0](#Apache-20)/[MIT](#MIT) | Copyright (c) The Rust Windowing contributors | | nellymoser-rs | [MIT](#MIT) | Copyright (c) 2021 relrelb Copyright (c) 2007 a840bda5870ba11f19698ff6eb9581dfb0f95fa5, 539459aeb7d425140b62a3ec7dbf6dc8e408a306, and 520e17cd55896441042b14df2566a6eb610ed444 Copyright (c) 2007 Loic Minier, Benjamin Larsson. Derived from MIT licensed source in the nelly2pcm and FFmpeg projects. | | [net2](https://github.com/deprecrated/net2-rs) | [Apache-2.0](#Apache-20)/[MIT](#MIT) | Copyright (c) 2014 The Rust Project Developers | +| [nihav-vp6](https://github.com/ruffle-rs/nihav-vp6) | [MIT](#MIT) | Copyright (c) 2021 Kostya Shishkov | | [nix](https://github.com/nix-rust/nix) | [MIT](#MIT) | Copyright (c) 2015 Carl Lerche + nix-rust Authors | | [nom](https://github.com/Geal/nom) | [MIT](#MIT) | Copyright (c) 2014-2019 Geoffroy Couprie | | [num-complex](https://github.com/rust-num/num-complex) | [Apache-2.0](#Apache-20)/[MIT](#MIT) | Copyright (c) 2014 The Rust Project Developers | diff --git a/core/Cargo.toml b/core/Cargo.toml index 937526dc16eb..9f917ac54c0f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -43,6 +43,9 @@ json = "0.12.4" lzma-rs = {version = "0.2.0", optional = true } dasp = { git = "https://github.com/RustAudio/dasp", rev = "f05a703", features = ["interpolate", "interpolate-linear", "signal"] } symphonia = { version = "0.4.0", default-features = false, features = ["mp3"], optional = true } +nihav_core = { git = "https://github.com/ruffle-rs/nihav-vp6", rev = "9416fcc9fc8aab8f4681aa9093b42922214abbd3", optional = true } +nihav_codec_support = { git = "https://github.com/ruffle-rs/nihav-vp6", rev = "9416fcc9fc8aab8f4681aa9093b42922214abbd3", optional = true } +nihav_duck = { git = "https://github.com/ruffle-rs/nihav-vp6", rev = "9416fcc9fc8aab8f4681aa9093b42922214abbd3", optional = true } [dependencies.jpeg-decoder] version = "0.1.22" @@ -54,6 +57,7 @@ approx = "0.5.0" [features] default = ["minimp3", "serde"] h263 = ["h263-rs", "h263-rs-yuv"] +vp6 = ["nihav_core", "nihav_codec_support", "nihav_duck", "h263-rs-yuv"] lzma = ["lzma-rs", "swf/lzma"] wasm-bindgen = [ "instant/wasm-bindgen" ] avm_debug = [] diff --git a/core/src/backend/video/software.rs b/core/src/backend/video/software.rs index f04e0bf748dc..2ca7a19fe3ee 100644 --- a/core/src/backend/video/software.rs +++ b/core/src/backend/video/software.rs @@ -32,13 +32,17 @@ impl VideoBackend for SoftwareVideoBackend { fn register_video_stream( &mut self, _num_frames: u32, - _size: (u16, u16), + size: (u16, u16), codec: VideoCodec, _filter: VideoDeblocking, ) -> Result { let decoder: Box = match codec { #[cfg(feature = "h263")] VideoCodec::H263 => Box::new(h263::H263Decoder::new()), + #[cfg(feature = "vp6")] + VideoCodec::Vp6 => Box::new(vp6::Vp6Decoder::new(false, size)), + #[cfg(feature = "vp6")] + VideoCodec::Vp6WithAlpha => Box::new(vp6::Vp6Decoder::new(true, size)), _ => return Err(format!("Unsupported video codec type {:?}", codec).into()), }; let stream = VideoStream::new(decoder); @@ -198,3 +202,213 @@ mod h263 { } } } + +#[cfg(feature = "vp6")] +mod vp6 { + use crate::backend::video::software::VideoDecoder; + use crate::backend::video::{DecodedFrame, EncodedFrame, Error, FrameDependency}; + + use h263_rs_yuv::bt601::yuv420_to_rgba; + + use nihav_codec_support::codecs::NAVideoInfo; + use nihav_codec_support::codecs::{NABufferType::Video, YUV420_FORMAT}; + use nihav_core::codecs::NADecoderSupport; + use nihav_duck::codecs::vp6::{VP56Decoder, VP56Parser, VP6BR}; + use nihav_duck::codecs::vpcommon::{BoolCoder, VP_YUVA420_FORMAT}; + + /// VP6 video decoder. + pub struct Vp6Decoder { + with_alpha: bool, + bounds: (u16, u16), + decoder: VP56Decoder, + support: NADecoderSupport, + bitreader: VP6BR, + init_called: bool, + } + + impl Vp6Decoder { + pub fn new(with_alpha: bool, bounds: (u16, u16)) -> Self { + // Unfortunately, `init()` cannot be called on the decoder + // just yet, because `bounds` is only the declared size of + // the video, to which it will be cropped. + // This can be (rarely) even much smaller than the actual + // encoded size of the frames. + // `VP56Decoder::init()` needs the full encoded frame size, + // so it can allocate its internal buffers accordingly. + // The encoded frame size will be parsed from the header of + // the first encoded frame passed to `Self::decode_frame()`. + + Self { + with_alpha, + bounds, + decoder: VP56Decoder::new(6, with_alpha, true), + support: NADecoderSupport::new(), + bitreader: VP6BR::new(), + init_called: false, + } + } + } + + impl VideoDecoder for Vp6Decoder { + fn preload_frame( + &mut self, + encoded_frame: EncodedFrame<'_>, + ) -> Result { + // Luckily the very first bit of the encoded frames is exactly this flag, + // so we don't have to bother asking any "proper" decoder or parser. + Ok( + if !encoded_frame.data.is_empty() && (encoded_frame.data[0] & 0b_1000_0000) == 0 { + FrameDependency::None + } else { + FrameDependency::Past + }, + ) + } + + fn decode_frame(&mut self, encoded_frame: EncodedFrame<'_>) -> Result { + // If this is the first frame, the decoder needs to be initialized. + + if !self.init_called { + let mut bool_coder = BoolCoder::new(if self.with_alpha { + // The 24 bits alpha offset needs to be skipped first in this case + &encoded_frame.data[3..] + } else { + encoded_frame.data + }) + .map_err(|error| { + Error::from(format!("Error constructing VP6 bool coder: {:?}", error)) + })?; + + let header = self + .bitreader + .parse_header(&mut bool_coder) + .map_err(|error| { + Error::from(format!("Error parsing VP6 frame header: {:?}", error)) + })?; + + let video_info = NAVideoInfo::new( + header.disp_w as usize * 16, + header.disp_h as usize * 16, + true, + if self.with_alpha { + VP_YUVA420_FORMAT + } else { + YUV420_FORMAT + }, + ); + + self.decoder + .init(&mut self.support, video_info) + .map_err(|error| { + Error::from(format!("Error initializing VP6 decoder: {:?}", error)) + })?; + + self.init_called = true; + } + + // Actually decoding the frame and extracting the buffer it is stored in. + + let decoded = self + .decoder + .decode_frame(&mut self.support, encoded_frame.data, &mut self.bitreader) + .map_err(|error| Error::from(format!("VP6 decoder error: {:?}", error)))?; + + let frame = match decoded { + (Video(buffer), _) => Ok(buffer), + _ => Err(Error::from( + "Unexpected buffer type after decoding a VP6 frame", + )), + }?; + + // Converting it from YUV420 to RGBA. + + let yuv = frame.get_data(); + + let (mut width, mut height) = frame.get_dimensions(0); + let (chroma_width, chroma_height) = frame.get_dimensions(1); + + // We assume that there is no padding between rows + debug_assert!(frame.get_stride(0) == frame.get_dimensions(0).0); + debug_assert!(frame.get_stride(1) == frame.get_dimensions(1).0); + debug_assert!(frame.get_stride(2) == frame.get_dimensions(2).0); + + // Where each plane starts in the buffer + let offsets = ( + frame.get_offset(0), + frame.get_offset(1), + frame.get_offset(2), + ); + + let mut rgba = yuv420_to_rgba( + &yuv[offsets.0..offsets.0 + width * height], + &yuv[offsets.1..offsets.1 + chroma_width * chroma_height], + &yuv[offsets.2..offsets.2 + chroma_width * chroma_height], + width, + chroma_width, + ); + + // Adding in the alpha component, if present. + + if self.with_alpha { + debug_assert!(frame.get_stride(3) == frame.get_dimensions(3).0); + let alpha_offset = frame.get_offset(3); + let alpha = &yuv[alpha_offset..alpha_offset + width * height]; + for (alpha, rgba) in alpha.iter().zip(rgba.chunks_mut(4)) { + // The SWF spec mandates the `min` to avoid any accidental "invalid" + // premultiplied colors, which would cause strange results after blending. + // And the alpha data is encoded in full range (0-255), unlike the Y + // component of the main color data, so no remapping is needed. + rgba.copy_from_slice(&[ + u8::min(rgba[0], *alpha), + u8::min(rgba[1], *alpha), + u8::min(rgba[2], *alpha), + *alpha, + ]); + } + } + + // Cropping the encoded frame (containing whole macroblocks) to the + // size requested by the the bounds attribute. + + let &bounds = &self.bounds; + + if width < bounds.0 as usize || height < bounds.1 as usize { + log::warn!("A VP6 video frame is smaller than the bounds of the stream it belongs in. This is not supported."); + // Flash Player just produces a black image in this case! + } + + if width > bounds.0 as usize { + // Removing the unwanted pixels on the right edge (most commonly: unused pieces of macroblocks) + // by squishing all the rows tightly next to each other. + // Bitmap at the moment does not allow these gaps, so we need to remove them. + let new_width = bounds.0 as usize; + let new_height = usize::min(height, bounds.1 as usize); + // no need to move the first row, nor any rows on the bottom that will end up being cropped entirely + for row in 1..new_height { + rgba.copy_within( + row * width * 4..(row * width + new_width) * 4, + row * new_width * 4, + ); + } + width = new_width; + height = new_height; + } + + // Cropping the unwanted rows on the bottom, also dropping any unused space at the end left by the squish above + height = usize::min(height, bounds.1 as usize); + rgba.truncate(width * height * 4); + + Ok(DecodedFrame { + width: width as u16, + height: height as u16, + rgba, + }) + } + } + + impl Default for Vp6Decoder { + fn default() -> Self { + Self::new(false, (0, 0)) + } + } +} diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index a3d35069247c..090805506c77 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -29,12 +29,13 @@ winapi = "0.3.9" embed-resource = "1" [features] -default = ["h263"] +default = ["h263", "vp6"] # core features avm_debug = ["ruffle_core/avm_debug"] h263 = ["ruffle_core/h263"] lzma = ["ruffle_core/lzma"] +vp6 = ["ruffle_core/vp6"] # wgpu features render_debug_labels = ["ruffle_render_wgpu/render_debug_labels"] diff --git a/web/Cargo.toml b/web/Cargo.toml index 39b699f76cb2..f2ba7a0e227f 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -48,7 +48,7 @@ base64 = "0.13.0" [dependencies.ruffle_core] path = "../core" default-features = false -features = ["h263", "serde", "wasm-bindgen"] +features = ["h263", "vp6", "serde", "wasm-bindgen"] [dependencies.web-sys] version = "0.3.50"