iOS開發之視頻播放功能、邊播放邊緩存


最近新做一個類似獎勵視頻的內置視頻播放功能,並且實現邊下載邊播放,緩存后下次直接播放本地視頻,自動適應橫豎屏展示,給大家分享下核心代碼

有不太清楚的地方可以加我微信一起探討、主要六個文件如下

ECGRewardVideoView.h、

ECGRewardVideoView.m

ECGPlayVideoResourceLoaderDelegate.h

ECGPlayVideoResourceLoaderDelegate.m

ECGPlayVideoRequestTask.h 

ECGPlayVideoRequestTask.m

代碼如下,直接復制粘貼會有報錯地方,但是我覺得你們肯定一看就懂的哈,如有不懂記得微信交流。

外部調用代碼

參數說明:url-視頻url、duration-播放時間、width-視頻寬度、height-視頻高度。

[[ECGRewardVideoView sharedInstance] showInsideRewardVideoWithUrl:url duration:duration width:width height:height];

 

ECGRewardVideoView.h代碼,這是一個繼承單例類

#import <UIKit/UIKit.h>
#import "ECGBaseSingleInstance.h"

@interface ECGRewardVideoView : ECGBaseSingleInstance

/** 顯示內置獎勵視頻*/
- (void)showInsideRewardVideoWithUrl:(NSString *)url duration:(NSInteger)duration width:(NSInteger)width height:(NSInteger)height;

@end

ECGRewardVideoView.m代碼實現

#import "ECGRewardVideoView.h"
#import <AVKit/AVKit.h>
#import "CSUtility.h"
#import "ECGNCMultiLanguage.h"
#import "ECGPlayVideoUtility.h"
#import "ECGPlayVideoResourceLoaderDelegate.h"
#import "ECGNativeiOSAdapter.h"
#import "ECGCustomAlertView.h"
#import "ECGNCUtility.h"

@interface ECGRewardVideoView ()

/** 關閉視頻按鈕*/
@property (strong, nonatomic) UIButton *closeButton;
/** 關閉視頻按鈕*/
@property (strong, nonatomic) UIImageView *skipImageView;
/** 倒計時文本*/
@property (strong, nonatomic) UILabel *downTimeLabel;
/** 播放視頻layer*/
@property (strong, nonatomic) AVPlayerLayer *playerLayer;
/** 播放視頻背景*/
@property (strong, nonatomic) UIView *playBGView;
/** 播放器*/
@property (strong, nonatomic) AVPlayer *player;
/** 播放器*/
@property (strong, nonatomic) AVPlayerItem *playerItem;
/** 視頻是否播放完畢*/
@property (assign, nonatomic) BOOL launchVideoIsFinish;
/** 是否橫屏*/
@property (assign, nonatomic) BOOL isHorizontalScreen;
/** 視頻播放urlasset*/
@property (strong, nonatomic) AVURLAsset *urlAsset;
/** 緩存代理*/
@property (strong, nonatomic) ECGPlayVideoResourceLoaderDelegate *resourceLoaderDelegate;
/** 監聽播放進度*/
@property (strong, nonatomic) id timeObserverToken;
@property (strong, nonatomic) UIActivityIndicatorView *loadingView;

@end

@implementation ECGRewardVideoView

