Initial commit: RustDesk 1.4.7 macOS desktop port
- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common) - Patched Flutter 3.44 / Dart 3.12 compatibility: * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64 * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0 * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64 - Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter - Verified flutter build macos --debug and --release both produce working .app - Verified .dmg installer (27MB arm64) created via hdiutil - Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
use crate::android::ffi::*;
|
||||
use crate::{Frame, Pixfmt};
|
||||
use lazy_static::lazy_static;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::{io, time::Duration};
|
||||
|
||||
lazy_static! {
|
||||
pub(crate) static ref SCREEN_SIZE: Mutex<(u16, u16, u16)> = Mutex::new((0, 0, 0)); // (width, height, scale)
|
||||
}
|
||||
|
||||
pub struct Capturer {
|
||||
display: Display,
|
||||
rgba: Vec<u8>,
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
Ok(Capturer {
|
||||
display,
|
||||
rgba: Vec::new(),
|
||||
saved_raw_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.display.width() as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.display.height() as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::TraitCapturer for Capturer {
|
||||
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
if get_video_raw(&mut self.rgba, &mut self.saved_raw_data).is_some() {
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
&self.rgba,
|
||||
self.width(),
|
||||
self.height(),
|
||||
)))
|
||||
} else {
|
||||
return Err(io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PixelBuffer<'a> {
|
||||
data: &'a [u8],
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a> PixelBuffer<'a> {
|
||||
pub fn new(data: &'a [u8], width: usize, height: usize) -> Self {
|
||||
let stride0 = data.len() / height;
|
||||
let mut stride = Vec::new();
|
||||
stride.push(stride0);
|
||||
PixelBuffer {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
self.stride.clone()
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt {
|
||||
Pixfmt::RGBA
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display {
|
||||
default: bool,
|
||||
rect: Rect,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
struct Rect {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub w: u16,
|
||||
pub h: u16,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
let mut size = SCREEN_SIZE.lock().unwrap();
|
||||
if size.0 == 0 || size.1 == 0 {
|
||||
*size = get_size().unwrap_or_default();
|
||||
}
|
||||
Ok(Display {
|
||||
default: true,
|
||||
rect: Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: size.0,
|
||||
h: size.1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn all() -> io::Result<Vec<Display>> {
|
||||
Ok(vec![Display::primary()?])
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.rect.w as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.rect.h as usize
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
let r = self.rect;
|
||||
(r.x as _, r.y as _)
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
self.default
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
"Android".into()
|
||||
}
|
||||
|
||||
pub fn refresh_size() {
|
||||
let mut size = SCREEN_SIZE.lock().unwrap();
|
||||
*size = get_size().unwrap_or_default();
|
||||
}
|
||||
|
||||
// Big android screen size will be shrinked, to improve performance when screen-capturing and encoding
|
||||
// e.g 2280x1080 size will be set to 1140x540, and `scale` is 2
|
||||
// need to multiply by `4` (2*2) when compute the bitrate
|
||||
pub fn fix_quality() -> u16 {
|
||||
let scale = SCREEN_SIZE.lock().unwrap().2;
|
||||
if scale <= 0 {
|
||||
1
|
||||
} else {
|
||||
scale * scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_size() -> Option<(u16, u16, u16)> {
|
||||
let res = call_main_service_get_by_name("screen_size").ok()?;
|
||||
if let Ok(json) = serde_json::from_str::<HashMap<String, Value>>(&res) {
|
||||
if let (Some(Value::Number(w)), Some(Value::Number(h)), Some(Value::Number(scale))) =
|
||||
(json.get("width"), json.get("height"), json.get("scale"))
|
||||
{
|
||||
let w = w.as_i64()? as _;
|
||||
let h = h.as_i64()? as _;
|
||||
let scale = scale.as_i64()? as _;
|
||||
return Some((w, h, scale));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_start() -> Option<bool> {
|
||||
let res = call_main_service_get_by_name("is_start").ok()?;
|
||||
Some(res == "true")
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/aom_ffi.rs"));
|
||||
|
||||
use crate::codec::{base_bitrate, codec_thread_num};
|
||||
use crate::{codec::EncoderApi, EncodeFrame, STRIDE_ALIGN};
|
||||
use crate::{common::GoogleImage, generate_call_macro, generate_call_ptr_macro, Error, Result};
|
||||
use crate::{EncodeInput, EncodeYuvFormat, Pixfmt};
|
||||
use hbb_common::{
|
||||
anyhow::{anyhow, Context},
|
||||
bytes::Bytes,
|
||||
log,
|
||||
message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
|
||||
ResultType,
|
||||
};
|
||||
use std::{ptr, slice};
|
||||
|
||||
generate_call_macro!(call_aom, false);
|
||||
generate_call_macro!(call_aom_allow_err, true);
|
||||
generate_call_ptr_macro!(call_aom_ptr);
|
||||
|
||||
impl Default for aom_codec_enc_cfg_t {
|
||||
fn default() -> Self {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for aom_codec_ctx_t {
|
||||
fn default() -> Self {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for aom_image_t {
|
||||
fn default() -> Self {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AomEncoderConfig {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub quality: f32,
|
||||
pub keyframe_interval: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct AomEncoder {
|
||||
ctx: aom_codec_ctx_t,
|
||||
width: usize,
|
||||
height: usize,
|
||||
i444: bool,
|
||||
yuvfmt: EncodeYuvFormat,
|
||||
}
|
||||
|
||||
// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/video_coding/codecs/av1/libaom_av1_encoder.cc
|
||||
mod webrtc {
|
||||
use super::*;
|
||||
|
||||
const kUsageProfile: u32 = AOM_USAGE_REALTIME;
|
||||
const kBitDepth: u32 = 8;
|
||||
const kLagInFrames: u32 = 0; // No look ahead.
|
||||
pub(super) const kTimeBaseDen: i64 = 1000;
|
||||
|
||||
// Only positive speeds, range for real-time coding currently is: 6 - 8.
|
||||
// Lower means slower/better quality, higher means fastest/lower quality.
|
||||
fn get_cpu_speed(width: u32, height: u32) -> u32 {
|
||||
// aux_config_ = nullptr, kComplexityHigh
|
||||
if width * height <= 320 * 180 {
|
||||
8
|
||||
} else if width * height <= 640 * 360 {
|
||||
9
|
||||
} else {
|
||||
10
|
||||
}
|
||||
}
|
||||
|
||||
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
|
||||
use aom_superblock_size::*;
|
||||
let resolution = width * height;
|
||||
if threads >= 4 && resolution >= 960 * 540 && resolution < 1920 * 1080 {
|
||||
AOM_SUPERBLOCK_SIZE_64X64
|
||||
} else {
|
||||
AOM_SUPERBLOCK_SIZE_DYNAMIC
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enc_cfg(
|
||||
i: *const aom_codec_iface,
|
||||
cfg: AomEncoderConfig,
|
||||
i444: bool,
|
||||
) -> ResultType<aom_codec_enc_cfg> {
|
||||
let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
|
||||
call_aom!(aom_codec_enc_config_default(i, &mut c, kUsageProfile));
|
||||
|
||||
// Overwrite default config with input encoder settings & RTC-relevant values.
|
||||
c.g_w = cfg.width;
|
||||
c.g_h = cfg.height;
|
||||
c.g_threads = codec_thread_num(64) as _;
|
||||
c.g_timebase.num = 1;
|
||||
c.g_timebase.den = kTimeBaseDen as _;
|
||||
c.g_input_bit_depth = kBitDepth;
|
||||
if let Some(keyframe_interval) = cfg.keyframe_interval {
|
||||
c.kf_min_dist = 0;
|
||||
c.kf_max_dist = keyframe_interval as _;
|
||||
} else {
|
||||
c.kf_mode = aom_kf_mode::AOM_KF_DISABLED;
|
||||
}
|
||||
let (q_min, q_max) = AomEncoder::calc_q_values(cfg.quality);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate = AomEncoder::bitrate(cfg.width as _, cfg.height as _, cfg.quality);
|
||||
c.rc_undershoot_pct = 50;
|
||||
c.rc_overshoot_pct = 50;
|
||||
c.rc_buf_initial_sz = 600;
|
||||
c.rc_buf_optimal_sz = 600;
|
||||
c.rc_buf_sz = 1000;
|
||||
c.g_usage = kUsageProfile;
|
||||
c.g_error_resilient = 0;
|
||||
// Low-latency settings.
|
||||
c.rc_end_usage = aom_rc_mode::AOM_CBR; // Constant Bit Rate (CBR) mode
|
||||
c.g_pass = aom_enc_pass::AOM_RC_ONE_PASS; // One-pass rate control
|
||||
c.g_lag_in_frames = kLagInFrames; // No look ahead when lag equals 0.
|
||||
|
||||
// https://aomedia.googlesource.com/aom/+/refs/tags/v3.6.0/av1/common/enums.h#82
|
||||
c.g_profile = if i444 { 1 } else { 0 };
|
||||
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
pub fn set_controls(ctx: *mut aom_codec_ctx_t, cfg: &aom_codec_enc_cfg) -> ResultType<()> {
|
||||
use aom_tune_content::*;
|
||||
use aome_enc_control_id::*;
|
||||
macro_rules! call_ctl {
|
||||
($ctx:expr, $av1e:expr, $arg:expr) => {{
|
||||
call_aom_allow_err!(aom_codec_control($ctx, $av1e as i32, $arg));
|
||||
}};
|
||||
}
|
||||
|
||||
call_ctl!(ctx, AOME_SET_CPUUSED, get_cpu_speed(cfg.g_w, cfg.g_h));
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_CDEF, 1);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_TPL_MODEL, 0);
|
||||
call_ctl!(ctx, AV1E_SET_DELTAQ_MODE, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_ORDER_HINT, 0);
|
||||
call_ctl!(ctx, AV1E_SET_AQ_MODE, 3);
|
||||
call_ctl!(ctx, AOME_SET_MAX_INTRA_BITRATE_PCT, 300);
|
||||
call_ctl!(ctx, AV1E_SET_COEFF_COST_UPD_FREQ, 3);
|
||||
call_ctl!(ctx, AV1E_SET_MODE_COST_UPD_FREQ, 3);
|
||||
call_ctl!(ctx, AV1E_SET_MV_COST_UPD_FREQ, 3);
|
||||
// kScreensharing
|
||||
call_ctl!(ctx, AV1E_SET_TUNE_CONTENT, AOM_CONTENT_SCREEN);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_PALETTE, 1);
|
||||
let tile_set = if cfg.g_threads == 4 && cfg.g_w == 640 && (cfg.g_h == 360 || cfg.g_h == 480)
|
||||
{
|
||||
AV1E_SET_TILE_ROWS
|
||||
} else {
|
||||
AV1E_SET_TILE_COLUMNS
|
||||
};
|
||||
// Failed on android
|
||||
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
|
||||
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
|
||||
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_WARPED_MOTION, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_GLOBAL_MOTION, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_REF_FRAME_MVS, 0);
|
||||
call_ctl!(
|
||||
ctx,
|
||||
AV1E_SET_SUPERBLOCK_SIZE,
|
||||
get_super_block_size(cfg.g_w, cfg.g_h, cfg.g_threads)
|
||||
);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_CFL_INTRA, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_SMOOTH_INTRA, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_ANGLE_DELTA, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_FILTER_INTRA, 0);
|
||||
call_ctl!(ctx, AV1E_SET_INTRA_DEFAULT_TX_ONLY, 1);
|
||||
call_ctl!(ctx, AV1E_SET_DISABLE_TRELLIS_QUANT, 1);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_DIST_WTD_COMP, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_DIFF_WTD_COMP, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_DUAL_FILTER, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_INTERINTRA_COMP, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_INTERINTRA_WEDGE, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_INTRA_EDGE_FILTER, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_INTRABC, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_MASKED_COMP, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_PAETH_INTRA, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_QM, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_RECT_PARTITIONS, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_RESTORATION, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_SMOOTH_INTERINTRA, 0);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_TX64, 0);
|
||||
call_ctl!(ctx, AV1E_SET_MAX_REFERENCE_FRAMES, 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderApi for AomEncoder {
|
||||
fn new(cfg: crate::codec::EncoderCfg, i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match cfg {
|
||||
crate::codec::EncoderCfg::AOM(config) => {
|
||||
let i = call_aom_ptr!(aom_codec_av1_cx());
|
||||
let c = webrtc::enc_cfg(i, config, i444)?;
|
||||
|
||||
let mut ctx = Default::default();
|
||||
// Flag options: AOM_CODEC_USE_PSNR and AOM_CODEC_USE_HIGHBITDEPTH
|
||||
let flags: aom_codec_flags_t = 0;
|
||||
call_aom!(aom_codec_enc_init_ver(
|
||||
&mut ctx,
|
||||
i,
|
||||
&c,
|
||||
flags,
|
||||
AOM_ENCODER_ABI_VERSION as _
|
||||
));
|
||||
webrtc::set_controls(&mut ctx, &c)?;
|
||||
Ok(Self {
|
||||
ctx,
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
i444,
|
||||
yuvfmt: Self::get_yuvfmt(config.width, config.height, i444),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType<VideoFrame> {
|
||||
let mut frames = Vec::new();
|
||||
for ref frame in self
|
||||
.encode(ms, input.yuv()?, STRIDE_ALIGN)
|
||||
.with_context(|| "Failed to encode")?
|
||||
{
|
||||
frames.push(Self::create_frame(frame));
|
||||
}
|
||||
if frames.len() > 0 {
|
||||
Ok(Self::create_video_frame(frames))
|
||||
} else {
|
||||
Err(anyhow!("no valid frame"))
|
||||
}
|
||||
}
|
||||
|
||||
fn yuvfmt(&self) -> crate::EncodeYuvFormat {
|
||||
self.yuvfmt.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn input_texture(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let mut c = unsafe { *self.ctx.config.enc.to_owned() };
|
||||
let (q_min, q_max) = Self::calc_q_values(ratio);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio);
|
||||
call_aom!(aom_codec_enc_config_set(&mut self.ctx, &c));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bitrate(&self) -> u32 {
|
||||
let c = unsafe { *self.ctx.config.enc.to_owned() };
|
||||
c.rc_target_bitrate
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn latency_free(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_hardware(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn disable(&self) {}
|
||||
}
|
||||
|
||||
impl AomEncoder {
|
||||
pub fn encode<'a>(&'a mut self, ms: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames<'a>> {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
}
|
||||
let fmt = if self.i444 {
|
||||
aom_img_fmt::AOM_IMG_FMT_I444
|
||||
} else {
|
||||
aom_img_fmt::AOM_IMG_FMT_I420
|
||||
};
|
||||
|
||||
let mut image = Default::default();
|
||||
call_aom_ptr!(aom_img_wrap(
|
||||
&mut image,
|
||||
fmt,
|
||||
self.width as _,
|
||||
self.height as _,
|
||||
stride_align as _,
|
||||
data.as_ptr() as _,
|
||||
));
|
||||
let pts = webrtc::kTimeBaseDen / 1000 * ms;
|
||||
let duration = webrtc::kTimeBaseDen / 1000;
|
||||
call_aom!(aom_codec_encode(
|
||||
&mut self.ctx,
|
||||
&image,
|
||||
pts as _,
|
||||
duration as _, // Duration
|
||||
0, // Flags
|
||||
));
|
||||
|
||||
Ok(EncodeFrames {
|
||||
ctx: &mut self.ctx,
|
||||
iter: ptr::null(),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn create_video_frame(frames: Vec<EncodedVideoFrame>) -> VideoFrame {
|
||||
let mut vf = VideoFrame::new();
|
||||
let av1s = EncodedVideoFrames {
|
||||
frames: frames.into(),
|
||||
..Default::default()
|
||||
};
|
||||
vf.set_av1s(av1s);
|
||||
vf
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_frame(frame: &EncodeFrame) -> EncodedVideoFrame {
|
||||
EncodedVideoFrame {
|
||||
data: Bytes::from(frame.data.to_vec()),
|
||||
key: frame.key,
|
||||
pts: frame.pts,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn bitrate(width: u32, height: u32, ratio: f32) -> u32 {
|
||||
let bitrate = base_bitrate(width, height) as f32;
|
||||
(bitrate * ratio) as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn calc_q_values(ratio: f32) -> (u32, u32) {
|
||||
let b = (ratio * 100.0) as u32;
|
||||
let b = std::cmp::min(b, 200);
|
||||
let q_min1 = 24;
|
||||
let q_min2 = 5;
|
||||
let q_max1 = 45;
|
||||
let q_max2 = 25;
|
||||
|
||||
let t = b as f32 / 200.0;
|
||||
|
||||
let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
|
||||
let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
|
||||
|
||||
q_min = q_min.clamp(q_min2, q_min1);
|
||||
q_max = q_max.clamp(q_max2, q_max1);
|
||||
|
||||
(q_min, q_max)
|
||||
}
|
||||
|
||||
fn get_yuvfmt(width: u32, height: u32, i444: bool) -> EncodeYuvFormat {
|
||||
let mut img = Default::default();
|
||||
let fmt = if i444 {
|
||||
aom_img_fmt::AOM_IMG_FMT_I444
|
||||
} else {
|
||||
aom_img_fmt::AOM_IMG_FMT_I420
|
||||
};
|
||||
unsafe {
|
||||
aom_img_wrap(
|
||||
&mut img,
|
||||
fmt,
|
||||
width as _,
|
||||
height as _,
|
||||
crate::STRIDE_ALIGN as _,
|
||||
0x1 as _,
|
||||
);
|
||||
}
|
||||
let pixfmt = if i444 { Pixfmt::I444 } else { Pixfmt::I420 };
|
||||
EncodeYuvFormat {
|
||||
pixfmt,
|
||||
w: img.w as _,
|
||||
h: img.h as _,
|
||||
stride: img.stride.map(|s| s as usize).to_vec(),
|
||||
u: img.planes[1] as usize - img.planes[0] as usize,
|
||||
v: img.planes[2] as usize - img.planes[0] as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AomEncoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let result = aom_codec_destroy(&mut self.ctx);
|
||||
if result != aom_codec_err_t::AOM_CODEC_OK {
|
||||
panic!("failed to destroy aom codec");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EncodeFrames<'a> {
|
||||
ctx: &'a mut aom_codec_ctx_t,
|
||||
iter: aom_codec_iter_t,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for EncodeFrames<'a> {
|
||||
type Item = EncodeFrame<'a>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
unsafe {
|
||||
let pkt = aom_codec_get_cx_data(self.ctx, &mut self.iter);
|
||||
if pkt.is_null() {
|
||||
return None;
|
||||
} else if (*pkt).kind == aom_codec_cx_pkt_kind::AOM_CODEC_CX_FRAME_PKT {
|
||||
let f = &(*pkt).data.frame;
|
||||
return Some(Self::Item {
|
||||
data: slice::from_raw_parts(f.buf as _, f.sz as _),
|
||||
key: (f.flags & AOM_FRAME_IS_KEY) != 0,
|
||||
pts: f.pts,
|
||||
});
|
||||
} else {
|
||||
// Ignore the packet.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AomDecoder {
|
||||
ctx: aom_codec_ctx_t,
|
||||
}
|
||||
|
||||
impl AomDecoder {
|
||||
pub fn new() -> Result<Self> {
|
||||
let i = call_aom_ptr!(aom_codec_av1_dx());
|
||||
let mut ctx = Default::default();
|
||||
let cfg = aom_codec_dec_cfg_t {
|
||||
threads: codec_thread_num(64) as _,
|
||||
w: 0,
|
||||
h: 0,
|
||||
allow_lowbitdepth: 1,
|
||||
};
|
||||
call_aom!(aom_codec_dec_init_ver(
|
||||
&mut ctx,
|
||||
i,
|
||||
&cfg,
|
||||
0,
|
||||
AOM_DECODER_ABI_VERSION as _,
|
||||
));
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result<DecodeFrames<'a>> {
|
||||
call_aom!(aom_codec_decode(
|
||||
&mut self.ctx,
|
||||
data.as_ptr(),
|
||||
data.len() as _,
|
||||
ptr::null_mut(),
|
||||
));
|
||||
|
||||
Ok(DecodeFrames {
|
||||
ctx: &mut self.ctx,
|
||||
iter: ptr::null(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Notify the decoder to return any pending frame
|
||||
pub fn flush<'a>(&'a mut self) -> Result<DecodeFrames<'a>> {
|
||||
call_aom!(aom_codec_decode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
));
|
||||
Ok(DecodeFrames {
|
||||
ctx: &mut self.ctx,
|
||||
iter: ptr::null(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AomDecoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let result = aom_codec_destroy(&mut self.ctx);
|
||||
if result != aom_codec_err_t::AOM_CODEC_OK {
|
||||
panic!("failed to destroy aom codec");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DecodeFrames<'a> {
|
||||
ctx: &'a mut aom_codec_ctx_t,
|
||||
iter: aom_codec_iter_t,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for DecodeFrames<'a> {
|
||||
type Item = Image;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let img = unsafe { aom_codec_get_frame(self.ctx, &mut self.iter) };
|
||||
if img.is_null() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(Image(img));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Image(*mut aom_image_t);
|
||||
impl Image {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.0.is_null()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn format(&self) -> aom_img_fmt_t {
|
||||
self.inner().fmt
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &aom_image_t {
|
||||
unsafe { &*self.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl GoogleImage for Image {
|
||||
#[inline]
|
||||
fn width(&self) -> usize {
|
||||
self.inner().d_w as _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn height(&self) -> usize {
|
||||
self.inner().d_h as _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stride(&self) -> Vec<i32> {
|
||||
self.inner().stride.iter().map(|x| *x as i32).collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn planes(&self) -> Vec<*mut u8> {
|
||||
self.inner().planes.iter().map(|p| *p as *mut u8).collect()
|
||||
}
|
||||
|
||||
fn chroma(&self) -> Chroma {
|
||||
match self.inner().fmt {
|
||||
aom_img_fmt::AOM_IMG_FMT_I444 => Chroma::I444,
|
||||
_ => Chroma::I420,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Image {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe { aom_img_free(self.0) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for aom_codec_ctx_t {}
|
||||
@@ -0,0 +1,286 @@
|
||||
use std::{
|
||||
io,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use nokhwa::{
|
||||
pixel_format::RgbAFormat,
|
||||
query,
|
||||
utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType},
|
||||
Camera,
|
||||
};
|
||||
|
||||
use hbb_common::message_proto::{DisplayInfo, Resolution};
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
use crate::AdapterDevice;
|
||||
|
||||
use crate::common::{bail, ResultType};
|
||||
use crate::{Frame, TraitCapturer};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use crate::{PixelBuffer, Pixfmt};
|
||||
|
||||
pub const PRIMARY_CAMERA_IDX: usize = 0;
|
||||
lazy_static::lazy_static! {
|
||||
static ref SYNC_CAMERA_DISPLAYS: Arc<Mutex<Vec<DisplayInfo>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
const CAMERA_NOT_SUPPORTED: &str = "This platform doesn't support camera yet";
|
||||
|
||||
pub struct Cameras;
|
||||
|
||||
// pre-condition
|
||||
pub fn primary_camera_exists() -> bool {
|
||||
Cameras::exists(PRIMARY_CAMERA_IDX)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
impl Cameras {
|
||||
pub fn all_info() -> ResultType<Vec<DisplayInfo>> {
|
||||
match query(ApiBackend::Auto) {
|
||||
Ok(cameras) => {
|
||||
let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap();
|
||||
camera_displays.clear();
|
||||
// FIXME: nokhwa returns duplicate info for one physical camera on linux for now.
|
||||
// issue: https://github.com/l1npengtul/nokhwa/issues/171
|
||||
// Use only one camera as a temporary hack.
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
let Some(info) = cameras.first() else {
|
||||
bail!("No camera found")
|
||||
};
|
||||
// Use index (0) camera as main camera, fallback to the first camera if index (0) is not available.
|
||||
// But maybe we also need to check index (1) or the lowest index camera.
|
||||
//
|
||||
// https://askubuntu.com/questions/234362/how-to-fix-this-problem-where-sometimes-dev-video0-becomes-automatically-dev
|
||||
// https://github.com/rustdesk/rustdesk/pull/12010#issue-3125329069
|
||||
let mut camera_index = info.index().clone();
|
||||
if !matches!(camera_index, CameraIndex::Index(0)) {
|
||||
if cameras.iter().any(|cam| matches!(cam.index(), CameraIndex::Index(0))) {
|
||||
camera_index = CameraIndex::Index(0);
|
||||
}
|
||||
}
|
||||
let camera = Self::create_camera(&camera_index)?;
|
||||
let resolution = camera.resolution();
|
||||
let (width, height) = (resolution.width() as i32, resolution.height() as i32);
|
||||
camera_displays.push(DisplayInfo {
|
||||
x: 0,
|
||||
y: 0,
|
||||
name: info.human_name().clone(),
|
||||
width,
|
||||
height,
|
||||
online: true,
|
||||
cursor_embedded: false,
|
||||
scale:1.0,
|
||||
original_resolution: Some(Resolution {
|
||||
width,
|
||||
height,
|
||||
..Default::default()
|
||||
}).into(),
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
let mut x = 0;
|
||||
for info in &cameras {
|
||||
let camera = Self::create_camera(info.index())?;
|
||||
let resolution = camera.resolution();
|
||||
let (width, height) = (resolution.width() as i32, resolution.height() as i32);
|
||||
camera_displays.push(DisplayInfo {
|
||||
x,
|
||||
y: 0,
|
||||
name: info.human_name().clone(),
|
||||
width,
|
||||
height,
|
||||
online: true,
|
||||
cursor_embedded: false,
|
||||
scale:1.0,
|
||||
original_resolution: Some(Resolution {
|
||||
width,
|
||||
height,
|
||||
..Default::default()
|
||||
}).into(),
|
||||
..Default::default()
|
||||
});
|
||||
x += width;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(camera_displays.clone())
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("Query cameras error: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exists(index: usize) -> bool {
|
||||
match query(ApiBackend::Auto) {
|
||||
Ok(cameras) => index < cameras.len(),
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_camera(index: &CameraIndex) -> ResultType<Camera> {
|
||||
let format_type = if cfg!(target_os = "linux") {
|
||||
RequestedFormatType::None
|
||||
} else {
|
||||
RequestedFormatType::AbsoluteHighestResolution
|
||||
};
|
||||
let result = Camera::new(
|
||||
index.clone(),
|
||||
RequestedFormat::new::<RgbAFormat>(format_type),
|
||||
);
|
||||
match result {
|
||||
Ok(camera) => Ok(camera),
|
||||
Err(e) => bail!("create camera{} error: {}", index, e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_camera_resolution(index: usize) -> ResultType<Resolution> {
|
||||
let index = CameraIndex::Index(index as u32);
|
||||
let camera = Self::create_camera(&index)?;
|
||||
let resolution = camera.resolution();
|
||||
Ok(Resolution {
|
||||
width: resolution.width() as i32,
|
||||
height: resolution.height() as i32,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_sync_cameras() -> Vec<DisplayInfo> {
|
||||
SYNC_CAMERA_DISPLAYS.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_capturer(current: usize) -> ResultType<Box<dyn TraitCapturer>> {
|
||||
Ok(Box::new(CameraCapturer::new(current)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
impl Cameras {
|
||||
pub fn all_info() -> ResultType<Vec<DisplayInfo>> {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
pub fn exists(_index: usize) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_camera_resolution(_index: usize) -> ResultType<Resolution> {
|
||||
bail!(CAMERA_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
pub fn get_sync_cameras() -> Vec<DisplayInfo> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
pub fn get_capturer(_current: usize) -> ResultType<Box<dyn TraitCapturer>> {
|
||||
bail!(CAMERA_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
pub struct CameraCapturer {
|
||||
camera: Camera,
|
||||
data: Vec<u8>,
|
||||
last_data: Vec<u8>, // for faster compare and copy
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
pub struct CameraCapturer;
|
||||
|
||||
impl CameraCapturer {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
fn new(current: usize) -> ResultType<Self> {
|
||||
let index = CameraIndex::Index(current as u32);
|
||||
let camera = Cameras::create_camera(&index)?;
|
||||
Ok(CameraCapturer {
|
||||
camera,
|
||||
data: Vec::new(),
|
||||
last_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
fn new(_current: usize) -> ResultType<Self> {
|
||||
bail!(CAMERA_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
impl TraitCapturer for CameraCapturer {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result<Frame<'a>> {
|
||||
// TODO: move this check outside `frame`.
|
||||
if !self.camera.is_stream_open() {
|
||||
if let Err(e) = self.camera.open_stream() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Camera open stream error: {}", e),
|
||||
));
|
||||
}
|
||||
}
|
||||
match self.camera.frame() {
|
||||
Ok(buffer) => {
|
||||
match buffer.decode_image::<RgbAFormat>() {
|
||||
Ok(decoded) => {
|
||||
self.data = decoded.as_raw().to_vec();
|
||||
crate::would_block_if_equal(&mut self.last_data, &self.data)?;
|
||||
// FIXME: macos's PixelBuffer cannot be directly created from bytes slice.
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "windows"))] {
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
&self.data,
|
||||
Pixfmt::RGBA,
|
||||
decoded.width() as usize,
|
||||
decoded.height() as usize,
|
||||
)))
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Camera is not supported on this platform yet"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Camera frame decode error: {}", e),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Camera frame error: {}", e),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result<Frame<'a>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
CAMERA_NOT_SUPPORTED.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_gdi(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn set_gdi(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn device(&self) -> AdapterDevice {
|
||||
AdapterDevice::default()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn set_output_texture(&mut self, _texture: bool) {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/yuv_ffi.rs"));
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use crate::PixelBuffer;
|
||||
use crate::{generate_call_macro, EncodeYuvFormat, TraitPixelBuffer};
|
||||
use hbb_common::{bail, log, ResultType};
|
||||
|
||||
generate_call_macro!(call_yuv, false);
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn convert_to_yuv(
|
||||
captured: &PixelBuffer,
|
||||
dst_fmt: EncodeYuvFormat,
|
||||
dst: &mut Vec<u8>,
|
||||
mid_data: &mut Vec<u8>,
|
||||
) -> ResultType<()> {
|
||||
let src = captured.data();
|
||||
let src_stride = captured.stride();
|
||||
let src_pixfmt = captured.pixfmt();
|
||||
let src_width = captured.width();
|
||||
let src_height = captured.height();
|
||||
if src_width > dst_fmt.w || src_height > dst_fmt.h {
|
||||
bail!(
|
||||
"src rect > dst rect: ({src_width}, {src_height}) > ({},{})",
|
||||
dst_fmt.w,
|
||||
dst_fmt.h
|
||||
);
|
||||
}
|
||||
if src_pixfmt == crate::Pixfmt::BGRA
|
||||
|| src_pixfmt == crate::Pixfmt::RGBA
|
||||
|| src_pixfmt == crate::Pixfmt::RGB565LE
|
||||
{
|
||||
// stride is calculated, not real, so we need to check it
|
||||
if src_stride[0] < src_width * src_pixfmt.bytes_per_pixel() {
|
||||
bail!(
|
||||
"src_stride too small: {} < {}",
|
||||
src_stride[0],
|
||||
src_width * src_pixfmt.bytes_per_pixel()
|
||||
);
|
||||
}
|
||||
if src.len() < src_stride[0] * src_height {
|
||||
bail!(
|
||||
"wrong src len, {} < {} * {}",
|
||||
src.len(),
|
||||
src_stride[0],
|
||||
src_height
|
||||
);
|
||||
}
|
||||
}
|
||||
let align = |x: usize| (x + 63) / 64 * 64;
|
||||
let unsupported = format!(
|
||||
"unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}",
|
||||
dst_fmt.pixfmt
|
||||
);
|
||||
|
||||
match (src_pixfmt, dst_fmt.pixfmt) {
|
||||
(crate::Pixfmt::BGRA, crate::Pixfmt::I420)
|
||||
| (crate::Pixfmt::RGBA, crate::Pixfmt::I420)
|
||||
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I420) => {
|
||||
let dst_stride_y = dst_fmt.stride[0];
|
||||
let dst_stride_uv = dst_fmt.stride[1];
|
||||
dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
||||
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
||||
let f = match src_pixfmt {
|
||||
crate::Pixfmt::BGRA => ARGBToI420,
|
||||
crate::Pixfmt::RGBA => ABGRToI420,
|
||||
crate::Pixfmt::RGB565LE => RGB565ToI420,
|
||||
_ => bail!(unsupported),
|
||||
};
|
||||
call_yuv!(f(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_uv as _,
|
||||
dst_v,
|
||||
dst_stride_uv as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
}
|
||||
(crate::Pixfmt::BGRA, crate::Pixfmt::NV12)
|
||||
| (crate::Pixfmt::RGBA, crate::Pixfmt::NV12)
|
||||
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::NV12) => {
|
||||
let dst_stride_y = dst_fmt.stride[0];
|
||||
let dst_stride_uv = dst_fmt.stride[1];
|
||||
dst.resize(
|
||||
align(dst_fmt.h) * (align(dst_stride_y) + align(dst_stride_uv / 2)),
|
||||
0,
|
||||
);
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_uv = dst[dst_fmt.u..].as_mut_ptr();
|
||||
let (input, input_stride) = match src_pixfmt {
|
||||
crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
|
||||
crate::Pixfmt::RGBA => (src.as_ptr(), src_stride[0]),
|
||||
crate::Pixfmt::RGB565LE => {
|
||||
let mid_stride = src_width * 4;
|
||||
mid_data.resize(mid_stride * src_height, 0);
|
||||
call_yuv!(RGB565ToARGB(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
mid_data.as_mut_ptr(),
|
||||
mid_stride as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
(mid_data.as_ptr(), mid_stride)
|
||||
}
|
||||
_ => bail!(unsupported),
|
||||
};
|
||||
let f = match src_pixfmt {
|
||||
crate::Pixfmt::BGRA => ARGBToNV12,
|
||||
crate::Pixfmt::RGBA => ABGRToNV12,
|
||||
crate::Pixfmt::RGB565LE => ARGBToNV12,
|
||||
_ => bail!(unsupported),
|
||||
};
|
||||
call_yuv!(f(
|
||||
input,
|
||||
input_stride as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_uv,
|
||||
dst_stride_uv as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
}
|
||||
(crate::Pixfmt::BGRA, crate::Pixfmt::I444)
|
||||
| (crate::Pixfmt::RGBA, crate::Pixfmt::I444)
|
||||
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I444) => {
|
||||
let dst_stride_y = dst_fmt.stride[0];
|
||||
let dst_stride_u = dst_fmt.stride[1];
|
||||
let dst_stride_v = dst_fmt.stride[2];
|
||||
dst.resize(
|
||||
align(dst_fmt.h)
|
||||
* (align(dst_stride_y) + align(dst_stride_u) + align(dst_stride_v)),
|
||||
0,
|
||||
);
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
||||
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
||||
let (input, input_stride) = match src_pixfmt {
|
||||
crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
|
||||
crate::Pixfmt::RGBA => {
|
||||
mid_data.resize(src.len(), 0);
|
||||
call_yuv!(ABGRToARGB(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
mid_data.as_mut_ptr(),
|
||||
src_stride[0] as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
(mid_data.as_ptr(), src_stride[0])
|
||||
}
|
||||
crate::Pixfmt::RGB565LE => {
|
||||
let mid_stride = src_width * 4;
|
||||
mid_data.resize(mid_stride * src_height, 0);
|
||||
call_yuv!(RGB565ToARGB(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
mid_data.as_mut_ptr(),
|
||||
mid_stride as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
(mid_data.as_ptr(), mid_stride)
|
||||
}
|
||||
_ => bail!(unsupported),
|
||||
};
|
||||
|
||||
call_yuv!(ARGBToI444(
|
||||
input,
|
||||
input_stride as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_u as _,
|
||||
dst_v,
|
||||
dst_stride_v as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
bail!(unsupported);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn convert(captured: &PixelBuffer, pixfmt: crate::Pixfmt, dst: &mut Vec<u8>) -> ResultType<()> {
|
||||
if captured.pixfmt() == pixfmt {
|
||||
dst.extend_from_slice(captured.data());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let src = captured.data();
|
||||
let src_stride = captured.stride();
|
||||
let src_pixfmt = captured.pixfmt();
|
||||
let src_width = captured.width();
|
||||
let src_height = captured.height();
|
||||
|
||||
let unsupported = format!(
|
||||
"unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}",
|
||||
pixfmt
|
||||
);
|
||||
|
||||
match (src_pixfmt, pixfmt) {
|
||||
(crate::Pixfmt::BGRA, crate::Pixfmt::RGBA) | (crate::Pixfmt::RGBA, crate::Pixfmt::BGRA) => {
|
||||
dst.resize(src.len(), 0);
|
||||
call_yuv!(ABGRToARGB(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
dst.as_mut_ptr(),
|
||||
src_stride[0] as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
bail!(unsupported);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
#[cfg(feature = "vram")]
|
||||
use crate::AdapterDevice;
|
||||
use crate::{common::TraitCapturer, dxgi, Frame, Pixfmt};
|
||||
use std::{
|
||||
io::{
|
||||
self,
|
||||
ErrorKind::{NotFound, TimedOut, WouldBlock},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct Capturer {
|
||||
inner: dxgi::Capturer,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let width = display.width();
|
||||
let height = display.height();
|
||||
let inner = dxgi::Capturer::new(display.0)?;
|
||||
Ok(Capturer {
|
||||
inner,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_gdi(&mut self) {
|
||||
self.inner.cancel_gdi()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
match self.inner.frame(timeout.as_millis() as _) {
|
||||
Ok(frame) => Ok(frame),
|
||||
Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_gdi(&self) -> bool {
|
||||
self.inner.is_gdi()
|
||||
}
|
||||
|
||||
fn set_gdi(&mut self) -> bool {
|
||||
self.inner.set_gdi()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn device(&self) -> AdapterDevice {
|
||||
self.inner.device()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn set_output_texture(&mut self, texture: bool) {
|
||||
self.inner.set_output_texture(texture);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PixelBuffer<'a> {
|
||||
data: &'a [u8],
|
||||
pixfmt: Pixfmt,
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a> PixelBuffer<'a> {
|
||||
pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self {
|
||||
let stride0 = data.len() / height;
|
||||
let mut stride = Vec::new();
|
||||
stride.push(stride0);
|
||||
PixelBuffer {
|
||||
data,
|
||||
pixfmt,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn with_BGRA(data: &'a [u8], width: usize, height: usize) -> Self {
|
||||
Self::new(data, Pixfmt::BGRA, width, height)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
self.stride.clone()
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt {
|
||||
self.pixfmt
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display(dxgi::Display);
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
// not implemented yet
|
||||
Err(NotFound.into())
|
||||
}
|
||||
|
||||
pub fn all() -> io::Result<Vec<Display>> {
|
||||
let displays_gdi = dxgi::Displays::get_from_gdi()
|
||||
.drain(..)
|
||||
.map(Display)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let displays_dxgi = Self::all_().unwrap_or(Default::default());
|
||||
|
||||
// Return gdi displays if dxgi is not supported
|
||||
if displays_dxgi.is_empty() {
|
||||
println!("Display got from gdi");
|
||||
return Ok(displays_gdi);
|
||||
}
|
||||
|
||||
// Return dxgi displays if length is not equal
|
||||
if displays_dxgi.len() != displays_gdi.len() {
|
||||
return Ok(displays_dxgi);
|
||||
}
|
||||
|
||||
// Check if names are equal
|
||||
let names_gdi = displays_gdi.iter().map(|d| d.name()).collect::<Vec<_>>();
|
||||
let names_dxgi = displays_dxgi.iter().map(|d| d.name()).collect::<Vec<_>>();
|
||||
for name in names_gdi.iter() {
|
||||
if !names_dxgi.contains(name) {
|
||||
return Ok(displays_dxgi);
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder displays from dxgi
|
||||
let mut displays_dxgi = displays_dxgi;
|
||||
let mut displays_dxgi_ordered = Vec::new();
|
||||
for name in names_gdi.iter() {
|
||||
let pos = match displays_dxgi.iter().position(|d| d.name() == *name) {
|
||||
Some(pos) => pos,
|
||||
None => {
|
||||
// unreachable!
|
||||
0
|
||||
}
|
||||
};
|
||||
displays_dxgi_ordered.push(displays_dxgi.remove(pos));
|
||||
}
|
||||
|
||||
Ok(displays_dxgi_ordered)
|
||||
}
|
||||
|
||||
fn all_() -> io::Result<Vec<Display>> {
|
||||
Ok(dxgi::Displays::new()?.map(Display).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.width() as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.height() as usize
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::prelude::*;
|
||||
OsString::from_wide(self.0.name())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
self.0.is_online()
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
self.0.origin()
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-devmodea
|
||||
self.origin() == (0, 0)
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
pub fn adapter_luid(&self) -> Option<i64> {
|
||||
self.0.adapter_luid()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CapturerMag {
|
||||
inner: dxgi::mag::CapturerMag,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CapturerMag {
|
||||
pub fn is_supported() -> bool {
|
||||
dxgi::mag::CapturerMag::is_supported()
|
||||
}
|
||||
|
||||
pub fn new(origin: (i32, i32), width: usize, height: usize) -> io::Result<Self> {
|
||||
Ok(CapturerMag {
|
||||
inner: dxgi::mag::CapturerMag::new(origin, width, height)?,
|
||||
data: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result<bool> {
|
||||
self.inner.exclude(cls, name)
|
||||
}
|
||||
// ((x, y), w, h)
|
||||
pub fn get_rect(&self) -> ((i32, i32), usize, usize) {
|
||||
self.inner.get_rect()
|
||||
}
|
||||
}
|
||||
|
||||
impl TraitCapturer for CapturerMag {
|
||||
fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result<Frame<'a>> {
|
||||
self.inner.frame(&mut self.data)?;
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
|
||||
&self.data,
|
||||
self.inner.get_rect().1,
|
||||
self.inner.get_rect().2,
|
||||
)))
|
||||
}
|
||||
|
||||
fn is_gdi(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_gdi(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn device(&self) -> AdapterDevice {
|
||||
AdapterDevice::default()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn set_output_texture(&mut self, _texture: bool) {}
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
use crate::{
|
||||
codec::{base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg},
|
||||
convert::*,
|
||||
CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN,
|
||||
};
|
||||
use hbb_common::{
|
||||
anyhow::{anyhow, bail, Context},
|
||||
bytes::Bytes,
|
||||
log,
|
||||
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
|
||||
serde_derive::{Deserialize, Serialize},
|
||||
serde_json, ResultType,
|
||||
};
|
||||
use hwcodec::{
|
||||
common::{
|
||||
DataFormat, HwcodecErrno,
|
||||
Quality::{self, *},
|
||||
RateControl::{self, *},
|
||||
},
|
||||
ffmpeg::AVPixelFormat,
|
||||
ffmpeg_ram::{
|
||||
decode::{DecodeContext, DecodeFrame, Decoder},
|
||||
encode::{EncodeContext, EncodeFrame, Encoder},
|
||||
ffmpeg_linesize_offset_length, CodecInfo,
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12;
|
||||
pub const DEFAULT_FPS: i32 = 30;
|
||||
const DEFAULT_GOP: i32 = i32::MAX;
|
||||
const DEFAULT_HW_QUALITY: Quality = Quality_Default;
|
||||
pub const ERR_HEVC_POC: i32 = HwcodecErrno::HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC as i32;
|
||||
|
||||
crate::generate_call_macro!(call_yuv, false);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref CONFIG: std::sync::Arc<std::sync::Mutex<Option<HwCodecConfig>>> = Default::default();
|
||||
static ref CONFIG_SET_BY_IPC: std::sync::Arc<std::sync::Mutex<bool>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwRamEncoderConfig {
|
||||
pub name: String,
|
||||
pub mc_name: Option<String>,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub quality: f32,
|
||||
pub keyframe_interval: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct HwRamEncoder {
|
||||
encoder: Encoder,
|
||||
pub format: DataFormat,
|
||||
pub pixfmt: AVPixelFormat,
|
||||
bitrate: u32, //kbs
|
||||
config: HwRamEncoderConfig,
|
||||
}
|
||||
|
||||
impl EncoderApi for HwRamEncoder {
|
||||
fn new(cfg: EncoderCfg, _i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match cfg {
|
||||
EncoderCfg::HWRAM(config) => {
|
||||
let rc = Self::rate_control(&config);
|
||||
let mut bitrate =
|
||||
Self::bitrate(&config.name, config.width, config.height, config.quality);
|
||||
bitrate = Self::check_bitrate_range(&config, bitrate);
|
||||
let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32;
|
||||
let ctx = EncodeContext {
|
||||
name: config.name.clone(),
|
||||
mc_name: config.mc_name.clone(),
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
pixfmt: DEFAULT_PIXFMT,
|
||||
align: HW_STRIDE_ALIGN as _,
|
||||
kbs: bitrate as i32,
|
||||
fps: DEFAULT_FPS,
|
||||
gop,
|
||||
quality: DEFAULT_HW_QUALITY,
|
||||
rc,
|
||||
q: -1,
|
||||
thread_count: codec_thread_num(16) as _, // ffmpeg's thread_count is used for cpu
|
||||
};
|
||||
let format = match Encoder::format_from_name(config.name.clone()) {
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
return Err(anyhow!(format!(
|
||||
"failed to get format from name:{}",
|
||||
config.name
|
||||
)))
|
||||
}
|
||||
};
|
||||
match Encoder::new(ctx.clone()) {
|
||||
Ok(encoder) => Ok(HwRamEncoder {
|
||||
encoder,
|
||||
format,
|
||||
pixfmt: ctx.pixfmt,
|
||||
bitrate,
|
||||
config,
|
||||
}),
|
||||
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType<VideoFrame> {
|
||||
let mut vf = VideoFrame::new();
|
||||
let mut frames = Vec::new();
|
||||
for frame in self
|
||||
.encode(input.yuv()?, ms)
|
||||
.with_context(|| "Failed to encode")?
|
||||
{
|
||||
frames.push(EncodedVideoFrame {
|
||||
data: Bytes::from(frame.data),
|
||||
pts: frame.pts,
|
||||
key: frame.key == 1,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if frames.len() > 0 {
|
||||
let frames = EncodedVideoFrames {
|
||||
frames: frames.into(),
|
||||
..Default::default()
|
||||
};
|
||||
match self.format {
|
||||
DataFormat::H264 => vf.set_h264s(frames),
|
||||
DataFormat::H265 => vf.set_h265s(frames),
|
||||
_ => bail!("unsupported format: {:?}", self.format),
|
||||
}
|
||||
Ok(vf)
|
||||
} else {
|
||||
Err(anyhow!("no valid frame"))
|
||||
}
|
||||
}
|
||||
|
||||
fn yuvfmt(&self) -> crate::EncodeYuvFormat {
|
||||
let pixfmt = if self.pixfmt == AVPixelFormat::AV_PIX_FMT_NV12 {
|
||||
Pixfmt::NV12
|
||||
} else {
|
||||
Pixfmt::I420
|
||||
};
|
||||
let stride = self
|
||||
.encoder
|
||||
.linesize
|
||||
.clone()
|
||||
.drain(..)
|
||||
.map(|i| i as usize)
|
||||
.collect();
|
||||
crate::EncodeYuvFormat {
|
||||
pixfmt,
|
||||
w: self.encoder.ctx.width as _,
|
||||
h: self.encoder.ctx.height as _,
|
||||
stride,
|
||||
u: self.encoder.offset[0] as _,
|
||||
v: if pixfmt == Pixfmt::NV12 {
|
||||
0
|
||||
} else {
|
||||
self.encoder.offset[1] as _
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn input_texture(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let mut bitrate = Self::bitrate(
|
||||
&self.config.name,
|
||||
self.config.width,
|
||||
self.config.height,
|
||||
ratio,
|
||||
);
|
||||
if bitrate > 0 {
|
||||
bitrate = Self::check_bitrate_range(&self.config, bitrate);
|
||||
self.encoder.set_bitrate(bitrate as _).ok();
|
||||
self.bitrate = bitrate;
|
||||
}
|
||||
self.config.quality = ratio;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
["vaapi"].iter().all(|&x| !self.config.name.contains(x))
|
||||
}
|
||||
|
||||
fn latency_free(&self) -> bool {
|
||||
["mediacodec", "videotoolbox"]
|
||||
.iter()
|
||||
.all(|&x| !self.config.name.contains(x))
|
||||
}
|
||||
|
||||
fn is_hardware(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn disable(&self) {
|
||||
HwCodecConfig::clear(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
impl HwRamEncoder {
|
||||
pub fn try_get(format: CodecFormat) -> Option<CodecInfo> {
|
||||
let mut info = None;
|
||||
let best = CodecInfo::prioritized(HwCodecConfig::get().ram_encode);
|
||||
match format {
|
||||
CodecFormat::H264 => {
|
||||
if let Some(v) = best.h264 {
|
||||
info = Some(v);
|
||||
}
|
||||
}
|
||||
CodecFormat::H265 => {
|
||||
if let Some(v) = best.h265 {
|
||||
info = Some(v);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
pub fn encode(&mut self, yuv: &[u8], ms: i64) -> ResultType<Vec<EncodeFrame>> {
|
||||
match self.encoder.encode(yuv, ms) {
|
||||
Ok(v) => {
|
||||
let mut data = Vec::<EncodeFrame>::new();
|
||||
data.append(v);
|
||||
Ok(data)
|
||||
}
|
||||
Err(_) => Ok(Vec::<EncodeFrame>::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn rate_control(_config: &HwRamEncoderConfig) -> RateControl {
|
||||
#[cfg(target_os = "android")]
|
||||
if _config.name.contains("mediacodec") {
|
||||
return RC_VBR;
|
||||
}
|
||||
RC_CBR
|
||||
}
|
||||
|
||||
pub fn bitrate(name: &str, width: usize, height: usize, ratio: f32) -> u32 {
|
||||
Self::calc_bitrate(width, height, ratio, name.contains("h264"))
|
||||
}
|
||||
|
||||
pub fn calc_bitrate(width: usize, height: usize, ratio: f32, h264: bool) -> u32 {
|
||||
let base = base_bitrate(width as _, height as _) as f32 * ratio;
|
||||
let threshold = 2000.0;
|
||||
let decay_rate = 0.001; // 1000 * 0.001 = 1
|
||||
let factor: f32 = if cfg!(target_os = "android") {
|
||||
// https://stackoverflow.com/questions/26110337/what-are-valid-bit-rates-to-set-for-mediacodec?rq=3
|
||||
if base > threshold {
|
||||
1.0 + 4.0 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
5.0
|
||||
}
|
||||
} else if h264 {
|
||||
if base > threshold {
|
||||
1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
2.0
|
||||
}
|
||||
} else {
|
||||
if base > threshold {
|
||||
1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
1.5
|
||||
}
|
||||
};
|
||||
(base * factor) as u32
|
||||
}
|
||||
|
||||
pub fn check_bitrate_range(_config: &HwRamEncoderConfig, bitrate: u32) -> u32 {
|
||||
#[cfg(target_os = "android")]
|
||||
if _config.name.contains("mediacodec") {
|
||||
let info = crate::android::ffi::get_codec_info();
|
||||
if let Some(info) = info {
|
||||
if let Some(codec) = info
|
||||
.codecs
|
||||
.iter()
|
||||
.find(|c| Some(c.name.clone()) == _config.mc_name && c.is_encoder)
|
||||
{
|
||||
if codec.max_bitrate > codec.min_bitrate {
|
||||
if bitrate > codec.max_bitrate {
|
||||
return codec.max_bitrate;
|
||||
}
|
||||
if bitrate < codec.min_bitrate {
|
||||
return codec.min_bitrate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bitrate
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HwRamDecoder {
|
||||
decoder: Decoder,
|
||||
pub info: CodecInfo,
|
||||
}
|
||||
|
||||
impl HwRamDecoder {
|
||||
pub fn try_get(format: CodecFormat) -> Option<CodecInfo> {
|
||||
let mut info = None;
|
||||
let soft = CodecInfo::soft();
|
||||
match format {
|
||||
CodecFormat::H264 => {
|
||||
if let Some(v) = soft.h264 {
|
||||
info = Some(v);
|
||||
}
|
||||
}
|
||||
CodecFormat::H265 => {
|
||||
if let Some(v) = soft.h265 {
|
||||
info = Some(v);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if enable_hwcodec_option() {
|
||||
let best = CodecInfo::prioritized(HwCodecConfig::get().ram_decode);
|
||||
match format {
|
||||
CodecFormat::H264 => {
|
||||
if let Some(v) = best.h264 {
|
||||
info = Some(v);
|
||||
}
|
||||
}
|
||||
CodecFormat::H265 => {
|
||||
if let Some(v) = best.h265 {
|
||||
info = Some(v);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
pub fn new(format: CodecFormat) -> ResultType<Self> {
|
||||
let info = HwRamDecoder::try_get(format);
|
||||
log::info!("try create {info:?} ram decoder");
|
||||
let Some(info) = info else {
|
||||
bail!("unsupported format: {:?}", format);
|
||||
};
|
||||
let ctx = DecodeContext {
|
||||
name: info.name.clone(),
|
||||
device_type: info.hwdevice.clone(),
|
||||
thread_count: codec_thread_num(16) as _,
|
||||
};
|
||||
match Decoder::new(ctx) {
|
||||
Ok(decoder) => Ok(HwRamDecoder { decoder, info }),
|
||||
Err(_) => {
|
||||
HwCodecConfig::clear(false, false);
|
||||
Err(anyhow!(format!("Failed to create decoder")))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType<Vec<HwRamDecoderImage<'a>>> {
|
||||
match self.decoder.decode(data) {
|
||||
Ok(v) => Ok(v.iter().map(|f| HwRamDecoderImage { frame: f }).collect()),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HwRamDecoderImage<'a> {
|
||||
frame: &'a DecodeFrame,
|
||||
}
|
||||
|
||||
impl HwRamDecoderImage<'_> {
|
||||
// rgb [in/out] fmt and stride must be set in ImageRgb
|
||||
pub fn to_fmt(&self, rgb: &mut ImageRgb, i420: &mut Vec<u8>) -> ResultType<()> {
|
||||
let frame = self.frame;
|
||||
let width = frame.width;
|
||||
let height = frame.height;
|
||||
rgb.w = width as _;
|
||||
rgb.h = height as _;
|
||||
let dst_align = rgb.align();
|
||||
let bytes_per_row = (rgb.w * 4 + dst_align - 1) & !(dst_align - 1);
|
||||
rgb.raw.resize(rgb.h * bytes_per_row, 0);
|
||||
match frame.pixfmt {
|
||||
AVPixelFormat::AV_PIX_FMT_NV12 => {
|
||||
// I420ToARGB is much faster than NV12ToARGB in tests on Windows
|
||||
if cfg!(windows) {
|
||||
let Ok((linesize_i420, offset_i420, len_i420)) = ffmpeg_linesize_offset_length(
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
width as _,
|
||||
height as _,
|
||||
HW_STRIDE_ALIGN,
|
||||
) else {
|
||||
bail!("failed to get i420 linesize, offset, length");
|
||||
};
|
||||
i420.resize(len_i420 as _, 0);
|
||||
let i420_offset_y = unsafe { i420.as_ptr().add(0) as _ };
|
||||
let i420_offset_u = unsafe { i420.as_ptr().add(offset_i420[0] as _) as _ };
|
||||
let i420_offset_v = unsafe { i420.as_ptr().add(offset_i420[1] as _) as _ };
|
||||
call_yuv!(NV12ToI420(
|
||||
frame.data[0].as_ptr(),
|
||||
frame.linesize[0],
|
||||
frame.data[1].as_ptr(),
|
||||
frame.linesize[1],
|
||||
i420_offset_y,
|
||||
linesize_i420[0],
|
||||
i420_offset_u,
|
||||
linesize_i420[1],
|
||||
i420_offset_v,
|
||||
linesize_i420[2],
|
||||
width,
|
||||
height,
|
||||
));
|
||||
let f = match rgb.fmt() {
|
||||
ImageFormat::ARGB => I420ToARGB,
|
||||
ImageFormat::ABGR => I420ToABGR,
|
||||
_ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()),
|
||||
};
|
||||
call_yuv!(f(
|
||||
i420_offset_y,
|
||||
linesize_i420[0],
|
||||
i420_offset_u,
|
||||
linesize_i420[1],
|
||||
i420_offset_v,
|
||||
linesize_i420[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
width,
|
||||
height,
|
||||
));
|
||||
} else {
|
||||
let f = match rgb.fmt() {
|
||||
ImageFormat::ARGB => NV12ToARGB,
|
||||
ImageFormat::ABGR => NV12ToABGR,
|
||||
_ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()),
|
||||
};
|
||||
call_yuv!(f(
|
||||
frame.data[0].as_ptr(),
|
||||
frame.linesize[0],
|
||||
frame.data[1].as_ptr(),
|
||||
frame.linesize[1],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
width,
|
||||
height,
|
||||
));
|
||||
}
|
||||
}
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P => {
|
||||
let f = match rgb.fmt() {
|
||||
ImageFormat::ARGB => I420ToARGB,
|
||||
ImageFormat::ABGR => I420ToABGR,
|
||||
_ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()),
|
||||
};
|
||||
call_yuv!(f(
|
||||
frame.data[0].as_ptr(),
|
||||
frame.linesize[0],
|
||||
frame.data[1].as_ptr(),
|
||||
frame.linesize[1],
|
||||
frame.data[2].as_ptr(),
|
||||
frame.linesize[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
width,
|
||||
height,
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn get_mime_type(codec: DataFormat) -> &'static str {
|
||||
match codec {
|
||||
DataFormat::VP8 => "video/x-vnd.on2.vp8",
|
||||
DataFormat::VP9 => "video/x-vnd.on2.vp9",
|
||||
DataFormat::AV1 => "video/av01",
|
||||
DataFormat::H264 => "video/avc",
|
||||
DataFormat::H265 => "video/hevc",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct HwCodecConfig {
|
||||
#[serde(default)]
|
||||
pub signature: u64,
|
||||
#[serde(default)]
|
||||
pub ram_encode: Vec<CodecInfo>,
|
||||
#[serde(default)]
|
||||
pub ram_decode: Vec<CodecInfo>,
|
||||
#[cfg(feature = "vram")]
|
||||
#[serde(default)]
|
||||
pub vram_encode: Vec<hwcodec::vram::FeatureContext>,
|
||||
#[cfg(feature = "vram")]
|
||||
#[serde(default)]
|
||||
pub vram_decode: Vec<hwcodec::vram::DecodeContext>,
|
||||
}
|
||||
|
||||
// HwCodecConfig2 is used to store the config in json format,
|
||||
// confy can't serde HwCodecConfig successfully if the non-first struct Vec is empty due to old toml version.
|
||||
// struct T { a: Vec<A>, b: Vec<String>} will fail if b is empty, but struct T { a: Vec<String>, b: Vec<String>} is ok.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
struct HwCodecConfig2 {
|
||||
#[serde(default)]
|
||||
pub config: String,
|
||||
}
|
||||
|
||||
// ipc server process start check process once, other process get from ipc server once
|
||||
// install: --server start check process, check process send to --server, ui get from --server
|
||||
// portable: ui start check process, check process send to ui
|
||||
// sciter and unilink: get from ipc server
|
||||
impl HwCodecConfig {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn set(config: String) {
|
||||
let config = serde_json::from_str(&config).unwrap_or_default();
|
||||
log::info!("set hwcodec config");
|
||||
log::debug!("{config:?}");
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
hbb_common::config::common_store(
|
||||
&HwCodecConfig2 {
|
||||
config: serde_json::to_string_pretty(&config).unwrap_or_default(),
|
||||
},
|
||||
"_hwcodec",
|
||||
);
|
||||
*CONFIG.lock().unwrap() = Some(config);
|
||||
*CONFIG_SET_BY_IPC.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
pub fn get() -> HwCodecConfig {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let info = crate::android::ffi::get_codec_info();
|
||||
log::info!("all codec info: {info:?}");
|
||||
struct T {
|
||||
name_prefix: &'static str,
|
||||
data_format: DataFormat,
|
||||
}
|
||||
let ts = vec![
|
||||
T {
|
||||
name_prefix: "h264",
|
||||
data_format: DataFormat::H264,
|
||||
},
|
||||
T {
|
||||
name_prefix: "hevc",
|
||||
data_format: DataFormat::H265,
|
||||
},
|
||||
];
|
||||
let mut e = vec![];
|
||||
if let Some(info) = info {
|
||||
ts.iter().for_each(|t| {
|
||||
let codecs: Vec<_> = info
|
||||
.codecs
|
||||
.iter()
|
||||
.filter(|c| {
|
||||
c.is_encoder
|
||||
&& c.mime_type.as_str() == get_mime_type(t.data_format)
|
||||
&& c.nv12
|
||||
&& c.hw == Some(true) //only use hardware codec
|
||||
})
|
||||
.collect();
|
||||
let screen_wh = std::cmp::max(info.w, info.h);
|
||||
let mut best = None;
|
||||
if let Some(codec) = codecs
|
||||
.iter()
|
||||
.find(|c| c.max_width >= screen_wh && c.max_height >= screen_wh)
|
||||
{
|
||||
best = Some(codec.name.clone());
|
||||
} else {
|
||||
// find the max resolution
|
||||
let mut max_area = 0;
|
||||
for codec in codecs.iter() {
|
||||
if codec.max_width * codec.max_height > max_area {
|
||||
best = Some(codec.name.clone());
|
||||
max_area = codec.max_width * codec.max_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(best) = best {
|
||||
e.push(CodecInfo {
|
||||
name: format!("{}_mediacodec", t.name_prefix),
|
||||
mc_name: Some(best),
|
||||
format: t.data_format,
|
||||
hwdevice: hwcodec::ffmpeg::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
log::debug!("e: {e:?}");
|
||||
HwCodecConfig {
|
||||
ram_encode: e,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
let config = CONFIG.lock().unwrap().clone();
|
||||
match config {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
log::info!("try load cached hwcodec config");
|
||||
let c = hbb_common::config::common_load::<HwCodecConfig2>("_hwcodec");
|
||||
let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default();
|
||||
let new_signature = hwcodec::common::get_gpu_signature();
|
||||
if c.signature == new_signature {
|
||||
log::debug!("load cached hwcodec config: {c:?}");
|
||||
*CONFIG.lock().unwrap() = Some(c.clone());
|
||||
c
|
||||
} else {
|
||||
log::info!(
|
||||
"gpu signature changed, {} -> {}",
|
||||
c.signature,
|
||||
new_signature
|
||||
);
|
||||
HwCodecConfig::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
CONFIG.lock().unwrap().clone().unwrap_or_default()
|
||||
}
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
HwCodecConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn get_set_value() -> Option<HwCodecConfig> {
|
||||
let set = CONFIG_SET_BY_IPC.lock().unwrap().clone();
|
||||
if set {
|
||||
CONFIG.lock().unwrap().clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn already_set() -> bool {
|
||||
CONFIG_SET_BY_IPC.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn clear(vram: bool, encode: bool) {
|
||||
log::info!("clear hwcodec config, vram: {vram}, encode: {encode}");
|
||||
#[cfg(target_os = "android")]
|
||||
crate::android::ffi::clear_codec_info();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let mut c = CONFIG.lock().unwrap();
|
||||
if let Some(c) = c.as_mut() {
|
||||
if vram {
|
||||
#[cfg(feature = "vram")]
|
||||
if encode {
|
||||
c.vram_encode = vec![];
|
||||
} else {
|
||||
c.vram_decode = vec![];
|
||||
}
|
||||
} else {
|
||||
if encode {
|
||||
c.ram_encode = vec![];
|
||||
} else {
|
||||
c.ram_decode = vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::codec::Encoder::update(crate::codec::EncodingUpdate::Check);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_available_hwcodec() -> String {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
hwcodec::common::setup_parent_death_signal();
|
||||
let ctx = EncodeContext {
|
||||
name: String::from(""),
|
||||
mc_name: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
pixfmt: DEFAULT_PIXFMT,
|
||||
align: HW_STRIDE_ALIGN as _,
|
||||
kbs: 1000,
|
||||
fps: DEFAULT_FPS,
|
||||
gop: DEFAULT_GOP,
|
||||
quality: DEFAULT_HW_QUALITY,
|
||||
rc: RC_CBR,
|
||||
q: -1,
|
||||
thread_count: 4,
|
||||
};
|
||||
#[cfg(feature = "vram")]
|
||||
let vram = crate::vram::check_available_vram();
|
||||
#[cfg(feature = "vram")]
|
||||
let vram_string = vram.2;
|
||||
#[cfg(not(feature = "vram"))]
|
||||
let vram_string = "".to_owned();
|
||||
let c = HwCodecConfig {
|
||||
ram_encode: Encoder::available_encoders(ctx, Some(vram_string)),
|
||||
ram_decode: Decoder::available_decoders(),
|
||||
#[cfg(feature = "vram")]
|
||||
vram_encode: vram.0,
|
||||
#[cfg(feature = "vram")]
|
||||
vram_decode: vram.1,
|
||||
signature: hwcodec::common::get_gpu_signature(),
|
||||
};
|
||||
log::debug!("{c:?}");
|
||||
serde_json::to_string(&c).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn start_check_process() {
|
||||
if !enable_hwcodec_option() || HwCodecConfig::already_set() {
|
||||
return;
|
||||
}
|
||||
use hbb_common::allow_err;
|
||||
use std::sync::Once;
|
||||
let f = || {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(_) = exe.file_name().to_owned() {
|
||||
let arg = "--check-hwcodec-config";
|
||||
if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() {
|
||||
#[cfg(windows)]
|
||||
hwcodec::common::child_exit_when_parent_exit(child.id());
|
||||
// wait up to 30 seconds, it maybe slow on windows startup for poorly performing machines
|
||||
for _ in 0..30 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
allow_err!(child.kill());
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
log::info!("Check hwcodec config, exit with: {status}")
|
||||
}
|
||||
Ok(None) => {
|
||||
log::info!(
|
||||
"Check hwcodec config, status not ready yet, let's really wait"
|
||||
);
|
||||
let res = child.wait();
|
||||
log::info!("Check hwcodec config, wait result: {res:?}");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Check hwcodec config, error attempting to wait: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
std::thread::spawn(f);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use crate::{
|
||||
common::{
|
||||
wayland,
|
||||
x11::{self},
|
||||
TraitCapturer,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
use std::{io, time::Duration};
|
||||
|
||||
pub enum Capturer {
|
||||
X11(x11::Capturer),
|
||||
WAYLAND(wayland::Capturer),
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
Ok(match display {
|
||||
Display::X11(d) => Capturer::X11(x11::Capturer::new(d)?),
|
||||
Display::WAYLAND(d) => Capturer::WAYLAND(wayland::Capturer::new(d)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
match self {
|
||||
Capturer::X11(d) => d.width(),
|
||||
Capturer::WAYLAND(d) => d.width(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
match self {
|
||||
Capturer::X11(d) => d.height(),
|
||||
Capturer::WAYLAND(d) => d.height(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
match self {
|
||||
Capturer::X11(d) => d.frame(timeout),
|
||||
Capturer::WAYLAND(d) => d.frame(timeout),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Display {
|
||||
X11(x11::Display),
|
||||
WAYLAND(wayland::Display),
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
Ok(if super::is_x11() {
|
||||
Display::X11(x11::Display::primary()?)
|
||||
} else {
|
||||
Display::WAYLAND(wayland::Display::primary()?)
|
||||
})
|
||||
}
|
||||
|
||||
// Currently, wayland need to call wayland::clear() before call Display::all()
|
||||
pub fn all() -> io::Result<Vec<Display>> {
|
||||
Ok(if super::is_x11() {
|
||||
x11::Display::all()?
|
||||
.drain(..)
|
||||
.map(|x| Display::X11(x))
|
||||
.collect()
|
||||
} else {
|
||||
wayland::Display::all()?
|
||||
.drain(..)
|
||||
.map(|x| Display::WAYLAND(x))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.width(),
|
||||
Display::WAYLAND(d) => d.width(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.height(),
|
||||
Display::WAYLAND(d) => d.height(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
match self {
|
||||
Display::X11(_d) => 1.0,
|
||||
Display::WAYLAND(d) => d.scale(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_width(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.width(),
|
||||
Display::WAYLAND(d) => d.logical_width(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_height(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.height(),
|
||||
Display::WAYLAND(d) => d.logical_height(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
match self {
|
||||
Display::X11(d) => d.origin(),
|
||||
Display::WAYLAND(d) => d.origin(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
match self {
|
||||
Display::X11(d) => d.is_online(),
|
||||
Display::WAYLAND(d) => d.is_online(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
match self {
|
||||
Display::X11(d) => d.is_primary(),
|
||||
Display::WAYLAND(d) => d.is_primary(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Display::X11(d) => d.name(),
|
||||
Display::WAYLAND(d) => d.name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use hbb_common::{anyhow::Error, bail, log, ResultType};
|
||||
use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat};
|
||||
use std::ops::Deref;
|
||||
use std::{
|
||||
io::Write,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::ImageFormat;
|
||||
use crate::{
|
||||
codec::{EncoderApi, EncoderCfg},
|
||||
CodecFormat, I420ToABGR, I420ToARGB, ImageRgb,
|
||||
};
|
||||
|
||||
/// MediaCodec mime type name
|
||||
const H264_MIME_TYPE: &str = "video/avc";
|
||||
const H265_MIME_TYPE: &str = "video/hevc";
|
||||
// const VP8_MIME_TYPE: &str = "video/x-vnd.on2.vp8";
|
||||
// const VP9_MIME_TYPE: &str = "video/x-vnd.on2.vp9";
|
||||
|
||||
// TODO MediaCodecEncoder
|
||||
|
||||
pub static H264_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false);
|
||||
pub static H265_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub struct MediaCodecDecoder {
|
||||
decoder: MediaCodec,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Deref for MediaCodecDecoder {
|
||||
type Target = MediaCodec;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.decoder
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaCodecDecoder {
|
||||
pub fn new(format: CodecFormat) -> Option<MediaCodecDecoder> {
|
||||
match format {
|
||||
CodecFormat::H264 => create_media_codec(H264_MIME_TYPE, MediaCodecDirection::Decoder),
|
||||
CodecFormat::H265 => create_media_codec(H265_MIME_TYPE, MediaCodecDirection::Decoder),
|
||||
_ => {
|
||||
log::error!("Unsupported codec format: {}", format);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rgb [in/out] fmt and stride must be set in ImageRgb
|
||||
pub fn decode(&mut self, data: &[u8], rgb: &mut ImageRgb) -> ResultType<bool> {
|
||||
// take dst_stride into account please
|
||||
let dst_stride = rgb.stride();
|
||||
match self.dequeue_input_buffer(Duration::from_millis(10))? {
|
||||
Some(mut input_buffer) => {
|
||||
let mut buf = input_buffer.buffer_mut();
|
||||
if data.len() > buf.len() {
|
||||
log::error!("Failed to decode, the input data size is bigger than input buf");
|
||||
bail!("The input data size is bigger than input buf");
|
||||
}
|
||||
buf.write_all(&data)?;
|
||||
self.queue_input_buffer(input_buffer, 0, data.len(), 0, 0)?;
|
||||
}
|
||||
None => {
|
||||
log::debug!("Failed to dequeue_input_buffer: No available input_buffer");
|
||||
}
|
||||
};
|
||||
|
||||
return match self.dequeue_output_buffer(Duration::from_millis(100))? {
|
||||
Some(output_buffer) => {
|
||||
let res_format = self.output_format();
|
||||
let w = res_format
|
||||
.i32("width")
|
||||
.ok_or(Error::msg("Failed to dequeue_output_buffer, width is None"))?
|
||||
as usize;
|
||||
let h = res_format.i32("height").ok_or(Error::msg(
|
||||
"Failed to dequeue_output_buffer, height is None",
|
||||
))? as usize;
|
||||
let stride = res_format.i32("stride").ok_or(Error::msg(
|
||||
"Failed to dequeue_output_buffer, stride is None",
|
||||
))?;
|
||||
let buf = output_buffer.buffer();
|
||||
let bps = 4;
|
||||
let u = buf.len() * 2 / 3;
|
||||
let v = buf.len() * 5 / 6;
|
||||
rgb.raw.resize(h * w * bps, 0);
|
||||
let y_ptr = buf.as_ptr();
|
||||
let u_ptr = buf[u..].as_ptr();
|
||||
let v_ptr = buf[v..].as_ptr();
|
||||
unsafe {
|
||||
match rgb.fmt() {
|
||||
ImageFormat::ARGB => {
|
||||
I420ToARGB(
|
||||
y_ptr,
|
||||
stride,
|
||||
u_ptr,
|
||||
stride / 2,
|
||||
v_ptr,
|
||||
stride / 2,
|
||||
rgb.raw.as_mut_ptr(),
|
||||
(w * bps) as _,
|
||||
w as _,
|
||||
h as _,
|
||||
);
|
||||
}
|
||||
ImageFormat::ARGB => {
|
||||
I420ToABGR(
|
||||
y_ptr,
|
||||
stride,
|
||||
u_ptr,
|
||||
stride / 2,
|
||||
v_ptr,
|
||||
stride / 2,
|
||||
rgb.raw.as_mut_ptr(),
|
||||
(w * bps) as _,
|
||||
w as _,
|
||||
h as _,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
bail!("Unsupported image format");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.release_output_buffer(output_buffer, false)?;
|
||||
Ok(true)
|
||||
}
|
||||
None => {
|
||||
log::debug!("Failed to dequeue_output: No available dequeue_output");
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option<MediaCodecDecoder> {
|
||||
let codec = MediaCodec::from_decoder_type(name)?;
|
||||
let media_format = MediaFormat::new();
|
||||
media_format.set_str("mime", name);
|
||||
media_format.set_i32("width", 0);
|
||||
media_format.set_i32("height", 0);
|
||||
media_format.set_i32("color-format", 19); // COLOR_FormatYUV420Planar
|
||||
if let Err(e) = codec.configure(&media_format, None, direction) {
|
||||
log::error!("Failed to init decoder: {:?}", e);
|
||||
return None;
|
||||
};
|
||||
log::error!("decoder init success");
|
||||
if let Err(e) = codec.start() {
|
||||
log::error!("Failed to start decoder: {:?}", e);
|
||||
return None;
|
||||
};
|
||||
log::debug!("Init decoder succeeded!: {:?}", name);
|
||||
return Some(MediaCodecDecoder {
|
||||
decoder: codec,
|
||||
name: name.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn check_mediacodec() {
|
||||
std::thread::spawn(move || {
|
||||
// check decoders
|
||||
let decoders = MediaCodecDecoder::new_decoders();
|
||||
H264_DECODER_SUPPORT.swap(decoders.h264.is_some(), Ordering::SeqCst);
|
||||
H265_DECODER_SUPPORT.swap(decoders.h265.is_some(), Ordering::SeqCst);
|
||||
decoders.h264.map(|d| d.stop());
|
||||
decoders.h265.map(|d| d.stop());
|
||||
// TODO encoders
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
pub use self::vpxcodec::*;
|
||||
use hbb_common::{
|
||||
bail, log,
|
||||
message_proto::{video_frame, Chroma, VideoFrame},
|
||||
ResultType,
|
||||
};
|
||||
use std::{ffi::c_void, slice};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(quartz)] {
|
||||
mod quartz;
|
||||
pub use self::quartz::*;
|
||||
} else if #[cfg(x11)] {
|
||||
cfg_if! {
|
||||
if #[cfg(feature="wayland")] {
|
||||
mod linux;
|
||||
mod wayland;
|
||||
mod x11;
|
||||
pub use self::linux::*;
|
||||
pub use self::wayland::set_map_err;
|
||||
pub use self::x11::PixelBuffer;
|
||||
} else {
|
||||
mod x11;
|
||||
pub use self::x11::*;
|
||||
}
|
||||
}
|
||||
} else if #[cfg(dxgi)] {
|
||||
mod dxgi;
|
||||
pub use self::dxgi::*;
|
||||
} else if #[cfg(target_os = "android")] {
|
||||
mod android;
|
||||
pub use self::android::*;
|
||||
}else {
|
||||
//TODO: Fallback implementation.
|
||||
}
|
||||
}
|
||||
|
||||
pub mod codec;
|
||||
pub mod convert;
|
||||
#[cfg(feature = "hwcodec")]
|
||||
pub mod hwcodec;
|
||||
#[cfg(feature = "mediacodec")]
|
||||
pub mod mediacodec;
|
||||
pub mod vpxcodec;
|
||||
#[cfg(feature = "vram")]
|
||||
pub mod vram;
|
||||
pub use self::convert::*;
|
||||
pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller
|
||||
pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer
|
||||
|
||||
pub mod aom;
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pub mod camera;
|
||||
pub mod record;
|
||||
mod vpx;
|
||||
|
||||
#[repr(usize)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ImageFormat {
|
||||
Raw,
|
||||
ABGR,
|
||||
ARGB,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone)]
|
||||
pub struct ImageRgb {
|
||||
pub raw: Vec<u8>,
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
pub fmt: ImageFormat,
|
||||
pub align: usize,
|
||||
}
|
||||
|
||||
impl ImageRgb {
|
||||
pub fn new(fmt: ImageFormat, align: usize) -> Self {
|
||||
Self {
|
||||
raw: Vec::new(),
|
||||
w: 0,
|
||||
h: 0,
|
||||
fmt,
|
||||
align,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fmt(&self) -> ImageFormat {
|
||||
self.fmt
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn align(&self) -> usize {
|
||||
self.align
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_align(&mut self, align: usize) {
|
||||
self.align = align;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageTexture {
|
||||
pub texture: *mut c_void,
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
}
|
||||
|
||||
impl Default for ImageTexture {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
texture: std::ptr::null_mut(),
|
||||
w: 0,
|
||||
h: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()> {
|
||||
// does this really help?
|
||||
if b == &old[..] {
|
||||
return Err(std::io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
old.resize(b.len(), 0);
|
||||
old.copy_from_slice(b);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait TraitCapturer {
|
||||
// We doesn't support
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result<Frame<'a>>;
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_gdi(&self) -> bool;
|
||||
#[cfg(windows)]
|
||||
fn set_gdi(&mut self) -> bool;
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn device(&self) -> AdapterDevice;
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn set_output_texture(&mut self, texture: bool);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AdapterDevice {
|
||||
pub device: *mut c_void,
|
||||
pub vendor_id: ::std::os::raw::c_uint,
|
||||
pub luid: i64,
|
||||
}
|
||||
|
||||
impl Default for AdapterDevice {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device: std::ptr::null_mut(),
|
||||
vendor_id: Default::default(),
|
||||
luid: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TraitPixelBuffer {
|
||||
fn data(&self) -> &[u8];
|
||||
|
||||
fn width(&self) -> usize;
|
||||
|
||||
fn height(&self) -> usize;
|
||||
|
||||
fn stride(&self) -> Vec<usize>;
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt;
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pub enum Frame<'a> {
|
||||
PixelBuffer(PixelBuffer<'a>),
|
||||
Texture((*mut c_void, usize)),
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
impl Frame<'_> {
|
||||
pub fn valid<'a>(&'a self) -> bool {
|
||||
match self {
|
||||
Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(),
|
||||
Frame::Texture((texture, _)) => !texture.is_null(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to<'a>(
|
||||
&'a self,
|
||||
yuvfmt: EncodeYuvFormat,
|
||||
yuv: &'a mut Vec<u8>,
|
||||
mid_data: &mut Vec<u8>,
|
||||
) -> ResultType<EncodeInput<'a>> {
|
||||
match self {
|
||||
Frame::PixelBuffer(pixelbuffer) => {
|
||||
convert_to_yuv(&pixelbuffer, yuvfmt, yuv, mid_data)?;
|
||||
Ok(EncodeInput::YUV(yuv))
|
||||
}
|
||||
Frame::Texture(texture) => Ok(EncodeInput::Texture(*texture)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EncodeInput<'a> {
|
||||
YUV(&'a [u8]),
|
||||
Texture((*mut c_void, usize)),
|
||||
}
|
||||
|
||||
impl<'a> EncodeInput<'a> {
|
||||
pub fn yuv(&self) -> ResultType<&'_ [u8]> {
|
||||
match self {
|
||||
Self::YUV(f) => Ok(f),
|
||||
_ => bail!("not pixelfbuffer frame"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> ResultType<(*mut c_void, usize)> {
|
||||
match self {
|
||||
Self::Texture(f) => Ok(*f),
|
||||
_ => bail!("not texture frame"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Pixfmt {
|
||||
BGRA,
|
||||
RGBA,
|
||||
RGB565LE,
|
||||
I420,
|
||||
NV12,
|
||||
I444,
|
||||
}
|
||||
|
||||
impl Pixfmt {
|
||||
pub fn bpp(&self) -> usize {
|
||||
match self {
|
||||
Pixfmt::BGRA | Pixfmt::RGBA => 32,
|
||||
Pixfmt::RGB565LE => 16,
|
||||
Pixfmt::I420 | Pixfmt::NV12 => 12,
|
||||
Pixfmt::I444 => 24,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_per_pixel(&self) -> usize {
|
||||
(self.bpp() + 7) / 8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncodeYuvFormat {
|
||||
pub pixfmt: Pixfmt,
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
pub stride: Vec<usize>,
|
||||
pub u: usize,
|
||||
pub v: usize,
|
||||
}
|
||||
|
||||
#[cfg(x11)]
|
||||
#[inline]
|
||||
pub fn is_x11() -> bool {
|
||||
hbb_common::platform::linux::is_x11_or_headless()
|
||||
}
|
||||
|
||||
#[cfg(x11)]
|
||||
#[inline]
|
||||
pub fn is_cursor_embedded() -> bool {
|
||||
if is_x11() {
|
||||
x11::IS_CURSOR_EMBEDDED
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(x11))]
|
||||
#[inline]
|
||||
pub fn is_cursor_embedded() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CodecName {
|
||||
VP8,
|
||||
VP9,
|
||||
AV1,
|
||||
H264RAM(String),
|
||||
H265RAM(String),
|
||||
H264VRAM,
|
||||
H265VRAM,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub enum CodecFormat {
|
||||
VP8,
|
||||
VP9,
|
||||
AV1,
|
||||
H264,
|
||||
H265,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&VideoFrame> for CodecFormat {
|
||||
fn from(it: &VideoFrame) -> Self {
|
||||
match it.union {
|
||||
Some(video_frame::Union::Vp8s(_)) => CodecFormat::VP8,
|
||||
Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9,
|
||||
Some(video_frame::Union::Av1s(_)) => CodecFormat::AV1,
|
||||
Some(video_frame::Union::H264s(_)) => CodecFormat::H264,
|
||||
Some(video_frame::Union::H265s(_)) => CodecFormat::H265,
|
||||
_ => CodecFormat::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&video_frame::Union> for CodecFormat {
|
||||
fn from(it: &video_frame::Union) -> Self {
|
||||
match it {
|
||||
video_frame::Union::Vp8s(_) => CodecFormat::VP8,
|
||||
video_frame::Union::Vp9s(_) => CodecFormat::VP9,
|
||||
video_frame::Union::Av1s(_) => CodecFormat::AV1,
|
||||
video_frame::Union::H264s(_) => CodecFormat::H264,
|
||||
video_frame::Union::H265s(_) => CodecFormat::H265,
|
||||
_ => CodecFormat::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CodecName> for CodecFormat {
|
||||
fn from(value: &CodecName) -> Self {
|
||||
match value {
|
||||
CodecName::VP8 => Self::VP8,
|
||||
CodecName::VP9 => Self::VP9,
|
||||
CodecName::AV1 => Self::AV1,
|
||||
CodecName::H264RAM(_) | CodecName::H264VRAM => Self::H264,
|
||||
CodecName::H265RAM(_) | CodecName::H265VRAM => Self::H265,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for CodecFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
CodecFormat::VP8 => "VP8".into(),
|
||||
CodecFormat::VP9 => "VP9".into(),
|
||||
CodecFormat::AV1 => "AV1".into(),
|
||||
CodecFormat::H264 => "H264".into(),
|
||||
CodecFormat::H265 => "H265".into(),
|
||||
CodecFormat::Unknown => "Unknown".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
FailedCall(String),
|
||||
BadPtr(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generate_call_macro {
|
||||
($func_name:ident, $allow_err:expr) => {
|
||||
macro_rules! $func_name {
|
||||
($x:expr) => {{
|
||||
let result = unsafe { $x };
|
||||
let result_int = unsafe { std::mem::transmute::<_, i32>(result) };
|
||||
if result_int != 0 {
|
||||
let message = format!(
|
||||
"errcode={} {}:{}:{}:{}",
|
||||
result_int,
|
||||
module_path!(),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
if $allow_err {
|
||||
log::warn!("Failed to call {}, {}", stringify!($func_name), message);
|
||||
} else {
|
||||
return Err(crate::Error::FailedCall(message).into());
|
||||
}
|
||||
}
|
||||
result
|
||||
}};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generate_call_ptr_macro {
|
||||
($func_name:ident) => {
|
||||
macro_rules! $func_name {
|
||||
($x:expr) => {{
|
||||
let result = unsafe { $x };
|
||||
let result_int = unsafe { std::mem::transmute::<_, isize>(result) };
|
||||
if result_int == 0 {
|
||||
return Err(crate::Error::BadPtr(format!(
|
||||
"errcode={} {}:{}:{}:{}",
|
||||
result_int,
|
||||
module_path!(),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
result
|
||||
}};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait GoogleImage {
|
||||
fn width(&self) -> usize;
|
||||
fn height(&self) -> usize;
|
||||
fn stride(&self) -> Vec<i32>;
|
||||
fn planes(&self) -> Vec<*mut u8>;
|
||||
fn chroma(&self) -> Chroma;
|
||||
fn get_bytes_per_row(w: usize, fmt: ImageFormat, align: usize) -> usize {
|
||||
let bytes_per_pixel = match fmt {
|
||||
ImageFormat::Raw => 3,
|
||||
ImageFormat::ARGB | ImageFormat::ABGR => 4,
|
||||
};
|
||||
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L128
|
||||
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L129
|
||||
(w * bytes_per_pixel + align - 1) & !(align - 1)
|
||||
}
|
||||
// rgb [in/out] fmt and stride must be set in ImageRgb
|
||||
fn to(&self, rgb: &mut ImageRgb) {
|
||||
rgb.w = self.width();
|
||||
rgb.h = self.height();
|
||||
let bytes_per_row = Self::get_bytes_per_row(rgb.w, rgb.fmt, rgb.align());
|
||||
rgb.raw.resize(rgb.h * bytes_per_row, 0);
|
||||
let stride = self.stride();
|
||||
let planes = self.planes();
|
||||
unsafe {
|
||||
match (self.chroma(), rgb.fmt()) {
|
||||
(Chroma::I420, ImageFormat::Raw) => {
|
||||
super::I420ToRAW(
|
||||
planes[0],
|
||||
stride[0],
|
||||
planes[1],
|
||||
stride[1],
|
||||
planes[2],
|
||||
stride[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
(Chroma::I420, ImageFormat::ARGB) => {
|
||||
super::I420ToARGB(
|
||||
planes[0],
|
||||
stride[0],
|
||||
planes[1],
|
||||
stride[1],
|
||||
planes[2],
|
||||
stride[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
(Chroma::I420, ImageFormat::ABGR) => {
|
||||
super::I420ToABGR(
|
||||
planes[0],
|
||||
stride[0],
|
||||
planes[1],
|
||||
stride[1],
|
||||
planes[2],
|
||||
stride[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
(Chroma::I444, ImageFormat::ARGB) => {
|
||||
super::I444ToARGB(
|
||||
planes[0],
|
||||
stride[0],
|
||||
planes[1],
|
||||
stride[1],
|
||||
planes[2],
|
||||
stride[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
(Chroma::I444, ImageFormat::ABGR) => {
|
||||
super::I444ToABGR(
|
||||
planes[0],
|
||||
stride[0],
|
||||
planes[1],
|
||||
stride[1],
|
||||
planes[2],
|
||||
stride[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
// (Chroma::I444, ImageFormat::Raw), new version libyuv have I444ToRAW
|
||||
_ => log::error!("unsupported pixfmt: {:?}", self.chroma()),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn data(&self) -> (&[u8], &[u8], &[u8]) {
|
||||
unsafe {
|
||||
let stride = self.stride();
|
||||
let planes = self.planes();
|
||||
let h = (self.height() as usize + 1) & !1;
|
||||
let n = stride[0] as usize * h;
|
||||
let y = slice::from_raw_parts(planes[0], n);
|
||||
let n = stride[1] as usize * (h >> 1);
|
||||
let u = slice::from_raw_parts(planes[1], n);
|
||||
let v = slice::from_raw_parts(planes[2], n);
|
||||
(y, u, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn screen_size() -> (u16, u16, u16) {
|
||||
SCREEN_SIZE.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn is_start() -> Option<bool> {
|
||||
android::is_start()
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
use crate::{quartz, Frame, Pixfmt};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex, TryLockError};
|
||||
use std::{io, mem};
|
||||
|
||||
pub struct Capturer {
|
||||
inner: quartz::Capturer,
|
||||
frame: Arc<Mutex<Option<quartz::Frame>>>,
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let frame = Arc::new(Mutex::new(None));
|
||||
|
||||
let f = frame.clone();
|
||||
let inner = quartz::Capturer::new(
|
||||
display.0,
|
||||
display.width(),
|
||||
display.height(),
|
||||
quartz::PixelFormat::Argb8888,
|
||||
Default::default(),
|
||||
move |inner| {
|
||||
if let Ok(mut f) = f.lock() {
|
||||
*f = Some(inner);
|
||||
}
|
||||
},
|
||||
)
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::Other))?;
|
||||
|
||||
Ok(Capturer {
|
||||
inner,
|
||||
frame,
|
||||
saved_raw_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.inner.width()
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.inner.height()
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::TraitCapturer for Capturer {
|
||||
fn frame<'a>(&'a mut self, _timeout_ms: std::time::Duration) -> io::Result<Frame<'a>> {
|
||||
match self.frame.try_lock() {
|
||||
Ok(mut handle) => {
|
||||
let mut frame = None;
|
||||
mem::swap(&mut frame, &mut handle);
|
||||
|
||||
match frame {
|
||||
Some(mut frame) => {
|
||||
crate::would_block_if_equal(&mut self.saved_raw_data, frame.inner())?;
|
||||
frame.surface_to_bgra(self.height());
|
||||
Ok(Frame::PixelBuffer(PixelBuffer {
|
||||
frame,
|
||||
data: PhantomData,
|
||||
width: self.width(),
|
||||
height: self.height(),
|
||||
}))
|
||||
}
|
||||
|
||||
None => Err(io::ErrorKind::WouldBlock.into()),
|
||||
}
|
||||
}
|
||||
|
||||
Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()),
|
||||
|
||||
Err(TryLockError::Poisoned(..)) => Err(io::ErrorKind::Other.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PixelBuffer<'a> {
|
||||
frame: quartz::Frame,
|
||||
data: PhantomData<&'a [u8]>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
&*self.frame
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
let mut v = Vec::new();
|
||||
v.push(self.frame.stride());
|
||||
v
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt {
|
||||
Pixfmt::BGRA
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display(quartz::Display);
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
Ok(Display(quartz::Display::primary()))
|
||||
}
|
||||
|
||||
pub fn all() -> io::Result<Vec<Display>> {
|
||||
Ok(quartz::Display::online()
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::Other))?
|
||||
.into_iter()
|
||||
.map(Display)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.width()
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.height()
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
self.0.scale()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.0.id().to_string()
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
self.0.is_online()
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
let o = self.0.bounds().origin;
|
||||
(o.x as _, o.y as _)
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
self.0.is_primary()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
use crate::CodecFormat;
|
||||
#[cfg(feature = "hwcodec")]
|
||||
use hbb_common::anyhow::anyhow;
|
||||
use hbb_common::{
|
||||
bail, chrono, log,
|
||||
message_proto::{message, video_frame, EncodedVideoFrame, Message},
|
||||
ResultType,
|
||||
};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
use hwcodec::mux::{MuxContext, Muxer};
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
sync::mpsc::Sender,
|
||||
time::Instant,
|
||||
};
|
||||
use webm::mux::{self, Segment, Track, VideoTrack, Writer};
|
||||
|
||||
const MIN_SECS: u64 = 1;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecorderContext {
|
||||
pub server: bool,
|
||||
pub id: String,
|
||||
pub dir: String,
|
||||
pub display_idx: usize,
|
||||
pub camera: bool,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecorderContext2 {
|
||||
pub filename: String,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub format: CodecFormat,
|
||||
}
|
||||
|
||||
impl RecorderContext2 {
|
||||
pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> {
|
||||
if !PathBuf::from(&ctx.dir).exists() {
|
||||
std::fs::create_dir_all(&ctx.dir)?;
|
||||
}
|
||||
let file = if ctx.server { "incoming" } else { "outgoing" }.to_string()
|
||||
+ "_"
|
||||
+ &ctx.id.clone()
|
||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
|
||||
+ &format!(
|
||||
"{}{}_",
|
||||
if ctx.camera { "camera" } else { "display" },
|
||||
ctx.display_idx
|
||||
)
|
||||
+ &self.format.to_string().to_lowercase()
|
||||
+ if self.format == CodecFormat::VP9
|
||||
|| self.format == CodecFormat::VP8
|
||||
|| self.format == CodecFormat::AV1
|
||||
{
|
||||
".webm"
|
||||
} else {
|
||||
".mp4"
|
||||
};
|
||||
self.filename = PathBuf::from(&ctx.dir)
|
||||
.join(file)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Recorder {}
|
||||
unsafe impl Sync for Recorder {}
|
||||
|
||||
pub trait RecorderApi {
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RecordState {
|
||||
NewFile(String),
|
||||
NewFrame,
|
||||
WriteTail,
|
||||
RemoveFile,
|
||||
}
|
||||
|
||||
pub struct Recorder {
|
||||
pub inner: Option<Box<dyn RecorderApi>>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: Option<RecorderContext2>,
|
||||
pts: Option<i64>,
|
||||
check_failed: bool,
|
||||
}
|
||||
|
||||
impl Deref for Recorder {
|
||||
type Target = Option<Box<dyn RecorderApi>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Recorder {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Recorder {
|
||||
pub fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||
Ok(Self {
|
||||
inner: None,
|
||||
ctx,
|
||||
ctx2: None,
|
||||
pts: None,
|
||||
check_failed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
||||
match self.ctx2 {
|
||||
Some(ref ctx2) => {
|
||||
if ctx2.width != w || ctx2.height != h || ctx2.format != format {
|
||||
let mut ctx2 = RecorderContext2 {
|
||||
width: w,
|
||||
height: h,
|
||||
format,
|
||||
filename: Default::default(),
|
||||
};
|
||||
ctx2.set_filename(&self.ctx)?;
|
||||
self.ctx2 = Some(ctx2);
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut ctx2 = RecorderContext2 {
|
||||
width: w,
|
||||
height: h,
|
||||
format,
|
||||
filename: Default::default(),
|
||||
};
|
||||
ctx2.set_filename(&self.ctx)?;
|
||||
self.ctx2 = Some(ctx2);
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
let Some(ctx2) = &self.ctx2 else {
|
||||
bail!("ctx2 is None");
|
||||
};
|
||||
if self.inner.is_none() {
|
||||
self.inner = match format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new(
|
||||
WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?,
|
||||
)),
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Some(Box::new(HwRecorder::new(
|
||||
self.ctx.clone(),
|
||||
(*ctx2).clone(),
|
||||
)?)),
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
_ => bail!("unsupported codec type"),
|
||||
};
|
||||
// pts is None when new inner is created
|
||||
self.pts = None;
|
||||
self.send_state(RecordState::NewFile(ctx2.filename.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) {
|
||||
if let Some(message::Union::VideoFrame(vf)) = &msg.union {
|
||||
if let Some(frame) = &vf.union {
|
||||
self.write_frame(frame, w, h).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_frame(
|
||||
&mut self,
|
||||
frame: &video_frame::Union,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> ResultType<()> {
|
||||
if self.check_failed {
|
||||
bail!("check failed");
|
||||
}
|
||||
let format = CodecFormat::from(frame);
|
||||
if format == CodecFormat::Unknown {
|
||||
bail!("unsupported frame type");
|
||||
}
|
||||
let res = self.check(w, h, format);
|
||||
if res.is_err() {
|
||||
self.check_failed = true;
|
||||
log::error!("check failed: {:?}", res);
|
||||
res?;
|
||||
}
|
||||
match frame {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
for f in vp8s.frames.iter() {
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
for f in vp9s.frames.iter() {
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Av1s(av1s) => {
|
||||
for f in av1s.frames.iter() {
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
for f in h264s.frames.iter() {
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H265s(h265s) => {
|
||||
for f in h265s.frames.iter() {
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
_ => bail!("unsupported frame type"),
|
||||
}
|
||||
self.send_state(RecordState::NewFrame);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_pts(
|
||||
&mut self,
|
||||
pts: i64,
|
||||
key: bool,
|
||||
w: usize,
|
||||
h: usize,
|
||||
format: CodecFormat,
|
||||
) -> ResultType<()> {
|
||||
// https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c
|
||||
if self.pts.is_none() && !key {
|
||||
bail!("first frame is not key frame");
|
||||
}
|
||||
let old_pts = self.pts;
|
||||
self.pts = Some(pts);
|
||||
if old_pts.clone().unwrap_or_default() > pts {
|
||||
log::info!("pts {:?} -> {}, change record filename", old_pts, pts);
|
||||
self.inner = None;
|
||||
self.ctx2 = None;
|
||||
let res = self.check(w, h, format);
|
||||
if res.is_err() {
|
||||
self.check_failed = true;
|
||||
log::error!("check failed: {:?}", res);
|
||||
res?;
|
||||
}
|
||||
self.pts = Some(pts);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_state(&self, state: RecordState) {
|
||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||
}
|
||||
}
|
||||
|
||||
struct WebmRecorder {
|
||||
vt: VideoTrack,
|
||||
webm: Option<Segment<Writer<File>>>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: RecorderContext2,
|
||||
key: bool,
|
||||
written: bool,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl RecorderApi for WebmRecorder {
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||
let out = match {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&ctx2.filename)
|
||||
} {
|
||||
Ok(file) => file,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let mut webm = match mux::Segment::new(mux::Writer::new(out)) {
|
||||
Some(v) => v,
|
||||
None => bail!("Failed to create webm mux"),
|
||||
};
|
||||
let vt = webm.add_video_track(
|
||||
ctx2.width as _,
|
||||
ctx2.height as _,
|
||||
None,
|
||||
if ctx2.format == CodecFormat::VP9 {
|
||||
mux::VideoCodecId::VP9
|
||||
} else if ctx2.format == CodecFormat::VP8 {
|
||||
mux::VideoCodecId::VP8
|
||||
} else {
|
||||
mux::VideoCodecId::AV1
|
||||
},
|
||||
);
|
||||
if ctx2.format == CodecFormat::AV1 {
|
||||
// [129, 8, 12, 0] in 3.6.0, but zero works
|
||||
let codec_private = vec![0, 0, 0, 0];
|
||||
if !webm.set_codec_private(vt.track_number(), &codec_private) {
|
||||
bail!("Failed to set codec private");
|
||||
}
|
||||
}
|
||||
Ok(WebmRecorder {
|
||||
vt,
|
||||
webm: Some(webm),
|
||||
ctx,
|
||||
ctx2,
|
||||
key: false,
|
||||
written: false,
|
||||
start: Instant::now(),
|
||||
})
|
||||
}
|
||||
|
||||
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool {
|
||||
if frame.key {
|
||||
self.key = true;
|
||||
}
|
||||
if self.key {
|
||||
let ok = self
|
||||
.vt
|
||||
.add_frame(&frame.data, frame.pts as u64 * 1_000_000, frame.key);
|
||||
if ok {
|
||||
self.written = true;
|
||||
}
|
||||
ok
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WebmRecorder {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None));
|
||||
let mut state = RecordState::WriteTail;
|
||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||
state = RecordState::RemoveFile;
|
||||
}
|
||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
struct HwRecorder {
|
||||
muxer: Option<Muxer>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: RecorderContext2,
|
||||
written: bool,
|
||||
key: bool,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
impl RecorderApi for HwRecorder {
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||
let muxer = Muxer::new(MuxContext {
|
||||
filename: ctx2.filename.clone(),
|
||||
width: ctx2.width,
|
||||
height: ctx2.height,
|
||||
is265: ctx2.format == CodecFormat::H265,
|
||||
framerate: crate::hwcodec::DEFAULT_FPS as _,
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
||||
Ok(HwRecorder {
|
||||
muxer: Some(muxer),
|
||||
ctx,
|
||||
ctx2,
|
||||
written: false,
|
||||
key: false,
|
||||
start: Instant::now(),
|
||||
})
|
||||
}
|
||||
|
||||
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool {
|
||||
if frame.key {
|
||||
self.key = true;
|
||||
}
|
||||
if self.key {
|
||||
let ok = self
|
||||
.muxer
|
||||
.as_mut()
|
||||
.map(|m| m.write_video(&frame.data, frame.key).is_ok())
|
||||
.unwrap_or_default();
|
||||
if ok {
|
||||
self.written = true;
|
||||
}
|
||||
ok
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
impl Drop for HwRecorder {
|
||||
fn drop(&mut self) {
|
||||
self.muxer.as_mut().map(|m| m.write_tail().ok());
|
||||
let mut state = RecordState::WriteTail;
|
||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||
// The process cannot access the file because it is being used by another process
|
||||
self.muxer = None;
|
||||
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||
state = RecordState::RemoveFile;
|
||||
}
|
||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
impl Default for vpx_codec_enc_cfg {
|
||||
fn default() -> Self {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for vpx_codec_ctx {
|
||||
fn default() -> Self {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for vpx_image_t {
|
||||
fn default() -> Self {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs"));
|
||||
@@ -0,0 +1,597 @@
|
||||
// https://github.com/astraw/vpx-encode
|
||||
// https://github.com/astraw/env-libvpx-sys
|
||||
// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs
|
||||
// https://github.com/chromium/chromium/blob/e7b24573bc2e06fed4749dd6b6abfce67f29052f/media/video/vpx_video_encoder.cc#L522
|
||||
|
||||
use hbb_common::anyhow::{anyhow, Context};
|
||||
use hbb_common::log;
|
||||
use hbb_common::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame};
|
||||
use hbb_common::ResultType;
|
||||
|
||||
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi};
|
||||
use crate::{EncodeInput, EncodeYuvFormat, GoogleImage, Pixfmt, STRIDE_ALIGN};
|
||||
|
||||
use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
|
||||
use crate::{generate_call_macro, generate_call_ptr_macro, Error, Result};
|
||||
use hbb_common::bytes::Bytes;
|
||||
use std::os::raw::{c_int, c_uint};
|
||||
use std::{ptr, slice};
|
||||
|
||||
generate_call_macro!(call_vpx, false);
|
||||
generate_call_ptr_macro!(call_vpx_ptr);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum VpxVideoCodecId {
|
||||
VP8,
|
||||
VP9,
|
||||
}
|
||||
|
||||
impl Default for VpxVideoCodecId {
|
||||
fn default() -> VpxVideoCodecId {
|
||||
VpxVideoCodecId::VP9
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VpxEncoder {
|
||||
ctx: vpx_codec_ctx_t,
|
||||
width: usize,
|
||||
height: usize,
|
||||
id: VpxVideoCodecId,
|
||||
i444: bool,
|
||||
yuvfmt: EncodeYuvFormat,
|
||||
}
|
||||
|
||||
pub struct VpxDecoder {
|
||||
ctx: vpx_codec_ctx_t,
|
||||
}
|
||||
|
||||
impl EncoderApi for VpxEncoder {
|
||||
fn new(cfg: crate::codec::EncoderCfg, i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match cfg {
|
||||
crate::codec::EncoderCfg::VPX(config) => {
|
||||
let i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
|
||||
};
|
||||
let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
|
||||
call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
|
||||
|
||||
// https://www.webmproject.org/docs/encoder-parameters/
|
||||
// default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63
|
||||
// try rc_resize_allowed later
|
||||
|
||||
c.g_w = config.width;
|
||||
c.g_h = config.height;
|
||||
c.g_timebase.num = 1;
|
||||
c.g_timebase.den = 1000; // Output timestamp precision
|
||||
c.rc_undershoot_pct = 95;
|
||||
// When the data buffer falls below this percentage of fullness, a dropped frame is indicated. Set the threshold to zero (0) to disable this feature.
|
||||
// In dynamic scenes, low bitrate gets low fps while high bitrate gets high fps.
|
||||
c.rc_dropframe_thresh = 25;
|
||||
c.g_threads = codec_thread_num(64) as _;
|
||||
c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
|
||||
// https://developers.google.com/media/vp9/bitrate-modes/
|
||||
// Constant Bitrate mode (CBR) is recommended for live streaming with VP9.
|
||||
c.rc_end_usage = vpx_rc_mode::VPX_CBR;
|
||||
if let Some(keyframe_interval) = config.keyframe_interval {
|
||||
c.kf_min_dist = 0;
|
||||
c.kf_max_dist = keyframe_interval as _;
|
||||
} else {
|
||||
c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot
|
||||
}
|
||||
|
||||
let (q_min, q_max) = Self::calc_q_values(config.quality);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate =
|
||||
Self::bitrate(config.width as _, config.height as _, config.quality);
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp9/common/vp9_enums.h#29
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp8/vp8_cx_iface.c#282
|
||||
c.g_profile = if i444 && config.codec == VpxVideoCodecId::VP9 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
/*
|
||||
The VPX encoder supports two-pass encoding for rate control purposes.
|
||||
In two-pass encoding, the entire encoding process is performed twice.
|
||||
The first pass generates new control parameters for the second pass.
|
||||
|
||||
This approach enables the best PSNR at the same bit rate.
|
||||
*/
|
||||
|
||||
let mut ctx = Default::default();
|
||||
call_vpx!(vpx_codec_enc_init_ver(
|
||||
&mut ctx,
|
||||
i,
|
||||
&c,
|
||||
0,
|
||||
VPX_ENCODER_ABI_VERSION as _
|
||||
));
|
||||
|
||||
if config.codec == VpxVideoCodecId::VP9 {
|
||||
// set encoder internal speed settings
|
||||
// in ffmpeg, it is --speed option
|
||||
/*
|
||||
set to 0 or a positive value 1-16, the codec will try to adapt its
|
||||
complexity depending on the time it spends encoding. Increasing this
|
||||
number will make the speed go up and the quality go down.
|
||||
Negative values mean strict enforcement of this
|
||||
while positive values are adaptive
|
||||
*/
|
||||
/* https://developers.google.com/media/vp9/live-encoding
|
||||
Speed 5 to 8 should be used for live / real-time encoding.
|
||||
Lower numbers (5 or 6) are higher quality but require more CPU power.
|
||||
Higher numbers (7 or 8) will be lower quality but more manageable for lower latency
|
||||
use cases and also for lower CPU power devices such as mobile.
|
||||
*/
|
||||
call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 7,));
|
||||
// set row level multi-threading
|
||||
/*
|
||||
as some people in comments and below have already commented,
|
||||
more recent versions of libvpx support -row-mt 1 to enable tile row
|
||||
multi-threading. This can increase the number of tiles by up to 4x in VP9
|
||||
(since the max number of tile rows is 4, regardless of video height).
|
||||
To enable this, use -tile-rows N where N is the number of tile rows in
|
||||
log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile
|
||||
rows). The total number of active threads will then be equal to
|
||||
$tile_rows * $tile_columns
|
||||
*/
|
||||
call_vpx!(vpx_codec_control_(
|
||||
&mut ctx,
|
||||
VP9E_SET_ROW_MT as _,
|
||||
1 as c_int
|
||||
));
|
||||
|
||||
call_vpx!(vpx_codec_control_(
|
||||
&mut ctx,
|
||||
VP9E_SET_TILE_COLUMNS as _,
|
||||
4 as c_int
|
||||
));
|
||||
} else if config.codec == VpxVideoCodecId::VP8 {
|
||||
// https://github.com/webmproject/libvpx/blob/972149cafeb71d6f08df89e91a0130d6a38c4b15/vpx/vp8cx.h#L172
|
||||
// https://groups.google.com/a/webmproject.org/g/webm-discuss/c/DJhSrmfQ61M
|
||||
call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 12,));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ctx,
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
id: config.codec,
|
||||
i444,
|
||||
yuvfmt: Self::get_yuvfmt(config.width, config.height, i444),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType<VideoFrame> {
|
||||
let mut frames = Vec::new();
|
||||
for ref frame in self
|
||||
.encode(ms, input.yuv()?, STRIDE_ALIGN)
|
||||
.with_context(|| "Failed to encode")?
|
||||
{
|
||||
frames.push(VpxEncoder::create_frame(frame));
|
||||
}
|
||||
for ref frame in self.flush().with_context(|| "Failed to flush")? {
|
||||
frames.push(VpxEncoder::create_frame(frame));
|
||||
}
|
||||
|
||||
// to-do: flush periodically, e.g. 1 second
|
||||
if frames.len() > 0 {
|
||||
Ok(VpxEncoder::create_video_frame(self.id, frames))
|
||||
} else {
|
||||
Err(anyhow!("no valid frame"))
|
||||
}
|
||||
}
|
||||
|
||||
fn yuvfmt(&self) -> crate::EncodeYuvFormat {
|
||||
self.yuvfmt.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn input_texture(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let mut c = unsafe { *self.ctx.config.enc.to_owned() };
|
||||
let (q_min, q_max) = Self::calc_q_values(ratio);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio);
|
||||
call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &c));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bitrate(&self) -> u32 {
|
||||
let c = unsafe { *self.ctx.config.enc.to_owned() };
|
||||
c.rc_target_bitrate
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn latency_free(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_hardware(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn disable(&self) {}
|
||||
}
|
||||
|
||||
impl VpxEncoder {
|
||||
pub fn encode<'a>(&'a mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames<'a>> {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
}
|
||||
let fmt = if self.i444 {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I444
|
||||
} else {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I420
|
||||
};
|
||||
|
||||
let mut image = Default::default();
|
||||
call_vpx_ptr!(vpx_img_wrap(
|
||||
&mut image,
|
||||
fmt,
|
||||
self.width as _,
|
||||
self.height as _,
|
||||
stride_align as _,
|
||||
data.as_ptr() as _,
|
||||
));
|
||||
|
||||
call_vpx!(vpx_codec_encode(
|
||||
&mut self.ctx,
|
||||
&image,
|
||||
pts as _,
|
||||
1, // Duration
|
||||
0, // Flags
|
||||
VPX_DL_REALTIME as _,
|
||||
));
|
||||
|
||||
Ok(EncodeFrames {
|
||||
ctx: &mut self.ctx,
|
||||
iter: ptr::null(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Notify the encoder to return any pending packets
|
||||
pub fn flush<'a>(&'a mut self) -> Result<EncodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_encode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
-1, // PTS
|
||||
1, // Duration
|
||||
0, // Flags
|
||||
VPX_DL_REALTIME as _,
|
||||
));
|
||||
|
||||
Ok(EncodeFrames {
|
||||
ctx: &mut self.ctx,
|
||||
iter: ptr::null(),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn create_video_frame(
|
||||
codec_id: VpxVideoCodecId,
|
||||
frames: Vec<EncodedVideoFrame>,
|
||||
) -> VideoFrame {
|
||||
let mut vf = VideoFrame::new();
|
||||
let vpxs = EncodedVideoFrames {
|
||||
frames: frames.into(),
|
||||
..Default::default()
|
||||
};
|
||||
match codec_id {
|
||||
VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs),
|
||||
VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs),
|
||||
}
|
||||
vf
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_frame(frame: &EncodeFrame) -> EncodedVideoFrame {
|
||||
EncodedVideoFrame {
|
||||
data: Bytes::from(frame.data.to_vec()),
|
||||
key: frame.key,
|
||||
pts: frame.pts,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn bitrate(width: u32, height: u32, ratio: f32) -> u32 {
|
||||
let bitrate = base_bitrate(width, height) as f32;
|
||||
(bitrate * ratio) as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn calc_q_values(ratio: f32) -> (u32, u32) {
|
||||
let b = (ratio * 100.0) as u32;
|
||||
let b = std::cmp::min(b, 200);
|
||||
let q_min1 = 36;
|
||||
let q_min2 = 0;
|
||||
let q_max1 = 56;
|
||||
let q_max2 = 37;
|
||||
|
||||
let t = b as f32 / 200.0;
|
||||
|
||||
let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
|
||||
let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
|
||||
|
||||
q_min = q_min.clamp(q_min2, q_min1);
|
||||
q_max = q_max.clamp(q_max2, q_max1);
|
||||
|
||||
(q_min, q_max)
|
||||
}
|
||||
|
||||
fn get_yuvfmt(width: u32, height: u32, i444: bool) -> EncodeYuvFormat {
|
||||
let mut img = Default::default();
|
||||
let fmt = if i444 {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I444
|
||||
} else {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I420
|
||||
};
|
||||
unsafe {
|
||||
vpx_img_wrap(
|
||||
&mut img,
|
||||
fmt,
|
||||
width as _,
|
||||
height as _,
|
||||
crate::STRIDE_ALIGN as _,
|
||||
0x1 as _,
|
||||
);
|
||||
}
|
||||
let pixfmt = if i444 { Pixfmt::I444 } else { Pixfmt::I420 };
|
||||
EncodeYuvFormat {
|
||||
pixfmt,
|
||||
w: img.w as _,
|
||||
h: img.h as _,
|
||||
stride: img.stride.map(|s| s as usize).to_vec(),
|
||||
u: img.planes[1] as usize - img.planes[0] as usize,
|
||||
v: img.planes[2] as usize - img.planes[0] as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VpxEncoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let result = vpx_codec_destroy(&mut self.ctx);
|
||||
if result != VPX_CODEC_OK {
|
||||
panic!("failed to destroy vpx codec");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct EncodeFrame<'a> {
|
||||
/// Compressed data.
|
||||
pub data: &'a [u8],
|
||||
/// Whether the frame is a keyframe.
|
||||
pub key: bool,
|
||||
/// Presentation timestamp (in timebase units).
|
||||
pub pts: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VpxEncoderConfig {
|
||||
/// The width (in pixels).
|
||||
pub width: c_uint,
|
||||
/// The height (in pixels).
|
||||
pub height: c_uint,
|
||||
/// The bitrate ratio
|
||||
pub quality: f32,
|
||||
/// The codec
|
||||
pub codec: VpxVideoCodecId,
|
||||
/// keyframe interval
|
||||
pub keyframe_interval: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VpxDecoderConfig {
|
||||
pub codec: VpxVideoCodecId,
|
||||
}
|
||||
|
||||
pub struct EncodeFrames<'a> {
|
||||
ctx: &'a mut vpx_codec_ctx_t,
|
||||
iter: vpx_codec_iter_t,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for EncodeFrames<'a> {
|
||||
type Item = EncodeFrame<'a>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
unsafe {
|
||||
let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter);
|
||||
if pkt.is_null() {
|
||||
return None;
|
||||
} else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
|
||||
let f = &(*pkt).data.frame;
|
||||
return Some(Self::Item {
|
||||
data: slice::from_raw_parts(f.buf as _, f.sz as _),
|
||||
key: (f.flags & VPX_FRAME_IS_KEY) != 0,
|
||||
pts: f.pts,
|
||||
});
|
||||
} else {
|
||||
// Ignore the packet.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VpxDecoder {
|
||||
/// Create a new decoder
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The function may fail if the underlying libvpx does not provide
|
||||
/// the VP9 decoder.
|
||||
pub fn new(config: VpxDecoderConfig) -> Result<Self> {
|
||||
// This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can
|
||||
// cause UB if uninitialized.
|
||||
let i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
|
||||
};
|
||||
let mut ctx = Default::default();
|
||||
let cfg = vpx_codec_dec_cfg_t {
|
||||
threads: codec_thread_num(64) as _,
|
||||
w: 0,
|
||||
h: 0,
|
||||
};
|
||||
/*
|
||||
unsafe {
|
||||
println!("{}", vpx_codec_get_caps(i));
|
||||
}
|
||||
*/
|
||||
call_vpx!(vpx_codec_dec_init_ver(
|
||||
&mut ctx,
|
||||
i,
|
||||
&cfg,
|
||||
0,
|
||||
VPX_DECODER_ABI_VERSION as _,
|
||||
));
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
|
||||
/// Feed some compressed data to the encoder
|
||||
///
|
||||
/// The `data` slice is sent to the decoder
|
||||
///
|
||||
/// It matches a call to `vpx_codec_decode`.
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result<DecodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_decode(
|
||||
&mut self.ctx,
|
||||
data.as_ptr(),
|
||||
data.len() as _,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
));
|
||||
|
||||
Ok(DecodeFrames {
|
||||
ctx: &mut self.ctx,
|
||||
iter: ptr::null(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Notify the decoder to return any pending frame
|
||||
pub fn flush<'a>(&'a mut self) -> Result<DecodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_decode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
0
|
||||
));
|
||||
Ok(DecodeFrames {
|
||||
ctx: &mut self.ctx,
|
||||
iter: ptr::null(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VpxDecoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let result = vpx_codec_destroy(&mut self.ctx);
|
||||
if result != VPX_CODEC_OK {
|
||||
panic!("failed to destroy vpx codec");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DecodeFrames<'a> {
|
||||
ctx: &'a mut vpx_codec_ctx_t,
|
||||
iter: vpx_codec_iter_t,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for DecodeFrames<'a> {
|
||||
type Item = Image;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) };
|
||||
if img.is_null() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(Image(img));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c
|
||||
pub struct Image(*mut vpx_image_t);
|
||||
impl Image {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.0.is_null()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn format(&self) -> vpx_img_fmt_t {
|
||||
// VPX_IMG_FMT_I420
|
||||
self.inner().fmt
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &vpx_image_t {
|
||||
unsafe { &*self.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl GoogleImage for Image {
|
||||
#[inline]
|
||||
fn width(&self) -> usize {
|
||||
self.inner().d_w as _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn height(&self) -> usize {
|
||||
self.inner().d_h as _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stride(&self) -> Vec<i32> {
|
||||
self.inner().stride.iter().map(|x| *x as i32).collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn planes(&self) -> Vec<*mut u8> {
|
||||
self.inner().planes.iter().map(|p| *p as *mut u8).collect()
|
||||
}
|
||||
|
||||
fn chroma(&self) -> Chroma {
|
||||
match self.inner().fmt {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I444 => Chroma::I444,
|
||||
_ => Chroma::I420,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Image {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe { vpx_img_free(self.0) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for vpx_codec_ctx_t {}
|
||||
@@ -0,0 +1,404 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::c_void,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
codec::{enable_vram_option, EncoderApi, EncoderCfg},
|
||||
hwcodec::HwCodecConfig,
|
||||
AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt,
|
||||
};
|
||||
use hbb_common::{
|
||||
anyhow::{anyhow, bail, Context},
|
||||
bytes::Bytes,
|
||||
log,
|
||||
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
|
||||
ResultType,
|
||||
};
|
||||
use hwcodec::{
|
||||
common::{DataFormat, Driver, MAX_GOP},
|
||||
vram::{
|
||||
decode::{self, DecodeFrame, Decoder},
|
||||
encode::{self, EncodeFrame, Encoder},
|
||||
Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext,
|
||||
},
|
||||
};
|
||||
|
||||
// https://www.reddit.com/r/buildapc/comments/d2m4ny/two_graphics_cards_two_monitors/
|
||||
// https://www.reddit.com/r/techsupport/comments/t2v9u6/dual_monitor_setup_with_dual_gpu/
|
||||
// https://cybersided.com/two-monitors-two-gpus/
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getadapterluid#remarks
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENOCDE_NOT_USE: Arc<Mutex<HashMap<String, bool>>> = Default::default();
|
||||
static ref FALLBACK_GDI_DISPLAYS: Arc<Mutex<HashSet<String>>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VRamEncoderConfig {
|
||||
pub device: AdapterDevice,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub quality: f32,
|
||||
pub feature: FeatureContext,
|
||||
pub keyframe_interval: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct VRamEncoder {
|
||||
encoder: Encoder,
|
||||
pub format: DataFormat,
|
||||
ctx: EncodeContext,
|
||||
bitrate: u32,
|
||||
last_frame_len: usize,
|
||||
same_bad_len_counter: usize,
|
||||
}
|
||||
|
||||
impl EncoderApi for VRamEncoder {
|
||||
fn new(cfg: EncoderCfg, _i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match cfg {
|
||||
EncoderCfg::VRAM(config) => {
|
||||
let bitrate = Self::bitrate(
|
||||
config.feature.data_format,
|
||||
config.width,
|
||||
config.height,
|
||||
config.quality,
|
||||
);
|
||||
let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32;
|
||||
let ctx = EncodeContext {
|
||||
f: config.feature.clone(),
|
||||
d: DynamicContext {
|
||||
device: Some(config.device.device),
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
kbitrate: bitrate as _,
|
||||
framerate: 30,
|
||||
gop,
|
||||
},
|
||||
};
|
||||
match Encoder::new(ctx.clone()) {
|
||||
Ok(encoder) => Ok(VRamEncoder {
|
||||
encoder,
|
||||
ctx,
|
||||
format: config.feature.data_format,
|
||||
bitrate,
|
||||
last_frame_len: 0,
|
||||
same_bad_len_counter: 0,
|
||||
}),
|
||||
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_to_message(
|
||||
&mut self,
|
||||
frame: EncodeInput,
|
||||
ms: i64,
|
||||
) -> ResultType<hbb_common::message_proto::VideoFrame> {
|
||||
let (texture, rotation) = frame.texture()?;
|
||||
if rotation != 0 {
|
||||
// to-do: support rotation
|
||||
// Both the encoder and display(w,h) information need to be changed.
|
||||
bail!("rotation not supported");
|
||||
}
|
||||
let mut vf = VideoFrame::new();
|
||||
let mut frames = Vec::new();
|
||||
for frame in self
|
||||
.encode(texture, ms)
|
||||
.with_context(|| "Failed to encode")?
|
||||
{
|
||||
frames.push(EncodedVideoFrame {
|
||||
data: Bytes::from(frame.data),
|
||||
pts: frame.pts,
|
||||
key: frame.key == 1,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if frames.len() > 0 {
|
||||
// This kind of problem is occurred after a period of time when using AMD encoding,
|
||||
// the encoding length is fixed at about 40, and the picture is still
|
||||
const MIN_BAD_LEN: usize = 100;
|
||||
const MAX_BAD_COUNTER: usize = 30;
|
||||
let this_frame_len = frames[0].data.len();
|
||||
if this_frame_len < MIN_BAD_LEN && this_frame_len == self.last_frame_len {
|
||||
self.same_bad_len_counter += 1;
|
||||
if self.same_bad_len_counter >= MAX_BAD_COUNTER {
|
||||
log::info!(
|
||||
"{} times encoding len is {}, switch",
|
||||
self.same_bad_len_counter,
|
||||
self.last_frame_len
|
||||
);
|
||||
bail!(crate::codec::ENCODE_NEED_SWITCH);
|
||||
}
|
||||
} else {
|
||||
self.same_bad_len_counter = 0;
|
||||
}
|
||||
self.last_frame_len = this_frame_len;
|
||||
let frames = EncodedVideoFrames {
|
||||
frames: frames.into(),
|
||||
..Default::default()
|
||||
};
|
||||
match self.format {
|
||||
DataFormat::H264 => vf.set_h264s(frames),
|
||||
DataFormat::H265 => vf.set_h265s(frames),
|
||||
_ => bail!("{:?} not supported", self.format),
|
||||
}
|
||||
Ok(vf)
|
||||
} else {
|
||||
Err(anyhow!("no valid frame"))
|
||||
}
|
||||
}
|
||||
|
||||
fn yuvfmt(&self) -> EncodeYuvFormat {
|
||||
// useless
|
||||
EncodeYuvFormat {
|
||||
pixfmt: Pixfmt::BGRA,
|
||||
w: self.ctx.d.width as _,
|
||||
h: self.ctx.d.height as _,
|
||||
stride: Vec::new(),
|
||||
u: 0,
|
||||
v: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn input_texture(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let bitrate = Self::bitrate(
|
||||
self.ctx.f.data_format,
|
||||
self.ctx.d.width as _,
|
||||
self.ctx.d.height as _,
|
||||
ratio,
|
||||
);
|
||||
if bitrate > 0 {
|
||||
if self.encoder.set_bitrate((bitrate) as _).is_ok() {
|
||||
self.bitrate = bitrate;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn latency_free(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_hardware(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn disable(&self) {
|
||||
HwCodecConfig::clear(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
impl VRamEncoder {
|
||||
pub fn try_get(device: &AdapterDevice, format: CodecFormat) -> Option<FeatureContext> {
|
||||
let v: Vec<_> = Self::available(format)
|
||||
.drain(..)
|
||||
.filter(|e| e.luid == device.luid)
|
||||
.collect();
|
||||
if v.len() > 0 {
|
||||
// prefer ffmpeg
|
||||
if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) {
|
||||
return Some(ctx.clone());
|
||||
}
|
||||
Some(v[0].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available(format: CodecFormat) -> Vec<FeatureContext> {
|
||||
let fallbacks = FALLBACK_GDI_DISPLAYS.lock().unwrap().clone();
|
||||
if !fallbacks.is_empty() {
|
||||
log::info!("fallback gdi displays not empty: {fallbacks:?}");
|
||||
return vec![];
|
||||
}
|
||||
let not_use = ENOCDE_NOT_USE.lock().unwrap().clone();
|
||||
if not_use.values().any(|not_use| *not_use) {
|
||||
log::info!("currently not use vram encoders: {not_use:?}");
|
||||
return vec![];
|
||||
}
|
||||
let data_format = match format {
|
||||
CodecFormat::H264 => DataFormat::H264,
|
||||
CodecFormat::H265 => DataFormat::H265,
|
||||
_ => return vec![],
|
||||
};
|
||||
let v: Vec<_> = crate::hwcodec::HwCodecConfig::get()
|
||||
.vram_encode
|
||||
.drain(..)
|
||||
.filter(|c| c.data_format == data_format)
|
||||
.collect();
|
||||
if crate::hwcodec::HwRamEncoder::try_get(format).is_some() {
|
||||
// has fallback, no need to require all adapters support
|
||||
v
|
||||
} else {
|
||||
let Ok(displays) = crate::Display::all() else {
|
||||
log::error!("failed to get displays");
|
||||
return vec![];
|
||||
};
|
||||
if displays.is_empty() {
|
||||
log::error!("no display found");
|
||||
return vec![];
|
||||
}
|
||||
let luids = displays
|
||||
.iter()
|
||||
.map(|d| d.adapter_luid())
|
||||
.collect::<Vec<_>>();
|
||||
if luids
|
||||
.iter()
|
||||
.all(|luid| v.iter().any(|f| Some(f.luid) == *luid))
|
||||
{
|
||||
v
|
||||
} else {
|
||||
log::info!("not all adapters support {data_format:?}, luids = {luids:?}");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&mut self, texture: *mut c_void, ms: i64) -> ResultType<Vec<EncodeFrame>> {
|
||||
match self.encoder.encode(texture, ms) {
|
||||
Ok(v) => {
|
||||
let mut data = Vec::<EncodeFrame>::new();
|
||||
data.append(v);
|
||||
Ok(data)
|
||||
}
|
||||
Err(_) => Ok(Vec::<EncodeFrame>::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bitrate(fmt: DataFormat, width: usize, height: usize, ratio: f32) -> u32 {
|
||||
crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264)
|
||||
}
|
||||
|
||||
pub fn set_not_use(video_service_name: String, not_use: bool) {
|
||||
log::info!("set {video_service_name} not use vram encode to {not_use}");
|
||||
ENOCDE_NOT_USE
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(video_service_name, not_use);
|
||||
}
|
||||
|
||||
pub fn set_fallback_gdi(video_service_name: String, fallback: bool) {
|
||||
if fallback {
|
||||
FALLBACK_GDI_DISPLAYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(video_service_name);
|
||||
} else {
|
||||
FALLBACK_GDI_DISPLAYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&video_service_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VRamDecoder {
|
||||
decoder: Decoder,
|
||||
}
|
||||
|
||||
impl VRamDecoder {
|
||||
pub fn try_get(format: CodecFormat, luid: Option<i64>) -> Option<DecodeContext> {
|
||||
let v: Vec<_> = Self::available(format, luid);
|
||||
if v.len() > 0 {
|
||||
// prefer ffmpeg
|
||||
if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) {
|
||||
return Some(ctx.clone());
|
||||
}
|
||||
Some(v[0].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available(format: CodecFormat, luid: Option<i64>) -> Vec<DecodeContext> {
|
||||
let luid = luid.unwrap_or_default();
|
||||
let data_format = match format {
|
||||
CodecFormat::H264 => DataFormat::H264,
|
||||
CodecFormat::H265 => DataFormat::H265,
|
||||
_ => return vec![],
|
||||
};
|
||||
crate::hwcodec::HwCodecConfig::get()
|
||||
.vram_decode
|
||||
.drain(..)
|
||||
.filter(|c| c.data_format == data_format && c.luid == luid && luid != 0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn possible_available_without_check() -> (bool, bool) {
|
||||
if !enable_vram_option(false) {
|
||||
return (false, false);
|
||||
}
|
||||
let v = crate::hwcodec::HwCodecConfig::get().vram_decode;
|
||||
(
|
||||
v.iter().any(|d| d.data_format == DataFormat::H264),
|
||||
v.iter().any(|d| d.data_format == DataFormat::H265),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new(format: CodecFormat, luid: Option<i64>) -> ResultType<Self> {
|
||||
let ctx = Self::try_get(format, luid).ok_or(anyhow!("Failed to get decode context"))?;
|
||||
log::info!("try create vram decoder: {ctx:?}");
|
||||
match Decoder::new(ctx) {
|
||||
Ok(decoder) => Ok(Self { decoder }),
|
||||
Err(_) => {
|
||||
HwCodecConfig::clear(true, false);
|
||||
Err(anyhow!(format!(
|
||||
"Failed to create decoder, format: {:?}",
|
||||
format
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType<Vec<VRamDecoderImage<'a>>> {
|
||||
match self.decoder.decode(data) {
|
||||
Ok(v) => Ok(v.iter().map(|f| VRamDecoderImage { frame: f }).collect()),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VRamDecoderImage<'a> {
|
||||
pub frame: &'a DecodeFrame,
|
||||
}
|
||||
|
||||
impl VRamDecoderImage<'_> {}
|
||||
|
||||
pub(crate) fn check_available_vram() -> (Vec<FeatureContext>, Vec<DecodeContext>, String) {
|
||||
let d = DynamicContext {
|
||||
device: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
kbitrate: 5000,
|
||||
framerate: 60,
|
||||
gop: MAX_GOP as _,
|
||||
};
|
||||
let encoders = encode::available(d);
|
||||
let decoders = decode::available();
|
||||
let available = Available {
|
||||
e: encoders.clone(),
|
||||
d: decoders.clone(),
|
||||
};
|
||||
(
|
||||
encoders,
|
||||
decoders,
|
||||
available.serialize().unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use crate::{
|
||||
wayland::{capturable::*, *},
|
||||
Frame, TraitCapturer,
|
||||
};
|
||||
use std::{io, sync::RwLock, time::Duration};
|
||||
|
||||
use super::x11::PixelBuffer;
|
||||
|
||||
pub struct Capturer(Display, Box<dyn Recorder>, Vec<u8>);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MAP_ERR: RwLock<Option<fn(err: String)-> io::Error>> = Default::default();
|
||||
}
|
||||
|
||||
pub fn set_map_err(f: fn(err: String) -> io::Error) {
|
||||
*MAP_ERR.write().unwrap() = Some(f);
|
||||
}
|
||||
|
||||
fn map_err<E: ToString>(err: E) -> io::Error {
|
||||
if let Some(f) = *MAP_ERR.read().unwrap() {
|
||||
f(err.to_string())
|
||||
} else {
|
||||
io::Error::new(io::ErrorKind::Other, err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let r = display.0.recorder(false).map_err(map_err)?;
|
||||
Ok(Capturer(display, r, Default::default()))
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.width()
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.height()
|
||||
}
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
match self.1.capture(timeout.as_millis() as _).map_err(map_err)? {
|
||||
PixelProvider::BGR0(w, h, x) => Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
x,
|
||||
crate::Pixfmt::BGRA,
|
||||
w,
|
||||
h,
|
||||
))),
|
||||
PixelProvider::RGB0(w, h, x) => Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
x,
|
||||
crate::Pixfmt::RGBA,
|
||||
w,
|
||||
h,
|
||||
))),
|
||||
PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()),
|
||||
_ => Err(map_err("Invalid data")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display(pub(crate) pipewire::PipeWireCapturable);
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
let mut all = Display::all()?;
|
||||
if all.is_empty() {
|
||||
return Err(io::ErrorKind::NotFound.into());
|
||||
}
|
||||
Ok(all.remove(0))
|
||||
}
|
||||
|
||||
pub fn all() -> io::Result<Vec<Display>> {
|
||||
Ok(pipewire::get_capturables()
|
||||
.map_err(map_err)?
|
||||
.drain(..)
|
||||
.map(|x| Display(x))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.physical_width()
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.physical_height()
|
||||
}
|
||||
|
||||
pub fn physical_width(&self) -> usize {
|
||||
self.0.physical_size.0
|
||||
}
|
||||
|
||||
pub fn physical_height(&self) -> usize {
|
||||
self.0.physical_size.1
|
||||
}
|
||||
|
||||
pub fn logical_width(&self) -> usize {
|
||||
self.0.logical_size.0
|
||||
}
|
||||
|
||||
pub fn logical_height(&self) -> usize {
|
||||
self.0.logical_size.1
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
if self.logical_width() == 0 {
|
||||
1.0
|
||||
} else {
|
||||
self.physical_width() as f64 / self.logical_width() as f64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
self.0.position
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
self.0.primary
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use crate::{common::TraitCapturer, x11, Frame, Pixfmt, TraitPixelBuffer};
|
||||
use std::{io, time::Duration};
|
||||
|
||||
pub struct Capturer(x11::Capturer);
|
||||
|
||||
pub const IS_CURSOR_EMBEDDED: bool = false;
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
x11::Capturer::new(display.0).map(Capturer)
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.display().rect().w as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.display().rect().h as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
let width = self.width();
|
||||
let height = self.height();
|
||||
let pixfmt = self.0.display().pixfmt();
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
self.0.frame()?,
|
||||
pixfmt,
|
||||
width,
|
||||
height,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PixelBuffer<'a> {
|
||||
data: &'a [u8],
|
||||
pixfmt: Pixfmt,
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a> PixelBuffer<'a> {
|
||||
pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self {
|
||||
let stride0 = data.len() / height;
|
||||
let mut stride = Vec::new();
|
||||
stride.push(stride0);
|
||||
Self {
|
||||
data,
|
||||
pixfmt,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TraitPixelBuffer for PixelBuffer<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
self.stride.clone()
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> crate::Pixfmt {
|
||||
self.pixfmt
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display(x11::Display);
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
let server = match x11::Server::default() {
|
||||
Ok(server) => server,
|
||||
Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()),
|
||||
};
|
||||
|
||||
let mut displays = x11::Server::displays(server);
|
||||
let mut best = displays.next();
|
||||
if best.as_ref().map(|x| x.is_default()) == Some(false) {
|
||||
best = displays.find(|x| x.is_default()).or(best);
|
||||
}
|
||||
|
||||
match best {
|
||||
Some(best) => Ok(Display(best)),
|
||||
None => Err(io::ErrorKind::NotFound.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all() -> io::Result<Vec<Display>> {
|
||||
let server = match x11::Server::default() {
|
||||
Ok(server) => server,
|
||||
Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()),
|
||||
};
|
||||
|
||||
Ok(x11::Server::displays(server).map(Display).collect())
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.rect().w as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.rect().h as usize
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
let r = self.0.rect();
|
||||
(r.x as _, r.y as _)
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
self.0.is_default()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
pub fn get_shm_status(&self) -> Result<(), x11::Error> {
|
||||
self.0.server().get_shm_status()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user