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 行删除
  1 +---
  2 +"client-sdk-android": minor
  3 +---
  4 +
  5 +Implement custom audio mixing into audio track
  1 +---
  2 +"client-sdk-android": minor
  3 +---
  4 +
  5 +Update to webrtc-sdk 125.6422.06.1
  1 +---
  2 +"client-sdk-android": minor
  3 +---
  4 +
  5 +Implement screen share audio capturer
1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4"> 2 <project version="4">
3 <component name="CompilerConfiguration"> 3 <component name="CompilerConfiguration">
4 - <bytecodeTargetLevel target="1.8" /> 4 + <bytecodeTargetLevel target="1.8">
  5 + <module name="livekit-android.examples.screenshareaudio" target="17" />
  6 + </bytecodeTargetLevel>
5 </component> 7 </component>
6 </project> 8 </project>
  1 +plugins {
  2 + id 'com.android.application'
  3 + id 'org.jetbrains.kotlin.android'
  4 +}
  5 +
  6 +android {
  7 + namespace 'io.livekit.android.example.screenshareaudio'
  8 + compileSdk 34
  9 +
  10 + defaultConfig {
  11 + applicationId "io.livekit.android.example.screenshareaudio"
  12 + minSdk 29
  13 + targetSdk 34
  14 + versionCode 1
  15 + versionName "1.0"
  16 +
  17 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  18 + vectorDrawables {
  19 + useSupportLibrary true
  20 + }
  21 + }
  22 +
  23 + buildTypes {
  24 + release {
  25 + minifyEnabled false
  26 + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
  27 + }
  28 + }
  29 + compileOptions {
  30 + sourceCompatibility JavaVersion.VERSION_1_8
  31 + targetCompatibility JavaVersion.VERSION_1_8
  32 + }
  33 + kotlinOptions {
  34 + jvmTarget = '1.8'
  35 + }
  36 + buildFeatures {
  37 + compose true
  38 + }
  39 + composeOptions {
  40 + kotlinCompilerExtensionVersion compose_compiler_version
  41 + }
  42 + packaging {
  43 + resources {
  44 + excludes += '/META-INF/{AL2.0,LGPL2.1}'
  45 + }
  46 + }
  47 +}
  48 +
  49 +dependencies {
  50 +
  51 + // If building the sample app outside the context of this repo, replace the following with:
  52 + // api "io.livekit:livekit-android:<version>"
  53 + api project(":livekit-android-sdk")
  54 + api project(":sample-app-common")
  55 + implementation libs.androidx.core.ktx
  56 + implementation libs.androidx.lifecycle.runtime.ktx
  57 + implementation libs.androidx.activity.compose
  58 + implementation platform(libs.compose.bom)
  59 + implementation libs.androidx.ui
  60 + implementation libs.androidx.ui.graphics
  61 + implementation libs.androidx.ui.tooling.preview
  62 + implementation libs.androidx.material3
  63 + implementation libs.timber
  64 + testImplementation libs.junit
  65 + androidTestImplementation libs.androidx.test.junit
  66 + androidTestImplementation libs.espresso
  67 + androidTestImplementation platform(libs.compose.bom)
  68 + androidTestImplementation libs.androidx.ui.test.junit4
  69 + debugImplementation libs.androidx.ui.tooling
  70 + debugImplementation libs.androidx.ui.test.manifest
  71 +}
  1 +# Add project specific ProGuard rules here.
  2 +# You can control the set of applied configuration files using the
  3 +# proguardFiles setting in build.gradle.
  4 +#
  5 +# For more details, see
  6 +# http://developer.android.com/guide/developing/tools/proguard.html
  7 +
  8 +# If your project uses WebView with JS, uncomment the following
  9 +# and specify the fully qualified class name to the JavaScript interface
  10 +# class:
  11 +#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
  12 +# public *;
  13 +#}
  14 +
  15 +# Uncomment this to preserve the line number information for
  16 +# debugging stack traces.
  17 +#-keepattributes SourceFile,LineNumberTable
  18 +
  19 +# If you keep the line number information, uncomment this to
  20 +# hide the original source file name.
  21 +#-renamesourcefileattribute SourceFile
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.example.screenshareaudio
  18 +
  19 +import androidx.test.ext.junit.runners.AndroidJUnit4
  20 +import androidx.test.platform.app.InstrumentationRegistry
  21 +import org.junit.Assert.*
  22 +import org.junit.Test
  23 +import org.junit.runner.RunWith
  24 +
  25 +/**
  26 + * Instrumented test, which will execute on an Android device.
  27 + *
  28 + * See [testing documentation](http://d.android.com/tools/testing).
  29 + */
  30 +@RunWith(AndroidJUnit4::class)
  31 +class ExampleInstrumentedTest {
  32 + @Test
  33 + fun useAppContext() {
  34 + // Context of the app under test.
  35 + val appContext = InstrumentationRegistry.getInstrumentation().targetContext
  36 + assertEquals("io.livekit.android.example.screenshareaudio", appContext.packageName)
  37 + }
  38 +}
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  3 +
  4 + <application
  5 + android:allowBackup="true"
  6 + android:icon="@mipmap/ic_launcher"
  7 + android:label="@string/app_name"
  8 + android:roundIcon="@mipmap/ic_launcher_round"
  9 + android:supportsRtl="true"
  10 + android:theme="@style/Theme.Livekitandroid"
  11 + android:usesCleartextTraffic="true">
  12 + <activity
  13 + android:name=".MainActivity"
  14 + android:exported="true"
  15 + android:label="@string/app_name"
  16 + android:theme="@style/Theme.Livekitandroid">
  17 + <intent-filter>
  18 + <action android:name="android.intent.action.MAIN" />
  19 +
  20 + <category android:name="android.intent.category.LAUNCHER" />
  21 + </intent-filter>
  22 + </activity>
  23 + </application>
  24 +
  25 +</manifest>
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.example.screenshareaudio
  18 +
  19 +import android.media.projection.MediaProjectionManager
  20 +import android.os.Bundle
  21 +import androidx.activity.ComponentActivity
  22 +import androidx.activity.compose.setContent
  23 +import androidx.activity.result.contract.ActivityResultContracts
  24 +import androidx.activity.viewModels
  25 +import androidx.compose.foundation.layout.fillMaxSize
  26 +import androidx.compose.material3.Button
  27 +import androidx.compose.material3.MaterialTheme
  28 +import androidx.compose.material3.Surface
  29 +import androidx.compose.material3.Text
  30 +import androidx.compose.runtime.getValue
  31 +import androidx.compose.runtime.mutableStateOf
  32 +import androidx.compose.runtime.remember
  33 +import androidx.compose.runtime.setValue
  34 +import androidx.compose.ui.Modifier
  35 +import io.livekit.android.LiveKit
  36 +import io.livekit.android.example.screenshareaudio.ui.theme.LivekitandroidTheme
  37 +import io.livekit.android.util.LoggingLevel
  38 +
  39 +class MainActivity : ComponentActivity() {
  40 +
  41 + private val viewModel: MainViewModel by viewModels<MainViewModel>()
  42 + private val screenCaptureIntentLauncher =
  43 + registerForActivityResult(
  44 + ActivityResultContracts.StartActivityForResult(),
  45 + ) { result ->
  46 + val resultCode = result.resultCode
  47 + val data = result.data
  48 + if (resultCode != RESULT_OK || data == null) {
  49 + return@registerForActivityResult
  50 + }
  51 + viewModel.startScreenCapture(data)
  52 + }
  53 +
  54 + private fun requestMediaProjection() {
  55 + val mediaProjectionManager =
  56 + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
  57 + screenCaptureIntentLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
  58 + }
  59 +
  60 + override fun onCreate(savedInstanceState: Bundle?) {
  61 + super.onCreate(savedInstanceState)
  62 + LiveKit.loggingLevel = LoggingLevel.INFO
  63 + viewModel
  64 + setContent {
  65 + LivekitandroidTheme {
  66 + // A surface container using the 'background' color from the theme
  67 + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
  68 + var enableScreenCapture by remember {
  69 + mutableStateOf(true)
  70 + }
  71 + Button(
  72 + onClick = {
  73 + if (enableScreenCapture) {
  74 + requestMediaProjection()
  75 + } else {
  76 + viewModel.stopScreenCapture()
  77 + }
  78 + enableScreenCapture = !enableScreenCapture
  79 + },
  80 + ) {
  81 + val text = if (enableScreenCapture) {
  82 + "enable"
  83 + } else {
  84 + "disable"
  85 + }
  86 + Text(text = text)
  87 + }
  88 + }
  89 + }
  90 + }
  91 + }
  92 +}
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.example.screenshareaudio
  18 +
  19 +import android.Manifest
  20 +import android.app.Application
  21 +import android.content.Intent
  22 +import android.content.pm.PackageManager
  23 +import android.os.Build
  24 +import androidx.annotation.RequiresApi
  25 +import androidx.core.app.ActivityCompat
  26 +import androidx.lifecycle.AndroidViewModel
  27 +import androidx.lifecycle.viewModelScope
  28 +import io.livekit.android.LiveKit
  29 +import io.livekit.android.audio.ScreenAudioCapturer
  30 +import io.livekit.android.room.track.LocalAudioTrack
  31 +import io.livekit.android.room.track.LocalVideoTrack
  32 +import io.livekit.android.room.track.Track
  33 +import io.livekit.android.sample.service.ForegroundService
  34 +import kotlinx.coroutines.Dispatchers
  35 +import kotlinx.coroutines.launch
  36 +
  37 +val url = "wss://example.com"
  38 +val token = ""
  39 +
  40 +@RequiresApi(Build.VERSION_CODES.Q)
  41 +class MainViewModel(application: Application) : AndroidViewModel(application) {
  42 +
  43 + val room = LiveKit.create(application)
  44 + var audioCapturer: ScreenAudioCapturer? = null
  45 +
  46 + init {
  47 + viewModelScope.launch(Dispatchers.IO) {
  48 + room.connect(url, token)
  49 + }
  50 +
  51 + // Start a foreground service to keep the call from being interrupted if the
  52 + // app goes into the background.
  53 + val foregroundServiceIntent = Intent(application, ForegroundService::class.java)
  54 + application.startForegroundService(foregroundServiceIntent)
  55 + }
  56 +
  57 + fun startScreenCapture(data: Intent) {
  58 + viewModelScope.launch(Dispatchers.IO) {
  59 + if (ActivityCompat.checkSelfPermission(getApplication(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
  60 + return@launch
  61 + }
  62 + room.localParticipant.setScreenShareEnabled(true, data)
  63 + room.localParticipant.setMicrophoneEnabled(true)
  64 + val screenCaptureTrack = room.localParticipant.getTrackPublication(Track.Source.SCREEN_SHARE)?.track as? LocalVideoTrack ?: return@launch
  65 + val audioTrack = room.localParticipant.getTrackPublication(Track.Source.MICROPHONE)?.track as? LocalAudioTrack ?: return@launch
  66 +
  67 + audioCapturer = ScreenAudioCapturer.createFromScreenShareTrack(screenCaptureTrack) ?: return@launch
  68 + audioCapturer?.gain = 0.1f // Lower the volume so that mic can still be heard clearly.
  69 + audioTrack.setAudioBufferCallback(audioCapturer!!)
  70 + }
  71 + }
  72 +
  73 + fun stopScreenCapture() {
  74 + viewModelScope.launch(Dispatchers.IO) {
  75 + (room.localParticipant.getTrackPublication(Track.Source.MICROPHONE)?.track as? LocalAudioTrack)
  76 + ?.setAudioBufferCallback(null)
  77 + room.localParticipant.setMicrophoneEnabled(false)
  78 + room.localParticipant.setScreenShareEnabled(false)
  79 + audioCapturer?.releaseAudioResources()
  80 + }
  81 + }
  82 +}
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.example.screenshareaudio.ui.theme
  18 +
  19 +import androidx.compose.ui.graphics.Color
  20 +
  21 +val Purple80 = Color(0xFFD0BCFF)
  22 +val PurpleGrey80 = Color(0xFFCCC2DC)
  23 +val Pink80 = Color(0xFFEFB8C8)
  24 +
  25 +val Purple40 = Color(0xFF6650a4)
  26 +val PurpleGrey40 = Color(0xFF625b71)
  27 +val Pink40 = Color(0xFF7D5260)
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.example.screenshareaudio.ui.theme
  18 +
  19 +import android.app.Activity
  20 +import android.os.Build
  21 +import androidx.compose.foundation.isSystemInDarkTheme
  22 +import androidx.compose.material3.MaterialTheme
  23 +import androidx.compose.material3.darkColorScheme
  24 +import androidx.compose.material3.dynamicDarkColorScheme
  25 +import androidx.compose.material3.dynamicLightColorScheme
  26 +import androidx.compose.material3.lightColorScheme
  27 +import androidx.compose.runtime.Composable
  28 +import androidx.compose.runtime.SideEffect
  29 +import androidx.compose.ui.graphics.toArgb
  30 +import androidx.compose.ui.platform.LocalContext
  31 +import androidx.compose.ui.platform.LocalView
  32 +import androidx.core.view.WindowCompat
  33 +
  34 +private val DarkColorScheme = darkColorScheme(
  35 + primary = Purple80,
  36 + secondary = PurpleGrey80,
  37 + tertiary = Pink80
  38 +)
  39 +
  40 +private val LightColorScheme = lightColorScheme(
  41 + primary = Purple40,
  42 + secondary = PurpleGrey40,
  43 + tertiary = Pink40
  44 +
  45 + /* Other default colors to override
  46 + background = Color(0xFFFFFBFE),
  47 + surface = Color(0xFFFFFBFE),
  48 + onPrimary = Color.White,
  49 + onSecondary = Color.White,
  50 + onTertiary = Color.White,
  51 + onBackground = Color(0xFF1C1B1F),
  52 + onSurface = Color(0xFF1C1B1F),
  53 + */
  54 +)
  55 +
  56 +@Composable
  57 +fun LivekitandroidTheme(
  58 + darkTheme: Boolean = isSystemInDarkTheme(),
  59 + // Dynamic color is available on Android 12+
  60 + dynamicColor: Boolean = true,
  61 + content: @Composable () -> Unit
  62 +) {
  63 + val colorScheme = when {
  64 + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
  65 + val context = LocalContext.current
  66 + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
  67 + }
  68 +
  69 + darkTheme -> DarkColorScheme
  70 + else -> LightColorScheme
  71 + }
  72 + val view = LocalView.current
  73 + if (!view.isInEditMode) {
  74 + SideEffect {
  75 + val window = (view.context as Activity).window
  76 + window.statusBarColor = colorScheme.primary.toArgb()
  77 + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
  78 + }
  79 + }
  80 +
  81 + MaterialTheme(
  82 + colorScheme = colorScheme,
  83 + typography = Typography,
  84 + content = content
  85 + )
  86 +}
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.example.screenshareaudio.ui.theme
  18 +
  19 +import androidx.compose.material3.Typography
  20 +import androidx.compose.ui.text.TextStyle
  21 +import androidx.compose.ui.text.font.FontFamily
  22 +import androidx.compose.ui.text.font.FontWeight
  23 +import androidx.compose.ui.unit.sp
  24 +
  25 +// Set of Material typography styles to start with
  26 +val Typography = Typography(
  27 + bodyLarge = TextStyle(
  28 + fontFamily = FontFamily.Default,
  29 + fontWeight = FontWeight.Normal,
  30 + fontSize = 16.sp,
  31 + lineHeight = 24.sp,
  32 + letterSpacing = 0.5.sp
  33 + )
  34 + /* Other default text styles to override
  35 + titleLarge = TextStyle(
  36 + fontFamily = FontFamily.Default,
  37 + fontWeight = FontWeight.Normal,
  38 + fontSize = 22.sp,
  39 + lineHeight = 28.sp,
  40 + letterSpacing = 0.sp
  41 + ),
  42 + labelSmall = TextStyle(
  43 + fontFamily = FontFamily.Default,
  44 + fontWeight = FontWeight.Medium,
  45 + fontSize = 11.sp,
  46 + lineHeight = 16.sp,
  47 + letterSpacing = 0.5.sp
  48 + )
  49 + */
  50 +)
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  3 + android:width="108dp"
  4 + android:height="108dp"
  5 + android:viewportWidth="108"
  6 + android:viewportHeight="108">
  7 + <path
  8 + android:fillColor="#3DDC84"
  9 + android:pathData="M0,0h108v108h-108z" />
  10 + <path
  11 + android:fillColor="#00000000"
  12 + android:pathData="M9,0L9,108"
  13 + android:strokeWidth="0.8"
  14 + android:strokeColor="#33FFFFFF" />
  15 + <path
  16 + android:fillColor="#00000000"
  17 + android:pathData="M19,0L19,108"
  18 + android:strokeWidth="0.8"
  19 + android:strokeColor="#33FFFFFF" />
  20 + <path
  21 + android:fillColor="#00000000"
  22 + android:pathData="M29,0L29,108"
  23 + android:strokeWidth="0.8"
  24 + android:strokeColor="#33FFFFFF" />
  25 + <path
  26 + android:fillColor="#00000000"
  27 + android:pathData="M39,0L39,108"
  28 + android:strokeWidth="0.8"
  29 + android:strokeColor="#33FFFFFF" />
  30 + <path
  31 + android:fillColor="#00000000"
  32 + android:pathData="M49,0L49,108"
  33 + android:strokeWidth="0.8"
  34 + android:strokeColor="#33FFFFFF" />
  35 + <path
  36 + android:fillColor="#00000000"
  37 + android:pathData="M59,0L59,108"
  38 + android:strokeWidth="0.8"
  39 + android:strokeColor="#33FFFFFF" />
  40 + <path
  41 + android:fillColor="#00000000"
  42 + android:pathData="M69,0L69,108"
  43 + android:strokeWidth="0.8"
  44 + android:strokeColor="#33FFFFFF" />
  45 + <path
  46 + android:fillColor="#00000000"
  47 + android:pathData="M79,0L79,108"
  48 + android:strokeWidth="0.8"
  49 + android:strokeColor="#33FFFFFF" />
  50 + <path
  51 + android:fillColor="#00000000"
  52 + android:pathData="M89,0L89,108"
  53 + android:strokeWidth="0.8"
  54 + android:strokeColor="#33FFFFFF" />
  55 + <path
  56 + android:fillColor="#00000000"
  57 + android:pathData="M99,0L99,108"
  58 + android:strokeWidth="0.8"
  59 + android:strokeColor="#33FFFFFF" />
  60 + <path
  61 + android:fillColor="#00000000"
  62 + android:pathData="M0,9L108,9"
  63 + android:strokeWidth="0.8"
  64 + android:strokeColor="#33FFFFFF" />
  65 + <path
  66 + android:fillColor="#00000000"
  67 + android:pathData="M0,19L108,19"
  68 + android:strokeWidth="0.8"
  69 + android:strokeColor="#33FFFFFF" />
  70 + <path
  71 + android:fillColor="#00000000"
  72 + android:pathData="M0,29L108,29"
  73 + android:strokeWidth="0.8"
  74 + android:strokeColor="#33FFFFFF" />
  75 + <path
  76 + android:fillColor="#00000000"
  77 + android:pathData="M0,39L108,39"
  78 + android:strokeWidth="0.8"
  79 + android:strokeColor="#33FFFFFF" />
  80 + <path
  81 + android:fillColor="#00000000"
  82 + android:pathData="M0,49L108,49"
  83 + android:strokeWidth="0.8"
  84 + android:strokeColor="#33FFFFFF" />
  85 + <path
  86 + android:fillColor="#00000000"
  87 + android:pathData="M0,59L108,59"
  88 + android:strokeWidth="0.8"
  89 + android:strokeColor="#33FFFFFF" />
  90 + <path
  91 + android:fillColor="#00000000"
  92 + android:pathData="M0,69L108,69"
  93 + android:strokeWidth="0.8"
  94 + android:strokeColor="#33FFFFFF" />
  95 + <path
  96 + android:fillColor="#00000000"
  97 + android:pathData="M0,79L108,79"
  98 + android:strokeWidth="0.8"
  99 + android:strokeColor="#33FFFFFF" />
  100 + <path
  101 + android:fillColor="#00000000"
  102 + android:pathData="M0,89L108,89"
  103 + android:strokeWidth="0.8"
  104 + android:strokeColor="#33FFFFFF" />
  105 + <path
  106 + android:fillColor="#00000000"
  107 + android:pathData="M0,99L108,99"
  108 + android:strokeWidth="0.8"
  109 + android:strokeColor="#33FFFFFF" />
  110 + <path
  111 + android:fillColor="#00000000"
  112 + android:pathData="M19,29L89,29"
  113 + android:strokeWidth="0.8"
  114 + android:strokeColor="#33FFFFFF" />
  115 + <path
  116 + android:fillColor="#00000000"
  117 + android:pathData="M19,39L89,39"
  118 + android:strokeWidth="0.8"
  119 + android:strokeColor="#33FFFFFF" />
  120 + <path
  121 + android:fillColor="#00000000"
  122 + android:pathData="M19,49L89,49"
  123 + android:strokeWidth="0.8"
  124 + android:strokeColor="#33FFFFFF" />
  125 + <path
  126 + android:fillColor="#00000000"
  127 + android:pathData="M19,59L89,59"
  128 + android:strokeWidth="0.8"
  129 + android:strokeColor="#33FFFFFF" />
  130 + <path
  131 + android:fillColor="#00000000"
  132 + android:pathData="M19,69L89,69"
  133 + android:strokeWidth="0.8"
  134 + android:strokeColor="#33FFFFFF" />
  135 + <path
  136 + android:fillColor="#00000000"
  137 + android:pathData="M19,79L89,79"
  138 + android:strokeWidth="0.8"
  139 + android:strokeColor="#33FFFFFF" />
  140 + <path
  141 + android:fillColor="#00000000"
  142 + android:pathData="M29,19L29,89"
  143 + android:strokeWidth="0.8"
  144 + android:strokeColor="#33FFFFFF" />
  145 + <path
  146 + android:fillColor="#00000000"
  147 + android:pathData="M39,19L39,89"
  148 + android:strokeWidth="0.8"
  149 + android:strokeColor="#33FFFFFF" />
  150 + <path
  151 + android:fillColor="#00000000"
  152 + android:pathData="M49,19L49,89"
  153 + android:strokeWidth="0.8"
  154 + android:strokeColor="#33FFFFFF" />
  155 + <path
  156 + android:fillColor="#00000000"
  157 + android:pathData="M59,19L59,89"
  158 + android:strokeWidth="0.8"
  159 + android:strokeColor="#33FFFFFF" />
  160 + <path
  161 + android:fillColor="#00000000"
  162 + android:pathData="M69,19L69,89"
  163 + android:strokeWidth="0.8"
  164 + android:strokeColor="#33FFFFFF" />
  165 + <path
  166 + android:fillColor="#00000000"
  167 + android:pathData="M79,19L79,89"
  168 + android:strokeWidth="0.8"
  169 + android:strokeColor="#33FFFFFF" />
  170 +</vector>
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + xmlns:aapt="http://schemas.android.com/aapt"
  3 + android:width="108dp"
  4 + android:height="108dp"
  5 + android:viewportWidth="108"
  6 + android:viewportHeight="108">
  7 + <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">
  8 + <aapt:attr name="android:fillColor">
  9 + <gradient
  10 + android:endX="85.84757"
  11 + android:endY="92.4963"
  12 + android:startX="42.9492"
  13 + android:startY="49.59793"
  14 + android:type="linear">
  15 + <item
  16 + android:color="#44000000"
  17 + android:offset="0.0" />
  18 + <item
  19 + android:color="#00000000"
  20 + android:offset="1.0" />
  21 + </gradient>
  22 + </aapt:attr>
  23 + </path>
  24 + <path
  25 + android:fillColor="#FFFFFF"
  26 + android:fillType="nonZero"
  27 + 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"
  28 + android:strokeWidth="1"
  29 + android:strokeColor="#00000000" />
  30 +</vector>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  3 + <background android:drawable="@drawable/ic_launcher_background" />
  4 + <foreground android:drawable="@drawable/ic_launcher_foreground" />
  5 + <monochrome android:drawable="@drawable/ic_launcher_foreground" />
  6 +</adaptive-icon>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  3 + <background android:drawable="@drawable/ic_launcher_background" />
  4 + <foreground android:drawable="@drawable/ic_launcher_foreground" />
  5 + <monochrome android:drawable="@drawable/ic_launcher_foreground" />
  6 +</adaptive-icon>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<resources>
  3 + <color name="purple_200">#FFBB86FC</color>
  4 + <color name="purple_500">#FF6200EE</color>
  5 + <color name="purple_700">#FF3700B3</color>
  6 + <color name="teal_200">#FF03DAC5</color>
  7 + <color name="teal_700">#FF018786</color>
  8 + <color name="black">#FF000000</color>
  9 + <color name="white">#FFFFFFFF</color>
  10 +</resources>
  1 +<resources>
  2 + <string name="app_name">ScreenShareAudio</string>
  3 +</resources>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<resources>
  3 +
  4 + <style name="Theme.Livekitandroid" parent="android:Theme.Material.Light.NoActionBar" />
  5 +</resources>
1 [versions] 1 [versions]
2 -webrtc = "125.6422.05" 2 +webrtc = "125.6422.06.1"
3 3
4 androidJainSipRi = "1.3.0-91" 4 androidJainSipRi = "1.3.0-91"
5 androidx-activity = "1.9.0" 5 androidx-activity = "1.9.0"
@@ -96,6 +96,13 @@ robolectric = { module = "org.robolectric:robolectric", version = "4.11.1" } @@ -96,6 +96,13 @@ robolectric = { module = "org.robolectric:robolectric", version = "4.11.1" }
96 turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" } 96 turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" }
97 appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 97 appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
98 material = { group = "com.google.android.material", name = "material", version.ref = "material" } 98 material = { group = "com.google.android.material", name = "material", version.ref = "material" }
  99 +androidx-ui = { group = "androidx.compose.ui", name = "ui" }
  100 +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
  101 +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
  102 +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
  103 +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
  104 +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
  105 +androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
