@yume-chan/scrcpy
Version:
TypeScript implementation of Scrcpy.
236 lines • 8.2 kB
JavaScript
import { getUint32BigEndian } from "@yume-chan/no-data-view";
import { BufferedReadableStream, PushReadableStream, } from "@yume-chan/stream-extra";
import Struct, { placeholder } from "@yume-chan/struct";
import { CodecOptions, ScrcpyOptions1_16, ScrcpyUnsignedFloatFieldDefinition, } from "./1_16/index.js";
import { ScrcpyOptions1_21 } from "./1_21.js";
import { ScrcpyOptions1_24 } from "./1_24.js";
import { ScrcpyOptions1_25 } from "./1_25/index.js";
import { ScrcpyAudioCodec, ScrcpyVideoCodecId } from "./codec.js";
import { ScrcpyOptions } from "./types.js";
export const ScrcpyInjectTouchControlMessage2_0 = new Struct()
.uint8("type")
.uint8("action", placeholder())
.uint64("pointerId")
.uint32("pointerX")
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.field("pressure", ScrcpyUnsignedFloatFieldDefinition)
.uint32("actionButton")
.uint32("buttons");
export class ScrcpyInstanceId {
static NONE = new ScrcpyInstanceId(-1);
static random() {
// A random 31-bit unsigned integer
return new ScrcpyInstanceId((Math.random() * 0x80000000) | 0);
}
value;
constructor(value) {
this.value = value;
}
toOptionValue() {
if (this.value < 0) {
return undefined;
}
return this.value.toString(16);
}
}
export function omit(obj, keys) {
const result = {};
for (const key in obj) {
if (!keys.includes(key)) {
result[key] = obj[key];
}
}
return result;
}
export class ScrcpyOptions2_0 extends ScrcpyOptions {
static async parseAudioMetadata(stream, sendCodecMeta, mapMetadata, getOptionCodec) {
const buffered = new BufferedReadableStream(stream);
const buffer = await buffered.readExactly(4);
// Treat it as a 32-bit number for simpler comparisons
const codecMetadataValue = getUint32BigEndian(buffer, 0);
// Server will send `0x00_00_00_00` and `0x00_00_00_01` even if `sendCodecMeta` is false
switch (codecMetadataValue) {
case 0x00_00_00_00:
return {
type: "disabled",
};
case 0x00_00_00_01:
return {
type: "errored",
};
}
if (sendCodecMeta) {
return {
type: "success",
codec: mapMetadata(codecMetadataValue),
stream: buffered.release(),
};
}
return {
type: "success",
// Infer codec from `audioCodec` option
codec: getOptionCodec(),
stream: new PushReadableStream(async (controller) => {
// Put the first 4 bytes back
await controller.enqueue(buffer);
const stream = buffered.release();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
await controller.enqueue(value);
}
}),
};
}
static DEFAULTS = {
...omit(ScrcpyOptions1_24.DEFAULTS, [
"bitRate",
"codecOptions",
"encoderName",
]),
scid: ScrcpyInstanceId.NONE,
videoCodec: "h264",
videoBitRate: 8000000,
videoCodecOptions: new CodecOptions(),
videoEncoder: undefined,
audio: true,
audioCodec: "opus",
audioBitRate: 128000,
audioCodecOptions: new CodecOptions(),
audioEncoder: undefined,
listEncoders: false,
listDisplays: false,
sendCodecMeta: true,
};
get defaults() {
return ScrcpyOptions2_0.DEFAULTS;
}
constructor(init) {
super(ScrcpyOptions1_25, init, ScrcpyOptions2_0.DEFAULTS);
}
serialize() {
return ScrcpyOptions1_21.serialize(this.value, this.defaults);
}
setListEncoders() {
this.value.listEncoders = true;
}
setListDisplays() {
this.value.listDisplays = true;
}
parseEncoder(line) {
let match = line.match(/^\s+--video-codec=(\S+)\s+--video-encoder='([^']+)'$/);
if (match) {
return {
type: "video",
codec: match[1],
name: match[2],
};
}
match = line.match(/^\s+--audio-codec=(\S+)\s+--audio-encoder='([^']+)'$/);
if (match) {
return {
type: "audio",
codec: match[1],
name: match[2],
};
}
return undefined;
}
parseDisplay(line) {
const match = line.match(/^\s+--display=(\d+)\s+\(([^)]+)\)$/);
if (match) {
const display = {
id: Number.parseInt(match[1], 10),
};
if (match[2] !== "size unknown") {
display.resolution = match[2];
}
return display;
}
return undefined;
}
parseVideoStreamMetadata(stream) {
const { sendDeviceMeta, sendCodecMeta } = this.value;
if (!sendDeviceMeta && !sendCodecMeta) {
let codec;
switch (this.value.videoCodec) {
case "h264":
codec = ScrcpyVideoCodecId.H264;
break;
case "h265":
codec = ScrcpyVideoCodecId.H265;
break;
case "av1":
codec = ScrcpyVideoCodecId.AV1;
break;
}
return { stream, metadata: { codec } };
}
return (async () => {
const buffered = new BufferedReadableStream(stream);
// `sendDeviceMeta` now only contains device name,
// can't use `super.parseVideoStreamMetadata` here
let deviceName;
if (sendDeviceMeta) {
deviceName = await ScrcpyOptions1_16.parseCString(buffered, 64);
}
let codec;
let width;
let height;
if (sendCodecMeta) {
codec = await ScrcpyOptions1_16.parseUint32BE(buffered);
width = await ScrcpyOptions1_16.parseUint32BE(buffered);
height = await ScrcpyOptions1_16.parseUint32BE(buffered);
}
else {
switch (this.value.videoCodec) {
case "h264":
codec = ScrcpyVideoCodecId.H264;
break;
case "h265":
codec = ScrcpyVideoCodecId.H265;
break;
case "av1":
codec = ScrcpyVideoCodecId.AV1;
break;
}
}
return {
stream: buffered.release(),
metadata: { deviceName, codec, width, height },
};
})();
}
parseAudioStreamMetadata(stream) {
return ScrcpyOptions2_0.parseAudioMetadata(stream, this.value.sendCodecMeta, (value) => {
switch (value) {
case ScrcpyAudioCodec.RAW.metadataValue:
return ScrcpyAudioCodec.RAW;
case ScrcpyAudioCodec.OPUS.metadataValue:
return ScrcpyAudioCodec.OPUS;
case ScrcpyAudioCodec.AAC.metadataValue:
return ScrcpyAudioCodec.AAC;
default:
throw new Error(`Unknown audio codec metadata value: ${value}`);
}
}, () => {
switch (this.value.audioCodec) {
case "raw":
return ScrcpyAudioCodec.RAW;
case "opus":
return ScrcpyAudioCodec.OPUS;
case "aac":
return ScrcpyAudioCodec.AAC;
}
});
}
serializeInjectTouchControlMessage(message) {
return ScrcpyInjectTouchControlMessage2_0.serialize(message);
}
}
//# sourceMappingURL=2_0.js.map