mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
1021 lines
40 KiB
TypeScript
1021 lines
40 KiB
TypeScript
import dashjs from 'modules/dashjs';
|
|
import Hls, { LevelLoadedData } from 'modules/hls.js';
|
|
import { EventMessage, EventType, GenericMediaMetadata, KeyEvent, MediaItem, MediaItemEvent, MetadataType, 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, Timer } 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;
|
|
|
|
// HTML elements
|
|
const idleIcon = document.getElementById('title-icon');
|
|
const loadingSpinner = document.getElementById('loading-spinner');
|
|
const idleBackground = document.getElementById('idle-background');
|
|
const thumbnailImage = document.getElementById('thumbnailImage') as HTMLImageElement;
|
|
const videoElement = document.getElementById("videoPlayer") as HTMLVideoElement;
|
|
const videoCaptions = document.getElementById("videoCaptions") as HTMLDivElement;
|
|
const mediaTitle = document.getElementById("mediaTitle");
|
|
|
|
const playerControls = document.getElementById("controls");
|
|
|
|
const playerCtrlPlayPrevious = document.getElementById("playPrevious");
|
|
const playerCtrlAction = document.getElementById("action");
|
|
const playerCtrlPlayNext = document.getElementById("playNext");
|
|
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 playlistIndex = 0;
|
|
let isMediaItem = false;
|
|
let playItemCached = false;
|
|
|
|
let uiHideTimer = new Timer(() => {
|
|
uiVisible = false;
|
|
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
|
}, 3000);
|
|
|
|
let showDurationTimer = new Timer(mediaEndHandler, 0, false);
|
|
let mediaTitleShowTimer = new Timer(() => {
|
|
mediaTitle.style.display = 'none';
|
|
}, 5000);
|
|
|
|
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);
|
|
|
|
if (player.getAutoplay()) {
|
|
// 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 !== null && value.volume >= 0) {
|
|
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);
|
|
|
|
mediaPlayHandler(value);
|
|
player.play();
|
|
}
|
|
else {
|
|
setIdleScreenVisible(true, false, value);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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();
|
|
player = null;
|
|
}
|
|
|
|
setIdleScreenVisible(true, true);
|
|
sendPlaybackUpdate(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, () => { mediaPlayHandler(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) => {
|
|
toast('Media playback error, please close the player and reconnect sender devices if you experience issues', ToastIcon.WARNING);
|
|
logger.error('Dash player error:', data);
|
|
|
|
window.targetAPI.sendPlaybackError({
|
|
message: JSON.stringify(data)
|
|
});
|
|
});
|
|
|
|
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (data) => {
|
|
toast('Media playback error, please close the player and reconnect sender devices if you experience issues', ToastIcon.WARNING);
|
|
logger.error('Dash player playback error:', data);
|
|
|
|
window.targetAPI.sendPlaybackError({
|
|
message: 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) => {
|
|
if (data.fatal) {
|
|
toast('Media playback error, please close the player and reconnect sender devices if you experience issues', ToastIcon.WARNING);
|
|
logger.error('HLS player error:', data);
|
|
|
|
window.targetAPI.sendPlaybackError({
|
|
message: JSON.stringify(data)
|
|
});
|
|
|
|
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
player.hlsPlayer.recoverMediaError();
|
|
}
|
|
}
|
|
else {
|
|
logger.warn('HLS non-fatal error:', 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 = () => { mediaPlayHandler(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;
|
|
}
|
|
};
|
|
|
|
// parameters seem to always be undefined...
|
|
// videoElement.onerror = (event: Event | string, source?: string, lineno?: number, colno?: number, error?: Error) => {
|
|
videoElement.onerror = () => {
|
|
toast('Media playback error, please close the player and reconnect sender devices if you experience issues', ToastIcon.WARNING);
|
|
logger.error('Html player error:', { playMessage: value, videoError: videoElement.error });
|
|
|
|
window.targetAPI.sendPlaybackError({
|
|
message: JSON.stringify({ playMessage: value, videoError: videoElement.error })
|
|
});
|
|
};
|
|
|
|
videoElement.onloadedmetadata = (ev) => {
|
|
if (videoElement.duration === Infinity) {
|
|
isLive = true;
|
|
isLivePosition = true;
|
|
}
|
|
else {
|
|
isLive = false;
|
|
isLivePosition = false;
|
|
}
|
|
|
|
onPlayerLoad(value);
|
|
};
|
|
}
|
|
|
|
player.setAutoPlay(true);
|
|
player.load();
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
function setPlaylistItem(index: number) {
|
|
if (index >= 0 && index < cachedPlaylist.items.length) {
|
|
logger.info(`Setting playlist item to index ${index}`);
|
|
playlistIndex = index;
|
|
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
|
|
playItemCached = true;
|
|
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
|
|
showDurationTimer.stop();
|
|
}
|
|
else {
|
|
logger.warn(`Playlist index out of bounds ${index}, 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);
|
|
window.targetAPI.onSetPlaylistItem((_event, value: SetPlaylistItemMessage) => { setPlaylistItem(value.itemIndex); });
|
|
|
|
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: {
|
|
if (isMediaItem) {
|
|
playerCtrlPlayPrevious.style.display = 'block';
|
|
playerCtrlPlayNext.style.display = 'block';
|
|
}
|
|
else {
|
|
playerCtrlPlayPrevious.style.display = 'none';
|
|
playerCtrlPlayNext.style.display = 'none';
|
|
}
|
|
|
|
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.style.display = 'block';
|
|
playerCtrlPosition.style.display = 'none';
|
|
playerCtrlDurationSeparator.style.display = 'none';
|
|
playerCtrlDuration.style.display = 'none';
|
|
}
|
|
else {
|
|
playerCtrlLiveBadge.style.display = 'none';
|
|
playerCtrlPosition.style.display = 'block';
|
|
playerCtrlDurationSeparator.style.display = 'block';
|
|
playerCtrlDuration.style.display = 'block';
|
|
|
|
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
|
|
playerCtrlDuration.innerHTML = formatDuration(player.getDuration());
|
|
}
|
|
|
|
if (player.isCaptionsSupported()) {
|
|
playerCtrlCaptions.style.display = 'block';
|
|
videoCaptions.style.display = 'block';
|
|
}
|
|
else {
|
|
playerCtrlCaptions.style.display = 'none';
|
|
videoCaptions.style.display = 'none';
|
|
player.enableCaptions(false);
|
|
}
|
|
playerCtrlStateUpdate(PlayerControlEvent.SetCaptions);
|
|
|
|
if (supportedAudioTypes.find(v => v === cachedPlayMediaItem.container.toLocaleLowerCase())) {
|
|
if (cachedPlayMediaItem.metadata && cachedPlayMediaItem.metadata?.type === MetadataType.Generic) {
|
|
const metadata = cachedPlayMediaItem.metadata as GenericMediaMetadata;
|
|
|
|
if (metadata.title) {
|
|
mediaTitle.innerHTML = metadata.title;
|
|
|
|
captionsContentHeight = mediaTitle.getBoundingClientRect().height - captionsLineHeight;
|
|
const captionsHeight = captionsBaseHeightExpanded + captionsContentHeight;
|
|
mediaTitle.setAttribute("style", `display: block; bottom: ${captionsHeight}px;`);
|
|
mediaTitleShowTimer.start();
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case PlayerControlEvent.Pause:
|
|
playerCtrlAction.setAttribute("class", "play iconSize");
|
|
stopUiHideTimer();
|
|
showDurationTimer.pause();
|
|
break;
|
|
|
|
case PlayerControlEvent.Play:
|
|
playerCtrlAction.setAttribute("class", "pause iconSize");
|
|
uiHideTimer.start();
|
|
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.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.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.style.display = 'block';
|
|
} else {
|
|
playerCtrlCaptions.setAttribute("class", "captions_off iconSize");
|
|
videoCaptions.style.display = 'none';
|
|
}
|
|
|
|
break;
|
|
|
|
case PlayerControlEvent.ToggleSpeedMenu: {
|
|
if (playerCtrlSpeedMenuShown) {
|
|
playerCtrlSpeedMenu.style.display = 'none';
|
|
} else {
|
|
playerCtrlSpeedMenu.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.style.opacity = '0';
|
|
});
|
|
|
|
// Ignore updating GUI for custom rates
|
|
if (entryElement !== null) {
|
|
entryElement.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();
|
|
}
|
|
};
|
|
|
|
playerCtrlPlayPrevious.onclick = () => { setPlaylistItem(playlistIndex - 1); }
|
|
playerCtrlPlayNext.onclick = () => { setPlaylistItem(playlistIndex + 1); }
|
|
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(); };
|
|
thumbnailImage.onclick = () => { videoClickedHandler(); };
|
|
idleIcon.onclick = () => { videoClickedHandler(); };
|
|
|
|
function setIdleScreenVisible(visible: boolean, loading: boolean = false, message?: PlayMessage) {
|
|
if (visible) {
|
|
idleBackground.style.display = 'block';
|
|
thumbnailImage.style.display = 'none';
|
|
|
|
if (loading) {
|
|
idleIcon.style.display = 'none';
|
|
loadingSpinner.style.display = 'block';
|
|
}
|
|
else {
|
|
idleIcon.style.display = 'block';
|
|
loadingSpinner.style.display = 'none';
|
|
}
|
|
}
|
|
else {
|
|
if (!supportedAudioTypes.find(v => v === message.container.toLocaleLowerCase())) {
|
|
idleIcon.style.display = 'none';
|
|
idleBackground.style.display = 'none';
|
|
thumbnailImage.style.display = 'none';
|
|
}
|
|
else {
|
|
let displayThumbnail = false;
|
|
if (message?.metadata?.type === MetadataType.Generic) {
|
|
const metadata = message.metadata as GenericMediaMetadata;
|
|
displayThumbnail = metadata.thumbnailUrl ? true : false;
|
|
thumbnailImage.src = metadata.thumbnailUrl;
|
|
}
|
|
|
|
if (displayThumbnail) {
|
|
idleIcon.style.display = 'none';
|
|
idleBackground.style.display = 'none';
|
|
thumbnailImage.style.display = 'block';
|
|
}
|
|
else {
|
|
idleIcon.style.display = 'block';
|
|
idleBackground.style.display = 'block';
|
|
thumbnailImage.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
loadingSpinner.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function mediaPlayHandler(message: PlayMessage) {
|
|
if (playbackState === PlaybackState.Idle) {
|
|
logger.info('Media playback start:', cachedPlayMediaItem);
|
|
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem)));
|
|
setIdleScreenVisible(false, false, message);
|
|
|
|
if (isMediaItem && cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
|
|
showDurationTimer.start(cachedPlayMediaItem.showDuration * 1000);
|
|
}
|
|
}
|
|
else {
|
|
showDurationTimer.resume();
|
|
}
|
|
|
|
sendPlaybackUpdate(PlaybackState.Playing);
|
|
playerCtrlStateUpdate(PlayerControlEvent.Play);
|
|
}
|
|
|
|
function mediaEndHandler() {
|
|
showDurationTimer.stop();
|
|
|
|
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)));
|
|
|
|
setIdleScreenVisible(true);
|
|
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)));
|
|
|
|
setIdleScreenVisible(true);
|
|
player.setAutoPlay(false);
|
|
player.stop();
|
|
}
|
|
}
|
|
|
|
// Component hiding
|
|
let uiVisible = true;
|
|
|
|
function stopUiHideTimer() {
|
|
uiHideTimer.stop();
|
|
|
|
if (!uiVisible) {
|
|
uiVisible = true;
|
|
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
|
|
}
|
|
}
|
|
|
|
document.onmouseout = () => {
|
|
uiHideTimer.stop();
|
|
uiVisible = false;
|
|
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
|
}
|
|
|
|
document.onmousemove = () => {
|
|
stopUiHideTimer();
|
|
|
|
if (player && !player.isPaused()) {
|
|
uiHideTimer.start();
|
|
}
|
|
};
|
|
|
|
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,
|
|
idleBackground,
|
|
thumbnailImage,
|
|
idleIcon,
|
|
videoElement,
|
|
videoCaptions,
|
|
playerCtrlProgressBar,
|
|
playerCtrlProgressBarBuffer,
|
|
playerCtrlProgressBarProgress,
|
|
playerCtrlProgressBarHandle,
|
|
playerCtrlVolumeBar,
|
|
playerCtrlVolumeBarProgress,
|
|
playerCtrlVolumeBarHandle,
|
|
playerCtrlLiveBadge,
|
|
playerCtrlPosition,
|
|
playerCtrlDuration,
|
|
playerCtrlCaptions,
|
|
player,
|
|
isLive,
|
|
captionsBaseHeight,
|
|
captionsLineHeight,
|
|
onPlay,
|
|
playerCtrlStateUpdate,
|
|
formatDuration,
|
|
skipBack,
|
|
skipForward,
|
|
};
|