Skip to main content
Realtime API

Real-time speech recognition with AOQ + fun-asr-realtime

Use AOQ to connect to fun-asr-realtime for streaming microphone audio and receiving real-time speech recognition results. Client code examples use Android Java; other AOQ-supported platforms share the same interface.

Overview

fun-asr-realtime transcribes an audio stream into punctuated text in real time. The AOQ SDK separates media and events into distinct tracks: the client sends audio on the Audio track and exchanges control/recognition events on the Data track. This model uses the Inference event protocol rather than the Realtime event protocol. This approach suits real-time captions, meeting transcription, voice input, and intelligent assistants. The Audio track avoids encoding audio into event messages, while the Data track preserves full task semantics such as run-task, result-generated, and finish-task.
  1. The client requests temporary AOQ connection credentials from the business AppServer.
  2. The AppServer uses an API Key to request a Token from QwenCloud, then returns the connection fields to the client.
  3. The client establishes an AOQ connection and sends run-task; after receiving task-started, it begins streaming microphone audio.
  4. The server continuously returns result-generated events; the client sends finish-task and waits for the final result and task-finished.

Prerequisites

  1. Activate QwenCloud and obtain an API Key following Get and configure an API Key. Keep the API Key only on the business AppServer -- never embed it in client code or commit it to a repository.
  2. Download the latest AOQ Client SDK as described in SDK download. This tutorial transports PCM audio and does not require the optional Opus plugin.
  3. Set up a business AppServer and implement AOQ Inference server-side proxy authentication as described in Token authentication. The client should obtain fresh connection credentials from the AppServer before each new connection.

Import the SDK

Choose the import method for your development platform. The following client implementation uses Android Java; other platforms share the same interface design and event flow.
  • Android
  • iOS
  • HarmonyOS
  • Linux (Python)
  1. Place AoqClientSdk-release.aar in the app/libs directory and configure the dependency and ABI filters in app/build.gradle:
android {
  defaultConfig {
    minSdk 21
    ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
  }
}
dependencies {
  implementation fileTree(dir: 'libs', include: ['*.aar'])
}
  1. Declare network and recording 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" />
  1. Request the RECORD_AUDIO permission at runtime before starting recording. Speech recognition alone does not require CAMERA permission.

Try the demo

QwenCloud provides an Android demo app to quickly verify AOQ connectivity. Download the APK and configure the API key and workspaceId to try selected models. Scan the following QR code to download the demo:
QR code for downloading the demo

Implementation flow

  1. The AppServer uses the Inference Token endpoint to obtain AOQ connection parameters for fun-asr-realtime.
  2. The client converts the Token response into an AoqConnectConfig, publishes Audio and Data tracks, and subscribes to the Data track.
  3. The client configures audio encoding parameters per business requirements and model specifications, starts microphone capture without sending audio, then establishes the AOQ connection.
  4. After connecting, the client sends run-task; upon receiving task-started, it enables Audio track sending.
  5. The client handles result-generated in onDataMsg; to end recording, it disables Audio track sending and then sends finish-task.
  6. After receiving task-finished, the client can start a new recognition round on the same connection with a new task_id, or disconnect and destroy the engine.
aoq-realtime-asr-sequence-zh

AppServer Token request

Set DASHSCOPE_API_KEY on the AppServer and send a request to the AOQ Inference Token endpoint. clientIp is the terminal's real public IP; this field is optional but recommended so the service can assign an appropriate Relay access point.
curl -X POST \
  "https://dashscope-intl.aliyuncs.com/api/v1/webrtc/inference?model=fun-asr-realtime" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
  -H "x-dashscope-rtc-transport: moq" \
  -d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
If the AppServer cannot determine the terminal's real public IP, remove the clientIp field from the request body entirely -- do not pass an empty string.
Return the following response fields from the AppServer to the client. Never return the API Key to the client in production. For full request parameters and response fields, see Token authentication.
Response fieldSDK field
aoqTokenForClientAoqConnectConfig.token
sidAoqConnectConfig.sid
clientRelayCertFingerprintAoqConnectConfig.certFingerprint
clientRelayEndpointsAoqConnectConfig.relayEndpoints

Implement the Android client

The following steps break down the Android Java client code in the order of connection and task execution. Each snippet comes from the complete example shown later.

1. Create the engine and set callbacks

Create the AOQ client engine and register connection status and Data track event callbacks. Implement callback handling per your business logic; start the recognition task only after a successful connection.
AoqClientListener listener = new AoqClientListener() {
  @Override
  public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
    connected = status == AoqClientEngine.AoqConnectionStatus
            .AoqConnectionStatusConnected;
    if (connected) {
      beginRecognition();
    }
  }
  @Override
  public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
    handleServerEvent(msg);
  }
};
AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
createConfig.workDir = context.getFilesDir().getAbsolutePath();
engine = AoqClientEngine.createEngine(context, createConfig, listener);

