AVFoundation 視頻播放

1. 播放視頻綜述

AVFoundation 對於播放封裝了主要的三個類 AVPlay、AVPlayerLayer 和 AVPlayerItem。數組

  • AVPlayer

AVPlayer 是一個用於播放基於時間的試聽媒體的控制器對象,能夠播放本地、分佈下載以及 HTTP Live Streaming 協議獲得的流媒體。服務器

HTTP Live Streaming(縮寫是HLS)是一個由蘋果公司提出的基於HTTP的流媒體網絡傳輸協議。是蘋果公司QuickTime X和iPhone軟件系統的一部分。它的工做原理是把整個流分紅一個個小的基於HTTP的文件來下載,每次只下載一些。當媒體流正在播放時,客戶端能夠選擇從許多不一樣的備用源中以不一樣的速率下載一樣的資源,容許流媒體會話適應不一樣的數據速率。在開始一個流媒體會話時,客戶端會下載一個包含元數據的extended M3U (m3u8)playlist文件,用於尋找可用的媒體流。網絡

HLS只請求基本的HTTP報文,與實時傳輸協議(RTP)不一樣,HLS能夠穿過任何容許HTTP數據經過的防火牆或者代理服務器。它也很容易使用內容分發網絡來傳輸媒體流。數據結構

蘋果公司把HLS協議做爲一個互聯網草案(逐步提交),在第一階段中已做爲一個非正式的標準提交到IETF。可是,即便蘋果偶爾地提交一些小的更新,IETF卻沒有關於制定此標準的有關進一步的動做。async

AVPlayer 只管理一個單獨資源的播放,其子類 AVQueuePlayer 能夠管理資源隊列。ide

  • AVPlayerLayer

AVPlayerLayer 構建於 Core Animation 之上,擴展了 Core Animation 的 CALayer 類,不提供除內容渲染面之外的任何可視化控件,支持的自定義屬性只有 video gravity,能夠選擇 AVLayerVideoGravityResizeAspect、AVLayerVideoGravityResizeAspectFill、AVLayerVideoGravityResize 三個值,分別是等比例徹底展現,等比例徹底鋪滿,和不等比例徹底鋪滿。性能

  • AVPlayerItem

AVAsset 只包含媒體資源的靜態信息,AVPlayerItem 能夠創建媒體資源動態視角的數據模型,並保存 AVPlayer 播放狀態。ui

2. 播放視頻

從一個待播放的 AVAsset 開始,須要作如下初始化操做網絡傳輸協議

self.avPlayerItem = [AVPlayerItem playerItemWithAsset:self.targetAVAsset];
        self.avPlayer = [AVPlayer playerWithPlayerItem:self.avPlayerItem];
        self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer];
        [self.layer addSublayer:self.avPlayerLayer];
複製代碼

依次建立 AVPlayerItem、AVPlayer 和 AVPlayerLayer 三個對象,最終將 AVPlaterLayer 加入到待展現內容的 view 上。可是此時不能當即播放,AVPlayerItem 有一個 status 狀態,用於標識當前視頻是否準備好被播放,須要監聽這一個屬性。spa

[RACObserve(self.avPlayerItem, status) subscribeNext:^(id x) {
            @strongify(self);
            if (self.avPlayerItem.status == AVPlayerItemStatusReadyToPlay) {
                // 視頻準備就緒
                if (self.autoPlayMode) {
                    self.playerButton.hidden = YES;
                    [self beginPlay];
                } else {
                    self.playerButton.enabled = YES;
                    self.playerButton.hidden = NO;
                }
            }else if (self.avPlayerItem.status == AVPlayerItemStatusFailed){
                NSLog(@"failed");
            }
        }];
複製代碼

3. 處理時間

使用浮點型數據類型來表示時間在視頻播放時會因爲數據不精確、多時間計算累加致使時間明顯偏移,是的數據流沒法同步,且不能作到自我描述,在不一樣的時間軸進行比較和運算時比較困難。因此 AVFoundation 使用 CMTime 數據結構來表示時間。

typedef struct
{
	CMTimeValue	value;
	CMTimeScale	timescale;
	CMTimeFlags	flags;
	CMTimeEpoch	epoch;		/* CMTime 結構體的紀元數量一般設置爲 0,可是你能夠用它來區分不相關的時間軸。例如,紀元能夠經過使用演示循環每一個週期遞增,區分循環0中的時間 N與循環1中的時間 N。*/
} CMTime;
複製代碼