/** 顯示內置獎勵視頻*/
- (void)showInsideRewardVideoWithUrl:(NSString *)urlStr duration:(NSInteger)duration width:(NSInteger)width height:(NSInteger)height
{
    self.isHorizontalScreen = NO;
    self.launchVideoIsFinish = NO;
    UIViewController *showVC = [CSUtility cs_getCurrentShowViewController];
    CGRect rect = [UIScreen mainScreen].bounds;
    CGSize size = rect.size;
    NSURL *url = nil;
    if ([urlStr hasPrefix:@"http://"] || [urlStr hasPrefix:@"https://"]) {
        url = [NSURL URLWithString:urlStr];
        
        NSString *strUrl = urlStr;
        //判斷是否已經下載完畢,本地緩存路徑
        NSString *strLocalPath = [ECGPlayVideoUtility pathForSaveFullVideoDataWithVideoFileName:strUrl.lastPathComponent];
        if ([ECGPlayVideoUtility isFileExistAtPath:strLocalPath]) {
            url = [NSURL fileURLWithPath:strLocalPath];
            _playerItem = [AVPlayerItem playerItemWithURL:url];
        } else {
            //實現的邊下載邊緩存
            NSURL *customUrl = [ECGPlayVideoUtility customUrlForStandardUrl:url];
           _urlAsset = [AVURLAsset assetWithURL:customUrl];
            self.resourceLoaderDelegate = [[ECGPlayVideoResourceLoaderDelegate alloc] initWithVideoOrigionalUrlString:strUrl];
            self.resourceLoaderDelegate.didFailLoadingBlock = ^(NSError *error) {
                ECGNativeLogHCL(@"HCL視頻加載失敗 - error %@", error);
            };
            [_urlAsset.resourceLoader setDelegate:self.resourceLoaderDelegate queue:dispatch_get_main_queue()];
            _playerItem = [AVPlayerItem playerItemWithAsset:_urlAsset];
        }
    } else {
        if ([CSUtility cs_isExistFileForPath:urlStr]) {
            url = [NSURL fileURLWithPath:urlStr];
            _playerItem = [AVPlayerItem playerItemWithURL:url];
        } else {
            return;
        }
    }
    AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:_playerItem];
    _player = player;
    _playBGView = [[UIView alloc] initWithFrame:rect];
    _playBGView.backgroundColor = [UIColor blackColor];
    _playBGView.userInteractionEnabled = YES;
    [[UIApplication sharedApplication].delegate.window addSubview:_playBGView];
    UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleDoubleTap:)];
    doubleTapGesture.numberOfTapsRequired = 2;
    [_playBGView addGestureRecognizer:doubleTapGesture];
    
    _playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    _playerLayer.frame = _playBGView.bounds;
    _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    [_playBGView.layer addSublayer:_playerLayer];
    
    [_playerLayer.player play];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(0, 0, 100, 50);
    button.center = CGPointMake(size.width-50, 50);
    [button addTarget:self action:@selector(closeVideoButtonClick) forControlEvents:UIControlEventTouchUpInside];
    [[UIApplication sharedApplication].delegate.window addSubview:button];
    self.closeButton = button;
    
    UIImageView *imageview = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ecg_video_close"]];
    imageview.frame = CGRectMake(40, 5, 35, 35);
    [button addSubview:imageview];
    _skipImageView = imageview;
    
    UILabel *label = [[UILabel alloc] init];
    label.font = [UIFont systemFontOfSize:15];
    label.textColor = [UIColor whiteColor];
    label.textAlignment = NSTextAlignmentCenter;
    label.frame = CGRectMake(0, 5, 40, 35);
    [button addSubview:label];
    _downTimeLabel = label;
    _downTimeLabel.text = @(duration).stringValue;
    
    if (height < width) {
        _playBGView.frame = CGRectMake(0, 0, size.height, size.width);
        _playBGView.center = CGPointMake(size.width/2, size.height/2);
        _playBGView.transform = CGAffineTransformRotate(_playBGView.transform, M_PI_2);
        _playerLayer.frame = _playBGView.bounds;
        
        button.center = CGPointMake(size.width - 50, size.height - 50);
        button.transform = CGAffineTransformRotate(button.transform, M_PI_2);
        
        self.isHorizontalScreen = YES;
    }
    
    [[UIApplication sharedApplication].delegate.window addSubview:self.loadingView];
    
    [_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:NULL];
    [_playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:NULL];
    [_playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:NULL];
    [self delayStartAnimatingLoadingView];
    
    //注冊播放結束監聽
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoPlayEndNeedCallBackGame) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    
    CMTime interval = CMTimeMakeWithSeconds(0.5, NSEC_PER_SEC);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    __weak typeof(self) weakSelf = self;
    //監聽播放進度
    self.timeObserverToken = [self.player addPeriodicTimeObserverForInterval:interval queue:mainQueue usingBlock:^(CMTime time) {
        [weakSelf updatePlayProgressUIWithTime:time];
    }];
}

#pragma mark - 觀察播放器狀態
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        [self handlePlayerStatusChange:change];
    }else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {//正在緩沖
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayStartAnimatingLoadingView) object:nil];
        [self performSelector:@selector(delayStartAnimatingLoadingView) withObject:nil afterDelay:2];
    } else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {//緩沖到可播放程度了
        [self handlePlaybackLikelyToKeepUpWithChange:change];
    }
}

/** 雙擊手勢處理*/
- (void)handleDoubleTap:(UITapGestureRecognizer *)tap
{
    if (_playerLayer.videoGravity == AVLayerVideoGravityResizeAspectFill) {
         _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    } else {
        _playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    }
}

/** 處理 playbackLikelyToKeepUp 變化*/
- (void)handlePlaybackLikelyToKeepUpWithChange:(NSDictionary<NSKeyValueChangeKey,id> *)change
{
    id newValue = [change objectForKey:NSKeyValueChangeNewKey];
    BOOL playbackLikelyToKeepUp = [newValue boolValue];
    if (!playbackLikelyToKeepUp) {
        return;
    }
    [self stopAnimatingLoadingView];
}

/** 處理播放器狀態變化*/
- (void)handlePlayerStatusChange:(NSDictionary<NSKeyValueChangeKey,id> *)change
{
    NSInteger iNewStatus = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
    if (AVPlayerItemStatusReadyToPlay == iNewStatus) {
        ECGNativeLogHCL(@"HCL ============= 廣告視頻播放成功");
    } else if (AVPlayerItemStatusFailed == iNewStatus) {
        ECGNativeLogHCL(@"HCL ============= 廣告視頻播放失敗!");
    } else {
        ECGNativeLogHCL(@"HCL ============= 廣告視頻播放狀態異常:%@!", @(iNewStatus));
    }
}

/** 延遲顯示loading動畫*/
- (void)delayStartAnimatingLoadingView
{
    [self.loadingView startAnimating];
}

/** 停止loading動畫*/
- (void)stopAnimatingLoadingView
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayStartAnimatingLoadingView) object:nil];
    [self.loadingView stopAnimating];
}

#pragma mark - 成員延遲初始化
- (UIActivityIndicatorView *)loadingView
{
    if (nil == _loadingView)
    {
        _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        _loadingView.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/2);
        _loadingView.hidesWhenStopped = true;
    }
    return  _loadingView;
}

/** 更新播放倒計時UI*/
- (void)updatePlayProgressUIWithTime:(CMTime)time
{
    double total = CMTimeGetSeconds(_playerItem.duration);
    double current = CMTimeGetSeconds(time);
    double progress = current/total;
    int downTime = (int)(total-current);
    if (downTime > 0) {
        NSString *downTimeStr = [NSString stringWithFormat:@"%d", downTime];
        _downTimeLabel.text = downTimeStr;
    }
    if (current >= total) {
        _downTimeLabel.text = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"149906"]; //149906=關閉
    }
}

