react-native-media-notification 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -149,6 +149,8 @@ MediaControls.addEventListener('duck', () => {}); // reduce volume for interrupt
149
149
  MediaControls.addEventListener('unduck', () => {}); // restore volume after interruption
150
150
  ```
151
151
 
152
+ **Note**: On iOS, seekForward and seekBackward are not fired, even when technically triggered. Instead, a seek event with the corresponding timestamp is fired.
153
+
152
154
  ### Stop Media Notification
153
155
 
154
156
  ```typescript
@@ -58,11 +58,11 @@ class MediaControlsModule(reactContext: ReactApplicationContext) :
58
58
  ensureServiceStarted()
59
59
 
60
60
  val trackMetadata = MediaTrackMetadata(
61
- title = metadata.getString("title") ?: "",
62
- artist = metadata.getString("artist") ?: "",
63
- album = metadata.getString("album"),
61
+ title = if (metadata.hasKey("title")) metadata.getString("title") else null,
62
+ artist = if (metadata.hasKey("artist")) metadata.getString("artist") else null,
63
+ album = if (metadata.hasKey("album")) metadata.getString("album") else null,
64
64
  duration = if (metadata.hasKey("duration")) metadata.getDouble("duration") else null,
65
- artwork = metadata.getString("artwork"),
65
+ artwork = if (metadata.hasKey("artwork")) metadata.getString("artwork") else null,
66
66
  position = if (metadata.hasKey("position")) metadata.getDouble("position") else null,
67
67
  isPlaying = if (metadata.hasKey("isPlaying")) metadata.getBoolean("isPlaying") else null,
68
68
  shuffleMode = if (metadata.hasKey("shuffle")) metadata.getBoolean("shuffle") else null,
@@ -143,19 +143,19 @@ class MediaControlsPlayer(
143
143
  audioFocusListener.requestAudioFocus()
144
144
  }
145
145
 
146
- this.currentMetadata = metadata
146
+ this.currentMetadata = this.currentMetadata?.merge(metadata) ?: metadata
147
147
 
148
148
  val mediaMetadata = MediaMetadata.Builder()
149
- .setTitle(metadata.title)
150
- .setArtist(metadata.artist)
151
- .setAlbumTitle(metadata.album)
152
- .setDurationMs(metadata.duration?.times(1000)?.toLong())
149
+ .setTitle(this.currentMetadata!!.title)
150
+ .setArtist(this.currentMetadata!!.artist)
151
+ .setAlbumTitle(this.currentMetadata!!.album)
152
+ .setDurationMs(this.currentMetadata!!.duration?.times(1000)?.toLong())
153
153
  .setIsPlayable(true)
154
154
  .setIsBrowsable(false)
155
155
  .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
156
- .apply {
157
- metadata.artwork?.let { artworkUrl ->
158
- setArtworkUri(artworkUrl.toUri())
156
+ .also {
157
+ this.currentMetadata!!.artwork?.let { artworkUrl ->
158
+ it.setArtworkUri(artworkUrl.toUri())
159
159
  }
160
160
  }
161
161
  .build()
@@ -171,23 +171,23 @@ class MediaControlsPlayer(
171
171
 
172
172
  val mediaItemData = MediaItemData.Builder(mediaId)
173
173
  .setMediaItem(mediaItem)
174
- .setDefaultPositionUs(metadata.position?.times(1_000_000)?.toLong() ?: 0)
175
- .setDurationUs(metadata.duration?.times(1_000_000)?.toLong() ?: androidx.media3.common.C.TIME_UNSET)
174
+ .setDefaultPositionUs(this.currentMetadata!!.position?.times(1_000_000)?.toLong() ?: 0)
175
+ .setDurationUs(this.currentMetadata!!.duration?.times(1_000_000)?.toLong() ?: androidx.media3.common.C.TIME_UNSET)
176
176
  .setIsSeekable(true)
177
177
  .build()
178
178
 
179
179
  updateState { builder ->
180
180
  builder.setPlaylist(listOf(mediaItemData))
181
181
  .setCurrentMediaItemIndex(0)
182
- .setContentPositionMs(metadata.position?.times(1000)?.toLong() ?: 0)
182
+ .setContentPositionMs(this.currentMetadata!!.position?.times(1000)?.toLong() ?: 0)
183
183
  .setPlayWhenReady(
184
- metadata.isPlaying ?: false,
184
+ this.currentMetadata!!.isPlaying ?: false,
185
185
  Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
186
186
  )
187
187
  .setPlaybackState(Player.STATE_READY)
188
188
  .setAvailableCommands(state.availableCommands)
189
- .setRepeatMode(metadata.repeatMode)
190
- .setShuffleModeEnabled(metadata.shuffleMode)
189
+ .setRepeatMode(this.currentMetadata!!.repeatMode)
190
+ .setShuffleModeEnabled(this.currentMetadata!!.shuffleMode)
191
191
  }
192
192
  }
193
193
 
@@ -313,8 +313,8 @@ class MediaControlsPlayer(
313
313
 
314
314
  // Data class for metadata
315
315
  data class MediaTrackMetadata(
316
- val title: String,
317
- val artist: String,
316
+ val title: String? = null,
317
+ val artist: String? = null,
318
318
  val album: String? = null,
319
319
  val duration: Double? = null,
320
320
  val artwork: String? = null,
@@ -322,4 +322,17 @@ data class MediaTrackMetadata(
322
322
  val isPlaying: Boolean? = null,
323
323
  val repeatMode: String? = null,
324
324
  val shuffleMode: Boolean? = null
325
- )
325
+ ) {
326
+
327
+ fun merge(other: MediaTrackMetadata): MediaTrackMetadata = MediaTrackMetadata(
328
+ title = other.title ?: this.title,
329
+ artist = other.artist ?: this.artist,
330
+ album = other.album ?: this.album,
331
+ duration = other.duration ?: this.duration,
332
+ artwork = other.artwork ?: this.artwork,
333
+ position = other.position ?: this.position,
334
+ isPlaying = other.isPlaying ?: this.isPlaying,
335
+ repeatMode = other.repeatMode ?: this.repeatMode,
336
+ shuffleMode = other.shuffleMode ?: this.shuffleMode
337
+ )
338
+ }
@@ -7,6 +7,10 @@
7
7
  @property (nonatomic, assign) BOOL hasListeners;
8
8
  @property (nonatomic, assign) BOOL audioInterrupted;
9
9
  @property (nonatomic, assign) BOOL explictlyPaused;
10
+ @property (nonatomic, assign) BOOL isSeeking;
11
+ @property (nonatomic, assign) BOOL isBackwardSeeking;
12
+ @property (nonatomic, assign) float seekTime;
13
+ @property (nonatomic, assign) NSTimer* seekTimer;
10
14
  @end
11
15
 
12
16
  @implementation MediaControls
@@ -83,17 +87,17 @@ RCT_EXPORT_METHOD(updateMetadata:(JS::NativeMediaControls::NativeMediaTrackMetad
83
87
  MPNowPlayingInfoCenter *_nowPlayingCenter = [MPNowPlayingInfoCenter defaultCenter];
84
88
 
85
89
  @try {
86
- NSMutableDictionary *nowPlayingInfo = [NSMutableDictionary dictionary];
90
+ NSMutableDictionary *nowPlayingInfo = [[NSMutableDictionary alloc] initWithDictionary: _nowPlayingCenter.nowPlayingInfo];
87
91
 
88
- if (metadata.title().length > 0) {
92
+ if (metadata.title() != nil && metadata.title().length > 0) {
89
93
  nowPlayingInfo[MPMediaItemPropertyTitle] = metadata.title();
90
94
  }
91
95
 
92
- if (metadata.artist().length > 0) {
96
+ if (metadata.artist() != nil && metadata.artist().length > 0) {
93
97
  nowPlayingInfo[MPMediaItemPropertyArtist] = metadata.artist();
94
98
  }
95
99
 
96
- if (metadata.album().length > 0) {
100
+ if (metadata.album() != nil && metadata.album().length > 0) {
97
101
  nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata.album();
98
102
  }
99
103
 
@@ -106,17 +110,20 @@ RCT_EXPORT_METHOD(updateMetadata:(JS::NativeMediaControls::NativeMediaTrackMetad
106
110
  double position = metadata.position().value();
107
111
  nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = [NSNumber numberWithDouble:position];
108
112
  }
109
-
110
- nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = metadata.isPlaying() ? [NSNumber numberWithDouble:1] : [NSNumber numberWithDouble:0];
113
+
114
+ if (metadata.isPlaying().has_value()) {
115
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = metadata.isPlaying().value() ? [NSNumber numberWithDouble:1] : [NSNumber numberWithDouble:0];
116
+ }
117
+
111
118
 
112
119
  _nowPlayingCenter.nowPlayingInfo = nowPlayingInfo;
113
120
 
114
- if (@available(iOS 11.0, *)) {
121
+ if (@available(iOS 11.0, *) && metadata.isPlaying().has_value()) {
115
122
  if (!self.audioInterrupted) {
116
123
  self.explictlyPaused = false;
117
124
  }
118
125
 
119
- if (metadata.isPlaying()) {
126
+ if (metadata.isPlaying().value()) {
120
127
  _nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePlaying;
121
128
  } else {
122
129
  _nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePaused;
@@ -129,7 +136,7 @@ RCT_EXPORT_METHOD(updateMetadata:(JS::NativeMediaControls::NativeMediaTrackMetad
129
136
 
130
137
 
131
138
  // Load artwork if provided
132
- if (metadata.artwork().length > 0) {
139
+ if (metadata.artwork() != nil && metadata.artwork().length > 0) {
133
140
  NSString *artworkURL = metadata.artwork();
134
141
  [self loadArtworkFromURL:artworkURL completion:^(UIImage *image) {
135
142
  if (!image) {
@@ -256,12 +263,29 @@ RCT_EXPORT_METHOD(enableBackgroundMode:(BOOL) enabled){
256
263
  }
257
264
 
258
265
  - (MPRemoteCommandHandlerStatus)handleSeekForwardCommand:(MPRemoteCommandEvent *)event {
259
- [self emitEvent:@"seekForward" position:nil];
266
+ if (self.isSeeking) {
267
+ [self emitEvent:@"seek" position:[NSNumber numberWithFloat:self.seekTime]];
268
+ self.isSeeking = false;
269
+ [self.seekTimer invalidate];
270
+ self.seekTimer = nil;
271
+ return MPRemoteCommandHandlerStatusSuccess;
272
+ }
273
+ self.isSeeking = true;
274
+ self.isBackwardSeeking = false;
275
+
276
+ MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
277
+ self.seekTime = [[center.nowPlayingInfo objectForKey:MPNowPlayingInfoPropertyElapsedPlaybackTime] floatValue];
278
+ self.seekTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(seekTimerCallback) userInfo:nil repeats:YES];
260
279
  return MPRemoteCommandHandlerStatusSuccess;
261
280
  }
262
281
 
263
282
  - (MPRemoteCommandHandlerStatus)handleSeekBackwardCommand:(MPRemoteCommandEvent *)event {
264
- [self emitEvent:@"seekBackward" position:nil];
283
+ self.isSeeking = true;
284
+ self.isBackwardSeeking = true;
285
+
286
+ MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
287
+ self.seekTime = [[center.nowPlayingInfo objectForKey:MPNowPlayingInfoPropertyElapsedPlaybackTime] floatValue];
288
+ self.seekTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(seekTimerCallback) userInfo:nil repeats:YES];
265
289
  return MPRemoteCommandHandlerStatusSuccess;
266
290
  }
267
291
 
@@ -326,6 +350,15 @@ RCT_EXPORT_METHOD(enableBackgroundMode:(BOOL) enabled){
326
350
  });
327
351
  }
328
352
 
353
+ -(void)seekTimerCallback{
354
+ self.seekTime += 2 * (self.isBackwardSeeking ? -1 : 1);
355
+
356
+ MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
357
+ NSMutableDictionary *mediaDict = [[NSMutableDictionary alloc] initWithDictionary: center.nowPlayingInfo];
358
+ mediaDict[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(self.seekTime);
359
+ center.nowPlayingInfo = mediaDict;
360
+ }
361
+
329
362
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
330
363
  (const facebook::react::ObjCTurboModule::InitParams &)params
331
364
  {
@@ -4,8 +4,8 @@ export declare const ALL_MEDIA_EVENTS: readonly ["play", "pause", "stop", "skipT
4
4
  export type MediaControl = (typeof ALL_MEDIA_EVENTS)[number];
5
5
  export type MediaControlEvent = MediaControl | 'duck' | 'unDuck';
6
6
  export interface NativeMediaTrackMetadata {
7
- title: string;
8
- artist: string;
7
+ title?: string;
8
+ artist?: string;
9
9
  album?: string;
10
10
  duration?: number;
11
11
  artwork?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeMediaControls.d.ts","sourceRoot":"","sources":["../../../src/NativeMediaControls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAG9E,eAAO,MAAM,gBAAgB,oIAWnB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxD,cAAc,CAAC,QAAQ,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAGvC,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAGzD,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAE7C,QAAQ,IAAI,IAAI,CAAC;IAGjB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;CAC7C;;AAED,wBAAuE"}
1
+ {"version":3,"file":"NativeMediaControls.d.ts","sourceRoot":"","sources":["../../../src/NativeMediaControls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAG9E,eAAO,MAAM,gBAAgB,oIAWnB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxD,cAAc,CAAC,QAAQ,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAGvC,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAGzD,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAE7C,QAAQ,IAAI,IAAI,CAAC;IAGjB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;CAC7C;;AAED,wBAAuE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-media-notification",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Display and manage media style notifications based on react-native-music-control",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,56 +1,56 @@
1
- import type { TurboModule } from 'react-native';
2
- import { TurboModuleRegistry } from 'react-native';
3
- import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
4
-
5
- // Event types
6
- export const ALL_MEDIA_EVENTS = [
7
- 'play',
8
- 'pause',
9
- 'stop',
10
- 'skipToNext',
11
- 'skipToPrevious',
12
- 'seekForward',
13
- 'seekBackward',
14
- 'seek',
15
- 'shuffle',
16
- 'repeatMode',
17
- ] as const;
18
- export type MediaControl = (typeof ALL_MEDIA_EVENTS)[number];
19
-
20
- export type MediaControlEvent = MediaControl | 'duck' | 'unDuck';
21
-
22
- export interface NativeMediaTrackMetadata {
23
- title: string;
24
- artist: string;
25
- album?: string;
26
- duration?: number;
27
- artwork?: string;
28
- position?: number;
29
- isPlaying?: boolean;
30
- repeatMode?: 'off' | 'one' | 'all';
31
- shuffle?: boolean;
32
- }
33
-
34
- export interface NativeEvent {
35
- command: string;
36
- seekPosition?: number; // Position in seconds for seek events
37
- }
38
-
39
- export interface Spec extends TurboModule {
40
- setControlEnabled(name: string, enabled: boolean): void;
41
- updateMetadata(metadata: NativeMediaTrackMetadata): Promise<void>;
42
- stopMediaNotification(): Promise<void>;
43
-
44
- // Audio interruption handling
45
- enableAudioInterruption(enabled: boolean): Promise<void>;
46
-
47
- // Audio session activation
48
- enableBackgroundMode(enabled: boolean): void;
49
-
50
- shutdown(): void;
51
-
52
- // Event listeners (native events will be emitted)
53
- readonly onEvent: EventEmitter<NativeEvent>;
54
- }
55
-
56
- export default TurboModuleRegistry.getEnforcing<Spec>('MediaControls');
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+ import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
4
+
5
+ // Event types
6
+ export const ALL_MEDIA_EVENTS = [
7
+ 'play',
8
+ 'pause',
9
+ 'stop',
10
+ 'skipToNext',
11
+ 'skipToPrevious',
12
+ 'seekForward',
13
+ 'seekBackward',
14
+ 'seek',
15
+ 'shuffle',
16
+ 'repeatMode',
17
+ ] as const;
18
+ export type MediaControl = (typeof ALL_MEDIA_EVENTS)[number];
19
+
20
+ export type MediaControlEvent = MediaControl | 'duck' | 'unDuck';
21
+
22
+ export interface NativeMediaTrackMetadata {
23
+ title?: string;
24
+ artist?: string;
25
+ album?: string;
26
+ duration?: number;
27
+ artwork?: string;
28
+ position?: number;
29
+ isPlaying?: boolean;
30
+ repeatMode?: 'off' | 'one' | 'all';
31
+ shuffle?: boolean;
32
+ }
33
+
34
+ export interface NativeEvent {
35
+ command: string;
36
+ seekPosition?: number; // Position in seconds for seek events
37
+ }
38
+
39
+ export interface Spec extends TurboModule {
40
+ setControlEnabled(name: string, enabled: boolean): void;
41
+ updateMetadata(metadata: NativeMediaTrackMetadata): Promise<void>;
42
+ stopMediaNotification(): Promise<void>;
43
+
44
+ // Audio interruption handling
45
+ enableAudioInterruption(enabled: boolean): Promise<void>;
46
+
47
+ // Audio session activation
48
+ enableBackgroundMode(enabled: boolean): void;
49
+
50
+ shutdown(): void;
51
+
52
+ // Event listeners (native events will be emitted)
53
+ readonly onEvent: EventEmitter<NativeEvent>;
54
+ }
55
+
56
+ export default TurboModuleRegistry.getEnforcing<Spec>('MediaControls');