实时语音开发指南
SDK 获取
可以在 下载页 获得 TapSDK,引入 TapRTC
模块:
- Unity
- Android
- iOS
请先确保系统已安装 git-lfs,然后通过 UPM 方式添加依赖:
"dependencies":{
...
"com.taptap.tds.rtc":"https://github.com/TapTap/TapRTC-Unity.git#3.18.2",
}
repositories{
flatDir {
dirs 'libs'
}
}
dependencies {
...
implementation (name:'TapRTC_1.1.0', ext:'aar')
}
- 在 Xcode 选择工程,到 Build Setting > Other Linker Flags 添加
-ObjC
。 - 拖拽
TapRTC_SDK
目录到项目目录。 TapRTC.framework
和GMESDK.framework
文件拖进项目,选择Do Not Embed
。- 将资源文件 TapRTC.bundle 拖进项目。
- 添加 SDK 依赖的系统库:libz、libresolv、libiconv、libc++、CoreMedia.framework、CoreAudio.framework、AVFoundation.framework、SystemConfiguration.framework、UIKit.framework、AudioToolbox.framework、OpenAL.framework、Security.framework
TapRTC.framework
注意事项
- RTC 使用前需要在开发者中心进行相关配置。
- 开发者需要在自己的服务器上实现相应的签名鉴权服务。
- C# SDK 需要周期性的调用
TapRTC.Poll
接口,才能触发相关的事件回调。
Android 平台需要申请网络、音频相关权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
iOS 平台需要申请麦克风权限(Privacy - Microphone Usage Description
)。
游戏通常没有后台通话的需求,玩家一般边玩游戏边语音,游戏保持在前台。 如果游戏类型比较特殊,需要支持后台通话,那么还需申请后台播放权限: 在 target 中配置 Capability > Background Modes > Audio, AirPlay, and Picture in Picture。
核心接口
RTC 通过 TapRTCConfig
来进行初始化配置。初始化的过程是异步的,需要等待初始化结果返回之后,才能进行下一步的操作。
ClientId
、ClientToken
、ServerUrl
,参见关于应用凭证的说明。UserId
: 开发者自定义的用户 Id,用于标记玩家DeviceId
: 开发者自定义的设备 Id,用于标记设备AudioPerfProfile
: 音频质量配置(LOW
、MID
、HIGH
,默认为MID
)
- Unity
- Android
- iOS
using TapTap.RTC;
var config = new TapRTCConfig.Builder()
.ClientID("ClientId")
.ClientToken("ClientToken")
.ServerUrl("ServerUrl")
.UserId("UserId")
.DeviceId("DeviceId")
.AudioProfile(AudioPerfProfile.MID)
.ConfigBuilder();
ResultCode code = await TapRTC.Init(config);
if (code == ResultCode.OK) {
// 初始化成功
} else {
// 失败
}
// SDK 的 RTC 模块中,返回 ResultCode 的接口均以 ResultCode.OK 表示操作成功。
import android.app.Application;
import com.taptap.taprtc.Config;
import com.taptap.taprtc.Config.AudioPerfProfile;
import com.taptap.taprtc.DeviceID;
import com.taptap.taprtc.UserID;
import com.taptap.taprtc.TapRTCEngine;
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
Config config = new Config();
config.appId = "AppId";
config.appKey = "AppKey";
config.serverUrl = "ServerUrl"; // 请去掉 http/https 前缀,直接传入自定义域名或者形如「xxx.cloud.tds1.tapapis.cn」之类的域名即可。
config.userId = new UserID("UserId");
config.deviceId = new DeviceID("DeviceId");
// 如需使用范围语音功能,此项必须设为 LOW
config.profile = AudioPerfProfile.MID;
try {
TapRTCEngine.get().init(this, config, resultCode -> {
if (resultCode == ResultCode.OK) {
// 初始化成功
} else {
// 初始化失败
}
});
} catch (TapRTCException e) {
throw new RuntimeException(e);
}
}
}
// SDK 的 RTC 模块中,返回 ResultCode 的接口均以 ResultCode.OK 表示操作成功。
TapRTCConfig *config = [[TapRTCConfig alloc] initWithAppId:@"AppId"
appKey:@"AppKey" serverUrl:@"ServerUrl"
userId:@"UserId" deviceId:@"DeviceId"
profile:AudioPerfProfileMID];
TapRTCEngine *engine = [TapRTCEngine defaultEngine];
[engine initializeWithConfig:config resultBlock:^(NSError * _Nullable error) {
if (error) {
// handle error
}
})];
触发回调事件
C# SDK 需要在 Update
方法中调用 Poll
方法触发事件回调。如果不调用该方法,会导致 SDK 运行异常。
public void Update()
{
ResultCode code = TapRTC.Poll();
if (code == ResultCode.OK) {
// 成功触发回调
} else {
// 失败
}
}
Java SDK、Objective-C SDK 无需定期调用 Poll
方法。
恢复系统
- Unity
- Android
- iOS
ResultCode code = TapRTC.Resume();
if (code == ResultCode.OK) {
// 成功恢复
} else {
// 失败
}
import com.taptap.taprtc.TapRTCEngine;
ResultCode code = TapRTCEngine.get().resume();
TapRTCResultCode resultCode = [engine resume];
if (resultCode == TapRTCResultCode_Success) {
// resumed
} else {
// failed to resume
}
暂停系统
- Unity
- Android
- iOS
ResultCode code = TapRTC.Pause();
if (code == ResultCode.OK) {
// 成功暂停
} else {
// 失败
}
import com.taptap.taprtc.TapRTCEngine;
ResultCode code = TapRTCEngine.get().pause();
TapRTCResultCode resultCode = [engine pause];
if (resultCode == TapRTCResultCode_Success) {
// paused
} else {
// failed to pause
}
房间相关接口
创建房间
初始化成功之后,SDK 在创建房间之后,才可以进行实时语音通话。
创建房间时需指定房间号(roomId
)。
是否启用范围语音也需在创建房间时设置,C# SDK 默认不启用,Java SDK 创建时必须指定是否启用范围语音,Objective-C SDK 使用单独的接口创建范围语音房间。
- Unity
- Android
- iOS
bool enableRangeAudio = false;
var room = await TapRTC.AcquireRoom("roomId", enableRangeAudio);
import com.taptap.taprtc.TapRTCEngine;
import com.taptap.taprtc.RoomID;
RoomId roomId = new RoomID("roomId");
boolean enableRangeAudio = false;
TapRTCRoom room = TapRTCEngine.get().acquireRoom(roomId, enableRangeAudio);
TapRTCRoom *room = [engine acquireRoomWithRoomId:@"roomID"];
注册房间相关的回调事件
- Unity
- Android
- iOS
room.RegisterEventAction(new TapRTCEvent()
{
OnDisconnect = (code, message) => { label.text += "\n" + $"断开连接 code:{code} msg:{e}"; },
OnEnterFailure = s => { label.text += "\n" + $"进入房间失败:{s}"; },
OnEnterSuccess = () => { label.text += "\n" + $"进入房间成功"; },
OnExit = () => { label.text += "\n" + $"退出房间成功"; },
OnUserEnter = userId => { label.text += "\n" + $"玩家{userId}进入房间"; },
OnUserExit = userId => { label.text += "\n" + $"玩家{userId}退出房间"; },
OnUserSpeaker = (userId, volume) => { label.text += "\n" + $"玩家{userId}在房间说话,音量{volume}"; },
OnUserSpeakEnd = userId => { label.text += "\n" + $"玩家{userId}在房间说话结束"; },
// 返回切换后的音质,参见下文「切换音频质量」一节
OnRoomTypeChanged = (i) => { label.text += "\n" + $"房间音质改为{i}"; },
OnRoomQualityChanged = (weight, loss, delay) =>
{
Debug.Log($"音频质量:{weight} 丢包率:{loss}% 延迟:{delay}ms");
},
});
import com.taptap.taprtc.UserID;
import com.taptap.taprtc.TapRTCRoom;
room.registerCallback(new TapRTCRoom.Callback() {
// 进入房间成功
@Override public void onEnterSuccess() {}
// 进入房间失败
@Override public void onEnterFailure(String msg) {}
// 连接断开
@Override public void onDisconnect() {}
// 重连成功
@Override public void onReConnected() {}
// 当前玩家退出房间
@Override public void onExit() {}
// 其他玩家进入房间
@Override public void onUserEnter(UserID userId) {}
// 其他玩家退出房间
@Override public void onUserExit(UserID userId) {}
// 玩家说话开始
@Override public void onUserSpeakStart(UserID userId, int volume) {}
// 玩家说话结束
@Override public void onUserSpeakEnd(UserID userId) {}
// 音频质量变化
@Override public void onRoomQualityChanged(int weight, double loss, int delay) {}
});
// 需实现 TapRTCRoomDelegate
// 进入房间成功
- (void)onEnterSuccess;
// 进入房间失败
- (void)onEnterFailure:(NSError *)error;
// 其他玩家进入房间
- (void)onUsersEnter:(NSString *)userId;
// 其他玩家退出房间
- (void)onUsersExit:(NSString *)userId;
// 当前玩家退出房间
- (void)onExit;
// 连接断开
- (void)onDisconnect;
// 重连成功
- (void)onReconnected;
// 玩家说话开始
- (void)onUsersSpeakStart:(NSString *)userId volume:(NSInteger)volume;
// 玩家说话结束
- (void)onUsersSpeakEnd:(NSString *)userId;
// 音频质量变化(返回音频质量、丢包率、延迟)
- (void)onQualityCallBackWithWeight:(int)weight loss:(float)loss delay:(int)delay;
加入房间
使用服务端生成的鉴权信息进入房间。
进入房间成功之后,会通过 TapRTCEvent
中的 OnEnterSuccess
进行回调。
- Unity
- Android
- iOS
ResultCode code = await room.Join("authBuffer");
if (code == ResultCode.OK) {
// 成功加入
}
if (code == ResultCode.ERROR_ALREADY_IN_ROOM) {
// 玩家已经在此房间内
}
import com.taptap.taprtc.Authority;
Authority authBuffer = new Authority("authBuffer");
ResultCode code = room.join(authBuffer);
if (code == ResultCode.OK) {
// 成功加入
}
if (code == ResultCode.ERROR_ALREADY_IN_ROOM) {
// 玩家已经在此房间内
}
[room joinWithAuth:@"authBuffer"];
authBuffer
是在服务端生成的鉴权信息,详见下文服务端鉴权一节的说明。
退出房间
退出房间之后,会通过 TapRTCEvent
中的 OnExit
进行回调。
- Unity
- Android
- iOS
ResultCode code = room.Exit();
ResultCode code = room.exit();
TapRTCResultCode resultCode = [room exit];
if (resultCode == TapRTCResultCode_Success) {
// exited
} else {
// failed exit
}
收听某人语音(默认收听)
- Unity
- Android
- iOS
ResultCode code = room.EnableUserAudio("userId");
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_USER_NOT_EXIST) {
// 玩家不存在
}
import com.taptap.taprtc.UserID;
UserId userId = new UserID("userId");
ResultCode code = room.enableUserAudio(userId);
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_USER_NOT_EXIST) {
// 玩家不存在
}
TapRTCResultCode resultCode = [room enableUserAudioWithUserId:@"userId"];
拒收某人语音
- Unity
- Android
- iOS
ResultCode code = room.DisableUserAudio("userId");
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_USER_NOT_EXIST) {
// 玩家不存在
}
import com.taptap.taprtc.UserID;
UserId userId = new UserID("userId");
ResultCode code = room.disableUserAudio(userId);
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_USER_NOT_EXIST) {
// 玩家不存在
}
TapRTCResultCode resultCode = [room disableUserAudioWithUserId:@"userId"];
打开/关闭语音
这个接口设置音频下行的开关。 一般来说,推荐游戏使用打开/关闭扬声器的接口。
- Unity
- Android
- iOS
// 开启
ResultCode code = room.EnableAudioReceiver(true);
// 关闭
ResultCode code = room.EnableAudioReceiver(false);
// 开启
ResultCode code = room.enableAudioReceiver(true);
// 关闭
ResultCode code = room.enableAudioReceiver(false);
TapRTCResultCode resultCode = [room enableAudioReceiver:YES];
切换音频质量
音频质量分为 LOW、MID、HIGH 三档。
进入房间后,可以切换音频质量。
- Unity
- Android
- iOS
room.ChangeRoomType(AudioPerfProfile.LOW);
room.ChangeRoomType(AudioPerfProfile.MID);
room.ChangeRoomType(AudioPerfProfile.HIGH);
// 暂未支持
// 暂未支持
切换音频质量会触发 OnRoomTypeChanged
回调。
获取当前房间的用户
- Unity
- Android
- iOS
HashSet<string> userIdList = room.Users;
List<UserID> userIdList = room.getUsers();
[room getUsers:^(NSArray<NSString *>*userIDs, NSError * _Nullable error) {
if (error) {
// 处理错误
} else {
// userIDs 是房间内的用户 ID 列表
}
})];
音频控制相关接口
打开/关闭麦克风
- Unity
- Android
- iOS
// 打开
ResultCode code = TapRTC.GetAudioDevice().EnableMic(true);
// 关闭
ResultCode code = TapRTC.GetAudioDevice().EnableMic(false);
// 打开
boolean ok = TapRTCEngine.get().getAudioDevice().enableMic(true);
// 关闭
boolean ok = TapRTCEngine.get().getAudioDevice().enableMic(false);
// 打开
TapRTCResultCode code = [engine.audioDevice enableMic:YES];
// 关闭
TapRTCResultCode code = [engine.audioDevice enableMic:NO];
打开/关闭扬声器
- Unity
- Android
- iOS
// 打开
ResultCode code = TapRTC.GetAudioDevice().EnableSpeaker(true);
// 关闭
ResultCode code = TapRTC.GetAudioDevice().EnableSpeaker(false);
// 打开
boolean ok = TapRTCEngine.get().getAudioDevice().enableSpeaker(true);
// 关闭
boolean ok = TapRTCEngine.get().getAudioDevice().enableSpeaker(false);
// 打开
TapRTCResultCode code = [engine.audioDevice enableSpeaker:YES];
// 关闭
TapRTCResultCode code = [engine.audioDevice enableSpeaker:NO];
设置/获取音量
音量是 0 到 100 之间的整数。
- Unity
- Android
- iOS
int vol = 60;
// 设置麦克风音量
ResultCode code = TapRTC.GetAudioDevice().SetMicVolume(vol);
// 设置扬声器音量
ResultCode code = TapRTC.GetAudioDevice().SetSpeakerVolume(vol);
// 获取麦克风音量
int micVolume = TapRTC.GetAudioDevice().GetMicVolume();
// 获取扬声器音量
int speakerVolume = TapRTC.GetAudioDevice().GetSpeakerVolume();
int vol = 60;
// 设置麦克风音量
TapRTCEngine.get().getAudioDevice().setMicVolume(vol);
// 设置扬声器音量
boolean ok = TapRTCEngine.get().getAudioDevice().setSpeakerVolume(vol);
// 获取麦克风音量
int micVolume = TapRTCEngine.get().getAudioDevice().getMicVolume();
// 获取扬声器音量
int speakerVolume = TapRTCEngine.get().getAudioDevice().getSpeakerVolume();
int vol = 60;
// 设置麦克风音量
TapRTCResultCode code = [engine.audioDevice setMicVolume:vol];
// 设置扬声器音量
TapRTCResultCode code = [engine.audioDevice setSpeakerVolume:vol];
// 获取麦克风音量
int micVolume = [engine.audioDevice getMicVolume];
// 获取扬声器音量
int speakerVolume = [engine.audioDevice getSpeakerVolume];
打开/关闭播放设备
- Unity
- Android
- iOS
// 打开
ResultCode code = TapRTC.GetAudioDevice().EnableAudioPlay(true);
// 关闭
ResultCode code = TapRTC.GetAudioDevice().EnableAudioPlay(false);
// 打开
boolean ok = TapRTCEngine.get().getAudioDevice().enableAudioPlay(true);
// 关闭
boolean ok = TapRTCEngine.get().getAudioDevice().enableAudioPlay(false);
// 打开
TapRTCResultCode code = [engine.audioDevice enableAudioPlay:YES];
// 关闭
TapRTCResultCode code = [engine.audioDevice enableAudioPlay:NO];
打开/关闭耳返
- Unity
- Android
- iOS
// 打开
ResultCode code = TapRTC.GetAudioDevice().EnableLoopback(true);
// 关闭
ResultCode code = TapRTC.GetAudioDevice().EnableLoopback(false);
// 打开
boolean ok = TapRTCEngine.get().getAudioDevice().enableLoopback(true);
// 关闭
boolean ok = TapRTCEngine.get().getAudioDevice().enableLoopback(false);
// 打开
TapRTCResultCode code = [engine.audioDevice enableLoopback:YES];
// 关闭
TapRTCResultCode code = [engine.audioDevice enableLoopback:NO];
范围语音
范围语音功能可以支持以下特性:
- 玩家附近一定范围内的其他小队玩家也能听到该玩家的语音;
- 在同一房间内,支持大量用户同时打开麦克风进行语音通话。
要使用范围语音功能,首先创建房间时需要指定启用范围语音功能:
- Unity
- Android
- iOS
bool enableRangeAudio = true;
var room = await TapRTC.AcquireRoom("roomId", enableRangeAudio);
import com.taptap.taprtc.TapRTCEngine;
import com.taptap.taprtc.RoomID;
RoomId roomId = new RoomID("roomId");
boolean enableRangeAudio = true;
TapRTCRoom room = TapRTCEngine.get().acquireRoom(roomId, enableRangeAudio);
TapRTCRoom *room = [engine acquireRangeAudioRoomWithRoomId:@"roomID"];
其次,玩家进入房间前,需要切换音频质量为 LOW:
- Unity
- Android
- iOS
room.ChangeRoomType(AudioPerfProfile.LOW);
// Java SDK 未提供切换音频质量的接口。
// Java SDK 下,如需使用范围语音功能,**SDK 初始化时音频质量需设置为 LOW**。
// Objective-C SDK 在进入范围语言房间时会自动把音质设为 LOW
接着设定小队号,并指定语音模式:
- 世界模式:距离当前玩家一定范围内的其他小队成员也可以听到该玩家的语音;
- 小队模式:仅小队成员之间可以互相通话。
无论哪种模式下,小队成员之间都能互相通话,不论距离远近。
- Unity
- Android
- iOS
int teamId = 12345678;
ResultCode code = room.GetRtcRangeAudioCtrl().SetRangeAudioTeam(teamId);
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
// 世界模式
ResultCode resultCode = room.GetRtcRangeAudioCtrl().SetRangeAudioMode(RangeAudioMode.WORLD);
if (resultCode == ResultCode.OK) {
// 成功
}
if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
// 小队模式
ResultCode resultCode = room.GetRtcRangeAudioCtrl().SetRangeAudioMode(RangeAudioMode.TEAM);
if (resultCode == ResultCode.OK) {
// 成功
}
if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
import com.taptap.taprtc.TapRTCRangeAudioCtrl.RangeAudioMode;
int teamId = 12345678;
ResultCode code = room.rangeAudioCtrl().setRangeAudioTeam(new TeamID(teamId));
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
// 世界模式
ResultCode resultCode = room.rangeAudioCtrl().setRangeAudioMode(RangeAudioMode.WORLD);
if (resultCode == ResultCode.OK) {
// 成功
}
if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
// 小队模式
ResultCode resultCode = room.rangeAudioCtrl().setRangeAudioMode(RangeAudioMode.TEAM);
if (resultCode == ResultCode.OK) {
// 成功
}
if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
int teamId = 12345678;
TapRTCResultCode resultCode = [room setRangeAudioTeamId:teamId];
// 世界模式
TapRTCResultCode code = [room setRangeAudioMode:TapRTCRangeAudioModeWorld];
TapRTCResultCode code = [room setRangeAudioMode:TapRTCRangeAudioModeTeam];
然后进入房间,并设置语音接收范围、更新声源方位,范围语音即可生效。
进入房间后,如需切换语音模式,可再次调用 SetRangeAudioMode
。
设置语音接收范围
语音接收范围控制世界模式下其他小队成员能否听到声音,在进房后调用,一般仅需设置一次。
- Unity
- Android
- iOS
int range = 300;
ResultCode code = room.GetRtcRangeAudioCtrl().UpdateAudioReceiverRange(range);
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
int range = 300;
ResultCode code = room.rangeAudioCtrl().updateAudioReceiverRange(range);
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
int range = 300;
TapRTCResultCode code = [room updateAudioReceiverRange:range];
范围(range
)之外的其他小队成员无法听到。
如果同时启用 3D 语音,那么距离远近还会影响音量大小:
音源距离 | 音量衰减 |
---|---|
N < range/10 | 1.0 (无衰减) |
N >= range/10 | range/10/N |
更新声源方位朝向
成功进入房间后,需要在 Unity 的 Update 方法中调用该接口更新声源方位、朝向,范围语音才能生效。 方位通过世界坐标系的前、右、上三个坐标指定,朝向通过自身坐标系的前、右、上轴的单位向量指定。
- Unity
- Android
- iOS
int x = 1;
int y = 2;
int z = 3;
Position position = new Position(x, y, z);
float[] axisForward = new float[3] {1.0, 0.0, 0.0};
float[] axisRight = new float[3] {0.0, 1.0, 0.0};
float[] axisUp = new float[3] {0.0, 0.0, 1.0};
Forward forward = new Forward(axisForward, axisRight, axisUp);
ResultCode code = room.GetRtcRangeAudioCtrl().UpdateSelfPosition(position, forward);
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
import com.taptap.taprtc.TapRTCRangeAudioCtrl.Position;
import com.taptap.taprtc.TapRTCRangeAudioCtrl.Forward;
int x = 1;
int y = 2;
int z = 3;
Position position = new Position(x, y, z);
float[] axisForward = {1.0, 0.0, 0.0};
float[] axisRight = {0.0, 1.0, 0.0};
float[] axisUp = {0.0, 0.0, 1.0};
Forward forward = new Forward(axisForward, axisRight, axisUp);
ResultCode code = room.rangeAudioCtrl().updateSelfPosition(position, forward);
if (code == ResultCode.OK) {
// 成功
}
if (code == ResultCode.ERROR_NOT_RANGE_ROOM) {
// 该房间未启用范围语音
}
TapRTCPosition position;
position.x = 1.0;
position.y = 2.0;
position.z = 3.0;
TapRTCAxis axisForward;
axisForward.x = 1.0;
axisForward.y = 0.0;
axisForward.z = 0.0;
TapRTCAxis axisRight;
axisRight.x = 0.0;
axisRight.y = 1.0;
axisRight.z = 0.0;
TapRTCAxis axisUp;
axisUp.x = 0.0;
axisUp.y = 0.0;
axisUp.z = 1.0;
TapRTCForward forward;
forward.forward = axisForward;
forward.rightward = axisRight;
forward.upward = axisUp;
TapRTCResultCode code = [room updateSelfPosition:position forward:forward];
朝向不影响是否能听到语音,因此如未启用 3D 语音,更新声源方位朝向时,朝向参数可随意设置。 但启用 3D 语音时,需正确设置朝向,这样才能得到准确的 3D 音效。
开关 3D 语音
开启 3D 语音,可以将无方位感的声音处理成带有声源方位感的声音,以增加玩家的沉浸感。 这个接口接受两个参数,第一个参数指定当前玩家是否可以听到 3D 音效,第二个参数指定 3D 语音是否作用于小队内部。
- Unity
- Android
- iOS
bool enable3D = true;
bool applyToTeam = true;
ResultCode code = TapRTC.GetAudioDevice().EnableSpatializer(enable3D, applyToTeam);
boolean enable3D = true;
boolean applyToTeam = true;
boolean ok = TapRTCEngine.get().getAudioDevice().enableSpatializer(enable3D, applyToTeam);
TapRTCResultCode code = [engine.audioDevice EnableSpatializer:YES applyTeam:YES];
错误码
上述文档中的部分操作会返回 ResultCode,代码示例中给出了部分常见的错误类型对应的错误码。 完整的错误码列表如下:
- Unity
- Android
- iOS
namespace TapTap.RTC
{
public enum ResultCode
{
OK = 0,
ERROR_UNKNOWN = 1,
ERROR_UNIMPLEMENTED = 2,
ERROR_NOT_ON_MAIN_THREAD = 3,
ERROR_INVAIDARGS = 4,
ERROR_NOT_INIT = 5,
ERROR_CONFIG_ERROR = 11,
ERROR_NET = 21,
ERROR_NET_TIMEOUT = 22,
ERROR_USER_NOT_EXIST = 101,
ERROR_ROOM_NOT_EXIST = 102,
ERROR_DEVICE_NOT_EXIST = 103,
ERROR_TEAM_ID_NOT_NULL = 104,
ERROR_ALREADY_IN_ROOM = 105,
ERROR_NO_PERMISSION = 106,
ERROR_AUTH_FAILED = 107,
ERROR_LIB_ERROR = 108,
ERROR_NOT_RANGE_ROOM = 109,
}
}
public enum ResultCode {
OK(0, "Success"),
ERROR_UNKNOWN(1, "Unknown Error"),
ERROR_UNIMPLEMENTED(2, "Unimplemented Functionality"),
ERROR_NOT_ON_MAIN_THREAD(3, "Not running on the main thread"),
ERROR_INVALID_ARGUMENT(4, "Invalid parameter"),
ERROR_NOT_INIT(5, "Uninitialized"),
ERROR_CONFIG_ERROR(11, "Configuration error"),
ERROR_NET(21, "Network error"),
ERROR_NET_TIMEOUT(22, "Network request timeout"),
ERROR_USER_NOT_EXIST(101, "User does not exist"),
ERROR_ROOM_NOT_EXIST(102, "Room does not exist"),
ERROR_DEVICE_NOT_EXIST(103, "Device does not exist"),
ERROR_TEAM_ID_NOT_NULL(104, "TeamID cannot be Zero"),
ERROR_ALREADY_IN_ROOM(105, "It's already in the other room"),
ERROR_NO_PERMISSION(106, "TapRTCCode_Error_NoPermission\tNo Permission"),
ERROR_AUTH_FAILED(107, "Authorization failure"),
ERROR_LIB_ERROR(108, "Service provider library error"),
ERROR_NOT_RANGE_ROOM(109, "Not Support Range Room"),
}
FOUNDATION_EXPORT NSString * const TapRTCNetworkErrorDomain;
FOUNDATION_EXPORT NSString * const TapRTCResultErrorDomain;
typedef NS_ENUM(NSInteger, TapRTCResultCode) {
TapRTCCode_OK = 0,
TapRTCCode_Error_Unknown = 1,
TapRTCCode_Error_Unimplemented = 2,
TapRTCCode_Error_NotOnMainThread,
TapRTCCode_Error_InvaidArgs,
TapRTCCode_Error_NotInit,
TapRTCCode_ConfigError_Error = 11,
TapRTCCode_ConfigError_AppID,
TapRTCCode_ConfigError_AppKey,
TapRTCCode_ConfigError_ServerUrl,
TapRTCCode_ConfigError_UserID,
TapRTCCode_ConfigError_DeviceID,
TapRTCCode_NetError_Error = 21,
TapRTCCode_NetError_Timeout,
TapRTCCode_Error_UserNotExist = 101,
TapRTCCode_Error_RoomNotExist,
TapRTCCode_Error_DeviceNotExist,
TapRTCCode_Error_TeamIDNotBeZero,
TapRTCCode_Error_AlreadyInOtherRoom,
TapRTCCode_Error_NoPermission,
TapRTCCode_Error_AuthFailed,
TapRTCCode_LibError,
};
服务端
为了保证聊天通道的安全性,实时语音服务需要搭配游戏自己的鉴权服务器使用。 此外,游戏自己的服务器还用于响应合规回调和调用剔除玩家的接口。
服务端鉴权
客户端加入房间前,需要先通过开发者自己的鉴权服务器获取签名,之后实时语音云端会验证该签名,只有附带有效签名的请求才会被执行,非法请求会被阻止。
- 客户端加入房间前,向游戏自己的鉴权服务器请求签名;
- 鉴权服务器根据下文所述的鉴权密钥算法生成签名返回给客户端;
- 客户端获得签名后,编码到请求中,发给实时语音服务器;
- 实时语音服务器对请求的内容和签名做一遍验证,验证通过后执行后续的实际操作。
签名采用 HMAC-SHA1 算法和 Base64 结合。 针对不同的请求,开发者生成不同的签名(参见后续格式说明),总体上,签名就是使用特定的密钥(在这里我们使用应用的 Master Key),对玩家和房间信息进行签名。
鉴权密钥算法
鉴权所用到的签名产生过程涉及到明文、密钥和加密算法。
明文
明文为以下字段的 json 字符串(字段顺序无关)
字段 | 类型/长度 | 说明 |
---|---|---|
userId | string | 进入语音聊天房间的用户标识 |
appId | string | 游戏的 Client ID |
expireAt | unsigned int/4 | 过期时间(当前时间+有效期(单位:秒, 建议值:300s)) |
roomId | string | 房间ID |
密钥
游戏的 Master Key
(即 Server Secret
)。
Client ID
和 Server Secret
都可在 开发者中心 > 你的游戏 > 游戏服务 > 应用配置 查看。
加密算法
加密算法采用 HMAC-SHA1 算法和 Base64 结合,类似 JWT 的格式。 生成的结果包含 payload(明文)和 sign(加密串)两部分。
按照上面表格中字段构造 JSON 字符串。
Base64 编码上一步的 JSON 字符串得到 payload。
通过 HMAC-SHA1 对 payload 用 密钥 生成 sign。
使用
.
(英文半角句号)拼接 payload 和 sign。
注意:JSON 字符串本身是字段顺序无关的,但是传递的 payload 和用于生成 sign 的 payload 的字段顺序必须相同,否则无法通过实时语音服务云端的校验。
下面给出 Java 和 Go 的示例代码供参考:
Java 示例
import com.google.gson.Gson;
import org.junit.Test;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.*;
import java.time.Instant;
import java.util.Base64;
public class JUnitTestSuite {
private static final String MAC_NAME = "HmacSHA1";
@Test
public void testToken() throws Exception {
String masterKey = "masterKey";
Token t = new Token();
t.appId = "appId";
t.userId ="user_test";
t.roomId ="room_test";;
int expTime = (int) Instant.now().getEpochSecond() + 5 * 60;
t.expireAt = expTime;
// server authBuff to your SDK Client
String authBuff = genToken(t, masterKey);
assertNotNull(authBuff);
}
private String genToken(Token token, String key) throws Exception {
Gson gson = new Gson();
String t = gson.toJson(token);
String payload = Base64.getEncoder().encodeToString(t.getBytes(StandardCharsets.UTF_8));
byte[] pEncryptOutBuf = hmacSHA1Encrypt(payload.getBytes(StandardCharsets.UTF_8), key);
String sign = Base64.getEncoder().encodeToString(pEncryptOutBuf);
return payload + "." + sign;
}
byte[] hmacSHA1Encrypt(byte[] text, String key) throws Exception {
byte[] data = key.getBytes(StandardCharsets.UTF_8);
SecretKey secretKey = new SecretKeySpec(data, MAC_NAME);
Mac mac = Mac.getInstance(MAC_NAME);
mac.init(secretKey);
return mac.doFinal(text);
}
class Token {
String userId;
String appId;
String roomId;
long expireAt;
}
}
Go 示例
package configs
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestToken(t *testing.T) {
assert := assert.New(t)
t1 := &Token{
UserId: "appId",
AppId: "user_test",
RoomId: "roomId_test",
ExpireAt: time.Now().Unix() + 5*60,
}
authBuff := GenToken(t1, "masterKey")
assert.NotEmpty(authBuff)
fmt.Println(authBuff)
}
const (
sep = "."
)
func GenToken(t *Token, masterKey string) string {
b, err := json.Marshal(t)
if err != nil {
return ""
}
payload := base64.StdEncoding.EncodeToString(b)
sign := base64.StdEncoding.EncodeToString(HmacSHA1(masterKey, payload))
return payload + sep + sign
}
func HmacSHA1(key string, data string) []byte {
mac := hmac.New(sha1.New, []byte(key))
mac.Write([]byte(data))
return mac.Sum(nil)
}
type Token struct {
UserId string `json:"userId,omitempty"`
AppId string `json:"appId,omitempty"`
RoomId string `json:"roomId,omitempty"`
ExpireAt int64 `json:"expireAt,omitempty"`
}
部署方式
由于加密密钥使用了 Server Secret
,加密算法的逻辑需在服务端实现,切勿在客户端部署加密方案。
使用方法
游戏自己的鉴权服务器生成加密串后,下发给客户端,客户端调用加入房间的接口时传入相应的鉴权信息。
C# SDK 还提供了一个 GenToken
工具方法,供客户端接入 SDK 测试开发时使用。
比如,客户端开发人员可以在等待服务端开发人员实现、部署相应接口前先行在客户端测试加入房间的功能。
再比如,客户端开发人员可以比较 SDK 自带的 GenToken
生成的加密串和服务端生成的加密串是否一致,以便验证服务端正确实现了加密算法。
var authBuffer = AuthBufferHelper.GenToken(appId, roomId, userId, masterKey);
注意,由于这个方法需要传入 Server Secret
作为参数,因此仅供内部测试开发时使用,切勿在对外提供的代码或安装包中使用。
如果担心因为人为失误导致对外提供的代码或安装包中泄漏 Server Secret
,或者出于安全考虑希望尽可能少的内部开发人员接触到 Server Secret
,建议不使用 SDK 提供的 GenToken
方法,内部测试开发时也同样通过游戏自己的鉴权服务器生成加密串。
合规回调
开发者在实时语音服务管理后台(开发者中心 > 你的游戏 > 游戏服务 > 实时语音>设置)设置回调地址后,语音内容有违规时会调用设置的回调地址。
回调地址需要是一个 HTTP(S) 协议接口的 URL,需要支持 POST 方法,传输数据编码采用 UTF-8。
回调的 POST body 示例:
{
"HitFlag":true,
"Msg":"不堪入目的消息内容",
"ScanFinishTime":1634893736,
"ScanStartTime":1634893734,
"Scenes":[
"default"
],
"VoiceFilterPiece":[
{
"Duration":14000,
"HitFlag":true,
"Info":"不堪入目的消息内容",
"MainType":"abuse",
"Offset":0,
"PieceStartTime":1634893734,
"RoomId":"1234",
"UserId":"123456",
"VoiceFilterDetail":[
{
"EndTime":0,
"KeyWord":"不堪入目的关键词",
"Label":"abuse",
"Rate":"0.00",
"StartTime":0
}
]
}
]
}
开发者可以在回调的Header中拿到 sign
字段,以便校验请求是否来自实时语音服务云端。
合规回调校验算法
在回调的 POST body 前附加
POST
前缀,得到 payload:POST{"HitFlag":true,"Msg":"不堪入目的消息内容",/* 略 */}
注意:
- 请直接从 HTTP 请求体中读取 JSON 内容,反序列化到程序语言的数据结构可能会改变字段顺序,导致校验失败
- POST 和 body 直接连接,中间无空格。
对 payload 进行 HMAC-SHA1 加密,密钥为游戏应用的
Server Secret
。对上一步的结果进行 BASE64 编码,得到 sign。
和回调的 HTTP 头中的 sign 字段的值进行比较,相同则表明请求来自实时语音服务云端。
下面给出 Go 的示例代码供参考:
Go 示例
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"github.com/labstack/echo/v4"
"io/ioutil"
"net/http"
)
func testCallback(c echo.Context) error {
sign := c.Request().Header.Get("sign")
body, _ := ioutil.ReadAll(c.Request().Body)
checkGMESign(sign, "yourMasterKey", string(body))
return c.NoContent(http.StatusOK)
}
func checkGMESign(signature, secretKey, body string) bool {
sign := genSign(secretKey, body)
return sign == signature
}
func genSign(secretKey, body string) string {
content := "POST" + body
a := hmacSHA1(secretKey, content)
return base64.StdEncoding.EncodeToString(a)
}
func hmacSHA1(key string, data string) []byte {
mac := hmac.New(sha1.New, []byte(key))
mac.Write([]byte(data))
return mac.Sum(nil)
}
剔除玩家
在一些场景下,游戏可能有从房间剔除玩家(踢人)的需求,比如涉及违规内容时。 开发者可以在自己的服务端调用实时语音服务的 REST API 接口实现这一需求。
请求格式
对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 application/json
。
请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,参数如下表:
Key | Value | 含义 | 来源 |
---|---|---|---|
X-LC-Id | {{appid}} | 当前应用的 App Id (即 Client Id ) | 可在控制台查看 |
X-LC-Key | {{masterkey}},master | 当前应用的 Master Key (即 Server Secret ) | 可在控制台查看 |
Base URL
REST API 请求的 Base URL(下文 curl 示例中用 {{host}}
表示)即应用的 API 自定义域名,可以在控制台绑定、查看。
详见文档关于域名的说明。
REST API
curl -X DELETE \
-H "Content-Type: application/json" \
-H "X-LC-Id: {{appId}}" \
-H "X-LC-Key: {{masterKey}},master" \
-d '{"roomId":"YOUR-ROOM-ID", "userId":"YOUR-USER-ID"}' \
https://{{host}}/rtc/v1/room/member
成功剔除时响应的 HTTP 状态码为 200
,出错时响应的 HTTP 状态码为相应的错误码,例如没有权限时 HTTP 状态码为 401.
注意,剔除玩家后,玩家可以重新加入房间,加入房间后又可以发言。 游戏还需在自己的鉴权服务器上实现对应的封禁逻辑,相应玩家重新加入房间时不下发签名,以阻止玩家通过重新加入房间的方式绕开限制。