/** 關閉視頻按鈕點擊*/
- (void)closeVideoButtonClick
{
    if (self.launchVideoIsFinish) {
        [self removeVideoView];
        //視頻播放完成,滿足獎勵
        [ECGNativeiOSAdapter playInsideRewerdVideoFinishCallBackResult:1];
    } else {
        __weak typeof(self) weakSelf = self;
        NSString *cancelStr = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"108532"];  //108532=取消
        NSString *confirmStr = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"140369"]; //140369=退出
        NSString *message = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"660505"]; //660505=當前視頻未播放完畢無法獲得獎勵,您確定要退出嗎?
        ECGCustomAlertView *alertView = [[ECGCustomAlertView alloc] initWithTitle:nil message:message cancelTitle:cancelStr cancelClickBlock:^{
            weakSelf.closeButton.userInteractionEnabled = YES;
        } confirmTitle:confirmStr confirmClickBlock:^{
            weakSelf.closeButton.userInteractionEnabled = YES;
            [weakSelf removeVideoView];
            if (weakSelf.launchVideoIsFinish) {
                //視頻播放完成,獎勵
                [ECGNativeiOSAdapter playInsideRewerdVideoFinishCallBackResult:1];
            } else {
                //視頻未播放完成,沒有獎勵
                [ECGNativeiOSAdapter playInsideRewerdVideoFinishCallBackResult:0];
            }
        }];
        [alertView showInWindow];
        if (self.isHorizontalScreen) {
            alertView.transform = CGAffineTransformRotate(alertView.transform, M_PI_2);
        }
        //讓關閉按鈕不能點擊
        _closeButton.userInteractionEnabled = NO;
    }
}

/** 播放結束*/
- (void)videoPlayEndNeedCallBackGame
{
    self.launchVideoIsFinish = YES;
    [self stopAnimatingLoadingView];
}

/** 播放視頻結束*/
- (void)removeVideoView
{
    if (nil != _timeObserverToken && nil != _player) {
        [_player removeTimeObserver:_timeObserverToken];
    }
    //播放器結束
    [_player pause];
    [_player replaceCurrentItemWithPlayerItem:nil];
    //移除視頻
    [_playerLayer removeFromSuperlayer];
    _playerLayer = nil;
    [_playBGView removeFromSuperview];
    _playBGView = nil;
    _playerItem = nil;
    _player = nil;
    _urlAsset = nil;
    _skipImageView = nil;
    _downTimeLabel = nil;
    //移除button
    [_closeButton removeFromSuperview];
    _closeButton = nil;
    [self stopAnimatingLoadingView];
}

@end    

實現邊下載邊播放的類

ECGPlayVideoResourceLoaderDelegate.h代碼

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

typedef void(^ECGPlayVideoResourceLoaderDelegateVoidBlock)(void);
typedef void(^ECGPlayVideoResourceLoaderDelegateErrorBlock)(NSError *error);

@interface ECGPlayVideoResourceLoaderDelegate : NSObject <AVAssetResourceLoaderDelegate>

@property (readonly, nonatomic) NSString *videoOrigionalUrlString;//視頻原始url,區別於替換成自定義url Scheme后的url
@property (assign,   nonatomic) BOOL isUserChangePlayProgress;//用戶是否改變了播放進度,比如滑動進度條

@property (copy, nonatomic) ECGPlayVideoResourceLoaderDelegateErrorBlock didFailLoadingBlock;
@property (copy, nonatomic) ECGPlayVideoResourceLoaderDelegateVoidBlock didFinishLoadingBlock;

- (instancetype)initWithVideoOrigionalUrlString:(NSString *)strUrl;

@end

ECGPlayVideoResourceLoaderDelegate.m實現代碼

#import <MobileCoreServices/MobileCoreServices.h>
#import "ECGPlayVideoResourceLoaderDelegate.h"
#import "ECGPlayVideoRequestTask.h"
#import "ECGPlayVideoUtility.h"

@interface ECGPlayVideoResourceLoaderDelegate ()

@property (copy,   nonatomic) NSString *videoOrigionalUrlString;
@property (strong, nonatomic) NSMutableArray<AVAssetResourceLoadingRequest *> *loadingRequestArray;
@property (strong, nonatomic) ECGPlayVideoRequestTask *requestTask;

@end

@implementation ECGPlayVideoResourceLoaderDelegate

- (instancetype)initWithVideoOrigionalUrlString:(NSString *)strUrl
{
    self = [super init];
    if (self) {
        self.videoOrigionalUrlString = strUrl;
    }
    return self;
}

