Skip to main content

Uso de AOQ con qwen3.8-omni-flash-realtime para llamadas en tiempo real

Describe cómo integrar el SDK de cliente de AOQ en Android, iOS y HarmonyOS para implementar llamadas de audio y video mediante AOQ y qwen3.8-omni-flash-realtime.

Obtener el SDK

Para el AOQ Client SDK y el complemento de audio Opus, consulte Descarga del SDK. Este ejemplo utiliza Opus y requiere el complemento Opus.

Importar el SDK

Copie los archivos principales del SDK al directorio de dependencias de su proyecto y declare los permisos necesarios en la configuración de su proyecto.

Android

Coloque AoqClientSdk-release.aar en el directorio app/libs/ de su proyecto. Coloque libPluginOpus.so por ABI en app/libs/armeabi-v7a/ y app/libs/arm64-v8a/. En app/build.gradle:
android {
    defaultConfig {
        minSdk 21
        ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
    }
    sourceSets { main { jniLibs.srcDirs = ['libs'] } }
    packagingOptions {
        // Avoid conflicts with same-named .so files in the host project
        pickFirsts += ['lib/*/*.so']
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.aar'])
}
Declare permisos en 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" />
RECORD_AUDIO y CAMERA son permisos de tiempo de ejecución. Su aplicación debe llamar a ActivityCompat.requestPermissions() de Android en tiempo de ejecución para solicitar la autorización del usuario.

iOS (framework)

  1. Arrastre AoqClientSdk.framework y PluginOpus.framework a su proyecto de Xcode. En Target > General > Frameworks, Libraries, and Embedded Content, seleccione Embed & Sign.
  2. Declarar permisos: en Xcode, seleccione su Target > Info > Custom iOS Target Properties y agregue las siguientes dos descripciones de uso de permisos:

    Clave

    Valor

    NSMicrophoneUsageDescription

    Para llamadas de voz en tiempo real

    NSCameraUsageDescription

    Para videollamadas en tiempo real

  3. Proyecto Swift: import AoqClientSdk. Proyecto Objective-C: #import <AoqClientSdk/AoqClientSdk.h>.

HarmonyOS (har)

  1. Coloque aoq-client-sdk.har en el directorio libs/ de su proyecto. Coloque libPluginOpus.so por ABI en entry/libs/armeabi-v7a/ y entry/libs/arm64-v8a/. Declare la dependencia en entry/oh-package.json5.
  2. Agregue permisos en 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" } }
]
  1. En EntryAbility, active la autorización en tiempo de ejecución mediante abilityAccessCtrl.createAtManager().requestPermissionsFromUser.

Probar la demostración

Utilice la demostración de Android de Alibaba Cloud Model Studio para verificar rápidamente la conectividad AOQ. Descargue el APK y configure la clave de API y workspaceId para probar modelos seleccionados. Escanee el siguiente código QR para descargar e instalar la demostración de Android:
Código QR para descargar la demostración

Obtener un token del AppServer

Establezca {endpoint} en {WorkspaceId}.cn-beijing.maas.aliyuncs.com para China (Beijing) o {WorkspaceId}.ap-southeast-1.maas.aliyuncs.com para Singapur. Reemplace {WorkspaceId} con su ID de espacio de trabajo y utilice la clave de API correspondiente a la misma región y espacio de trabajo. Configure un AppServer según los Pasos de autenticación de AOQ para Qwen-Omni-Realtime. Antes de cada llamada, el cliente solicita credenciales de conexión nuevas a AppServer.

Implementar llamadas de audio/video con IA

111

Crear motor y configurar callbacks

Llame a la API createEngine para crear una instancia de AoqClientEngine. iOS:
let config = AoqCreateConfig()
config.workDir = workDir
engine = AoqClientEngine.createEngine(config, delegate: self)
Implemente el protocolo AoqEngineDelegate para escuchar los callbacks onConnectionStatusChange, onDataMsg, onError y otros. Android:
AoqCreateConfig config = new AoqCreateConfig();
config.workDir = appCtx.getFilesDir().getAbsolutePath();
engine = AoqClientEngine.createEngine(appCtx, config, this);
HarmonyOS:
const config: AoqCreateConfig = { workDir: context.filesDir, extras: '' };
engine = AoqClientEngine.createEngine(config, this, context);

Iniciar captura y reproducción de audio/video

Llame a startAudioCapture y startAudioPlayer para iniciar la captura y reproducción de audio local. Llame a startVideoCapture para iniciar la cámara y utilice setLocalView para vincular el destino de renderizado del SDK a su control de vista previa. En los siguientes ejemplos, el SDK reproduce el audio automáticamente. Para revisar el audio antes de reproducirlo, establezca isExternal en true en la configuración de reproducción que se pasa a startAudioPlayer para desactivar la reproducción automática del SDK y habilite el callback de tramas de audio de reproducción. La aplicación puede copiar y almacenar en búfer el audio PCM del callback y, tras revisarlo, decidir si lo reproduce o lo descarta. Consulte Reproducción de audio personalizada para obtener los detalles de configuración. iOS: Para llamadas con altavoz, establece isVoipMode en true tanto para la captura como para la reproducción antes de iniciarlas. Esto activa la cancelación de eco por hardware. Mantén el micrófono activo durante la reproducción para que la detección de actividad de voz (VAD) pueda detectar cuándo habla el usuario e interrumpir la respuesta.
// Audio capture
let capCfg = AoqAudioCaptureConfig()
capCfg.channel = 1; capCfg.isExternal = false
capCfg.isVoipMode = true
engine.startAudioCapture(capCfg)

// Audio playback
let playCfg = AoqAudioPlaybackConfig()
playCfg.channel = 1; playCfg.isExternal = false
playCfg.isVoipMode = true
playCfg.isDefaultSpeaker = true
engine.startAudioPlayer(playCfg)

// Video capture
let vidCfg = AoqVideoCaptureConfig()
vidCfg.width = 720; vidCfg.height = 1280; vidCfg.fps = 15
engine.startVideoCapture(vidCfg)

// Set render view for local video preview
let canvas = AoqVideoCanvas()
canvas.view = localPreview
canvas.renderMode = .crop
engine.setLocalView(.video, canvas: canvas)
Android:
// Audio capture
AoqAudioCaptureConfig capCfg = new AoqAudioCaptureConfig();
capCfg.channel = 1; capCfg.isExternal = false;
engine.startAudioCapture(capCfg);

// Audio playback
AoqAudioPlaybackConfig playCfg = new AoqAudioPlaybackConfig();
playCfg.channel = 1; playCfg.isExternal = false;
engine.startAudioPlayer(playCfg);

// Video capture
AoqVideoCaptureConfig vidCfg = new AoqVideoCaptureConfig();
vidCfg.width = 720; vidCfg.height = 1280; vidCfg.fps = 15;
engine.startVideoCapture(vidCfg);

// Set render view for local video preview
AoqVideoCanvas canvas = new AoqVideoCanvas();
canvas.view = localPreview;
canvas.renderMode = AoqRenderMode.AoqRenderModeCrop;
engine.setLocalView(AoqTrackType.AoqTrackTypeVideo, canvas);
HarmonyOS:
// Audio capture
const capCfg: AoqAudioCaptureConfig = { channel: 1, isExternal: false };
engine.startAudioCapture(capCfg);

// Audio playback
const playCfg: AoqAudioPlaybackConfig = { channel: 1, isExternal: false };
engine.startAudioPlayer(playCfg);

// Video capture
const vidCfg: AoqVideoCaptureConfig = { width: 720, height: 1280, fps: 15, isExternal: false };
engine.startVideoCapture(vidCfg);

// Set render view for local video preview
const canvas: AoqVideoCanvas = { view: localCtrl, renderMode: AoqRenderMode.AoqRenderModeCrop };
engine.setLocalView(AoqTrackType.AoqTrackTypeVideo, canvas);

Obtener credenciales de conexión

El cliente solicita las credenciales de conexión a AppServer.

Configurar códecs y establecer conexión

