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

Unverified Commit b08ad297 authored by Simon Chan's avatar Simon Chan
Browse files

feat(scrcpy): support Scrcpy 2.0 (#495)

parent 3a3f4b88
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@
        "CLSE",
        "CNXN",
        "colour",
        "Demuxer",
        "Deserialization",
        "DESERIALIZERS",
        "ebml",
@@ -23,6 +24,7 @@
        "genymobile",
        "Genymobile's",
        "getprop",
        "Golomb",
        "griffel",
        "keyof",
        "laggy",
@@ -39,9 +41,11 @@
        "PKCS",
        "ponyfill",
        "runtimes",
        "scid",
        "Scrcpy",
        "sendrecv",
        "sideload",
        "Sodb",
        "streamsaver",
        "struct",
        "struct's",
@@ -97,5 +101,6 @@
    },
    "[typescriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    }
    },
    "explorer.sortOrder": "mixed"
}
+11 −8
Original line number Diff line number Diff line
@@ -3,29 +3,31 @@
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "postinstall": "fetch-scrcpy-server 1.25 && node scripts/manifest.mjs",
        "postinstall": "fetch-scrcpy-server 2.0 && node scripts/manifest.mjs",
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
    },
    "dependencies": {
        "@fluentui/react": "^8.106.5",
        "@fluentui/react-file-type-icons": "^8.8.12",
        "@fluentui/react-hooks": "^8.6.19",
        "@fluentui/react": "^8.106.7",
        "@fluentui/react-file-type-icons": "^8.8.13",
        "@fluentui/react-hooks": "^8.6.20",
        "@fluentui/react-icons": "^2.0.196",
        "@fluentui/style-utilities": "^8.9.5",
        "@fluentui/style-utilities": "^8.9.6",
        "@griffel/react": "^1.5.5",
        "@yume-chan/adb": "workspace:^0.0.19",
        "@yume-chan/adb-backend-direct-sockets": "workspace:^0.0.9",
        "@yume-chan/adb-backend-webusb": "workspace:^0.0.19",
        "@yume-chan/adb-backend-ws": "workspace:^0.0.9",
        "@yume-chan/adb-credential-web": "workspace:^0.0.19",
        "@yume-chan/adb-scrcpy": "workspace:^0.0.19",
        "@yume-chan/android-bin": "workspace:^0.0.19",
        "@yume-chan/aoa": "workspace:^0.0.18",
        "@yume-chan/async": "^2.2.0",
        "@yume-chan/b-tree": "workspace:^0.0.16",
        "@yume-chan/event": "workspace:^0.0.19",
        "@yume-chan/pcm-player": "workspace:^0.0.19",
        "@yume-chan/scrcpy": "workspace:^0.0.19",
        "@yume-chan/scrcpy-decoder-tinyh264": "workspace:^0.0.19",
        "@yume-chan/scrcpy-decoder-webcodecs": "workspace:^0.0.19",
@@ -49,13 +51,14 @@
        "@mdx-js/loader": "^2.2.1",
        "@mdx-js/react": "^2.2.1",
        "@next/mdx": "^13.2.4",
        "@types/node": "^18.15.0",
        "@types/react": "18.0.28",
        "@types/dom-webcodecs": "^0.1.6",
        "@types/node": "^18.15.3",
        "@types/react": "18.0.33",
        "@yume-chan/next-pwa": "5.6.0-mod.2",
        "eslint": "^8.36.0",
        "eslint-config-next": "13.2.4",
        "prettier": "^2.8.4",
        "source-map-loader": "^4.0.1",
        "typescript": "^4.9.4"
        "typescript": "^5.0.3"
    }
}
+0 −2
Original line number Diff line number Diff line
@@ -4,8 +4,6 @@ self.addEventListener("install", () => {
    self.skipWaiting();
});

console.log("updated");

