UNPKG

@yume-chan/scrcpy

Version:
236 lines 8.2 kB
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