rvis-aiui-kit 1.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 (44) hide show
  1. package/README.md +22 -0
  2. package/components/image/index.ink +44 -0
  3. package/components/list/index.ink +198 -0
  4. package/components/list/readme.md +71 -0
  5. package/components/markdown/index.ink +109 -0
  6. package/components/markdown/parser.js +149 -0
  7. package/components/markdown/readme.md +50 -0
  8. package/components/model-list/index.ink +181 -0
  9. package/components/model-list/readme.md +111 -0
  10. package/components/paragraph/index.ink +61 -0
  11. package/components/paragraph/readme.md +17 -0
  12. package/components/table/index.ink +272 -0
  13. package/components/table/readme.md +83 -0
  14. package/package.json +35 -0
  15. package/sdk/README.md +1013 -0
  16. package/sdk/api-map.js +25 -0
  17. package/sdk/core/client.js +641 -0
  18. package/sdk/core/constants.js +10 -0
  19. package/sdk/core/transport.js +55 -0
  20. package/sdk/index.js +19 -0
  21. package/sdk/modules/audio/index.js +177 -0
  22. package/sdk/modules/audio/readme.md +136 -0
  23. package/sdk/modules/camera/index.js +380 -0
  24. package/sdk/modules/camera/readme.md +302 -0
  25. package/sdk/modules/device-context/index.js +206 -0
  26. package/sdk/modules/device-context/readme.md +243 -0
  27. package/sdk/modules/face/index.js +108 -0
  28. package/sdk/modules/face/readme.md +196 -0
  29. package/sdk/modules/motion/index.js +116 -0
  30. package/sdk/modules/motion/readme.md +148 -0
  31. package/sdk/modules/notification/index.js +192 -0
  32. package/sdk/modules/notification/readme.md +175 -0
  33. package/sdk/modules/offline-command/index.js +265 -0
  34. package/sdk/modules/offline-command/readme.md +143 -0
  35. package/sdk/modules/screen/index.js +64 -0
  36. package/sdk/modules/screen/readme.md +110 -0
  37. package/sdk/modules/tts/index.js +75 -0
  38. package/sdk/modules/tts/readme.md +162 -0
  39. package/sdk/utils/api-builder.js +16 -0
  40. package/sdk/utils/case.js +19 -0
  41. package/sdk/utils/errors.js +32 -0
  42. package/sdk/utils/events.js +22 -0
  43. package/sdk/utils/message.js +63 -0
  44. package/sdk/utils/request-id.js +18 -0
