consortium
Version:
Remote control and session sharing CLI for AI coding agents
512 lines (502 loc) • 14.9 kB
JavaScript
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 };