2. Configure audio encoding

Configure the audio encoding sent to the model. Set the format, sample rate, and channel count per your business requirements and model specifications. The following example uses 16 kHz mono PCM; for supported ranges, see the run-task parameters in Client events.
AoqClientEngine.AoqAudioCodecConfig encoder =
        new AoqClientEngine.AoqAudioCodecConfig();
encoder.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
encoder.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
encoder.sampleRate = 16000;
encoder.channel = 1;
encoder.bitrate = 24000;
engine.setAudioEncoderConfig(encoder);

3. Configure connection and transport tracks

Configure the AOQ connection using the credentials returned by the AppServer, and choose which tracks to publish and subscribe. The following code publishes Audio and Data tracks and subscribes to the Data track for real-time speech recognition.
addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio);
addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);
addTrack(connectConfig, false,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);

4. Start audio capture and connect

Configure audio capture and establish the AOQ connection. Choose built-in or external capture, VoIP mode, and channel count per your business needs. Keep Audio track sending disabled until task-started is received.
AoqClientEngine.AoqAudioCaptureConfig capture =
        new AoqClientEngine.AoqAudioCaptureConfig();
capture.isExternal = false;
capture.isVoipMode = true;
capture.channel = 1;
engine.startAudioCapture(capture);
// Do not send audio until task-started is received.
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);

5. Start the recognition task

After connecting, generate a task ID and send run-task to start recognition. Configure model, format, sample_rate, and other task parameters per your model and audio input. For full parameter descriptions, see Client events.
taskId = UUID.randomUUID().toString();
JSONObject header = createHeader("run-task");
JSONObject parameters = new JSONObject()
        .put("format", "pcm")
        .put("sample_rate", 16000);
JSONObject payload = new JSONObject()
        .put("task_group", "audio")
        .put("task", "asr")
        .put("function", "recognition")
        .put("model", "fun-asr-realtime")
        .put("parameters", parameters)
        .put("input", new JSONObject());
send(new JSONObject().put("header", header).put("payload", payload));

6. Handle server events