@@ -0,0 +1,380 @@
1
+ import {
2
+ COMPLETION_EVENT,
3
+ COMPLETION_READY_EVENT_STREAM
4
+ } from '../../core/constants.js';
5
+ import { snakeToCamelCase } from '../../utils/case.js';
6
+ import { Glass3Error } from '../../utils/errors.js';
7
+
8
+ const PREVIEW_LAYOUT_FIELDS = [
9
+ 'left',
10
+ 'top',
11
+ 'width',
12
+ 'height',
13
+ 'cornerRadius'
14
+ ];
15
+
16
+ const PREVIEW_FIELDS = PREVIEW_LAYOUT_FIELDS.concat('outline');
17
+
18
+ const DEFAULT_PREVIEW_ARGS = {
19
+ left: 0,
20
+ top: 40,
21
+ width: 168,
22
+ height: 103,
23
+ cornerRadius: 2,
24
+ outline: true
25
+ };
26
+
27
+ function createParameterError(message) {
28
+ return new Glass3Error(message, {
29
+ code: 'INVALID_PARAMS',
30
+ stage: 'params',
31
+ namespace: 'rokid.tools',
32
+ method: 'invoke'
33
+ });
34
+ }
35
+
36
+ function validatePreviewValue(name, value) {
37
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
38
+ throw createParameterError(`camera.startPreview ${name} must be a finite number`);
39
+ }
40
+
41
+ if ((name === 'width' || name === 'height') && value <= 0) {
42
+ throw createParameterError(`camera.startPreview ${name} must be greater than 0`);
43
+ }
44
+
45
+ if (
46
+ (name === 'left' || name === 'top' || name === 'cornerRadius') &&
47
+ value < 0
48
+ ) {
49
+ throw createParameterError(`camera.startPreview ${name} must not be negative`);
50
+ }
51
+ }
52
+
53
+ function buildStartPreviewArgs(args) {
54
+ if (
55
+ args !== undefined &&
56
+ args !== null &&
57
+ (typeof args !== 'object' || Array.isArray(args))
58
+ ) {
59
+ throw createParameterError('camera.startPreview parameters must be an object');
60
+ }
61
+
62
+ const input = args || {};
63
+ const inputFields = Object.keys(input);
64
+ let suppliedLayoutCount = 0;
65
+
66
+ PREVIEW_LAYOUT_FIELDS.forEach(name => {
67
+ if (Object.prototype.hasOwnProperty.call(input, name)) {
68
+ suppliedLayoutCount += 1;
69
+ }
70
+ });
71
+
72
+ inputFields.forEach(name => {
73
+ if (PREVIEW_FIELDS.indexOf(name) === -1) {
74
+ throw createParameterError(`camera.startPreview does not support ${name}`);
75
+ }
76
+ });
77
+
78
+ if (inputFields.length === 0) {
79
+ return {
80
+ left: DEFAULT_PREVIEW_ARGS.left,
81
+ top: DEFAULT_PREVIEW_ARGS.top,
82
+ width: DEFAULT_PREVIEW_ARGS.width,
83
+ height: DEFAULT_PREVIEW_ARGS.height,
84
+ cornerRadius: DEFAULT_PREVIEW_ARGS.cornerRadius,
85
+ outline: DEFAULT_PREVIEW_ARGS.outline
86
+ };
87
+ }
88
+
89
+ if (
90
+ suppliedLayoutCount !== 0 &&
91
+ suppliedLayoutCount !== PREVIEW_LAYOUT_FIELDS.length
92
+ ) {
93
+ throw createParameterError(
94
+ 'camera.startPreview requires left, top, width, height and cornerRadius together'
95
+ );
96
+ }
97
+
98
+ if (
99
+ Object.prototype.hasOwnProperty.call(input, 'outline') &&
100
+ typeof input.outline !== 'boolean'
101
+ ) {
102
+ throw createParameterError('camera.startPreview outline must be a boolean');
103
+ }
104
+
105
+ PREVIEW_LAYOUT_FIELDS.forEach(name => {
106
+ if (suppliedLayoutCount > 0) {
107
+ validatePreviewValue(name, input[name]);
108
+ }
109
+ });
110
+
111
+ return {
112
+ left: suppliedLayoutCount > 0 ? input.left : DEFAULT_PREVIEW_ARGS.left,
113
+ top: suppliedLayoutCount > 0 ? input.top : DEFAULT_PREVIEW_ARGS.top,
114
+ width: suppliedLayoutCount > 0 ? input.width : DEFAULT_PREVIEW_ARGS.width,
115
+ height: suppliedLayoutCount > 0 ? input.height : DEFAULT_PREVIEW_ARGS.height,
116
+ cornerRadius: suppliedLayoutCount > 0
117
+ ? input.cornerRadius
118
+ : DEFAULT_PREVIEW_ARGS.cornerRadius,
119
+ outline: input.outline === undefined
120
+ ? DEFAULT_PREVIEW_ARGS.outline
121
+ : input.outline
122
+ };
123
+ }
124
+
125
+ function buildStopPreviewArgs(args) {
126
+ if (!args || typeof args.requestId !== 'string' || !args.requestId) {
127
+ throw createParameterError('camera.stopPreview requires a requestId');
128
+ }
129
+
130
+ return {
131
+ targetRequestId: args.requestId
132
+ };
133
+ }
134
+
135
+ function buildTakePhotoArgs(args) {
136
+ if (
137
+ args !== undefined &&
138
+ args !== null &&
139
+ (typeof args !== 'object' || Array.isArray(args))
140
+ ) {
141
+ throw createParameterError('camera.takePhoto parameters must be an object');
142
+ }
143
+
144
+ const input = args || {};
145
+ const inputFields = Object.keys(input);
146
+ inputFields.forEach(name => {
147
+ if (
148
+ name !== 'returnBase64' &&
149
+ name !== 'upload' &&
150
+ name !== 'timeout'
151
+ ) {
152
+ throw createParameterError(`camera.takePhoto does not support ${name}`);
153
+ }
154
+ });
155
+
156
+ const returnBase64 = input.returnBase64 === undefined
157
+ ? true
158
+ : input.returnBase64;
159
+ const upload = input.upload === undefined ? false : input.upload;
160
+
161
+ if (typeof returnBase64 !== 'boolean') {
162
+ throw createParameterError(
163
+ 'camera.takePhoto returnBase64 must be a boolean'
164
+ );
165
+ }
166
+
167
+ if (typeof upload !== 'boolean') {
168
+ throw createParameterError('camera.takePhoto upload must be a boolean');
169
+ }
170
+
171
+ const timeout = input.timeout === undefined ? 5000 : input.timeout;
172
+ if (!Number.isInteger(timeout) || timeout <= 0) {
173
+ throw createParameterError(
174
+ 'camera.takePhoto timeout must be a positive integer in milliseconds'
175
+ );
176
+ }
177
+
178
+ return { returnBase64, upload };
179
+ }
180
+
181
+ function getTakePhotoTimeoutMs(args) {
182
+ const input = args || {};
183
+ return input.timeout === undefined ? 5000 : input.timeout;
184
+ }
185
+
186
+ function buildStartTakeVideoArgs(args) {
187
+ if (
188
+ args !== undefined &&
189
+ args !== null &&
190
+ (typeof args !== 'object' || Array.isArray(args))
191
+ ) {
192
+ throw createParameterError(
193
+ 'camera.startTakeVideo parameters must be an object'
194
+ );
195
+ }
196
+
197
+ const input = args || {};
198
+ const inputFields = Object.keys(input);
199
+ inputFields.forEach(name => {
200
+ if (name !== 'enableAudio' && name !== 'segmentMinutes') {
201
+ throw createParameterError(
202
+ `camera.startTakeVideo does not support ${name}`
203
+ );
204
+ }
205
+ });
206
+
207
+ const enableAudio = input.enableAudio === undefined
208
+ ? true
209
+ : input.enableAudio;
210
+ const segmentMinutes = input.segmentMinutes === undefined
211
+ ? 20
212
+ : input.segmentMinutes;
213
+
214
+ if (typeof enableAudio !== 'boolean') {
215
+ throw createParameterError(
216
+ 'camera.startTakeVideo enableAudio must be a boolean'
217
+ );
218
+ }
219
+
220
+ if (!Number.isInteger(segmentMinutes) || segmentMinutes <= 0) {
221
+ throw createParameterError(
222
+ 'camera.startTakeVideo segmentMinutes must be a positive integer'
223
+ );
224
+ }
225
+
226
+ return {
227
+ enable_audio: enableAudio,
228
+ segment_minutes: segmentMinutes
229
+ };
230
+ }
231
+
232
+ function buildStopTakeVideoArgs(args) {
233
+ if (
234
+ !args ||
235
+ typeof args !== 'object' ||
236
+ Array.isArray(args) ||
237
+ typeof args.requestId !== 'string' ||
238
+ !args.requestId
239
+ ) {
240
+ throw createParameterError('camera.stopTakeVideo requires a requestId');
241
+ }
242
+
243
+ const inputFields = Object.keys(args);
244
+ if (inputFields.length !== 1 || inputFields[0] !== 'requestId') {
245
+ throw createParameterError('camera.stopTakeVideo only supports requestId');
246
+ }
247
+
248
+ return {
249
+ targetRequestId: args.requestId
250
+ };
251
+ }
252
+
253
+ function selectEventResult(eventData) {
254
+ return Object.assign(
255
+ { requestId: eventData.requestId },
256
+ eventData.result
257
+ );
258
+ }
259
+
260
+ const cameraStartPreview = {
261
+ publicModule: 'camera',
262
+ publicMethod: 'startPreview',
263
+
264
+ namespace: 'rokid.tools',
265
+ method: 'invoke',
266
+ toolName: 'cameraPreview',
267
+ toolAction: 'start',
268
+
269
+ completion: COMPLETION_EVENT,
270
+ eventNamespace: 'rokid.tools',
271
+ eventName: 'toolResult',
272
+ terminalStates: ['started'],
273
+ responseCompletionStates: ['started'],
274
+ buildArgs: buildStartPreviewArgs,
275
+ selectEventResult
276
+ };
277
+
278
+ const cameraStopPreview = {
279
+ publicModule: 'camera',
280
+ publicMethod: 'stopPreview',
281
+
282
+ namespace: 'rokid.tools',
283
+ method: 'invoke',
284
+ toolName: 'cameraPreview',
285
+ toolAction: 'stop',
286
+
287
+ completion: COMPLETION_EVENT,
288
+ eventNamespace: 'rokid.tools',
289
+ eventName: 'toolResult',
290
+ terminalStates: ['cancel'],
291
+ buildArgs: buildStopPreviewArgs,
292
+ selectEventResult
293
+ };
294
+
295
+ const cameraTakePhoto = {
296
+ publicModule: 'camera',
297
+ publicMethod: 'takePhoto',
298
+
299
+ namespace: 'rokid.tools',
300
+ method: 'invoke',
301
+ toolName: 'takePhoto',
302
+ toolAction: 'start',
303
+
304
+ completion: COMPLETION_EVENT,
305
+ eventNamespace: 'rokid.tools',
306
+ eventName: 'toolResult',
307
+ isTerminalResult(result) {
308
+ return Boolean(result && result.ok === true);
309
+ },
310
+ buildArgs: buildTakePhotoArgs,
311
+ getTimeoutMs: getTakePhotoTimeoutMs,
312
+ transformEventResult: snakeToCamelCase,
313
+ selectEventResult(eventData) {
314
+ return Object.assign(
315
+ { requestId: eventData.requestId },
316
+ snakeToCamelCase(eventData.result)
317
+ );
318
+ }
319
+ };
320
+
321
+ const cameraStartTakeVideo = {
322
+ publicModule: 'camera',
323
+ publicMethod: 'startTakeVideo',
324
+
325
+ namespace: 'rokid.tools',
326
+ method: 'invoke',
327
+ toolName: 'lawEnforcementRecord',
328
+ toolAction: 'start',
329
+
330
+ completion: COMPLETION_READY_EVENT_STREAM,
331
+ eventNamespace: 'rokid.tools',
332
+ eventName: 'toolResult',
333
+ eventToolActions: ['start', 'stop'],
334
+ responseCompletionStates: ['started'],
335
+ buildArgs: buildStartTakeVideoArgs,
336
+ transformEventResult: snakeToCamelCase,
337
+ isReadyResult(result) {
338
+ return result.state === 'started';
339
+ },
340
+ isStreamTerminalResult(result) {
341
+ return result.hasMore === false || result.state === 'canceled';
342
+ },
343
+ selectReadyResult(requestId, result) {
344
+ return {
345
+ requestId,
346
+ ok: result.ok
347
+ };
348
+ }
349
+ };
350
+
351
+ const cameraStopTakeVideo = {
352
+ publicModule: 'camera',
353
+ publicMethod: 'stopTakeVideo',
354
+
355
+ namespace: 'rokid.tools',
356
+ method: 'invoke',
357
+ toolName: 'lawEnforcementRecord',
358
+ toolAction: 'stop',
359
+
360
+ completion: COMPLETION_EVENT,
361
+ eventNamespace: 'rokid.tools',
362
+ eventName: 'toolResult',
363
+ terminalStates: ['finished'],
364
+ buildArgs: buildStopTakeVideoArgs,
365
+ transformEventResult: snakeToCamelCase,
366
+ selectEventResult(eventData) {
367
+ return Object.assign(
368
+ { requestId: eventData.requestId },
369
+ snakeToCamelCase(eventData.result)
370
+ );
371
+ }
372
+ };
373
+
374
+ export default [
375
+ cameraStartPreview,
376
+ cameraStopPreview,
377
+ cameraTakePhoto,
378
+ cameraStartTakeVideo,
379
+ cameraStopTakeVideo
380
+ ];
@@ -0,0 +1,302 @@
1
+ # Camera API
2
+
3
+ Camera 模块通过 `glass3.camera` 暴露拍照、相机预览和分段录像能力。
4
+
5
+ 页面必须把宿主的 `onMessage` 消息交给 `glass3.handleMessage`。页面销毁时可调用 `glass3.dispose()` 清理仍在等待的本地调用。
6
+
7
+ ```js
8
+ import glass3 from 'rvis-aiui-kit';
9
+ ```
10
+
11
+ 以上路径适用于 `pages` 下的一级页面,其他目录层级需要相应调整相对路径。
12
+
13
+ ## API 列表
14
+
15
+ | API | 说明 |
16
+ | --- | --- |
17
+ | `camera.startPreview` | 开启相机画面预览。 |
18
+ | `camera.stopPreview` | 关闭指定的相机预览。 |
19
+ | `camera.takePhoto` | 拍摄照片并等待 Native 返回照片结果。 |
20
+ | `camera.startTakeVideo` | 开始分段录像并持续接收文件事件。 |
21
+ | `camera.stopTakeVideo` | 停止指定的录像。 |
22
+
23
+ ## `camera.startPreview`
24
+
25
+ ```js
26
+ const preview = await glass3.camera.startPreview();
27
+ ```
28
+
29
+ 不传参数或传空对象时,SDK 会补齐以下默认值:
30
+
31
+ ```js
32
+ {
33
+ left: 0,
34
+ top: 40,
35
+ width: 168,
36
+ height: 103,
37
+ cornerRadius: 2,
38
+ outline: true
39
+ }
40
+ ```
41
+
42
+ 也可以自定义预览区域:
43
+
44
+ ```js
45
+ const preview = await glass3.camera.startPreview({
46
+ left: 40,
47
+ top: 60,
48
+ width: 240,
49
+ height: 180,
50
+ cornerRadius: 16,
51
+ outline: false
52
+ });
53
+ ```
54
+
55
+ 只要传入任意一个位置或尺寸参数,就必须同时传入全部五个区域字段。`outline` 可以单独传入;省略时默认为 `true`。SDK 调用 Native 时使用同名的 `outline` 参数。
56
+
57
+ | 字段 | 类型 | 约束 |
58
+ | --- | --- | --- |
59
+ | `left` | `number` | 有限数字且不能为负数。 |
60
+ | `top` | `number` | 有限数字且不能为负数。 |
61
+ | `width` | `number` | 有限数字且必须大于 `0`。 |
62
+ | `height` | `number` | 有限数字且必须大于 `0`。 |
63
+ | `cornerRadius` | `number` | 有限数字且不能为负数。 |
64
+ | `outline` | `boolean` | 是否使用线框方式呈现预览流画面,默认为 `true`。 |
65
+
66
+ Promise 在收到以下任一消息后完成:
67
+
68
+ - `kind: "event"` 且 `data.result.state: "started"`;
69
+ - 已通过请求校验的 `kind: "response"` 且 `result.state: "started"`。
70
+
71
+ response 消息中的 `ok` 位于外层,而 event 消息中的 `ok` 位于 `data.result`。SDK 会将两种结构统一为以下基础返回格式;event 结果包含的其他 Native 字段仍会保留:
72
+
73
+ ```js
74
+ {
75
+ requestId: '预览启动 UUID',
76
+ ok: true,
77
+ state: 'started'
78
+ }
79
+ ```
80
+
81
+ Native 映射:`rokid.tools / invoke / cameraPreview / start`。
82
+
83
+ ## `camera.stopPreview`
84
+
85
+ ```js
86
+ const result = await glass3.camera.stopPreview({
87
+ requestId: preview.requestId
88
+ });
89
+ ```
90
+
91
+ | 字段 | 类型 | 必填 | 说明 |
92
+ | --- | --- | --- | --- |
93
+ | `requestId` | `string` | 是 | `startPreview` 返回的预览启动 ID。 |
94
+
95
+ SDK 会将其转换为 Native 参数 `targetRequestId`。stop 调用会生成自己的 requestId,并在收到自己的 `state: "cancel"` 事件后完成。
96
+
97
+ ```js
98
+ {
99
+ requestId: '本次 stop UUID',
100
+ ok: true,
101
+ state: 'cancel'
102
+ }
103
+ ```
104
+
105
+ Native 映射:`rokid.tools / invoke / cameraPreview / stop`。
106
+
107
+ ## `camera.takePhoto`
108
+
109
+ 拍摄一张照片。调用不传参数时,SDK 默认要求 Native 返回 Base64,且不上传服务器:
110
+
111
+ ```js
112
+ const photo = await glass3.camera.takePhoto();
113
+ ```
114
+
115
+ 也可以显式控制返回方式:
116
+
117
+ ```js
118
+ const photo = await glass3.camera.takePhoto({
119
+ returnBase64: false,
120
+ upload: true,
121
+ timeout: 8000
122
+ });
123
+ ```
124
+
125
+ | 字段 | 类型 | 必填 | 默认值 | 说明 |
126
+ | --- | --- | --- | --- | --- |
127
+ | `returnBase64` | `boolean` | 否 | `true` | 是否在结果中返回 JPEG Base64。 |
128
+ | `upload` | `boolean` | 否 | `false` | 是否将照片上传服务器。 |
129
+ | `timeout` | `number` | 否 | `5000` | JS 等待照片结果的超时时间,单位毫秒,必须是大于 0 的整数。 |
130
+
131
+ 参数是闭合对象,只支持上面的三个字段。`timeout` 仅用于 JS 侧控制
132
+ Promise,不会发送给 Native。SDK 发送给 Native 的 `args` 仍只包含:
133
+
134
+ ```js
135
+ {
136
+ returnBase64: false,
137
+ upload: true
138
+ }
139
+ ```
140
+
141
+ 超时从调用 `takePhoto()` 时开始计算,包含等待 Native accepted response 和最终
142
+ 照片 event 的时间。fetch 返回 Native 已受理时 Promise 不会完成。只有在超时前
143
+ 收到同一 requestId 的成功 `toolResult` 事件后,`await` 才会结束。
144
+
145
+ 超过 `timeout` 后,Promise 会以 `Glass3Error` 拒绝:
146
+
147
+ ```js
148
+ try {
149
+ await glass3.camera.takePhoto({ timeout: 5000 });
150
+ } catch (error) {
151
+ if (error.code === 'CALL_TIMEOUT') {
152
+ // error.stage === 'timeout'
153
+ }
154
+ }
155
+ ```
156
+
157
+ 超时后 SDK 会释放本次调用;随后到达的 response 或照片 event 会被忽略。超时只
158
+ 停止 JS 等待,不表示 Native 拍照操作一定被取消。
159
+
160
+ Native 结果中的下划线字段会递归转换为驼峰,公开返回值示例:
161
+
162
+ ```js
163
+ {
164
+ requestId: '本次拍照 UUID',
165
+ ok: true,
166
+ photoSize: 41056,
167
+ filePath: '/storage/emulated/0/.../tool_photo_xxx.jpg',
168
+ width: 1080,
169
+ height: 720,
170
+ photoMime: 'image/jpeg',
171
+ photoBase64: '...',
172
+ fileUrl: 'https://...',
173
+ inspectionResultId: 42,
174
+ uploadError: 'no_session'
175
+ }
176
+ ```
177
+
178
+ `photoMime` 和 `photoBase64` 仅在 `returnBase64: true` 时出现。`fileUrl` 与 `inspectionResultId` 仅在上传成功时出现;上传失败时照片调用本身仍可成功,并通过 `uploadError` 描述上传错误。
179
+
180
+ Native 映射:`rokid.tools / invoke / takePhoto / start`。
181
+
182
+ ## `camera.startTakeVideo`
183
+
184
+ 开始分段录像。请求通过校验后,Promise 在收到以下任一消息时完成:
185
+
186
+ - `kind: "event"` 且 `data.result.state: "started"`;
187
+ - `kind: "response"` 且 `result.state: "started"`。
188
+
189
+ 后续分段文件继续通过 `onEvent` 回调。response 不会作为 event 传给 `onEvent`。
190
+
191
+ ```js
192
+ const recording = await glass3.camera.startTakeVideo(
193
+ {
194
+ enableAudio: true,
195
+ segmentMinutes: 20
196
+ },
197
+ {
198
+ onEvent(eventResult) {
199
+ console.log('recording event:', JSON.stringify(eventResult));
200
+ }
201
+ }
202
+ );
203
+ ```
204
+
205
+ | 字段 | 类型 | 必填 | 默认值 | 说明 |
206
+ | --- | --- | --- | --- | --- |
207
+ | `enableAudio` | `boolean` | 否 | `true` | 是否录音。发送给 Native 时转换为 `enable_audio`。 |
208
+ | `segmentMinutes` | `number` | 否 | `20` | 分段时长,必须是正整数。发送给 Native 时转换为 `segment_minutes`。 |
209
+
210
+ 启动返回值:
211
+
212
+ ```js
213
+ {
214
+ requestId: '录像启动 UUID',
215
+ ok: true
216
+ }
217
+ ```
218
+
219
+ `onEvent` 包含启动事件、每一段文件事件和异常事件。Native 下划线字段会递归转换为驼峰:
220
+
221
+ ```js
222
+ { ok: true, state: 'started' }
223
+
224
+ { ok: true, state: 'canceled' }
225
+
226
+ {
227
+ ok: true,
228
+ filePath: '/storage/emulated/0/Pictures/example.mp4',
229
+ startTime: 1784543000000,
230
+ endTime: 1784544200000,
231
+ hasMore: true
232
+ }
233
+
234
+ {
235
+ ok: false,
236
+ errorCode: 500,
237
+ error: 'recording failed'
238
+ }
239
+ ```
240
+
241
+ `state: "canceled"` 表示录像被系统停止,而不是开发者主动调用 `stopTakeVideo`。该事件有以下特殊关联规则:
242
+
243
+ - `requestId` 是原 `startTakeVideo` 的 requestId。
244
+ - `toolName` 是 `lawEnforcementRecord`。
245
+ - `toolAction` 是 `stop`,但事件仍属于原 start 调用。
246
+ - SDK 会先把 `{ ok: true, state: "canceled" }` 透传给 start 的 `onEvent`,然后清理该录像流监听。
247
+
248
+ Native 原始事件示例:
249
+
250
+ ```json
251
+ {
252
+ "version": "2.0.0",
253
+ "kind": "event",
254
+ "namespace": "rokid.tools",
255
+ "event": "toolResult",
256
+ "data": {
257
+ "requestId": "原 start requestId",
258
+ "toolName": "lawEnforcementRecord",
259
+ "toolAction": "stop",
260
+ "frameId": "",
261
+ "result": {
262
+ "ok": true,
263
+ "state": "canceled"
264
+ },
265
+ "extra": {}
266
+ }
267
+ }
268
+ ```
269
+
270
+ `hasMore: false` 表示最后一段,SDK 同样会在透传该事件后清理监听。启动前收到错误会拒绝 Promise;启动后的错误会通过 `onEvent` 透传并结束事件监听。
271
+
272
+ 下行调用映射为 `rokid.tools / invoke / lawEnforcementRecord / start`。上行事件通常使用 `toolAction: "start"`,系统取消事件使用 `toolAction: "stop"`。
273
+
274
+ ## `camera.stopTakeVideo`
275
+
276
+ ```js
277
+ const result = await glass3.camera.stopTakeVideo({
278
+ requestId: recording.requestId
279
+ });
280
+ ```
281
+
282
+ | 字段 | 类型 | 必填 | 说明 |
283
+ | --- | --- | --- | --- |
284
+ | `requestId` | `string` | 是 | `startTakeVideo` 返回的录像启动 ID。只允许传该字段。 |
285
+
286
+ SDK 会将参数转换成 `{ targetRequestId }`。stop 使用独立 requestId,并在收到自己的 `state: "finished"` 事件后完成。
287
+
288
+ ```js
289
+ {
290
+ requestId: '本次 stop UUID',
291
+ ok: true,
292
+ state: 'finished'
293
+ }
294
+ ```
295
+
296
+ `stopTakeVideo` 与 `startTakeVideo` 的本地事件监听相互独立,stop 不会主动清理 start 的监听。
297
+
298
+ Native 映射:`rokid.tools / invoke / lawEnforcementRecord / stop`。
299
+
300
+ ## 错误
301
+
302
+ 参数错误会抛出错误码为 `INVALID_PARAMS` 的 `Glass3Error`。Native 事件返回 `ok: false` 时,尚未完成的 Promise 会以 `NATIVE_EVENT_ERROR` 拒绝。