davidliu
Committed by GitHub

Fix VirtualBackgroundVideoProcessor not responding to changing backgroundImage (#752)

* Fix VirtualBackgroundVideoProcessor not responding to changes in backgroundImage

* lint fixes

* spotless
正在显示 43 个修改的文件 包含 305 行增加144 行删除
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +Fix VirtualBackgroundVideoProcessor not responding to changes in backgroundImage
@@ -45,6 +45,17 @@ class MainActivity : AppCompatActivity() { @@ -45,6 +45,17 @@ class MainActivity : AppCompatActivity() {
45 enableButton.setText(if (state) "Disable" else "Enable") 45 enableButton.setText(if (state) "Disable" else "Enable")
46 } 46 }
47 47
  48 + val enableBackgroundButton = findViewById<Button>(R.id.buttonBackground)
  49 + enableBackgroundButton.setOnClickListener {
  50 + val state = viewModel.toggleVirtualBackground()
  51 + enableBackgroundButton.setText(if (state) "Disable Background" else "Enable Background")
  52 + }
  53 +
  54 + val flipCameraButton = findViewById<Button>(R.id.buttonFlip)
  55 + flipCameraButton.setOnClickListener {
  56 + viewModel.flipCamera()
  57 + }
  58 +
48 val renderer = findViewById<TextureViewRenderer>(R.id.renderer) 59 val renderer = findViewById<TextureViewRenderer>(R.id.renderer)
49 viewModel.room.initVideoRenderer(renderer) 60 viewModel.room.initVideoRenderer(renderer)
50 viewModel.track.observe(this) { track -> 61 viewModel.track.observe(this) { track ->
@@ -87,7 +98,7 @@ fun ComponentActivity.requestNeededPermissions(onPermissionsGranted: (() -> Unit @@ -87,7 +98,7 @@ fun ComponentActivity.requestNeededPermissions(onPermissionsGranted: (() -> Unit
87 } 98 }
88 } 99 }
89 100
90 - val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) 101 + val neededPermissions = listOf(Manifest.permission.CAMERA)
91 .filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED } 102 .filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED }
92 .toTypedArray() 103 .toTypedArray()
93 104
@@ -54,10 +54,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -54,10 +54,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
54 eglBase = eglBase, 54 eglBase = eglBase,
55 ), 55 ),
56 ) 56 )
  57 +
  58 + private val virtualBackground = (AppCompatResources.getDrawable(application, R.drawable.background) as BitmapDrawable).bitmap
  59 +
57 private var blur = 16f 60 private var blur = 16f
58 private val processor = VirtualBackgroundVideoProcessor(eglBase, Dispatchers.IO, initialBlurRadius = blur).apply { 61 private val processor = VirtualBackgroundVideoProcessor(eglBase, Dispatchers.IO, initialBlurRadius = blur).apply {
59 - val drawable = AppCompatResources.getDrawable(application, R.drawable.background) as BitmapDrawable  
60 - backgroundImage = drawable.bitmap 62 + backgroundImage = virtualBackground
61 } 63 }
62 64
63 private var cameraProvider: CameraCapturerUtils.CameraProvider? = null 65 private var cameraProvider: CameraCapturerUtils.CameraProvider? = null
@@ -119,4 +121,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -119,4 +121,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
119 blur += 5 121 blur += 5
120 processor.updateBlurRadius(blur) 122 processor.updateBlurRadius(blur)
121 } 123 }
  124 +
  125 + fun toggleVirtualBackground(): Boolean {
  126 + if (processor.backgroundImage != virtualBackground) {
  127 + processor.backgroundImage = virtualBackground
  128 + return true
  129 + } else {
  130 + processor.backgroundImage = null
  131 + return false
  132 + }
  133 + }
  134 +
  135 + fun flipCamera() {
  136 + val videoTrack = track.value ?: return
  137 + val newPosition = when (videoTrack.options.position) {
  138 + CameraPosition.FRONT -> CameraPosition.BACK
  139 + CameraPosition.BACK -> CameraPosition.FRONT
  140 + else -> CameraPosition.FRONT
  141 + }
  142 + videoTrack.switchCamera(position = newPosition)
  143 + }
122 } 144 }
@@ -10,6 +10,11 @@ @@ -10,6 +10,11 @@
10 android:layout_width="match_parent" 10 android:layout_width="match_parent"
11 android:layout_height="match_parent" /> 11 android:layout_height="match_parent" />
12 12
  13 + <LinearLayout
  14 + android:layout_width="wrap_content"
  15 + android:layout_height="wrap_content"
  16 + android:orientation="vertical">
  17 +
13 <Button 18 <Button
14 android:id="@+id/button" 19 android:id="@+id/button"
15 android:layout_width="wrap_content" 20 android:layout_width="wrap_content"
@@ -18,6 +23,21 @@ @@ -18,6 +23,21 @@
18 android:text="Disable" /> 23 android:text="Disable" />
19 24
20 <Button 25 <Button
  26 + android:id="@+id/buttonBackground"
  27 + android:layout_width="wrap_content"
  28 + android:layout_height="wrap_content"
  29 + android:layout_margin="10dp"
  30 + android:text="Disable Background" />
  31 +
  32 + <Button
  33 + android:id="@+id/buttonFlip"
  34 + android:layout_width="wrap_content"
  35 + android:layout_height="wrap_content"
  36 + android:layout_margin="10dp"
  37 + android:text="Flip Camera" />
  38 + </LinearLayout>
  39 +
  40 + <Button
