Flutter PIP 插件 ---- iOS Video Call 自定义PIP WINDOW渲染内容

news2025/5/10 12:29:19

简介

画中画(Picture in Picture, PiP)是一项允许用户在使用其他应用时继续观看视频内容的功能。本文将详细介绍如何在 iOS 应用中实现 PiP 功能,包括自定义内容渲染和控制系统控件的显示。

效果展示

PiP 效果演示

功能特性

已完成功能

  • ✅ 基础 PiP 接口实现(设置、启动、停止、释放等)
  • ✅ 支持自定义内容渲染,将 PiP 窗口渲染内容与插件分离
  • ✅ 支持 PiP 窗口控制样式(显示/隐藏系统控件)
  • ✅ 支持后台自动进入 PiP 模式
  • ✅ 支持调整 PiP 窗口大小和比例
  • ✅ 提供自定义窗口渲染内容 Demo(UIView 循环播放图片)

待实现功能

  • ⏳ 播放事件监听与资源优化
  • ⏳ 根据系统版本和应用类型自动切换实现方式
  • ⏳ 通过 MPNowPlayingSession 更新播放信息
  • ⏳ 细节优化和最佳实践示例

实现原理

Apple 官方文档主要描述了基于 AVPlayer 的 PiP 实现和 VOIP PiP,对于自定义渲染和控制样式等高级功能描述较少。本文结合实践经验,提供完整的实现方案。

核心思路

  1. PiP 窗口显示

    核心是将 UIView(AVSampleBufferDisplayLayer) 插入到指定的 contentSourceView 中,并渲染透明图像。这样既不影响原有内容,又能实现 PiP 功能。

  2. 自定义内容渲染

    通过动态添加自定义 UIView 到 PiP 窗口实现,而不是使用标准的视频帧显示方式。这种方式更灵活,便于封装。

技术要点

关键注意事项

  1. 音频会话设置

    即使视频没有声音,也需要设置 audio session 为 movie playback,否则应用进入后台时 PiP 窗口不会打开。

  2. 控件显示控制

    除了 requiresLinearPlayback 可以控制快进/后退按钮外,其他控件(如播放/暂停按钮、进度条)需要通过 KVO 设置 controlStyle

  3. 视图控制器访问

    无法直接访问 PiP 窗口的 ViewController,目前有两种方案:

    • 获取当前 activate window 添加视图
    • 通过反射获取 Controller 的私有属性 viewController

    注意:使用私有 API 可能有上架风险,建议寻找更稳定的替代方案。

实现步骤

1. 创建 PipView

PipView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@class AVSampleBufferDisplayLayer;

@interface PipView : UIView

@property (nonatomic) AVSampleBufferDisplayLayer *sampleBufferDisplayLayer;

- (void)updateFrameSize:(CGSize)frameSize;

@end

NS_ASSUME_NONNULL_END

PipView.m

#import "PipView.h"
#import <AVFoundation/AVFoundation.h>

@implementation PipView

+ (Class)layerClass {
    return [AVSampleBufferDisplayLayer class];
}

- (AVSampleBufferDisplayLayer *)sampleBufferDisplayLayer {
    return (AVSampleBufferDisplayLayer *)self.layer;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        self.alpha = 0;
    }
    return self;
}

- (void)updateFrameSize:(CGSize)frameSize {
    CMTimebaseRef timebase;
    CMTimebaseCreateWithSourceClock(nil, CMClockGetHostTimeClock(), &timebase);
    CMTimebaseSetTime(timebase, kCMTimeZero);
    CMTimebaseSetRate(timebase, 1);
    self.sampleBufferDisplayLayer.controlTimebase = timebase;
    if (timebase) {
        CFRelease(timebase);
    }

    CMSampleBufferRef sampleBuffer =
        [self makeSampleBufferWithFrameSize:frameSize];
    if (sampleBuffer) {
        [self.sampleBufferDisplayLayer enqueueSampleBuffer:sampleBuffer];
        CFRelease(sampleBuffer);
    }
}

- (CMSampleBufferRef)makeSampleBufferWithFrameSize:(CGSize)frameSize {
    size_t width = (size_t)frameSize.width;
    size_t height = (size_t)frameSize.height;

    const int pixel = 0xFF000000; // {0x00, 0x00, 0x00, 0xFF};//BGRA

    CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferCreate(NULL, width, height, kCVPixelFormatType_32BGRA,
                        (__bridge CFDictionaryRef)
                            @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}},
                        &pixelBuffer);
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    int *bytes = CVPixelBufferGetBaseAddress(pixelBuffer);
    for (NSUInteger i = 0, length = height *
                                    CVPixelBufferGetBytesPerRow(pixelBuffer) / 4;
         i < length; ++i) {
        bytes[i] = pixel;
    }
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    CMSampleBufferRef sampleBuffer =
        [self makeSampleBufferWithPixelBuffer:pixelBuffer];
    CVPixelBufferRelease(pixelBuffer);
    return sampleBuffer;
}

- (CMSampleBufferRef)makeSampleBufferWithPixelBuffer:
    (CVPixelBufferRef)pixelBuffer {
    CMSampleBufferRef sampleBuffer = NULL;
    OSStatus err = noErr;
    CMVideoFormatDescriptionRef formatDesc = NULL;
    err = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
                                                       pixelBuffer, &formatDesc);

    if (err != noErr) {
        return nil;
    }

    CMSampleTimingInfo sampleTimingInfo = {
        .duration = CMTimeMakeWithSeconds(1, 600),
        .presentationTimeStamp =
            CMTimebaseGetTime(self.sampleBufferDisplayLayer.timebase),
        .decodeTimeStamp = kCMTimeInvalid};

    err = CMSampleBufferCreateReadyWithImageBuffer(
        kCFAllocatorDefault, pixelBuffer, formatDesc, &sampleTimingInfo,
        &sampleBuffer);

    if (err != noErr) {
        return nil;
    }

    CFRelease(formatDesc);

    return sampleBuffer;
}

@end

2. 配置 PiP 控制器

// 创建 PipView
PipView *pipView = [[PipView alloc] init];
pipView.translatesAutoresizingMaskIntoConstraints = NO;

// 添加到源视图
[currentVideoSourceView insertSubview:pipView atIndex:0];
[pipView updateFrameSize:CGSizeMake(100, 100)];

// 创建内容源
AVPictureInPictureControllerContentSource *contentSource =
    [[AVPictureInPictureControllerContentSource alloc]
        initWithSampleBufferDisplayLayer:pipView.sampleBufferDisplayLayer
                        playbackDelegate:self];

// 创建 PiP 控制器
AVPictureInPictureController *pipController =
    [[AVPictureInPictureController alloc] initWithContentSource:contentSource];
pipController.delegate = self;
pipController.canStartPictureInPictureAutomaticallyFromInline = YES;

3. 设置控制样式

// 控制快进/后退按钮
pipController.requiresLinearPlayback = YES;

// 控制其他控件
[pipController setValue:@(1) forKey:@"controlsStyle"]; // 隐藏前进/后退、播放/暂停按钮和进度条
// [pipController setValue:@(2) forKey:@"controlsStyle"]; // 隐藏所有系统控件

4. 处理播放代理

- (CMTimeRange)pictureInPictureControllerTimeRangeForPlayback:
    (AVPictureInPictureController *)pictureInPictureController {
    return CMTimeRangeMake(kCMTimeZero, kCMTimePositiveInfinity);
}

5. 管理自定义视图

// 添加自定义视图
- (void)pictureInPictureControllerDidStartPictureInPicture:
    (AVPictureInPictureController *)pictureInPictureController {
    [pipViewController.view insertSubview:contentView atIndex:0];
    [pipViewController.view bringSubviewToFront:contentView];
    
    // 设置约束
    contentView.translatesAutoresizingMaskIntoConstraints = NO;
    [pipViewController.view addConstraints:@[
        [contentView.leadingAnchor constraintEqualToAnchor:pipViewController.view.leadingAnchor],
        [contentView.trailingAnchor constraintEqualToAnchor:pipViewController.view.trailingAnchor],
        [contentView.topAnchor constraintEqualToAnchor:pipViewController.view.topAnchor],
        [contentView.bottomAnchor constraintEqualToAnchor:pipViewController.view.bottomAnchor],
    ]];
}

