自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

西瓜視頻 iOS 播放器技術(shù)重構(gòu)

原創(chuàng) 精選
移動開發(fā) iOS
本次重構(gòu)采用自下而上的方式進(jìn)行,從極簡播放器到播放框架、再到播放架構(gòu)在副場景驗證,進(jìn)而延伸到主場景,最終整體完成。

播放器簡介

播放器是西瓜視頻等視頻類 App 最主要的業(yè)務(wù)場景,也是最主要的流量入口,其承載包括下層基礎(chǔ)播放,上層的各種播放業(yè)務(wù):狀態(tài)欄、彈幕、音量、亮度、評論、點贊、進(jìn)度、倍速、清晰度、選集、合集、商業(yè)化等。

西瓜對整個業(yè)務(wù)播放器做了整體抽象,提供了一套可插拔,可復(fù)用的播放器業(yè)務(wù)框架,包括:視頻播放、播控交互、業(yè)務(wù)拓展。

本文播放器是指業(yè)務(wù)播放器,主要包括視頻播放、播控交互、播放業(yè)務(wù)拓展,本播放器旨在提供一套完整的架構(gòu)來包容播放器所有業(yè)務(wù),實現(xiàn)播放業(yè)務(wù)可插拔。

圖片

現(xiàn)狀分析

現(xiàn)有播放架構(gòu)

  • 播放器架構(gòu)圖

圖片

  • 現(xiàn)有架構(gòu)存在的問題:
  • 播放架構(gòu)以 Redux 為核心,播放器與業(yè)務(wù)播放器強耦合,也會存在 Redux 套 Redux 的現(xiàn)況。
  • 業(yè)務(wù)播放器使用 KVC 的方式對播放器進(jìn)行操作。
  • 播放器和業(yè)務(wù)播放器大量使用 Category 來對業(yè)務(wù)進(jìn)行解耦。

Redux 播放器架構(gòu)分析

Redux 架構(gòu)介紹

  • 關(guān)于 Redux
  • 什么是 Redux?“一種可預(yù)測的狀態(tài)容器”。他其實也是 Flux 里面“單向數(shù)據(jù)流”的思想,只是充分利用了函數(shù)的特性。
  • 為什么使用 Redux?為了解決組件之間的通信問題。(用戶操作繁瑣,導(dǎo)致組件間需要有狀態(tài)依賴、客戶端權(quán)限較多且有大量交互、客戶端與服務(wù)端有大量交互)
  • Redux 是怎么工作的?↓

圖片

Redux 三大原則

單一數(shù)據(jù)源

  • Store 全局唯一的一個對象,就把 TA 當(dāng)成一個容器,所有的狀態(tài)都在 Store 下進(jìn)行統(tǒng)一“配置”。(Redux 狀態(tài)管理 => 一個全局對象 Store => 所有狀態(tài)都在全局 Store 下統(tǒng)一配置,為了統(tǒng)一管理)

State 是只讀的

  • Action 是用來描述發(fā)生了什么的“關(guān)鍵詞”,而 Redux 唯一改變 State 的方法就是觸發(fā) Action,而具體使 Action 在 State 上更新生效的是 Reducer---用來描述發(fā)生的詳細(xì)過程,他充當(dāng)了一個 Action 到 State 的橋梁。
  • (狀態(tài) => 直接改變 State 不能觸發(fā) Render => 唯一改變的方式是觸發(fā) Action)
  • (Action => 描述事件發(fā)生的詳細(xì)過程)
  • (Reducer => 充當(dāng)發(fā)起 Action 到 State 的橋梁)
  • (修改狀態(tài) => 當(dāng)試圖修改狀態(tài)時,Redux 會記錄這個動作是什么類型、具體完成什么功能,調(diào)試時為開發(fā)者提供完整的數(shù)據(jù)流路徑)

Reducer 必須是一個純函數(shù)

  • 描述 Action 是怎么改變 State 的。接收舊 State 和 Action,返回新 State。
  • (Reducer => 內(nèi)部執(zhí)行必須無副作用,不能直接修改 State => 狀態(tài)發(fā)生變化時,要返回一個全新的對象代表新的 State)

Middleware

  • Middleware 異步數(shù)據(jù)流--沒有返回值
  • 用戶觸發(fā)點贊->產(chǎn)生 Digg Action -> Action 經(jīng)過 Middleware 異步處理(請求點贊接口)->異步完成(API 請求成功)->Action 傳給 Reducer->Action+OldState->newState->Store 中的 State 更新->通知 View
  • 高階函數(shù)
- (ReduxNSArrayReduce)redux_reduce {
__weak NSArray* warray = self;
return ^id(id initial, ReduxNSArrayReduceOperator ro) {
__strong NSArray *array = warray;
id result = initial;
for(id object in array) {
result = ro(result, object);
}
return result;
};
}


self.dispatchFunction = middlewares.redux_reverse.redux_reduce(defaultDispatch, ^id(ReduxDispatchFunc df, ReduxMiddleware mw){
return mw([retryDispatch copy], [getState copy])(df);
});
- (id)reduce:(ReduxNSArrayReduceOperator)ro initial:(id)initial {
id result = initial;
for (id obj in self) {
result = ro(result, obj);
}
return result;
}