Configure los parámetros del códec y llame a connect. Nota: qwen3.8-omni-flash-realtime requiere que el cliente comience a enviar datos multimedia solo después de recibir session.updated del servidor. Para evitar el envío accidental de contenido multimedia durante el intervalo entre una llamada connect exitosa y la llegada de session.updated, invoque enableSendMediaStream(trackType, false) para cada pista ascendente antes de llamar a connect. Para obtener detalles sobre los eventos de WebSocket, consulte Eventos del cliente. iOS:
// Audio codec configuration
let encCfg = AoqAudioCodecConfig()
encCfg.codecType = .audioOpus; encCfg.sampleRate = 16000; encCfg.channel = 1
engine.setAudioEncoderConfig(encCfg)
engine.setAudioDecoderConfig(encCfg)

// Disable media sending before connect; enable after receiving session.updated
engine.enableSendMediaStream(.audio, enable: false)
engine.enableSendMediaStream(.video, enable: false)

// Establish connection
let conn = AoqConnectConfig()
conn.token = token; conn.sid = sid; conn.certFingerprint = cert
conn.relayEndpoints = endpoints; conn.workspaceIdHash = workspaceIdHash

let aTrack = AoqTrackParam(); aTrack.trackType = .audio
let vTrack = AoqTrackParam(); vTrack.trackType = .video
let dTrack = AoqTrackParam(); dTrack.trackType = .data
conn.publishTracks   = [aTrack, vTrack, dTrack]
conn.subscribeTracks = [aTrack, dTrack]
engine.connect(conn)
Android:
// Audio codec configuration
AoqAudioCodecConfig encCfg = new AoqAudioCodecConfig();
encCfg.codecType = AoqEncoderType.AoqEncoderTypeAudioOpus;
encCfg.sampleRate = 16000; encCfg.channel = 1;
engine.setAudioEncoderConfig(encCfg);
engine.setAudioDecoderConfig(encCfg);

// Disable media sending before connect; enable after receiving session.updated
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeAudio, false);
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeVideo, false);

// Establish connection
AoqConnectConfig conn = new AoqConnectConfig();
conn.token = token; conn.sid = sid; conn.certFingerprint = cert;
conn.relayEndpoints.addAll(endpoints); conn.workspaceIdHash = workspaceIdHash;

AoqTrackParam aTrack = new AoqTrackParam(); aTrack.trackType = AoqTrackType.AoqTrackTypeAudio;
AoqTrackParam vTrack = new AoqTrackParam(); vTrack.trackType = AoqTrackType.AoqTrackTypeVideo;
AoqTrackParam dTrack = new AoqTrackParam(); dTrack.trackType = AoqTrackType.AoqTrackTypeData;
conn.publishTracks.add(aTrack);
conn.publishTracks.add(vTrack);
conn.publishTracks.add(dTrack);
conn.subscribeTracks.add(aTrack);
conn.subscribeTracks.add(dTrack);
engine.connect(conn);
HarmonyOS:
// Audio codec configuration
const encCfg: AoqAudioCodecConfig = {
  codecType: AoqEncoderType.AoqEncoderTypeAudioOpus,
  sampleRate: 16000, channel: 1
};
engine.setAudioEncoderConfig(encCfg);
engine.setAudioDecoderConfig(encCfg);

// Disable media sending before connect; enable after receiving session.updated
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeAudio, false);
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeVideo, false);

// Establish connection
const conn: AoqConnectConfig = {
  token, sid, certFingerprint: cert,
  relayEndpoints: endpoints,
  workspaceIdHash,
  publishTracks: [
    { trackType: AoqTrackType.AoqTrackTypeAudio },
    { trackType: AoqTrackType.AoqTrackTypeVideo },
    { trackType: AoqTrackType.AoqTrackTypeData }
  ],
  subscribeTracks: [
    { trackType: AoqTrackType.AoqTrackTypeAudio },
    { trackType: AoqTrackType.AoqTrackTypeData }
  ]
};
engine.connect(conn);
Importante: El SDK de AOQ comienza a enviar datos multimedia de forma predeterminada después de conectarse. Este ejemplo demuestra cómo deshabilitar el envío de medios al conectarse a un modelo.

