本文转自:AVAudioFoundation(2):音视频播放 | www.samirchen.com

本文主要内容来自 AVFoundation Programming Guide

要播放 AVAsset 可以使用 AVPlayer。在播放期间,可以使用一个 AVPlayerItem 实例来管理 asset 的整体的播放状态,使用 AVPlayerItemTrack 来管理各个 track 的播放状态。对于视频的渲染,使用 AVPlayerLayer 来处理。

播放 Asset

AVPlayer 是一个控制 asset 播放的控制器,它的功能包括:开始播放、停止播放、seek 等等。你可以使用 AVPlayer 来播放单个 asset。如果你想播放一组 asset,你可以使用 AVQueuePlayerAVQueuePlayerAVPlayer 的子类。

AVPlayer 也会提供当前的播放状态,这样我们就可以根据当前的播放状态调整交互。我们需要将 AVPlayer 的画面输出到一个特定的 Core Animation Layer 上,通常是一个 AVPlayerLayerAVSynchronizedLayer 实例。

需要注意的是,你可以从一个 AVPlayer 实例创建多个 AVPlayerLayer 对象,但是只有最新创建的那个才会渲染画面到屏幕。

对于 AVPlayer 来说,虽然最终播放的是 asset,但是我们并不直接提供一个 AVAsset 给它,而是提供一个 AVPlayerItem 实例。AVPlayerItem 是用来管理与之关联的 asset 的播放状态的,一个 AVPlayerItem 包含了一组 AVPlayerItemTrack 实例,对应着 asset 中的音视频轨道。它们直接的关系大致如下图所示:

注意:该图的原图是苹果官方文档上的,但是原图是有错的,把 AVPlayerItemTrack 所属的框标成了 AVAsset,这里做了修正。

这种实现方式就意味着,我们可以用多个播放器同时播放一个 asset,并且各个播放器可以使用不同的模式来渲染。下图就展示了一种用两个不同的 AVPlayer 采用不同的设置播放同一个 AVAsset 的场景。在播放中,还可以禁掉某些 track 的播放。

我们可以通过网络来加载 asset,通常简单的初始化 AVPlayerItem 后并不意味着它就直接能播放,所以我们可以 KVO AVPlayerItemstatus 属性来监听它是否已经可播再决定后续的行为。

处理不同类型的 Asset

我们配置 asset 来播放的方式多多少少会依赖 asset 的类型,一般我们有两种不同类型的 asset:

  • 1)基于文件的 asset,一般可以来源于本地视频文件、相册资源库等等。
  • 2)流式 asset,比如 HLS 格式的视频。

加载基于文件的 asset 一般分为如下几步:

  • 基于文件路径的 URL 创建 AVURLAsset 实例。
  • 基于 AVURLAsset 实例创建 AVPlayerItem 实例。
  • AVPlayerItem 实例与一个 AVPlayer 实例关联。
  • KVO 监测 AVPlayerItemstatus 属性来等待其已经可播,即加载完成。