[middlewares.redux_reverse reduce:^id(ReduxDispatchFunc df, ReduxMiddleware mv) {
return mv([retryDispatch copy], [getState copy])(df);
} initial:defaultDispatch];
  • 現(xiàn)存播放架構(gòu)問題
  • 通過 Instruments 測試可以看到,Redux 狀態(tài)模型成復(fù)制本過高,在復(fù)雜業(yè)務(wù)中性能表現(xiàn)欠佳。
  • Reducer 更新完 State 后,需要遍歷所有 Part 通知狀態(tài)變化,效率很低。
  • Part 之間耦合嚴(yán)重,存在 Redux 套 Redux 的情況,維護成本高。
  • 播放器業(yè)務(wù)層使用 KVC 獲取底層播放實例,底層播放實例與業(yè)務(wù)耦合嚴(yán)重。
  • 框架層和業(yè)務(wù)層 Player 都使用大量的 Category 來開發(fā)業(yè)務(wù) UI,播放器框架層與業(yè)務(wù)邏輯耦合。

Redux 播放器性能分析

通過線下性能分析,將播放流程中所有的耗時點、卡頓點整體歸類,總結(jié)出卡頓耗時任務(wù),其中 Redux 和播放業(yè)務(wù)導(dǎo)致的卡頓占大多數(shù)。

新方案

目標(biāo)

針對現(xiàn)存播放器問題,重新設(shè)計了播放架構(gòu),以解決播放器上手成本高、不能方便插拔業(yè)務(wù)、復(fù)雜業(yè)務(wù)性能差的問題。

  • 對播放器進(jìn)行分層設(shè)計,將底層、框架層、業(yè)務(wù)層隔離開。
  • 重新設(shè)計業(yè)務(wù)層框架,降低業(yè)務(wù)耦合,真正實現(xiàn)業(yè)務(wù)可插拔的同時提升業(yè)務(wù)播放器整體性能。
  • 新框架將復(fù)雜邏輯封裝,提供友好的對外接口,使用簡便。

播放器架構(gòu)設(shè)計方案

架構(gòu)設(shè)計圖

圖片

架構(gòu)設(shè)計思路

  • 播放器整體將分為 3 層:極簡播放器、基礎(chǔ)播放器、業(yè)務(wù)播放器。
  • 極簡播放器:播放器最底層封裝,提供播放、監(jiān)控、播放狀態(tài)等,可以獨立播放視頻。
  • 基礎(chǔ)播放器:播放器基礎(chǔ)框架,提供播放,播控,監(jiān)控,上下文,任務(wù)管理,優(yōu)化等。
  • 業(yè)務(wù)播放器:最上層業(yè)務(wù)播放器,可以根據(jù)需要將業(yè)務(wù)進(jìn)行組合。
  • 新架構(gòu)各層之間耦合度低,極簡播放器、Context、DI 等模塊都可以獨立使用。
  • 將眾多業(yè)務(wù)進(jìn)行抽象,設(shè)計好生命周期,高內(nèi)聚低耦合,各業(yè)務(wù)之間互相獨立。
  • 將眾多業(yè)務(wù)任務(wù)化,在播放器框架層實現(xiàn)整體調(diào)度。
  • 采用 Module 的方式與業(yè)務(wù)交互,寫 Module 就像寫一個普通的 VC 一樣,上手成本低,也能與現(xiàn)有架構(gòu)進(jìn)行融合,實現(xiàn)最小限度影響業(yè)務(wù)。

播放器框架方案

Player

Player 是西瓜播放器的主容器,會封裝 AMPlayer、Context&DI、Interaction、Module、生命周期、任務(wù)調(diào)度、面板管理、品質(zhì)擴展等模塊,會對外提供使用接口。

  • AMPlayer:極簡(輕量)播放器,對播放流程進(jìn)行封裝,可以獨立播放視頻。
  • Context&DI:播放器狀態(tài)同步、服務(wù)解耦框架,可以高性能同步播放狀態(tài)&業(yè)務(wù)狀態(tài)。
  • Interaction:播放器交互層,提供了包括播控、手勢、Module 框架承載等能力。
  • Module:業(yè)務(wù)框架層,將業(yè)務(wù)按模塊進(jìn)行封裝,模塊之間相互獨立,模塊之間可插拔、可定制。
  • 生命周期:對播放器和 Module 分別劃分生命周期,定義好播放步驟。
  • 任務(wù)調(diào)度:對加載進(jìn)入播放器的業(yè)務(wù)進(jìn)行打散、延時非必要的模塊加載,優(yōu)先核心播放流程。
  • 面板管理:為業(yè)務(wù)彈框&面板提供的統(tǒng)一接口,可以供業(yè)務(wù)以播放器為基礎(chǔ)展示相關(guān)內(nèi)容。

AMPlayer

極簡播放器

  • 架構(gòu)設(shè)計圖

圖片

  • 核心邏輯圖

圖片

  • 基礎(chǔ)播放器提供了:
  • 播放源解析能力
  • 播放信息邏輯處理能力
  • 網(wǎng)絡(luò)代理能力
  • 播放器 Action、State 等管理能力
  • 預(yù)加載能力
  • 極簡播放器封裝播放底層,包括:Engine 封裝、播放狀態(tài)封裝、網(wǎng)絡(luò)封裝、播放信息封裝、預(yù)加載封裝、核心播放邏輯封裝、播放核心接口封裝。
  • 使用
/// 初始化播放信息
PlayerInfo *playerInfo = [PlayerInfo playerInfoWithVideoEngineModel:videoModel];
/// 初始化播放器
AMEnginePlayer *player = [[AMEnginePlayer alloc] init];
player.delegate = self;
[player setPlayerInfo:playerInfo];
[self addSubView:player.playerView];
player.playerView.frame = self.bounds;
[player play];

Context&DI

狀態(tài)同步&解依賴模塊

Context

  • Context 是狀態(tài)同步模塊,用來高效同步播放器的播放狀態(tài)、業(yè)務(wù)狀態(tài)。其底層是存儲在 Storage 中。

DI

  • 局部依賴注入框架,以 Context 為基礎(chǔ),對外提供宏綁定、宏鏈接等服務(wù)。
  • 通過 DI,可以對播放器內(nèi)部各服務(wù)進(jìn)行解依賴,只需要提供接口就可以使用。

存儲層設(shè)計圖

圖片

Interaction

播放器交互能力封裝,包含視圖管理、播控、手勢管理、Module 管理

視圖管理:ActionView

  • 視圖管理容器,提供根據(jù) ViewType 管理視圖層級的能力

圖片

ActionView 接口

@interface PlayerActionView : UIView

/// 添加視圖,并根據(jù)viewType插入到合適層級
/// @param view subview
/// @param viewType 視圖類型
- (void)addSubview:(UIView *)view viewType:(ViewType)viewType;

@end

播控視圖:PlayerControlView

播控結(jié)構(gòu)示意圖

  • 不同的業(yè)務(wù)場景,播控式樣雖然各不相同,結(jié)構(gòu)上基本都是劃分出一些布局區(qū)域,各個區(qū)域負(fù)責(zé)自身的布局。
  • 下圖結(jié)構(gòu)只是講解示例

圖片

相關(guān)定義介紹

播控視圖:ControlView

  • ControlView 是 ActionView 的一個子視圖。
  • ControlView 對外提供播控元素添加、視圖管理等能力。

播控元素及 ViewType

  • 每一個播控元素都要求有一個對應(yīng)的 ViewType,用于標(biāo)識該視圖的類型,便于 ControlView 對其管理。

播控區(qū)域布局容器:AreaView

  • 業(yè)務(wù)上可以根據(jù)實際情況,將播控劃分成一個或多個 AreaView。
  • 每一個 AreaView 聲明該區(qū)域支持的 ViewType 列表,并負(fù)責(zé)相關(guān)播控元素的布局。
  • ControlView 添加播控元素時,會根據(jù)其 ViewType 將其添加至對應(yīng) AreaView。
/// 布局容器視圖協(xié)議
@protocol PlayerControlItemContainerViewProtocol <NSObject>

@required
/// container支持的item類型順序列表
- (NSArray<ViewType> * _Nonnull)itemViewTypeList;

/// container屬于播控的哪一層
- (PlayerControlViewLayer)atLayer;

/// container在播控上的哪一區(qū)域
- (PlayerControlViewArea)atPosition;

/// 移除所有元素
- (NSArray<UIView *> *)removeAllItemViews;

@end

播控 Layer 定義:PlayerControlViewLayer

  • 播控上的每一個 AreaView 需要聲明其所屬 Layer,ControlView 根據(jù) Layer 信息控制 AreaView 的顯藏。
  • Layer 為 OPTIONS 類型,支持同時設(shè)置多個 Layer。
  • Layer 內(nèi)置定義如下,業(yè)務(wù)可根據(jù)自身情況進(jìn)一步擴展。
/// 播控分層定義
typedef NS_OPTIONS(NSUInteger, XX_Layer) {
XX_Layer_None = 0,
XX_Layer_NormalShow = 1 << 0, // 播控顯示層
XX_Layer_NormalHidden = 1 << 1, // 播控隱藏層
XX_Layer_LockShow = 1 << 2, // 播控鎖定顯示層
XX_Layer_LockHidden = 1 << 3, // 播控鎖定隱藏層
};

播控區(qū)域定義:PlayerControlViewArea

  • 播控上的每一個 AreaView 需要聲明其所在 Area,ControlView 根據(jù) area 信息對其管理(播控局部隱藏、播控局部淡化)
  • PlayerControlViewArea 為 OPTIONS 類型,支持同時設(shè)置多個 Area。
  • Area 內(nèi)置定義 Top、Left、Right、Bottom、Center,業(yè)務(wù)可根據(jù)自身情況進(jìn)一步擴展。
/// 播控區(qū)域劃分定義
typedef NS_OPTIONS(NSUInteger, XX_Area) {
XX_Area_None = 0,
XX_Area_Top = 1 << 0,
XX_Area_Left = 1 << 1,
XX_Area_Bottom = 1 << 2,
XX_Area_Right = 1 << 3,
XX_Area_Center = 1 << 4,
};

播控模版

  • 播控模版是對播控結(jié)構(gòu)的定義,業(yè)務(wù)根據(jù)實際情況劃分、定義 AreaView。
  • ControlView 通過切換模版來整體更新播控結(jié)構(gòu)。