21 android:id="@+id/buttonIncreaseBlur" 41 android:id="@+id/buttonIncreaseBlur"
22 android:layout_width="wrap_content" 42 android:layout_width="wrap_content"
23 android:layout_height="wrap_content" 43 android:layout_height="wrap_content"
@@ -26,7 +26,6 @@ import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_O @@ -26,7 +26,6 @@ import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_O
26 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON 26 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
27 import android.hardware.camera2.CaptureRequest 27 import android.hardware.camera2.CaptureRequest
28 import android.os.Build 28 import android.os.Build
29 -import android.os.Build.VERSION  
30 import android.os.Handler 29 import android.os.Handler
31 import android.util.Range 30 import android.util.Range
32 import android.util.Size 31 import android.util.Size
@@ -345,7 +344,7 @@ internal constructor( @@ -345,7 +344,7 @@ internal constructor(
345 if (id == deviceId) return CameraDeviceId(id, null) 344 if (id == deviceId) return CameraDeviceId(id, null)
346 345
347 // Then check if deviceId is a physical camera ID in a logical camera 346 // Then check if deviceId is a physical camera ID in a logical camera
348 - if (VERSION.SDK_INT >= Build.VERSION_CODES.P) { 347 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
349 val characteristic = cameraManager.getCameraCharacteristics(id) 348 val characteristic = cameraManager.getCameraCharacteristics(id)
350 349
351 for (physicalId in characteristic.physicalCameraIds) { 350 for (physicalId in characteristic.physicalCameraIds) {
@@ -23,7 +23,12 @@ import android.os.Build @@ -23,7 +23,12 @@ import android.os.Build
23 import android.os.Handler 23 import android.os.Handler
24 import android.os.HandlerThread 24 import android.os.HandlerThread
25 import android.os.Looper 25 import android.os.Looper
26 -import com.twilio.audioswitch.* 26 +import com.twilio.audioswitch.AbstractAudioSwitch
  27 +import com.twilio.audioswitch.AudioDevice
  28 +import com.twilio.audioswitch.AudioDeviceChangeListener
  29 +import com.twilio.audioswitch.AudioSwitch
  30 +import com.twilio.audioswitch.LegacyAudioSwitch
  31 +import io.livekit.android.room.Room
27 import io.livekit.android.util.LKLog 32 import io.livekit.android.util.LKLog
28 import javax.inject.Inject 33 import javax.inject.Inject
29 import javax.inject.Singleton 34 import javax.inject.Singleton
@@ -234,7 +234,6 @@ constructor( @@ -234,7 +234,6 @@ constructor(
234 AudioFormat.ENCODING_PCM_8BIT -> 1 234 AudioFormat.ENCODING_PCM_8BIT -> 1
235 AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2 235 AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2
236 AudioFormat.ENCODING_PCM_FLOAT -> 4 236 AudioFormat.ENCODING_PCM_FLOAT -> 4
237 - AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat")  
238 else -> throw IllegalArgumentException("Bad audio format $audioFormat") 237 else -> throw IllegalArgumentException("Bad audio format $audioFormat")
239 } 238 }
240 } 239 }
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.
@@ -58,7 +58,7 @@ constructor( @@ -58,7 +58,7 @@ constructor(
58 this.peerConnectionFactory = peerConnectionFactory 58 this.peerConnectionFactory = peerConnectionFactory
59 } 59 }
60 60
61 - public fun keyProvider(): KeyProvider { 61 + fun keyProvider(): KeyProvider {
62 return this.keyProvider 62 return this.keyProvider
63 } 63 }
64 64
@@ -70,16 +70,16 @@ constructor( @@ -70,16 +70,16 @@ constructor(
70 this.enabled = true 70 this.enabled = true
71 this.room = room 71 this.room = room
72 this.emitEvent = emitEvent 72 this.emitEvent = emitEvent
73 - this.room?.localParticipant?.trackPublications?.forEach() { item -> 73 + this.room?.localParticipant?.trackPublications?.forEach { item ->
74 var participant = this.room!!.localParticipant 74 var participant = this.room!!.localParticipant
75 var publication = item.value 75 var publication = item.value
76 if (publication.track != null) { 76 if (publication.track != null) {
77 addPublishedTrack(publication.track!!, publication, participant, room) 77 addPublishedTrack(publication.track!!, publication, participant, room)
78 } 78 }
79 } 79 }
80 - this.room?.remoteParticipants?.forEach() { item -> 80 + this.room?.remoteParticipants?.forEach { item ->
81 var participant = item.value 81 var participant = item.value
82 - participant.trackPublications.forEach() { item -> 82 + participant.trackPublications.forEach { item ->
83 var publication = item.value 83 var publication = item.value
84 if (publication.track != null) { 84 if (publication.track != null) {
85 addSubscribedTrack(publication.track!!, publication, participant, room) 85 addSubscribedTrack(publication.track!!, publication, participant, room)
@@ -88,7 +88,7 @@ constructor( @@ -88,7 +88,7 @@ constructor(
88 } 88 }
89 } 89 }
90 90
91 - public fun addSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) { 91 + fun addSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
92 var rtpReceiver: RtpReceiver? = when (publication.track!!) { 92 var rtpReceiver: RtpReceiver? = when (publication.track!!) {
93 is RemoteAudioTrack -> (publication.track!! as RemoteAudioTrack).receiver 93 is RemoteAudioTrack -> (publication.track!! as RemoteAudioTrack).receiver
94 is RemoteVideoTrack -> (publication.track!! as RemoteVideoTrack).receiver 94 is RemoteVideoTrack -> (publication.track!! as RemoteVideoTrack).receiver
@@ -111,7 +111,7 @@ constructor( @@ -111,7 +111,7 @@ constructor(
111 } 111 }
112 } 112 }
113 113
114 - public fun removeSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) { 114 + fun removeSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
115 var trackId = publication.sid 115 var trackId = publication.sid
116 var participantId = participant.identity 116 var participantId = participant.identity
117 var frameCryptor = frameCryptors.get(trackId to participantId) 117 var frameCryptor = frameCryptors.get(trackId to participantId)
@@ -122,7 +122,7 @@ constructor( @@ -122,7 +122,7 @@ constructor(
122 } 122 }
123 } 123 }
124 124
125 - public fun addPublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) { 125 + fun addPublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
126 var rtpSender: RtpSender? = when (publication.track!!) { 126 var rtpSender: RtpSender? = when (publication.track!!) {
127 is LocalAudioTrack -> (publication.track!! as LocalAudioTrack)?.sender 127 is LocalAudioTrack -> (publication.track!! as LocalAudioTrack)?.sender
128 is LocalVideoTrack -> (publication.track!! as LocalVideoTrack)?.sender 128 is LocalVideoTrack -> (publication.track!! as LocalVideoTrack)?.sender
@@ -146,7 +146,7 @@ constructor( @@ -146,7 +146,7 @@ constructor(
146 } 146 }
147 } 147 }
148 148
149 - public fun removePublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) { 149 + fun removePublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
150 var trackId = publication.sid 150 var trackId = publication.sid
151 var participantId = participant.identity 151 var participantId = participant.identity
152 var frameCryptor = frameCryptors.get(trackId to participantId) 152 var frameCryptor = frameCryptors.get(trackId to participantId)
@@ -202,7 +202,7 @@ constructor( @@ -202,7 +202,7 @@ constructor(
202 * Enable or disable E2EE 202 * Enable or disable E2EE
203 * @param enabled 203 * @param enabled
204 */ 204 */
205 - public fun enableE2EE(enabled: Boolean) { 205 + fun enableE2EE(enabled: Boolean) {
206 this.enabled = enabled 206 this.enabled = enabled
207 for (item in frameCryptors.entries) { 207 for (item in frameCryptors.entries) {
208 var frameCryptor = item.value 208 var frameCryptor = item.value
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,9 +25,8 @@ internal const val defaultFailureTolerance = -1 @@ -25,9 +25,8 @@ internal const val defaultFailureTolerance = -1
25 internal const val defaultKeyRingSize = 16 25 internal const val defaultKeyRingSize = 16
26 internal const val defaultDiscardFrameWhenCryptorNotReady = false 26 internal const val defaultDiscardFrameWhenCryptorNotReady = false
27 27
28 -class E2EEOptions  
29 -constructor(  
30 - keyProvider: KeyProvider = BaseKeyProvider( 28 +class E2EEOptions(
  29 + var keyProvider: KeyProvider = BaseKeyProvider(
31 defaultRatchetSalt, 30 defaultRatchetSalt,
32 defaultMagicBytes, 31 defaultMagicBytes,
33 defaultRatchetWindowSize, 32 defaultRatchetWindowSize,
@@ -38,11 +37,9 @@ constructor( @@ -38,11 +37,9 @@ constructor(
38 ), 37 ),
39 encryptionType: Encryption.Type = Encryption.Type.GCM, 38 encryptionType: Encryption.Type = Encryption.Type.GCM,
40 ) { 39 ) {
41 - var keyProvider: KeyProvider  
42 var encryptionType: Encryption.Type = Encryption.Type.NONE 40 var encryptionType: Encryption.Type = Encryption.Type.NONE
43 41
44 init { 42 init {
45 - this.keyProvider = keyProvider  
46 this.encryptionType = encryptionType 43 this.encryptionType = encryptionType
47 } 44 }
48 } 45 }
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,14 +20,13 @@ import io.livekit.android.util.LKLog @@ -20,14 +20,13 @@ import io.livekit.android.util.LKLog
20 import livekit.org.webrtc.FrameCryptorFactory 20 import livekit.org.webrtc.FrameCryptorFactory
21 import livekit.org.webrtc.FrameCryptorKeyProvider 21 import livekit.org.webrtc.FrameCryptorKeyProvider
22 22
23 -class KeyInfo  
24 -constructor(var participantId: String, var keyIndex: Int, var key: String) { 23 +class KeyInfo(var participantId: String, var keyIndex: Int, var key: String) {
25 override fun toString(): String { 24 override fun toString(): String {
26 return "KeyInfo(participantId='$participantId', keyIndex=$keyIndex)" 25 return "KeyInfo(participantId='$participantId', keyIndex=$keyIndex)"
27 } 26 }
28 } 27 }
29 28
30 -public interface KeyProvider { 29 +interface KeyProvider {
31 fun setSharedKey(key: String, keyIndex: Int? = 0): Boolean 30 fun setSharedKey(key: String, keyIndex: Int? = 0): Boolean
32 fun ratchetSharedKey(keyIndex: Int? = 0): ByteArray 31 fun ratchetSharedKey(keyIndex: Int? = 0): ByteArray
33 fun exportSharedKey(keyIndex: Int? = 0): ByteArray 32 fun exportSharedKey(keyIndex: Int? = 0): ByteArray
@@ -41,17 +40,30 @@ public interface KeyProvider { @@ -41,17 +40,30 @@ public interface KeyProvider {
41 var enableSharedKey: Boolean 40 var enableSharedKey: Boolean
42 } 41 }
43 42
44 -class BaseKeyProvider  
45 -constructor(  
46 - private var ratchetSalt: String = defaultRatchetSalt,  
47 - private var uncryptedMagicBytes: String = defaultMagicBytes,  
48 - private var ratchetWindowSize: Int = defaultRatchetWindowSize, 43 +class BaseKeyProvider(
  44 + ratchetSalt: String = defaultRatchetSalt,
  45 + uncryptedMagicBytes: String = defaultMagicBytes,
  46 + ratchetWindowSize: Int = defaultRatchetWindowSize,
49 override var enableSharedKey: Boolean = true, 47 override var enableSharedKey: Boolean = true,
50 - private var failureTolerance: Int = defaultFailureTolerance,  
51 - private var keyRingSize: Int = defaultKeyRingSize,  
52 - private var discardFrameWhenCryptorNotReady: Boolean = defaultDiscardFrameWhenCryptorNotReady, 48 + failureTolerance: Int = defaultFailureTolerance,
  49 + keyRingSize: Int = defaultKeyRingSize,
  50 + discardFrameWhenCryptorNotReady: Boolean = defaultDiscardFrameWhenCryptorNotReady,
53 ) : KeyProvider { 51 ) : KeyProvider {
  52 + override val rtcKeyProvider: FrameCryptorKeyProvider
  53 +
54 private var keys: MutableMap<String, MutableMap<Int, String>> = mutableMapOf() 54 private var keys: MutableMap<String, MutableMap<Int, String>> = mutableMapOf()
  55 +
  56 + init {
  57 + this.rtcKeyProvider = FrameCryptorFactory.createFrameCryptorKeyProvider(
  58 + enableSharedKey,
  59 + ratchetSalt.toByteArray(),
  60 + ratchetWindowSize,
  61 + uncryptedMagicBytes.toByteArray(),
  62 + failureTolerance,
  63 + keyRingSize,
  64 + discardFrameWhenCryptorNotReady,
  65 + )
  66 + }
55 override fun setSharedKey(key: String, keyIndex: Int?): Boolean { 67 override fun setSharedKey(key: String, keyIndex: Int?): Boolean {
56 return rtcKeyProvider.setSharedKey(keyIndex ?: 0, key.toByteArray()) 68 return rtcKeyProvider.setSharedKey(keyIndex ?: 0, key.toByteArray())
57 } 69 }
@@ -100,18 +112,4 @@ constructor( @@ -100,18 +112,4 @@ constructor(
100 override fun setSifTrailer(trailer: ByteArray) { 112 override fun setSifTrailer(trailer: ByteArray) {
101 rtcKeyProvider.setSifTrailer(trailer) 113 rtcKeyProvider.setSifTrailer(trailer)
102 } 114 }
103 -  
104 - override val rtcKeyProvider: FrameCryptorKeyProvider  
105 -  
106 - init {  
107 - this.rtcKeyProvider = FrameCryptorFactory.createFrameCryptorKeyProvider(  
108 - enableSharedKey,  
109 - ratchetSalt.toByteArray(),  
110 - ratchetWindowSize,  
111 - uncryptedMagicBytes.toByteArray(),  
112 - failureTolerance,  
113 - keyRingSize,  
114 - discardFrameWhenCryptorNotReady,  
115 - )  
116 - }  
117 } 115 }
@@ -22,8 +22,18 @@ import android.view.TextureView @@ -22,8 +22,18 @@ import android.view.TextureView
22 import android.view.View 22 import android.view.View
23 import io.livekit.android.room.track.video.ViewVisibility 23 import io.livekit.android.room.track.video.ViewVisibility
24 import io.livekit.android.util.LKLog 24 import io.livekit.android.util.LKLog
25 -import livekit.org.webrtc.*  
26 -import livekit.org.webrtc.RendererCommon.* 25 +import livekit.org.webrtc.EglBase
  26 +import livekit.org.webrtc.EglRenderer
  27 +import livekit.org.webrtc.GlRectDrawer
  28 +import livekit.org.webrtc.Logging
  29 +import livekit.org.webrtc.RendererCommon.GlDrawer
  30 +import livekit.org.webrtc.RendererCommon.RendererEvents
  31 +import livekit.org.webrtc.RendererCommon.ScalingType
  32 +import livekit.org.webrtc.RendererCommon.VideoLayoutMeasure
  33 +import livekit.org.webrtc.SurfaceEglRenderer
  34 +import livekit.org.webrtc.ThreadUtils
  35 +import livekit.org.webrtc.VideoFrame
  36 +import livekit.org.webrtc.VideoSink
27 import java.util.concurrent.CountDownLatch 37 import java.util.concurrent.CountDownLatch
28 38
29 /** 39 /**
@@ -119,7 +129,7 @@ open class TextureViewRenderer : @@ -119,7 +129,7 @@ open class TextureViewRenderer :
119 * It should be lightweight and must not call removeFrameListener. 129 * It should be lightweight and must not call removeFrameListener.
120 * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is 130 * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
121 * required. 131 * required.
122 - * @param drawer Custom drawer to use for this frame listener. 132 + * @param drawerParam Custom drawer to use for this frame listener.
123 */ 133 */
124 fun addFrameListener( 134 fun addFrameListener(
125 listener: EglRenderer.FrameListener?, 135 listener: EglRenderer.FrameListener?,
@@ -578,7 +578,7 @@ internal constructor( @@ -578,7 +578,7 @@ internal constructor(
578 } 578 }
579 579
580 LKLog.v { "ws reconnected, restarting ICE" } 580 LKLog.v { "ws reconnected, restarting ICE" }
581 - listener?.onSignalConnected(!isFullReconnect) 581 + listener?.onSignalConnected(true)
582 582
583 // trigger publisher reconnect 583 // trigger publisher reconnect
584 // only restart publisher if it's needed 584 // only restart publisher if it's needed
@@ -833,13 +833,11 @@ internal constructor( @@ -833,13 +833,11 @@ internal constructor(
833 833
834 val rtcConfig = connectOptions.rtcConfig?.copy()?.apply { 834 val rtcConfig = connectOptions.rtcConfig?.copy()?.apply {
835 val mergedServers = iceServers.toMutableList() 835 val mergedServers = iceServers.toMutableList()
836 - if (connectOptions.iceServers != null) {  
837 - connectOptions.iceServers.forEach { server -> 836 + connectOptions.iceServers?.forEach { server ->
838 if (!mergedServers.contains(server)) { 837 if (!mergedServers.contains(server)) {
839 mergedServers.add(server) 838 mergedServers.add(server)
840 } 839 }
841 } 840 }
842 - }  
843 841
844 // Only use server-provided servers if user doesn't provide any. 842 // Only use server-provided servers if user doesn't provide any.
845 if (mergedServers.isEmpty()) { 843 if (mergedServers.isEmpty()) {
@@ -1088,9 +1086,7 @@ internal constructor( @@ -1088,9 +1086,7 @@ internal constructor(
1088 abortPendingPublishTracks() 1086 abortPendingPublishTracks()
1089 1087
1090 if (leave.hasRegions()) { 1088 if (leave.hasRegions()) {
1091 - regionUrlProvider?.let {  
1092 - it.setServerReportedRegions(RegionSettings.fromProto(leave.regions))  
1093 - } 1089 + regionUrlProvider?.setServerReportedRegions(RegionSettings.fromProto(leave.regions))
1094 } 1090 }
1095 1091
1096 when { 1092 when {
@@ -1242,8 +1238,7 @@ internal constructor( @@ -1242,8 +1238,7 @@ internal constructor(
1242 } 1238 }
1243 } 1239 }
1244 1240
1245 - val dataChannelInfos = LivekitModels.DataPacket.Kind.values()  
1246 - .toList() 1241 + val dataChannelInfos = LivekitModels.DataPacket.Kind.entries
1247 .filterNot { it == LivekitModels.DataPacket.Kind.UNRECOGNIZED } 1242 .filterNot { it == LivekitModels.DataPacket.Kind.UNRECOGNIZED }
1248 .mapNotNull { kind -> dataChannelForKind(kind) } 1243 .mapNotNull { kind -> dataChannelForKind(kind) }
1249 .map { dataChannel -> 1244 .map { dataChannel ->
1 /* 1 /*
2 - * Copyright 2024 LiveKit, Inc. 2 + * Copyright 2024-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.
@@ -87,7 +87,11 @@ constructor( @@ -87,7 +87,11 @@ constructor(
87 if (!response.isSuccessful) { 87 if (!response.isSuccessful) {
88 throw RoomException.ConnectException("Could not fetch region settings: ${response.code} ${response.message}") 88 throw RoomException.ConnectException("Could not fetch region settings: ${response.code} ${response.message}")
89 } 89 }
90 - return@use response.body?.string() ?: return null 90 + return@use response.body?.string()
  91 + }
  92 +
  93 + if (bodyString == null) {
  94 + throw RoomException.ConnectException("Could not fetch region settings: empty response body!")
91 } 95 }
92 96
93 return json.decodeFromString<RegionSettings>(bodyString).also { 97 return json.decodeFromString<RegionSettings>(bodyString).also {
@@ -319,7 +319,7 @@ internal constructor( @@ -319,7 +319,7 @@ internal constructor(
319 * @param screenCaptureParams When enabling the screenshare, this must be provided with 319 * @param screenCaptureParams When enabling the screenshare, this must be provided with
320 * [ScreenCaptureParams.mediaProjectionPermissionResultData] containing resultData returned from launching 320 * [ScreenCaptureParams.mediaProjectionPermissionResultData] containing resultData returned from launching
321 * [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()). 321 * [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()).
322 - * @throws IllegalArgumentException if attempting to enable screenshare without [mediaProjectionPermissionResultData] 322 + * @throws IllegalArgumentException if attempting to enable screenshare without [screenCaptureParams]
323 * @see Room.screenShareTrackCaptureDefaults 323 * @see Room.screenShareTrackCaptureDefaults
324 * @see Room.screenShareTrackPublishDefaults 324 * @see Room.screenShareTrackPublishDefaults
325 * @see ScreenAudioCapturer 325 * @see ScreenAudioCapturer
@@ -429,9 +429,7 @@ open class Participant( @@ -429,9 +429,7 @@ open class Participant(
429 429
430 other as Participant 430 other as Participant
431 431
432 - if (sid != other.sid) return false  
433 -  
434 - return true 432 + return sid == other.sid
435 } 433 }
436 434
437 override fun hashCode(): Int { 435 override fun hashCode(): Int {
@@ -38,7 +38,6 @@ import io.livekit.android.room.util.EncodingUtils @@ -38,7 +38,6 @@ import io.livekit.android.room.util.EncodingUtils
38 import io.livekit.android.util.FlowObservable 38 import io.livekit.android.util.FlowObservable
39 import io.livekit.android.util.LKLog 39 import io.livekit.android.util.LKLog
40 import io.livekit.android.util.flowDelegate 40 import io.livekit.android.util.flowDelegate
41 -import livekit.LivekitModels  
42 import livekit.LivekitRtc 41 import livekit.LivekitRtc
43 import livekit.LivekitRtc.SubscribedCodec 42 import livekit.LivekitRtc.SubscribedCodec
44 import livekit.org.webrtc.CameraVideoCapturer 43 import livekit.org.webrtc.CameraVideoCapturer
@@ -357,7 +356,7 @@ constructor( @@ -357,7 +356,7 @@ constructor(
357 val rid = EncodingUtils.ridForVideoQuality(quality.quality) ?: continue 356 val rid = EncodingUtils.ridForVideoQuality(quality.quality) ?: continue
358 val encoding = encodings.firstOrNull { it.rid == rid } 357 val encoding = encodings.firstOrNull { it.rid == rid }
359 // use low quality layer settings for non-simulcasted streams 358 // use low quality layer settings for non-simulcasted streams
360 - ?: encodings.takeIf { it.size == 1 && quality.quality == LivekitModels.VideoQuality.LOW }?.first() 359 + ?: encodings.takeIf { it.size == 1 && quality.quality == ProtoVideoQuality.LOW }?.first()
361 ?: continue 360 ?: continue
362 if (encoding.active != quality.enabled) { 361 if (encoding.active != quality.enabled) {
363 hasChanged = true 362 hasChanged = true
@@ -229,5 +229,5 @@ sealed class TrackException(message: String? = null, cause: Throwable? = null) : @@ -229,5 +229,5 @@ sealed class TrackException(message: String? = null, cause: Throwable? = null) :
229 TrackException(message, cause) 229 TrackException(message, cause)
230 } 230 }
231 231
232 -public const val KIND_AUDIO = "audio"  
233 -public const val KIND_VIDEO = "video" 232 +const val KIND_AUDIO = "audio"
  233 +const val KIND_VIDEO = "video"
1 /* 1 /*
2 - * Copyright 2023 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.
@@ -32,7 +32,7 @@ import kotlin.coroutines.resume @@ -32,7 +32,7 @@ import kotlin.coroutines.resume
32 * Handles connecting to a [ScreenCaptureService]. 32 * Handles connecting to a [ScreenCaptureService].
33 */ 33 */
34 internal class ScreenCaptureConnection(private val context: Context) { 34 internal class ScreenCaptureConnection(private val context: Context) {
35 - public var isBound = false 35 + var isBound = false
36 private set 36 private set
37 private var service: ScreenCaptureService? = null 37 private var service: ScreenCaptureService? = null
38 private val queuedConnects = mutableSetOf<Continuation<Unit>>() 38 private val queuedConnects = mutableSetOf<Continuation<Unit>>()
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,7 +20,6 @@ import android.app.Notification @@ -20,7 +20,6 @@ import android.app.Notification
20 import android.app.NotificationChannel 20 import android.app.NotificationChannel
21 import android.app.NotificationManager 21 import android.app.NotificationManager
22 import android.app.Service 22 import android.app.Service
23 -import android.content.Context  
24 import android.content.Intent 23 import android.content.Intent
25 import android.os.Binder 24 import android.os.Binder
26 import android.os.Build 25 import android.os.Build
@@ -69,7 +68,7 @@ open class ScreenCaptureService : Service() { @@ -69,7 +68,7 @@ open class ScreenCaptureService : Service() {
69 "Screen Capture", 68 "Screen Capture",
70 NotificationManager.IMPORTANCE_LOW, 69 NotificationManager.IMPORTANCE_LOW,
71 ) 70 )
72 - val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 71 + val service = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
73 service.createNotificationChannel(channel) 72 service.createNotificationChannel(channel)
74 } 73 }
75 74
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,7 +23,6 @@ import android.view.View @@ -23,7 +23,6 @@ import android.view.View
23 import android.view.ViewTreeObserver 23 import android.view.ViewTreeObserver
24 import androidx.annotation.CallSuper 24 import androidx.annotation.CallSuper
25 import io.livekit.android.room.track.Track 25 import io.livekit.android.room.track.Track
26 -import io.livekit.android.room.track.video.ViewVisibility.Notifier  
27 import java.util.Observable 26 import java.util.Observable
28 27
29 /** 28 /**
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.
@@ -26,8 +26,6 @@ import kotlin.coroutines.resume @@ -26,8 +26,6 @@ import kotlin.coroutines.resume
26 26
27 /** 27 /**
28 * Returns an RTCStatsReport with all the relevant information pertaining to a track. 28 * Returns an RTCStatsReport with all the relevant information pertaining to a track.
29 - *  
30 - * @param trackIdentifier track, sender, or receiver id  
31 */ 29 */
32 fun RTCStatsReport.getFilteredStats(track: MediaStreamTrack): RTCStatsReport { 30 fun RTCStatsReport.getFilteredStats(track: MediaStreamTrack): RTCStatsReport {
33 return getFilteredStats(track.id()) 31 return getFilteredStats(track.id())
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.
@@ -256,7 +256,7 @@ class MockPeerConnection( @@ -256,7 +256,7 @@ class MockPeerConnection(
256 if (currentOrdinal < newOrdinal) { 256 if (currentOrdinal < newOrdinal) {
257 // Ensure that we move through each state. 257 // Ensure that we move through each state.
258 for (ordinal in ((currentOrdinal + 1)..newOrdinal)) { 258 for (ordinal in ((currentOrdinal + 1)..newOrdinal)) {
259 - iceConnectionState = IceConnectionState.values()[ordinal] 259 + iceConnectionState = IceConnectionState.entries[ordinal]
260 } 260 }
261 } else { 261 } else {
262 iceConnectionState = newState 262 iceConnectionState = newState
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.
@@ -48,7 +48,7 @@ class MockWebSocket( @@ -48,7 +48,7 @@ class MockWebSocket(
48 isClosed = true 48 isClosed = true
49 listener.onClosing(this, code, reason ?: "") 49 listener.onClosing(this, code, reason ?: "")
50 listener.onClosed(this, code, reason ?: "") 50 listener.onClosed(this, code, reason ?: "")
51 - return willClose 51 + return true
52 } 52 }
53 53
54 override fun queueSize(): Long = 0 54 override fun queueSize(): Long = 0
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,7 +20,7 @@ import com.google.protobuf.ByteString @@ -20,7 +20,7 @@ import com.google.protobuf.ByteString
20 import livekit.LivekitRtc 20 import livekit.LivekitRtc
21 import okio.ByteString.Companion.toByteString 21 import okio.ByteString.Companion.toByteString
22 22
23 -fun com.google.protobuf.ByteString.toOkioByteString() = toByteArray().toByteString() 23 +fun ByteString.toOkioByteString() = toByteArray().toByteString()
24 24
25 fun okio.ByteString.toPBByteString() = ByteString.copyFrom(toByteArray()) 25 fun okio.ByteString.toPBByteString() = ByteString.copyFrom(toByteArray())
26 26
1 /* 1 /*
2 - * Copyright 2024 LiveKit, Inc. 2 + * Copyright 2024-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.
@@ -97,8 +97,7 @@ class MixerAudioBufferCallbackTest { @@ -97,8 +97,7 @@ class MixerAudioBufferCallbackTest {
97 AudioFormat.ENCODING_PCM_FLOAT -> { 97 AudioFormat.ENCODING_PCM_FLOAT -> {
98 byteBuffer.asFloatBuffer().put(0, INCREMENT.toFloat()) 98 byteBuffer.asFloatBuffer().put(0, INCREMENT.toFloat())
99 } 99 }
100 -  
101 - AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat") 100 + else -> throw IllegalArgumentException("Bad audio format $audioFormat")
102 } 101 }
103 102
104 return BufferResponse(byteBuffer) 103 return BufferResponse(byteBuffer)
@@ -111,7 +110,6 @@ private fun getBytesPerSample(audioFormat: Int): Int { @@ -111,7 +110,6 @@ private fun getBytesPerSample(audioFormat: Int): Int {
111 AudioFormat.ENCODING_PCM_8BIT -> 1 110 AudioFormat.ENCODING_PCM_8BIT -> 1
112 AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2 111 AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2
113 AudioFormat.ENCODING_PCM_FLOAT -> 4 112 AudioFormat.ENCODING_PCM_FLOAT -> 4
114 - AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat")  
115 else -> throw IllegalArgumentException("Bad audio format $audioFormat") 113 else -> throw IllegalArgumentException("Bad audio format $audioFormat")
116 } 114 }
117 } 115 }
@@ -58,10 +58,10 @@ class RoomMockE2ETest : MockE2ETest() { @@ -58,10 +58,10 @@ class RoomMockE2ETest : MockE2ETest() {
58 val collector = FlowCollector(room::state.flow, coroutineRule.scope) 58 val collector = FlowCollector(room::state.flow, coroutineRule.scope)
59 connect() 59 connect()
60 val events = collector.stopCollecting() 60 val events = collector.stopCollecting()
61 - Assert.assertEquals(3, events.size)  
62 - Assert.assertEquals(Room.State.DISCONNECTED, events[0])  
63 - Assert.assertEquals(Room.State.CONNECTING, events[1])  
64 - Assert.assertEquals(Room.State.CONNECTED, events[2]) 61 + assertEquals(3, events.size)
  62 + assertEquals(Room.State.DISCONNECTED, events[0])
  63 + assertEquals(Room.State.CONNECTING, events[1])
  64 + assertEquals(Room.State.CONNECTED, events[2])
65 } 65 }
66 66
67 @Test 67 @Test
@@ -155,9 +155,9 @@ class RoomMockE2ETest : MockE2ETest() { @@ -155,9 +155,9 @@ class RoomMockE2ETest : MockE2ETest() {
155 ) 155 )
156 val events = eventCollector.stopCollecting() 156 val events = eventCollector.stopCollecting()
157 157
158 - Assert.assertEquals(ConnectionQuality.EXCELLENT, room.localParticipant.connectionQuality)  
159 - Assert.assertEquals(1, events.size)  
160 - Assert.assertEquals(true, events[0] is RoomEvent.ConnectionQualityChanged) 158 + assertEquals(ConnectionQuality.EXCELLENT, room.localParticipant.connectionQuality)
  159 + assertEquals(1, events.size)
  160 + assertEquals(true, events[0] is RoomEvent.ConnectionQualityChanged)
161 } 161 }
162 162
163 @Test 163 @Test
@@ -252,8 +252,8 @@ class RoomMockE2ETest : MockE2ETest() { @@ -252,8 +252,8 @@ class RoomMockE2ETest : MockE2ETest() {
252 ) 252 )
253 val events = eventCollector.stopCollecting() 253 val events = eventCollector.stopCollecting()
254 254
255 - Assert.assertEquals(1, events.size)  
256 - Assert.assertEquals(true, events[0] is RoomEvent.ActiveSpeakersChanged) 255 + assertEquals(1, events.size)
  256 + assertEquals(true, events[0] is RoomEvent.ActiveSpeakersChanged)
257 } 257 }
258 258
259 @Test 259 @Test
@@ -286,11 +286,11 @@ class RoomMockE2ETest : MockE2ETest() { @@ -286,11 +286,11 @@ class RoomMockE2ETest : MockE2ETest() {
286 ) 286 )
287 val events = eventCollector.stopCollecting() 287 val events = eventCollector.stopCollecting()
288 288
289 - Assert.assertEquals(1, events.size)  
290 - Assert.assertEquals(true, events[0] is RoomEvent.TrackStreamStateChanged) 289 + assertEquals(1, events.size)
  290 + assertEquals(true, events[0] is RoomEvent.TrackStreamStateChanged)
291 291
292 val event = events[0] as RoomEvent.TrackStreamStateChanged 292 val event = events[0] as RoomEvent.TrackStreamStateChanged
293 - Assert.assertEquals(Track.StreamState.ACTIVE, event.streamState) 293 + assertEquals(Track.StreamState.ACTIVE, event.streamState)
294 } 294 }
295 295
296 @Test 296 @Test
@@ -320,13 +320,13 @@ class RoomMockE2ETest : MockE2ETest() { @@ -320,13 +320,13 @@ class RoomMockE2ETest : MockE2ETest() {
320 ) 320 )
321 val events = eventCollector.stopCollecting() 321 val events = eventCollector.stopCollecting()
322 322
323 - Assert.assertEquals(1, events.size)  
324 - Assert.assertEquals(true, events[0] is RoomEvent.TrackSubscriptionPermissionChanged) 323 + assertEquals(1, events.size)
  324 + assertEquals(true, events[0] is RoomEvent.TrackSubscriptionPermissionChanged)
325 325
326 val event = events[0] as RoomEvent.TrackSubscriptionPermissionChanged 326 val event = events[0] as RoomEvent.TrackSubscriptionPermissionChanged
327 - Assert.assertEquals(TestData.REMOTE_PARTICIPANT.sid, event.participant.sid.value)  
328 - Assert.assertEquals(TestData.REMOTE_AUDIO_TRACK.sid, event.trackPublication.sid)  
329 - Assert.assertEquals(false, event.subscriptionAllowed) 327 + assertEquals(TestData.REMOTE_PARTICIPANT.sid, event.participant.sid.value)
  328 + assertEquals(TestData.REMOTE_AUDIO_TRACK.sid, event.trackPublication.sid)
  329 + assertEquals(false, event.subscriptionAllowed)
330 } 330 }
331 331
332 @Test 332 @Test
@@ -495,6 +495,6 @@ class RoomMockE2ETest : MockE2ETest() { @@ -495,6 +495,6 @@ class RoomMockE2ETest : MockE2ETest() {
495 connect() 495 connect()
496 room.disconnect() 496 room.disconnect()
497 connect() 497 connect()
498 - Assert.assertEquals(room.state, Room.State.CONNECTED) 498 + assertEquals(room.state, Room.State.CONNECTED)
499 } 499 }
500 } 500 }
@@ -25,7 +25,6 @@ import io.livekit.android.test.util.toPBByteString @@ -25,7 +25,6 @@ import io.livekit.android.test.util.toPBByteString
25 import kotlinx.coroutines.ExperimentalCoroutinesApi 25 import kotlinx.coroutines.ExperimentalCoroutinesApi
26 import livekit.LivekitRtc 26 import livekit.LivekitRtc
27 import livekit.org.webrtc.PeerConnection 27 import livekit.org.webrtc.PeerConnection
28 -import org.junit.Assert  
29 import org.junit.Assert.assertEquals 28 import org.junit.Assert.assertEquals
30 import org.junit.Assert.assertTrue 29 import org.junit.Assert.assertTrue
31 import org.junit.Test 30 import org.junit.Test
@@ -76,7 +75,7 @@ class RoomReconnectionMockE2ETest : MockE2ETest() { @@ -76,7 +75,7 @@ class RoomReconnectionMockE2ETest : MockE2ETest() {
76 return@any sentRequest.hasSyncState() 75 return@any sentRequest.hasSyncState()
77 } 76 }
78 77
79 - Assert.assertTrue(sentSyncState) 78 + assertTrue(sentSyncState)
80 } 79 }
81 80
82 @Test 81 @Test
@@ -146,6 +145,6 @@ class RoomReconnectionMockE2ETest : MockE2ETest() { @@ -146,6 +145,6 @@ class RoomReconnectionMockE2ETest : MockE2ETest() {
146 } 145 }
147 146
148 println(sentRequests) 147 println(sentRequests)
149 - Assert.assertTrue(sentAddTrack) 148 + assertTrue(sentAddTrack)
150 } 149 }
151 } 150 }
@@ -14,11 +14,11 @@ @@ -14,11 +14,11 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
17 -package io.livekit.android.room.participant 17 +package io.livekit.android.room.rpc
18 18
19 import com.google.protobuf.ByteString 19 import com.google.protobuf.ByteString
20 import io.livekit.android.room.RTCEngine 20 import io.livekit.android.room.RTCEngine
21 -import io.livekit.android.room.rpc.RpcManager 21 +import io.livekit.android.room.participant.Participant
22 import io.livekit.android.rpc.RpcError 22 import io.livekit.android.rpc.RpcError
23 import io.livekit.android.test.MockE2ETest 23 import io.livekit.android.test.MockE2ETest
24 import io.livekit.android.test.mock.MockDataChannel 24 import io.livekit.android.test.mock.MockDataChannel
@@ -140,7 +140,7 @@ class VirtualBackgroundTransformer( @@ -140,7 +140,7 @@ class VirtualBackgroundTransformer(
140 if (backgroundImage != null) { 140 if (backgroundImage != null) {
141 val bgTextureFrameBuffer = bgTextureFrameBuffers.first 141 val bgTextureFrameBuffer = bgTextureFrameBuffers.first
142 142
143 - if (backgroundImageNeedsUploading || true) { 143 + if (backgroundImageNeedsUploading) {
144 val byteBuffer = ByteBuffer.allocateDirect(backgroundImage.byteCount) 144 val byteBuffer = ByteBuffer.allocateDirect(backgroundImage.byteCount)
145 backgroundImage.copyPixelsToBuffer(byteBuffer) 145 backgroundImage.copyPixelsToBuffer(byteBuffer)
146 byteBuffer.rewind() 146 byteBuffer.rewind()
@@ -42,6 +42,7 @@ import livekit.org.webrtc.SurfaceTextureHelper @@ -42,6 +42,7 @@ import livekit.org.webrtc.SurfaceTextureHelper
42 import livekit.org.webrtc.VideoFrame 42 import livekit.org.webrtc.VideoFrame
43 import livekit.org.webrtc.VideoSink 43 import livekit.org.webrtc.VideoSink
44 import java.util.concurrent.Semaphore 44 import java.util.concurrent.Semaphore
  45 +import kotlin.math.roundToInt
45 46
46 /** 47 /**
47 * A virtual background video processor for the local camera video stream. 48 * A virtual background video processor for the local camera video stream.
@@ -90,6 +91,9 @@ class VirtualBackgroundVideoProcessor( @@ -90,6 +91,9 @@ class VirtualBackgroundVideoProcessor(
90 */ 91 */
91 var enabled: Boolean = true 92 var enabled: Boolean = true
92 93
  94 + /**
  95 + * A virtual background image to use.
  96 + */
93 var backgroundImage: Bitmap? = null 97 var backgroundImage: Bitmap? = null
94 set(value) { 98 set(value) {
95 field = value 99 field = value
@@ -191,7 +195,11 @@ class VirtualBackgroundVideoProcessor( @@ -191,7 +195,11 @@ class VirtualBackgroundVideoProcessor(
191 frame.retain() 195 frame.retain()
192 surfaceTextureHelper.handler.post { 196 surfaceTextureHelper.handler.post {
193 val backgroundImage = this.backgroundImage 197 val backgroundImage = this.backgroundImage
194 - if (backgroundImageNeedsUpdating && backgroundImage != null) { 198 + if (backgroundImageNeedsUpdating) {
  199 + if (backgroundImage == null) {
  200 + backgroundTransformer.backgroundImage = null
  201 + backgroundImageNeedsUpdating = false
  202 + } else {
195 val imageAspect = backgroundImage.width / backgroundImage.height.toFloat() 203 val imageAspect = backgroundImage.width / backgroundImage.height.toFloat()
196 val targetAspect = frame.rotatedWidth / frame.rotatedHeight.toFloat() 204 val targetAspect = frame.rotatedWidth / frame.rotatedHeight.toFloat()
197 var sx = 0 205 var sx = 0
@@ -200,15 +208,13 @@ class VirtualBackgroundVideoProcessor( @@ -200,15 +208,13 @@ class VirtualBackgroundVideoProcessor(
200 var sHeight = backgroundImage.height 208 var sHeight = backgroundImage.height
201 209
202 if (imageAspect > targetAspect) { 210 if (imageAspect > targetAspect) {
203 - sWidth = Math.round(backgroundImage.height * targetAspect)  
204 - sx = Math.round((backgroundImage.width - sWidth) / 2f) 211 + sWidth = (backgroundImage.height * targetAspect).roundToInt()
  212 + sx = ((backgroundImage.width - sWidth) / 2f).roundToInt()
205 } else { 213 } else {
206 - sHeight = Math.round(backgroundImage.width / targetAspect)  
207 - sy = Math.round((backgroundImage.height - sHeight) / 2f) 214 + sHeight = (backgroundImage.width / targetAspect).roundToInt()
  215 + sy = ((backgroundImage.height - sHeight) / 2f).roundToInt()
208 } 216 }
209 217
210 - val diffAspect = targetAspect / imageAspect  
211 -  
212 val matrix = Matrix() 218 val matrix = Matrix()
213 219
214 matrix.postRotate(-frame.rotation.toFloat()) 220 matrix.postRotate(-frame.rotation.toFloat())
@@ -225,6 +231,7 @@ class VirtualBackgroundVideoProcessor( @@ -225,6 +231,7 @@ class VirtualBackgroundVideoProcessor(
225 backgroundTransformer.backgroundImage = resizedImage 231 backgroundTransformer.backgroundImage = resizedImage
226 backgroundImageNeedsUpdating = false 232 backgroundImageNeedsUpdating = false
227 } 233 }
  234 + }
228 235
229 lastMask?.let { 236 lastMask?.let {
230 backgroundTransformer.updateMask(it) 237 backgroundTransformer.updateMask(it)
@@ -61,7 +61,7 @@ public class LKGlTextureFrameBuffer { @@ -61,7 +61,7 @@ public class LKGlTextureFrameBuffer {
61 textureId = GlUtil.generateTexture(GLES30.GL_TEXTURE_2D); 61 textureId = GlUtil.generateTexture(GLES30.GL_TEXTURE_2D);
62 } 62 }
63 if (frameBufferId == 0) { 63 if (frameBufferId == 0) {
64 - final int frameBuffers[] = new int[1]; 64 + final int[] frameBuffers = new int[1];
65 GLES30.glGenFramebuffers(1, frameBuffers, 0); 65 GLES30.glGenFramebuffers(1, frameBuffers, 0);
66 frameBufferId = frameBuffers[0]; 66 frameBufferId = frameBuffers[0];
67 } 67 }
1 /* 1 /*
2 - * Copyright 2024 LiveKit, Inc. 2 + * Copyright 2024-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.
@@ -19,7 +19,6 @@ package io.livekit.android.sample.service @@ -19,7 +19,6 @@ package io.livekit.android.sample.service
19 import android.app.NotificationChannel 19 import android.app.NotificationChannel
20 import android.app.NotificationManager 20 import android.app.NotificationManager
21 import android.app.Service 21 import android.app.Service
22 -import android.content.Context  
23 import android.content.Intent 22 import android.content.Intent
24 import android.os.Build 23 import android.os.Build
25 import android.os.IBinder 24 import android.os.IBinder
@@ -61,7 +60,7 @@ open class ForegroundService : Service() { @@ -61,7 +60,7 @@ open class ForegroundService : Service() {
61 "Foreground", 60 "Foreground",
62 NotificationManager.IMPORTANCE_LOW 61 NotificationManager.IMPORTANCE_LOW
63 ) 62 )
64 - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 63 + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
65 notificationManager.createNotificationChannel(channel) 64 notificationManager.createNotificationChannel(channel)
66 } 65 }
67 66
@@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
16 16
17 package io.livekit.android.composesample 17 package io.livekit.android.composesample
18 18
19 -import android.app.Activity  
20 import android.media.projection.MediaProjectionManager 19 import android.media.projection.MediaProjectionManager
21 import android.os.Bundle 20 import android.os.Bundle
22 import android.os.Parcelable 21 import android.os.Parcelable
@@ -100,7 +99,7 @@ class CallActivity : AppCompatActivity() { @@ -100,7 +99,7 @@ class CallActivity : AppCompatActivity() {
100 ) { result -> 99 ) { result ->
101 val resultCode = result.resultCode 100 val resultCode = result.resultCode
102 val data = result.data 101 val data = result.data
103 - if (resultCode != Activity.RESULT_OK || data == null) { 102 + if (resultCode != RESULT_OK || data == null) {
104 return@registerForActivityResult 103 return@registerForActivityResult
105 } 104 }
106 viewModel.startScreenCapture(data) 105 viewModel.startScreenCapture(data)
@@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
16 16
17 package io.livekit.android.sample 17 package io.livekit.android.sample
18 18
19 -import android.app.Activity  
20 import android.media.projection.MediaProjectionManager 19 import android.media.projection.MediaProjectionManager
21 import android.os.Bundle 20 import android.os.Bundle
22 import android.os.Parcelable 21 import android.os.Parcelable
@@ -63,7 +62,7 @@ class CallActivity : AppCompatActivity() { @@ -63,7 +62,7 @@ class CallActivity : AppCompatActivity() {
63 ) { result -> 62 ) { result ->
64 val resultCode = result.resultCode 63 val resultCode = result.resultCode
65 val data = result.data 64 val data = result.data
66 - if (resultCode != Activity.RESULT_OK || data == null) { 65 + if (resultCode != RESULT_OK || data == null) {
67 return@registerForActivityResult 66 return@registerForActivityResult
68 } 67 }
69 viewModel.startScreenCapture(data) 68 viewModel.startScreenCapture(data)
  1 +/*
  2 + * Copyright 2025 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
1 package io.livekit.android.videoencodedecode 17 package io.livekit.android.videoencodedecode
2 18
3 import android.os.Bundle 19 import android.os.Bundle
@@ -18,7 +34,7 @@ import androidx.constraintlayout.compose.Dimension @@ -18,7 +34,7 @@ import androidx.constraintlayout.compose.Dimension
18 import androidx.lifecycle.ViewModel 34 import androidx.lifecycle.ViewModel
19 import androidx.lifecycle.ViewModelProvider 35 import androidx.lifecycle.ViewModelProvider
20 import androidx.lifecycle.viewmodel.CreationExtras 36 import androidx.lifecycle.viewmodel.CreationExtras
21 -import io.livekit.android.composesample.ui.theme.AppTheme 37 +import io.livekit.android.videoencodedecode.ui.theme.AppTheme
22 import kotlinx.parcelize.Parcelize 38 import kotlinx.parcelize.Parcelize
23 39
24 class CallActivity : AppCompatActivity() { 40 class CallActivity : AppCompatActivity() {
1 /* 1 /*
2 - * Copyright 2023 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.
@@ -36,8 +36,8 @@ import androidx.compose.ui.tooling.preview.Preview @@ -36,8 +36,8 @@ import androidx.compose.ui.tooling.preview.Preview
36 import androidx.compose.ui.unit.dp 36 import androidx.compose.ui.unit.dp
37 import androidx.core.content.ContextCompat 37 import androidx.core.content.ContextCompat
38 import com.google.accompanist.pager.ExperimentalPagerApi 38 import com.google.accompanist.pager.ExperimentalPagerApi
39 -import io.livekit.android.composesample.ui.theme.AppTheme  
40 import io.livekit.android.sample.common.R 39 import io.livekit.android.sample.common.R
  40 +import io.livekit.android.videoencodedecode.ui.theme.AppTheme
41 41
42 @ExperimentalPagerApi 42 @ExperimentalPagerApi
43 class MainActivity : ComponentActivity() { 43 class MainActivity : ComponentActivity() {
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.
@@ -31,13 +31,13 @@ import androidx.compose.ui.res.painterResource @@ -31,13 +31,13 @@ import androidx.compose.ui.res.painterResource
31 import androidx.compose.ui.unit.dp 31 import androidx.compose.ui.unit.dp
32 import androidx.constraintlayout.compose.ConstraintLayout 32 import androidx.constraintlayout.compose.ConstraintLayout
33 import androidx.constraintlayout.compose.Dimension 33 import androidx.constraintlayout.compose.Dimension
34 -import io.livekit.android.composesample.ui.theme.BlueMain  
35 -import io.livekit.android.composesample.ui.theme.NoVideoBackground  
36 import io.livekit.android.room.Room 34 import io.livekit.android.room.Room
37 import io.livekit.android.room.participant.ConnectionQuality 35 import io.livekit.android.room.participant.ConnectionQuality
38 import io.livekit.android.room.participant.Participant 36 import io.livekit.android.room.participant.Participant
39 import io.livekit.android.sample.common.R 37 import io.livekit.android.sample.common.R
40 import io.livekit.android.util.flow 38 import io.livekit.android.util.flow
  39 +import io.livekit.android.videoencodedecode.ui.theme.BlueMain
  40 +import io.livekit.android.videoencodedecode.ui.theme.NoVideoBackground
41 41
42 /** 42 /**
43 * Widget for displaying a participant. 43 * Widget for displaying a participant.
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.
@@ -21,7 +21,14 @@ import androidx.compose.foundation.layout.fillMaxSize @@ -21,7 +21,14 @@ import androidx.compose.foundation.layout.fillMaxSize
21 import androidx.compose.foundation.layout.size 21 import androidx.compose.foundation.layout.size
22 import androidx.compose.material.Icon 22 import androidx.compose.material.Icon
23 import androidx.compose.material.Surface 23 import androidx.compose.material.Surface
24 -import androidx.compose.runtime.* 24 +import androidx.compose.runtime.Composable
  25 +import androidx.compose.runtime.DisposableEffect
  26 +import androidx.compose.runtime.collectAsState
  27 +import androidx.compose.runtime.currentCompositeKeyHash
  28 +import androidx.compose.runtime.getValue
  29 +import androidx.compose.runtime.mutableStateOf
  30 +import androidx.compose.runtime.remember
  31 +import androidx.compose.runtime.setValue
25 import androidx.compose.ui.Alignment 32 import androidx.compose.ui.Alignment
26 import androidx.compose.ui.Modifier 33 import androidx.compose.ui.Modifier
27 import androidx.compose.ui.graphics.Color 34 import androidx.compose.ui.graphics.Color
@@ -39,7 +46,6 @@ import io.livekit.android.room.track.Track @@ -39,7 +46,6 @@ import io.livekit.android.room.track.Track
39 import io.livekit.android.room.track.VideoTrack 46 import io.livekit.android.room.track.VideoTrack
40 import io.livekit.android.util.flow 47 import io.livekit.android.util.flow
41 import io.livekit.android.videoencodedecode.ui.ComposeVisibility 48 import io.livekit.android.videoencodedecode.ui.ComposeVisibility
42 -import kotlinx.coroutines.flow.*  
43 49
44 /** 50 /**
45 * Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use 51 * Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use
1 -package io.livekit.android.composesample.ui 1 +/*
  2 + * Copyright 2025 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.videoencodedecode.ui
2 18
3 import androidx.compose.foundation.background 19 import androidx.compose.foundation.background
4 import androidx.compose.foundation.layout.* 20 import androidx.compose.foundation.layout.*
1 -package io.livekit.android.composesample.ui.theme 1 +/*
  2 + * Copyright 2025 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.videoencodedecode.ui.theme
2 18
3 import androidx.compose.ui.graphics.Color 19 import androidx.compose.ui.graphics.Color
4 20
1 -package io.livekit.android.composesample.ui.theme 1 +/*
  2 + * Copyright 2025 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.videoencodedecode.ui.theme
2 18
3 import androidx.compose.foundation.shape.RoundedCornerShape 19 import androidx.compose.foundation.shape.RoundedCornerShape
4 import androidx.compose.material.Shapes 20 import androidx.compose.material.Shapes
1 -package io.livekit.android.composesample.ui.theme 1 +/*
  2 + * Copyright 2025 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.videoencodedecode.ui.theme
2 18
3 import androidx.compose.material.MaterialTheme 19 import androidx.compose.material.MaterialTheme
4 import androidx.compose.material.darkColors 20 import androidx.compose.material.darkColors
1 -package io.livekit.android.composesample.ui.theme 1 +/*
  2 + * Copyright 2025 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.videoencodedecode.ui.theme
2 18
3 import androidx.compose.material.Typography 19 import androidx.compose.material.Typography
4 import androidx.compose.ui.text.TextStyle 20 import androidx.compose.ui.text.TextStyle