播放器輪子製造回顧|項目覆盤

1、項目簡介:

最近利用閒暇時間來重構一下以前的播放器,此次的主要升級功能以下:git

  • 支持動態切換內核
  • 支持音/視頻播放,midi文件播放
  • 支持在線播放/本地播放
  • 支持後臺播放,音頻提取播放
  • 支持視頻邊下邊播,分片下載播放存儲
  • 支持斷點續載續播,下次直接優先從緩衝讀取播放
  • 支持緩存管理,清除時間段緩存
  • 支持試看,自動跳過片頭
  • 支持記錄上次播放時間
  • 支持自動播放,自動連續播放
  • 支持隨機/重複/順序播放
  • 支持重力感應,全屏/半屏切換
  • 支持基本手勢操做,進度音量等
  • 支持切換不一樣分辨率視頻
  • 支持直播流媒體播放

視頻支持格式:mp四、m3u八、wav、avi
音頻支持格式:midi、mp三、github


2、項目背景:

在很早以前寫個一個播放器架子,可是這個架子只是當初簡單的實現播放處理,那麼這段時間事情很少,因此就利用閒暇時間來升級重構一下,其實網上關於播放器的輪子也很是多 ZFPlayerKRVideoPlayerSJVideoPlayer數據庫

這些輪子都很優秀,因此在製做的時候有借鑑參考
而後慢慢完善實現一款能夠高度自定義而且支持動態切換內核的播放器殼子,支持邊下邊播邊存,續播續傳功能,基本手勢播放等等功能
至於流媒體暫時網上也沒發現有什麼能夠實現邊下邊播邊存的方案,我其實這裏有這樣一個思路,能夠利用搭建本地服務器來處理,先將分片數據下載到本地,而後播放這樣也就只須要用戶一次網絡數據下載,等空閒時間我再來慢慢實現
目前已初步實現KJAVPlyaer、KJIJKPlayer、KJMIDIPlayer三種內核,緩存

stateDiagram-v2
播放器 --> KJBasePlayer
KJBasePlayer --> KJAVPlayer
KJBasePlayer --> KJIJKPlayer
KJBasePlayer --> KJMIDIPlayer

KJAVPlayer --> KJAVPlayer+KJCache
  • KJAVPlyaer基於AVPlayer封裝使用
  • KJIJKPlayer基於b站IJKMediaFramework封裝使用
  • KJMIDIPlayer基於AudioToolBox封裝使用

3、實踐過程:

技術選型:項目目前主要仍是基於AVPlayer的靈活封裝使用,而後也有對Bilibili開源IJKMediaFramework的使用處理
基於AVPlayer實現邊下邊播邊存功能,目前不少網上資料基本都是基於唱吧KTVHTTPCache來實現,而本文則是採用NSURLSession封裝的下載器,在結合文件寫入NSFileHandle來實現,而後將信息存儲在數據庫服務器

4、模塊介紹

主要就是分爲如下幾大模塊,markdown

KJBaseFunctionPlayer播放器協議

全部播放器殼子都是基於該基礎作處理,提取公共部分網絡

API & Property 類型 功能
delegate Property 委託代理
requestHeader Property 視頻請求頭
roregroundResume Property 返回前臺繼續播放
backgroundPause Property 進入後臺暫停播放
autoPlay Property 是否開啓自動播放
speed Property 播放速度
volume Property 播放音量
cacheTime Property 緩存達到多少秒才能播放
skipHeadTime Property 跳過片頭
timeSpace Property 時間刻度
kVideoTotalTime Property 獲取視頻總時長
kVideoURLFromat Property 獲取視頻格式
kVideoTryLookTime Property 免費試看時間和試看結束回調
videoURL Property 視頻地址
localityData Property 是否爲本地資源
isPlaying Property 是否正在播放
currentTime Property 當前播放時間
ecode Property 播放失敗
kVideoAdvanceAndReverse Property 快進或快退
shared Property 單例屬性
kj_sharedInstance Instance 建立單例
kj_attempDealloc Instance 銷燬單例
kj_play Instance 準備播放
kj_replay Instance 重播
kj_resume Instance 繼續
kj_pause Instance 暫停
kj_stop Instance 中止

KJPlayerDelegate委託代理

/* 當前播放器狀態 */
- (void)kj_player:(KJBasePlayer*)player state:(KJPlayerState)state;
/* 播放進度 */
- (void)kj_player:(KJBasePlayer*)player currentTime:(NSTimeInterval)time;
/* 緩存進度 */
- (void)kj_player:(KJBasePlayer*)player loadProgress:(CGFloat)progress;
/* 播放錯誤 */
- (void)kj_player:(KJBasePlayer*)player playFailed:(NSError*)failed;
複製代碼

