NuwaAI Digital Human PlatformNuwaAI Digital Human Platform
  • 简体中文
  • English
  • 简体中文
  • English
  • Real-time digital-human interaction protocol

Real-time digital-human interaction protocol

1. Overview

The Nuwa platform provides an open API protocol for real-time digital-human interaction, enabling developers to integrate digital-human capabilities. The protocol consists of two cooperating components:

1.1 Signaling

The client establishes a signaling channel with the Nuwa platform over WebSocket. This channel manages sessions and business logic, including:

  • WebSocket connection verification: Verify the digital-human identity.
  • Media negotiation: Negotiate audio/video transport protocols.
  • Voice/text reporting: Send user input.
  • Business notifications: Receive digital-human status and response text.

1.2 Live audio and video

The client uses WebRTC or RTMP to pull the digital-human's live audio/video stream from the negotiated address.

Example

Live demo

Tips

If you only need ASR capabilities, see: Nuwa ASR real-time speech recognition protocol

2. Before development

  • Get an API key:

Before you begin, complete these steps on the NuwaAI platform:

  1. Register a NuwaAI account and sign in.
  2. Record your user ID.
  3. Create a digital human and record its ID.
  4. Create and securely record an API key.

Get an API key, digital-human ID, and user ID

  • Get an access token:

    • Use the authentication API to obtain an access token and securely verify user permissions.
  • Interaction flow:

    • WebRTC interaction flow
sequenceDiagram
    participant Client
    participant Platform as Nuwa Platform
    participant Service as Nuwa Digital Human Service

    Client->>Platform: 1. Open a WebSocket connection
    Platform-->>Client: 2. Return avator-connect (authentication result)

    Client->>Platform: 3. playStream (with local SDP)
    Platform-->>Client: 4. Return avator-play (remote SDP)
    Client->>Client: 5. Parse the SDP and initialize WebRTC stream reception
    Service-->>Client: 6. Send the audio/video stream

    Service-->>Client: 7. avator-playstop (opening message complete)

    Client-->>Platform: 8. 16 kHz mono audio data
    Platform-->>Service: Forward the audio data
    Client-->>Platform: 9. Send a conversation message (optional)

    Platform-->>Client: 10. chat_begin (conversation started)
    Platform-->>Client: 11. chat (conversation in progress)
    Platform-->>Client: 12. chat_end (conversation ended)

    Service-->>Client: 13. avator-text (digital-human response text)
    Service-->>Client: 14. avator-text (subsequent text)
    Service-->>Client: 15. avator-playstop (response complete)
  • RTMP interaction flow
sequenceDiagram
    participant C as Client 📱
    participant P as Nuwa Platform ☁️
    participant S as Nuwa Digital Human Service 🤖
    participant R as RTMP receiver 📡

    %% 1. Connection and authentication
    C->>P: 1. Open a WebSocket connection
    P-->>C: 2. Return avator-connet (authentication result)

    %% 2. Start the RTMP stream
    C->>P: 3. playRtmp (with rmtp_url)
    P->>S: 4. playRtmp (forward rmtp_url)
    
    %% 3. Platform notifies the client that playback can begin
    S-->>P: 5. Stream ready (internal status)
    P-->>C: 6. Return avator-play (start playback)

    %% 4. Digital-human service streams to the receiver
    S->>R: 7. RTMP stream (video)

    %% 5. Opening-message completion notification
    S-->>P: 8. avator-playstop (opening message complete)
    P-->>C: 9. Forward avator-playstop

    %% 6. Voice-input phase
    C->>P: 10. 16 kHz mono audio data
    P->>S: 11. Forward the audio data

    %% 7. Conversation-message notifications (optional)
    P-->>C: 12. chat_begin (conversation started)
    P-->>C: 13. chat (conversation in progress)
    P-->>C: 14. chat_end (conversation ended)

    %% 8. Response
    S-->>P: 15. avator-text (digital-human response)
    P-->>C: 16. Forward avator-text
    S-->>P: 17. avator-text (subsequent text)
    P-->>C: 18. Forward avator-text
    S-->>P: 19. avator-playstop (response complete)
    P-->>C: 20. Forward avator-playstop

