概述
ijkplayer 是Bilibili開(kāi)發(fā)并開(kāi)源的輕量級(jí)視頻播放器,支持本地網(wǎng)絡(luò)視頻播放以及流媒體播放,支持iOS和Android平臺(tái)。ijkplayer基于 FFmpeg 是一套可以用來(lái)記錄、轉(zhuǎn)換數(shù)字音頻、視頻,并能將其轉(zhuǎn)化為流的開(kāi)源計(jì)算機(jī)程序。 FFmpeg 采用LGPL或GPL許可證,提供了錄制、轉(zhuǎn)換以及流化音視頻的完整解決方案,包括了領(lǐng)先的音、視頻編碼庫(kù)libavcodec等。這篇文章的主要目的是介紹ijkplayer中IJKMPMoviePlayerController和IJKAVMoviePlayerController。之所以放在一起是因?yàn)樗鼈兊牡讓佣际钦{(diào)用系統(tǒng)的播放器接口,因此源碼相對(duì)IJKFFMoviePlayerController來(lái)說(shuō)比較簡(jiǎn)單。
特性
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實(shí)現(xiàn)了IJKMediaPlayback協(xié)議。通過(guò)實(shí)現(xiàn) IJKMediaPlayback 協(xié)議,雖然每個(gè)播放器的底層實(shí)現(xiàn)不同,但是可以提供一套統(tǒng)一的播放接口。MPMoviePlayerController支持本地視頻和網(wǎng)絡(luò)視頻的播放,它實(shí)現(xiàn)了MPMediaPlayback協(xié)議,因此具備一般的播放器控制功能,例如播放、暫停、停止等。但是 MPMediaPlayerController自身并不是一個(gè)完整的視圖控制器,如果要在UI中展示視頻需要將view屬性添加到界面中。
- 初始化。URL可以是本地視頻的URL,也可以是網(wǎng)絡(luò)視頻的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) {
//構(gòu)建本地URL
url = [NSURL fileURLWithPath:aUrl];
}
else {
url = [NSURL URLWithString:aUrl];
}
self = [self initWithContentURL:url];
if (self) {
}
return self;
}
- 相關(guān)方法。方法更多的是對(duì)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
}
- 相關(guān)通知
// 做好播放準(zhǔn)備后
IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
// 媒體播放完成或用戶手動(dòng)退出,具體完成原因可以通過(guò)通知userInfo中的key為IJKMPMoviePlayerPlaybackDidFinishReasonUserInfoKey的對(duì)象獲取
IJKMPMoviePlayerPlaybackDidFinishNotification
// 播放狀態(tài)改變
IJKMPMoviePlayerPlaybackStateDidChangeNotification
// 媒體網(wǎng)絡(luò)加載狀態(tài)改變
IJKMPMoviePlayerLoadStateDidChangeNotification
// 當(dāng)媒體開(kāi)始通過(guò)AirPlay播放或者結(jié)束AirPlay播放
IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification
// 獲取了媒體的實(shí)際尺寸
IJKMPMovieNaturalSizeAvailableNotification
IJKAVMoviePlayerController
IJKAVMoviePlayerController 是對(duì) AVPlayer 的封裝。IJKAVMoviePlayerController 相比 IJKMPMoviePlayerController 要復(fù)雜些,MPMoviePlayerController 提供的播放器具有高度的封裝性,使得自定義播放器變的很難。如果需要自定義播放器樣式的時(shí)候,一般使用 AVPlayer。AVPlayer 存在于 AVFoundtion 中,更接近于底層,也更加靈活。
- 初始化。這里需要注意的是在初始化的時(shí)候并沒(méi)有初始化 AVPlayer,只是初始化相關(guān)的實(shí)例變量。
- (id)initWithContentURL:(NSURL *)aUrl;
- (id)initWithContentURLString:(NSString *)aUrl;
// 根據(jù)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;
}
// 根據(jù)路徑初始化
- (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;
}
- 異步加載。由于多媒體文件一般比較大,獲取或計(jì)算出Asset中的屬性非常耗時(shí),Apple對(duì)Asset的屬性采用了懶惰加載模式。在創(chuàng)建AVAsset的時(shí)候,只生成一個(gè)實(shí)例,并不初始化屬性。只有當(dāng)?shù)谝淮卧L問(wèn)屬性時(shí),系統(tǒng)才會(huì)根據(jù)多媒體中的數(shù)據(jù)初始化這個(gè)屬性。由于不用同時(shí)加載所有屬性,耗時(shí)問(wèn)題得到了一定緩解。但是屬性加載在計(jì)算量比較大的時(shí)候仍舊可能會(huì)阻塞線程。為了解決這個(gè)問(wèn)題,AVFoundation提供了AVAsynchronousKeyValueLoading協(xié)議,可以異步加載屬性:
@interface AVMetadataItem (AVAsynchronousKeyValueLoading)
// 異步加載屬性,通過(guò)keys傳入要加載的key數(shù)組,在handler中做加載完成的操作。
- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key error:(NSError * _Nullable * _Nullable)outError NS_AVAILABLE(10_7, 4_2);
// 獲得屬性的加載狀態(tài),如果是AVKeyValueStatusLoaded狀態(tài),表示已經(jīng)加載完成。
- (void)loadValuesAsynchronouslyForKeys:(NSArray<NSString *> *)keys completionHandler:(nullable void (^)(void))handler NS_AVAILABLE(10_7, 4_2);
@end
- 相關(guān)方法。IJKAVMoviePlayerController的方法比較多,在這里主要關(guān)注IJKAVMoviePlayerController播放器從初始化到播放的整體流程:
1、根據(jù)初始化的URL構(gòu)建AVURLAsset對(duì)象;
2、異步加載獲取視頻相關(guān)屬性;
3、加載完成后初始化AVPlayerItem,并監(jiān)聽(tīng)它的相關(guān)屬性;
4、初始化AVPlayer,并監(jiān)聽(tīng)它的相關(guān)屬性;
5、當(dāng)狀態(tài)為AVPlayerItemStatusReadyToPlay的時(shí)候發(fā)送相關(guān)通知,如果開(kāi)啟了自動(dòng)播放則自動(dòng)播放。如果沒(méi)有開(kāi)啟自動(dòng)播放,我們可以監(jiān)聽(tīng)I(yíng)JKMPMediaPlaybackIsPreparedToPlayDidChangeNotification這個(gè)通知,收到通知后再去播放。
// 預(yù)加載,異步加載相關(guān)屬性
- (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];
});
}];
}
// 異步加載完后,監(jiān)聽(tīng)相關(guān)通知、屬性以及初始化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. */
// 監(jiān)聽(tīng)AVPlayer的狀態(tài),比較重要
[_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;
}
// 監(jiān)聽(tīng)到相關(guān)狀態(tài)改變的時(shí)候做進(jìn)一步處理,并且發(fā)送相關(guān)通知
- (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;
// 準(zhǔn)備播放
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];
// 如果自動(dòng)播放,且應(yīng)用程序?yàn)榧せ顮顟B(tài)則自動(dòng)播放
if (_shouldAutoplay && (!_pauseInBackground || [UIApplication sharedApplication].applicationState == UIApplicationStateActive))
[_player play];
});
}
break;
// 播放準(zhǔn)備失敗
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;
}
// 定位到新的播放時(shí)間
- (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];
});
}];
}
- 相關(guān)通知
// 做好播放準(zhǔn)備后
IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
// 媒體播放完成或用戶手動(dòng)退出,具體完成原因可以通過(guò)通知userInfo中的key為IJKMPMoviePlayerPlaybackDidFinishReasonUserInfoKey的對(duì)象獲取
IJKMPMoviePlayerPlaybackDidFinishNotification
// 播放狀態(tài)改變
IJKMPMoviePlayerPlaybackStateDidChangeNotification
// 媒體網(wǎng)絡(luò)加載狀態(tài)改變
IJKMPMoviePlayerLoadStateDidChangeNotification
// 當(dāng)媒體開(kāi)始通過(guò)AirPlay播放或者結(jié)束AirPlay播放
IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification
// 獲取了媒體的實(shí)際尺寸
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];
}
總結(jié)
ijkplayer 中的 IJKMPMoviePlayerController 底層由 MPMoviePlayerController 實(shí)現(xiàn),由于它具有高度的封裝性。因此,二次封裝的時(shí)候比較簡(jiǎn)單,可定制化程度低。IJKAVMoviePlayerController 底層通過(guò) AVPlayer 實(shí)現(xiàn),更加靈活,可定程度高,二次封裝相對(duì)比較困難。如果希望了解如何定制 AVPlayer,讀一讀IJKAVMoviePlayerController 的源碼是個(gè)不錯(cuò)的選擇。