KJBaseUIPlayer播放器協議

播放器UI相關協議框架

API & Property 類型 功能
playerView Property 播放器載體
background Property 背景顏色
placeholder Property 佔位圖
videoGravity Property 視頻顯示模式
kVideoSize Property 獲取視頻尺寸大小
kVideoTimeScreenshots Property 獲取當前截屏
kVideoPlaceholderImage Property 子線程獲取封面圖,圖片會存儲在磁盤
kj_startAnimation Instance 圓圈加載動畫
kj_stopAnimation Instance 中止動畫
kj_displayHintText: Instance 支持富文本提示的文本框,零秒錶示不自動消失
kj_displayHintText:time:max:position: Instance 支持富文本提示的文本框,零秒錶示不自動消失
kj_hideHintText Instance 隱藏提示文字

KJBasePlayerView播放器視圖基類,播放器控件父類

只要子控件沒有涉及到手勢交互,我均採用Layer的方式來處理,而後根據zPosition來區分控件的上下層級關係async

/* 控件位置和大小發生改變信息通知 */
extern NSString *kPlayerBaseViewChangeNotification;
/* 控件位置和大小發生改變key */
extern NSString *kPlayerBaseViewChangeKey;
@protocol KJPlayerBaseViewDelegate;
@interface KJBasePlayerView : UIImageView
/* 委託代理 */
@property (nonatomic,weak) id <KJPlayerBaseViewDelegate> delegate;
/* 主色調,默認白色 */
@property (nonatomic,strong) UIColor *mainColor;
/* 副色調,默認紅色 */
@property (nonatomic,strong) UIColor *viceColor;
/* 支持手勢,支持多枚舉 */
@property (nonatomic,assign) KJPlayerGestureType gestureType;
/* 長按執行時間,默認1秒 */
@property (nonatomic,assign) NSTimeInterval longPressTime;
/* 操做面板自動隱藏時間,默認2秒而後爲零表示不隱藏 */
@property (nonatomic,assign) NSTimeInterval autoHideTime;
/* 操做面板高度,默認60px */
@property (nonatomic,assign) CGFloat operationViewHeight;
/* 當前操做面板狀態 */
@property (nonatomic,assign,readonly) BOOL displayOperation;
/* 隱藏操做面板時是否隱藏返回按鈕,默認yes */
@property (nonatomic,assign) BOOL isHiddenBackButton;
/* 小屏狀態下是否顯示返回按鈕,默認yes */
@property (nonatomic,assign) BOOL smallScreenHiddenBackButton;
/* 全屏狀態下是否顯示返回按鈕,默認no */
@property (nonatomic,assign) BOOL fullScreenHiddenBackButton;
/* 是否開啓自動旋轉,默認yes */
@property (nonatomic,assign) BOOL autoRotate;
/* 是否爲全屏,名字別亂改後面kvc有使用 */
@property (nonatomic,assign) BOOL isFullScreen;
/* 當前屏幕狀態,名字別亂改後面kvc有使用 */
@property (nonatomic,assign,readonly) KJPlayerVideoScreenState screenState;
/* 當前屏幕狀態發生改變 */
@property (nonatomic,copy,readwrite) void (^kVideoChangeScreenState)(KJPlayerVideoScreenState state);
/* 返回回調 */
@property (nonatomic,copy,readwrite) void (^kVideoClickButtonBack)(KJBasePlayerView *view);
/* 提示文字面板屬性,默認最大寬度250px */
@property (nonatomic,copy,readonly) void (^kVideoHintTextInfo)(void(^)(KJPlayerHintInfo *info));

#pragma mark - 控件
/* 快進快退進度控件 */
@property (nonatomic,strong) KJPlayerFastLayer *fastLayer;
/* 音量亮度控件 */
@property (nonatomic,strong) KJPlayerSystemLayer *vbLayer;
/* 加載動畫層 */
@property (nonatomic,strong) KJPlayerLoadingLayer *loadingLayer;
/* 文本提示框 */
@property (nonatomic,strong) KJPlayerHintTextLayer *hintTextLayer;
/* 頂部操做面板 */
@property (nonatomic,strong) KJPlayerOperationView *topView;
/* 底部操做面板 */
@property (nonatomic,strong) KJPlayerOperationView *bottomView;
/* 返回按鈕 */
@property (nonatomic,strong) KJPlayerButton *backButton;
/* 鎖屏按鈕 */
@property (nonatomic,strong) KJPlayerButton *lockButton;
/* 播放按鈕 */
@property (nonatomic,strong) KJPlayerButton *centerPlayButton;