99 106
100 [plugins] 107 [plugins]
101 108
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.audio
  18 +
  19 +import android.media.AudioFormat
  20 +import java.nio.ByteBuffer
  21 +
  22 +/**
  23 + * @suppress
  24 + */
  25 +class AudioBufferCallbackDispatcher : livekit.org.webrtc.audio.JavaAudioDeviceModule.AudioBufferCallback {
  26 + var bufferCallback: AudioBufferCallback? = null
  27 +
  28 + override fun onBuffer(buffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): Long {
  29 + return bufferCallback?.onBuffer(
  30 + buffer = buffer,
  31 + audioFormat = audioFormat,
  32 + channelCount = channelCount,
  33 + sampleRate = sampleRate,
  34 + bytesRead = bytesRead,
  35 + captureTimeNs = captureTimeNs,
  36 + ) ?: 0L
  37 + }
  38 +}
  39 +
  40 +interface AudioBufferCallback {
  41 + /**
  42 + * Called when new audio samples are ready.
  43 + * @param buffer the buffer of audio bytes. Changes to this buffer will be published on the audio track.
  44 + * @param audioFormat the audio encoding. See [AudioFormat.ENCODING_PCM_8BIT],
  45 + * [AudioFormat.ENCODING_PCM_16BIT], and [AudioFormat.ENCODING_PCM_FLOAT]. Note
  46 + * that [AudioFormat.ENCODING_DEFAULT] defaults to PCM-16bit.
  47 + * @param channelCount
  48 + * @param sampleRate
  49 + * @param bytesRead the byte count originally read from the microphone.
  50 + * @param captureTimeNs the capture timestamp of the original audio data in nanoseconds.
  51 + * @return the capture timestamp in nanoseconds. Return 0 if not available.
  52 + */
  53 + fun onBuffer(buffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): Long
  54 +}
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.audio
  18 +
  19 +import android.media.AudioFormat
  20 +import io.livekit.android.util.LKLog
  21 +import java.nio.ByteBuffer
  22 +import java.nio.ByteOrder
  23 +import java.nio.FloatBuffer
  24 +import java.nio.ShortBuffer
  25 +import kotlin.math.min
  26 +
  27 +/**
  28 + * A convenience class that handles mixing the microphone data and custom audio data.
  29 + */
  30 +abstract class MixerAudioBufferCallback : AudioBufferCallback {
  31 +
  32 + class BufferResponse(
  33 + /**
  34 + * The byteBuffer to mix into the audio track.
  35 + */
  36 + val byteBuffer: ByteBuffer? = null,
  37 + /**
  38 + * The capture time stamp in nanoseconds, or null if not available.
  39 + */
  40 + val captureTimeNs: Long? = null,
  41 + )
  42 +
  43 + final override fun onBuffer(buffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): Long {
  44 + val response = onBufferRequest(buffer, audioFormat, channelCount, sampleRate, bytesRead, captureTimeNs)
  45 +
  46 + val customAudioBuffer = response?.byteBuffer
  47 +
  48 + if (customAudioBuffer != null) {
  49 + buffer.order(ByteOrder.nativeOrder()).position(0)
  50 + customAudioBuffer.order(ByteOrder.nativeOrder()).position(0)
  51 +
  52 + when (audioFormat) {
  53 + AudioFormat.ENCODING_PCM_8BIT -> {
  54 + mixByteBuffers(original = buffer, customAudioBuffer)
  55 + }
  56 +
  57 + AudioFormat.ENCODING_PCM_16BIT,
  58 + AudioFormat.ENCODING_DEFAULT,
  59 + -> {
  60 + mixShortBuffers(original = buffer.asShortBuffer(), customAudioBuffer.asShortBuffer())
  61 + }
  62 +
  63 + AudioFormat.ENCODING_PCM_FLOAT -> {
  64 + mixFloatBuffers(original = buffer.asFloatBuffer(), customAudioBuffer.asFloatBuffer())
  65 + }
  66 +
  67 + else -> {
  68 + LKLog.w { "Unsupported audio format: $audioFormat" }
  69 + }
  70 + }
  71 + }
  72 +
  73 + val mixedCaptureTime = if (captureTimeNs != 0L) {
  74 + captureTimeNs
  75 + } else {
  76 + response?.captureTimeNs ?: 0L
  77 + }
  78 +
  79 + return mixedCaptureTime
  80 + }
  81 +
  82 + abstract fun onBufferRequest(originalBuffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): BufferResponse?
  83 +
  84 + private fun mixByteBuffers(
  85 + original: ByteBuffer,
  86 + addBuffer: ByteBuffer,
  87 + ) {
  88 + val size = min(original.capacity(), addBuffer.capacity())
  89 + if (size <= 0) return
  90 + for (i in 0 until size) {
  91 + val sum = (original[i].toInt() + addBuffer[i].toInt())
  92 + .coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt())
  93 + original.put(i, sum.toByte())
  94 + }
  95 + }
  96 +
  97 + private fun mixShortBuffers(
  98 + original: ShortBuffer,
  99 + addBuffer: ShortBuffer,
  100 + ) {
  101 + val size = min(original.capacity(), addBuffer.capacity())
  102 + if (size <= 0) return
  103 +
  104 + for (i in 0 until size) {
  105 + val sum = (original[i].toInt() + addBuffer[i].toInt())
  106 + .coerceIn(
  107 + minimumValue = Short.MIN_VALUE.toInt(),
  108 + maximumValue = Short.MAX_VALUE.toInt(),
  109 + )
  110 + original.put(i, sum.toShort())
  111 + }
  112 + }
  113 +
  114 + private fun mixFloatBuffers(
  115 + original: FloatBuffer,
  116 + addBuffer: FloatBuffer,
  117 + ) {
  118 + val size = min(original.capacity(), addBuffer.capacity())
  119 + if (size <= 0) return
  120 + for (i in 0 until size) {
  121 + val sum = (original[i] + addBuffer[i])
  122 + .coerceIn(-1f, 1f)
  123 + original.put(i, sum)
  124 + }
  125 + }
  126 +}
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.audio
  18 +
  19 +import android.Manifest
  20 +import android.annotation.SuppressLint
  21 +import android.media.AudioAttributes
  22 +import android.media.AudioFormat
  23 +import android.media.AudioPlaybackCaptureConfiguration
  24 +import android.media.AudioRecord
  25 +import android.media.projection.MediaProjection
  26 +import android.os.Build
  27 +import androidx.annotation.RequiresApi
  28 +import androidx.annotation.RequiresPermission
  29 +import io.livekit.android.room.track.LocalVideoTrack
  30 +import io.livekit.android.room.track.Track
  31 +import io.livekit.android.util.LKLog
  32 +import livekit.org.webrtc.ScreenCapturerAndroid
  33 +import java.nio.ByteBuffer
  34 +import java.nio.ByteOrder
  35 +import java.nio.FloatBuffer
  36 +import java.nio.ShortBuffer
  37 +import kotlin.math.abs
  38 +import kotlin.math.max
  39 +import kotlin.math.roundToInt
  40 +
  41 +private const val BUFFER_SIZE_FACTOR = 2
  42 +private const val MIN_GAIN_CHANGE = 0.01f
  43 +private const val DEFAULT_GAIN = 1f
  44 +
  45 +private val DEFAULT_CONFIGURATOR: AudioPlaybackCaptureConfigurator = { builder ->
  46 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  47 + builder.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
  48 + builder.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
  49 + builder.addMatchingUsage(AudioAttributes.USAGE_GAME)
  50 + }
  51 +}
  52 +
  53 +/**
  54 + * A mixer for capturing screen share audio.
  55 + *
  56 + * Requires a media projection, which can be obtained from the screen share track.
  57 + *
  58 + * Additionally, for screen capture to work properly while your app is in the
  59 + * background, a foreground service with the type `microphone` must be running.
  60 + * Otherwise, audio capture will not return any audio data.
  61 + *
  62 + * Example usage:
  63 + * ```
  64 + *
  65 + * ```
  66 + */
  67 +@RequiresApi(Build.VERSION_CODES.Q)
  68 +class ScreenAudioCapturer
  69 +@RequiresPermission(Manifest.permission.RECORD_AUDIO)
  70 +constructor(
  71 + private val mediaProjection: MediaProjection,
  72 + /**
  73 + * Screen share audio capture requires the use of [AudioPlaybackCaptureConfiguration].
  74 + * This parameter allows customizing the configuration used. Note that
  75 + * the configuration must have at least one match rule applied to it or
  76 + * an exception will be thrown.
  77 + *
  78 + * The default configurator adds matching rules against all available usage
  79 + * types that can be captured.
  80 + */
  81 + private val captureConfigurator: AudioPlaybackCaptureConfigurator = DEFAULT_CONFIGURATOR,
  82 +) : MixerAudioBufferCallback() {
  83 + private var audioRecord: AudioRecord? = null
  84 +
  85 + private var hasInitialized = false
  86 + private var byteBuffer: ByteBuffer? = null
  87 +
  88 + /**
  89 + * A multiplier to adjust the volume of the captured audio data.
  90 + *
  91 + * Values above 1 will increase the volume, values less than 1 will decrease it.
  92 + */
  93 + var gain = DEFAULT_GAIN
  94 +
  95 + override fun onBufferRequest(originalBuffer: ByteBuffer, audioFormat: Int, channelCount: Int, sampleRate: Int, bytesRead: Int, captureTimeNs: Long): BufferResponse? {
  96 + if (!hasInitialized && audioRecord == null) {
  97 + hasInitialized = true
  98 + initAudioRecord(audioFormat = audioFormat, channelCount = channelCount, sampleRate = sampleRate)
  99 + }
  100 +
  101 + val audioRecord = this.audioRecord ?: return null
  102 + val recordBuffer = this.byteBuffer ?: return null
  103 + audioRecord.read(recordBuffer, recordBuffer.capacity())
  104 +
  105 + if (abs(gain - DEFAULT_GAIN) > MIN_GAIN_CHANGE) {
  106 + recordBuffer.position(0)
  107 + when (audioFormat) {
  108 + AudioFormat.ENCODING_PCM_8BIT -> {
  109 + adjustByteBuffer(recordBuffer, gain)
  110 + }
  111 +
  112 + AudioFormat.ENCODING_PCM_16BIT,
  113 + AudioFormat.ENCODING_DEFAULT,
  114 + -> {
  115 + adjustShortBuffer(recordBuffer.asShortBuffer(), gain)
  116 + }
  117 +
  118 + AudioFormat.ENCODING_PCM_FLOAT -> {
  119 + adjustFloatBuffer(recordBuffer.asFloatBuffer(), gain)
  120 + }
  121 +
  122 + else -> {
  123 + LKLog.w { "Unsupported audio format: $audioFormat" }
  124 + }
  125 + }
  126 + }
  127 + return BufferResponse(recordBuffer)
  128 + }
  129 +
  130 + @SuppressLint("MissingPermission")
  131 + fun initAudioRecord(audioFormat: Int, channelCount: Int, sampleRate: Int): Boolean {
  132 + val audioCaptureConfig = AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
  133 + .apply(captureConfigurator)
  134 + .build()
  135 + val channelMask = if (channelCount == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO
  136 +
  137 + val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelMask, audioFormat)
  138 + if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
  139 + throw IllegalStateException("minBuffer size error: $minBufferSize")
  140 + }
  141 + LKLog.v { "AudioRecord.getMinBufferSize: $minBufferSize" }
  142 +
  143 + val bytesPerFrame = channelCount * getBytesPerSample(audioFormat)
  144 + val framesPerBuffer = sampleRate / 100
  145 + val readBufferCapacity = bytesPerFrame * framesPerBuffer
  146 + val byteBuffer = ByteBuffer.allocateDirect(readBufferCapacity)
  147 + .order(ByteOrder.nativeOrder())
  148 +
  149 + if (!byteBuffer.hasArray()) {
  150 + LKLog.e { "ByteBuffer does not have backing array." }
  151 + return false
  152 + }
  153 +
  154 + this.byteBuffer = byteBuffer
  155 + val bufferSizeInBytes: Int = max(BUFFER_SIZE_FACTOR * minBufferSize, readBufferCapacity)
  156 +
  157 + val audioRecord = AudioRecord.Builder()
  158 + .setAudioFormat(
  159 + AudioFormat.Builder()
  160 + .setEncoding(audioFormat)
  161 + .setSampleRate(sampleRate)
  162 + .setChannelMask(channelMask)
  163 + .build(),
  164 + )
  165 + .setBufferSizeInBytes(bufferSizeInBytes)
  166 + .setAudioPlaybackCaptureConfig(audioCaptureConfig)
  167 + .build()
  168 +
  169 + try {
  170 + audioRecord.startRecording()
  171 + } catch (e: Exception) {
  172 + LKLog.e(e) { "AudioRecord.startRecording failed:" }
  173 + audioRecord.release()
  174 + return false
  175 + }
  176 + if (audioRecord.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
  177 + LKLog.e {
  178 + "AudioRecord.startRecording failed - incorrect state: ${audioRecord.recordingState}"
  179 + }
  180 + return false
  181 + }
  182 +
  183 + this.audioRecord = audioRecord
  184 +
  185 + return true
  186 + }
  187 +
  188 + /**
  189 + * Release any audio resources associated with this capturer.
  190 + * This is not managed by LiveKit, so you must call this function
  191 + * when finished to prevent memory leaks.
  192 + */
  193 + fun releaseAudioResources() {
  194 + val audioRecord = this.audioRecord
  195 + if (audioRecord != null) {
  196 + audioRecord.release()
  197 + this.audioRecord = null
  198 + }
  199 + }
  200 +
  201 + private fun getBytesPerSample(audioFormat: Int): Int {
  202 + return when (audioFormat) {
  203 + AudioFormat.ENCODING_PCM_8BIT -> 1
  204 + AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2
  205 + AudioFormat.ENCODING_PCM_FLOAT -> 4
  206 + AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat")
  207 + else -> throw IllegalArgumentException("Bad audio format $audioFormat")
  208 + }
  209 + }
  210 +
  211 + companion object {
  212 + @RequiresPermission(Manifest.permission.RECORD_AUDIO)
  213 + fun createFromScreenShareTrack(track: Track?): ScreenAudioCapturer? {
  214 + val screenShareTrack = track as? LocalVideoTrack
  215 +
  216 + if (screenShareTrack == null) {
  217 + LKLog.e { "Tried to create screen audio capturer but passed track is not a video track: $track" }
  218 + return null
  219 + }
  220 +
  221 + val capturer = screenShareTrack.capturer as? ScreenCapturerAndroid
  222 +
  223 + if (capturer == null) {
  224 + LKLog.e { "Tried to create screen audio capturer but passed track does not contain a screen capturer: ${screenShareTrack.capturer}" }
  225 + return null
  226 + }
  227 + val mediaProjection = capturer.mediaProjection
  228 +
  229 + if (mediaProjection == null) {
  230 + LKLog.e { "Tried to create screen audio capturer but the capturer doesn't have a media projection. Have you called startCapture?" }
  231 + return null
  232 + }
  233 +
  234 + return ScreenAudioCapturer(mediaProjection)
  235 + }
  236 + }
  237 +
  238 + private fun adjustByteBuffer(
  239 + buffer: ByteBuffer,
  240 + gain: Float,
  241 + ) {
  242 + for (i in 0 until buffer.capacity()) {
  243 + val adjusted = (buffer[i] * gain)
  244 + .roundToInt()
  245 + .coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt())
  246 + buffer.put(i, adjusted.toByte())
  247 + }
  248 + }
  249 +
  250 + private fun adjustShortBuffer(
  251 + buffer: ShortBuffer,
  252 + gain: Float,
  253 + ) {
  254 + for (i in 0 until buffer.capacity()) {
  255 + val adjusted = (buffer[i] * gain)
  256 + .roundToInt()
  257 + .coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt())
  258 + buffer.put(i, adjusted.toShort())
  259 + }
  260 + }
  261 +
  262 + private fun adjustFloatBuffer(
  263 + buffer: FloatBuffer,
  264 + gain: Float,
  265 + ) {
  266 + for (i in 0 until buffer.capacity()) {
  267 + val adjusted = (buffer[i] * gain)
  268 + .coerceIn(-1f, 1f)
  269 + buffer.put(i, adjusted)
  270 + }
  271 + }
  272 +}
  273 +
  274 +typealias AudioPlaybackCaptureConfigurator = (AudioPlaybackCaptureConfiguration.Builder) -> Unit
