☰
HarmonyOS Media Kit屏幕捕获C API:图像填充模式与隐私过滤实战
2026/10/14 4:42:14 网站建设 项目流程

最近调 HarmonyOS 6.0 的 Media Kit,一个录屏模块前后折腾了三周。最难受的倒不是性能,而是输出画面的比例和局部遮挡:你明明用的是等比缩放,画面要么被拉变形,要么上下两条大黑边;想在录屏里把某个弹窗遮掉,遮挡框跟着窗口一挪就全偏了。这些问题的根源,最后都指向同一个东西——图像填充模式(Image Fill Mode)。这篇把 C API 层面的图像填充模式拆开讲清楚,包括怎么配置、什么时候该用哪种模式、以及文档里不会写的那些小坑。适合正在做屏幕捕获、远程协助、会议共享,或者需要把屏幕内容嵌套进固定画布的同学参考。

1. 为什么屏幕捕获也需要“细粒度控制”

1.1 录屏业务里的真实痛点

很多人觉得屏幕捕获就是把屏幕像素拿过来丢给编码器,镜头一转画面就出来了。真做业务的时候会发现完全不是这么回事,输出侧的要求远比想象中复杂。

先说远程协助。源设备很可能是一台竖屏手机,分辨率 1080x2400,但远端坐着的是一台 16:10 的显示器,辅助窗口固定 1280x800。如果你直接把原始画面拉伸到 1280x800,画面会明显横向变形,人像变胖,文字发虚。这种情况不能简单“拉伸”,得先做等比缩放,然后决定周围留黑边还是裁剪。

再看录课或者直播场景。很多直播伴侣需要把手机屏幕内容转成横屏输出,同时还要在画面边缘留出一个摄像头 PiP 窗口。也就是说,屏幕内容不能占满整个输出画布,只能占其中一块区域。这时候填充模式决定这块内容在目标画布上怎么摆放。

还有一类是隐私保护需求。录屏时经常要遮住通知栏、悬浮球、输入法联想区或者某个弹窗。这些区域和屏幕捕获的坐标体系相关,而且会随窗口大小变化,不能靠简单涂黑做死。Media Kit 在 C API 层面提供了设置隐私过滤区域的能力,这属于“细粒度控制屏幕捕获”里很关键的一环。

1.2 图像填充模式解决的三个核心问题

图像填充模式看起来只是“图片怎么塞进画布”的小事,但它实际上决定了三件事:

  1. 输出尺寸和源尺寸不一致时,画面的缩放策略。
  2. 画面内容在目标画布上的摆放位置和边界处理。
  3. 是否允许内容被裁剪,裁剪后是否影响业务可读性。

先看一组尺寸,源画面 864x1920,输出目标 1280x720。宽高比分别是 0.45 和 1.78,完全不同的方向。如果按拉伸模式处理,源画面会被强制拉成 1280x720,结果就是画面被压扁,而且扁得非常明显。如果按等比缩放适应(AspectFit),源画面等比放大到高度 720 时,宽度只有 324,画面会缩在中间,左右两侧是黑边。如果按等比缩放填充(AspectFill),为了让宽度撑满 1280,高度会等比放大到 2844,超出输出区域的部分会被裁掉。三种模式拍到同一帧,业务效果差异非常大。

我给一个实际经验:面向人的内容,比如视频通话、远程协助,尽量不要用拉伸。拉伸对文字和自然人脸的影响是灾难性的,用户一眼就能看出来。用 AspectFit 会引入黑边,但在画面里是可控的,可以在黑边区域叠加 UI。用 AspectFill 适合不需要展示全部内容的场景,比如手机竖屏游戏投到横屏直播时,只保留中间有效区域,效果比带黑边好。

1.3 细粒度控制还包括什么

填充模式只是“细粒度控制”里的一块。在 Media Kit 的屏幕捕获 C API 中,细粒度还体现在这几个方面:

  • 捕获区域控制:可以只捕获某个窗口,也可以捕获整个屏幕,甚至可以指定一个矩形区域。
  • 隐私过滤:通过矩形列表设置遮挡区域,被覆盖的区域在输出画面里不可见。
  • 帧率和分辨率控制:输出尺寸和帧率都可配置,而不是只能用系统默认参数。
  • 动态模式切换:捕获进行中,可以根据时间或事件动态调整填充模式和隐私区域。

