iOS AVPlayer 实现后台连续播放视频

最近接到一个需求,需要做一个在后台播放视频的功能。折腾了一下,最后总算完成了。因此写一篇文章,介绍下具体的实现步骤,也说说自己遇到的坑,算是总结和记录。

前言

当 App 退到后台时,会进入 suspend 状态,若此时在播放视频,则会自动暂停。我们需要实现的效果是,当 App 退到后台时,视频中的声音还能继续播放。另外,我们还同时实现视频的连续播放功能,和在锁屏界面控制视频播放的功能。具体怎么做,下面听我一一道来。

注意:由于 iOS 模拟器存在 BUG,尤其是 iOS 11 的模拟器,不能在后台播放音频,因此以下功能最好使用真机测试。

一、后台播放音频

要实现后台播放视频功能,首先需要实现后台播放音频功能。实现后台播放音频很简单,只要简单配置一下就可以了。总共有三步:

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];

至此就实现了后台播放音频的功能,但这不是我们的最终目的,请继续往下看。

二、后台播放视频

网上讲实现后台播放视频的资料并不多(可能比较少有这么坑的需求)。我在网上找了一圈,只有 这篇文章 提到了,方法也很简单,分为两步:

1. 退到后台时移除 playerLayer 上的 player

viewController 中添加退到后台监听:

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
           selector:@selector(removePlayerOnPlayerLayer)
               name:UIApplicationDidEnterBackgroundNotification
             object:nil];

移除 player :

- (void)removePlayerOnPlayerLayer {
    
    _playerLayer.player = nil;
}

2. 回到前台时重新添加 player

viewController 中添加回到前台监听:

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
           selector:@selector(resetPlayerToPlayerLayer)
               name:UIApplicationWillEnterForegroundNotification
             object:nil];

重新添加 player :

- (void)resetPlayerToPlayerLayer {
    
    _playerLayer.player = _player;
}

这样简单的后台播放视频就实现了。

对于上面的实现后台播放视频的方法,我的理解是,iOS 是支持后台播放音频的,而 AVPlayer 在播放视频时,会将图像渲染在 layer 上,因此只要取消图像的渲染,只播放音频,就可以实现后台播放。

3. 连续播放视频

后台连续播放视频的逻辑,其实和前台连续播放的逻辑一样。可以通过监听 playerItem 播放结束的通知来切换歌曲,则当播放结束时,需要移除对当前 playerItem 的监听,然后添加下一个 playerItem 的监听。

这里直接通过判断进度条是否完成,来切换歌曲。

// 监听播放进度
__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 的广告。可跳过。

上面监听播放进度的时候,用到了一个叫 CMTime 的东西,这里简单地讲一下我的理解。
一般我们用 CMTime 的时候,都是使用 CMTimeGetSeconds(time) 将它转成秒数。
那为何不直接使用 NSTimeInterval 来表示时间就好了?

原因只有一个 —— 精度

浮点数没有办法进行准确的加减运算,当多次加减后,可能会出现较大误差。因此在视频一般用 CMTime 来表示时间,因为 CMTime 可以规定最小的精度,从而保证累加后时间的准确性。

CMTime 的构造方法 CMTimeMakeWithSeconds(seconds, timescale)seconds 表示秒数, 1 / timescale 表示最小精度。
另一个构造方法 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、设置歌曲名
    [playingInfoDict setObject:[NSString stringWithFormat:@"歌曲%ld", (long)_currentIndex + 1]
                        forKey:MPMediaItemPropertyTitle];
    [playingInfoDict setObject:[NSString stringWithFormat:@"专辑%ld", (long)_currentIndex + 1]
                        forKey:MPMediaItemPropertyAlbumTitle];
    
    
    // 3、设置封面的图片
    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、设置歌曲的时长和已经消耗的时间
    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 不需要频繁调用,锁屏界面的进度条会自己计时,只需要在关键的时刻去同步这个已播放时长。一般需要调用的时刻有,切换歌曲、暂停、播放、拖动进度条等。

这里有个坑。我们知道 player 有个 rate 属性,为 0 的时候表示暂停,为 1.0 的时候表示播放。相应的, nowPlayingInfo 也有个 MPNowPlayingInfoPropertyPlaybackRate 属性。前面说到,「锁屏界面的进度条会自己计时」,它是否在计时就是取决于这个属性。坑的地方在于,这个属性和 playerrate 并不同步。也就是说,单纯地在锁屏界面点暂停后, player 会暂停, rate 也会变成 0 ,但是 MPNowPlayingInfoPropertyPlaybackRate 却不为 0 。导致的结果是,在锁屏界面点击了暂停按钮,这个时候进度条表面看起来停止了走动,但是其实还是在计时,所以再点击播放的时候,锁屏界面进度条的光标会发生位置闪动。

解决方法:在视频暂停和播放的时候,同步视频的已播放时长 _player.currentItem.currentTimeMPNowPlayingInfoPropertyElapsedPlaybackTime 、视频的当前播放速率 _player.rateMPNowPlayingInfoPropertyPlaybackRate

2. 用 MPRemoteCommandCenter 实现播放控制

先上代码:

// 添加远程控制
- (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 添加响应事件有两种方式:

一种是通过 addTargetWithHandler:,以 Block 的方式传入响应事件,需要返回 MPRemoteCommandHandlerStatusSuccess 来告知响应成功。

另一种是通过 addTarget: action:,因为 MPRemoteCommandCenter 是个单例,所以在 targetdealloc 中要记得调用 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 以后才可用,所以这里加了系统判断。

到这里就实现了锁屏界面的播放控制。

源码

请到 GitHub 上查看完整例子。

参考

iOS AVPlayer之后台连续播放视频
AVPlayer 音乐播放后台播放,以及锁屏主题设置
这可能是最详细的CMTime教程

获取更佳的阅读体验,请访问原文地址 【Lyman's Blog】iOS AVPlayer 实现后台连续播放视频

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容