手册手册
  • 实时数字人交互协议详解

实时数字人交互协议详解

1. 简介

Nuwa 平台为数字人实时交互提供了一套开放接口协议,旨在允许开发者集成数字人能力。 该协议的核心分为两个协同工作的部分:

1.1 信令交互

客户端通过 WebSocket 与 Nuwa 平台建立信令通道。此通道负责管理会话与业务逻辑,主要功能包括:

  • websocket 连接验证:验证数字人身份
  • 媒体协商:协商音视频传输协议
  • 语音/文本消息上报:发送用户输入
  • 业务消息通知:接收数字人状态、响应文本等

1.2 音视频实时流

客户端通过 WebRTC 或 RTMP 协议,从协商好的地址拉取数字人的实时音视频流。

示例

演示Demo

注意

如果只想用ASR能力参考: Nuwa ASR 实时语音识别协议

2. 开发前须知

  • 获取 API Key:

在开始之前,您需要在 NuwaAI平台完成以下步骤:

  1. 注册一个 NuwaAI账号,并登录
  2. 登录后,记录下用户ID
  3. 创建一个数字人,并记录下数字人 ID
  4. 创建一个API Key,并记录下 API Key

获取 API Key, 数字人ID, 用户ID

  • 获取 Access Token:

    • 实时数字人交互协议需要通过鉴权接口协议获取Access Token验证用户权限,确保交互安全
  • 实时数字人交互协议交互流程:

    • 使用WebRTC协议的交互流程
sequenceDiagram
    participant 客户端 as 客户端
    participant 平台 as Nuwa 平台
    participant 服务 as Nuwa 数字人服务

    客户端->>平台: 1. 创建 WebSocket 连接
    平台-->>客户端: 2. 响应 avator-connect (鉴权结果)

    客户端->>平台: 3. playStream (携带本地 SDP)
    平台-->>客户端: 4. 响应 avator-play (返回远端 SDP)
    客户端->>客户端: 5. 解析 SDP,初始化接收端 WebRTC 收流处理
    服务-->>客户端: 6. 推送音视频流

    服务-->>客户端: 7. avator-playstop (开场白结束)

    客户端-->>平台: 8. 16k 单通道语音数据
    平台-->>服务: 转发语音数据
    客户端-->>平台: 9. 发送对话消息(可选)

    平台-->>客户端: 10. chat_begin (对话开始)
    平台-->>客户端: 11. chat (对话进行中)
    平台-->>客户端: 12. chat_end (对话结束)

    服务-->>客户端: 13. avator-text (数字人回复文本)
    服务-->>客户端: 14. avator-text (后续文本)
    服务-->>客户端: 15. avator-playstop (回复结束)
  • 使用RTMP协议的交互流程
sequenceDiagram
    participant C as 客户端 📱
    participant P as Nuwa 平台 ☁️
    participant S as Nuwa 数字人服务 🤖
    participant R as RTMP接收端 📡

    %% 1. 连接与鉴权
    C->>P: 1. 创建 websocket 连接
    P-->>C: 2. 响应 avator-connet (鉴权结果)

    %% 2. 启动 RTMP 推流
    C->>P: 3. playRtmp (携带 rmtp_url)
    P->>S: 4. playRtmp (转发 rmtp_url)
    
    %% 3. 平台通知客户端可以播放
    S-->>P: 5. 推流准备就绪 (内部状态)
    P-->>C: 6. 响应 avator-play (通知播放)

    %% 4. 数字人服务推流至接收端
    S->>R: 7. Rtmp 推流 (视频流)

    %% 5. 开场白结束通知
    S-->>P: 8. avator-playstop (开场白结束)
    P-->>C: 9. 转发 avator-playstop

    %% 6. 语音输入阶段
    C->>P: 10. 16k 单通道语音数据
    P->>S: 11. 转发语音数据

    %% 7. 对话消息通知(可选)
    P-->>C: 12. chat_begin (对话开始)
    P-->>C: 13. chat (对话进行中)
    P-->>C: 14. chat_end (对话结束)

    %% 8. 回复
    S-->>P: 15. avator-text (数字人回复)
    P-->>C: 16. 转发 avator-text
    S-->>P: 17. avator-text (后续文本)
    P-->>C: 18. 转发 avator-text
    S-->>P: 19. avator-playstop (回复结束)
    P-->>C: 20. 转发 avator-playstop

3. websocket信令

3.1 websocket 连接验证

创建 WebSocket 连接

wss://wsapi.nuwaai.com/websk/humanAgent/humanchat/{protocol}/{avatorid}/{bizid}?access_token={token}

参数说明

参数名称类型是否必填参数说明
protocolString否流传输协议:webrtc(默认)rtmp
avatoridString是数字人角色
bizidString是业务 id (humanctrl场景中启用asr则为asr,其他场景为0)
tokenString是鉴权后获得的token

注意

如果是仅控制数字人(播报、动作、表情等)的业务,使用请求:

wss://wsapi.nuwaai.com/websk/humanAgent/humanctrl/{protocol}/{avatorid}/{bizid}?access_token={token}

3.1.2 等待验证回应

协议: JSON 文本消息

{
  "code": {code},
  "msg": {message},
  "type": "avator-connect",
  "data": {
    "question": {question},
    "sessionid": {sessionid}
  }
}

参数说明

参数名称类型是否必填参数说明
codeint是响应码:200 为 ok,其他为错误,具体错误 message
messageString否当响应码为 200 时返回。具体错误原因
typeString是消息类型:avator-connect
dataObject是消息体
data.questionArray是3 个提示问题: ["提示1", "提示2", "提示3"]
data.sessionidString是会话 id
示例
// WebSocket 连接示例
const protocol = 'webrtc'; // 或 'rtmp'
const avatorId = 'your-avator-id';
const bizId = '0'; // 或 'asr'(humanctrl场景中启用asr)
const token = 'your-access-token';

const wsUrl = `wss://wsapi.nuwaai.com/websk/humanAgent/humanchat/${protocol}/${avatorId}/${bizId}?access_token=${token}`;
const ws = new WebSocket(wsUrl);

// 连接建立
ws.onopen = function() {
  console.log('WebSocket 连接已建立');
};

// 接收消息
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('收到消息:', message);
  
  // 处理连接验证响应
  if (message.type === 'avator-connect') {
    if (message.code === 200) {
      console.log('连接验证成功,会话ID:', message.data.sessionid);
      console.log('提示问题:', message.data.question);
      // 连接验证成功后,可以进行下一步操作
    } else {
      console.error('连接验证失败:', message.msg);
    }
  }
};

// 连接关闭
ws.onclose = function() {
  console.log('WebSocket 连接已关闭');
};

// 连接错误
ws.onerror = function(error) {
  console.error('WebSocket 连接错误:', error);
};
import websocket
import json

# WebSocket 连接示例
protocol = 'webrtc'  # 或 'rtmp'
avator_id = 'your-avator-id'
biz_id = '0'  # 或 'asr'(humanctrl场景中启用asr)
token = 'your-access-token'

ws_url = f'wss://wsapi.nuwaai.com/websk/humanAgent/humanchat/{protocol}/{avator_id}/{biz_id}?access_token={token}'

# 定义回调函数
def on_open(ws):
    print('WebSocket 连接已建立')

def on_message(ws, message):
    msg = json.loads(message)
    print('收到消息:', msg)
    
    # 处理连接验证响应
    if msg.get('type') == 'avator-connect':
        if msg.get('code') == 200:
            print('连接验证成功,会话ID:', msg.get('data', {}).get('sessionid'))
            print('提示问题:', msg.get('data', {}).get('question'))
            # 连接验证成功后,可以进行下一步操作
        else:
            print('连接验证失败:', msg.get('msg'))

def on_close(ws, close_status_code, close_msg):
    print('WebSocket 连接已关闭')

def on_error(ws, error):
    print('WebSocket 连接错误:', error)

# 创建 WebSocket 连接
ws = websocket.WebSocketApp(ws_url, 
                          on_open=on_open, 
                          on_message=on_message, 
                          on_close=on_close, 
                          on_error=on_error)

# 运行 WebSocket 客户端
ws.run_forever()

3.2 媒体协商

通过 WebSocket 发送媒体协商请求。协商协议支持 WebRTC 和 RTMP,WebSocket 的连接协议参数和协商协议需一致。

3.2.1 WebRTC 协议

发送媒体协商请求

协议: JSON 文本消息

{
  "type": "playStream",
  "data": {
    "sdp": {sdp}
  }
}

参数说明

参数名称类型是否必填参数说明
typeString是消息类型:playStream
dataObject是消息体
data.sdpString是本地 sdp
等待协商回应

协议: JSON 文本消息

{
  "code": {code},
  "msg": {message},
  "type": "avator-play",
  "data": {
    "sdp": {sdp}
  }
}

参数说明

