@9b9387/android-stream-scrcpy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +351 -0
  3. package/assets/scrcpy-server-v4.0.jar +0 -0
  4. package/dist/backend/adb/scrcpy-adb-client.d.ts +13 -0
  5. package/dist/backend/adb/scrcpy-adb-client.js +49 -0
  6. package/dist/backend/index.d.ts +6 -0
  7. package/dist/backend/index.js +6 -0
  8. package/dist/backend/io/buffered-stream-reader.d.ts +10 -0
  9. package/dist/backend/io/buffered-stream-reader.js +40 -0
  10. package/dist/backend/scrcpy-backend.d.ts +42 -0
  11. package/dist/backend/scrcpy-backend.js +250 -0
  12. package/dist/backend/server/options.d.ts +26 -0
  13. package/dist/backend/server/options.js +31 -0
  14. package/dist/backend/server/server-command.d.ts +3 -0
  15. package/dist/backend/server/server-command.js +24 -0
  16. package/dist/backend/server/socket-name.d.ts +2 -0
  17. package/dist/backend/server/socket-name.js +4 -0
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.js +4 -0
  20. package/dist/protocol/core/binary.d.ts +5 -0
  21. package/dist/protocol/core/binary.js +23 -0
  22. package/dist/protocol/core/types.d.ts +15 -0
  23. package/dist/protocol/core/types.js +1 -0
  24. package/dist/protocol/index.d.ts +3 -0
  25. package/dist/protocol/index.js +3 -0
  26. package/dist/protocol/registry.d.ts +3 -0
  27. package/dist/protocol/registry.js +16 -0
  28. package/dist/protocol/v4_0/codecs.d.ts +14 -0
  29. package/dist/protocol/v4_0/codecs.js +22 -0
  30. package/dist/protocol/v4_0/control-message-types.d.ts +115 -0
  31. package/dist/protocol/v4_0/control-message-types.js +47 -0
  32. package/dist/protocol/v4_0/control-message.d.ts +2 -0
  33. package/dist/protocol/v4_0/control-message.js +149 -0
  34. package/dist/protocol/v4_0/device-message.d.ts +17 -0
  35. package/dist/protocol/v4_0/device-message.js +33 -0
  36. package/dist/protocol/v4_0/frame-header.d.ts +19 -0
  37. package/dist/protocol/v4_0/frame-header.js +29 -0
  38. package/dist/protocol/v4_0/index.d.ts +9 -0
  39. package/dist/protocol/v4_0/index.js +21 -0
  40. package/dist/protocol/v4_0/server-options.d.ts +19 -0
  41. package/dist/protocol/v4_0/server-options.js +38 -0
  42. package/dist/protocol/v4_0/string-payload.d.ts +2 -0
  43. package/dist/protocol/v4_0/string-payload.js +23 -0
  44. package/dist/service/index.d.ts +3 -0
  45. package/dist/service/index.js +3 -0
  46. package/dist/service/packet-queue-subscriber.d.ts +11 -0
  47. package/dist/service/packet-queue-subscriber.js +56 -0
  48. package/dist/service/scrcpy-stream-service.d.ts +26 -0
  49. package/dist/service/scrcpy-stream-service.js +178 -0
  50. package/dist/service/types.d.ts +25 -0
  51. package/dist/service/types.js +14 -0
  52. package/dist/snapshot/ffmpeg-snapshot-cache.d.ts +34 -0
  53. package/dist/snapshot/ffmpeg-snapshot-cache.js +195 -0
  54. package/dist/snapshot/index.d.ts +2 -0
  55. package/dist/snapshot/index.js +2 -0
  56. package/dist/snapshot/types.d.ts +15 -0
  57. package/dist/snapshot/types.js +1 -0
  58. package/dist/websocket/binary-packet.d.ts +2 -0
  59. package/dist/websocket/binary-packet.js +33 -0
  60. package/dist/websocket/control-json.d.ts +2 -0
  61. package/dist/websocket/control-json.js +129 -0
  62. package/dist/websocket/index.d.ts +4 -0
  63. package/dist/websocket/index.js +4 -0
  64. package/dist/websocket/scrcpy-websocket-bridge.d.ts +10 -0
  65. package/dist/websocket/scrcpy-websocket-bridge.js +75 -0
  66. package/dist/websocket/types.d.ts +9 -0
  67. package/dist/websocket/types.js +4 -0
  68. package/package.json +90 -0