创建并加载一个 HTTP Live Stream(HLS)格式的资源来播放时,可以按照下面几步来做:

  • 基于资源的 URL 初始化一个 AVPlayerItem 实例,因为你无法直接创建一个 AVAsset 来表示 HLS 资源。
  • 当你将 AVPlayerItemAVPlayer 实例关联起来后,他就开始为播放做准备,当一切就绪时 AVPlayerItem 会创建出 AVAssetAVAssetTrack 实例以用来对接 HLS 视频流的音视频内容。
  • 要获取视频流的时长,你需要 KVO 监测 AVPlayerItemduration 属性,当资源可以播放时,它会被更新为正确的值。
  1. NSURL *url = [NSURL URLWithString:@"<#Live stream URL#>];
  2. // You may find a test stream at <http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8>.
  3. self.playerItem = [AVPlayerItem playerItemWithURL:url];
  4. [playerItem addObserver:self forKeyPath:@"status" options:0 context:&ItemStatusContext];
  5. self.player = [AVPlayer playerWithPlayerItem:playerItem];

当你不知道一个 URL 对应的是什么类型的 asset 时,你可以这样做:

  • 尝试基于 URL 来初始化一个 AVURLAsset,并加载它的 tracks 属性。如果 tracks 属性加载成功,就基于 asset 来创建一个 AVPlayerItem 实例。
  • 如果 tracks 属性加载失败,那么就直接基于 URL 创建一个 AVPlayerItem 实例,并 KVO 监测 AVPlayerstatus 属性来看它何时可以播放。
  • 如果上述尝试都失败,那就清理掉 AVPlayerItem

播放一个 AVPlayerItem

调用 AVPlayerplay 接口即可开始播放。

  1. - (IBAction)play:sender {
  2. [player play];
  3. }

除了简单的播放,还可以通过设置 rate 属性设置播放速率。

  1. player.rate = 0.5;
  2. player.rate = 2.0;

播放速率设置为 1.0 表示正常播放,设置为 0.0 表示暂停(等同调用 pause 效果)。

除了正向播放,有的音视频还能支持倒播,不过需要需要检查几个属性:

  • canPlayReverse:支持设置播放速率为 -1.0。
  • canPlaySlowReverse:支持设置播放速率为 -1.0 到 0.0。
  • canPlayFastReverse:支持设置播放速率为小于 -1.0 的值。

可以通过 seekToTime: 接口来调整播放位置。但是这个接口主要是为性能考虑,不保证精确。

  1. CMTime fiveSecondsIn = CMTimeMake(5, 1);
  2. [player seekToTime:fiveSecondsIn];

如果要精确调整,可以用 seekToTime:toleranceBefore:toleranceAfter: 接口。

  1. CMTime fiveSecondsIn = CMTimeMake(5, 1);
  2. [player seekToTime:fiveSecondsIn toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];

需要注意的是,设置 tolerance 为 zero 会耗费较大的计算性能,所以一般只在编写复杂的音视频编辑功能是这样设置。

我们可以通过监听 AVPlayerItemDidPlayToEndTimeNotification 来获得播放结束事件,在播放结束后可以用 seekToTime: 调整播放位置到 zero,否则调用 play 会无效。

  1. // Register with the notification center after creating the player item.
  2. [[NSNotificationCenter defaultCenter]
  3. addObserver:self
  4. selector:@selector(playerItemDidReachEnd:)
  5. name:AVPlayerItemDidPlayToEndTimeNotification
  6. object:<#The player item#>];
  7. - (void)playerItemDidReachEnd:(NSNotification *)notification {
  8. [player seekToTime:kCMTimeZero];
  9. }

此外,我们还能设置播放器的 actionAtItemEnd 属性来设置其在播放结束后的行为,比如 AVPlayerActionAtItemEndPause 表示播放结束后会暂停。

播放多个 AVPlayerItem

我们可以用 AVQueuePlayer 来顺序播放多个 AVPlayerItemAVQueuePlayerAVPlayer 的子类。

  1. NSArray *items = <#An array of player items#>;
  2. AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];

通过调用 play 即可顺序播放,也可以调用 advanceToNextItem 跳到下个 item。除此之外,我们还可以用 insertItem:afterItem:removeItem:removeAllItems 来控制播放资源。

当插入一个 item 的时候,可以需要用 canInsertItem:afterItem: 检查下是否可以插入, 对 afterItem 传入 nil,则检查是否可以插入到队尾。

  1. AVPlayerItem *anItem = <#Get a player item#>;
  2. if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
  3. [queuePlayer insertItem:anItem afterItem:nil];
  4. }

监测播放状态

我们可以监测一些 AVPlayer 的状态和正在播放的 AVPlayerItem 的状态,这对于处理那些不在你直接控制下的 state 是很有用的,比如:

  • 如果用户使用多任务处理切换到另一个应用程序,播放器的 rate 属性将下降到 0.0。
  • 当播放远程媒体资源(比如网络视频)时,监测 AVPlayerItemloadedTimeRangesseekableTimeRanges 可以知道可以播放和 seek 的资源时长。
  • 当播放 HTTP Live Stream 时,播放器的 currentItem 可能发生变化。
  • 当播放 HTTP Live Stream 时,AVPlayerItemtracks 可能发生变化。这种情况可能发生在播放流切换了编码。
  • 当播放失败时,AVPlayerAVPlayerItemstatus 可能发生变化。

