前言
Picture-In-Picture is coming to iPhone as well, the implementation looking similar to that currently available on the iPad.
画中画是在桌面以浮窗的形式播放视频的功能,从 iOS 14 开始支持。桌面这块宝地肯定是各个App 都眼馋的,就会出现各种骚操作,本来是只能用来播放视频的,用来展示其他内容,比如某云的桌面歌词。老板看了说也想要,无奈只能找找资料看看怎么做。(个人不推荐这种骚操作)
以下是一些问题记录。
如何自定义
添加自定义 UIView
创建 AVPictureInPictureController
之后,系统会创建一个 PGHostedWindow
,windowLevel
为 -10000000。所以可以通过 UIApplication
获取到这个 window,将自定义的 UIView 添加到这个 window 即可。
添加 View 的时机
pictureInPictureControllerWillStartPictureInPicture
或者 pictureInPictureControllerDidStartPictureInPicture
代理方法回调时添加。
但 willStart 时,window 的实际大小还不确定,更推荐在 didStart 时添加
获取 Window 的时机
在添加 view 的时候获取,即 pictureInPictureControllerWillStartPictureInPicture:
代理方法。
ini
UIWindow *window = [UIApplication sharedApplication].windows.firstObject;
此方法有一定的风险,如果创建了多个 AVPictureInPictureController
,对应的会存在多个 PGHostedWindow
。
可以监听 UIWindowDidBecomeVisibleNotification
通知,在收到通知后持有下这个 window 的引用。
ini
- (void)windowDidBecomeVisible:(NSNotification *)notification {
id object = notification.object;
if ([object isKindOfClass:NSClassFromString(@"PGHostedWindow")]) {
self.hostedWindow = notification.object;
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIWindowDidBecomeVisibleNotification
object:nil];
}
}
或者用 KVO 监听 isPictureInPicturePossible
属性
设置 contentSource (iOS 15 +)
将自定义 View 的 layerClass 改写成 AVSampleBufferDisplayerLayer
objectivec
@implementation PIPCustomView
+ (Class)layerClass {
return [AVSampleBufferDisplayLayer class];
}
@end
再通过 initWithSampleBufferDisplayLayer
创建 AVPictureInPictureControllerContentSource
ini
PIPCustomView *customView = [[PIPCustomView alloc] init];
AVSampleBufferDisplayLayer *layer = (AVSampleBufferDisplayLayer *)customView.layer;
AVPictureInPictureControllerContentSource *source = [[AVPictureInPictureControllerContentSource alloc] initWithSampleBufferDisplayLayer:layer playbackDelegate:customView];
AVPictureInPictureController *controller = [[AVPictureInPictureController alloc] initWithContentSource:source];
再将 CALayer 转换成 CMSampleBuffer
CALayer -> UIImage -> CVPixelBufferRef -> CMSampleBufferRef
再通过 enqueueSampleBuffer
更新到画中画
直接交互🚫
在自定义的 UIView 添加一个 UIButton,iOS 14、15 可正常执行点击回调方法,iOS 16 及以上则没有任何反应
去除播放相关按钮
css
[controller setValue:[NSNumber numberWithInt:1] forKey:@"controlsStyle"];
在 iOS 16 及以上版本支持点击画中画直接回到 App
css
[controller setValue:[NSNumber numberWithInt:2] forKey:@"controlsStyle"];
控制画中画窗口大小
无法直接控制画中画窗口的大小,只能控制窗口的比例,系统会根据所播放视频的比例自动适配到相应的大小。并且在同一个视频在不同的机型,实际显示大小也不一致。
视频大小 336 x 456
iPhone 8 窗口实际大小 191 x 259.5
iPhone 15 窗口实际大小 164.333 x 223
画中画开启时机
应用在前台✅
直接调用 startPictureInPicture
即可
App 切换到后台时自动开启✅
canStartPictureInPictureAutomaticallyFromInline
设置为 YES 后,可在 App 切换到后台时自动开启。但需要注意的是该属性只在 iOS 14.2 及以上可用,并且视频必须是在播放中的。
应用在后台🚫
如果想在后台收到某个通知时打开,系统是不允许的,会出现如下错误
Error Domain=AVKitErrorDomain Code=-1001 "Failed to start picture in picture." UserInfo={NSLocalizedDescription=Failed to start picture in picture., NSLocalizedFailureReason=The UIScene for the content source has an activation state other than UISceneActivationStateForegroundActive, which is not allowed.}
参考
Adopting Picture in Picture in a Custom Player | Apple Developer Documentation
Can I add custom view on AVPictureInPictureController?
Picture in Picture for any UIView
GitHub - CaiWanFeng/PiP: The best way to customize picture-in-picture for iOS.