Loading .vscode/settings.json +6 −1 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ "CLSE", "CNXN", "colour", "Demuxer", "Deserialization", "DESERIALIZERS", "ebml", Loading @@ -23,6 +24,7 @@ "genymobile", "Genymobile's", "getprop", "Golomb", "griffel", "keyof", "laggy", Loading @@ -39,9 +41,11 @@ "PKCS", "ponyfill", "runtimes", "scid", "Scrcpy", "sendrecv", "sideload", "Sodb", "streamsaver", "struct", "struct's", Loading Loading @@ -97,5 +101,6 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } }, "explorer.sortOrder": "mixed" } apps/demo/package.json +11 −8 Original line number Diff line number Diff line Loading @@ -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", Loading @@ -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" } } apps/demo/public/StreamSaver/sw.js +0 −2 Original line number Diff line number Diff line Loading @@ -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("/")); Loading apps/demo/src/components/scrcpy/audio-decode-stream.ts 0 → 100644 +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(); }, }); } } apps/demo/src/components/scrcpy/command-bar.tsx +15 −69 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading @@ -172,7 +159,7 @@ const ITEMS = computed(() => { onClick: () => { STATE.fullScreenContainer!.focus(); STATE.client!.controlMessageSerializer!.rotateDevice(); STATE.client!.controlMessageWriter!.rotateDevice(); }, }, { Loading Loading @@ -214,7 +201,7 @@ const ITEMS = computed(() => { onClick: () => { STATE.fullScreenContainer!.focus(); STATE.client!.controlMessageSerializer!.setScreenPowerMode( STATE.client!.controlMessageWriter!.setScreenPowerMode( AndroidScreenPowerMode.Off ); }, Loading @@ -228,7 +215,7 @@ const ITEMS = computed(() => { onClick: () => { STATE.fullScreenContainer!.focus(); STATE.client!.controlMessageSerializer!.setScreenPowerMode( STATE.client!.controlMessageWriter!.setScreenPowerMode( AndroidScreenPowerMode.Normal ); }, Loading Loading @@ -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 }, Loading @@ -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 Loading
.vscode/settings.json +6 −1 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ "CLSE", "CNXN", "colour", "Demuxer", "Deserialization", "DESERIALIZERS", "ebml", Loading @@ -23,6 +24,7 @@ "genymobile", "Genymobile's", "getprop", "Golomb", "griffel", "keyof", "laggy", Loading @@ -39,9 +41,11 @@ "PKCS", "ponyfill", "runtimes", "scid", "Scrcpy", "sendrecv", "sideload", "Sodb", "streamsaver", "struct", "struct's", Loading Loading @@ -97,5 +101,6 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } }, "explorer.sortOrder": "mixed" }
apps/demo/package.json +11 −8 Original line number Diff line number Diff line Loading @@ -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", Loading @@ -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" } }
apps/demo/public/StreamSaver/sw.js +0 −2 Original line number Diff line number Diff line Loading @@ -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("/")); Loading
apps/demo/src/components/scrcpy/audio-decode-stream.ts 0 → 100644 +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(); }, }); } }
apps/demo/src/components/scrcpy/command-bar.tsx +15 −69 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading @@ -172,7 +159,7 @@ const ITEMS = computed(() => { onClick: () => { STATE.fullScreenContainer!.focus(); STATE.client!.controlMessageSerializer!.rotateDevice(); STATE.client!.controlMessageWriter!.rotateDevice(); }, }, { Loading Loading @@ -214,7 +201,7 @@ const ITEMS = computed(() => { onClick: () => { STATE.fullScreenContainer!.focus(); STATE.client!.controlMessageSerializer!.setScreenPowerMode( STATE.client!.controlMessageWriter!.setScreenPowerMode( AndroidScreenPowerMode.Off ); }, Loading @@ -228,7 +215,7 @@ const ITEMS = computed(() => { onClick: () => { STATE.fullScreenContainer!.focus(); STATE.client!.controlMessageSerializer!.setScreenPowerMode( STATE.client!.controlMessageWriter!.setScreenPowerMode( AndroidScreenPowerMode.Normal ); }, Loading Loading @@ -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 }, Loading @@ -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