3. WebSocket signaling

3.1 Verify the WebSocket connection

Create a WebSocket connection

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

Parameters

NameTypeRequiredDescription
protocolStringNoStream protocol: webrtc (default) or rtmp.
avatoridStringYesDigital-human role ID.
bizidStringYesBusiness ID (asr when ASR is enabled for humanctrl; otherwise 0).
tokenStringYesToken obtained through authentication.

Tips

For digital-human control only (speech, actions, expressions, and similar operations), use:

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

3.1.2 Wait for verification response

Protocol: JSON text message

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

Parameters

NameTypeRequiredDescription
codeintYesResponse code: 200 means OK; other values indicate an error described by message.
messageStringNoDetailed error message.
typeStringYesMessage type: avator-connect.
dataObjectYesMessage payload.
data.questionArrayYesThree suggested questions.
data.sessionidStringYesSession ID.
示例
// WebSocket connection example
const protocol = 'webrtc'; // or 'rtmp'
const avatorId = 'your-avator-id';
const bizId = '0'; // or 'asr' (when ASR is enabled for humanctrl)
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);

// Connection established
ws.onopen = function() {
  console.log('WebSocket connection established');
};

// Receive messages
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('Message received:', message);
  
  // Handle the connection-verification response
  if (message.type === 'avator-connect') {
    if (message.code === 200) {
      console.log('Connection verified; session ID:', message.data.sessionid);
      console.log('Suggested questions:', message.data.question);
      // After verification succeeds, continue to the next operation
    } else {
      console.error('Connection verification failed:', message.msg);
    }
  }
};

// Connection closed
ws.onclose = function() {
  console.log('WebSocket connection closed');
};

// Connection error
ws.onerror = function(error) {
  console.error('WebSocket connection error:', error);
};
import websocket
import json

# WebSocket connection example
protocol = 'webrtc'  # or 'rtmp'
avator_id = 'your-avator-id'
biz_id = '0'  # or 'asr' (when ASR is enabled for humanctrl)
token = 'your-access-token'

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

# Define callback functions
def on_open(ws):
    print('WebSocket connection established')

def on_message(ws, message):
    msg = json.loads(message)
    print('Message received:', msg)
    
    # Handle the connection-verification response
    if msg.get('type') == 'avator-connect':
        if msg.get('code') == 200:
            print('Connection verified; session ID:', msg.get('data', {}).get('sessionid'))
            print('Suggested questions:', msg.get('data', {}).get('question'))
            # After verification succeeds, continue to the next operation
        else:
            print('Connection verification failed:', msg.get('msg'))

def on_close(ws, close_status_code, close_msg):
    print('WebSocket connection closed')

def on_error(ws, error):
    print('WebSocket connection error:', error)

# Create the WebSocket connection
ws = websocket.WebSocketApp(ws_url, 
                          on_open=on_open, 
                          on_message=on_message, 
                          on_close=on_close, 
                          on_error=on_error)

# Run the WebSocket client
ws.run_forever()

3.2 Media negotiation

Send media-negotiation requests over WebSocket. WebRTC and RTMP are supported; the WebSocket protocol parameter must match the negotiated protocol.

3.2.1 WebRTC protocol

Send a media-negotiation request

Protocol: JSON text message

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

Parameters

NameTypeRequiredDescription
typeStringYesMessage type: playStream.
dataObjectYesMessage payload.
data.sdpStringYesLocal SDP.
Wait for negotiation response

Protocol: JSON text message

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

Parameters

NameTypeRequiredDescription
codeintYesResponse code: 200 means OK; otherwise see message.
messageStringNoDetailed error message.
typeStringYesMessage type: avator-play.
dataObjectYesMessage payload.
data.sdpsdpYesRemote SDP information.
示例
// Send a WebRTC media-negotiation request
function sendPlayStream(ws, localSdp) {
  const playStreamMessage = {
    type: 'playStream',
    data: {
      sdp: localSdp
    }
  };
  
  ws.send(JSON.stringify(playStreamMessage));
  console.log('playStream request sent:', playStreamMessage);
}

