AVFoundation框架(四) - 視頻的播放

視頻播放

播放視頻是AVFoundation的最核心功能之一,這里我們將學習創(chuàng)建一個自定義視頻播放器.

1. 先了解關于播放功能的類

  • AVPlayer:
    AVPlayer是一個用來播放 基于時間的視聽媒體 的管理對象.支持播放 本地,分布下載以及HTTP Live Streaming 協(xié)議等得到的流媒體.

AVPlayer只管理一個單獨資源的播放.如果你需要播放多個或循環(huán)播放時可以使用它的子類AVQueuePlayer.

  • AVPlayerLayer:
    由于AVPlayer是一個不可見組件,所以如果要將視頻資源導出到用戶界面的目標位置,我們需要使用AVPlayerLayer.
    AVPlayerLayer是構建于Core Animation之上的可視化組件,是擴展了的CALayer類,作為視頻內(nèi)容的渲染面來在屏幕上顯示視頻.
    因此,創(chuàng)建AVPlayerLayer需要一個指向AVPlayer對象的指針,來將圖層和播放器緊密綁定在一起,從而保證當播放器基于時間的方法出現(xiàn)時二者能保持同步.

AVPlayerLayer使用比較簡單,所以供開發(fā)者自定義的地方也少.只有一個videoGravity屬性.它定義了三個不同的gravity值,用來確定在承載層的范圍內(nèi) 視頻可以拉伸或縮放的 程度.

  • AVPlayerItem:
    我們最終目的是使用AVPLayer來播放AVAsset. 但是AVAsset只包含媒體資源的不可變的靜態(tài)信息.我們無法獲取播放的當前時間等動態(tài)信息.所以引入AVPlayerItemAVPlayerItemTrack來構建相應的動態(tài)內(nèi)容.
    AVPlayerItem會建立媒體資源動態(tài)視角的數(shù)據(jù)模型 并保存AVPlayer在播放資源時的呈現(xiàn)狀態(tài).

AVPlayerItem由一個或多個媒體曲目組成,跟AVAsset中AVAssetTrack對應. AVPlayerItemTrack實例則用于表示播放器條目中的類型統(tǒng)一的媒體流.比如音頻流, 視頻流.

// 應用流程
//1. 創(chuàng)建AVAsset
AVAsset *asset = [AVAsset assetWithURL:assetURL];
//2. 創(chuàng)建對應播放元素
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
//3. 創(chuàng)建播放器
AVPlayer *player = [AVPlayer playerWithPlayerItem: playerItem];
//4. 創(chuàng)建視頻顯示的圖層.
AVPlayerLayer * playerLayer = [AVPlayerLayer playerLayerWithPlayer: player];
// 最后添加圖層到view上

當然,上面只是基本步驟,真正應用時還要看當前媒體是否已經(jīng)加入到了播放隊列中,播放器是否準備完全. 可以使用KVO監(jiān)聽AVPlayerItem的status屬性得知.

  • 補充:CMTime
    AVPlayer和AVPlayerItem都是基于時間的對象. AVFoundation使用CMTime數(shù)據(jù)結構為時間表示. 它屬于基于C的底層CoreMedia框架.使用更精確的分數(shù)格式來處理時間數(shù)據(jù).
typedef struct {
  CMTimeValue value;
  CMTimeScale timescale;
  CMTimeFlags flags;
  CMTimeEpoch epoch; 
} CMTime; 

// 創(chuàng)建時間例子
CMTime halfSecond = CMTimeMake(1,2);          // 0.5s
CMTime fiveSeconds = CMTimeMake(5,1);        // 5s
CMTime time = CMTimeMake(1,44100);            // 44.1赫茲 

2. 視頻播放器

2.1 創(chuàng)建視頻視圖UIView.

由上面的類介紹可知,我們要創(chuàng)建視頻視圖就是使用了AVPlayerLayer圖層的UIView. 我們既可以手動創(chuàng)建layer并添加到UIView的原有圖層,也可以重寫layerClass方法自定義view的圖層類型來使用. 推薦第二種方法, 這樣就不用考慮圖層級關系了.

// THPlayerView.m
+ (Class)layerClass {                                                       
    return [AVPlayerLayer class];
}

- (id)initWithPlayer:(AVPlayer *)player {
    if (self = [super initWithFrame:CGRectZero]) {
        self.backgroundColor = [UIColor blackColor];
        self.autoresizingMask = UIViewAutoresizingFlexibleHeight |
                                UIViewAutoresizingFlexibleWidth;
  // 使之成為AVPlayer實例視頻輸出的圖層
        [(AVPlayerLayer *) [self layer] setPlayer:player];                  
  // THOverlayView-操作視頻交互界面.
        [[NSBundle mainBundle] loadNibNamed:@"THOverlayView"                
                                      owner:self
                                    options:nil];
        
        [self addSubview:_overlayView];
    }
    return self;
}
2.2 創(chuàng)建視頻控制器
static const NSString *PlayerItemStatusContext;

