功能齐全的视频播放器-SJVideoPlayer基本介绍和使用

Installation

# Player with default control layer.
pod 'SJVideoPlayer'
# The base player, without the control layer, can be used if you need a custom control layer. 需要自己定义播放UI时使用
pod 'SJBaseVideoPlayer'

天朝

# 如果网络不行安装不了, 可改成以下方式进行安装
pod 'SJBaseVideoPlayer', :git => 'https://gitee.com/changsanjiang/SJBaseVideoPlayer.git'
pod 'SJVideoPlayer', :git => 'https://gitee.com/changsanjiang/SJVideoPlayer.git'
pod 'SJUIKit/AttributesFactory', :git => 'https://gitee.com/changsanjiang/SJUIKit.git'
pod 'SJUIKit/ObserverHelper', :git => 'https://gitee.com/changsanjiang/SJUIKit.git'
pod 'SJUIKit/Queues', :git => 'https://gitee.com/changsanjiang/SJUIKit.git'
$ pod update --no-repo-update   (不要用 pod install 了, 用这个命令安装)

Example 使用示例

_player = [SJVideoPlayer player];
_player.view.frame = CGRectMake(0, 0, 200, 200);
[self.view addSubview:_player.view];
// 设置资源进行播放
_player.URLAsset = [[SJVideoPlayerURLAsset alloc] initWithURL:URL];
... 等等, 更多设置, 请查看头文件. 相应功能均为懒加载, 用到时才会创建. 

Documents

1. 视图层次结构
2. URLAsset
3. 播放控制
4. 控制层的显示和隐藏
5. 设备亮度和音量
6. 旋转
7. 直接全屏而不旋转
8. 镜像翻转
9. 网络状态
10. 手势
11. 占位图
12. 显示提示文本
13. 一些固定代码
14. 截屏
15. 导出视频或GIF
16. 滚动相关
17. 自动播放 - 在 UICollectionView 或者 UITableView 中
18. 对控制层上的Item的操作
19. 对控制层上的Item的一些补充
20. SJEdgeControlLayer 的补充

以下为详细介绍:

1.1 UIView
1.1 UIView
_player = [SJVideoPlayer player];
_player.view.frame = ...;
[self.view addSubview:_player.view];
// 设置资源进行播放
SJVideoPlayerURLAsset *asset = [[SJVideoPlayerURLAsset alloc] initWithURL:URL];
_player.URLAsset = asset;

在普通视图中播放时, 不需要指定视图层次, 直接创建资源进行播放即可.

1.2 UITableView 中的层次结构

由于 UITableView 及 UICollectionView 的复用机制, 会导致播放器视图显示在错误的位置上, 为防止出现此种情况, 在创建资源时指定视图层次结构, 使得播放器能够定位具体的父视图, 依此来控制隐藏与显示.

1.2.1 UITableViewCell
--  UITableView
--  UITableViewCell
--  Player.superview
--  Player.view
_player = [SJVideoPlayer player];

UIView *playerSuperview = cell.coverImageView;
SJPlayModel *playModel = [SJPlayModel UITableViewCellPlayModelWithPlayerSuperviewTag:playerSuperview.tag atIndexPath:indexPath tableView:self.tableView];
_player.URLAsset = [[SJVideoPlayerURLAsset alloc] initWithURL:URL playModel:playModel];

在 UITableViewCell 中播放时, 需指定 Cell 所处的 indexPath 以及播放器父视图的 tag.
在滑动时, 管理类将会通过这两个参数控制播放器父视图的显示与隐藏.

1.2.2 UITableView.tableHeaderView
--  UITableView
--  UITableView.tableHeaderView 或者 UITableView.tableFooterView  
--  Player.superview
--  Player.view
UIView *playerSuperview = self.tableView.tableHeaderView;
// 也可以设置子视图
// playerSuperview = self.tableView.tableHeaderView.coverImageView;
SJPlayModel *playModel = [SJPlayModel UITableViewHeaderViewPlayModelWithPlayerSuperview:playerSuperview tableView:self.tableView];
1.2.4 UITableViewHeaderFooterView
--  UITableView
--  UITableViewHeaderFooterView 
--  Player.superview
--  Player.view            
/// isHeader: 当在header中播放时, 传YES, 在footer时, 传NO.
SJPlayModel *playModel = [SJPlayModel UITableViewHeaderFooterViewPlayModelWithPlayerSuperviewTag:sectionHeaderView.coverImageView.tag inSection:section isHeader:YES tableView:self.tableView];

1.3 UICollectionView 中的层次结构

1.3.1 UICollectionViewCell
--  UICollectionView
--  UICollectionViewCell
--  Player.superview
--  Player.view
SJPlayModel *playModel = [SJPlayModel UICollectionViewCellPlayModelWithPlayerSuperviewTag:cell.coverImageView.tag atIndexPath:indexPath collectionView:self.collectionView];

1.4 嵌套时的视图层次

嵌套的情况下, 传递的参数比较多, 不过熟悉了前面的套路, 下面的这些也不成问题. (会被复用的视图, 传 tag. 如果不会被复用, 则直接传视图)

