@yume-chan/adb-scrcpy
Version:
Use `@yume-chan/adb` to bootstrap `@yume-chan/scrcpy`.
136 lines • 4.96 kB
JavaScript
import { AdbReverseNotSupportedError, NOOP } from "@yume-chan/adb";
import { delay } from "@yume-chan/async";
import { BufferedReadableStream, PushReadableStream, } from "@yume-chan/stream-extra";
export const SCRCPY_SOCKET_NAME_PREFIX = "scrcpy";
export class AdbScrcpyConnection {
adb;
options;
socketName;
constructor(adb, options) {
this.adb = adb;
this.options = options;
this.socketName = this.getSocketName();
}
initialize() {
// pure virtual method
}
getSocketName() {
let socketName = "localabstract:" + SCRCPY_SOCKET_NAME_PREFIX;
if (this.options.scid !== undefined) {
socketName += "_" + this.options.scid.padStart(8, "0");
}
return socketName;
}
dispose() {
// pure virtual method
}
}
export 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) {
// Can't guarantee the stream will preserve message boundaries,
// so buffer the stream
const buffered = new BufferedReadableStream(stream.readable);
// Skip the dummy byte
// Google ADB forward tunnel listens on a socket on the computer,
// when a client connects to that socket, Google ADB will forward
// the connection to the socket on the device.
// However, connecting to that socket will always succeed immediately,
// which doesn't mean that Google ADB has connected to
// the socket on the device.
// Thus Scrcpy server sends a dummy byte to the socket, to let the client
// know that the connection is truly established.
await buffered.readExactly(1);
return {
readable: buffered.release(),
writable: stream.writable,
};
}
return stream;
}
catch {
// Maybe the server is still starting
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;
}
}
export class AdbScrcpyReverseConnection extends AdbScrcpyConnection {
#streams;
#address;
async initialize() {
// try to unbind first
await this.adb.reverse.remove(this.socketName).catch((e) => {
if (e instanceof AdbReverseNotSupportedError) {
throw e;
}
// Ignore other errors when unbinding
});
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() {
// Don't await this!
// `reverse.remove`'s response will never arrive
// before we read all pending data from Scrcpy streams
// NOOP: failed to remove reverse tunnel is not a big deal
this.adb.reverse.remove(this.#address).catch(NOOP);
}
}
//# sourceMappingURL=connection.js.map