@myerscarpenter/quest-dev 1.4.1 → 2.0.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 (142) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.github/workflows/docs.yml +45 -0
  3. package/.github/workflows/publish.yml +11 -1
  4. package/README.md +27 -0
  5. package/build/cast/decoder.d.ts +48 -0
  6. package/build/cast/decoder.d.ts.map +1 -0
  7. package/build/cast/decoder.js +152 -0
  8. package/build/cast/decoder.js.map +1 -0
  9. package/build/cast/session.d.ts +87 -0
  10. package/build/cast/session.d.ts.map +1 -0
  11. package/build/cast/session.js +565 -0
  12. package/build/cast/session.js.map +1 -0
  13. package/build/commands/logcat.d.ts.map +1 -1
  14. package/build/commands/logcat.js +7 -6
  15. package/build/commands/logcat.js.map +1 -1
  16. package/build/commands/screenshot.d.ts.map +1 -1
  17. package/build/commands/screenshot.js +17 -20
  18. package/build/commands/screenshot.js.map +1 -1
  19. package/build/commands/stay-awake.d.ts +2 -15
  20. package/build/commands/stay-awake.d.ts.map +1 -1
  21. package/build/commands/stay-awake.js +14 -77
  22. package/build/commands/stay-awake.js.map +1 -1
  23. package/build/daemon/cast-manager.d.ts +42 -0
  24. package/build/daemon/cast-manager.d.ts.map +1 -0
  25. package/build/daemon/cast-manager.js +243 -0
  26. package/build/daemon/cast-manager.js.map +1 -0
  27. package/build/daemon/client.d.ts +40 -0
  28. package/build/daemon/client.d.ts.map +1 -0
  29. package/build/daemon/client.js +133 -0
  30. package/build/daemon/client.js.map +1 -0
  31. package/build/daemon/daemon.d.ts +20 -0
  32. package/build/daemon/daemon.d.ts.map +1 -0
  33. package/build/daemon/daemon.js +130 -0
  34. package/build/daemon/daemon.js.map +1 -0
  35. package/build/daemon/deploy.d.ts +44 -0
  36. package/build/daemon/deploy.d.ts.map +1 -0
  37. package/build/daemon/deploy.js +230 -0
  38. package/build/daemon/deploy.js.map +1 -0
  39. package/build/daemon/logcat-manager.d.ts +39 -0
  40. package/build/daemon/logcat-manager.d.ts.map +1 -0
  41. package/build/daemon/logcat-manager.js +194 -0
  42. package/build/daemon/logcat-manager.js.map +1 -0
  43. package/build/daemon/server.d.ts +19 -0
  44. package/build/daemon/server.d.ts.map +1 -0
  45. package/build/daemon/server.js +482 -0
  46. package/build/daemon/server.js.map +1 -0
  47. package/build/daemon/stay-awake-manager.d.ts +22 -0
  48. package/build/daemon/stay-awake-manager.d.ts.map +1 -0
  49. package/build/daemon/stay-awake-manager.js +74 -0
  50. package/build/daemon/stay-awake-manager.js.map +1 -0
  51. package/build/index.js +272 -45
  52. package/build/index.js.map +1 -1
  53. package/build/public/dashboard.js +749 -0
  54. package/build/public/index.html +12 -0
  55. package/build/public/style.css +106 -0
  56. package/build/utils/adb.d.ts +6 -0
  57. package/build/utils/adb.d.ts.map +1 -1
  58. package/build/utils/adb.js +62 -66
  59. package/build/utils/adb.js.map +1 -1
  60. package/build/utils/casting-apk.d.ts +40 -0
  61. package/build/utils/casting-apk.d.ts.map +1 -0
  62. package/build/utils/casting-apk.js +252 -0
  63. package/build/utils/casting-apk.js.map +1 -0
  64. package/build/utils/config.d.ts +5 -3
  65. package/build/utils/config.d.ts.map +1 -1
  66. package/build/utils/config.js +18 -38
  67. package/build/utils/config.js.map +1 -1
  68. package/build/utils/exec.d.ts +5 -0
  69. package/build/utils/exec.d.ts.map +1 -1
  70. package/build/utils/exec.js +17 -0
  71. package/build/utils/exec.js.map +1 -1
  72. package/build/utils/filename.d.ts +7 -1
  73. package/build/utils/filename.d.ts.map +1 -1
  74. package/build/utils/filename.js +17 -2
  75. package/build/utils/filename.js.map +1 -1
  76. package/build/utils/filename.test.js +33 -1
  77. package/build/utils/filename.test.js.map +1 -1
  78. package/build/utils/jpeg-comment.d.ts +14 -0
  79. package/build/utils/jpeg-comment.d.ts.map +1 -0
  80. package/build/utils/jpeg-comment.js +28 -0
  81. package/build/utils/jpeg-comment.js.map +1 -0
  82. package/build/utils/test-properties.d.ts +34 -0
  83. package/build/utils/test-properties.d.ts.map +1 -0
  84. package/build/utils/test-properties.js +73 -0
  85. package/build/utils/test-properties.js.map +1 -0
  86. package/package.json +11 -5
  87. package/packages/cast2-protocol/README.md +86 -0
  88. package/packages/cast2-protocol/docs/_config.yml +4 -0
  89. package/packages/cast2-protocol/docs/feature-flags.md +102 -0
  90. package/packages/cast2-protocol/docs/index.md +24 -0
  91. package/packages/cast2-protocol/docs/open-investigations.md +149 -0
  92. package/packages/cast2-protocol/docs/protocol.md +602 -0
  93. package/packages/cast2-protocol/package.json +46 -0
  94. package/packages/cast2-protocol/src/constants.ts +65 -0
  95. package/packages/cast2-protocol/src/index.ts +7 -0
  96. package/packages/cast2-protocol/src/mgik.ts +69 -0
  97. package/packages/cast2-protocol/src/mud.ts +294 -0
  98. package/packages/cast2-protocol/src/pose.ts +99 -0
  99. package/packages/cast2-protocol/src/resolutions.ts +34 -0
  100. package/packages/cast2-protocol/src/types.ts +64 -0
  101. package/packages/cast2-protocol/src/xrsp.ts +73 -0
  102. package/packages/cast2-protocol/tests/mgik.test.ts +80 -0
  103. package/packages/cast2-protocol/tests/mud.test.ts +295 -0
  104. package/packages/cast2-protocol/tests/pose.test.ts +173 -0
  105. package/packages/cast2-protocol/tests/xrsp.test.ts +90 -0
  106. package/packages/cast2-protocol/tsconfig.json +20 -0
  107. package/pnpm-workspace.yaml +2 -0
  108. package/src/cast/decoder.ts +178 -0
  109. package/src/cast/session.ts +708 -0
  110. package/src/commands/logcat.ts +6 -5
  111. package/src/commands/screenshot.ts +19 -13
  112. package/src/commands/stay-awake.ts +22 -91
  113. package/src/daemon/adbkit-apkreader.d.ts +14 -0
  114. package/src/daemon/cast-manager.ts +282 -0
  115. package/src/daemon/client.ts +166 -0
  116. package/src/daemon/daemon.ts +169 -0
  117. package/src/daemon/deploy.ts +307 -0
  118. package/src/daemon/logcat-manager.ts +229 -0
  119. package/src/daemon/server.ts +595 -0
  120. package/src/daemon/stay-awake-manager.ts +83 -0
  121. package/src/index.ts +326 -56
  122. package/src/public/dashboard.js +288 -0
  123. package/src/public/index.html +12 -0
  124. package/src/public/style.css +106 -0
  125. package/src/utils/adb.ts +70 -57
  126. package/src/utils/casting-apk.ts +276 -0
  127. package/src/utils/config.ts +18 -36
  128. package/src/utils/exec.ts +20 -0
  129. package/src/utils/filename.test.ts +41 -1
  130. package/src/utils/filename.ts +18 -2
  131. package/src/utils/jpeg-comment.ts +30 -0
  132. package/src/utils/test-properties.ts +94 -0
  133. package/tests/cast/auto-layer.test.ts +87 -0
  134. package/tests/cast/decoder.test.ts +82 -0
  135. package/tests/cast/session-restart.test.ts +107 -0
  136. package/tests/config.test.ts +17 -22
  137. package/tests/daemon/api-status.test.ts +82 -0
  138. package/tests/daemon/cast-manager.test.ts +69 -0
  139. package/tests/daemon/mjpeg-stream.test.ts +144 -0
  140. package/tests/daemon/pose-endpoint.test.ts +63 -0
  141. package/tests/daemon/start-guard.test.ts +77 -0
  142. package/vitest.config.ts +10 -0
