Committed by
GitHub
Improve track publish failure handling (#637)
* Avoid exception on request add track failure * Stop audio and video track on publish failure * Start the video capture on connect if requested * Fire a room event when the track publication failed * Add changeset * Fixes unit tests * Update livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt * Also return true from setDeviceEnabled --------- Co-authored-by: davidliu <davidliu@deviange.net>
正在显示
6 个修改的文件
包含
182 行增加
和
63 行删除
.changeset/tender-pears-sell.md
0 → 100644
| 1 | +--- | ||
| 2 | +"client-sdk-android": patch | ||
| 3 | +--- | ||
| 4 | + | ||
| 5 | +Improved handling of track publication failures by introducing a new TrackPublicationFailed event and fixing a broken state issue where the track remained active but inaccessible, causing the microphone or camera to stay on without a published track and leading to unreliable republishing. |
| 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. |
| @@ -23,6 +23,7 @@ import io.livekit.android.room.participant.RemoteParticipant | @@ -23,6 +23,7 @@ import io.livekit.android.room.participant.RemoteParticipant | ||
| 23 | import io.livekit.android.room.track.LocalTrackPublication | 23 | import io.livekit.android.room.track.LocalTrackPublication |
| 24 | import io.livekit.android.room.track.RemoteTrackPublication | 24 | import io.livekit.android.room.track.RemoteTrackPublication |
| 25 | import io.livekit.android.room.track.Track | 25 | import io.livekit.android.room.track.Track |
| 26 | +import io.livekit.android.room.track.TrackException | ||
| 26 | import io.livekit.android.room.track.TrackPublication | 27 | import io.livekit.android.room.track.TrackPublication |
| 27 | import io.livekit.android.room.types.TranscriptionSegment | 28 | import io.livekit.android.room.types.TranscriptionSegment |
| 28 | 29 | ||
| @@ -89,6 +90,15 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() { | @@ -89,6 +90,15 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() { | ||
| 89 | ParticipantEvent(participant) | 90 | ParticipantEvent(participant) |
| 90 | 91 | ||
| 91 | /** | 92 | /** |
| 93 | + * Error had occurred while publishing a track | ||
| 94 | + */ | ||
| 95 | + class LocalTrackPublicationFailed( | ||
| 96 | + override val participant: LocalParticipant, | ||
| 97 | + val track: Track, | ||
| 98 | + val e: TrackException.PublishException, | ||
| 99 | + ) : ParticipantEvent(participant) | ||
| 100 | + | ||
| 101 | + /** | ||
| 92 | * A [LocalParticipant] has unpublished a track | 102 | * A [LocalParticipant] has unpublished a track |
| 93 | */ | 103 | */ |
| 94 | class LocalTrackUnpublished(override val participant: LocalParticipant, val publication: LocalTrackPublication) : | 104 | class LocalTrackUnpublished(override val participant: LocalParticipant, val publication: LocalTrackPublication) : |
| @@ -27,6 +27,7 @@ import io.livekit.android.room.participant.RemoteParticipant | @@ -27,6 +27,7 @@ import io.livekit.android.room.participant.RemoteParticipant | ||
| 27 | import io.livekit.android.room.track.LocalTrackPublication | 27 | import io.livekit.android.room.track.LocalTrackPublication |
| 28 | import io.livekit.android.room.track.RemoteTrackPublication | 28 | import io.livekit.android.room.track.RemoteTrackPublication |
| 29 | import io.livekit.android.room.track.Track | 29 | import io.livekit.android.room.track.Track |
| 30 | +import io.livekit.android.room.track.TrackException | ||
| 30 | import io.livekit.android.room.track.TrackPublication | 31 | import io.livekit.android.room.track.TrackPublication |
| 31 | import io.livekit.android.room.types.TranscriptionSegment | 32 | import io.livekit.android.room.types.TranscriptionSegment |
| 32 | import livekit.LivekitModels | 33 | import livekit.LivekitModels |
| @@ -139,6 +140,17 @@ sealed class RoomEvent(val room: Room) : Event() { | @@ -139,6 +140,17 @@ sealed class RoomEvent(val room: Room) : Event() { | ||
| 139 | class TrackPublished(room: Room, val publication: TrackPublication, val participant: Participant) : RoomEvent(room) | 140 | class TrackPublished(room: Room, val publication: TrackPublication, val participant: Participant) : RoomEvent(room) |
| 140 | 141 | ||
| 141 | /** | 142 | /** |
| 143 | + * Error had occurred while publishing a track, for LocalParticipant only | ||
| 144 | + * not fire for tracks that are already published | ||
| 145 | + */ | ||
| 146 | + class TrackPublicationFailed( | ||
| 147 | + room: Room, | ||
| 148 | + val track: Track, | ||
| 149 | + val participant: LocalParticipant, | ||
| 150 | + e: TrackException.PublishException, | ||
| 151 | + ) : RoomEvent(room) | ||
| 152 | + | ||
| 153 | + /** | ||
| 142 | * A [Participant] has unpublished a track | 154 | * A [Participant] has unpublished a track |
| 143 | */ | 155 | */ |
| 144 | class TrackUnpublished(room: Room, val publication: TrackPublication, val participant: Participant) : | 156 | class TrackUnpublished(room: Room, val publication: TrackPublication, val participant: Participant) : |
| @@ -479,14 +479,21 @@ constructor( | @@ -479,14 +479,21 @@ constructor( | ||
| 479 | ensureActive() | 479 | ensureActive() |
| 480 | networkCallbackManager.registerCallback() | 480 | networkCallbackManager.registerCallback() |
| 481 | if (options.audio) { | 481 | if (options.audio) { |
| 482 | - val audioTrack = localParticipant.createAudioTrack() | 482 | + val audioTrack = localParticipant.getOrCreateDefaultAudioTrack() |
| 483 | audioTrack.prewarm() | 483 | audioTrack.prewarm() |
| 484 | - localParticipant.publishAudioTrack(audioTrack) | 484 | + if (!localParticipant.publishAudioTrack(audioTrack)) { |
| 485 | + audioTrack.stop() | ||
| 486 | + audioTrack.stopPrewarm() | ||
| 487 | + } | ||
| 485 | } | 488 | } |
| 486 | ensureActive() | 489 | ensureActive() |
| 487 | if (options.video) { | 490 | if (options.video) { |
| 488 | - val videoTrack = localParticipant.createVideoTrack() | ||
| 489 | - localParticipant.publishVideoTrack(videoTrack) | 491 | + val videoTrack = localParticipant.getOrCreateDefaultVideoTrack() |
| 492 | + videoTrack.startCapture() | ||
| 493 | + if (!localParticipant.publishVideoTrack(videoTrack)) { | ||
| 494 | + videoTrack.stopCapture() | ||
| 495 | + videoTrack.stop() | ||
| 496 | + } | ||
| 490 | } | 497 | } |
| 491 | 498 | ||
| 492 | coroutineScope.launch { | 499 | coroutineScope.launch { |
| @@ -612,6 +619,15 @@ constructor( | @@ -612,6 +619,15 @@ constructor( | ||
| 612 | ), | 619 | ), |
| 613 | ) | 620 | ) |
| 614 | 621 | ||
| 622 | + is ParticipantEvent.LocalTrackPublicationFailed -> emitWhenConnected( | ||
| 623 | + RoomEvent.TrackPublicationFailed( | ||
| 624 | + room = this@Room, | ||
| 625 | + track = it.track, | ||
| 626 | + participant = it.participant, | ||
| 627 | + e = it.e, | ||
| 628 | + ), | ||
| 629 | + ) | ||
| 630 | + | ||
| 615 | is ParticipantEvent.TrackUnpublished -> emitWhenConnected( | 631 | is ParticipantEvent.TrackUnpublished -> emitWhenConnected( |
| 616 | RoomEvent.TrackUnpublished( | 632 | RoomEvent.TrackUnpublished( |
| 617 | room = this@Room, | 633 | room = this@Room, |
| @@ -137,6 +137,29 @@ internal constructor( | @@ -137,6 +137,29 @@ internal constructor( | ||
| 137 | 137 | ||
| 138 | internal val enabledPublishVideoCodecs = Collections.synchronizedList(mutableListOf<Codec>()) | 138 | internal val enabledPublishVideoCodecs = Collections.synchronizedList(mutableListOf<Codec>()) |
| 139 | 139 | ||
| 140 | + private var defaultAudioTrack: LocalAudioTrack? = null | ||
| 141 | + private var defaultVideoTrack: LocalVideoTrack? = null | ||
| 142 | + | ||
| 143 | + /** | ||
| 144 | + * Returns the default audio track, or creates one if it doesn't exist. | ||
| 145 | + * @exception SecurityException will be thrown if [Manifest.permission.RECORD_AUDIO] permission is missing. | ||
| 146 | + */ | ||
| 147 | + fun getOrCreateDefaultAudioTrack(): LocalAudioTrack { | ||
| 148 | + return defaultAudioTrack ?: createAudioTrack().also { | ||
| 149 | + defaultAudioTrack = it | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + /** | ||
| 154 | + * Returns the default video track, or creates one if it doesn't exist. | ||
| 155 | + * @exception SecurityException will be thrown if [Manifest.permission.CAMERA] permission is missing. | ||
| 156 | + */ | ||
| 157 | + fun getOrCreateDefaultVideoTrack(): LocalVideoTrack { | ||
| 158 | + return defaultVideoTrack ?: createVideoTrack().also { | ||
| 159 | + defaultVideoTrack = it | ||
| 160 | + } | ||
| 161 | + } | ||
| 162 | + | ||
| 140 | /** | 163 | /** |
| 141 | * Creates an audio track, recording audio through the microphone with the given [options]. | 164 | * Creates an audio track, recording audio through the microphone with the given [options]. |
| 142 | * | 165 | * |
| @@ -256,10 +279,11 @@ internal constructor( | @@ -256,10 +279,11 @@ internal constructor( | ||
| 256 | * | 279 | * |
| 257 | * @see Room.videoTrackCaptureDefaults | 280 | * @see Room.videoTrackCaptureDefaults |
| 258 | * @see Room.videoTrackPublishDefaults | 281 | * @see Room.videoTrackPublishDefaults |
| 282 | + * @return true if the change was successful, or false if it failed. | ||
| 259 | */ | 283 | */ |
| 260 | @Throws(TrackException.PublishException::class) | 284 | @Throws(TrackException.PublishException::class) |
| 261 | - suspend fun setCameraEnabled(enabled: Boolean) { | ||
| 262 | - setTrackEnabled(Track.Source.CAMERA, enabled) | 285 | + suspend fun setCameraEnabled(enabled: Boolean): Boolean { |
| 286 | + return setTrackEnabled(Track.Source.CAMERA, enabled) | ||
| 263 | } | 287 | } |
| 264 | 288 | ||
| 265 | /** | 289 | /** |
| @@ -271,10 +295,11 @@ internal constructor( | @@ -271,10 +295,11 @@ internal constructor( | ||
| 271 | * | 295 | * |
| 272 | * @see Room.audioTrackCaptureDefaults | 296 | * @see Room.audioTrackCaptureDefaults |
| 273 | * @see Room.audioTrackPublishDefaults | 297 | * @see Room.audioTrackPublishDefaults |
| 298 | + * @return true if the change was successful, or false if it failed. | ||
| 274 | */ | 299 | */ |
| 275 | @Throws(TrackException.PublishException::class) | 300 | @Throws(TrackException.PublishException::class) |
| 276 | - suspend fun setMicrophoneEnabled(enabled: Boolean) { | ||
| 277 | - setTrackEnabled(Track.Source.MICROPHONE, enabled) | 301 | + suspend fun setMicrophoneEnabled(enabled: Boolean): Boolean { |
| 302 | + return setTrackEnabled(Track.Source.MICROPHONE, enabled) | ||
| 278 | } | 303 | } |
| 279 | 304 | ||
| 280 | /** | 305 | /** |
| @@ -292,41 +317,58 @@ internal constructor( | @@ -292,41 +317,58 @@ internal constructor( | ||
| 292 | * @see Room.screenShareTrackCaptureDefaults | 317 | * @see Room.screenShareTrackCaptureDefaults |
| 293 | * @see Room.screenShareTrackPublishDefaults | 318 | * @see Room.screenShareTrackPublishDefaults |
| 294 | * @see ScreenAudioCapturer | 319 | * @see ScreenAudioCapturer |
| 320 | + * @return true if the change was successful, or false if it failed. | ||
| 295 | */ | 321 | */ |
| 296 | @Throws(TrackException.PublishException::class) | 322 | @Throws(TrackException.PublishException::class) |
| 297 | suspend fun setScreenShareEnabled( | 323 | suspend fun setScreenShareEnabled( |
| 298 | enabled: Boolean, | 324 | enabled: Boolean, |
| 299 | screenCaptureParams: ScreenCaptureParams? = null, | 325 | screenCaptureParams: ScreenCaptureParams? = null, |
| 300 | - ) { | ||
| 301 | - setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, screenCaptureParams) | 326 | + ): Boolean { |
| 327 | + return setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, screenCaptureParams) | ||
| 302 | } | 328 | } |
| 303 | 329 | ||
| 304 | private suspend fun setTrackEnabled( | 330 | private suspend fun setTrackEnabled( |
| 305 | source: Track.Source, | 331 | source: Track.Source, |
| 306 | enabled: Boolean, | 332 | enabled: Boolean, |
| 307 | screenCaptureParams: ScreenCaptureParams? = null, | 333 | screenCaptureParams: ScreenCaptureParams? = null, |
| 308 | - ) { | 334 | + ): Boolean { |
| 335 | + var success = false | ||
| 309 | val pubLock = sourcePubLocks[source]!! | 336 | val pubLock = sourcePubLocks[source]!! |
| 310 | pubLock.withLock { | 337 | pubLock.withLock { |
| 311 | val pub = getTrackPublication(source) | 338 | val pub = getTrackPublication(source) |
| 312 | if (enabled) { | 339 | if (enabled) { |
| 313 | if (pub != null) { | 340 | if (pub != null) { |
| 341 | + // Publication exists, just unmute the existing track. | ||
| 314 | pub.muted = false | 342 | pub.muted = false |
| 315 | if (source == Track.Source.CAMERA && pub.track is LocalVideoTrack) { | 343 | if (source == Track.Source.CAMERA && pub.track is LocalVideoTrack) { |
| 316 | (pub.track as? LocalVideoTrack)?.startCapture() | 344 | (pub.track as? LocalVideoTrack)?.startCapture() |
| 317 | } | 345 | } |
| 346 | + success = true | ||
| 318 | } else { | 347 | } else { |
| 348 | + // Not published yet, create the default track and publish. | ||
| 319 | when (source) { | 349 | when (source) { |
| 320 | Track.Source.CAMERA -> { | 350 | Track.Source.CAMERA -> { |
| 321 | - val track = createVideoTrack() | 351 | + val track = getOrCreateDefaultVideoTrack() |
| 352 | + track.start() | ||
| 322 | track.startCapture() | 353 | track.startCapture() |
| 323 | - publishVideoTrack(track) | 354 | + if (!publishVideoTrack(track)) { |
| 355 | + track.stopCapture() | ||
| 356 | + track.stop() | ||
| 357 | + } else { | ||
| 358 | + success = true | ||
| 359 | + } | ||
| 324 | } | 360 | } |
| 325 | 361 | ||
| 326 | Track.Source.MICROPHONE -> { | 362 | Track.Source.MICROPHONE -> { |
| 327 | - val track = createAudioTrack() | 363 | + val track = getOrCreateDefaultAudioTrack() |
| 328 | track.prewarm() | 364 | track.prewarm() |
| 329 | - publishAudioTrack(track) | 365 | + track.start() |
| 366 | + if (!publishAudioTrack(track)) { | ||
| 367 | + track.stop() | ||
| 368 | + track.stopPrewarm() | ||
| 369 | + } else { | ||
| 370 | + success = true | ||
| 371 | + } | ||
| 330 | } | 372 | } |
| 331 | 373 | ||
| 332 | Track.Source.SCREEN_SHARE -> { | 374 | Track.Source.SCREEN_SHARE -> { |
| @@ -340,7 +382,16 @@ internal constructor( | @@ -340,7 +382,16 @@ internal constructor( | ||
| 340 | } | 382 | } |
| 341 | track.startForegroundService(screenCaptureParams.notificationId, screenCaptureParams.notification) | 383 | track.startForegroundService(screenCaptureParams.notificationId, screenCaptureParams.notification) |
| 342 | track.startCapture() | 384 | track.startCapture() |
| 343 | - publishVideoTrack(track, options = VideoTrackPublishOptions(null, screenShareTrackPublishDefaults)) | 385 | + if (!publishVideoTrack(track, options = VideoTrackPublishOptions(null, screenShareTrackPublishDefaults))) { |
| 386 | + screenCaptureParams.onStop?.invoke() | ||
| 387 | + track.apply { | ||
| 388 | + stopCapture() | ||
| 389 | + stop() | ||
| 390 | + dispose() | ||
| 391 | + } | ||
| 392 | + } else { | ||
| 393 | + success = true | ||
| 394 | + } | ||
| 344 | } | 395 | } |
| 345 | 396 | ||
| 346 | else -> { | 397 | else -> { |
| @@ -362,9 +413,12 @@ internal constructor( | @@ -362,9 +413,12 @@ internal constructor( | ||
| 362 | } | 413 | } |
| 363 | } | 414 | } |
| 364 | } | 415 | } |
| 416 | + success = true | ||
| 365 | } | 417 | } |
| 366 | return@withLock | 418 | return@withLock |
| 367 | } | 419 | } |
| 420 | + | ||
| 421 | + return success | ||
| 368 | } | 422 | } |
| 369 | 423 | ||
| 370 | /** | 424 | /** |
| @@ -380,7 +434,7 @@ internal constructor( | @@ -380,7 +434,7 @@ internal constructor( | ||
| 380 | audioTrackPublishDefaults, | 434 | audioTrackPublishDefaults, |
| 381 | ), | 435 | ), |
| 382 | publishListener: PublishListener? = null, | 436 | publishListener: PublishListener? = null, |
| 383 | - ) { | 437 | + ): Boolean { |
| 384 | val encodings = listOf( | 438 | val encodings = listOf( |
| 385 | RtpParameters.Encoding(null, true, null).apply { | 439 | RtpParameters.Encoding(null, true, null).apply { |
| 386 | if (options.audioBitrate != null && options.audioBitrate > 0) { | 440 | if (options.audioBitrate != null && options.audioBitrate > 0) { |
| @@ -408,6 +462,8 @@ internal constructor( | @@ -408,6 +462,8 @@ internal constructor( | ||
| 408 | } | 462 | } |
| 409 | jobs[publication] = job | 463 | jobs[publication] = job |
| 410 | } | 464 | } |
| 465 | + | ||
| 466 | + return publication != null | ||
| 411 | } | 467 | } |
| 412 | 468 | ||
| 413 | /** | 469 | /** |
| @@ -420,7 +476,7 @@ internal constructor( | @@ -420,7 +476,7 @@ internal constructor( | ||
| 420 | track: LocalVideoTrack, | 476 | track: LocalVideoTrack, |
| 421 | options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults), | 477 | options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults), |
| 422 | publishListener: PublishListener? = null, | 478 | publishListener: PublishListener? = null, |
| 423 | - ) { | 479 | + ): Boolean { |
| 424 | @Suppress("NAME_SHADOWING") var options = options | 480 | @Suppress("NAME_SHADOWING") var options = options |
| 425 | 481 | ||
| 426 | synchronized(enabledPublishVideoCodecs) { | 482 | synchronized(enabledPublishVideoCodecs) { |
| @@ -456,7 +512,7 @@ internal constructor( | @@ -456,7 +512,7 @@ internal constructor( | ||
| 456 | val videoLayers = | 512 | val videoLayers = |
| 457 | EncodingUtils.videoLayersFromEncodings(track.dimensions.width, track.dimensions.height, encodings, isSVC) | 513 | EncodingUtils.videoLayersFromEncodings(track.dimensions.width, track.dimensions.height, encodings, isSVC) |
| 458 | 514 | ||
| 459 | - publishTrackImpl( | 515 | + return publishTrackImpl( |
| 460 | track = track, | 516 | track = track, |
| 461 | options = options, | 517 | options = options, |
| 462 | requestConfig = { | 518 | requestConfig = { |
| @@ -489,7 +545,7 @@ internal constructor( | @@ -489,7 +545,7 @@ internal constructor( | ||
| 489 | }, | 545 | }, |
| 490 | encodings = encodings, | 546 | encodings = encodings, |
| 491 | publishListener = publishListener, | 547 | publishListener = publishListener, |
| 492 | - ) | 548 | + ) != null |
| 493 | } | 549 | } |
| 494 | 550 | ||
| 495 | private fun hasPermissionsToPublish(source: Track.Source): Boolean { | 551 | private fun hasPermissionsToPublish(source: Track.Source): Boolean { |
| @@ -522,13 +578,22 @@ internal constructor( | @@ -522,13 +578,22 @@ internal constructor( | ||
| 522 | encodings: List<RtpParameters.Encoding> = emptyList(), | 578 | encodings: List<RtpParameters.Encoding> = emptyList(), |
| 523 | publishListener: PublishListener? = null, | 579 | publishListener: PublishListener? = null, |
| 524 | ): LocalTrackPublication? { | 580 | ): LocalTrackPublication? { |
| 581 | + fun onPublishFailure(e: TrackException.PublishException, triggerEvent: Boolean = true) { | ||
| 582 | + publishListener?.onPublishFailure(e) | ||
| 583 | + if (triggerEvent) { | ||
| 584 | + eventBus.postEvent(ParticipantEvent.LocalTrackPublicationFailed(this, track, e), scope) | ||
| 585 | + } | ||
| 586 | + } | ||
| 587 | + | ||
| 525 | val addTrackRequestBuilder = AddTrackRequest.newBuilder().apply { | 588 | val addTrackRequestBuilder = AddTrackRequest.newBuilder().apply { |
| 526 | this.requestConfig() | 589 | this.requestConfig() |
| 527 | } | 590 | } |
| 528 | 591 | ||
| 529 | val trackSource = Track.Source.fromProto(addTrackRequestBuilder.source ?: LivekitModels.TrackSource.UNRECOGNIZED) | 592 | val trackSource = Track.Source.fromProto(addTrackRequestBuilder.source ?: LivekitModels.TrackSource.UNRECOGNIZED) |
| 530 | if (!hasPermissionsToPublish(trackSource)) { | 593 | if (!hasPermissionsToPublish(trackSource)) { |
| 531 | - throw TrackException.PublishException("Failed to publish track, insufficient permissions") | 594 | + val exception = TrackException.PublishException("Failed to publish track, insufficient permissions") |
| 595 | + onPublishFailure(exception) | ||
| 596 | + throw exception | ||
| 532 | } | 597 | } |
| 533 | 598 | ||
| 534 | @Suppress("NAME_SHADOWING") var options = options | 599 | @Suppress("NAME_SHADOWING") var options = options |
| @@ -536,12 +601,12 @@ internal constructor( | @@ -536,12 +601,12 @@ internal constructor( | ||
| 536 | @Suppress("NAME_SHADOWING") var encodings = encodings | 601 | @Suppress("NAME_SHADOWING") var encodings = encodings |
| 537 | 602 | ||
| 538 | if (localTrackPublications.any { it.track == track }) { | 603 | if (localTrackPublications.any { it.track == track }) { |
| 539 | - publishListener?.onPublishFailure(TrackException.PublishException("Track has already been published")) | 604 | + onPublishFailure(TrackException.PublishException("Track has already been published"), triggerEvent = false) |
| 540 | return null | 605 | return null |
| 541 | } | 606 | } |
| 542 | 607 | ||
| 543 | if (engine.connectionState == ConnectionState.DISCONNECTED) { | 608 | if (engine.connectionState == ConnectionState.DISCONNECTED) { |
| 544 | - publishListener?.onPublishFailure(TrackException.PublishException("Not connected!")) | 609 | + onPublishFailure(TrackException.PublishException("Not connected!")) |
| 545 | } | 610 | } |
| 546 | 611 | ||
| 547 | val cid = track.rtcTrack.id() | 612 | val cid = track.rtcTrack.id() |
| @@ -569,7 +634,7 @@ internal constructor( | @@ -569,7 +634,7 @@ internal constructor( | ||
| 569 | 634 | ||
| 570 | if (transceiver == null) { | 635 | if (transceiver == null) { |
| 571 | val exception = TrackException.PublishException("null sender returned from peer connection") | 636 | val exception = TrackException.PublishException("null sender returned from peer connection") |
| 572 | - publishListener?.onPublishFailure(exception) | 637 | + onPublishFailure(exception) |
| 573 | throw exception | 638 | throw exception |
| 574 | } | 639 | } |
| 575 | 640 | ||
| @@ -603,7 +668,7 @@ internal constructor( | @@ -603,7 +668,7 @@ internal constructor( | ||
| 603 | // so no need to call negotiate manually. | 668 | // so no need to call negotiate manually. |
| 604 | } | 669 | } |
| 605 | 670 | ||
| 606 | - suspend fun requestAddTrack(): TrackInfo { | 671 | + suspend fun requestAddTrack(): TrackInfo? { |
| 607 | return try { | 672 | return try { |
| 608 | engine.addTrack( | 673 | engine.addTrack( |
| 609 | cid = cid, | 674 | cid = cid, |
| @@ -613,13 +678,12 @@ internal constructor( | @@ -613,13 +678,12 @@ internal constructor( | ||
| 613 | builder = addTrackRequestBuilder, | 678 | builder = addTrackRequestBuilder, |
| 614 | ) | 679 | ) |
| 615 | } catch (e: Exception) { | 680 | } catch (e: Exception) { |
| 616 | - val exception = TrackException.PublishException("Failed to publish track", e) | ||
| 617 | - publishListener?.onPublishFailure(exception) | ||
| 618 | - throw exception | 681 | + onPublishFailure(TrackException.PublishException("Failed to publish track", e)) |
| 682 | + null | ||
| 619 | } | 683 | } |
| 620 | } | 684 | } |
| 621 | 685 | ||
| 622 | - val trackInfo: TrackInfo | 686 | + val trackInfo: TrackInfo? |
| 623 | if (enabledPublishVideoCodecs.isNotEmpty()) { | 687 | if (enabledPublishVideoCodecs.isNotEmpty()) { |
| 624 | // Can simultaneous publish and negotiate. | 688 | // Can simultaneous publish and negotiate. |
| 625 | // codec is pre-verified in publishVideoTrack | 689 | // codec is pre-verified in publishVideoTrack |
| @@ -633,41 +697,45 @@ internal constructor( | @@ -633,41 +697,45 @@ internal constructor( | ||
| 633 | } else { | 697 | } else { |
| 634 | // legacy path. | 698 | // legacy path. |
| 635 | trackInfo = requestAddTrack() | 699 | trackInfo = requestAddTrack() |
| 636 | - | ||
| 637 | - if (options is VideoTrackPublishOptions) { | ||
| 638 | - // server might not support the codec the client has requested, in that case, fallback | ||
| 639 | - // to a supported codec | ||
| 640 | - val primaryCodecMime = trackInfo.codecsList.firstOrNull()?.mimeType | ||
| 641 | - | ||
| 642 | - if (primaryCodecMime != null) { | ||
| 643 | - val updatedCodec = primaryCodecMime.mimeTypeToVideoCodec() | ||
| 644 | - if (updatedCodec != null && updatedCodec != options.videoCodec) { | ||
| 645 | - LKLog.d { "falling back to server selected codec: $updatedCodec" } | ||
| 646 | - options = options.copy(videoCodec = updatedCodec) | ||
| 647 | - | ||
| 648 | - // recompute encodings since bitrates/etc could have changed | ||
| 649 | - encodings = computeVideoEncodings((track as LocalVideoTrack).dimensions, options) | 700 | + if (trackInfo != null) { |
| 701 | + if (options is VideoTrackPublishOptions) { | ||
| 702 | + // server might not support the codec the client has requested, in that case, fallback | ||
| 703 | + // to a supported codec | ||
| 704 | + val primaryCodecMime = trackInfo.codecsList.firstOrNull()?.mimeType | ||
| 705 | + | ||
| 706 | + if (primaryCodecMime != null) { | ||
| 707 | + val updatedCodec = primaryCodecMime.mimeTypeToVideoCodec() | ||
| 708 | + if (updatedCodec != null && updatedCodec != options.videoCodec) { | ||
| 709 | + LKLog.d { "falling back to server selected codec: $updatedCodec" } | ||
| 710 | + options = options.copy(videoCodec = updatedCodec) | ||
| 711 | + | ||
| 712 | + // recompute encodings since bitrates/etc could have changed | ||
| 713 | + encodings = computeVideoEncodings((track as LocalVideoTrack).dimensions, options) | ||
| 714 | + } | ||
| 650 | } | 715 | } |
| 651 | } | 716 | } |
| 652 | - } | ||
| 653 | 717 | ||
| 654 | - negotiate() | 718 | + negotiate() |
| 719 | + } | ||
| 655 | } | 720 | } |
| 656 | 721 | ||
| 657 | - val publication = LocalTrackPublication( | ||
| 658 | - info = trackInfo, | ||
| 659 | - track = track, | ||
| 660 | - participant = this, | ||
| 661 | - options = options, | ||
| 662 | - ) | ||
| 663 | - addTrackPublication(publication) | ||
| 664 | - LKLog.v { "add track publication $publication" } | ||
| 665 | - | ||
| 666 | - publishListener?.onPublishSuccess(publication) | ||
| 667 | - internalListener?.onTrackPublished(publication, this) | ||
| 668 | - eventBus.postEvent(ParticipantEvent.LocalTrackPublished(this, publication), scope) | 722 | + return if (trackInfo != null) { |
| 723 | + val publication = LocalTrackPublication( | ||
| 724 | + info = trackInfo, | ||
| 725 | + track = track, | ||
| 726 | + participant = this, | ||
| 727 | + options = options, | ||
| 728 | + ) | ||
| 729 | + addTrackPublication(publication) | ||
| 730 | + LKLog.v { "add track publication $publication" } | ||
| 669 | 731 | ||
| 670 | - return publication | 732 | + publishListener?.onPublishSuccess(publication) |
| 733 | + internalListener?.onTrackPublished(publication, this) | ||
| 734 | + eventBus.postEvent(ParticipantEvent.LocalTrackPublished(this, publication), scope) | ||
| 735 | + publication | ||
| 736 | + } else { | ||
| 737 | + null | ||
| 738 | + } | ||
| 671 | } | 739 | } |
| 672 | 740 | ||
| 673 | private fun computeVideoEncodings( | 741 | private fun computeVideoEncodings( |
| @@ -1443,11 +1511,14 @@ internal constructor( | @@ -1443,11 +1511,14 @@ internal constructor( | ||
| 1443 | unpublishTrack(track, false) | 1511 | unpublishTrack(track, false) |
| 1444 | // Cannot publish muted tracks. | 1512 | // Cannot publish muted tracks. |
| 1445 | if (!pub.muted) { | 1513 | if (!pub.muted) { |
| 1446 | - when (track) { | 1514 | + val success = when (track) { |
| 1447 | is LocalAudioTrack -> publishAudioTrack(track, pub.options as AudioTrackPublishOptions, null) | 1515 | is LocalAudioTrack -> publishAudioTrack(track, pub.options as AudioTrackPublishOptions, null) |
| 1448 | is LocalVideoTrack -> publishVideoTrack(track, pub.options as VideoTrackPublishOptions, null) | 1516 | is LocalVideoTrack -> publishVideoTrack(track, pub.options as VideoTrackPublishOptions, null) |
| 1449 | else -> throw IllegalStateException("LocalParticipant has a non local track publish?") | 1517 | else -> throw IllegalStateException("LocalParticipant has a non local track publish?") |
| 1450 | } | 1518 | } |
| 1519 | + if (!success) { | ||
| 1520 | + track.stop() | ||
| 1521 | + } | ||
| 1451 | } | 1522 | } |
| 1452 | } | 1523 | } |
| 1453 | } | 1524 | } |
| @@ -1479,6 +1550,10 @@ internal constructor( | @@ -1479,6 +1550,10 @@ internal constructor( | ||
| 1479 | * @suppress | 1550 | * @suppress |
| 1480 | */ | 1551 | */ |
| 1481 | fun cleanup() { | 1552 | fun cleanup() { |
| 1553 | + defaultAudioTrack?.dispose() | ||
| 1554 | + defaultAudioTrack = null | ||
| 1555 | + defaultVideoTrack?.dispose() | ||
| 1556 | + defaultVideoTrack = null | ||
| 1482 | for (pub in trackPublications.values) { | 1557 | for (pub in trackPublications.values) { |
| 1483 | val track = pub.track | 1558 | val track = pub.track |
| 1484 | 1559 |
| @@ -591,20 +591,21 @@ class LocalParticipantMockE2ETest : MockE2ETest() { | @@ -591,20 +591,21 @@ class LocalParticipantMockE2ETest : MockE2ETest() { | ||
| 591 | } | 591 | } |
| 592 | 592 | ||
| 593 | @Test | 593 | @Test |
| 594 | - fun publishWithNoResponseCausesException() = runTest { | 594 | + fun publishWithNoResponseReturnFalseWithoutException() = runTest { |
| 595 | connect() | 595 | connect() |
| 596 | 596 | ||
| 597 | wsFactory.unregisterSignalRequestHandler(wsFactory.defaultSignalRequestHandler) | 597 | wsFactory.unregisterSignalRequestHandler(wsFactory.defaultSignalRequestHandler) |
| 598 | var didThrow = false | 598 | var didThrow = false |
| 599 | + var success: Boolean? = null | ||
| 599 | launch { | 600 | launch { |
| 600 | try { | 601 | try { |
| 601 | - room.localParticipant.publishVideoTrack(createLocalTrack()) | 602 | + success = room.localParticipant.publishVideoTrack(createLocalTrack()) |
| 602 | } catch (e: TrackException.PublishException) { | 603 | } catch (e: TrackException.PublishException) { |
| 603 | didThrow = true | 604 | didThrow = true |
| 604 | } | 605 | } |
| 605 | } | 606 | } |
| 606 | 607 | ||
| 607 | coroutineRule.dispatcher.scheduler.advanceUntilIdle() | 608 | coroutineRule.dispatcher.scheduler.advanceUntilIdle() |
| 608 | - assertTrue(didThrow) | 609 | + assertTrue(!didThrow && success == false) |
| 609 | } | 610 | } |
| 610 | } | 611 | } |
-
请 注册 或 登录 后发表评论