Compare commits

...

3 Commits

Author SHA1 Message Date
3d3f8710f2 feat: use camerax with webrtc lib 2025-04-01 08:20:16 +02:00
645f8e2f04 chore: upgrade project dependencies 2025-04-01 08:14:41 +02:00
60f146afbc feat: listen to websocket 2025-04-01 08:13:06 +02:00
11 changed files with 349 additions and 86 deletions

View File

@ -52,6 +52,7 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.websockets)
implementation(libs.androidx.camera.core) implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.lifecycle)
@ -60,7 +61,7 @@ dependencies {
implementation(libs.androidx.camera.mlkit.vision) implementation(libs.androidx.camera.mlkit.vision)
implementation(libs.androidx.camera.extensions) implementation(libs.androidx.camera.extensions)
implementation(libs.material3) implementation(libs.material3)
implementation(libs.opencsv) implementation(libs.stream.webrtc.android)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -5,6 +5,9 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.camera.core.ImageAnalysis
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -21,8 +24,13 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.tvcontroller.ui.theme.TVControllerTheme import com.example.tvcontroller.ui.theme.TVControllerTheme
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.CameraService import com.example.tvcontroller.services.CameraService
import com.example.tvcontroller.services.ControllerService import com.example.tvcontroller.services.ControllerService
@ -30,6 +38,10 @@ import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.views.CameraView import com.example.tvcontroller.ui.views.CameraView
import com.example.tvcontroller.ui.views.RemoteView import com.example.tvcontroller.ui.views.RemoteView
import com.example.tvcontroller.ui.views.SettingsView import com.example.tvcontroller.ui.views.SettingsView
import com.example.tvcontroller.webrtc.CameraXCapturer
import com.example.tvcontroller.webrtc.RtcPeerConnection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
const val TAG = "MainActivity" const val TAG = "MainActivity"
@ -42,10 +54,22 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
bluetoothService = BluetoothService(this.applicationContext) val lifecycleOwner: LifecycleOwner = this
deviceService = DeviceService(this.applicationContext) val rtcPeerConnection = RtcPeerConnection(applicationContext)
cameraService = CameraService(this.applicationContext) val cameraController =
controllerService = ControllerService(this.applicationContext, bluetoothService) LifecycleCameraController(applicationContext).apply {
setEnabledUseCases(CameraController.IMAGE_ANALYSIS)
setImageAnalysisAnalyzer(
ContextCompat.getMainExecutor(applicationContext),
rtcPeerConnection.cameraXCapturer
)
bindToLifecycle(lifecycleOwner)
}
bluetoothService = BluetoothService(applicationContext)
deviceService = DeviceService(applicationContext)
cameraService = CameraService(applicationContext)
controllerService = ControllerService(applicationContext, bluetoothService)
checkPermissions() checkPermissions()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
@ -53,7 +77,9 @@ class MainActivity : ComponentActivity() {
TvControllerApp( TvControllerApp(
deviceService = deviceService, deviceService = deviceService,
controllerService = controllerService, controllerService = controllerService,
bluetoothService = bluetoothService bluetoothService = bluetoothService,
rtcPeerConnection = rtcPeerConnection,
cameraController = cameraController
) )
} }
} }
@ -76,7 +102,9 @@ fun TvControllerApp(
navController: NavHostController = rememberNavController(), navController: NavHostController = rememberNavController(),
deviceService: DeviceService, deviceService: DeviceService,
controllerService: ControllerService, controllerService: ControllerService,
bluetoothService: BluetoothService bluetoothService: BluetoothService,
rtcPeerConnection: RtcPeerConnection,
cameraController: LifecycleCameraController
) { ) {
val backStackEntry by navController.currentBackStackEntryAsState() val backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen = Screen.valueOf(backStackEntry?.destination?.route ?: Screen.Camera.name) val currentScreen = Screen.valueOf(backStackEntry?.destination?.route ?: Screen.Camera.name)
@ -122,11 +150,11 @@ fun TvControllerApp(
}) { innerPadding -> }) { innerPadding ->
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Remote.name, startDestination = Screen.Settings.name,
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { ) {
composable(route = Screen.Camera.name) { composable(route = Screen.Camera.name) {
CameraView() CameraView(cameraController)
} }
composable(route = Screen.Remote.name) { composable(route = Screen.Remote.name) {
RemoteView( RemoteView(
@ -136,7 +164,8 @@ fun TvControllerApp(
composable(route = Screen.Settings.name) { composable(route = Screen.Settings.name) {
SettingsView( SettingsView(
deviceService = deviceService, deviceService = deviceService,
bluetoothService = bluetoothService bluetoothService = bluetoothService,
rtcPeerConnection = rtcPeerConnection
) )
} }
} }

View File

@ -2,9 +2,7 @@ package com.example.tvcontroller.services
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.example.tvcontroller.R
import com.example.tvcontroller.data.RemoteCommand import com.example.tvcontroller.data.RemoteCommand
import com.opencsv.CSVReader
import org.json.JSONObject import org.json.JSONObject
@ -38,21 +36,6 @@ class ControllerService(
} }
fun loadCommands() { fun loadCommands() {
Log.i("ControllerService", "Loading commands");
var inputStream = context.resources.openRawResource(R.raw.samsung)
var csvReader = CSVReader(inputStream.reader())
csvReader.forEach { nextLine ->
if (nextLine.size < 5) return@forEach
if (nextLine[0] == "functionname") return@forEach
var remoteCommand = RemoteCommand()
remoteCommand.functionName = nextLine[0]
remoteCommand.protocol = "samsung"
remoteCommand.device = nextLine[2]
remoteCommand.subdevice = nextLine[3]
remoteCommand.function = nextLine[4]
samsungCommands[remoteCommand.functionName!!] = remoteCommand
}
Log.i("ControllerService", "Commands loaded: ${samsungCommands.size}")
} }
companion object { companion object {

View File

@ -8,18 +8,25 @@ import io.ktor.client.engine.cio.*
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.plugins.cookies.HttpCookies import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocket
import io.ktor.client.request.headers import io.ktor.client.request.headers
import io.ktor.client.request.request import io.ktor.client.request.request
import io.ktor.client.request.setBody import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.runBlocking
import org.json.JSONObject import org.json.JSONObject
private const val SHARED_PREFERENCES_NAME = "devices"; private const val SHARED_PREFERENCES_NAME = "devices";
private const val TAG = "DeviceService"
class DeviceService(private val context: Context) { class DeviceService(private val context: Context) {
private var client = HttpClient(CIO) { private var client = HttpClient(CIO) {
install(HttpCookies) install(HttpCookies)
install(WebSockets)
} }
private var serverAddress: String = "" private var serverAddress: String = ""
private var token: String = "" private var token: String = ""
@ -30,7 +37,7 @@ class DeviceService(private val context: Context) {
} }
suspend fun registerIntegration(name: String, code: String) { suspend fun registerIntegration(name: String, code: String) {
Log.i("DeviceService", "Creating integration for $name with code $code at $serverAddress") Log.i(TAG, "Creating integration for $name with code $code at $serverAddress")
val requestJson = JSONObject() val requestJson = JSONObject()
requestJson.put("name", name) requestJson.put("name", name)
requestJson.put("code", code) requestJson.put("code", code)
@ -49,21 +56,21 @@ class DeviceService(private val context: Context) {
val responseJson = JSONObject(body) val responseJson = JSONObject(body)
if (response.status.value != 200) { if (response.status.value != 200) {
val error = responseJson.getString("error") val error = responseJson.getString("error")
Log.e("DeviceService", "Error getting integration: ${response.status.value} $error") Log.e(TAG, "Error getting integration: ${response.status.value} $error")
return return
} }
token = responseJson.getString("token") token = responseJson.getString("token")
deviceId = responseJson.getString("id") deviceId = responseJson.getString("id")
savePreferences() savePreferences()
Log.i("DeviceService", "Response: ${response.status.value} $body") Log.i(TAG, "Response: ${response.status.value} $body")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("DeviceService", "Error registering integration", e) Log.e(TAG, "Error registering integration", e)
} }
} }
suspend fun getIntegration(): Integration? { suspend fun getIntegration(): Integration? {
Log.i("DeviceService", "Getting integration $deviceId at $serverAddress") Log.i(TAG, "Getting integration $deviceId at $serverAddress")
try { try {
val response: HttpResponse = val response: HttpResponse =
client.request("http://$serverAddress/api/integrations/$deviceId") { client.request("http://$serverAddress/api/integrations/$deviceId") {
@ -77,7 +84,7 @@ class DeviceService(private val context: Context) {
val responseJson = JSONObject(body) val responseJson = JSONObject(body)
if (response.status.value != 200) { if (response.status.value != 200) {
val error = responseJson.getString("error") val error = responseJson.getString("error")
Log.e("DeviceService", "Error getting integration: ${response.status.value} $error") Log.e(TAG, "Error getting integration: ${response.status.value} $error")
return null return null
} }
val integration = Integration( val integration = Integration(
@ -86,7 +93,7 @@ class DeviceService(private val context: Context) {
) )
return integration return integration
} catch (e: Exception) { } catch (e: Exception) {
Log.e("DeviceService", "Error getting integration", e) Log.e(TAG, "Error getting integration", e)
} }
return null return null
} }
@ -96,7 +103,7 @@ class DeviceService(private val context: Context) {
serverAddress = sharedPreferences.getString("server_address", "")!! serverAddress = sharedPreferences.getString("server_address", "")!!
token = sharedPreferences.getString("token", "")!! token = sharedPreferences.getString("token", "")!!
deviceId = sharedPreferences.getString("device_id", "")!! deviceId = sharedPreferences.getString("device_id", "")!!
Log.i("DeviceService", "Loaded preferences: $serverAddress $token") Log.i(TAG, "Loaded preferences: $serverAddress $token")
} }
private fun savePreferences() { private fun savePreferences() {
@ -110,6 +117,26 @@ class DeviceService(private val context: Context) {
} }
} }
fun connect() {
Log.i(TAG, "Connecting to websocket at $serverAddress")
runBlocking {
// split server address into host and port
val (host, port) = serverAddress.split(":")
// if no port is specified, assume 80
val portInt = if (port.isEmpty()) 80 else port.toInt()
client.webSocket(method = HttpMethod.Get, host = host, port = portInt, path = "/ws") {
Log.i(TAG, "Listening for incoming websocket messages")
while (true) {
val frame = incoming.receive()
if (frame is Frame.Text) {
val message = frame.readText()
Log.i(TAG, "Received message: $message")
}
}
}
}
}
fun setServerAddress(url: String) { fun setServerAddress(url: String) {
serverAddress = url serverAddress = url
} }

View File

@ -12,13 +12,11 @@ fun CameraPreview(
controller: LifecycleCameraController, controller: LifecycleCameraController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { factory = {
PreviewView(it).apply { PreviewView(it).apply {
this.controller = controller this.controller = controller
controller.bindToLifecycle(lifecycleOwner)
} }
}, },
) )

View File

@ -1,34 +1,26 @@
package com.example.tvcontroller.ui.views package com.example.tvcontroller.ui.views
import androidx.camera.core.ImageAnalysis
import androidx.camera.view.CameraController import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.example.tvcontroller.ui.components.CameraPreview import com.example.tvcontroller.ui.components.CameraPreview
@Composable @Composable
fun CameraView() { fun CameraView(cameraController: LifecycleCameraController) {
val context = LocalContext.current
val controller = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(all = 16.dp), .padding(all = 16.dp),
) { ) {
CameraPreview(controller = controller, modifier = Modifier.fillMaxSize()) CameraPreview(controller = cameraController, modifier = Modifier.fillMaxSize())
} }
} }

View File

@ -32,24 +32,20 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.tvcontroller.R import com.example.tvcontroller.R
import com.example.tvcontroller.Settings import com.example.tvcontroller.Settings
import com.example.tvcontroller.ui.views.SettingsViewModel
import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.CONNECT_CONTROLLER_VIEW import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.CONNECT_CONTROLLER_VIEW
import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.MAIN_SETTINGS_VIEW import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.MAIN_SETTINGS_VIEW
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.ControllerService
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.webrtc.RtcPeerConnection
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsView( fun SettingsView(
deviceService: DeviceService, deviceService: DeviceService,
bluetoothService: BluetoothService, bluetoothService: BluetoothService
rtcPeerConnection: RtcPeerConnection
) { ) {
val viewModel = viewModel<SettingsViewModel>( val viewModel = viewModel<SettingsViewModel>(
factory = SettingsViewModel.provideFactory( factory = SettingsViewModel.provideFactory(
deviceService, bluetoothService, rtcPeerConnection deviceService, bluetoothService
) )
) )
val navController = rememberNavController() val navController = rememberNavController()
@ -121,13 +117,6 @@ fun SettingsView(
stringResource(id = R.string.connect_button_label) stringResource(id = R.string.connect_button_label)
) )
} }
Button(
onClick = { viewModel.connectRtcPeerConnection() },
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Connect RTC Peer")
}
} }
} }