CMTime 對時間的描述就是 time = value/timescale。

4. 實踐

4.1 建立視頻視圖

UIView 寄宿在 CALayer 實例之上,能夠繼承 UIView 覆寫其類方法 + (Class)layerClass 返回特定類型的 CALayer,這樣 UIView 在初始化時就會選擇此類型來建立宿主 Layer。

+ (Class)layerClass {
    return [AVPlayerLayer class];
}
複製代碼

接下來在自定義初始化方法裏直接傳入一個 AVPlayer 對象就能夠對 UIView 的根 layer 設置 AVPlayer 屬性了。

- (id)initWithPlayer:(AVPlayer *)player {
    self = [super initWithFrame:CGRectZero];
    if (self) {
        self.backgroundColor = [UIColor blackColor];
        [(AVPlayerLayer *) [self layer] setPlayer:player];
    }
    return self;
}
複製代碼

能夠在加載 AVPlayerItem 時選擇一些元數據 key 值進行加載,形式以下

NSArray *keys = @[
        @"tracks",
        @"duration",
        @"commonMetadata",
        @"availableMediaCharacteristicsWithMediaSelectionOptions"
    ];
    self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset
                           automaticallyLoadedAssetKeys:keys];
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
    self.playerView = [[THPlayerView alloc] initWithPlayer:self.player];
複製代碼

這樣能夠在加載 AVPlayerItem 時同時加載音軌、時長、common 元數據和備用。

4.2 監聽狀態

初始化 AVPlayerItem 以後須要等待其狀態變爲 AVPlayerItemStatusReadyToPlay,所以須要進行監聽

[RACObserve(self.avPlayerItem, status) subscribeNext:^(id x) {
            @strongify(self);
            if (self.avPlayerItem.status == AVPlayerItemStatusReadyToPlay) {
                // 視頻準備就緒
                CMTime duration = self.playerItem.duration;
                [self.player play];
            } else if (self.avPlayerItem.status == AVPlayerItemStatusFailed){
                // 視頻沒法播放
            }
        }];
複製代碼

4.3 監聽時間

對於播放時間的監聽,AVPlayer 提供了兩個方法

  • 按期監聽
self.intervalObserver =  [self.avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 2) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            NSLog(@"%f", CMTimeGetSeconds(time));
        }];
複製代碼

這個方法以必定時間間隔,發送消息到指定隊列,這裏要求隊列必須是串行隊列,回調 block 的參數是一個用 CMTime 表示的播放器的當前時間。

  • 邊界時間監聽
self.intervalObserver = [self.avPlayer addBoundaryTimeObserverForTimes:@[[NSValue valueWithCMTime:CMTimeMake(1, 2)], [NSValue valueWithCMTime:CMTimeMake(2, 2)]] queue:dispatch_get_main_queue() usingBlock:^{
            NSLog(@"..");
        }];
複製代碼

這個方法接受一個 CMTime 組成的數組,當到達數組包含的邊界點時觸發回調 block,但 block 不提供當前的 CMTime 值。

同時要注意對監聽的釋放

if (self.intervalObserver){
            [self.avPlayer removeTimeObserver:self.intervalObserver];
        }
複製代碼

4.4 監聽播放結束

視頻播放結束時會發出 AVPlayerItemDidPlayToEndTimeNotification 通知,能夠註冊此通知來獲知視頻已經播放結束

[[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:self.avPlayerItem queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"did play to end");
    }];
複製代碼

還有一種辦法是監聽 AVPlayer 的速度 rate,當速度降爲 0 時,判斷當前時間與總時長的關係

@weakify(self);
        [RACObserve(self.avPlayer, rate) subscribeNext:^(id x) {
            @strongify(self);
            float currentTime = CMTimeGetSeconds(self.avPlayerItem.currentTime);
            float durationTime = CMTimeGetSeconds(self.avPlayerItem.duration);
            if (self.avPlayer.rate == 0 && currentTime >= durationTime) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self endPlayer];
                });
            }
        }];
複製代碼

4.5 控制播放進度

咱們用一個 UISlider 來控制視頻播放,UISlider 有三個事件能夠加入 selector,分別是

  • 點按開始 UIControlEventTouchDown
  • 滑動中 UIControlEventValueChanged
  • 點按結束 UIControlEventTouchUpInside