// 播控模版定義
@protocol PlayerControlViewTemplate <NSObject>

@required
/// 播控模版中的布局容器
@property (nonatomic, copy, readonly) NSArray<UIView<PlayerControlItemContainerViewProtocol> *> *itemContainerViews;

/// 播控模版支持的浮動視圖(不需要布局容器承載)
@property (nonatomic, copy, readonly) NSArray<PlayerViewType> *supportFloatItemTypes;

/// 播控模版完成加載
- (void)controlViewDidLoadTemplate:(PlayerControlView *)controlView;

/// controlView添加Container中的itemView
/// @param controlView controlView description
/// @param itemView itemView description
/// @param viewType 視圖類型
- (void)controlView:(PlayerControlView *)controlView didAddItemview:(__kindof UIView *)itemView viewType:(ViewType)viewType;

/// controlView添加浮動子視圖
/// @param controlView controlView description
/// @param floatItemView 浮動子視圖
/// @param viewType 視圖類型
- (void)controlView:(PlayerControlView *)controlView didAddFloatItemview:(__kindof UIView *)floatItemView viewType:(ViewType)viewType;

/// 布局controlView中的容器視圖
- (void)controlViewLayoutItemContainerViews:(PlayerControlView *)controlView;

/// 播控模版完成卸載
- (void)controlViewDidUnloadTemplate:(PlayerControlView *)controlView;

@end

功能總結(jié)

  • 布局模版切換。
  • 顯示圖層切換。
  • 屏蔽指定類型視圖。
  • 隱藏指定區(qū)域視圖。
  • 半透明指定區(qū)域視圖。
@interface TTVPlayerControlView : UIView

@property (nonatomic, weak, nullable) id<PlayerControlViewDelegate> delegate;
/// 當(dāng)前播控模版
@property (nonatomic, strong, readonly) id<PlayerControlViewTemplate> controlTemplate;
/// 當(dāng)前展示層級
@property (nonatomic, assign, readonly) PlayerControlViewLayer currentLayer;


/// 更新播控模版
/// @param controlTemplateClass 新播控模版
- (BOOL)updateControlViewTemplate:(Class<PlayerControlViewTemplate>)controlTemplateClass;

/// 添加播控元素視圖,
- (void)addItemView:(UIView *)itemView viewType:(ViewType)viewType;

/// 切換展示層(備注:支持同時展示多個層,可靈活使用)
/// @param layer 播控層
/// @param duration 動畫時長,0表示無動畫
- (void)showLayer:(PlayerControlViewLayer)layer animateWithDuration:(CGFloat)duration;

/// 設(shè)置某一區(qū)域的alpha值,達(dá)到淡化某一區(qū)域的UI效果
/// @param alpha alpha description
/// @param position 作用位置
- (void)setAlpha:(CGFloat)alpha forPosition:(PlayerControlViewArea)position;

/// 屏蔽掉指定位置的布局容器視圖
/// @param positions 位置
/// @param key key description
- (void)setPositionsMask:(PlayerControlViewArea)positions forKey:(NSString *)key;


/// 獲取key對應(yīng)位置屏蔽信息
/// @param key key description
- (NSArray<ViewType> *)controlItemTypeMaskForKey:(NSString *)key;

/// 移除位置屏蔽
/// @param key key description
- (void)removePositionsMaskForKey:(NSString *)key;

/// 清除所有位置屏蔽
- (void)removeAllPositionsMask;

/// 屏蔽掉指定類型的ItemView(記得移除??????)
/// @param itemTypes 要屏蔽的item集合
/// @param key key
- (void)setItemTypeMask:(NSArray<ViewType> *)itemTypes forKey:(NSString *)key;

/// 移除ItemType屏蔽
- (void)removeItemTypeMaskForKey:(NSString *)key;

/// 清除掉所有ItemType屏蔽
- (void)removeAllItemTypeMask;

@end

中視頻播控的鎖定、顯隱控制實現(xiàn)

PlayerControlView 中只有 Layer 的概念,沒有鎖定、顯示、隱藏的概念,為了更好滿足中、長視頻中播控需求,內(nèi)置有PlayerControlViewModule(支持單擊手勢呼起、隱藏播控,播控自動隱藏,播控鎖定、解鎖等能力),業(yè)務(wù)可以選擇使用

/// 播控業(yè)務(wù)邏輯接口
@protocol PlayerControlViewInterface <NSObject>

/// 播控是否鎖定
- (BOOL)locked;

/// 鎖定/解鎖 播控
- (void)lockControlView:(BOOL)lock animation:(BOOL)animation;

/// 播控是否顯示
- (BOOL)isShowing;

/// 顯示/隱藏播控
- (void)showControlView:(BOOL)show animation:(BOOL)animation;

/// 是否可以自動隱藏
- (BOOL)canAutoHidden;

/// 禁止播控自動隱藏
- (void)disableAutoHiddenControl:(BOOL)disable forKey:(NSString *)key;

/// 自動隱藏重新計時
- (void)retimeAutoHiddenControl;

@end

手勢管理

  • 提供點擊、雙擊、拖動、長按、捏合五種常見手勢
  • 每一種手勢都支持多個訂閱者同時訂閱,每一個訂閱者都可以同時訂閱多種手勢手勢識別管理過程

