fix(scrcpy): fix UHID output stream from server version 2.6

This commit also replaced `ScrcpyOptions`'s `parseDeviceMessage`/`endDeviceMessageStream` methods with `deviceMessageParsers`. If you are using that two methods directly, you need to move to the new API
This commit is contained in:
Simon Chan 2025-06-21 18:28:33 +08:00
parent eff718ce36
commit 7edd616b43
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
28 changed files with 341 additions and 555 deletions

View file

@ -0,0 +1,6 @@
---
"@yume-chan/adb-scrcpy": patch
"@yume-chan/scrcpy": patch
---
Fix UHID output stream doesn't work from server version 2.6

View file

@ -25,6 +25,7 @@ import {
PushReadableStream,
SplitStringStream,
TextDecoderStream,
tryCancel,
WritableStream,
} from "@yume-chan/stream-extra";
import { ExactReadableEndedError } from "@yume-chan/struct";
@ -321,22 +322,24 @@ export class AdbScrcpyClient<TOptions extends AdbScrcpyOptions<object>> {
const buffered = new BufferedReadableStream(controlStream);
try {
while (true) {
let type: number;
let id: number;
try {
const result = await buffered.readExactly(1);
type = result[0]!;
id = result[0]!;
} catch (e) {
if (e instanceof ExactReadableEndedError) {
this.#options.endDeviceMessageStream();
this.#options.deviceMessageParsers.close();
break;
}
throw e;
}
await this.#options.parseDeviceMessage(type, buffered);
await this.#options.deviceMessageParsers.parse(id, buffered);
}
} catch (e) {
this.#options.endDeviceMessageStream(e);
buffered.cancel(e).catch(() => {});
this.#options.deviceMessageParsers.error(e);
await tryCancel(buffered);
}
}

View file