_scrubberSlider = [[UISlider alloc] init];
        [_scrubberSlider addTarget:self action:@selector(sliderValueChange) forControlEvents:UIControlEventValueChanged];
        [_scrubberSlider addTarget:self action:@selector(sliderStop) forControlEvents:UIControlEventTouchUpInside];
        [_scrubberSlider addTarget:self action:@selector(sliderBegin) forControlEvents:UIControlEventTouchDown];
複製代碼

同時獲取到視頻大小後能夠設置 slider 的 value 屬性

self.scrubberSlider.minimumValue = 0.0;
        self.scrubberSlider.maximumValue = CMTimeGetSeconds(self.avPlayerItem.duration);
複製代碼

接下來是三個 selector 的實現

- (void)sliderBegin
{
    [self pausePlayer];
}

- (void)sliderValueChange
{
    [self.avPlayerItem cancelPendingSeeks];
    [self.avPlayerItem seekToTime:CMTimeMakeWithSeconds(self.scrubberSlider.value, NSEC_PER_SEC)];
}

- (void)sliderStop
{
    [self beginPlay];
}
複製代碼

其中當滑動開始時要暫時中止視頻播放,滑動過程當中出於性能考慮,調用 cancelPendingSeeks 方法,它能取消以前全部的 seekTime 操做,而後再根據 slider 的 value 值去進行 seekToTime 操做,最後滑動結束後恢復播放。

4.6 獲取圖片序列

AVAssetImageGenerator 能夠用來生成一個視頻的固定時間點的圖片序列集合,其具體使用以下。

首先初始化一個 AVAssetImageGenerator 對象

self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:targetAVAsset];
        self.imageGenerator.maximumSize = CGSizeMake(400.0f, 0.0f);
        [self.imageGenerator setRequestedTimeToleranceBefore:kCMTimeZero];
        [self.imageGenerator setRequestedTimeToleranceAfter:kCMTimeZero];
複製代碼

setRequestedTimeToleranceBefore 和 setRequestedTimeToleranceAfter 方法能夠設置獲取的幀時值偏移程度,越精確對性能要求越高。

而後生成一串時值數組

CMTime duration = self.targetAVAsset.duration;
        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];
複製代碼

最後調用方法生成圖片

[self.imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef  _Nullable imageref, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
            if (result == AVAssetImageGeneratorSucceeded) {
                UIImage *image = [UIImage imageWithCGImage:imageref];
                [images addObject:image];
            } else {
                NSLog(@"Error: %@", [error localizedDescription]);
            }
            
            if (--imageCount == 0) {
                
            }
        }];
複製代碼

4.7 顯示字幕

AVMediaSelectionOption 用於標識 AVAsset 的備用媒體呈現方式,包含備用音頻、視頻或文本軌道,這些軌道多是特定語言的音頻軌道、備用相機角度或字幕。

首先經過 AVAsset 的 availableMediaCharacteristicsWithMediaSelectionOptions 屬性來獲取當前視頻的全部備用軌道,返回的字符串多是 AVMediaCharacteristicVisual(備用視頻軌道)、AVMediaCharacteristicAudible(備用音頻軌道)、AVMediaCharacteristicLegible(字幕)等。

獲取到此數組後,經過 mediaSelectionGroupForMediaCharacteristic 獲取到對應類型軌道包含的全部軌道的組合 AVMediaSelectionGroup,而後遍歷 AVMediaSelectionGroup 的 options 屬性能夠獲取到全部的 AVMediaSelectionOption 對象。獲得 AVMediaSelectionOption 對象後就能夠進行 AVPlayerItem 的屬性設置了。

NSString *mc = AVMediaCharacteristicLegible;
    AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:mc];
    if (group) {
        NSMutableArray *subtitles = [NSMutableArray array];
        for (AVMediaSelectionOption *option in group.options) {
            [subtitles addObject:option.displayName];
        }
        // 獲取到全部支持的字幕名稱
    } else {
    }
   
   
    NSString *mc = AVMediaCharacteristicLegible;
    AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:mc]; 
    BOOL selected = NO;
    for (AVMediaSelectionOption *option in group.options) {
        if ([option.displayName isEqualToString:subtitle]) {
            [self.playerItem selectMediaOption:option inMediaSelectionGroup:group];
            // 匹配後設置字幕屬性
        }
    }
    
    
    [self.playerItem selectMediaOption:nil inMediaSelectionGroup:group];// 設置爲 nil 能夠取消字幕
複製代碼
相關文章
相關標籤/搜索