圖片

  1. 手勢識別器接收到 touch 事件,詢問手勢管理器是否接收該 touch 事件 -[UIGestureRecognizerDelegate gestureRecognizer:shouldReceiveTouch:]
  2. 手勢管理器輪詢訂閱者是否要屏蔽該手勢 -[PlayerGestureHandlerProtocol gestureRecognizerShouldDisable:gestureType:]
  3. 手勢管理器返回 shouldReceiveTouch 結(jié)果:如果沒有響應(yīng)者,或存在任一響應(yīng)者禁用該手勢,則返回 NO(即該手勢識別器不進(jìn)行識別),反之 YES(即該手勢識別器繼續(xù)下一步識別)
  4. 手勢識別器詢問手勢管理器是否應(yīng)該開始 -[UIGestureRecognizerDelegate gestureRecognizer:gestureRecognizerShouldBegin:]
  5. 手勢管理器輪詢訂閱者是否有要響應(yīng)該手勢的, -[PlayerGestureHandlerProtocol gestureRecognizerShouldBegin:gestureType:]
  6. 當(dāng)存在多個相應(yīng)者時,根據(jù)相應(yīng)者的優(yōu)先級確認(rèn)一個最終相應(yīng)者 , -[PlayerGestureHandlerProtocol handlerPriorityForGestureType:]
  7. 手勢管理器保存本次手勢識別的響應(yīng)者
  8. 手勢管理器返回 ShouldBegin 結(jié)果:如果沒找到響應(yīng)者返回 NO(即手勢不進(jìn)行識別),反之 YES(手勢識別器正常進(jìn)行識別)
  9. 手勢識別器識別成功并觸發(fā) Action
  10. 手勢管理器通知響應(yīng)者手勢識狀態(tài)變化 -[PlayerGestureHandlerProtocol handleGestureRecognizer:gestureType:]
  11. 手勢識別結(jié)束,手勢管理器重置保存的響應(yīng)者

接口設(shè)計

@protocol PlayerGestureServiceInterface <NSObject>

/// 添加handler,響應(yīng)指定手勢類型
/// @param handler 手勢處理器
/// @param gestureType 手勢類型,可選多個手勢類型
- (void)addGestureHandler:(id<PlayerGestureHandlerProtocol>)handler forType:(GestureType)gestureType;

/// 刪除handler,只針對指定手勢類型
/// @param handler 手勢處理器
/// @param gestureType 手勢類型,可選多個手勢類型
- (void)removeGestureHandler:(id<PlayerGestureHandlerProtocol>)handler forType:(GestureType)gestureType;

/// 便利方法:刪除handler,gestureType = TTVGestureTypeAll
/// @param handler 手勢處理器
- (void)removeGestureHandler:(id<PlayerGestureHandlerProtocol>)handler;

/// 便利方法:屏蔽指定手勢類型
/// @param gestureType 手勢類型,可選多個手勢類型
/// @param scene 場景信息,方便異常調(diào)試
- (id<PlayerGestureHandlerProtocol>)disableGestureType:(GestureType)gestureType scene:(NSString *)scene;

@end

/// 手勢Hnadler協(xié)議
@protocol PlayerGestureHandlerProtocol <NSObject>
@optional
// 當(dāng)多個handler都可以相應(yīng)同一手勢時,需要根據(jù)優(yōu)先級選擇一個,默認(rèn)為0
- (NSInteger)handlerPriorityForGestureType:(GestureType)gestureType;

/// 是否禁止該手勢,默認(rèn)為NO
- (BOOL)gestureRecognizerShouldDisable:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;

/// 是否響應(yīng)該手勢,默認(rèn)為YES
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;

/// 手勢處理回調(diào)
- (void)handleGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;

@end

Module 管理

按照一定規(guī)則將播放器拆分成不同的模塊/插件(后續(xù)統(tǒng)稱模塊),模塊之間、模塊與底層播放器互相不耦合,支持業(yè)務(wù)插拔、靈活組裝、定制播放器模塊,并且單個播放器功能模塊功能收斂和隔離。

播放器模塊化架構(gòu)問題模型

實現(xiàn)播放器模塊化主要是解決下述問題:

  • 播放器功能模塊如何劃分
  • 播放器功能模塊之間如何交互
  • 獲取其它功能模塊狀態(tài) (例如獲取是否全屏、播控是否顯示等)。
  • 自己狀態(tài)改變,通知訂閱者 (例如顯示播控時隱藏 XX 視圖)。

播放器模塊和底層播放器交互

  • 調(diào)用底層播放器接口(例如 Seek、Play、Pause、Stop、切換清晰度、倍速等)。
  • 獲取播放器信息和狀態(tài)(例如播放狀態(tài)、Loading 狀態(tài)、readyforDisPlay 等)。
  • 監(jiān)聽播放器狀態(tài)改變。

業(yè)務(wù)和播放器模塊交互

  • 動態(tài)配置播放器模塊(例如打開或關(guān)閉重力感應(yīng)全屏)。
  • 傳遞播放器模塊需要的數(shù)據(jù)(例如播放標(biāo)題、作者信息等)。
  • 監(jiān)聽播放器模塊狀態(tài)變化。

播放器播控

  • 播放器模塊在播控上添加視圖、布局視圖 (例如進(jìn)度條模塊在播控上添加進(jìn)度條、時長信息 Lable 等)。
  • 業(yè)務(wù)在播控上添加視圖、布局視圖 (例如中視頻在播控上添加點贊、評論、彈幕、作者信息等)。
  • 業(yè)務(wù)獲取、定制、修改播放器模塊的 UI 視圖,控制播控視圖的顯示、隱藏、顯示順序等。

圖片

新架構(gòu) PlayerModule + PlayerContext

Module 的設(shè)計初衷是一個承載播放器功能模塊的容器,播放器的功能邏輯收斂在 Module 容器內(nèi)部,業(yè)務(wù)、播放器模塊、底層播放器之間采用低耦合的方式進(jìn)行交互,支持業(yè)務(wù)動態(tài)組合和插拔 Module。其中主要是為 Module 注入生命周期、狀態(tài)查詢和同步機制、綁定和獲取服務(wù)(依賴抽象接口)的能力。

PlayerContext

  • 通過 Key 發(fā)送通知 (類似 NSNotificationCenter)
  • 通過 Key 查詢狀態(tài) (存儲播放器所有狀態(tài)的一個大字典)
  • 通過協(xié)議綁定和獲取服務(wù)(DI)

新的 Module 架構(gòu)解決業(yè)務(wù)、播放器模塊、底層播放器交互解耦合:

  • 播放器功能模塊如何劃分 (建議按照功能,業(yè)務(wù)也可隨意劃分)
  • 播放器功能模塊之間如何交互
  • 不關(guān)心其它模塊而是只關(guān)心狀態(tài),通過 PlayerContext 查詢狀態(tài)。
  • 自己狀態(tài)改變,直接 PlayerContext 發(fā)送通知。
  • 需要調(diào)用其它功能模塊接口的,不顯示依賴其它模塊而是依賴抽象協(xié)議,通過 PlayerContext 獲取協(xié)議類,調(diào)用接口。

播放器模塊和底層播放器交互

  • 不直接依賴播放器,而是依賴播放器抽象接口服務(wù),通過 PlayerContext 獲取協(xié)議類。
  • 通過 PlayerContext 獲取播放器信息和狀態(tài)(例如播放狀態(tài)、loading 狀態(tài)、readyforDisPlay 等)。
  • 通過 PlayerContext 監(jiān)聽播放器狀態(tài)改變的通知。

業(yè)務(wù)和播放器模塊交互

  • 通過 PlayerContext 發(fā)送通知修改狀態(tài)來動態(tài)配置播放器模塊(例如打開或關(guān)閉重力感應(yīng)全屏)。
  • 業(yè)務(wù)不直接獲取播放器模塊傳遞數(shù)據(jù),而是播放器統(tǒng)一封裝數(shù)據(jù) Model,業(yè)務(wù)統(tǒng)一配置,模塊自己獲取需要的數(shù)據(jù)(例如播放標(biāo)題、作者信息、水印信息、互動貼紙信息等)。
  • 通過 PlayerContext 監(jiān)聽播放器模塊狀態(tài)變化通知。

播放器播控

  • 播放器模板更新時通知模塊在播控上添加視圖(例如 Seek 模塊在播控上添加進(jìn)度條、時長信息 Lable 等)。
  • 業(yè)務(wù)通過新增 Module 在播控上添加視圖、布局視圖 (例如中視頻在播控上添加點贊、評論、彈幕、作者信息等)。
  • 業(yè)務(wù)通過繼承 Player 內(nèi)部的 Module 重寫生成播控的方法來獲取、定制、修改播放器模塊的 UI 視圖。
  • 業(yè)務(wù)重寫播放器 UI 定制服務(wù)來定制、修改播控 UI。

Module 設(shè)計類圖:

圖片

  • PlayerModuleManager
  • 提供 Module 增刪查接口,持有所有 Module
  • 為 Module 注入 Context(Module 間通信、DI)
  • 為 Module 注入生命周期
@interface PlayerModuleManager : NSObject

#pragma mark - add && remove Module
- (void)addModule:(id<PlayerBaseModuleProtocol>)module;
- (void)removeModule:(id<PlayerBaseModuleProtocol>)module;
- (void)addModuleByClzz:(Class)clzz;
- (void)removeModuleByClzz:(Class)clzz;
- (void)removeAllModules;
#pragma mark - Data
// 設(shè)置Module數(shù)據(jù)
- (void)setupData:(id)data;

#pragma mark - Life cycle
// 播放器viewDidLoad
- (void)viewDidLoad;
// 模板更新,可以添加播控的回調(diào)
- (void)templateDidUpdate;

@end
  • PlayerBaseModule
  • 角色:播放器的功能拆分模塊,整體拆分為 MVC,作為 Controller 的角色,作為播放器模塊功能的一個容器
  • 提供通信和 DI 工具 PlayerContext
  • 提供類似 ViewController 的生命周期方法和常用播放器生命周期方法(適配中發(fā)現(xiàn)只需要 ViewDidLoad,刪除 ViewWillApper 等方法),UI 在內(nèi)部自己創(chuàng)建,并通過播放器 UI 服務(wù)綁定一個 key(每個播控對應(yīng)一個唯一的 Key),然后通過模板服務(wù)添加。添加到的位置是業(yè)務(wù)播控模板配置決定的
