David Zhao

Support protocol 2, DataPackets

@@ -231,7 +231,7 @@ constructor( @@ -231,7 +231,7 @@ constructor(
231 } 231 }
232 } 232 }
233 233
234 - fun handleSignalResponse(response: LivekitRtc.SignalResponse) { 234 + private fun handleSignalResponse(response: LivekitRtc.SignalResponse) {
235 if (!isConnected) { 235 if (!isConnected) {
236 // Only handle joins if not connected. 236 // Only handle joins if not connected.
237 if (response.hasJoin()) { 237 if (response.hasJoin()) {
@@ -307,7 +307,7 @@ constructor( @@ -307,7 +307,7 @@ constructor(
307 const val SD_TYPE_ANSWER = "answer" 307 const val SD_TYPE_ANSWER = "answer"
308 const val SD_TYPE_OFFER = "offer" 308 const val SD_TYPE_OFFER = "offer"
309 const val SD_TYPE_PRANSWER = "pranswer" 309 const val SD_TYPE_PRANSWER = "pranswer"
310 - const val PROTOCOL_VERSION = 1; 310 + const val PROTOCOL_VERSION = 2;
311 311
312 private fun iceServer(url: String) = 312 private fun iceServer(url: String) =
313 PeerConnection.IceServer.builder(url).createIceServer() 313 PeerConnection.IceServer.builder(url).createIceServer()
@@ -29,7 +29,7 @@ constructor( @@ -29,7 +29,7 @@ constructor(
29 val client: RTCClient, 29 val client: RTCClient,
30 private val pctFactory: PeerConnectionTransport.Factory, 30 private val pctFactory: PeerConnectionTransport.Factory,
31 @Named(InjectionNames.DISPATCHER_IO) ioDispatcher: CoroutineDispatcher, 31 @Named(InjectionNames.DISPATCHER_IO) ioDispatcher: CoroutineDispatcher,
32 -) : RTCClient.Listener { 32 +) : RTCClient.Listener, DataChannel.Observer {
33 33
34 var listener: Listener? = null 34 var listener: Listener? = null
35 var rtcConnected: Boolean = false 35 var rtcConnected: Boolean = false
@@ -41,7 +41,8 @@ constructor( @@ -41,7 +41,8 @@ constructor(
41 private val subscriberObserver = SubscriberTransportObserver(this) 41 private val subscriberObserver = SubscriberTransportObserver(this)
42 internal lateinit var publisher: PeerConnectionTransport 42 internal lateinit var publisher: PeerConnectionTransport
43 private lateinit var subscriber: PeerConnectionTransport 43 private lateinit var subscriber: PeerConnectionTransport
44 - private lateinit var privateDataChannel: DataChannel 44 + internal var reliableDataChannel: DataChannel? = null
  45 + internal var lossyDataChannel: DataChannel? = null
45 46
46 private val coroutineScope = CloseableCoroutineScope(SupervisorJob() + ioDispatcher) 47 private val coroutineScope = CloseableCoroutineScope(SupervisorJob() + ioDispatcher)
47 init { 48 init {
@@ -115,10 +116,13 @@ constructor( @@ -115,10 +116,13 @@ constructor(
115 fun onUpdateSpeakers(speakers: List<LivekitRtc.SpeakerInfo>) 116 fun onUpdateSpeakers(speakers: List<LivekitRtc.SpeakerInfo>)
116 fun onDisconnect(reason: String) 117 fun onDisconnect(reason: String)
117 fun onFailToConnect(error: Exception) 118 fun onFailToConnect(error: Exception)
  119 + fun onUserPacket(packet: LivekitRtc.UserPacket, kind: LivekitRtc.DataPacket.Kind)
118 } 120 }
119 121
120 companion object { 122 companion object {
121 - private const val PRIVATE_DATA_CHANNEL_LABEL = "_private" 123 + private const val RELIABLE_DATA_CHANNEL_LABEL = "_reliable"
  124 + private const val LOSSY_DATA_CHANNEL_LABEL = "_lossy"
  125 + internal const val MAX_DATA_PACKET_SIZE = 15000
122 126
123 private val OFFER_CONSTRAINTS = MediaConstraints().apply { 127 private val OFFER_CONSTRAINTS = MediaConstraints().apply {
124 with(mandatory) { 128 with(mandatory) {
@@ -136,6 +140,8 @@ constructor( @@ -136,6 +140,8 @@ constructor(
136 } 140 }
137 } 141 }
138 142
  143 + //---------------------------------- RTCClient.Listener --------------------------------------//
  144 +
139 override fun onJoin(info: LivekitRtc.JoinResponse) { 145 override fun onJoin(info: LivekitRtc.JoinResponse) {
140 val iceServers = mutableListOf<PeerConnection.IceServer>() 146 val iceServers = mutableListOf<PeerConnection.IceServer>()
141 for(serverInfo in info.iceServersList){ 147 for(serverInfo in info.iceServersList){
@@ -170,10 +176,21 @@ constructor( @@ -170,10 +176,21 @@ constructor(
170 publisher = pctFactory.create(rtcConfig, publisherObserver) 176 publisher = pctFactory.create(rtcConfig, publisherObserver)
171 subscriber = pctFactory.create(rtcConfig, subscriberObserver) 177 subscriber = pctFactory.create(rtcConfig, subscriberObserver)
172 178
173 - privateDataChannel = publisher.peerConnection.createDataChannel(  
174 - PRIVATE_DATA_CHANNEL_LABEL,  
175 - DataChannel.Init() 179 + val reliableInit = DataChannel.Init()
  180 + reliableInit.ordered = true
  181 + reliableDataChannel = publisher.peerConnection.createDataChannel(
  182 + RELIABLE_DATA_CHANNEL_LABEL,
  183 + reliableInit
  184 + )
  185 + reliableDataChannel!!.registerObserver(this)
  186 + val lossyInit = DataChannel.Init()
  187 + lossyInit.ordered = true
  188 + lossyInit.maxRetransmits = 1
  189 + lossyDataChannel = publisher.peerConnection.createDataChannel(
  190 + LOSSY_DATA_CHANNEL_LABEL,
  191 + lossyInit
176 ) 192 )
  193 +
177 coroutineScope.launch { 194 coroutineScope.launch {
178 val sdpOffer = 195 val sdpOffer =
179 when (val outcome = publisher.peerConnection.createOffer(OFFER_CONSTRAINTS)) { 196 when (val outcome = publisher.peerConnection.createOffer(OFFER_CONSTRAINTS)) {
@@ -302,4 +319,31 @@ constructor( @@ -302,4 +319,31 @@ constructor(
302 override fun onError(error: Exception) { 319 override fun onError(error: Exception) {
303 listener?.onFailToConnect(error) 320 listener?.onFailToConnect(error)
304 } 321 }
  322 +
  323 + //--------------------------------- DataChannel.Observer ------------------------------------//
  324 +
  325 + override fun onBufferedAmountChange(previousAmount: Long) {
  326 + }
  327 +
  328 + override fun onStateChange() {
  329 + }
  330 +
  331 + override fun onMessage(buffer: DataChannel.Buffer?) {
  332 + if (buffer == null) {
  333 + return
  334 + }
  335 + val dp = LivekitRtc.DataPacket.parseFrom(buffer.data)
  336 + when (dp.valueCase) {
  337 + LivekitRtc.DataPacket.ValueCase.SPEAKER -> {
  338 + listener?.onUpdateSpeakers(dp.speaker.speakersList)
  339 + }
  340 + LivekitRtc.DataPacket.ValueCase.USER -> {
  341 + listener?.onUserPacket(dp.user, dp.kind)
  342 + }
  343 + LivekitRtc.DataPacket.ValueCase.VALUE_NOT_SET,
  344 + null -> {
  345 + Timber.v { "invalid value for data packet" }
  346 + }
  347 + }
  348 + }
305 } 349 }
@@ -257,6 +257,17 @@ constructor( @@ -257,6 +257,17 @@ constructor(
257 /** 257 /**
258 * @suppress 258 * @suppress
259 */ 259 */
  260 + override fun onUserPacket(packet: LivekitRtc.UserPacket, kind: LivekitRtc.DataPacket.Kind) {
  261 + val participant = remoteParticipants[packet.participantSid] ?: return
  262 + val data = packet.payload.toByteArray()
  263 +
  264 + listener?.onDataReceived(data, participant, this)
  265 + participant.listener?.onDataReceived(data, participant)
  266 + }
  267 +
  268 + /**
  269 + * @suppress
  270 + */
260 override fun onDisconnect(reason: String) { 271 override fun onDisconnect(reason: String) {
261 Timber.v { "engine did disconnect: $reason" } 272 Timber.v { "engine did disconnect: $reason" }
262 handleDisconnect() 273 handleDisconnect()
@@ -333,17 +344,6 @@ constructor( @@ -333,17 +344,6 @@ constructor(
333 344
334 /** 345 /**
335 * @suppress 346 * @suppress
336 - */  
337 - override fun onDataReceived(  
338 - data: ByteBuffer,  
339 - dataTrack: DataTrack,  
340 - participant: RemoteParticipant  
341 - ) {  
342 - listener?.onDataReceived(data, dataTrack, participant, this)  
343 - }  
344 -  
345 - /**  
346 - * @suppress  
347 * // TODO(@dl): can this be moved out of Room/SDK? 347 * // TODO(@dl): can this be moved out of Room/SDK?
348 */ 348 */
349 fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) { 349 fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) {
@@ -440,9 +440,9 @@ interface RoomListener { @@ -440,9 +440,9 @@ interface RoomListener {
440 fun onTrackUnsubscribed(track: Track, publications: TrackPublication, participant: RemoteParticipant, room: Room) {} 440 fun onTrackUnsubscribed(track: Track, publications: TrackPublication, participant: RemoteParticipant, room: Room) {}
441 441
442 /** 442 /**
443 - * Message received over a [DataTrack] 443 + * Received data published by another participant
444 */ 444 */
445 - fun onDataReceived(data: ByteBuffer, dataTrack: DataTrack, participant: RemoteParticipant, room: Room) {} 445 + fun onDataReceived(data: ByteArray, participant: RemoteParticipant, room: Room) {}
446 } 446 }
447 447
448 sealed class RoomException(message: String? = null, cause: Throwable? = null) : 448 sealed class RoomException(message: String? = null, cause: Throwable? = null) :
@@ -2,13 +2,16 @@ package io.livekit.android.room.participant @@ -2,13 +2,16 @@ package io.livekit.android.room.participant
2 2
3 import android.content.Context 3 import android.content.Context
4 import com.github.ajalt.timberkt.Timber 4 import com.github.ajalt.timberkt.Timber
  5 +import com.google.protobuf.ByteString
5 import dagger.assisted.Assisted 6 import dagger.assisted.Assisted
6 import dagger.assisted.AssistedFactory 7 import dagger.assisted.AssistedFactory
7 import dagger.assisted.AssistedInject 8 import dagger.assisted.AssistedInject
8 import io.livekit.android.room.RTCEngine 9 import io.livekit.android.room.RTCEngine
9 import io.livekit.android.room.track.* 10 import io.livekit.android.room.track.*
10 import livekit.LivekitModels 11 import livekit.LivekitModels
  12 +import livekit.LivekitRtc
11 import org.webrtc.* 13 import org.webrtc.*
  14 +import java.nio.ByteBuffer
12 15
13 class LocalParticipant 16 class LocalParticipant
14 @AssistedInject 17 @AssistedInject
@@ -107,39 +110,6 @@ internal constructor( @@ -107,39 +110,6 @@ internal constructor(
107 publishListener?.onPublishSuccess(publication) 110 publishListener?.onPublishSuccess(publication)
108 } 111 }
109 112
110 - suspend fun publishDataTrack(  
111 - track: LocalDataTrack,  
112 - publishListener: PublishListener? = null  
113 - ) {  
114 - if (localTrackPublications.any { it.track == track }) {  
115 - publishListener?.onPublishFailure(TrackException.PublishException("Track has already been published"))  
116 - return  
117 - }  
118 -  
119 - // data track cid isn't ready until peer connection creates it, so we'll use name  
120 - val cid = track.name  
121 - val trackInfo =  
122 - engine.addTrack(cid = cid, name = track.name, track.kind)  
123 - val publication = LocalTrackPublication(trackInfo, track, this)  
124 -  
125 - val config = DataChannel.Init().apply {  
126 - ordered = track.options.ordered  
127 - maxRetransmitTimeMs = track.options.maxRetransmitTimeMs  
128 - maxRetransmits = track.options.maxRetransmits  
129 - }  
130 -  
131 - val dataChannel = engine.publisher.peerConnection.createDataChannel(track.name, config)  
132 - if (dataChannel == null) {  
133 - publishListener?.onPublishFailure(TrackException.PublishException("could not create data channel"))  
134 - return  
135 - }  
136 - track.dataChannel = dataChannel  
137 - track.updateConfig(config)  
138 - addTrackPublication(publication)  
139 -  
140 - publishListener?.onPublishSuccess(publication)  
141 - }  
142 -  
143 fun unpublishTrack(track: Track) { 113 fun unpublishTrack(track: Track) {
144 val publication = localTrackPublications.firstOrNull { it.track == track } 114 val publication = localTrackPublications.firstOrNull { it.track == track }
145 if (publication === null) { 115 if (publication === null) {
@@ -156,9 +126,47 @@ internal constructor( @@ -156,9 +126,47 @@ internal constructor(
156 LivekitModels.TrackType.AUDIO -> audioTracks.remove(sid) 126 LivekitModels.TrackType.AUDIO -> audioTracks.remove(sid)
157 LivekitModels.TrackType.VIDEO -> videoTracks.remove(sid) 127 LivekitModels.TrackType.VIDEO -> videoTracks.remove(sid)
158 LivekitModels.TrackType.DATA -> dataTracks.remove(sid) 128 LivekitModels.TrackType.DATA -> dataTracks.remove(sid)
  129 + else -> {}
159 } 130 }
160 } 131 }
161 132
  133 + /**
  134 + * Publish a new data payload to the room. Data will be forwarded to each participant in the room.
  135 + * Each payload must not exceed 15k in size
  136 + *
  137 + * @param data payload to send
  138 + * @param reliability for delivery guarantee, use RELIABLE. for fastest delivery without guarantee, use LOSSY
  139 + */
  140 + fun publishData(data: ByteArray, reliability: DataPublishReliability) {
  141 + if (data.size > RTCEngine.MAX_DATA_PACKET_SIZE) {
  142 + throw IllegalArgumentException("cannot publish data larger than " + RTCEngine.MAX_DATA_PACKET_SIZE)
  143 + }
  144 +
  145 + val kind = when (reliability) {
  146 + DataPublishReliability.RELIABLE -> LivekitRtc.DataPacket.Kind.RELIABLE
  147 + DataPublishReliability.LOSSY -> LivekitRtc.DataPacket.Kind.LOSSY
  148 + }
  149 + val channel = when (reliability) {
  150 + DataPublishReliability.RELIABLE -> engine.reliableDataChannel
  151 + DataPublishReliability.LOSSY -> engine.lossyDataChannel
  152 + } ?: throw TrackException.PublishException("data channel not established")
  153 +
  154 + val userPacket = LivekitRtc.UserPacket.newBuilder().
  155 + setPayload(ByteString.copyFrom(data)).
  156 + setParticipantSid(sid).
  157 + build()
  158 + val dataPacket = LivekitRtc.DataPacket.newBuilder().
  159 + setUser(userPacket).
  160 + setKind(kind).
  161 + build()
  162 + val buf = DataChannel.Buffer(
  163 + ByteBuffer.wrap(dataPacket.toByteArray()),
  164 + true,
  165 + )
  166 +
  167 + channel.send(buf)
  168 + }
  169 +
162 override fun updateFromInfo(info: LivekitModels.ParticipantInfo) { 170 override fun updateFromInfo(info: LivekitModels.ParticipantInfo) {
163 super.updateFromInfo(info) 171 super.updateFromInfo(info)
164 172
@@ -175,11 +183,11 @@ internal constructor( @@ -175,11 +183,11 @@ internal constructor(
175 track: T, 183 track: T,
176 sid: String 184 sid: String
177 ) where T : MediaTrack { 185 ) where T : MediaTrack {
178 - val senders = engine.publisher?.peerConnection?.senders ?: return 186 + val senders = engine.publisher.peerConnection.senders ?: return
179 for (sender in senders) { 187 for (sender in senders) {
180 val t = sender.track() ?: continue 188 val t = sender.track() ?: continue
181 if (t == track.rtcTrack) { 189 if (t == track.rtcTrack) {
182 - engine.publisher?.peerConnection?.removeTrack(sender) 190 + engine.publisher.peerConnection.removeTrack(sender)
183 } 191 }
184 } 192 }
185 } 193 }
@@ -161,12 +161,7 @@ interface ParticipantListener { @@ -161,12 +161,7 @@ interface ParticipantListener {
161 } 161 }
162 162
163 /** 163 /**
164 - * Data was received on a data track 164 + * Received data published by another participant
165 */ 165 */
166 - fun onDataReceived(  
167 - data: ByteBuffer,  
168 - dataTrack: DataTrack,  
169 - participant: RemoteParticipant  
170 - ) {  
171 - } 166 + fun onDataReceived(data: ByteArray, participant: RemoteParticipant) {}
172 } 167 }
@@ -144,8 +144,7 @@ class RemoteParticipant( @@ -144,8 +144,7 @@ class RemoteParticipant(
144 } 144 }
145 145
146 override fun onMessage(buffer: DataChannel.Buffer) { 146 override fun onMessage(buffer: DataChannel.Buffer) {
147 - internalListener?.onDataReceived(buffer.data, track, this@RemoteParticipant)  
148 - listener?.onDataReceived(buffer.data, track, this@RemoteParticipant) 147 +
149 } 148 }
150 }) 149 })
151 internalListener?.onTrackSubscribed(track, publication, participant = this) 150 internalListener?.onTrackSubscribed(track, publication, participant = this)
1 package io.livekit.android.room.track 1 package io.livekit.android.room.track
2 2
3 data class LocalTrackPublicationOptions(val placeholder: Unit) 3 data class LocalTrackPublicationOptions(val placeholder: Unit)
  4 +
  5 +enum class DataPublishReliability {
  6 + RELIABLE,
  7 + LOSSY,
  8 +}
@@ -12,40 +12,12 @@ open class Track( @@ -12,40 +12,12 @@ open class Track(
12 internal set 12 internal set
13 var kind = kind 13 var kind = kind
14 internal set 14 internal set
15 - var state: State = State.NONE  
16 var sid: String? = null 15 var sid: String? = null
17 internal set 16 internal set
18 17
19 - enum class State {  
20 - ENDED, LIVE, NONE;  
21 - }  
22 -  
23 open fun stop() { 18 open fun stop() {
24 // subclasses override to provide stop behavior 19 // subclasses override to provide stop behavior
25 } 20 }
26 -  
27 - companion object {  
28 - fun stateFromRTCMediaTrackState(trackState: MediaStreamTrack.State): State {  
29 - return when (trackState) {  
30 - MediaStreamTrack.State.ENDED -> State.ENDED  
31 - MediaStreamTrack.State.LIVE -> State.LIVE  
32 - }  
33 - }  
34 -  
35 - fun stateFromRTCDataChannelState(dataChannelState: DataChannel.State): State {  
36 - return when (dataChannelState) {  
37 - DataChannel.State.CONNECTING,  
38 - DataChannel.State.OPEN -> {  
39 - State.LIVE  
40 - }  
41 - DataChannel.State.CLOSING,  
42 - DataChannel.State.CLOSED -> {  
43 - State.ENDED  
44 - }  
45 - }  
46 - }  
47 - }  
48 -  
49 } 21 }
50 22
51 sealed class TrackException(message: String? = null, cause: Throwable? = null) : 23 sealed class TrackException(message: String? = null, cause: Throwable? = null) :