- (id)initWithURL:(NSURL *)assetURL {
    if (self = [super init]) {
  // 加載本地或遠端視頻資源
        _asset = [AVAsset assetWithURL:assetURL];                           
        [self prepareToPlay];
    }
    return self;
}

- (void)prepareToPlay {
    NSArray *keys = @[
        @"tracks",
        @"duration",
        @"commonMetadata",
        @"availableMediaCharacteristicsWithMediaSelectionOptions"
    ];
// iOS 7之后可自動載入資源屬性.不需要再調用loadValuesAsynchronouslyForKeys
    self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset          
                           automaticallyLoadedAssetKeys:keys];
// 監(jiān)聽 playerItem 狀態(tài)是否準備完全.
    [self.playerItem addObserver:self                                       
                      forKeyPath:STATUS_KEYPATH
                         options:0
                         context:&PlayerItemStatusContext];
// 創(chuàng)建視頻視圖
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];          
    self.playerView = [[THPlayerView alloc] initWithPlayer:self.player];    
}

// KVO實現(xiàn)- 監(jiān)聽狀態(tài)改變
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if (context == &PlayerItemStatusContext) {
        dispatch_async(dispatch_get_main_queue(), ^{     
            [self.playerItem removeObserver:self forKeyPath:STATUS_KEYPATH];
            
            if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
/*
                // 此部分設置播放器的時間監(jiān)聽. 下面小結會介紹這個功能
                [self addPlayerItemTimeObserver];
                [self addItemEndObserverForPlayerItem];

             // transport 是自定義的關聯(lián)屬性,用來溝通交互界面和實際視頻播放狀態(tài)信息.
                CMTime duration = self.playerItem.duration; 
                  // 設置當前時間和視頻總長.將用戶界面的時間和播放媒體進行同步 .
                [self.transport setCurrentTime:CMTimeGetSeconds(kCMTimeZero)
                                      duration:CMTimeGetSeconds(duration)];
                  // 傳遞視頻標題, AVAsset沒有title屬性,這是分類添加的,增加可讀性.
                [self.transport setTitle:self.asset.title];                 
*/
                // 播放
                [self.player play];                                         
            } else {
                [UIAlertView showAlertWithTitle:@"Error"
                                        message:@"Failed to load video"];
            }
        });
    }
}

通過上面代碼視頻資源就可以播放了.但是還需要提供用戶界面的控制功能,反饋信息.

2.3 時間監(jiān)聽

上面使用了KVO來監(jiān)聽播放條目的status屬性,KVO確實可以監(jiān)聽AVPlayerItem和AVPlayer很多屬性,但是對于AVPlayer的時間變化這種明顯動態(tài)特性和非常高的精確度要求,KVO難以勝任.
對此AVPlayer提供了兩種基于時間方法進行監(jiān)聽:

  • 定期監(jiān)聽:
    一定時間間隔獲得通知(如更新時間變化,進度條的移動).
- (void)addPlayerItemTimeObserver {
    // 創(chuàng)建0.5s的刷新間隔
    CMTime interval =
        CMTimeMakeWithSeconds(0.5f, NSEC_PER_SEC);             
    // 一般主線程更新UI,所以用主隊列.
    dispatch_queue_t queue = dispatch_get_main_queue();               
    __weak THPlayerController *weakSelf = self;                             
    void (^callback)(CMTime time) = ^(CMTime time) {
        NSTimeInterval currentTime = CMTimeGetSeconds(time);
        NSTimeInterval duration = CMTimeGetSeconds(weakSelf.playerItem.duration);
        [weakSelf.transport setCurrentTime:currentTime duration:duration];  // 回調傳入最新值
    };
    
// 返回id型指針, 后面會用此移除監(jiān)聽器. [self.player removeTimeObserver:self.timeObserver];
    self.timeObserver =                                                     
        [self.player addPeriodicTimeObserverForInterval:interval
                                                  queue:queue
                                             usingBlock:callback];
}
  • 邊界監(jiān)聽:
    監(jiān)聽視頻播放視頻通過自定義時間軸邊界點.例如播放到25%,50%等.
    - (id)addBoundaryTimeObserverForTimes:(NSArray<NSValue *> *)times queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(void))block;
  • 條目結束監(jiān)聽:
    還有一個常見監(jiān)聽事件就是播放完畢的時間.當視頻播放完畢,AVPlayerItem會發(fā)送一個AVPlayerItemDidPlayToEndTimeNotification通知.
