【iOS】ZFPlayer源碼解讀<中>

前言

本篇繼ZFPlayer源碼解讀<上>基礎(chǔ)之上啤咽,主要解析說明控制層播放器衬廷,因?yàn)樵谏掀恼轮连F(xiàn)在并未提及絲毫關(guān)于這兩個(gè)類業(yè)務(wù)的實(shí)現(xiàn)巷疼。

首先說下這兩個(gè)類各自的職責(zé)。

  • 控制層:主要負(fù)責(zé)響應(yīng)與用戶之間的交互按脚,如手勢控制的播放于毙,暫停,重試辅搬,快進(jìn)快退唯沮,亮度聲音等等。
  • 播放器:主要負(fù)責(zé)視頻的播放堪遂,不用想的太復(fù)雜介蛉,只是一展示視頻的layer而已。

個(gè)人感覺之所以視頻復(fù)雜的原因是因?yàn)槿芡剩c用戶的交互太多币旧,異常操作又太多,業(yè)務(wù)太復(fù)雜而已猿妈,實(shí)質(zhì)一個(gè)個(gè)功能模塊實(shí)現(xiàn)完畢后吹菱,最后就剩下這幾個(gè)類的api之間的調(diào)用了。

控制層

pod 'ZFPlayer/ControlView', '~> 3.0'
如果對此沒有高度的個(gè)性化的自定義彭则,可以直接使用原庫自帶的控制層鳍刷。pod install后如下:

控制層文件夾.png

UIImageView+ZFCache,UIView+ZFFrame類: 加載圖片分類俯抖,基礎(chǔ)類输瓜,略
ZZLandScapeControlView類: 橫屏狀態(tài)下的控制層
ZZPortaitControlView類: 橫屏狀態(tài)下的控制層
ZZLoaingView類: 加載菊花
ZZSmallFloatControlView類: 播放中的cell滑出屏幕后展示的小浮窗上面的控制層
ZFNetworkSpeedMonitor類: 網(wǎng)絡(luò)加載速度計(jì)算器
ZFSpeedLoadingView類: 網(wǎng)絡(luò)速度信息視圖
ZFSliderView類: 進(jìn)度滑桿視圖,播放進(jìn)度芬萍,緩沖進(jìn)度
ZFNetworkSpeedMonitor類: 網(wǎng)絡(luò)加載速度計(jì)算器
ZFVolumeBrightnessView類: 亮度與音量視圖
ZFPlayerControlView類: 控制層核心類尤揣,協(xié)調(diào)播放器player與ControlView更新同步
ZFUtilities類: 工具類,無其它業(yè)務(wù)

播放器

ZFAVPlayerManager類柬祠,實(shí)現(xiàn)上篇中提到的 ZFAVPlayerManager 協(xié)議北戏。播放器的核心類。

ZFAVPlayerManager.m中大概不足500行代碼漫蛔,即核心的東西其實(shí)并不多最欠。
在這里我認(rèn)為,無論是音頻或者視頻惩猫,業(yè)務(wù)都是通用并且是固定的芝硬。可以從業(yè)務(wù)上考慮一下轧房,我需要什么樣的方法拌阴,然后一一封裝實(shí)現(xiàn)就可以了。

Git上有好多相關(guān)的播放器demo參考奶镶。但是有個(gè)不好的地方在于迟赃,復(fù)雜的demo功能多好用陪拘,但是修改的話就需要花費(fèi)太大的時(shí)間與精力,因?yàn)槭紫刃枰炊髡叩脑创a才能在基礎(chǔ)之上修改纤壁。簡單的demo只有播放功能或者業(yè)務(wù)功能耦合太強(qiáng)左刽,不好入手。
考慮到以上弊端酌媒,我們可以把功能單獨(dú)拆開欠痴,即播放器,視圖秒咨,用戶交互喇辽。然后再根據(jù)功能組合即可。模塊功能拆分源碼下載