Configurar sesión de IA

En el callback onConnectionStatusChange(Connected), envíe un mensaje session.update mediante sendDataMsg. Este mensaje contiene parámetros de sesión como modalidades, voz, instrucciones y turn_detection, completando el protocolo de enlace de la sesión. Para obtener detalles sobre los eventos de WebSocket, consulte Eventos del cliente. iOS:
func onConnectionStatusChange(_ status: AoqConnectionStatus) {
    if status == .connected { sendSessionUpdate() }
}

private func sendSessionUpdate() {
    let json = """
    {
      // Event ID, generated by the client
      "event_id": "event_ToPZqeobitzUJnt3QqtWg",
      // Event type, always session.update
      "type": "session.update",
      // Session configuration
      "session": {
          // Output modalities: ["text"] (text only) or ["text","audio"] (text and audio)
          "modalities": [
              "text",
              "audio"
          ],
          // Output audio voice
          "voice": "Tina",
          // Input audio format; currently only "pcm" is supported. Input audio is 16 kHz PCM.
          "input_audio_format": "pcm",
          // Output audio format; currently only "pcm" is supported. Output audio is 24 kHz PCM.
          "output_audio_format": "pcm",
          // System message for setting the model's objective or role.
          "instructions": "You are an AI customer service agent for a five-star hotel. Accurately and helpfully answer customer inquiries about room types, facilities, pricing, and reservation policies. Always respond professionally and helpfully. Do not provide unverified information or information outside the scope of hotel services.",
          // Whether to enable voice activity detection. If enabled, pass a config object and the server will automatically detect voice start and end.
          // Set to null to let the client control when to trigger a model response.
          "turn_detection": {
              // VAD type: server_vad or semantic_vad. Recommended: semantic_vad.
              "type": "semantic_vad",
              // VAD detection threshold. Increase in noisy environments; decrease in quiet ones.
              "threshold": 0.5,
              // Duration of silence after which a model response is triggered
              "silence_duration_ms": 800
          }
      }
    }
    """
    let msg = AoqDataMsg()
    msg.data = json.data(using: .utf8)!
    engine.send(msg)
}
Android:
@Override
public void onConnectionStatusChange(AoqConnectionStatus status) {
    if (status == AoqConnectionStatus.AoqConnectionStatusConnected) {
        sendSessionUpdate();
    }
}

private void sendSessionUpdate() {
    String sessionUpdateJson = /* Same session.update JSON as in the Swift example above */;
    AoqDataMsg msg = new AoqDataMsg();
    msg.data = sessionUpdateJson.getBytes(StandardCharsets.UTF_8);
    engine.sendDataMsg(msg);
}
HarmonyOS:
onConnectionStatusChange(status: AoqConnectionStatus): void {
  if (status === AoqConnectionStatus.AoqConnectionStatusConnected) {
    this.sendSessionUpdate();
  }
}

private sendSessionUpdate(): void {
  const sessionUpdateJson = /* Same session.update JSON as in the Swift example above */;
  const msg: AoqDataMsg = { data: new TextEncoder().encode(sessionUpdateJson).buffer };
  this.engine.sendDataMsg(msg);
}

Habilitar el envío de medios tras recibir session.updated

