cranpose 0.1.150

Cranpose runtime and UI facade
Documentation
#![allow(unsafe_code)]

use std::{
    sync::{
        Arc,
        atomic::{AtomicU32, Ordering},
    },
    time::Duration,
};

use cranpose_media::SoftwareMediaPlayer;
use cranpose_services::{
    AudioFocus, EqualizerBand, EqualizerSettings, MediaCapabilities, MediaCommand, MediaError,
    MediaItem, MediaMetadata, MediaPlayer, MediaSourceHandle, MediaSourceOpener, playback_progress,
    publish_audio_focus, publish_media_command, set_platform_media_player,
    set_platform_media_source_opener,
};
use jni::{
    EnvUnowned, jni_sig, jni_str,
    objects::{JClass, JValue},
    signature::MethodSignature,
    sys::{jfloat, jint, jlong},
};

use crate::android_jni::{clear_pending_android_jni_exception, with_android_activity_env};

const SESSION_STOPPED: jint = 0;
const SESSION_PLAYING: jint = 1;
const SESSION_PAUSED: jint = 2;

const FOCUS_GAINED: jint = 0;
const FOCUS_DUCKED: jint = 1;
const FOCUS_LOST_TRANSIENT: jint = 2;
const FOCUS_LOST: jint = 3;

const COMMAND_PLAY: jint = 0;
const COMMAND_PAUSE: jint = 1;
const COMMAND_TOGGLE: jint = 2;
const COMMAND_STOP: jint = 3;
const COMMAND_NEXT: jint = 4;
const COMMAND_PREVIOUS: jint = 5;
const COMMAND_SEEK: jint = 6;

const NO_DURATION: jlong = -1;

pub(crate) fn register(app: android_activity::AndroidApp) {
    set_platform_media_source_opener(Arc::new(DocumentOpener));
    set_platform_media_player(Arc::new(AndroidMediaPlayer {
        app,
        player: SoftwareMediaPlayer::new(),
        speed: AtomicU32::new(1.0f32.to_bits()),
    }));
}

struct DocumentOpener;

impl MediaSourceOpener for DocumentOpener {
    fn open(&self, uri: &str) -> std::io::Result<MediaSourceHandle> {
        Ok(MediaSourceHandle {
            stream: crate::android_file_picker::open_content_uri(uri)?,
            len: crate::android_file_picker::content_uri_length(uri),
        })
    }
}

struct AndroidMediaPlayer {
    app: android_activity::AndroidApp,
    player: SoftwareMediaPlayer,
    speed: AtomicU32,
}

impl AndroidMediaPlayer {
    fn call_bool(&self, name: &'static jni::strings::JNIStr, default: bool) -> bool {
        let called = with_android_activity_env(&self.app, |env, activity| {
            env.call_method(&activity, name, jni_sig!("()Z"), &[])
                .and_then(|value| value.z())
                .map_err(|error| {
                    clear_pending_android_jni_exception(env);
                    error.to_string()
                })
        });
        match called {
            Ok(value) => value,
            Err(error) => {
                log::warn!("cranpose: android media call failed: {error}");
                default
            }
        }
    }

    fn call(
        &self,
        name: &'static jni::strings::JNIStr,
        signature: MethodSignature,
        arguments: &[JValue<'_>],
    ) {
        let called = with_android_activity_env(&self.app, |env, activity| {
            env.call_method(&activity, name, signature, arguments)
                .map(|_| ())
                .map_err(|error| {
                    clear_pending_android_jni_exception(env);
                    error.to_string()
                })
        });
        if let Err(error) = called {
            log::warn!("cranpose: android media call failed: {error}");
        }
    }

    fn publish_session(&self, state: jint) {
        let progress = playback_progress();
        let duration = progress
            .duration
            .map(|duration| duration.as_millis().min(jlong::MAX as u128) as jlong)
            .unwrap_or(NO_DURATION);
        self.call(
            jni_str!("cranposeMediaSessionUpdate"),
            jni_sig!("(IJJF)V"),
            &[
                JValue::Int(state),
                JValue::Long(progress.position.as_millis().min(jlong::MAX as u128) as jlong),
                JValue::Long(duration),
                JValue::Float(f32::from_bits(self.speed.load(Ordering::Relaxed)) as jfloat),
            ],
        );
    }
}

impl MediaPlayer for AndroidMediaPlayer {
    fn capabilities(&self) -> MediaCapabilities {
        MediaCapabilities {
            session: true,
            ..self.player.capabilities()
        }
    }

