一、概述

随着鸿蒙 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 中的 audioRecordingdataTransfer:用于录音和数据传输类长时任务。

如果后续要将快照保存到系统图库,而不是仅保存到应用沙盒目录,还需要根据鸿蒙 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.etsSmartPublisherScreenWrapper.etsPublisherScreenEngine.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.etsScreenCaptureSourceNative.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博客 

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