@yume-chan/scrcpy
Version:
TypeScript implementation of Scrcpy.
179 lines • 6.71 kB
JavaScript
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