From c8a0c4160cd3f68eb969e7585ee3d676f387536c Mon Sep 17 00:00:00 2001 From: Fritz Heiden Date: Tue, 15 Apr 2025 00:28:56 +0200 Subject: [PATCH] feat: handle webrtc data channel to send commands --- .../com/example/tvcontroller/MainActivity.kt | 2 +- .../tvcontroller/data/RemoteCommand.kt | 4 +- .../services/ControllerService.kt | 41 ++++++++++++++----- .../services/webrtc/RtcPeerConnection.kt | 25 +++++++---- .../services/webrtc/WebRtcService.kt | 7 ++++ .../tvcontroller/ui/views/RemoteViewModel.kt | 5 ++- 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/example/tvcontroller/MainActivity.kt b/app/src/main/java/com/example/tvcontroller/MainActivity.kt index 32c7cf5..4413296 100644 --- a/app/src/main/java/com/example/tvcontroller/MainActivity.kt +++ b/app/src/main/java/com/example/tvcontroller/MainActivity.kt @@ -34,7 +34,7 @@ class MainActivity : ComponentActivity() { } private val bluetoothService by lazy { BluetoothService(applicationContext) } private val deviceService by lazy { DeviceService(applicationContext, webClient, websocketClient) } - private val controllerService by lazy { ControllerService(bluetoothService) } + private val controllerService by lazy { ControllerService(bluetoothService, webRtcService) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/example/tvcontroller/data/RemoteCommand.kt b/app/src/main/java/com/example/tvcontroller/data/RemoteCommand.kt index 709e2db..87ff738 100644 --- a/app/src/main/java/com/example/tvcontroller/data/RemoteCommand.kt +++ b/app/src/main/java/com/example/tvcontroller/data/RemoteCommand.kt @@ -1,9 +1,7 @@ package com.example.tvcontroller.data class RemoteCommand { - var functionName: String? = null var protocol: String? = null var device: String? = null - var subdevice: String? = null - var function: String? = null + var command: String? = null } \ No newline at end of file diff --git a/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt b/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt index 9c7d9ec..bc0c1c4 100644 --- a/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt @@ -1,32 +1,33 @@ package com.example.tvcontroller.services -import android.content.Context import android.util.Log import com.example.tvcontroller.data.RemoteCommand +import com.example.tvcontroller.services.webrtc.WebRtcService import org.json.JSONObject +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets class ControllerService( - private val bluetoothService: BluetoothService + private val bluetoothService: BluetoothService, + private val webRtcService: WebRtcService ) { - private val samsungCommands = mutableMapOf() - init { loadCommands() + webRtcService.onDataChannelData(this::handleWebRtcData) } - fun sendCommand(command: String) { - if (samsungCommands[command] == null) return - Log.i("ControllerService", "Sending command: $command") - val jsonString = remoteCommandToJsonString(samsungCommands[command]!!) + fun sendCommand(command: RemoteCommand) { + val jsonString = remoteCommandToJsonString(command) + Log.i(TAG, "Sending command: $jsonString") sendData(jsonString) } fun remoteCommandToJsonString(command: RemoteCommand): String { var commandObject = JSONObject() commandObject.put("protocol", command.protocol) - commandObject.put("address", command.device) - commandObject.put("command", command.function) + commandObject.put("device", command.device) + commandObject.put("command", command.command) return commandObject.toString() } @@ -37,7 +38,27 @@ class ControllerService( fun loadCommands() { } + fun handleWebRtcData(data: ByteBuffer) { + val dataString = StandardCharsets.UTF_8.decode(data).toString() + val json = JSONObject(dataString) + if (!json.has("type") || json.getString("type") != MESSAGE_TYPE_COMMAND) return + val commandJson = json.getJSONObject("data") + val protocol = if (commandJson.has("protocol")) commandJson.getString("protocol") else null + val device = if (commandJson.has("device")) commandJson.getString("device") else null + val command = if (commandJson.has("commandNumber")) commandJson.getString("commandNumber") else null + val remoteCommand = RemoteCommand().apply { + this.protocol = protocol + this.device = device + this.command = command + } + sendCommand(remoteCommand) + } + companion object { + private const val TAG = "ControllerService" + + const val MESSAGE_TYPE_COMMAND = "command" + const val POWER = "POWER" const val CURSOR_UP = "CURSOR UP" const val CURSOR_DOWN = "CURSOR DOWN" diff --git a/app/src/main/java/com/example/tvcontroller/services/webrtc/RtcPeerConnection.kt b/app/src/main/java/com/example/tvcontroller/services/webrtc/RtcPeerConnection.kt index 3367f00..8ea02b2 100644 --- a/app/src/main/java/com/example/tvcontroller/services/webrtc/RtcPeerConnection.kt +++ b/app/src/main/java/com/example/tvcontroller/services/webrtc/RtcPeerConnection.kt @@ -1,16 +1,11 @@ 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 com.example.tvcontroller.services.CameraService 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 @@ -20,11 +15,11 @@ 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 +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets private const val TAG = "RtcPeerConnection" @@ -37,6 +32,7 @@ class RtcPeerConnection(private val context: Context, private val cameraService: private val videoSource by lazy { createVideoSource() } private var peerConnection: PeerConnection? = null private val iceCandidateHandlers = ArrayList<((IceCandidate) -> Unit)>() + private val dataChannelHandlers = ArrayList<((ByteBuffer) -> Unit)>() fun initialize() { @@ -52,7 +48,16 @@ class RtcPeerConnection(private val context: Context, private val cameraService: override fun onIceCandidatesRemoved(p0: Array?) {} override fun onAddStream(p0: MediaStream?) {} override fun onRemoveStream(p0: MediaStream?) {} - override fun onDataChannel(p0: DataChannel?) {} + override fun onDataChannel(channel: DataChannel?) { + Log.i(TAG, "Data channel created: $channel") + channel?.registerObserver(object : DataChannel.Observer { + override fun onBufferedAmountChange(p0: Long) { } + override fun onStateChange() { } + override fun onMessage(p0: DataChannel.Buffer?) { + dataChannelHandlers.forEach { it(p0?.data!!) } + } + }) + } override fun onRenegotiationNeeded() {} } var rtcConfig = PeerConnection.RTCConfiguration(iceServers) @@ -132,6 +137,10 @@ class RtcPeerConnection(private val context: Context, private val cameraService: iceCandidateHandlers.add(handler) } + fun onDataChannelData(handler: (ByteBuffer) -> Unit) { + dataChannelHandlers.add(handler) + } + private fun initializeFactory(): PeerConnectionFactory { val initOptions = InitializationOptions.builder(context).createInitializationOptions() PeerConnectionFactory.initialize(initOptions) diff --git a/app/src/main/java/com/example/tvcontroller/services/webrtc/WebRtcService.kt b/app/src/main/java/com/example/tvcontroller/services/webrtc/WebRtcService.kt index 41d0ba5..dc41a65 100644 --- a/app/src/main/java/com/example/tvcontroller/services/webrtc/WebRtcService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/webrtc/WebRtcService.kt @@ -9,12 +9,14 @@ import org.json.JSONObject import org.webrtc.IceCandidate import org.webrtc.MediaConstraints import org.webrtc.SessionDescription +import java.nio.ByteBuffer class WebRtcService( private val context: Context, private val websocketClient: WebsocketClient, private val cameraService: CameraService ) { + private val dataChannelHandlers = ArrayList<((ByteBuffer) -> Unit)>() private var rtcPeerConnection: RtcPeerConnection = createRtcPeerConnection() val videoTrack by lazy { rtcPeerConnection.createVideoTrack() } val audioTrack by lazy { rtcPeerConnection.createAudioTrack() } @@ -25,6 +27,10 @@ class WebRtcService( websocketClient.onData(this::handleData) } + fun onDataChannelData(handler: (ByteBuffer) -> Unit) { + dataChannelHandlers.add(handler) + } + private fun createRtcPeerConnection(): RtcPeerConnection { val iceServers = arrayOf("stun:stun.l.google.com:19302") val webRtcService = this @@ -33,6 +39,7 @@ class WebRtcService( onIceCandidate(webRtcService::sendIceCandidate) initialize() } + dataChannelHandlers.forEach { rtcPeerConnection.onDataChannelData(it) } return rtcPeerConnection } diff --git a/app/src/main/java/com/example/tvcontroller/ui/views/RemoteViewModel.kt b/app/src/main/java/com/example/tvcontroller/ui/views/RemoteViewModel.kt index deb94be..a748bd8 100644 --- a/app/src/main/java/com/example/tvcontroller/ui/views/RemoteViewModel.kt +++ b/app/src/main/java/com/example/tvcontroller/ui/views/RemoteViewModel.kt @@ -3,6 +3,7 @@ package com.example.tvcontroller.ui.views import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.example.tvcontroller.data.RemoteCommand import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.ControllerService import kotlinx.coroutines.Dispatchers @@ -12,8 +13,10 @@ import org.json.JSONObject class RemoteViewModel( private val controllerService: ControllerService ) : ViewModel() { + private val commands = mutableMapOf() - fun sendCommand(command: String) { + fun sendCommand(commandType: String) { + val command = commands[commandType]?: return viewModelScope.launch(Dispatchers.IO) { controllerService.sendCommand(command) }