概述
ijkplayer 是Bilibili开发并开源的轻量级视频播放器,支持本地网络视频播放以及流媒体播放,支持iOS和Android平台。ijkplayer基于 FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。 FFmpeg 采用LGPL或GPL许可证,提供了录制、转换以及流化音视频的完整解决方案,包括了领先的音、视频编码库libavcodec等。这篇文章的主要目的是介绍ijkplayer中IJKMPMoviePlayerController
和IJKAVMoviePlayerController
。之所以放在一起是因为它们的底层都是调用系统的播放器接口,因此源码相对IJKFFMoviePlayerController
来说比较简单。
特性
platform | version | CPU| video-output|audio-output|hw-decoder
:---:|:---:|:---:|:---:|:---:
iOS | iOS 7.0+ | armv7, arm64, i386, x86_64|OpenGL ES 2.0|AudioQueue, AudioUnit|VideoToolbox (iOS 8+)
Android | API 9+ | ARMv7a, ARM64v8a, x86 |NativeWindow, OpenGL ES 2.0|AudioTrack, OpenSL ES|MediaCodec (API 16+, Android 4.1+)
播放效果
IJKMPMoviePlayerController
IJKMPMoviePlayerController 继承自MPMoviePlayerController实现了IJKMediaPlayback协议。通过实现 IJKMediaPlayback 协议,虽然每个播放器的底层实现不同,但是可以提供一套统一的播放接口。MPMoviePlayerController支持本地视频和网络视频的播放,它实现了MPMediaPlayback协议,因此具备一般的播放器控制功能,例如播放、暂停、停止等。但是 MPMediaPlayerController自身并不是一个完整的视图控制器,如果要在UI中展示视频需要将view属性添加到界面中。
- 初始化。URL可以是本地视频的URL,也可以是网络视频的URL。
- (id)initWithContentURL:(NSURL *)aUrl;
- (id)initWithContentURLString:(NSString *)aUrl;
// URL初始化
- (id)initWithContentURL:(NSURL *)aUrl
{
self = [super initWithContentURL:aUrl];
if (self) {
self.scalingMode = MPMovieScalingModeAspectFit;
self.shouldAutoplay = YES;
_notificationManager = [[IJKNotificationManager alloc] init];
[self IJK_installMovieNotificationObservers];
[[IJKAudioKit sharedInstance] setupAudioSession];
_bufferingProgress = -1;
}
return self;
}
// 路径初始化
- (id)initWithContentURLString:(NSString *)aUrl
{
NSURL *url;
// 判断是否为文件
if ([aUrl rangeOfString:@"/"].location == 0) {
//构建本地URL
url = [NSURL fileURLWithPath:aUrl];
}
else {
url = [NSURL URLWithString:aUrl];
}
self = [self initWithContentURL:url];
if (self) {
}
return self;
}
- 相关方法。方法更多的是对MPMoviePlayerController的封装。
- (BOOL)isPlaying
{
switch (self.playbackState) {
case MPMoviePlaybackStatePlaying:
return YES;
default:
return NO;
}
}
- (void)shutdown
{
// do nothing
}
-(int64_t)numberOfBytesTransferred
{
NSArray *events = self.accessLog.events;
if (events.count>0) {
MPMovieAccessLogEvent *currentEvent = [events objectAtIndex:events.count -1];
return currentEvent.numberOfBytesTransferred;
}
return 0;
}
- (UIImage *)thumbnailImageAtCurrentTime
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [super thumbnailImageAtTime:self.currentPlaybackTime timeOption:MPMovieTimeOptionExact];
#pragma clang diagnostic pop
}
- 相关通知
// 做好播放准备后
IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
// 媒体播放完成或用户手动退出,具体完成原因可以通过通知userInfo中的key为IJKMPMoviePlayerPlaybackDidFinishReasonUserInfoKey的对象获取
IJKMPMoviePlayerPlaybackDidFinishNotification
// 播放状态改变
IJKMPMoviePlayerPlaybackStateDidChangeNotification
// 媒体网络加载状态改变
IJKMPMoviePlayerLoadStateDidChangeNotification
// 当媒体开始通过AirPlay播放或者结束AirPlay播放
IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification
// 获取了媒体的实际尺寸
IJKMPMovieNaturalSizeAvailableNotification
IJKAVMoviePlayerController
IJKAVMoviePlayerController 是对 AVPlayer 的封装。IJKAVMoviePlayerController 相比 IJKMPMoviePlayerController 要复杂些,MPMoviePlayerController 提供的播放器具有高度的封装性,使得自定义播放器变的很难。如果需要自定义播放器样式的时候,一般使用 AVPlayer。AVPlayer 存在于 AVFoundtion 中,更接近于底层,也更加灵活。
- 初始化。这里需要注意的是在初始化的时候并没有初始化 AVPlayer,只是初始化相关的实例变量。
- (id)initWithContentURL:(NSURL *)aUrl;
- (id)initWithContentURLString:(NSString *)aUrl;
// 根据URL初始化
- (id)initWithContentURL:(NSURL *)aUrl
{
self = [super init];
if (self != nil) {
self.scalingMode = IJKMPMovieScalingModeAspectFit;
self.shouldAutoplay = NO;
_playUrl = aUrl;
// 初始化播放视图
_avView = [[IJKAVPlayerLayerView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view = _avView;
// TODO:
[[IJKAudioKit sharedInstance] setupAudioSession];
_isPrerolling = NO;
_isSeeking = NO;
_isError = NO;
_isCompleted = NO;
self.bufferingProgress = 0;
_playbackLikelyToKeeyUp = NO;
_playbackBufferEmpty = YES;
_playbackBufferFull = NO;
_playbackRate = 1.0f;
_playbackVolume = 1.0f;
// init extra
[self setScreenOn:YES];
_notificationManager = [[IJKNotificationManager alloc] init];
}
return self;
}
// 根据路径初始化
- (id)initWithContentURLString:(NSString *)aUrl
{
NSURL *url;
if (aUrl == nil) {
aUrl = @"";
}
if ([aUrl rangeOfString:@"/"].location == 0) {
//本地
url = [NSURL fileURLWithPath:aUrl];
}
else {
url = [NSURL URLWithString:aUrl];
}
self = [self initWithContentURL:url];
if (self != nil) {
}
return self;
}
- 异步加载。由于多媒体文件一般比较大,获取或计算出Asset中的属性非常耗时,Apple对Asset的属性采用了懒惰加载模式。在创建AVAsset的时候,只生成一个实例,并不初始化属性。只有当第一次访问属性时,系统才会根据多媒体中的数据初始化这个属性。由于不用同时加载所有属性,耗时问题得到了一定缓解。但是属性加载在计算量比较大的时候仍旧可能会阻塞线程。为了解决这个问题,AVFoundation提供了AVAsynchronousKeyValueLoading协议,可以异步加载属性:
@interface AVMetadataItem (AVAsynchronousKeyValueLoading)
// 异步加载属性,通过keys传入要加载的key数组,在handler中做加载完成的操作。
- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key error:(NSError * _Nullable * _Nullable)outError NS_AVAILABLE(10_7, 4_2);
// 获得属性的加载状态,如果是AVKeyValueStatusLoaded状态,表示已经加载完成。
- (void)loadValuesAsynchronouslyForKeys:(NSArray<NSString *> *)keys completionHandler:(nullable void (^)(void))handler NS_AVAILABLE(10_7, 4_2);
@end
- 相关方法。IJKAVMoviePlayerController的方法比较多,在这里主要关注IJKAVMoviePlayerController播放器从初始化到播放的整体流程:
1、根据初始化的URL构建AVURLAsset对象;
2、异步加载获取视频相关属性;
3、加载完成后初始化AVPlayerItem,并监听它的相关属性;
4、初始化AVPlayer,并监听它的相关属性;
5、当状态为AVPlayerItemStatusReadyToPlay的时候发送相关通知,如果开启了自动播放则自动播放。如果没有开启自动播放,我们可以监听IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification这个通知,收到通知后再去播放。
// 预加载,异步加载相关属性
- (void)prepareToPlay
{
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:_playUrl options:nil];
NSLog(@"%@", asset);
NSArray *requestedKeys = @[@"playable"];
_playAsset = asset;
// 异步加载属性
[asset loadValuesAsynchronouslyForKeys:requestedKeys
completionHandler:^{
dispatch_async( dispatch_get_main_queue(), ^{
[self didPrepareToPlayAsset:asset withKeys:requestedKeys];
[[NSNotificationCenter defaultCenter]
postNotificationName:IJKMPMovieNaturalSizeAvailableNotification
object:self];
});
}];
}
// 异步加载完后,监听相关通知、属性以及初始化AVPlayer
- (void)didPrepareToPlayAsset:(AVURLAsset *)asset withKeys:(NSArray *)requestedKeys
{
if (_isShutdown)
return;
/* Make sure that the value of each key has loaded successfully. */
for (NSString *thisKey in requestedKeys)
{
NSError *error = nil;
AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error];
if (keyStatus == AVKeyValueStatusFailed)
{
[self assetFailedToPrepareForPlayback:error];
return;
} else if (keyStatus == AVKeyValueStatusCancelled) {
// TODO [AVAsset cancelLoading]
error = [self createErrorWithCode:kEC_PlayerItemCancelled
description:@"player item cancelled"
reason:nil];
[self assetFailedToPrepareForPlayback:error];
return;
}
}
/* Use the AVAsset playable property to detect whether the asset can be played. */
if (!asset.playable)
{
NSError *assetCannotBePlayedError = [NSError errorWithDomain:@"AVMoviePlayer"
code:0
userInfo:nil];
[self assetFailedToPrepareForPlayback:assetCannotBePlayedError];
return;
}
/* At this point we're ready to set up for playback of the asset. */
/* Stop observing our prior AVPlayerItem, if we have one. */
[_playerItemKVO safelyRemoveAllObservers];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:nil
object:_playerItem];
/* Create a new instance of AVPlayerItem from the now successfully loaded AVAsset. */
_playerItem = [AVPlayerItem playerItemWithAsset:asset];
_playerItemKVO = [[IJKKVOController alloc] initWithTarget:_playerItem];
[self registerApplicationObservers];
/* Observe the player item "status" key to determine when it is ready to play. */
// 监听AVPlayer的状态,比较重要
[_playerItemKVO safelyAddObserver:self
forKeyPath:@"status"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayerItem_state];
[_playerItemKVO safelyAddObserver:self
forKeyPath:@"loadedTimeRanges"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayerItem_loadedTimeRanges];
[_playerItemKVO safelyAddObserver:self
forKeyPath:@"playbackLikelyToKeepUp"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayerItem_playbackLikelyToKeepUp];
[_playerItemKVO safelyAddObserver:self
forKeyPath:@"playbackBufferEmpty"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayerItem_playbackBufferEmpty];
[_playerItemKVO safelyAddObserver:self
forKeyPath:@"playbackBufferFull"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayerItem_playbackBufferFull];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:_playerItem];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemFailedToPlayToEndTime:)
name:AVPlayerItemFailedToPlayToEndTimeNotification
object:_playerItem];
_isCompleted = NO;
/* Create new player, if we don't already have one. */
if (!_player)
{
/* Get a new AVPlayer initialized to play the specified player item. */
_player = [AVPlayer playerWithPlayerItem:_playerItem];
_playerKVO = [[IJKKVOController alloc] initWithTarget:_player];
NSLog(@"%@", _player);
/* Observe the AVPlayer "currentItem" property to find out when any
AVPlayer replaceCurrentItemWithPlayerItem: replacement will/did
occur.*/
[_playerKVO safelyAddObserver:self
forKeyPath:@"currentItem"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayer_currentItem];
/* Observe the AVPlayer "rate" property to update the scrubber control. */
[_playerKVO safelyAddObserver:self
forKeyPath:@"rate"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayer_rate];
[_playerKVO safelyAddObserver:self
forKeyPath:@"airPlayVideoActive"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:KVO_AVPlayer_airplay];
}
/* Make our new AVPlayerItem the AVPlayer's current item. */
if (_player.currentItem != _playerItem)
{
/* Replace the player item with a new player item. The item replacement occurs
asynchronously; observe the currentItem property to find out when the
replacement will/did occur
If needed, configure player item here (example: adding outputs, setting text style rules,
selecting media options) before associating it with a player
*/
[_player replaceCurrentItemWithPlayerItem:_playerItem];
// TODO: notify state change
}
// TODO: set time to 0;
}
// 监听到相关状态改变的时候做进一步处理,并且发送相关通知
- (void)observeValueForKeyPath:(NSString*)path
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context
{
if (_isShutdown)
return;
if (context == KVO_AVPlayerItem_state)
{
/* AVPlayerItem "status" property value observer. */
AVPlayerItemStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
switch (status)
{
case AVPlayerItemStatusUnknown:
{
/* Indicates that the status of the player is not yet known because
it has not tried to load new media resources for playback */
}
break;
// 准备播放
case AVPlayerItemStatusReadyToPlay:
{
/* Once the AVPlayerItem becomes ready to play, i.e.
[playerItem status] == AVPlayerItemStatusReadyToPlay,
its duration can be fetched from the item. */
dispatch_once(&_readyToPlayToken, ^{
[_avView setPlayer:_player];
self.isPreparedToPlay = YES;
AVPlayerItem *playerItem = (AVPlayerItem *)object;
NSTimeInterval duration = CMTimeGetSeconds(playerItem.duration);
if (duration <= 0)
self.duration = 0.0f;
else
self.duration = duration;
[[NSNotificationCenter defaultCenter]
postNotificationName:IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
object:self];
// 如果自动播放,且应用程序为激活状态则自动播放
if (_shouldAutoplay && (!_pauseInBackground || [UIApplication sharedApplication].applicationState == UIApplicationStateActive))
[_player play];
});
}
break;
// 播放准备失败
case AVPlayerItemStatusFailed:
{
AVPlayerItem *playerItem = (AVPlayerItem *)object;
[self assetFailedToPrepareForPlayback:playerItem.error];
}
break;
}
[self didPlaybackStateChange];
[self didLoadStateChange];
}
else if (context == KVO_AVPlayerItem_loadedTimeRanges)
{
AVPlayerItem *playerItem = (AVPlayerItem *)object;
if (_player != nil && playerItem.status == AVPlayerItemStatusReadyToPlay) {
NSArray *timeRangeArray = playerItem.loadedTimeRanges;
CMTime currentTime = [_player currentTime];
BOOL foundRange = NO;
CMTimeRange aTimeRange = {0};
if (timeRangeArray.count) {
aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue];
if(CMTimeRangeContainsTime(aTimeRange, currentTime)) {
foundRange = YES;
}
}
if (foundRange) {
CMTime maxTime = CMTimeRangeGetEnd(aTimeRange);
NSTimeInterval playableDuration = CMTimeGetSeconds(maxTime);
if (playableDuration > 0) {
self.playableDuration = playableDuration;
[self didPlayableDurationUpdate];
}
}
}
else
{
self.playableDuration = 0;
}
}
else if (context == KVO_AVPlayerItem_playbackLikelyToKeepUp) {
AVPlayerItem *playerItem = (AVPlayerItem *)object;
NSLog(@"KVO_AVPlayerItem_playbackLikelyToKeepUp: %@\n", playerItem.isPlaybackLikelyToKeepUp ? @"YES" : @"NO");
[self fetchLoadStateFromItem:playerItem];
[self didLoadStateChange];
}
else if (context == KVO_AVPlayerItem_playbackBufferEmpty) {
AVPlayerItem *playerItem = (AVPlayerItem *)object;
BOOL isPlaybackBufferEmpty = playerItem.isPlaybackBufferEmpty;
NSLog(@"KVO_AVPlayerItem_playbackBufferEmpty: %@\n", isPlaybackBufferEmpty ? @"YES" : @"NO");
if (isPlaybackBufferEmpty)
_isPrerolling = YES;
[self fetchLoadStateFromItem:playerItem];
[self didLoadStateChange];
}
else if (context == KVO_AVPlayerItem_playbackBufferFull) {
AVPlayerItem *playerItem = (AVPlayerItem *)object;
NSLog(@"KVO_AVPlayerItem_playbackBufferFull: %@\n", playerItem.isPlaybackBufferFull ? @"YES" : @"NO");
[self fetchLoadStateFromItem:playerItem];
[self didLoadStateChange];
}
else if (context == KVO_AVPlayer_rate)
{
if (_player != nil && !isFloatZero(_player.rate))
_isPrerolling = NO;
/* AVPlayer "rate" property value observer. */
[self didPlaybackStateChange];
[self didLoadStateChange];
}
else if (context == KVO_AVPlayer_currentItem)
{
_isPrerolling = NO;
/* AVPlayer "currentItem" property observer.
Called when the AVPlayer replaceCurrentItemWithPlayerItem:
replacement will/did occur. */
AVPlayerItem *newPlayerItem = [change objectForKey:NSKeyValueChangeNewKey];
/* Is the new player item null? */
if (newPlayerItem == (id)[NSNull null])
{
NSError *error = [self createErrorWithCode:kEC_CurrentPlayerItemIsNil
description:@"current player item is nil"
reason:nil];
[self assetFailedToPrepareForPlayback:error];
}
else /* Replacement of player currentItem has occurred */
{
[_avView setPlayer:_player];
[self didPlaybackStateChange];
[self didLoadStateChange];
}
}
else if (context == KVO_AVPlayer_airplay)
{
[[NSNotificationCenter defaultCenter] postNotificationName:IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification object:nil userInfo:nil];
}
else
{
[super observeValueForKeyPath:path ofObject:object change:change context:context];
}
}
// 播放音视频
- (void)play
{
if (_isCompleted)
{
_isCompleted = NO;
[_player seekToTime:kCMTimeZero];
}
[_player play];
}
// 生成截图
- (UIImage *)thumbnailImageAtCurrentTime
{
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:_playAsset];
NSError *error = nil;
CMTime time = CMTimeMakeWithSeconds(self.currentPlaybackTime, 1);
CMTime actualTime;
CGImageRef cgImage = [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *image = [UIImage imageWithCGImage:cgImage];
return image;
}
// 定位到新的播放时间
- (void)setCurrentPlaybackTime:(NSTimeInterval)aCurrentPlaybackTime
{
if (!_player)
return;
_seekingTime = aCurrentPlaybackTime;
_isSeeking = YES;
_bufferingProgress = 0;
[self didPlaybackStateChange];
[self didLoadStateChange];
if (_isPrerolling) {
[_player pause];
}
[_player seekToTime:CMTimeMakeWithSeconds(aCurrentPlaybackTime, NSEC_PER_SEC)
completionHandler:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
_isSeeking = NO;
if (_isPrerolling) {
[_player play];
}
[self didPlaybackStateChange];
[self didLoadStateChange];
});
}];
}
- 相关通知
// 做好播放准备后
IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
// 媒体播放完成或用户手动退出,具体完成原因可以通过通知userInfo中的key为IJKMPMoviePlayerPlaybackDidFinishReasonUserInfoKey的对象获取
IJKMPMoviePlayerPlaybackDidFinishNotification
// 播放状态改变
IJKMPMoviePlayerPlaybackStateDidChangeNotification
// 媒体网络加载状态改变
IJKMPMoviePlayerLoadStateDidChangeNotification
// 当媒体开始通过AirPlay播放或者结束AirPlay播放
IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification
// 获取了媒体的实际尺寸
IJKMPMovieNaturalSizeAvailableNotification
- 播放器使用
- (void)setupMPPlayer
{
_mpPlayer = [[IJKMPMoviePlayerController alloc] initWithContentURLString:[[NSBundle mainBundle] pathForResource:@"1" ofType:@"mp4"]];
_mpPlayer.scalingMode = IJKMPMovieScalingModeAspectFit;
_mpPlayer.view.frame = self.view.bounds;
[self.view addSubview:_mpPlayer.view];
[_mpPlayer prepareToPlay];
}
- (void)setupAVPlayer
{
_avPlayer = [[IJKAVMoviePlayerController alloc] initWithContentURLString:[[NSBundle mainBundle] pathForResource:@"1" ofType:@"mp4"]];
[self.view addSubview:_avPlayer.view];
[_avPlayer setShouldAutoplay:YES];
[_avPlayer prepareToPlay];
}
总结
ijkplayer 中的 IJKMPMoviePlayerController 底层由 MPMoviePlayerController 实现,由于它具有高度的封装性。因此,二次封装的时候比较简单,可定制化程度低。IJKAVMoviePlayerController 底层通过 AVPlayer 实现,更加灵活,可定程度高,二次封装相对比较困难。如果希望了解如何定制 AVPlayer,读一读IJKAVMoviePlayerController 的源码是个不错的选择。