IOS视频通话,启动画中画,技术上如何实现呢,类似微信IOS悬浮窗视频通话功能
参考GPT和自己的思路,要实现iOS视频通话画中画功能,您需要使用iOS的内置框架AVFoundation和UIKit。以下是一些步骤和示例代码,以帮助您开始构建该功能。
1.为视频通话创建一个视图控制器,并在其中添加AVCaptureSession。
class VideoCallViewController: UIViewController {
private let captureSession = AVCaptureSession()
override func viewDidLoad() {
super.viewDidLoad()
setupCaptureSession()
}
private func setupCaptureSession() {
captureSession.sessionPreset = .high
guard let videoCaptureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { return }
do {
let videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
} else {
print("Unable to add video input")
return
}
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
if captureSession.canAddOutput(videoOutput) {
captureSession.addOutput(videoOutput)
} else {
print("Unable to add video output")
return
}
let connection = videoOutput.connection(with: .video)
connection?.videoOrientation = .portrait
} catch let error {
print("Error setting up capture session: \(error.localizedDescription)")
return
}
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.frame = view.bounds
view.layer.insertSublayer(previewLayer, at: 0)
captureSession.startRunning()
}
}
2.将视频通话视图控制器中的AVCaptureSession添加到画中画控制器中。
class VideoCallViewController: UIViewController {
// ...其他代码
func enterPictureInPictureMode() {
let pipController = AVPictureInPictureController(playerLayer: previewLayer)
pipController.delegate = self
if pipController.isPictureInPicturePossible {
pipController.startPictureInPicture()
}
}
}
extension VideoCallViewController: AVPictureInPictureControllerDelegate {
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// Picture in Picture did stop
}
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// Picture in Picture will start
}
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// Picture in Picture did start
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
// Picture in Picture failed to start
}
}
3.创建一个按钮,并为其添加一个操作,使其可以激活画中画控制器
class VideoCallViewController: UIViewController {
// ...其他代码
override func viewDidLoad() {
super.viewDidLoad()
// Create the button
let pipButton = UIButton(type: .system)
pipButton.setTitle("Enter PiP Mode", for: .normal)
pipButton.addTarget(self, action: #selector(enterPictureInPictureMode), for: .touchUpInside)
// Add the button to the view
view.addSubview(pipButton)
pipButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pipButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pipButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
])
}
// ...其他代码
}
这样就可以在视频通话视图控制器中添加一个按钮,当用户点击该按钮时,将进入画中画模式。要退出画中画模式,用户可以点击设备上的Home键,或者点击画中画窗口中的关闭按钮。
回答不易,还请采纳!!!
该回答引用ChatGPT
如有疑问,可以回复我!
要实现 iOS 视频通话中的画中画效果,可以按照以下步骤进行:
1、在应用中创建一个 AVPlayerLayer,用于渲染视频流。
2、将 AVPlayerLayer 添加到应用的界面上,并调整大小和位置。
3、当需要在画中画模式下显示视频时,创建一个 AVPictureInPictureController 对象,并将 AVPlayerLayer 对象传递给它。
4、调用 AVPictureInPictureController 对象的 startPictureInPicture 方法,启动画中画模式。
5、用户可以通过拖动和缩放手势来调整画中画窗口的大小和位置。在画中画模式下,可以使用 AVPictureInPictureController 对象的可用方法控制视频播放。
具体实现可以参考以下示例代码:
// 创建 AVPlayerLayer 对象并添加到应用的界面上
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[playerLayer setFrame:self.view.bounds];
[self.view.layer addSublayer:playerLayer];
// 创建 AVPictureInPictureController 对象
AVPictureInPictureController *pictureInPictureController = [[AVPictureInPictureController alloc] initWithPlayerLayer:playerLayer];
// 调用 startPictureInPicture 方法,启动画中画模式
[pictureInPictureController startPictureInPicture];
需要注意的是,为了启用画中画模式,应用必须支持 iPad,并且在 Info.plist 文件中设置支持画中画模式的 UIRequiredDeviceCapabilities 键。
以下答案由GPT-3.5大模型与博主波罗歌共同编写:
要实现iOS视频通话悬浮窗,可以使用iOS的Picture in Picture(PiP)模式实现。PiP模式是iOS 9引入的一种画中画模式,可以让用户在主屏幕运行应用程序时,同时在一个小窗口中观看视频,类似于电视机上的画中画模式。
以下是实现步骤:
@import AVKit;
@import AVFoundation;
在你的项目 Info.plist 文件里,加入下列代码,开启 PIP 功能。
<key>UIRequiresFullScreen</key>
<false/>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
创建 AVPlayerViewController 对象
AVPlayerViewController 是一个 AVKit 中的控制器,可以用于播放视频和管理视频画面,使用以下代码创建 AVPlayerViewController 实例。
AVPlayerViewController *viewController = [[AVPlayerViewController alloc] init];
创建 AVPlayer 对象
AVPlayer 为 AVFoundation 中的播放器,使用以下代码创建 AVPlayer 实例,并设置它的 URL 为视频地址。
```
NSURL *url = [NSURL URLWithString:@"http://e/
参考GPT和自己的思路:iOS 的画中画功能(Picture in Picture)是一个将视频播放器缩小为一个浮动窗口的功能,允许用户在使用其他应用程序时继续观看视频。要实现这个功能,你可以按照以下步骤进行:
1 首先,你需要使用 AVKit 框架中提供的 AVPlayerViewController 来实现视频播放器功能。
2 然后,在你的应用程序中启用画中画功能。要启用画中画功能,你需要为你的 AVPlayerViewController 对象设置一个代理,并在代理方法中返回 true,以表明你的应用程序支持画中画功能。示例代码如下:
class ViewController: AVPlayerViewController, AVPlayerViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
return true
}
}
3 接下来,你需要在你的应用程序中实现画中画控制器。画中画控制器允许用户在视频播放器缩小为一个浮动窗口时对其进行控制。你需要创建一个 AVPictureInPictureController 对象,并在视频播放器开始播放时启动画中画控制器。示例代码如下:
class ViewController: AVPlayerViewController, AVPlayerViewControllerDelegate {
var pipController: AVPictureInPictureController?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.pipController = AVPictureInPictureController(playerLayer: self.playerLayer)
}
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
return true
}
@IBAction func startPIP(_ sender: Any) {
if AVPictureInPictureController.isPictureInPictureSupported() {
self.pipController?.startPictureInPicture()
}
}
}
4 最后,你需要在你的应用程序中实现画中画控制器的委托方法,以便在用户使用画中画控制器时对视频播放器进行更新。示例代码如下:
class ViewController: AVPlayerViewController, AVPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate {
var pipController: AVPictureInPictureController?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.pipController = AVPictureInPictureController(playerLayer: self.playerLayer)
self.pipController?.delegate = self
}
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
return true
}
@IBAction func startPIP(_ sender: Any) {
if AVPictureInPictureController.isPictureInPictureSupported() {
self.pipController?.startPictureInPicture()
}
}
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// TODO: 在开始画中画时对视频播放器进行更新
}
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// TODO: 在停止画中画时对视频播放器进行更新
}
}
在这段示例代码中,我们首先创建了一个 AVPictureInPictureController 实例并将其委托给当前视图控制器。在开始画中画时,我们调用 startPictureInPicture() 方法启动画中画。同时,我们还实现了 AVPlayerViewControllerDelegate 和 AVPictureInPictureControllerDelegate 两个协议中的方法,以便在用户使用画中画控制器时对视频播放器进行更新。在方法 pictureInPictureControllerWillStartPictureInPicture() 中,我们可以在开始画中画时对视频播放器进行更新,在方法 pictureInPictureControllerDidStopPictureInPicture() 中,我们可以在停止画中画时对视频播放器进行更新。
//
// QIEPIPManager.m
// QEZB
//
// Created by 李超群 on 2021/7/6.
// Copyright © 2021 zhou. All rights reserved.
//
#import "QIEPIPManager.h"
#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>
#import "DYTabBarManager.h"
#import "DYTabBarViewController.h"
#import <QIEBasePlayerCompont/QIEPlayer.h>
#import <MediaPlayer/MediaPlayer.h>
///kvo 监听状态
static NSString *const kForPlayerItemStatus = @"status";
@interface QIEPIPManager ()<AVPictureInPictureControllerDelegate>
///#画中画
@property (nonatomic, strong) AVPictureInPictureController *pipViewController;// 画中画
/** 播放 */
@property (nonatomic, weak) UIView *tempView;
@property (nonatomic, strong) UIView *playerContent;
@property (nonatomic, strong) AVPlayerItem *loadingItem;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, strong) AVQueuePlayer *queuePlayer;
@end
@implementation QIEPIPManager
+ (instancetype)pictureInpicture {
static QIEPIPManager *_p;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_p = [QIEPIPManager new];
});
return _p;
}
-(void)prepareToPlay{
[self.queuePlayer play];
[self pipViewController];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)openPictureInPicture:(NSString *)url{
if (!url || url.length == 0 ) return;
if (![url containsString:@"m3u8"]) return;
// - 播放视频
NSArray *keys = @[@"tracks"];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:url] options:nil];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^(){
for (NSString *thisKey in keys) {
NSError *error = nil;
AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error];
if (keyStatus != AVKeyValueStatusLoaded) {
return ;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
AVPlayerItem *item = [[AVPlayerItem alloc] initWithAsset:asset];
[item addObserver:self forKeyPath:kForPlayerItemStatus options:NSKeyValueObservingOptionNew context:nil];
[self.queuePlayer replaceCurrentItemWithPlayerItem:item];
});
}];
}
-(void)_changeToLoadingVideo{
[self.queuePlayer pause];
[self.queuePlayer replaceCurrentItemWithPlayerItem:self.loadingItem];
[self.queuePlayer play];
}
-(void)configPlayCenter{
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
//歌曲名称
dict[MPMediaItemPropertyTitle] = @"标题";
dict[MPMediaItemPropertyArtist] = @"用户信息";
dict[MPMediaItemPropertyAlbumTitle] = @"专辑名称";
CGSize size = CGSizeMake(250, 100);
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:size requestHandler:^UIImage * _Nonnull(CGSize size) {
UIImage *image = [UIImage imageNamed:@"medal_share_header"];
return image;
}];
[dict setObject:artwork forKey:MPMediaItemPropertyArtwork];
//音乐当前播放时间 在计时器中修改
dict[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @0;
//设置锁屏状态下屏幕显示播放音乐信息
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
- (void)_removeObserve {
if (_loadingItem) {
[_loadingItem removeObserver:self forKeyPath:@"status"];
_loadingItem = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)_removePlayerContentView {
if (_playerContent && _playerContent.superview) {
[_playerContent removeFromSuperview];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if (![keyPath isEqualToString:kForPlayerItemStatus]) return;
// 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];
// }
// });
if (object == self.loadingItem) {
NSLog(@"// - 李超群 console [error] ... beginItem %ld", (long)self.queuePlayer.status);
}else {
// - 如果当前开始使用 _queuePlayer 播放m3u8, 停止播放视频.
[[QIEPlayer shareInstance] stopPlay];
NSLog(@"// - 李超群 console [error] ... xxxxx");
}
}
-(void)_mediaPlayDidEnd:(NSNotification *)noti{
[self performSelectorOnMainThread:@selector(_mediaPlayDidEndInMainThread:) withObject:noti waitUntilDone:NO];
}
-(void)_mediaPlayDidEndInMainThread:(NSNotification *)noti{
AVPlayerItem *item = [noti object];
if (item != self.loadingItem) return;
[self.loadingItem seekToTime:kCMTimeZero completionHandler:nil];
}
-(void)applicationDidBecomeActive{
[self.pipViewController stopPictureInPicture];
}
// - MARK: <-- 代理方法 -->
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerWillStartPictureInPicture");
}
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerDidStartPictureInPicture");
// - 临时黑色的cover
[self.tempView removeFromSuperview];
UIView *v = [[UIView alloc]initWithFrame:self.playerContent.bounds];
[self.playerContent.superview addSubview:v];
v.backgroundColor = [UIColor blackColor];
self.tempView = v;
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error {
NSLog(@"// - 李超群 console [log] ...pictureInPictureController:failedToStartPictureInPictureWithError");
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler {
NSLog(@"// - 李超群 console [log] ...pictureInPictureController");
[self _changeToLoadingVideo];
completionHandler(YES);
}
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerWillStopPictureInPicture");
}
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerDidStopPictureInPicture");
[UIView animateWithDuration:0.3 animations:^{
self.tempView.backgroundColor = [UIColor clearColor];
} completion:^(BOOL finished) {
[self.tempView removeFromSuperview];
}];
}
// - MARK: <-- 懒加载 -->
/** 加载的video */
-(AVPlayerItem *)loadingItem{
if(!_loadingItem){
_loadingItem = [AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"loading11"ofType:@"mov"]]];
[_loadingItem addObserver:self forKeyPath:kForPlayerItemStatus options:NSKeyValueObservingOptionNew context:nil];
}
return _loadingItem;
}
- (AVQueuePlayer *)queuePlayer{
if (!_queuePlayer) {
_queuePlayer = [AVQueuePlayer queuePlayerWithItems:@[self.loadingItem]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_mediaPlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:_queuePlayer.currentItem];
_queuePlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
_playerLayer.frame = self.playerContent.bounds;
}
return _queuePlayer;
}
- (AVPlayerLayer *)playerLayer{
if (!_playerLayer) {
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_queuePlayer];
_playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; // 适配视频尺寸
_playerLayer.backgroundColor = (__bridge CGColorRef _Nullable)([UIColor blackColor]);
}
return _playerLayer;
}
- (UIView *)playerContent{
if (!_playerContent) {
_playerContent = [UIView new];
_playerContent.frame = [UIScreen mainScreen].bounds;
_playerContent.backgroundColor = [UIColor blackColor];
_playerContent.userInteractionEnabled = NO;
[_playerContent.layer addSublayer:self.playerLayer];
[[DYTabBarManager shareInstance].tabBarController.view insertSubview:_playerContent atIndex:0];
}
return _playerContent;
}
- (AVPictureInPictureController *)pipViewController{
if (!_pipViewController && [AVPictureInPictureController isPictureInPictureSupported]) {
_pipViewController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer];
_pipViewController.delegate = self;
}
return _pipViewController;
}
@end
iOS视频通话启动画中画(Picture in Picture)功能,需要使用AVPictureInPictureController类库。这个类库允许开发者在用户切换到其他应用之后仍能够在画中画视图中显示视频,同时还能使用户在视图之间拖动和缩放。下面是一些实现步骤:
导入 AVKit 和 AVFoundation 框架;
初始化一个 AVPlayer 实例,将要显示的视频添加到其中;
创建一个 AVPictureInPictureController 实例,并将其连接到播放器实例;
根据需要配置AVPictureInPictureController 实例的代理方法,以便在画中画视图的生命周期内响应事件;
调用AVPictureInPictureController 实例的startPictureInPicture方法启动画中画功能。
以下是代码示例:
import AVKit
class ViewController: UIViewController, AVPictureInPictureControllerDelegate {
var player: AVPlayer!
var pictureInPictureController: AVPictureInPictureController!
override func viewDidLoad() {
super.viewDidLoad()
// 初始化一个AVPlayer实例
let videoURL = URL(string: "http://example.com/sample.mp4")!
player = AVPlayer(url: videoURL)
// 创建一个AVPictureInPictureController实例
pictureInPictureController = AVPictureInPictureController(playerLayer: AVPlayerLayer(player: player))
pictureInPictureController.delegate = self
}
@IBAction func startPictureInPicture() {
//启动画中画功能
pictureInPictureController.startPictureInPicture()
}
// 实现AVPictureInPictureControllerDelegate代理方法
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 画中画视图已经启动
}
func pictureInPictureControllerFailedToStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController, withError error: Error) {
// 画中画视图启动失败
}
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 画中画视图已经停止
}
func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 即将停止画中画视图
}
}
注意,在调用startPictureInPicture方法之前,你需要检查当前设备是否支持画中画功能。你可以通过检查pictureInPictureController的isPictureInPictureSupported属性来实现这一点。
在iOS视频通话中实现画中画功能,需要在界面上添加一个小窗口,用于显示对方的视频画面。通常情况下,这个小窗口会被放置在应用程序的某个角落,用户可以通过拖动、缩放等手势来移动和调整窗口大小。
具体实现过程如下:
创建画中画窗口
在iOS中,可以使用AVPictureInPictureController类来创建画中画窗口。首先,需要初始化一个AVPictureInPictureController对象,并将其设置为当前视图控制器的代理。然后,通过startPictureInPicture()方法来启动画中画功能。该方法会创建一个小窗口,并将其添加到当前界面中。
import AVKit
class MyViewController: UIViewController, AVPictureInPictureControllerDelegate {
var pictureInPictureController: AVPictureInPictureController?
override func viewDidLoad() {
super.viewDidLoad()
// 初始化画中画控制器
pictureInPictureController = AVPictureInPictureController(playerLayer: playerLayer)
pictureInPictureController?.delegate = self
}
func startPictureInPicture() {
// 启动画中画功能
pictureInPictureController?.startPictureInPicture()
}
}
处理画中画窗口的显示和隐藏
在启动画中画功能之后,需要处理画中画窗口的显示和隐藏。可以通过实现AVPictureInPictureControllerDelegate协议来监听画中画窗口的显示状态,并根据需要调整界面布局。
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 画中画窗口即将开始显示
// 可以在此处调整界面布局
}
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 画中画窗口已经停止显示
// 可以在此处恢复界面布局
}
处理画中画窗口的手势操作
在画中画窗口中,用户可以通过手势操作来移动和调整窗口大小。可以通过实现AVPictureInPictureControllerDelegate协议来监听用户的手势操作,并根据需要调整窗口的位置和大小。
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
// 画中画窗口启动失败
// 可以在此处进行错误处理
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
// 画中画窗口即将停止显示
// 可以在此处恢复界面布局
completionHandler(true)
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, willMoveTo: AVAudioSession.Category) {
// 画中画窗口将要移动到指定的音
原理
在播放视频的时候,先使用IJKPlayer播放流视频, 同时启动AVQueuePlayer播放一个loading的视频(AVQueuePlayer 播放的视频需要循环播放, 播放loading, 是为了在程序进入后台时候, 有一个loading的假象)
在程序进入后台时候, 画中画会自动启动, 同时关掉IJKPlayer播放, 让AVQueuePlayer独占音频播放, 然后预加载m3u8的视频流, 当m3u8可以播放时候, 直接让 AVQueuePlayer 播放 m3u8的视频流.
程序进入前台时候, 继续让 AVQueuePlayer 播放loading的视频;
趟坑
如果loading的视频时长很短(4s), 在播放loading视频时候程序退到后台, 无法启动画中画.
启动画中画, 必须将 AVAudioSession 的category 必须是 AVAudioSessionCategoryPlayback.
如果程序退出到后台在使用AVQueuePlayer 播放视频, 是无法启动画中画的, 所以为了避免这个问题, 需要在一个不显示的view上 静默启动 AVQueuePlayer 播放loading的视频.
_playerContent是防止动画导致卡顿一下的情况
程序进入前台, 直接播放静默的loading的视频.
如果希望自动小窗播放, 需要延迟0s在主线程启动画中画, 如代码中所示.
需要在 [player play] 之前 初始化 AVPictureInPictureController, 并设置 AVPictureInPictureController的playerLayer, 否则户出现第一次启动不成功的问题.
代码参考:
//
// QIEPIPManager.m
// QEZB
//
// Created by 李超群 on 2021/7/6.
// Copyright © 2021 zhou. All rights reserved.
//
#import "QIEPIPManager.h"
#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>
#import "DYTabBarManager.h"
#import "DYTabBarViewController.h"
#import <QIEBasePlayerCompont/QIEPlayer.h>
#import <MediaPlayer/MediaPlayer.h>
///kvo 监听状态
static NSString *const kForPlayerItemStatus = @"status";
@interface QIEPIPManager ()<AVPictureInPictureControllerDelegate>
///#画中画
@property (nonatomic, strong) AVPictureInPictureController *pipViewController;// 画中画
/** 播放 */
@property (nonatomic, weak) UIView *tempView;
@property (nonatomic, strong) UIView *playerContent;
@property (nonatomic, strong) AVPlayerItem *loadingItem;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, strong) AVQueuePlayer *queuePlayer;
@end
@implementation QIEPIPManager
+ (instancetype)pictureInpicture {
static QIEPIPManager *_p;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_p = [QIEPIPManager new];
});
return _p;
}
-(void)prepareToPlay{
[self.queuePlayer play];
[self pipViewController];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)openPictureInPicture:(NSString *)url{
if (!url || url.length == 0 ) return;
if (![url containsString:@"m3u8"]) return;
// - 播放视频
NSArray *keys = @[@"tracks"];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:url] options:nil];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^(){
for (NSString *thisKey in keys) {
NSError *error = nil;
AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error];
if (keyStatus != AVKeyValueStatusLoaded) {
return ;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
AVPlayerItem *item = [[AVPlayerItem alloc] initWithAsset:asset];
[item addObserver:self forKeyPath:kForPlayerItemStatus options:NSKeyValueObservingOptionNew context:nil];
[self.queuePlayer replaceCurrentItemWithPlayerItem:item];
});
}];
}
-(void)_changeToLoadingVideo{
[self.queuePlayer pause];
[self.queuePlayer replaceCurrentItemWithPlayerItem:self.loadingItem];
[self.queuePlayer play];
}
-(void)configPlayCenter{
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
//歌曲名称
dict[MPMediaItemPropertyTitle] = @"标题";
dict[MPMediaItemPropertyArtist] = @"用户信息";
dict[MPMediaItemPropertyAlbumTitle] = @"专辑名称";
CGSize size = CGSizeMake(250, 100);
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:size requestHandler:^UIImage * _Nonnull(CGSize size) {
UIImage *image = [UIImage imageNamed:@"medal_share_header"];
return image;
}];
[dict setObject:artwork forKey:MPMediaItemPropertyArtwork];
//音乐当前播放时间 在计时器中修改
dict[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @0;
//设置锁屏状态下屏幕显示播放音乐信息
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
- (void)_removeObserve {
if (_loadingItem) {
[_loadingItem removeObserver:self forKeyPath:@"status"];
_loadingItem = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)_removePlayerContentView {
if (_playerContent && _playerContent.superview) {
[_playerContent removeFromSuperview];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if (![keyPath isEqualToString:kForPlayerItemStatus]) return;
// 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];
// }
// });
if (object == self.loadingItem) {
NSLog(@"// - 李超群 console [error] ... beginItem %ld", (long)self.queuePlayer.status);
}else {
// - 如果当前开始使用 _queuePlayer 播放m3u8, 停止播放视频.
[[QIEPlayer shareInstance] stopPlay];
NSLog(@"// - 李超群 console [error] ... xxxxx");
}
}
-(void)_mediaPlayDidEnd:(NSNotification *)noti{
[self performSelectorOnMainThread:@selector(_mediaPlayDidEndInMainThread:) withObject:noti waitUntilDone:NO];
}
-(void)_mediaPlayDidEndInMainThread:(NSNotification *)noti{
AVPlayerItem *item = [noti object];
if (item != self.loadingItem) return;
[self.loadingItem seekToTime:kCMTimeZero completionHandler:nil];
}
-(void)applicationDidBecomeActive{
[self.pipViewController stopPictureInPicture];
}
// - MARK: <-- 代理方法 -->
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerWillStartPictureInPicture");
}
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerDidStartPictureInPicture");
// - 临时黑色的cover
[self.tempView removeFromSuperview];
UIView *v = [[UIView alloc]initWithFrame:self.playerContent.bounds];
[self.playerContent.superview addSubview:v];
v.backgroundColor = [UIColor blackColor];
self.tempView = v;
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error {
NSLog(@"// - 李超群 console [log] ...pictureInPictureController:failedToStartPictureInPictureWithError");
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler {
NSLog(@"// - 李超群 console [log] ...pictureInPictureController");
[self _changeToLoadingVideo];
completionHandler(YES);
}
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerWillStopPictureInPicture");
}
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"// - 李超群 console [log] ...pictureInPictureControllerDidStopPictureInPicture");
[UIView animateWithDuration:0.3 animations:^{
self.tempView.backgroundColor = [UIColor clearColor];
} completion:^(BOOL finished) {
[self.tempView removeFromSuperview];
}];
}
// - MARK: <-- 懒加载 -->
/** 加载的video */
-(AVPlayerItem *)loadingItem{
if(!_loadingItem){
_loadingItem = [AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"loading11"ofType:@"mov"]]];
[_loadingItem addObserver:self forKeyPath:kForPlayerItemStatus options:NSKeyValueObservingOptionNew context:nil];
}
return _loadingItem;
}
- (AVQueuePlayer *)queuePlayer{
if (!_queuePlayer) {
_queuePlayer = [AVQueuePlayer queuePlayerWithItems:@[self.loadingItem]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_mediaPlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:_queuePlayer.currentItem];
_queuePlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
_playerLayer.frame = self.playerContent.bounds;
}
return _queuePlayer;
}
- (AVPlayerLayer *)playerLayer{
if (!_playerLayer) {
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_queuePlayer];
_playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; // 适配视频尺寸
_playerLayer.backgroundColor = (__bridge CGColorRef _Nullable)([UIColor blackColor]);
}
return _playerLayer;
}
- (UIView *)playerContent{
if (!_playerContent) {
_playerContent = [UIView new];
_playerContent.frame = [UIScreen mainScreen].bounds;
_playerContent.backgroundColor = [UIColor blackColor];
_playerContent.userInteractionEnabled = NO;
[_playerContent.layer addSublayer:self.playerLayer];
[[DYTabBarManager shareInstance].tabBarController.view insertSubview:_playerContent atIndex:0];
}
return _playerContent;
}
- (AVPictureInPictureController *)pipViewController{
if (!_pipViewController && [AVPictureInPictureController isPictureInPictureSupported]) {
_pipViewController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer];
_pipViewController.delegate = self;
}
return _pipViewController;
}
@end