SceneKIt+ AVFoundation 打造VR播放器(1)

下面是我寫的播放器

支持VR,全景,視頻縮放购公,本地萌京,網(wǎng)絡(luò)視頻播放,實時獲取視頻幀宏浩,獲取播放時間知残,獲取緩存時間,播放比庄,暫停


2017-06-22 17_47_06.gif

要想完成一個Vr播放器求妹,需要完成兩個功能

1、寫一個可以實時獲取視頻幀的播放器
2佳窑、寫一個可以渲染每一幀圖片為全景圖片的view

SCN3DVideoAdatper 視頻播放器

用于解碼視頻的每一幀圖片
使用的是<AVFoundation/AVFoundation.h>框架

下面是一些相關(guān)的方法

//
//  SCN3DVideoAdatper.h
//  SCN3DPlayer
//
//  Created by 俞濤濤 on 16/11/11.
//  Copyright ? 2016年 俞濤濤. All rights reserved.
//

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


/////////////////////////////////////////////////////////////////////////////////////////////////////////

@class SCN3DVideoAdatper;

@protocol SCN3DVideoAdatperDelegate <NSObject>

@optional

/**
 準(zhǔn)備播放視頻
 實現(xiàn)SCN3DVideoAdatperDelegate協(xié)議最先調(diào)用該方法

 @param videoAdatper SCN3DVideoAdatper對象

 */
- (void)videoPlayerIsReadyToPlayVideo:(SCN3DVideoAdatper *)videoAdatper;

/**
 播放視頻結(jié)束
 實現(xiàn)SCN3DVideoAdatperDelegate協(xié)議 在視頻播放結(jié)束以后調(diào)用該方法

 @param videoAdatper SCN3DVideoAdatper
 */
- (void)videoPlayerDidReachEnd:(SCN3DVideoAdatper *)videoAdatper;

/**
 播放時間監(jiān)聽
 實現(xiàn)SCN3DVideoAdatperDelegate協(xié)議 在視頻播放時會返回當(dāng)前播放的時間

 @param videoAdatper SCN3DVideoAdatper 對象
 @param cmTime  CMTime
 */
- (void)videoPlayer:(SCN3DVideoAdatper *)videoAdatper timeDidChange:(CMTime)cmTime;

/**
 播放已加載的緩存時間監(jiān)聽
 實現(xiàn)SCN3DVideoAdatperDelegate協(xié)議 在視頻播放的時候會返回當(dāng)前已加載的視頻緩存百分比
 @param videoAdatper SCN3DVideoAdatper 對象
 @param duration float
 */
- (void)videoPlayer:(SCN3DVideoAdatper *)videoAdatper loadedTimeRangeDidChange:(float)duration;

/**
 播放錯誤監(jiān)聽
 實現(xiàn)SCN3DVideoAdatperDelegate協(xié)議 在播放視頻失敗的時候會調(diào)用該方法

 @param videoAdatper SCN3DVideoAdatper 對象
 @param error  NSError 對象
 */
- (void)videoPlayer:(SCN3DVideoAdatper *)videoAdatper didFailWithError:(NSError *)error;

/**
 獲取視頻的每一幀
 實現(xiàn)SCN3DVideoAdatperDelegate協(xié)議 在視頻播放的時候制恍,該方法可以得到視頻得每一幀圖片

 @param videoAdatper SCN3DVideoAdatper 對象
 @param videoImage UIImage 對象
 */
- (void)videoPlayer:(SCN3DVideoAdatper *)videoAdatper displaylinkCallbackImage:(UIImage *)videoImage;

@end

/////////////////////////////////////////////////////////////////////////////////////////////////////////

@interface SCN3DVideoAdatper : NSObject

@property (nonatomic, weak) id<SCN3DVideoAdatperDelegate> delegate;
@property (nonatomic, strong, readonly) AVPlayer     *player;
@property (nonatomic, strong, readonly) AVPlayerItem *playerItem;
@property (nonatomic, strong, readonly) AVPlayerItemVideoOutput *output;
@property (nonatomic, assign, getter=isPlaying, readonly) BOOL playing;
@property (nonatomic, assign, getter=isLooping) BOOL looping;
@property (nonatomic, assign, getter=isMuted) BOOL muted;

// Setting

- (void)setURL:(NSURL *)URL;
- (void)setPlayerItem:(AVPlayerItem *)playerItem;
- (void)setAsset:(AVAsset *)asset;


// 開始播放
- (void)play;
//暫停播放
- (void)pause;
//重置播放器
- (void)reset;

/**
 跳轉(zhuǎn)到相應(yīng)的時間段
 在相應(yīng)的時間段里面播放

 @param time 傳入一個float 得參數(shù),
 @param completion completion description
 */
