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> = RwLock::new(None); static ref MAIN_SERVICE_CTX: RwLock> = RwLock::new(None); // MainService -> video service / audio service / info static ref VIDEO_RAW: Mutex = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT)); static ref AUDIO_RAW: Mutex = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT)); static ref NDK_CONTEXT_INITED: Mutex = Default::default(); static ref MEDIA_CODEC_INFOS: RwLock> = RwLock::new(None); static ref CLIPBOARD_MANAGER: RwLock> = RwLock::new(None); static ref CLIPBOARDS_HOST: Mutex> = Mutex::new(None); static ref CLIPBOARDS_CLIENT: Mutex> = 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, 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, last: &mut Vec) -> 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, last: &mut Vec) -> Option<()> { VIDEO_RAW.lock().ok()?.take(dst, last) } pub fn get_audio_raw<'a>(dst: &mut Vec, last: &mut Vec) -> Option<()> { AUDIO_RAW.lock().ok()?.take(dst, last) } pub fn get_clipboards(client: bool) -> Option { 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, // api 29+ pub mime_type: String, pub surface: bool, pub nv12: bool, #[serde(default)] pub low_latency: Option, // 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, } #[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::(&info) { *MEDIA_CODEC_INFOS.write().unwrap() = Some(infos); } } } pub fn get_codec_info() -> Option { 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(name: S, sig: T, args: &[JValue]) -> JniResult<()> where S: Into, T: Into + AsRef, { 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_service_clipboard(enable: bool) -> JniResult<()> { _call_clipboard_manager( "rustEnableServiceClipboard", "(Z)V", &[JValue::Bool(jboolean::from(enable))], ) } 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 { 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 { 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; } // 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() }