import dashjs from 'modules/dashjs'; import Hls, { LevelLoadedData } from 'modules/hls.js'; import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlaybackState, PlaybackUpdateMessage, PlaylistContent, PlayMessage, SeekMessage, SetPlaylistItemMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets'; import { Player, PlayerType } from './Player'; import * as connectionMonitor from 'common/ConnectionMonitor'; import { supportedAudioTypes } from 'common/MimeTypes'; import { mediaItemFromPlayMessage, playMessageFromMediaItem } from 'common/UtilityFrontend'; import { toast, ToastIcon } from 'common/components/Toast'; import { targetPlayerCtrlStateUpdate, targetKeyDownEventListener, captionsBaseHeightCollapsed, captionsBaseHeightExpanded, captionsLineHeight } from 'src/player/Renderer'; const logger = window.targetAPI.logger; function formatDuration(duration: number) { if (isNaN(duration)) { return '00:00'; } const totalSeconds = Math.floor(duration); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = Math.floor(totalSeconds % 60); const paddedMinutes = String(minutes).padStart(2, '0'); const paddedSeconds = String(seconds).padStart(2, '0'); if (hours > 0) { return `${hours}:${paddedMinutes}:${paddedSeconds}`; } else { return `${paddedMinutes}:${paddedSeconds}`; } } function sendPlaybackUpdate(updateState: PlaybackState) { const updateMessage = new PlaybackUpdateMessage(Date.now(), updateState, player.getCurrentTime(), player.getDuration(), player.getPlaybackRate()); playbackState = updateState; if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) { lastPlayerUpdateGenerationTime = updateMessage.generationTime; window.targetAPI.sendPlaybackUpdate(updateMessage); } }; function onPlayerLoad(value: PlayMessage) { playerCtrlStateUpdate(PlayerControlEvent.Load); loadingSpinner.style.display = 'none'; if (player.getAutoplay()) { if (!supportedAudioTypes.find(v => v === value.container.toLocaleLowerCase())) { idleIcon.style.display = 'none'; idleBackground.style.display = 'none'; } else { idleIcon.style.display = 'block'; idleBackground.style.display = 'block'; } // Subtitles break when seeking post stream initialization for the DASH player. // Its currently done on player initialization. if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) { if (value.time) { player.setCurrentTime(value.time); } } if (value.speed) { player.setPlaybackRate(value.speed); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); } if (value.volume) { volumeChangeHandler(value.volume); } else { // Protocol v2 FCast PlayMessage does not contain volume field and could result in the receiver // getting out-of-sync with the sender on 1st playback. volumeChangeHandler(1.0); window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: 1.0 }); } playerCtrlStateUpdate(PlayerControlEvent.VolumeChange); playbackState = PlaybackState.Playing; logger.info('Media playback start:', cachedPlayMediaItem); window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem))); player.play(); } } // HTML elements const idleIcon = document.getElementById('title-icon'); const loadingSpinner = document.getElementById('loading-spinner'); const idleBackground = document.getElementById('idle-background'); const videoElement = document.getElementById("videoPlayer") as HTMLVideoElement; const videoCaptions = document.getElementById("videoCaptions") as HTMLDivElement; const playerControls = document.getElementById("controls"); const playerCtrlAction = document.getElementById("action"); const playerCtrlVolume = document.getElementById("volume"); const playerCtrlProgressBar = document.getElementById("progressBar"); const playerCtrlProgressBarBuffer = document.getElementById("progressBarBuffer"); const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress"); const playerCtrlProgressBarPosition = document.getElementById("progressBarPosition"); const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle"); const PlayerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea"); const playerCtrlVolumeBar = document.getElementById("volumeBar"); const playerCtrlVolumeBarProgress = document.getElementById("volumeBarProgress"); const playerCtrlVolumeBarHandle = document.getElementById("volumeBarHandle"); const playerCtrlVolumeBarInteractiveArea = document.getElementById("volumeBarInteractiveArea"); const playerCtrlLiveBadge = document.getElementById("liveBadge"); const playerCtrlPosition = document.getElementById("position"); const playerCtrlDurationSeparator = document.getElementById("durationSeparator"); const playerCtrlDuration = document.getElementById("duration"); const playerCtrlCaptions = document.getElementById("captions"); const playerCtrlSpeed = document.getElementById("speed"); const playerCtrlSpeedMenu = document.getElementById("speedMenu"); let playerCtrlSpeedMenuShown = false; const playbackRates = ["0.25", "0.50", "0.75", "1.00", "1.25", "1.50", "1.75", "2.00"]; const playbackUpdateInterval = 1.0; const playerVolumeUpdateInterval = 0.01; const livePositionDelta = 5.0; const livePositionWindow = livePositionDelta * 4; let player: Player; let playbackState: PlaybackState = PlaybackState.Idle; let playerPrevTime: number = 1; let playerPrevVolume: number = 1; let lastPlayerUpdateGenerationTime = 0; let isLive = false; let isLivePosition = false; let captionsBaseHeight = 0; let captionsContentHeight = 0; let cachedPlaylist: PlaylistContent = null; let cachedPlayMediaItem: MediaItem = null; let showDurationTimeout: number = null; let playlistIndex = 0; let isMediaItem = false; let playItemCached = false; function onPlay(_event, value: PlayMessage) { if (!playItemCached) { cachedPlayMediaItem = mediaItemFromPlayMessage(value); isMediaItem = false; } window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem))); logger.info('Media playback changed:', cachedPlayMediaItem); playItemCached = false; idleIcon.style.display = 'none'; loadingSpinner.style.display = 'block'; idleBackground.style.display = 'block'; if (player) { if ((player.getSource() === value.url) || (player.getSource() === value.content)) { if (value.time) { console.info('Skipped changing video URL because URL is the same. Discarding time and using current receiver time instead'); } return; } player.destroy(); } playbackState = PlaybackState.Idle; playerPrevTime = 0; lastPlayerUpdateGenerationTime = 0; isLive = false; isLivePosition = false; captionsBaseHeight = captionsBaseHeightExpanded; if ((value.url || value.content) && value.container && videoElement) { player = new Player(videoElement, value); logger.info(`Loaded ${PlayerType[player.playerType]} player`); if (value.container === 'application/dash+xml') { // Player event handlers player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, () => { mediaStartHandler(value); }); player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, () => { sendPlaybackUpdate(PlaybackState.Paused); playerCtrlStateUpdate(PlayerControlEvent.Pause); }); player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ENDED, () => { mediaEndHandler(); }); player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); if (Math.abs(player.dashPlayer.time() - playerPrevTime) >= playbackUpdateInterval) { sendPlaybackUpdate(playbackState); playerPrevTime = player.dashPlayer.time(); } }); player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_RATE_CHANGED, () => { sendPlaybackUpdate(playbackState); }); // Buffering UI update when paused player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PROGRESS, () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); }); player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_VOLUME_CHANGED, () => { const updateVolume = player.dashPlayer.isMuted() ? 0 : player.dashPlayer.getVolume(); playerCtrlStateUpdate(PlayerControlEvent.VolumeChange); if (Math.abs(updateVolume - playerPrevVolume) >= playerVolumeUpdateInterval) { window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume }); playerPrevVolume = updateVolume; } }); player.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (data) => { window.targetAPI.sendPlaybackError({ message: `DashJS ERROR: ${JSON.stringify(data)}` })}); player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (data) => { window.targetAPI.sendPlaybackError({ message: `DashJS PLAYBACK_ERROR: ${JSON.stringify(data)}` })}); player.dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { onPlayerLoad(value); }); player.dashPlayer.on(dashjs.MediaPlayer.events.CUE_ENTER, (e: any) => { const subtitle = document.createElement("p") subtitle.setAttribute("id", "subtitle-" + e.cueID) subtitle.textContent = e.text; videoCaptions.appendChild(subtitle); captionsContentHeight = subtitle.getBoundingClientRect().height - captionsLineHeight; const captionsHeight = captionsBaseHeight + captionsContentHeight; if (player.isCaptionsEnabled()) { videoCaptions.setAttribute("style", `display: block; bottom: ${captionsHeight}px;`); } else { videoCaptions.setAttribute("style", `display: none; bottom: ${captionsHeight}px;`); } }); player.dashPlayer.on(dashjs.MediaPlayer.events.CUE_EXIT, (e: any) => { document.getElementById("subtitle-" + e.cueID)?.remove(); }); player.dashPlayer.updateSettings({ // debug: { // logLevel: dashjs.LogLevel.LOG_LEVEL_INFO // }, streaming: { text: { dispatchForManualRendering: true } } }); } else if ((value.container === 'application/vnd.apple.mpegurl' || value.container === 'application/x-mpegURL') && !videoElement.canPlayType(value.container)) { player.hlsPlayer.on(Hls.Events.ERROR, (eventName, data) => { window.targetAPI.sendPlaybackError({ message: `HLS player error: ${JSON.stringify(data)}` }); }); player.hlsPlayer.on(Hls.Events.LEVEL_LOADED, (eventName, level: LevelLoadedData) => { isLive = level.details.live; isLivePosition = isLive ? true : false; // Event can fire after video load and play initialization if (isLive && playerCtrlLiveBadge.style.display === "none") { playerCtrlLiveBadge.style.display = "block"; playerCtrlPosition.style.display = "none"; playerCtrlDurationSeparator.style.display = "none"; playerCtrlDuration.style.display = "none"; } }); } // Player event handlers if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) { videoElement.onplay = () => { mediaStartHandler(value); }; videoElement.onpause = () => { sendPlaybackUpdate(PlaybackState.Paused); playerCtrlStateUpdate(PlayerControlEvent.Pause); }; videoElement.onended = () => { mediaEndHandler(); }; videoElement.ontimeupdate = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); if (Math.abs(videoElement.currentTime - playerPrevTime) >= playbackUpdateInterval) { sendPlaybackUpdate(playbackState); playerPrevTime = videoElement.currentTime; } }; // Buffering UI update when paused videoElement.onprogress = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); }; videoElement.onratechange = () => { sendPlaybackUpdate(playbackState); }; videoElement.onvolumechange = () => { const updateVolume = videoElement.muted ? 0 : videoElement.volume; playerCtrlStateUpdate(PlayerControlEvent.VolumeChange); if (Math.abs(updateVolume - playerPrevVolume) >= playerVolumeUpdateInterval) { window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume }); playerPrevVolume = updateVolume; } }; videoElement.onerror = (event: Event | string, source?: string, lineno?: number, colno?: number, error?: Error) => { logger.error("Player error", {source, lineno, colno, error}); }; videoElement.onloadedmetadata = (ev) => { if (videoElement.duration === Infinity) { isLive = true; isLivePosition = true; } else { isLive = false; isLivePosition = false; } onPlayerLoad(value); }; } player.setAutoPlay(true); player.load(); } if (isMediaItem && cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) { showDurationTimeout = window.setTimeout(mediaEndHandler, cachedPlayMediaItem.showDuration * 1000); } // Sender generated event handlers window.targetAPI.onPause(() => { player.pause(); }); window.targetAPI.onResume(() => { player.play(); }); window.targetAPI.onSeek((_event, value: SeekMessage) => { player.setCurrentTime(value.time); }); window.targetAPI.onSetVolume((_event, value: SetVolumeMessage) => { volumeChangeHandler(value.volume); }); window.targetAPI.onSetSpeed((_event, value: SetSpeedMessage) => { player.setPlaybackRate(value.speed); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); }); } function onPlayPlaylist(_event, value: PlaylistContent) { logger.info('Handle play playlist message', JSON.stringify(value)); cachedPlaylist = value; const offset = value.offset ? value.offset : 0; const volume = value.items[offset].volume ? value.items[offset].volume : value.volume; const speed = value.items[offset].speed ? value.items[offset].speed : value.speed; const playMessage = new PlayMessage( value.items[offset].container, value.items[offset].url, value.items[offset].content, value.items[offset].time, volume, speed, value.items[offset].headers, value.items[offset].metadata ); isMediaItem = true; cachedPlayMediaItem = value.items[offset]; playItemCached = true; window.targetAPI.sendPlayRequest(playMessage, playlistIndex); } window.targetAPI.onSetPlaylistItem((_event, value: SetPlaylistItemMessage) => { if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) { logger.info(`Setting playlist item to index ${value.itemIndex}`); playlistIndex = value.itemIndex; cachedPlayMediaItem = cachedPlaylist.items[playlistIndex]; playItemCached = true; window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex); if (showDurationTimeout) { window.clearTimeout(showDurationTimeout); showDurationTimeout = null; } } else { logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`); } }); connectionMonitor.setUiUpdateCallbacks({ onConnect: (connections: string[], initialUpdate: boolean = false) => { if (!initialUpdate) { toast('Device connected', ToastIcon.INFO); } }, onDisconnect: (connections: string[]) => { toast('Device disconnected. If you experience playback issues, please reconnect.', ToastIcon.INFO); }, }); window.targetAPI.onPlay(onPlay); window.targetAPI.onPlayPlaylist(onPlayPlaylist); let scrubbing = false; let volumeChanging = false; enum PlayerControlEvent { Load, Pause, Play, VolumeChange, TimeUpdate, UiFadeOut, UiFadeIn, SetCaptions, ToggleSpeedMenu, SetPlaybackRate, ToggleFullscreen, ExitFullscreen, } // UI update handlers function playerCtrlStateUpdate(event: PlayerControlEvent) { const handledCase = targetPlayerCtrlStateUpdate(event); if (handledCase) { return; } switch (event) { case PlayerControlEvent.Load: { playerCtrlProgressBarBuffer.setAttribute("style", "width: 0px"); playerCtrlProgressBarProgress.setAttribute("style", "width: 0px"); playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetLeft}px`); const volume = Math.round(player.getVolume() * playerCtrlVolumeBar.offsetWidth); playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`); playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume + 8}px`); if (isLive) { playerCtrlLiveBadge.setAttribute("style", "display: block"); playerCtrlPosition.setAttribute("style", "display: none"); playerCtrlDurationSeparator.setAttribute("style", "display: none"); playerCtrlDuration.setAttribute("style", "display: none"); } else { playerCtrlLiveBadge.setAttribute("style", "display: none"); playerCtrlPosition.setAttribute("style", "display: block"); playerCtrlDurationSeparator.setAttribute("style", "display: block"); playerCtrlDuration.setAttribute("style", "display: block"); playerCtrlPosition.textContent = formatDuration(player.getCurrentTime()); playerCtrlDuration.innerHTML = formatDuration(player.getDuration()); } if (player.isCaptionsSupported()) { playerCtrlCaptions.setAttribute("style", "display: block"); videoCaptions.setAttribute("style", "display: block"); } else { playerCtrlCaptions.setAttribute("style", "display: none"); videoCaptions.setAttribute("style", "display: none"); player.enableCaptions(false); } playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); break; } case PlayerControlEvent.Pause: playerCtrlAction.setAttribute("class", "play iconSize"); stopUiHideTimer(); break; case PlayerControlEvent.Play: playerCtrlAction.setAttribute("class", "pause iconSize"); startUiHideTimer(); break; case PlayerControlEvent.VolumeChange: { // logger.info(`VolumeChange: isMute ${player?.isMuted()}, volume: ${player?.getVolume()}`); const volume = Math.round(player?.getVolume() * playerCtrlVolumeBar.offsetWidth); if (player?.isMuted()) { playerCtrlVolume.setAttribute("class", "mute iconSize"); playerCtrlVolumeBarProgress.setAttribute("style", `width: 0px`); playerCtrlVolumeBarHandle.setAttribute("style", `left: 0px`); } else if (player?.getVolume() >= 0.5) { playerCtrlVolume.setAttribute("class", "volume_high iconSize"); playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`); playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume}px`); } else { playerCtrlVolume.setAttribute("class", "volume_low iconSize"); playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`); playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume}px`); } break; } case PlayerControlEvent.TimeUpdate: { // logger.info(`TimeUpdate: Position: ${player.getCurrentTime()}, Duration: ${player.getDuration()}`); if (isLive) { if (isLivePosition && player.getDuration() - player.getCurrentTime() > livePositionWindow) { isLivePosition = false; playerCtrlLiveBadge.setAttribute("style", `background-color: #595959`); } else if (!isLivePosition && player.getDuration() - player.getCurrentTime() <= livePositionWindow) { isLivePosition = true; playerCtrlLiveBadge.setAttribute("style", `background-color: red`); } } if (isLivePosition) { playerCtrlProgressBarProgress.setAttribute("style", `width: ${playerCtrlProgressBar.offsetWidth}px`); playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetWidth + playerCtrlProgressBar.offsetLeft}px`); } else { const buffer = Math.round((player.getBufferLength() / player.getDuration()) * playerCtrlProgressBar.offsetWidth); const progress = Math.round((player.getCurrentTime() / player.getDuration()) * playerCtrlProgressBar.offsetWidth); const handle = progress + playerCtrlProgressBar.offsetLeft; playerCtrlProgressBarBuffer.setAttribute("style", `width: ${buffer}px`); playerCtrlProgressBarProgress.setAttribute("style", `width: ${progress}px`); playerCtrlProgressBarHandle.setAttribute("style", `left: ${handle}px`); playerCtrlPosition.textContent = formatDuration(player.getCurrentTime()); } break; } case PlayerControlEvent.UiFadeOut: { document.body.style.cursor = "none"; playerControls.setAttribute("style", "opacity: 0"); captionsBaseHeight = captionsBaseHeightCollapsed; const captionsHeight = captionsBaseHeight + captionsContentHeight; if (player?.isCaptionsEnabled()) { videoCaptions.setAttribute("style", `display: block; transition: bottom 0.2s ease-in-out; bottom: ${captionsHeight}px;`); } else { videoCaptions.setAttribute("style", `display: none; bottom: ${captionsHeight}px;`); } break; } case PlayerControlEvent.UiFadeIn: { document.body.style.cursor = "default"; playerControls.setAttribute("style", "opacity: 1"); captionsBaseHeight = captionsBaseHeightExpanded; const captionsHeight = captionsBaseHeight + captionsContentHeight; if (player?.isCaptionsEnabled()) { videoCaptions.setAttribute("style", `display: block; transition: bottom 0.2s ease-in-out; bottom: ${captionsHeight}px;`); } else { videoCaptions.setAttribute("style", `display: none; bottom: ${captionsHeight}px;`); } break; } case PlayerControlEvent.SetCaptions: if (player?.isCaptionsEnabled()) { playerCtrlCaptions.setAttribute("class", "captions_on iconSize"); videoCaptions.setAttribute("style", "display: block"); } else { playerCtrlCaptions.setAttribute("class", "captions_off iconSize"); videoCaptions.setAttribute("style", "display: none"); } break; case PlayerControlEvent.ToggleSpeedMenu: { if (playerCtrlSpeedMenuShown) { playerCtrlSpeedMenu.setAttribute("style", "display: none"); } else { playerCtrlSpeedMenu.setAttribute("style", "display: block"); } playerCtrlSpeedMenuShown = !playerCtrlSpeedMenuShown; break; } case PlayerControlEvent.SetPlaybackRate: { const rate = player?.getPlaybackRate().toFixed(2); const entryElement = document.getElementById(`speedMenuEntry_${rate}_enabled`); playbackRates.forEach(r => { const entry = document.getElementById(`speedMenuEntry_${r}_enabled`); entry.setAttribute("style", "opacity: 0"); }); // Ignore updating GUI for custom rates if (entryElement !== null) { entryElement.setAttribute("style", "opacity: 1"); } break; } default: break; } } function scrubbingMouseUIHandler(e: MouseEvent) { const progressBarOffset = e.offsetX - playerCtrlProgressBar.offsetLeft; const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBar.offsetLeft * 2); let time = isLive ? Math.round((1 - (progressBarOffset / progressBarWidth)) * player?.getDuration()) : Math.round((progressBarOffset / progressBarWidth) * player?.getDuration()); time = Math.min(player?.getDuration(), Math.max(0.0, time)); if (scrubbing && isLive && e.buttons === 1) { isLivePosition = false; playerCtrlLiveBadge.setAttribute("style", `background-color: #595959`); } const livePrefix = isLive && Math.floor(time) !== 0 ? "-" : ""; playerCtrlProgressBarPosition.textContent = isLive ? `${livePrefix}${formatDuration(time)}` : formatDuration(time); let offset = e.offsetX - (playerCtrlProgressBarPosition.offsetWidth / 2); offset = Math.min(PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBarPosition.offsetWidth / 1), Math.max(8, offset)); playerCtrlProgressBarPosition.setAttribute("style", `display: block; left: ${offset}px`); } // Receiver generated event handlers playerCtrlAction.onclick = () => { if (player?.isPaused()) { player?.play(); } else { player?.pause(); } }; playerCtrlVolume.onclick = () => { player?.setMute(!player?.isMuted()); }; PlayerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) }; PlayerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; }; PlayerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => { if (e.buttons === 0) { volumeChanging = false; } scrubbingMouseUIHandler(e); }; PlayerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); }; PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) }; function scrubbingMouseHandler(e: MouseEvent) { const progressBarOffset = e.offsetX - playerCtrlProgressBar.offsetLeft; const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBar.offsetLeft * 2); let time = Math.round((progressBarOffset / progressBarWidth) * player?.getDuration()); time = Math.min(player?.getDuration(), Math.max(0.0, time)); if (scrubbing && e.buttons === 1) { player?.setCurrentTime(time); } scrubbingMouseUIHandler(e); } playerCtrlVolumeBarInteractiveArea.onmousedown = (e: MouseEvent) => { volumeChanging = true; volumeChangeMouseHandler(e) }; playerCtrlVolumeBarInteractiveArea.onmouseup = () => { volumeChanging = false; }; playerCtrlVolumeBarInteractiveArea.onmouseenter = (e: MouseEvent) => { if (e.buttons === 0) { scrubbing = false; } }; playerCtrlVolumeBarInteractiveArea.onmousemove = (e: MouseEvent) => { volumeChangeMouseHandler(e) }; playerCtrlVolumeBarInteractiveArea.onwheel = (e: WheelEvent) => { const delta = -e.deltaY; if (delta > 0 ) { volumeChangeHandler(Math.min(player?.getVolume() + volumeIncrement, 1)); } else if (delta < 0) { volumeChangeHandler(Math.max(player?.getVolume() - volumeIncrement, 0)); } }; function volumeChangeMouseHandler(e: MouseEvent) { if (volumeChanging && e.buttons === 1) { const volumeBarOffsetX = e.offsetX - playerCtrlVolumeBar.offsetLeft; const volumeBarWidth = playerCtrlVolumeBarInteractiveArea.offsetWidth - (playerCtrlVolumeBar.offsetLeft * 2); const volume = volumeBarOffsetX / volumeBarWidth; volumeChangeHandler(volume); } } function volumeChangeHandler(volume: number) { if (!player?.isMuted() && volume <= 0) { player?.setMute(true); } else if (player?.isMuted() && volume > 0) { player?.setMute(false); } player?.setVolume(volume); } playerCtrlLiveBadge.onclick = () => { setLivePosition(); }; function setLivePosition() { if (!isLivePosition) { isLivePosition = true; player?.setCurrentTime(player?.getDuration() - livePositionDelta); playerCtrlLiveBadge.setAttribute("style", `background-color: red`); if (player?.isPaused()) { player?.play(); } } } playerCtrlCaptions.onclick = () => { player?.enableCaptions(!player?.isCaptionsEnabled()); playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); }; playerCtrlSpeed.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); }; playbackRates.forEach(r => { const entry = document.getElementById(`speedMenuEntry_${r}`); entry.onclick = () => { player?.setPlaybackRate(parseFloat(r)); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); }; }); function videoClickedHandler() { if (!playerCtrlSpeedMenuShown) { if (player?.isPaused()) { player?.play(); } else { player?.pause(); } } } videoElement.onclick = () => { videoClickedHandler(); }; idleBackground.onclick = () => { videoClickedHandler(); }; idleIcon.onclick = () => { videoClickedHandler(); }; function mediaStartHandler(message: PlayMessage) { if (playbackState === PlaybackState.Idle) { logger.info('Media playback start:', cachedPlayMediaItem); window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem))); if (!supportedAudioTypes.find(v => v === message.container.toLocaleLowerCase())) { idleIcon.style.display = 'none'; idleBackground.style.display = 'none'; } else { idleIcon.style.display = 'block'; idleBackground.style.display = 'block'; } } sendPlaybackUpdate(PlaybackState.Playing); playerCtrlStateUpdate(PlayerControlEvent.Play); } function mediaEndHandler() { if (showDurationTimeout) { window.clearTimeout(showDurationTimeout); showDurationTimeout = null; } if (isMediaItem) { playlistIndex++; if (playlistIndex < cachedPlaylist.items.length) { cachedPlayMediaItem = cachedPlaylist.items[playlistIndex]; playItemCached = true; window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex); } else { logger.info('End of playlist:', cachedPlayMediaItem); sendPlaybackUpdate(PlaybackState.Idle); window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem))); idleIcon.style.display = 'block'; idleBackground.style.display = 'block'; player.setAutoPlay(false); player.stop(); } } else { logger.info('Media playback ended:', cachedPlayMediaItem); sendPlaybackUpdate(PlaybackState.Idle); window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem))); idleIcon.style.display = 'block'; idleBackground.style.display = 'block'; player.setAutoPlay(false); player.stop(); } } // Component hiding let uiHideTimer = null; let uiVisible = true; function startUiHideTimer() { if (uiHideTimer === null) { uiHideTimer = window.setTimeout(() => { uiHideTimer = null; uiVisible = false; playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut); }, 3000); } } function stopUiHideTimer() { if (uiHideTimer) { window.clearTimeout(uiHideTimer); uiHideTimer = null; } if (!uiVisible) { uiVisible = true; playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn); } } document.onmouseout = () => { if (uiHideTimer) { window.clearTimeout(uiHideTimer); uiHideTimer = null; } uiVisible = false; playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut); } document.onmousemove = () => { stopUiHideTimer(); if (player && !player.isPaused()) { startUiHideTimer(); } }; window.onresize = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); }; // Listener for hiding speed menu when clicking outside element document.addEventListener('click', (event: MouseEvent) => { const node = event.target as Node; if (playerCtrlSpeedMenuShown && !playerCtrlSpeed.contains(node) && !playerCtrlSpeedMenu.contains(node)){ playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); } }); // Add the keydown event listener to the document const skipInterval = 10; const volumeIncrement = 0.1; function keyDownEventListener(event: KeyboardEvent) { // logger.info("KeyDown", event); let handledCase = targetKeyDownEventListener(event); if (!handledCase) { switch (event.code) { case 'ArrowLeft': skipBack(); event.preventDefault(); handledCase = true; break; case 'ArrowRight': skipForward(); event.preventDefault(); handledCase = true; break; case "Home": player?.setCurrentTime(0); event.preventDefault(); handledCase = true; break; case "End": if (isLive) { setLivePosition(); } else { player?.setCurrentTime(player?.getDuration()); } event.preventDefault(); handledCase = true; break; case 'KeyK': case 'Space': case 'Enter': // Play/pause toggle if (player?.isPaused()) { player?.play(); } else { player?.pause(); } event.preventDefault(); handledCase = true; break; case 'KeyM': // Mute toggle player?.setMute(!player?.isMuted()); handledCase = true; break; case 'ArrowUp': // Volume up volumeChangeHandler(Math.min(player?.getVolume() + volumeIncrement, 1)); handledCase = true; break; case 'ArrowDown': // Volume down volumeChangeHandler(Math.max(player?.getVolume() - volumeIncrement, 0)); handledCase = true; break; default: break; } } if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) { window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase))); } } function skipBack() { player?.setCurrentTime(Math.max(player?.getCurrentTime() - skipInterval, 0)); } function skipForward() { if (!isLivePosition) { player?.setCurrentTime(Math.min(player?.getCurrentTime() + skipInterval, player?.getDuration())); } } document.addEventListener('keydown', keyDownEventListener); document.addEventListener('keyup', (event: KeyboardEvent) => { if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) { window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false))); } }); export { PlayerControlEvent, videoElement, videoCaptions, playerCtrlProgressBar, playerCtrlProgressBarBuffer, playerCtrlProgressBarProgress, playerCtrlProgressBarHandle, playerCtrlVolumeBar, playerCtrlVolumeBarProgress, playerCtrlVolumeBarHandle, playerCtrlLiveBadge, playerCtrlPosition, playerCtrlDuration, playerCtrlCaptions, player, isLive, captionsBaseHeight, captionsLineHeight, onPlay, playerCtrlStateUpdate, formatDuration, skipBack, skipForward, };