Skip to content

Commit 24a5a58

Browse files
committed
fix(scrcpy): skip default value in 1.21 option serialization
workaround Genymobile/scrcpy#2841
1 parent 2f6a914 commit 24a5a58

File tree

6 files changed

+102
-138
lines changed

6 files changed

+102
-138
lines changed

apps/demo/pages/scrcpy.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,8 @@ class ScrcpyPageState {
444444
client.onSizeChanged(action((size) => {
445445
const { croppedWidth, croppedHeight, } = size;
446446

447+
this.log.push(`[client] Video size changed: ${croppedWidth}x${croppedHeight}`);
448+
447449
this.width = croppedWidth;
448450
this.height = croppedHeight;
449451

@@ -472,8 +474,8 @@ class ScrcpyPageState {
472474
});
473475

474476
runInAction(() => {
475-
this.log.push(`Server version: ${SCRCPY_SERVER_VERSION}`);
476-
this.log.push(`Server arguments: ${options.formatServerArguments()}`);
477+
this.log.push(`[client] Server version: ${SCRCPY_SERVER_VERSION}`);
478+
this.log.push(`[client] Server arguments: ${options.formatServerArguments().join(' ')}`);
477479
});
478480

479481
await client.start(

libraries/scrcpy/src/client.ts

+18-28
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Adb, AdbBufferedStream, AdbLegacyShell, AdbShell, DataEventEmitter } fr
22
import { PromiseResolver } from '@yume-chan/async';
33
import { EventEmitter } from '@yume-chan/event';
44
import Struct from '@yume-chan/struct';
5-
import { ScrcpyClientConnection } from "./connection";
65
import { AndroidKeyEventAction, AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectScrollControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage } from './message';
76
import { ScrcpyOptions } from "./options";
87
import { pushServer, PushServerOptions } from "./push-server";
@@ -74,13 +73,13 @@ export class ScrcpyClient {
7473
device: Adb,
7574
path: string,
7675
version: string,
77-
options: ScrcpyOptions
76+
options: ScrcpyOptions<any>
7877
): Promise<string[]> {
79-
const client = new ScrcpyClient(device);
80-
const encoderNameRegex = options.getOutputEncoderNameRegex();
81-
8278
const resolver = new PromiseResolver<string[]>();
79+
const encoderNameRegex = options.getOutputEncoderNameRegex();
8380
const encoders: string[] = [];
81+
82+
const client = new ScrcpyClient(device);
8483
client.onOutput((line) => {
8584
const match = line.match(encoderNameRegex);
8685
if (match) {
@@ -92,13 +91,16 @@ export class ScrcpyClient {
9291
resolver.resolve(encoders);
9392
});
9493

94+
// Provide an invalid encoder name
95+
// So the server will return all available encoders
96+
options.value.encoderName = '_';
97+
9598
// Scrcpy server will open connections, before initializing encoder
9699
// Thus although an invalid encoder name is given, the start process will success
97-
await client.startCore(
100+
await client.start(
98101
path,
99102
version,
100-
options.formatGetEncoderListArguments(),
101-
options.createConnection(device)
103+
options
102104
);
103105

104106
return resolver.promise;
@@ -138,19 +140,21 @@ export class ScrcpyClient {
138140
private readonly clipboardChangeEvent = new EventEmitter<string>();
139141
public get onClipboardChange() { return this.clipboardChangeEvent.event; }
140142

141-
private options: ScrcpyOptions | undefined;
143+
private options: ScrcpyOptions<any> | undefined;
142144
private sendingTouchMessage = false;
143145

144146
public constructor(device: Adb) {
145147
this.device = device;
146148
}
147149

148-
private async startCore(
150+
public async start(
149151
path: string,
150152
version: string,
151-
serverArguments: string[],
152-
connection: ScrcpyClientConnection
153-
): Promise<void> {
153+
options: ScrcpyOptions<any>
154+
) {
155+
this.options = options;
156+
157+
const connection = options.createConnection(this.device);
154158
let process: AdbShell | undefined;
155159

156160
try {
@@ -163,7 +167,7 @@ export class ScrcpyClient {
163167
/* unused */ '/',
164168
'com.genymobile.scrcpy.Server',
165169
version,
166-
...serverArguments
170+
...options.formatServerArguments(),
167171
],
168172
{
169173
// Scrcpy server doesn't split stdout and stderr,
@@ -201,20 +205,6 @@ export class ScrcpyClient {
201205
}
202206
}
203207

204-
public start(
205-
path: string,
206-
version: string,
207-
options: ScrcpyOptions
208-
) {
209-
this.options = options;
210-
return this.startCore(
211-
path,
212-
version,
213-
options.formatServerArguments(),
214-
options.createConnection(this.device)
215-
);
216-
}
217-
218208
private handleProcessOutput(data: ArrayBuffer) {
219209
const text = decodeUtf8(data);
220210
for (const line of splitLines(text)) {

libraries/scrcpy/src/options/1_16.ts

+48-72
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,30 @@ import Struct, { placeholder } from "@yume-chan/struct";
33
import { AndroidCodecLevel, AndroidCodecProfile } from "../codec";
44
import { ScrcpyClientConnection, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
55
import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../message";
6-
import { ScrcpyLogLevel, ScrcpyOptions, ScrcpyScreenOrientation, toScrcpyOption, ToScrcpyOption } from "./common";
6+
import { ScrcpyLogLevel, ScrcpyOptions, ScrcpyOptionValue, ScrcpyScreenOrientation, toScrcpyOptionValue } from "./common";
77

88
export interface CodecOptionsType {
99
profile: AndroidCodecProfile;
1010

1111
level: AndroidCodecLevel;
1212
}
1313

14-
export class CodecOptions implements ToScrcpyOption {
15-
public value: CodecOptionsType;
16-
17-
public constructor({
18-
profile = AndroidCodecProfile.Baseline,
19-
level = AndroidCodecLevel.Level4,
20-
}: Partial<CodecOptionsType>) {
21-
this.value = {
22-
profile,
23-
level,
24-
};
14+
export class CodecOptions implements ScrcpyOptionValue {
15+
public value: Partial<CodecOptionsType>;
16+
17+
public constructor(value: Partial<CodecOptionsType>) {
18+
this.value = value;
2519
}
2620

27-
public toScrcpyOption(): string {
28-
return Object.entries(this.value)
21+
public toOptionValue(): string | undefined {
22+
const entries = Object.entries(this.value)
23+
.filter(([key, value]) => value !== undefined);
24+
25+
if (entries.length === 0) {
26+
return undefined;
27+
}
28+
29+
return entries
2930
.map(([key, value]) => `${key}=${value}`)
3031
.join(',');
3132
}
@@ -53,9 +54,7 @@ export interface ScrcpyOptions1_16Type {
5354

5455
tunnelForward: boolean;
5556

56-
// Because Scrcpy 1.21 changed the empty value from '-' to '',
57-
// We mark properties which can be empty with `| undefined`
58-
crop: string | undefined;
57+
crop: string;
5958

6059
sendFrameMeta: boolean;
6160

@@ -67,60 +66,30 @@ export interface ScrcpyOptions1_16Type {
6766

6867
stayAwake: boolean;
6968

70-
codecOptions: CodecOptions | undefined;
69+
codecOptions: CodecOptions;
7170

72-
encoderName: string | undefined;
71+
encoderName: string;
7372
}
7473

7574
export const ScrcpyBackOrScreenOnEvent1_16 =
7675
new Struct()
7776
.uint8('type', placeholder<ScrcpyControlMessageType.BackOrScreenOn>());
7877

79-
export class ScrcpyOptions1_16<T extends ScrcpyOptions1_16Type = ScrcpyOptions1_16Type> implements ScrcpyOptions {
80-
public value: T;
81-
82-
public constructor({
83-
logLevel = ScrcpyLogLevel.Error,
84-
maxSize = 0,
85-
bitRate = 8_000_000,
86-
maxFps = 0,
87-
lockVideoOrientation = ScrcpyScreenOrientation.Unlocked,
88-
tunnelForward = false,
89-
crop,
90-
sendFrameMeta = true,
91-
control = true,
92-
displayId = 0,
93-
showTouches = false,
94-
stayAwake = true,
95-
codecOptions,
96-
encoderName,
97-
}: Partial<ScrcpyOptions1_16Type>) {
78+
export class ScrcpyOptions1_16<T extends ScrcpyOptions1_16Type = ScrcpyOptions1_16Type> implements ScrcpyOptions<T> {
79+
public value: Partial<T>;
80+
81+
public constructor(value: Partial<ScrcpyOptions1_16Type>) {
9882
if (new.target === ScrcpyOptions1_16 &&
99-
logLevel === ScrcpyLogLevel.Verbose) {
100-
logLevel = ScrcpyLogLevel.Debug;
83+
value.logLevel === ScrcpyLogLevel.Verbose) {
84+
value.logLevel = ScrcpyLogLevel.Debug;
10185
}
10286

10387
if (new.target === ScrcpyOptions1_16 &&
104-
lockVideoOrientation === ScrcpyScreenOrientation.Initial) {
105-
lockVideoOrientation = ScrcpyScreenOrientation.Unlocked;
88+
value.lockVideoOrientation === ScrcpyScreenOrientation.Initial) {
89+
value.lockVideoOrientation = ScrcpyScreenOrientation.Unlocked;
10690
}
10791

108-
this.value = {
109-
logLevel,
110-
maxSize,
111-
bitRate,
112-
maxFps,
113-
lockVideoOrientation,
114-
tunnelForward,
115-
crop,
116-
sendFrameMeta,
117-
control,
118-
displayId,
119-
showTouches,
120-
stayAwake,
121-
codecOptions,
122-
encoderName,
123-
} as T;
92+
this.value = value as Partial<T>;
12493
}
12594

12695
protected getArgumnetOrder(): (keyof T)[] {
@@ -142,22 +111,29 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptions1_16Type = ScrcpyOptions1_
142111
];
143112
}
144113

145-
public formatServerArguments(): string[] {
146-
return this.getArgumnetOrder().map(key => {
147-
return toScrcpyOption(this.value[key], '-');
148-
});
114+
protected getDefaultValue(): T {
115+
return {
116+
logLevel: ScrcpyLogLevel.Error,
117+
maxSize: 0,
118+
bitRate: 8_000_000,
119+
maxFps: 0,
120+
lockVideoOrientation: ScrcpyScreenOrientation.Unlocked,
121+
tunnelForward: false,
122+
crop: '-',
123+
sendFrameMeta: true,
124+
control: true,
125+
displayId: 0,
126+
showTouches: false,
127+
stayAwake: true,
128+
codecOptions: new CodecOptions({}),
129+
encoderName: '-',
130+
} as T;
149131
}
150132

151-
public formatGetEncoderListArguments(): string[] {
152-
return this.getArgumnetOrder().map(key => {
153-
if (key === 'encoderName') {
154-
// Provide an invalid encoder name
155-
// So the server will return all available encoders
156-
return '_';
157-
}
158-
159-
return toScrcpyOption(this.value[key], '-');
160-
});
133+
public formatServerArguments(): string[] {
134+
const defaults = this.getDefaultValue();
135+
return this.getArgumnetOrder()
136+
.map(key => toScrcpyOptionValue(this.value[key] || defaults[key], '-'));
161137
}
162138

163139
public createConnection(device: Adb): ScrcpyClientConnection {

libraries/scrcpy/src/options/1_18.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@ export const ScrcpyBackOrScreenOnEvent1_18 =
1313
.uint8('action', placeholder<AndroidKeyEventAction>());
1414

1515
export class ScrcpyOptions1_18<T extends ScrcpyOptions1_18Type = ScrcpyOptions1_18Type> extends ScrcpyOptions1_16<T> {
16-
constructor(init: Partial<ScrcpyOptions1_18Type>) {
17-
super(init);
18-
const {
19-
powerOffOnClose = false,
20-
} = init;
21-
this.value.powerOffOnClose = powerOffOnClose;
16+
constructor(value: Partial<ScrcpyOptions1_18Type>) {
17+
super(value);
2218
}
2319

2420
protected override getArgumnetOrder(): (keyof T)[] {
2521
return super.getArgumnetOrder().concat(['powerOffOnClose']);
2622
}
2723

24+
protected override getDefaultValue(): T {
25+
return {
26+
...super.getDefaultValue(),
27+
powerOffOnClose: false,
28+
};
29+
}
30+
2831
public override getOutputEncoderNameRegex(): RegExp {
2932
return /\s+scrcpy --encoder '(.*?)'/;
3033
}

libraries/scrcpy/src/options/1_21.ts

+11-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ScrcpyOptions1_18, ScrcpyOptions1_18Type } from './1_18';
2-
import { toScrcpyOption } from "./common";
2+
import { toScrcpyOptionValue } from "./common";
33

44
export interface ScrcpyOptions1_21Type extends ScrcpyOptions1_18Type {
55
clipboardAutosync?: boolean;
@@ -12,26 +12,19 @@ function toSnakeCase(input: string): string {
1212
export class ScrcpyOptions1_21<T extends ScrcpyOptions1_21Type = ScrcpyOptions1_21Type> extends ScrcpyOptions1_18<T> {
1313
public constructor(init: Partial<ScrcpyOptions1_21Type>) {
1414
super(init);
15-
const {
16-
clipboardAutosync = true,
17-
} = init;
18-
this.value.clipboardAutosync = clipboardAutosync;
1915
}
2016

21-
public override formatServerArguments(): string[] {
22-
return Object.entries(this.value)
23-
.map(([key, value]) => {
24-
return `${toSnakeCase(key)}=${toScrcpyOption(value, '')}`;
25-
});
17+
protected override getDefaultValue(): T {
18+
return {
19+
...super.getDefaultValue(),
20+
clipboardAutosync: true,
21+
};
2622
}
2723

28-
public override formatGetEncoderListArguments(): string[] {
29-
return Object.entries(this.value).map(([key, value]) => {
30-
if (key === 'encoderName') {
31-
value = '_';
32-
}
33-
34-
return `${toSnakeCase(key)}=${toScrcpyOption(value, '')}`;
35-
});
24+
public override formatServerArguments(): string[] {
25+
return Object.entries(this.value)
26+
.map(([key, value]) => [key, toScrcpyOptionValue(value, undefined)] as const)
27+
.filter((pair): pair is [string, string] => pair[1] !== undefined)
28+
.map(([key, value]) => `${toSnakeCase(key)}=${value}`);
3629
}
3730
}

0 commit comments

Comments
 (0)