audio_service/darwin/Classes/AudioServicePlugin.m
2020-09-18 19:25:00 +02:00

618 lines
26 KiB
Objective-C

#import "AudioServicePlugin.h"
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
// If you'd like to help, please see the TODO comments below, then open a
// GitHub issue to announce your intention to work on a particular feature, and
// submit a pull request. We have an open discussion over at issue #10 about
// all things iOS if you'd like to discuss approaches or ask for input. Thank
// you for your support!
@implementation AudioServicePlugin
static FlutterMethodChannel *channel = nil;
static FlutterMethodChannel *backgroundChannel = nil;
static BOOL _running = NO;
static FlutterResult startResult = nil;
static MPRemoteCommandCenter *commandCenter = nil;
static NSArray *queue = nil;
static NSMutableDictionary *mediaItem = nil;
static long actionBits;
static NSArray *commands;
static BOOL _controlsUpdated = NO;
static enum AudioProcessingState processingState = none;
static BOOL playing = NO;
static NSNumber *position = nil;
static NSNumber *bufferedPosition = nil;
static NSNumber *updateTime = nil;
static NSNumber *speed = nil;
static NSNumber *repeatMode = nil;
static NSNumber *shuffleMode = nil;
static NSNumber *fastForwardInterval = nil;
static NSNumber *rewindInterval = nil;
static NSMutableDictionary *params = nil;
static MPMediaItemArtwork* artwork = nil;
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
@synchronized(self) {
// TODO: Need a reliable way to detect whether this is the client
// or background.
// TODO: Handle multiple clients.
// As no separate isolate is used on macOS, add both handlers to the one registrar.
#if TARGET_OS_IPHONE
if (channel == nil) {
#endif
AudioServicePlugin *instance = [[AudioServicePlugin alloc] init:registrar];
channel = [FlutterMethodChannel
methodChannelWithName:@"ryanheise.com/audioService"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:channel];
#if TARGET_OS_IPHONE
} else {
AudioServicePlugin *instance = [[AudioServicePlugin alloc] init:registrar];
#endif
backgroundChannel = [FlutterMethodChannel
methodChannelWithName:@"ryanheise.com/audioServiceBackground"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:backgroundChannel];
#if TARGET_OS_IPHONE
}
#endif
}
}
- (instancetype)init:(NSObject<FlutterPluginRegistrar> *)registrar {
self = [super init];
NSAssert(self, @"super init cannot be nil");
return self;
}
- (void)broadcastPlaybackState {
[channel invokeMethod:@"onPlaybackStateChanged" arguments:@[
// processingState
@(processingState),
// playing
@(playing),
// actions
@(actionBits),
// position
position,
// bufferedPosition
bufferedPosition,
// playback speed
speed,
// update time since epoch
updateTime,
// repeat mode
repeatMode,
// shuffle mode
shuffleMode,
]];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
// TODO:
// - Restructure this so that we have a separate method call delegate
// for the client instance and the background instance so that methods
// can't be called on the wrong instance.
if ([@"connect" isEqualToString:call.method]) {
long long msSinceEpoch = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
if (position == nil) {
position = @(0);
bufferedPosition = @(0);
updateTime = [NSNumber numberWithLongLong: msSinceEpoch];
speed = [NSNumber numberWithDouble: 1.0];
repeatMode = @(0);
shuffleMode = @(0);
}
// Notify client of state on subscribing.
[self broadcastPlaybackState];
[channel invokeMethod:@"onMediaChanged" arguments:@[mediaItem ? mediaItem : [NSNull null]]];
[channel invokeMethod:@"onQueueChanged" arguments:@[queue ? queue : [NSNull null]]];
result(nil);
} else if ([@"disconnect" isEqualToString:call.method]) {
result(nil);
} else if ([@"start" isEqualToString:call.method]) {
if (_running) {
result(@NO);
return;
}
_running = YES;
// The result will be sent after the background task actually starts.
// See the "ready" case below.
startResult = result;
#if TARGET_OS_IPHONE
[AVAudioSession sharedInstance];
#endif
// Set callbacks on MPRemoteCommandCenter
fastForwardInterval = [call.arguments objectForKey:@"fastForwardInterval"];
rewindInterval = [call.arguments objectForKey:@"rewindInterval"];
commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
commands = @[
commandCenter.stopCommand,
commandCenter.pauseCommand,
commandCenter.playCommand,
commandCenter.skipBackwardCommand,
commandCenter.previousTrackCommand,
commandCenter.nextTrackCommand,
commandCenter.skipForwardCommand,
[NSNull null],
commandCenter.changePlaybackPositionCommand,
commandCenter.togglePlayPauseCommand,
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
commandCenter.changeRepeatModeCommand,
[NSNull null],
[NSNull null],
commandCenter.changeShuffleModeCommand,
commandCenter.seekBackwardCommand,
commandCenter.seekForwardCommand,
];
[commandCenter.changePlaybackRateCommand setEnabled:YES];
[commandCenter.togglePlayPauseCommand setEnabled:YES];
[commandCenter.togglePlayPauseCommand addTarget:self action:@selector(togglePlayPause:)];
// TODO: enable more commands
// Language options
if (@available(iOS 9.0, macOS 10.12.2, *)) {
[commandCenter.enableLanguageOptionCommand setEnabled:NO];
[commandCenter.disableLanguageOptionCommand setEnabled:NO];
}
// Rating
[commandCenter.ratingCommand setEnabled:NO];
// Feedback
[commandCenter.likeCommand setEnabled:NO];
[commandCenter.dislikeCommand setEnabled:NO];
[commandCenter.bookmarkCommand setEnabled:NO];
[self updateControls];
// Params
params = [call.arguments objectForKey:@"params"];
#if TARGET_OS_OSX
// No isolate can be used for macOS until https://github.com/flutter/flutter/issues/65222 is resolved.
// We send a result here, and then the Dart code continues in the main isolate.
result(@YES);
#endif
} else if ([@"ready" isEqualToString:call.method]) {
NSMutableDictionary *startParams = [NSMutableDictionary new];
startParams[@"fastForwardInterval"] = fastForwardInterval;
startParams[@"rewindInterval"] = rewindInterval;
startParams[@"params"] = params;
result(startParams);
} else if ([@"started" isEqualToString:call.method]) {
#if TARGET_OS_IPHONE
if (startResult) {
startResult(@YES);
startResult = nil;
}
#endif
result(@YES);
} else if ([@"stopped" isEqualToString:call.method]) {
_running = NO;
[channel invokeMethod:@"onStopped" arguments:nil];
[commandCenter.changePlaybackRateCommand setEnabled:NO];
[commandCenter.togglePlayPauseCommand setEnabled:NO];
[commandCenter.togglePlayPauseCommand removeTarget:nil];
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil;
processingState = none;
playing = NO;
position = nil;
bufferedPosition = nil;
updateTime = nil;
speed = nil;
artwork = nil;
mediaItem = nil;
repeatMode = @(0);
shuffleMode = @(0);
actionBits = 0;
[self updateControls];
_controlsUpdated = NO;
queue = nil;
startResult = nil;
fastForwardInterval = nil;
rewindInterval = nil;
params = nil;
commandCenter = nil;
result(@YES);
} else if ([@"isRunning" isEqualToString:call.method]) {
if (_running) {
result(@YES);
} else {
result(@NO);
}
} else if ([@"setBrowseMediaParent" isEqualToString:call.method]) {
result(@YES);
} else if ([@"addQueueItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onAddQueueItem" arguments:@[call.arguments] result: result];
} else if ([@"addQueueItemAt" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onAddQueueItemAt" arguments:call.arguments result: result];
} else if ([@"removeQueueItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onRemoveQueueItem" arguments:@[call.arguments] result: result];
} else if ([@"updateQueue" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onUpdateQueue" arguments:@[call.arguments] result: result];
} else if ([@"updateMediaItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onUpdateMediaItem" arguments:@[call.arguments] result: result];
} else if ([@"click" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onClick" arguments:@[call.arguments] result: result];
} else if ([@"prepare" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPrepare" arguments:nil result: result];
} else if ([@"prepareFromMediaId" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPrepareFromMediaId" arguments:@[call.arguments] result: result];
} else if ([@"play" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPlay" arguments:nil result: result];
} else if ([@"playFromMediaId" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPlayFromMediaId" arguments:@[call.arguments] result: result];
} else if ([@"playMediaItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPlayMediaItem" arguments:@[call.arguments] result: result];
} else if ([@"skipToQueueItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSkipToQueueItem" arguments:@[call.arguments] result: result];
} else if ([@"pause" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPause" arguments:nil result: result];
} else if ([@"stop" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onStop" arguments:nil result: result];
} else if ([@"seekTo" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSeekTo" arguments:@[call.arguments] result: result];
} else if ([@"skipToNext" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSkipToNext" arguments:nil result: result];
} else if ([@"skipToPrevious" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSkipToPrevious" arguments:nil result: result];
} else if ([@"fastForward" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onFastForward" arguments:nil result: result];
} else if ([@"rewind" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onRewind" arguments:nil result: result];
} else if ([@"setRepeatMode" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetRepeatMode" arguments:@[call.arguments] result: result];
} else if ([@"setShuffleMode" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetShuffleMode" arguments:@[call.arguments] result: result];
} else if ([@"setRating" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetRating" arguments:@[call.arguments[@"rating"], call.arguments[@"extras"]] result: result];
} else if ([@"setSpeed" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetSpeed" arguments:@[call.arguments] result: result];
} else if ([@"seekForward" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSeekForward" arguments:@[call.arguments] result: result];
} else if ([@"seekBackward" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSeekBackward" arguments:@[call.arguments] result: result];
} else if ([@"setState" isEqualToString:call.method]) {
long long msSinceEpoch;
if (call.arguments[7] != [NSNull null]) {
msSinceEpoch = [call.arguments[7] longLongValue];
} else {
msSinceEpoch = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
}
actionBits = 0;
NSArray *controlsArray = call.arguments[0];
for (int i = 0; i < controlsArray.count; i++) {
NSDictionary *control = (NSDictionary *)controlsArray[i];
NSNumber *actionIndex = (NSNumber *)control[@"action"];
int actionCode = 1 << [actionIndex intValue];
actionBits |= actionCode;
}
NSArray *systemActionsArray = call.arguments[1];
for (int i = 0; i < systemActionsArray.count; i++) {
NSNumber *actionIndex = (NSNumber *)systemActionsArray[i];
int actionCode = 1 << [actionIndex intValue];
actionBits |= actionCode;
}
processingState = [call.arguments[2] intValue];
playing = [call.arguments[3] boolValue];
position = call.arguments[4];
bufferedPosition = call.arguments[5];
speed = call.arguments[6];
repeatMode = call.arguments[9];
shuffleMode = call.arguments[10];
updateTime = [NSNumber numberWithLongLong: msSinceEpoch];
[self broadcastPlaybackState];
[self updateControls];
[self updateNowPlayingInfo];
result(@(YES));
} else if ([@"setQueue" isEqualToString:call.method]) {
queue = call.arguments;
[channel invokeMethod:@"onQueueChanged" arguments:@[queue]];
result(@YES);
} else if ([@"setMediaItem" isEqualToString:call.method]) {
mediaItem = call.arguments;
NSString* artUri = mediaItem[@"artUri"];
artwork = nil;
if (![artUri isEqual: [NSNull null]]) {
NSString* artCacheFilePath = [NSNull null];
NSDictionary* extras = mediaItem[@"extras"];
if (![extras isEqual: [NSNull null]]) {
artCacheFilePath = extras[@"artCacheFile"];
}
if (![artCacheFilePath isEqual: [NSNull null]]) {
#if TARGET_OS_IPHONE
UIImage* artImage = [UIImage imageWithContentsOfFile:artCacheFilePath];
#else
NSImage* artImage = [[NSImage alloc] initWithContentsOfFile:artCacheFilePath];
#endif
if (artImage != nil) {
#if TARGET_OS_IPHONE
artwork = [[MPMediaItemArtwork alloc] initWithImage: artImage];
#else
artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:artImage.size requestHandler:^NSImage* _Nonnull(CGSize aSize) {
return artImage;
}];
#endif
}
}
}
[self updateNowPlayingInfo];
[channel invokeMethod:@"onMediaChanged" arguments:@[call.arguments]];
result(@(YES));
} else if ([@"notifyChildrenChanged" isEqualToString:call.method]) {
result(@YES);
} else if ([@"androidForceEnableMediaButtons" isEqualToString:call.method]) {
result(@YES);
} else {
// TODO: Check if this implementation is correct.
// Can I just pass on the result as the last argument?
[backgroundChannel invokeMethod:call.method arguments:call.arguments result: result];
}
}
- (MPRemoteCommandHandlerStatus) play: (MPRemoteCommandEvent *) event {
NSLog(@"play");
[backgroundChannel invokeMethod:@"onPlay" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) pause: (MPRemoteCommandEvent *) event {
NSLog(@"pause");
[backgroundChannel invokeMethod:@"onPause" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (void) updateNowPlayingInfo {
NSMutableDictionary *nowPlayingInfo = [NSMutableDictionary new];
if (mediaItem) {
nowPlayingInfo[MPMediaItemPropertyTitle] = mediaItem[@"title"];
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = mediaItem[@"album"];
if (mediaItem[@"artist"] != [NSNull null]) {
nowPlayingInfo[MPMediaItemPropertyArtist] = mediaItem[@"artist"];
}
if (mediaItem[@"duration"] != [NSNull null]) {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = [NSNumber numberWithLongLong: ([mediaItem[@"duration"] longLongValue] / 1000)];
}
if (@available(iOS 3.0, macOS 10.13.2, *)) {
if (artwork) {
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork;
}
}
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = [NSNumber numberWithInt:([position intValue] / 1000)];
}
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = [NSNumber numberWithDouble: playing ? 1.0 : 0.0];
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo;
}
- (void) updateControls {
for (enum MediaAction action = AStop; action <= ASeekForward; action++) {
[self updateControl:action];
}
_controlsUpdated = YES;
}
- (void) updateControl:(enum MediaAction)action {
MPRemoteCommand *command = commands[action];
if (command == [NSNull null]) return;
// Shift the actionBits right until the least significant bit is the tested action bit, and AND that with a 1 at the same position.
// All bytes become 0, other than the tested action bit, which will be 0 or 1 according to its status in the actionBits long.
BOOL enable = ((actionBits >> action) & 1);
if (_controlsUpdated && enable == command.enabled) return;
[command setEnabled:enable];
switch (action) {
case AStop:
if (enable) {
[commandCenter.stopCommand addTarget:self action:@selector(stop:)];
} else {
[commandCenter.stopCommand removeTarget:nil];
}
break;
case APause:
if (enable) {
[commandCenter.pauseCommand addTarget:self action:@selector(pause:)];
} else {
[commandCenter.pauseCommand removeTarget:nil];
}
break;
case APlay:
if (enable) {
[commandCenter.playCommand addTarget:self action:@selector(play:)];
} else {
[commandCenter.playCommand removeTarget:nil];
}
break;
case ARewind:
if (rewindInterval.integerValue > 0) {
if (enable) {
[commandCenter.skipBackwardCommand addTarget: self action:@selector(skipBackward:)];
int rewindIntervalInSeconds = [rewindInterval intValue]/1000;
NSNumber *rewindIntervalInSec = [NSNumber numberWithInt: rewindIntervalInSeconds];
commandCenter.skipBackwardCommand.preferredIntervals = @[rewindIntervalInSec];
} else {
[commandCenter.skipBackwardCommand removeTarget:nil];
}
}
break;
case ASkipToPrevious:
if (enable) {
[commandCenter.previousTrackCommand addTarget:self action:@selector(previousTrack:)];
} else {
[commandCenter.previousTrackCommand removeTarget:nil];
}
break;
case ASkipToNext:
if (enable) {
[commandCenter.nextTrackCommand addTarget:self action:@selector(nextTrack:)];
} else {
[commandCenter.nextTrackCommand removeTarget:nil];
}
break;
case AFastForward:
if (fastForwardInterval.integerValue > 0) {
if (enable) {
[commandCenter.skipForwardCommand addTarget: self action:@selector(skipForward:)];
int fastForwardIntervalInSeconds = [fastForwardInterval intValue]/1000;
NSNumber *fastForwardIntervalInSec = [NSNumber numberWithInt: fastForwardIntervalInSeconds];
commandCenter.skipForwardCommand.preferredIntervals = @[fastForwardIntervalInSec];
} else {
[commandCenter.skipForwardCommand removeTarget:nil];
}
}
break;
case ASetRating:
// TODO:
// commandCenter.ratingCommand
// commandCenter.dislikeCommand
// commandCenter.bookmarkCommand
break;
case ASeekTo:
if (@available(iOS 9.1, macOS 10.12.2, *)) {
if (enable) {
[commandCenter.changePlaybackPositionCommand addTarget:self action:@selector(changePlaybackPosition:)];
} else {
[commandCenter.changePlaybackPositionCommand removeTarget:nil];
}
}
case APlayPause:
// Automatically enabled.
break;
case ASetRepeatMode:
if (enable) {
[commandCenter.changeRepeatModeCommand addTarget:self action:@selector(changeRepeatMode:)];
} else {
[commandCenter.changeRepeatModeCommand removeTarget:nil];
}
break;
case ASetShuffleMode:
if (enable) {
[commandCenter.changeShuffleModeCommand addTarget:self action:@selector(changeShuffleMode:)];
} else {
[commandCenter.changeShuffleModeCommand removeTarget:nil];
}
break;
case ASeekBackward:
if (enable) {
[commandCenter.seekBackwardCommand addTarget:self action:@selector(seekBackward:)];
} else {
[commandCenter.seekBackwardCommand removeTarget:nil];
}
break;
case ASeekForward:
if (enable) {
[commandCenter.seekForwardCommand addTarget:self action:@selector(seekForward:)];
} else {
[commandCenter.seekForwardCommand removeTarget:nil];
}
break;
}
}
- (MPRemoteCommandHandlerStatus) togglePlayPause: (MPRemoteCommandEvent *) event {
NSLog(@"togglePlayPause");
[backgroundChannel invokeMethod:@"onClick" arguments:@[@(0)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) stop: (MPRemoteCommandEvent *) event {
NSLog(@"stop");
[backgroundChannel invokeMethod:@"onStop" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) nextTrack: (MPRemoteCommandEvent *) event {
NSLog(@"nextTrack");
[backgroundChannel invokeMethod:@"onSkipToNext" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) previousTrack: (MPRemoteCommandEvent *) event {
NSLog(@"previousTrack");
[backgroundChannel invokeMethod:@"onSkipToPrevious" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) changePlaybackPosition: (MPChangePlaybackPositionCommandEvent *) event {
NSLog(@"changePlaybackPosition");
[backgroundChannel invokeMethod:@"onSeekTo" arguments: @[@((long long) (event.positionTime * 1000))]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) skipForward: (MPRemoteCommandEvent *) event {
NSLog(@"skipForward");
[backgroundChannel invokeMethod:@"onFastForward" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) skipBackward: (MPRemoteCommandEvent *) event {
NSLog(@"skipBackward");
[backgroundChannel invokeMethod:@"onRewind" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) seekForward: (MPSeekCommandEvent *) event {
NSLog(@"seekForward");
BOOL begin = event.type == MPSeekCommandEventTypeBeginSeeking;
[backgroundChannel invokeMethod:@"onSeekForward" arguments:@[@(begin)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) seekBackward: (MPSeekCommandEvent *) event {
NSLog(@"seekBackward");
BOOL begin = event.type == MPSeekCommandEventTypeBeginSeeking;
[backgroundChannel invokeMethod:@"onSeekBackward" arguments:@[@(begin)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) changeRepeatMode: (MPChangeRepeatModeCommandEvent *) event {
NSLog(@"changeRepeatMode");
int modeIndex;
switch (event.repeatType) {
case MPRepeatTypeOff:
modeIndex = 0;
break;
case MPRepeatTypeOne:
modeIndex = 1;
break;
// MPRepeatTypeAll
default:
modeIndex = 2;
break;
}
[backgroundChannel invokeMethod:@"onSetRepeatMode" arguments:@[@(modeIndex)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) changeShuffleMode: (MPChangeShuffleModeCommandEvent *) event {
NSLog(@"changeShuffleMode");
int modeIndex;
switch (event.shuffleType) {
case MPShuffleTypeOff:
modeIndex = 0;
break;
case MPShuffleTypeItems:
modeIndex = 1;
break;
// MPShuffleTypeCollections
default:
modeIndex = 2;
break;
}
[backgroundChannel invokeMethod:@"onSetShuffleMode" arguments:@[@(modeIndex)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end