En el callback onDataMsg, analice los mensajes entrantes. Cuando se reciba session.updated del modelo, llame a enableSendMediaStream(trackType, true) para cada tipo de pista que se haya deshabilitado anteriormente. Para obtener detalles sobre los eventos de WebSocket, consulte Eventos del servidor. iOS:
func onDataMsg(_ msg: AoqDataMsg) {
    guard let obj = try? JSONSerialization.jsonObject(with: msg.data) as? [String: Any],
          let type = obj["type"] as? String else { return }
    if type == "session.updated" {
        engine.enableSendMediaStream(.audio, enable: true)
        engine.enableSendMediaStream(.video, enable: true)
    }
}
Android:
@Override
public void onDataMsg(AoqDataMsg msg) {
    if (msg == null || msg.data == null) return;
    try {
        JSONObject obj = new JSONObject(new String(msg.data, StandardCharsets.UTF_8));
        if ("session.updated".equals(obj.optString("type"))) {
            engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeAudio, true);
            engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeVideo, true);
        }
    } catch (JSONException ignored) {}
}
HarmonyOS:
onDataMsg(msg: AoqDataMsg): void {
  if (!msg?.data) return;
  try {
    const text = new TextDecoder('utf-8').decode(new Uint8Array(msg.data));
    const obj = JSON.parse(text) as { type?: string };
    if (obj.type === 'session.updated') {
      this.engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeAudio, true);
      this.engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeVideo, true);
    }
  } catch (_) { /* Not JSON, ignore */ }
}
Importante
  1. Comience a enviar flujos multimedia solo después de recibir session.updated. Es posible que la IA no esté lista para recibir datos antes de este evento.
  2. Las pistas de audio y video agregadas durante la conexión (canales multimedia AOQ) entregan automáticamente datos al servidor.
    1. Audio: transmitido directamente a través de la pista de audio; no es necesario enviar eventos input_audio_buffer.append.
    2. Video: los fotogramas se envían a través de la pista de video; no es necesario enviar eventos input_image_buffer.append.

Desconectar y destruir motor

engine.disconnect()
AoqClientEngine.destroy()

Escenarios comunes

Interrupción

  • El SDK está profundamente integrado con Alibaba Cloud Model Studio. Los mensajes de interrupción del modelo interrumpen el turno anterior cuando comienza un nuevo turno.
  • El SDK proporciona la interfaz interruptAudioPlayer para la interrupción de la reproducción local. Llame a esta API cuando el usuario wants detener la reproducción.
// iOS
engine.interruptAudioPlayer(.audio, fadeMs: 100)

Silenciar / activar sonido

Después de silenciar, el SDK continúa capturando audio pero solo envía tramas silenciosas. La session no se interrumpe.
engine.muteAudioCapture(true);   // Mute microphone (capture continues, but only silent frames are sent)
engine.muteAudioCapture(false);  // Unmute

Cambiar entre cámaras frontal y trasera

// Pass the desired camera direction enum
engine.switchCamera(AoqCameraDirection.AoqCameraDirectionFront);
engine.switchCamera(AoqCameraDirection.AoqCameraDirectionBack);

Subtítulos de llamada y resultados de ASR

El servidor envía resultados de ASR y respuestas de texto de IA mediante mensajes de datos descendentes. En su aplicación, enrútelos según el campo type en el callback onDataMsg. Para obtener detalles sobre los eventos de WebSocket, consulte Eventos del servidor.

Notas importantes

  1. Semántica de singleton: createEngine es un singleton; las llamadas repetidas devuelven la misma instancia. El motor solo puede recrearse después de llamar a destroy. Para uso en múltiples páginas, gestione el ciclo de vida del motor a nivel de Application o Ability.
  2. Tipos de vista de previsualización local:
    • Android: SurfaceView o TextureView. No se admiten otros tipos.
    • iOS: Cualquier subclase de UIView.
    • HarmonyOS: Consulte la documentación del SDK.
  3. Cambios en la ruta de audio: Eventos como la inserción o extracción de auriculares o la conexión Bluetooth activan onAudioDeviceRouteChanged. Por lo general, no se requiere manejo en su aplicación. Si su interfaz incluye un alternador de altavoz/auricular, sincronice el estado basándose en este callback.
  4. Continuación de audio en segundo plano: Para que el audio continúe cuando la llamada pasa a segundo plano, Info.plist debe tener habilitado UIBackgroundModes = audio y AVAudioSession debe activarse correctamente en primer plano. El SDK maneja la mayoría de los casos; utilice setAudioSessionRestriction: para un control detallado.

Código fuente de la demostración para iOS

Interfaz de demostración de iOS
Para aprender a implementar AOQ en iOS, descargue el código fuente de ejemplo: aoqdemo.zip.

Documentos relacionados