Skip to main content

android_media/
player.rs

1use {
2    super::{constants::*, load_class},
3    jni::{
4        JNIEnv,
5        errors::Error as JniError,
6        objects::{GlobalRef, JObject, JString},
7    },
8    std::{
9        collections::HashMap,
10        error::Error,
11        fmt::{Display, Formatter, Result as FmtResult},
12        mem::transmute,
13        sync::{
14            LazyLock, Mutex, PoisonError,
15            atomic::{AtomicI16, Ordering},
16        },
17    },
18    tokio::sync::oneshot::{Sender, channel, error::RecvError},
19    tracing::error,
20};
21
22#[derive(Debug)]
23pub enum PlayerError {
24    Jni(JniError),
25    Poison(String),
26    Recv(RecvError),
27    Write(String),
28}
29
30impl Display for PlayerError {
31    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
32        write!(f, "PlayerError: ")?;
33        match self {
34            Self::Jni(e) => Display::fmt(e, f),
35            Self::Poison(msg) => write!(f, "PoisonError: {}", msg),
36            Self::Recv(e) => Display::fmt(e, f),
37            Self::Write(msg) => write!(f, "WriteError: {}", msg),
38        }
39    }
40}
41
42impl Error for PlayerError {}
43
44impl From<JniError> for PlayerError {
45    fn from(value: JniError) -> Self {
46        Self::Jni(value)
47    }
48}
49
50impl From<RecvError> for PlayerError {
51    fn from(value: RecvError) -> Self {
52        Self::Recv(value)
53    }
54}
55
56impl<T> From<PoisonError<T>> for PlayerError {
57    fn from(value: PoisonError<T>) -> Self {
58        Self::Poison(value.to_string())
59    }
60}
61
62type PlayerResult<T> = Result<T, PlayerError>;
63
64/// Player write task callbacks
65static PLAYER_WRITE_CALLBACKS: LazyLock<Mutex<HashMap<i16, Sender<PlayerResult<()>>>>> =
66    LazyLock::new(|| Mutex::new(Default::default()));
67
68/// Rust wrapper for Android audio player
69pub struct AudioPlayer {
70    /// JNI environment
71    env: JNIEnv<'static>,
72    /// Java object instance
73    this: GlobalRef,
74    /// Sample rate (Hz)
75    sample_rate: i32,
76    /// Audio channel count
77    channel_count: i32,
78    /// Audio encoding format
79    audio_format: i32,
80    /// Playback mode
81    mode: i32,
82}
83
84//noinspection SpellCheckingInspection
85#[allow(dead_code)]
86impl AudioPlayer {
87    /// Create a new AudioPlayer instance
88    ///
89    /// # Arguments
90    /// * `env` - JNI environment
91    /// * `context` - GlobalRef to context object (used to get the correct ClassLoader)
92    /// * `sample_rate` - Sample rate (e.g., 16000, 44100, 48000)
93    /// * `channel_config` - Channel configuration (use ChannelOutConfig constants)
94    /// * `audio_format` - Audio encoding format (use AudioEncoding constants)
95    /// * `mode` - Playback mode (use PlayerMode constants)
96    ///
97    /// # Returns
98    /// Returns initialized AudioPlayer instance
99    ///
100    /// # Examples
101    ///
102    /// See the README.md for complete usage examples.
103    pub fn new(
104        mut env: JNIEnv,
105        context: &GlobalRef,
106        sample_rate: i32,
107        channel_config: i32,
108        audio_format: i32,
109        mode: i32,
110    ) -> PlayerResult<Self> {
111        // Load Player class using ClassLoader
112        let player_class = load_class(&mut env, context, "rust.android.Player")?;
113
114        // Create Player Java object
115        let player_obj = env.new_object(
116            &player_class,
117            "(IIII)V",
118            &[
119                sample_rate.into(),
120                channel_config.into(),
121                audio_format.into(),
122                mode.into(),
123            ],
124        )?;
125        let this = env.new_global_ref(player_obj)?;
126
127        // Initialize audio player
128        env.call_method(
129            &this,
130            "init",
131            "(IIII)V",
132            &[
133                sample_rate.into(),
134                channel_config.into(),
135                audio_format.into(),
136                mode.into(),
137            ],
138        )?;
139
140        unsafe {
141            Ok(Self {
142                env: transmute(env),
143                this,
144                sample_rate,
145                channel_count: if channel_config == ChannelOutConfig::Mono.value() {
146                    1
147                } else {
148                    2
149                },
150                audio_format,
151                mode,
152            })
153        }
154    }
155
156    fn get_env(&self) -> JNIEnv<'_> {
157        unsafe { self.env.unsafe_clone() }
158    }
159
160    /// Start playback
161    pub fn play(&self) -> PlayerResult<()> {
162        self.get_env().call_method(&self.this, "play", "()V", &[])?;
163        Ok(())
164    }
165
166    /// Stop playback
167    pub fn stop(&self) -> PlayerResult<()> {
168        self.get_env().call_method(&self.this, "stop", "()V", &[])?;
169
170        Ok(())
171    }
172
173    /// Pause playback
174    pub fn pause(&self) -> PlayerResult<()> {
175        self.get_env()
176            .call_method(&self.this, "pause", "()V", &[])?;
177        Ok(())
178    }
179
180    /// Flush buffer
181    pub fn flush(&self) -> PlayerResult<()> {
182        self.get_env()
183            .call_method(&self.this, "flush", "()V", &[])?;
184
185        Ok(())
186    }
187
188    /// Release resources
189    pub fn release(&self) -> PlayerResult<()> {
190        self.get_env()
191            .call_method(&self.this, "release", "()V", &[])?;
192
193        Ok(())
194    }
195
196    /// Write audio data asynchronously
197    ///
198    /// # Arguments
199    /// * `audio_data` - Audio data (raw PCM data)
200    ///
201    /// # Returns
202    /// * `Result<(), PlayerError>` - Returns () on success, error on failure
203    ///
204    /// # Examples
205    ///
206    /// See the README.md for complete usage examples.
207    pub async fn write(&self, audio_data: &[u8]) -> PlayerResult<()> {
208        static ID: AtomicI16 = AtomicI16::new(0);
209        let task_id = ID.fetch_add(1, Ordering::AcqRel);
210        let (tx, rx) = channel();
211
212        {
213            let mut lock = PLAYER_WRITE_CALLBACKS.lock()?;
214            lock.insert(task_id, tx);
215        }
216
217        // Create byte array
218        {
219            let jbyte_array = self.env.byte_array_from_slice(audio_data)?;
220
221            // Call Java layer's write method
222            self.get_env()
223                .call_method(
224                    &self.this,
225                    "performWrite",
226                    "(S[B)V",
227                    &[task_id.into(), (&jbyte_array).into()],
228                )?
229                .v()?;
230        }
231
232        rx.await?
233    }
234
235    /// Get sample rate
236    pub fn get_sample_rate(&self) -> i32 {
237        self.sample_rate
238    }
239
240    /// Get channel count
241    pub fn get_channel_count(&self) -> i32 {
242        self.channel_count
243    }
244
245    /// Get audio format
246    pub fn get_audio_format(&self) -> i32 {
247        self.audio_format
248    }
249
250    /// Get playback mode
251    pub fn get_mode(&self) -> i32 {
252        self.mode
253    }
254
255    /// Get buffer size
256    pub fn get_buffer_size(&self) -> PlayerResult<i32> {
257        Ok(self
258            .get_env()
259            .call_method(&self.this, "getBufferSize", "()I", &[])?
260            .i()?)
261    }
262}
263
264unsafe impl Send for AudioPlayer {}
265unsafe impl Sync for AudioPlayer {}
266
267impl Drop for AudioPlayer {
268    fn drop(&mut self) {
269        if let Err(e) = self.release() {
270            error!(?e, "Dropping error.");
271        }
272    }
273}
274
275//noinspection SpellCheckingInspection
276/// Player write success callback
277///
278/// This function is called by the Java layer after audio data is successfully written
279///
280/// # Arguments
281/// * `env` - JNI environment
282/// * `_this` - Java object
283/// * `task_id` - Task ID
284#[allow(non_snake_case)]
285#[unsafe(no_mangle)]
286extern "C" fn Java_rust_android_Player_onWriteSuccess(_env: JNIEnv, _this: JObject, task_id: i16) {
287    let Ok(mut lock) = PLAYER_WRITE_CALLBACKS.lock() else {
288        return;
289    };
290    let Some(tx) = lock.remove(&task_id) else {
291        return;
292    };
293    drop(lock);
294
295    let _ = tx.send(Ok(()));
296}
297
298//noinspection SpellCheckingInspection
299/// Player write failure callback
300///
301/// This function is called by the Java layer after audio data write fails
302///
303/// # Arguments
304/// * `env` - JNI environment
305/// * `_this` - Java object
306/// * `task_id` - Task ID
307/// * `error_message` - Error message (string)
308#[allow(non_snake_case)]
309#[unsafe(no_mangle)]
310extern "C" fn Java_rust_android_Player_onWriteFailure(
311    mut env: JNIEnv,
312    _this: JObject,
313    task_id: i16,
314    error_message: JString,
315) {
316    let Ok(mut lock) = PLAYER_WRITE_CALLBACKS.lock() else {
317        return;
318    };
319    let Some(tx) = lock.remove(&task_id) else {
320        return;
321    };
322
323    let error_msg = match env.get_string(&error_message) {
324        Ok(msg) => msg.to_string_lossy().to_string(),
325        Err(e) => format!("Parsing error failed: {}", e),
326    };
327
328    if let Err(e) = tx.send(Err(PlayerError::Write(format!(
329        "Player write failed: {}",
330        error_msg
331    )))) {
332        error!(?e, "Sending error failed.");
333    }
334}