react-native-media-notification 0.2.0

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.
Files changed (29) hide show
  1. package/LICENSE +20 -0
  2. package/MediaControls.podspec +30 -0
  3. package/README.md +237 -0
  4. package/android/build.gradle +89 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +29 -0
  7. package/android/src/main/java/com/mediacontrols/AudioFocusListener.kt +79 -0
  8. package/android/src/main/java/com/mediacontrols/Controls.kt +22 -0
  9. package/android/src/main/java/com/mediacontrols/CustomCommandButton.kt +72 -0
  10. package/android/src/main/java/com/mediacontrols/MediaControlsModule.kt +188 -0
  11. package/android/src/main/java/com/mediacontrols/MediaControlsPackage.kt +36 -0
  12. package/android/src/main/java/com/mediacontrols/MediaControlsPlayer.kt +321 -0
  13. package/android/src/main/java/com/mediacontrols/MediaControlsService.kt +233 -0
  14. package/android/src/main/java/com/mediacontrols/MediaNotificationProvider.kt +74 -0
  15. package/ios/MediaControls.h +5 -0
  16. package/ios/MediaControls.mm +300 -0
  17. package/lib/module/NativeMediaControls.js +7 -0
  18. package/lib/module/NativeMediaControls.js.map +1 -0
  19. package/lib/module/index.js +75 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/typescript/package.json +1 -0
  23. package/lib/typescript/src/NativeMediaControls.d.ts +31 -0
  24. package/lib/typescript/src/NativeMediaControls.d.ts.map +1 -0
  25. package/lib/typescript/src/index.d.ts +34 -0
  26. package/lib/typescript/src/index.d.ts.map +1 -0
  27. package/package.json +169 -0
  28. package/src/NativeMediaControls.ts +54 -0
  29. package/src/index.tsx +87 -0
@@ -0,0 +1,233 @@
1
+ package com.mediacontrols
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.Service
6
+ import android.content.ComponentName
7
+ import android.content.Intent
8
+ import android.os.Binder
9
+ import android.os.Build
10
+ import android.os.Bundle
11
+ import android.os.IBinder
12
+ import android.view.KeyEvent
13
+ import androidx.annotation.RequiresApi
14
+ import androidx.media3.common.MediaItem
15
+ import androidx.media3.common.Player
16
+ import androidx.media3.common.util.UnstableApi
17
+ import androidx.media3.session.MediaController
18
+ import androidx.media3.session.MediaSession
19
+ import androidx.media3.session.MediaSessionService
20
+ import androidx.media3.session.SessionCommand
21
+ import androidx.media3.session.SessionError
22
+ import androidx.media3.session.SessionResult
23
+ import androidx.media3.session.SessionToken
24
+ import com.facebook.react.bridge.ReactApplicationContext
25
+ import com.google.common.util.concurrent.Futures
26
+ import com.google.common.util.concurrent.ListenableFuture
27
+
28
+ @UnstableApi
29
+ class MediaControlsService : MediaSessionService() {
30
+
31
+ private var mediaSession: MediaSession? = null
32
+ private val binder = LocalBinder()
33
+ private var mediaController: MediaController? = null
34
+
35
+ companion object {
36
+ private const val CHANNEL_ID = "media_controls_channel"
37
+ var reactContext: ReactApplicationContext? = null
38
+ var player: MediaControlsPlayer? = null
39
+ }
40
+
41
+ inner class LocalBinder : Binder() {
42
+ fun getService(): MediaControlsService = this@MediaControlsService
43
+ }
44
+
45
+ override fun onBind(intent: Intent?): IBinder {
46
+ return if (intent?.action == "androidx.media3.session.MediaSessionService" ||
47
+ intent?.action == "android.media.browse.MediaBrowserService") {
48
+ super.onBind(intent)!!
49
+ } else {
50
+ binder
51
+ }
52
+ }
53
+
54
+ override fun onCreate() {
55
+ super.onCreate()
56
+
57
+ // TODO: handle headless service with HeadlessJsTask
58
+ if (player == null || reactContext == null) {
59
+ return
60
+ }
61
+
62
+ // Create notification channel for Android O and above
63
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
64
+ createNotificationChannel()
65
+ }
66
+
67
+ // Create media session
68
+ mediaSession = MediaSession.Builder(this, player!!)
69
+ .setCallback(MediaSessionCallback())
70
+ .setId("MediaControlsSession")
71
+ .build()
72
+
73
+ updateCustomLayout()
74
+
75
+ player?.addListener(object : Player.Listener {
76
+ override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
77
+ updateCustomLayout()
78
+ }
79
+
80
+ override fun onRepeatModeChanged(repeatMode: Int) {
81
+ updateCustomLayout()
82
+ }
83
+ })
84
+
85
+ setMediaNotificationProvider(MediaNotificationProvider(this))
86
+
87
+ // Create MediaController for media controls
88
+ setupMediaController()
89
+
90
+ android.util.Log.d("MediaControlsService", "Service created with new player instance")
91
+ }
92
+
93
+ private fun updateCustomLayout() {
94
+ mediaSession?.setMediaButtonPreferences(player!!.getAvailableCustomCommands().toList())
95
+ }
96
+
97
+ private fun setupMediaController() {
98
+ mediaSession?.let { session ->
99
+ val sessionToken = SessionToken(this, ComponentName(this, MediaControlsService::class.java))
100
+
101
+ val controllerFuture = MediaController.Builder(this, sessionToken)
102
+ .buildAsync()
103
+
104
+ controllerFuture.addListener({
105
+ try {
106
+ mediaController = controllerFuture.get()
107
+ } catch (e: Exception) {
108
+ android.util.Log.e("MediaControlsService", "Failed to create MediaController", e)
109
+ }
110
+ }, androidx.core.content.ContextCompat.getMainExecutor(this))
111
+ }
112
+ }
113
+
114
+ @RequiresApi(Build.VERSION_CODES.O)
115
+ private fun createNotificationChannel() {
116
+ val channel = NotificationChannel(
117
+ CHANNEL_ID,
118
+ "Media Controls",
119
+ NotificationManager.IMPORTANCE_LOW
120
+ ).apply {
121
+ description = "Media playback controls"
122
+ setShowBadge(false)
123
+ setSound(null, null)
124
+ }
125
+
126
+ val notificationManager = getSystemService(NotificationManager::class.java)
127
+ notificationManager.createNotificationChannel(channel)
128
+ }
129
+
130
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
131
+ return mediaSession
132
+ }
133
+
134
+ override fun onDestroy() {
135
+ this.stopNotificationAndService()
136
+ super.onDestroy()
137
+ }
138
+
139
+ fun stopNotificationAndService() {
140
+ stopForeground(Service.STOP_FOREGROUND_REMOVE)
141
+ stopSelf()
142
+ player?.releaseFocus()
143
+ mediaSession?.release()
144
+ mediaSession = null
145
+ }
146
+
147
+ fun getPlayer(): MediaControlsPlayer? = player
148
+
149
+ private inner class MediaSessionCallback : MediaSession.Callback {
150
+
151
+ override fun onConnect(
152
+ session: MediaSession,
153
+ controller: MediaSession.ControllerInfo
154
+ ): MediaSession.ConnectionResult {
155
+ // Accept all connections and provide full access to player commands
156
+ val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
157
+ .addSessionCommands(CustomCommandButton.entries.map { c -> c.commandButton.sessionCommand!! })
158
+ .build()
159
+
160
+ val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
161
+ .build()
162
+
163
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
164
+ .setAvailableSessionCommands(sessionCommands)
165
+ .setAvailablePlayerCommands(playerCommands)
166
+ .build()
167
+ }
168
+
169
+ override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
170
+ super.onPostConnect(session, controller)
171
+ //mediaSession?.setCustomLayout(CustomCommandButton.entries.map { c -> c.commandButton })
172
+ }
173
+
174
+ override fun onCustomCommand(
175
+ session: MediaSession,
176
+ controller: MediaSession.ControllerInfo,
177
+ customCommand: SessionCommand,
178
+ args: Bundle
179
+ ): ListenableFuture<SessionResult> {
180
+ // Handle custom commands if needed
181
+ return when (customCommand.customAction) {
182
+ "TOGGLE_PLAY_PAUSE" -> {
183
+ player?.let { p ->
184
+ if (p.isPlaying) {
185
+ p.pause()
186
+ } else {
187
+ p.play()
188
+ }
189
+ }
190
+ Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
191
+ }
192
+ CustomCommandButton.FORWARD.customAction -> {
193
+ player?.seekForward()
194
+ Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
195
+ }
196
+ CustomCommandButton.REWIND.customAction -> {
197
+ player?.seekBack()
198
+ Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
199
+ }
200
+ CustomCommandButton.SHUFFLE_ON.customAction, CustomCommandButton.SHUFFLE_OFF.customAction -> {
201
+ player?.emitShuffleClicked()
202
+ Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
203
+ }
204
+ CustomCommandButton.REPEAT_ONE.customAction, CustomCommandButton.REPEAT_OFF.customAction, CustomCommandButton.REPEAT_ALL.customAction -> {
205
+ player?.emitRepeatClicked()
206
+ Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
207
+ }
208
+ else -> Futures.immediateFuture(SessionResult(SessionError.ERROR_UNKNOWN))
209
+ }
210
+ }
211
+
212
+ override fun onAddMediaItems(
213
+ mediaSession: MediaSession,
214
+ controller: MediaSession.ControllerInfo,
215
+ mediaItems: List<MediaItem>
216
+ ): ListenableFuture<List<MediaItem>> {
217
+ // Return the media items as-is since we're handling metadata updates separately
218
+ return Futures.immediateFuture(mediaItems)
219
+ }
220
+
221
+ override fun onSetMediaItems(
222
+ mediaSession: MediaSession,
223
+ controller: MediaSession.ControllerInfo,
224
+ mediaItems: List<MediaItem>,
225
+ startIndex: Int,
226
+ startPositionMs: Long
227
+ ): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
228
+ return Futures.immediateFuture(
229
+ MediaSession.MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs)
230
+ )
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,74 @@
1
+ package com.mediacontrols
2
+
3
+ import android.app.PendingIntent
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.pm.PackageManager
7
+ import android.os.Bundle
8
+ import androidx.media3.session.CommandButton
9
+ import androidx.media3.session.MediaNotification
10
+ import androidx.media3.session.MediaSession
11
+ import androidx.media3.ui.PlayerNotificationManager
12
+ import androidx.media3.common.util.UnstableApi
13
+ import com.google.common.collect.ImmutableList
14
+
15
+ @UnstableApi
16
+ class MediaNotificationProvider(private val context: Context) : MediaNotification.Provider {
17
+
18
+ override fun createNotification(
19
+ mediaSession: MediaSession,
20
+ customLayout: ImmutableList<CommandButton>,
21
+ actionFactory: MediaNotification.ActionFactory,
22
+ onNotificationChangedCallback: MediaNotification.Provider.Callback
23
+ ): MediaNotification {
24
+
25
+ // Create PendingIntent to open the app when notification is clicked
26
+ val contentIntent = createContentIntent()
27
+
28
+ // Get the default notification from the session
29
+ val defaultProvider = androidx.media3.session.DefaultMediaNotificationProvider(context)
30
+ val notification = defaultProvider.createNotification(
31
+ mediaSession,
32
+ customLayout,
33
+ actionFactory,
34
+ onNotificationChangedCallback
35
+ )
36
+
37
+ // Modify the notification to add the content intent
38
+ val modifiedNotification = notification.notification.apply {
39
+ contentIntent?.let { pendingIntent ->
40
+ this.contentIntent = pendingIntent
41
+ }
42
+ }
43
+
44
+ return MediaNotification(notification.notificationId, modifiedNotification)
45
+ }
46
+
47
+ private fun createContentIntent(): PendingIntent? {
48
+ return try {
49
+ // Get the launcher activity from the host app
50
+ val packageManager = context.packageManager
51
+ val intent = packageManager.getLaunchIntentForPackage(context.packageName)
52
+ intent?.let {
53
+ PendingIntent.getActivity(
54
+ context,
55
+ 0,
56
+ it,
57
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
58
+ )
59
+ }
60
+ } catch (e: Exception) {
61
+ android.util.Log.e("MediaNotificationProvider", "Failed to create content intent", e)
62
+ null
63
+ }
64
+ }
65
+
66
+ override fun handleCustomCommand(
67
+ session: MediaSession,
68
+ action: String,
69
+ extras: Bundle
70
+ ): Boolean {
71
+ // Handle custom commands if needed
72
+ return false
73
+ }
74
+ }
@@ -0,0 +1,5 @@
1
+ #import <MediaControlsSpec/MediaControlsSpec.h>
2
+
3
+ @interface MediaControls : NativeMediaControlsSpecBase <NativeMediaControlsSpec>
4
+
5
+ @end
@@ -0,0 +1,300 @@
1
+ #import "MediaControls.h"
2
+ #import <MediaPlayer/MediaPlayer.h>
3
+ #import <AVFoundation/AVFoundation.h>
4
+
5
+ @interface MediaControls ()
6
+ @property (nonatomic, assign) BOOL audioInterruptionEnabled;
7
+ @property (nonatomic, assign) BOOL hasListeners;
8
+ @property (nonatomic, assign) BOOL audioInterrupted;
9
+ @property (nonatomic, assign) BOOL explictlyPaused;
10
+ @end
11
+
12
+ @implementation MediaControls
13
+
14
+ RCT_EXPORT_MODULE()
15
+
16
+ - (instancetype)init {
17
+ self = [super init];
18
+ if (self) {
19
+ _audioInterruptionEnabled = NO;
20
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioHardwareRouteChanged:) name:AVAudioSessionRouteChangeNotification object:nil];
21
+ [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
22
+ }
23
+ return self;
24
+ }
25
+
26
+ - (void)stopObserving {
27
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
28
+ }
29
+
30
+ - (NSArray<NSString *> *)supportedEvents {
31
+ return @[@"play", @"pause", @"stop", @"skipToNext", @"skipToPrevious", @"seekForward", @"seekBackward", @"seek"];
32
+ }
33
+
34
+ #pragma mark - React Native Methods
35
+
36
+ RCT_EXPORT_METHOD(setControlEnabled:(NSString*)name enabled:(BOOL)enabled) {
37
+ MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
38
+ if ([name isEqual: @"play"]) {
39
+ [commandCenter.playCommand addTarget:self action:@selector(handlePlayCommand:)];
40
+ commandCenter.playCommand.enabled = enabled;
41
+ }
42
+
43
+ if ([name isEqual: @"pause"]) {
44
+ [commandCenter.pauseCommand addTarget:self action:@selector(handlePauseCommand:)];
45
+ commandCenter.pauseCommand.enabled = enabled;
46
+ }
47
+
48
+ if ([name isEqual: @"stop"]) {
49
+ [commandCenter.stopCommand addTarget:self action:@selector(handleStopCommand:)];
50
+ commandCenter.stopCommand.enabled = enabled;
51
+ }
52
+
53
+ if ([name isEqual: @"skipToNext"]) {
54
+ [commandCenter.nextTrackCommand addTarget:self action:@selector(handleNextTrackCommand:)];
55
+ commandCenter.nextTrackCommand.enabled = enabled;
56
+ }
57
+
58
+ if ([name isEqual: @"skipToPrevious"]) {
59
+ [commandCenter.previousTrackCommand addTarget:self action:@selector(handlePreviousTrackCommand:)];
60
+ commandCenter.previousTrackCommand.enabled = enabled;
61
+ }
62
+
63
+ if ([name isEqual: @"seekForward"]) {
64
+ [commandCenter.seekForwardCommand addTarget:self action:@selector(handleSeekForwardCommand:)];
65
+ commandCenter.seekForwardCommand.enabled = enabled;
66
+ }
67
+
68
+ if ([name isEqual: @"seekBackward"]) {
69
+ [commandCenter.seekBackwardCommand addTarget:self action:@selector(handleSeekBackwardCommand:)];
70
+ commandCenter.seekBackwardCommand.enabled = enabled;
71
+ }
72
+
73
+ if ([name isEqual: @"seek"]) {
74
+ [commandCenter.changePlaybackPositionCommand addTarget:self action:@selector(handleChangePlaybackPositionCommand:)];
75
+ commandCenter.changePlaybackPositionCommand.enabled = enabled;
76
+ }
77
+ }
78
+
79
+ RCT_EXPORT_METHOD(updateMetadata:(JS::NativeMediaControls::MediaTrackMetadata &)metadata
80
+ resolve:(RCTPromiseResolveBlock)resolve
81
+ reject:(RCTPromiseRejectBlock)reject) {
82
+
83
+ MPNowPlayingInfoCenter *_nowPlayingCenter = [MPNowPlayingInfoCenter defaultCenter];
84
+
85
+ @try {
86
+ NSMutableDictionary *nowPlayingInfo = [NSMutableDictionary dictionary];
87
+
88
+ if (metadata.title().length > 0) {
89
+ nowPlayingInfo[MPMediaItemPropertyTitle] = metadata.title();
90
+ }
91
+
92
+ if (metadata.artist().length > 0) {
93
+ nowPlayingInfo[MPMediaItemPropertyArtist] = metadata.artist();
94
+ }
95
+
96
+ if (metadata.album().length > 0) {
97
+ nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata.album();
98
+ }
99
+
100
+ if (metadata.duration().has_value()) {
101
+ double duration = metadata.duration().value();
102
+ nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = [NSNumber numberWithDouble:duration];
103
+ }
104
+
105
+ if (metadata.position().has_value()) {
106
+ double position = metadata.position().value();
107
+ nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = [NSNumber numberWithDouble:position];
108
+ }
109
+
110
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = metadata.isPlaying() ? [NSNumber numberWithDouble:1] : [NSNumber numberWithDouble:0];
111
+
112
+ _nowPlayingCenter.nowPlayingInfo = nowPlayingInfo;
113
+
114
+ if (@available(iOS 11.0, *)) {
115
+ if (!self.audioInterrupted) {
116
+ self.explictlyPaused = false;
117
+ }
118
+
119
+ if (metadata.isPlaying()) {
120
+ _nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePlaying;
121
+ } else {
122
+ _nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePaused;
123
+
124
+ if (!self.audioInterrupted) {
125
+ self.explictlyPaused = true;
126
+ }
127
+ }
128
+ }
129
+
130
+
131
+ // Load artwork if provided
132
+ if (metadata.artwork().length > 0) {
133
+ NSString *artworkURL = metadata.artwork();
134
+ [self loadArtworkFromURL:artworkURL completion:^(UIImage *image) {
135
+ if (!image) {
136
+ return;
137
+ }
138
+ dispatch_async(dispatch_get_main_queue(), ^{
139
+ MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
140
+ MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:image.size requestHandler:^UIImage * _Nonnull(CGSize size) {
141
+ return image;
142
+ }];
143
+ NSMutableDictionary *mediaDict = (center.nowPlayingInfo != nil) ? [[NSMutableDictionary alloc] initWithDictionary: center.nowPlayingInfo] : [NSMutableDictionary dictionary];
144
+ [mediaDict setValue:artwork forKey:MPMediaItemPropertyArtwork];
145
+ center.nowPlayingInfo = mediaDict;
146
+ });
147
+ }];
148
+ }
149
+
150
+ resolve(nil);
151
+ }
152
+ @catch (NSException *exception) {
153
+ reject(@"UPDATE_METADATA_ERROR", exception.reason, nil);
154
+ }
155
+ }
156
+
157
+ RCT_EXPORT_METHOD(stopMediaNotification:(RCTPromiseResolveBlock)resolve
158
+ reject:(RCTPromiseRejectBlock)reject) {
159
+ @try {
160
+ [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil;
161
+ resolve(nil);
162
+ }
163
+ @catch (NSException *exception) {
164
+ reject(@"STOP_NOTIFICATION_ERROR", exception.reason, nil);
165
+ }
166
+ }
167
+
168
+ RCT_EXPORT_METHOD(enableAudioInterruption:(BOOL)enabled
169
+ resolve:(RCTPromiseResolveBlock)resolve
170
+ reject:(RCTPromiseRejectBlock)reject) {
171
+ @try {
172
+ _audioInterruptionEnabled = enabled;
173
+
174
+ if (enabled) {
175
+ [[NSNotificationCenter defaultCenter] addObserver:self
176
+ selector:@selector(audioSessionInterrupted:)
177
+ name:AVAudioSessionInterruptionNotification
178
+ object:nil];
179
+ } else {
180
+ [[NSNotificationCenter defaultCenter] removeObserver:self
181
+ name:AVAudioSessionInterruptionNotification
182
+ object:nil];
183
+ }
184
+
185
+ resolve(nil);
186
+ }
187
+ @catch (NSException *exception) {
188
+ reject(@"AUDIO_INTERRUPTION_ERROR", exception.reason, nil);
189
+ }
190
+ }
191
+
192
+ RCT_EXPORT_METHOD(enableBackgroundMode:(BOOL) enabled){
193
+ AVAudioSession *session = [AVAudioSession sharedInstance];
194
+ [session setCategory: AVAudioSessionCategoryPlayback error: nil];
195
+ [session setActive: enabled error: nil];
196
+ }
197
+
198
+ - (MPRemoteCommandHandlerStatus)handlePlayCommand:(MPRemoteCommandEvent *)event {
199
+ [self emitEvent:@"play" position:nil];
200
+ return MPRemoteCommandHandlerStatusSuccess;
201
+ }
202
+
203
+ - (MPRemoteCommandHandlerStatus)handlePauseCommand:(MPRemoteCommandEvent *)event {
204
+ [self emitEvent:@"pause" position:nil];
205
+ return MPRemoteCommandHandlerStatusSuccess;
206
+ }
207
+
208
+ - (MPRemoteCommandHandlerStatus)handleStopCommand:(MPRemoteCommandEvent *)event {
209
+ [self emitEvent:@"stop" position:nil];
210
+ return MPRemoteCommandHandlerStatusSuccess;
211
+ }
212
+
213
+ - (MPRemoteCommandHandlerStatus)handleNextTrackCommand:(MPRemoteCommandEvent *)event {
214
+ [self emitEvent:@"skipToNext" position:nil];
215
+ return MPRemoteCommandHandlerStatusSuccess;
216
+ }
217
+
218
+ - (MPRemoteCommandHandlerStatus)handlePreviousTrackCommand:(MPRemoteCommandEvent *)event {
219
+ [self emitEvent:@"skipToPrevious" position:nil];
220
+ return MPRemoteCommandHandlerStatusSuccess;
221
+ }
222
+
223
+ - (MPRemoteCommandHandlerStatus)handleSeekForwardCommand:(MPRemoteCommandEvent *)event {
224
+ [self emitEvent:@"seekForward" position:nil];
225
+ return MPRemoteCommandHandlerStatusSuccess;
226
+ }
227
+
228
+ - (MPRemoteCommandHandlerStatus)handleSeekBackwardCommand:(MPRemoteCommandEvent *)event {
229
+ [self emitEvent:@"seekBackward" position:nil];
230
+ return MPRemoteCommandHandlerStatusSuccess;
231
+ }
232
+
233
+ - (MPRemoteCommandHandlerStatus)handleChangePlaybackPositionCommand:(MPChangePlaybackPositionCommandEvent *)event {
234
+ [self emitEvent:@"seek" position:[NSNumber numberWithDouble:event.positionTime]];
235
+ return MPRemoteCommandHandlerStatusSuccess;
236
+ }
237
+
238
+ - (void)emitEvent:(NSString*) name position:(nullable NSNumber*) position {
239
+ NSMutableDictionary *params = [NSMutableDictionary dictionary];
240
+ params[@"command"] = name;
241
+ if (position) {
242
+ params[@"seekPosition"] = position;
243
+ }
244
+ [self emitOnEvent:params];
245
+ }
246
+
247
+ #pragma mark - Audio Interruption Handler
248
+
249
+ - (void)audioSessionInterrupted:(NSNotification *)notification {
250
+ if (!_audioInterruptionEnabled) return;
251
+
252
+ NSInteger interruptionType = [notification.userInfo[AVAudioSessionInterruptionTypeKey] integerValue];
253
+
254
+ if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
255
+ self.audioInterrupted = true;
256
+ if (!self.explictlyPaused) {
257
+ [self emitEvent:@"pause" position:nil];
258
+ }
259
+ } else if (interruptionType == AVAudioSessionInterruptionTypeEnded) {
260
+ self.audioInterrupted = false;
261
+ if (!self.explictlyPaused) {
262
+ [self emitEvent:@"play" position:nil];
263
+ }
264
+ }
265
+ }
266
+
267
+ - (void)audioHardwareRouteChanged:(NSNotification *)notification {
268
+ NSInteger routeChangeReason = [notification.userInfo[AVAudioSessionRouteChangeReasonKey] integerValue];
269
+ if (routeChangeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
270
+ //headphones unplugged or bluetooth device disconnected, iOS will pause audio
271
+ [self emitEvent:@"pause" position:nil];
272
+ }
273
+ }
274
+
275
+ #pragma mark - Helper Methods
276
+
277
+ - (void)loadArtworkFromURL:(NSString *)urlString completion:(void (^)(UIImage *))completion {
278
+ NSURL *url = [NSURL URLWithString:urlString];
279
+ if (!url) {
280
+ completion(nil);
281
+ return;
282
+ }
283
+
284
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
285
+ NSData *imageData = [NSData dataWithContentsOfURL:url];
286
+ UIImage *image = imageData ? [UIImage imageWithData:imageData] : nil;
287
+
288
+ dispatch_async(dispatch_get_main_queue(), ^{
289
+ completion(image);
290
+ });
291
+ });
292
+ }
293
+
294
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
295
+ (const facebook::react::ObjCTurboModule::InitParams &)params
296
+ {
297
+ return std::make_shared<facebook::react::NativeMediaControlsSpecJSI>(params);
298
+ }
299
+
300
+ @end
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ // Event types
5
+ export const ALL_MEDIA_EVENTS = ['play', 'pause', 'stop', 'skipToNext', 'skipToPrevious', 'seekForward', 'seekBackward', 'seek', 'shuffle', 'repeatMode'];
6
+ export default TurboModuleRegistry.getEnforcing('MediaControls');
7
+ //# sourceMappingURL=NativeMediaControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","ALL_MEDIA_EVENTS","getEnforcing"],"sourceRoot":"..\\..\\src","sources":["NativeMediaControls.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAGlD;AACA,OAAO,MAAMC,gBAAgB,GAAG,CAC9B,MAAM,EACN,OAAO,EACP,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,MAAM,EACN,SAAS,EACT,YAAY,CACJ;AAqCV,eAAeD,mBAAmB,CAACE,YAAY,CAAO,eAAe,CAAC","ignoreList":[]}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+
3
+ import MediaControls, { ALL_MEDIA_EVENTS } from "./NativeMediaControls.js";
4
+ import { EventEmitter } from 'fbemitter';
5
+ const eventEmitter = new EventEmitter();
6
+ let unsubscribe = null;
7
+ const setUpNativeEventListener = () => {
8
+ if (unsubscribe) return;
9
+ unsubscribe = MediaControls.onEvent(event => {
10
+ const {
11
+ command,
12
+ seekPosition
13
+ } = event;
14
+ eventEmitter.emit(command, {
15
+ position: seekPosition
16
+ });
17
+ });
18
+ };
19
+
20
+ /**
21
+ * Updates the metadata for the current media track.
22
+ */
23
+ export async function updateMetadata(metadata) {
24
+ return MediaControls.updateMetadata(metadata);
25
+ }
26
+
27
+ /**
28
+ * Stops the media notification and clears any ongoing playback state.
29
+ */
30
+ export async function stopMediaNotification() {
31
+ return MediaControls.stopMediaNotification();
32
+ }
33
+
34
+ /**
35
+ * Activates or deactivates audio interruption handling.
36
+ */
37
+ export async function enableAudioInterruption(enabled) {
38
+ return MediaControls.enableAudioInterruption(enabled);
39
+ }
40
+
41
+ /**
42
+ * Activates the audio session for media playback.
43
+ * Call this method before starting media playback to ensure proper Control Center integration.
44
+ */
45
+ export function enableBackgroundMode(enabled) {
46
+ MediaControls.enableBackgroundMode(enabled);
47
+ }
48
+ export function setControlEnabled(name, enabled) {
49
+ if (!ALL_MEDIA_EVENTS.includes(name)) {
50
+ throw new Error(`Unknown media control event: ${name}`);
51
+ }
52
+ MediaControls.setControlEnabled(name, enabled);
53
+ }
54
+
55
+ /**
56
+ * Register an event listener for media control events.
57
+ */
58
+ export function addEventListener(event, handler) {
59
+ setUpNativeEventListener();
60
+ return eventEmitter.addListener(event, handler);
61
+ }
62
+
63
+ /**
64
+ * Remove a specific event listener for media control events.
65
+ */
66
+ export function removeAllListeners(event) {
67
+ if (event) {
68
+ eventEmitter.removeAllListeners(event);
69
+ } else {
70
+ ALL_MEDIA_EVENTS.forEach(e => eventEmitter.removeAllListeners(e));
71
+ }
72
+ }
73
+
74
+ // Export types
75
+ //# sourceMappingURL=index.js.map