iOS AVPlayer 實現(xiàn)后臺連續(xù)播放視頻

最近接到一個需求,需要做一個在后臺播放視頻的功能攻泼。折騰了一下火架,最后總算完成了。因此寫一篇文章忙菠,介紹下具體的實現(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

AppDelegateapplication: 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 屬性。前面說到玄渗,「鎖屏界面的進度條會自己計時」座菠,它是否在計時就是取決于這個屬性√偈鳎坑的地方在于浴滴,這個屬性和 playerrate 并不同步。也就是說岁钓,單純地在鎖屏界面點暫停后升略, player 會暫停, rate 也會變成 0 屡限,但是 MPNowPlayingInfoPropertyPlaybackRate 卻不為 0 品嚣。導(dǎo)致的結(jié)果是,在鎖屏界面點擊了暫停按鈕钧大,這個時候進度條表面看起來停止了走動翰撑,但是其實還是在計時,所以再點擊播放的時候啊央,鎖屏界面進度條的光標會發(fā)生位置閃動眶诈。

解決方法:在視頻暫停和播放的時候,同步視頻的已播放時長 _player.currentItem.currentTimeMPNowPlayingInfoPropertyElapsedPlaybackTime 劣挫、視頻的當(dāng)前播放速率 _player.rateMPNowPlayingInfoPropertyPlaybackRate 册养。

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 是個單例,所以在 targetdealloc 中要記得調(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ù)播放視頻

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末日戈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子孙乖,更是在濱河造成了極大的恐慌浙炼,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唯袄,死亡現(xiàn)場離奇詭異弯屈,居然都是意外死亡,警方通過查閱死者的電腦和手機恋拷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門资厉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔬顾,你說我怎么就攤上這事酌住。” “怎么了阎抒?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長消痛。 經(jīng)常有香客問我且叁,道長,這世上最難降的妖魔是什么秩伞? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任逞带,我火速辦了婚禮,結(jié)果婚禮上纱新,老公的妹妹穿的比我還像新娘展氓。我一直安慰自己,他們只是感情好脸爱,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布遇汞。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪空入。 梳的紋絲不亂的頭發(fā)上络它,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音歪赢,去河邊找鬼化戳。 笑死,一個胖子當(dāng)著我的面吹牛埋凯,可吹牛的內(nèi)容都是我干的点楼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼白对,長吁一口氣:“原來是場噩夢啊……” “哼掠廓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起躏结,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤却盘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后媳拴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體黄橘,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年屈溉,在試婚紗的時候發(fā)現(xiàn)自己被綠了塞关。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡子巾,死狀恐怖帆赢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情线梗,我是刑警寧澤椰于,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站仪搔,受9級特大地震影響瘾婿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烤咧,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一偏陪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧煮嫌,春花似錦笛谦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恳邀。三九已至,卻和暖如春好啰,著一層夾襖步出監(jiān)牢的瞬間轩娶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工框往, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鳄抒,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓椰弊,卻偏偏與公主長得像许溅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子秉版,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容