2.4 視頻縮略圖

現(xiàn)在流行用視頻中一個或多個縮略圖作為視頻封面. 通過AVAssetImageGenerator類可以實現(xiàn).
它定義了兩個方法實現(xiàn)從視頻資源中檢索圖片.:

  • - (nullable CGImageRef)copyCGImageAtTime:(CMTime)requestedTime actualTime:(nullable CMTime *)actualTime error:(NSError * _Nullable * _Nullable)outError CF_RETURNS_RETAINED; 在指定時間點捕捉圖片.
  • - (void)generateCGImagesAsynchronouslyForTimes:(NSArray<NSValue *> *)requestedTimes completionHandler:(AVAssetImageGeneratorCompletionHandler)handler; 在指定時間段生成一個圖片序列.

AVAssetImageGenerator可以生成本地圖片,也能生成持續(xù)下載的資源.

// 1 創(chuàng)建AVAssetImageGenerator
    self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:self.asset];
    // 2. 配置生成圖片大小 @2x.
    self.imageGenerator.maximumSize = CGSizeMake(200.0f, 0.0f);
    // 3. 計算視頻中捕捉位置.  均分20份.
    CMTime duration = self.asset.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];
    // 定義handler
    AVAssetImageGeneratorCompletionHandler handler;
    handler = ^(CMTime requestedTime, // 請求的最初時間,對應于生成圖像的調用中指定的times數(shù)組中的值.
                CGImageRef imageRef, // 生成的CGImageRef.
                CMTime actualTime,   // 圖片實際生成的時間.基于實際效率.
                AVAssetImageGeneratorResult result,
                NSError *error) {

        if (result == AVAssetImageGeneratorSucceeded) { // 圖片生成成功.保存圖片和對應時間點到thumbnail 模型中
          UIImage *image = [UIImage imageWithCGImage:imageRef];
            id thumbnail =
                [THThumbnail thumbnailWithImage:image time:actualTime];
            [images addObject:thumbnail];
        } else {
            NSLog(@"Error: %@", [error localizedDescription]);
        }

        // 每次調用減一,為0表示所有圖片處理完成.
        if (--imageCount == 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSString *name = THThumbnailsGeneratedNotification;
                NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
                [nc postNotificationName:name object:images];
            });
        }
    };
  
  // 調用
    [self.imageGenerator generateCGImagesAsynchronouslyForTimes:times
                                              completionHandler:handler];
2.5 顯示字幕

AVFoundation提供了展示和隱藏字幕的方法,AVPlayerLayer會自動渲染這些元素. 這里要使用兩個類:AVMediaSelectionGroupAVMediaSelectionOption

  • AVMediaSelectionOption :表示AVAsset中備用的媒體呈現(xiàn)方式. 例如通常一個資源可能包含中英雙音頻,不同指定語言的字幕等備用選擇. AVAsset有一個availableMediaCharacteristicsWithMediaSelectionOptions字符串數(shù)組屬性, 保存了這些可用選項的媒體特征. 特征這些具體來說有AVMediaCharacteristicAudible(音頻),AVMediaCharacteristicLegible(字幕或隱式字幕)AVMediaCharacteristicVisual(視頻)三方面.
  • AVMediaSelectionGroup:而這個則作為上面類的實例容器,因為通常一個資源可以有一個和多個互斥的AVMediaSelectionOption實例可供選擇.
// 檢索資源備用呈現(xiàn)方式.(首先需要在一開始加載資源時,添加上availableMediaCharacteristicsWithMediaSelectionOptions屬性.參考上面- (void)prepareToPlay 方法)
- (void)loadMediaOptions { 
// 這里只檢索字幕相關的選項
    NSString *mc = AVMediaCharacteristicLegible;                            
    AVMediaSelectionGroup *group =
        [self.asset mediaSelectionGroupForMediaCharacteristic:mc];          
    if (group) {            
        for (AVMediaSelectionOption *option in group.options) {
          NSLog(@"字幕可選方式: %@", option.displayName);
        }
    } 
}
// 選擇想要的呈現(xiàn)方式.
- (void)subtitleSelected:(NSString *)subtitle {
    NSString *mc = AVMediaCharacteristicLegible;
    AVMediaSelectionGroup *group =
        [self.asset mediaSelectionGroupForMediaCharacteristic:mc];          
    BOOL selected = NO;
    for (AVMediaSelectionOption *option in group.options) {
    // 遍歷所有組選項,找到匹配你要的option.
        if ([option.displayName isEqualToString:subtitle]) {
        // 激活選擇
            [self.playerItem selectMediaOption:option                       
                         inMediaSelectionGroup:group];
            selected = YES;
        }
    }
    if (!selected) {
        [self.playerItem selectMediaOption:nil                              
                     inMediaSelectionGroup:group];
    }
}

