davidliu
Committed by GitHub

Implement Track getStats method (#227)

* Implement Track getStats method

* Fix tests
正在显示 18 个修改的文件 包含 217 行增加51 行删除
@@ -70,12 +70,14 @@ internal constructor( @@ -70,12 +70,14 @@ internal constructor(
70 listener?.onEngineReconnected() 70 listener?.onEngineReconnected()
71 } 71 }
72 } 72 }
  73 +
73 ConnectionState.DISCONNECTED -> { 74 ConnectionState.DISCONNECTED -> {
74 LKLog.d { "primary ICE disconnected" } 75 LKLog.d { "primary ICE disconnected" }
75 if (oldVal == ConnectionState.CONNECTED) { 76 if (oldVal == ConnectionState.CONNECTED) {
76 reconnect() 77 reconnect()
77 } 78 }
78 } 79 }
  80 +
79 else -> { 81 else -> {
80 } 82 }
81 } 83 }
@@ -588,6 +590,7 @@ internal constructor( @@ -588,6 +590,7 @@ internal constructor(
588 null 590 null
589 } 591 }
590 } 592 }
  593 +
591 is Either.Right -> { 594 is Either.Right -> {
592 if (serverResponse.value.hasClientConfiguration()) { 595 if (serverResponse.value.hasClientConfiguration()) {
593 serverResponse.value.clientConfiguration 596 serverResponse.value.clientConfiguration
@@ -612,7 +615,7 @@ internal constructor( @@ -612,7 +615,7 @@ internal constructor(
612 fun onEngineDisconnected(reason: DisconnectReason) 615 fun onEngineDisconnected(reason: DisconnectReason)
613 fun onFailToConnect(error: Throwable) 616 fun onFailToConnect(error: Throwable)
614 fun onJoinResponse(response: JoinResponse) 617 fun onJoinResponse(response: JoinResponse)
615 - fun onAddTrack(track: MediaStreamTrack, streams: Array<out MediaStream>) 618 + fun onAddTrack(receiver: RtpReceiver, track: MediaStreamTrack, streams: Array<out MediaStream>)
616 fun onUpdateParticipants(updates: List<LivekitModels.ParticipantInfo>) 619 fun onUpdateParticipants(updates: List<LivekitModels.ParticipantInfo>)
617 fun onActiveSpeakersUpdate(speakers: List<LivekitModels.SpeakerInfo>) 620 fun onActiveSpeakersUpdate(speakers: List<LivekitModels.SpeakerInfo>)
618 fun onRemoteMuteChanged(trackSid: String, muted: Boolean) 621 fun onRemoteMuteChanged(trackSid: String, muted: Boolean)
@@ -654,6 +657,7 @@ internal constructor( @@ -654,6 +657,7 @@ internal constructor(
654 is Either.Left -> { 657 is Either.Left -> {
655 // do nothing. 658 // do nothing.
656 } 659 }
  660 +
657 is Either.Right -> { 661 is Either.Right -> {
658 LKLog.e { "error setting remote description for answer: ${outcome.value} " } 662 LKLog.e { "error setting remote description for answer: ${outcome.value} " }
659 } 663 }
@@ -671,6 +675,7 @@ internal constructor( @@ -671,6 +675,7 @@ internal constructor(
671 LKLog.e { "error setting remote description for answer: ${outcome.value} " } 675 LKLog.e { "error setting remote description for answer: ${outcome.value} " }
672 return@launch 676 return@launch
673 } 677 }
  678 +
674 else -> {} 679 else -> {}
675 } 680 }
676 } 681 }
@@ -691,6 +696,7 @@ internal constructor( @@ -691,6 +696,7 @@ internal constructor(
691 LKLog.e { "error setting local description for answer: ${outcome.value}" } 696 LKLog.e { "error setting local description for answer: ${outcome.value}" }
692 return@launch 697 return@launch
693 } 698 }
  699 +
694 else -> {} 700 else -> {}
695 } 701 }
696 } 702 }
@@ -709,6 +715,7 @@ internal constructor( @@ -709,6 +715,7 @@ internal constructor(
709 LKLog.w { "received candidate for publisher when we don't have one. ignoring." } 715 LKLog.w { "received candidate for publisher when we don't have one. ignoring." }
710 } 716 }
711 } 717 }
  718 +
