JJPlayerView
AVPlayer依赖于AVFoundation,另外AVPlayer不包含UI,想要完成播放功能,需要自己写UI。
项目中往往会用到视频播放功能,GitHub上面也有很多star很高的库也能满足需求,但是当我们需要高度自定义UI的时候,他们却不能满足要求。
所以对于要求较高的播放功能,我们需要自己封装一个播放器。
今天我们就来实现一个简单的播放器,目的是了解播放器的使用。
iOS开发中会有音视频播放的需求。目前常用的音视频播放器有 AVPlayer,AVPlayer支持本地播放,分步下载,在线流媒体播放。
AVPlayer基础知识简介
- AVPlayer是用于管理媒体资源播放的控制器,它提供了播放器的诸多功能:播放、暂停、倍速等。AVPlayer一次只能播放一个资源文件(本地或者远程)。
AVQueuePlayer可以播放一个列表,是AVPlayer的子类。
然后我们根据视频资源,创建我们需要的AVPlayer。系统为我们也提供了几个初始化方法。
+ (instancetype)playerWithURL:(NSURL *)URL;
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item;
- (instancetype)initWithURL:(NSURL *)URL;
- (instancetype)initWithPlayerItem:(nullable AVPlayerItem *)item;
- AVPlayerItem代表了一个播放资源,每播放一个视频需要一个数据资源,我们需要初始化一个AVPlayerItem。
系统提供了几个初始化AVPlayerItem
的方法。
/// - AVPlayerItem的初始化方法
+ (instancetype)playerItemWithURL:(NSURL *)URL;
+ (instancetype)playerItemWithAsset:(AVAsset *)asset;
- (instancetype)initWithURL:(NSURL *)URL;
- (instancetype)initWithAsset:(AVAsset *)asset;
- 单纯使用AVPlayer是无法显示视频的,需要将视频添加至AVPlayerLayer上,然后添加到我们自己创建的视图layer上面。
封装JJPlayerView
封装播放器可以分为两个部分:
- 第一是播放视频的功能
- 第二个是UI交互。
两个部分不是完全分开的,UI就是调用这个方法,达到播放视频的功能。
- 创建一个JJPalyerView,作为承载视图。
- 创建必要的属性
AVPlayer
,AVPlayerItem
,AVPlayerLayer
等
/// 播放器
@property (nonatomic, strong) AVPlayer *player;
/// 播放器item
@property (nonatomic, strong) AVPlayerItem *playerItem;
/// 播放器layer
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
/// 控件的原始frame
@property (nonatomic, assign) CGRect customFrame;
/// 父类控件
@property (nonatomic, strong) UIView *fatherView;
/// 视图拉伸模式
@property (nonatomic, copy) NSString *fillMode;
/// 是否处于全屏状态
@property (nonatomic, assign) BOOL isFullScreen;
/// 工具条是否隐藏
@property (nonatomic, assign) BOOL isDisappear;
/// 用户播放标记
@property (nonatomic, assign) BOOL isUserPlay;
/// 点击最大化标记
@property (nonatomic, assign) BOOL isUserTapMaxButton;
/// 是否播放完毕
@property (nonatomic, assign) BOOL isFinish;
/// 是否在调节音量:YES为音量,NO为屏幕亮度
@property (nonatomic, assign) BOOL isVolume;
/// 是否在拖拽
@property (nonatomic, assign) BOOL isDragged;
/// 缓冲
@property (nonatomic, assign) BOOL isBuffering;
/// 用来保存快进的总时长
@property (nonatomic, assign) CGFloat sumTime;
/// 播放器配置信息
@property (nonatomic, strong) JJPlayerConfigure *playerConfigure;
/// 视频播放控制面板(遮罩)
@property (nonatomic, strong) JJPlayerMaskView *playerMaskView;
/// 滑动方向
@property (nonatomic, assign) JJPanDirection panDirection;
/// 音量滑杆
@property (nonatomic, strong) UISlider *volumeViewSlider;
/// 点击屏幕定时器
@property (nonatomic, strong) NSTimer *tapTimer;
/// 播放器的播放状态
@property (nonatomic, assign) JJPlayerState playerState;
/// 记录点击屏幕定时器的时间
@property (nonatomic, assign) NSInteger tapTimeCount;
/// 是否已经移除了KVO
@property (nonatomic, assign) BOOL isRemoveObserver;
- 创建播放完成和返回按钮回调事件
/// 返回按钮回调
@property (nonatomic, copy) void(^BackBlock) (UIButton *backButton);
/// 播放完成回调
@property (nonatomic, copy) void(^EndBlock)(void);
- 添加通知,监听播放器的各种状态
//开启
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
/// 监听横竖屏的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
/// 进入后台
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterBackground:) name:UIApplicationWillResignActiveNotification object:nil];
/// 进入前台
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterPlayground:) name:UIApplicationDidBecomeActiveNotification object:nil];
// 添加播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
// 添加打断播放的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionComing:) name:AVAudioSessionInterruptionNotification object:nil];
// 添加插拔耳机的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChanged:) name:AVAudioSessionRouteChangeNotification object:nil];
- 将AVPlayerLayer添加到视图上
定义视频在AVPlayerLayer中的显示方式,默认方式AVLayerVideoGravityResizeAspect
;
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
playerLayer.frame = frame;
playerLayer.backgroundColor = [UIColor blackColor].CGColor;
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
[view.layer addSublayer: playerLayer];
我们看到了,需要设置AVPlayerLayer
的frame
,还需要通过videoGravity
设置资源填充展示的样式.
但是这里有更好的写法,创建一个PlayerLayerView,将这个PlayerLayerView的layer返回class,设置为AVPlayerLayer类型,这样就不用设置layer的frame了,直接设置view的frame就可以,更加直观方便。
@interface JJPlayerLayer : UIView
@end
@implementation JJPlayerLayer
+ (Class)layerClass {
return AVPlayerLayer.class;
}
@end
/// 播放器layer
@property (nonatomic, strong) JJPlayerLayer *playerLayer;
- AVPlayer提供了音视频的播放和暂停功能,与之对应的方法:
play
、pause
。
/// 开始播放
- (void)play{
// 这里不只是播放,点击播放的时候还要设置相关UI,判断当前状态是否可以播放。并不是直接播放就可以。
// 判断当前是否播放完成,播放完成后要从头开始播放
self.playerMaskView.playButton.selected = YES;
// [self.layer insertSublayer:self.playerLayer atIndex:0];
if (self.isFinish && self.playerMaskView.slider.value == 1) {
_isFinish = NO;
[_player seekToTime:CMTimeMake(0, 1) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
} else {
[self.player play];
}
}
/// 暂停
- (void)pause{
/// 设置暂停相关UI样式。
[self.player pause];
}
- 播放器一次只能给你管理一个播放资源,但我们需要切换资源的时候,可以调用相关
replaceCurrentItemWithPlayerItem
方法。AVFoundation框架还提供了一个专门的类:AVQueuePlayer
,用来管理资源列表,我们暂时不讨论,后面再延伸.
- (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;
但是我们为了更方便使用,当切换资源的时候,重置播放器。也就是销毁原来的资源,重新创建。
#pragma mark - 重置播放器
- (void)resetPlayer{
//重置状态
self.playerState = JJPlayerStatePause;
_isUserPlay = YES;//用户点击标志
_isDisappear = NO;//工具条隐藏标记
//移除之前的
[self pause];//先暂停
[self.playerLayer removeFromSuperlayer];
self.playerLayer = nil;
self.player = nil;
//还原进度条和缓冲条
self.playerMaskView.slider.value = 0;
self.playerMaskView.progressView.progress = 0;
//重置时间
self.playerMaskView.currentTimeLabel.text = @"00:00";
self.playerMaskView.totalTimeLabel.text = @"00:00";
[self destoryToolBarTimer];
//重置Toolbar
[UIView animateWithDuration:0.25 animations:^{
self.playerMaskView.topToolBar.alpha = 1.0;
self.playerMaskView.bottomToolBar.alpha = 1.0;
}];
//重新添加工具条消失定时器
[self resetTooBarDisappearTimer];
self.playerMaskView.failButton.hidden = YES;
//开始转子动化
[self.playerMaskView.loadingView startAnimation];
}
然后再重新添加新的资源
self.playerItem = [AVPlayerItem playerItemWithURL:_url];
//创建播放器
_player = [AVPlayer playerWithPlayerItem:_playerItem];
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
_playerLayer.videoGravity = _fillMode;
[self.layer insertSublayer:_playerLayer atIndex:0];
- 监听资源的状态
我们可以使用KVO监听playerItem的几个属性status
,loadedTimeRanges
,playbackBufferEmpty
。
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
只有当status发生改变,并且值为AVPlayerItemStatusReadyToPlay的时候,说明视频资源准备完成,才可以播放。
结果是AVPlayerItemStatusFailed的时候,说明解析失败,需要展示相关UI和文案。loadedTimeRanges可以计算缓冲进度
playbackBufferEmpty说明当前缓冲为空,需要做暂停处理,加载一会...........
- (void)setPlayerItem:(AVPlayerItem *)playerItem{
if (_playerItem == playerItem) {
return;
}
if (_playerItem) {
if (!self.isRemoveObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[_playerItem removeObserver:self forKeyPath:@"status"];
[_playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
[_playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
}
self.isRemoveObserver = YES;
//重置播放器
[self resetPlayer];
}
_playerItem = playerItem;
if (playerItem) {
self.isRemoveObserver = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
}
}
#pragma mark - kvo监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:@"status"]) {
if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) {//加载完成,可以播放
//加载完成后,再添加平移手势
//添加平移手势,用来控制音量/亮度/快进快退
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureDirection:)];
pan.delegate = self;
pan.maximumNumberOfTouches = 1; //一根手指
pan.delaysTouchesBegan = YES;
pan.delaysTouchesEnded = YES;
pan.cancelsTouchesInView = YES;
[self.playerMaskView addGestureRecognizer:pan];
self.player.muted = self.playerConfigure.isMute;
[self addPeriodicTimeObserver];
if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) {
self.playerState = JJPlayerStatePlaying;
}else if (self.player.currentItem.status == AVPlayerItemStatusFailed) {
self.playerState = JJPlayerStateFailed;
}
}else if (self.player.currentItem.status == AVPlayerItemStatusFailed){
// 解析失败(播放失败)
self.playerState = JJPlayerStateFailed;
}
}else if ([keyPath isEqualToString:@"loadedTimeRanges"]){
//计算缓冲进度
NSTimeInterval timeInterval = [self calculateBufferProgress];
CMTime duration = self.playerItem.duration;
CGFloat totalDuratuon = CMTimeGetSeconds(duration);
[self.playerMaskView.progressView setProgress:(timeInterval / totalDuratuon) animated:NO];
}else if ([keyPath isEqualToString:@"playbackBufferEmpty"]){
//当前缓冲时空的时候
if (self.playerItem.isPlaybackBufferEmpty) {
self.playerState = JJPlayerStateBuffering;
[self bufferingSomeSecond];//卡顿一会,缓冲几秒
}
}
}
我们可以看到添加KVO之前。先移除,是因为我们可能会替换资源,如果不移除,监听到的就是之前的资源信息。
- 添加通知监听其他状态,实现UI交互功能
其实这个时候其实就一个可以将视频播放出来了,但是我们为了更好地体验,需要获取到播放器的状态:播放、加载、播放完成等等。
这里我们可以通过通知
来获取当前的具体状态。
通过通知AVPlayerItemDidPlayToEndTimeNotification
可以接收到视频播放完成通知.
通过通知AVAudioSessionInterruptionNotification
可以接收到打断播放的通知.
通过通知AVAudioSessionRouteChangeNotification
可以接收到插拔耳机的通知.
// 添加播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
// 添加打断播放的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionComing:) name:AVAudioSessionInterruptionNotification object:nil];
// 添加插拔耳机的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChanged:) name:AVAudioSessionRouteChangeNotification object:nil];
- AVPlayer 中播放进度的获取如下,通过回传的time参数,可以更新进度.
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;
当拿到回传的时间,我们需要更新进度指示器。
@weakify(self);
[_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 10) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
@strongify(self);
[self sliderTimerAction];
}];
- (void)sliderTimerAction{
if (self.playerItem.duration.timescale != 0) {
self.playerMaskView.slider.maximumValue = 1;
CGFloat total = _playerItem.duration.value / _playerItem.duration.timescale;
self.playerMaskView.slider.value = CMTimeGetSeconds(self.playerItem.currentTime) / total;
//判断是否正在播放
if (self.playerItem.isPlaybackLikelyToKeepUp && self.playerMaskView.slider.value > 0) {
self.playerState = JJPlayerStatePlaying;
}
//当前时长
NSInteger proMin = (NSInteger)CMTimeGetSeconds([_player currentTime]) / 60;//当前分钟
NSInteger proSec = (NSInteger)CMTimeGetSeconds([_player currentTime]) % 60;//当前秒
self.playerMaskView.currentTimeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld", (long)proMin, (long)proSec];
//总时长
NSInteger durMin = (NSInteger)_playerItem.duration.value / _playerItem.duration.timescale / 60;//总分钟
NSInteger durSec = (NSInteger)_playerItem.duration.value / _playerItem.duration.timescale % 60;//总秒
self.playerMaskView.totalTimeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld", (long)durMin, (long)durSec];
}
}
- 有的时候用户可以快进,或者手动拖动到指定时间点播放资源:
- (void)seekToTime:(CMTime)time;
当我们滑动播放器进度的时候,或者活动屏幕的时候,可以调用次方法,改变播放进度。
- 其他通知的监听
电话打断播放,home键回到主页面,屏幕翻转等等:
/// 添加打断播放的通知
[[NSNotificationCenter defaultCenter] addObserver: self selector:@selector(interruptionComing:) name: AVAudioSessionInterruptionNotification object:nil];
/// 监听横竖屏的通知
[[NSNotificationCenter defaultCenter] addObserver: self selector:@selector(orientationChanged:) name: UIDeviceOrientationDidChangeNotification object:nil];
/// 进入后台
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterBackground:) name:UIApplicationWillResignActiveNotification object:nil]
/// 进入前台
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterPlayground:) name:UIApplicationDidBecomeActiveNotification object:nil];
- 创建一个UISlider,作为食品播放器的滑竿。
系统的滑竿是非常好用的,但是UI不满足要求,我们继承系统UISlider,稍作修改:
#import
NS_ASSUME_NONNULL_BEGIN
@interface JJSlider : UISlider
@end
NS_ASSUME_NONNULL_END
@implementation JJSlider
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self setupUI];
}
return self;
}
- (void)setupUI {
UIImage *thumbImage = [UIImage imageWithName:@"JJRoundButton"];
[self setThumbImage:thumbImage forState:UIControlStateHighlighted];
[self setThumbImage:thumbImage forState:UIControlStateNormal];
}
@end
修改滑竿的按钮图片即可。
当拖动滑竿的时候改变视频播放的进度。
- 当可以播放的时候,通过参数
rate
可以获取到当前是在加载中还是播放中。也可以通过它设置播放速度。 - 我们需要创建一个遮罩层,添加一些基本的功能,比如,胡奥明屏幕实现快进,快退功能,改变音量,改变屏幕亮度,还要添加一些标题,播放按钮等功能。
我们创建一个JJPlayerMaskView作为遮罩层。
遮罩层里面包含的属性:标题,返回按钮,播放按钮,暂停按钮,进度条,缓冲条,播放失败提示,全屏按钮等等。
页面上需要添加手势,点击隐藏/显示 这些控件。
创建一个协议,将这些事件传递出去。
@protocol JJPlayerMaskViewDelegate
/// 返回按钮点击事件代理
- (void)jj_playerMaskViewBackButtonAction:(UIButton *_Nonnull)button;
/// 播放按钮点击事件代理
- (void)jj_playerMaskViewPlayButtonAction:(UIButton *_Nonnull)button;
/// 全屏按钮点击事件代理
- (void)jj_playerMaskViewFullButtonAction:(UIButton *_Nonnull)button;
/// 开始滑动
- (void)jj_playerMaskViewProgressSliderTouchBegan:(JJSlider *_Nullable)slider;
/// 滑动中
- (void)jj_playerMaskViewProgressSliderTouchChanged:(JJSlider *_Nullable)slider;
/// 滑动结束
- (void)jj_playerMaskViewProgressSliderTouchEnd:(JJSlider *_Nullable)slider;
/// 播放失败按钮事件
- (void)jj_playerMaskViewFailButtonAction:(UIButton *_Nonnull)button;
/// 代理事件
@property (nonatomic, weak) id delegate;
/// 顶部工具条
@property (nonatomic, strong) UIView *topToolBar;
/// 标题
@property (nonatomic, strong) UILabel *titleLabel;
/// 底部工具条
@property (nonatomic, strong) UIView *bottomToolBar;
/// 加载动画
@property (nonatomic, strong, nullable) JJAnimationView *loadingView;
/// 顶部工具条返回按钮(会和工具条一起消失显示)
@property (nonatomic, strong) UIButton *backButton;
/// 顶部返回按钮,一直显示
@property (nonatomic, strong) UIButton *backOnlyButton;
@property (nonatomic, strong) UIButton *lockButton;
/// 底部工具条播放/暂停按钮
@property (nonatomic, strong) UIButton *playButton;
/// 底部工具条全屏按钮
@property (nonatomic, strong) UIButton *fullButton;
/// 底部工具条当前时间
@property (nonatomic, strong) UILabel *currentTimeLabel;
/// 底部工具条总时间
@property (nonatomic, strong) UILabel *totalTimeLabel;
/// 缓冲进度条
@property (nonatomic, strong) UIProgressView *progressView;
/// 播放进度条
@property (nonatomic, strong) JJSlider *slider;
/// 加载失败按钮
@property (nonatomic, strong) UIButton *failButton;
- 判断滑动方向,并记录
水平滑动的时候
if (self.panDirection == JJPanDirectionHorizontalMoved) {
[self panHorizontalMoved:thePoint.x];
}
水平滑动的时候改变slider的UI,并调节进度
#pragma mark - 水平滑动,调节进度
- (void)panHorizontalMoved:(CGFloat)value{
//水平滑动进度,逻辑多,多做一些判断
if (value == 0) {
return;
}
//每次滑动时间需要叠加.
self.sumTime += value/200.0;
//需要给sumTime限制范围
CMTime totalTime = self.playerItem.duration;
CGFloat totalMovieDuration = (CGFloat)(totalTime.value/totalTime.timescale);
if (self.sumTime > totalMovieDuration) {
self.sumTime = totalMovieDuration;
}
if (self.sumTime < 0) {
self.sumTime = 0;
}
self.isDragged = YES;
//计算出拖动的当前秒数
CGFloat dragedSeconds = self.sumTime;
//滑杆进度
CGFloat sliderValue = dragedSeconds/totalMovieDuration;
//设置滑杆
self.playerMaskView.slider.value = sliderValue;
//转换成CMTime才能player来控制进度
CMTime dragedCMTime = CMTimeMake(dragedSeconds, 1);
[self.player seekToTime:dragedCMTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
NSInteger proMin = (NSInteger)CMTimeGetSeconds(dragedCMTime) / 60;//当前秒
NSInteger proSec = (NSInteger)CMTimeGetSeconds(dragedCMTime) % 60;//当前分钟
self.playerMaskView.currentTimeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld",proSec,proMin];
}
- 创建音量滑竿
/// 音量滑杆
@property (nonatomic, strong) UISlider *volumeViewSlider;
当垂直滑动的时候,以中心线为标准,判断滑动位置,一侧改变音量,另一侧改变屏幕亮度
if (self.panDirection == JJPanDirectionVerticalMoved){
[self panVerticalMoved:thePoint.y];
}
#pragma mark - 垂直滑动,调节音量和亮度
- (void)panVerticalMoved:(CGFloat)value{
if (self.isVolume) { //音量
self.volumeViewSlider.value -= value/10000.0;
}else{ //亮度
[UIScreen mainScreen].brightness -= value/10000.0;
}
}
- 全屏处理。
视频播放一般有全屏播放的功能。
我们做法是,全屏的时候将播放器添加至keyWindow上面,并记录原来父视图,返回时,添加到圆的父视图上面。
点击全屏的时候
- (UIWindow *)getKeyWindow{
if (@available(iOS 13.0, *)){
return [UIApplication sharedApplication].windows.lastObject;
}else{
return [UIApplication sharedApplication].keyWindow;
}
}
//添加到keyWindow上
UIWindow *keyWindow = [self getKeyWindow];
[keyWindow addSubview:self];
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIInterfaceOrientationLandscapeRight] forKey:@"orientation"];
只设置翻转一个方向即可!
还原的时候:
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIInterfaceOrientationPortrait] forKey:@"orientation"];
self.frame = _customFrame;
// 还原到原有父类上
[_fatherView addSubview:self];
self.playerMaskView.fullButton.selected = NO;
- 其他细节处理
锁屏功能,点击后页面不再处理事件。
这个功能简单,记录锁屏按钮状态,处理各事件前,判断锁屏状态。或者锁屏的时候,将控件隐藏起来。
弹幕功能,在遮罩上面的一部分区域,添加弹幕即可。
demo地址