Loading apps/demo/src/pages/scrcpy.tsx +83 −11 Original line number Diff line number Diff line import { CommandBar, Dialog, Dropdown, ICommandBarItemProps, Icon, IconButton, IDropdownOption, LayerHost, Position, ProgressIndicator, SpinButton, Stack, Toggle, TooltipHost } from "@fluentui/react"; import { useId } from "@fluentui/react-hooks"; import { makeStyles } from "@griffel/react"; import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx"; import { observer } from "mobx-react-lite"; import { NextPage } from "next"; Loading Loading @@ -207,6 +208,10 @@ class ScrcpyPageState { width = 0; height = 0; rotate = 0; get rotatedWidth() { return state.rotate & 1 ? state.height : state.width; } get rotatedHeight() { return state.rotate & 1 ? state.width : state.height; } client: ScrcpyClient | undefined = undefined; Loading Loading @@ -252,10 +257,45 @@ class ScrcpyPageState { key: 'fullscreen', disabled: !this.running, iconProps: { iconName: Icons.FullScreenMaximize }, iconOnly: true, text: 'Fullscreen', onClick: () => { this.deviceView?.enterFullscreen(); }, }); result.push({ key: 'rotateDevice', disabled: !this.running, iconProps: { iconName: Icons.Orientation }, iconOnly: true, text: 'Rotate Device', onClick: () => { this.client!.rotateDevice(); }, }); result.push({ key: 'rotateVideoLeft', disabled: !this.running, iconProps: { iconName: Icons.RotateLeft }, iconOnly: true, text: 'Rotate Video Left', onClick: () => { this.rotate -= 1; if (this.rotate < 0) { this.rotate = 3; } } }); result.push({ key: 'rotateVideoRight', disabled: !this.running, iconProps: { iconName: Icons.RotateRight }, iconOnly: true, text: 'Rotate Video Right', onClick: () => { this.rotate = (this.rotate + 1) & 3; }, }); return result; } Loading @@ -266,6 +306,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.PanelBottom }, checked: this.navigationBarVisible, text: 'Navigation Bar', iconOnly: true, onClick: action(() => { this.navigationBarVisible = !this.navigationBarVisible; }), Loading @@ -275,6 +316,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.TextGrammarError }, checked: this.logVisible, text: 'Log', iconOnly: true, onClick: action(() => { this.logVisible = !this.logVisible; }), Loading @@ -284,6 +326,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.Settings }, checked: this.settingsVisible, text: 'Settings', iconOnly: true, onClick: action(() => { this.settingsVisible = !this.settingsVisible; }), Loading @@ -293,6 +336,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.Wand }, checked: this.demoModeVisible, text: 'Demo Mode', iconOnly: true, onClick: action(() => { this.demoModeVisible = !this.demoModeVisible; }), Loading Loading @@ -727,15 +771,29 @@ class ScrcpyPageState { }; calculatePointerPosition(clientX: number, clientY: number) { const view = this.rendererContainer!.getBoundingClientRect(); const pointerViewX = clientX - view.x; const pointerViewY = clientY - view.y; const pointerScreenX = clamp(pointerViewX / view.width, 0, 1) * this.width; const pointerScreenY = clamp(pointerViewY / view.height, 0, 1) * this.height; const viewRect = this.rendererContainer!.getBoundingClientRect(); let pointerViewX = clamp((clientX - viewRect.x) / viewRect.width, 0, 1); let pointerViewY = clamp((clientY - viewRect.y) / viewRect.height, 0, 1); if (this.rotate & 1) { ([pointerViewX, pointerViewY] = [pointerViewY, pointerViewX]); } switch (this.rotate) { case 1: pointerViewY = 1 - pointerViewY; break; case 2: pointerViewX = 1 - pointerViewX; pointerViewY = 1 - pointerViewY; break; case 3: pointerViewX = 1 - pointerViewX; break; } return { x: pointerScreenX, y: pointerScreenY, x: pointerViewX * this.width, y: pointerViewY * this.height, }; } Loading Loading @@ -880,7 +938,7 @@ const ConnectionDialog = observer(() => { ); }); const NavigationBar = observer(({ const NavigationBar = observer(function NavigationBar({ className, style, children Loading @@ -888,7 +946,7 @@ const NavigationBar = observer(({ className: string; style: CSSProperties; children: ReactNode; }) => { }) { if (!state.navigationBarVisible) { return null; } Loading Loading @@ -920,7 +978,15 @@ const NavigationBar = observer(({ ); }); const useClasses = makeStyles({ video: { transformOrigin: 'center center', }, }); const Scrcpy: NextPage = () => { const classes = useClasses(); return ( <Stack {...RouteStackProps}> <Head> Loading @@ -932,13 +998,19 @@ const Scrcpy: NextPage = () => { <Stack horizontal grow styles={{ root: { height: 0 } }}> <DeviceView ref={state.handleDeviceViewRef} width={state.width} height={state.height} width={state.rotatedWidth} height={state.rotatedHeight} BottomElement={NavigationBar} > <div ref={state.handleRendererContainerRef} tabIndex={-1} className={classes.video} style={{ width: state.width, height: state.height, transform: `translate(${(state.rotatedWidth - state.width) / 2}px, ${(state.rotatedHeight - state.height) / 2}px) rotate(${state.rotate * 90}deg)` }} onPointerDown={state.handlePointerDown} onPointerMove={state.handlePointerMove} onPointerUp={state.handlePointerUp} Loading apps/demo/src/utils/icons.tsx +7 −1 Original line number Diff line number Diff line import { registerIcons } from "@fluentui/react"; import { FilterRegular, AddCircleRegular, ArrowClockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, BugRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, MoreHorizontalRegular, NavigationRegular, PanelBottomRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, PowerRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WarningRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons'; import { AddCircleRegular, ArrowClockwiseRegular, ArrowRotateClockwiseRegular, ArrowRotateCounterclockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, BugRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FilterRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, MoreHorizontalRegular, NavigationRegular, OrientationRegular, PanelBottomRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, PowerRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WarningRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons'; const STYLE = {}; Loading @@ -26,6 +26,7 @@ export function register() { FullScreenMaximize: <FullScreenMaximizeRegular style={STYLE} />, Info: <InfoRegular style={STYLE} />, Navigation: <NavigationRegular style={STYLE} />, Orientation: <OrientationRegular style={STYLE} />, PanelBottom: <PanelBottomRegular style={STYLE} />, PersonFeedback: <PersonFeedbackRegular style={STYLE} />, Phone: <PhoneRegular style={STYLE} />, Loading @@ -34,6 +35,8 @@ export function register() { PlugConnected: <PlugConnectedRegular style={STYLE} />, PlugDisconnected: <PlugDisconnectedRegular style={STYLE} />, Power: <PowerRegular style={STYLE} />, RotateLeft: <ArrowRotateCounterclockwiseRegular style={STYLE} />, RotateRight: <ArrowRotateClockwiseRegular style={STYLE} />, Save: <SaveRegular style={STYLE} />, Settings: <SettingsRegular style={STYLE} />, Stop: <StopRegular style={STYLE} />, Loading Loading @@ -80,6 +83,7 @@ export default { FullScreenMaximize: 'FullScreenMaximize', Info: 'Info', Navigation: 'Navigation', Orientation: 'Orientation', PanelBottom: 'PanelBottom', PersonFeedback: 'PersonFeedback', Phone: 'Phone', Loading @@ -88,6 +92,8 @@ export default { PlugConnected: 'PlugConnected', PlugDisconnected: 'PlugDisconnected', Power: 'Power', RotateLeft: 'RotateLeft', RotateRight: 'RotateRight', Save: 'Save', Settings: 'Settings', Stop: 'Stop', Loading libraries/scrcpy/src/client.ts +27 −6 Original line number Diff line number Diff line import { AdbBufferedStream, AdbSubprocessNoneProtocol, DecodeUtf8Stream, InspectStream, TransformStream, WritableStream, type Adb, type AdbSocket, type AdbSubprocessProtocol, type ReadableStream, type WritableStreamDefaultWriter } from '@yume-chan/adb'; import { EventEmitter } from '@yume-chan/event'; import Struct from '@yume-chan/struct'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message.js'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, ScrcpySimpleControlMessage, type AndroidKeyEventAction } from './message.js'; import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options/index.js"; function* splitLines(text: string): Generator<string, void, void> { Loading Loading @@ -195,12 +195,21 @@ export class ScrcpyClient { return this._controlStreamWriter; } private getControlMessageTypeValue(type: ScrcpyControlMessageType) { const list = this.options.getControlMessageTypes(); const index = list.indexOf(type); if (index === -1) { throw new Error('Not supported'); } return index; } public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) { const controlStream = this.checkControlStream('injectKeyCode'); await controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({ ...message, type: ScrcpyControlMessageType.InjectKeycode, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectKeycode), })); } Loading @@ -208,7 +217,7 @@ export class ScrcpyClient { const controlStream = this.checkControlStream('injectText'); await controlStream.write(ScrcpyInjectTextControlMessage.serialize({ type: ScrcpyControlMessageType.InjectText, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectText), text, })); } Loading @@ -234,7 +243,7 @@ export class ScrcpyClient { this.lastTouchMessage = now; await controlStream.write(ScrcpyInjectTouchControlMessage.serialize({ ...message, type: ScrcpyControlMessageType.InjectTouch, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectTouch), screenWidth: this.screenWidth, screenHeight: this.screenHeight, })); Loading @@ -249,7 +258,7 @@ export class ScrcpyClient { const buffer = this.options!.serializeInjectScrollControlMessage({ ...message, type: ScrcpyControlMessageType.InjectScroll, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectScroll), screenWidth: this.screenWidth, screenHeight: this.screenHeight, }); Loading @@ -260,7 +269,7 @@ export class ScrcpyClient { const controlStream = this.checkControlStream('pressBackOrTurnOnScreen'); const buffer = this.options!.serializeBackOrScreenOnControlMessage({ type: ScrcpyControlMessageType.BackOrScreenOn, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.BackOrScreenOn), action, }); if (buffer) { Loading @@ -268,6 +277,18 @@ export class ScrcpyClient { } } private async sendSimpleControlMessage(type: ScrcpyControlMessageType, name: string) { const controlStream = this.checkControlStream(name); const buffer = ScrcpySimpleControlMessage.serialize({ type: this.getControlMessageTypeValue(type), }); await controlStream.write(buffer); } public async rotateDevice() { await this.sendSimpleControlMessage(ScrcpyControlMessageType.RotateDevice, 'rotateDevice'); } public async close() { // No need to close streams. Kill the process will destroy them from the other side. await this.process?.kill(); Loading libraries/scrcpy/src/message.ts +10 −3 Original line number Diff line number Diff line import Struct, { placeholder } from '@yume-chan/struct'; // https://github.com/Genymobile/scrcpy/blob/fa5b2a29e983a46b49531def9cf3d80c40c3de37/app/src/control_msg.h#L23 // For their message bodies, see https://github.com/Genymobile/scrcpy/blob/5c62f3419d252d10cd8c9cbb7c918b358b81f2d0/app/src/control_msg.c#L92 export enum ScrcpyControlMessageType { InjectKeycode, InjectText, Loading @@ -7,6 +9,7 @@ export enum ScrcpyControlMessageType { InjectScroll, BackOrScreenOn, ExpandNotificationPanel, ExpandSettingPanel, CollapseNotificationPanel, GetClipboard, SetClipboard, Loading @@ -31,9 +34,13 @@ export enum AndroidMotionEventAction { ButtonRelease, } export const ScrcpySimpleControlMessage = new Struct() .uint8('type'); export const ScrcpyInjectTouchControlMessage = new Struct() .uint8('type', ScrcpyControlMessageType.InjectTouch as const) .fields(ScrcpySimpleControlMessage) .uint8('action', placeholder<AndroidMotionEventAction>()) .uint64('pointerId') .uint32('pointerX') Loading @@ -47,7 +54,7 @@ export type ScrcpyInjectTouchControlMessage = typeof ScrcpyInjectTouchControlMes export const ScrcpyInjectTextControlMessage = new Struct() .uint8('type', ScrcpyControlMessageType.InjectText as const) .fields(ScrcpySimpleControlMessage) .uint32('length') .string('text', { lengthField: 'length' }); Loading Loading @@ -95,7 +102,7 @@ export enum AndroidKeyCode { export const ScrcpyInjectKeyCodeControlMessage = new Struct() .uint8('type', ScrcpyControlMessageType.InjectKeycode as const) .fields(ScrcpySimpleControlMessage) .uint8('action', placeholder<AndroidKeyEventAction>()) .uint32('keyCode') .uint32('repeat') Loading libraries/scrcpy/src/options/1_16/index.ts +20 −5 Original line number Diff line number Diff line import { StructDeserializeStream, TransformStream, type Adb } from "@yume-chan/adb"; import Struct, { placeholder } from "@yume-chan/struct"; import Struct from "@yume-chan/struct"; import type { AndroidCodecLevel, AndroidCodecProfile } from "../../codec.js"; import { ScrcpyClientConnection, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection, type ScrcpyClientConnectionOptions } from "../../connection.js"; import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../../message.js"; import { AndroidKeyEventAction, ScrcpyControlMessageType, ScrcpySimpleControlMessage } from "../../message.js"; import type { ScrcpyBackOrScreenOnEvent1_18 } from "../1_18.js"; import type { ScrcpyInjectScrollControlMessage1_22 } from "../1_22.js"; import { toScrcpyOptionValue, type ScrcpyOptions, type ScrcpyOptionValue, type VideoStreamPacket } from "../common.js"; Loading Loading @@ -119,12 +119,11 @@ export const VideoPacket = export const NO_PTS = BigInt(1) << BigInt(63); export const ScrcpyBackOrScreenOnEvent1_16 = new Struct() .uint8('type', placeholder<ScrcpyControlMessageType.BackOrScreenOn>()); ScrcpySimpleControlMessage; export const ScrcpyInjectScrollControlMessage1_16 = new Struct() .uint8('type', ScrcpyControlMessageType.InjectScroll as const) .fields(ScrcpySimpleControlMessage) .uint32('pointerX') .uint32('pointerY') .uint16('screenWidth') Loading Loading @@ -297,6 +296,22 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn }; } public getControlMessageTypes(): ScrcpyControlMessageType[] { return [ /* 0 */ ScrcpyControlMessageType.InjectKeycode, /* 1 */ ScrcpyControlMessageType.InjectText, /* 2 */ ScrcpyControlMessageType.InjectTouch, /* 3 */ ScrcpyControlMessageType.InjectScroll, /* 4 */ ScrcpyControlMessageType.BackOrScreenOn, /* 5 */ ScrcpyControlMessageType.ExpandNotificationPanel, /* 6 */ ScrcpyControlMessageType.CollapseNotificationPanel, /* 7 */ ScrcpyControlMessageType.GetClipboard, /* 8 */ ScrcpyControlMessageType.SetClipboard, /* 9 */ ScrcpyControlMessageType.SetScreenPowerMode, /* 10 */ ScrcpyControlMessageType.RotateDevice, ]; } public serializeBackOrScreenOnControlMessage( message: ScrcpyBackOrScreenOnEvent1_18, ) { Loading Loading
apps/demo/src/pages/scrcpy.tsx +83 −11 Original line number Diff line number Diff line import { CommandBar, Dialog, Dropdown, ICommandBarItemProps, Icon, IconButton, IDropdownOption, LayerHost, Position, ProgressIndicator, SpinButton, Stack, Toggle, TooltipHost } from "@fluentui/react"; import { useId } from "@fluentui/react-hooks"; import { makeStyles } from "@griffel/react"; import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx"; import { observer } from "mobx-react-lite"; import { NextPage } from "next"; Loading Loading @@ -207,6 +208,10 @@ class ScrcpyPageState { width = 0; height = 0; rotate = 0; get rotatedWidth() { return state.rotate & 1 ? state.height : state.width; } get rotatedHeight() { return state.rotate & 1 ? state.width : state.height; } client: ScrcpyClient | undefined = undefined; Loading Loading @@ -252,10 +257,45 @@ class ScrcpyPageState { key: 'fullscreen', disabled: !this.running, iconProps: { iconName: Icons.FullScreenMaximize }, iconOnly: true, text: 'Fullscreen', onClick: () => { this.deviceView?.enterFullscreen(); }, }); result.push({ key: 'rotateDevice', disabled: !this.running, iconProps: { iconName: Icons.Orientation }, iconOnly: true, text: 'Rotate Device', onClick: () => { this.client!.rotateDevice(); }, }); result.push({ key: 'rotateVideoLeft', disabled: !this.running, iconProps: { iconName: Icons.RotateLeft }, iconOnly: true, text: 'Rotate Video Left', onClick: () => { this.rotate -= 1; if (this.rotate < 0) { this.rotate = 3; } } }); result.push({ key: 'rotateVideoRight', disabled: !this.running, iconProps: { iconName: Icons.RotateRight }, iconOnly: true, text: 'Rotate Video Right', onClick: () => { this.rotate = (this.rotate + 1) & 3; }, }); return result; } Loading @@ -266,6 +306,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.PanelBottom }, checked: this.navigationBarVisible, text: 'Navigation Bar', iconOnly: true, onClick: action(() => { this.navigationBarVisible = !this.navigationBarVisible; }), Loading @@ -275,6 +316,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.TextGrammarError }, checked: this.logVisible, text: 'Log', iconOnly: true, onClick: action(() => { this.logVisible = !this.logVisible; }), Loading @@ -284,6 +326,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.Settings }, checked: this.settingsVisible, text: 'Settings', iconOnly: true, onClick: action(() => { this.settingsVisible = !this.settingsVisible; }), Loading @@ -293,6 +336,7 @@ class ScrcpyPageState { iconProps: { iconName: Icons.Wand }, checked: this.demoModeVisible, text: 'Demo Mode', iconOnly: true, onClick: action(() => { this.demoModeVisible = !this.demoModeVisible; }), Loading Loading @@ -727,15 +771,29 @@ class ScrcpyPageState { }; calculatePointerPosition(clientX: number, clientY: number) { const view = this.rendererContainer!.getBoundingClientRect(); const pointerViewX = clientX - view.x; const pointerViewY = clientY - view.y; const pointerScreenX = clamp(pointerViewX / view.width, 0, 1) * this.width; const pointerScreenY = clamp(pointerViewY / view.height, 0, 1) * this.height; const viewRect = this.rendererContainer!.getBoundingClientRect(); let pointerViewX = clamp((clientX - viewRect.x) / viewRect.width, 0, 1); let pointerViewY = clamp((clientY - viewRect.y) / viewRect.height, 0, 1); if (this.rotate & 1) { ([pointerViewX, pointerViewY] = [pointerViewY, pointerViewX]); } switch (this.rotate) { case 1: pointerViewY = 1 - pointerViewY; break; case 2: pointerViewX = 1 - pointerViewX; pointerViewY = 1 - pointerViewY; break; case 3: pointerViewX = 1 - pointerViewX; break; } return { x: pointerScreenX, y: pointerScreenY, x: pointerViewX * this.width, y: pointerViewY * this.height, }; } Loading Loading @@ -880,7 +938,7 @@ const ConnectionDialog = observer(() => { ); }); const NavigationBar = observer(({ const NavigationBar = observer(function NavigationBar({ className, style, children Loading @@ -888,7 +946,7 @@ const NavigationBar = observer(({ className: string; style: CSSProperties; children: ReactNode; }) => { }) { if (!state.navigationBarVisible) { return null; } Loading Loading @@ -920,7 +978,15 @@ const NavigationBar = observer(({ ); }); const useClasses = makeStyles({ video: { transformOrigin: 'center center', }, }); const Scrcpy: NextPage = () => { const classes = useClasses(); return ( <Stack {...RouteStackProps}> <Head> Loading @@ -932,13 +998,19 @@ const Scrcpy: NextPage = () => { <Stack horizontal grow styles={{ root: { height: 0 } }}> <DeviceView ref={state.handleDeviceViewRef} width={state.width} height={state.height} width={state.rotatedWidth} height={state.rotatedHeight} BottomElement={NavigationBar} > <div ref={state.handleRendererContainerRef} tabIndex={-1} className={classes.video} style={{ width: state.width, height: state.height, transform: `translate(${(state.rotatedWidth - state.width) / 2}px, ${(state.rotatedHeight - state.height) / 2}px) rotate(${state.rotate * 90}deg)` }} onPointerDown={state.handlePointerDown} onPointerMove={state.handlePointerMove} onPointerUp={state.handlePointerUp} Loading
apps/demo/src/utils/icons.tsx +7 −1 Original line number Diff line number Diff line import { registerIcons } from "@fluentui/react"; import { FilterRegular, AddCircleRegular, ArrowClockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, BugRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, MoreHorizontalRegular, NavigationRegular, PanelBottomRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, PowerRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WarningRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons'; import { AddCircleRegular, ArrowClockwiseRegular, ArrowRotateClockwiseRegular, ArrowRotateCounterclockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, BugRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FilterRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, MoreHorizontalRegular, NavigationRegular, OrientationRegular, PanelBottomRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, PowerRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WarningRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons'; const STYLE = {}; Loading @@ -26,6 +26,7 @@ export function register() { FullScreenMaximize: <FullScreenMaximizeRegular style={STYLE} />, Info: <InfoRegular style={STYLE} />, Navigation: <NavigationRegular style={STYLE} />, Orientation: <OrientationRegular style={STYLE} />, PanelBottom: <PanelBottomRegular style={STYLE} />, PersonFeedback: <PersonFeedbackRegular style={STYLE} />, Phone: <PhoneRegular style={STYLE} />, Loading @@ -34,6 +35,8 @@ export function register() { PlugConnected: <PlugConnectedRegular style={STYLE} />, PlugDisconnected: <PlugDisconnectedRegular style={STYLE} />, Power: <PowerRegular style={STYLE} />, RotateLeft: <ArrowRotateCounterclockwiseRegular style={STYLE} />, RotateRight: <ArrowRotateClockwiseRegular style={STYLE} />, Save: <SaveRegular style={STYLE} />, Settings: <SettingsRegular style={STYLE} />, Stop: <StopRegular style={STYLE} />, Loading Loading @@ -80,6 +83,7 @@ export default { FullScreenMaximize: 'FullScreenMaximize', Info: 'Info', Navigation: 'Navigation', Orientation: 'Orientation', PanelBottom: 'PanelBottom', PersonFeedback: 'PersonFeedback', Phone: 'Phone', Loading @@ -88,6 +92,8 @@ export default { PlugConnected: 'PlugConnected', PlugDisconnected: 'PlugDisconnected', Power: 'Power', RotateLeft: 'RotateLeft', RotateRight: 'RotateRight', Save: 'Save', Settings: 'Settings', Stop: 'Stop', Loading
libraries/scrcpy/src/client.ts +27 −6 Original line number Diff line number Diff line import { AdbBufferedStream, AdbSubprocessNoneProtocol, DecodeUtf8Stream, InspectStream, TransformStream, WritableStream, type Adb, type AdbSocket, type AdbSubprocessProtocol, type ReadableStream, type WritableStreamDefaultWriter } from '@yume-chan/adb'; import { EventEmitter } from '@yume-chan/event'; import Struct from '@yume-chan/struct'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message.js'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, ScrcpySimpleControlMessage, type AndroidKeyEventAction } from './message.js'; import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options/index.js"; function* splitLines(text: string): Generator<string, void, void> { Loading Loading @@ -195,12 +195,21 @@ export class ScrcpyClient { return this._controlStreamWriter; } private getControlMessageTypeValue(type: ScrcpyControlMessageType) { const list = this.options.getControlMessageTypes(); const index = list.indexOf(type); if (index === -1) { throw new Error('Not supported'); } return index; } public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) { const controlStream = this.checkControlStream('injectKeyCode'); await controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({ ...message, type: ScrcpyControlMessageType.InjectKeycode, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectKeycode), })); } Loading @@ -208,7 +217,7 @@ export class ScrcpyClient { const controlStream = this.checkControlStream('injectText'); await controlStream.write(ScrcpyInjectTextControlMessage.serialize({ type: ScrcpyControlMessageType.InjectText, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectText), text, })); } Loading @@ -234,7 +243,7 @@ export class ScrcpyClient { this.lastTouchMessage = now; await controlStream.write(ScrcpyInjectTouchControlMessage.serialize({ ...message, type: ScrcpyControlMessageType.InjectTouch, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectTouch), screenWidth: this.screenWidth, screenHeight: this.screenHeight, })); Loading @@ -249,7 +258,7 @@ export class ScrcpyClient { const buffer = this.options!.serializeInjectScrollControlMessage({ ...message, type: ScrcpyControlMessageType.InjectScroll, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectScroll), screenWidth: this.screenWidth, screenHeight: this.screenHeight, }); Loading @@ -260,7 +269,7 @@ export class ScrcpyClient { const controlStream = this.checkControlStream('pressBackOrTurnOnScreen'); const buffer = this.options!.serializeBackOrScreenOnControlMessage({ type: ScrcpyControlMessageType.BackOrScreenOn, type: this.getControlMessageTypeValue(ScrcpyControlMessageType.BackOrScreenOn), action, }); if (buffer) { Loading @@ -268,6 +277,18 @@ export class ScrcpyClient { } } private async sendSimpleControlMessage(type: ScrcpyControlMessageType, name: string) { const controlStream = this.checkControlStream(name); const buffer = ScrcpySimpleControlMessage.serialize({ type: this.getControlMessageTypeValue(type), }); await controlStream.write(buffer); } public async rotateDevice() { await this.sendSimpleControlMessage(ScrcpyControlMessageType.RotateDevice, 'rotateDevice'); } public async close() { // No need to close streams. Kill the process will destroy them from the other side. await this.process?.kill(); Loading
libraries/scrcpy/src/message.ts +10 −3 Original line number Diff line number Diff line import Struct, { placeholder } from '@yume-chan/struct'; // https://github.com/Genymobile/scrcpy/blob/fa5b2a29e983a46b49531def9cf3d80c40c3de37/app/src/control_msg.h#L23 // For their message bodies, see https://github.com/Genymobile/scrcpy/blob/5c62f3419d252d10cd8c9cbb7c918b358b81f2d0/app/src/control_msg.c#L92 export enum ScrcpyControlMessageType { InjectKeycode, InjectText, Loading @@ -7,6 +9,7 @@ export enum ScrcpyControlMessageType { InjectScroll, BackOrScreenOn, ExpandNotificationPanel, ExpandSettingPanel, CollapseNotificationPanel, GetClipboard, SetClipboard, Loading @@ -31,9 +34,13 @@ export enum AndroidMotionEventAction { ButtonRelease, } export const ScrcpySimpleControlMessage = new Struct() .uint8('type'); export const ScrcpyInjectTouchControlMessage = new Struct() .uint8('type', ScrcpyControlMessageType.InjectTouch as const) .fields(ScrcpySimpleControlMessage) .uint8('action', placeholder<AndroidMotionEventAction>()) .uint64('pointerId') .uint32('pointerX') Loading @@ -47,7 +54,7 @@ export type ScrcpyInjectTouchControlMessage = typeof ScrcpyInjectTouchControlMes export const ScrcpyInjectTextControlMessage = new Struct() .uint8('type', ScrcpyControlMessageType.InjectText as const) .fields(ScrcpySimpleControlMessage) .uint32('length') .string('text', { lengthField: 'length' }); Loading Loading @@ -95,7 +102,7 @@ export enum AndroidKeyCode { export const ScrcpyInjectKeyCodeControlMessage = new Struct() .uint8('type', ScrcpyControlMessageType.InjectKeycode as const) .fields(ScrcpySimpleControlMessage) .uint8('action', placeholder<AndroidKeyEventAction>()) .uint32('keyCode') .uint32('repeat') Loading
libraries/scrcpy/src/options/1_16/index.ts +20 −5 Original line number Diff line number Diff line import { StructDeserializeStream, TransformStream, type Adb } from "@yume-chan/adb"; import Struct, { placeholder } from "@yume-chan/struct"; import Struct from "@yume-chan/struct"; import type { AndroidCodecLevel, AndroidCodecProfile } from "../../codec.js"; import { ScrcpyClientConnection, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection, type ScrcpyClientConnectionOptions } from "../../connection.js"; import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../../message.js"; import { AndroidKeyEventAction, ScrcpyControlMessageType, ScrcpySimpleControlMessage } from "../../message.js"; import type { ScrcpyBackOrScreenOnEvent1_18 } from "../1_18.js"; import type { ScrcpyInjectScrollControlMessage1_22 } from "../1_22.js"; import { toScrcpyOptionValue, type ScrcpyOptions, type ScrcpyOptionValue, type VideoStreamPacket } from "../common.js"; Loading Loading @@ -119,12 +119,11 @@ export const VideoPacket = export const NO_PTS = BigInt(1) << BigInt(63); export const ScrcpyBackOrScreenOnEvent1_16 = new Struct() .uint8('type', placeholder<ScrcpyControlMessageType.BackOrScreenOn>()); ScrcpySimpleControlMessage; export const ScrcpyInjectScrollControlMessage1_16 = new Struct() .uint8('type', ScrcpyControlMessageType.InjectScroll as const) .fields(ScrcpySimpleControlMessage) .uint32('pointerX') .uint32('pointerY') .uint16('screenWidth') Loading Loading @@ -297,6 +296,22 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn }; } public getControlMessageTypes(): ScrcpyControlMessageType[] { return [ /* 0 */ ScrcpyControlMessageType.InjectKeycode, /* 1 */ ScrcpyControlMessageType.InjectText, /* 2 */ ScrcpyControlMessageType.InjectTouch, /* 3 */ ScrcpyControlMessageType.InjectScroll, /* 4 */ ScrcpyControlMessageType.BackOrScreenOn, /* 5 */ ScrcpyControlMessageType.ExpandNotificationPanel, /* 6 */ ScrcpyControlMessageType.CollapseNotificationPanel, /* 7 */ ScrcpyControlMessageType.GetClipboard, /* 8 */ ScrcpyControlMessageType.SetClipboard, /* 9 */ ScrcpyControlMessageType.SetScreenPowerMode, /* 10 */ ScrcpyControlMessageType.RotateDevice, ]; } public serializeBackOrScreenOnControlMessage( message: ScrcpyBackOrScreenOnEvent1_18, ) { Loading