Commit 97faca4e by 邓成林

新建工程

parents
File added
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <UIKit/UIKit.h>
UIKIT_EXTERN void JPDispatchSyncOnMainQueue(void (^block)(void));
UIKIT_EXTERN void JPDispatchAsyncOnMainQueue(void (^block)(void));
UIKIT_EXTERN void JPDispatchAsyncOnQueue(dispatch_queue_t queue, void (^block)(void));
UIKIT_EXTERN void JPDispatchSyncOnQueue(dispatch_queue_t queue, void (^block)(void));
UIKIT_EXTERN void JPDispatchAsyncOnNextRunloop(void (^block)(void));
UIKIT_EXTERN void JPDispatchAfterTimeIntervalInSecond(NSTimeInterval timeInterval, void (^block)(void));
/**
* benchmark 工具(返回值为代码每次执行耗时, 单位为 ns), 注意线上环境禁用.
*
* @param count benchmark 次数.
* @param block benchmark 代码.
*/
UIKIT_EXTERN int64_t jp_dispatch_benchmark(size_t count, void (^block)(void));
#define JPAssertMainThread NSAssert([NSThread isMainThread], @"代码应该在主线程调用.")
#define JPAssertNotMainThread NSAssert(![NSThread isMainThread], @"代码不应该在主线程调用.")
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPGCDExtensions.h"
#import <pthread.h>
extern uint64_t dispatch_benchmark(size_t count, void (^block)(void));
void JPDispatchSyncOnMainQueue(void (^block)(void)) {
if (!block) {
return;
}
if (pthread_main_np()) {
block();
return;
}
dispatch_sync(dispatch_get_main_queue(), block);
}
void JPDispatchAsyncOnMainQueue(void (^block)(void)) {
if (!block) {
return;
}
if (pthread_main_np()) {
JPDispatchAsyncOnNextRunloop(block);
return;
}
dispatch_async(dispatch_get_main_queue(), block);
}
void JPDispatchAsyncOnNextRunloop(void (^block)(void)) {
dispatch_async(dispatch_get_main_queue(), block);
}
void JPDispatchAsyncOnQueue(dispatch_queue_t queue, void (^block)(void)) {
if (!queue) {
dispatch_async(dispatch_get_main_queue(), block);
return;
}
dispatch_async(queue, block);
}
void JPDispatchSyncOnQueue(dispatch_queue_t queue, void (^block)(void)) {
if (!queue) {
dispatch_sync(dispatch_get_main_queue(), block);
return;
}
dispatch_sync(queue, block);
}
void JPDispatchAfterTimeIntervalInSecond(NSTimeInterval timeInterval, void (^block)(void)) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), block);
}
int64_t jp_dispatch_benchmark(size_t count, void (^block)(void)) {
return dispatch_benchmark(count, block);
}
\ No newline at end of file
//
// Created by NewPan on 2018/9/5.
// Copyright (c) 2018 NewPan. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
/**
* 依赖注入工具类.
*/
#define jp_metamacro_stringify_(VALUE) # VALUE
#define jp_metamacro_stringify(VALUE) \
jp_metamacro_stringify_(VALUE)
#define jp_concrete \
optional \
#define jp_concreteprotocol(NAME) \
interface NAME ## _JPProtocolMethodContainer : NSObject < NAME > {} \
@end \
@implementation NAME ## _JPProtocolMethodContainer \
+ (void)load { \
if (!jp_addConcreteProtocol(objc_getProtocol(jp_metamacro_stringify(NAME)), self)) \
fprintf(stderr, "ERROR: Could not load concrete protocol %s\n", jp_metamacro_stringify(NAME)); \
} \
__attribute__((constructor)) \
static void rs_ ## NAME ## _inject (void) { \
jp_loadConcreteProtocol(objc_getProtocol(jp_metamacro_stringify(NAME))); \
}
BOOL jp_addConcreteProtocol(Protocol *protocol, Class methodContainer);
void jp_loadConcreteProtocol(Protocol *protocol);
//
// Created by NewPan on 2018/9/5.
// Copyright (c) 2018 NewPan. All rights reserved.
//
#import "JPMethodInjecting.h"
#import <pthread.h>
#import "JPVideoPlayerSupportUtils.h"
typedef struct JPSpecialProtocol {
__unsafe_unretained Protocol *protocol;
Class containerClass;
BOOL ready;
} JPSpecialProtocol;
static JPSpecialProtocol * restrict jp_specialProtocols = NULL;
static size_t jp_specialProtocolCount = 0;
static size_t jp_specialProtocolCapacity = 0;
static size_t jp_specialProtocolsReady = 0;
static pthread_mutex_t jp_specialProtocolsLock = PTHREAD_MUTEX_INITIALIZER;
static NSRecursiveLock *jpinjecting_recursiveLock;
BOOL rs_loadSpecialProtocol (Protocol *protocol, Class containerClass) {
@autoreleasepool {
NSCParameterAssert(protocol != nil);
if (pthread_mutex_lock(&jp_specialProtocolsLock) != 0) {
fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
return NO;
}
if (jp_specialProtocolCount == SIZE_MAX) {
pthread_mutex_unlock(&jp_specialProtocolsLock);
return NO;
}
if (jp_specialProtocolCount >= jp_specialProtocolCapacity) {
size_t newCapacity;
if (jp_specialProtocolCapacity == 0)
newCapacity = 1;
else {
newCapacity = jp_specialProtocolCapacity << 1;
if (newCapacity < jp_specialProtocolCapacity) {
newCapacity = SIZE_MAX;
if (newCapacity <= jp_specialProtocolCapacity) {
pthread_mutex_unlock(&jp_specialProtocolsLock);
return NO;
}
}
}
void * restrict ptr = realloc(jp_specialProtocols, sizeof(*jp_specialProtocols) * newCapacity);
if (!ptr) {
pthread_mutex_unlock(&jp_specialProtocolsLock);
return NO;
}
jp_specialProtocols = ptr;
jp_specialProtocolCapacity = newCapacity;
}
assert(jp_specialProtocolCount < jp_specialProtocolCapacity);
#ifndef __clang_analyzer__
jp_specialProtocols[jp_specialProtocolCount] = (JPSpecialProtocol){
.protocol = protocol,
.containerClass = containerClass,
.ready = NO,
};
#endif
++jp_specialProtocolCount;
pthread_mutex_unlock(&jp_specialProtocolsLock);
}
return YES;
}
static void rs_orderSpecialProtocols(void) {
qsort_b(jp_specialProtocols, jp_specialProtocolCount, sizeof(JPSpecialProtocol), ^(const void *a, const void *b){
if (a == b)
return 0;
const JPSpecialProtocol *protoA = a;
const JPSpecialProtocol *protoB = b;
int (^protocolInjectionPriority)(const JPSpecialProtocol *) = ^(const JPSpecialProtocol *specialProtocol){
int runningTotal = 0;
for (size_t i = 0;i < jp_specialProtocolCount;++i) {
if (specialProtocol == jp_specialProtocols + i)
continue;
if (protocol_conformsToProtocol(specialProtocol->protocol, jp_specialProtocols[i].protocol))
runningTotal++;
}
return runningTotal;
};
return protocolInjectionPriority(protoB) - protocolInjectionPriority(protoA);
});
}
void rs_specialProtocolReadyForInjection (Protocol *protocol) {
@autoreleasepool {
NSCParameterAssert(protocol != nil);
if (pthread_mutex_lock(&jp_specialProtocolsLock) != 0) {
fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
return;
}
for (size_t i = 0;i < jp_specialProtocolCount;++i) {
if (jp_specialProtocols[i].protocol == protocol) {
if (!jp_specialProtocols[i].ready) {
jp_specialProtocols[i].ready = YES;
assert(jp_specialProtocolsReady < jp_specialProtocolCount);
if (++jp_specialProtocolsReady == jp_specialProtocolCount)
rs_orderSpecialProtocols();
}
break;
}
}
pthread_mutex_unlock(&jp_specialProtocolsLock);
}
}
static void rs_logInstanceAndClassMethod(Class cls) {
unsigned imethodCount = 0;
Method *imethodList = class_copyMethodList(cls, &imethodCount);
NSLog(@"instance Method--------------------");
for (unsigned methodIndex = 0;methodIndex < imethodCount;++methodIndex) {
Method method = imethodList[methodIndex];
SEL selector = method_getName(method);
NSLog(@"%@", [NSString stringWithFormat:@"-[%@ %@]", NSStringFromClass(cls), NSStringFromSelector(selector)]);
}
free(imethodList); imethodList = NULL;
unsigned cmethodCount = 0;
Method *cmethodList = class_copyMethodList(object_getClass(cls), &cmethodCount);
NSLog(@"class Method--------------------");
for (unsigned methodIndex = 0;methodIndex < cmethodCount;++methodIndex) {
Method method = cmethodList[methodIndex];
SEL selector = method_getName(method);
NSLog(@"%@", [NSString stringWithFormat:@"+[%@ %@]", NSStringFromClass(cls), NSStringFromSelector(selector)]);
}
free(cmethodList); cmethodList = NULL;
NSLog(@"end----------------------------------------");
}
static void rs_injectConcreteProtocolInjectMethod(Class containerClass, Class pairClass) {
unsigned imethodCount = 0;
Method *imethodList = class_copyMethodList(containerClass, &imethodCount);
for (unsigned methodIndex = 0;methodIndex < imethodCount;++methodIndex) {
Method method = imethodList[methodIndex];
SEL selector = method_getName(method);
IMP imp = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(pairClass, selector, imp, types);
}
free(imethodList); imethodList = NULL;
(void)[containerClass class];
unsigned cmethodCount = 0;
Method *cmethodList = class_copyMethodList(object_getClass(containerClass), &cmethodCount);
Class metaclass = object_getClass(pairClass);
for (unsigned methodIndex = 0;methodIndex < cmethodCount;++methodIndex) {
Method method = cmethodList[methodIndex];
SEL selector = method_getName(method);
if (selector == @selector(initialize)) {
continue;
}
IMP imp = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(metaclass, selector, imp, types);
}
free(cmethodList); cmethodList = NULL;
(void)[containerClass class];
}
static NSArray * rs_injectMethod(id object) {
NSMutableArray *rs_matchSpecialProtocolsToClass = @[].mutableCopy;
for (size_t i = 0;i < jp_specialProtocolCount;++i) {
@autoreleasepool {
Protocol *protocol = jp_specialProtocols[i].protocol;
if (!class_conformsToProtocol([object class], protocol)) {
continue;
}
[rs_matchSpecialProtocolsToClass addObject:[NSValue value:&jp_specialProtocols[i] withObjCType:@encode(struct JPSpecialProtocol)]];
}
}
if(!rs_matchSpecialProtocolsToClass.count) {
return nil;
}
struct JPSpecialProtocol protocol;
for(NSValue *value in rs_matchSpecialProtocolsToClass) {
[value getValue:&protocol];
rs_injectConcreteProtocolInjectMethod(protocol.containerClass, [object class]);
}
return rs_matchSpecialProtocolsToClass.copy;
}
static bool rs_resolveMethodForObject(id object) {
@autoreleasepool {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
jpinjecting_recursiveLock = [NSRecursiveLock new];
});
[jpinjecting_recursiveLock lock];
// 处理继承自有注入的父类.
Class currentClass = [object class];
NSArray *matchSpecialProtocolsToClass = nil;
do {
NSArray *protocols = rs_injectMethod(currentClass);
if(!matchSpecialProtocolsToClass) {
matchSpecialProtocolsToClass = protocols;
}
}while((currentClass = class_getSuperclass(currentClass)));
if(!matchSpecialProtocolsToClass.count) {
[jpinjecting_recursiveLock unlock];
return nil;
}
[jpinjecting_recursiveLock unlock];
return YES;
}
}
BOOL jp_addConcreteProtocol(Protocol *protocol, Class methodContainer) {
return rs_loadSpecialProtocol(protocol, methodContainer);
}
void jp_loadConcreteProtocol(Protocol *protocol) {
rs_specialProtocolReadyForInjection(protocol);
}
@interface NSObject(JPInjecting)
@end
@implementation NSObject(JPInjecting)
+ (void)load {
NSError *iError;
NSError *cError;
[self jp_swizzleClassMethod:@selector(resolveInstanceMethod:)
withClassMethod:@selector(jpinjecting_resolveInstanceMethod:)
error:&iError];
[self jp_swizzleClassMethod:@selector(resolveClassMethod:)
withClassMethod:@selector(jpinjecting_resolveClassMethod:)
error:&cError];
NSParameterAssert(!iError);
NSParameterAssert(!cError);
}
+ (BOOL)jpinjecting_resolveClassMethod:(SEL)sel {
if(rs_resolveMethodForObject(self)) {
return YES;
}
return [self jpinjecting_resolveClassMethod:sel];
}
+ (BOOL)jpinjecting_resolveInstanceMethod:(SEL)sel {
if(rs_resolveMethodForObject(self)) {
return YES;
}
return [self jpinjecting_resolveInstanceMethod:sel];
}
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <Foundation/Foundation.h>
#import "JPVideoPlayerCompat.h"
@class AVAssetResourceLoadingRequest,
JPVideoPlayerCacheFile,
JPResourceLoadingRequestTask;
NS_ASSUME_NONNULL_BEGIN
@protocol JPResourceLoadingRequestTaskDelegate<NSObject>
@optional
/**
* This method call when the request received response.
*
* @param requestTask The current instance.
* @param response The response of request.
*/
- (void)requestTask:(JPResourceLoadingRequestTask *)requestTask
didReceiveResponse:(NSURLResponse *)response;
/**
* This method call when the request received data.
*
* @param requestTask The current instance.
* @param data A fragment video data.
*/
- (void)requestTask:(JPResourceLoadingRequestTask *)requestTask
didReceiveData:(NSData *)data;
/**
* This method call when the request task did complete.
*
* @param requestTask The current instance.
* @param error The request error, nil mean success.
*/
- (void)requestTask:(JPResourceLoadingRequestTask *)requestTask
didCompleteWithError:(NSError *)error;
@end
@interface JPResourceLoadingRequestTask : NSObject
@property (nonatomic, weak) id<JPResourceLoadingRequestTaskDelegate> delegate;
/**
* The loadingRequest passed in when this class initialize.
*/
@property (nonatomic, strong, readonly) AVAssetResourceLoadingRequest *loadingRequest;
/**
* The range passed in when this class initialize.
*/
@property(nonatomic, assign, readonly) NSRange requestRange;
/**
* The cache file take responsibility for save video data to disk and read cached video from disk.
*
* @see `JPVideoPlayerCacheFile`.
*/
@property (nonatomic, strong, readonly) JPVideoPlayerCacheFile *cacheFile;
/**
* The url custom passed in.
*/
@property (nonatomic, strong, readonly) NSURL *customURL;
/**
* A flag represent the video file of requestRange is cached on disk or not.
*/
@property(nonatomic, assign, readonly, getter=isCached) BOOL cached;
@property (nonatomic, assign, readonly, getter = isExecuting) BOOL executing;
@property (nonatomic, assign, readonly, getter = isFinished) BOOL finished;
@property (nonatomic, assign, readonly, getter = isCancelled) BOOL cancelled;
/**
* Convenience method to fetch instance of this class.
*
* @param loadingRequest The loadingRequest from `AVPlayer`.
* @param requestRange The range need request from web.
* @param cacheFile The cache file take responsibility for save video data to disk and read cached video from disk.
* @param customURL The url custom passed in.
* @param cached A flag represent the video file of requestRange is cached on disk or not.
*
* @return A instance of this class.
*/
+ (instancetype)requestTaskWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
requestRange:(NSRange)requestRange
cacheFile:(JPVideoPlayerCacheFile *)cacheFile
customURL:(NSURL *)customURL
cached:(BOOL)cached;
/**
* Designated initializer method.
*
* @param loadingRequest The loadingRequest from `AVPlayer`.
* @param requestRange The range need request from web.
* @param cacheFile The cache file take responsibility for save video data to disk and read cached video from disk.
* @param customURL The url custom passed in.
* @param cached A flag represent the video file of requestRange is cached on disk or not.
*
* @return A instance of this class.
*/
- (instancetype)initWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
requestRange:(NSRange)requestRange
cacheFile:(JPVideoPlayerCacheFile *)cacheFile
customURL:(NSURL *)customURL
cached:(BOOL)cached NS_DESIGNATED_INITIALIZER;
/**
* The request did receive response.
*
* @param response The response of request.
*/
- (void)requestDidReceiveResponse:(NSURLResponse *)response;
/**
* The request did receive data.
*
* @param data A fragment video data.
* @param completion Call when store the data finished.
*/
- (void)requestDidReceiveData:(NSData *)data
storedCompletion:(dispatch_block_t)completion;
/**
* The request did finish.
*
* @param error The request error, if nil mean success.
*/
- (void)requestDidCompleteWithError:(NSError *_Nullable)error NS_REQUIRES_SUPER;
/**
* Begins the execution of the task, execute on main queue.
*/
- (void)start NS_REQUIRES_SUPER;
/**
* Begins the execution of the task on given queue.
*
* @param queue A dispatch queue.
*/
- (void)startOnQueue:(dispatch_queue_t)queue NS_REQUIRES_SUPER;
/**
* Advises the task object that it should stop executing its task.
*/
- (void)cancel NS_REQUIRES_SUPER;
@end
@interface JPResourceLoadingRequestLocalTask: JPResourceLoadingRequestTask
@end
@interface JPResourceLoadingRequestWebTask: JPResourceLoadingRequestTask
/**
* The operation's task.
*/
@property (strong, nonatomic, readonly) NSURLSessionDataTask *dataTask;
/**
* The request used by the operation's task.
*/
@property (strong, nonatomic, nullable) NSURLRequest *request;
/**
* The JPVideoPlayerDownloaderOptions for the receiver.
*/
@property (assign, nonatomic) JPVideoPlayerDownloaderOptions options;
/**
* This is weak because it is injected by whoever manages this session.
* If this gets nil-ed out, we won't be able to run.
* the task associated with this operation.
*/
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPResourceLoadingRequestTask.h"
#import "JPVideoPlayerCacheFile.h"
#import "JPVideoPlayerSupportUtils.h"
#import <pthread.h>
#import "JPVideoPlayerCompat.h"
@interface JPResourceLoadingRequestTask()
@property (nonatomic, assign, getter = isExecuting) BOOL executing;
@property (nonatomic, assign, getter = isFinished) BOOL finished;
@property (nonatomic, assign, getter = isCancelled) BOOL cancelled;
@property (nonatomic) pthread_mutex_t lock;
@end
static NSUInteger kJPVideoPlayerFileReadBufferSize = 1024 * 32;
static const NSString *const kJPVideoPlayerContentRangeKey = @"Content-Range";
@implementation JPResourceLoadingRequestTask
- (void)dealloc {
pthread_mutex_destroy(&_lock);
}
- (instancetype)init {
NSAssert(NO, @"Please use given initialize method.");
return [self initWithLoadingRequest:(AVAssetResourceLoadingRequest *)[NSURLRequest new]
requestRange:JPInvalidRange
cacheFile:[JPVideoPlayerCacheFile new]
customURL:[NSURL new]
cached:NO];
}
- (instancetype)initWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
requestRange:(NSRange)requestRange
cacheFile:(JPVideoPlayerCacheFile *)cacheFile
customURL:(NSURL *)customURL
cached:(BOOL)cached {
if(!loadingRequest || !JPValidByteRange(requestRange) || !cacheFile || !customURL){
return nil;
}
self = [super init];
if(self){
_loadingRequest = loadingRequest;
_requestRange = requestRange;
_cacheFile = cacheFile;
_customURL = customURL;
_cached = cached;
_executing = NO;
_cancelled = NO;
_finished = NO;
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_lock, &mutexattr);
}
return self;
}
+ (instancetype)requestTaskWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
requestRange:(NSRange)requestRange
cacheFile:(JPVideoPlayerCacheFile *)cacheFile
customURL:(NSURL *)customURL
cached:(BOOL)cached {
return [[self.class alloc] initWithLoadingRequest:loadingRequest
requestRange:requestRange
cacheFile:cacheFile
customURL:customURL
cached:cached];
}
- (void)requestDidReceiveResponse:(NSURLResponse *)response {
NSAssert(NO, @"You must subclass this class and override this method");
}
- (void)requestDidReceiveData:(NSData *)data
storedCompletion:(dispatch_block_t)completion {
NSAssert(NO, @"You must subclass this class and override this method");
}
- (void)requestDidCompleteWithError:(NSError *_Nullable)error {
JPDispatchSyncOnMainQueue(^{
self.executing = NO;
self.finished = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(requestTask:didCompleteWithError:)]) {
[self.delegate requestTask:self didCompleteWithError:error];
}
});
}
- (void)start {
int lock = pthread_mutex_trylock(&_lock);;
self.executing = YES;
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
- (void)startOnQueue:(dispatch_queue_t)queue {
dispatch_async(queue, ^{
int lock = pthread_mutex_trylock(&_lock);;
self.executing = YES;
if (!lock) {
pthread_mutex_unlock(&_lock);
}
});
}
- (void)cancel {
JPDebugLog(@"调用了 RequestTask 的取消方法");
int lock = pthread_mutex_trylock(&_lock);;
self.executing = NO;
self.cancelled = YES;
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
#pragma mark - Private
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setCancelled:(BOOL)cancelled {
[self willChangeValueForKey:@"isCancelled"];
_cancelled = cancelled;
[self didChangeValueForKey:@"isCancelled"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (NSString *)internalFetchUUID {
CFUUIDRef uuidRef = CFUUIDCreate(NULL);
CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef);
CFRelease(uuidRef);
NSString *uuidValue = (__bridge_transfer NSString *)uuidStringRef;
uuidValue = [uuidValue lowercaseString];
uuidValue = [uuidValue stringByReplacingOccurrencesOfString:@"-" withString:@""];
return uuidValue;
}
@end
@interface JPResourceLoadingRequestLocalTask()
@property (nonatomic) pthread_mutex_t plock;
@end
@implementation JPResourceLoadingRequestLocalTask
- (void)dealloc {
JPDebugLog(@"Local task dealloc");
pthread_mutex_destroy(&_plock);
}
- (instancetype)initWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
requestRange:(NSRange)requestRange
cacheFile:(JPVideoPlayerCacheFile *)cacheFile
customURL:(NSURL *)customURL
cached:(BOOL)cached {
self = [super initWithLoadingRequest:loadingRequest
requestRange:requestRange
cacheFile:cacheFile
customURL:customURL
cached:cached];
if(self){
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_plock, &mutexattr);
if(cacheFile.responseHeaders && !loadingRequest.contentInformationRequest.contentType){
[self fillContentInformation];
}
}
return self;
}
- (void)startOnQueue:(dispatch_queue_t)queue {
[super startOnQueue:queue];
dispatch_async(queue, ^{
[self internalStart];
});
}
- (void)start {
NSAssert(![NSThread isMainThread], @"Do not use main thread when start a local task");
[super start];
[self internalStart];
}
- (void)internalStart {
if ([self isCancelled]) {
[self requestDidCompleteWithError:nil];
return;
}
JPDebugLog(@"开始响应本地请求");
// task fetch data from disk.
int lock = pthread_mutex_trylock(&_plock);
NSUInteger offset = self.requestRange.location;
while (offset < NSMaxRange(self.requestRange)) {
if ([self isCancelled]) {
break;
}
@autoreleasepool {
NSRange range = NSMakeRange(offset, MIN(NSMaxRange(self.requestRange) - offset, kJPVideoPlayerFileReadBufferSize));
NSData *data = [self.cacheFile dataWithRange:range];
[self.loadingRequest.dataRequest respondWithData:data];
offset = NSMaxRange(range);
}
}
JPDebugLog(@"完成本地请求");
if (!lock) {
pthread_mutex_unlock(&_plock);
}
[self requestDidCompleteWithError:nil];
}
- (void)fillContentInformation {
int lock = pthread_mutex_trylock(&_plock);
NSMutableDictionary *responseHeaders = [self.cacheFile.responseHeaders mutableCopy];
BOOL supportRange = responseHeaders[kJPVideoPlayerContentRangeKey] != nil;
if (supportRange && JPValidByteRange(self.requestRange)) {
NSUInteger fileLength = [self.cacheFile fileLength];
NSString *contentRange = [NSString stringWithFormat:@"bytes %tu-%tu/%tu", self.requestRange.location, fileLength, fileLength];
responseHeaders[kJPVideoPlayerContentRangeKey] = contentRange;
}
else {
[responseHeaders removeObjectForKey:kJPVideoPlayerContentRangeKey];
}
NSUInteger contentLength = self.requestRange.length != NSUIntegerMax ? self.requestRange.length : self.cacheFile.fileLength - self.requestRange.location;
responseHeaders[@"Content-Length"] = [NSString stringWithFormat:@"%tu", contentLength];
NSInteger statusCode = supportRange ? 206 : 200;
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:self.loadingRequest.request.URL
statusCode:statusCode
HTTPVersion:@"HTTP/1.1"
headerFields:responseHeaders];
[self.loadingRequest jp_fillContentInformationWithResponse:response];
if (!lock) {
pthread_mutex_unlock(&_plock);
}
}
@end
@interface JPResourceLoadingRequestWebTask()
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
@property (strong, nonatomic) NSURLSessionDataTask *dataTask;
@property(nonatomic, assign) NSUInteger offset;
@property(nonatomic, assign) NSUInteger requestLength;
@property(nonatomic, assign) BOOL haveDataSaved;
@property (nonatomic) pthread_mutex_t plock;
@end
@implementation JPResourceLoadingRequestWebTask
- (void)dealloc {
JPDebugLog(@"Web task dealloc: %@", self);
pthread_mutex_destroy(&_plock);
}
- (instancetype)initWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
requestRange:(NSRange)requestRange
cacheFile:(JPVideoPlayerCacheFile *)cacheFile
customURL:(NSURL *)customURL
cached:(BOOL)cached {
self = [super initWithLoadingRequest:loadingRequest
requestRange:requestRange
cacheFile:cacheFile
customURL:customURL
cached:cached];
if(self){
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_plock, &mutexattr);
_haveDataSaved = NO;
_offset = requestRange.location;
_requestLength = requestRange.length;
}
return self;
}
- (void)start {
[super start];
JPDispatchSyncOnMainQueue(^{
[self internalStart];
});
}
- (void)startOnQueue:(dispatch_queue_t)queue {
[super startOnQueue:queue];
dispatch_async(queue, ^{
[self internalStart];
});
}
- (void)cancel {
if (self.isCancelled || self.isFinished) {
return;
}
[super cancel];
[self synchronizeCacheFileIfNeeded];
if (self.dataTask) {
// cancel web request.
JPDebugLog(@"取消了一个网络请求, id 是: %d", self.dataTask.taskIdentifier);
[self.dataTask cancel];
JPDispatchSyncOnMainQueue(^{
[[NSNotificationCenter defaultCenter] postNotificationName:JPVideoPlayerDownloadStopNotification object:self];
});
}
}
- (BOOL)shouldContinueWhenAppEntersBackground {
return self.options & JPVideoPlayerDownloaderContinueInBackground;
}
- (void)internalStart {
// task request data from web.
if(!self.unownedSession || !self.request){
[self requestDidCompleteWithError:JPErrorWithDescription(@"unownedSession or request can not be nil")];
return;
}
if ([self isCancelled]) {
[self requestDidCompleteWithError:nil];
return;
}
__weak __typeof__ (self) wself = self;
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if(!sself) return;
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}];
}
NSURLSession *session = self.unownedSession;
self.dataTask = [session dataTaskWithRequest:self.request];
JPDebugLog(@"开始网络请求, 网络请求创建一个 dataTask, id 是: %d", self.dataTask.taskIdentifier);
[self.dataTask resume];
if (self.dataTask) {
[[NSNotificationCenter defaultCenter] postNotificationName:JPVideoPlayerDownloadStartNotification object:self];
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
}
- (void)requestDidReceiveResponse:(NSURLResponse *)response {
if ([response isKindOfClass:[NSHTTPURLResponse class]] && !self.loadingRequest.contentInformationRequest.contentType) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
[self.cacheFile storeResponse:httpResponse];
[self.loadingRequest jp_fillContentInformationWithResponse:httpResponse];
if (![(NSHTTPURLResponse *)response jp_supportRange]) {
self.offset = 0;
}
}
}
- (void)requestDidReceiveData:(NSData *)data
storedCompletion:(dispatch_block_t)completion {
if (data.bytes) {
[self.cacheFile storeVideoData:data
atOffset:self.offset
synchronize:NO
storedCompletion:completion];
int lock = pthread_mutex_trylock(&_plock);
self.haveDataSaved = YES;
self.offset += [data length];
[self.loadingRequest.dataRequest respondWithData:data];
static BOOL _needLog = YES;
if(_needLog) {
_needLog = NO;
JPDebugLog(@"收到数据响应, 数据长度为: %u", data.length);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
_needLog = YES;
});
}
if (!lock) {
pthread_mutex_unlock(&_plock);
}
}
}
- (void)requestDidCompleteWithError:(NSError *_Nullable)error {
[self synchronizeCacheFileIfNeeded];
[super requestDidCompleteWithError:error];
}
- (void)synchronizeCacheFileIfNeeded {
if (self.haveDataSaved) {
[self.cacheFile synchronize];
}
}
@end
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "JPVideoPlayerCompat.h"
#import "JPVideoPlayerProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@class JPVideoPlayer,
JPResourceLoadingRequestWebTask,
JPVideoPlayerResourceLoader;
@protocol JPVideoPlayerInternalDelegate <NSObject>
@required
/**
* This method will be called when the current instance receive new loading request.
*
* @param videoPlayer The current `JPVideoPlayer`.
* @prama requestTask A abstract instance packageing the loading request.
*/
- (void)videoPlayer:(JPVideoPlayer *)videoPlayer
didReceiveLoadingRequestTask:(JPResourceLoadingRequestWebTask *)requestTask;
@optional
/**
* Controls which video should automatic replay when the video is playing completed.
*
* @param videoPlayer The current `JPVideoPlayer`.
* @param videoURL The url of the video to be play.
*
* @return Return NO to prevent replay for the video. If not implemented, YES is implied.
*/
- (BOOL)videoPlayer:(nonnull JPVideoPlayer *)videoPlayer
shouldAutoReplayVideoForURL:(nonnull NSURL *)videoURL;
/**
* Notify the player status.
*
* @param videoPlayer The current `JPVideoPlayer`.
* @param playerStatus The current player status.
*/
- (void)videoPlayer:(nonnull JPVideoPlayer *)videoPlayer
playerStatusDidChange:(JPVideoPlayerStatus)playerStatus;
/**
* Notify the playing progress value. this method will be called on main thread.
*
* @param videoPlayer The current `videoPlayer`.
* @param elapsedSeconds The current played seconds.
* @param totalSeconds The total seconds of this video for given url.
*/
- (void)videoPlayerPlayProgressDidChange:(nonnull JPVideoPlayer *)videoPlayer
elapsedSeconds:(double)elapsedSeconds
totalSeconds:(double)totalSeconds;
/**
* Called on some error raise in player.
*
* @param videoPlayer The current instance.
* @param error The error.
*/
- (void)videoPlayer:(nonnull JPVideoPlayer *)videoPlayer
playFailedWithError:(NSError *)error;
@end
@interface JPVideoPlayerModel : NSObject<JPVideoPlayerPlaybackProtocol>
/**
* The current player's layer.
*/
@property (nonatomic, strong, readonly, nullable) AVPlayerLayer *playerLayer;
/**
* The player to play video.
*/
@property (nonatomic, strong, readonly, nullable) AVPlayer *player;
/**
* The resourceLoader for the videoPlayer.
*/
@property (nonatomic, strong, readonly, nullable) JPVideoPlayerResourceLoader *resourceLoader;
/**
* options
*/
@property (nonatomic, assign, readonly) JPVideoPlayerOptions playerOptions;
@end
@interface JPVideoPlayer : NSObject<JPVideoPlayerPlaybackProtocol>
@property (nonatomic, weak, nullable) id<JPVideoPlayerInternalDelegate> delegate;
@property (nonatomic, strong, readonly, nullable) JPVideoPlayerModel *playerModel;
@property (nonatomic, assign, readonly) JPVideoPlayerStatus playerStatus;
/**
* Play the existed video file in disk.
*
* @param url The video url to play.
* @param fullVideoCachePath The full video file path in disk.
* @param showLayer The layer to show the video display layer.
* @param configuration The block will be call when video player config finished. because initialize player is not synchronize,
* so other category method is disabled before config finished.
*
* @return token (@see JPPlayVideoManagerModel) that can be passed to -stopPlayVideo: to stop play.
*/
- (JPVideoPlayerModel *_Nullable)playExistedVideoWithURL:(NSURL *)url
fullVideoCachePath:(NSString *)fullVideoCachePath
options:(JPVideoPlayerOptions)options
showOnLayer:(CALayer *)showLayer
configuration:(JPVideoPlayerConfiguration)configuration;
/**
* Play the not existed video from web.
*
* @param url The video url to play.
* @param options The options to use when downloading the video. @see JPVideoPlayerOptions for the possible values.
* @param showLayer The view to show the video display layer.
* @param configuration The block will be call when video player config finished. because initialize player is not synchronize,
* so other category method is disabled before config finished.
*
* @return token (@see JPPlayVideoManagerModel) that can be passed to -stopPlayVideo: to stop play.
*/
- (JPVideoPlayerModel *_Nullable)playVideoWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
showLayer:(CALayer *)showLayer
configuration:(JPVideoPlayerConfiguration)configuration;
/**
* Call this method to resume play.
*
* @param showLayer The view to show the video display layer.
* @param options The options to use when downloading the video. @see JPVideoPlayerOptions for the possible values.
* @param configuration The block will be call when video player config finished. because initialize player is not synchronize,
* so other category method is disabled before config finished.
*/
- (void)resumePlayWithShowLayer:(CALayer *)showLayer
options:(JPVideoPlayerOptions)options
configuration:(JPVideoPlayerConfiguration)configuration;
/**
* This method used to seek to record playback when hava record playback history.
*/
- (void)seekToTimeWhenRecordPlayback:(CMTime)time;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayer.h"
#import "JPVideoPlayerResourceLoader.h"
#import "UIView+WebVideoCache.h"
#import <pthread.h>
@interface JPVideoPlayerModel()
@property(nonatomic, strong, nullable)NSURL *url;
/**
* The layer of the video picture will show on.
*/
@property(nonatomic, weak, nullable)CALayer *unownedShowLayer;
@property(nonatomic, assign)JPVideoPlayerOptions playerOptions;
@property(nonatomic, strong, nullable)AVPlayer *player;
@property(nonatomic, strong, nullable)AVPlayerLayer *playerLayer;
@property(nonatomic, strong, nullable)AVPlayerItem *playerItem;
@property(nonatomic, strong, nullable)AVURLAsset *videoURLAsset;
@property(nonatomic, assign, getter=isCancelled)BOOL cancelled;
/**
* The resourceLoader for the videoPlayer.
*/
@property(nonatomic, strong, nullable)JPVideoPlayerResourceLoader *resourceLoader;
/**
* The play progress observer.
*/
@property(nonatomic, strong)id timeObserver;
/*
* videoPlayer.
*/
@property(nonatomic, weak) JPVideoPlayer *videoPlayer;
@property(nonatomic, assign) NSTimeInterval elapsedSeconds;
@property(nonatomic, assign) NSTimeInterval totalSeconds;
@end
static NSString *JPVideoPlayerURLScheme = @"com.jpvideoplayer.system.cannot.recognition.scheme.www";
static NSString *JPVideoPlayerURL = @"www.newpan.com";
@implementation JPVideoPlayerModel
#pragma mark - JPVideoPlayerPlaybackProtocol
- (void)setRate:(float)rate {
self.player.rate = rate;
}
- (float)rate {
return self.player.rate;
}
- (void)setMuted:(BOOL)muted {
self.player.muted = muted;
}
- (BOOL)muted {
return self.player.muted;
}
- (void)setVolume:(float)volume {
self.player.volume = volume;
}
- (float)volume {
return self.player.volume;
}
- (BOOL)seekToTime:(CMTime)time {
NSAssert(NO, @"You cannot call this method.");
return NO;
}
- (void)pause {
[self.player pause];
}
- (void)resume {
[self.player play];
}
- (CMTime)currentTime {
return self.player.currentTime;
}
- (void)stopPlay {
self.cancelled = YES;
[self reset];
}
- (void)reset {
// remove video layer from superlayer.
if (self.playerLayer.superlayer) {
[self.playerLayer removeFromSuperlayer];
}
// remove observer.
[self.playerItem removeObserver:self.videoPlayer forKeyPath:@"status"];
[self.playerItem removeObserver:self.videoPlayer forKeyPath:@"playbackLikelyToKeepUp"];
[self.playerItem removeObserver:self.videoPlayer forKeyPath:@"playbackBufferEmpty"];
[self.playerItem removeObserver:self.videoPlayer forKeyPath:@"playbackBufferFull"];
[self.player removeTimeObserver:self.timeObserver];
[self.player removeObserver:self.videoPlayer forKeyPath:@"rate"];
// remove player
[self.player pause];
[self.player cancelPendingPrerolls];
self.player = nil;
[self.videoURLAsset.resourceLoader setDelegate:nil queue:dispatch_get_main_queue()];
self.playerItem = nil;
self.playerLayer = nil;
self.videoURLAsset = nil;
self.resourceLoader = nil;
self.elapsedSeconds = 0;
self.totalSeconds = 0;
}
@end
@interface JPVideoPlayer()<JPVideoPlayerResourceLoaderDelegate>
/**
* The current play video item.
*/
@property(nonatomic, strong, nullable)JPVideoPlayerModel *playerModel;
@property(nonatomic, assign) JPVideoPlayerStatus playerStatus;
@property(nonatomic, assign) BOOL seekingToTime;
@end
@implementation JPVideoPlayer
- (void)dealloc {
[self stopPlay];
[self removePlayerItemDidPlayToEndObserver];
}
- (instancetype)init{
self = [super init];
if (self) {
_playerStatus = JPVideoPlayerStatusUnknown;
_seekingToTime = NO;
}
return self;
}
#pragma mark - Public
- (JPVideoPlayerModel *)playExistedVideoWithURL:(NSURL *)url
fullVideoCachePath:(NSString *)fullVideoCachePath
options:(JPVideoPlayerOptions)options
showOnLayer:(CALayer *)showLayer
configuration:(JPVideoPlayerConfiguration)configuration {
if (!url.absoluteString.length) {
[self callDelegateMethodWithError:JPErrorWithDescription(@"The url is disable")];
return nil;
}
if (fullVideoCachePath.length==0) {
[self callDelegateMethodWithError:JPErrorWithDescription(@"The file path is disable")];
return nil;
}
if (!showLayer) {
[self callDelegateMethodWithError:JPErrorWithDescription(@"The layer to display video layer is nil")];
return nil;
}
if(self.playerModel){
[self.playerModel reset];
self.playerModel = nil;
}
NSURL *videoPathURL = [NSURL fileURLWithPath:fullVideoCachePath];
AVURLAsset *videoURLAsset = [AVURLAsset URLAssetWithURL:videoPathURL options:nil];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:videoURLAsset];
[self removePlayerItemDidPlayToEndObserver];
[self addPlayerItemDidPlayToEndObserver:playerItem];
JPVideoPlayerModel *model = [self playerModelWithURL:url
playerItem:playerItem
options:options
showOnLayer:showLayer];
if (options & JPVideoPlayerMutedPlay) {
model.player.muted = YES;
}
self.playerModel = model;
if(configuration) configuration(model);
return model;
}
- (nullable JPVideoPlayerModel *)playVideoWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
showLayer:(CALayer *)showLayer
configuration:(JPVideoPlayerConfiguration)configuration {
if (!url.absoluteString.length) {
[self callDelegateMethodWithError:JPErrorWithDescription(@"The url is disable")];
return nil;
}
if (!showLayer) {
[self callDelegateMethodWithError:JPErrorWithDescription(@"The layer to display video layer is nil")];
return nil;
}
if(self.playerModel){
[self.playerModel reset];
self.playerModel = nil;
}
// Re-create all all configuration again.
// Make the `resourceLoader` become the delegate of 'videoURLAsset', and provide data to the player.
JPVideoPlayerResourceLoader *resourceLoader = [JPVideoPlayerResourceLoader resourceLoaderWithCustomURL:url];
resourceLoader.delegate = self;
// url instead of `[self composeFakeVideoURL]`, otherwise some urls can not play normally
AVURLAsset *videoURLAsset = [AVURLAsset URLAssetWithURL:[self composeFakeVideoURL] options:nil];
[videoURLAsset.resourceLoader setDelegate:resourceLoader queue:dispatch_get_main_queue()];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:videoURLAsset];
[self removePlayerItemDidPlayToEndObserver];
[self addPlayerItemDidPlayToEndObserver:playerItem];
JPVideoPlayerModel *model = [self playerModelWithURL:url
playerItem:playerItem
options:options
showOnLayer:showLayer];
self.playerModel = model;
model.resourceLoader = resourceLoader;
if (options & JPVideoPlayerMutedPlay) {
model.player.muted = YES;
}
if(configuration) configuration(model);
[self invokePlayerStatusDidChangeDelegateMethod];
return model;
}
- (void)resumePlayWithShowLayer:(CALayer *)showLayer
options:(JPVideoPlayerOptions)options
configuration:(JPVideoPlayerConfiguration)configuration {
JPAssertMainThread;
if (!showLayer) {
[self callDelegateMethodWithError:JPErrorWithDescription(@"The layer to display video layer is nil")];
return;
}
[self.playerModel.playerLayer removeFromSuperlayer];
self.playerModel.unownedShowLayer = showLayer;
if (options & JPVideoPlayerMutedPlay) {
self.playerModel.player.muted = YES;
}
else {
self.playerModel.player.muted = NO;
}
[self setVideoGravityWithOptions:options playerModel:self.playerModel];
[self displayVideoPicturesOnShowLayer];
if(configuration) configuration(self.playerModel);
[self invokePlayerStatusDidChangeDelegateMethod];
}
- (void)seekToTimeWhenRecordPlayback:(CMTime)time {
if(!self.playerModel){
return;
}
if(!CMTIME_IS_VALID(time)){
return;
}
__weak typeof(self) wself = self;
[self.playerModel.player seekToTime:time completionHandler:^(BOOL finished) {
__strong typeof(wself) sself = wself;
if(finished){
[sself internalResumeWithNeedCallDelegate:YES];
}
}];
}
#pragma mark - JPVideoPlayerPlaybackProtocol
- (void)setRate:(float)rate {
if(!self.playerModel){
return;
}
[self.playerModel setRate:rate];
}
- (float)rate {
if(!self.playerModel){
return 0.f;
}
return self.playerModel.rate;
}
- (void)setMuted:(BOOL)muted {
if(!self.playerModel){
return;
}
[self.playerModel setMuted:muted];
}
- (BOOL)muted {
if(!self.playerModel){
return NO;
}
return self.playerModel.muted;
}
- (void)setVolume:(float)volume {
if(!self.playerModel){
return;
}
[self.playerModel setVolume:volume];
}
- (float)volume {
if(!self.playerModel){
return 0.f;
}
return self.playerModel.volume;
}
- (BOOL)seekToTime:(CMTime)time {
if(!self.playerModel || !CMTIME_IS_VALID(time)) return NO;
if (self.playerStatus == JPVideoPlayerStatusUnknown || self.playerStatus == JPVideoPlayerStatusFailed || self.playerStatus == JPVideoPlayerStatusStop) {
return NO;
}
BOOL needResume = self.playerModel.player.rate != 0;
[self internalPauseWithNeedCallDelegate:NO];
self.seekingToTime = YES;
__weak typeof(self) wself = self;
[self.playerModel.player seekToTime:time completionHandler:^(BOOL finished) {
__strong typeof(wself) sself = wself;
sself.seekingToTime = NO;
if(finished && needResume){
[sself internalResumeWithNeedCallDelegate:NO];
}
}];
return YES;
}
- (NSTimeInterval)elapsedSeconds {
return [self.playerModel elapsedSeconds];
}
- (NSTimeInterval)totalSeconds {
return [self.playerModel totalSeconds];
}
- (void)pause {
if(!self.playerModel){
return;
}
[self internalPauseWithNeedCallDelegate:YES];
}
- (void)resume {
if(!self.playerModel){
return;
}
if(self.playerStatus == JPVideoPlayerStatusStop){
self.playerStatus = JPVideoPlayerStatusUnknown;
[self seekToHeaderThenStartPlayback];
return;
}
[self internalResumeWithNeedCallDelegate:YES];
}
- (CMTime)currentTime {
if(!self.playerModel){
return kCMTimeZero;
}
return self.playerModel.currentTime;
}
- (void)stopPlay{
if(!self.playerModel){
return;
}
[self.playerModel stopPlay];
self.playerModel = nil;
self.playerStatus = JPVideoPlayerStatusStop;
[self invokePlayerStatusDidChangeDelegateMethod];
}
#pragma mark - JPVideoPlayerResourceLoaderDelegate
- (void)resourceLoader:(JPVideoPlayerResourceLoader *)resourceLoader
didReceiveLoadingRequestTask:(JPResourceLoadingRequestWebTask *)requestTask {
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayer:didReceiveLoadingRequestTask:)]) {
[self.delegate videoPlayer:self didReceiveLoadingRequestTask:requestTask];
}
}
#pragma mark - App Observer
- (void)addPlayerItemDidPlayToEndObserver:(AVPlayerItem *)playerItem {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidPlayToEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:playerItem];
}
- (void)removePlayerItemDidPlayToEndObserver {
[NSNotificationCenter.defaultCenter removeObserver:self
name:AVPlayerItemDidPlayToEndTimeNotification
object:nil];
}
#pragma mark - AVPlayer Observer
- (void)playerItemDidPlayToEnd:(NSNotification *)notification {
AVPlayerItem *playerItem = notification.object;
if(playerItem != self.playerModel.playerItem) return;
self.playerStatus = JPVideoPlayerStatusStop;
[self invokePlayerStatusDidChangeDelegateMethod];
// ask need automatic replay or not.
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayer:shouldAutoReplayVideoForURL:)]) {
if (![self.delegate videoPlayer:self shouldAutoReplayVideoForURL:self.playerModel.url]) {
return;
}
}
[self seekToHeaderThenStartPlayback];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context{
if (object == self.playerModel.player) {
if([keyPath isEqualToString:@"rate"]) {
float rate = [change[NSKeyValueChangeNewKey] floatValue];
if((rate != 0) && (self.playerStatus == JPVideoPlayerStatusReadyToPlay)){
self.playerStatus = JPVideoPlayerStatusPlaying;
[self invokePlayerStatusDidChangeDelegateMethod];
}
}
}
else if (object == self.playerModel.playerItem) {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItem *playerItem = (AVPlayerItem *)object;
AVPlayerItemStatus status = playerItem.status;
switch (status) {
case AVPlayerItemStatusUnknown:{
JPDebugLog(@"AVPlayerItemStatusUnknown");
self.playerStatus = JPVideoPlayerStatusUnknown;
[self invokePlayerStatusDidChangeDelegateMethod];
}
break;
case AVPlayerItemStatusReadyToPlay:{
JPDebugLog(@"AVPlayerItemStatusReadyToPlay");
self.playerStatus = JPVideoPlayerStatusReadyToPlay;
// When get ready to play note, we can go to play, and can add the video picture on show view.
if (!self.playerModel) return;
[self invokePlayerStatusDidChangeDelegateMethod];
[self.playerModel.player play];
[self displayVideoPicturesOnShowLayer];
}
break;
case AVPlayerItemStatusFailed:{
self.playerStatus = JPVideoPlayerStatusFailed;
JPDebugLog(@"AVPlayerItemStatusFailed");
[self callDelegateMethodWithError:JPErrorWithDescription(@"AVPlayerItemStatusFailed")];
[self invokePlayerStatusDidChangeDelegateMethod];
}
break;
default:
break;
}
}
else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
BOOL playbackLikelyToKeepUp = self.playerModel.playerItem.playbackLikelyToKeepUp;
JPDebugLog(@"%@", playbackLikelyToKeepUp ? @"buffering finished, start to play." : @"start to buffer.");
self.playerStatus = playbackLikelyToKeepUp ? JPVideoPlayerStatusPlaying : JPVideoPlayerStatusBuffering;
[self invokePlayerStatusDidChangeDelegateMethod];
}
else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
BOOL playbackBufferEmpty = self.playerModel.playerItem.playbackBufferEmpty;
JPDebugLog(@"playbackBufferEmpty: %@.", playbackBufferEmpty ? @"empty" : @"not empty");
if (playbackBufferEmpty) {
self.playerStatus = JPVideoPlayerStatusBuffering;
[self invokePlayerStatusDidChangeDelegateMethod];
}
}
else if ([keyPath isEqualToString:@"playbackBufferFull"]) {
BOOL playbackBufferFull = self.playerModel.playerItem.playbackBufferFull;
JPDebugLog(@"playbackBufferFull: %@.", playbackBufferFull ? @"full" : @"not full");
if (playbackBufferFull) {
self.playerStatus = JPVideoPlayerStatusPlaying;
[self invokePlayerStatusDidChangeDelegateMethod];
}
}
}
}
#pragma mark - Private
- (void)seekToHeaderThenStartPlayback {
// Seek the start point of file data and repeat play, this handle have no memory surge.
__weak typeof(self.playerModel) weak_Item = self.playerModel;
__weak typeof(self) wself = self;
[self invokePlayerStatusDidChangeDelegateMethod];
[self.playerModel.player seekToTime:CMTimeMake(0, 1) completionHandler:^(BOOL finished) {
__strong typeof(weak_Item) strong_Item = weak_Item;
__weak typeof(wself) sself = wself;
[strong_Item.player play];
sself.playerStatus = JPVideoPlayerStatusPlaying;
[sself invokePlayerStatusDidChangeDelegateMethod];
}];
}
- (void)invokePlayerStatusDidChangeDelegateMethod {
JPDispatchAsyncOnMainQueue(^{
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayer:playerStatusDidChange:)]) {
[self.delegate videoPlayer:self playerStatusDidChange:self.playerStatus];
}
});
}
- (void)internalPauseWithNeedCallDelegate:(BOOL)needCallDelegate {
[self.playerModel pause];
self.playerStatus = JPVideoPlayerStatusPause;
if(needCallDelegate){
[self invokePlayerStatusDidChangeDelegateMethod];
}
}
- (void)internalResumeWithNeedCallDelegate:(BOOL)needCallDelegate {
[self.playerModel resume];
self.playerStatus = JPVideoPlayerStatusPlaying;
if(needCallDelegate){
[self invokePlayerStatusDidChangeDelegateMethod];
}
}
- (JPVideoPlayerModel *)playerModelWithURL:(NSURL *)url
playerItem:(AVPlayerItem *)playerItem
options:(JPVideoPlayerOptions)options
showOnLayer:(CALayer *)showLayer {
JPVideoPlayerModel *model = [JPVideoPlayerModel new];
model.unownedShowLayer = showLayer;
model.url = url;
model.playerOptions = options;
model.playerItem = playerItem;
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"playbackBufferFull" options:NSKeyValueObservingOptionNew context:nil];
model.player = [AVPlayer playerWithPlayerItem:playerItem];
[model.player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:nil];
if ([model.player respondsToSelector:@selector(automaticallyWaitsToMinimizeStalling)]) {
model.player.automaticallyWaitsToMinimizeStalling = NO;
}
model.playerLayer = [AVPlayerLayer playerLayerWithPlayer:model.player];
[self setVideoGravityWithOptions:options playerModel:model];
model.videoPlayer = self;
self.playerStatus = JPVideoPlayerStatusUnknown;
// add observer for video playing progress.
__weak typeof(model) wItem = model;
__weak typeof(self) wself = self;
[model.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 10) queue:dispatch_get_main_queue() usingBlock:^(CMTime time){
__strong typeof(wItem) sItem = wItem;
__strong typeof(wself) sself = wself;
if (!sItem || !sself) return;
double elapsedSeconds = CMTimeGetSeconds(time);
double totalSeconds = CMTimeGetSeconds(sItem.playerItem.duration);
sself.playerModel.elapsedSeconds = elapsedSeconds;
sself.playerModel.totalSeconds = totalSeconds;
if(totalSeconds == 0 || isnan(totalSeconds) || elapsedSeconds > totalSeconds) return;
if (!sself.seekingToTime) {
JPDispatchSyncOnMainQueue(^{
if (sself.delegate && [sself.delegate respondsToSelector:@selector(videoPlayerPlayProgressDidChange:elapsedSeconds:totalSeconds:)]) {
[sself.delegate videoPlayerPlayProgressDidChange:sself
elapsedSeconds:elapsedSeconds
totalSeconds:totalSeconds];
}
});
}
}];
return model;
}
- (void)setVideoGravityWithOptions:(JPVideoPlayerOptions)options
playerModel:(JPVideoPlayerModel *)playerModel {
NSString *videoGravity = nil;
if (options & JPVideoPlayerLayerVideoGravityResizeAspect) {
videoGravity = AVLayerVideoGravityResizeAspect;
}
else if (options & JPVideoPlayerLayerVideoGravityResize){
videoGravity = AVLayerVideoGravityResize;
}
else if (options & JPVideoPlayerLayerVideoGravityResizeAspectFill){
videoGravity = AVLayerVideoGravityResizeAspectFill;
}
playerModel.playerLayer.videoGravity = videoGravity;
}
- (NSURL *)composeFakeVideoURL {
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:JPVideoPlayerURL] resolvingAgainstBaseURL:NO];
components.scheme = JPVideoPlayerURLScheme;
return [components URL];
}
- (void)displayVideoPicturesOnShowLayer {
if (!self.playerModel.isCancelled && !self.playerModel.playerLayer.superlayer) {
// fixed #26.
self.playerModel.playerLayer.frame = self.playerModel.unownedShowLayer.bounds;
// remove all layer layout animations.
[self.playerModel.unownedShowLayer removeAllAnimations];
[self.playerModel.playerLayer removeAllAnimations];
[self.playerModel.unownedShowLayer addSublayer:self.playerModel.playerLayer];
}
}
- (void)callDelegateMethodWithError:(NSError *)error {
JPErrorLog(@"Player abort because of error: %@", error);
JPDispatchAsyncOnMainQueue(^{
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayer:playFailedWithError:)]) {
[self.delegate videoPlayer:self playFailedWithError:error];
}
});
}
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <Foundation/Foundation.h>
#import "JPVideoPlayerCompat.h"
NS_ASSUME_NONNULL_BEGIN
@interface JPVideoPlayerCacheConfiguration : NSObject
/**
* The maximum length of time to keep an video in the cache, in seconds
*/
@property (assign, nonatomic) NSInteger maxCacheAge;
/**
* The maximum size of the cache, in bytes.
* If the cache Beyond this value, it will delete the video file by the cache time automatic.
*/
@property (assign, nonatomic) NSUInteger maxCacheSize;
/**
* disable iCloud backup [defaults to YES]
*/
@property (assign, nonatomic) BOOL shouldDisableiCloud;
@end
typedef NS_ENUM(NSInteger, JPVideoPlayerCacheType) {
/**
* The video wasn't available the JPVideoPlayer caches.
*/
JPVideoPlayerCacheTypeNone,
/**
* The video was obtained on the disk cache.
*/
JPVideoPlayerCacheTypeExisted,
/**
* A location source.
*/
JPVideoPlayerCacheTypeLocation
};
typedef void(^JPVideoPlayerCacheQueryCompletion)(NSString * _Nullable videoPath, JPVideoPlayerCacheType cacheType);
typedef void(^JPVideoPlayerCheckCacheCompletion)(BOOL isInDiskCache);
typedef void(^JPVideoPlayerCalculateSizeCompletion)(NSUInteger fileCount, NSUInteger totalSize);
/**
* JPVideoPlayerCache maintains a disk cache. Disk cache write operations are performed
* asynchronous so it doesn’t add unnecessary latency to the UI.
*/
@interface JPVideoPlayerCache : NSObject
#pragma mark - Singleton and initialization
/**
* Cache Config object - storing all kind of settings.
*/
@property (nonatomic, readonly) JPVideoPlayerCacheConfiguration *cacheConfiguration;
/**
* Init with given cacheConfig.
*
* @see `JPVideoPlayerCacheConfig`.
*/
- (instancetype)initWithCacheConfiguration:(JPVideoPlayerCacheConfiguration * _Nullable)cacheConfiguration NS_DESIGNATED_INITIALIZER;
/**
* Returns global shared cache instance.
*
* @return JPVideoPlayerCache global instance.
*/
+ (instancetype)sharedCache;
# pragma mark - Query and Retrieve Options
/**
* Async check if video exists in disk cache already (does not load the video).
*
* @param key The key describing the url.
* @param completion The block to be executed when the check is done.
* @note the completion block will be always executed on the main queue.
*/
- (void)diskVideoExistsWithKey:(NSString *)key
completion:(JPVideoPlayerCheckCacheCompletion _Nullable)completion;
/**
* Operation that queries the cache asynchronously and call the completion when done.
*
* @param key The unique key used to store the wanted video.
* @param completion The completion block. Will not get called if the operation is cancelled.
*/
- (void)queryCacheOperationForKey:(NSString *)key
completion:(JPVideoPlayerCacheQueryCompletion _Nullable)completion;
/**
* Async check if video exists in disk cache already (does not load the video).
*
* @param path The path need to check in disk.
*
* @return If the file is existed for given video path, return YES, return NO, otherwise.
*/
- (BOOL)diskVideoExistsOnPath:(NSString *)path;
# pragma mark - Clear Cache Events
/**
* Remove the video data from disk cache asynchronously
*
* @param key The unique video cache key.
* @param completion A block that should be executed after the video has been removed (optional).
*/
- (void)removeVideoCacheForKey:(NSString *)key
completion:(dispatch_block_t _Nullable)completion;
/**
* Async remove all expired cached video from disk. Non-blocking method - returns immediately.
*
* @param completion A block that should be executed after cache expiration completes (optional)
*/
- (void)deleteOldFilesOnCompletion:(dispatch_block_t _Nullable)completion;
/**
* Async clear all disk cached videos. Non-blocking method - returns immediately.
*
* @param completion A block that should be executed after cache expiration completes (optional).
*/
- (void)clearDiskOnCompletion:(dispatch_block_t _Nullable)completion;
/**
* Async clear videos cache in disk on version 2.x.
*
* @param completion A block that should be executed after cache expiration completes (optional).
*/
- (void)clearVideoCacheOnVersion2OnCompletion:(dispatch_block_t _Nullable)completion;
# pragma mark - Cache Info
/**
* To check is have enough free size in disk to cache file with given size.
*
* @param fileSize the need to cache size of file.
*
* @return if the disk have enough size to cache the given size file, return YES, return NO otherwise.
*/
- (BOOL)haveFreeSizeToCacheFileWithSize:(NSUInteger)fileSize;
/**
* Get the free size of device.
*
* @return the free size of device.
*/
- (unsigned long long)getDiskFreeSize;
/**
* Get the size used by the disk cache, synchronously.
*/
- (unsigned long long)getSize;
/**
* Get the number of images in the disk cache, synchronously.
*/
- (NSUInteger)getDiskCount;
/**
* Calculate the disk cache's size, asynchronously .
*/
- (void)calculateSizeOnCompletion:(JPVideoPlayerCalculateSizeCompletion _Nullable)completion;
# pragma mark - File Name
/**
* Generate the video file's name for given key.
*
* @return the file's name.
*/
- (NSString *)cacheFileNameForKey:(NSString *)key;
@end
@interface JPVideoPlayerCache(Deprecated)
/**
* Remove the video data from disk cache asynchronously
*
* @param key The unique video cache key.
* @param completion A block that should be executed after the video has been removed (optional).
*/
- (void)removeFullCacheForKey:(NSString *)key
completion:(dispatch_block_t _Nullable)completion JPDEPRECATED_ATTRIBUTE("`removeFullCacheForKey:completion:` is deprecated on 3.0.")
/**
* Clear the temporary cache video for given key.
*
* @param key The unique flag for the given url in this framework.
* @param completion A block that should be executed after the video has been removed (optional).
*/
- (void)removeTempCacheForKey:(NSString *)key
completion:(nullable dispatch_block_t)completion JPDEPRECATED_ATTRIBUTE("`removeTempCacheForKey:completion:` is deprecated on 3.0.")
/**
* Async delete all temporary cached videos. Non-blocking method - returns immediately.
* @param completion A block that should be executed after cache expiration completes (optional).
*/
- (void)deleteAllTempCacheOnCompletion:(dispatch_block_t _Nullable)completion JPDEPRECATED_ATTRIBUTE("`deleteAllTempCacheOnCompletion:` is deprecated on 3.0.");
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerCache.h"
#import "JPVideoPlayerCachePath.h"
#import "JPVideoPlayerCompat.h"
#import "JPVideoPlayerManager.h"
#import "JPVideoPlayerSupportUtils.h"
#include <sys/param.h>
#include <sys/mount.h>
#import <CommonCrypto/CommonDigest.h>
#import <pthread.h>
static const NSInteger kDefaultCacheMaxCacheAge = 60*60*24*7; // 1 week
static const NSInteger kDefaultCacheMaxSize = 1000*1000*1000; // 1 GB
@implementation JPVideoPlayerCacheConfiguration
- (instancetype)init{
self = [super init];
if (self) {
_maxCacheAge = kDefaultCacheMaxCacheAge;
_maxCacheSize = kDefaultCacheMaxSize;
}
return self;
}
@end
@interface JPVideoPlayerCache()
@property (nonatomic, strong, nonnull) dispatch_queue_t ioQueue;
@property (nonatomic) pthread_mutex_t lock;
@property (nonatomic, strong) NSFileManager *fileManager;
@end
static NSString *kJPVideoPlayerVersion2CacheHasBeenClearedKey = @"com.newpan.version2.cache.clear.key.www";
@implementation JPVideoPlayerCache
- (instancetype)initWithCacheConfiguration:(JPVideoPlayerCacheConfiguration *_Nullable)cacheConfiguration {
self = [super init];
if (self) {
// Create IO serial queue
_ioQueue = dispatch_queue_create("com.NewPan.JPVideoPlayerCache", DISPATCH_QUEUE_SERIAL);
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_lock, &mutexattr);
JPVideoPlayerCacheConfiguration *configuration = cacheConfiguration;
if (!configuration) {
configuration = [[JPVideoPlayerCacheConfiguration alloc] init];
}
_cacheConfiguration = configuration;
_fileManager = [NSFileManager defaultManager];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
return self;
}
- (instancetype)init{
NSAssert(NO, @"please use given init method");
return [self initWithCacheConfiguration:nil];
}
+ (nonnull instancetype)sharedCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [[self alloc] initWithCacheConfiguration:nil];
});
return instance;
}
#pragma mark - Query and Retrieve Options
- (void)diskVideoExistsWithKey:(NSString *)key
completion:(JPVideoPlayerCheckCacheCompletion)completion {
dispatch_async(_ioQueue, ^{
BOOL exists = [self.fileManager fileExistsAtPath:[JPVideoPlayerCachePath videoCachePathForKey:key]];
if (completion) {
JPDispatchSyncOnMainQueue(^{
completion(exists);
});
}
});
}
- (void)queryCacheOperationForKey:(NSString *)key
completion:(JPVideoPlayerCacheQueryCompletion _Nullable)completion {
if (!key) {
if (completion) {
JPDispatchSyncOnMainQueue(^{
completion(nil, JPVideoPlayerCacheTypeNone);
});
}
return;
}
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
BOOL exists = [self.fileManager fileExistsAtPath:[JPVideoPlayerCachePath videoCachePathForKey:key]];
if(!exists){
if (completion) {
JPDispatchSyncOnMainQueue(^{
completion(nil, JPVideoPlayerCacheTypeNone);
});
}
return;
}
if (completion) {
JPDispatchSyncOnMainQueue(^{
completion([JPVideoPlayerCachePath videoCachePathForKey:key], JPVideoPlayerCacheTypeExisted);
});
}
}
});
}
- (BOOL)diskVideoExistsOnPath:(NSString *)path {
return [self.fileManager fileExistsAtPath:path];
}
#pragma mark - Clear Cache Events
- (void)removeVideoCacheForKey:(NSString *)key
completion:(dispatch_block_t _Nullable)completion {
dispatch_async(self.ioQueue, ^{
if ([self.fileManager fileExistsAtPath:[JPVideoPlayerCachePath videoCachePathForKey:key]]) {
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCachePathForKey:key] error:nil];
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCacheIndexFilePathForKey:key] error:nil];
JPDispatchSyncOnMainQueue(^{
if (completion) {
completion();
}
});
}
});
}
- (void)deleteOldFilesOnCompletion:(dispatch_block_t _Nullable)completion {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:[JPVideoPlayerCachePath videoCachePath] isDirectory:YES];
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.cacheConfiguration.maxCacheAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// Enumerate all of the files in the cache directory. This loop has two purposes:
//
// 1. Removing files that are older than the expiration date.
// 2. Storing file attributes for the size-based cleanup pass.
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
@autoreleasepool {
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// Skip directories and errors.
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
}
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
if (self.cacheConfiguration.maxCacheSize > 0 && currentCacheSize > self.cacheConfiguration.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.cacheConfiguration.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completion) {
JPDispatchSyncOnMainQueue(^{
completion();
});
}
});
}
- (void)clearDiskOnCompletion:(nullable dispatch_block_t)completion{
dispatch_async(self.ioQueue, ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCachePathForAllFullFile] error:nil];
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCachePathForAllTemporaryFile] error:nil];
#pragma clang diagnostic pop
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCachePath] error:nil];
JPDispatchSyncOnMainQueue(^{
if (completion) {
completion();
}
});
});
}
- (void)clearVideoCacheOnVersion2OnCompletion:(dispatch_block_t _Nullable)completion {
BOOL version2CacheHasBeenCleared = [NSUserDefaults.standardUserDefaults boolForKey:kJPVideoPlayerVersion2CacheHasBeenClearedKey];
if(version2CacheHasBeenCleared){
if(completion){
completion();
}
return;
}
[NSUserDefaults.standardUserDefaults setBool:YES forKey:kJPVideoPlayerVersion2CacheHasBeenClearedKey];
[NSUserDefaults.standardUserDefaults synchronize];
dispatch_async(self.ioQueue, ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCachePathForAllFullFile] error:nil];
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCachePathForAllTemporaryFile] error:nil];
#pragma clang diagnostic pop
JPDispatchSyncOnMainQueue(^{
if (completion) {
completion();
}
});
});
}
#pragma mark - File Name
- (NSString *)cacheFileNameForKey:(NSString *)key{
return [self cachedFileNameForKey:key];
}
- (NSString *)cachedFileNameForKey:(NSString *)key {
if(!key.length){
JPErrorLog(@"the key is nil");
return nil;
}
NSURL *url = [NSURL URLWithString:key];
if(url){
key = [url jp_cURLCommand];
}
const char *str = key.UTF8String;
if (str == NULL) str = "";
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15]];
return filename;
}
#pragma mark - Cache Info
- (BOOL)haveFreeSizeToCacheFileWithSize:(NSUInteger)fileSize{
unsigned long long freeSizeOfDevice = [self getDiskFreeSize];
if (fileSize > freeSizeOfDevice) {
return NO;
}
return YES;
}
- (unsigned long long)getSize {
__block unsigned long long size = 0;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSString *tempFilePath = [JPVideoPlayerCachePath videoCachePathForAllTemporaryFile];
NSString *fullFilePath = [JPVideoPlayerCachePath videoCachePathForAllFullFile];
#pragma clang diagnostic pop
NSString *videoCachePath = [JPVideoPlayerCachePath videoCachePath];
@autoreleasepool {
NSDirectoryEnumerator *fileEnumerator_temp = [self.fileManager enumeratorAtPath:tempFilePath];
for (NSString *fileName in fileEnumerator_temp) {
NSString *filePath = [tempFilePath stringByAppendingPathComponent:fileName];
NSDictionary<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
NSDirectoryEnumerator *fileEnumerator_full = [self.fileManager enumeratorAtPath:fullFilePath];
for (NSString *fileName in fileEnumerator_full) {
NSString *filePath = [fullFilePath stringByAppendingPathComponent:fileName];
NSDictionary<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
NSDirectoryEnumerator *fileEnumerator_video = [self.fileManager enumeratorAtPath:videoCachePath];
for (NSString *fileName in fileEnumerator_video) {
NSString *filePath = [videoCachePath stringByAppendingPathComponent:fileName];
NSDictionary<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
}
return size;
}
- (NSUInteger)getDiskCount{
__block NSUInteger count = 0;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSString *tempFilePath = [JPVideoPlayerCachePath videoCachePathForAllTemporaryFile];
NSString *fullFilePath = [JPVideoPlayerCachePath videoCachePathForAllFullFile];
#pragma clang diagnostic pop
NSString *videoCachePath = [JPVideoPlayerCachePath videoCachePath];
NSDirectoryEnumerator *fileEnumerator_temp = [self.fileManager enumeratorAtPath:tempFilePath];
count += fileEnumerator_temp.allObjects.count;
NSDirectoryEnumerator *fileEnumerator_full = [self.fileManager enumeratorAtPath:fullFilePath];
count += fileEnumerator_full.allObjects.count;
NSDirectoryEnumerator *fileEnumerator_video = [self.fileManager enumeratorAtPath:videoCachePath];
count += fileEnumerator_video.allObjects.count;
return count;
}
- (void)calculateSizeOnCompletion:(JPVideoPlayerCalculateSizeCompletion _Nullable)completion {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSString *tempFilePath = [JPVideoPlayerCachePath videoCachePathForAllTemporaryFile];
NSString *fullFilePath = [JPVideoPlayerCachePath videoCachePathForAllFullFile];
#pragma clang diagnostic pop
NSString *videoFilePath = [JPVideoPlayerCachePath videoCachePath];
NSURL *diskCacheURL_temp = [NSURL fileURLWithPath:tempFilePath isDirectory:YES];
NSURL *diskCacheURL_full = [NSURL fileURLWithPath:fullFilePath isDirectory:YES];
NSURL *diskCacheURL_video = [NSURL fileURLWithPath:videoFilePath isDirectory:YES];
dispatch_async(self.ioQueue, ^{
NSUInteger fileCount = 0;
NSUInteger totalSize = 0;
NSDirectoryEnumerator *fileEnumerator_temp = [self.fileManager enumeratorAtURL:diskCacheURL_temp includingPropertiesForKeys:@[NSFileSize] options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
for (NSURL *fileURL in fileEnumerator_temp) {
NSNumber *fileSize;
[fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
totalSize += fileSize.unsignedIntegerValue;
fileCount += 1;
}
NSDirectoryEnumerator *fileEnumerator_full = [self.fileManager enumeratorAtURL:diskCacheURL_full includingPropertiesForKeys:@[NSFileSize] options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
for (NSURL *fileURL in fileEnumerator_full) {
NSNumber *fileSize;
[fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
totalSize += fileSize.unsignedIntegerValue;
fileCount += 1;
}
NSDirectoryEnumerator *fileEnumerator_video = [self.fileManager enumeratorAtURL:diskCacheURL_video includingPropertiesForKeys:@[NSFileSize] options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
for (NSURL *fileURL in fileEnumerator_video) {
NSNumber *fileSize;
[fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
totalSize += fileSize.unsignedIntegerValue;
fileCount += 1;
}
if (completion) {
JPDispatchSyncOnMainQueue(^{
completion(fileCount, totalSize);
});
}
});
}
- (unsigned long long)getDiskFreeSize{
struct statfs buf;
unsigned long long freespace = -1;
if(statfs("/var", &buf) >= 0){
freespace = (long long)(buf.f_bsize * buf.f_bfree);
}
return freespace;
}
#pragma mark - Private
- (void)deleteOldFiles {
[self deleteOldFilesOnCompletion:nil];
}
- (void)backgroundDeleteOldFiles {
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Start the long-running task and return immediately.
[self deleteOldFilesOnCompletion:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
@end
@implementation JPVideoPlayerCache(Deprecated)
- (void)removeFullCacheForKey:(NSString *)key
completion:(dispatch_block_t _Nullable)completion{
dispatch_async(self.ioQueue, ^{
if ([self.fileManager fileExistsAtPath:[JPVideoPlayerCachePath videoCacheFullPathForKey:key]]) {
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCacheFullPathForKey:key] error:nil];
JPDispatchSyncOnMainQueue(^{
if (completion) {
completion();
}
});
}
});
}
- (void)removeTempCacheForKey:(NSString *)key
completion:(nullable dispatch_block_t)completion{
dispatch_async(self.ioQueue, ^{
NSString *path = [JPVideoPlayerCachePath videoCachePathForAllTemporaryFile];
path = [path stringByAppendingPathComponent:[[JPVideoPlayerCache sharedCache] cacheFileNameForKey:key]];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:path]) {
[fileManager removeItemAtPath:path error:nil];
JPDispatchSyncOnMainQueue(^{
if (completion) {
completion();
}
});
// For Test.
// printf("Remove temp video data finished, file url string is %@", key);
}
});
}
- (void)deleteAllTempCacheOnCompletion:(nullable dispatch_block_t)completion{
dispatch_async(self.ioQueue, ^{
[self.fileManager removeItemAtPath:[JPVideoPlayerCachePath videoCachePathForAllTemporaryFile] error:nil];
JPDispatchSyncOnMainQueue(^{
if (completion) {
completion();
}
});
});
}
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JPVideoPlayerCacheFile : NSObject
#pragma mark - Properties
/**
* The cache file path of video data.
*/
@property (nonatomic, copy, readonly) NSString *cacheFilePath;
/**
* The index file cache path. because the video datas cache in disk maybe are discontinuous like:
* "01010101*********0101*****010101010101"(the 0 and 1 represent video data, and the * represent no data).
* So we need index to map this video data, and the index file is a colletion of indexes store in disk.
*/
@property (nonatomic, copy, readonly, nullable) NSString *indexFilePath;
/**
* The video data expected length.
* Note this value is not always equal to the cache video data length.
*/
@property (nonatomic, assign, readonly) NSUInteger fileLength;
/**
* The fragment of video data that cached in disk.
*/
@property (nonatomic, strong, readonly, nullable) NSArray<NSValue *> *fragmentRanges;
/**
* The offset of read postion.
*/
@property (nonatomic, assign, readonly) NSUInteger readOffset;
/**
* The response header from web.
*/
@property (nonatomic, copy, readonly, nullable) NSDictionary *responseHeaders;
/**
* A flag represent the video data is cache finished or not.
*/
@property (nonatomic, readonly) BOOL isCompleted;
/**
* A flag represent read offset is point to the end of video data file.
*/
@property (nonatomic, readonly) BOOL isEOF;
/**
* A flag represent file length is greater than 0.
*/
@property (nonatomic, readonly) BOOL isFileLengthValid;
/**
* The cached video data length.
*/
@property (nonatomic, readonly) NSUInteger cachedDataBound;
#pragma mark - Methods
/**
* Convenience method to fetch instance of this class.
* Note this class take responsibility for save video data to disk and read cached video from disk.
*
* @param filePath The video data cache path.
* @param indexFilePath The index file cache path.
*
* @return A instance of this class.
*/
+ (instancetype)cacheFileWithFilePath:(NSString *)filePath
indexFilePath:(NSString *)indexFilePath;
/**
* Designated initializer method.
* Note this class take responsibility for save video data to disk and read cached video from disk.
*
* @param filePath The video data cache path.
* @param indexFilePath The index file cache path.
*
* @return A instance of this class.
*/
- (instancetype)initWithFilePath:(NSString *)filePath
indexFilePath:(NSString *)indexFilePath NS_DESIGNATED_INITIALIZER;
#pragma mark - Store
/**
* Call this method to store video data to disk.
*
* @param data Video data.
* @param offset The offset of the data in video file.
* @param synchronize A flag indicator store index to index file synchronize or not.
* @param completion Call when store the data finished.
*/
- (void)storeVideoData:(NSData *)data
atOffset:(NSUInteger)offset
synchronize:(BOOL)synchronize
storedCompletion:(dispatch_block_t)completion;
/**
* Set the response from web when request video data.
*
* @param response The response from web when request video data.
*
* @return The result of storing response.
*/
- (BOOL)storeResponse:(NSHTTPURLResponse *)response;
/**
* Store index to index file synchronize.
*
* @return The result of store index to index file successed or failed.
*/
- (BOOL)synchronize;
#pragma mark - Read
/**
* Fetch data from the readOffset to given length position.
* Note call `seekToPosition:` to set the readOffset before call this method.
* Note the data not always have video data if the data not cached in disk.
*
* @param length The length of data.
*
* @return Data from the header to given length position.
*/
- (NSData * _Nullable)readDataWithLength:(NSUInteger)length;
/**
* Fetch data in given range.
* Note the data not always have video data if the data not cached in disk.
*
* @param range The range in file.
*
* @return Data in given range.
*/
- (NSData *)dataWithRange:(NSRange)range;
#pragma mark - Remove
/**
* Remove the cached video data.
*/
- (void)removeCache;
#pragma mark - Range
/**
* Fetch the cached video data range for given range.
*
* @param range A given range.
*
* @return The cached video data range for given range.
*/
- (NSRange)cachedRangeForRange:(NSRange)range;
/**
* Fetch the range of cached video data contain given position.
*
* @param position A position point to a point of video file.
*
* @return The range of cached video data contain given position.
*/
- (NSRange)cachedRangeContainsPosition:(NSUInteger)position;
/**
* Find the first range of video data not cached in given postion.
*
* @param position A position point to a point of video file.
*
* @return The first range of video data not cached in given postion.
*/
- (NSRange)firstNotCachedRangeFromPosition:(NSUInteger)position;
#pragma mark - Seek
/**
* Set the fileHandle seek to the given offset.
*
* @param position A position point to a point of video file.
*/
- (void)seekToPosition:(NSUInteger)position;
/**
* Set the fileHandle seek to end of file.
*/
- (void)seekToEnd;
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerCacheFile.h"
#import "JPVideoPlayerCompat.h"
#import "JPVideoPlayerSupportUtils.h"
#import "JPVideoPlayerCompat.h"
#import <pthread.h>
@interface JPVideoPlayerCacheFile()
@property (nonatomic, strong) NSMutableArray<NSValue *> *internalFragmentRanges;
@property (nonatomic, strong) NSFileHandle *writeFileHandle;
@property (nonatomic, strong) NSFileHandle *readFileHandle;
@property(nonatomic, assign) BOOL completed;
@property (nonatomic, assign) NSUInteger fileLength;
@property (nonatomic, assign) NSUInteger readOffset;
@property (nonatomic, copy) NSDictionary *responseHeaders;
@property (nonatomic) pthread_mutex_t lock;
@end
static const NSString *kJPVideoPlayerCacheFileZoneKey = @"com.newpan.zone.key.www";
static const NSString *kJPVideoPlayerCacheFileSizeKey = @"com.newpan.size.key.www";
static const NSString *kJPVideoPlayerCacheFileResponseHeadersKey = @"com.newpan.response.header.key.www";
@implementation JPVideoPlayerCacheFile
+ (instancetype)cacheFileWithFilePath:(NSString *)filePath
indexFilePath:(NSString *)indexFilePath {
return [[self alloc] initWithFilePath:filePath
indexFilePath:indexFilePath];
}
- (instancetype)init {
NSAssert(NO, @"Please use given initializer method");
return [self initWithFilePath:@""
indexFilePath:@""];
}
- (instancetype)initWithFilePath:(NSString *)filePath
indexFilePath:(NSString *)indexFilePath {
if (!filePath.length || !indexFilePath.length) {
JPErrorLog(@"filePath and indexFilePath can not be nil.");
return nil;
}
self = [super init];
if (self) {
_cacheFilePath = filePath;
_indexFilePath = indexFilePath;
_internalFragmentRanges = [[NSMutableArray alloc] init];
_readFileHandle = [NSFileHandle fileHandleForReadingAtPath:_cacheFilePath];
_writeFileHandle = [NSFileHandle fileHandleForWritingAtPath:_cacheFilePath];
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_lock, &mutexattr);
NSString *indexStr = [NSString stringWithContentsOfFile:self.indexFilePath encoding:NSUTF8StringEncoding error:nil];
NSData *data = [indexStr dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *indexDictionary = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers | NSJSONReadingAllowFragments
error:nil];
if (![self serializeIndex:indexDictionary]) {
[self truncateFileWithFileLength:0];
}
[self checkIsCompleted];
}
return self;
}
- (void)dealloc {
[self.readFileHandle closeFile];
[self.writeFileHandle closeFile];
pthread_mutex_destroy(&_lock);
}
#pragma mark - Properties
- (NSUInteger)cachedDataBound {
if (self.internalFragmentRanges.count > 0) {
NSRange range = [[self.internalFragmentRanges lastObject] rangeValue];
return NSMaxRange(range);
}
return 0;
}
- (BOOL)isFileLengthValid {
return self.fileLength != 0;
}
- (BOOL)isCompleted {
return self.completed;
}
- (BOOL)isEOF {
if (self.readOffset + 1 >= self.fileLength) {
return YES;
}
return NO;
}
#pragma mark - Range
- (NSArray<NSValue *> *)fragmentRanges {
return self.internalFragmentRanges;
}
- (void)mergeRangesIfNeed {
JPAssertMainThread;
BOOL isMerge = NO;
for (int i = 0; i < self.internalFragmentRanges.count; ++i) {
if ((i + 1) < self.internalFragmentRanges.count) {
NSRange currentRange = [self.internalFragmentRanges[i] rangeValue];
NSRange nextRange = [self.internalFragmentRanges[i + 1] rangeValue];
if (JPRangeCanMerge(currentRange, nextRange)) {
[self.internalFragmentRanges removeObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, 2)]];
[self.internalFragmentRanges insertObject:[NSValue valueWithRange:NSUnionRange(currentRange, nextRange)] atIndex:i];
i -= 1;
isMerge = YES;
}
}
}
if(isMerge){
NSString *string = @"";
for(NSValue *rangeValue in self.internalFragmentRanges){
NSRange range = [rangeValue rangeValue];
string = [string stringByAppendingString:[NSString stringWithFormat:@"%@; ", NSStringFromRange(range)]];
}
/// JPDebugLog(@"合并后已缓存区间: %@", string);
}
}
- (void)addRange:(NSRange)range
completion:(dispatch_block_t)completion {
if (range.length == 0 || range.location >= self.fileLength) {
return;
}
JPDispatchSyncOnMainQueue(^{
BOOL inserted = NO;
for (int i = 0; i < self.internalFragmentRanges.count; ++i) {
NSRange currentRange = [self.internalFragmentRanges[i] rangeValue];
if (currentRange.location >= range.location) {
[self.internalFragmentRanges insertObject:[NSValue valueWithRange:range] atIndex:i];
inserted = YES;
break;
}
}
if (!inserted) {
[self.internalFragmentRanges addObject:[NSValue valueWithRange:range]];
}
[self mergeRangesIfNeed];
[self checkIsCompleted];
if(completion){
completion();
}
});
}
- (NSRange)cachedRangeForRange:(NSRange)range {
NSRange cachedRange = [self cachedRangeContainsPosition:range.location];
NSRange ret = NSIntersectionRange(cachedRange, range);
if (ret.length > 0) {
return ret;
}
else {
return JPInvalidRange;
}
}
- (NSRange)cachedRangeContainsPosition:(NSUInteger)position {
if (position >= self.fileLength) {
return JPInvalidRange;
}
int lock = pthread_mutex_trylock(&_lock);
for (int i = 0; i < self.internalFragmentRanges.count; ++i) {
NSRange range = [self.internalFragmentRanges[i] rangeValue];
if (NSLocationInRange(position, range)) {
return range;
}
}
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return JPInvalidRange;
}
- (NSRange)firstNotCachedRangeFromPosition:(NSUInteger)position {
if (position >= self.fileLength) {
return JPInvalidRange;
}
int lock = pthread_mutex_trylock(&_lock);
NSRange targetRange = JPInvalidRange;
NSUInteger start = position;
for (int i = 0; i < self.internalFragmentRanges.count; ++i) {
NSRange range = [self.internalFragmentRanges[i] rangeValue];
if (NSLocationInRange(start, range)) {
start = NSMaxRange(range);
}
else {
if (start >= NSMaxRange(range)) {
continue;
}
else {
targetRange = NSMakeRange(start, range.location - start);
}
}
}
if (start < self.fileLength) {
targetRange = NSMakeRange(start, self.fileLength - start);
}
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return targetRange;
}
- (void)checkIsCompleted {
int lock = pthread_mutex_trylock(&_lock);
self.completed = NO;
if (self.internalFragmentRanges && self.internalFragmentRanges.count == 1) {
NSRange range = [self.internalFragmentRanges[0] rangeValue];
if (range.location == 0 && (range.length == self.fileLength)) {
self.completed = YES;
}
}
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
#pragma mark - File
- (BOOL)truncateFileWithFileLength:(NSUInteger)fileLength {
JPDebugLog(@"Truncate file to length: %u", fileLength);
if (!self.writeFileHandle) {
return NO;
}
int lock = pthread_mutex_trylock(&_lock);
self.fileLength = fileLength;
@try {
[self.writeFileHandle truncateFileAtOffset:self.fileLength * sizeof(Byte)];
unsigned long long end = [self.writeFileHandle seekToEndOfFile];
if (end != self.fileLength) {
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return NO;
}
}
@catch (NSException * e) {
JPErrorLog(@"Truncate file raise a exception: %@", e);
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return NO;
}
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return YES;
}
- (void)removeCache {
[[NSFileManager defaultManager] removeItemAtPath:self.cacheFilePath error:NULL];
[[NSFileManager defaultManager] removeItemAtPath:self.indexFilePath error:NULL];
}
- (BOOL)storeResponse:(NSHTTPURLResponse *)response {
BOOL success = YES;
if (![self isFileLengthValid]) {
success = [self truncateFileWithFileLength:(NSUInteger)response.jp_fileLength];
}
self.responseHeaders = [[response allHeaderFields] copy];
success = success && [self synchronize];
return success;
}
- (void)storeVideoData:(NSData *)data
atOffset:(NSUInteger)offset
synchronize:(BOOL)synchronize
storedCompletion:(dispatch_block_t)completion {
if (!self.writeFileHandle) {
JPErrorLog(@"self.writeFileHandle is nil");
}
@try {
[self.writeFileHandle seekToFileOffset:offset];
[self.writeFileHandle jp_safeWriteData:data];
}
@catch (NSException * e) {
JPErrorLog(@"Write file raise a exception: %@", e);
}
[self addRange:NSMakeRange(offset, [data length])
completion:completion];
if (synchronize) {
[self synchronize];
}
}
#pragma mark - read data
- (NSData *)dataWithRange:(NSRange)range {
if (!JPValidFileRange(range)) {
return nil;
}
if (self.readOffset != range.location) {
[self seekToPosition:range.location];
}
return [self readDataWithLength:range.length];
}
- (NSData *)readDataWithLength:(NSUInteger)length {
NSRange range = [self cachedRangeForRange:NSMakeRange(self.readOffset, length)];
if (JPValidFileRange(range)) {
int lock = pthread_mutex_trylock(&_lock);
NSData *data = [self.readFileHandle readDataOfLength:range.length];
self.readOffset += [data length];
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return data;
}
return nil;
}
#pragma mark - seek
- (void)seekToPosition:(NSUInteger)position {
int lock = pthread_mutex_trylock(&_lock);
[self.readFileHandle seekToFileOffset:position];
self.readOffset = (NSUInteger)self.readFileHandle.offsetInFile;
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
- (void)seekToEnd {
int lock = pthread_mutex_trylock(&_lock);
[self.readFileHandle seekToEndOfFile];
self.readOffset = (NSUInteger)self.readFileHandle.offsetInFile;
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
#pragma mark - Index
- (BOOL)serializeIndex:(NSDictionary *)indexDictionary {
if (![indexDictionary isKindOfClass:[NSDictionary class]]) {
return NO;
}
int lock = pthread_mutex_trylock(&_lock);
NSNumber *fileSize = indexDictionary[kJPVideoPlayerCacheFileSizeKey];
if (fileSize && [fileSize isKindOfClass:[NSNumber class]]) {
self.fileLength = [fileSize unsignedIntegerValue];
}
if (self.fileLength == 0) {
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return NO;
}
[self.internalFragmentRanges removeAllObjects];
NSMutableArray *rangeArray = indexDictionary[kJPVideoPlayerCacheFileZoneKey];
for (NSString *rangeStr in rangeArray) {
NSRange range = NSRangeFromString(rangeStr);
[self.internalFragmentRanges addObject:[NSValue valueWithRange:range]];
}
self.responseHeaders = indexDictionary[kJPVideoPlayerCacheFileResponseHeadersKey];
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return YES;
}
- (NSString *)unserializeIndex {
int lock = pthread_mutex_trylock(&_lock);
NSMutableDictionary *dict = [@{
kJPVideoPlayerCacheFileSizeKey: @(self.fileLength),
} mutableCopy];
NSMutableArray *rangeArray = [[NSMutableArray alloc] init];
for (NSValue *range in self.internalFragmentRanges) {
[rangeArray addObject:NSStringFromRange([range rangeValue])];
}
if(rangeArray.count){
dict[kJPVideoPlayerCacheFileZoneKey] = rangeArray;
}
JPDebugLog(@"存储字典: %@", dict);
if (self.responseHeaders) {
dict[kJPVideoPlayerCacheFileResponseHeadersKey] = self.responseHeaders;
}
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
if (data) {
NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return dataString;
}
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return nil;
}
- (BOOL)synchronize {
NSString *indexString = [self unserializeIndex];
int lock = pthread_mutex_trylock(&_lock);
JPDebugLog(@"Did synchronize index file");
[self.writeFileHandle synchronizeFile];
BOOL synchronize = [indexString writeToFile:self.indexFilePath atomically:YES encoding:NSUTF8StringEncoding error:NULL];
if (!lock) {
pthread_mutex_unlock(&_lock);
}
return synchronize;
}
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <UIKit/UIKit.h>
#import "JPVideoPlayerCompat.h"
UIKIT_EXTERN NSString * _Nonnull const JPVideoPlayerCacheVideoPathForTemporaryFile;
UIKIT_EXTERN NSString * _Nonnull const JPVideoPlayerCacheVideoPathForFullFile;
NS_ASSUME_NONNULL_BEGIN
@interface JPVideoPlayerCachePath : NSObject
/**
* Get the video cache path on version 3.x.
*
* @return The file path.
*/
+ (NSString *)videoCachePath;
/**
* Fetch the video cache path for given key on version 3.x.
*
* @param key A given key.
*
* @return The file path.
*/
+ (NSString *)videoCachePathForKey:(NSString *)key;
/**
* Fetch the video cache path and create video file for given key on version 3.x.
*
* @param key A given key.
*
* @return The file path.
*/
+ (NSString *)createVideoFileIfNeedThenFetchItForKey:(NSString *)key;
/**
* Fetch the index file path for given key on version 3.x.
*
* @param key A given key.
*
* @return The path of index file.
*/
+ (NSString *)videoCacheIndexFilePathForKey:(NSString *)key;
/**
* Fetch the index file path and create video index file for given key on version 3.x.
*
* @param key A given key.
*
* @return The path of index file.
*/
+ (NSString *)createVideoIndexFileIfNeedThenFetchItForKey:(NSString *)key;
/**
* Fetch the playback record file path.
*
* @return The path of playback record.
*/
+ (NSString *)videoPlaybackRecordFilePath;
@end
@interface JPVideoPlayerCachePath(Deprecated)
/**
* Get the local video cache path for temporary video file.
*
* @param key The unique flag for the given url in this framework.
*
* @return the temporary file path.
*/
+ (NSString *)videoCacheTemporaryPathForKey:(NSString *)key JPDEPRECATED_ATTRIBUTE("`videoCacheTemporaryPathForKey:` is deprecated on 3.0.")
/**
* Get the local video cache path for all full video file.
*
* @param key The unique flag for the given url in this framework.
*
* @return the full file path.
*/
+ (NSString *)videoCacheFullPathForKey:(NSString *)key JPDEPRECATED_ATTRIBUTE("`videoCacheFullPathForKey:` is deprecated on 3.0.")
/**
* Get the local video cache path for all temporary video file on version 2.x.
*
* @return the temporary file path.
*/
+ (NSString *)videoCachePathForAllTemporaryFile JPDEPRECATED_ATTRIBUTE("`videoCachePathForAllTemporaryFile` is deprecated on 3.0.")
/**
* Get the local video cache path for all full video file on version 2.x.
*
* @return the full file path.
*/
+ (NSString *)videoCachePathForAllFullFile JPDEPRECATED_ATTRIBUTE("`videoCachePathForAllFullFile` is deprecated on 3.0.")
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerCachePath.h"
#import "JPVideoPlayerCache.h"
NSString * const JPVideoPlayerCacheVideoPathForTemporaryFile = @"/TemporaryFile";
NSString * const JPVideoPlayerCacheVideoPathForFullFile = @"/FullFile";
static NSString * const kJPVideoPlayerCacheVideoPathDomain = @"/com.jpvideoplayer.www";
static NSString * const kJPVideoPlayerCacheVideoFileExtension = @".mp4";
static NSString * const kJPVideoPlayerCacheVideoIndexFileExtension = @".index";
static NSString * const kJPVideoPlayerCacheVideoPlaybackRecordFileExtension = @".record";
@implementation JPVideoPlayerCachePath
#pragma mark - Public
+ (NSString *)videoCachePath {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject
stringByAppendingPathComponent:kJPVideoPlayerCacheVideoPathDomain];
if (![fileManager fileExistsAtPath:path]){
[fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
return path;
}
+ (NSString *)videoCachePathForKey:(NSString *)key {
if (!key) {
return nil;
}
NSString *videoCachePath = [self videoCachePath];
NSString *filePath = [videoCachePath stringByAppendingPathComponent:[JPVideoPlayerCache.sharedCache cacheFileNameForKey:key]];
return filePath;
}
+ (NSString *)createVideoFileIfNeedThenFetchItForKey:(NSString *)key {
NSString *filePath = [self videoCachePathForKey:key];
if(!filePath){
return nil;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:filePath]) {
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
}
return filePath;
}
+ (NSString *)videoCacheIndexFilePathForKey:(NSString *)key {
if (!key) {
return nil;
}
NSString *videoCachePath = [self videoCachePath];
NSString *filePath = [videoCachePath stringByAppendingPathComponent:[JPVideoPlayerCache.sharedCache cacheFileNameForKey:key]];
filePath = [filePath stringByAppendingString:kJPVideoPlayerCacheVideoIndexFileExtension];
return filePath;
}
+ (NSString *)createVideoIndexFileIfNeedThenFetchItForKey:(NSString *)key {
NSString *filePath = [self videoCacheIndexFilePathForKey:key];
if(!filePath){
return nil;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:filePath]) {
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
}
return filePath;
}
+ (NSString *)videoPlaybackRecordFilePath {
NSString *filePath = [self videoCachePath];
if(!filePath){
return nil;
}
filePath = [filePath stringByAppendingPathComponent:kJPVideoPlayerCacheVideoPlaybackRecordFileExtension];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:filePath]) {
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
}
return filePath;
}
@end
@implementation JPVideoPlayerCachePath(Deprecated)
+ (NSString *)videoCacheTemporaryPathForKey:(NSString * _Nonnull)key{
if (!key) {
return nil;
}
NSString *path = [self getFilePathWithAppendingString:JPVideoPlayerCacheVideoPathForTemporaryFile];
path = [path stringByAppendingPathComponent:[JPVideoPlayerCache.sharedCache cacheFileNameForKey:key]];
path = [path stringByAppendingString:kJPVideoPlayerCacheVideoFileExtension];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:path]) {
[fileManager createFileAtPath:path contents:nil attributes:nil];
}
return path;
}
+ (NSString *)videoCacheFullPathForKey:(NSString * _Nonnull)key{
if (!key) {
return nil;
}
NSString *path = [self getFilePathWithAppendingString:JPVideoPlayerCacheVideoPathForFullFile];
NSString *fileName = [[JPVideoPlayerCache.sharedCache cacheFileNameForKey:key]
stringByAppendingString:kJPVideoPlayerCacheVideoFileExtension];
path = [path stringByAppendingPathComponent:fileName];
return path;
}
+ (NSString *)videoCachePathForAllTemporaryFile{
return [self getFilePathWithAppendingString:JPVideoPlayerCacheVideoPathForTemporaryFile];
}
+ (NSString *)videoCachePathForAllFullFile{
return [self getFilePathWithAppendingString:JPVideoPlayerCacheVideoPathForFullFile];
}
+ (NSString *)getFilePathWithAppendingString:(nonnull NSString *)apdStr{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject
stringByAppendingPathComponent:apdStr];
if (![fileManager fileExistsAtPath:path]){
[fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
return path;
}
@end
//
// Created by NewPan on 2019-02-24.
// Copyright (c) 2019 NewPan. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger , JPVideoPlayerUnreachableCellType) {
JPVideoPlayerUnreachableCellTypeNone = 0,
JPVideoPlayerUnreachableCellTypeTop,
JPVideoPlayerUnreachableCellTypeDown
};
NS_ASSUME_NONNULL_BEGIN
@protocol JPVideoPlayerCellProtocol <NSObject>
@required
/**
* The video path url.
*
* @note The url may a web url or local file url.
*/
@property (nonatomic, nullable) NSURL *jp_videoURL;
/**
* The view to display video layer.
*/
@property (nonatomic, nullable) UIView *jp_videoPlayView;
/**
* The style of cell cannot stop in screen center.
*/
@property(nonatomic) JPVideoPlayerUnreachableCellType jp_unreachableCellType;
/**
* Returns a Boolean value that indicates whether a given cell is equal to
* the receiver using `jp_videoURL` comparison.
*
* @param cell The cell with which to compare the receiver.
*
* @return YES if cell is equivalent to the receiver (if they have the same `jp_videoURL` comparison), otherwise NO.
*/
- (BOOL)jp_isEqualToCell:(UIView<JPVideoPlayerCellProtocol> *)cell;
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
//
// Created by NewPan on 2019-02-24.
// Copyright (c) 2019 NewPan. All rights reserved.
//
#import "JPVideoPlayerCellProtocol.h"
#import "JPMethodInjecting.h"
@jp_concreteprotocol(JPVideoPlayerCellProtocol)
- (void)setJp_videoURL:(NSURL *)jp_videoURL {
objc_setAssociatedObject(self, @selector(jp_videoURL), jp_videoURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSURL *)jp_videoURL {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setJp_videoPlayView:(UIView *)jp_videoPlayView {
objc_setAssociatedObject(self, @selector(jp_videoPlayView), jp_videoPlayView, OBJC_ASSOCIATION_ASSIGN);
}
- (UIView *)jp_videoPlayView {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setJp_unreachableCellType:(JPVideoPlayerUnreachableCellType)jp_unreachableCellType {
objc_setAssociatedObject(self, @selector(jp_unreachableCellType), @(jp_unreachableCellType), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (JPVideoPlayerUnreachableCellType)jp_unreachableCellType {
return [objc_getAssociatedObject(self, _cmd) integerValue];
}
- (BOOL)jp_isEqualToCell:(UIView<JPVideoPlayerCellProtocol> *)cell {
if(!self.jp_videoURL && !cell.jp_videoURL){
return self == cell;
}
return [self.jp_videoURL.absoluteString isEqualToString:cell.jp_videoURL.absoluteString];
}
@end
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <UIKit/UIKit.h>
#import <AVFoundation/AVAssetResourceLoader.h>
#import <objc/runtime.h>
#import "JPGCDExtensions.h"
@class JPVideoPlayerModel;
#ifndef JPVideoPlayerCompat
#define JPVideoPlayerCompat
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, JPVideoPlayViewInterfaceOrientation) {
JPVideoPlayViewInterfaceOrientationUnknown = 0,
JPVideoPlayViewInterfaceOrientationPortrait,
JPVideoPlayViewInterfaceOrientationLandscape,
};
typedef NS_ENUM(NSUInteger, JPVideoPlayerStatus) {
JPVideoPlayerStatusUnknown = 0,
JPVideoPlayerStatusBuffering,
JPVideoPlayerStatusReadyToPlay,
JPVideoPlayerStatusPlaying,
JPVideoPlayerStatusPause,
JPVideoPlayerStatusFailed,
JPVideoPlayerStatusStop,
};
typedef NS_ENUM(NSUInteger, JPLogLevel) {
// no log output.
JPLogLevelNone = 0,
// output debug, warning and error log.
JPLogLevelError = 1,
// output debug and warning log.
JPLogLevelWarning = 2,
// output debug log.
JPLogLevelDebug = 3,
};
typedef NS_OPTIONS(NSUInteger, JPVideoPlayerOptions) {
/**
* By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
* This flag disable this blacklisting.
*/
JPVideoPlayerRetryFailed = 1 << 0,
/**
* In iOS 4+, continue the download of the video if the app goes to background. This is achieved by asking the system for
* extra time in background to let the request finish. If the background task expires the operation will be cancelled.
*/
JPVideoPlayerContinueInBackground = 1 << 1,
/**
* Handles cookies stored in NSHTTPCookieStore by setting
* NSMutableURLRequest.HTTPShouldHandleCookies = YES;
*/
JPVideoPlayerHandleCookies = 1 << 2,
/**
* Enable to allow untrusted SSL certificates.
* Useful for testing purposes. Use with caution in production.
*/
JPVideoPlayerAllowInvalidSSLCertificates = 1 << 3,
/**
* Playing video muted.
*/
JPVideoPlayerMutedPlay = 1 << 4,
/**
* Stretch to fill layer bounds.
*/
JPVideoPlayerLayerVideoGravityResize = 1 << 5,
/**
* Preserve aspect ratio; fit within layer bounds.
* Default value.
*/
JPVideoPlayerLayerVideoGravityResizeAspect = 1 << 6,
/**
* Preserve aspect ratio; fill layer bounds.
*/
JPVideoPlayerLayerVideoGravityResizeAspectFill = 1 << 7,
// TODO: Disable cache if need.
};
typedef NS_OPTIONS(NSUInteger, JPVideoPlayerDownloaderOptions) {
/**
* Call completion block with nil video/videoData if the image was read from NSURLCache
* (to be combined with `JPVideoPlayerDownloaderUseNSURLCache`).
*/
JPVideoPlayerDownloaderIgnoreCachedResponse = 1 << 0,
/**
* In iOS 4+, continue the download of the video if the app goes to background. This is achieved by asking the system for
* extra time in background to let the request finish. If the background task expires the operation will be cancelled.
*/
JPVideoPlayerDownloaderContinueInBackground = 1 << 1,
/**
* Handles cookies stored in NSHTTPCookieStore by setting
* NSMutableURLRequest.HTTPShouldHandleCookies = YES;
*/
JPVideoPlayerDownloaderHandleCookies = 1 << 2,
/**
* Enable to allow untrusted SSL certificates.
* Useful for testing purposes. Use with caution in production.
*/
JPVideoPlayerDownloaderAllowInvalidSSLCertificates = 1 << 3,
};
typedef void(^JPPlayVideoConfiguration)(UIView *_Nonnull view, JPVideoPlayerModel *_Nonnull playerModel);
typedef void(^JPVideoPlayerConfiguration)(JPVideoPlayerModel *_Nonnull playerModel);
UIKIT_EXTERN NSString * _Nonnull const JPVideoPlayerDownloadStartNotification;
UIKIT_EXTERN NSString * _Nonnull const JPVideoPlayerDownloadReceiveResponseNotification;
UIKIT_EXTERN NSString * _Nonnull const JPVideoPlayerDownloadStopNotification;
UIKIT_EXTERN NSString * _Nonnull const JPVideoPlayerDownloadFinishNotification;
UIKIT_EXTERN NSString *const JPVideoPlayerErrorDomain;
FOUNDATION_EXTERN const NSRange JPInvalidRange;
static JPLogLevel _logLevel;
#define JPDEPRECATED_ATTRIBUTE(msg) __attribute__((deprecated(msg)));
/**
* Call this method to check range valid or not.
*
* @param range The range wanna check valid.
*
* @return Yes means valid, otherwise NO.
*/
BOOL JPValidByteRange(NSRange range);
/**
* Call this method to check range is valid file range or not.
*
* @param range The range wanna check valid.
*
* @return Yes means valid, otherwise NO.
*/
BOOL JPValidFileRange(NSRange range);
/**
* Call this method to check the end point of range1 is equal to the start point of range2,
* or the end point of range2 is equal to the start point of range2,
* or this two range have intersection and the intersection greater than 0.
*
* @param range1 A file range.
* @param range2 A file range.
*
* @return YES means those two range can be merge, otherwise NO.
*/
BOOL JPRangeCanMerge(NSRange range1, NSRange range2);
/**
* Convert a range to HTTP range header string.
*
* @param range A range.
*
* @return HTTP range header string
*/
NSString* JPRangeToHTTPRangeHeader(NSRange range);
/**
* Generate error object with error message.
*
* @param description The error message.
*
* @return A `NSError` object.
*/
NSError *JPErrorWithDescription(NSString *description);
#endif
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerCompat.h"
#import <AVFoundation/AVFoundation.h>
NSString *const JPVideoPlayerDownloadStartNotification = @"www.jpvideplayer.download.start.notification";
NSString *const JPVideoPlayerDownloadReceiveResponseNotification = @"www.jpvideoplayer.download.received.response.notification";
NSString *const JPVideoPlayerDownloadStopNotification = @"www.jpvideplayer.download.stop.notification";
NSString *const JPVideoPlayerDownloadFinishNotification = @"www.jpvideplayer.download.finished.notification";
NSString *const JPVideoPlayerErrorDomain = @"com.jpvideoplayer.error.domain.www";
const NSRange JPInvalidRange = {NSNotFound, 0};
BOOL JPValidByteRange(NSRange range) {
return ((range.location != NSNotFound) || (range.length > 0));
}
BOOL JPValidFileRange(NSRange range) {
return ((range.location != NSNotFound) && range.length > 0 && range.length != NSUIntegerMax);
}
BOOL JPRangeCanMerge(NSRange range1, NSRange range2) {
return (NSMaxRange(range1) == range2.location) || (NSMaxRange(range2) == range1.location) || NSIntersectionRange(range1, range2).length > 0;
}
NSString* JPRangeToHTTPRangeHeader(NSRange range) {
if (JPValidByteRange(range)) {
if (range.location == NSNotFound) {
return [NSString stringWithFormat:@"bytes=-%tu",range.length];
}
else if (range.length == NSUIntegerMax) {
return [NSString stringWithFormat:@"bytes=%tu-",range.location];
}
else {
return [NSString stringWithFormat:@"bytes=%tu-%tu",range.location, NSMaxRange(range) - 1];
}
}
else {
return nil;
}
}
NSError *JPErrorWithDescription(NSString *description) {
assert(description);
if(!description.length){
return nil;
}
return [NSError errorWithDomain:JPVideoPlayerErrorDomain
code:0 userInfo:@{
NSLocalizedDescriptionKey : description
}];
}
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerProtocol.h"
@class JPVideoPlayerControlProgressView,
JPVideoPlayerControlView;
NS_ASSUME_NONNULL_BEGIN
UIKIT_EXTERN NSString *JPVideoPlayerControlProgressViewUserDidStartDragNotification;
UIKIT_EXTERN NSString *JPVideoPlayerControlProgressViewUserDidEndDragNotification;
@interface JPVideoPlayerControlProgressView : UIView<JPVideoPlayerControlProgressProtocol>
@property (nonatomic, strong, readonly) NSArray<NSValue *> *rangesValue;
@property (nonatomic, assign, readonly) NSUInteger fileLength;
@property (nonatomic, assign, readonly) NSTimeInterval totalSeconds;
@property (nonatomic, assign, readonly) NSTimeInterval elapsedSeconds;
@property (nonatomic, weak, readonly, nullable) UIView *playerView;
@property (nonatomic, strong, readonly) UISlider *dragSlider;
@property (nonatomic, strong, readonly) UIView *cachedProgressView;
@property (nonatomic, strong, readonly) UIProgressView *trackProgressView;
@end
@interface JPVideoPlayerControlBar : UIView<JPVideoPlayerProtocol>
@property (nonatomic, strong, readonly) UIButton *playButton;
@property (nonatomic, strong, readonly) UIView<JPVideoPlayerControlProgressProtocol> *progressView;
@property (nonatomic, strong, readonly) UILabel *timeLabel;
@property (nonatomic, strong, readonly) UIButton *landscapeButton;
- (instancetype)initWithProgressView:(UIView<JPVideoPlayerControlProgressProtocol> *_Nullable)progressView NS_DESIGNATED_INITIALIZER;
@end
@interface JPVideoPlayerControlView : UIView<JPVideoPlayerProtocol>
@property (nonatomic, strong, readonly) UIView<JPVideoPlayerProtocol> *controlBar;
@property (nonatomic, strong, readonly) UIImage *blurImage;
/**
* A designated initializer.
*
* @param controlBar The view abide by the `JPVideoPlayerProgressProtocol`.
* @param blurImage A image on back of controlBar.
*
* @return The current instance.
*/
- (instancetype)initWithControlBar:(UIView<JPVideoPlayerProtocol> *_Nullable)controlBar
blurImage:(UIImage *_Nullable)blurImage NS_DESIGNATED_INITIALIZER;
@end
UIKIT_EXTERN const CGFloat JPVideoPlayerProgressViewElementHeight;
@interface JPVideoPlayerProgressView : UIView<JPVideoPlayerProtocol>
@property (nonatomic, strong, readonly) NSArray<NSValue *> *rangesValue;
@property (nonatomic, assign, readonly) NSUInteger fileLength;
@property (nonatomic, assign, readonly) NSTimeInterval totalSeconds;
@property (nonatomic, assign, readonly) NSTimeInterval elapsedSeconds;
@property (nonatomic, strong, readonly) UIProgressView *trackProgressView;
@property (nonatomic, strong, readonly) UIView *cachedProgressView;
@property (nonatomic, strong, readonly) UIProgressView *elapsedProgressView;
@end
@interface JPVideoPlayerBufferingIndicator : UIView<JPVideoPlayerBufferingProtocol>
@property (nonatomic, strong, readonly)UIActivityIndicatorView *activityIndicator;
@property (nonatomic, strong, readonly)UIVisualEffectView *blurView;
@property (nonatomic, assign, readonly, getter=isAnimating)BOOL animating;
@end
@interface JPVideoPlayerView : UIView
/**
* A placeholderView to custom your own business.
*/
@property (nonatomic, strong, readonly) UIView *placeholderView;
/**
* A layer to display video layer.
*/
@property (nonatomic, strong, readonly) CALayer *videoContainerLayer;
/**
* A placeholder view to display controlView
*/
@property (nonatomic, strong, readonly) UIView *controlContainerView;
/**
* A placeholder view to display progress view.
*/
@property (nonatomic, strong, readonly) UIView *progressContainerView;
/**
* A placeholder view to display buffering indicator view.
*/
@property (nonatomic, strong, readonly) UIView *bufferingIndicatorContainerView;
/**
* A view to receive user interaction.
*/
@property (nonatomic, strong, readonly) UIView *userInteractionContainerView;
/**
* To control need automatic hide controlView when user touched.
*/
@property (nonatomic, assign, readonly) BOOL needAutoHideControlViewWhenUserTapping;
- (instancetype)initWithNeedAutoHideControlViewWhenUserTapping:(BOOL)needAutoHideControlViewWhenUserTapping;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerControlViews.h"
#import "JPVideoPlayerCompat.h"
#import "UIView+WebVideoCache.h"
@interface JPVideoPlayerControlProgressView()
@property (nonatomic, strong) NSArray<NSValue *> *rangesValue;
@property(nonatomic, assign) NSUInteger fileLength;
@property(nonatomic, assign) NSTimeInterval totalSeconds;
@property(nonatomic, assign) NSTimeInterval elapsedSeconds;
@property (nonatomic, strong) UISlider *dragSlider;
@property (nonatomic, strong) UIView *cachedProgressView;
@property (nonatomic, strong) UIProgressView *trackProgressView;
@property (nonatomic, weak) UIView *playerView;
@end
static const CGFloat kJPVideoPlayerDragSliderLeftEdge = 2;
static const CGFloat kJPVideoPlayerCachedProgressViewHeight = 2;
NSString *JPVideoPlayerControlProgressViewUserDidStartDragNotification = @"com.jpvideoplayer.progressview.user.drag.start.www";
NSString *JPVideoPlayerControlProgressViewUserDidEndDragNotification = @"com.jpvideoplayer.progressview.user.drag.end.www";;
@implementation JPVideoPlayerControlProgressView {
BOOL _userDragging;
NSTimeInterval _userDragTimeInterval;
}
- (instancetype)init {
self = [super init];
if (self) {
[self _setup];
}
return self;
}
#pragma mark - JPVideoPlayerLayoutProtocol
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation {
CGSize referenceSize = constrainedRect.size;
self.trackProgressView.frame = CGRectMake(kJPVideoPlayerDragSliderLeftEdge,
(referenceSize.height - kJPVideoPlayerCachedProgressViewHeight) * 0.5,
referenceSize.width - 2 * kJPVideoPlayerDragSliderLeftEdge, kJPVideoPlayerCachedProgressViewHeight);
self.dragSlider.frame = constrainedRect;
[self updateCacheProgressViewIfNeed];
[self playProgressDidChangeElapsedSeconds:self.elapsedSeconds
totalSeconds:self.totalSeconds
videoURL:[NSURL new]];
}
#pragma mark - JPVideoPlayerProtocol
- (void)viewWillAddToSuperView:(UIView *)view {
self.playerView = view;
}
- (void)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
playerStatusDidChanged:(JPVideoPlayerStatus)playerStatus {
BOOL userInteractionEnabled = playerStatus != JPVideoPlayerStatusUnknown && playerStatus != JPVideoPlayerStatusFailed && playerStatus != JPVideoPlayerStatusStop;
self.dragSlider.userInteractionEnabled = userInteractionEnabled;
}
- (void)viewWillPrepareToReuse {
[self cacheRangeDidChange:@[] videoURL:[NSURL new]];
[self playProgressDidChangeElapsedSeconds:0
totalSeconds:1
videoURL:[NSURL new]];
}
- (void)cacheRangeDidChange:(NSArray<NSValue *> *)cacheRanges
videoURL:(NSURL *)videoURL {
_rangesValue = cacheRanges;
[self updateCacheProgressViewIfNeed];
}
- (void)playProgressDidChangeElapsedSeconds:(NSTimeInterval)elapsedSeconds
totalSeconds:(NSTimeInterval)totalSeconds
videoURL:(NSURL *)videoURL {
if(self.userDragging) return;
if(totalSeconds == 0) totalSeconds = 1;
float delta = (float)(elapsedSeconds / totalSeconds);
if (delta < 0 || delta > 1) {
JPErrorLog(@"delta must between [0, 1]");
}
delta = MIN(1, delta);
delta = MAX(0, delta);
[self.dragSlider setValue:delta animated:YES];
self.totalSeconds = totalSeconds;
self.elapsedSeconds = elapsedSeconds;
}
- (void)didFetchVideoFileLength:(NSUInteger)videoLength
videoURL:(NSURL *)videoURL {
self.fileLength = videoLength;
}
- (void)videoPlayerStatusDidChange:(JPVideoPlayerStatus)playerStatus
videoURL:(NSURL *)videoURL {
BOOL userInteractionEnabled = playerStatus != JPVideoPlayerStatusUnknown && playerStatus != JPVideoPlayerStatusFailed && playerStatus != JPVideoPlayerStatusStop;
self.dragSlider.userInteractionEnabled = userInteractionEnabled;
}
- (void)videoPlayerInterfaceOrientationDidChange:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation
videoURL:(NSURL *)videoURL {
}
- (void)setUserDragging:(BOOL)userDragging {
[self willChangeValueForKey:@"userDragging"];
_userDragging = userDragging;
[self didChangeValueForKey:@"userDragging"];
}
- (BOOL)userDragging {
return _userDragging;
}
- (void)setUserDragTimeInterval:(NSTimeInterval)userDragTimeInterval {
[self willChangeValueForKey:@"userDragTimeInterval"];
_userDragTimeInterval = userDragTimeInterval;
[self didChangeValueForKey:@"userDragTimeInterval"];
}
- (NSTimeInterval)userDragTimeInterval {
return _userDragTimeInterval;
}
#pragma mark - Private
- (void)_setup {
NSBundle *bundle = [NSBundle bundleForClass:[JPVideoPlayer class]];
NSString *bundlePath = [bundle pathForResource:@"JPVideoPlayer" ofType:@"bundle"];
self.trackProgressView = ({
UIProgressView *view = [UIProgressView new];
view.trackTintColor = [UIColor colorWithWhite:1 alpha:0.15];
[self addSubview:view];
view;
});
self.cachedProgressView = ({
UIView *view = [UIView new];
[self.trackProgressView addSubview:view];
view.clipsToBounds = YES;
view.layer.cornerRadius = 1;
view.backgroundColor = [UIColor colorWithWhite:1 alpha:0.3];
view;
});
self.dragSlider = ({
UISlider *view = [UISlider new];
[view setThumbImage:[UIImage imageNamed:[bundlePath stringByAppendingPathComponent:@"jp_videoplayer_progress_handler_normal"]] forState:UIControlStateNormal];
[view setThumbImage:[UIImage imageNamed:[bundlePath stringByAppendingPathComponent:@"jp_videoplayer_progress_handler_hightlight"]] forState:UIControlStateHighlighted];
view.maximumTrackTintColor = [UIColor clearColor];
[view addTarget:self action:@selector(dragSliderDidDrag:) forControlEvents:UIControlEventValueChanged];
[view addTarget:self action:@selector(dragSliderDidStart:) forControlEvents:UIControlEventTouchDown];
[view addTarget:self action:@selector(dragSliderDidEnd:) forControlEvents:UIControlEventTouchUpInside];
view.userInteractionEnabled = NO;
[self addSubview:view];
view;
});
}
- (void)dragSliderDidStart:(UISlider *)slider {
self.userDragging = YES;
[NSNotificationCenter.defaultCenter postNotificationName:JPVideoPlayerControlProgressViewUserDidStartDragNotification object:self];
}
- (void)dragSliderDidDrag:(UISlider *)slider {
self.userDragTimeInterval = slider.value * self.totalSeconds;
}
- (void)dragSliderDidEnd:(UISlider *)slider {
self.userDragging = NO;
[self userDidFinishDrag];
[NSNotificationCenter.defaultCenter postNotificationName:JPVideoPlayerControlProgressViewUserDidEndDragNotification object:self];
}
- (void)userDidFinishDrag {
if(!self.totalSeconds)return;
[self updateCacheProgressViewIfNeed];
[self.playerView jp_seekToTime:CMTimeMakeWithSeconds([self fetchElapsedTimeInterval], 1000)];
}
- (void)updateCacheProgressViewIfNeed {
[self displayCacheProgressViewIfNeed];
}
- (void)removeCacheProgressViewIfNeed {
if(self.cachedProgressView.superview){
[self.cachedProgressView removeFromSuperview];
}
}
- (void)displayCacheProgressViewIfNeed {
if(self.userDragging || !self.rangesValue.count){
return;
}
[self removeCacheProgressViewIfNeed];
NSRange targetRange = JPInvalidRange;
NSUInteger dragStartLocation = [self fetchDragStartLocation];
if(self.rangesValue.count == 1){
if(JPValidFileRange([self.rangesValue.firstObject rangeValue])){
targetRange = [self.rangesValue.firstObject rangeValue];
}
}
else {
// find the range that the closest to dragStartLocation.
for(NSValue *value in self.rangesValue){
NSRange range = [value rangeValue];
NSUInteger distance = NSUIntegerMax;
if(JPValidFileRange(range)){
if(NSLocationInRange(dragStartLocation, range)){
targetRange = range;
break;
}
else {
int deltaDistance = abs((int)(range.location - dragStartLocation));
deltaDistance = abs((int)(NSMaxRange(range) - dragStartLocation)) < deltaDistance ?: deltaDistance;
if(deltaDistance < distance){
distance = deltaDistance;
targetRange = range;
}
}
}
}
}
if(!JPValidFileRange(targetRange)){
return;
}
if(self.fileLength == 0){
return;
}
CGFloat cacheProgressViewOriginX = targetRange.location * self.trackProgressView.bounds.size.width / self.fileLength;
CGFloat cacheProgressViewWidth = targetRange.length * self.trackProgressView.bounds.size.width / self.fileLength;
self.cachedProgressView.frame = CGRectMake(cacheProgressViewOriginX, 0, cacheProgressViewWidth, self.trackProgressView.bounds.size.height);
[self.trackProgressView addSubview:self.cachedProgressView];
}
- (NSUInteger)fetchDragStartLocation {
return self.fileLength * self.dragSlider.value;
}
- (NSTimeInterval)fetchElapsedTimeInterval {
return self.dragSlider.value * self.totalSeconds;
}
@end
@interface JPVideoPlayerControlBar()<JPVideoPlayerProtocol>
@property (nonatomic, strong) UIButton *playButton;
@property (nonatomic, strong) UIView<JPVideoPlayerControlProgressProtocol> *progressView;
@property (nonatomic, strong) UILabel *timeLabel;
@property (nonatomic, strong) UIButton *landscapeButton;
@property (nonatomic, weak) UIView *playerView;
@property(nonatomic, assign) NSTimeInterval totalSeconds;
@end
static const CGFloat kJPVideoPlayerControlBarButtonWidthHeight = 22;
static const CGFloat kJPVideoPlayerControlBarElementGap = 16;
static const CGFloat kJPVideoPlayerControlBarTimeLabelWidth = 68;
@implementation JPVideoPlayerControlBar
- (void)dealloc {
[self.progressView removeObserver:self forKeyPath:@"userDragTimeInterval"];
}
- (instancetype)initWithFrame:(CGRect)frame {
NSAssert(NO, @"Please use given method to initialize this class.");
return [self initWithProgressView:nil];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
NSAssert(NO, @"Please use given method to initialize this class.");
return [self initWithProgressView:nil];
}
- (instancetype)initWithProgressView:(UIView <JPVideoPlayerControlProgressProtocol> *_Nullable)progressView {
self = [super initWithFrame:CGRectZero];
if (self) {
_progressView = progressView;
[self _setup];
}
return self;
}
- (instancetype)init {
NSAssert(NO, @"Please use given method to initialize this class.");
return [self initWithProgressView:nil];
}
#pragma mark - JPVideoPlayerLayoutProtocol
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation {
CGSize referenceSize = constrainedRect.size;
CGFloat elementOriginY = (referenceSize.height - kJPVideoPlayerControlBarButtonWidthHeight) * 0.5;
self.playButton.frame = CGRectMake(kJPVideoPlayerControlBarElementGap,
elementOriginY,
kJPVideoPlayerControlBarButtonWidthHeight,
kJPVideoPlayerControlBarButtonWidthHeight);
self.landscapeButton.frame = CGRectMake(referenceSize.width - kJPVideoPlayerControlBarElementGap - kJPVideoPlayerControlBarButtonWidthHeight,
elementOriginY,
kJPVideoPlayerControlBarButtonWidthHeight,
kJPVideoPlayerControlBarButtonWidthHeight);
self.timeLabel.frame = CGRectMake(self.landscapeButton.frame.origin.x - kJPVideoPlayerControlBarTimeLabelWidth - kJPVideoPlayerControlBarElementGap,
elementOriginY,
kJPVideoPlayerControlBarTimeLabelWidth,
kJPVideoPlayerControlBarButtonWidthHeight);
CGFloat progressViewOriginX = self.playButton.frame.origin.x + self.playButton.frame.size.width + kJPVideoPlayerControlBarElementGap;
CGFloat progressViewWidth = self.timeLabel.frame.origin.x - progressViewOriginX - kJPVideoPlayerControlBarElementGap;
self.progressView.frame = CGRectMake(progressViewOriginX,
elementOriginY,
progressViewWidth,
kJPVideoPlayerControlBarButtonWidthHeight);
if([self.progressView respondsToSelector:@selector(layoutThatFits:nearestViewControllerInViewTree:interfaceOrientation:)]){
[self.progressView layoutThatFits:self.progressView.bounds
nearestViewControllerInViewTree:nearestViewController
interfaceOrientation:interfaceOrientation];
}
}
#pragma mark - JPVideoPlayerProtocol
- (void)viewWillAddToSuperView:(UIView *)view {
self.playerView = view;
[self updateTimeLabelWithElapsedSeconds:0 totalSeconds:0];
[self.progressView viewWillAddToSuperView:view];
}
- (void)viewWillPrepareToReuse {
[self updateTimeLabelWithElapsedSeconds:0 totalSeconds:0];
[self.progressView viewWillPrepareToReuse];
}
- (void)cacheRangeDidChange:(NSArray<NSValue *> *)cacheRanges
videoURL:(NSURL *)videoURL {
[self.progressView cacheRangeDidChange:cacheRanges
videoURL:videoURL];
}
- (void)playProgressDidChangeElapsedSeconds:(NSTimeInterval)elapsedSeconds
totalSeconds:(NSTimeInterval)totalSeconds
videoURL:(NSURL *)videoURL {
self.totalSeconds = totalSeconds;
if(!self.progressView.userDragging){
[self updateTimeLabelWithElapsedSeconds:elapsedSeconds totalSeconds:totalSeconds];
}
[self.progressView playProgressDidChangeElapsedSeconds:elapsedSeconds
totalSeconds:totalSeconds
videoURL:videoURL];
}
- (void)didFetchVideoFileLength:(NSUInteger)videoLength
videoURL:(NSURL *)videoURL {
[self.progressView didFetchVideoFileLength:videoLength
videoURL:videoURL];
}
- (void)videoPlayerStatusDidChange:(JPVideoPlayerStatus)playerStatus
videoURL:(NSURL *)videoURL {
BOOL isPlaying = playerStatus == JPVideoPlayerStatusBuffering || playerStatus == JPVideoPlayerStatusPlaying || playerStatus == JPVideoPlayerStatusReadyToPlay;
self.playButton.selected = !isPlaying;
[self.progressView videoPlayerStatusDidChange:playerStatus
videoURL:videoURL];
}
- (void)videoPlayerInterfaceOrientationDidChange:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation
videoURL:(NSURL *)videoURL {
self.landscapeButton.selected = interfaceOrientation == JPVideoPlayViewInterfaceOrientationLandscape;
[self.progressView videoPlayerInterfaceOrientationDidChange:interfaceOrientation
videoURL:videoURL];
}
#pragma mark - Private
- (void)updateTimeLabelWithElapsedSeconds:(NSTimeInterval)elapsedSeconds
totalSeconds:(NSTimeInterval)totalSeconds {
NSString *elapsedString = [self convertSecondsToTimeString:elapsedSeconds];
NSString *totalString = [self convertSecondsToTimeString:totalSeconds];
self.timeLabel.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@/%@", elapsedString, totalString]
attributes:@{
NSFontAttributeName : [UIFont systemFontOfSize:10],
NSForegroundColorAttributeName : [UIColor whiteColor]
}];
}
- (NSString *)convertSecondsToTimeString:(NSTimeInterval)seconds {
NSUInteger minute = (NSUInteger)(seconds / 60);
NSUInteger second = (NSUInteger)((NSUInteger)seconds % 60);
return [NSString stringWithFormat:@"%02d:%02d", (int)minute, (int)second];
}
- (void)playButtonDidClick:(UIButton *)button {
button.selected = !button.selected;
BOOL isPlay = self.playerView.jp_playerStatus == JPVideoPlayerStatusBuffering ||
self.playerView.jp_playerStatus == JPVideoPlayerStatusPlaying;
isPlay ? [self.playerView jp_pause] : [self.playerView jp_resume];
}
- (void)landscapeButtonDidClick:(UIButton *)button {
button.selected = !button.selected;
self.playerView.jp_viewInterfaceOrientation == JPVideoPlayViewInterfaceOrientationPortrait ? [self.playerView jp_gotoLandscape] : [self.playerView jp_gotoPortrait];
}
- (void)_setup {
NSBundle *bundle = [NSBundle bundleForClass:[JPVideoPlayer class]];
NSString *bundlePath = [bundle pathForResource:@"JPVideoPlayer" ofType:@"bundle"];
self.backgroundColor = [UIColor clearColor];
self.playButton = ({
UIButton *button = [UIButton new];
[button setImage:[UIImage imageNamed:[bundlePath stringByAppendingPathComponent:@"jp_videoplayer_pause"]] forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:[bundlePath stringByAppendingPathComponent:@"jp_videoplayer_play"]] forState:UIControlStateSelected];
[button addTarget:self action:@selector(playButtonDidClick:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
button;
});
if(!self.progressView){
self.progressView = ({
JPVideoPlayerControlProgressView *view = [JPVideoPlayerControlProgressView new];
[view addObserver:self forKeyPath:@"userDragTimeInterval" options:NSKeyValueObservingOptionNew context:nil];
[self addSubview:view];
view;
});
}
self.timeLabel = ({
UILabel *label = [UILabel new];
label.textAlignment = NSTextAlignmentCenter;
[self addSubview:label];
label;
});
self.landscapeButton = ({
UIButton *button = [UIButton new];
[button setImage:[UIImage imageNamed:[bundlePath stringByAppendingPathComponent:@"jp_videoplayer_landscape"]] forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:[bundlePath stringByAppendingPathComponent:@"jp_videoplayer_portrait"]] forState:UIControlStateSelected];
[button addTarget:self action:@selector(landscapeButtonDidClick:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
button;
});
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context {
if([keyPath isEqualToString:@"userDragTimeInterval"]) {
NSNumber *timeIntervalNumber = change[NSKeyValueChangeNewKey];
NSTimeInterval timeInterval = timeIntervalNumber.floatValue;
[self updateTimeLabelWithElapsedSeconds:timeInterval totalSeconds:self.totalSeconds];
}
}
@end
@interface JPVideoPlayerControlView()
@property (nonatomic, strong) UIView<JPVideoPlayerProtocol> *controlBar;
@property (nonatomic, strong) UIImageView *blurImageView;
@end
static const CGFloat kJPVideoPlayerControlBarHeight = 38;
static const CGFloat kJPVideoPlayerControlBarLandscapeUpOffset = 12;
@implementation JPVideoPlayerControlView
- (instancetype)initWithFrame:(CGRect)frame {
NSAssert(NO, @"Please use given method to initialize this class.");
return [self initWithControlBar:nil blurImage:nil];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
NSAssert(NO, @"Please use given method to initialize this class.");
return [self initWithControlBar:nil blurImage:nil];
}
- (instancetype)initWithControlBar:(UIView <JPVideoPlayerProtocol> *)controlBar
blurImage:(UIImage *)blurImage {
self = [super initWithFrame:CGRectZero];
if(self){
_controlBar = controlBar;
_blurImage = blurImage;
[self _setup];
}
return self;
}
- (instancetype)init {
NSAssert(NO, @"Please use given method to initialize this class.");
return [self initWithControlBar:nil blurImage:nil];
}
#pragma mark - JPVideoPlayerLayoutProtocol
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation {
self.blurImageView.frame = constrainedRect;
CGRect controlBarFrame = CGRectMake(0,
constrainedRect.size.height - kJPVideoPlayerControlBarHeight,
constrainedRect.size.width,
kJPVideoPlayerControlBarHeight);
if(interfaceOrientation == JPVideoPlayViewInterfaceOrientationLandscape){ // landscape.
CGFloat controlBarOriginX = 0;
if (@available(iOS 11.0, *)) {
UIEdgeInsets insets = self.window.safeAreaInsets;
controlBarOriginX = insets.bottom;
}
controlBarFrame = CGRectMake(controlBarOriginX,
constrainedRect.size.height - kJPVideoPlayerControlBarHeight - kJPVideoPlayerControlBarLandscapeUpOffset,
constrainedRect.size.width - 2 * controlBarOriginX,
kJPVideoPlayerControlBarHeight);
}
self.controlBar.frame = controlBarFrame;
if([self.controlBar respondsToSelector:@selector(layoutThatFits:nearestViewControllerInViewTree:interfaceOrientation:)]){
[self.controlBar layoutThatFits:self.controlBar.bounds
nearestViewControllerInViewTree:nearestViewController
interfaceOrientation:interfaceOrientation];
}
}
#pragma mark - JPVideoPlayerProtocol
- (void)viewWillAddToSuperView:(UIView *)view {
[self.controlBar viewWillAddToSuperView:view];
}
- (void)viewWillPrepareToReuse {
[self.controlBar viewWillPrepareToReuse];
}
- (void)cacheRangeDidChange:(NSArray<NSValue *> *)cacheRanges
videoURL:(NSURL *)videoURL {
[self.controlBar cacheRangeDidChange:cacheRanges
videoURL:videoURL];
}
- (void)playProgressDidChangeElapsedSeconds:(NSTimeInterval)elapsedSeconds
totalSeconds:(NSTimeInterval)totalSeconds
videoURL:(NSURL *)videoURL {
[self.controlBar playProgressDidChangeElapsedSeconds:elapsedSeconds
totalSeconds:totalSeconds
videoURL:videoURL];
}
- (void)didFetchVideoFileLength:(NSUInteger)videoLength
videoURL:(NSURL *)videoURL {
[self.controlBar didFetchVideoFileLength:videoLength
videoURL:videoURL];
}
- (void)videoPlayerStatusDidChange:(JPVideoPlayerStatus)playerStatus
videoURL:(NSURL *)videoURL {
[self.controlBar videoPlayerStatusDidChange:playerStatus
videoURL:videoURL];
}
- (void)videoPlayerInterfaceOrientationDidChange:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation
videoURL:(NSURL *)videoURL {
[self.controlBar videoPlayerInterfaceOrientationDidChange:interfaceOrientation
videoURL:videoURL];
}
#pragma mark - Private
- (void)_setup {
NSBundle *bundle = [NSBundle bundleForClass:[JPVideoPlayer class]];
NSString *bundlePath = [bundle pathForResource:@"JPVideoPlayer" ofType:@"bundle"];
self.blurImageView = ({
UIImageView *view = [UIImageView new];
UIImage *blurImage = self.blurImage;
if(!blurImage){
blurImage = [UIImage imageNamed:[bundlePath stringByAppendingPathComponent:@"jp_videoplayer_blur"]];
}
view.image = blurImage;
[self addSubview:view];
view;
});
if(!self.controlBar){
self.controlBar = ({
JPVideoPlayerControlBar *bar = [[JPVideoPlayerControlBar alloc] initWithProgressView:nil];
[self addSubview:bar];
bar;
});
}
}
@end
@interface JPVideoPlayerProgressView()
@property (nonatomic, strong) UIProgressView *trackProgressView;
@property (nonatomic, strong) UIView *cachedProgressView;
@property (nonatomic, strong) UIProgressView *elapsedProgressView;
@property (nonatomic, strong) NSArray<NSValue *> *rangesValue;
@property(nonatomic, assign) NSUInteger fileLength;
@property(nonatomic, assign) NSTimeInterval totalSeconds;
@property(nonatomic, assign) NSTimeInterval elapsedSeconds;
@end
const CGFloat JPVideoPlayerProgressViewElementHeight = 2;
@implementation JPVideoPlayerProgressView
- (instancetype)init {
self = [super init];
if(self){
[self _setup];
}
return self;
}
#pragma mark - Setup
- (void)_setup {
self.trackProgressView = ({
UIProgressView *view = [UIProgressView new];
view.trackTintColor = [UIColor colorWithWhite:1 alpha:0.15];
[self addSubview:view];
view;
});
self.cachedProgressView = ({
UIView *view = [UIView new];
view.backgroundColor = [UIColor colorWithWhite:1 alpha:0.3];
[self.trackProgressView addSubview:view];
view;
});
self.elapsedProgressView = ({
UIProgressView *view = [UIProgressView new];
view.trackTintColor = [UIColor clearColor];
[self addSubview:view];
view;
});
}
#pragma mark - JPVideoPlayerLayoutProtocol
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation {
self.trackProgressView.frame = CGRectMake(0,
constrainedRect.size.height - JPVideoPlayerProgressViewElementHeight,
constrainedRect.size.width,
JPVideoPlayerProgressViewElementHeight);
self.cachedProgressView.frame = self.trackProgressView.bounds;
self.elapsedProgressView.frame = self.trackProgressView.frame;
}
#pragma mark - JPVideoPlayerProtocol
- (void)viewWillAddToSuperView:(UIView *)view {
}
- (void)viewWillPrepareToReuse {
[self cacheRangeDidChange:@[] videoURL:[NSURL new]];
[self playProgressDidChangeElapsedSeconds:0
totalSeconds:1
videoURL:[NSURL new]];
}
- (void)cacheRangeDidChange:(NSArray<NSValue *> *)cacheRanges
videoURL:(NSURL *)videoURL {
_rangesValue = cacheRanges;
[self displayCacheProgressViewIfNeed];
}
- (void)playProgressDidChangeElapsedSeconds:(NSTimeInterval)elapsedSeconds
totalSeconds:(NSTimeInterval)totalSeconds
videoURL:(NSURL *)videoURL {
if((NSInteger)totalSeconds == 0){
totalSeconds = 1;
}
float delta = (float)(elapsedSeconds / totalSeconds);
if (delta < 0 || delta > 1) {
JPErrorLog(@"delta must between [0, 1]");
}
delta = MIN(1, delta);
delta = MAX(0, delta);
[self.elapsedProgressView setProgress:delta animated:YES];
self.totalSeconds = totalSeconds;
self.elapsedSeconds = elapsedSeconds;
}
- (void)didFetchVideoFileLength:(NSUInteger)videoLength
videoURL:(NSURL *)videoURL {
self.fileLength = videoLength;
}
- (void)displayCacheProgressViewIfNeed {
if(!self.rangesValue.count){
return;
}
[self removeCacheProgressViewIfNeed];
NSRange targetRange = JPInvalidRange;
NSUInteger dragStartLocation = [self fetchDragStartLocation];
if(self.rangesValue.count == 1){
if(JPValidFileRange([self.rangesValue.firstObject rangeValue])){
targetRange = [self.rangesValue.firstObject rangeValue];
}
}
else {
// find the range that the closest to dragStartLocation.
for(NSValue *value in self.rangesValue){
NSRange range = [value rangeValue];
NSUInteger distance = NSUIntegerMax;
if(JPValidFileRange(range)){
if(NSLocationInRange(dragStartLocation, range)){
targetRange = range;
break;
}
else {
int deltaDistance = abs((int)(range.location - dragStartLocation));
deltaDistance = abs((int)(NSMaxRange(range) - dragStartLocation)) < deltaDistance ?: deltaDistance;
if(deltaDistance < distance){
distance = deltaDistance;
targetRange = range;
}
}
}
}
}
if(!JPValidFileRange(targetRange)){
return;
}
if(self.fileLength == 0){
return;
}
CGFloat cacheProgressViewOriginX = targetRange.location * self.trackProgressView.bounds.size.width / self.fileLength;
CGFloat cacheProgressViewWidth = targetRange.length * self.trackProgressView.bounds.size.width / self.fileLength;
self.cachedProgressView.frame = CGRectMake(cacheProgressViewOriginX, 0, cacheProgressViewWidth, self.trackProgressView.bounds.size.height);
[self.trackProgressView addSubview:self.cachedProgressView];
}
- (void)removeCacheProgressViewIfNeed {
if(self.cachedProgressView.superview){
[self.cachedProgressView removeFromSuperview];
}
}
- (NSUInteger)fetchDragStartLocation {
return self.fileLength * self.elapsedProgressView.progress;
}
@end
@interface JPVideoPlayerBufferingIndicator()
@property(nonatomic, strong)UIActivityIndicatorView *activityIndicator;
@property(nonatomic, strong)UIVisualEffectView *blurView;
@property(nonatomic, assign, getter=isAnimating)BOOL animating;
@property (nonatomic, strong) UIView *blurBackgroundView;
@end
CGFloat const JPVideoPlayerBufferingIndicatorWidthHeight = 46;
@implementation JPVideoPlayerBufferingIndicator
- (instancetype)init {
self = [super init];
if (self) {
[self _setup];
}
return self;
}
#pragma mark - JPVideoPlayerLayoutProtocol
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation {
CGSize referenceSize = constrainedRect.size;
self.blurBackgroundView.frame = CGRectMake((referenceSize.width - JPVideoPlayerBufferingIndicatorWidthHeight) * 0.5f,
(referenceSize.height - JPVideoPlayerBufferingIndicatorWidthHeight) * 0.5f,
JPVideoPlayerBufferingIndicatorWidthHeight,
JPVideoPlayerBufferingIndicatorWidthHeight);
self.activityIndicator.frame = self.blurBackgroundView.bounds;
self.blurView.frame = self.blurBackgroundView.bounds;
}
- (void)startAnimating{
if (!self.isAnimating || self.hidden) {
self.hidden = NO;
[self.activityIndicator startAnimating];
self.animating = YES;
}
}
- (void)stopAnimating{
if (self.isAnimating || !self.hidden) {
self.hidden = YES;
[self.activityIndicator stopAnimating];
self.animating = NO;
}
}
#pragma mark - JPVideoPlayerBufferingProtocol
- (void)didStartBufferingVideoURL:(NSURL *)videoURL {
[self startAnimating];
}
- (void)didFinishBufferingVideoURL:(NSURL *)videoURL {
[self stopAnimating];
}
#pragma mark - Private
- (void)_setup{
self.backgroundColor = [UIColor clearColor];
self.blurBackgroundView = ({
UIView *view = [UIView new];
view.backgroundColor = [UIColor colorWithWhite:1 alpha:0.6];
view.layer.cornerRadius = 10;
view.clipsToBounds = YES;
[self addSubview:view];
view;
});
self.blurView = ({
UIVisualEffectView *blurView = [[UIVisualEffectView alloc]initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]];
[self.blurBackgroundView addSubview:blurView];
blurView;
});
self.activityIndicator = ({
UIActivityIndicatorView *indicator = [UIActivityIndicatorView new];
indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
indicator.color = [UIColor colorWithRed:35.0/255 green:35.0/255 blue:35.0/255 alpha:1];
[self.blurBackgroundView addSubview:indicator];
indicator;
});
self.animating = NO;
}
@end
@interface _JPVideoPlayerPlaceholderView : UIView
@end
@implementation _JPVideoPlayerPlaceholderView
@end
@interface _JPVideoPlayerVideoContainerView : UIView
@end
@implementation _JPVideoPlayerVideoContainerView
@end
@interface _JPVideoPlayerControlContainerView : UIView
@end
@implementation _JPVideoPlayerControlContainerView
@end
@interface _JPVideoPlayerProgressContainerView : UIView
@end
@implementation _JPVideoPlayerProgressContainerView
@end
@interface _JPVideoPlayerBufferingIndicatorContainerView : UIView
@end
@implementation _JPVideoPlayerBufferingIndicatorContainerView
@end
@interface _JPVideoPlayerUserInteractionContainerView : UIView
@end
@implementation _JPVideoPlayerUserInteractionContainerView
@end
@interface JPVideoPlayerView()
@property (nonatomic, strong) UIView *placeholderView;
@property (nonatomic, strong) UIView *videoContainerView;
@property (nonatomic, strong) UIView *controlContainerView;
@property (nonatomic, strong) UIView *progressContainerView;
@property (nonatomic, strong) UIView *bufferingIndicatorContainerView;
@property (nonatomic, strong) UIView *userInteractionContainerView;
@property (nonatomic, strong) NSTimer *timer;
@property(nonatomic, assign) BOOL isInterruptTimer;
@end
static const NSTimeInterval kJPControlViewAutoHiddenTimeInterval = 5;
@implementation JPVideoPlayerView
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self];
}
- (instancetype)initWithNeedAutoHideControlViewWhenUserTapping:(BOOL)needAutoHideControlViewWhenUserTapping {
self = [super init];
if(self){
_needAutoHideControlViewWhenUserTapping = needAutoHideControlViewWhenUserTapping;
[self _setup];
}
return self;
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
self.placeholderView.frame = self.bounds;
self.videoContainerView.frame = self.bounds;
self.controlContainerView.frame = self.bounds;
self.progressContainerView.frame = self.bounds;
self.bufferingIndicatorContainerView.frame = self.bounds;
self.userInteractionContainerView.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height - kJPVideoPlayerControlBarHeight);
[self layoutContainerSubviewsWithBounds:CGRectZero center:CGPointZero frame:frame];
[self callLayoutMethodForContainerSubviews];
}
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
self.videoContainerView.frame = CGRectMake(self.videoContainerView.center.x - bounds.size.width * 0.5,
self.videoContainerView.center.y - bounds.size.height * 0.5,
bounds.size.width,
bounds.size.height);
self.placeholderView.frame = self.videoContainerView.frame;
self.controlContainerView.frame = self.videoContainerView.frame;
self.progressContainerView.frame = self.videoContainerView.frame;
self.bufferingIndicatorContainerView.frame = self.videoContainerView.frame;
self.userInteractionContainerView.frame = CGRectMake(self.userInteractionContainerView.center.x - bounds.size.width * 0.5,
self.userInteractionContainerView.center.y - bounds.size.height * 0.5,
bounds.size.width,
bounds.size.height - kJPVideoPlayerControlBarHeight);
[self layoutContainerSubviewsWithBounds:bounds center:CGPointZero frame:CGRectZero];
[self callLayoutMethodForContainerSubviews];
}
- (void)setCenter:(CGPoint)center {
[super setCenter:center];
self.videoContainerView.frame = CGRectMake(center.y - self.videoContainerView.bounds.size.width * 0.5,
center.x - self.videoContainerView.bounds.size.height * 0.5,
self.videoContainerView.bounds.size.width,
self.videoContainerView.bounds.size.height);
self.placeholderView.frame = self.videoContainerView.frame;
self.controlContainerView.frame = self.videoContainerView.frame;
self.progressContainerView.frame = self.videoContainerView.frame;
self.bufferingIndicatorContainerView.frame = self.videoContainerView.frame;
self.userInteractionContainerView.frame = CGRectMake(center.y - self.userInteractionContainerView.bounds.size.width * 0.5,
center.x - self.userInteractionContainerView.bounds.size.height * 0.5,
self.userInteractionContainerView.bounds.size.width,
self.userInteractionContainerView.bounds.size.height - kJPVideoPlayerControlBarHeight);
[self layoutContainerSubviewsWithBounds:CGRectZero center:center frame:CGRectZero];
[self callLayoutMethodForContainerSubviews];
}
- (CALayer *)videoContainerLayer {
return self.videoContainerView.layer;
}
- (void)tapGestureDidTap {
[UIView animateWithDuration:0.35
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
if(self.controlContainerView.alpha == 0){
self.controlContainerView.alpha = 1;
self.progressContainerView.alpha = 0;
[self startTimer];
}
else {
self.controlContainerView.alpha = 0;
self.progressContainerView.alpha = 1;
[self endTimer];
}
}
completion:^(BOOL finished) {
}];
}
- (void)layoutContainerSubviewsWithBounds:(CGRect)bounds center:(CGPoint)center frame:(CGRect)frame {
for(UIView *view in self.controlContainerView.subviews){
if(!CGRectIsEmpty(frame)){
view.frame = frame;
}
else {
if(CGRectIsEmpty(bounds)){
bounds = view.bounds;
}
if(CGPointEqualToPoint(center, CGPointZero)){
center = view.center;
}
view.frame = CGRectMake(center.y - bounds.size.width * 0.5,
center.x - bounds.size.height * 0.5,
bounds.size.width,
bounds.size.height);
}
}
for(UIView *view in self.progressContainerView.subviews){
if(!CGRectIsEmpty(frame)){
view.frame = frame;
}
else {
if(CGRectIsEmpty(bounds)){
bounds = view.bounds;
}
if(CGPointEqualToPoint(center, CGPointZero)){
center = view.center;
}
view.frame = CGRectMake(center.y - bounds.size.width * 0.5,
center.x - bounds.size.height * 0.5,
bounds.size.width,
bounds.size.height);
}
}
for(UIView *view in self.bufferingIndicatorContainerView.subviews){
if(!CGRectIsEmpty(frame)){
view.frame = frame;
}
else {
if(CGRectIsEmpty(bounds)){
bounds = view.bounds;
}
if(CGPointEqualToPoint(center, CGPointZero)){
center = view.center;
}
view.frame = CGRectMake(center.y - bounds.size.width * 0.5,
center.x - bounds.size.height * 0.5,
bounds.size.width,
bounds.size.height);
}
}
}
- (void)callLayoutMethodForContainerSubviews {
UIViewController *nearestViewController = [self findNearestViewControllerForView:self.superview];
for(UIView<JPVideoPlayerProtocol> *view in self.controlContainerView.subviews){
if([view respondsToSelector:@selector(layoutThatFits:nearestViewControllerInViewTree:interfaceOrientation:)]){
[view layoutThatFits:self.bounds
nearestViewControllerInViewTree:nearestViewController
interfaceOrientation:[self fetchCurrentInterfaceOrientation]];
}
}
for(UIView<JPVideoPlayerProtocol> *view in self.progressContainerView.subviews){
if([view respondsToSelector:@selector(layoutThatFits:nearestViewControllerInViewTree:interfaceOrientation:)]){
[view layoutThatFits:self.bounds
nearestViewControllerInViewTree:nearestViewController
interfaceOrientation:[self fetchCurrentInterfaceOrientation]];
}
}
for(UIView<JPVideoPlayerProtocol> *view in self.bufferingIndicatorContainerView.subviews){
if([view respondsToSelector:@selector(layoutThatFits:nearestViewControllerInViewTree:interfaceOrientation:)]){
[view layoutThatFits:self.bounds
nearestViewControllerInViewTree:nearestViewController
interfaceOrientation:[self fetchCurrentInterfaceOrientation]];
}
}
}
- (JPVideoPlayViewInterfaceOrientation)fetchCurrentInterfaceOrientation {
return self.superview.jp_viewInterfaceOrientation;
}
- (UIViewController *)findNearestViewControllerForView:(UIView *)view {
if(!view){
return nil;
}
BOOL isFind = [[view nextResponder] isKindOfClass:[UIViewController class]] && CGRectEqualToRect(view.bounds, [UIScreen mainScreen].bounds);
if(isFind){
return (UIViewController *)[view nextResponder];
}
return [self findNearestViewControllerForView:view.superview];
}
#pragma mark - Setup
- (void)_setup {
self.placeholderView = ({
UIView *view = [_JPVideoPlayerPlaceholderView new];
view.backgroundColor = [UIColor clearColor];
[self addSubview:view];
view;
});
self.videoContainerView = ({
UIView *view = [_JPVideoPlayerVideoContainerView new];
view.backgroundColor = [UIColor clearColor];
[self addSubview:view];
view.userInteractionEnabled = NO;
view;
});
self.bufferingIndicatorContainerView = ({
UIView *view = [_JPVideoPlayerBufferingIndicatorContainerView new];
view.backgroundColor = [UIColor clearColor];
[self addSubview:view];
view.userInteractionEnabled = NO;
view;
});
self.progressContainerView = ({
UIView *view = [_JPVideoPlayerProgressContainerView new];
view.backgroundColor = [UIColor clearColor];
[self addSubview:view];
view;
});
self.controlContainerView = ({
UIView *view = [_JPVideoPlayerControlContainerView new];
view.backgroundColor = [UIColor clearColor];
[self addSubview:view];
view;
});
self.userInteractionContainerView = ({
UIView *view = [_JPVideoPlayerUserInteractionContainerView new];
view.backgroundColor = [UIColor clearColor];
[self addSubview:view];
view;
});
if (self.needAutoHideControlViewWhenUserTapping) {
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureDidTap)];
[self.userInteractionContainerView addGestureRecognizer:tapGestureRecognizer];
[self startTimer];
}
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(didReceiveUserStartDragNotification)
name:JPVideoPlayerControlProgressViewUserDidStartDragNotification
object:nil];
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(didReceiveUserEndDragNotification)
name:JPVideoPlayerControlProgressViewUserDidEndDragNotification
object:nil];
}
- (void)didReceiveUserStartDragNotification {
if(self.timer){
self.isInterruptTimer = YES;
[self endTimer];
}
}
- (void)didReceiveUserEndDragNotification {
if(self.isInterruptTimer){
[self startTimer];
}
}
- (void)startTimer {
if(!self.timer){
self.timer = [NSTimer timerWithTimeInterval:kJPControlViewAutoHiddenTimeInterval
target:self
selector:@selector(timeDidChange:)
userInfo:nil
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
}
- (void)endTimer {
if(self.timer){
[self.timer invalidate];
self.timer = nil;
}
}
- (void)timeDidChange:(NSTimer *)timer {
[self tapGestureDidTap];
[self endTimer];
}
@end
// TODO: 捕获音量, 自定义音量控制.
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <Foundation/Foundation.h>
#import "JPVideoPlayerCompat.h"
NS_ASSUME_NONNULL_BEGIN
@class JPVideoPlayerDownloader, JPResourceLoadingRequestTask;
@class JPResourceLoadingRequestWebTask;
@protocol JPVideoPlayerDownloaderDelegate<NSObject>
@optional
/**
* This method will be called when received response from web,
* this method will execute on main-thread.
*
* @param downloader The current instance.
* @param response The response content.
*/
- (void)downloader:(JPVideoPlayerDownloader *)downloader
didReceiveResponse:(NSURLResponse *)response;
/**
* This method will be called when received data.
* this method will execute on any-thread.
*
* @param downloader The current instance.
* @param data The received new data.
* @param receivedSize The size of received data.
* @param expectedSize The expexted size of request.
*/
- (void)downloader:(JPVideoPlayerDownloader *)downloader
didReceiveData:(NSData *)data
receivedSize:(NSUInteger)receivedSize
expectedSize:(NSUInteger)expectedSize;
/**s
* This method will be called when request completed or some error happened other situations.
* this method will execute on main-thread.
*
* @param downloader The current instance.
* @param error The error when request, maybe nil if successed.
*/
- (void)downloader:(JPVideoPlayerDownloader *)downloader
didCompleteWithError:(NSError *)error;
@end
@interface JPVideoPlayerDownloader : NSObject
/**
* Set the default URL credential to be set for request operations.
*/
@property (strong, nonatomic, nullable) NSURLCredential *urlCredential;
/**
* Set username
*/
@property (strong, nonatomic, nullable) NSString *username;
/**
* Set password
*/
@property (strong, nonatomic, nullable) NSString *password;
/**
* The timeout value (in seconds) for the download operation. Default: 15.0s.
*/
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
/**
* The current url, may nil if no download operation.
*/
@property (nonatomic, weak, readonly, nullable) JPResourceLoadingRequestWebTask *runningTask;
/**
* The current downloaderOptions, may nil if no download operation.
*/
@property (nonatomic, assign, readonly) JPVideoPlayerDownloaderOptions downloaderOptions;
@property (nonatomic, weak) id<JPVideoPlayerDownloaderDelegate> delegate;
/**
* @brief Customize acceptable response MIMETypes.
*
* @discussion
*
* Original acceptable MIMEType is just `audio` and `video`, but there are some
* other kind of MIMETypes, such as `application/oct-stream` and so on.
*
* @param types
*
* The supported MIMETypes.
*/
+ (void)registerSupportedMIMETypes:(NSArray<NSString *> *)types;
/**
* Creates an instance of a downloader with specified session configuration.
* *Note*: `timeoutIntervalForRequest` is going to be overwritten.
* @return new instance of downloader class
*/
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
/**
* Singleton method, returns the shared instance.
*
* @return global shared instance of downloader class.
*/
+ (nonnull instancetype)sharedDownloader;
/**
* Start download video data for given url.
*
* @param requestTask A abstract instance packageing the loading request.
* @param downloadOptions The options to be used for this download.
*/
- (void)downloadVideoWithRequestTask:(JPResourceLoadingRequestWebTask *)requestTask
downloadOptions:(JPVideoPlayerDownloaderOptions)downloadOptions;
/**
* Cancel current download task.
*/
- (void)cancel;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerDownloader.h"
#import <pthread.h>
#import "JPVideoPlayerManager.h"
#import "JPResourceLoadingRequestTask.h"
#import "JPVideoPlayerCacheFile.h"
#import "JPVideoPlayerSupportUtils.h"
static NSArray<NSString *> *JPVideoPlayerDownloaderSupportedMIMETypes;
@interface JPVideoPlayerDownloader()<NSURLSessionDelegate, NSURLSessionDataDelegate>
// The session in which data tasks will run
@property (strong, nonatomic) NSURLSession *session;
// The size of received data now.
@property(nonatomic, assign)NSUInteger receivedSize;
/*
* The expected size.
*/
@property(nonatomic, assign) NSUInteger expectedSize;
@property (nonatomic) pthread_mutex_t lock;
/*
* The running operation.
*/
@property(nonatomic, weak, nullable) JPResourceLoadingRequestWebTask *runningTask;
@end
@implementation JPVideoPlayerDownloader
+ (void)load {
JPVideoPlayerDownloaderSupportedMIMETypes = @[@"video", @"audio"];
}
+ (nonnull instancetype)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (nonnull instancetype)init {
return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
}
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_lock, &mutexattr);
_expectedSize = 0;
_receivedSize = 0;
_runningTask = nil;
if (!sessionConfiguration) {
sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
sessionConfiguration.timeoutIntervalForRequest = 15.f;
/**
* Create the session for this task.
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate.
* method calls and downloadCompletion handler calls.
*/
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
#pragma mark - Public
+ (void)registerSupportedMIMETypes:(NSArray<NSString *> *)types {
JPAssertMainThread;
if (!types.count) return;
NSMutableArray *mutableSupportedMIMETypes = [JPVideoPlayerDownloaderSupportedMIMETypes mutableCopy];
[types enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![mutableSupportedMIMETypes containsObject:obj]) {
[mutableSupportedMIMETypes addObject:obj];
}
}];
JPVideoPlayerDownloaderSupportedMIMETypes = [mutableSupportedMIMETypes copy];
}
- (void)downloadVideoWithRequestTask:(JPResourceLoadingRequestWebTask *)requestTask
downloadOptions:(JPVideoPlayerDownloaderOptions)downloadOptions {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil.
// If it is nil immediately call the completed block with no video or data.
if (requestTask.customURL == nil) {
JPErrorLog(@"The URL will be used as the key to the callbacks dictionary so it cannot be nil.");
[self callCompleteDelegateIfNeedWithError:JPErrorWithDescription(@"Please check the download URL, because it is nil")];
return;
}
[self reset];
_runningTask = requestTask;
_downloaderOptions = downloadOptions;
[self startDownloadOpeartionWithRequestTask:requestTask
options:downloadOptions];
}
- (void)cancel {
int lock = pthread_mutex_trylock(&_lock);
if (self.runningTask) {
[self.runningTask cancel];
[self reset];
}
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
#pragma mark - Download Operation
- (void)startDownloadOpeartionWithRequestTask:(JPResourceLoadingRequestWebTask *)requestTask
options:(JPVideoPlayerDownloaderOptions)options {
if (!self.downloadTimeout) {
self.downloadTimeout = 15.f;
}
// In order to prevent from potential duplicate caching (NSURLCache + JPVideoPlayerCache),
// we disable the cache for video requests if told otherwise.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:requestTask.customURL
cachePolicy:(NSURLRequestReloadIgnoringLocalCacheData)
timeoutInterval:self.downloadTimeout];
request.HTTPShouldHandleCookies = (options & JPVideoPlayerDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (!self.urlCredential && self.username && self.password) {
self.urlCredential = [NSURLCredential credentialWithUser:self.username
password:self.password
persistence:NSURLCredentialPersistenceForSession];
}
NSString *rangeValue = JPRangeToHTTPRangeHeader(requestTask.requestRange);
if (rangeValue) {
[request setValue:rangeValue forHTTPHeaderField:@"Range"];
}
self.runningTask = requestTask;
requestTask.request = request;
requestTask.unownedSession = self.session;
JPDebugLog(@"Downloader 处理完一个请求");
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
if (response) {
JPDebugLog(@"URLSession will perform HTTP redirection");
self.runningTask.loadingRequest.redirect = request;
}
if(completionHandler){
completionHandler(request);
}
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
JPDebugLog(@"URLSession 收到响应");
//'304 Not Modified' is an exceptional one.
if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
NSInteger expected = MAX((NSInteger)response.expectedContentLength, 0);
self.expectedSize = expected;
// there are a lot of MIMETypes represent audio and video
NSMutableArray *supportedMIMETypes = [JPVideoPlayerDownloaderSupportedMIMETypes mutableCopy];
[supportedMIMETypes addObjectsFromArray:@[@"video", @"audio"]];
BOOL isSupportedMIMEType = NO;
for (NSString *type in supportedMIMETypes) {
if ([response.MIMEType containsString:type]) {
isSupportedMIMEType = YES;
break;
}
}
if(!isSupportedMIMEType){
JPErrorLog(@"Not support MIMEType: %@", response.MIMEType);
JPDispatchSyncOnMainQueue(^{
[self cancel];
[self callCompleteDelegateIfNeedWithError:JPErrorWithDescription([NSString stringWithFormat:@"Not support MIMEType: %@", response.MIMEType])];
[[NSNotificationCenter defaultCenter] postNotificationName:JPVideoPlayerDownloadStopNotification object:self];
});
if (completionHandler) {
completionHandler(NSURLSessionResponseCancel);
}
return;
}
// May the free size of the device less than the expected size of the video data.
if (![[JPVideoPlayerCache sharedCache] haveFreeSizeToCacheFileWithSize:expected]) {
JPDispatchSyncOnMainQueue(^{
[self cancel];
[self callCompleteDelegateIfNeedWithError:JPErrorWithDescription(@"No enough size of device to cache the video data")];
[[NSNotificationCenter defaultCenter] postNotificationName:JPVideoPlayerDownloadStopNotification object:self];
});
if (completionHandler) {
completionHandler(NSURLSessionResponseCancel);
}
}
else{
JPDispatchSyncOnMainQueue(^{
if(!self.runningTask){
if (completionHandler) {
completionHandler(NSURLSessionResponseCancel);
}
return;
}
[self.runningTask requestDidReceiveResponse:response];
if (self.delegate && [self.delegate respondsToSelector:@selector(downloader:didReceiveResponse:)]) {
[self.delegate downloader:self didReceiveResponse:response];
}
[[NSNotificationCenter defaultCenter] postNotificationName:JPVideoPlayerDownloadReceiveResponseNotification object:self];
});
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
}
else {
JPDispatchSyncOnMainQueue(^{
[self cancel];
NSString *errorMsg = [NSString stringWithFormat:@"The statusCode of response is: %ld", (long)((NSHTTPURLResponse *)response).statusCode];
[self callCompleteDelegateIfNeedWithError:JPErrorWithDescription(errorMsg)];
[[NSNotificationCenter defaultCenter] postNotificationName:JPVideoPlayerDownloadStopNotification object:self];
});
if (completionHandler) {
completionHandler(NSURLSessionResponseCancel);
}
}
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
// may runningTask is dealloc in main-thread and this method called in sub-thread.
if(!self.runningTask){
[self reset];
return;
}
self.receivedSize += data.length;
[self.runningTask requestDidReceiveData:data
storedCompletion:^{
JPDispatchSyncOnMainQueue(^{
if (self.delegate && [self.delegate respondsToSelector:@selector(downloader:didReceiveData:receivedSize:expectedSize:)]) {
[self.delegate downloader:self
didReceiveData:data
receivedSize:self.receivedSize
expectedSize:self.expectedSize];
}
});
}];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
JPDispatchSyncOnMainQueue(^{
JPDebugLog(@"URLSession 完成了一个请求, id 是 %ld, error 是: %@", task.taskIdentifier, error);
BOOL completeValid = self.runningTask && task.taskIdentifier == self.runningTask.dataTask.taskIdentifier;
if(!completeValid){
JPDebugLog(@"URLSession 完成了一个不是正在请求的请求, id 是: %d", task.taskIdentifier);
return;
}
[self.runningTask requestDidCompleteWithError:error];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:JPVideoPlayerDownloadFinishNotification object:self];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(downloader:didCompleteWithError:)]) {
[self.delegate downloader:self didCompleteWithError:error];
}
});
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
downloadCompletionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))downloadCompletionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (!(self.runningTask.options & JPVideoPlayerDownloaderAllowInvalidSSLCertificates)) {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
else {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
disposition = NSURLSessionAuthChallengeUseCredential;
}
}
else {
if (challenge.previousFailureCount == 0) {
if (self.urlCredential) {
credential = self.urlCredential;
disposition = NSURLSessionAuthChallengeUseCredential;
}
else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
if (downloadCompletionHandler) {
downloadCompletionHandler(disposition, credential);
}
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
downloadCompletionHandler:(void (^)(NSCachedURLResponse *cachedResponse))downloadCompletionHandler {
// If this method is called, it means the response wasn't read from cache
NSCachedURLResponse *cachedResponse = proposedResponse;
if (self.runningTask.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
// Prevents caching of responses
cachedResponse = nil;
}
if (downloadCompletionHandler) {
downloadCompletionHandler(cachedResponse);
}
}
#pragma mark - Private
- (void)callCompleteDelegateIfNeedWithError:(NSError *)error {
if (self.delegate && [self.delegate respondsToSelector:@selector(downloader:didCompleteWithError:)]) {
[self.delegate downloader:self didCompleteWithError:error];
}
}
- (void)reset {
JPDebugLog(@"调用了 reset");
self.runningTask = nil;
self.expectedSize = 0;
self.receivedSize = 0;
}
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "UIView+WebVideoCache.h"
#import "JPVideoPlayerControlViews.h"
#import "UITableView+WebVideoCache.h"
#import "UITableViewCell+WebVideoCache.h"
#import "UICollectionViewCell+WebVideoCache.h"
#import "UICollectionView+WebVideoCache.h"
#import "JPVideoPlayerCache.h"
#import "JPVideoPlayerManager.h"
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <UIKit/UIKit.h>
#import "JPVideoPlayerDownloader.h"
#import "JPVideoPlayerCache.h"
#import "JPVideoPlayer.h"
#import "JPVideoPlayerProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@class JPVideoPlayerManager;
@protocol JPVideoPlayerManagerDelegate <NSObject>
@optional
/**
* Controls which video should be downloaded when the video is not found in the cache.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be downloaded.
*
* @return Return NO to prevent the downloading of the video on cache misses. If not implemented, YES is implied.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldDownloadVideoForURL:(NSURL *)videoURL;
/**
* Controls which video should automatic replay when the video is play completed.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
*
* @return Return NO to prevent replay for the video. If not implemented, YES is implied.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldAutoReplayForURL:(NSURL *)videoURL;
/**
* Notify the playing status.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param playerStatus The current playing status.
*/
- (void)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
playerStatusDidChanged:(JPVideoPlayerStatus)playerStatus;
/**
* Notify the video file length.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoLength The file length of video data.
*/
- (void)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
didFetchVideoFileLength:(NSUInteger)videoLength;
/**
* Notify the download progress value. this method will be called on main thread.
* If the video is local or cached file, this method will be called once and the receive size equal to expected size,
* If video is existed on web, this method will be called when the download progress value changed,
* If some error happened, the error is no nil.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param cacheType The video data cache type.
* @param fragmentRanges The fragment of video data that cached in disk.
* @param expectedSize The expected data size.
* @param error The error when download video data.
*/
- (void)videoPlayerManagerDownloadProgressDidChange:(JPVideoPlayerManager *)videoPlayerManager
cacheType:(JPVideoPlayerCacheType)cacheType
fragmentRanges:(NSArray<NSValue *> * _Nullable)fragmentRanges
expectedSize:(NSUInteger)expectedSize
error:(NSError *_Nullable)error;
/**
* Notify the playing progress value. this method will be called on main thread.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param elapsedSeconds The current played seconds.
* @param totalSeconds The total seconds of this video for given url.
* @param error The error when playing video.
*/
- (void)videoPlayerManagerPlayProgressDidChange:(JPVideoPlayerManager *)videoPlayerManager
elapsedSeconds:(double)elapsedSeconds
totalSeconds:(double)totalSeconds
error:(NSError *_Nullable)error;
/**
* Called when application will resign active.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldPausePlaybackWhenApplicationWillResignActiveForURL:(NSURL *)videoURL;
/**
* Called when application did enter background.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:(NSURL *)videoURL;
/**
* Called only when application become active from `Control Center`,
* `Notification Center`, `pop UIAlert`, `double click Home-Button`.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:(NSURL *)videoURL;
/**
* Called only when application become active from `Share to other application`,
* `Enter background`, `Lock screen`.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:(NSURL *)videoURL;
/**
* Called when call resume play but can not resume play.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldTranslateIntoPlayVideoFromResumePlayForURL:(NSURL *)videoURL;
/**
* Called when receive audio session interruption notification.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldPausePlaybackWhenReceiveAudioSessionInterruptionNotificationForURL:(NSURL *)videoURL;
/**
* Provide custom audio session category to play video.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
*
* @return The prefer audio session category.
*/
- (AVAudioSessionCategory)videoPlayerManagerPreferAudioSessionCategory:(JPVideoPlayerManager *)videoPlayerManager;
/**
* Called when play a already played video.
*
* @param videoPlayerManager The current `JPVideoPlayerManager`.
* @param videoURL The url of the video to be play.
* @param elapsedSeconds The elapsed seconds last playback recorded.
*/
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldResumePlaybackFromPlaybackRecordForURL:(NSURL *)videoURL
elapsedSeconds:(NSTimeInterval)elapsedSeconds;
@end
@interface JPVideoPlayerManagerModel : NSObject
@property (nonatomic, strong, readonly) NSURL *videoURL;
@property (nonatomic, assign) JPVideoPlayerCacheType cacheType;
@property (nonatomic, assign) NSUInteger fileLength;
/**
* The fragment of video data that cached in disk.
*/
@property (nonatomic, strong, readonly, nullable) NSArray<NSValue *> *fragmentRanges;
@end
@interface JPVideoPlayerManager : NSObject<JPVideoPlayerPlaybackProtocol>
@property (weak, nonatomic, nullable) id <JPVideoPlayerManagerDelegate> delegate;
@property (strong, nonatomic, readonly, nullable) JPVideoPlayerCache *videoCache;
@property (strong, nonatomic, readonly, nullable) JPVideoPlayerDownloader *videoDownloader;
@property (nonatomic, strong, readonly) JPVideoPlayerManagerModel *managerModel;
@property (nonatomic, strong, readonly) JPVideoPlayer *videoPlayer;
#pragma mark - Singleton and Initialization
/**
* Returns global `JPVideoPlayerManager` instance.
*
* @return `JPVideoPlayerManager` shared instance
*/
+ (nonnull instancetype)sharedManager;
/**
* Set the log level. `JPLogLevelDebug` by default.
*
* @see `JPLogLevel`.
*
* @param logLevel The log level to control log type.
*/
+ (void)preferLogLevel:(JPLogLevel)logLevel;
/**
* Allows to specify instance of cache and video downloader used with video manager.
* @return new instance of `JPVideoPlayerManager` with specified cache and downloader.
*/
- (nonnull instancetype)initWithCache:(nonnull JPVideoPlayerCache *)cache
downloader:(nonnull JPVideoPlayerDownloader *)downloader NS_DESIGNATED_INITIALIZER;
# pragma mark - Play Video
/**
* Play the video for the given URL.
* @param url The URL of video.
* @param showLayer The layer of video layer display on.
* @param options A flag to specify options to use for this request.
* @param configuration The block will be call when video player config finished. because initialize player is not synchronize,
* so other category method is disabled before config finished.
*/
- (void)playVideoWithURL:(NSURL *)url
showOnLayer:(CALayer *)showLayer
options:(JPVideoPlayerOptions)options
configuration:(JPVideoPlayerConfiguration)configuration;
/**
* Resume video play for the given URL.
* @param url The URL of video.
* @param showLayer The layer of video layer display on.
* @param options A flag to specify options to use for this request.
* @param configuration The block will be call when video player config finished. because initialize player is not synchronize,
* so other category method is disabled before config finished.
*/
- (void)resumePlayWithURL:(NSURL *)url
showOnLayer:(CALayer *)showLayer
options:(JPVideoPlayerOptions)options
configuration:(JPVideoPlayerConfiguration)configuration;
/**
* Return the cache key for a given URL.
*/
- (NSString *_Nullable)cacheKeyForURL:(NSURL *)url;
#pragma mark - Version
/**
* Return the version of SDK;
*/
- (NSString *)SDKVersion;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerManager.h"
#import "JPVideoPlayerCompat.h"
#import "JPVideoPlayerCachePath.h"
#import "JPVideoPlayer.h"
#import "UIView+WebVideoCache.h"
#import <pthread.h>
#import "JPVideoPlayerSupportUtils.h"
#import "JPVideoPlayerCacheFile.h"
#import "JPVideoPlayerResourceLoader.h"
@interface JPVideoPlayerManagerModel()
@property (nonatomic, strong, nullable) NSArray<NSValue *> *fragmentRanges;
@property (nonatomic, strong) NSURL *videoURL;
@end
@implementation JPVideoPlayerManagerModel
@end
@interface JPVideoPlayerManager()<JPVideoPlayerInternalDelegate,
JPVideoPlayerDownloaderDelegate,
JPApplicationStateMonitorDelegate>
@property (strong, nonatomic, readwrite, nonnull) JPVideoPlayerCache *videoCache;
@property (strong, nonatomic) JPVideoPlayerDownloader *videoDownloader;
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
@property(nonatomic, assign) BOOL isReturnWhenApplicationDidEnterBackground;
@property(nonatomic, assign) BOOL isReturnWhenApplicationWillResignActive;
@property (nonatomic, strong) JPApplicationStateMonitor *applicationStateMonitor;
@property (nonatomic, strong) JPVideoPlayerManagerModel *managerModel;
@property (nonatomic, strong) JPVideoPlayer *videoPlayer;
@property(nonatomic, strong) dispatch_queue_t syncQueue;
@end
static NSString * const JPVideoPlayerSDKVersionKey = @"com.jpvideoplayer.sdk.version.www";
@implementation JPVideoPlayerManager
@synthesize volume;
@synthesize muted;
@synthesize rate;
+ (nonnull instancetype)sharedManager {
static dispatch_once_t once;
static JPVideoPlayerManager *jpVideoPlayerManagerInstance;
dispatch_once(&once, ^{
jpVideoPlayerManagerInstance = [self new];
[[NSUserDefaults standardUserDefaults] setObject:@"3.1.1" forKey:JPVideoPlayerSDKVersionKey];
[[NSUserDefaults standardUserDefaults] synchronize];
[JPMigration migrateToSDKVersion:@"3.1.1" block:^{
[jpVideoPlayerManagerInstance.videoCache clearDiskOnCompletion:nil];
}];
});
return jpVideoPlayerManagerInstance;
}
- (nonnull instancetype)init {
JPVideoPlayerCache *cache = [JPVideoPlayerCache sharedCache];
JPVideoPlayerDownloader *downloader = [JPVideoPlayerDownloader sharedDownloader];
downloader.delegate = self;
return [self initWithCache:cache downloader:downloader];
}
- (nonnull instancetype)initWithCache:(nonnull JPVideoPlayerCache *)cache
downloader:(nonnull JPVideoPlayerDownloader *)downloader {
if ((self = [super init])) {
_videoCache = cache;
_videoDownloader = downloader;
_failedURLs = [NSMutableSet new];
_videoPlayer = [JPVideoPlayer new];
_videoPlayer.delegate = self;
_isReturnWhenApplicationDidEnterBackground = NO;
_isReturnWhenApplicationWillResignActive = NO;
_applicationStateMonitor = [JPApplicationStateMonitor new];
_applicationStateMonitor.delegate = self;
_syncQueue = dispatch_queue_create("com.jpvideoplayer.manager.sync.queue.www", DISPATCH_QUEUE_SERIAL);
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(audioSessionInterruptionNotification:)
name:AVAudioSessionInterruptionNotification
object:nil];
}
return self;
}
#pragma mark - Public
+ (void)preferLogLevel:(JPLogLevel)logLevel {
_logLevel = logLevel;
}
- (void)playVideoWithURL:(NSURL *)url
showOnLayer:(CALayer *)showLayer
options:(JPVideoPlayerOptions)options
configuration:(JPVideoPlayerConfiguration)configuration {
JPAssertMainThread;
if (self.managerModel && [self.managerModel.videoURL.absoluteString isEqualToString:url.absoluteString]) {
}
if(!url || !showLayer){
JPErrorLog(@"url and showLayer can not be nil.");
return;
}
[self reset];
[self activeAudioSessionIfNeed];
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
self.managerModel = [JPVideoPlayerManagerModel new];
self.managerModel.videoURL = url;
BOOL isFailedUrl = NO;
if (url) {
isFailedUrl = [self.failedURLs containsObject:url];
}
if (url.absoluteString.length == 0 || (!(options & JPVideoPlayerRetryFailed) && isFailedUrl)) {
NSError *error = [NSError errorWithDomain:JPVideoPlayerErrorDomain
code:NSURLErrorFileDoesNotExist
userInfo:@{NSLocalizedDescriptionKey : @"The file of given URL not exists"}];
[self callDownloadDelegateMethodWithFragmentRanges:nil
expectedSize:1
cacheType:JPVideoPlayerCacheTypeNone
error:error];
return;
}
BOOL isFileURL = [url isFileURL];
if (isFileURL) {
// play file URL.
JPDispatchAsyncOnQueue(self.syncQueue, ^{
[self playLocalVideoWithShowLayer:showLayer
url:url
options:options
configuration:configuration];
});
return;
}
NSString *key = [self cacheKeyForURL:url];
[self.videoCache queryCacheOperationForKey:key completion:^(NSString *_Nullable videoPath, JPVideoPlayerCacheType cacheType) {
if (!showLayer) {
[self reset];
return;
}
if (!videoPath && (![self.delegate respondsToSelector:@selector(videoPlayerManager:shouldDownloadVideoForURL:)] || [self.delegate videoPlayerManager:self shouldDownloadVideoForURL:url])) {
// play web video.
JPDispatchAsyncOnQueue(self.syncQueue, ^{
JPDebugLog(@"Start play a web video: %@", url);
self.managerModel.cacheType = JPVideoPlayerCacheTypeNone;
[self.videoPlayer playVideoWithURL:url
options:options
showLayer:showLayer
configuration:configuration];
});
}
else if (videoPath) {
self.managerModel.cacheType = JPVideoPlayerCacheTypeExisted;
JPDebugLog(@"Start play a existed video: %@", url);
JPDispatchAsyncOnQueue(self.syncQueue, ^{
[self playFragmentVideoWithURL:url
options:options
showLayer:showLayer
configuration:configuration];
});
}
else {
// video not in cache and download disallowed by delegate.
NSError *error = [NSError errorWithDomain:JPVideoPlayerErrorDomain
code:NSURLErrorFileDoesNotExist
userInfo:@{NSLocalizedDescriptionKey: @"Video not in cache and download disallowed by delegate"}];
[self callDownloadDelegateMethodWithFragmentRanges:nil
expectedSize:1
cacheType:JPVideoPlayerCacheTypeNone
error:error];
[self reset];
}
}];
}
- (void)resumePlayWithURL:(NSURL *)url
showOnLayer:(CALayer *)showLayer
options:(JPVideoPlayerOptions)options
configuration:(JPVideoPlayerConfiguration)configuration {
JPAssertMainThread;
if(!url){
JPErrorLog(@"url can not be nil");
return;
}
[self activeAudioSessionIfNeed];
BOOL canResumePlaying = self.managerModel &&
[self.managerModel.videoURL.absoluteString isEqualToString:url.absoluteString] &&
self.videoPlayer;
if(!canResumePlaying){
JPDebugLog(@"Called resume play, but can not resume play, translate to normal play if need.");
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldTranslateIntoPlayVideoFromResumePlayForURL:)]) {
BOOL preferSwitch = [self.delegate videoPlayerManager:self
shouldTranslateIntoPlayVideoFromResumePlayForURL:url];
if(preferSwitch){
[self playVideoWithURL:url
showOnLayer:showLayer
options:options
configuration:configuration];
}
}
return;
}
[self callVideoLengthDelegateMethodWithVideoLength:self.managerModel.fileLength];
[self callDownloadDelegateMethodWithFragmentRanges:self.managerModel.fragmentRanges
expectedSize:self.managerModel.fileLength
cacheType:self.managerModel.cacheType
error:nil];
JPDebugLog(@"Resume play now.");
[self.videoPlayer resumePlayWithShowLayer:showLayer
options:options
configuration:configuration];
}
- (NSString *_Nullable)cacheKeyForURL:(NSURL *)url {
if (!url) {
return nil;
}
return [url absoluteString];
}
- (NSString *)SDKVersion {
NSString *res = [[NSUserDefaults standardUserDefaults] valueForKey:JPVideoPlayerSDKVersionKey];
return (res ? res : @"");
}
#pragma mark - JPVideoPlayerPlaybackProtocol
- (void)setRate:(float)rate {
[self.videoPlayer setRate:rate];
}
- (float)rate {
return self.videoPlayer.rate;
}
- (void)setMuted:(BOOL)muted {
[self.videoPlayer setMuted:muted];
}
- (BOOL)muted {
return self.videoPlayer.muted;
}
- (void)setVolume:(float)volume {
[self.videoPlayer setVolume:volume];
}
- (float)volume {
return self.videoPlayer.volume;
}
- (BOOL)seekToTime:(CMTime)time {
return [self.videoPlayer seekToTime:time];
}
- (NSTimeInterval)elapsedSeconds {
return [self.videoPlayer elapsedSeconds];
}
- (NSTimeInterval)totalSeconds {
return [self.videoPlayer totalSeconds];
}
- (void)pause {
[self.videoPlayer pause];
}
- (void)resume {
[self.videoPlayer resume];
}
- (CMTime)currentTime {
return self.videoPlayer.currentTime;
}
- (void)stopPlay {
JPDispatchSyncOnMainQueue(^{
[self.videoDownloader cancel];
[self.videoPlayer stopPlay];
[self reset];
});
}
#pragma mark - JPVideoPlayerInternalDelegate
- (void)videoPlayer:(nonnull JPVideoPlayer *)videoPlayer
didReceiveLoadingRequestTask:(JPResourceLoadingRequestWebTask *)requestTask {
JPVideoPlayerDownloaderOptions downloaderOptions = [self fetchDownloadOptionsWithOptions:videoPlayer.playerModel.playerOptions];
[self.videoDownloader downloadVideoWithRequestTask:requestTask
downloadOptions:downloaderOptions];
}
- (BOOL)videoPlayer:(JPVideoPlayer *)videoPlayer
shouldAutoReplayVideoForURL:(NSURL *)videoURL {
[self savePlaybackElapsedSeconds:0 forVideoURL:videoURL];
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldAutoReplayForURL:)]) {
return [self.delegate videoPlayerManager:self shouldAutoReplayForURL:videoURL];
}
return YES;
}
- (void)videoPlayer:(nonnull JPVideoPlayer *)videoPlayer
playerStatusDidChange:(JPVideoPlayerStatus)playerStatus {
if(playerStatus == JPVideoPlayerStatusReadyToPlay){
if([self fetchPlaybackRecordForVideoURL:self.managerModel.videoURL] > 0){
BOOL shouldSeek = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldResumePlaybackFromPlaybackRecordForURL:elapsedSeconds:)]) {
shouldSeek = [self.delegate videoPlayerManager:self
shouldResumePlaybackFromPlaybackRecordForURL:self.managerModel.videoURL
elapsedSeconds:[self fetchPlaybackRecordForVideoURL:self.managerModel.videoURL]];
}
if(shouldSeek){
[self.videoPlayer seekToTimeWhenRecordPlayback:CMTimeMakeWithSeconds([self fetchPlaybackRecordForVideoURL:self.managerModel.videoURL], 1000)];
}
}
}
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:playerStatusDidChanged:)]) {
[self.delegate videoPlayerManager:self playerStatusDidChanged:playerStatus];
}
}
- (void)videoPlayerPlayProgressDidChange:(nonnull JPVideoPlayer *)videoPlayer
elapsedSeconds:(double)elapsedSeconds
totalSeconds:(double)totalSeconds {
if(elapsedSeconds > 0){
[self savePlaybackElapsedSeconds:self.elapsedSeconds forVideoURL:self.managerModel.videoURL];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManagerPlayProgressDidChange:elapsedSeconds:totalSeconds:error:)]) {
[self.delegate videoPlayerManagerPlayProgressDidChange:self
elapsedSeconds:elapsedSeconds
totalSeconds:totalSeconds
error:nil];
}
}
- (void)videoPlayer:(nonnull JPVideoPlayer *)videoPlayer
playFailedWithError:(NSError *)error {
[self stopPlay];
[self callPlayDelegateMethodWithElapsedSeconds:0
totalSeconds:0
error:error];
}
#pragma mark - JPVideoPlayerDownloaderDelegate
- (void)downloader:(JPVideoPlayerDownloader *)downloader
didReceiveResponse:(NSURLResponse *)response {
NSUInteger fileLength = self.videoPlayer.playerModel.resourceLoader.cacheFile.fileLength;
self.managerModel.fileLength = fileLength;
[self callVideoLengthDelegateMethodWithVideoLength:fileLength];
}
- (void)downloader:(JPVideoPlayerDownloader *)downloader
didReceiveData:(NSData *)data
receivedSize:(NSUInteger)receivedSize
expectedSize:(NSUInteger)expectedSize {
NSUInteger fileLength = self.videoPlayer.playerModel.resourceLoader.cacheFile.fileLength;
NSArray<NSValue *> *fragmentRanges = self.videoPlayer.playerModel.resourceLoader.cacheFile.fragmentRanges;
self.managerModel.cacheType = JPVideoPlayerCacheTypeExisted;
self.managerModel.fragmentRanges = fragmentRanges;
[self callDownloadDelegateMethodWithFragmentRanges:fragmentRanges
expectedSize:fileLength
cacheType:self.managerModel.cacheType
error:nil];
}
- (void)downloader:(JPVideoPlayerDownloader *)downloader
didCompleteWithError:(NSError *)error {
if (error){
[self callDownloadDelegateMethodWithFragmentRanges:nil
expectedSize:1
cacheType:JPVideoPlayerCacheTypeNone
error:error];
if (error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
if(self.managerModel.videoURL){
[self.failedURLs addObject:self.managerModel.videoURL];
}
}
[self stopPlay];
}
else {
if ((self.videoPlayer.playerModel.playerOptions & JPVideoPlayerRetryFailed)) {
if ([self.failedURLs containsObject:self.managerModel.videoURL]) {
[self.failedURLs removeObject:self.managerModel.videoURL];
}
}
}
}
#pragma mark - JPApplicationStateMonitorDelegate
- (void)applicationStateMonitor:(JPApplicationStateMonitor *)monitor
applicationStateDidChange:(JPApplicationState)applicationState {
BOOL needReturn = !self.managerModel.videoURL ||
self.videoPlayer.playerStatus == JPVideoPlayerStatusStop ||
self.videoPlayer.playerStatus == JPVideoPlayerStatusPause ||
self.videoPlayer.playerStatus == JPVideoPlayerStatusFailed;
if(applicationState == JPApplicationStateWillResignActive){
BOOL needPause = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldPausePlaybackWhenApplicationWillResignActiveForURL:)]) {
needPause = [self.delegate videoPlayerManager:self
shouldPausePlaybackWhenApplicationWillResignActiveForURL:self.managerModel.videoURL];
}
if(!needPause){
self.isReturnWhenApplicationWillResignActive = YES;
return;
}
self.isReturnWhenApplicationWillResignActive = needReturn;
if(needReturn){
return;
}
[self.videoPlayer pause];
}
else if(applicationState == JPApplicationStateDidEnterBackground){
if(!self.isReturnWhenApplicationWillResignActive){
self.isReturnWhenApplicationDidEnterBackground = self.isReturnWhenApplicationWillResignActive;
return;
}
BOOL needPause = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:)]) {
needPause = [self.delegate videoPlayerManager:self
shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:self.managerModel.videoURL];
}
if(!needPause){
return;
}
self.isReturnWhenApplicationDidEnterBackground = needReturn;
if(needReturn){
return;
}
[self.videoPlayer pause];
}
}
- (void)applicationDidBecomeActiveFromBackground:(JPApplicationStateMonitor *)monitor {
if(self.isReturnWhenApplicationDidEnterBackground){
return;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:)]) {
BOOL needResume = [self.delegate videoPlayerManager:self
shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:self.managerModel.videoURL];
if(needResume){
[self.videoPlayer resume];
[self activeAudioSessionIfNeed];
}
}
}
- (void)applicationDidBecomeActiveFromResignActive:(JPApplicationStateMonitor *)monitor {
if(self.isReturnWhenApplicationWillResignActive){
return;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:)]) {
BOOL needResume = [self.delegate videoPlayerManager:self
shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:self.managerModel.videoURL];
if(needResume){
[self.videoPlayer resume];
[self activeAudioSessionIfNeed];
}
}
}
#pragma mark - Private
- (long long)fetchFileSizeAtPath:(NSString *)filePath{
NSFileManager* manager = [NSFileManager defaultManager];
if ([manager fileExistsAtPath:filePath]){
return [[manager attributesOfItemAtPath:filePath error:nil] fileSize];
}
return 0;
}
- (void)reset {
self.managerModel = nil;
self.isReturnWhenApplicationDidEnterBackground = NO;
self.isReturnWhenApplicationWillResignActive = NO;
}
- (JPVideoPlayerDownloaderOptions)fetchDownloadOptionsWithOptions:(JPVideoPlayerOptions)options {
// download if no cache, and download allowed by delegate.
JPVideoPlayerDownloaderOptions downloadOptions = 0;
if (options & JPVideoPlayerContinueInBackground)
downloadOptions |= JPVideoPlayerDownloaderContinueInBackground;
if (options & JPVideoPlayerHandleCookies)
downloadOptions |= JPVideoPlayerDownloaderHandleCookies;
if (options & JPVideoPlayerAllowInvalidSSLCertificates)
downloadOptions |= JPVideoPlayerDownloaderAllowInvalidSSLCertificates;
return downloadOptions;
}
- (void)callVideoLengthDelegateMethodWithVideoLength:(NSUInteger)videoLength {
JPDispatchAsyncOnMainQueue(^{
if([self.delegate respondsToSelector:@selector(videoPlayerManager:didFetchVideoFileLength:)]){
[self.delegate videoPlayerManager:self
didFetchVideoFileLength:videoLength];
}
});
}
- (void)callDownloadDelegateMethodWithFragmentRanges:(NSArray<NSValue *> *)fragmentRanges
expectedSize:(NSUInteger)expectedSize
cacheType:(JPVideoPlayerCacheType)cacheType
error:(nullable NSError *)error {
JPDispatchAsyncOnMainQueue(^{
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManagerDownloadProgressDidChange:cacheType:fragmentRanges:expectedSize:error:)]) {
[self.delegate videoPlayerManagerDownloadProgressDidChange:self
cacheType:cacheType
fragmentRanges:fragmentRanges
expectedSize:expectedSize
error:error];
}
});
}
- (void)callPlayDelegateMethodWithElapsedSeconds:(double)elapsedSeconds
totalSeconds:(double)totalSeconds
error:(nullable NSError *)error {
JPDispatchAsyncOnMainQueue(^{
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManagerPlayProgressDidChange:elapsedSeconds:totalSeconds:error:)]) {
[self.delegate videoPlayerManagerPlayProgressDidChange:self
elapsedSeconds:elapsedSeconds
totalSeconds:totalSeconds
error:error];
}
});
}
#pragma mark - Play Video
- (void)playFragmentVideoWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
showLayer:(CALayer *)showLayer
configuration:(JPVideoPlayerConfiguration)configuration {
JPVideoPlayerModel *model = [self.videoPlayer playVideoWithURL:url
options:options
showLayer:showLayer
configuration:configuration];
self.managerModel.fileLength = model.resourceLoader.cacheFile.fileLength;
self.managerModel.fragmentRanges = model.resourceLoader.cacheFile.fragmentRanges;
[self callVideoLengthDelegateMethodWithVideoLength:model.resourceLoader.cacheFile.fileLength];
[self callDownloadDelegateMethodWithFragmentRanges:model.resourceLoader.cacheFile.fragmentRanges
expectedSize:model.resourceLoader.cacheFile.fileLength
cacheType:self.managerModel.cacheType
error:nil];
}
- (void)playLocalVideoWithShowLayer:(CALayer *)showLayer
url:(NSURL *)url
options:(JPVideoPlayerOptions)options
configuration:(JPVideoPlayerConfiguration)configuration {
// local file.
JPDebugLog(@"Start play a local video: %@", url);
NSString *path = [url.absoluteString stringByReplacingOccurrencesOfString:@"file://" withString:@""];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
self.managerModel.cacheType = JPVideoPlayerCacheTypeLocation;
self.managerModel.fileLength = (NSUInteger)[self fetchFileSizeAtPath:path];;
self.managerModel.fragmentRanges = @[[NSValue valueWithRange:NSMakeRange(0, self.managerModel.fileLength)]];
[self callVideoLengthDelegateMethodWithVideoLength:self.managerModel.fileLength];
[self callDownloadDelegateMethodWithFragmentRanges:self.managerModel.fragmentRanges
expectedSize:self.managerModel.fileLength
cacheType:self.managerModel.cacheType
error:nil];
[self.videoPlayer playExistedVideoWithURL:url
fullVideoCachePath:path
options:options
showOnLayer:showLayer
configuration:configuration];
}
else{
NSError *error = [NSError errorWithDomain:JPVideoPlayerErrorDomain
code:NSURLErrorFileDoesNotExist
userInfo:@{NSLocalizedDescriptionKey : @"The file of given URL not exists"}];
[self callDownloadDelegateMethodWithFragmentRanges:nil
expectedSize:1
cacheType:JPVideoPlayerCacheTypeNone
error:error];
}
}
#pragma mark - AudioSession
- (void)activeAudioSessionIfNeed {
AVAudioSessionCategory audioSessionCategory = AVAudioSessionCategoryPlayback;
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManagerPreferAudioSessionCategory:)]) {
audioSessionCategory = [self.delegate videoPlayerManagerPreferAudioSessionCategory:self];
}
[AVAudioSession.sharedInstance setActive:YES error:nil];
[AVAudioSession.sharedInstance setCategory:audioSessionCategory error:nil];
}
#pragma mark - AVAudioSessionInterruptionNotification
- (void)audioSessionInterruptionNotification:(NSNotification *)note {
AVPlayer *player = note.object;
// the player is self player, return.
if(player == self.videoPlayer.playerModel.player){
return;
}
// self not playing.
if(!self.videoPlayer.playerModel){
return;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(videoPlayerManager:shouldPausePlaybackWhenReceiveAudioSessionInterruptionNotificationForURL:)]) {
BOOL shouldPause = [self.delegate videoPlayerManager:self
shouldPausePlaybackWhenReceiveAudioSessionInterruptionNotificationForURL:self.managerModel.videoURL];
if(shouldPause){
[self pause];
}
return;
}
[self pause];
}
#pragma mark - Playback Record
- (double)fetchPlaybackRecordForVideoURL:(NSURL *)videoURL {
if(!videoURL){
JPErrorLog(@"videoURL can not be nil.");
return 0;
}
NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfFile:[JPVideoPlayerCachePath videoPlaybackRecordFilePath]];
if(!dictionary){
return 0;
}
NSNumber *number = [dictionary valueForKey:[self cacheKeyForURL:videoURL]];
if(number){
return [number doubleValue];
}
return 0;
}
- (void)savePlaybackElapsedSeconds:(double)elapsedSeconds
forVideoURL:(NSURL *)videoURL {
if(!videoURL){
return;
}
JPDispatchSyncOnMainQueue(^{
NSMutableDictionary *dictionary = [NSDictionary dictionaryWithContentsOfFile:[JPVideoPlayerCachePath videoPlaybackRecordFilePath]].mutableCopy;
if(!dictionary){
dictionary = [@{} mutableCopy];
}
elapsedSeconds == 0 ? [dictionary removeObjectForKey:[self cacheKeyForURL:videoURL]] : [dictionary setObject:@(elapsedSeconds) forKey:[self cacheKeyForURL:videoURL]];
[dictionary writeToFile:[JPVideoPlayerCachePath videoPlaybackRecordFilePath] atomically:YES];
});
}
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <UIKit/UIKit.h>
#import "JPVideoPlayerCompat.h"
#import <AVFoundation/AVFoundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol JPVideoPlayerLayoutProtocol<NSObject>
@required
/**
* This method called when need layout subviews, suggest you layout subviews in this method.
*
* @param constrainedRect The bounds of superview.
* @param nearestViewController The nearest `UIViewController` of view in view tree,
* it be use to fetch `safeAreaInsets` to help layout subviews.
* @param interfaceOrientation The current interface orientation of view.
*/
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation;
@end
@protocol JPVideoPlayerProtocol<JPVideoPlayerLayoutProtocol>
@optional
/**
* This method will be called when the view as a subview be add to a view.
*
* @note User can hold this view to control player, but remember that do not retain this view, suggest to
* weak hold this view.
*
* @param view The view to control player.
*/
- (void)viewWillAddToSuperView:(UIView *)view;
/**
* This method will be call when the view be reuse in `UITableView`,
* you need reset progress value in this method for good user experience.
*/
- (void)viewWillPrepareToReuse;
/**
* This method will be called when the downloader fetched the file length or read from disk.
*
* @warning This method may be call repeatedly when download a video.
*
* @param videoLength The video file length.
* @param videoURL The URL of video.
*/
- (void)didFetchVideoFileLength:(NSUInteger)videoLength
videoURL:(NSURL *)videoURL;
/**
* This method will be called when received new video data from web.
*
* @param cacheRanges The ranges of video data cached in disk.
* @param videoURL The URL of video.
*/
- (void)cacheRangeDidChange:(NSArray<NSValue *> *)cacheRanges
videoURL:(NSURL *)videoURL;
/**
* This method will be called when play progress changed.
*
* @param elapsedSeconds The elapsed player time.
* @param totalSeconds The length of the video.
* @param videoURL The URL of video.
*/
- (void)playProgressDidChangeElapsedSeconds:(NSTimeInterval)elapsedSeconds
totalSeconds:(NSTimeInterval)totalSeconds
videoURL:(NSURL *)videoURL;
/**
* This method will be called when video player status did change.
*
* @param playerStatus The player status.
* @param videoURL The URL of video.
*/
- (void)videoPlayerStatusDidChange:(JPVideoPlayerStatus)playerStatus
videoURL:(NSURL *)videoURL;
/**
* This method will be called when the interfaceOrientation of player was changed.
*
* @param interfaceOrientation The interfaceOrientation of player.
* @param videoURL The URL of video.
*/
- (void)videoPlayerInterfaceOrientationDidChange:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation
videoURL:(NSURL *)videoURL;
@end
@protocol JPVideoPlayerControlProgressProtocol<JPVideoPlayerProtocol>
/**
* Control progress must implement this method, and implement
*
* @code
* [self willChangeValueForKey:@"userDragging"];
* _userDragging = userDragging;
* [self didChangeValueForKey:@"userDragging"];
*@endcode
*/
@property(nonatomic) BOOL userDragging;
/**
* Control progress must implement this method, and implement
*
* @code
* [self willChangeValueForKey:@"userDragTimeInterval"];
* _userDragTimeInterval = userDragTimeInterval;
* [self didChangeValueForKey:@"userDragTimeInterval"];
*@endcode
*/
@property(nonatomic) NSTimeInterval userDragTimeInterval;
@end
@protocol JPVideoPlayerBufferingProtocol<JPVideoPlayerLayoutProtocol>
@optional
/**
* This method will be called when player buffering.
*
* @param videoURL The URL of video.
*/
- (void)didStartBufferingVideoURL:(NSURL *)videoURL;
/**
* This method will be called when player finish buffering and start play.
*
* @param videoURL The URL of video.
*/
- (void)didFinishBufferingVideoURL:(NSURL *)videoURL;
@end
@protocol JPVideoPlayerPlaybackProtocol<NSObject>
@required
/**
* The current playback rate.
*/
@property(nonatomic) float rate;
/**
* A Boolean value that indicates whether the audio output of the player is muted.
*/
@property(nonatomic) BOOL muted;
/**
* The audio playback volume for the player, ranging from 0.0 through 1.0 on a linear scale.
*/
@property(nonatomic) float volume;
/**
* Moves the playback cursor.
*
* @param time The time where seek to.
*
* @return The result of Moving the playback cursor, YES means succeed, NO means failure.
*/
- (BOOL)seekToTime:(CMTime)time;
/**
* Fetch the elapsed seconds of player.
*/
- (NSTimeInterval)elapsedSeconds;
/**
* Fetch the total seconds of player.
*/
- (NSTimeInterval)totalSeconds;
/**
* Call this method to pause playback.
*/
- (void)pause;
/**
* Call this method to resume playback.
*/
- (void)resume;
/**
* @return Returns the current time of the current player item.
*/
- (CMTime)currentTime;
/**
* Call this method to stop play video.
*/
- (void)stopPlay;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@class JPVideoPlayerResourceLoader,
JPResourceLoadingRequestWebTask,
JPVideoPlayerCacheFile;
NS_ASSUME_NONNULL_BEGIN
@protocol JPVideoPlayerResourceLoaderDelegate<NSObject>
@required
/**
* This method will be called when the current instance receive new loading request.
*
* @prama resourceLoader The current resource loader for videoURLAsset.
* @prama requestTask A abstract instance packaging the loading request.
*/
- (void)resourceLoader:(JPVideoPlayerResourceLoader *)resourceLoader
didReceiveLoadingRequestTask:(JPResourceLoadingRequestWebTask *)requestTask;
@end
@interface JPVideoPlayerResourceLoader : NSObject<AVAssetResourceLoaderDelegate>
@property (nonatomic, weak) id<JPVideoPlayerResourceLoaderDelegate> delegate;
/**
* The url custom passed in.
*/
@property (nonatomic, strong, readonly) NSURL *customURL;
/**
* The cache file take responsibility for save video data to disk and read cached video from disk.
*/
@property (nonatomic, strong, readonly) JPVideoPlayerCacheFile *cacheFile;
/**
* Convenience method to fetch instance of this class.
*
* @param customURL The url custom passed in.
*
* @return A instance of this class.
*/
+ (instancetype)resourceLoaderWithCustomURL:(NSURL *)customURL;
/**
* Designated initializer method.
*
* @param customURL The url custom passed in.
*
* @return A instance of this class.
*/
- (instancetype)initWithCustomURL:(NSURL *)customURL NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerResourceLoader.h"
#import "JPVideoPlayerCompat.h"
#import "JPVideoPlayerCacheFile.h"
#import "JPVideoPlayerCachePath.h"
#import "JPVideoPlayerManager.h"
#import "JPResourceLoadingRequestTask.h"
#import "JPVideoPlayerSupportUtils.h"
#import <pthread.h>
@interface JPVideoPlayerResourceLoader()<JPResourceLoadingRequestTaskDelegate>
@property (nonatomic, strong)NSMutableArray<AVAssetResourceLoadingRequest *> *loadingRequests;
@property (nonatomic, strong) AVAssetResourceLoadingRequest *runningLoadingRequest;
@property (nonatomic, strong) JPVideoPlayerCacheFile *cacheFile;
@property (nonatomic, strong) NSMutableArray<JPResourceLoadingRequestTask *> *requestTasks;
@property (nonatomic, strong) JPResourceLoadingRequestTask *runningRequestTask;
@property (nonatomic) pthread_mutex_t lock;
@property (nonatomic, strong) dispatch_queue_t ioQueue;
@end
@implementation JPVideoPlayerResourceLoader
- (void)dealloc {
if(self.runningRequestTask){
[self.runningRequestTask cancel];
[self removeCurrentRequestTaskAndResetAll];
}
self.loadingRequests = nil;
pthread_mutex_destroy(&_lock);
}
- (instancetype)init {
NSAssert(NO, @"Please use given initialize method.");
return [self initWithCustomURL:[NSURL new]];
}
+ (instancetype)resourceLoaderWithCustomURL:(NSURL *)customURL {
return [[JPVideoPlayerResourceLoader alloc] initWithCustomURL:customURL];
}
- (instancetype)initWithCustomURL:(NSURL *)customURL {
if(!customURL){
JPErrorLog(@"customURL can not be nil");
return nil;
}
self = [super init];
if(self){
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_lock, &mutexattr);
_ioQueue = dispatch_queue_create("com.NewPan.jpvideoplayer.resource.loader.www", DISPATCH_QUEUE_SERIAL);
_customURL = customURL;
_loadingRequests = [@[] mutableCopy];
NSString *key = [JPVideoPlayerManager.sharedManager cacheKeyForURL:customURL];
_cacheFile = [JPVideoPlayerCacheFile cacheFileWithFilePath:[JPVideoPlayerCachePath createVideoFileIfNeedThenFetchItForKey:key]
indexFilePath:[JPVideoPlayerCachePath createVideoIndexFileIfNeedThenFetchItForKey:key]];
}
return self;
}
#pragma mark - AVAssetResourceLoaderDelegate
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
if (resourceLoader && loadingRequest){
[self.loadingRequests addObject:loadingRequest];
JPDebugLog(@"ResourceLoader 接收到新的请求, 当前请求数: %ld <<<<<<<<<<<<<<", self.loadingRequests.count);
if(!self.runningLoadingRequest){
[self findAndStartNextLoadingRequestIfNeed];
}
}
return YES;
}
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader
didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
if ([self.loadingRequests containsObject:loadingRequest]) {
if(loadingRequest == self.runningLoadingRequest){
JPDebugLog(@"取消了一个正在进行的请求");
if(self.runningLoadingRequest && self.runningRequestTask){
[self.runningRequestTask cancel];
}
if([self.loadingRequests containsObject:self.runningLoadingRequest]){
[self.loadingRequests removeObject:self.runningLoadingRequest];
}
[self removeCurrentRequestTaskAndResetAll];
[self findAndStartNextLoadingRequestIfNeed];
}
else {
JPDebugLog(@"取消了一个不在进行的请求");
[self.loadingRequests removeObject:loadingRequest];
}
}
else {
JPDebugLog(@"要取消的请求已经完成了");
}
}
#pragma mark - JPResourceLoadingRequestTaskDelegate
- (void)requestTask:(JPResourceLoadingRequestTask *)requestTask
didCompleteWithError:(NSError *)error {
if (error.code == NSURLErrorCancelled) {
return;
}
if (![self.requestTasks containsObject:requestTask]) {
JPDebugLog(@"完成的 task 不是正在进行的 task");
return;
}
if (error) {
[self finishCurrentRequestWithError:error];
}
else {
[self finishCurrentRequestWithError:nil];
}
}
#pragma mark - Finish Request
- (void)finishCurrentRequestWithError:(NSError *)error {
if (error) {
JPDebugLog(@"ResourceLoader 完成一个请求 error: %@", error);
[self.runningRequestTask.loadingRequest finishLoadingWithError:error];
[self.loadingRequests removeObject:self.runningLoadingRequest];
[self removeCurrentRequestTaskAndResetAll];
[self findAndStartNextLoadingRequestIfNeed];
}
else {
JPDebugLog(@"ResourceLoader 完成一个请求, 没有错误");
// 要所有的请求都完成了才行.
[self.requestTasks removeObject:self.runningRequestTask];
if(!self.requestTasks.count){ // 全部完成.
[self.runningRequestTask.loadingRequest finishLoading];
[self.loadingRequests removeObject:self.runningLoadingRequest];
[self removeCurrentRequestTaskAndResetAll];
[self findAndStartNextLoadingRequestIfNeed];
}
else { // 完成了一部分, 继续请求.
[self startNextTaskIfNeed];
}
}
}
#pragma mark - Private
- (void)findAndStartNextLoadingRequestIfNeed {
if(self.runningLoadingRequest || self.runningRequestTask){
return;
}
if (self.loadingRequests.count == 0) {
return;
}
self.runningLoadingRequest = [self.loadingRequests firstObject];
NSRange dataRange = [self fetchRequestRangeWithRequest:self.runningLoadingRequest];
if (dataRange.length == NSUIntegerMax) {
dataRange.length = [self.cacheFile fileLength] - dataRange.location;
}
[self startCurrentRequestWithLoadingRequest:self.runningLoadingRequest
range:dataRange];
}
- (void)startCurrentRequestWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
range:(NSRange)dataRange {
/// 是否已经完全缓存完成.
BOOL isCompleted = self.cacheFile.isCompleted;
JPDebugLog(@"ResourceLoader 处理新的请求, 数据范围是: %@, 是否已经缓存完成: %@", NSStringFromRange(dataRange), isCompleted ? @"是" : @"否");
if (dataRange.length == NSUIntegerMax) {
[self addTaskWithLoadingRequest:loadingRequest
range:NSMakeRange(dataRange.location, NSUIntegerMax)
cached:NO];
}
else {
NSUInteger start = dataRange.location;
NSUInteger end = NSMaxRange(dataRange);
while (start < end) {
NSRange firstNotCachedRange = [self.cacheFile firstNotCachedRangeFromPosition:start];
if (!JPValidFileRange(firstNotCachedRange)) {
[self addTaskWithLoadingRequest:loadingRequest
range:dataRange
cached:self.cacheFile.cachedDataBound > 0];
start = end;
}
else if (firstNotCachedRange.location >= end) {
[self addTaskWithLoadingRequest:loadingRequest
range:dataRange
cached:YES];
start = end;
}
else if (firstNotCachedRange.location >= start) {
if (firstNotCachedRange.location > start) {
[self addTaskWithLoadingRequest:loadingRequest
range:NSMakeRange(start, firstNotCachedRange.location - start)
cached:YES];
}
NSUInteger notCachedEnd = MIN(NSMaxRange(firstNotCachedRange), end);
[self addTaskWithLoadingRequest:loadingRequest
range:NSMakeRange(firstNotCachedRange.location, notCachedEnd - firstNotCachedRange.location)
cached:NO];
start = notCachedEnd;
}
else {
[self addTaskWithLoadingRequest:loadingRequest
range:dataRange
cached:YES];
start = end;
}
}
}
// 发起请求.
[self startNextTaskIfNeed];
}
- (void)addTaskWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
range:(NSRange)range
cached:(BOOL)cached {
JPResourceLoadingRequestTask *task;
if(cached){
JPDebugLog(@"ResourceLoader 创建了一个本地请求");
task = [JPResourceLoadingRequestLocalTask requestTaskWithLoadingRequest:loadingRequest
requestRange:range
cacheFile:self.cacheFile
customURL:self.customURL
cached:cached];
}
else {
task = [JPResourceLoadingRequestWebTask requestTaskWithLoadingRequest:loadingRequest
requestRange:range
cacheFile:self.cacheFile
customURL:self.customURL
cached:cached];
JPDebugLog(@"ResourceLoader 创建一个网络请求: %@", task);
if (self.delegate && [self.delegate respondsToSelector:@selector(resourceLoader:didReceiveLoadingRequestTask:)]) {
[self.delegate resourceLoader:self didReceiveLoadingRequestTask:(JPResourceLoadingRequestWebTask *)task];
}
}
int lock = pthread_mutex_trylock(&_lock);
task.delegate = self;
if (!self.requestTasks) {
self.requestTasks = [@[] mutableCopy];
}
[self.requestTasks addObject:task];
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
- (void)removeCurrentRequestTaskAndResetAll {
self.runningLoadingRequest = nil;
self.requestTasks = [@[] mutableCopy];
self.runningRequestTask = nil;
}
- (void)startNextTaskIfNeed {
int lock = pthread_mutex_trylock(&_lock);;
self.runningRequestTask = self.requestTasks.firstObject;
if ([self.runningRequestTask isKindOfClass:[JPResourceLoadingRequestLocalTask class]]) {
[self.runningRequestTask startOnQueue:self.ioQueue];
}
else {
[self.runningRequestTask start];
}
if (!lock) {
pthread_mutex_unlock(&_lock);
}
}
- (NSRange)fetchRequestRangeWithRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
NSUInteger location, length;
// data range.
if ([loadingRequest.dataRequest respondsToSelector:@selector(requestsAllDataToEndOfResource)] && loadingRequest.dataRequest.requestsAllDataToEndOfResource) {
location = (NSUInteger)loadingRequest.dataRequest.requestedOffset;
length = NSUIntegerMax;
}
else {
location = (NSUInteger)loadingRequest.dataRequest.requestedOffset;
length = loadingRequest.dataRequest.requestedLength;
}
if(loadingRequest.dataRequest.currentOffset > 0){
location = (NSUInteger)loadingRequest.dataRequest.currentOffset;
}
return NSMakeRange(location, length);
}
@end
//
// Created by NewPan on 2019-02-25.
// Copyright (c) 2019 NewPan. All rights reserved.
//
#import "JPVideoPlayerCellProtocol.h"
typedef NS_ENUM(NSUInteger, JPScrollPlayStrategyType) {
/**
* `JPScrollFindBestCell` strategy mean find which cell need play video by the space from the center of cell
* to the center of `jp_tableViewVisibleFrame`, h1 on bottom picture.
*/
JPScrollPlayStrategyTypeBestCell = 0,
/**
* `JPScrollFindBestCell` strategy mean find which cell need play video by the space from the center of videoView
* to the center of `jp_tableViewVisibleFrame`, h2 on bottom picture.
*/
JPScrollPlayStrategyTypeBestVideoView,
};
typedef UIView<JPVideoPlayerCellProtocol> *_Nullable (^JPPlayVideoInVisibleCellsBlock)(NSArray<UIView<JPVideoPlayerCellProtocol> *> *_Nullable visibleCells);
NS_ASSUME_NONNULL_BEGIN
@protocol JPVideoPlayerScrollViewProtocol;
@protocol JPScrollViewPlayVideoDelegate<NSObject>
@optional
/**
* This method will be call when call `jp_playVideoInVisibleCellsIfNeed` and the find the best cell to play video when
* tableView or collectionView scroll end.
*
* @param tableView The tableView or collectionView.
* @param cell The cell ready to play video, you can call `[cell.jp_videoPlayView jp_playVideoMuteWithURL:cell.jp_videoURL progressView:nil]`
* or other method given to play video.
*/
- (void)scrollView:(UIScrollView<JPVideoPlayerScrollViewProtocol> *)scrollView willPlayVideoOnCell:(UIView<JPVideoPlayerCellProtocol> *)cell;
@end
@protocol JPVideoPlayerScrollViewProtocol <NSObject>
@property (nonatomic) id<JPScrollViewPlayVideoDelegate> jp_delegate;
/**
* The cell is playing video.
*/
@property(nonatomic, readonly, nullable) UIView<JPVideoPlayerCellProtocol> *jp_playingVideoCell;
/**
* The visible frame of tableView. `visible` mean when the tableView frame is {0, 0, screenWidth, screenHeight},
* but tableView is wrapped by `UITabBarController` and `UINavigationController`, `UINavigationBar` and `UITabBar`
* is visible, so the visible frame of tableView is {0, navigationBarHeight, screenWidth, screenHeight - navigationBarHeight - tabBarHeight}.
* {0, navigationBarHeight, screenWidth, screenHeight - navigationBarHeight} if `UITabBar` is hidden.
*
* @warning This value must be not empty.
*/
@property (nonatomic) CGRect jp_scrollViewVisibleFrame;
/**
* Display visible frame of the scrollView for debug, default is NO.
*/
@property(nonatomic, assign) BOOL jp_debugScrollViewVisibleFrame;
/**
* The play cell strategy when tableView stop scroll, `JPScrollFindStrategyBestCell` by default.
*
* @see `JPScrollFindStrategy`.
*
*
* ****************************** center of `jp_tableViewVisibleFrame`
* |h2 |h1
* ----------|-----|-------------
* | | | |
* | cell | | |
* | | | |
* | --------|-----|--- |
* | |videoView | | |
* | | | * <- cell center
* | | * <- videoView center
* | | | |
* | | | |
* | ------------------ |
* | |
* ------------------------------
*/
@property (nonatomic) JPScrollPlayStrategyType jp_scrollPlayStrategyType;
/**
* Because we play video on cell that stopped on screen center when the tableView was stopped scrolling,
* so some cell may can not stop in screen center, this type cell always is on top or bottom in tableView, we call this type cell `unreachableCell`.
* so we need handle this especially. but first we need do is to check the situation of this type cell appear.
*
* Here is the result of my measure on iPhone 6s(CH).
* The number of visible cells in screen: 4 3 2
* The number of cells cannot stop in screen center: 1 1 0
*
* The default dictionary content is: @{
* @"4" : @"1",
* @"3" : @"1",
* @"2" : @"0"
* };
*
* @warning you need to know that the mean of result, For example, when we got 4 cells in screen,
* this time we find 1 cell that can not stop in screen center on top, and we got the same cell that cannot stop in screen center on bottom at the same time.
* The cell of cannot stop in screen center only appear when the count of visible cells is greater than 3.
*
* @note You can custom this dictionary.
*/
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *jp_unreachableCellDictionary;
/**
* Use this block to custom choosing cell process when call `jp_playVideoInVisibleCellsIfNeed`.
*/
@property(nonatomic) JPPlayVideoInVisibleCellsBlock jp_playVideoInVisibleCellsBlock;
/**
* Use this block to custom finding the best cell process when scrollView did stop scroll.
*/
@property(nonatomic) JPPlayVideoInVisibleCellsBlock jp_findBestCellInVisibleCellsBlock;
/**
* This method be used to find the first cell need to play video in visible cells.
* This method should be call after tableView is finished `-reloadData`.
* Suggest call this method in `-viewDidAppear:` method.
*/
- (void)jp_playVideoInVisibleCellsIfNeed;
/**
* Call this method to stop video play.
*/
- (void)jp_stopPlayIfNeed;
/**
* This method must be call after called `reloadData` for tableView.
*/
- (void)jp_handleCellUnreachableTypeInVisibleCellsAfterReloadData;
/**
* This method must be called in `-tableView:cellForRowAtIndexPath:`, and pass cell and indexPath in.
*
* @param cell A `UITableViewCell`.
* @param indexPath The indexPath of cell.
*
* @warning This method must be call in given method.
*/
- (void)jp_handleCellUnreachableTypeForCell:(UIView<JPVideoPlayerCellProtocol> *)cell
atIndexPath:(NSIndexPath *)indexPath;
/**
* This method must be call in `-scrollViewDidScroll:` method.
*
* * @warning This method must be call in given method.
*/
- (void)jp_scrollViewDidScroll;
/**
* This method must be call in `scrollViewDidEndDragging:willDecelerate:`.
*
* @param decelerate The tableView will decelerate or not.
*
* @warning This method must be call in given method.
*/
- (void)jp_scrollViewDidEndDraggingWillDecelerate:(BOOL)decelerate;
/**
* This method must be call in `scrollViewDidEndDecelerating:`.
*
* @warning This method must be call in given method.
*/
- (void)jp_scrollViewDidEndDecelerating;
/**
* You can use this method to judge a view is visible or not when scrollView did scroll.
*
* @param view The target view, the view must be a subview on this tableView.
*
* @return The result.
*/
- (BOOL)jp_viewIsVisibleInVisibleFrameAtScrollViewDidScroll:(UIView *)view;
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
//
// Created by NewPan on 2019-02-25.
// Copyright (c) 2019 NewPan. All rights reserved.
//
#import "JPVideoPlayerScrollViewProtocol.h"
#import "JPMethodInjecting.h"
#import "JPVideoPlayerSupportUtils.h"
@jp_concreteprotocol(JPVideoPlayerScrollViewProtocol)
- (void)setJp_delegate:(id <JPScrollViewPlayVideoDelegate>)jp_delegate {
self.helper.delegate = jp_delegate;
}
- (id <JPScrollViewPlayVideoDelegate>)jp_delegate {
return self.helper.delegate;
}
- (UITableViewCell *)jp_playingVideoCell {
return [self.helper playingVideoCell];
}
- (void)setJp_scrollViewVisibleFrame:(CGRect)jp_scrollViewVisibleFrame {
self.helper.scrollViewVisibleFrame = jp_scrollViewVisibleFrame;
}
- (CGRect)jp_scrollViewVisibleFrame {
return self.helper.scrollViewVisibleFrame;
}
- (void)setJp_debugScrollViewVisibleFrame:(BOOL)jp_debugScrollViewVisibleFrame {
self.helper.debugScrollViewVisibleFrame = jp_debugScrollViewVisibleFrame;
}
- (BOOL)jp_debugScrollViewVisibleFrame {
return self.helper.debugScrollViewVisibleFrame;
}
- (void)setJp_scrollPlayStrategyType:(JPScrollPlayStrategyType)jp_scrollPlayStrategyType {
self.helper.scrollPlayStrategyType = jp_scrollPlayStrategyType;
}
- (JPScrollPlayStrategyType)jp_scrollPlayStrategyType {
return self.helper.scrollPlayStrategyType;
}
- (void)setJp_unreachableCellDictionary:(NSDictionary<NSString *, NSString *> *)jp_unreachableCellDictionary {
self.helper.unreachableCellDictionary = jp_unreachableCellDictionary;
}
- (NSDictionary<NSString *, NSNumber *> *)jp_unreachableCellDictionary {
return self.helper.unreachableCellDictionary;
}
- (void)setJp_playVideoInVisibleCellsBlock:(JPPlayVideoInVisibleCellsBlock)jp_playVideoInVisibleCellsBlock {
self.helper.playVideoInVisibleCellsBlock = jp_playVideoInVisibleCellsBlock;
}
- (JPPlayVideoInVisibleCellsBlock)jp_playVideoInVisibleCellsBlock {
return self.helper.playVideoInVisibleCellsBlock;
}
- (void)setJp_findBestCellInVisibleCellsBlock:(JPPlayVideoInVisibleCellsBlock)jp_findBestCellInVisibleCellsBlock {
self.helper.findBestCellInVisibleCellsBlock = jp_findBestCellInVisibleCellsBlock;
}
- (JPPlayVideoInVisibleCellsBlock)jp_findBestCellInVisibleCellsBlock {
return self.helper.findBestCellInVisibleCellsBlock;
}
- (void)jp_playVideoInVisibleCellsIfNeed {
[self.helper playVideoInVisibleCellsIfNeed];
}
- (void)jp_stopPlayIfNeed {
[self.helper stopPlayIfNeed];
}
- (void)jp_handleCellUnreachableTypeInVisibleCellsAfterReloadData {
[self.helper handleCellUnreachableTypeInVisibleCellsAfterReloadData];
}
- (void)jp_handleCellUnreachableTypeForCell:(UIView<JPVideoPlayerCellProtocol> *)cell
atIndexPath:(NSIndexPath *)indexPath {
[self.helper handleCellUnreachableTypeForCell:cell
atIndexPath:indexPath];
}
- (void)jp_scrollViewDidEndDraggingWillDecelerate:(BOOL)decelerate {
[self.helper scrollViewDidEndDraggingWillDecelerate:decelerate];
}
- (void)jp_scrollViewDidEndDecelerating {
[self.helper scrollViewDidEndDecelerating];
}
- (void)jp_scrollViewDidScroll {
[self.helper scrollViewDidScroll];
}
- (BOOL)jp_viewIsVisibleInVisibleFrameAtScrollViewDidScroll:(UIView *)view {
return [self.helper viewIsVisibleInVisibleFrameAtScrollViewDidScroll:view];
}
#pragma mark - Private
- (JPVideoPlayerScrollViewInternalObject *)helper {
JPVideoPlayerScrollViewInternalObject *_helper = objc_getAssociatedObject(self, _cmd);
if(!_helper){
_helper = [[JPVideoPlayerScrollViewInternalObject alloc] initWithScrollView:self];
objc_setAssociatedObject(self, _cmd, _helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return _helper;
}
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import <UIKit/UIKit.h>
#import "JPResourceLoadingRequestTask.h"
#import "UITableViewCell+WebVideoCache.h"
#import "UITableView+WebVideoCache.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (cURL)
/**
* Returns a `curl` command string equivalent of the current object.
*
* @return The URL to format.
*/
- (NSString *)jp_cURLCommand;
@end
@interface NSHTTPURLResponse (JPVideoPlayer)
/**
* Fetch the file length of response.
*
* @return The file length of response.
*/
- (long long)jp_fileLength;
/**
* Check the response support streaming or not.
*
* @return The response support streaming or not.
*/
- (BOOL)jp_supportRange;
@end
@interface AVAssetResourceLoadingRequest (JPVideoPlayer)
/**
* Fill content information for current request use response conent.
*
* @param response A response.
*/
- (void)jp_fillContentInformationWithResponse:(NSHTTPURLResponse *)response;
@end
@interface NSFileHandle (JPVideoPlayer)
- (BOOL)jp_safeWriteData:(NSData *)data;
@end
@interface NSURLSessionTask(JPVideoPlayer)
@property(nonatomic) JPResourceLoadingRequestWebTask * webTask;
@end
@interface NSObject (JPSwizzle)
+ (BOOL)jp_swizzleMethod:(SEL)origSel withMethod:(SEL)altSel error:(NSError**)error;
+ (BOOL)jp_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_;
@end
@interface JPLog : NSObject
/**
* Output message to console.
*
* @param logLevel The log type.
* @param file The current file name.
* @param function The current function name.
* @param line The current line number.
* @param format The log format.
*/
+ (void)logWithFlag:(JPLogLevel)logLevel
file:(const char *)file
function:(const char *)function
line:(NSUInteger)line
format:(NSString *)format, ...;
@end
#ifdef __OBJC__
#define JP_LOG_MACRO(logFlag, frmt, ...) \
[JPLog logWithFlag:logFlag\
file:__FILE__ \
function:__FUNCTION__ \
line:__LINE__ \
format:(frmt), ##__VA_ARGS__]
#define JP_LOG_MAYBE(logFlag, frmt, ...) JP_LOG_MACRO(logFlag, frmt, ##__VA_ARGS__)
#if DEBUG
/**
* Log debug log.
*/
#define JPDebugLog(frmt, ...) JP_LOG_MAYBE(JPLogLevelDebug, frmt, ##__VA_ARGS__)
/**
* Log debug and warning log.
*/
#define JPWarningLog(frmt, ...) JP_LOG_MAYBE(JPLogLevelWarning, frmt, ##__VA_ARGS__)
/**
* Log debug, warning and error log.
*/
#define JPErrorLog(frmt, ...) JP_LOG_MAYBE(JPLogLevelError, frmt, ##__VA_ARGS__)
#else
#define JPDebugLog(frmt, ...)
#define JPWarningLog(frmt, ...)
#define JPErrorLog(frmt, ...)
#endif
#endif
typedef NS_ENUM(NSInteger, JPApplicationState) {
JPApplicationStateUnknown = 0,
JPApplicationStateWillResignActive,
JPApplicationStateDidEnterBackground,
JPApplicationStateWillEnterForeground,
JPApplicationStateDidBecomeActive
};
@class JPApplicationStateMonitor;
@protocol JPApplicationStateMonitorDelegate <NSObject>
@optional
/**
* This method will be called when application state changed.
*
* @param monitor The current object.
* @param applicationState The application state.
*/
- (void)applicationStateMonitor:(JPApplicationStateMonitor *)monitor
applicationStateDidChange:(JPApplicationState)applicationState;
/**
* This method will be called only when application become active from `Control Center`,
* `Notification Center`, `pop UIAlert`, `double click Home-Button`.
*
* @param monitor The current object.
*/
- (void)applicationDidBecomeActiveFromResignActive:(JPApplicationStateMonitor *)monitor;
/**
* This method will be called only when application become active from `Share to other application`,
* `Enter background`, `Lock screen`.
*
* @param monitor The current object.
*/
- (void)applicationDidBecomeActiveFromBackground:(JPApplicationStateMonitor *)monitor;
@end
@interface JPApplicationStateMonitor : NSObject
@property(nonatomic, weak) id<JPApplicationStateMonitorDelegate> delegate;
@property (nonatomic, assign, readonly) JPApplicationState applicationState;
@end
@protocol JPScrollViewPlayVideoDelegate;
@interface JPVideoPlayerScrollViewInternalObject : NSObject
@property (nonatomic, weak, readonly, nullable) UIScrollView<JPVideoPlayerScrollViewProtocol> *scrollView;
@property (nonatomic, weak, readonly, nullable) UIView<JPVideoPlayerCellProtocol> *playingVideoCell;
@property (nonatomic, assign) CGRect scrollViewVisibleFrame;
@property(nonatomic, assign) BOOL debugScrollViewVisibleFrame;
@property (nonatomic, assign) JPScrollPlayStrategyType scrollPlayStrategyType;
@property(nonatomic) JPPlayVideoInVisibleCellsBlock playVideoInVisibleCellsBlock;
@property(nonatomic) JPPlayVideoInVisibleCellsBlock findBestCellInVisibleCellsBlock;
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *unreachableCellDictionary;
@property (nonatomic, weak) id<JPScrollViewPlayVideoDelegate> delegate;
@property (nonatomic, assign) NSUInteger playVideoSection;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithScrollView:(UIScrollView<JPVideoPlayerScrollViewProtocol> *)scrollView NS_DESIGNATED_INITIALIZER;
- (void)handleCellUnreachableTypeForCell:(UIView<JPVideoPlayerCellProtocol> *)cell
atIndexPath:(NSIndexPath *)indexPath;
- (void)handleCellUnreachableTypeInVisibleCellsAfterReloadData;
- (void)playVideoInVisibleCellsIfNeed;
- (void)stopPlayIfNeed;
- (void)scrollViewDidScroll;
- (void)scrollViewDidEndDraggingWillDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating;
- (BOOL)viewIsVisibleInVisibleFrameAtScrollViewDidScroll:(UIView *)view;
@end
@interface JPMigration : NSObject
/**
* Executes a block of code for a specific version number and remembers this version as the latest migration done.
*
* @param version A string with a specific version number.
* @param migrationBlock A block object to be executed when the SDK version matches the string 'version'.
* This parameter can't be nil.
*/
+ (void)migrateToSDKVersion:(NSString *)version
block:(dispatch_block_t)migrationBlock;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerSupportUtils.h"
#import "JPVideoPlayer.h"
#import "UIView+WebVideoCache.h"
#import <MobileCoreServices/MobileCoreServices.h>
#import "JPGCDExtensions.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSMutableString (JPURLRequestFormatter)
- (void)jp_appendCommandLineArgument:(NSString *)arg;
@end
@implementation NSMutableString (JPURLRequestFormatter)
- (void)jp_appendCommandLineArgument:(NSString *)arg {
[self appendFormat:@" %@", [arg stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
}
@end
@interface JPURLRequestFormatter : NSObject
@end
@implementation JPURLRequestFormatter
+ (NSString *)cURLCommandFromURLRequest:(NSURLRequest *)request {
NSMutableString *command = [NSMutableString stringWithString:@"curl"];
[command jp_appendCommandLineArgument:[NSString stringWithFormat:@"-X %@", [request HTTPMethod]]];
if ([[request HTTPBody] length] > 0) {
NSMutableString *HTTPBodyString = [[NSMutableString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding];
[HTTPBodyString replaceOccurrencesOfString:@"\\" withString:@"\\\\" options:0 range:NSMakeRange(0, [HTTPBodyString length])];
[HTTPBodyString replaceOccurrencesOfString:@"`" withString:@"\\`" options:0 range:NSMakeRange(0, [HTTPBodyString length])];
[HTTPBodyString replaceOccurrencesOfString:@"\"" withString:@"\\\"" options:0 range:NSMakeRange(0, [HTTPBodyString length])];
[HTTPBodyString replaceOccurrencesOfString:@"$" withString:@"\\$" options:0 range:NSMakeRange(0, [HTTPBodyString length])];
[command jp_appendCommandLineArgument:[NSString stringWithFormat:@"-d \"%@\"", HTTPBodyString]];
}
NSString *acceptEncodingHeader = [[request allHTTPHeaderFields] valueForKey:@"Accept-Encoding"];
if ([acceptEncodingHeader rangeOfString:@"gzip"].location != NSNotFound) {
[command jp_appendCommandLineArgument:@"--compressed"];
}
if ([request URL]) {
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:[request URL]];
if (cookies.count) {
NSMutableString *mutableCookieString = [NSMutableString string];
for (NSHTTPCookie *cookie in cookies) {
[mutableCookieString appendFormat:@"%@=%@;", cookie.name, cookie.value];
}
[command jp_appendCommandLineArgument:[NSString stringWithFormat:@"--cookie \"%@\"", mutableCookieString]];
}
}
for (id field in [request allHTTPHeaderFields]) {
[command jp_appendCommandLineArgument:[NSString stringWithFormat:@"-H %@", [NSString stringWithFormat:@"'%@: %@'", field, [[request valueForHTTPHeaderField:field] stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"]]]];
}
[command jp_appendCommandLineArgument:[NSString stringWithFormat:@"\"%@\"", [[request URL] absoluteString]]];
return [NSString stringWithString:command];
}
@end
@implementation NSURL (cURL)
- (NSString *)jp_cURLCommand {
NSURLRequest *request = [NSURLRequest requestWithURL:self];
if(!request){
return nil;
}
return [JPURLRequestFormatter cURLCommandFromURLRequest:request];
}
@end
@implementation NSFileHandle (JPVideoPlayer)
- (BOOL)jp_safeWriteData:(NSData *)data {
NSInteger retry = 3;
size_t bytesLeft = data.length;
const void *bytes = [data bytes];
int fileDescriptor = [self fileDescriptor];
while (bytesLeft > 0 && retry > 0) {
ssize_t amountSent = write(fileDescriptor, bytes + data.length - bytesLeft, bytesLeft);
if (amountSent < 0) {
// write failed.
JPErrorLog(@"Write file failed");
break;
}
else {
bytesLeft = bytesLeft - amountSent;
if (bytesLeft > 0) {
// not finished continue write after sleep 1 second.
JPWarningLog(@"Write file retry");
sleep(1); //probably too long, but this is quite rare.
retry--;
}
}
}
return bytesLeft == 0;
}
@end
@implementation NSHTTPURLResponse (JPVideoPlayer)
- (long long)jp_fileLength {
NSString *range = [self allHeaderFields][@"Content-Range"];
if (range) {
NSArray *ranges = [range componentsSeparatedByString:@"/"];
if (ranges.count > 0) {
NSString *lengthString = [[ranges lastObject] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
return [lengthString longLongValue];
}
}
else {
return [self expectedContentLength];
}
return 0;
}
- (BOOL)jp_supportRange {
return [self allHeaderFields][@"Content-Range"] != nil;
}
@end
@implementation AVAssetResourceLoadingRequest (JPVideoPlayer)
- (void)jp_fillContentInformationWithResponse:(NSHTTPURLResponse *)response {
if (!response) {
return;
}
self.response = response;
if (!self.contentInformationRequest) {
return;
}
NSString *mimeType = [response MIMEType];
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL);
self.contentInformationRequest.byteRangeAccessSupported = [response jp_supportRange];
self.contentInformationRequest.contentType = CFBridgingRelease(contentType);
self.contentInformationRequest.contentLength = [response jp_fileLength];
JPDebugLog(@"填充了响应信息到 contentInformationRequest");
}
@end
@implementation NSURLSessionTask(JPVideoPlayer)
- (void)setWebTask:(JPResourceLoadingRequestWebTask *)webTask {
id __weak __weak_object = webTask;
id (^__weak_block)(void) = ^{
return __weak_object;
};
objc_setAssociatedObject(self, @selector(webTask), __weak_block, OBJC_ASSOCIATION_COPY);
}
- (JPResourceLoadingRequestWebTask *)webTask {
id (^__weak_block)(void) = objc_getAssociatedObject(self, _cmd);
if (!__weak_block) {
return nil;
}
return __weak_block();
}
@end
NSString *kJPSwizzleErrorDomain = @"com.jpvideoplayer.swizzle.www";
@implementation NSObject (JPSwizzle)
#if OBJC_API_VERSION >= 2
#define GetClass(obj) object_getClass(obj)
#else
#define GetClass(obj) (obj ? obj->isa : Nil)
#endif
+ (BOOL)jp_swizzleMethod:(SEL)origSel withMethod:(SEL)altSel error:(NSError**)error {
Method origMethod = class_getInstanceMethod(self, origSel);
if (!origMethod) {
*error = [NSError errorWithDomain:kJPSwizzleErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"original method %@ not found for class %@", NSStringFromSelector(origSel), [self class]]
}];
return NO;
}
Method altMethod = class_getInstanceMethod(self, altSel);
if (!altMethod) {
*error = [NSError errorWithDomain:kJPSwizzleErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"alternate method %@ not found for class %@", NSStringFromSelector(altSel), [self class]]
}];
return NO;
}
class_addMethod(self,
origSel,
class_getMethodImplementation(self, origSel),
method_getTypeEncoding(origMethod));
class_addMethod(self,
altSel,
class_getMethodImplementation(self, altSel),
method_getTypeEncoding(altMethod));
method_exchangeImplementations(class_getInstanceMethod(self, origSel), class_getInstanceMethod(self, altSel));
return YES;
}
+ (BOOL)jp_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_ {
return [GetClass((id)self) jp_swizzleMethod:origSel_ withMethod:altSel_ error:error_];
}
@end
NSString *JPLogMessage = nil;
NSString *JPLogThreadName = nil;
static dispatch_queue_t JPLogSyncQueue;
@implementation JPLog
+ (void)initialize {
_logLevel = JPLogLevelDebug;
JPLogSyncQueue = dispatch_queue_create("com.jpvideoplayer.log.sync.queue.www", DISPATCH_QUEUE_SERIAL);
}
+ (void)logWithFlag:(JPLogLevel)logLevel
file:(const char *)file
function:(const char *)function
line:(NSUInteger)line
format:(NSString *)format, ... {
if (logLevel > _logLevel || !format) return;
va_list args;
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
JPDispatchAsyncOnQueue(JPLogSyncQueue, ^{
JPLogMessage = message;
if (JPLogMessage.length) {
NSString *flag;
switch (logLevel) {
case JPLogLevelDebug:
flag = @"DEBUG";
break;
case JPLogLevelWarning:
flag = @"Waring";
break;
case JPLogLevelError:
flag = @"Error";
break;
default:
break;
}
JPLogThreadName = [[NSThread currentThread] description];
JPLogThreadName = [JPLogThreadName componentsSeparatedByString:@">"].lastObject;
JPLogThreadName = [JPLogThreadName componentsSeparatedByString:@","].firstObject;
JPLogThreadName = [JPLogThreadName stringByReplacingOccurrencesOfString:@"{number = " withString:@""];
// message = [NSString stringWithFormat:@"[%@] [Thread: %@] %@ => [%@ + %ld]", flag, threadName, message, tempString, line];
JPLogMessage = [NSString stringWithFormat:@"[%@] [Thread: %02ld] [%@]", flag, (long)[JPLogThreadName integerValue], JPLogMessage];
NSLog(@"%@", JPLogMessage);
}
});
}
@end
@interface JPApplicationStateMonitor()
@property(nonatomic, strong) NSMutableArray<NSNumber *> *applicationStateArray;
@property (nonatomic, assign) JPApplicationState applicationState;
@end
@implementation JPApplicationStateMonitor
- (instancetype)init {
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (void)dealloc {
[self removeNotificationObserver];
}
#pragma mark - Setup
- (void)setup {
[self addNotificationObserver];
self.applicationStateArray = [NSMutableArray array];
self.applicationState = JPApplicationStateUnknown;
}
- (void)addNotificationObserver {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackgroundNotification)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForegroundNotification)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActiveNotification)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActiveNotification)
name:UIApplicationWillResignActiveNotification
object:nil];
}
- (void)removeNotificationObserver {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Notification
- (void)applicationWillResignActiveNotification {
if (self.applicationStateArray.count) {
[self.applicationStateArray removeAllObjects];
}
[self.applicationStateArray addObject:@(JPApplicationStateWillResignActive)];
[self callDelegateMethodWithApplicationState:JPApplicationStateWillResignActive];
self.applicationState = JPApplicationStateWillResignActive;
JPDebugLog(@"JPApplicationStateWillResignActive");
}
- (void)applicationDidEnterBackgroundNotification {
[self.applicationStateArray addObject:@(JPApplicationStateDidEnterBackground)];
[self callDelegateMethodWithApplicationState:JPApplicationStateDidEnterBackground];
self.applicationState = JPApplicationStateDidEnterBackground;
JPDebugLog(@"JPApplicationStateDidEnterBackground");
}
- (void)applicationWillEnterForegroundNotification {
[self.applicationStateArray addObject:@(JPApplicationStateWillEnterForeground)];
[self callDelegateMethodWithApplicationState:JPApplicationStateWillEnterForeground];
self.applicationState = JPApplicationStateWillEnterForeground;
JPDebugLog(@"JPApplicationStateWillEnterForeground");
}
- (void)applicationDidBecomeActiveNotification{
[self callDelegateMethodWithApplicationState:JPApplicationStateDidBecomeActive];
self.applicationState = JPApplicationStateDidBecomeActive;
JPDebugLog(@"JPApplicationStateDidBecomeActive");
BOOL didEnterBackground = NO;
for (NSNumber *appStateNumber in self.applicationStateArray) {
NSInteger appState = appStateNumber.integerValue;
if (appState == JPApplicationStateDidEnterBackground) {
didEnterBackground = YES;
break;
}
}
if (!didEnterBackground) {
if (self.delegate && [self.delegate respondsToSelector:@selector(applicationDidBecomeActiveFromResignActive:)]) {
[self.delegate applicationDidBecomeActiveFromResignActive:self];
}
}
else {
if (self.delegate && [self.delegate respondsToSelector:@selector(applicationDidBecomeActiveFromBackground:)]) {
[self.delegate applicationDidBecomeActiveFromBackground:self];
}
}
}
- (void)callDelegateMethodWithApplicationState:(JPApplicationState)applicationState {
if (self.delegate && [self.delegate respondsToSelector:@selector(applicationStateMonitor:applicationStateDidChange:)]) {
[self.delegate applicationStateMonitor:self applicationStateDidChange:applicationState];
}
}
@end
@interface JPVideoPlayerScrollViewInternalObject()
@property (nonatomic, weak) UIView<JPVideoPlayerCellProtocol> *playingVideoCell;
@property(nonatomic, strong) CAShapeLayer *debugScrollViewVisibleFrameLayer;
@end
@implementation JPVideoPlayerScrollViewInternalObject
+ (instancetype)new {
NSAssert(NO, @"Please use given initialize method.");
return nil;
}
- (instancetype)init {
NSAssert(NO, @"Please use given initialize method.");
return nil;
};
- (instancetype)initWithScrollView:(UIScrollView<JPVideoPlayerScrollViewProtocol> *)scrollView {
if(!scrollView){
JPErrorLog(@"scrollView can not be nil.");
return nil;
}
self = [super init];
if(self){
_scrollView = scrollView;
_scrollViewVisibleFrame = CGRectZero;
}
return self;
}
- (void)handleCellUnreachableTypeInVisibleCellsAfterReloadData {
if (!self.scrollView || ![self.scrollView isKindOfClass:[UITableView class]] && ![self.scrollView isKindOfClass:[UICollectionView class]]) return;
JPDispatchAfterTimeIntervalInSecond(0.3f, ^{
UITableView *tableView = (UITableView *)self.scrollView;
for(UITableViewCell *cell in tableView.visibleCells){
[self handleCellUnreachableTypeForCell:cell atIndexPath:[tableView indexPathForCell:cell]];
}
});
}
- (void)handleCellUnreachableTypeForCell:(UIView<JPVideoPlayerCellProtocol> *)cell
atIndexPath:(NSIndexPath *)indexPath {
if (!self.scrollView || ![self.scrollView isKindOfClass:[UITableView class]] && ![self.scrollView isKindOfClass:[UICollectionView class]]) return;
UITableView *tableView = (UITableView *)self.scrollView;
NSArray<UITableViewCell *> *visibleCells = [tableView visibleCells];
if(!visibleCells.count) return;
NSUInteger unreachableCellCount = [self fetchUnreachableCellCountWithVisibleCellsCount:visibleCells.count];
NSInteger sectionsCount = tableView.numberOfSections;
NSInteger rows = 0;
if ([self.scrollView isKindOfClass:[UITableView class]]) {
rows = [tableView numberOfRowsInSection:indexPath.section];
}
else if ([self.scrollView isKindOfClass:[UICollectionView class]]) {
UICollectionView *collectionView = (UICollectionView *)self.scrollView;
rows = [collectionView numberOfItemsInSection:indexPath.section];
}
BOOL isFirstSectionInSections = YES;
BOOL isLastSectionInSections = YES;
if(sectionsCount > 1){
if(indexPath.section != 0){
isFirstSectionInSections = NO;
}
if(indexPath.section != (sectionsCount - 1)){
isLastSectionInSections = NO;
}
}
if (unreachableCellCount > 0) {
if (indexPath.row <= (unreachableCellCount - 1)) {
if(isFirstSectionInSections){
cell.jp_unreachableCellType = JPVideoPlayerUnreachableCellTypeTop;
}
}
else if (indexPath.row >= (rows - unreachableCellCount)){
if(isLastSectionInSections){
cell.jp_unreachableCellType = JPVideoPlayerUnreachableCellTypeDown;
}
}
else{
cell.jp_unreachableCellType = JPVideoPlayerUnreachableCellTypeNone;
}
}
else{
cell.jp_unreachableCellType = JPVideoPlayerUnreachableCellTypeNone;
}
}
- (void)playVideoInVisibleCellsIfNeed {
if (!self.scrollView || ![self.scrollView isKindOfClass:[UITableView class]] && ![self.scrollView isKindOfClass:[UICollectionView class]]) return;
if(self.playingVideoCell){
[self playVideoWithCell:self.playingVideoCell];
return;
}
// handle the first cell cannot play video when initialized.
[self handleCellUnreachableTypeInVisibleCellsAfterReloadData];
NSArray<UITableViewCell *> *visibleCells = [(UITableView *)self.scrollView visibleCells];
// Find first cell need play video in visible cells.
UIView<JPVideoPlayerCellProtocol> *targetCell = nil;
if(self.playVideoInVisibleCellsBlock){
targetCell = self.playVideoInVisibleCellsBlock(visibleCells);
}
else {
for (UITableViewCell *cell in visibleCells) {
if (cell.jp_videoURL.absoluteString.length > 0) {
targetCell = cell;
break;
}
}
}
// Play if found.
if (targetCell) {
[self playVideoWithCell:targetCell];
}
}
- (void)stopPlayIfNeed {
[self.playingVideoCell.jp_videoPlayView jp_stopPlay];
self.playingVideoCell = nil;
}
- (void)scrollViewDidScroll {
[self handleQuickScrollIfNeed];
}
- (void)scrollViewDidEndDraggingWillDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self handleScrollStopIfNeed];
}
}
- (void)scrollViewDidEndDecelerating {
[self handleScrollStopIfNeed];
}
- (BOOL)viewIsVisibleInVisibleFrameAtScrollViewDidScroll:(UIView *)view {
return [self viewIsVisibleInTableViewVisibleFrame:view];
}
- (void)setDebugScrollViewVisibleFrame:(BOOL)debugScrollViewVisibleFrame {
_debugScrollViewVisibleFrame = debugScrollViewVisibleFrame;
[self displayScrollViewVisibleFrame:debugScrollViewVisibleFrame];
}
- (void)setScrollViewVisibleFrame:(CGRect)scrollViewVisibleFrame {
_scrollViewVisibleFrame = scrollViewVisibleFrame;
[self displayScrollViewVisibleFrame:self.debugScrollViewVisibleFrame];
}
#pragma mark - Private
- (void)displayScrollViewVisibleFrame:(BOOL)display {
if (CGRectEqualToRect(self.scrollViewVisibleFrame, CGRectZero)) return;
if (self.debugScrollViewVisibleFrameLayer) {
[self.debugScrollViewVisibleFrameLayer removeFromSuperlayer];
}
if (!display) return;
self.debugScrollViewVisibleFrameLayer = ({
CAShapeLayer *layer = [CAShapeLayer new];
CGRect rect = self.scrollViewVisibleFrame;
layer.frame = rect;
rect.origin.y = 0.f;
rect.origin.x += 3.f;
rect.size.width -= 6.f;
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:rect];
[bezierPath moveToPoint:CGPointMake(rect.origin.x, CGRectGetMaxY(rect) * 0.5f)];
[bezierPath addLineToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect) * 0.5f)];
layer.path = bezierPath.CGPath;
layer.lineWidth = 1.f;
layer.strokeColor = [UIColor redColor].CGColor;
layer.fillColor = [UIColor clearColor].CGColor;
[self.scrollView.superview.layer addSublayer:layer];
layer;
});
}
- (BOOL)playingCellIsVisible {
if(CGRectIsEmpty(self.scrollViewVisibleFrame)){
return NO;
}
if(!self.playingVideoCell){
return NO;
}
UIView *strategyView = self.scrollPlayStrategyType == JPScrollPlayStrategyTypeBestCell ? self.playingVideoCell : self.playingVideoCell.jp_videoPlayView;
if(!strategyView){
return NO;
}
return [self viewIsVisibleInTableViewVisibleFrame:strategyView];
}
- (BOOL)viewIsVisibleInTableViewVisibleFrame:(UIView *)view {
CGRect referenceRect = [self.scrollView.superview convertRect:self.scrollViewVisibleFrame toView:nil];
CGPoint viewLeftTopPoint = view.frame.origin;
viewLeftTopPoint.y += 1;
CGPoint topCoordinatePoint = [view.superview convertPoint:viewLeftTopPoint toView:nil];
BOOL isTopContain = CGRectContainsPoint(referenceRect, topCoordinatePoint);
CGFloat viewBottomY = viewLeftTopPoint.y + view.bounds.size.height;
viewBottomY -= 2;
CGPoint viewLeftBottomPoint = CGPointMake(viewLeftTopPoint.x, viewBottomY);
CGPoint bottomCoordinatePoint = [view.superview convertPoint:viewLeftBottomPoint toView:nil];
BOOL isBottomContain = CGRectContainsPoint(referenceRect, bottomCoordinatePoint);
return !(!isTopContain && !isBottomContain);
}
- (UIView<JPVideoPlayerCellProtocol> *)findBestCellForPlayingVideo {
if (!self.scrollView || ![self.scrollView isKindOfClass:[UITableView class]] && ![self.scrollView isKindOfClass:[UICollectionView class]]) return nil;
if(CGRectIsEmpty(self.scrollViewVisibleFrame)) return nil;
// To find next cell need play video.
UITableViewCell *targetCell = nil;
UITableView *tableView = (UITableView *)self.scrollView;
NSArray<UITableViewCell *> *visibleCells = [tableView visibleCells];
if(self.findBestCellInVisibleCellsBlock){
return self.findBestCellInVisibleCellsBlock(visibleCells);
}
CGFloat gap = MAXFLOAT;
CGRect referenceRect = [tableView.superview convertRect:self.scrollViewVisibleFrame toView:nil];
for (UITableViewCell *cell in visibleCells) {
if (!(cell.jp_videoURL.absoluteString.length > 0)) {
continue;
}
// If need to play video.
// Find the cell cannot stop in screen center first.
UIView *strategyView = self.scrollPlayStrategyType == JPScrollPlayStrategyTypeBestCell ? cell : cell.jp_videoPlayView;
if(!strategyView){
continue;
}
if (cell.jp_unreachableCellType != JPVideoPlayerUnreachableCellTypeNone) {
// Must the all area of the cell is visible.
if (cell.jp_unreachableCellType == JPVideoPlayerUnreachableCellTypeTop) {
CGPoint strategyViewLeftUpPoint = strategyView.frame.origin;
strategyViewLeftUpPoint.y += 2;
CGPoint coordinatePoint = [strategyView.superview convertPoint:strategyViewLeftUpPoint toView:nil];
if (CGRectContainsPoint(referenceRect, coordinatePoint)){
targetCell = cell;
break;
}
}
else if (cell.jp_unreachableCellType == JPVideoPlayerUnreachableCellTypeDown){
CGPoint strategyViewLeftUpPoint = strategyView.frame.origin;
CGFloat strategyViewDownY = strategyViewLeftUpPoint.y + strategyView.bounds.size.height;
CGPoint strategyViewLeftDownPoint = CGPointMake(strategyViewLeftUpPoint.x, strategyViewDownY);
strategyViewLeftDownPoint.y -= 1;
CGPoint coordinatePoint = [strategyView.superview convertPoint:strategyViewLeftDownPoint toView:nil];
if (CGRectContainsPoint(referenceRect, coordinatePoint)){
targetCell = cell;
break;
}
}
}
else{
CGPoint coordinateCenterPoint = [strategyView.superview convertPoint:strategyView.center toView:nil];
CGFloat delta = fabs(coordinateCenterPoint.y - referenceRect.size.height * 0.5 - referenceRect.origin.y);
if (delta < gap) {
gap = delta;
targetCell = cell;
}
}
}
return targetCell;
}
- (NSUInteger)fetchUnreachableCellCountWithVisibleCellsCount:(NSUInteger)visibleCellsCount {
return [self.unreachableCellDictionary[[NSString stringWithFormat:@"%d", (int)visibleCellsCount]] intValue];
}
- (NSDictionary<NSString *, NSString *> *)unreachableCellDictionary {
if(!_unreachableCellDictionary){
// The key is the number of visible cells in screen,
// the value is the number of cells cannot stop in screen center.
_unreachableCellDictionary = @{
@"4" : @1,
@"3" : @1,
@"2" : @0
};
}
return _unreachableCellDictionary;
}
- (void)playVideoWithCell:(UIView<JPVideoPlayerCellProtocol> *)cell {
if(!cell){
return;
}
self.playingVideoCell = cell;
if (self.delegate && [self.delegate respondsToSelector:@selector(scrollView:willPlayVideoOnCell:)]) {
[self.delegate scrollView:self.scrollView willPlayVideoOnCell:cell];
}
}
- (void)handleQuickScrollIfNeed {
if (!self.playingVideoCell) {
return;
}
// Stop play when the cell playing video is un-visible.
if (![self playingCellIsVisible]) {
[self stopPlayIfNeed];
}
}
- (void)handleScrollStopIfNeed {
UITableViewCell *bestCell = [self findBestCellForPlayingVideo];
if(!bestCell){
return;
}
// If the found cell is the cell playing video, this situation cannot play video again.
if([bestCell jp_isEqualToCell:self.playingVideoCell]){
return;
}
[self.playingVideoCell.jp_videoPlayView jp_stopPlay];
[self playVideoWithCell:bestCell];
}
@end
static NSString * const JPMigrationLastSDKVersionKey = @"com.jpvideoplayer.last.migration.version.www";
@implementation JPMigration
+ (void)migrateToSDKVersion:(NSString *)version
block:(dispatch_block_t)migrationBlock {
// version > lastMigrationVersion
if ([version compare:[self lastMigrationVersion] options:NSNumericSearch] == NSOrderedDescending) {
migrationBlock();
JPDebugLog(@"JPMigration: Running migration for version %@", version);
[self setLastMigrationVersion:version];
}
}
+ (NSString *)lastMigrationVersion {
NSString *res = [[NSUserDefaults standardUserDefaults] valueForKey:JPMigrationLastSDKVersionKey];
return (res ? res : @"");
}
+ (void)setLastMigrationVersion:(NSString *)version {
[[NSUserDefaults standardUserDefaults] setValue:version forKey:JPMigrationLastSDKVersionKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerScrollViewProtocol.h"
@interface UICollectionView (WebVideoCache) <JPVideoPlayerScrollViewProtocol>
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "UICollectionView+WebVideoCache.h"
@implementation UICollectionView (WebVideoCache)
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerCellProtocol.h"
@interface UICollectionViewCell (WebVideoCache) <JPVideoPlayerCellProtocol>
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "UICollectionViewCell+WebVideoCache.h"
#import <objc/runtime.h>
@implementation UICollectionViewCell (WebVideoCache)
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerScrollViewProtocol.h"
@interface UITableView (WebVideoCache) <JPVideoPlayerScrollViewProtocol>
@end
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "UITableView+WebVideoCache.h"
@implementation UITableView (WebVideoCache)
@end
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerCellProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@interface UITableViewCell (WebVideoCache) <JPVideoPlayerCellProtocol>
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "UITableViewCell+WebVideoCache.h"
@implementation UITableViewCell (WebVideoCache)
@end
\ No newline at end of file
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "JPVideoPlayerManager.h"
#import "JPVideoPlayerSupportUtils.h"
#import "JPVideoPlayerProtocol.h"
#import "JPVideoPlayerCompat.h"
#import "JPVideoPlayerControlViews.h"
NS_ASSUME_NONNULL_BEGIN
@protocol JPVideoPlayerDelegate <NSObject>
@optional
/**
* Controls which video should be downloaded when the video is not found in the cache.
*
* @param videoURL the url of the video to be download.
*
* @return Return NO to prevent the downloading of the video on cache misses. If not implemented, YES is implied.
*/
- (BOOL)shouldDownloadVideoForURL:(nonnull NSURL *)videoURL;
/**
* Controls which video should automatic replay when the video is play completed.
*
* @param videoURL the url of the video to be play.
*
* @return Return NO to prevent replay for the video. If not implemented, YES is implied.
*/
- (BOOL)shouldAutoReplayForURL:(nonnull NSURL *)videoURL;
/**
* Controls the background color of the video layer before player really start play video.
* by default it is NO, means that the color of the layer is `clearColor`.
*
* @return Return YES to make the background color of the video layer be `blackColor`.
*/
- (BOOL)shouldShowBlackBackgroundBeforePlaybackStart;
/**
* Controls the background color of the video layer when player start play video.
* by default it is YES, means that the color of the layer is `blackColor` when start playing.
*
* @return Return NO to make the background color of the video layer be `clearColor`.
*/
- (BOOL)shouldShowBlackBackgroundWhenPlaybackStart;
/**
* Controls the auto hiding of JPVideoPlayerView`s controlContainerView .
* by default it is YES, means that JPVideoPlayerView`s auto hide controlContainerView after a few seconds and show it
* again when user tapping the video playing view.
*
* @return Return NO to make the JPVideoPlayerView`s show controlContainerView all the time.
*
* @warning The `userInteractionEnabled` need be set YES;
*/
- (BOOL)shouldAutoHideControlContainerViewWhenUserTapping;
/**
* Controls the Behavior of adding default ControlView / BufferingIndicator / ProgressView when give nil to params.
* By default it is YES, which means default views will be added when params given nil.
*
* @return Return NO to don`t display any view when params are given nil.
*/
- (BOOL)shouldShowDefaultControlAndIndicatorViews;
/**
* Notify the player status.
*
* @param playerStatus The current playing status.
*/
- (void)playerStatusDidChanged:(JPVideoPlayerStatus)playerStatus;
/**
* Called when application will resign active.
*
* @param videoURL The url of the video to be play.
*/
- (BOOL)shouldPausePlaybackWhenApplicationWillResignActiveForURL:(NSURL *)videoURL;
/**
* Called when application did enter background.
*
* @param videoURL The url of the video to be play.
*/
- (BOOL)shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:(NSURL *)videoURL;
/**
* Called only when application become active from `Control Center`,
* `Notification Center`, `pop UIAlert`, `double click Home-Button`.
*
* @param videoURL The url of the video to be play.
*/
- (BOOL)shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:(NSURL *)videoURL;
/**
* Called only when application become active from `Share to other application`,
* `Enter background`, `Lock screen`.
*
* @param videoURL The url of the video to be play.
*/
- (BOOL)shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:(NSURL *)videoURL;
/**
* Called when call resume play but can not resume play.
*
* @param videoURL The url of the video to be play.
*/
- (BOOL)shouldTranslateIntoPlayVideoFromResumePlayForURL:(NSURL *)videoURL;
/**
* Called when receive audio session interruption notification.
*
* @param videoURL The url of the video to be play.
*/
- (BOOL)shouldPausePlaybackWhenReceiveAudioSessionInterruptionNotificationForURL:(NSURL *)videoURL;
/**
* Called when play video failed.
*
* @param error The reason of why play video failed.
* @param videoURL The url of the video to be play.
*/
- (void)playVideoFailWithError:(NSError *)error
videoURL:(NSURL *)videoURL;
/**
* Provide custom audio session category to play video, `AVAudioSessionCategoryPlayback` by default.
*
* @return The prefer audio session category.
*/
- (AVAudioSessionCategory)preferAudioSessionCategory;
/**
* Called when play a already played video, `NO` by default, return `YES` to enable resume playback from a playback record.
*
* @param videoURL The url of the video to be play.
* @param elapsedSeconds The elapsed seconds last playback recorded.
*/
- (BOOL)shouldResumePlaybackFromPlaybackRecordForURL:(NSURL *)videoURL
elapsedSeconds:(NSTimeInterval)elapsedSeconds;
@end
@interface UIView (WebVideoCache)<JPVideoPlayerManagerDelegate>
#pragma mark - Property
@property (nonatomic, nullable) id<JPVideoPlayerDelegate> jp_videoPlayerDelegate;
@property (nonatomic, readonly) JPVideoPlayViewInterfaceOrientation jp_viewInterfaceOrientation;
@property (nonatomic, readonly) JPVideoPlayerStatus jp_playerStatus;
@property(nonatomic, strong, readonly, nullable) JPVideoPlayerView *jp_videoPlayerView;
@property (nonatomic, readonly, nullable) UIView<JPVideoPlayerProtocol> *jp_progressView;
@property (nonatomic, readonly, nullable) UIView<JPVideoPlayerProtocol> *jp_controlView;
@property (nonatomic, readonly, nullable) UIView<JPVideoPlayerBufferingProtocol> *jp_bufferingIndicator;
@property(nonatomic, copy, readonly, nullable) NSURL *jp_videoURL;
#pragma mark - Play Video Methods
/**
* Play a local or web video for given url with no progressView, no controlView, no bufferingIndicator, and play audio at the same time.
*
* The download is asynchronous and cached.
*
* @param url The url for the video.
*/
- (void)jp_playVideoWithURL:(NSURL *)url;
/**
* Play a local or web video for given url with bufferingIndicator and progressView, and the player is muted.
*
* The download is asynchronous and cached.
*
* @param url The url for the video.
* @param bufferingIndicator The view show buffering animation when player buffering, should compliance with the `JPVideoPlayerBufferingProtocol`,
* it will display default bufferingIndicator if pass nil in. @see `JPVideoPlayerBufferingIndicator`.
* @param progressView The view to display the download and play progress, should compliance with the `JPVideoPlayerProgressProtocol`,
* it will display default progressView if pass nil, @see `JPVideoPlayerProgressView`.
* @param configuration The block will be call when video player complete the configuration. because initialize player is not synchronize,
* so other category method is disabled before complete the configuration.
*/
- (void)jp_playVideoMuteWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration;
/**
* Resume play for given url with bufferingIndicator and progressView, and the player is muted.
* `Resume` mean that user is playing video in tableView, when user tap the cell of playing video,
* user open a detail video viewController that play the same video, but we do not wanna user play the same video from the beginning,
* so we use `resume` method to get this goal.
*
* The download is asynchronous and cached.
*
* @param url The url for the video.
* @param bufferingIndicator The view show buffering animation when player buffering, should compliance with the `JPVideoPlayerBufferingProtocol`,
* it will display default bufferingIndicator if pass nil in. @see `JPVideoPlayerBufferingIndicator`.
* @param progressView The view to display the download and play progress, should compliance with the `JPVideoPlayerProgressProtocol`,
* it will display default progressView if pass nil, @see `JPVideoPlayerProgressView`.
* @param configuration The block will be call when video player complete the configuration. because initialize player is not synchronize,
* so other category method is disabled before complete the configuration.
*/
- (void)jp_resumeMutePlayWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration;
/**
* Play a local or web video for given url with bufferingIndicator, controlView, progressView, and play audio at the same time.
*
* The download is asynchronous and cached.
*
* The control view will display, and display indicator view when buffer empty.
*
* @param url The url for the video.
* @param bufferingIndicator The view show buffering animation when player buffering, should compliance with the `JPVideoPlayerBufferingProtocol`,
* it will display default bufferingIndicator if pass nil in. @see `JPVideoPlayerBufferingIndicator`.
* @param controlView The view to display the download and play progress, should compliance with the `JPVideoPlayerProgressProtocol`,
* it will display default controlView if pass nil, @see `JPVideoPlayerControlView`.
* @param progressView The view to display the download and play progress, should compliance with the `JPVideoPlayerProgressProtocol`,
* it will display default progressView if pass nil, @see `JPVideoPlayerProgressView`.
* @param configuration The block will be call when video player complete the configuration. because initialize player is not synchronize,
* so other category method is disabled before complete the configuration.
*/
- (void)jp_playVideoWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
controlView:(UIView <JPVideoPlayerProtocol> *_Nullable)controlView
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration;
/**
* Resume play for given url with bufferingIndicator, controlView, progressView, and play audio at the same time.
* `Resume` mean that user is playing video in tableView, when user tap the cell of playing video,
* user open a detail video viewController that play the same video, but we do not wanna user play the same video from the beginning,
* so we use `resume` method to get this goal.
*
* The download is asynchronous and cached.
*
* The control view will display, and display indicator view when buffering empty.
*
* @param url The url for the video.
* @param bufferingIndicator The view show buffering animation when player buffering, should compliance with the `JPVideoPlayerBufferingProtocol`,
* it will display default bufferingIndicator if pass nil in. @see `JPVideoPlayerBufferingIndicator`.
* @param controlView The view to display the download and play progress, should compliance with the `JPVideoPlayerProgressProtocol`,
* it will display default controlView if pass nil, @see `JPVideoPlayerControlView`.
* @param progressView The view to display the download and play progress, should compliance with the `JPVideoPlayerProgressProtocol`,
* it will display default progressView if pass nil, @see `JPVideoPlayerProgressView`.
* @param configuration The block will be call when video player complete the configuration. because initialize player is not synchronize,
* so other category method is disabled before complete the configuration.
*/
- (void)jp_resumePlayWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
controlView:(UIView <JPVideoPlayerProtocol> *_Nullable)controlView
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration;
/**
* Play a local or web video with given url.
*
* The download is asynchronous and cached.
*
* @param url The url for the video.
* @param options The options to use when downloading the video. @see JPVideoPlayerOptions for the possible values.
* @param configuration The block will be call when video player complete the configuration. because initialize player is not synchronize,
* so other category method is disabled before complete the configuration.
*/
- (void)jp_playVideoWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
configuration:(JPPlayVideoConfiguration _Nullable)configuration;
/**
* Resume play with given url.
* `Resume` mean that user is playing video in tableView, when user tap the cell of playing video,
* user open a detail video viewController that play the same video, but we do not wanna user play the same video from the beginning,
* so we use `resume` method to get this goal.
*
* The download is asynchronous and cached.
*
* @param url The url for the video.
* @param options The options to use when downloading the video. @see JPVideoPlayerOptions for the possible values.
* @param configuration The block will be call when video player complete the configuration. because initialize player is not synchronize,
* so other category method is disabled before complete the configuration.
*/
- (void)jp_resumePlayWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
configuration:(JPPlayVideoConfiguration _Nullable)configuration;
#pragma mark - Playback Control
/**
* The current playback rate.
*/
@property (nonatomic) float jp_rate;
/**
* A Boolean value that indicates whether the audio output of the player is muted.
*/
@property (nonatomic) BOOL jp_muted;
/**
* The audio playback volume for the player, ranging from 0.0 through 1.0 on a linear scale.
*/
@property (nonatomic) float jp_volume;
/**
* Moves the playback cursor.
*
* @param time The time where seek to.
*/
- (BOOL)jp_seekToTime:(CMTime)time;
/**
* Fetch the elapsed seconds of player.
*/
- (NSTimeInterval)jp_elapsedSeconds;
/**
* Fetch the total seconds of player.
*/
- (NSTimeInterval)jp_totalSeconds;
/**
* Call this method to pause playback.
*/
- (void)jp_pause;
/**
* Call this method to resume playback.
*/
- (void)jp_resume;
/**
* @return Returns the current time of the current player item.
*/
- (CMTime)jp_currentTime;
/**
* Call this method to stop play video.
*/
- (void)jp_stopPlay;
#pragma mark - Landscape Or Portrait Control
/**
* Call this method to enter full screen.
*/
- (void)jp_gotoLandscape;
/**
* Call this method to enter full screen.
*
* @param flag Need landscape animation or not.
* @param completion Call back when landscape finished.
*/
- (void)jp_gotoLandscapeAnimated:(BOOL)flag
completion:(dispatch_block_t _Nullable)completion;
/**
* Call this method to exit full screen.
*/
- (void)jp_gotoPortrait;
/**
* Call this method to exit full screen.
*
* @param flag Need portrait animation or not.
* @param completion Call back when portrait finished.
*/
- (void)jp_gotoPortraitAnimated:(BOOL)flag
completion:(dispatch_block_t _Nullable)completion;
@end
NS_ASSUME_NONNULL_END
/*
* This file is part of the JPVideoPlayer package.
* (c) NewPan <13246884282@163.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Click https://github.com/newyjp
* or http://www.jianshu.com/users/e2f2d779c022/latest_articles to contact me.
*/
#import "UIView+WebVideoCache.h"
#import <objc/runtime.h>
#import "JPVideoPlayer.h"
#import "JPVideoPlayerSupportUtils.h"
#import "JPVideoPlayerControlViews.h"
@interface JPVideoPlayerHelper : NSObject
@property(nonatomic, strong) JPVideoPlayerView *videoPlayerView;
@property(nonatomic, strong) UIView<JPVideoPlayerProtocol> *progressView;
@property(nonatomic, strong) UIView<JPVideoPlayerProtocol> *controlView;
@property(nonatomic, strong) UIView<JPVideoPlayerBufferingProtocol> *bufferingIndicator;
@property(nonatomic, weak) id<JPVideoPlayerDelegate> videoPlayerDelegate;
@property(nonatomic, assign) JPVideoPlayViewInterfaceOrientation viewInterfaceOrientation;
@property(nonatomic, assign)JPVideoPlayerStatus playerStatus;
@property (nonatomic, weak) UIView *playVideoView;
@property(nonatomic, copy) NSURL *videoURL;
@end
@implementation JPVideoPlayerHelper
- (instancetype)initWithPlayVideoView:(UIView *)playVideoView {
self = [super init];
if(self){
_playVideoView = playVideoView;
}
return self;
}
- (JPVideoPlayViewInterfaceOrientation)viewInterfaceOrientation {
if(_viewInterfaceOrientation == JPVideoPlayViewInterfaceOrientationUnknown){
CGSize referenceSize = self.playVideoView.window.bounds.size;
_viewInterfaceOrientation = referenceSize.width < referenceSize.height ? JPVideoPlayViewInterfaceOrientationPortrait :
JPVideoPlayViewInterfaceOrientationLandscape;
}
return _viewInterfaceOrientation;
}
- (JPVideoPlayerView *)videoPlayerView {
if(!_videoPlayerView){
BOOL autoHide = YES;
if (_playVideoView.jp_videoPlayerDelegate && [_playVideoView.jp_videoPlayerDelegate respondsToSelector:@selector(shouldAutoHideControlContainerViewWhenUserTapping)]) {
autoHide = [_playVideoView.jp_videoPlayerDelegate shouldAutoHideControlContainerViewWhenUserTapping];
}
_videoPlayerView = [[JPVideoPlayerView alloc] initWithNeedAutoHideControlViewWhenUserTapping:autoHide];
}
return _videoPlayerView;
}
@end
@interface UIView()
@property(nonatomic, readonly)JPVideoPlayerHelper *helper;
@end
@implementation UIView (WebVideoCache)
#pragma mark - Properties
- (JPVideoPlayViewInterfaceOrientation)jp_viewInterfaceOrientation {
return self.helper.viewInterfaceOrientation;
}
- (JPVideoPlayerStatus)jp_playerStatus {
return self.helper.playerStatus;
}
- (JPVideoPlayerView *)jp_videoPlayerView {
return self.helper.videoPlayerView;
}
- (void)setJp_progressView:(UIView <JPVideoPlayerProtocol> *)jp_progressView {
self.helper.progressView = jp_progressView;
}
- (UIView <JPVideoPlayerProtocol> *)jp_progressView {
return self.helper.progressView;
}
- (void)setJp_controlView:(UIView <JPVideoPlayerProtocol> *)jp_controlView {
self.helper.controlView = jp_controlView;
}
- (UIView <JPVideoPlayerProtocol> *)jp_controlView {
return self.helper.controlView;
}
- (void)setJp_bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *)jp_bufferingIndicator {
self.helper.bufferingIndicator = jp_bufferingIndicator;
}
- (UIView <JPVideoPlayerBufferingProtocol> *)jp_bufferingIndicator {
return self.helper.bufferingIndicator;
}
- (void)setJp_videoPlayerDelegate:(id <JPVideoPlayerDelegate>)jp_videoPlayerDelegate {
self.helper.videoPlayerDelegate = jp_videoPlayerDelegate;
}
- (id <JPVideoPlayerDelegate>)jp_videoPlayerDelegate {
return self.helper.videoPlayerDelegate;
}
- (NSURL *)jp_videoURL {
return self.helper.videoURL;
}
- (void)setJp_videoURL:(NSURL *)jp_videoURL {
self.helper.videoURL = jp_videoURL.copy;
}
#pragma mark - Play Video Methods
- (void)jp_playVideoWithURL:(NSURL *)url {
[self jp_playVideoWithURL:url
options:JPVideoPlayerContinueInBackground |
JPVideoPlayerLayerVideoGravityResizeAspect
configuration:nil];
}
- (void)jp_playVideoMuteWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration {
[self setBufferingIndicator:bufferingIndicator
controlView:nil
progressView:progressView
needSetControlView:NO];
[self jp_stopPlay];
[self jp_playVideoWithURL:url
options:JPVideoPlayerContinueInBackground |
JPVideoPlayerLayerVideoGravityResizeAspect |
JPVideoPlayerMutedPlay
configuration:configuration];
}
- (void)jp_resumeMutePlayWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration {
[self setBufferingIndicator:bufferingIndicator
controlView:nil
progressView:progressView
needSetControlView:NO];
[self jp_resumePlayWithURL:url
options:JPVideoPlayerContinueInBackground |
JPVideoPlayerLayerVideoGravityResizeAspect |
JPVideoPlayerMutedPlay
configuration:configuration];
}
- (void)jp_playVideoWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
controlView:(UIView <JPVideoPlayerProtocol> *_Nullable)controlView
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration {
[self setBufferingIndicator:bufferingIndicator
controlView:controlView
progressView:progressView
needSetControlView:YES];
[self jp_stopPlay];
[self jp_playVideoWithURL:url
options:JPVideoPlayerContinueInBackground |
JPVideoPlayerLayerVideoGravityResizeAspect
configuration:configuration];
}
- (void)jp_resumePlayWithURL:(NSURL *)url
bufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
controlView:(UIView <JPVideoPlayerProtocol> *_Nullable)controlView
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
configuration:(JPPlayVideoConfiguration _Nullable)configuration {
[self setBufferingIndicator:bufferingIndicator
controlView:controlView
progressView:progressView
needSetControlView:YES];
[self jp_resumePlayWithURL:url
options:JPVideoPlayerContinueInBackground |
JPVideoPlayerLayerVideoGravityResizeAspect
configuration:configuration];
}
- (void)setBufferingIndicator:(UIView <JPVideoPlayerBufferingProtocol> *_Nullable)bufferingIndicator
controlView:(UIView <JPVideoPlayerProtocol> *_Nullable)controlView
progressView:(UIView <JPVideoPlayerProtocol> *_Nullable)progressView
needSetControlView:(BOOL)needSetControlView {
// should show default.
BOOL showDefaultView = YES;
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldShowDefaultControlAndIndicatorViews)]) {
showDefaultView = [self.jp_videoPlayerDelegate shouldShowDefaultControlAndIndicatorViews];
}
// user update progressView.
if(progressView && self.jp_progressView){
[self.jp_progressView removeFromSuperview];
}
if(showDefaultView && !progressView && !self.jp_progressView){
// Use default `JPVideoPlayerProgressView` if no progressView.
progressView = [JPVideoPlayerProgressView new];
}
if(progressView){
self.jp_progressView = progressView;
}
// user update bufferingIndicator.
if(bufferingIndicator && self.jp_bufferingIndicator){
[self.jp_bufferingIndicator removeFromSuperview];
}
if(showDefaultView && !bufferingIndicator && !self.jp_bufferingIndicator){
// Use default `JPVideoPlayerBufferingIndicator` if no bufferingIndicator.
bufferingIndicator = [JPVideoPlayerBufferingIndicator new];
}
if(bufferingIndicator){
self.jp_bufferingIndicator = bufferingIndicator;
}
if(needSetControlView){
//before setting controllerView userInteractionEnabled should be enabled.
self.userInteractionEnabled = YES;
// user update controlView.
if(controlView && self.jp_controlView){
[self.jp_controlView removeFromSuperview];
}
if(showDefaultView && !controlView && !self.jp_controlView){
// Use default `JPVideoPlayerControlView` if no controlView.
controlView = [[JPVideoPlayerControlView alloc] initWithControlBar:nil blurImage:nil];
}
if(controlView){
self.jp_controlView = controlView;
}
}
}
- (void)jp_playVideoWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
configuration:(JPPlayVideoConfiguration _Nullable)configuration {
[self playVideoWithURL:url
options:options
configuration:configuration
isResume:NO];
}
- (void)jp_resumePlayWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
configuration:(JPPlayVideoConfiguration _Nullable)configuration {
[self playVideoWithURL:url
options:options
configuration:configuration
isResume:YES];
}
- (void)playVideoWithURL:(NSURL *)url
options:(JPVideoPlayerOptions)options
configuration:(JPPlayVideoConfiguration _Nullable)configuration
isResume:(BOOL)isResume {
JPAssertMainThread;
self.jp_videoURL = url;
if (url) {
[JPVideoPlayerManager sharedManager].delegate = self;
self.helper.viewInterfaceOrientation = JPVideoPlayViewInterfaceOrientationPortrait;
/// handler the reuse of progressView in `UITableView`.
{
if(self.jp_progressView && [self.jp_progressView respondsToSelector:@selector(viewWillPrepareToReuse)]){
[self.jp_progressView viewWillPrepareToReuse];
}
if(self.jp_controlView && [self.jp_controlView respondsToSelector:@selector(viewWillPrepareToReuse)]){
[self.jp_controlView viewWillPrepareToReuse];
}
[self invokeFinishBufferingDelegateMethod];
}
/// buffering indicator.
{
if(self.jp_bufferingIndicator && !self.jp_bufferingIndicator.superview){
self.jp_bufferingIndicator.frame = self.bounds;
[self.helper.videoPlayerView.bufferingIndicatorContainerView addSubview:self.jp_bufferingIndicator];
}
if(self.jp_bufferingIndicator){
[self invokeFinishBufferingDelegateMethod];
}
}
/// progress view.
{
if(self.jp_progressView && !self.jp_progressView.superview){
self.jp_progressView.frame = self.bounds;
if(self.jp_progressView && [self.jp_progressView respondsToSelector:@selector(viewWillAddToSuperView:)]){
[self.jp_progressView viewWillAddToSuperView:self];
}
[self.helper.videoPlayerView.progressContainerView addSubview:self.jp_progressView];
}
}
/// control view.
{
if(self.jp_controlView && !self.jp_controlView.superview){
self.jp_controlView.frame = self.bounds;
if(self.jp_controlView && [self.jp_controlView respondsToSelector:@selector(viewWillAddToSuperView:)]){
[self.jp_controlView viewWillAddToSuperView:self];
}
[self.helper.videoPlayerView.controlContainerView addSubview:self.jp_controlView];
self.helper.videoPlayerView.progressContainerView.alpha = 0;
}
}
/// video player view.
{
self.helper.videoPlayerView.hidden = NO;
if(!self.helper.videoPlayerView.superview){
[self addSubview:self.helper.videoPlayerView];
}
self.helper.videoPlayerView.frame = self.bounds;
self.helper.videoPlayerView.backgroundColor = [UIColor clearColor];
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldShowBlackBackgroundBeforePlaybackStart)]) {
BOOL shouldShow = [self.jp_videoPlayerDelegate shouldShowBlackBackgroundBeforePlaybackStart];
if(shouldShow){
self.helper.videoPlayerView.backgroundColor = [UIColor clearColor];
}
}
}
/// nobody retain this block.
JPVideoPlayerConfiguration _configuration = ^(JPVideoPlayerModel *model){
if (!model) JPDebugLog(@"model can not be nil");
if(configuration) configuration(self, model);
};
if(!isResume){
[[JPVideoPlayerManager sharedManager] playVideoWithURL:url
showOnLayer:self.helper.videoPlayerView.videoContainerLayer
options:options
configuration:_configuration];
[self callOrientationDelegateWithInterfaceOrientation:self.jp_viewInterfaceOrientation];
}
else {
[[JPVideoPlayerManager sharedManager] resumePlayWithURL:url
showOnLayer:self.helper.videoPlayerView.videoContainerLayer
options:options
configuration:_configuration];
}
}
else {
JPDispatchSyncOnMainQueue(^{
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(playVideoFailWithError:videoURL:)]) {
[self.jp_videoPlayerDelegate playVideoFailWithError:JPErrorWithDescription(@"Try to play video with a invalid url")
videoURL:url];
}
});
}
}
#pragma mark - Playback Control
- (void)setJp_rate:(float)jp_rate {
JPVideoPlayerManager.sharedManager.rate = jp_rate;
}
- (float)jp_rate {
return JPVideoPlayerManager.sharedManager.rate;
}
- (void)setJp_muted:(BOOL)jp_muted {
JPVideoPlayerManager.sharedManager.muted = jp_muted;
}
- (BOOL)jp_muted {
return JPVideoPlayerManager.sharedManager.muted;
}
- (void)setJp_volume:(float)jp_volume {
JPVideoPlayerManager.sharedManager.volume = jp_volume;
}
- (float)jp_volume {
return JPVideoPlayerManager.sharedManager.volume;
}
- (BOOL)jp_seekToTime:(CMTime)time {
return [[JPVideoPlayerManager sharedManager] seekToTime:time];
}
- (NSTimeInterval)jp_elapsedSeconds {
return [JPVideoPlayerManager.sharedManager elapsedSeconds];
}
- (NSTimeInterval)jp_totalSeconds {
return [JPVideoPlayerManager.sharedManager totalSeconds];
}
- (void)jp_pause {
[[JPVideoPlayerManager sharedManager] pause];
}
- (void)jp_resume {
[[JPVideoPlayerManager sharedManager] resume];
}
- (CMTime)jp_currentTime {
return JPVideoPlayerManager.sharedManager.currentTime;
}
- (void)jp_stopPlay {
[[JPVideoPlayerManager sharedManager] stopPlay];
self.helper.videoPlayerView.hidden = YES;
self.helper.videoPlayerView.backgroundColor = [UIColor clearColor];
[self invokeFinishBufferingDelegateMethod];
}
#pragma mark - Landscape & Portrait Control
- (void)jp_gotoLandscape {
[self jp_gotoLandscapeAnimated:YES
completion:nil];
}
- (void)jp_gotoLandscapeAnimated:(BOOL)flag
completion:(dispatch_block_t)completion {
if (self.jp_viewInterfaceOrientation != JPVideoPlayViewInterfaceOrientationPortrait) {
return;
}
self.helper.viewInterfaceOrientation = JPVideoPlayViewInterfaceOrientationLandscape;
JPVideoPlayerView *videoPlayerView = self.helper.videoPlayerView;
videoPlayerView.backgroundColor = [UIColor whiteColor];
CGRect videoPlayerViewFrameInWindow = [self convertRect:videoPlayerView.frame toView:nil];
[videoPlayerView removeFromSuperview];
[[UIApplication sharedApplication].keyWindow addSubview:videoPlayerView];
videoPlayerView.frame = videoPlayerViewFrameInWindow;
videoPlayerView.controlContainerView.alpha = 0;
if (flag) {
[UIView animateWithDuration:0.35
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
[self executeLandscape];
}
completion:^(BOOL finished) {
if (completion) {
completion();
}
[UIView animateWithDuration:0.5 animations:^{
videoPlayerView.controlContainerView.alpha = 1;
}];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// hide status bar.
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
#pragma clang diagnostic pop
}];
}
else{
[self executeLandscape];
if (completion) {
completion();
}
[UIView animateWithDuration:0.5 animations:^{
videoPlayerView.controlContainerView.alpha = 0;
}];
}
[self refreshStatusBarOrientation:UIInterfaceOrientationLandscapeRight];
[self callOrientationDelegateWithInterfaceOrientation:JPVideoPlayViewInterfaceOrientationLandscape];
}
- (void)jp_gotoPortrait {
[self jp_gotoPortraitAnimated:YES
completion:nil];
}
- (void)jp_gotoPortraitAnimated:(BOOL)flag
completion:(dispatch_block_t)completion{
if (self.jp_viewInterfaceOrientation != JPVideoPlayViewInterfaceOrientationLandscape) {
return;
}
self.helper.viewInterfaceOrientation = JPVideoPlayViewInterfaceOrientationPortrait;
JPVideoPlayerView *videoPlayerView = self.helper.videoPlayerView;
videoPlayerView.backgroundColor = [UIColor clearColor];
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldShowBlackBackgroundWhenPlaybackStart)]) {
BOOL shouldShow = [self.jp_videoPlayerDelegate shouldShowBlackBackgroundWhenPlaybackStart];
videoPlayerView.backgroundColor = shouldShow ? [UIColor clearColor] : [UIColor clearColor];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// display status bar.
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
#pragma clang diagnostic pop
videoPlayerView.controlContainerView.alpha = 0;
if (flag) {
[UIView animateWithDuration:0.35
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
[self executePortrait];
}
completion:^(BOOL finished) {
[self finishPortrait];
if (completion) {
completion();
}
}];
}
else{
[self executePortrait];
[self finishPortrait];
if (completion) {
completion();
}
}
[self refreshStatusBarOrientation:UIInterfaceOrientationPortrait];
[self callOrientationDelegateWithInterfaceOrientation:JPVideoPlayViewInterfaceOrientationPortrait];
}
#pragma mark - Private
- (void)callOrientationDelegateWithInterfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation {
if(self.jp_controlView && [self.jp_controlView respondsToSelector:@selector(videoPlayerInterfaceOrientationDidChange:videoURL:)]){
[self.jp_controlView videoPlayerInterfaceOrientationDidChange:interfaceOrientation videoURL:self.jp_videoURL];
}
if(self.jp_progressView && [self.jp_progressView respondsToSelector:@selector(videoPlayerInterfaceOrientationDidChange:videoURL:)]){
[self.jp_progressView videoPlayerInterfaceOrientationDidChange:interfaceOrientation videoURL:self.jp_videoURL];
}
}
- (void)invokeStartBufferingDelegateMethod {
if(self.jp_bufferingIndicator && [self.jp_bufferingIndicator respondsToSelector:@selector(didStartBufferingVideoURL:)]){
[self.jp_bufferingIndicator didStartBufferingVideoURL:self.jp_videoURL];
}
}
- (void)invokeFinishBufferingDelegateMethod {
if(self.jp_bufferingIndicator && [self.jp_bufferingIndicator respondsToSelector:@selector(didFinishBufferingVideoURL:)]){
[self.jp_bufferingIndicator didFinishBufferingVideoURL:self.jp_videoURL];
}
}
- (void)finishPortrait {
JPVideoPlayerView *videoPlayerView = self.helper.videoPlayerView;
[videoPlayerView removeFromSuperview];
[self addSubview:videoPlayerView];
videoPlayerView.frame = self.bounds;
[[JPVideoPlayerManager sharedManager] videoPlayer].playerModel.playerLayer.frame = self.bounds;
[UIView animateWithDuration:0.5 animations:^{
videoPlayerView.controlContainerView.alpha = 1;
}];
}
- (void)executePortrait {
UIView *videoPlayerView = self.helper.videoPlayerView;
CGRect frame = [self.superview convertRect:self.frame toView:nil];
videoPlayerView.transform = CGAffineTransformIdentity;
videoPlayerView.frame = frame;
[[JPVideoPlayerManager sharedManager] videoPlayer].playerModel.playerLayer.frame = self.bounds;
}
- (void)executeLandscape {
UIView *videoPlayerView = self.helper.videoPlayerView;
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGRect bounds = CGRectMake(0, 0, CGRectGetHeight(screenBounds), CGRectGetWidth(screenBounds));
CGPoint center = CGPointMake(CGRectGetMidX(screenBounds), CGRectGetMidY(screenBounds));
videoPlayerView.bounds = bounds;
videoPlayerView.center = center;
videoPlayerView.transform = CGAffineTransformMakeRotation(M_PI_2);
[[JPVideoPlayerManager sharedManager] videoPlayer].playerModel.playerLayer.frame = bounds;
}
- (void)refreshStatusBarOrientation:(UIInterfaceOrientation)interfaceOrientation {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[UIApplication sharedApplication] setStatusBarOrientation:interfaceOrientation animated:YES];
#pragma clang diagnostic pop
}
- (JPVideoPlayerHelper *)helper {
JPVideoPlayerHelper *helper = objc_getAssociatedObject(self, _cmd);
if(!helper){
helper = [[JPVideoPlayerHelper alloc] initWithPlayVideoView:self];
objc_setAssociatedObject(self, _cmd, helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return helper;
}
#pragma mark - JPVideoPlayerManager
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldDownloadVideoForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldDownloadVideoForURL:)]) {
return [self.jp_videoPlayerDelegate shouldDownloadVideoForURL:videoURL];
}
return YES;
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldAutoReplayForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldAutoReplayForURL:)]) {
return [self.jp_videoPlayerDelegate shouldAutoReplayForURL:videoURL];
}
return YES;
}
- (void)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
playerStatusDidChanged:(JPVideoPlayerStatus)playerStatus {
if(playerStatus == JPVideoPlayerStatusPlaying){
self.helper.videoPlayerView.backgroundColor = [UIColor clearColor];
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldShowBlackBackgroundWhenPlaybackStart)]) {
BOOL shouldShow = [self.jp_videoPlayerDelegate shouldShowBlackBackgroundWhenPlaybackStart];
self.helper.videoPlayerView.backgroundColor = shouldShow ? [UIColor clearColor] : [UIColor clearColor];
}
}
self.helper.playerStatus = playerStatus;
// JPDebugLog(@"playerStatus: %ld", playerStatus);
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(playerStatusDidChanged:)]) {
[self.jp_videoPlayerDelegate playerStatusDidChanged:playerStatus];
}
BOOL needDisplayBufferingIndicator =
playerStatus == JPVideoPlayerStatusBuffering ||
playerStatus == JPVideoPlayerStatusUnknown ||
playerStatus == JPVideoPlayerStatusFailed;
needDisplayBufferingIndicator ? [self invokeStartBufferingDelegateMethod] : [self invokeFinishBufferingDelegateMethod];
if(self.jp_controlView && [self.jp_controlView respondsToSelector:@selector(videoPlayerStatusDidChange:videoURL:)]){
[self.jp_controlView videoPlayerStatusDidChange:playerStatus videoURL:self.jp_videoURL];
}
if(self.jp_progressView && [self.jp_progressView respondsToSelector:@selector(videoPlayerStatusDidChange:videoURL:)]){
[self.jp_progressView videoPlayerStatusDidChange:playerStatus videoURL:self.jp_videoURL];
}
}
- (void)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
didFetchVideoFileLength:(NSUInteger)videoLength {
if(self.helper.controlView && [self.helper.controlView respondsToSelector:@selector(didFetchVideoFileLength:videoURL:)]){
[self.helper.controlView didFetchVideoFileLength:videoLength videoURL:self.jp_videoURL];
}
if(self.helper.progressView && [self.helper.progressView respondsToSelector:@selector(didFetchVideoFileLength:videoURL:)]){
[self.helper.progressView didFetchVideoFileLength:videoLength videoURL:self.jp_videoURL];
}
}
- (void)videoPlayerManagerDownloadProgressDidChange:(JPVideoPlayerManager *)videoPlayerManager
cacheType:(JPVideoPlayerCacheType)cacheType
fragmentRanges:(NSArray<NSValue *> *_Nullable)fragmentRanges
expectedSize:(NSUInteger)expectedSize
error:(NSError *_Nullable)error {
if(error){
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(playVideoFailWithError:videoURL:)]) {
[self.jp_videoPlayerDelegate playVideoFailWithError:JPErrorWithDescription(@"Try to play video with a invalid url")
videoURL:videoPlayerManager.managerModel.videoURL];
}
return;
}
switch(cacheType){
case JPVideoPlayerCacheTypeLocation:
if (!fragmentRanges) {
JPErrorLog(@"fragmentRanges can not be nil");
}
break;
default:
break;
}
if(self.helper.controlView && [self.helper.controlView respondsToSelector:@selector(cacheRangeDidChange:videoURL:)]){
[self.helper.controlView cacheRangeDidChange:fragmentRanges videoURL:self.jp_videoURL];
}
if(self.helper.progressView && [self.helper.progressView respondsToSelector:@selector(cacheRangeDidChange:videoURL:)]){
[self.helper.progressView cacheRangeDidChange:fragmentRanges videoURL:self.jp_videoURL];
}
}
- (void)videoPlayerManagerPlayProgressDidChange:(JPVideoPlayerManager *)videoPlayerManager
elapsedSeconds:(double)elapsedSeconds
totalSeconds:(double)totalSeconds
error:(NSError *_Nullable)error {
if(error){
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(playVideoFailWithError:videoURL:)]) {
[self.jp_videoPlayerDelegate playVideoFailWithError:JPErrorWithDescription(@"Try to play video with a invalid url")
videoURL:videoPlayerManager.managerModel.videoURL];
}
return;
}
if(self.helper.controlView && [self.helper.controlView respondsToSelector:@selector(playProgressDidChangeElapsedSeconds:totalSeconds:videoURL:)]){
[self.helper.controlView playProgressDidChangeElapsedSeconds:elapsedSeconds
totalSeconds:totalSeconds
videoURL:self.jp_videoURL];
}
if(self.helper.progressView && [self.helper.progressView respondsToSelector:@selector(playProgressDidChangeElapsedSeconds:totalSeconds:videoURL:)]){
[self.helper.progressView playProgressDidChangeElapsedSeconds:elapsedSeconds
totalSeconds:totalSeconds
videoURL:self.jp_videoURL];
}
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldPausePlaybackWhenApplicationWillResignActiveForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldPausePlaybackWhenApplicationWillResignActiveForURL:)]) {
return [self.jp_videoPlayerDelegate shouldPausePlaybackWhenApplicationWillResignActiveForURL:videoURL];
}
return NO;
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:)]) {
return [self.jp_videoPlayerDelegate shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:videoURL];
}
return YES;
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:)]) {
return [self.jp_videoPlayerDelegate shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:videoURL];
}
return YES;
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:)]) {
return [self.jp_videoPlayerDelegate shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:videoURL];
}
return NO;
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldTranslateIntoPlayVideoFromResumePlayForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldTranslateIntoPlayVideoFromResumePlayForURL:)]) {
return [self.jp_videoPlayerDelegate shouldTranslateIntoPlayVideoFromResumePlayForURL:videoURL];
}
return YES;
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldPausePlaybackWhenReceiveAudioSessionInterruptionNotificationForURL:(NSURL *)videoURL {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldPausePlaybackWhenReceiveAudioSessionInterruptionNotificationForURL:)]) {
return [self.jp_videoPlayerDelegate shouldPausePlaybackWhenReceiveAudioSessionInterruptionNotificationForURL:videoURL];
}
return YES;
}
- (AVAudioSessionCategory)videoPlayerManagerPreferAudioSessionCategory:(JPVideoPlayerManager *)videoPlayerManager {
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(preferAudioSessionCategory)]) {
return [self.jp_videoPlayerDelegate preferAudioSessionCategory];
}
return AVAudioSessionCategoryPlayback;
}
- (BOOL)videoPlayerManager:(JPVideoPlayerManager *)videoPlayerManager
shouldResumePlaybackFromPlaybackRecordForURL:(NSURL *)videoURL
elapsedSeconds:(NSTimeInterval)elapsedSeconds {
BOOL shouldResume = NO;
if (self.jp_videoPlayerDelegate && [self.jp_videoPlayerDelegate respondsToSelector:@selector(shouldResumePlaybackFromPlaybackRecordForURL:elapsedSeconds:)]) {
shouldResume = [self.jp_videoPlayerDelegate shouldResumePlaybackFromPlaybackRecordForURL:videoURL
elapsedSeconds:elapsedSeconds];
}
return shouldResume;
}
@end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment