+
+
+
+
+
+
+
+
Waiting for a connection
+
+
+
+
+
+
Manual connection information
+
+
+
IPs
+
Port
46899 (TCP), 46898 (WS)
+
+
Automatic discovery is available via mDNS
+
+
Scan with a FCast sender app.
+
Need a sender app?
Download Grayjay at https://grayjay.app
+
+
+
+
+
App will continue to listen for connections when suspended in the background
+
+
+
+
+
+
+
diff --git a/receivers/tizen/src/main/style.css b/receivers/tizen/src/main/style.css
new file mode 100644
index 0000000..1ccf67e
--- /dev/null
+++ b/receivers/tizen/src/main/style.css
@@ -0,0 +1,96 @@
+.card-title {
+ font-family: InterBold;
+}
+
+#overlay {
+ font-family: InterRegular;
+ font-size: 28px;
+
+ /* gap not supported in WebOS 6.0 */
+ gap: unset;
+}
+
+#main-view {
+ padding: 25px;
+
+ /* gap not supported in WebOS 6.0 */
+ gap: unset;
+ margin-right: 15vw;
+}
+
+#title-text {
+ font-family: OutfitExtraBold;
+ font-size: 140px;
+}
+
+#title-icon {
+ width: 124px;
+ height: 124px;
+}
+
+#manual-connection-info {
+ font-family: InterBold;
+}
+
+#scan-to-connect {
+ font-family: InterBold;
+}
+
+.app-download {
+ font-weight: bold;
+ font-family: InterBold;
+}
+
+#window-can-be-closed {
+ font-family: InterRegular;
+ font-size: 24px;
+}
+
+.lds-ring {
+ width: 120px;
+ height: 120px;
+}
+.lds-ring div {
+ width: 104px;
+ height: 104px;
+}
+
+#loading-screen {
+ height: 100%;
+ width: 100%;
+ object-fit: cover;
+ background-color: black;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+#loading-text {
+ font-size: 100px;
+ font-weight: 800;
+ text-align: center;
+ color: white;
+ padding-right: 20px;
+}
+
+#connection-check {
+ width: 104px;
+ height: 104px;
+}
+
+#toast-notification {
+ gap: unset;
+ top: -250px;
+}
+
+#toast-icon {
+ width: 88px;
+ height: 88px;
+ margin-right: 20px;
+}
+
+#toast-text {
+ font-family: InterRegular;
+ font-size: 28px;
+}
diff --git a/receivers/tizen/src/player/Preload.ts b/receivers/tizen/src/player/Preload.ts
new file mode 100644
index 0000000..3d1185a
--- /dev/null
+++ b/receivers/tizen/src/player/Preload.ts
@@ -0,0 +1,100 @@
+import { preloadData } from 'common/player/Preload';
+import { Opcode, PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets';
+import { toast, ToastIcon } from 'common/components/Toast';
+import * as tizen from 'tizen-common-web';
+
+
+const serviceId = 'qL5oFoTHoJ.FCastReceiverService.dll';
+// const serviceId = 'com.futo.FCastReceiverService';
+const servicePort = tizen.messageport.requestRemoteMessagePort(serviceId, 'ipcPort');
+
+preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => {
+ servicePort.sendMessage([
+ { key: 'opcode', value: Opcode.PlaybackError.toString() },
+ { key: 'data', value: JSON.stringify(error) }
+ ]);
+};
+preloadData.sendPlaybackUpdateCb = (update: PlaybackUpdateMessage) => {
+ servicePort.sendMessage([
+ { key: 'opcode', value: Opcode.PlaybackUpdate.toString() },
+ { key: 'data', value: JSON.stringify(update) }
+ ]);
+};
+preloadData.sendVolumeUpdateCb = (update: VolumeUpdateMessage) => {
+ servicePort.sendMessage([
+ { key: 'opcode', value: Opcode.VolumeUpdate.toString() },
+ { key: 'data', value: JSON.stringify(update) }
+ ]);
+};
+
+let playerWindowOpen = false;
+window.tizenOSAPI = {
+ pendingPlay: JSON.parse(sessionStorage.getItem('playData'))
+};
+
+const ipcPort = tizen.messageport.requestLocalMessagePort('ipcPort');
+const ipcListener = ipcPort.addMessagePortListener((data) => {
+ const messageIndex = data.findIndex((i) => { return i.key === 'message' });
+ const dataIndex = data.findIndex((i) => { return i.key === 'data' });
+ const message = JSON.parse(data[dataIndex].value as string);
+ console.log('Received data:', JSON.stringify(data));
+ // console.log('Received message:', JSON.stringify(message));
+
+ switch (data[messageIndex].value) {
+ // case 'serviceStart':
+ // toast("FCast network service started");
+ // break;
+
+ case 'toast': {
+ toast(message.message, message.icon, message.duration);
+ break;
+ }
+
+ case 'ping':
+ break;
+
+ case 'play':
+ if (message !== null) {
+ if (!playerWindowOpen) {
+ playerWindowOpen = true;
+ }
+
+ if (preloadData.onPlayCb === undefined) {
+ window.tizenOSAPI.pendingPlay = message;
+ }
+ else {
+ preloadData.onPlayCb(null, message);
+ }
+ }
+ break;
+
+ case 'pause':
+ preloadData.onPauseCb();
+ break;
+
+ case 'resume':
+ preloadData.onResumeCb();
+ break;
+
+ case 'stop':
+ playerWindowOpen = false;
+ window.open('../main_window/index.html', '_self');
+ break;
+
+ case 'seek':
+ preloadData.onSeekCb(null, message);
+ break;
+
+ case 'setvolume':
+ preloadData.onSetVolumeCb(null, message);
+ break;
+
+ case 'setspeed':
+ preloadData.onSetSpeedCb(null, message);
+ break;
+
+ default:
+ console.warn(`Unknown ipc message type: ${data[messageIndex].value}, value: ${data[dataIndex].value}`);
+ break;
+ }
+});
diff --git a/receivers/tizen/src/player/Renderer.ts b/receivers/tizen/src/player/Renderer.ts
new file mode 100644
index 0000000..c02d075
--- /dev/null
+++ b/receivers/tizen/src/player/Renderer.ts
@@ -0,0 +1,165 @@
+import {
+ isLive,
+ onPlay,
+ player,
+ PlayerControlEvent,
+ playerCtrlCaptions,
+ playerCtrlDuration,
+ playerCtrlLiveBadge,
+ playerCtrlPosition,
+ playerCtrlProgressBar,
+ playerCtrlProgressBarBuffer,
+ playerCtrlProgressBarHandle,
+ playerCtrlProgressBarProgress,
+ playerCtrlStateUpdate,
+ playerCtrlVolumeBar,
+ playerCtrlVolumeBarHandle,
+ playerCtrlVolumeBarProgress,
+ videoCaptions,
+ formatDuration,
+ skipBack,
+ skipForward,
+} from 'common/player/Renderer';
+
+const captionsBaseHeightCollapsed = 150;
+const captionsBaseHeightExpanded = 320;
+const captionsLineHeight = 68;
+
+enum RemoteKeyCode {
+ Stop = 413,
+ Rewind = 412,
+ Play = 415,
+ Pause = 19,
+ FastForward = 417,
+ Back = 10009,
+ MediaPlayPause = 10252,
+}
+
+tizen.tvinputdevice.registerKeyBatch(['MediaRewind',
+ 'MediaFastForward', 'MediaPlay', 'MediaPause', 'MediaStop'
+]);
+
+export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
+ let handledCase = false;
+
+ switch (event) {
+ case PlayerControlEvent.Load: {
+ playerCtrlProgressBarBuffer.setAttribute("style", "width: 0px");
+ playerCtrlProgressBarProgress.setAttribute("style", "width: 0px");
+ playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetLeft}px`);
+
+ const volume = Math.round(player.getVolume() * playerCtrlVolumeBar.offsetWidth);
+ playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
+ playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume + 8}px`);
+
+ if (isLive) {
+ playerCtrlLiveBadge.setAttribute("style", "display: block");
+ playerCtrlPosition.setAttribute("style", "display: none");
+ playerCtrlDuration.setAttribute("style", "display: none");
+ }
+ else {
+ playerCtrlLiveBadge.setAttribute("style", "display: none");
+ playerCtrlPosition.setAttribute("style", "display: block");
+ playerCtrlDuration.setAttribute("style", "display: block");
+ playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
+ playerCtrlDuration.innerHTML = formatDuration(player.getDuration());
+ }
+
+ if (player.isCaptionsSupported()) {
+ // Disabling receiver captions control on TV players
+ playerCtrlCaptions.setAttribute("style", "display: none");
+ // playerCtrlCaptions.setAttribute("style", "display: block");
+ videoCaptions.setAttribute("style", "display: block");
+ }
+ else {
+ playerCtrlCaptions.setAttribute("style", "display: none");
+ videoCaptions.setAttribute("style", "display: none");
+ player.enableCaptions(false);
+ }
+ playerCtrlStateUpdate(PlayerControlEvent.SetCaptions);
+
+ handledCase = true;
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return handledCase;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function targetKeyDownEventListener(event: any): boolean {
+ let handledCase = false;
+
+ switch (event.keyCode) {
+ case RemoteKeyCode.Stop:
+ window.open('../main_window/index.html');
+ handledCase = true;
+ break;
+
+ case RemoteKeyCode.Rewind:
+ skipBack();
+ event.preventDefault();
+ handledCase = true;
+ break;
+
+ case RemoteKeyCode.Play:
+ if (player.isPaused()) {
+ player.play();
+ }
+ event.preventDefault();
+ handledCase = true;
+ break;
+
+ case RemoteKeyCode.Pause:
+ if (!player.isPaused()) {
+ player.pause();
+ }
+ event.preventDefault();
+ handledCase = true;
+ break;
+
+ // Default behavior is to bring up a secondary menu where the user
+ // can use the arrow keys for other media controls, so don't handle
+ // this key manually
+ // case RemoteKeyCode.MediaPlayPause:
+ // if (!player.isPaused()) {
+ // player.pause();
+ // }
+ // else {
+ // player.play();
+ // }
+ // event.preventDefault();
+ // handledCase = true;
+ // break;
+
+ case RemoteKeyCode.FastForward:
+ skipForward();
+ event.preventDefault();
+ handledCase = true;
+ break;
+
+ case RemoteKeyCode.Back:
+ window.open('../main_window/index.html');
+ event.preventDefault();
+ handledCase = true;
+ break;
+
+ default:
+ break;
+ }
+
+ return handledCase;
+};
+
+if (window.tizenOSAPI.pendingPlay !== null) {
+ onPlay(null, window.tizenOSAPI.pendingPlay);
+}
+
+export {
+ captionsBaseHeightCollapsed,
+ captionsBaseHeightExpanded,
+ captionsLineHeight,
+}
diff --git a/receivers/tizen/src/player/index.html b/receivers/tizen/src/player/index.html
new file mode 100644
index 0000000..2ff19a9
--- /dev/null
+++ b/receivers/tizen/src/player/index.html
@@ -0,0 +1,99 @@
+
+
+
+