diff --git a/apps/common-app/src/demos/Record/Record.tsx b/apps/common-app/src/demos/Record/Record.tsx index 9f3f1b6b3..784c56612 100644 --- a/apps/common-app/src/demos/Record/Record.tsx +++ b/apps/common-app/src/demos/Record/Record.tsx @@ -24,7 +24,13 @@ const Record: FC = () => { const [state, setState] = useState(RecordingState.Idle); const [hasPermissions, setHasPermissions] = useState(false); - const setNotification = (paused: boolean) => { + const updateNotification = (paused: boolean) => { + RecordingNotificationManager.show({ + paused, + }); + }; + + const setupNotification = (paused: boolean) => { RecordingNotificationManager.show({ title: 'Recording Demo', contentText: paused ? 'Paused recording' : 'Recording...', @@ -34,7 +40,7 @@ const Record: FC = () => { resumeIconResourceName: 'resume', color: 0xff6200, }); - }; + } const onStartRecording = useCallback(async () => { if (state !== RecordingState.Idle) { @@ -62,7 +68,7 @@ const Record: FC = () => { } const result = Recorder.start(); - setNotification(false); + setupNotification(false); if (result.status === 'success') { console.log('Recording started, file path:', result.path); @@ -77,13 +83,13 @@ const Record: FC = () => { const onPauseRecording = useCallback(() => { Recorder.pause(); - setNotification(true); + updateNotification(true); setState(RecordingState.Paused); }, []); const onResumeRecording = useCallback(() => { Recorder.resume(); - setNotification(false); + updateNotification(false); setState(RecordingState.Recording); }, []); diff --git a/apps/common-app/src/examples/AudioFile/AudioPlayer.ts b/apps/common-app/src/examples/AudioFile/AudioPlayer.ts index 5441e86c3..c65c61750 100644 --- a/apps/common-app/src/examples/AudioFile/AudioPlayer.ts +++ b/apps/common-app/src/examples/AudioFile/AudioPlayer.ts @@ -35,7 +35,7 @@ class AudioPlayer { } this.isPlaying = true; - PlaybackNotificationManager.update({ + PlaybackNotificationManager.show({ state: 'playing', }); @@ -75,8 +75,9 @@ class AudioPlayer { this.sourceNode?.stop(this.audioContext.currentTime); await this.audioContext.suspend(); - PlaybackNotificationManager.update({ + PlaybackNotificationManager.show({ state: 'paused', + elapsedTime: this.currentElapsedTime, }); this.isPlaying = false; @@ -90,7 +91,7 @@ class AudioPlayer { } else if (this.currentElapsedTime > this.getDuration()) { this.currentElapsedTime = this.getDuration(); } - PlaybackNotificationManager.update({ + PlaybackNotificationManager.show({ elapsedTime: this.currentElapsedTime, }); diff --git a/packages/audiodocs/docs/system/audio-manager.mdx b/packages/audiodocs/docs/system/audio-manager.mdx index dbca0ae4d..225b8c046 100644 --- a/packages/audiodocs/docs/system/audio-manager.mdx +++ b/packages/audiodocs/docs/system/audio-manager.mdx @@ -242,7 +242,7 @@ interface EventTypeWithValue { } interface OnInterruptionEventType { - type: 'ended' | 'began'; / /if interruption event has started or ended + type: 'ended' | 'began'; // if interruption event has started or ended shouldResume: boolean; // if the interruption was temporary and we can resume the playback/recording } @@ -284,28 +284,11 @@ type SystemEventCallback = (
Type definitions ```typescript -class AudioEventSubscription { - private readonly audioEventEmitter: AudioEventEmitter; - private readonly eventName: AudioEventName; +interface AudioEventSubscription { /** @internal */ public readonly subscriptionId: string; - constructor( - subscriptionId: string, - eventName: AudioEventName, - audioEventEmitter: AudioEventEmitter - ) { - this.subscriptionId = subscriptionId; - this.eventName = eventName; - this.audioEventEmitter = audioEventEmitter; - } - - public remove(): void { - this.audioEventEmitter.removeAudioEventListener( - this.eventName, - this.subscriptionId - ); - } + public remove(): void; // used to remove the subscription } ```
diff --git a/packages/audiodocs/docs/system/playback-notification-manager.mdx b/packages/audiodocs/docs/system/playback-notification-manager.mdx index 5c0a0ef28..a90d34b85 100644 --- a/packages/audiodocs/docs/system/playback-notification-manager.mdx +++ b/packages/audiodocs/docs/system/playback-notification-manager.mdx @@ -17,7 +17,7 @@ The `PlaybackNotificationManager` provides media session integration and playbac **iOS Requirements:** - Notification controls only appear when an active `AudioContext` is running -- `show()`, `update()` or `hide()` only update metadata - they don't control notification visibility +- `show()` or `hide()` only update metadata - they don't control notification visibility - The notification automatically appears/disappears based on audio session state - To show: create and resume an AudioContext - To hide: suspend or close the AudioContext @@ -25,14 +25,12 @@ The `PlaybackNotificationManager` provides media session integration and playbac **Android:** - Notification visibility is directly controlled by `show()` and `hide()` methods - Works independently of AudioContext state - ::: ## Example ```tsx -// Register and show notification -await PlaybackNotificationManager.register(); +// show notification await PlaybackNotificationManager.show({ title: 'My Song', artist: 'My Artist', @@ -45,7 +43,7 @@ const playListener = PlaybackNotificationManager.addEventListener( 'playbackNotificationPlay', () => { // Handle play action - PlaybackNotificationManager.update({ state: 'playing' }); + PlaybackNotificationManager.show({ state: 'playing' }); } ); @@ -53,7 +51,7 @@ const pauseListener = PlaybackNotificationManager.addEventListener( 'playbackNotificationPause', () => { // Handle pause action - PlaybackNotificationManager.update({ state: 'paused' }); + PlaybackNotificationManager.show({ state: 'paused' }); } ); @@ -61,34 +59,22 @@ const seekToListener = PlaybackNotificationManager.addEventListener( 'playbackNotificationSeekTo', (event) => { // Handle seek to position (event.value is in seconds) - PlaybackNotificationManager.update({ elapsedTime: event.value }); + PlaybackNotificationManager.show({ elapsedTime: event.value }); } ); // Update progress -PlaybackNotificationManager.update({ elapsedTime: 60 }); +PlaybackNotificationManager.show({ elapsedTime: 60 }); // Cleanup -PlaybackNotificationManager.removeEventListener(playListener); -PlaybackNotificationManager.removeEventListener(pauseListener); -PlaybackNotificationManager.removeEventListener(seekToListener); -PlaybackNotificationManager.unregister(); +playListener.remove(); +pauseListener.remove(); +seekToListener.remove(); +PlaybackNotificationManager.hide(); ``` ## Methods -### `register` - -Register the playback notification with the system. Must be called before showing any notification. - -#### Returns `Promise`. - -#### Errors - -| Error type | Description | -| :--------: | :---------------------------------------------------------- | -| `Error` | NativeAudioAPIModule is not available or registration fails | - ### `show` Display the notification with initial metadata. @@ -97,29 +83,13 @@ Display the notification with initial metadata. On iOS, this method only sets the metadata. The notification controls will only appear when an `AudioContext` is actively running. Make sure to create and resume an AudioContext before calling `show()`. ::: -| Parameter | Type | Description | -| :-------: | :-----------------------------------------------------------------------------------------------: | :---------------------------- | -| `info` | [`PlaybackNotificationInfo`](/docs/system/playback-notification-manager#playbacknotificationinfo) | Initial notification metadata | - -#### Returns `Promise`. - -#### Errors - -| Error type | Description | -| :--------: | :----------------------------------------------------------------- | -| `Error` | Notification must be registered first or native module unavailable | - -### `update` - -Update the notification with new metadata or state. - -:::note iOS Behavior -On iOS, this method only updates the metadata. It does not control notification visibility - controls remain visible as long as the AudioContext is running. +:::info +Metadata is remembered between calls, so after initial passing the metadata to show function, you can only call it with elements that are supposed to change. ::: -| Parameter | Type | Description | -| :-------: | :-----------------------------------------------------------------------------------------------: | :---------------------------- | -| `info` | [`PlaybackNotificationInfo`](/docs/system/playback-notification-manager#playbacknotificationinfo) | Updated notification metadata | +| Parameter | Type | Description | +| :-------: | :----------: | :----- | +| `info` | [`PlaybackNotificationInfo`](playback-notification-manager#playbacknotificationinfo) | Initial notification metadata | #### Returns `Promise`. @@ -133,20 +103,14 @@ On iOS, this method clears the metadata but does not hide the notification contr #### Returns `Promise`. -### `unregister` - -Unregister the notification from the system. Must call `register()` again to use. - -#### Returns `Promise`. - ### `enableControl` Enable or disable specific playback controls. -| Parameter | Type | Description | -| :-------: | :-------------------------------------------------------------------------------------: | :------------------------------------ | -| `control` | [`PlaybackControlName`](/docs/system/playback-notification-manager#playbackcontrolname) | The control to enable/disable | -| `enabled` | `boolean` | Whether the control should be enabled | +| Parameter | Type | Description | +| :-------: | :-----: | :------ | +| `control` | [`PlaybackControlName`](playback-notification-manager#playbackcontrolname) | The control to enable/disable | +| `enabled` | `boolean` | Whether the control should be enabled | #### Returns `Promise`. @@ -160,21 +124,13 @@ Check if the notification is currently active and visible. Add an event listener for notification actions. -| Parameter | Type | Description | -| :---------: | :---------------------------------------------------------------------------------------------------------: | :---------------------- | -| `eventName` | [`PlaybackNotificationEventName`](/docs/system/playback-notification-manager#playbacknotificationeventname) | The event to listen for | -| `callback` | [`SystemEventCallback`](/docs/system/audio-manager#systemeventname--remotecommandeventname) | Callback function | +| Parameter | Type | Description | +| :---------: | :------: | :------- | +| `eventName` | [`PlaybackNotificationEventName`](playback-notification-manager#playbacknotificationeventname) | The event to listen for | +| `callback` | [`SystemEventCallback`](/docs/system/audio-manager#systemeventname--remotecommandeventname) | Callback function | #### Returns [`AudioEventSubscription`](/docs/system/audio-manager#audioeventsubscription). -### `removeEventListener` - -Remove an event listener. - -| Parameter | Type | Description | -| :------------: | :---------------------------------------------------------------------------: | :------------------------- | -| `subscription` | [`AudioEventSubscription`](/docs/system/audio-manager#audioeventsubscription) | The subscription to remove | - ## Remarks ### `PlaybackNotificationInfo` @@ -205,16 +161,16 @@ interface PlaybackNotificationInfo {
Type definitions - ```typescript - type PlaybackControlName = - | 'play' - | 'pause' - | 'next' - | 'previous' - | 'skipForward' - | 'skipBackward' - | 'seekTo'; - ``` +```typescript +type PlaybackControlName = + | 'play' + | 'pause' + | 'next' + | 'previous' + | 'skipForward' + | 'skipBackward' + | 'seekTo'; +```
### `PlaybackNotificationEventName` @@ -222,19 +178,23 @@ interface PlaybackNotificationInfo {
Type definitions ```typescript - interface PlaybackNotificationEvent { - playbackNotificationPlay: EventEmptyType; - playbackNotificationPause: EventEmptyType; - playbackNotificationNext: EventEmptyType; - playbackNotificationPrevious: EventEmptyType; - playbackNotificationSkipForward: EventTypeWithValue; - playbackNotificationSkipBackward: EventTypeWithValue; - playbackNotificationSeekTo: EventTypeWithValue; - playbackNotificationDismissed: EventEmptyType; - } - - type PlaybackNotificationEventName = keyof PlaybackNotificationEvent; - ``` +interface EventTypeWithValue { + value: number; +} + +interface PlaybackNotificationEvent { + playbackNotificationPlay: EventEmptyType; + playbackNotificationPause: EventEmptyType; + playbackNotificationNext: EventEmptyType; + playbackNotificationPrevious: EventEmptyType; + playbackNotificationSkipForward: EventTypeWithValue; + playbackNotificationSkipBackward: EventTypeWithValue; + playbackNotificationSeekTo: EventTypeWithValue; + playbackNotificationDismissed: EventEmptyType; +} + +type PlaybackNotificationEventName = keyof PlaybackNotificationEvent; +```
``` diff --git a/packages/audiodocs/docs/system/recording-notification-manager.mdx b/packages/audiodocs/docs/system/recording-notification-manager.mdx index 6d2c76598..7c43a03e0 100644 --- a/packages/audiodocs/docs/system/recording-notification-manager.mdx +++ b/packages/audiodocs/docs/system/recording-notification-manager.mdx @@ -31,8 +31,8 @@ const resumeEventListener = RecordingNotificationManager.addEventListener('recor console.log('Notification resume action received'); }); -RecordingNotificationManager.removeEventListener(pauseEventListener); -RecordingNotificationManager.removeEventListener(resumeEventListener); +pauseEventListener.remove(); +resumeEventListener.remove(); RecordingNotificationManager.hide(); ``` @@ -42,9 +42,14 @@ RecordingNotificationManager.hide(); Shows the recording notification with the parameters. +:::info +Metadata is saved between calls, so after the initial pass to the show method, you need only call it with elements that are supposed to change. +::: + + | Parameter |Type| Description| | :-------: | :--: | :----| -| `info` | [`RecordingNotificationInfo`](recording-notification-manager#recordingnotificationinfo) | Initial notification metadata | +| `info` | [`RecordingNotificationInfo`](recording-notification-manager#recordingnotificationinfo) | Initial notification metadata | #### Returns `Promise`. diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt index ff308bbcd..ca5cc0612 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt @@ -13,7 +13,6 @@ import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.view.KeyEvent import androidx.core.app.NotificationCompat -import androidx.core.graphics.drawable.IconCompat import androidx.media.app.NotificationCompat.MediaStyle import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt index 57eaa4c07..38924a24a 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt @@ -239,19 +239,46 @@ class RecordingNotification( } private fun parseMapFromRN(options: ReadableMap?) { - state.title = if (options?.hasKey("title") == true) options.getString("title") else "Recording Audio" + state.title = if (options?.hasKey("title") == true) options.getString("title") else state.title ?: "Recording Audio" state.contentText = if (options?.hasKey("contentText") == true) { options.getString("contentText") } else { - "Audio recording is in progress/paused" + state.contentText ?: "Audio recording is in progress/paused" + } + state.smallIconResourceName = + if (options?.hasKey("smallIconResourceName") == + true + ) { + options.getString("smallIconResourceName") + } else { + state.smallIconResourceName ?: null + } + state.largeIconResourceName = + if (options?.hasKey("largeIconResourceName") == + true + ) { + options.getString("largeIconResourceName") + } else { + state.largeIconResourceName ?: null + } + state.pauseIconResourceName = + if (options?.hasKey("pauseIconResourceName") == + true + ) { + options.getString("pauseIconResourceName") + } else { + state.pauseIconResourceName ?: null } - state.smallIconResourceName = if (options?.hasKey("smallIconResourceName") == true) options.getString("smallIconResourceName") else null - state.largeIconResourceName = if (options?.hasKey("largeIconResourceName") == true) options.getString("largeIconResourceName") else null - state.pauseIconResourceName = if (options?.hasKey("pauseIconResourceName") == true) options.getString("pauseIconResourceName") else null state.resumeIconResourceName = - if (options?.hasKey("resumeIconResourceName") == true) options.getString("resumeIconResourceName") else null - state.backgroundColor = if (options?.hasKey("color") == true) options.getInt("color") else null + if (options?.hasKey("resumeIconResourceName") == + true + ) { + options.getString("resumeIconResourceName") + } else { + state.resumeIconResourceName ?: null + } + state.backgroundColor = if (options?.hasKey("color") == true) options.getInt("color") else state.backgroundColor ?: null state.paused = if (options?.hasKey("paused") == true) options.getBoolean("paused") else false } diff --git a/packages/react-native-audio-api/src/system/notification/PlaybackNotificationManager.ts b/packages/react-native-audio-api/src/system/notification/PlaybackNotificationManager.ts index adaf50c69..1ccc9e1e5 100644 --- a/packages/react-native-audio-api/src/system/notification/PlaybackNotificationManager.ts +++ b/packages/react-native-audio-api/src/system/notification/PlaybackNotificationManager.ts @@ -9,7 +9,6 @@ import type { } from './types'; import { AudioApiError } from '../../errors'; -/// Manager for media playback notifications with controls and MediaSession integration. class PlaybackNotificationManager implements NotificationManager @@ -21,8 +20,13 @@ class PlaybackNotificationManager this.audioEventEmitter = new AudioEventEmitter(global.AudioEventEmitter); } - /// Show the notification with metadata or update if already visible. - /// Automatically creates the notification on first call. + /** + * Show the notification with metadata or update if already visible. + * Automatically creates the notification on first call. + * + * @param info - The info to be displayed. + * @returns Promise that resolves after creating notification. + */ async show(info: PlaybackNotificationInfo): Promise { if (!NativeAudioAPIModule) { throw new AudioApiError('NativeAudioAPIModule is not available'); @@ -39,13 +43,11 @@ class PlaybackNotificationManager } } - /// Update the notification with new metadata or state. - /// This is an alias for show() since show handles both initial display and updates. - async update(info: PlaybackNotificationInfo): Promise { - return this.show(info); - } - - /// Hide the notification. + /** + * Hide the notification. + * + * @returns Promise that resolves after hiding notification. + */ async hide(): Promise { if (!NativeAudioAPIModule) { throw new AudioApiError('NativeAudioAPIModule is not available'); @@ -60,7 +62,13 @@ class PlaybackNotificationManager } } - /// Enable or disable a specific playback control. + /** + * Enable or disable a specific playback control. + * + * @param control - The control to enable or disable on the notification. + * @param enabled - Whether to enable (true) or disable (false) the control. + * @returns Promise that resolves after showing modified notification. + */ async enableControl( control: PlaybackControlName, enabled: boolean @@ -81,7 +89,11 @@ class PlaybackNotificationManager } } - /// Check if the notification is currently active. + /** + * Check if the notification is currently active. + * + * @returns Promise that resolves to whether notification is active. + */ async isActive(): Promise { if (!NativeAudioAPIModule) { return false; @@ -92,7 +104,13 @@ class PlaybackNotificationManager ); } - /// Add an event listener for notification actions. + /** + * Add an event listener for notification actions. + * + * @param eventName - The event name to listen for. + * @param callback - The callback to invoke on event. + * @returns Class that represents the subscription. + */ addEventListener( eventName: T, callback: (event: NotificationEvents[T]) => void diff --git a/packages/react-native-audio-api/src/system/notification/RecordingNotificationManager.ts b/packages/react-native-audio-api/src/system/notification/RecordingNotificationManager.ts index 691b60723..101713a09 100644 --- a/packages/react-native-audio-api/src/system/notification/RecordingNotificationManager.ts +++ b/packages/react-native-audio-api/src/system/notification/RecordingNotificationManager.ts @@ -84,7 +84,7 @@ class RecordingNotificationManager * * @param eventName - The event name to listen for. * @param callback - The callback to invoke on event. - * @returns Promise that resolves to whether notification is active. + * @returns Class that represents the subscription. */ addEventListener( eventName: T,