iOS 自定义视频播放器实战:全屏旋转 + 画中画(PiP)+ 多页面切换不中断播放

在很多视频类 App 中,我们都能看到这样一套体验:

  • 视频支持内嵌播放 + 自定义控制栏

  • 一键横屏全屏,并且正确处理状态栏 / Home Indicator

  • 页面切换时,视频自动进入 画中画(PiP)

  • 点击 PiP 恢复按钮,可以自动回到原播放器页面

本文将完整拆解一套 基于 AVPlayer + AVPictureInPictureController 的自定义播放器实现方案 ,并给出可直接复用的架构思路。


一、整体架构设计

1️⃣ 页面结构

复制代码
UIWindow
 └── BaseNavigationController
      ├── ViewController(播放器页)
      │    └── HLPlayerView(自定义播放器 View)
      ├── ListViewController
      └── ProfileViewController

核心思想是:

  • 播放器是 View,不是 VC

  • 旋转 / 状态栏 / PiP 恢复 由 VC 统一处理

  • 播放器只负责播放 & UI,不碰系统层逻辑


1️⃣ SceneDelegate 设置根控制器

复制代码
BaseNavigationController *nav =
    [[BaseNavigationController alloc] initWithRootViewController:[ViewController new]];
self.window.rootViewController = nav;
[self.window makeKeyAndVisible];

2️⃣ 让状态栏控制权交给顶层 VC

复制代码
- (UIViewController *)childViewControllerForStatusBarHidden {
    return self.topViewController;
}

- (UIViewController *)childViewControllerForStatusBarStyle {
    return self.topViewController;
}

⚠️ 这是自定义全屏播放器必须做的一步

否则 prefersStatusBarHidden 不会生效。


三、播放器页(ViewController)职责划分

主要职责

  • 管理播放器的 全屏 / 竖屏状态

  • 处理 系统旋转

  • 控制 状态栏 & Home Indicator

  • 处理 PiP 恢复 UI 的跳转

关键属性

复制代码
@property (nonatomic, assign) BOOL isFullScreen;
@property (nonatomic, assign) BOOL isStatusHidden;
@property (nonatomic, assign) CGRect portraitFrame;

四、全屏旋转的正确姿势(iOS 16+ & 兼容方案)

1️⃣ 点击全屏按钮 → 通知 VC

复制代码
[self.delegate hlPlayerView:self didTapFullScreenButton:self.isFullScreen];

2️⃣ VC 主动请求系统旋转

复制代码
- (void)rotateToOrientation:(UIInterfaceOrientationMask)orientation {
    if (@available(iOS 16.0, *)) {
        UIWindowScene *windowScene = self.view.window.windowScene;
        UIWindowSceneGeometryPreferencesIOS *preferences =
            [[UIWindowSceneGeometryPreferencesIOS alloc]
             initWithInterfaceOrientations:orientation];
        [windowScene requestGeometryUpdateWithPreferences:preferences errorHandler:nil];
    } else {
        [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight)
                                    forKey:@"orientation"];
        [UIViewController attemptRotationToDeviceOrientation];
    }
}

3️⃣ 必须重写的系统方法

复制代码
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return self.isFullScreen ?
           UIInterfaceOrientationMaskLandscapeRight :
           UIInterfaceOrientationMaskPortrait;
}

- (BOOL)prefersStatusBarHidden {
    return self.isFullScreen;
}

- (BOOL)prefersHomeIndicatorAutoHidden {
    return self.isFullScreen;
}

五、播放器 View(HLPlayerView)设计要点

1️⃣ 核心组件

  • AVPlayer

  • AVPlayerLayer

  • AVPictureInPictureController

    self.player = [AVPlayer playerWithURL:url];
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    [self.layer addSublayer:self.playerLayer];


2️⃣ 自定义控制栏(不使用 AVPlayerViewController)

  • 播放 / 暂停

  • 进度条

  • 时间显示

  • PiP 按钮

  • 全屏按钮

    self.controlView.alpha = self.controlsHidden ? 0 : 1;

并配合 3 秒自动隐藏


六、进度 & 时间同步

复制代码
[self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, NSEC_PER_SEC)
                                         queue:dispatch_get_main_queue()
                                    usingBlock:^(CMTime time) {
    float current = CMTimeGetSeconds(time);
    float total = CMTimeGetSeconds(self.player.currentItem.duration);
    self.progressSlider.value = current / total;
}];

七、画中画(PiP)完整闭环方案

1️⃣ 初始化 PiP

复制代码
self.pipController =
    [[AVPictureInPictureController alloc] initWithPlayerLayer:self.playerLayer];
self.pipController.canStartPictureInPictureAutomaticallyFromInline = YES;

2️⃣ 页面切换时自动进入 PiP

复制代码
if ([self.player isPictureInPicturePossible] &&
    ![self.player isPictureInPictureActive]) {
    [self.player startPictureInPicture];
}

3️⃣ 点击 PiP 恢复按钮 → 自动回到播放器页

复制代码
- (void)pictureInPictureController:
    (AVPictureInPictureController *)controller
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:
    (void (^)(BOOL))completionHandler {

    [self.delegate hlPlayerViewDidRequestRestoreUserInterface:self];
    completionHandler(YES);
}

- (void)hlPlayerViewDidRequestRestoreUserInterface:(HLPlayerView *)playerView {
    [self.navigationController popToViewController:self animated:YES];
}

八、多 VC 切换不中断播放的关键点

✅ 播放器 不销毁

✅ 使用 PiP 托管播放

✅ VC 只做页面跳转,不持有播放逻辑

最终效果:

  • 视频在 List / Profile 页面依然播放

  • PiP 点击恢复,准确回到播放器页

  • 不闪屏、不重建播放器


九、适用场景

这套方案非常适合:

  • 🎬 短视频 / 长视频 App

  • 📺 直播播放器

  • 📚 教育 / 课程播放

  • 🎵 音视频 SDK 封装


相关推荐
庚鑫2 小时前
iOS 27 兼容放大模式下,keyboardFrame 与 UIScreen.bounds 的坐标差异
ios·objective-c
william_yangshun2 小时前
DigUp 实测:用多模态嵌入做 macOS 全盘语义搜索
macos·向量检索
czt_java3 小时前
网络基础入门:IPv4、IPv6、私网公网、MAC、以太网帧与 DNS 学习笔记
网络·学习·macos
HEJOO93 小时前
iOS 高效绘图:从 Core Graphics 到 Metal 的实践指南
macos·ios·cocoa
Hello_Pyhx3 小时前
RetainPDF v4.2.6:Windows、macOS、Linux PDF 翻译的 API 配置与任务排查
linux·macos·pdf
HEJOO93 小时前
iOS 性能优化全面详解
ios·性能优化·cocoa
万年咸鱼3 小时前
iOS 27 适配笔记
笔记·ios·cocoa
Rebecca.Yan3 小时前
Claude Code 深度学习
macos
HEJOO94 小时前
iOS Dark Mode 适配笔记
笔记·ios·cocoa
hey you~4 小时前
嘈杂环境ASR识别优化:降噪、VAD切分与Whisper模型微调完整教程(2026版)
macos·whisper·xcode