这些能力组合起来,屏幕捕获才能从“能录”变成“能按业务规则录”。理解了整体做什么,后面看 C API 接口就会顺很多。

2. 图像填充模式C API的接口设计解读

2.1 填充模式的枚举定义

在 Media Kit 的屏幕捕获 C API 里,填充模式是一个枚举类型。我拿到的 SDK 头文件里大概是这样定义的,接口名为示意,实际以你环境里的头文件为准:

typedef enum { OH_SCREENCAP_FILL_MODE_STRETCH = 0, OH_SCREENCAP_FILL_MODE_ASPECT_FIT = 1, OH_SCREENCAP_FILL_MODE_ASPECT_FILL = 2 } OH_ScreenCaptureFillMode;

三种模式对应的行为,可以类比图片加载控件里的常见概念。

OH_SCREENCAP_FILL_MODE_STRETCH是拉伸模式。源画面的宽高被强制映射到目标输出尺寸,完全不考虑原始比例。这种模式的优势是计算简单,性能开销最小,缺点也很明显:只要宽高比例不一致,画面就会变形。我一般只在输出端需要“满铺”且画面内容对比例不敏感的场合用,比如一些全屏动态壁纸。

OH_SCREENCAP_FILL_MODE_ASPECT_FIT是等比缩放适应模式。源画面按比例缩放,直到完整落在目标画布内,X 轴或 Y 轴方向会留下黑边。这个模式适合需要展示完整内容的业务,比如远程协助里要看到整个屏幕,哪怕有黑边也没关系。黑边区域可以作为操作栏、品牌标识等 UI 的摆放位置。

OH_SCREENCAP_FILL_MODE_ASPECT_FILL是等比缩放填充模式。源画面按比例缩放,直到铺满整个目标画布,超出画布的内容会被裁剪。这个模式适合“内容优先填满”的场景,比如游戏直播时竖屏画面切横屏输出,裁剪掉上下部并不会影响观看核心内容。

2.2 隐私过滤与坐标结构

细粒度控制不只是填充模式,隐私过滤区域也是一个独立的配置结构。大致长这样:

typedef struct { int32_t x; int32_t y; uint32_t width; uint32_t height; } OH_ScreenCaptureRect; typedef struct { OH_ScreenCaptureRect *rects; uint32_t rectCount; } OH_ScreenCapturePrivacyFilter;

这个结构体列表的坐标基于捕获源坐标系,单位是像素。比如你只需要把通知栏遮掉,传入的矩形就可以是{0, 0, 1080, 120}。Media Kit 在合成输出画面时,会对落入这些矩形内的像素做遮挡处理,让敏感内容不出现在最终画布里。

需要注意,隐私过滤区域的坐标是“源坐标”,不是“输出坐标”。如果你的输出分辨率做了缩放,遮挡矩形不需要跟着输出分辨率做换算,它会按照源坐标系正确映射到输出画面。这个设计一开始容易搞反,后面实操部分我再细说。

2.3 核心调用链路

屏幕捕获的 C API 调用链路并不复杂,大致是一个状态机:创建会话、配置参数、注册回调、启动、处理数据、停止、销毁。

配置阶段有两个最容易忽略的接口:设置填充模式的接口和设置隐私过滤区域的接口。从接口分类来看,填充模式属于“输出画布策略”,隐私过滤属于“内容安全策略”,两种配置都支持在会话启动前设置,部分场景也支持动态更新。

大致调用顺序是:

  1. 创建捕获会话对象。
  2. 读取或指定输出分辨率。
  3. 设置帧率。
  4. 设置填充模式。
  5. 设置隐私过滤区域。
  6. 注册数据回调。
  7. 启动捕获。
  8. 在回调里拿帧数据。

这里记住一个原则:填充模式和隐私过滤最好在Start之前设置。虽然很多实现支持运行中改配置,但我实测下来,运行中切换填充模式在部分设备上会触发画面闪烁一两帧。如果你做的是严肃录屏工具,能提前确定策略就尽量提前确定。

2.4 C API 和 ArkTS 怎么选

HarmonyOS 的 Media Kit 同时有 ArkTS 和 C API 两种使用路径。ArkTS 上手快,写业务逻辑方便,但对于需要高帧率、低延迟、或者要做 native 层处理的场景,C API 有明显优势。

