Use o AOQ para conectar-se ao qwen3.5-omni-plus-realtime e permita que o cliente controle os limites de turno em conversas push-to-talk, com suporte opcional a perguntas por imagem. O código do cliente usa iOS Swift.
Visão geral da solução
O Qwen-Omni-Realtime oferece suporte a VAD no lado do servidor e ao modo Manual controlado pelo cliente. Este tutorial define session.turn_detection como null. O cliente envia áudio enquanto o usuário mantém um botão pressionado, confirma o envio e solicita explicitamente uma resposta ao soltar o botão.
O modo Manual é ideal para botões de interfone físico, controles de pressionar e segurar, ambientes ruidosos em que a aplicação determina os limites de turno e turnos que incluem opcionalmente uma imagem. O transporte de áudio ocorre pela faixa de áudio do AOQ. Não envie input_audio_buffer.append.
Item | Modo VAD | Modo Manual |
Limite de turno | Detectado por server_vad ou semantic_vad | Controlado por botão ou estado da aplicação |
Configuração da sessão | turn_detection contém definições de VAD | turn_detection é null |
Confirmação de áudio | O service realiza automaticamente | O cliente envia input_audio_buffer.commit |
Gatilho de resposta | O service aciona automaticamente | O cliente envia response.create |
Entrada de imagem | Faixa de vídeo contínua ou imagem via faixa de dados | Faixa de vídeo contínua ou imagem via faixa de dados |
Pré-requisitos
- Ative o Model Studio e siga Obtain and configure an API key. Armazene a chave de API exclusivamente no servidor da sua aplicação. Não a inclua no código do cliente nem a confirme em repositórios de código.
- Confirme o endpoint do AOQ correspondente à região onde sua aplicação está implantada. Para orientações sobre a seleção, consulte Select a region, deployment scope, and endpoint.
- Baixe a versão mais recente do SDK do cliente AOQ conforme descrito em SDK download.
- Construa um servidor de aplicação e implemente a autenticação proxy conforme detalhado em Token authentication. Antes de cada nova conexão, o cliente deve obter novas credenciais de conexão no servidor da aplicação.
Importe o SDK
Importe o SDK adequado à sua plataforma de desenvolvimento. A implementação do cliente usa iOS Swift, mas outras plataformas oferecem as mesmas interfaces e fluxo de eventos. Este tutorial emprega streams de áudio PCM. Um plugin fornece a codificação Opus. Caso o uplink utilize Opus, importe também o plugin correspondente.
Android
- Coloque o arquivo AoqClientSdk-release.aar em app/libs e configure a dependência e as ABIs suportadas pelo SDK no arquivo app/build.gradle:
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- Declare as seguintes permissões no AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
- Solicite as permissões RECORD_AUDIO e CAMERA em tempo de execução antes de usar os dispositivos correspondentes.
iOS
- Adicione o AoqClientSdk.framework ao projeto Xcode e selecione Embed & Sign em Target > General > Frameworks, Libraries, and Embedded Content. O SDK suporta dispositivos arm64 com iOS 13.0 ou superior.
- Inclua NSMicrophoneUsageDescription e NSCameraUsageDescription no Info.plist e solicite autorização antes de usar os respectivos dispositivos.
- Use import AoqClientSdk em Swift ou #import <AoqClientSdk/AoqClientSdk.h> em Objective-C.
HarmonyOS
- Coloque o AoqClientSdk.har em entry/libs e declare a dependência em entry/oh-package.json5. O SDK é compatível com a API 12 e suporta arm64-v8a:
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
- Declare as seguintes permissões em entry/src/main/module.json5:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.MICROPHONE",
"reason": "$string:perm_mic_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } },
{ "name": "ohos.permission.CAMERA",
"reason": "$string:perm_camera_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }
]
- Antes de usar os dispositivos correspondentes, chame abilityAccessCtrl.createAtManager().requestPermissionsFromUser para solicitar ohos.permission.MICROPHONE e ohos.permission.CAMERA.
Linux (Python)
- Extraia o SDK e mantenha aoq_client_sdk.py, libAoqClientSdk.so e libonnxruntime.so.1.16.3 no mesmo diretório.
- Adicione o diretório do SDK aos caminhos de busca do Python e das bibliotecas dinâmicas:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
- Use import aoq_client_sdk em Python. Você também pode especificar o caminho absoluto de libAoqClientSdk.so por meio da variável AOQ_CLIENT_SDK_LIB.
Fluxo de implementação
- O servidor da aplicação obtém os parâmetros de conexão AOQ para o qwen3.5-omni-plus-realtime pela URL de token do Realtime.
- O cliente cria o engine e configura os codecs de áudio e as faixas. Se for necessária compreensão visual contínua, configure também a faixa de vídeo.
- O cliente inicia a captura e a reprodução locais, desativa o envio da faixa de áudio por padrão, conecta-se ao AOQ e envia session.update.
- Após receber session.updated, a opção de vídeo contínuo ativa a faixa de vídeo. A faixa de áudio permanece desativada até que o usuário pressione o botão de fala.
- Ao pressionar o botão, o cliente ativa a faixa de áudio. Ao soltá-lo, desativa a faixa, envia opcionalmente uma imagem e, em seguida, transmite input_audio_buffer.commit e response.create.
- Depois de receber response.done, outro turno pode ser iniciado. Para encerrar, pare os dispositivos, desconecte-se e destrua o engine.
Faixa de vídeo contínua
Publique a faixa de vídeo e ative o envio de vídeo após session.updated. O modelo visualiza continuamente os frames mais recentes. Cada turno de voz requer apenas a confirmação do áudio e a solicitação de resposta.