响应 status 属性的变化

通过 KVO 监测 AVPlayer 和正在播放的 AVPlayerItemstatus 属性,可以获得对应的通知,比如当播放出现错误时,你可能会收到 AVPlayerStatusFailedAVPlayerItemStatusFailed 通知,这时你就可以做相应的处理。

需要注意的是,由于 AVFoundation 不会指定在哪个线程发送通知,所以如果你需要在收到通知后更新用户界面的话,你需要切到主线程。

  1. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  2. if (context == <#Player status context#>) {
  3. AVPlayer *thePlayer = (AVPlayer *) object;
  4. if ([thePlayer status] == AVPlayerStatusFailed) {
  5. NSError *error = [<#The AVPlayer object#> error];
  6. // Respond to error: for example, display an alert sheet.
  7. return;
  8. }
  9. // Deal with other status change if appropriate.
  10. }
  11. // Deal with other change notifications if appropriate.
  12. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  13. return;
  14. }

跟踪视觉内容就绪状态

我们可以监测 AVPlayerLayer 实例的 readyForDisplay 属性来获得播放器已经可以开始渲染视觉内容的通知。

基于这个能力,我们就能实现在播放器的视觉内容就绪时才将 player layer 插入到 layer 树中去展示给用户。

追踪播放时间变化

我们可以使用 AVPlayeraddPeriodicTimeObserverForInterval:queue:usingBlock:addBoundaryTimeObserverForTimes:queue:usingBlock: 这两个接口来追踪当前播放位置的变化,这样我们就可以在用户界面上做出更新,反馈给用户当前的播放时间和剩余的播放时间等等。

  • addPeriodicTimeObserverForInterval:queue:usingBlock:,这个接口将会在播放时间发生变化时在回调 block 中通知我们当前播放时间。
  • addBoundaryTimeObserverForTimes:queue:usingBlock:,这个接口允许我们传入一组时间(CMTime 数组)当播放器播到这些时间时会在回调 block 中通知我们。

这两个接口都会返回一个 observer 角色的对象给我们,我们需要在监测时间的这个过程中强引用这个对象,同时在不需要使用它时调用 removeTimeObserver: 接口来移除它。

此外,AVFoundation 也不保证在每次时间变化或设置时间到达时都回调 block 来通知你。比如当上一次回调 block 还没完成的情况时,又到了此次回调 block 的时机,AVFoundation 这次就不会调用 block。所以我们需要确保不要在 block 回调里做开销太大、耗时太长的任务。

  1. // Assume a property: @property (strong) id playerObserver;
  2. Float64 durationSeconds = CMTimeGetSeconds([<#An asset#> duration]);
  3. CMTime firstThird = CMTimeMakeWithSeconds(durationSeconds/3.0, 1);
  4. CMTime secondThird = CMTimeMakeWithSeconds(durationSeconds*2.0/3.0, 1);
  5. NSArray *times = @[[NSValue valueWithCMTime:firstThird], [NSValue valueWithCMTime:secondThird]];
  6. self.playerObserver = [<#A player#> addBoundaryTimeObserverForTimes:times queue:NULL usingBlock:^{
  7. NSString *timeDescription = (NSString *)
  8. CFBridgingRelease(CMTimeCopyDescription(NULL, [self.player currentTime]));
  9. NSLog(@"Passed a boundary at %@", timeDescription);
  10. }];

播放结束

监听 AVPlayerItemDidPlayToEndTimeNotification 这个通知即可。上文有提到,这里不再重复。

一个完整示例

这里的示例将展示如果使用 AVPlayer 来播放一个视频文件,主要包括下面几个步骤:

  • 配置一个使用 AVPlayerLayer layer 的 UIView
  • 创建一个 AVPlayer 实例。
  • 基于文件类型的 asset 创建一个 AVPlayerItem 实例,并用 KVO 监测其 status 属性。
  • 响应 AVPlayerItem 实例可以播放的通知,显示出一个按钮。
  • 播放 AVPlayerItem 并播放完成后将其播放位置调整到开始位置。

首先是 PlayerView:

  1. #import <UIKit/UIKit.h>
  2. #import <AVFoundation/AVFoundation.h>
  3. @interface PlayerView : UIView
  4. @property (nonatomic) AVPlayer *player;
  5. @end
  6. @implementation PlayerView
  7. + (Class)layerClass {
  8. return [AVPlayerLayer class];
  9. }
  10. - (AVPlayer*)player {
  11. return [(AVPlayerLayer *)[self layer] player];
  12. }
  13. - (void)setPlayer:(AVPlayer *)player {
  14. [(AVPlayerLayer *)[self layer] setPlayer:player];
  15. }
  16. @end

一个简单的 PlayerViewController:

  1. @class PlayerView;
  2. @interface PlayerViewController : UIViewController
  3. @property (nonatomic) AVPlayer *player;
  4. @property (nonatomic) AVPlayerItem *playerItem;
  5. @property (nonatomic, weak) IBOutlet PlayerView *playerView;
  6. @property (nonatomic, weak) IBOutlet UIButton *playButton;
  7. - (IBAction)loadAssetFromFile:sender;
  8. - (IBAction)play:sender;
  9. - (void)syncUI;
  10. @end

同步 UI 的方法:

  1. - (void)syncUI {
  2. if ((self.player.currentItem != nil) &&
  3. ([self.player.currentItem status] == AVPlayerItemStatusReadyToPlay)) {
  4. self.playButton.enabled = YES;
  5. }
  6. else {
  7. self.playButton.enabled = NO;
  8. }
  9. }

viewDidLoad 时先调用一下 syncUI

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. [self syncUI];
  4. }

创建并加载 AVURLAsset,在加载成功时,创建 item、初始化播放器以及添加各种监听:

  1. static const NSString *ItemStatusContext;
  2. - (IBAction)loadAssetFromFile:sender {
  3. NSURL *fileURL = [[NSBundle mainBundle] URLForResource:<#@"VideoFileName"#> withExtension:<#@"extension"#>];
  4. AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
  5. NSString *tracksKey = @"tracks";
  6. [asset loadValuesAsynchronouslyForKeys:@[tracksKey] completionHandler: ^{
  7. // The completion block goes here.
  8. dispatch_async(dispatch_get_main_queue(), ^{
  9. NSError *error;
  10. AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];
  11. if (status == AVKeyValueStatusLoaded) {
  12. self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
  13. // ensure that this is done before the playerItem is associated with the player
  14. [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionInitial context:&ItemStatusContext];
  15. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
  16. self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
  17. [self.playerView setPlayer:self.player];
  18. } else {
  19. // You should deal with the error appropriately.
  20. NSLog(@"The asset's tracks were not loaded:\n%@", [error localizedDescription]);
  21. }
  22. });
  23. }];
  24. }

响应 status 的监听通知:

  1. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  2. if (context == &ItemStatusContext) {
  3. dispatch_async(dispatch_get_main_queue(), ^{
  4. [self syncUI];
  5. });
  6. return;
  7. }
  8. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  9. return;
  10. }

播放,以及播放完成时的处理:

  1. - (IBAction)play:sender {
  2. [self.player play];
  3. }
  4. - (void)playerItemDidReachEnd:(NSNotification *)notification {
  5. [self.player seekToTime:kCMTimeZero];
  6. }