我自己的选择标准很简单:如果只是临时录个屏、存个文件,用 ArkTS 足够;如果要在录屏基础上做实时推流、实时遮挡、动态布局,或者本身就被嵌入在一个 native 多媒体引擎里,就走 C API。C API 的数据回调直接给到 native 层 buffer,少一次跨语言拷贝,延迟和内存占用都可控。

3. 实操:一个带填充模式的屏幕捕获调用

3.1 创建会话与基础配置

先看一眼整体代码骨架。这里我用的是 Media Kit 里的屏幕捕获 C 接口,头文件引入方式和函数名是常见写法,实际 SDK 版本不一样时请以本地头文件为准。

#include <multimedia/media_kit/media_kit.h> #include <multimedia/media_kit/screen_capture/screen_capture.h> OH_ScreenCapture *capture = OH_ScreenCapture_Create(); if (capture == NULL) { // 创建失败,多半是系统组件未初始化 return; }

创建完成后,先配置输出分辨率。我在这里强烈建议不要直接写死 1080x1920 之类的“拍脑袋尺寸”,而是通过系统接口读取当前屏幕尺寸,再决定输出尺寸。原因很简单:折叠屏、平板、异形屏的尺寸差异极大,写死尺寸的代码换台设备就翻车。

int32_t screenWidth = 0; int32_t screenHeight = 0; OH_ScreenCapture_GetScreenSize(capture, &screenWidth, &screenHeight); uint32_t outputWidth = 1280; uint32_t outputHeight = 720; OH_ScreenCapture_SetSize(capture, outputWidth, outputHeight);

如果你的场景必须保住源画面的完整比例,可以按源屏幕的宽高比计算输出尺寸。比如源是 1080x2400,比例 9:20,你想输出宽度 1080,那高度就是 2400,只是这种竖屏分辨率在多数播放器里兼容性不好。更常见的做法是固定 16:9 输出,然后用 AspectFit 留黑边,这个策略在后面填充模式那里接管。

3.2 设置填充模式,并算清楚黑边和裁剪

配置填充模式的代码很简单:

OH_ScreenCaptureSetFillMode(capture, OH_SCREENCAP_FILL_MODE_ASPECT_FIT);

但真正决定效果的,是你对缩放结果有没有预期。我建议在设置之前先自己手算一遍,提前知道画面会是什么样。

拿前面的例子算:源 864x1920,输出 1280x720。

AspectFit 的算法是:先求源宽高比864 / 1920 = 0.45,目标宽高比1280 / 720 = 1.78。因为目标比例比源更宽,画面会被限制在高度方向:源等比缩放到高度 720,宽度 =720 * 0.45 = 324。最终画面 324x720 居中放在 1280x720 画布内,左右黑边各(1280 - 324) / 2 = 478像素。

这个黑边宽度是非常可观的,接近一半画面是黑的。如果你把这种画面直接推流,用户看到的就是一条“细长的手机屏幕被夹在中间”,体验很差。所以如果目标是横屏直播,我通常会选 AspectFill,或者干脆把源画面裁剪成 16:9 再捕获。

AspectFill 的算法:为了填满 1280x720,按宽度对齐缩放。源宽缩放到 1280,缩放倍率 =1280 / 864 = 1.48,高度变成1920 * 1.48 = 2841,输出时取垂直居中区域,上下各裁掉(2841 - 720) / 2 = 1060像素。对竖屏游戏来说,这个裁掉的范围往往会把手指操作区域裁掉一半,所以直播场景常用“竖屏内容横屏输出 + 背景模糊填充”的方案,而不是硬裁。

3.3 设置隐私过滤区域

隐私过滤区域同样在启动前配置。我习惯把它独立封装成一个函数,方便后续动态更新:

OH_ScreenCaptureRect hideRect = {0, 0, 1080, 120}; OH_ScreenCapturePrivacyFilter filter = {&hideRect, 1}; OH_ScreenCapture_SetPrivacyFilter(capture, &filter);

这里传进去的是一个矩形数组,而不是单一矩形,意味着你可以在一次调用里设置多个遮挡区域。比如同时遮住顶部通知栏、底部导航栏、以及某个悬浮球,分别对应三个矩形,一次性交付给会话。

需要注意,隐私过滤矩形的坐标是相对整个捕获源的,不是相对某个窗口。而且设置完后,输出画面里的遮挡区域会以纯色或者马赛克形式出现,具体表现取决于系统实现。我实测下来,纯色遮挡占用的性能几乎可以忽略,马赛克遮挡会多一部分额外开销。如果你同时开了几路视频流,遮挡区域又多,建议先用纯色验证效果,再决定是否要马赛克。

