iPad 畫中畫 功能添加
熊貓直播 iPad 版本 目前線上是沒畫中畫功能的。這里畫中畫功能,主要模仿虎牙的畫中畫功能。
如下畫面。
難點
直播間播放的時候正常情況下 是 FLV 格式的。但是目前畫中畫功能只支持 hls 格式。并且使用系統自帶的控件。
接來來我們看看虎牙怎么實現的
1:使用Charles 抓包。
因為hls 格式的東東,會不斷的發起http 請求,并且緩存10s 的短視頻。
初步懷疑,虎牙支持畫中畫的房間都是使用hls 格式的視頻流。
<strong>實踐是打臉的唯一標準</strong>
虎牙只有在啟動畫中畫功能的時候,才請求了http hls 格式的視頻流。。
所以,方案有了,退出直播間,的時候,切換視頻流格式。
2:使用hopper看看虎牙都做了什么,從iTunes 上下載虎牙 的 iPad 版本安裝包,解壓,看看里面的內容。不看不知道,一看嚇一跳。里面有個短視頻,mp4格式的,就是每次開打虎牙直播間的時候都是用的那個加載中,最開始我還一直以為是直播間自帶的
因為從iTunes 上下載的都是有殼的,我們也是能看個大概,
看到beginPip 那個MP4 文件了么。。
在hopper 上,搜 pic 或者 pip (這里只是嘗試,畢竟畫中畫系統的名字都是這樣子取的),大概可以看到虎牙的實現畫中的這些個類。
hopper 上看到的東東
這里就是虎牙實現畫中類的所有方法名了,我們可以根據方法名猜測個大概??!
干貨時間:
實現如下:
NS_ASSUME_NONNULL_BEGIN
@interface PTVPictureInpicture : NSObject
+ (instancetype)pictureInpicture;
///是否支持畫中畫中能
+ (BOOL)isSupportPictureInPicture;
@property (nonatomic, copy) NSString *roomID;
///#初始化 url m3u8格式
- (void)openPictureInPicture:(NSString *)url;
///#開啟畫中畫
- (void)doPicInPic;
///#關閉畫中畫
- (void)closePicInPic;
@end
NS_ASSUME_NONNULL_END
.m文件
///kvo 監聽狀態
static NSString *const kForPlayerItemStatus = @"status";
@interface PTVPictureInpicture()<AVPictureInPictureControllerDelegate>
///#畫中畫
@property (nonatomic, strong) AVPictureInPictureController *pipViewController;// 畫中畫
@end
@implementation PTVPictureInpicture
{
BOOL _needEnterRoom;
UIView *_playerContent;
AVQueuePlayer *_queuePlayer;
///#開始
AVPlayerItem *_beginItem;
AVPlayerItem *_playerItem;
AVPlayerLayer *_playerLayer;
}
+ (instancetype)pictureInpicture {
static PTVPictureInpicture *_p;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_p = [PTVPictureInpicture new];
});
return _p;
}
+ (BOOL)isSupportPictureInPicture {
static BOOL _isSuportPic = NO;
// static dispatch_once_t onceToken;
// dispatch_once(&onceToken, ^{
Class _c = NSClassFromString(@"AVPictureInPictureController");
if (_c != nil) {
_isSuportPic = [AVPictureInPictureController isPictureInPictureSupported];
}
// });
return _isSuportPic;
}
- (void)_initPicture {
if (![[self class] isSupportPictureInPicture]) return;
[self setupSuport];
}
-(void)setupSuport
{
if([AVPictureInPictureController isPictureInPictureSupported]) {
_pipViewController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer];
_pipViewController.delegate = self;
}
}
- (void)openPictureInPicture:(NSString *)url {
if (![[self class] isSupportPictureInPicture]) return;
if (!url || url.length == 0 ) return;
if (![url containsString:@"m3u8"]) return;
[self closePicInPic];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive: YES error: nil];
_playerItem = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:url]];
///#等待資源加載好
NSString *path = [[NSBundle mainBundle] pathForResource:@"BeginPIP"
ofType:@"mp4"];
NSURL *sourceMovieUrl = [NSURL fileURLWithPath:path];
AVAsset *movieAsset = [AVURLAsset URLAssetWithURL:sourceMovieUrl options:nil];
_beginItem = [AVPlayerItem playerItemWithAsset:movieAsset];
[_playerItem addObserver:self
forKeyPath:kForPlayerItemStatus
options:NSKeyValueObservingOptionNew context:nil];// 監聽loadedTimeRanges屬性
[_beginItem addObserver:self
forKeyPath:kForPlayerItemStatus
options:NSKeyValueObservingOptionNew context:nil];// 監聽loadedTimeRanges屬性
_queuePlayer = [AVQueuePlayer queuePlayerWithItems:@[_beginItem,_playerItem]];
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_queuePlayer];
_playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; // 適配視頻尺寸
_playerLayer.backgroundColor = (__bridge CGColorRef _Nullable)([UIColor blackColor]);
[self _initPicture];
if (!_playerContent) {
_playerContent = [UIView new];
_playerContent.frame = CGRectMake(-10, -10, 1, 1);
_playerContent.alpha = 0.0;
_playerContent.backgroundColor = [UIColor clearColor];
_playerContent.userInteractionEnabled = NO;
}
_playerLayer.frame = CGRectMake(0, 0, 1, 1);
[_playerContent.layer addSublayer:_playerLayer];
UIWindow *window = (UIWindow *)GetAppDelegate.window;
[window addSubview:_playerContent];
[_queuePlayer play];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"status"]) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (_queuePlayer.status == AVPlayerStatusReadyToPlay) {
[_queuePlayer play];
if (!_pipViewController.isPictureInPictureActive) {
[self doPicInPic];
}
} else {
[self closePicInPic];
}
});
}
}
- (void)doPicInPic {
if (![[self class] isSupportPictureInPicture]) return;
if (!_pipViewController.pictureInPictureActive) {
[_pipViewController startPictureInPicture];
_needEnterRoom = YES;
}
}
- (void)closePicInPic {
if (![[self class] isSupportPictureInPicture]) return;
if (!_pipViewController) return;
[self _removePlayerContentView];
_needEnterRoom = NO;
[self _removeObserve];
if (_pipViewController.pictureInPictureActive) {
[_pipViewController stopPictureInPicture];
}
///# 釋放資源
_playerItem = nil;
_playerLayer = nil;
_beginItem = nil;
_queuePlayer = nil;
}
- (void)_removeObserve {
if (_playerItem) {
[_playerItem removeObserver:self
forKeyPath:@"status"];
_playerItem = nil;
}
if (_beginItem) {
[_beginItem removeObserver:self
forKeyPath:@"status"];
_beginItem = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler {
if (_needEnterRoom) {
[self _removePlayerContentView];
if (self.roomID) {
####進入直播間
}
[self _removeObserve];
}
completionHandler(YES);
}
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
}
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
}
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
[self _removeObserve];
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error {
[self _removePlayerContentView];
}
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
}
- (void)_removePlayerContentView {
if (_playerContent && _playerContent.superview) {
[_playerContent removeFromSuperview];
}
}
@end
稍微說兩句。此處,最開始先加載一個本地視頻,因為,切換視頻格式的時候,不能馬上喚起畫中畫的畫面。只有等到 <code>AVPlayerItem</code> 的 status 是 AVPlayerStatusReadyToPlay 的時候才能顯示,所以,直接加載一個本地視頻,本地視頻的 AVPlayerItem 就直接 AVPlayerStatusReadyToPlay 了。
這里使用 AVQueuePlayer ,切換兩個 AVPlayerItem 的時候,過程中間有一個 菊花在轉動。挺好
效果圖: