《AV Foundation 开发秘籍》读书笔记(四)

第四章 视频播放

1. 主要框架

AVPlayer

AV Foundation 的播放都围绕 AVPlayer 类展开,AVPlayer 是一个用来播放基于时间的视听媒体的控制器对象,支持播放从本地、分布下载或通过 HTTP Live Streaming 协议得到的流媒体。

AVPlayer 是一个不可见组件,如果播放音频文件,没有可视化的用户界面也不会有什么问题,如果播放视频文件,要将视频资源导出到用户界面,需要使用 AVPlayerLayer 类。

AVPlayer 只管理一个单独资源的播放,不过框架还提供了 AVPlayer 的一个子类 AVQueuePlayer,可以用来管理一个资源队列。当你需要在一个序列中播放多个条目或者为音频、视频资源设置播放循环时可使用该子类。

AVPlayerLayer

AVPlayerLayer 构建于 Core Animation 之上,Core Animation 本身具有基于时间的属性,并且由于它基于 OpenGL,所以具有很好的性能,能非常好地满足 AV Foundation 的各种需要。

AVPlayerLayer 使用起来简单,开发者可以自定义的只有 videoGravity 属性,用来确定视频的拉伸或缩放程度。

AVPlayerItem

我们最终的目的是使用 AVPlayer 播放 AVAsset。但是 AVAsset 只包含媒体资源的静态信息,无法实现播放功能,所以,需要通过 AVPlayerItem 和 AVPlayerItemTrack 构建响应的动态内容。

AVPlayerItem 会建立媒体资源动态视角的数据模型,保存 AVPlayer 在播放时呈现的状态。AVPlayerItem 由一个或者多个媒体曲目组成,由 AVPlayerItemTrack 类建立模型。AVPlayerItemTrack 实例用于表示播放器条目中的类型统一的媒体流,比如音频或者视频。AVPlayerItem 中的曲目直接与基础 AVAsset 中的 AVAssetTrack 实例相对应。

2. 视频播放

- (void)viewDidLoad {
    [super viewDidLoad];
   
    // AVPlayerItem:提供数据
    NSURL *assetURL = [[NSBundle mainBundle] URLForResource:@"一骑当千01" withExtension:@"mp4"];
    AVAsset *asset = [AVAsset assetWithURL:assetURL];
    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
    [playerItem addObserver:self forKeyPath:@"status" options:0 context:nil];
    
    // AVPlayer:控制播放
    _player = [AVPlayer playerWithPlayerItem:playerItem];
    
    // AVPlayerLayer:显示播放
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
    playerLayer.frame = self.view.bounds;
    [self.view.layer addSublayer:playerLayer];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        
        AVPlayerItem *playerItem = (AVPlayerItem *)object;
        if (playerItem.status == AVPlayerItemStatusReadyToPlay) {            
            [self.player play];
        }
    }
}

3. 时间处理

AVAudioPlayer 使用 NSTimeInterval 表示时间,但是 double 类型的运算会导致不精确的情况。此外,double 类型呈现时间信息无法做到自我描述,这就导致在使用不同时间轴进行比较和运算时比较困难。所以 AV Foundation 使用一种可靠性更高的方法来展示时间信息,这就是 CMTime

typedef struct {
    CMTimeValue value;      
    CMTimeScale timescale;  
    CMTimeFlags flags;      
    CMTimeEpoch epoch;  
} CMTime;

CMTime 是一种结构体,最关键的两个值为 value 和 timescale,value 是一个 64 位整数值,timescale 是一个 32 位整数值,分别为分子和分母。

// 0.5 s
CMTime halfSecond = CMTimeMake(1, 2);

// 5 s
CMTime fiveSeconds = CMTimeMake(5, 1);

// 44.1 kHz
CMTime oneSample = CMTimeMake(1, 44100);

// Zero time value
CMTime zeroTime = kCMTimeZero;

4. 显示字幕

显示字幕需要用到两个类:AVMediaSelectionGroup 和 AVMediaSelectionOption,AVMediaSelectionOption 表示 AVAsset 中的备用媒体呈现方式,比如音频、视频、文本轨道。这些轨道可能是指定语言的音频轨道、备用相机角度、指定语言字幕。

NSArray *mediaCharacteristics = self.asset.availableMediaCharacteristicsWithMediaSelectionOptions;

该数组中可能包含的字符串值为:

  • AVMediaCharacteristicVisual(视频)
  • AVMediaCharacteristicAudible(音频)
  • AVMediaCharacteristicLegible(字幕或隐藏式字幕)

获取所有可选择字幕语言数组

- (NSArray *)getSubtitles {

    AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
    if (group) {
        NSMutableArray *subtitles = [NSMutableArray array];
        for (AVMediaSelectionOption *option in group.options) {
            [subtitles addObject:option.displayName];  // 语言字幕轨道名称,比如 English、Italian、Russian
        }
        return subtitles;
    } else {
        return nil;
    }
}

设置字幕

- (void)subtitleSelected:(NSString *)subtitle {
    
    AVMediaSelectionGroup *group =
        [self.asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
    
    for (AVMediaSelectionOption *option in group.options) {
        if ([option.displayName isEqualToString:subtitle]) {
            [self.playerItem selectMediaOption:option inMediaSelectionGroup:group];
            return;
        }
    }
    [self.playerItem selectMediaOption:nil inMediaSelectionGroup:group];
}

5. 缩略图

AVAssetImageGenerator 定义了两个方法实现从视频资源中检索图片:

  1. copyCGImageAtTime:actualTime:error: 允许在指定时间点捕捉图片,适合在展示视频缩略图。

  2. generateCGImagesAsynchronouslyForTimes:completionHandler:
    允许按照第一个参数所指定的时间段生成一个图片序列,该方法具有很高的性能,只需要调用这一个方法就可以生成一组图片

下面方法为第一种 copyCGImageAtTime:actualTime:error: 方法:

- (void)generateThumbnails {
    
    self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:self.asset];
    
    // 默认情况下,捕捉的图片都保持原始维度。设置 maximumSize 的宽度,则会根据视频的宽高比自动设置高度值
    self.imageGenerator.maximumSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 0.0f);
    
    CMTime duration = self.asset.duration;
    
    // 将视频时间轴平均分为 20 个 CMTime 值
    NSMutableArray *times = [NSMutableArray array];
    CMTimeValue increment = duration.value / 20;
    CMTimeValue currentValue = 2.0 * duration.timescale;
    while (currentValue <= duration.value) {
        CMTime time = CMTimeMake(currentValue, duration.timescale);
        [times addObject:[NSValue valueWithCMTime:time]];
        currentValue += increment;
    }
    
    __block NSUInteger imageCount = times.count;
    __block NSMutableArray *images = [NSMutableArray array];
    
    AVAssetImageGeneratorCompletionHandler handler;
    
    // requestedTime : 请求的最初时间,它对应于生成图像的调用中指定的 times 数组中的值
    // imageRef      : 生成的 CGImageRef,如果在给定的时间点没有生成图片则为 NULL
    // actualTime    : 图片实际生成的时间,可能和请求时间不同。可以在生成图片前通过 AVAssetImageGenerator 实例设置 requestedTimeToleranceBefore 和 requestedTimeToleranceAfter 值来调整 requestedTime 和 actualTime 的接近程度
    // result        : AVAssetImageGeneratorResult 用来表示生成图片成功、失败、取消
    handler = ^(CMTime requestedTime,
                CGImageRef imageRef,
                CMTime actualTime,
                AVAssetImageGeneratorResult result,
                NSError *error) {
        
        if (result == AVAssetImageGeneratorSucceeded) {
            MYThumbnail *thumbnail =
                [MYThumbnail thumbnailWithImage:[UIImage imageWithCGImage:imageRef] time:actualTime];
            [images addObject:thumbnail];
        } else {
            NSAssert(error.localizedDescription, error.localizedDescription);
        }
        
        // 如果 imageCount 等于 0 这表明所有图片都处理完成
        if (--imageCount == 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.playerView.transport setStillsImages:images];
            });
        }
    };
    [self.imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:handler];
}

6. 封装框架

有很多知识点通过笔记是无法表达清楚的,具体实现还需看代码理解的透彻。根据 Learning-AV-Foundation Chapter 4 自行封装了一个视频播放、设置字幕、获取视频缩略图的框架,下载地址:https://github.com/Mayan29/MYAVFoundation

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

推荐阅读更多精彩内容