Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,9 @@ actual object PlayerSettingsStorage {
?.apply()
}

actual fun loadWindowsHdmiPassthroughEnabled(): Boolean? = null

actual fun saveWindowsHdmiPassthroughEnabled(enabled: Boolean) = Unit

actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
Expand Down
4 changes: 4 additions & 0 deletions composeApp/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1952,6 +1952,10 @@
<string name="player_ios_preset_custom_label">Custom</string>
<string name="player_ios_preset_custom_desc">Use your advanced values below.</string>
<string name="player_ios_hardware_decoder_off">Off</string>
<!-- Playback settings Windows audio -->
<string name="settings_playback_windows_audio_section">Windows audio</string>
<string name="settings_playback_hdmi_passthrough">HDMI audio passthrough</string>
<string name="settings_playback_hdmi_passthrough_desc">Send Dolby Digital, Dolby Digital Plus/Atmos, TrueHD/Atmos, DTS, DTS-HD, and DTS:X unchanged to the Windows default HDMI receiver. The receiver controls volume; unsupported audio plays as decoded PCM.</string>
<!-- Playback settings Windows NVIDIA RTX Video -->
<string name="settings_playback_nvidia_rtx_video_section">NVIDIA RTX Video</string>
<string name="settings_playback_nvidia_rtx_super_resolution">RTX Video Super Resolution</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ data class PlayerSettingsUiState(
val iosSaturation: Int = 0,
val iosGamma: Int = 0,
val nvidiaRtxSuperResolutionEnabled: Boolean = false,
val windowsHdmiPassthroughEnabled: Boolean = false,
)

