use super::{BackendSnapshot, MediaSource, VideoError, VideoState};
use jni::objects::{GlobalRef, JObject, JValue};
use std::time::Duration;
const FRAME_OPTION_CLOSEST: i32 = 3;
pub(crate) struct AndroidVideoBackend {
player: GlobalRef,
retriever: GlobalRef,
asset_descriptor: Option<GlobalRef>,
width: u32,
height: u32,
duration: Option<Duration>,
last_position_ms: i32,
looping: bool,
muted: bool,
volume: f32,
}
impl AndroidVideoBackend {
pub(crate) fn open(source: &MediaSource) -> Result<Self, VideoError> {
let jvm = crate::android::get_jvm()
.ok_or_else(|| VideoError::Backend("Android JVM is unavailable".to_string()))?;
let mut env = jvm
.attach_current_thread()
.map_err(|error| VideoError::Backend(error.to_string()))?;
let player = env
.new_object("android/media/MediaPlayer", "()V", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?;
let retriever = env
.new_object("android/media/MediaMetadataRetriever", "()V", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?;
let asset_descriptor = match source {
MediaSource::Asset(path) => {
let activity = crate::android::get_activity().ok_or_else(|| {
VideoError::Backend("Android activity is unavailable".to_string())
})?;
let asset_manager = env
.call_method(
activity.as_obj(),
"getAssets",
"()Landroid/content/res/AssetManager;",
&[],
)
.map_err(|error| VideoError::Backend(error.to_string()))?
.l()
.map_err(|error| VideoError::Backend(error.to_string()))?;
let asset_path = env
.new_string(normalize_asset_path(path))
.map_err(|error| VideoError::Backend(error.to_string()))?;
let asset_path_obj = JObject::from(asset_path);
let descriptor = env
.call_method(
&asset_manager,
"openFd",
"(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;",
&[JValue::Object(&asset_path_obj)],
)
.map_err(|error| {
VideoError::Backend(format!(
"failed to open Android video asset '{}': {error}; video assets must be stored uncompressed",
path
))
})?
.l()
.map_err(|error| VideoError::Backend(error.to_string()))?;
let file_descriptor = env
.call_method(
&descriptor,
"getFileDescriptor",
"()Ljava/io/FileDescriptor;",
&[],
)
.map_err(|error| VideoError::Backend(error.to_string()))?
.l()
.map_err(|error| VideoError::Backend(error.to_string()))?;
let start_offset = env
.call_method(&descriptor, "getStartOffset", "()J", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?
.j()
.map_err(|error| VideoError::Backend(error.to_string()))?;
let length = env
.call_method(&descriptor, "getLength", "()J", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?
.j()
.map_err(|error| VideoError::Backend(error.to_string()))?;
let args = [
JValue::Object(&file_descriptor),
JValue::Long(start_offset),
JValue::Long(length),
];
env.call_method(
&player,
"setDataSource",
"(Ljava/io/FileDescriptor;JJ)V",
&args,
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
env.call_method(
&retriever,
"setDataSource",
"(Ljava/io/FileDescriptor;JJ)V",
&args,
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
Some(
env.new_global_ref(&descriptor)
.map_err(|error| VideoError::Backend(error.to_string()))?,
)
}
_ => {
let uri = env
.new_string(source.uri())
.map_err(|error| VideoError::Backend(error.to_string()))?;
let uri_obj = JObject::from(uri);
env.call_method(
&player,
"setDataSource",
"(Ljava/lang/String;)V",
&[JValue::Object(&uri_obj)],
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
env.call_method(
&retriever,
"setDataSource",
"(Ljava/lang/String;)V",
&[JValue::Object(&uri_obj)],
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
None
}
};
env.call_method(&player, "prepare", "()V", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?;
let width = metadata_int(&mut env, &retriever, 18)?;
let height = metadata_int(&mut env, &retriever, 19)?;
let duration_ms = metadata_int(&mut env, &retriever, 9)?;
let player_ref = env
.new_global_ref(&player)
.map_err(|error| VideoError::Backend(error.to_string()))?;
let retriever_ref = env
.new_global_ref(&retriever)
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(Self {
player: player_ref,
retriever: retriever_ref,
asset_descriptor,
width: width.max(1) as u32,
height: height.max(1) as u32,
duration: (duration_ms > 0).then(|| Duration::from_millis(duration_ms as u64)),
last_position_ms: -1,
looping: false,
muted: false,
volume: 1.0,
})
}
pub(crate) fn close(&mut self) {
if let Some(jvm) = crate::android::get_jvm()
&& let Ok(mut env) = jvm.attach_current_thread()
{
let _ = env.call_method(&self.player, "release", "()V", &[]);
let _ = env.call_method(&self.retriever, "release", "()V", &[]);
if let Some(descriptor) = &self.asset_descriptor {
let _ = env.call_method(descriptor, "close", "()V", &[]);
}
}
}
pub(crate) fn update(&mut self) -> Result<BackendSnapshot, VideoError> {
let jvm = crate::android::get_jvm()
.ok_or_else(|| VideoError::Backend("Android JVM is unavailable".to_string()))?;
let mut env = jvm
.attach_current_thread()
.map_err(|error| VideoError::Backend(error.to_string()))?;
let position_ms = call_int(&mut env, &self.player, "getCurrentPosition", "()I", &[])?;
let playing = call_bool(&mut env, &self.player, "isPlaying", "()Z", &[])?;
let ended = !playing
&& position_ms > 0
&& self
.duration
.map(|duration| (position_ms as u128) >= duration.as_millis().saturating_sub(80))
.unwrap_or(false);
let rgba = if position_ms != self.last_position_ms {
self.last_position_ms = position_ms;
self.frame_rgba(&mut env, position_ms)?
} else {
None
};
let state = if ended {
VideoState::Ended
} else if playing {
VideoState::Playing
} else if position_ms > 0 {
VideoState::Paused
} else {
VideoState::Ready
};
Ok(BackendSnapshot {
state,
duration: self.duration,
position: Duration::from_millis(position_ms.max(0) as u64),
dimensions: Some((self.width, self.height)),
rgba,
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
external_video: None,
})
}
fn frame_rgba(
&self,
env: &mut jni::JNIEnv<'_>,
position_ms: i32,
) -> Result<Option<Vec<u8>>, VideoError> {
let bitmap = env
.call_method(
&self.retriever,
"getFrameAtTime",
"(JI)Landroid/graphics/Bitmap;",
&[
JValue::Long(position_ms.max(0) as i64 * 1000),
JValue::Int(FRAME_OPTION_CLOSEST),
],
)
.map_err(|error| VideoError::Backend(error.to_string()))?
.l()
.map_err(|error| VideoError::Backend(error.to_string()))?;
if bitmap.is_null() {
return Ok(None);
}
let width = call_int(env, &bitmap, "getWidth", "()I", &[])? as usize;
let height = call_int(env, &bitmap, "getHeight", "()I", &[])? as usize;
if width == 0 || height == 0 {
return Ok(None);
}
let pixels = env
.new_int_array((width * height) as i32)
.map_err(|error| VideoError::Backend(error.to_string()))?;
env.call_method(
&bitmap,
"getPixels",
"([IIIIIII)V",
&[
JValue::Object(pixels.as_ref()),
JValue::Int(0),
JValue::Int(width as i32),
JValue::Int(0),
JValue::Int(0),
JValue::Int(width as i32),
JValue::Int(height as i32),
],
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
let mut argb = vec![0; width * height];
env.get_int_array_region(&pixels, 0, &mut argb)
.map_err(|error| VideoError::Backend(error.to_string()))?;
let mut rgba = Vec::with_capacity(argb.len() * 4);
for pixel in argb {
rgba.extend_from_slice(&[
((pixel >> 16) & 0xff) as u8,
((pixel >> 8) & 0xff) as u8,
(pixel & 0xff) as u8,
((pixel >> 24) & 0xff) as u8,
]);
}
Ok(Some(rgba))
}
pub(crate) fn play(&mut self) -> Result<(), VideoError> {
self.with_player(|env, player| {
env.call_method(player, "start", "()V", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(())
})
}
pub(crate) fn pause(&mut self) -> Result<(), VideoError> {
self.with_player(|env, player| {
env.call_method(player, "pause", "()V", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(())
})
}
pub(crate) fn stop(&mut self) -> Result<(), VideoError> {
self.with_player(|env, player| {
env.call_method(player, "pause", "()V", &[])
.map_err(|error| VideoError::Backend(error.to_string()))?;
env.call_method(player, "seekTo", "(I)V", &[JValue::Int(0)])
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(())
})
}
pub(crate) fn seek(&mut self, position: Duration) -> Result<(), VideoError> {
self.with_player(|env, player| {
env.call_method(
player,
"seekTo",
"(I)V",
&[JValue::Int(
position.as_millis().min(i32::MAX as u128) as i32
)],
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(())
})
}
pub(crate) fn set_loop(&mut self, looping: bool) -> Result<(), VideoError> {
self.looping = looping;
self.with_player(|env, player| {
env.call_method(player, "setLooping", "(Z)V", &[JValue::Bool(looping as u8)])
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(())
})
}
pub(crate) fn set_muted(&mut self, muted: bool) -> Result<(), VideoError> {
self.muted = muted;
self.set_volume(self.volume)
}
pub(crate) fn set_volume(&mut self, volume: f32) -> Result<(), VideoError> {
self.volume = volume.clamp(0.0, 1.0);
let effective = if self.muted { 0.0 } else { self.volume };
self.with_player(|env, player| {
env.call_method(
player,
"setVolume",
"(FF)V",
&[JValue::Float(effective), JValue::Float(effective)],
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(())
})
}
pub(crate) fn set_playback_rate(&mut self, rate: f64) -> Result<(), VideoError> {
self.with_player(|env, player| {
let params = env
.call_method(
player,
"getPlaybackParams",
"()Landroid/media/PlaybackParams;",
&[],
)
.map_err(|error| VideoError::Backend(error.to_string()))?
.l()
.map_err(|error| VideoError::Backend(error.to_string()))?;
env.call_method(
¶ms,
"setSpeed",
"(F)Landroid/media/PlaybackParams;",
&[JValue::Float(rate as f32)],
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
env.call_method(
player,
"setPlaybackParams",
"(Landroid/media/PlaybackParams;)V",
&[JValue::Object(¶ms)],
)
.map_err(|error| VideoError::Backend(error.to_string()))?;
Ok(())
})
}
fn with_player(
&self,
f: impl FnOnce(&mut jni::JNIEnv<'_>, &JObject<'_>) -> Result<(), VideoError>,
) -> Result<(), VideoError> {
let jvm = crate::android::get_jvm()
.ok_or_else(|| VideoError::Backend("Android JVM is unavailable".to_string()))?;
let mut env = jvm
.attach_current_thread()
.map_err(|error| VideoError::Backend(error.to_string()))?;
f(&mut env, self.player.as_obj())
}
}
fn metadata_int(
env: &mut jni::JNIEnv<'_>,
retriever: &JObject<'_>,
key: i32,
) -> Result<i32, VideoError> {
let value = env
.call_method(
retriever,
"extractMetadata",
"(I)Ljava/lang/String;",
&[JValue::Int(key)],
)
.map_err(|error| VideoError::Backend(error.to_string()))?
.l()
.map_err(|error| VideoError::Backend(error.to_string()))?;
if value.is_null() {
return Ok(0);
}
let value: jni::objects::JString = value.into();
env.get_string(&value)
.map_err(|error| VideoError::Backend(error.to_string()))?
.to_str()
.ok()
.and_then(|value| value.parse().ok())
.ok_or_else(|| VideoError::Backend(format!("invalid Android video metadata key {key}")))
}
fn normalize_asset_path(path: &str) -> String {
let path = path.strip_prefix("./").unwrap_or(path);
path.strip_prefix("assets/").unwrap_or(path).to_string()
}
fn call_int(
env: &mut jni::JNIEnv<'_>,
object: &JObject<'_>,
name: &str,
signature: &str,
args: &[JValue<'_, '_>],
) -> Result<i32, VideoError> {
env.call_method(object, name, signature, args)
.map_err(|error| VideoError::Backend(error.to_string()))?
.i()
.map_err(|error| VideoError::Backend(error.to_string()))
}
fn call_bool(
env: &mut jni::JNIEnv<'_>,
object: &JObject<'_>,
name: &str,
signature: &str,
args: &[JValue<'_, '_>],
) -> Result<bool, VideoError> {
env.call_method(object, name, signature, args)
.map_err(|error| VideoError::Backend(error.to_string()))?
.z()
.map_err(|error| VideoError::Backend(error.to_string()))
}