Skip to content

Commit 23890cc

Browse files
committed
update scrcpy usage
1 parent 4a48a0f commit 23890cc

File tree

2 files changed

+161
-31
lines changed

2 files changed

+161
-31
lines changed

docs/scrcpy/control/touch.mdx

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,41 @@ interface ScrcpyInjectTouchControlMessage {
3535
- `pointerId`: The ID of the pointer. There are some predefined values in `ScrcpyPointerId` namespace, but touch screen devices can assign a value for each finger.
3636
- `pointerX`: The X coordinate of the event.
3737
- `pointerY`: The Y coordinate of the event.
38-
- `screenWidth`: The width of the screen. It must match the current video stream resolution to prevent de-synchronization.
39-
- `screenHeight`: The height of the screen. It must match the current video stream resolution to prevent de-synchronization.
40-
- `pressure`: The pressure of the event. A floating-point value between 0 and 1.
4138
- `actionButton`: The button that changed. It's a value of `AndroidMotionEventButton` enum.
4239
- `buttons`: The state of all the buttons. It's a bit-or combination of `AndroidMotionEventButton` values.
4340

41+
### `screenWidth` and `screenHeight`
42+
43+
The size of latest video frame.
44+
45+
The server will validate these values, and ignore the command if they don't match. This is to prevent invalid `pointerX` and `pointerY` values from being sent to the device, for example when device orientation is changed.
46+
47+
When using `AdbScrcpyClient`, it will parse and save the size (if video stream is enabled):
48+
49+
```ts transpile
50+
import type { AdbScrcpyClient } from "@yume-chan/adb-scrcpy";
51+
52+
declare const client: AdbScrcpyClient;
53+
54+
console.log(client.screenWidth, client.screenHeight);
55+
```
56+
57+
When using one of the [built-in video decoders](../video/index.mdx#decode-and-render-video), the [`sizeChanged` event](../video/tiny-h264.mdx#handle-size-changes) can be used to get the latest `screenWidth` and `screenHeight` values.
58+
59+
```ts transpile
60+
import type { ScrcpyVideoDecoder } from "@yume-chan/scrcpy-decoder-tinyh264";
61+
62+
let screenWidth = 0;
63+
let screenHeight = 0;
64+
65+
declare const decoder: ScrcpyVideoDecoder;
66+
67+
decoder.sizeChanged(({ width, height }) => {
68+
screenWidth = width;
69+
screenHeight = height;
70+
});
71+
```
72+
4473
## Usage
4574

4675
```ts transpile
@@ -89,3 +118,75 @@ await client.controller!.injectTouch({
89118
buttons: AndroidMotionEventButton.Primary,
90119
});
91120
```
121+
122+
### DOM Events Example
123+
124+
The `pointerdown`, `pointermove` and `pointerup` events can be mapped to Scrcpy touch events.
125+
126+
```ts transpile
127+
import type {
128+
ScrcpyControlMessageSerializer,
129+
AndroidMotionEventAction,
130+
clamp,
131+
} from "@yume-chan/scrcpy";
132+
133+
declare const serializer: ScrcpyControlMessageSerializer; // ScrcpyControlMessageWriter` and `AdbScrcpyClient` can also be used
134+
declare let screenWidth: number;
135+
declare let screenHeight: number;
136+
137+
const PointerEventButtonToAndroidButton = [
138+
AndroidMotionEventButton.Primary,
139+
AndroidMotionEventButton.Tertiary,
140+
AndroidMotionEventButton.Secondary,
141+
AndroidMotionEventButton.Back,
142+
AndroidMotionEventButton.Forward,
143+
];
144+
145+
function handlePointerEvent(event: PointerEvent) {
146+
e.preventDefault();
147+
e.stopPropagation();
148+
149+
const target = e.currentTarget as HTMLElement;
150+
target.setPointerCapture(e.pointerId);
151+
152+
const { type, clientX, clientY, button, buttons } = e;
153+
154+
let action: AndroidMotionEventAction;
155+
switch (type) {
156+
case "pointerdown":
157+
action = AndroidMotionEventAction.Down;
158+
break;
159+
case "pointermove":
160+
if (buttons === 0) {
161+
action = AndroidMotionEventAction.HoverMove;
162+
} else {
163+
action = AndroidMotionEventAction.Move;
164+
}
165+
break;
166+
case "pointerup":
167+
action = AndroidMotionEventAction.Up;
168+
break;
169+
default:
170+
throw new Error(`Unsupported event type: ${type}`);
171+
}
172+
173+
const rect = target.getBoundingClientRect();
174+
const percentageX = clamp((clientX - rect.x) / rect.width, 0, 1);
175+
const percentageY = clamp((clientY - rect.y) / rect.height, 0, 1);
176+
177+
const pointerX = percentageX * screenWidth;
178+
const pointerY = percentageY * screenHeight;
179+
180+
return serializer.injectTouch({
181+
action,
182+
pointerId: BigInt(e.pointerId),
183+
pointerX,
184+
pointerY,
185+
screenWidth,
186+
screenHeight,
187+
pressure: buttons === 0 ? 0 : 1,
188+
actionButton: PointerEventButtonToAndroidButton[button],
189+
buttons,
190+
});
191+
}
192+
```

docs/scrcpy/video/index.mdx

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,36 @@ stateDiagram-v2
2222
Data --> [*]
2323
```
2424

25+
## Format
26+
27+
This table show how versions and options affect the video stream format.
28+
29+
- ✅ means the field is always present
30+
- ⛔ means the field is not present
31+
- An option name means the field is present if the option is `true`
32+
33+
| Value | v1.15 ~ v1.21 | 1.22 | v1.23 ~ v1.25 | v2.0 ~ v3.1 |
34+
| ----------------------------------------------------- | --------------- | ---------------- | ---------------- | ------------------------------------------------------ |
35+
| Metadata || `sendDeviceMeta` | `sendDeviceMeta` | <code>sendDeviceMeta &#124;&#124; sendCodecMeta</code> |
36+
| <span style={{marginRight:'2em'}}/>Device name || `sendDeviceMeta` | `sendDeviceMeta` | `sendDeviceMeta` |
37+
| <span style={{marginRight:'2em'}}/>Initial video size || `sendDeviceMeta` | `sendDeviceMeta` | `sendCodecMeta` |
38+
| <span style={{marginRight:'2em'}}/>Video codec |||| `sendCodecMeta` |
39+
| Configuration | `sendFrameMeta` | `sendFrameMeta` | `sendFrameMeta` | `sendFrameMeta` |
40+
| Data Packet Header | `sendFrameMeta` | `sendFrameMeta` | `sendFrameMeta` | `sendFrameMeta` |
41+
| <span style={{marginRight:'2em'}}/> PTS | `sendFrameMeta` | `sendFrameMeta` | `sendFrameMeta` | `sendFrameMeta` |
42+
| <span style={{marginRight:'2em'}}/> Keyframe Mark ||| `sendFrameMeta` | `sendFrameMeta` |
43+
2544
## Raw mode
2645

27-
Since v1.22, if `sendDeviceMeta` and `sendFrameMeta` options are `false`<Version since="v2.0">, and `sendCodecMeta` option is also `false`</Version>, the server directly returns the encoded video stream without any encapsulations.
46+
As the above table shows, since v1.22, if `sendDeviceMeta` and `sendFrameMeta` options are `false`<Version since="v2.0">, and `sendCodecMeta` option is also `false`</Version>, all listed fields are not present.
2847

29-
In this mode, the video stream only contains codec-specific data. Tango doesn't support parsing the video stream in this mode, but it can be decoded by FFmpeg, or saved directly.
48+
This is called the raw mode. In this mode, the video socket only contains codec-specific, encoded video data.
3049

31-
Otherwise, the video stream contains additional metadata and frame information. The following methods can be used to parse and handle the video stream.
50+
Without the extra information, it's much harder to process the video stream. Because of that, Tango only has limited support for parsing the video stream in raw mode. Its methods can process the stream without errors, but some fields will be `undefined`, and [built-in video decoders](#decode-and-render-video) can't decode raw mode video stream.
3251

3352
## Video stream metadata
3453

35-
Scrcpy server first sends some metadata about the video stream:
54+
If the server [version and options requirements](#format) are met, the server will first send some metadata about the device and the video stream:
3655

3756
```ts
3857
interface ScrcpyVideoStreamMetadata {
@@ -43,15 +62,19 @@ interface ScrcpyVideoStreamMetadata {
4362
}
4463
```
4564

46-
- `deviceName`: The device's model name<Version since="v1.22">, or `undefined` if `sendDeviceMeta` option is `false`.</Version>.
47-
- `width` and `height`: Size of the first video frame<Version since="v1.22" until='v2.0'>, or `undefined` if `sendDeviceMeta` option is `false`.</Version><Version since="v2.0">, or `undefined` if `sendCodecMeta` option is `false`.</Version>.
48-
- `codec`: The codec returned from server<Version since="v2.0">, or the codec inferred from options if `sendCodecMeta` option is `false`.</Version>.
65+
- `deviceName`: The device's model name.
66+
- `width`/`height`: Size of the first video frame.
67+
- `codec`: The codec of the video stream.
4968

50-
When device's screen resolution changes (for example, when device rotates, or when a foldable device folds/unfolds), the server restarts video capture and encoding with the new resolution. However, it won't send a new metadata packets with the updated size. The only way to keep track of the video resolution is by parsing the video stream directly. The exact code depends on the video codec.
69+
### Size changes
70+
71+
The metadata will only be sent once. When device screen size changes (for example, when device orientation changes, or a foldable device unfolds), the server will restart the video encoder, but it won't send a new metadata with the new size.
72+
73+
To track the video resolution, parsing the video stream is required. [built-in video decoders](#decode-and-render-video) have a [`sizeChanged`](./tiny-h264.mdx#handle-size-changes) event, and [`AdbScrcpyClient`](../start-server.mdx#with-yume-chanadb-scrcpy) has `screenWidth` and `screenHeight` properties.
5174

5275
### With `@yume-chan/scrcpy`
5376

54-
If you already have a `ReadableStream<Uint8Array>` that reads from the video socket, use the `parseVideoStreamMetadata` method from the corresponding `ScrcpyOptionsX_YY` class to parse the metadata. This method will return the metadata, and a new stream that contains the remaining stream.
77+
If you already have a `ReadableStream<Uint8Array>` that reads from the video socket, the `parseVideoStreamMetadata` method from the corresponding `ScrcpyOptionsX_YY` class can be used to parse the metadata. This method will return the metadata, and a new stream that contains the remaining stream.
5578

5679
```ts transpile
5780
import { ScrcpyOptions2_1, ScrcpyVideoStreamPacket } from "@yume-chan/scrcpy";
@@ -67,18 +90,23 @@ const { metadata: videoMetadata, stream: videoStream } =
6790
await options.parseVideoStreamMetadata(videoSocket);
6891
```
6992

70-
`parseVideoStreamMetadata` also supports raw mode:
93+
#### `codec`
94+
95+
If metadata is not present, or doesn't contain codec information, the `codec` field will <Version until="v2.0">always be `ScrcpyVideoCodecId.H264` because it's the only supported codec</Version><Version since="v2.0">be the same as the `videoCodec` option</Version>.
96+
97+
#### Raw mode
7198

72-
* In `metadata`, only the `codec` field is defined, and is inferred from the option values.
73-
* The `stream` field is the input `videoSocket` returned as-is.
99+
If the whole metadata is not present, all fields except `codec` will be `undefined`. The `stream` field returned will be the same object as the parameter.
74100

75101
### With `@yume-chan/adb-scrcpy`
76102

77-
In `AdbScrcpyClient`, parsing video stream metadata and transforming video packets can be done in a single step. See [the section below](#with-yume-chanadb-scrcpy-1).
103+
See [`AdbScrcpyClient.prototype.videoStream` property](#with-yume-chanadb-scrcpy-1) below. It uses `parseVideoStreamMetadata` internally to parse the metadata, and it also parses the video stream into packets.
104+
105+
The [`codec`](#codec) and [raw mode](#raw-mode-1) behavior mentioned above also apply.
78106

79107
## Video packets
80108

81-
When not in raw mode, Scrcpy server will encapsulate each encoded video frame with extra information. Tango parses the frames into two types of packets:
109+
If the server [version and options requirements](#format) are met, the server will encapsulate each encoded video frame with extra information. Tango parses them into two types of packets:
82110

83111
```ts
84112
interface ScrcpyMediaStreamConfigurationPacket {
@@ -98,29 +126,24 @@ type ScrcpyMediaStreamPacket =
98126
| ScrcpyMediaStreamDataPacket;
99127
```
100128

101-
The behavior of the packets depends on the `sendFrameMeta` option:
102-
103-
| Value | `sendFrameMeta: true` | `sendFrameMeta: false` |
104-
| -------------------------------------- | -------------------------------------------------------- | ---------------------- |
105-
| `ScrcpyMediaStreamConfigurationPacket` | Must be the first packet, but can also appear anywhere | Not exist |
106-
| `ScrcpyMediaStreamDataPacket.keyframe` | `boolean` indicating if the current packet is a keyframe | `undefined` |
107-
| `ScrcpyMediaStreamDataPacket.pts` | `bigint` indicating the presentation timestamp | `undefined` |
108-
109129
### Configuration packet
110130

111-
When the encoder is restarted, a new configuration packet will be generated and sent.
131+
If present, will always be the first packet. However, when the encoder is restarted, a new configuration packet will be generated and sent.
112132

113133
The `data` field contains the codec-specific configuration information:
114134

115-
* H.264: Sequence Parameter Set (SPS) and Picture Parameter Set (PPS) in Annex B format.
116-
* H.265: Video Parameter Set (VPS), Sequence Parameter Set (SPS), and Picture Parameter Set (PPS) in Annex B format.
117-
* AV1: The first 3 bytes of `AV1CodecConfigurationRecord` (https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox). The remaining configuration OBUs are in the next data packet.
135+
- H.264: Sequence Parameter Set (SPS) and Picture Parameter Set (PPS) in Annex B format.
136+
- H.265: Video Parameter Set (VPS), Sequence Parameter Set (SPS), and Picture Parameter Set (PPS) in Annex B format.
137+
- AV1: The first 3 bytes of `AV1CodecConfigurationRecord` (https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox). The remaining configuration OBUs are in the next data packet.
118138

119139
The client should handle this packet and update the decoder accordingly.
120140

121141
### Data packet
122142

123-
Each data packet contains exactly one encoded frame.
143+
Each data packet represents exactly one encoded frame, and if [version and options requirements](#format) are met, some extra information:
144+
145+
* `keyframe`: `true` if the current packet is a keyframe. Many decoders can decode the video stream without knowing if each frame is a keyframe or not, but some decoders require this information.
146+
* `pts`: Presentation timestamp in nanoseconds. When rendering the video in real-time, generally you want to present the decoded frames as they arrive to minimize the latency, but this information can be used to remove processing time deviations when [recording](./record.mdx).
124147

125148
### With `@yume-chan/scrcpy`
126149

@@ -156,6 +179,12 @@ videoPacketStream
156179

157180
Similar to [`options.clipboard`](../options/index.mdx#watch-device-clipboard-changes), don't `await` the `pipeTo`. The returned `Promise` only resolves when `videoSocket` ends, but waiting here and not handling other streams will block `videoSocket`, causing a deadlock.
158181

182+
#### Raw mode
183+
184+
If data packet header is not present, the `keyframe` and `pts` fields will be `undefined`.
185+
186+
The `data` field contains the data in one `read` call, because there is no packet boundaries, it might contain partial or multiple frames.
187+
159188
### With `@yume-chan/adb-scrcpy`
160189

161190
When `video` option is not `false`, `AdbScrcpyClient.videoStream` is a `Promise` that resolves to an `AdbScrcpyVideoStream`.
@@ -167,7 +196,7 @@ interface AdbScrcpyVideoStream {
167196
}
168197
```
169198

170-
It's very similar to the `videoMetadata` and `videoPacketStream` values in the above section, because `AdbScrcpyClient` calls `parseVideoStreamMetadata` and `createMediaStreamTransformer` internally.
199+
It uses [`parseVideoStreamMetadata`](#with-yume-chanscrcpy) and [`createMediaStreamTransformer`](#with-yume-chanscrcpy-1) internally, so the return value is a combination of those two methods.
171200

172201
```ts transpile
173202
if (client.videoSteam) {

0 commit comments

Comments
 (0)