react-native-media-notification 0.3.4 → 0.3.6

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,
@@ -1,25 +1,32 @@
1
1
  package com.mediacontrols
2
2
 
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.graphics.drawable.BitmapDrawable
3
6
  import android.os.Handler
4
7
  import android.os.Looper
8
+ import android.webkit.URLUtil
9
+ import androidx.core.net.toUri
5
10
  import androidx.media3.common.MediaItem
6
11
  import androidx.media3.common.MediaMetadata
7
12
  import androidx.media3.common.Player
8
13
  import androidx.media3.common.SimpleBasePlayer
9
14
  import androidx.media3.common.util.UnstableApi
15
+ import androidx.media3.session.CommandButton
10
16
  import com.facebook.react.bridge.ReactApplicationContext
17
+ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
11
18
  import com.google.common.util.concurrent.Futures
12
19
  import com.google.common.util.concurrent.ListenableFuture
13
20
  import kotlinx.coroutines.CoroutineScope
14
21
  import kotlinx.coroutines.Dispatchers
15
22
  import kotlinx.coroutines.SupervisorJob
16
23
  import kotlinx.coroutines.cancel
17
- import androidx.core.net.toUri
18
- import androidx.media3.session.CommandButton
24
+ import java.io.ByteArrayOutputStream
25
+
19
26
 
20
27
  @UnstableApi