Handle task status, recognition results, and error events, passing results to the business layer. Implement callback logic per your application's display and state management needs; send audio only after receiving task-started and filter heartbeat events when displaying results. For full response structures, see Server events.
String eventName = header.optString("event", "");
if ("task-started".equals(eventName)) {
  taskStarted = true;
  engine.enableSendMediaStream(
          AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
} else if ("result-generated".equals(eventName)) {
  JSONObject payload = event.optJSONObject("payload");
  JSONObject output = payload == null ? null : payload.optJSONObject("output");
  JSONObject sentence = output == null ? null : output.optJSONObject("sentence");
  if (sentence != null && !sentence.optBoolean("heartbeat", false)) {
    String text = sentence.optString("text", "");
    if (!text.isEmpty()) {
      resultListener.onResult(
              text, sentence.optBoolean("sentence_end", false));
    }
  }
} else if ("task-finished".equals(eventName)) {
  resetTaskState();
  resultListener.onTaskFinished();
} else if ("task-failed".equals(eventName)) {
  String message = header.optString("error_message", "Recognition failed");
  resetTaskState();
  resultListener.onError(message);
}

7. End the recognition task

When the user finishes the current recording session, stop audio uplink and send finish-task. Keep the connection open until the final recognition result and task-finished are received; then start a new task or release the connection as needed. For event formats, see Client events.
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
JSONObject payload = new JSONObject().put("input", new JSONObject());
send(new JSONObject()
        .put("header", createHeader("finish-task"))
        .put("payload", payload));

8. Disconnect and destroy the engine

When the page is destroyed or recognition is no longer needed, release audio capture, the AOQ connection, and engine resources. Choose the release timing per your application lifecycle -- do not release immediately after sending finish-task.
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.stopAudioCapture();
engine.disconnect();
AoqClientEngine.destroy();

Complete example

This Android Java class converts the AppServer's JSON response into an AoqConnectConfig and combines the connection, capture, task, and resource release logic described above.
import android.content.Context;
import com.alibaba.aoq.clientsdk.AoqClientEngine;
import com.alibaba.aoq.clientsdk.AoqClientListener;
import org.json.JSONArray;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public final class AsrClient {
  public interface ResultListener {
    void onResult(String text, boolean sentenceEnd);
    void onTaskFinished();
    void onError(String message);
  }
  private final AoqClientEngine engine;
  private final ResultListener resultListener;
  private String taskId;
  private boolean connected;
  private boolean taskStarted;
  public AsrClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig,
                   ResultListener resultListener) {
    this.resultListener = resultListener;
    AoqClientListener listener = new AoqClientListener() {
      @Override
      public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
        connected = status == AoqClientEngine.AoqConnectionStatus
                .AoqConnectionStatusConnected;
        if (connected) {
          beginRecognition();
        }
      }
      @Override
      public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
        handleServerEvent(msg);
      }
    };
    AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
    createConfig.workDir = context.getFilesDir().getAbsolutePath();
    engine = AoqClientEngine.createEngine(context, createConfig, listener);
    configureAudioEncoder();
    configureTracks(connectConfig);
    startAudioCapture();
    engine.connect(connectConfig);
  }
  private void configureAudioEncoder() {
    AoqClientEngine.AoqAudioCodecConfig encoder = new AoqClientEngine.AoqAudioCodecConfig();
    encoder.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
    encoder.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
    encoder.sampleRate = 16000;
    encoder.channel = 1;
    encoder.bitrate = 24000;
    engine.setAudioEncoderConfig(encoder);
  }
  private static void configureTracks(AoqClientEngine.AoqConnectConfig connectConfig) {
    addTrack(connectConfig, true, AoqClientEngine.AoqTrackType.AoqTrackTypeAudio);
    addTrack(connectConfig, true, AoqClientEngine.AoqTrackType.AoqTrackTypeData);
    addTrack(connectConfig, false, AoqClientEngine.AoqTrackType.AoqTrackTypeData);
  }
  private void startAudioCapture() {
    AoqClientEngine.AoqAudioCaptureConfig capture =
            new AoqClientEngine.AoqAudioCaptureConfig();
    capture.isExternal = false;
    capture.isVoipMode = true;
    capture.channel = 1;
    engine.startAudioCapture(capture);
    engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
  }
  /** Start a new recognition task on the existing AOQ connection. */
  public void beginRecognition() {
    if (!connected || taskStarted || taskId != null) {
      return;
    }
    taskId = UUID.randomUUID().toString();
    JSONObject header = createHeader("run-task");
    JSONObject parameters = new JSONObject()
            .put("format", "pcm")
            .put("sample_rate", 16000);
    JSONObject payload = new JSONObject()
            .put("task_group", "audio")
            .put("task", "asr")
            .put("function", "recognition")
            .put("model", "fun-asr-realtime")
            .put("parameters", parameters)
            .put("input", new JSONObject());
    send(new JSONObject().put("header", header).put("payload", payload));
  }
  /** End the current task. Wait for task-finished before disconnecting. */
  public void finishRecognition() {
    if (taskId == null) {
      return;
    }
    engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
    JSONObject payload = new JSONObject().put("input", new JSONObject());
    send(new JSONObject()
            .put("header", createHeader("finish-task"))
            .put("payload", payload));
  }
  public void close() {
    engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
    engine.stopAudioCapture();
    engine.disconnect();
    AoqClientEngine.destroy();
  }
  private void handleServerEvent(AoqClientEngine.AoqDataMsg msg) {
    if (msg == null || msg.data == null) {
      return;
    }
    JSONObject event = new JSONObject(new String(msg.data, StandardCharsets.UTF_8));
    JSONObject header = event.optJSONObject("header");
    if (header == null) {
      return;
    }
    String eventName = header.optString("event", "");
    if ("task-started".equals(eventName)) {
      taskStarted = true;
      engine.enableSendMediaStream(
              AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
    } else if ("result-generated".equals(eventName)) {
      handleRecognitionResult(event);
    } else if ("task-finished".equals(eventName)) {
      resetTaskState();
      resultListener.onTaskFinished();
    } else if ("task-failed".equals(eventName)) {
      String message = header.optString("error_message", "Recognition failed");
      resetTaskState();
      resultListener.onError(message);
    }
  }
  private void handleRecognitionResult(JSONObject event) {
    JSONObject payload = event.optJSONObject("payload");
    JSONObject output = payload == null ? null : payload.optJSONObject("output");
    JSONObject sentence = output == null ? null : output.optJSONObject("sentence");
    if (sentence == null || sentence.optBoolean("heartbeat", false)) {
      return;
    }
    String text = sentence.optString("text", "");
    if (!text.isEmpty()) {
      resultListener.onResult(text, sentence.optBoolean("sentence_end", false));
    }
  }
  private void resetTaskState() {
    engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
    taskStarted = false;
    taskId = null;
  }
  private JSONObject createHeader(String action) {
    return new JSONObject()
            .put("action", action)
            .put("task_id", taskId)
            .put("streaming", "duplex");
  }
  private void send(JSONObject event) {
    AoqClientEngine.AoqDataMsg msg = new AoqClientEngine.AoqDataMsg();
    msg.data = event.toString().getBytes(StandardCharsets.UTF_8);
    engine.sendDataMsg(msg);
  }
  private static void addTrack(AoqClientEngine.AoqConnectConfig config, boolean publish,
                               AoqClientEngine.AoqTrackType type) {
    AoqClientEngine.AoqTrackParam track = new AoqClientEngine.AoqTrackParam();
    track.trackType = type;
    if (publish) {
      config.publishTracks.add(track);
    } else {
      config.subscribeTracks.add(track);
    }
  }
  /** Convert the AppServer Token response into an SDK connection config. */
  public static AoqClientEngine.AoqConnectConfig parseConnectConfig(String responseText) {
    JSONObject response = new JSONObject(responseText);
    AoqClientEngine.AoqConnectConfig config = new AoqClientEngine.AoqConnectConfig();
    config.token = response.optString("aoqTokenForClient", "");
    config.sid = response.optString("sid", "");
    config.certFingerprint = response.optString("clientRelayCertFingerprint", "");
    JSONArray endpoints = response.optJSONArray("clientRelayEndpoints");
    if (endpoints != null) {
      for (int i = 0; i < endpoints.length(); i++) {
        JSONObject item = endpoints.optJSONObject(i);
        if (item == null) {
          continue;
        }
        AoqClientEngine.AoqRelayEndpoint endpoint =
                new AoqClientEngine.AoqRelayEndpoint();
        endpoint.routeIndex = item.has("route_index")
                ? item.optInt("route_index", i) : i;
        endpoint.endpoint = item.optString("endpoint", "");
        endpoint.port = item.optInt("port", 0);
        config.relayEndpoints.add(endpoint);
      }
    }
    return config;
  }
}