1.4.1 UICollectionView 嵌套在 UITableViewCell 中
--  UITableView
--  UITableViewCell
--  UITableViewCell.UICollectionView
--  UICollectionViewCell
--  Player.superview
--  Player.view
SJPlayModel *playModel = [SJPlayModel UICollectionViewNestedInUITableViewCellPlayModelWithPlayerSuperviewTag:collectionViewCell.coverImageView.tag atIndexPath:collectionViewCellAtIndexPath collectionViewTag:tableViewCell.collectionView.tag collectionViewAtIndexPath:tableViewCellAtIndexPath tableView:self.tableView];
1.4.2 UICollectionView 嵌套在 UITableViewHeaderView 中
--  UITableView
--  UITableView.tableHeaderView 或者 UITableView.tableFooterView  
--  tableHeaderView.UICollectionView
--  UICollectionViewCell
--  Player.superview
--  Player.view
SJPlayModel *playModel = [SJPlayModel UICollectionViewNestedInUITableViewHeaderViewPlayModelWithPlayerSuperviewTag:cell.coverImageView.tag atIndexPath:indexPath collectionView:tableHeaderView.collectionView tableView:self.tableView];
1.4.3 UICollectionView 嵌套在 UICollectionViewCell 中
--  UICollectionView
--  UICollectionViewCell
--  UICollectionViewCell.UICollectionView
--  UICollectionViewCell
--  Player.superview
--  Player.view
SJPlayModel *playModel = [SJPlayModel UICollectionViewNestedInUICollectionViewCellPlayModelWithPlayerSuperviewTag:collectionViewCell.coverImageView.tag atIndexPath:collectionViewCellAtIndexPath collectionViewTag:rootCollectionViewCell.collectionView.tag collectionViewAtIndexPath:collectionViewAtIndexPath rootCollectionView:self.collectionView];

2. URLAsset

播放器 播放的资源是通过 SJVideoPlayerURLAsset 创建的. SJVideoPlayerURLAsset 由两部分组成:
视图层次 (第一部分中的SJPlayModel) 资源地址 (可以是本地资源/URL/AVAsset)
默认情况下, 创建了 SJVideoPlayerURLAsset , 赋值给播放器后即可播放.

2.1 播放 URL(本地文件或远程资源)
NSURL *URL = [NSURL URLWithString:@"https://...example.mp4"];
_player.URLAsset = [[SJVideoPlayerURLAsset alloc] initWithURL:URL];

2.3 从指定的位置开始播放

NSTimeInterval secs = 20.0;
_player.URLAsset = [[SJVideoPlayerURLAsset alloc] initWithURL:URL specifyStartTime:secs]; // 直接从20秒处开始播放

2.4 续播(进入下个页面时, 继续播放)

我们可能需要切换界面时, 希望视频能够在下一个界面无缝的进行播放. 使用如下方法, 传入正在播放的资源, 将新的资源赋值给播放器播放即可.

// otherAsset 即为上一个页面播放的Asset
// 除了需要一个otherAsset, 其他方面同以上的示例一模一样
_player.URLAsset = [SJVideoPlayerURLAsset.alloc initWithOtherAsset:otherAsset]; 

2.5 销毁时的回调. 可在此做一些记录工作, 如播放记录

// 每个资源dealloc时的回调
_player.assetDeallocExeBlock = ^(__kindof SJBaseVideoPlayer * _Nonnull videoPlayer) {
// .....
};

当资源销毁时, 播放器将会回调该 block.

3. 播放控制

播放控制: 对播放进行的操作. 此部分的内容由 "id <SJMediaPlaybackController> playbackController" 提供支持.
大多数对播放进行的操作, 均在协议 SJMediaPlaybackController 进行了声明.
正常来说实现了此协议的任何对象, 均可赋值给 player.playbackController 来替换原始实现.

3.1 播放
[_player play];
3.2 暂停
[_player pause];
3.3 刷新

在播放一个资源时, 可能有一些意外情况导致播放失败(如网络环境差).
此时当用户点击刷新按钮, 我们需要对当前的资源(Asset)进行刷新.
SJBaseVideoPlayer提供了直接的方法去刷新, 不需要开发者再重复的去创建新的Asset.

[_player refresh];
3.4 重播

从头开始重新播放

[_player replay];
3.5 停止

停止播放, 请注意: 当前资源将会被清空, 如需重播, 请重新设置新资源

[_player stop];
3.6 静音
_player.muted = YES;
3.7 调速
// 默认值为 1.0
_player.rate = 1.0;
3.8 报错

当播放发生错误时, 可以通过它来获取错误信息

