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 | } |
| @@ -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 |
| @@ -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( |
-
请 注册 或 登录 后发表评论