参数名称类型是否必填参数说明
codeint是响应码:200 为 ok,其他为错误,具体错误 message
messageString否当响应码为 200 时返回。具体错误原因
typeString是消息类型:avator-play
dataObject是消息体
data.sdpsdp是远端 sdp 信息
示例
// 发送 WebRTC 媒体协商请求
function sendPlayStream(ws, localSdp) {
  const playStreamMessage = {
    type: 'playStream',
    data: {
      sdp: localSdp
    }
  };
  
  ws.send(JSON.stringify(playStreamMessage));
  console.log('发送 playStream 请求:', playStreamMessage);
}

// 处理 WebRTC 协商回应
function handleAvatorPlay(message) {
  if (message.code === 200) {
    const remoteSdp = message.data.sdp;
    console.log('收到 avator-play 响应,远端 SDP:', remoteSdp);
    
    // 使用远端 SDP 建立 WebRTC 连接
    // 这里需要根据你的 WebRTC 实现进行处理
    // 例如:
    // pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: remoteSdp }));
  } else {
    console.error('媒体协商失败:', message.msg);
  }
}

// 示例:在 WebSocket 消息处理中添加
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('收到消息:', message);
  
  if (message.type === 'avator-connect') {
    // 处理连接验证响应...
  } else if (message.type === 'avator-play') {
    // 处理媒体协商响应
    handleAvatorPlay(message);
  }
};
import json

# 发送 WebRTC 媒体协商请求
def send_play_stream(ws, local_sdp):
    play_stream_message = {
        'type': 'playStream',
        'data': {
            'sdp': local_sdp
        }
    }
    
    ws.send(json.dumps(play_stream_message))
    print('发送 playStream 请求:', play_stream_message)

# 处理 WebRTC 协商回应
def handle_avator_play(message):
    if message.get('code') == 200:
        remote_sdp = message.get('data', {}).get('sdp')
        print('收到 avator-play 响应,远端 SDP:', remote_sdp)
        
        # 使用远端 SDP 建立 WebRTC 连接
        # 这里需要根据你的 WebRTC 实现进行处理
    else:
        print('媒体协商失败:', message.get('msg'))

# 示例:在 WebSocket 消息处理中添加
def on_message(ws, message):
    msg = json.loads(message)
    print('收到消息:', msg)
    
    if msg.get('type') == 'avator-connect':
        # 处理连接验证响应...
        pass
    elif msg.get('type') == 'avator-play':
        # 处理媒体协商响应
        handle_avator_play(msg)

3.2.2 RTMP 协议

发送媒体协商请求

协议: JSON 文本消息

{
  "type": "playRtmp",
  "data": {
    "url": {url}
  }
}

参数说明

参数名称类型是否必填参数说明
typeString是消息类型:playRtmp
dataObject是消息体
data.urlString是数字人推送地址
等待协商回应

协议: JSON 文本消息

{
  "code": {code},
  "msg": {message},
  "type": "avator-play"
}

参数说明

参数名称类型是否必填参数说明
codeint是响应码:200 为 ok,其他为错误,具体错误 message
messageString否当响应码为 200 时返回。具体错误原因
typeString是消息类型:avator-play
示例
// 发送 RTMP 媒体协商请求
function sendPlayRtmp(ws, rtmpUrl) {
  const playRtmpMessage = {
    type: 'playRtmp',
    data: {
      url: rtmpUrl
    }
  };
  
  ws.send(JSON.stringify(playRtmpMessage));
  console.log('发送 playRtmp 请求:', playRtmpMessage);
}

// 处理 RTMP 协商回应
function handleAvatorPlayRtmp(message) {
  if (message.code === 200) {
    console.log('RTMP 媒体协商成功');
    
    // 开始通过 RTMP 协议接收音视频流
    // 这里需要根据你的 RTMP 播放器实现进行处理
    // 例如:使用 video.js 或其他播放器加载 RTMP 流
  } else {
    console.error('RTMP 媒体协商失败:', message.msg);
  }
}

// 示例:在 WebSocket 消息处理中添加
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('收到消息:', message);
  
  if (message.type === 'avator-connect') {
    // 处理连接验证响应...
  } else if (message.type === 'avator-play') {
    // 处理媒体协商响应
    handleAvatorPlayRtmp(message);
  }
};
import json

# 发送 RTMP 媒体协商请求
def send_play_rtmp(ws, rtmp_url):
    play_rtmp_message = {
        'type': 'playRtmp',
        'data': {
            'url': rtmp_url
        }
    }
    
    ws.send(json.dumps(play_rtmp_message))
    print('发送 playRtmp 请求:', play_rtmp_message)

