Committed by
GitHub
Permissions API (#37)
* permissions API * naming * don't add track if disallowed * hold on to track for now * room event for subscription permission update * update test * language cleanup * Move track permission handling to RemoteParticipant to allow emission of ParticipantEvent * Subscription status enum * revert preventing subscription to disallowed track * keep subscribe boolean and add extra subscriptionStatus * fix build
正在显示
14 个修改的文件
包含
267 行增加
和
43 行删除
| 1 | package io.livekit.android.events | 1 | package io.livekit.android.events |
| 2 | 2 | ||
| 3 | +import io.livekit.android.room.Room | ||
| 3 | import io.livekit.android.room.participant.LocalParticipant | 4 | import io.livekit.android.room.participant.LocalParticipant |
| 4 | import io.livekit.android.room.participant.Participant | 5 | import io.livekit.android.room.participant.Participant |
| 5 | import io.livekit.android.room.participant.RemoteParticipant | 6 | import io.livekit.android.room.participant.RemoteParticipant |
| @@ -107,4 +108,14 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() { | @@ -107,4 +108,14 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() { | ||
| 107 | val trackPublication: TrackPublication, | 108 | val trackPublication: TrackPublication, |
| 108 | val streamState: Track.StreamState | 109 | val streamState: Track.StreamState |
| 109 | ) : ParticipantEvent(participant) | 110 | ) : ParticipantEvent(participant) |
| 111 | + | ||
| 112 | + | ||
| 113 | + /** | ||
| 114 | + * A remote track's subscription permissions have changed. | ||
| 115 | + */ | ||
| 116 | + class TrackSubscriptionPermissionChanged( | ||
| 117 | + override val participant: RemoteParticipant, | ||
| 118 | + val trackPublication: RemoteTrackPublication, | ||
| 119 | + val subscriptionAllowed: Boolean | ||
| 120 | + ) : ParticipantEvent(participant) | ||
| 110 | } | 121 | } |
| @@ -6,6 +6,7 @@ import io.livekit.android.room.participant.LocalParticipant | @@ -6,6 +6,7 @@ import io.livekit.android.room.participant.LocalParticipant | ||
| 6 | import io.livekit.android.room.participant.Participant | 6 | import io.livekit.android.room.participant.Participant |
| 7 | import io.livekit.android.room.participant.RemoteParticipant | 7 | import io.livekit.android.room.participant.RemoteParticipant |
| 8 | import io.livekit.android.room.track.LocalTrackPublication | 8 | import io.livekit.android.room.track.LocalTrackPublication |
| 9 | +import io.livekit.android.room.track.RemoteTrackPublication | ||
| 9 | import io.livekit.android.room.track.Track | 10 | import io.livekit.android.room.track.Track |
| 10 | import io.livekit.android.room.track.TrackPublication | 11 | import io.livekit.android.room.track.TrackPublication |
| 11 | 12 | ||
| @@ -131,6 +132,16 @@ sealed class RoomEvent(val room: Room) : Event() { | @@ -131,6 +132,16 @@ sealed class RoomEvent(val room: Room) : Event() { | ||
| 131 | ) : RoomEvent(room) | 132 | ) : RoomEvent(room) |
| 132 | 133 | ||
| 133 | /** | 134 | /** |
| 135 | + * A remote track's subscription permissions have changed. | ||
| 136 | + */ | ||
| 137 | + class TrackSubscriptionPermissionChanged( | ||
| 138 | + room: Room, | ||
| 139 | + val participant: RemoteParticipant, | ||
| 140 | + val trackPublication: RemoteTrackPublication, | ||
| 141 | + val subscriptionAllowed: Boolean | ||
| 142 | + ) : RoomEvent(room) | ||
| 143 | + | ||
| 144 | + /** | ||
| 134 | * Received data published by another participant | 145 | * Received data published by another participant |
| 135 | */ | 146 | */ |
| 136 | class DataReceived(room: Room, val data: ByteArray, val participant: RemoteParticipant) : RoomEvent(room) | 147 | class DataReceived(room: Room, val data: ByteArray, val participant: RemoteParticipant) : RoomEvent(room) |
| @@ -3,6 +3,7 @@ package io.livekit.android.room | @@ -3,6 +3,7 @@ package io.livekit.android.room | ||
| 3 | import android.os.SystemClock | 3 | import android.os.SystemClock |
| 4 | import io.livekit.android.ConnectOptions | 4 | import io.livekit.android.ConnectOptions |
| 5 | import io.livekit.android.dagger.InjectionNames | 5 | import io.livekit.android.dagger.InjectionNames |
| 6 | +import io.livekit.android.room.participant.ParticipantTrackPermission | ||
| 6 | import io.livekit.android.room.track.TrackException | 7 | import io.livekit.android.room.track.TrackException |
| 7 | import io.livekit.android.room.util.* | 8 | import io.livekit.android.room.util.* |
| 8 | import io.livekit.android.util.CloseableCoroutineScope | 9 | import io.livekit.android.util.CloseableCoroutineScope |
| @@ -229,6 +230,13 @@ internal constructor( | @@ -229,6 +230,13 @@ internal constructor( | ||
| 229 | } | 230 | } |
| 230 | } | 231 | } |
| 231 | 232 | ||
| 233 | + fun updateSubscriptionPermissions( | ||
| 234 | + allParticipants: Boolean, | ||
| 235 | + participantTrackPermissions: List<ParticipantTrackPermission> | ||
| 236 | + ) { | ||
| 237 | + client.sendUpdateSubscriptionPermissions(allParticipants, participantTrackPermissions) | ||
| 238 | + } | ||
| 239 | + | ||
| 232 | fun updateMuteStatus(sid: String, muted: Boolean) { | 240 | fun updateMuteStatus(sid: String, muted: Boolean) { |
| 233 | client.sendMuteTrack(sid, muted) | 241 | client.sendMuteTrack(sid, muted) |
| 234 | } | 242 | } |
| @@ -386,6 +394,7 @@ internal constructor( | @@ -386,6 +394,7 @@ internal constructor( | ||
| 386 | fun onUserPacket(packet: LivekitModels.UserPacket, kind: LivekitModels.DataPacket.Kind) | 394 | fun onUserPacket(packet: LivekitModels.UserPacket, kind: LivekitModels.DataPacket.Kind) |
| 387 | fun onStreamStateUpdate(streamStates: List<LivekitRtc.StreamStateInfo>) | 395 | fun onStreamStateUpdate(streamStates: List<LivekitRtc.StreamStateInfo>) |
| 388 | fun onSubscribedQualityUpdate(subscribedQualityUpdate: LivekitRtc.SubscribedQualityUpdate) | 396 | fun onSubscribedQualityUpdate(subscribedQualityUpdate: LivekitRtc.SubscribedQualityUpdate) |
| 397 | + fun onSubscriptionPermissionUpdate(subscriptionPermissionUpdate: LivekitRtc.SubscriptionPermissionUpdate) | ||
| 389 | } | 398 | } |
| 390 | 399 | ||
| 391 | companion object { | 400 | companion object { |
| @@ -531,6 +540,10 @@ internal constructor( | @@ -531,6 +540,10 @@ internal constructor( | ||
| 531 | listener?.onSubscribedQualityUpdate(subscribedQualityUpdate) | 540 | listener?.onSubscribedQualityUpdate(subscribedQualityUpdate) |
| 532 | } | 541 | } |
| 533 | 542 | ||
| 543 | + override fun onSubscriptionPermissionUpdate(subscriptionPermissionUpdate: LivekitRtc.SubscriptionPermissionUpdate) { | ||
| 544 | + listener?.onSubscriptionPermissionUpdate(subscriptionPermissionUpdate) | ||
| 545 | + } | ||
| 546 | + | ||
| 534 | //--------------------------------- DataChannel.Observer ------------------------------------// | 547 | //--------------------------------- DataChannel.Observer ------------------------------------// |
| 535 | 548 | ||
| 536 | override fun onBufferedAmountChange(previousAmount: Long) { | 549 | override fun onBufferedAmountChange(previousAmount: Long) { |
| @@ -226,6 +226,14 @@ constructor( | @@ -226,6 +226,14 @@ constructor( | ||
| 226 | it.streamState | 226 | it.streamState |
| 227 | ) | 227 | ) |
| 228 | ) | 228 | ) |
| 229 | + is ParticipantEvent.TrackSubscriptionPermissionChanged -> eventBus.postEvent( | ||
| 230 | + RoomEvent.TrackSubscriptionPermissionChanged( | ||
| 231 | + this@Room, | ||
| 232 | + it.participant, | ||
| 233 | + it.trackPublication, | ||
| 234 | + it.subscriptionAllowed | ||
| 235 | + ) | ||
| 236 | + ) | ||
| 229 | } | 237 | } |
| 230 | } | 238 | } |
| 231 | } | 239 | } |
| @@ -483,6 +491,11 @@ constructor( | @@ -483,6 +491,11 @@ constructor( | ||
| 483 | localParticipant.handleSubscribedQualityUpdate(subscribedQualityUpdate) | 491 | localParticipant.handleSubscribedQualityUpdate(subscribedQualityUpdate) |
| 484 | } | 492 | } |
| 485 | 493 | ||
| 494 | + override fun onSubscriptionPermissionUpdate(subscriptionPermissionUpdate: LivekitRtc.SubscriptionPermissionUpdate) { | ||
| 495 | + val participant = getParticipant(subscriptionPermissionUpdate.participantSid) as? RemoteParticipant ?: return | ||
| 496 | + participant.onSubscriptionPermissionUpdate(subscriptionPermissionUpdate) | ||
| 497 | + } | ||
| 498 | + | ||
| 486 | /** | 499 | /** |
| 487 | * @suppress | 500 | * @suppress |
| 488 | */ | 501 | */ |
| @@ -5,6 +5,7 @@ import com.vdurmont.semver4j.Semver | @@ -5,6 +5,7 @@ import com.vdurmont.semver4j.Semver | ||
| 5 | import io.livekit.android.ConnectOptions | 5 | import io.livekit.android.ConnectOptions |
| 6 | import io.livekit.android.Version | 6 | import io.livekit.android.Version |
| 7 | import io.livekit.android.dagger.InjectionNames | 7 | import io.livekit.android.dagger.InjectionNames |
| 8 | +import io.livekit.android.room.participant.ParticipantTrackPermission | ||
| 8 | import io.livekit.android.room.track.Track | 9 | import io.livekit.android.room.track.Track |
| 9 | import io.livekit.android.util.CloseableCoroutineScope | 10 | import io.livekit.android.util.CloseableCoroutineScope |
| 10 | import io.livekit.android.util.Either | 11 | import io.livekit.android.util.Either |
| @@ -329,6 +330,21 @@ constructor( | @@ -329,6 +330,21 @@ constructor( | ||
| 329 | sendRequest(request) | 330 | sendRequest(request) |
| 330 | } | 331 | } |
| 331 | 332 | ||
| 333 | + fun sendUpdateSubscriptionPermissions( | ||
| 334 | + allParticipants: Boolean, | ||
| 335 | + participantTrackPermissions: List<ParticipantTrackPermission> | ||
| 336 | + ) { | ||
| 337 | + val update = LivekitRtc.UpdateSubscriptionPermissions.newBuilder() | ||
| 338 | + .setAllParticipants(allParticipants) | ||
| 339 | + .addAllTrackPermissions(participantTrackPermissions.map { it.toProto() }) | ||
| 340 | + | ||
| 341 | + val request = LivekitRtc.SignalRequest.newBuilder() | ||
| 342 | + .setSubscriptionPermissions(update) | ||
| 343 | + .build() | ||
| 344 | + | ||
| 345 | + sendRequest(request) | ||
| 346 | + } | ||
| 347 | + | ||
| 332 | fun sendLeave() { | 348 | fun sendLeave() { |
| 333 | val request = LivekitRtc.SignalRequest.newBuilder() | 349 | val request = LivekitRtc.SignalRequest.newBuilder() |
| 334 | .setLeave(LivekitRtc.LeaveRequest.newBuilder().build()) | 350 | .setLeave(LivekitRtc.LeaveRequest.newBuilder().build()) |
| @@ -433,7 +449,7 @@ constructor( | @@ -433,7 +449,7 @@ constructor( | ||
| 433 | listener?.onSubscribedQualityUpdate(response.subscribedQualityUpdate) | 449 | listener?.onSubscribedQualityUpdate(response.subscribedQualityUpdate) |
| 434 | } | 450 | } |
| 435 | LivekitRtc.SignalResponse.MessageCase.SUBSCRIPTION_PERMISSION_UPDATE -> { | 451 | LivekitRtc.SignalResponse.MessageCase.SUBSCRIPTION_PERMISSION_UPDATE -> { |
| 436 | - // TODO | 452 | + listener?.onSubscriptionPermissionUpdate(response.subscriptionPermissionUpdate) |
| 437 | } | 453 | } |
| 438 | LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET, | 454 | LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET, |
| 439 | null -> { | 455 | null -> { |
| @@ -463,6 +479,7 @@ constructor( | @@ -463,6 +479,7 @@ constructor( | ||
| 463 | fun onError(error: Throwable) | 479 | fun onError(error: Throwable) |
| 464 | fun onStreamStateUpdate(streamStates: List<LivekitRtc.StreamStateInfo>) | 480 | fun onStreamStateUpdate(streamStates: List<LivekitRtc.StreamStateInfo>) |
| 465 | fun onSubscribedQualityUpdate(subscribedQualityUpdate: LivekitRtc.SubscribedQualityUpdate) | 481 | fun onSubscribedQualityUpdate(subscribedQualityUpdate: LivekitRtc.SubscribedQualityUpdate) |
| 482 | + fun onSubscriptionPermissionUpdate(subscriptionPermissionUpdate: LivekitRtc.SubscriptionPermissionUpdate) | ||
| 466 | } | 483 | } |
| 467 | 484 | ||
| 468 | companion object { | 485 | companion object { |
| @@ -419,6 +419,30 @@ internal constructor( | @@ -419,6 +419,30 @@ internal constructor( | ||
| 419 | } | 419 | } |
| 420 | } | 420 | } |
| 421 | 421 | ||
| 422 | + /** | ||
| 423 | + * Control who can subscribe to LocalParticipant's published tracks. | ||
| 424 | + * | ||
| 425 | + * By default, all participants can subscribe. This allows fine-grained control over | ||
| 426 | + * who is able to subscribe at a participant and track level. | ||
| 427 | + * | ||
| 428 | + * Note: if access is given at a track-level (i.e. both [allParticipantsAllowed] and | ||
| 429 | + * [ParticipantTrackPermission.allTracksAllowed] are false), any newer published tracks | ||
| 430 | + * will not grant permissions to any participants and will require a subsequent | ||
| 431 | + * permissions update to allow subscription. | ||
| 432 | + * | ||
| 433 | + * @param allParticipantsAllowed Allows all participants to subscribe all tracks. | ||
| 434 | + * Takes precedence over [participantTrackPermissions] if set to true. | ||
| 435 | + * By default this is set to true. | ||
| 436 | + * @param participantTrackPermissions Full list of individual permissions per | ||
| 437 | + * participant/track. Any omitted participants will not receive any permissions. | ||
| 438 | + */ | ||
| 439 | + fun setTrackSubscriptionPermissions( | ||
| 440 | + allParticipantsAllowed: Boolean, | ||
| 441 | + participantTrackPermissions: List<ParticipantTrackPermission> = emptyList() | ||
| 442 | + ) { | ||
| 443 | + engine.updateSubscriptionPermissions(allParticipantsAllowed, participantTrackPermissions) | ||
| 444 | + } | ||
| 445 | + | ||
| 422 | fun unpublishTrack(track: Track) { | 446 | fun unpublishTrack(track: Track) { |
| 423 | val publication = localTrackPublications.firstOrNull { it.track == track } | 447 | val publication = localTrackPublications.firstOrNull { it.track == track } |
| 424 | if (publication === null) { | 448 | if (publication === null) { |
| @@ -616,4 +640,29 @@ data class AudioTrackPublishOptions( | @@ -616,4 +640,29 @@ data class AudioTrackPublishOptions( | ||
| 616 | base.audioBitrate, | 640 | base.audioBitrate, |
| 617 | base.dtx | 641 | base.dtx |
| 618 | ) | 642 | ) |
| 643 | +} | ||
| 644 | + | ||
| 645 | +data class ParticipantTrackPermission( | ||
| 646 | + /** | ||
| 647 | + * The participant id this permission applies to. | ||
| 648 | + */ | ||
| 649 | + val participantSid: String, | ||
| 650 | + /** | ||
| 651 | + * If set to true, the target participant can subscribe to all tracks from the local participant. | ||
| 652 | + * | ||
| 653 | + * Takes precedence over [allowedTrackSids]. | ||
| 654 | + */ | ||
| 655 | + val allTracksAllowed: Boolean, | ||
| 656 | + /** | ||
| 657 | + * The list of track ids that the target participant can subscribe to. | ||
| 658 | + */ | ||
| 659 | + val allowedTrackSids: List<String> = emptyList() | ||
| 660 | +) { | ||
| 661 | + fun toProto(): LivekitRtc.TrackPermission { | ||
| 662 | + return LivekitRtc.TrackPermission.newBuilder() | ||
| 663 | + .setParticipantSid(participantSid) | ||
| 664 | + .setAllTracks(allTracksAllowed) | ||
| 665 | + .addAllTrackSids(allowedTrackSids) | ||
| 666 | + .build() | ||
| 667 | + } | ||
| 619 | } | 668 | } |
| @@ -10,6 +10,7 @@ import kotlinx.coroutines.SupervisorJob | @@ -10,6 +10,7 @@ import kotlinx.coroutines.SupervisorJob | ||
| 10 | import kotlinx.coroutines.delay | 10 | import kotlinx.coroutines.delay |
| 11 | import kotlinx.coroutines.launch | 11 | import kotlinx.coroutines.launch |
| 12 | import livekit.LivekitModels | 12 | import livekit.LivekitModels |
| 13 | +import livekit.LivekitRtc | ||
| 13 | import org.webrtc.AudioTrack | 14 | import org.webrtc.AudioTrack |
| 14 | import org.webrtc.MediaStreamTrack | 15 | import org.webrtc.MediaStreamTrack |
| 15 | import org.webrtc.VideoTrack | 16 | import org.webrtc.VideoTrack |
| @@ -19,8 +20,8 @@ class RemoteParticipant( | @@ -19,8 +20,8 @@ class RemoteParticipant( | ||
| 19 | identity: String? = null, | 20 | identity: String? = null, |
| 20 | val signalClient: SignalClient, | 21 | val signalClient: SignalClient, |
| 21 | private val ioDispatcher: CoroutineDispatcher, | 22 | private val ioDispatcher: CoroutineDispatcher, |
| 22 | - defaultdispatcher: CoroutineDispatcher, | ||
| 23 | -) : Participant(sid, identity, defaultdispatcher) { | 23 | + defaultDispatcher: CoroutineDispatcher, |
| 24 | +) : Participant(sid, identity, defaultDispatcher) { | ||
| 24 | /** | 25 | /** |
| 25 | * @suppress | 26 | * @suppress |
| 26 | */ | 27 | */ |
| @@ -28,18 +29,18 @@ class RemoteParticipant( | @@ -28,18 +29,18 @@ class RemoteParticipant( | ||
| 28 | info: LivekitModels.ParticipantInfo, | 29 | info: LivekitModels.ParticipantInfo, |
| 29 | signalClient: SignalClient, | 30 | signalClient: SignalClient, |
| 30 | ioDispatcher: CoroutineDispatcher, | 31 | ioDispatcher: CoroutineDispatcher, |
| 31 | - defaultdispatcher: CoroutineDispatcher, | 32 | + defaultDispatcher: CoroutineDispatcher, |
| 32 | ) : this( | 33 | ) : this( |
| 33 | info.sid, | 34 | info.sid, |
| 34 | info.identity, | 35 | info.identity, |
| 35 | signalClient, | 36 | signalClient, |
| 36 | ioDispatcher, | 37 | ioDispatcher, |
| 37 | - defaultdispatcher | 38 | + defaultDispatcher |
| 38 | ) { | 39 | ) { |
| 39 | updateFromInfo(info) | 40 | updateFromInfo(info) |
| 40 | } | 41 | } |
| 41 | 42 | ||
| 42 | - private val coroutineScope = CloseableCoroutineScope(SupervisorJob()) | 43 | + private val coroutineScope = CloseableCoroutineScope(defaultDispatcher + SupervisorJob()) |
| 43 | 44 | ||
| 44 | fun getTrackPublication(sid: String): RemoteTrackPublication? = tracks[sid] as? RemoteTrackPublication | 45 | fun getTrackPublication(sid: String): RemoteTrackPublication? = tracks[sid] as? RemoteTrackPublication |
| 45 | 46 | ||
| @@ -98,17 +99,8 @@ class RemoteParticipant( | @@ -98,17 +99,8 @@ class RemoteParticipant( | ||
| 98 | triesLeft: Int = 20 | 99 | triesLeft: Int = 20 |
| 99 | ) { | 100 | ) { |
| 100 | val publication = getTrackPublication(sid) | 101 | val publication = getTrackPublication(sid) |
| 101 | - val track: Track = when (val kind = mediaTrack.kind()) { | ||
| 102 | - KIND_AUDIO -> AudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "") | ||
| 103 | - KIND_VIDEO -> RemoteVideoTrack( | ||
| 104 | - rtcTrack = mediaTrack as VideoTrack, | ||
| 105 | - name = "", | ||
| 106 | - autoManageVideo = autoManageVideo, | ||
| 107 | - dispatcher = ioDispatcher | ||
| 108 | - ) | ||
| 109 | - else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind") | ||
| 110 | - } | ||
| 111 | 102 | ||
| 103 | + // We may receive subscribed tracks before publications come in. Retry until then. | ||
| 112 | if (publication == null) { | 104 | if (publication == null) { |
| 113 | if (triesLeft == 0) { | 105 | if (triesLeft == 0) { |
| 114 | val message = "Could not find published track with sid: $sid" | 106 | val message = "Could not find published track with sid: $sid" |
| @@ -127,6 +119,17 @@ class RemoteParticipant( | @@ -127,6 +119,17 @@ class RemoteParticipant( | ||
| 127 | return | 119 | return |
| 128 | } | 120 | } |
| 129 | 121 | ||
| 122 | + val track: Track = when (val kind = mediaTrack.kind()) { | ||
| 123 | + KIND_AUDIO -> AudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "") | ||
| 124 | + KIND_VIDEO -> RemoteVideoTrack( | ||
| 125 | + rtcTrack = mediaTrack as VideoTrack, | ||
| 126 | + name = "", | ||
| 127 | + autoManageVideo = autoManageVideo, | ||
| 128 | + dispatcher = ioDispatcher | ||
| 129 | + ) | ||
| 130 | + else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind") | ||
| 131 | + } | ||
| 132 | + | ||
| 130 | publication.track = track | 133 | publication.track = track |
| 131 | track.name = publication.name | 134 | track.name = publication.name |
| 132 | track.sid = publication.sid | 135 | track.sid = publication.sid |
| @@ -158,6 +161,19 @@ class RemoteParticipant( | @@ -158,6 +161,19 @@ class RemoteParticipant( | ||
| 158 | } | 161 | } |
| 159 | } | 162 | } |
| 160 | 163 | ||
| 164 | + internal fun onSubscriptionPermissionUpdate(subscriptionPermissionUpdate: LivekitRtc.SubscriptionPermissionUpdate) { | ||
| 165 | + val pub = tracks[subscriptionPermissionUpdate.trackSid] as? RemoteTrackPublication ?: return | ||
| 166 | + | ||
| 167 | + if (pub.subscriptionAllowed != subscriptionPermissionUpdate.allowed) { | ||
| 168 | + pub.subscriptionAllowed = subscriptionPermissionUpdate.allowed | ||
| 169 | + | ||
| 170 | + eventBus.postEvent( | ||
| 171 | + ParticipantEvent.TrackSubscriptionPermissionChanged(this, pub, pub.subscriptionAllowed), | ||
| 172 | + coroutineScope | ||
| 173 | + ) | ||
| 174 | + } | ||
| 175 | + } | ||
| 176 | + | ||
| 161 | // Internal methods just for posting events. | 177 | // Internal methods just for posting events. |
| 162 | internal fun onDataReceived(data: ByteArray) { | 178 | internal fun onDataReceived(data: ByteArray) { |
| 163 | listener?.onDataReceived(data, this) | 179 | listener?.onDataReceived(data, this) |
| @@ -42,20 +42,6 @@ class RemoteTrackPublication( | @@ -42,20 +42,6 @@ class RemoteTrackPublication( | ||
| 42 | } | 42 | } |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | - private fun handleVisibilityChanged(trackEvent: TrackEvent.VisibilityChanged) { | ||
| 46 | - disabled = !trackEvent.isVisible | ||
| 47 | - sendUpdateTrackSettings.invoke() | ||
| 48 | - } | ||
| 49 | - | ||
| 50 | - private fun handleVideoDimensionsChanged(trackEvent: TrackEvent.VideoDimensionsChanged) { | ||
| 51 | - videoDimensions = trackEvent.newDimensions | ||
| 52 | - sendUpdateTrackSettings.invoke() | ||
| 53 | - } | ||
| 54 | - | ||
| 55 | - private fun handleStreamStateChanged(trackEvent: TrackEvent.StreamStateChanged) { | ||
| 56 | - participant.get()?.onTrackStreamStateChanged(trackEvent) | ||
| 57 | - } | ||
| 58 | - | ||
| 59 | private var trackJob: Job? = null | 45 | private var trackJob: Job? = null |
| 60 | 46 | ||
| 61 | private var unsubscribed: Boolean = false | 47 | private var unsubscribed: Boolean = false |
| @@ -63,16 +49,36 @@ class RemoteTrackPublication( | @@ -63,16 +49,36 @@ class RemoteTrackPublication( | ||
| 63 | private var videoQuality: LivekitModels.VideoQuality? = LivekitModels.VideoQuality.HIGH | 49 | private var videoQuality: LivekitModels.VideoQuality? = LivekitModels.VideoQuality.HIGH |
| 64 | private var videoDimensions: Track.Dimensions? = null | 50 | private var videoDimensions: Track.Dimensions? = null |
| 65 | 51 | ||
| 52 | + var subscriptionAllowed: Boolean = true | ||
| 53 | + internal set | ||
| 54 | + | ||
| 66 | val isAutoManaged: Boolean | 55 | val isAutoManaged: Boolean |
| 67 | get() = (track as? RemoteVideoTrack)?.autoManageVideo ?: false | 56 | get() = (track as? RemoteVideoTrack)?.autoManageVideo ?: false |
| 68 | 57 | ||
| 58 | + /** | ||
| 59 | + * Returns true if track is subscribed, and ready for playback | ||
| 60 | + * | ||
| 61 | + * @see [subscriptionStatus] | ||
| 62 | + */ | ||
| 69 | override val subscribed: Boolean | 63 | override val subscribed: Boolean |
| 70 | get() { | 64 | get() { |
| 71 | - if (unsubscribed) { | 65 | + if (unsubscribed || !subscriptionAllowed) { |
| 72 | return false | 66 | return false |
| 73 | } | 67 | } |
| 74 | return super.subscribed | 68 | return super.subscribed |
| 75 | } | 69 | } |
| 70 | + | ||
| 71 | + val subscriptionStatus: SubscriptionStatus | ||
| 72 | + get() { | ||
| 73 | + return if (!unsubscribed || track == null) { | ||
| 74 | + SubscriptionStatus.UNSUBSCRIBED | ||
| 75 | + } else if (!subscriptionAllowed) { | ||
| 76 | + SubscriptionStatus.SUBSCRIBED_AND_NOT_ALLOWED | ||
| 77 | + } else { | ||
| 78 | + SubscriptionStatus.SUBSCRIBED | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + | ||
| 76 | override var muted: Boolean = false | 82 | override var muted: Boolean = false |
| 77 | set(v) { | 83 | set(v) { |
| 78 | if (field == v) { | 84 | if (field == v) { |
| @@ -88,12 +94,11 @@ class RemoteTrackPublication( | @@ -88,12 +94,11 @@ class RemoteTrackPublication( | ||
| 88 | } | 94 | } |
| 89 | 95 | ||
| 90 | /** | 96 | /** |
| 91 | - * subscribe or unsubscribe from this track | 97 | + * Subscribe or unsubscribe from this track |
| 92 | */ | 98 | */ |
| 93 | fun setSubscribed(subscribed: Boolean) { | 99 | fun setSubscribed(subscribed: Boolean) { |
| 94 | unsubscribed = !subscribed | 100 | unsubscribed = !subscribed |
| 95 | val participant = this.participant.get() as? RemoteParticipant ?: return | 101 | val participant = this.participant.get() as? RemoteParticipant ?: return |
| 96 | - | ||
| 97 | participant.signalClient.sendUpdateSubscription(sid, !unsubscribed) | 102 | participant.signalClient.sendUpdateSubscription(sid, !unsubscribed) |
| 98 | } | 103 | } |
| 99 | 104 | ||
| @@ -147,6 +152,20 @@ class RemoteTrackPublication( | @@ -147,6 +152,20 @@ class RemoteTrackPublication( | ||
| 147 | sendUpdateTrackSettings.invoke() | 152 | sendUpdateTrackSettings.invoke() |
| 148 | } | 153 | } |
| 149 | 154 | ||
| 155 | + private fun handleVisibilityChanged(trackEvent: TrackEvent.VisibilityChanged) { | ||
| 156 | + disabled = !trackEvent.isVisible | ||
| 157 | + sendUpdateTrackSettings.invoke() | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + private fun handleVideoDimensionsChanged(trackEvent: TrackEvent.VideoDimensionsChanged) { | ||
| 161 | + videoDimensions = trackEvent.newDimensions | ||
| 162 | + sendUpdateTrackSettings.invoke() | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + private fun handleStreamStateChanged(trackEvent: TrackEvent.StreamStateChanged) { | ||
| 166 | + participant.get()?.onTrackStreamStateChanged(trackEvent) | ||
| 167 | + } | ||
| 168 | + | ||
| 150 | // Debounce just in case multiple settings get changed at once. | 169 | // Debounce just in case multiple settings get changed at once. |
| 151 | private val sendUpdateTrackSettings = debounce<Unit, Unit>(100L, CoroutineScope(ioDispatcher)) { | 170 | private val sendUpdateTrackSettings = debounce<Unit, Unit>(100L, CoroutineScope(ioDispatcher)) { |
| 152 | sendUpdateTrackSettingsImpl() | 171 | sendUpdateTrackSettingsImpl() |
| @@ -162,4 +181,21 @@ class RemoteTrackPublication( | @@ -162,4 +181,21 @@ class RemoteTrackPublication( | ||
| 162 | videoQuality | 181 | videoQuality |
| 163 | ) | 182 | ) |
| 164 | } | 183 | } |
| 184 | + | ||
| 185 | + enum class SubscriptionStatus { | ||
| 186 | + /** | ||
| 187 | + * Has a valid track, receiving data. | ||
| 188 | + */ | ||
| 189 | + SUBSCRIBED, | ||
| 190 | + | ||
| 191 | + /** | ||
| 192 | + * Has a track, but no data will be received due to permissions. | ||
| 193 | + */ | ||
| 194 | + SUBSCRIBED_AND_NOT_ALLOWED, | ||
| 195 | + | ||
| 196 | + /** | ||
| 197 | + * Not subscribed. | ||
| 198 | + */ | ||
| 199 | + UNSUBSCRIBED | ||
| 200 | + } | ||
| 165 | } | 201 | } |
| @@ -4,6 +4,9 @@ import org.webrtc.AudioTrack | @@ -4,6 +4,9 @@ import org.webrtc.AudioTrack | ||
| 4 | import org.webrtc.MediaStream | 4 | import org.webrtc.MediaStream |
| 5 | import org.webrtc.VideoTrack | 5 | import org.webrtc.VideoTrack |
| 6 | 6 | ||
| 7 | +fun createMediaStreamId(participantSid: String, trackSid: String) = | ||
| 8 | + "${TestData.REMOTE_PARTICIPANT.sid}|${TestData.REMOTE_AUDIO_TRACK.sid}" | ||
| 9 | + | ||
| 7 | class MockMediaStream(private val id: String = "id") : MediaStream(1L) { | 10 | class MockMediaStream(private val id: String = "id") : MediaStream(1L) { |
| 8 | 11 | ||
| 9 | override fun addTrack(track: AudioTrack): Boolean { | 12 | override fun addTrack(track: AudioTrack): Boolean { |
| @@ -9,10 +9,10 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher | @@ -9,10 +9,10 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher | ||
| 9 | import javax.inject.Named | 9 | import javax.inject.Named |
| 10 | 10 | ||
| 11 | @Module | 11 | @Module |
| 12 | -object TestCoroutinesModule { | ||
| 13 | - | 12 | +class TestCoroutinesModule( |
| 14 | @OptIn(ExperimentalCoroutinesApi::class) | 13 | @OptIn(ExperimentalCoroutinesApi::class) |
| 15 | val coroutineDispatcher: CoroutineDispatcher = TestCoroutineDispatcher() | 14 | val coroutineDispatcher: CoroutineDispatcher = TestCoroutineDispatcher() |
| 15 | +) { | ||
| 16 | 16 | ||
| 17 | @Provides | 17 | @Provides |
| 18 | @Named(InjectionNames.DISPATCHER_DEFAULT) | 18 | @Named(InjectionNames.DISPATCHER_DEFAULT) |
| @@ -23,6 +23,6 @@ interface TestLiveKitComponent : LiveKitComponent { | @@ -23,6 +23,6 @@ interface TestLiveKitComponent : LiveKitComponent { | ||
| 23 | 23 | ||
| 24 | @Component.Factory | 24 | @Component.Factory |
| 25 | interface Factory { | 25 | interface Factory { |
| 26 | - fun create(@BindsInstance appContext: Context): TestLiveKitComponent | 26 | + fun create(@BindsInstance appContext: Context, coroutinesModule: TestCoroutinesModule = TestCoroutinesModule()): TestLiveKitComponent |
| 27 | } | 27 | } |
| 28 | } | 28 | } |
| @@ -4,16 +4,17 @@ import android.content.Context | @@ -4,16 +4,17 @@ import android.content.Context | ||
| 4 | import androidx.test.core.app.ApplicationProvider | 4 | import androidx.test.core.app.ApplicationProvider |
| 5 | import io.livekit.android.coroutines.TestCoroutineRule | 5 | import io.livekit.android.coroutines.TestCoroutineRule |
| 6 | import io.livekit.android.events.EventCollector | 6 | import io.livekit.android.events.EventCollector |
| 7 | +import io.livekit.android.events.ParticipantEvent | ||
| 7 | import io.livekit.android.events.RoomEvent | 8 | import io.livekit.android.events.RoomEvent |
| 8 | import io.livekit.android.mock.* | 9 | import io.livekit.android.mock.* |
| 9 | import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent | 10 | import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent |
| 11 | +import io.livekit.android.mock.dagger.TestCoroutinesModule | ||
| 10 | import io.livekit.android.room.participant.ConnectionQuality | 12 | import io.livekit.android.room.participant.ConnectionQuality |
| 11 | import io.livekit.android.room.track.Track | 13 | import io.livekit.android.room.track.Track |
| 12 | import io.livekit.android.util.toOkioByteString | 14 | import io.livekit.android.util.toOkioByteString |
| 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi | 15 | import kotlinx.coroutines.ExperimentalCoroutinesApi |
| 14 | import kotlinx.coroutines.launch | 16 | import kotlinx.coroutines.launch |
| 15 | import kotlinx.coroutines.test.runBlockingTest | 17 | import kotlinx.coroutines.test.runBlockingTest |
| 16 | -import livekit.LivekitRtc | ||
| 17 | import org.junit.Assert | 18 | import org.junit.Assert |
| 18 | import org.junit.Before | 19 | import org.junit.Before |
| 19 | import org.junit.Rule | 20 | import org.junit.Rule |
| @@ -41,7 +42,7 @@ class RoomMockE2ETest { | @@ -41,7 +42,7 @@ class RoomMockE2ETest { | ||
| 41 | context = ApplicationProvider.getApplicationContext() | 42 | context = ApplicationProvider.getApplicationContext() |
| 42 | val component = DaggerTestLiveKitComponent | 43 | val component = DaggerTestLiveKitComponent |
| 43 | .factory() | 44 | .factory() |
| 44 | - .create(context) | 45 | + .create(context, TestCoroutinesModule(coroutineRule.dispatcher)) |
| 45 | 46 | ||
| 46 | room = component.roomFactory() | 47 | room = component.roomFactory() |
| 47 | .create(context) | 48 | .create(context) |
| @@ -55,9 +56,10 @@ class RoomMockE2ETest { | @@ -55,9 +56,10 @@ class RoomMockE2ETest { | ||
| 55 | token = "", | 56 | token = "", |
| 56 | ) | 57 | ) |
| 57 | } | 58 | } |
| 58 | - | ||
| 59 | wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.JOIN.toOkioByteString()) | 59 | wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.JOIN.toOkioByteString()) |
| 60 | 60 | ||
| 61 | + // PeerTransport negotiation is on a debounce delay. | ||
| 62 | + coroutineRule.dispatcher.advanceTimeBy(1000L) | ||
| 61 | runBlockingTest { | 63 | runBlockingTest { |
| 62 | job.join() | 64 | job.join() |
| 63 | } | 65 | } |
| @@ -69,7 +71,7 @@ class RoomMockE2ETest { | @@ -69,7 +71,7 @@ class RoomMockE2ETest { | ||
| 69 | } | 71 | } |
| 70 | 72 | ||
| 71 | @Test | 73 | @Test |
| 72 | - fun connectFailureProperlyContinues(){ | 74 | + fun connectFailureProperlyContinues() { |
| 73 | 75 | ||
| 74 | var didThrowException = false | 76 | var didThrowException = false |
| 75 | val job = coroutineRule.scope.launch { | 77 | val job = coroutineRule.scope.launch { |
| @@ -91,6 +93,7 @@ class RoomMockE2ETest { | @@ -91,6 +93,7 @@ class RoomMockE2ETest { | ||
| 91 | 93 | ||
| 92 | Assert.assertTrue(didThrowException) | 94 | Assert.assertTrue(didThrowException) |
| 93 | } | 95 | } |
| 96 | + | ||
| 94 | @Test | 97 | @Test |
| 95 | fun roomUpdateTest() { | 98 | fun roomUpdateTest() { |
| 96 | connect() | 99 | connect() |
| @@ -203,7 +206,14 @@ class RoomMockE2ETest { | @@ -203,7 +206,14 @@ class RoomMockE2ETest { | ||
| 203 | // add track. | 206 | // add track. |
| 204 | room.onAddTrack( | 207 | room.onAddTrack( |
| 205 | MockAudioStreamTrack(), | 208 | MockAudioStreamTrack(), |
| 206 | - arrayOf(MockMediaStream(id = "${TestData.REMOTE_PARTICIPANT.sid}|${TestData.REMOTE_AUDIO_TRACK.sid}")) | 209 | + arrayOf( |
| 210 | + MockMediaStream( | ||
| 211 | + id = createMediaStreamId( | ||
| 212 | + TestData.REMOTE_PARTICIPANT.sid, | ||
| 213 | + TestData.REMOTE_AUDIO_TRACK.sid | ||
| 214 | + ) | ||
| 215 | + ) | ||
| 216 | + ) | ||
| 207 | ) | 217 | ) |
| 208 | val eventCollector = EventCollector(room.events, coroutineRule.scope) | 218 | val eventCollector = EventCollector(room.events, coroutineRule.scope) |
| 209 | wsFactory.listener.onMessage( | 219 | wsFactory.listener.onMessage( |
| @@ -220,6 +230,41 @@ class RoomMockE2ETest { | @@ -220,6 +230,41 @@ class RoomMockE2ETest { | ||
| 220 | } | 230 | } |
| 221 | 231 | ||
| 222 | @Test | 232 | @Test |
| 233 | + fun trackSubscriptionPermissionChanged() { | ||
| 234 | + connect() | ||
| 235 | + | ||
| 236 | + wsFactory.listener.onMessage( | ||
| 237 | + wsFactory.ws, | ||
| 238 | + SignalClientTest.PARTICIPANT_JOIN.toOkioByteString() | ||
| 239 | + ) | ||
| 240 | + room.onAddTrack( | ||
| 241 | + MockAudioStreamTrack(), | ||
| 242 | + arrayOf( | ||
| 243 | + MockMediaStream( | ||
| 244 | + id = createMediaStreamId( | ||
| 245 | + TestData.REMOTE_PARTICIPANT.sid, | ||
| 246 | + TestData.REMOTE_AUDIO_TRACK.sid | ||
| 247 | + ) | ||
| 248 | + ) | ||
| 249 | + ) | ||
| 250 | + ) | ||
| 251 | + val eventCollector = EventCollector(room.events, coroutineRule.scope) | ||
| 252 | + wsFactory.listener.onMessage( | ||
| 253 | + wsFactory.ws, | ||
| 254 | + SignalClientTest.SUBSCRIPTION_PERMISSION_UPDATE.toOkioByteString() | ||
| 255 | + ) | ||
| 256 | + val events = eventCollector.stopCollecting() | ||
| 257 | + | ||
| 258 | + Assert.assertEquals(1, events.size) | ||
| 259 | + Assert.assertEquals(true, events[0] is RoomEvent.TrackSubscriptionPermissionChanged) | ||
| 260 | + | ||
| 261 | + val event = events[0] as RoomEvent.TrackSubscriptionPermissionChanged | ||
| 262 | + Assert.assertEquals(TestData.REMOTE_PARTICIPANT.sid, event.participant.sid) | ||
| 263 | + Assert.assertEquals(TestData.REMOTE_AUDIO_TRACK.sid, event.trackPublication.sid) | ||
| 264 | + Assert.assertEquals(false, event.subscriptionAllowed) | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + @Test | ||
| 223 | fun leave() { | 268 | fun leave() { |
| 224 | connect() | 269 | connect() |
| 225 | val eventCollector = EventCollector(room.events, coroutineRule.scope) | 270 | val eventCollector = EventCollector(room.events, coroutineRule.scope) |
| @@ -241,6 +241,16 @@ class SignalClientTest { | @@ -241,6 +241,16 @@ class SignalClientTest { | ||
| 241 | } | 241 | } |
| 242 | build() | 242 | build() |
| 243 | } | 243 | } |
| 244 | + | ||
| 245 | + val SUBSCRIPTION_PERMISSION_UPDATE = with(LivekitRtc.SignalResponse.newBuilder()) { | ||
| 246 | + subscriptionPermissionUpdate = with(LivekitRtc.SubscriptionPermissionUpdate.newBuilder()) { | ||
| 247 | + participantSid = TestData.REMOTE_PARTICIPANT.sid | ||
| 248 | + trackSid = TestData.REMOTE_AUDIO_TRACK.sid | ||
| 249 | + allowed = false | ||
| 250 | + build() | ||
| 251 | + } | ||
| 252 | + build() | ||
| 253 | + } | ||
| 244 | val LEAVE = with(LivekitRtc.SignalResponse.newBuilder()) { | 254 | val LEAVE = with(LivekitRtc.SignalResponse.newBuilder()) { |
| 245 | leave = with(leaveBuilder) { | 255 | leave = with(leaveBuilder) { |
| 246 | build() | 256 | build() |
| @@ -24,7 +24,7 @@ class RemoteParticipantTest { | @@ -24,7 +24,7 @@ class RemoteParticipantTest { | ||
| 24 | "sid", | 24 | "sid", |
| 25 | signalClient = signalClient, | 25 | signalClient = signalClient, |
| 26 | ioDispatcher = coroutineRule.dispatcher, | 26 | ioDispatcher = coroutineRule.dispatcher, |
| 27 | - defaultdispatcher = coroutineRule.dispatcher, | 27 | + defaultDispatcher = coroutineRule.dispatcher, |
| 28 | ) | 28 | ) |
| 29 | } | 29 | } |
| 30 | 30 | ||
| @@ -38,7 +38,7 @@ class RemoteParticipantTest { | @@ -38,7 +38,7 @@ class RemoteParticipantTest { | ||
| 38 | info, | 38 | info, |
| 39 | signalClient, | 39 | signalClient, |
| 40 | ioDispatcher = coroutineRule.dispatcher, | 40 | ioDispatcher = coroutineRule.dispatcher, |
| 41 | - defaultdispatcher = coroutineRule.dispatcher, | 41 | + defaultDispatcher = coroutineRule.dispatcher, |
| 42 | ) | 42 | ) |
| 43 | 43 | ||
| 44 | assertEquals(1, participant.tracks.values.size) | 44 | assertEquals(1, participant.tracks.values.size) |
-
请 注册 或 登录 后发表评论