1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Improved livestream support

This commit is contained in:
Michael Hollister 2024-11-05 01:07:07 -06:00
parent ad9763614c
commit 00ec611ccf
3 changed files with 109 additions and 21 deletions

View file

@ -1,5 +1,5 @@
import dashjs from 'dashjs'; import dashjs from 'dashjs';
import Hls from 'hls.js'; import Hls, { LevelLoadedData } from 'hls.js';
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from '../Packets'; import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from '../Packets';
import { Player, PlayerType } from './Player'; import { Player, PlayerType } from './Player';
@ -23,7 +23,6 @@ function sendPlaybackUpdate(updateState: number) {
const updateMessage = new PlaybackUpdateMessage(Date.now(), player.getCurrentTime(), player.getDuration(), updateState, player.getPlaybackRate()); const updateMessage = new PlaybackUpdateMessage(Date.now(), player.getCurrentTime(), player.getDuration(), updateState, player.getPlaybackRate());
if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) { if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) {
console.log(`Sending playback update: ${JSON.stringify(updateMessage)}`);
lastPlayerUpdateGenerationTime = updateMessage.generationTime; lastPlayerUpdateGenerationTime = updateMessage.generationTime;
window.electronAPI.sendPlaybackUpdate(updateMessage); window.electronAPI.sendPlaybackUpdate(updateMessage);
} }
@ -68,6 +67,7 @@ const playerCtrlVolume = document.getElementById("volume");
const playerCtrlProgressBar = document.getElementById("progressBar"); const playerCtrlProgressBar = document.getElementById("progressBar");
const playerCtrlProgressBarBuffer = document.getElementById("progressBarBuffer"); const playerCtrlProgressBarBuffer = document.getElementById("progressBarBuffer");
const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress"); const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress");
const playerCtrlProgressBarPosition = document.getElementById("progressBarPosition");
const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle"); const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle");
const PlayerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea"); const PlayerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea");
@ -90,9 +90,14 @@ let playerCtrlSpeedMenuShown = false;
const playbackRates = ["0.25", "0.50", "0.75", "1.00", "1.25", "1.50", "1.75", "2.00"]; const playbackRates = ["0.25", "0.50", "0.75", "1.00", "1.25", "1.50", "1.75", "2.00"];
const playbackUpdateInterval = 1.0; const playbackUpdateInterval = 1.0;
const livePositionDelta = 5.0;
const livePositionWindow = livePositionDelta * 4;
let player: Player; let player: Player;
let playerPrevTime: number = 0; let playerPrevTime: number = 0;
let lastPlayerUpdateGenerationTime = 0; let lastPlayerUpdateGenerationTime = 0;
let isLive = false;
let isLivePosition = false;
window.electronAPI.onPlay((_event, value: PlayMessage) => { window.electronAPI.onPlay((_event, value: PlayMessage) => {
console.log("Handle play message renderer", value); console.log("Handle play message renderer", value);
@ -215,12 +220,17 @@ window.electronAPI.onPlay((_event, value: PlayMessage) => {
const hlsPlayer = new Hls(config); const hlsPlayer = new Hls(config);
hlsPlayer.on(Hls.Events.ERROR, function(eventName, data) { hlsPlayer.on(Hls.Events.ERROR, (eventName, data) => {
window.electronAPI.sendPlaybackError({ window.electronAPI.sendPlaybackError({
message: `HLS player error: ${JSON.stringify(data)}` message: `HLS player error: ${JSON.stringify(data)}`
}); });
}); });
hlsPlayer.on(Hls.Events.LEVEL_LOADED, (eventName, level: LevelLoadedData) => {
isLive = level.details.live;
isLivePosition = isLive ? true : false;
});
player = new Player(PlayerType.Hls, videoElement, hlsPlayer); player = new Player(PlayerType.Hls, videoElement, hlsPlayer);
// value.url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8?ref=developerinsider.co"; // value.url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8?ref=developerinsider.co";
@ -304,8 +314,17 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`); playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume + 8}px`); playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume + 8}px`);
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime()); if (isLive) {
playerCtrlDuration.innerHTML = `/&nbsp&nbsp${formatDuration(player.getDuration())}`; playerCtrlLiveBadge.setAttribute("style", "display: block");
playerCtrlPosition.setAttribute("style", "display: none");
playerCtrlDuration.setAttribute("style", "display: none");
}
else {
playerCtrlLiveBadge.setAttribute("style", "display: none");
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
playerCtrlDuration.innerHTML = `/&nbsp&nbsp${formatDuration(player.getDuration())}`;
}
playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); playerCtrlStateUpdate(PlayerControlEvent.SetCaptions);
break; break;
@ -326,6 +345,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
case PlayerControlEvent.ToggleMute: case PlayerControlEvent.ToggleMute:
player.setMute(!player.isMuted()); player.setMute(!player.isMuted());
window.electronAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: 0 });
// fallthrough // fallthrough
case PlayerControlEvent.VolumeChange: { case PlayerControlEvent.VolumeChange: {
@ -350,15 +370,33 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
} }
case PlayerControlEvent.TimeUpdate: { case PlayerControlEvent.TimeUpdate: {
const buffer = Math.round((player.getBufferLength() / player.getDuration()) * playerCtrlProgressBar.offsetWidth); if (isLive) {
const progress = Math.round((player.getCurrentTime() / player.getDuration()) * playerCtrlProgressBar.offsetWidth); if (isLivePosition && player.getDuration() - player.getCurrentTime() > livePositionWindow) {
const handle = progress + playerCtrlProgressBar.offsetLeft; isLivePosition = false;
playerCtrlLiveBadge.setAttribute("style", `background-color: #595959`);
}
else if (!isLivePosition && player.getDuration() - player.getCurrentTime() <= livePositionWindow) {
isLivePosition = true;
playerCtrlLiveBadge.setAttribute("style", `background-color: red`);
}
}
playerCtrlProgressBarBuffer.setAttribute("style", `width: ${buffer}px`); if (isLivePosition) {
playerCtrlProgressBarProgress.setAttribute("style", `width: ${progress}px`); playerCtrlProgressBarProgress.setAttribute("style", `width: ${playerCtrlProgressBar.offsetWidth}px`);
playerCtrlProgressBarHandle.setAttribute("style", `left: ${handle}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());
}
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
break; break;
} }
@ -373,7 +411,6 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
break; break;
case PlayerControlEvent.SetCaptions: case PlayerControlEvent.SetCaptions:
console.log(player.isCaptionsEnabled());
if (player.isCaptionsEnabled()) { if (player.isCaptionsEnabled()) {
playerCtrlCaptions.setAttribute("class", "captions_on"); playerCtrlCaptions.setAttribute("class", "captions_on");
videoCaptions.setAttribute("style", "display: block"); videoCaptions.setAttribute("style", "display: block");
@ -453,18 +490,42 @@ PlayerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
if (e.buttons === 0) { if (e.buttons === 0) {
volumeChanging = false; volumeChanging = false;
} }
scrubbingMouseUIHandler(e);
}; };
PlayerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); };
PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) }; PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) };
function scrubbingMouseHandler(e: MouseEvent) { function scrubbingMouseHandler(e: MouseEvent) {
if (scrubbing && e.buttons === 1) { const progressBarOffset = e.offsetX - 8;
const progressBarOffset = e.offsetX - 8; const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16;
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16; let time = Math.round((progressBarOffset / progressBarWidth) * player.getDuration());
const time = Math.round((progressBarOffset / progressBarWidth) * player.getDuration()); time = Math.min(player.getDuration(), Math.max(0.0, time));
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); if (scrubbing && e.buttons === 1) {
player.setCurrentTime(time); player.setCurrentTime(time);
} }
scrubbingMouseUIHandler(e);
}
function scrubbingMouseUIHandler(e: MouseEvent) {
const progressBarOffset = e.offsetX - 8;
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16;
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`);
} }
playerCtrlVolumeBarInteractiveArea.onmousedown = (e: MouseEvent) => { volumeChanging = true; volumeChangeMouseHandler(e) }; playerCtrlVolumeBarInteractiveArea.onmousedown = (e: MouseEvent) => { volumeChanging = true; volumeChangeMouseHandler(e) };
@ -505,6 +566,15 @@ function volumeChangeHandler(volume: number) {
player.setVolume(volume); player.setVolume(volume);
} }
playerCtrlLiveBadge.onclick = () => {
if (!isLivePosition) {
isLivePosition = true;
player.setCurrentTime(player.getDuration() - livePositionDelta);
playerCtrlLiveBadge.setAttribute("style", `background-color: red`);
}
};
playerCtrlCaptions.onclick = () => { player.enableCaptions(!player.isCaptionsEnabled()); playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); }; playerCtrlCaptions.onclick = () => { player.enableCaptions(!player.isCaptionsEnabled()); playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); };
playerCtrlSpeed.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); }; playerCtrlSpeed.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); };
playerCtrlFullscreen.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); }; playerCtrlFullscreen.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };

View file

@ -15,6 +15,7 @@
<div id="progressBar" ref="progressBar" class="progressBar" ></div> <div id="progressBar" ref="progressBar" class="progressBar" ></div>
<div id="progressBarBuffer" class="progressBarBuffer" ></div> <div id="progressBarBuffer" class="progressBarBuffer" ></div>
<div id="progressBarProgress" class="progressBarProgress" ></div> <div id="progressBarProgress" class="progressBarProgress" ></div>
<div id="progressBarPosition" class="progressBarPosition" ></div>
<!-- <div class="progressBarChapterContainer"></div> --> <!-- <div class="progressBarChapterContainer"></div> -->
<div id="progressBarHandle" class="progressBarHandle" ></div> <div id="progressBarHandle" class="progressBarHandle" ></div>
<div id="progressBarInteractiveArea" class="progressBarInteractiveArea" ></div> <div id="progressBarInteractiveArea" class="progressBarInteractiveArea" ></div>

View file

@ -183,6 +183,20 @@ body {
pointer-events: none; pointer-events: none;
} }
.progressBarPosition {
position: absolute;
bottom: 25px;
padding: 2px 5px;
font-family: InterVariable;
font-size: 16px;
font-style: normal;
font-weight: 400;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.5);
}
.progressBarHandle { .progressBarHandle {
position: absolute; position: absolute;
/* bottom: 70px; */ /* bottom: 70px; */
@ -227,10 +241,13 @@ body {
.liveBadge { .liveBadge {
background-color: red; background-color: red;
margin-top: -2px; /* margin-top: -2px; */
padding: 5px 5px; /* padding: 5px 5px; */
padding: 2px 5px;
border-radius: 4px; border-radius: 4px;
margin-left: 10px; /* margin-left: 10px; */
margin-right: 10px;
cursor: pointer;
} }
.play { .play {