# 处理 RTMP 协商回应
def handle_avator_play_rtmp(message):
    if message.get('code') == 200:
        print('RTMP 媒体协商成功')
        
        # 开始通过 RTMP 协议接收音视频流
        # 这里需要根据你的 RTMP 播放器实现进行处理
    else:
        print('RTMP 媒体协商失败:', message.get('msg'))

# 示例:在 WebSocket 消息处理中添加
def on_message(ws, message):
    msg = json.loads(message)
    print('收到消息:', msg)
    
    if msg.get('type') == 'avator-connect':
        # 处理连接验证响应...
        pass
    elif msg.get('type') == 'avator-play':
        # 处理媒体协商响应
        handle_avator_play_rtmp(msg)

3.3 发送对话消息

语音对话消息,直接发送 16k 单通道 PCM 数据,按 60ms 一个 chunk 发送。文本消息按照 JSON 文本消息格式发送。

3.3.1 文本对话协议

协议: JSON 文本消息

{
  "type": "agent",
  "data": {
    "content": {text}
  }
}

参数说明

参数名称类型是否必填参数说明
typeString是消息类型:agent
dataObject是消息体
data.contentString是对话内容
示例
// 发送文本对话消息
function sendTextMessage(ws, text) {
  const textMessage = {
    type: 'agent',
    data: {
      content: text
    }
  };
  
  ws.send(JSON.stringify(textMessage));
  console.log('发送文本消息:', textMessage);
}

// 示例:发送文本消息
// sendTextMessage(ws, '你好,数字人!');

// 处理对话相关的响应
function handleChatMessages(message) {
  switch (message.type) {
    case 'chat_begin':
      console.log('对话开始');
      break;
    case 'chat':
      console.log('大模型回应:', message.data.content);
      break;
    case 'chat_end':
      console.log('对话结束');
      break;
    case 'avator-text':
      console.log('数字人回复:', message.data.text);
      break;
    case 'avator-playstop':
      console.log('数字人回复结束');
      break;
  }
}

// 示例:在 WebSocket 消息处理中添加
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('收到消息:', message);
  
  if (message.type === 'avator-connect') {
    // 处理连接验证响应...
  } else if (message.type === 'avator-play') {
    // 处理媒体协商响应...
  } else if (['chat_begin', 'chat', 'chat_end', 'avator-text', 'avator-playstop'].includes(message.type)) {
    // 处理对话相关的响应
    handleChatMessages(message);
  }
};
import json

# 发送文本对话消息
def send_text_message(ws, text):
    text_message = {
        'type': 'agent',
        'data': {
            'content': text
        }
    }
    
    ws.send(json.dumps(text_message))
    print('发送文本消息:', text_message)

# 示例:发送文本消息
# send_text_message(ws, '你好,数字人!')

# 处理对话相关的响应
def handle_chat_messages(message):
    msg_type = message.get('type')
    if msg_type == 'chat_begin':
        print('对话开始')
    elif msg_type == 'chat':
        print('大模型回应:', message.get('data', {}).get('content'))
    elif msg_type == 'chat_end':
        print('对话结束')
    elif msg_type == 'avator-text':
        print('数字人回复:', message.get('data', {}).get('text'))
    elif msg_type == 'avator-playstop':
        print('数字人回复结束')

# 示例:在 WebSocket 消息处理中添加
def on_message(ws, message):
    msg = json.loads(message)
    print('收到消息:', msg)
    
    if msg.get('type') == 'avator-connect':
        # 处理连接验证响应...
        pass
    elif msg.get('type') == 'avator-play':
        # 处理媒体协商响应...
        pass
    elif msg.get('type') in ['chat_begin', 'chat', 'chat_end', 'avator-text', 'avator-playstop']:
        # 处理对话相关的响应
        handle_chat_messages(msg)

3.3.2 数字人控制消息

仅 humanctrl 连接中有效。

协议: JSON 文本消息

{
  "type": "avatar",
  "data": {
    "content": {text},
    "audio": {
      "segment": {segment},
      "speech": {speech},
      "music": {music}
    }
  }
}

参数说明

参数名称类型是否必填参数说明
typeString是消息类型:avatar
dataObject是消息体
data.contentString是数字人的播报内容
data.audioObject否音频数据:16k 单通道 PCM
data.audio.segmentNumber是音频分片索引,-1 表示最后分片,0为第一分片,以此类推
data.audio.speechString是人声语音数据,base64 编码, 分片音频<=10K
data.audio.musicString是人声+音乐的混音数据,base64 编码, 分片音频<=10K

注意

当前仅支持16k 单通道 PCM 音频数据,base64 编码。

当传入音频数据时,数字人会根据speech音频数驱动嘴型,music音频数做播报。