@@ -48,6 +48,7 @@ object InjectionNames { @@ -48,6 +48,7 @@ object InjectionNames {
48 const val LIB_WEBRTC_INITIALIZATION = "lib_webrtc_initialization" 48 const val LIB_WEBRTC_INITIALIZATION = "lib_webrtc_initialization"
49 49
50 const val LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER = "local_audio_record_samples_dispatcher" 50 const val LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER = "local_audio_record_samples_dispatcher"
  51 + const val LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER = "local_audio_record_samples_dispatcher"
51 52
52 // Overrides 53 // Overrides
53 const val OVERRIDE_OKHTTP = "override_okhttp" 54 const val OVERRIDE_OKHTTP = "override_okhttp"
@@ -25,6 +25,7 @@ import androidx.annotation.Nullable @@ -25,6 +25,7 @@ import androidx.annotation.Nullable
25 import dagger.Module 25 import dagger.Module
26 import dagger.Provides 26 import dagger.Provides
27 import io.livekit.android.LiveKit 27 import io.livekit.android.LiveKit
  28 +import io.livekit.android.audio.AudioBufferCallbackDispatcher
28 import io.livekit.android.audio.AudioProcessingController 29 import io.livekit.android.audio.AudioProcessingController
29 import io.livekit.android.audio.AudioProcessorOptions 30 import io.livekit.android.audio.AudioProcessorOptions
30 import io.livekit.android.audio.AudioRecordSamplesDispatcher 31 import io.livekit.android.audio.AudioRecordSamplesDispatcher
@@ -136,6 +137,13 @@ internal object RTCModule { @@ -136,6 +137,13 @@ internal object RTCModule {
136 } 137 }
137 138
138 @Provides 139 @Provides
  140 + @Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
  141 + @Singleton
  142 + fun localAudioBufferCallbackDispatcher(): AudioBufferCallbackDispatcher {
  143 + return AudioBufferCallbackDispatcher()
  144 + }
  145 +
  146 + @Provides
139 @Singleton 147 @Singleton
140 @JvmSuppressWildcards 148 @JvmSuppressWildcards
141 fun audioModule( 149 fun audioModule(
@@ -151,6 +159,8 @@ internal object RTCModule { @@ -151,6 +159,8 @@ internal object RTCModule {
151 communicationWorkaround: CommunicationWorkaround, 159 communicationWorkaround: CommunicationWorkaround,
152 @Named(InjectionNames.LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER) 160 @Named(InjectionNames.LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER)
153 audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher, 161 audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher,
  162 + @Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
  163 + audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher,
154 ): AudioDeviceModule { 164 ): AudioDeviceModule {
155 if (audioDeviceModuleOverride != null) { 165 if (audioDeviceModuleOverride != null) {
156 return audioDeviceModuleOverride 166 return audioDeviceModuleOverride
@@ -229,6 +239,7 @@ internal object RTCModule { @@ -229,6 +239,7 @@ internal object RTCModule {
229 // VOICE_COMMUNICATION needs to be used for echo cancelling. 239 // VOICE_COMMUNICATION needs to be used for echo cancelling.
230 .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) 240 .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
231 .setAudioAttributes(audioOutputAttributes) 241 .setAudioAttributes(audioOutputAttributes)
  242 + .setAudioBufferCallback(audioBufferCallbackDispatcher)
232 243
233 moduleCustomizer?.invoke(builder) 244 moduleCustomizer?.invoke(builder)
234 return builder.createAudioDeviceModule() 245 return builder.createAudioDeviceModule()
@@ -529,7 +529,7 @@ internal constructor( @@ -529,7 +529,7 @@ internal constructor(
529 options = options, 529 options = options,
530 ) 530 )
531 addTrackPublication(publication) 531 addTrackPublication(publication)
532 - LKLog.e { "add track publication $publication" } 532 + LKLog.v { "add track publication $publication" }
533 533
534 publishListener?.onPublishSuccess(publication) 534 publishListener?.onPublishSuccess(publication)
535 internalListener?.onTrackPublished(publication, this) 535 internalListener?.onTrackPublished(publication, this)
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 package io.livekit.android.room.provisions 17 package io.livekit.android.room.provisions
18 18
19 import livekit.org.webrtc.EglBase 19 import livekit.org.webrtc.EglBase
  20 +import livekit.org.webrtc.audio.AudioDeviceModule
20 import javax.inject.Inject 21 import javax.inject.Inject
21 import javax.inject.Provider 22 import javax.inject.Provider
22 23
@@ -32,7 +33,11 @@ class LKObjects @@ -32,7 +33,11 @@ class LKObjects
32 @Inject 33 @Inject
33 constructor( 34 constructor(
34 private val eglBaseProvider: Provider<EglBase>, 35 private val eglBaseProvider: Provider<EglBase>,
  36 + private val audioDeviceModuleProvider: Provider<AudioDeviceModule>,
35 ) { 37 ) {
36 val eglBase: EglBase 38 val eglBase: EglBase
37 get() = eglBaseProvider.get() 39 get() = eglBaseProvider.get()
  40 +
  41 + val audioDeviceModule: AudioDeviceModule
  42 + get() = audioDeviceModuleProvider.get()
38 } 43 }
@@ -23,11 +23,15 @@ import androidx.core.content.ContextCompat @@ -23,11 +23,15 @@ import androidx.core.content.ContextCompat
23 import dagger.assisted.Assisted 23 import dagger.assisted.Assisted
24 import dagger.assisted.AssistedFactory 24 import dagger.assisted.AssistedFactory
25 import dagger.assisted.AssistedInject 25 import dagger.assisted.AssistedInject
  26 +import io.livekit.android.audio.AudioBufferCallback
  27 +import io.livekit.android.audio.AudioBufferCallbackDispatcher
26 import io.livekit.android.audio.AudioProcessingController 28 import io.livekit.android.audio.AudioProcessingController
27 import io.livekit.android.audio.AudioRecordSamplesDispatcher 29 import io.livekit.android.audio.AudioRecordSamplesDispatcher
  30 +import io.livekit.android.audio.MixerAudioBufferCallback
28 import io.livekit.android.dagger.InjectionNames 31 import io.livekit.android.dagger.InjectionNames
29 import io.livekit.android.room.participant.LocalParticipant 32 import io.livekit.android.room.participant.LocalParticipant
30 import io.livekit.android.util.FlowObservable 33 import io.livekit.android.util.FlowObservable
  34 +import io.livekit.android.util.LKLog
31 import io.livekit.android.util.flow 35 import io.livekit.android.util.flow
32 import io.livekit.android.util.flowDelegate 36 import io.livekit.android.util.flowDelegate
33 import kotlinx.coroutines.CoroutineDispatcher 37 import kotlinx.coroutines.CoroutineDispatcher
@@ -64,6 +68,8 @@ constructor( @@ -64,6 +68,8 @@ constructor(
64 private val dispatcher: CoroutineDispatcher, 68 private val dispatcher: CoroutineDispatcher,
65 @Named(InjectionNames.LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER) 69 @Named(InjectionNames.LOCAL_AUDIO_RECORD_SAMPLES_DISPATCHER)
66 private val audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher, 70 private val audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher,
  71 + @Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
  72 + private val audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher,
67 ) : AudioTrack(name, mediaTrack) { 73 ) : AudioTrack(name, mediaTrack) {
68 /** 74 /**
69 * To only be used for flow delegate scoping, and should not be cancelled. 75 * To only be used for flow delegate scoping, and should not be cancelled.
@@ -99,6 +105,16 @@ constructor( @@ -99,6 +105,16 @@ constructor(
99 } 105 }
100 106
101 /** 107 /**
  108 + * Use this method to mix in custom audio.
  109 + *
  110 + * See [MixerAudioBufferCallback] for automatic handling of mixing in
  111 + * the provided audio data.
  112 + */
  113 + fun setAudioBufferCallback(callback: AudioBufferCallback?) {
  114 + audioBufferCallbackDispatcher.bufferCallback = callback
  115 + }
  116 +
  117 + /**
102 * Changes can be observed by using [io.livekit.android.util.flow] 118 * Changes can be observed by using [io.livekit.android.util.flow]
103 */ 119 */
104 @FlowObservable 120 @FlowObservable
@@ -158,7 +174,7 @@ constructor( @@ -158,7 +174,7 @@ constructor(
158 if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != 174 if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) !=
159 PackageManager.PERMISSION_GRANTED 175 PackageManager.PERMISSION_GRANTED
160 ) { 176 ) {
161 - throw SecurityException("Record audio permissions are required to create an audio track.") 177 + LKLog.w { "Record audio permissions not granted, microphone recording will not be used." }
162 } 178 }
163 179
164 val audioConstraints = MediaConstraints() 180 val audioConstraints = MediaConstraints()
@@ -17,11 +17,23 @@ @@ -17,11 +17,23 @@
17 package io.livekit.android.test.mock 17 package io.livekit.android.test.mock
18 18
19 import io.livekit.android.room.provisions.LKObjects 19 import io.livekit.android.room.provisions.LKObjects
  20 +import livekit.org.webrtc.audio.AudioDeviceModule
20 21
21 object MockLKObjects { 22 object MockLKObjects {
22 fun get(): LKObjects { 23 fun get(): LKObjects {
23 return LKObjects( 24 return LKObjects(
24 eglBaseProvider = { MockEglBase() }, 25 eglBaseProvider = { MockEglBase() },
  26 + audioDeviceModuleProvider = {
  27 + object : AudioDeviceModule {
  28 + override fun getNativeAudioDeviceModulePointer(): Long = 1
  29 +
  30 + override fun release() {}
  31 +
  32 + override fun setSpeakerMute(p0: Boolean) {}
  33 +
  34 + override fun setMicrophoneMute(p0: Boolean) {}
  35 + }
  36 + },
25 ) 37 )
26 } 38 }
27 } 39 }
@@ -20,6 +20,7 @@ import android.content.Context @@ -20,6 +20,7 @@ import android.content.Context
20 import android.javax.sdp.SdpFactory 20 import android.javax.sdp.SdpFactory
21 import dagger.Module 21 import dagger.Module
22 import dagger.Provides 22 import dagger.Provides
  23 +import io.livekit.android.audio.AudioBufferCallbackDispatcher
23 import io.livekit.android.audio.AudioProcessingController 24 import io.livekit.android.audio.AudioProcessingController
24 import io.livekit.android.audio.AudioRecordSamplesDispatcher 25 import io.livekit.android.audio.AudioRecordSamplesDispatcher
25 import io.livekit.android.dagger.CapabilitiesGetter 26 import io.livekit.android.dagger.CapabilitiesGetter
@@ -40,6 +41,13 @@ import javax.inject.Singleton @@ -40,6 +41,13 @@ import javax.inject.Singleton
40 object TestRTCModule { 41 object TestRTCModule {
41 42
42 @Provides 43 @Provides
  44 + @Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
  45 + @Singleton
  46 + fun localAudioBufferCallbackDispatcher(): AudioBufferCallbackDispatcher {
  47 + return AudioBufferCallbackDispatcher()
  48 + }
  49 +
  50 + @Provides
43 @Singleton 51 @Singleton
44 fun eglBase(): EglBase { 52 fun eglBase(): EglBase {
45 return MockEglBase() 53 return MockEglBase()
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 package io.livekit.android.test.mock.room.track 17 package io.livekit.android.test.mock.room.track
18 18
  19 +import io.livekit.android.audio.AudioBufferCallbackDispatcher
19 import io.livekit.android.audio.AudioProcessingController 20 import io.livekit.android.audio.AudioProcessingController
20 import io.livekit.android.audio.AudioRecordSamplesDispatcher 21 import io.livekit.android.audio.AudioRecordSamplesDispatcher
21 import io.livekit.android.room.track.LocalAudioTrack 22 import io.livekit.android.room.track.LocalAudioTrack
@@ -36,6 +37,7 @@ fun MockE2ETest.createMockLocalAudioTrack( @@ -36,6 +37,7 @@ fun MockE2ETest.createMockLocalAudioTrack(
36 audioProcessingController: AudioProcessingController = MockAudioProcessingController(), 37 audioProcessingController: AudioProcessingController = MockAudioProcessingController(),
37 dispatcher: CoroutineDispatcher = coroutineRule.dispatcher, 38 dispatcher: CoroutineDispatcher = coroutineRule.dispatcher,
38 audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher = AudioRecordSamplesDispatcher(), 39 audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher = AudioRecordSamplesDispatcher(),
  40 + audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher = AudioBufferCallbackDispatcher(),
39 ): LocalAudioTrack { 41 ): LocalAudioTrack {
40 return LocalAudioTrack( 42 return LocalAudioTrack(
41 name = name, 43 name = name,
@@ -44,5 +46,6 @@ fun MockE2ETest.createMockLocalAudioTrack( @@ -44,5 +46,6 @@ fun MockE2ETest.createMockLocalAudioTrack(
44 audioProcessingController = audioProcessingController, 46 audioProcessingController = audioProcessingController,
45 dispatcher = dispatcher, 47 dispatcher = dispatcher,
46 audioRecordSamplesDispatcher = audioRecordSamplesDispatcher, 48 audioRecordSamplesDispatcher = audioRecordSamplesDispatcher,
  49 + audioBufferCallbackDispatcher = audioBufferCallbackDispatcher,
47 ) 50 )
48 } 51 }
@@ -29,3 +29,4 @@ include ':sample-app-record-local' @@ -29,3 +29,4 @@ include ':sample-app-record-local'
29 include ':examples:selfie-segmentation' 29 include ':examples:selfie-segmentation'
30 include ':livekit-android-test' 30 include ':livekit-android-test'
31 include ':livekit-android-camerax' 31 include ':livekit-android-camerax'
  32 +include ':examples:screenshare-audio'