// Handle the WebRTC negotiation response
function handleAvatorPlay(message) {
  if (message.code === 200) {
    const remoteSdp = message.data.sdp;
    console.log('avator-play response received; remote SDP:', remoteSdp);
    
    // Establish the WebRTC connection with the remote SDP
    // Handle this according to your WebRTC implementation
    // For example:
    // pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: remoteSdp }));
  } else {
    console.error('Media negotiation failed:', message.msg);
  }
}

// Example: add this to the WebSocket message handler
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('Message received:', message);
  
  if (message.type === 'avator-connect') {
    // Handle the connection-verification response...
  } else if (message.type === 'avator-play') {
    // Handle the media-negotiation response
    handleAvatorPlay(message);
  }
};
import json

# Send a WebRTC media-negotiation request
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 request sent:', play_stream_message)

# Handle the WebRTC negotiation response
def handle_avator_play(message):
    if message.get('code') == 200:
        remote_sdp = message.get('data', {}).get('sdp')
        print('avator-play response received; remote SDP:', remote_sdp)
        
        # Establish the WebRTC connection with the remote SDP
        # Handle this according to your WebRTC implementation
    else:
        print('Media negotiation failed:', message.get('msg'))

# Example: add this to the WebSocket message handler
def on_message(ws, message):
    msg = json.loads(message)
    print('Message received:', msg)
    
    if msg.get('type') == 'avator-connect':
        # Handle the connection-verification response...
        pass
    elif msg.get('type') == 'avator-play':
        # Handle the media-negotiation response
        handle_avator_play(msg)

3.2.2 RTMP protocol

Send a media-negotiation request

Protocol: JSON text message

{
  "type": "playRtmp",
  "data": {
    "url": {url},
    "rate": {
      "default": 6000000, 
      "max": 8000000, 
      "min": 4000000
    }
    "codec": {
      "type": "H264", 
      "profile": "baseline", 
      "gop": 30,
      "width": 1280,
      "height": 720
    }

  }
}

Parameters

NameTypeRequiredDescription
typeStringYesMessage type: playRtmp.
dataObjectYesMessage payload.
data.urlStringYesDigital-human stream URL.
data.rateObjectNoBitrate settings.
data.rate.defaultintYesInitial bitrate.
data.rate.maxintYesMaximum bitrate.
data.rate.minintYesMinimum bitrate.
data.codecObjectNoEncoder settings.
data.codec.typestrYesEncoder type; only H.264 is supported.
data.codec.profilestrYesbaseline, main, or high.
data.codec.gopintYesGreater than 10.
data.codec.widthintNoMultiple of 2.
data.codec.heightintNoMultiple of 2.

Tips

If data.codec.width or height is provided, both values are required. If only one is set, neither value takes effect.

Wait for negotiation response

Protocol: JSON text message

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

Parameters

NameTypeRequiredDescription
codeintYesResponse code: 200 means OK; otherwise see message.
messageStringNoDetailed error message.
typeStringYesMessage type: avator-play.
示例
// Send an RTMP media-negotiation request
function sendPlayRtmp(ws, rtmpUrl) {
  const playRtmpMessage = {
    type: 'playRtmp',
    data: {
      url: rtmpUrl
    }
  };
  
  ws.send(JSON.stringify(playRtmpMessage));
  console.log('playRtmp request sent:', playRtmpMessage);
}

// Handle the RTMP negotiation response
function handleAvatorPlayRtmp(message) {
  if (message.code === 200) {
    console.log('RTMP media negotiation succeeded');
    
    // Begin receiving the audio/video stream over RTMP
    // Handle this according to your RTMP player implementation
    // For example, load the RTMP stream with video.js or another player
  } else {
    console.error('RTMP media negotiation failed:', message.msg);
  }
}

// Example: add this to the WebSocket message handler
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('Message received:', message);
  
  if (message.type === 'avator-connect') {
    // Handle the connection-verification response...
  } else if (message.type === 'avator-play') {
    // Handle the media-negotiation response
    handleAvatorPlayRtmp(message);
  }
};
import json

# Send an RTMP media-negotiation request
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 request sent:', play_rtmp_message)