当不传入音频数据时,数字人会根据content转换为音频内容做播报。

示例
// 发送数字人控制消息(仅在 humanctrl 连接中有效)
function sendAvatarControlMessage(ws, content) {
  const controlMessage = {
    type: 'avatar',
    data: {
      content: content
    }
  };
  
  ws.send(JSON.stringify(controlMessage));
  console.log('发送数字人控制消息:', controlMessage);
}

// 示例:发送数字人控制消息
// sendAvatarControlMessage(ws, '欢迎使用 Nuwa 数字人服务!');

// 处理数字人控制相关的响应
function handleAvatarControlResponse(message) {
  if (message.type === 'avator-ctrl') {
    console.log('数字人控制消息:', message.data);
  } else if (message.type === 'avator-playstop') {
    console.log('数字人回复结束');
  }
}

// 示例:在 WebSocket 消息处理中添加
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('收到消息:', message);
  
  if (message.type === 'avator-connect') {
    // 处理连接验证响应...
  } else if (message.type === 'avator-ctrl' || message.type === 'avator-playstop') {
    // 处理数字人控制相关的响应
    handleAvatarControlResponse(message);
  }
};
import json

# 发送数字人控制消息(仅在 humanctrl 连接中有效)
def send_avatar_control_message(ws, content):
    control_message = {
        'type': 'avatar',
        'data': {
            'content': content
        }
    }
    
    ws.send(json.dumps(control_message))
    print('发送数字人控制消息:', control_message)

# 示例:发送数字人控制消息
# send_avatar_control_message(ws, '欢迎使用 Nuwa 数字人服务!')

# 处理数字人控制相关的响应
def handle_avatar_control_response(message):
    if message.get('type') == 'avator-ctrl':
        print('数字人控制消息:', message.get('data'))
    elif message.get('type') == 'avator-playstop':
        print('数字人回复结束')

# 示例:在 WebSocket 消息处理中添加
def on_message(ws, message):
    msg = json.loads(message)
    print('收到消息:', msg)
    
    if msg.get('type') == 'avator-connect':
        # 处理连接验证响应...
        pass
    elif msg.get('type') in ['avator-ctrl', 'avator-playstop']:
        # 处理数字人控制相关的响应
        handle_avatar_control_response(msg)

3.3.2 数字人打断消息

仅 humanctrl 连接中有效。打断数字人说话

协议: JSON 文本消息

{
  "type": "interrupt_chat"
}

参数说明

参数名称类型是否必填参数说明
typeString是消息类型:interrupt_chat
示例
// 发送数字人打断消息(仅在 humanctrl 连接中有效)
function sendInterruptChatMessage(ws) {
  const interrupt_chat_message = {
    type: 'interrupt_chat'
  };
  
  ws.send(JSON.stringify(interrupt_chat_message));
  console.log('发送数字人打断消息:', interrupt_chat_message);
}
import json

# 发送数字人打断消息(仅在 humanctrl 连接中有效)
def send_interrupt_chat_message(ws):
    control_message = {
        'type': 'interrupt_chat',
    }
    
    ws.send(json.dumps(interrupt_chat_message))
    print('发送数字人打断消息:', interrupt_chat_message)

3.4 业务消息通知

数字人连接建立后,数字人服务会通过 WebSocket 发送业务通知。

协议: JSON 文本消息

{
  "code": {code},
  "msg": {message},
  "type": {type},
  "data": {data}
}

参数说明

参数名称类型是否必填参数说明
codeint是响应码:200 为 ok,其他为错误,具体错误 message
messageString否当响应码为 200 时返回。具体错误原因
typeString是消息类型
datajson否由 type 值决定 data 内容

消息类型说明

type说明
avator-load数字人加载通知
avator-connect数字人连接通知(参考鉴权)
avator-play数字人 sdp 通知(参考接收 sdp 信息)
avator-text数字人当前说话文本(字幕)
avator-playstop数字人回应结束
avator-ctrl数字人控制消息
asr-text用户说话实时文本(全量)
asr-commit用户说话提交给大模型的文本
chat_begin大模型的回应开始
chat大模型回应文本
chat_end大模型回应结束

data 值描述

type: avator-text
{
  "text": {text}
}
参数名称类型是否必填参数说明
textString是文本内容
type: asr-text / asr-commit / chat
{
  "content": {text}
}
参数名称类型是否必填参数说明
textString是文本内容
type: avator-ctrl
{
  "cmd": {cmd},
  "action": {action},
  "src": {src}
}
参数名称类型是否必填参数说明
cmdString是控制操作:picture, video
actionString是动作类型:ppt
srcString是picture 或者 video 的路径

