diff --git a/crates/erika/src/presenter.rs b/crates/erika/src/presenter.rs index 56fd5a2..40aa44f 100644 --- a/crates/erika/src/presenter.rs +++ b/crates/erika/src/presenter.rs @@ -248,6 +248,8 @@ pub struct PresenterRuntime { current_surface_metrics: Option, current_danmaku_viewport: Option, subtitle_font_scale: f64, + muted: bool, + saved_volume: f64, subtitles: SubtitleFrameState, overlay: OverlayTimeline, render_test_pattern_when_idle: bool, @@ -550,6 +552,8 @@ impl PresenterRuntime { current_surface_metrics: None, current_danmaku_viewport: None, subtitle_font_scale: DEFAULT_SUBTITLE_FONT_SCALE, + muted: false, + saved_volume: 1.0, subtitles: SubtitleFrameState::default(), overlay: config.overlay, render_test_pattern_when_idle: config.render_test_pattern_when_idle, @@ -693,11 +697,28 @@ impl PresenterRuntime { } pub fn set_volume(&mut self, volume: f64) { - self.audio_output.set_volume(volume as f32); + self.saved_volume = normalize_presenter_volume(volume); + let output = effective_output_volume(self.muted, self.saved_volume); + self.audio_output.set_volume(output as f32); } + /// Returns the user-selected volume. While muted the audio output runs at + /// zero gain, but this still reports the saved volume so UIs can restore + /// the slider position on unmute. pub fn volume(&self) -> f64 { - self.audio_output.volume() as f64 + self.saved_volume + } + + /// Mutes or unmutes audio without discarding the saved volume. Volume + /// changes made while muted are remembered and applied on unmute. + pub fn set_muted(&mut self, muted: bool) { + self.muted = muted; + let output = effective_output_volume(self.muted, self.saved_volume); + self.audio_output.set_volume(output as f32); + } + + pub fn muted(&self) -> bool { + self.muted } pub fn set_subtitle_scale(&mut self, scale: f64) { @@ -2158,6 +2179,18 @@ fn normalize_subtitle_font_scale(scale: f64) -> f64 { } } +fn normalize_presenter_volume(volume: f64) -> f64 { + if volume.is_finite() { + volume.clamp(0.0, 1.0) + } else { + 1.0 + } +} + +fn effective_output_volume(muted: bool, saved_volume: f64) -> f64 { + if muted { 0.0 } else { saved_volume } +} + fn bump_generation(current_generation: &mut u64, danmaku_generation: &mut u64) { *danmaku_generation = danmaku_generation.saturating_add(1).max(1); *current_generation = current_generation @@ -3323,6 +3356,36 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text assert_eq!(presenter.volume(), 1.0); } + #[test] + #[cfg(feature = "wgpu")] + fn presenter_mute_preserves_saved_volume() { + let mut presenter = PresenterRuntime::new(PresenterConfig::default()).unwrap(); + + presenter.set_volume(0.6); + presenter.set_muted(true); + assert!(presenter.muted()); + // Output gain is silenced while the reported volume stays at the + // user's saved value. + assert_eq!(presenter.audio_output.volume(), 0.0); + assert!((presenter.volume() - 0.6).abs() < 0.000_001); + + // Volume changes while muted are remembered but not applied yet. + presenter.set_volume(0.3); + assert_eq!(presenter.audio_output.volume(), 0.0); + assert!((presenter.volume() - 0.3).abs() < 0.000_001); + + presenter.set_muted(false); + assert!(!presenter.muted()); + assert!((f64::from(presenter.audio_output.volume()) - 0.3).abs() < 0.000_001); + } + + #[test] + fn effective_output_volume_silences_only_while_muted() { + assert_eq!(effective_output_volume(true, 0.7), 0.0); + assert_eq!(effective_output_volume(false, 0.7), 0.7); + assert_eq!(effective_output_volume(false, 0.0), 0.0); + } + #[test] fn surface_dimensions_are_converted_to_full_output_danmaku_viewport() { let viewport = surface_metrics_to_viewport(SurfaceMetrics::new(1600, 900, 2.0)); diff --git a/crates/erika_capi/include/erika.h b/crates/erika_capi/include/erika.h index 9b0fa5c..eb8594f 100644 --- a/crates/erika_capi/include/erika.h +++ b/crates/erika_capi/include/erika.h @@ -436,6 +436,13 @@ ErikaStatus erika_presenter_close(ErikaPresenterHandle *handle); ErikaStatus erika_presenter_seek(ErikaPresenterHandle *handle, uint64_t position_micros); ErikaStatus erika_presenter_set_playback_rate(ErikaPresenterHandle *handle, double rate); ErikaStatus erika_presenter_set_volume(ErikaPresenterHandle *handle, double volume); +/* get_volume reports the saved user volume even while muted so hosts can + * restore their slider position on unmute. */ +ErikaStatus erika_presenter_get_volume(ErikaPresenterHandle *handle, double *out_volume); +/* Mute silences output without discarding the saved volume; set_volume while + * muted updates the saved value and is applied on unmute. */ +ErikaStatus erika_presenter_set_muted(ErikaPresenterHandle *handle, bool muted); +ErikaStatus erika_presenter_muted(ErikaPresenterHandle *handle, bool *out_muted); ErikaStatus erika_presenter_set_upscaler(ErikaPresenterHandle *handle, int32_t mode); ErikaStatus erika_presenter_set_subtitle_scale(ErikaPresenterHandle *handle, double scale); ErikaStatus erika_presenter_set_output_headroom( diff --git a/crates/erika_capi/src/android_jni.rs b/crates/erika_capi/src/android_jni.rs index 3031606..69376d0 100644 --- a/crates/erika_capi/src/android_jni.rs +++ b/crates/erika_capi/src/android_jni.rs @@ -763,6 +763,15 @@ unsafe fn invoke_presenter( let scale = required_f64(args, "scale")?; status_value(unsafe { erika_presenter_set_subtitle_scale(handle, scale) }) } + "setMuted" => { + let muted = required_bool(args, "muted")?; + status_value(unsafe { erika_presenter_set_muted(handle, muted) }) + } + "getVolume" => { + let mut volume = 1.0f64; + call_status(unsafe { erika_presenter_get_volume(handle, &mut volume) })?; + Ok(json!(volume)) + } "setOutputHeadroom" => { let headroom = required_f64(args, "headroom")? as f32; let known = @@ -1382,6 +1391,10 @@ fn optional_bool(args: &Map, name: &str) -> Option { args.get(name).and_then(Value::as_bool) } +fn required_bool(args: &Map, name: &str) -> Result { + optional_bool(args, name).ok_or_else(|| format!("{name} is required")) +} + fn c_string(value: &str, name: &str) -> Result { CString::new(value).map_err(|_| format!("{name} contains an embedded NUL byte")) } diff --git a/crates/erika_capi/src/lib.rs b/crates/erika_capi/src/lib.rs index 81aee1c..1f84911 100644 --- a/crates/erika_capi/src/lib.rs +++ b/crates/erika_capi/src/lib.rs @@ -1637,6 +1637,73 @@ pub unsafe extern "C" fn erika_presenter_set_volume( target_os = "android", target_env = "ohos" ))] +/// # Safety +/// `handle` must be a live pointer returned by `erika_presenter_create*` and +/// `out_volume` must point to writable memory for one `f64`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn erika_presenter_get_volume( + handle: *mut ErikaPresenterHandle, + out_volume: *mut f64, +) -> ErikaStatus { + if out_volume.is_null() { + return ErikaStatus::NullPointer; + } + with_presenter_mut(handle, |handle| { + let volume = handle.presenter.volume(); + unsafe { *out_volume = volume }; + ErikaStatus::Ok + }) +} + +#[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "windows", + target_os = "android" +))] +/// # Safety +/// `handle` must be a live pointer returned by `erika_presenter_create*`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn erika_presenter_set_muted( + handle: *mut ErikaPresenterHandle, + muted: bool, +) -> ErikaStatus { + with_presenter_mut(handle, |handle| { + handle.presenter.set_muted(muted); + ErikaStatus::Ok + }) +} + +#[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "windows", + target_os = "android" +))] +/// # Safety +/// `handle` must be a live pointer returned by `erika_presenter_create*` and +/// `out_muted` must point to writable memory for one `bool`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn erika_presenter_muted( + handle: *mut ErikaPresenterHandle, + out_muted: *mut bool, +) -> ErikaStatus { + if out_muted.is_null() { + return ErikaStatus::NullPointer; + } + with_presenter_mut(handle, |handle| { + let muted = handle.presenter.muted(); + unsafe { *out_muted = muted }; + ErikaStatus::Ok + }) +} + +#[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "windows", + target_os = "android" +))] #[unsafe(no_mangle)] pub unsafe extern "C" fn erika_presenter_set_upscaler( handle: *mut ErikaPresenterHandle, @@ -2434,6 +2501,54 @@ pub unsafe extern "C" fn erika_presenter_set_volume( target_env = "ohos" )))] #[unsafe(no_mangle)] +pub unsafe extern "C" fn erika_presenter_get_volume( + _handle: *mut std::ffi::c_void, + out_volume: *mut f64, +) -> ErikaStatus { + if out_volume.is_null() { + return ErikaStatus::NullPointer; + } + ErikaStatus::PlayerError +} + +#[cfg(not(any( + target_os = "macos", + target_os = "ios", + target_os = "windows", + target_os = "android" +)))] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn erika_presenter_set_muted( + _handle: *mut std::ffi::c_void, + _muted: bool, +) -> ErikaStatus { + ErikaStatus::PlayerError +} + +#[cfg(not(any( + target_os = "macos", + target_os = "ios", + target_os = "windows", + target_os = "android" +)))] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn erika_presenter_muted( + _handle: *mut std::ffi::c_void, + out_muted: *mut bool, +) -> ErikaStatus { + if out_muted.is_null() { + return ErikaStatus::NullPointer; + } + ErikaStatus::PlayerError +} + +#[cfg(not(any( + target_os = "macos", + target_os = "ios", + target_os = "windows", + target_os = "android" +)))] +#[unsafe(no_mangle)] pub unsafe extern "C" fn erika_presenter_set_upscaler( _handle: *mut std::ffi::c_void, _mode: i32, @@ -4092,6 +4207,72 @@ mod tests { target_env = "ohos" ))] #[test] + fn c_presenter_volume_and_mute_round_trip() { + let mut volume = f64::NAN; + assert_eq!( + unsafe { erika_presenter_get_volume(std::ptr::null_mut(), &mut volume) }, + ErikaStatus::NullPointer + ); + let mut muted = false; + assert_eq!( + unsafe { erika_presenter_set_muted(std::ptr::null_mut(), true) }, + ErikaStatus::NullPointer + ); + assert_eq!( + unsafe { erika_presenter_muted(std::ptr::null_mut(), &mut muted) }, + ErikaStatus::NullPointer + ); + + let handle = erika_presenter_create(); + assert!(!handle.is_null()); + assert_eq!( + unsafe { erika_presenter_get_volume(handle, std::ptr::null_mut()) }, + ErikaStatus::NullPointer + ); + assert_eq!( + unsafe { erika_presenter_muted(handle, std::ptr::null_mut()) }, + ErikaStatus::NullPointer + ); + + assert_eq!( + unsafe { erika_presenter_set_volume(handle, 0.4) }, + ErikaStatus::Ok + ); + assert_eq!( + unsafe { erika_presenter_set_muted(handle, true) }, + ErikaStatus::Ok + ); + assert_eq!( + unsafe { erika_presenter_muted(handle, &mut muted) }, + ErikaStatus::Ok + ); + assert!(muted); + // Muting must not clobber the saved volume the getter reports. + assert_eq!( + unsafe { erika_presenter_get_volume(handle, &mut volume) }, + ErikaStatus::Ok + ); + assert!((volume - 0.4).abs() < 0.000_001); + + assert_eq!( + unsafe { erika_presenter_set_muted(handle, false) }, + ErikaStatus::Ok + ); + assert_eq!( + unsafe { erika_presenter_muted(handle, &mut muted) }, + ErikaStatus::Ok + ); + assert!(!muted); + unsafe { erika_presenter_destroy(handle) }; + } + + #[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "windows", + target_os = "android" + ))] + #[test] fn c_presenter_set_upscaler_accepts_valid_handle() { assert_eq!( unsafe { diff --git a/packages/erika_flutter/android/src/main/kotlin/dev/aimesoft/erika_flutter/ErikaFlutterPlugin.kt b/packages/erika_flutter/android/src/main/kotlin/dev/aimesoft/erika_flutter/ErikaFlutterPlugin.kt index eda00f5..8492083 100644 --- a/packages/erika_flutter/android/src/main/kotlin/dev/aimesoft/erika_flutter/ErikaFlutterPlugin.kt +++ b/packages/erika_flutter/android/src/main/kotlin/dev/aimesoft/erika_flutter/ErikaFlutterPlugin.kt @@ -1718,6 +1718,8 @@ class ErikaFlutterPlugin : "seek", "setPlaybackRate", "setVolume", + "getVolume", + "setMuted", "setUpscaler", "setSubtitleScale", "getUpscalerStatus", diff --git a/packages/erika_flutter/ios/Classes/ErikaFlutterPlugin.swift b/packages/erika_flutter/ios/Classes/ErikaFlutterPlugin.swift index 163529f..43d6bc2 100644 --- a/packages/erika_flutter/ios/Classes/ErikaFlutterPlugin.swift +++ b/packages/erika_flutter/ios/Classes/ErikaFlutterPlugin.swift @@ -354,6 +354,8 @@ private final class ErikaNativeLibrary { typealias SeekFn = @convention(c) (UnsafeMutableRawPointer?, UInt64) -> Int32 typealias SetPlaybackRateFn = @convention(c) (UnsafeMutableRawPointer?, Double) -> Int32 typealias SetVolumeFn = @convention(c) (UnsafeMutableRawPointer?, Double) -> Int32 + typealias GetVolumeFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutablePointer?) -> Int32 + typealias SetMutedFn = @convention(c) (UnsafeMutableRawPointer?, Bool) -> Int32 typealias SetUpscalerFn = @convention(c) (UnsafeMutableRawPointer?, Int32) -> Int32 typealias SetSubtitleScaleFn = @convention(c) (UnsafeMutableRawPointer?, Double) -> Int32 typealias GetUpscalerStatusFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Int32 @@ -416,6 +418,8 @@ private final class ErikaNativeLibrary { let seek: SeekFn let setPlaybackRate: SetPlaybackRateFn? let setVolume: SetVolumeFn? + let getVolume: GetVolumeFn? + let setMuted: SetMutedFn? let setUpscaler: SetUpscalerFn? let setSubtitleScale: SetSubtitleScaleFn? let getUpscalerStatus: GetUpscalerStatusFn? @@ -474,6 +478,8 @@ private final class ErikaNativeLibrary { seek = try Self.load("erika_presenter_seek", from: libraryHandle, as: SeekFn.self) setPlaybackRate = Self.loadOptional("erika_presenter_set_playback_rate", from: libraryHandle, as: SetPlaybackRateFn.self) setVolume = Self.loadOptional("erika_presenter_set_volume", from: libraryHandle, as: SetVolumeFn.self) + getVolume = Self.loadOptional("erika_presenter_get_volume", from: libraryHandle, as: GetVolumeFn.self) + setMuted = Self.loadOptional("erika_presenter_set_muted", from: libraryHandle, as: SetMutedFn.self) setUpscaler = Self.loadOptional("erika_presenter_set_upscaler", from: libraryHandle, as: SetUpscalerFn.self) setSubtitleScale = Self.loadOptional("erika_presenter_set_subtitle_scale", from: libraryHandle, as: SetSubtitleScaleFn.self) getUpscalerStatus = Self.loadOptional("erika_presenter_get_upscaler_status", from: libraryHandle, as: GetUpscalerStatusFn.self) @@ -659,6 +665,22 @@ private final class ErikaPlayerHost { try check(setVolume(handle, clampedVolume), operation: "set_volume") } + func volume() throws -> Double { + guard let getVolume = library.getVolume else { + throw ErikaPluginError.symbolMissing("erika_presenter_get_volume") + } + var value: Double = 1.0 + try check(getVolume(handle, &value), operation: "get_volume") + return value + } + + func setMuted(_ muted: Bool) throws { + guard let setMuted = library.setMuted else { + throw ErikaPluginError.symbolMissing("erika_presenter_set_muted") + } + try check(setMuted(handle, muted), operation: "set_muted") + } + func setUpscaler(mode: Int32) throws { guard let setUpscaler = library.setUpscaler else { throw ErikaPluginError.symbolMissing("erika_presenter_set_upscaler") @@ -1468,6 +1490,16 @@ public final class ErikaFlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHan } try playerHost(from: args).setVolume(volume) result(nil) + case "getVolume": + let args = try dictionaryArgs(call.arguments) + result(try playerHost(from: args).volume()) + case "setMuted": + let args = try dictionaryArgs(call.arguments) + guard let muted = boolValue(args["muted"]) else { + throw ErikaPluginError.invalidArguments("muted is required.") + } + try playerHost(from: args).setMuted(muted) + result(nil) case "setUpscaler": let args = try dictionaryArgs(call.arguments) guard let mode = int32Value(args["mode"]) else { diff --git a/packages/erika_flutter/lib/src/erika_player.dart b/packages/erika_flutter/lib/src/erika_player.dart index d3504ec..97f2aef 100644 --- a/packages/erika_flutter/lib/src/erika_player.dart +++ b/packages/erika_flutter/lib/src/erika_player.dart @@ -760,6 +760,31 @@ class ErikaPlayer { }); } + /// Returns the saved user volume in `[0.0, 1.0]`. While muted this still + /// reports the volume set via [setVolume] so UIs can keep their slider + /// position; the audio output itself runs at zero gain. + Future getVolume() async { + final playerId = await ensureCreated(); + final volume = await _channel.invokeMethod( + 'getVolume', + {'playerId': playerId}, + ); + if (volume == null) { + throw StateError('Erika volume returned null.'); + } + return volume.toDouble(); + } + + /// Mutes or unmutes audio output without discarding the saved volume. + /// Volume changes made while muted are remembered and applied on unmute. + Future setMuted(bool muted) async { + final playerId = await ensureCreated(); + await _invoke('setMuted', { + 'playerId': playerId, + 'muted': muted, + }); + } + Future setUpscaler(ErikaUpscalerMode mode) async { final playerId = await ensureCreated(); await _invoke('setUpscaler', { diff --git a/packages/erika_flutter/macos/Classes/ErikaFlutterPlugin.swift b/packages/erika_flutter/macos/Classes/ErikaFlutterPlugin.swift index cc599a7..e60cab4 100644 --- a/packages/erika_flutter/macos/Classes/ErikaFlutterPlugin.swift +++ b/packages/erika_flutter/macos/Classes/ErikaFlutterPlugin.swift @@ -225,6 +225,8 @@ private final class ErikaNativeLibrary { typealias SeekFn = @convention(c) (UnsafeMutableRawPointer?, UInt64) -> Int32 typealias SetPlaybackRateFn = @convention(c) (UnsafeMutableRawPointer?, Double) -> Int32 typealias SetVolumeFn = @convention(c) (UnsafeMutableRawPointer?, Double) -> Int32 + typealias GetVolumeFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutablePointer?) -> Int32 + typealias SetMutedFn = @convention(c) (UnsafeMutableRawPointer?, Bool) -> Int32 typealias SetUpscalerFn = @convention(c) (UnsafeMutableRawPointer?, Int32) -> Int32 typealias SetSubtitleScaleFn = @convention(c) (UnsafeMutableRawPointer?, Double) -> Int32 typealias GetUpscalerStatusFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Int32 @@ -287,6 +289,8 @@ private final class ErikaNativeLibrary { let seek: SeekFn let setPlaybackRate: SetPlaybackRateFn? let setVolume: SetVolumeFn? + let getVolume: GetVolumeFn? + let setMuted: SetMutedFn? let setUpscaler: SetUpscalerFn? let setSubtitleScale: SetSubtitleScaleFn? let getUpscalerStatus: GetUpscalerStatusFn? @@ -339,6 +343,8 @@ private final class ErikaNativeLibrary { seek = try Self.load("erika_presenter_seek", from: libraryHandle, as: SeekFn.self) setPlaybackRate = Self.loadOptional("erika_presenter_set_playback_rate", from: libraryHandle, as: SetPlaybackRateFn.self) setVolume = Self.loadOptional("erika_presenter_set_volume", from: libraryHandle, as: SetVolumeFn.self) + getVolume = Self.loadOptional("erika_presenter_get_volume", from: libraryHandle, as: GetVolumeFn.self) + setMuted = Self.loadOptional("erika_presenter_set_muted", from: libraryHandle, as: SetMutedFn.self) setUpscaler = Self.loadOptional("erika_presenter_set_upscaler", from: libraryHandle, as: SetUpscalerFn.self) setSubtitleScale = Self.loadOptional("erika_presenter_set_subtitle_scale", from: libraryHandle, as: SetSubtitleScaleFn.self) getUpscalerStatus = Self.loadOptional("erika_presenter_get_upscaler_status", from: libraryHandle, as: GetUpscalerStatusFn.self) @@ -537,6 +543,22 @@ private final class ErikaPlayerHost { try check(setVolume(handle, clampedVolume), operation: "set_volume") } + func volume() throws -> Double { + guard let getVolume = library.getVolume else { + throw ErikaPluginError.symbolMissing("erika_presenter_get_volume") + } + var value: Double = 1.0 + try check(getVolume(handle, &value), operation: "get_volume") + return value + } + + func setMuted(_ muted: Bool) throws { + guard let setMuted = library.setMuted else { + throw ErikaPluginError.symbolMissing("erika_presenter_set_muted") + } + try check(setMuted(handle, muted), operation: "set_muted") + } + func setUpscaler(mode: Int32) throws { guard let setUpscaler = library.setUpscaler else { throw ErikaPluginError.symbolMissing("erika_presenter_set_upscaler") @@ -1496,6 +1518,17 @@ public final class ErikaFlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHan } try host.setVolume(volume) result(nil) + case "getVolume": + let args = try dictionaryArgs(call.arguments) + result(try playerHost(from: args).volume()) + case "setMuted": + let args = try dictionaryArgs(call.arguments) + let host = try playerHost(from: args) + guard let muted = boolValue(args["muted"]) else { + throw ErikaPluginError.invalidArguments("muted is required.") + } + try host.setMuted(muted) + result(nil) case "setUpscaler": let args = try dictionaryArgs(call.arguments) let host = try playerHost(from: args) diff --git a/packages/erika_flutter/test/erika_player_test.dart b/packages/erika_flutter/test/erika_player_test.dart index 1a23d78..9ca02bf 100644 --- a/packages/erika_flutter/test/erika_player_test.dart +++ b/packages/erika_flutter/test/erika_player_test.dart @@ -402,6 +402,50 @@ void main() { await player.dispose(); }); + test('mute flag is forwarded to native player', () async { + final player = ErikaPlayer(); + + await player.setMuted(true); + await player.setMuted(false); + + final calls = playerCalls + .where((MethodCall call) => call.method == 'setMuted') + .toList(); + expect(calls, hasLength(2)); + expect(calls[0].arguments, { + 'playerId': 7, + 'muted': true, + }); + expect(calls[1].arguments, { + 'playerId': 7, + 'muted': false, + }); + + await player.dispose(); + }); + + test('volume getter reads the saved native volume', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(playerChannel, (MethodCall call) async { + playerCalls.add(call); + return switch (call.method) { + 'create' => 7, + 'getVolume' => 0.35, + _ => null, + }; + }); + final player = ErikaPlayer(); + + expect(await player.getVolume(), closeTo(0.35, 0.000001)); + + final call = playerCalls.singleWhere( + (MethodCall call) => call.method == 'getVolume', + ); + expect(call.arguments, {'playerId': 7}); + + await player.dispose(); + }); + test('window overlay methods forward surface geometry', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(playerChannel, (MethodCall call) async { diff --git a/packages/erika_flutter/windows/erika_flutter_plugin.cpp b/packages/erika_flutter/windows/erika_flutter_plugin.cpp index 56cbf6b..98ae0c2 100644 --- a/packages/erika_flutter/windows/erika_flutter_plugin.cpp +++ b/packages/erika_flutter/windows/erika_flutter_plugin.cpp @@ -638,6 +638,8 @@ struct ErikaFlutterPlugin::ErikaNativeLibrary { using SeekFn = ErikaStatus (*)(ErikaPresenterHandle*, uint64_t); using SetPlaybackRateFn = ErikaStatus (*)(ErikaPresenterHandle*, double); using SetVolumeFn = ErikaStatus (*)(ErikaPresenterHandle*, double); + using GetVolumeFn = ErikaStatus (*)(ErikaPresenterHandle*, double*); + using SetMutedFn = ErikaStatus (*)(ErikaPresenterHandle*, bool); using SetUpscalerFn = ErikaStatus (*)(ErikaPresenterHandle*, int32_t); using SetSubtitleScaleFn = ErikaStatus (*)(ErikaPresenterHandle*, double); using GetUpscalerStatusFn = @@ -746,6 +748,8 @@ struct ErikaFlutterPlugin::ErikaNativeLibrary { SeekFn seek = nullptr; SetPlaybackRateFn set_playback_rate = nullptr; SetVolumeFn set_volume = nullptr; + GetVolumeFn get_volume = nullptr; + SetMutedFn set_muted = nullptr; SetUpscalerFn set_upscaler = nullptr; SetSubtitleScaleFn set_subtitle_scale = nullptr; GetUpscalerStatusFn get_upscaler_status = nullptr; @@ -803,6 +807,8 @@ struct ErikaFlutterPlugin::ErikaNativeLibrary { set_playback_rate = LoadOptional("erika_presenter_set_playback_rate"); set_volume = LoadOptional("erika_presenter_set_volume"); + get_volume = LoadOptional("erika_presenter_get_volume"); + set_muted = LoadOptional("erika_presenter_set_muted"); set_upscaler = LoadOptional("erika_presenter_set_upscaler"); set_subtitle_scale = LoadOptional( @@ -1147,6 +1153,24 @@ struct ErikaFlutterPlugin::PlayerHost { library->TakeLastError()); } + double GetVolume() { + if (library->get_volume == nullptr) { + throw PluginError("Missing Erika C ABI symbol: erika_presenter_get_volume"); + } + double volume = 1.0; + Check(library->get_volume(handle, &volume), "get_volume", + library->TakeLastError()); + return volume; + } + + void SetMuted(bool muted) { + if (library->set_muted == nullptr) { + throw PluginError("Missing Erika C ABI symbol: erika_presenter_set_muted"); + } + Check(library->set_muted(handle, muted), "set_muted", + library->TakeLastError()); + } + void SetUpscaler(int32_t mode) { if (library->set_upscaler == nullptr) { throw PluginError("Missing Erika C ABI symbol: erika_presenter_set_upscaler"); @@ -2135,6 +2159,12 @@ void ErikaFlutterPlugin::HandleMethodCall( PlayerFromArgs(args).SetVolume( DoubleValue(FindArg(args, "volume")).value_or(1.0)); result->Success(); + } else if (method == "getVolume") { + result->Success(EncodableValue(PlayerFromArgs(args).GetVolume())); + } else if (method == "setMuted") { + PlayerFromArgs(args).SetMuted( + BoolValue(FindArg(args, "muted")).value_or(false)); + result->Success(); } else if (method == "setUpscaler") { PlayerFromArgs(args).SetUpscaler( static_cast(RequiredInt64(args, "mode")));