_player.error
3.9 跳转
///
/// 是否精确跳转, default value is NO.
///
@property (nonatomic) BOOL accurateSeeking;
///
/// 跳转到指定位置播放
///
- (void)seekToTime:(NSTimeInterval)secs completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;
- (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;
3.10 切换清晰度
<pre style="box-sizing: border-box; overflow: auto; font-family: SFMono-Regular, Menlo, Monaco, Consolas, &quot;Liberation Mono&quot;, &quot;Courier New&quot;, monospace; font-size: 13.6px; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; line-height: 1.45; background-color: rgb(246, 248, 250); border-radius: 3px; word-break: normal; tab-size: 4; color: rgb(51, 51, 51);">///
/// 切换清晰度
///
- (void)switchVideoDefinition:(SJVideoPlayerURLAsset *)URLAsset;

///
/// 当前清晰度切换的信息
///
@property (nonatomic, strong, readonly) SJVideoDefinitionSwitchingInfo *definitionSwitchingInfo;

/// 以下为设置 SJVideoPlayer.definitionURLAssets, 将会在清晰度切换控制层中显示这些资源项. 

SJVideoPlayerURLAsset *asset1 = [[SJVideoPlayerURLAsset alloc] initWithURL:VideoURL_Level4];
asset1.definition_fullName = @"超清 1080P";
asset1.definition_lastName = @"超清";

SJVideoPlayerURLAsset *asset2 = [[SJVideoPlayerURLAsset alloc] initWithURL:VideoURL_Level3];
asset2.definition_fullName = @"高清 720P";
asset2.definition_lastName = @"AAAAAAA";

SJVideoPlayerURLAsset *asset3 = [[SJVideoPlayerURLAsset alloc] initWithURL:VideoURL_Level2];
asset3.definition_fullName = @"清晰 480P";
asset3.definition_lastName = @"480P";
_player.definitionURLAssets = @[asset1, asset2, asset3];

// 先播放asset1\. (asset2 和 asset3 将会在用户选择后进行切换)
_player.URLAsset = asset1;</pre>

### [](https://gitee.com/changsanjiang/SJVideoPlayer#311)
3.11 当前时间
@property (nonatomic, readonly) NSTimeInterval currentTime;                         ///< 当前播放到的时间
3.12 总时长
@property (nonatomic, readonly) NSTimeInterval duration;                            ///< 总时长
3.13 缓冲时长
@property (nonatomic, readonly) NSTimeInterval playableDuration;                    ///< 缓冲到的时间
3.14 是否已播放完毕
@property (nonatomic, readonly) BOOL isPlayedToEndTime;                             ///< 当前资源是否已播放结束
3.15 是否调用过播放
@property (nonatomic, readonly) BOOL isPlayed;                                      ///< 是否播放过当前的资源
3.16 是否调用过重播
@property (nonatomic, readonly) BOOL isReplayed;                                    ///< 是否重播过当前的资源
3.17 设置新资源时, 是否自动播放
@property (nonatomic) BOOL autoplayWhenSetNewAsset;                    ///< 设置新的资源后, 是否自动调用播放. 默认为 YES
3.18 进入后台, 是否暂停播放

关于后台播放视频, 引用自: https://juejin.im/post/5a38e1a0f265da4327185a26
当您想在后台播放视频时:
1.需要设置 videoPlayer.pauseWhenAppDidEnterBackground = NO; (该值默认为YES, 即App进入后台默认暂停).
2.前往 TARGETS -> Capability -> enable Background Modes -> select this mode Audio, AirPlay, and Picture in Picture

_player.pauseWhenAppDidEnterBackground = NO; // 默认值为 YES, 即进入后台后 暂停.
3.19 进入前台, 是否恢复播放
@property (nonatomic) BOOL resumePlaybackWhenAppDidEnterForeground;    ///< 进入前台时, 是否恢复播放. 默认为 NO
3.20 跳转完成, 是否恢复播放
@property (nonatomic) BOOL resumePlaybackWhenPlayerHasFinishedSeeking; ///< 当`seekToTime:`操作完成后, 是否恢复播放. 默认为 YES
3.21 资源准备状态

资源准备(或初始化)的状态
当未设置资源时, 此时 player.assetStatus = .unknown 当设置新资源时, 此时 player.assetStatus = .preparing 当准备好播放时, 此时 player.assetStatus = .readyToPlay 当初始化失败时, 此时 player.assetStatus = .failed

typedef NS_ENUM(NSInteger, SJAssetStatus) {
///
/// 未知状态
///
SJAssetStatusUnknown,

///
/// 准备中
///
SJAssetStatusPreparing,

///
/// 当前资源可随时进行播放(播放控制请查看`timeControlStatus`)
///
SJAssetStatusReadyToPlay,

///
/// 发生错误
///
SJAssetStatusFailed
};
3.22 播放控制状态

暂停或播放的控制状态
当调用了暂停时, 此时 player.timeControlStatus = .paused
当调用了播放时, 此时 将可能处于以下两种状态中的任意一个:
player.timeControlStatus = .playing 正在播放中.
player.timeControlStatus = .waitingToPlay 等待播放, 等待的原因请查看 player.reasonForWaitingToPlay

typedef NS_ENUM(NSInteger, SJPlaybackTimeControlStatus) {
///
/// 暂停状态(已调用暂停或未执行任何操作的状态)
///
SJPlaybackTimeControlStatusPaused,

///
/// 播放状态(已调用播放), 当前正在缓冲或正在评估能否播放. 可以通过`reasonForWaitingToPlay`来获取原因, UI层可以根据原因来控制loading视图的状态.
///
SJPlaybackTimeControlStatusWaitingToPlay,

///
/// 播放状态(已调用播放), 当前播放器正在播放
///
SJPlaybackTimeControlStatusPlaying
};
3.23 播放等待的原因

当调用了播放, 播放器未能播放处于等待状态时的原因
等待原因有以下3种状态: 1.未设置资源, 此时设置资源后, 当player.assetStatus = .readyToPlay, 播放器将自动进行播放. 2.可能是由于缓冲不足, 播放器在等待缓存足够时自动恢复播放, 此时可以显示loading视图. 3.可能是正在评估缓冲中, 这个过程会进行的很快, 不需要显示loading视图.

///
/// 缓冲中, UI层建议显示loading视图 
///
extern SJWaitingReason const SJWaitingToMinimizeStallsReason;

///
/// 正在评估能否播放, 处于此状态时, 不建议UI层显示loading视图
///
extern SJWaitingReason const SJWaitingWhileEvaluatingBufferingRateReason;

///
/// 未设置资源
///
extern SJWaitingReason const SJWaitingWithNoAssetToPlayReason;
3.24 监听状态改变🔥
///
/// 观察者
///
///         可以如下设置block, 来监听某个状态的改变
///         了解更多请前往头文件查看
///         player.playbackObserver.currentTimeDidChangeExeBlock = ...;
///         player.playbackObserver.durationDidChangeExeBlock = ...;
///         player.playbackObserver.timeControlStatusDidChangeExeBlock = ...;
///
@property (nonatomic, strong, readonly) SJPlaybackObservation *playbackObserver;
3.25 已观看的时长(当前资源)
@property (nonatomic, readonly) NSTimeInterval durationWatched;                     ///< 已观看的时长(当前资源)
3.26 接入别的视频 SDK, 自己动手撸一个 SJVideoPlayerPlaybackController, 替换作者原始实现

某些时候, 我们需要接入第三方的视频SDK, 但是又想使用 SJBaseVideoPlayer 封装的其他的功能.
这个时候, 我们可以自己动手, 将第三方的SDK封装一下, 实现 SJVideoPlayerPlaybackController 协议, 管理 SJBaseVideoPlayer 中的播放操作.
示例:

_player.playbackController = Your PlaybackController.

4. 控制层的显示和隐藏

控制层的显示和隐藏, 此部分的内容由 "id <SJControlLayerAppearManager> controlLayerAppearManager" 提供支持.
controlLayerAppearManager 内部存在一个定时器, 当控制层显示时, 会开启此定时器. 一定间隔后, 会尝试隐藏控制层.
其他相关操作, 请见以下内容.

4.1 让控制层显示

当控制层需要显示时, 可以调用下面方法.

[_player controlLayerNeedAppear];

此方法将会回调控制层的代理方法:
"- (void)controlLayerNeedAppear:(__kindof SJBaseVideoPlayer *)videoPlayer;"
代理将会对当前的控制层进行显示处理.

4.2 让控制层隐藏

当控制层需要隐藏时, 可以调用下面方法.

[_player controlLayerNeedDisappear];

此方法将会回调控制层的代理方法:
"- (void)controlLayerNeedDisappear:(__kindof SJBaseVideoPlayer *)videoPlayer;"
代理将会对当前的控制层进行隐藏处理.

4.3 控制层是否显示中
///
/// 控制层的显示状态(是否已显示)
///
@property (nonatomic, getter=isControlLayerAppeared) BOOL controlLayerAppeared;
4.4 是否在暂停时保持控制层显示
///
/// 暂停的时候是否保持控制层显示
///
///         default value is NO
///
@property (nonatomic) BOOL pausedToKeepAppearState;
4.5 监听状态改变🔥
///
/// 观察者
///
///         当需要监听控制层的显示和隐藏时, 可以设置`player.controlLayerAppearObserver.appearStateDidChangeExeBlock = ...;`
///
@property (nonatomic, strong, readonly) id<SJControlLayerAppearManagerObserver> controlLayerAppearObserver;
4.6 自己动手撸一个 SJControlLayerAppearManager, 替换作者原始实现

同样的, 协议 "SJControlLayerAppearManager" 定义了一系列的操作, 只要实现了这些协议方法的对象, 就可以管理控制层的显示和隐藏.

_player.controlLayerAppearManager = Your controlLayerAppearManager; 

5. 设备亮度和音量

设备亮度和音量的调整, 此部分的内容由 "id <SJDeviceVolumeAndBrightnessManager> deviceVolumeAndBrightnessManager" 提供支持.

5.1 调整设备亮度
// 0 到 1
_player.deviceVolumeAndBrightnessManager.brightness = 1.0;

5.2 调整设备声音

// 0 到 1
_player.deviceVolumeAndBrightnessManager.volume = 1.0;
5.3 监听状态改变🔥
///
/// 观察者
///
@property (nonatomic, strong, readonly) id<SJDeviceVolumeAndBrightnessManagerObserver> deviceVolumeAndBrightnessObserver;
5.4 禁止播放器设置
_player.disableBrightnessSetting = YES;
_player.disableVolumeSetting = YES;
5.5 自己动手撸一个 SJDeviceVolumeAndBrightnessManager, 替换作者原始实现

当需要对设备音量视图进行自定义时, 可以自己动手撸一个 SJDeviceVolumeAndBrightnessManager.

_player.deviceVolumeAndBrightnessManager = Your deviceVolumeAndBrightnessManager;

6. 旋转

此部分的内容由 "id <SJRotationManagerProtocol> rotationManager" 提供支持.
对于旋转, 我们开发者肯定需要绝对的控制, 例如: 设置自动旋转所支持方向. 能够主动+自动旋转, 而且还需要能在适当的时候禁止自动旋转. 旋转前后的回调等等... 放心这些功能都有, 我挨个给大家介绍.
另外旋转有两种方式:
仅旋转播放器视图 (默认情况下)
使 ViewController 也一起旋转
具体请看下面介绍.

6.1 自动旋转

先说说何为自动旋转. 其实就是当设备方向变更时, 播放器根据设备方向进行自动旋转.

6.2 设置自动旋转支持的方向
/// 设置自动旋转支持的方向
_player.rotationManager.autorotationSupportedOrientations = SJOrientationMaskLandscapeLeft | SJOrientationMaskLandscapeRight;
/**
自动旋转支持的方向
- SJOrientationMaskPortrait:       竖屏
- SJOrientationMaskLandscapeLeft:  支持全屏, Home键在右侧
- SJOrientationMaskLandscapeRight: 支持全屏, Home键在左侧
- SJOrientationMaskAll:            全部方向
*/
typedef enum : NSUInteger {
SJOrientationMaskPortrait = 1 << SJOrientation_Portrait,
SJOrientationMaskLandscapeLeft = 1 << SJOrientation_LandscapeLeft,
SJOrientationMaskLandscapeRight = 1 << SJOrientation_LandscapeRight,
SJOrientationMaskAll = SJOrientationMaskPortrait | SJOrientationMaskLandscapeLeft | SJOrientationMaskLandscapeRight,
} SJOrientationMask;
6.3 禁止自动旋转

这里有两点需要注意:
合适的时候要记得恢复自动旋转.
禁止自动旋转后, 主动调用旋转, 还是可以旋转的.

_player.rotationManager.disabledAutorotation = YES;
6.4 主动调用旋转

主动旋转. 当我们想主动旋转时, 大概分为以下三点:

播放器旋转到用户当前的设备方向或恢复小屏.
主动旋转到指定方向.
主动旋转完成后的回调.
请看以下方法, 分别对应以上三点:

- (void)rotate;
- (void)rotate:(SJOrientation)orientation animated:(BOOL)animated;
- (void)rotate:(SJOrientation)orientation animated:(BOOL)animated completion:(void (^ _Nullable)(__kindof SJBaseVideoPlayer *player))block;
6.5 是否全屏
/// 如果为YES, 表示全屏
@property (nonatomic, readonly) BOOL isFullScreen;                              ///< 是否已全屏
6.6 是否正在旋转
/// 如果为YES, 表示正在旋转中
@property (nonatomic, readonly) BOOL isTransitioning;
6.7 当前旋转的方向
_player.rotationManager.currentOrientation
6.8 监听状态改变🔥
///
/// 观察者
///
/// 当需要监听旋转时, 可以设置`player.rotationObserver.rotationDidStartExeBlock = ...;`
/// 了解更多请前往头文件查看
///
@property (nonatomic, strong, readonly) id<SJRotationManagerObserver> rotationObserver;
6.9 自己动手撸一个 SJRotationManager, 替换作者原始实现

当你想替换原始实现时, 可以实现 SJRotationManagerProtocol 中定义的方法.

7. 直接全屏而不旋转

直接全屏, 或者说充满屏幕, 但不旋转.

7.1 全屏和恢复
_player.fitOnScreen = YES;

[_player setFitOnScreen:NO animated:NO];

[_player setFitOnScreen:YES animated:YES completionHandler:^(__kindof SJBaseVideoPlayer * _Nonnull player) {
/// ...
}];
7.2 监听状态改变🔥
@property (nonatomic, copy, nullable) void(^fitOnScreenWillBeginExeBlock)(__kindof SJBaseVideoPlayer *player);
@property (nonatomic, copy, nullable) void(^fitOnScreenDidEndExeBlock)(__kindof SJBaseVideoPlayer *player);;
7.3 是否是全屏
/// YES 为充满屏幕 
_player.isFitOnScreen
7.4 自己动手撸一个 SJFitOnScreenManager, 替换作者原始实现

该部分管理类的协议定义在 SJFitOnScreenManagerProtocol 中, 实现该协议的任何对象, 均可赋值给播放器, 替换原始实现.

8. 镜像翻转

此部分内容由 id<SJFlipTransitionManager> flipTransitionManager 提供支持
目前镜像翻转只写了 水平翻转, 未来可能会加入更多的翻转类型.

typedef enum : NSUInteger {
SJViewFlipTransition_Identity,
SJViewFlipTransition_Horizontally, // 水平翻转
} SJViewFlipTransition;
8.1 翻转和恢复
/// 当前的翻转类型
_player.flipTransition
/// 翻转相关方法
[_player setFlipTransition:SJViewFlipTransition_Horizontally];
[_player setFlipTransition:SJViewFlipTransition_Horizontally animated:YES];
[_player setFlipTransition:SJViewFlipTransition_Identity animated:YES completionHandler:^(__kindof SJBaseVideoPlayer * _Nonnull player) {
/// ...
}];
8.2 监听状态改变🔥
///
/// 观察者
///
///         可以如下设置block, 来监听某个状态的改变
///
///         player.flipTransitionObserver.flipTransitionDidStartExeBlock = ...;
///         player.flipTransitionObserver.flipTransitionDidStopExeBlock = ...;
///
@property (nonatomic, strong, readonly) id<SJFlipTransitionManagerObserver> flipTransitionObserver;
8.3 自己动手撸一个 SJFlipTransitionManager, 替换作者原始实现

该部分管理类的协议定义在 SJFlipTransitionManagerProtocol 中, 实现该协议的任何对象, 均可赋值给播放器, 替换原始实现.

9. 网络状态

此部分内容由 id<SJReachability> reachability 提供支持
默认的 reachability 是个单例, 在App生命周期中, 仅创建一次. 因此每个播放器对象持有的 reachability 都是相同的.

9.1 当前的网络状态
@property (nonatomic, readonly) SJNetworkStatus networkStatus;
9.2 监听状态改变🔥
///
/// 观察者
///
@property (nonatomic, strong, readonly) id<SJReachabilityObserver> reachabilityObserver;
9.3 自己动手撸一个 SJReachability, 替换作者原始实现

该部分管理类的协议定义在 SJNetworkStatus 中, 实现该协议的任何对象, 均可赋值给播放器, 替换原始实现.

10. 手势

此部分内容由 id<SJPlayerGestureControl> gestureControl 提供支持
播放器默认存在四种手势, 每个手势触发的回调均定义在 SJPlayerGestureControl 中, 当想改变某个手势的处理时, 可以直接修改对应手势触发的 block 即可.
具体请看以下部分.

10.1 单击手势

当用户单击播放器时, 播放器会调用 显示或隐藏控制层的操作
以下为默认实现:

__weak typeof(self) _self = self;
_gestureControl.singleTapHandler = ^(id<SJPlayerGestureControl>  _Nonnull control, CGPoint location) {
__strong typeof(_self) self = _self;
if ( !self ) return ;
/// 让控制层显示或隐藏
[self.controlLayerAppearManager switchAppearState];
};
10.2 双击手势

双击会触发暂停或播放的操作

__weak typeof(self) _self = self;
_gestureControl.doubleTapHandler = ^(id<SJPlayerGestureControl>  _Nonnull control, CGPoint location) {
__strong typeof(_self) self = _self;
if ( !self ) return ;
if ( [self playStatus_isPlaying] )
[self pause];
else
[self play];
};
10.3 移动手势
  • 垂直滑动时, 默认情况下如果在屏幕左边, 则会触发调整亮度的操作, 并显示亮度提示视图. 如果在屏幕右边, 则会触发调整声音的操作, 并显示系统音量提示视图
  • 水平滑动时, 会触发控制层相应的代理方法
__weak typeof(self) _self = self;
_gestureControl.panHandler = ^(id<SJPlayerGestureControl>  _Nonnull control, SJPanGestureTriggeredPosition position, SJPanGestureMovingDirection direction, SJPanGestureRecognizerState state, CGPoint translate) {
__strong typeof(_self) self = _self;
if ( !self ) return ;
/// ....
};
10.4 捏合手势

当用户做放大或收缩触发该手势时, 会设置播放器显示模式AspectAspectFill.

__weak typeof(self) _self = self;
_gestureControl.pinchHandler = ^(id<SJPlayerGestureControl>  _Nonnull control, CGFloat scale) {
__strong typeof(_self) self = _self;
if ( !self ) return ;
self.playbackController.videoGravity = scale > 1 ?AVLayerVideoGravityResizeAspectFill:AVLayerVideoGravityResizeAspect;
};
10.5 设置支持的手势
_player.gestureControl.supportedGestureTypes = SJPlayerGestureTypeMask_All
typedef enum : NSUInteger {
SJPlayerGestureTypeMask_None,
SJPlayerGestureTypeMask_SingleTap = 1 << 0,
SJPlayerGestureTypeMask_DoubleTap = 1 << 1,
SJPlayerGestureTypeMask_Pan_H = 1 << 2, // 水平方向
SJPlayerGestureTypeMask_Pan_V = 1 << 3, // 垂直方向
SJPlayerGestureTypeMask_Pinch = 1 << 4,
SJPlayerGestureTypeMask_Pan = SJPlayerGestureTypeMask_Pan_H | SJPlayerGestureTypeMask_Pan_V,
SJPlayerGestureTypeMask_All = SJPlayerGestureTypeMask_SingleTap |
SJPlayerGestureTypeMask_DoubleTap |
SJPlayerGestureTypeMask_Pan |
SJPlayerGestureTypeMask_Pinch,
} SJPlayerGestureTypeMask;
10.6 自定义某个手势的处理
/// 例如 替换单击手势的处理
__weak typeof(self) _self = self;
_player.gestureControl.singleTapHandler = ^(id<SJPlayerGestureControl>  _Nonnull control, CGPoint location) {
__strong typeof(_self) self = _self;
if ( !self ) return ;
/// .....你的处理
};

11. 占位图

资源在初始化时, 由于暂时没有画面可以呈现, 会出现短暂的黑屏. 在此期间, 建议大家设置一下占位图.

11.1 设置本地占位图
_player.presentView.placeholderImageView.image = [UIImage imageNamed:@"..."];
11.2 设置网络占位图
[_player.presentView.placeholderImageView sd_setImageWithURL:URL placeholderImage:img];
11.3 是否隐藏占位图 - 播放器准备好显示时
/// 播放器准备好显示时, 是否隐藏占位图
/// - 默认为YES
@property (nonatomic) BOOL hiddenPlaceholderImageViewWhenPlayerIsReadyForDisplay;

12. 显示提示文本

目前仅支持 NSAttributedString.

12.1 显示管理类
///
/// 中心弹出文本提示
///
///         了解更多请前往协议头文件查看
///
@property (nonatomic, strong, null_resettable) id<SJPromptProtocol> prompt;

///
/// 右下角弹出提示
///
///         了解更多请前往协议头文件查看
///
@property (nonatomic, strong, null_resettable) id<SJPopPromptControllerProtocol> popPromptController;
12.2 配置提示文本
_player.prompt.backgroundColor = ...;
_player.prompt.contentInset = ...;

13. 一些固定代码

接入播放器的 ViewController 中, 会写一些固定的代码, 我将这些固定代码(例如 进入下个页面时, 需要当前页面的播放器暂停), 都封装在了以下方法中.

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[_player vc_viewDidAppear];
}