View File

@ -12,14 +12,12 @@ import com.example.tvcontroller.Settings
import com.example.tvcontroller.data.BluetoothDevice import com.example.tvcontroller.data.BluetoothDevice
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.webrtc.RtcPeerConnection
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SettingsViewModel( class SettingsViewModel(
private val deviceService: DeviceService, private val deviceService: DeviceService,
private val bluetoothService: BluetoothService, private val bluetoothService: BluetoothService
private val rtcPeerConnection: RtcPeerConnection
) : ViewModel() { ) : ViewModel() {
var serverAddress by mutableStateOf(deviceService.getServerAddress()) var serverAddress by mutableStateOf(deviceService.getServerAddress())
private set private set
@ -54,12 +52,7 @@ class SettingsViewModel(
deviceService.registerIntegration(deviceName, registrationCode) deviceService.registerIntegration(deviceName, registrationCode)
updateConnectionState() updateConnectionState()
updateDeviceInfo() updateDeviceInfo()
} deviceService.connect()
}
fun connectRtcPeerConnection() {
viewModelScope.launch(Dispatchers.IO) {
rtcPeerConnection.connect()
} }
} }
@ -112,12 +105,11 @@ class SettingsViewModel(
fun provideFactory( fun provideFactory(
deviceService: DeviceService, deviceService: DeviceService,
bluetoothService: BluetoothService, bluetoothService: BluetoothService
rtcPeerConnection: RtcPeerConnection
) = object : ViewModelProvider.Factory { ) = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SettingsViewModel(deviceService, bluetoothService, rtcPeerConnection) as T return SettingsViewModel(deviceService, bluetoothService) as T
} }
} }
} }