// 移除自定义视图
- (void)pictureInPictureControllerDidStopPictureInPicture:
    (AVPictureInPictureController *)pictureInPictureController {
    [contentView removeFromSuperview];
}

参考资源

  • Adopting Picture in Picture in a Custom Player
  • 在 iOS App 上添加"画中画(PiP)"功能
  • iOS 使用AVPictureInPictureController画中画实现自定义歌词
  • 一文学会iOS画中画浮窗
  • How to hide system controls on AVPictureInPictureController’s float window?
  • PiPBugDemo

项目地址

欢迎 Star 支持!

  • GitHub
  • pub.dev

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2338697.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

xml+html 概述

1.什么是xml xml 是可扩展标记语言的缩写&#xff1a; Extensible Markup Language。 <root><h1> text 1</h1> </root> web 应用开发&#xff0c;需要配置 web.xml&#xff0c;就是个典型的 xml文件 <web-app><servlet><servlet-name&…

Java从入门到“放弃”(精通)之旅——数组的定义与使用⑥

Java从入门到“放弃”&#xff08;精通&#xff09;之旅&#x1f680;——数组⑥ 前言——什么是数组&#xff1f; 数组&#xff1a;可以看成是相同类型元素的一个集合&#xff0c;在内存中是一段连续的空间。比如现实中的车库&#xff0c;在java中&#xff0c;包含6个整形类…

如何对docker镜像存在的gosu安全漏洞进行修复——筑梦之路

这里以mysql的官方镜像为例进行说明&#xff0c;主要流程为&#xff1a; 1. 分析镜像存在的安全漏洞具体是什么 2. 根据分析结果有针对性地进行修复处理 3. 基于当前镜像进行修复安全漏洞并复核验证 # 镜像地址mysql:8.0.42 安全漏洞现状分析 dockerhub网站上获取该镜像的…

基于springboot的老年医疗保健系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

使用Ollama本地运行deepseek模型

Ollama 是一个用于管理 AI 模型的工具 下载 Ollama Ollama 选择版本 下载模型 安装好后&#xff0c;下载模型 选择模型 选择模型大小&#xff0c;复制对应命令&#xff08;越大越聪明&#xff0c;但是内存要求越高&#xff09; 打开控制台运行命令&#xff0c;第一次运行会自动…

网络编程 - 3

目录 UDP 连接拓展&#xff08;业务逻辑&#xff09; 词典服务器实现 完 UDP 连接拓展&#xff08;业务逻辑&#xff09; 我们上一篇文章实现了一个回显服务器&#xff0c;在服务端中业务方法 process 中&#xff0c;只是单纯的将客户端输入的东西 return 了一下&#xff0…

5G 毫米波滤波器的最优选择是什么?

新的选择有很多&#xff0c;但到目前为止还没有明确的赢家。 蜂窝电话技术利用大量的带带&#xff0c;为移动用途提供不断增加的带宽。 其中的每一个频带都需要透过滤波器将信号与其他频带分开&#xff0c;但目前用于手机的滤波器技术可能无法扩展到5G所规划的全部毫米波&#…

【HDFS入门】HDFS性能调优实战:压缩与编码技术深度解析

目录 1 HDFS性能调优概述 2 HDFS压缩技术原理与应用 2.1 常见压缩算法比较 2.2 压缩流程架构 2.3 压缩配置实践 3 列式存储编码技术 3.1 ORC与Parquet对比 3.2 ORC文件结构 3.3 Parquet编码流程 4 性能调优实战建议 4.1 压缩选择策略 4.2 编码优化技巧 5 性能测试…

如何在 IntelliJ IDEA 中安装通义灵码 - AI编程助手提升开发效率

随着人工智能技术的飞速发展&#xff0c;AI 编程助手已成为提升开发效率和代码质量的强大工具。在众多 AI 编程助手之中&#xff0c;阿里云推出的通义灵码凭借其智能代码补全、代码解释、生成单元测试等丰富功能&#xff0c;脱颖而出&#xff0c;为开发者带来了全新的编程体验。…

从零到一:管理系统设计新手如何快速上手?

管理系统设计是一项复杂而富有挑战性的任务&#xff0c;它要求设计者具备多方面的知识和技能&#xff0c;包括需求分析、架构设计、数据管理、用户界面设计等。对于初次接触这一领域的新手而言&#xff0c;如何快速上手并成为一名合格的管理系统设计者呢&#xff1f;本文将从管…

WSL (ext4.vhdx文件)占用空间过大,清理方式记录,同时更改 WSL 保存位置

一、问题 之前使用 WSL Ubuntu 进行过开发板的 Yocto 项目编译&#xff0c;占用空间达到了 70GB 多的空间。后来进行了项目迁移&#xff0c;删除了 WSL 中的所有文件&#xff0c;但是从 Windows 查看空间占用却没有减少&#xff1a; 占用依然是 70 多&#xff0c;查阅发现 vhdx…

《软件设计师》复习笔记(14.2)——统一建模语言UML、事务关系图

目录 1. UML概述 2. UML构造块 (1) 事物&#xff08;Things&#xff09; (2) 关系&#xff08;Relationships&#xff09; 真题示例&#xff1a; 3. UML图分类 (1) 结构图&#xff08;静态&#xff09; (2) 行为图&#xff08;动态&#xff09; 4. 核心UML图详解 5.…

[文献阅读] EnCodec - High Fidelity Neural Audio Compression

[文献信息]&#xff1a;[2210.13438] High Fidelity Neural Audio Compression facebook团队提出的一个用于高质量音频高效压缩的模型&#xff0c;称为EnCodec。Encodec是VALL-E的重要前置工作&#xff0c;正是Encodec的压缩量化使得VALL-E能够出现&#xff0c;把语音领域带向大…

【操作系统原理01】操作系统引论

文章目录 大纲一、中断与异常0.大纲1. 中断的作用2. 中断类型2.1 内中断2.2 外中断2.3 判断内外中断 3. 中断机制原理 二、系统调用0. 大纲1.什么是系统调用2.系统调用分类 三、操作性系统内核(了解)0.大纲1.内核2.各种操作系统结构特性 四、操作系统引论0.大纲1.磁盘存储 图片…

最新得物小程序sign签名加密,请求参数解密,响应数据解密逆向分析

点击精选&#xff0c;出现https://app.dewu.com/api/v1/h5/index/fire/index 这个请求 直接搜索sign的话不容易定位 直接搜newAdvForH5就一个&#xff0c;进去再搜sign&#xff0c;打上断点 可以看到t.params就是没有sign的请求参数&#xff0c; 经过Object(a.default)该函数…

Day2—3:前端项目uniapp壁纸实战

接下来我们做一个专题精选 <view class"theme"><common-title><template #name>专题精选</template><template #custom><navigator url"" class"more">More</navigator></template></common…

Python基于知识图谱的医疗问答系统【附源码、文档说明】

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

股指期货跨期套利是如何赚取价差利润的?

股指期货跨期套利&#xff0c;简单来说&#xff0c;就是在同一交易所内&#xff0c;针对同一股指期货品种的不同交割月份合约进行的套利交易。投资者会同时买入某一月份的股指期货合约&#xff0c;并卖出另一月份的股指期货合约&#xff0c;待未来某个时间点&#xff0c;再将这…

w297毕业生实习与就业管理系统

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;原创团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文…

Java集合框架中的List、Map、Set详解

在Java开发中&#xff0c;集合框架是处理数据时不可或缺的工具之一。今天&#xff0c;我们来深入了解一下Java集合框架中的List、Map和Set&#xff0c;并探讨它们的常见方法操作。 目录 一、List集合 1.1 List集合介绍 1.2 List集合的常见方法 添加元素 获取元素 修改元素…