David Liu

flesh out rtcclient

@@ -37,14 +37,12 @@ android { @@ -37,14 +37,12 @@ android {
37 37
38 protobuf { 38 protobuf {
39 protoc { 39 protoc {
40 - // You still need protoc like in the non-Android case  
41 artifact = "com.google.protobuf:protoc:${versions.protobuf}" 40 artifact = "com.google.protobuf:protoc:${versions.protobuf}"
42 } 41 }
43 generateProtoTasks { 42 generateProtoTasks {
44 all().each { task -> 43 all().each { task ->
45 task.builtins { 44 task.builtins {
46 java { 45 java {
47 - option "lite"  
48 } 46 }
49 } 47 }
50 } 48 }
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
3 import com.github.ajalt.timberkt.Timber 3 import com.github.ajalt.timberkt.Timber
  4 +import com.google.protobuf.util.JsonFormat
  5 +import livekit.Model
  6 +import livekit.Rtc
4 import okhttp3.Request 7 import okhttp3.Request
5 import okhttp3.Response 8 import okhttp3.Response
6 import okhttp3.WebSocket 9 import okhttp3.WebSocket
7 import okhttp3.WebSocketListener 10 import okhttp3.WebSocketListener
8 import okio.ByteString 11 import okio.ByteString
  12 +import org.webrtc.SessionDescription
9 import javax.inject.Inject 13 import javax.inject.Inject
10 14
11 internal class RTCClient 15 internal class RTCClient
12 @Inject 16 @Inject
13 constructor( 17 constructor(
14 private val websocketFactory: WebSocket.Factory, 18 private val websocketFactory: WebSocket.Factory,
  19 + private val fromJson: JsonFormat.Parser,
  20 + private val toJson: JsonFormat.Printer,
15 ) : WebSocketListener() { 21 ) : WebSocketListener() {
16 22
17 private var isConnected = false 23 private var isConnected = false
18 private var currentWs: WebSocket? = null 24 private var currentWs: WebSocket? = null
  25 + var listener: Listener? = null
  26 +
19 fun connect( 27 fun connect(
20 host: String, 28 host: String,
21 token: String, 29 token: String,
@@ -37,7 +45,21 @@ constructor( @@ -37,7 +45,21 @@ constructor(
37 } 45 }
38 46
39 override fun onMessage(webSocket: WebSocket, text: String) { 47 override fun onMessage(webSocket: WebSocket, text: String) {
40 - super.onMessage(webSocket, text) 48 + val signalResponseBuilder = Rtc.SignalResponse.newBuilder()
  49 + fromJson.merge(text, signalResponseBuilder)
  50 + val response = signalResponseBuilder.build()
  51 +
  52 + if (!isConnected) {
  53 + // Only handle joins if not connected.
  54 + if (response.hasJoin()) {
  55 + isConnected = true
  56 + listener?.onJoin(response.join)
  57 + } else {
  58 + Timber.e { "out of order message?" }
  59 + }
  60 + return
  61 + }
  62 + handleSignalResponse(response)
41 } 63 }
42 64
43 override fun onMessage(webSocket: WebSocket, bytes: ByteString) { 65 override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
@@ -56,4 +78,78 @@ constructor( @@ -56,4 +78,78 @@ constructor(
56 super.onFailure(webSocket, t, response) 78 super.onFailure(webSocket, t, response)
57 } 79 }
58 80
  81 +
  82 + fun fromProtoSessionDescription(sd: Rtc.SessionDescription): SessionDescription {
  83 + val rtcSdpType = when (sd.type) {
  84 + SD_TYPE_ANSWER -> SessionDescription.Type.ANSWER
  85 + SD_TYPE_OFFER -> SessionDescription.Type.OFFER
  86 + SD_TYPE_PRANSWER -> SessionDescription.Type.PRANSWER
  87 + else -> throw IllegalArgumentException("invalid RTC SdpType: ${sd.type}")
  88 + }
  89 + return SessionDescription(rtcSdpType, sd.sdp)
  90 + }
  91 +
  92 + fun toProtoSessionDescription(sdp: SessionDescription): Rtc.SessionDescription {
  93 + val sdBuilder = Rtc.SessionDescription.newBuilder()
  94 + sdBuilder.sdp = sdp.description
  95 + sdBuilder.type = when (sdp.type) {
  96 + SessionDescription.Type.ANSWER -> SD_TYPE_ANSWER
  97 + SessionDescription.Type.OFFER -> SD_TYPE_OFFER
  98 + SessionDescription.Type.PRANSWER -> SD_TYPE_PRANSWER
  99 + else -> throw IllegalArgumentException("invalid RTC SdpType: ${sdp.type}")
  100 + }
  101 +
  102 + return sdBuilder.build()
  103 + }
  104 +
  105 + fun handleSignalResponse(response: Rtc.SignalResponse) {
  106 + if (!isConnected) {
  107 + Timber.e { "Received response while not connected. ${toJson.print(response)}" }
  108 + return
  109 + }
  110 + when (response.messageCase) {
  111 + Rtc.SignalResponse.MessageCase.ANSWER -> {
  112 + val sd = fromProtoSessionDescription(response.answer)
  113 + listener?.onAnswer(sd)
  114 + }
  115 + Rtc.SignalResponse.MessageCase.OFFER -> {
  116 + val sd = fromProtoSessionDescription(response.offer)
  117 + listener?.onOffer(sd)
  118 + }
  119 +// Rtc.SignalResponse.MessageCase.TRICKLE -> {
  120 +// TODO()
  121 +// }
  122 + Rtc.SignalResponse.MessageCase.UPDATE -> {
  123 + listener?.onParticipantUpdate(response.update.participantsList)
  124 + }
  125 + Rtc.SignalResponse.MessageCase.TRACK_PUBLISHED -> {
  126 + listener?.onLocalTrackPublished(response.trackPublished)
  127 + }
  128 + Rtc.SignalResponse.MessageCase.SPEAKER -> TODO()
  129 + Rtc.SignalResponse.MessageCase.MESSAGE_NOT_SET -> TODO()
  130 + else -> {
  131 + Timber.v { "unhandled response type: ${response.messageCase.name}" }
  132 + }
  133 + }
  134 +
  135 + }
  136 +
  137 + interface Listener {
  138 + fun onJoin(info: Rtc.JoinResponse)
  139 +
  140 + fun onAnswer(sessionDescription: SessionDescription)
  141 + fun onOffer(sessionDescription: SessionDescription)
  142 +
  143 + //fun onTrickle(candidate: RTCIceCandidate, target: Rtc.SignalTarget)
  144 + fun onLocalTrackPublished(trackPublished: Rtc.TrackPublishedResponse)
  145 + fun onParticipantUpdate(updates: List<Model.ParticipantInfo>)
  146 + fun onClose(reason: String, code: Int)
  147 + fun onError(error: Error)
  148 + }
  149 +
  150 + companion object {
  151 + const val SD_TYPE_ANSWER = "answer"
  152 + const val SD_TYPE_OFFER = "offer"
  153 + const val SD_TYPE_PRANSWER = "pranswer"
  154 + }
59 } 155 }