Android、iOS、および HarmonyOS で AOQ Client SDK を統合し、AOQ と qwen3.5-omni-plus-realtime を使用してオーディオ/ビデオ通話を実装する方法を説明します。
SDK の取得
AOQ Client SDK および Opus オーディオプラグインについては、「SDK ダウンロード」をご参照ください。Opus コーデックは独立したプラグインとして提供されています。ユースケースに応じて組み込んでください。
SDK のインポート
コア SDK ファイルをプロジェクトの依存関係ディレクトリにコピーし、プロジェクト設定で必要な権限を宣言します。
Android
AoqClientSdk-release.aar をプロジェクトの app/libs/ ディレクトリに配置します。libPluginOpus.so を ABI ごとに app/libs/armeabi-v7a/ および app/libs/arm64-v8a/ に配置します。app/build.gradle で次のように設定します。
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
sourceSets { main { jniLibs.srcDirs = ['libs'] } }
packagingOptions {
// ホストプロジェクトで同名の .so ファイルとの競合を回避します
pickFirsts += ['lib/*/*.so']
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
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 および CAMERA はランタイム権限です。アプリケーションは実行時に Android の ActivityCompat.requestPermissions() を呼び出してユーザーに承認を要求する必要があります。
iOS (フレームワーク)
-
AoqClientSdk.frameworkおよびPluginOpus.frameworkを Xcode プロジェクトにドラッグします。Target > General > Frameworks, Libraries, and Embedded Content で、[埋め込みと署名] を選択します。 -
権限の宣言:Xcode で Target > Info > Custom iOS Target Properties を選択し、次の 2 つの権限使用目的説明を追加します。
Key
Value
NSMicrophoneUsageDescriptionリアルタイム音声通話のため
NSCameraUsageDescriptionリアルタイムビデオ通話のため
-
Swift プロジェクトでは
import AoqClientSdk、Objective-C プロジェクトでは#import <AoqClientSdk/AoqClientSdk.h>を使用します。
HarmonyOS (har)
aoq-client-sdk.harをプロジェクトのlibs/ディレクトリに配置します。libPluginOpus.soを ABI ごとにentry/libs/armeabi-v7a/およびentry/libs/arm64-v8a/に配置します。entry/oh-package.json5で依存関係を宣言します。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" } }
]
EntryAbilityで、abilityAccessCtrl.createAtManager().requestPermissionsFromUserを使用してランタイム認証をトリガーします。
デモの試用
Alibaba Cloud Model Studio の Android デモを使用して、AOQ の接続性を迅速に検証できます。APK をダウンロードし、API キーと workspaceId を設定して、選択したモデルを試してください。
次の QR コードをスキャンして Android デモをダウンロードし、インストールします。
AppServer からのトークン取得
「トークン認証」の AOQ セクションに従って、トークンを取得するための AppServer を設定します。各呼び出しの前に、クライアントは AppServer にトークンを要求する必要があります。
AI オーディオ/ビデオ通話の実装
エンジンの作成とコールバックの設定
createEngine API を呼び出して、AoqClientEngine インスタンスを作成します。
let config = AoqCreateConfig()
config.workDir = workDir
engine = AoqClientEngine.createEngine(config, delegate: self)
AoqEngineDelegate プロトコルを実装して、onConnectionStatusChange、onDataMsg、onError などのコールバックをリッスンします。
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);
オーディオ/ビデオのキャプチャと再生の開始
startAudioCapture と startAudioPlayer を呼び出して、ローカルオーディオのキャプチャと再生を開始します。startVideoCapture を呼び出してカメラを起動し、setLocalView を使用して SDK のレンダリングターゲットをプレビューコントロールにバインドします。
// オーディオキャプチャ
let capCfg = AoqAudioCaptureConfig()
capCfg.channel = 1; capCfg.isExternal = false
engine.startAudioCapture(capCfg)
// オーディオ再生
let playCfg = AoqAudioPlaybackConfig()
playCfg.channel = 1; playCfg.isExternal = false
engine.startAudioPlayer(playCfg)
// ビデオキャプチャ
let vidCfg = AoqVideoCaptureConfig()
vidCfg.width = 720; vidCfg.height = 1280; vidCfg.fps = 15
engine.startVideoCapture(vidCfg)
// ローカルビデオプレビューのレンダリングビューを設定
let canvas = AoqVideoCanvas()
canvas.view = localPreview
canvas.renderMode = .crop
engine.setLocalView(.video, canvas: canvas)
Android:
// オーディオキャプチャ
AoqAudioCaptureConfig capCfg = new AoqAudioCaptureConfig();
capCfg.channel = 1; capCfg.isExternal = false;
engine.startAudioCapture(capCfg);
// オーディオ再生
AoqAudioPlaybackConfig playCfg = new AoqAudioPlaybackConfig();
playCfg.channel = 1; playCfg.isExternal = false;
engine.startAudioPlayer(playCfg);
// ビデオキャプチャ
AoqVideoCaptureConfig vidCfg = new AoqVideoCaptureConfig();
vidCfg.width = 720; vidCfg.height = 1280; vidCfg.fps = 15;
engine.startVideoCapture(vidCfg);
// ローカルビデオプレビューのレンダリングビューを設定
AoqVideoCanvas canvas = new AoqVideoCanvas();
canvas.view = localPreview;
canvas.renderMode = AoqRenderMode.AoqRenderModeCrop;
engine.setLocalView(AoqTrackType.AoqTrackTypeVideo, canvas);
HarmonyOS:
// オーディオキャプチャ
const capCfg: AoqAudioCaptureConfig = { channel: 1, isExternal: false };
engine.startAudioCapture(capCfg);
// オーディオ再生
const playCfg: AoqAudioPlaybackConfig = { channel: 1, isExternal: false };
engine.startAudioPlayer(playCfg);
// ビデオキャプチャ
const vidCfg: AoqVideoCaptureConfig = { width: 720, height: 1280, fps: 15, isExternal: false };
engine.startVideoCapture(vidCfg);
// ローカルビデオプレビューのレンダリングビューを設定
const canvas: AoqVideoCanvas = { view: localCtrl, renderMode: AoqRenderMode.AoqRenderModeCrop };
engine.setLocalView(AoqTrackType.AoqTrackTypeVideo, canvas);
接続認証情報の取得
AppServer に Alibaba Cloud Model Studio へのリクエストをプロキシさせます。詳細については、「トークン認証」をご参照ください。
コーデックの設定と接続の確立
コーデックパラメータを設定し、connect を呼び出します。
注:qwen3.5-omni-plus-realtime では、クライアントはサーバーから session.updated を受信した後にのみメディアデータの送信を開始する必要があります。connect が成功してから session.updated が到着するまでの間に誤ってメディアを送信しないようにするため、connect を呼び出す前に、各アップストリームトラックに対して enableSendMediaStream(trackType, false) を呼び出してください。WebSocket イベントの詳細については、「クライアントイベント」をご参照ください。
// オーディオコーデック設定
let encCfg = AoqAudioCodecConfig()
encCfg.codecType = .audioPCM; encCfg.sampleRate = 16000; encCfg.channel = 1
engine.setAudioEncoderConfig(encCfg)
engine.setAudioDecoderConfig(encCfg)
// connect の前にメディア送信を無効化し、session.updated 受信後に有効化します
engine.enableSendMediaStream(.audio, enable: false)
engine.enableSendMediaStream(.video, enable: false)
// 接続の確立
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:
// オーディオコーデック設定
AoqAudioCodecConfig encCfg = new AoqAudioCodecConfig();
encCfg.codecType = AoqEncoderType.AoqEncoderTypeAudioPCM;
encCfg.sampleRate = 16000; encCfg.channel = 1;
engine.setAudioEncoderConfig(encCfg);
engine.setAudioDecoderConfig(encCfg);
// connect の前にメディア送信を無効化し、session.updated 受信後に有効化します
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeAudio, false);
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeVideo, false);
// 接続の確立
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:
// オーディオコーデック設定
const encCfg: AoqAudioCodecConfig = {
codecType: AoqEncoderType.AoqEncoderTypeAudioPCM,
sampleRate: 16000, channel: 1
};
engine.setAudioEncoderConfig(encCfg);
engine.setAudioDecoderConfig(encCfg);
// connect の前にメディア送信を無効化し、session.updated 受信後に有効化します
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeAudio, false);
engine.enableSendMediaStream(AoqTrackType.AoqTrackTypeVideo, false);
// 接続の確立
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);
重要:AOQ SDK は接続後にデフォルトでメディアデータの送信を開始します。この例では、モデルに接続する際にメディア送信を無効にする方法を示しています。
AI セッションの設定
onConnectionStatusChange(Connected) コールバックで、sendDataMsg を介して session.update メッセージを送信します。このメッセージには、モダリティ、音声、指示、turn_detection などのセッションパラメータが含まれ、セッションハンドシェイクを完了します。WebSocket イベントの詳細については、「クライアントイベント」をご参照ください。
func onConnectionStatusChange(_ status: AoqConnectionStatus) {
if status == .connected { sendSessionUpdate() }
}
private func sendSessionUpdate() {
let json = """
{
// イベントID、クライアント側で生成
"event_id": "event_ToPZqeobitzUJnt3QqtWg",
// イベントタイプ、常に session.update
"type": "session.update",
// セッション設定
"session": {
// 出力モダリティ: ["text"] (テキストのみ) または ["text","audio"] (テキストとオーディオ)
"modalities": [
"text",
"audio"
],
// 出力オーディオ音声
"voice": "Ethan",
// 入力オーディオ形式。現在は "pcm" のみサポート。入力オーディオは 16 kHz PCM。
"input_audio_format": "pcm",
// 出力オーディオ形式。現在は "pcm" のみサポート。出力オーディオは 24 kHz PCM。
"output_audio_format": "pcm",
// モデルの目的や役割を設定するためのシステムメッセージ
"instructions": "あなたは五つ星ホテルの AI カスタマーサービスエージェントです。客室タイプ、設備、価格、予約ポリシーに関するお客様のお問い合わせに正確かつ親切に回答してください。常にプロフェッショナルで親切に対応してください。検証されていない情報やホテルサービスの範囲外の情報を提供しないでください。",
// 音声アクティビティ検出 (VAD) を有効にするかどうか。有効にする場合は config オブジェクトを渡すと、サーバーが音声の開始と終了を自動的に検出します。
// クライアントがモデル応答をトリガーするタイミングを制御する場合は null に設定します。
"turn_detection": {
// VADタイプ: server_vad または semantic_vad。qwen3.5-omni-plus-realtime では semantic_vad を推奨
"type": "semantic_vad",
// VAD検出のしきい値。ノイズの多い環境では増やし、静かな環境では減らします。
"threshold": 0.5,
// モデル応答がトリガーされる無音期間
"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 = /* 上記の Swift の例と同じ session.update JSON */;
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 = /* 上記の Swift の例と同じ session.update JSON */;
const msg: AoqDataMsg = { data: new TextEncoder().encode(sessionUpdateJson).buffer };
this.engine.sendDataMsg(msg);
}
session.updated 受信後のメディア送信の有効化
onDataMsg コールバックで、受信メッセージを解析します。モデルから session.updated を受信したら、以前に無効化した各トラックタイプに対して enableSendMediaStream(trackType, true) を呼び出します。WebSocket イベントの詳細については、「サーバーイベント」をご参照ください。
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 (_) { /* JSONでない場合は無視 */ }
}
重要
-
session.updatedを受信した後にのみメディアストリームの送信を開始してください。このイベントの前に AI はデータを受信する準備ができていない可能性があります。 -
接続時に追加されたオーディオおよびビデオトラック (AOQ メディアチャネル) は、自動的にサーバーにデータを配信します。
- オーディオ:オーディオトラックを介して直接送信されます。—
input_audio_buffer.appendイベントを送信する必要はありません。 - ビデオ:フレームはビデオトラックを介して送信されます。—
input_image_buffer.appendイベントを送信する必要はありません。
- オーディオ:オーディオトラックを介して直接送信されます。—
接続の切断とエンジンの破棄
engine.disconnect()
AoqClientEngine.destroy()
一般的なシナリオ
割り込み発話
- SDK は Alibaba Cloud Model Studio と深く統合されています。新しいターンが開始されると、モデルからの割り込み発話メッセージが前のターンを中断します。
- SDK は、ローカル再生を中断するための
interruptAudioPlayerインターフェイスを提供します。ユーザーが再生を停止したい場合は、この API を呼び出してください。
// iOS
engine.interruptAudioPlayer(.audio, fadeMs: 100)
ミュート / ミュート解除
ミュート後、SDK はオーディオのキャプチャを続けますが、無音フレームのみを送信します。session は中断されません。
engine.muteAudioCapture(true); // マイクをミュート (キャプチャは継続、無音フレームのみ送信)
engine.muteAudioCapture(false); // ミュート解除
フロントカメラとリアカメラの切り替え
// 対象のカメラ方向を示すenumを渡します
engine.switchCamera(AoqCameraDirection.AoqCameraDirectionFront);
engine.switchCamera(AoqCameraDirection.AoqCameraDirectionBack);
通話字幕と ASR 結果
サーバーは、ダウンストリームデータメッセージを介して ASR 結果と AI テキスト応答をプッシュします。アプリケーションでは、onDataMsg コールバックの type フィールドを元にルーティングしてください。WebSocket イベントの詳細については、「サーバーイベント」をご参照ください。
注意事項
-
シングルトンセマンティクス:
createEngineはシングルトンです — 繰り返し呼び出すと同じインスタンスが返されます。エンジンはdestroyが呼び出された後にのみ再作成できます。複数ページで使用する場合は、Application または Ability レベルでエンジンライフサイクルを管理してください。 -
ローカルプレビュー用ビューのタイプ:
- Android:
SurfaceViewまたはTextureView。その他のタイプはサポートされていません。 - iOS:任意の
UIViewサブクラス。 - HarmonyOS:SDK ドキュメントをご参照ください。
- Android:
-
オーディオルートの変更:ヘッドフォンの挿入/取り外しや Bluetooth 接続などのイベントは、
onAudioDeviceRouteChangedをトリガーします。通常、アプリケーション側での対応は必要ありません。UI にスピーカー/イヤピースの切り替えが含まれている場合は、このコールバックに基づいて状態を同期してください。 -
バックグラウンドでの音声継続:アプリがバックグラウンドに移行したときにオーディオを継続するには、
Info.plistでUIBackgroundModes = audioを有効にし、AVAudioSessionをフォアグラウンドで正しくアクティブにする必要があります。SDK はほとんどのケースを処理します。きめ細かい制御にはsetAudioSessionRestriction:を使用してください。
iOS デモのソースコード
iOS で AOQ を実装する方法を学ぶには、サンプルソースコードをダウンロードしてください:aoqdemo.zip。
関連ドキュメント
- AOQ Client SDK API の詳細:「SDK 概要」
- qwen3.5-omni-plus-realtime モデルのクライアントイベント:「クライアントイベント」
- qwen3.5-omni-plus-realtime モデルのサーバーイベント:「サーバーイベント」