# Handle the RTMP negotiation response
def handle_avator_play_rtmp(message):
    if message.get('code') == 200:
        print('RTMP media negotiation succeeded')
        
        # Begin receiving the audio/video stream over RTMP
        # Handle this according to your RTMP player implementation
    else:
        print('RTMP media negotiation failed:', message.get('msg'))

# Example: add this to the WebSocket message handler
def on_message(ws, message):
    msg = json.loads(message)
    print('Message received:', msg)
    
    if msg.get('type') == 'avator-connect':
        # Handle the connection-verification response...
        pass
    elif msg.get('type') == 'avator-play':
        # Handle the media-negotiation response
        handle_avator_play_rtmp(msg)

3.3 Send conversation messages

For voice conversations, send 16 kHz mono PCM data in 60 ms chunks. Send text conversations as JSON messages.

3.3.1 Text conversation protocol

Protocol: JSON text message

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

Parameters

NameTypeRequiredDescription
typeStringYesMessage type: agent.
dataObjectYesMessage payload.
data.contentStringYesConversation content.
示例
// Send a text conversation message
function sendTextMessage(ws, text) {
  const textMessage = {
    type: 'agent',
    data: {
      content: text
    }
  };
  
  ws.send(JSON.stringify(textMessage));
  console.log('Text message sent:', textMessage);
}

// Example: send a text message
// sendTextMessage(ws, '你好,数字人!');

// Handle conversation responses
function handleChatMessages(message) {
  switch (message.type) {
    case 'chat_begin':
      console.log('Conversation started');
      break;
    case 'chat':
      console.log('Language-model response:', message.data.content);
      break;
    case 'chat_end':
      console.log('Conversation ended');
      break;
    case 'avator-text':
      console.log('Digital-human response:', message.data.text);
      break;
    case 'avator-playstop':
      console.log('Digital-human response complete');
      break;
  }
}

// Example: add this to the WebSocket message handler
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('Message received:', message);
  
  if (message.type === 'avator-connect') {
    // Handle the connection-verification response...
  } else if (message.type === 'avator-play') {
    // Handle the media-negotiation response...
  } else if (['chat_begin', 'chat', 'chat_end', 'avator-text', 'avator-playstop'].includes(message.type)) {
    // Handle conversation responses
    handleChatMessages(message);
  }
};
import json

# Send a text conversation message
def send_text_message(ws, text):
    text_message = {
        'type': 'agent',
        'data': {
            'content': text
        }
    }
    
    ws.send(json.dumps(text_message))
    print('Text message sent:', text_message)

# Example: send a text message
# send_text_message(ws, '你好,数字人!')

# Handle conversation responses
def handle_chat_messages(message):
    msg_type = message.get('type')
    if msg_type == 'chat_begin':
        print('Conversation started')
    elif msg_type == 'chat':
        print('Language-model response:', message.get('data', {}).get('content'))
    elif msg_type == 'chat_end':
        print('Conversation ended')
    elif msg_type == 'avator-text':
        print('Digital-human response:', message.get('data', {}).get('text'))
    elif msg_type == 'avator-playstop':
        print('Digital-human response complete')

# Example: add this to the WebSocket message handler
def on_message(ws, message):
    msg = json.loads(message)
    print('Message received:', msg)
    
    if msg.get('type') == 'avator-connect':
        # Handle the connection-verification response...
        pass
    elif msg.get('type') == 'avator-play':
        # Handle the media-negotiation response...
        pass
    elif msg.get('type') in ['chat_begin', 'chat', 'chat_end', 'avator-text', 'avator-playstop']:
        # Handle conversation responses
        handle_chat_messages(msg)

3.3.2 Digital-human control messages

Valid only for humanctrl connections.

Protocol: JSON text message

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

Parameters

NameTypeRequiredDescription
typeStringYesMessage type: avatar.
dataObjectYesMessage payload.
data.contentStringYesDigital-human speech content.
data.audioObjectNo16 kHz mono PCM audio data.
data.audio.segmentNumberYesAudio segment index; -1 is the final segment and 0 is the first.
data.audio.speechStringYesBase64-encoded voice data; each segment ≤10 KB.
data.audio.musicStringYesBase64-encoded voice+music mix; each segment ≤10 KB.

