公司项目的一个音频播放的小需求,要求支持iOS的远程控制和耳机线控然后写完后尝试总结一下吧
现在把这个改成了Flutter的插件,支持Android 和iOS,都支持远程控制
Demo地址
Flutter Demo
大致需求:
1.音频播放
2.可切换播放音频源
3.支持远程控制和线控
4.一个基于三角函数的播放动画
音频播放用的库:pod 'StreamingKit', '~> 0.1.30'
MPNowPlayingInfoCenter 负责远程控制的内容展示
-(void)updateNowPlayingInfo:(TJMediaBackGroundModel *__nullable)nowPlayingInfo palyState:(BOOL)isPlay
{
_nowPlayingInfo = nowPlayingInfo;
NSMutableDictionary *songInfo = nil;
MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
if (nowPlayingInfo) {
songInfo = [[NSMutableDictionary alloc] initWithDictionary:[center nowPlayingInfo]];
__block UIImage *coverImage = nowPlayingInfo.coverImage;
SDWebImageManager *manager = [SDWebImageManager sharedManager];
NSString* key = [manager cacheKeyForURL:[NSURL URLWithString:nowPlayingInfo.coverUrl]];
SDImageCache* cache = [SDImageCache sharedImageCache];
coverImage = [cache imageFromDiskCacheForKey:key];
if(!coverImage){
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:nowPlayingInfo.coverUrl] options:SDWebImageDownloaderHighPriority progress:nil completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
coverImage = image;
//媒体封面
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:CGSizeMake(200, 200) requestHandler:^UIImage * _Nonnull(CGSize size) {
if(!coverImage){
coverImage = [UIImage imageNamed:@"AppIcon"];
}
return coverImage;
}];
//标题
[songInfo setObject: returnBeNil(nowPlayingInfo.title) forKey:MPMediaItemPropertyTitle];
//作者
[songInfo setObject: returnBeNil(nowPlayingInfo.auther) forKey:MPMediaItemPropertyArtist];
[songInfo setObject: artwork forKey:MPMediaItemPropertyArtwork];
//媒体资源总时长
[songInfo setObject:[NSNumber numberWithDouble:nowPlayingInfo.playbackDuration] forKey:MPMediaItemPropertyPlaybackDuration];
//设置已经播放时长
[songInfo setObject:[NSNumber numberWithDouble:nowPlayingInfo.playbackTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//播放速率
[songInfo setValue:[NSNumber numberWithDouble:isPlay?1.0:0.0] forKey:MPNowPlayingInfoPropertyPlaybackRate];
[songInfo setValue:[NSNumber numberWithDouble:isPlay?1.0:0.0] forKey:MPNowPlayingInfoPropertyDefaultPlaybackRate];
[center setNowPlayingInfo:songInfo];
});
}];
}else{
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:CGSizeMake(200, 200) requestHandler:^UIImage * _Nonnull(CGSize size) {
if(!coverImage){
coverImage = [UIImage imageNamed:@"AppIcon"];
}
return coverImage;
}];
[songInfo setObject: returnBeNil(nowPlayingInfo.title) forKey:MPMediaItemPropertyTitle];
[songInfo setObject: returnBeNil(nowPlayingInfo.auther) forKey:MPMediaItemPropertyArtist];
[songInfo setObject: artwork forKey:MPMediaItemPropertyArtwork ];
[songInfo setObject:[NSNumber numberWithDouble:nowPlayingInfo.playbackDuration] forKey:MPMediaItemPropertyPlaybackDuration];
//设置已经播放时长
[songInfo setObject:[NSNumber numberWithDouble:nowPlayingInfo.playbackTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[songInfo setValue:[NSNumber numberWithDouble:isPlay?1.0:0.0] forKey:MPNowPlayingInfoPropertyPlaybackRate];
[songInfo setValue:[NSNumber numberWithDouble:isPlay?1.0:0.0] forKey:MPNowPlayingInfoPropertyDefaultPlaybackRate];
[center setNowPlayingInfo:songInfo];
}
}else{
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:nil];
}
}
注:
播放进度由播放速率和播放时长自动计算,不需要频繁更新播放进度
MPRemoteCommandCenter 负责事件控制
-(void)registerRemoteCommandAction:(BOOL)moreData{
MPRemoteCommandCenter *remoteCommandCenter = [MPRemoteCommandCenter sharedCommandCenter];
//播放
MPRemoteCommand *playCommand = [remoteCommandCenter playCommand];
//暂停
MPRemoteCommand *pauseCommand = [remoteCommandCenter pauseCommand];
//下一首
MPRemoteCommand *nextTrackCommand = [remoteCommandCenter nextTrackCommand];
//上一首
MPRemoteCommand *previousTrackCommand = [remoteCommandCenter previousTrackCommand];
//快进10s
MPSkipIntervalCommand *skipForwardCommand = [remoteCommandCenter skipForwardCommand];
//后退10s
MPSkipIntervalCommand *skipBackwardCommand = [remoteCommandCenter skipBackwardCommand];
//拖动进度条
MPChangePlaybackPositionCommand *changePositionCommand = [remoteCommandCenter changePlaybackPositionCommand];
[playCommand setEnabled:YES];
[pauseCommand setEnabled:YES];
[changePositionCommand setEnabled:YES];
[playCommand addTarget:self action:@selector(playCommandAction:)];
[pauseCommand addTarget:self action:@selector(pauseCommandAction:)];
[changePositionCommand addTarget:self action:@selector(changePositionCommandAction:)];
if(moreData){
_nextTrackCommandActionBlock?[nextTrackCommand setEnabled:YES]:[nextTrackCommand setEnabled:NO];
_previousTrackCommandActionBlock?[previousTrackCommand setEnabled:YES]:[previousTrackCommand setEnabled:NO];
[nextTrackCommand addTarget:self action:@selector(nextTrackCommandAction:)];
[previousTrackCommand addTarget:self action:@selector(previousTrackCommandAction:)];
}else{
_skipForwardCommandActionBlock?[skipForwardCommand setEnabled:YES]:[skipForwardCommand setEnabled:NO];
_skipBackwardCommandActionBlock?[skipBackwardCommand setEnabled:YES]:[skipBackwardCommand setEnabled:NO];
[skipForwardCommand addTarget:self action:@selector(skipForwardCommandAction:)];
[skipBackwardCommand addTarget:self action:@selector(skipBackwardCommandAction:)];
}
//插拔耳机
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];
}
//AppDelegate 中实现 远程控制操作
-(void)remoteControlReceivedWithEvent:(UIEvent *)event{}
App切到后台时,其他App播放或者电话来电等中断操作
//在低系统(如7.1.2)可能收不到这个回调,请在onAppDidEnterBackGround和onAppWillEnterForeground里面处理打断逻辑
- (void)onAudioSessionEvent: (NSNotification *) notification
{
NSDictionary *info = notification.userInfo;
AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (type == AVAudioSessionInterruptionTypeBegan) { //音频被其他app占用
if([self getAudioIsPlaying]){
[self pause];
}
}else{
AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
if (options == AVAudioSessionInterruptionOptionShouldResume) {
AVAudioSession *session = [AVAudioSession sharedInstance];
if(session.category != AVAudioSessionCategoryPlayback){
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback mode:AVAudioSessionModeMoviePlayback options:AVAudioSessionCategoryOptionAllowAirPlay error:nil];
[[AVAudioSession sharedInstance] setActive:YES
error:nil];
}
}
}
}
#pragma mark 监听来电状态 --------------------
-(CXCallObserver *)callObserver{
if(!_callObserver){
_callObserver = [CXCallObserver new];
}
return _callObserver;
}
- (void)callObserver:(CXCallObserver *)callObserver callChanged:(CXCall *)call
{
//接通
if (call.outgoing && call.hasConnected && !call.hasEnded) {
if(self.cxCallObserverCallback){
self.cxCallObserverCallback(YES);
}
}
//挂断
if (call.outgoing && call.hasConnected && call.hasEnded) {
if(self.cxCallObserverCallback){
self.cxCallObserverCallback(NO);
}
}
}
注意点:
1.系统电话需要 添加 CXCallObserver 监听
2.除了 AVAudioSessionCategoryMultiRoute 外,其他的 Category 都遵循 last in wins 原则,
即最后接入的音频设备作为输入或输出的主设备,并且触发 AVAudioSessionInterruptionNotification 通知 中断
3.支持耳机操作 需要 [[UIApplicationsharedApplication]beginReceivingRemoteControlEvents];
并且在 AppDelegate 中实现
-(void)remoteControlReceivedWithEvent:(UIEvent )event{ } 代理
并且 MPRemoteCommandCenter 的操作和 remoteControlReceivedWithEvent会同时触发,需要过滤
4. 播放状态不受代码控制,由系统判断,并且播放器容器或者音频编解码需要使用系统库处理才会同步播放状态改变
网友评论