712 LivekitRtc.SignalTarget.SUBSCRIBER -> subscriber.addIceCandidate(candidate) 719 LivekitRtc.SignalTarget.SUBSCRIBER -> subscriber.addIceCandidate(candidate)
713 else -> LKLog.i { "unknown ice candidate target?" } 720 else -> LKLog.i { "unknown ice candidate target?" }
714 } 721 }
@@ -814,9 +821,11 @@ internal constructor( @@ -814,9 +821,11 @@ internal constructor(
814 LivekitModels.DataPacket.ValueCase.SPEAKER -> { 821 LivekitModels.DataPacket.ValueCase.SPEAKER -> {
815 listener?.onActiveSpeakersUpdate(dp.speaker.speakersList) 822 listener?.onActiveSpeakersUpdate(dp.speaker.speakersList)
816 } 823 }
  824 +
817 LivekitModels.DataPacket.ValueCase.USER -> { 825 LivekitModels.DataPacket.ValueCase.USER -> {
818 listener?.onUserPacket(dp.user, dp.kind) 826 listener?.onUserPacket(dp.user, dp.kind)
819 } 827 }
  828 +
820 LivekitModels.DataPacket.ValueCase.VALUE_NOT_SET, 829 LivekitModels.DataPacket.ValueCase.VALUE_NOT_SET,
821 null -> { 830 null -> {
822 LKLog.v { "invalid value for data packet" } 831 LKLog.v { "invalid value for data packet" }
@@ -25,6 +25,7 @@ import io.livekit.android.util.FlowObservable @@ -25,6 +25,7 @@ import io.livekit.android.util.FlowObservable
25 import io.livekit.android.util.LKLog 25 import io.livekit.android.util.LKLog
26 import io.livekit.android.util.flowDelegate 26 import io.livekit.android.util.flowDelegate
27 import io.livekit.android.util.invoke 27 import io.livekit.android.util.invoke
  28 +import io.livekit.android.webrtc.createStatsGetter
28 import io.livekit.android.webrtc.getFilteredStats 29 import io.livekit.android.webrtc.getFilteredStats
29 import kotlinx.coroutines.* 30 import kotlinx.coroutines.*
30 import livekit.LivekitModels 31 import livekit.LivekitModels
@@ -202,6 +203,7 @@ constructor( @@ -202,6 +203,7 @@ constructor(
202 participant = it.participant, 203 participant = it.participant,
203 ) 204 )
204 ) 205 )
  206 +
205 is ParticipantEvent.ParticipantPermissionsChanged -> emitWhenConnected( 207 is ParticipantEvent.ParticipantPermissionsChanged -> emitWhenConnected(
206 RoomEvent.ParticipantPermissionsChanged( 208 RoomEvent.ParticipantPermissionsChanged(
207 room = this@Room, 209 room = this@Room,
@@ -210,6 +212,7 @@ constructor( @@ -210,6 +212,7 @@ constructor(
210 oldPermissions = it.oldPermissions, 212 oldPermissions = it.oldPermissions,
211 ) 213 )
212 ) 214 )
  215 +
213 is ParticipantEvent.MetadataChanged -> { 216 is ParticipantEvent.MetadataChanged -> {
214 listener?.onMetadataChanged(it.participant, it.prevMetadata, this@Room) 217 listener?.onMetadataChanged(it.participant, it.prevMetadata, this@Room)
215 emitWhenConnected( 218 emitWhenConnected(
@@ -220,6 +223,7 @@ constructor( @@ -220,6 +223,7 @@ constructor(
220 ) 223 )
221 ) 224 )
222 } 225 }
  226 +
223 is ParticipantEvent.NameChanged -> { 227 is ParticipantEvent.NameChanged -> {
224 emitWhenConnected( 228 emitWhenConnected(
225 RoomEvent.ParticipantNameChanged( 229 RoomEvent.ParticipantNameChanged(
@@ -229,6 +233,7 @@ constructor( @@ -229,6 +233,7 @@ constructor(
229 ) 233 )
230 ) 234 )
231 } 235 }
  236 +
232 else -> { 237 else -> {
233 /* do nothing */ 238 /* do nothing */
234 } 239 }
@@ -355,6 +360,7 @@ constructor( @@ -355,6 +360,7 @@ constructor(
355 ) 360 )
356 } 361 }
357 } 362 }
  363 +
358 is ParticipantEvent.TrackStreamStateChanged -> eventBus.postEvent( 364 is ParticipantEvent.TrackStreamStateChanged -> eventBus.postEvent(
359 RoomEvent.TrackStreamStateChanged( 365 RoomEvent.TrackStreamStateChanged(
360 this@Room, 366 this@Room,
@@ -362,6 +368,7 @@ constructor( @@ -362,6 +368,7 @@ constructor(
362 it.streamState 368 it.streamState
363 ) 369 )
364 ) 370 )
  371 +
365 is ParticipantEvent.TrackSubscriptionPermissionChanged -> eventBus.postEvent( 372 is ParticipantEvent.TrackSubscriptionPermissionChanged -> eventBus.postEvent(
366 RoomEvent.TrackSubscriptionPermissionChanged( 373 RoomEvent.TrackSubscriptionPermissionChanged(
367 this@Room, 374 this@Room,
@@ -370,6 +377,7 @@ constructor( @@ -370,6 +377,7 @@ constructor(
370 it.subscriptionAllowed 377 it.subscriptionAllowed
371 ) 378 )
372 ) 379 )
  380 +
373 is ParticipantEvent.MetadataChanged -> { 381 is ParticipantEvent.MetadataChanged -> {
374 listener?.onMetadataChanged(it.participant, it.prevMetadata, this@Room) 382 listener?.onMetadataChanged(it.participant, it.prevMetadata, this@Room)
375 emitWhenConnected( 383 emitWhenConnected(
@@ -380,6 +388,7 @@ constructor( @@ -380,6 +388,7 @@ constructor(
380 ) 388 )
381 ) 389 )
382 } 390 }
  391 +
383 is ParticipantEvent.NameChanged -> { 392 is ParticipantEvent.NameChanged -> {
384 emitWhenConnected( 393 emitWhenConnected(
385 RoomEvent.ParticipantNameChanged( 394 RoomEvent.ParticipantNameChanged(
@@ -389,6 +398,7 @@ constructor( @@ -389,6 +398,7 @@ constructor(
389 ) 398 )
390 ) 399 )
391 } 400 }
  401 +
392 is ParticipantEvent.ParticipantPermissionsChanged -> eventBus.postEvent( 402 is ParticipantEvent.ParticipantPermissionsChanged -> eventBus.postEvent(
393 RoomEvent.ParticipantPermissionsChanged( 403 RoomEvent.ParticipantPermissionsChanged(
394 room = this@Room, 404 room = this@Room,
@@ -397,6 +407,7 @@ constructor( @@ -397,6 +407,7 @@ constructor(
397 oldPermissions = it.oldPermissions, 407 oldPermissions = it.oldPermissions,
398 ) 408 )
399 ) 409 )
  410 +
400 else -> { 411 else -> {
401 /* do nothing */ 412 /* do nothing */
402 } 413 }
@@ -641,7 +652,7 @@ constructor( @@ -641,7 +652,7 @@ constructor(
641 /** 652 /**
642 * @suppress 653 * @suppress
643 */ 654 */
644 - override fun onAddTrack(track: MediaStreamTrack, streams: Array<out MediaStream>) { 655 + override fun onAddTrack(receiver: RtpReceiver, track: MediaStreamTrack, streams: Array<out MediaStream>) {
645 if (streams.count() < 0) { 656 if (streams.count() < 0) {
646 LKLog.i { "add track with empty streams?" } 657 LKLog.i { "add track with empty streams?" }
647 return 658 return
@@ -652,7 +663,13 @@ constructor( @@ -652,7 +663,13 @@ constructor(
652 trackSid = track.id() 663 trackSid = track.id()
653 } 664 }
654 val participant = getOrCreateRemoteParticipant(participantSid) 665 val participant = getOrCreateRemoteParticipant(participantSid)
655 - participant.addSubscribedMediaTrack(track, trackSid!!, adaptiveStream) 666 + val statsGetter = createStatsGetter(engine.subscriber.peerConnection, receiver)
  667 + participant.addSubscribedMediaTrack(
  668 + track,
  669 + trackSid!!,
  670 + autoManageVideo = adaptiveStream,
  671 + statsGetter = statsGetter
  672 + )
656 } 673 }
657 674
658 /** 675 /**
@@ -2,7 +2,14 @@ package io.livekit.android.room @@ -2,7 +2,14 @@ package io.livekit.android.room
2 2
3 import io.livekit.android.util.LKLog 3 import io.livekit.android.util.LKLog
4 import livekit.LivekitRtc 4 import livekit.LivekitRtc
5 -import org.webrtc.* 5 +import org.webrtc.CandidatePairChangeEvent
  6 +import org.webrtc.DataChannel
  7 +import org.webrtc.IceCandidate
  8 +import org.webrtc.MediaStream
  9 +import org.webrtc.MediaStreamTrack
  10 +import org.webrtc.PeerConnection
  11 +import org.webrtc.RtpReceiver
  12 +import org.webrtc.RtpTransceiver
6 13
7 /** 14 /**
8 * @suppress 15 * @suppress
@@ -23,7 +30,7 @@ class SubscriberTransportObserver( @@ -23,7 +30,7 @@ class SubscriberTransportObserver(
23 override fun onAddTrack(receiver: RtpReceiver, streams: Array<out MediaStream>) { 30 override fun onAddTrack(receiver: RtpReceiver, streams: Array<out MediaStream>) {
24 val track = receiver.track() ?: return 31 val track = receiver.track() ?: return
25 LKLog.v { "onAddTrack: ${track.kind()}, ${track.id()}, ${streams.fold("") { sum, it -> "$sum, $it" }}" } 32 LKLog.v { "onAddTrack: ${track.kind()}, ${track.id()}, ${streams.fold("") { sum, it -> "$sum, $it" }}" }
26 - engine.listener?.onAddTrack(track, streams) 33 + engine.listener?.onAddTrack(receiver, track, streams)
27 } 34 }
28 35
29 override fun onTrack(transceiver: RtpTransceiver) { 36 override fun onTrack(transceiver: RtpTransceiver) {
@@ -16,6 +16,7 @@ import io.livekit.android.room.RTCEngine @@ -16,6 +16,7 @@ import io.livekit.android.room.RTCEngine
16 import io.livekit.android.room.track.* 16 import io.livekit.android.room.track.*
17 import io.livekit.android.room.util.EncodingUtils 17 import io.livekit.android.room.util.EncodingUtils
18 import io.livekit.android.util.LKLog 18 import io.livekit.android.util.LKLog
  19 +import io.livekit.android.webrtc.createStatsGetter
19 import kotlinx.coroutines.CoroutineDispatcher 20 import kotlinx.coroutines.CoroutineDispatcher
20 import livekit.LivekitModels 21 import livekit.LivekitModels
21 import livekit.LivekitRtc 22 import livekit.LivekitRtc
@@ -180,10 +181,12 @@ internal constructor( @@ -180,10 +181,12 @@ internal constructor(
180 track.startCapture() 181 track.startCapture()
181 publishVideoTrack(track) 182 publishVideoTrack(track)
182 } 183 }
  184 +
183 Track.Source.MICROPHONE -> { 185 Track.Source.MICROPHONE -> {
184 val track = createAudioTrack() 186 val track = createAudioTrack()
185 publishAudioTrack(track) 187 publishAudioTrack(track)
186 } 188 }
  189 +
187 Track.Source.SCREEN_SHARE -> { 190 Track.Source.SCREEN_SHARE -> {
188 if (mediaProjectionPermissionResultData == null) { 191 if (mediaProjectionPermissionResultData == null) {
189 throw IllegalArgumentException("Media Projection permission result data is required to create a screen share track.") 192 throw IllegalArgumentException("Media Projection permission result data is required to create a screen share track.")
@@ -192,6 +195,7 @@ internal constructor( @@ -192,6 +195,7 @@ internal constructor(
192 createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData) 195 createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
193 publishVideoTrack(track) 196 publishVideoTrack(track)
194 } 197 }
  198 +
195 else -> { 199 else -> {
196 LKLog.w { "Attempting to enable an unknown source, ignoring." } 200 LKLog.w { "Attempting to enable an unknown source, ignoring." }
197 } 201 }
@@ -307,6 +311,7 @@ internal constructor( @@ -307,6 +311,7 @@ internal constructor(
307 return false 311 return false
308 } 312 }
309 313
  314 + track.statsGetter = createStatsGetter(engine.publisher.peerConnection, transceiver.sender)
310 315
311 if (options is VideoTrackPublishOptions && options.videoCodec != null) { 316 if (options is VideoTrackPublishOptions && options.videoCodec != null) {
312 val targetCodec = options.videoCodec.lowercase() 317 val targetCodec = options.videoCodec.lowercase()
@@ -5,6 +5,7 @@ import io.livekit.android.room.SignalClient @@ -5,6 +5,7 @@ import io.livekit.android.room.SignalClient
5 import io.livekit.android.room.track.* 5 import io.livekit.android.room.track.*
6 import io.livekit.android.util.CloseableCoroutineScope 6 import io.livekit.android.util.CloseableCoroutineScope
7 import io.livekit.android.util.LKLog 7 import io.livekit.android.util.LKLog
  8 +import io.livekit.android.webrtc.RTCStatsGetter
8 import kotlinx.coroutines.CoroutineDispatcher 9 import kotlinx.coroutines.CoroutineDispatcher
9 import kotlinx.coroutines.SupervisorJob 10 import kotlinx.coroutines.SupervisorJob
10 import kotlinx.coroutines.delay 11 import kotlinx.coroutines.delay
@@ -96,6 +97,7 @@ class RemoteParticipant( @@ -96,6 +97,7 @@ class RemoteParticipant(
96 fun addSubscribedMediaTrack( 97 fun addSubscribedMediaTrack(
97 mediaTrack: MediaStreamTrack, 98 mediaTrack: MediaStreamTrack,
98 sid: String, 99 sid: String,
  100 + statsGetter: RTCStatsGetter,
99 autoManageVideo: Boolean = false, 101 autoManageVideo: Boolean = false,
100 triesLeft: Int = 20 102 triesLeft: Int = 20
101 ) { 103 ) {
@@ -114,23 +116,26 @@ class RemoteParticipant( @@ -114,23 +116,26 @@ class RemoteParticipant(
114 } else { 116 } else {
115 coroutineScope.launch { 117 coroutineScope.launch {
116 delay(150) 118 delay(150)
117 - addSubscribedMediaTrack(mediaTrack, sid, autoManageVideo, triesLeft - 1) 119 + addSubscribedMediaTrack(mediaTrack, sid, statsGetter, autoManageVideo, triesLeft - 1)
118 } 120 }
119 } 121 }
120 return 122 return
121 } 123 }
122 124
123 val track: Track = when (val kind = mediaTrack.kind()) { 125 val track: Track = when (val kind = mediaTrack.kind()) {
124 - KIND_AUDIO -> AudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "") 126 + KIND_AUDIO -> RemoteAudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "")
125 KIND_VIDEO -> RemoteVideoTrack( 127 KIND_VIDEO -> RemoteVideoTrack(
126 rtcTrack = mediaTrack as VideoTrack, 128 rtcTrack = mediaTrack as VideoTrack,
127 name = "", 129 name = "",
128 autoManageVideo = autoManageVideo, 130 autoManageVideo = autoManageVideo,
129 dispatcher = ioDispatcher 131 dispatcher = ioDispatcher
130 ) 132 )
  133 +
131 else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind") 134 else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind")
132 } 135 }
133 136
  137 + track.statsGetter = statsGetter
  138 +
134 publication.track = track 139 publication.track = track
135 publication.subscriptionAllowed = true 140 publication.subscriptionAllowed = true
136 track.name = publication.name 141 track.name = publication.name
1 package io.livekit.android.room.track 1 package io.livekit.android.room.track
2 2
3 -import livekit.LivekitModels  
4 import org.webrtc.AudioTrack 3 import org.webrtc.AudioTrack
5 4
6 -open class AudioTrack(name: String, override val rtcTrack: AudioTrack) : 5 +abstract class AudioTrack(name: String, override val rtcTrack: AudioTrack) :
7 Track(name, Kind.AUDIO, rtcTrack) { 6 Track(name, Kind.AUDIO, rtcTrack) {
8 } 7 }
  1 +package io.livekit.android.room.track
  2 +
  3 +import org.webrtc.AudioTrack
  4 +
  5 +class RemoteAudioTrack(name: String, rtcTrack: AudioTrack) : io.livekit.android.room.track.AudioTrack(name, rtcTrack)
@@ -3,11 +3,15 @@ package io.livekit.android.room.track @@ -3,11 +3,15 @@ package io.livekit.android.room.track
3 import io.livekit.android.events.BroadcastEventBus 3 import io.livekit.android.events.BroadcastEventBus
4 import io.livekit.android.events.TrackEvent 4 import io.livekit.android.events.TrackEvent
5 import io.livekit.android.util.flowDelegate 5 import io.livekit.android.util.flowDelegate
  6 +import io.livekit.android.webrtc.RTCStatsGetter
  7 +import io.livekit.android.webrtc.getStats
6 import livekit.LivekitModels 8 import livekit.LivekitModels
7 import livekit.LivekitRtc 9 import livekit.LivekitRtc
8 import org.webrtc.MediaStreamTrack 10 import org.webrtc.MediaStreamTrack
  11 +import org.webrtc.RTCStatsCollectorCallback
  12 +import org.webrtc.RTCStatsReport
9 13
10 -open class Track( 14 +abstract class Track(
11 name: String, 15 name: String,
12 kind: Kind, 16 kind: Kind,
13 open val rtcTrack: MediaStreamTrack 17 open val rtcTrack: MediaStreamTrack
@@ -28,6 +32,20 @@ open class Track( @@ -28,6 +32,20 @@ open class Track(
28 } 32 }
29 internal set 33 internal set
30 34
  35 + var statsGetter: RTCStatsGetter? = null
  36 +
  37 + /**
  38 + * Return the [RTCStatsReport] for this track, or null if none is available.
  39 + */
  40 + suspend fun getRTCStats(): RTCStatsReport? = statsGetter?.getStats()
  41 +
  42 + /**
  43 + * Calls the [callback] with the [RTCStatsReport] for this track, or null if none is available.
  44 + */
  45 + fun getRTCStats(callback: RTCStatsCollectorCallback) {
  46 + statsGetter?.invoke(callback) ?: callback.onStatsDelivered(null)
  47 + }
  48 +
31 enum class Kind(val value: String) { 49 enum class Kind(val value: String) {
32 AUDIO("audio"), 50 AUDIO("audio"),
33 VIDEO("video"), 51 VIDEO("video"),
@@ -3,7 +3,7 @@ package io.livekit.android.room.track @@ -3,7 +3,7 @@ package io.livekit.android.room.track
3 import org.webrtc.VideoSink 3 import org.webrtc.VideoSink
4 import org.webrtc.VideoTrack 4 import org.webrtc.VideoTrack
5 5
6 -open class VideoTrack(name: String, override val rtcTrack: VideoTrack) : 6 +abstract class VideoTrack(name: String, override val rtcTrack: VideoTrack) :
7 Track(name, Kind.VIDEO, rtcTrack) { 7 Track(name, Kind.VIDEO, rtcTrack) {
8 protected val sinks: MutableList<VideoSink> = ArrayList(); 8 protected val sinks: MutableList<VideoSink> = ArrayList();
9 9
1 package io.livekit.android.webrtc 1 package io.livekit.android.webrtc
2 2
3 import io.livekit.android.util.LKLog 3 import io.livekit.android.util.LKLog
  4 +import kotlinx.coroutines.suspendCancellableCoroutine
4 import org.webrtc.MediaStreamTrack 5 import org.webrtc.MediaStreamTrack
  6 +import org.webrtc.PeerConnection
5 import org.webrtc.RTCStats 7 import org.webrtc.RTCStats
  8 +import org.webrtc.RTCStatsCollectorCallback
6 import org.webrtc.RTCStatsReport 9 import org.webrtc.RTCStatsReport
  10 +import org.webrtc.RtpReceiver
  11 +import org.webrtc.RtpSender
  12 +import kotlin.coroutines.resume
7 13
8 /** 14 /**
9 * Returns an RTCStatsReport with all the relevant information pertaining to a track. 15 * Returns an RTCStatsReport with all the relevant information pertaining to a track.
@@ -144,3 +150,23 @@ private fun getExtraStats( @@ -144,3 +150,23 @@ private fun getExtraStats(
144 } 150 }
145 return extraStats 151 return extraStats
146 } 152 }
  153 +
  154 +typealias RTCStatsGetter = (RTCStatsCollectorCallback) -> Unit
  155 +
  156 +suspend fun RTCStatsGetter.getStats(): RTCStatsReport = suspendCancellableCoroutine { cont ->
  157 + val listener = RTCStatsCollectorCallback { report ->
  158 + cont.resume(report)
  159 + }
  160 + this.invoke(listener)
  161 +}
  162 +
  163 +fun createStatsGetter(peerConnection: PeerConnection, sender: RtpSender): RTCStatsGetter =
  164 + { statsCallback: RTCStatsCollectorCallback ->
  165 + peerConnection.getStats(statsCallback, sender)
  166 + }
  167 +
  168 +fun createStatsGetter(peerConnection: PeerConnection, receiver: RtpReceiver): RTCStatsGetter =
  169 + { statsCallback: RTCStatsCollectorCallback ->
  170 + peerConnection.getStats(statsCallback, receiver)
  171 + }
  172 +
@@ -5,6 +5,7 @@ import org.webrtc.IceCandidate @@ -5,6 +5,7 @@ import org.webrtc.IceCandidate
5 import org.webrtc.MediaConstraints 5 import org.webrtc.MediaConstraints
6 import org.webrtc.MediaStream 6 import org.webrtc.MediaStream
7 import org.webrtc.MediaStreamTrack 7 import org.webrtc.MediaStreamTrack
  8 +import org.webrtc.MockRtpTransceiver
8 import org.webrtc.NativePeerConnectionFactory 9 import org.webrtc.NativePeerConnectionFactory
9 import org.webrtc.PeerConnection 10 import org.webrtc.PeerConnection
10 import org.webrtc.RTCStatsCollectorCallback 11 import org.webrtc.RTCStatsCollectorCallback
  1 +package io.livekit.android.mock
  2 +
  3 +import org.mockito.Mockito
  4 +import org.webrtc.RtpReceiver
  5 +
  6 +object MockRtpReceiver {
  7 + fun create(): RtpReceiver {
  8 + return Mockito.mock(RtpReceiver::class.java)
  9 + }
  10 +}
  1 +package io.livekit.android.mock
  2 +
  3 +import org.mockito.Mockito
  4 +import org.webrtc.RtpSender
  5 +
  6 +object MockRtpSender {
  7 + fun create(): RtpSender {
  8 + return Mockito.mock(RtpSender::class.java)
  9 + }
  10 +}
@@ -9,6 +9,7 @@ import io.livekit.android.assert.assertIsClassList @@ -9,6 +9,7 @@ import io.livekit.android.assert.assertIsClassList
9 import io.livekit.android.events.* 9 import io.livekit.android.events.*
10 import io.livekit.android.mock.MockAudioStreamTrack 10 import io.livekit.android.mock.MockAudioStreamTrack
11 import io.livekit.android.mock.MockMediaStream 11 import io.livekit.android.mock.MockMediaStream
  12 +import io.livekit.android.mock.MockRtpReceiver
12 import io.livekit.android.mock.TestData 13 import io.livekit.android.mock.TestData
13 import io.livekit.android.mock.createMediaStreamId 14 import io.livekit.android.mock.createMediaStreamId
14 import io.livekit.android.room.participant.ConnectionQuality 15 import io.livekit.android.room.participant.ConnectionQuality
@@ -198,7 +199,8 @@ class RoomMockE2ETest : MockE2ETest() { @@ -198,7 +199,8 @@ class RoomMockE2ETest : MockE2ETest() {
198 // We intentionally don't emit if the track isn't subscribed, so need to 199 // We intentionally don't emit if the track isn't subscribed, so need to
199 // add track. 200 // add track.
200 room.onAddTrack( 201 room.onAddTrack(
201 - MockAudioStreamTrack(), 202 + receiver = MockRtpReceiver.create(),
  203 + track = MockAudioStreamTrack(),
202 arrayOf( 204 arrayOf(
203 MockMediaStream( 205 MockMediaStream(
204 id = createMediaStreamId( 206 id = createMediaStreamId(
@@ -231,6 +233,7 @@ class RoomMockE2ETest : MockE2ETest() { @@ -231,6 +233,7 @@ class RoomMockE2ETest : MockE2ETest() {
231 SignalClientTest.PARTICIPANT_JOIN.toOkioByteString() 233 SignalClientTest.PARTICIPANT_JOIN.toOkioByteString()
232 ) 234 )
233 room.onAddTrack( 235 room.onAddTrack(
  236 + MockRtpReceiver.create(),
234 MockAudioStreamTrack(), 237 MockAudioStreamTrack(),
235 arrayOf( 238 arrayOf(
236 MockMediaStream( 239 MockMediaStream(
@@ -317,6 +320,41 @@ class RoomMockE2ETest : MockE2ETest() { @@ -317,6 +320,41 @@ class RoomMockE2ETest : MockE2ETest() {
317 } 320 }
318 321
319 @Test 322 @Test
  323 + fun disconnectCleansUpParticipants() = runTest {
  324 + connect()
  325 +
  326 + room.onUpdateParticipants(SignalClientTest.PARTICIPANT_JOIN.update.participantsList)
  327 + room.onAddTrack(
  328 + MockRtpReceiver.create(),
  329 + MockAudioStreamTrack(),
  330 + arrayOf(
  331 + MockMediaStream(
  332 + id = createMediaStreamId(
  333 + TestData.REMOTE_PARTICIPANT.sid,
  334 + TestData.REMOTE_AUDIO_TRACK.sid
  335 + )
  336 + )
  337 + )
  338 + )
  339 +
  340 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  341 + room.onEngineDisconnected(DisconnectReason.CLIENT_INITIATED)
  342 + val events = eventCollector.stopCollecting()
  343 +
  344 + assertIsClassList(
  345 + listOf(
  346 + RoomEvent.TrackUnsubscribed::class.java,
  347 + RoomEvent.TrackUnpublished::class.java,
  348 + RoomEvent.TrackUnpublished::class.java,
  349 + RoomEvent.ParticipantDisconnected::class.java,
  350 + RoomEvent.Disconnected::class.java
  351 + ),
  352 + events
  353 + )
  354 + Assert.assertTrue(room.remoteParticipants.isEmpty())
  355 + }
  356 +
  357 + @Test
320 fun serverDisconnectReason() = runTest { 358 fun serverDisconnectReason() = runTest {
321 connect() 359 connect()
322 360
@@ -188,38 +188,4 @@ class RoomTest { @@ -188,38 +188,4 @@ class RoomTest {
188 assertNull(room.name) 188 assertNull(room.name)
189 assertFalse(room.isRecording) 189 assertFalse(room.isRecording)
190 } 190 }
191 -  
192 - @Test  
193 - fun disconnectCleansUpParticipants() = runTest {  
194 - connect()  
195 -  
196 - room.onUpdateParticipants(SignalClientTest.PARTICIPANT_JOIN.update.participantsList)  
197 - room.onAddTrack(  
198 - MockAudioStreamTrack(),  
199 - arrayOf(  
200 - MockMediaStream(  
201 - id = createMediaStreamId(  
202 - TestData.REMOTE_PARTICIPANT.sid,  
203 - TestData.REMOTE_AUDIO_TRACK.sid  
204 - )  
205 - )  
206 - )  
207 - )  
208 -  
209 - val eventCollector = EventCollector(room.events, coroutineRule.scope)  
210 - room.onEngineDisconnected(DisconnectReason.CLIENT_INITIATED)  
211 - val events = eventCollector.stopCollecting()  
212 -  
213 - assertIsClassList(  
214 - listOf(  
215 - RoomEvent.TrackUnsubscribed::class.java,  
216 - RoomEvent.TrackUnpublished::class.java,  
217 - RoomEvent.TrackUnpublished::class.java,  
218 - RoomEvent.ParticipantDisconnected::class.java,  
219 - RoomEvent.Disconnected::class.java  
220 - ),  
221 - events  
222 - )  
223 - assertTrue(room.remoteParticipants.isEmpty())  
224 - }  
225 } 191 }
@@ -7,7 +7,6 @@ import io.livekit.android.util.toOkioByteString @@ -7,7 +7,6 @@ import io.livekit.android.util.toOkioByteString
7 import io.livekit.android.util.toPBByteString 7 import io.livekit.android.util.toPBByteString
8 import kotlinx.coroutines.ExperimentalCoroutinesApi 8 import kotlinx.coroutines.ExperimentalCoroutinesApi
9 import kotlinx.coroutines.test.advanceUntilIdle 9 import kotlinx.coroutines.test.advanceUntilIdle
10 -import livekit.LivekitModels  
11 import livekit.LivekitModels.VideoQuality 10 import livekit.LivekitModels.VideoQuality
12 import livekit.LivekitRtc 11 import livekit.LivekitRtc
13 import org.junit.Assert.assertEquals 12 import org.junit.Assert.assertEquals
@@ -33,6 +32,7 @@ class RemoteTrackPublicationTest : MockE2ETest() { @@ -33,6 +32,7 @@ class RemoteTrackPublicationTest : MockE2ETest() {
33 ) 32 )
34 33
35 room.onAddTrack( 34 room.onAddTrack(
  35 + MockRtpReceiver.create(),
36 MockVideoStreamTrack(), 36 MockVideoStreamTrack(),
37 arrayOf( 37 arrayOf(
38 MockMediaStream( 38 MockMediaStream(
1 -package io.livekit.android.mock 1 +package org.webrtc
2 2
  3 +import io.livekit.android.mock.MockRtpReceiver
  4 +import io.livekit.android.mock.MockRtpSender
3 import org.mockito.Mockito 5 import org.mockito.Mockito
4 -import org.webrtc.MediaStreamTrack  
5 -import org.webrtc.RtpTransceiver 6 +import org.webrtc.RtpTransceiver.RtpTransceiverDirection
6 7
7 object MockRtpTransceiver { 8 object MockRtpTransceiver {
8 fun create( 9 fun create(
@@ -19,6 +20,28 @@ object MockRtpTransceiver { @@ -19,6 +20,28 @@ object MockRtpTransceiver {
19 } 20 }
20 } 21 }
21 22
  23 + val direction = RtpTransceiverDirection.fromNativeIndex(init.directionNativeIndex)
  24 +
  25 + when (direction) {
  26 + RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.SEND_ONLY -> {
  27 + val sender = MockRtpSender.create()
  28 + Mockito.`when`(mock.sender)
  29 + .then { sender }
  30 + }
  31 +
  32 + else -> {}
  33 + }
  34 +
  35 + when (direction) {
  36 + RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.RECV_ONLY -> {
  37 + val receiver = MockRtpReceiver.create()
  38 + Mockito.`when`(mock.receiver)
  39 + .then { receiver }
  40 + }
  41 +
  42 + else -> {}
  43 + }
  44 +
22 return mock 45 return mock
23 } 46 }
24 } 47 }
@@ -25,7 +25,13 @@ import io.livekit.android.room.track.LocalVideoTrack @@ -25,7 +25,13 @@ import io.livekit.android.room.track.LocalVideoTrack
25 import io.livekit.android.room.track.Track 25 import io.livekit.android.room.track.Track
26 import io.livekit.android.sample.service.ForegroundService 26 import io.livekit.android.sample.service.ForegroundService
27 import io.livekit.android.util.flow 27 import io.livekit.android.util.flow
28 -import kotlinx.coroutines.flow.* 28 +import kotlinx.coroutines.delay
  29 +import kotlinx.coroutines.flow.Flow
  30 +import kotlinx.coroutines.flow.MutableSharedFlow
  31 +import kotlinx.coroutines.flow.MutableStateFlow
  32 +import kotlinx.coroutines.flow.StateFlow
  33 +import kotlinx.coroutines.flow.combine
  34 +import kotlinx.coroutines.flow.map
29 import kotlinx.coroutines.launch 35 import kotlinx.coroutines.launch
30 36
31 class CallViewModel( 37 class CallViewModel(
@@ -109,6 +115,11 @@ class CallViewModel( @@ -109,6 +115,11 @@ class CallViewModel(
109 val message = it.data.toString(Charsets.UTF_8) 115 val message = it.data.toString(Charsets.UTF_8)
110 mutableDataReceived.emit("$identity: $message") 116 mutableDataReceived.emit("$identity: $message")
111 } 117 }
  118 +
  119 + is RoomEvent.TrackSubscribed -> {
  120 + launch { collectTrackStats(it) }
  121 + }
  122 +
112 else -> { 123 else -> {
113 Timber.e { "Room event: $it" } 124 Timber.e { "Room event: $it" }
114 } 125 }
@@ -130,6 +141,22 @@ class CallViewModel( @@ -130,6 +141,22 @@ class CallViewModel(
130 } 141 }
131 } 142 }
132 143
  144 + private suspend fun collectTrackStats(event: RoomEvent.TrackSubscribed) {
  145 + val pub = event.publication
  146 + while (true) {
  147 + delay(10000)
  148 + if (pub.subscribed) {
  149 + val statsReport = pub.track?.getRTCStats() ?: continue
  150 + Timber.e { "stats for ${pub.sid}:" }
  151 +
  152 + for (entry in statsReport.statsMap) {
  153 + Timber.e { "${entry.key} = ${entry.value}" }
  154 + }
  155 + }
  156 + }
  157 +
  158 + }
  159 +
133 private suspend fun connectToRoom() { 160 private suspend fun connectToRoom() {
134 try { 161 try {
135 room.connect( 162 room.connect(