davidliu
Committed by GitHub

Custom Audio input (#528)

* Custom audio handling

* Screen share audio example

* Screen Audio Capturer in sdk

* spotless

* Spotless

* fix extraneous logging

* changeset

* Update webrtc-sdk to 125.6422.06.1

* Test compile fixes
正在显示 44 个修改的文件 包含 1262 行增加4 行删除
---
"client-sdk-android": minor
---
Implement custom audio mixing into audio track
... ...
---
"client-sdk-android": minor
---
Update to webrtc-sdk 125.6422.06.1
... ...
---
"client-sdk-android": minor
---
Implement screen share audio capturer
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
<bytecodeTargetLevel target="1.8">
<module name="livekit-android.examples.screenshareaudio" target="17" />
</bytecodeTargetLevel>
</component>
</project>
\ No newline at end of file
... ...
/build
\ No newline at end of file
... ...
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'io.livekit.android.example.screenshareaudio'
compileSdk 34
defaultConfig {
applicationId "io.livekit.android.example.screenshareaudio"
minSdk 29
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_compiler_version
}
packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
// If building the sample app outside the context of this repo, replace the following with:
// api "io.livekit:livekit-android:<version>"
api project(":livekit-android-sdk")
api project(":sample-app-common")
implementation libs.androidx.core.ktx
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.activity.compose
implementation platform(libs.compose.bom)
implementation libs.androidx.ui
implementation libs.androidx.ui.graphics
implementation libs.androidx.ui.tooling.preview
implementation libs.androidx.material3
implementation libs.timber
testImplementation libs.junit
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.espresso
androidTestImplementation platform(libs.compose.bom)
androidTestImplementation libs.androidx.ui.test.junit4
debugImplementation libs.androidx.ui.tooling
debugImplementation libs.androidx.ui.test.manifest
}
... ...
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
... ...
/*
* Copyright 2024 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.example.screenshareaudio
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.livekit.android.example.screenshareaudio", appContext.packageName)
}
}
... ...
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Livekitandroid"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Livekitandroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
... ...
/*
* Copyright 2024 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.example.screenshareaudio
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import io.livekit.android.LiveKit
import io.livekit.android.example.screenshareaudio.ui.theme.LivekitandroidTheme
import io.livekit.android.util.LoggingLevel
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels<MainViewModel>()
private val screenCaptureIntentLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode != RESULT_OK || data == null) {
return@registerForActivityResult
}
viewModel.startScreenCapture(data)
}
private fun requestMediaProjection() {
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
screenCaptureIntentLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LiveKit.loggingLevel = LoggingLevel.INFO
viewModel
setContent {
LivekitandroidTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
var enableScreenCapture by remember {
mutableStateOf(true)
}
Button(
onClick = {
if (enableScreenCapture) {
requestMediaProjection()
} else {
viewModel.stopScreenCapture()
}
enableScreenCapture = !enableScreenCapture
},
) {
val text = if (enableScreenCapture) {
"enable"
} else {
"disable"
}
Text(text = text)
}
}
}
}
}
}
... ...
/*
* Copyright 2024 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.example.screenshareaudio
import android.Manifest
import android.app.Application
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.livekit.android.LiveKit
import io.livekit.android.audio.ScreenAudioCapturer
import io.livekit.android.room.track.LocalAudioTrack
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.sample.service.ForegroundService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
val url = "wss://example.com"
val token = ""
@RequiresApi(Build.VERSION_CODES.Q)
class MainViewModel(application: Application) : AndroidViewModel(application) {
val room = LiveKit.create(application)
var audioCapturer: ScreenAudioCapturer? = null
init {
viewModelScope.launch(Dispatchers.IO) {
room.connect(url, token)
}
// Start a foreground service to keep the call from being interrupted if the
// app goes into the background.
val foregroundServiceIntent = Intent(application, ForegroundService::class.java)
application.startForegroundService(foregroundServiceIntent)
}
fun startScreenCapture(data: Intent) {
viewModelScope.launch(Dispatchers.IO) {
if (ActivityCompat.checkSelfPermission(getApplication(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
return@launch
}
room.localParticipant.setScreenShareEnabled(true, data)
room.localParticipant.setMicrophoneEnabled(true)
val screenCaptureTrack = room.localParticipant.getTrackPublication(Track.Source.SCREEN_SHARE)?.track as? LocalVideoTrack ?: return@launch
val audioTrack = room.localParticipant.getTrackPublication(Track.Source.MICROPHONE)?.track as? LocalAudioTrack ?: return@launch
audioCapturer = ScreenAudioCapturer.createFromScreenShareTrack(screenCaptureTrack) ?: return@launch
audioCapturer?.gain = 0.1f // Lower the volume so that mic can still be heard clearly.
audioTrack.setAudioBufferCallback(audioCapturer!!)
}
}
fun stopScreenCapture() {
viewModelScope.launch(Dispatchers.IO) {
(room.localParticipant.getTrackPublication(Track.Source.MICROPHONE)?.track as? LocalAudioTrack)
?.setAudioBufferCallback(null)
room.localParticipant.setMicrophoneEnabled(false)
room.localParticipant.setScreenShareEnabled(false)
audioCapturer?.releaseAudioResources()
}
}
}
... ...
/*
* Copyright 2024 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.example.screenshareaudio.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
... ...
/*
* Copyright 2024 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.example.screenshareaudio.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun LivekitandroidTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
... ...
/*
* Copyright 2024 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.example.screenshareaudio.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
... ...
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
... ...
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
... ...
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
... ...
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
... ...
<resources>
<string name="app_name">ScreenShareAudio</string>
</resources>
\ No newline at end of file
... ...
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Livekitandroid" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
... ...
[versions]
webrtc = "125.6422.05"
webrtc = "125.6422.06.1"
androidJainSipRi = "1.3.0-91"
androidx-activity = "1.9.0"
... ... @@ -96,6 +96,13 @@ robolectric = { module = "org.robolectric:robolectric", version = "4.11.1" }
turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
... ...
/*
* Copyright 2024 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.audio
import android.media.AudioFormat
import java.nio.ByteBuffer
/**
* @suppress
*/
class AudioBufferCallbackDispatcher : livekit.org.webrtc.audio.JavaAudioDeviceModule.AudioBufferCallback {
var bufferCallback: AudioBufferCallback? = null
override fun onBuffer(buffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): Long {
return bufferCallback?.onBuffer(
buffer = buffer,
audioFormat = audioFormat,
channelCount = channelCount,
sampleRate = sampleRate,
bytesRead = bytesRead,
captureTimeNs = captureTimeNs,
) ?: 0L
}
}
interface AudioBufferCallback {
/**
* Called when new audio samples are ready.
* @param buffer the buffer of audio bytes. Changes to this buffer will be published on the audio track.
* @param audioFormat the audio encoding. See [AudioFormat.ENCODING_PCM_8BIT],
* [AudioFormat.ENCODING_PCM_16BIT], and [AudioFormat.ENCODING_PCM_FLOAT]. Note
* that [AudioFormat.ENCODING_DEFAULT] defaults to PCM-16bit.
* @param channelCount
* @param sampleRate
* @param bytesRead the byte count originally read from the microphone.
* @param captureTimeNs the capture timestamp of the original audio data in nanoseconds.
* @return the capture timestamp in nanoseconds. Return 0 if not available.
*/
fun onBuffer(buffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): Long
}
... ...
/*
* Copyright 2024 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.audio
import android.media.AudioFormat
import io.livekit.android.util.LKLog
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.ShortBuffer
import kotlin.math.min
/**
* A convenience class that handles mixing the microphone data and custom audio data.
*/
abstract class MixerAudioBufferCallback : AudioBufferCallback {
class BufferResponse(
/**
* The byteBuffer to mix into the audio track.
*/
val byteBuffer: ByteBuffer? = null,
/**
* The capture time stamp in nanoseconds, or null if not available.
*/
val captureTimeNs: Long? = null,
)
final override fun onBuffer(buffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): Long {
val response = onBufferRequest(buffer, audioFormat, channelCount, sampleRate, bytesRead, captureTimeNs)
val customAudioBuffer = response?.byteBuffer
if (customAudioBuffer != null) {
buffer.order(ByteOrder.nativeOrder()).position(0)
customAudioBuffer.order(ByteOrder.nativeOrder()).position(0)
when (audioFormat) {
AudioFormat.ENCODING_PCM_8BIT -> {
mixByteBuffers(original = buffer, customAudioBuffer)
}
AudioFormat.ENCODING_PCM_16BIT,
AudioFormat.ENCODING_DEFAULT,
-> {
mixShortBuffers(original = buffer.asShortBuffer(), customAudioBuffer.asShortBuffer())
}
AudioFormat.ENCODING_PCM_FLOAT -> {
mixFloatBuffers(original = buffer.asFloatBuffer(), customAudioBuffer.asFloatBuffer())
}
else -> {
LKLog.w { "Unsupported audio format: $audioFormat" }
}
}
}
val mixedCaptureTime = if (captureTimeNs != 0L) {
captureTimeNs
} else {
response?.captureTimeNs ?: 0L
}
return mixedCaptureTime
}
abstract fun onBufferRequest(originalBuffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): BufferResponse?
private fun mixByteBuffers(
original: ByteBuffer,
addBuffer: ByteBuffer,
) {
val size = min(original.capacity(), addBuffer.capacity())
if (size <= 0) return
for (i in 0 until size) {
val sum = (original[i].toInt() + addBuffer[i].toInt())
.coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt())
original.put(i, sum.toByte())
}
}
private fun mixShortBuffers(
original: ShortBuffer,
addBuffer: ShortBuffer,
) {
val size = min(original.capacity(), addBuffer.capacity())
if (size <= 0) return
for (i in 0 until size) {
val sum = (original[i].toInt() + addBuffer[i].toInt())
.coerceIn(
minimumValue = Short.MIN_VALUE.toInt(),
maximumValue = Short.MAX_VALUE.toInt(),
)
original.put(i, sum.toShort())
}
}
private fun mixFloatBuffers(
original: FloatBuffer,
addBuffer: FloatBuffer,
) {
val size = min(original.capacity(), addBuffer.capacity())
if (size <= 0) return
for (i in 0 until size) {
val sum = (original[i] + addBuffer[i])
.coerceIn(-1f, 1f)
original.put(i, sum)
}
}
}
... ...
/*
* Copyright 2024 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.audio
import android.Manifest
import android.annotation.SuppressLint
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioPlaybackCaptureConfiguration
import android.media.AudioRecord
import android.media.projection.MediaProjection
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.util.LKLog
import livekit.org.webrtc.ScreenCapturerAndroid
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.ShortBuffer
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
private const val BUFFER_SIZE_FACTOR = 2
private const val MIN_GAIN_CHANGE = 0.01f
private const val DEFAULT_GAIN = 1f
private val DEFAULT_CONFIGURATOR: AudioPlaybackCaptureConfigurator = { builder ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
builder.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
builder.addMatchingUsage(AudioAttributes.USAGE_GAME)
}
}
/**
* A mixer for capturing screen share audio.
*
* Requires a media projection, which can be obtained from the screen share track.
*
* Additionally, for screen capture to work properly while your app is in the
* background, a foreground service with the type `microphone` must be running.
* Otherwise, audio capture will not return any audio data.
*
* Example usage:
* ```
*
* ```
*/
@RequiresApi(Build.VERSION_CODES.Q)
class ScreenAudioCapturer
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
constructor(
private val mediaProjection: MediaProjection,
/**
* Screen share audio capture requires the use of [AudioPlaybackCaptureConfiguration].
* This parameter allows customizing the configuration used. Note that
* the configuration must have at least one match rule applied to it or
* an exception will be thrown.
*
* The default configurator adds matching rules against all available usage
* types that can be captured.
*/
private val captureConfigurator: AudioPlaybackCaptureConfigurator = DEFAULT_CONFIGURATOR,
) : MixerAudioBufferCallback() {
private var audioRecord: AudioRecord? = null
private var hasInitialized = false
private var byteBuffer: ByteBuffer? = null
/**
* A multiplier to adjust the volume of the captured audio data.
*
* Values above 1 will increase the volume, values less than 1 will decrease it.
*/
var gain = DEFAULT_GAIN
override fun onBufferRequest(originalBuffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): BufferResponse? {
if (!hasInitialized && audioRecord == null) {
hasInitialized = true
initAudioRecord(audioFormat = audioFormat, channelCount = channelCount, sampleRate = sampleRate)
}
val audioRecord = this.audioRecord ?: return null
val recordBuffer = this.byteBuffer ?: return null
audioRecord.read(recordBuffer, recordBuffer.capacity())
if (abs(gain - DEFAULT_GAIN) > MIN_GAIN_CHANGE) {
recordBuffer.position(0)
when (audioFormat) {
AudioFormat.ENCODING_PCM_8BIT -> {
adjustByteBuffer(recordBuffer, gain)
}
AudioFormat.ENCODING_PCM_16BIT,
AudioFormat.ENCODING_DEFAULT,
-> {
adjustShortBuffer(recordBuffer.asShortBuffer(), gain)
}
AudioFormat.ENCODING_PCM_FLOAT -> {
adjustFloatBuffer(recordBuffer.asFloatBuffer(), gain)
}
else -> {
LKLog.w { "Unsupported audio format: $audioFormat" }
}
}
}
return BufferResponse(recordBuffer)
}
@SuppressLint("MissingPermission")
fun initAudioRecord(audioFormat: Int, channelCount: Int, sampleRate: Int): Boolean {
val audioCaptureConfig = AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
.apply(captureConfigurator)
.build()
val channelMask = if (channelCount == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO
val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelMask, audioFormat)
if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
throw IllegalStateException("minBuffer size error: $minBufferSize")
}
LKLog.v { "AudioRecord.getMinBufferSize: $minBufferSize" }
val bytesPerFrame = channelCount * getBytesPerSample(audioFormat)
val framesPerBuffer = sampleRate / 100
val readBufferCapacity = bytesPerFrame * framesPerBuffer
val byteBuffer = ByteBuffer.allocateDirect(readBufferCapacity)
.order(ByteOrder.nativeOrder())
if (!byteBuffer.hasArray()) {
LKLog.e { "ByteBuffer does not have backing array." }
return false
}
this.byteBuffer = byteBuffer
val bufferSizeInBytes: Int = max(BUFFER_SIZE_FACTOR * minBufferSize, readBufferCapacity)
val audioRecord = AudioRecord.Builder()
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(audioFormat)
.setSampleRate(sampleRate)
.setChannelMask(channelMask)
.build(),
)
.setBufferSizeInBytes(bufferSizeInBytes)
.setAudioPlaybackCaptureConfig(audioCaptureConfig)
.build()
try {
audioRecord.startRecording()
} catch (e: Exception) {
LKLog.e(e) { "AudioRecord.startRecording failed:" }
audioRecord.release()
return false
}
if (audioRecord.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
LKLog.e {
"AudioRecord.startRecording failed - incorrect state: ${audioRecord.recordingState}"
}
return false
}
this.audioRecord = audioRecord
return true
}
/**
* Release any audio resources associated with this capturer.
* This is not managed by LiveKit, so you must call this function
* when finished to prevent memory leaks.
*/
fun releaseAudioResources() {
val audioRecord = this.audioRecord
if (audioRecord != null) {
audioRecord.release()
this.audioRecord = null
}
}
private fun getBytesPerSample(audioFormat: Int): Int {
return when (audioFormat) {
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")
}
}
companion object {
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
fun createFromScreenShareTrack(track: Track?): ScreenAudioCapturer? {
val screenShareTrack = track as? LocalVideoTrack
if (screenShareTrack == null) {
LKLog.e { "Tried to create screen audio capturer but passed track is not a video track: $track" }
return null
}
val capturer = screenShareTrack.capturer as? ScreenCapturerAndroid
if (capturer == null) {
LKLog.e { "Tried to create screen audio capturer but passed track does not contain a screen capturer: ${screenShareTrack.capturer}" }
return null
}
val mediaProjection = capturer.mediaProjection
if (mediaProjection == null) {
LKLog.e { "Tried to create screen audio capturer but the capturer doesn't have a media projection. Have you called startCapture?" }
return null
}
return ScreenAudioCapturer(mediaProjection)
}
}
private fun adjustByteBuffer(
buffer: ByteBuffer,
gain: Float,
) {
for (i in 0 until buffer.capacity()) {
val adjusted = (buffer[i] * gain)
.roundToInt()
.coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt())
buffer.put(i, adjusted.toByte())
}
}
private fun adjustShortBuffer(
buffer: ShortBuffer,
gain: Float,
) {
for (i in 0 until buffer.capacity()) {
val adjusted = (buffer[i] * gain)
.roundToInt()
.coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt())
buffer.put(i, adjusted.toShort())
}
}
private fun adjustFloatBuffer(
buffer: FloatBuffer,
gain: Float,
) {
for (i in 0 until buffer.capacity()) {
val adjusted = (buffer[i] * gain)
.coerceIn(-1f, 1f)
buffer.put(i, adjusted)
}
}
}
typealias AudioPlaybackCaptureConfigurator = (AudioPlaybackCaptureConfiguration.Builder) -> Unit
... ...
... ... @@ -48,6 +48,7 @@ object InjectionNames {
const val LIB_WEBRTC_INITIALIZATION = "lib_webrtc_initialization"
const val LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER = "local_audio_record_samples_dispatcher"
const val LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER = "local_audio_record_samples_dispatcher"
// Overrides
const val OVERRIDE_OKHTTP = "override_okhttp"
... ...
... ... @@ -25,6 +25,7 @@ import androidx.annotation.Nullable
import dagger.Module
import dagger.Provides
import io.livekit.android.LiveKit
import io.livekit.android.audio.AudioBufferCallbackDispatcher
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.audio.AudioProcessorOptions
import io.livekit.android.audio.AudioRecordSamplesDispatcher
... ... @@ -136,6 +137,13 @@ internal object RTCModule {
}
@Provides
@Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
@Singleton
fun localAudioBufferCallbackDispatcher(): AudioBufferCallbackDispatcher {
return AudioBufferCallbackDispatcher()
}
@Provides
@Singleton
@JvmSuppressWildcards
fun audioModule(
... ... @@ -151,6 +159,8 @@ internal object RTCModule {
communicationWorkaround: CommunicationWorkaround,
@Named(InjectionNames.LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER)
audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher,
@Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher,
): AudioDeviceModule {
if (audioDeviceModuleOverride != null) {
return audioDeviceModuleOverride
... ... @@ -229,6 +239,7 @@ internal object RTCModule {
// VOICE_COMMUNICATION needs to be used for echo cancelling.
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioAttributes(audioOutputAttributes)
.setAudioBufferCallback(audioBufferCallbackDispatcher)
moduleCustomizer?.invoke(builder)
return builder.createAudioDeviceModule()
... ...
... ... @@ -529,7 +529,7 @@ internal constructor(
options = options,
)
addTrackPublication(publication)
LKLog.e { "add track publication $publication" }
LKLog.v { "add track publication $publication" }
publishListener?.onPublishSuccess(publication)
internalListener?.onTrackPublished(publication, this)
... ...
... ... @@ -17,6 +17,7 @@
package io.livekit.android.room.provisions
import livekit.org.webrtc.EglBase
import livekit.org.webrtc.audio.AudioDeviceModule
import javax.inject.Inject
import javax.inject.Provider
... ... @@ -32,7 +33,11 @@ class LKObjects
@Inject
constructor(
private val eglBaseProvider: Provider<EglBase>,
private val audioDeviceModuleProvider: Provider<AudioDeviceModule>,
) {
val eglBase: EglBase
get() = eglBaseProvider.get()
val audioDeviceModule: AudioDeviceModule
get() = audioDeviceModuleProvider.get()
}
... ...
... ... @@ -23,11 +23,15 @@ import androidx.core.content.ContextCompat
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.audio.AudioBufferCallback
import io.livekit.android.audio.AudioBufferCallbackDispatcher
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.audio.AudioRecordSamplesDispatcher
import io.livekit.android.audio.MixerAudioBufferCallback
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.LKLog
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
import kotlinx.coroutines.CoroutineDispatcher
... ... @@ -64,6 +68,8 @@ constructor(
private val dispatcher: CoroutineDispatcher,
@Named(InjectionNames.LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER)
private val audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher,
@Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
private val audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher,
) : AudioTrack(name, mediaTrack) {
/**
* To only be used for flow delegate scoping, and should not be cancelled.
... ... @@ -99,6 +105,16 @@ constructor(
}
/**
* Use this method to mix in custom audio.
*
* See [MixerAudioBufferCallback] for automatic handling of mixing in
* the provided audio data.
*/
fun setAudioBufferCallback(callback: AudioBufferCallback?) {
audioBufferCallbackDispatcher.bufferCallback = callback
}
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@FlowObservable
... ... @@ -158,7 +174,7 @@ constructor(
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) !=
PackageManager.PERMISSION_GRANTED
) {
throw SecurityException("Record audio permissions are required to create an audio track.")
LKLog.w { "Record audio permissions not granted, microphone recording will not be used." }
}
val audioConstraints = MediaConstraints()
... ...
... ... @@ -17,11 +17,23 @@
package io.livekit.android.test.mock
import io.livekit.android.room.provisions.LKObjects
import livekit.org.webrtc.audio.AudioDeviceModule
object MockLKObjects {
fun get(): LKObjects {
return LKObjects(
eglBaseProvider = { MockEglBase() },
audioDeviceModuleProvider = {
object : AudioDeviceModule {
override fun getNativeAudioDeviceModulePointer(): Long = 1
override fun release() {}
override fun setSpeakerMute(p0: Boolean) {}
override fun setMicrophoneMute(p0: Boolean) {}
}
},
)
}
}
... ...
... ... @@ -20,6 +20,7 @@ import android.content.Context
import android.javax.sdp.SdpFactory
import dagger.Module
import dagger.Provides
import io.livekit.android.audio.AudioBufferCallbackDispatcher
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.audio.AudioRecordSamplesDispatcher
import io.livekit.android.dagger.CapabilitiesGetter
... ... @@ -40,6 +41,13 @@ import javax.inject.Singleton
object TestRTCModule {
@Provides
@Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
@Singleton
fun localAudioBufferCallbackDispatcher(): AudioBufferCallbackDispatcher {
return AudioBufferCallbackDispatcher()
}
@Provides
@Singleton
fun eglBase(): EglBase {
return MockEglBase()
... ...
... ... @@ -16,6 +16,7 @@
package io.livekit.android.test.mock.room.track
import io.livekit.android.audio.AudioBufferCallbackDispatcher
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.audio.AudioRecordSamplesDispatcher
import io.livekit.android.room.track.LocalAudioTrack
... ... @@ -36,6 +37,7 @@ fun MockE2ETest.createMockLocalAudioTrack(
audioProcessingController: AudioProcessingController = MockAudioProcessingController(),
dispatcher: CoroutineDispatcher = coroutineRule.dispatcher,
audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher = AudioRecordSamplesDispatcher(),
audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher = AudioBufferCallbackDispatcher(),
): LocalAudioTrack {
return LocalAudioTrack(
name = name,
... ... @@ -44,5 +46,6 @@ fun MockE2ETest.createMockLocalAudioTrack(
audioProcessingController = audioProcessingController,
dispatcher = dispatcher,
audioRecordSamplesDispatcher = audioRecordSamplesDispatcher,
audioBufferCallbackDispatcher = audioBufferCallbackDispatcher,
)
}
... ...
... ... @@ -29,3 +29,4 @@ include ':sample-app-record-local'
include ':examples:selfie-segmentation'
include ':livekit-android-test'
include ':livekit-android-camerax'
include ':examples:screenshare-audio'
... ...