@@ -0,0 +1,149 @@
1
+ import { ControlMessageType, INJECT_TEXT_MAX_LENGTH, SET_CLIPBOARD_TEXT_MAX_LENGTH, START_APP_NAME_MAX_LENGTH, UHID_NAME_MAX_LENGTH, } from "./control-message-types.js";
2
+ import { truncateUtf8, writeTinyString } from "./string-payload.js";
3
+ function u16FixedPoint(value) {
4
+ if (value >= 1.0)
5
+ return 0xffff;
6
+ if (value <= 0.0)
7
+ return 0;
8
+ return Math.floor(value * 0x10000) & 0xffff;
9
+ }
10
+ function i16FixedPoint(value) {
11
+ if (value >= 1.0)
12
+ return 0x7fff;
13
+ if (value <= -1.0)
14
+ return 0x8000;
15
+ return Math.floor(value * 0x8000) & 0xffff;
16
+ }
17
+ export function serializeControlMessage(msg) {
18
+ switch (msg.type) {
19
+ case ControlMessageType.INJECT_KEYCODE: {
20
+ const buf = new Uint8Array(14);
21
+ const view = new DataView(buf.buffer);
22
+ view.setUint8(0, msg.type);
23
+ view.setUint8(1, msg.action & 0xff);
24
+ view.setUint32(2, msg.keycode >>> 0);
25
+ view.setUint32(6, (msg.repeat ?? 0) >>> 0);
26
+ view.setUint32(10, (msg.metaState ?? 0) >>> 0);
27
+ return buf;
28
+ }
29
+ case ControlMessageType.INJECT_TEXT: {
30
+ const textBuf = truncateUtf8(msg.text, INJECT_TEXT_MAX_LENGTH);
31
+ const buf = new Uint8Array(5 + textBuf.byteLength);
32
+ const view = new DataView(buf.buffer);
33
+ view.setUint8(0, msg.type);
34
+ view.setUint32(1, textBuf.byteLength);
35
+ buf.set(textBuf, 5);
36
+ return buf;
37
+ }
38
+ case ControlMessageType.INJECT_TOUCH_EVENT: {
39
+ const buf = new Uint8Array(32);
40
+ const view = new DataView(buf.buffer);
41
+ view.setUint8(0, msg.type);
42
+ view.setUint8(1, msg.action & 0xff);
43
+ view.setBigUint64(2, BigInt.asUintN(64, msg.pointerId));
44
+ view.setInt32(10, Math.floor(msg.x));
45
+ view.setInt32(14, Math.floor(msg.y));
46
+ view.setUint16(18, msg.screenWidth & 0xffff);
47
+ view.setUint16(20, msg.screenHeight & 0xffff);
48
+ view.setUint16(22, u16FixedPoint(msg.pressure ?? 1.0));
49
+ view.setUint32(24, (msg.actionButton ?? 0) >>> 0);
50
+ view.setUint32(28, (msg.buttons ?? 0) >>> 0);
51
+ return buf;
52
+ }
53
+ case ControlMessageType.INJECT_SCROLL_EVENT: {
54
+ const buf = new Uint8Array(21);
55
+ const view = new DataView(buf.buffer);
56
+ view.setUint8(0, msg.type);
57
+ view.setInt32(1, Math.floor(msg.x));
58
+ view.setInt32(5, Math.floor(msg.y));
59
+ view.setUint16(9, msg.screenWidth & 0xffff);
60
+ view.setUint16(11, msg.screenHeight & 0xffff);
61
+ const h = Math.max(-1.0, Math.min(1.0, (msg.hScroll ?? 0.0) / 16.0));
62
+ const v = Math.max(-1.0, Math.min(1.0, (msg.vScroll ?? 0.0) / 16.0));
63
+ view.setUint16(13, i16FixedPoint(h));
64
+ view.setUint16(15, i16FixedPoint(v));
65
+ view.setUint32(17, (msg.buttons ?? 0) >>> 0);
66
+ return buf;
67
+ }
68
+ case ControlMessageType.BACK_OR_SCREEN_ON: {
69
+ return Uint8Array.of(msg.type, msg.action & 0xff);
70
+ }
71
+ case ControlMessageType.GET_CLIPBOARD: {
72
+ return Uint8Array.of(msg.type, msg.copyKey & 0xff);
73
+ }
74
+ case ControlMessageType.SET_CLIPBOARD: {
75
+ const textBuf = truncateUtf8(msg.text, SET_CLIPBOARD_TEXT_MAX_LENGTH);
76
+ const buf = new Uint8Array(14 + textBuf.byteLength);
77
+ const view = new DataView(buf.buffer);
78
+ view.setUint8(0, msg.type);
79
+ view.setBigUint64(1, BigInt.asUintN(64, msg.sequence));
80
+ view.setUint8(9, msg.paste ? 1 : 0);
81
+ view.setUint32(10, textBuf.byteLength);
82
+ buf.set(textBuf, 14);
83
+ return buf;
84
+ }
85
+ case ControlMessageType.SET_DISPLAY_POWER:
86
+ case ControlMessageType.CAMERA_SET_TORCH: {
87
+ return Uint8Array.of(msg.type, msg.on ? 1 : 0);
88
+ }
89
+ case ControlMessageType.UHID_CREATE: {
90
+ const nameBuf = truncateUtf8(msg.name, UHID_NAME_MAX_LENGTH);
91
+ const reportDescriptor = msg.reportDescriptor;
92
+ const buf = new Uint8Array(10 + nameBuf.byteLength + reportDescriptor.byteLength);
93
+ const view = new DataView(buf.buffer);
94
+ let offset = 0;
95
+ view.setUint8(offset++, msg.type);
96
+ view.setUint16(offset, msg.id & 0xffff);
97
+ offset += 2;
98
+ view.setUint16(offset, msg.vendorId & 0xffff);
99
+ offset += 2;
100
+ view.setUint16(offset, msg.productId & 0xffff);
101
+ offset += 2;
102
+ offset += writeTinyString(buf, offset, msg.name, UHID_NAME_MAX_LENGTH);
103
+ view.setUint16(offset, reportDescriptor.byteLength & 0xffff);
104
+ offset += 2;
105
+ buf.set(reportDescriptor, offset);
106
+ return buf;
107
+ }
108
+ case ControlMessageType.UHID_INPUT: {
109
+ const buf = new Uint8Array(5 + msg.data.byteLength);
110
+ const view = new DataView(buf.buffer);
111
+ view.setUint8(0, msg.type);
112
+ view.setUint16(1, msg.id & 0xffff);
113
+ view.setUint16(3, msg.data.byteLength & 0xffff);
114
+ buf.set(msg.data, 5);
115
+ return buf;
116
+ }
117
+ case ControlMessageType.UHID_DESTROY: {
118
+ const buf = new Uint8Array(3);
119
+ const view = new DataView(buf.buffer);
120
+ view.setUint8(0, msg.type);
121
+ view.setUint16(1, msg.id & 0xffff);
122
+ return buf;
123
+ }
124
+ case ControlMessageType.START_APP: {
125
+ const nameBuf = truncateUtf8(msg.name, START_APP_NAME_MAX_LENGTH);
126
+ const buf = new Uint8Array(2 + nameBuf.byteLength);
127
+ new DataView(buf.buffer).setUint8(0, msg.type);
128
+ writeTinyString(buf, 1, msg.name, START_APP_NAME_MAX_LENGTH);
129
+ return buf;
130
+ }
131
+ case ControlMessageType.RESIZE_DISPLAY: {
132
+ const buf = new Uint8Array(5);
133
+ const view = new DataView(buf.buffer);
134
+ view.setUint8(0, msg.type);
135
+ view.setUint16(1, msg.width & 0xffff);
136
+ view.setUint16(3, msg.height & 0xffff);
137
+ return buf;
138
+ }
139
+ case ControlMessageType.EXPAND_NOTIFICATION_PANEL:
140
+ case ControlMessageType.EXPAND_SETTINGS_PANEL:
141
+ case ControlMessageType.COLLAPSE_PANELS:
142
+ case ControlMessageType.ROTATE_DEVICE:
143
+ case ControlMessageType.OPEN_HARD_KEYBOARD_SETTINGS:
144
+ case ControlMessageType.RESET_VIDEO:
145
+ case ControlMessageType.CAMERA_ZOOM_IN:
146
+ case ControlMessageType.CAMERA_ZOOM_OUT:
147
+ return Uint8Array.of(msg.type);
148
+ }
149
+ }
@@ -0,0 +1,17 @@
1
+ export declare enum DeviceMessageType {
2
+ CLIPBOARD = 0,
3
+ ACK_CLIPBOARD = 1,
4
+ UHID_OUTPUT = 2
5
+ }
6
+ export type DeviceMessage = {
7
+ type: DeviceMessageType.CLIPBOARD;
8
+ text: string;
9
+ } | {
10
+ type: DeviceMessageType.ACK_CLIPBOARD;
11
+ sequence: bigint;
12
+ } | {
13
+ type: DeviceMessageType.UHID_OUTPUT;
14
+ uhidId: number;
15
+ data: Uint8Array;
16
+ };
17
+ export declare function parseDeviceMessage(readExact: (n: number) => Promise<Uint8Array>): Promise<DeviceMessage>;
@@ -0,0 +1,33 @@
1
+ import { dataView, utf8Decode } from "../core/binary.js";
2
+ export var DeviceMessageType;
3
+ (function (DeviceMessageType) {
4
+ DeviceMessageType[DeviceMessageType["CLIPBOARD"] = 0] = "CLIPBOARD";
5
+ DeviceMessageType[DeviceMessageType["ACK_CLIPBOARD"] = 1] = "ACK_CLIPBOARD";
6
+ DeviceMessageType[DeviceMessageType["UHID_OUTPUT"] = 2] = "UHID_OUTPUT";
7
+ })(DeviceMessageType || (DeviceMessageType = {}));
8
+ export async function parseDeviceMessage(readExact) {
9
+ const head = await readExact(1);
10
+ const type = head[0];
11
+ switch (type) {
12
+ case DeviceMessageType.CLIPBOARD: {
13
+ const lenBuf = await readExact(4);
14
+ const length = dataView(lenBuf).getUint32(0);
15
+ const textBuf = await readExact(length);
16
+ return { type, text: utf8Decode(textBuf) };
17
+ }
18
+ case DeviceMessageType.ACK_CLIPBOARD: {
19
+ const seqBuf = await readExact(8);
20
+ return { type, sequence: dataView(seqBuf).getBigUint64(0) };
21
+ }
22
+ case DeviceMessageType.UHID_OUTPUT: {
23
+ const metaBuf = await readExact(4);
24
+ const meta = dataView(metaBuf);
25
+ const uhidId = meta.getUint16(0);
26
+ const length = meta.getUint16(2);
27
+ const data = await readExact(length);
28
+ return { type, uhidId, data };
29
+ }
30
+ default:
31
+ throw new Error(`unknown device message type: ${type}`);
32
+ }
33
+ }
@@ -0,0 +1,19 @@
1
+ export declare const SESSION_PACKET_FLAG: bigint;
2
+ export declare const CONFIG_PACKET_FLAG: bigint;
3
+ export declare const KEY_FRAME_FLAG: bigint;
4
+ export declare const PTS_MASK: bigint;
5
+ export interface SessionPacket {
6
+ kind: "session";
7
+ width: number;
8
+ height: number;
9
+ clientResized: boolean;
10
+ }
11
+ export interface FrameMeta {
12
+ kind: "frame";
13
+ ptsUs: bigint;
14
+ size: number;
15
+ config: boolean;
16
+ keyFrame: boolean;
17
+ }
18
+ export type StreamPacketMeta = SessionPacket | FrameMeta;
19
+ export declare function parseFrameHeader(header: Uint8Array): StreamPacketMeta;
@@ -0,0 +1,29 @@
1
+ import { dataView } from "../core/binary.js";
2
+ export const SESSION_PACKET_FLAG = 1n << 63n;
3
+ export const CONFIG_PACKET_FLAG = 1n << 62n;
4
+ export const KEY_FRAME_FLAG = 1n << 61n;
5
+ export const PTS_MASK = (1n << 61n) - 1n;
6
+ export function parseFrameHeader(header) {
7
+ if (header.byteLength !== 12) {
8
+ throw new Error(`frame header must be 12 bytes, got ${header.byteLength}`);
9
+ }
10
+ const view = dataView(header);
11
+ const ptsAndFlags = view.getBigUint64(0);
12
+ const size = view.getUint32(8);
13
+ if (ptsAndFlags & SESSION_PACKET_FLAG) {
14
+ const flagsHigh = Number((ptsAndFlags >> 32n) & 0xffffffffn);
15
+ return {
16
+ kind: "session",
17
+ width: Number(ptsAndFlags & 0xffffffffn),
18
+ height: size,
19
+ clientResized: (flagsHigh & 0x01) !== 0,
20
+ };
21
+ }
22
+ return {
23
+ kind: "frame",
24
+ ptsUs: ptsAndFlags & PTS_MASK,
25
+ size,
26
+ config: (ptsAndFlags & CONFIG_PACKET_FLAG) !== 0n,
27
+ keyFrame: (ptsAndFlags & KEY_FRAME_FLAG) !== 0n,
28
+ };
29
+ }
@@ -0,0 +1,9 @@
1
+ import { ScrcpyProtocol } from "../core/types.js";
2
+ export * from "./codecs.js";
3
+ export * from "./control-message.js";
4
+ export * from "./control-message-types.js";
5
+ export * from "./device-message.js";
6
+ export * from "./frame-header.js";
7
+ export * from "./server-options.js";
8
+ export * from "./string-payload.js";
9
+ export declare const SCRCPY_4_0_PROTOCOL: ScrcpyProtocol;
@@ -0,0 +1,21 @@
1
+ import { serializeControlMessage } from "./control-message.js";
2
+ import { parseDeviceMessage } from "./device-message.js";
3
+ import { parseFrameHeader } from "./frame-header.js";
4
+ import { buildServerOptions, getSocketName, SCRCPY_4_0_SERVER_JAR, SCRCPY_4_0_SERVER_VERSION, } from "./server-options.js";
5
+ export * from "./codecs.js";
6
+ export * from "./control-message.js";
7
+ export * from "./control-message-types.js";
8
+ export * from "./device-message.js";
9
+ export * from "./frame-header.js";
10
+ export * from "./server-options.js";
11
+ export * from "./string-payload.js";
12
+ export const SCRCPY_4_0_PROTOCOL = {
13
+ version: "4.0",
14
+ serverVersion: SCRCPY_4_0_SERVER_VERSION,
15
+ serverJarAssetName: SCRCPY_4_0_SERVER_JAR,
16
+ serializeControlMessage,
17
+ parseFrameHeader,
18
+ parseDeviceMessage,
19
+ buildServerOptions,
20
+ getSocketName,
21
+ };
@@ -0,0 +1,19 @@
1
+ import { AudioCodec, VideoCodec } from "./codecs.js";
2
+ export declare const SCRCPY_4_0_SERVER_VERSION = "4.0";
3
+ export declare const SCRCPY_4_0_SERVER_JAR = "scrcpy-server-v4.0.jar";
4
+ export declare const DEFAULT_SCRCPY_SCID = 10;
5
+ export interface ScrcpyServerOptionInput {
6
+ scid?: number;
7
+ logLevel?: "verbose" | "debug" | "info" | "warn" | "error";
8
+ video: boolean;
9
+ audio: boolean;
10
+ control: boolean;
11
+ videoCodec: VideoCodec;
12
+ audioCodec: AudioCodec;
13
+ maxSize: number;
14
+ maxFps: number;
15
+ videoBitRate: number;
16
+ audioBitRate: number;
17
+ }
18
+ export declare function getSocketName(scid?: number): string;
19
+ export declare function buildServerOptions(input: ScrcpyServerOptionInput): string[];
@@ -0,0 +1,38 @@
1
+ export const SCRCPY_4_0_SERVER_VERSION = "4.0";
2
+ export const SCRCPY_4_0_SERVER_JAR = "scrcpy-server-v4.0.jar";
3
+ export const DEFAULT_SCRCPY_SCID = 0x0000000a;
4
+ export function getSocketName(scid = DEFAULT_SCRCPY_SCID) {
5
+ if (scid === -1)
6
+ return "scrcpy";
7
+ if (scid < -1) {
8
+ throw new Error(`scrcpy scid must be -1 or a non-negative 31-bit value, got ${scid}`);
9
+ }
10
+ return `scrcpy_${scid.toString(16).padStart(8, "0")}`;
11
+ }
12
+ export function buildServerOptions(input) {
13
+ const scid = input.scid ?? DEFAULT_SCRCPY_SCID;
14
+ return [
15
+ SCRCPY_4_0_SERVER_VERSION,
16
+ `log_level=${input.logLevel ?? "info"}`,
17
+ "tunnel_forward=true",
18
+ `scid=${scid.toString(16)}`,
19
+ `video=${input.video}`,
20
+ `audio=${input.audio}`,
21
+ `control=${input.control}`,
22
+ `video_codec=${input.videoCodec}`,
23
+ `audio_codec=${input.audioCodec}`,
24
+ `max_size=${input.maxSize}`,
25
+ `max_fps=${input.maxFps}`,
26
+ `video_bit_rate=${input.videoBitRate}`,
27
+ `audio_bit_rate=${input.audioBitRate}`,
28
+ "send_device_meta=true",
29
+ "send_frame_meta=true",
30
+ "send_dummy_byte=true",
31
+ "send_stream_meta=true",
32
+ "cleanup=true",
33
+ "stay_awake=false",
34
+ "show_touches=false",
35
+ "power_off_on_close=false",
36
+ "clipboard_autosync=false",
37
+ ];
38
+ }
@@ -0,0 +1,2 @@
1
+ export declare function truncateUtf8(text: string, maxBytes: number): Uint8Array;
2
+ export declare function writeTinyString(target: Uint8Array, offset: number, text: string, maxBytes: number): number;
@@ -0,0 +1,23 @@
1
+ import { utf8Encode } from "../core/binary.js";
2
+ export function truncateUtf8(text, maxBytes) {
3
+ const bytes = utf8Encode(text);
4
+ if (bytes.byteLength <= maxBytes)
5
+ return bytes;
6
+ let end = maxBytes;
7
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) {
8
+ end--;
9
+ }
10
+ if (end > 0 && (bytes[end] & 0x80) !== 0) {
11
+ end--;
12
+ }
13
+ return bytes.subarray(0, end);
14
+ }
15
+ export function writeTinyString(target, offset, text, maxBytes) {
16
+ if (maxBytes > 0xff) {
17
+ throw new Error(`tiny string max length must fit in one byte, got ${maxBytes}`);
18
+ }
19
+ const payload = truncateUtf8(text, maxBytes);
20
+ target[offset] = payload.byteLength;
21
+ target.set(payload, offset + 1);
22
+ return 1 + payload.byteLength;
23
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./packet-queue-subscriber.js";
2
+ export * from "./scrcpy-stream-service.js";
3
+ export * from "./types.js";
@@ -0,0 +1,3 @@
1
+ export * from "./packet-queue-subscriber.js";
2
+ export * from "./scrcpy-stream-service.js";
3
+ export * from "./types.js";
@@ -0,0 +1,11 @@
1
+ import { MediaPacket } from "./types.js";
2
+ export declare class PacketQueueSubscriber implements AsyncIterable<MediaPacket> {
3
+ private maxQueue;
4
+ private queue;
5
+ private resolveNext;
6
+ private closed;
7
+ constructor(maxQueue: number);
8
+ push(packet: MediaPacket): void;
9
+ close(): void;
10
+ [Symbol.asyncIterator](): AsyncIterator<MediaPacket>;
11
+ }
@@ -0,0 +1,56 @@
1
+ import { MediaKind } from "./types.js";
2
+ export class PacketQueueSubscriber {
3
+ maxQueue;
4
+ queue = [];
5
+ resolveNext = null;
6
+ closed = false;
7
+ constructor(maxQueue) {
8
+ this.maxQueue = maxQueue;
9
+ }
10
+ push(packet) {
11
+ if (this.closed)
12
+ return;
13
+ if (this.resolveNext) {
14
+ const resolve = this.resolveNext;
15
+ this.resolveNext = null;
16
+ resolve({ done: false, value: packet });
17
+ return;
18
+ }
19
+ if (this.queue.length >= this.maxQueue) {
20
+ // Preserve config/session packets because late subscribers need them to
21
+ // initialize decoders before receiving regular media frames.
22
+ const index = this.queue.findIndex((item) => !item.config && item.kind !== MediaKind.SESSION);
23
+ if (index !== -1) {
24
+ this.queue.splice(index, 1);
25
+ }
26
+ else {
27
+ this.queue.shift();
28
+ }
29
+ }
30
+ this.queue.push(packet);
31
+ }
32
+ close() {
33
+ this.closed = true;
34
+ if (this.resolveNext) {
35
+ const resolve = this.resolveNext;
36
+ this.resolveNext = null;
37
+ resolve({ done: true, value: undefined });
38
+ }
39
+ }
40
+ async *[Symbol.asyncIterator]() {
41
+ while (true) {
42
+ if (this.queue.length > 0) {
43
+ yield this.queue.shift();
44
+ continue;
45
+ }
46
+ if (this.closed)
47
+ return;
48
+ const result = await new Promise((resolve) => {
49
+ this.resolveNext = resolve;
50
+ });
51
+ if (result.done)
52
+ return;
53
+ yield result.value;
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,26 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { StreamMeta } from "../backend/scrcpy-backend.js";
3
+ import { ControlMessage } from "../protocol/index.js";
4
+ import { MediaPacket, ScrcpyStreamServiceOptions, StreamState } from "./types.js";
5
+ export declare class ScrcpyStreamService extends EventEmitter {
6
+ private backend;
7
+ private state;
8
+ private meta;
9
+ private queueMaxPackets;
10
+ private videoConfig;
11
+ private audioConfig;
12
+ private latestKeyFrame;
13
+ private latestSession;
14
+ private subscribers;
15
+ constructor(options?: ScrcpyStreamServiceOptions);
16
+ start(): Promise<StreamMeta>;
17
+ stop(): void;
18
+ sendControlMessage(msg: ControlMessage): void;
19
+ subscribe(): AsyncIterableIterator<MediaPacket>;
20
+ get currentState(): StreamState;
21
+ get currentMeta(): StreamMeta | null;
22
+ private setupBackendListeners;
23
+ private pushSnapshot;
24
+ private broadcast;
25
+ private setState;
26
+ }
@@ -0,0 +1,178 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { ScrcpyBackend } from "../backend/scrcpy-backend.js";
3
+ import { PacketQueueSubscriber } from "./packet-queue-subscriber.js";
4
+ import { MediaKind, StreamState, } from "./types.js";
5
+ const EMPTY_PAYLOAD = new Uint8Array(0);
6
+ export class ScrcpyStreamService extends EventEmitter {
7
+ backend;
8
+ state = StreamState.STOPPED;
9
+ meta = null;
10
+ queueMaxPackets;
11
+ videoConfig = null;
12
+ audioConfig = null;
13
+ latestKeyFrame = null;
14
+ latestSession = null;
15
+ subscribers = new Set();
16
+ constructor(options = {}) {
17
+ super();
18
+ this.queueMaxPackets = options.queueMaxPackets ?? 240;
19
+ this.backend = new ScrcpyBackend(options);
20
+ this.setupBackendListeners();
21
+ }
22
+ async start() {
23
+ if (this.state === StreamState.RUNNING ||
24
+ this.state === StreamState.STARTING) {
25
+ return this.meta;
26
+ }
27
+ this.setState(StreamState.STARTING);
28
+ try {
29
+ this.meta = await this.backend.start();
30
+ this.setState(StreamState.RUNNING);
31
+ return this.meta;
32
+ }
33
+ catch (e) {
34
+ this.setState(StreamState.ERROR);
35
+ throw e;
36
+ }
37
+ }
38
+ stop() {
39
+ if (this.state === StreamState.STOPPED)
40
+ return;
41
+ this.setState(StreamState.STOPPING);
42
+ this.backend.stop();
43
+ this.videoConfig = null;
44
+ this.audioConfig = null;
45
+ this.latestKeyFrame = null;
46
+ this.latestSession = null;
47
+ this.subscribers.forEach((subscriber) => subscriber.close());
48
+ this.subscribers.clear();
49
+ this.setState(StreamState.STOPPED);
50
+ }
51
+ sendControlMessage(msg) {
52
+ this.backend.sendControlMessage(msg);
53
+ }
54
+ subscribe() {
55
+ const subscriber = new PacketQueueSubscriber(this.queueMaxPackets);
56
+ this.subscribers.add(subscriber);
57
+ this.pushSnapshot(subscriber);
58
+ const iterator = subscriber[Symbol.asyncIterator]();
59
+ return {
60
+ next: () => iterator.next(),
61
+ return: async (value) => {
62
+ this.subscribers.delete(subscriber);
63
+ subscriber.close();
64
+ if (iterator.return)
65
+ return iterator.return(value);
66
+ return { done: true, value };
67
+ },
68
+ throw: async (e) => {
69
+ this.subscribers.delete(subscriber);
70
+ subscriber.close();
71
+ if (iterator.throw)
72
+ return iterator.throw(e);
73
+ throw e;
74
+ },
75
+ [Symbol.asyncIterator]() {
76
+ return this;
77
+ },
78
+ };
79
+ }
80
+ get currentState() {
81
+ return this.state;
82
+ }
83
+ get currentMeta() {
84
+ return this.meta;
85
+ }
86
+ setupBackendListeners() {
87
+ this.backend.on("video", (meta, payload) => {
88
+ const packet = {
89
+ kind: MediaKind.VIDEO,
90
+ ptsUs: meta.ptsUs,
91
+ config: meta.config,
92
+ keyFrame: meta.keyFrame,
93
+ payload,
94
+ };
95
+ if (meta.config)
96
+ this.videoConfig = payload;
97
+ if (meta.keyFrame)
98
+ this.latestKeyFrame = packet;
99
+ this.broadcast(packet);
100
+ });
101
+ this.backend.on("audio", (meta, payload) => {
102
+ const packet = {
103
+ kind: MediaKind.AUDIO,
104
+ ptsUs: meta.ptsUs,
105
+ config: meta.config,
106
+ keyFrame: false,
107
+ payload,
108
+ };
109
+ if (meta.config)
110
+ this.audioConfig = payload;
111
+ this.broadcast(packet);
112
+ });
113
+ this.backend.on("session", (session) => {
114
+ this.latestSession = session;
115
+ this.broadcast({
116
+ kind: MediaKind.SESSION,
117
+ ptsUs: 0n,
118
+ config: false,
119
+ keyFrame: false,
120
+ payload: EMPTY_PAYLOAD,
121
+ width: session.width,
122
+ height: session.height,
123
+ });
124
+ });
125
+ this.backend.on("deviceMessage", (msg) => {
126
+ this.emit("deviceMessage", msg);
127
+ });
128
+ this.backend.on("error", (e) => {
129
+ this.setState(StreamState.ERROR);
130
+ this.emit("error", e);
131
+ this.stop();
132
+ });
133
+ }
134
+ pushSnapshot(subscriber) {
135
+ if (this.latestSession) {
136
+ subscriber.push({
137
+ kind: MediaKind.SESSION,
138
+ ptsUs: 0n,
139
+ config: false,
140
+ keyFrame: false,
141
+ payload: EMPTY_PAYLOAD,
142
+ width: this.latestSession.width,
143
+ height: this.latestSession.height,
144
+ });
145
+ }
146
+ if (this.videoConfig) {
147
+ subscriber.push({
148
+ kind: MediaKind.VIDEO,
149
+ ptsUs: 0n,
150
+ config: true,
151
+ keyFrame: false,
152
+ payload: this.videoConfig,
153
+ });
154
+ }
155
+ if (this.latestKeyFrame)
156
+ subscriber.push(this.latestKeyFrame);
157
+ if (this.audioConfig) {
158
+ subscriber.push({
159
+ kind: MediaKind.AUDIO,
160
+ ptsUs: 0n,
161
+ config: true,
162
+ keyFrame: false,
163
+ payload: this.audioConfig,
164
+ });
165
+ }
166
+ }
167
+ broadcast(packet) {
168
+ for (const subscriber of this.subscribers) {
169
+ subscriber.push(packet);
170
+ }
171
+ }
172
+ setState(state) {
173
+ if (this.state === state)
174
+ return;
175
+ this.state = state;
176
+ this.emit("state", state);
177
+ }
178
+ }
@@ -0,0 +1,25 @@
1
+ import { ScrcpyBackendOptions } from "../backend/server/options.js";
2
+ export declare enum StreamState {
3
+ STOPPED = "stopped",
4
+ STARTING = "starting",
5
+ RUNNING = "running",
6
+ STOPPING = "stopping",
7
+ ERROR = "error"
8
+ }
9
+ export declare enum MediaKind {
10
+ VIDEO = "video",
11
+ AUDIO = "audio",
12
+ SESSION = "session"
13
+ }
14
+ export interface MediaPacket {
15
+ kind: MediaKind;
16
+ ptsUs: bigint;
17
+ config: boolean;
18
+ keyFrame: boolean;
19
+ payload: Uint8Array;
20
+ width?: number;
21
+ height?: number;
22
+ }
23
+ export interface ScrcpyStreamServiceOptions extends ScrcpyBackendOptions {
24
+ queueMaxPackets?: number;
25
+ }