#pragma mark - 業務函數

/** 給請求填充信息*/
- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest
{
    NSString *mimeType = self.requestTask.mimeType;
    CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL);
    contentInformationRequest.byteRangeAccessSupported = YES;
    contentInformationRequest.contentType = CFBridgingRelease(contentType);
    contentInformationRequest.contentLength = self.requestTask.videoLength;
}

/** 響應請求數據*/
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
    long long startOffset = dataRequest.requestedOffset;
    
    if (dataRequest.currentOffset != 0) {
        startOffset = dataRequest.currentOffset;
    }
    
    if ((self.requestTask.offset +self.requestTask.downLoadingOffset) < startOffset)
    {
        //NSLog(@"NO DATA FOR REQUEST");
        return NO;
    }
    
    if (startOffset < self.requestTask.offset) {
        return NO;
    }
    
    NSAssert([self.videoOrigionalUrlString hasPrefix:@"http"], @"respondWithDataForRequest:self.videoOrigionalUrlString異常,應該是http開頭的網絡url");
    NSString *strTempPath = [ECGPlayVideoUtility pathForTempVideoDataWithVideoFileName:[_videoOrigionalUrlString lastPathComponent]];
    
    NSData *filedata = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:strTempPath] options:NSDataReadingMappedIfSafe error:nil];
    
    // This is the total data we have from startOffset to whatever has been downloaded so far
    NSUInteger unreadBytes = self.requestTask.downLoadingOffset - ((NSInteger)startOffset - self.requestTask.offset);
    
    // Respond with whatever is available if we can't satisfy the request fully yet
    NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
    
    [dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.requestTask.offset, (NSUInteger)numberOfBytesToRespondWith)]];
    
    long long endOffset = startOffset + dataRequest.requestedLength;
    BOOL didRespondFully = (self.requestTask.offset + self.requestTask.downLoadingOffset) >= endOffset;
    
    return didRespondFully;
}

/** 處理請求數組*/
- (void)processLoadingRequestArray
{
    NSMutableArray<AVAssetResourceLoadingRequest *> *finishRequests = [NSMutableArray array];
    for (AVAssetResourceLoadingRequest *loadingRequest in self.loadingRequestArray)
    {
        [self fillInContentInformation:loadingRequest.contentInformationRequest]; //對每次請求加上長度,文件類型等信息
        BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; //判斷此次請求的數據是否處理完全
        if (didRespondCompletely) {
            [finishRequests addObject:loadingRequest];
            [loadingRequest finishLoading];
        }
    }
    
    [self.loadingRequestArray removeObjectsInArray:finishRequests];
    finishRequests = nil;
}

/** 處理視頻數據請求*/
- (void)processLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
    if (![loadingRequest.request.URL.lastPathComponent isEqualToString:_videoOrigionalUrlString.lastPathComponent]) {
        NSAssert(NO, @"processLoadingRequest:當前請求的視頻不是期望請求的視頻?");
        return;
    }
    
    if (_requestTask.downLoadingOffset > 0) {
        [self processLoadingRequestArray];
    }
    
    if (nil == _requestTask || _isUserChangePlayProgress) {
        [self.requestTask setVideoUrl:[NSURL URLWithString:_videoOrigionalUrlString] offset:0];
    }
}

#pragma mark - AVAssetResourceLoaderDelegate
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
    NSAssert([loadingRequest.request.URL.absoluteString hasPrefix:[ECGPlayVideoUtility customUrlScheme]], @"沒有自定義url scheme?");
    [self.loadingRequestArray addObject:loadingRequest];
    [self processLoadingRequest:loadingRequest];
    return YES;
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
    [self.loadingRequestArray removeObject:loadingRequest];
}

#pragma mark - 成員延遲初始化
- (NSMutableArray<AVAssetResourceLoadingRequest *> *)loadingRequestArray
{
    if (nil == _loadingRequestArray) {
        _loadingRequestArray = [NSMutableArray array];
    }
    return _loadingRequestArray;
}

