动手点关注 干货不迷路 👆
播放器是西瓜视频等视频类 App 最主要的业务场景,也是最主要的流量入口,其承载包括下层基础播放,上层的各种播放业务:状态栏、弹幕、音量、亮度、评论、点赞、进度、倍速、清晰度、选集、合集、商业化等。
西瓜对整个业务播放器做了整体抽象,提供了一套可插拔,可复用的播放器业务框架,包括:视频播放、播控交互、业务拓展。
本文播放器是指业务播放器,主要包括视频播放、播控交互、播放业务拓展,本播放器旨在提供一套完整的架构来包容播放器所有业务,实现播放业务可插拔。
Redux 架构介绍
Redux 三大原则
单一数据源
State 是只读的
Reducer 必须是一个纯函数
Middleware
- (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];
现存播放架构问题
通过线下性能分析,将播放流程中所有的耗时点、卡顿点整体归类,总结出卡顿耗时任务,其中 Redux 和播放业务导致的卡顿占大多数。
针对现存播放器问题,重新设计了播放架构,以解决播放器上手成本高、不能方便插拔业务、复杂业务性能差的问题。
架构设计思路
Player 是西瓜播放器的主容器,会封装 AMPlayer、Context&DI、Interaction、Module、生命周期、任务调度、面板管理、品质扩展等模块,会对外提供使用接口。
极简播放器
/// 初始化播放信息
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];
状态同步&解依赖模块
播放器交互能力封装,包含视图管理、播控、手势管理、Module 管理
视图管理:ActionView
ActionView 接口
@interface PlayerActionView : UIView
/// 添加视图,并根据viewType插入到合适层级
/// @param view subview
/// @param viewType 视图类型
- (void)addSubview:(UIView *)view viewType:(ViewType)viewType;
@end
播控视图:PlayerControlView
播控结构示意图
相关定义介绍
/// 布局容器视图协议
@protocol PlayerControlItemContainerViewProtocol <NSObject>
@required
/// container支持的item类型顺序列表
- (NSArray<ViewType> * _Nonnull)itemViewTypeList;
/// container属于播控的哪一层
- (PlayerControlViewLayer)atLayer;
/// container在播控上的哪一区域
- (PlayerControlViewArea)atPosition;
/// 移除所有元素
- (NSArray<UIView *> *)removeAllItemViews;
@end
/// 播控分层定义
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, // 播控锁定隐藏层
};
/// 播控区域划分定义
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,
};
// 播控模版定义
@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
功能总结
@interface TTVPlayerControlView : UIView
@property (nonatomic, weak, nullable) id<PlayerControlViewDelegate> delegate;
/// 当前播控模版
@property (nonatomic, strong, readonly) id<PlayerControlViewTemplate> controlTemplate;
/// 当前展示层级
@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;
/// 设置某一区域的alpha值,达到淡化某一区域的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对应位置屏蔽信息
/// @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
中视频播控的锁定、显隐控制实现
PlayerControlView 中只有 Layer 的概念,没有锁定、显示、隐藏的概念,为了更好满足中、长视频中播控需求,内置有PlayerControlViewModule(支持单击手势呼起、隐藏播控,播控自动隐藏,播控锁定、解锁等能力),业务可以选择使用
/// 播控业务逻辑接口
@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
手势管理
接口设计
@protocol PlayerGestureServiceInterface <NSObject>
/// 添加handler,响应指定手势类型
/// @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 场景信息,方便异常调试
- (id<PlayerGestureHandlerProtocol>)disableGestureType:(GestureType)gestureType scene:(NSString *)scene;
@end
/// 手势Hnadler协议
@protocol PlayerGestureHandlerProtocol <NSObject>
@optional
// 当多个handler都可以相应同一手势时,需要根据优先级选择一个,默认为0
- (NSInteger)handlerPriorityForGestureType:(GestureType)gestureType;
/// 是否禁止该手势,默认为NO
- (BOOL)gestureRecognizerShouldDisable:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;
/// 是否响应该手势,默认为YES
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;
/// 手势处理回调
- (void)handleGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;
@end
Module 管理
按照一定规则将播放器拆分成不同的模块/插件(后续统称模块),模块之间、模块与底层播放器互相不耦合,支持业务插拔、灵活组装、定制播放器模块,并且单个播放器功能模块功能收敛和隔离。
播放器模块化架构问题模型
实现播放器模块化主要是解决下述问题:
新架构 PlayerModule + PlayerContext
Module 的设计初衷是一个承载播放器功能模块的容器,播放器的功能逻辑收敛在 Module 容器内部,业务、播放器模块、底层播放器之间采用低耦合的方式进行交互,支持业务动态组合和插拔 Module。其中主要是为 Module 注入生命周期、状态查询和同步机制、绑定和获取服务(依赖抽象接口)的能力。
PlayerContext
新的 Module 架构解决业务、播放器模块、底层播放器交互解耦合:
播放器功能模块如何划分 (建议按照功能,业务也可随意划分)
播放器功能模块之间如何交互
播放器模块和底层播放器交互
业务和播放器模块交互
播放器播控
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
// 设置Module数据
- (void)setupData:(id)data;
#pragma mark - Life cycle
// 播放器viewDidLoad
- (void)viewDidLoad;
// 模板更新,可以添加播控的回调
- (void)templateDidUpdate;
@end
@interface PlayerBaseModule : NSObject
//播放器状态通信、DI
@property (nonatomic, weak) PlayerContext *context;
#pragma mark - Life cycle
//Module加载(通过context注册服务)和添加播放器ViewDidLoad前的状态监听
- (void)moduleDidLoad;
// 播放器viewDidLoad(添加状态监听)
- (void)viewDidLoad;
// 模板更新,可以添加播控的回调,在该回调方法中通过UI定制服务获取或创建播控视图
- (void)templateDidUpdate;
//Module移除(解除服务、移除监听、移除视图等)
- (void)moduleDidUnLoad;
@end
/// 播放器播控视图自定义服务协议
@protocol PlayerUICustomizeInterface <NSObject>
@required
/// 根据当前播控模版,按需加载视图
/// @param viewType 视图类型
- (nullable __kindof UIView *)itemViewWithViewType:(ViewType)viewType;
/// 根据当前播控模版,按需加载、更新试图
/// @param view 入参view,view为nil时,尝试构建该类型视图
/// @param viewType 是图类型
/// @param loadViewBlock 当入参view为nil,并重新新建视图时回调该block
- (void)updateItemView:(nullable UIView *)view
viewType:(ViewType)viewType
loadViewBlock:(void(^ __nonnull)(__kindof UIView * __nonnull view))loadViewBlock;
/// 当前播控模版是否支持该viewType
- (BOOL)isSupportedForTemplate:(ViewType)viewType;
@end
PlayerModule 伪代码
生命周期:整个播放器创建、播放、释放、复用的周期流程,单次播放流程中只进行一次
状态变化:播放器播放状态、视图状态的变化在单次生命周期中多次(频繁)发生变化
核心思想:优先播放任务,打散、延时非必要的模块加载
PlayerModuleLoader 介绍
/// 播放器内置基础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
播放器作为核心的消费场景,各个业务模块经常需要以它为基础展示面板、弹窗,有些不仅影响播放器还会影响其他业务面板,为了方便管理和感知类似的视图,播放器提供了统一的面板展示接口。
/// 展示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;
之前视频业务的业务播放器是 PlayerViewController,业务逻辑分散在各个业务 Part 和 PlayerViewController 的 Category 中,而且上层 Feed 业务、详情页业务都存在直接获取底层播放器,直接使用 playerState、playerStore 的现象。
此次适配为新架构,主要需做以下事情:
Module 加载
功能模块适配为 Module
本次重构采用自下而上的方式进行,从极简播放器到播放框架、再到播放架构在副场景验证,进而延伸到主场景,最终整体完成。
本次播放适配超 5 万行代码改动。通过 AB 实验,本次演进在业务、性能等方面均有不错的收益,在人效方面也有很大的提升。
西瓜视频急招 iOS/Android 研发工程师,工作地点:北京、杭州、上海可联系 WX:Darwin_x1,更多招聘详细介绍点击原文链接。