self.addEventListener("activate", (event) => {
    const url = serviceWorker.scriptURL;
    const baseUrl = url.substring(0, url.lastIndexOf("/"));
+119 −0
Original line number Diff line number Diff line
import { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
import { TransformStream } from "@yume-chan/stream-extra";

export class AacDecodeStream extends TransformStream<
    ScrcpyMediaStreamPacket,
    Float32Array[]
> {
    constructor(config: AudioDecoderConfig) {
        let decoder: AudioDecoder;
        super({
            start(controller) {
                decoder = new AudioDecoder({
                    error(error) {
                        console.log("audio decoder error: ", error);
                        controller.error(error);
                    },
                    output(output) {
                        controller.enqueue(
                            Array.from({ length: 2 }, (_, i) => {
                                const options: AudioDataCopyToOptions = {
                                    // AAC decodes to "f32-planar",
                                    // converting to another format may cause audio glitches on Chrome.
                                    format: "f32-planar",
                                    planeIndex: i,
                                };
                                const buffer = new Float32Array(
                                    output.allocationSize(options) /
                                        Float32Array.BYTES_PER_ELEMENT
                                );
                                output.copyTo(buffer, options);
                                return buffer;
                            })
                        );
                    },
                });
            },
            transform(chunk) {
                switch (chunk.type) {
                    case "configuration":
                        // https://www.w3.org/TR/webcodecs-aac-codec-registration/#audiodecoderconfig-description
                        // Raw AAC stream needs `description` to be set.
                        decoder.configure({
                            ...config,
                            description: chunk.data,
                        });
                        break;
                    case "data":
                        decoder.decode(
                            new EncodedAudioChunk({
                                data: chunk.data,
                                type: "key",
                                timestamp: 0,
                            })
                        );
                }
            },
            async flush() {
                await decoder!.flush();
            },
        });
    }
}

export class OpusDecodeStream extends TransformStream<
    ScrcpyMediaStreamPacket,
    Float32Array
> {
    constructor(config: AudioDecoderConfig) {
        let decoder: AudioDecoder;
        super({
            start(controller) {
                decoder = new AudioDecoder({
                    error(error) {
                        console.log("audio decoder error: ", error);
                        controller.error(error);
                    },
                    output(output) {
                        // Opus decodes to "f32",
                        // converting to another format may cause audio glitches on Chrome.
                        const options: AudioDataCopyToOptions = {
                            format: "f32",
                            planeIndex: 0,
                        };
                        const buffer = new Float32Array(
                            output.allocationSize(options) /
                                Float32Array.BYTES_PER_ELEMENT
                        );
                        output.copyTo(buffer, options);
                        controller.enqueue(buffer);
                    },
                });
                decoder.configure(config);
            },
            transform(chunk) {
                switch (chunk.type) {
                    case "configuration":
                        // configuration data is a opus-in-ogg identification header,
                        // but stream data is raw opus,
                        // so it has no use here.
                        break;
                    case "data":
                        if (chunk.data.length === 0) {
                            break;
                        }
                        decoder.decode(
                            new EncodedAudioChunk({
                                type: "key",
                                timestamp: 0,
                                data: chunk.data,
                            })
                        );
                }
            },
            async flush() {
                await decoder!.flush();
            },
        });
    }
}
+15 −69
Original line number Diff line number Diff line
@@ -10,33 +10,20 @@ import {
} from "@yume-chan/scrcpy";
import { action, computed } from "mobx";
import { observer } from "mobx-react-lite";
import { GLOBAL_STATE } from "../../state";
import { Icons } from "../../utils";
import { CommandBarSpacerItem } from "../command-bar-spacer-item";
import { ExternalLink } from "../external-link";
import { RECORD_STATE } from "./recorder";
import { SETTING_STATE } from "./settings";
import { STATE } from "./state";

const ITEMS = computed(() => {
    const result: ICommandBarItemProps[] = [];

    if (!STATE.running) {
        result.push({
            key: "start",
            disabled: !GLOBAL_STATE.device,
            iconProps: { iconName: Icons.Play },
            text: "Start",
            onClick: STATE.start as VoidFunction,
        });
    } else {
    result.push({
        key: "stop",
        iconProps: { iconName: Icons.Stop },
        text: "Stop",
        onClick: STATE.stop as VoidFunction,
    });
    }

    result.push(
        RECORD_STATE.recording
@@ -100,13 +87,13 @@ const ITEMS = computed(() => {
                STATE.fullScreenContainer!.focus();

                // TODO: Auto repeat when holding
                await STATE.client?.controlMessageSerializer!.injectKeyCode({
                await STATE.client?.controlMessageWriter!.injectKeyCode({
                    action: AndroidKeyEventAction.Down,
                    keyCode: AndroidKeyCode.VolumeUp,
                    repeat: 0,
                    metaState: 0,
                });
                await STATE.client?.controlMessageSerializer!.injectKeyCode({
                await STATE.client?.controlMessageWriter!.injectKeyCode({
                    action: AndroidKeyEventAction.Up,
                    keyCode: AndroidKeyCode.VolumeUp,
                    repeat: 0,
@@ -123,13 +110,13 @@ const ITEMS = computed(() => {
            onClick: (async () => {
                STATE.fullScreenContainer!.focus();

                await STATE.client?.controlMessageSerializer!.injectKeyCode({
                await STATE.client?.controlMessageWriter!.injectKeyCode({
                    action: AndroidKeyEventAction.Down,
                    keyCode: AndroidKeyCode.VolumeDown,
                    repeat: 0,
                    metaState: 0,
                });
                await STATE.client?.controlMessageSerializer!.injectKeyCode({
                await STATE.client?.controlMessageWriter!.injectKeyCode({
                    action: AndroidKeyEventAction.Up,
                    keyCode: AndroidKeyCode.VolumeDown,
                    repeat: 0,
@@ -146,13 +133,13 @@ const ITEMS = computed(() => {
            onClick: (async () => {
                STATE.fullScreenContainer!.focus();

                await STATE.client?.controlMessageSerializer!.injectKeyCode({
                await STATE.client?.controlMessageWriter!.injectKeyCode({
                    action: AndroidKeyEventAction.Down,
                    keyCode: AndroidKeyCode.VolumeMute,
                    repeat: 0,
                    metaState: 0,
                });
                await STATE.client?.controlMessageSerializer!.injectKeyCode({
                await STATE.client?.controlMessageWriter!.injectKeyCode({
                    action: AndroidKeyEventAction.Up,
                    keyCode: AndroidKeyCode.VolumeMute,
                    repeat: 0,
@@ -172,7 +159,7 @@ const ITEMS = computed(() => {
            onClick: () => {
                STATE.fullScreenContainer!.focus();

                STATE.client!.controlMessageSerializer!.rotateDevice();
                STATE.client!.controlMessageWriter!.rotateDevice();
            },
        },
        {
@@ -214,7 +201,7 @@ const ITEMS = computed(() => {
            onClick: () => {
                STATE.fullScreenContainer!.focus();

                STATE.client!.controlMessageSerializer!.setScreenPowerMode(
                STATE.client!.controlMessageWriter!.setScreenPowerMode(
                    AndroidScreenPowerMode.Off
                );
            },
@@ -228,7 +215,7 @@ const ITEMS = computed(() => {
            onClick: () => {
                STATE.fullScreenContainer!.focus();

                STATE.client!.controlMessageSerializer!.setScreenPowerMode(
                STATE.client!.controlMessageWriter!.setScreenPowerMode(
                    AndroidScreenPowerMode.Normal
                );
            },
@@ -282,17 +269,6 @@ const ITEMS = computed(() => {
                STATE.logVisible = !STATE.logVisible;
            }),
        },
        {
            key: "Settings",
            iconProps: { iconName: Icons.Settings },
            canCheck: true,
            checked: SETTING_STATE.settingsVisible,
            text: "Settings",
            iconOnly: true,
            onClick: action(() => {
                SETTING_STATE.settingsVisible = !SETTING_STATE.settingsVisible;
            }),
        },
        {
            key: "DemoMode",
            iconProps: { iconName: Icons.Wand },
@@ -303,36 +279,6 @@ const ITEMS = computed(() => {
            onClick: action(() => {
                STATE.demoModeVisible = !STATE.demoModeVisible;
            }),
        },
        {
            key: "info",
            iconProps: { iconName: Icons.Info },
            iconOnly: true,
            text: "About",
            tooltipHostProps: {
                content: (
                    <>
                        <p>
                            <ExternalLink
                                href="https://github.com/Genymobile/scrcpy"
                                spaceAfter
                            >
                                Scrcpy
                            </ExternalLink>
                            developed by Genymobile can display the screen with
                            low latency (1~2 frames) and control the device, all
                            without root access.
                        </p>
                        <p>
                            This is a TypeScript implementation of the client
                            part. Paired with official pre-built server binary.
                        </p>
                    </>
                ),
                calloutProps: {
                    calloutMaxWidth: 300,
                },
            },
        }
    );

Loading