在很多视频类 App 中,我们都能看到这样一套体验:
视频支持内嵌播放 + 自定义控制栏
一键横屏全屏,并且正确处理状态栏 / Home Indicator
页面切换时,视频自动进入 画中画(PiP)
点击 PiP 恢复按钮,可以自动回到原播放器页面
本文将完整拆解一套 基于 AVPlayer + AVPictureInPictureController 的自定义播放器实现方案 ,并给出可直接复用的架构思路。
一、整体架构设计
1️⃣ 页面结构
UIWindow
└── BaseNavigationController
├── ViewController(播放器页)
│ └── HLPlayerView(自定义播放器 View)
├── ListViewController
└── ProfileViewController
核心思想是:
-
播放器是 View,不是 VC
-
旋转 / 状态栏 / PiP 恢复 由 VC 统一处理
-
播放器只负责播放 & UI,不碰系统层逻辑
二、SceneDelegate & Navigation Controller 配置
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 -
AVPictureInPictureControllerself.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 封装