feat: handle webrtc data channel to send commands

This commit is contained in:
Fritz Heiden 2025-04-15 00:28:56 +02:00
parent 22570e0e6d
commit c8a0c4160c
6 changed files with 61 additions and 23 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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<String, RemoteCommand>()
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"

View File

@ -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<out IceCandidate?>?) {}
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)

View File

@ -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
}

View File

@ -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<String, RemoteCommand>()
fun sendCommand(command: String) {
fun sendCommand(commandType: String) {
val command = commands[commandType]?: return
viewModelScope.launch(Dispatchers.IO) {
controllerService.sendCommand(command)
}