语音对讲
客户端在已建立的 TiRTC 连接上采集本地麦克风,并把声音发送给设备端,适用于门铃对讲等场景。
如果你还没完成客户端工程接入或连接,先看对应客户端的 SDK 接入页(例如 Web、Flutter 或 React Native)和连接设备。远端音视频播放不是语音对讲的前置任务。
前提条件
- 客户端已经完成
TiRtc.initialize(...)。 - 连接已经建立,状态进入
connected/CONNECTED。 - 应用已经完成系统麦克风权限申请。Android 需要
RECORD_AUDIO;iOS 需要麦克风权限;Flutter 和 React Native 按目标平台申请对应权限。 - 设备端已经准备好接收客户端本地音频,并和客户端约定相同的
stream_id。
与设备端约定对讲使用的音频 stream_id
语音对讲使用客户端本地麦克风输入。客户端的示例和 DevTools CLI 默认使用:
| 用途 | stream_id |
|---|---|
| 客户端麦克风对讲 | 14 |
14 是示例取值;也可以约定其他 stream_id,取值范围是 0 到 15。
设备端订阅客户端的对讲音频
设备端只注册 TIRTCCALLBACKS.on_audio,还不会开始接收客户端的对讲音频。连接建立后,设备端需要调用 TiRtcSubscribeAudio(...),订阅客户端 TiRtcAudioInput.attach(...) 使用的同一个 stream_id。
下面的设备端示例在收到连接后订阅 stream_id = 14,并通过 on_audio 接收音频帧:
#include <stdio.h>
#include <tirtc/tiRTC.h>
#define TALKBACK_STREAM_ID 14
static void on_conn_accepted(tirtc_conn_t hconn)
{
int code = TiRtcSubscribeAudio(hconn, TALKBACK_STREAM_ID);
if (code < 0) {
printf("subscribe talkback audio failed: %d\n", code);
}
}
static void on_audio(tirtc_conn_t hconn,
const TIRTCFRAMEINFO *pFi,
void *data)
{
(void)hconn;
if (pFi == NULL || data == NULL || pFi->length == 0 ||
pFi->stream_id != TALKBACK_STREAM_ID) {
return;
}
// 在回调返回前复制 pFi->length 字节到业务线程,再解码或播放。
}
static const TIRTCCALLBACKS kCallbacks = {
.on_conn_accepted = on_conn_accepted,
.on_audio = on_audio,
};
void stop_receiving_talkback(tirtc_conn_t hconn)
{
int code = TiRtcUnsubscribeAudio(hconn, TALKBACK_STREAM_ID);
if (code < 0) {
printf("unsubscribe talkback audio failed: %d\n", code);
}
}把 &kCallbacks 传给 TiRtcStart(...)。TiRtcSubscribeAudio(...) 返回非负值表示调用成功,返回负数表示失败。不再接收对讲音频时,调用 TiRtcUnsubscribeAudio(...)。
客户端调用 TiRtcConn.subscribeAudio(...) 是请求设备端向客户端发送音频,不代表设备端已经反向订阅客户端的对讲音频。
启动语音对讲
Native SDK 的基本顺序是:创建 TiRtcAudioInput -> 设置采集参数 -> 绑定连接和本地音频 stream_id -> 调用 start()。Web SDK 需要在用户手势触发的回调中先调用 start() 获取麦克风权限,再调用 attach(),具体以对应代码示例为准。
final TiRtcAudioInput talkbackInput = TiRtcAudioInput();
Future<int> startTalkback(TiRtcConn conn) async {
talkbackInput.onStateChanged = (TiRtcInputState state) {
debugPrint('talkback state=$state');
};
talkbackInput.onError = (int code, String? message) {
debugPrint('talkback error=$code message=${message ?? ""}');
};
int code = await talkbackInput.setOptions(
const TiRtcAudioInputOptions(
codec: TiRtcAudioCodec.g711a,
sampleRate: TiRtcAudioSampleRate.rate16k,
channels: TiRtcAudioChannelCount.mono,
aecMode: TiRtcAudioAecMode.enabled,
),
);
if (code != 0) return code;
code = await talkbackInput.attach(connection: conn, streamId: 14);
if (code != 0) return code;
return talkbackInput.start();
}
Future<void> stopTalkback(TiRtcConn conn) async {
await talkbackInput.stop();
await talkbackInput.detach(connection: conn);
await talkbackInput.dispose();
}import {
TiRtcAudioAecMode,
TiRtcAudioChannelCount,
TiRtcAudioCodec,
TiRtcAudioInput,
TiRtcAudioSampleRate,
TiRtcConn,
} from 'tirtc-react-native';
const talkbackInput = new TiRtcAudioInput();
async function startTalkback(conn: TiRtcConn): Promise<number> {
talkbackInput.onStateChanged = (state) => {
console.info(`talkback state=${state}`);
};
talkbackInput.onError = (code, message) => {
console.warn(`talkback error=${code} message=${message ?? ''}`);
};
let code = await talkbackInput.setOptions({
codec: TiRtcAudioCodec.g711a,
sampleRate: TiRtcAudioSampleRate.rate16k,
channels: TiRtcAudioChannelCount.mono,
aecMode: TiRtcAudioAecMode.enabled,
});
if (code !== 0) return code;
code = await talkbackInput.attach(conn, 14);
if (code !== 0) return code;
return talkbackInput.start();
}
async function stopTalkback(conn: TiRtcConn): Promise<void> {
await talkbackInput.stop();
await talkbackInput.detach(conn);
await talkbackInput.dispose();
}val talkbackInput = TiRtcAudioInput()
fun startTalkback(conn: TiRtcConn): Int {
talkbackInput.onStateChanged = TiRtcInputStateListener { state ->
Log.i("TiRTC", "talkback state=$state")
}
talkbackInput.onError = TiRtcInputErrorListener { code, message ->
Log.w("TiRTC", "talkback error=$code message=$message")
}
var code = talkbackInput.setOptions(
TiRtcAudioInputOptions(
codec = TiRtcAudioCodec.G711A,
sampleRate = TiRtcAudioSampleRate.RATE_16K,
channels = TiRtcAudioChannelCount.MONO,
aecMode = 1,
),
)
if (code != 0) return code
code = talkbackInput.attach(conn, 14)
if (code != 0) return code
return talkbackInput.start()
}
fun stopTalkback(conn: TiRtcConn) {
talkbackInput.stop()
talkbackInput.detach(conn)
talkbackInput.dispose()
}import { TiRtcAudioInput, TiRtcAudioCodec, TiRtcAudioSampleRate } from 'tirtc/Index';
const talkbackInput = new TiRtcAudioInput();
async function startTalkback(conn: TiRtcConn): Promise<number> {
let code = await talkbackInput.setOptions({
codec: TiRtcAudioCodec.g711a,
sampleRate: TiRtcAudioSampleRate.rate16k,
channels: 1,
aecMode: 1,
});
if (code !== 0) return code;
code = await talkbackInput.attach({ connection: conn, streamId: 14 });
if (code !== 0) return code;
return talkbackInput.start();
}
async function stopTalkback(conn: TiRtcConn): Promise<void> {
await talkbackInput.stop();
await talkbackInput.detach({ connection: conn });
await talkbackInput.dispose();
}final class Talkback: NSObject, TiRtcAudioInputDelegate {
private let input = TiRtcAudioInput()
override init() {
super.init()
input.delegate = self
}
func start(conn: TiRtcConn) -> Int32 {
let options = TiRtcAudioInputOptions()
options.codec = .g711a
options.sampleRate = .rate16k
options.channels = .mono
options.aecMode = 1
var code = input.setOptions(options)
if code != 0 { return code }
code = input.attach(connection: conn, streamId: 14)
if code != 0 { return code }
return input.start()
}
func stop(conn: TiRtcConn) {
_ = input.stop()
_ = input.detach(connection: conn)
input.dispose()
}
func audioInput(_ input: TiRtcAudioInput, didChangeState state: TiRtcInputState) {
print("talkback state=\(state.rawValue)")
}
func audioInput(_ input: TiRtcAudioInput, didFailWithCode code: Int32, message: String?) {
print("talkback error=\(code) message=\(message ?? "")")
}
}const talkbackInput = TiRtcAudioInput({
connection,
streamId: 14,
});
async function startTalkback() {
talkbackInput.setOptions({ sampleRate: 8000 });
await talkbackInput.start(); // 在点击按钮等用户手势里调用,浏览器会在这里申请麦克风权限
await talkbackInput.attach();
}
async function stopTalkback() {
talkbackInput.stop();
await talkbackInput.detach();
}确认语音已送到设备端
对于返回整数并提供输入状态回调的客户端 SDK,TiRtcAudioInput.start() 返回 0、输入状态进入 running / RUNNING,表示本地音频输入已经启动。Web 端以 start() 返回的 Promise 正常完成为准。这些信号都不表示设备端已经订阅或收到音频。
真正跑通语音对讲,需要同时看到两个结果:
- 客户端出现对应平台的本地音频输入启动成功信号,并且没有触发输入错误。
- 你在客户端说话时,设备端的
TIRTCCALLBACKS.on_audio持续收到对应stream_id的非空音频帧;需要从扬声器听到声音时,再确认设备端已经正确解码并播放这些帧。示例的stream_id是14。
常见问题
打开麦克风失败
优先检查:
- 应用是否已经获得系统麦克风权限。
- 连接是否已经进入 connected。
- 本地对讲使用的
stream_id是否和设备端接收配置一致。 TiRtcAudioInput是否还绑定着旧连接或旧stream_id;更换绑定前先stop()和detach()。
设备端听不到客户端声音
优先检查:
- 连接建立后,设备端是否调用
TiRtcSubscribeAudio(...)订阅对讲音频,返回值是否为非负数。 - 设备端订阅的
stream_id是否与客户端传给TiRtcAudioInput.attach(...)的值一致。 - Native 客户端是否调用了
TiRtcAudioInput.start()并拿到返回值0;Web 客户端调用start()返回的 Promise 是否正常完成。 - 设备端的
on_audio是否持续收到对应stream_id的非空音频帧。收到帧但扬声器没有声音时,继续检查设备端的解码和播放处理。 - 客户端系统输入设备是否正常,麦克风是否被其他应用独占。
- 如果使用 DevTools CLI 验证语音对讲,CLI 是否用
--input system --output both启动。