- (void)seekToTime:(float)time completion:(void (^)())completion;

//設(shè)置音量大小
- (void)setVolume:(float)volume;
//增加音量
- (void)fadeInVolume;
//降低音量
- (void)fadeOutVolume;

// 添加 Displaylink
- (void)addDisplaylink;
// 移除  Displaylink
- (void)removeDisplaylink;

@end

下面講一下主要方法實現(xiàn)

添加移除播放器監(jiān)聽

/////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Player Observers
/////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)addPlayerObservers {
    [self.player addObserver:self
                  forKeyPath:NSStringFromSelector(@selector(rate))
                     options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                     context:VideoPlayer_PlayerRateChangedContext];
}

- (void)removePlayerObservers {
    @try {
        [self.player removeObserver:self
                         forKeyPath:NSStringFromSelector(@selector(rate))
                            context:VideoPlayer_PlayerRateChangedContext];
    }
    @catch (NSException *exception) {
        NSLog(@"Exception removing observer: %@", exception);
    }
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - PlayerItem Observers
/////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)addPlayerItemObservers:(AVPlayerItem *)playerItem {
    [playerItem addObserver:self
                 forKeyPath:NSStringFromSelector(@selector(status))
                    options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                    context:VideoPlayer_PlayerItemStatusContext];
    
    [playerItem addObserver:self
                 forKeyPath:NSStringFromSelector(@selector(loadedTimeRanges))
                    options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                    context:VideoPlayer_PlayerItemLoadedTimeRangesContext];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playerItemDidPlayToEndTime:)
                                                 name:AVPlayerItemDidPlayToEndTimeNotification
                                               object:playerItem];
}

- (void)removePlayerItemObservers:(AVPlayerItem *)playerItem {
    [playerItem cancelPendingSeeks];
    @try {
        [playerItem removeObserver:self
                        forKeyPath:NSStringFromSelector(@selector(status))
                           context:VideoPlayer_PlayerItemStatusContext];
    }
    @catch (NSException *exception) {
        NSLog(@"Exception removing observer: %@", exception);
    }
    
    @try {
        [playerItem removeObserver:self
                        forKeyPath:NSStringFromSelector(@selector(loadedTimeRanges))
                           context:VideoPlayer_PlayerItemLoadedTimeRangesContext];
    }
    @catch (NSException *exception) {
        NSLog(@"Exception removing observer: %@", exception);
    }
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Time Observer
/////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)addTimeObserver {
    if (self.timeObserverToken || self.player == nil) {
        return;
    }
    
    __weak typeof (self) weakSelf = self;
    self.timeObserverToken = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(TimeObserverInterval, NSEC_PER_SEC)
                                                                       queue:dispatch_get_main_queue()
                                                                  usingBlock:^(CMTime time)
    {
        __strong typeof (self) strongSelf = weakSelf;
        if (!strongSelf) {
            return;
        }
        if ([strongSelf.delegate respondsToSelector:@selector(videoPlayer:timeDidChange:)]) {
            [strongSelf.delegate videoPlayer:strongSelf timeDidChange:time];
        }
    }];
}

判斷視頻是否播放結(jié)束和獲取視頻緩存時間

- (BOOL)isAtEndTime { // TODO: this is a fucked up override, seems like something could be wrong [AH]
    if (self.player && self.player.currentItem) {
        if (_isAtEndTime) {
            return _isAtEndTime;
        }
        
        float currentTime = 0.0f;
        if (CMTIME_IS_INVALID(self.player.currentTime) == NO) {
            currentTime = CMTimeGetSeconds(self.player.currentTime);
        }
        
        float videoDuration = 0.0f;
        if (CMTIME_IS_INVALID(self.player.currentItem.duration) == NO) {
            videoDuration = CMTimeGetSeconds(self.player.currentItem.duration);
        }
        
        if (currentTime > 0.0f && videoDuration > 0.0f) {
            if (fabs(currentTime - videoDuration) <= 0.01f) {
                return YES;
            }
        }
    }
    return NO;
}
//視頻緩存時間
- (float)calcLoadedDuration {
    float loadedDuration = 0.0f;
    if (self.player && self.player.currentItem) {
        NSArray *loadedTimeRanges = self.player.currentItem.loadedTimeRanges;
        
        if (loadedTimeRanges && [loadedTimeRanges count]) {
            CMTimeRange timeRange = [[loadedTimeRanges firstObject] CMTimeRangeValue];
            float startSeconds = CMTimeGetSeconds(timeRange.start);
            float durationSeconds = CMTimeGetSeconds(timeRange.duration);
            loadedDuration = startSeconds + durationSeconds;
        }
    }
    return loadedDuration;
}