Envio de imagem pela faixa de dados
Não publique a faixa de vídeo. Quando uma imagem for necessária, envie input_image_buffer.append após soltar o botão e, em seguida, confirme o áudio e solicite a resposta.

Obtenha um token do servidor da aplicação
Defina DASHSCOPE_API_KEY no servidor da aplicação e envie a requisição para o endpoint da região selecionada. O campo clientIp representa o endereço IP público real do cliente. Embora opcional, especificá-lo auxilia o service na alocação de um endpoint de retransmissão adequado.
curl -X POST \
"https://{endpoint}/api/v1/webrtc/realtime?model=qwen3.5-omni-plus-realtime" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
-H "x-dashscope-rtc-transport: moq" \
-d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
ObservaçãoCaso o servidor da aplicação não consiga obter o endereço IP público real do cliente, omita clientIp em vez de passar uma string vazia.
O servidor da aplicação retorna os seguintes campos de resposta ao cliente. Jamais retorne a chave de API para um cliente em ambiente de produção. Para todos os campos de requisição e resposta, consulte Token authentication.
Campo de resposta | Campo do SDK |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
Implemente o cliente iOS
Após o cliente obter AoqConnectConfig do servidor da aplicação, siga estas etapas para implementar conversas de voz push-to-talk no iOS.
1. Crie o engine e registre callbacks
Crie a instância singleton do engine AOQ e registre o objeto da aplicação como receptor dos callbacks. Trate estados de conexão, eventos do servidor, erros e avisos nesses callbacks.
let createConfig = AoqCreateConfig()
createConfig.workDir = workDir
engine = AoqClientEngine.createEngine(createConfig, delegate: self)
2. Inicie os dispositivos de áudio e vídeo
Inicialize a captura e a reprodução de áudio. Inicie a câmera apenas se a opção de vídeo contínuo for utilizada. Obtenha as permissões de microfone e câmera antes de chamar estes métodos.
let captureConfig = AoqAudioCaptureConfig()
captureConfig.channel = 1
captureConfig.isExternal = false
engine.startAudioCapture(captureConfig)
let playbackConfig = AoqAudioPlaybackConfig()
playbackConfig.channel = 1
playbackConfig.isExternal = false
playbackConfig.isDefaultSpeaker = true
engine.startAudioPlayer(playbackConfig)
3. Configure codecs e faixas
Configure os codecs de áudio compatíveis com o modelo selecionado e o formato de áudio da aplicação, além de escolher as faixas adequadas à opção de entrada de imagem. Os valores de áudio e vídeo abaixo são exemplos; ajuste-os conforme os requisitos do modelo e o cenário da aplicação. Desative o envio da faixa de áudio antes de estabelecer a conexão.
Faixa de vídeo contínua
Configure as faixas de publicação de áudio, vídeo e dados. Ajuste as configurações de codificação de vídeo para atender à qualidade de imagem desejada e à largura de banda disponível.
let audioEncoderConfig = AoqAudioCodecConfig()
audioEncoderConfig.trackType = .audio
audioEncoderConfig.codecType = .audioPCM
audioEncoderConfig.sampleRate = 16_000
audioEncoderConfig.channel = 1
engine.setAudioEncoderConfig(audioEncoderConfig)
let audioDecoderConfig = AoqAudioCodecConfig()
audioDecoderConfig.trackType = .audio
audioDecoderConfig.codecType = .audioPCM
audioDecoderConfig.sampleRate = 24_000
audioDecoderConfig.channel = 1
engine.setAudioDecoderConfig(audioDecoderConfig)
let videoEncoderConfig = AoqVideoCodecConfig()
videoEncoderConfig.trackType = .video
videoEncoderConfig.codecType = .videoJpeg
videoEncoderConfig.width = 960
videoEncoderConfig.height = 540
videoEncoderConfig.fps = 2
videoEncoderConfig.bitrate = 500_000
engine.setVideoEncoderConfig(videoEncoderConfig)
let publishAudioTrack = AoqTrackParam()
publishAudioTrack.trackType = .audio
let publishVideoTrack = AoqTrackParam()
publishVideoTrack.trackType = .video
let publishDataTrack = AoqTrackParam()
publishDataTrack.trackType = .data
let subscribeAudioTrack = AoqTrackParam()
subscribeAudioTrack.trackType = .audio
let subscribeDataTrack = AoqTrackParam()
subscribeDataTrack.trackType = .data
connectConfig.publishTracks = [publishAudioTrack, publishVideoTrack, publishDataTrack]
connectConfig.subscribeTracks = [subscribeAudioTrack, subscribeDataTrack]
Envio de imagem pela faixa de dados
Configure apenas as faixas de áudio e dados. Não configure um codificador de vídeo para evitar captura, codificação e transmissão contínuas de vídeo. Os valores de áudio são meramente exemplificativos.
let audioEncoderConfig = AoqAudioCodecConfig()
audioEncoderConfig.trackType = .audio
audioEncoderConfig.codecType = .audioPCM
audioEncoderConfig.sampleRate = 16_000
audioEncoderConfig.channel = 1
engine.setAudioEncoderConfig(audioEncoderConfig)
let audioDecoderConfig = AoqAudioCodecConfig()
audioDecoderConfig.trackType = .audio
audioDecoderConfig.codecType = .audioPCM
audioDecoderConfig.sampleRate = 24_000
audioDecoderConfig.channel = 1
engine.setAudioDecoderConfig(audioDecoderConfig)
let publishAudioTrack = AoqTrackParam()
publishAudioTrack.trackType = .audio
let publishDataTrack = AoqTrackParam()
publishDataTrack.trackType = .data
let subscribeAudioTrack = AoqTrackParam()
subscribeAudioTrack.trackType = .audio
let subscribeDataTrack = AoqTrackParam()
subscribeDataTrack.trackType = .data
connectConfig.publishTracks = [publishAudioTrack, publishDataTrack]
connectConfig.subscribeTracks = [subscribeAudioTrack, subscribeDataTrack]
4. Configure uma sessão Manual
Após estabelecer a conexão, chame sendDataMsg para enviar um evento session.update. Defina turn_detection como null e selecione a voz, as instruções e as modalidades de saída adequadas à sua aplicação. Mantenha os parâmetros de áudio do exemplo consistentes com as configurações de codec do SDK. Para todos os campos, consulte Client events.
private func sendSessionUpdate() {
let event: [String: Any] = [
"type": "session.update",
"session": [
"modalities": ["text", "audio"],
"voice": "Ethan",
"audio": [
"input": ["format": ["type": "pcm", "sample_rate": 16_000]],
"output": ["format": ["type": "pcm", "sample_rate": 24_000]]
],
"turn_detection": NSNull()
]
]
guard let data = try? JSONSerialization.data(withJSONObject: event) else { return }
let dataMessage = AoqDataMsg()
dataMessage.data = data
engine.sendDataMsg(dataMessage)
}
5. Aguarde a configuração da sessão
Trate o evento session.updated no callback onDataMsg. Não envie mídia até receber esse evento. Na opção de vídeo contínuo, chame enableSendMediaStream para ativar a faixa de vídeo neste momento, mas mantenha a faixa de áudio desativada para evitar que sons anteriores ao pressionamento do botão entrem no buffer de entrada.
func onDataMsg(_ msg: AoqDataMsg) {
guard let event = try? JSONSerialization.jsonObject(with: msg.data) as? [String: Any],
let type = event["type"] as? String else { return }
if type == "session.updated", imageMode == .continuousVideo {
engine.enableSendMediaStream(.video, enable: true)
}
// Keep Audio-track sending disabled until the talk button is pressed.
}
6. Implemente a interação push-to-talk
Ao pressionar o botão, chame enableSendMediaStream para ativar a faixa de áudio. Ao soltá-lo, chame enableSendMediaStream para desativar a faixa, garanta que o turno contenha áudio, envie opcionalmente uma imagem e chame sendDataMsg para transmitir input_audio_buffer.commit seguido de response.create.
func onPushToTalkPressed() {
hasAudioInCurrentTurn = true
engine.enableSendMediaStream(.audio, enable: true)
}
func onPushToTalkReleased(base64Jpeg: String? = nil) {
engine.enableSendMediaStream(.audio, enable: false)
guard hasAudioInCurrentTurn else { return }
if imageMode == .singleImage, let base64Jpeg {
let imageEvent: [String: Any] = [
"type": "input_image_buffer.append",
"image": base64Jpeg
]
if let data = try? JSONSerialization.data(withJSONObject: imageEvent) {
let dataMessage = AoqDataMsg()
dataMessage.data = data
engine.sendDataMsg(dataMessage)
}
}
for event in [
["type": "input_audio_buffer.commit"],
["type": "response.create"]
] {
guard let data = try? JSONSerialization.data(withJSONObject: event) else { continue }
let dataMessage = AoqDataMsg()
dataMessage.data = data
engine.sendDataMsg(dataMessage)
}
hasAudioInCurrentTurn = false
}
7. Escolha uma opção de entrada de imagem
A compreensão visual contínua e as perguntas pontuais por imagem usam configurações de faixa e comportamentos de envio distintos. Selecione uma opção considerando largura de banda, consumo de energia e design de interação.
Faixa de vídeo contínua
Indicado para chamadas de vídeo, cenários com mudanças rápidas ou contexto visual contínuo. Após publicar a faixa de vídeo, não envie input_image_buffer.append.
Envio de imagem única pela faixa de dados
Recomendado para perguntas com imagem ou envios esporádicos. Converta a imagem para JPG ou JPEG, codifique-a em Base64 e envie-a após soltar o botão e antes de confirmar o áudio:
{
"type": "input_image_buffer.append",
"image": "<Base64-encoded JPEG data>"
}
- Prefira resoluções 480p ou 720p para melhor desempenho. Não ultrapasse 1080p.
- A imagem codificada em Base64 não pode exceder 256 KB. Mantenha a imagem original em até 190 KB para reservar espaço ao wrapper JSON.
- A faixa de áudio deve ter enviado pelo menos um frame de áudio no turno antes do envio da imagem. O evento subsequente input_audio_buffer.commit confirma tanto os buffers de áudio quanto de imagem.
8. Desconecte e destrua o engine
Ao finalizar a sessão, desconecte-se e destrua o engine. As operações disconnect ou destroy fecham automaticamente os dispositivos de mídia, eliminando a necessidade de chamar métodos stop separadamente. O AoqClientEngine é um singleton e só pode ser criado novamente após a chamada de destroy.
engine.disconnect()
AoqClientEngine.destroy()
Exemplo completo
A classe a seguir aceita um AoqConnectConfig mapeado a partir da resposta de token do servidor da aplicação. Em produção, adicione gerenciamento de estado da UI, tratamento de permissões, recuperação de erros e compressão de imagens.
import Foundation
import AoqClientSdk
final class ManualPushToTalkClient: NSObject, AoqEngineDelegate {
enum ImageMode: Equatable {
case none
case continuousVideo
case singleImage
}
private var engine: AoqClientEngine!
private let imageMode: ImageMode
private var hasAudioInCurrentTurn = false
init(workDir: String, connectConfig: AoqConnectConfig, imageMode: ImageMode) {
self.imageMode = imageMode
super.init()
let createConfig = AoqCreateConfig()
createConfig.workDir = workDir
self.engine = AoqClientEngine.createEngine(createConfig, delegate: self)
// Example values. Match these settings to the selected model and application format.
let audioEncoderConfig = AoqAudioCodecConfig()
audioEncoderConfig.trackType = .audio
audioEncoderConfig.codecType = .audioPCM
audioEncoderConfig.sampleRate = 16_000
audioEncoderConfig.channel = 1
engine.setAudioEncoderConfig(audioEncoderConfig)
let audioDecoderConfig = AoqAudioCodecConfig()
audioDecoderConfig.trackType = .audio
audioDecoderConfig.codecType = .audioPCM
audioDecoderConfig.sampleRate = 24_000
audioDecoderConfig.channel = 1
engine.setAudioDecoderConfig(audioDecoderConfig)
let publishAudioTrack = AoqTrackParam()
publishAudioTrack.trackType = .audio
let publishDataTrack = AoqTrackParam()
publishDataTrack.trackType = .data
let subscribeAudioTrack = AoqTrackParam()
subscribeAudioTrack.trackType = .audio
let subscribeDataTrack = AoqTrackParam()
subscribeDataTrack.trackType = .data
connectConfig.publishTracks = [publishAudioTrack, publishDataTrack]
connectConfig.subscribeTracks = [subscribeAudioTrack, subscribeDataTrack]
if imageMode == .continuousVideo {
let videoEncoderConfig = AoqVideoCodecConfig()
videoEncoderConfig.trackType = .video
videoEncoderConfig.codecType = .videoJpeg
videoEncoderConfig.width = 960
videoEncoderConfig.height = 540
videoEncoderConfig.fps = 2
videoEncoderConfig.bitrate = 500_000
engine.setVideoEncoderConfig(videoEncoderConfig)
let publishVideoTrack = AoqTrackParam()
publishVideoTrack.trackType = .video
connectConfig.publishTracks = [publishAudioTrack, publishVideoTrack, publishDataTrack]
}
let captureConfig = AoqAudioCaptureConfig()
captureConfig.channel = 1
captureConfig.isExternal = false
engine.startAudioCapture(captureConfig)
let playbackConfig = AoqAudioPlaybackConfig()
playbackConfig.channel = 1
playbackConfig.isExternal = false
playbackConfig.isDefaultSpeaker = true
engine.startAudioPlayer(playbackConfig)
if imageMode == .continuousVideo {
let videoCaptureConfig = AoqVideoCaptureConfig()
videoCaptureConfig.width = 1280
videoCaptureConfig.height = 720
videoCaptureConfig.fps = 15
engine.startVideoCapture(videoCaptureConfig)
}
engine.enableSendMediaStream(.audio, enable: false)
if imageMode == .continuousVideo {
engine.enableSendMediaStream(.video, enable: false)
}
engine.connect(connectConfig)
}
func onPushToTalkPressed() {
hasAudioInCurrentTurn = true
engine.enableSendMediaStream(.audio, enable: true)
}
func onPushToTalkReleased(base64Jpeg: String? = nil) {
engine.enableSendMediaStream(.audio, enable: false)
guard hasAudioInCurrentTurn else { return }
if imageMode == .singleImage, let base64Jpeg {
let imageEvent: [String: Any] = [
"type": "input_image_buffer.append",
"image": base64Jpeg
]
if let data = try? JSONSerialization.data(withJSONObject: imageEvent) {
let dataMessage = AoqDataMsg()
dataMessage.data = data
engine.sendDataMsg(dataMessage)
}
}
for event in [
["type": "input_audio_buffer.commit"],
["type": "response.create"]
] {
guard let data = try? JSONSerialization.data(withJSONObject: event) else { continue }
let dataMessage = AoqDataMsg()
dataMessage.data = data
engine.sendDataMsg(dataMessage)
}
hasAudioInCurrentTurn = false
}
private func sendSessionUpdate() {
let event: [String: Any] = [
"type": "session.update",
"session": [
"modalities": ["text", "audio"],
"voice": "Ethan",
"audio": [
"input": ["format": ["type": "pcm", "sample_rate": 16_000]],
"output": ["format": ["type": "pcm", "sample_rate": 24_000]]
],
"turn_detection": NSNull()
]
]
guard let data = try? JSONSerialization.data(withJSONObject: event) else { return }
let dataMessage = AoqDataMsg()
dataMessage.data = data
engine.sendDataMsg(dataMessage)
}
func close() {
engine.disconnect()
AoqClientEngine.destroy()
}
func onConnectionStatusChange(_ status: AoqConnectionStatus) {
if status == .connected { sendSessionUpdate() }
}
func onDataMsg(_ msg: AoqDataMsg) {
guard let event = try? JSONSerialization.jsonObject(with: msg.data) as? [String: Any],
let type = event["type"] as? String else { return }
if type == "session.updated", imageMode == .continuousVideo {
engine.enableSendMediaStream(.video, enable: true)
}
}
func onError(_ code: Int, message: String) {}
func onWarning(_ code: Int, message: String) {}
func onStats(_ stats: AoqStats) {}
func onAudioDeviceStateChanged(_ state: AoqAudioDeviceState) {}
func onAudioDeviceRouteChanged(_ routeType: Int) {}
func onAudioDeviceInterrupted(_ interrupt: Bool) {}
func onAudioFileState(_ state: AoqAudioFileState) {}
func onVideoDeviceStateChanged(_ state: AoqVideoDeviceState) {}
}
Execute e verifique
Realize um turno push-to-talk apenas com áudio e outro com imagem. Resultados esperados:
- A faixa de áudio permanece desativada antes do pressionamento do botão e transmite áudio continuamente enquanto ele estiver pressionado.
- Após soltar o botão, os eventos input_audio_buffer.committed, response.created e response.done são recebidos em sequência, e o áudio do modelo é reproduzido na faixa de áudio inscrita.
- Na opção de imagem única, o modelo responde usando a imagem e o áudio do turno. No modo de vídeo contínuo, ele usa os frames de vídeo mais recentes.
Para detalhes sobre campos de eventos do servidor e esquemas completos de resposta, consulte Server events.
Considerações importantes
- O AOQ transporta áudio pela faixa de áudio. Não envie adicionalmente input_audio_buffer.append.
- O evento input_audio_buffer.commit apenas confirma o turno, sem acionar uma resposta do modelo. Envie response.create logo em seguida.
- Evite confirmar um buffer de áudio vazio, pois o service retornará um erro.
- Não ative o envio de mídia antes de receber session.updated. No modo Manual, não ative a faixa de áudio antes que o usuário pressione o botão.
Informações relacionadas
Para consultar todos os parâmetros, campos de eventos e interfaces para outras plataformas, veja: