From f100b1f20597c816c6ff2d4c8bdd57783222a439 Mon Sep 17 00:00:00 2001 From: Talo Halton Date: Fri, 17 May 2024 16:46:27 +0100 Subject: [PATCH] Improve HeadlessPlayer safety, use kotlin.time --- .../cinterop/mpv/LibMpvClient.kt.disabled | 5 +- .../cinterop/mpv/LibMpvClient.kt.enabled | 9 +- .../kotlin/cinterop/mpv/MpvClientImpl.kt | 2 +- .../kotlin/cinterop/zmq/ZmqRouter.kt | 5 +- .../kotlin/cinterop/zmq/ZmqSocket.kt | 9 +- .../spms/client/cli/CommandLineClient.kt | 3 +- .../spms/client/cli/CommandLineClientMode.kt | 4 +- .../kotlin/spms/client/cli/modes/Poll.kt | 15 ++-- .../kotlin/spms/client/cli/modes/Run.kt | 16 ++-- .../kotlin/spms/client/player/PlayerClient.kt | 34 ++++---- .../localisation/strings/CliLocalisation.kt | 10 ++- .../localisation/strings/CliLocalisationEn.kt | 13 +-- .../localisation/strings/CliLocalisationJa.kt | 13 +-- .../strings/ServerActionLocalisation.kt | 4 +- .../strings/ServerActionLocalisationEn.kt | 6 +- .../strings/ServerActionLocalisationJa.kt | 6 +- .../player/{ => headless}/HeadlessPlayer.kt | 83 +++++++------------ .../spms/player/headless/PlaybackState.kt | 34 ++++++++ src/commonMain/kotlin/spms/server/SpMs.kt | 35 ++++---- .../kotlin/spms/server/SpMsCommand.kt | 4 +- .../socketapi/server/ServerActionGetStatus.kt | 8 +- .../server/ServerActionReadyToPlay.kt | 5 +- 22 files changed, 186 insertions(+), 137 deletions(-) rename src/commonMain/kotlin/spms/player/{ => headless}/HeadlessPlayer.kt (84%) create mode 100644 src/commonMain/kotlin/spms/player/headless/PlaybackState.kt diff --git a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled index e9a6dbc..9cc1f26 100644 --- a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled +++ b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled @@ -11,7 +11,10 @@ class mpv_event { val event_id: Int get() = throw NotImplementedError() } -abstract class LibMpvClient(val headless: Boolean = true): Player { +abstract class LibMpvClient( + val headless: Boolean = true, + playlist_auto_progress: Boolean = true +): Player { companion object { fun isAvailable(): Boolean = false } diff --git a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled index 5b8f405..fbc855d 100644 --- a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled +++ b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled @@ -9,7 +9,10 @@ import cnames.structs.mpv_handle as MpvHandle import libmpv.mpv_format as MpvFormat @OptIn(ExperimentalForeignApi::class) -abstract class LibMpvClient(val headless: Boolean = true): Player { +abstract class LibMpvClient( + val headless: Boolean = true, + playlist_auto_progress: Boolean = true +): Player { companion object { fun isAvailable(): Boolean = true } @@ -32,6 +35,10 @@ abstract class LibMpvClient(val headless: Boolean = true): Player { osd_level.value = 3 mpv_set_option(ctx, "osd-level", MPV_FORMAT_INT64, osd_level.ptr) } + + if (!playlist_auto_progress) { + mpv_set_option_string(ctx, "keep-open", "always") + } } val init_result: Int = mpv_initialize(ctx) diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt b/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt index 259efa2..d3dfe19 100644 --- a/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt +++ b/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt @@ -20,7 +20,7 @@ inline fun CPointer<*>?.pointedAs(): T = this!!.reinterpret().pointed @OptIn(ExperimentalForeignApi::class) -abstract class MpvClientImpl(headless: Boolean = true): LibMpvClient(headless) { +abstract class MpvClientImpl(headless: Boolean = true, playlist_auto_progress: Boolean = true): LibMpvClient(headless = headless, playlist_auto_progress = playlist_auto_progress) { companion object { fun isAvailable(): Boolean = LibMpvClient.isAvailable() } diff --git a/src/commonMain/kotlin/cinterop/zmq/ZmqRouter.kt b/src/commonMain/kotlin/cinterop/zmq/ZmqRouter.kt index 1663a1c..07bec6f 100644 --- a/src/commonMain/kotlin/cinterop/zmq/ZmqRouter.kt +++ b/src/commonMain/kotlin/cinterop/zmq/ZmqRouter.kt @@ -6,6 +6,7 @@ import kotlinx.cinterop.cstr import kotlinx.cinterop.toCValues import libzmq.* import spms.socketapi.shared.SpMsSocketApi +import kotlin.time.Duration @OptIn(ExperimentalForeignApi::class) abstract class ZmqRouter(mem_scope: MemScope) { @@ -36,8 +37,8 @@ abstract class ZmqRouter(mem_scope: MemScope) { socket.release() } - protected fun recvMultipart(timeout_ms: Long?): Message? { - val parts: List = socket.recvMultipart(timeout_ms) ?: return null + protected fun recvMultipart(timeout: Duration?): Message? { + val parts: List = socket.recvMultipart(timeout) ?: return null var client_id: ByteArray? = null val message_parts: MutableList = mutableListOf() diff --git a/src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt b/src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt index 8309bf4..ea84bf0 100644 --- a/src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt +++ b/src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt @@ -6,6 +6,7 @@ import platform.posix.memcpy import spms.zmqPollerWait import spms.socketapi.shared.SpMsSocketApi import spms.socketapi.shared.SPMS_MESSAGE_MAX_SIZE +import kotlin.time.Duration @OptIn(ExperimentalForeignApi::class) class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { @@ -88,14 +89,14 @@ class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { zmq_ctx_destroy(context) } - fun recvStringMultipart(timeout_ms: Long?): List? { - val message: List = recvMultipart(timeout_ms) ?: return null + fun recvStringMultipart(timeout: Duration?): List? { + val message: List = recvMultipart(timeout) ?: return null return SpMsSocketApi.decode(message.map { it.decodeToString() }) } - fun recvMultipart(timeout_ms: Long?): List? = memScoped { + fun recvMultipart(timeout: Duration?): List? = memScoped { val event: zmq_poller_event_t = alloc() - zmqPollerWait(poller, event.ptr, timeout_ms ?: ZMQ_NOBLOCK.toLong()) + zmqPollerWait(poller, event.ptr, timeout?.inWholeMilliseconds ?: ZMQ_NOBLOCK.toLong()) if (event.events.toInt() != ZMQ_POLLIN) { return null diff --git a/src/commonMain/kotlin/spms/client/cli/CommandLineClient.kt b/src/commonMain/kotlin/spms/client/cli/CommandLineClient.kt index 416094c..eb0aa73 100644 --- a/src/commonMain/kotlin/spms/client/cli/CommandLineClient.kt +++ b/src/commonMain/kotlin/spms/client/cli/CommandLineClient.kt @@ -13,8 +13,9 @@ import spms.client.cli.modes.Interactive import spms.client.cli.modes.Poll import spms.client.cli.modes.Run import toRed +import kotlin.time.Duration -const val SERVER_REPLY_TIMEOUT_MS: Long = 2000 +val SERVER_REPLY_TIMEOUT: Duration = with (Duration) { 2.seconds } private fun getClientName(): String = "SpMs CLI" diff --git a/src/commonMain/kotlin/spms/client/cli/CommandLineClientMode.kt b/src/commonMain/kotlin/spms/client/cli/CommandLineClientMode.kt index 41bd0a0..72aebdb 100644 --- a/src/commonMain/kotlin/spms/client/cli/CommandLineClientMode.kt +++ b/src/commonMain/kotlin/spms/client/cli/CommandLineClientMode.kt @@ -48,10 +48,10 @@ abstract class CommandLineClientMode( ) context.socket.sendStringMultipart(listOf(Json.encodeToString(handshake))) - val reply: List? = context.socket.recvStringMultipart(SERVER_REPLY_TIMEOUT_MS) + val reply: List? = context.socket.recvStringMultipart(SERVER_REPLY_TIMEOUT) if (reply == null) { - throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotRespond(SERVER_REPLY_TIMEOUT_MS)) + throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotRespond(SERVER_REPLY_TIMEOUT)) } log(currentContext.loc.cli.handshake_reply_received + " " + reply.toString()) diff --git a/src/commonMain/kotlin/spms/client/cli/modes/Poll.kt b/src/commonMain/kotlin/spms/client/cli/modes/Poll.kt index a654767..f046fea 100644 --- a/src/commonMain/kotlin/spms/client/cli/modes/Poll.kt +++ b/src/commonMain/kotlin/spms/client/cli/modes/Poll.kt @@ -8,11 +8,11 @@ import libzmq.ZMQ_NOBLOCK import spms.client.cli.CommandLineClientMode import spms.client.cli.SpMsCommandLineClientError import spms.localisation.loc -import kotlin.system.getTimeMillis import spms.socketapi.shared.SpMsSocketApi import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.time.* -private const val SERVER_EVENT_TIMEOUT_MS: Long = 10000 +private val SERVER_EVENT_TIMEOUT: Duration = with (Duration) { 10000.milliseconds } private const val POLL_INTERVAL: Long = 100 @OptIn(ExperimentalForeignApi::class) @@ -28,16 +28,17 @@ class Poll: CommandLineClientMode("poll", { "TODO" }) { while (true) { delay(POLL_INTERVAL) - val wait_end: Long = getTimeMillis() + SERVER_EVENT_TIMEOUT_MS + val wait_start: TimeMark = TimeSource.Monotonic.markNow() var events: List? = null - while (events == null && getTimeMillis() < wait_end) { - val message: List? = + while (events == null && wait_start.elapsedNow() < SERVER_EVENT_TIMEOUT) { + val message: List? = with (Duration) { socket.recvStringMultipart( - (wait_end - getTimeMillis()).coerceAtLeast(ZMQ_NOBLOCK.toLong()) + (SERVER_EVENT_TIMEOUT - wait_start.elapsedNow()).inWholeMilliseconds.coerceAtLeast(ZMQ_NOBLOCK.toLong()).milliseconds )?.let { SpMsSocketApi.decode(it) } + } events = message?.map { Json.decodeFromString(it) @@ -45,7 +46,7 @@ class Poll: CommandLineClientMode("poll", { "TODO" }) { } if (events == null) { - throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotSendEvents(SERVER_EVENT_TIMEOUT_MS)) + throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotSendEvents(SERVER_EVENT_TIMEOUT)) } if (events.isNotEmpty()) { diff --git a/src/commonMain/kotlin/spms/client/cli/modes/Run.kt b/src/commonMain/kotlin/spms/client/cli/modes/Run.kt index cfa42d6..e9d2087 100644 --- a/src/commonMain/kotlin/spms/client/cli/modes/Run.kt +++ b/src/commonMain/kotlin/spms/client/cli/modes/Run.kt @@ -23,7 +23,7 @@ import libzmq.ZMQ_NOBLOCK import spms.socketapi.Action import spms.socketapi.shared.SpMsSocketApi import spms.client.cli.CommandLineClientMode -import spms.client.cli.SERVER_REPLY_TIMEOUT_MS +import spms.client.cli.SERVER_REPLY_TIMEOUT import spms.localisation.loc import spms.socketapi.shared.SpMsServerHandshake import spms.socketapi.shared.SpMsActionReply @@ -33,6 +33,7 @@ import spms.socketapi.server.ServerAction import toRed import kotlin.system.getTimeMillis import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.time.* private fun CommandLineClientMode.jsonModeOption() = option("-j", "--json").flag().help { context.loc.server_actions.option_help_json } @@ -120,12 +121,13 @@ class ActionCommandLineClientMode( val reply: SpMsActionReply? = executeActionOnSocket( action, parameter_values, - SERVER_REPLY_TIMEOUT_MS, - currentContext, silent = silent + SERVER_REPLY_TIMEOUT, + currentContext, + silent = silent ) if (reply == null) { - throw CliktError(currentContext.loc.server_actions.replyNotReceived(SERVER_REPLY_TIMEOUT_MS).toRed()) + throw CliktError(currentContext.loc.server_actions.replyNotReceived(SERVER_REPLY_TIMEOUT).toRed()) } else if (reply.success) { if (json_mode || parent_json_mode) { @@ -152,7 +154,7 @@ class ActionCommandLineClientMode( private fun executeActionOnSocket( action: Action, parameter_values: List, - reply_timeout_ms: Long?, + reply_timeout: Duration?, context: Context, silent: Boolean = false ): SpMsActionReply? { @@ -168,7 +170,7 @@ class ActionCommandLineClientMode( println(context.loc.server_actions.actionSentAndWaitingForReply(action.identifier)) } - val timeout_end: Long? = reply_timeout_ms?.let { getTimeMillis() + it } + val timeout_end: TimeMark? = reply_timeout?.let { TimeSource.Monotonic.markNow() + it } do { if (!reply.isNullOrEmpty()) { if (!silent) { @@ -190,7 +192,7 @@ class ActionCommandLineClientMode( else if (!silent) { println(context.loc.server_actions.receivedEmptyReplyFromServer(action.identifier)) } - } while (timeout_end == null || getTimeMillis() < timeout_end) + } while (timeout_end == null || timeout_end.hasNotPassedNow()) return null } diff --git a/src/commonMain/kotlin/spms/client/player/PlayerClient.kt b/src/commonMain/kotlin/spms/client/player/PlayerClient.kt index 7b53bc3..fd09c18 100644 --- a/src/commonMain/kotlin/spms/client/player/PlayerClient.kt +++ b/src/commonMain/kotlin/spms/client/player/PlayerClient.kt @@ -24,16 +24,17 @@ import spms.socketapi.parseSocketMessage import spms.socketapi.player.PlayerAction import spms.socketapi.shared.* import kotlin.system.getTimeMillis +import kotlin.time.* -private const val SERVER_REPLY_TIMEOUT_MS: Long = 2000 -private const val SERVER_EVENT_TIMEOUT_MS: Long = 11000 -private const val POLL_INTERVAL_MS: Long = 100 -private const val CLIENT_REPLY_TIMEOUT_MS: Long = 1000 +private val SERVER_REPLY_TIMEOUT: Duration = with (Duration) { 2.seconds } +private val SERVER_EVENT_TIMEOUT: Duration = with (Duration) { 11.seconds } +private val POLL_INTERVAL: Duration = with (Duration) { 100.milliseconds } +private val CLIENT_REPLY_TIMEOUT: Duration = with (Duration) { 1.seconds } private fun getClientName(): String = "SpMs Player Client" -private abstract class PlayerImpl(headless: Boolean = true): MpvClientImpl(headless) { +private abstract class PlayerImpl(headless: Boolean = true): MpvClientImpl(headless = headless, playlist_auto_progress = false) { override fun onEvent(event: SpMsPlayerEvent, clientless: Boolean) { if (event.type == SpMsPlayerEvent.Type.READY_TO_PLAY) { onReadyToPlay() @@ -183,11 +184,11 @@ class PlayerClient private constructor(): Command( while (!shutdown) { try { - delay(POLL_INTERVAL_MS) + delay(POLL_INTERVAL) // We don't actually care about the client handshake, it's just for consistency with the main server api // val handshake_message: List = - socket.recvStringMultipart(CLIENT_REPLY_TIMEOUT_MS) ?: continue + socket.recvStringMultipart(CLIENT_REPLY_TIMEOUT) ?: continue val handshake_reply: SpMsServerHandshake = SpMsServerHandshake( @@ -203,7 +204,7 @@ class PlayerClient private constructor(): Command( ) val message: List = - socket.recvStringMultipart(CLIENT_REPLY_TIMEOUT_MS) ?: continue + socket.recvStringMultipart(CLIENT_REPLY_TIMEOUT) ?: continue val reply: List = parseSocketMessage(message) { action_name, action_params -> @@ -241,9 +242,9 @@ class PlayerClient private constructor(): Command( ) socket.sendStringMultipart(listOf(json.encodeToString(handshake))) - val reply: List? = socket.recvStringMultipart(SERVER_REPLY_TIMEOUT_MS) + val reply: List? = socket.recvStringMultipart(SERVER_REPLY_TIMEOUT) if (reply == null) { - throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotRespond(SERVER_REPLY_TIMEOUT_MS)) + throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotRespond(SERVER_REPLY_TIMEOUT)) } val server_handshake: SpMsServerHandshake = Json.decodeFromString(reply.first()) @@ -258,7 +259,7 @@ class PlayerClient private constructor(): Command( val message: MutableList = mutableListOf() while (!shutdown) { -// delay(POLL_INTERVAL_MS) +// delay(POLL_INTERVAL) val events: List = socket.pollEvents() for (event in events) { @@ -285,14 +286,15 @@ class PlayerClient private constructor(): Command( } private fun ZmqSocket.pollEvents(): List { - val wait_end: Long = getTimeMillis() + SERVER_EVENT_TIMEOUT_MS + val wait_start: TimeMark = TimeSource.Monotonic.markNow() var events: List? = null - while (events == null && getTimeMillis() < wait_end) { - val message: List? = + while (events == null && wait_start.elapsedNow() < SERVER_EVENT_TIMEOUT) { + val message: List? = with (Duration) { recvStringMultipart( - (wait_end - getTimeMillis()).coerceAtLeast(ZMQ_NOBLOCK.toLong()) + (SERVER_EVENT_TIMEOUT - wait_start.elapsedNow()).inWholeMilliseconds.coerceAtLeast(ZMQ_NOBLOCK.toLong()).milliseconds ) + } events = message?.mapNotNull { try { @@ -306,7 +308,7 @@ class PlayerClient private constructor(): Command( } if (events == null) { - throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotSendEvents(SERVER_EVENT_TIMEOUT_MS)) + throw SpMsCommandLineClientError(currentContext.loc.cli.errServerDidNotSendEvents(SERVER_EVENT_TIMEOUT)) } return events.orEmpty() diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt index e7fa782..f764110 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt @@ -1,5 +1,7 @@ package spms.localisation.strings +import kotlin.time.Duration + interface CliLocalisation { val bug_report_notice: String @@ -18,8 +20,8 @@ interface CliLocalisation { val status_key_state: String val status_key_is_playing: String val status_key_current_item_index: String - val status_key_current_position_ms: String - val status_key_duration_ms: String + val status_key_current_position: String + val status_key_duration: String val status_key_repeat_mode: String fun connectingToSocket(address: String): String @@ -30,6 +32,6 @@ interface CliLocalisation { val poll_polling_server_for_events: String - fun errServerDidNotRespond(timeout_ms: Long): String - fun errServerDidNotSendEvents(timeout_ms: Long): String + fun errServerDidNotRespond(timeout: Duration): String + fun errServerDidNotSendEvents(timeout: Duration): String } diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt index 1d61abc..9cdf40a 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt @@ -1,6 +1,7 @@ package spms.localisation.strings import spms.server.BUG_REPORT_URL +import kotlin.time.Duration class CliLocalisationEn: CliLocalisation { override val bug_report_notice: String = "Report bugs at $BUG_REPORT_URL" @@ -25,8 +26,8 @@ class CliLocalisationEn: CliLocalisation { override val status_key_state: String = "Playback state" override val status_key_is_playing: String = "Is playing" override val status_key_current_item_index: String = "Current item index" - override val status_key_current_position_ms: String = "Item position (ms)" - override val status_key_duration_ms: String = "Item duration (ms)" + override val status_key_current_position: String = "Item position" + override val status_key_duration: String = "Item duration" override val status_key_repeat_mode: String = "Repeat mode" override fun connectingToSocket(address: String): String = @@ -38,8 +39,8 @@ class CliLocalisationEn: CliLocalisation { override val poll_polling_server_for_events: String = "Polling server for events..." - override fun errServerDidNotRespond(timeout_ms: Long): String = - "Server did not respond within timeout (${timeout_ms}ms)" - override fun errServerDidNotSendEvents(timeout_ms: Long): String = - "Server did not send events within timeout (${timeout_ms}ms)" + override fun errServerDidNotRespond(timeout: Duration): String = + "Server did not respond within timeout ($timeout)" + override fun errServerDidNotSendEvents(timeout: Duration): String = + "Server did not send events within timeout ($timeout)" } diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt index 219c3e7..62e0634 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt @@ -1,6 +1,7 @@ package spms.localisation.strings import spms.server.BUG_REPORT_URL +import kotlin.time.Duration class CliLocalisationJa: CliLocalisation { override val bug_report_notice: String = "$BUG_REPORT_URL にバグを報告してください" @@ -25,8 +26,8 @@ class CliLocalisationJa: CliLocalisation { override val status_key_state: String = "再生状態" override val status_key_is_playing: String = "再生中" override val status_key_current_item_index: String = "再生中のアイテムのインデックス" - override val status_key_current_position_ms: String = "アイテム内の時間(ミリ秒)" - override val status_key_duration_ms: String = "アイテムの長さ(ミリ秒)" + override val status_key_current_position: String = "アイテム内の時間" + override val status_key_duration: String = "アイテムの長さ" override val status_key_repeat_mode: String = "リピートモード" override fun connectingToSocket(address: String): String = @@ -38,8 +39,8 @@ class CliLocalisationJa: CliLocalisation { override val poll_polling_server_for_events: String = "サーバーのイベントをポール中..." - override fun errServerDidNotRespond(timeout_ms: Long): String = - "タイムアウト(${timeout_ms}ミリ秒)の内にサーバーから返信が来ませんでした" - override fun errServerDidNotSendEvents(timeout_ms: Long): String = - "タイムアウト(${timeout_ms}ミリ秒)の内にサーバーからイベントが来ませんでした" + override fun errServerDidNotRespond(timeout: Duration): String = + "タイムアウト($timeout)の内にサーバーから返信が来ませんでした" + override fun errServerDidNotSendEvents(timeout: Duration): String = + "タイムアウト($timeout)の内にサーバーからイベントが来ませんでした" } diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisation.kt b/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisation.kt index 16c2e90..6af191d 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisation.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisation.kt @@ -1,11 +1,13 @@ package spms.localisation.strings +import kotlin.time.Duration + interface ServerActionLocalisation { fun sendingActionToServer(action_identifier: String): String fun actionSentAndWaitingForReply(action_identifier: String): String fun receivedReplyFromServer(action_identifier: String): String fun receivedEmptyReplyFromServer(action_identifier: String): String - fun replyNotReceived(timeout_ms: Long): String + fun replyNotReceived(timeout: Duration): String val server_completed_request_successfully: String fun serverDidNotCompleteRequest(error: String, cause: String): String diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationEn.kt b/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationEn.kt index 4e686d9..3ac05c2 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationEn.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationEn.kt @@ -1,5 +1,7 @@ package spms.localisation.strings +import kotlin.time.Duration + class ServerActionLocalisationEn: ServerActionLocalisation { override fun sendingActionToServer(action_identifier: String): String = "Sending action '$action_identifier' to server..." @@ -11,8 +13,8 @@ class ServerActionLocalisationEn: ServerActionLocalisation { override fun receivedEmptyReplyFromServer(action_identifier: String): String = "Received empty reply from server for action '$action_identifier', continuing to wait..." - override fun replyNotReceived(timeout_ms: Long): String = - "Did not receive valid reply from server within timeout (${timeout_ms}ms)" + override fun replyNotReceived(timeout: Duration): String = + "Did not receive valid reply from server within timeout ($timeout)" override val server_completed_request_successfully: String = "The server completed the request successfully" override fun serverDidNotCompleteRequest(error: String, cause: String): String = diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationJa.kt b/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationJa.kt index 17c42e2..0e34db7 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationJa.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationJa.kt @@ -1,5 +1,7 @@ package spms.localisation.strings +import kotlin.time.Duration + class ServerActionLocalisationJa: ServerActionLocalisation { override fun sendingActionToServer(action_identifier: String): String = "アクション「$action_identifier」をサーバーに送信中..." @@ -11,8 +13,8 @@ class ServerActionLocalisationJa: ServerActionLocalisation { override fun receivedEmptyReplyFromServer(action_identifier: String): String = "サーバーから「$action_identifier」アクションの空の返信を受け取りました。返信を待つ..." - override fun replyNotReceived(timeout_ms: Long): String = - "タイムアウト(${timeout_ms}ミリ秒)の内にサーバから正確な返信が届きませんでした" + override fun replyNotReceived(timeout: Duration): String = + "タイムアウト($timeout)の内にサーバから正確な返信が届きませんでした" override val server_completed_request_successfully: String = "サーバーがリクエストを正常に完了しました" override fun serverDidNotCompleteRequest(error: String, cause: String): String = diff --git a/src/commonMain/kotlin/spms/player/HeadlessPlayer.kt b/src/commonMain/kotlin/spms/player/headless/HeadlessPlayer.kt similarity index 84% rename from src/commonMain/kotlin/spms/player/HeadlessPlayer.kt rename to src/commonMain/kotlin/spms/player/headless/HeadlessPlayer.kt index 6e5ac1d..224c46a 100644 --- a/src/commonMain/kotlin/spms/player/HeadlessPlayer.kt +++ b/src/commonMain/kotlin/spms/player/headless/HeadlessPlayer.kt @@ -1,4 +1,4 @@ -package spms.player +package spms.player.headless import kotlinx.atomicfu.locks.ReentrantLock import kotlinx.atomicfu.locks.withLock @@ -11,14 +11,16 @@ import kotlinx.serialization.json.JsonPrimitive import spms.socketapi.shared.SpMsPlayerEvent import spms.socketapi.shared.SpMsPlayerRepeatMode import spms.socketapi.shared.SpMsPlayerState +import spms.player.* import kotlin.system.getTimeNanos +import kotlin.time.* abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Player { - protected abstract fun getCachedItemDuration(item_id: String): Long? - protected abstract suspend fun loadItemDuration(item_id: String): Long - fun onDurationLoaded(item_id: String, item_duration_ms: Long) { + protected abstract fun getCachedItemDuration(item_id: String): Duration? + protected abstract suspend fun loadItemDuration(item_id: String): Duration + fun onDurationLoaded(item_id: String, item_duration: Duration) { if (item_id == getItem()) { - onEvent(SpMsPlayerEvent.PropertyChanged("duration_ms", JsonPrimitive(item_duration_ms))) + onEvent(SpMsPlayerEvent.PropertyChanged("duration_ms", JsonPrimitive(item_duration.inWholeMilliseconds))) } } @@ -32,26 +34,24 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe _state = value onEvent(SpMsPlayerEvent.PropertyChanged("state", JsonPrimitive(value.ordinal))) } - final override var is_playing: Boolean = false - private set + final override val is_playing: Boolean get() = playback_state.is_playing final override val item_count: Int get() = queue.size final override var current_item_index: Int = -1 private set + private var playback_state: PlaybackState = PlaybackState.Paused(Duration.ZERO) + final override val current_position_ms: Long - get() = - if (is_playing) (getTimeNanos() - playback_mark) / 1000000 - else playback_mark - final override val duration_ms: Long get() = queue.getOrNull(current_item_index)?.let { getCachedItemDuration(it) } ?: 0 + get() = playback_state.current_position.inWholeMilliseconds + final override val duration_ms: Long get() = queue.getOrNull(current_item_index)?.let { getCachedItemDuration(it)?.inWholeMilliseconds } ?: 0 final override var repeat_mode: SpMsPlayerRepeatMode = SpMsPlayerRepeatMode.NONE private set private val queue: MutableList = mutableListOf() private val playback_scope: CoroutineScope = CoroutineScope(Dispatchers.Default) - private var playback_mark: Long = 0 private val playback_lock: ReentrantLock = ReentrantLock() private inline fun withLock(block: () -> T): T = playback_lock.withLock(block) @@ -60,7 +60,7 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe private fun log(message: Any?) { if (enable_logging) { - println("HeadlessPlayer: $message") + println("HeadlessPlayer (state=$playback_state): $message") } } @@ -83,8 +83,7 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe } else { current_item_index++ - playback_mark = 0 - onQueueOrPositionChanged() + playback_state = PlaybackState.Paused(Duration.ZERO) onItemTransition(current_item_index) } } @@ -94,11 +93,8 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe val item_id: String = queue.getOrNull(to) ?: return - val duration: Long? = getCachedItemDuration(item_id) - onEvent(SpMsPlayerEvent.PropertyChanged("duration_ms", JsonPrimitive(duration ?: 0))) - } - - private fun onQueueOrPositionChanged() { + val duration: Duration? = getCachedItemDuration(item_id) + onEvent(SpMsPlayerEvent.PropertyChanged("duration_ms", JsonPrimitive(duration?.inWholeMilliseconds ?: 0))) } override fun play() { @@ -109,7 +105,6 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe if (current_item_index < 0) { if (queue.isNotEmpty()) { current_item_index = 0 - onQueueOrPositionChanged() onItemTransition(0) } else { @@ -118,18 +113,16 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe } } - val current_position: Long = playback_mark - playback_mark = getTimeNanos() - current_position - val item_id: String = queue[current_item_index] - var duration: Long? = getCachedItemDuration(item_id) + var duration: Duration? = getCachedItemDuration(item_id) if (duration == null) { state = SpMsPlayerState.BUFFERING + playback_state = playback_state.toPaused() } else { state = SpMsPlayerState.READY - is_playing = true + playback_state = playback_state.toPlaying() onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(true))) } @@ -141,17 +134,9 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe duration = loadItemDuration(item_id) - if (duration == null) { - state = SpMsPlayerState.IDLE - is_playing = false - playback_mark = 0 - println("play(): Duration is null, cannot play") - TODO() - } - withLock { state = SpMsPlayerState.READY - is_playing = true + playback_state = playback_state.toPlaying() onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(true))) } } @@ -159,14 +144,14 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe log("play() $item_id: Using existing duration") } - log("play() will wait for ${duration!! - current_position}ms (${duration} $current_position)") - delay(duration!! - current_position) + val wait_duration: Duration = duration!! - playback_state.current_position + log("play() will wait for ${wait_duration}ms ($duration)") + delay(wait_duration) log("play() resumed after delay") withLock { - is_playing = false state = SpMsPlayerState.IDLE - playback_mark = duration!! + playback_state = PlaybackState.Paused(duration) onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(false))) onItemPlaybackEnded() } @@ -182,8 +167,6 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe if (state == SpMsPlayerState.READY) { onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(false))) } - - playback_mark = current_position_ms stop() } else { @@ -206,7 +189,7 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe private fun stop() { playback_scope.coroutineContext.cancelChildren() - is_playing = false + playback_state = playback_state.toPaused() state = SpMsPlayerState.IDLE } @@ -227,7 +210,9 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe withLock { val target_position: Long = position_ms.coerceIn(0, duration_ms) modifyPlayback { - playback_mark = target_position + playback_state = with (Duration) { + PlaybackState.Paused(target_position.milliseconds) + } } onEvent(SpMsPlayerEvent.SeekedToTime(target_position)) @@ -236,7 +221,7 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe private fun performSeekToItem(index: Int) { stop() - playback_mark = 0 + playback_state = PlaybackState.Paused(Duration.ZERO) current_item_index = index onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(false))) } @@ -249,7 +234,6 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe performSeekToItem(target_index) seekToTime(position_ms) - onQueueOrPositionChanged() onItemTransition(target_index) } } @@ -268,7 +252,6 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe performSeekToItem(seek_target) - onQueueOrPositionChanged() onItemTransition(current_item_index) return true @@ -284,7 +267,6 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe performSeekToItem(current_item_index - 1) - onQueueOrPositionChanged() onItemTransition(current_item_index) return true @@ -338,7 +320,6 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe current_item_index++ } - onQueueOrPositionChanged() onEvent(SpMsPlayerEvent.ItemAdded(item_id, target_index)) if (queue.size == 1) { @@ -366,7 +347,6 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe current_item_index = target_to } - onQueueOrPositionChanged() onEvent(SpMsPlayerEvent.ItemMoved(target_from, target_to)) } } @@ -388,14 +368,13 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe current_item_index-- } modifyPlayback { - playback_mark = 0 + playback_state = PlaybackState.Paused(Duration.ZERO) } } else if (target_index < current_item_index) { current_item_index-- } - onQueueOrPositionChanged() onEvent(SpMsPlayerEvent.ItemRemoved(target_index)) if (target_index == current_item_index) { @@ -410,7 +389,7 @@ abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Playe stop() queue.clear() current_item_index = -1 - playback_mark = 0 + playback_state = PlaybackState.Paused(Duration.ZERO) onEvent(SpMsPlayerEvent.QueueCleared()) onItemTransition(-1) diff --git a/src/commonMain/kotlin/spms/player/headless/PlaybackState.kt b/src/commonMain/kotlin/spms/player/headless/PlaybackState.kt new file mode 100644 index 0000000..9b89f7e --- /dev/null +++ b/src/commonMain/kotlin/spms/player/headless/PlaybackState.kt @@ -0,0 +1,34 @@ +package spms.player.headless + +import kotlin.time.* + +internal interface PlaybackState { + val is_playing: Boolean + val current_position: Duration + + fun toPlaying(): PlaybackState + fun toPaused(): PlaybackState + + class Playing(initial_position: Duration): PlaybackState { + private val start_time: ComparableTimeMark = TimeSource.Monotonic.markNow() - initial_position + + override val is_playing: Boolean = true + override val current_position: Duration get() = TimeSource.Monotonic.markNow() - start_time + + override fun toPlaying(): PlaybackState = this + override fun toPaused(): PlaybackState = Paused(current_position) + + override fun toString(): String = + "PlaybackState.Playing(current_position=$current_position)" + } + + class Paused(override val current_position: Duration): PlaybackState { + override val is_playing: Boolean = false + + override fun toPlaying(): PlaybackState = Playing(current_position) + override fun toPaused(): PlaybackState = this + + override fun toString(): String = + "PlaybackState.Paused(current_position=$current_position)" + } +} diff --git a/src/commonMain/kotlin/spms/server/SpMs.kt b/src/commonMain/kotlin/spms/server/SpMs.kt index 695462f..54dc40a 100644 --- a/src/commonMain/kotlin/spms/server/SpMs.kt +++ b/src/commonMain/kotlin/spms/server/SpMs.kt @@ -13,8 +13,8 @@ import okio.Path import okio.Path.Companion.toPath import platform.posix.getenv import spms.getHostname -import spms.player.HeadlessPlayer import spms.player.Player +import spms.player.headless.HeadlessPlayer import spms.socketapi.parseSocketMessage import spms.socketapi.player.PlayerAction import spms.socketapi.server.ServerAction @@ -23,8 +23,9 @@ import spms.localisation.SpMsLocalisation import kotlin.experimental.ExperimentalNativeApi import kotlin.system.exitProcess import kotlin.system.getTimeMillis +import kotlin.time.* -private const val CLIENT_REPLY_TIMEOUT_MS: Long = 100 +private val CLIENT_REPLY_TIMEOUT: Duration = with (Duration) { 100.milliseconds } @OptIn(ExperimentalForeignApi::class) class SpMs( @@ -33,7 +34,7 @@ class SpMs( enable_gui: Boolean = false, enable_media_session: Boolean = false ): ZmqRouter(mem_scope) { - private var item_durations: MutableMap = mutableMapOf() + private var item_durations: MutableMap = mutableMapOf() private val item_durations_channel: Channel = Channel() private var executing_client_id: Int? = null @@ -45,10 +46,10 @@ class SpMs( val player: Player = if (headless || !MpvClientImpl.isAvailable()) object : HeadlessPlayer() { - override fun getCachedItemDuration(item_id: String): Long? = item_durations[item_id] + override fun getCachedItemDuration(item_id: String): Duration? = item_durations[item_id] - override suspend fun loadItemDuration(item_id: String): Long { - var cached: Long? = item_durations[item_id] + override suspend fun loadItemDuration(item_id: String): Duration { + var cached: Duration? = item_durations[item_id] while (cached == null) { item_durations_channel.receive() cached = item_durations[item_id] @@ -135,12 +136,12 @@ class SpMs( } // Wait for client to reply - val wait_end: Long = getTimeMillis() + CLIENT_REPLY_TIMEOUT_MS + val wait_start: TimeMark = TimeSource.Monotonic.markNow() var client_reply: Message? = null while (true) { - val remaining = wait_end - getTimeMillis() - if (remaining <= 0) { + val remaining: Duration = CLIENT_REPLY_TIMEOUT - wait_start.elapsedNow() + if (remaining <= Duration.ZERO) { break } @@ -214,7 +215,7 @@ class SpMs( } } - fun onClientReadyToPlay(client_id: SpMsClientID, item_index: Int, item_id: String, item_duration_ms: Long) { + fun onClientReadyToPlay(client_id: SpMsClientID, item_index: Int, item_id: String, item_duration: Duration) { if (!playback_waiting_for_clients) { println("Got readyToPlay from a client, but playback_waiting_for_clients is false, ignoring") return @@ -226,8 +227,8 @@ class SpMs( return } - if (item_duration_ms <= 0) { - println("Got readyToPlay from a $ready_client with invalid duration ($item_duration_ms), ignoring") + if (item_duration <= Duration.ZERO) { + println("Got readyToPlay from $ready_client with invalid duration ($item_duration), ignoring") return } @@ -236,15 +237,15 @@ class SpMs( return } - item_durations[item_id] = item_duration_ms + item_durations[item_id] = item_duration if (player is HeadlessPlayer) { - player.onDurationLoaded(item_id, item_duration_ms) + player.onDurationLoaded(item_id, item_duration) } item_durations_channel.trySend(Unit) if (ready_client.ready_to_play) { - println("Got readyToPlay from $ready_client, but it is already marked as ready, ignoring (duration=$item_duration_ms)") + println("Got readyToPlay from $ready_client, but it is already marked as ready, ignoring (duration=$item_duration)") return } @@ -253,13 +254,13 @@ class SpMs( val waiting_for: Int = clients.count { it.type.playsAudio() && !it.ready_to_play } if (waiting_for == 0) { - println("Got readyToPlay from $ready_client, beginning playback (duration=$item_duration_ms)") + println("Got readyToPlay from $ready_client, beginning playback (duration=$item_duration)") playback_waiting_for_clients = false player.play() } else { - println("Got readyToPlay from $ready_client, but still waiting for $waiting_for other clients to be ready (duration=$item_duration_ms)") + println("Got readyToPlay from $ready_client, but still waiting for $waiting_for other clients to be ready (duration=$item_duration)") } } diff --git a/src/commonMain/kotlin/spms/server/SpMsCommand.kt b/src/commonMain/kotlin/spms/server/SpMsCommand.kt index 458f688..2ad1062 100644 --- a/src/commonMain/kotlin/spms/server/SpMsCommand.kt +++ b/src/commonMain/kotlin/spms/server/SpMsCommand.kt @@ -34,7 +34,7 @@ const val PROJECT_URL: String = "https://github.com/toasterofbread/spmp-server" const val BUG_REPORT_URL: String = PROJECT_URL + "/issues" const val DEFAULT_ADDRESS: String = "127.0.0.1" -private const val POLL_INTERVAL_MS: Long = 100 +private const val POLL_INTERVAL: Long = 100 private const val CLIENT_REPLY_ATTEMPTS: Int = 10 @Suppress("OPT_IN_USAGE") @@ -153,7 +153,7 @@ class SpMsCommand: Command( println("--- ${localisation.server.polling_started} ---") while (!stop) { server.poll(CLIENT_REPLY_ATTEMPTS) - delay(POLL_INTERVAL_MS) + delay(POLL_INTERVAL) } println("--- ${localisation.server.polling_ended} ---") diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt b/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt index 2138f30..dcaf5c5 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt +++ b/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt @@ -19,6 +19,7 @@ import spms.server.SpMs import spms.socketapi.shared.SpMsClientID import spms.socketapi.shared.SpMsPlayerRepeatMode import spms.socketapi.shared.SpMsPlayerState +import kotlin.time.Duration @Suppress("OPT_IN_USAGE") @OptIn(ExperimentalForeignApi::class) @@ -81,8 +82,8 @@ class ServerActionGetStatus: ServerAction( "state" -> status_key_state "is_playing" -> status_key_is_playing "current_item_index" -> status_key_current_item_index - "current_position_ms" -> status_key_current_position_ms - "duration_ms" -> status_key_duration_ms + "current_position_ms" -> status_key_current_position + "duration_ms" -> status_key_duration "repeat_mode" -> status_key_repeat_mode else -> entry.key.replaceFirstChar { it.uppercaseChar() }.replace('_', ' ') } @@ -112,6 +113,9 @@ class ServerActionGetStatus: ServerAction( } "state" -> value.jsonPrimitive.intOrNull?.let { SpMsPlayerState.values().getOrNull(it)?.name } ?: value.jsonPrimitive.toString() "repeat_mode" -> value.jsonPrimitive.intOrNull?.let { SpMsPlayerRepeatMode.values().getOrNull(it)?.name } ?: value.jsonPrimitive.toString() + "current_position_ms", "duration_ms" -> with (Duration) { + value.jsonPrimitive.longOrNull?.let { it.milliseconds }.toString() + } else -> value.toString() } } diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionReadyToPlay.kt b/src/commonMain/kotlin/spms/socketapi/server/ServerActionReadyToPlay.kt index f9a0ea6..1ff6157 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionReadyToPlay.kt +++ b/src/commonMain/kotlin/spms/socketapi/server/ServerActionReadyToPlay.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long import spms.server.SpMs import spms.socketapi.shared.SpMsClientID +import kotlin.time.Duration class ServerActionReadyToPlay: ServerAction( identifier = "readyToPlay", @@ -38,7 +39,9 @@ class ServerActionReadyToPlay: ServerAction( client, context.getParameterValue("item_index")!!.jsonPrimitive.int, context.getParameterValue("item_id")!!.jsonPrimitive.content, - context.getParameterValue("item_duration_ms")!!.jsonPrimitive.long + with (Duration) { + context.getParameterValue("item_duration_ms")!!.jsonPrimitive.long.milliseconds + } ) return null }