21
28
  class MediaControlsPlayer(
22
- reactContext: ReactApplicationContext,
29
+ private val reactContext: ReactApplicationContext,
23
30
  private val module: MediaControlsModule,
24
31
  ) : SimpleBasePlayer(Looper.getMainLooper()) {
25
32
 
@@ -143,19 +150,19 @@ class MediaControlsPlayer(
143
150
  audioFocusListener.requestAudioFocus()
144
151
  }
145
152
 
146
- this.currentMetadata = metadata
153
+ this.currentMetadata = this.currentMetadata?.merge(metadata) ?: metadata
147
154
 
148
155
  val mediaMetadata = MediaMetadata.Builder()
149
- .setTitle(metadata.title)
150
- .setArtist(metadata.artist)
151
- .setAlbumTitle(metadata.album)
152
- .setDurationMs(metadata.duration?.times(1000)?.toLong())
156
+ .setTitle(this.currentMetadata!!.title)
157
+ .setArtist(this.currentMetadata!!.artist)
158
+ .setAlbumTitle(this.currentMetadata!!.album)
159
+ .setDurationMs(this.currentMetadata!!.duration?.times(1000)?.toLong())
153
160
  .setIsPlayable(true)
154
161
  .setIsBrowsable(false)
155
162
  .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
156
- .apply {
157
- metadata.artwork?.let { artworkUrl ->
158
- setArtworkUri(artworkUrl.toUri())
163
+ .also {
164
+ this.currentMetadata!!.artwork?.let { artworkUrl ->
165
+ it.setArtwork(artworkUrl)
159
166
  }
160
167
  }
161
168
  .build()
@@ -171,23 +178,23 @@ class MediaControlsPlayer(
171
178
 
172
179
  val mediaItemData = MediaItemData.Builder(mediaId)
173
180
  .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)
181
+ .setDefaultPositionUs(this.currentMetadata!!.position?.times(1_000_000)?.toLong() ?: 0)
182
+ .setDurationUs(this.currentMetadata!!.duration?.times(1_000_000)?.toLong() ?: androidx.media3.common.C.TIME_UNSET)
176
183
  .setIsSeekable(true)
177
184
  .build()
178
185
 
179
186
  updateState { builder ->
180
187
  builder.setPlaylist(listOf(mediaItemData))
181
188
  .setCurrentMediaItemIndex(0)
182
- .setContentPositionMs(metadata.position?.times(1000)?.toLong() ?: 0)
189
+ .setContentPositionMs(this.currentMetadata!!.position?.times(1000)?.toLong() ?: 0)
183
190
  .setPlayWhenReady(
184
- metadata.isPlaying ?: false,
191
+ this.currentMetadata!!.isPlaying ?: false,
185
192
  Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
186
193
  )
187
194
  .setPlaybackState(Player.STATE_READY)
188
195
  .setAvailableCommands(state.availableCommands)
189
- .setRepeatMode(metadata.repeatMode)
190
- .setShuffleModeEnabled(metadata.shuffleMode)
196
+ .setRepeatMode(this.currentMetadata!!.repeatMode)
197
+ .setShuffleModeEnabled(this.currentMetadata!!.shuffleMode)
191
198
  }
192
199
  }
193
200
 
@@ -211,6 +218,28 @@ class MediaControlsPlayer(
211
218
  return this.setShuffleModeEnabled(enabled)
212
219
  }
213
220
 
221
+ private fun MediaMetadata.Builder.setArtwork(artwork: String?): MediaMetadata.Builder {
222
+ if (artwork == null) {
223
+ return this
224
+ }
225
+ if (URLUtil.isValidUrl(artwork)) {
226
+ this.setArtworkUri(artwork.toUri())
227
+ } else {
228
+ val helper = ResourceDrawableIdHelper.getInstance()
229
+ val image = helper.getResourceDrawable(reactContext, artwork)
230
+
231
+ val bitmap = if (image is BitmapDrawable) {
232
+ image.bitmap
233
+ } else {
234
+ BitmapFactory.decodeFile(artwork)
235
+ }
236
+ val stream = ByteArrayOutputStream()
237
+ bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream)
238
+ this.setArtworkData(stream.toByteArray(), MediaMetadata.PICTURE_TYPE_OTHER)
239
+ }
240
+ return this
241
+ }
242
+
214
243
  fun setControlEnabled(controlName: Controls, enabled: Boolean) {
215
244
  enabledControls[controlName] = enabled
216
245
 
@@ -313,8 +342,8 @@ class MediaControlsPlayer(
313
342
 
314
343
  // Data class for metadata
315
344
  data class MediaTrackMetadata(
316
- val title: String,
317
- val artist: String,
345
+ val title: String? = null,
346
+ val artist: String? = null,
318
347
  val album: String? = null,
319
348
  val duration: Double? = null,
320
349
  val artwork: String? = null,
@@ -322,4 +351,17 @@ data class MediaTrackMetadata(
322
351
  val isPlaying: Boolean? = null,
323
352
  val repeatMode: String? = null,
324
353
  val shuffleMode: Boolean? = null
325
- )
354
+ ) {
355
+
356
+ fun merge(other: MediaTrackMetadata): MediaTrackMetadata = MediaTrackMetadata(
357
+ title = other.title ?: this.title,
358
+ artist = other.artist ?: this.artist,
359
+ album = other.album ?: this.album,
360
+ duration = other.duration ?: this.duration,
361
+ artwork = other.artwork ?: this.artwork,
362
+ position = other.position ?: this.position,
363
+ isPlaying = other.isPlaying ?: this.isPlaying,
364
+ repeatMode = other.repeatMode ?: this.repeatMode,
365
+ shuffleMode = other.shuffleMode ?: this.shuffleMode
366
+ )
367
+ }
@@ -87,17 +87,17 @@ RCT_EXPORT_METHOD(updateMetadata:(JS::NativeMediaControls::NativeMediaTrackMetad
87
87
  MPNowPlayingInfoCenter *_nowPlayingCenter = [MPNowPlayingInfoCenter defaultCenter];
88
88
 
89
89
  @try {
90
- NSMutableDictionary *nowPlayingInfo = [NSMutableDictionary dictionary];
90
+ NSMutableDictionary *nowPlayingInfo = [[NSMutableDictionary alloc] initWithDictionary: _nowPlayingCenter.nowPlayingInfo];
91
91
 
92
- if (metadata.title().length > 0) {
92
+ if (metadata.title() != nil && metadata.title().length > 0) {
93
93
  nowPlayingInfo[MPMediaItemPropertyTitle] = metadata.title();
94
94
  }
95
95
 
96
- if (metadata.artist().length > 0) {
96
+ if (metadata.artist() != nil && metadata.artist().length > 0) {
97
97
  nowPlayingInfo[MPMediaItemPropertyArtist] = metadata.artist();
98
98
  }
99
99
 
100
- if (metadata.album().length > 0) {
100
+ if (metadata.album() != nil && metadata.album().length > 0) {
101
101
  nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata.album();
102
102
  }
103
103
 
@@ -110,17 +110,20 @@ RCT_EXPORT_METHOD(updateMetadata:(JS::NativeMediaControls::NativeMediaTrackMetad
110
110
  double position = metadata.position().value();
111
111
  nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = [NSNumber numberWithDouble:position];
112
112
  }
113
-
114
- 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
+
115
118
 
116
119
  _nowPlayingCenter.nowPlayingInfo = nowPlayingInfo;
117
120
 
118
- if (@available(iOS 11.0, *)) {
121
+ if (@available(iOS 11.0, *) && metadata.isPlaying().has_value()) {
119
122
  if (!self.audioInterrupted) {
120
123
  self.explictlyPaused = false;
121
124
  }
122
125
 
123
- if (metadata.isPlaying()) {
126
+ if (metadata.isPlaying().value()) {
124
127
  _nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePlaying;
125
128
  } else {
126
129
  _nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePaused;
@@ -133,7 +136,7 @@ RCT_EXPORT_METHOD(updateMetadata:(JS::NativeMediaControls::NativeMediaTrackMetad
133
136
 
134
137
 
135
138
  // Load artwork if provided
136
- if (metadata.artwork().length > 0) {
139
+ if (metadata.artwork() != nil && metadata.artwork().length > 0) {
137
140
  NSString *artworkURL = metadata.artwork();
138
141
  [self loadArtworkFromURL:artworkURL completion:^(UIImage *image) {
139
142
  if (!image) {
@@ -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.4",
3
+ "version": "0.3.6",
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');