@ -16,6 +16,8 @@ export class ClipboardStream
{
#controller: PushReadableStreamController<string>;
readonly id = 0;
constructor() {
let controller!: PushReadableStreamController<string>;
super((controller_) => {
@ -24,13 +26,9 @@ export class ClipboardStream
this.#controller = controller;
}
async parse(id: number, stream: AsyncExactReadable): Promise<boolean> {
if (id === 0) {
async parse(_id: number, stream: AsyncExactReadable): Promise<undefined> {
const message = await ClipboardDeviceMessage.deserialize(stream);
await this.#controller.enqueue(message.content);
return true;
}
return false;
}
close() {

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -10,6 +9,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -47,11 +47,18 @@ export class ScrcpyOptions1_15 implements ScrcpyOptions<Init> {
return this.#clipboard;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control) {
this.#clipboard = new ClipboardStream();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
}
}
@ -73,25 +80,6 @@ export class ScrcpyOptions1_15 implements ScrcpyOptions<Init> {
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard!.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard!.error(e);
} else {
this.#clipboard!.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -12,6 +11,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -54,11 +54,18 @@ export class ScrcpyOptions1_17
return this.#clipboard;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control) {
this.#clipboard = new ClipboardStream();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
}
}
@ -88,25 +95,6 @@ export class ScrcpyOptions1_17
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard!.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard!.error(e);
} else {
this.#clipboard!.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -12,6 +11,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -54,11 +54,18 @@ export class ScrcpyOptions1_18
return this.#clipboard;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control) {
this.#clipboard = new ClipboardStream();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
}
}
@ -88,25 +95,6 @@ export class ScrcpyOptions1_18
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard!.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard!.error(e);
} else {
this.#clipboard!.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -29,18 +29,15 @@ export class AckClipboardHandler implements ScrcpyDeviceMessageParser {
#closed = false;
async parse(id: number, stream: AsyncExactReadable) {
if (id !== 1) {
return false;
}
readonly id = 1;
async parse(_id: number, stream: AsyncExactReadable): Promise<undefined> {
const message = await AckClipboardDeviceMessage.deserialize(stream);
const resolver = this.#resolvers.get(message.sequence);
if (resolver) {
resolver.resolve();
this.#resolvers.delete(message.sequence);
}
return true;
}
close(): void {

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -12,6 +11,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -55,12 +55,22 @@ export class ScrcpyOptions1_21
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -90,31 +100,6 @@ export class ScrcpyOptions1_21
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -12,6 +11,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -55,12 +55,22 @@ export class ScrcpyOptions1_22
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -90,31 +100,6 @@ export class ScrcpyOptions1_22
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -12,6 +11,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -55,12 +55,22 @@ export class ScrcpyOptions1_23
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -90,31 +100,6 @@ export class ScrcpyOptions1_23
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -12,6 +11,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -55,12 +55,22 @@ export class ScrcpyOptions1_24
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -90,31 +100,6 @@ export class ScrcpyOptions1_24
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
@ -12,6 +11,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -55,12 +55,22 @@ export class ScrcpyOptions1_25
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -90,31 +100,6 @@ export class ScrcpyOptions1_25
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -56,12 +56,22 @@ export class ScrcpyOptions2_0
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -97,31 +107,6 @@ export class ScrcpyOptions2_0
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -56,12 +56,22 @@ export class ScrcpyOptions2_1<TVideo extends boolean>
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -97,31 +107,6 @@ export class ScrcpyOptions2_1<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -56,6 +56,11 @@ export class ScrcpyOptions2_2<TVideo extends boolean>
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -64,8 +69,13 @@ export class ScrcpyOptions2_2<TVideo extends boolean>
}
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -101,31 +111,6 @@ export class ScrcpyOptions2_2<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -56,6 +56,11 @@ export class ScrcpyOptions2_3<TVideo extends boolean>
#ackClipboardHandler: AckClipboardHandler | undefined;
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -64,8 +69,13 @@ export class ScrcpyOptions2_3<TVideo extends boolean>
}
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
}
@ -101,31 +111,6 @@ export class ScrcpyOptions2_3<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -23,6 +23,8 @@ export class UHidOutputStream
{
#controller: PushReadableStreamController<UHidOutputDeviceMessage>;
readonly id = 2;
constructor() {
let controller!: PushReadableStreamController<UHidOutputDeviceMessage>;
super((controller_) => {
@ -31,14 +33,9 @@ export class UHidOutputStream
this.#controller = controller;
}
async parse(id: number, stream: AsyncExactReadable): Promise<boolean> {
if (id !== 2) {
return false;
}
async parse(_id: number, stream: AsyncExactReadable): Promise<undefined> {
const message = await UHidOutputDeviceMessage.deserialize(stream);
await this.#controller.enqueue(message);
return true;
}
close() {

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -67,6 +67,11 @@ export class ScrcpyOptions2_4<TVideo extends boolean>
return this.#uHidOutput;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -76,11 +81,18 @@ export class ScrcpyOptions2_4<TVideo extends boolean>
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
this.#uHidOutput = new UHidOutputStream();
this.#uHidOutput = this.#deviceMessageParsers.add(
new UHidOutputStream(),
);
}
}
@ -116,37 +128,6 @@ export class ScrcpyOptions2_4<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
if (await this.#uHidOutput?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
this.#uHidOutput?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
this.#uHidOutput?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -67,6 +67,11 @@ export class ScrcpyOptions2_6<TVideo extends boolean>
return this.#uHidOutput;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -80,11 +85,18 @@ export class ScrcpyOptions2_6<TVideo extends boolean>
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
this.#uHidOutput = new UHidOutputStream();
this.#uHidOutput = this.#deviceMessageParsers.add(
new UHidOutputStream(),
);
}
}
@ -120,31 +132,6 @@ export class ScrcpyOptions2_6<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,19 +1,17 @@
import type { StructInit } from "@yume-chan/struct";
import { buffer, string, struct, u16, u8 } from "@yume-chan/struct";
import { ScrcpyControlMessageType } from "../../base/control-message-type.js";
import type { ScrcpyUHidCreateControlMessage } from "../../latest.js";
export const UHidCreateControlMessage = /* #__PURE__ */ (() =>
struct(
export const UHidCreateControlMessage = struct(
{
type: u8(ScrcpyControlMessageType.UHidCreate),
type: u8,
id: u16,
name: string(u8),
data: buffer(u16),
},
{ littleEndian: false },
))();
);
export type UHidCreateControlMessage = StructInit<
typeof UHidCreateControlMessage

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -67,6 +67,11 @@ export class ScrcpyOptions2_7<TVideo extends boolean>
return this.#uHidOutput;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -80,11 +85,18 @@ export class ScrcpyOptions2_7<TVideo extends boolean>
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
this.#uHidOutput = new UHidOutputStream();
this.#uHidOutput = this.#deviceMessageParsers.add(
new UHidOutputStream(),
);
}
}
@ -120,31 +132,6 @@ export class ScrcpyOptions2_7<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -67,6 +67,11 @@ export class ScrcpyOptions3_0<TVideo extends boolean>
return this.#uHidOutput;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -80,11 +85,18 @@ export class ScrcpyOptions3_0<TVideo extends boolean>
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
this.#uHidOutput = new UHidOutputStream();
this.#uHidOutput = this.#deviceMessageParsers.add(
new UHidOutputStream(),
);
}
}
@ -120,31 +132,6 @@ export class ScrcpyOptions3_0<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,13 +1,11 @@
import type { StructInit } from "@yume-chan/struct";
import { buffer, string, struct, u16, u8 } from "@yume-chan/struct";
import { ScrcpyControlMessageType } from "../../base/control-message-type.js";
import type { ScrcpyUHidCreateControlMessage } from "../../latest.js";
export const UHidCreateControlMessage = /* #__PURE__ */ (() =>
struct(
export const UHidCreateControlMessage = struct(
{
type: u8(ScrcpyControlMessageType.UHidCreate),
type: u8,
id: u16,
vendorId: u16,
productId: u16,
@ -15,7 +13,7 @@ export const UHidCreateControlMessage = /* #__PURE__ */ (() =>
data: buffer(u16),
},
{ littleEndian: false },
))();
);
export type UHidCreateControlMessage = StructInit<
typeof UHidCreateControlMessage

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -67,6 +67,11 @@ export class ScrcpyOptions3_1<TVideo extends boolean>
return this.#uHidOutput;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -80,11 +85,18 @@ export class ScrcpyOptions3_1<TVideo extends boolean>
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
this.#uHidOutput = new UHidOutputStream();
this.#uHidOutput = this.#deviceMessageParsers.add(
new UHidOutputStream(),
);
}
}
@ -120,31 +132,6 @@ export class ScrcpyOptions3_1<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
@ -13,6 +12,7 @@ import type {
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import { ScrcpyDeviceMessageParsers } from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
@ -67,6 +67,11 @@ export class ScrcpyOptions3_2<TVideo extends boolean>
return this.#uHidOutput;
}
#deviceMessageParsers = new ScrcpyDeviceMessageParsers();
get deviceMessageParsers() {
return this.#deviceMessageParsers;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
@ -80,11 +85,18 @@ export class ScrcpyOptions3_2<TVideo extends boolean>
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
this.#clipboard = this.#deviceMessageParsers.add(
new ClipboardStream(),
);
this.#ackClipboardHandler = this.#deviceMessageParsers.add(
new AckClipboardHandler(),
);
}
this.#uHidOutput = new UHidOutputStream();
this.#uHidOutput = this.#deviceMessageParsers.add(
new UHidOutputStream(),
);
}
}
@ -120,31 +132,6 @@ export class ScrcpyOptions3_2<TVideo extends boolean>
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -1,9 +1,58 @@
import type { AsyncExactReadable } from "@yume-chan/struct";
export interface ScrcpyDeviceMessageParser {
parse(id: number, stream: AsyncExactReadable): Promise<boolean>;
readonly id: number | readonly number[];
parse(id: number, stream: AsyncExactReadable): Promise<undefined>;
close(): void;
error(e?: unknown): void;
}
export class ScrcpyDeviceMessageParsers {
#parsers: ScrcpyDeviceMessageParser[] = [];
get parsers(): readonly ScrcpyDeviceMessageParser[] {
return this.#parsers;
}
#add(id: number, parser: ScrcpyDeviceMessageParser) {
if (this.#parsers[id]) {
throw new Error(`Duplicate parser for id ${id}`);
}
this.#parsers[id] = parser;
}
add<T extends ScrcpyDeviceMessageParser>(parser: T): T {
if (Array.isArray(parser.id)) {
for (const id of parser.id) {
this.#add(id as number, parser);
}
} else {
this.#add(parser.id as number, parser);
}
return parser;
}
async parse(id: number, stream: AsyncExactReadable): Promise<undefined> {
const parser = this.#parsers[id];
if (!parser) {
throw new Error(`Unknown device message id ${id}`);
}
return parser.parse(id, stream);
}
close() {
for (const parser of this.#parsers) {
parser.close();
}
}
error(e?: unknown) {
for (const parser of this.#parsers) {
parser.error(e);
}
}
}