Tips

Only base64-encoded 16 kHz mono PCM audio is supported.

When audio is provided, speech drives lip movements and music is used for playback.

Without audio, the digital human converts content to speech for playback.

示例
// Send a digital-human control message (valid only for humanctrl connections)
function sendAvatarControlMessage(ws, content) {
  const controlMessage = {
    type: 'avatar',
    data: {
      content: content
    }
  };
  
  ws.send(JSON.stringify(controlMessage));
  console.log('Digital-human control message sent:', controlMessage);
}

// Example: send a digital-human control message
// sendAvatarControlMessage(ws, '欢迎使用 Nuwa 数字人服务!');

// Handle digital-human control responses
function handleAvatarControlResponse(message) {
  if (message.type === 'avator-ctrl') {
    console.log('Digital-human control message:', message.data);
  } else if (message.type === 'avator-playstop') {
    console.log('Digital-human response complete');
  }
}

// Example: add this to the WebSocket message handler
ws.onmessage = function(event) {
  const message = JSON.parse(event.data);
  console.log('Message received:', message);
  
  if (message.type === 'avator-connect') {
    // Handle the connection-verification response...
  } else if (message.type === 'avator-ctrl' || message.type === 'avator-playstop') {
    // Handle digital-human control responses
    handleAvatarControlResponse(message);
  }
};
import json

# Send a digital-human control message (valid only for humanctrl connections)
def send_avatar_control_message(ws, content):
    control_message = {
        'type': 'avatar',
        'data': {
            'content': content
        }
    }
    
    ws.send(json.dumps(control_message))
    print('Digital-human control message sent:', control_message)

# Example: send a digital-human control message
# send_avatar_control_message(ws, '欢迎使用 Nuwa 数字人服务!')

# Handle digital-human control responses
def handle_avatar_control_response(message):
    if message.get('type') == 'avator-ctrl':
        print('Digital-human control message:', message.get('data'))
    elif message.get('type') == 'avator-playstop':
        print('Digital-human response complete')

# Example: add this to the WebSocket message handler
def on_message(ws, message):
    msg = json.loads(message)
    print('Message received:', msg)
    
    if msg.get('type') == 'avator-connect':
        # Handle the connection-verification response...
        pass
    elif msg.get('type') in ['avator-ctrl', 'avator-playstop']:
        # Handle digital-human control responses
        handle_avatar_control_response(msg)

3.3.2 Interrupt the digital human

Valid only for humanctrl connections. Interrupt the digital human's speech.

Protocol: JSON text message

{
  "type": "interrupt_chat"
}

Parameters

NameTypeRequiredDescription
typeStringYesMessage type: interrupt_chat.
示例
// Send a digital-human interruption message (valid only for humanctrl connections)
function sendInterruptChatMessage(ws) {
  const interrupt_chat_message = {
    type: 'interrupt_chat'
  };
  
  ws.send(JSON.stringify(interrupt_chat_message));
  console.log('Digital-human interruption message sent:', interrupt_chat_message);
}
import json

# Send a digital-human interruption message (valid only for humanctrl connections)
def send_interrupt_chat_message(ws):
    control_message = {
        'type': 'interrupt_chat',
    }
    
    ws.send(json.dumps(interrupt_chat_message))
    print('Digital-human interruption message sent:', interrupt_chat_message)

3.4 Business notifications

After the digital-human connection is established, the service sends business notifications over WebSocket.

Protocol: JSON text message

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

Parameters

NameTypeRequiredDescription
codeintYesResponse code: 200 means OK; otherwise see message.
messageStringNoDetailed error message.
typeStringYesMessage type.
datajsonNoPayload determined by type.

Message types

typeDescription
avator-loadDigital-human loading notification
avator-connectDigital-human connection notification (see authentication)
avator-playDigital-human SDP notification
avator-textCurrent digital-human speech text (captions)
avator-playstopDigital-human response complete
avator-ctrlDigital-human control message
asr-textFull real-time user speech text
asr-commitUser speech committed to the language model
chat_beginLanguage-model response started
chatLanguage-model response text
chat_endLanguage-model response complete