object PlayerSettingsRepository {
Expand Down Expand Up @@ -164,6 +165,7 @@ object PlayerSettingsRepository {
private var iosSaturation = 0
private var iosGamma = 0
private var nvidiaRtxSuperResolutionEnabled = false
private var windowsHdmiPassthroughEnabled = false

fun ensureLoaded() {
if (hasLoaded) return
Expand Down Expand Up @@ -238,6 +240,7 @@ object PlayerSettingsRepository {
iosSaturation = 0
iosGamma = 0
nvidiaRtxSuperResolutionEnabled = false
windowsHdmiPassthroughEnabled = false
publish()
}

Expand Down Expand Up @@ -389,6 +392,7 @@ object PlayerSettingsRepository {
iosSaturation = PlayerSettingsStorage.loadIosSaturation() ?: 0
iosGamma = PlayerSettingsStorage.loadIosGamma() ?: 0
nvidiaRtxSuperResolutionEnabled = PlayerSettingsStorage.loadNvidiaRtxSuperResolutionEnabled() ?: false
windowsHdmiPassthroughEnabled = PlayerSettingsStorage.loadWindowsHdmiPassthroughEnabled() ?: false
publish()
}

Expand Down Expand Up @@ -794,6 +798,14 @@ object PlayerSettingsRepository {
PlayerSettingsStorage.saveNvidiaRtxSuperResolutionEnabled(enabled)
}

fun setWindowsHdmiPassthroughEnabled(enabled: Boolean) {
ensureLoaded()
if (windowsHdmiPassthroughEnabled == enabled) return
windowsHdmiPassthroughEnabled = enabled
publish()
PlayerSettingsStorage.saveWindowsHdmiPassthroughEnabled(enabled)
}

fun setLibassRenderType(renderType: String) {
ensureLoaded()
if (libassRenderType == renderType) return
Expand Down Expand Up @@ -1031,6 +1043,7 @@ object PlayerSettingsRepository {
iosSaturation = iosSaturation,
iosGamma = iosGamma,
nvidiaRtxSuperResolutionEnabled = nvidiaRtxSuperResolutionEnabled,
windowsHdmiPassthroughEnabled = windowsHdmiPassthroughEnabled,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ internal expect object PlayerSettingsStorage {
fun saveIosGamma(value: Int)
fun loadNvidiaRtxSuperResolutionEnabled(): Boolean?
fun saveNvidiaRtxSuperResolutionEnabled(enabled: Boolean)
fun loadWindowsHdmiPassthroughEnabled(): Boolean?
fun saveWindowsHdmiPassthroughEnabled(enabled: Boolean)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,21 @@ private fun PlaybackSettingsSection(
}

if (isWindows) {
SettingsSection(
title = stringResource(Res.string.settings_playback_windows_audio_section),
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
SettingsSwitchRow(
title = stringResource(Res.string.settings_playback_hdmi_passthrough),
description = stringResource(Res.string.settings_playback_hdmi_passthrough_desc),
checked = autoPlayPlayerSettings.windowsHdmiPassthroughEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setWindowsHdmiPassthroughEnabled,
)
}
}

SettingsSection(
title = stringResource(Res.string.settings_playback_nvidia_rtx_video_section),
isTablet = isTablet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ private fun NativePlayerSurface(
val playerSettings by PlayerSettingsRepository.uiState.collectAsState()
val decoderPriority = playerSettings.decoderPriority
val nvidiaRtxSuperResolutionEnabled = playerSettings.nvidiaRtxSuperResolutionEnabled
val windowsHdmiPassthroughEnabled =
DesktopHostOs.current == DesktopHostOs.WINDOWS && playerSettings.windowsHdmiPassthroughEnabled

SideEffect {
onControllerReady(controller)
Expand Down Expand Up @@ -188,6 +190,7 @@ private fun NativePlayerSurface(
playbackHeaders,
decoderPriority,
nvidiaRtxSuperResolutionEnabled,
windowsHdmiPassthroughEnabled,
hostFirstFullSizePaintComplete.value,
initialPositionMs,
initialPositionRequestKey,
Expand All @@ -203,6 +206,7 @@ private fun NativePlayerSurface(
initialPositionMs = initialPositionMs,
decoderPriority = decoderPriority,
nvidiaRtxSuperResolutionEnabled = nvidiaRtxSuperResolutionEnabled,
windowsHdmiPassthroughEnabled = windowsHdmiPassthroughEnabled,
onError = { message -> latestOnError.value(message) },
)
initialPositionRequestKey?.let { key ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ internal actual object PlayerSettingsStorage {
private const val iosSaturationKey = "ios_saturation"
private const val iosGammaKey = "ios_gamma"
private const val nvidiaRtxSuperResolutionEnabledKey = "nvidia_rtx_super_resolution_enabled"
private const val windowsHdmiPassthroughEnabledKey = "windows_hdmi_passthrough_enabled"
private val syncKeys = listOf(
showLoadingOverlayKey,
showParentalGuideKey,
Expand Down Expand Up @@ -312,6 +313,9 @@ internal actual object PlayerSettingsStorage {

actual fun loadNvidiaRtxSuperResolutionEnabled(): Boolean? = loadBoolean(nvidiaRtxSuperResolutionEnabledKey)
actual fun saveNvidiaRtxSuperResolutionEnabled(enabled: Boolean) = saveBoolean(nvidiaRtxSuperResolutionEnabledKey, enabled)
actual fun loadWindowsHdmiPassthroughEnabled(): Boolean? = loadBoolean(windowsHdmiPassthroughEnabledKey)
actual fun saveWindowsHdmiPassthroughEnabled(enabled: Boolean) =
saveBoolean(windowsHdmiPassthroughEnabledKey, enabled)

private fun scoped(key: String): String = ProfileScopedKey.of(key)
private fun loadString(key: String): String? = store.getString(scoped(key))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ internal object NativePlayerBridge {
controlsPageUrl: String,
decoderPriority: Int,
nvidiaRtxSuperResolutionEnabled: Boolean,
windowsHdmiPassthroughEnabled: Boolean,
eventSink: NativePlayerEventSink,
): Long

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ internal typealias NativePlayerCreate = (
String,
Int,
Boolean,
Boolean,
NativePlayerEventSink,
) -> Long

Expand Down Expand Up @@ -113,6 +114,7 @@ internal class NativePlayerController(
initialPositionMs: Long,
decoderPriority: Int,
nvidiaRtxSuperResolutionEnabled: Boolean,
windowsHdmiPassthroughEnabled: Boolean,
onError: (String?) -> Unit,
) {
val pending = PendingSource(
Expand All @@ -122,6 +124,7 @@ internal class NativePlayerController(
initialPositionMs = initialPositionMs.coerceAtLeast(0L),
decoderPriority = decoderPriority,
nvidiaRtxSuperResolutionEnabled = nvidiaRtxSuperResolutionEnabled,
windowsHdmiPassthroughEnabled = windowsHdmiPassthroughEnabled,
onError = onError,
)
var terminalFailure: String? = null
Expand All @@ -144,7 +147,8 @@ internal class NativePlayerController(
}
log.d {
"attach requested source=${sourceUrl.toPlaybackLogKey()} headers=${sourceHeaders.size} " +
"playWhenReady=$playWhenReady initialPositionMs=$initialPositionMs decoderPriority=$decoderPriority"
"playWhenReady=$playWhenReady initialPositionMs=$initialPositionMs decoderPriority=$decoderPriority " +
"hdmiPassthrough=$windowsHdmiPassthroughEnabled"
}
host.onPeerReady = { attachPending() }
if (isHostDisplayable()) {
Expand Down Expand Up @@ -296,6 +300,7 @@ internal class NativePlayerController(
NativePlayerBridge.controlsPageUrl,
pending.decoderPriority,
pending.nvidiaRtxSuperResolutionEnabled,
pending.windowsHdmiPassthroughEnabled,
eventSink,
).also { if (it == 0L) error("Native player did not return a handle.") }
}.onSuccess { created ->
Expand Down Expand Up @@ -546,6 +551,7 @@ internal class NativePlayerController(
}

private fun setFallbackVolume(level: Float) {
if (pendingSource?.windowsHdmiPassthroughEnabled == true) return
val current = handle
if (current != 0L) {
val nextLevel = level.coerceIn(0f, 1f)
Expand All @@ -570,6 +576,10 @@ internal class NativePlayerController(
private fun applyRememberedVolume() {
val current = handle
if (current == 0L) return
if (pendingSource?.windowsHdmiPassthroughEnabled == true) {
controlsState = controlsState.copy(volumeLevel = 1f)
return
}
val level = rememberedVolumeLevel.coerceIn(0f, 1f)
NativePlayerBridge.setVolume(current, level)
controlsState = controlsState.copy(volumeLevel = level)
Expand All @@ -584,6 +594,7 @@ internal class NativePlayerController(
}

private fun cycleFallbackSpeed() {
if (pendingSource?.windowsHdmiPassthroughEnabled == true) return
val current = handle
if (current == 0L) return
val speeds = listOf(1f, 1.25f, 1.5f, 2f)
Expand Down Expand Up @@ -873,12 +884,14 @@ internal class NativePlayerController(
initialPositionMs = pending.initialPositionMs,
decoderPriority = pending.decoderPriority,
nvidiaRtxSuperResolutionEnabled = pending.nvidiaRtxSuperResolutionEnabled,
windowsHdmiPassthroughEnabled = pending.windowsHdmiPassthroughEnabled,
onError = pending.onError,
)
}

override fun setPlaybackSpeed(speed: Float) {
log.d { "setPlaybackSpeed speed=$speed handle=$handle" }
if (pendingSource?.windowsHdmiPassthroughEnabled == true) return
handle.takeIf { it != 0L }?.let { NativePlayerBridge.setSpeed(it, speed) }
}

Expand Down Expand Up @@ -1089,6 +1102,7 @@ private data class PendingSource(
val initialPositionMs: Long,
val decoderPriority: Int,
val nvidiaRtxSuperResolutionEnabled: Boolean,
val windowsHdmiPassthroughEnabled: Boolean,
val onError: (String?) -> Unit,
)

Expand Down
3 changes: 2 additions & 1 deletion composeApp/src/desktopMain/native/linux/player_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1457,7 +1457,8 @@ JNIEXPORT jlong JNICALL NP(create)(
JNIEnv *env, jobject /*thiz*/, jlong hostViewPtr, jstring sourceUrl,
jobjectArray headerLines, jboolean playWhenReady, jlong initialPositionMs,
jstring controlsPageUrl, jint decoderPriority,
jboolean /*nvidiaRtxSuperResolutionEnabled*/, jobject eventSink) {
jboolean /*nvidiaRtxSuperResolutionEnabled*/,
jboolean /*windowsHdmiPassthroughEnabled*/, jobject eventSink) {

// libmpv requires LC_NUMERIC=C (e.g. non-"C" locales with comma
// decimals make mpv_create fail); the JVM uses java.util.Locale, so
Expand Down
1 change: 1 addition & 0 deletions composeApp/src/desktopMain/native/macos/player_bridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2543,6 +2543,7 @@ static void throwJavaError(JNIEnv *env, NSString *message) {
jstring controlsPageUrl,
jint decoderPriority,
jboolean nvidiaRtxSuperResolutionEnabled,
jboolean /* windowsHdmiPassthroughEnabled */,
jobject eventSink
) {
NSView *hostView = (__bridge NSView *)(void *)(intptr_t)hostViewPtr;
Expand Down
28 changes: 22 additions & 6 deletions composeApp/src/desktopMain/native/windows/player_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,7 @@ class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPla
JavaVM *vm,
int decoderPriority,
bool nvidiaRtxSuperResolutionEnabled,
bool windowsHdmiPassthroughEnabled,
jobject sink,
jmethodID method
) {
Expand All @@ -864,8 +865,8 @@ class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPla
auto initState = std::make_shared<InitializationState>();
auto self = shared_from_this();
uiThread = std::thread(
[self, sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, decoderPriority, nvidiaRtxSuperResolutionEnabled, initState]() {
self->runNativeUiThread(sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, decoderPriority, nvidiaRtxSuperResolutionEnabled, initState);
[self, sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, decoderPriority, nvidiaRtxSuperResolutionEnabled, windowsHdmiPassthroughEnabled, initState]() {
self->runNativeUiThread(sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, decoderPriority, nvidiaRtxSuperResolutionEnabled, windowsHdmiPassthroughEnabled, initState);
}
);

Expand Down Expand Up @@ -1302,11 +1303,12 @@ class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPla
std::string controlsUrl,
int decoderPriority,
bool nvidiaRtxSuperResolutionEnabled,
bool windowsHdmiPassthroughEnabled,
std::shared_ptr<InitializationState> initState
) {
std::string failure;
try {
initializeOnNativeUiThread(sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, decoderPriority, nvidiaRtxSuperResolutionEnabled);
initializeOnNativeUiThread(sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, decoderPriority, nvidiaRtxSuperResolutionEnabled, windowsHdmiPassthroughEnabled);
} catch (const std::exception &error) {
failure = error.what();
cleanupUiResources();
Expand Down Expand Up @@ -1337,7 +1339,8 @@ class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPla
long long initialPositionMs,
const std::string &controlsUrl,
int decoderPriority,
bool nvidiaRtxSuperResolutionEnabled
bool nvidiaRtxSuperResolutionEnabled,
bool windowsHdmiPassthroughEnabled
) {
registerWindowClasses();
uiThreadId = GetCurrentThreadId();
Expand Down Expand Up @@ -1389,7 +1392,7 @@ class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPla
}

startWebView(controlsUrl);
startMpv(sourceUrl, headerLines, playWhenReady, initialPositionMs, decoderPriority, nvidiaRtxSuperResolutionEnabled);
startMpv(sourceUrl, headerLines, playWhenReady, initialPositionMs, decoderPriority, nvidiaRtxSuperResolutionEnabled, windowsHdmiPassthroughEnabled);
layoutNativeSubviews();
if (!SetTimer(messageHwnd, NUVIO_TIMER_ID, 500, nullptr)) {
throw std::runtime_error("Unable to start native player timer.");
Expand Down Expand Up @@ -1584,7 +1587,8 @@ class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPla
bool playWhenReady,
long long initialPositionMs,
int decoderPriority,
bool nvidiaRtxSuperResolutionEnabled
bool nvidiaRtxSuperResolutionEnabled,
bool windowsHdmiPassthroughEnabled
) {
MpvApi &api = mpvApi();
{
Expand All @@ -1601,6 +1605,16 @@ class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPla
setMpvOptionStringLocked("input-vo-keyboard", "no");
setMpvOptionStringLocked("keep-open", "yes");
setMpvOptionStringLocked("vo", "gpu-next");
if (windowsHdmiPassthroughEnabled) {
// mpv falls back to PCM decoding when the active WASAPI endpoint
// rejects a requested IEC 61937 bitstream format.
setMpvOptionStringLocked("ao", "wasapi");
setMpvOptionStringLocked("audio-exclusive", "yes");
setMpvOptionStringLocked("audio-spdif", "ac3,eac3,dts,dts-hd,truehd");
setMpvOptionStringLocked("audio-channels", "auto-safe");
setMpvOptionStringLocked("speed", "1");
setMpvOptionStringLocked("volume", "100");
}
if (nvidiaRtxSuperResolutionEnabled) {
setMpvOptionStringLocked("gpu-api", "d3d11");
setMpvOptionStringLocked("hwdec", "d3d11va");
Expand Down Expand Up @@ -2224,6 +2238,7 @@ Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_create(
jstring controlsPageUrl,
jint decoderPriority,
jboolean nvidiaRtxSuperResolutionEnabled,
jboolean windowsHdmiPassthroughEnabled,
jobject eventSink
) {
HWND hostHwnd = (HWND)(intptr_t)hostViewPtr;
Expand Down Expand Up @@ -2259,6 +2274,7 @@ Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_create(
javaVm,
decoderPriority,
nvidiaRtxSuperResolutionEnabled == JNI_TRUE,
windowsHdmiPassthroughEnabled == JNI_TRUE,
eventSinkRef,
eventMethod
);
Expand Down
Loading