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:
parent
cd9206f08d
commit
3869950578
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue