mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
Finished first version of Chrome extension to cast to FCast. Added support for WebSocket to terminal client. Added global support for setting playback speed. Added support for casting local file using terminal client. Added global support for playback error messages. Fixed crash caused by failing to unregister MDNS. Fixed issue where subtitles would always show for HLS. Added support for fractional seconds globally. Layout fixes to desktop casting client. Added footer telling user they can close the window.
This commit is contained in:
parent
fd9a63dac0
commit
18b61d549c
26 changed files with 1116 additions and 193 deletions
|
@ -38,8 +38,18 @@ class DiscoveryService(private val _context: Context) {
|
|||
fun stop() {
|
||||
if (_nsdManager == null) return
|
||||
|
||||
_nsdManager?.unregisterService(_registrationListenerTcp)
|
||||
_nsdManager?.unregisterService(_registrationListenerWs)
|
||||
try {
|
||||
_nsdManager?.unregisterService(_registrationListenerTcp)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to unregister TCP Listener.");
|
||||
}
|
||||
|
||||
try {
|
||||
_nsdManager?.unregisterService(_registrationListenerWs)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to unregister TCP Listener.");
|
||||
}
|
||||
|
||||
_nsdManager = null
|
||||
}
|
||||
|
||||
|
@ -60,4 +70,8 @@ class DiscoveryService(private val _context: Context) {
|
|||
Log.e("DiscoveryService", "Service unregistration failed: errorCode=$errorCode")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DiscoveryService"
|
||||
}
|
||||
}
|
|
@ -6,9 +6,9 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
import java.io.DataOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.Socket
|
||||
import java.net.SocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
enum class SessionState {
|
||||
Idle,
|
||||
|
@ -26,7 +26,9 @@ enum class Opcode(val value: Byte) {
|
|||
Seek(5),
|
||||
PlaybackUpdate(6),
|
||||
VolumeUpdate(7),
|
||||
SetVolume(8)
|
||||
SetVolume(8),
|
||||
PlaybackError(9),
|
||||
SetSpeed(10)
|
||||
}
|
||||
|
||||
const val LENGTH_BYTES = 4
|
||||
|
@ -38,6 +40,11 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
|
|||
private var _packetLength = 0
|
||||
private var _state = SessionState.WaitingForLength
|
||||
private var _outputStream: DataOutputStream? = DataOutputStream(outputStream)
|
||||
val id = UUID.randomUUID()
|
||||
|
||||
fun sendPlaybackError(value: PlaybackErrorMessage) {
|
||||
send(Opcode.PlaybackError, value)
|
||||
}
|
||||
|
||||
fun sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
||||
send(Opcode.PlaybackUpdate, value)
|
||||
|
@ -82,7 +89,7 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
|
|||
|
||||
Log.d(TAG, "Sent $size bytes: '$jsonString'.")
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to send message.", e)
|
||||
Log.i(TAG, "Failed to send message ${id}.", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@ -189,6 +196,7 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
|
|||
Opcode.Stop -> _service.onCastStop()
|
||||
Opcode.Seek -> _service.onCastSeek(Json.decodeFromString(body!!))
|
||||
Opcode.SetVolume -> _service.onSetVolume(Json.decodeFromString(body!!))
|
||||
Opcode.SetSpeed -> _service.onSetSpeed(Json.decodeFromString(body!!))
|
||||
else -> { }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
|
|
@ -58,43 +58,30 @@ class NetworkService : Service() {
|
|||
|
||||
val onNewSession: (FCastSession) -> Unit = { session ->
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
Log.i(TAG, "On new session ${session.id}")
|
||||
|
||||
var encounteredError = false
|
||||
while (!_stopped && !encounteredError) {
|
||||
try {
|
||||
val player = PlayerActivity.instance
|
||||
val updateMessage = if (player != null) {
|
||||
PlaybackUpdateMessage(
|
||||
System.currentTimeMillis(),
|
||||
player.currentPosition / 1000.0,
|
||||
player.duration / 1000.0,
|
||||
if (player.isPlaying) 1 else 2
|
||||
)
|
||||
} else {
|
||||
PlaybackUpdateMessage(
|
||||
System.currentTimeMillis(),
|
||||
0.0,
|
||||
0.0,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
val updateMessage = generateUpdateMessage()
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
session.sendPlaybackUpdate(updateMessage)
|
||||
Log.i(TAG, "Update sent ${session.id}")
|
||||
} catch (eSend: Throwable) {
|
||||
Log.e(TAG, "Unhandled error sending update", eSend)
|
||||
Log.e(TAG, "Unhandled error sending update ${session.id}", eSend)
|
||||
encounteredError = true
|
||||
return@withContext
|
||||
}
|
||||
|
||||
Log.i(TAG, "Update sent")
|
||||
}
|
||||
} catch (eTimer: Throwable) {
|
||||
Log.e(TAG, "Unhandled error on timer thread", eTimer)
|
||||
Log.e(TAG, "Unhandled error on timer thread ${session.id}", eTimer)
|
||||
} finally {
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Send loop closed ${session.id}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,11 +89,11 @@ class NetworkService : Service() {
|
|||
start()
|
||||
}
|
||||
|
||||
_tcpListenerService = TcpListenerService(this, onNewSession).apply {
|
||||
_tcpListenerService = TcpListenerService(this) { onNewSession(it) }.apply {
|
||||
start()
|
||||
}
|
||||
|
||||
_webSocketListenerService = WebSocketListenerService(this, onNewSession).apply {
|
||||
_webSocketListenerService = WebSocketListenerService(this) { onNewSession(it) }.apply {
|
||||
start()
|
||||
}
|
||||
|
||||
|
@ -153,11 +140,82 @@ class NetworkService : Service() {
|
|||
instance = null
|
||||
}
|
||||
|
||||
fun generateUpdateMessage(): PlaybackUpdateMessage {
|
||||
val player = PlayerActivity.instance
|
||||
return if (player != null) {
|
||||
PlaybackUpdateMessage(
|
||||
System.currentTimeMillis(),
|
||||
player.currentPosition / 1000.0,
|
||||
player.duration / 1000.0,
|
||||
if (player.isPlaying) 1 else 2,
|
||||
player.speed.toDouble()
|
||||
)
|
||||
} else {
|
||||
PlaybackUpdateMessage(
|
||||
System.currentTimeMillis(),
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
0.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendPlaybackError(error: String) {
|
||||
val message = PlaybackErrorMessage(error)
|
||||
_tcpListenerService?.forEachSession { session ->
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.sendPlaybackError(message)
|
||||
Log.i(TAG, "Playback error sent ${session.id}")
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send playback error", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_webSocketListenerService?.forEachSession { session ->
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.sendPlaybackError(message)
|
||||
Log.i(TAG, "Playback error sent ${session.id}")
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send playback error", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendPlaybackUpdate(message: PlaybackUpdateMessage) {
|
||||
_tcpListenerService?.forEachSession { session ->
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.sendPlaybackUpdate(message)
|
||||
Log.i(TAG, "Playback update sent ${session.id}")
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send playback update", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_webSocketListenerService?.forEachSession { session ->
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.sendPlaybackUpdate(message)
|
||||
Log.i(TAG, "Playback update sent ${session.id}")
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send playback update", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendCastVolumeUpdate(value: VolumeUpdateMessage) {
|
||||
_tcpListenerService?.forEachSession { session ->
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.sendVolumeUpdate(value)
|
||||
Log.i(TAG, "Volume update sent ${session.id}")
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send volume update", e)
|
||||
}
|
||||
|
@ -165,9 +223,10 @@ class NetworkService : Service() {
|
|||
}
|
||||
|
||||
_webSocketListenerService?.forEachSession { session ->
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.sendVolumeUpdate(value)
|
||||
Log.i(TAG, "Volume update sent ${session.id}")
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send volume update", e)
|
||||
}
|
||||
|
@ -178,7 +237,7 @@ class NetworkService : Service() {
|
|||
fun onCastPlay(playMessage: PlayMessage) {
|
||||
Log.i(TAG, "onPlay")
|
||||
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (PlayerActivity.instance == null) {
|
||||
val i = Intent(this@NetworkService, PlayerActivity::class.java)
|
||||
|
@ -187,6 +246,7 @@ class NetworkService : Service() {
|
|||
i.putExtra("url", playMessage.url)
|
||||
i.putExtra("content", playMessage.content)
|
||||
i.putExtra("time", playMessage.time)
|
||||
i.putExtra("speed", playMessage.speed)
|
||||
|
||||
if (activityCount > 0) {
|
||||
startActivity(i)
|
||||
|
@ -219,7 +279,7 @@ class NetworkService : Service() {
|
|||
fun onCastPause() {
|
||||
Log.i(TAG, "onPause")
|
||||
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
PlayerActivity.instance?.pause()
|
||||
} catch (e: Throwable) {
|
||||
|
@ -231,7 +291,7 @@ class NetworkService : Service() {
|
|||
fun onCastResume() {
|
||||
Log.i(TAG, "onResume")
|
||||
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
PlayerActivity.instance?.resume()
|
||||
} catch (e: Throwable) {
|
||||
|
@ -243,7 +303,7 @@ class NetworkService : Service() {
|
|||
fun onCastStop() {
|
||||
Log.i(TAG, "onStop")
|
||||
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
PlayerActivity.instance?.finish()
|
||||
} catch (e: Throwable) {
|
||||
|
@ -255,7 +315,7 @@ class NetworkService : Service() {
|
|||
fun onCastSeek(seekMessage: SeekMessage) {
|
||||
Log.i(TAG, "onSeek")
|
||||
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
PlayerActivity.instance?.seek(seekMessage)
|
||||
} catch (e: Throwable) {
|
||||
|
@ -267,7 +327,7 @@ class NetworkService : Service() {
|
|||
fun onSetVolume(setVolumeMessage: SetVolumeMessage) {
|
||||
Log.i(TAG, "onSetVolume")
|
||||
|
||||
_scope?.launch {
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
PlayerActivity.instance?.setVolume(setVolumeMessage)
|
||||
} catch (e: Throwable) {
|
||||
|
@ -276,6 +336,18 @@ class NetworkService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
fun onSetSpeed(setSpeedMessage: SetSpeedMessage) {
|
||||
Log.i(TAG, "setSpeedMessage")
|
||||
|
||||
_scope?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
PlayerActivity.instance?.setSpeed(setSpeedMessage)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to seek", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "NetworkListenerServiceChannel"
|
||||
private const val NOTIFICATION_ID = 1
|
||||
|
|
|
@ -7,7 +7,8 @@ data class PlayMessage(
|
|||
val container: String,
|
||||
val url: String? = null,
|
||||
val content: String? = null,
|
||||
val time: Long? = null
|
||||
val time: Double? = null,
|
||||
val speed: Double? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -20,7 +21,8 @@ data class PlaybackUpdateMessage(
|
|||
val generationTime: Long,
|
||||
val time: Double,
|
||||
val duration: Double,
|
||||
val state: Int
|
||||
val state: Int,
|
||||
val speed: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -29,6 +31,16 @@ data class VolumeUpdateMessage(
|
|||
val volume: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PlaybackErrorMessage(
|
||||
val message: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SetSpeedMessage(
|
||||
val speed: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SetVolumeMessage(
|
||||
val volume: Double
|
||||
|
|
|
@ -39,6 +39,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||
private var _wasPlaying = false
|
||||
|
||||
val currentPosition get() = _exoPlayer.currentPosition
|
||||
val speed get() = _exoPlayer.playbackParameters.speed
|
||||
val duration get() = _exoPlayer.duration
|
||||
val isPlaying get() = _exoPlayer.isPlaying
|
||||
|
||||
|
@ -82,7 +83,15 @@ class PlayerActivity : AppCompatActivity() {
|
|||
setStatus(true, null)
|
||||
}
|
||||
|
||||
//TODO: Send playback update
|
||||
NetworkService.instance?.generateUpdateMessage()?.let {
|
||||
_scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
NetworkService.instance?.sendPlaybackUpdate(it)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Unhandled error sending playback update", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
|
@ -105,9 +114,16 @@ class PlayerActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
//TODO: Send error notification
|
||||
val fullMessage = getFullExceptionMessage(error)
|
||||
setStatus(false, fullMessage)
|
||||
|
||||
setStatus(false, getFullExceptionMessage(error))
|
||||
_scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
NetworkService.instance?.sendPlaybackError(fullMessage)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Unhandled error sending playback error", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVolumeChanged(volume: Float) {
|
||||
|
@ -118,8 +134,19 @@ class PlayerActivity : AppCompatActivity() {
|
|||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Unhandled error sending volume update", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Update sent")
|
||||
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
|
||||
super.onPlaybackParametersChanged(playbackParameters)
|
||||
NetworkService.instance?.generateUpdateMessage()?.let {
|
||||
_scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
NetworkService.instance?.sendPlaybackUpdate(it)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Unhandled error sending playback update", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +169,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||
val trackSelector = DefaultTrackSelector(this)
|
||||
trackSelector.parameters = trackSelector.parameters
|
||||
.buildUpon()
|
||||
.setPreferredTextLanguage("en")
|
||||
.setPreferredTextLanguage("df")
|
||||
.setSelectUndeterminedTextLanguage(true)
|
||||
.build()
|
||||
|
||||
|
@ -165,9 +192,10 @@ class PlayerActivity : AppCompatActivity() {
|
|||
val container = intent.getStringExtra("container") ?: ""
|
||||
val url = intent.getStringExtra("url")
|
||||
val content = intent.getStringExtra("content")
|
||||
val time = intent.getLongExtra("time", 0L)
|
||||
val time = intent.getDoubleExtra("time", 0.0)
|
||||
val speed = intent.getDoubleExtra("speed", 1.0)
|
||||
|
||||
play(PlayMessage(container, url, content, time))
|
||||
play(PlayMessage(container, url, content, time, speed))
|
||||
|
||||
instance = this
|
||||
NetworkService.activityCount++
|
||||
|
@ -302,9 +330,10 @@ class PlayerActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
_exoPlayer.setMediaSource(mediaSource)
|
||||
_exoPlayer.setPlaybackSpeed(playMessage.speed?.toFloat() ?: 1.0f)
|
||||
|
||||
if (playMessage.time != null) {
|
||||
_exoPlayer.seekTo(playMessage.time * 1000)
|
||||
_exoPlayer.seekTo((playMessage.time * 1000).toLong())
|
||||
}
|
||||
|
||||
setStatus(true, null)
|
||||
|
@ -326,6 +355,10 @@ class PlayerActivity : AppCompatActivity() {
|
|||
_exoPlayer.seekTo((seekMessage.time * 1000.0).toLong())
|
||||
}
|
||||
|
||||
fun setSpeed(setSpeedMessage: SetSpeedMessage) {
|
||||
_exoPlayer.setPlaybackSpeed(setSpeedMessage.speed.toFloat())
|
||||
}
|
||||
|
||||
fun setVolume(setVolumeMessage: SetVolumeMessage) {
|
||||
_exoPlayer.volume = setVolumeMessage.volume.toFloat()
|
||||
}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
package com.futo.fcast.receiver
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedInputStream
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
|
|
|
@ -6,44 +6,54 @@ import org.java_websocket.handshake.ClientHandshake
|
|||
import org.java_websocket.server.WebSocketServer
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.IdentityHashMap
|
||||
|
||||
class WebSocketListenerService(private val _networkService: NetworkService, private val _onNewSession: (session: FCastSession) -> Unit) : WebSocketServer(InetSocketAddress(PORT)) {
|
||||
private var _sessions = IdentityHashMap<WebSocket, FCastSession>()
|
||||
private val _sockets = arrayListOf<WebSocket>()
|
||||
|
||||
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
|
||||
val session = FCastSession(WebSocketOutputStream(conn), conn.remoteSocketAddress, _networkService)
|
||||
synchronized(_sessions) {
|
||||
_sessions[conn] = session
|
||||
conn.setAttachment(session)
|
||||
|
||||
synchronized(_sockets) {
|
||||
_sockets.add(conn)
|
||||
}
|
||||
|
||||
_onNewSession(session)
|
||||
|
||||
Log.i(TAG, "New connection from ${conn.remoteSocketAddress}")
|
||||
Log.i(TAG, "New connection from ${conn.remoteSocketAddress} ${session.id}")
|
||||
}
|
||||
|
||||
override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) {
|
||||
synchronized(_sessions) {
|
||||
_sessions.remove(conn)
|
||||
synchronized(_sockets) {
|
||||
_sockets.remove(conn)
|
||||
}
|
||||
|
||||
Log.i(TAG, "Closed connection from ${conn.remoteSocketAddress}")
|
||||
Log.i(TAG, "Closed connection from ${conn.remoteSocketAddress} ${conn.getAttachment<FCastSession>().id}")
|
||||
}
|
||||
|
||||
override fun onMessage(conn: WebSocket?, message: String?) {
|
||||
if (conn == null) {
|
||||
Log.i(TAG, "Conn is null, ignore onMessage")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "Received string message, but not processing: $message")
|
||||
}
|
||||
|
||||
override fun onMessage(conn: WebSocket?, message: ByteBuffer?) {
|
||||
if (conn == null) {
|
||||
Log.i(TAG, "Conn is null, ignore onMessage")
|
||||
return
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
Log.i(TAG, "Received byte message null")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "Received byte message (offset = ${message.arrayOffset()}, size = ${message.remaining()})")
|
||||
|
||||
synchronized(_sessions) {
|
||||
_sessions[conn]?.processBytes(message)
|
||||
}
|
||||
val session = conn.getAttachment<FCastSession>()
|
||||
Log.i(TAG, "Received byte message (offset = ${message.arrayOffset()}, size = ${message.remaining()}, id = ${session.id})")
|
||||
session.processBytes(message)
|
||||
}
|
||||
|
||||
override fun onError(conn: WebSocket?, ex: Exception) {
|
||||
|
@ -55,9 +65,9 @@ class WebSocketListenerService(private val _networkService: NetworkService, priv
|
|||
}
|
||||
|
||||
fun forEachSession(handler: (FCastSession) -> Unit) {
|
||||
synchronized(_sessions) {
|
||||
for (pair in _sessions) {
|
||||
handler(pair.value)
|
||||
synchronized(_sockets) {
|
||||
_sockets.forEach {
|
||||
handler(it.getAttachment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue