Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

video/external: Make OpenH264 support optional at build time #17878

Merged
merged 3 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ flate2 = "1.0.33"
futures = "0.3.30"
image = { version = "0.25.2", default-features = false }
js-sys = "0.3.70"
web-sys = "0.3.70"
log = "0.4"
num-derive = "0.4.2"
num-traits = "0.2.19"
Expand Down
2 changes: 1 addition & 1 deletion desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ ruffle_core = { path = "../core", features = ["audio", "clap", "mp3", "nellymose
ruffle_render = { path = "../render", features = ["clap"] }
ruffle_render_wgpu = { path = "../render/wgpu", features = ["clap"] }
ruffle_video_software = { path = "../video/software", optional = true }
ruffle_video_external = { path = "../video/external", optional = true }
ruffle_video_external = { path = "../video/external", features = ["openh264"], optional = true }
ruffle_frontend_utils = { path = "../frontend-utils", features = ["cpal"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
Expand Down
15 changes: 8 additions & 7 deletions desktop/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,19 +228,20 @@ impl ActivePlayer {
if cfg!(feature = "external_video") && preferences.openh264_enabled() {
#[cfg(feature = "external_video")]
{
use ruffle_video_external::backend::ExternalVideoBackend;
use ruffle_video_external::{
backend::ExternalVideoBackend, decoder::openh264::OpenH264Codec,
};
let openh264 = tokio::task::block_in_place(|| {
ExternalVideoBackend::load_openh264(&opt.cache_directory.join("video"))
OpenH264Codec::load(&opt.cache_directory.join("video"))
});
let openh264 = match openh264 {
Ok(codec) => Some(codec),
let backend = match openh264 {
Ok(codec) => ExternalVideoBackend::new_with_openh264(codec),
Err(e) => {
tracing::error!("Failed to load OpenH264: {}", e);
None
ExternalVideoBackend::new()
}
};

builder = builder.with_video(ExternalVideoBackend::new(openh264));
builder = builder.with_video(backend);
}
} else {
#[cfg(feature = "software_video")]
Expand Down
2 changes: 1 addition & 1 deletion render/canvas/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ruffle_render = { path = "..", features = ["web"] }
swf = { path = "../../swf" }

[dependencies.web-sys]
version = "0.3.69"
workspace = true
features = [
"CanvasGradient", "CanvasPattern", "CanvasRenderingContext2d", "CanvasWindingRule", "CssStyleDeclaration",
"Document", "DomMatrix", "Element", "HtmlCanvasElement", "ImageData", "Navigator", "Path2d", "SvgMatrix",
Expand Down
2 changes: 1 addition & 1 deletion render/webgl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ swf = { path = "../../swf" }
thiserror = { workspace = true }

[dependencies.web-sys]
version = "0.3.70"
workspace = true
features = [
"HtmlCanvasElement", "OesVertexArrayObject", "WebGl2RenderingContext", "WebGlBuffer", "WebglDebugRendererInfo",
"WebGlFramebuffer", "WebGlProgram", "WebGlRenderbuffer", "WebGlRenderingContext", "WebGlShader", "WebGlTexture",
Expand Down
2 changes: 1 addition & 1 deletion render/wgpu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ workspace = true

# wasm
[target.'cfg(target_family = "wasm")'.dependencies.web-sys]
version = "0.3.70"
workspace = true
features = ["HtmlCanvasElement"]

[features]
Expand Down
2 changes: 1 addition & 1 deletion tests/framework/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ruffle_render = { path = "../../render", features = ["serde"] }
ruffle_input_format = { path = "../input-format" }
ruffle_socket_format = { path = "../socket-format" }
ruffle_video_software = { path = "../../video/software", optional = true }
ruffle_video_external = { path = "../../video/external", optional = true }
ruffle_video_external = { path = "../../video/external", features = ["openh264"], optional = true }
image = { workspace = true, features = ["png"] }
regex = "1.10.6"
url = { workspace = true }
Expand Down
8 changes: 5 additions & 3 deletions tests/framework/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,14 @@ impl PlayerOptions {
let current_exe = std::env::current_exe()?;
let directory = current_exe.parent().expect("Executable parent dir");

use ruffle_video_external::backend::ExternalVideoBackend;
let openh264 = ExternalVideoBackend::load_openh264(directory)
use ruffle_video_external::{
backend::ExternalVideoBackend, decoder::openh264::OpenH264Codec,
};
let openh264 = OpenH264Codec::load(directory)
.map_err(|e| anyhow!("Couldn't load OpenH264: {}", e))?;

player_builder =
player_builder.with_video(ExternalVideoBackend::new(Some(openh264)));
player_builder.with_video(ExternalVideoBackend::new_with_openh264(openh264));
}

#[cfg(all(
Expand Down
15 changes: 9 additions & 6 deletions video/external/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ ruffle_video_software = { path = "../software" }
thiserror = { workspace = true }

# Needed for OpenH264:
libloading = "0.8.5"
reqwest = { version = "0.12.7", default-features = false, features = ["blocking"] }
hex = "0.4.3"
bzip2 = { version = "0.4.4", features = ["static"] }
tempfile = "3.12.0"
sha2 = "0.10.8"
libloading = { version = "0.8.5", optional = true }
reqwest = { version = "0.12.7", default-features = false, features = ["blocking"], optional = true }
hex = { version = "0.4.3", optional = true }
bzip2 = { version = "0.4.4", features = ["static"], optional = true }
tempfile = { version = "3.12.0", optional = true }
sha2 = { version = "0.10.8", optional = true }

[features]
openh264 = ["libloading", "reqwest", "hex", "bzip2", "tempfile", "sha2"]
160 changes: 23 additions & 137 deletions video/external/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
#[cfg(feature = "openh264")]
use crate::decoder::openh264::OpenH264Codec;
use crate::decoder::VideoDecoder;
use bzip2::read::BzDecoder;

use ruffle_render::backend::RenderBackend;
use ruffle_render::bitmap::{BitmapHandle, BitmapInfo, PixelRegion};
use ruffle_video::backend::VideoBackend;
use ruffle_video::error::Error;
use ruffle_video::frame::{EncodedFrame, FrameDependency};
use ruffle_video::VideoStreamHandle;
use ruffle_video_software::backend::SoftwareVideoBackend;
use sha2::{Digest, Sha256};
use slotmap::SlotMap;
use std::fs::File;
use std::io::copy;
use std::path::{Path, PathBuf};

use swf::{VideoCodec, VideoDeblocking};

enum ProxyOrStream {
Expand All @@ -25,154 +23,46 @@ enum ProxyOrStream {
Owned(VideoStream),
}

struct OpenH264Data {
local_filenames: Vec<&'static str>,
download_filename: &'static str,
download_sha256: &'static str,
}

/// A video backend that falls back to the software backend for most codecs,
/// except for H.264, for which it uses an external decoder.
pub struct ExternalVideoBackend {
streams: SlotMap<VideoStreamHandle, ProxyOrStream>,
#[cfg(feature = "openh264")]
openh264_codec: Option<OpenH264Codec>,
software: SoftwareVideoBackend,
}

impl Default for ExternalVideoBackend {
fn default() -> Self {
Self::new(None)
Self::new()
}
}

impl ExternalVideoBackend {
fn get_openh264_data() -> Result<OpenH264Data, Box<dyn std::error::Error>> {
const OS: &str = std::env::consts::OS;
const ARCH: &str = std::env::consts::ARCH;

let local_filenames = match OS {
"linux" => vec!["libopenh264.so.7", "libopenh264.so.2.4.1", "libopenh264.so"],
// TODO: investigate other OSes
_ => vec![],
};

// Source: https://github.com/cisco/openh264/releases/tag/v2.4.1
let (download_filename, download_sha256) = match (OS, ARCH) {
("linux", "x86") => (
"libopenh264-2.4.1-linux32.7.so",
"b7cf0e407f99056d90cbf62787a34820a7595b2129b165319d50766e00a66704",
),
("linux", "x86_64") => (
"libopenh264-2.4.1-linux64.7.so",
"1392d21466bc638e68151b716d5b2086d54cd812afd43253f1adb5b6e0185f51",
),
("linux", "arm") => (
"libopenh264-2.4.1-linux-arm.7.so",
"fd1dfb27d30bb72e903c9d2b4c650104a4369d2e7ffe8a4a533e8db2e7e9b19e",
),
("linux", "aarch64") => (
"libopenh264-2.4.1-linux-arm64.7.so",
"e8ea7e42855ceb4a90e7bd0b3abeba0c58b5f97166e8b0a30eefd58e099557a4",
),
("macos", "x86_64") => (
"libopenh264-2.4.1-mac-x64.dylib",
"cc0ba518a63791c37571f3c851f0aa03a4fbda5410acc214ecd4f24f8d1c478e",
),
("macos", "aarch64") => (
"libopenh264-2.4.1-mac-arm64.dylib",
"213ff93831cfa3dd6d7ad0c3a3403a6ceedf4ac1341e1278b5b869d42fefb496",
),
("windows", "x86") => (
"openh264-2.4.1-win32.dll",
"83270149640469c994a62cc32a6d8c0413cd7b802b7f1f2f532159f5bdc1cedd",
),
("windows", "x86_64") => (
"openh264-2.4.1-win64.dll",
"081b0c081480d177cbfddfbc90b1613640e702f875897b30d8de195cde73dd34",
),
(os, arch) => return Err(format!("Unsupported OS/arch: {}/{}", os, arch).into()),
};

Ok(OpenH264Data {
local_filenames,
download_filename,
download_sha256,
})
}

fn download_openh264(
openh264_data: &OpenH264Data,
directory: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
// See the license at: https://www.openh264.org/BINARY_LICENSE.txt
const URL_BASE: &str = "http://ciscobinary.openh264.org/";
const URL_SUFFIX: &str = ".bz2";

let (filename, sha256sum) = (
openh264_data.download_filename,
openh264_data.download_sha256,
);

std::fs::create_dir_all(directory)?;
let filepath = directory.join(filename);

// If the binary doesn't exist in the expected location, download it.
if !filepath.is_file() {
let url = format!("{}{}{}", URL_BASE, filename, URL_SUFFIX);
let response = reqwest::blocking::get(url)?;
let mut bzip2_reader = BzDecoder::new(response);

let mut tempfile = tempfile::NamedTempFile::with_prefix_in(filename, directory)?;
copy(&mut bzip2_reader, &mut tempfile)?;
// Let's assume that if this fails, it's because another process has already put it there
// and loaded it, therefore it can't be overwritten (on Windows at least), but in the end,
// all's fine - the hash will still be checked before attempting to load the library.
let _ = tempfile.persist(&filepath);
fn make_decoder(&mut self) -> Result<Box<dyn VideoDecoder>, Error> {
#[cfg(feature = "openh264")]
if let Some(h264_codec) = self.openh264_codec.as_ref() {
let decoder = Box::new(crate::decoder::openh264::H264Decoder::new(h264_codec));
return Ok(decoder);
}

// Regardless of whether the library was already there, or we just downloaded it, let's check the MD5 hash.
let mut sha256 = Sha256::new();
copy(&mut File::open(filepath.clone())?, &mut sha256)?;
let sha256digest = sha256.finalize();
let result: [u8; 32] = sha256digest.into();

if result[..] != hex::decode(sha256sum)?[..] {
let size = filepath.metadata().map(|f| f.len()).unwrap_or_default();
return Err(format!(
"SHA256 checksum mismatch for {filename}; expected {sha256sum}, found {sha256digest:x} (with a size of {size} bytes)",
)
.into());
}

Ok(filepath)
Err(Error::DecoderError("No OpenH264".into()))
}

pub fn load_openh264(directory: &Path) -> Result<OpenH264Codec, Box<dyn std::error::Error>> {
let openh264_data = Self::get_openh264_data()?;

for filename in &openh264_data.local_filenames {
match OpenH264Codec::new(filename) {
Ok(codec) => return Ok(codec),
Err(err) => {
tracing::warn!(
"Failed to load system OpenH264 library {}: {}",
filename,
err
);
}
}
pub fn new() -> Self {
Self {
streams: SlotMap::with_key(),
#[cfg(feature = "openh264")]
openh264_codec: None,
software: SoftwareVideoBackend::new(),
}

tracing::info!("Downloading OpenH264 library");
let filename = Self::download_openh264(&openh264_data, directory)?;
tracing::info!("Using OpenH264 at {:?}", filename);
Ok(OpenH264Codec::new(&filename)?)
}

pub fn new(openh264_codec: Option<OpenH264Codec>) -> Self {
#[cfg(feature = "openh264")]
pub fn new_with_openh264(openh264_codec: OpenH264Codec) -> Self {
Self {
streams: SlotMap::with_key(),
openh264_codec,
openh264_codec: Some(openh264_codec),
software: SoftwareVideoBackend::new(),
}
}
Expand All @@ -189,13 +79,9 @@ impl VideoBackend for ExternalVideoBackend {
filter: VideoDeblocking,
) -> Result<VideoStreamHandle, Error> {
let proxy_or_stream = if codec == VideoCodec::H264 {
if let Some(h264_codec) = self.openh264_codec.as_ref() {
let decoder = Box::new(crate::decoder::openh264::H264Decoder::new(h264_codec));
let stream = VideoStream::new(decoder);
ProxyOrStream::Owned(stream)
} else {
return Err(Error::DecoderError("No OpenH264".into()));
}
let decoder = self.make_decoder()?;
let stream = VideoStream::new(decoder);
ProxyOrStream::Owned(stream)
} else {
ProxyOrStream::Proxied(
self.software
Expand Down
2 changes: 2 additions & 0 deletions video/external/src/decoder.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// bindgen ../openh264/codec/api/wels/codec_api.h --no-prepend-enum-name \
// --dynamic-loading OpenH264 -o openh264_sys.rs
#[cfg(feature = "openh264")]
#[allow(non_upper_case_globals)]
#[allow(non_camel_case_types)]
#[allow(non_snake_case)]
#[allow(dead_code)]
mod openh264_sys;

#[cfg(feature = "openh264")]
pub mod openh264;

pub use ruffle_video_software::decoder::VideoDecoder;
Loading