4. 音视频实时流协议

4.1 WebRTC 实时流协议

WebRTC (Web Real-Time Communication) 是一种支持网页浏览器进行实时语音、视频通话的开源项目,在 Nuwa 平台中用于拉取数字人的实时音视频流。

4.1.1 协议概述

  • 传输方式:基于 UDP 的点对点传输
  • 媒体格式:支持 H.264 视频编码和 Opus 音频编码
  • 延迟:低延迟,适合实时交互场景
  • 安全性:内置加密机制

4.1.2 连接流程

  1. 建立 WebSocket 连接:使用配置信息构建 WebSocket URL 并建立连接
  2. 接收连接验证:接收 avator-connect 消息,获取会话 ID
  3. 开始媒体协商:创建 RTCPeerConnection,添加音频和视频接收器
  4. 发送协商请求:发送 playStream 请求,携带本地 SDP
  5. 处理协商响应:接收 avator-play 消息,获取远端 SDP
  6. 设置远程描述:使用远端 SDP 设置 RTCPeerConnection 的远程描述
  7. 接收音视频流:通过 ontrack 事件接收数字人的音视频流
sequenceDiagram
    participant 客户端 as 客户端
    participant 平台 as Nuwa 平台
    participant 服务 as Nuwa 数字人服务

    客户端->>平台: 1. 建立 WebSocket 连接
    平台-->>客户端: 2. 响应 avator-connect (返回会话 ID)
    客户端->>客户端: 3. 创建 RTCPeerConnection
    客户端->>客户端: 4. 添加音频和视频接收器
    客户端->>客户端: 5. createOffer 获取本地 SDP
    客户端->>客户端: 6. setLocalDescription 设置本地 SDP
    客户端->>平台: 7. 发送 playStream (携带本地 SDP)
    平台->>服务: 8. 转发 playStream 请求
    服务-->>平台: 9. 返回远端 SDP
    平台-->>客户端: 10. 响应 avator-play (返回远端 SDP)
    客户端->>客户端: 11. setRemoteDescription 设置远程 SDP
    服务-->>客户端: 12. 推送音视频流
    客户端->>客户端: 13. 通过 ontrack 事件接收流

4.1.3 WebRTC 实现示例

示例
// WebRTC 连接示例
let peerConnection = null;
let peerStream = null;

// 开始媒体协商
function startMediaNegotiation(socket, videoElementId) {
  // 创建 RTCPeerConnection
  peerConnection = new RTCPeerConnection(null);
  
  // 创建 MediaStream
  peerStream = new MediaStream();
  
  // 设置视频元素的源
  const video = document.getElementById(videoElementId);
  video.srcObject = peerStream;
  video.onplay = function() {
    video.muted = false;
  };
  
  // 处理音视频流
  peerConnection.ontrack = function(event) {
    console.log('收到视频流: ' + event.streams.length + ' 个流');
    peerStream.addTrack(event.track);
  };
  
  // 添加音频和视频接收器
  peerConnection.addTransceiver('audio', { direction: 'recvonly' });
  peerConnection.addTransceiver('video', { direction: 'recvonly' });
  
  // 创建并发送 SDP
  peerConnection.createOffer().then(offer => {
    return peerConnection.setLocalDescription(offer);
  }).then(() => {
    const message = {
      type: 'playStream',
      data: {
        sdp: peerConnection.localDescription.sdp
      }
    };
    socket.send(JSON.stringify(message));
    console.log('发送 WebRTC 协商请求');
  }).catch(error => {
    console.error('WebRTC 协商错误: ' + error);
  });
}

// 处理 SDP 应答
function handleSdpAnswer(sdp) {
  if (peerConnection) {
    const remoteDescription = new RTCSessionDescription({ type: 'answer', sdp: sdp });
    peerConnection.setRemoteDescription(remoteDescription).then(() => {
      console.log('WebRTC 协商成功');
    }).catch(error => {
      console.error('WebRTC 协商应答错误: ' + error);
    });
  }
}

// 断开 WebRTC 连接
function disconnectWebRTC() {
  if (peerConnection) {
    peerConnection.close();
    peerConnection = null;
  }
  if (peerStream) {
    peerStream.getTracks().forEach(track => track.stop());
    peerStream = null;
  }
  console.log('WebRTC 连接已断开');
}

