Webhook是一种基于HTTP/HTTPS协议的回调机制,允许服务端主动推送数据。RTC回调通知服务器使用Webhook将相关事件回调给开发者服务器,以便开发者按需处理自己的业务逻辑。
使用方法
前提条件
您已部署用于接收回调消息的HTTP/HTTPS服务。
您已经注册了阿里云账号并完成账号实名认证。注册地址请参见阿里云官网。注册指引请参见注册阿里云账号。实名认证指引请参见个人实名认证或企业实名认证和个体工商户认证。
您已经开通了RTC服务。开通步骤请参见开通服务。
使用流程
在控制台开通某个AppID的事件回调功能。
登录RTC控制台,在左侧导航栏选择配置管理 > 事件通知 > 选中对应的AppID,进行回调设置页面。按需配置具体的事件。
触发回调事件。
完成AppID应用事件通知配置之后,您可以通过服务端API,发起相关任务,比如开启录制、开启推流等操作来触发相应的回调事件产生。
接收回调事件。
当回调事件产生之后,比如录制文件生成,若回调成功,您可以在您部署的回调接受服务器中查看具体的回调事件通知。
回调机制
您需要自行部署一个HTTP服务来接收回调消息,并在控制台中具体业务中配置回调URL。
当事件产生时,RTC回调通知服务器会向该URL发起HTTP POST请求,事件通知内容将通过HTTP Body送达。
您的HTTP服务对HTTP POST请求进行响应且HTTP状态码为200,即视为回调成功;若响应其他状态码或响应超时,则视为回调失败。
回调成功后,您配置的回调URL中将接收到相应的事件通知内容。
回调格式
回调消息以HTTP POST请求发送到您的服务器,请求Body格式为JSON。字符编码为UTF-8。
回调消息的请求Header 中包含以下字段:
字段 | 示例值 | 描述 |
Content-Type | application/json | 固定值 |
trace-id | 2401058********622012463d9 | 该字段用于排查问题使用 |
DingRTC-Signature | z5jbvxxx.1718877424.xx3e7691142ffe4342e13e25dc317695b17827e34ec248a5cc35d3a7e1e1cd44 | RTC回调服务加密算法生成的加密值。详见验证签名。 |
回调消息请求的Body中包含以下字段:
名称 | 类型 | 是否必须 | 示例值 | 描述 |
eventId | string | 是 | 12343aed********* | 事件ID |
eventType | string | 是 | 101 | 事件类型,RTC回调服务器有个eventType,具体类型见下文回调消息列表 |
notifyTime | long | 是 | 1701056041128 | 通知时间戳,单位:毫秒 |
eventData | JSONObject | 是 |
| 回调消息具体内容,每种类型的事件不一样,具体见下文回调消息列表 |
您的服务器收到的通知顺序和事件发生的顺序不一定完全一致。
为确保回调消息通知的可靠性,每次事件可能会有不止一次消息通知,您的服务器可能需要做消息幂等处理。
验证签名
RTC回调服务器在通知客户服务器时,为了校验本次请求的合法性,约定签名算法如下。
消息头中的 DingRTC-Signature 由三部分组成,用.
拼接在一起。格式为AppId.TimeStamp.Signature
,字段含义如下:
AppID: 应用ID。
TimeStamp:UTC时间戳(精确到秒)。
Signature:签名,由HTTP请求内容的原始字符串、时间戳、回调密钥计算得出,详细算法如下:
Signature=hexString(HmacSHA256(请求内容的原始字符串+TimeStamp,callbackSecret))
回调通知密钥请通过控制台获取。
验证签名时,您可以参考以下代码:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public class SignUtil {
public static final String HMAC_SHA_256 = "HmacSHA256";
public static String hmacSha256(String message, String secret) {
try {
SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA_256);
Mac mac = Mac.getInstance(HMAC_SHA_256);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return bytesToHex(rawHmac);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() < 2) {
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
public static void main(String[] args) {
String requestBody = "{\"eventData\":{\"channelId\":\"55\",\"timestamp\":1718877424674},\"eventId\":\"2133cc0c17188774246986428d0cb0\",\"eventType\":\"101\",\"notifyTime\":1718877424701}";
String secret = "your callback secret";
String signatureHeader = "z5jbvxxx.1718877424.150f2b8e107a0f4399671dcf2b1e3e2ac78252a26c9626abf4a29a77464a96c1";
String appId = signatureHeader.split("\\.")[0];
String timestamp = signatureHeader.split("\\.")[1];
String signature = signatureHeader.split("\\.")[2];
if (signature.equals(hmacSha256(requestBody + timestamp, secret))) {
System.out.println("DingRTC-Signature is valid");
} else {
System.out.println("DingRTC-Signature is invalid");
}
}
}
# !-*- coding: utf-8 -*-
import hashlib
import hmac
request_body='{"eventData":{"channelId":"55","timestamp":1718877424674},"eventId":"2133cc0c17188774246986428d0cb0","eventType":"101","notifyTime":1718877424701}'
secret = 'your callback secret'
signature_header = 'z5jbvxxx.1718877424.150f2b8e107a0f4399671dcf2b1e3e2ac78252a26c9626abf4a29a77464a96c1'
appId = signature_header.split('.')[0]
timestamp = signature_header.split('.')[1]
signature = signature_header.split('.')[2]
sign_body = request_body + timestamp
if (signature == hmac.new(secret.encode('utf-8'), sign_body.encode('utf-8'), hashlib.sha256).hexdigest()):
print("DingRTC-Signature is valid")
else:
print("DingRTC-Signature is invalid")
回调消息列表
本文档的JSON示例省略了Body中eventId
和notifyTime
。
回调内容可能会增加字段,或者调整字段顺序,请根据您使用的开发语言采用适当的解析方式。
验证事件
001 回调验证
该场景仅在控制台设置具体回调URL或者手动校验时触发。
{
"eventType": "001",
"eventData":{
"appId": "12adxxxx2"
}
}
频道事件
101 频道开始
{
"eventType": "101",
"eventData":{
"channelId": "room**" // 频道id
"timestamp": 1709696165584 // 发生时间(ms)
}
}
102 频道结束
{
"eventType": "102",
"eventData":{
"channelId": "room**" // 频道id
"timestamp": 1709696165584 // 发生时间(ms)
}
}
103 用户加入
{
"eventType": "103",
"eventData":{
"channelId": "room**" // 频道id
"user":{
"userId":"123444"
}
"timestamp": 1709696165584 // 发生时间(ms)
}
}
104 用户离开
{
"eventType": "104",
"eventData":{
"channelId": "room**" // 频道id
"reasonCode": 20003001, // 用户离开原因,详见状态码表格
"user":{
"userId":"123444"
}
"timestamp": 1709696165584 // 发生时间(ms)
}
}
推流事件
1000 开始推流
{
"eventType": "1000",
"eventData": {
"channelId": "room**", // 频道id
"liveState":{
"code": 20000000, // 状态码,详见状态码表格
},
"taskId": "task-03061", // 任务id
"timestamp": 1709737037688 // 发生时间(ms)
}
}
1001 推流正常结束
{
"eventType": "1001",
"eventData": {
"channelId": "room**", // 频道id
"liveState":{
"code": 20000000, // 状态码,详见状态码表格
},
"taskId": "task-03061", // 任务id
"timestamp": 1709737037688 // 发生时间(ms)
}
}
1002 推流异常
{
"eventType": "1002",
"eventData": {
"channelId": "room**", // 频道id
"liveState":{
"code": 50001001, // 状态码,详见状态码表格
}
"taskId": "task-03061", // 任务id
"timestamp": 1709737037688 // 发生时间(ms)
}
}
录制事件
2000 开始录制
{
"eventType": "2000",
"eventData": {
"channelId": "room**",
"recordState": {
"bucket":"rtc*******", // 录制文件存放bucket
"vendor":1, // 对象存储供应商,见开启录制接口
"region":1, // 对象存储region,见开启录制接口
"startTs":1709737037688, // 录制开始时间戳,单位:毫秒(ms)
"code": 20000000
},
"taskId": "task-0422",
"timestamp": 1709737037688
}
}
2001 录制成功
{
"eventType": "2001",
"eventData": {
"channelId": "room**",
"recordState": {
"bucket":"rtc*******", // 录制文件存放bucket
"vendor":1, // 对象存储供应商,见开启录制接口
"region":1, // 对象存储region,见开启录制接口
"startTs":1709737037688, // 录制开始时间戳,单位:毫秒(ms)
"code": 20000000, // 状态码,详见状态码表格
"fileFailCount": 0,
"fileInfo": [
{
"fileDuration": 7859, // 录制文件时长,单位:毫秒(ms)
"fileSize": 216777, // 录制文件大小,单位:字节(Byte)
"filePath": "record/v980**/65e82ef000210**/1709737028486_1709737030532/1709737028486-1709737030532.mp4",// 录制文件存储路径
"status": 0, // 0表示成功, 其他表示失败
"timestamp": 1709737037679 // 录制文件生成的时间戳(ms)
}
],
"fileCount": 1 // 录制文件总数
},
"taskId": "task-03061",
"timestamp": 1709737037688
}
}
2002 录制失败
{
"eventType": "2002",
"eventData": {
"channelId": "room**",
"recordState": {
"bucket":"rtc*******", // 录制文件存放bucket
"vendor":1, // 对象存储供应商,见开启录制接口
"region":1, // 对象存储region,见开启录制接口
"startTs":1709737037688, // 录制开始时间戳,单位:毫秒(ms)
"reason": "WritePlaylist failed",
"code": 50002001, //状态码,见最后状态码表格
"fileFailCount": 2,
"fileInfo": [
{
"reason": "write flv file fail", // 失败原因
"status": 50002001,
"timestamp": 1709721091674
},
{
"reason": "WritePlaylist failed",
"fileDuration": 30437,
"fileSize": 123875456,
"filePath": "taskidtaskId-199-cid65e844**e000000001ac0000/playlist.m3u8",
"status": 50002001,
"timestamp": 1709721103666
}
],
"fileCount": 2
},
"taskId": "taskId-199",
"timestamp": 1709721103673
}
}
2010 录制服务状态变化
该事件默认不会主动回调,请通过控制台或者OpenAPI 完成订阅。
{
"eventType": "2010",
"eventData": {
"channelId": "room**",
"recordState": {
"bucket":"rtc*******", // 录制文件存放bucket
"vendor":1, // 对象存储供应商,见开启录制接口
"region":1, // 对象存储region,见开启录制接口
"startTs":1709737037688, // 录制开始时间戳,单位:毫秒(ms)
"code": 20002002 // 状态码,详见状态码表格
},
"taskId": "taskId-199",
"timestamp": 1709721103673
}
}
2011 录制音频流变化
该事件默认不会主动回调,请通过控制台或者OpenAPI 完成订阅。
{
"eventType": "2011",
"eventData": {
"channelId": "room**",
"recordState": {
"streamChangeInfo": { // 流变化信息
"streamType": 3, // 流类型 1: 摄像头流 2:共享流 3: 音频合流 4: 视频合流
"state": 1, // 录制收流状态 1: 正在接收 2: 未在接收
"direction": 2, // 流方向 1: 输入 2: 输出
"timestamp": 1721112755076 // 状态变化Unix 毫秒时间戳
}
},
"taskId": "taskId-199",
"timestamp": 1709721103673
}
}
2012 录制视频流变化
该事件默认不会主动回调,请通过控制台或者OpenAPI 完成订阅。
{
"eventType": "2012",
"eventData": {
"channelId": "room**",
"recordState": {
"streamChangeInfo": { // 流变化信息
"uid": "user1", // 流方向为输出时,流属于合流,uid为空。否则为具体uid
"streamType": 1, // 流类型 1: 摄像头流 2:共享流 3: 音频合流 4: 视频合流
"state": 1, // 录制收流状态 1: 正在接收 2: 未在接收
"direction": 1, // 流方向 1: 输入 2: 输出
"timestamp": 1721112755076 // 状态变化Unix 毫秒时间戳
}
},
"taskId": "taskId-199",
"timestamp": 1709721103673
}
}
纪要事件
3000 纪要开始
{
"eventType": "3000",
"eventData": {
"channelId": "room**",
"asrState": {
"code": 20000000 // 状态码,详见状态码表格
},
"taskId": "taskId-199",
"timestamp": 1709721103673
}
}
3001 纪要成功
{
"eventType": "3001",
"eventData": {
"asrState": {
"transcriptionFilePath": "cloudNote/6pz38941/1234_1234/transcription_1734069823271.json", // 转写结果
"serviceInspectionFilePath": "cloudNote/6pz38941/1234_1234/serviceInspection_1734069824007.json", // 服务质检结果
"customPromptFilePath": "cloudNote/6pz38941/1234_1234/customPrompt_1734069824057.json", // 自定义Prompt结果
"meetingAssistanceFilePath": "cloudNote/6pz38941/1234_1234/meetingAssistance_1734069823787.json", // 要点提炼结果
"summarizationFilePath": "cloudNote/6pz38941/1234_1234/summarization_1734069823845.json", // 摘要总结结果
"textPolishFilePath": "cloudNote/6pz38941/1234_1234/textPolish_1734069823903.json", // 口语书面化结果
"autoChaptersFilePath": "cloudNote/6pz38941/1234_1234/autoChapters_1734069823728.json", // 自动章节结果
"vendor": 1, // 对象存储提供方
"region": 1, // 对象存储region
"bucket": "rtc-qa-test" // bucket 名称
},
"channelId": "room**",
"taskId": "taskId-199",
"timestamp": 1709721103673
}
}
结果文件路径格式规则:cloudNote/{appId}/{channelId}_{taskId}/{biz}_{putTs}.json
3002 纪要失败
{
"eventType": "3000",
"eventData": {
"channelId": "room**",
"asrState": {
"code": 50004001 // 状态码,详见状态码表格
},
"taskId": "taskId-199",
"timestamp": 1709721103673
}
}
状态码表格
类型 | 状态码 | 说明 |
公共 | 20000000 | 成功 |
50000000 | 服务器内部错误 | |
推流 | 50001001 | 推流异常 |
录制 | 50002001 | 写入用户存储失败, 可能是网络问题 |
50002002 | 启动用户存储失败, 可能是入参AK/SK/Bucket/Region/Vendor输入错误 | |
50002003 | 录制时间过短,没有生成录制文件 | |
50002004 | 用户存储密钥错误 | |
50002005 | bucket不存在 | |
50002006 | 访问用户存储被拒绝 | |
20002001 | 没有开始云端录制 | |
20002002 | 云端录制初始化完成 | |
20002003 | 录制组件开始启动 | |
20002004 | 录制组件启动完成 | |
20002005 | 停止录制 | |
20002006 | 上传组件已启动 | |
20002007 | 已成功上传第一个文件 | |
用户 | 20003001 | 客户端主动退会 |
20003002 | 客户端保活异常 | |
20003003 | 用户被踢出 | |
20003004 | 相同uid移除 | |
20003005 | 未知原因退会 | |
纪要 | 50004001 | 纪要服务器异常 |
50004002 | 纪要任务超过最大时间 | |
30006001 | 用户ak/sk/bucket配置异常 |