diff --git a/apps/common-app/src/examples/AudioFile/AudioFile.tsx b/apps/common-app/src/examples/AudioFile/AudioFile.tsx index 20a25c1b4..6e88994bc 100644 --- a/apps/common-app/src/examples/AudioFile/AudioFile.tsx +++ b/apps/common-app/src/examples/AudioFile/AudioFile.tsx @@ -76,6 +76,7 @@ const AudioFile: FC = () => { state: 'paused', speed: 1.0, elapsedTime: 0, + skipInterval: 10, // seconds to skip on fast-forward/rewind; default: 15 }); await PlaybackNotificationManager.enableControl('skipBackward', true); await PlaybackNotificationManager.enableControl('nextTrack', true); diff --git a/packages/audiodocs/docs/system/playback-notification-manager.mdx b/packages/audiodocs/docs/system/playback-notification-manager.mdx index 0d42fc03d..b84cf25d7 100644 --- a/packages/audiodocs/docs/system/playback-notification-manager.mdx +++ b/packages/audiodocs/docs/system/playback-notification-manager.mdx @@ -35,6 +35,7 @@ await PlaybackNotificationManager.show({ artist: 'My Artist', duration: 180, state: 'paused', + skipInterval: 10 }); // Listen for notification controls @@ -54,6 +55,20 @@ const pauseListener = PlaybackNotificationManager.addEventListener( } ); +const skipForwardListener = PlaybackNotificationManager.addEventListener( + 'playbackNotificationSkipForward', + (event) => { + seekBy(event.value); + } +); + +const skipBackwardListener = PlaybackNotificationManager.addEventListener( + 'playbackNotificationSkipBackward', + (event) => { + seekBy(-event.value); + } +); + const seekToListener = PlaybackNotificationManager.addEventListener( 'playbackNotificationSeekTo', (event) => { @@ -68,6 +83,8 @@ PlaybackNotificationManager.show({ elapsedTime: 60 }); // Cleanup playListener.remove(); pauseListener.remove(); +skipForwardListener.remove(); +skipBackwardListener.remove(); seekToListener.remove(); PlaybackNotificationManager.hide(); ``` @@ -152,10 +169,20 @@ interface PlaybackNotificationInfo { elapsedTime?: number; speed?: number; state?: 'playing' | 'paused'; + + skipInterval?: number; } ``` +#### `skipInterval` + +- Configures how many seconds `playbackNotificationSkipForward` and `playbackNotificationSkipBackward` advance or rewind. +- Defaults to `15` when omitted. +- Clamped to `1–120` seconds in JavaScript before being passed to native. +- Pass `skipInterval` in the same `show()` call (or earlier) before enabling skip controls, so the interval is applied when controls are first set up. +- On Android, skip action icons show `15` when `skipInterval` is `15` (default); otherwise generic skip icons without a number are used. + ### `PlaybackControlName`
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 3364c7ab0..2b3d7c3dd 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 @@ -45,8 +45,13 @@ class PlaybackNotification( const val ACTION_SKIP_FORWARD = "com.swmansion.audioapi.ACTION_SKIP_FORWARD" const val ACTION_SKIP_BACKWARD = "com.swmansion.audioapi.ACTION_SKIP_BACKWARD" const val ID = 100 + + // Must match kDefaultSkipIntervalSeconds on iOS. + const val DEFAULT_SKIP_INTERVAL_SECONDS = 15 } + private var skipIntervalSeconds: Int = DEFAULT_SKIP_INTERVAL_SECONDS + private var mediaSession: MediaSessionCompat? = null private var notificationBuilder: NotificationCompat.Builder? = null private var pb: PlaybackStateCompat.Builder = PlaybackStateCompat.Builder() @@ -99,12 +104,12 @@ class PlaybackNotification( } override fun onFastForward() { - val body = HashMap().apply { put("value", 15) } + val body = HashMap().apply { put("value", skipIntervalSeconds) } audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_FORWARD.ordinal, body) } override fun onRewind() { - val body = HashMap().apply { put("value", 15) } + val body = HashMap().apply { put("value", skipIntervalSeconds) } audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_BACKWARD.ordinal, body) } @@ -164,6 +169,9 @@ class PlaybackNotification( } override fun show(options: ReadableMap?): Notification { + if (options != null) { + updateSkipIntervalFromOptions(options) + } initializeIfNeeded() if (options != null) { updateInternal(options) @@ -194,7 +202,42 @@ class PlaybackNotification( override fun getChannelId(): String = channelId + private fun updateSkipIntervalFromOptions(info: ReadableMap) { + if (info.hasKey("skipInterval")) { + skipIntervalSeconds = info.getDouble("skipInterval").toInt() + if (isInitialized) { + refreshSkipControlIcons() + } + } + } + + private fun skipForwardIcon(): Int = + if (skipIntervalSeconds == DEFAULT_SKIP_INTERVAL_SECONDS) { + R.drawable.skip_forward_15 + } else { + R.drawable.skip_forward + } + + private fun skipBackwardIcon(): Int = + if (skipIntervalSeconds == DEFAULT_SKIP_INTERVAL_SECONDS) { + R.drawable.skip_backward_15 + } else { + R.drawable.skip_backward + } + + private fun refreshSkipControlIcons() { + if (hasControl(PlaybackStateCompat.ACTION_REWIND) || + hasControl(PlaybackStateCompat.ACTION_FAST_FORWARD) + ) { + updatePlaybackActionState() + updatePlaybackState(playbackStateVal) + } + updateNotificationsActions() + } + private fun updateInternal(info: ReadableMap) { + updateSkipIntervalFromOptions(info) + if (info.hasKey("control") && info.hasKey("enabled")) { enableControl(info.getString("control"), info.getBoolean("enabled")) } @@ -328,7 +371,7 @@ class PlaybackNotification( .Builder( "SkipBackward", "Skip Backward", - R.drawable.skip_backward_15, + skipBackwardIcon(), ).build(), ) } @@ -339,7 +382,7 @@ class PlaybackNotification( .Builder( "SkipForward", "Skip Forward", - R.drawable.skip_forward_15, + skipForwardIcon(), ).build(), ) } @@ -377,7 +420,7 @@ class PlaybackNotification( if (hasControl(PlaybackStateCompat.ACTION_REWIND)) { notificationBuilder?.addAction( - createAction("skip_backward", "Skip Backward", R.drawable.skip_backward_15, PlaybackStateCompat.ACTION_REWIND), + createAction("skip_backward", "Skip Backward", skipBackwardIcon(), PlaybackStateCompat.ACTION_REWIND), ) actionsList.add(index++) } @@ -403,7 +446,7 @@ class PlaybackNotification( if (hasControl(PlaybackStateCompat.ACTION_FAST_FORWARD)) { notificationBuilder?.addAction( - createAction("skip_forward", "Skip Forward", R.drawable.skip_forward_15, PlaybackStateCompat.ACTION_FAST_FORWARD), + createAction("skip_forward", "Skip Forward", skipForwardIcon(), PlaybackStateCompat.ACTION_FAST_FORWARD), ) actionsList.add(index++) } @@ -437,6 +480,7 @@ class PlaybackNotification( val customActionName = if (name == "skip_forward") ACTION_SKIP_FORWARD else ACTION_SKIP_BACKWARD val intent = Intent(customActionName) intent.setPackage(context.packageName) + intent.putExtra(PlaybackNotificationReceiver.EXTRA_SKIP_INTERVAL_SECONDS, skipIntervalSeconds) pendingIntent = PendingIntent.getBroadcast( context, diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt index d1cb3b679..f5f4f0701 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt @@ -14,6 +14,7 @@ class PlaybackNotificationReceiver : BroadcastReceiver() { const val ACTION_NOTIFICATION_DISMISSED = "com.swmansion.audioapi.PLAYBACK_NOTIFICATION_DISMISSED" const val ACTION_SKIP_FORWARD = "com.swmansion.audioapi.ACTION_SKIP_FORWARD" const val ACTION_SKIP_BACKWARD = "com.swmansion.audioapi.ACTION_SKIP_BACKWARD" + const val EXTRA_SKIP_INTERVAL_SECONDS = "skipIntervalSeconds" private var audioAPIModule: AudioAPIModule? = null @@ -32,12 +33,22 @@ class PlaybackNotificationReceiver : BroadcastReceiver() { } ACTION_SKIP_FORWARD -> { - val body = HashMap().apply { put("value", 15) } + val skipInterval = + intent.getIntExtra( + EXTRA_SKIP_INTERVAL_SECONDS, + PlaybackNotification.DEFAULT_SKIP_INTERVAL_SECONDS, + ) + val body = HashMap().apply { put("value", skipInterval) } audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_FORWARD.ordinal, body) } ACTION_SKIP_BACKWARD -> { - val body = HashMap().apply { put("value", 15) } + val skipInterval = + intent.getIntExtra( + EXTRA_SKIP_INTERVAL_SECONDS, + PlaybackNotification.DEFAULT_SKIP_INTERVAL_SECONDS, + ) + val body = HashMap().apply { put("value", skipInterval) } audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_BACKWARD.ordinal, body) } } diff --git a/packages/react-native-audio-api/android/src/main/res/drawable/skip_backward.xml b/packages/react-native-audio-api/android/src/main/res/drawable/skip_backward.xml new file mode 100644 index 000000000..14d85a0e0 --- /dev/null +++ b/packages/react-native-audio-api/android/src/main/res/drawable/skip_backward.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/react-native-audio-api/android/src/main/res/drawable/skip_forward.xml b/packages/react-native-audio-api/android/src/main/res/drawable/skip_forward.xml new file mode 100644 index 000000000..5b935a2d4 --- /dev/null +++ b/packages/react-native-audio-api/android/src/main/res/drawable/skip_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/notification/PlaybackNotification.mm b/packages/react-native-audio-api/ios/audioapi/ios/system/notification/PlaybackNotification.mm index ce533ba8e..b9181a218 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/notification/PlaybackNotification.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/notification/PlaybackNotification.mm @@ -12,9 +12,13 @@ @"artwork" : MPMediaItemPropertyArtwork \ } +// Must match PlaybackNotification.DEFAULT_SKIP_INTERVAL_SECONDS on Android. +static const NSInteger kDefaultSkipIntervalSeconds = 15; + @implementation PlaybackNotification { BOOL _isInitialized; NSMutableDictionary *_currentInfo; + NSInteger _skipInterval; } - (instancetype)initWithAudioAPIModule:(AudioAPIModule *)audioAPIModule @@ -25,6 +29,7 @@ - (instancetype)initWithAudioAPIModule:(AudioAPIModule *)audioAPIModule _isInitialized = false; _isActive = false; _currentInfo = [[NSMutableDictionary alloc] init]; + _skipInterval = kDefaultSkipIntervalSeconds; } return self; @@ -58,6 +63,8 @@ - (BOOL)initializeWithOptions:(NSDictionary *)options - (BOOL)showWithOptions:(NSDictionary *)options { + [self updateSkipIntervalFromOptions:options]; + if (!_isInitialized) { if (![self initializeWithOptions:options]) { return false; @@ -137,6 +144,24 @@ - (NSString *)getNotificationType #pragma mark - Private Methods +- (void)updateSkipIntervalFromOptions:(NSDictionary *)options +{ + id skipIntervalValue = options[@"skipInterval"]; + if (skipIntervalValue == nil) { + return; + } + + _skipInterval = (NSInteger)[skipIntervalValue doubleValue]; + [self applySkipIntervals]; +} + +- (void)applySkipIntervals +{ + MPRemoteCommandCenter *remoteCenter = [MPRemoteCommandCenter sharedCommandCenter]; + remoteCenter.skipForwardCommand.preferredIntervals = @[ @(_skipInterval) ]; + remoteCenter.skipBackwardCommand.preferredIntervals = @[ @(_skipInterval) ]; +} + - (void)updateNowPlayingInfo:(NSDictionary *)info { if (!info) { @@ -300,12 +325,12 @@ - (void)enableRemoteCommand:(NSString *)name enabled:(BOOL)enabled withSelector:@selector(onPreviousTrack:) enabled:enabled]; } else if ([name isEqualToString:@"skipForward"]) { - remoteCenter.skipForwardCommand.preferredIntervals = @[ @(15) ]; + [self applySkipIntervals]; [self enableCommand:remoteCenter.skipForwardCommand withSelector:@selector(onSkipForward:) enabled:enabled]; } else if ([name isEqualToString:@"skipBackward"]) { - remoteCenter.skipBackwardCommand.preferredIntervals = @[ @(15) ]; + [self applySkipIntervals]; [self enableCommand:remoteCenter.skipBackwardCommand withSelector:@selector(onSkipBackward:) enabled:enabled]; 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 6be6c729b..3304d46e2 100644 --- a/packages/react-native-audio-api/src/system/notification/PlaybackNotificationManager.ts +++ b/packages/react-native-audio-api/src/system/notification/PlaybackNotificationManager.ts @@ -8,6 +8,11 @@ import type { PlaybackNotificationInfo, } from './types'; import { AudioApiError } from '../../errors'; +import { clamp } from '../../utils'; + +const DEFAULT_SKIP_INTERVAL_SECONDS = 15; +const MIN_SKIP_INTERVAL_SECONDS = 1; +const MAX_SKIP_INTERVAL_SECONDS = 120; class PlaybackNotificationManager implements NotificationManager< PlaybackNotificationInfo, @@ -15,6 +20,7 @@ class PlaybackNotificationManager implements NotificationManager< > { private notificationKey = 'playback'; private audioEventEmitter: AudioEventEmitter; + private skipIntervalSeconds = DEFAULT_SKIP_INTERVAL_SECONDS; constructor() { this.audioEventEmitter = new AudioEventEmitter( @@ -34,10 +40,21 @@ class PlaybackNotificationManager implements NotificationManager< throw new AudioApiError('NativeAudioAPIModule is not available'); } + if (info.skipInterval !== undefined) { + this.skipIntervalSeconds = clamp( + info.skipInterval, + MIN_SKIP_INTERVAL_SECONDS, + MAX_SKIP_INTERVAL_SECONDS + ); + } + const result = await NativeAudioAPIModule.showNotification( 'playback', this.notificationKey, - info as Record + { + ...info, + skipInterval: this.skipIntervalSeconds, + } as Record ); if (result.error) { diff --git a/packages/react-native-audio-api/src/system/notification/types.ts b/packages/react-native-audio-api/src/system/notification/types.ts index 533146432..3f1060233 100644 --- a/packages/react-native-audio-api/src/system/notification/types.ts +++ b/packages/react-native-audio-api/src/system/notification/types.ts @@ -35,6 +35,8 @@ export interface PlaybackNotificationInfo { elapsedTime?: number; speed?: number; state?: 'playing' | 'paused'; + /** Skip interval in seconds for skipForward/skipBackward. Default: 15 */ + skipInterval?: number; } /// Available playback control actions.