data values

type: avator-text
{
  "text": {text}
}
NameTypeRequiredDescription
textStringYesText content.
type: asr-text / asr-commit / chat
{
  "content": {text}
}
NameTypeRequiredDescription
textStringYesText content.
type: avator-ctrl
{
  "cmd": {cmd},
  "action": {action},
  "src": {src}
}
NameTypeRequiredDescription
cmdStringYesControl operation: picture or video.
actionStringYesAction type, for example ppt.
srcStringYesPath to the picture or video.

4. Live audio and video protocol

4.1 WebRTC live-stream protocol

WebRTC (Web Real-Time Communication) is an open standard for real-time voice and video in browsers. Nuwa uses it to deliver the digital human's live stream.

4.1.1 Protocol overview

  • Transport: UDP-based peer-to-peer transmission.
  • Media formats: H.264 video and Opus audio.
  • Latency: Low latency for interactive scenarios.
  • Security: Built-in encryption.

4.1.2 Connection flow

  1. Open a WebSocket connection using the configured URL.
  2. Receive connection verification: Handle avator-connect and obtain the session ID.
  3. Start media negotiation: Create an RTCPeerConnection and add audio/video receivers.
  4. Send the negotiation request: Send playStream with local SDP.
  5. Handle the response: Receive avator-play and obtain remote SDP.
  6. Set the remote description on the peer connection.
  7. Receive the media stream through the ontrack event.
sequenceDiagram
    participant Client
    participant Platform as Nuwa Platform
    participant Service as Nuwa Digital Human Service

    Client->>Platform: 1. Open a WebSocket connection
    Platform-->>Client: 2. Return avator-connect (session ID)
    Client->>Client: 3. Create an RTCPeerConnection
    Client->>Client: 4. Add audio and video receivers
    Client->>Client: 5. Use createOffer to obtain the local SDP
    Client->>Client: 6. Use setLocalDescription to set the local SDP
    Client->>Platform: 7. Send playStream (with local SDP)
    Platform->>Service: 8. Forward the playStream request
    Service-->>Platform: 9. Return the remote SDP
    Platform-->>Client: 10. Return avator-play (remote SDP)
    Client->>Client: 11. Use setRemoteDescription to set the remote SDP
    Service-->>Client: 12. Send the audio/video stream
    Client->>Client: 13. Receive the stream through the ontrack event

4.1.3 WebRTC implementation example

示例
// WebRTC connection example
let peerConnection = null;
let peerStream = null;

// Start media negotiation
function startMediaNegotiation(socket, videoElementId) {
  // Create an RTCPeerConnection
  peerConnection = new RTCPeerConnection(null);
  
  // Create a MediaStream
  peerStream = new MediaStream();
  
  // Set the video element source
  const video = document.getElementById(videoElementId);
  video.srcObject = peerStream;
  video.onplay = function() {
    video.muted = false;
  };
  
  // Handle the audio/video stream
  peerConnection.ontrack = function(event) {
    console.log('Video stream received: ' + event.streams.length + ' stream(s)');
    peerStream.addTrack(event.track);
  };
  
  // Add audio and video receivers
  peerConnection.addTransceiver('audio', { direction: 'recvonly' });
  peerConnection.addTransceiver('video', { direction: 'recvonly' });
  
  // Create and send the 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 negotiation request sent');
  }).catch(error => {
    console.error('WebRTC negotiation error: ' + error);
  });
}

// Handle the SDP answer
function handleSdpAnswer(sdp) {
  if (peerConnection) {
    const remoteDescription = new RTCSessionDescription({ type: 'answer', sdp: sdp });
    peerConnection.setRemoteDescription(remoteDescription).then(() => {
      console.log('WebRTC negotiation succeeded');
    }).catch(error => {
      console.error('WebRTC negotiation-answer error: ' + error);
    });
  }
}