    fn probe_duration(&self, item: &MediaItem) -> Option<Duration> {
        self.player.probe_duration(item)
    }

    fn audio_extensions(&self) -> Vec<&'static str> {
        self.player.audio_extensions()
    }

    fn prepare(&self, item: &MediaItem) -> Result<(), MediaError> {
        self.player.prepare(item)?;
        self.publish_session(SESSION_PAUSED);
        Ok(())
    }

    fn play(&self) -> Result<(), MediaError> {
        if !self.call_bool(jni_str!("cranposeMediaRequestFocus"), true) {
            return Err(MediaError::Failed(
                "another app holds the audio output".to_owned(),
            ));
        }
        self.player.play()?;
        self.publish_session(SESSION_PLAYING);
        Ok(())
    }

    fn pause(&self) {
        self.player.pause();
        self.publish_session(SESSION_PAUSED);
    }

    fn stop(&self) {
        self.player.stop();
        self.publish_session(SESSION_STOPPED);
        self.call(jni_str!("cranposeMediaAbandonFocus"), jni_sig!("()V"), &[]);
    }

    fn seek_to(&self, position: Duration) -> Result<(), MediaError> {
        self.player.seek_to(position)?;
        self.publish_session(if cranpose_services::playback_state().is_playing() {
            SESSION_PLAYING
        } else {
            SESSION_PAUSED
        });
        Ok(())
    }

    fn set_volume(&self, volume: f32) {
        self.player.set_volume(volume);
    }

    fn set_speed(&self, speed: f32) -> bool {
        if !self.player.set_speed(speed) {
            return false;
        }
        self.speed.store(speed.to_bits(), Ordering::Relaxed);
        true
    }

    fn set_looping(&self, looping: bool) {
        self.player.set_looping(looping);
    }

    fn equalizer_bands(&self) -> Vec<EqualizerBand> {
        self.player.equalizer_bands()
    }

    fn set_equalizer(&self, settings: &EqualizerSettings) {
        self.player.set_equalizer(settings);
    }

    fn set_analysis_enabled(&self, enabled: bool) -> bool {
        self.player.set_analysis_enabled(enabled)
    }

    fn set_session_metadata(&self, metadata: &MediaMetadata) {
        let called = with_android_activity_env(&self.app, |env, activity| {
            let title = env.new_string(&metadata.title).map_err(|error| {
                clear_pending_android_jni_exception(env);
                error.to_string()
            })?;
            let artist = env.new_string(&metadata.artist).map_err(|error| {
                clear_pending_android_jni_exception(env);
                error.to_string()
            })?;
            env.call_method(
                &activity,
                jni_str!("cranposeMediaSetMetadata"),
                jni_sig!("(Ljava/lang/String;Ljava/lang/String;)V"),
                &[JValue::Object(&title), JValue::Object(&artist)],
            )
            .map(|_| ())
            .map_err(|error| {
                clear_pending_android_jni_exception(env);
                error.to_string()
            })
        });
        if let Err(error) = called {
            log::warn!("cranpose: android media metadata failed: {error}");
        }
    }
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnMediaAudioFocus(
    _env: EnvUnowned<'_>,
    _class: JClass<'_>,
    focus: jint,
) {
    publish_audio_focus(match focus {
        FOCUS_DUCKED => AudioFocus::Ducked,
        FOCUS_LOST_TRANSIENT => AudioFocus::LostTransient,
        FOCUS_LOST => AudioFocus::Lost,
        FOCUS_GAINED => AudioFocus::Gained,
        _ => return,
    });
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnMediaCommand(
    _env: EnvUnowned<'_>,
    _class: JClass<'_>,
    command: jint,
    position_ms: jlong,
) {
    publish_media_command(match command {
        COMMAND_PLAY => MediaCommand::Play,
        COMMAND_PAUSE => MediaCommand::Pause,
        COMMAND_TOGGLE => MediaCommand::TogglePlayPause,
        COMMAND_STOP => MediaCommand::Stop,
        COMMAND_NEXT => MediaCommand::Next,
        COMMAND_PREVIOUS => MediaCommand::Previous,
        COMMAND_SEEK => MediaCommand::SeekTo(Duration::from_millis(position_ms.max(0) as u64)),
        _ => return,
    });
}