refactor: structure webrtc code properly
This commit is contained in:
parent
8bf72a23ea
commit
11838f0abd
@ -5,8 +5,6 @@ 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.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
|
||||||
@ -19,54 +17,46 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
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 androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.example.tvcontroller.client.WebClient
|
||||||
|
import com.example.tvcontroller.client.WebsocketClient
|
||||||
import com.example.tvcontroller.services.BluetoothService
|
import com.example.tvcontroller.services.BluetoothService
|
||||||
import com.example.tvcontroller.services.CameraService
|
|
||||||
import com.example.tvcontroller.services.ControllerService
|
import com.example.tvcontroller.services.ControllerService
|
||||||
import com.example.tvcontroller.services.DeviceService
|
import com.example.tvcontroller.services.DeviceService
|
||||||
|
import com.example.tvcontroller.services.webrtc.WebRtcService
|
||||||
import com.example.tvcontroller.ui.theme.TVControllerTheme
|
import com.example.tvcontroller.ui.theme.TVControllerTheme
|
||||||
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.RtcPeerConnection
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
const val TAG = "MainActivity"
|
const val TAG = "MainActivity"
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var bluetoothService: BluetoothService
|
private val webClient by lazy { WebClient() }
|
||||||
private lateinit var deviceService: DeviceService
|
private val websocketClient by lazy { WebsocketClient(webClient.client) }
|
||||||
private lateinit var cameraService: CameraService
|
private val webRtcService by lazy { WebRtcService(applicationContext, websocketClient) }
|
||||||
private lateinit var controllerService: ControllerService
|
private val bluetoothService by lazy { BluetoothService(applicationContext) }
|
||||||
|
private val deviceService by lazy { DeviceService(applicationContext, webClient) }
|
||||||
|
private val controllerService by lazy { ControllerService(bluetoothService) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val lifecycleOwner: LifecycleOwner = this
|
|
||||||
val rtcPeerConnection = RtcPeerConnection(applicationContext)
|
|
||||||
val cameraController =
|
|
||||||
LifecycleCameraController(applicationContext).apply {
|
|
||||||
setEnabledUseCases(CameraController.IMAGE_ANALYSIS)
|
|
||||||
bindToLifecycle(lifecycleOwner)
|
|
||||||
}
|
|
||||||
bluetoothService = BluetoothService(applicationContext)
|
|
||||||
deviceService = DeviceService(applicationContext)
|
|
||||||
deviceService.rtcPeerConnection = rtcPeerConnection
|
|
||||||
rtcPeerConnection.deviceService = deviceService
|
|
||||||
cameraService = CameraService(applicationContext)
|
|
||||||
controllerService = ControllerService(applicationContext, bluetoothService)
|
|
||||||
checkPermissions()
|
checkPermissions()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
deviceService.initialize()
|
deviceService.initialize()
|
||||||
|
if (deviceService.serverAddress.isEmpty() || deviceService.token.isEmpty()) return@launch
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
websocketClient.connect(deviceService.serverAddress, deviceService.token)
|
||||||
|
}
|
||||||
|
webRtcService.connect()
|
||||||
}
|
}
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
@ -75,8 +65,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
deviceService = deviceService,
|
deviceService = deviceService,
|
||||||
controllerService = controllerService,
|
controllerService = controllerService,
|
||||||
bluetoothService = bluetoothService,
|
bluetoothService = bluetoothService,
|
||||||
rtcPeerConnection = rtcPeerConnection,
|
webRtcService = webRtcService
|
||||||
cameraController = cameraController
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,9 +73,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun checkPermissions() {
|
private fun checkPermissions() {
|
||||||
Log.i(TAG, "Checking permissions")
|
Log.i(TAG, "Checking permissions")
|
||||||
if (!cameraService.hasRequiredPermissions()) {
|
|
||||||
ActivityCompat.requestPermissions(this, CameraService.CAMERAX_PERMISSIONS, 0)
|
|
||||||
}
|
|
||||||
if (!bluetoothService.hasRequiredPermissions()) {
|
if (!bluetoothService.hasRequiredPermissions()) {
|
||||||
Log.i(TAG, "Requesting Bluetooth permissions")
|
Log.i(TAG, "Requesting Bluetooth permissions")
|
||||||
ActivityCompat.requestPermissions(this, BluetoothService.BLUETOOTH_PERMISSIONS, 0)
|
ActivityCompat.requestPermissions(this, BluetoothService.BLUETOOTH_PERMISSIONS, 0)
|
||||||
@ -100,8 +86,7 @@ fun TvControllerApp(
|
|||||||
deviceService: DeviceService,
|
deviceService: DeviceService,
|
||||||
controllerService: ControllerService,
|
controllerService: ControllerService,
|
||||||
bluetoothService: BluetoothService,
|
bluetoothService: BluetoothService,
|
||||||
rtcPeerConnection: RtcPeerConnection,
|
webRtcService: WebRtcService
|
||||||
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)
|
||||||
@ -111,37 +96,25 @@ fun TvControllerApp(
|
|||||||
Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
|
Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
onClick = { navController.navigate(Screen.Camera.name) },
|
onClick = { navController.navigate(Screen.Camera.name) }, icon = {
|
||||||
icon = {
|
|
||||||
Icon(
|
Icon(
|
||||||
baselineCamera24,
|
baselineCamera24, contentDescription = "Camera"
|
||||||
contentDescription = "Camera"
|
|
||||||
)
|
)
|
||||||
},
|
}, label = { Text("Camera") }, selected = currentScreen == Screen.Camera
|
||||||
label = { Text("Camera") },
|
|
||||||
selected = currentScreen == Screen.Camera
|
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
onClick = { navController.navigate(Screen.Remote.name) },
|
onClick = { navController.navigate(Screen.Remote.name) }, icon = {
|
||||||
icon = {
|
|
||||||
Icon(
|
Icon(
|
||||||
baselineRemote24,
|
baselineRemote24, contentDescription = "Remote"
|
||||||
contentDescription = "Remote"
|
|
||||||
)
|
)
|
||||||
},
|
}, label = { Text("Remote") }, selected = currentScreen == Screen.Remote
|
||||||
label = { Text("Remote") },
|
|
||||||
selected = currentScreen == Screen.Remote
|
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
onClick = { navController.navigate(Screen.Settings.name) },
|
onClick = { navController.navigate(Screen.Settings.name) }, icon = {
|
||||||
icon = {
|
|
||||||
Icon(
|
Icon(
|
||||||
baselineSettings24,
|
baselineSettings24, contentDescription = "Settings"
|
||||||
contentDescription = "Settings"
|
|
||||||
)
|
)
|
||||||
},
|
}, label = { Text("Settings") }, selected = currentScreen == Screen.Settings
|
||||||
label = { Text("Settings") },
|
|
||||||
selected = currentScreen == Screen.Settings
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}) { innerPadding ->
|
}) { innerPadding ->
|
||||||
@ -151,7 +124,10 @@ fun TvControllerApp(
|
|||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
composable(route = Screen.Camera.name) {
|
composable(route = Screen.Camera.name) {
|
||||||
CameraView(eglBaseContext = rtcPeerConnection.eglBaseContext, videoTrack = rtcPeerConnection.videoTrack)
|
CameraView(
|
||||||
|
eglBaseContext = webRtcService.eglBaseContext,
|
||||||
|
videoTrack = webRtcService.videoTrack
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(route = Screen.Remote.name) {
|
composable(route = Screen.Remote.name) {
|
||||||
RemoteView(
|
RemoteView(
|
||||||
@ -160,8 +136,7 @@ fun TvControllerApp(
|
|||||||
}
|
}
|
||||||
composable(route = Screen.Settings.name) {
|
composable(route = Screen.Settings.name) {
|
||||||
SettingsView(
|
SettingsView(
|
||||||
deviceService = deviceService,
|
deviceService = deviceService, bluetoothService = bluetoothService
|
||||||
bluetoothService = bluetoothService
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.example.tvcontroller.client
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.client.plugins.websocket.WebSockets
|
||||||
|
import io.ktor.client.request.cookie
|
||||||
|
import io.ktor.client.request.headers
|
||||||
|
import io.ktor.client.request.request
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.client.statement.HttpResponse
|
||||||
|
import io.ktor.http.HttpMethod
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
private const val TAG = "WebClient"
|
||||||
|
|
||||||
|
class WebClient {
|
||||||
|
val client = HttpClient(CIO) {
|
||||||
|
install(WebSockets)
|
||||||
|
}
|
||||||
|
var defaultCookies = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
suspend fun sendRequest(
|
||||||
|
url: String,
|
||||||
|
method: HttpMethod,
|
||||||
|
headers: Map<String, String> = emptyMap(),
|
||||||
|
body: String? = ""
|
||||||
|
): HttpResponse? {
|
||||||
|
try {
|
||||||
|
val response = client.request(url) {
|
||||||
|
this.method = method
|
||||||
|
setBody(body)
|
||||||
|
headers {
|
||||||
|
headers.forEach { (key, value) ->
|
||||||
|
append(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultCookies.forEach { (key, value) ->
|
||||||
|
cookie(name = key, value = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "error sending json request", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendJsonRequest(url: String, method: HttpMethod, json: JSONObject): HttpResponse? {
|
||||||
|
val headers = mutableMapOf<String, String>()
|
||||||
|
headers.put("Content-Type", "application/json")
|
||||||
|
return sendRequest(url, method, headers = headers, body = json.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.example.tvcontroller.client
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.plugins.websocket.webSocket
|
||||||
|
import io.ktor.client.request.cookie
|
||||||
|
import io.ktor.http.HttpMethod
|
||||||
|
import io.ktor.websocket.DefaultWebSocketSession
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
|
import io.ktor.websocket.readText
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
private const val TAG = "WebsocketClient"
|
||||||
|
|
||||||
|
class WebsocketClient(private val client: HttpClient) {
|
||||||
|
private var websocket: DefaultWebSocketSession? = null
|
||||||
|
private val dataHandlers = mutableListOf<(String) -> Unit>()
|
||||||
|
|
||||||
|
suspend fun connect(serverAddress: String, token: String) {
|
||||||
|
Log.i(TAG, "Connecting to websocket at $serverAddress")
|
||||||
|
val (host, port) = serverAddress.split(":")
|
||||||
|
val portInt = if (port.isEmpty()) 80 else port.toInt()
|
||||||
|
client.webSocket(
|
||||||
|
method = HttpMethod.Get,
|
||||||
|
host = host,
|
||||||
|
port = portInt,
|
||||||
|
path = "/ws",
|
||||||
|
request = {
|
||||||
|
cookie(name = "token", value = token)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Log.i(TAG, "Listening for incoming websocket messages")
|
||||||
|
websocket = this
|
||||||
|
while (true) {
|
||||||
|
val frame = incoming.receive()
|
||||||
|
Log.d(TAG, "Received frame: $frame")
|
||||||
|
if (frame is Frame.Text) {
|
||||||
|
val dataString = frame.readText()
|
||||||
|
dataHandlers.forEach { it(dataString) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onData(handler: (String) -> Unit) {
|
||||||
|
Log.d(TAG, "Adding data handler")
|
||||||
|
dataHandlers.add(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendJson(json: JSONObject) {
|
||||||
|
val frame = Frame.Text(json.toString())
|
||||||
|
websocket?.send(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,6 @@ import org.json.JSONObject
|
|||||||
|
|
||||||
|
|
||||||
class ControllerService(
|
class ControllerService(
|
||||||
private val context: Context,
|
|
||||||
private val bluetoothService: BluetoothService
|
private val bluetoothService: BluetoothService
|
||||||
) {
|
) {
|
||||||
private val samsungCommands = mutableMapOf<String, RemoteCommand>()
|
private val samsungCommands = mutableMapOf<String, RemoteCommand>()
|
||||||
|
|||||||
@ -3,110 +3,76 @@ package com.example.tvcontroller.services
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.example.tvcontroller.client.WebClient
|
||||||
import com.example.tvcontroller.data.Integration
|
import com.example.tvcontroller.data.Integration
|
||||||
import com.example.tvcontroller.webrtc.RtcPeerConnection
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.cio.CIO
|
|
||||||
import io.ktor.client.plugins.websocket.WebSockets
|
|
||||||
import io.ktor.client.plugins.websocket.webSocket
|
|
||||||
import io.ktor.client.request.cookie
|
|
||||||
import io.ktor.client.request.headers
|
|
||||||
import io.ktor.client.request.request
|
|
||||||
import io.ktor.client.request.setBody
|
|
||||||
import io.ktor.client.statement.HttpResponse
|
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.http.HttpMethod
|
||||||
import io.ktor.websocket.DefaultWebSocketSession
|
|
||||||
import io.ktor.websocket.Frame
|
|
||||||
import io.ktor.websocket.readText
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.webrtc.IceCandidate
|
|
||||||
|
|
||||||
private const val SHARED_PREFERENCES_NAME = "devices";
|
private const val SHARED_PREFERENCES_NAME = "devices";
|
||||||
private const val TAG = "DeviceService"
|
private const val TAG = "DeviceService"
|
||||||
|
|
||||||
class DeviceService(private val context: Context) {
|
class DeviceService(private val context: Context, private val client: WebClient) {
|
||||||
private var client = HttpClient(CIO) {
|
var serverAddress: String = ""
|
||||||
install(WebSockets)
|
var token: String = ""
|
||||||
}
|
private set(value) {
|
||||||
private var websocket: DefaultWebSocketSession? = null
|
field = value
|
||||||
private var serverAddress: String = ""
|
updateDefaultCookies()
|
||||||
private var token: String = ""
|
}
|
||||||
private var deviceId: String = ""
|
private var deviceId: String = ""
|
||||||
var rtcPeerConnection: RtcPeerConnection? = null
|
|
||||||
|
|
||||||
suspend fun initialize() {
|
suspend fun initialize() {
|
||||||
loadPreferences()
|
loadPreferences()
|
||||||
if (token.isEmpty()) return
|
if (token.isEmpty()) return
|
||||||
getIntegration()?.let {
|
getIntegration()
|
||||||
connect()
|
}
|
||||||
}
|
|
||||||
|
private fun updateDefaultCookies() {
|
||||||
|
Log.i(TAG, "Updating default cookies with token $token")
|
||||||
|
client.defaultCookies["token"] = token
|
||||||
|
Log.i(TAG, "Default cookies: ${client.defaultCookies}")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun registerIntegration(name: String, code: String) {
|
suspend fun registerIntegration(name: String, code: String) {
|
||||||
Log.i(TAG, "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().apply {
|
||||||
requestJson.put("name", name)
|
put("name", name)
|
||||||
requestJson.put("code", code)
|
put("code", code)
|
||||||
token = ""
|
|
||||||
deviceId = ""
|
|
||||||
try {
|
|
||||||
val response: HttpResponse =
|
|
||||||
client.request("http://$serverAddress/api/integrations/register") {
|
|
||||||
method = HttpMethod.Post
|
|
||||||
setBody(requestJson.toString())
|
|
||||||
headers {
|
|
||||||
append("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
cookie(name = "token", value = token)
|
|
||||||
}
|
|
||||||
|
|
||||||
val body: String = response.body()
|
|
||||||
val responseJson = JSONObject(body)
|
|
||||||
if (response.status.value != 200) {
|
|
||||||
val error = responseJson.getString("error")
|
|
||||||
Log.e(TAG, "Error getting integration: ${response.status.value} $error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token = responseJson.getString("token")
|
|
||||||
deviceId = responseJson.getString("id")
|
|
||||||
savePreferences()
|
|
||||||
|
|
||||||
Log.i(TAG, "Response: ${response.status.value} $body")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error registering integration", e)
|
|
||||||
}
|
}
|
||||||
|
val response =
|
||||||
|
client.sendJsonRequest(
|
||||||
|
"http://$serverAddress/api/integrations",
|
||||||
|
HttpMethod.Post,
|
||||||
|
requestJson
|
||||||
|
) ?: return
|
||||||
|
|
||||||
|
val responseJson = JSONObject(response.body<String>())
|
||||||
|
if (response.status.value != 200) {
|
||||||
|
val error = responseJson.getString("error")
|
||||||
|
Log.e(TAG, "Error getting integration: ${response.status.value} $error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token = responseJson.getString("token")
|
||||||
|
deviceId = responseJson.getString("id")
|
||||||
|
savePreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getIntegration(): Integration? {
|
suspend fun getIntegration(): Integration? {
|
||||||
Log.i(TAG, "Getting integration $deviceId at $serverAddress")
|
val response =
|
||||||
try {
|
client.sendRequest("http://$serverAddress/api/integrations/$deviceId", HttpMethod.Get)
|
||||||
val response: HttpResponse =
|
?: return null
|
||||||
client.request("http://$serverAddress/api/integrations/$deviceId") {
|
|
||||||
method = HttpMethod.Get
|
|
||||||
headers {
|
|
||||||
append("Authorization", "Bearer $token")
|
|
||||||
}
|
|
||||||
cookie(name = "token", value = token)
|
|
||||||
}
|
|
||||||
|
|
||||||
val body: String = response.body()
|
val responseJson = JSONObject(response.body<String>())
|
||||||
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(TAG, "Error getting integration: ${response.status.value} $error")
|
||||||
Log.e(TAG, "Error getting integration: ${response.status.value} $error")
|
return null
|
||||||
return null
|
|
||||||
}
|
|
||||||
val integration = Integration(
|
|
||||||
responseJson.getString("id"),
|
|
||||||
responseJson.getString("name")
|
|
||||||
)
|
|
||||||
return integration
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error getting integration", e)
|
|
||||||
}
|
}
|
||||||
return null
|
val integration = Integration(
|
||||||
|
responseJson.getString("id"),
|
||||||
|
responseJson.getString("name")
|
||||||
|
)
|
||||||
|
return integration
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPreferences() {
|
private fun loadPreferences() {
|
||||||
@ -128,117 +94,6 @@ class DeviceService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun connect() {
|
|
||||||
Log.i(TAG, "Connecting to websocket at $serverAddress")
|
|
||||||
val (host, port) = serverAddress.split(":")
|
|
||||||
val portInt = if (port.isEmpty()) 80 else port.toInt()
|
|
||||||
client.webSocket(
|
|
||||||
method = HttpMethod.Get,
|
|
||||||
host = host,
|
|
||||||
port = portInt,
|
|
||||||
path = "/ws",
|
|
||||||
request = {
|
|
||||||
cookie(name = "token", value = token)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Log.i(TAG, "Listening for incoming websocket messages")
|
|
||||||
websocket = this
|
|
||||||
while (true) {
|
|
||||||
val frame = incoming.receive()
|
|
||||||
if (frame is Frame.Text) {
|
|
||||||
val dataString = frame.readText()
|
|
||||||
val dataJson = JSONObject(dataString)
|
|
||||||
val senderId = dataJson.getString("sender")
|
|
||||||
val message = dataJson.getJSONObject("message")
|
|
||||||
val type = message.getString("type")
|
|
||||||
when (type) {
|
|
||||||
RtcPeerConnection.TYPE_OFFER -> {
|
|
||||||
Log.i(TAG, "Received offer from $senderId")
|
|
||||||
val sdp = message.getString("sdp")
|
|
||||||
rtcPeerConnection?.connect(senderId, sdp)
|
|
||||||
}
|
|
||||||
|
|
||||||
RtcPeerConnection.TYPE_ICE_CANDIDATE -> {
|
|
||||||
Log.i(TAG, "Received ice candidate")
|
|
||||||
val candidateString = message.getString("candidate")
|
|
||||||
val candidateJson = JSONObject(candidateString)
|
|
||||||
val sdpMid = candidateJson.getString("sdpMid")
|
|
||||||
val sdpMLineIndex = candidateJson.getInt("sdpMLineIndex")
|
|
||||||
val sdp = candidateJson.getString("candidate")
|
|
||||||
val candidate = IceCandidate(sdpMid, sdpMLineIndex, sdp)
|
|
||||||
Log.i(TAG, "Candidate: $candidate")
|
|
||||||
rtcPeerConnection?.addIceCandidate(candidate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendWebRtcAnswer(targetId: String, sdp: String) {
|
|
||||||
val messageJson = JSONObject()
|
|
||||||
messageJson.put("type", TYPE_SIGNALING)
|
|
||||||
messageJson.put("target", targetId)
|
|
||||||
messageJson.put("message", JSONObject().apply {
|
|
||||||
put("sdp", sdp)
|
|
||||||
put("type", RtcPeerConnection.TYPE_ANSWER)
|
|
||||||
})
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
Log.i(TAG, "Sending answer")
|
|
||||||
val frame = Frame.Text(messageJson.toString())
|
|
||||||
websocket?.send(frame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendWebRtcOffer(targetId: String, sdp: String) {
|
|
||||||
val messageJson = JSONObject()
|
|
||||||
messageJson.put("type", TYPE_SIGNALING)
|
|
||||||
messageJson.put("target", targetId)
|
|
||||||
messageJson.put("message", JSONObject().apply {
|
|
||||||
put("sdp", sdp)
|
|
||||||
put("type", RtcPeerConnection.TYPE_OFFER)
|
|
||||||
})
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
Log.i(TAG, "Sending offer")
|
|
||||||
val frame = Frame.Text(messageJson.toString())
|
|
||||||
websocket?.send(frame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendWebRtcIceCandidate(targetId: String, candidate: IceCandidate) {
|
|
||||||
val messageJson = JSONObject()
|
|
||||||
messageJson.put("type", TYPE_SIGNALING)
|
|
||||||
messageJson.put("target", targetId)
|
|
||||||
messageJson.put("message", JSONObject().apply {
|
|
||||||
put("candidate", JSONObject().apply {
|
|
||||||
put("sdpMid", candidate.sdpMid)
|
|
||||||
put("sdpMLineIndex", candidate.sdpMLineIndex)
|
|
||||||
put("candidate", candidate.sdp)
|
|
||||||
})
|
|
||||||
put("type", RtcPeerConnection.TYPE_ICE_CANDIDATE)
|
|
||||||
})
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
Log.i(TAG, "Sending ice candidate")
|
|
||||||
val frame = Frame.Text(messageJson.toString())
|
|
||||||
websocket?.send(frame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setServerAddress(url: String) {
|
|
||||||
serverAddress = url
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerAddress(): String {
|
|
||||||
return serverAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getToken(): String {
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TYPE_SIGNALING = "signaling"
|
const val TYPE_SIGNALING = "signaling"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,191 @@
|
|||||||
|
package com.example.tvcontroller.services.webrtc
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.util.Log
|
||||||
|
import org.webrtc.AudioTrack
|
||||||
|
import org.webrtc.Camera2Capturer
|
||||||
|
import org.webrtc.DataChannel
|
||||||
|
import org.webrtc.DefaultVideoDecoderFactory
|
||||||
|
import org.webrtc.EglBase
|
||||||
|
import org.webrtc.HardwareVideoEncoderFactory
|
||||||
|
import org.webrtc.IceCandidate
|
||||||
|
import org.webrtc.MediaConstraints
|
||||||
|
import org.webrtc.MediaStream
|
||||||
|
import org.webrtc.MediaStreamTrack
|
||||||
|
import org.webrtc.PeerConnection
|
||||||
|
import org.webrtc.PeerConnectionFactory
|
||||||
|
import org.webrtc.PeerConnectionFactory.InitializationOptions
|
||||||
|
import org.webrtc.SdpObserver
|
||||||
|
import org.webrtc.SessionDescription
|
||||||
|
import org.webrtc.SimulcastVideoEncoderFactory
|
||||||
|
import org.webrtc.SoftwareVideoEncoderFactory
|
||||||
|
import org.webrtc.SurfaceTextureHelper
|
||||||
|
import org.webrtc.VideoSource
|
||||||
|
import org.webrtc.VideoTrack
|
||||||
|
|
||||||
|
private const val TAG = "RtcPeerConnection"
|
||||||
|
|
||||||
|
class RtcPeerConnection(private val context: Context) {
|
||||||
|
private val peerConnectionFactory by lazy { initializeFactory() }
|
||||||
|
private var iceServers = ArrayList<PeerConnection.IceServer>()
|
||||||
|
var eglBaseContext: EglBase.Context = EglBase.create().eglBaseContext
|
||||||
|
private val cameraManager by lazy { context.getSystemService(Context.CAMERA_SERVICE) as CameraManager }
|
||||||
|
private val videoCapturer by lazy { createCameraCapturer() }
|
||||||
|
private val surfaceTextureHelper by lazy { createSurfaceTextureHelper() }
|
||||||
|
private val videoSource by lazy { createVideoSource() }
|
||||||
|
private val videoDecoderFactory by lazy { DefaultVideoDecoderFactory(eglBaseContext) }
|
||||||
|
private val videoEncoderFactory by lazy {
|
||||||
|
SimulcastVideoEncoderFactory(
|
||||||
|
HardwareVideoEncoderFactory(eglBaseContext, true, true),
|
||||||
|
SoftwareVideoEncoderFactory()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private var peerConnection: PeerConnection? = null
|
||||||
|
private val iceCandidateHandlers = ArrayList<((IceCandidate) -> Unit)>()
|
||||||
|
|
||||||
|
|
||||||
|
fun initialize() {
|
||||||
|
var observer = object : PeerConnection.Observer {
|
||||||
|
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {}
|
||||||
|
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {}
|
||||||
|
override fun onIceConnectionReceivingChange(p0: Boolean) {}
|
||||||
|
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {}
|
||||||
|
override fun onIceCandidate(iceCandidate: IceCandidate?) {
|
||||||
|
iceCandidateHandlers.forEach { it(iceCandidate!!) }
|
||||||
|
}
|
||||||
|
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate?>?) {}
|
||||||
|
override fun onAddStream(p0: MediaStream?) {}
|
||||||
|
override fun onRemoveStream(p0: MediaStream?) {}
|
||||||
|
override fun onDataChannel(p0: DataChannel?) {}
|
||||||
|
override fun onRenegotiationNeeded() {}
|
||||||
|
}
|
||||||
|
var rtcConfig = PeerConnection.RTCConfiguration(iceServers)
|
||||||
|
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIceServers(iceServerStrings: Array<String>) {
|
||||||
|
iceServers = ArrayList<PeerConnection.IceServer>()
|
||||||
|
iceServerStrings.forEach {
|
||||||
|
iceServers.add(
|
||||||
|
PeerConnection.IceServer.builder(it).createIceServer()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createAudioTrack(): AudioTrack {
|
||||||
|
val audioConstraints = MediaConstraints()
|
||||||
|
val audioSource = peerConnectionFactory.createAudioSource(audioConstraints)
|
||||||
|
val audioTrack = peerConnectionFactory.createAudioTrack("audio_track", audioSource)
|
||||||
|
return audioTrack
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTack(track: MediaStreamTrack) {
|
||||||
|
peerConnection?.addTrack(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createVideoTrack(): VideoTrack {
|
||||||
|
val videoTrack = peerConnectionFactory.createVideoTrack("video_track", videoSource)
|
||||||
|
return videoTrack
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocalDescription(sessionDescription: SessionDescription, callback: () -> Unit) {
|
||||||
|
val onLocalDescriptionSet = object : SdpObserver {
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||||
|
override fun onCreateFailure(p0: String?) {}
|
||||||
|
override fun onSetFailure(p0: String?) {}
|
||||||
|
}
|
||||||
|
peerConnection?.setLocalDescription(onLocalDescriptionSet, sessionDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRemoteDescription(sessionDescription: SessionDescription, callback: () -> Unit) {
|
||||||
|
val onRemoteDescriptionSet = object : SdpObserver {
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||||
|
override fun onCreateFailure(p0: String?) {}
|
||||||
|
override fun onSetFailure(p0: String?) {}
|
||||||
|
}
|
||||||
|
peerConnection?.setRemoteDescription(onRemoteDescriptionSet, sessionDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createAnswer(mediaConstraints: MediaConstraints, callback: (SessionDescription) -> Unit) {
|
||||||
|
val onAnswerCreated = object : SdpObserver {
|
||||||
|
override fun onCreateSuccess(sessionDescription: SessionDescription?) {
|
||||||
|
callback(sessionDescription!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetSuccess() {}
|
||||||
|
override fun onCreateFailure(p0: String?) {}
|
||||||
|
override fun onSetFailure(p0: String?) {}
|
||||||
|
}
|
||||||
|
peerConnection?.createAnswer(onAnswerCreated, mediaConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addIceCandidate(iceCandidate: IceCandidate) {
|
||||||
|
Log.i(TAG, "Adding ice candidate $iceCandidate to $peerConnection")
|
||||||
|
peerConnection?.addIceCandidate(iceCandidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIceCandidate(handler: (IceCandidate) -> Unit) {
|
||||||
|
iceCandidateHandlers.add(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeFactory(): PeerConnectionFactory {
|
||||||
|
val initOptions = InitializationOptions.builder(context).createInitializationOptions()
|
||||||
|
PeerConnectionFactory.initialize(initOptions)
|
||||||
|
|
||||||
|
val options = PeerConnectionFactory.Options()
|
||||||
|
val peerConnectionFactory =
|
||||||
|
PeerConnectionFactory.builder().setVideoDecoderFactory(videoDecoderFactory)
|
||||||
|
.setVideoEncoderFactory(videoEncoderFactory).setOptions(options)
|
||||||
|
.createPeerConnectionFactory()
|
||||||
|
return peerConnectionFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCameraCapturer(): Camera2Capturer {
|
||||||
|
val ids = cameraManager.cameraIdList
|
||||||
|
var foundCamera = false;
|
||||||
|
var cameraId = ""
|
||||||
|
for (id in ids) {
|
||||||
|
val characteristics = cameraManager.getCameraCharacteristics(id)
|
||||||
|
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
||||||
|
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
|
||||||
|
cameraId = id
|
||||||
|
foundCamera = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundCamera) {
|
||||||
|
cameraId = ids.first()
|
||||||
|
}
|
||||||
|
val cameraCapturer = Camera2Capturer(context, cameraId, null)
|
||||||
|
return cameraCapturer
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSurfaceTextureHelper(): SurfaceTextureHelper {
|
||||||
|
val surfaceTextureHelper =
|
||||||
|
SurfaceTextureHelper.create("SurfaceTextureHelperThread", eglBaseContext)
|
||||||
|
return surfaceTextureHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createVideoSource(): VideoSource {
|
||||||
|
val videoSource = peerConnectionFactory.createVideoSource(false)
|
||||||
|
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
|
||||||
|
videoCapturer.startCapture(1920, 1080, 30)
|
||||||
|
return videoSource
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TYPE_OFFER = "offer"
|
||||||
|
const val TYPE_ANSWER = "answer"
|
||||||
|
const val TYPE_ICE_CANDIDATE = "ice_candidate"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
package com.example.tvcontroller.services.webrtc
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.example.tvcontroller.client.WebsocketClient
|
||||||
|
import com.example.tvcontroller.services.DeviceService
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.webrtc.EglBase
|
||||||
|
import org.webrtc.IceCandidate
|
||||||
|
import org.webrtc.MediaConstraints
|
||||||
|
import org.webrtc.SessionDescription
|
||||||
|
|
||||||
|
private const val TAG = "WebRtcService"
|
||||||
|
|
||||||
|
class WebRtcService(private val context: Context, private val websocketClient: WebsocketClient) {
|
||||||
|
private val rtcPeerConnection by lazy { createRtcPeerConnection() }
|
||||||
|
val videoTrack by lazy { rtcPeerConnection.createVideoTrack() }
|
||||||
|
val audioTrack by lazy { rtcPeerConnection.createAudioTrack() }
|
||||||
|
val eglBaseContext: EglBase.Context
|
||||||
|
get() = rtcPeerConnection.eglBaseContext
|
||||||
|
private var peerId: String = ""
|
||||||
|
|
||||||
|
fun connect() {
|
||||||
|
Log.i(TAG, "Connecting to signaling server")
|
||||||
|
websocketClient.onData(this::handleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRtcPeerConnection(): RtcPeerConnection {
|
||||||
|
val iceServers = arrayOf("stun:stun.l.google.com:19302")
|
||||||
|
val webRtcService = this
|
||||||
|
val rtcPeerConnection = RtcPeerConnection(context).apply {
|
||||||
|
setIceServers(iceServers)
|
||||||
|
onIceCandidate(webRtcService::sendIceCandidate)
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
return rtcPeerConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOffer(sdp: String) {
|
||||||
|
var mediaConstraints = MediaConstraints()
|
||||||
|
val remoteSessionDescription = SessionDescription(SessionDescription.Type.OFFER, sdp)
|
||||||
|
rtcPeerConnection.apply {
|
||||||
|
addTack(audioTrack)
|
||||||
|
addTack(videoTrack)
|
||||||
|
setRemoteDescription(remoteSessionDescription) {
|
||||||
|
createAnswer(mediaConstraints) { localSessionDescription ->
|
||||||
|
setLocalDescription(localSessionDescription) {
|
||||||
|
sendAnswer(
|
||||||
|
peerId, localSessionDescription.description ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleData(data: String) {
|
||||||
|
Log.d(TAG, "Received data: $data")
|
||||||
|
val dataJson = JSONObject(data)
|
||||||
|
val senderId = dataJson.getString("sender")
|
||||||
|
val message = dataJson.getJSONObject("message")
|
||||||
|
val type = message.getString("type")
|
||||||
|
when (type) {
|
||||||
|
RtcPeerConnection.TYPE_OFFER -> {
|
||||||
|
Log.i(TAG, "Received offer from $senderId")
|
||||||
|
val sdp = message.getString("sdp")
|
||||||
|
peerId = senderId
|
||||||
|
handleOffer(sdp)
|
||||||
|
}
|
||||||
|
|
||||||
|
RtcPeerConnection.TYPE_ICE_CANDIDATE -> {
|
||||||
|
val candidateString = message.getString("candidate")
|
||||||
|
handleReceiveIceCandidate(candidateString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReceiveIceCandidate(candidateString: String) {
|
||||||
|
Log.i(TAG, "Received ice candidate")
|
||||||
|
val candidateJson = JSONObject(candidateString)
|
||||||
|
val sdpMid = candidateJson.getString("sdpMid")
|
||||||
|
val sdpMLineIndex = candidateJson.getInt("sdpMLineIndex")
|
||||||
|
val sdp = candidateJson.getString("candidate")
|
||||||
|
val candidate = IceCandidate(sdpMid, sdpMLineIndex, sdp)
|
||||||
|
Log.i(TAG, "Candidate: $candidate")
|
||||||
|
rtcPeerConnection.addIceCandidate(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendIceCandidate(iceCandidate: IceCandidate) {
|
||||||
|
val messageJson = JSONObject()
|
||||||
|
messageJson.put("type", DeviceService.Companion.TYPE_SIGNALING)
|
||||||
|
messageJson.put("target", peerId)
|
||||||
|
messageJson.put("message", JSONObject().apply {
|
||||||
|
put("candidate", JSONObject().apply {
|
||||||
|
put("sdpMid", iceCandidate.sdpMid)
|
||||||
|
put("sdpMLineIndex", iceCandidate.sdpMLineIndex)
|
||||||
|
put("candidate", iceCandidate.sdp)
|
||||||
|
})
|
||||||
|
put("type", RtcPeerConnection.TYPE_ICE_CANDIDATE)
|
||||||
|
})
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
Log.i(TAG, "Sending ice candidate")
|
||||||
|
websocketClient.sendJson(messageJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendAnswer(targetId: String, sdp: String) {
|
||||||
|
val messageJson = JSONObject()
|
||||||
|
messageJson.put("type", DeviceService.Companion.TYPE_SIGNALING)
|
||||||
|
messageJson.put("target", targetId)
|
||||||
|
messageJson.put("message", JSONObject().apply {
|
||||||
|
put("sdp", sdp)
|
||||||
|
put("type", RtcPeerConnection.TYPE_ANSWER)
|
||||||
|
})
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
Log.i(TAG, "Sending answer")
|
||||||
|
websocketClient.sendJson(messageJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,11 +15,13 @@ import com.example.tvcontroller.services.DeviceService
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val TAG = "SettingsViewModel"
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
private val deviceService: DeviceService,
|
private val deviceService: DeviceService,
|
||||||
private val bluetoothService: BluetoothService
|
private val bluetoothService: BluetoothService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var serverAddress by mutableStateOf(deviceService.getServerAddress())
|
var serverAddress by mutableStateOf(deviceService.serverAddress)
|
||||||
private set
|
private set
|
||||||
var deviceName by mutableStateOf(Build.MANUFACTURER + " " + Build.MODEL)
|
var deviceName by mutableStateOf(Build.MANUFACTURER + " " + Build.MODEL)
|
||||||
private set
|
private set
|
||||||
@ -48,17 +50,16 @@ class SettingsViewModel(
|
|||||||
fun connect() {
|
fun connect() {
|
||||||
//Log.i("SettingsScreen", "Save settings: $serverUrl, $deviceName, $registrationCode")
|
//Log.i("SettingsScreen", "Save settings: $serverUrl, $deviceName, $registrationCode")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
deviceService.setServerAddress(serverAddress)
|
deviceService.serverAddress = serverAddress
|
||||||
deviceService.registerIntegration(deviceName, registrationCode)
|
deviceService.registerIntegration(deviceName, registrationCode)
|
||||||
updateConnectionState()
|
updateConnectionState()
|
||||||
updateDeviceInfo()
|
updateDeviceInfo()
|
||||||
deviceService.connect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateConnectionState() {
|
private fun updateConnectionState() {
|
||||||
Log.i("SettingsViewModel", "Device token: ${deviceService.getToken()}")
|
Log.i(TAG, "Device token: ${deviceService.token}")
|
||||||
connectionState = if (deviceService.getToken().isEmpty()) {
|
connectionState = if (deviceService.token.isEmpty()) {
|
||||||
Settings.ConnectionState.Unregistered
|
Settings.ConnectionState.Unregistered
|
||||||
} else {
|
} else {
|
||||||
Settings.ConnectionState.Registered
|
Settings.ConnectionState.Registered
|
||||||
|
|||||||
@ -1,113 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
private const val TAG = "CameraXCapturer"
|
|
||||||
|
|
||||||
class CameraXCapturer : ImageAnalysis.Analyzer {
|
|
||||||
var capturerObserver: CapturerObserver? = null
|
|
||||||
|
|
||||||
override fun analyze(image: ImageProxy) {
|
|
||||||
if (image.format != YUV_420_888) throw Exception("Unsupported format")
|
|
||||||
var videoFrame = imageProxyToVideoFrame(image)
|
|
||||||
Log.i(TAG, "Handing frame to capturer observer $capturerObserver")
|
|
||||||
capturerObserver?.onFrameCaptured(videoFrame) ?: image.close()
|
|
||||||
Log.i(TAG, "Frame handled by capturer observer")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,282 +0,0 @@
|
|||||||
package com.example.tvcontroller.webrtc
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
|
||||||
import android.hardware.camera2.CameraManager
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import com.example.tvcontroller.services.DeviceService
|
|
||||||
import org.webrtc.AudioTrack
|
|
||||||
import org.webrtc.Camera2Capturer
|
|
||||||
import org.webrtc.DataChannel
|
|
||||||
import org.webrtc.DefaultVideoDecoderFactory
|
|
||||||
import org.webrtc.DefaultVideoEncoderFactory
|
|
||||||
import org.webrtc.EglBase
|
|
||||||
import org.webrtc.HardwareVideoEncoderFactory
|
|
||||||
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.RtpSender
|
|
||||||
import org.webrtc.RtpTransceiver
|
|
||||||
import org.webrtc.SdpObserver
|
|
||||||
import org.webrtc.SessionDescription
|
|
||||||
import org.webrtc.SimulcastVideoEncoderFactory
|
|
||||||
import org.webrtc.SoftwareVideoEncoderFactory
|
|
||||||
import org.webrtc.SurfaceTextureHelper
|
|
||||||
import org.webrtc.VideoSource
|
|
||||||
import org.webrtc.VideoTrack
|
|
||||||
import kotlin.getValue
|
|
||||||
|
|
||||||
private const val TAG = "RtcPeerConnection"
|
|
||||||
|
|
||||||
class RtcPeerConnection(private val context: Context) : PeerConnection.Observer {
|
|
||||||
private val peerConnectionFactory by lazy { initializeFactory() }
|
|
||||||
private val iceServers by lazy { initializeIceServers() }
|
|
||||||
private val audioTrack by lazy { createAudioTrack() }
|
|
||||||
val videoTrack by lazy { createVideoTrack() }
|
|
||||||
var eglBaseContext: EglBase.Context = EglBase.create().eglBaseContext
|
|
||||||
private val cameraManager by lazy { context.getSystemService(Context.CAMERA_SERVICE) as CameraManager }
|
|
||||||
private val videoCapturer by lazy { createCameraCapturer() }
|
|
||||||
private val surfaceTextureHelper by lazy { createSurfaceTextureHelper() }
|
|
||||||
private val videoSource by lazy { createVideoSource() }
|
|
||||||
private val videoDecoderFactory by lazy { DefaultVideoDecoderFactory(eglBaseContext) }
|
|
||||||
private val videoEncoderFactory by lazy {
|
|
||||||
SimulcastVideoEncoderFactory(
|
|
||||||
HardwareVideoEncoderFactory(eglBaseContext, true, true),
|
|
||||||
SoftwareVideoEncoderFactory()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private var peerConnection: PeerConnection? = null
|
|
||||||
var deviceService: DeviceService? = null
|
|
||||||
|
|
||||||
private var peerId: String = ""
|
|
||||||
|
|
||||||
private var offer = ""
|
|
||||||
|
|
||||||
private fun initializeFactory(): PeerConnectionFactory {
|
|
||||||
val initOptions = InitializationOptions.builder(context).createInitializationOptions()
|
|
||||||
PeerConnectionFactory.initialize(initOptions)
|
|
||||||
|
|
||||||
val options = PeerConnectionFactory.Options()
|
|
||||||
val peerConnectionFactory =
|
|
||||||
PeerConnectionFactory.builder().setVideoDecoderFactory(videoDecoderFactory)
|
|
||||||
.setVideoEncoderFactory(videoEncoderFactory).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 localVideoTrack = peerConnectionFactory.createVideoTrack("video_track", videoSource)
|
|
||||||
//localVideoTrack.setEnabled(true)
|
|
||||||
return localVideoTrack
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCameraCapturer(): Camera2Capturer {
|
|
||||||
val ids = cameraManager.cameraIdList
|
|
||||||
var foundCamera = false;
|
|
||||||
var cameraId = ""
|
|
||||||
for (id in ids) {
|
|
||||||
val characteristics = cameraManager.getCameraCharacteristics(id)
|
|
||||||
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
|
||||||
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
|
|
||||||
cameraId = id
|
|
||||||
foundCamera = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundCamera) {
|
|
||||||
cameraId = ids.first()
|
|
||||||
}
|
|
||||||
val cameraCapturer = Camera2Capturer(context, cameraId, null)
|
|
||||||
return cameraCapturer
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSurfaceTextureHelper(): SurfaceTextureHelper {
|
|
||||||
val surfaceTextureHelper =
|
|
||||||
SurfaceTextureHelper.create("SurfaceTextureHelperThread", eglBaseContext)
|
|
||||||
return surfaceTextureHelper
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createVideoSource(): VideoSource {
|
|
||||||
val videoSource = peerConnectionFactory.createVideoSource(false)
|
|
||||||
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
|
|
||||||
videoCapturer.startCapture(1920, 1080, 30)
|
|
||||||
return videoSource
|
|
||||||
}
|
|
||||||
|
|
||||||
fun connect(targetId: String, sdp: String) {
|
|
||||||
peerId = targetId
|
|
||||||
offer = sdp
|
|
||||||
Log.i(TAG, "Connecting rtc peer connection")
|
|
||||||
var rtcConfig = PeerConnection.RTCConfiguration(iceServers)
|
|
||||||
Log.i(TAG, "Creating peer connection")
|
|
||||||
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, this)
|
|
||||||
Log.i(TAG, "Peer connection created")
|
|
||||||
//peerConnection?.addTransceiver(audioTrack, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY))
|
|
||||||
//peerConnection?.addTransceiver( videoTrack, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY) )
|
|
||||||
peerConnection?.addTrack(audioTrack)
|
|
||||||
peerConnection?.addTrack(videoTrack)
|
|
||||||
handleOffer(sdp)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleOffer(sdp: String) {
|
|
||||||
var mediaConstraints = MediaConstraints()
|
|
||||||
var localSessionDescription: SessionDescription? = null
|
|
||||||
val remoteSessionDescription = SessionDescription(SessionDescription.Type.OFFER, sdp)
|
|
||||||
|
|
||||||
Log.i(TAG, "Handling offer $sdp")
|
|
||||||
val onLocalDescriptionSet = object : SdpObserver {
|
|
||||||
override fun onSetSuccess() {
|
|
||||||
Log.i(TAG, "Local description set")
|
|
||||||
peerConnection?.transceivers?.forEach {
|
|
||||||
|
|
||||||
Log.i(TAG, "${it.mediaType} Transceiver: ${it.currentDirection}")
|
|
||||||
}
|
|
||||||
deviceService?.sendWebRtcAnswer(
|
|
||||||
peerId, localSessionDescription?.description ?: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateSuccess(sessionDescription: SessionDescription?) {}
|
|
||||||
override fun onCreateFailure(p0: String?) {}
|
|
||||||
override fun onSetFailure(p0: String?) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onAnswerCreated = object : SdpObserver {
|
|
||||||
override fun onCreateSuccess(sessionDescription: SessionDescription?) {
|
|
||||||
Log.i(TAG, "Answer created")
|
|
||||||
localSessionDescription = sessionDescription
|
|
||||||
peerConnection?.setLocalDescription(onLocalDescriptionSet, sessionDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSetSuccess() {}
|
|
||||||
override fun onCreateFailure(p0: String?) {}
|
|
||||||
override fun onSetFailure(p0: String?) {}
|
|
||||||
}
|
|
||||||
val onRemoteDescriptionSet = object : SdpObserver {
|
|
||||||
override fun onSetSuccess() {
|
|
||||||
Log.i(TAG, "Remote description set")
|
|
||||||
peerConnection?.createAnswer(onAnswerCreated, mediaConstraints)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateSuccess(p0: SessionDescription?) {}
|
|
||||||
override fun onCreateFailure(p0: String?) {}
|
|
||||||
override fun onSetFailure(p0: String?) {}
|
|
||||||
}
|
|
||||||
Log.i(TAG, "Setting remote description")
|
|
||||||
peerConnection?.setRemoteDescription(onRemoteDescriptionSet, remoteSessionDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createOffer() {
|
|
||||||
var mediaConstraints = MediaConstraints()
|
|
||||||
var localSessionDescription: SessionDescription? = null
|
|
||||||
|
|
||||||
val onLocalDescriptionSet = object : SdpObserver {
|
|
||||||
override fun onSetSuccess() {
|
|
||||||
Log.i(TAG, "Local description set")
|
|
||||||
peerConnection?.transceivers?.forEach {
|
|
||||||
Log.i(TAG, "${it.mediaType} Transceiver: ${it.currentDirection}")
|
|
||||||
}
|
|
||||||
deviceService?.sendWebRtcOffer(
|
|
||||||
peerId, localSessionDescription?.description ?: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateSuccess(sessionDescription: SessionDescription?) {}
|
|
||||||
override fun onCreateFailure(p0: String?) {}
|
|
||||||
override fun onSetFailure(p0: String?) {}
|
|
||||||
}
|
|
||||||
val onOfferCreated = object : SdpObserver {
|
|
||||||
override fun onCreateSuccess(sessionDescription: SessionDescription?) {
|
|
||||||
Log.i(TAG, "Offer created")
|
|
||||||
peerConnection?.setLocalDescription(onLocalDescriptionSet, sessionDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSetSuccess() {}
|
|
||||||
override fun onCreateFailure(p0: String?) {}
|
|
||||||
override fun onSetFailure(p0: String?) {}
|
|
||||||
}
|
|
||||||
Log.i(TAG, "Creating offer")
|
|
||||||
peerConnection?.createOffer(onOfferCreated, mediaConstraints)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addIceCandidate(iceCandidate: IceCandidate) {
|
|
||||||
Log.i(TAG, "Adding ice candidate $iceCandidate to $peerConnection")
|
|
||||||
peerConnection?.addIceCandidate(iceCandidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
|
||||||
Log.i(TAG, "onSignalingChange: $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
|
||||||
Log.i(TAG, "onIceConnectionChange: $p0")
|
|
||||||
if (p0 == PeerConnection.IceConnectionState.CONNECTED) {
|
|
||||||
videoCapturer.startCapture(1280, 720, 30)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
p0?.let {
|
|
||||||
deviceService?.sendWebRtcIceCandidate(peerId, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
//if (offer.isNotEmpty()) {
|
|
||||||
// handleOffer(offer)
|
|
||||||
//} else {
|
|
||||||
// createOffer()
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TYPE_OFFER = "offer"
|
|
||||||
const val TYPE_ANSWER = "answer"
|
|
||||||
const val TYPE_ICE_CANDIDATE = "ice_candidate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user