本文已参与「新人创作礼」活动,一起开启掘金创作之路
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
写一下最近项目里面用到的音频播放器
背景
- 项目急着上线,关于音频播放的项目, 类似喜马拉雅的一款听书APP
- 要求,播放器常用的功能,播放、暂停、快进、快退、指定时间播放,上一首,下一首,后台播放等
需求分析
- 播放器嘛,首先想到的是AVAudioPlayer
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlStr]];
AVAudioPlayer *player = [[AVAudioPlayer alloc]initWithData:data error:**nil**];
/*
/* Transport control */
/* Methods that return BOOL return YES on success and NO on failure. */
/* Get ready to play the sound. This happens automatically on play. */
- (**BOOL**)prepareToPlay;
\
/* This method starts the audio hardware synchronously (if not already running), and triggers the sound playback which is streamed asynchronously. */
- (**BOOL**)play;
\
/* This method starts the audio hardware synchronously (if not already running), and triggers the sound playback which is streamed asynchronously at the specified time in the future.
Time is an absolute time based on and greater than deviceCurrentTime. */
- (**BOOL**)playAtTime:(NSTimeInterval)time API_AVAILABLE(macos(10.7), ios(4.0), watchos(2.0), tvos(9.0));
\
/* Pauses playback, but remains ready to play. */
- (**void**)pause;
\
/* Synchronously stops playback, no longer ready to play.
NOTE: - This will block while releasing the audio hardware that was acquired upon calling play() or prepareToPlay() */
- (**void**)stop;
*/
//倍数播放
player.enableRate = **YES**;
player.rate = 3.5;
player.duration//音频时长
player.currentTime//播放的当前时间
到这里,播放器的所有功能都满足。开开心心集成进去。好巧遇上公司网络不好,调好的播放器,突然就不播放了,打断点一看, data是nil,明白过来,dataWithContentsOfURL 是把网络数据转成本地二进制数据,也就是音频文件下载成本地文件再播放。一个长一点的音频文件 可能几百M,显然不现实。
重新构思
AVPlayer 支持在线播放,稍微看了一下,基本需求都能满足,改用AVPlayer
- (**void**)play NS_SWIFT_UI_ACTOR;
- (**void**)pause NS_SWIFT_UI_ACTOR;
//倍数播放
- (**void**)playImmediatelyAtRate:(**float**)rate NS_SWIFT_UI_ACTOR API_AVAILABLE(macos(10.12), ios(10.0), tvos(10.0), watchos(3.0));
AVPlayerItem * playerItem = [AVPlayerItem playerItemWithURL:url];
playerItem.duration//音频文件的时长
- (**id**)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(**nullable** dispatch_queue_t)queue usingBlock:(**void** (^)(CMTime time))block;//获取播放的当前时长
//设置指定时间播放
[**self**.player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
[**self**.player play];
至此,播放器的所有功能需要的接口都已经找到了,剩下的就是封装了。(时间比较急,又不屑于用一些第三方的轮子)集成到项目里面了,稍后独立出来,再把单独的模块的贴出来
另一个问题出现
- 播放器肯定是支持后台播放,进入到后台需要监听播放、暂停,上一首、下一首的事件。监听事件写到控制器,控制器跳转了或者销毁了就监听不到了,用户可能在任一界面进入后台,都是很常规的操作
解决思路
- 播放器在任何界面,音频都是可以播放的。那就在APP启动的时候写一个不会销毁的类。刚好UI需求有一个全局的播放浮窗。(就算没有这个UI,也可以写一个隐藏的全局UI)我这里用的是一个uiwindow,直接贴代码
-(**instancetype**)initWithFrame:(CGRect)frame
{
**if** (**self** = [**super** initWithFrame:frame]) {
[**self** configUI];
[**self** configPlayer];
**self**.backgroundColor = BGWHITECOLOR;
}
**return** **self**;
}
-(**void**)configPlayer
{
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[**self** becomeFirstResponder];
// 连续播发
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}
- (**BOOL**)canBecomeFirstResponder
{
NSLog(@"canBecomeFirstResponder");
**return** **YES**;
}
- (**void**) remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
**if** (receivedEvent.type == UIEventTypeRemoteControl) {
**switch** (receivedEvent.subtype) {
**case** UIEventSubtypeRemoteControlPlay:
NSLog(@"播放");
**break**;
**case** UIEventSubtypeRemoteControlPause:
NSLog(@"暂停");
**break**;
**case** UIEventSubtypeRemoteControlPreviousTrack:
NSLog(@"上一首");
[**self** setBackgroundInfo];
**break**;
**case** UIEventSubtypeRemoteControlNextTrack:
NSLog(@"下一首");
[**self** setBackgroundInfo];
**break**;
**default**:
**break**;
}
}
}
#pragma mark **- 开始播放歌曲**
- (**void**)startPlayingMusicWithMode:(GJBookItemModel *)model
{
**self**.model = model;
[**self** playMusicWithCurrentModel];
[**self** setDataWithmodel:model isShouldPlay:**YES**];//更新播放UI信息
}
- (**void**)playMusicWithCurrentModel
{
NSString *audioUrl = **self**.model.audioUrl;
**self**.currentPlayer = [[GJNetPlayer alloc]init];
[**self** notifyProgress];
[**self**.currentPlayer createAudioPlayerWith:audioUrl];
**self**.currentPlayer.changedTime = **self**.model.listenTotal;
[**self**.currentPlayer play];
[**self** setBackgroundInfo];
}
//更新锁屏时的歌曲信息
- (**void**)setBackgroundInfo
{
**if** (NSClassFromString(@"MPNowPlayingInfoCenter"))
{
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
NSString *auth =@"";
NSString *coverUrl = @"";
**if** (**self**.model.createdByName) {
auth = **self**.model.createdByName;
}
UIImage *newImage =PLACEHODERIMAGE;
**if** (**self**.model.coverUrl) {
coverUrl = **self**.model.coverUrl;
newImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:coverUrl]]];
}
[dict setObject:**self**.model.name forKey:MPMediaItemPropertyTitle];//歌曲名设置
[dict setObject:auth forKey:MPMediaItemPropertyArtist];
[dict setObject:auth forKey:MPMediaItemPropertyAlbumTitle];//专辑名
[dict setObject:[[MPMediaItemArtwork alloc] initWithImage:newImage]
forKey:MPMediaItemPropertyArtwork];//专辑图片设置
[dict setObject:[NSNumber numberWithDouble:**self**.currentPlayer.currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //音乐当前已经播放时间
[dict setObject:[NSNumber numberWithFloat:1.0] forKey:MPNowPlayingInfoPropertyPlaybackRate];//进度光标的速度
[dict setObject:[NSNumber numberWithDouble:**self**.model.duration] forKey:MPMediaItemPropertyPlaybackDuration];//歌曲总时间设置
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
}