跳到主要内容

内容审核接入

WebRTC 把媒体加密做到了链路层,但链路加密不解决违规内容。只要业务对公众开放,监管和平台责任就要求你能在直播/连麦/视频会议里看到、阻断、留存违规内容。这篇讲清楚音频、视频、文本三条审核入口怎么接,以及它们各自的延迟、成本和处置粒度差异。

1. 为什么实时通信必须做内容审核

不是技术品味问题,是合规底线。

  • 监管要求:中国《网络音视频信息服务管理规定》(2020)、《互联网信息服务深度合成管理规定》(2023)明确要求音视频信息服务提供者具备"违法违规音视频信息识别处置技术能力"。海外有 DSA(欧盟数字服务法)、英国 Online Safety Act、美国 KOSA 等。
  • 平台责任:UGC 平台对违规内容的"过错责任"在司法实践里普遍成立。没有审核记录 = 没有免责凭证。
  • 社区健康:实时通信场景下用户互相骚扰、刷屏、引战的速度比图文社区快一个量级,纯靠用户举报兜不住。
  • 商业风险:广告主、应用商店、支付通道都会拿"是否有完整审核体系"作为合作条件。

什么时候适用:所有面向 C 端、半公开(如企业内多租户)以及含未成年人的实时通信业务。

什么时候不适用:私有化部署、闭环企业内部会议、医疗/律师等强保密场景,且法务确认豁免——但仍建议留存录制以便事后取证。

失败时的样子:监管通报、应用下架、被支付通道封停、出现极端事件时被追究刑责。

2. 三条审核入口的架构差异

实时通信里能产生违规内容的载体只有三类,每类的审核入口和工程模型都不一样。

维度音频流审核视频流审核文本聊天审核
数据形态PCM / Opus 帧I 帧 / 短片段UTF-8 字符串
采样窗口5~10 秒滑窗每 5~30 秒 1 帧每条消息
端到端延迟3~8 秒5~30 秒<500ms(同步)/ 1~3 秒(异步)
同步/异步旁路异步旁路异步可同步可异步
单次成本量级0.001~0.01 元 / 分钟0.001 元 / 张0.0001 元 / 条
命中粒度静音、踢出、上报关闭视频、踢出、上报拦截、撤回、禁言
漏检风险ASR 错字、方言、背景音帧采样间隙、模糊画面变体、谐音、跨语种
E2EE 冲突高,需端侧或放弃 E2EE高,需端侧或放弃 E2EE中,可端侧拦截

三条入口共用一个处置中心(policy engine + 房间控制 API),各自的差异主要在采集层推理层

3. 音频流审核

音频是实时通信里违规密度最高的载体,因为说话比打字门槛低。但它也是 ASR 链路最长、成本最高的入口。

3.1 链路

SFU 解包 SRTP → 单声道 PCM 16k → 流式 ASR → 滑窗文本 → 文本审核引擎 → 命中处置
  • 解包来源:SFU 拿到的就是明文 RTP(SRTP 已经在 SFU 上解过)。直接对接 Opus 解码出 PCM,不要再回头走 RTMP 旁路,那会引入 2 秒以上额外延迟。
  • 采样窗口:510 秒滑窗 + 12 秒重叠。窗口太小会让上下文不足,比如脏话被切成两半;太大则命中延迟拉长。
  • ASR 选型:流式 ASR(streaming ASR)必须支持中间结果(partial),不要用批式 ASR。

什么时候适用:所有有公开语音的房间(直播间、语聊房、多人会议)。

什么时候不适用:1v1 私聊且双方实名、企业内部小会、明确 E2EE 场景。

失败时的样子:方言/外语漏识别,背景音乐被识别成歌词命中误伤,长串脏话被切窗分割漏检。

3.2 SFU 旁路推流代码示例

下面是 mediasoup 风格的 SFU 旁路音频实现,把每个 producer 的 RTP 用 PlainTransport 发到本地审核 worker:

// audio-moderation-relay.js
import { types as MS } from 'mediasoup';