// 示例:在 WebSocket 消息处理中添加
function handleWebSocketMessage(message, socket) {
  switch (message.type) {
    case 'avator-connect':
      if (message.code === 200) {
        console.log('鉴权成功,会话 ID: ' + message.data.sessionid);
        // 开始媒体协商
        startMediaNegotiation(socket, 'remote-video');
      } else {
        console.log('鉴权失败: ' + message.msg);
      }
      break;
    case 'avator-play':
      if (message.code === 200 && message.data && message.data.sdp) {
        // 处理 SDP 应答
        handleSdpAnswer(message.data.sdp);
      }
      break;
    // 其他消息处理...
  }
}
import asyncio
from aiortc import RTCPeerConnection, RTCSessionDescription

async def setup_webrtc():
    # 创建 RTCPeerConnection
    pc = RTCPeerConnection()
    
    # 处理音视频流
    @pc.on('track')
    async def on_track(track):
        print(f"收到 {track.kind} 轨道")
        if track.kind == 'video':
            # 处理视频流
            pass
        elif track.kind == 'audio':
            # 处理音频流
            pass
    
    # 处理 ICE 候选
    @pc.on('icecandidate')
    async def on_icecandidate(candidate):
        if candidate:
            # 发送 ICE 候选到服务器
            ice_candidate_message = {
                'type': 'iceCandidate',
                'data': {
                    'candidate': candidate.sdp
                }
            }
            # 这里需要通过 WebSocket 发送
            # ws.send(json.dumps(ice_candidate_message))
    
    # 创建本地 SDP
    offer = await pc.create_offer()
    await pc.setLocalDescription(offer)
    
    # 等待远端 SDP
    # 这里需要在 WebSocket 消息处理中获取并设置
    # remote_sdp = ...
    # await pc.setRemoteDescription(RTCSessionDescription(sdp=remote_sdp, type='answer'))
    
    return pc

# 示例使用
async def main():
    pc = await setup_webrtc()
    # 保持连接
    await asyncio.sleep(3600)

# 运行
if __name__ == "__main__":
    asyncio.run(main())

4.1.4 错误处理

错误类型可能原因解决方案
ICE 连接失败网络 NAT 穿透失败配置 TURN 服务器
媒体流无法播放SDP 解析错误检查 SDP 格式是否正确
音视频卡顿网络带宽不足调整视频分辨率或比特率
连接断开网络不稳定实现重连机制

4.2 RTMP 实时流协议

RTMP (Real-Time Messaging Protocol) 是一种用于音视频流传输的协议,在 Nuwa 平台中用于拉取数字人的实时音视频流。

4.2.1 协议概述

  • 传输方式:基于 TCP 的流媒体协议
  • 媒体格式:支持 H.264 视频编码和 AAC 音频编码
  • 延迟:中等延迟,适合直播场景
  • 兼容性:广泛支持各种播放器

4.2.2 连接流程

  1. 获取推流地址:客户端提供 RTMP 推流地址
  2. 发送协商请求:通过 WebSocket 发送 playRtmp 请求
  3. 等待响应:接收平台返回的 avator-play 响应
  4. 拉取流:使用 RTMP 播放器从指定地址拉取音视频流
sequenceDiagram
    participant 客户端 as 客户端
    participant 平台 as Nuwa 平台
    participant 服务 as Nuwa 数字人服务
    participant 播放器 as RTMP 播放器

    客户端->>平台: 1. playRtmp (携带 rmtp_url)
    平台->>服务: 2. 转发 playRtmp 请求
    服务-->>平台: 3. 推流准备就绪
    平台-->>客户端: 4. 响应 avator-play (通知播放)
    服务->>播放器: 5. RTMP 推流 (视频流)
    播放器-->>客户端: 6. 显示音视频

4.2.3 RTMP 实现示例

示例
// RTMP 播放器示例(使用 video.js)
// 首先引入 video.js 和 videojs-flash 插件
// <script src="https://cdn.jsdelivr.net/npm/video.js@7.15.4/dist/video.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/videojs-flash@2.2.1/dist/videojs-flash.min.js"></script>

// 初始化播放器
const player = videojs('remote-video', {
  techOrder: ['html5', 'flash'],
  autoplay: true,
  controls: true,
  sources: [
    {
      src: 'rtmp://your-rtmp-server/live/stream',
      type: 'rtmp/flv'
    }
  ]
});

// 播放器事件监听
player.on('play', function() {
  console.log('播放器开始播放');
});

player.on('error', function() {
  console.error('播放器错误:', player.error());
});

player.on('ended', function() {
  console.log('播放结束');
});

// 发送 playRtmp 请求
function sendPlayRtmp(ws, rtmpUrl) {
  const playRtmpMessage = {
    type: 'playRtmp',
    data: {
      url: rtmpUrl
    }
  };
  
  ws.send(JSON.stringify(playRtmpMessage));
  console.log('发送 playRtmp 请求:', playRtmpMessage);
}