#pragma mark - method
/* 隱藏操做面板,是否隱藏返回按鈕 */
- (void)kj_hiddenOperationView;
/* 顯示操做面板 */
- (void)kj_displayOperationView;
/* 取消收起操做面板,可用於滑動滑桿時刻不自動隱藏 */
- (void)kj_cancelHiddenOperationView;
複製代碼

KJPlayerBaseViewDelegate控件載體協議

控件相關委託代理ide

/* 單雙擊手勢反饋 */
- (void)kj_basePlayerView:(KJBasePlayerView*)view isSingleTap:(BOOL)tap;
/* 長按手勢反饋 */
- (void)kj_basePlayerView:(KJBasePlayerView*)view longPress:(UILongPressGestureRecognizer*)longPress;
/* 進度手勢反饋,不替換UI請返回當前時間和總時間,範圍-1 ~ 1 */
- (NSArray*)kj_basePlayerView:(KJBasePlayerView*)view progress:(float)progress end:(BOOL)end;
/* 音量手勢反饋,是否替換自帶UI,範圍0 ~ 1 */
- (BOOL)kj_basePlayerView:(KJBasePlayerView*)view volumeValue:(float)value;
/* 亮度手勢反饋,是否替換自帶UI,範圍0 ~ 1 */
- (BOOL)kj_basePlayerView:(KJBasePlayerView*)view brightnessValue:(float)value;
/* 是否鎖屏,根據KJPlayerButton的type來肯定當前按鈕類型 */
- (void)kj_basePlayerView:(KJBasePlayerView*)view PlayerButton:(KJPlayerButton*)button;
複製代碼

KJPlayerType

枚舉文件夾和公共方法管理

  • KJPlayerState:播放器狀態
  • KJPlayerCustomCode:錯誤code
  • KJPlayerGestureType:手勢操做
  • KJPlayerPlayType:播放類型
  • KJPlayerDeviceDirection:手機方向
  • KJPlayerVideoGravity:播放器充滿類型
  • KJPlayerVideoFromat:視頻格式

DBPlayerDataInfo

主要包括兩部分,數據庫模型和增刪改查等工具
數據庫結構

dbid:惟一id,視頻連接去除scheme而後md5
videoUrl:視頻連接  
saveTime:存儲時間戳
sandboxPath:沙盒地址
videoFormat:視頻格式
videoTime:視頻時間
videoData:視頻數據
複製代碼

數據庫工具

方法 功能
kj_insertData:Data: 插入數據,重複數據替換處理
kj_deleteData: 刪除數據
kj_addData: 新添加數據
kj_updateData:Data: 更新數據
kj_checkData: 查詢數據,傳空傳所有數據
kCheckAppointDatas 指定條件查詢

KJResourceLoader

中間橋樑做用,把網絡請求緩存到本地的臨時數據傳遞給播放器
關於這塊的詳細介紹,我單獨寫了一篇 開發播放器框架之邊下邊播邊存方案分享

KJPlayer - AVPlayer播放器內核

工做流程:

graph TD
獲取視頻類型 --> 處理視頻 --> 處理視頻連接地址 --> 判斷地址是否可用 --> ... --> 播放處理

一、獲取視頻類型,根據網址來肯定,目前沒找到更好的方式(知道的朋友能夠指點一下)

/// 根據連接獲取Asset類型
NS_INLINE KJPlayerAssetType kPlayerVideoAesstType(NSURL *url){
    if (url == nil) return KJPlayerAssetTypeNONE;
    if (url.pathExtension.length) {
        if ([url.pathExtension containsString:@"m3u8"] || [url.pathExtension containsString:@"ts"]) {
            return KJPlayerAssetTypeHLS;
        }
    }
    NSArray * array = [url.path componentsSeparatedByString:@"."];
    if (array.count == 0) {
        return KJPlayerAssetTypeNONE;
    }else{
        if ([array.lastObject containsString:@"m3u8"] || [array.lastObject containsString:@"ts"]) {
            return KJPlayerAssetTypeHLS;
        }
    }
    return KJPlayerAssetTypeFILE;
}
複製代碼

二、處理視頻,這裏才用隊列組來處理,子線程處理解決第一次加載卡頓問題

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    if ([weakself kj_dealVideoURL:&tempURL]) {
        if (![tempURL.absoluteString isEqualToString:self->_videoURL.absoluteString]) {
            self->_videoURL = tempURL;
            [weakself kj_initPreparePlayer];
        }else{
            [weakself kj_playerReplay];
        }
    }
});
複製代碼

三、處理視頻連接地址,這裏分兩種狀況,使用緩存就從緩存當中讀取

