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

Add WasmJS target #114

Merged
merged 16 commits into from
May 9, 2024
113 changes: 76 additions & 37 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,84 @@
name: Pull Request
name: Pull Request Checks

on:
pull_request:
branches: [ main ]

jobs:
build:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
cache: gradle

- name: Gradle cache
uses: gradle/gradle-build-action@v2

- name: Run Kotlin linter
run: ./gradlew ktlintCheck

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
cache: gradle

- name: Gradle cache
uses: gradle/gradle-build-action@v2

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run Android instrumented tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
target: google_apis
arch: x86_64
profile: Nexus 6
script: ./gradlew webrtc-kmp:connectedAndroidTest

- name: Upload Android test artifact
uses: actions/upload-artifact@v2
if: failure()
with:
name: "Android Instrumented Tests Report HTML"
path: "webrtc-kmp/build/reports/androidTests/connected"

- name: Run JS tests
run: ./gradlew cleanTest kotlinUpgradeYarnLock webrtc-kmp:jsTest

- name: Upload JS test artifact
uses: actions/upload-artifact@v2
if: failure()
with:
name: "JS Tests Report HTML"
path: "webrtc-kmp/build/reports/tests/js*Test"

- name: Run WasmJS tests
run: ./gradlew cleanTest kotlinUpgradeYarnLock webrtc-kmp:wasmJsTest

- name: Upload WasmJS test artifact
uses: actions/upload-artifact@v2
if: failure()
with:
name: "JS Tests Report HTML"
path: "webrtc-kmp/build/reports/tests/wasmJs*Test"

test-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
Expand All @@ -30,46 +103,12 @@ jobs:
- name: Gradle cache
uses: gradle/gradle-build-action@v2

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run Kotlin linter
run: ./gradlew ktlintCheck

# TODO: Android instrumented tests don't work

# - name: Run Android instrumented tests
# uses: reactivecircus/android-emulator-runner@v2
# with:
# api-level: 29
# target: google_apis
# arch: x86_64
# profile: Nexus 6
# script: ./gradlew webrtc-kmp:connectedAndroidTest
#
# - name: Upload Android test artifact
# uses: actions/upload-artifact@v2
# if: failure()
# with:
# name: "Android Instrumented Tests Report HTML"
# path: "webrtc-kmp/build/reports/androidTests/connected"

- name: Run iOS tests
run: ./gradlew cleanTest webrtc-kmp:iosX64Test
run: ./gradlew cleanTest webrtc-kmp:iosSimulatorArm64Test

- name: Upload iOS test artifact
uses: actions/upload-artifact@v2
if: failure()
with:
name: "iOS Tests Report HTML"
path: "webrtc-kmp/build/reports/tests/iosX64Test"

- name: Run JS tests
run: ./gradlew cleanTest kotlinUpgradeYarnLock webrtc-kmp:jsTest

- name: Upload JS test artifact
uses: actions/upload-artifact@v2
if: failure()
with:
name: "JS Tests Report HTML"
path: "webrtc-kmp/build/reports/tests/jsTest"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ It supports Android, iOS, JS. Other platforms - PRs are welcome.


## API implementation map
API | Android | iOS | JS
API | Android | iOS | JS/WasmJS
:-: |:------------------:| :-: | :---:
Audio/Video | :white_check_mark: | :white_check_mark: | :white_check_mark:
Data channel | :white_check_mark: | :white_check_mark: | :white_check_mark:
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ kotlin.mpp.enableCInteropCommonization=true

#Compose
org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.wasm.enabled=true

# Versions
webRtcKmpVersion=0.114.4
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ compose-plugin = "1.6.2"
webrtc-sdk = { module = "io.github.webrtc-sdk:android", version.ref = "webrtc-android-sdk" }
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlin-coroutines" }
kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines" }
androidx-coreKtx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
Expand Down
10 changes: 8 additions & 2 deletions sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ Run Android emulator or connect real device.

Open `sample/iosApp/iosApp.xcodeproj` in XCode build and run

### Web
### Web JS

```bash
./gradlew jsBrowserRun
./gradlew sample:composeApp:jsBrowserRun
```

### Web WasmJS

```bash
./gradlew sample:composeApp:wasmJsBrowserRun
```
21 changes: 21 additions & 0 deletions sample/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ kotlin {
}
}

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
moduleName = "composeApp"
browser {
commonWebpackConfig {
outputFileName = "composeApp.js"
devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
static = (static ?: mutableListOf()).apply {
// Serve sources to debug inside browser
add(project.projectDir.path)
}
}
}
}
binaries.executable()
}

sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
Expand Down Expand Up @@ -111,6 +128,10 @@ android {
}
}

compose.experimental {
web.application {}
}

fun KotlinNativeTarget.configureWebRtcCinterops() {
val webRtcFrameworkPath = file("$buildDir/cocoapods/synthetic/IOS/Pods/WebRTC-SDK")
.resolveArchPath(konanTarget, "WebRTC")
Expand Down
11 changes: 6 additions & 5 deletions sample/composeApp/src/androidMain/kotlin/Video.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,29 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.shepeliev.webrtckmp.AudioStreamTrack
import com.shepeliev.webrtckmp.VideoStreamTrack
import com.shepeliev.webrtckmp.WebRtc
import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer
import org.webrtc.VideoSink

@Composable
actual fun Video(track: VideoStreamTrack, modifier: Modifier) {
actual fun Video(videoTrack: VideoStreamTrack, modifier: Modifier, audioTrack: AudioStreamTrack?) {
var renderer by remember { mutableStateOf<SurfaceViewRenderer?>(null) }

val lifecycleEventObserver = remember(renderer, track) {
val lifecycleEventObserver = remember(renderer, videoTrack) {
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
renderer?.also {
it.init(WebRtc.rootEglBase.eglBaseContext, null)
track.addSinkCatching(it)
videoTrack.addSinkCatching(it)
}
}

Lifecycle.Event.ON_PAUSE -> {
renderer?.also { track.removeSinkCatching(it) }
renderer?.also { videoTrack.removeSinkCatching(it) }
renderer?.release()
}

Expand All @@ -46,7 +47,7 @@ actual fun Video(track: VideoStreamTrack, modifier: Modifier) {
lifecycle.addObserver(lifecycleEventObserver)

onDispose {
renderer?.let { track.removeSinkCatching(it) }
renderer?.let { videoTrack.removeSinkCatching(it) }
renderer?.release()
lifecycle.removeObserver(lifecycleEventObserver)
}
Expand Down
24 changes: 19 additions & 5 deletions sample/composeApp/src/commonMain/kotlin/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import co.touchlab.kermit.Logger
import co.touchlab.kermit.platformLogWriter
import com.shepeliev.webrtckmp.AudioStreamTrack
import com.shepeliev.webrtckmp.MediaDevices
import com.shepeliev.webrtckmp.MediaStream
import com.shepeliev.webrtckmp.PeerConnection
Expand All @@ -35,22 +36,29 @@ fun App() {
val scope = rememberCoroutineScope()
val (localStream, setLocalStream) = remember { mutableStateOf<MediaStream?>(null) }
val (remoteVideoTrack, setRemoteVideoTrack) = remember { mutableStateOf<VideoStreamTrack?>(null) }
val (remoteAudioTrack, setRemoteAudioTrack) = remember { mutableStateOf<AudioStreamTrack?>(null) }
val (peerConnections, setPeerConnections) = remember {
mutableStateOf<Pair<PeerConnection, PeerConnection>?>(null)
}

LaunchedEffect(localStream, peerConnections) {
if (peerConnections == null || localStream == null) return@LaunchedEffect
makeCall(peerConnections, localStream, setRemoteVideoTrack)
makeCall(peerConnections, localStream, setRemoteVideoTrack, setRemoteAudioTrack)
}

Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
val localVideoTrack = localStream?.videoTracks?.firstOrNull()

localVideoTrack?.let { Video(track = it, modifier = Modifier.weight(1f)) }
localVideoTrack?.let { Video(videoTrack = it, modifier = Modifier.weight(1f)) }
?: Box(modifier = Modifier.weight(1f))

remoteVideoTrack?.let { Video(track = it, modifier = Modifier.weight(1f)) }
remoteVideoTrack?.let {
Video(
videoTrack = it,
audioTrack = remoteAudioTrack,
modifier = Modifier.weight(1f),
)
}
?: Box(modifier = Modifier.weight(1f))

Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Expand All @@ -67,9 +75,12 @@ fun App() {

StopButton(
onClick = {
hangup(peerConnections, setPeerConnections, setRemoteVideoTrack)
hangup(peerConnections)
localStream.release()
setLocalStream(null)
setPeerConnections(null)
setRemoteVideoTrack(null)
setRemoteAudioTrack(null)
}
)

Expand All @@ -85,7 +96,10 @@ fun App() {
)
} else {
HangupButton(onClick = {
hangup(peerConnections, setPeerConnections, setRemoteVideoTrack)
hangup(peerConnections)
setPeerConnections(null)
setRemoteVideoTrack(null)
setRemoteAudioTrack(null)
})
}
}
Expand Down
9 changes: 1 addition & 8 deletions sample/composeApp/src/commonMain/kotlin/Hangup.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import com.shepeliev.webrtckmp.PeerConnection
import com.shepeliev.webrtckmp.VideoStreamTrack

fun hangup(
peerConnections: Pair<PeerConnection, PeerConnection>?,
setPeerConnections: (Pair<PeerConnection, PeerConnection>?) -> Unit,
setRemoteVideoTrack: (VideoStreamTrack?) -> Unit
) {
fun hangup(peerConnections: Pair<PeerConnection, PeerConnection>?) {
val (pc1, pc2) = peerConnections ?: return
pc1.getTransceivers().forEach { pc1.removeTrack(it.sender) }
pc1.close()
pc2.close()
setPeerConnections(null)
setRemoteVideoTrack(null)
}
18 changes: 14 additions & 4 deletions sample/composeApp/src/commonMain/kotlin/MakeCall.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import co.touchlab.kermit.Logger
import com.shepeliev.webrtckmp.AudioStreamTrack
import com.shepeliev.webrtckmp.IceCandidate
import com.shepeliev.webrtckmp.MediaStream
import com.shepeliev.webrtckmp.MediaStreamTrackKind
Expand All @@ -13,14 +14,16 @@ import com.shepeliev.webrtckmp.onSignalingStateChange
import com.shepeliev.webrtckmp.onTrack
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach

suspend fun makeCall(
peerConnections: Pair<PeerConnection, PeerConnection>,
localStream: MediaStream,
setRemoteVideoTrack: (VideoStreamTrack?) -> Unit
onRemoteVideoTrack: (VideoStreamTrack) -> Unit,
onRemoteAudioTrack: (AudioStreamTrack) -> Unit = {},
): Nothing = coroutineScope {
val (pc1, pc2) = peerConnections
localStream.tracks.forEach { pc1.addTrack(it) }
Expand Down Expand Up @@ -81,8 +84,15 @@ suspend fun makeCall(
.launchIn(this)
pc2.onTrack
.onEach { Logger.d { "PC2 onTrack: ${it.track?.kind}" } }
.filter { it.track?.kind == MediaStreamTrackKind.Video }
.onEach { setRemoteVideoTrack(it.track as VideoStreamTrack) }
.map { it.track }
.filterNotNull()
.onEach {
if (it.kind == MediaStreamTrackKind.Audio) {
onRemoteAudioTrack(it as AudioStreamTrack)
} else if (it.kind == MediaStreamTrackKind.Video) {
onRemoteVideoTrack(it as VideoStreamTrack)
}
}
.launchIn(this)
val offer = pc1.createOffer(OfferAnswerOptions(offerToReceiveVideo = true, offerToReceiveAudio = true))
pc1.setLocalDescription(offer)
Expand Down
3 changes: 2 additions & 1 deletion sample/composeApp/src/commonMain/kotlin/Video.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.shepeliev.webrtckmp.AudioStreamTrack
import com.shepeliev.webrtckmp.VideoStreamTrack

@Composable
expect fun Video(track: VideoStreamTrack, modifier: Modifier = Modifier)
expect fun Video(videoTrack: VideoStreamTrack, modifier: Modifier = Modifier, audioTrack: AudioStreamTrack? = null)
Loading
Loading