實時獲取視頻每一幀


- (void)addDisplaylink {
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)removeDisplaylink {
    if (self.displayLink) {
        [self.displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        self.displayLink = nil;
    }
}
- (void)displayLinkCallback:(CADisplayLink *)sender {
    @autoreleasepool {
        CMTime outputItemTime = [self.output itemTimeForHostTime:CACurrentMediaTime()];
        if([self.output hasNewPixelBufferForItemTime:outputItemTime]) {
            CVPixelBufferRef bufferRef = [self.output copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL];
        
            if (bufferRef != nil) {
                UIImage *videoImage = [self pixelBufferToImage:bufferRef];
                if ([self.delegate respondsToSelector:@selector(videoPlayer:displaylinkCallbackImage:)]) {
                    [self.delegate videoPlayer:self displaylinkCallbackImage:videoImage];
                }
                CFRelease(bufferRef);
            }
        }
    }

}

- (UIImage *)pixelBufferToImage:(CVPixelBufferRef)bufferRef {
    CIImage   *ciImage     = [CIImage imageWithCVPixelBuffer:bufferRef];
    CIContext *tempContext = [CIContext contextWithOptions:nil];
    CGFloat    videoWidth  = CVPixelBufferGetWidth(bufferRef);
    CGFloat    videoHeight = CVPixelBufferGetHeight(bufferRef);
    CGImageRef videoImage  = [tempContext createCGImage:ciImage fromRect:CGRectMake(0, 0, videoWidth, videoHeight)];
    
    UIImage *image = [UIImage imageWithCGImage:videoImage];
    CGImageRelease(videoImage);
    return image;
}

音量相關(guān)設(shè)置

- (void)setVolume:(float)volume {
    [self cancelFadeVolume];
    self.player.volume = volume;
}

- (void)cancelFadeVolume {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeInVolume) object:nil];
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeOutVolume) object:nil];
}

- (void)fadeInVolume {
    if (self.player == nil) {
        return;
    }
    [self cancelFadeVolume];
    
    if (self.player.volume >= 1.0f - 0.01f) {
        self.player.volume = 1.0f;
    }
    else {
        self.player.volume += 1.0f/10.0f;
        [self performSelector:@selector(fadeInVolume) withObject:nil afterDelay:DefaultVolumeFadeDuration/10.0f];
    }
}

- (void)fadeOutVolume {
    if (self.player == nil) {
        return;
    }
    [self cancelFadeVolume];
    
    if (self.player.volume <= 0.01f) {
        self.player.volume = 0.0f;
    }
    else {
        self.player.volume -= 1.0f/10.0f;
        [self performSelector:@selector(fadeOutVolume) withObject:nil afterDelay:DefaultVolumeFadeDuration/10.0f];
    }
}

源代碼下載
如果喜歡的話神凑,就點個贊净神,star一下,本文里面有誤的地方溉委,請大家指教

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鹃唯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瓣喊,更是在濱河造成了極大的恐慌坡慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藻三,死亡現(xiàn)場離奇詭異洪橘,居然都是意外死亡,警方通過查閱死者的電腦和手機棵帽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門熄求,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岖寞,你說我怎么就攤上這事抡四。” “怎么了仗谆?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵指巡,是天一觀的道長。 經(jīng)常有香客問我隶垮,道長藻雪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任狸吞,我火速辦了婚禮勉耀,結(jié)果婚禮上指煎,老公的妹妹穿的比我還像新娘。我一直安慰自己便斥,他們只是感情好至壤,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著枢纠,像睡著了一般像街。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晋渺,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天镰绎,我揣著相機與錄音,去河邊找鬼木西。 笑死畴栖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的八千。 我是一名探鬼主播吗讶,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼叼丑!你這毒婦竟也來了关翎?” 一聲冷哼從身側(cè)響起扛门,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤鸠信,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后论寨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體星立,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年葬凳,在試婚紗的時候發(fā)現(xiàn)自己被綠了绰垂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡火焰,死狀恐怖劲装,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昌简,我是刑警寧澤占业,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站纯赎,受9級特大地震影響谦疾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜犬金,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一念恍、第九天 我趴在偏房一處隱蔽的房頂上張望六剥。 院中可真熱鬧,春花似錦峰伙、人聲如沸疗疟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秃嗜。三九已至,卻和暖如春顿膨,著一層夾襖步出監(jiān)牢的瞬間锅锨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工恋沃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留必搞,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓囊咏,卻偏偏與公主長得像恕洲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子梅割,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容