最近利用閒暇時間來重構一下以前的播放器,此次的主要升級功能以下:git
視頻支持格式:mp四、m3u八、wav、avi
音頻支持格式:midi、mp三、github
![]() |
![]() |
---|
在很早以前寫個一個播放器架子,可是這個架子只是當初簡單的實現播放處理,那麼這段時間事情很少,因此就利用閒暇時間來升級重構一下,其實網上關於播放器的輪子也很是多 ZFPlayer、KRVideoPlayer、SJVideoPlayer數據庫
這些輪子都很優秀,因此在製做的時候有借鑑參考
而後慢慢完善實現一款能夠高度自定義而且支持動態切換內核的播放器殼子,支持邊下邊播邊存,續播續傳功能,基本手勢播放等等功能
至於流媒體暫時網上也沒發現有什麼能夠實現邊下邊播邊存的方案,我其實這裏有這樣一個思路,能夠利用搭建本地服務器來處理,先將分片數據下載到本地,而後播放這樣也就只須要用戶一次網絡數據下載,等空閒時間我再來慢慢實現
目前已初步實現KJAVPlyaer、KJIJKPlayer、KJMIDIPlayer三種內核,緩存
stateDiagram-v2 播放器 --> KJBasePlayer KJBasePlayer --> KJAVPlayer KJBasePlayer --> KJIJKPlayer KJBasePlayer --> KJMIDIPlayer KJAVPlayer --> KJAVPlayer+KJCache
技術選型:項目目前主要仍是基於AVPlayer的靈活封裝使用,而後也有對Bilibili開源IJKMediaFramework的使用處理
基於AVPlayer實現邊下邊播邊存功能,目前不少網上資料基本都是基於唱吧KTVHTTPCache來實現,而本文則是採用NSURLSession
封裝的下載器,在結合文件寫入NSFileHandle
來實現,而後將信息存儲在數據庫服務器
主要就是分爲如下幾大模塊,markdown
全部播放器殼子都是基於該基礎作處理,提取公共部分網絡
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 | 中止 |
/* 當前播放器狀態 */
- (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;
複製代碼
播放器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 | 隱藏提示文字 |
只要子控件沒有涉及到手勢交互,我均採用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;
複製代碼
控件相關委託代理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;
複製代碼
枚舉文件夾和公共方法管理
主要包括兩部分,數據庫模型和增刪改查等工具
數據庫結構
dbid:惟一id,視頻連接去除scheme而後md5
videoUrl:視頻連接
saveTime:存儲時間戳
sandboxPath:沙盒地址
videoFormat:視頻格式
videoTime:視頻時間
videoData:視頻數據
複製代碼
數據庫工具
方法 | 功能 |
---|---|
kj_insertData:Data: | 插入數據,重複數據替換處理 |
kj_deleteData: | 刪除數據 |
kj_addData: | 新添加數據 |
kj_updateData:Data: | 更新數據 |
kj_checkData: | 查詢數據,傳空傳所有數據 |
kCheckAppointDatas | 指定條件查詢 |
中間橋樑做用,把網絡請求緩存到本地的臨時數據傳遞給播放器
關於這塊的詳細介紹,我單獨寫了一篇 開發播放器框架之邊下邊播邊存方案分享
工做流程:
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也寫的很詳細,能夠本身去看看
在重構播放器的時候,其中碰見很多細節上面的問題,如今應該仍是存在很多的問題,也但願朋友們指出,而後我好慢慢修改完善。
我Demo寫的很詳細,感興趣的老哥能夠去下載來玩玩
Demo地址:KJPlayerDemo