// Close the WebRTC connection
function disconnectWebRTC() {
  if (peerConnection) {
    peerConnection.close();
    peerConnection = null;
  }
  if (peerStream) {
    peerStream.getTracks().forEach(track => track.stop());
    peerStream = null;
  }
  console.log('WebRTC connection closed');
}

// Example: add this to the WebSocket message handler
function handleWebSocketMessage(message, socket) {
  switch (message.type) {
    case 'avator-connect':
      if (message.code === 200) {
        console.log('Authentication succeeded; session ID: ' + message.data.sessionid);
        // Start media negotiation
        startMediaNegotiation(socket, 'remote-video');
      } else {
        console.log('Authentication failed: ' + message.msg);
      }
      break;
    case 'avator-play':
      if (message.code === 200 && message.data && message.data.sdp) {
        // Handle the SDP answer
        handleSdpAnswer(message.data.sdp);
      }
      break;
    // Handle other messages...
  }
}
import asyncio
from aiortc import RTCPeerConnection, RTCSessionDescription

async def setup_webrtc():
    # Create an RTCPeerConnection
    pc = RTCPeerConnection()
    
    # Handle the audio/video stream
    @pc.on('track')
    async def on_track(track):
        print(f"Received {track.kind} track")
        if track.kind == 'video':
            # Handle the video stream
            pass
        elif track.kind == 'audio':
            # Handle the audio stream
            pass
    
    # Handle ICE candidates
    @pc.on('icecandidate')
    async def on_icecandidate(candidate):
        if candidate:
            # Send the ICE candidate to the server
            ice_candidate_message = {
                'type': 'iceCandidate',
                'data': {
                    'candidate': candidate.sdp
                }
            }
            # Send this over WebSocket
            # ws.send(json.dumps(ice_candidate_message))
    
    # Create the local SDP
    offer = await pc.create_offer()
    await pc.setLocalDescription(offer)
    
    # Wait for the remote SDP
    # Obtain and set this in the WebSocket message handler
    # remote_sdp = ...
    # await pc.setRemoteDescription(RTCSessionDescription(sdp=remote_sdp, type='answer'))
    
    return pc

# Usage example
async def main():
    pc = await setup_webrtc()
    # Keep the connection open
    await asyncio.sleep(3600)

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

4.1.4 Error handling

ErrorPossible causeSolution
ICE connection failedNAT traversal failedConfigure a TURN server.
Media cannot playInvalid SDPCheck the SDP format.
Audio/video stuttersInsufficient bandwidthAdjust resolution or bitrate.
Connection droppedUnstable networkImplement reconnection.

4.2 RTMP live-stream protocol

RTMP (Real-Time Messaging Protocol) transports audio/video streams. Nuwa uses it to deliver the digital human's live stream.

4.2.1 Protocol overview

  • Transport: TCP-based streaming.
  • Media formats: H.264 video and AAC audio.
  • Latency: Moderate latency, suitable for live streaming.
  • Compatibility: Widely supported by media players.

4.2.2 Connection flow

  1. Obtain a stream URL: The client provides an RTMP stream URL.
  2. Send a negotiation request: Send playRtmp over WebSocket.
  3. Wait for a response: Receive the platform's avator-play response.
  4. Pull the stream: Use an RTMP player to retrieve audio/video from the URL.
sequenceDiagram
    participant Client
    participant Platform as Nuwa Platform
    participant Service as Nuwa Digital Human Service
    participant Player as RTMP player

    Client->>Platform: 1. playRtmp (with rmtp_url)
    Platform->>Service: 2. Forward the playRtmp request
    Service-->>Platform: 3. Stream ready
    Platform-->>Client: 4. Return avator-play (start playback)
    Service->>Player: 5. RTMP stream (video)
    Player-->>Client: 6. Display audio and video

4.2.3 RTMP implementation example

示例
// RTMP player example (using video.js)
// First, include video.js and the videojs-flash plugin
// <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>

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

// Listen for player events
player.on('play', function() {
  console.log('Player started playback');
});

player.on('error', function() {
  console.error('Player error:', player.error());
});

player.on('ended', function() {
  console.log('Playback ended');
});

