大牛直播SDK 鸿蒙NEXT 同屏 RTMP推流、轻量级RTSP服务模块集成指南
本文基于大牛直播SDK(SmartMediaKit)鸿蒙NEXT同屏推流Demo,系统介绍屏幕、麦克风与系统音采集,软硬编码、RTMP推流、轻量级RTSP服务、实时录像与快照等模块的集成方法,涵盖APP名称修改、libSmartPublisher.so拷贝、权限配置、ArkTS与NAPI接口封装、事件回调和横竖屏热切处理,适合无纸化会议、移动巡检、远程协作等低延迟音视频场景参考。
一、概述
随着鸿蒙 NEXT 在行业终端、无纸化会议、移动执法、现场巡检、远程协作等场景中的落地,越来越多业务需要在端侧完成屏幕、麦克风、系统声音的实时采集,并将音视频数据低延迟推送到服务器或在局域网内直接分发。
传统做法通常是:上层单独完成屏幕采集、音频采集、编码、协议封装、推流、录像、快照等多个环节,然后再分别对接 RTMP Server、RTSP Server 或录像模块。这种方式看似灵活,但在实际工程中会遇到大量问题:采集和编码时序复杂、横竖屏切换容易花屏、音视频同步难控制、后台保活需要额外处理、RTSP 局域网分发还要单独部署服务端。
基于大牛直播SDK(SmartMediaKit)的鸿蒙 NEXT 推流方案,则将这些能力统一封装到 libSmartPublisher.so 中,上层 ArkTS 主要负责 UI、参数配置、权限申请、状态管理和业务编排,底层 SDK 负责音视频编码、RTMP 打包推送、轻量级 RTSP 服务、录像、快照、事件回调等核心能力。
本文以鸿蒙 NEXT 屏幕推流 Demo 工程为例,介绍如何集成大牛直播SDK的 RTMP 直播推流模块和轻量级 RTSP 服务模块,实现:
- 屏幕采集;
- 麦克风采集;
- 系统声音采集;
- 系统音 + 麦克风混音;
- H.264 / H.265 编码;
- 软编码、硬编码 Buffer 模式、硬编码 Surface 模式;
- RTMP 实时推流;
- 轻量级 RTSP 服务分发;
- 实时录像;
- 快照;
- 横竖屏和分辨率切换;
- 运行日志和事件回调展示。
二、整体架构

