diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yml similarity index 100% rename from .github/workflows/docker-publish.yaml rename to .github/workflows/docker-publish.yml diff --git a/README.md b/README.md index 8525c3f..6066600 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ ## Moonlight-Tizen-NaCl GameStream client for Samsung Smart TV's running Tizen OS (3.0 to 6.0) +## PLEASE DO NOT USE MY FORK, THIS IS MY TESTING FORK. Please use the installation below or the actual link to the original which will be untouched, and linked to the official/original build. + +https://github.com/OneLiberty/moonlight-tizen-nacl + + ### Note As a non-developer with limited coding knowledge, I do my best to maintain the repository and address issues. If you encounter problems, please report them in the issue section. While I can't guarantee a solution, I will certainly investigate. This project is delivered as a POC, don't expect good performances and a fully working environement. diff --git a/config.xml b/config.xml index 6d8aa39..f319ece 100644 --- a/config.xml +++ b/config.xml @@ -13,6 +13,7 @@ MoonlightNaCl + diff --git a/gamepad.cpp b/gamepad.cpp index c146d50..da23921 100644 --- a/gamepad.cpp +++ b/gamepad.cpp @@ -1,42 +1,59 @@ #include "moonlight.hpp" - #include "ppapi/c/ppb_gamepad.h" - #include - #include +#define AXIS_DEAD_ZONE 0.1f + static const unsigned short k_StandardGamepadButtonMapping[] = { A_FLAG, B_FLAG, X_FLAG, Y_FLAG, LB_FLAG, RB_FLAG, - 0, 0, // Triggers + 0, 0, BACK_FLAG, PLAY_FLAG, LS_CLK_FLAG, RS_CLK_FLAG, UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG, SPECIAL_FLAG }; -static const unsigned int k_StandardGamepadTriggerButtonIndexes[] = { - 6, 7 +static const unsigned short k_BTGamepadButtonMapping[] = { + A_FLAG, // btn0 + B_FLAG, // btn1 + 0, // btn2 unused + X_FLAG, // btn3 + Y_FLAG, // btn4 + 0, // btn5 unused + 0, // btn6 right stick X (handled as axis) + 0, // btn7 right stick Y (handled as axis) + BACK_FLAG, // btn8 + PLAY_FLAG, // btn9 + LS_CLK_FLAG, // btn10 + RS_CLK_FLAG, // btn11 + UP_FLAG, // btn12 + DOWN_FLAG, // btn13 + LEFT_FLAG, // btn14 + RIGHT_FLAG, // btn15 + SPECIAL_FLAG // btn16 }; +static bool s_padSeen[4] = {false, false, false, false}; +static bool s_isTizenBT[4] = {false, false, false, false}; + +static float ApplyDeadZone(float value) { + if (value > -AXIS_DEAD_ZONE && value < AXIS_DEAD_ZONE) { + return 0.0f; + } + return value; +} + static short GetActiveGamepadMask(PP_GamepadsSampleData& gamepadData) { short controllerIndex = 0; short activeGamepadMask = 0; for (unsigned int p = 0; p < gamepadData.length; p++) { PP_GamepadSampleData& padData = gamepadData.items[p]; - - printf("[NaCl, GetActiveGamepadMask] Gamepad %u: connected = %d, timestamp = %f\n", p, padData.connected, padData.timestamp); - if (!padData.connected) { continue; } - - if (padData.timestamp == 0) { - continue; - } - activeGamepadMask |= (1 << controllerIndex); controllerIndex++; } @@ -48,80 +65,124 @@ void MoonlightInstance::PollGamepads() { PP_GamepadsSampleData gamepadData; short controllerIndex = 0; short activeGamepadMask; - - m_GamepadApi->Sample(pp_instance(), &gamepadData); + m_GamepadApi->Sample(pp_instance(), &gamepadData); activeGamepadMask = GetActiveGamepadMask(gamepadData); - printf("[NaCl] Active gamepad mask: %d\n", activeGamepadMask); - for (unsigned int p = 0; p < gamepadData.length; p++) { PP_GamepadSampleData& padData = gamepadData.items[p]; - + if (!padData.connected) { + s_padSeen[p] = false; + s_isTizenBT[p] = false; + controllerIndex++; continue; } - - if (padData.timestamp == m_LastPadTimestamps[p]) { + + // Latch BT detection once on first sight of this pad, + // but wait until both axes have settled to non-zero values + if (!s_padSeen[p]) { + if (padData.axes_length >= 2) { + float ax0 = padData.axes[0]; + float ax1 = padData.axes[1]; + if (ax0 != 0.0f && ax1 != 0.0f) { + s_padSeen[p] = true; + s_isTizenBT[p] = (ax0 > 0.5f && ax0 < 1.5f && + ax1 > 0.5f && ax1 < 1.5f); + } + } + } + + if (padData.timestamp != 0 && padData.timestamp == m_LastPadTimestamps[p]) { controllerIndex++; continue; } - + m_LastPadTimestamps[p] = padData.timestamp; - - printf("[NaCl] Gamepad %u: timestamp = %f\n", p, padData.timestamp); - + + bool isTizenBT = s_isTizenBT[p]; + int buttonFlags = 0; unsigned char leftTrigger = 0, rightTrigger = 0; short leftStickX = 0, leftStickY = 0; short rightStickX = 0, rightStickY = 0; - - // Handle buttons and triggers - for (unsigned int i = 0; i < padData.buttons_length; i++) { - if (i >= sizeof(k_StandardGamepadButtonMapping) / sizeof(k_StandardGamepadButtonMapping[0])) { - // Ignore unmapped buttons - break; + + const unsigned short* mapping = isTizenBT ? + k_BTGamepadButtonMapping : k_StandardGamepadButtonMapping; + size_t mappingSize = isTizenBT ? + sizeof(k_BTGamepadButtonMapping) / sizeof(k_BTGamepadButtonMapping[0]) : + sizeof(k_StandardGamepadButtonMapping) / sizeof(k_StandardGamepadButtonMapping[0]); + + if (isTizenBT) { + // Buttons + for (unsigned int i = 0; i < padData.buttons_length && i < mappingSize; i++) { + if (mapping[i] && padData.buttons[i] > 0.5f) { + buttonFlags |= mapping[i]; + } } - - // Handle triggers first - if (i == k_StandardGamepadTriggerButtonIndexes[0]) { - leftTrigger = padData.buttons[i] * 0xFF; + + // Left stick: axes offset by 1.0 (range 0-2, center ~1.0) + if (padData.axes_length >= 2) { + float lx = padData.axes[0] - 1.0f; + float ly = padData.axes[1] - 1.0f; + leftStickX = (short)(ApplyDeadZone(lx) * 0x7FFF); + leftStickY = (short)(-ApplyDeadZone(ly) * 0x7FFF); } - else if (i == k_StandardGamepadTriggerButtonIndexes[1]) { - rightTrigger = padData.buttons[i] * 0xFF; + + // Right stick: packed into btn6 (X) and btn7 (Y) + // range 0-1, center ~0.5, remap to -1 to 1 + if (padData.buttons_length > 7) { + float rx = (padData.buttons[6] - 0.5f) * 2.0f; + float ry = (padData.buttons[7] - 0.5f) * 2.0f; + rightStickX = (short)(ApplyDeadZone(rx) * 0x7FFF); + rightStickY = (short)(-ApplyDeadZone(ry) * 0x7FFF); } - // Now normal buttons - else if (padData.buttons[i] > 0.5f) { - buttonFlags |= k_StandardGamepadButtonMapping[i]; + + // LB, RB, LT, RT not reported on Tizen 4.0 BT - left as zero + + } else { + // Standard USB layout + for (unsigned int i = 0; i < padData.buttons_length && i < mappingSize; i++) { + if (i == 6) { + leftTrigger = (unsigned char)(padData.buttons[i] * 0xFF); + continue; + } + if (i == 7) { + rightTrigger = (unsigned char)(padData.buttons[i] * 0xFF); + continue; + } + if (mapping[i] && padData.buttons[i] > 0.5f) { + buttonFlags |= mapping[i]; + } + } + + if (padData.axes_length >= 2) { + leftStickX = (short)(ApplyDeadZone(padData.axes[0]) * 0x7FFF); + leftStickY = (short)(-ApplyDeadZone(padData.axes[1]) * 0x7FFF); + } + if (padData.axes_length >= 4) { + rightStickX = (short)(ApplyDeadZone(padData.axes[2]) * 0x7FFF); + rightStickY = (short)(-ApplyDeadZone(padData.axes[3]) * 0x7FFF); } } - - // Get left stick values - if (padData.axes_length >= 2) { - leftStickX = padData.axes[0] * 0x7FFF; - leftStickY = -padData.axes[1] * 0x7FFF; - } - - // Get right stick values - if (padData.axes_length >= 4) { - rightStickX = padData.axes[2] * 0x7FFF; - rightStickY = -padData.axes[3] * 0x7FFF; - } - + LiSendMultiControllerEvent(controllerIndex, activeGamepadMask, - buttonFlags, leftTrigger, rightTrigger, - leftStickX, leftStickY, rightStickX, rightStickY); + buttonFlags, leftTrigger, rightTrigger, + leftStickX, leftStickY, rightStickX, rightStickY); + controllerIndex++; } } -void MoonlightInstance::ClControllerRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) +void MoonlightInstance::ClControllerRumble(unsigned short controllerNumber, + unsigned short lowFreqMotor, unsigned short highFreqMotor) { const float weakMagnitude = static_cast(highFreqMotor) / static_cast(UINT16_MAX); const float strongMagnitude = static_cast(lowFreqMotor) / static_cast(UINT16_MAX); std::ostringstream ss; ss << controllerNumber << "," << weakMagnitude << "," << strongMagnitude; + pp::Var response(std::string("controllerRumble: ") + ss.str()); g_Instance->PostMessage(response); -} \ No newline at end of file +} diff --git a/index.html b/index.html index b758169..588a4af 100644 --- a/index.html +++ b/index.html @@ -126,6 +126,7 @@
+
@@ -192,4 +193,4 @@

Add Host Manually

- \ No newline at end of file + diff --git a/static/js/index.js b/static/js/index.js index fcd30b5..aec5264 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -5,6 +5,17 @@ var myUniqueid = '0123456789ABCDEF'; // Use the same UID as other Moonlight clie var api; // `api` should only be set if we're in a host-specific screen. on the initial screen it should always be null. var isInGame = false; // flag indicating whether the game stream started +function loadProductInfos() { + const modelCodePlaceholder = document.getElementById("modelCodePlaceholder"); + if (modelCodePlaceholder) { + const model = window.tizen.systeminfo.getCapability('http://tizen.org/system/model_name') || "Not Available"; + const moonlightVersion = window.tizen.application.getAppInfo().version || "Not Available"; + const tizenVersion = window.tizen.systeminfo.getCapability('http://tizen.org/feature/platform.version') || "Not Available"; + modelCodePlaceholder.innerText = `TV Model: ${model} ; Moonlight: v${moonlightVersion} ; Tizen: v${tizenVersion}; This is a NaCl Build!`; + } +} + + // Called by the common.js module. function attachListeners() { changeUiModeForNaClLoad(); @@ -55,6 +66,12 @@ function attachListeners() { if (gamepadMapping[key]) { gamepadMapping[key](); } + }); + // DEBUG: log all keydown events during gameplay to detect BT shoulder/trigger keys + window.addEventListener('keydown', function(e) { + if (isInGame) { + console.log('[index.js, keydown] isInGame keyCode=' + e.keyCode + ' key=' + e.key + ' keyIdentifier=' + e.keyIdentifier); + } }); } @@ -758,6 +775,7 @@ function startGame(host, appID) { function playGameMode() { console.log('%c[index.js, playGameMode]', 'color:green;', 'Entering play game mode'); isInGame = true; + Controller.stopWatching(); // Hand off controller to NaCl, stop JS polling $("#main-navigation").hide(); $("#main-content").children().not("#listener, #loadingSpinner").hide(); @@ -821,6 +839,7 @@ function stopGameWithConfirmation() { function stopGame(host, callbackFunction) { isInGame = false; + Controller.startWatching(); // Return controller to JS for menu navigation if (!host.paired) { return; @@ -1118,6 +1137,8 @@ function onWindowLoad() { } }); + loadProductInfos(); + console.log('Load stored remote audio prefs'); getData('remoteAudio', function (previousValue) { if (previousValue.remoteAudio == null) { @@ -1158,8 +1179,19 @@ function onWindowLoad() { }); initSamsungKeys(); + + // DEBUG: dump all supported input device keys + try { + var supportedKeys = tizen.tvinputdevice.getSupportedKeys(); + for (var i = 0; i < supportedKeys.length; i++) { + console.log('[index.js, supportedKeys] name=' + supportedKeys[i].name + ' code=' + supportedKeys[i].code); + } + } catch(e) { + console.log('[index.js, supportedKeys] failed:', e); + } } + window.onload = onWindowLoad; // Required on TizenTV, to get gamepad events. @@ -1167,19 +1199,19 @@ window.addEventListener('gamepadconnected', function (event) { var connectedGamepad = event.gamepad; console.log('%c[index.js, gamepadconnected] gamepad connected: ', 'color: green;', connectedGamepad); - if (connectedGamepad.vibrationActuator) { // Check if the gamepad supports rumble - console.log('Gamepad supports vibration.'); - connectedGamepad.vibrationActuator.playEffect('dual-rumble', { - duration: 1000, - strongMagnitude: 1.0, - weakMagnitude: 1.0}); - } else { - console.log('Gamepad does not support vibration.'); + // Wrapped in try/catch - Tizen 4.0 may crash on vibrationActuator access + try { + if (connectedGamepad.vibrationActuator && connectedGamepad.vibrationActuator.playEffect) { + console.log('Gamepad supports vibration.'); + connectedGamepad.vibrationActuator.playEffect('dual-rumble', { + duration: 1000, + strongMagnitude: 1.0, + weakMagnitude: 1.0 + }); + } else { + console.log('Gamepad does not support vibration.'); + } + } catch(e) { + console.log('Gamepad vibration not supported on this Tizen version:', e); } }); - -window.addEventListener('gamepaddisconnected', function (event) { - console.log('%c[index.js, gamepaddisconnected] gamepad disconnected: ' + - JSON.stringify(event.gamepad), - event.gamepad); -}); \ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js index 7bff1d4..aec9565 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -58,21 +58,13 @@ function getConnectedGamepadMask() { for (var i = 0; i < gamepads.length; i++) { var gamepad = gamepads[i]; if (gamepad) { - // See logic in gamepad.cpp - // These must stay in sync! - if (!gamepad.connected) { - // Not connected continue; } - if (gamepad.timestamp == 0) { - // On some platforms, Chrome returns "connected" pads that - // really aren't, so timestamp stays at zero. To work around this, - // we'll only count gamepads that have a non-zero timestamp in our - // controller index. - continue; - } + // Removed timestamp == 0 check — on Tizen 4.0 reports timestamp=0 + // permanently even for fully working controllers, so we can't + // use it to filter out "ghost" gamepads here. mask |= 1 << count++; } @@ -82,6 +74,7 @@ function getConnectedGamepadMask() { return mask; } + String.prototype.toHex = function() { var hex = ''; for (var i = 0; i < this.length; i++) {