远程遥控完成
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import type {
|
||||
TVRemoteKey,
|
||||
TVRemoteKeyCommand,
|
||||
TVRemoteTextCommand,
|
||||
} from './tv-remote-types';
|
||||
|
||||
const keyConfigs: Record<
|
||||
TVRemoteKey,
|
||||
{ key: string; code: string; keyCode: number }
|
||||
> = {
|
||||
up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
|
||||
down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
|
||||
left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
|
||||
right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
|
||||
ok: { key: 'Enter', code: 'Enter', keyCode: 13 },
|
||||
back: { key: 'Escape', code: 'Escape', keyCode: 27 },
|
||||
menu: { key: 'ContextMenu', code: 'ContextMenu', keyCode: 93 },
|
||||
home: { key: 'Home', code: 'Home', keyCode: 36 },
|
||||
playPause: { key: 'Enter', code: 'Enter', keyCode: 13 },
|
||||
pageUp: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
|
||||
pageDown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
|
||||
digit: { key: '0', code: 'Digit0', keyCode: 48 },
|
||||
};
|
||||
|
||||
function dispatchKeyboardEvent(type: 'keydown' | 'keyup', cfg: {
|
||||
key: string;
|
||||
code: string;
|
||||
keyCode: number;
|
||||
}, repeat = false) {
|
||||
const event = new KeyboardEvent(type, {
|
||||
key: cfg.key,
|
||||
code: cfg.code,
|
||||
repeat,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(event, 'keyCode', { get: () => cfg.keyCode });
|
||||
Object.defineProperty(event, 'which', { get: () => cfg.keyCode });
|
||||
|
||||
document.activeElement?.dispatchEvent(event);
|
||||
window.dispatchEvent(event);
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
export function fireTVRemoteKey(command: TVRemoteKey | TVRemoteKeyCommand, repeat = false) {
|
||||
const normalized =
|
||||
typeof command === 'string'
|
||||
? { key: command, repeat }
|
||||
: command;
|
||||
let cfg = keyConfigs[normalized.key];
|
||||
|
||||
if (normalized.key === 'digit') {
|
||||
const digit = /^[0-9]$/.test(normalized.digit || '')
|
||||
? normalized.digit || '0'
|
||||
: '0';
|
||||
cfg = {
|
||||
key: digit,
|
||||
code: `Digit${digit}`,
|
||||
keyCode: 48 + Number(digit),
|
||||
};
|
||||
}
|
||||
|
||||
dispatchKeyboardEvent('keydown', cfg, Boolean(normalized.repeat));
|
||||
dispatchKeyboardEvent('keyup', cfg, Boolean(normalized.repeat));
|
||||
}
|
||||
|
||||
function setNativeValue(element: HTMLInputElement | HTMLTextAreaElement, value: string) {
|
||||
const prototype = Object.getPrototypeOf(element);
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
||||
descriptor?.set?.call(element, value);
|
||||
}
|
||||
|
||||
function getTextTarget() {
|
||||
const active = document.activeElement;
|
||||
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
|
||||
return active;
|
||||
}
|
||||
|
||||
return document.querySelector<HTMLInputElement | HTMLTextAreaElement>(
|
||||
'input:not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly])'
|
||||
);
|
||||
}
|
||||
|
||||
export function applyTVRemoteText(command: TVRemoteTextCommand) {
|
||||
const target = getTextTarget();
|
||||
if (!target) return false;
|
||||
|
||||
target.focus({ preventScroll: true });
|
||||
|
||||
const start = target.selectionStart ?? target.value.length;
|
||||
const end = target.selectionEnd ?? target.value.length;
|
||||
const text = command.text || '';
|
||||
let next = target.value;
|
||||
let nextCaret = start;
|
||||
|
||||
if (command.mode === 'replace') {
|
||||
next = text;
|
||||
nextCaret = next.length;
|
||||
} else if (command.mode === 'append') {
|
||||
next = `${target.value.slice(0, start)}${text}${target.value.slice(end)}`;
|
||||
nextCaret = start + text.length;
|
||||
} else if (command.mode === 'backspace') {
|
||||
if (start !== end) {
|
||||
next = `${target.value.slice(0, start)}${target.value.slice(end)}`;
|
||||
nextCaret = start;
|
||||
} else if (start > 0) {
|
||||
next = `${target.value.slice(0, start - 1)}${target.value.slice(end)}`;
|
||||
nextCaret = start - 1;
|
||||
}
|
||||
} else if (command.mode === 'clear') {
|
||||
next = '';
|
||||
nextCaret = 0;
|
||||
}
|
||||
|
||||
setNativeValue(target, next);
|
||||
target.setSelectionRange(nextCaret, nextCaret);
|
||||
target.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
target.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
const HUB_KEY = '__moonTvRemoteHub';
|
||||
|
||||
function getGlobalHub() {
|
||||
if (!globalThis[HUB_KEY]) {
|
||||
globalThis[HUB_KEY] = {
|
||||
io: null,
|
||||
devices: new Map(),
|
||||
socketToDevice: new Map(),
|
||||
};
|
||||
}
|
||||
return globalThis[HUB_KEY];
|
||||
}
|
||||
|
||||
function attachTVRemoteIO(io) {
|
||||
const hub = getGlobalHub();
|
||||
hub.io = io;
|
||||
}
|
||||
|
||||
function registerTVRemoteDevice(socketId, username, data) {
|
||||
const hub = getGlobalHub();
|
||||
const deviceId = String(data?.deviceId || '').slice(0, 128);
|
||||
if (!deviceId) return { success: false, error: '缺少设备 ID' };
|
||||
|
||||
const device = {
|
||||
deviceId,
|
||||
socketId,
|
||||
username,
|
||||
deviceName: String(data?.deviceName || 'Web TV').slice(0, 80),
|
||||
currentPath: String(data?.currentPath || '/tv').slice(0, 240),
|
||||
title: String(data?.title || '').slice(0, 120),
|
||||
lastActiveAt: Date.now(),
|
||||
};
|
||||
|
||||
hub.devices.set(deviceId, device);
|
||||
hub.socketToDevice.set(socketId, deviceId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
function updateTVRemoteDevice(socketId, username, data) {
|
||||
const hub = getGlobalHub();
|
||||
const deviceId = String(data?.deviceId || hub.socketToDevice.get(socketId) || '');
|
||||
const device = hub.devices.get(deviceId);
|
||||
if (!device || device.socketId !== socketId || device.username !== username) {
|
||||
return false;
|
||||
}
|
||||
|
||||
device.currentPath = String(data?.currentPath || device.currentPath).slice(0, 240);
|
||||
device.title = String(data?.title || device.title || '').slice(0, 120);
|
||||
device.lastActiveAt = Date.now();
|
||||
hub.devices.set(deviceId, device);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeTVRemoteSocket(socketId) {
|
||||
const hub = getGlobalHub();
|
||||
const deviceId = hub.socketToDevice.get(socketId);
|
||||
if (!deviceId) return;
|
||||
|
||||
const device = hub.devices.get(deviceId);
|
||||
if (device?.socketId === socketId) {
|
||||
hub.devices.delete(deviceId);
|
||||
}
|
||||
hub.socketToDevice.delete(socketId);
|
||||
}
|
||||
|
||||
function listTVRemoteDevices(username) {
|
||||
const hub = getGlobalHub();
|
||||
const now = Date.now();
|
||||
return Array.from(hub.devices.values())
|
||||
.filter((device) => device.username === username && now - device.lastActiveAt < 45_000)
|
||||
.map(({ deviceId, deviceName, currentPath, title, lastActiveAt }) => ({
|
||||
deviceId,
|
||||
deviceName,
|
||||
currentPath,
|
||||
title,
|
||||
lastActiveAt,
|
||||
}))
|
||||
.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
||||
}
|
||||
|
||||
function sendTVRemoteCommand(username, deviceId, eventName, command) {
|
||||
const hub = getGlobalHub();
|
||||
const device = hub.devices.get(String(deviceId || ''));
|
||||
if (!hub.io) return { success: false, error: '遥控服务未启动' };
|
||||
if (!device || device.username !== username) {
|
||||
return { success: false, error: '电视端不在线' };
|
||||
}
|
||||
|
||||
device.lastActiveAt = Date.now();
|
||||
hub.devices.set(device.deviceId, device);
|
||||
hub.io.to(device.socketId).emit(eventName, command);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
function cleanupTVRemoteDevices() {
|
||||
const hub = getGlobalHub();
|
||||
const now = Date.now();
|
||||
for (const [deviceId, device] of hub.devices.entries()) {
|
||||
if (now - device.lastActiveAt > 90_000) {
|
||||
hub.devices.delete(deviceId);
|
||||
hub.socketToDevice.delete(device.socketId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearTVRemoteHub() {
|
||||
const hub = getGlobalHub();
|
||||
hub.io = null;
|
||||
hub.devices.clear();
|
||||
hub.socketToDevice.clear();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
attachTVRemoteIO,
|
||||
cleanupTVRemoteDevices,
|
||||
clearTVRemoteHub,
|
||||
listTVRemoteDevices,
|
||||
registerTVRemoteDevice,
|
||||
removeTVRemoteSocket,
|
||||
sendTVRemoteCommand,
|
||||
updateTVRemoteDevice,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
export type TVRemoteKey =
|
||||
| 'up'
|
||||
| 'down'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'ok'
|
||||
| 'back'
|
||||
| 'menu'
|
||||
| 'home'
|
||||
| 'playPause'
|
||||
| 'pageUp'
|
||||
| 'pageDown'
|
||||
| 'digit';
|
||||
|
||||
export type TVRemoteTextMode = 'replace' | 'append' | 'backspace' | 'clear';
|
||||
|
||||
export interface TVRemoteDevice {
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
currentPath: string;
|
||||
title?: string;
|
||||
lastActiveAt: number;
|
||||
}
|
||||
|
||||
export interface TVRemoteKeyCommand {
|
||||
key: TVRemoteKey;
|
||||
repeat?: boolean;
|
||||
digit?: string;
|
||||
}
|
||||
|
||||
export interface TVRemoteTextCommand {
|
||||
mode: TVRemoteTextMode;
|
||||
text?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user