在鸿蒙与 Electron 的融合开发中,跨端音视频流传输是极具代表性的场景,比如 Electron 桌面端接收鸿蒙摄像头的实时视频流、鸿蒙设备播放 Electron 端的音频资源等。鸿蒙媒体服务(Media Service)提供了强大的音视频采集、编码和解码能力,将其与 Electron 结合,可实现 “鸿蒙采集、Electron 展示” 的跨端音视频协同体系。本文将详细讲解如何集成鸿蒙媒体能力到 Electron 应用中,实现鸿蒙音视频流向 Electron 端的实时传输与播放,附带完整代码案例和工程化配置。
一、核心价值与应用场景
1. 核心价值
- 原生媒体能力复用:借助鸿蒙设备的摄像头、麦克风原生采集能力,避免 Electron 端外设适配的繁琐问题。
- 低延迟流传输:采用 WebRTC(实时通信)协议实现音视频流传输,延迟控制在毫秒级,满足实时性需求。
- 技术栈轻量化:前端开发者使用 HTML5 的
video/audio标签即可播放跨端音视频流,无需深入音视频编解码底层。
2. 典型应用场景
- 视频监控:鸿蒙智能摄像头采集的实时视频流,Electron 监控端实时播放并录制。
- 远程会议:鸿蒙手机端的音视频流,通过 Electron 桌面端共享到会议系统。
- 工业质检:鸿蒙工业设备的摄像头采集产品画面,Electron 端实时显示并进行 AI 分析。
二、环境搭建与前置准备
1. 基础环境要求
- Electron:Node.js(v18+)、Electron(v28+)、
webrtc-adapter(WebRTC 兼容处理)、ws(信令服务) - 鸿蒙:DevEco Studio(最新版)、鸿蒙 SDK(API 10+)、鸿蒙真机(带摄像头 / 麦克风,模拟器暂不支持媒体采集)
- 网络:Electron 与鸿蒙设备处于同一局域网,确保 UDP/TCP 端口畅通(WebRTC 依赖)
2. 工程化初始化
2.1 创建 Electron 工程
bash
运行
# 初始化项目 mkdir harmony-electron-media && cd harmony-electron-media npm init -y # 安装核心依赖 npm install electron electron-builder webrtc-adapter ws --save npm install nodemon --save-dev2.2 配置 package.json
json
{ "name": "harmony-electron-media", "version": "1.0.0", "main": "main/index.js", "scripts": { "start": "electron .", "dev": "nodemon --exec electron .", "build": "electron-builder" }, "build": { "appId": "com.example.harmonymedia", "productName": "HarmonyElectronMedia", "directories": { "output": "dist" }, "win": { "target": "nsis" }, "mac": { "target": "dmg" }, "linux": { "target": "deb" } } }2.3 鸿蒙工程配置(媒体权限与 WebRTC 依赖)
在鸿蒙工程的entry/src/main/module.json5中添加媒体采集权限:
json5
{ "module": { "name": "entry", "type": "entry", "requestPermissions": [ { "name": "ohos.permission.INTERNET" }, { "name": "ohos.permission.CAMERA" }, { "name": "ohos.permission.MICROPHONE" }, { "name": "ohos.permission.DISTRIBUTED_DATASYNC" }, { "name": "ohos.permission.RECORD_AUDIO" } ], "distributedConfiguration": { "deviceCommunication": true } } }三、核心原理:WebRTC 音视频流传输流程
- 信令服务:通过 WebSocket 搭建简单信令服务,实现 Electron 与鸿蒙设备的 SDP(会话描述协议)、ICE(交互式连接建立)候选地址交换。
- 音视频采集:鸿蒙端通过媒体服务采集摄像头 / 麦克风数据,编码为 RTP 流。
- P2P 连接:基于 WebRTC 建立鸿蒙与 Electron 的 P2P 连接,传输音视频流。
- 流播放:Electron 端通过 HTML5 的
video标签解码并播放接收到的音视频流。
四、核心代码案例:跨端音视频流传输与播放
步骤 1:Electron 端搭建信令服务与 WebRTC 客户端
1.1 Electron 主进程(信令服务 + 窗口管理)
javascript
运行
// main/index.js const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const WebSocket = require('ws'); // 全局变量 let mainWindow; // WebSocket信令服务 let wss; // 创建Electron窗口 function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, '../preload/index.js'), contextIsolation: true, sandbox: false, // 开启WebRTC支持 webSecurity: false, allowRunningInsecureContent: true } }); mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); mainWindow.webContents.openDevTools(); } // 启动WebSocket信令服务(端口:8080) function startSignalingServer() { wss = new WebSocket.Server({ port: 8080 }); console.log('WebSocket信令服务已启动,端口:8080'); // 存储连接的客户端(鸿蒙/Electron) const clients = new Set(); wss.on('connection', (ws) => { clients.add(ws); console.log('新客户端连接,当前客户端数:', clients.size); // 转发消息到其他客户端(信令交换核心) ws.on('message', (data) => { clients.forEach((client) => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(data); } }); }); // 移除断开的客户端 ws.on('close', () => { clients.delete(ws); console.log('客户端断开连接,当前客户端数:', clients.size); }); ws.on('error', (err) => { console.error('客户端连接错误:', err); clients.delete(ws); }); }); } // 应用就绪后初始化 app.whenReady().then(() => { createWindow(); startSignalingServer(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { // 关闭信令服务 if (wss) wss.close(); app.quit(); } }); // 暴露获取本机IP的接口(供渲染进程使用) ipcMain.handle('get-local-ip', () => { const os = require('os'); const interfaces = os.networkInterfaces(); for (const dev in interfaces) { const iface = interfaces[dev]; for (const alias of iface) { if (alias.family === 'IPv4' && !alias.internal) { return alias.address; } } } return '127.0.0.1'; });1.2 Electron 预加载脚本(暴露 API)
javascript
运行
// preload/index.js const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronApi', { // 获取本机IP地址 getLocalIp: () => ipcRenderer.invoke('get-local-ip'), // 监听主进程消息(可选) on: (channel, callback) => { ipcRenderer.on(channel, (event, ...args) => callback(...args)); } });1.3 Electron 渲染进程(WebRTC 客户端 + 视频播放)
html
预览
<!-- renderer/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>鸿蒙Electron音视频流播放</title> <style> body { font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5; } .container { max-width: 1100px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } #videoPlayer { width: 100%; height: 600px; border: 1px solid #eee; border-radius: 8px; background-color: #000; } .status { margin-top: 20px; padding: 10px; border-radius: 4px; background-color: #e9ecef; } button { padding: 10px 20px; margin-top: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:hover { background-color: #0056b3; } </style> </head> <body> <div class="container"> <h1>鸿蒙Electron音视频流播放</h1> <video id="videoPlayer" autoplay playsinline muted="muted"></video> <button onclick="startPlay()">开始接收流</button> <div class="status" id="status">就绪</div> </div> <script src="../node_modules/webrtc-adapter/out/adapter.js"></script> <script> const videoPlayer = document.getElementById('videoPlayer'); const statusDiv = document.getElementById('status'); let ws; let pc; // RTCPeerConnection // 日志更新函数 function updateStatus(message, isError = false) { const now = new Date().toLocaleString(); statusDiv.innerHTML = `[${now}] ${message}`; statusDiv.style.color = isError ? 'red' : 'black'; } // 初始化WebRTC连接 async function initWebRTC() { // 创建RTCPeerConnection实例 const configuration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } // 公共STUN服务器,用于获取公网ICE候选 ] }; pc = new RTCPeerConnection(configuration); updateStatus('初始化WebRTC连接成功'); // 监听ICE候选事件(发送到鸿蒙端) pc.onicecandidate = (event) => { if (event.candidate) { ws.send(JSON.stringify({ type: 'ice-candidate', candidate: event.candidate })); updateStatus('发送ICE候选到鸿蒙端'); } }; // 监听远程流事件(播放流) pc.ontrack = (event) => { updateStatus('接收到鸿蒙端音视频流'); videoPlayer.srcObject = event.streams[0]; }; // 监听连接状态变化 pc.onconnectionstatechange = () => { updateStatus(`WebRTC连接状态:${pc.connectionState}`); if (pc.connectionState === 'failed') { pc.restartIce(); } }; } // 开始接收流 async function startPlay() { // 获取本机IP,连接WebSocket信令服务 const localIp = await window.electronApi.getLocalIp(); ws = new WebSocket(`ws://${localIp}:8080`); ws.onopen = async () => { updateStatus('已连接到信令服务'); await initWebRTC(); // 创建offer(Electron作为接收端,也可由鸿蒙端发起) const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // 发送SDP offer到鸿蒙端 ws.send(JSON.stringify({ type: 'offer', sdp: pc.localDescription })); updateStatus('发送SDP Offer到鸿蒙端'); }; ws.onmessage = async (event) => { const data = JSON.parse(event.data); updateStatus(`收到信令:${data.type}`); try { switch (data.type) { case 'answer': // 处理鸿蒙端的SDP answer await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); break; case 'ice-candidate': // 处理鸿蒙端的ICE候选 await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); break; default: updateStatus(`未知信令类型:${data.type}`, true); break; } } catch (error) { updateStatus(`处理信令失败:${error.message}`, true); } }; ws.onclose = () => { updateStatus('信令服务连接关闭', true); if (pc) { pc.close(); } }; ws.onerror = (error) => { updateStatus(`信令服务连接错误:${error.message}`, true); }; } // 页面关闭时清理资源 window.onbeforeunload = () => { if (ws) ws.close(); if (pc) pc.close(); }; </script> </body> </html>步骤 2:鸿蒙端实现音视频采集与 WebRTC 客户端
2.1 鸿蒙端媒体采集工具类(摄像头 / 麦克风采集)
typescript
运行
// entry/src/main/ets/utils/MediaCaptureUtil.ets import media from '@ohos.multimedia.media'; import stream from '@ohos.multimedia.stream'; import common from '@ohos.app.ability.common'; // 媒体采集实例 let capture: media.AVRecorder | null = null; // 音视频流数据管道 let pipeline: stream.Pipeline | null = null; // 本地媒体流 let localStream: media.MediaStream | null = null; // 初始化媒体采集(摄像头+麦克风) export async function initMediaCapture(context: common.UIAbilityContext) { try { // 创建数据管道 pipeline = stream.createPipeline(); // 创建媒体采集源(摄像头+麦克风) const videoSource = await pipeline.createSource({ sourceType: stream.SourceType.VIDEO, videoSourceConfig: { cameraId: '0', // 后置摄像头,1为前置 resolution: { width: 1280, height: 720 }, frameRate: 30 } }); const audioSource = await pipeline.createSource({ sourceType: stream.SourceType.AUDIO, audioSourceConfig: { microphoneId: '0', sampleRate: 44100, channels: 2 } }); // 混合音视频流 const mixer = await pipeline.createMixer({ mixerType: stream.MixerType.AUDIO_VIDEO }); await pipeline.connect(videoSource, mixer); await pipeline.connect(audioSource, mixer); // 创建媒体流输出 const output = await pipeline.createOutput({ outputType: stream.OutputType.MEDIA_STREAM }); await pipeline.connect(mixer, output); // 启动管道,获取本地媒体流 localStream = await output.getMediaStream(); await pipeline.start(); updateStatus('媒体采集初始化成功,已启动摄像头+麦克风'); return localStream; } catch (error) { updateStatus(`媒体采集初始化失败:${error.message}`, true); return null; } } // 停止媒体采集 export async function stopMediaCapture() { if (pipeline) { await pipeline.stop(); await pipeline.destroy(); pipeline = null; } localStream = null; updateStatus('媒体采集已停止'); } // 状态更新函数(可替换为UI回调) function updateStatus(message: string, isError = false) { console.log(`[MediaCapture] ${message}`); }2.2 鸿蒙端 WebRTC 客户端(信令交换 + 流传输)
typescript
运行
// entry/src/main/ets/utils/WebRTCUtil.ets import webSocket from '@ohos.net.webSocket'; import { BusinessError } from '@ohos.base'; import media from '@ohos.multimedia.media'; // WebSocket实例 let ws: webSocket.WebSocket | null = null; // RTCPeerConnection实例(鸿蒙端WebRTC API,需依赖鸿蒙WebRTC插件) let pc: any = null; // 连接WebSocket信令服务 export async function connectSignalingServer(serverIp: string, port: number) { try { ws = webSocket.createWebSocket(); updateStatus(`连接信令服务:ws://${serverIp}:${port}`); // 监听连接成功 ws.on('open', () => { updateStatus('已连接到信令服务'); }); // 监听消息(处理Electron端信令) ws.on('message', async (message: string | ArrayBuffer) => { const data = JSON.parse(message.toString()); updateStatus(`收到信令:${data.type}`); await handleSignaling(data); }); // 监听连接关闭 ws.on('close', (code: number, reason: string) => { updateStatus(`信令服务连接关闭:${code} - ${reason}`, true); if (pc) { pc.close(); } }); // 监听错误 ws.on('error', (error: BusinessError) => { updateStatus(`信令服务错误:${error.message}`, true); }); // 连接到信令服务 await ws.connect(`ws://${serverIp}:${port}`); } catch (error) { updateStatus(`连接信令服务失败:${error.message}`, true); } } // 处理信令消息 async function handleSignaling(data: any) { try { switch (data.type) { case 'offer': // 处理Electron端的SDP offer await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); // 创建SDP answer const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); // 发送answer到Electron端 sendMessage({ type: 'answer', sdp: pc.localDescription }); break; case 'ice-candidate': // 添加ICE候选 await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); break; default: updateStatus(`未知信令类型:${data.type}`); break; } } catch (error) { updateStatus(`处理信令失败:${error.message}`, true); } } // 初始化WebRTC并发送流 export async function initWebRTC(localStream: media.MediaStream) { // 鸿蒙端需引入WebRTC相关库(此处为伪代码,实际需依赖鸿蒙WebRTC SDK) const configuration = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; // @ts-ignore 实际需使用鸿蒙提供的RTCPeerConnection pc = new RTCPeerConnection(configuration); updateStatus('初始化WebRTC成功'); // 添加本地流到PeerConnection localStream.getTracks().forEach((track) => { pc.addTrack(track, localStream); }); // 监听ICE候选 pc.onicecandidate = (event: any) => { if (event.candidate) { sendMessage({ type: 'ice-candidate', candidate: event.candidate }); updateStatus('发送ICE候选到Electron端'); } }; // 监听连接状态 pc.onconnectionstatechange = () => { updateStatus(`WebRTC连接状态:${pc.connectionState}`); }; } // 发送消息到信令服务 export function sendMessage(data: any) { if (ws && ws.readyState === webSocket.ReadyState.OPEN) { ws.send(JSON.stringify(data)); } else { updateStatus('WebSocket未连接,无法发送消息', true); } } // 状态更新函数 function updateStatus(message: string, isError = false) { console.log(`[WebRTC] ${message}`); } // 关闭WebRTC连接 export function closeWebRTC() { if (pc) { pc.close(); pc = null; } if (ws) { ws.close(); ws = null; } }2.3 鸿蒙端页面整合媒体采集与 WebRTC
typescript
运行
// entry/src/main/ets/pages/Index.ets import { initMediaCapture, stopMediaCapture } from '../utils/MediaCaptureUtil'; import { connectSignalingServer, initWebRTC, closeWebRTC } from '../utils/WebRTCUtil'; import common from '@ohos.app.ability.common'; @Entry @Component struct Index { @State message: string = '鸿蒙音视频流传输初始化中...'; private context = getContext(this) as common.UIAbilityContext; private localStream: any = null; aboutToAppear() { this.initAll(); } aboutToDisappear() { // 清理资源 stopMediaCapture(); closeWebRTC(); } async initAll() { try { // 1. 初始化媒体采集 this.localStream = await initMediaCapture(this.context); if (!this.localStream) { this.message = '媒体采集初始化失败'; return; } // 2. 连接Electron的信令服务(替换为实际Electron设备IP) await connectSignalingServer('192.168.1.101', 8080); // 3. 初始化WebRTC并发送流 await initWebRTC(this.localStream); this.message = '初始化完成,正在传输音视频流...'; } catch (error) { this.message = `初始化失败:${error.message}`; } } build() { Column() { Text(this.message) .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ top: 100 }) .textAlign(TextAlign.Center); } .width('100%') .height('100%') .backgroundColor(Color.White); } }五、运行与测试流程
1. 前置准备
- 确保鸿蒙真机与 Electron 设备处于同一局域网,记录 Electron 设备的 IP 地址。
- 鸿蒙真机开启开发者模式,授予应用摄像头、麦克风、网络权限。
2. Electron 侧运行
- 执行命令启动 Electron 应用:
bash
运行
npm run start - 点击 “开始接收流” 按钮,Electron 端将启动 WebRTC 并连接信令服务。
3. 鸿蒙侧运行
- 在 DevEco Studio 中,修改鸿蒙代码中的 Electron 设备 IP 地址。
- 将鸿蒙工程运行到真机,应用将自动初始化媒体采集并连接信令服务。
4. 测试结果验证
- Electron 端的
video标签将实时播放鸿蒙真机摄像头采集的视频流,同时接收麦克风音频流。 - 查看 Electron 端的状态日志,可看到 WebRTC 连接建立、ICE 候选交换、流接收的完整过程。
六、工程化优化与避坑指南
1. 优化建议
- 音频同步:Electron 端取消
muted属性,确保音视频同步播放(需处理回声消除)。 - 分辨率适配:根据网络状况动态调整鸿蒙端的视频采集分辨率,网络差时降低分辨率提升流畅度。
- 重连机制:信令服务或 WebRTC 连接断开后,添加自动重连逻辑,提升稳定性。
- 流录制:Electron 端添加
MediaRecorderAPI,实现音视频流的本地录制。
2. 常见坑点与解决方案
- WebRTC 连接失败:确保设备间网络互通,关闭防火墙;使用公共 STUN 服务器,若处于内网环境,需部署 TURN 服务器。
- 鸿蒙媒体采集失败:鸿蒙模拟器不支持摄像头 / 麦克风采集,必须使用真机;确保应用已获取媒体权限。
- 视频流卡顿:降低视频采集的分辨率和帧率;检查网络带宽,确保上传速度足够。
- 信令服务连接失败:确认 Electron 端的信令服务已启动,端口未被占用;鸿蒙端的 IP 地址配置正确。
七、扩展场景:Electron 端推流到鸿蒙设备
本文实现了鸿蒙端推流、Electron 端播放的单向流传输,若需实现 Electron 端推流、鸿蒙端播放,仅需调整以下步骤:
- Electron 端添加媒体采集逻辑(使用
navigator.mediaDevices.getUserMedia获取本地音视频流)。 - 鸿蒙端添加视频播放组件,接收并播放 WebRTC 流。
- 调整信令交换的发起方(由 Electron 端发起 offer 或鸿蒙端发起)。
八、总结
本文通过 WebRTC 协议结合鸿蒙媒体服务,实现了鸿蒙音视频流向 Electron 端的实时传输与播放。这种方案充分利用了 WebRTC 的低延迟 P2P 传输特性和鸿蒙的原生媒体采集能力,解决了跨端音视频流传输的核心问题。
开发者可基于本文的思路,拓展更多功能,如多人音视频连麦、流的实时滤镜处理、云端推流等,进一步完善跨端音视频协同体系。随着鸿蒙生态对 WebRTC 的支持不断完善,Electron 与鸿蒙的融合将为音视频类跨端应用带来更多创新可能。
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。