// 处理 avator-play 响应
function handleAvatorPlay(message) {
  if (message.code === 200) {
    console.log('RTMP 媒体协商成功,开始播放');
    // 播放器已经配置好,会自动开始播放
  } else {
    console.error('RTMP 媒体协商失败:', message.msg);
  }
}
import cv2
import subprocess

# RTMP 流拉取示例(使用 OpenCV 和 FFmpeg)
def pull_rtmp_stream(rtmp_url):
    # 使用 FFmpeg 拉取 RTMP 流并转换为 OpenCV 可处理的格式
    command = [
        'ffmpeg',
        '-i', rtmp_url,
        '-f', 'image2pipe',
        '-pix_fmt', 'bgr24',
        '-vcodec', 'rawvideo',
        '-'
    ]
    
    process = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=10**8)
    
    while True:
        # 读取一帧
        raw_frame = process.stdout.read(1920 * 1080 * 3)  # 假设分辨率为 1920x1080
        if not raw_frame:
            break
        
        # 转换为 OpenCV 格式
        frame = cv2.imdecode(np.frombuffer(raw_frame, np.uint8), cv2.IMREAD_COLOR)
        
        # 显示帧
        cv2.imshow('RTMP Stream', frame)
        
        # 按 'q' 退出
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    process.kill()
    cv2.destroyAllWindows()

# 发送 playRtmp 请求
import json
def send_play_rtmp(ws, rtmp_url):
    play_rtmp_message = {
        'type': 'playRtmp',
        'data': {
            'url': rtmp_url
        }
    }
    
    ws.send(json.dumps(play_rtmp_message))
    print('发送 playRtmp 请求:', play_rtmp_message)

# 处理 avator-play 响应
def handle_avator_play(message):
    if message.get('code') == 200:
        print('RTMP 媒体协商成功,开始播放')
        # 开始拉取 RTMP 流
        # pull_rtmp_stream('rtmp://your-rtmp-server/live/stream')
    else:
        print('RTMP 媒体协商失败:', message.get('msg'))

4.2.4 错误处理

错误类型可能原因解决方案
连接失败RTMP 服务器地址错误检查 RTMP 地址是否正确
流无法播放推流未开始等待数字人服务准备就绪
音视频卡顿网络带宽不足调整视频分辨率或使用更稳定的网络
播放器错误播放器不支持 RTMP使用支持 RTMP 的播放器,如 video.js + flash 插件

5. 最佳实践

5.1 连接管理

  • 建议使用自动重连机制,当 WebSocket 连接断开时自动重新连接
  • 设置合理的超时时间,避免长时间等待
  • 做好连接状态管理,确保用户了解当前连接状态

5.2 性能优化

  • 语音数据发送时,建议按照 100ms/200ms 一个 chunk 进行分片

5.3 错误处理

  • 建议实现完善的错误回调机制,及时处理各种异常情况
  • 对于可恢复的错误(如网络波动),实现自动重试机制
  • 记录错误日志,便于问题排查

5.4 安全性

  • 妥善保管 API Key,不要暴露在客户端代码中
  • 使用 HTTPS 和 WSS 加密传输,保护数据安全

6. 常见问题

6.1 连接相关

Q: WebSocket 连接失败怎么办?

A: 请检查以下几点:

  1. 确认 access_token 是否有效
  2. 检查网络连接是否正常
  3. 确认数字人 ID 是否正确
  4. 查看错误码信息,定位具体原因

Q: 连接成功后没有收到鉴权响应?

A: 可能是网络延迟或数字人模型加载处理较慢,建议增加超时等待时间,同时检查 WebSocket 连接状态。

6.2 音视频相关

Q: 音视频卡顿怎么办?

A: 建议检查以下几点:

  1. 网络带宽是否充足
  2. 客户端设备性能是否满足当前数字人的H264解码能力要求
  3. 检查是否有其他应用占用网络带宽

Q: 无法拉取音视频流?

A: 请确认:

  1. 媒体协商是否成功
  2. SDP 解析是否正确
  3. 防火墙是否阻止了相关端口

6.3 消息相关

Q: 发送语音数据后没有响应?

A: 请检查:

  1. 语音数据格式是否为 16k 单通道 PCM
  2. 数据分片大小是否正确(建议 100ms/200ms 一个 chunk)

Q: 收到的文本消息是乱码?

A: 请确保使用 UTF-8 编码解析消息内容。

最近更新:: 2026/6/23 12:12
Contributors: 邹丹晔, 005609