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 bluetoothService by lazy { BluetoothService(applicationContext) }
private val deviceService by lazy { DeviceService(applicationContext, webClient, websocketClient) } 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -1,9 +1,7 @@
package com.example.tvcontroller.data package com.example.tvcontroller.data
class RemoteCommand { class RemoteCommand {
var functionName: String? = null
var protocol: String? = null var protocol: String? = null
var device: String? = null var device: String? = null
var subdevice: String? = null var command: String? = null
var function: String? = null
} }

View File

@ -1,32 +1,33 @@
package com.example.tvcontroller.services package com.example.tvcontroller.services
import android.content.Context
import android.util.Log import android.util.Log
import com.example.tvcontroller.data.RemoteCommand import com.example.tvcontroller.data.RemoteCommand
import com.example.tvcontroller.services.webrtc.WebRtcService
import org.json.JSONObject import org.json.JSONObject
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
class ControllerService( class ControllerService(
private val bluetoothService: BluetoothService private val bluetoothService: BluetoothService,
private val webRtcService: WebRtcService
) { ) {
private val samsungCommands = mutableMapOf<String, RemoteCommand>()
init { init {
loadCommands() loadCommands()
webRtcService.onDataChannelData(this::handleWebRtcData)
} }
fun sendCommand(command: String) { fun sendCommand(command: RemoteCommand) {
if (samsungCommands[command] == null) return val jsonString = remoteCommandToJsonString(command)
Log.i("ControllerService", "Sending command: $command") Log.i(TAG, "Sending command: $jsonString")
val jsonString = remoteCommandToJsonString(samsungCommands[command]!!)
sendData(jsonString) sendData(jsonString)
} }
fun remoteCommandToJsonString(command: RemoteCommand): String { fun remoteCommandToJsonString(command: RemoteCommand): String {
var commandObject = JSONObject() var commandObject = JSONObject()
commandObject.put("protocol", command.protocol) commandObject.put("protocol", command.protocol)
commandObject.put("address", command.device) commandObject.put("device", command.device)
commandObject.put("command", command.function) commandObject.put("command", command.command)
return commandObject.toString() return commandObject.toString()
} }
@ -37,7 +38,27 @@ class ControllerService(
fun loadCommands() { 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 { companion object {
private const val TAG = "ControllerService"
const val MESSAGE_TYPE_COMMAND = "command"
const val POWER = "POWER" const val POWER = "POWER"
const val CURSOR_UP = "CURSOR UP" const val CURSOR_UP = "CURSOR UP"
const val CURSOR_DOWN = "CURSOR DOWN" const val CURSOR_DOWN = "CURSOR DOWN"

View File

@ -1,16 +1,11 @@
package com.example.tvcontroller.services.webrtc package com.example.tvcontroller.services.webrtc
import android.content.Context import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.util.Log import android.util.Log
import com.example.tvcontroller.services.CameraService import com.example.tvcontroller.services.CameraService
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
import org.webrtc.Camera2Capturer
import org.webrtc.DataChannel import org.webrtc.DataChannel
import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.EglBase import org.webrtc.EglBase
import org.webrtc.HardwareVideoEncoderFactory
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
import org.webrtc.MediaConstraints import org.webrtc.MediaConstraints
import org.webrtc.MediaStream import org.webrtc.MediaStream
@ -20,11 +15,11 @@ import org.webrtc.PeerConnectionFactory
import org.webrtc.PeerConnectionFactory.InitializationOptions import org.webrtc.PeerConnectionFactory.InitializationOptions
import org.webrtc.SdpObserver import org.webrtc.SdpObserver
import org.webrtc.SessionDescription import org.webrtc.SessionDescription
import org.webrtc.SimulcastVideoEncoderFactory
import org.webrtc.SoftwareVideoEncoderFactory
import org.webrtc.SurfaceTextureHelper import org.webrtc.SurfaceTextureHelper
import org.webrtc.VideoSource import org.webrtc.VideoSource
import org.webrtc.VideoTrack import org.webrtc.VideoTrack
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
private const val TAG = "RtcPeerConnection" 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 val videoSource by lazy { createVideoSource() }
private var peerConnection: PeerConnection? = null private var peerConnection: PeerConnection? = null
private val iceCandidateHandlers = ArrayList<((IceCandidate) -> Unit)>() private val iceCandidateHandlers = ArrayList<((IceCandidate) -> Unit)>()
private val dataChannelHandlers = ArrayList<((ByteBuffer) -> Unit)>()
fun initialize() { fun initialize() {
@ -52,7 +48,16 @@ class RtcPeerConnection(private val context: Context, private val cameraService:
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate?>?) {} override fun onIceCandidatesRemoved(p0: Array<out IceCandidate?>?) {}
override fun onAddStream(p0: MediaStream?) {} override fun onAddStream(p0: MediaStream?) {}
override fun onRemoveStream(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() {} override fun onRenegotiationNeeded() {}
} }
var rtcConfig = PeerConnection.RTCConfiguration(iceServers) var rtcConfig = PeerConnection.RTCConfiguration(iceServers)
@ -132,6 +137,10 @@ class RtcPeerConnection(private val context: Context, private val cameraService:
iceCandidateHandlers.add(handler) iceCandidateHandlers.add(handler)
} }
fun onDataChannelData(handler: (ByteBuffer) -> Unit) {
dataChannelHandlers.add(handler)
}
private fun initializeFactory(): PeerConnectionFactory { private fun initializeFactory(): PeerConnectionFactory {
val initOptions = InitializationOptions.builder(context).createInitializationOptions() val initOptions = InitializationOptions.builder(context).createInitializationOptions()
PeerConnectionFactory.initialize(initOptions) PeerConnectionFactory.initialize(initOptions)

View File

@ -9,12 +9,14 @@ import org.json.JSONObject
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
import org.webrtc.MediaConstraints import org.webrtc.MediaConstraints
import org.webrtc.SessionDescription import org.webrtc.SessionDescription
import java.nio.ByteBuffer
class WebRtcService( class WebRtcService(
private val context: Context, private val context: Context,
private val websocketClient: WebsocketClient, private val websocketClient: WebsocketClient,
private val cameraService: CameraService private val cameraService: CameraService
) { ) {
private val dataChannelHandlers = ArrayList<((ByteBuffer) -> Unit)>()
private var rtcPeerConnection: RtcPeerConnection = createRtcPeerConnection() private var rtcPeerConnection: RtcPeerConnection = createRtcPeerConnection()
val videoTrack by lazy { rtcPeerConnection.createVideoTrack() } val videoTrack by lazy { rtcPeerConnection.createVideoTrack() }
val audioTrack by lazy { rtcPeerConnection.createAudioTrack() } val audioTrack by lazy { rtcPeerConnection.createAudioTrack() }
@ -25,6 +27,10 @@ class WebRtcService(
websocketClient.onData(this::handleData) websocketClient.onData(this::handleData)
} }
fun onDataChannelData(handler: (ByteBuffer) -> Unit) {
dataChannelHandlers.add(handler)
}
private fun createRtcPeerConnection(): RtcPeerConnection { private fun createRtcPeerConnection(): RtcPeerConnection {
val iceServers = arrayOf("stun:stun.l.google.com:19302") val iceServers = arrayOf("stun:stun.l.google.com:19302")
val webRtcService = this val webRtcService = this
@ -33,6 +39,7 @@ class WebRtcService(
onIceCandidate(webRtcService::sendIceCandidate) onIceCandidate(webRtcService::sendIceCandidate)
initialize() initialize()
} }
dataChannelHandlers.forEach { rtcPeerConnection.onDataChannelData(it) }
return rtcPeerConnection return rtcPeerConnection
} }

View File

@ -3,6 +3,7 @@ package com.example.tvcontroller.ui.views
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.tvcontroller.data.RemoteCommand
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.ControllerService import com.example.tvcontroller.services.ControllerService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -12,8 +13,10 @@ import org.json.JSONObject
class RemoteViewModel( class RemoteViewModel(
private val controllerService: ControllerService private val controllerService: ControllerService
) : ViewModel() { ) : ViewModel() {
private val commands = mutableMapOf<String, RemoteCommand>()
fun sendCommand(command: String) { fun sendCommand(commandType: String) {
val command = commands[commandType]?: return
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
controllerService.sendCommand(command) controllerService.sendCommand(command)
} }