feat(scrcpy): support server version 2.1

This commit is contained in:
Simon Chan 2023-06-22 12:58:47 +08:00
parent ef583779fa
commit 419a7559fe
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
16 changed files with 243 additions and 94 deletions

View file

@ -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",

View file

@ -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;
});

View file

@ -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;
const { metadata: videoMetadata, stream: videoPacketStream } =
// `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>({

View file

@ -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)

View file

@ -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 audio = this.options.audio
? (await this.connectAndRetry()).readable
: undefined;
const streams: AdbScrcpyConnectionStreams = {};
const control = this.options.control
? await this.connectAndRetry()
: undefined;
if (this.options.video) {
const video = await this.connectAndRetry(sendDummyByte);
streams.video = video.readable;
sendDummyByte = false;
}
return { video, audio, control };
if (this.options.audio) {
const audio = await this.connectAndRetry(sendDummyByte);
streams.audio = audio.readable;
sendDummyByte = false;
}
if (this.options.control) {
const control = await this.connectAndRetry(sendDummyByte);
sendDummyByte = false;
streams.control = 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;
}
return { video, audio, control };
if (this.options.control) {
const control = await this.accept();
streams.control = control;
}
return streams;
}
public override dispose() {

View file

@ -107,11 +107,12 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
adb,
{
scid: -1,
video: true,
audio: false,
// Old versions always have control stream no matter what the option is
// Pass `control: false` to `Connection` will disable the control stream
control: true,
sendDummyByte: true,
audio: false,
},
this.tunnelForwardOverride || this.value.tunnelForward
);

View file

@ -32,9 +32,10 @@ export class AdbScrcpyOptions1_22 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
adb,
{
scid: -1,
video: true,
audio: false,
control: this.value.control,
sendDummyByte: this.value.sendDummyByte,
audio: false,
},
this.tunnelForwardOverride || this.value.tunnelForward
);

View file

