Skip to main content
Best Practices

Realtime call with qwen3.5-omni-plus-realtime via AOQ

Use qwen3.5-omni-plus-realtime model via AOQ protocol for real-time calls

Describes how to integrate the AOQ Client SDK on Android, iOS, and HarmonyOS to implement audio/video calls using AOQ and qwen3.5-omni-plus-realtime.

Get the SDK

For the AOQ Client SDK and the Opus audio plug-in, see SDK download. The Opus codec is provided as a separate plug-in. Include it as needed for your use case.

Import the SDK

Copy the core SDK files to your project's dependency directory and declare the required permissions in your project configuration.

Android

Place AoqClientSdk-release.aar in your project's app/libs/ directory. Place libPluginOpus.so by ABI into app/libs/armeabi-v7a/ and app/libs/arm64-v8a/. In 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 permissions in 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 and CAMERA are runtime permissions. Your app must call Android's ActivityCompat.requestPermissions() at runtime to request user authorization.

iOS (framework)

  1. Drag AoqClientSdk.framework and PluginOpus.framework into your Xcode project. Under Target > General > Frameworks, Libraries, and Embedded Content, select Embed & Sign.
  2. Declare permissions: in Xcode, select your Target > Info > Custom iOS Target Properties, and add the following two permission usage descriptions:
    KeyValue
    NSMicrophoneUsageDescriptionFor real-time voice calls
    NSCameraUsageDescriptionFor real-time video calls
  3. Swift project: import AoqClientSdk. Objective-C project: #import <AoqClientSdk/AoqClientSdk.h>.

HarmonyOS (har)

  1. Place aoq-client-sdk.har in your project's libs/ directory. Place libPluginOpus.so by ABI into entry/libs/armeabi-v7a/ and entry/libs/arm64-v8a/. Declare the dependency in entry/oh-package.json5.
  2. Add permissions in 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. In EntryAbility, trigger runtime authorization via abilityAccessCtrl.createAtManager().requestPermissionsFromUser.

Get a token from the AppServer

Follow the AOQ section in Token authentication to set up an AppServer that obtains tokens. Before each call, the client must request a token from your AppServer.

Implement AI audio/video calls

111

Create engine and set callbacks

Call the createEngine API to create an AoqClientEngine instance.
  • iOS
  • Android
  • HarmonyOS
let config = AoqCreateConfig()
config.workDir = workDir
engine = AoqClientEngine.createEngine(config, delegate: self)
Implement the AoqEngineDelegate protocol to listen for onConnectionStatusChange, onDataMsg, onError, and other callbacks.

Start audio/video capture and playback

Call startAudioCapture and startAudioPlayer to start local audio capture and playback. Call startVideoCapture to start the camera, and use setLocalView to bind the SDK's render target to your preview control.
  • iOS
  • Android
  • HarmonyOS
// Audio capture
let capCfg = AoqAudioCaptureConfig()
capCfg.channel = 1; capCfg.isExternal = false
engine.startAudioCapture(capCfg)
// Audio playback
let playCfg = AoqAudioPlaybackConfig()
playCfg.channel = 1; playCfg.isExternal = false
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)

Get connection credentials

Have your AppServer proxy the request to QwenCloud. See Token authentication.

Configure codecs and establish connection

Set codec parameters and call connect. Note: qwen3.5-omni-plus-realtime requires the client to start sending media data only after receiving session.updated from the server. To avoid accidentally sending media during the window between a successful connect and the arrival of session.updated, call enableSendMediaStream(trackType, false) for each upstream track before calling connect. For WebSocket event details, see Client events.
  • iOS
  • Android
  • HarmonyOS
// Audio codec configuration
let encCfg = AoqAudioCodecConfig()
encCfg.codecType = .audioPCM; 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)
The AOQ SDK starts sending media data by default after connecting. This example demonstrates how to disable media sending when connecting to a model.

Configure AI session

In the onConnectionStatusChange(Connected) callback, send a session.update message via sendDataMsg. This message contains session parameters such as modalities, voice, instructions, and turn_detection, completing the session handshake. For WebSocket event details, see Client events.
  • iOS
  • Android
  • HarmonyOS
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": "Ethan",
        // 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 for qwen3.5-omni-realtime.
            "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)
}

Enable media sending after receiving session.updated

In the onDataMsg callback, parse incoming messages. When session.updated is received from the model, call enableSendMediaStream(trackType, true) for each track type that was disabled earlier. For WebSocket event details, see Server events.
  • iOS
  • Android
  • HarmonyOS
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)
  }
}
  1. Start sending media streams only after receiving session.updated. The AI may not be ready to receive data before this event.
  2. The audio and video tracks added during connection (AOQ media channels) automatically deliver data to the server.
    • Audio: transmitted directly through the audio track -- no need to send input_audio_buffer.append events.
    • Video: frames are sent through the video track -- no need to send input_image_buffer.append events.

Disconnect and destroy engine

engine.disconnect()
AoqClientEngine.destroy()

Common scenarios

Barge-in

  • The SDK is deeply integrated with QwenCloud. Barge-in messages from the model interrupt the previous turn when a new turn begins.
  • The SDK provides the interruptAudioPlayer interface for local playback interruption. Call this API when the user wants to stop playback.
// iOS
engine.interruptAudioPlayer(.audio, fadeMs: 100)

Mute / unmute

After muting, the SDK continues to capture audio but sends only silent frames. The session is not interrupted.
engine.muteAudioCapture(true);   // Mute microphone (capture continues, but only silent frames are sent)
engine.muteAudioCapture(false);  // Unmute

Switch between front and rear cameras

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

Call subtitles and ASR results

The server pushes ASR results and AI text responses through downstream data messages. In your app, route them by the type field in the onDataMsg callback. For WebSocket event details, see Server events.

Important notes

  1. Singleton semantics: createEngine is a singleton -- repeated calls return the same instance. The engine can only be recreated after destroy is called. For multi-page use, manage the engine lifecycle at the Application or Ability level.
  2. Local preview view types:
    • Android: SurfaceView or TextureView. Other types are not supported.
    • iOS: Any UIView subclass.
    • HarmonyOS: See the SDK documentation.
  3. Audio route changes: Events such as headphone insertion/removal or Bluetooth connection trigger onAudioDeviceRouteChanged. No handling is usually required in your app. If your UI includes a speaker/earpiece toggle, sync the state based on this callback.
  4. Background audio continuation: For audio to continue when the call moves to the background, Info.plist must have UIBackgroundModes = audio enabled and AVAudioSession must be correctly activated in the foreground. The SDK handles most cases; use setAudioSessionRestriction: for fine-grained control.

Download the demo

image
Download sample source code: iOS: aoqdemo.zip