Committed by
GitHub
Fast track publication support (#612)
* protocol update * Fast track publication support
正在显示
12 个修改的文件
包含
193 行增加
和
91 行删除
.changeset/six-flowers-hunt.md
0 → 100644
| 1 | <component name="InspectionProjectProfileManager"> | 1 | <component name="InspectionProjectProfileManager"> |
| 2 | <profile version="1.0"> | 2 | <profile version="1.0"> |
| 3 | <option name="myName" value="Project Default" /> | 3 | <option name="myName" value="Project Default" /> |
| 4 | + <inspection_tool class="AndroidLintVisibleForTests" enabled="true" level="WARNING" enabled_by_default="true"> | ||
| 5 | + <scope name="Library Projects" level="WARNING" enabled="false" /> | ||
| 6 | + </inspection_tool> | ||
| 7 | + <inspection_tool class="MemberVisibilityCanBePrivate" enabled="true" level="WEAK WARNING" enabled_by_default="true"> | ||
| 8 | + <scope name="Library Projects" level="WEAK WARNING" enabled="false" /> | ||
| 9 | + </inspection_tool> | ||
| 4 | <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> | 10 | <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> |
| 5 | <option name="composableFile" value="true" /> | 11 | <option name="composableFile" value="true" /> |
| 6 | <option name="previewFile" value="true" /> | 12 | <option name="previewFile" value="true" /> |
| @@ -37,5 +43,8 @@ | @@ -37,5 +43,8 @@ | ||
| 37 | <option name="composableFile" value="true" /> | 43 | <option name="composableFile" value="true" /> |
| 38 | <option name="previewFile" value="true" /> | 44 | <option name="previewFile" value="true" /> |
| 39 | </inspection_tool> | 45 | </inspection_tool> |
| 46 | + <inspection_tool class="UnusedSymbol" enabled="true" level="WARNING" enabled_by_default="true"> | ||
| 47 | + <scope name="Library Projects" level="WARNING" enabled="false" /> | ||
| 48 | + </inspection_tool> | ||
| 40 | </profile> | 49 | </profile> |
| 41 | </component> | 50 | </component> |
.idea/scopes/Library_Projects.xml
0 → 100644
| 1 | +<component name="DependencyValidationManager"> | ||
| 2 | + <scope name="Library Projects" pattern="file[livekit-android.livekit-android-sdk*]:*//*||file[livekit-android.livekit-android-camerax*]:*//*||file[livekit-android.livekit-android-test*]:*//*||file[livekit-android.livekit-lint*]:*//*" /> | ||
| 3 | +</component> |
| 1 | /* | 1 | /* |
| 2 | - * Copyright 2023-2024 LiveKit, Inc. | 2 | + * Copyright 2023-2025 LiveKit, Inc. |
| 3 | * | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. | 5 | * you may not use this file except in compliance with the License. |
| @@ -131,7 +131,7 @@ constructor( | @@ -131,7 +131,7 @@ constructor( | ||
| 131 | return result | 131 | return result |
| 132 | } | 132 | } |
| 133 | 133 | ||
| 134 | - val negotiate = debounce<MediaConstraints?, Unit>(100, coroutineScope) { | 134 | + val negotiate = debounce<MediaConstraints?, Unit>(20, coroutineScope) { |
| 135 | if (it != null) { | 135 | if (it != null) { |
| 136 | createAndSendOffer(it) | 136 | createAndSendOffer(it) |
| 137 | } else { | 137 | } else { |
| @@ -156,7 +156,7 @@ internal constructor( | @@ -156,7 +156,7 @@ internal constructor( | ||
| 156 | private val publisherObserver = PublisherTransportObserver(this, client) | 156 | private val publisherObserver = PublisherTransportObserver(this, client) |
| 157 | private val subscriberObserver = SubscriberTransportObserver(this, client) | 157 | private val subscriberObserver = SubscriberTransportObserver(this, client) |
| 158 | 158 | ||
| 159 | - private var publisher: PeerConnectionTransport? = null | 159 | + internal var publisher: PeerConnectionTransport? = null |
| 160 | private var subscriber: PeerConnectionTransport? = null | 160 | private var subscriber: PeerConnectionTransport? = null |
| 161 | 161 | ||
| 162 | private var reliableDataChannel: DataChannel? = null | 162 | private var reliableDataChannel: DataChannel? = null |
| @@ -214,7 +214,7 @@ internal constructor( | @@ -214,7 +214,7 @@ internal constructor( | ||
| 214 | configure(joinResponse, options) | 214 | configure(joinResponse, options) |
| 215 | 215 | ||
| 216 | // create offer | 216 | // create offer |
| 217 | - if (!isSubscriberPrimary) { | 217 | + if (!isSubscriberPrimary || joinResponse.fastPublish) { |
| 218 | negotiatePublisher() | 218 | negotiatePublisher() |
| 219 | } | 219 | } |
| 220 | client.onReadyForResponses() | 220 | client.onReadyForResponses() |
| @@ -1082,6 +1082,10 @@ internal constructor( | @@ -1082,6 +1082,10 @@ internal constructor( | ||
| 1082 | LivekitModels.DataPacket.ValueCase.STREAM_CHUNK -> { | 1082 | LivekitModels.DataPacket.ValueCase.STREAM_CHUNK -> { |
| 1083 | // TODO | 1083 | // TODO |
| 1084 | } | 1084 | } |
| 1085 | + | ||
| 1086 | + LivekitModels.DataPacket.ValueCase.STREAM_TRAILER -> { | ||
| 1087 | + // TODO | ||
| 1088 | + } | ||
| 1085 | } | 1089 | } |
| 1086 | } | 1090 | } |
| 1087 | 1091 |
| @@ -585,6 +585,7 @@ constructor( | @@ -585,6 +585,7 @@ constructor( | ||
| 585 | } | 585 | } |
| 586 | 586 | ||
| 587 | localParticipant.updateFromInfo(response.participant) | 587 | localParticipant.updateFromInfo(response.participant) |
| 588 | + localParticipant.setEnabledPublishCodecs(response.enabledPublishCodecsList) | ||
| 588 | 589 | ||
| 589 | if (response.otherParticipantsList.isNotEmpty()) { | 590 | if (response.otherParticipantsList.isNotEmpty()) { |
| 590 | response.otherParticipantsList.forEach { info -> | 591 | response.otherParticipantsList.forEach { info -> |
| @@ -57,6 +57,7 @@ import io.livekit.android.util.flow | @@ -57,6 +57,7 @@ import io.livekit.android.util.flow | ||
| 57 | import io.livekit.android.webrtc.sortVideoCodecPreferences | 57 | import io.livekit.android.webrtc.sortVideoCodecPreferences |
| 58 | import kotlinx.coroutines.CoroutineDispatcher | 58 | import kotlinx.coroutines.CoroutineDispatcher |
| 59 | import kotlinx.coroutines.Job | 59 | import kotlinx.coroutines.Job |
| 60 | +import kotlinx.coroutines.async | ||
| 60 | import kotlinx.coroutines.coroutineScope | 61 | import kotlinx.coroutines.coroutineScope |
| 61 | import kotlinx.coroutines.delay | 62 | import kotlinx.coroutines.delay |
| 62 | import kotlinx.coroutines.launch | 63 | import kotlinx.coroutines.launch |
| @@ -64,7 +65,9 @@ import kotlinx.coroutines.suspendCancellableCoroutine | @@ -64,7 +65,9 @@ import kotlinx.coroutines.suspendCancellableCoroutine | ||
| 64 | import kotlinx.coroutines.sync.Mutex | 65 | import kotlinx.coroutines.sync.Mutex |
| 65 | import kotlinx.coroutines.sync.withLock | 66 | import kotlinx.coroutines.sync.withLock |
| 66 | import livekit.LivekitModels | 67 | import livekit.LivekitModels |
| 68 | +import livekit.LivekitModels.Codec | ||
| 67 | import livekit.LivekitModels.DataPacket | 69 | import livekit.LivekitModels.DataPacket |
| 70 | +import livekit.LivekitModels.TrackInfo | ||
| 68 | import livekit.LivekitRtc | 71 | import livekit.LivekitRtc |
| 69 | import livekit.LivekitRtc.AddTrackRequest | 72 | import livekit.LivekitRtc.AddTrackRequest |
| 70 | import livekit.LivekitRtc.SimulcastCodec | 73 | import livekit.LivekitRtc.SimulcastCodec |
| @@ -127,8 +130,9 @@ internal constructor( | @@ -127,8 +130,9 @@ internal constructor( | ||
| 127 | // For ensuring that only one caller can execute setTrackEnabled at a time. | 130 | // For ensuring that only one caller can execute setTrackEnabled at a time. |
| 128 | // Without it, there's a potential to create multiple of the same source, | 131 | // Without it, there's a potential to create multiple of the same source, |
| 129 | // Camera has deadlock issues with multiple CameraCapturers trying to activate/stop. | 132 | // Camera has deadlock issues with multiple CameraCapturers trying to activate/stop. |
| 130 | - private val sourcePubLocks = Track.Source.values() | ||
| 131 | - .associate { source -> source to Mutex() } | 133 | + private val sourcePubLocks = Track.Source.entries.associateWith { Mutex() } |
| 134 | + | ||
| 135 | + internal val enabledPublishVideoCodecs = Collections.synchronizedList(mutableListOf<Codec>()) | ||
| 132 | 136 | ||
| 133 | /** | 137 | /** |
| 134 | * Creates an audio track, recording audio through the microphone with the given [options]. | 138 | * Creates an audio track, recording audio through the microphone with the given [options]. |
| @@ -405,9 +409,26 @@ internal constructor( | @@ -405,9 +409,26 @@ internal constructor( | ||
| 405 | options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults), | 409 | options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults), |
| 406 | publishListener: PublishListener? = null, | 410 | publishListener: PublishListener? = null, |
| 407 | ) { | 411 | ) { |
| 412 | + @Suppress("NAME_SHADOWING") var options = options | ||
| 413 | + | ||
| 414 | + synchronized(enabledPublishVideoCodecs) { | ||
| 415 | + if (enabledPublishVideoCodecs.isNotEmpty()) { | ||
| 416 | + if (enabledPublishVideoCodecs.none { allowedCodec -> allowedCodec.mime.mimeTypeToVideoCodec() == options.videoCodec }) { | ||
| 417 | + val oldCodec = options.videoCodec | ||
| 418 | + val newCodec = enabledPublishVideoCodecs | ||
| 419 | + .firstOrNull { it.mime.mimeTypeToVideoCodec() != null } | ||
| 420 | + ?.mime?.mimeTypeToVideoCodec() | ||
| 421 | + | ||
| 422 | + if (newCodec != null) { | ||
| 423 | + LKLog.w { "$oldCodec not enabled on server, falling back to supported codec $newCodec" } | ||
| 424 | + options = options.copy(videoCodec = newCodec) | ||
| 425 | + } | ||
| 426 | + } | ||
| 427 | + } | ||
| 428 | + } | ||
| 429 | + | ||
| 408 | val isSVC = isSVCCodec(options.videoCodec) | 430 | val isSVC = isSVCCodec(options.videoCodec) |
| 409 | 431 | ||
| 410 | - @Suppress("NAME_SHADOWING") var options = options | ||
| 411 | if (isSVC) { | 432 | if (isSVC) { |
| 412 | dynacast = true | 433 | dynacast = true |
| 413 | 434 | ||
| @@ -478,84 +499,122 @@ internal constructor( | @@ -478,84 +499,122 @@ internal constructor( | ||
| 478 | return null | 499 | return null |
| 479 | } | 500 | } |
| 480 | 501 | ||
| 481 | - val cid = track.rtcTrack.id() | ||
| 482 | - val builder = AddTrackRequest.newBuilder().apply { | ||
| 483 | - this.requestConfig() | 502 | + if (engine.connectionState == ConnectionState.DISCONNECTED) { |
| 503 | + publishListener?.onPublishFailure(TrackException.PublishException("Not connected!")) | ||
| 484 | } | 504 | } |
| 485 | 505 | ||
| 486 | - val trackInfo = try { | ||
| 487 | - engine.addTrack( | ||
| 488 | - cid = cid, | ||
| 489 | - name = options.name ?: track.name, | ||
| 490 | - kind = track.kind.toProto(), | ||
| 491 | - stream = options.stream, | ||
| 492 | - builder = builder, | 506 | + val cid = track.rtcTrack.id() |
| 507 | + | ||
| 508 | + // For fast publish, we can negotiate PC and request add track at the same time | ||
| 509 | + suspend fun negotiate() { | ||
| 510 | + if (this.engine.publisher == null) { | ||
| 511 | + throw IllegalStateException("publisher is not configured yet!") | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + val transInit = RtpTransceiverInit( | ||
| 515 | + RtpTransceiver.RtpTransceiverDirection.SEND_ONLY, | ||
| 516 | + listOf(this.sid.value), | ||
| 517 | + encodings, | ||
| 493 | ) | 518 | ) |
| 494 | - } catch (e: Exception) { | ||
| 495 | - publishListener?.onPublishFailure(TrackException.PublishException("Failed to publish track", e)) | ||
| 496 | - return null | ||
| 497 | - } | 519 | + val transceiver = engine.createSenderTransceiver(track.rtcTrack, transInit) |
| 498 | 520 | ||
| 499 | - if (options is VideoTrackPublishOptions) { | ||
| 500 | - // server might not support the codec the client has requested, in that case, fallback | ||
| 501 | - // to a supported codec | ||
| 502 | - val primaryCodecMime = trackInfo.codecsList.firstOrNull()?.mimeType | 521 | + when (track) { |
| 522 | + is LocalVideoTrack -> track.transceiver = transceiver | ||
| 523 | + is LocalAudioTrack -> track.transceiver = transceiver | ||
| 524 | + else -> { | ||
| 525 | + throw IllegalArgumentException("Trying to publish a non local track of type ${track.javaClass}") | ||
| 526 | + } | ||
| 527 | + } | ||
| 503 | 528 | ||
| 504 | - if (primaryCodecMime != null) { | ||
| 505 | - val updatedCodec = primaryCodecMime.mimeTypeToVideoCodec() | ||
| 506 | - if (updatedCodec != null && updatedCodec != options.videoCodec) { | ||
| 507 | - LKLog.d { "falling back to server selected codec: $updatedCodec" } | ||
| 508 | - options = options.copy(videoCodec = updatedCodec) | 529 | + if (transceiver == null) { |
| 530 | + val exception = TrackException.PublishException("null sender returned from peer connection") | ||
| 531 | + publishListener?.onPublishFailure(exception) | ||
| 532 | + throw exception | ||
| 533 | + } | ||
| 509 | 534 | ||
| 510 | - // recompute encodings since bitrates/etc could have changed | ||
| 511 | - encodings = computeVideoEncodings((track as LocalVideoTrack).dimensions, options) | 535 | + track.statsGetter = engine.createStatsGetter(transceiver.sender) |
| 536 | + | ||
| 537 | + val finalOptions = options | ||
| 538 | + // Handle trackBitrates | ||
| 539 | + if (encodings.isNotEmpty()) { | ||
| 540 | + if (finalOptions is VideoTrackPublishOptions && isSVCCodec(finalOptions.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) { | ||
| 541 | + engine.registerTrackBitrateInfo( | ||
| 542 | + cid = cid, | ||
| 543 | + TrackBitrateInfo( | ||
| 544 | + codec = finalOptions.videoCodec, | ||
| 545 | + maxBitrate = (encodings.first().maxBitrateBps?.div(1000) ?: 0).toLong(), | ||
| 546 | + ), | ||
| 547 | + ) | ||
| 512 | } | 548 | } |
| 513 | } | 549 | } |
| 514 | - } | ||
| 515 | 550 | ||
| 516 | - val transInit = RtpTransceiverInit( | ||
| 517 | - RtpTransceiver.RtpTransceiverDirection.SEND_ONLY, | ||
| 518 | - listOf(this.sid.value), | ||
| 519 | - encodings, | ||
| 520 | - ) | ||
| 521 | - val transceiver = engine.createSenderTransceiver(track.rtcTrack, transInit) | 551 | + if (finalOptions is VideoTrackPublishOptions) { |
| 552 | + // Set preferred video codec order | ||
| 553 | + transceiver.sortVideoCodecPreferences(finalOptions.videoCodec, capabilitiesGetter) | ||
| 554 | + (track as LocalVideoTrack).codec = finalOptions.videoCodec | ||
| 522 | 555 | ||
| 523 | - when (track) { | ||
| 524 | - is LocalVideoTrack -> track.transceiver = transceiver | ||
| 525 | - is LocalAudioTrack -> track.transceiver = transceiver | ||
| 526 | - else -> { | ||
| 527 | - throw IllegalArgumentException("Trying to publish a non local track of type ${track.javaClass}") | 556 | + val rtpParameters = transceiver.sender.parameters |
| 557 | + rtpParameters.degradationPreference = finalOptions.degradationPreference | ||
| 558 | + transceiver.sender.parameters = rtpParameters | ||
| 528 | } | 559 | } |
| 529 | - } | ||
| 530 | 560 | ||
| 531 | - if (transceiver == null) { | ||
| 532 | - publishListener?.onPublishFailure(TrackException.PublishException("null sender returned from peer connection")) | ||
| 533 | - return null | 561 | + // PublisherTransportObserver.onRenegotiationNeeded() gets triggered automatically |
| 562 | + // so no need to call negotiate manually. | ||
| 534 | } | 563 | } |
| 535 | 564 | ||
| 536 | - track.statsGetter = engine.createStatsGetter(transceiver.sender) | 565 | + suspend fun requestAddTrack(): TrackInfo { |
| 566 | + val builder = AddTrackRequest.newBuilder().apply { | ||
| 567 | + this.requestConfig() | ||
| 568 | + } | ||
| 537 | 569 | ||
| 538 | - // Handle trackBitrates | ||
| 539 | - if (encodings.isNotEmpty()) { | ||
| 540 | - if (options is VideoTrackPublishOptions && isSVCCodec(options.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) { | ||
| 541 | - engine.registerTrackBitrateInfo( | 570 | + return try { |
| 571 | + engine.addTrack( | ||
| 542 | cid = cid, | 572 | cid = cid, |
| 543 | - TrackBitrateInfo( | ||
| 544 | - codec = options.videoCodec, | ||
| 545 | - maxBitrate = (encodings.first().maxBitrateBps?.div(1000) ?: 0).toLong(), | ||
| 546 | - ), | 573 | + name = options.name ?: track.name, |
| 574 | + kind = track.kind.toProto(), | ||
| 575 | + stream = options.stream, | ||
| 576 | + builder = builder, | ||
| 547 | ) | 577 | ) |
| 578 | + } catch (e: Exception) { | ||
| 579 | + val exception = TrackException.PublishException("Failed to publish track", e) | ||
| 580 | + publishListener?.onPublishFailure(exception) | ||
| 581 | + throw exception | ||
| 548 | } | 582 | } |
| 549 | } | 583 | } |
| 550 | 584 | ||
| 551 | - if (options is VideoTrackPublishOptions) { | ||
| 552 | - // Set preferred video codec order | ||
| 553 | - transceiver.sortVideoCodecPreferences(options.videoCodec, capabilitiesGetter) | ||
| 554 | - (track as LocalVideoTrack).codec = options.videoCodec | 585 | + val trackInfo: TrackInfo |
| 586 | + if (enabledPublishVideoCodecs.isNotEmpty()) { | ||
| 587 | + // Can simultaneous publish and negotiate. | ||
| 588 | + // codec is pre-verified in publishVideoTrack | ||
| 589 | + trackInfo = coroutineScope { | ||
| 590 | + val negotiateJob = launch { negotiate() } | ||
| 591 | + val publishJob = async { requestAddTrack() } | ||
| 592 | + | ||
| 593 | + negotiateJob.join() | ||
| 594 | + return@coroutineScope publishJob.await() | ||
| 595 | + } | ||
| 596 | + } else { | ||
| 597 | + // legacy path. | ||
| 598 | + trackInfo = requestAddTrack() | ||
| 599 | + | ||
| 600 | + if (options is VideoTrackPublishOptions) { | ||
| 601 | + // server might not support the codec the client has requested, in that case, fallback | ||
| 602 | + // to a supported codec | ||
| 603 | + val primaryCodecMime = trackInfo.codecsList.firstOrNull()?.mimeType | ||
| 604 | + | ||
| 605 | + if (primaryCodecMime != null) { | ||
| 606 | + val updatedCodec = primaryCodecMime.mimeTypeToVideoCodec() | ||
| 607 | + if (updatedCodec != null && updatedCodec != options.videoCodec) { | ||
| 608 | + LKLog.d { "falling back to server selected codec: $updatedCodec" } | ||
| 609 | + options = options.copy(videoCodec = updatedCodec) | ||
| 610 | + | ||
| 611 | + // recompute encodings since bitrates/etc could have changed | ||
| 612 | + encodings = computeVideoEncodings((track as LocalVideoTrack).dimensions, options) | ||
| 613 | + } | ||
| 614 | + } | ||
| 615 | + } | ||
| 555 | 616 | ||
| 556 | - val rtpParameters = transceiver.sender.parameters | ||
| 557 | - rtpParameters.degradationPreference = options.degradationPreference | ||
| 558 | - transceiver.sender.parameters = rtpParameters | 617 | + negotiate() |
| 559 | } | 618 | } |
| 560 | 619 | ||
| 561 | val publication = LocalTrackPublication( | 620 | val publication = LocalTrackPublication( |
| @@ -1266,13 +1325,8 @@ internal constructor( | @@ -1266,13 +1325,8 @@ internal constructor( | ||
| 1266 | LKLog.w { "couldn't create new transceiver! $codec" } | 1325 | LKLog.w { "couldn't create new transceiver! $codec" } |
| 1267 | return@launch | 1326 | return@launch |
| 1268 | } | 1327 | } |
| 1269 | - transceiver.sortVideoCodecPreferences(newOptions.videoCodec, capabilitiesGetter) | ||
| 1270 | - simulcastTrack.sender = transceiver.sender | ||
| 1271 | - | ||
| 1272 | val trackRequest = AddTrackRequest.newBuilder().apply { | 1328 | val trackRequest = AddTrackRequest.newBuilder().apply { |
| 1273 | - cid = transceiver.sender.id() | ||
| 1274 | sid = existingPublication.sid | 1329 | sid = existingPublication.sid |
| 1275 | - type = track.kind.toProto() | ||
| 1276 | muted = !track.enabled | 1330 | muted = !track.enabled |
| 1277 | source = existingPublication.source.toProto() | 1331 | source = existingPublication.source.toProto() |
| 1278 | addSimulcastCodecs( | 1332 | addSimulcastCodecs( |
| @@ -1291,17 +1345,23 @@ internal constructor( | @@ -1291,17 +1345,23 @@ internal constructor( | ||
| 1291 | ), | 1345 | ), |
| 1292 | ) | 1346 | ) |
| 1293 | } | 1347 | } |
| 1348 | + val negotiateJob = launch { | ||
| 1349 | + transceiver.sortVideoCodecPreferences(newOptions.videoCodec, capabilitiesGetter) | ||
| 1350 | + simulcastTrack.sender = transceiver.sender | ||
| 1294 | 1351 | ||
| 1295 | - val trackInfo = engine.addTrack( | ||
| 1296 | - cid = simulcastTrack.rtcTrack.id(), | ||
| 1297 | - name = existingPublication.name, | ||
| 1298 | - kind = existingPublication.kind.toProto(), | ||
| 1299 | - stream = options.stream, | ||
| 1300 | - builder = trackRequest, | ||
| 1301 | - ) | ||
| 1302 | - | ||
| 1303 | - engine.negotiatePublisher() | ||
| 1304 | - | 1352 | + engine.negotiatePublisher() |
| 1353 | + } | ||
| 1354 | + val publishJob = async { | ||
| 1355 | + engine.addTrack( | ||
| 1356 | + cid = simulcastTrack.rtcTrack.id(), | ||
| 1357 | + name = existingPublication.name, | ||
| 1358 | + kind = existingPublication.kind.toProto(), | ||
| 1359 | + stream = options.stream, | ||
| 1360 | + builder = trackRequest, | ||
| 1361 | + ) | ||
| 1362 | + } | ||
| 1363 | + negotiateJob.join() | ||
| 1364 | + val trackInfo = publishJob.await() | ||
| 1305 | LKLog.d { "published $codec for track ${track.sid}, $trackInfo" } | 1365 | LKLog.d { "published $codec for track ${track.sid}, $trackInfo" } |
| 1306 | } | 1366 | } |
| 1307 | } | 1367 | } |
| @@ -1360,6 +1420,20 @@ internal constructor( | @@ -1360,6 +1420,20 @@ internal constructor( | ||
| 1360 | eventBus.postEvent(ParticipantEvent.LocalTrackSubscribed(this, publication), scope) | 1420 | eventBus.postEvent(ParticipantEvent.LocalTrackSubscribed(this, publication), scope) |
| 1361 | } | 1421 | } |
| 1362 | 1422 | ||
| 1423 | + internal fun setEnabledPublishCodecs(codecs: List<Codec>) { | ||
| 1424 | + synchronized(enabledPublishVideoCodecs) { | ||
| 1425 | + enabledPublishVideoCodecs.clear() | ||
| 1426 | + enabledPublishVideoCodecs.addAll( | ||
| 1427 | + codecs.filter { codec -> | ||
| 1428 | + codec.mime.split('/') | ||
| 1429 | + .takeIf { it.isNotEmpty() } | ||
| 1430 | + ?.get(0) | ||
| 1431 | + ?.lowercase() == "video" | ||
| 1432 | + }, | ||
| 1433 | + ) | ||
| 1434 | + } | ||
| 1435 | + } | ||
| 1436 | + | ||
| 1363 | /** | 1437 | /** |
| 1364 | * @suppress | 1438 | * @suppress |
| 1365 | */ | 1439 | */ |
| @@ -1386,6 +1460,7 @@ internal constructor( | @@ -1386,6 +1460,7 @@ internal constructor( | ||
| 1386 | */ | 1460 | */ |
| 1387 | override fun dispose() { | 1461 | override fun dispose() { |
| 1388 | cleanup() | 1462 | cleanup() |
| 1463 | + enabledPublishVideoCodecs.clear() | ||
| 1389 | super.dispose() | 1464 | super.dispose() |
| 1390 | } | 1465 | } |
| 1391 | 1466 |
| 1 | /* | 1 | /* |
| 2 | - * Copyright 2023-2024 LiveKit, Inc. | 2 | + * Copyright 2023-2025 LiveKit, Inc. |
| 3 | * | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. | 5 | * you may not use this file except in compliance with the License. |
| @@ -85,7 +85,7 @@ enum class VideoCodec(val codecName: String) { | @@ -85,7 +85,7 @@ enum class VideoCodec(val codecName: String) { | ||
| 85 | 85 | ||
| 86 | companion object { | 86 | companion object { |
| 87 | fun fromCodecName(codecName: String): VideoCodec { | 87 | fun fromCodecName(codecName: String): VideoCodec { |
| 88 | - return VideoCodec.values().first { it.codecName.equals(codecName, ignoreCase = true) } | 88 | + return entries.first { it.codecName.equals(codecName, ignoreCase = true) } |
| 89 | } | 89 | } |
| 90 | } | 90 | } |
| 91 | } | 91 | } |
| 1 | /* | 1 | /* |
| 2 | - * Copyright 2023-2024 LiveKit, Inc. | 2 | + * Copyright 2023-2025 LiveKit, Inc. |
| 3 | * | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. | 5 | * you may not use this file except in compliance with the License. |
| @@ -18,9 +18,12 @@ package io.livekit.android.test.mock | @@ -18,9 +18,12 @@ package io.livekit.android.test.mock | ||
| 18 | 18 | ||
| 19 | import livekit.org.webrtc.RtpReceiver | 19 | import livekit.org.webrtc.RtpReceiver |
| 20 | import org.mockito.Mockito | 20 | import org.mockito.Mockito |
| 21 | +import org.mockito.kotlin.whenever | ||
| 21 | 22 | ||
| 22 | object MockRtpReceiver { | 23 | object MockRtpReceiver { |
| 23 | - fun create(): RtpReceiver { | ||
| 24 | - return Mockito.mock(RtpReceiver::class.java) | 24 | + fun create(id: String = "receiver_id"): RtpReceiver { |
| 25 | + return Mockito.mock(RtpReceiver::class.java).apply { | ||
| 26 | + whenever(this.id()).thenReturn(id) | ||
| 27 | + } | ||
| 25 | } | 28 | } |
| 26 | } | 29 | } |
| 1 | /* | 1 | /* |
| 2 | - * Copyright 2023-2024 LiveKit, Inc. | 2 | + * Copyright 2023-2025 LiveKit, Inc. |
| 3 | * | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. | 5 | * you may not use this file except in compliance with the License. |
| @@ -25,7 +25,7 @@ import org.mockito.kotlin.whenever | @@ -25,7 +25,7 @@ import org.mockito.kotlin.whenever | ||
| 25 | import java.util.UUID | 25 | import java.util.UUID |
| 26 | 26 | ||
| 27 | object MockRtpSender { | 27 | object MockRtpSender { |
| 28 | - fun create(): RtpSender { | 28 | + fun create(id: String = "sender_id"): RtpSender { |
| 29 | var rtpParameters: RtpParameters = MockRtpParameters( | 29 | var rtpParameters: RtpParameters = MockRtpParameters( |
| 30 | transactionId = UUID.randomUUID().toString(), | 30 | transactionId = UUID.randomUUID().toString(), |
| 31 | degradationPreference = null, | 31 | degradationPreference = null, |
| @@ -36,6 +36,7 @@ object MockRtpSender { | @@ -36,6 +36,7 @@ object MockRtpSender { | ||
| 36 | ) | 36 | ) |
| 37 | return Mockito.mock(RtpSender::class.java).apply { | 37 | return Mockito.mock(RtpSender::class.java).apply { |
| 38 | whenever(this.parameters).thenAnswer { rtpParameters } | 38 | whenever(this.parameters).thenAnswer { rtpParameters } |
| 39 | + whenever(this.id()).thenReturn(id) | ||
| 39 | whenever(this.setParameters(any())).thenAnswer { | 40 | whenever(this.setParameters(any())).thenAnswer { |
| 40 | rtpParameters = it.getArgument(0) | 41 | rtpParameters = it.getArgument(0) |
| 41 | true | 42 | true |
| 1 | /* | 1 | /* |
| 2 | - * Copyright 2023-2024 LiveKit, Inc. | 2 | + * Copyright 2023-2025 LiveKit, Inc. |
| 3 | * | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. | 5 | * you may not use this file except in compliance with the License. |
| @@ -20,6 +20,7 @@ import io.livekit.android.test.mock.MockRtpReceiver | @@ -20,6 +20,7 @@ import io.livekit.android.test.mock.MockRtpReceiver | ||
| 20 | import io.livekit.android.test.mock.MockRtpSender | 20 | import io.livekit.android.test.mock.MockRtpSender |
| 21 | import livekit.org.webrtc.RtpTransceiver.RtpTransceiverDirection | 21 | import livekit.org.webrtc.RtpTransceiver.RtpTransceiverDirection |
| 22 | import org.mockito.Mockito | 22 | import org.mockito.Mockito |
| 23 | +import java.util.UUID | ||
| 23 | 24 | ||
| 24 | object MockRtpTransceiver { | 25 | object MockRtpTransceiver { |
| 25 | fun create( | 26 | fun create( |
| @@ -27,7 +28,7 @@ object MockRtpTransceiver { | @@ -27,7 +28,7 @@ object MockRtpTransceiver { | ||
| 27 | init: RtpTransceiver.RtpTransceiverInit = RtpTransceiver.RtpTransceiverInit(), | 28 | init: RtpTransceiver.RtpTransceiverInit = RtpTransceiver.RtpTransceiverInit(), |
| 28 | ): RtpTransceiver { | 29 | ): RtpTransceiver { |
| 29 | val mock = Mockito.mock(RtpTransceiver::class.java) | 30 | val mock = Mockito.mock(RtpTransceiver::class.java) |
| 30 | - | 31 | + val id = UUID.randomUUID().toString() |
| 31 | Mockito.`when`(mock.mediaType).then { | 32 | Mockito.`when`(mock.mediaType).then { |
| 32 | return@then when (track.kind()) { | 33 | return@then when (track.kind()) { |
| 33 | MediaStreamTrack.AUDIO_TRACK_KIND -> MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO | 34 | MediaStreamTrack.AUDIO_TRACK_KIND -> MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO |
| @@ -40,7 +41,7 @@ object MockRtpTransceiver { | @@ -40,7 +41,7 @@ object MockRtpTransceiver { | ||
| 40 | 41 | ||
| 41 | when (direction) { | 42 | when (direction) { |
| 42 | RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.SEND_ONLY -> { | 43 | RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.SEND_ONLY -> { |
| 43 | - val sender = MockRtpSender.create() | 44 | + val sender = MockRtpSender.create(id = id) |
| 44 | Mockito.`when`(mock.sender) | 45 | Mockito.`when`(mock.sender) |
| 45 | .then { sender } | 46 | .then { sender } |
| 46 | } | 47 | } |
| @@ -50,7 +51,7 @@ object MockRtpTransceiver { | @@ -50,7 +51,7 @@ object MockRtpTransceiver { | ||
| 50 | 51 | ||
| 51 | when (direction) { | 52 | when (direction) { |
| 52 | RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.RECV_ONLY -> { | 53 | RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.RECV_ONLY -> { |
| 53 | - val receiver = MockRtpReceiver.create() | 54 | + val receiver = MockRtpReceiver.create(id = id) |
| 54 | Mockito.`when`(mock.receiver) | 55 | Mockito.`when`(mock.receiver) |
| 55 | .then { receiver } | 56 | .then { receiver } |
| 56 | } | 57 | } |
-
请 注册 或 登录 后发表评论