TiRTC Android Integration Guide
This guide explains how to integrate the TiRTC SDK into an Android project and complete connection setup, remote audio-video playback, stream messaging, and troubleshooting.
Official Demo
If you want a standalone sample project you can build directly, start with the official open-source Android demo repository: tangeai/tirtc-example-android.
Prerequisites
Confirm the following before integration starts:
| Item | Requirement |
|---|---|
| Android version | Android 5.0 (API Level 21) or higher |
| Supported ABI | arm64-v8a only |
| Android Studio | No hard requirement; latest stable version recommended |
| Gradle | No hard requirement; version 8.0 or newer recommended |
| Java | No hard requirement; version 17 or newer recommended, aligned with Gradle requirements |
| Android Gradle Plugin | No hard requirement; version 8.1.1 or newer recommended |
compileSdk | 35 |
Keep your toolchain versions as close as possible to the official samples.
Get the SDK
Get the SDK version from the Android SDK Release Notes, then replace <latest-version> in the example below.
pluginManagement {
repositories {
maven {
url = uri("http://repo-sdk.tange-ai.com/repository/maven-public/")
isAllowInsecureProtocol = true
credentials {
username = "tange_user"
password = "tange_user"
}
}
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencies {
implementation("com.tange.ai:tirtc-av:<latest-version>")
}Declare Permissions
AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />Initialize the SDK
Initialize the TiRTC runtime before calling any other SDK API.
class DemoApp : Application() {
override fun onCreate() {
super.onCreate()
TiRtc.initialize(
TiRtc.Config.Builder(this)
.setEnableConsoleLog(BuildConfig.DEBUG)
.build(),
)
}
}Get a Connection Token
A connection token is a short-lived credential used to verify that the client is allowed to connect to TiRTC.
Before you connect, prepare peerId and then fetch the token required for this connection.
There are two common ways to get a token:
- During development and debugging, generate it with TiRTC DevTools CLI
- In production, generate it in your own backend according to the documented signing rule. See Generate a Connection Token
TiRTC DevTools CLI is only suitable for development and local debugging. It is not a production delivery strategy.
In production, your business backend should sign and issue the token, and the client should only request the result.
Do not store accessId, secretKey, or any other signing credentials in the client app. These are server-side secrets, and leakage is a serious security risk.
Establish a Connection
Use TiRtcConn to create one connection to the remote peer. Commands, stream messages, and audio-video bindings all operate around this connection.
The following examples assume a single PlayerActivity:
class PlayerActivity : AppCompatActivity() {
private val conn = TiRtcConn()
private val peerId = "your-peer-id"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
conn.setObserver(createConnObserver())
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
val token = yourBackend.fetchTiRtcToken(peerId = peerId)
conn.connect(peerId, token)
}
}
override fun onPause() {
super.onPause()
conn.disconnect()
}
override fun onDestroy() {
super.onDestroy()
releaseRtc()
}
}peerIdcan come from your device list, fixed config, or business-provided datatokenshould be fetched right before connecting, not hard-coded in the clientyourBackend.fetchTiRtcToken(...)is only a placeholder. The real implementation depends on your backend API
private fun createConnObserver() =
object : TiRtcConn.Observer {
override fun onStateChanged(state: TiRtcConn.State) {
println("conn state=$state")
}
override fun onDisconnected(errorCode: Int) {
println("conn disconnected error=$errorCode")
}
override fun onRemoteCommandRequest(
remoteRequestId: Long,
command: RtcConnCommand,
) {
replyRemoteCommand(remoteRequestId, command)
}
override fun onStreamMessageReceived(
streamId: Int,
message: RtcStreamMessage,
) {
val text = message.data.decodeToString()
println(
"stream message received streamId=$streamId ts=${message.timestampMs} text=$text",
)
}
override fun onError(code: Int, message: String) {
println("conn error code=$code message=$message")
}
}connrepresents this connectiononResume()establishes the connectiononPause()disconnects itonDestroy()callsreleaseRtc()to release object resourcesreleaseRtc()is defined later in the Release Resources section- Audio and video objects are added in the next section
Play Remote Audio and Video
Before you can play remote media, prepare a video container, an audio output, a video output, and the corresponding streamId values.
Prepare the Video Container
<FrameLayout
android:id="@+id/remote_video_container"
android:layout_width="match_parent"
android:layout_height="240dp" />Add the Media Members to the Activity
private val audioOutput = TiRtcAudioOutput()
private val videoOutput = TiRtcVideoOutput()
private lateinit var remoteVideoContainer: FrameLayout
private val audioStreamId = 1
private val videoStreamId = 2
private val messageStreamId = 3audioOutputplays remote audiovideoOutputrenders remote videomessageStreamIdis used for custom business stream messages and must not reuse the audio or videostreamId
Bind the Outputs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
// ...
bindRemoteMedia()
}private fun bindRemoteMedia() {
videoOutput.attach(remoteVideoContainer)
conn.attachAudioOutput(audioStreamId, audioOutput)
conn.attachVideoOutput(videoStreamId, videoOutput)
}When the remote media arrives and the streamId matches, audio starts playing and video is rendered into the container.
Kotlin examples use the TiRtcConn extension methods directly. If you use Java, see the API Reference for the binding-based API shape.
audioStreamId and videoStreamId
Audio and video use separate streamId values.
audioStreamIdbinds the remote audio streamvideoStreamIdbinds the remote video stream- These values usually come from the remote capability contract or your business configuration
Send and Receive Stream Messages
Stream messages are suitable for lightweight business data and are always attached to a specific streamId.
Receive Stream Messages
override fun onStreamMessageReceived(
streamId: Int,
message: RtcStreamMessage,
) {
val text = message.data.decodeToString()
println(
"stream message received streamId=$streamId ts=${message.timestampMs} text=$text",
)
}Send a Stream Message
private fun sendStreamPing() {
conn.sendStreamMessage(
streamId = messageStreamId,
timestampMs = System.currentTimeMillis(),
data = "hello".encodeToByteArray(),
)
}Send Commands
Commands are used to control remote behavior or to send a request and wait for a response.
First define the command IDs agreed between your client and the remote side:
private val toggleLedCommandId = 0x1001
private val getTimeCommandId = 0x1002These commandId values must be agreed in your peer protocol and stay within 0x1000..0x7FFF. Values below 0x1000 are reserved for internal runtime commands.
One-Way Command
private fun sendToggleCommand() {
conn.sendCommand(toggleLedCommandId, "toggle_led".encodeToByteArray())
}Request-Response Command
private fun requestRemoteTime() {
conn.requestCommand(
commandId = getTimeCommandId,
data = "get_time".encodeToByteArray(),
timeoutMs = 3_000,
callback = object : TiRtcConn.CommandCallback {
override fun onSuccess(response: RtcConnCommandResponse) {
println(
"request success commandId=${response.commandId} payload=${response.data.decodeToString()}",
)
}
override fun onFailure(code: Int, message: String) {
println("request failed code=$code message=$message")
}
},
)
}Reply to a Remote Request
private fun replyRemoteCommand(
remoteRequestId: Long,
command: RtcConnCommand,
) {
conn.replyRemoteCommand(
remoteRequestId = remoteRequestId,
commandId = command.commandId,
data = "ok".encodeToByteArray(),
)
}Release Resources
When the page exits or these objects are no longer needed, release them in the reverse direction of creation.
private fun releaseRtc() {
conn.detachAudioOutput(audioStreamId)
conn.detachVideoOutput(videoStreamId)
videoOutput.detach()
audioOutput.destroy()
videoOutput.destroy()
conn.destroy()
}Troubleshooting Starts Here
Cannot Connect
If the connection cannot be established, check these items first:
- Is
peerIdcorrect - Has the
tokenexpired or does it not match the target - Has the app declared the
INTERNETpermission - Did you call
TiRtc.initialize(...)before using the SDK
Then inspect the callbacks for direct clues:
onError(...)onDisconnected(...)
Upload Logs
When you need help diagnosing an issue, call TiRtcDebugging.uploadLogs(...) to upload logs.
val code =
TiRtcDebugging.uploadLogs(
object : TiRtcDebugging.UploadLogsCallback {
override fun onSuccess(logId: String) {
println("upload logs success logId=$logId")
}
override fun onFailure(code: Int, message: String) {
println("upload logs failed code=$code message=$message")
}
},
)
println("uploadLogs submit code=$code")When reporting an issue, it is best to include:
- The time when the problem occurred
- Device model and Android version
- The connection target
- Whether the issue is connection failure, no audio, no video, or a command problem