/**
* 为指定 audio producer 建立审核旁路。
* @param {MS.Router} router 当前房间的 mediasoup Router
* @param {MS.Producer} producer 音频 producer
* @param {{ host: string, rtpPort: number, rtcpPort: number }} sink 审核 worker 监听地址
*/
export async function attachAudioModeration(router, producer, sink) {
if (producer.kind !== 'audio') return null;

const transport = await router.createPlainTransport({
listenIp: '127.0.0.1',
rtcpMux: false,
comedia: false,
});

await transport.connect({ ip: sink.host, port: sink.rtpPort, rtcpPort: sink.rtcpPort });

const consumer = await transport.consume({
producerId: producer.id,
rtpCapabilities: router.rtpCapabilities,
paused: false,
});

producer.observer.once('close', () => transport.close());
return { transport, consumer };
}

审核 worker 端用 GStreamer / ffmpeg 把 RTP 还原为 PCM 后送给流式 ASR:

// asr-stream-worker.js
import { spawn } from 'node:child_process';
import { createAsrClient } from './asr-client.js'; // 厂商无关封装

const ffmpeg = spawn('ffmpeg', [
'-protocol_whitelist', 'file,udp,rtp',
'-i', 'sdp/in.sdp', // SDP 由 SFU 旁路时下发
'-vn', '-acodec', 'pcm_s16le',
'-ar', '16000', '-ac', '1',
'-f', 's16le', 'pipe:1',
]);

const asr = createAsrClient({ sampleRate: 16000, language: 'zh-CN' });

asr.on('partial', (text, ts) => {
// 中间结果只看趋势,不做处置
metrics.observe('asr_partial_len', text.length);
});

asr.on('final', async ({ text, startMs, endMs, speakerId }) => {
const verdict = await moderateText(text, { scene: 'live-audio' });
if (verdict.action !== 'pass') {
await policy.enforce({
roomId, userId: speakerId, channel: 'audio',
verdict, evidence: { text, startMs, endMs },
});
}
});

ffmpeg.stdout.on('data', (chunk) => asr.write(chunk));
ffmpeg.on('exit', () => asr.end());

要点:ASR 的 partial 只用来观测,final 才进审核。否则同一句话会被审核引擎判多次,命中策略会乱。

4. 视频流审核

视频审核处理的不是连续视频流,而是抽帧后的图片序列短片段。原因有两个:一是连续视频审核模型很贵,二是违规画面通常持续秒级以上,抽帧足够覆盖。

4.1 链路

SFU 解包视频 → 解码取 I 帧 → 抽帧(每 5~30s 一张)→ 图像审核 / 短视频审核 → 命中处置
  • 为什么抽 I 帧:I 帧本身就是完整画面,不需要参考前后帧,解码成本最低。P/B 帧抽出来还得跟前置 I 帧一起解码。
  • 采样间隔
    • 低风险房间(实名认证、付费用户)30 秒 / 帧足够。
    • 普通公开房间 10~15 秒 / 帧。
    • 高风险房间(陌生人交友、新注册用户、被举报过的用户)3~5 秒 / 帧,必要时切到短视频审核(每 5 秒一段 3 秒视频)。
  • 关键帧请求:如果当前视频流的 GOP 太长(10 秒以上),SFU 可以主动发 PLI 让发布端补 I 帧。但这会消耗带宽,谨慎做。

什么时候适用:所有开摄像头的房间,尤其是陌生人社交、秀场直播、未实名场景。

什么时候不适用:纯音频房间、屏幕共享纯文档展示(可以降到 60 秒 / 帧),但完全不审依然不建议。

失败时的样子:违规画面在两帧采样间隙快闪、画面被刻意压低分辨率/加马赛克绕过模型、夜间低光场景模型置信度坍塌。

4.2 关键帧抽取代码示例

// keyframe-sampler.js
import { spawn } from 'node:child_process';

/**
* 从 SFU 旁路出的 RTP 视频流里按时间间隔抽取关键帧 JPEG。
* @param {string} sdpPath SFU 下发的 SDP 文件
* @param {(jpeg: Buffer, ts: number) => void} onFrame
* @param {{ intervalSec: number }} opts
*/
export function startKeyframeSampler(sdpPath, onFrame, opts) {
const args = [
'-protocol_whitelist', 'file,udp,rtp',
'-i', sdpPath,
'-vf', `fps=1/${opts.intervalSec},select='eq(pict_type,I)'`,
'-vsync', 'vfr',
'-f', 'image2pipe',
'-vcodec', 'mjpeg',
'pipe:1',
];
const ff = spawn('ffmpeg', args);

let buf = Buffer.alloc(0);
ff.stdout.on('data', (chunk) => {
buf = Buffer.concat([buf, chunk]);
// JPEG SOI 0xFFD8 / EOI 0xFFD9 拆帧
let start;
while ((start = buf.indexOf(Buffer.from([0xff, 0xd8]))) >= 0) {
const end = buf.indexOf(Buffer.from([0xff, 0xd9]), start);
if (end < 0) break;
const jpeg = buf.slice(start, end + 2);
onFrame(jpeg, Date.now());
buf = buf.slice(end + 2);
}
});

return () => ff.kill('SIGTERM');
}