AVAudioFoundation(2):音视频播放的更多相关文章

  1. Android音视频之MediaPlayer音视频播放

    前言: 昨天总结了视频录制,今天来学习一下视频的播放,Android的视频播放主要采用MediaPlayer类. MediaPlayer介绍 MediaPlayer类可用于控制音频/视频文件或流的播放 ...

  2. Pyqt 音视频播放器

    在寻找如何使用Pyqt做一个播放器时首先找到的是openCV2 openCV2 貌似太强大了,各种关于图像处理的事情它都能完成,如 读取摄像头.图像识别.人脸识别.  图像灰度处理 . 播放视频等,强 ...

  3. iOS AVKit音视频播放全面详解

    公司项目中经常要用到音视频处理,也需要去定制一些东西,然后整理这些音视频处理就显得尤为重要!方便自己和广大朋友学习收藏! 以下参考连接特别重要: 苹果官方:AVKit API 苹果官方:AVFound ...

  4. iOS - AVPlayer 音视频播放

    前言 NS_CLASS_AVAILABLE(10_7, 4_0) @interface AVPlayer : NSObject @available(iOS 4.0, *) public class ...

  5. 分享几个不错的Android开源音视频播放器

    整理了一下Github上几个开源的音视频播放器项目,有兴趣的同学可以clone代码去研究学习.   UniversalMusicPlayer https://github.com/googlesamp ...

  6. 6、Qt Project之音视频播放

    音视频播放  这里简单的制作了一个音乐播放器,播放器的界面设计如下所示: Step1:这里是界面对应的HTML文件: <?xml version="1.0" encoding ...

  7. FFmpeg简易播放器的实现-音视频播放

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10235926.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  8. 一些不错的Android开源音视频播放器

    摘要:来自Github上的一点点整理,希望对你有用! 整理了一下Github上几个开源的音视频播放器项目,有兴趣的同学可以clone代码去研究学习. 1.UniversalMusicPlayer ht ...

  9. 开源安卓Android流媒体音视频播放器实现声音自动停止、恢复、一键静音功能源码

    本文转自EasyDarwin团队John的博客:http://blog.csdn.net/jyt0551/article/details/60802145 我们在开发安卓Android流媒体音视频播放 ...

随机推荐

  1. 【跑会指南】2017年3-5月IT技术会议大合集

    2016年各类大会让人应接不暇,技术圈儿最不缺的就是各种大会小会,有的纯干货,有的纯广告.作为一名技术开发者,参加了几场大会,你是不是也开始思忖:究竟哪些会议才值得参加?下面活动家为你推荐几场2017 ...

  2. iOS 制作自动打包脚本 Xcode8.3.2

    本文包含以下内容: 前言 1.shell脚本的编写 2.xcodebuild命令 3.完整的可用示例 参考资料 前言 做iOS开发,打包APP是比较频繁的事情,每次都手动去配置一堆东西确实是比较乏味. ...

  3. 数值类型中JDk的编译期检查和编译期优化

    byte b1 = 5;//编译期检查,判断是否在byte范围内 byte b2 = 5+4;//编译期优化,相当于b2=9 byte b3 = 127;//编译通过,在byte范围内 byte b4 ...

  4. 腾讯IVWEB团队:前端 fetch 通信

    欢迎大家关注腾讯云技术社区-博客园官方主页,我们将持续在博客园为大家推荐技术精品文章哦~ 作者:villainthr 文章摘自: 前端小吉米 随着前端异步的发展, XHR 这种耦合方式的书写不利于前端 ...

  5. [linux 整理] linux启动过程3

    本文介绍linux启动过程的第三步 busybox--------------------> rc init busybox位置即内容 busybox/init/init.c 1.各种设置信号 ...

  6. vim 编辑中执行正则表达式

    1.进入vim 编辑模式 2.输入:set magic 3.输入/,然后再次输入正则表达式

  7. kotlin, 一种新的android平台一级开发语言

    最近看到一则科技新闻, 大致内容是google将kotlin语言作为android应用开发的一级语言, 与java并驾齐驱, 这是一个开发界的大事件大新闻, 连google的亲儿子go语言也没有这种待 ...

  8. 详解CockroachDB事务处理系统

    本文提到的一些术语,比如Serializability和Linearizability,解释看Linearizability, Serializability and Strict Serializa ...

  9. Day1-三元运算及

    三元运算:result = Value1 if Condition else Vlaue2 >>> a,b,c = 1,3,5>>> d = a if a > ...

  10. let与const详解

    在ES6中,js首次引入了块级作用域的概念,而什么是块级作用域? 众所就知,在js当中存在预解析的概念,就是变量提升.并且只存在全局作用域和私有作用域.在全局定义的变量就是全局变量,而在函数内部定义的 ...