@@ -0,0 +1,602 @@
1
+ ---
2
+ title: Cast 2.0 Protocol Reference
3
+ layout: default
4
+ ---
5
+
6
+ # Cast 2.0 Protocol Reference
7
+
8
+ Cast 2.0 (codename "Magic Island") is a compositor-level VR streaming protocol
9
+ used by Meta Quest headsets. It streams live H.264 video from the headset to a
10
+ desktop host, with support for remote pose injection, resolution control, and
11
+ input forwarding.
12
+
13
+ ## Architecture
14
+
15
+ ```
16
+ +------------------+ ADB/TCP +---------------------+
17
+ | Desktop Client | <------ XRSP -------> | Quest Headset |
18
+ | (quest-dev) | MGIK messages | (Casting Service) |
19
+ +------------------+ +---------------------+
20
+ | |
21
+ H.264 decoder OpenXR Capture Layer
22
+ + Web dashboard + H.264 HW Encoder
23
+ + Pose controller + XRSP Participant
24
+ ```
25
+
26
+ ### Protocol Stack
27
+
28
+ ```
29
+ ┌─────────────────────────────┐
30
+ │ MUD Messages │ Application layer
31
+ ├─────────────────────────────┤
32
+ │ MGIK Sub-Header (24B) │ Session framing
33
+ ├─────────────────────────────┤
34
+ │ MGIK Header (8B) │ Quest → Desktop only
35
+ ├─────────────────────────────┤
36
+ │ XRSP Packet (8B header) │ Transport framing
37
+ ├─────────────────────────────┤
38
+ │ TCP │ Reliable delivery
39
+ └─────────────────────────────┘
40
+ ```
41
+
42
+ ## TCP Connections
43
+
44
+ Cast 2.0 uses two unidirectional TCP connections, both initiated by the Quest
45
+ to the desktop on port 4445:
46
+
47
+ | Connection | Direction | Carries | MGIK header? |
48
+ |---|---|---|---|
49
+ | Control (first SYN, lower source port) | Desktop → Quest | MUD commands | No |
50
+ | Video (second SYN, higher source port) | Quest → Desktop | Video, LayerConfig, ACKs | Yes |
51
+
52
+ The control connection can be identified because it does **not** carry MGIK
53
+ headers. The video connection can be identified by the TRANSPORT string sent
54
+ as its first message.
55
+
56
+ ### TRANSPORT String
57
+
58
+ The first message on the video connection is a TRANSPORT identification string:
59
+
60
+ ```
61
+ 0 1 2 3
62
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
63
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
64
+ | 0x00000001 |
65
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
66
+ | type (BE u32) |
67
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
68
+ | string length (BE u32) |
69
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
70
+ | ASCII string data ... |
71
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
72
+ ```
73
+
74
+ Example: `"tcpclient,localhost,4445"`
75
+
76
+ ## XRSP Packet Format
77
+
78
+ Each XRSP packet consists of an 8-byte little-endian header followed by
79
+ a payload padded to 4-byte alignment.
80
+
81
+ ```
82
+ 0 1 2 3
83
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
84
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
85
+ | flags | topic_id | word_count (LE u16) |
86
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
87
+ | sequence_number (LE u16) | padding (LE u16) |
88
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
89
+ | |
90
+ | payload (variable) |
91
+ | |
92
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
93
+ ```
94
+
95
+ - **flags**: `0x10` = standard, `0x18` = has alignment padding
96
+ - **topic_id**: always `2` for Cast 2.0
97
+ - **word_count**: total packet size = `word_count × 4` bytes
98
+ - **payload size**: `(word_count - 1) × 4` bytes
99
+
100
+ Cast 2.0 uses only topic 2 (Command) for all traffic — both video and control
101
+ flow through the same topic. This is simpler than Quest Link, which uses 34+
102
+ topics with Cap'n Proto serialization.
103
+
104
+ ## MGIK Header (8 bytes, Quest → Desktop only)
105
+
106
+ ```
107
+ 0 1 2 3
108
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
109
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
110
+ | 'M' | 'G' | 'I' | 'K' |
111
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
112
+ | version | reserved |
113
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
114
+ ```
115
+
116
+ - **magic**: `0x4D47494B` ("MGIK" as big-endian u32)
117
+ - **version**: `0x02` = control message, `0x03` = video data
118
+
119
+ The desktop does **not** send MGIK headers — only the sub-header + payload.
120
+
121
+ ## MGIK Sub-Header (24 bytes, both directions)
122
+
123
+ ```
124
+ 0 1 2 3
125
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
126
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
127
+ | appId (BE u32) |
128
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
129
+ | datagramId (BE u32) |
130
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
131
+ | messageId (BE u32) |
132
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
133
+ | partIndex (BE u32) |
134
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
135
+ | partCount (BE u32) |
136
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
137
+ | qos (BE u32) |
138
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
139
+ ```
140
+
141
+ - **appId**: session-specific magic, learned from the Quest's first message, echoed in all replies
142
+ - **datagramId**: monotonic per-datagram sequence number
143
+ - **messageId**: monotonic per-message sequence number
144
+ - **partIndex**: fragment index (`0` for unfragmented messages)
145
+ - **partCount**: total fragment count (`1` for unfragmented messages)
146
+ - **qos**: always `2` (HIGH)
147
+
148
+ For unfragmented messages (the common case), the last 12 bytes are always
149
+ `0x00000000 0x00000001 0x00000002`.
150
+
151
+ ## Cast Session Handshake
152
+
153
+ After the TCP connections are established, the MGIK-level handshake follows
154
+ this sequence:
155
+
156
+ ```
157
+ Quest Desktop
158
+ | |
159
+ |--- TRANSPORT string (video) ----->| identifies video connection
160
+ |--- Sub-header (control) --------->| desktop learns appId
161
+ | |
162
+ |<-- INIT (×4) --------------------| cmd 0x258, 16 zero bytes
163
+ |<-- START_CAST --------------------| cmd 0x07, width/height/UUID/timestamp
164
+ |<-- CONFIRM (×1) -----------------| cmd 0x12D
165
+ |<-- READY (×1) -------------------| cmd 0x65
166
+ | |
167
+ |--- LayerConfig ------------------>| reports layer info (type, resolution)
168
+ |--- VideoMeta + H.264 NAL ------->| video frames begin flowing
169
+ | |
170
+ |<-- KEEPALIVE ---------------------| cmd 0x04, periodic
171
+ |<-- POSE --------------------------| cmd 0xCE, continuous
172
+ |--- ACK --------------------------->| cumulative acknowledgment
173
+ ```
174
+
175
+ ### CONFIRM Rules
176
+
177
+ CONFIRM messages (cmd 0x12D) follow specific rules:
178
+
179
+ - **1×** after START_CAST (always, initial stream setup)
180
+ - **2×** after a RES_CHANGE that changes width or height (encoder reallocation)
181
+ - **0×** after a RES_CHANGE that only changes eye selection (same dimensions)
182
+
183
+ ## MUD Message Types
184
+
185
+ MUD ("Messages Under Delivery") is the application-level message layer carried
186
+ inside MGIK framing.
187
+
188
+ ### Quest → Desktop
189
+
190
+ | Message | Purpose |
191
+ |---|---|
192
+ | Config | Layer configuration (resolution, serial, APK name) |
193
+ | VideoSegment | H.264 NAL units (main payload) |
194
+ | AudioSegment | Audio data (48kHz stereo PCM) |
195
+ | ACK | Cumulative acknowledgment |
196
+ | Ping | Latency measurement |
197
+ | AppStateChange | App state transitions |
198
+ | MediaAvailable | Media readiness notification |
199
+ | StopCasting | Quest-initiated stop |
200
+ | DestroyLayer | Layer teardown |
201
+
202
+ ### Desktop → Quest
203
+
204
+ | Message | Purpose |
205
+ |---|---|
206
+ | INIT | Session initialization |
207
+ | StartCasting | Begin casting (resolution, bitrate, fps) |
208
+ | StopCasting | End casting |
209
+ | UpdateCastingConfig | Change resolution/bitrate at runtime |
210
+ | Pose | 6DoF virtual camera position and orientation |
211
+ | RES_CHANGE | Resolution and eye selection |
212
+ | CONFIRM | Acknowledge stream setup |
213
+ | KEEPALIVE | Session keepalive |
214
+ | SetProperty | Key-value configuration |
215
+ | SetDeviceEyeFov | FOV parameters |
216
+ | Screenshot | Request screenshot |
217
+ | AudioState | Enable/disable audio |
218
+ | InputForwardingState | Enable/disable input forwarding |
219
+ | StartInputForwarding | Begin input forwarding |
220
+ | VirtualMouse | Mouse cursor position/clicks |
221
+ | ActivateLayer | Select active layer |
222
+
223
+ ## Desktop → Quest Wire Formats
224
+
225
+ All Desktop → Quest messages are framed as:
226
+
227
+ ```
228
+ 0 1 2 3
229
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
230
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
231
+ | |
232
+ | MGIK Sub-Header (24 bytes) |
233
+ | |
234
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
235
+ | command ID (BE u32) |
236
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
237
+ | |
238
+ | command payload (variable) |
239
+ | |
240
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
241
+ ```
242
+
243
+ ### INIT (cmd 0x258)
244
+
245
+ ```
246
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
247
+ | 0x00000258 (INIT) |
248
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
249
+ | |
250
+ | 16 zero bytes |
251
+ | |
252
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
253
+ ```
254
+
255
+ Sent 4 times during handshake.
256
+
257
+ ### START_CAST (cmd 0x07)
258
+
259
+ ```
260
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
261
+ | 0x00000007 (START_CAST) |
262
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
263
+ | width (BE u32) |
264
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
265
+ | height (BE u32) |
266
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
267
+ | 0x00000000 |
268
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
269
+ | 0x00000001 |
270
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
271
+ | 0x00000000 |
272
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
273
+ | 0x00000000 |
274
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
275
+ | UUID (MUD string) |
276
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
277
+ | timestamp (MUD string) |
278
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
279
+ ```
280
+
281
+ ### KEEPALIVE (cmd 0x04)
282
+
283
+ ```
284
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
285
+ | 0x00000004 (KEEPALIVE) |
286
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
287
+ | ack_value (BE u32) |
288
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
289
+ | UUID (MUD string) |
290
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
291
+ | timestamp (MUD string) |
292
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
293
+ ```
294
+
295
+ `ack_value` starts at `0xC8` (200) and increments by `0xC8` each keepalive.
296
+
297
+ ### RES_CHANGE (cmd 0x09)
298
+
299
+ ```
300
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
301
+ | 0x00000009 (RES_CHANGE) |
302
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
303
+ | width (BE u32) |
304
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
305
+ | height (BE u32) |
306
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
307
+ | 0x00000000 |
308
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
309
+ | eye selector (BE u32) |
310
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
311
+ | 0x00000000 |
312
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
313
+ | 0x00000000 |
314
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
315
+ ```
316
+
317
+ Eye selector: `0` = right, `1` = left (default), `2` = stereo (side-by-side).
318
+
319
+ ### POSE (cmd 0xCE)
320
+
321
+ ```
322
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
323
+ | 0x000000CE (POSE) |
324
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
325
+ | type (i32) — 0=VIRTUAL_CAMERA |
326
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
327
+ | posX (BE f32) |
328
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
329
+ | posY (BE f32) |
330
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
331
+ | posZ (BE f32) |
332
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
333
+ | qW (BE f32) |
334
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
335
+ | qX (BE f32) |
336
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
337
+ | qY (BE f32) |
338
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
339
+ | qZ (BE f32) |
340
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
341
+ ```
342
+
343
+ Coordinates follow OpenXR convention: +X=right, +Y=up, -Z=forward. Position
344
+ is relative to the headset's position when casting started (not world space).
345
+
346
+ The desktop sends pose continuously; the Quest does not report pose back.
347
+
348
+ ### SET_PROPERTY (cmd 0x0A)
349
+
350
+ ```
351
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
352
+ | 0x0000000A (SET_PROPERTY) |
353
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
354
+ | key (MUD string) |
355
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
356
+ | value (MUD string) |
357
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
358
+ ```
359
+
360
+ ### VIRTUAL_MOUSE (cmd 0xC8)
361
+
362
+ ```
363
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
364
+ | 0x000000C8 (VIRTUAL_MOUSE) |
365
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
366
+ | layerId (BE u32) |
367
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
368
+ | action (BE u32) |
369
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
370
+ | x (BE f32) |
371
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
372
+ | y (BE f32) |
373
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
374
+ ```
375
+
376
+ Mouse actions: `1` = move, `3` = button down, `4` = button up.
377
+
378
+ ### Simple Commands
379
+
380
+ | cmd | Name | Payload |
381
+ |-----|------|---------|
382
+ | 0x65 (101) | READY | u32(0) |
383
+ | 0x08 (8) | STOP | (empty) |
384
+ | 0xCD (205) | INPUT_FWD_STATE | u32(state) — 0=normal, 1=camera |
385
+ | 0xCF (207) | START_INPUT_FWD | (empty) |
386
+ | 0x12D (301) | CONFIRM / ACTIVATE_LAYER | u32(0) or u32(layerId) |
387
+ | 0x130 (304) | SCREENSHOT | u32(0) + u32(type) — 1=composite, 2=raw |
388
+
389
+ ## Quest → Desktop Wire Formats
390
+
391
+ ### ACK
392
+
393
+ Quest ACKs are bare 8-byte messages with **no** MGIK header and **no** sub-header:
394
+
395
+ ```
396
+ 0 1 2 3
397
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
398
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
399
+ | 0x00000003 (ACK) |
400
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
401
+ | cumulative_ack_counter (BE u32) |
402
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
403
+ ```
404
+
405
+ ACKs are infrequent (~1 per 13 seconds) and cumulative.
406
+
407
+ ### LayerConfig (cmd 0x12C)
408
+
409
+ ```
410
+ 0 1 2 3
411
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
412
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
413
+ | 0x0000012C (LAYER_CONFIG) |
414
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
415
+ | layer id (BE u32) |
416
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
417
+ | width (BE u32) |
418
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
419
+ | height (BE u32) |
420
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
421
+ | posX (BE f32) |
422
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
423
+ | posY (BE f32) |
424
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
425
+ | depth (BE f32) |
426
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
427
+ | layer type (BE u32) |
428
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
429
+ | app name (MUD string) |
430
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
431
+ | layer name (MUD string) |
432
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
433
+ | device serial (MUD string) |
434
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
435
+ | package name (MUD string) |
436
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
437
+ ```
438
+
439
+ Layer types: `0` = PANEL_APP, `1` = EYE_BUFFER, `2` = AETHER_APP, `3` = VOLUMETRIC_WINDOW
440
+
441
+ ### VideoMeta + H.264 NAL
442
+
443
+ Each video frame on the wire is:
444
+
445
+ ```
446
+ MGIK header (8B) + sub-header (24B) + VideoMeta (16B) + H.264 NAL data
447
+ ```
448
+
449
+ ```
450
+ 0 1 2 3
451
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
452
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
453
+ | 0x00000064 (VIDEO_META) |
454
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
455
+ | chunk_type (BE u32) |
456
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
457
+ | frame_index (BE u32) |
458
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
459
+ | reserved |
460
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
461
+ | |
462
+ | H.264 NAL data (Annex B format) |
463
+ | 0x00000001 start codes |
464
+ | |
465
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
466
+ ```
467
+
468
+ Chunk types: `1` = SPS/PPS, `2` = IDR (keyframe), `3` = P-frame
469
+
470
+ ## MUD String Encoding
471
+
472
+ Strings in MUD messages use a length-prefixed encoding with 4-byte alignment:
473
+
474
+ ```
475
+ 0 1 2 3
476
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
477
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
478
+ | byte length (BE u32) |
479
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
480
+ | |
481
+ | UTF-8 string data |
482
+ | |
483
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
484
+ | padding (0-3 zero bytes to align) |
485
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
486
+ ```
487
+
488
+ ## Video Stream
489
+
490
+ The Quest sends H.264 video with these characteristics:
491
+
492
+ - **Profile**: High, Level 5.0
493
+ - **Pixel format**: YUV420P, BT.709 color space
494
+ - **NAL format**: Annex B (0x00000001 start codes)
495
+ - **Frame types**: SPS/PPS, IDR (keyframes), P-frames
496
+ - **Default resolution**: 2064×1162 (16:9 mono)
497
+ - **Default bitrate**: 40 Mbps
498
+
499
+ ### Eye View
500
+
501
+ Eye selection is controlled via RES_CHANGE (cmd 0x09):
502
+
503
+ | Eye | Value | Resolution | Layout |
504
+ |---|---|---|---|
505
+ | Right | 0 | 2064×2208 or 2064×1162 | Single eye |
506
+ | Left (default) | 1 | 2064×2208 or 2064×1162 | Single eye |
507
+ | Stereo | 2 | 4128×2208 | Both eyes side-by-side |
508
+
509
+ START_CAST does not include the eye field — initial eye defaults to left.
510
+
511
+ ### Resolution Presets
512
+
513
+ | Key | Display | Aspect |
514
+ |---|---|---|
515
+ | `1024p36fps` | 1024p @ 36fps | 1:1 (default) |
516
+ | `1024p60fps` | 1024p @ 60fps | 1:1 |
517
+ | `1080p36fps` | 1080p @ 36fps | 16:9 |
518
+ | `1080p60fps` | 1080p @ 60fps | 16:9 |
519
+ | `1440p36fps` | 1440p @ 36fps | 16:9 |
520
+ | `1440p60fps` | 1440p @ 60fps | 16:9 |
521
+ | `2160p36fps` | 2160p @ 36fps | 16:9 |
522
+ | `2160p60fps` | 2160p @ 60fps | 16:9 |
523
+
524
+ ### Bitrate
525
+
526
+ | Value | Quality |
527
+ |---|---|
528
+ | 5 Mbps | Very low (default) |
529
+ | 8 Mbps | Low |
530
+ | 25 Mbps | High |
531
+ | 40 Mbps | Very high |
532
+
533
+ ## ADB Setup
534
+
535
+ ### Casting Lifecycle
536
+
537
+ ```bash
538
+ # Set media capture mode (required before casting)
539
+ adb shell setprop debug.oculus.command_line_media_capture Casting
540
+
541
+ # Disable proximity sensor (prevents sleep when headset removed)
542
+ adb shell am broadcast -a com.oculus.vrpowermanager.prox_close --ei duration 600000
543
+
544
+ # Disable guardian boundaries
545
+ adb shell setprop debug.oculus.guardian_pause 1
546
+
547
+ # Stop casting
548
+ adb shell am broadcast -a com.oculus.magicislandcastingservice.STOP_CASTING
549
+
550
+ # Re-enable proximity sensor
551
+ adb shell am broadcast -a com.oculus.vrpowermanager.automation_disable
552
+ ```
553
+
554
+ ### Casting Service Intents
555
+
556
+ ```
557
+ com.oculus.magicislandcastingservice.START_CASTING
558
+ com.oculus.magicislandcastingservice.STOP_CASTING
559
+ com.oculus.magicislandcastingservice.CONNECT
560
+ com.oculus.magicislandcastingservice.START_RECORDING
561
+ com.oculus.magicislandcastingservice.STOP_RECORDING
562
+ com.oculus.magicislandcastingservice.UPDATE_CASTING_CONFIG
563
+ com.oculus.magicislandcastingservice.SET_DEVICE_EYE_FOV
564
+ com.oculus.magicislandcastingservice.ENABLE_PANEL_STREAMING
565
+ com.oculus.magicislandcastingservice.DISABLE_PANEL_STREAMING
566
+ ```
567
+
568
+ ### Feature Flags
569
+
570
+ Feature flags control optional capabilities:
571
+
572
+ - `wireless_casting_2` — WiFi casting
573
+ - `input_forwarding_2` — Keyboard/mouse forwarding
574
+ - `gaze_click` — Gaze-based click input
575
+ - `text_forwarding` — Text input forwarding
576
+ - `mic_audio` — Microphone audio
577
+ - `image_stabilization` — Image stabilization
578
+ - `obs_support` — OBS integration
579
+ - `panel_streaming` — 2D panel streaming
580
+
581
+ ### Configuration Properties
582
+
583
+ Runtime configuration via SET_PROPERTY (cmd 0x0A). All keys are in the
584
+ `debug.oculus.magic.*` namespace and can also be set via
585
+ `adb shell setprop debug.oculus.magic.<key> <value>`.
586
+
587
+ | Property | Default | Unit |
588
+ |----------|---------|------|
589
+ | `serverPort` | 4445 | TCP port |
590
+ | `captureEncoderBitrate` | 40,000,000 | bits/sec |
591
+ | `maxEncoderFps` | 60 | fps |
592
+ | `minEncoderFps` | 30 | fps |
593
+ | `scalingFactor` | 1.0 | multiplier |
594
+ | `remoteInputFrameValidDuration` | 5.0 | seconds |
595
+ | `targetAudioLatency` | 0.05 | seconds |
596
+ | `audioCaptureBitrate` | 160,000 | bits/sec |
597
+ | `congestionControlWindow` | 1.0 | seconds |
598
+ | `congestionControlMultiplicativeDecrease` | 0.85 | multiplier |
599
+ | `congestionControlAdditiveIncrease` | 250,000 | bits/sec |
600
+ | `transportConnectTimeout` | 5,000 | ms |
601
+ | `transportConnectRetryInterval` | 5,000 | ms |
602
+
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@myerscarpenter/cast2-protocol",
3
+ "version": "0.1.0",
4
+ "description": "Cast 2.0 protocol implementation for Meta Quest video casting — XRSP framing, MGIK sub-headers, MUD message builders, pose math, and resolution presets",
5
+ "type": "module",
6
+ "main": "./build/index.js",
7
+ "types": "./build/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./build/index.d.ts",
11
+ "import": "./build/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "build",
16
+ "docs"
17
+ ],
18
+ "scripts": {
19
+ "clean": "rm -rf build",
20
+ "build": "tsc",
21
+ "test": "vitest run"
22
+ },
23
+ "keywords": [
24
+ "cast2",
25
+ "meta-quest",
26
+ "xrsp",
27
+ "mgik",
28
+ "vr",
29
+ "xr",
30
+ "protocol"
31
+ ],
32
+ "author": "Myers Carpenter <myers@maski.org>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/myers/quest-dev",
37
+ "directory": "packages/cast2-protocol"
38
+ },
39
+ "devDependencies": {
40
+ "typescript": "^5.9.2",
41
+ "vitest": "^4.0.3"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ }
46
+ }