新需求要在项目中增加音频播放的功能,原本使用AVPlayer自己写了一个demo实现了音频文件的播放,但是考虑到以后的拓展和内存的优化等问题,决定找找有没有好一点的第三方框架可以使用,于是乎发现了FreeStreamer。
使用FreeStreamer实现音频播放,音频播放控制,后台播放,后台播放控制等功能。
一、实现音频播放
-
使用cocoapods添加FreeStreamer库
pod 'FreeStreamer', '~> 3.8.2'
-
导入FreeStreamer框架
#import
-
创建播放类并进行播放音频
_audioStream = [[FSAudioStream alloc] init];
// 播放失败的回调
_audioStream.onFailure = ^(FSAudioStreamError error,NSString *description){
NSLog(@"播放过程中发生错误,错误信息:%@",description);
};
// 播放完成的回调
_audioStream.onCompletion=^(){
NSLog(@"播放完成!");
};
// 设置音量
[_audioStream setVolume:0.5];
// 使用音频链接URL播放音频
NSString *urlStr = @"http://up.mcyt.net/down/45957.mp3";
NSURL *url = [NSURL URLWithString:urlStr];
[_audioStream playFromURL:url];
如果出现Strict content type checking active, application/octet-stream is not an audio content type
的错误信息,许多服务器也许会给你发送错误的 MIME 类型。这种情况下,FreeStreamer 也许就不能播放这个音频了。如果你想避免 content-type 检查(但是这个 steam 依然是音频文件),你可以设置如下的属性:
// 不进行检测格式 <开启检测之后,有些网络音频链接无法播放>
_audioStream.strictContentTypeChecking = NO;
_audioStream.defaultContentType = @"audio/mpeg";
二、实现音频的播放控制
-
暂停
直接调用[self.audioStream pause]
即可
- (void)pauseAction:(id)sender {
if (self.audioStream.isPlaying) {
[self.audioStream pause];
}
}
-
播放
[self.audioStream play]
不是音频暂停之后进行播放,而是初始化完成之后开始播放音频,所以,音频暂停之后继续调用pause方法即可恢复播放。
- (void)playAction:(id)sender {
if (!self.audioStream.isPlaying) {
[self.audioStream pause];
}
}
-
上一曲 / 下一曲
使用[self.audioStream playFromURL:url]
直接切换音频链接即可
// 上一曲
- (IBAction)lastMusicAction:(UIButton *)sender {
[self.audioStream stop];
NSString *urlStr = [NSString stringWithFormat:@"http://up.mcyt.net/down/47541.mp3", _currentID];
NSURL *url=[NSURL URLWithString:urlStr];
[self.audioStream playFromURL:url];
}
// 下一曲
- (IBAction)nextMusicAction:(id)sender {
[self.audioStream stop];
NSString *urlStr = @"http://up.mcyt.net/down/47541.mp3";
NSURL *url=[NSURL URLWithString:urlStr];
[self.audioStream playFromURL:url];
}
-
快进 / 快退
使用[self.audioStream seekToPosition:position]
进行播放进度的切换
根据不同的状态给UISlider
添加不同的addTarget
方法:
[self.progress addTarget:self action:@selector(progressChangeAction:) forControlEvents:(UIControlEventValueChanged)];
[self.progress addTarget:self action:@selector(progressTouchBeginAction:) forControlEvents:(UIControlEventTouchDown)];
[self.progress addTarget:self action:@selector(progressTouchEndAction:) forControlEvents:(UIControlEventTouchUpInside)];
实现addTarget
方法,实现快进或快退:
// 进度正在改变
- (void)progressChangeAction:(UISlider *)slider {
float value = slider.value;
// 进度 * 总时间 获取当前时间
float current = value * _totalTime;
// 当前分钟数
double minutesElapsed =floor(fmod(current/60.0,60.0));
// 当前秒数
double secondsElapsed =floor(fmod(current,60.0));
// 格式化当前时间
NSString *currentTime = [NSString stringWithFormat:@"%02.0f:%02.0f", minutesElapsed, secondsElapsed];
// 改变显示当前时间的标签文字
self.currentTimeLabel.text = currentTime;
}
// 开始改变进度
- (void)progressTouchBeginAction:(UISlider *)sender {
NSLog(@"开始触摸");
[self removeTimer];
// 暂停
[self pauseAction:nil];
}
// 结束改变进度
- (void)progressTouchEndAction:(UISlider *)sender {
NSLog(@"结束触摸");
[self addTimer];
// 播放
[self playAction:nil];
// 获取进度 0 ~ 1
float value = sender.value == 0 ? 0.001 : sender.value;
// 创建播放进度对象
FSStreamPosition position;
// 赋值
position.position = value;
// 跳转进度
[self.audioStream seekToPosition:position];
}
FSStreamPosition
是一个结构体,可使用跳转的分钟minute
和秒second
;跳转的时间总秒数playbackTimeInSeconds
;跳转的位置position
三种方式进行跳转:
typedef struct {
/**
* minute 分钟数
* second 秒数
*/
unsigned minute;
unsigned second;
/**
* Playback time in seconds. 播放时间总秒数
*/
float playbackTimeInSeconds;
/**
* Position within the stream, where 0 is the beginning
* and 1.0 is the end. 播放时间的位置<进度0~1>
*/
float position;
} FSStreamPosition;
-
动态改变播放进度、播放时间
该方法需要使用定时器持续调用
- (void)playProgressAction {
FSStreamPosition cur = self.audioStream.currentTimePlayed;
float playbackTime = cur.playbackTimeInSeconds/1;
double minutesElapsed = floor(fmod(playbackTime/60.0,60.0));
double secondsElapsed = floor(fmod(playbackTime,60.0));
NSString *currentTime = [NSString stringWithFormat:@"%02.0f:%02.0f", minutesElapsed, secondsElapsed];
NSLog(@"当前播放时间:%f", playbackTime);//播放进度
NSLog(@"格式化当前播放时间:%@", currentTime);
// 获取视频的总时长
float totalTime = playbackTime / cur.position;
// 记录音频总时间
_totalTime = totalTime;
NSLog(@"总时间:%f", totalTime);
if ([[NSString stringWithFormat:@"%f",totalTime] isEqualToString:@"nan"]) {
NSLog(@"格式化总时间:00:00");
}else{
double minutesElapsed1 =floor(fmod(totalTime/60.0,60.0));
double secondsElapsed1 =floor(fmod(totalTime,60.0));
NSString *total = [NSString stringWithFormat:@"%02.0f:%02.0f",minutesElapsed1, secondsElapsed1];
NSLog(@"格式化总时间:%@", total);
// 改变当前播放时间和音频总时间的显示
self.currentTimeLabel.text = currentTime;
self.totalTimeLabel.text = total;
}
float prebuffer = (float)self.audioStream.prebufferedByteCount;
float contentlength = (float)self.audioStream.contentLength;
if (contentlength>0) {
NSLog(@"缓存进度:%f", prebuffer / contentlength);
// 改变播放进度
self.progress.value = cur.position;
}
}
-
调节音量
给调节音量的UISlider
添加addTarget
方法:
[self.valumSlider addTarget:self action:@selector(valumChangeAction:) forControlEvents:(UIControlEventValueChanged)];
实现调节音量的方法:
- (void)valumChangeAction:(UISlider *)slider {
self.audioStream.volume = slider.value;
}
三、实现后台播放
-
打开后台模式
进入工程TARGETS打开后台模式:
-
检查info.plist是否自动生成后台播放标签
打开后台模式之后,在info.plist
文件中自动生成Required background modes
标签:
至此,返回后台已经可以继续播放音频了。
-
添加后台播放任务
以上配置完成之后,在进入后台播放一段时间后,还是会被系统停止音频播放,此时还应在AppDelegate.m
中开启后台任务。
@interface AppDelegate ()
{
UIBackgroundTaskIdentifier _bgTaskId;
}
@end
- (void)applicationWillResignActive:(UIApplication *)application {
//开启后台处理多媒体事件
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
AVAudioSession *session=[AVAudioSession sharedInstance];
[session setActive:YES error:nil];
//后台播放
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
//这样做,可以在按home键进入后台后 ,播放一段时间,几分钟吧。但是不能持续播放网络歌曲,若需要持续播放网络歌曲,还需要申请后台任务id,具体做法是:
_bgTaskId=[AppDelegate backgroundPlayerID:_bgTaskId];
//其中的_bgTaskId是后台任务UIBackgroundTaskIdentifier _bgTaskId;
}
+ (UIBackgroundTaskIdentifier)backgroundPlayerID:(UIBackgroundTaskIdentifier)backTaskId {
//设置并激活音频会话类别
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[session setActive:YES error:nil];
//允许应用程序接收远程控制
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
//设置后台任务ID
UIBackgroundTaskIdentifier newTaskId = UIBackgroundTaskInvalid;
newTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
if(newTaskId != UIBackgroundTaskInvalid && backTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:backTaskId];
}
return newTaskId;
}
-
优化后台播放可能遇到的问题
后台播放时可能会出现[avas] AVAudioSession.mm:1074:-[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session.
的错误信息,解决办法可查看这篇文章。
四、线控及锁屏信息
-
配置第一响应者
让播放控制类成为第一响应者,后台的控制在该类中响应:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
//以及设置app支持接受远程控制事件代码。设置app支持接受远程控制事件,
//其实就是在dock中可以显示应用程序图标,同时点击该图片时,打开app。
//或者锁屏时,双击home键,屏幕上方出现应用程序播放控制按钮。
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder]; //成为FristResponder
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
}
-
接收远程控制信息
实现远程控制接收事件,进行区分事件的类别,响应不同的操作:
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
if (event.type == UIEventTypeRemoteControl) {
switch (event.subtype) {
// 播放
case UIEventSubtypeRemoteControlPlay:
{
[self playAction:nil];
}
break;
// 暂停
case UIEventSubtypeRemoteControlPause:
{
[self pauseAction:nil];
}
break;
// 停止播放
case UIEventSubtypeRemoteControlStop:
{
[self.audioStream stop];
}
break;
// 播放下一曲按钮
case UIEventSubtypeRemoteControlNextTrack:
{
[self nextMusicAction:nil];
}
break;
// 播放上一曲按钮
case UIEventSubtypeRemoteControlPreviousTrack:
{
[self lastMusicAction:nil];
}
break;
case UIEventSubtypeRemoteControlTogglePlayPause:
{
if (self.audioStream.isPlaying) {
[self pauseAction:nil];
} else {
[self playAction:nil];
}
}
break;
default:
break;
}
}
}
-
修改锁屏界面音频信息
当前音频开始播放及时修改信息。
// 改变锁屏歌曲信息
- (void)setLockScreenNowPlayingInfo {
//更新锁屏时的歌曲信息
if (NSClassFromString(@"MPNowPlayingInfoCenter")) {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
// 歌曲名
[dict setObject:@"体面" forKey:MPMediaItemPropertyTitle];
// 演唱者
[dict setObject:@"于文文" forKey:MPMediaItemPropertyArtist];
// 专辑名
[dict setObject:@"专辑《体面》" forKey:MPMediaItemPropertyAlbumTitle];
//专辑缩略图
UIImage *newImage = [UIImage imageNamed:@"音乐"];
[dict setObject:[[MPMediaItemArtwork alloc] initWithImage:newImage] forKey:MPMediaItemPropertyArtwork];
//设置锁屏状态下屏幕显示播放音乐信息
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
}
-
修改锁屏界面音频的播放进度
需要在修改音频播放进度条的方法中动态调用该方法,以动态改变锁屏界面的播放进度。
/**
定时器修改进度
@param duration 总时间
@param current 当前时间
*/
- (void)changeLockProgress:(NSInteger)duration current:(NSInteger)current {
if(self.audioStream.isPlaying) {
//当前播放时间
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
// 歌曲总时长
[dict setObject:@(duration) forKey:MPMediaItemPropertyPlaybackDuration];
// 当前播放时间
[dict setObject:@(current) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
}