在适当的时候直接调用即可, 以下为内部实现:

13.1 - (void)vc_viewDidAppear;

当 ViewController 的 viewDidAppear 调用时, 恢复播放
实现如下:

- (void)vc_viewDidAppear {
if ( !self.isPlayOnScrollView || (self.isPlayOnScrollView && self.isScrollAppeared) ) {
/// 恢复播放
[self play];
}
/// 标识vc已显示 
/// vc_isDisappeared 是自动旋转触发的条件之一, 如果控制器 disappear 了, 就不会触发旋转 
self.vc_isDisappeared = NO;
}
13.2 - (void)vc_viewWillDisappear;

当 ViewController 的 viewWillDisappear 调用时, 设置标识为YES
实现如下:

- (void)vc_viewWillDisappear {
/// 标识vc已显示 
/// vc_isDisappeared 是自动旋转触发的条件之一, 如果控制器 disappear 了, 就不会触发旋转 
self.vc_isDisappeared = YES;
}
13.3 - (void)vc_viewDidDisappear;

当 ViewController 的 viewDidDisappear 调用时, 暂停播放
实现如下:

- (void)vc_viewDidDisappear {
[self pause];
}
13.4 - (BOOL)vc_prefersStatusBarHidden;

状态栏是否可以隐藏
实现如下:

- (BOOL)vc_prefersStatusBarHidden {
if ( _tmpShowStatusBar ) return NO;         // 临时显示
if ( _tmpHiddenStatusBar ) return YES;      // 临时隐藏
if ( self.lockedScreen ) return YES;        // 锁屏时, 不显示
if ( self.rotationManager.isTransitioning ) { // 旋转时, 不显示
if ( !self.disabledControlLayerAppearManager && self.isControlLayerAppeared ) return NO;
return YES;
}
// 全屏播放时, 使状态栏根据控制层显示或隐藏
if ( self.isFullScreen ) return !self.isControlLayerAppeared;
return NO;
}
13.5 - (UIStatusBarStyle)vc_preferredStatusBarStyle;

状态栏显示白色还是黑色
实现如下:

- (UIStatusBarStyle)vc_preferredStatusBarStyle {
// 全屏播放时, 使状态栏变成白色
if ( self.isFullScreen || self.fitOnScreen ) return UIStatusBarStyleLightContent;
return UIStatusBarStyleDefault;
}
13.6 - 临时显示状态栏

有时候, 可能会希望临时显示状态栏, 例如全屏转回小屏时, 旋转之前, 需要将状态栏显示.

[_player needShowStatusBar]; 
13.7 - 临时隐藏状态栏