本工程核心结构可以抽象为:
┌──────────────────────────────────────────────┐
│ SmartScreenPublisherPage.ets │
│ UI 层:参数配置、按钮操作、状态展示、日志展示 │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ PublisherScreenEngine.ets │
│ Engine 层:统一编排采集、推流、RTSP、录像 │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ SmartPublisherScreenWrapper.ets │
│ Wrapper 层:封装 SmartPublisher 生命周期 │
│ open / applyConfig / start / stop / recorder │
│ RTSP service / RTSP stream / media input │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ SmartPublisherNative.ets │
│ ArkTS Native 封装层:类型化调用 NAPI 接口 │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ libSmartPublisher.so │
│ C/C++ Native 核心:采集、编码、推流、RTSP、录像 │
└──────────────────────────────────────────────┘
屏幕采集侧则通过:
ArktsScreenCaptureAdapterImpl.ets
│
▼
ScreenCaptureSourceNative.ets
│
▼
libSmartPublisher.so 内部 OH_AVScreenCapture 封装
完成屏幕、系统音、麦克风数据的采集和回调。
三、工程目录说明
典型目录结构如下:
SmartScreenPublisherOhos/
├── AppScope/
│ ├── app.json5
│ └── resources/base/element/string.json
│
├── entry/
│ ├── libs/
│ │ ├── arm64-v8a/
│ │ │ └── libSmartPublisher.so
│ │ └── x86_64/
│ │ └── libSmartPublisher.so
│ │
│ ├── oh-package.json5
│ │
│ └── src/main/
│ ├── cpp/types/libSmartPublisher/
│ │ ├── index.d.ts
│ │ └── oh-package.json5
│ │
│ ├── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets
│ │ │
│ │ ├── media/
│ │ │ ├── SmartPublisherNative.ets
│ │ │ ├── SmartPublisherScreenWrapper.ets
│ │ │ ├── PublisherScreenEngine.ets
│ │ │ ├── ScreenCaptureSourceNative.ets
│ │ │ ├── ArktsScreenCaptureAdapterImpl.ets
│ │ │ ├── ScreenPublishConfigUtils.ets
│ │ │ ├── SmartPublisherEventFormatter.ets
│ │ │ ├── NTSmartEventID.ets
│ │ │ └── NTLicenseHelper.ets
│ │ │
│ │ └── pages/
│ │ ├── SmartScreenPublisherPage.ets
│ │ ├── RecorderManagerPage.ets
│ │ └── RecorderPlaybackPage.ets
│ │
│ ├── module.json5
│ └── resources/base/profile/main_pages.json
四、修改 APP 名称
鸿蒙 NEXT 工程中,APP 桌面显示名称主要由 AppScope/resources/base/element/string.json 中的 app_name 控制。
示例:
{
"string": [
{
"name": "app_name",
"value": "SmartPublisherDemo"
}
]
}
如果希望改成中文名称,比如“大牛推流Demo”,可以修改为:
{
"string": [
{
"name": "app_name",
"value": "大牛推流Demo"
}
]
}
同时,AppScope/app.json5 中通过:
{
"app": {
"bundleName": "com.smartpublisher.demo",
"vendor": "SmartPublisher",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
指定了应用包名、厂商信息、版本号、图标和应用名称。
如果要正式发布或给客户交付,建议同步检查:
"bundleName": "com.smartpublisher.demo"
包名不要和其他应用冲突。
需要注意的是,大牛直播SDK授权校验可能会读取应用名称、包名、签名指纹等运行时信息。当前工程中 NTLicenseHelper.ets 会通过系统接口获取:
RuntimeAppIdentity {
appName: string;
bundleName: string;
appIdentifier: string;
fingerprint: string;
}
如果客户工程修改了 APP 名称、bundleName 或签名证书,授权信息也应保持一致,避免出现 native 层 open 失败或 SDK 授权校验失败的问题。
五、拷贝 libSmartPublisher.so 到指定目录
示例工程中已经预留了 so 库目录:
entry/libs/
├── arm64-v8a/
└── x86_64/
真实集成时,需要根据目标设备 ABI 放置对应版本的 libSmartPublisher.so。
手机真机一般放:
entry/libs/arm64-v8a/libSmartPublisher.so
如果需要模拟器调试,可以放:
entry/libs/x86_64/libSmartPublisher.so
同时,工程中已经配置了 ArkTS 侧类型声明依赖:
{
"modelVersion": "5.0.0",
"name": "entry",
"version": "1.0.0",
"description": "SmartPublisher Demo Entry Module",
"dependencies": {
"libSmartPublisher.so": "file:./src/main/cpp/types/libSmartPublisher"
}
}
对应类型声明文件位于:
entry/src/main/cpp/types/libSmartPublisher/index.d.ts
ArkTS 层通过:
import smartpublisher from 'libSmartPublisher.so';
调用 native 导出的 NAPI 接口。
如果编译时报类似:
Cannot find module 'libSmartPublisher.so'
或运行时报 native 接口不存在,通常需要重点检查:
entry/libs/arm64-v8a/libSmartPublisher.so 是否存在;
ABI 是否匹配当前设备;
entry/oh-package.json5 中依赖是否正确;
index.d.ts 中声明的接口是否与当前 so 导出的接口一致;
DevEco 是否重新 Sync / Clean / Rebuild。
六、module.json5 权限与后台模式配置
屏幕推流涉及网络、麦克风、后台保活等能力,module.json5 中需要配置对应权限。
示例工程中核心配置如下:
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"label": "$string:app_name",
"orientation": "auto_rotation_restricted",
"backgroundModes": [
"audioRecording",
"dataTransfer"
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:microphone_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.GET_NETWORK_INFO"
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
}
}
其中:
INTERNET:用于 RTMP 推流、RTSP 服务访问;
MICROPHONE:用于麦克风采集;
GET_NETWORK_INFO:用于网络状态相关能力;
KEEP_BACKGROUND_RUNNING:用于推流、录像场景下的后台保活;
backgroundModes 中的 audioRecording 和 dataTransfer:用于录音和数据传输类长时任务。
如果后续要将快照保存到系统图库,而不是仅保存到应用沙盒目录,还需要根据鸿蒙 NEXT 的图库访问要求补充对应媒体写入权限和 PhotoAccessHelper 相关逻辑。
七、EntryAbility 初始化
EntryAbility.ets 中主要完成窗口创建、方向策略设置和页面加载。
示例:
onWindowStageCreate(windowStage: window.WindowStage): void {
this.mainWindow = windowStage.getMainWindowSync();
this.mainWindow.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
windowStage.loadContent('pages/SmartScreenPublisherPage', (err) => {
if (err.code) {
hilog.error(0x0000, 'SmartPublisher', 'loadContent failed: %{public}s', JSON.stringify(err));
return;
}
hilog.info(0x0000, 'SmartPublisher', 'loadContent success');
});
}
这里建议保持 AUTO_ROTATION_RESTRICTED,让 APP 能够响应横竖屏方向变化,同时又避免完全失控的自动旋转。
屏幕采集推流场景中,方向处理非常关键。如果上层 UI、采集分辨率、编码器输入尺寸、RTMP/RTSP 输出尺寸不同步,很容易出现画面拉伸、旋转 90 度、花屏、绿边、横竖屏切换后编码器不出帧等问题。
八、核心类型声明:index.d.ts
entry/src/main/cpp/types/libSmartPublisher/index.d.ts 是 ArkTS 调用 native so 的类型声明文件。它定义了:
推流实例创建和关闭;
- SDK 授权接口;
- RTMP 推流接口;
- 软编码参数;
- 硬编码参数;
- OHOS Surface 输入硬编码接口;
- 音频参数;
- 录像接口;
- 快照接口;
- RTSP Server 接口;
- RTSP Stream 接口;
- 屏幕采集 Source 接口;
- 事件回调接口。
典型接口包括:
SmartPublisherOpen(audioOpt: number, videoOpt: number, width: number, height: number): number;
SmartPublisherClose(handle: number): number;
SetSmartPublisherEventCallback(handle: number, callback: PublisherEventCallback | null): number;
SmartPublisherSetURL(handle: number, url: string): number;
SmartPublisherStartPublisher(handle: number): number;
SmartPublisherStopPublisher(handle: number): number;
录像相关接口:
SmartPublisherCreateFileDirectory(path: string): number;
SmartPublisherSetRecorderDirectory(handle: number, path: string): number;
SmartPublisherSetRecorderFileMaxSize(handle: number, sizeMB: number): number;
SmartPublisherStartRecorder(handle: number): number;
SmartPublisherStopRecorder(handle: number): number;
快照相关接口:
SmartPublisherSaveCurImage(handle: number, fileName: string): number;
CaptureImage(handle: number, format: number, quality: number, fileName: string, userData: string): number;
轻量级 RTSP 服务接口:
InitRtspServer(reserve: number): number;
OpenRtspServer(reserve: number): number;
SetRtspServerPort(serverHandle: number, port: number): number;
StartRtspServer(serverHandle: number, reserve: number): number;
StopRtspServer(serverHandle: number): number;
GetRtspServerClientSessionNumbers(serverHandle: number): number;
将推流数据发布到内置 RTSP 服务:
SetRtspStreamName(handle: number, name: string): number;
AddRtspStreamServer(handle: number, serverHandle: number, reserve: number): number;
StartRtspStream(handle: number, reserve: number): number;
StopRtspStream(handle: number): number;
屏幕采集 Source 接口:
SmartPublisherCreateScreenSource(
width: number,
height: number,
sampleRate: number,
channels: number,
enableInnerAudio: boolean,
enableMic: boolean
): number;
SmartPublisherStartScreenSource(handle: number): number;
SmartPublisherStartScreenSourceWithSurface(handle: number, surfaceId: string): number;
SmartPublisherResizeScreenSourceCanvas(handle: number, width: number, height: number): number;
这些接口是 ArkTS 与 native SDK 的边界。正式集成时,不建议业务层直接大量调用这些底层接口,而是通过 SmartPublisherNative.ets、SmartPublisherScreenWrapper.ets 和 PublisherScreenEngine.ets 做统一封装。
九、编码模式设计:软编、硬编 Buffer、硬编 Surface
示例工程中提供了 5 种编码选项:
private readonly encoderPresets: EncoderEntry[] = [
new EncoderEntry('软编 H.264', PublisherVideoEncoderType.SOFT_H264, false),
new EncoderEntry('硬编 H.264', PublisherVideoEncoderType.HW_H264, false),
new EncoderEntry('硬编 H.265', PublisherVideoEncoderType.HW_H265, false),
new EncoderEntry('硬编 H.264(Surface)', PublisherVideoEncoderType.HW_H264, true),
new EncoderEntry('硬编 H.265(Surface)', PublisherVideoEncoderType.HW_H265, true),
]
这几种模式的差异如下。
1. 软编 H.264
屏幕采集回调 RGBA/RGBX 数据到 ArkTS,再投递给 SDK:
this.nativePublisher.onRGBAData(frame, rowStride, width, height)
优点是兼容性好,便于调试,适合功能验证和部分低分辨率场景。
2. 硬编 H.264 / H.265 Buffer 模式
屏幕采集回调 RGBA/RGBX 数据到 ArkTS,但视频投递走 Layer 路径:
this.nativePublisher.postLayerImageRGBX8888(
0, 0, 0,
frame,
0,
rowStride,
width,
height,
false,
false,
0,
0,
0,
0
)
这里没有直接走普通 onRGBAData(),而是通过 Layer 通道把 RGBA/RGBX 标准化后送入硬编码 Buffer 路径。这样做的好处是更容易兼容鸿蒙硬编码器对 stride、slice height、颜色格式和对齐方式的要求,避免非标准分辨率下出现花屏、绿边、颜色错乱等问题。
3. 硬编 H.264 / H.265 Surface 模式
Surface 模式下,视频数据不再回调到 ArkTS 层,而是由 OH_AVScreenCapture 直接写入编码器输入 Surface:
OH_AVScreenCapture
│
▼
Encoder Input Surface
│
▼
libSmartPublisher.so 编码 / 打包 / 推送
ArkTS 层只负责拿到 surfaceId:
const surfaceId: string = this.publisher.prepareEncoderInputSurface(
next.width,
next.height,
next.fps
)
this.capturer.setEncoderSurfaceId(surfaceId)
然后启动 Surface 模式采集:
SmartPublisherStartScreenSourceWithSurface(handle, surfaceId)
Surface 模式减少了视频帧在 ArkTS 层的回调和拷贝,理论上延迟更低、CPU 占用更低,更适合正式推屏场景。
十、屏幕采集流程

屏幕采集由 ArktsScreenCaptureAdapterImpl.ets 和 ScreenCaptureSourceNative.ets 封装。
核心流程为:
SmartScreenPublisherPage 点击“申请屏幕权限”
│
▼
PublisherScreenEngine.prepare(config)
│
▼
构建 ScreenCaptureStartOptions
│
▼
ArktsScreenCaptureAdapterImpl.start(options)
│
▼
ScreenCaptureSourceNative.create()
│
▼
SmartPublisherCreateScreenSource()
│
▼
SmartPublisherStartScreenSource()
或
SmartPublisherStartScreenSourceWithSurface()
ScreenCaptureStartOptions 主要包含:
export interface ScreenCaptureStartOptions {
width: number
height: number
fps: number
enableMic: boolean
enableInnerAudio: boolean
showCursor: boolean
canvasRotation: boolean
useEncoderSurface: boolean
}
构建逻辑在 PublisherScreenEngine.ets 中:
private buildCaptureOptions(config: ScreenPublishConfig): ScreenCaptureStartOptions {
return {
width: config.width,
height: config.height,
fps: config.fps,
enableMic: this.needMic(config.audioOutputType),
enableInnerAudio: this.needInnerAudio(config.audioOutputType),
showCursor: true,
canvasRotation: config.width < config.height,
useEncoderSurface: this.useSurfaceInput(config),
}
}
这里有几个关键点:
竖屏时 width < height,启用 canvasRotation;
是否采集麦克风由音频输出类型决定;
是否采集系统音由音频输出类型决定;
Surface 硬编时,视频不再走 ArkTS buffer 回调;
Buffer 模式下,视频帧仍会通过回调进入 Engine,再投递给 SDK。
十一、音频采集:麦克风、系统音、混音

示例工程支持 4 种音频模式:
private readonly audioPresets: AudioEntry[] = [
new AudioEntry('静音', ScreenAudioOutputType.NONE),
new AudioEntry('仅麦克风', ScreenAudioOutputType.MIC),
new AudioEntry('仅系统音', ScreenAudioOutputType.SYSTEM),
new AudioEntry('系统音 + 麦克风', ScreenAudioOutputType.SYSTEM_AND_MIC),
]
对应枚举:
export enum ScreenAudioOutputType {
NONE = 0,
MIC = 1,
SYSTEM = 2,
SYSTEM_AND_MIC = 3,
}
音频采集回调后,Engine 层会先切成 10ms PCM 帧,再投递给 SDK:
this.innerFramer.push(chunk.buffer, (frame, sr, ch, pcs) => {
this.publisher.postSystemAudioPCM(frame, sr, ch, pcs)
})
麦克风音频:
this.micFramer.push(chunk.buffer, (frame, sr, ch, pcs) => {
this.publisher.postMicAudioPCM(frame, sr, ch, pcs)
})
如果是“系统音 + 麦克风”模式,麦克风走混音第二路:
this.nativePublisher.onMixPCMData(
1,
pcm10ms,
0,
pcm10ms.byteLength,
sampleRate,
channels,
perChannelSamples
)
对应地,Wrapper 层会启用音频混音:
this.nativePublisher.setAudioMix(
config.audioOutputType === ScreenAudioOutputType.SYSTEM_AND_MIC
)
这样可以实现屏幕声音和麦克风讲解声音同时进入 RTMP 推流、RTSP 分发和本地录像。
十二、RTMP 推流集成流程

RTMP 推流核心流程如下:
1. 构建 ScreenPublishConfig
2. Engine.prepare(config)
3. SmartPublisherOpen()
4. 设置 URL / FPS / GOP / 编码器 / 音频参数
5. 启动屏幕采集
6. SmartPublisherStartPublisher()
7. SDK 事件回调上报连接状态
页面层点击“开始推流”后:
private async doStartPublish(): Promise<void> {
if (!this.engine) return
if (!this.isPrepared) {
this.appendLog('请先点击"申请屏幕权限"')
return
}
this.refreshDerivedTexts()
const config: ScreenPublishConfig = this.buildConfig(false)
const ok: boolean = await this.engine.startPublish(config)
this.appendLog(`startPublish ret=${ok}`)
this.syncUiState()
}
Engine 层:
async startPublish(config: ScreenPublishConfig): Promise<boolean> {
const next: ScreenPublishConfig = cloneScreenConfig(config)
next.enableRecorder = false
if (!await this.prepare(next)) return false
const ok: boolean = this.publisher.startPublisherDirect()
this.callbacks.onLog?.(`engine: publisher.startPublisherDirect ret=${ok}`)
this.callbacks.onConfigUpdated?.(cloneScreenConfig(this.lastConfig ?? next))
return ok
}
Wrapper 层最终调用:
this.nativePublisher.startPublisher()
也就是:
SmartPublisherStartPublisher(handle)
停止推流则调用:
SmartPublisherStopPublisher(handle)
需要注意的是,当前设计中“申请屏幕权限”和“开始推流”是分开的。这样做的好处是:
用户可以先准备采集管线;
可以在同一个采集管线上同时启动 RTMP、RTSP、录像;
停止其中一路输出时,不必立即释放整个采集和编码链路;
只有 RTMP、RTSP、录像都停止后,才释放 Publisher 和 ScreenSource。
这种设计比“每点一次推流就重新申请屏幕权限”更适合实际产品。
鸿蒙NEXT无纸化同屏端到端时延测试
十三、轻量级 RTSP 服务集成流程
轻量级 RTSP 服务的目标是:不额外部署 RTSP Server,直接在鸿蒙 NEXT 设备端启动一个内置 RTSP 服务,让局域网内的播放器直接拉流。

典型流程:
1. InitRtspServer()
2. OpenRtspServer()
3. SetRtspServerPort()
4. StartRtspServer()
5. 设置 RTSP streamName
6. AddRtspStreamServer()
7. StartRtspStream()
8. 通过事件回调拿到 RTSP URL
页面层配置端口和流名称:
@State rtspServerPortText: string = '38554'
@State rtspStreamName: string = 'stream1'
启动 RTSP 服务:
private doStartRtspService(): void {
if (!this.engine) return
const port: number = this.safeInt(this.rtspServerPortText, 38554)
const ok: boolean = this.engine.startRtspService(port)
this.appendLog(`startRtspService ret=${ok}`)
this.syncUiState()
}
Engine 层:
startRtspService(port: number): boolean {
const ok: boolean = this.publisher.startRtspService(port)
this.callbacks.onLog?.(`engine: publisher.startRtspService ret=${ok}`)
return ok
}
Wrapper 层:
startRtspService(port: number, userName: string = '', password: string = ''): boolean {
if (this.rtspServer.init() !== DANIULIVE_RETURN_OK) return false
if (this.rtspServer.open() !== DANIULIVE_RETURN_OK) {
this.rtspServer.uninit()
return false
}
if (!this.startRtspServerInternal(port, userName, password, hasUser)) {
this.rtspServer.close()
this.rtspServer.uninit()
return false
}
this.rtspServerPort = port
this.isRtspServiceRunning = true
return true
}
发布 RTSP 流:
async startRtspStream(config: ScreenPublishConfig, streamName: string): Promise<boolean> {
const next: ScreenPublishConfig = cloneScreenConfig(config)
next.enableRecorder = false
if (!await this.prepare(next)) return false
const ok: boolean = this.publisher.startRtspStreamDirect(streamName)
this.callbacks.onLog?.(`engine: publisher.startRtspStreamDirect ret=${ok}`)
return ok
}
Wrapper 层调用:
this.nativePublisher.setRtspStreamName(streamName)
this.nativePublisher.clearRtspStreamServer()
this.nativePublisher.addRtspStreamServer(this.rtspServer.getHandle())
this.nativePublisher.startRtspStream()
当 SDK 生成 RTSP 播放 URL 后,会通过事件回调上报:
EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL
事件中 strParam 就是可播放 URL:
case EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL: {
const url: string = (record.strParam ?? '').trim()
if (url.length > 0) {
this.rtspPlaybackUrl = url
}
this.syncUiState()
return
}
页面上会显示:
RTSP:rtsp://设备IP:38554/stream1
局域网内客户端可以直接使用大牛直播SDK RTSP 播放器、VLC之类播放器拉取该地址。
鸿蒙NEXT无纸化同屏之轻量级RTSP服务器端到端时延测试
十四、实时录像
实时录像与 RTMP 推流、RTSP 服务共用同一个 Publisher 实例和采集管线。

默认录像目录:
private recordDir: string = '/data/storage/el2/base/files/smartpublisher_record'
private recordFileMaxSizeMB: number = 200
开始录像:
private async doStartRecorder(): Promise<void> {
if (!this.engine) return
if (!this.isPrepared) {
this.appendLog('请先点击"申请屏幕权限"')
return
}
const ok: boolean = await this.engine.startRecorder(this.buildConfig(true))
this.appendLog(`startRecorder ret=${ok}`)
this.syncUiState()
}
Engine 层:
async startRecorder(config: ScreenPublishConfig): Promise<boolean> {
const next: ScreenPublishConfig = cloneScreenConfig(config)
next.enableRecorder = true
if (!await this.prepare(next)) return false
const effective: ScreenPublishConfig = this.lastConfig ? cloneScreenConfig(this.lastConfig) : next
effective.enableRecorder = true
const ok: boolean = this.publisher.applyRecorderConfigAndStart(effective)
this.callbacks.onLog?.(`engine: publisher.applyRecorderConfigAndStart ret=${ok}`)
this.callbacks.onConfigUpdated?.(cloneScreenConfig(effective))
return ok
}
Wrapper 层只下发录像相关配置,避免重复初始化编码器:
SmartPublisherNative.createFileDirectory(config.recordDir)
this.nativePublisher.setRecorderDirectory(config.recordDir)
this.nativePublisher.setRecorderFileMaxSize(config.recordFileMaxSizeMB)
this.nativePublisher.setRecorderVideo(true)
this.nativePublisher.setRecorderAudio(config.audioOutputType !== ScreenAudioOutputType.NONE)
this.nativePublisher.startRecorder()
这里有个重要设计点:录像启动时不重新下发完整编码参数,而是只下发录像目录、文件大小、音视频开关等 recorder 专属参数。
这样可以避免在 RTMP 推流或 RTSP 分发运行期间,因重复 applyConfig 触发编码器重建,导致画面抖动、断流或 Surface 失效。
录像事件包括:
EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE
EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED
事件格式化:
case EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE:
return `开始一个新的录像文件: ${safeStr}`
case EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED:
return `已生成一个录像文件: ${safeStr} (时长 ${param1} ms)`
这样页面日志中可以直接看到每个录像文件的完整路径,便于测试和问题定位。
十五、快照能力
推流端快照接口已经在 SmartPublisherNative.ets 中封装:
saveCurImage(path: string): number {
return smartpublisher.SmartPublisherSaveCurImage(this.handle, path);
}
captureImage(format: number, quality: number, path: string, userData: string = ''): number {
return smartpublisher.CaptureImage(this.handle, format, quality, path, userData);
}
底层接口支持:
SmartPublisherSaveCurImage(handle, fileName)
CaptureImage(handle, format, quality, fileName, userData)
其中 CaptureImage() 可指定格式和质量:
format = 0:JPEG
format = 1:PNG
quality = 1~100
快照完成后,SDK 会上报:
EVENT_DANIULIVE_ERC_PUBLISHER_CAPTURE_IMAGE
格式化日志:
case EVENT_DANIULIVE_ERC_PUBLISHER_CAPTURE_IMAGE:
return param1 === 0
? `截取快照成功,路径: ${safeStr}`
: `截取快照失败,路径: ${safeStr}`
实际产品中可以在页面增加“快照”按钮,逻辑类似:
const fileName = `/data/storage/el2/base/files/snapshot_${Date.now()}.jpg`
const ret = this.nativePublisher.captureImage(0, 80, fileName, '')
如果要保存到系统图库,需要在鸿蒙 NEXT 侧补充图库写入权限和 PhotoAccessHelper 逻辑;如果只保存到应用沙盒目录,则实现更简单。
十六、事件回调设计

推流端事件从 native 层一路透传到页面:
libSmartPublisher.so
│
▼
SmartPublisherNative.setEventCallback()
│
▼
SmartPublisherScreenWrapper.publisherEventListener
│
▼
PublisherScreenEngine.callbacks.onPublisherEvent
│
▼
SmartScreenPublisherPage.handlePublisherEvent()
事件 ID 定义在 NTSmartEventID.ets 中,与 Android 端保持一致:
export const EVENT_DANIULIVE_ERC_PUBLISHER_STARTED = EVENT_DANIULIVE_PUBLISHER_SDK | 0x1
export const EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING = EVENT_DANIULIVE_PUBLISHER_SDK | 0x2
export const EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED = EVENT_DANIULIVE_PUBLISHER_SDK | 0x3
export const EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED = EVENT_DANIULIVE_PUBLISHER_SDK | 0x4
export const EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED = EVENT_DANIULIVE_PUBLISHER_SDK | 0x5
export const EVENT_DANIULIVE_ERC_PUBLISHER_STOP = EVENT_DANIULIVE_PUBLISHER_SDK | 0x6
事件格式化集中在 SmartPublisherEventFormatter.ets 中:
export function formatPublisherEvent(
eventId: number,
param1: number,
param2: number,
strParam: string,
): string {
switch (eventId) {
case EVENT_DANIULIVE_ERC_PUBLISHER_STARTED:
return '开始推流..'
case EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING:
return '连接中..'
case EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED:
return '连接成功..'
case EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED:
return '连接断开..'
case EVENT_DANIULIVE_ERC_PUBLISHER_STOP:
return '停止推流..'
}
}
这样做的好处是:
事件值和 native 层保持一致;
页面层不用解析 eventId 细节;
日志格式统一;
后续新增事件只需要集中维护 formatter;
RTMP、RTSP、录像、快照事件都可以统一显示在“最近事件”和“运行日志”中。
十七、常见问题
1. open 返回 0
一般需要检查:
libSmartPublisher.so 是否放到 entry/libs/arm64-v8a/;
ABI 是否匹配;
SmartPublisherSetAppIdentityResolver 是否注册成功;
APP 名称、bundleName、签名信息是否和授权匹配;
index.d.ts 中接口是否和 so 导出一致。
2. import libSmartPublisher.so 失败
检查:
"dependencies": {
"libSmartPublisher.so": "file:./src/main/cpp/types/libSmartPublisher"
}
以及:
entry/src/main/cpp/types/libSmartPublisher/index.d.ts
是否存在。
3. RTMP 推流连接失败
检查:
RTMP URL 是否正确;
手机和服务器网络是否互通;
服务器端口是否开放;
是否使用了正确的 app/streamName;
日志中是否出现 连接中、连接成功、连接失败 事件。
4. RTSP 服务启动成功但客户端拉不到流
检查:
RTSP 服务是否已启动;
RTSP Stream 是否已发布;
是否收到了 EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL 事件;
手机 IP 是否和播放器在同一局域网;
端口如 38554 是否被防火墙或网络策略拦截;
客户端拉流地址是否完整。
5. 录像没有文件
检查:
是否先申请了屏幕采集权限;
录像目录是否创建成功;
SmartPublisherSetRecorderDirectory() 是否返回成功;
SmartPublisherStartRecorder() 是否返回成功;
是否收到 开始一个新的录像文件 事件。
6. 快照失败
检查:
当前是否已经打开 Publisher;
是否有视频帧输入;
快照路径是否可写;
如果保存到图库,是否补充了图库权限和写入逻辑。
总结
基于大牛直播SDK(SmartMediaKit)的鸿蒙 NEXT 屏幕推流方案,不只是一个简单的 RTMP 推流 Demo,而是一套较完整的端侧实时音视频采集、编码、推送、分发和录像架构。
它的核心价值在于:
- 屏幕、系统音、麦克风统一采集;
- RTMP 推流和轻量级 RTSP 服务共用同一套采集编码链路;
- 支持实时录像和快照;
- 支持软编、硬编 Buffer、硬编 Surface 多种编码模式;
- 支持横竖屏和分辨率切换;
- 通过事件回调统一管理连接状态、RTSP URL、录像文件、快照结果;
- ArkTS 层保持清晰,底层复杂音视频能力由 native SDK 承担。
对于无纸化会议、移动执法、远程巡检、教育培训、医疗会诊、工业现场可视化等场景,这种“端侧采集 + RTMP 上云 + RTSP 内网分发 + 本地录像留档”的组合非常实用。
如果业务侧只需要公网直播,可以启用 RTMP 推流;
如果业务侧需要局域网低延迟观看,可以启用轻量级 RTSP 服务;
如果业务侧需要证据留存或过程追溯,可以同步开启录像;
如果业务侧需要页面截图或关键画面保存,可以接入快照接口。
从工程角度看,最推荐的集成方式是:保留 PublisherScreenEngine 这种统一编排层,不要让页面直接操作大量 native 接口。这样既能减少状态错乱,也方便后续扩展摄像头采集、GB28181 接入、RTSP 推送、SEI 数据发送等更多能力。
📎 CSDN官方博客:音视频牛哥-CSDN博客
更多推荐

所有评论(0)