Files
MoonTVPlus/src/components/tv/TVRemoteReceiver.tsx
T
2026-05-30 22:34:07 +08:00

136 lines
3.8 KiB
TypeScript

'use client';
import { useEffect } from 'react';
import { type Socket,io } from 'socket.io-client';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import {
applyTVRemoteText,
fireTVRemoteKey,
} from '@/lib/tv-remote-core';
import type {
TVRemoteKeyCommand,
TVRemoteTextCommand,
} from '@/lib/tv-remote-types';
const DEVICE_ID_KEY = 'moontv_tv_remote_device_id';
type TVRemoteReceiverSingleton = {
socket: Socket | null;
refCount: number;
disconnectTimer: number | null;
};
const receiverState: TVRemoteReceiverSingleton = {
socket: null,
refCount: 0,
disconnectTimer: null,
};
function getDeviceId() {
let id = localStorage.getItem(DEVICE_ID_KEY);
if (!id) {
id =
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? crypto.randomUUID()
: `tv-${Date.now()}-${Math.random().toString(36).slice(2)}`;
localStorage.setItem(DEVICE_ID_KEY, id);
}
return id;
}
function getDeviceName() {
const ua = navigator.userAgent;
if (/Android/i.test(ua)) return 'Android TV Web';
if (/Windows/i.test(ua)) return 'Windows TV Web';
if (/Macintosh|Mac OS/i.test(ua)) return 'Mac TV Web';
return 'Web TV';
}
export default function TVRemoteReceiver() {
useEffect(() => {
const auth = getAuthInfoFromBrowserCookie();
if (!auth?.username) return;
receiverState.refCount += 1;
if (receiverState.disconnectTimer) {
window.clearTimeout(receiverState.disconnectTimer);
receiverState.disconnectTimer = null;
}
if (!receiverState.socket) {
receiverState.socket = io({
path: '/socket.io',
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
}
const socket = receiverState.socket;
const register = () => {
socket.timeout(5000).emit(
'tv-remote:register-tv',
{
deviceId: getDeviceId(),
deviceName: getDeviceName(),
currentPath: window.location.pathname,
title: document.title,
},
(error: Error | null, response?: { success: boolean; error?: string }) => {
if (error || !response?.success) {
// eslint-disable-next-line no-console
console.warn('[TVRemote] TV registration failed:', error || response?.error);
}
}
);
};
const updateState = () => {
socket.emit('tv-remote:tv-state', {
deviceId: getDeviceId(),
currentPath: window.location.pathname,
title: document.title,
});
};
socket.on('connect', register);
socket.on('tv-remote:key', (command: TVRemoteKeyCommand) => {
fireTVRemoteKey(command);
});
socket.on('tv-remote:text', (command: TVRemoteTextCommand) => {
applyTVRemoteText(command);
});
const interval = window.setInterval(updateState, 10000);
const onVisibilityChange = () => {
if (!document.hidden) updateState();
};
document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('focus', updateState);
return () => {
window.clearInterval(interval);
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('focus', updateState);
socket.off('connect', register);
socket.off('tv-remote:key');
socket.off('tv-remote:text');
receiverState.refCount = Math.max(0, receiverState.refCount - 1);
if (receiverState.refCount === 0) {
receiverState.disconnectTimer = window.setTimeout(() => {
if (receiverState.refCount > 0) return;
receiverState.socket?.disconnect();
receiverState.socket = null;
receiverState.disconnectTimer = null;
}, 1000);
}
};
}, []);
return null;
}