mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
Finished first version of Chrome extension to cast to FCast. Added support for WebSocket to terminal client. Added global support for setting playback speed. Added support for casting local file using terminal client. Added global support for playback error messages. Fixed crash caused by failing to unregister MDNS. Fixed issue where subtitles would always show for HLS. Added support for fractional seconds globally. Layout fixes to desktop casting client. Added footer telling user they can close the window.
This commit is contained in:
parent
fd9a63dac0
commit
18b61d549c
26 changed files with 1116 additions and 193 deletions
|
@ -1,6 +1,6 @@
|
|||
import net = require('net');
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
enum SessionState {
|
||||
|
@ -19,7 +19,9 @@ enum Opcode {
|
|||
Seek = 5,
|
||||
PlaybackUpdate = 6,
|
||||
VolumeUpdate = 7,
|
||||
SetVolume = 8
|
||||
SetVolume = 8,
|
||||
PlaybackError = 9,
|
||||
SetSpeed = 10
|
||||
};
|
||||
|
||||
const LENGTH_BYTES = 4;
|
||||
|
@ -40,6 +42,10 @@ export class FCastSession {
|
|||
this.state = SessionState.WaitingForLength;
|
||||
}
|
||||
|
||||
sendPlaybackError(value: PlaybackErrorMessage) {
|
||||
this.send(Opcode.PlaybackError, value);
|
||||
}
|
||||
|
||||
sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
||||
this.send(Opcode.PlaybackUpdate, value);
|
||||
}
|
||||
|
@ -178,6 +184,9 @@ export class FCastSession {
|
|||
case Opcode.SetVolume:
|
||||
this.emitter.emit("setvolume", JSON.parse(body) as SetVolumeMessage);
|
||||
break;
|
||||
case Opcode.SetSpeed:
|
||||
this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Error handling packet from.`, e);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog } from 'electron';
|
||||
import path = require('path');
|
||||
import { TcpListenerService } from './TcpListenerService';
|
||||
import { PlaybackUpdateMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { DiscoveryService } from './DiscoveryService';
|
||||
import { Updater } from './Updater';
|
||||
import { WebSocketListenerService } from './WebSocketListenerService';
|
||||
|
@ -137,8 +137,13 @@ export default class Main {
|
|||
|
||||
l.emitter.on("seek", (message) => Main.playerWindow?.webContents?.send("seek", message));
|
||||
l.emitter.on("setvolume", (message) => Main.playerWindow?.webContents?.send("setvolume", message));
|
||||
l.emitter.on("setspeed", (message) => Main.playerWindow?.webContents?.send("setspeed", message));
|
||||
l.start();
|
||||
|
||||
ipcMain.on('send-playback-error', (event: IpcMainEvent, value: PlaybackErrorMessage) => {
|
||||
l.sendPlaybackError(value);
|
||||
});
|
||||
|
||||
ipcMain.on('send-playback-update', (event: IpcMainEvent, value: PlaybackUpdateMessage) => {
|
||||
l.sendPlaybackUpdate(value);
|
||||
});
|
||||
|
@ -198,6 +203,8 @@ export default class Main {
|
|||
Main.mainWindow = new BrowserWindow({
|
||||
fullscreen: true,
|
||||
autoHideMenuBar: true,
|
||||
minWidth: 500,
|
||||
minHeight: 920,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'main/preload.js')
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ export class PlayMessage {
|
|||
public container: String,
|
||||
public url: String = null,
|
||||
public content: String = null,
|
||||
public time: number = null
|
||||
public time: number = null,
|
||||
public speed: number = null
|
||||
) {}
|
||||
}
|
||||
|
||||
|
@ -18,7 +19,14 @@ export class PlaybackUpdateMessage {
|
|||
public generationTime: number,
|
||||
public time: number,
|
||||
public duration: number,
|
||||
public state: number
|
||||
public state: number,
|
||||
public speed: number
|
||||
) {}
|
||||
}
|
||||
|
||||
export class PlaybackErrorMessage {
|
||||
constructor(
|
||||
public message: String
|
||||
) {}
|
||||
}
|
||||
|
||||
|
@ -33,4 +41,10 @@ export class SetVolumeMessage {
|
|||
constructor(
|
||||
public volume: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SetSpeedMessage {
|
||||
constructor(
|
||||
public speed: number,
|
||||
) {}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import net = require('net');
|
||||
import { FCastSession } from './FCastSession';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { dialog } from 'electron';
|
||||
import Main from './Main';
|
||||
|
||||
|
@ -33,6 +33,19 @@ export class TcpListenerService {
|
|||
server.close();
|
||||
}
|
||||
|
||||
sendPlaybackError(value: PlaybackErrorMessage) {
|
||||
console.info("Sending playback error.", value);
|
||||
|
||||
this.sessions.forEach(session => {
|
||||
try {
|
||||
session.sendPlaybackError(value);
|
||||
} catch (e) {
|
||||
console.warn("Failed to send error.", e);
|
||||
session.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
||||
console.info("Sending playback update.", value);
|
||||
|
||||
|
@ -89,6 +102,7 @@ export class TcpListenerService {
|
|||
session.emitter.on("stop", () => { this.emitter.emit("stop") });
|
||||
session.emitter.on("seek", (body: SeekMessage) => { this.emitter.emit("seek", body) });
|
||||
session.emitter.on("setvolume", (body: SetVolumeMessage) => { this.emitter.emit("setvolume", body) });
|
||||
session.emitter.on("setspeed", (body: SetSpeedMessage) => { this.emitter.emit("setspeed", body) });
|
||||
this.sessions.push(session);
|
||||
|
||||
socket.on("error", (err) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import net = require('net');
|
||||
import { FCastSession } from './FCastSession';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { dialog } from 'electron';
|
||||
import Main from './Main';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
|
@ -33,6 +33,19 @@ export class WebSocketListenerService {
|
|||
server.close();
|
||||
}
|
||||
|
||||
sendPlaybackError(value: PlaybackErrorMessage) {
|
||||
console.info("Sending playback error.", value);
|
||||
|
||||
this.sessions.forEach(session => {
|
||||
try {
|
||||
session.sendPlaybackError(value);
|
||||
} catch (e) {
|
||||
console.warn("Failed to send error.", e);
|
||||
session.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
||||
console.info("Sending playback update.", value);
|
||||
|
||||
|
@ -89,6 +102,7 @@ export class WebSocketListenerService {
|
|||
session.emitter.on("stop", () => { this.emitter.emit("stop") });
|
||||
session.emitter.on("seek", (body: SeekMessage) => { this.emitter.emit("seek", body) });
|
||||
session.emitter.on("setvolume", (body: SetVolumeMessage) => { this.emitter.emit("setvolume", body) });
|
||||
session.emitter.on("setspeed", (body: SetSpeedMessage) => { this.emitter.emit("setspeed", body) });
|
||||
this.sessions.push(session);
|
||||
|
||||
socket.on("error", (err) => {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
<div id="automatic-discovery">Automatic discovery is available via mDNS</div>
|
||||
<div id="qr-code"></div>
|
||||
<div id="scan-to-connect">Scan to connect</div>
|
||||
<div id="scan-to-connect" style="font-weight: bold;">Scan to connect</div>
|
||||
</div>
|
||||
<!--<div id="update-dialog">There is an update available. Do you wish to update?</div>
|
||||
<div id="update-button">Update</div>
|
||||
|
@ -34,6 +34,8 @@
|
|||
<div id="update-spinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div id="progress-text"></div>
|
||||
</div>-->
|
||||
|
||||
<div id="window-can-be-closed" style="color: #666666; position: absolute; bottom: 0; margin-bottom: 20px;">App will continue to run as tray app when the window is closed</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
|
||||
|
|
|
@ -48,10 +48,14 @@ body, html {
|
|||
background-color: white;
|
||||
}
|
||||
|
||||
#update-dialog, #waiting-for-connection, #manual-connection-info, #ips, #automatic-discovery, #scan-to-connect {
|
||||
#update-dialog, #waiting-for-connection, #ips, #automatic-discovery, #scan-to-connect {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#update-button {
|
||||
background: blue;
|
||||
padding: 10px 28px;
|
||||
|
|
|
@ -3,11 +3,13 @@ const { contextBridge, ipcRenderer } = require('electron');
|
|||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
toggleFullScreen: () => ipcRenderer.send('toggle-full-screen'),
|
||||
exitFullScreen: () => ipcRenderer.send('exit-full-screen'),
|
||||
sendPlaybackError: (error) => ipcRenderer.send('send-playback-error', error),
|
||||
sendPlaybackUpdate: (update) => ipcRenderer.send('send-playback-update', update),
|
||||
sendVolumeUpdate: (update) => ipcRenderer.send('send-volume-update', update),
|
||||
onPlay: (callback) => ipcRenderer.on("play", callback),
|
||||
onPause: (callback) => ipcRenderer.on("pause", callback),
|
||||
onResume: (callback) => ipcRenderer.on("resume", callback),
|
||||
onSeek: (callback) => ipcRenderer.on("seek", callback),
|
||||
onSetVolume: (callback) => ipcRenderer.on("setvolume", callback)
|
||||
onSetVolume: (callback) => ipcRenderer.on("setvolume", callback),
|
||||
onSetSpeed: (callback) => ipcRenderer.on("setspeed", callback)
|
||||
});
|
|
@ -15,32 +15,47 @@ const player = videojs("video-player", options, function onPlayerReady() {
|
|||
}
|
||||
});
|
||||
|
||||
player.on("pause", () => { window.electronAPI.sendPlaybackUpdate({
|
||||
player.on("pause", () => { window.electronAPI.sendPlaybackUpdate({
|
||||
generationTime: Date.now(),
|
||||
time: Math.round(player.currentTime()),
|
||||
duration: Math.round(player.duration()),
|
||||
state: 2
|
||||
time: player.currentTime(),
|
||||
duration: player.duration(),
|
||||
state: 2,
|
||||
speed: player.playbackRate()
|
||||
})});
|
||||
|
||||
player.on("play", () => { window.electronAPI.sendPlaybackUpdate({
|
||||
generationTime: Date.now(),
|
||||
time: Math.round(player.currentTime()),
|
||||
duration: Math.round(player.duration()),
|
||||
state: 1
|
||||
time: player.currentTime(),
|
||||
duration: player.duration(),
|
||||
state: 1,
|
||||
speed: player.playbackRate()
|
||||
})});
|
||||
|
||||
player.on("seeked", () => { window.electronAPI.sendPlaybackUpdate({
|
||||
generationTime: Date.now(),
|
||||
time: Math.round(player.currentTime()),
|
||||
duration: Math.round(player.duration()),
|
||||
state: player.paused() ? 2 : 1 })
|
||||
});
|
||||
time: player.currentTime(),
|
||||
duration: player.duration(),
|
||||
state: player.paused() ? 2 : 1,
|
||||
speed: player.playbackRate()
|
||||
})});
|
||||
|
||||
player.on("volumechange", () => { window.electronAPI.sendVolumeUpdate({
|
||||
generationTime: Date.now(),
|
||||
volume: player.volume()
|
||||
})});
|
||||
|
||||
player.on("ratechange", () => { window.electronAPI.sendPlaybackUpdate({
|
||||
generationTime: Date.now(),
|
||||
time: player.currentTime(),
|
||||
duration: player.duration(),
|
||||
state: player.paused() ? 2 : 1,
|
||||
speed: player.playbackRate()
|
||||
})});
|
||||
|
||||
player.on('error', () => { window.electronAPI.sendPlaybackError({
|
||||
message: JSON.stringify(player.error())
|
||||
})});
|
||||
|
||||
window.electronAPI.onPlay((_event, value) => {
|
||||
console.log("Handle play message renderer", value);
|
||||
|
||||
|
@ -50,11 +65,22 @@ window.electronAPI.onPlay((_event, value) => {
|
|||
player.src({ type: value.container, src: value.url });
|
||||
}
|
||||
|
||||
player.play();
|
||||
const onLoadedMetadata = () => {
|
||||
if (value.time) {
|
||||
player.currentTime(value.time);
|
||||
}
|
||||
|
||||
if (value.time) {
|
||||
player.currentTime(value.time);
|
||||
}
|
||||
if (value.speed) {
|
||||
player.playbackRate(value.speed);
|
||||
} else {
|
||||
player.playbackRate(1.0);
|
||||
}
|
||||
|
||||
player.off('loadedmetadata', onLoadedMetadata);
|
||||
};
|
||||
|
||||
player.on('loadedmetadata', onLoadedMetadata);
|
||||
player.play();
|
||||
});
|
||||
|
||||
window.electronAPI.onPause((_event) => {
|
||||
|
@ -77,12 +103,18 @@ window.electronAPI.onSetVolume((_event, value) => {
|
|||
player.volume(Math.min(1.0, Math.max(0.0, value.volume)));
|
||||
});
|
||||
|
||||
window.electronAPI.onSetSpeed((_event, value) => {
|
||||
console.log("Handle setSpeed");
|
||||
player.playbackRate(value.speed);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
window.electronAPI.sendPlaybackUpdate({
|
||||
generationTime: Date.now(),
|
||||
time: Math.round(player.currentTime()),
|
||||
duration: Math.round(player.duration()),
|
||||
state: player.paused() ? 2 : 1
|
||||
time: (player.currentTime()),
|
||||
duration: (player.duration()),
|
||||
state: player.paused() ? 2 : 1,
|
||||
speed: player.playbackRate()
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
|
@ -171,7 +203,7 @@ player.ready(() => {
|
|||
textTracks.addEventListener("change", function () {
|
||||
console.log("Text tracks changed", textTracks);
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
if (textTracks[i].language === "en" && textTracks[i].mode !== "showing") {
|
||||
if (textTracks[i].language === "df" && textTracks[i].mode !== "showing") {
|
||||
textTracks[i].mode = "showing";
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +212,7 @@ player.ready(() => {
|
|||
player.on('loadedmetadata', function () {
|
||||
console.log("Metadata loaded", textTracks);
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
if (textTracks[i].language === "en" && textTracks[i].mode !== "showing") {
|
||||
if (textTracks[i].language === "df" && textTracks[i].mode !== "showing") {
|
||||
textTracks[i].mode = "showing";
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue