UNPKG

@yume-chan/scrcpy

Version:
179 lines 6.71 kB
import { getUint16BigEndian, getUint32BigEndian, } from "@yume-chan/no-data-view"; import { BufferedReadableStream, PushReadableStream, StructDeserializeStream, TransformStream, } from "@yume-chan/stream-extra"; import { decodeUtf8 } from "@yume-chan/struct"; import { AndroidKeyEventAction } from "../../control/index.js"; import { ScrcpyVideoCodecId } from "../codec.js"; import { ScrcpyOptions, toScrcpyOptionValue } from "../types.js"; import { CodecOptions } from "./codec-options.js"; import { ScrcpyLogLevel1_16, ScrcpyVideoOrientation1_16 } from "./init.js"; import { SCRCPY_CONTROL_MESSAGE_TYPES_1_16, SCRCPY_MEDIA_PACKET_FLAG_CONFIG, ScrcpyBackOrScreenOnControlMessage1_16, ScrcpyClipboardDeviceMessage, ScrcpyInjectTouchControlMessage1_16, ScrcpyMediaStreamRawPacket, ScrcpySetClipboardControlMessage1_15, } from "./message.js"; import { ScrcpyScrollController1_16 } from "./scroll.js"; export class ScrcpyOptions1_16 extends ScrcpyOptions { static DEFAULTS = { logLevel: ScrcpyLogLevel1_16.Debug, maxSize: 0, bitRate: 8_000_000, maxFps: 0, lockVideoOrientation: ScrcpyVideoOrientation1_16.Unlocked, tunnelForward: false, crop: undefined, sendFrameMeta: true, control: true, displayId: 0, showTouches: false, stayAwake: false, codecOptions: new CodecOptions(), }; static SERIALIZE_ORDER = [ "logLevel", "maxSize", "bitRate", "maxFps", "lockVideoOrientation", "tunnelForward", "crop", "sendFrameMeta", "control", "displayId", "showTouches", "stayAwake", "codecOptions", ]; static serialize(options, order) { return order.map((key) => toScrcpyOptionValue(options[key], "-")); } /** * Parse a fixed-length, null-terminated string. * @param stream The stream to read from * @param maxLength The maximum length of the string, including the null terminator, in bytes * @returns The parsed string, without the null terminator */ static async parseCString(stream, maxLength) { const buffer = await stream.readExactly(maxLength); // If null terminator is not found, `subarray(0, -1)` will remove the last byte // But since it's a invalid case, it's fine return decodeUtf8(buffer.subarray(0, buffer.indexOf(0))); } static async parseUint16BE(stream) { const buffer = await stream.readExactly(2); return getUint16BigEndian(buffer, 0); } static async parseUint32BE(stream) { const buffer = await stream.readExactly(4); return getUint32BigEndian(buffer, 0); } defaults = ScrcpyOptions1_16.DEFAULTS; get controlMessageTypes() { return SCRCPY_CONTROL_MESSAGE_TYPES_1_16; } #clipboardController; #clipboard; get clipboard() { return this.#clipboard; } constructor(init) { super(undefined, init, ScrcpyOptions1_16.DEFAULTS); this.#clipboard = new PushReadableStream((controller) => { this.#clipboardController = controller; }); } serialize() { return ScrcpyOptions1_16.serialize(this.value, ScrcpyOptions1_16.SERIALIZE_ORDER); } setListEncoders() { throw new Error("Not supported"); } setListDisplays() { // Set to an invalid value // Server will print valid values before crashing // (server will crash before opening sockets) this.value.displayId = -1; } parseEncoder() { throw new Error("Not supported"); } parseDisplay(line) { const match = line.match(/^\s+scrcpy --display (\d+)$/); if (match) { return { id: Number.parseInt(match[1], 10), }; } return undefined; } parseVideoStreamMetadata(stream) { return (async () => { const buffered = new BufferedReadableStream(stream); const metadata = { codec: ScrcpyVideoCodecId.H264, }; metadata.deviceName = await ScrcpyOptions1_16.parseCString(buffered, 64); metadata.width = await ScrcpyOptions1_16.parseUint16BE(buffered); metadata.height = await ScrcpyOptions1_16.parseUint16BE(buffered); return { stream: buffered.release(), metadata }; })(); } parseAudioStreamMetadata() { throw new Error("Not supported"); } async parseDeviceMessage(id, stream) { switch (id) { case 0: { const message = await ScrcpyClipboardDeviceMessage.deserialize(stream); await this.#clipboardController.enqueue(message.content); return true; } default: return false; } } createMediaStreamTransformer() { // Optimized path for video frames only if (!this.value.sendFrameMeta) { return new TransformStream({ transform(chunk, controller) { controller.enqueue({ type: "data", data: chunk, }); }, }); } const deserializeStream = new StructDeserializeStream(ScrcpyMediaStreamRawPacket); return { writable: deserializeStream.writable, readable: deserializeStream.readable.pipeThrough(new TransformStream({ transform(packet, controller) { if (packet.pts === SCRCPY_MEDIA_PACKET_FLAG_CONFIG) { controller.enqueue({ type: "configuration", data: packet.data, }); return; } controller.enqueue({ type: "data", pts: packet.pts, data: packet.data, }); }, })), }; } serializeInjectTouchControlMessage(message) { return ScrcpyInjectTouchControlMessage1_16.serialize(message); } serializeBackOrScreenOnControlMessage(message) { if (message.action === AndroidKeyEventAction.Down) { return ScrcpyBackOrScreenOnControlMessage1_16.serialize(message); } return undefined; } serializeSetClipboardControlMessage(message) { return ScrcpySetClipboardControlMessage1_15.serialize(message); } createScrollController() { return new ScrcpyScrollController1_16(); } } //# sourceMappingURL=options.js.map