@interface PlayerBaseModule : NSObject
//播放器狀態(tài)通信、DI
@property (nonatomic, weak) PlayerContext *context;

#pragma mark - Life cycle
//Module加載(通過context注冊服務(wù))和添加播放器ViewDidLoad前的狀態(tài)監(jiān)聽
- (void)moduleDidLoad;
// 播放器viewDidLoad(添加狀態(tài)監(jiān)聽)
- (void)viewDidLoad;
// 模板更新,可以添加播控的回調(diào),在該回調(diào)方法中通過UI定制服務(wù)獲取或創(chuàng)建播控視圖
- (void)templateDidUpdate;
//Module移除(解除服務(wù)、移除監(jiān)聽、移除視圖等)
- (void)moduleDidUnLoad;

@end
  • PlayerUICustomizeInterface
  • 定制播放器 UI 的協(xié)議
/// 播放器播控視圖自定義服務(wù)協(xié)議
@protocol PlayerUICustomizeInterface <NSObject>
@required

/// 根據(jù)當(dāng)前播控模版,按需加載視圖
/// @param viewType 視圖類型
- (nullable __kindof UIView *)itemViewWithViewType:(ViewType)viewType;

/// 根據(jù)當(dāng)前播控模版,按需加載、更新試圖
/// @param view 入?yún)iew,view為nil時,嘗試構(gòu)建該類型視圖
/// @param viewType 是圖類型
/// @param loadViewBlock 當(dāng)入?yún)iew為nil,并重新新建視圖時回調(diào)該block
- (void)updateItemView:(nullable UIView *)view
viewType:(ViewType)viewType
loadViewBlock:(void(^ __nonnull)(__kindof UIView * __nonnull view))loadViewBlock;

/// 當(dāng)前播控模版是否支持該viewType
- (BOOL)isSupportedForTemplate:(ViewType)viewType;

@end

PlayerModule 偽代碼

  • 新建 PlayerXXModule 繼承自 PlayerBaseModule 或者直接實現(xiàn) PlayerBaseModuleProtocol 協(xié)議
  • 在 PlayerXXModule 根據(jù)需求定義屬性通過 PlayerContext 的 DI 獲取服務(wù)
  • 如果 Module 為外部提供接口調(diào)用服務(wù),則在 moduleDidLoad 綁定服務(wù)
  • 在 viewDidLoad 方法中添加狀態(tài)監(jiān)聽,根據(jù)狀態(tài)變化處理業(yè)務(wù)邏輯
  • 在 templateDidUpdate 通過 UI 定制服務(wù)獲取視圖,再通過 actionViewInterface 播控服務(wù)添加
  • 在 moduleDidUnLoad 方法中解綁服務(wù)和移除 UI 視圖

播放器生命周期

生命周期:整個播放器創(chuàng)建、播放、釋放、復(fù)用的周期流程,單次播放流程中只進(jìn)行一次

狀態(tài)變化:播放器播放狀態(tài)、視圖狀態(tài)的變化在單次生命周期中多次(頻繁)發(fā)生變化

圖片

  • 根據(jù)播放器的整個生命周期流程,播放器內(nèi)部功能可以通過簡單的注冊和回調(diào)在期望時機進(jìn)行執(zhí)行任務(wù)。

播放器異步加載

核心思想:優(yōu)先播放任務(wù),打散、延時非必要的模塊加載

圖片

PlayerModuleLoader 介紹

  • PlayerModuleLoader 是播放器內(nèi)置的模塊加載器,支持打散、異步加載模塊,業(yè)務(wù)選擇使用 CoreModules 核心模塊,播放器初始化后會立即加載所有的 Core Modules。
  • AsyncLoadModules 是允許異步加載的模塊,PlayerModuleLoader 在 viewDidLoad 時機,讀取 getAsyncLoadModules,然后開始執(zhí)行異步、打散加載。
  • 異步加載是在 NSDefaultRunLoopMode 模式下執(zhí)行,并且 App 進(jìn)入后臺后,會自動暫停加載。
/// 播放器內(nèi)置基礎(chǔ)ModuleLoader,支持Module異步打散加載
@interface PlayerModuleLoader : PlayerBaseModule

#pragma mark - Override Method

/// 核心模塊,會在moduleDidLoad時機同步加載
- (NSArray<id<PlayerBaseModuleProtocol>> *)getCoreModules;

/// 異步加載模塊,會在viewDidLoad時機開始異步加載
- (NSArray<id<PlayerBaseModuleProtocol>> *)getAsyncLoadModules;

#pragma mark - 添加、移除接口

/// 添加module
- (void)addModule:(id<PlayerBaseModuleProtocol>)module;

// 移除module
- (void)removeModule:(id<PlayerBaseModuleProtocol>)module;

@end

播放器面板管理

播放器作為核心的消費場景,各個業(yè)務(wù)模塊經(jīng)常需要以它為基礎(chǔ)展示面板、彈窗,有些不僅影響播放器還會影響其他業(yè)務(wù)面板,為了方便管理和感知類似的視圖,播放器提供了統(tǒng)一的面板展示接口。

接口定義

