video/vp6: Add VP6[A] video decoding support

Gated behind the "vp6" feature, enabled by default.
Utilizing a heavily stripped-down version of the NihAV project,
retaining only the VP6 decoder, relicensed under MIT.
Including VP6WithAlpha decoding, proper FrameDependency reporting,
and cropping the unwanted encoded pixels on the right/bottom manually.
This commit is contained in:
TÖRÖK Attila 2021-08-22 18:47:24 +02:00 committed by Mike Welsh
parent cd9206f08d
commit 3869950578
7 changed files with 258 additions and 4 deletions

25
Cargo.lock generated
View File

@ -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",

View File

@ -32,3 +32,12 @@ opt-level = 3
[profile.dev.package.h263-rs-yuv]
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

View File

@ -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 |

View File

@ -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 = []

View File

@ -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<VideoStreamHandle, Error> {
let decoder: Box<dyn VideoDecoder> = 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<FrameDependency, Error> {
// 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<DecodedFrame, Error> {
// 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))
}
}
}

View File

@ -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"]

View File

@ -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"