Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 419a7559 authored by Simon Chan's avatar Simon Chan
Browse files

feat(scrcpy): support server version 2.1

parent ef583779
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "postinstall": "fetch-scrcpy-server 2.0 && node scripts/manifest.mjs",
        "postinstall": "fetch-scrcpy-server 2.1 && node scripts/manifest.mjs",
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
+1 −1
Original line number Diff line number Diff line
@@ -320,7 +320,7 @@ export class ScrcpyPageState {

            RECORD_STATE.recorder = new MatroskaMuxingRecorder();

            client.videoStream.then(({ stream, metadata }) => {
            client.videoStream!.then(({ stream, metadata }) => {
                runInAction(() => {
                    RECORD_STATE.recorder.videoMetadata = metadata;
                });
+42 −10
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ Similar to `@yume-chan/scrcpy`, this package supports multiple Scrcpy versions,
| 1.16~1.21 | `AdbScrcpyOptions1_16` |
| 1.22~1.25 | `AdbScrcpyOptions1_22` |
| 2.0       | `AdbScrcpyOptions2_0`  |
| 2.1       | `AdbScrcpyOptions2_1`  |

### Push server binary

@@ -73,9 +74,9 @@ To start the server, use the `AdbScrcpyClient.start()` method. It automatically
```js
import {
    AdbScrcpyClient,
    AdbScrcpyOptions2_0,
    AdbScrcpyOptions2_1,
    DEFAULT_SERVER_PATH,
    ScrcpyOptions2_0,
    ScrcpyOptions2_1,
} from "@yume-chan/scrcpy";
import SCRCPY_SERVER_VERSION from "@yume-chan/scrcpy/bin/version.js";

@@ -84,18 +85,40 @@ const client: AdbScrcpyClient = await AdbScrcpyClient.start(
    DEFAULT_SERVER_PATH,
    // If server binary was downloaded manually, must provide the correct version
    SCRCPY_SERVER_VERSION,
    new AdbScrcpyOptions2_0(
        ScrcpyOptions2_0({
    new AdbScrcpyOptions2_1(
        ScrcpyOptions2_1({
            // options
        })
    )
);

const stdout: ReadableStream<string> = client.stdout;

// `undefined` if `video: false` option was given
if (client.videoSteam) {
    const { metadata: videoMetadata, stream: videoPacketStream } =
        await client.videoStream;
const { metadata: audioMetadata, stream: audioPacketStream } =
    await client.audioStream;
}

// `undefined` if `audio: false` option was given
if (client.audioStream) {
    const metadata = await client.audioStream;
    switch (metadata.type) {
        case "disabled":
            // Audio not supported by device
            break;
        case "errored":
            // Other error when initializing audio
            break;
        case "success":
            // Audio packets in the codec specified in options
            const audioPacketStream: ReadableStream<ScrcpyMediaStreamPacket> =
                metadata.stream;
            break;
    }
}

// `undefined` if `control: false` option was given
const controlMessageWriter: ScrcpyControlMessageWriter | undefined =
    client.controlMessageWriter;
const deviceMessageStream: ReadableStream<ScrcpyDeviceMessage> | undefined =
@@ -110,7 +133,6 @@ client.close();
In Web Streams API, pipes will block its upstream when downstream's queue is full (back-pressure mechanism). If multiple streams are separated from the same source (for example, all Scrcpy streams are from the same USB or TCP connection), blocking one stream means blocking all of them, so it's important to always read from all streams, even if you don't care about their data.

```ts
// when using `AdbScrcpyClient`
stdout
    .pipeTo(
        new WritableStream<string>({
@@ -126,7 +148,7 @@ stdout

videoPacketStream
    .pipeTo(
        new WritableStream<ScrcpyVideoStreamPacket>({
        new WritableStream<ScrcpyMediaStreamPacket>({
            write: (packet) => {
                // Handle or ignore the video packet
            },
@@ -134,6 +156,16 @@ videoPacketStream
    )
    .catch(() => {});

audioPacketStream
    .pipeTo(
        new WritableStream<ScrcpyMediaStreamPacket>({
            write: (packet) => {
                // Handle or ignore the audio packet
            },
        })
    )
    .catch(() => {});

deviceMessageStream
    .pipeTo(
        new WritableStream<ScrcpyDeviceMessage>({
+5 −3
Original line number Diff line number Diff line
@@ -75,7 +75,7 @@ interface AdbScrcpyClientInit {
    process: AdbSubprocessProtocol;
    stdout: ReadableStream<string>;

    videoStream: ReadableStream<Uint8Array>;
    videoStream: ReadableStream<Uint8Array> | undefined;
    audioStream: ReadableStream<Uint8Array> | undefined;
    controlStream:
        | ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
@@ -259,7 +259,7 @@ export class AdbScrcpyClient {
        return this._screenHeight;
    }

    private _videoStream: Promise<AdbScrcpyVideoStream>;
    private _videoStream: Promise<AdbScrcpyVideoStream> | undefined;
    public get videoStream() {
        return this._videoStream;
    }
@@ -293,7 +293,9 @@ export class AdbScrcpyClient {
        this._process = process;
        this._stdout = stdout;

        this._videoStream = this.createVideoStream(videoStream);
        this._videoStream = videoStream
            ? this.createVideoStream(videoStream)
            : undefined;

        this._audioStream = audioStream
            ? this.createAudioStream(audioStream)
+59 −37
Original line number Diff line number Diff line
@@ -17,6 +17,10 @@ import type { ValueOrPromise } from "@yume-chan/struct";
export interface AdbScrcpyConnectionOptions {
    scid: number;

    video: boolean;

    audio: boolean;

    /**
     * Whether to create a control stream
     */
@@ -26,18 +30,14 @@ export interface AdbScrcpyConnectionOptions {
     * In forward tunnel mode, read a byte from video socket on start to detect connection issues
     */
    sendDummyByte: boolean;

    audio: boolean;
}

export const SCRCPY_SOCKET_NAME_PREFIX = "scrcpy";

export interface AdbScrcpyConnectionStreams {
    video: ReadableStream<Uint8Array>;
    audio: ReadableStream<Uint8Array> | undefined;
    control:
        | ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
        | undefined;
    video?: ReadableStream<Uint8Array>;
    audio?: ReadableStream<Uint8Array>;
    control?: ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>;
}

export abstract class AdbScrcpyConnection implements Disposable {
@@ -81,12 +81,25 @@ export class AdbScrcpyForwardConnection extends AdbScrcpyConnection {
        return this.adb.createSocket(this.socketName);
    }

    private async connectAndRetry(): Promise<
        ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
    > {
    private async connectAndRetry(
        sendDummyByte: boolean
    ): Promise<ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>> {
        for (let i = 0; !this._disposed && i < 100; i += 1) {
            try {
                return await this.connect();
                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
                    );
                    await buffered.readExactly(1);
                    return {
                        readable: buffered.release(),
                        writable: stream.writable,
                    };
                }
                return stream;
            } catch (e) {
                // Maybe the server is still starting
                await delay(100);
@@ -95,30 +108,30 @@ export class AdbScrcpyForwardConnection extends AdbScrcpyConnection {
        throw new Error(`Can't connect to server after 100 retries`);
    }

    private async connectVideoStream(): Promise<ReadableStream<Uint8Array>> {
        const { readable: stream } = await this.connectAndRetry();
        if (this.options.sendDummyByte) {
            // Can't guarantee the stream will preserve message boundaries,
            // so buffer the stream
            const buffered = new BufferedReadableStream(stream);
            await buffered.readExactly(1);
            return buffered.release();
        }
        return stream;
    }

    public override async getStreams(): Promise<AdbScrcpyConnectionStreams> {
        const video = await this.connectVideoStream();
        let { sendDummyByte } = this.options;

        const streams: AdbScrcpyConnectionStreams = {};

        if (this.options.video) {
            const video = await this.connectAndRetry(sendDummyByte);
            streams.video = video.readable;
            sendDummyByte = false;
        }

        const audio = this.options.audio
            ? (await this.connectAndRetry()).readable
            : undefined;
        if (this.options.audio) {
            const audio = await this.connectAndRetry(sendDummyByte);
            streams.audio = audio.readable;
            sendDummyByte = false;
        }

        const control = this.options.control
            ? await this.connectAndRetry()
            : undefined;
        if (this.options.control) {
            const control = await this.connectAndRetry(sendDummyByte);
            sendDummyByte = false;
            streams.control = control;
        }

        return { video, audio, control };
        return streams;
    }

    public override dispose(): void {
@@ -161,15 +174,24 @@ export class AdbScrcpyReverseConnection extends AdbScrcpyConnection {
    }

    public async getStreams(): Promise<AdbScrcpyConnectionStreams> {
        const { readable: video } = await this.accept();
        const streams: AdbScrcpyConnectionStreams = {};

        const audio = this.options.audio
            ? (await this.accept()).readable
            : undefined;
        if (this.options.video) {
            const video = await this.accept();
            streams.video = video.readable;
        }

        const control = this.options.control ? await this.accept() : undefined;
        if (this.options.audio) {
            const audio = await this.accept();
            streams.audio = audio.readable;
        }

        if (this.options.control) {
            const control = await this.accept();
            streams.control = control;
        }

        return { video, audio, control };
        return streams;
    }

    public override dispose() {
Loading