本文为您详细介绍 PlayerKit 的架构、核心模块以及核心功能的使用方式。
PlayerKit 包含 SceneKit、PlayerModule 和 PlayerCore 三个核心模块,如下图所示:
各模块的详细介绍如下表:
模块 | 说明 |
---|---|
SceneKit | 场景层,基于 PlayerCore 和 PlayerModule 实现不同的播放场景。 |
PlayerModule | 核心开发层,播控相关的开发都在这一层,播放控件都可以是一个 PlayerModule ,业务跟播放相关组件也可以设计为一个 PlayerModule,例如支付、短剧一些显示 UI 等,播放控件层在不同场景内置常用的 PlayerModule 方便业务的使用。 |
PlayerCore | 播放器 SDK 核心封装层。一般情况下,您无需进行修改。 |
Context 模块提供基础消息通信功能,允许播放器实例与模块之间、模块与模块之间通过 key-value 方式实现消息的发送与监听,从而达成播放器状态等消息的同步。该模块使用简便,具体实现可参考以下代码:
// 发送状态变更 [self.context post:@(self.playbackState) forKey:VEPlayerContextKeyPlaybackState]; // 监听状态变更 [self.context addKey:VEPlayerContextKeyPlaybackState withObserver:self handler:^(id _Nullable object, NSString *key) { if (self.playerInterface.playbackState == VEVideoPlaybackStatePlaying) { [self _startTimer]; } else { [self _invalidateTimer]; } }];
DI 模块依托 Context 模块,提供了三个宏定义用于绑定和链接服务。通过 DI 链接的服务可直接使用,从而实现各模块间的解耦。
// VEPlayerInteraction: 初始化绑定 3 个服务 /// _playerModuleManager: 负责所有 Module 的管理 /// _gestureService: 负责处理所有的手势 /// _playerViewService: 负责处理 UI 视图展示 - (instancetype)initWithContext:(VEPlayerContext *)context { self = [super init]; if (self) { _context = context; _playerModuleManager = [[VEPlayerModuleManager alloc] initWithPlayerContext:context]; VEPlayerContextDIBind(_playerModuleManager, VEPlayerModuleManagerInterface, self.context); _gestureService = [[VEPlayerGestureService alloc] init]; VEPlayerContextDIBind(_gestureService, VEPlayerGestureServiceInterface, self.context); _playerViewService = [[VEPlayerViewService alloc] init]; _playerViewService.moduleManager = _playerModuleManager; VEPlayerContextDIBind(_playerViewService, VEPlayerActionViewInterface, self.context); } return self; } // 自定义 PlayerModule @implementation VEPlayerSeekModule // 在 Moudule 组件 Link 所需的服务 VEPlayerContextDILink(playerInterface, VEVideoPlayback, self.context); VEPlayerContextDILink(gestureService, VEPlayerGestureServiceInterface, self.context); VEPlayerContextDILink(actionViewInterface, VEPlayerActionViewInterface, self.context); @end
播放控件默认提供了经过内部积累、适用于各种不同场景的 UI 布局视图,并采用懒加载方式按需创建,供业务使用,基本能够满足大多数业务的使用场景。
视图 | 说明 |
---|---|
playerContainerView | 播放器的父容器视图。若当前组件超出播放器视图范围,可利用播放器的父视图进行布局。 |
actionView | UI 控件的父容器视图,负责管理上层的 controlView。若当前的 controlView 视图无法满足业务需求,可自定义 controlView 并将其添加至 actionView。 |
underlayControlView | 在 playbackControlView 下方、始终展示的视图,例如弹幕、上下遮罩等可放置于此视图。 |
playbackControlView | 播放控制层,所有具备控制播放器功能的元素均被称为“control”,模块中具备控制功能的控件将被添加至该视图,负责管控整体控件的显示与隐藏。 |
playbackLockControlView | 锁屏状态下的控制视图,在锁屏时不会消失。 |
overlayControlView | 在 playbackControlView 上方、始终展示的视图,例如各类提示信息和加载状态可放置于此视图。 |
说明
playerContainerView 和 actionView 默认创建,其他显示视图以懒加载方式按需创建。
播放 UI 层级示意图如下:
如前文所述,使用播放控件进行开发的关键在于依据业务需求开发所需的 UI。若默认提供的播放控件无法完全满足您的业务需求,您可自定义开发播放控件。本节以 PlayerModule 为例,详细介绍如何进行播放控件的自定义开发。
定义所需的播控服务对象和 UI 控件:
// 播放器实例 @property (nonatomic, weak) id<VEVideoPlayback> playerInterface; // 播控手势实例,可以添加 单击、双击、拖拽收拾 @property (nonatomic, weak) id<VEPlayerGestureServiceInterface> gestureService; // 播放视图的实例,自定义 UI 控件可以方便添加到播放视图上 @property (nonatomic, weak) id<VEPlayerActionViewInterface> actionViewInterface; // 自定义 UI 控件 @property (nonatomic, strong) UIButton *playButton;
链接播控服务对象:
VEPlayerContextDILink(playerInterface, VEVideoPlayback, self.context); VEPlayerContextDILink(gestureService, VEPlayerGestureServiceInterface, self.context); VEPlayerContextDILink(actionViewInterface, VEPlayerActionViewInterface, self.context);
绑定播放监听:
// 监听播放 Action,修改播放按钮显示状态 // Demo 中内置了常用 Action 事件给业务使用,业务也可以根据自身需求新增 Action ,也非常简单具体可以参考任意一个 Action @weakify(self); [self.context addKey:VEPlayerContextKeyPlayAction withObserver:self handler:^(id _Nullable object, NSString *key) { @strongify(self); if (object) { [self updatePlayButtonWithPlayState:YES]; } }];
处理手势事件:
// 播放控件封装手机操作,业务使用非常简单 // 1. 添加需要的手势操作,例如这里需要一个点击播放页面的手势操作 [self.gestureService addGestureHandler:self forType:VEGestureType_SingleTap]; // 2. 实现手势回调,并在回调中实现自身业务逻辑 // 注意 PlayerMoudle 需要实现 <VEPlayerGestureHandlerProtocol> 协议 - (void)handleGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer gestureType:(VEGestureType)gestureType { VEVideoPlaybackState playbackState = (VEVideoPlaybackState)[self.context integerForHandlerKey:VEPlayerContextKeyPlaybackState]; if (playbackState == VEVideoPlaybackStatePlaying) { [self.playerInterface pause]; } else if (playbackState == VEVideoPlaybackStatePaused) { [self.playerInterface play]; } [self.context post:@(self.playerInterface.playbackState) forKey:VEPlayerContextKeyPlayButtonSingleTap]; }
实现 PlayerModue 生命周期函数:
#pragma mark - Life Cycle // module 加载完成 - (void)moduleDidLoad { [super moduleDidLoad]; } // 该方法跟 UIViewController viewDidLoad 类似,可以在这里添加监听、手势、处理UI等操作 - (void)viewDidLoad { [super viewDidLoad]; [self configuratoinCustomView]; [self.gestureService addGestureHandler:self forType:VEGestureType_SingleTap]; @weakify(self); [self.context addKey:VEPlayerContextKeyPlayAction withObserver:self handler:^(id _Nullable object, NSString *key) { @strongify(self); if (object) { [self updatePlayButtonWithPlayState:YES]; } }]; } // 处理模板,业务可不关注该方法 - (void)controlViewTemplateDidUpdate { [super controlViewTemplateDidUpdate]; } // module 卸载,这个方法里释放坚挺、手势、移除UI等操作 - (void)moduleDidUnLoad { [super moduleDidUnLoad]; [self.gestureService removeGestureHandler:self]; [self.context removeHandlersForObserver:self]; if (self.playButton) { [self.playButton removeFromSuperview]; self.playButton = nil; } }
通过 PlayerModule 中所链接的 actionViewInterface 实例对播放器上的 UI 进行操作。当前的播放 UI 应添加至 playbackControlView
的子视图中。以下示例展示了如何将播放按钮添加到 playbackControlView
视图,并使用常用的布局组件进行布局:
- (void)configuratoinCustomView { [self.actionViewInterface.playbackControlView addSubview:self.playButton]; [self.playButton mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.actionViewInterface.playbackControlView); }]; }
通过 PlayerModuleLoader 加载自定义的 PlayerModule:
@interface SimplePlayerKitPlayerModuleLoader : VEPlayerBaseModuleLoader @end @implementation SimplePlayerKitPlayerModuleLoader - (NSArray<id<VEPlayerBaseModuleProtocol>> *)getCoreModules { NSMutableArray *coreModules = [NSMutableArray array]; [coreModules addObject:[VEPlayerLoadingModule new]]; [coreModules addObject:[VEPlayerSeekModule new]]; [coreModules addObject:[VEPlayerSeekProgressModule new]]; [coreModules addObject:[VEPlayerPlayModule new]]; // ... 其它 player module return coreModules; }
TTVideoEngineMediaSource 类是 PlayerKit 中对播放源的定义。支持 Vid、DirectUrl 和 VideoModel 三种播放源,您可根据自身业务需求,选择创建相应的播放源。
如果您已将视频上传至火山引擎视频点播服务,可使用 Vid 方式播放视频。您需要将 vid 参数设为视频点播服务生成的 Vid,将 playAuthToken 参数设为临时播放 Token。Vid 和临时播放 Token 由应用服务端下发的。应用客户端无需关心,调用应用服务端的接口获取即可,详情请见通过临时播放 Token 播放。
与 DirectUrl 和 VideoModel 播放源相比,使用 Vid 播放源时,SDK 内部会发起 HTTP 请求,调用 GetPlayInfo 接口以获取 VideoModel,这会增加视频首帧的耗时。Vid 播放源支持多档位切换,您需指定一个起播分辨率。
- (id<TTVideoEngineMediaSource>)createVidMediaSource { NSString *vid = @"your video id"; NSString *playAuthToken = @"your video id's playAuthToken"; // 播放档位选择 TTVideoEngineResolutionType resolution = TTVideoEngineResolutionTypeFullHD; TTVideoEngineVidSource *vidSource = [[TTVideoEngineVidSource alloc] initWithVid:vid playAuthToken:playAuthToken resolution:resolution]; return vidSource; }
若采用 DirectUrl 方式播放视频,需将 url 参数设定为视频播放地址。该播放地址可以是第三方点播地址,也可以是视频点播服务生成的播放地址。url 可设置为数组形式,支持主、备用 URL 切换。此类播放源不支持多档位切换。
此外,针对 DirectUrl 播放源,还需设置 cacheKey 参数。cacheKey 是本地播放缓存的唯一标识。若同一播放地址的 cacheKey 发生更改,本地缓存将无法进行正确索引,从而导致重新加载数据,造成流量浪费。建议对播放 URL 中的固定部分进行 MD5 加密,并将加密结果作为 cacheKey。
- (id<TTVideoEngineMediaSource>)createUrlMediaSource { NSString *url = @"your play url"; // 一般使用 url 去掉时间戳的 path 部分作为缓存 key NSString *cacheKey = [url btd_md5String]; TTVideoEngineUrlSource *urlSrouce = [[TTVideoEngineUrlSource alloc] initWithUrl:url cacheKey:cacheKey]; return urlSrouce; }
使用 VideoModel 播放源可避免因调用 GetPlayInfo 接口带来的网络请求,缩短视频首帧耗时。Vid 播放源支持多档位切换。当前 PlayerKit 未提供 VideoModel 类作为播放源构造方式,开发者需依据 GetPlayInfo 返回参数的 JSON 结构手动构建 VideoModel 播放源。
- (id<TTVideoEngineMediaSource>)createVideoModelMediaSource { NSString *vid = @"your video id"; NSString *videoModelJson = @"your video model json"; // 播放档位选择 TTVideoEngineResolutionType resolution = TTVideoEngineResolutionTypeFullHD; TTVideoEngineModel *engineModel = [TTVideoEngineModel videoModelWithMediaJsonString:videoModelJson]; TTVideoEngineVideoModelSource *videoModelSource = [[TTVideoEngineVideoModelSource alloc] initWithVid:vid resolution:resolution videoModel:engineModel]; return videoModelSource; }
通常情况下,一个播放场景需要创建一个 ModuleLoader。ModuleLoader 负责绑定并加载该场景下的所有播放控件。以下示例代码展示了播放器 ModuleLoader 绑定并加载了加载状态提示(loading)、进度条和播放按钮这三个基础播放控件:
@interface SimplePlayerKitPlayerModuleLoader : VEPlayerBaseModuleLoader @end @implementation SimplePlayerKitPlayerModuleLoader - (NSArray<id<VEPlayerBaseModuleProtocol>> *)getCoreModules { NSMutableArray *coreModules = [NSMutableArray array]; [coreModules addObject:[VEPlayerLoadingModule new]]; [coreModules addObject:[VEPlayerSeekModule new]]; [coreModules addObject:[VEPlayerPlayModule new]]; // ... 其它 player module return coreModules; }
您可设置播放器的初始化状态,例如显示模式、是否静音、是否循环播放等。
@interface VEVideoPlayerConfiguration : NSObject // 播放器默认显示模式, 默认值 VEVideoViewModeAspectFill @property (nonatomic, assign) VEVideoViewMode videoViewMode; // 是否音频模式播放, 默认值 NO @property (nonatomic, assign) BOOL audioMode; // 是否静音播放, 默认值 NO @property (nonatomic, assign) BOOL muted; // 是否循环播放, 默认值 NO @property (nonatomic, assign) BOOL looping; // 播放倍速, 默认值 1.0 @property (nonatomic, assign) CGFloat playbackRate; // 起播时间点, 默认值 0 @property (nonatomic, assign) NSTimeInterval startTime; // 是否开启H.265播放,H.265播放源开启, 默认关闭 @property (nonatomic, assign) BOOL isH265; // 是否开启硬解, 默认开启 @property (nonatomic, assign) BOOL isOpenHardware; // 是否开启超分, 默认关闭 @property (nonatomic, assign) BOOL isOpenSR; + (VEVideoPlayerConfiguration *)defaultPlayerConfiguration; @end
// 1. 初始化播放器实例 /// 初始化播放器配置 VEVideoPlayerConfiguration *playerConfiguration = [VEVideoPlayerConfiguration defaultPlayerConfiguration]; /// 初始化 modules loader _moduleLoader = [[SimplePlayerKitPlayerModuleLoader alloc] init]; /// 初始化播放器 _playerController = [[VEVideoPlayerController alloc] initWithConfiguration:playerConfiguration moduleLoader:_moduleLoader]; // 2. 添加播放视图到显示视图 [self.view addSubview:self.playerController.view]; [self.playerController.view mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.equalTo(self.view); make.bottom.equalTo(self.view).with.offset(-84); }]; // 3. 设置播放源 id<TTVideoEngineMediaSource> urlSource = [self createUrlMediaSource]; [self.playerController setMediaSource:urlSource]; // 4. 开始播放 [self.playerController play];
VEVideoPlayerConfiguration *playerConfiguration = [VEVideoPlayerConfiguration defaultPlayerConfiguration]; playerConfiguration.startTime = 10.0; // 10 秒起播
// 使用预渲染需调用该方法,调用后会触发首帧渲染 // 正常播放直接调用 play 方法即可,无需调用该方法 [self.playerController prepareToPlay];
[self.playerController play];
[self.playerController pause];
// seek 到 20 秒 [self.playerController seekToTime:20 complete:^(BOOL success) { // seek 完成 } renderComplete:^{ // seek 后渲染完成 }];
[self.playerController close];
// 单位秒 NSTimeInterval duration = [self.playerController duration];
// 单位秒 NSTimeInterval currentPlaybackTime = [self.playerController currentPlaybackTime];
// 单位秒 NSTimeInterval playableDuration = [self.playerController playableDuration];
// 取值范围 [0, 1],建议使用系统音量方法进行调节。 self.playerController.playbackVolume = 1;
// YES 静音;NO 取消静音 self.playerController.muted = YES;
// 取值范围 (0, 3] self.playerController.playbackRate = 1;
// YES 开启循环播放;NO 关闭循环播放 self.playerController.looping = YES;
@protocol VEVideoPlaybackDelegate <NSObject> @optional /** * @brief 播放器准备完成回调 * @param player 播放实例 */ - (void)videoPlayerPrepared:(id<VEVideoPlayback> _Nullable)player; /** * @brief 播放器首帧回调 * @param player 播放实例 */ - (void)videoPlayerReadyToDisplay:(id<VEVideoPlayback> _Nullable)player; /** * @brief 播放器播放状态改变回调 * @param player 播放实例 * @param state 播放器加载状态 */ - (void)videoPlayer:(id<VEVideoPlayback> _Nullable)player loadStateDidChange:(VEVideoLoadState)state; /** * @brief 播放器加载状态改变回调 * @param player 播放实例 * @param state 播放器播放状态 */ - (void)videoPlayer:(id<VEVideoPlayback> _Nullable)player playbackStateDidChange:(VEVideoPlaybackState)state; /** * @brief 播放器播放结束回调 * @param player 播放实例 * @param finishStatus 播放器播放结束原因 */ - (void)videoPlayer:(id<VEVideoPlayback> _Nullable)player didFinishedWithStatus:(VEPlayFinishStatus *_Nullable)finishStatus; /** * @brief 播放器播放是否命中缓存的回调 * @param player 播放实例 * @param dataSize 命中缓存的大小 */ - (void)videoPlayer:(id<VEVideoPlayback> _Nullable)player key:(NSString * _Nullable)key hitVideoPreloadDataSize:(NSInteger)dataSize; /** * @brief 播放器切换分辨率档位的回调 * @param player 播放实例 * @param resolution 切换后的分辨率 * @param bitrate 切换后的码率 */ - (void)videoPlayerBitrateDidChange:(id<VEVideoPlayback> _Nullable)player resolution:(TTVideoEngineResolutionType)resolution bitrate:(NSInteger)bitrate; /** * @brief 播放器播放视图宽高改变的回调 * @param player 播放实例 * @param videoWidth 播放器显示视图宽 * @param videoHeight 播放器显示视图高 */ - (void)videoPlayerViewSizeDidChange:(id<VEVideoPlayback> _Nullable)player videoWidth:(NSInteger)videoWidth videoHeight:(NSInteger)videoHeight; @end