{"version":3,"file":"static/js/main.cb9b033f.chunk.js","sources":["../node_modules/webrtc-test-suite/dist sync","i18n.js","helpers/constants.js","components/ErrorMessage.js","helpers/userMessages.js","services/Storage.js","services/ApiService.js","services/EmbedService.js","features/demo/demoSlice.js","features/demo/Demo.js","features/embed/embedSlice.js","services/GameService.js","services/ServiceBase.js","helpers/logger.js","services/SignallingService.js","features/player/playerSlice.js","helpers/network.js","helpers/device.js","features/player/PopupPlayer.js","components/PlayerWarning.js","services/WebRtcService.js","services/UEController.js","services/LatencyTester.js","services/UEConnectionService.js","features/signalling/AfkMonitor.js","features/signalling/NetworkChecker.js","components/PlayerDataModal.js","features/signalling/UEConsole.js","features/signalling/WebRTCPlayer.js","features/player/Player.js","img/icons/icon-rotate-phone.svg","features/player/FullscreenFeatures.js","features/player/FullPagePlayer.js","features/embed/Embed.js","features/player/FullscreenPlayer.js","features/player/IFrame.js","App.js","app/store.js","serviceWorker.js","index.js"],"sourceRoot":"","sourcesContent":["function webpackEmptyContext(req) {\n\tvar e = new Error(\"Cannot find module '\" + req + \"'\");\n\te.code = 'MODULE_NOT_FOUND';\n\tthrow e;\n}\nwebpackEmptyContext.keys = function() { return []; };\nwebpackEmptyContext.resolve = webpackEmptyContext;\nmodule.exports = webpackEmptyContext;\nwebpackEmptyContext.id = 225;","import i18n from \"i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nimport { initReactI18next } from \"react-i18next\";\nimport en from \"./i18n/en.json\";\nimport nb from \"./i18n/nb.json\";\nimport pl from \"./i18n/pl.json\";\n\ni18n\n .use(LanguageDetector)\n .use(initReactI18next)\n .init({\n resources: {\n en: en,\n nb: nb,\n no: nb,\n pl: pl,\n },\n keySeparator: \".\",\n fallbackLng: \"en\",\n interpolation: {\n escapeValue: false,\n },\n detection: {\n caches: [\"localStorage\", \"cookie\"],\n order: [\"querystring\", \"cookie\", \"localStorage\", \"navigator\"],\n },\n });\n\nexport default i18n;\n","export const ToClientMessageType = {\n QualityControlOwnership: 0,\n Response: 1,\n Command: 2,\n FreezeFrame: 3,\n UnfreezeFrame: 4,\n VideoEncoderAvgQP: 5,\n LatencyTest: 6,\n InitialSettings: 7,\n};\n\nexport const ToStreamerMessageType = {\n // Must be kept in sync with PixelStreamingProtocol::EToUE4Msg C++ enum.\n\n /*\n * Control Messages. Range = 0..49.\n */\n IFrameRequest: 0,\n RequestQualityControl: 1,\n MaxFpsRequest: 2,\n AverageBitrateRequest: 3,\n StartStreaming: 4,\n StopStreaming: 5,\n LatencyTest: 6,\n RequestInitialSettings: 7,\n\n /*\n * Input Messages. Range = 50..89.\n */\n\n // Generic Input Messages. Range = 50..59.\n UIInteraction: 50,\n Command: 51,\n};\n\nexport const ErrorCodes = {\n // Game and embeds\n GameNotFound: 1001,\n EmbedNotFound: 1002,\n EmbedInactive: 1003,\n DemoNotFound: 1004,\n DemoInactive: 1005,\n EmbedNotInDomain: 1006,\n\n // CoreAPI\n CoreApiConnectionFailed: 2001,\n CoreApiServerError: 2002,\n CoreApiBadRequest: 2003,\n\n // Signalling and WebSockets\n SignallingConnectError: 3001,\n SignallingDisconnected: 3002,\n NoStreamerAvailable: 3003,\n\n // WebRTC\n WebRtcConnectRelayFailed: 4001,\n WebRtcDisconnected: 4002,\n\n // Misc\n UnsupportedBrowser: 5001,\n UserConnectionFailed: 5002,\n};\n\nexport const AllowedDomains = [\"localhost\", \"127.0.0.1\", \"play.apprend.ly\"];\n","import React from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport i18n from \"../i18n\";\nimport PropTypes from \"prop-types\";\nimport classNames from \"classnames\";\nimport { getErrorKey } from \"../helpers/userMessages\";\n\nconst ErrorMessage = (props) => {\n const { t } = useTranslation();\n\n const errorKey = props.errorKey ?? getErrorKey(props.errorCode);\n\n const getDescriptionKey = () => {\n return `errors:${errorKey}.description`;\n };\n\n return (\n \n

{t(`errors:${errorKey}.header`)}

\n {i18n.exists(getDescriptionKey()) && (\n

{t(getDescriptionKey())}

\n )}\n {props.errorCode && (\n

\n {t(\"general:errorCode\")}: {props.errorCode}\n

\n )}\n \n );\n};\n\nErrorMessage.propTypes = {\n errorCode: PropTypes.number,\n errorKey: PropTypes.string,\n};\n\nexport default ErrorMessage;\n","import { ErrorCodes } from \"./constants\";\n\nexport const getErrorKey = (errorCode) => {\n return Object.keys(ErrorCodes).find((key) => ErrorCodes[key] === errorCode);\n};\n","export const localStorageSupported = () => {\r\n var storageSupported = false;\r\n\r\n try {\r\n storageSupported = window.localStorage && true;\r\n } catch (e) {}\r\n\r\n return storageSupported;\r\n};\r\n\r\nexport const setItem = (key, value) => {\r\n if (!localStorageSupported()) {\r\n return;\r\n }\r\n localStorage.setItem(key, JSON.stringify(value));\r\n};\r\n\r\nexport const getItem = (key) => {\r\n if (!localStorageSupported()) {\r\n return null;\r\n }\r\n\r\n return JSON.parse(localStorage.getItem(key));\r\n};\r\n\r\nexport const removeItem = (key) => {\r\n if (!localStorageSupported()) {\r\n return null;\r\n }\r\n\r\n return localStorage.removeItem(key);\r\n};\r\n","import axios from \"axios\";\n\nexport class ApiService {\n get baseUrl() {\n const domain =\n process.env.REACT_APP_CORE_API_BASE_URL || \"https://api.apprend.ly\";\n return `${domain}/api/v1`;\n }\n\n getApiUrl(uri) {\n return `${this.baseUrl}/${uri}`;\n }\n\n apiRequestGet(uri, queryParams = {}, options) {\n return axios.get(this.getApiUrl(uri), { ...options, params: queryParams });\n }\n\n apiRequestPatch(uri, entityId, data) {\n return axios.patch(this.getApiUrl(`${uri}/${entityId}`), data)\n }\n}\n","import { ApiService } from \"./ApiService\";\n\nexport class EmbedService extends ApiService {\n getEmbedDetails(token) {\n return this.apiRequestGet(`embeds/public/${token}`);\n }\n}\n","import { put } from \"redux-saga/effects\";\nimport { createModule } from \"saga-slice\";\nimport { ErrorCodes } from \"../../helpers/constants\";\nimport { EmbedService } from \"../../services/EmbedService\";\n\nconst embedService = new EmbedService();\n\nexport const demoSlice = createModule({\n name: \"demo\",\n initialState: {\n demoToken: \"\",\n demoInfo: null,\n isFetching: false,\n hasError: false,\n errorMessage: \"\",\n },\n\n reducers: {\n fetchDemo: (state, token) => {\n state.isFetching = true;\n },\n fetchSuccess: (state, gameInfo) => {\n state.isFetching = false;\n state.demoInfo = gameInfo;\n },\n fetchFail: (state, message) => {\n state.hasError = true;\n state.errorMessage = message;\n },\n },\n\n sagas: (A) => ({\n *[A.fetchDemo]({ payload }) {\n try {\n const result = yield embedService\n .getEmbedDetails(payload)\n .then((result) => result.data);\n yield put(A.fetchSuccess(result));\n } catch (error) {\n let errorMessage;\n\n if (error.response && error.response.status === 500) {\n errorMessage = ErrorCodes.CoreApiServerError;\n } else if (!error.response || !error.response.status) {\n errorMessage = ErrorCodes.CoreApiConnectionFailed;\n } else {\n errorMessage = ErrorCodes.DemoNotFound;\n }\n\n yield put(A.fetchFail(errorMessage));\n }\n },\n }),\n});\n\nexport const selectDemoInfo = (state) => state.demo.demoInfo;\nexport const selectErrorMessage = (state) => state.demo.errorMessage;\n\nexport const { actions } = demoSlice;\nexport default demoSlice;\n","import React, { useEffect } from \"react\";\nimport { useCallback } from \"react\";\nimport { useState } from \"react\";\nimport {\n browserName,\n browserVersion,\n deviceType,\n fullBrowserVersion,\n osName,\n osVersion,\n} from \"react-device-detect\";\nimport { useTranslation } from \"react-i18next\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { useParams } from \"react-router-dom\";\nimport { StringParam, useQueryParams } from \"use-query-params\";\nimport ErrorMessage from \"../../components/ErrorMessage\";\nimport { getItem, localStorageSupported } from \"../../services/Storage\";\nimport { selectDemoInfo, selectErrorMessage, actions } from \"./demoSlice\";\nimport copy from \"copy-to-clipboard\";\n\nexport function Demo(props) {\n const demoInfo = useSelector(selectDemoInfo);\n const { t } = useTranslation();\n const errorMessage = useSelector(selectErrorMessage);\n const dispatch = useDispatch();\n const params = useParams();\n const [streamerInfo, setStreamerInfo] = useState(null);\n const [queryParams] = useQueryParams({\n enableTracking: StringParam,\n });\n const [showDebugInfo, setShowDebugInfo] = useState(false);\n\n useEffect(() => {\n window.addEventListener(\"message\", onMessageReceived);\n });\n\n const onMessageReceived = useCallback((event) => {\n if (event.data?.type === \"streamerConnected\") {\n setStreamerInfo(event.data.streamer);\n }\n }, []);\n\n const getDebugInfo = () => {\n return {\n \"Player ID\": getPlayerId(),\n \"Device type\": deviceType === \"browser\" ? \"desktop\" : deviceType,\n OS: osName,\n \"OS version\": osVersion,\n Browser: browserName,\n \"Browser version\": fullBrowserVersion,\n \"Last streamer\": streamerInfo\n ? `${streamerInfo?.ip} (${streamerInfo?.id}) ${\n streamerInfo?.isAutoScaled ? \"aws\" : \"gcp\"\n }`\n : \"Never connected\",\n };\n };\n\n const debugTableRows = () => {\n const info = getDebugInfo();\n\n return (\n \n {Object.keys(info).map((key) => (\n \n {key}\n {info[key]}\n \n ))}\n \n );\n };\n\n const debugAsText = () => {\n const info = getDebugInfo();\n let output = \"\";\n\n Object.keys(info).forEach((key) => {\n console.log(key);\n output += `${key}: ${info[key]}\\n`;\n });\n\n return output;\n };\n\n const showSupport = () => {\n setShowDebugInfo(true);\n copy(debugAsText());\n\n const debugInfo = encodeURIComponent(debugAsText());\n const formURL =\n \"https://docs.google.com/forms/d/e/1FAIpQLScw6T0uX01SrDTBwcjO_BoiqoVIBDXK9mnSnr_XT6ik8UXMmw/viewform\";\n\n window.open(`${formURL}?entry.753756817=${debugInfo}`, \"_blank\").focus();\n };\n\n const getPlayerId = () => {\n if (localStorageSupported()) {\n return getItem(\"apprendly:playerId\");\n }\n\n return \"\";\n };\n\n useEffect(() => {\n if (!demoInfo && params.token) {\n dispatch(actions.fetchDemo(params.token));\n }\n }, [params.token, dispatch, demoInfo]);\n\n useEffect(() => {\n if (demoInfo?.accessToken) {\n document.title = `Apprend.ly demo: ${demoInfo.name}`;\n window.addApprendlyEmbed(demoInfo.accessToken, {\n enableTracking: queryParams.enableTracking,\n });\n }\n }, [demoInfo, params.token]);\n\n return (\n
\n
\n {demoInfo && (\n

\n {demoInfo.name}\n

\n )}\n {demoInfo && (\n
\n
\n
\n )}\n {errorMessage && }\n
\n
\n

\n {t(\"player:havingIssues\")}{\" \"}\n \n {t(\"player:sendBugReport\")}\n \n

\n\n {showDebugInfo && (\n <>\n copy(debugAsText())}\n >\n Copy info\n \n
\n \n {debugTableRows()}\n
\n
\n \n )}\n
\n \n );\n}\n","import { put } from \"redux-saga/effects\";\nimport { createModule } from \"saga-slice\";\nimport { ErrorCodes } from \"../../helpers/constants\";\nimport { EmbedService } from \"../../services/EmbedService\";\n\nconst embedService = new EmbedService();\n\nexport const embedSlice = createModule({\n name: \"embed\",\n initialState: {\n embedToken: \"\",\n embedInfo: null,\n isFetching: false,\n hasError: false,\n errorMessage: \"\",\n },\n\n reducers: {\n fetchEmbed: (state, token) => {\n state.isFetching = true;\n },\n fetchSuccess: (state, gameInfo) => {\n state.isFetching = false;\n state.embedInfo = gameInfo;\n },\n fetchFail: (state, message) => {\n state.hasError = true;\n state.errorMessage = message;\n },\n },\n\n sagas: (A) => ({\n *[A.fetchEmbed]({ payload }) {\n try {\n const result = yield embedService\n .getEmbedDetails(payload)\n .then((result) => result.data);\n yield put(A.fetchSuccess(result));\n } catch (error) {\n let errorMessage;\n\n if (error.response && error.response.status === 500) {\n errorMessage = ErrorCodes.CoreApiServerError;\n } else if(error.response && error.response.status === 403) {\n errorMessage = ErrorCodes.EmbedNotInDomain;\n } else if (error.response?.status === 404) {\n errorMessage = ErrorCodes.EmbedNotFound;\n } else {\n errorMessage = ErrorCodes.EmbedInactive;\n }\n\n yield put(A.fetchFail(errorMessage));\n }\n },\n }),\n});\n\nexport const selectEmbedInfo = (state) => state.embed.embedInfo;\nexport const selectErrorMessage = (state) => state.embed.errorMessage;\n\nexport const { actions } = embedSlice;\nexport default embedSlice;\n","import { ApiService } from \"./ApiService\";\n\nexport class GameService extends ApiService {\n getGameDetails(id) {\n return this.apiRequestGet(`games/${id}/meta`);\n }\n}\n","export class ServiceBase {\n eventListeners = {};\n\n /**\n * Add a callback function for a WebRTC event.\n *\n * @param {string} eventName\n * @param {function} callback\n */\n addEventListener(eventName, callback) {\n this.eventListeners[eventName] = this.eventListeners[eventName]\n ? [...this.eventListeners[eventName], callback]\n : [callback];\n }\n\n /**\n * Set callback function for a WebRTC event. Removes any previous listeners.\n *\n * @param {string} eventName\n * @param {function} callback\n */\n setEventListener(eventName, callback) {\n this.eventListeners[eventName] = [callback];\n }\n\n /**\n * Remove all event listeners for a named event.\n *\n * @param {string} eventName\n */\n removeAllEventListeners(eventName) {\n delete this.eventListeners[eventName];\n }\n\n /**\n * Call all event listeners connected to a named event.\n *\n * @param {string} eventName\n * @param {*} data\n */\n callEventListeners(eventName, ...args) {\n if (\n this.eventListeners[eventName] &&\n this.eventListeners[eventName].length\n ) {\n this.eventListeners[eventName].forEach((listener) => listener(...args));\n }\n }\n}\n","export const LogLevel = {\n debug: {\n name: \"debug\",\n level: 0,\n function: \"log\",\n color: \"gray\",\n },\n info: {\n name: \"info\",\n level: 1,\n function: \"log\",\n color: \"green\",\n },\n warning: {\n name: \"warning\",\n level: 2,\n function: \"warn\",\n color: \"yellow\",\n },\n error: {\n name: \"error\",\n level: 3,\n function: \"error\",\n color: \"red\",\n },\n};\n\nexport const getCurrentLogLevel = () => {\n const minLogLevel = process.env.REACT_APP_LOGGING_LEVEL || \"info\",\n minLogLevelSettings = LogLevel[minLogLevel];\n\n return minLogLevelSettings;\n};\n\nexport const log = (level, ...data) => {\n const minLogLevelSettings = getCurrentLogLevel();\n\n if (level.level < minLogLevelSettings.level) {\n return;\n }\n\n console[level.function](\n `%c[${level.name}] `,\n `color: ${level.color};`,\n ...data\n );\n};\n","import { ServiceBase } from \"./ServiceBase\";\nimport { log, LogLevel } from \"../helpers/logger\";\n\nexport class SignallingService extends ServiceBase {\n socket = null;\n callbacks = {};\n initiated = false;\n paused = false;\n socketHost = \"\";\n\n constructor() {\n super();\n\n const socketProtocol =\n window.location.protocol === \"http:\" ? \"ws:\" : \"wss:\";\n const domain = this.getSocketHostDomain();\n const port = process.env.REACT_APP_CORE_API_PXS_PORT || 3210;\n\n this.socketHost = `${socketProtocol}//${domain}:${port}`;\n }\n\n get connected() {\n return this.socket && this.socket.readyState !== WebSocket.CLOSED;\n }\n\n init() {\n log(\n LogLevel.debug,\n \"Init signalling service and connection\",\n this.initiated\n );\n\n if (this.initiated) {\n return;\n }\n\n this.initiated = true;\n this.socket = new WebSocket(this.socketHost);\n\n this.socket.addEventListener(\"open\", () => {\n this.callEventListeners(\"connect\");\n });\n\n this.socket.addEventListener(\"disconnect\", (event) => {\n if (!this.paused) {\n this.callEventListeners(\"disconnect\", event);\n }\n });\n\n this.socket.addEventListener(\"message\", (message) => {\n const data = JSON.parse(message.data);\n log(\n LogLevel.debug,\n \"SignallingService message\",\n message,\n data.type,\n data\n );\n this.callEventListeners(\"message\", data.type, data);\n });\n\n this.socket.addEventListener(\"error\", (error) => {\n console.error(\"SignallingService error\", error);\n this.callEventListeners(\"error\", error);\n });\n\n this.socket.addEventListener(\"close\", (error) => {\n console.error(\"SignallingService close event\", error);\n this.callEventListeners(\"error\", error);\n });\n }\n\n getSocketHostDomain = () => {\n let domain = process.env.REACT_APP_CORE_API_PXS_DOMAIN || \"pxs.apprend.ly\";\n\n const regionMatches = window.location.host.match(/([a-z-]+)\\.play/i);\n if (\n regionMatches &&\n regionMatches[1] &&\n !process.env.PUBLIC_URL.includes(`${regionMatches[1]}.play`)\n ) {\n return `${regionMatches[1]}.${domain}`;\n }\n\n return domain;\n };\n\n pause() {\n if (!this.connected) {\n return;\n }\n\n this.paused = true;\n this.disconnect();\n }\n\n resume() {\n this.socket.connect();\n }\n\n disconnect() {\n if (!this.connected) {\n return;\n }\n\n this.socket.disconnect();\n }\n\n connect() {\n if (this.connected) {\n return;\n }\n\n if (!this.initiated) {\n return this.init();\n }\n\n this.socket.connect();\n }\n\n sendMessage(type, data) {\n if (this.connected) {\n log(LogLevel.debug, \"SignallingService sendMessage\", type, data);\n this.socket.send(JSON.stringify({ type, ...data }));\n }\n }\n\n async getWebRtcConfig() {\n return new Promise((resolve, reject) => {\n if (this.connected) {\n this.socket.send(\"getConfig\", (config) => resolve(config));\n } else {\n reject();\n }\n });\n }\n}\n","import { put } from \"redux-saga/effects\";\r\nimport { createModule } from \"saga-slice\";\r\nimport { GameService } from \"../../services/GameService\";\r\nimport { SignallingService } from \"../../services/SignallingService\";\r\nimport { ErrorCodes } from \"../../helpers/constants\";\r\nimport { setItem } from \"../../services/Storage\";\r\n\r\nconst gameService = new GameService();\r\n\r\nexport const playerSlice = createModule({\r\n name: \"player\",\r\n initialState: {\r\n gameId: \"\",\r\n gameInfo: null,\r\n isFetching: false,\r\n hasError: false,\r\n errorMessage: \"\",\r\n signallingService: new SignallingService(),\r\n signallingConnected: false,\r\n playerId: null,\r\n isScrolledDown: false,\r\n },\r\n\r\n reducers: {\r\n setError: (state, error) => {\r\n state.errorMessage = error;\r\n },\r\n fetchGame: (state, id) => {\r\n state.isFetching = true;\r\n },\r\n setGameInfo: (state, gameInfo) => {\r\n state.gameInfo = gameInfo;\r\n },\r\n fetchSuccess: (state, gameInfo) => {\r\n state.isFetching = false;\r\n state.gameInfo = gameInfo;\r\n },\r\n fetchFail: (state, message) => {\r\n state.hasError = true;\r\n state.errorMessage = message;\r\n },\r\n setupSignallingConnection: (state, payload) => {\r\n state.signallingService.init(payload.gameId);\r\n },\r\n setPlayerId: (state, payload) => {\r\n state.playerId = payload;\r\n setItem(\"apprendly:playerId\", payload);\r\n },\r\n setIsScrolledDown: (state, payload) => {\r\n state.isScrolledDown = payload;\r\n },\r\n },\r\n\r\n sagas: (A) => ({\r\n *[A.fetchGame]({ payload }) {\r\n try {\r\n const result = yield gameService\r\n .getGameDetails(payload)\r\n .then((result) => result.data);\r\n yield put(A.fetchSuccess(result));\r\n } catch (error) {\r\n let errorMessage = error.message;\r\n\r\n if (error.response && error.response.status === 404) {\r\n errorMessage = ErrorCodes.GameNotFound;\r\n } else if (error.response && error.response.status === 500) {\r\n errorMessage = ErrorCodes.CoreApiServerError;\r\n } else {\r\n errorMessage = ErrorCodes.GameNotFound;\r\n }\r\n\r\n yield put(A.fetchFail(errorMessage));\r\n }\r\n },\r\n }),\r\n});\r\n\r\nexport const selectGame = (state) => state.player.gameInfo;\r\nexport const selectErrorMessage = (state) => state.player.errorMessage;\r\nexport const selectSignallingService = (state) =>\r\n state.player.signallingService;\r\nexport const selectPlayerId = (state) => state.player.playerId;\r\nexport const selectIsScrolledDown = (state) => state.isScrolledDown;\r\n\r\nexport const { actions } = playerSlice;\r\nexport default playerSlice;\r\n","import https from \"https\";\n\nconst downloadFile = (url, fileSizeInBytes, minMbps, maxPing) => {\n let startTime = new Date().getTime(),\n startData,\n dataReceived = 0,\n cancelled = false;\n\n return new Promise((resolve, _) => {\n const calculateSpeed = () => {\n const endTime = new Date().getTime();\n const duration = (endTime - startData) / 1000;\n // Convert bytes into bits by multiplying with 8\n const bitsLoaded = dataReceived * 8;\n const bps = bitsLoaded / duration;\n const ping = startData - startTime;\n resolve({ bps, ping, cancelled });\n };\n\n const request = https.get(\n url + `?t=${new Date().getTime()}`,\n (response) => {\n response.once(\"data\", () => {\n startData = new Date().getTime();\n });\n\n response.on(\"data\", (data) => {\n dataReceived += data.length;\n });\n\n response.on(\"end\", () => {\n if (!cancelled) {\n calculateSpeed();\n }\n });\n }\n );\n\n // Cancel request if run for so long that it's below half of the min speed\n const maxTime = (fileSizeInBytes * 8) / (minMbps * 1000 * 1000);\n\n setTimeout(() => {\n cancelled = true;\n calculateSpeed();\n request.end();\n }, maxTime * 2 * 1000 + maxPing);\n }).catch((error) => {\n throw new Error(\"Failed download file test: \" + error);\n });\n};\n\nexport const checkDownloadSpeed = async (minMbps, maxPing) => {\n const files = [\n [\"512kb\", 512 * 1024],\n [\"1mb\", 1024 * 1024],\n [\"3mb\", 3 * 1024 * 1024],\n ];\n\n const baseUrl =\n \"https://duvtp3klbff1f.cloudfront.net/resources/download-test-\";\n let speed = 0,\n ping = 0;\n\n for (let file of files) {\n const result = await downloadFile(\n `${baseUrl}${file[0]}`,\n file[1],\n minMbps,\n maxPing\n );\n\n if (result.cancelled) {\n return { result: false, cancelled: true };\n }\n\n speed += result.bps;\n ping += result.ping;\n }\n\n const bps = Math.round(speed / files.length);\n const mbps = bps / 1000 / 1000;\n const avgPing = ping / files.length;\n\n return {\n result: mbps >= minMbps && avgPing < maxPing,\n cancelled: false,\n mbps,\n avgPing,\n };\n};\n","import {\n isChrome,\n isChromium,\n isEdge,\n isFirefox,\n isIOS,\n isMobileOnly,\n isSafari,\n} from \"react-device-detect\";\n\nexport const isSupportedBrowser = () => {\n return isChrome || isChromium || isEdge || isSafari || isFirefox;\n};\n\nexport const supportedBrowsers = [\n \"Google Chrome\",\n \"Firefox\",\n \"Safari\",\n \"Microsoft Edge\",\n];\n\nexport const supportsFullscreen = () => {\n if (isIOS && isMobileOnly) {\n return false;\n }\n\n const el = document.body;\n\n if (!el) return false;\n\n return (\n el.requestFullscreen ||\n el.mozRequestFullScreen ||\n el.msRequestFullscreen ||\n el.webkitRequestFullScreen\n );\n};\n\nexport const isFullscreen = () => {\n const fullscreenMode =\n document.webkitIsFullScreen ||\n document.mozFullScreen ||\n (document.msFullscreenElement && document.msFullscreenElement !== null) ||\n (document.fullscreenElement && document.fullscreenElement !== null);\n\n return !!fullscreenMode;\n};\n\nexport const setFullscreenListener = (callback) => {\n document.addEventListener(\"webkitfullscreenchange\", callback, false);\n document.addEventListener(\"mozfullscreenchange\", callback, false);\n document.addEventListener(\"fullscreenchange\", callback, false);\n document.addEventListener(\"MSFullscreenChange\", callback, false);\n};\n\nexport const supportsVibrate = () => {\n return !!(\"vibrate\" in navigator);\n};\n\n/**\n * Go into fullscreen with one specific HTML element.\n *\n * @param {*} fullscreenElement\n * @returns {boolean} True if going fullscreen succeeded, otherwise false.\n */\nexport const startFullscreenElement = (fullscreenElement) => {\n let fullscreenFunc = fullscreenElement.requestFullscreen;\n\n if (!fullscreenFunc) {\n [\n \"mozRequestFullScreen\",\n \"msRequestFullscreen\",\n \"webkitRequestFullScreen\",\n ].forEach(function (req) {\n fullscreenFunc = fullscreenFunc || fullscreenElement[req];\n });\n }\n\n if (fullscreenFunc) {\n fullscreenFunc.call(fullscreenElement);\n return true;\n }\n\n return false;\n};\n\nexport const exitFullscreen = () => {\n if (document.exitFullscreen) {\n document.exitFullscreen();\n } else if (document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n};\n","import React from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport PropTypes from \"prop-types\";\nimport { useSelector } from \"react-redux\";\nimport { selectSignallingService } from \"./playerSlice\";\nimport { log, LogLevel } from \"../../helpers/logger\";\n\nconst PopupPlayer = (props) => {\n const { t } = useTranslation();\n\n const signallingService = useSelector(selectSignallingService);\n\n const onClickPlay = () => {\n const url = `${window.location.origin}/fullscreen/${props.embedId}`;\n const newWindow = window.open(url);\n signallingService.pause();\n\n newWindow.addEventListener(\"beforeunload\", () => {\n log(LogLevel.debug, \"Popup player window closed\");\n signallingService.resume();\n });\n };\n\n return (\n
\n

\n {t(\"player:readyToPlay\")}\n

\n \n
\n );\n};\n\nPopupPlayer.propTypes = {\n gameId: PropTypes.string.isRequired,\n embedId: PropTypes.string.isRequired,\n};\n\nexport default PopupPlayer;\n","import React, { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport PropTypes from \"prop-types\";\nimport classNames from \"classnames\";\nimport { log, LogLevel } from \"../helpers/logger\";\nimport { localStorageSupported } from \"../services/Storage\";\n\nconst PlayerWarning = (props) => {\n const { t } = useTranslation();\n\n const [isRemoved, setIsRemoved] = useState(null);\n\n useEffect(() => {\n const shouldBeRemoved = getRemovedWarnings()[props.warningName];\n setIsRemoved(shouldBeRemoved);\n }, [props.warningName]);\n\n const getRemovedWarnings = () => {\n if (!localStorageSupported()) {\n return {};\n }\n\n const fromLs = localStorage.getItem(\"removeWarnings\");\n\n if (!fromLs) {\n return {};\n }\n\n return JSON.parse(fromLs);\n };\n\n const onRemove = () => {\n setIsRemoved(true);\n\n if (props.rememberRemove && localStorageSupported()) {\n try {\n const warnings = getRemovedWarnings();\n warnings[props.warningName] = true;\n localStorage.setItem(\"removeWarnings\", JSON.stringify(warnings));\n } catch (error) {\n log(LogLevel.error, \"Failed to store to localstorage.\", error);\n }\n }\n };\n\n if (!!isRemoved) {\n return null;\n }\n\n return (\n \n
\n {props.children}\n\n {props.removable && (\n \n {t(\"player:continue\")}\n \n )}\n
\n \n );\n};\n\nPlayerWarning.propTypes = {\n warningName: PropTypes.string.isRequired,\n isForeground: PropTypes.bool,\n removable: PropTypes.bool,\n rememberRemove: PropTypes.bool,\n};\n\nexport default PlayerWarning;\n","import { ServiceBase } from \"./ServiceBase\";\n\nexport class WebRtcService extends ServiceBase {\n config = null;\n playerEl = null;\n isConnected = false;\n enableMic = false;\n audioEl = null;\n\n pcClient = null;\n dcClient = null;\n tnClient = null;\n\n sdpConstraints = {\n offerToReceiveAudio: 1,\n offerToReceiveVideo: 1,\n voiceActivityDetection: false,\n };\n\n dataChannelOptions = { ordered: true };\n availableVideoStreams = new Map();\n\n printDebugMessages = true;\n\n constructor() {\n super();\n }\n\n /**\n * Set the video player element.\n *\n * @param {*} playerEl\n * @param {*} audioEl\n */\n setPlayer(playerEl, audioEl) {\n this.playerEl = playerEl;\n this.audioEl = audioEl;\n\n if (this.config) {\n this.setupConnection();\n }\n }\n\n /**\n * Set the WebRTC config to be used for the connection.\n *\n * @param {*} config\n */\n setConfig(config) {\n this.config = {\n ...config,\n sdpSemantics: \"unified-plan\",\n };\n\n if (this.playerEl) {\n this.setupConnection();\n }\n }\n\n setupConnection() {\n this.playerEl.playsInline = true;\n this.playerEl.addEventListener(\n \"loadedmetadata\",\n (event) => {\n this.isConnected = true;\n this.callEventListeners(\"videoInitialised\");\n },\n true\n );\n }\n\n setupDataChannel = (pc, label, options) => {\n try {\n let datachannel = pc.createDataChannel(label, options);\n datachannel.binaryType = \"arraybuffer\";\n\n if (this.printDebugMessages)\n console.log(`Created datachannel (${label})`);\n\n datachannel.onopen = (e) => {\n if (this.printDebugMessages)\n console.log(`data channel (${label}) connect`);\n this.callEventListeners(\"dataChannelConnected\");\n };\n\n datachannel.onclose = (e) => {\n if (this.printDebugMessages)\n console.log(`data channel (${label}) closed`);\n this.callEventListeners(\"dataChannelClosed\");\n };\n\n datachannel.onmessage = (e) => {\n const readToIntArray = (data) => {\n if (data.arrayBuffer !== undefined) {\n return data.arrayBuffer();\n }\n\n return new Promise((resolve, reject) => {\n resolve(data);\n });\n };\n\n readToIntArray(e.data).then((buffer) => {\n const view = new Uint8Array(buffer);\n\n this.callEventListeners(\"dataChannelMessage\", {\n type: view[0],\n data: view.slice(1),\n });\n });\n };\n\n return datachannel;\n } catch (e) {\n console.warn(\"No data channel\", e);\n return null;\n }\n };\n\n handleCandidateFromServer = (iceCandidate) => {\n if (this.printDebugMessages) console.log(\"ICE candidate: \", iceCandidate);\n\n let candidate = new RTCIceCandidate(iceCandidate);\n this.pcClient.addIceCandidate(candidate).then((_) => {\n if (this.printDebugMessages)\n console.log(\"ICE candidate successfully added\");\n });\n };\n\n handleOnAudioTrack = function (audioMediaStream) {\n // do nothing the video has the same media stream as the audio track we have here (they are linked)\n if (this.playerEl.srcObject == audioMediaStream) {\n return;\n }\n // video element has some other media stream that is not associated with this audio track\n else if (\n this.playerEl.srcObject &&\n this.playerEl.srcObject !== audioMediaStream\n ) {\n this.audioEl.srcObject = audioMediaStream;\n console.log(\"Created new audio element to play separate audio stream.\");\n }\n };\n\n handleOnTrack = (e) => {\n if (e.track) {\n console.log(\n \"Got track. | Kind=\" +\n e.track.kind +\n \" | Id=\" +\n e.track.id +\n \" | readyState=\" +\n e.track.readyState +\n \" |\"\n );\n }\n if (this.printDebugMessages) console.log(\"handleOnTrack\", e.streams);\n\n if (e.track.kind === \"audio\") {\n this.handleOnAudioTrack(e.streams[0]);\n return;\n } else if (\n e.track.kind === \"video\" &&\n this.playerEl.srcObject !== e.streams[0]\n ) {\n if (this.printDebugMessages)\n console.log(\"setting video stream from ontrack\");\n this.playerEl.srcObject = e.streams[0];\n }\n };\n\n handleOnIcecandidate = (e) => {\n if (this.printDebugMessages) console.log(\"ICE candidate\", e);\n if (e.candidate && e.candidate.candidate) {\n this.callEventListeners(\"webRtcCandidate\", e.candidate.toJSON());\n }\n };\n\n mungeSDP = (offer) => {\n let audioSDP = \"\";\n\n // set max bitrate to highest bitrate Opus supports\n audioSDP += \"maxaveragebitrate=510000;\";\n\n if (this.useMic) {\n // set the max capture rate to 48khz (so we can send high quality audio from mic)\n audioSDP += \"sprop-maxcapturerate=48000;\";\n }\n\n // Force mono or stereo based on whether ?forceMono was passed or not\n audioSDP += this.forceMonoAudio ? \"stereo=0;\" : \"stereo=1;\";\n\n // enable in-band forward error correction for opus audio\n audioSDP += \"useinbandfec=1\";\n\n // We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.\n offer.sdp = offer.sdp.replace(\"useinbandfec=1\", audioSDP);\n };\n\n handleCreateOffer = () => {\n this.pcClient.createOffer(this.sdpConstraints).then(\n (offer) => {\n this.mungeSDP(offer);\n this.callEventListeners(\"webRtcOffer\", offer);\n },\n function () {\n console.warn(\"Couldn't create offer\");\n }\n );\n };\n\n setupPeerConnection = () => {\n //Setup peerConnection events\n this.pcClient.onsignallingstatechange = (e) =>\n this.callEventListeners(\"onsignallingstatechange\", e);\n this.pcClient.oniceconnectionstatechange = (e) =>\n this.callEventListeners(\"oniceconnectionstatechange\", e);\n this.pcClient.onicegatheringstatechange = (e) =>\n this.callEventListeners(\"onicegatheringstatechange\", e);\n\n this.pcClient.ontrack = this.handleOnTrack;\n this.pcClient.onicecandidate = this.handleOnIcecandidate;\n };\n\n setupTransceivers = async () => {\n this.pcClient.addTransceiver(\"video\", { direction: \"recvonly\" });\n\n if (!this.enableMic) {\n this.pcClient.addTransceiver(\"audio\", { direction: \"recvonly\" });\n } else {\n let audioSendOptions = this.enableMic\n ? {\n autoGainControl: false,\n channelCount: 1,\n echoCancellation: false,\n latency: 0,\n noiseSuppression: false,\n sampleRate: 48000,\n sampleSize: 16,\n volume: 1.0,\n }\n : false;\n\n // Note using mic on android chrome requires SSL or chrome://flags/ \"unsafely-treat-insecure-origin-as-secure\"\n const stream = await navigator.mediaDevices.getUserMedia({\n video: false,\n audio: audioSendOptions,\n });\n if (stream) {\n if (this.pcClient.getTransceivers().length > 0) {\n for (let transceiver of this.pcClient.getTransceivers()) {\n if (\n transceiver &&\n transceiver.receiver &&\n transceiver.receiver.track &&\n transceiver.receiver.track.kind === \"audio\"\n ) {\n for (const track of stream.getTracks()) {\n if (track.kind && track.kind == \"audio\") {\n transceiver.sender.replaceTrack(track);\n transceiver.direction = \"sendrecv\";\n }\n }\n }\n }\n } else {\n for (const track of stream.getTracks()) {\n if (track.kind && track.kind == \"audio\") {\n this.pcClient.addTransceiver(track, { direction: \"sendrecv\" });\n }\n }\n }\n } else {\n this.pcClient.addTransceiver(\"audio\", { direction: \"recvonly\" });\n }\n }\n };\n\n createOffer = () => {\n if (this.pcClient) {\n console.log(\"Closing existing PeerConnection\");\n this.pcClient.close();\n this.pcClient = null;\n }\n\n if (this.printDebugMessages) console.log(\"Set up connection\", this.config);\n\n this.pcClient = new RTCPeerConnection(this.config);\n\n this.setupTransceivers().finally(() => {\n this.setupPeerConnection(this.pcClient);\n\n this.dcClient = this.setupDataChannel(\n this.pcClient,\n \"cirrus\",\n this.dataChannelOptions\n );\n\n this.handleCreateOffer(this.pcClient);\n });\n };\n\n //Called externaly when an answer is received from the server\n receiveAnswer = (answer) => {\n if (this.printDebugMessages) console.log(\"Received answer\", answer);\n const answerDesc = new RTCSessionDescription(answer);\n this.pcClient.setRemoteDescription(answerDesc);\n };\n\n //Called externaly when an offer is received from the server\n receiveOffer = (offer) => {\n if (offer.sfu) {\n this.sfu = true;\n delete offer.sfu;\n }\n\n if (!this.pcClient) {\n console.log(\"Creating a new PeerConnection in the browser.\");\n this.pcClient = new RTCPeerConnection(this.config);\n this.setupPeerConnection(this.pcClient);\n\n // Put things here that happen post transceiver setup\n this.pcClient.setRemoteDescription(offer).then(() => {\n this.setupTransceivers().finally(() => {\n this.dcClient = this.setupDataChannel(\n this.pcClient,\n \"cirrus\",\n this.dataChannelOptions\n );\n\n this.pcClient\n .createAnswer()\n .then((answer) => {\n this.mungeSDP(answer);\n return this.pcClient.setLocalDescription(answer);\n })\n .then(() => {\n this.callEventListeners(\n \"webRtcAnswer\",\n this.pcClient.localDescription\n );\n })\n .then(() => {\n let receivers = this.pcClient.getReceivers();\n for (let receiver of receivers) {\n receiver.playoutDelayHint = 0;\n }\n })\n .catch((error) => console.error(\"createAnswer() failed:\", error));\n });\n });\n }\n };\n\n close = () => {\n if (this.pcClient) {\n if (this.printDebugMessages) console.log(\"Closing existing peerClient\");\n this.pcClient.close();\n this.pcClient = null;\n }\n\n if (this.aggregateStatsIntervalId)\n clearInterval(this.aggregateStatsIntervalId);\n };\n\n //Sends data across the datachannel\n send = (data, type) => {\n if (this.dcClient && this.dcClient.readyState === \"open\") {\n this.dcClient.send(data);\n\n this.callEventListeners(\"dataSent\", type);\n }\n };\n\n getStats = (onStats) => {\n if (this.pcClient && onStats) {\n this.pcClient.getStats(null).then((stats) => {\n onStats(stats);\n });\n }\n };\n\n aggregateStats = (checkInterval) => {\n let printAggregatedStats = () => {\n this.getStats(this.generateAggregatedStats);\n };\n\n console.log(\"Setting interval: aggregateStatsIntervalId\");\n this.aggregateStatsIntervalId = setInterval(\n printAggregatedStats,\n checkInterval\n );\n };\n\n generateAggregatedStats = (stats) => {\n if (!stats) {\n return;\n }\n\n let newStat = {};\n\n stats.forEach((stat) => {\n // console.log(JSON.stringify(stat, undefined, 4));\n if (\n stat.type === \"inbound-rtp\" &&\n !stat.isRemote &&\n (stat.mediaType === \"video\" || stat.id.toLowerCase().includes(\"video\"))\n ) {\n newStat.timestamp = stat.timestamp;\n newStat.bytesReceived = stat.bytesReceived;\n newStat.framesDecoded = stat.framesDecoded;\n newStat.packetsLost = stat.packetsLost;\n newStat.bytesReceivedStart =\n this.aggregatedStats && this.aggregatedStats.bytesReceivedStart\n ? this.aggregatedStats.bytesReceivedStart\n : stat.bytesReceived;\n newStat.framesDecodedStart =\n this.aggregatedStats && this.aggregatedStats.framesDecodedStart\n ? this.aggregatedStats.framesDecodedStart\n : stat.framesDecoded;\n newStat.timestampStart =\n this.aggregatedStats && this.aggregatedStats.timestampStart\n ? this.aggregatedStats.timestampStart\n : stat.timestamp;\n\n if (this.aggregatedStats && this.aggregatedStats.timestamp) {\n if (this.aggregatedStats.bytesReceived) {\n // bitrate = bits received since last time / number of ms since last time\n //This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)\n newStat.bitrate =\n (8 *\n (newStat.bytesReceived - this.aggregatedStats.bytesReceived)) /\n (newStat.timestamp - this.aggregatedStats.timestamp);\n newStat.bitrate = Math.floor(newStat.bitrate);\n newStat.lowBitrate =\n this.aggregatedStats.lowBitrate &&\n this.aggregatedStats.lowBitrate < newStat.bitrate\n ? this.aggregatedStats.lowBitrate\n : newStat.bitrate;\n newStat.highBitrate =\n this.aggregatedStats.highBitrate &&\n this.aggregatedStats.highBitrate > newStat.bitrate\n ? this.aggregatedStats.highBitrate\n : newStat.bitrate;\n }\n\n if (this.aggregatedStats.bytesReceivedStart) {\n newStat.avgBitrate =\n (8 *\n (newStat.bytesReceived -\n this.aggregatedStats.bytesReceivedStart)) /\n (newStat.timestamp - this.aggregatedStats.timestampStart);\n newStat.avgBitrate = Math.floor(newStat.avgBitrate);\n }\n\n if (this.aggregatedStats.framesDecoded) {\n // framerate = frames decoded since last time / number of seconds since last time\n newStat.framerate =\n (newStat.framesDecoded - this.aggregatedStats.framesDecoded) /\n ((newStat.timestamp - this.aggregatedStats.timestamp) / 1000);\n newStat.framerate = Math.floor(newStat.framerate);\n newStat.lowFramerate =\n this.aggregatedStats.lowFramerate &&\n this.aggregatedStats.lowFramerate < newStat.framerate\n ? this.aggregatedStats.lowFramerate\n : newStat.framerate;\n newStat.highFramerate =\n this.aggregatedStats.highFramerate &&\n this.aggregatedStats.highFramerate > newStat.framerate\n ? this.aggregatedStats.highFramerate\n : newStat.framerate;\n }\n\n if (this.aggregatedStats.framesDecodedStart) {\n newStat.avgframerate =\n (newStat.framesDecoded -\n this.aggregatedStats.framesDecodedStart) /\n ((newStat.timestamp - this.aggregatedStats.timestampStart) /\n 1000);\n newStat.avgframerate = Math.floor(newStat.avgframerate);\n }\n }\n }\n\n //Read video track stats\n if (\n stat.type === \"track\" &&\n (stat.trackIdentifier === \"video_label\" || stat.kind === \"video\")\n ) {\n newStat.framesDropped = stat.framesDropped;\n newStat.framesReceived = stat.framesReceived;\n newStat.framesDroppedPercentage =\n (stat.framesDropped / stat.framesReceived) * 100;\n newStat.frameHeight = stat.frameHeight;\n newStat.frameWidth = stat.frameWidth;\n newStat.frameHeightStart =\n this.aggregatedStats && this.aggregatedStats.frameHeightStart\n ? this.aggregatedStats.frameHeightStart\n : stat.frameHeight;\n newStat.frameWidthStart =\n this.aggregatedStats && this.aggregatedStats.frameWidthStart\n ? this.aggregatedStats.frameWidthStart\n : stat.frameWidth;\n }\n\n if (\n stat.type === \"candidate-pair\" &&\n stat.hasOwnProperty(\"currentRoundTripTime\") &&\n stat.currentRoundTripTime !== 0\n ) {\n newStat.currentRoundTripTime = stat.currentRoundTripTime;\n }\n });\n\n this.aggregatedStats = newStat;\n this.callEventListeners(\"aggregatedStats\", newStat);\n };\n}\n","export const MessageType = {\n // Keyboard Input Message. Range = 60..69.\n KeyDown: 60,\n KeyUp: 61,\n KeyPress: 62,\n\n // Mouse Input Messages. Range = 70..79.\n MouseEnter: 70,\n MouseLeave: 71,\n MouseDown: 72,\n MouseUp: 73,\n MouseMove: 74,\n MouseWheel: 75,\n\n // Touch Input Messages. Range = 80..89.\n TouchStart: 80,\n TouchEnd: 81,\n TouchMove: 82,\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button\nexport const MouseButton = {\n MainButton: 0, // Left button.\n AuxiliaryButton: 1, // Wheel button.\n SecondaryButton: 2, // Right button.\n FourthButton: 3, // Browser Back button.\n FifthButton: 4, // Browser Forward button.\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons\nexport const MouseButtonsMask = {\n PrimaryButton: 1, // Left button.\n SecondaryButton: 2, // Right button.\n AuxiliaryButton: 4, // Wheel button.\n FourthButton: 8, // Browser Back button.\n FifthButton: 16, // Browser Forward button.\n};\n\n// Must be kept in sync with JavaScriptKeyCodeToFKey C++ array. The index of the\n// entry in the array is the special key code given below.\nexport const SpecialKeyCodes = {\n BackSpace: 8,\n Shift: 16,\n Control: 17,\n Alt: 18,\n RightShift: 253,\n RightControl: 254,\n RightAlt: 255,\n};\n\nexport const controlTypes = {\n locked: 1,\n unlocked: 2,\n};\n\nexport const disallowedKeys = [\n \"f\", // Used for fullscreen from JS, don't send to UE\n \"F11\", // Triggers fullscreen change in UE, we want this for ourselves\n];\n\nexport class UEController {\n playerEl = null;\n connection = null;\n\n normalizeAndQuantizeUnsigned = null;\n normalizeAndQuantizeSigned = null;\n printDebug = false;\n mouseLocked = false;\n\n playerRes = {};\n\n inputOptions = {\n // Browser keys are those which are typically used by the browser UI. We\n // usually want to suppress these to allow, for example, UE4 to show shader\n // complexity with the F5 key without the web page refreshing.\n suppressBrowserKeys: true,\n\n // UE4 has a faketouches option which fakes a single finger touch when the\n // user drags with their mouse. We may perform the reverse; a single finger\n // touch may be converted into a mouse drag UE4 side. This allows a\n // non-touch application to be controlled partially via a touch device.\n fakeMouseWithTouches: true,\n };\n\n controlType = controlTypes.locked;\n\n playerAspectRatio = 0;\n videoAspectRatio = 0;\n\n /**\n * Constructor.\n *\n * @param {UEConnectionService} connection\n * @param {*} playerEl\n */\n constructor(connection, playerEl) {\n this.connection = connection;\n this.playerEl = playerEl;\n }\n\n /**\n * Send input data to the UE connection.\n *\n * @param {Buffer} buffer\n */\n sendInputData = (buffer) => {\n this.connection.sendInputData(buffer);\n };\n\n /**\n * Normalize and quantize unsigned input.\n *\n * @param {*} x\n * @param {*} y\n * @returns\n */\n normalizeAndQuantizeUnsigned = (x, y) => {\n if (this.playerAspectRatio > this.videoAspectRatio) {\n const ratio = this.playerAspectRatio / this.videoAspectRatio;\n\n let normalizedX = x / this.playerRes.width;\n let normalizedY = ratio * (y / this.playerRes.height - 0.5) + 0.5;\n\n if (\n normalizedX < 0.0 ||\n normalizedX > 1.0 ||\n normalizedY < 0.0 ||\n normalizedY > 1.0\n ) {\n return {\n inRange: false,\n x: 65535,\n y: 65535,\n };\n } else {\n return {\n inRange: true,\n x: normalizedX * 65536,\n y: normalizedY * 65536,\n };\n }\n } else {\n const ratio = this.videoAspectRatio / this.playerAspectRatio;\n\n let normalizedX = ratio * (x / this.playerRes.width - 0.5) + 0.5;\n let normalizedY = y / this.playerRes.height;\n if (\n normalizedX < 0.0 ||\n normalizedX > 1.0 ||\n normalizedY < 0.0 ||\n normalizedY > 1.0\n ) {\n return {\n inRange: false,\n x: 65535,\n y: 65535,\n };\n } else {\n return {\n inRange: true,\n x: normalizedX * 65536,\n y: normalizedY * 65536,\n };\n }\n }\n };\n\n unquantizeAndDenormalizeUnsigned = (x, y) => {\n let normalizedX, normalizedY;\n\n if (this.playerAspectRatio > this.videoAspectRatio) {\n const ratio = this.playerAspectRatio / this.videoAspectRatio;\n\n normalizedX = x / 65536;\n normalizedY = (y / 65536 - 0.5) / ratio + 0.5;\n } else {\n const ratio = this.videoAspectRatio / this.playerAspectRatio;\n\n normalizedX = (x / 65536 - 0.5) / ratio + 0.5;\n normalizedY = y / 65536;\n }\n\n return {\n x: normalizedX * this.playerRes.width,\n y: normalizedY * this.playerRes.height,\n };\n };\n\n normalizeAndQuantizeSigned = (x, y) => {\n let normalizedX, normalizedY;\n\n if (this.playerAspectRatio > this.videoAspectRatio) {\n const ratio = this.playerAspectRatio / this.videoAspectRatio;\n normalizedX = (ratio * x) / (0.5 * this.playerRes.width);\n normalizedY = y / (0.5 * this.playerRes.height);\n } else {\n const ratio = this.videoAspectRatio / this.playerAspectRatio;\n normalizedX = (ratio * x) / (0.5 * this.playerRes.width);\n normalizedY = y / (0.5 * this.playerRes.height);\n }\n\n return {\n x: normalizedX * 32767,\n y: normalizedY * 32767,\n };\n };\n\n /**\n * Set up the functions for normalizing and quantizing mouse input from a player to be sent to UE.\n *\n * @param {object} videoRes Object with width and height of the video stream from UE.\n * @param {object} playerRes Object with width and height of the video player used on the website/app.\n */\n setupNormalizeAndQuantize = (videoRes, playerRes) => {\n this.playerRes = playerRes;\n\n const playerWidth = this.playerRes.width;\n const playerHeight = this.playerRes.height;\n\n this.playerAspectRatio = playerHeight / playerWidth;\n this.videoAspectRatio = videoRes.height / videoRes.width;\n\n if (this.printDebug) {\n console.log(\n \"Setting up normalize. Ratios: \",\n this.playerAspectRatio,\n this.videoAspectRatio,\n videoRes,\n playerRes\n );\n }\n };\n\n emitMouseMove = (x, y, deltaX, deltaY) => {\n let coord = this.normalizeAndQuantizeUnsigned(x, y);\n let delta = this.normalizeAndQuantizeSigned(deltaX, deltaY);\n\n if (this.printDebug) {\n console.log(\n `x: ${x}, y:${y}, dX: ${deltaX}, dY: ${deltaY}`,\n coord,\n delta\n );\n }\n\n let Data = new DataView(new ArrayBuffer(9));\n Data.setUint8(0, MessageType.MouseMove);\n Data.setUint16(1, coord.x, true);\n Data.setUint16(3, coord.y, true);\n Data.setInt16(5, delta.x, true);\n Data.setInt16(7, delta.y, true);\n this.sendInputData(Data.buffer);\n };\n\n emitMouseDown = (button, x, y) => {\n if (this.printDebug) {\n console.log(`mouse button ${button} down at (${x}, ${y})`);\n }\n\n let coord = this.normalizeAndQuantizeUnsigned(x, y);\n let Data = new DataView(new ArrayBuffer(6));\n Data.setUint8(0, MessageType.MouseDown);\n Data.setUint8(1, button);\n Data.setUint16(2, coord.x, true);\n Data.setUint16(4, coord.y, true);\n this.sendInputData(Data.buffer);\n };\n\n emitMouseUp = (button, x, y) => {\n if (this.printDebug) {\n console.log(`mouse button ${button} up at (${x}, ${y})`);\n }\n\n let coord = this.normalizeAndQuantizeUnsigned(x, y);\n let Data = new DataView(new ArrayBuffer(6));\n Data.setUint8(0, MessageType.MouseUp);\n Data.setUint8(1, button);\n Data.setUint16(2, coord.x, true);\n Data.setUint16(4, coord.y, true);\n this.sendInputData(Data.buffer);\n };\n\n emitMouseWheel = (delta, x, y) => {\n if (this.printDebug) {\n console.log(`mouse wheel with delta ${delta} at (${x}, ${y})`);\n }\n\n let coord = this.normalizeAndQuantizeUnsigned(x, y);\n let Data = new DataView(new ArrayBuffer(7));\n Data.setUint8(0, MessageType.MouseWheel);\n Data.setInt16(1, delta, true);\n Data.setUint16(3, coord.x, true);\n Data.setUint16(5, coord.y, true);\n this.sendInputData(Data.buffer);\n };\n\n // If the user has any mouse buttons pressed then release them.\n releaseMouseButtons = (buttons, x, y) => {\n if (buttons & MouseButtonsMask.PrimaryButton) {\n this.emitMouseUp(MouseButton.MainButton, x, y);\n }\n if (buttons & MouseButtonsMask.SecondaryButton) {\n this.emitMouseUp(MouseButton.SecondaryButton, x, y);\n }\n if (buttons & MouseButtonsMask.AuxiliaryButton) {\n this.emitMouseUp(MouseButton.AuxiliaryButton, x, y);\n }\n if (buttons & MouseButtonsMask.FourthButton) {\n this.emitMouseUp(MouseButton.FourthButton, x, y);\n }\n if (buttons & MouseButtonsMask.FifthButton) {\n this.emitMouseUp(MouseButton.FifthButton, x, y);\n }\n };\n\n // If the user has any mouse buttons pressed then press them again.\n pressMouseButtons = (buttons, x, y) => {\n if (buttons & MouseButtonsMask.PrimaryButton) {\n this.emitMouseDown(MouseButton.MainButton, x, y);\n }\n if (buttons & MouseButtonsMask.SecondaryButton) {\n this.emitMouseDown(MouseButton.SecondaryButton, x, y);\n }\n if (buttons & MouseButtonsMask.AuxiliaryButton) {\n this.emitMouseDown(MouseButton.AuxiliaryButton, x, y);\n }\n if (buttons & MouseButtonsMask.FourthButton) {\n this.emitMouseDown(MouseButton.FourthButton, x, y);\n }\n if (buttons & MouseButtonsMask.FifthButton) {\n this.emitMouseDown(MouseButton.FifthButton, x, y);\n }\n };\n\n inputCallbacks = {\n getXYValues: (e) => {\n let x = e.offsetX,\n y = e.offsetY;\n\n if (this.controlType === controlTypes.locked) {\n x = this.playerRes.width / 2;\n y = this.playerRes.height / 2;\n }\n\n return { x, y };\n },\n\n doCallback: (e, callback) => {\n if (this.controlType === controlTypes.locked && !this.mouseLocked) {\n return;\n }\n\n const { x, y } = this.inputCallbacks.getXYValues(e);\n\n callback(x, y);\n },\n\n updateMousePosition: (e) => {\n this.inputCallbacks.doCallback(e, (x, y) => {\n x += e.movementX;\n y += e.movementY;\n\n // // Constrain mouse movement to inside the player frame, instead of moving to the left when disappearing out on the right side, etc.\n // // This way is more like normal mouse movement inside a screen.\n if (x >= this.playerRes.width - 1) {\n x = this.playerRes.width - 1;\n }\n if (y >= this.playerRes.height - 1) {\n y = this.playerRes.height - 1;\n }\n if (x <= 1) {\n x = 1;\n }\n if (y <= 1) {\n y = 1;\n }\n\n this.emitMouseMove(x, y, e.movementX, e.movementY);\n e.preventDefault();\n });\n },\n\n onMouseDown: (e) => {\n this.inputCallbacks.doCallback(e, (x, y) => {\n this.emitMouseDown(e.button, x, y);\n });\n },\n\n onMouseUp: (e) => {\n this.inputCallbacks.doCallback(e, (x, y) => {\n this.emitMouseUp(e.button, x, y);\n });\n },\n\n onMouseWheel: (e) => {\n this.inputCallbacks.doCallback(e, (x, y) => {\n const delta = e.wheelDelta !== undefined ? e.wheelDelta : e.deltaY * -1;\n e.preventDefault();\n e.stopImmediatePropagation();\n this.emitMouseWheel(delta, x, y);\n return false;\n });\n },\n\n onPressMouseButton: (e) => {\n this.inputCallbacks.doCallback(e, (x, y) => {\n this.pressMouseButtons(e.buttons, x, y);\n });\n },\n\n onReleaseMouseButton: (e) => {\n this.inputCallbacks.doCallback(e, (x, y) => {\n this.releaseMouseButtons(e.buttons, x, y);\n });\n },\n };\n\n lockStateChange = () => {\n this.playerEl.requestPointerLock =\n this.playerEl.requestPointerLock || this.playerEl.mozRequestPointerLock;\n\n document.exitPointerLock =\n document.exitPointerLock || document.mozExitPointerLock;\n\n if (\n document.pointerLockElement === this.playerEl ||\n document.mozPointerLockElement === this.playerEl\n ) {\n if (!this.mouseLocked) {\n document.addEventListener(\n \"mousemove\",\n this.inputCallbacks.updateMousePosition,\n false\n );\n this.mouseLocked = true;\n }\n } else {\n console.log(\"The pointer lock status is now unlocked\");\n this.mouseLocked = false;\n\n document.removeEventListener(\n \"mousemove\",\n this.inputCallbacks.updateMousePosition,\n false\n );\n }\n };\n\n /**\n * Register mouse events with the cursor locked into the video player.\n */\n registerLockedMouseEvents = () => {\n this.controlType = controlTypes.locked;\n\n this.playerEl.onclick = () => {\n this.playerEl.requestPointerLock();\n };\n\n // Respond to lock state change events\n // document.removeEventListener(\"pointerlockchange\", this.lockStateChange);\n // document.removeEventListener(\"mozpointerlockchange\", this.lockStateChange);\n document.onpointerlockchange = this.lockStateChange;\n document.onmozpointerlockchange = this.lockStateChange;\n\n this.playerEl.onmousedown = this.inputCallbacks.onMouseDown;\n this.playerEl.onmouseup = this.inputCallbacks.onMouseUp;\n this.playerEl.onwheel = this.inputCallbacks.onMouseWheel;\n this.playerEl.pressMouseButtons = this.inputCallbacks.onPressMouseButton;\n this.playerEl.releaseMouseButtons =\n this.inputCallbacks.onReleaseMouseButton;\n };\n\n /**\n * Register mouse events for when the mouse is visible in the browser and not locked into the video player.\n */\n registerHoveringMouseEvents = () => {\n this.controlType = controlTypes.unlocked;\n\n this.playerEl.onmousemove = this.inputCallbacks.updateMousePosition;\n this.playerEl.onmousedown = this.inputCallbacks.onMouseDown;\n this.playerEl.onmouseup = this.inputCallbacks.onMouseUp;\n this.playerEl.onwheel = this.inputCallbacks.onMouseWheel;\n this.playerEl.pressMouseButtons = this.inputCallbacks.onPressMouseButton;\n this.playerEl.releaseMouseButtons =\n this.inputCallbacks.onReleaseMouseButton;\n\n // When the context menu is shown then it is safest to release the button\n // which was pressed when the event happened. This will guarantee we will\n // get at least one mouse up corresponding to a mouse down event. Otherwise\n // the mouse can get stuck.\n // https://github.com/facebook/react/issues/5531\n this.playerEl.oncontextmenu = (e) => {\n this.emitMouseUp(e.button, e.offsetX, e.offsetY);\n e.preventDefault();\n };\n };\n\n unregisterMouseEvents = () => {};\n\n // Browser keys do not have a charCode so we only need to test keyCode.\n isKeyCodeBrowserKey = (keyCode) => {\n // Function keys or tab key.\n return (\n // Function keys\n (keyCode >= 112 && keyCode <= 123) ||\n // Arrow keys\n keyCode >= 37 ||\n keyCode <= 40 ||\n keyCode === 9\n );\n };\n\n // We want to be able to differentiate between left and right versions of some\n // keys.\n getKeyCode = (e) => {\n if (e.keyCode === SpecialKeyCodes.Shift && e.code === \"ShiftRight\")\n return SpecialKeyCodes.RightShift;\n else if (e.keyCode === SpecialKeyCodes.Control && e.code === \"ControlRight\")\n return SpecialKeyCodes.RightControl;\n else if (e.keyCode === SpecialKeyCodes.Alt && e.code === \"AltRight\")\n return SpecialKeyCodes.RightAlt;\n else return e.keyCode;\n };\n\n registerKeyboardEvents = () => {\n document.onkeydown = (e) => {\n if (this.printDebug) {\n console.log(`key down ${e.keyCode}, repeat = ${e.repeat}`);\n }\n\n if (disallowedKeys.includes(e.key)) {\n return;\n }\n\n this.sendInputData(\n new Uint8Array([MessageType.KeyDown, this.getKeyCode(e), e.repeat])\n .buffer\n );\n\n if (\n this.inputOptions.suppressBrowserKeys &&\n this.isKeyCodeBrowserKey(e.keyCode)\n ) {\n e.preventDefault();\n }\n };\n\n document.onkeyup = (e) => {\n if (this.printDebug) {\n console.log(`key up ${e.keyCode}`);\n }\n\n if (disallowedKeys.includes(e.key)) {\n return;\n }\n\n this.sendInputData(\n new Uint8Array([MessageType.KeyUp, this.getKeyCode(e)]).buffer\n );\n\n if (\n this.inputOptions.suppressBrowserKeys &&\n this.isKeyCodeBrowserKey(e.keyCode)\n ) {\n e.preventDefault();\n }\n };\n\n document.addEventListener(\"keypress\", (e) => {\n if (this.printDebug) {\n console.log(`key press ${e.charCode}`);\n }\n\n if (disallowedKeys.includes(e.key)) {\n return;\n }\n\n let data = new DataView(new ArrayBuffer(3));\n data.setUint8(0, MessageType.KeyPress);\n data.setUint16(1, e.charCode, true);\n this.sendInputData(data.buffer);\n });\n };\n\n unregisterKeyboardEvents = () => {\n document.onkeydown = null;\n document.onkeyup = null;\n };\n\n registerMouseEnterAndLeaveEvents = () => {\n this.playerEl.onmouseenter = (e) => {\n if (this.printDebug) {\n console.log(\"mouse enter\");\n }\n\n let Data = new DataView(new ArrayBuffer(1));\n Data.setUint8(0, MessageType.MouseEnter);\n this.sendInputData(Data.buffer);\n this.pressMouseButtons(e);\n };\n\n this.playerEl.onmouseleave = (e) => {\n if (this.printDebug) {\n console.log(\"mouse leave\");\n }\n\n let Data = new DataView(new ArrayBuffer(1));\n Data.setUint8(0, MessageType.MouseLeave);\n this.sendInputData(Data.buffer);\n this.releaseMouseButtons(e);\n };\n };\n\n registerTouchEvents = () => {\n // We need to assign a unique identifier to each finger.\n // We do this by mapping each Touch object to the identifier.\n const fingers = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];\n let fingerIds = {};\n\n const rememberTouch = (touch) => {\n let finger = fingers.pop();\n if (finger === undefined) {\n console.log(\"exhausted touch indentifiers\");\n }\n fingerIds[touch.identifier] = finger;\n };\n\n const forgetTouch = (touch) => {\n fingers.push(fingerIds[touch.identifier]);\n delete fingerIds[touch.identifier];\n };\n\n const emitTouchData = (type, touches) => {\n let data = new DataView(new ArrayBuffer(2 + 6 * touches.length));\n data.setUint8(0, type);\n data.setUint8(1, touches.length);\n let byte = 2;\n\n for (let t = 0; t < touches.length; t++) {\n let touch = touches[t];\n let x = touch.clientX - this.playerEl.offsetLeft;\n let y = touch.clientY - this.playerEl.offsetTop;\n\n if (this.printDebug) {\n console.log(`F${fingerIds[touch.identifier]}=(${x}, ${y})`);\n }\n\n let coord = this.normalizeAndQuantizeUnsigned(x, y);\n data.setUint16(byte, coord.x, true);\n byte += 2;\n data.setUint16(byte, coord.y, true);\n byte += 2;\n data.setUint8(byte, fingerIds[touch.identifier], true);\n byte += 1;\n data.setUint8(byte, 255 * touch.force, true); // force is between 0.0 and 1.0 so quantize into byte.\n byte += 1;\n }\n\n this.sendInputData(data.buffer);\n };\n\n if (this.inputOptions.fakeMouseWithTouches) {\n let finger = undefined;\n\n this.playerEl.ontouchstart = (e) => {\n if (finger === undefined) {\n let firstTouch = e.changedTouches[0];\n finger = {\n id: firstTouch.identifier,\n x: firstTouch.clientX - this.playerRes.left,\n y: firstTouch.clientY - this.playerRes.top,\n };\n // Hack: Mouse events require an enter and leave so we just\n // enter and leave manually with each touch as this event\n // is not fired with a touch device.\n this.playerEl.onmouseenter(e);\n\n const x = finger.x;\n const y = finger.y;\n\n this.emitMouseMove(x, y, 0, 0);\n\n setTimeout(() => {\n this.emitMouseMove(x, y, 0, 0);\n this.emitMouseDown(MouseButton.MainButton, x, y);\n }, 20);\n }\n e.preventDefault();\n };\n\n this.playerEl.ontouchend = (e) => {\n for (let t = 0; t < e.changedTouches.length; t++) {\n let touch = e.changedTouches[t];\n if (finger && touch.identifier === finger.id) {\n let x = touch.clientX - this.playerRes.left;\n let y = touch.clientY - this.playerRes.top;\n\n setTimeout(() => {\n this.emitMouseUp(MouseButton.MainButton, x, y);\n // Hack: Manual mouse leave event.\n this.playerEl.onmouseleave(e);\n }, 20);\n\n finger = undefined;\n break;\n }\n }\n e.preventDefault();\n };\n\n this.playerEl.ontouchmove = (e) => {\n for (let t = 0; t < e.touches.length; t++) {\n let touch = e.touches[t];\n if (finger && touch.identifier === finger.id) {\n let x = touch.clientX - this.playerRes.left;\n let y = touch.clientY - this.playerRes.top;\n this.emitMouseMove(x, y, x - finger.x, y - finger.y);\n finger.x = x;\n finger.y = y;\n break;\n }\n }\n e.preventDefault();\n };\n } else {\n this.playerEl.ontouchstart = (e) => {\n // Assign a unique identifier to each touch.\n for (let t = 0; t < e.changedTouches.length; t++) {\n rememberTouch(e.changedTouches[t]);\n }\n\n if (this.printDebug) {\n console.log(\"touch start\");\n }\n emitTouchData(MessageType.TouchStart, e.changedTouches);\n e.preventDefault();\n };\n\n this.playerEl.ontouchend = (e) => {\n if (this.printDebug) {\n console.log(\"touch end\");\n }\n emitTouchData(MessageType.TouchEnd, e.changedTouches);\n\n // Re-cycle unique identifiers previously assigned to each touch.\n for (let t = 0; t < e.changedTouches.length; t++) {\n forgetTouch(e.changedTouches[t]);\n }\n e.preventDefault();\n };\n\n this.playerEl.ontouchmove = (e) => {\n if (this.printDebug) {\n console.log(\"touch move\");\n }\n emitTouchData(MessageType.TouchMove, e.touches);\n e.preventDefault();\n };\n }\n };\n}\n","import { ServiceBase } from \"./ServiceBase\";\n\nexport class LatencyTester extends ServiceBase {\n TestStartTimeMs = null;\n UEReceiptTimeMs = null;\n UEPreCaptureTimeMs = null;\n UEPostCaptureTimeMs = null;\n UEPreEncodeTimeMs = null;\n UEPostEncodeTimeMs = null;\n UETransmissionTimeMs = null;\n BrowserReceiptTimeMs = null;\n FrameDisplayDeltaTimeMs = null;\n\n reset = () => {\n this.TestStartTimeMs = null;\n this.UEReceiptTimeMs = null;\n this.UEPreCaptureTimeMs = null;\n this.UEPostCaptureTimeMs = null;\n this.UEPreEncodeTimeMs = null;\n this.UEPostEncodeTimeMs = null;\n this.UETransmissionTimeMs = null;\n this.BrowserReceiptTimeMs = null;\n this.FrameDisplayDeltaTimeMs = null;\n };\n\n setUETimings = (UETimings) => {\n this.UEReceiptTimeMs = UETimings.ReceiptTimeMs;\n this.UEPreCaptureTimeMs = UETimings.PreCaptureTimeMs;\n this.UEPostCaptureTimeMs = UETimings.PostCaptureTimeMs;\n this.UEPreEncodeTimeMs = UETimings.PreEncodeTimeMs;\n this.UEPostEncodeTimeMs = UETimings.PostEncodeTimeMs;\n this.UETransmissionTimeMs = UETimings.TransmissionTimeMs;\n this.BrowserReceiptTimeMs = Date.now();\n this.onAllLatencyTimingsReady();\n };\n\n setFrameDisplayDeltaTime = (DeltaTimeMs) => {\n if (this.FrameDisplayDeltaTimeMs == null) {\n this.FrameDisplayDeltaTimeMs = Math.round(DeltaTimeMs);\n this.onAllLatencyTimingsReady();\n }\n };\n\n onAllLatencyTimingsReady = () => {\n if (!this.BrowserReceiptTimeMs) {\n return;\n }\n\n let latencyExcludingDecode =\n this.BrowserReceiptTimeMs - this.TestStartTimeMs;\n let uePixelStreamLatency =\n this.UEPreEncodeTimeMs === 0 || this.UEPreCaptureTimeMs === 0\n ? \"???\"\n : this.UEPostEncodeTimeMs - this.UEPreCaptureTimeMs;\n let captureLatency = this.UEPostCaptureTimeMs - this.UEPreCaptureTimeMs;\n let encodeLatency = this.UEPostEncodeTimeMs - this.UEPreEncodeTimeMs;\n let ueLatency = this.UETransmissionTimeMs - this.UEReceiptTimeMs;\n let networkLatency = latencyExcludingDecode - ueLatency;\n let browserSendLatency =\n latencyExcludingDecode - networkLatency - ueLatency;\n\n //these ones depend on FrameDisplayDeltaTimeMs\n let endToEndLatency = null;\n let browserSideLatency = null;\n\n if (this.FrameDisplayDeltaTimeMs && this.BrowserReceiptTimeMs) {\n endToEndLatency = this.FrameDisplayDeltaTimeMs + latencyExcludingDecode;\n browserSideLatency = endToEndLatency - networkLatency - ueLatency;\n }\n\n const stats = {\n captureLatency,\n encodeLatency,\n uePixelStreamLatency,\n ueLatency,\n networkLatency,\n latencyExcludingDecode,\n endToEndLatency,\n browserSendLatency,\n browserSideLatency,\n };\n\n this.callEventListeners(\"latencyStats\", stats);\n };\n\n startLatencyTest = () => {\n this.reset();\n this.TestStartTimeMs = Date.now();\n };\n}\n","import { UEController } from \"./UEController\";\nimport {\n ToClientMessageType,\n ToStreamerMessageType,\n} from \"../helpers/constants\";\nimport { ServiceBase } from \"./ServiceBase\";\nimport { LatencyTester } from \"./LatencyTester\";\n\nexport class UEConnectionService extends ServiceBase {\n printDebug = true;\n\n webRtcConn = null;\n controller = null;\n\n latencyTestRunning = false;\n latencyTestInterval = null;\n latencyTester;\n\n constructor(webRtcConn, playerEl) {\n super();\n\n this.webRtcConn = webRtcConn;\n this.controller = new UEController(this, playerEl);\n\n this.webRtcConn.setEventListener(\"videoInitialised\", () => {\n this.callEventListeners(\"connectionOpened\");\n });\n\n this.webRtcConn.setEventListener(\"dataChannelMessage\", ({ type, data }) => {\n if (type === ToClientMessageType.LatencyTest) {\n this.onLatencyTestResult(data);\n }\n\n this.callEventListeners(\"onMessage\", { type, data });\n });\n\n this.latencyTester = new LatencyTester();\n this.latencyTester.setEventListener(\"latencyStats\", (data) => {\n this.callEventListeners(\"latencyStats\", data);\n });\n }\n\n /**\n * Send data to UE.\n *\n * @param {Buffer} data The data to be sent, as a buffer.\n * @param {int} type\n */\n sendData = (data, type) => {\n this.webRtcConn.send(data, type);\n };\n\n /**\n * Send input data to UE.\n *\n * @param {Buffer} inputData The data to be sent, as a buffer.\n */\n sendInputData = (data) => {\n this.sendData(data, ToStreamerMessageType.UIInteraction);\n };\n\n /**\n * Send a command with optional params to UE.\n *\n * @param {string} command\n * @param {object} params\n */\n sendCommand = (command, params) => {\n let descriptor = {\n Command: command,\n ...params,\n };\n\n return this.sendJson(descriptor, ToStreamerMessageType.UIInteraction);\n };\n\n /**\n * Send a JSON message of any type to UE.\n *\n * @param {object} json\n * @param {ToStreamerMessageType} messageType\n */\n sendJson = (json, messageType) => {\n let jsonString = JSON.stringify(json);\n\n // Add the UTF-16 JSON string to the array byte buffer, going two bytes at\n // a time.\n let data = new DataView(new ArrayBuffer(1 + 2 + 2 * jsonString.length));\n let byteIdx = 0;\n data.setUint8(byteIdx, messageType);\n byteIdx++;\n data.setUint16(byteIdx, jsonString.length, true);\n byteIdx += 2;\n\n for (let i = 0; i < jsonString.length; i++) {\n data.setUint16(byteIdx, jsonString.charCodeAt(i), true);\n byteIdx += 2;\n }\n\n this.sendData(data.buffer, messageType);\n };\n\n /**\n * Request quality control; the current user's connection should control the bitrate\n * of the video stream from UE.\n */\n requestQualityControl = () => {\n this.sendData(\n new Uint8Array([ToStreamerMessageType.RequestQualityControl]).buffer,\n ToStreamerMessageType.RequestQualityControl\n );\n };\n\n /**\n * Set up periodic latency testing, at given interval.\n *\n * @param {number} interval Interval length in milliseconds.\n * @returns\n */\n startLatencyTesting = (interval) => {\n if (this.latencyTestRunning) {\n return;\n }\n\n this.latencyTestRunning = true;\n this.latencyTestInterval = setInterval(() => {\n this.startLatencyTest();\n }, interval);\n };\n\n /**\n * Stop running periodic latency tests.\n */\n stopLatencyTesting = () => {\n clearInterval(this.latencyTestInterval);\n this.latencyTestRunning = false;\n };\n\n /**\n * Start a latency test, gathering latency info from UE, connection and browser.\n *\n * @param {*} onTestStarted\n * @returns\n */\n startLatencyTest = () => {\n if (!this.webRtcConn.playerEl) {\n return;\n }\n\n this.latencyTester.startLatencyTest();\n\n let descriptor = {\n StartTime: this.latencyTester.TestStartTimeMs,\n };\n\n this.sendJson(descriptor, ToStreamerMessageType.LatencyTest);\n };\n\n onLatencyTestResult = (data) => {\n let latencyTimingsAsString = new TextDecoder(\"utf-16\").decode(data);\n\n let latencyTimingsFromUE = JSON.parse(latencyTimingsAsString);\n this.latencyTester.setUETimings(latencyTimingsFromUE);\n };\n\n /**\n * Set the desired resolution.\n */\n setResolution = (width, height) => {\n this.sendJson(\n { \"Resolution.Width\": width, \"Resolution.Height\": height },\n ToStreamerMessageType.Command\n );\n };\n}\n","import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport PropTypes from \"prop-types\";\nimport { WebRtcService } from \"../../services/WebRtcService\";\nimport { log, LogLevel } from \"../../helpers/logger\";\nimport { ToStreamerMessageType } from \"../../helpers/constants\";\n\nconst settings = {\n afkWarningTime: 540,\n afkDisconnectTime: 60,\n};\n\nconst AfkMonitor = function ({\n webRtcConn,\n onDisconnect,\n onAfkWarningClicked,\n}) {\n const { t } = useTranslation();\n const listenersSet = useRef(false);\n\n const warningTimer = useRef(null);\n const disconnectInterval = useRef(null);\n const [countdownTime, setCountdownTime] = useState(\n settings.afkDisconnectTime\n );\n\n /**\n * Set up a disconnect timeout, while showing the Afk warning overlay.\n */\n const setupDisconnectTimeout = useCallback(() => {\n log(LogLevel.debug, \"Set up disconnect timeout\");\n setCountdownTime(settings.afkDisconnectTime);\n\n if (warningTimer.current !== null) {\n clearTimeout(warningTimer.current);\n warningTimer.current = null;\n }\n\n if (disconnectInterval.current === null) {\n let timeLeft = settings.afkDisconnectTime;\n console.log(\"Setting interval: disconnectInterval\");\n disconnectInterval.current = setInterval(() => {\n if (timeLeft <= 0) {\n clearInterval(disconnectInterval.current);\n clearTimeout(warningTimer.current);\n\n webRtcConn.close();\n if (onDisconnect) {\n onDisconnect();\n }\n }\n\n setCountdownTime(timeLeft--);\n }, 1000);\n }\n }, [disconnectInterval, onDisconnect, webRtcConn]);\n\n /**\n * Reset the warning timeout. Shows a warning when the time runs out.\n */\n const resetWarningTimeout = useCallback(() => {\n if (warningTimer.current !== null) {\n clearTimeout(warningTimer.current);\n warningTimer.current = null;\n }\n\n if (disconnectInterval.current === null) {\n warningTimer.current = setTimeout(\n setupDisconnectTimeout,\n settings.afkWarningTime * 1000\n );\n }\n }, [warningTimer, setupDisconnectTimeout]);\n\n /**\n * Cancel the disconnect timer.\n */\n const cancelDisconnectTimeout = useCallback(() => {\n clearInterval(disconnectInterval.current);\n disconnectInterval.current = null;\n setCountdownTime(settings.afkDisconnectTime);\n resetWarningTimeout();\n }, [disconnectInterval, resetWarningTimeout]);\n\n /**\n * Set up listener for data on the WebRTC channel. If we are sending data, cancel the timeouts.\n */\n useEffect(() => {\n if (!listenersSet.current) {\n listenersSet.current = true;\n\n webRtcConn.setEventListener(\"dataSent\", (type) => {\n if (type === ToStreamerMessageType.UIInteraction) {\n if (disconnectInterval.current) {\n cancelDisconnectTimeout();\n }\n\n resetWarningTimeout();\n }\n });\n }\n }, [\n cancelDisconnectTimeout,\n disconnectInterval,\n resetWarningTimeout,\n webRtcConn,\n countdownTime,\n ]);\n\n /**\n * Clear all listeners for the WebRTC channel.\n */\n useEffect(() => {\n return function cleanup() {\n clearInterval(disconnectInterval.current);\n clearTimeout(warningTimer.current);\n webRtcConn.removeAllEventListeners(\"dataSent\");\n listenersSet.current = false;\n };\n }, [webRtcConn]);\n\n /**\n * When clicking the warning overlay, cancel the timers.\n */\n const warningClicked = () => {\n cancelDisconnectTimeout();\n\n if (onAfkWarningClicked) {\n onAfkWarningClicked();\n }\n };\n\n log(LogLevel.debug, disconnectInterval.current);\n\n return !disconnectInterval.current ? null : (\n \n

{t(\"player:afkWarningHeader\")}

\n

{t(\"player:moveToContinue\")}

\n

\n {t(\"player:disconnectingCountdown\", { countdownTime })}\n

\n \n );\n};\n\nAfkMonitor.propTypes = {\n webRtcConn: PropTypes.instanceOf(WebRtcService).isRequired,\n onDisconnect: PropTypes.func,\n onWarningClick: PropTypes.func,\n};\n\nexport default AfkMonitor;\n","import React, { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport PropTypes from \"prop-types\";\nimport { UEConnectionService } from \"../../services/UEConnectionService\";\nimport { log, LogLevel } from \"../../helpers/logger\";\n\nconst NetworkChecker = function ({ ueConn }) {\n const { t } = useTranslation();\n\n const [slowConnection, setSlowConnection] = useState(false);\n\n useEffect(() => {\n log(LogLevel.debug, \"Starting periodic latency test\");\n\n ueConn.startLatencyTesting(5000);\n\n ueConn.setEventListener(\"latencyStats\", (data) => {\n const highLatency = data.networkLatency > 350;\n setSlowConnection(highLatency);\n log(LogLevel.debug, \"Received latency stats\", data);\n });\n }, [ueConn]);\n\n return (\n <>\n {slowConnection && (\n
\n {t(\"player:unstableConnectionWarning\")}\n
\n )}\n \n );\n};\n\nNetworkChecker.propTypes = {\n ueConn: PropTypes.instanceOf(UEConnectionService).isRequired,\n};\n\nexport default NetworkChecker;\n","import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nconst PlayerDataModal = ({\n openPlayerModal,\n updatePlayerData,\n closePlayerDataModal,\n}) => {\n const { t } = useTranslation();\n\n const [input, setInput] = useState(\"\");\n\n const handleKeyPress = (event) => {\n // look for the `Enter` keyCode\n if (event.keyCode === 13 || event.which === 13) {\n updatePlayerData(input);\n event.target.blur();\n }\n }\n\n return (\n \n
\n
\n
\n

{t(\"player:whatIsYourName\")}

\n
\n
\n
\n setInput(e.target.value)}\n onKeyPress={handleKeyPress}\n />\n
\n
\n
\n updatePlayerData(input)}\n >\n {t(\"player:submit\")}\n \n
\n
\n
\n \n );\n};\n\nexport default PlayerDataModal;\n","import React, { useState } from \"react\";\nimport PropTypes from \"prop-types\";\nimport { UEConnectionService } from \"../../services/UEConnectionService\";\n\nconst UEConsole = function ({ ueConnection, toggleVisibility, consoleValue }) {\n const [inputValue, setInputValue] = useState(\"\");\n\n const sendCommand = () => {\n ueConnection.sendCommand(\"ExecuteConsoleCommand\", {\n ConsoleCommand: inputValue,\n });\n setInputValue(\"\");\n };\n\n const getValue = () => {\n ueConnection.sendCommand(\"GetConsoleValue\", { ValueName: inputValue });\n setInputValue(\"\");\n };\n\n const onSubmit = (e) => {\n e.preventDefault();\n sendCommand();\n };\n\n return (\n
\n {consoleValue && (\n
\n {consoleValue.name}: {consoleValue.value}\n
\n )}\n\n
\n
\n setInputValue(e.target.value)}\n onFocus={(e) => ueConnection.controller.unregisterKeyboardEvents()}\n onBlur={(e) => ueConnection.controller.registerKeyboardEvents()}\n type=\"text\"\n className=\"w-full bg-transparent rounded-md py-1 px-2 border-white text-black\"\n placeholder=\"Console command line (Enter to send)\"\n />\n \n \n Send command\n \n \n Get value\n \n toggleVisibility(false)}\n className=\"w-8 h-8 rounded-full bg-yellow text-black font-bold\"\n >\n X\n \n
\n
\n );\n};\n\nUEConsole.propTypes = {\n ueConnection: PropTypes.instanceOf(UEConnectionService).isRequired,\n toggleVisibility: PropTypes.func.isRequired,\n};\n\nexport default UEConsole;\n","import React, { createRef, useCallback, useEffect, useState } from \"react\";\nimport PropTypes from \"prop-types\";\nimport useStateRef from \"react-usestateref\";\nimport { WebRtcService } from \"../../services/WebRtcService\";\nimport { UEConnectionService } from \"../../services/UEConnectionService\";\nimport { ErrorCodes, ToClientMessageType } from \"../../helpers/constants\";\nimport { useTranslation } from \"react-i18next\";\nimport classNames from \"classnames\";\nimport AfkMonitor from \"./AfkMonitor\";\nimport { isMobile, isMobileOnly } from \"react-device-detect\";\nimport NetworkChecker from \"./NetworkChecker\";\nimport { log, LogLevel } from \"../../helpers/logger\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport debounce from \"lodash.debounce\";\nimport {\n selectPlayerId,\n selectSignallingService,\n actions,\n} from \"../player/playerSlice\";\nimport {\n isFullscreen,\n setFullscreenListener,\n startFullscreenElement,\n supportsFullscreen,\n supportsVibrate,\n} from \"../../helpers/device\";\nimport { getItem, setItem } from \"../../services/Storage\";\nimport { usePageVisibility } from \"react-page-visibility\";\nimport PlayerDataModal from \"../../components/PlayerDataModal\";\nimport { useQueryParam, StringParam } from \"use-query-params\";\nimport UEConsole from \"./UEConsole\";\n\nconst statusTypes = {\n ready: 1,\n loading: 2,\n loaded: 3,\n playing: 4,\n streamerDisconnected: 5,\n error: 6,\n waiting: 7,\n};\n\nconst WebRTCPlayer = function (props) {\n const { t } = useTranslation();\n const dispatch = useDispatch();\n\n const signallingService = useSelector(selectSignallingService);\n\n const videoPlayerRef = createRef();\n const audioPlayerRef = createRef();\n const videoContainerRef = createRef();\n\n const [signallingConn, setSignallingConn] = useState(null);\n const [streamerConnected, setStreamerConnected] = useState(false);\n const [webRtcConn, setWebRtcConn] = useState(null);\n const [ueConn, setUeConn, ueConnRef] = useStateRef(null);\n const [statusText, setStatusText] = useState();\n const [buttonText, setButtonText] = useState();\n const [status, setStatus, statusRef] = useStateRef(statusTypes.ready);\n const [statusIsTransitioning, setStatusIsTransitioning] = useState();\n const [isChangingScene, setIsChangingScene] = useState(false);\n const [showCursor, setShowCursor] = useState(true);\n const [fullscreenMode, setFullscreenMode] = useState(false);\n const soundMode = getItem(\"apprendly:webplayer:soundMode\");\n const [muted, setMuted] = useState(soundMode && soundMode === \"off\");\n const [playerEl, setPlayerEl, playerElRef] = useStateRef(null);\n const [showQuitConfirmation, setShowQuitConfirmation] = useState(false);\n const [showConsoleInput, setShowConsoleInput] = useState(false);\n const [consoleValue, setConsoleValue] = useState(false);\n const windowIsVisible = usePageVisibility();\n\n const [openPlayerModal, setOpenPlayerModal] = useState(false);\n const [shouldDisableInput, setShouldDisableInput] = useState(true);\n\n const playerId = useSelector(selectPlayerId);\n\n const [playerData] = useQueryParam(\"playerData\", StringParam);\n\n useEffect(() => {\n if (status === statusTypes.playing) {\n setStatusIsTransitioning(true);\n setTimeout(() => setStatusIsTransitioning(false), 500);\n }\n }, [status]);\n\n useEffect(() => {\n if (buttonText?.length) {\n setStatusText(null);\n }\n }, [buttonText]);\n\n useEffect(() => {\n if (statusText?.length) {\n setButtonText(null);\n }\n }, [statusText]);\n\n /**\n * Start playing the game.\n */\n const startPlaying = useCallback(() => {\n setStatus(statusTypes.loading);\n setStatusText(t(\"player:connecting\") + \"...\");\n log(LogLevel.debug, \"Connecting to streamer\", 3);\n signallingService.sendMessage(\"connectToStreamer\");\n\n /* Do not show modal if playerData is set */\n /* This is set as a query param on embed.js */\n if (\n !playerData &&\n !getItem(\"apprendly:webplayer:playerData\") &&\n props.askForPlayerName &&\n props.enableTracking\n ) {\n setShouldDisableInput(false);\n setTimeout(() => {\n setOpenPlayerModal(true);\n }, 2500);\n }\n }, [setStatus, signallingService, t]);\n\n const updatePlayerData = (inputPlayerData) => {\n const inputParts = inputPlayerData.split(\" \");\n const [firstName, lastName] =\n inputParts.length > 1\n ? [\n inputParts.slice(0, -1).join(\" \"), // Join all elements except the last one\n inputParts[inputParts.length - 1], // Get the last element\n ]\n : inputParts;\n const playerData = { firstName, lastName };\n sendGameMessage({\n type: \"gameEvent\",\n eventName: \"playerDataInserted\",\n data: { apprendly: true, playerData },\n });\n\n /* Store value in localStorage */\n setItem(\"apprendly:webplayer:playerData\", playerData);\n\n signallingService.sendMessage(\"playerDataAdded\", playerData);\n\n /* Re-enable inputs */\n if (ueConn) {\n ueConn.controller.registerKeyboardEvents();\n ueConn.controller.registerTouchEvents();\n }\n\n setOpenPlayerModal(false);\n };\n\n const closePlayerDataModal = () => {\n /* Re-enable inputs */\n if (ueConn) {\n ueConn.controller.registerKeyboardEvents();\n ueConn.controller.registerTouchEvents();\n }\n\n setOpenPlayerModal(false);\n };\n\n /**\n * If in autoplay mode, start playing as soon as we're ready.\n */\n useEffect(() => {\n if (props.autostart && status === statusTypes.ready) {\n startPlaying();\n\n if (isMobileOnly) {\n setFullscreenMode(true);\n }\n }\n }, [status, props.autostart, startPlaying]);\n\n /**\n * Send a message regarding the game or player, that can be forwareded to the embed or website.\n *\n * @param {*} data\n */\n const sendGameMessage = useCallback(\n (data) => {\n if (props.onGameMessage) {\n props.onGameMessage(data);\n }\n },\n [props]\n );\n\n const lockCursor = () => {\n if (\n !showCursor &&\n webRtcConn?.playerEl &&\n webRtcConn.playerEl.requestPointerLock\n ) {\n webRtcConn.playerEl.requestPointerLock();\n }\n };\n\n const exitPointerLock = () => {\n if (document.exitPointerLock) {\n document.exitPointerLock();\n }\n };\n\n /**\n * Set up controls for the player with proper translation from click position to screen position in game.\n *\n * Needs to be run after any change of the player size or position.\n */\n const normalizeAndQuantize = () => {\n if (playerElRef.current && ueConnRef.current?.controller) {\n const videoRes = {\n width: playerElRef.current.videoWidth,\n height: playerElRef.current.videoHeight,\n };\n\n const playerRes = playerElRef.current.getBoundingClientRect();\n\n ueConnRef.current.controller.setupNormalizeAndQuantize(\n videoRes,\n playerRes\n );\n }\n };\n\n const normalizeAndQuantizeDebounced = useCallback(\n debounce(normalizeAndQuantize, 500),\n []\n );\n\n useEffect(() => {\n if (windowIsVisible) {\n normalizeAndQuantizeDebounced();\n }\n }, [windowIsVisible, normalizeAndQuantizeDebounced]);\n\n /**\n * Get the desired video resolution based on the size of the video player on screen.\n *\n * @param {*} videoPlayerEl\n * @returns\n */\n const getDesiredResolution = (videoPlayerEl) => {\n const vSize = videoPlayerEl.getBoundingClientRect();\n\n // Set a pixel ratio between 1.3 and 2, depending on device\n const pixelRatio = Math.min(\n 2,\n Math.max(window.devicePixelRatio || 1.3, 1.3)\n );\n\n let width = Math.max(vSize.width, vSize.height) * pixelRatio,\n height = Math.min(vSize.width, vSize.height) * pixelRatio;\n\n if (!isMobile) {\n if (height > width * 0.75) {\n height = width * 0.75;\n } else if (height < width / 2) {\n width = height * 2;\n }\n }\n\n return {\n width,\n height,\n };\n };\n\n const requestLockCursor = useCallback(\n (force) => {\n if (\n (!showCursor || force) &&\n webRtcConn?.playerEl &&\n webRtcConn.playerEl.requestPointerLock\n ) {\n webRtcConn.playerEl.requestPointerLock();\n }\n },\n [showCursor, webRtcConn]\n );\n\n /**\n * Setting up the UE connection with event listeners.\n */\n const setupUeConn = useCallback(\n (ueConn) => {\n // const videoPlayerEl = webRtcConn.playerEl;\n\n ueConn.controller.registerMouseEnterAndLeaveEvents();\n\n ueConn.setEventListener(\"connectionOpened\", () => {\n log(LogLevel.debug, \"UE Connection opened\");\n\n setStatusText(t(\"player:loading\") + \"...\");\n setStatus(statusTypes.loading);\n setPlayerEl(webRtcConn.playerEl);\n\n // webRtcConn.aggregateStats(2000);\n // webRtcConn.addEventListener(\"aggregatedStats\", (stats) =>\n // console.log(stats)\n // );\n\n console.log(\"Setting interval: normalizeAndQuantizeDebounced\");\n\n setInterval(normalizeAndQuantizeDebounced, 800);\n\n const openGameSettings = {\n Id: props.gameId,\n EmbedId: props.embedId,\n PlayerId: playerId,\n EnableTracking: props.enableTracking,\n };\n\n setTimeout(\n () => ueConn.sendCommand(\"OpenGame\", openGameSettings),\n 1000\n );\n\n log(LogLevel.debug, \"Opening game\", openGameSettings);\n\n // ueConn.sendCommand(\"SetDpiScaling\", {\n // Scale:\n // !isMobile || window.devicePixelRatio <= 1\n // ? 1\n // : Math.max(1, Math.min(1.35, window.devicePixelRatio * 0.7)),\n // });\n\n window.addEventListener(\"resize\", () => {\n setTimeout(normalizeAndQuantizeDebounced, 300);\n });\n\n window.addEventListener(\n \"orientationchange scroll\",\n normalizeAndQuantizeDebounced\n );\n\n webRtcConn.playerEl.addEventListener(\n \"resize\",\n normalizeAndQuantizeDebounced\n );\n\n normalizeAndQuantizeDebounced();\n\n if (shouldDisableInput) {\n ueConn.controller.registerKeyboardEvents();\n ueConn.controller.registerTouchEvents();\n }\n });\n\n ueConn.setEventListener(\"onMessage\", ({ type, data }) => {\n switch (type) {\n case ToClientMessageType.Response:\n const received = new TextDecoder(\"utf-16\").decode(data);\n let response;\n if (received) {\n try {\n response = JSON.parse(received);\n } catch (e) {\n response = received;\n }\n }\n\n switch (response.Type) {\n case \"ScenarioLoaded\":\n if (statusRef.current === statusTypes.loading) {\n setTimeout(() => {\n if (response.Settings.MouseVisible && !props.autostart) {\n ueConn.sendCommand(\"InitPlayerControls\", {});\n normalizeAndQuantizeDebounced();\n setStatus(statusTypes.playing);\n } else {\n setStatus(statusTypes.loaded);\n setButtonText(t(\"player:startGame\"));\n }\n }, 3000);\n\n setShowCursor(response.Settings.MouseVisible);\n }\n\n break;\n\n case \"QuitGame\":\n setStatus(statusTypes.streamerDisconnected);\n setStatusText(t(\"player:gameEnded\"));\n exitPointerLock();\n\n if (props.onQuitGame) {\n props.onQuitGame();\n }\n\n setTimeout(() => {\n ueConn.webRtcConn.close();\n setStreamerConnected(false);\n signallingService.sendMessage(\"disconnectedFromStreamer\", {\n reason: \"quitGame\",\n });\n }, 1500);\n\n break;\n case \"MouseVisibility\":\n const visible = response.Visible;\n setShowCursor(visible);\n\n if (visible) {\n ueConn.controller.registerHoveringMouseEvents();\n exitPointerLock();\n } else {\n ueConn.controller.registerLockedMouseEvents();\n requestLockCursor(true);\n }\n\n break;\n case \"GameEvent\":\n sendGameMessage({\n type: \"gameEvent\",\n eventName: response.EventName,\n data: response.Data,\n });\n\n log(\n LogLevel.debug,\n \"Received game event\",\n response.EventName,\n response.Data\n );\n\n if (response.EventName === \"playthroughReady\") {\n const playthroughInfo = {\n playthroughId: response.Data.id,\n gameId: props.gameId,\n gameName: props.gameName,\n playerId: props.playerId,\n };\n\n signallingService.sendMessage(\n \"playthroughStarted\",\n playthroughInfo\n );\n log(\n LogLevel.debug,\n \"Sending playthrough info to signalling\",\n playthroughInfo\n );\n }\n\n if (response.EventName === \"sceneChange\") {\n setIsChangingScene(true);\n } else if (response.EventName === \"sceneStarted\") {\n setIsChangingScene(false);\n }\n\n break;\n\n case \"ToggleConsole\":\n setShowConsoleInput(response.Enabled);\n break;\n\n case \"ConsoleValue\":\n log(LogLevel.debug, \"Received console value\", response);\n setConsoleValue({\n name: response.ValueName,\n value: response.Value,\n });\n break;\n\n default:\n log(\n LogLevel.debug,\n \"Unknown response type from streamer\",\n response\n );\n }\n break;\n\n case ToClientMessageType.Command:\n // let commandAsString = new TextDecoder(\"utf-16\").decode(data);\n // console.log(commandAsString);\n // let command = JSON.parse(commandAsString);\n // if (command.command === \"onScreenKeyboard\") {\n // showOnScreenKeyboard(command);\n // }\n break;\n\n case ToClientMessageType.FreezeFrame:\n // freezeFrame.size = new DataView(data.slice(2, 6).buffer).getInt32(\n // 0,\n // true\n // );\n // freezeFrame.jpeg = data.slice(6);\n // if (freezeFrame.jpeg.length < freezeFrame.size) {\n // console.log(\n // `received first chunk of freeze frame: ${freezeFrame.jpeg.length}/${freezeFrame.size}`\n // );\n // freezeFrame.receiving = true;\n // } else {\n // console.log(\n // `received complete freeze frame: ${freezeFrame.jpeg.length}/${freezeFrame.size}`\n // );\n // showFreezeFrame();\n // }\n break;\n\n case ToClientMessageType.UnfreezeFrame:\n // invalidateFreezeFrameOverlay();\n break;\n\n case ToClientMessageType.VideoEncoderAvgQP:\n // let VideoEncoderQP = new TextDecoder(\"utf-16\").decode(data.slice(1));\n break;\n\n case ToClientMessageType.LatencyTest:\n break;\n\n default:\n console.error(\n `unrecognised data received, packet ID ${type}`,\n data\n );\n }\n });\n },\n [\n webRtcConn,\n setStatus,\n t,\n requestLockCursor,\n sendGameMessage,\n signallingService,\n props,\n statusRef,\n playerId,\n normalizeAndQuantizeDebounced,\n setPlayerEl,\n shouldDisableInput,\n ]\n );\n\n /**\n * Setting up the listeners for WebRTC messages and events.\n */\n const setupWebRtcListeners = useCallback(() => {\n log(LogLevel.debug, \"Setting up webrtc listeners\");\n\n if (!ueConn) {\n log(LogLevel.debug, \"Setting up UE Connection\");\n\n const ueConnection = new UEConnectionService(\n webRtcConn,\n webRtcConn.playerEl\n );\n\n setUeConn(ueConnection);\n setupUeConn(ueConnection);\n }\n\n webRtcConn.setEventListener(\"dataChannelConnected\", () => {});\n\n webRtcConn.setEventListener(\"webRtcOffer\", (offer) => {\n signallingService.sendMessage(\"offer\", offer);\n });\n\n webRtcConn.setEventListener(\"webRtcAnswer\", (answer) => {\n log(LogLevel.debug, \"Sending answer\", answer);\n signallingService.sendMessage(\"answerLegacy\", { answer });\n });\n\n webRtcConn.setEventListener(\"webRtcCandidate\", (candidate) => {\n signallingService.sendMessage(\"iceCandidateLegacy\", {\n candidate,\n });\n });\n\n webRtcConn.playerEl.onclick = () => {\n requestLockCursor();\n };\n\n // webRtcConn.createOffer();\n }, [webRtcConn, ueConn, requestLockCursor, setupUeConn, signallingService]);\n\n const onSignallingServiceMessage = useCallback(\n (type, data) => {\n log(LogLevel.debug, \"Received signalling message\", type, data);\n\n const onConfigReceived = (config) => {\n webRtcConn.setConfig(config.peerConnectionOptions);\n };\n\n const onWebRtcAnswerReceived = (webRtcData) => {\n webRtcConn.receiveAnswer(webRtcData);\n };\n\n const onIceCandidateReceived = (candidate) => {\n webRtcConn.handleCandidateFromServer(candidate);\n };\n\n switch (type) {\n case \"streamerConnected\":\n log(LogLevel.debug, \"Streamer connected\");\n setStreamerConnected(true);\n setupWebRtcListeners();\n\n sendGameMessage({\n type: \"streamerConnected\",\n streamer: data.streamer,\n });\n\n if (status !== statusTypes.loading) {\n setStatus(statusTypes.loading);\n setStatusText(t(\"player:connecting\") + \"...\");\n }\n\n break;\n case \"streamerDisconnected\":\n log(LogLevel.debug, \"Streamer disconnected\", status, data);\n\n if (data.reason === \"playthroughTimeout\") {\n setStatus(statusTypes.streamerDisconnected);\n setStatusText(t(\"player:streamerDisconnectedTimeout\"));\n setStreamerConnected(false);\n setWebRtcConn(null);\n return;\n }\n\n // Check if we're still actually receiving data. If we are, this is a false\n // If still loading while the streamer disconnects, try to find a new one\n if (status === statusTypes.loading) {\n console.log(type, data);\n log(LogLevel.debug, \"Connecting to streamer\", 1);\n setStatus(statusTypes.waiting);\n setStatusText(t(\"player:connecting\") + \"...\");\n setStreamerConnected(false);\n setWebRtcConn(null);\n signallingService.sendMessage(\"connectToStreamer\");\n return;\n }\n\n setStatus(statusTypes.streamerDisconnected);\n setStreamerConnected(false);\n setStatusText(t(\"player:streamerDisconnected\"));\n break;\n case \"couldNotConnectToStreamer\":\n if (data.reason === \"noStreamerAvailable\") {\n // Set to waiting for connection, but show message about no server after 5 minutes\n setTimeout(() => {\n setStatus((status) => {\n if (status === statusTypes.waiting) {\n setStatusText(t(\"player:couldNotConnectToServerTryAgain\"));\n dispatch(actions.setError(ErrorCodes.NoStreamerAvailable));\n return statusTypes.error;\n }\n\n return status;\n });\n }, 1000 * 60 * 5);\n setStatus(statusTypes.waiting);\n setStatusText(t(\"player:noStreamerAvailable\"));\n } else {\n setStatus(statusTypes.error);\n setStatusText(t(\"player:couldNotConnectToServerTryAgain\"));\n }\n break;\n case \"newStreamerAvailable\":\n log(LogLevel.debug, \"New streamer available\", statusRef.current);\n setTimeout(() => {\n if (statusRef.current === statusTypes.waiting) {\n log(LogLevel.debug, \"Connecting to streamer\", 2);\n signallingService.sendMessage(\"connectToStreamer\");\n setStatus(statusTypes.loading);\n }\n }, 500);\n break;\n case \"config\":\n log(LogLevel.debug, \"Config received\", type, data);\n onConfigReceived(data);\n break;\n case \"answer\":\n log(LogLevel.debug, \"Received answer\", type, data);\n onWebRtcAnswerReceived(data);\n break;\n case \"offer\":\n log(LogLevel.debug, \"Received offer\", type, data);\n webRtcConn.receiveOffer(data);\n break;\n case \"iceCandidate\":\n log(LogLevel.debug, \"Received ICE candidate\", type, data);\n onIceCandidateReceived(data.candidate);\n break;\n default:\n log(LogLevel.debug, \"Unknown message type: \", type, data);\n }\n },\n [\n status,\n statusRef,\n setStatus,\n setupWebRtcListeners,\n t,\n webRtcConn,\n signallingService,\n dispatch,\n ]\n );\n\n /**\n * Set up connection to the signalling server.\n */\n const setupSignalling = useCallback(() => {\n signallingService.addEventListener(\"disconnect\", () => {\n setStatusText(\n statusRef.current === statusTypes.playing\n ? t(\"player:lostConnectionToServer\")\n : t(\"player:couldNotConnectServer\")\n );\n setStatus(statusTypes.error);\n setSignallingConn(null);\n setStreamerConnected(false);\n });\n\n signallingService.setEventListener(\"connect\", () => {\n setButtonText(t(\"player:startGame\"));\n setStatus(statusTypes.ready);\n\n signallingService.sendMessage(\"addPlayerInfo\", {\n gameId: props.gameId,\n gameName: props.gameName,\n embedId: props.embedId,\n });\n });\n\n setSignallingConn(true);\n log(LogLevel.debug, \"Setting up signalling connection\");\n }, [t, statusRef, setStatus, signallingService]);\n\n useEffect(() => {\n signallingService.setEventListener(\"message\", onSignallingServiceMessage);\n }, [status, onSignallingServiceMessage, signallingService]);\n\n /**\n * When player is ready, set up connection for Signalling and WebRTC.\n */\n useEffect(() => {\n if (webRtcConn && !signallingConn && status !== statusTypes.error) {\n setupSignalling();\n }\n if (!webRtcConn && videoPlayerRef.current) {\n const webRtcService = new WebRtcService();\n webRtcService.setPlayer(videoPlayerRef.current, audioPlayerRef.current);\n webRtcService.enableMic = props.enableMic;\n setWebRtcConn(webRtcService);\n }\n }, [\n props.gameId,\n props.enableMic,\n webRtcConn,\n signallingConn,\n status,\n setupSignalling,\n videoPlayerRef,\n ]);\n\n /**\n * On click on the video player overlay.\n */\n const overlayClicked = (e) => {\n switch (status) {\n case statusTypes.ready:\n videoPlayerRef.current.play();\n audioPlayerRef.current.play();\n startPlaying();\n\n if (props.onStartLoading) {\n props.onStartLoading();\n }\n\n break;\n\n case statusTypes.streamerDisconnected:\n case statusTypes.error:\n setStatus(statusTypes.loading);\n setStatusText(t(\"player:connecting\") + \"...\");\n console.log(signallingConn, videoPlayerRef.current);\n videoPlayerRef.current.play();\n audioPlayerRef.current.play();\n\n if (signallingConn) {\n log(LogLevel.debug, \"Connecting to streamer\", 4);\n signallingService.sendMessage(\"connectToStreamer\");\n } else {\n setupSignalling();\n }\n break;\n case statusTypes.loaded:\n setStatus(statusTypes.playing);\n setStatusText(null);\n ueConn.sendCommand(\"InitPlayerControls\", {});\n\n setTimeout(() => {\n lockCursor();\n webRtcConn.playerEl.play();\n webRtcConn.audioEl.play();\n\n normalizeAndQuantizeDebounced();\n }, 200);\n\n if (props.onStartPlaying) {\n props.onStartPlaying();\n }\n break;\n case statusTypes.playing:\n lockCursor();\n break;\n default:\n break;\n }\n };\n\n /**\n * Trigger fullscreen.\n */\n const triggerFullscreen = () => {\n if (!props.allowFullscreen) {\n return;\n }\n\n if (fullscreenMode) {\n if (document.exitFullscreen) {\n document.exitFullscreen();\n } else if (document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n\n if (!supportsFullscreen()) {\n sendGameMessage({ type: \"setFullscreen\", fullscreen: false });\n }\n\n setFullscreenMode(false);\n return;\n }\n\n const fullscreenEl = videoContainerRef.current;\n if (startFullscreenElement(fullscreenEl)) {\n if (status === statusTypes.playing) {\n setTimeout(() => lockCursor, 500);\n }\n }\n\n setFullscreenMode(true);\n\n if (!supportsFullscreen()) {\n sendGameMessage({ type: \"setFullscreen\", fullscreen: true });\n }\n };\n\n /**\n * Quit Game from web player quit button.\n */\n const quitGame = () => {\n setStatus(statusTypes.streamerDisconnected);\n setStreamerConnected(false);\n // setStatusText(t(\"player:timeoutDisconnected\"));\n exitPointerLock();\n signallingService.sendMessage(\"disconnectedFromStreamer\", {\n reason: \"userQuit\",\n });\n if (props.onQuitGame) {\n props.onQuitGame();\n }\n setStatus(statusTypes.ready);\n setShowQuitConfirmation(false);\n };\n\n /**\n * Trigger sound mode.\n */\n const triggerSoundMode = () => {\n setMuted(!muted);\n setItem(\"apprendly:webplayer:soundMode\", muted ? \"on\" : \"off\");\n };\n\n /**\n * Handling fullscreen change.\n *\n * When connected to a game, send the new resolution, and make sure mouse gets locked if relevant.\n */\n const onFullscreenChange = useCallback(() => {\n setFullscreenMode(isFullscreen());\n sendGameMessage({ type: \"setFullscreen\", fullscreen: isFullscreen() });\n\n if (ueConn && webRtcConn) {\n setTimeout(normalizeAndQuantizeDebounced, 300);\n\n if (isFullscreen()) {\n requestLockCursor();\n }\n }\n }, [ueConn, webRtcConn, requestLockCursor, sendGameMessage]);\n\n useEffect(() => {\n setFullscreenListener(onFullscreenChange);\n }, [onFullscreenChange]);\n\n const onAfkDisconnect = () => {\n setStatus(statusTypes.error);\n setStreamerConnected(false);\n setStatusText(t(\"player:timeoutDisconnected\"));\n exitPointerLock();\n signallingService.sendMessage(\"disconnectedFromStreamer\", {\n reason: \"afk\",\n });\n };\n\n const onAfkWarningClicked = () => {\n // When the AFK warning is clicked, it means the user has chosen to continue playing, and doesn't have pointer lock currently.\n if (status === statusTypes.playing) {\n setStatus(statusTypes.playing);\n setTimeout(() => lockCursor, 100);\n } else {\n overlayClicked();\n }\n };\n\n return (\n <>\n \n {props.askForPlayerName && (\n \n )}\n\n \n \n {(status === statusTypes.loading ||\n status === statusTypes.waiting) && (\n
\n
\n
\n
\n
\n
\n
\n {/*
*/}\n
\n )}\n {statusText?.length > 0 &&\n status !== statusTypes.ready &&\n !(isMobileOnly && status === statusTypes.loaded) && (\n
\n {statusText}\n
\n )}\n {buttonText?.length && (\n \n )}\n \n \n {!isMobileOnly && status !== statusTypes.ready && (\n setShowQuitConfirmation(true)}\n className=\"icon-container absolute left-2 top-2 w-10 h-10 z-50\"\n title={t(\"player:quit\")}\n >\n
\n \n )}\n {showQuitConfirmation && (\n
\n

\n {t(\"player:quitConfirmation\")}\n

\n
\n \n {t(\"player:quit\")}\n \n setShowQuitConfirmation(false)}\n >\n {t(\"player:cancel\")}\n \n
\n
\n )}\n \n \n \n {props.allowFullscreen && !isMobileOnly && !showConsoleInput && (\n \n \n \n )}\n {props.gameId && (\n <>\n \n \n \n )}\n {streamerConnected && (\n <>\n {(status === statusTypes.playing ||\n status === statusTypes.loaded) && (\n \n )}\n {[statusTypes.loaded, statusTypes.playing].includes(status) &&\n !isChangingScene &&\n ueConn &&\n webRtcConn && }\n \n )}\n {showConsoleInput && (\n \n )}\n \n \n );\n};\n\nWebRTCPlayer.propTypes = {\n gameId: PropTypes.string.isRequired,\n gameName: PropTypes.string,\n embedId: PropTypes.string.isRequired,\n playerId: PropTypes.string,\n allowFullscreen: PropTypes.bool,\n autostart: PropTypes.bool,\n onQuitGame: PropTypes.func,\n onStartLoading: PropTypes.func,\n onStartPlaying: PropTypes.func,\n onGameMessage: PropTypes.func,\n enableTracking: PropTypes.bool,\n askForPlayerName: PropTypes.bool,\n enableMic: PropTypes.bool,\n};\n\nexport default WebRTCPlayer;\n","import React, { useCallback, useEffect, useState } from \"react\";\r\nimport { useSelector, useDispatch } from \"react-redux\";\r\nimport {\r\n selectGame,\r\n selectErrorMessage,\r\n actions,\r\n selectSignallingService,\r\n} from \"./playerSlice\";\r\n\r\nimport { useParams, useLocation } from \"react-router-dom\";\r\nimport { useQueryParam, StringParam } from \"use-query-params\";\r\nimport i18n from \"../../i18n\";\r\nimport { checkDownloadSpeed } from \"../../helpers/network\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { getCurrentLogLevel, log, LogLevel } from \"../../helpers/logger\";\r\nimport { isMobileOnly } from \"react-device-detect\";\r\nimport {\r\n isSupportedBrowser,\r\n supportedBrowsers,\r\n supportsVibrate,\r\n} from \"../../helpers/device\";\r\nimport * as webRtcTest from \"webrtc-test-suite\";\r\nimport { ErrorCodes } from \"../../helpers/constants\";\r\nimport PropTypes from \"prop-types\";\r\nimport PopupPlayer from \"./PopupPlayer\";\r\nimport ErrorMessage from \"../../components/ErrorMessage\";\r\nimport PlayerWarning from \"../../components/PlayerWarning\";\r\nimport { ApiService } from \"../../services/ApiService\";\r\nimport useStateRef from \"react-usestateref\";\r\nimport * as Sentry from \"@sentry/react\";\r\nimport { getItem } from \"../../services/Storage\";\r\nimport { v4 as uuidv4 } from \"uuid\";\r\nimport qs from \"query-string\";\r\nimport WebRTCPlayer from \"../signalling/WebRTCPlayer\";\r\n\r\nconst apiService = new ApiService();\r\n\r\nconst Player = (props) => {\r\n const { t } = useTranslation();\r\n const gameInfo = useSelector(selectGame);\r\n const { search } = useLocation();\r\n const errorMessage = useSelector(selectErrorMessage);\r\n const signallingService = useSelector(selectSignallingService);\r\n const [hasDisconnected, setHasDisconnected, hasDisconnectedRef] =\r\n useStateRef(false);\r\n const [hasPressedPlay, setHasPressedPlay, hasPressedPlayRef] =\r\n useStateRef(false);\r\n const [signallingTested, setSignallingTested] = useState(false);\r\n const [allowFullscreen] = useQueryParam(\"allowFullscreen\", StringParam);\r\n const [loadedFrom] = useQueryParam(\"load\", StringParam);\r\n const [slowConnection, setSlowConnection] = useState(false);\r\n const dispatch = useDispatch();\r\n const params = useParams();\r\n const searchTerms = qs.parse(search);\r\n\r\n /*\r\n * Check local storage if playerId is set otherwise\r\n * Check if playerId is coming as a prop from Embed or FullScreen (fetched from query params) otherwise.\r\n * Generate a random value.\r\n */\r\n let playerId = props.playerId || getItem(\"apprendly:playerId\");\r\n\r\n if (!playerId || playerId.length === 0) {\r\n playerId = uuidv4();\r\n }\r\n\r\n /**\r\n * Send a message to the embed's owner window.\r\n */\r\n const sendEmbedMessage = useCallback(\r\n (type, data = {}) => {\r\n data = { apprendly: true, id: props.embedId, type, ...data };\r\n\r\n if (type === \"error\") {\r\n try {\r\n throw new Error(JSON.stringify(data));\r\n } catch (e) {\r\n Sentry.captureException(e);\r\n }\r\n }\r\n\r\n if ((!props.isFullscreen || props.isFullPage) && window.parent) {\r\n /* Check if player was loaded directly from an iframe.\r\n * In this case send a message to window -> embed.js and parent to propagate events.\r\n */\r\n if (searchTerms.loadedFrom && searchTerms.loadedFrom === \"iframe\") {\r\n window.postMessage(data, \"*\");\r\n window.parent.postMessage(data, \"*\");\r\n } else {\r\n window.parent.postMessage(data, \"*\");\r\n }\r\n } else if (props.isFullscreen && window.opener) {\r\n /* When opener is set we are on mobile. Fullscreen component is used. */\r\n /* This double event messaging is needed on iframe load. On the rest of the loads it will send a double playthough update. */\r\n window.opener?.parent?.postMessage(data, \"*\");\r\n window.postMessage(data, \"*\");\r\n }\r\n },\r\n [props.embedId, props.isFullscreen, searchTerms]\r\n );\r\n\r\n /**\r\n * Game info is received from props. Use to set the game info for the player.\r\n */\r\n useEffect(() => {\r\n if (props.gameInfo) {\r\n dispatch(actions.setGameInfo(props.gameInfo));\r\n }\r\n }, [dispatch, props.gameInfo]);\r\n\r\n /**\r\n * When params are ready, fetch game info from db, unless passed from props already.\r\n */\r\n useEffect(() => {\r\n if (!gameInfo && (props.gameId || params.id) && !props.gameInfo) {\r\n dispatch(actions.fetchGame(props.gameId || params.id));\r\n }\r\n }, [params.id, props.gameId, dispatch, gameInfo, props.gameInfo]);\r\n\r\n /**\r\n * Change UI language based on game settings.\r\n */\r\n useEffect(() => {\r\n if (gameInfo && gameInfo.contents?.Language) {\r\n i18n.changeLanguage(gameInfo.contents.Language);\r\n }\r\n }, [gameInfo]);\r\n\r\n /**\r\n * Set player ID based on props, so it can be used elsewhere in the player.\r\n */\r\n useEffect(() => {\r\n dispatch(actions.setPlayerId(playerId));\r\n }, [playerId, dispatch]);\r\n\r\n /**\r\n * If receiving an error related to signalling connection, set up automatic reconnecting.\r\n */\r\n useEffect(() => {\r\n const disconnectedErrors = [\r\n ErrorCodes.SignallingDisconnected,\r\n ErrorCodes.SignallingConnectError,\r\n ErrorCodes.UserConnectionFailed,\r\n ];\r\n\r\n if (signallingService && disconnectedErrors.includes(errorMessage)) {\r\n signallingService.setEventListener(\"reconnect\", () => {\r\n log(LogLevel.debug, \"Reconnected to signalling\", errorMessage);\r\n\r\n sendEmbedMessage(\"reconnected\");\r\n setHasDisconnected(false);\r\n dispatch(actions.setError(null));\r\n });\r\n }\r\n }, [\r\n errorMessage,\r\n dispatch,\r\n signallingService,\r\n sendEmbedMessage,\r\n setHasDisconnected,\r\n ]);\r\n\r\n /**\r\n * When an error occurs, send info to embed if of a relevant type.\r\n */\r\n useEffect(() => {\r\n if (errorMessage?.length === 0) {\r\n return;\r\n }\r\n\r\n const connectionFailed = [\r\n ErrorCodes.SignallingConnectError,\r\n ErrorCodes.WebRtcConnectRelayFailed,\r\n ErrorCodes.CoreApiConnectionFailed,\r\n ];\r\n const disconnected = [\r\n ErrorCodes.SignallingDisconnected,\r\n ErrorCodes.WebRtcDisconnected,\r\n ErrorCodes.UserConnectionFailed,\r\n ];\r\n\r\n log(\r\n LogLevel.debug,\r\n \"Error sending\",\r\n errorMessage,\r\n connectionFailed.includes(errorMessage),\r\n disconnected.includes(errorMessage)\r\n );\r\n\r\n if (connectionFailed.includes(errorMessage)) {\r\n sendEmbedMessage(\"error\", {\r\n errorType: \"connectionFailed\",\r\n errorCode: errorMessage,\r\n });\r\n } else if (disconnected.includes(errorMessage)) {\r\n sendEmbedMessage(\"error\", {\r\n errorType: \"disconnected\",\r\n errorCode: errorMessage,\r\n });\r\n }\r\n }, [errorMessage, sendEmbedMessage]);\r\n\r\n /**\r\n * Check if the signalling server is up, according to CoreAPI.\r\n *\r\n * @returns {boolean} True if PXS is possibly or confirmed up, false if confirmed down.\r\n */\r\n const checkPxsStatus = async () => {\r\n try {\r\n const result = await apiService.apiRequestGet(\"system/status\");\r\n return result.data.pxsStatus;\r\n } catch (error) {\r\n return true;\r\n }\r\n };\r\n\r\n /**\r\n * Set up connection to signalling, and test WebRTC connection.\r\n */\r\n useEffect(() => {\r\n if (gameInfo && !signallingService.initiated) {\r\n sendEmbedMessage(\"playerLoaded\");\r\n\r\n signallingService.init();\r\n\r\n // Test WebRTC capabilities (if not already in fullscreen, where it's been tested)\r\n if (!props.isFullscreen && process.env.ENABLE_WEBRTC_TEST) {\r\n signallingService.addEventListener(\"connect\", async () => {\r\n const config = await signallingService.getWebRtcConfig();\r\n log(LogLevel.debug, \"Received WebRTC config\", config);\r\n\r\n try {\r\n const webRtcTestResult = await webRtcTest.checkPeerConnection(\r\n {\r\n ...config,\r\n timeout: 15000,\r\n iceTransportPolicy: \"relay\",\r\n },\r\n getCurrentLogLevel() === LogLevel.debug,\r\n 15000\r\n );\r\n log(LogLevel.debug, \"Got WebRTC test result\", webRtcTestResult);\r\n setSignallingTested(true);\r\n sendEmbedMessage(\"playerReady\");\r\n } catch (error) {\r\n log(LogLevel.error, \"Failed to connect to WebRTC test\", error);\r\n dispatch(actions.setError(ErrorCodes.WebRtcConnectRelayFailed));\r\n }\r\n });\r\n } else {\r\n log(LogLevel.debug, \"Skipping WebRTC test\");\r\n setSignallingTested(true);\r\n sendEmbedMessage(\"playerReady\");\r\n }\r\n\r\n signallingService.setEventListener(\"connect_error\", async (data) => {\r\n if (hasDisconnectedRef.current) {\r\n return;\r\n }\r\n\r\n const pxsUp = await checkPxsStatus();\r\n\r\n const errorCode = pxsUp\r\n ? ErrorCodes.UserConnectionFailed\r\n : ErrorCodes.SignallingConnectError;\r\n\r\n log(LogLevel.error, \"Failed to connect to signalling.\", data);\r\n dispatch(actions.setError(errorCode));\r\n });\r\n\r\n signallingService.setEventListener(\"disconnect\", async (data) => {\r\n const pxsUp = await checkPxsStatus();\r\n\r\n const errorCode = pxsUp\r\n ? ErrorCodes.UserConnectionFailed\r\n : ErrorCodes.SignallingDisconnected;\r\n\r\n setHasDisconnected(true);\r\n log(LogLevel.error, \"Disconnected from signalling.\", data);\r\n dispatch(actions.setError(errorCode));\r\n });\r\n }\r\n }, [\r\n signallingService,\r\n gameInfo,\r\n dispatch,\r\n props.isFullscreen,\r\n sendEmbedMessage,\r\n hasDisconnectedRef,\r\n setHasDisconnected,\r\n ]);\r\n\r\n /**\r\n * Callback when receiving a window message.\r\n */\r\n const onWindowMessage = useCallback(\r\n (event) => {\r\n // Messages from a popup player\r\n if (!props.isFullscreen && event.origin === window.origin) {\r\n // Requests from a popup player to receive game info\r\n if (gameInfo && event.data?.type === \"getEmbedInfo\") {\r\n event.source.postMessage({\r\n type: \"embedInfo\",\r\n gameInfo,\r\n enableTracking: props.enableTracking,\r\n playerId,\r\n });\r\n }\r\n // General game messages from a popup player. Forwards so that embed players etc. can receive them.\r\n // else if (event.data?.apprendly && window.parent) {\r\n // window.parent.postMessage(event.data, \"*\");\r\n // }\r\n }\r\n },\r\n [gameInfo, props.isFullscreen, props.enableTracking, playerId]\r\n );\r\n\r\n /**\r\n * Set up listeners for window messages.\r\n */\r\n useEffect(() => {\r\n if (gameInfo) {\r\n window.addEventListener(\"message\", onWindowMessage);\r\n }\r\n }, [gameInfo, onWindowMessage]);\r\n\r\n /**\r\n * Initiate connection speed testing.\r\n */\r\n useEffect(() => {\r\n if (!props.isFullscreen) {\r\n const checkSpeed = async () => {\r\n const speed = await checkDownloadSpeed(3, 350);\r\n log(LogLevel.debug, \"Network download speed test\", speed);\r\n\r\n if (!speed.result) {\r\n setTimeout(() => {\r\n setSlowConnection(true);\r\n }, 3000);\r\n }\r\n };\r\n\r\n setTimeout(() => {\r\n if (!hasPressedPlayRef.current) {\r\n checkSpeed();\r\n }\r\n }, 2000);\r\n }\r\n }, [props.isFullscreen]);\r\n\r\n /**\r\n * Callback when user quits the game (from inside game stream).\r\n */\r\n const onQuitGame = () => {\r\n sendEmbedMessage(\"quitGame\");\r\n\r\n if (props.isFullscreen) {\r\n window.close();\r\n }\r\n };\r\n\r\n /**\r\n * Callback when receiving messages from the WebRTC player with info like game events\r\n * or player changes.\r\n *\r\n * @param {any} data\r\n */\r\n const onGameMessage = (data) => {\r\n sendEmbedMessage(\"gameEvent\", data);\r\n\r\n log(LogLevel.debug, \"Received game message\", data);\r\n\r\n // Small hack: if a value called VibratePhone is set to 1, try to vibrate (works only on Android)\r\n if (\r\n data.type === \"gameEvent\" &&\r\n data.eventName === \"valueChanged\" &&\r\n data.data.valueName === \"VibratePhone\"\r\n ) {\r\n let pattern = data.data.value.trim().split(\",\");\r\n\r\n if (pattern.length > 0 && pattern[0]) {\r\n pattern = pattern.map((v) => parseFloat(v.trim()) * 1000);\r\n\r\n log(\r\n LogLevel.debug,\r\n \"Vibration message received\",\r\n \"Supports vibration: \",\r\n supportsVibrate(),\r\n pattern\r\n );\r\n\r\n if (supportsVibrate()) {\r\n navigator.vibrate(pattern);\r\n }\r\n }\r\n }\r\n };\r\n\r\n /**\r\n * Callback when the user has pressed \"Play\", so the game has started loading.\r\n */\r\n const onStartLoading = () => {\r\n sendEmbedMessage(\"gameMessage\", { eventName: \"startedLoading\" });\r\n setHasPressedPlay(true);\r\n\r\n if (props.onStartLoading) {\r\n props.onStartLoading();\r\n }\r\n };\r\n\r\n return (\r\n \r\n {(props.isFullscreen || signallingTested) && !errorMessage && (\r\n <>\r\n {(!isMobileOnly || props.isFullscreen) && gameInfo && (\r\n \r\n )}\r\n {isMobileOnly && !props.isFullscreen && gameInfo && (\r\n \r\n )}\r\n {!isSupportedBrowser() && (\r\n \r\n

\r\n {t(\"player:unsupportedBrowser\")}\r\n

\r\n

\r\n {t(\"player:supportedBrowsers\", {\r\n replace: { browserList: supportedBrowsers.join(\", \") },\r\n })}\r\n

\r\n \r\n )}\r\n {slowConnection && !hasPressedPlay && (\r\n \r\n

\r\n {t(\"player:slowConnectionHeader\")}\r\n

\r\n

\r\n {t(\"player:slowConnectionDescription\")}\r\n

\r\n \r\n )}\r\n \r\n )}\r\n {!signallingTested && !props.isFullscreen && !errorMessage && (\r\n
{t(\"player:settingUpConnection\")}
\r\n )}\r\n {errorMessage && }\r\n \r\n );\r\n};\r\n\r\nPlayer.propTypes = {\r\n gameId: PropTypes.string.isRequired,\r\n embedId: PropTypes.string.isRequired,\r\n playerId: PropTypes.string,\r\n gameInfo: PropTypes.object,\r\n isFullscreen: PropTypes.bool,\r\n isFullPage: PropTypes.bool,\r\n autostart: PropTypes.bool,\r\n onStartPlaying: PropTypes.func,\r\n onStartLoading: PropTypes.func,\r\n enableTracking: PropTypes.bool,\r\n};\r\n\r\nexport default Player;\r\n","function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\nimport * as React from \"react\";\n\nvar _ref2 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M28.5 9.5H11.5C10.3954 9.5 9.5 10.3954 9.5 11.5V40.5C9.5 41.6046 10.3954 42.5 11.5 42.5H28.5C29.6046 42.5 30.5 41.6046 30.5 40.5V11.5C30.5 10.3954 29.6046 9.5 28.5 9.5Z\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref3 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M24.5 54.5C23.7044 54.5 22.9413 54.1839 22.3787 53.6213C21.8161 53.0587 21.5 52.2956 21.5 51.5\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref4 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M51.5 54.5C52.2956 54.5 53.0587 54.1839 53.6213 53.6213C54.1839 53.0587 54.5 52.2956 54.5 51.5\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref5 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M21.5 42.5V45.5\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref6 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M54.5 42.5V45.5\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref7 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M30.5 54.5H35\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref8 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M41 54.5H45.5\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref9 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M51.5 33.5C52.2956 33.5 53.0587 33.8161 53.6213 34.3787C54.1839 34.9413 54.5 35.7044 54.5 36.5\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref10 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M30.5 33.5H35\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref11 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M41 33.5H45.5\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref12 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M38.8799 11L35.4399 14.44C35.2996 14.5786 35.1883 14.7436 35.1122 14.9256C35.0362 15.1076 34.9971 15.3028 34.9971 15.5C34.9971 15.6972 35.0362 15.8924 35.1122 16.0744C35.1883 16.2564 35.2996 16.4214 35.4399 16.56L38.8799 20\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref13 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M44 25.12L47.44 28.56C47.5786 28.7003 47.7436 28.8117 47.9256 28.8877C48.1076 28.9637 48.3028 29.0029 48.5 29.0029C48.6972 29.0029 48.8924 28.9637 49.0744 28.8877C49.2564 28.8117 49.4214 28.7003 49.56 28.56L53 25.12\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nvar _ref14 = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M35 15.5H42.5C44.0913 15.5 45.6174 16.1321 46.7426 17.2574C47.8679 18.3826 48.5 19.9087 48.5 21.5V29\",\n stroke: \"white\",\n strokeWidth: 3,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n});\n\nfunction SvgIconRotatePhone(_ref, svgRef) {\n var title = _ref.title,\n titleId = _ref.titleId,\n props = _objectWithoutProperties(_ref, [\"title\", \"titleId\"]);\n\n return /*#__PURE__*/React.createElement(\"svg\", _extends({\n width: 64,\n height: 64,\n viewBox: \"0 0 64 64\",\n fill: \"none\",\n xmlns: \"http://www.w3.org/2000/svg\",\n ref: svgRef,\n \"aria-labelledby\": titleId\n }, props), title ? /*#__PURE__*/React.createElement(\"title\", {\n id: titleId\n }, title) : null, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12, _ref13, _ref14);\n}\n\nvar ForwardRef = /*#__PURE__*/React.forwardRef(SvgIconRotatePhone);\nexport default __webpack_public_path__ + \"static/media/icon-rotate-phone.da27e80b.svg\";\nexport { ForwardRef as ReactComponent };","import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n exitFullscreen,\n isFullscreen,\n setFullscreenListener,\n startFullscreenElement,\n supportsFullscreen,\n} from \"../../helpers/device\";\nimport { useTranslation } from \"react-i18next\";\nimport PropTypes from \"prop-types\";\n\nimport { ReactComponent as RotateIcon } from \"../../img/icons/icon-rotate-phone.svg\";\n\nconst FullscreenFeatures = (props) => {\n const { t } = useTranslation();\n\n const [showQuitConfirmation, setShowQuitConfirmation] = useState(false);\n const [showFullscreenResume, setShowFullscreenResume] = useState(false);\n\n useEffect(() => {\n if (props.shouldSetupFullscreen) {\n setFullscreenListener(onFullscreenChange);\n startFullscreen();\n } else if (props.shouldSetupFullscreen === false && isFullscreen()) {\n exitFullscreen();\n }\n }, [props.shouldSetupFullscreen]);\n\n const onFullscreenChange = () => {\n setShowFullscreenResume(!isFullscreen());\n };\n\n const startFullscreen = () => {\n setShowFullscreenResume(false);\n const fullscreenEl = props.parentRef.current;\n startFullscreenElement(fullscreenEl);\n };\n\n return (\n <>\n {props.enableRotateWarning && (\n
\n

{t(\"player:rotateDevice\")}

\n \n
\n )}\n\n {showFullscreenResume && supportsFullscreen() && (\n
\n \n {t(\"player:resumeFullscreen\")}\n \n
\n )}\n {props.showQuitButton && (\n setShowQuitConfirmation(true)}\n className=\"icon-container fixed left-2 top-2 w-10 h-10 z-50\"\n >\n
\n \n )}\n {showQuitConfirmation && (\n
\n

\n {t(\"player:quitConfirmation\")}\n

\n
\n \n {t(\"player:quit\")}\n \n setShowQuitConfirmation(false)}\n >\n {t(\"player:cancel\")}\n \n
\n
\n )}\n \n );\n};\n\nFullscreenFeatures.propTypes = {\n onQuit: PropTypes.func.isRequired,\n showQuitButton: PropTypes.bool,\n};\n\nexport default FullscreenFeatures;\n","import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport PropTypes from \"prop-types\";\nimport Player from \"./Player\";\nimport { supportsFullscreen } from \"../../helpers/device\";\nimport FullscreenFeatures from \"./FullscreenFeatures\";\nimport { useQueryParams } from \"use-query-params\";\nimport { isMobileOnly } from \"react-device-detect\";\n\nconst FullPagePlayer = (props) => {\n const { t } = useTranslation();\n\n const containerRef = useRef();\n const [errorMessage] = useState(null);\n const [setupFullscreen] = useState(null);\n const [, setIsScrolledDown] = useState(false);\n const [hasLoaded, setHasLoaded] = useState(false);\n\n const onScroll = useCallback(() => {\n if (supportsFullscreen()) {\n return;\n }\n\n const scrolledDown = window.scrollY >= window.innerHeight / 2 + 100;\n setIsScrolledDown(scrolledDown);\n }, []);\n\n useEffect(() => {\n if (!supportsFullscreen()) {\n window.addEventListener(\"scroll\", onScroll);\n window.addEventListener(\"orientationchange\", () => {\n window.scrollTo(0, 0);\n });\n } else {\n setIsScrolledDown(true);\n }\n }, [onScroll]);\n\n /**\n * Send a message to the embed's owner window.\n */\n const sendEmbedMessage = useCallback(\n (type, data = {}) => {\n data = { apprendly: true, id: props.embedId, type, ...data };\n\n if (window.parent) {\n window.parent.postMessage(data, \"*\");\n }\n },\n [props.embedId]\n );\n\n const quitGame = () => {\n sendEmbedMessage(\"quitGame\");\n };\n\n const onStartLoading = () => {\n setHasLoaded(true);\n };\n\n return (\n
\n {props.embedId && !errorMessage && (\n \n )}\n {errorMessage &&
{errorMessage}
}\n \n
\n );\n};\n\nFullPagePlayer.propTypes = {\n embedId: PropTypes.string.isRequired,\n};\n\nexport default FullPagePlayer;\n","import React, { useEffect } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { AllowedDomains, ErrorCodes } from \"../../helpers/constants\";\nimport { useParams } from \"react-router-dom\";\nimport { selectEmbedInfo, selectErrorMessage, actions } from \"./embedSlice\";\nimport Player from \"../player/Player\";\nimport ErrorMessage from \"../../components/ErrorMessage\";\nimport { useQueryParams, StringParam } from \"use-query-params\";\nimport { removeItem } from \"../../services/Storage\";\nimport FullPagePlayer from \"../player/FullPagePlayer\";\n\nexport function Embed(props) {\n const embedInfo = useSelector(selectEmbedInfo);\n const errorMessage = useSelector(selectErrorMessage);\n const dispatch = useDispatch();\n const params = useParams();\n const [queryParams] = useQueryParams({\n autostart: StringParam,\n playerId: StringParam,\n enableTracking: StringParam,\n resetPlayerData: StringParam,\n gameId: StringParam,\n fullPage: StringParam,\n });\n\n const getGameId = () => {\n if (embedInfo.game && embedInfo.game.id) {\n return embedInfo.game.id;\n } else if (\n embedInfo.course &&\n queryParams.gameId &&\n embedInfo.course.games.indexOf(queryParams.gameId) >= 0\n ) {\n return queryParams.gameId;\n }\n\n return null;\n };\n\n const isDomainAllowed = (domains) => {\n const domain = window.location.hostname;\n let inAllowedDomains = false;\n\n AllowedDomains.forEach((allowedDomain) => {\n const index = domain.indexOf(allowedDomain);\n if (index !== -1) {\n inAllowedDomains = true;\n }\n });\n\n if (inAllowedDomains) {\n return true;\n }\n\n if (domains.includes(domain)) {\n return true;\n }\n return false;\n };\n\n useEffect(() => {\n if (!embedInfo && params.embedId) {\n dispatch(actions.fetchEmbed(params.embedId));\n }\n }, [params.token, dispatch, embedInfo, params.embedId]);\n\n useEffect(() => {\n window.apprendly.initialize();\n\n if (queryParams.resetPlayerData === \"true\") {\n removeItem(\"apprendly:webplayer:playerData\");\n removeItem(\"apprendly:playerId\");\n }\n }, []);\n\n useEffect(() => {\n if (embedInfo) {\n // Check if embed is restricted for specific domains.\n if (embedInfo.allowedDomains.length) {\n const inDomains = isDomainAllowed(embedInfo.allowedDomains);\n if (!inDomains) {\n dispatch(actions.fetchFail(ErrorCodes.EmbedNotInDomain));\n }\n }\n document.title = embedInfo.name;\n }\n }, [embedInfo, dispatch]);\n\n return (\n
\n {embedInfo &&\n !errorMessage &&\n getGameId() &&\n queryParams.fullPage !== \"true\" && (\n
\n \n \n
\n )}\n {embedInfo &&\n !errorMessage &&\n getGameId() &&\n queryParams.fullPage === \"true\" && (\n
\n \n
\n )}\n {embedInfo && !errorMessage && !getGameId() && (\n \n )}\n {errorMessage && }\n
\n );\n}\n","import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { selectGame, actions } from \"./playerSlice\";\nimport { useParams } from \"react-router\";\nimport PropTypes from \"prop-types\";\nimport Player from \"./Player\";\nimport { log, LogLevel } from \"../../helpers/logger\";\nimport { supportsFullscreen } from \"../../helpers/device\";\nimport FullscreenFeatures from \"./FullscreenFeatures\";\n\nconst FullscreenPlayer = (props) => {\n const { t } = useTranslation();\n\n const containerRef = useRef();\n const [errorMessage, setErrorMessage] = useState(null);\n const [embedInfo, setEmbedInfo] = useState(null);\n const [setupFullscreen, setSetupFullscreen] = useState(false);\n const gameInfo = useSelector(selectGame);\n const dispatch = useDispatch();\n const params = useParams();\n const [isScrolledDown, setIsScrolledDown] = useState(false);\n\n const onScroll = useCallback(() => {\n if (supportsFullscreen()) {\n return;\n }\n\n const scrolledDown = window.scrollY >= window.innerHeight / 2 + 100;\n setIsScrolledDown(scrolledDown);\n }, []);\n\n useEffect(() => {\n if (!supportsFullscreen()) {\n window.addEventListener(\"scroll\", onScroll);\n window.addEventListener(\"orientationchange\", () => {\n window.scrollTo(0, 0);\n });\n } else {\n setIsScrolledDown(true);\n }\n }, [onScroll]);\n\n /**\n * Send a message to the embed's owner window.\n */\n const sendEmbedMessage = useCallback(\n (type, data = {}) => {\n data = { apprendly: true, id: props.embedId, type, ...data };\n\n if (window.opener) {\n window.opener.postMessage(data, window.location.origin);\n }\n },\n [props.embedId]\n );\n\n useEffect(() => {\n window.apprendly.initialize();\n }, []);\n\n const onMessageReceived = useCallback(\n (event) => {\n if (event.data?.type === \"embedInfo\" && !gameInfo) {\n log(LogLevel.debug, \"Received embed info\", event.data);\n dispatch(actions.setGameInfo(event.data.gameInfo));\n dispatch(actions.setPlayerId(event.data.playerId));\n setEmbedInfo(event.data);\n }\n },\n [gameInfo, dispatch]\n );\n\n useEffect(() => {\n if (!gameInfo && params.embedId && window.opener) {\n log(LogLevel.debug, \"Requesting game info\");\n\n window.opener.postMessage(\n {\n type: \"getEmbedInfo\",\n embedId: params.embedId,\n },\n window.location.origin\n );\n window.addEventListener(\"message\", onMessageReceived);\n } else if (!window.opener) {\n setErrorMessage(t(\"player:couldNotConnectServer\"));\n }\n }, [params.embedId, gameInfo, onMessageReceived, t]);\n\n useEffect(() => {\n if (gameInfo?.name) {\n document.title = gameInfo.name;\n }\n }, [gameInfo]);\n\n const quitGame = () => {\n sendEmbedMessage(\"quitGame\");\n window.close();\n };\n\n return (\n
\n {!supportsFullscreen() && (\n
\n

{t(\"player:swipeDown\")}

\n
\n )}\n {gameInfo && embedInfo && !errorMessage && (\n setSetupFullscreen(true) : undefined\n }\n />\n )}\n {errorMessage &&
{errorMessage}
}\n \n
\n );\n};\n\nexport default FullscreenPlayer;\n","import { useEffect } from \"react\";\nimport { useParams } from \"react-router-dom\";\n\nconst IFrame = () => {\n\n const params = useParams();\n\n useEffect(() => {\n\n const logEvents = (event) => {\n console.log(event);\n }\n\n window.addEventListener('message', logEvents)\n\n return () => {\n window.removeEventListener('message', logEvents)\n }\n })\n\n return (\n \n \n )\n}\n\nexport default IFrame;","import React from \"react\";\r\nimport { BrowserRouter as Router, Route, Switch } from \"react-router-dom\";\r\nimport { QueryParamProvider } from \"use-query-params\";\r\nimport { Demo } from \"./features/demo/Demo\";\r\nimport { Embed } from \"./features/embed/Embed\";\r\nimport FullscreenPlayer from \"./features/player/FullscreenPlayer\";\r\nimport IFrame from \"./features/player/IFrame\";\r\n\r\nfunction App() {\r\n return (\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n