如果你需要在捕获过程中动态更新隐私区域,比如用户拖了一个弹窗,遮挡区域要跟着弹窗走,那么可以再次调用OH_ScreenCapture_SetPrivacyFilter传入新的矩形数组。动态更新的生效时延通常在一帧到两帧之间,肉眼基本无感。

3.4 注册数据回调并处理帧数据

屏幕捕获的数据通过回调交给业务层。回调签名大概是:

typedef void (*OH_ScreenCaptureDataCallback)(OH_ScreenCapture *capture, OH_AVBuffer *buffer, void *userData);

注册回调:

OH_ScreenCapture_SetDataCallback(capture, OnScreenData, this);

在回调里,第一件事是确认buffer不为空,然后取到 buffer 的地址和大小。这里有一个我踩过很多次坑的点:回调里拿到的 buffer 是系统复用的,处理完必须尽快释放或者拷贝,不能把buffer存入队列后延时处理,否则缓冲池会被占满,画面会开始卡顿。

void OnScreenData(OH_ScreenCapture *capture, OH_AVBuffer *buffer, void *userData) { if (buffer == NULL) { return; } // 从 OH_AVBuffer 中取内存地址 uint8_t *addr = OH_AVBuffer_GetAddr(buffer); int32_t size = OH_AVBuffer_GetSize(buffer); // 业务处理,比如送编码器、送推流器 HandleEncodedFrame(addr, size, userData); }

如果你只是想把屏幕内容保存为视频,可以直接把回调数据送进编码器;如果要预览到纹理,还需要根据 buffer 的像素格式做一次转换。我的建议是能直接入编码器就不要转成 BGRA 再转回来,每次 CPU 拷贝和颜色空间转换都会吃掉不少帧时间预算。

3.5 停止与资源释放

停止捕获时,要先停掉数据流,再销毁会话对象:

OH_ScreenCapture_Stop(capture); OH_ScreenCapture_Destroy(capture); capture = NULL;

这里有个小讲究:先置空回调引用,或者确保回调不会再触发,再销毁对象。否则极端的时序下,数据回调可能访问一个已经释放的userData,直接崩溃。

4. 踩坑实录:常见问题与排查指引

4.1 黑屏:权限和时序问题最多

屏幕捕获最常见的失败表现是黑屏。权限都申请了,代码也没报错,但回调拿到的数据是纯黑。这类问题我遇到三类原因:

第一类,权限没有真正生效。HarmonyOS 对屏幕捕获有专门的权限约束,需要在配置阶段拿授权,有些设备还会弹系统级授权弹窗。如果你的应用是后台运行,或者授权弹窗还没点确认就开始捕获,很容易拿到黑帧。需要确保授权回调完成后再启动。

第二类,数据回调触发前,Surface 或 buffer 还没有绑定完成。建议在启动捕获前先创建一个输出目标,然后等待首次回调,而不是启动后立刻去读 buffer。

第三类,动态隐私过滤区域过大。我曾经把隐私区域误设置为整个屏幕的 100% 区域,结果输出画面整体被遮挡,看起来就是全黑。排查时先临时把过滤区域去掉,确认基础画面正常,再逐步加回来。

4.2 填充模式不生效:时机和坐标系

有朋友反馈设置 AspectFit 后画面还是拉伸。排查下来,大多是设置时机问题。如果会话已经在跑,你再调SetFillMode,部分设备会忽略这个变化。最稳妥的做法是在Start之前完成填充模式设置。

还有一种情况是填充模式生效了,但画面内容看起来和预期不一致。这通常和源坐标系有关。屏幕捕获的源区域默认是整个屏幕,但如果你设置了部分区域捕获,比如只录某个窗口,那么填充模式的源尺寸是“窗口尺寸”,而不是“屏幕尺寸”。窗口是 800x600,输出目标却是 1920x1080,AspectFit 的效果和你用全屏源时完全不同。排查时,先把GetScreenSize的值打日志打出来,再对照实际捕获区域确认。

4.3 旋转和窗口尺寸变化:动态更新是必选项

屏幕旋转是录屏工具绕不开的问题。手机转横屏之后,源的宽高交换,填充模式基于新尺寸重新计算,如果你的输出画布是固定 1280x720,AspectFit 的黑边方向就会从左右变成上下。很多业务不能接受这种跳动,必须监听系统的方向变化事件,在旋转发生时重新计算输出尺寸、填充策略、隐私过滤区域。

我通常的做法是维护一个“当前策略结构体”,包含宽、高、填充模式、隐私矩形列表。屏幕方向变化时,先更新这个结构体,再一次性应用到捕获会话,减少多次调用的中间态。窗口尺寸变化同理,如果你只捕获某个应用窗口,窗口 resize 后,旧坐标区域可能会捕获到别的应用内容,必须重新计算。

4.4 性能和延迟:裁剪不是免费的

填充模式的运算量其实很小,真正的开销在数据格式和内存拷贝。AspectFill 的裁剪只在输出画布里做区域计算,不会对每一帧产生巨大开销。但如果你把捕获到的帧从 YUV 转成 RGBA,再转回 YUV 送编码器,这中间的拷贝和转换时间会直接叠加到帧延迟上。

排查性能问题时,建议用两个指标分开看:帧回调间隔和编码器输入时间。帧回调间隔如果超过 40ms,说明捕获链路本身有瓶颈;回调正常但编码器输入时间很高,那问题在格式转换。用OH_AVBuffer直接流转,不经过额外缓冲,可以省掉不少无谓分配。实测下来,在不做任何转换直接送编码器时,延迟能压低到 100ms 以内;一旦加了两次转换,延迟很容易翻倍。

5. 扩展玩法:把屏幕捕获做成真正可用的模块

5.1 动态填充加窗口跟踪

很多录屏模块不只是录整个屏幕,而是要跟踪某个窗口。窗口可能在屏幕上移动,也可能被其他窗口盖住一部分。如果只做静态捕获区域,出来就是一段“窗口飞出画面”的废视频。

一个可行方案是:每隔几百毫秒查询一次目标窗口的位置和尺寸,转换成屏幕坐标系,更新捕获区域。如果窗口被完全遮挡,就把填充模式切到一种淡出效果,或者直接暂停编码器。配合动态隐私过滤,可以让录制内容始终聚焦在业务重点上。这部分逻辑用 C API 写会舒服很多,因为可以放在一个 native 线程里做高频轮询,不用频繁跨语言做数据同步。

5.2 多实例捕获和分布式场景

Media Kit 的屏幕捕获 C API 允许在同一个应用里创建多个捕获会话。这可以用来做画中画:一个会话捕获整个屏幕,另一个会话捕获某个小窗口,再把小窗口的画面叠加到大画面上面。叠加时需要把第二路流纹理化,计算好位置和缩放,再送进编码器。这个场景的填充模式选择也要分两层:底层用 AspectFill 铺满背景,上层用 AspectFit 完整展示窗口内容。

我实际做过的一个项目里,还需要把屏幕捕获的数据通过网络发送到另一台设备,在那边解码显示。这时候填充模式不只是影响本地画面,还会影响远端接收端的黑边区域。远端设备如果跑的是不同分辨率的程序,需要能根据黑边信息自动适配画面比例。所以建议在自定义包头里带上源分辨率、输出分辨率、填充模式三个字段,接收端根据这组信息做二次适配,而不是靠猜。

5.3 和编码器串联的正确姿势

屏幕捕获和编码器串联的关键,是不要再引入额外的拷贝层。系统给的OH_AVBuffer可以直接喂给编码器,只要保证 buffer 的生命周期同步。我用过一个比较顺手的模式:捕获回调里加一个轻量队列,队列元素只保存 buffer 指针和时间戳,编码线程从队列取走 buffer 后立刻提交给编码器。队列积压超过一定阈值,就主动丢旧帧,保证实时性优先于完整帧率。

如果要做截图功能,直接从捕获数据流里抽一帧转成 JPEG 也可以,但注意截图的尺寸要单独算,不能直接依赖输出尺寸。截图输出一般用源画面原始分辨率更清晰,而推流画面为了吞吐量可能已经缩过尺寸。


最后分享一个我自己的调试习惯。每接入一台新设备做屏幕捕获适配时,不要急着接业务逻辑,先写一个最小用例:创建会话,设置固定分辨率和 AspectFit,启动后只打日志记录每帧回调间隔和首帧尺寸。连跑五分钟,把回调间隔的抖动范围和输出画面尺寸都记下来。这个小用例能暴露绝大多数设备差异问题,省去后面业务联调时到处猜的麻烦。屏幕捕获这层东西,看着简单,真正做细了,坑还是很多的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询