@ -9,27 +9,29 @@ import { AdbScrcpyClient, AdbScrcpyExitedError } from "../client.js";
import type { AdbScrcpyConnection } from "../connection.js";
import { AdbScrcpyOptions1_16 } from "./1_16.js";
import type { AdbScrcpyOptions } from "./types.js";
import { AdbScrcpyOptionsBase } from "./types.js";
export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2_0> {
public override async getEncoders(
public static async getEncoders(
adb: Adb,
path: string,
version: string
): Promise<ScrcpyEncoder[]> {
version: string,
options: AdbScrcpyOptions<object>
) {
try {
const client = await AdbScrcpyClient.start(
adb,
path,
version,
this
options
);
await client.close();
} catch (e) {
if (e instanceof AdbScrcpyExitedError) {
const encoders: ScrcpyEncoder[] = [];
for (const line of e.output) {
const encoder = this.parseEncoder(line);
const encoder = options.parseEncoder(line);
if (encoder) {
encoders.push(encoder);
}
@ -40,6 +42,14 @@ export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2
throw new Error("Unexpected error");
}
public override async getEncoders(
adb: Adb,
path: string,
version: string
): Promise<ScrcpyEncoder[]> {
return AdbScrcpyOptions2_0.getEncoders(adb, path, version, this);
}
public override getDisplays(
adb: Adb,
path: string,
@ -53,9 +63,10 @@ export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2
adb,
{
scid: this.value.scid.value,
video: true,
audio: this.value.audio,
control: this.value.control,
sendDummyByte: this.value.sendDummyByte,
audio: this.value.audio,
},
this.tunnelForwardOverride || this.value.tunnelForward
);

View file

@ -0,0 +1,44 @@
import type { Adb } from "@yume-chan/adb";
import type {
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyOptionsInit2_1,
} from "@yume-chan/scrcpy";
import type { AdbScrcpyConnection } from "../connection.js";
import { AdbScrcpyOptions1_16 } from "./1_16.js";
import { AdbScrcpyOptions2_0 } from "./2_0.js";
import { AdbScrcpyOptionsBase } from "./types.js";
export class AdbScrcpyOptions2_1 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2_1> {
public override async getEncoders(
adb: Adb,
path: string,
version: string
): Promise<ScrcpyEncoder[]> {
return AdbScrcpyOptions2_0.getEncoders(adb, path, version, this);
}
public override getDisplays(
adb: Adb,
path: string,
version: string
): Promise<ScrcpyDisplay[]> {
return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this);
}
public override createConnection(adb: Adb): AdbScrcpyConnection {
return AdbScrcpyOptions1_16.createConnection(
adb,
{
scid: this.value.scid.value,
video: this.value.video,
audio: this.value.audio,
control: this.value.control,
sendDummyByte: this.value.sendDummyByte,
},
this.tunnelForwardOverride || this.value.tunnelForward
);
}
}

View file

@ -1,3 +1,20 @@
{
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json"
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
"references": [
{
"path": "../adb/tsconfig.build.json"
},
{
"path": "../event/tsconfig.build.json"
},
{
"path": "../scrcpy/tsconfig.build.json"
},
{
"path": "../stream-extra/tsconfig.build.json"
},
{
"path": "../struct/tsconfig.build.json"
},
]
}

View file

@ -1,20 +1,5 @@
{
"references": [
{
"path": "../adb/tsconfig.build.json"
},
{
"path": "../event/tsconfig.build.json"
},
{
"path": "../scrcpy/tsconfig.build.json"
},
{
"path": "../stream-extra/tsconfig.build.json"
},
{
"path": "../struct/tsconfig.build.json"
},
{
"path": "./tsconfig.test.json"
},

View file

@ -67,7 +67,7 @@ npx fetch-scrcpy-server <version>
For example:
```
npx fetch-scrcpy-server 1.25
npx fetch-scrcpy-server 2.1
```
Add it to `scripts.postinstall` in your `package.json`, so running `npm install` automatically invokes the script:
@ -75,7 +75,7 @@ Add it to `scripts.postinstall` in your `package.json`, so running `npm install`
```json
{
"scripts": {
"postinstall": "fetch-scrcpy-server 1.25"
"postinstall": "fetch-scrcpy-server 2.1"
}
}
```
@ -87,7 +87,7 @@ It will also save the version number to `bin/version.js`.
```js
import SCRCPY_SERVER_VERSION from "@yume-chan/scrcpy/bin/version.js";
console.log(SCRCPY_SERVER_VERSION); // "1.25"
console.log(SCRCPY_SERVER_VERSION); // "2.1"
```
### Use the server binary
@ -171,6 +171,7 @@ The latest one may continue to work for future server versions, but there is no
| 1.24 | `ScrcpyOptions1_24` |
| 1.25 | `ScrcpyOptions1_25` |
| 2.0 | `ScrcpyOptions2_0` |
| 2.1 | `ScrcpyOptions2_1` |
## Reading and writing packets
@ -183,9 +184,9 @@ This packets operates on Web Streams API streams.
Requires a `ReadableStream<Uint8Array>` that reads from the video socket.
```ts
import { ScrcpyOptions1_25, ScrcpyVideoStreamPacket } from "@yume-chan/scrcpy";
import { ScrcpyOptions2_1, ScrcpyVideoStreamPacket } from "@yume-chan/scrcpy";
const options = new ScrcpyOptions1_25({
const options = new ScrcpyOptions2_1({
// use the same version and options when starting the server
});
@ -212,10 +213,10 @@ Control socket is optional if control is not enabled. Video socket and control s
```ts
import {
ScrcpyControlMessageWriter,
ScrcpyOptions1_25,
ScrcpyOptions2_1,
} from "@yume-chan/scrcpy";
const options = new ScrcpyOptions1_25({
const options = new ScrcpyOptions2_1({
// use the same version and options when starting the server
});
@ -235,10 +236,7 @@ controlMessageWriter.injectText("Hello World!");
Requires a `ReadableStream<Uint8Array>` that reads from the control socket.
```ts
import {
ScrcpyDeviceMessageDeserializeStream,
ScrcpyOptions1_24,
} from "@yume-chan/scrcpy";
import { ScrcpyDeviceMessageDeserializeStream } from "@yume-chan/scrcpy";
const controlStream: ReadableWritablePair<Uint8Array, Uint8Array>; // get the stream yourself
@ -253,7 +251,7 @@ const deviceMessageStream: ReadableStream<ScrcpyDeviceMessage> =
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`
// if using `AdbScrcpyClient`
stdout
.pipeTo(
new WritableStream<string>({

View file

@ -115,7 +115,7 @@ export class ScrcpyOptions1_18 extends ScrcpyOptionsBase<
);
}
public serialize(): string[] {
public override serialize(): string[] {
return ScrcpyOptions1_16.serialize(
this.value,
ScrcpyOptions1_18.SERIALIZE_ORDER

View file

@ -0,0 +1,35 @@
import { ScrcpyOptions1_21 } from "./1_21.js";
import type { ScrcpyOptionsInit2_0 } from "./2_0.js";
import { ScrcpyOptions2_0 } from "./2_0.js";
import { ScrcpyOptionsBase } from "./types.js";
export interface ScrcpyOptionsInit2_1 extends ScrcpyOptionsInit2_0 {
video?: boolean;
audioSource?: "output" | "mic";
}
export class ScrcpyOptions2_1 extends ScrcpyOptionsBase<
ScrcpyOptionsInit2_1,
ScrcpyOptions2_0
> {
public static readonly DEFAULTS = {
...ScrcpyOptions2_0.DEFAULTS,
video: true,
audioSource: "output",
} as const satisfies Required<ScrcpyOptionsInit2_1>;
public override get defaults(): Required<ScrcpyOptionsInit2_1> {
return ScrcpyOptions2_1.DEFAULTS;
}
public constructor(init: ScrcpyOptionsInit2_1) {
super(new ScrcpyOptions2_0(init), {
...ScrcpyOptions2_1.DEFAULTS,
...init,
});
}
public override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults);
}
}

View file

@ -7,6 +7,7 @@ export * from "./1_23.js";
export * from "./1_24.js";
export * from "./1_25/index.js";
export * from "./2_0.js";
export * from "./2_1.js";
export * from "./codec.js";
export * from "./latest.js";
export * from "./types.js";

View file

@ -1,6 +1,6 @@
import { ScrcpyLogLevel1_18, ScrcpyVideoOrientation1_18 } from "./1_18.js";
import type { ScrcpyOptionsInit2_0 } from "./2_0.js";
import { ScrcpyOptions2_0 } from "./2_0.js";
import type { ScrcpyOptionsInit2_1 } from "./2_1.js";
import { ScrcpyOptions2_1 } from "./2_1.js";
export const ScrcpyLogLevel = ScrcpyLogLevel1_18;
export type ScrcpyLogLevel = ScrcpyLogLevel1_18;
@ -8,7 +8,7 @@ export type ScrcpyLogLevel = ScrcpyLogLevel1_18;
export const ScrcpyVideoOrientation = ScrcpyVideoOrientation1_18;
export type ScrcpyVideoOrientation = ScrcpyVideoOrientation1_18;
export type ScrcpyOptionsInitLatest = ScrcpyOptionsInit2_0;
export class ScrcpyOptionsLatest extends ScrcpyOptions2_0 {}
export type ScrcpyOptionsInitLatest = ScrcpyOptionsInit2_1;
export class ScrcpyOptionsLatest extends ScrcpyOptions2_1 {}
export const ScrcpyLatestVersion = "2.0";
export const ScrcpyLatestVersion = "2.1";