Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement missing APIs for adopting WebRTC KMP to LiveKit client SDK #96

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f85a512
Add ContinualGatheringPolicy enum
shepeliev Sep 16, 2023
0c87e32
Make RtcConfiguration fields readable
shepeliev Sep 17, 2023
ef91270
Implement VideoTrack.shouldReceive
shepeliev Sep 17, 2023
b680446
Implement PeerConnection.getStats(RtpSender)
shepeliev Sep 17, 2023
dc96e2f
Make MediaStreamTrackImpl public
shepeliev Sep 17, 2023
f2fa80b
Remove suspend at RtpSender.replaceTrack
shepeliev Sep 17, 2023
a3137df
Implement AudioStreamTrack.addSink
shepeliev Sep 17, 2023
b3e9d0e
Implement PeerConnection.addTransceiver
shepeliev Sep 18, 2023
a8f58aa
Implement getCapabilities()
shepeliev Sep 18, 2023
8ff3d55
Make RtpEncodingParameters.active writable
shepeliev Sep 18, 2023
86d0e57
Upgrade WebRTC SDK: iOS 114.5735.08, Android 114.5735.05
shepeliev Sep 21, 2023
049b4d7
Add LocalAudioStreamTrack.addRenderer(), RemoteAudioStreamTrack.addRe…
shepeliev Sep 21, 2023
a2f3876
Fix lint errors
shepeliev Sep 21, 2023
59486a5
Add highpassFilter and typingNoiseDetection constraints
shepeliev Sep 24, 2023
87d9961
Catch IllegalStateException on accessing MediaStreamTrack state
shepeliev Sep 24, 2023
0f447b2
Add constructor to RtpEncodingParameters
shepeliev Sep 27, 2023
5f24e76
Rename (Audio|Video)StreamTrack to (Audio|Video)Track
shepeliev Sep 29, 2023
b783943
Add UUID util
shepeliev Sep 29, 2023
7f2b9c3
Refactor MediaStream
shepeliev Sep 29, 2023
372e8d0
Implement screen capturing in Android
shepeliev Sep 29, 2023
8156d52
Fix adding transceiver
shepeliev Sep 29, 2023
8aa0522
Refactor MediaStreamTrackImpl
shepeliev Sep 30, 2023
b48039b
Refactor PeerConnectionFactory builder initialization
shepeliev Sep 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions kotlin-js-store/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3997,6 +3997,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=

[email protected]:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==

uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
Expand Down
2 changes: 1 addition & 1 deletion libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ androidxCompose = "1.4.3"
decompose = "1.0.0-alpha-01"

[libraries]
webrtcSdk = "io.github.webrtc-sdk:android:114.5735.02"
webrtcSdk = "io.github.webrtc-sdk:android:114.5735.05"

kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" }
kotlin-coroutinesPlayServices = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinCoroutines" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.shepeliev.webrtckmp.VideoStreamTrack
import com.shepeliev.webrtckmp.VideoTrack
import com.shepeliev.webrtckmp.WebRtc
import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer
import org.webrtc.VideoSink

@Composable
fun Video(
track: VideoStreamTrack,
track: VideoTrack,
modifier: Modifier = Modifier,
scalingTypeMatchOrientation: RendererCommon.ScalingType = RendererCommon.ScalingType.SCALE_ASPECT_BALANCED,
scalingTypeMismatchOrientation: RendererCommon.ScalingType = RendererCommon.ScalingType.SCALE_ASPECT_FIT,
Expand Down Expand Up @@ -70,12 +70,12 @@ fun Video(
)
}

private fun VideoStreamTrack.addSinkCatching(sink: VideoSink) {
private fun VideoTrack.addSinkCatching(sink: VideoSink) {
// runCatching as track may be disposed while activity was in pause mode
runCatching { addSink(sink) }
}

private fun VideoStreamTrack.removeSinkCatching(sink: VideoSink) {
private fun VideoTrack.removeSinkCatching(sink: VideoSink) {
// runCatching as track may be disposed while activity was in pause mode
runCatching { removeSink(sink) }
}
2 changes: 1 addition & 1 deletion sample/app-ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
platform :ios, '11.0'
platform :ios, '12.0'

target 'app-ios' do
use_frameworks!
Expand Down
10 changes: 5 additions & 5 deletions sample/app-ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,8 @@ PODS:
- shared (1.0.0):
- FirebaseCore
- FirebaseFirestore
- WebRTC-SDK (= 114.5735.02)
- WebRTC-SDK (114.5735.02)
- WebRTC-SDK (= 114.5735.08)
- WebRTC-SDK (114.5735.08)

DEPENDENCIES:
- shared (from `../shared`)
Expand Down Expand Up @@ -766,9 +766,9 @@ SPEC CHECKSUMS:
leveldb-library: f03246171cce0484482ec291f88b6d563699ee06
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
shared: cf27933a6f2b84a6642d82be3a8ab8c890a0be05
WebRTC-SDK: dd913fd31cfbf1d43b9a22d83f4c6354c960c623
shared: 755975bad54ce02d3ddacfdcbcc061bafb7a6184
WebRTC-SDK: c24d2a6c9f571f2ed42297cb8ffba9557093142b

PODFILE CHECKSUM: 0e3d5ba6f75052d9af66e13da5a081f724211aa3
PODFILE CHECKSUM: d04804a6b3942d4583d0f591670e8b2f30717f4f

COCOAPODS: 1.12.1
5 changes: 3 additions & 2 deletions sample/app-web/src/main/kotlin/App.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.arkivanov.decompose.value.Value
import com.shepeliev.webrtckmp.native
import com.shepeliev.webrtckmp.sample.shared.Room
import csstype.px
import emotion.react.css
Expand Down Expand Up @@ -65,12 +66,12 @@ val App = FC<AppProps> { props ->

val localVideoRef = useRef<HTMLVideoElement>(null)
useEffect(roomModel, localVideoRef) {
localVideoRef.current?.srcObject = roomModel.localStream?.js
localVideoRef.current?.srcObject = roomModel.localStream?.native
}

val remoteVideoRef = useRef<HTMLVideoElement>(null)
useEffect {
remoteVideoRef.current?.srcObject = roomModel.remoteStream?.js
remoteVideoRef.current?.srcObject = roomModel.remoteStream?.native
}

div {
Expand Down
4 changes: 2 additions & 2 deletions sample/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ kotlin {
version = "1.0.0"
summary = "Shared framework for WebRTC KMP sample"
homepage = "https://github.com/shepeliev/webrtc-kmp/tree/main/sample"
ios.deploymentTarget = "11.0"
ios.deploymentTarget = "12.0"

pod("FirebaseCore")
pod("FirebaseFirestore")
pod("WebRTC-SDK") {
version = "114.5735.02"
version = "114.5735.08"
linkOnly = true
}

Expand Down
4 changes: 2 additions & 2 deletions sample/shared/shared.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Pod::Spec.new do |spec|
spec.summary = 'Shared framework for WebRTC KMP sample'
spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '11.0'
spec.ios.deployment_target = '12.0'
spec.dependency 'FirebaseCore'
spec.dependency 'FirebaseFirestore'
spec.dependency 'WebRTC-SDK', '114.5735.02'
spec.dependency 'WebRTC-SDK', '114.5735.08'

spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':sample:shared',
Expand Down
5 changes: 3 additions & 2 deletions webrtc-kmp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ kotlin {
version = webRtcKmpVersion
summary = "WebRTC Kotlin Multiplatform SDK"
homepage = "https://github.com/shepeliev/webrtc-kmp"
ios.deploymentTarget = "10.0"
ios.deploymentTarget = "12.0"

pod("WebRTC-SDK") {
version = "114.5735.02"
version = "114.5735.08"
moduleName = "WebRTC"
packageName = "WebRTC"
}
Expand Down Expand Up @@ -57,6 +57,7 @@ dependencies {
androidTestImplementation(deps.androidx.test.core)
androidTestImplementation(deps.androidx.test.runner)
jsMainImplementation(npm("webrtc-adapter", "8.1.1"))
jsMainImplementation(npm("uuid", "9.0.0"))
}

publishing {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.shepeliev.webrtckmp

import org.webrtc.AudioTrackSink

actual interface AudioTrack : MediaStreamTrack {
fun addSink(sink: AudioTrackSink)
fun removeSink(sink: AudioTrackSink)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,76 +11,17 @@ import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

internal class CameraVideoCaptureController(
private val constraints: MediaTrackConstraints,
videoSource: VideoSource,
) : VideoCaptureController(videoSource) {
private val enumerator = WebRtc.cameraEnumerator
constraints: MediaTrackConstraints,
) : VideoCaptureController(videoSource, CameraCapturerHelper.buildMediaTrackSettings(constraints)) {
private var pendingDeviceId: String? = null

override fun createVideoCapturer(): VideoCapturer {
selectDevice()
return enumerator.createCapturer(
checkNotNull(settings.deviceId),
CameraEventsHandler()
)
}

private fun selectDevice() {
val isFrontFacing = constraints.facingMode?.value != FacingMode.Environment

val searchCriteria: (String) -> Boolean = if (constraints.deviceId != null) {
{ it == constraints.deviceId }
} else {
{ enumerator.isFrontFacing(it) == isFrontFacing }
}

val deviceId = enumerator.deviceNames.firstOrNull(searchCriteria)
?: throw CameraVideoCapturerException.notFound(constraints)

settings = settings.copy(
deviceId = deviceId,

facingMode = (
if (isFrontFacing) {
FacingMode.User
} else {
FacingMode.Environment
}
)
)
}

override fun selectVideoSize(): Size {
val requestedWidth = constraints.width?.value ?: DEFAULT_VIDEO_WIDTH
val requestedHeight = constraints.height?.value ?: DEFAULT_VIDEO_HEIGHT
val formats = enumerator.getSupportedFormats(checkNotNull(settings.deviceId))
val sizes = formats?.map { Size(it.width, it.height) } ?: emptyList()
if (sizes.isEmpty()) throw CameraVideoCapturerException.notFound(constraints)

return CameraEnumerationAndroid.getClosestSupportedSize(
sizes,
requestedWidth,
requestedHeight
)
}

override fun selectFps(): Int {
val requestedFps = constraints.frameRate?.value ?: DEFAULT_FRAME_RATE
val formats = enumerator.getSupportedFormats(checkNotNull(settings.deviceId))
val frameRates = formats?.map { it.framerate } ?: emptyList()
if (frameRates.isEmpty()) throw CameraVideoCapturerException.notFound(constraints)

val requestedFpsInt = requestedFps.toInt()
val range = CameraEnumerationAndroid.getClosestSupportedFramerateRange(
frameRates,
requestedFpsInt
)

return requestedFpsInt.coerceIn(range.min / 1000, range.max / 1000)
return WebRtc.cameraEnumerator.createCapturer(checkNotNull(settings.deviceId), CameraEventsHandler())
}

suspend fun switchCamera() {
val deviceNames = enumerator.deviceNames
val deviceNames = WebRtc.cameraEnumerator.deviceNames
if (deviceNames.size < 2) {
throw CameraVideoCapturerException("Can't switch camera. No other camera available.")
}
Expand Down Expand Up @@ -125,15 +66,15 @@ internal class CameraVideoCaptureController(

private inner class CameraEventsHandler : CameraVideoCapturer.CameraEventsHandler {
override fun onCameraError(errorDescription: String) {
videoCapturerErrorListener.onError("Error: $errorDescription")
videoCapturerStopListener.onStop("Error: $errorDescription")
}

override fun onCameraDisconnected() {
videoCapturerErrorListener.onError("Camera disconnected")
videoCapturerStopListener.onStop("Camera disconnected")
}

override fun onCameraFreezed(errorDescription: String) {
videoCapturerErrorListener.onError("Camera freezed: $errorDescription")
videoCapturerStopListener.onStop("Camera freezed: $errorDescription")
}

override fun onCameraOpening(cameraId: String) {
Expand All @@ -149,3 +90,59 @@ internal class CameraVideoCaptureController(
}
}
}

private object CameraCapturerHelper {
fun buildMediaTrackSettings(constraints: MediaTrackConstraints): MediaTrackSettings {
val deviceId = selectDevice(constraints)
val facingMode = getFacingMode(deviceId)
val size = selectVideoSize(constraints, deviceId)
val frameRate = selectFps(constraints, deviceId)

return MediaTrackSettings(
width = size.width,
height = size.height,
deviceId = deviceId,
facingMode = facingMode,
frameRate = frameRate.toDouble()
)
}

private fun selectDevice(constraints: MediaTrackConstraints): String {
val isFrontFacing = constraints.facingMode?.value != FacingMode.Environment

val searchCriteria: (String) -> Boolean = if (constraints.deviceId != null) {
{ it == constraints.deviceId }
} else {
{ WebRtc.cameraEnumerator.isFrontFacing(it) == isFrontFacing }
}

return WebRtc.cameraEnumerator.deviceNames.firstOrNull(searchCriteria)
?: throw CameraVideoCapturerException.notFound(constraints)
}

private fun getFacingMode(deviceId: String): FacingMode {
return if (WebRtc.cameraEnumerator.isFrontFacing(deviceId)) FacingMode.User else FacingMode.Environment
}

private fun selectVideoSize(constraints: MediaTrackConstraints, deviceId: String): Size {
val requestedWidth = constraints.width?.value ?: DEFAULT_VIDEO_WIDTH
val requestedHeight = constraints.height?.value ?: DEFAULT_VIDEO_HEIGHT
val formats = WebRtc.cameraEnumerator.getSupportedFormats(deviceId)
val sizes = formats?.map { Size(it.width, it.height) } ?: emptyList()
if (sizes.isEmpty()) throw CameraVideoCapturerException.notFound(constraints)

return CameraEnumerationAndroid.getClosestSupportedSize(sizes, requestedWidth, requestedHeight)
}

private fun selectFps(constraints: MediaTrackConstraints, deviceId: String): Int {
val requestedFps = constraints.frameRate?.value ?: DEFAULT_FRAME_RATE
val formats = WebRtc.cameraEnumerator.getSupportedFormats(deviceId)
val frameRates = formats?.map { it.framerate } ?: emptyList()
if (frameRates.isEmpty()) throw CameraVideoCapturerException.notFound(constraints)

val requestedFpsInt = requestedFps.toInt()
val range = CameraEnumerationAndroid.getClosestSupportedFramerateRange(frameRates, requestedFpsInt)

return requestedFpsInt.coerceIn(range.min / 1000, range.max / 1000)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.shepeliev.webrtckmp

import org.webrtc.AudioSource
import org.webrtc.AudioTrackSink
import org.webrtc.AudioTrack as AndroidAudioTrack

internal class LocalAudioTrack(
native: AndroidAudioTrack,
private val audioSource: AudioSource,
override val constraints: MediaTrackConstraints,
) : MediaStreamTrackImpl(native), AudioTrack {

override fun addSink(sink: AudioTrackSink) {
(native as AndroidAudioTrack).addSink(sink)
}

override fun removeSink(sink: AudioTrackSink) {
(native as AndroidAudioTrack).removeSink(sink)
}

override fun onStop() {
audioSource.dispose()
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package com.shepeliev.webrtckmp

import org.webrtc.VideoTrack
import org.webrtc.VideoTrack as AndroidVideoTrack

internal class LocalVideoStreamTrack(
android: VideoTrack,
internal class LocalVideoTrack(
native: AndroidVideoTrack,
private val videoCaptureController: VideoCaptureController,
) : RenderedVideoStreamTrack(android), VideoStreamTrack {
) : RenderedVideoTrack(native), VideoTrack {
override val settings: MediaTrackSettings get() = videoCaptureController.settings

override var shouldReceive: Boolean?
get() = (native as AndroidVideoTrack).shouldReceive()
set(value) { (native as AndroidVideoTrack).setShouldReceive(checkNotNull(value)) }

init {
videoCaptureController.videoCapturerErrorListener = VideoCapturerErrorListener { stop() }
videoCaptureController.startCapture()
videoCaptureController.videoCapturerStopListener = VideoCapturerStopListener { stop() }
if (native.enabled()) {
videoCaptureController.startCapture()
}
}

override suspend fun switchCamera(deviceId: String?) {
Expand Down
Loading