Skip to content
Merged
1 change: 1 addition & 0 deletions apps/common-app/src/examples/AudioFile/AudioFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
basiav marked this conversation as resolved.
});
await PlaybackNotificationManager.enableControl('skipBackward', true);
await PlaybackNotificationManager.enableControl('nextTrack', true);
Expand Down
27 changes: 27 additions & 0 deletions packages/audiodocs/docs/system/playback-notification-manager.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ await PlaybackNotificationManager.show({
artist: 'My Artist',
duration: 180,
state: 'paused',
skipInterval: 10
});

// Listen for notification controls
Expand All @@ -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) => {
Expand All @@ -68,6 +83,8 @@ PlaybackNotificationManager.show({ elapsedTime: 60 });
// Cleanup
playListener.remove();
pauseListener.remove();
skipForwardListener.remove();
skipBackwardListener.remove();
seekToListener.remove();
PlaybackNotificationManager.hide();
```
Expand Down Expand Up @@ -152,10 +169,20 @@ interface PlaybackNotificationInfo {
elapsedTime?: number;
speed?: number;
state?: 'playing' | 'paused';

skipInterval?: number;
}
```
</details>

#### `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`

<details>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -99,12 +104,12 @@ class PlaybackNotification(
}

override fun onFastForward() {
val body = HashMap<String, Any>().apply { put("value", 15) }
val body = HashMap<String, Any>().apply { put("value", skipIntervalSeconds) }
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_FORWARD.ordinal, body)
}

override fun onRewind() {
val body = HashMap<String, Any>().apply { put("value", 15) }
val body = HashMap<String, Any>().apply { put("value", skipIntervalSeconds) }
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_BACKWARD.ordinal, body)
}

Expand Down Expand Up @@ -164,6 +169,9 @@ class PlaybackNotification(
}

override fun show(options: ReadableMap?): Notification {
if (options != null) {
updateSkipIntervalFromOptions(options)
}
initializeIfNeeded()
if (options != null) {
updateInternal(options)
Expand Down Expand Up @@ -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"))
}
Expand Down Expand Up @@ -328,7 +371,7 @@ class PlaybackNotification(
.Builder(
"SkipBackward",
"Skip Backward",
R.drawable.skip_backward_15,
skipBackwardIcon(),
).build(),
)
}
Expand All @@ -339,7 +382,7 @@ class PlaybackNotification(
.Builder(
"SkipForward",
"Skip Forward",
R.drawable.skip_forward_15,
skipForwardIcon(),
).build(),
)
}
Expand Down Expand Up @@ -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++)
}
Expand All @@ -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++)
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -32,12 +33,22 @@ class PlaybackNotificationReceiver : BroadcastReceiver() {
}

ACTION_SKIP_FORWARD -> {
val body = HashMap<String, Any>().apply { put("value", 15) }
val skipInterval =
intent.getIntExtra(
EXTRA_SKIP_INTERVAL_SECONDS,
PlaybackNotification.DEFAULT_SKIP_INTERVAL_SECONDS,
)
val body = HashMap<String, Any>().apply { put("value", skipInterval) }
audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_FORWARD.ordinal, body)
}

ACTION_SKIP_BACKWARD -> {
val body = HashMap<String, Any>().apply { put("value", 15) }
val skipInterval =
intent.getIntExtra(
EXTRA_SKIP_INTERVAL_SECONDS,
PlaybackNotification.DEFAULT_SKIP_INTERVAL_SECONDS,
)
val body = HashMap<String, Any>().apply { put("value", skipInterval) }
audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_BACKWARD.ordinal, body)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,6L12,2l-5,5l5,5L12,8c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.42,6 12,6z"
android:fillColor="#FFFFFF"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,14c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8L18,14z"
android:fillColor="#FFFFFF"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
@"artwork" : MPMediaItemPropertyArtwork \
}

// Must match PlaybackNotification.DEFAULT_SKIP_INTERVAL_SECONDS on Android.
static const NSInteger kDefaultSkipIntervalSeconds = 15;
Comment thread
basiav marked this conversation as resolved.

@implementation PlaybackNotification {
BOOL _isInitialized;
NSMutableDictionary *_currentInfo;
NSInteger _skipInterval;
}

- (instancetype)initWithAudioAPIModule:(AudioAPIModule *)audioAPIModule
Expand All @@ -25,6 +29,7 @@ - (instancetype)initWithAudioAPIModule:(AudioAPIModule *)audioAPIModule
_isInitialized = false;
_isActive = false;
_currentInfo = [[NSMutableDictionary alloc] init];
_skipInterval = kDefaultSkipIntervalSeconds;
}

return self;
Expand Down Expand Up @@ -58,6 +63,8 @@ - (BOOL)initializeWithOptions:(NSDictionary *)options

- (BOOL)showWithOptions:(NSDictionary *)options
{
[self updateSkipIntervalFromOptions:options];

if (!_isInitialized) {
if (![self initializeWithOptions:options]) {
return false;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ 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,
PlaybackNotificationEventName
> {
private notificationKey = 'playback';
private audioEventEmitter: AudioEventEmitter;
private skipIntervalSeconds = DEFAULT_SKIP_INTERVAL_SECONDS;

constructor() {
this.audioEventEmitter = new AudioEventEmitter(
Expand All @@ -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<string, string | number | boolean | undefined>
{
...info,
skipInterval: this.skipIntervalSeconds,
} as Record<string, string | number | boolean | undefined>
);

if (result.error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface PlaybackNotificationInfo {
elapsedTime?: number;
speed?: number;
state?: 'playing' | 'paused';
/** Skip interval in seconds for skipForward/skipBackward. Default: 15 */
Comment thread
basiav marked this conversation as resolved.
skipInterval?: number;
}

/// Available playback control actions.
Expand Down
Loading