davidliu
Committed by GitHub

Fix VirtualBackgroundVideoProcessor not responding to changing backgroundImage (#752)

* Fix VirtualBackgroundVideoProcessor not responding to changes in backgroundImage

* lint fixes

* spotless
正在显示 43 个修改的文件 包含 335 行增加174 行删除
---
"client-sdk-android": patch
---
Fix VirtualBackgroundVideoProcessor not responding to changes in backgroundImage
... ...
... ... @@ -45,6 +45,17 @@ class MainActivity : AppCompatActivity() {
enableButton.setText(if (state) "Disable" else "Enable")
}
val enableBackgroundButton = findViewById<Button>(R.id.buttonBackground)
enableBackgroundButton.setOnClickListener {
val state = viewModel.toggleVirtualBackground()
enableBackgroundButton.setText(if (state) "Disable Background" else "Enable Background")
}
val flipCameraButton = findViewById<Button>(R.id.buttonFlip)
flipCameraButton.setOnClickListener {
viewModel.flipCamera()
}
val renderer = findViewById<TextureViewRenderer>(R.id.renderer)
viewModel.room.initVideoRenderer(renderer)
viewModel.track.observe(this) { track ->
... ... @@ -87,7 +98,7 @@ fun ComponentActivity.requestNeededPermissions(onPermissionsGranted: (() -> Unit
}
}
val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
val neededPermissions = listOf(Manifest.permission.CAMERA)
.filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED }
.toTypedArray()
... ...
... ... @@ -54,10 +54,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
eglBase = eglBase,
),
)
private val virtualBackground = (AppCompatResources.getDrawable(application, R.drawable.background) as BitmapDrawable).bitmap
private var blur = 16f
private val processor = VirtualBackgroundVideoProcessor(eglBase, Dispatchers.IO, initialBlurRadius = blur).apply {
val drawable = AppCompatResources.getDrawable(application, R.drawable.background) as BitmapDrawable
backgroundImage = drawable.bitmap
backgroundImage = virtualBackground
}
private var cameraProvider: CameraCapturerUtils.CameraProvider? = null
... ... @@ -119,4 +121,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
blur += 5
processor.updateBlurRadius(blur)
}
fun toggleVirtualBackground(): Boolean {
if (processor.backgroundImage != virtualBackground) {
processor.backgroundImage = virtualBackground
return true
} else {
processor.backgroundImage = null
return false
}
}
fun flipCamera() {
val videoTrack = track.value ?: return
val newPosition = when (videoTrack.options.position) {
CameraPosition.FRONT -> CameraPosition.BACK
CameraPosition.BACK -> CameraPosition.FRONT
else -> CameraPosition.FRONT
}
videoTrack.switchCamera(position = newPosition)
}
}
... ...
... ... @@ -10,12 +10,32 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/button"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Disable" />
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Disable" />
<Button
android:id="@+id/buttonBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Disable Background" />
<Button
android:id="@+id/buttonFlip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Flip Camera" />
</LinearLayout>
<Button
android:id="@+id/buttonIncreaseBlur"
... ...
... ... @@ -26,7 +26,6 @@ import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_O
import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
import android.hardware.camera2.CaptureRequest
import android.os.Build
import android.os.Build.VERSION
import android.os.Handler
import android.util.Range
import android.util.Size
... ... @@ -345,7 +344,7 @@ internal constructor(
if (id == deviceId) return CameraDeviceId(id, null)
// Then check if deviceId is a physical camera ID in a logical camera
if (VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val characteristic = cameraManager.getCameraCharacteristics(id)
for (physicalId in characteristic.physicalCameraIds) {
... ...
... ... @@ -23,7 +23,12 @@ import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import com.twilio.audioswitch.*
import com.twilio.audioswitch.AbstractAudioSwitch
import com.twilio.audioswitch.AudioDevice
import com.twilio.audioswitch.AudioDeviceChangeListener
import com.twilio.audioswitch.AudioSwitch
import com.twilio.audioswitch.LegacyAudioSwitch
import io.livekit.android.room.Room
import io.livekit.android.util.LKLog
import javax.inject.Inject
import javax.inject.Singleton
... ...
... ... @@ -234,7 +234,6 @@ constructor(
AudioFormat.ENCODING_PCM_8BIT -> 1
AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2
AudioFormat.ENCODING_PCM_FLOAT -> 4
AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat")
else -> throw IllegalArgumentException("Bad audio format $audioFormat")
}
}
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -58,7 +58,7 @@ constructor(
this.peerConnectionFactory = peerConnectionFactory
}
public fun keyProvider(): KeyProvider {
fun keyProvider(): KeyProvider {
return this.keyProvider
}
... ... @@ -70,16 +70,16 @@ constructor(
this.enabled = true
this.room = room
this.emitEvent = emitEvent
this.room?.localParticipant?.trackPublications?.forEach() { item ->
this.room?.localParticipant?.trackPublications?.forEach { item ->
var participant = this.room!!.localParticipant
var publication = item.value
if (publication.track != null) {
addPublishedTrack(publication.track!!, publication, participant, room)
}
}
this.room?.remoteParticipants?.forEach() { item ->
this.room?.remoteParticipants?.forEach { item ->
var participant = item.value
participant.trackPublications.forEach() { item ->
participant.trackPublications.forEach { item ->
var publication = item.value
if (publication.track != null) {
addSubscribedTrack(publication.track!!, publication, participant, room)
... ... @@ -88,7 +88,7 @@ constructor(
}
}
public fun addSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
fun addSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
var rtpReceiver: RtpReceiver? = when (publication.track!!) {
is RemoteAudioTrack -> (publication.track!! as RemoteAudioTrack).receiver
is RemoteVideoTrack -> (publication.track!! as RemoteVideoTrack).receiver
... ... @@ -111,7 +111,7 @@ constructor(
}
}
public fun removeSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
fun removeSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
var trackId = publication.sid
var participantId = participant.identity
var frameCryptor = frameCryptors.get(trackId to participantId)
... ... @@ -122,7 +122,7 @@ constructor(
}
}
public fun addPublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
fun addPublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
var rtpSender: RtpSender? = when (publication.track!!) {
is LocalAudioTrack -> (publication.track!! as LocalAudioTrack)?.sender
is LocalVideoTrack -> (publication.track!! as LocalVideoTrack)?.sender
... ... @@ -146,7 +146,7 @@ constructor(
}
}
public fun removePublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
fun removePublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
var trackId = publication.sid
var participantId = participant.identity
var frameCryptor = frameCryptors.get(trackId to participantId)
... ... @@ -202,7 +202,7 @@ constructor(
* Enable or disable E2EE
* @param enabled
*/
public fun enableE2EE(enabled: Boolean) {
fun enableE2EE(enabled: Boolean) {
this.enabled = enabled
for (item in frameCryptors.entries) {
var frameCryptor = item.value
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -25,9 +25,8 @@ internal const val defaultFailureTolerance = -1
internal const val defaultKeyRingSize = 16
internal const val defaultDiscardFrameWhenCryptorNotReady = false
class E2EEOptions
constructor(
keyProvider: KeyProvider = BaseKeyProvider(
class E2EEOptions(
var keyProvider: KeyProvider = BaseKeyProvider(
defaultRatchetSalt,
defaultMagicBytes,
defaultRatchetWindowSize,
... ... @@ -38,11 +37,9 @@ constructor(
),
encryptionType: Encryption.Type = Encryption.Type.GCM,
) {
var keyProvider: KeyProvider
var encryptionType: Encryption.Type = Encryption.Type.NONE
init {
this.keyProvider = keyProvider
this.encryptionType = encryptionType
}
}
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -20,14 +20,13 @@ import io.livekit.android.util.LKLog
import livekit.org.webrtc.FrameCryptorFactory
import livekit.org.webrtc.FrameCryptorKeyProvider
class KeyInfo
constructor(var participantId: String, var keyIndex: Int, var key: String) {
class KeyInfo(var participantId: String, var keyIndex: Int, var key: String) {
override fun toString(): String {
return "KeyInfo(participantId='$participantId', keyIndex=$keyIndex)"
}
}
public interface KeyProvider {
interface KeyProvider {
fun setSharedKey(key: String, keyIndex: Int? = 0): Boolean
fun ratchetSharedKey(keyIndex: Int? = 0): ByteArray
fun exportSharedKey(keyIndex: Int? = 0): ByteArray
... ... @@ -41,17 +40,30 @@ public interface KeyProvider {
var enableSharedKey: Boolean
}
class BaseKeyProvider
constructor(
private var ratchetSalt: String = defaultRatchetSalt,
private var uncryptedMagicBytes: String = defaultMagicBytes,
private var ratchetWindowSize: Int = defaultRatchetWindowSize,
class BaseKeyProvider(
ratchetSalt: String = defaultRatchetSalt,
uncryptedMagicBytes: String = defaultMagicBytes,
ratchetWindowSize: Int = defaultRatchetWindowSize,
override var enableSharedKey: Boolean = true,
private var failureTolerance: Int = defaultFailureTolerance,
private var keyRingSize: Int = defaultKeyRingSize,
private var discardFrameWhenCryptorNotReady: Boolean = defaultDiscardFrameWhenCryptorNotReady,
failureTolerance: Int = defaultFailureTolerance,
keyRingSize: Int = defaultKeyRingSize,
discardFrameWhenCryptorNotReady: Boolean = defaultDiscardFrameWhenCryptorNotReady,
) : KeyProvider {
override val rtcKeyProvider: FrameCryptorKeyProvider
private var keys: MutableMap<String, MutableMap<Int, String>> = mutableMapOf()
init {
this.rtcKeyProvider = FrameCryptorFactory.createFrameCryptorKeyProvider(
enableSharedKey,
ratchetSalt.toByteArray(),
ratchetWindowSize,
uncryptedMagicBytes.toByteArray(),
failureTolerance,
keyRingSize,
discardFrameWhenCryptorNotReady,
)
}
override fun setSharedKey(key: String, keyIndex: Int?): Boolean {
return rtcKeyProvider.setSharedKey(keyIndex ?: 0, key.toByteArray())
}
... ... @@ -100,18 +112,4 @@ constructor(
override fun setSifTrailer(trailer: ByteArray) {
rtcKeyProvider.setSifTrailer(trailer)
}
override val rtcKeyProvider: FrameCryptorKeyProvider
init {
this.rtcKeyProvider = FrameCryptorFactory.createFrameCryptorKeyProvider(
enableSharedKey,
ratchetSalt.toByteArray(),
ratchetWindowSize,
uncryptedMagicBytes.toByteArray(),
failureTolerance,
keyRingSize,
discardFrameWhenCryptorNotReady,
)
}
}
... ...
... ... @@ -22,8 +22,18 @@ import android.view.TextureView
import android.view.View
import io.livekit.android.room.track.video.ViewVisibility
import io.livekit.android.util.LKLog
import livekit.org.webrtc.*
import livekit.org.webrtc.RendererCommon.*
import livekit.org.webrtc.EglBase
import livekit.org.webrtc.EglRenderer
import livekit.org.webrtc.GlRectDrawer
import livekit.org.webrtc.Logging
import livekit.org.webrtc.RendererCommon.GlDrawer
import livekit.org.webrtc.RendererCommon.RendererEvents
import livekit.org.webrtc.RendererCommon.ScalingType
import livekit.org.webrtc.RendererCommon.VideoLayoutMeasure
import livekit.org.webrtc.SurfaceEglRenderer
import livekit.org.webrtc.ThreadUtils
import livekit.org.webrtc.VideoFrame
import livekit.org.webrtc.VideoSink
import java.util.concurrent.CountDownLatch
/**
... ... @@ -119,7 +129,7 @@ open class TextureViewRenderer :
* It should be lightweight and must not call removeFrameListener.
* @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
* required.
* @param drawer Custom drawer to use for this frame listener.
* @param drawerParam Custom drawer to use for this frame listener.
*/
fun addFrameListener(
listener: EglRenderer.FrameListener?,
... ...
... ... @@ -578,7 +578,7 @@ internal constructor(
}
LKLog.v { "ws reconnected, restarting ICE" }
listener?.onSignalConnected(!isFullReconnect)
listener?.onSignalConnected(true)
// trigger publisher reconnect
// only restart publisher if it's needed
... ... @@ -833,11 +833,9 @@ internal constructor(
val rtcConfig = connectOptions.rtcConfig?.copy()?.apply {
val mergedServers = iceServers.toMutableList()
if (connectOptions.iceServers != null) {
connectOptions.iceServers.forEach { server ->
if (!mergedServers.contains(server)) {
mergedServers.add(server)
}
connectOptions.iceServers?.forEach { server ->
if (!mergedServers.contains(server)) {
mergedServers.add(server)
}
}
... ... @@ -1088,9 +1086,7 @@ internal constructor(
abortPendingPublishTracks()
if (leave.hasRegions()) {
regionUrlProvider?.let {
it.setServerReportedRegions(RegionSettings.fromProto(leave.regions))
}
regionUrlProvider?.setServerReportedRegions(RegionSettings.fromProto(leave.regions))
}
when {
... ... @@ -1242,8 +1238,7 @@ internal constructor(
}
}
val dataChannelInfos = LivekitModels.DataPacket.Kind.values()
.toList()
val dataChannelInfos = LivekitModels.DataPacket.Kind.entries
.filterNot { it == LivekitModels.DataPacket.Kind.UNRECOGNIZED }
.mapNotNull { kind -> dataChannelForKind(kind) }
.map { dataChannel ->
... ...
/*
* Copyright 2024 LiveKit, Inc.
* Copyright 2024-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -87,9 +87,13 @@ constructor(
if (!response.isSuccessful) {
throw RoomException.ConnectException("Could not fetch region settings: ${response.code} ${response.message}")
}
return@use response.body?.string() ?: return null
return@use response.body?.string()
}
if (bodyString == null) {
throw RoomException.ConnectException("Could not fetch region settings: empty response body!")
}
return json.decodeFromString<RegionSettings>(bodyString).also {
regionSettings = it
lastUpdateAt = SystemClock.elapsedRealtime()
... ...
... ... @@ -319,7 +319,7 @@ internal constructor(
* @param screenCaptureParams When enabling the screenshare, this must be provided with
* [ScreenCaptureParams.mediaProjectionPermissionResultData] containing resultData returned from launching
* [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()).
* @throws IllegalArgumentException if attempting to enable screenshare without [mediaProjectionPermissionResultData]
* @throws IllegalArgumentException if attempting to enable screenshare without [screenCaptureParams]
* @see Room.screenShareTrackCaptureDefaults
* @see Room.screenShareTrackPublishDefaults
* @see ScreenAudioCapturer
... ...
... ... @@ -429,9 +429,7 @@ open class Participant(
other as Participant
if (sid != other.sid) return false
return true
return sid == other.sid
}
override fun hashCode(): Int {
... ...
... ... @@ -38,7 +38,6 @@ import io.livekit.android.room.util.EncodingUtils
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.LKLog
import io.livekit.android.util.flowDelegate
import livekit.LivekitModels
import livekit.LivekitRtc
import livekit.LivekitRtc.SubscribedCodec
import livekit.org.webrtc.CameraVideoCapturer
... ... @@ -357,7 +356,7 @@ constructor(
val rid = EncodingUtils.ridForVideoQuality(quality.quality) ?: continue
val encoding = encodings.firstOrNull { it.rid == rid }
// use low quality layer settings for non-simulcasted streams
?: encodings.takeIf { it.size == 1 && quality.quality == LivekitModels.VideoQuality.LOW }?.first()
?: encodings.takeIf { it.size == 1 && quality.quality == ProtoVideoQuality.LOW }?.first()
?: continue
if (encoding.active != quality.enabled) {
hasChanged = true
... ...
... ... @@ -229,5 +229,5 @@ sealed class TrackException(message: String? = null, cause: Throwable? = null) :
TrackException(message, cause)
}
public const val KIND_AUDIO = "audio"
public const val KIND_VIDEO = "video"
const val KIND_AUDIO = "audio"
const val KIND_VIDEO = "video"
... ...
/*
* Copyright 2023 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -32,7 +32,7 @@ import kotlin.coroutines.resume
* Handles connecting to a [ScreenCaptureService].
*/
internal class ScreenCaptureConnection(private val context: Context) {
public var isBound = false
var isBound = false
private set
private var service: ScreenCaptureService? = null
private val queuedConnects = mutableSetOf<Continuation<Unit>>()
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -20,7 +20,6 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build
... ... @@ -69,7 +68,7 @@ open class ScreenCaptureService : Service() {
"Screen Capture",
NotificationManager.IMPORTANCE_LOW,
)
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val service = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(channel)
}
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -23,7 +23,6 @@ import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.CallSuper
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.video.ViewVisibility.Notifier
import java.util.Observable
/**
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -26,8 +26,6 @@ import kotlin.coroutines.resume
/**
* Returns an RTCStatsReport with all the relevant information pertaining to a track.
*
* @param trackIdentifier track, sender, or receiver id
*/
fun RTCStatsReport.getFilteredStats(track: MediaStreamTrack): RTCStatsReport {
return getFilteredStats(track.id())
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -256,7 +256,7 @@ class MockPeerConnection(
if (currentOrdinal < newOrdinal) {
// Ensure that we move through each state.
for (ordinal in ((currentOrdinal + 1)..newOrdinal)) {
iceConnectionState = IceConnectionState.values()[ordinal]
iceConnectionState = IceConnectionState.entries[ordinal]
}
} else {
iceConnectionState = newState
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -48,7 +48,7 @@ class MockWebSocket(
isClosed = true
listener.onClosing(this, code, reason ?: "")
listener.onClosed(this, code, reason ?: "")
return willClose
return true
}
override fun queueSize(): Long = 0
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -20,7 +20,7 @@ import com.google.protobuf.ByteString
import livekit.LivekitRtc
import okio.ByteString.Companion.toByteString
fun com.google.protobuf.ByteString.toOkioByteString() = toByteArray().toByteString()
fun ByteString.toOkioByteString() = toByteArray().toByteString()
fun okio.ByteString.toPBByteString() = ByteString.copyFrom(toByteArray())
... ...
/*
* Copyright 2024 LiveKit, Inc.
* Copyright 2024-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -97,8 +97,7 @@ class MixerAudioBufferCallbackTest {
AudioFormat.ENCODING_PCM_FLOAT -> {
byteBuffer.asFloatBuffer().put(0, INCREMENT.toFloat())
}
AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat")
else -> throw IllegalArgumentException("Bad audio format $audioFormat")
}
return BufferResponse(byteBuffer)
... ... @@ -111,7 +110,6 @@ private fun getBytesPerSample(audioFormat: Int): Int {
AudioFormat.ENCODING_PCM_8BIT -> 1
AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2
AudioFormat.ENCODING_PCM_FLOAT -> 4
AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat")
else -> throw IllegalArgumentException("Bad audio format $audioFormat")
}
}
... ...
... ... @@ -58,10 +58,10 @@ class RoomMockE2ETest : MockE2ETest() {
val collector = FlowCollector(room::state.flow, coroutineRule.scope)
connect()
val events = collector.stopCollecting()
Assert.assertEquals(3, events.size)
Assert.assertEquals(Room.State.DISCONNECTED, events[0])
Assert.assertEquals(Room.State.CONNECTING, events[1])
Assert.assertEquals(Room.State.CONNECTED, events[2])
assertEquals(3, events.size)
assertEquals(Room.State.DISCONNECTED, events[0])
assertEquals(Room.State.CONNECTING, events[1])
assertEquals(Room.State.CONNECTED, events[2])
}
@Test
... ... @@ -155,9 +155,9 @@ class RoomMockE2ETest : MockE2ETest() {
)
val events = eventCollector.stopCollecting()
Assert.assertEquals(ConnectionQuality.EXCELLENT, room.localParticipant.connectionQuality)
Assert.assertEquals(1, events.size)
Assert.assertEquals(true, events[0] is RoomEvent.ConnectionQualityChanged)
assertEquals(ConnectionQuality.EXCELLENT, room.localParticipant.connectionQuality)
assertEquals(1, events.size)
assertEquals(true, events[0] is RoomEvent.ConnectionQualityChanged)
}
@Test
... ... @@ -252,8 +252,8 @@ class RoomMockE2ETest : MockE2ETest() {
)
val events = eventCollector.stopCollecting()
Assert.assertEquals(1, events.size)
Assert.assertEquals(true, events[0] is RoomEvent.ActiveSpeakersChanged)
assertEquals(1, events.size)
assertEquals(true, events[0] is RoomEvent.ActiveSpeakersChanged)
}
@Test
... ... @@ -286,11 +286,11 @@ class RoomMockE2ETest : MockE2ETest() {
)
val events = eventCollector.stopCollecting()
Assert.assertEquals(1, events.size)
Assert.assertEquals(true, events[0] is RoomEvent.TrackStreamStateChanged)
assertEquals(1, events.size)
assertEquals(true, events[0] is RoomEvent.TrackStreamStateChanged)
val event = events[0] as RoomEvent.TrackStreamStateChanged
Assert.assertEquals(Track.StreamState.ACTIVE, event.streamState)
assertEquals(Track.StreamState.ACTIVE, event.streamState)
}
@Test
... ... @@ -320,13 +320,13 @@ class RoomMockE2ETest : MockE2ETest() {
)
val events = eventCollector.stopCollecting()
Assert.assertEquals(1, events.size)
Assert.assertEquals(true, events[0] is RoomEvent.TrackSubscriptionPermissionChanged)
assertEquals(1, events.size)
assertEquals(true, events[0] is RoomEvent.TrackSubscriptionPermissionChanged)
val event = events[0] as RoomEvent.TrackSubscriptionPermissionChanged
Assert.assertEquals(TestData.REMOTE_PARTICIPANT.sid, event.participant.sid.value)
Assert.assertEquals(TestData.REMOTE_AUDIO_TRACK.sid, event.trackPublication.sid)
Assert.assertEquals(false, event.subscriptionAllowed)
assertEquals(TestData.REMOTE_PARTICIPANT.sid, event.participant.sid.value)
assertEquals(TestData.REMOTE_AUDIO_TRACK.sid, event.trackPublication.sid)
assertEquals(false, event.subscriptionAllowed)
}
@Test
... ... @@ -495,6 +495,6 @@ class RoomMockE2ETest : MockE2ETest() {
connect()
room.disconnect()
connect()
Assert.assertEquals(room.state, Room.State.CONNECTED)
assertEquals(room.state, Room.State.CONNECTED)
}
}
... ...
... ... @@ -25,7 +25,6 @@ import io.livekit.android.test.util.toPBByteString
import kotlinx.coroutines.ExperimentalCoroutinesApi
import livekit.LivekitRtc
import livekit.org.webrtc.PeerConnection
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
... ... @@ -76,7 +75,7 @@ class RoomReconnectionMockE2ETest : MockE2ETest() {
return@any sentRequest.hasSyncState()
}
Assert.assertTrue(sentSyncState)
assertTrue(sentSyncState)
}
@Test
... ... @@ -146,6 +145,6 @@ class RoomReconnectionMockE2ETest : MockE2ETest() {
}
println(sentRequests)
Assert.assertTrue(sentAddTrack)
assertTrue(sentAddTrack)
}
}
... ...
... ... @@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.livekit.android.room.participant
package io.livekit.android.room.rpc
import com.google.protobuf.ByteString
import io.livekit.android.room.RTCEngine
import io.livekit.android.room.rpc.RpcManager
import io.livekit.android.room.participant.Participant
import io.livekit.android.rpc.RpcError
import io.livekit.android.test.MockE2ETest
import io.livekit.android.test.mock.MockDataChannel
... ...
... ... @@ -140,7 +140,7 @@ class VirtualBackgroundTransformer(
if (backgroundImage != null) {
val bgTextureFrameBuffer = bgTextureFrameBuffers.first
if (backgroundImageNeedsUploading || true) {
if (backgroundImageNeedsUploading) {
val byteBuffer = ByteBuffer.allocateDirect(backgroundImage.byteCount)
backgroundImage.copyPixelsToBuffer(byteBuffer)
byteBuffer.rewind()
... ...
... ... @@ -42,6 +42,7 @@ import livekit.org.webrtc.SurfaceTextureHelper
import livekit.org.webrtc.VideoFrame
import livekit.org.webrtc.VideoSink
import java.util.concurrent.Semaphore
import kotlin.math.roundToInt
/**
* A virtual background video processor for the local camera video stream.
... ... @@ -90,6 +91,9 @@ class VirtualBackgroundVideoProcessor(
*/
var enabled: Boolean = true
/**
* A virtual background image to use.
*/
var backgroundImage: Bitmap? = null
set(value) {
field = value
... ... @@ -191,39 +195,42 @@ class VirtualBackgroundVideoProcessor(
frame.retain()
surfaceTextureHelper.handler.post {
val backgroundImage = this.backgroundImage
if (backgroundImageNeedsUpdating && backgroundImage != null) {
val imageAspect = backgroundImage.width / backgroundImage.height.toFloat()
val targetAspect = frame.rotatedWidth / frame.rotatedHeight.toFloat()
var sx = 0
var sy = 0
var sWidth = backgroundImage.width
var sHeight = backgroundImage.height
if (imageAspect > targetAspect) {
sWidth = Math.round(backgroundImage.height * targetAspect)
sx = Math.round((backgroundImage.width - sWidth) / 2f)
if (backgroundImageNeedsUpdating) {
if (backgroundImage == null) {
backgroundTransformer.backgroundImage = null
backgroundImageNeedsUpdating = false
} else {
sHeight = Math.round(backgroundImage.width / targetAspect)
sy = Math.round((backgroundImage.height - sHeight) / 2f)
val imageAspect = backgroundImage.width / backgroundImage.height.toFloat()
val targetAspect = frame.rotatedWidth / frame.rotatedHeight.toFloat()
var sx = 0
var sy = 0
var sWidth = backgroundImage.width
var sHeight = backgroundImage.height
if (imageAspect > targetAspect) {
sWidth = (backgroundImage.height * targetAspect).roundToInt()
sx = ((backgroundImage.width - sWidth) / 2f).roundToInt()
} else {
sHeight = (backgroundImage.width / targetAspect).roundToInt()
sy = ((backgroundImage.height - sHeight) / 2f).roundToInt()
}
val matrix = Matrix()
matrix.postRotate(-frame.rotation.toFloat())
val resizedImage = Bitmap.createBitmap(
backgroundImage,
sx,
sy,
sWidth,
sHeight,
matrix,
true,
)
backgroundTransformer.backgroundImage = resizedImage
backgroundImageNeedsUpdating = false
}
val diffAspect = targetAspect / imageAspect
val matrix = Matrix()
matrix.postRotate(-frame.rotation.toFloat())
val resizedImage = Bitmap.createBitmap(
backgroundImage,
sx,
sy,
sWidth,
sHeight,
matrix,
true,
)
backgroundTransformer.backgroundImage = resizedImage
backgroundImageNeedsUpdating = false
}
lastMask?.let {
... ...
... ... @@ -61,7 +61,7 @@ public class LKGlTextureFrameBuffer {
textureId = GlUtil.generateTexture(GLES30.GL_TEXTURE_2D);
}
if (frameBufferId == 0) {
final int frameBuffers[] = new int[1];
final int[] frameBuffers = new int[1];
GLES30.glGenFramebuffers(1, frameBuffers, 0);
frameBufferId = frameBuffers[0];
}
... ...
/*
* Copyright 2024 LiveKit, Inc.
* Copyright 2024-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -19,7 +19,6 @@ package io.livekit.android.sample.service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
... ... @@ -61,7 +60,7 @@ open class ForegroundService : Service() {
"Foreground",
NotificationManager.IMPORTANCE_LOW
)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
... ...
... ... @@ -16,7 +16,6 @@
package io.livekit.android.composesample
import android.app.Activity
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.os.Parcelable
... ... @@ -100,7 +99,7 @@ class CallActivity : AppCompatActivity() {
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode != Activity.RESULT_OK || data == null) {
if (resultCode != RESULT_OK || data == null) {
return@registerForActivityResult
}
viewModel.startScreenCapture(data)
... ...
... ... @@ -16,7 +16,6 @@
package io.livekit.android.sample
import android.app.Activity
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.os.Parcelable
... ... @@ -63,7 +62,7 @@ class CallActivity : AppCompatActivity() {
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode != Activity.RESULT_OK || data == null) {
if (resultCode != RESULT_OK || data == null) {
return@registerForActivityResult
}
viewModel.startScreenCapture(data)
... ...
/*
* Copyright 2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.videoencodedecode
import android.os.Bundle
... ... @@ -18,7 +34,7 @@ import androidx.constraintlayout.compose.Dimension
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import io.livekit.android.composesample.ui.theme.AppTheme
import io.livekit.android.videoencodedecode.ui.theme.AppTheme
import kotlinx.parcelize.Parcelize
class CallActivity : AppCompatActivity() {
... ...
/*
* Copyright 2023 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -36,8 +36,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.accompanist.pager.ExperimentalPagerApi
import io.livekit.android.composesample.ui.theme.AppTheme
import io.livekit.android.sample.common.R
import io.livekit.android.videoencodedecode.ui.theme.AppTheme
@ExperimentalPagerApi
class MainActivity : ComponentActivity() {
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -31,13 +31,13 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import io.livekit.android.composesample.ui.theme.BlueMain
import io.livekit.android.composesample.ui.theme.NoVideoBackground
import io.livekit.android.room.Room
import io.livekit.android.room.participant.ConnectionQuality
import io.livekit.android.room.participant.Participant
import io.livekit.android.sample.common.R
import io.livekit.android.util.flow
import io.livekit.android.videoencodedecode.ui.theme.BlueMain
import io.livekit.android.videoencodedecode.ui.theme.NoVideoBackground
/**
* Widget for displaying a participant.
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -21,7 +21,14 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
... ... @@ -39,7 +46,6 @@ import io.livekit.android.room.track.Track
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.util.flow
import io.livekit.android.videoencodedecode.ui.ComposeVisibility
import kotlinx.coroutines.flow.*
/**
* Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use
... ...
package io.livekit.android.composesample.ui
/*
* Copyright 2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.videoencodedecode.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
... ...
package io.livekit.android.composesample.ui.theme
/*
* Copyright 2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.videoencodedecode.ui.theme
import androidx.compose.ui.graphics.Color
... ...
package io.livekit.android.composesample.ui.theme
/*
* Copyright 2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.videoencodedecode.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
... ...
package io.livekit.android.composesample.ui.theme
/*
* Copyright 2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.videoencodedecode.ui.theme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
... ...
package io.livekit.android.composesample.ui.theme
/*
* Copyright 2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.videoencodedecode.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
... ...