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,511 @@
|
||||
use jni::objects::JByteBuffer;
|
||||
use jni::objects::JString;
|
||||
use jni::objects::JValue;
|
||||
use jni::sys::jboolean;
|
||||
use jni::JNIEnv;
|
||||
use jni::{
|
||||
objects::{GlobalRef, JClass, JObject},
|
||||
strings::JNIString,
|
||||
JavaVM,
|
||||
};
|
||||
|
||||
use hbb_common::{message_proto::MultiClipboards, protobuf::Message};
|
||||
use jni::errors::{Error as JniError, Result as JniResult};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Not;
|
||||
use std::os::raw::c_void;
|
||||
use std::sync::atomic::{AtomicPtr, Ordering::SeqCst};
|
||||
use std::sync::{Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
lazy_static! {
|
||||
static ref JVM: RwLock<Option<JavaVM>> = RwLock::new(None);
|
||||
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
|
||||
static ref APPLICATION_CONTEXT: RwLock<Option<GlobalRef>> = RwLock::new(None);
|
||||
static ref VIDEO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT));
|
||||
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
|
||||
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
|
||||
static ref MEDIA_CODEC_INFOS: RwLock<Option<MediaCodecInfos>> = RwLock::new(None);
|
||||
static ref CLIPBOARD_MANAGER: RwLock<Option<GlobalRef>> = RwLock::new(None);
|
||||
static ref CLIPBOARDS_HOST: Mutex<Option<MultiClipboards>> = Mutex::new(None);
|
||||
static ref CLIPBOARDS_CLIENT: Mutex<Option<MultiClipboards>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
const MAX_AUDIO_FRAME_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
struct FrameRaw {
|
||||
name: &'static str,
|
||||
ptr: AtomicPtr<u8>,
|
||||
len: usize,
|
||||
last_update: Instant,
|
||||
timeout: Duration,
|
||||
enable: bool,
|
||||
}
|
||||
|
||||
impl FrameRaw {
|
||||
fn new(name: &'static str, timeout: Duration) -> Self {
|
||||
FrameRaw {
|
||||
name,
|
||||
ptr: AtomicPtr::default(),
|
||||
len: 0,
|
||||
last_update: Instant::now(),
|
||||
timeout,
|
||||
enable: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_enable(&mut self, value: bool) {
|
||||
self.enable = value;
|
||||
self.ptr.store(std::ptr::null_mut(), SeqCst);
|
||||
self.len = 0;
|
||||
}
|
||||
|
||||
fn update(&mut self, data: *mut u8, len: usize) {
|
||||
if self.enable.not() {
|
||||
return;
|
||||
}
|
||||
self.len = len;
|
||||
self.ptr.store(data, SeqCst);
|
||||
self.last_update = Instant::now();
|
||||
}
|
||||
|
||||
// take inner data as slice
|
||||
// release when success
|
||||
fn take<'a>(&mut self, dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
|
||||
if self.enable.not() {
|
||||
return None;
|
||||
}
|
||||
let ptr = self.ptr.load(SeqCst);
|
||||
if ptr.is_null() || self.len == 0 {
|
||||
None
|
||||
} else {
|
||||
if self.last_update.elapsed() > self.timeout {
|
||||
log::trace!("Failed to take {} raw,timeout!", self.name);
|
||||
return None;
|
||||
}
|
||||
let slice = unsafe { std::slice::from_raw_parts(ptr, self.len) };
|
||||
self.release();
|
||||
if last.len() == slice.len() && crate::would_block_if_equal(last, slice).is_err() {
|
||||
return None;
|
||||
}
|
||||
dst.resize(slice.len(), 0);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(slice.as_ptr(), dst.as_mut_ptr(), slice.len());
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
self.len = 0;
|
||||
self.ptr.store(std::ptr::null_mut(), SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_video_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
|
||||
VIDEO_RAW.lock().ok()?.take(dst, last)
|
||||
}
|
||||
|
||||
pub fn get_audio_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
|
||||
AUDIO_RAW.lock().ok()?.take(dst, last)
|
||||
}
|
||||
|
||||
pub fn get_clipboards(client: bool) -> Option<MultiClipboards> {
|
||||
if client {
|
||||
CLIPBOARDS_CLIENT.lock().ok()?.take()
|
||||
} else {
|
||||
CLIPBOARDS_HOST.lock().ok()?.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
buffer: JObject,
|
||||
) {
|
||||
let jb = JByteBuffer::from(buffer);
|
||||
if let Ok(data) = env.get_direct_buffer_address(&jb) {
|
||||
if let Ok(len) = env.get_direct_buffer_capacity(&jb) {
|
||||
VIDEO_RAW.lock().unwrap().update(data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
buffer: JObject,
|
||||
) {
|
||||
let jb = JByteBuffer::from(buffer);
|
||||
if let Ok(data) = env.get_direct_buffer_address(&jb) {
|
||||
if let Ok(len) = env.get_direct_buffer_capacity(&jb) {
|
||||
AUDIO_RAW.lock().unwrap().update(data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onClipboardUpdate(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
buffer: JByteBuffer,
|
||||
) {
|
||||
if let Ok(data) = env.get_direct_buffer_address(&buffer) {
|
||||
if let Ok(len) = env.get_direct_buffer_capacity(&buffer) {
|
||||
let data = unsafe { std::slice::from_raw_parts(data, len) };
|
||||
if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) {
|
||||
let is_client = data[0] == 1;
|
||||
if is_client {
|
||||
*CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips);
|
||||
} else {
|
||||
*CLIPBOARDS_HOST.lock().unwrap() = Some(clips);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_setFrameRawEnable(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
name: JString,
|
||||
value: jboolean,
|
||||
) {
|
||||
let mut env = env;
|
||||
if let Ok(name) = env.get_string(&name) {
|
||||
let name: String = name.into();
|
||||
let value = value.eq(&1);
|
||||
if name.eq("video") {
|
||||
VIDEO_RAW.lock().unwrap().set_enable(value);
|
||||
} else if name.eq("audio") {
|
||||
AUDIO_RAW.lock().unwrap().set_enable(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) {
|
||||
log::debug!("MainService init from java");
|
||||
if let Ok(jvm) = env.get_java_vm() {
|
||||
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
|
||||
let mut jvm_lock = JVM.write().unwrap();
|
||||
if jvm_lock.is_none() {
|
||||
*jvm_lock = Some(jvm);
|
||||
}
|
||||
drop(jvm_lock);
|
||||
if let Ok(context) = env.new_global_ref(ctx) {
|
||||
let context_jobject = context.as_obj().as_raw() as *mut c_void;
|
||||
*MAIN_SERVICE_CTX.write().unwrap() = Some(context);
|
||||
init_ndk_context(java_vm, context_jobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_setClipboardManager(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
clipboard_manager: JObject,
|
||||
) {
|
||||
log::debug!("ClipboardManager init from java");
|
||||
if let Ok(jvm) = env.get_java_vm() {
|
||||
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
|
||||
let mut jvm_lock = JVM.write().unwrap();
|
||||
if jvm_lock.is_none() {
|
||||
*jvm_lock = Some(jvm);
|
||||
}
|
||||
drop(jvm_lock);
|
||||
if let Ok(manager) = env.new_global_ref(clipboard_manager) {
|
||||
*CLIPBOARD_MANAGER.write().unwrap() = Some(manager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct MediaCodecInfo {
|
||||
pub name: String,
|
||||
pub is_encoder: bool,
|
||||
#[serde(default)]
|
||||
pub hw: Option<bool>, // api 29+
|
||||
pub mime_type: String,
|
||||
pub surface: bool,
|
||||
pub nv12: bool,
|
||||
#[serde(default)]
|
||||
pub low_latency: Option<bool>, // api 30+, decoder
|
||||
pub min_bitrate: u32,
|
||||
pub max_bitrate: u32,
|
||||
pub min_width: usize,
|
||||
pub max_width: usize,
|
||||
pub min_height: usize,
|
||||
pub max_height: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct MediaCodecInfos {
|
||||
pub version: usize,
|
||||
pub w: usize, // aligned
|
||||
pub h: usize, // aligned
|
||||
pub codecs: Vec<MediaCodecInfo>,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_setCodecInfo(env: JNIEnv, _class: JClass, info: JString) {
|
||||
let mut env = env;
|
||||
if let Ok(info) = env.get_string(&info) {
|
||||
let info: String = info.into();
|
||||
if let Ok(infos) = serde_json::from_str::<MediaCodecInfos>(&info) {
|
||||
*MEDIA_CODEC_INFOS.write().unwrap() = Some(infos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_codec_info() -> Option<MediaCodecInfos> {
|
||||
MEDIA_CODEC_INFOS.read().unwrap().as_ref().cloned()
|
||||
}
|
||||
|
||||
pub fn clear_codec_info() {
|
||||
*MEDIA_CODEC_INFOS.write().unwrap() = None;
|
||||
}
|
||||
|
||||
// another way to fix "reference table overflow" error caused by new_string and call_main_service_pointer_input frequently calld
|
||||
// is below, but here I change kind from string to int for performance
|
||||
/*
|
||||
env.with_local_frame(10, || {
|
||||
let kind = env.new_string(kind)?;
|
||||
env.call_method(
|
||||
ctx,
|
||||
"rustPointerInput",
|
||||
"(Ljava/lang/String;III)V",
|
||||
&[
|
||||
JValue::Object(&JObject::from(kind)),
|
||||
JValue::Int(mask),
|
||||
JValue::Int(x),
|
||||
JValue::Int(y),
|
||||
],
|
||||
)?;
|
||||
Ok(JObject::null())
|
||||
})?;
|
||||
*/
|
||||
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
|
||||
if let (Some(jvm), Some(ctx)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread_as_daemon()?;
|
||||
let kind = if kind == "touch" { 0 } else { 1 };
|
||||
env.call_method(
|
||||
ctx,
|
||||
"rustPointerInput",
|
||||
"(IIII)V",
|
||||
&[
|
||||
JValue::Int(kind),
|
||||
JValue::Int(mask),
|
||||
JValue::Int(x),
|
||||
JValue::Int(y),
|
||||
],
|
||||
)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> {
|
||||
if let (Some(jvm), Some(ctx)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread_as_daemon()?;
|
||||
let data = env.byte_array_from_slice(data)?;
|
||||
|
||||
env.call_method(
|
||||
ctx,
|
||||
"rustKeyEventInput",
|
||||
"([B)V",
|
||||
&[JValue::Object(&JObject::from(data))],
|
||||
)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
fn _call_clipboard_manager<S, T>(name: S, sig: T, args: &[JValue]) -> JniResult<()>
|
||||
where
|
||||
S: Into<JNIString>,
|
||||
T: Into<JNIString> + AsRef<str>,
|
||||
{
|
||||
if let (Some(jvm), Some(cm)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread()?;
|
||||
env.call_method(cm, name, sig, args)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> {
|
||||
if let (Some(jvm), Some(cm)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread()?;
|
||||
let data = env.byte_array_from_slice(data)?;
|
||||
|
||||
env.call_method(
|
||||
cm,
|
||||
"rustUpdateClipboard",
|
||||
"([B)V",
|
||||
&[JValue::Object(&JObject::from(data))],
|
||||
)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> {
|
||||
_call_clipboard_manager(
|
||||
"rustEnableClientClipboard",
|
||||
"(Z)V",
|
||||
&[JValue::Bool(jboolean::from(enable))],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn call_main_service_get_by_name(name: &str) -> JniResult<String> {
|
||||
if let (Some(jvm), Some(ctx)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread_as_daemon()?;
|
||||
let res = env.with_local_frame(10, |env| -> JniResult<String> {
|
||||
let name = env.new_string(name)?;
|
||||
let res = env
|
||||
.call_method(
|
||||
ctx,
|
||||
"rustGetByName",
|
||||
"(Ljava/lang/String;)Ljava/lang/String;",
|
||||
&[JValue::Object(&JObject::from(name))],
|
||||
)?
|
||||
.l()?;
|
||||
let res = JString::from(res);
|
||||
let res = env.get_string(&res)?;
|
||||
let res = res.to_string_lossy().to_string();
|
||||
Ok(res)
|
||||
})?;
|
||||
Ok(res)
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_main_service_set_by_name(
|
||||
name: &str,
|
||||
arg1: Option<&str>,
|
||||
arg2: Option<&str>,
|
||||
) -> JniResult<()> {
|
||||
if let (Some(jvm), Some(ctx)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread_as_daemon()?;
|
||||
env.with_local_frame(10, |env| -> JniResult<()> {
|
||||
let name = env.new_string(name)?;
|
||||
let arg1 = env.new_string(arg1.unwrap_or(""))?;
|
||||
let arg2 = env.new_string(arg2.unwrap_or(""))?;
|
||||
|
||||
env.call_method(
|
||||
ctx,
|
||||
"rustSetByName",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
|
||||
&[
|
||||
JValue::Object(&JObject::from(name)),
|
||||
JValue::Object(&JObject::from(arg1)),
|
||||
JValue::Object(&JObject::from(arg2)),
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
// Difference between MainService, MainActivity, JNI_OnLoad:
|
||||
// jvm is the same, ctx is differen and ctx of JNI_OnLoad is null.
|
||||
// cpal: all three works
|
||||
// Service(GetByName, ...): only ctx from MainService works, so use 2 init context functions
|
||||
// On app start: JNI_OnLoad or MainActivity init context
|
||||
// On service start first time: MainService replace the context
|
||||
|
||||
fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) {
|
||||
let mut lock = NDK_CONTEXT_INITED.lock().unwrap();
|
||||
if *lock {
|
||||
unsafe {
|
||||
ndk_context::release_android_context();
|
||||
}
|
||||
*lock = false;
|
||||
}
|
||||
unsafe {
|
||||
ndk_context::initialize_android_context(java_vm, context_jobject);
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hwcodec::android::ffmpeg_set_java_vm(java_vm);
|
||||
}
|
||||
*lock = true;
|
||||
}
|
||||
|
||||
fn try_init_rustls_platform_verifier(env: &mut JNIEnv, context_jobject: *mut c_void) {
|
||||
use hbb_common::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED as INITIALIZED;
|
||||
use std::sync::atomic::Ordering;
|
||||
let initialized = INITIALIZED.load(Ordering::Relaxed);
|
||||
if !initialized {
|
||||
let ctx_for_rustls = unsafe { JObject::from_raw(context_jobject as jni::sys::jobject) };
|
||||
if let Err(e) =
|
||||
hbb_common::rustls_platform_verifier::android::init_hosted(env, ctx_for_rustls)
|
||||
{
|
||||
log::error!("Failed to initialize rustls-platform-verifier: {:?}", e);
|
||||
} else {
|
||||
INITIALIZED.store(true, Ordering::Relaxed);
|
||||
log::info!("rustls-platform-verifier initialized successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init
|
||||
#[no_mangle]
|
||||
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
|
||||
if let Ok(env) = vm.get_env() {
|
||||
let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void;
|
||||
init_ndk_context(vm, res);
|
||||
}
|
||||
jni::JNIVersion::V6.into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onAppStart(mut env: JNIEnv, _class: JClass, ctx: JObject) {
|
||||
if ctx.is_null() {
|
||||
log::error!("application context is null");
|
||||
return;
|
||||
}
|
||||
if APPLICATION_CONTEXT.read().unwrap().is_some() {
|
||||
log::info!("application context already initialized");
|
||||
return;
|
||||
}
|
||||
if let Ok(jvm) = env.get_java_vm() {
|
||||
if let Ok(context) = env.new_global_ref(ctx) {
|
||||
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
|
||||
let context_jobject = context.as_obj().as_raw() as *mut c_void;
|
||||
*APPLICATION_CONTEXT.write().unwrap() = Some(context);
|
||||
try_init_rustls_platform_verifier(&mut env, context_jobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod ffi;
|
||||
|
||||
pub use ffi::*;
|
||||
@@ -0,0 +1,10 @@
|
||||
#include <aom/aom.h>
|
||||
#include <aom/aom_image.h>
|
||||
#include <aom/aom_integer.h>
|
||||
#include <aom/aom_codec.h>
|
||||
#include <aom/aom_external_partition.h>
|
||||
#include <aom/aom_frame_buffer.h>
|
||||
#include <aom/aom_encoder.h>
|
||||
#include <aom/aom_decoder.h>
|
||||
#include <aom/aomcx.h>
|
||||
#include <aom/aomdx.h>
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <vpx/vp8.h>
|
||||
#include <vpx/vp8cx.h>
|
||||
#include <vpx/vp8dx.h>
|
||||
#include <vpx/vpx_codec.h>
|
||||
#include <vpx/vpx_decoder.h>
|
||||
#include <vpx/vpx_encoder.h>
|
||||
#include <vpx/vpx_frame_buffer.h>
|
||||
#include <vpx/vpx_image.h>
|
||||
#include <vpx/vpx_integer.h>
|
||||
@@ -0,0 +1,6 @@
|
||||
#include <libyuv/convert.h>
|
||||
#include <libyuv/convert_argb.h>
|
||||
#include <libyuv/convert_from.h>
|
||||
#include <libyuv/convert_from_argb.h>
|
||||
#include <libyuv/rotate.h>
|
||||
#include <libyuv/rotate_argb.h>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
use std::mem::size_of;
|
||||
use winapi::{
|
||||
shared::windef::{HBITMAP, HDC},
|
||||
um::wingdi::{
|
||||
BitBlt,
|
||||
CreateCompatibleBitmap,
|
||||
CreateCompatibleDC,
|
||||
CreateDCW,
|
||||
DeleteDC,
|
||||
DeleteObject,
|
||||
GetDIBits,
|
||||
SelectObject,
|
||||
BITMAPINFO,
|
||||
BITMAPINFOHEADER,
|
||||
BI_RGB,
|
||||
CAPTUREBLT,
|
||||
DIB_RGB_COLORS, //CAPTUREBLT,
|
||||
HGDI_ERROR,
|
||||
RGBQUAD,
|
||||
SRCCOPY,
|
||||
},
|
||||
};
|
||||
|
||||
const PIXEL_WIDTH: i32 = 4;
|
||||
|
||||
pub struct CapturerGDI {
|
||||
screen_dc: HDC,
|
||||
dc: HDC,
|
||||
bmp: HBITMAP,
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
impl CapturerGDI {
|
||||
pub fn new(name: &[u16], width: i32, height: i32) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
/* or Enumerate monitors with EnumDisplayMonitors,
|
||||
https://stackoverflow.com/questions/34987695/how-can-i-get-an-hmonitor-handle-from-a-display-device-name
|
||||
#[no_mangle]
|
||||
pub extern "C" fn callback(m: HMONITOR, dc: HDC, rect: LPRECT, lp: LPARAM) -> BOOL {}
|
||||
*/
|
||||
/*
|
||||
shared::windef::HMONITOR,
|
||||
winuser::{GetMonitorInfoW, GetSystemMetrics, MONITORINFOEXW},
|
||||
let mut mi: MONITORINFOEXW = std::mem::MaybeUninit::uninit().assume_init();
|
||||
mi.cbSize = size_of::<MONITORINFOEXW>() as _;
|
||||
if GetMonitorInfoW(m, &mut mi as *mut MONITORINFOEXW as _) == 0 {
|
||||
return Err(format!("Failed to get monitor information of: {:?}", m).into());
|
||||
}
|
||||
*/
|
||||
unsafe {
|
||||
if name.is_empty() {
|
||||
return Err("Empty display name".into());
|
||||
}
|
||||
let screen_dc = CreateDCW(&name[0], 0 as _, 0 as _, 0 as _);
|
||||
if screen_dc.is_null() {
|
||||
return Err("Failed to create dc from monitor name".into());
|
||||
}
|
||||
|
||||
// Create a Windows Bitmap, and copy the bits into it
|
||||
let dc = CreateCompatibleDC(screen_dc);
|
||||
if dc.is_null() {
|
||||
DeleteDC(screen_dc);
|
||||
return Err("Can't get a Windows display".into());
|
||||
}
|
||||
|
||||
let bmp = CreateCompatibleBitmap(screen_dc, width, height);
|
||||
if bmp.is_null() {
|
||||
DeleteDC(screen_dc);
|
||||
DeleteDC(dc);
|
||||
return Err("Can't create a Windows buffer".into());
|
||||
}
|
||||
|
||||
let res = SelectObject(dc, bmp as _);
|
||||
if res.is_null() || res == HGDI_ERROR {
|
||||
DeleteDC(screen_dc);
|
||||
DeleteDC(dc);
|
||||
DeleteObject(bmp as _);
|
||||
return Err("Can't select Windows buffer".into());
|
||||
}
|
||||
Ok(Self {
|
||||
screen_dc,
|
||||
dc,
|
||||
bmp,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame(&self, data: &mut Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
unsafe {
|
||||
let res = BitBlt(
|
||||
self.dc,
|
||||
0,
|
||||
0,
|
||||
self.width,
|
||||
self.height,
|
||||
self.screen_dc,
|
||||
0,
|
||||
0,
|
||||
SRCCOPY | CAPTUREBLT, // CAPTUREBLT enable layered window but also make cursor blinking
|
||||
);
|
||||
if res == 0 {
|
||||
return Err("Failed to copy screen to Windows buffer".into());
|
||||
}
|
||||
|
||||
let stride = self.width * PIXEL_WIDTH;
|
||||
let size: usize = (stride * self.height) as usize;
|
||||
let mut data1: Vec<u8> = Vec::with_capacity(size);
|
||||
data1.set_len(size);
|
||||
data.resize(size, 0);
|
||||
|
||||
let mut bmi = BITMAPINFO {
|
||||
bmiHeader: BITMAPINFOHEADER {
|
||||
biSize: size_of::<BITMAPINFOHEADER>() as _,
|
||||
biWidth: self.width as _,
|
||||
biHeight: self.height as _,
|
||||
biPlanes: 1,
|
||||
biBitCount: (8 * PIXEL_WIDTH) as _,
|
||||
biCompression: BI_RGB,
|
||||
biSizeImage: (self.width * self.height * PIXEL_WIDTH) as _,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
biClrImportant: 0,
|
||||
},
|
||||
bmiColors: [RGBQUAD {
|
||||
rgbBlue: 0,
|
||||
rgbGreen: 0,
|
||||
rgbRed: 0,
|
||||
rgbReserved: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
// copy bits into Vec
|
||||
let res = GetDIBits(
|
||||
self.dc,
|
||||
self.bmp,
|
||||
0,
|
||||
self.height as _,
|
||||
&mut data[0] as *mut u8 as _,
|
||||
&mut bmi as _,
|
||||
DIB_RGB_COLORS,
|
||||
);
|
||||
if res == 0 {
|
||||
return Err("GetDIBits failed".into());
|
||||
}
|
||||
crate::common::ARGBMirror(
|
||||
data.as_ptr(),
|
||||
stride,
|
||||
data1.as_mut_ptr(),
|
||||
stride,
|
||||
self.width,
|
||||
self.height,
|
||||
);
|
||||
crate::common::ARGBRotate(
|
||||
data1.as_ptr(),
|
||||
stride,
|
||||
data.as_mut_ptr(),
|
||||
stride,
|
||||
self.width,
|
||||
self.height,
|
||||
crate::RotationMode::kRotate180,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CapturerGDI {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
DeleteDC(self.screen_dc);
|
||||
DeleteDC(self.dc);
|
||||
DeleteObject(self.bmp as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::*;
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test() {
|
||||
match Displays::new().unwrap().next() {
|
||||
Some(d) => {
|
||||
let w = d.width();
|
||||
let h = d.height();
|
||||
let c = CapturerGDI::new(d.name(), w, h).unwrap();
|
||||
let mut data = Vec::new();
|
||||
c.frame(&mut data).unwrap();
|
||||
let mut bitflipped = Vec::with_capacity((w * h * 4) as usize);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = (w * 4 * y + 4 * x) as usize;
|
||||
bitflipped.extend_from_slice(&[data[i + 2], data[i + 1], data[i], 255]);
|
||||
}
|
||||
}
|
||||
repng::encode(
|
||||
std::fs::File::create("gdi_screen.png").unwrap(),
|
||||
d.width() as u32,
|
||||
d.height() as u32,
|
||||
&bitflipped,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
_ => {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
// logic from webrtc -- https://github.com/shiguredo/libwebrtc/blob/main/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use lazy_static;
|
||||
use std::{
|
||||
ffi::CString,
|
||||
io::{Error, ErrorKind, Result},
|
||||
mem::size_of,
|
||||
sync::Mutex,
|
||||
};
|
||||
use winapi::{
|
||||
shared::{
|
||||
basetsd::SIZE_T,
|
||||
guiddef::{IsEqualGUID, GUID},
|
||||
minwindef::{BOOL, DWORD, FALSE, FARPROC, HINSTANCE, HMODULE, HRGN, TRUE, UINT},
|
||||
ntdef::{LONG, NULL},
|
||||
windef::{HWND, RECT},
|
||||
winerror::ERROR_CLASS_ALREADY_EXISTS,
|
||||
},
|
||||
um::{
|
||||
errhandlingapi::GetLastError,
|
||||
libloaderapi::{FreeLibrary, GetModuleHandleExA, GetProcAddress, LoadLibraryExA},
|
||||
winuser::*,
|
||||
},
|
||||
};
|
||||
|
||||
pub const MW_FILTERMODE_EXCLUDE: u32 = 0;
|
||||
pub const MW_FILTERMODE_INCLUDE: u32 = 1;
|
||||
pub const GET_MODULE_HANDLE_EX_FLAG_PIN: u32 = 1;
|
||||
pub const GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT: u32 = 2;
|
||||
pub const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 4;
|
||||
pub const LOAD_LIBRARY_AS_DATAFILE: u32 = 2;
|
||||
pub const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 8;
|
||||
pub const LOAD_IGNORE_CODE_AUTHZ_LEVEL: u32 = 16;
|
||||
pub const LOAD_LIBRARY_AS_IMAGE_RESOURCE: u32 = 32;
|
||||
pub const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: u32 = 64;
|
||||
pub const LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: u32 = 128;
|
||||
pub const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: u32 = 256;
|
||||
pub const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: u32 = 512;
|
||||
pub const LOAD_LIBRARY_SEARCH_USER_DIRS: u32 = 1024;
|
||||
pub const LOAD_LIBRARY_SEARCH_SYSTEM32: u32 = 2048;
|
||||
pub const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: u32 = 4096;
|
||||
pub const LOAD_LIBRARY_SAFE_CURRENT_DIRS: u32 = 8192;
|
||||
pub const LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER: u32 = 16384;
|
||||
pub const LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY: u32 = 32768;
|
||||
|
||||
extern "C" {
|
||||
pub static GUID_WICPixelFormat32bppRGBA: GUID;
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
|
||||
}
|
||||
|
||||
pub type REFWICPixelFormatGUID = *const GUID;
|
||||
pub type WICPixelFormatGUID = GUID;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct tagMAGIMAGEHEADER {
|
||||
pub width: UINT,
|
||||
pub height: UINT,
|
||||
pub format: WICPixelFormatGUID,
|
||||
pub stride: UINT,
|
||||
pub offset: UINT,
|
||||
pub cbSize: SIZE_T,
|
||||
}
|
||||
pub type MAGIMAGEHEADER = tagMAGIMAGEHEADER;
|
||||
pub type PMAGIMAGEHEADER = *mut tagMAGIMAGEHEADER;
|
||||
|
||||
// Function types
|
||||
pub type MagImageScalingCallback = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
hwnd: HWND,
|
||||
srcdata: *mut ::std::os::raw::c_void,
|
||||
srcheader: MAGIMAGEHEADER,
|
||||
destdata: *mut ::std::os::raw::c_void,
|
||||
destheader: MAGIMAGEHEADER,
|
||||
unclipped: RECT,
|
||||
clipped: RECT,
|
||||
dirty: HRGN,
|
||||
) -> BOOL,
|
||||
>;
|
||||
|
||||
extern "C" {
|
||||
pub fn MagShowSystemCursor(fShowCursor: BOOL) -> BOOL;
|
||||
}
|
||||
pub type MagInitializeFunc = ::std::option::Option<unsafe extern "C" fn() -> BOOL>;
|
||||
pub type MagUninitializeFunc = ::std::option::Option<unsafe extern "C" fn() -> BOOL>;
|
||||
pub type MagSetWindowSourceFunc =
|
||||
::std::option::Option<unsafe extern "C" fn(hwnd: HWND, rect: RECT) -> BOOL>;
|
||||
pub type MagSetWindowFilterListFunc = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
hwnd: HWND,
|
||||
dwFilterMode: DWORD,
|
||||
count: ::std::os::raw::c_int,
|
||||
pHWND: *mut HWND,
|
||||
) -> BOOL,
|
||||
>;
|
||||
pub type MagSetImageScalingCallbackFunc = ::std::option::Option<
|
||||
unsafe extern "C" fn(hwnd: HWND, callback: MagImageScalingCallback) -> BOOL,
|
||||
>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct MagInterface {
|
||||
init_succeeded: bool,
|
||||
lib_handle: HINSTANCE,
|
||||
pub mag_initialize_func: MagInitializeFunc,
|
||||
pub mag_uninitialize_func: MagUninitializeFunc,
|
||||
pub set_window_source_func: MagSetWindowSourceFunc,
|
||||
pub set_window_filter_list_func: MagSetWindowFilterListFunc,
|
||||
pub set_image_scaling_callback_func: MagSetImageScalingCallbackFunc,
|
||||
}
|
||||
|
||||
// NOTE: MagInitialize and MagUninitialize should not be called in global init and uninit.
|
||||
// If so, strange errors occur.
|
||||
impl MagInterface {
|
||||
fn new() -> Result<Self> {
|
||||
let mut s = MagInterface {
|
||||
init_succeeded: false,
|
||||
lib_handle: NULL as _,
|
||||
mag_initialize_func: None,
|
||||
mag_uninitialize_func: None,
|
||||
set_window_source_func: None,
|
||||
set_window_filter_list_func: None,
|
||||
set_image_scaling_callback_func: None,
|
||||
};
|
||||
s.init_succeeded = false;
|
||||
unsafe {
|
||||
// load lib
|
||||
let lib_file_name = "Magnification.dll";
|
||||
let lib_file_name_c = CString::new(lib_file_name)?;
|
||||
s.lib_handle = LoadLibraryExA(
|
||||
lib_file_name_c.as_ptr() as _,
|
||||
NULL,
|
||||
LOAD_LIBRARY_SEARCH_SYSTEM32,
|
||||
);
|
||||
if s.lib_handle.is_null() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed to LoadLibraryExA {}, error {}",
|
||||
lib_file_name,
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
};
|
||||
|
||||
// load functions
|
||||
s.mag_initialize_func = Some(std::mem::transmute(Self::load_func(
|
||||
s.lib_handle,
|
||||
"MagInitialize",
|
||||
)?));
|
||||
s.mag_uninitialize_func = Some(std::mem::transmute(Self::load_func(
|
||||
s.lib_handle,
|
||||
"MagUninitialize",
|
||||
)?));
|
||||
s.set_window_source_func = Some(std::mem::transmute(Self::load_func(
|
||||
s.lib_handle,
|
||||
"MagSetWindowSource",
|
||||
)?));
|
||||
s.set_window_filter_list_func = Some(std::mem::transmute(Self::load_func(
|
||||
s.lib_handle,
|
||||
"MagSetWindowFilterList",
|
||||
)?));
|
||||
s.set_image_scaling_callback_func = Some(std::mem::transmute(Self::load_func(
|
||||
s.lib_handle,
|
||||
"MagSetImageScalingCallback",
|
||||
)?));
|
||||
|
||||
// MagInitialize
|
||||
if let Some(init_func) = s.mag_initialize_func {
|
||||
if FALSE == init_func() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Failed to MagInitialize, error {}", Error::last_os_error()),
|
||||
));
|
||||
} else {
|
||||
s.init_succeeded = true;
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Unreachable, mag_initialize_func should not be none",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
unsafe fn load_func(lib_module: HMODULE, func_name: &str) -> Result<FARPROC> {
|
||||
let func_name_c = CString::new(func_name)?;
|
||||
let func = GetProcAddress(lib_module, func_name_c.as_ptr() as _);
|
||||
if func.is_null() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed to GetProcAddress {}, error {}",
|
||||
func_name,
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(func)
|
||||
}
|
||||
|
||||
pub(super) fn uninit(&mut self) {
|
||||
if self.init_succeeded {
|
||||
if let Some(uninit_func) = self.mag_uninitialize_func {
|
||||
unsafe {
|
||||
if FALSE == uninit_func() {
|
||||
println!("Failed MagUninitialize, error {}", Error::last_os_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.lib_handle.is_null() {
|
||||
unsafe {
|
||||
if FALSE == FreeLibrary(self.lib_handle) {
|
||||
println!("Failed FreeLibrary, error {}", Error::last_os_error())
|
||||
}
|
||||
}
|
||||
self.lib_handle = NULL as _;
|
||||
}
|
||||
}
|
||||
self.init_succeeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MagInterface {
|
||||
fn drop(&mut self) {
|
||||
self.uninit();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CapturerMag {
|
||||
mag_interface: MagInterface,
|
||||
host_window: HWND,
|
||||
magnifier_window: HWND,
|
||||
|
||||
magnifier_host_class: CString,
|
||||
host_window_name: CString,
|
||||
magnifier_window_class: CString,
|
||||
magnifier_window_name: CString,
|
||||
|
||||
rect: RECT,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl Drop for CapturerMag {
|
||||
fn drop(&mut self) {
|
||||
self.destroy_windows();
|
||||
self.mag_interface.uninit();
|
||||
}
|
||||
}
|
||||
|
||||
impl CapturerMag {
|
||||
pub(crate) fn is_supported() -> bool {
|
||||
MagInterface::new().is_ok()
|
||||
}
|
||||
|
||||
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
|
||||
unsafe {
|
||||
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
if !(origin.0 >= x as i32
|
||||
&& origin.1 >= y as i32
|
||||
&& width <= w as usize
|
||||
&& height <= h as usize)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed Check screen rect ({}, {}, {} , {}) to ({}, {}, {}, {})",
|
||||
origin.0,
|
||||
origin.1,
|
||||
origin.0 + width as i32,
|
||||
origin.1 + height as i32,
|
||||
x,
|
||||
y,
|
||||
x + w,
|
||||
y + h
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut s = Self {
|
||||
mag_interface: MagInterface::new()?,
|
||||
host_window: 0 as _,
|
||||
magnifier_window: 0 as _,
|
||||
magnifier_host_class: CString::new("ScreenCapturerWinMagnifierHost")?,
|
||||
host_window_name: CString::new("MagnifierHost")?,
|
||||
magnifier_window_class: CString::new("Magnifier")?,
|
||||
magnifier_window_name: CString::new("MagnifierWindow")?,
|
||||
rect: RECT {
|
||||
left: origin.0 as _,
|
||||
top: origin.1 as _,
|
||||
right: origin.0 + width as LONG,
|
||||
bottom: origin.1 + height as LONG,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let mut instance = 0 as HMODULE;
|
||||
if 0 == GetModuleHandleExA(
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
|
||||
| GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
DefWindowProcA as _,
|
||||
&mut instance as _,
|
||||
) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed to GetModuleHandleExA, error {}",
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Register the host window class. See the MSDN documentation of the
|
||||
// Magnification API for more information.
|
||||
let wcex = WNDCLASSEXA {
|
||||
cbSize: size_of::<WNDCLASSEXA>() as _,
|
||||
style: 0,
|
||||
lpfnWndProc: Some(DefWindowProcA),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
hInstance: instance,
|
||||
hIcon: 0 as _,
|
||||
hCursor: LoadCursorA(NULL as _, IDC_ARROW as _),
|
||||
hbrBackground: 0 as _,
|
||||
lpszClassName: s.magnifier_host_class.as_ptr() as _,
|
||||
lpszMenuName: 0 as _,
|
||||
hIconSm: 0 as _,
|
||||
};
|
||||
|
||||
// Ignore the error which may happen when the class is already registered.
|
||||
if 0 == RegisterClassExA(&wcex) {
|
||||
let code = GetLastError();
|
||||
if code != ERROR_CLASS_ALREADY_EXISTS {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed to RegisterClassExA, error {}",
|
||||
Error::from_raw_os_error(code as _)
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Create the host window.
|
||||
s.host_window = CreateWindowExA(
|
||||
WS_EX_LAYERED,
|
||||
s.magnifier_host_class.as_ptr(),
|
||||
s.host_window_name.as_ptr(),
|
||||
WS_POPUP,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL as _,
|
||||
NULL as _,
|
||||
instance,
|
||||
NULL,
|
||||
);
|
||||
if s.host_window.is_null() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed to CreateWindowExA host_window, error {}",
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Create the magnifier control.
|
||||
s.magnifier_window = CreateWindowExA(
|
||||
0,
|
||||
s.magnifier_window_class.as_ptr(),
|
||||
s.magnifier_window_name.as_ptr(),
|
||||
WS_CHILD | WS_VISIBLE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
s.host_window,
|
||||
NULL as _,
|
||||
instance,
|
||||
NULL,
|
||||
);
|
||||
if s.magnifier_window.is_null() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed CreateWindowA magnifier_window, error {}",
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Hide the host window.
|
||||
let _ = ShowWindow(s.host_window, SW_HIDE);
|
||||
|
||||
// Set the scaling callback to receive captured image.
|
||||
if let Some(set_callback_func) = s.mag_interface.set_image_scaling_callback_func {
|
||||
if FALSE
|
||||
== set_callback_func(
|
||||
s.magnifier_window,
|
||||
Some(Self::on_gag_image_scaling_callback),
|
||||
)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed to MagSetImageScalingCallback, error {}",
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Unreachable, set_image_scaling_callback_func should not be none",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
||||
let name_c = CString::new(name)?;
|
||||
unsafe {
|
||||
let mut hwnd = if cls.len() == 0 {
|
||||
FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr())
|
||||
} else {
|
||||
let cls_c = CString::new(cls).unwrap();
|
||||
FindWindowExA(NULL as _, NULL as _, cls_c.as_ptr(), name_c.as_ptr())
|
||||
};
|
||||
|
||||
if hwnd.is_null() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(set_window_filter_list_func) =
|
||||
self.mag_interface.set_window_filter_list_func
|
||||
{
|
||||
if FALSE
|
||||
== set_window_filter_list_func(
|
||||
self.magnifier_window,
|
||||
MW_FILTERMODE_EXCLUDE,
|
||||
1,
|
||||
&mut hwnd,
|
||||
)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed MagSetWindowFilterList for cls {} name {}, error {}",
|
||||
cls,
|
||||
name,
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Unreachable, MagSetWindowFilterList should not be none",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn get_rect(&self) -> ((i32, i32), usize, usize) {
|
||||
(
|
||||
(self.rect.left as _, self.rect.top as _),
|
||||
self.width as _,
|
||||
self.height as _,
|
||||
)
|
||||
}
|
||||
|
||||
fn clear_data() {
|
||||
let mut lock = MAG_BUFFER.lock().unwrap();
|
||||
lock.0 = false;
|
||||
lock.1.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn frame(&mut self, data: &mut Vec<u8>) -> Result<()> {
|
||||
Self::clear_data();
|
||||
|
||||
unsafe {
|
||||
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
if !(self.rect.left >= x as i32
|
||||
&& self.rect.top >= y as i32
|
||||
&& self.rect.right <= (x + w) as i32
|
||||
&& self.rect.bottom <= (y + h) as i32)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed Check screen rect ({}, {}, {} , {}) to ({}, {}, {}, {})",
|
||||
self.rect.left,
|
||||
self.rect.top,
|
||||
self.rect.right,
|
||||
self.rect.bottom,
|
||||
x,
|
||||
y,
|
||||
x + w,
|
||||
y + h
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if FALSE
|
||||
== SetWindowPos(
|
||||
self.magnifier_window,
|
||||
HWND_TOP,
|
||||
self.rect.left,
|
||||
self.rect.top,
|
||||
self.rect.right - self.rect.left,
|
||||
self.rect.bottom - self.rect.top,
|
||||
0,
|
||||
)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed SetWindowPos (x, y, w , h) - ({}, {}, {}, {}), error {}",
|
||||
self.rect.left,
|
||||
self.rect.top,
|
||||
self.rect.right - self.rect.left,
|
||||
self.rect.bottom - self.rect.top,
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// on_gag_image_scaling_callback will be called and fill in the
|
||||
// frame before set_window_source_func_ returns.
|
||||
if let Some(set_window_source_func) = self.mag_interface.set_window_source_func {
|
||||
if FALSE == set_window_source_func(self.magnifier_window, self.rect) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Failed to MagSetWindowSource, error {}",
|
||||
Error::last_os_error()
|
||||
),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Unreachable, set_window_source_func should not be none",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut lock = MAG_BUFFER.lock().unwrap();
|
||||
if !lock.0 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"No data captured by magnifier",
|
||||
));
|
||||
}
|
||||
|
||||
data.resize(lock.1.len(), 0);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(&mut lock.1[0], &mut data[0], data.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy_windows(&mut self) {
|
||||
if !self.magnifier_window.is_null() {
|
||||
unsafe {
|
||||
if FALSE == DestroyWindow(self.magnifier_window) {
|
||||
//
|
||||
println!(
|
||||
"Failed DestroyWindow magnifier window, error {}",
|
||||
Error::last_os_error()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.magnifier_window = NULL as _;
|
||||
|
||||
if !self.host_window.is_null() {
|
||||
unsafe {
|
||||
if FALSE == DestroyWindow(self.host_window) {
|
||||
//
|
||||
println!(
|
||||
"Failed DestroyWindow host window, error {}",
|
||||
Error::last_os_error()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.host_window = NULL as _;
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_gag_image_scaling_callback(
|
||||
_hwnd: HWND,
|
||||
srcdata: *mut ::std::os::raw::c_void,
|
||||
srcheader: MAGIMAGEHEADER,
|
||||
_destdata: *mut ::std::os::raw::c_void,
|
||||
_destheader: MAGIMAGEHEADER,
|
||||
_unclipped: RECT,
|
||||
_clipped: RECT,
|
||||
_dirty: HRGN,
|
||||
) -> BOOL {
|
||||
Self::clear_data();
|
||||
|
||||
if !IsEqualGUID(&srcheader.format, &GUID_WICPixelFormat32bppRGBA) {
|
||||
// log warning?
|
||||
return FALSE;
|
||||
}
|
||||
let mut lock = MAG_BUFFER.lock().unwrap();
|
||||
lock.1.resize(srcheader.cbSize, 0);
|
||||
std::ptr::copy_nonoverlapping(srcdata as _, &mut lock.1[0], srcheader.cbSize);
|
||||
lock.0 = true;
|
||||
TRUE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test() {
|
||||
let mut capture_mag = CapturerMag::new((0, 0), 1920, 1080).unwrap();
|
||||
capture_mag.exclude("", "RustDeskPrivacyWindow").unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000 * 10));
|
||||
let mut data = Vec::new();
|
||||
capture_mag.frame(&mut data).unwrap();
|
||||
println!("capture data len: {}", data.len());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
use std::{io, mem, ptr, slice};
|
||||
pub mod gdi;
|
||||
pub use gdi::CapturerGDI;
|
||||
pub mod mag;
|
||||
|
||||
use winapi::{
|
||||
shared::{
|
||||
dxgi::*,
|
||||
dxgi1_2::*,
|
||||
dxgitype::*,
|
||||
minwindef::{DWORD, FALSE, TRUE, UINT},
|
||||
ntdef::LONG,
|
||||
windef::{HMONITOR, RECT},
|
||||
winerror::*,
|
||||
// dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE},
|
||||
},
|
||||
um::{
|
||||
d3d11::*, d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, unknwnbase::IUnknown, wingdi::*,
|
||||
winnt::HRESULT, winuser::*,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::RotationMode::*;
|
||||
|
||||
use crate::{AdapterDevice, Frame, PixelBuffer};
|
||||
use std::ffi::c_void;
|
||||
|
||||
pub struct ComPtr<T>(*mut T);
|
||||
impl<T> ComPtr<T> {
|
||||
fn is_null(&self) -> bool {
|
||||
self.0.is_null()
|
||||
}
|
||||
}
|
||||
impl<T> Drop for ComPtr<T> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.is_null() {
|
||||
(*(self.0 as *mut IUnknown)).Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Capturer {
|
||||
device: ComPtr<ID3D11Device>,
|
||||
display: Display,
|
||||
context: ComPtr<ID3D11DeviceContext>,
|
||||
duplication: ComPtr<IDXGIOutputDuplication>,
|
||||
fastlane: bool,
|
||||
surface: ComPtr<IDXGISurface>,
|
||||
texture: ComPtr<ID3D11Texture2D>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
rotated: Vec<u8>,
|
||||
gdi_capturer: Option<CapturerGDI>,
|
||||
gdi_buffer: Vec<u8>,
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
output_texture: bool,
|
||||
adapter_desc1: DXGI_ADAPTER_DESC1,
|
||||
rotate: Rotate,
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let mut device = ptr::null_mut();
|
||||
let mut context = ptr::null_mut();
|
||||
let mut duplication = ptr::null_mut();
|
||||
#[allow(invalid_value)]
|
||||
let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() };
|
||||
#[allow(invalid_value)]
|
||||
let mut adapter_desc1 = unsafe { mem::MaybeUninit::uninit().assume_init() };
|
||||
let mut gdi_capturer = None;
|
||||
|
||||
let mut res = if display.gdi {
|
||||
wrap_hresult(1)
|
||||
} else {
|
||||
let res = wrap_hresult(unsafe {
|
||||
D3D11CreateDevice(
|
||||
display.adapter.0 as *mut _,
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
ptr::null_mut(), // No software rasterizer.
|
||||
0, // No device flags.
|
||||
ptr::null_mut(), // Feature levels.
|
||||
0, // Feature levels' length.
|
||||
D3D11_SDK_VERSION,
|
||||
&mut device,
|
||||
ptr::null_mut(),
|
||||
&mut context,
|
||||
)
|
||||
});
|
||||
if res.is_ok() {
|
||||
wrap_hresult(unsafe { (*display.adapter.0).GetDesc1(&mut adapter_desc1) })
|
||||
} else {
|
||||
res
|
||||
}
|
||||
};
|
||||
let device = ComPtr(device);
|
||||
let context = ComPtr(context);
|
||||
|
||||
if res.is_err() {
|
||||
gdi_capturer = display.create_gdi();
|
||||
println!("Fallback to GDI");
|
||||
if gdi_capturer.is_some() {
|
||||
res = Ok(());
|
||||
}
|
||||
} else {
|
||||
res = wrap_hresult(unsafe {
|
||||
let hres = (*display.inner.0).DuplicateOutput(device.0 as *mut _, &mut duplication);
|
||||
if hres != S_OK {
|
||||
gdi_capturer = display.create_gdi();
|
||||
println!("Fallback to GDI");
|
||||
if gdi_capturer.is_some() {
|
||||
S_OK
|
||||
} else {
|
||||
hres
|
||||
}
|
||||
} else {
|
||||
hres
|
||||
}
|
||||
|
||||
// NVFBC(NVIDIA Capture SDK) which xpra used already deprecated, https://developer.nvidia.com/capture-sdk
|
||||
|
||||
// also try high version DXGI for better performance, e.g.
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/dxgi-1-2-improvements
|
||||
// dxgi-1-6 may too high, only support win10 (2018)
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format
|
||||
// DXGI_FORMAT_420_OPAQUE
|
||||
// IDXGIOutputDuplication::GetFrameDirtyRects and IDXGIOutputDuplication::GetFrameMoveRects
|
||||
// can help us update screen incrementally
|
||||
|
||||
/* // not supported on my PC, try in the future
|
||||
use winapi::shared::dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
|
||||
let format : Vec<DXGI_FORMAT> = vec![DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE];
|
||||
(*display.inner).DuplicateOutput1(
|
||||
device as *mut _,
|
||||
0 as UINT,
|
||||
2 as UINT,
|
||||
format.as_ptr(),
|
||||
&mut duplication
|
||||
)
|
||||
*/
|
||||
|
||||
// if above not work, I think below should not work either, try later
|
||||
// https://developer.nvidia.com/capture-sdk deprecated
|
||||
// examples using directx + nvideo sdk for GPU-accelerated video encoding/decoding
|
||||
// https://github.com/NVIDIA/video-sdk-samples
|
||||
});
|
||||
}
|
||||
|
||||
res?;
|
||||
|
||||
if !duplication.is_null() {
|
||||
unsafe {
|
||||
(*duplication).GetDesc(&mut desc);
|
||||
}
|
||||
}
|
||||
let rotate = Self::create_rotations(device.0, context.0, &display);
|
||||
|
||||
Ok(Capturer {
|
||||
device,
|
||||
context,
|
||||
duplication: ComPtr(duplication),
|
||||
fastlane: desc.DesktopImageInSystemMemory == TRUE,
|
||||
surface: ComPtr(ptr::null_mut()),
|
||||
texture: ComPtr(ptr::null_mut()),
|
||||
width: display.width() as usize,
|
||||
height: display.height() as usize,
|
||||
display,
|
||||
rotated: Vec::new(),
|
||||
gdi_capturer,
|
||||
gdi_buffer: Vec::new(),
|
||||
saved_raw_data: Vec::new(),
|
||||
output_texture: false,
|
||||
adapter_desc1,
|
||||
rotate,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_rotations(
|
||||
device: *mut ID3D11Device,
|
||||
context: *mut ID3D11DeviceContext,
|
||||
display: &Display,
|
||||
) -> Rotate {
|
||||
let mut video_context: *mut ID3D11VideoContext = ptr::null_mut();
|
||||
let mut video_device: *mut ID3D11VideoDevice = ptr::null_mut();
|
||||
let mut video_processor_enum: *mut ID3D11VideoProcessorEnumerator = ptr::null_mut();
|
||||
let mut video_processor: *mut ID3D11VideoProcessor = ptr::null_mut();
|
||||
let processor_rotation = match display.rotation() {
|
||||
DXGI_MODE_ROTATION_ROTATE90 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_90),
|
||||
DXGI_MODE_ROTATION_ROTATE180 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_180),
|
||||
DXGI_MODE_ROTATION_ROTATE270 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_270),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(processor_rotation) = processor_rotation {
|
||||
println!("create rotations");
|
||||
if !device.is_null() && !context.is_null() {
|
||||
unsafe {
|
||||
(*context).QueryInterface(
|
||||
&IID_ID3D11VideoContext,
|
||||
&mut video_context as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
if !video_context.is_null() {
|
||||
(*device).QueryInterface(
|
||||
&IID_ID3D11VideoDevice,
|
||||
&mut video_device as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
if !video_device.is_null() {
|
||||
let (input_width, input_height) = match display.rotation() {
|
||||
DXGI_MODE_ROTATION_ROTATE90 | DXGI_MODE_ROTATION_ROTATE270 => {
|
||||
(display.height(), display.width())
|
||||
}
|
||||
_ => (display.width(), display.height()),
|
||||
};
|
||||
let (output_width, output_height) = (display.width(), display.height());
|
||||
let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
|
||||
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
|
||||
InputFrameRate: DXGI_RATIONAL {
|
||||
Numerator: 30,
|
||||
Denominator: 1,
|
||||
},
|
||||
InputWidth: input_width as _,
|
||||
InputHeight: input_height as _,
|
||||
OutputFrameRate: DXGI_RATIONAL {
|
||||
Numerator: 30,
|
||||
Denominator: 1,
|
||||
},
|
||||
OutputWidth: output_width as _,
|
||||
OutputHeight: output_height as _,
|
||||
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
|
||||
};
|
||||
(*video_device).CreateVideoProcessorEnumerator(
|
||||
&content_desc,
|
||||
&mut video_processor_enum,
|
||||
);
|
||||
if !video_processor_enum.is_null() {
|
||||
let mut caps: D3D11_VIDEO_PROCESSOR_CAPS = mem::zeroed();
|
||||
if S_OK == (*video_processor_enum).GetVideoProcessorCaps(&mut caps)
|
||||
{
|
||||
if caps.FeatureCaps
|
||||
& D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_ROTATION
|
||||
!= 0
|
||||
{
|
||||
(*video_device).CreateVideoProcessor(
|
||||
video_processor_enum,
|
||||
0,
|
||||
&mut video_processor,
|
||||
);
|
||||
if !video_processor.is_null() {
|
||||
(*video_context).VideoProcessorSetStreamRotation(
|
||||
video_processor,
|
||||
0,
|
||||
TRUE,
|
||||
processor_rotation,
|
||||
);
|
||||
(*video_context)
|
||||
.VideoProcessorSetStreamAutoProcessingMode(
|
||||
video_processor,
|
||||
0,
|
||||
FALSE,
|
||||
);
|
||||
(*video_context).VideoProcessorSetStreamFrameFormat(
|
||||
video_processor,
|
||||
0,
|
||||
D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
|
||||
);
|
||||
(*video_context).VideoProcessorSetStreamSourceRect(
|
||||
video_processor,
|
||||
0,
|
||||
TRUE,
|
||||
&RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: input_width as _,
|
||||
bottom: input_height as _,
|
||||
},
|
||||
);
|
||||
(*video_context).VideoProcessorSetStreamDestRect(
|
||||
video_processor,
|
||||
0,
|
||||
TRUE,
|
||||
&RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: output_width as _,
|
||||
bottom: output_height as _,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let video_context = ComPtr(video_context);
|
||||
let video_device = ComPtr(video_device);
|
||||
let video_processor_enum = ComPtr(video_processor_enum);
|
||||
let video_processor = ComPtr(video_processor);
|
||||
let rotated_texture = ComPtr(ptr::null_mut());
|
||||
Rotate {
|
||||
video_context,
|
||||
video_device,
|
||||
video_processor_enum,
|
||||
video_processor,
|
||||
texture: (rotated_texture, false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_gdi(&self) -> bool {
|
||||
self.gdi_capturer.is_some()
|
||||
}
|
||||
|
||||
pub fn set_gdi(&mut self) -> bool {
|
||||
self.gdi_capturer = self.display.create_gdi();
|
||||
self.is_gdi()
|
||||
}
|
||||
|
||||
pub fn cancel_gdi(&mut self) {
|
||||
self.gdi_buffer = Vec::new();
|
||||
self.gdi_capturer.take();
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
pub fn set_output_texture(&mut self, texture: bool) {
|
||||
self.output_texture = texture;
|
||||
}
|
||||
|
||||
unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> {
|
||||
let mut frame = ptr::null_mut();
|
||||
#[allow(invalid_value)]
|
||||
let mut info = mem::MaybeUninit::uninit().assume_init();
|
||||
|
||||
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
||||
let frame = ComPtr(frame);
|
||||
|
||||
if *info.LastPresentTime.QuadPart() == 0 {
|
||||
return Err(std::io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
|
||||
#[allow(invalid_value)]
|
||||
let mut rect = mem::MaybeUninit::uninit().assume_init();
|
||||
if self.fastlane {
|
||||
wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?;
|
||||
} else {
|
||||
self.surface = ComPtr(self.ohgodwhat(frame.0)?);
|
||||
wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?;
|
||||
}
|
||||
Ok((rect.pBits, rect.Pitch))
|
||||
}
|
||||
|
||||
// copy from GPU memory to system memory
|
||||
unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<*mut IDXGISurface> {
|
||||
let mut texture: *mut ID3D11Texture2D = ptr::null_mut();
|
||||
(*frame).QueryInterface(
|
||||
&IID_ID3D11Texture2D,
|
||||
&mut texture as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
let texture = ComPtr(texture);
|
||||
|
||||
#[allow(invalid_value)]
|
||||
let mut texture_desc = mem::MaybeUninit::uninit().assume_init();
|
||||
(*texture.0).GetDesc(&mut texture_desc);
|
||||
|
||||
texture_desc.Usage = D3D11_USAGE_STAGING;
|
||||
texture_desc.BindFlags = 0;
|
||||
texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
texture_desc.MiscFlags = 0;
|
||||
|
||||
let mut readable = ptr::null_mut();
|
||||
wrap_hresult((*self.device.0).CreateTexture2D(
|
||||
&mut texture_desc,
|
||||
ptr::null(),
|
||||
&mut readable,
|
||||
))?;
|
||||
(*readable).SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM);
|
||||
let readable = ComPtr(readable);
|
||||
|
||||
let mut surface = ptr::null_mut();
|
||||
(*readable.0).QueryInterface(
|
||||
&IID_IDXGISurface,
|
||||
&mut surface as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
|
||||
(*self.context.0).CopyResource(readable.0 as *mut _, texture.0 as *mut _);
|
||||
|
||||
Ok(surface)
|
||||
}
|
||||
|
||||
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {
|
||||
if self.output_texture {
|
||||
Ok(Frame::Texture(self.get_texture(timeout)?))
|
||||
} else {
|
||||
let width = self.width;
|
||||
let height = self.height;
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
|
||||
self.get_pixelbuffer(timeout)?,
|
||||
width,
|
||||
height,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pixelbuffer<'a>(&'a mut self, timeout: UINT) -> io::Result<&'a [u8]> {
|
||||
unsafe {
|
||||
// Release last frame.
|
||||
// No error checking needed because we don't care.
|
||||
// None of the errors crash anyway.
|
||||
let result = {
|
||||
if let Some(gdi_capturer) = &self.gdi_capturer {
|
||||
match gdi_capturer.frame(&mut self.gdi_buffer) {
|
||||
Ok(_) => {
|
||||
crate::would_block_if_equal(
|
||||
&mut self.saved_raw_data,
|
||||
&self.gdi_buffer,
|
||||
)?;
|
||||
&self.gdi_buffer
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, err.to_string()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.unmap();
|
||||
let r = self.load_frame(timeout)?;
|
||||
let rotate = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => kRotate0,
|
||||
DXGI_MODE_ROTATION_ROTATE90 => kRotate90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => kRotate180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => kRotate270,
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Unknown rotation".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if rotate == kRotate0 {
|
||||
slice::from_raw_parts(r.0, r.1 as usize * self.height)
|
||||
} else {
|
||||
self.rotated.resize(self.width * self.height * 4, 0);
|
||||
crate::common::ARGBRotate(
|
||||
r.0,
|
||||
r.1,
|
||||
self.rotated.as_mut_ptr(),
|
||||
4 * self.width as i32,
|
||||
if rotate == kRotate180 {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
} as _,
|
||||
if rotate != kRotate180 {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
} as _,
|
||||
rotate,
|
||||
);
|
||||
&self.rotated[..]
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_texture(&mut self, timeout: UINT) -> io::Result<(*mut c_void, usize)> {
|
||||
unsafe {
|
||||
if self.duplication.0.is_null() {
|
||||
return Err(std::io::ErrorKind::AddrNotAvailable.into());
|
||||
}
|
||||
(*self.duplication.0).ReleaseFrame();
|
||||
let mut frame = ptr::null_mut();
|
||||
#[allow(invalid_value)]
|
||||
let mut info = mem::MaybeUninit::uninit().assume_init();
|
||||
|
||||
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
||||
let frame = ComPtr(frame);
|
||||
|
||||
if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 {
|
||||
return Err(std::io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
|
||||
let mut texture: *mut ID3D11Texture2D = ptr::null_mut();
|
||||
(*frame.0).QueryInterface(
|
||||
&IID_ID3D11Texture2D,
|
||||
&mut texture as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
let texture = ComPtr(texture);
|
||||
self.texture = texture;
|
||||
|
||||
let mut final_texture = self.texture.0 as *mut c_void;
|
||||
let mut rotation = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_ROTATE90 => 90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => 180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => 270,
|
||||
_ => 0,
|
||||
};
|
||||
if rotation != 0
|
||||
&& !self.texture.is_null()
|
||||
&& !self.rotate.video_context.is_null()
|
||||
&& !self.rotate.video_device.is_null()
|
||||
&& !self.rotate.video_processor_enum.is_null()
|
||||
&& !self.rotate.video_processor.is_null()
|
||||
{
|
||||
let mut desc: D3D11_TEXTURE2D_DESC = mem::zeroed();
|
||||
(*self.texture.0).GetDesc(&mut desc);
|
||||
if rotation == 90 || rotation == 270 {
|
||||
let tmp = desc.Width;
|
||||
desc.Width = desc.Height;
|
||||
desc.Height = tmp;
|
||||
}
|
||||
if !self.rotate.texture.1 {
|
||||
self.rotate.texture.1 = true;
|
||||
let mut rotated_texture: *mut ID3D11Texture2D = ptr::null_mut();
|
||||
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
|
||||
(*self.device.0).CreateTexture2D(&desc, ptr::null(), &mut rotated_texture);
|
||||
self.rotate.texture.0 = ComPtr(rotated_texture);
|
||||
}
|
||||
if !self.rotate.texture.0.is_null()
|
||||
&& desc.Width == self.width as u32
|
||||
&& desc.Height == self.height as u32
|
||||
{
|
||||
let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
|
||||
FourCC: 0,
|
||||
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
|
||||
Texture2D: D3D11_TEX2D_VPIV {
|
||||
ArraySlice: 0,
|
||||
MipSlice: 0,
|
||||
},
|
||||
};
|
||||
let mut input_view = ptr::null_mut();
|
||||
(*self.rotate.video_device.0).CreateVideoProcessorInputView(
|
||||
self.texture.0 as *mut _,
|
||||
self.rotate.video_processor_enum.0 as *mut _,
|
||||
&input_view_desc,
|
||||
&mut input_view,
|
||||
);
|
||||
if !input_view.is_null() {
|
||||
let input_view = ComPtr(input_view);
|
||||
let mut output_view_desc: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC =
|
||||
mem::zeroed();
|
||||
output_view_desc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D;
|
||||
output_view_desc.u.Texture2D_mut().MipSlice = 0;
|
||||
let mut output_view = ptr::null_mut();
|
||||
(*self.rotate.video_device.0).CreateVideoProcessorOutputView(
|
||||
self.rotate.texture.0 .0 as *mut _,
|
||||
self.rotate.video_processor_enum.0 as *mut _,
|
||||
&output_view_desc,
|
||||
&mut output_view,
|
||||
);
|
||||
if !output_view.is_null() {
|
||||
let output_view = ComPtr(output_view);
|
||||
let mut stream_data: D3D11_VIDEO_PROCESSOR_STREAM = mem::zeroed();
|
||||
stream_data.Enable = TRUE;
|
||||
stream_data.pInputSurface = input_view.0;
|
||||
(*self.rotate.video_context.0).VideoProcessorBlt(
|
||||
self.rotate.video_processor.0,
|
||||
output_view.0,
|
||||
0,
|
||||
1,
|
||||
&stream_data,
|
||||
);
|
||||
final_texture = self.rotate.texture.0 .0 as *mut c_void;
|
||||
rotation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((final_texture, rotation))
|
||||
}
|
||||
}
|
||||
|
||||
fn unmap(&self) {
|
||||
unsafe {
|
||||
(*self.duplication.0).ReleaseFrame();
|
||||
if self.fastlane {
|
||||
(*self.duplication.0).UnMapDesktopSurface();
|
||||
} else {
|
||||
if !self.surface.is_null() {
|
||||
(*self.surface.0).Unmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn device(&self) -> AdapterDevice {
|
||||
AdapterDevice {
|
||||
device: self.device.0 as _,
|
||||
vendor_id: self.adapter_desc1.VendorId,
|
||||
luid: ((self.adapter_desc1.AdapterLuid.HighPart as i64) << 32)
|
||||
| self.adapter_desc1.AdapterLuid.LowPart as i64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Capturer {
|
||||
fn drop(&mut self) {
|
||||
if !self.duplication.is_null() {
|
||||
self.unmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Displays {
|
||||
factory: ComPtr<IDXGIFactory1>,
|
||||
adapter: ComPtr<IDXGIAdapter1>,
|
||||
/// Index of the CURRENT adapter.
|
||||
nadapter: UINT,
|
||||
/// Index of the NEXT display to fetch.
|
||||
ndisplay: UINT,
|
||||
}
|
||||
|
||||
impl Displays {
|
||||
pub fn new() -> io::Result<Displays> {
|
||||
let mut factory = ptr::null_mut();
|
||||
wrap_hresult(unsafe { CreateDXGIFactory1(&IID_IDXGIFactory1, &mut factory) })?;
|
||||
|
||||
let factory = factory as *mut IDXGIFactory1;
|
||||
let mut adapter = ptr::null_mut();
|
||||
unsafe {
|
||||
// On error, our adapter is null, so it's fine.
|
||||
(*factory).EnumAdapters1(0, &mut adapter);
|
||||
};
|
||||
|
||||
Ok(Displays {
|
||||
factory: ComPtr(factory),
|
||||
adapter: ComPtr(adapter),
|
||||
nadapter: 0,
|
||||
ndisplay: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_from_gdi() -> Vec<Display> {
|
||||
let mut all = Vec::new();
|
||||
let mut i: DWORD = 0;
|
||||
loop {
|
||||
#[allow(invalid_value)]
|
||||
let mut d: DISPLAY_DEVICEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() };
|
||||
d.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as _;
|
||||
let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), i, &mut d as _, 0) };
|
||||
if ok == FALSE {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
if 0 == (d.StateFlags & DISPLAY_DEVICE_ACTIVE)
|
||||
|| (d.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) > 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// let is_primary = (d.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) > 0;
|
||||
let mut disp = Display {
|
||||
inner: ComPtr(std::ptr::null_mut()),
|
||||
adapter: ComPtr(std::ptr::null_mut()),
|
||||
desc: unsafe { std::mem::zeroed() },
|
||||
gdi: true,
|
||||
};
|
||||
disp.desc.DeviceName = d.DeviceName;
|
||||
#[allow(invalid_value)]
|
||||
let mut m: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() };
|
||||
m.dmSize = std::mem::size_of::<DEVMODEW>() as _;
|
||||
m.dmDriverExtra = 0;
|
||||
let ok = unsafe {
|
||||
EnumDisplaySettingsExW(
|
||||
disp.desc.DeviceName.as_ptr(),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mut m as _,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if ok == FALSE {
|
||||
continue;
|
||||
}
|
||||
disp.desc.DesktopCoordinates.left = unsafe { m.u1.s2().dmPosition.x };
|
||||
disp.desc.DesktopCoordinates.top = unsafe { m.u1.s2().dmPosition.y };
|
||||
disp.desc.DesktopCoordinates.right =
|
||||
disp.desc.DesktopCoordinates.left + m.dmPelsWidth as i32;
|
||||
disp.desc.DesktopCoordinates.bottom =
|
||||
disp.desc.DesktopCoordinates.top + m.dmPelsHeight as i32;
|
||||
disp.desc.AttachedToDesktop = 1;
|
||||
all.push(disp);
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
// No Adapter => Some(None)
|
||||
// Non-Empty Adapter => Some(Some(OUTPUT))
|
||||
// End of Adapter => None
|
||||
fn read_and_invalidate(&mut self) -> Option<Option<Display>> {
|
||||
// If there is no adapter, there is nothing left for us to do.
|
||||
|
||||
if self.adapter.is_null() {
|
||||
return Some(None);
|
||||
}
|
||||
|
||||
// Otherwise, we get the next output of the current adapter.
|
||||
|
||||
let output = unsafe {
|
||||
let mut output = ptr::null_mut();
|
||||
(*self.adapter.0).EnumOutputs(self.ndisplay, &mut output);
|
||||
ComPtr(output)
|
||||
};
|
||||
|
||||
// If the current adapter is done, we free it.
|
||||
// We return None so the caller gets the next adapter and tries again.
|
||||
|
||||
if output.is_null() {
|
||||
self.adapter = ComPtr(ptr::null_mut());
|
||||
return None;
|
||||
}
|
||||
|
||||
// Advance to the next display.
|
||||
|
||||
self.ndisplay += 1;
|
||||
|
||||
// We get the display's details.
|
||||
|
||||
let desc = unsafe {
|
||||
#[allow(invalid_value)]
|
||||
let mut desc = mem::MaybeUninit::uninit().assume_init();
|
||||
(*output.0).GetDesc(&mut desc);
|
||||
desc
|
||||
};
|
||||
|
||||
// We cast it up to the version needed for desktop duplication.
|
||||
|
||||
let mut inner: *mut IDXGIOutput1 = ptr::null_mut();
|
||||
unsafe {
|
||||
(*output.0).QueryInterface(&IID_IDXGIOutput1, &mut inner as *mut *mut _ as *mut *mut _);
|
||||
}
|
||||
|
||||
// If it's null, we have an error.
|
||||
// So we act like the adapter is done.
|
||||
|
||||
if inner.is_null() {
|
||||
self.adapter = ComPtr(ptr::null_mut());
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
(*self.adapter.0).AddRef();
|
||||
}
|
||||
|
||||
Some(Some(Display {
|
||||
inner: ComPtr(inner),
|
||||
adapter: ComPtr(self.adapter.0),
|
||||
desc,
|
||||
gdi: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Displays {
|
||||
type Item = Display;
|
||||
fn next(&mut self) -> Option<Display> {
|
||||
if let Some(res) = self.read_and_invalidate() {
|
||||
res
|
||||
} else {
|
||||
// We need to replace the adapter.
|
||||
|
||||
self.ndisplay = 0;
|
||||
self.nadapter += 1;
|
||||
|
||||
self.adapter = unsafe {
|
||||
let mut adapter = ptr::null_mut();
|
||||
(*self.factory.0).EnumAdapters1(self.nadapter, &mut adapter);
|
||||
ComPtr(adapter)
|
||||
};
|
||||
|
||||
if let Some(res) = self.read_and_invalidate() {
|
||||
res
|
||||
} else {
|
||||
// All subsequent adapters will also be empty.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display {
|
||||
inner: ComPtr<IDXGIOutput1>,
|
||||
adapter: ComPtr<IDXGIAdapter1>,
|
||||
desc: DXGI_OUTPUT_DESC,
|
||||
gdi: bool,
|
||||
}
|
||||
|
||||
// optimized for updated region
|
||||
// https://github.com/dchapyshev/aspia/blob/master/source/base/desktop/win/dxgi_output_duplicator.cc
|
||||
// rotation
|
||||
// https://github.com/bryal/dxgcap-rs/blob/master/src/lib.rs
|
||||
|
||||
impl Display {
|
||||
pub fn width(&self) -> LONG {
|
||||
self.desc.DesktopCoordinates.right - self.desc.DesktopCoordinates.left
|
||||
}
|
||||
|
||||
pub fn height(&self) -> LONG {
|
||||
self.desc.DesktopCoordinates.bottom - self.desc.DesktopCoordinates.top
|
||||
}
|
||||
|
||||
pub fn attached_to_desktop(&self) -> bool {
|
||||
self.desc.AttachedToDesktop != 0
|
||||
}
|
||||
|
||||
pub fn rotation(&self) -> DXGI_MODE_ROTATION {
|
||||
self.desc.Rotation
|
||||
}
|
||||
|
||||
fn create_gdi(&self) -> Option<CapturerGDI> {
|
||||
if let Ok(res) = CapturerGDI::new(self.name(), self.width(), self.height()) {
|
||||
Some(res)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hmonitor(&self) -> HMONITOR {
|
||||
self.desc.Monitor
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &[u16] {
|
||||
let s = &self.desc.DeviceName;
|
||||
let i = s.iter().position(|&x| x == 0).unwrap_or(s.len());
|
||||
&s[..i]
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
self.desc.AttachedToDesktop != 0
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (LONG, LONG) {
|
||||
(
|
||||
self.desc.DesktopCoordinates.left,
|
||||
self.desc.DesktopCoordinates.top,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
pub fn adapter_luid(&self) -> Option<i64> {
|
||||
unsafe {
|
||||
if !self.adapter.is_null() {
|
||||
#[allow(invalid_value)]
|
||||
let mut adapter_desc1 = mem::MaybeUninit::uninit().assume_init();
|
||||
if wrap_hresult((*self.adapter.0).GetDesc1(&mut adapter_desc1)).is_ok() {
|
||||
let luid = ((adapter_desc1.AdapterLuid.HighPart as i64) << 32)
|
||||
| adapter_desc1.AdapterLuid.LowPart as i64;
|
||||
return Some(luid);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_hresult(x: HRESULT) -> io::Result<()> {
|
||||
use std::io::ErrorKind::*;
|
||||
Err((match x {
|
||||
S_OK => return Ok(()),
|
||||
DXGI_ERROR_ACCESS_LOST => ConnectionReset,
|
||||
DXGI_ERROR_WAIT_TIMEOUT => TimedOut,
|
||||
DXGI_ERROR_INVALID_CALL => InvalidData,
|
||||
E_ACCESSDENIED => PermissionDenied,
|
||||
DXGI_ERROR_UNSUPPORTED => ConnectionRefused,
|
||||
DXGI_ERROR_NOT_CURRENTLY_AVAILABLE => Interrupted,
|
||||
DXGI_ERROR_SESSION_DISCONNECTED => ConnectionAborted,
|
||||
E_INVALIDARG => InvalidInput,
|
||||
_ => {
|
||||
// 0x8000ffff https://www.auslogics.com/en/articles/windows-10-update-error-0x8000ffff-fixed/
|
||||
return Err(io::Error::new(Other, format!("Error code: {:#X}", x)));
|
||||
}
|
||||
})
|
||||
.into())
|
||||
}
|
||||
|
||||
struct Rotate {
|
||||
video_context: ComPtr<ID3D11VideoContext>,
|
||||
video_device: ComPtr<ID3D11VideoDevice>,
|
||||
video_processor_enum: ComPtr<ID3D11VideoProcessorEnumerator>,
|
||||
video_processor: ComPtr<ID3D11VideoProcessor>,
|
||||
texture: (ComPtr<ID3D11Texture2D>, bool),
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#[cfg(quartz)]
|
||||
extern crate block;
|
||||
#[macro_use]
|
||||
extern crate cfg_if;
|
||||
pub use hbb_common::libc;
|
||||
#[cfg(dxgi)]
|
||||
extern crate winapi;
|
||||
|
||||
pub use common::*;
|
||||
|
||||
#[cfg(quartz)]
|
||||
pub mod quartz;
|
||||
|
||||
#[cfg(x11)]
|
||||
pub mod x11;
|
||||
|
||||
#[cfg(all(x11, feature = "wayland"))]
|
||||
pub mod wayland;
|
||||
|
||||
#[cfg(dxgi)]
|
||||
pub mod dxgi;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod android;
|
||||
|
||||
mod common;
|
||||
@@ -0,0 +1,111 @@
|
||||
use std::ptr;
|
||||
|
||||
use block::{Block, ConcreteBlock};
|
||||
use hbb_common::libc::c_void;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::config::Config;
|
||||
use super::display::Display;
|
||||
use super::ffi::*;
|
||||
use super::frame::Frame;
|
||||
|
||||
pub struct Capturer {
|
||||
stream: CGDisplayStreamRef,
|
||||
queue: DispatchQueue,
|
||||
|
||||
width: usize,
|
||||
height: usize,
|
||||
format: PixelFormat,
|
||||
display: Display,
|
||||
stopped: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new<F: Fn(Frame) + 'static>(
|
||||
display: Display,
|
||||
width: usize,
|
||||
height: usize,
|
||||
format: PixelFormat,
|
||||
config: Config,
|
||||
handler: F,
|
||||
) -> Result<Capturer, CGError> {
|
||||
let stopped = Arc::new(Mutex::new(false));
|
||||
let cloned_stopped = stopped.clone();
|
||||
let handler: FrameAvailableHandler = ConcreteBlock::new(move |status, _, surface, _| {
|
||||
use self::CGDisplayStreamFrameStatus::*;
|
||||
if status == Stopped {
|
||||
let mut lock = cloned_stopped.lock().unwrap();
|
||||
*lock = true;
|
||||
return;
|
||||
}
|
||||
if status == FrameComplete {
|
||||
handler(unsafe { Frame::new(surface) });
|
||||
}
|
||||
})
|
||||
.copy();
|
||||
|
||||
let queue = unsafe {
|
||||
dispatch_queue_create(
|
||||
b"quadrupleslap.scrap\0".as_ptr() as *const i8,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
let stream = unsafe {
|
||||
let config = config.build();
|
||||
let stream = CGDisplayStreamCreateWithDispatchQueue(
|
||||
display.id(),
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
config,
|
||||
queue,
|
||||
&*handler as *const Block<_, _> as *const c_void,
|
||||
);
|
||||
CFRelease(config);
|
||||
stream
|
||||
};
|
||||
|
||||
match unsafe { CGDisplayStreamStart(stream) } {
|
||||
CGError::Success => Ok(Capturer {
|
||||
stream,
|
||||
queue,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
display,
|
||||
stopped,
|
||||
}),
|
||||
x => Err(x),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
pub fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
pub fn format(&self) -> PixelFormat {
|
||||
self.format
|
||||
}
|
||||
pub fn display(&self) -> Display {
|
||||
self.display
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Capturer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = CGDisplayStreamStop(self.stream);
|
||||
loop {
|
||||
if *self.stopped.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
}
|
||||
CFRelease(self.stream);
|
||||
dispatch_release(self.queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::ptr;
|
||||
|
||||
use hbb_common::libc::c_void;
|
||||
|
||||
use super::ffi::*;
|
||||
|
||||
//TODO: Color space, YCbCr matrix.
|
||||
pub struct Config {
|
||||
/// Whether the cursor is visible.
|
||||
pub cursor: bool,
|
||||
/// Whether it should letterbox or stretch.
|
||||
pub letterbox: bool,
|
||||
/// Minimum seconds per frame.
|
||||
pub throttle: f64,
|
||||
/// How many frames are allocated.
|
||||
/// 3 is the recommended value.
|
||||
/// 8 is the maximum value.
|
||||
pub queue_length: i8,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Don't forget to CFRelease this!
|
||||
pub fn build(self) -> CFDictionaryRef {
|
||||
unsafe {
|
||||
let throttle = CFNumberCreate(
|
||||
ptr::null_mut(),
|
||||
CFNumberType::Float64,
|
||||
&self.throttle as *const _ as *const c_void,
|
||||
);
|
||||
let queue_length = CFNumberCreate(
|
||||
ptr::null_mut(),
|
||||
CFNumberType::SInt8,
|
||||
&self.queue_length as *const _ as *const c_void,
|
||||
);
|
||||
|
||||
let keys: [CFStringRef; 4] = [
|
||||
kCGDisplayStreamShowCursor,
|
||||
kCGDisplayStreamPreserveAspectRatio,
|
||||
kCGDisplayStreamMinimumFrameTime,
|
||||
kCGDisplayStreamQueueDepth,
|
||||
];
|
||||
let values: [*mut c_void; 4] = [
|
||||
cfbool(self.cursor),
|
||||
cfbool(self.letterbox),
|
||||
throttle,
|
||||
queue_length,
|
||||
];
|
||||
|
||||
let res = CFDictionaryCreate(
|
||||
ptr::null_mut(),
|
||||
keys.as_ptr(),
|
||||
values.as_ptr(),
|
||||
4,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
);
|
||||
|
||||
CFRelease(throttle);
|
||||
CFRelease(queue_length);
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
cursor: false,
|
||||
letterbox: true,
|
||||
throttle: 0.0,
|
||||
queue_length: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::mem;
|
||||
|
||||
use super::ffi::*;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct Display(u32);
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> Display {
|
||||
Display(unsafe { CGMainDisplayID() })
|
||||
}
|
||||
|
||||
pub fn online() -> Result<Vec<Display>, CGError> {
|
||||
unsafe {
|
||||
#[allow(invalid_value)]
|
||||
let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init();
|
||||
let mut len: u32 = 0;
|
||||
|
||||
match CGGetOnlineDisplayList(16, arr.as_mut_ptr(), &mut len) {
|
||||
CGError::Success => (),
|
||||
x => return Err(x),
|
||||
}
|
||||
|
||||
let mut res = Vec::with_capacity(16);
|
||||
for i in 0..len as usize {
|
||||
res.push(Display(*arr.get_unchecked(i)));
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn width(self) -> usize {
|
||||
let w = unsafe { CGDisplayPixelsWide(self.0) };
|
||||
let s = self.scale();
|
||||
if s > 1.0 {
|
||||
((w as f64) * s).round() as usize
|
||||
} else {
|
||||
w
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(self) -> usize {
|
||||
let h = unsafe { CGDisplayPixelsHigh(self.0) };
|
||||
let s = self.scale();
|
||||
if s > 1.0 {
|
||||
((h as f64) * s).round() as usize
|
||||
} else {
|
||||
h
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_builtin(self) -> bool {
|
||||
unsafe { CGDisplayIsBuiltin(self.0) != 0 }
|
||||
}
|
||||
|
||||
pub fn is_primary(self) -> bool {
|
||||
unsafe { CGDisplayIsMain(self.0) != 0 }
|
||||
}
|
||||
|
||||
pub fn is_active(self) -> bool {
|
||||
unsafe { CGDisplayIsActive(self.0) != 0 }
|
||||
}
|
||||
|
||||
pub fn is_online(self) -> bool {
|
||||
unsafe { CGDisplayIsOnline(self.0) != 0 }
|
||||
}
|
||||
|
||||
pub fn scale(self) -> f64 {
|
||||
let s = unsafe { BackingScaleFactor(self.0) as _ };
|
||||
if s > 1. {
|
||||
let enable_retina = super::ENABLE_RETINA.lock().unwrap().clone();
|
||||
if enable_retina {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
1.
|
||||
}
|
||||
|
||||
pub fn bounds(self) -> CGRect {
|
||||
unsafe { CGDisplayBounds(self.0) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use block::RcBlock;
|
||||
use hbb_common::libc::c_void;
|
||||
|
||||
pub type CGDisplayStreamRef = *mut c_void;
|
||||
pub type CFDictionaryRef = *mut c_void;
|
||||
pub type CFBooleanRef = *mut c_void;
|
||||
pub type CFNumberRef = *mut c_void;
|
||||
pub type CFStringRef = *mut c_void;
|
||||
pub type CGDisplayStreamUpdateRef = *mut c_void;
|
||||
pub type IOSurfaceRef = *mut c_void;
|
||||
pub type DispatchQueue = *mut c_void;
|
||||
pub type DispatchQueueAttr = *mut c_void;
|
||||
pub type CFAllocatorRef = *mut c_void;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct CFDictionaryKeyCallBacks {
|
||||
callbacks: [usize; 5],
|
||||
version: i32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct CFDictionaryValueCallBacks {
|
||||
callbacks: [usize; 4],
|
||||
version: i32,
|
||||
}
|
||||
|
||||
macro_rules! pixel_format {
|
||||
($a:expr, $b:expr, $c:expr, $d:expr) => {
|
||||
($a as i32) << 24 | ($b as i32) << 16 | ($c as i32) << 8 | ($d as i32)
|
||||
};
|
||||
}
|
||||
|
||||
pub const SURFACE_LOCK_READ_ONLY: u32 = 0x0000_0001;
|
||||
pub const SURFACE_LOCK_AVOID_SYNC: u32 = 0x0000_0002;
|
||||
|
||||
pub fn cfbool(x: bool) -> CFBooleanRef {
|
||||
unsafe {
|
||||
if x {
|
||||
kCFBooleanTrue
|
||||
} else {
|
||||
kCFBooleanFalse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
pub enum CGDisplayStreamFrameStatus {
|
||||
/// A new frame was generated.
|
||||
FrameComplete = 0,
|
||||
/// A new frame was not generated because the display did not change.
|
||||
FrameIdle = 1,
|
||||
/// A new frame was not generated because the display has gone blank.
|
||||
FrameBlank = 2,
|
||||
/// The display stream was stopped.
|
||||
Stopped = 3,
|
||||
#[doc(hidden)]
|
||||
__Nonexhaustive,
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
pub enum CFNumberType {
|
||||
/* Fixed-width types */
|
||||
SInt8 = 1,
|
||||
SInt16 = 2,
|
||||
SInt32 = 3,
|
||||
SInt64 = 4,
|
||||
Float32 = 5,
|
||||
Float64 = 6,
|
||||
/* 64-bit IEEE 754 */
|
||||
/* Basic C types */
|
||||
Char = 7,
|
||||
Short = 8,
|
||||
Int = 9,
|
||||
Long = 10,
|
||||
LongLong = 11,
|
||||
Float = 12,
|
||||
Double = 13,
|
||||
/* Other */
|
||||
CFIndex = 14,
|
||||
NSInteger = 15,
|
||||
CGFloat = 16,
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
#[must_use]
|
||||
pub enum CGError {
|
||||
Success = 0,
|
||||
Failure = 1000,
|
||||
IllegalArgument = 1001,
|
||||
InvalidConnection = 1002,
|
||||
InvalidContext = 1003,
|
||||
CannotComplete = 1004,
|
||||
NotImplemented = 1006,
|
||||
RangeCheck = 1007,
|
||||
TypeCheck = 1008,
|
||||
InvalidOperation = 1010,
|
||||
NoneAvailable = 1011,
|
||||
#[doc(hidden)]
|
||||
__Nonexhaustive,
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
pub enum PixelFormat {
|
||||
/// Packed Little Endian ARGB8888
|
||||
Argb8888 = pixel_format!('B', 'G', 'R', 'A'),
|
||||
/// Packed Little Endian ARGB2101010
|
||||
Argb2101010 = pixel_format!('l', '1', '0', 'r'),
|
||||
/// 2-plane "video" range YCbCr 4:2:0
|
||||
YCbCr420Video = pixel_format!('4', '2', '0', 'v'),
|
||||
/// 2-plane "full" range YCbCr 4:2:0
|
||||
YCbCr420Full = pixel_format!('4', '2', '0', 'f'),
|
||||
#[doc(hidden)]
|
||||
__Nonexhaustive,
|
||||
}
|
||||
|
||||
pub type CGDisplayStreamFrameAvailableHandler = *const c_void;
|
||||
|
||||
pub type FrameAvailableHandler = RcBlock<
|
||||
(
|
||||
CGDisplayStreamFrameStatus, // status
|
||||
u64, // displayTime
|
||||
IOSurfaceRef, // frameSurface
|
||||
CGDisplayStreamUpdateRef, // updateRef
|
||||
),
|
||||
(),
|
||||
>;
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub type CGFloat = f64;
|
||||
#[cfg(not(target_pointer_width = "64"))]
|
||||
pub type CGFloat = f32;
|
||||
#[repr(C)]
|
||||
pub struct CGPoint {
|
||||
pub x: CGFloat,
|
||||
pub y: CGFloat,
|
||||
}
|
||||
#[repr(C)]
|
||||
pub struct CGSize {
|
||||
pub width: CGFloat,
|
||||
pub height: CGFloat,
|
||||
}
|
||||
#[repr(C)]
|
||||
pub struct CGRect {
|
||||
pub origin: CGPoint,
|
||||
pub size: CGSize,
|
||||
}
|
||||
|
||||
#[link(name = "System", kind = "dylib")]
|
||||
#[link(name = "CoreGraphics", kind = "framework")]
|
||||
#[link(name = "CoreFoundation", kind = "framework")]
|
||||
#[link(name = "IOSurface", kind = "framework")]
|
||||
extern "C" {
|
||||
// CoreGraphics
|
||||
|
||||
pub static kCGDisplayStreamShowCursor: CFStringRef;
|
||||
pub static kCGDisplayStreamPreserveAspectRatio: CFStringRef;
|
||||
pub static kCGDisplayStreamMinimumFrameTime: CFStringRef;
|
||||
pub static kCGDisplayStreamQueueDepth: CFStringRef;
|
||||
|
||||
pub fn CGDisplayStreamCreateWithDispatchQueue(
|
||||
display: u32,
|
||||
output_width: usize,
|
||||
output_height: usize,
|
||||
pixel_format: PixelFormat,
|
||||
properties: CFDictionaryRef,
|
||||
queue: DispatchQueue,
|
||||
handler: CGDisplayStreamFrameAvailableHandler,
|
||||
) -> CGDisplayStreamRef;
|
||||
|
||||
pub fn CGDisplayStreamStart(displayStream: CGDisplayStreamRef) -> CGError;
|
||||
|
||||
pub fn CGDisplayStreamStop(displayStream: CGDisplayStreamRef) -> CGError;
|
||||
|
||||
pub fn CGMainDisplayID() -> u32;
|
||||
pub fn CGDisplayPixelsWide(display: u32) -> usize;
|
||||
pub fn CGDisplayPixelsHigh(display: u32) -> usize;
|
||||
|
||||
pub fn CGGetOnlineDisplayList(
|
||||
max_displays: u32,
|
||||
online_displays: *mut u32,
|
||||
display_count: *mut u32,
|
||||
) -> CGError;
|
||||
|
||||
pub fn CGDisplayIsBuiltin(display: u32) -> i32;
|
||||
pub fn CGDisplayIsMain(display: u32) -> i32;
|
||||
pub fn CGDisplayIsActive(display: u32) -> i32;
|
||||
pub fn CGDisplayIsOnline(display: u32) -> i32;
|
||||
|
||||
pub fn CGDisplayBounds(display: u32) -> CGRect;
|
||||
pub fn BackingScaleFactor(display: u32) -> f32;
|
||||
|
||||
// IOSurface
|
||||
|
||||
pub fn IOSurfaceGetAllocSize(buffer: IOSurfaceRef) -> usize;
|
||||
pub fn IOSurfaceGetBaseAddress(buffer: IOSurfaceRef) -> *mut c_void;
|
||||
pub fn IOSurfaceIncrementUseCount(buffer: IOSurfaceRef);
|
||||
pub fn IOSurfaceDecrementUseCount(buffer: IOSurfaceRef);
|
||||
pub fn IOSurfaceLock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32;
|
||||
pub fn IOSurfaceUnlock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32;
|
||||
pub fn IOSurfaceGetBaseAddressOfPlane(buffer: IOSurfaceRef, index: usize) -> *mut c_void;
|
||||
pub fn IOSurfaceGetBytesPerRowOfPlane(buffer: IOSurfaceRef, index: usize) -> usize;
|
||||
|
||||
// Dispatch
|
||||
|
||||
pub fn dispatch_queue_create(label: *const i8, attr: DispatchQueueAttr) -> DispatchQueue;
|
||||
|
||||
pub fn dispatch_release(object: DispatchQueue);
|
||||
|
||||
// Core Foundation
|
||||
|
||||
pub static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
|
||||
pub static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
|
||||
|
||||
// EVEN THE BOOLEANS ARE REFERENCES.
|
||||
pub static kCFBooleanTrue: CFBooleanRef;
|
||||
pub static kCFBooleanFalse: CFBooleanRef;
|
||||
|
||||
pub fn CFNumberCreate(
|
||||
allocator: CFAllocatorRef,
|
||||
theType: CFNumberType,
|
||||
valuePtr: *const c_void,
|
||||
) -> CFNumberRef;
|
||||
|
||||
pub fn CFDictionaryCreate(
|
||||
allocator: CFAllocatorRef,
|
||||
keys: *const *mut c_void,
|
||||
values: *const *mut c_void,
|
||||
numValues: i64,
|
||||
keyCallBacks: *const CFDictionaryKeyCallBacks,
|
||||
valueCallBacks: *const CFDictionaryValueCallBacks,
|
||||
) -> CFDictionaryRef;
|
||||
|
||||
pub fn CFRetain(cf: *const c_void);
|
||||
pub fn CFRelease(cf: *const c_void);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::{ops, ptr, slice};
|
||||
|
||||
use super::ffi::*;
|
||||
|
||||
pub struct Frame {
|
||||
surface: IOSurfaceRef,
|
||||
inner: &'static [u8],
|
||||
bgra: Vec<u8>,
|
||||
bgra_stride: usize,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub unsafe fn new(surface: IOSurfaceRef) -> Frame {
|
||||
CFRetain(surface);
|
||||
IOSurfaceIncrementUseCount(surface);
|
||||
|
||||
IOSurfaceLock(surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut());
|
||||
|
||||
let inner = slice::from_raw_parts(
|
||||
IOSurfaceGetBaseAddress(surface) as *const u8,
|
||||
IOSurfaceGetAllocSize(surface),
|
||||
);
|
||||
|
||||
Frame {
|
||||
surface,
|
||||
inner,
|
||||
bgra: Vec::new(),
|
||||
bgra_stride: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &[u8] {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn stride(&self) -> usize {
|
||||
self.bgra_stride
|
||||
}
|
||||
|
||||
pub fn surface_to_bgra<'a>(&'a mut self, h: usize) {
|
||||
unsafe {
|
||||
let plane0 = IOSurfaceGetBaseAddressOfPlane(self.surface, 0);
|
||||
self.bgra_stride = IOSurfaceGetBytesPerRowOfPlane(self.surface, 0);
|
||||
self.bgra.resize(self.bgra_stride * h, 0);
|
||||
std::ptr::copy_nonoverlapping(
|
||||
plane0 as _,
|
||||
self.bgra.as_mut_ptr(),
|
||||
self.bgra_stride * h,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Frame {
|
||||
type Target = [u8];
|
||||
fn deref<'a>(&'a self) -> &'a [u8] {
|
||||
&self.bgra
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Frame {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
IOSurfaceUnlock(self.surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut());
|
||||
|
||||
IOSurfaceDecrementUseCount(self.surface);
|
||||
CFRelease(self.surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
pub use self::capturer::Capturer;
|
||||
pub use self::config::Config;
|
||||
pub use self::display::Display;
|
||||
pub use self::ffi::{CGError, PixelFormat};
|
||||
pub use self::frame::Frame;
|
||||
|
||||
mod capturer;
|
||||
mod config;
|
||||
mod display;
|
||||
pub mod ffi;
|
||||
mod frame;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref ENABLE_RETINA: Arc<Mutex<bool>> = Arc::new(Mutex::new(true));
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod capturable;
|
||||
pub mod pipewire;
|
||||
pub mod display;
|
||||
mod screencast_portal;
|
||||
mod request_portal;
|
||||
pub mod remote_desktop_portal;
|
||||
@@ -0,0 +1,11 @@
|
||||
# About
|
||||
|
||||
Derived from https://github.com/H-M-H/Weylus/tree/master/src/capturable with the author's consent, https://github.com/rustdesk/rustdesk/issues/56#issuecomment-882727967
|
||||
|
||||
# Dep
|
||||
|
||||
Works fine on Ubuntu 21.04 with pipewire 3 and xdg-desktop-portal 1.8
|
||||
|
||||
`
|
||||
apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
`
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::boxed::Box;
|
||||
use std::error::Error;
|
||||
|
||||
pub enum PixelProvider<'a> {
|
||||
// 8 bits per color
|
||||
RGB(usize, usize, &'a [u8]),
|
||||
RGB0(usize, usize, &'a [u8]),
|
||||
BGR0(usize, usize, &'a [u8]),
|
||||
// width, height, stride
|
||||
BGR0S(usize, usize, usize, &'a [u8]),
|
||||
NONE,
|
||||
}
|
||||
|
||||
impl<'a> PixelProvider<'a> {
|
||||
pub fn size(&self) -> (usize, usize) {
|
||||
match self {
|
||||
PixelProvider::RGB(w, h, _) => (*w, *h),
|
||||
PixelProvider::RGB0(w, h, _) => (*w, *h),
|
||||
PixelProvider::BGR0(w, h, _) => (*w, *h),
|
||||
PixelProvider::BGR0S(w, h, _, _) => (*w, *h),
|
||||
PixelProvider::NONE => (0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Recorder {
|
||||
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider<'_>, Box<dyn Error>>;
|
||||
}
|
||||
|
||||
pub trait BoxCloneCapturable {
|
||||
fn box_clone(&self) -> Box<dyn Capturable>;
|
||||
}
|
||||
|
||||
impl<T> BoxCloneCapturable for T
|
||||
where
|
||||
T: Clone + Capturable + 'static,
|
||||
{
|
||||
fn box_clone(&self) -> Box<dyn Capturable> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Capturable: Send + BoxCloneCapturable {
|
||||
/// Name of the Capturable, for example the window title, if it is a window.
|
||||
fn name(&self) -> String;
|
||||
/// Return x, y, width, height of the Capturable as floats relative to the absolute size of the
|
||||
/// screen. For example x=0.5, y=0.0, width=0.5, height=1.0 means the right half of the screen.
|
||||
fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box<dyn Error>>;
|
||||
/// Callback that is called right before input is simulated.
|
||||
/// Useful to focus the window on input.
|
||||
fn before_input(&mut self) -> Result<(), Box<dyn Error>>;
|
||||
/// Return a Recorder that can record the current capturable.
|
||||
fn recorder(&self, capture_cursor: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>>;
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn Capturable> {
|
||||
fn clone(&self) -> Self {
|
||||
self.box_clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
use hbb_common::regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Mutex;
|
||||
use std::{
|
||||
process::{Command, Output, Stdio},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use hbb_common::platform::linux::{get_wayland_displays, WaylandDisplayInfo};
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAYS: Mutex<Option<Arc<Displays>>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
pub struct Displays {
|
||||
pub primary: usize,
|
||||
pub displays: Vec<WaylandDisplayInfo>,
|
||||
}
|
||||
|
||||
// We need this helper to run commands with a timeout, as some commands may hang.
|
||||
// `kscreen-doctor -o` is known to hang when:
|
||||
// 1. On Archlinux, Both GNOME and KDE Plasma are installed.
|
||||
// 2. Run this command in a GNOME session.
|
||||
fn run_with_timeout(
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
timeout: Duration,
|
||||
label: &str,
|
||||
) -> Option<Output> {
|
||||
let mut child = Command::new(program)
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
break;
|
||||
}
|
||||
if start.elapsed() >= timeout {
|
||||
warn!("{} command timed out after {:?}", label, timeout);
|
||||
if let Err(e) = child.kill() {
|
||||
warn!("Failed to kill child process for '{}': {}", label, e);
|
||||
}
|
||||
if let Err(e) = child.wait() {
|
||||
warn!("Failed to wait for child process for '{}': {}", label, e);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
}
|
||||
|
||||
match child.wait_with_output() {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
warn!("{} command failed with status: {}", label, output.status);
|
||||
return None;
|
||||
}
|
||||
Some(output)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// There are some limitations with xrandr method:
|
||||
// 1. It only works when XWayland is running.
|
||||
// 2. The distro may not have xrandr installed by default.
|
||||
// 3. xrandr may not report "primary" in its output. eg. openSUSE Leap 15.6 KDE Plasma.
|
||||
fn try_xrandr_primary() -> Option<String> {
|
||||
let output = Command::new("xrandr").output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if line.contains("primary") && line.contains("connected") {
|
||||
if let Some(name) = line.split_whitespace().next() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn try_kscreen_primary() -> Option<String> {
|
||||
if !hbb_common::platform::linux::is_kde_session() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let output = run_with_timeout(
|
||||
"kscreen-doctor",
|
||||
&["-o"],
|
||||
COMMAND_TIMEOUT,
|
||||
"kscreen-doctor -o",
|
||||
)?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Remove ANSI color codes
|
||||
let re_ansi = Regex::new(r"\x1b\[[0-9;]*m").ok()?;
|
||||
let clean_text = re_ansi.replace_all(&text, "");
|
||||
|
||||
// Split the text into blocks, each starting with "Output:".
|
||||
// The first element of the split will be empty, so we skip it.
|
||||
for block in clean_text.split("Output:").skip(1) {
|
||||
// Check if this block describes the primary monitor.
|
||||
if block.contains("priority 1") {
|
||||
// The monitor name is the second piece of text in the block, after the ID.
|
||||
// e.g., " 1 eDP-1 enabled..." -> "eDP-1"
|
||||
if let Some(name) = block.split_whitespace().nth(1) {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn try_gdbus_primary() -> Option<String> {
|
||||
let output = run_with_timeout(
|
||||
"gdbus",
|
||||
&[
|
||||
"call",
|
||||
"--session",
|
||||
"--dest",
|
||||
"org.gnome.Mutter.DisplayConfig",
|
||||
"--object-path",
|
||||
"/org/gnome/Mutter/DisplayConfig",
|
||||
"--method",
|
||||
"org.gnome.Mutter.DisplayConfig.GetCurrentState",
|
||||
],
|
||||
COMMAND_TIMEOUT,
|
||||
"gdbus DisplayConfig.GetCurrentState",
|
||||
)?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Match logical monitor entries with primary=true
|
||||
// Pattern: (x, y, scale, transform, true, [('connector-name', ...), ...], ...)
|
||||
// Use regex to find entries where 5th field is true, then extract connector name
|
||||
// Example matched text: "(0, 0, 1.5, 0, true, [('HDMI-1', 'MHH', 'Monitor', '0x00000000')], ...)"
|
||||
let re = Regex::new(r"\([^()]*,\s*true,\s*\[\('([^']+)'").ok()?;
|
||||
|
||||
if let Some(captures) = re.captures(&text) {
|
||||
return captures.get(1).map(|m| m.as_str().to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_primary_monitor() -> Option<String> {
|
||||
try_xrandr_primary()
|
||||
.or_else(try_kscreen_primary)
|
||||
.or_else(try_gdbus_primary)
|
||||
}
|
||||
|
||||
pub fn get_displays() -> Arc<Displays> {
|
||||
let mut lock = DISPLAYS.lock().unwrap();
|
||||
match lock.as_ref() {
|
||||
Some(displays) => displays.clone(),
|
||||
None => match get_wayland_displays() {
|
||||
Ok(displays) => {
|
||||
let mut primary_index = None;
|
||||
if let Some(name) = get_primary_monitor() {
|
||||
for (i, display) in displays.iter().enumerate() {
|
||||
if display.name == name {
|
||||
primary_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
if primary_index.is_none() {
|
||||
for (i, display) in displays.iter().enumerate() {
|
||||
if display.x == 0 && display.y == 0 {
|
||||
primary_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let displays = Arc::new(Displays {
|
||||
primary: primary_index.unwrap_or(0),
|
||||
displays,
|
||||
});
|
||||
*lock = Some(displays.clone());
|
||||
displays
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to get wayland displays: {}", err);
|
||||
Arc::new(Displays {
|
||||
primary: 0,
|
||||
displays: Vec::new(),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear_wayland_displays_cache() {
|
||||
let _ = DISPLAYS.lock().unwrap().take();
|
||||
}
|
||||
|
||||
// Return (min_x, max_x, min_y, max_y)
|
||||
pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
||||
let wayland_displays = get_displays();
|
||||
let displays = &wayland_displays.displays;
|
||||
if displays.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// For compatibility, if only one display, we use the physical size for `uinput`.
|
||||
// Otherwise, we use the logical size for `uinput`.
|
||||
if displays.len() == 1 {
|
||||
let d = &displays[0];
|
||||
return Some((d.x, d.x + d.width, d.y, d.y + d.height));
|
||||
}
|
||||
|
||||
let mut min_x = i32::MAX;
|
||||
let mut min_y = i32::MAX;
|
||||
let mut max_x = i32::MIN;
|
||||
let mut max_y = i32::MIN;
|
||||
for d in displays.iter() {
|
||||
min_x = min_x.min(d.x);
|
||||
min_y = min_y.min(d.y);
|
||||
let size = if let Some(logical_size) = d.logical_size {
|
||||
logical_size
|
||||
} else {
|
||||
// When `logical_size` is None, we cannot obtain the correct desktop rectangle.
|
||||
// This may occur if the Wayland compositor does not provide logical size information,
|
||||
// or if display information is incomplete. We fall back to physical size, which provides
|
||||
// usable dimensions, but may not always be correct depending on compositor behavior.
|
||||
warn!(
|
||||
"Display at ({}, {}) is missing logical_size; falling back to physical size ({}, {}).",
|
||||
d.x, d.y, d.width, d.height
|
||||
);
|
||||
(d.width, d.height)
|
||||
};
|
||||
max_x = max_x.max(d.x + size.0);
|
||||
max_y = max_y.max(d.y + size.1);
|
||||
}
|
||||
Some((min_x, max_x, min_y, max_y))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,315 @@
|
||||
// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs
|
||||
// https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.RemoteDesktop.xml
|
||||
use dbus;
|
||||
#[allow(unused_imports)]
|
||||
use dbus::arg;
|
||||
use dbus::blocking;
|
||||
|
||||
pub trait OrgFreedesktopPortalRemoteDesktop {
|
||||
fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error>;
|
||||
fn select_devices(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error>;
|
||||
fn start(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
parent_window: &str,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error>;
|
||||
fn notify_pointer_motion(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_pointer_motion_absolute(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
stream: u32,
|
||||
x_: f64,
|
||||
y_: f64,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_pointer_button(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
button: i32,
|
||||
state: u32,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_pointer_axis(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_pointer_axis_discrete(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
axis: u32,
|
||||
steps: i32,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_keyboard_keycode(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
keycode: i32,
|
||||
state: u32,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_keyboard_keysym(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
keysym: i32,
|
||||
state: u32,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_touch_down(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
stream: u32,
|
||||
slot: u32,
|
||||
x_: f64,
|
||||
y_: f64,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_touch_motion(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
stream: u32,
|
||||
slot: u32,
|
||||
x_: f64,
|
||||
y_: f64,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn notify_touch_up(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
slot: u32,
|
||||
) -> Result<(), dbus::Error>;
|
||||
fn connect_to_eis(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<arg::OwnedFd, dbus::Error>;
|
||||
fn available_device_types(&self) -> Result<u32, dbus::Error>;
|
||||
fn version(&self) -> Result<u32, dbus::Error>;
|
||||
}
|
||||
|
||||
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>>
|
||||
OrgFreedesktopPortalRemoteDesktop for blocking::Proxy<'a, C>
|
||||
{
|
||||
fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"CreateSession",
|
||||
(options,),
|
||||
)
|
||||
.and_then(|r: (dbus::Path<'static>,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn select_devices(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"SelectDevices",
|
||||
(session_handle, options),
|
||||
)
|
||||
.and_then(|r: (dbus::Path<'static>,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn start(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
parent_window: &str,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"Start",
|
||||
(session_handle, parent_window, options),
|
||||
)
|
||||
.and_then(|r: (dbus::Path<'static>,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn notify_pointer_motion(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyPointerMotion",
|
||||
(session_handle, options, dx, dy),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_pointer_motion_absolute(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
stream: u32,
|
||||
x_: f64,
|
||||
y_: f64,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyPointerMotionAbsolute",
|
||||
(session_handle, options, stream, x_, y_),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_pointer_button(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
button: i32,
|
||||
state: u32,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyPointerButton",
|
||||
(session_handle, options, button, state),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_pointer_axis(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyPointerAxis",
|
||||
(session_handle, options, dx, dy),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_pointer_axis_discrete(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
axis: u32,
|
||||
steps: i32,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyPointerAxisDiscrete",
|
||||
(session_handle, options, axis, steps),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_keyboard_keycode(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
keycode: i32,
|
||||
state: u32,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyKeyboardKeycode",
|
||||
(session_handle, options, keycode, state),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_keyboard_keysym(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
keysym: i32,
|
||||
state: u32,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyKeyboardKeysym",
|
||||
(session_handle, options, keysym, state),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_touch_down(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
stream: u32,
|
||||
slot: u32,
|
||||
x_: f64,
|
||||
y_: f64,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyTouchDown",
|
||||
(session_handle, options, stream, slot, x_, y_),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_touch_motion(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
stream: u32,
|
||||
slot: u32,
|
||||
x_: f64,
|
||||
y_: f64,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyTouchMotion",
|
||||
(session_handle, options, stream, slot, x_, y_),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_touch_up(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
slot: u32,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"NotifyTouchUp",
|
||||
(session_handle, options, slot),
|
||||
)
|
||||
}
|
||||
|
||||
fn connect_to_eis(
|
||||
&self,
|
||||
session_handle: &dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<arg::OwnedFd, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"ConnectToEIS",
|
||||
(session_handle, options),
|
||||
)
|
||||
.and_then(|r: (arg::OwnedFd,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn available_device_types(&self) -> Result<u32, dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
|
||||
&self,
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"AvailableDeviceTypes",
|
||||
)
|
||||
}
|
||||
|
||||
fn version(&self) -> Result<u32, dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
|
||||
&self,
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"version",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs
|
||||
// https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.Request.xml
|
||||
use dbus;
|
||||
#[allow(unused_imports)]
|
||||
use dbus::arg;
|
||||
use dbus::blocking;
|
||||
|
||||
pub trait OrgFreedesktopPortalRequest {
|
||||
fn close(&self) -> Result<(), dbus::Error>;
|
||||
}
|
||||
|
||||
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopPortalRequest
|
||||
for blocking::Proxy<'a, C>
|
||||
{
|
||||
fn close(&self) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.portal.Request", "Close", ())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopPortalRequestResponse {
|
||||
pub response: u32,
|
||||
pub results: arg::PropMap,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopPortalRequestResponse {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.response, i);
|
||||
arg::RefArg::append(&self.results, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopPortalRequestResponse {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(OrgFreedesktopPortalRequestResponse {
|
||||
response: i.read()?,
|
||||
results: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse {
|
||||
const NAME: &'static str = "Response";
|
||||
const INTERFACE: &'static str = "org.freedesktop.portal.Request";
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs
|
||||
// https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.ScreenCast.xml
|
||||
use dbus;
|
||||
#[allow(unused_imports)]
|
||||
use dbus::arg;
|
||||
use dbus::blocking;
|
||||
|
||||
pub trait OrgFreedesktopPortalScreenCast {
|
||||
fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error>;
|
||||
fn select_sources(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error>;
|
||||
fn start(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
parent_window: &str,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error>;
|
||||
fn open_pipe_wire_remote(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<arg::OwnedFd, dbus::Error>;
|
||||
fn available_source_types(&self) -> Result<u32, dbus::Error>;
|
||||
fn available_cursor_modes(&self) -> Result<u32, dbus::Error>;
|
||||
fn version(&self) -> Result<u32, dbus::Error>;
|
||||
}
|
||||
|
||||
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>>
|
||||
OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C>
|
||||
{
|
||||
fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
"CreateSession",
|
||||
(options,),
|
||||
)
|
||||
.map(|r: (dbus::Path<'static>,)| r.0)
|
||||
}
|
||||
|
||||
fn select_sources(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
"SelectSources",
|
||||
(session_handle, options),
|
||||
)
|
||||
.map(|r: (dbus::Path<'static>,)| r.0)
|
||||
}
|
||||
|
||||
fn start(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
parent_window: &str,
|
||||
options: arg::PropMap,
|
||||
) -> Result<dbus::Path<'static>, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
"Start",
|
||||
(session_handle, parent_window, options),
|
||||
)
|
||||
.map(|r: (dbus::Path<'static>,)| r.0)
|
||||
}
|
||||
|
||||
fn open_pipe_wire_remote(
|
||||
&self,
|
||||
session_handle: dbus::Path,
|
||||
options: arg::PropMap,
|
||||
) -> Result<arg::OwnedFd, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
"OpenPipeWireRemote",
|
||||
(session_handle, options),
|
||||
)
|
||||
.map(|r: (arg::OwnedFd,)| r.0)
|
||||
}
|
||||
|
||||
fn available_source_types(&self) -> Result<u32, dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
|
||||
&self,
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
"AvailableSourceTypes",
|
||||
)
|
||||
}
|
||||
|
||||
fn available_cursor_modes(&self) -> Result<u32, dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
|
||||
&self,
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
"AvailableCursorModes",
|
||||
)
|
||||
}
|
||||
|
||||
fn version(&self) -> Result<u32, dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
|
||||
&self,
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
"version",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::ffi::*;
|
||||
use super::Display;
|
||||
use hbb_common::libc;
|
||||
use std::{io, ptr, slice};
|
||||
|
||||
pub struct Capturer {
|
||||
display: Display,
|
||||
shmid: i32,
|
||||
xcbid: u32,
|
||||
buffer: *const u8,
|
||||
|
||||
size: usize,
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
// Calculate dimensions.
|
||||
|
||||
let pixel_width = display.pixfmt().bytes_per_pixel();
|
||||
let rect = display.rect();
|
||||
let size = (rect.w as usize) * (rect.h as usize) * pixel_width;
|
||||
|
||||
// Create a shared memory segment.
|
||||
|
||||
let shmid = unsafe {
|
||||
libc::shmget(
|
||||
libc::IPC_PRIVATE,
|
||||
size,
|
||||
// Everyone can do anything.
|
||||
libc::IPC_CREAT | 0o777,
|
||||
)
|
||||
};
|
||||
|
||||
if shmid == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// Attach the segment to a readable address.
|
||||
|
||||
let buffer = unsafe { libc::shmat(shmid, ptr::null(), libc::SHM_RDONLY) } as *mut u8;
|
||||
|
||||
if buffer as isize == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// Attach the segment to XCB.
|
||||
|
||||
let server = display.server().raw();
|
||||
let xcbid = unsafe { xcb_generate_id(server) };
|
||||
unsafe {
|
||||
xcb_shm_attach(
|
||||
server,
|
||||
xcbid,
|
||||
shmid as u32,
|
||||
0, // False, i.e. not read-only.
|
||||
);
|
||||
}
|
||||
|
||||
let c = Capturer {
|
||||
display,
|
||||
shmid,
|
||||
xcbid,
|
||||
buffer,
|
||||
size,
|
||||
saved_raw_data: Vec::new(),
|
||||
};
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
pub fn display(&self) -> &Display {
|
||||
&self.display
|
||||
}
|
||||
|
||||
fn get_image(&self) {
|
||||
let rect = self.display.rect();
|
||||
unsafe {
|
||||
let request = xcb_shm_get_image_unchecked(
|
||||
self.display.server().raw(),
|
||||
self.display.root(),
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.w,
|
||||
rect.h,
|
||||
!0,
|
||||
XCB_IMAGE_FORMAT_Z_PIXMAP,
|
||||
self.xcbid,
|
||||
0,
|
||||
);
|
||||
let response =
|
||||
xcb_shm_get_image_reply(self.display.server().raw(), request, ptr::null_mut());
|
||||
libc::free(response as *mut _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame<'b>(&'b mut self) -> std::io::Result<&'b [u8]> {
|
||||
self.get_image();
|
||||
let result = unsafe { slice::from_raw_parts(self.buffer, self.size) };
|
||||
crate::would_block_if_equal(&mut self.saved_raw_data, result)?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Capturer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
// Detach segment from XCB.
|
||||
xcb_shm_detach(self.display.server().raw(), self.xcbid);
|
||||
// Detach segment from our space.
|
||||
libc::shmdt(self.buffer as *mut _);
|
||||
// Destroy the shared memory segment.
|
||||
libc::shmctl(self.shmid, libc::IPC_RMID, ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::ffi::*;
|
||||
use super::Server;
|
||||
use crate::Pixfmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Display {
|
||||
server: Rc<Server>,
|
||||
default: bool,
|
||||
rect: Rect,
|
||||
root: xcb_window_t,
|
||||
name: String,
|
||||
pixfmt: Pixfmt,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct Rect {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub w: u16,
|
||||
pub h: u16,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub unsafe fn new(
|
||||
server: Rc<Server>,
|
||||
default: bool,
|
||||
rect: Rect,
|
||||
root: xcb_window_t,
|
||||
name: String,
|
||||
pixfmt: Pixfmt,
|
||||
) -> Display {
|
||||
Display {
|
||||
server,
|
||||
default,
|
||||
rect,
|
||||
root,
|
||||
name,
|
||||
pixfmt,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server(&self) -> &Rc<Server> {
|
||||
&self.server
|
||||
}
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.default
|
||||
}
|
||||
pub fn rect(&self) -> Rect {
|
||||
self.rect
|
||||
}
|
||||
pub fn w(&self) -> usize {
|
||||
self.rect.w as _
|
||||
}
|
||||
pub fn h(&self) -> usize {
|
||||
self.rect.h as _
|
||||
}
|
||||
pub fn root(&self) -> xcb_window_t {
|
||||
self.root
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn pixfmt(&self) -> Pixfmt {
|
||||
self.pixfmt
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
use hbb_common::libc::c_void;
|
||||
|
||||
#[link(name = "xcb")]
|
||||
#[link(name = "xcb-shm")]
|
||||
#[link(name = "xcb-randr")]
|
||||
extern "C" {
|
||||
pub fn xcb_connect(displayname: *const i8, screenp: *mut i32) -> *mut xcb_connection_t;
|
||||
|
||||
pub fn xcb_disconnect(c: *mut xcb_connection_t);
|
||||
|
||||
pub fn xcb_connection_has_error(c: *mut xcb_connection_t) -> i32;
|
||||
|
||||
pub fn xcb_get_setup(c: *mut xcb_connection_t) -> *const xcb_setup_t;
|
||||
|
||||
pub fn xcb_setup_roots_iterator(r: *const xcb_setup_t) -> xcb_screen_iterator_t;
|
||||
|
||||
pub fn xcb_screen_next(i: *mut xcb_screen_iterator_t);
|
||||
|
||||
pub fn xcb_generate_id(c: *mut xcb_connection_t) -> u32;
|
||||
|
||||
pub fn xcb_shm_attach(
|
||||
c: *mut xcb_connection_t,
|
||||
shmseg: xcb_shm_seg_t,
|
||||
shmid: u32,
|
||||
read_only: u8,
|
||||
) -> xcb_void_cookie_t;
|
||||
|
||||
pub fn xcb_shm_detach(c: *mut xcb_connection_t, shmseg: xcb_shm_seg_t) -> xcb_void_cookie_t;
|
||||
|
||||
pub fn xcb_shm_get_image_unchecked(
|
||||
c: *mut xcb_connection_t,
|
||||
drawable: xcb_drawable_t,
|
||||
x: i16,
|
||||
y: i16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
plane_mask: u32,
|
||||
format: u8,
|
||||
shmseg: xcb_shm_seg_t,
|
||||
offset: u32,
|
||||
) -> xcb_shm_get_image_cookie_t;
|
||||
|
||||
pub fn xcb_shm_get_image_reply(
|
||||
c: *mut xcb_connection_t,
|
||||
cookie: xcb_shm_get_image_cookie_t,
|
||||
e: *mut *mut xcb_generic_error_t,
|
||||
) -> *mut xcb_shm_get_image_reply_t;
|
||||
|
||||
pub fn xcb_randr_get_monitors_unchecked(
|
||||
c: *mut xcb_connection_t,
|
||||
window: xcb_window_t,
|
||||
get_active: u8,
|
||||
) -> xcb_randr_get_monitors_cookie_t;
|
||||
|
||||
pub fn xcb_randr_get_monitors_reply(
|
||||
c: *mut xcb_connection_t,
|
||||
cookie: xcb_randr_get_monitors_cookie_t,
|
||||
e: *mut *mut xcb_generic_error_t,
|
||||
) -> *mut xcb_randr_get_monitors_reply_t;
|
||||
|
||||
pub fn xcb_randr_get_monitors_monitors_iterator(
|
||||
r: *const xcb_randr_get_monitors_reply_t,
|
||||
) -> xcb_randr_monitor_info_iterator_t;
|
||||
|
||||
pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t);
|
||||
|
||||
pub fn xcb_get_atom_name(
|
||||
c: *mut xcb_connection_t,
|
||||
atom: xcb_atom_t,
|
||||
) -> xcb_get_atom_name_cookie_t;
|
||||
|
||||
pub fn xcb_get_atom_name_reply(
|
||||
c: *mut xcb_connection_t,
|
||||
cookie: xcb_get_atom_name_cookie_t,
|
||||
e: *mut *mut xcb_generic_error_t,
|
||||
) -> *const xcb_get_atom_name_reply_t;
|
||||
|
||||
pub fn xcb_get_atom_name_name(reply: *const xcb_get_atom_name_request_t) -> *const u8;
|
||||
|
||||
pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32;
|
||||
|
||||
pub fn xcb_shm_query_version(c: *mut xcb_connection_t) -> xcb_shm_query_version_cookie_t;
|
||||
|
||||
pub fn xcb_shm_query_version_reply(
|
||||
c: *mut xcb_connection_t,
|
||||
cookie: xcb_shm_query_version_cookie_t,
|
||||
e: *mut *mut xcb_generic_error_t,
|
||||
) -> *const xcb_shm_query_version_reply_t;
|
||||
|
||||
pub fn xcb_get_geometry_unchecked(
|
||||
c: *mut xcb_connection_t,
|
||||
drawable: xcb_drawable_t,
|
||||
) -> xcb_get_geometry_cookie_t;
|
||||
|
||||
pub fn xcb_get_geometry_reply(
|
||||
c: *mut xcb_connection_t,
|
||||
cookie: xcb_get_geometry_cookie_t,
|
||||
e: *mut *mut xcb_generic_error_t,
|
||||
) -> *mut xcb_get_geometry_reply_t;
|
||||
|
||||
}
|
||||
|
||||
pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2;
|
||||
|
||||
pub type xcb_atom_t = u32;
|
||||
pub type xcb_connection_t = c_void;
|
||||
pub type xcb_window_t = u32;
|
||||
pub type xcb_keycode_t = u8;
|
||||
pub type xcb_visualid_t = u32;
|
||||
pub type xcb_timestamp_t = u32;
|
||||
pub type xcb_colormap_t = u32;
|
||||
pub type xcb_shm_seg_t = u32;
|
||||
pub type xcb_drawable_t = u32;
|
||||
pub type xcb_get_atom_name_cookie_t = u32;
|
||||
pub type xcb_get_atom_name_reply_t = u32;
|
||||
pub type xcb_get_atom_name_request_t = xcb_get_atom_name_reply_t;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_setup_t {
|
||||
pub status: u8,
|
||||
pub pad0: u8,
|
||||
pub protocol_major_version: u16,
|
||||
pub protocol_minor_version: u16,
|
||||
pub length: u16,
|
||||
pub release_number: u32,
|
||||
pub resource_id_base: u32,
|
||||
pub resource_id_mask: u32,
|
||||
pub motion_buffer_size: u32,
|
||||
pub vendor_len: u16,
|
||||
pub maximum_request_length: u16,
|
||||
pub roots_len: u8,
|
||||
pub pixmap_formats_len: u8,
|
||||
pub image_byte_order: u8,
|
||||
pub bitmap_format_bit_order: u8,
|
||||
pub bitmap_format_scanline_unit: u8,
|
||||
pub bitmap_format_scanline_pad: u8,
|
||||
pub min_keycode: xcb_keycode_t,
|
||||
pub max_keycode: xcb_keycode_t,
|
||||
pub pad1: [u8; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_screen_iterator_t {
|
||||
pub data: *mut xcb_screen_t,
|
||||
pub rem: i32,
|
||||
pub index: i32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_screen_t {
|
||||
pub root: xcb_window_t,
|
||||
pub default_colormap: xcb_colormap_t,
|
||||
pub white_pixel: u32,
|
||||
pub black_pixel: u32,
|
||||
pub current_input_masks: u32,
|
||||
pub width_in_pixels: u16,
|
||||
pub height_in_pixels: u16,
|
||||
pub width_in_millimeters: u16,
|
||||
pub height_in_millimeters: u16,
|
||||
pub min_installed_maps: u16,
|
||||
pub max_installed_maps: u16,
|
||||
pub root_visual: xcb_visualid_t,
|
||||
pub backing_stores: u8,
|
||||
pub save_unders: u8,
|
||||
pub root_depth: u8,
|
||||
pub allowed_depths_len: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_randr_monitor_info_iterator_t {
|
||||
pub data: *mut xcb_randr_monitor_info_t,
|
||||
pub rem: i32,
|
||||
pub index: i32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_randr_monitor_info_t {
|
||||
pub name: xcb_atom_t,
|
||||
pub primary: u8,
|
||||
pub automatic: u8,
|
||||
pub n_output: u16,
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub width_mm: u32,
|
||||
pub height_mm: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct xcb_randr_get_monitors_cookie_t {
|
||||
pub sequence: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct xcb_shm_get_image_cookie_t {
|
||||
pub sequence: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct xcb_void_cookie_t {
|
||||
pub sequence: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct xcb_get_geometry_cookie_t {
|
||||
pub sequence: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_generic_error_t {
|
||||
pub response_type: u8,
|
||||
pub error_code: u8,
|
||||
pub sequence: u16,
|
||||
pub resource_id: u32,
|
||||
pub minor_code: u16,
|
||||
pub major_code: u8,
|
||||
pub pad0: u8,
|
||||
pub pad: [u32; 5],
|
||||
pub full_sequence: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_shm_get_image_reply_t {
|
||||
pub response_type: u8,
|
||||
pub depth: u8,
|
||||
pub sequence: u16,
|
||||
pub length: u32,
|
||||
pub visual: xcb_visualid_t,
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_randr_get_monitors_reply_t {
|
||||
pub response_type: u8,
|
||||
pub pad0: u8,
|
||||
pub sequence: u16,
|
||||
pub length: u32,
|
||||
pub timestamp: xcb_timestamp_t,
|
||||
pub n_monitors: u32,
|
||||
pub n_outputs: u32,
|
||||
pub pad1: [u8; 12],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_shm_query_version_cookie_t {
|
||||
pub sequence: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_shm_query_version_reply_t {
|
||||
pub response_type: u8,
|
||||
pub shared_pixmaps: u8,
|
||||
pub sequence: u16,
|
||||
pub length: u32,
|
||||
pub major_version: u16,
|
||||
pub minor_version: u16,
|
||||
pub uid: u16,
|
||||
pub gid: u16,
|
||||
pub pixmap_format: u8,
|
||||
pub pad0: [u8; 15],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xcb_get_geometry_reply_t {
|
||||
pub response_type: u8,
|
||||
pub depth: u8,
|
||||
pub sequence: u16,
|
||||
pub length: u32,
|
||||
pub root: xcb_window_t,
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub border_width: u16,
|
||||
pub pad0: [u8; 2],
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use std::ffi::CString;
|
||||
use std::ptr;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Pixfmt;
|
||||
use hbb_common::libc;
|
||||
|
||||
use super::ffi::*;
|
||||
use super::{Display, Rect, Server};
|
||||
|
||||
//TODO: Do I have to free the displays?
|
||||
|
||||
pub struct DisplayIter {
|
||||
outer: xcb_screen_iterator_t,
|
||||
inner: Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)>,
|
||||
server: Rc<Server>,
|
||||
}
|
||||
|
||||
impl DisplayIter {
|
||||
pub unsafe fn new(server: Rc<Server>) -> DisplayIter {
|
||||
let mut outer = xcb_setup_roots_iterator(server.setup());
|
||||
let inner = Self::next_screen(&mut outer, &server);
|
||||
DisplayIter {
|
||||
outer,
|
||||
inner,
|
||||
server,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_screen(
|
||||
outer: &mut xcb_screen_iterator_t,
|
||||
server: &Server,
|
||||
) -> Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)> {
|
||||
if outer.rem == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let root = (*outer.data).root;
|
||||
|
||||
let cookie = xcb_randr_get_monitors_unchecked(
|
||||
server.raw(),
|
||||
root,
|
||||
1, //TODO: I don't know if this should be true or false.
|
||||
);
|
||||
|
||||
let response = xcb_randr_get_monitors_reply(server.raw(), cookie, ptr::null_mut());
|
||||
|
||||
let inner = xcb_randr_get_monitors_monitors_iterator(response);
|
||||
|
||||
libc::free(response as *mut _);
|
||||
xcb_screen_next(outer);
|
||||
|
||||
Some((inner, root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for DisplayIter {
|
||||
type Item = Display;
|
||||
|
||||
fn next(&mut self) -> Option<Display> {
|
||||
loop {
|
||||
if let Some((ref mut inner, root)) = self.inner {
|
||||
// If there is something in the current screen, return that.
|
||||
if inner.rem != 0 {
|
||||
unsafe {
|
||||
let data = &*inner.data;
|
||||
let name = get_atom_name(self.server.raw(), data.name);
|
||||
let pixfmt = get_pixfmt(self.server.raw(), root).unwrap_or(Pixfmt::BGRA);
|
||||
let display = Display::new(
|
||||
self.server.clone(),
|
||||
data.primary != 0,
|
||||
Rect {
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
w: data.width,
|
||||
h: data.height,
|
||||
},
|
||||
root,
|
||||
name,
|
||||
pixfmt,
|
||||
);
|
||||
|
||||
xcb_randr_monitor_info_next(inner);
|
||||
return Some(display);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there is no current screen, the screen iterator is empty.
|
||||
return None;
|
||||
}
|
||||
|
||||
// The current screen was empty, so try the next screen.
|
||||
self.inner = Self::next_screen(&mut self.outer, &self.server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String {
|
||||
let empty = "".to_owned();
|
||||
if atom == 0 {
|
||||
return empty;
|
||||
}
|
||||
unsafe {
|
||||
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
|
||||
let reply = xcb_get_atom_name_reply(conn, xcb_get_atom_name(conn, atom), &mut e as _);
|
||||
if reply == std::ptr::null() {
|
||||
return empty;
|
||||
}
|
||||
let length = xcb_get_atom_name_name_length(reply);
|
||||
let name = xcb_get_atom_name_name(reply);
|
||||
let mut v = vec![0u8; length as _];
|
||||
std::ptr::copy_nonoverlapping(name as _, v.as_mut_ptr(), length as _);
|
||||
libc::free(reply as *mut _);
|
||||
if let Ok(s) = CString::new(v) {
|
||||
return s.to_string_lossy().to_string();
|
||||
}
|
||||
empty
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_pixfmt(conn: *mut xcb_connection_t, root: xcb_window_t) -> Option<Pixfmt> {
|
||||
let geo_cookie = xcb_get_geometry_unchecked(conn, root);
|
||||
let geo = xcb_get_geometry_reply(conn, geo_cookie, ptr::null_mut());
|
||||
if geo.is_null() {
|
||||
return None;
|
||||
}
|
||||
let depth = (*geo).depth;
|
||||
libc::free(geo as _);
|
||||
// now only support little endian
|
||||
// https://github.com/FFmpeg/FFmpeg/blob/a9c05eb657d0d05f3ac79fe9973581a41b265a5e/libavdevice/xcbgrab.c#L519
|
||||
match depth {
|
||||
16 => Some(Pixfmt::RGB565LE),
|
||||
32 => Some(Pixfmt::BGRA),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pub use self::capturer::*;
|
||||
pub use self::display::*;
|
||||
pub use self::iter::*;
|
||||
pub use self::server::*;
|
||||
|
||||
mod capturer;
|
||||
mod display;
|
||||
mod ffi;
|
||||
mod iter;
|
||||
mod server;
|
||||
@@ -0,0 +1,146 @@
|
||||
use hbb_common::libc;
|
||||
use std::ptr;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::ffi::*;
|
||||
use super::DisplayIter;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Server {
|
||||
raw: *mut xcb_connection_t,
|
||||
screenp: i32,
|
||||
setup: *const xcb_setup_t,
|
||||
}
|
||||
|
||||
/*
|
||||
use std::cell::RefCell;
|
||||
thread_local! {
|
||||
static SERVER: RefCell<Option<Rc<Server>>> = RefCell::new(None);
|
||||
}
|
||||
*/
|
||||
|
||||
impl Server {
|
||||
pub fn displays(slf: Rc<Server>) -> DisplayIter {
|
||||
unsafe { DisplayIter::new(slf) }
|
||||
}
|
||||
|
||||
pub fn default() -> Result<Rc<Server>, Error> {
|
||||
Ok(Rc::new(Server::connect(ptr::null())?))
|
||||
/*
|
||||
let mut res = Err(Error::from(0));
|
||||
SERVER.with(|xdo| {
|
||||
if let Ok(mut server) = xdo.try_borrow_mut() {
|
||||
if server.is_some() {
|
||||
unsafe {
|
||||
if 0 != xcb_connection_has_error(server.as_ref().unwrap().raw) {
|
||||
*server = None;
|
||||
println!("Reset x11 connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
if server.is_none() {
|
||||
println!("New x11 connection");
|
||||
match Server::connect(ptr::null()) {
|
||||
Ok(s) => {
|
||||
let s = Rc::new(s);
|
||||
res = Ok(s.clone());
|
||||
*server = Some(s);
|
||||
}
|
||||
Err(err) => {
|
||||
res = Err(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = Ok(server.as_ref().map(|x| x.clone()).unwrap());
|
||||
}
|
||||
}
|
||||
});
|
||||
res
|
||||
*/
|
||||
}
|
||||
|
||||
pub fn connect(addr: *const i8) -> Result<Server, Error> {
|
||||
unsafe {
|
||||
let mut screenp = 0;
|
||||
let raw = xcb_connect(addr, &mut screenp);
|
||||
|
||||
let error = xcb_connection_has_error(raw);
|
||||
if error != 0 {
|
||||
xcb_disconnect(raw);
|
||||
Err(Error::from(error))
|
||||
} else {
|
||||
let setup = xcb_get_setup(raw);
|
||||
Ok(Server {
|
||||
raw,
|
||||
screenp,
|
||||
setup,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> *mut xcb_connection_t {
|
||||
self.raw
|
||||
}
|
||||
pub fn screenp(&self) -> i32 {
|
||||
self.screenp
|
||||
}
|
||||
pub fn setup(&self) -> *const xcb_setup_t {
|
||||
self.setup
|
||||
}
|
||||
pub fn get_shm_status(&self) -> Result<(), Error> {
|
||||
unsafe { check_x11_shm_available(self.raw) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error> {
|
||||
let cookie = xcb_shm_query_version(c);
|
||||
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
|
||||
let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _);
|
||||
if reply.is_null() {
|
||||
// TODO: Should separate SHM disabled from SHM not supported?
|
||||
return Err(Error::UnsupportedExtension);
|
||||
} else {
|
||||
// https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229
|
||||
libc::free(reply as *mut _);
|
||||
if e.is_null() {
|
||||
return Ok(());
|
||||
} else {
|
||||
libc::free(e as *mut _);
|
||||
// TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here?
|
||||
return Err(Error::Generic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
xcb_disconnect(self.raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Error {
|
||||
Generic,
|
||||
UnsupportedExtension,
|
||||
InsufficientMemory,
|
||||
RequestTooLong,
|
||||
ParseError,
|
||||
InvalidScreen,
|
||||
}
|
||||
|
||||
impl From<i32> for Error {
|
||||
fn from(x: i32) -> Error {
|
||||
use self::Error::*;
|
||||
match x {
|
||||
2 => UnsupportedExtension,
|
||||
3 => InsufficientMemory,
|
||||
4 => RequestTooLong,
|
||||
5 => ParseError,
|
||||
6 => InvalidScreen,
|
||||
_ => Generic,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user