/// 展示panelView,已有panelview會被移除掉
/// @param panelView panelView
/// @param animations 自定義展示動畫
/// @param onClickMaskBlock 背景點擊block
- (void)showPanelView:(UIView *)panelView
animations:(PanelViewAnimations)animations
onClickMaskBlock:(PanelViewOnClickMaskBlock)onClickMaskBlock;

/// 展示panelView,已有panelview會被壓棧
- (void)pushPanelView:(UIView *)panelView
animations:(PanelViewAnimations)animations
onClickMaskBlock:(PanelViewOnClickMaskBlock)onClickMaskBlock;

/// 隱藏panelView,支持動畫
/// @param panelView panelView
/// @param animations 自定義消失動畫
- (void)dismissPanelView:(UIView *)panelView animations:(PanelViewAnimations)animations;

/// 移除panelView,無動畫
/// @param panelView panelView
- (void)removePanelView:(UIView *)panelView;

/// 移除所有的panelView
- (void)removeAllPanelViews;

業(yè)務(wù)適配

視頻業(yè)務(wù)

之前視頻業(yè)務(wù)的業(yè)務(wù)播放器是 PlayerViewController,業(yè)務(wù)邏輯分散在各個業(yè)務(wù) Part 和 PlayerViewController 的 Category 中,而且上層 Feed 業(yè)務(wù)、詳情頁業(yè)務(wù)都存在直接獲取底層播放器,直接使用 playerState、playerStore 的現(xiàn)象。

此次適配為新架構(gòu),主要需做以下事情:

  1. 將功能業(yè)務(wù)進(jìn)一步拆分、適配到業(yè)務(wù) Module 中。
  2. 將業(yè)務(wù)播放器 PlayerViewController 的 Category 中的功能邏輯收斂進(jìn)對應(yīng)的 Module 中。
  3. 將接口適配到新老架構(gòu)中,在業(yè)務(wù)播放器內(nèi)做 AB。
  4. 去除上層業(yè)務(wù)對底層播放器的的直接依賴,使用業(yè)務(wù)播放層的接口。

Module 加載

  1. 創(chuàng)建一個視頻業(yè)務(wù)的 moduleLoader 來管理中視頻播放器的 Module。
  2. 將 Seek、controlView 播控視圖、埋點等 9 個 Module 放在 coreModules 中,在播放器創(chuàng)建時就加載這些 Module,其余的功能 Module(20+)均放在 asyncModules 中,在播放器創(chuàng)建完之后再異步加載。

功能模塊適配為 Module

  1. 業(yè)務(wù)功能模塊適配為 Module,保證業(yè)務(wù)邏輯一致,僅對接口形式進(jìn)行變更。
  2. 對一些細(xì)粒度的功能模塊和邏輯直接合并進(jìn)相應(yīng) Module 中。

總結(jié)

本次重構(gòu)采用自下而上的方式進(jìn)行,從極簡播放器到播放框架、再到播放架構(gòu)在副場景驗證,進(jìn)而延伸到主場景,最終整體完成。

本次播放適配超 5 萬行代碼改動。通過 AB 實驗,本次演進(jìn)在業(yè)務(wù)、性能等方面均有不錯的收益,在人效方面也有很大的提升。

  • 業(yè)務(wù)收益:在播放 VV,播放時長,留存等方面均有不錯的表現(xiàn)。
  • 性能收益:卡頓、掉幀、首幀等方面均有很大的收益。
  • 架構(gòu)&效率:通過幾輪業(yè)務(wù)適配,新架構(gòu)擴展&維護簡便,上手成本也不高。
  • 針對不同業(yè)務(wù),Module 可以復(fù)用。(如不同體裁的適配,業(yè)務(wù)會有差異,可以通過復(fù)用 Module 來提高開發(fā)效率)
  • 校招同學(xué)適配小視頻業(yè)務(wù)時,只用了 2 人日就完成了適配。
責(zé)任編輯:未麗燕 來源: 字節(jié)跳動技術(shù)團隊
相關(guān)推薦

2022-08-16 17:37:06

視頻播放器鴻蒙

2015-09-01 16:48:44

ios暴風(fēng)視頻播放器

2015-05-21 15:25:42

VLC播放器

2011-07-20 16:21:20

iPhone 視頻 播放器

2023-07-10 18:44:18

開源播放器

2021-10-19 14:27:07

鴻蒙HarmonyOS應(yīng)用

2021-10-21 16:00:07

鴻蒙HarmonyOS應(yīng)用

2022-11-12 08:26:04

VLC視頻播放器裁剪視頻

2018-05-25 14:37:58

2023-03-29 09:32:15

視頻播放器應(yīng)用鴻蒙

2023-08-26 19:07:40

VLC旋轉(zhuǎn)視頻

2023-03-28 09:38:34

開發(fā)應(yīng)用鴻蒙

2023-03-28 09:44:02

開發(fā)應(yīng)用鴻蒙

2023-03-29 09:37:49

視頻播放器應(yīng)用鴻蒙

2023-03-06 16:20:08

視頻播放器VLC

2011-06-13 09:33:04

2011-05-09 15:17:24

亞馬遜iOS蘋果

2012-06-04 13:44:08

2021-08-30 07:49:32

Javascript西瓜視頻

2011-06-27 11:23:21

Qt 音樂播放器
點贊
收藏

51CTO技術(shù)棧公眾號