Usage example

Pass the AppServer's Token response to parseConnectConfig, then create the client. Recognition starts automatically after the first successful connection. The stop button only ends the current task; release the connection and local resources only when the page is destroyed.
private AsrClient client;
void startRecognition(Context context, String tokenResponseText) {
  AoqClientEngine.AoqConnectConfig config =
          AsrClient.parseConnectConfig(tokenResponseText);
  client = new AsrClient(context, config, new AsrClient.ResultListener() {
    @Override
    public void onResult(String text, boolean sentenceEnd) {
      // Update UI with intermediate or final sentence.
    }
    @Override
    public void onTaskFinished() {
      // Enable start button, or call beginRecognition() for a new task.
    }
    @Override
    public void onError(String message) {
      // Display or log the error.
    }
  });
}
void onStopButtonClick() {
  // End the current task while keeping the AOQ connection until task-finished.
  client.finishRecognition();
}
void onPageDestroyed() {
  // Release local resources only when the page closes.
  client.close();
}

Run and verify

  1. Start the AppServer. Confirm the Token request returns HTTP 200 with sid, aoqTokenForClient, clientRelayEndpoints, and clientRelayCertFingerprint.
  2. Install and run the app on an Android device, grant microphone permission, and speak.
  3. Observe the callbacks. The normal event sequence is:
task-started
result-generated (sentence_end=false)
result-generated (sentence_end=true)
task-finished
You should receive intermediate recognition text continuously while speaking. After calling finishRecognition, you should receive the final text for the current sentence followed by task-finished. Do not disconnect immediately after sending finish-task.

Common scenarios

Multiple recognition rounds on a single connection

After receiving task-finished, call beginRecognition to start the next recognition round on the same AOQ connection. Each round must use a new task_id. There is no need to request a new Token or rebuild the connection; however, if the connection has already been dropped, you must obtain new credentials.

Android background recognition

On Android 10 and above, to continue capturing microphone audio after the app enters the background, use a foreground service with foregroundServiceType=microphone and start the service while the app is still visible to the user.

FAQ

ProblemSolution
Connection failsConfirm the Token has not expired and check whether the AppServer is passing the terminal's real public IP. Do not reuse an old Token after disconnection.
Task started but no recognition resultsConfirm Audio track sending is enabled only after receiving task-started, and verify the audio format, sample rate, and other input parameters per the model's Client events.
No final result receivedDisable Audio track sending before sending finish-task; wait for the final result-generated and task-finished -- do not disconnect immediately.
Android SDK fails to loadConfirm the AAR is included as a dependency and the app packages only SDK-supported ABIs (armeabi-v7a or arm64-v8a).
Next task on the same connection is rejectedConfirm the previous round received task-finished, and generate a new task_id for the new run-task.
For full parameter details, event fields, and other platform interfaces, see:
Real-time speech recognition with AOQ + fun-asr-realtime - QwenCloud