- (ECGPlayVideoRequestTask *)requestTask
{
    if (nil == _requestTask) {
        _requestTask = [[ECGPlayVideoRequestTask alloc] init];
        __weak typeof(self) weakSelf = self;
        _requestTask.didReceiveVideoDataBlock = ^{
            [weakSelf processLoadingRequestArray];
        };
        _requestTask.didFailLoadingBlock = ^(NSError *error) {
            if (weakSelf.didFailLoadingBlock) {
                weakSelf.didFailLoadingBlock(error);
            }
        };
        _requestTask.didFinishLoadingBlock = ^{
            if (weakSelf.didFinishLoadingBlock) {
                weakSelf.didFinishLoadingBlock();
            }
        };
    }
    return _requestTask;
}

#pragma mark - 屬性設置方法
- (void)setIsUserChangePlayProgress:(BOOL)isUserChangePlayProgress
{
    _isUserChangePlayProgress = isUserChangePlayProgress;
    _requestTask.isUserChangePlayProgress = isUserChangePlayProgress;
}

@end

ECGPlayVideoRequestTask.h代碼

#import <Foundation/Foundation.h>

typedef void(^ECGPlayVideoRequestTaskVoidBlock)(void);
typedef void(^ECGPlayVideoRequestTaskErrorBlock)(NSError *error);

@interface ECGPlayVideoRequestTask : NSObject

@property (readonly, nonatomic) NSURL *url;
@property (readonly, nonatomic) NSUInteger offset;
@property (readonly, nonatomic) NSUInteger videoLength;
@property (readonly, nonatomic) NSUInteger downLoadingOffset;
@property (readonly, nonatomic) NSString *mimeType;
@property (readonly, nonatomic) BOOL isFinishLoad;

@property (copy,     nonatomic) ECGPlayVideoRequestTaskVoidBlock didReceiveVideoDataBlock;
@property (copy,     nonatomic) ECGPlayVideoRequestTaskVoidBlock didFinishLoadingBlock;
@property (copy,     nonatomic) ECGPlayVideoRequestTaskErrorBlock didFailLoadingBlock;

@property (assign,   nonatomic) BOOL isUserChangePlayProgress;//用戶是否改變了播放進度,比如滑動進度條

- (void)setVideoUrl:(NSURL *)url offset:(NSUInteger)offset;
- (void)cancel;
- (void)clearData;

@end

ECGPlayVideoRequestTask.m代碼

#import "ECGPlayVideoRequestTask.h"
#import "ECGPlayVideoUtility.h"

@interface ECGPlayVideoRequestTask () <NSURLSessionDataDelegate>

@property (strong, nonatomic) NSURL *url;
@property (assign, nonatomic) NSUInteger offset;
@property (assign, nonatomic) NSUInteger videoLength;
@property (assign, nonatomic) NSUInteger downLoadingOffset;
@property (copy,   nonatomic) NSString *mimeType;
@property (assign, nonatomic) BOOL isFinishLoad;

@property (copy,   nonatomic) NSString *tempFilePath;
@property (strong, nonatomic) NSURLSessionDataTask *dataTask;
@property (strong, nonatomic) NSFileHandle *fileHandle;

@end

@implementation ECGPlayVideoRequestTask

#pragma mark - 內部函數

/** 處理用戶修改播放進度*/
- (void)handleUserChangePlayProgress
{
    [self clearData];
    self.offset = 0;
    self.videoLength = 0;
    self.downLoadingOffset = 0;
    self.mimeType = nil;
    self.isFinishLoad = NO;
    self.tempFilePath = nil;
    self.fileHandle = nil;
    
    //刪除臨時文件
    [[NSFileManager defaultManager] removeItemAtPath:_tempFilePath error:nil];
}

/** 處理收到Response*/
- (void)handleDidReceiveResponse:(NSURLResponse *)response
{
    _isFinishLoad = NO;
    NSAssert([response isKindOfClass:[NSHTTPURLResponse class]], @"didReceiveResponse:不是 NSHTTPURLResponse");
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
    NSDictionary *dicAllHeaderFields = [httpResponse allHeaderFields] ;
    
    NSString *content = [dicAllHeaderFields valueForKey:@"Content-Range"];
    NSArray *array = [content componentsSeparatedByString:@"/"];
    NSString *strLength = array.lastObject;
    
    NSUInteger videoLength = 0;
    
    if (strLength.length < 1) {
        videoLength = (NSUInteger)httpResponse.expectedContentLength;
    } else {
        videoLength = [strLength integerValue];
    }
    
    self.videoLength = videoLength;
    
    NSString *strContentType = [dicAllHeaderFields objectForKey:@"Content-Type"];
    if ([strContentType isKindOfClass:[NSString class]] && strContentType.length > 0) {
        self.mimeType = strContentType;
    }else {
        self.mimeType = @"video/mp4";
    }
    
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:_tempFilePath];
}

/** 處理收到Data*/
- (void)handleDidReceiveData:(NSData *)data
{
    [self.fileHandle seekToEndOfFile];
    [self.fileHandle writeData:data];
    _downLoadingOffset += data.length;
    
    if (self.didReceiveVideoDataBlock) {
        self.didReceiveVideoDataBlock();
    }
}

/** 處理數據下載完畢*/
- (void)handleFinishReceiveData
{
    _isFinishLoad = YES;
    
    //如果用戶沒有修改播放進度,則保存臨時文件
    if (!_isUserChangePlayProgress) {
        NSString *moveToPath = [ECGPlayVideoUtility pathForSaveFullVideoDataWithVideoFileName:_url.lastPathComponent];
        
        //這里不要用moveItemToPath,因為視頻可能正在播放
        [[NSFileManager defaultManager] copyItemAtPath:_tempFilePath toPath:moveToPath error:nil];
    }
    
    if (self.didFinishLoadingBlock) {
        self.didFinishLoadingBlock();
    }
}

#pragma mark - 屬性設置方法
- (void)setIsUserChangePlayProgress:(BOOL)isUserChangePlayProgress
{
    _isUserChangePlayProgress = isUserChangePlayProgress;
    if (_isUserChangePlayProgress) {
        //刪除臨時文件
        [self handleUserChangePlayProgress];
    }
}

#pragma mark - 對外接口
- (void)setVideoUrl:(NSURL *)url offset:(NSUInteger)offset
{
    _url = url;
    _offset = offset;
    
    self.tempFilePath = [ECGPlayVideoUtility pathForTempVideoDataWithVideoFileName:[_url lastPathComponent]];
    if (![ECGPlayVideoUtility isFileExistAtPath:_tempFilePath]) {
        [[NSFileManager defaultManager] createFileAtPath:_tempFilePath contents:nil attributes:nil];
    }
    
    _downLoadingOffset = 0;
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15];
    
    if (offset > 0 && self.videoLength > 0) {
        [request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld",(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@"Range"];
    }
    
    [self cancel];
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    self.dataTask = [session dataTaskWithRequest:request];
    [self.dataTask resume];
    [session finishTasksAndInvalidate];
}

- (void)cancel
{
    [self.dataTask cancel];
}

- (void)clearData
{
    [self cancel];
    //移除文件
    [[NSFileManager defaultManager] removeItemAtPath:_tempFilePath error:nil];
}

#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    [self handleDidReceiveResponse:response];
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [self handleDidReceiveData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (nil != error) {
        [session finishTasksAndInvalidate];
        if (self.didFailLoadingBlock) {
            self.didFailLoadingBlock(error);
        }
    }else {
        [self handleFinishReceiveData];
    }
}

- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error
{
    [session finishTasksAndInvalidate];
}

@end

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM