diff --git a/build.gradle.kts b/build.gradle.kts index 38606e9..9c5bf6e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,6 @@ import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenRootPlugin.Compan import org.jetbrains.kotlin.gradle.tasks.CompileUsingKotlinDaemon import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile -val FLAG_LINK_STATIC: String = "linkStatic" val GENERATED_FILE_PREFIX: String = "// Generated on build in build.gradle.kts\n" plugins { @@ -24,11 +23,20 @@ repositories { maven("https://jitpack.io") } -val platform_specific_files: List = listOf( - "cinterop/indicator/TrayIndicatorImpl.kt", - "spms/Platform.kt", - "spms/server/SpMsMediaSession.kt" -) +val PLATFORM_SPECIFIC_FILES: List = + listOf( + "cinterop/indicator/TrayIndicatorImpl.kt", + "spms/Platform.kt", + "spms/server/SpMsMediaSession.kt" + ) + +enum class BuildFlag { + DISABLE_MPV, + LINK_STATIC; + + fun isEnabled(project: Project): Boolean = + project.hasProperty(name) +} enum class Arch { X86_64, ARM64; @@ -146,6 +154,18 @@ enum class CinteropLibraries { else -> true } + fun includeInProject(project: Project): Boolean = + when (this) { + LIBMPV -> !BuildFlag.DISABLE_MPV.isEnabled(project) + else -> true + } + + fun getDependentFiles(): List = + when (this) { + LIBMPV -> listOf("cinterop/mpv/LibMpvClient.kt", "cinterop/mpv/MpvClientEventLoop.kt") + else -> emptyList() + } + fun getBinaryDependencies(platform: Platform): List = when (this) { LIBMPV -> @@ -323,14 +343,14 @@ fun KotlinMultiplatformExtension.configureKotlinTarget(platform: Platform) { Platform.OSX_ARM -> macosArm64(platform.identifier) } - val static: Boolean = project.hasProperty(FLAG_LINK_STATIC) + val static: Boolean = BuildFlag.LINK_STATIC.isEnabled(project) val deps_directory: File = platform.getNativeDependenciesDir(project) native_target.apply { compilations.getByName("main") { cinterops { for (library in CinteropLibraries.values()) { - if (!library.includeOnPlatform(platform)) { + if (!library.includeOnPlatform(platform) || !library.includeInProject(project)) { continue } @@ -354,7 +374,7 @@ fun KotlinMultiplatformExtension.configureKotlinTarget(platform: Platform) { } for (library in CinteropLibraries.values()) { - if (!library.includeOnPlatform(platform)) { + if (!library.includeOnPlatform(platform) || !library.includeInProject(project)) { continue } library.configureExecutable(platform, static, this, deps_directory) @@ -436,6 +456,7 @@ tasks.register("bundleIcon") { } } + for (platform in Platform.supported) { val print_target_task by tasks.register("printTarget${platform.identifier}") { doLast { @@ -443,13 +464,43 @@ for (platform in Platform.supported) { } } - val configure_platform_files_task by tasks.register("configurePlatformFiles${platform.identifier}") { + fun String.getFile(suffix: String? = null): File = + project.file("src/commonMain/kotlin/" + if (suffix == null) this.replace(".kt", ".gen.kt") else (this + suffix)) + + val configure_library_dependent_files_task by tasks.register("configureLibraryDependentFiles${platform.identifier}") { outputs.upToDateWhen { false } - fun String.getFile(suffix: String? = null): File = - project.file("src/commonMain/kotlin/" + if (suffix == null) this.replace(".kt", ".gen.kt") else (this + suffix)) + for (library in CinteropLibraries.values()) { + for (path in library.getDependentFiles()) { + outputs.file(path.getFile()) + inputs.file(path.getFile(".enabled")) + inputs.file(path.getFile(".disabled")) + } + } + + doLast { + for (library in CinteropLibraries.values()) { + val enabled: Boolean = library.includeOnPlatform(platform) && library.includeInProject(project) + + for (path in library.getDependentFiles()) { + val out_file: File = path.getFile() + val in_file: File = path.getFile(if (enabled) ".enabled" else ".disabled") + + out_file.writer().use { writer -> + writer.write(GENERATED_FILE_PREFIX) + + in_file.reader().use { reader -> + reader.transferTo(writer) + } + } + } + } + } + } + val configure_platform_files_task by tasks.register("configurePlatformFiles${platform.identifier}") { + outputs.upToDateWhen { false } - for (path in platform_specific_files) { + for (path in PLATFORM_SPECIFIC_FILES) { outputs.file(path.getFile()) val input_files: List = @@ -462,7 +513,7 @@ for (platform in Platform.supported) { } doLast { - for (path in platform_specific_files) { + for (path in PLATFORM_SPECIFIC_FILES) { val out_file: File = path.getFile() for (platform_file in listOf( @@ -494,6 +545,7 @@ for (platform in Platform.supported) { dependsOn(print_target_task) dependsOn("bundleIcon") dependsOn(configure_platform_files_task) + dependsOn(configure_library_dependent_files_task) } val check_dependencies_task by tasks.register("checkDependencies${platform.identifier}") { diff --git a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled new file mode 100644 index 0000000..e9a6dbc --- /dev/null +++ b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled @@ -0,0 +1,35 @@ +package cinterop.mpv + +import spms.player.Player +import kotlinx.cinterop.CPrimitiveVar +import kotlinx.cinterop.MemScope +import kotlin.reflect.KClass + +typealias MpvFormat = Unit + +class mpv_event { + val event_id: Int get() = throw NotImplementedError() +} + +abstract class LibMpvClient(val headless: Boolean = true): Player { + companion object { + fun isAvailable(): Boolean = false + } + + init { + throw NotImplementedError() + } + + override fun release() { throw NotImplementedError() } + + protected fun runCommand(name: String, vararg args: Any?, check_result: Boolean = true): Int = throw NotImplementedError() + protected inline fun getProperty(name: String): V = throw NotImplementedError() + protected inline fun setProperty(name: String, value: T) { throw NotImplementedError() } + protected fun observeProperty(name: String, cls: KClass<*>) { throw NotImplementedError() } + protected inline fun MemScope.getPointerOf(v: T? = null): CPrimitiveVar = throw NotImplementedError() + protected fun getFormatOf(cls: KClass<*>): MpvFormat = throw NotImplementedError() + protected fun waitForEvent(): mpv_event? = throw NotImplementedError() + protected fun requestLogMessages() { throw NotImplementedError() } + protected fun addHook(name: String, priority: Int = 0) { throw NotImplementedError() } + protected fun continueHook(id: ULong) { throw NotImplementedError() } +} diff --git a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled similarity index 94% rename from src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt rename to src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled index 5a02280..5b8f405 100644 --- a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt +++ b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled @@ -10,11 +10,15 @@ import libmpv.mpv_format as MpvFormat @OptIn(ExperimentalForeignApi::class) abstract class LibMpvClient(val headless: Boolean = true): Player { + companion object { + fun isAvailable(): Boolean = true + } + protected val ctx: CPointer init { ctx = mpv_create_client(null, null) - ?: throw NullPointerException("Creating MPV client failed") + ?: throw NullPointerException("Creating mpv client failed") memScoped { val vid: BooleanVar = alloc() @@ -31,7 +35,7 @@ abstract class LibMpvClient(val headless: Boolean = true): Player { } val init_result: Int = mpv_initialize(ctx) - check(init_result == 0) { "Initialising MPV client failed ($init_result)" } + check(init_result == 0) { "Initialising mpv client failed ($init_result)" } } override fun release() { @@ -124,7 +128,7 @@ abstract class LibMpvClient(val headless: Boolean = true): Player { else -> throw NotImplementedError(cls.toString()) } - protected fun waitForEvent(): mpv_event? = + internal fun waitForEvent(): mpv_event? = mpv_wait_event(ctx, -1.0)?.pointed protected fun requestLogMessages() { diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.disabled b/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.disabled new file mode 100644 index 0000000..fdf44da --- /dev/null +++ b/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.disabled @@ -0,0 +1,5 @@ +package cinterop.mpv + +internal suspend fun MpvClientImpl.eventLoop() { + throw NotImplementedError() +} diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.enabled b/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.enabled new file mode 100644 index 0000000..8df1f34 --- /dev/null +++ b/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.enabled @@ -0,0 +1,70 @@ +package cinterop.mpv + +import libmpv.* +import kotlinx.coroutines.* +import kotlinx.serialization.json.JsonPrimitive +import spms.server.SpMs +import spms.socketapi.shared.SpMsPlayerEvent +import cinterop.utils.safeToKString +import kotlinx.cinterop.* + +internal suspend fun MpvClientImpl.eventLoop() = withContext(Dispatchers.IO) { + while (true) { + val event: mpv_event? = waitForEvent() + + when (event?.event_id) { + MPV_EVENT_START_FILE -> { + val data: mpv_event_start_file = event.data.pointedAs() + if (data.playlist_entry_id.toInt() != current_item_playlist_id) { + continue + } + + onEvent(SpMsPlayerEvent.ItemTransition(current_item_index), clientless = true) + } + MPV_EVENT_PLAYBACK_RESTART -> { + onEvent(SpMsPlayerEvent.PropertyChanged("state", JsonPrimitive(state.ordinal)), clientless = true) + onEvent(SpMsPlayerEvent.PropertyChanged("duration_ms", JsonPrimitive(duration_ms)), clientless = true) + onEvent(SpMsPlayerEvent.SeekedToTime(current_position_ms), clientless = true) + } + MPV_EVENT_END_FILE -> { + onEvent(SpMsPlayerEvent.PropertyChanged("state", JsonPrimitive(state.ordinal)), clientless = true) + } + MPV_EVENT_FILE_LOADED -> { + onEvent(SpMsPlayerEvent.ReadyToPlay(), clientless = true) + + song_initial_seek_position_ms?.also { position_ms -> + seekToTime(position_ms) + song_initial_seek_position_ms = null + } + } + MPV_EVENT_SHUTDOWN -> { + onShutdown() + } + MPV_EVENT_PROPERTY_CHANGE -> { + val data: mpv_event_property = event.data.pointedAs() + + when (data.name?.safeToKString()) { + "core-idle" -> { + val playing: Boolean = !data.data.pointedAs().value + onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(playing)), clientless = true) + } + } + } + + MPV_EVENT_HOOK -> { + val data: mpv_event_hook = event.data.pointedAs() + launch { + onMpvHook(data.name?.safeToKString(), data.id) + } + } + + MPV_EVENT_LOG_MESSAGE -> { + if (SpMs.logging_enabled) { + val message: mpv_event_log_message = event.data.pointedAs() + SpMs.log("From mpv (${message.prefix?.safeToKString()}): ${message.text?.safeToKString()}") + } + } + } + } +} + diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt b/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt index 6c7c6b6..259efa2 100644 --- a/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt +++ b/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt @@ -3,7 +3,6 @@ package cinterop.mpv import kotlinx.cinterop.* import kotlinx.coroutines.* import kotlinx.serialization.json.JsonPrimitive -import libmpv.* import okio.FileSystem import okio.Path.Companion.toPath import spms.socketapi.shared.SpMsPlayerEvent @@ -22,11 +21,15 @@ inline fun CPointer<*>?.pointedAs(): T = @OptIn(ExperimentalForeignApi::class) abstract class MpvClientImpl(headless: Boolean = true): LibMpvClient(headless) { + companion object { + fun isAvailable(): Boolean = LibMpvClient.isAvailable() + } + private val coroutine_scope = CoroutineScope(Job()) private var auth_headers: Map? = null private val local_files: MutableMap = mutableMapOf() - private var song_initial_seek_position_ms: Long? = null + internal var song_initial_seek_position_ms: Long? = null private fun urlToId(url: String): String? = if (url.startsWith(URL_PREFIX)) url.drop(URL_PREFIX.length) else null private fun idToUrl(item_id: String): String = URL_PREFIX + item_id @@ -69,7 +72,7 @@ abstract class MpvClientImpl(headless: Boolean = true): LibMpvClient(headless) { } } - private val current_item_playlist_id: Int + internal val current_item_playlist_id: Int get() = getProperty("playlist/${current_item_index}/id") override fun play() { @@ -263,67 +266,7 @@ abstract class MpvClientImpl(headless: Boolean = true): LibMpvClient(headless) { } } - private suspend fun eventLoop() = withContext(Dispatchers.IO) { - while (true) { - val event: mpv_event? = waitForEvent() - - when (event?.event_id) { - MPV_EVENT_START_FILE -> { - val data: mpv_event_start_file = event.data.pointedAs() - if (data.playlist_entry_id.toInt() != current_item_playlist_id) { - continue - } - - onEvent(SpMsPlayerEvent.ItemTransition(current_item_index), clientless = true) - } - MPV_EVENT_PLAYBACK_RESTART -> { - onEvent(SpMsPlayerEvent.PropertyChanged("state", JsonPrimitive(state.ordinal)), clientless = true) - onEvent(SpMsPlayerEvent.PropertyChanged("duration_ms", JsonPrimitive(duration_ms)), clientless = true) - onEvent(SpMsPlayerEvent.SeekedToTime(current_position_ms), clientless = true) - } - MPV_EVENT_END_FILE -> { - onEvent(SpMsPlayerEvent.PropertyChanged("state", JsonPrimitive(state.ordinal)), clientless = true) - } - MPV_EVENT_FILE_LOADED -> { - onEvent(SpMsPlayerEvent.ReadyToPlay(), clientless = true) - - song_initial_seek_position_ms?.also { position_ms -> - seekToTime(position_ms) - song_initial_seek_position_ms = null - } - } - MPV_EVENT_SHUTDOWN -> { - onShutdown() - } - MPV_EVENT_PROPERTY_CHANGE -> { - val data: mpv_event_property = event.data.pointedAs() - - when (data.name?.safeToKString()) { - "core-idle" -> { - val playing: Boolean = !data.data.pointedAs().value - onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(playing)), clientless = true) - } - } - } - - MPV_EVENT_HOOK -> { - val data: mpv_event_hook = event.data.pointedAs() - launch { - onMpvHook(data.name?.safeToKString(), data.id) - } - } - - MPV_EVENT_LOG_MESSAGE -> { - if (SpMs.logging_enabled) { - val message: mpv_event_log_message = event.data.pointedAs() - SpMs.log("From mpv (${message.prefix?.safeToKString()}): ${message.text?.safeToKString()}") - } - } - } - } - } - - private suspend fun onMpvHook(hook_name: String?, hook_id: ULong) { + internal suspend fun onMpvHook(hook_name: String?, hook_id: ULong) { try { when (hook_name) { "on_load" -> { diff --git a/src/commonMain/kotlin/main.kt b/src/commonMain/kotlin/main.kt index a7ded3f..4557e96 100644 --- a/src/commonMain/kotlin/main.kt +++ b/src/commonMain/kotlin/main.kt @@ -10,7 +10,7 @@ fun String.toRed() = fun main(args: Array) { try { - val command: Command = SpMsCommand().subcommands(CommandLineClient.get(), PlayerClient.get()) + val command: Command = SpMsCommand().subcommands(listOfNotNull(CommandLineClient.get(), PlayerClient.get())) command.main(args) } catch (e: Throwable) { diff --git a/src/commonMain/kotlin/spms/client/player/PlayerClient.kt b/src/commonMain/kotlin/spms/client/player/PlayerClient.kt index 03ffe30..7b53bc3 100644 --- a/src/commonMain/kotlin/spms/client/player/PlayerClient.kt +++ b/src/commonMain/kotlin/spms/client/player/PlayerClient.kt @@ -117,6 +117,10 @@ private abstract class PlayerImpl(headless: Boolean = true): MpvClientImpl(headl throw RuntimeException("Processing event $event failed", e) } } + + companion object { + fun isAvailable(): Boolean = MpvClientImpl.isAvailable() + } } @OptIn(ExperimentalForeignApi::class) @@ -125,6 +129,10 @@ class PlayerClient private constructor(): Command( help = { "TODO" }, is_default = true ) { + companion object { + fun get(): PlayerClient? = if (PlayerImpl.isAvailable()) PlayerClient() else null + } + private val client_options by ClientOptions() private val player_options by PlayerOptions() @@ -303,8 +311,4 @@ class PlayerClient private constructor(): Command( return events.orEmpty() } - - companion object { - fun get(): PlayerClient = PlayerClient() - } } diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt index 97944a1..e7fa782 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt @@ -3,7 +3,7 @@ package spms.localisation.strings interface CliLocalisation { val bug_report_notice: String - val command_help_root: String + fun commandHelpRoot(mpv_enabled: Boolean): String val command_help_ctrl: String val option_group_help_controller: String diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt index 6f8fd2d..1d61abc 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt @@ -5,7 +5,12 @@ import spms.server.BUG_REPORT_URL class CliLocalisationEn: CliLocalisation { override val bug_report_notice: String = "Report bugs at $BUG_REPORT_URL" - override val command_help_root: String = "Desktop server component for SpMp. Uses MPV for audio streaming and playback." + override fun commandHelpRoot(mpv_enabled: Boolean): String = + "Desktop server component for SpMp. " + ( + if (mpv_enabled) "Uses mpv for audio streaming and playback." + else "This binary was built without support for mpv. Headless mode will always be enabled." + ) + override val command_help_ctrl: String = "Command-line interface for interacting with other servers" override val option_group_help_controller: String = "Controller options" diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt index 8612069..219c3e7 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt +++ b/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt @@ -5,7 +5,12 @@ import spms.server.BUG_REPORT_URL class CliLocalisationJa: CliLocalisation { override val bug_report_notice: String = "$BUG_REPORT_URL にバグを報告してください" - override val command_help_root: String = "SpMpのデスクトップ用サーバープログラム。オーディオのストリーミングと再生にはmpvを使用します。" + override fun commandHelpRoot(mpv_enabled: Boolean): String = + "SpMpのデスクトップ用サーバープログラム。" + ( + if (mpv_enabled) "オーディオのストリーミングと再生にはmpvを使用します。" + else "このバイナリは mpv サポートなしでコンパイルされました。常に「headless」モードを使用します。" + ) + override val command_help_ctrl: String = "他のサーバと交流するコマンドラインインターフェイス" override val option_group_help_controller: String = "CLIのオプション" diff --git a/src/commonMain/kotlin/spms/server/SpMs.kt b/src/commonMain/kotlin/spms/server/SpMs.kt index a46e0fe..84c0da9 100644 --- a/src/commonMain/kotlin/spms/server/SpMs.kt +++ b/src/commonMain/kotlin/spms/server/SpMs.kt @@ -43,7 +43,7 @@ class SpMs( private var playback_waiting_for_clients: Boolean = false val player: Player = - if (headless) + if (headless || !MpvClientImpl.isAvailable()) object : HeadlessPlayer() { override fun getCachedItemDuration(item_id: String): Long? = item_durations[item_id] diff --git a/src/commonMain/kotlin/spms/server/SpMsCommand.kt b/src/commonMain/kotlin/spms/server/SpMsCommand.kt index 659ac55..458f688 100644 --- a/src/commonMain/kotlin/spms/server/SpMsCommand.kt +++ b/src/commonMain/kotlin/spms/server/SpMsCommand.kt @@ -28,6 +28,7 @@ import platform.posix.getenv import kotlinx.cinterop.toKString import spms.* import spms.socketapi.shared.SPMS_DEFAULT_PORT +import cinterop.mpv.LibMpvClient const val PROJECT_URL: String = "https://github.com/toasterofbread/spmp-server" const val BUG_REPORT_URL: String = PROJECT_URL + "/issues" @@ -99,7 +100,7 @@ class PlayerOptions: OptionGroup() { @OptIn(ExperimentalForeignApi::class) class SpMsCommand: Command( name = "spms", - help = { cli.command_help_root }, + help = { cli.commandHelpRoot(mpv_enabled = LibMpvClient.isAvailable()) }, is_default = true ) { private val port: Int by option("-p", "--port").int().default(SPMS_DEFAULT_PORT).help { context.loc.server.option_help_port } @@ -122,11 +123,11 @@ class SpMsCommand: Command( var stop: Boolean = false memScoped { - val server: SpMs = + val server: SpMs = SpMs( - mem_scope = this, - headless = headless, - enable_gui = player_options.enable_gui, + mem_scope = this, + headless = headless, + enable_gui = player_options.enable_gui, enable_media_session = !no_media_session ) server.bind(port)