YPDPlayer
一套支持边下边播的播放器方案:
·支持音/视频播放
·支持在线播放
·支持本地播放
·支持边下边播
·操作流畅
方案介绍
整体方案采用ZFPlayer + AVPlayer + KTVHTTPCache,ZFPlayer是一个播放器壳子,支持自定义播放器和控制层。本项目的核心是解决视频缓存的问题,所以这里先介绍KTVHTTPCache这个库。
KTVHTTPCache
功能特点
- 支持相同URL并发操作且线程安全;
- 全路径Log,支持控制台打印和输出到文件,可准确定位问题;
- 细粒度的缓存管理,可精确查看指定 URL 的完整缓存信息;
- 模块相互独立,提供使用不同 Level 的接口;
- 下载层高度可配置;
- 低耦合,集成简单;
框架设计
KTVHTTPCache 由 HTTP Server 和 Data Storage 两大模块组成,前者负责与Client交互,后者负责资源加载及缓存处理。
工作流程图:
工作流程简述:
1、Client 发出的请求被 HTTP Srever 接收到,HTTP Server 通过分析 HTTP Request 创建用于访问 Data Storage 的 Data Request 对象;
2、HTTP Server 使用 Data Request 创建 Data Reader,并以此作为从 Data Storage 获取数据的通道;
3、Data Reader 分析 Data Request 中的 Range 创建对应的网络数据源 Data Network Source 和文件数据源 Data File Source,并通过 Data Sourcer 进行管理;
4、Data Sourcer 开始加载数据;
5、Data Reader 从 Data Sourcer 读取数据并通过 HTTP Server 回传给 Client;
使用示例:
NSError *error = nil;
[KTVHTTPCache proxyStart:&error];//启动HttpServer,全局启动一次即可
//URL Encode:对中文、特殊符号、空格进行转译处理
NSString *URLString = [item.URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
//将原视频url替换成自定义格式url,请求本地服务器
NSURL *URL = [KTVHTTPCache proxyURLWithOriginalURL:[NSURL URLWithString:URLString]];
YPDMediaVC *vc = [[YPDMediaVC alloc] initWithURLString:URL.absoluteString];
[self presentViewController:vc animated:YES completion:nil];
使用注意点:
当单个视频大小超过设置的最大缓存时,视频无法播放,解决办法:
- 根据项目实际情况来设置最大缓存,尽量设置一个较大值;
- 后台在返回视频url时带上视频长度参数,客户端在播放前先做判断,超过最大缓存的直接在线播放,不走本地服务器(实际中这种情况比较少,以防万一);
以下为框架中相关处理代码,只是抛出错误,没有做相关处理:
//KTVHCDownload.m
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
...
if (!error) {
long long (^getDeletionLength)(long long) = ^(long long desireLength){
return desireLength + [KTVHCDataStorage storage].totalCacheLength - [KTVHCDataStorage storage].maxCacheLength;
};
long long length = getDeletionLength(dataResponse.contentLength);
if (length > 0) {
[[KTVHCDataUnitPool pool] deleteUnitsWithLength:length];
length = getDeletionLength(dataResponse.contentLength);
if (length > 0) {
error = [KTVHCError errorForNotEnoughDiskSpace:dataResponse.totalLength
request:dataResponse.contentLength
totalCacheLength:[KTVHCDataStorage storage].totalCacheLength
maxCacheLength:[KTVHCDataStorage storage].maxCacheLength];
}
}
}
if (error) {
KTVHCLogDownload(@"%p, Invaild response\nError : %@", self, error);
[self.errorDictionary setObject:error forKey:task];
completionHandler(NSURLSessionResponseCancel);
} else {
id delegate = [self.delegateDictionary objectForKey:task];
[delegate ktv_download:self didReceiveResponse:dataResponse];
completionHandler(NSURLSessionResponseAllow);
}
}
HttpServer
使用CocoaHTTPServer作为本地HttpServer,client发送数据请求,HttpServer先从本地获取数据,本地没有再从网络下载。主要类如下:
- KTVHCHTTPServer:是一个单例类,用来管理 HttpServer 服务,负责开启或关闭服务;
- KTVHCHTTPConnection:它继承于 HTTPConnection,表示一个连接,它主要为 HttpServer 提供 Response;
- KTVHCHTTPResponse:一个遵循HTTPResponse协议的response类;
生成response关键代码:
- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
KTVHCLogHTTPConnection(@"%p, Receive request\nmethod : %@\npath : %@\nURL : %@", self, method, path, request.url);
NSDictionary *parameters = [[KTVHCURLTool tool] parseQuery:request.url.query];
NSURL *URL = [NSURL URLWithString:[parameters objectForKey:@"url"]];
KTVHCDataRequest *dataRequest = [[KTVHCDataRequest alloc] initWithURL:URL headers:request.allHeaderFields];
KTVHCHTTPResponse *response = [[KTVHCHTTPResponse alloc] initWithConnection:self dataRequest:dataRequest];
return response;
}
DataStroage
主要用来缓存数据,加载数据,也就是提供数据给 HttpServer。上面代码中关键的一句代码[[KTVHCHTTPResponse alloc] initWithConnection:self dataRequest:dataRequest],它会在这个方法的内部使用KTVHCDataStorage生成一个KTVHCDataReader,负责读取数据。生成KTVHCDataReader后通过[self.reader prepare]来准备数据源KTVHCDataSourceManager,这里主要有两个数据源,KTVHCDataFileSource和KTVHCDataNetworkSource,它们实现了协议KTVHCDataSource。KTVHCDataNetworkSource会通过KTVHCDownload下载数据。
- KTVHCDataStorage:一个单利类,负责管理整个缓存,比如读取、保存、合并缓存;
- KTVHCDataReader:负责读取数据;
- KTVHCDataLoader:封装了reader的一个类,提供接口对外使用;
- KTVHCDataRequest:数据请求;
- KTVHCDataResponse:数据响应;
- KTVHCDataCacheItem:缓存数据模型,表示一个缓存项;
- KTVHCDataCacheItemZone:缓存区,一个缓存项中可能会有多个缓存区,比如099,100299等;
- KTVHCDataSource:定义了一组属性、方法的协议,下面三个类都遵循了该协议;
- KTVHCDataFileSource:本地数据源;
- KTVHCDataNetworkSource:网络数据源;
- KTVHCDataSourceManager:总数据源管理类;
- KTVHCDataCallback:封装了两个类方法的回调类;
- KTVHCDataUnit:数据单元,相当于一个缓存目录,比如一个视频的缓存;
- KTVHCDataUnitItem:数据单元项,缓存目录下不同片段的缓存;
- KTVHCDataUnitPool:数据单元池,它是一个单例,含有一个 KTVHCDataUnitQueue;
- KTVHCDataUnitQueue:数据单元队列,保存了多个 KTVHCDataUnit,它会以 archive 的方式缓存到本地;
初始数据都来源于网络,这里介绍下KTVHCDataNetworkSource中处理数据核心代码:
- (void)ktv_download:(KTVHCDownload *)download didReceiveResponse:(KTVHCDataResponse *)response
{
[self lock];
if (self.isClosed || self.error) {
[self unlock];
return;
}
self->_response = response;
NSString *path = [KTVHCPathTool filePathWithURL:self.request.URL offset:self.request.range.start];
self.unitItem = [[KTVHCDataUnitItem alloc] initWithPath:path offset:self.request.range.start];
KTVHCDataUnit *unit = [[KTVHCDataUnitPool pool] unitWithURL:self.request.URL];
[unit insertUnitItem:self.unitItem];
KTVHCLogDataNetworkSource(@"%p, Receive response\nResponse : %@\nUnit : %@\nUnitItem : %@", self, response, unit, self.unitItem);
[unit workingRelease];
//创建两个文件句柄,读和写
self.writingHandle = [NSFileHandle fileHandleForWritingAtPath:self.unitItem.absolutePath];
self.readingHandle = [NSFileHandle fileHandleForReadingAtPath:self.unitItem.absolutePath];
[self callbackForPrepared];
[self unlock];
}
- (void)ktv_download:(KTVHCDownload *)download didReceiveData:(NSData *)data
{
[self lock];
if (self.isClosed || self.error) {
[self unlock];
return;
}
@try {
//接收到数据后,写入文件
[self.writingHandle writeData:data];
self.downloadLength += data.length;
[self.unitItem updateLength:self.downloadLength];
KTVHCLogDataNetworkSource(@"%p, Receive data : %lld, %lld, %lld", self, (long long)data.length, self.downloadLength, self.unitItem.length);
//有可用数据,需要回调通知
[self callbackForHasAvailableData];
} @catch (NSException *exception) {
NSError *error = [KTVHCError errorForException:exception];
KTVHCLogDataNetworkSource(@"%p, write exception\nError : %@", self, error);
[self callbackForFailed:error];
if (!self.downloadCalledComplete) {
KTVHCLogDataNetworkSource(@"%p, Cancel download task when write exception", self);
[self.downlaodTask cancel];
self.downlaodTask = nil;
}
}
[self unlock];
}
缓存策略
以网络使用最小化为原则,设计了分片加载数据的功能。有 Network Source 和 File Source 两种用于加载数据的 Source,分别用于下载网络数据和读取本地数据。通过分析 Data Request 的 Range 和本地缓存状态来对应创建。
例如一次请求的 Range 为 0-999,本地缓存中已有 200-499 和 700-799 两段数据。那么会对应生成 5 个 Source,分别是:
- 网络:0~199
- 本地:200~499
- 网络:500~699
- 本地:700~799
- 网络:800~999
ZFPlayer
功能特点
- 普通模式的播放,类似于腾讯视频、爱奇艺等APP;
- 列表普通模式的播放,包括手动点击播放、滑动到屏幕中间自动播放,wifi网络智能播放等等;
- 列表的亮暗模式播放,类似于微博、UC浏览器视频列表等APP;
- 列表视频滑出屏幕后停止播放、滑出屏幕后小窗播放;
- 优雅的全屏,支持横屏和竖屏全屏模式;
使用介绍
实际项目中,如果要自定义播放器和控制层,只需要播放器SDK遵守ZFPlayerMediaPlayback
协议,控制层遵守ZFPlayerMediaControl
协议。
如何导入库:(CocoaPods)
pod 'ZFPlayer', '~> 4.0'
使用默认控制层:
pod 'ZFPlayer/ControlView', '~> 4.0'
使用AVPlayer播放器:
pod 'ZFPlayer/AVPlayer', '~> 4.0'
使用ijkplayer播放器:
pod 'ZFPlayer/ijkplayer', '~> 4.0'
本项目采用ZFPlayer+AVPlayer+KTVHTTPCache实现边下边播,关键代码如下:
- (void)configPlayer{
ZFAVPlayerManager *playerManager = [[ZFAVPlayerManager alloc] init];
if (@available(iOS 10.0, *)) {
//关闭AVPlayer默认的缓冲延迟播放策略,提高首屏播放速度
playerManager.player.automaticallyWaitsToMinimizeStalling = NO;
}
//初始化时设置containerViewTag,根据此tag在cell上找到播放器view显示的位置
self.player = [[ZFPlayerController alloc] initWithScrollView:self.tableView playerManager:playerManager containerViewTag:kContainerViewTag];
self.player.controlView = self.controlView;
/// 0.4是消失40%时候
self.player.playerDisapperaPercent = 0.4;
/// 0.6是出现60%时候
self.player.playerApperaPercent = 0.6;
/// 移动网络依然自动播放
self.player.WWANAutoPlay = YES;
/// 设置是否续播
// self.player.resumePlayRecord = YES;
@weakify(self)
//播放完当前视频自动播放下一个
self.player.playerDidToEnd = ^(id _Nonnull asset){
@strongify(self)
if (self.player.playingIndexPath.row < self.dataSource.count - 1) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.player.playingIndexPath.row+1 inSection:0];
[self playTheVideoAtIndexPath:indexPath scrollAnimated:YES];
} else {
//停止当前在cell上的播放并移除播放器view
[self.player stopCurrentPlayingCell];
}
};
/// 停止的时候找出最合适的播放
self.player.zf_scrollViewDidEndScrollingCallback = ^(NSIndexPath * _Nonnull indexPath) {
@strongify(self)
if (!self.player.playingIndexPath) {
[self playTheVideoAtIndexPath:indexPath scrollAnimated:NO];
}
};
}
- (void)playTheVideoAtIndexPath:(NSIndexPath *)indexPath scrollAnimated:(BOOL)animated {
ZFTableViewCellLayout *layout = self.dataSource[indexPath.row];
if (animated) {
[self.player playTheIndexPath:indexPath assetURL:[NSURL URLWithString:[self convertToProxyUrlString:layout.data.video_url]] scrollPosition:ZFPlayerScrollViewScrollPositionCenteredVertically animated:YES];
} else {
[self.player playTheIndexPath:indexPath assetURL:[NSURL URLWithString:[self convertToProxyUrlString:layout.data.video_url]]];
}
[self.controlView showTitle:layout.data.title
coverURLString:layout.data.thumbnail_url
fullScreenMode:layout.isVerticalVideo?ZFFullScreenModePortrait:ZFFullScreenModeLandscape];
}
//转换成请求本地服务器的url
- (NSString *)convertToProxyUrlString:(NSString *)urlString {
NSString *URLString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *URL = [KTVHTTPCache proxyURLWithOriginalURL:[NSURL URLWithString:URLString]];
return URL.absoluteString;
}
ZFPlayerController是ZFPlayer框架的核心类。
属性
- containerView:初始化时传递的容器视图,用来显示播放器view,和播放器view同等大小;
- currentPlayerManager:初始化时传递的播放器manager,必须遵守
ZFPlayerMediaPlayback
协议 - controlView:设置显示的控制层,遵守
ZFPlayerMediaControl
协议,可自定义; - notification:通知的管理类;
- containerType:容器的类型(cell和普通View);
- smallFloatView:播放器小窗的容器View;
- isSmallFloatViewShow:播放器小窗是否正在显示;
初始化方式
/// 普通播放的初始化
+ (instancetype)playerWithPlayerManager:(id)playerManager containerView:(UIView *)containerView;
/// 普通播放的初始化
- (instancetype)initWithPlayerManager:(id)playerManager containerView:(UIView *)containerView;
/// UITableView、UICollectionView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerViewTag:(NSInteger)containerViewTag;
/// UITableView、UICollectionView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerViewTag:(NSInteger)containerViewTag;
/// UIScrollView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerView:(UIView *)containerView;
/// UIScrollView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerView:(UIView *)containerView;
ZFPlayerMediaPlayback-播放器SDK遵守的协议
- 协议属性
/// 播放器视图继承于ZFPlayerView,处理一些手势冲突
@property (nonatomic) ZFPlayerView *view;
/// 0...1.0,播放器音量,不影响设备的音量大小
@property (nonatomic) float volume;
/// 播放器是否静音,不影响设备静音
@property (nonatomic, getter=isMuted) BOOL muted;
/// 0.5...2,播放速率,正常速率为 1
@property (nonatomic) float rate;
/// 当前播放时间
@property (nonatomic, readonly) NSTimeInterval currentTime;
/// 播放总时间
@property (nonatomic, readonly) NSTimeInterval totalTime;
/// 缓冲时间
@property (nonatomic, readonly) NSTimeInterval bufferTime;
/// 视频播放定位时间
@property (nonatomic) NSTimeInterval seekTime;
/// 视频是否正在播放中
@property (nonatomic, readonly) BOOL isPlaying;
/// 视频播放视图的填充模式,默认不做任何拉伸
@property (nonatomic) ZFPlayerScalingMode scalingMode;
/// 检查视频播放是否准备就绪,返回YES,调用play方法直接播放视频;返回NO,调用play方法内部自动调用prepareToPlay方法进行视频播放准备工作
@property (nonatomic, readonly) BOOL isPreparedToPlay;
/// 媒体播放资源URL
@property (nonatomic) NSURL *assetURL;
/// 视频的尺寸
@property (nonatomic, readonly) CGSize presentationSize;
/// 视频播放状态
@property (nonatomic, readonly) ZFPlayerPlaybackState playState;
/// 视频的加载状态
@property (nonatomic, readonly) ZFPlayerLoadState loadState;
///------------------------------------
///如果没有指定controlView,可以调用以下块。
///如果你指定了controlView,下面的代码块不能在外部调用,只能用于“ZFPlayerController”调用。
///------------------------------------
/// 准备播放
@property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id asset, NSURL *assetURL);
/// 开始播放了
@property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id asset, NSURL *assetURL);
/// 播放进度改变
@property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id asset, NSTimeInterval currentTime, NSTimeInterval duration);
/// 视频缓冲进度改变
@property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id asset, NSTimeInterval bufferTime);
/// 视频播放状态改变
@property (nonatomic, copy, nullable) void(^playerPlayStatChanged)(id asset, ZFPlayerPlaybackState playState);
/// 视频加载状态改变
@property (nonatomic, copy, nullable) void(^playerLoadStatChanged)(id asset, ZFPlayerLoadState loadState);
/// 视频播放已经结束
@property (nonatomic, copy, nullable) void(^playerDidToEnd)(id asset);
/// 视频的尺寸改变了
@property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id asset, CGSize size);
- 协议方法:
/// 视频播放准备,中断除non-mixible之外的任何音频会话
- (void)prepareToPlay;
/// 重新进行视频播放准备
- (void)reloadPlayer;
/// 视频播放
- (void)play;
/// 视频暂停
- (void)pause;
/// 视频重新播放
- (void)replay;
/// 视频播放停止
- (void)stop;
/// 视频播放当前时间的画面截图
- (UIImage *)thumbnailImageAtCurrentTime;
/// 替换当前媒体资源地址
- (void)replaceCurrentAssetURL:(NSURL *)assetURL;
/// 调节播放进度
- (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;
ZFPlayerMediaControl-控制层遵守的协议
- 视频状态相关
/// 视频播放准备就绪
- (void)videoPlayer:(ZFPlayerController *)videoPlayer prepareToPlay:(NSURL *)assetURL;
/// 视频播放状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer playStateChanged:(ZFPlayerPlaybackState)state;
/// 视频加载状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer loadStateChanged:(ZFPlayerLoadState)state;
- 播放进度
/// 视频播放时间进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
currentTime:(NSTimeInterval)currentTime
totalTime:(NSTimeInterval)totalTime;
/// 视频缓冲进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
bufferTime:(NSTimeInterval)bufferTime;
/// 视频定位播放时间
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
draggingTime:(NSTimeInterval)seekTime
totalTime:(NSTimeInterval)totalTime;
/// 视频播放结束
- (void)videoPlayerPlayEnd:(ZFPlayerController *)videoPlayer;
- 锁屏
/// 设置播放器锁屏时的协议方法
- (void)lockedVideoPlayer:(ZFPlayerController *)videoPlayer lockedScreen:(BOOL)locked;
- 屏幕旋转
/// 播放器全屏模式即将改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationWillChange:(ZFOrientationObserver *)observer;
/// 播放器全屏模式已经改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationDidChanged:(ZFOrientationObserver *)observer;
/// 当前网络状态发生变化
- (void)videoPlayer:(ZFPlayerController *)videoPlayer reachabilityChanged:(ZFReachabilityStatus)status;
- 手势方法
/// 相关手势设置
- (BOOL)gestureTriggerCondition:(ZFPlayerGestureControl *)gestureControl
gestureType:(ZFPlayerGestureType)gestureType
gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
touch:(UITouch *)touch;
/// 单击
- (void)gestureSingleTapped:(ZFPlayerGestureControl *)gestureControl;
/// 双击
- (void)gestureDoubleTapped:(ZFPlayerGestureControl *)gestureControl;
/// 开始拖拽
- (void)gestureBeganPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location;
/// 拖拽中
- (void)gestureChangedPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location
withVelocity:(CGPoint)velocity;
/// 拖拽结束
- (void)gestureEndedPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location;
/// 捏合手势变化
- (void)gesturePinched:(ZFPlayerGestureControl *)gestureControl
scale:(float)scale;
- scrollView上的播放器视图方法
/**
scrollView中的播放器视图将要出现的回调
*/
- (void)playerWillAppearInScrollView:(ZFPlayerController *)videoPlayer;
/**
scrollView中的播放器视图已经出现的回调
*/
- (void)playerDidAppearInScrollView:(ZFPlayerController *)videoPlayer;
/**
scrollView中的播放器视图即将消失的回调
*/
- (void)playerWillDisappearInScrollView:(ZFPlayerController *)videoPlayer;
/**
scrollView中的播放器视图已经消失的回调
*/
- (void)playerDidDisappearInScrollView:(ZFPlayerController *)videoPlayer;
/**
scrollView中的播放器视图正在显示的回调
*/
- (void)playerAppearingInScrollView:(ZFPlayerController *)videoPlayer playerApperaPercent:(CGFloat)playerApperaPercent;
/**
scrollView中的播放器视图正在消失的回调
*/
- (void)playerDisappearingInScrollView:(ZFPlayerController *)videoPlayer playerDisapperaPercent:(CGFloat)playerDisapperaPercent;
/**
小窗视图显示隐藏的回调
*/
- (void)videoPlayer:(ZFPlayerController *)videoPlayer floatViewShow:(BOOL)show;
关于ZFPlayer框架更详细介绍请参考ZFPlayer 3.0解析。 Demo地址