帧出来之后送到图像审核服务(自建或云服务):

const stopSampler = startKeyframeSampler(sdpPath, async (jpeg, ts) => {
const verdict = await imageModeration.detect(jpeg, {
scene: 'live-video',
biasTowards: ['porn', 'violence', 'politics', 'minor'],
});
if (verdict.action !== 'pass') {
await policy.enforce({
roomId, userId, channel: 'video',
verdict,
evidence: { jpeg, ts },
});
}
}, { intervalSec: 10 });

注意:evidence 里要保留原始 JPEG,不要只存模型置信度。监管核查、用户申诉都需要原图。

5. 文本聊天审核

文本审核是三条入口里最便宜、延迟容忍度最低的一条。问题不在算力,在变体识别

5.1 链路

文本可以走 DataChannel(WebRTC 自带),也可以走独立 IM 通道(WebSocket / MQTT)。审核位置选择:

  • 同步前置审核:客户端发 → 服务端 → 审核 → 命中阻断 / 通过 → 广播。延迟 200~500ms 是用户能接受的上限。
  • 异步审核 + 撤回:客户端发 → 服务端 → 广播 + 异步审核 → 命中后撤回。体验更顺滑,但违规内容会有秒级曝光。
  • 端侧前置 + 服务端复核:客户端做敏感词字典初筛,服务端用 AI 模型复核。适合高频弹幕场景。

什么时候适用:所有文本聊天,没有例外。

什么时候不适用:无。

失败时的样子:拼音/谐音/特殊字符变体绕过、跨语言混写绕过、字典更新跟不上热点、AI 模型对私聊上下文误判(如医疗咨询里的术语)。

5.2 DataChannel 同步审核拦截

// signaling-side text moderation
import { moderateText } from './moderation/text.js';

/**
* 房间内文本广播前置审核。
* 服务端代理 DataChannel 消息(不让客户端直连 DataChannel 互发),
* 这样才能保证审核不可绕过。
*/
async function handleChatMessage(socket, { roomId, text, msgId }) {
if (text.length > 500) {
return socket.emit('chat:reject', { msgId, reason: 'too_long' });
}

// 1) 字典初筛(毫秒级,本地内存)
const dictHit = sensitiveDict.scan(text);
if (dictHit.level === 'block') {
await evidence.log({ roomId, userId: socket.userId, text, dictHit });
return socket.emit('chat:reject', { msgId, reason: 'dict_block' });
}

// 2) AI 模型复核(百毫秒级,可并发)
const verdict = await moderateText(text, {
scene: 'live-chat',
userTags: socket.userTags,
roomTags: rooms.get(roomId).tags,
});

if (verdict.action === 'block') {
await evidence.log({ roomId, userId: socket.userId, text, verdict });
return socket.emit('chat:reject', { msgId, reason: verdict.reason });
}

if (verdict.action === 'review') {
// 异步人工复审,先放行带"待审"标记,命中后撤回
queueHumanReview({ roomId, userId: socket.userId, text, verdict });
}

io.to(roomId).emit('chat:new', { msgId, userId: socket.userId, text });
}

要点:客户端直连的 DataChannel 不能用于面向陌生人的聊天。直连 = 没有审核点。要么走服务端代理(这种 DataChannel 本质退化成 WebSocket),要么严格限制为已实名好友 1v1。

6. SFU 应该暴露的审核接口

一个生产可用的 SFU 必须对内提供这些能力,否则审核团队没法接:

接口用途形式
旁路推流把指定 producer 推到外部审核服务RTMP / SRT / 内部 RTP
内部 gRPC 拉流审核 worker 跨节点拉媒体gRPC streaming
关键帧回调让 SFU 每 N 秒主动推送 I 帧 JPEGWebhook
房间控制 API处置中心调用,做静音/踢人/关视频HTTP / gRPC
事件总线加入、离开、发流、停流事件Kafka / NATS
录制开关强制开启原始流录制HTTP