有时候, 可能会希望临时隐藏状态栏, 例如某个播放器控制层不需要显示状态栏.

[_player needHiddenStatusBar]; 

14. 截屏

14.1 当前时间截图
UIImage *img = [_player screenshot];
14.2 指定时间截图
- (void)screenshotWithTime:(NSTimeInterval)secs
completion:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, UIImage * __nullable image, NSError *__nullable error))block;

/// 可以通过 _player.playbackController.presentationSize 来获取当前视频宽高
- (void)screenshotWithTime:(NSTimeInterval)secs
size:(CGSize)size
completion:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, UIImage * __nullable image, NSError *__nullable error))block;

15. 导出视频或GIF

15.1 导出视频
- (void)exportWithBeginTime:(NSTimeInterval)beginTime
duration:(NSTimeInterval)duration
presetName:(nullable NSString *)presetName
progress:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, float progress))progressBlock
completion:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, NSURL *fileURL, UIImage *thumbnailImage))completion
failure:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, NSError *error))failure;
15.2 导出GIF
- (void)generateGIFWithBeginTime:(NSTimeInterval)beginTime
duration:(NSTimeInterval)duration
progress:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, float progress))progressBlock
completion:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, UIImage *imageGIF, UIImage *thumbnailImage, NSURL *filePath))completion
failure:(void(^)(__kindof SJBaseVideoPlayer *videoPlayer, NSError *error))failure;
15.3 取消操作
/// 取消导出操作
/// 播放器 dealloc 时, 会调用一次 
- (void)cancelExportOperation;