View file

@ -1,6 +1,5 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyBackOrScreenOnControlMessage,
@ -12,6 +11,7 @@ import type {
import type { ScrcpyAudioStreamMetadata } from "./audio.js";
import type { ScrcpyControlMessageType } from "./control-message-type.js";
import type { ScrcpyDeviceMessageParsers } from "./device-message.js";
import type { ScrcpyDisplay } from "./display.js";
import type { ScrcpyEncoder } from "./encoder.js";
import type { ScrcpyMediaStreamPacket } from "./media.js";
@ -29,6 +29,8 @@ export interface ScrcpyOptions<T extends object> {
| ReadableStream<ScrcpyUHidOutputDeviceMessage>
| undefined;
readonly deviceMessageParsers: ScrcpyDeviceMessageParsers;
serialize(): string[];
setListDisplays(): void;
@ -43,13 +45,6 @@ export interface ScrcpyOptions<T extends object> {
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyAudioStreamMetadata>;
parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void>;
endDeviceMessageStream(e?: unknown): void;
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket

View file

@ -20,11 +20,11 @@ export class ScrcpyControlMessageTypeMap {
return value;
}
fillMessageType<T extends { type: ScrcpyControlMessageType }>(
fillMessageType<T extends { type: number }>(
message: Omit<T, "type">,
type: T["type"],
type: ScrcpyControlMessageType,
): T {
(message as T).type = this.get(type) as ScrcpyControlMessageType;
(message as T).type = this.get(type);
return message as T;
}
}