为什么不直接让审核服务连客户端:审核 worker 走 WebRTC 接客户端是反模式——客户端可以拒绝连接、可以发假流,审核完全失效。审核必须在 SFU 下游做

7. 审核延迟与采样率的工程权衡

延迟和成本是反向的:采样越密、模型越大,命中越早,但成本爆炸。下面是一个有 10 万并发用户的语聊房参考预算:

配置成本量级
音频 ASR10 万路 × 10 元/千分钟6 万元 / 小时
文本审核弹幕 50 万条/分钟 × 0.1 厘300 元 / 小时
视频抽帧5 万路 × 1 帧/10 秒 × 0.5 厘9 千元 / 小时
总计约 7 万元 / 小时

省钱思路:

  • 风险分级采样:实名 + 历史无违规的用户降到 30 秒 / 帧,新注册或被举报用户拉到 3 秒 / 帧。
  • 静音检测:音频 VAD 判定为静音段直接跳过 ASR。
  • 关键房间优先:流量大、监管关注度高的房间走全量审核,长尾小房间走抽样。
  • 本地模型前置:在 SFU 节点跑轻量模型做初筛,命中可疑再上云端大模型。

反例:对所有用户一律 3 秒 / 帧 + 大模型,账单顶不住,最后被迫关掉,等于裸奔。

8. 命中策略与处置粒度

处置不是单一动作,要分级。建议至少四级:

等级触发条件动作
L1 提示低置信度命中、首次轻微违规弹窗提醒、客户端 toast
L2 静音 / 关视频中置信度命中单方向静音 60s、关闭视频、消息撤回
L3 踢出房间高置信度命中或累犯强制下麦、踢出房间
L4 封号 + 上报涉政、涉黄、涉暴恐全局封号、向监管平台上报、留存证据

处置要走处置中心统一执行,不要在审核 worker 里直接调 SFU API:

  • 处置中心负责去重(同一用户 1 秒内只触发一次)、冷却(同一房间踢人间隔)。
  • 处置中心负责写入证据库和审计日志。
  • 处置中心负责跨系统联动(IM 全局封禁、风控系统打分、人工审核队列)。

9. 违规处置时序

实时阻断 vs 事后回溯不是二选一,要双轨

  • 实时阻断:命中即处置,避免违规内容继续传播。
  • 事后回溯:保留完整录制 + 审核标记,便于监管核查、用户申诉、模型反哺训练。

10. 证据链与存证

处置可以被申诉,监管可以来要数据。证据缺失等于处置无效。必存项:

  • 媒体证据:违规帧 JPEG(原图)、违规音频片段(命中前后各 5 秒 PCM 或 Opus)、违规文本原文。
  • 元信息:roomId、userId、设备信息、IP、UA、SFU 节点 ID、SRTP SSRC。
  • 时间戳:UTC 毫秒级 + NTP 同步标记。
  • 审核结果:模型版本、置信度、命中标签、处置动作、操作人(系统/人工)。
  • 链路完整性:建议对证据包做哈希链或写入只读对象存储(如 OSS WORM)。

存证保留期参考:监管要求至少 60 日,建议 180 日;涉重大违规永久。不要把证据存在业务数据库,要单独的存证库 + 审计日志,并开启 WORM。

11. E2EE 场景下的审核冲突

端到端加密和服务端审核逻辑上不相容。SFU 看不到媒体内容,就没法审。三种处理路径:

  1. 放弃 E2EE:面向公开房间的业务直接放弃,绝大多数 C 端场景属于这类。监管和审核优先级高于 E2EE。
  2. 限定场景 E2EE:1v1 私聊、企业内会议默认 E2EE,公开直播房间禁用。审核策略按房间类型分流。
  3. 端侧审核:客户端在加密前跑本地模型做一次审核,命中后阻止发送并上报指纹(不上报原文)。代价是模型受设备性能限制、可被逆向绕过。Apple CSAM 早期方案是这一路,争议很大,慎用。

什么时候适用 E2EE:医疗、法律咨询、企业并购会议、明确告知用户"对方说什么我们也看不到"的场景。