// Send a playRtmp request
function sendPlayRtmp(ws, rtmpUrl) {
  const playRtmpMessage = {
    type: 'playRtmp',
    data: {
      url: rtmpUrl
    }
  };
  
  ws.send(JSON.stringify(playRtmpMessage));
  console.log('playRtmp request sent:', playRtmpMessage);
}

// Handle the avator-play response
function handleAvatorPlay(message) {
  if (message.code === 200) {
    console.log('RTMP media negotiation succeeded; starting playback');
    // The configured player starts playback automatically
  } else {
    console.error('RTMP media negotiation failed:', message.msg);
  }
}
import cv2
import subprocess

# RTMP stream-retrieval example (using OpenCV and FFmpeg)
def pull_rtmp_stream(rtmp_url):
    # Use FFmpeg to retrieve the RTMP stream and convert it to a format OpenCV can process
    command = [
        'ffmpeg',
        '-i', rtmp_url,
        '-f', 'image2pipe',
        '-pix_fmt', 'bgr24',
        '-vcodec', 'rawvideo',
        '-'
    ]
    
    process = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=10**8)
    
    while True:
        # Read one frame
        raw_frame = process.stdout.read(1920 * 1080 * 3)  # Assume a resolution of 1920x1080
        if not raw_frame:
            break
        
        # Convert to OpenCV format
        frame = cv2.imdecode(np.frombuffer(raw_frame, np.uint8), cv2.IMREAD_COLOR)
        
        # Display the frame
        cv2.imshow('RTMP Stream', frame)
        
        # Press 'q' to exit
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    process.kill()
    cv2.destroyAllWindows()

# Send a playRtmp request
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 request sent:', play_rtmp_message)

# Handle the avator-play response
def handle_avator_play(message):
    if message.get('code') == 200:
        print('RTMP media negotiation succeeded; starting playback')
        # Begin retrieving the RTMP stream
        # pull_rtmp_stream('rtmp://your-rtmp-server/live/stream')
    else:
        print('RTMP media negotiation failed:', message.get('msg'))

4.2.4 Error handling

ErrorPossible causeSolution
Connection failedIncorrect RTMP server URLCheck the RTMP URL.
Stream cannot playStreaming has not startedWait for the digital-human service to be ready.
Audio/video stuttersInsufficient bandwidthAdjust resolution or use a more stable network.
Player errorPlayer does not support RTMPUse an RTMP-capable player such as video.js with a Flash plugin.

5. Best practices

5.1 Connection management

  • Reconnect automatically when the WebSocket closes unexpectedly.
  • Set reasonable timeouts to avoid indefinite waits.
  • Track connection state so users know whether they are connected.

5.2 Performance optimization

  • Send voice data in 100–200 ms chunks.

5.3 Error handling

  • Implement comprehensive error callbacks.
  • Retry recoverable errors such as transient network failures.
  • Log errors for troubleshooting.

5.4 Security

  • Keep API keys secure; never expose them in client-side code.
  • Use HTTPS and WSS to encrypt traffic.

6. FAQ

6.1 Connection

Q: What should I do if the WebSocket connection fails?

A: Verify that:

  1. The access_token is valid.
  2. The network connection is working.
  3. The digital-human ID is correct.
  4. The error code does not identify another specific cause.

Q: Why is there no authentication response after connecting?

A: Network latency or slow model loading may delay the response. Increase the timeout and verify the WebSocket state.

6.2 Audio/video

Q: What should I do if audio/video stutters?

A: Check that:

  1. Network bandwidth is sufficient.
  2. The client device meets H.264 decoding requirements.
  3. No other application is consuming bandwidth.

Q: Why can't I pull the audio/video stream?

A: Confirm that:

  1. Media negotiation succeeded.
  2. SDP is parsed correctly.
  3. The firewall allows the required ports.

6.3 Messages

Q: Why is there no response after sending voice data?

A: Check that:

  1. Voice data is 16 kHz mono PCM.
  2. Chunk sizes are correct (100–200 ms recommended).

Q: Why is received text garbled?

A: Parse message content using UTF-8 encoding.

Last Updated:: 9/23/26, 3:03 PM
Contributors: 邹丹晔, 005609