123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- #include "VideoPlayer.h"
- #include "CVTextureCache.h"
- #include "CMVideoSampling.h"
- #include "GlesHelper.h"
- #import <AVFoundation/AVFoundation.h>
- static void* _ObserveItemStatusContext = (void*)0x1;
- static void* _ObservePlayerItemContext = (void*)0x2;
- @implementation VideoPlayerView
- + (Class)layerClass
- {
- return [AVPlayerLayer class];
- }
- - (AVPlayer*)player
- {
- return [(AVPlayerLayer*)[self layer] player];
- }
- - (void)setPlayer:(AVPlayer*)player
- {
- [(AVPlayerLayer*)[self layer] setPlayer: player];
- }
- - (void)dealloc
- {
- self.player = nil;
- }
- @end
- @implementation VideoPlayer
- {
- AVPlayerItem* _playerItem;
- AVPlayer* _player;
- AVAssetReader* _reader;
- AVAssetReaderTrackOutput* _videoOut;
- CMSampleBufferRef _cmSampleBuffer;
- CMVideoSampling _videoSampling;
- CMTime _duration;
- CMTime _curTime;
- CMTime _curFrameTimestamp;
- CMTime _lastFrameTimestamp;
- CGSize _videoSize;
- BOOL _playerReady;
- // we need to have both because the order of asset/item getting ready is not strict
- BOOL _assetReady;
- BOOL _itemReady;
- }
- @synthesize delegate;
- @synthesize player = _player;
- - (BOOL)readyToPlay { return _playerReady; }
- - (CGSize)videoSize { return _videoSize; }
- - (CMTime)duration { return _duration; }
- - (float)durationSeconds { return CMTIME_IS_VALID(_duration) ? (float)CMTimeGetSeconds(_duration) : 0.0f; }
- + (BOOL)CanPlayToTexture:(NSURL*)url { return [url isFileURL]; }
- + (BOOL)CheckScalingModeAspectFill:(CGSize)videoSize screenSize:(CGSize)screenSize
- {
- BOOL ret = NO;
- CGFloat screenAspect = (screenSize.width / screenSize.height);
- CGFloat videoAspect = (videoSize.width / videoSize.height);
- CGFloat width = ceilf(videoSize.width * videoAspect / screenAspect);
- CGFloat height = ceilf(videoSize.height * videoAspect / screenAspect);
- // Do additional input video and device resolution aspect ratio
- // rounding check to see if the width and height values are still
- // the ~same.
- //
- // If they still match, we can change the video scaling mode from
- // aspectFit to aspectFill, this works around some off-by-one scaling
- // errors with certain screen size and video resolution combos
- //
- // TODO: Shouldn't harm to extend width/height check to
- // match values within -1..+1 range from the original
- if (videoSize.width == width && videoSize.height == height)
- {
- ret = YES;
- }
- return ret;
- }
- - (void)reportError:(NSError*)error category:(const char*)category
- {
- ::printf("[%s]Error: %s\n", category, [[error localizedDescription] UTF8String]);
- ::printf("%s\n", [[error localizedFailureReason] UTF8String]);
- [delegate onPlayerError: error];
- }
- - (void)reportErrorWithString:(const char*)error category:(const char*)category
- {
- ::printf("[%s]Error: %s\n", category, error);
- [delegate onPlayerError: nil];
- }
- - (id)init
- {
- if ((self = [super init]))
- {
- _duration = _curTime = kCMTimeZero;
- _curFrameTimestamp = _lastFrameTimestamp = kCMTimeZero;
- }
- return self;
- }
- - (void)cleanupCVTextureCache
- {
- if (_cmSampleBuffer)
- {
- CFRelease(_cmSampleBuffer);
- _cmSampleBuffer = 0;
- }
- CMVideoSampling_Uninitialize(&_videoSampling);
- }
- - (void)cleanupAssetReader
- {
- if (_reader)
- [_reader cancelReading];
- _reader = nil;
- _videoOut = nil;
- }
- - (void)cleanupPlayer
- {
- if (_player)
- {
- [[NSNotificationCenter defaultCenter] removeObserver: self name: AVAudioSessionRouteChangeNotification object: nil];
- [_player.currentItem removeObserver: self forKeyPath: @"status"];
- [_player removeObserver: self forKeyPath: @"currentItem"];
- [_player pause];
- _player = nil;
- }
- if (_playerItem)
- {
- [[NSNotificationCenter defaultCenter] removeObserver: self name: AVPlayerItemDidPlayToEndTimeNotification object: _playerItem];
- _playerItem = nil;
- }
- }
- - (void)unloadPlayer
- {
- [self cleanupCVTextureCache];
- [self cleanupAssetReader];
- [self cleanupPlayer];
- _videoSize = CGSizeMake(0, 0);
- _duration = _curTime = kCMTimeZero;
- _curFrameTimestamp = _lastFrameTimestamp = kCMTimeZero;
- self->_playerReady = self->_assetReady = self->_itemReady = NO;
- }
- - (BOOL)loadVideo:(NSURL*)url
- {
- AVURLAsset* asset = [AVURLAsset URLAssetWithURL: url options: nil];
- if (!asset)
- return NO;
- NSArray* requestedKeys = @[@"tracks", @"playable"];
- [asset loadValuesAsynchronouslyForKeys: requestedKeys completionHandler:^{
- dispatch_async(dispatch_get_main_queue(), ^{
- [self prepareAsset: asset withKeys: requestedKeys];
- });
- }];
- return YES;
- }
- - (BOOL)_playWithPrepareBlock:(BOOL (^)())preparePlaybackBlock
- {
- if (!_playerReady)
- return NO;
- if (preparePlaybackBlock && preparePlaybackBlock() == NO)
- return NO;
- // do not do seekTo and setRate here, it seems that http streaming may hang sometimes if you do so. go figure
- _curFrameTimestamp = _lastFrameTimestamp = kCMTimeZero;
- [_player play];
- return YES;
- }
- - (BOOL)playToView:(VideoPlayerView*)view
- {
- return [self _playWithPrepareBlock:^() {
- view.player = _player;
- return YES;
- }];
- }
- - (BOOL)playToTexture
- {
- return [self _playWithPrepareBlock:^() {
- return [self prepareReader];
- }];
- }
- - (BOOL)playVideoPlayer
- {
- return [self _playWithPrepareBlock: nil];
- }
- - (BOOL)isPlaying { return _playerReady && _player.rate != 0.0f; }
- - (void)pause
- {
- if (_playerReady && _player.rate != 0.0f)
- [_player pause];
- }
- - (void)resume
- {
- if (_playerReady && _player.rate == 0.0f)
- [_player play];
- }
- - (void)rewind { [self seekToTimestamp: kCMTimeZero]; }
- - (void)seekTo:(float)timeSeconds { [self seekToTimestamp: CMTimeMakeWithSeconds(timeSeconds, 1)]; }
- - (void)seekToTimestamp:(CMTime)time
- {
- [_player seekToTime: time];
- _curFrameTimestamp = _lastFrameTimestamp = time;
- }
- - (intptr_t)curFrameTexture
- {
- if (!_reader)
- return 0;
- intptr_t curTex = CMVideoSampling_LastSampledTexture(&_videoSampling);
- CMTime time = [_player currentTime];
- // if we have changed audio route and due to current category apple decided to pause playback - resume automatically
- if (_AudioRouteWasChanged && _player.rate == 0.0f)
- _player.rate = 1.0f;
- if (CMTimeCompare(time, _curTime) == 0 || _reader.status != AVAssetReaderStatusReading)
- return curTex;
- _curTime = time;
- while (_reader.status == AVAssetReaderStatusReading && CMTimeCompare(_curFrameTimestamp, _curTime) <= 0)
- {
- if (_cmSampleBuffer)
- CFRelease(_cmSampleBuffer);
- // TODO: properly handle ending
- _cmSampleBuffer = [_videoOut copyNextSampleBuffer];
- if (_cmSampleBuffer == 0)
- {
- [self cleanupCVTextureCache];
- return 0;
- }
- _curFrameTimestamp = CMSampleBufferGetPresentationTimeStamp(_cmSampleBuffer);
- }
- if (CMTimeCompare(_lastFrameTimestamp, _curFrameTimestamp) < 0)
- {
- _lastFrameTimestamp = _curFrameTimestamp;
- size_t w, h;
- curTex = CMVideoSampling_SampleBuffer(&_videoSampling, _cmSampleBuffer, &w, &h);
- _videoSize = CGSizeMake(w, h);
- }
- return curTex;
- }
- - (BOOL)setAudioVolume:(float)volume
- {
- if (!_playerReady)
- return NO;
- NSArray* audio = [_playerItem.asset tracksWithMediaType: AVMediaTypeAudio];
- NSMutableArray* params = [NSMutableArray array];
- for (AVAssetTrack* track in audio)
- {
- AVMutableAudioMixInputParameters* inputParams = [AVMutableAudioMixInputParameters audioMixInputParameters];
- [inputParams setVolume: volume atTime: kCMTimeZero];
- [inputParams setTrackID: [track trackID]];
- [params addObject: inputParams];
- }
- AVMutableAudioMix* audioMix = [AVMutableAudioMix audioMix];
- [audioMix setInputParameters: params];
- [_playerItem setAudioMix: audioMix];
- return YES;
- }
- - (void)playerItemDidReachEnd:(NSNotification*)notification
- {
- [delegate onPlayerDidFinishPlayingVideo];
- }
- static bool _AudioRouteWasChanged = false;
- - (void)audioRouteChanged:(NSNotification*)notification
- {
- _AudioRouteWasChanged = true;
- }
- - (void)observeValueForKeyPath:(NSString*)path ofObject:(id)object change:(NSDictionary*)change context:(void*)context
- {
- BOOL reportPlayerReady = NO;
- if (context == _ObserveItemStatusContext)
- {
- AVPlayerStatus status = (AVPlayerStatus)[[change objectForKey: NSKeyValueChangeNewKey] integerValue];
- switch (status)
- {
- case AVPlayerStatusUnknown:
- break;
- case AVPlayerStatusReadyToPlay:
- {
- NSArray* video = [_playerItem.asset tracksWithMediaType: AVMediaTypeVideo];
- if ([video count])
- _videoSize = [(AVAssetTrack*)[video objectAtIndex: 0] naturalSize];
- _duration = [_playerItem duration];
- _assetReady = YES;
- reportPlayerReady = _itemReady;
- }
- break;
- case AVPlayerStatusFailed:
- {
- AVPlayerItem *playerItem = (AVPlayerItem*)object;
- [self reportError: playerItem.error category: "prepareAsset"];
- }
- break;
- }
- }
- else if (context == _ObservePlayerItemContext)
- {
- if ([change objectForKey: NSKeyValueChangeNewKey] != (id)[NSNull null])
- {
- _itemReady = YES;
- reportPlayerReady = _assetReady;
- }
- }
- else
- {
- [super observeValueForKeyPath: path ofObject: object change: change context: context];
- }
- if (reportPlayerReady)
- {
- _playerReady = YES;
- [delegate onPlayerReady];
- }
- }
- - (void)prepareAsset:(AVAsset*)asset withKeys:(NSArray*)requestedKeys
- {
- // check succesful loading
- for (NSString* key in requestedKeys)
- {
- NSError* error = nil;
- AVKeyValueStatus keyStatus = [asset statusOfValueForKey: key error: &error];
- if (keyStatus == AVKeyValueStatusFailed)
- {
- [self reportError: error category: "prepareAsset"];
- return;
- }
- }
- if (!asset.playable)
- {
- [self reportErrorWithString: "Item cannot be played" category: "prepareAsset"];
- return;
- }
- if (_playerItem)
- {
- [_playerItem removeObserver: self forKeyPath: @"status"];
- [[NSNotificationCenter defaultCenter] removeObserver: self name: AVPlayerItemDidPlayToEndTimeNotification object: _playerItem];
- _playerItem = nil;
- }
- _playerItem = [AVPlayerItem playerItemWithAsset: asset];
- [_playerItem addObserver: self forKeyPath: @"status"
- options: NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
- context: _ObserveItemStatusContext
- ];
- [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(playerItemDidReachEnd:)
- name: AVPlayerItemDidPlayToEndTimeNotification object: _playerItem
- ];
- if (!_player)
- {
- _player = [AVPlayer playerWithPlayerItem: _playerItem];
- [_player addObserver: self forKeyPath: @"currentItem"
- options: NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
- context: _ObservePlayerItemContext
- ];
- [_player setAllowsExternalPlayback: NO];
- // we want to subscribe to route change notifications, for that we need audio session active
- // and in case FMOD wasnt used up to this point it is still not active
- [[AVAudioSession sharedInstance] setActive: YES error: nil];
- [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(audioRouteChanged:)
- name: AVAudioSessionRouteChangeNotification object: nil
- ];
- }
- if (_player.currentItem != _playerItem)
- [_player replaceCurrentItemWithPlayerItem: _playerItem];
- else
- [_player seekToTime: kCMTimeZero];
- }
- - (BOOL)prepareReader
- {
- if (!_playerReady)
- return NO;
- [self cleanupAssetReader];
- AVURLAsset* asset = (AVURLAsset*)_playerItem.asset;
- if (![asset.URL isFileURL])
- {
- [self reportErrorWithString: "non-file url. no video to texture." category: "prepareReader"];
- return NO;
- }
- NSError* error = nil;
- _reader = [AVAssetReader assetReaderWithAsset: _playerItem.asset error: &error];
- if (error)
- [self reportError: error category: "prepareReader"];
- _reader.timeRange = CMTimeRangeMake(kCMTimeZero, _duration);
- AVAssetTrack* videoTrack = [[_playerItem.asset tracksWithMediaType: AVMediaTypeVideo] objectAtIndex: 0];
- NSDictionary* options = @{ (NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA) };
- _videoOut = [[AVAssetReaderTrackOutput alloc] initWithTrack: videoTrack outputSettings: options];
- _videoOut.alwaysCopiesSampleData = NO;
- if (![_reader canAddOutput: _videoOut])
- {
- [self reportErrorWithString: "canAddOutput returned false" category: "prepareReader"];
- return NO;
- }
- [_reader addOutput: _videoOut];
- if (![_reader startReading])
- {
- [self reportError: [_reader error] category: "prepareReader"];
- return NO;
- }
- [self cleanupCVTextureCache];
- CMVideoSampling_Initialize(&_videoSampling);
- return YES;
- }
- @end
|