mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-21 17:16:20 -07:00
fix(jellyfin): keep remote websocket alive
- Respond to keep-alive requests and reconnect when the socket goes silent - Warn when playback reports fail
This commit is contained in:
@@ -2,3 +2,4 @@ type: fixed
|
|||||||
area: jellyfin
|
area: jellyfin
|
||||||
|
|
||||||
- Authenticate Jellyfin playback, subtitle, artwork, and remote-control socket URLs with the `ApiKey` query parameter and stop sending the legacy `X-Emby-Token` and `X-Emby-Authorization` headers, so the integration keeps working on Jellyfin 12 where legacy authorization is disabled by default.
|
- Authenticate Jellyfin playback, subtitle, artwork, and remote-control socket URLs with the `ApiKey` query parameter and stop sending the legacy `X-Emby-Token` and `X-Emby-Authorization` headers, so the integration keeps working on Jellyfin 12 where legacy authorization is disabled by default.
|
||||||
|
- Keep the cast-target websocket alive by answering Jellyfin keep-alive requests and reconnect when the server stops replying, so "Play on SubMiner" keeps working on Jellyfin 12 instead of silently dying about a minute after connecting. Failed playback progress and stop reports are now logged as warnings.
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ import { buildJellyfinTimelinePayload, JellyfinRemoteSessionService } from './je
|
|||||||
|
|
||||||
class FakeWebSocket {
|
class FakeWebSocket {
|
||||||
private listeners: Record<string, Array<(...args: unknown[]) => void>> = {};
|
private listeners: Record<string, Array<(...args: unknown[]) => void>> = {};
|
||||||
|
sent: string[] = [];
|
||||||
|
terminated = false;
|
||||||
|
|
||||||
|
send(data: string): void {
|
||||||
|
this.sent.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate(): void {
|
||||||
|
this.terminated = true;
|
||||||
|
this.emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
on(event: string, listener: (...args: unknown[]) => void): this {
|
on(event: string, listener: (...args: unknown[]) => void): this {
|
||||||
if (!this.listeners[event]) {
|
if (!this.listeners[event]) {
|
||||||
@@ -356,3 +367,114 @@ test('advertiseNow validates server registration using Sessions endpoint', async
|
|||||||
assert.equal(ok, true);
|
assert.equal(ok, true);
|
||||||
assert.ok(calls.some((url) => url.endsWith('/Sessions')));
|
assert.ok(calls.some((url) => url.endsWith('/Sessions')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('answers ForceKeepAlive with KeepAlive messages on the advertised cadence', () => {
|
||||||
|
const sockets: FakeWebSocket[] = [];
|
||||||
|
const timers: Array<{ handler: () => void; delay: number }> = [];
|
||||||
|
|
||||||
|
const service = new JellyfinRemoteSessionService({
|
||||||
|
serverUrl: 'http://jellyfin.local',
|
||||||
|
accessToken: 'token-ka',
|
||||||
|
deviceId: 'device-ka',
|
||||||
|
webSocketFactory: () => {
|
||||||
|
const socket = new FakeWebSocket();
|
||||||
|
sockets.push(socket);
|
||||||
|
return socket as unknown as any;
|
||||||
|
},
|
||||||
|
fetchImpl: (async () => new Response(null, { status: 200 })) as typeof fetch,
|
||||||
|
setTimer: ((handler: () => void, delay?: number) => {
|
||||||
|
timers.push({ handler, delay: Number(delay) });
|
||||||
|
return timers.length as unknown as ReturnType<typeof setTimeout>;
|
||||||
|
}) as typeof setTimeout,
|
||||||
|
clearTimer: (() => undefined) as typeof clearTimeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.start();
|
||||||
|
sockets[0]!.emit('open');
|
||||||
|
assert.deepEqual(sockets[0]!.sent, ['{"MessageType":"KeepAlive"}']);
|
||||||
|
assert.equal(timers[0]!.delay, 30_000);
|
||||||
|
|
||||||
|
sockets[0]!.emit('message', JSON.stringify({ MessageType: 'ForceKeepAlive', Data: 20 }));
|
||||||
|
assert.equal(sockets[0]!.sent.length, 2);
|
||||||
|
assert.equal(timers.at(-1)!.delay, 10_000);
|
||||||
|
|
||||||
|
timers.at(-1)!.handler();
|
||||||
|
assert.equal(sockets[0]!.sent.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reconnects when the server stops answering keep-alives', () => {
|
||||||
|
let now = 1_000_000;
|
||||||
|
const sockets: FakeWebSocket[] = [];
|
||||||
|
const timers: Array<() => void> = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
const service = new JellyfinRemoteSessionService({
|
||||||
|
serverUrl: 'http://jellyfin.local',
|
||||||
|
accessToken: 'token-lost',
|
||||||
|
deviceId: 'device-lost',
|
||||||
|
webSocketFactory: () => {
|
||||||
|
const socket = new FakeWebSocket();
|
||||||
|
sockets.push(socket);
|
||||||
|
return socket as unknown as any;
|
||||||
|
},
|
||||||
|
fetchImpl: (async () => new Response(null, { status: 200 })) as typeof fetch,
|
||||||
|
getNow: () => now,
|
||||||
|
logWarn: (message) => {
|
||||||
|
warnings.push(message);
|
||||||
|
},
|
||||||
|
reconnectBaseDelayMs: 100,
|
||||||
|
setTimer: ((handler: () => void) => {
|
||||||
|
timers.push(handler);
|
||||||
|
return timers.length as unknown as ReturnType<typeof setTimeout>;
|
||||||
|
}) as typeof setTimeout,
|
||||||
|
clearTimer: (() => undefined) as typeof clearTimeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.start();
|
||||||
|
sockets[0]!.emit('open');
|
||||||
|
|
||||||
|
// Two silent ticks are still within the 90s tolerance; the third marks the socket lost.
|
||||||
|
now += 30_000;
|
||||||
|
timers.shift()!();
|
||||||
|
now += 30_000;
|
||||||
|
timers.shift()!();
|
||||||
|
assert.equal(sockets[0]!.sent.length, 3);
|
||||||
|
assert.equal(sockets[0]!.terminated, false);
|
||||||
|
|
||||||
|
now += 30_000;
|
||||||
|
timers.shift()!();
|
||||||
|
assert.equal(sockets[0]!.terminated, true);
|
||||||
|
assert.equal(service.isConnected(), false);
|
||||||
|
assert.equal(warnings.length, 1);
|
||||||
|
|
||||||
|
timers.shift()!();
|
||||||
|
assert.equal(sockets.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warns once per failing timeline endpoint until it recovers', async () => {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
let status = 400;
|
||||||
|
|
||||||
|
const service = new JellyfinRemoteSessionService({
|
||||||
|
serverUrl: 'http://jellyfin.local',
|
||||||
|
accessToken: 'token-warn',
|
||||||
|
deviceId: 'device-warn',
|
||||||
|
webSocketFactory: () => new FakeWebSocket() as unknown as any,
|
||||||
|
fetchImpl: (async () => new Response(null, { status })) as typeof fetch,
|
||||||
|
logWarn: (message) => {
|
||||||
|
warnings.push(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const state = { itemId: 'item-1', positionTicks: 10, playMethod: 'DirectPlay' };
|
||||||
|
|
||||||
|
assert.equal(await service.reportStopped(state), false);
|
||||||
|
assert.equal(await service.reportStopped(state), false);
|
||||||
|
assert.equal(warnings.length, 1);
|
||||||
|
assert.match(warnings[0]!, /Sessions\/Playing\/Stopped/);
|
||||||
|
|
||||||
|
status = 200;
|
||||||
|
assert.equal(await service.reportStopped(state), true);
|
||||||
|
status = 500;
|
||||||
|
assert.equal(await service.reportStopped(state), false);
|
||||||
|
assert.equal(warnings.length, 2);
|
||||||
|
});
|
||||||
|
|||||||
@@ -45,9 +45,22 @@ interface JellyfinRemoteSocket {
|
|||||||
on(event: 'close', listener: () => void): this;
|
on(event: 'close', listener: () => void): this;
|
||||||
on(event: 'error', listener: (error: Error) => void): this;
|
on(event: 'error', listener: (error: Error) => void): this;
|
||||||
on(event: 'message', listener: (data: unknown) => void): this;
|
on(event: 'message', listener: (data: unknown) => void): this;
|
||||||
|
send(data: string): void;
|
||||||
|
terminate?(): void;
|
||||||
close(): void;
|
close(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jellyfin advertises its keep-alive timeout in the ForceKeepAlive message (60s by default),
|
||||||
|
// drops sockets that stay silent past it, and since 12.0 also detaches the session's remote
|
||||||
|
// controller when that happens. The drop never reaches the client as a close frame, so the
|
||||||
|
// client has to keep sending KeepAlive and treat missing replies as a dead connection.
|
||||||
|
const DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 60_000;
|
||||||
|
const KEEP_ALIVE_LOST_FACTOR = 1.5;
|
||||||
|
|
||||||
|
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
||||||
|
(timer as unknown as { unref?: () => void }).unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
type JellyfinRemoteSocketHeaders = Record<string, string>;
|
type JellyfinRemoteSocketHeaders = Record<string, string>;
|
||||||
|
|
||||||
export interface JellyfinRemoteSessionServiceOptions {
|
export interface JellyfinRemoteSessionServiceOptions {
|
||||||
@@ -77,6 +90,9 @@ export interface JellyfinRemoteSessionServiceOptions {
|
|||||||
deviceName?: string;
|
deviceName?: string;
|
||||||
onConnected?: () => void;
|
onConnected?: () => void;
|
||||||
onDisconnected?: () => void;
|
onDisconnected?: () => void;
|
||||||
|
logWarn?: (message: string, details?: unknown) => void;
|
||||||
|
keepAliveTimeoutMs?: number;
|
||||||
|
getNow?: () => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeServerUrl(serverUrl: string): string {
|
function normalizeServerUrl(serverUrl: string): string {
|
||||||
@@ -196,6 +212,12 @@ export class JellyfinRemoteSessionService {
|
|||||||
private readonly authHeader: string;
|
private readonly authHeader: string;
|
||||||
private readonly onConnected?: () => void;
|
private readonly onConnected?: () => void;
|
||||||
private readonly onDisconnected?: () => void;
|
private readonly onDisconnected?: () => void;
|
||||||
|
private readonly logWarn?: (message: string, details?: unknown) => void;
|
||||||
|
private readonly now: () => number;
|
||||||
|
private keepAliveTimeoutMs: number;
|
||||||
|
private keepAliveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private lastInboundAtMs = 0;
|
||||||
|
private readonly failedRequestPaths = new Set<string>();
|
||||||
|
|
||||||
private readonly reconnectBaseDelayMs: number;
|
private readonly reconnectBaseDelayMs: number;
|
||||||
private readonly reconnectMaxDelayMs: number;
|
private readonly reconnectMaxDelayMs: number;
|
||||||
@@ -233,6 +255,12 @@ export class JellyfinRemoteSessionService {
|
|||||||
});
|
});
|
||||||
this.onConnected = options.onConnected;
|
this.onConnected = options.onConnected;
|
||||||
this.onDisconnected = options.onDisconnected;
|
this.onDisconnected = options.onDisconnected;
|
||||||
|
this.logWarn = options.logWarn;
|
||||||
|
this.now = options.getNow ?? Date.now;
|
||||||
|
this.keepAliveTimeoutMs = Math.max(
|
||||||
|
1000,
|
||||||
|
options.keepAliveTimeoutMs ?? DEFAULT_KEEP_ALIVE_TIMEOUT_MS,
|
||||||
|
);
|
||||||
this.reconnectBaseDelayMs = Math.max(100, options.reconnectBaseDelayMs ?? 500);
|
this.reconnectBaseDelayMs = Math.max(100, options.reconnectBaseDelayMs ?? 500);
|
||||||
this.reconnectMaxDelayMs = Math.max(
|
this.reconnectMaxDelayMs = Math.max(
|
||||||
this.reconnectBaseDelayMs,
|
this.reconnectBaseDelayMs,
|
||||||
@@ -250,6 +278,7 @@ export class JellyfinRemoteSessionService {
|
|||||||
public stop(): void {
|
public stop(): void {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
|
this.stopKeepAlive();
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
this.clearTimer(this.reconnectTimer);
|
this.clearTimer(this.reconnectTimer);
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
@@ -298,12 +327,15 @@ export class JellyfinRemoteSessionService {
|
|||||||
if (this.socket !== socket || !this.running) return;
|
if (this.socket !== socket || !this.running) return;
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
this.reconnectAttempt = 0;
|
this.reconnectAttempt = 0;
|
||||||
|
this.lastInboundAtMs = this.now();
|
||||||
|
this.startKeepAlive(socket, this.keepAliveTimeoutMs);
|
||||||
this.onConnected?.();
|
this.onConnected?.();
|
||||||
void this.postCapabilities();
|
void this.postCapabilities();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('message', (rawData) => {
|
socket.on('message', (rawData) => {
|
||||||
this.handleInboundMessage(rawData);
|
this.lastInboundAtMs = this.now();
|
||||||
|
this.handleInboundMessage(socket, rawData);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
const handleDisconnect = () => {
|
||||||
@@ -311,6 +343,7 @@ export class JellyfinRemoteSessionService {
|
|||||||
disconnected = true;
|
disconnected = true;
|
||||||
if (this.socket === socket) {
|
if (this.socket === socket) {
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
|
this.stopKeepAlive();
|
||||||
}
|
}
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.onDisconnected?.();
|
this.onDisconnected?.();
|
||||||
@@ -323,6 +356,51 @@ export class JellyfinRemoteSessionService {
|
|||||||
socket.on('error', handleDisconnect);
|
socket.on('error', handleDisconnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startKeepAlive(socket: JellyfinRemoteSocket, timeoutMs: number): void {
|
||||||
|
this.stopKeepAlive();
|
||||||
|
this.keepAliveTimeoutMs = timeoutMs;
|
||||||
|
this.sendKeepAlive(socket);
|
||||||
|
this.scheduleKeepAliveTick(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleKeepAliveTick(socket: JellyfinRemoteSocket): void {
|
||||||
|
const intervalMs = Math.max(1000, Math.floor(this.keepAliveTimeoutMs / 2));
|
||||||
|
const timer = this.setTimer(() => {
|
||||||
|
this.keepAliveTimer = null;
|
||||||
|
if (this.socket !== socket || !this.running) return;
|
||||||
|
const silentForMs = this.now() - this.lastInboundAtMs;
|
||||||
|
if (silentForMs >= this.keepAliveTimeoutMs * KEEP_ALIVE_LOST_FACTOR) {
|
||||||
|
this.logWarn?.('Jellyfin remote websocket stopped answering keep-alives; reconnecting.');
|
||||||
|
// Dropping the socket raises 'close', which schedules the reconnect.
|
||||||
|
if (socket.terminate) {
|
||||||
|
socket.terminate();
|
||||||
|
} else {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendKeepAlive(socket);
|
||||||
|
this.scheduleKeepAliveTick(socket);
|
||||||
|
}, intervalMs);
|
||||||
|
unrefTimer(timer);
|
||||||
|
this.keepAliveTimer = timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopKeepAlive(): void {
|
||||||
|
if (this.keepAliveTimer) {
|
||||||
|
this.clearTimer(this.keepAliveTimer);
|
||||||
|
this.keepAliveTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendKeepAlive(socket: JellyfinRemoteSocket): void {
|
||||||
|
try {
|
||||||
|
socket.send(JSON.stringify({ MessageType: 'KeepAlive' }));
|
||||||
|
} catch (error) {
|
||||||
|
this.logWarn?.('Failed to send Jellyfin remote keep-alive.', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
const delay = Math.min(
|
const delay = Math.min(
|
||||||
this.reconnectMaxDelayMs,
|
this.reconnectMaxDelayMs,
|
||||||
@@ -397,16 +475,38 @@ export class JellyfinRemoteSessionService {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
this.noteRequestOutcome(path, response.ok ? null : `HTTP ${response.status}`);
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
this.noteRequestOutcome(path, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleInboundMessage(rawData: unknown): void {
|
// Warn once per path while it keeps failing so a rejected stop report is visible in the
|
||||||
|
// log without a warning per progress tick.
|
||||||
|
private noteRequestOutcome(path: string, failure: unknown): void {
|
||||||
|
if (failure === null) {
|
||||||
|
this.failedRequestPaths.delete(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.failedRequestPaths.has(path)) return;
|
||||||
|
this.failedRequestPaths.add(path);
|
||||||
|
this.logWarn?.(`Jellyfin remote request failed: POST ${path}`, failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInboundMessage(socket: JellyfinRemoteSocket, rawData: unknown): void {
|
||||||
const message = parseInboundMessage(rawData);
|
const message = parseInboundMessage(rawData);
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
const messageType = message.MessageType;
|
const messageType = message.MessageType;
|
||||||
|
if (messageType === 'ForceKeepAlive') {
|
||||||
|
const seconds = Number(message.Data);
|
||||||
|
const timeoutMs =
|
||||||
|
Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : this.keepAliveTimeoutMs;
|
||||||
|
this.startKeepAlive(socket, timeoutMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (messageType === 'KeepAlive') return;
|
||||||
const payload = parseMessageData(message.Data);
|
const payload = parseMessageData(message.Data);
|
||||||
if (messageType === 'Play') {
|
if (messageType === 'Play') {
|
||||||
this.onPlay?.(payload);
|
this.onPlay?.(payload);
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export function composeJellyfinRemoteHandlers(
|
|||||||
getNow: options.getNow,
|
getNow: options.getNow,
|
||||||
ticksPerSecond: options.ticksPerSecond,
|
ticksPerSecond: options.ticksPerSecond,
|
||||||
logDebug: options.logDebug,
|
logDebug: options.logDebug,
|
||||||
|
logWarn: options.logWarn,
|
||||||
});
|
});
|
||||||
const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler(
|
const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler(
|
||||||
buildReportJellyfinRemoteProgressMainDepsHandler(),
|
buildReportJellyfinRemoteProgressMainDepsHandler(),
|
||||||
|
|||||||
@@ -75,5 +75,6 @@ export function createBuildReportJellyfinRemoteStoppedMainDepsHandler(
|
|||||||
getNow: deps.getNow ? () => deps.getNow?.() ?? Date.now() : undefined,
|
getNow: deps.getNow ? () => deps.getNow?.() ?? Date.now() : undefined,
|
||||||
ticksPerSecond: deps.ticksPerSecond,
|
ticksPerSecond: deps.ticksPerSecond,
|
||||||
logDebug: (message: string, error: unknown) => deps.logDebug(message, error),
|
logDebug: (message: string, error: unknown) => deps.logDebug(message, error),
|
||||||
|
...(deps.logWarn ? { logWarn: (message: string) => deps.logWarn?.(message) } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export type JellyfinRemoteStoppedReporterDeps = {
|
|||||||
getNow?: () => number;
|
getNow?: () => number;
|
||||||
ticksPerSecond: number;
|
ticksPerSecond: number;
|
||||||
logDebug: (message: string, error: unknown) => void;
|
logDebug: (message: string, error: unknown) => void;
|
||||||
|
logWarn?: (message: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createReportJellyfinRemoteStoppedHandler(deps: JellyfinRemoteStoppedReporterDeps) {
|
export function createReportJellyfinRemoteStoppedHandler(deps: JellyfinRemoteStoppedReporterDeps) {
|
||||||
@@ -244,7 +245,7 @@ export function createReportJellyfinRemoteStoppedHandler(deps: JellyfinRemoteSto
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
deps.logDebug('Failed to report Jellyfin remote final progress', error);
|
deps.logDebug('Failed to report Jellyfin remote final progress', error);
|
||||||
}
|
}
|
||||||
await session.reportStopped({
|
const reported = await session.reportStopped({
|
||||||
itemId: playback.itemId,
|
itemId: playback.itemId,
|
||||||
mediaSourceId: playback.mediaSourceId,
|
mediaSourceId: playback.mediaSourceId,
|
||||||
positionTicks,
|
positionTicks,
|
||||||
@@ -254,6 +255,11 @@ export function createReportJellyfinRemoteStoppedHandler(deps: JellyfinRemoteSto
|
|||||||
subtitleStreamIndex: playback.subtitleStreamIndex,
|
subtitleStreamIndex: playback.subtitleStreamIndex,
|
||||||
eventName: 'stop',
|
eventName: 'stop',
|
||||||
});
|
});
|
||||||
|
if (reported === false) {
|
||||||
|
deps.logWarn?.(
|
||||||
|
`Jellyfin did not accept the playback stop report for item ${playback.itemId}; the server may keep showing it as playing.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
deps.logDebug('Failed to report Jellyfin remote stop', error);
|
deps.logDebug('Failed to report Jellyfin remote stop', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type JellyfinRemoteServiceOptions = {
|
|||||||
};
|
};
|
||||||
onConnected: () => void;
|
onConnected: () => void;
|
||||||
onDisconnected: () => void;
|
onDisconnected: () => void;
|
||||||
|
logWarn?: (message: string, details?: unknown) => void;
|
||||||
onPlay: (payload: JellyfinRemoteEventPayload) => void;
|
onPlay: (payload: JellyfinRemoteEventPayload) => void;
|
||||||
onPlaystate: (payload: JellyfinRemoteEventPayload) => void;
|
onPlaystate: (payload: JellyfinRemoteEventPayload) => void;
|
||||||
onGeneralCommand: (payload: JellyfinRemoteEventPayload) => void;
|
onGeneralCommand: (payload: JellyfinRemoteEventPayload) => void;
|
||||||
@@ -110,6 +111,7 @@ export function createStartJellyfinRemoteSessionHandler(deps: {
|
|||||||
onDisconnected: () => {
|
onDisconnected: () => {
|
||||||
deps.logWarn('Jellyfin remote websocket disconnected; retrying.');
|
deps.logWarn('Jellyfin remote websocket disconnected; retrying.');
|
||||||
},
|
},
|
||||||
|
logWarn: (message, details) => deps.logWarn(message, details),
|
||||||
onPlay: (payload) => {
|
onPlay: (payload) => {
|
||||||
void deps.handlePlay(payload).catch((error) => {
|
void deps.handlePlay(payload).catch((error) => {
|
||||||
deps.logWarn('Failed handling Jellyfin remote Play event', error);
|
deps.logWarn('Failed handling Jellyfin remote Play event', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user