最近接到一個需求,需要做一個在后臺播放視頻的功能攻泼。折騰了一下火架,最后總算完成了。因此寫一篇文章忙菠,介紹下具體的實現(xiàn)步驟何鸡,也說說自己遇到的坑,算是總結(jié)和記錄牛欢。
前言
當(dāng) App 退到后臺時骡男,會進入 suspend
狀態(tài),若此時在播放視頻氢惋,則會自動暫停洞翩。我們需要實現(xiàn)的效果是稽犁,當(dāng) App 退到后臺時,視頻中的聲音還能繼續(xù)播放骚亿。另外已亥,我們還同時實現(xiàn)視頻的連續(xù)播放功能,和在鎖屏界面控制視頻播放的功能来屠。具體怎么做虑椎,下面聽我一一道來。
注意:由于 iOS 模擬器存在 BUG俱笛,尤其是 iOS 11 的模擬器捆姜,不能在后臺播放音頻,因此以下功能最好使用真機測試迎膜。
一泥技、后臺播放音頻
要實現(xiàn)后臺播放視頻功能,首先需要實現(xiàn)后臺播放音頻功能磕仅。實現(xiàn)后臺播放音頻很簡單珊豹,只要簡單配置一下就可以了¢哦總共有三步:
1. 修改 Info.plist
在 Info.plist
中添加 Required background modes
店茶,并在下面添加一項 App plays audio or streams audio/video using AirPlay
。如圖所示:
2. 修改 Capabilities
在 Capabilities
中開啟 Background Modes
劫恒。如圖所示:
3. 修改 AppDelegate
在 AppDelegate
的 application: didFinishLaunchingWithOptions:
方法中贩幻,添加以下代碼:
// 告訴app支持后臺播放
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setActive:YES error:nil];
至此就實現(xiàn)了后臺播放音頻的功能,但這不是我們的最終目的两嘴,請繼續(xù)往下看丛楚。
二、后臺播放視頻
網(wǎng)上講實現(xiàn)后臺播放視頻的資料并不多(可能比較少有這么坑的需求)溶诞。我在網(wǎng)上找了一圈鸯檬,只有 這篇文章 提到了,方法也很簡單螺垢,分為兩步:
1. 退到后臺時移除 playerLayer 上的 player
在 viewController
中添加退到后臺監(jiān)聽:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(removePlayerOnPlayerLayer)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
移除 player
:
- (void)removePlayerOnPlayerLayer {
_playerLayer.player = nil;
}
2. 回到前臺時重新添加 player
在 viewController
中添加回到前臺監(jiān)聽:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(resetPlayerToPlayerLayer)
name:UIApplicationWillEnterForegroundNotification
object:nil];
重新添加 player
:
- (void)resetPlayerToPlayerLayer {
_playerLayer.player = _player;
}
這樣簡單的后臺播放視頻就實現(xiàn)了喧务。
對于上面的實現(xiàn)后臺播放視頻的方法,我的理解是枉圃,iOS 是支持后臺播放音頻的功茴,而 AVPlayer
在播放視頻時,會將圖像渲染在 layer
上孽亲,因此只要取消圖像的渲染坎穿,只播放音頻,就可以實現(xiàn)后臺播放。
3. 連續(xù)播放視頻
后臺連續(xù)播放視頻的邏輯玲昧,其實和前臺連續(xù)播放的邏輯一樣栖茉。可以通過監(jiān)聽 playerItem
播放結(jié)束的通知來切換歌曲孵延,則當(dāng)播放結(jié)束時吕漂,需要移除對當(dāng)前 playerItem
的監(jiān)聽,然后添加下一個 playerItem
的監(jiān)聽尘应。
這里直接通過判斷進度條是否完成惶凝,來切換歌曲。
// 監(jiān)聽播放進度
__weak ViewController * weakSelf = self;
[self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, NSEC_PER_SEC)
queue:NULL
usingBlock:^(CMTime time) {
[weakSelf updateProgressView];
}];
// 更新進度條進度
- (void)updateProgressView {
self.currentDuration = CMTimeGetSeconds(_player.currentItem.duration);
CGFloat progress = CMTimeGetSeconds(_player.currentItem.currentTime) / _currentDuration;
if (progress == 1.0f) {
[self playNextVideo]; // 播放下一個視頻
} else {
[_viewVideoProgress setValue:progress]; // 更新進度條
}
}
下面插播一條
CMTime
的廣告犬钢〔韵剩可跳過。上面監(jiān)聽播放進度的時候玷犹,用到了一個叫
CMTime
的東西混滔,這里簡單地講一下我的理解。
一般我們用CMTime
的時候歹颓,都是使用CMTimeGetSeconds(time)
將它轉(zhuǎn)成秒數(shù)遍坟。
那為何不直接使用NSTimeInterval
來表示時間就好了?原因只有一個 —— 精度晴股。
浮點數(shù)沒有辦法進行準確的加減運算,當(dāng)多次加減后肺魁,可能會出現(xiàn)較大誤差电湘。因此在視頻一般用
CMTime
來表示時間,因為CMTime
可以規(guī)定最小的精度鹅经,從而保證累加后時間的準確性寂呛。
CMTime
的構(gòu)造方法CMTimeMakeWithSeconds(seconds, timescale)
,seconds
表示秒數(shù)瘾晃,1 / timescale
表示最小精度贷痪。
另一個構(gòu)造方法CMTimeMake(value, timescale)
,其中seconds
=value
/timescale
蹦误。
即CMTimeMakeWithSeconds(1, 1000)
等價于CMTimeMake(1000, 1000)
劫拢,都表示 1 秒,最小精度為 0.001 强胰。注意:需要滿足
seconds
>=1 / timescale
舱沧,即value
> 1,這也是精度存在的意義偶洋。
三熟吏、添加遠程控制
1. 用 MPNowPlayingInfoCenter 顯示歌曲信息
先上代碼:
// 更新鎖屏界面信息
- (void)updateLockScreenInfo {
if (!_player) {
return;
}
// 1.獲取鎖屏中心
MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
// 初始化一個存放音樂信息的字典
NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary];
// 2、設(shè)置歌曲名
[playingInfoDict setObject:[NSString stringWithFormat:@"歌曲%ld", (long)_currentIndex + 1]
forKey:MPMediaItemPropertyTitle];
[playingInfoDict setObject:[NSString stringWithFormat:@"專輯%ld", (long)_currentIndex + 1]
forKey:MPMediaItemPropertyAlbumTitle];
// 3、設(shè)置封面的圖片
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"cover%ld.jpg", (long)_currentIndex + 1]];
if (image) {
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image];
[playingInfoDict setObject:artwork forKey:MPMediaItemPropertyArtwork];
}
// 4牵寺、設(shè)置歌曲的時長和已經(jīng)消耗的時間
NSNumber *playbackDuration = @(CMTimeGetSeconds(_player.currentItem.duration));
NSNumber *elapsedPlaybackTime = @(CMTimeGetSeconds(_player.currentItem.currentTime));
if (!playbackDuration || !elapsedPlaybackTime) {
return;
}
[playingInfoDict setObject:playbackDuration
forKey:MPMediaItemPropertyPlaybackDuration];
[playingInfoDict setObject:elapsedPlaybackTime
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[playingInfoDict setObject:@(_player.rate) forKey:MPNowPlayingInfoPropertyPlaybackRate];
//音樂信息賦值給獲取鎖屏中心的nowPlayingInfo屬性
playingInfoCenter.nowPlayingInfo = playingInfoDict;
}
注意:
updateLockScreenInfo
不需要頻繁調(diào)用悍引,鎖屏界面的進度條會自己計時,只需要在關(guān)鍵的時刻去同步這個已播放時長帽氓。一般需要調(diào)用的時刻有趣斤,切換歌曲、暫停杏节、播放唬渗、拖動進度條等。
這里有個坑奋渔。我們知道
player
有個rate
屬性镊逝,為 0 的時候表示暫停,為 1.0 的時候表示播放嫉鲸。相應(yīng)的撑蒜,nowPlayingInfo
也有個MPNowPlayingInfoPropertyPlaybackRate
屬性。前面說到玄渗,「鎖屏界面的進度條會自己計時」座菠,它是否在計時就是取決于這個屬性√偈鳎坑的地方在于浴滴,這個屬性和player
的rate
并不同步。也就是說岁钓,單純地在鎖屏界面點暫停后升略,player
會暫停,rate
也會變成 0 屡限,但是MPNowPlayingInfoPropertyPlaybackRate
卻不為 0 品嚣。導(dǎo)致的結(jié)果是,在鎖屏界面點擊了暫停按鈕钧大,這個時候進度條表面看起來停止了走動翰撑,但是其實還是在計時,所以再點擊播放的時候啊央,鎖屏界面進度條的光標會發(fā)生位置閃動眶诈。
解決方法:在視頻暫停和播放的時候,同步視頻的已播放時長
_player.currentItem.currentTime
和MPNowPlayingInfoPropertyElapsedPlaybackTime
劣挫、視頻的當(dāng)前播放速率_player.rate
和MPNowPlayingInfoPropertyPlaybackRate
册养。
2. 用 MPRemoteCommandCenter 實現(xiàn)播放控制
先上代碼:
// 添加遠程控制
- (void)createRemoteCommandCenter {
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
MPRemoteCommand *pauseCommand = [commandCenter pauseCommand];
[pauseCommand setEnabled:YES];
[pauseCommand addTarget:self action:@selector(remotePauseEvent)];
MPRemoteCommand *playCommand = [commandCenter playCommand];
[playCommand setEnabled:YES];
[playCommand addTarget:self action:@selector(remotePlayEvent)];
MPRemoteCommand *nextCommand = [commandCenter nextTrackCommand];
[nextCommand setEnabled:YES];
[nextCommand addTarget:self action:@selector(remoteNextEvent)];
MPRemoteCommand *previousCommand = [commandCenter previousTrackCommand];
[previousCommand setEnabled:YES];
[previousCommand addTarget:self action:@selector(remotePreviousEvent)];
if (@available(iOS 9.1, *)) {
MPRemoteCommand *changePlaybackPositionCommand = [commandCenter changePlaybackPositionCommand];
[changePlaybackPositionCommand setEnabled:YES];
[changePlaybackPositionCommand addTarget:self action:@selector(remoteChangePlaybackPosition:)];
}
}
在 iOS 7.1 之后,可以通過 MPRemoteCommandCenter
來控制音頻播放压固。每個控制操作都封裝為一個 MPRemoteCommand
對象球拦,給 MPRemoteCommand
添加響應(yīng)事件有兩種方式:
一種是通過 addTargetWithHandler:
,以 Block
的方式傳入響應(yīng)事件,需要返回 MPRemoteCommandHandlerStatusSuccess
來告知響應(yīng)成功坎炼。
另一種是通過 addTarget: action:
愧膀,因為 MPRemoteCommandCenter
是個單例,所以在 target
的 dealloc
中要記得調(diào)用 removeTarget:
谣光。如下所示:
- (void)dealloc {
[self removeCommandCenterTargets];
}
- (void)removeCommandCenterTargets {
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
[[commandCenter playCommand] removeTarget:self];
[[commandCenter pauseCommand] removeTarget:self];
[[commandCenter nextTrackCommand] removeTarget:self];
[[commandCenter previousTrackCommand] removeTarget:self];
if (@available(iOS 9.1, *)) {
[commandCenter.changePlaybackPositionCommand removeTarget:self];
}
}
注意:因為
changePlaybackPositionCommand
在 iOS 9.1 以后才可用檩淋,所以這里加了系統(tǒng)判斷。
到這里就實現(xiàn)了鎖屏界面的播放控制萄金。
源碼
請到 GitHub 上查看完整例子蟀悦。
參考
iOS AVPlayer之后臺連續(xù)播放視頻
AVPlayer 音樂播放后臺播放,以及鎖屏主題設(shè)置
這可能是最詳細的CMTime教程
獲取更佳的閱讀體驗氧敢,請訪問原文地址 【Lyman's Blog】iOS AVPlayer 實現(xiàn)后臺連續(xù)播放視頻