@yume-chan/adb-scrcpy
Version:
Use `@yume-chan/adb` to bootstrap `@yume-chan/scrcpy`.
260 lines • 8.98 kB
JavaScript
import { AdbReverseNotSupportedError } from "@yume-chan/adb";
import { DefaultServerPath, ScrcpyControlMessageWriter, } from "@yume-chan/scrcpy";
import { AbortController, BufferedReadableStream, PushReadableStream, SplitStringStream, TextDecoderStream, tryCancel, WritableStream, } from "@yume-chan/stream-extra";
import { ExactReadableEndedError } from "@yume-chan/struct";
import { AdbScrcpyVideoStream } from "./video.js";
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);
}
}
});
}
export class AdbScrcpyExitedError extends Error {
output;
constructor(output) {
super("scrcpy server exited prematurely");
this.output = output;
}
}
export 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) {
// When reverse tunnel is not supported, try forward tunnel.
options.value.tunnelForward = true;
connection = options.createConnection(adb);
await connection.initialize();
}
else {
connection = undefined;
throw e;
}
}
// Use environment variable CLASSPATH instead of -cp flag
// This is the approach used by the official scrcpy C implementation
// See: https://github.com/Genymobile/scrcpy/blob/master/app/src/server.c
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"));
// Must read all streams, otherwise the whole connection will be blocked.
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);
}
get output() {
return this.
}
get exited() {
return this.
}
/**
* 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.
}
/**
* 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.
}
/**
* 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.
}
get clipboard() {
return this.
}
constructor({ options, process, output, videoStream, audioStream, controlStream, }) {
this.
this.
this.
this.
? this.
: undefined;
this.
? this.
: undefined;
if (controlStream) {
this.
this.
}
}
async
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.
break;
}
throw e;
}
await this.
}
}
catch (e) {
this.
await tryCancel(buffered);
}
}
async
const { metadata, stream } = await this.
return new AdbScrcpyVideoStream(this.
}
async
if (!this.
throw new Error("parsing audio stream is not supported in this version");
}
const metadata = await this.
switch (metadata.type) {
case "disabled":
case "errored":
return metadata;
case "success":
return {
...metadata,
stream: metadata.stream.pipeThrough(this.
};
default:
throw new Error(`Unexpected audio metadata type ${metadata["type"]}`);
}
}
async close() {
await this.
}
}
//# sourceMappingURL=client.js.map