Use AOQ to connect to qwen3.5-omni-plus-realtime and let the client control turn boundaries for push-to-talk conversations and optional image questions. The client code uses iOS Swift.
Solution overview
Qwen-Omni-Realtime supports server-side VAD and client-controlled Manual mode. This tutorial sets session.turn_detection to null. The client sends audio while the user holds a button, and commits the audio and explicitly requests a response when the user releases the button.
Manual mode is suitable for hardware intercom buttons, press-and-hold controls, noisy environments in which the application determines turn boundaries, and turns that optionally include an image. Audio is transported over the AOQ Audio track. Do not send input_audio_buffer.append.
| Item | VAD mode | Manual mode |
|---|---|---|
| Turn boundary | Detected by server_vad or semantic_vad | Controlled by a button or application state |
| Session setting | turn_detection contains VAD settings | turn_detection is null |
| Audio commit | Performed automatically by the service | The client sends input_audio_buffer.commit |
| Response trigger | Triggered automatically by the service | The client sends response.create |
| Image input | Continuous Video track or an image over the Data track | Continuous Video track or an image over the Data track |
Prerequisites
- Activate QwenCloud and obtain an API Key following Get and configure an API Key. Store the API key only on your application server. Do not include it in client code or commit it to a code repository.
- Download the latest AOQ Client SDK as described in SDK download.
- Build an application server and implement proxy authentication as described in Token authentication. Before each new connection, the client must obtain new connection credentials from the application server.
Import the SDK
Import the SDK for your development platform. The client implementation uses iOS Swift. Other platforms provide the same interfaces and event flow. This tutorial uses PCM audio streams. Opus encoding is provided by a plugin. Import the Opus plugin if the uplink uses Opus.
- Android
- iOS
- HarmonyOS
- Linux (Python)
- Place AoqClientSdk-release.aar in app/libs, and configure the dependency and SDK-supported ABIs in app/build.gradle:
- Declare the following permissions in AndroidManifest.xml:
- Request the RECORD_AUDIO and CAMERA permissions at runtime before the corresponding devices are used.
Implementation flow
- The application server obtains AOQ connection parameters for qwen3.5-omni-plus-realtime from the Realtime token URL.
- The client creates the engine and configures audio codecs and tracks. It also configures the Video track if continuous visual understanding is required.
- The client starts local capture and playback, disables Audio-track sending by default, connects to AOQ, and sends session.update.
- After session.updated is received, the continuous-video option enables the Video track. The Audio track remains disabled until the user presses the talk button.
- When the user presses the button, the client enables the Audio track. On release, it disables the Audio track, optionally sends an image, and then sends input_audio_buffer.commit and response.create.
- After response.done is received, another turn can start. To finish, stop the devices, disconnect, and destroy the engine.
- Continuous Video track
- Send an image over the Data track
Publish the Video track and enable video sending after session.updated. The model continuously sees the latest frames. Each voice turn only needs to commit audio and request a response.

Obtain a token from the application server
Set DASHSCOPE_API_KEY on the application server and send the request to the endpoint. clientIp is the actual public IP address of the client. This field is optional, but specifying it helps the service allocate an appropriate relay endpoint.
If the application server cannot obtain the actual public IP address of the client, omit
clientIp instead of passing an empty string.| Response field | SDK field |
|---|---|
| aoqTokenForClient | AoqConnectConfig.token |
| sid | AoqConnectConfig.sid |
| clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
| clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
Implement the iOS client
After the client obtains AoqConnectConfig from the application server, follow these steps to implement push-to-talk voice conversations on iOS.
1. Create the engine and register callbacks
Create the singleton AOQ engine and register the application object as the callback receiver. Handle connection states, server events, errors, and warnings in the callbacks.
2. Start audio and video devices
Initialize audio capture and playback. Start the camera only for the continuous-video option. Obtain microphone and camera permissions before these methods are called.
3. Configure codecs and tracks
Configure the audio codecs for the selected model and the application's audio format, and select tracks for the image-input option. The following audio and video values are examples. Adjust them for the model requirements and application scenario. Disable Audio-track sending before the connection is established.
- Continuous Video track
- Send an image over the Data track
Configure the Audio, Video, and Data publish tracks. Adjust the video encoding settings for the required image quality and available bandwidth.
4. Configure a Manual session
After the connection is established, call sendDataMsg to send a session.update event. Set turn_detection to null and select the voice, instructions, and output modalities for your application. Keep the example audio parameters consistent with the SDK codec settings. For all fields, see Client events.
5. Wait for the session configuration
Handle the session.updated event in the onDataMsg callback. Do not send media until this event is received. For the continuous-video option, call enableSendMediaStream to enable the Video track at this point, but keep the Audio track disabled so that audio before the user presses the button does not enter the input buffer.
6. Implement push-to-talk interaction
When the button is pressed, call enableSendMediaStream to enable the Audio track. On release, call enableSendMediaStream to disable the Audio track, make sure that the turn contains audio, optionally send an image, and call sendDataMsg to send input_audio_buffer.commit followed by response.create.
7. Select an image-input option
Continuous visual understanding and occasional image questions use different track configurations and send behavior. Select an option based on bandwidth, power consumption, and interaction design.
- Continuous Video track
- Send a single image over the Data track
Use this option for video calls, rapidly changing scenes, or continuous visual context. After the Video track is published, do not send input_image_buffer.append.
8. Disconnect and destroy the engine
When the session ends, disconnect and destroy the engine. disconnect or destroy automatically closes media devices, so you do not need to call stop methods separately. AoqClientEngine is a singleton and cannot be created again until destroy is called.
Complete example
The following class accepts an AoqConnectConfig that was mapped from the application-server token response. Add UI state, permissions, error recovery, and image compression in production.
Run and verify
Complete one audio-only push-to-talk turn and one turn with an image. Expected results:
- The Audio track is disabled before the button is pressed and sends audio continuously while the button is held.
- After release, input_audio_buffer.committed, response.created, and response.done are received in sequence, and model audio is played over the subscribed Audio track.
- With the single-image option, the model responds using the image and audio from the turn. With continuous video, it uses the latest video frames.
Important considerations
- AOQ transports audio over the Audio track. Do not also send input_audio_buffer.append.
- input_audio_buffer.commit only commits the turn and does not trigger a model response. Send response.create afterward.
- Do not commit an empty audio buffer. The service returns an error.
- Do not enable media sending before session.updated. In Manual mode, do not enable the Audio track before the user presses the button.