/* 判斷當前資源文件是否有緩存,修改成指定連接地址 */
- (void)kj_judgeHaveCacheWithVideoURL:(NSURL * _Nonnull __strong * _Nonnull)videoURL{
    self.locality = NO;
    KJCacheManager.kJudgeHaveCacheURL(^(BOOL locality) {
        self.locality = locality;
        if (locality) {
            self.playError = [DBPlayerDataInfo kj_errorSummarizing:KJPlayerCustomCodeCachedComplete];
        }
    }, videoURL);
}
複製代碼

獲取數據庫當中的數據

/* 判斷是否有緩存,返回緩存連接 */
+ (void(^)(void(^)(BOOL),NSURL * _Nonnull __strong * _Nonnull))kJudgeHaveCacheURL{
    return ^(void(^locality)(BOOL),NSURL * _Nonnull __strong * _Nonnull videoURL){
        NSArray<DBPlayerData*>*temps = [DBPlayerDataInfo kj_checkData:kPlayerIntactName(*videoURL)];
        BOOL boo = NO;
        if (temps.count) {
            DBPlayerData *data = temps.firstObject;
            NSString *path = data.sandboxPath;
            if (data.videoIntact && [KJCacheManager kj_haveFileSandboxPath:&path]) {
                //移出以前的臨時文件
                NSString *tempPath = [path stringByAppendingPathExtension:kTempReadName];
                [[NSFileManager defaultManager] removeItemAtPath:tempPath error:NULL];
                *videoURL = [NSURL fileURLWithPath:path];
                boo = YES;
            }
        }
        kGCD_player_main(^{
            if (locality) locality(boo);
        });
    };
}
複製代碼

四、判斷地址是否可用,添加下載和播放橋樑

PLAYER_WEAKSELF;
if (!kPlayerHaveTracks(*videoURL, ^(AVURLAsset * asset) {
    if (weakself.useCacheFunction && !weakself.localityData) {
        weakself.state = KJPlayerStateBuffering;
        weakself.loadState = KJPlayerLoadStateNone;
        NSURL * tempURL = weakself.connection.kj_createSchemeURL(*videoURL);
        weakself.asset = [AVURLAsset URLAssetWithURL:tempURL options:weakself.requestHeader];
        [weakself.asset.resourceLoader setDelegate:weakself.connection queue:dispatch_get_main_queue()];
    }else{
        weakself.asset = asset;
    }
}, self.requestHeader)) {
    self.ecode = KJPlayerCustomCodeVideoURLFault;
    self.state = KJPlayerStateFailed;
    [self kj_destroyPlayer];
    return NO;
}
複製代碼

五、播放準備操做設置playerItem,而後初始化player,添加時間觀察者處理播放

self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(_timeSpace, NSEC_PER_SEC) queue:dispatch_queue_create("kj.player.time.queue", NULL) usingBlock:^(CMTime time) {
    NSTimeInterval sec = CMTimeGetSeconds(time);
    if (isnan(sec) || sec < 0) sec = 0;
    if (weakself.totalTime <= 0) return;
    if ((NSInteger)sec >= (NSInteger)weakself.totalTime) {
        [weakself.player pause];
        weakself.state = KJPlayerStatePlayFinished;
        weakself.currentTime = 0;
    }else if (weakself.userPause == NO && weakself.buffered) {
        weakself.state = KJPlayerStatePlaying;
        weakself.currentTime = sec;
    }
    if (sec > weakself.tryTime && weakself.tryTime) {
        [weakself kj_pause];
        if (!weakself.tryLooked) {
            weakself.tryLooked = YES;
            kGCD_player_main(^{
                if (weakself.tryTimeBlock) weakself.tryTimeBlock();
            });
        }
    }else{
        weakself.tryLooked = NO;
    }
}];
複製代碼

六、處理視頻狀態,kvo監聽播放器五種狀態

  • status:監聽播放器狀態
  • loadedTimeRanges:監聽播放器緩衝進度
  • presentationSize:監聽視頻尺寸
  • playbackBufferEmpty:監聽緩存不夠的狀況
  • playbackLikelyToKeepUp:監聽緩存足夠

大體流程就差很少這樣子,Demo也寫的很詳細,能夠本身去看看

5、總結思考:

在重構播放器的時候,其中碰見很多細節上面的問題,如今應該仍是存在很多的問題,也但願朋友們指出,而後我好慢慢修改完善。
我Demo寫的很詳細,感興趣的老哥能夠去下載來玩玩

Demo地址

Demo地址:KJPlayerDemo

6、文章關聯

關於播放器其餘相關文章

播放器輪子製造回顧,老哥以爲好用還請幫我點個**小星星**傳送門

相關文章
相關標籤/搜索