diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-06-11 14:24:48 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-06-11 14:24:48 +0300 |
commit | a1fdf46087de8e1719ec80ac1ca145dacfc8e640 (patch) | |
tree | 9abfbe628947a02f54dcf68385b3d66ff4470d42 /src | |
parent | 31f7eafd9c6897cdf0ee7d6eeaade9dcc65cb006 (diff) |
iOS: Audio remote control, Now Playing info
Update the basic Now Playing info about the currently playing music, and respond to remote control commands.
Seems to work with MP3 but not other audio formats, probably because those are played via custom decoders. There must be some API for updating the playback status manually.
Diffstat (limited to 'src')
-rw-r--r-- | src/app.c | 7 | ||||
-rw-r--r-- | src/audio/player.c | 14 | ||||
-rw-r--r-- | src/audio/player.h | 2 | ||||
-rw-r--r-- | src/ios.h | 3 | ||||
-rw-r--r-- | src/ios.m | 87 |
5 files changed, 111 insertions, 2 deletions
@@ -1089,6 +1089,13 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
1089 | postRefresh_App(); | 1089 | postRefresh_App(); |
1090 | break; | 1090 | break; |
1091 | case SDL_APP_WILLENTERBACKGROUND: | 1091 | case SDL_APP_WILLENTERBACKGROUND: |
1092 | #if defined (iPlatformAppleMobile) | ||
1093 | updateNowPlayingInfo_iOS(); | ||
1094 | #endif | ||
1095 | setFreezeDraw_Window(d->window, iTrue); | ||
1096 | savePrefs_App_(d); | ||
1097 | saveState_App_(d); | ||
1098 | break; | ||
1092 | case SDL_APP_TERMINATING: | 1099 | case SDL_APP_TERMINATING: |
1093 | setFreezeDraw_Window(d->window, iTrue); | 1100 | setFreezeDraw_Window(d->window, iTrue); |
1094 | savePrefs_App_(d); | 1101 | savePrefs_App_(d); |
diff --git a/src/audio/player.c b/src/audio/player.c index a2a3955b..94bcd065 100644 --- a/src/audio/player.c +++ b/src/audio/player.c | |||
@@ -455,6 +455,8 @@ struct Impl_Player { | |||
455 | iAVFAudioPlayer * avfPlayer; /* iOS */ | 455 | iAVFAudioPlayer * avfPlayer; /* iOS */ |
456 | }; | 456 | }; |
457 | 457 | ||
458 | static iPlayer *activePlayer_; | ||
459 | |||
458 | iDefineTypeConstruction(Player) | 460 | iDefineTypeConstruction(Player) |
459 | 461 | ||
460 | static size_t sampleSize_Player_(const iPlayer *d) { | 462 | static size_t sampleSize_Player_(const iPlayer *d) { |
@@ -655,8 +657,14 @@ void deinit_Player(iPlayer *d) { | |||
655 | #if defined (iPlatformAppleMobile) | 657 | #if defined (iPlatformAppleMobile) |
656 | if (d->avfPlayer) { | 658 | if (d->avfPlayer) { |
657 | delete_AVFAudioPlayer(d->avfPlayer); | 659 | delete_AVFAudioPlayer(d->avfPlayer); |
660 | if (activePlayer_ == d) { | ||
661 | clearNowPlayingInfo_iOS(); | ||
662 | } | ||
658 | } | 663 | } |
659 | #endif | 664 | #endif |
665 | if (activePlayer_ == d) { | ||
666 | activePlayer_ = NULL; | ||
667 | } | ||
660 | } | 668 | } |
661 | 669 | ||
662 | iBool isStarted_Player(const iPlayer *d) { | 670 | iBool isStarted_Player(const iPlayer *d) { |
@@ -739,6 +747,7 @@ iBool start_Player(iPlayer *d) { | |||
739 | if (d->avfPlayer) { | 747 | if (d->avfPlayer) { |
740 | play_AVFAudioPlayer(d->avfPlayer); | 748 | play_AVFAudioPlayer(d->avfPlayer); |
741 | setNotIdle_Player(d); | 749 | setNotIdle_Player(d); |
750 | activePlayer_ = d; | ||
742 | return iTrue; | 751 | return iTrue; |
743 | } | 752 | } |
744 | #endif | 753 | #endif |
@@ -756,6 +765,7 @@ iBool start_Player(iPlayer *d) { | |||
756 | d->decoder->gain = d->volume; | 765 | d->decoder->gain = d->volume; |
757 | SDL_PauseAudioDevice(d->device, SDL_FALSE); | 766 | SDL_PauseAudioDevice(d->device, SDL_FALSE); |
758 | setNotIdle_Player(d); | 767 | setNotIdle_Player(d); |
768 | activePlayer_ = d; | ||
759 | return iTrue; | 769 | return iTrue; |
760 | } | 770 | } |
761 | 771 | ||
@@ -889,3 +899,7 @@ iString *metadataLabel_Player(const iPlayer *d) { | |||
889 | } | 899 | } |
890 | return meta; | 900 | return meta; |
891 | } | 901 | } |
902 | |||
903 | iPlayer *active_Player(void) { | ||
904 | return activePlayer_; | ||
905 | } | ||
diff --git a/src/audio/player.h b/src/audio/player.h index b131838d..ca307dc4 100644 --- a/src/audio/player.h +++ b/src/audio/player.h | |||
@@ -68,3 +68,5 @@ float streamProgress_Player (const iPlayer *); /* normalized 0...1 */ | |||
68 | 68 | ||
69 | uint32_t idleTimeMs_Player (const iPlayer *); | 69 | uint32_t idleTimeMs_Player (const iPlayer *); |
70 | iString * metadataLabel_Player (const iPlayer *); | 70 | iString * metadataLabel_Player (const iPlayer *); |
71 | |||
72 | iPlayer * active_Player (void); | ||
@@ -56,3 +56,6 @@ double currentTime_AVFAudioPlayer (const iAVFAudioPlayer *); | |||
56 | double duration_AVFAudioPlayer (const iAVFAudioPlayer *); | 56 | double duration_AVFAudioPlayer (const iAVFAudioPlayer *); |
57 | iBool isStarted_AVFAudioPlayer (const iAVFAudioPlayer *); | 57 | iBool isStarted_AVFAudioPlayer (const iAVFAudioPlayer *); |
58 | iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *); | 58 | iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *); |
59 | |||
60 | void clearNowPlayingInfo_iOS (void); | ||
61 | void updateNowPlayingInfo_iOS (void); | ||
@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
22 | 22 | ||
23 | #include "ios.h" | 23 | #include "ios.h" |
24 | #include "app.h" | 24 | #include "app.h" |
25 | #include "audio/player.h" | ||
25 | #include "ui/command.h" | 26 | #include "ui/command.h" |
26 | #include "ui/window.h" | 27 | #include "ui/window.h" |
27 | 28 | ||
@@ -32,9 +33,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
32 | #include <SDL_syswm.h> | 33 | #include <SDL_syswm.h> |
33 | #include <SDL_timer.h> | 34 | #include <SDL_timer.h> |
34 | 35 | ||
35 | #import <UIKit/UIKit.h> | ||
36 | #import <CoreHaptics/CoreHaptics.h> | ||
37 | #import <AVFAudio/AVFAudio.h> | 36 | #import <AVFAudio/AVFAudio.h> |
37 | #import <CoreHaptics/CoreHaptics.h> | ||
38 | #import <UIKit/UIKit.h> | ||
39 | #import <MediaPlayer/MediaPlayer.h> | ||
38 | 40 | ||
39 | static iBool isSystemDarkMode_ = iFalse; | 41 | static iBool isSystemDarkMode_ = iFalse; |
40 | static iBool isPhone_ = iFalse; | 42 | static iBool isPhone_ = iFalse; |
@@ -241,6 +243,55 @@ void setupApplication_iOS(void) { | |||
241 | name:UIKeyboardWillHideNotification | 243 | name:UIKeyboardWillHideNotification |
242 | object:nil]; | 244 | object:nil]; |
243 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; | 245 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; |
246 | /* Media player remote controls. */ | ||
247 | MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; | ||
248 | [[commandCenter pauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
249 | iPlayer *player = active_Player(); | ||
250 | if (player) { | ||
251 | setPaused_Player(player, iTrue); | ||
252 | return MPRemoteCommandHandlerStatusSuccess; | ||
253 | } | ||
254 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
255 | }]; | ||
256 | [[commandCenter playCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
257 | iPlayer *player = active_Player(); | ||
258 | if (player) { | ||
259 | if (isPaused_Player(player)) { | ||
260 | setPaused_Player(player, iFalse); | ||
261 | } | ||
262 | else { | ||
263 | start_Player(player); | ||
264 | } | ||
265 | return MPRemoteCommandHandlerStatusSuccess; | ||
266 | } | ||
267 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
268 | }]; | ||
269 | [[commandCenter stopCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
270 | iPlayer *player = active_Player(); | ||
271 | if (player) { | ||
272 | stop_Player(player); | ||
273 | return MPRemoteCommandHandlerStatusSuccess; | ||
274 | } | ||
275 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
276 | }]; | ||
277 | [[commandCenter togglePlayPauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
278 | iPlayer *player = active_Player(); | ||
279 | if (player) { | ||
280 | setPaused_Player(player, !isPaused_Player(player)); | ||
281 | return MPRemoteCommandHandlerStatusSuccess; | ||
282 | } | ||
283 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
284 | }]; | ||
285 | [[commandCenter nextTrackCommand] setEnabled:NO]; | ||
286 | [[commandCenter previousTrackCommand] setEnabled:NO]; | ||
287 | [[commandCenter changeRepeatModeCommand] setEnabled:NO]; | ||
288 | [[commandCenter changeShuffleModeCommand] setEnabled:NO]; | ||
289 | [[commandCenter changePlaybackRateCommand] setEnabled:NO]; | ||
290 | [[commandCenter seekForwardCommand] setEnabled:NO]; | ||
291 | [[commandCenter seekBackwardCommand] setEnabled:NO]; | ||
292 | [[commandCenter skipForwardCommand] setEnabled:NO]; | ||
293 | [[commandCenter skipBackwardCommand] setEnabled:NO]; | ||
294 | [[commandCenter changePlaybackPositionCommand] setEnabled:NO]; | ||
244 | } | 295 | } |
245 | 296 | ||
246 | static iBool isDarkMode_(iWindow *window) { | 297 | static iBool isDarkMode_(iWindow *window) { |
@@ -335,6 +386,38 @@ iBool processEvent_iOS(const SDL_Event *ev) { | |||
335 | return iFalse; /* allow normal processing */ | 386 | return iFalse; /* allow normal processing */ |
336 | } | 387 | } |
337 | 388 | ||
389 | void updateNowPlayingInfo_iOS(void) { | ||
390 | const iPlayer *player = active_Player(); | ||
391 | if (!player) { | ||
392 | clearNowPlayingInfo_iOS(); | ||
393 | return; | ||
394 | } | ||
395 | NSMutableDictionary<NSString *, id> *info = [[NSMutableDictionary<NSString *, id> alloc] init]; | ||
396 | [info setObject:[NSNumber numberWithDouble:time_Player(player)] | ||
397 | forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; | ||
398 | [info setObject:[NSNumber numberWithInt:MPNowPlayingInfoMediaTypeAudio] | ||
399 | forKey:MPNowPlayingInfoPropertyMediaType]; | ||
400 | [info setObject:[NSNumber numberWithDouble:duration_Player(player)] | ||
401 | forKey:MPMediaItemPropertyPlaybackDuration]; | ||
402 | const iString *title = tag_Player(player, title_PlayerTag); | ||
403 | const iString *artist = tag_Player(player, artist_PlayerTag); | ||
404 | if (isEmpty_String(title)) { | ||
405 | title = collectNewCStr_String("Audio"); /* TODO: Use link label or URL file name */ | ||
406 | } | ||
407 | if (isEmpty_String(artist)) { | ||
408 | artist = collectNewCStr_String("Lagrange"); /* TODO: Use domain or base URL */ | ||
409 | } | ||
410 | [info setObject:[NSString stringWithUTF8String:cstr_String(title)] | ||
411 | forKey:MPMediaItemPropertyTitle]; | ||
412 | [info setObject:[NSString stringWithUTF8String:cstr_String(artist)] | ||
413 | forKey:MPMediaItemPropertyArtist]; | ||
414 | [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:info]; | ||
415 | } | ||
416 | |||
417 | void clearNowPlayingInfo_iOS(void) { | ||
418 | [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:nil]; | ||
419 | } | ||
420 | |||
338 | void exportDownloadedFile_iOS(const iString *path) { | 421 | void exportDownloadedFile_iOS(const iString *path) { |
339 | NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) | 422 | NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) |
340 | encoding:NSUTF8StringEncoding]]; | 423 | encoding:NSUTF8StringEncoding]]; |