/// 取消GIF操作
/// 播放器 dealloc 时, 会调用一次 
- (void)cancelGenerateGIFOperation;

16. 滚动相关

此部分的内容由 SJPlayModelPropertiesObserver 提供支持.

16.1 是否在 UICollectionView 或者 UITableView 中播放
/// 是否是在 UICollectionView 或者 UITableView 中播放
_player.isPlayOnScrollView
16.2 是否已显示
///
/// 播放器视图是否显示
///
/// Whether the player is appeared when playing on scrollView. Because scrollview may be scrolled.
///
@property (nonatomic, readonly) BOOL isScrollAppeared;
16.3 播放器视图将要滚动显示和消失的回调
@property (nonatomic, copy, nullable) void(^playerViewWillAppearExeBlock)(__kindof SJBaseVideoPlayer *videoPlayer);
@property (nonatomic, copy, nullable) void(^playerViewWillDisappearExeBlock)(__kindof SJBaseVideoPlayer *videoPlayer);
16.4 滚动出去后, 是否暂停
///
/// 滚动出去后, 是否暂停. 默认为YES
///
/// - default value is YES.
///
@property (nonatomic) BOOL pauseWhenScrollDisappeared;
16.5 滚动进入时, 是否恢复播放
///
/// 滚动进入时, 是否恢复播放. 默认为YES
///
/// - default values is YES.
///
@property (nonatomic) BOOL resumePlaybackWhenScrollAppeared;
16.6 滚动出去后, 是否隐藏播放器视图
///
/// 滚动出去后, 是否隐藏播放器视图. 默认为YES
///
/// - default value is YES.
///
@property (nonatomic) BOOL hiddenViewWhenScrollDisappeared;

