David Liu

LocalParticipant

1 <component name="ProjectCodeStyleConfiguration"> 1 <component name="ProjectCodeStyleConfiguration">
2 <code_scheme name="Project" version="173"> 2 <code_scheme name="Project" version="173">
3 - <AndroidXmlCodeStyleSettings>  
4 - <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />  
5 - </AndroidXmlCodeStyleSettings>  
6 <JetCodeStyleSettings> 3 <JetCodeStyleSettings>
  4 + <option name="PACKAGES_TO_USE_STAR_IMPORTS">
  5 + <value>
  6 + <package name="java.util" alias="false" withSubpackages="false" />
  7 + <package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
  8 + <package name="io.ktor" alias="false" withSubpackages="true" />
  9 + </value>
  10 + </option>
  11 + <option name="PACKAGES_IMPORT_LAYOUT">
  12 + <value>
  13 + <package name="" alias="false" withSubpackages="true" />
  14 + <package name="java" alias="false" withSubpackages="true" />
  15 + <package name="javax" alias="false" withSubpackages="true" />
  16 + <package name="kotlin" alias="false" withSubpackages="true" />
  17 + <package name="" alias="true" withSubpackages="true" />
  18 + </value>
  19 + </option>
7 <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> 20 <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
8 </JetCodeStyleSettings> 21 </JetCodeStyleSettings>
9 <codeStyleSettings language="XML"> 22 <codeStyleSettings language="XML">
  1 +<component name="InspectionProjectProfileManager">
  2 + <profile version="1.0">
  3 + <option name="myName" value="Project Default" />
  4 + <inspection_tool class="KotlinThrowableNotThrown" enabled="false" level="WARNING" enabled_by_default="false" />
  5 + </profile>
  6 +</component>
@@ -47,7 +47,7 @@ constructor( @@ -47,7 +47,7 @@ constructor(
47 47
48 private val publisherObserver = PublisherTransportObserver(this) 48 private val publisherObserver = PublisherTransportObserver(this)
49 private val subscriberObserver = SubscriberTransportObserver(this) 49 private val subscriberObserver = SubscriberTransportObserver(this)
50 - private val publisher: PeerConnectionTransport 50 + internal val publisher: PeerConnectionTransport
51 private val subscriber: PeerConnectionTransport 51 private val subscriber: PeerConnectionTransport
52 52
53 private var privateDataChannel: DataChannel 53 private var privateDataChannel: DataChannel
@@ -4,6 +4,7 @@ import dagger.assisted.Assisted @@ -4,6 +4,7 @@ import dagger.assisted.Assisted
4 import dagger.assisted.AssistedFactory 4 import dagger.assisted.AssistedFactory
5 import dagger.assisted.AssistedInject 5 import dagger.assisted.AssistedInject
6 import io.livekit.android.ConnectOptions 6 import io.livekit.android.ConnectOptions
  7 +import io.livekit.android.room.participant.LocalParticipant
7 import io.livekit.android.room.participant.RemoteParticipant 8 import io.livekit.android.room.participant.RemoteParticipant
8 9
9 class Room 10 class Room
@@ -13,7 +14,26 @@ constructor( @@ -13,7 +14,26 @@ constructor(
13 private val engine: RTCEngine, 14 private val engine: RTCEngine,
14 ) { 15 ) {
15 16
  17 + enum class State {
  18 + CONNECTING,
  19 + CONNECTED,
  20 + DISCONNECTED,
  21 + RECONNECTING;
  22 + }
  23 +
  24 + inline class Sid(val sid: String)
  25 +
16 var listener: Listener? = null 26 var listener: Listener? = null
  27 +
  28 + var sid: Sid? = null
  29 + private set
  30 + var name: String? = null
  31 + private set
  32 + var state: State = State.DISCONNECTED
  33 + private set
  34 + var localParticipant: LocalParticipant? = TODO()
  35 +
  36 +
17 suspend fun connect(url: String, token: String, isSecure: Boolean) { 37 suspend fun connect(url: String, token: String, isSecure: Boolean) {
18 engine.join(url, token, isSecure) 38 engine.join(url, token, isSecure)
19 } 39 }
@@ -23,7 +43,6 @@ constructor( @@ -23,7 +43,6 @@ constructor(
23 fun create(connectOptions: ConnectOptions): Room 43 fun create(connectOptions: ConnectOptions): Room
24 } 44 }
25 45
26 -  
27 interface Listener { 46 interface Listener {
28 fun onConnect(room: Room) 47 fun onConnect(room: Room)
29 fun onDisconnect(room: Room, error: Exception) 48 fun onDisconnect(room: Room, error: Exception)
  1 +package io.livekit.android.room.participant
  2 +
  3 +import com.github.ajalt.timberkt.Timber
  4 +import io.livekit.android.room.RTCEngine
  5 +import io.livekit.android.room.track.*
  6 +import livekit.Model
  7 +import org.webrtc.DataChannel
  8 +import org.webrtc.RtpTransceiver
  9 +
  10 +class LocalParticipant(sid: Sid, name: String? = null) :
  11 + Participant(sid, name) {
  12 + constructor(info: Model.ParticipantInfo, engine: RTCEngine) : this(
  13 + Sid(info.sid),
  14 + info.identity
  15 + ) {
  16 + metadata = info.metadata
  17 + this.engine = engine
  18 + }
  19 +
  20 + private val streamId = "stream"
  21 +
  22 + val localAudioTrackPublications
  23 + get() = audioTracks.values.toList()
  24 + val localVideoTrackPublications
  25 + get() = videoTracks.values.toList()
  26 + val localDataTrackPublications
  27 + get() = dataTracks.values.toList()
  28 +
  29 + var engine: RTCEngine? = null
  30 + val listener: Listener? = null
  31 +
  32 + suspend fun publishAudioTrack(
  33 + track: LocalAudioTrack,
  34 + options: LocalTrackPublicationOptions? = null
  35 + ) {
  36 + if (localAudioTrackPublications.any { it.track == track }) {
  37 + listener?.onFailToPublishAudioTrack(TrackException.PublishException("Track has already been published"))
  38 + return
  39 + }
  40 +
  41 + val cid = track.rtcTrack.id()
  42 + val engine = this.engine ?: run {
  43 + listener?.onFailToPublishAudioTrack(IllegalStateException("engine is null!"))
  44 + return
  45 + }
  46 +
  47 + val trackInfo =
  48 + engine.addTrack(cid = Track.Cid(cid), name = track.name, kind = Model.TrackType.AUDIO)
  49 + val transInit = RtpTransceiver.RtpTransceiverInit(
  50 + RtpTransceiver.RtpTransceiverDirection.SEND_ONLY,
  51 + listOf(streamId)
  52 + )
  53 + val transceiver =
  54 + engine.publisher.peerConnection.addTransceiver(track.rtcTrack, transInit)
  55 +
  56 + if (transceiver == null) {
  57 + listener?.onFailToPublishAudioTrack(TrackException.PublishException("null sender returned from peer connection"))
  58 + return
  59 + }
  60 +
  61 + val publication = LocalAudioTrackPublication(trackInfo)
  62 + val trackSid = Track.Sid(trackInfo.sid)
  63 + track.sid = trackSid
  64 + audioTracks[trackSid] = publication
  65 + listener?.onPublishAudioTrack(track)
  66 + }
  67 +
  68 + suspend fun publishVideoTrack(
  69 + track: LocalVideoTrack,
  70 + options: LocalTrackPublicationOptions? = null
  71 + ) {
  72 +
  73 + if (localVideoTrackPublications.any { it.track == track }) {
  74 + listener?.onFailToPublishVideoTrack(TrackException.PublishException("Track has already been published"))
  75 + return
  76 + }
  77 +
  78 + val cid = track.rtcTrack.id()
  79 + val engine = this.engine ?: run {
  80 + listener?.onFailToPublishVideoTrack(IllegalStateException("engine is null!"))
  81 + return
  82 + }
  83 +
  84 + val trackInfo =
  85 + engine.addTrack(cid = Track.Cid(cid), name = track.name, kind = Model.TrackType.VIDEO)
  86 + val transInit = RtpTransceiver.RtpTransceiverInit(
  87 + RtpTransceiver.RtpTransceiverDirection.SEND_ONLY,
  88 + listOf(streamId)
  89 + )
  90 + val transceiver =
  91 + engine.publisher.peerConnection.addTransceiver(track.rtcTrack, transInit)
  92 +
  93 + if (transceiver == null) {
  94 + listener?.onFailToPublishVideoTrack(TrackException.PublishException("null sender returned from peer connection"))
  95 + return
  96 + }
  97 +
  98 + val publication = LocalVideoTrackPublication(trackInfo)
  99 + val trackSid = Track.Sid(trackInfo.sid)
  100 + track.sid = trackSid
  101 + videoTracks[trackSid] = publication
  102 + listener?.onPublishVideoTrack(track)
  103 + }
  104 +
  105 + suspend fun publishDataTrack(
  106 + track: LocalDataTrack,
  107 + options: LocalTrackPublicationOptions? = null
  108 + ) {
  109 +
  110 + if (localDataTrackPublications.any { it.track == track }) {
  111 + listener?.onFailToPublishDataTrack(TrackException.PublishException("Track has already been published"))
  112 + return
  113 + }
  114 +
  115 + val cid = track.cid
  116 + val engine = this.engine ?: run {
  117 + listener?.onFailToPublishDataTrack(IllegalStateException("engine is null!"))
  118 + return
  119 + }
  120 +
  121 + val trackInfo =
  122 + engine.addTrack(cid = cid, name = track.name, kind = Model.TrackType.DATA)
  123 + val publication = LocalDataTrackPublication(trackInfo, track)
  124 + val trackSid = Track.Sid(trackInfo.sid)
  125 + track.sid = trackSid
  126 +
  127 + val config = DataChannel.Init().apply {
  128 + ordered = track.options.ordered
  129 + maxRetransmitTimeMs = track.options.maxRetransmitTimeMs
  130 + maxRetransmits = track.options.maxRetransmits
  131 + }
  132 +
  133 + val dataChannel = engine.publisher.peerConnection.createDataChannel(track.name, config)
  134 + if (dataChannel != null) {
  135 + track.rtcTrack = dataChannel
  136 + track.updateConfig(config)
  137 + dataTracks[trackSid] = publication
  138 + listener?.onPublishDataTrack(track)
  139 + } else {
  140 + Timber.d { "error creating data channel with name: $name" }
  141 + unpublishDataTrack(track)
  142 + }
  143 + }
  144 +
  145 + fun unpublishAudioTrack(track: LocalAudioTrack) {
  146 + val sid = track.sid ?: run {
  147 + Timber.d { "this track was never published." }
  148 + return
  149 + }
  150 + unpublishMediaTrack(track, sid, audioTracks)
  151 + }
  152 +
  153 + fun unpublishVideoTrack(track: LocalVideoTrack) {
  154 + val sid = track.sid ?: run {
  155 + Timber.d { "this track was never published." }
  156 + return
  157 + }
  158 + unpublishMediaTrack(track, sid, audioTracks)
  159 + }
  160 +
  161 + fun unpublishDataTrack(track: LocalDataTrack) {
  162 +
  163 + val sid = track.sid ?: run {
  164 + Timber.d { "this track was never published." }
  165 + return
  166 + }
  167 +
  168 + val publication = dataTracks.remove(sid) as? LocalDataTrackPublication
  169 + if (publication == null) {
  170 + Timber.d { "track was not published with sid: $sid" }
  171 + return
  172 + }
  173 + publication.dataTrack?.rtcTrack?.dispose()
  174 + }
  175 +
  176 + private fun <T> unpublishMediaTrack(
  177 + track: T,
  178 + sid: Track.Sid,
  179 + publications: MutableMap<Track.Sid, TrackPublication>
  180 + ) where T : Track, T : MediaTrack {
  181 + val removed = publications.remove(sid)
  182 + if (removed != null) {
  183 + Timber.d { "track was not published with sid: $sid" }
  184 + return
  185 + }
  186 +
  187 + track.mediaTrack.setEnabled(false)
  188 + val senders = engine?.publisher?.peerConnection?.senders ?: return
  189 + for (sender in senders) {
  190 + val t = sender.track() ?: continue
  191 + if (t == track.mediaTrack) {
  192 + engine?.publisher?.peerConnection?.removeTrack(sender)
  193 + }
  194 + }
  195 + }
  196 +
  197 + interface Listener {
  198 +
  199 + fun onPublishAudioTrack(track: LocalAudioTrack)
  200 + fun onFailToPublishAudioTrack(exception: Exception)
  201 + fun onPublishVideoTrack(track: LocalVideoTrack)
  202 + fun onFailToPublishVideoTrack(exception: Exception)
  203 + fun onPublishDataTrack(track: LocalDataTrack)
  204 + fun onFailToPublishDataTrack(exception: Exception)
  205 + //fun onNetworkQualityLevelChange
  206 + }
  207 +
  208 +}
@@ -2,7 +2,7 @@ package io.livekit.android.room.participant @@ -2,7 +2,7 @@ package io.livekit.android.room.participant
2 2
3 import io.livekit.android.room.track.* 3 import io.livekit.android.room.track.*
4 4
5 -open class Participant(var sid: String, name: String? = null) { 5 +open class Participant(var sid: Sid, name: String? = null) {
6 inline class Sid(val sid: String) 6 inline class Sid(val sid: String)
7 7
8 var metadata: String? = null 8 var metadata: String? = null
@@ -14,10 +14,10 @@ import org.webrtc.VideoTrack @@ -14,10 +14,10 @@ import org.webrtc.VideoTrack
14 import java.nio.ByteBuffer 14 import java.nio.ByteBuffer
15 15
16 class RemoteParticipant( 16 class RemoteParticipant(
17 - sid: String, name: String? = null 17 + sid: Sid, name: String? = null
18 ) : Participant(sid, name), RemoteDataTrack.Listener { 18 ) : Participant(sid, name), RemoteDataTrack.Listener {
19 19
20 - constructor(info: Model.ParticipantInfo) : this(info.sid, info.identity) { 20 + constructor(info: Model.ParticipantInfo) : this(Sid(info.sid), info.identity) {
21 updateFromInfo(info) 21 updateFromInfo(info)
22 } 22 }
23 23
@@ -42,7 +42,7 @@ class RemoteParticipant( @@ -42,7 +42,7 @@ class RemoteParticipant(
42 42
43 fun updateFromInfo(info: Model.ParticipantInfo) { 43 fun updateFromInfo(info: Model.ParticipantInfo) {
44 val hadInfo = hasInfo 44 val hadInfo = hasInfo
45 - sid = info.sid 45 + sid = Sid(info.sid)
46 name = info.identity 46 name = info.identity
47 participantInfo = info 47 participantInfo = info
48 metadata = info.metadata 48 metadata = info.metadata
@@ -4,14 +4,27 @@ import org.webrtc.DataChannel @@ -4,14 +4,27 @@ import org.webrtc.DataChannel
4 4
5 open class DataTrack( 5 open class DataTrack(
6 name: String, 6 name: String,
7 - val rtcTrack: DataChannel  
8 -) : Track(name, stateFromRTCDataChannelState(rtcTrack.state())) { 7 + var rtcTrack: DataChannel? = null
  8 +) : Track(
  9 + name,
  10 + run {
  11 + if (rtcTrack != null) {
  12 + stateFromRTCDataChannelState(rtcTrack.state())
  13 + } else {
  14 + State.NONE
  15 + }
  16 + }) {
9 17
10 - /**  
11 - * TODO: These values are only available at [DataChannel.Init]  
12 - */  
13 - val ordered: Boolean = TODO()  
14 - val maxPacketLifeTime: Int = TODO()  
15 - val maxRetransmits: Int = TODO()  
16 - 18 + var ordered: Boolean = TODO()
  19 + private set
  20 + var maxRetransmitTimeMs: Int = TODO()
  21 + private set
  22 + var maxRetransmits: Int = TODO()
  23 + private set
  24 +
  25 + fun updateConfig(config: DataChannel.Init) {
  26 + ordered = config.ordered
  27 + maxRetransmitTimeMs = config.maxRetransmitTimeMs
  28 + maxRetransmits = config.maxRetransmits
  29 + }
17 } 30 }
@@ -2,7 +2,7 @@ package io.livekit.android.room.track @@ -2,7 +2,7 @@ package io.livekit.android.room.track
2 2
3 data class DataTrackOptions( 3 data class DataTrackOptions(
4 val ordered: Boolean = true, 4 val ordered: Boolean = true,
5 - val maxPacketLifetime: Int = -1, 5 + val maxRetransmitTimeMs: Int = -1,
6 val maxRetransmits: Int = -1, 6 val maxRetransmits: Int = -1,
7 val name: String 7 val name: String
8 ) 8 )
  1 +package io.livekit.android.room.track
  2 +
  3 +import livekit.Model
  4 +
  5 +class LocalAudioTrackPublication(info: Model.TrackInfo, track: Track? = null) :
  6 + LocalTrackPublication(info, track), AudioTrackPublication {
  7 + override val audioTrack: AudioTrack?
  8 + get() = track as? AudioTrack
  9 +}
@@ -2,6 +2,7 @@ package io.livekit.android.room.track @@ -2,6 +2,7 @@ package io.livekit.android.room.track
2 2
3 import org.webrtc.DataChannel 3 import org.webrtc.DataChannel
4 import java.nio.ByteBuffer 4 import java.nio.ByteBuffer
  5 +import java.util.*
5 6
6 class LocalDataTrack( 7 class LocalDataTrack(
7 val options: DataTrackOptions, 8 val options: DataTrackOptions,
@@ -9,15 +10,16 @@ class LocalDataTrack( @@ -9,15 +10,16 @@ class LocalDataTrack(
9 ) : DataTrack(options.name, rtcTrack) { 10 ) : DataTrack(options.name, rtcTrack) {
10 var sid: Sid? = null 11 var sid: Sid? = null
11 internal set 12 internal set
  13 + var cid: Cid = Cid(UUID.randomUUID().toString())
12 14
13 fun sendString(message: String) { 15 fun sendString(message: String) {
14 val byteBuffer = ByteBuffer.wrap(message.toByteArray()) 16 val byteBuffer = ByteBuffer.wrap(message.toByteArray())
15 val buffer = DataChannel.Buffer(byteBuffer, false) 17 val buffer = DataChannel.Buffer(byteBuffer, false)
16 - rtcTrack.send(buffer) 18 + rtcTrack?.send(buffer)
17 } 19 }
18 20
19 fun sendBytes(byteBuffer: ByteBuffer) { 21 fun sendBytes(byteBuffer: ByteBuffer) {
20 val buffer = DataChannel.Buffer(byteBuffer, true) 22 val buffer = DataChannel.Buffer(byteBuffer, true)
21 - rtcTrack.send(buffer) 23 + rtcTrack?.send(buffer)
22 } 24 }
23 } 25 }
  1 +package io.livekit.android.room.track
  2 +
  3 +import livekit.Model
  4 +
  5 +class LocalDataTrackPublication(info: Model.TrackInfo, track: Track? = null) :
  6 + LocalTrackPublication(info, track), DataTrackPublication {
  7 + override val dataTrack: DataTrack?
  8 + get() = track as? DataTrack
  9 +}
  1 +package io.livekit.android.room.track
  2 +
  3 +import livekit.Model
  4 +
  5 +open class LocalTrackPublication(info: Model.TrackInfo, track: Track? = null) :
  6 + TrackPublication(info, track) {
  7 + val localTrack
  8 + get() = track
  9 + var priority = Track.Priority.STANDARD
  10 + private set
  11 +}
  1 +package io.livekit.android.room.track
  2 +
  3 +data class LocalTrackPublicationOptions(val priority: Track.Priority)
  1 +package io.livekit.android.room.track
  2 +
  3 +import livekit.Model
  4 +
  5 +class LocalVideoTrackPublication(info: Model.TrackInfo, track: Track? = null) :
  6 + LocalTrackPublication(info, track), VideoTrackPublication {
  7 + override val videoTrack: VideoTrack?
  8 + get() = track as? VideoTrack
  9 +}