什么时候不适用:公开直播、陌生人社交、面向未成年人的产品——这些场景上 E2EE 等于把平台责任甩给用户,监管不认。

12. 与录制系统的集成

录制和审核共用同一份原始媒体流,工程上要协同:

  • 共享旁路出口:SFU 旁路一次,分发给录制写盘和审核管线,避免重复解码。
  • 审核标记落入录制元数据:每段命中违规的时间区间打 tag,回溯时直接跳到违规段,不用全程回放。
  • 录制存证一体化:录制文件本身就是证据来源。命中违规时,处置中心从录制文件中切片,而不是再单独存一份。
  • 隐私分级:被审核命中的录制片段进存证库(长保留),未命中的录制按业务策略短保留(如 7 天)。

13. 审核服务对接:自建 vs 云服务

主流路径只有两条,按业务规模选。

13.1 自建审核

  • 算力:GPU 集群跑 ASR、图像检测、文本检测模型。
  • 模型:开源底座(Whisper、CLIP、BERT 类)+ 自有违规样本微调。
  • 运维:模型迭代、A/B、误杀监控、人工审核队列。
  • 适用:日活千万级以上、违规样本量充足、监管对接频繁的平台。

13.2 云服务

阿里云、腾讯云、字节、网易易盾、Hive、Sightengine 等都提供音视频、图像、文本审核 API,接入模式高度同质化:

// vendor-neutral.js
export async function moderateImage(jpegBuffer, ctx) {
const resp = await fetch(`${VENDOR_BASE}/v1/image/moderation`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${signRequest(ctx)}`,
'Content-Type': 'application/octet-stream',
},
body: jpegBuffer,
});
const { suggestion, labels, traceId } = await resp.json();
return {
action: mapAction(suggestion), // pass / review / block
labels, // [{ name: 'porn', score: 0.92 }]
traceId, // 用于事后申诉时找回原始判定
};
}

function mapAction(suggestion) {
if (suggestion === 'block' || suggestion === 'reject') return 'block';
if (suggestion === 'review' || suggestion === 'suspect') return 'review';
return 'pass';
}

接入注意:

  • 厂商无关封装:审核业务方调统一接口,底层适配厂商,方便切换或多厂商互备。
  • 多厂商兜底:高风险场景双厂商并联,任一命中即拦截,降低单一模型偏差。
  • traceId 必存:所有审核请求的 traceId 落库,监管或用户申诉时能反查到底层判定。
  • 本地降级:云服务超时 / 限流时,本地敏感词字典 + 规则兜底,绝不"审核挂了就放过"。

14. 反模式

反模式后果
只在客户端做审核用户改包、改 JS 就绕过;客户端模型还能被逆向
DataChannel 客户端直连聊天审核没有挂载点,文本完全不可控
命中即封号不留申诉通道误杀引发投诉潮,监管反而要求整改
证据只存模型置信度不存原文申诉/复核拿不出原始物证,等同没审
全量用户拉满采样率成本爆炸,最后被迫关掉,导致裸奔
不区分房间风险等级高风险房间漏审、低风险房间浪费
处置直接由审核 worker 调 SFU没去重没冷却,瞬间踢爆房间
ASR 中间结果直接进审核同一句话被判多次,误杀飙升
录制和审核走两条独立旁路SFU CPU 翻倍、时间戳对不齐、证据切片困难
E2EE 房间不区分场景一刀切要么违规放行,要么审核误杀 E2EE 的私聊
云服务超时直接放行攻击者只需打满审核服务即可投放违规内容
审核结果不可申诉商业上劝退正常用户,监管层面也要求双向通道

15. 自查清单

  • 音频、视频、文本三条入口都有审核管线,且命中事件能在统一处置中心看到
  • SFU 提供旁路推流、关键帧回调、房间控制 API
  • 风险分级采样落地,不是所有用户都拉满
  • 文本聊天不允许客户端直连 DataChannel 互发
  • 处置分四级(提示 / 静音 / 踢出 / 封号上报),处置中心做去重和冷却
  • 证据库与业务数据库隔离,开启 WORM,保留期符合监管
  • 录制系统与审核共用旁路,命中区间打 tag
  • 云审核服务有本地兜底,不会因为审核挂掉而放行
  • 用户申诉通道存在,能反查 traceId 和原始证据
  • E2EE 与公开房间策略分流,不一刀切

16. 权威资料