iOS 视频播放画中画实现

842 阅读5分钟

Apple文档

1.开启后台模式

  • 通过capabilities添加
    image.png

  • 通过info.plist代码添加

<!-- Info.plist 添加后台模式权限 -->
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
    <string>picture-in-picture</string>
</array>

2.导入框架

#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>

3. 代码实现

使用系统播放器 AVPlayerViewController
//
//  ViewController.m
//  VideoInVideo
//
//  Created by 彭彬峰 on 2025/2/26.
//

#import "ViewController.h"
#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>

// 定义播放器及画中画控制器
@interface ViewController ()

@property (nonatomic, strong) AVPlayerViewController *playerVC;

@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self setupPlayer];
}

#pragma mark - 初始化播放器
- (void)setupPlayer {
    // 1. 创建播放器
    // 视频 URL
//    NSURL *videoURL = [NSURL URLWithString:@"https://www.example.com/path/to/your/video.mp4"]; // 请提供有效的视频 URL
    
    // 获取本地视频 URL
    NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"747a499aaea1ecb890c14912bdf864cf" withExtension:@"MP4"];

    // 创建 AVPlayer
    self.player = [AVPlayer playerWithURL:videoURL];
    
    // 2. 配置播放器视图
    self.playerVC = [[AVPlayerViewController alloc] init];
    self.playerVC.player = self.player;
    self.playerVC.allowsPictureInPicturePlayback = YES; // 启用画中画
    self.playerVC.requiresLinearPlayback = NO; // 允许后台播放
    self.playerVC.canStartPictureInPictureAutomaticallyFromInline = YES;
    self.playerVC.view.frame = CGRectMake(0, 200, self.view.bounds.size.width, 400);
    [self.view addSubview:self.playerVC.view];
    
    if ([AVPictureInPictureController isPictureInPictureSupported]) {
         NSLog(@"该设备支持画中画功能");
         //开启画中画后台声音权限
         NSError *error = nil;
         [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
         [[AVAudioSession sharedInstance] setActive:YES error:nil];
         if (error) {
             NSLog(@"请求权限失败的原因为%@",error);
         }
     } else {
         NSLog(@"该设备不支持画中画功能");
     }
    
    // 启动播放
    [self.player play];
    
}
@end
使用 AVPictureInPictureController
//1.判断是否支持画中画功能
if ([AVPictureInPictureController isPictureInPictureSupported]) {
    //2.开启权限
    @try {
        NSError *error = nil;
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionOrientationBack error:&error];
        [[AVAudioSession sharedInstance] setActive:YES error:&error];
    } @catch (NSException *exception) {
        NSLog(@"AVAudioSession发生错误");
    }
    self.pipVC = [[AVPictureInPictureController alloc] initWithPlayerLayer:self.player];
    self.pipVC.delegate = self;
}
// 开启或关闭画中画
if (self.pipVC.isPictureInPictureActive) {
    [self.pipVC stopPictureInPicture];
} else {
    [self.pipVC startPictureInPicture];
}

代理 AVPictureInPictureControllerDelegate

// 即将开启画中画
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;
// 已经开启画中画
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;
// 开启画中画失败
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error;
// 即将关闭画中画
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;
// 已经关闭画中画
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;
// 关闭画中画且恢复播放界面
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler;

关闭画中画会执行 pictureInPictureController:restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: 这个代理方法,用来恢复播放界面的

代码实现:

//
//  ViewController.m
//  VideoInVideo
//
//  Created by 彭彬峰 on 2025/2/26.
//

#import "ViewController.h"
#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>

// 定义播放器及画中画控制器
@interface ViewController ()<AVPictureInPictureControllerDelegate>

@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, strong) AVPictureInPictureController *pipController;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self setupPlayer];
    
    // 注册进入后台的通知
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationDidEnterBackground:)
                                                 name:UIApplicationDidEnterBackgroundNotification
                                               object:nil];
    
    // 注册即将进入前台的通知
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationWillEnterForeground:)
                                                 name:UIApplicationWillEnterForegroundNotification
                                               object:nil];
}

#pragma mark - 初始化播放器
- (void)setupPlayer {
    // 1. 创建播放器
    // 视频 URL
//    NSURL *videoURL = [NSURL URLWithString:@"https://www.example.com/path/to/your/video.mp4"]; // 请提供有效的视频 URL
    
    // 获取本地视频 URL
    NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"747a499aaea1ecb890c14912bdf864cf" withExtension:@"MP4"];

    // 创建 AVPlayer
    self.player = [AVPlayer playerWithURL:videoURL];
    
    // 2. 创建 AVPlayerLayer
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    self.playerLayer.frame = CGRectMake(0, 200, self.view.bounds.size.width, 400);
    [self.view.layer addSublayer:self.playerLayer];

    // 检查可用性并初始化 AVPictureInPictureController
    if ([AVPictureInPictureController isPictureInPictureSupported]) {
        if ([AVPictureInPictureController isPictureInPictureSupported]) {
             NSLog(@"该设备支持画中画功能");
             //开启画中画后台声音权限
             NSError *error = nil;
             [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
             [[AVAudioSession sharedInstance] setActive:YES error:nil];
             if (error) {
                 NSLog(@"请求权限失败的原因为%@",error);
             }
         } else {
             NSLog(@"该设备不支持画中画功能");
         }
       // 3. 初始化画中画控制器
       if (@available(iOS 15.0, *)) {
           // 初始化 AVPictureInPictureControllerContentSource
           AVPictureInPictureControllerContentSource *contentSource = [[AVPictureInPictureControllerContentSource alloc] initWithPlayerLayer:self.playerLayer];

           // 初始化 AVPictureInPictureController
           self.pipController = [[AVPictureInPictureController alloc] initWithContentSource:contentSource];
       } else {
           // iOS 14 及以下版本的初始化(直接用 playerLayer)
           self.pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:self.playerLayer];
       }
       
       // 设置代理(可选)
        self.pipController.delegate = self;
        self.pipController.requiresLinearPlayback = YES;
        self.pipController.canStartPictureInPictureAutomaticallyFromInline = YES;

        
        //在点击画中画按钮的时候 开启画中画
        [self togglePiPMode];
       
       // 启动播放
       [self.player play];
    } else {
       NSLog(@"Picture in Picture is not supported on this device.");
    }
}

#pragma mark - 画中画开关
- (void)togglePiPMode {
    if (![AVPictureInPictureController isPictureInPictureSupported]) {
//        [self showAlert:@"设备不支持画中画"];
        NSLog(@"设备不支持画中画");
        return;
    }
    
    if (self.pipController.isPictureInPictureActive) {
        [self.pipController stopPictureInPicture];
    } else {
        [self.pipController startPictureInPicture];
    }
}

#pragma mark - PiP代理方法
// 即将开启画中画
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;{
    NSLog(@"pictureInPictureControllerWillStartPictureInPicture");
}
// 已经开启画中画
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;{
    NSLog(@"pictureInPictureControllerDidStartPictureInPicture");
}
    
// 开启画中画失败
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error;{
    NSLog(@"failedToStartPictureInPictureWithError");
}
// 即将关闭画中画
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;{
    NSLog(@"pictureInPictureControllerWillStopPictureInPicture");
}
// 已经关闭画中画
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController;{
    NSLog(@"pictureInPictureControllerDidStopPictureInPicture");
}
// 关闭画中画且恢复播放界面
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler;{
    NSLog(@"restoreUserInterfaceForPictureInPictureStopWithCompletionHandler");
}

#pragma mark - 进入前台和后台
// 处理进入后台的逻辑
- (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"App did enter background");
    // 在这里加入你希望在应用进入后台时执行的代码
//    [self togglePiPMode];
}

// 处理即将进入前台的逻辑
- (void)applicationWillEnterForeground:(NSNotification *)notification {
    NSLog(@"App will enter foreground");
    // 在这里加入你希望在应用即将进入前台时执行的代码
//    [self togglePiPMode];
}

// 移除通知监听
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIApplicationDidEnterBackgroundNotification
                                                  object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIApplicationWillEnterForegroundNotification
                                                  object:nil];
}
@end

全局画中画注意点

  • 通过一个全局变量持有画中画控制器,可以在pictureInPictureControllerWillStartPictureInPicture持有,pictureInPictureControllerDidStopPictureInPicture释放;
  • 有可能不是点画中画按钮,而是从其它途径来打开当前画中画控制器,可以在viewWillAppear 进行判断并关闭;
  • 已有画中画的情况下开启新的画中画,需要等完全关闭完再开启新的,防止有未知的错误出现,因为关闭画中画是有过程的;
  • 如果创建AVPictureInPictureController并同时开启画中画功能,有可能会失效,出现这种情况延迟开启画中画功能即可。
  • 画中画中如何屏蔽原生播放按钮:controlsStyle属性设置为1。
  • 切后台如何自动唤起画中画:canStartPictureInPictureAutomaticallyFromInline属性设置为YES,并将视频一直循环播放,切后台就会自动唤起画中画。另外需要注意,视频播放的layer的frame一定要在可视区域,当不在可是区域时,也无法唤起画中画。