在這里只貼一下比較重要的幾個(gè)API雨席,另外我fork了此庫菩咨,若有意了解更多,點(diǎn)擊了解更多關(guān)于ZFPlayer的注解

  • (void)prepareToPlay; 初始化播放器陡厘,準(zhǔn)備并開始播放

  • (void)reloadPlayer; 刷新播放抽米,可用于網(wǎng)絡(luò)失敗重試加載,加載成功后會在上次播放失敗的進(jìn)度處繼續(xù)播放

  • (void)play糙置;開始播放缨硝。

  • (void)pause;音視頻暫停罢低,這里會保留播放的進(jìn)度。

  • (void)stop胖笛;停止播放网持,這里與上面的暫停不同于,會移除并置空初始化所有的相關(guān)的播放配置信息长踊,并將播放器置空功舀。

  • (void)replay ; 重新播放,本質(zhì)相當(dāng)于把播放進(jìn)度更新為0的位置繼續(xù)播放身弊。

  • (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler 辟汰;快進(jìn)快退到某一時(shí)刻。

  • (UIImage *)thumbnailImageAtCurrentTime 阱佛;獲取當(dāng)前時(shí)刻這一幀的圖片帖汞。

  • (NSTimeInterval)availableDuration;緩沖的可以播放的時(shí)長凑术。

  • (void)initializePlayer翩蘸;這里初始化了播放器。

  • (void)enableAudioTracks:(BOOL)enable inPlayerItem:(AVPlayerItem*)playerItem ;設(shè)置播放對象的音軌是否可用淮逊。

- (void)bufferingSomeSecond {
    // 緩沖較差時(shí)候回調(diào)這里
    // playbackBufferEmpty會反復(fù)進(jìn)入催首,因此在bufferingOneSecond延時(shí)播放執(zhí)行完之前再調(diào)用bufferingSomeSecond都忽略
    if (self.isBuffering) return;
    self.isBuffering = YES;
    
    // 需要先暫停一小會之后再播放扶踊,否則網(wǎng)絡(luò)狀況不好的時(shí)候時(shí)間在走,聲音播放不出來
    [self.player pause];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 如果此時(shí)用戶已經(jīng)暫停了郎任,則不再需要開啟播放了
        if (!self.isPlaying) {
            self.isBuffering = NO;
            return;
        }
        [self play];
        // 如果執(zhí)行了play還是沒有播放則說明還沒有緩存好秧耗,則再次緩存一段時(shí)間
        self.isBuffering = NO;
        if (!self.playerItem.isPlaybackLikelyToKeepUp) [self bufferingSomeSecond];
    });
}
當(dāng)在初始化視頻資源的時(shí)候,需要先移除之前的所有的觀察屬性舶治,再重新添加
- (void)itemObserving {
// 移除之前的
    [_playerItemKVO safelyRemoveAllObservers];

//觀察最新的播放item的屬性
    _playerItemKVO = [[ZFKVOController alloc] initWithTarget:_playerItem];

// 播放狀態(tài)
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kStatus
                              options:NSKeyValueObservingOptionNew
                              context:nil];

// 緩沖區(qū)是否為空
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kPlaybackBufferEmpty
                              options:NSKeyValueObservingOptionNew
                              context:nil];

// 緩沖區(qū)的加載的資源是否足夠播放
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kPlaybackLikelyToKeepUp
                              options:NSKeyValueObservingOptionNew
                              context:nil];

// 監(jiān)聽緩沖區(qū)加載資源變化
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kLoadedTimeRanges
                              options:NSKeyValueObservingOptionNew
                              context:nil];

// 視頻填充樣式變化
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kPresentationSize
                              options:NSKeyValueObservingOptionNew
                              context:nil];
    
// 類似NSTimer分井,這里設(shè)置單位一秒回調(diào)一次,注意block回調(diào)結(jié)果指定線程
    CMTime interval = CMTimeMakeWithSeconds(kTimeRefreshInterval, NSEC_PER_SEC);
    @weakify(self)
    _timeObserver = [self.player addPeriodicTimeObserverForInterval:interval queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        @strongify(self)
        if (!self) return;
        NSArray *loadedRanges = self.playerItem.seekableTimeRanges;
        if (loadedRanges.count > 0) {
// 有加載好的段資源才進(jìn)行回調(diào)歼疮,否則數(shù)據(jù) 拿到有誤
            if (self.playerPlayTimeChanged) self.playerPlayTimeChanged(self, self.currentTime, self.totalTime);
        }
    }];
    
// 音視頻播放結(jié)束回調(diào)
    _itemEndObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        @strongify(self)
        if (!self) return;
        self.playState = ZFPlayerPlayStatePlayStopped;
        if (self.playerDidToEnd) self.playerDidToEnd(self);
    }];
}
// KVO杂抽,核心監(jiān)聽上述觀察的相關(guān)屬性
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([keyPath isEqualToString:kStatus]) {
            if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) {
                
            
                }
            } else if (self.player.currentItem.status == AVPlayerItemStatusFailed) {
                
            }
        } else if ([keyPath isEqualToString:kPlaybackBufferEmpty]) {
            if (self.playerItem.playbackBufferEmpty) {
// 這里有觀察到緩沖音視頻不足為空,需要更新狀態(tài)緩沖中韩脏,然后播放器暫停缩麸,待播放器加載個(gè)幾秒后(這里可以自己定),再開始播放赡矢,調(diào)用 ```bufferingSomeSecond```方法杭朱。
                
            }
        } else if ([keyPath isEqualToString:kPlaybackLikelyToKeepUp]) {
           
        } else if ([keyPath isEqualToString:kLoadedTimeRanges]) {
            // 這里緩沖好后,不能立即播放吹散,因?yàn)樾枰袛嘀笆欠裨诓シ胖谢⌒担粼诓シ胖校瑒t繼續(xù)播放空民,若原本用戶已經(jīng)手動(dòng)暫停了刃唐,就算緩沖好了,此處應(yīng)該需要暫停界轩。
        } else if ([keyPath isEqualToString:kPresentationSize]) {
            
        } else {
           
        }
    });
}
  • (NSTimeInterval)totalTime画饥;音視頻總時(shí)間。

  • (NSTimeInterval)currentTime浊猾;當(dāng)前的已經(jīng)播放的時(shí)間抖甘。

  • (void)setPlayState:(ZFPlayerPlaybackState)playState;設(shè)置播放狀態(tài)葫慎,同時(shí)更新并刷新相關(guān)的UI狀態(tài)衔彻。

  • (void)setLoadState:(ZFPlayerLoadState)loadState;設(shè)置加載狀態(tài)偷办,這里可以更新并刷新相關(guān)的UI狀態(tài)艰额。

  • (void)setAssetURL:(NSURL *)assetURL ;更新播放資源URL,重新播放椒涯。

  • (void)setRate:(float)rate悴晰;播放速度。

  • (void)setMuted:(BOOL)muted;是否靜音铡溪。

  • (void)setScalingMode:(ZFPlayerScalingMode)scalingMode漂辐;設(shè)置視頻展示填充的樣式。

  • (void)setVolume:(float)volume; 設(shè)置播放器聲音棕硫。

滾動(dòng)播放業(yè)務(wù)流程原理

一般此業(yè)務(wù)存在于列表tableView或collectionView

  • 業(yè)務(wù)流程:當(dāng)視頻cell局部或者全部滾入到 可視區(qū)域后髓涯,視頻需要自動(dòng)播放,當(dāng)局部或者全部滾出可視區(qū)域后哈扮,需要暫停纬纪,依次類推。
  • 播放器播放原理:
  1. 首先滑肉,無論列表再怎么滾動(dòng)包各,播放器始終只有一個(gè),要知道改變的只是播放器的位置與播放資源而已靶庙。

  2. 播放資源來源于列表上每一個(gè)indexPath對應(yīng)的model问畅;
    坐標(biāo)來源于此indexPath對應(yīng)的cell上的某一個(gè)用于展示視頻視圖的容器視圖;

  3. 播放視頻六荒,暫停播放視頻調(diào)用的是播放器的API护姆;

4.什么時(shí)候調(diào)用開始播放,什么時(shí)候調(diào)用暫停播放掏击,取決于當(dāng)前的視頻cell是否一定比例進(jìn)入可視區(qū)域卵皂,一定比例離開可視區(qū)域;

  1. 上面的問題如果解決了砚亭,那么浮窗播放視圖就簡單了灯变,就是在cell離開播放區(qū)域,把正在播放的playerView放在浮窗視圖上捅膘,浮窗放在window上即可添祸。

針對第4點(diǎn),可以參考源碼UIScrollView + ZFPlayer,專門計(jì)算列表滑動(dòng)時(shí)篓跛,暫停時(shí),cell的坐標(biāo)相互轉(zhuǎn)換坦刀,播放view的離開比例愧沟,進(jìn)入比例等。

最后

上述的控制層與播放器都有了鲤遥,音頻視頻都可以正常的播放并且與用戶交互沐寺。
若想個(gè)性化自定義,建議直接在ork源碼盖奈,在基礎(chǔ)之上更改最效率混坞。

疑問

不過到目前為止,還有幾個(gè)問題需要解決?

  • 直播怎么搞究孕,類似虎牙那樣的啥酱,雖然是直播,但是也有播放器的些許功能厨诸;
  • 邊下邊播镶殷,類似市面上的音頻播放類app,視頻類愛其藝app等微酬;


ZFPlayer源碼解讀<下>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绘趋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子颗管,更是在濱河造成了極大的恐慌陷遮,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垦江,死亡現(xiàn)場離奇詭異帽馋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)疫粥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門茬斧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人梗逮,你說我怎么就攤上這事项秉。” “怎么了慷彤?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵娄蔼,是天一觀的道長。 經(jīng)常有香客問我底哗,道長岁诉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任跋选,我火速辦了婚禮涕癣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘前标。我一直安慰自己坠韩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布炼列。 她就那樣靜靜地躺著只搁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俭尖。 梳的紋絲不亂的頭發(fā)上氢惋,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天洞翩,我揣著相機(jī)與錄音,去河邊找鬼焰望。 笑死骚亿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柿估。 我是一名探鬼主播循未,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秫舌!你這毒婦竟也來了的妖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤足陨,失蹤者是張志新(化名)和其女友劉穎嫂粟,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體墨缘,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡星虹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了镊讼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宽涌。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蝶棋,靈堂內(nèi)的尸體忽然破棺而出卸亮,到底是詐尸還是另有隱情,我是刑警寧澤玩裙,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布兼贸,位于F島的核電站,受9級特大地震影響吃溅,放射性物質(zhì)發(fā)生泄漏溶诞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一决侈、第九天 我趴在偏房一處隱蔽的房頂上張望螺垢。 院中可真熱鬧,春花似錦赖歌、人聲如沸枉圃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽讯蒲。三九已至痊土,卻和暖如春肄扎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工犯祠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旭等,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓衡载,卻偏偏與公主長得像搔耕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子痰娱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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