UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

512 lines (502 loc) • 14.9 kB
import { I as InspectStream, S as SplitStringStream } from './split-string-seuJ1msu.mjs'; import { l as ScrcpyVideoCodecId, q as h264ParseConfiguration, t as h265ParseConfiguration, A as Av1, D as DefaultServerPath, d as ScrcpyControlMessageWriter, O as toScrcpyOptionValue, i as ScrcpyOptions2_1 } from './h265-Dfh0u-Xx.mjs'; import { A as AdbReverseNotSupportedError, N as NOOP, S as StickyEventEmitter } from './reverse-BeYKDdbE.mjs'; import { T as TextDecoderStream } from './encoding-Dra722yg.mjs'; import { P as PushReadableStream, A as AbortController, W as WritableStream, t as tryCancel } from './consumable-GBIKq8FA.mjs'; import { B as BufferedReadableStream, c as ExactReadableEndedError } from './struct-deserialize-DqeMe-dl.mjs'; import './string-BC-7T2zc.mjs'; function delay(time) { return new Promise((resolve) => { globalThis.setTimeout(() => resolve(), time); }); } const SCRCPY_SOCKET_NAME_PREFIX = "scrcpy"; class AdbScrcpyConnection { adb; options; socketName; constructor(adb, options) { this.adb = adb; this.options = options; this.socketName = this.getSocketName(); } initialize() { } getSocketName() { let socketName = "localabstract:" + SCRCPY_SOCKET_NAME_PREFIX; if (this.options.scid !== void 0) { socketName += "_" + this.options.scid.padStart(8, "0"); } return socketName; } dispose() { } } class AdbScrcpyForwardConnection extends AdbScrcpyConnection { #disposed = false; #connect() { return this.adb.createSocket(this.socketName); } async #connectAndRetry(sendDummyByte) { for (let i = 0; !this.#disposed && i < 100; i += 1) { try { const stream = await this.#connect(); if (sendDummyByte) { const buffered = new BufferedReadableStream(stream.readable); await buffered.readExactly(1); return { readable: buffered.release(), writable: stream.writable }; } return stream; } catch { await delay(100); } } throw new Error(`Can't connect to server after 100 retries`); } async getStreams() { let { sendDummyByte } = this.options; const streams = {}; if (this.options.video) { const stream = await this.#connectAndRetry(sendDummyByte); streams.video = stream.readable; sendDummyByte = false; } if (this.options.audio) { const stream = await this.#connectAndRetry(sendDummyByte); streams.audio = stream.readable; sendDummyByte = false; } if (this.options.control) { const stream = await this.#connectAndRetry(sendDummyByte); streams.control = stream; sendDummyByte = false; } return streams; } dispose() { this.#disposed = true; } } class AdbScrcpyReverseConnection extends AdbScrcpyConnection { #streams; #address; async initialize() { await this.adb.reverse.remove(this.socketName).catch((e) => { if (e instanceof AdbReverseNotSupportedError) { throw e; } }); let queueController; const queue = new PushReadableStream((controller) => { queueController = controller; }); this.#streams = queue.getReader(); this.#address = await this.adb.reverse.add(this.socketName, async (socket) => { await queueController.enqueue(socket); }); } async #accept() { return (await this.#streams.read()).value; } async getStreams() { const streams = {}; if (this.options.video) { const stream = await this.#accept(); streams.video = stream.readable; } if (this.options.audio) { const stream = await this.#accept(); streams.audio = stream.readable; } if (this.options.control) { const stream = await this.#accept(); streams.control = stream; } return streams; } dispose() { this.adb.reverse.remove(this.#address).catch(NOOP); } } class AdbScrcpyVideoStream { #options; #metadata; get metadata() { return this.#metadata; } #stream; get stream() { return this.#stream; } #sizeChanged = new StickyEventEmitter(); get sizeChanged() { return this.#sizeChanged.event; } #width = 0; get width() { return this.#width; } #height = 0; get height() { return this.#height; } constructor(options, metadata, stream) { this.#options = options; this.#metadata = metadata; this.#stream = stream.pipeThrough(this.#options.createMediaStreamTransformer()).pipeThrough(new InspectStream((packet) => { if (packet.type === "configuration") { switch (metadata.codec) { case ScrcpyVideoCodecId.H264: this.#configureH264(packet.data); break; case ScrcpyVideoCodecId.H265: this.#configureH265(packet.data); break; } } else if (metadata.codec === ScrcpyVideoCodecId.AV1) { this.#configureAv1(packet.data); } })); } #configureH264(data) { const { croppedWidth, croppedHeight } = h264ParseConfiguration(data); this.#width = croppedWidth; this.#height = croppedHeight; this.#sizeChanged.fire({ width: croppedWidth, height: croppedHeight }); } #configureH265(data) { const { croppedWidth, croppedHeight } = h265ParseConfiguration(data); this.#width = croppedWidth; this.#height = croppedHeight; this.#sizeChanged.fire({ width: croppedWidth, height: croppedHeight }); } #configureAv1(data) { const parser = new Av1(data); const sequenceHeader = parser.searchSequenceHeaderObu(); if (!sequenceHeader) { return; } const { max_frame_width_minus_1, max_frame_height_minus_1 } = sequenceHeader; const width = max_frame_width_minus_1 + 1; const height = max_frame_height_minus_1 + 1; this.#width = width; this.#height = height; this.#sizeChanged.fire({ width, height }); } } function arrayToStream(array) { return new PushReadableStream(async (controller) => { for (const item of array) { await controller.enqueue(item); } }); } function concatStreams(...streams) { return new PushReadableStream(async (controller) => { for (const stream of streams) { const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } await controller.enqueue(value); } } }); } class AdbScrcpyExitedError extends Error { output; constructor(output) { super("scrcpy server exited prematurely"); this.output = output; } } class AdbScrcpyClient { static async pushServer(adb, file, filename = DefaultServerPath) { const sync = await adb.sync(); try { await sync.write({ filename, file }); } finally { await sync.dispose(); } } static async start(adb, path, options) { let connection; let process; try { try { connection = options.createConnection(adb); await connection.initialize(); } catch (e) { if (e instanceof AdbReverseNotSupportedError) { options.value.tunnelForward = true; connection = options.createConnection(adb); await connection.initialize(); } else { connection = void 0; throw e; } } const args = [ `CLASSPATH=${path}`, // Set environment variable "app_process", "/", // Parent dir (unused but required) "com.genymobile.scrcpy.Server", options.version, ...options.serialize() ]; if (options.spawner) { process = await options.spawner.spawn(args); } else { process = await adb.subprocess.noneProtocol.spawn(args); } const output = process.output.pipeThrough(new TextDecoderStream()).pipeThrough(new SplitStringStream("\n")); const lines = []; const abortController = new AbortController(); const pipe = output.pipeTo(new WritableStream({ write(chunk) { lines.push(chunk); } }), { signal: abortController.signal, preventCancel: true }).catch((e) => { if (abortController.signal.aborted) { return; } throw e; }); const streams = await Promise.race([ process.exited.then(() => { throw new AdbScrcpyExitedError(lines); }), connection.getStreams() ]); abortController.abort(); await pipe; return new AdbScrcpyClient({ options, process, output: concatStreams(arrayToStream(lines), output), videoStream: streams.video, audioStream: streams.audio, controlStream: streams.control }); } catch (e) { await process?.kill(); throw e; } finally { connection?.dispose(); } } /** * This method will modify the given `options`, * so don't reuse it elsewhere. */ static getEncoders(adb, path, options) { options.setListEncoders(); return options.getEncoders(adb, path); } /** * This method will modify the given `options`, * so don't reuse it elsewhere. */ static getDisplays(adb, path, options) { options.setListDisplays(); return options.getDisplays(adb, path); } #options; #process; #output; get output() { return this.#output; } get exited() { return this.#process.exited; } #videoStream; /** * Gets a `Promise` that resolves to the parsed video stream. * * On server version 2.1 and above, it will be `undefined` if * video is disabled by `options.video: false`. * * Note: if it's not `undefined`, it must be consumed to prevent * the connection from being blocked. */ get videoStream() { return this.#videoStream; } #audioStream; /** * Gets a `Promise` that resolves to the parsed audio stream. * * On server versions before 2.0, it will always be `undefined`. * On server version 2.0 and above, it will be `undefined` if * audio is disabled by `options.audio: false`. * * Note: if it's not `undefined`, it must be consumed to prevent * the connection from being blocked. */ get audioStream() { return this.#audioStream; } #controller; /** * Gets the control message writer. * * On server version 1.22 and above, it will be `undefined` if * control is disabled by `options.control: false`. */ get controller() { return this.#controller; } get clipboard() { return this.#options.clipboard; } constructor({ options, process, output, videoStream, audioStream, controlStream }) { this.#options = options; this.#process = process; this.#output = output; this.#videoStream = videoStream ? this.#createVideoStream(videoStream) : void 0; this.#audioStream = audioStream ? this.#createAudioStream(audioStream) : void 0; if (controlStream) { this.#controller = new ScrcpyControlMessageWriter(controlStream.writable.getWriter(), options); this.#parseDeviceMessages(controlStream.readable).catch(() => { }); } } async #parseDeviceMessages(controlStream) { const buffered = new BufferedReadableStream(controlStream); try { while (true) { let id; try { const result = await buffered.readExactly(1); id = result[0]; } catch (e) { if (e instanceof ExactReadableEndedError) { this.#options.deviceMessageParsers.close(); break; } throw e; } await this.#options.deviceMessageParsers.parse(id, buffered); } } catch (e) { this.#options.deviceMessageParsers.error(e); await tryCancel(buffered); } } async #createVideoStream(initialStream) { const { metadata, stream } = await this.#options.parseVideoStreamMetadata(initialStream); return new AdbScrcpyVideoStream(this.#options, metadata, stream); } async #createAudioStream(initialStream) { if (!this.#options.parseAudioStreamMetadata) { throw new Error("parsing audio stream is not supported in this version"); } const metadata = await this.#options.parseAudioStreamMetadata(initialStream); switch (metadata.type) { case "disabled": case "errored": return metadata; case "success": return { ...metadata, stream: metadata.stream.pipeThrough(this.#options.createMediaStreamTransformer()) }; default: throw new Error(`Unexpected audio metadata type ${metadata["type"]}`); } } async close() { await this.#process.kill(); } } async function getDisplays(adb, path, options) { try { const client = await AdbScrcpyClient.start(adb, path, options); await client.close(); throw new Error("Unexpected server output"); } catch (e) { if (e instanceof AdbScrcpyExitedError) { if (e.output[0]?.startsWith("[server] ERROR:")) { throw e; } const displays = []; for (const line of e.output) { const display = options.parseDisplay(line); if (display) { displays.push(display); } } return displays; } throw e; } } async function getEncoders(adb, path, options) { try { const client = await AdbScrcpyClient.start(adb, path, options); await client.close(); throw new Error("Unexpected server output"); } catch (e) { if (e instanceof AdbScrcpyExitedError) { if (e.output[0]?.startsWith("[server] ERROR:")) { throw e; } const encoders = []; for (const line of e.output) { const encoder = options.parseEncoder(line); if (encoder) { encoders.push(encoder); } } return encoders; } throw e; } } function createConnection(adb, options) { const connectionOptions = { scid: toScrcpyOptionValue(options.scid, void 0), video: options.video, audio: options.audio, control: options.control, sendDummyByte: options.sendDummyByte }; if (options.tunnelForward) { return new AdbScrcpyForwardConnection(adb, connectionOptions); } else { return new AdbScrcpyReverseConnection(adb, connectionOptions); } } class AdbScrcpyOptions2_1 extends ScrcpyOptions2_1 { version; spawner; constructor(init, clientOptions) { super(init); this.version = clientOptions?.version ?? "2.1"; this.spawner = clientOptions?.spawner; } getEncoders(adb, path) { return getEncoders(adb, path, this); } getDisplays(adb, path) { return getDisplays(adb, path, this); } createConnection(adb) { return createConnection(adb, this.value); } } export { AdbScrcpyClient, AdbScrcpyConnection, AdbScrcpyExitedError, AdbScrcpyForwardConnection, AdbScrcpyOptions2_1, AdbScrcpyReverseConnection, SCRCPY_SOCKET_NAME_PREFIX };