3. 整合AirPlay

AirPlay是蘋果旨在用無線方式將流媒體內(nèi)容在Apple TV或第三方音頻系統(tǒng)上播放.
AVPlayer有一個allowsExternalPlayback屬性,允許啟用AirPlay功能,默認是YES.

  • 線路選擇功能
    在播放過程中上滑就能打開系統(tǒng)AirPlay界面,但是其實應用內(nèi)部也可以提供AirPlay線路選擇界面.
UIImage *airplayImage = [UIImage imageNamed:@"airplay"];
    self.volumeView = [[MPVolumeView alloc] initWithFrame:CGRectZero];
    self.volumeView.showsVolumeSlider = NO;
    self.volumeView.showsRouteButton = YES;
    [self.volumeView setRouteButtonImage:airplayImage forState:UIControlStateNormal];
    
    [self.volumeView sizeToFit];

    NSMutableArray *items = [NSMutableArray arrayWithArray:self.toolbar.items];
    UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:self.volumeView];
    [items addObject:item];
    self.toolbar.items = items;

4. AVKit

AVKit簡化了視頻播放器的創(chuàng)建過程,它使用蘋果原生的視頻界面,不需要我們過多的自定義.如果你只是在項目中需要簡單的播放功能,推薦使用AVKit.
iOS平臺AVkit是一個簡單的標準框架,只包含一個AVPlayerViewController類.用于展示并控制AVPlayer實例的播放.具有簡單的界面,提供以下幾個屬性和方法:

AVPlayerViewController提供的屬性和方法

使用方法也十分簡單

NSString * path = [[NSBundle mainBundle]pathForResource:@"iphone" ofType:@"mp4"];
NSURL *url = [NSURL fileURLWithPath:path];
AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
playerVC.player = [AVPlayer playerWithURL:url];
// 若要使用更多功能.
///  AVPlayerItem *playerItem = [AVPlayerItem alloc] initWithURL: url];
/// playerVC.player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
[self presentViewController:play animated:YES completion:nil];

iOS 8之前提供了MPMoviePlayerController和MPMoviePlayerViewController兩個類,它們提供了一種簡單的方法將完整視頻播放功能整合到應用中,相比較與AVKit, MPMoviePlayerController定義了一些標準播放控件,供我們選擇,但是同時它將所有基礎功能隱藏,讓開發(fā)者無法使用AVPlayer層的更高級的基礎功能.所以iOS9之后被易用.
而新的AVKit提供了一種動態(tài)播放控件,自動為用戶提供最好的體驗. 并且AVPlayerViewController暴露了底層AVPlayer和AVPlayerItem的接口,也支持開發(fā)者使用AVPlayer更高級的功能,

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蓄诽,一起剝皮案震驚了整個濱河市薛训,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仑氛,老刑警劉巖许蓖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異调衰,居然都是意外死亡,警方通過查閱死者的電腦和手機自阱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門嚎莉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沛豌,你說我怎么就攤上這事趋箩≡叨睿” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵叫确,是天一觀的道長跳芳。 經(jīng)常有香客問我,道長竹勉,這世上最難降的妖魔是什么飞盆? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮次乓,結果婚禮上吓歇,老公的妹妹穿的比我還像新娘。我一直安慰自己票腰,他們只是感情好城看,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杏慰,像睡著了一般测柠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缘滥,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天轰胁,我揣著相機與錄音,去河邊找鬼完域。 笑死软吐,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的吟税。 我是一名探鬼主播凹耙,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肠仪!你這毒婦竟也來了肖抱?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤异旧,失蹤者是張志新(化名)和其女友劉穎意述,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吮蛹,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡荤崇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了潮针。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片术荤。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖每篷,靈堂內(nèi)的尸體忽然破棺而出瓣戚,到底是詐尸還是另有隱情端圈,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布子库,位于F島的核電站舱权,受9級特大地震影響,放射性物質發(fā)生泄漏仑嗅。R本人自食惡果不足惜宴倍,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望无畔。 院中可真熱鬧啊楚,春花似錦、人聲如沸浑彰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽郭变。三九已至颜价,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诉濒,已是汗流浹背周伦。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留未荒,地道東北人专挪。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像片排,于是被迫代替她去往敵國和親寨腔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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