17. 自动播放 - 在 UICollectionView 或者 UITableView 中

目前支持在 UICollectionViewCell 和 UITableViewCell 中自动播放.
使用之前, 请导入头文件 #import "UIScrollView+ListViewAutoplaySJAdd.h"

17.1 开启
/// 配置列表自动播放
[_tableView sj_enableAutoplayWithConfig:[SJPlayerAutoplayConfig configWithPlayerSuperviewTag:101 autoplayDelegate:self]];
/// Delegate method
- (void)sj_playerNeedPlayNewAssetAtIndexPath:(NSIndexPath *)indexPath {
}
17.2 配置
typedef NS_ENUM(NSUInteger, SJAutoplayScrollAnimationType) {
SJAutoplayScrollAnimationTypeNone,
SJAutoplayScrollAnimationTypeTop,
SJAutoplayScrollAnimationTypeMiddle,
};

@interface SJPlayerAutoplayConfig : NSObject
+ (instancetype)configWithPlayerSuperviewTag:(NSInteger)playerSuperviewTag
autoplayDelegate:(id<SJPlayerAutoplayDelegate>)autoplayDelegate;

/// 滚动的动画类型
/// default is .Middle;
@property (nonatomic) SJAutoplayScrollAnimationType animationType;

@property (nonatomic, readonly) NSInteger playerSuperviewTag;
@property (nonatomic, weak, nullable, readonly) id<SJPlayerAutoplayDelegate> autoplayDelegate;
@end

@protocol SJPlayerAutoplayDelegate <NSObject>
- (void)sj_playerNeedPlayNewAssetAtIndexPath:(NSIndexPath *)indexPath;
@end
17.3 关闭
[_tableView sj_disenableAutoplay];
17.4 主动调用播放下一个资源
[_tableView sj_needPlayNextAsset];

18. 对控制层上面的Item的操作

18.1 添加
SJEdgeControlButtonItem *item = [[SJEdgeControlButtonItem alloc] initWithImage:[UIImage imageNamed:@"test"] target:self action:@selector(test) tag:SJTestImageItemTag];
[_player.defaultEdgeControlLayer.topAdapter addItem:item];
[_player.defaultEdgeControlLayer.topAdapter reload];

18.2 删除
[_player.defaultEdgeControlLayer.bottomAdapter removeItemForTag:SJEdgeControlLayerBottomItem_Separator];
[_player.defaultEdgeControlLayer.bottomAdapter reload];
18.3 调整位置
[_player.defaultEdgeControlLayer.bottomAdapter exchangeItemForTag:SJEdgeControlLayerBottomItem_DurationTime withItemForTag:SJEdgeControlLayerBottomItem_Progress];
[_player.defaultEdgeControlLayer.bottomAdapter reload];

19. 对控制层上的Item的一些补充

19.1 设置与前后item的间距
SJEdgeControlButtonItem *titleItem = [_player.defaultEdgeControlLayer.topAdapter itemForTag:SJEdgeControlLayerTopItem_Title];
titleItem.insets = SJEdgeInsetsMake(16, 16);
[_player.defaultEdgeControlLayer.topAdapter reload];
19.2 设置隐藏
SJEdgeControlButtonItem *titleItem = [_player.defaultEdgeControlLayer.topAdapter itemForTag:SJEdgeControlLayerTopItem_Title];
titleItem.hidden = YES;
[_player.defaultEdgeControlLayer.topAdapter reload];
19.3 填充剩余空间
SJEdgeControlButtonItem *titleItem = [_player.defaultEdgeControlLayer.topAdapter itemForTag:SJEdgeControlLayerTopItem_Title];
titleItem.fill = YES;
[_player.defaultEdgeControlLayer.topAdapter reload];

20. SJEdgeControlLayer 的补充

20.1 是否竖屏时隐藏返回按钮
_player.defaultEdgeControlLayer.hiddenBackButtonWhenOrientationIsPortrait = YES;
20.2 是否禁止网络状态变化提示
_player.defaultEdgeControlLayer.disabledPromptWhenNetworkStatusChanges = YES;
20.3 是否使返回按钮常驻
_player.defaultEdgeControlLayer.showResidentBackButton = YES;
20.4 是否隐藏底部进度条
_player.defaultEdgeControlLayer.hiddenBottomProgressIndicator = YES;
20.5 是否在loadingView上显示网速
_player.defaultEdgeControlLayer.showNetworkSpeedToLoadingView = YES;
20.6 自定义loadingView
// 实现协议`SJEdgeControlLayerLoadingViewProtocol`即可, 然后赋值给控制层
_player.defaultEdgeControlLayer.loadingView = Your Loading View;
20.7 调整边距
_player.defaultEdgeControlLayer.leftMargin = 16;
_player.defaultEdgeControlLayer.rightMargin = 16;
20.8 取消控制层上下视图的阴影
[_player.defaultEdgeControlLayer.topContainerView cleanColors];
[_player.defaultEdgeControlLayer.bottomContainerView cleanColors];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335