View File

@ -0,0 +1,113 @@
package com.example.tvcontroller.webrtc
import android.graphics.ImageFormat.YUV_420_888
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import org.webrtc.CapturerObserver
import org.webrtc.VideoFrame
import java.nio.ByteBuffer
class CameraXCapturer : ImageAnalysis.Analyzer {
private var capturerObserver: CapturerObserver? = null
fun setCapturerObserver(capturerObserver: CapturerObserver) {
this.capturerObserver = capturerObserver
}
override fun analyze(image: ImageProxy) {
if (image.format != YUV_420_888) throw Exception("Unsupported format")
Log.i("CameraXCapturer", "Received image from CameraX")
var videoFrame = imageProxyToVideoFrame(image)
capturerObserver?.onFrameCaptured(videoFrame) ?: image.close()
}
fun imageProxyToVideoFrame(image: ImageProxy): VideoFrame {
var buffer = object : VideoFrame.I420Buffer {
private var refCount = 0
override fun getWidth(): Int {
return image.width
}
override fun getHeight(): Int {
return image.height
}
override fun toI420(): VideoFrame.I420Buffer? {
return this
}
override fun retain() {
refCount++
}
override fun release() {
refCount--
if (refCount == 0) {
image.close()
}
}
override fun cropAndScale(
cropX: Int,
cropY: Int,
cropWidth: Int,
cropHeight: Int,
scaleWidth: Int,
scaleHeight: Int
): VideoFrame.Buffer? {
return this
}
override fun getDataY(): ByteBuffer? {
val format = image.format
if (format == YUV_420_888) {
return image.planes[0].buffer
}
return image.planes[0].buffer
}
override fun getDataU(): ByteBuffer? {
val format = image.format
if (format == YUV_420_888) {
return image.planes[1].buffer
}
return image.planes[0].buffer
}
override fun getDataV(): ByteBuffer? {
val format = image.format
if (format == YUV_420_888) {
return image.planes[2].buffer
}
return image.planes[0].buffer
}
override fun getStrideY(): Int {
val format = image.format
if (format == YUV_420_888) {
return image.planes[0].pixelStride
}
return image.planes[0].pixelStride
}
override fun getStrideU(): Int {
val format = image.format
if (format == YUV_420_888) {
return image.planes[1].pixelStride
}
return image.planes[0].pixelStride
}
override fun getStrideV(): Int {
val format = image.format
if (format == YUV_420_888) {
return image.planes[2].pixelStride
}
return image.planes[0].pixelStride
}
}
var videoFrame = VideoFrame(buffer, 0, 0)
return videoFrame
}
}

View File

@ -0,0 +1,138 @@
package com.example.tvcontroller.webrtc
import android.content.Context
import android.hardware.camera2.CameraManager
import android.util.Log
import org.webrtc.AudioTrack
import org.webrtc.Camera2Capturer
import org.webrtc.DataChannel
import org.webrtc.IceCandidate
import org.webrtc.MediaConstraints
import org.webrtc.MediaStream
import org.webrtc.PeerConnection
import org.webrtc.PeerConnectionFactory
import org.webrtc.PeerConnectionFactory.InitializationOptions
import org.webrtc.SdpObserver
import org.webrtc.SessionDescription
import org.webrtc.VideoTrack
const val TAG = "RtcPeerConnection"
class RtcPeerConnection(private val context: Context) : PeerConnection.Observer {
private val peerConnectionFactory by lazy { initializeFactory() }
private val iceServers by lazy { initializeIceServers() }
val cameraXCapturer by lazy { CameraXCapturer() }
private val audioTrack by lazy { createAudioTrack() }
private val videoTrack by lazy { createVideoTrack() }
private var peerConnection: PeerConnection? = null
private fun initializeFactory(): PeerConnectionFactory {
val initOptions = InitializationOptions.builder(context).createInitializationOptions()
PeerConnectionFactory.initialize(initOptions)
val options = PeerConnectionFactory.Options()
val peerConnectionFactory =
PeerConnectionFactory.builder().setOptions(options).createPeerConnectionFactory()
return peerConnectionFactory
}
private fun initializeIceServers(): ArrayList<PeerConnection.IceServer> {
val iceServers = ArrayList<PeerConnection.IceServer>()
iceServers.add(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
)
return iceServers
}
private fun createAudioTrack(): AudioTrack {
val audioConstraints = MediaConstraints()
val audioSource = peerConnectionFactory.createAudioSource(audioConstraints)
val localAudioTrack = peerConnectionFactory.createAudioTrack("audio_track", audioSource)
return localAudioTrack
}
private fun createVideoTrack(): VideoTrack {
val videoSource = peerConnectionFactory.createVideoSource(false)
cameraXCapturer.setCapturerObserver(videoSource.capturerObserver)
val localVideoTrack = peerConnectionFactory.createVideoTrack("video_track", videoSource)
localVideoTrack.setEnabled(true)
return localVideoTrack
}
fun connect() {
Log.i(TAG, "Connecting rtc peer connection")
var rtcConfig = PeerConnection.RTCConfiguration(iceServers)
var peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, this)
var mediaConstraints = MediaConstraints()
mediaConstraints.mandatory.add(
MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")
);
mediaConstraints.mandatory.add(
MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")
);
peerConnection?.addTrack(audioTrack)
peerConnection?.addTrack(videoTrack)
peerConnection?.createOffer(object : SdpObserver {
override fun onCreateSuccess(sessionDescription: SessionDescription?) {
Log.i(TAG, "onCreateSuccess: ${sessionDescription?.description}")
}
override fun onSetSuccess() {
Log.i(TAG, "onSetSuccess")
TODO("Not yet implemented")
}
override fun onCreateFailure(p0: String?) {
Log.i(TAG, "onCreateFailure: $p0")
TODO("Not yet implemented")
}
override fun onSetFailure(p0: String?) {
Log.i(TAG, "onSetFailure: $p0")
TODO("Not yet implemented")
}
}, mediaConstraints)
}
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
Log.i(TAG, "onSignalingChange: $p0")
}
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
Log.i(TAG, "onIceConnectionChange: $p0")
}
override fun onIceConnectionReceivingChange(p0: Boolean) {
Log.i(TAG, "onIceConnectionReceivingChange: $p0")
}
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
Log.i(TAG, "onIceGatheringChange: $p0")
}
override fun onIceCandidate(p0: IceCandidate?) {
Log.i(TAG, "onIceCandidate: $p0")
}
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate?>?) {
Log.i(TAG, "onIceCandidatesRemoved: $p0")
}
override fun onAddStream(p0: MediaStream?) {
Log.i(TAG, "onAddStream: $p0")
}
override fun onRemoveStream(p0: MediaStream?) {
Log.i(TAG, "onRemoveStream: $p0")
}
override fun onDataChannel(p0: DataChannel?) {
Log.i(TAG, "onDataChannel: $p0")
}
override fun onRenegotiationNeeded() {
Log.i(TAG, "onRenegotiationNeeded")
}
}

View File

@ -1,18 +1,18 @@
[versions] [versions]
agp = "8.9.0" agp = "8.9.1"
cameraCore = "1.4.1" cameraCore = "1.4.2"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.10.1" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.2.1"
espressoCore = "3.5.1" espressoCore = "3.6.1"
ktor = "3.1.0" ktor = "3.1.0"
lifecycleRuntimeKtx = "2.6.1" lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.8.0" activityCompose = "1.10.1"
composeBom = "2024.04.01" composeBom = "2025.03.01"
material3 = "1.4.0-alpha10" material3 = "1.4.0-alpha11"
navigationCompose = "2.8.4" navigationCompose = "2.8.9"
opencsv = "4.6" streamWebrtcAndroid = "1.3.8"
[libraries] [libraries]
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" } androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" }
@ -39,8 +39,9 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" } stream-webrtc-android = { module = "io.getstream:stream-webrtc-android", version.ref = "streamWebrtcAndroid" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }