arcane-os 0.2.2 → 0.3.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.
- package/CHANGELOG.md +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -1,20 +1,1402 @@
|
|
|
1
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
1
2
|
import GifEncoder from './GifEncoder.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export const SCREEN_CAPTURE_EVENT_TYPES=Object.freeze({
|
|
5
|
+
displaySelectionRequested:'capture-requesting',
|
|
6
|
+
captureStarted:'capture-start',
|
|
7
|
+
captureCompleted:'capture-result',
|
|
8
|
+
captureFailed:'capture-error',
|
|
9
|
+
captureStopped:'capture-stop'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const SCREEN_CAPTURE_STATUSES=Object.freeze({
|
|
13
|
+
selectingDisplay:'selecting-display',
|
|
14
|
+
recording:'recording',
|
|
15
|
+
captureReady:'capture-ready',
|
|
16
|
+
captureRejected:'capture-rejected',
|
|
17
|
+
captureStopped:'capture-stopped'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const SCREEN_CAPTURE_IMAGE_TYPE_FALLBACK=Object.freeze({
|
|
21
|
+
when:'blob-type-unreported',
|
|
22
|
+
mimeType:'image/png',
|
|
23
|
+
extension:'png'
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const SCREEN_CAPTURE_ERRORS=Object.freeze({
|
|
27
|
+
active:Object.freeze({
|
|
28
|
+
code:'ARCANE_SCREEN_CAPTURE_OPERATION_ACTIVE',
|
|
29
|
+
reason:'screen-capture-operation-active'
|
|
30
|
+
}),
|
|
31
|
+
disposed:Object.freeze({
|
|
32
|
+
code:'ARCANE_SCREEN_CAPTURE_DISPOSED',
|
|
33
|
+
reason:'screen-capture-disposed'
|
|
34
|
+
}),
|
|
35
|
+
optionsRecordRejected:Object.freeze({
|
|
36
|
+
code:'ARCANE_SCREEN_CAPTURE_OPTIONS_RECORD_REJECTED',
|
|
37
|
+
reason:'screen-capture-options-record-rejected'
|
|
38
|
+
}),
|
|
39
|
+
abortSignalRejected:Object.freeze({
|
|
40
|
+
code:'ARCANE_SCREEN_CAPTURE_ABORT_SIGNAL_REJECTED',
|
|
41
|
+
reason:'screen-capture-abort-signal-rejected'
|
|
42
|
+
}),
|
|
43
|
+
operationIdRejected:Object.freeze({
|
|
44
|
+
code:'ARCANE_SCREEN_CAPTURE_OPERATION_ID_REJECTED',
|
|
45
|
+
reason:'screen-capture-operation-id-rejected'
|
|
46
|
+
}),
|
|
47
|
+
cleanupRejected:Object.freeze({
|
|
48
|
+
code:'ARCANE_SCREEN_CAPTURE_CLEANUP_REJECTED',
|
|
49
|
+
reason:'screen-capture-cleanup-rejected'
|
|
50
|
+
}),
|
|
51
|
+
displayUnavailable:Object.freeze({
|
|
52
|
+
code:'ARCANE_SCREEN_CAPTURE_DISPLAY_UNAVAILABLE',
|
|
53
|
+
reason:'screen-capture-display-unavailable'
|
|
54
|
+
}),
|
|
55
|
+
displayMetadataRejected:Object.freeze({
|
|
56
|
+
code:'ARCANE_SCREEN_CAPTURE_DISPLAY_METADATA_REJECTED',
|
|
57
|
+
reason:'screen-capture-display-metadata-rejected'
|
|
58
|
+
}),
|
|
59
|
+
displayPlaybackRejected:Object.freeze({
|
|
60
|
+
code:'ARCANE_SCREEN_CAPTURE_DISPLAY_PLAYBACK_REJECTED',
|
|
61
|
+
reason:'screen-capture-display-playback-rejected'
|
|
62
|
+
}),
|
|
63
|
+
displaySelectionCancelled:Object.freeze({
|
|
64
|
+
code:'ARCANE_SCREEN_CAPTURE_DISPLAY_SELECTION_CANCELLED',
|
|
65
|
+
reason:'screen-capture-display-selection-cancelled'
|
|
66
|
+
}),
|
|
67
|
+
displaySelectionRejected:Object.freeze({
|
|
68
|
+
code:'ARCANE_SCREEN_CAPTURE_DISPLAY_SELECTION_REJECTED',
|
|
69
|
+
reason:'screen-capture-display-selection-rejected'
|
|
70
|
+
}),
|
|
71
|
+
displayTrackEndedBeforeRecording:Object.freeze({
|
|
72
|
+
code:'ARCANE_SCREEN_CAPTURE_DISPLAY_TRACK_ENDED_BEFORE_RECORDING',
|
|
73
|
+
reason:'screen-capture-display-track-ended-before-recording'
|
|
74
|
+
}),
|
|
75
|
+
eventTypeUndeclared:Object.freeze({
|
|
76
|
+
code:'ARCANE_SCREEN_CAPTURE_EVENT_TYPE_UNDECLARED',
|
|
77
|
+
reason:'screen-capture-event-type-undeclared'
|
|
78
|
+
}),
|
|
79
|
+
gifEncodingRejected:Object.freeze({
|
|
80
|
+
code:'ARCANE_SCREEN_CAPTURE_GIF_ENCODING_REJECTED',
|
|
81
|
+
reason:'screen-capture-gif-encoding-rejected'
|
|
82
|
+
}),
|
|
83
|
+
gifFrameReadRejected:Object.freeze({
|
|
84
|
+
code:'ARCANE_SCREEN_CAPTURE_GIF_FRAME_READ_REJECTED',
|
|
85
|
+
reason:'screen-capture-gif-frame-read-rejected'
|
|
86
|
+
}),
|
|
87
|
+
imageCanvasUnavailable:Object.freeze({
|
|
88
|
+
code:'ARCANE_SCREEN_CAPTURE_IMAGE_CANVAS_UNAVAILABLE',
|
|
89
|
+
reason:'screen-capture-image-canvas-unavailable'
|
|
90
|
+
}),
|
|
91
|
+
imageDrawRejected:Object.freeze({
|
|
92
|
+
code:'ARCANE_SCREEN_CAPTURE_IMAGE_DRAW_REJECTED',
|
|
93
|
+
reason:'screen-capture-image-draw-rejected'
|
|
94
|
+
}),
|
|
95
|
+
imageEncodingRejected:Object.freeze({
|
|
96
|
+
code:'ARCANE_SCREEN_CAPTURE_IMAGE_ENCODING_REJECTED',
|
|
97
|
+
reason:'screen-capture-image-encoding-rejected'
|
|
98
|
+
}),
|
|
99
|
+
recorderConstructionRejected:Object.freeze({
|
|
100
|
+
code:'ARCANE_SCREEN_CAPTURE_RECORDER_CONSTRUCTION_REJECTED',
|
|
101
|
+
reason:'screen-capture-recorder-construction-rejected'
|
|
102
|
+
}),
|
|
103
|
+
recorderErrorReceived:Object.freeze({
|
|
104
|
+
code:'ARCANE_SCREEN_CAPTURE_RECORDER_ERROR_RECEIVED',
|
|
105
|
+
reason:'screen-capture-recorder-error-received'
|
|
106
|
+
}),
|
|
107
|
+
recorderStartRejected:Object.freeze({
|
|
108
|
+
code:'ARCANE_SCREEN_CAPTURE_RECORDER_START_REJECTED',
|
|
109
|
+
reason:'screen-capture-recorder-start-rejected'
|
|
110
|
+
}),
|
|
111
|
+
recorderStopRejected:Object.freeze({
|
|
112
|
+
code:'ARCANE_SCREEN_CAPTURE_RECORDER_STOP_REJECTED',
|
|
113
|
+
reason:'screen-capture-recorder-stop-rejected'
|
|
114
|
+
}),
|
|
115
|
+
recorderUnavailable:Object.freeze({
|
|
116
|
+
code:'ARCANE_SCREEN_CAPTURE_RECORDER_UNAVAILABLE',
|
|
117
|
+
reason:'screen-capture-recorder-unavailable'
|
|
118
|
+
}),
|
|
119
|
+
operationAborted:Object.freeze({
|
|
120
|
+
code:'ARCANE_SCREEN_CAPTURE_OPERATION_ABORTED',
|
|
121
|
+
reason:'screen-capture-operation-aborted'
|
|
122
|
+
}),
|
|
123
|
+
operationStopped:Object.freeze({
|
|
124
|
+
code:'ARCANE_SCREEN_CAPTURE_OPERATION_STOPPED',
|
|
125
|
+
reason:'screen-capture-operation-stopped'
|
|
126
|
+
}),
|
|
127
|
+
reset:Object.freeze({
|
|
128
|
+
code:'ARCANE_SCREEN_CAPTURE_OPERATION_RESET',
|
|
129
|
+
reason:'screen-capture-reset'
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const SCREEN_CAPTURE_ERROR_CODES=Object.freeze(Object.fromEntries(
|
|
134
|
+
Object.entries(SCREEN_CAPTURE_ERRORS).map(function mapScreenCaptureCode(entry){
|
|
135
|
+
return [entry[0],entry[1].code];
|
|
136
|
+
})
|
|
137
|
+
));
|
|
138
|
+
|
|
139
|
+
export const SCREEN_CAPTURE_REASONS=Object.freeze({
|
|
140
|
+
displaySelectionRequested:'screen-capture-display-selection-requested',
|
|
141
|
+
gifCompleted:'screen-capture-gif-completed',
|
|
142
|
+
gifRecordingStarted:'screen-capture-gif-recording-started',
|
|
143
|
+
gifRecordingStopped:'screen-capture-gif-recording-stopped',
|
|
144
|
+
imageCompleted:'screen-capture-image-completed',
|
|
145
|
+
videoCompleted:'screen-capture-video-completed',
|
|
146
|
+
videoRecordingStarted:'screen-capture-video-recording-started',
|
|
147
|
+
videoRecordingStopped:'screen-capture-video-recording-stopped',
|
|
148
|
+
...Object.fromEntries(Object.entries(SCREEN_CAPTURE_ERRORS).map(
|
|
149
|
+
function mapScreenCaptureReason(entry){return [entry[0],entry[1].reason];}
|
|
150
|
+
))
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const EVENT_NAMES=Object.freeze({
|
|
154
|
+
requesting:SCREEN_CAPTURE_EVENT_TYPES.displaySelectionRequested,
|
|
155
|
+
start:SCREEN_CAPTURE_EVENT_TYPES.captureStarted,
|
|
156
|
+
result:SCREEN_CAPTURE_EVENT_TYPES.captureCompleted,
|
|
157
|
+
error:SCREEN_CAPTURE_EVENT_TYPES.captureFailed,
|
|
158
|
+
stop:SCREEN_CAPTURE_EVENT_TYPES.captureStopped
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function signalLike(value){
|
|
162
|
+
return value===undefined
|
|
163
|
+
||value===null
|
|
164
|
+
||(
|
|
165
|
+
typeof value==='object'
|
|
166
|
+
&&typeof value.aborted==='boolean'
|
|
167
|
+
&&typeof value.addEventListener==='function'
|
|
168
|
+
&&typeof value.removeEventListener==='function'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function captureError(contract,message,cause,ErrorType=Error){
|
|
173
|
+
const error=new ErrorType(message);
|
|
174
|
+
error.code=contract.code;
|
|
175
|
+
error.reason=contract.reason;
|
|
176
|
+
if(cause!==undefined)error.cause=cause;
|
|
177
|
+
return error;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function abortError(cause){
|
|
181
|
+
const error=captureError(
|
|
182
|
+
SCREEN_CAPTURE_ERRORS.operationAborted,
|
|
183
|
+
'The screen capture operation was aborted.',
|
|
184
|
+
cause
|
|
185
|
+
);
|
|
186
|
+
error.name='AbortError';
|
|
187
|
+
return error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function optionsRecord(value,label){
|
|
191
|
+
if(value===undefined)return {};
|
|
192
|
+
if(!value||typeof value!=='object'||Array.isArray(value)){
|
|
193
|
+
throw captureError(
|
|
194
|
+
SCREEN_CAPTURE_ERRORS.optionsRecordRejected,
|
|
195
|
+
label+' must be an object.',
|
|
196
|
+
undefined,
|
|
197
|
+
TypeError
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if(!signalLike(value.signal)){
|
|
201
|
+
throw captureError(
|
|
202
|
+
SCREEN_CAPTURE_ERRORS.abortSignalRejected,
|
|
203
|
+
label+' signal must be an AbortSignal.',
|
|
204
|
+
undefined,
|
|
205
|
+
TypeError
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function admittedOperationId(value){
|
|
212
|
+
if(value===undefined||value===null)return null;
|
|
213
|
+
if(typeof value!=='string'
|
|
214
|
+
||value.trim()!==value
|
|
215
|
+
||value.length<1
|
|
216
|
+
||value.length>256){
|
|
217
|
+
throw captureError(
|
|
218
|
+
SCREEN_CAPTURE_ERRORS.operationIdRejected,
|
|
219
|
+
'Screen capture operationId must contain 1-256 non-edge-whitespace characters.',
|
|
220
|
+
undefined,
|
|
221
|
+
TypeError
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizedWidth(value,fallback){
|
|
228
|
+
return Math.max(1,Number(value)||fallback);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function imageResultType(blob){
|
|
232
|
+
const reported=typeof blob?.type==='string'?blob.type.trim().toLowerCase():'';
|
|
233
|
+
if(!reported)return SCREEN_CAPTURE_IMAGE_TYPE_FALLBACK;
|
|
234
|
+
const subtype=reported.startsWith('image/')
|
|
235
|
+
?reported.slice('image/'.length).split(';',1)[0]
|
|
236
|
+
:'';
|
|
237
|
+
const canonicalSubtype=subtype.split('+',1)[0];
|
|
238
|
+
const extension=canonicalSubtype==='jpeg'
|
|
239
|
+
?'jpg'
|
|
240
|
+
:/^[a-z0-9][a-z0-9.-]*$/u.test(canonicalSubtype)
|
|
241
|
+
?canonicalSubtype
|
|
242
|
+
:'bin';
|
|
243
|
+
return Object.freeze({mimeType:reported,extension});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function supportedRecorderType(Recorder){
|
|
247
|
+
for(const type of [
|
|
248
|
+
'video/webm;codecs=vp9,opus',
|
|
249
|
+
'video/webm;codecs=vp8,opus',
|
|
250
|
+
'video/webm',
|
|
251
|
+
'video/mp4'
|
|
252
|
+
]){
|
|
253
|
+
if(Recorder.isTypeSupported?.(type))return type;
|
|
254
|
+
}
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function reportDetachedError(error){
|
|
259
|
+
if(typeof globalThis.reportError==='function')globalThis.reportError(error);
|
|
260
|
+
else globalThis.console?.error?.(error);
|
|
261
|
+
}
|
|
6
262
|
|
|
7
263
|
export default class ScreenCapture extends EventTarget{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
264
|
+
#acquisitions=new Set();
|
|
265
|
+
#disposed=false;
|
|
266
|
+
#disposing=false;
|
|
267
|
+
#events;
|
|
268
|
+
#operation=null;
|
|
269
|
+
#operationSequence=0;
|
|
270
|
+
|
|
271
|
+
constructor({
|
|
272
|
+
mediaDevices=globalThis.navigator?.mediaDevices,
|
|
273
|
+
Recorder=globalThis.MediaRecorder,
|
|
274
|
+
documentRef=globalThis.document
|
|
275
|
+
}={}){
|
|
276
|
+
super();
|
|
277
|
+
this.mediaDevices=mediaDevices;
|
|
278
|
+
this.Recorder=Recorder;
|
|
279
|
+
this.document=documentRef;
|
|
280
|
+
this.mode='idle';
|
|
281
|
+
this.stream=null;
|
|
282
|
+
this.recorder=null;
|
|
283
|
+
this.startedAt=0;
|
|
284
|
+
this.frames=[];
|
|
285
|
+
this.timer=0;
|
|
286
|
+
this.video=null;
|
|
287
|
+
this.canvas=null;
|
|
288
|
+
this.stopPromise=null;
|
|
289
|
+
this.gifDelay=0;
|
|
290
|
+
this.#events=createArcaneEventSource(this,{
|
|
291
|
+
source:'screen-capture',
|
|
292
|
+
eventTypes:Object.freeze(Object.values(SCREEN_CAPTURE_EVENT_TYPES))
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
addEventListener(type,listener,options){return this.#events.addEventListener(type,listener,options);}
|
|
297
|
+
removeEventListener(type,listener,options){return this.#events.removeEventListener(type,listener,options);}
|
|
298
|
+
on(type,listener,options){return this.#events.on(type,listener,options);}
|
|
299
|
+
subscribe(type,listener,options){return this.#events.subscribe(type,listener,options);}
|
|
300
|
+
dispatchEvent(value){return this.#events.dispatchEvent(value);}
|
|
301
|
+
|
|
302
|
+
available(){
|
|
303
|
+
return !this.#disposed
|
|
304
|
+
&&!this.#disposing
|
|
305
|
+
&&Boolean(this.mediaDevices?.getDisplayMedia&&this.document);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#disposedError(){
|
|
309
|
+
return captureError(
|
|
310
|
+
SCREEN_CAPTURE_ERRORS.disposed,
|
|
311
|
+
'The screen capture owner has been disposed.'
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#assertOpen(){
|
|
316
|
+
if(this.#disposed||this.#disposing)throw this.#disposedError();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
assertIdle(){
|
|
320
|
+
this.#assertOpen();
|
|
321
|
+
if(this.#operation){
|
|
322
|
+
throw captureError(
|
|
323
|
+
SCREEN_CAPTURE_ERRORS.active,
|
|
324
|
+
'Stop the active screen capture before starting another one.'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#linkSignal(signal,controller,cleanup){
|
|
331
|
+
if(signal===undefined||signal===null)return;
|
|
332
|
+
function abortLinkedScreenCapture(){
|
|
333
|
+
if(!controller.signal.aborted)controller.abort(signal.reason);
|
|
334
|
+
}
|
|
335
|
+
if(signal.aborted){
|
|
336
|
+
abortLinkedScreenCapture();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
signal.addEventListener('abort',abortLinkedScreenCapture,{once:true});
|
|
340
|
+
cleanup.push(function removeLinkedScreenCaptureAbort(){
|
|
341
|
+
signal.removeEventListener('abort',abortLinkedScreenCapture);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async acquire(optionsValue={}){
|
|
346
|
+
this.#assertOpen();
|
|
347
|
+
const options=optionsRecord(optionsValue,'Screen capture acquisition options');
|
|
348
|
+
if(!this.available()){
|
|
349
|
+
throw captureError(
|
|
350
|
+
SCREEN_CAPTURE_ERRORS.displayUnavailable,
|
|
351
|
+
'Display capture is unavailable in this browser.'
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if(options.signal?.aborted)throw abortError(options.signal.reason);
|
|
355
|
+
const controller=new AbortController();
|
|
356
|
+
const cleanup=[];
|
|
357
|
+
this.#linkSignal(options.signal??null,controller,cleanup);
|
|
358
|
+
const acquisition={controller,cleanup,returned:false};
|
|
359
|
+
this.#acquisitions.add(acquisition);
|
|
360
|
+
let pending;
|
|
361
|
+
try{
|
|
362
|
+
pending=Promise.resolve(this.mediaDevices.getDisplayMedia({
|
|
363
|
+
video:{frameRate:{ideal:15,max:30}},
|
|
364
|
+
audio:Boolean(options.audio)
|
|
365
|
+
}));
|
|
366
|
+
}catch(cause){
|
|
367
|
+
this.#acquisitions.delete(acquisition);
|
|
368
|
+
for(const remove of cleanup.splice(0))remove();
|
|
369
|
+
throw captureError(
|
|
370
|
+
SCREEN_CAPTURE_ERRORS.displaySelectionRejected,
|
|
371
|
+
'The display-capture request was rejected before display selection began.',
|
|
372
|
+
cause
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const owner=this;
|
|
376
|
+
return new Promise(function waitForDisplaySelection(resolve,reject){
|
|
377
|
+
function finish(){
|
|
378
|
+
owner.#acquisitions.delete(acquisition);
|
|
379
|
+
for(const remove of cleanup.splice(0))remove();
|
|
380
|
+
controller.signal.removeEventListener('abort',rejectAbortedAcquisition);
|
|
381
|
+
}
|
|
382
|
+
function rejectAbortedAcquisition(){
|
|
383
|
+
if(acquisition.returned)return;
|
|
384
|
+
acquisition.returned=true;
|
|
385
|
+
reject(owner.#disposed||owner.#disposing
|
|
386
|
+
?owner.#disposedError()
|
|
387
|
+
:abortError(controller.signal.reason));
|
|
388
|
+
}
|
|
389
|
+
controller.signal.addEventListener('abort',rejectAbortedAcquisition,{once:true});
|
|
390
|
+
if(controller.signal.aborted)rejectAbortedAcquisition();
|
|
391
|
+
pending.then(
|
|
392
|
+
function resolveDisplayStream(stream){
|
|
393
|
+
const stale=acquisition.returned
|
|
394
|
+
||controller.signal.aborted
|
|
395
|
+
||owner.#disposed
|
|
396
|
+
||owner.#disposing;
|
|
397
|
+
if(stale){
|
|
398
|
+
try{owner.stopTracks(stream);}catch(error){reportDetachedError(error);}
|
|
399
|
+
}else{
|
|
400
|
+
acquisition.returned=true;
|
|
401
|
+
resolve(stream);
|
|
402
|
+
}
|
|
403
|
+
finish();
|
|
404
|
+
},
|
|
405
|
+
function rejectDisplaySelection(cause){
|
|
406
|
+
if(!acquisition.returned){
|
|
407
|
+
acquisition.returned=true;
|
|
408
|
+
reject(captureError(
|
|
409
|
+
SCREEN_CAPTURE_ERRORS.displaySelectionRejected,
|
|
410
|
+
'The display-capture request was rejected.',
|
|
411
|
+
cause
|
|
412
|
+
));
|
|
413
|
+
}
|
|
414
|
+
finish();
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#nextOperationId(mode,requested){
|
|
421
|
+
if(requested)return requested;
|
|
422
|
+
this.#operationSequence+=1;
|
|
423
|
+
return this.#events.instanceId+':'+mode+':'+this.#operationSequence.toString(36);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#publicDetail(detail,{code=null,reason,status}){
|
|
427
|
+
const blob=detail?.blob;
|
|
428
|
+
return Object.freeze({
|
|
429
|
+
...(typeof detail?.mode==='string'?{mode:detail.mode}:{}),
|
|
430
|
+
...(typeof detail?.mimeType==='string'?{mimeType:detail.mimeType}:{}),
|
|
431
|
+
...(typeof detail?.extension==='string'?{extension:detail.extension}:{}),
|
|
432
|
+
...(Number.isFinite(detail?.duration)?{duration:Math.max(0,detail.duration)}:{}),
|
|
433
|
+
...(Number.isSafeInteger(detail?.width)?{width:detail.width}:{}),
|
|
434
|
+
...(Number.isSafeInteger(detail?.height)?{height:detail.height}:{}),
|
|
435
|
+
...(Number.isSafeInteger(blob?.size)?{byteCount:blob.size}:{}),
|
|
436
|
+
...(code?{code}:{}),
|
|
437
|
+
reason,
|
|
438
|
+
status
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#publish(operation,type,detail,{code=null,reason,status,cancelable=false}={}){
|
|
443
|
+
if(this.#events.disposed)return null;
|
|
444
|
+
const compatibilityDetail=Object.freeze({
|
|
445
|
+
...detail,
|
|
446
|
+
mode:typeof detail?.mode==='string'?detail.mode:operation.mode,
|
|
447
|
+
operationId:operation.id,
|
|
448
|
+
reason,
|
|
449
|
+
...(code?{code}:{})
|
|
450
|
+
});
|
|
451
|
+
return this.#events.dispatch(
|
|
452
|
+
EVENT_NAMES[type],
|
|
453
|
+
compatibilityDetail,
|
|
454
|
+
{
|
|
455
|
+
operationId:operation.id,
|
|
456
|
+
publicDetail:this.#publicDetail(compatibilityDetail,{code,reason,status}),
|
|
457
|
+
cancelable
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#beginOperation(mode,{signal=null,operationId=null}={}){
|
|
463
|
+
this.assertIdle();
|
|
464
|
+
if(signal?.aborted)throw abortError(signal.reason);
|
|
465
|
+
const controller=new AbortController();
|
|
466
|
+
const operation={
|
|
467
|
+
abortPublishesError:true,
|
|
468
|
+
cleanup:[],
|
|
469
|
+
completed:false,
|
|
470
|
+
controller,
|
|
471
|
+
errorPublished:false,
|
|
472
|
+
id:this.#nextOperationId(mode,operationId),
|
|
473
|
+
mode,
|
|
474
|
+
phase:'requesting',
|
|
475
|
+
recorderCleanup:[],
|
|
476
|
+
recorderOutcome:null,
|
|
477
|
+
resolveRecorderOutcome:null,
|
|
478
|
+
resultPublished:false,
|
|
479
|
+
stopCallPromise:null,
|
|
480
|
+
stopPublished:false,
|
|
481
|
+
stopReason:null,
|
|
482
|
+
terminalError:null
|
|
483
|
+
};
|
|
484
|
+
this.#operation=operation;
|
|
485
|
+
this.mode=mode;
|
|
486
|
+
const owner=this;
|
|
487
|
+
function abortActiveScreenCapture(){owner.#settleAbortedOperation(operation);}
|
|
488
|
+
controller.signal.addEventListener('abort',abortActiveScreenCapture,{once:true});
|
|
489
|
+
operation.cleanup.push(function removeActiveScreenCaptureAbort(){
|
|
490
|
+
controller.signal.removeEventListener('abort',abortActiveScreenCapture);
|
|
491
|
+
});
|
|
492
|
+
this.#linkSignal(signal,controller,operation.cleanup);
|
|
493
|
+
const publication=this.#publish(
|
|
494
|
+
operation,
|
|
495
|
+
'requesting',
|
|
496
|
+
Object.freeze({mode}),
|
|
497
|
+
{
|
|
498
|
+
reason:SCREEN_CAPTURE_REASONS.displaySelectionRequested,
|
|
499
|
+
status:SCREEN_CAPTURE_STATUSES.selectingDisplay,
|
|
500
|
+
cancelable:true
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
if(publication&&!publication.accepted){
|
|
504
|
+
const error=captureError(
|
|
505
|
+
SCREEN_CAPTURE_ERRORS.displaySelectionCancelled,
|
|
506
|
+
'Display selection was cancelled by a screen capture listener.'
|
|
507
|
+
);
|
|
508
|
+
this.#requestOperationAbort(operation,error,{
|
|
509
|
+
publishError:true,
|
|
510
|
+
stopReason:error.reason
|
|
511
|
+
});
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
return operation;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#isCurrent(operation){
|
|
518
|
+
return this.#operation===operation&&!operation.completed;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#assertCurrent(operation){
|
|
522
|
+
if(this.#isCurrent(operation)&&!operation.controller.signal.aborted)return;
|
|
523
|
+
throw operation.terminalError
|
|
524
|
+
??(this.#disposed||this.#disposing?this.#disposedError():abortError());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#setStream(operation,stream){
|
|
528
|
+
this.#assertCurrent(operation);
|
|
529
|
+
operation.stream=stream;
|
|
530
|
+
this.stream=stream;
|
|
531
|
+
this.#attachTrackEnd(operation,stream);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#attachTrackEnd(operation,stream){
|
|
535
|
+
for(const track of stream?.getTracks?.()||[]){
|
|
536
|
+
if(typeof track?.addEventListener!=='function')continue;
|
|
537
|
+
const owner=this;
|
|
538
|
+
function stopEndedScreenCaptureTrack(){
|
|
539
|
+
if(!owner.#isCurrent(operation))return;
|
|
540
|
+
if(operation.phase==='recording'){
|
|
541
|
+
owner.stop().catch(reportDetachedError);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if(operation.phase==='stopping'
|
|
545
|
+
||operation.phase==='completing'
|
|
546
|
+
||operation.phase==='aborting'
|
|
547
|
+
||operation.phase==='failing')return;
|
|
548
|
+
const error=captureError(
|
|
549
|
+
SCREEN_CAPTURE_ERRORS.displayTrackEndedBeforeRecording,
|
|
550
|
+
'The selected display track ended before capture recording began.'
|
|
551
|
+
);
|
|
552
|
+
owner.#requestOperationAbort(operation,error,{
|
|
553
|
+
publishError:true,
|
|
554
|
+
stopReason:error.reason
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
track.addEventListener('ended',stopEndedScreenCaptureTrack,{once:true});
|
|
558
|
+
operation.cleanup.push(function removeScreenCaptureTrackEnd(){
|
|
559
|
+
track.removeEventListener?.('ended',stopEndedScreenCaptureTrack);
|
|
560
|
+
});
|
|
561
|
+
if(track.readyState==='ended'){
|
|
562
|
+
stopEndedScreenCaptureTrack();
|
|
563
|
+
if(!this.#isCurrent(operation))break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
#setPreparedSurface(operation,{video,canvas}){
|
|
569
|
+
this.#assertCurrent(operation);
|
|
570
|
+
operation.video=video;
|
|
571
|
+
operation.canvas=canvas;
|
|
572
|
+
this.video=video;
|
|
573
|
+
this.canvas=canvas;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#removeRecorderListeners(operation){
|
|
577
|
+
for(const remove of operation.recorderCleanup.splice(0))remove();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#settleRecorder(operation,outcome){
|
|
581
|
+
if(!operation.resolveRecorderOutcome)return false;
|
|
582
|
+
const resolve=operation.resolveRecorderOutcome;
|
|
583
|
+
operation.resolveRecorderOutcome=null;
|
|
584
|
+
operation.recorderOutcome=outcome;
|
|
585
|
+
resolve(outcome);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
#cleanupResources(operation,{settleRecorderError=null}={}){
|
|
590
|
+
let firstError=null;
|
|
591
|
+
function remember(error){firstError??=error;}
|
|
592
|
+
clearInterval(operation.timer??0);
|
|
593
|
+
clearInterval(this.timer);
|
|
594
|
+
operation.timer=0;
|
|
595
|
+
this.timer=0;
|
|
596
|
+
if(settleRecorderError)this.#settleRecorder(operation,{error:settleRecorderError});
|
|
597
|
+
const recorder=operation.recorder??this.recorder;
|
|
598
|
+
if(recorder&&recorder.state!=='inactive'){
|
|
599
|
+
try{recorder.stop();}catch(error){remember(error);}
|
|
600
|
+
}
|
|
601
|
+
this.#removeRecorderListeners(operation);
|
|
602
|
+
try{this.stopTracks(operation.stream??this.stream);}catch(error){remember(error);}
|
|
603
|
+
const video=operation.video??this.video;
|
|
604
|
+
if(video){
|
|
605
|
+
try{video.pause?.();}catch(error){remember(error);}
|
|
606
|
+
try{video.srcObject=null;}catch(error){remember(error);}
|
|
607
|
+
}
|
|
608
|
+
return firstError;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
#releaseOperation(operation){
|
|
612
|
+
if(operation.completed)return false;
|
|
613
|
+
operation.completed=true;
|
|
614
|
+
for(const remove of operation.cleanup.splice(0))remove();
|
|
615
|
+
this.#removeRecorderListeners(operation);
|
|
616
|
+
if(this.#operation===operation)this.#operation=null;
|
|
617
|
+
this.mode='idle';
|
|
618
|
+
this.stream=null;
|
|
619
|
+
this.recorder=null;
|
|
620
|
+
this.startedAt=0;
|
|
621
|
+
this.frames=[];
|
|
622
|
+
this.timer=0;
|
|
623
|
+
this.video=null;
|
|
624
|
+
this.canvas=null;
|
|
625
|
+
this.stopPromise=null;
|
|
626
|
+
this.gifDelay=0;
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#publishFailure(operation,error){
|
|
631
|
+
if(operation.errorPublished||this.#events.disposed)return false;
|
|
632
|
+
operation.errorPublished=true;
|
|
633
|
+
this.#publish(
|
|
634
|
+
operation,
|
|
635
|
+
'error',
|
|
636
|
+
Object.freeze({error,mode:operation.mode}),
|
|
637
|
+
{
|
|
638
|
+
code:error.code,
|
|
639
|
+
reason:error.reason,
|
|
640
|
+
status:SCREEN_CAPTURE_STATUSES.captureRejected
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#publishStop(operation,reason,code=null){
|
|
647
|
+
if(operation.stopPublished||this.#events.disposed)return false;
|
|
648
|
+
operation.stopPublished=true;
|
|
649
|
+
this.#publish(
|
|
650
|
+
operation,
|
|
651
|
+
'stop',
|
|
652
|
+
Object.freeze({mode:operation.mode}),
|
|
653
|
+
{code,reason,status:SCREEN_CAPTURE_STATUSES.captureStopped}
|
|
654
|
+
);
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#requestOperationAbort(operation,error,{
|
|
659
|
+
publishError=true,
|
|
660
|
+
stopReason=error.reason
|
|
661
|
+
}={}){
|
|
662
|
+
if(!this.#isCurrent(operation))return false;
|
|
663
|
+
operation.terminalError=error;
|
|
664
|
+
operation.abortPublishesError=publishError;
|
|
665
|
+
operation.stopReason=stopReason;
|
|
666
|
+
if(!operation.controller.signal.aborted)operation.controller.abort(error);
|
|
667
|
+
else this.#settleAbortedOperation(operation);
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
#settleAbortedOperation(operation){
|
|
672
|
+
if(!this.#isCurrent(operation))return false;
|
|
673
|
+
const error=operation.terminalError
|
|
674
|
+
??(this.#disposed||this.#disposing
|
|
675
|
+
?this.#disposedError()
|
|
676
|
+
:abortError(operation.controller.signal.reason));
|
|
677
|
+
operation.terminalError=error;
|
|
678
|
+
operation.phase='aborting';
|
|
679
|
+
const cleanupError=this.#cleanupResources(operation,{settleRecorderError:error});
|
|
680
|
+
if(cleanupError&&!Object.hasOwn(error,'cleanupCause'))error.cleanupCause=cleanupError;
|
|
681
|
+
if(operation.abortPublishesError)this.#publishFailure(operation,error);
|
|
682
|
+
this.#publishStop(operation,operation.stopReason??error.reason,error.code);
|
|
683
|
+
this.#releaseOperation(operation);
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
#failOperation(operation,error){
|
|
688
|
+
if(!this.#isCurrent(operation))return false;
|
|
689
|
+
operation.terminalError=error;
|
|
690
|
+
operation.phase='failing';
|
|
691
|
+
const cleanupError=this.#cleanupResources(operation,{settleRecorderError:error});
|
|
692
|
+
if(cleanupError&&!Object.hasOwn(error,'cleanupCause'))error.cleanupCause=cleanupError;
|
|
693
|
+
this.#publishFailure(operation,error);
|
|
694
|
+
this.#publishStop(operation,error.reason,error.code);
|
|
695
|
+
this.#releaseOperation(operation);
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#operationError(cause,operation,stage){
|
|
700
|
+
if(operation.terminalError)return operation.terminalError;
|
|
701
|
+
if(cause?.code&&cause?.reason)return cause;
|
|
702
|
+
if(operation.controller.signal.aborted)return abortError(
|
|
703
|
+
operation.controller.signal.reason??cause
|
|
704
|
+
);
|
|
705
|
+
const contracts={
|
|
706
|
+
'display-selection':[
|
|
707
|
+
SCREEN_CAPTURE_ERRORS.displaySelectionRejected,
|
|
708
|
+
'The display-capture request was rejected.'
|
|
709
|
+
],
|
|
710
|
+
'display-preparation':[
|
|
711
|
+
SCREEN_CAPTURE_ERRORS.displayMetadataRejected,
|
|
712
|
+
'The selected display could not be prepared for capture.'
|
|
713
|
+
],
|
|
714
|
+
'image-draw':[
|
|
715
|
+
SCREEN_CAPTURE_ERRORS.imageDrawRejected,
|
|
716
|
+
'The selected display frame could not be drawn to the capture canvas.'
|
|
717
|
+
],
|
|
718
|
+
'image-encoding':[
|
|
719
|
+
SCREEN_CAPTURE_ERRORS.imageEncodingRejected,
|
|
720
|
+
'The captured display image could not be encoded.'
|
|
721
|
+
],
|
|
722
|
+
'recorder-construction':[
|
|
723
|
+
SCREEN_CAPTURE_ERRORS.recorderConstructionRejected,
|
|
724
|
+
'The display recording could not create a MediaRecorder.'
|
|
725
|
+
],
|
|
726
|
+
'recorder-start':[
|
|
727
|
+
SCREEN_CAPTURE_ERRORS.recorderStartRejected,
|
|
728
|
+
'The display recording could not start the MediaRecorder.'
|
|
729
|
+
],
|
|
730
|
+
'recorder-stop':[
|
|
731
|
+
SCREEN_CAPTURE_ERRORS.recorderStopRejected,
|
|
732
|
+
'The display recording could not stop the MediaRecorder.'
|
|
733
|
+
],
|
|
734
|
+
'gif-frame-read':[
|
|
735
|
+
SCREEN_CAPTURE_ERRORS.gifFrameReadRejected,
|
|
736
|
+
'A display frame could not be read for GIF capture.'
|
|
737
|
+
],
|
|
738
|
+
'gif-encoding':[
|
|
739
|
+
SCREEN_CAPTURE_ERRORS.gifEncodingRejected,
|
|
740
|
+
'The captured GIF frames could not be encoded.'
|
|
741
|
+
],
|
|
742
|
+
cleanup:[
|
|
743
|
+
SCREEN_CAPTURE_ERRORS.cleanupRejected,
|
|
744
|
+
'The screen capture resources could not be released.'
|
|
745
|
+
]
|
|
746
|
+
};
|
|
747
|
+
const selected=contracts[stage]??contracts.cleanup;
|
|
748
|
+
return captureError(selected[0],selected[1],cause);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async captureImage(optionsValue={}){
|
|
752
|
+
const options=optionsRecord(optionsValue,'Image capture options');
|
|
753
|
+
const operation=this.#beginOperation('image',{
|
|
754
|
+
signal:options.signal??null,
|
|
755
|
+
operationId:admittedOperationId(options.operationId)
|
|
756
|
+
});
|
|
757
|
+
let stage='display-selection';
|
|
758
|
+
try{
|
|
759
|
+
const stream=await this.acquire({audio:false,signal:operation.controller.signal});
|
|
760
|
+
this.#setStream(operation,stream);
|
|
761
|
+
stage='display-preparation';
|
|
762
|
+
const prepared=await this.prepare(
|
|
763
|
+
stream,
|
|
764
|
+
normalizedWidth(options.maxWidth,1920),
|
|
765
|
+
{signal:operation.controller.signal}
|
|
766
|
+
);
|
|
767
|
+
this.#setPreparedSurface(operation,prepared);
|
|
768
|
+
const context=prepared.canvas.getContext?.('2d');
|
|
769
|
+
if(!context){
|
|
770
|
+
throw captureError(
|
|
771
|
+
SCREEN_CAPTURE_ERRORS.imageCanvasUnavailable,
|
|
772
|
+
'The image capture canvas does not expose a 2D context.'
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
stage='image-draw';
|
|
776
|
+
context.drawImage(
|
|
777
|
+
prepared.video,
|
|
778
|
+
0,
|
|
779
|
+
0,
|
|
780
|
+
prepared.canvas.width,
|
|
781
|
+
prepared.canvas.height
|
|
782
|
+
);
|
|
783
|
+
this.#assertCurrent(operation);
|
|
784
|
+
stage='image-encoding';
|
|
785
|
+
const type=typeof options.type==='string'&&options.type.trim()
|
|
786
|
+
?options.type.trim()
|
|
787
|
+
:'image/png';
|
|
788
|
+
const blob=await this.#canvasBlob(
|
|
789
|
+
prepared.canvas,
|
|
790
|
+
type,
|
|
791
|
+
options.quality,
|
|
792
|
+
operation.controller.signal
|
|
793
|
+
);
|
|
794
|
+
this.#assertCurrent(operation);
|
|
795
|
+
const encodedType=imageResultType(blob);
|
|
796
|
+
const result={
|
|
797
|
+
blob,
|
|
798
|
+
mimeType:encodedType.mimeType,
|
|
799
|
+
extension:encodedType.extension,
|
|
800
|
+
duration:0,
|
|
801
|
+
width:prepared.canvas.width,
|
|
802
|
+
height:prepared.canvas.height
|
|
803
|
+
};
|
|
804
|
+
operation.phase='completing';
|
|
805
|
+
const cleanupError=this.#cleanupResources(operation);
|
|
806
|
+
if(cleanupError)throw this.#operationError(cleanupError,operation,'cleanup');
|
|
807
|
+
this.#publish(
|
|
808
|
+
operation,
|
|
809
|
+
'result',
|
|
810
|
+
result,
|
|
811
|
+
{
|
|
812
|
+
reason:SCREEN_CAPTURE_REASONS.imageCompleted,
|
|
813
|
+
status:SCREEN_CAPTURE_STATUSES.captureReady
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
operation.resultPublished=true;
|
|
817
|
+
this.#releaseOperation(operation);
|
|
818
|
+
return result;
|
|
819
|
+
}catch(cause){
|
|
820
|
+
const error=this.#operationError(cause,operation,stage);
|
|
821
|
+
if(this.#isCurrent(operation))this.#failOperation(operation,error);
|
|
822
|
+
throw operation.terminalError??error;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#configureRecorder(operation,recorder,mimeType){
|
|
827
|
+
operation.recorder=recorder;
|
|
828
|
+
this.recorder=recorder;
|
|
829
|
+
const chunks=[];
|
|
830
|
+
operation.stopPromise=new Promise(function createRecorderOutcome(resolve){
|
|
831
|
+
operation.resolveRecorderOutcome=resolve;
|
|
832
|
+
});
|
|
833
|
+
this.stopPromise=operation.stopPromise;
|
|
834
|
+
const owner=this;
|
|
835
|
+
function receiveRecordedData(event){
|
|
836
|
+
if(event.data?.size)chunks.push(event.data);
|
|
837
|
+
}
|
|
838
|
+
function receiveRecorderStop(){
|
|
839
|
+
let outcome;
|
|
840
|
+
try{
|
|
841
|
+
const type=recorder.mimeType||mimeType||'video/webm';
|
|
842
|
+
outcome={
|
|
843
|
+
result:{
|
|
844
|
+
blob:new Blob(chunks,{type}),
|
|
845
|
+
mimeType:type,
|
|
846
|
+
extension:type.includes('mp4')?'mp4':'webm',
|
|
847
|
+
duration:Math.max(0,Date.now()-operation.startedAt)
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}catch(cause){
|
|
851
|
+
outcome={
|
|
852
|
+
error:captureError(
|
|
853
|
+
SCREEN_CAPTURE_ERRORS.recorderStopRejected,
|
|
854
|
+
'The display recording could not assemble the recorded media.',
|
|
855
|
+
cause
|
|
856
|
+
)
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
owner.#settleRecorder(operation,outcome);
|
|
860
|
+
if(operation.phase==='recording'&&owner.#isCurrent(operation)){
|
|
861
|
+
if(outcome.error)owner.#failOperation(operation,outcome.error);
|
|
862
|
+
else owner.#completeDetachedVideo(operation,outcome.result);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function receiveRecorderError(event){
|
|
866
|
+
const error=captureError(
|
|
867
|
+
SCREEN_CAPTURE_ERRORS.recorderErrorReceived,
|
|
868
|
+
'The MediaRecorder reported a display-recording error.',
|
|
869
|
+
event?.error??event
|
|
870
|
+
);
|
|
871
|
+
owner.#settleRecorder(operation,{error});
|
|
872
|
+
if(operation.phase!=='stopping'&&owner.#isCurrent(operation)){
|
|
873
|
+
owner.#failOperation(operation,error);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
recorder.addEventListener('dataavailable',receiveRecordedData);
|
|
877
|
+
recorder.addEventListener('stop',receiveRecorderStop,{once:true});
|
|
878
|
+
recorder.addEventListener('error',receiveRecorderError,{once:true});
|
|
879
|
+
operation.recorderCleanup.push(
|
|
880
|
+
function removeRecordedDataListener(){
|
|
881
|
+
recorder.removeEventListener?.('dataavailable',receiveRecordedData);
|
|
882
|
+
},
|
|
883
|
+
function removeRecorderStopListener(){
|
|
884
|
+
recorder.removeEventListener?.('stop',receiveRecorderStop);
|
|
885
|
+
},
|
|
886
|
+
function removeRecorderErrorListener(){
|
|
887
|
+
recorder.removeEventListener?.('error',receiveRecorderError);
|
|
888
|
+
}
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
#completeDetachedVideo(operation,result){
|
|
893
|
+
if(!this.#isCurrent(operation))return false;
|
|
894
|
+
operation.phase='completing';
|
|
895
|
+
const cleanupError=this.#cleanupResources(operation);
|
|
896
|
+
if(cleanupError){
|
|
897
|
+
this.#failOperation(
|
|
898
|
+
operation,
|
|
899
|
+
this.#operationError(cleanupError,operation,'cleanup')
|
|
900
|
+
);
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
this.#publish(
|
|
904
|
+
operation,
|
|
905
|
+
'result',
|
|
906
|
+
result,
|
|
907
|
+
{
|
|
908
|
+
reason:SCREEN_CAPTURE_REASONS.videoCompleted,
|
|
909
|
+
status:SCREEN_CAPTURE_STATUSES.captureReady
|
|
910
|
+
}
|
|
911
|
+
);
|
|
912
|
+
operation.resultPublished=true;
|
|
913
|
+
this.#publishStop(operation,SCREEN_CAPTURE_REASONS.videoRecordingStopped);
|
|
914
|
+
this.#releaseOperation(operation);
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async startVideo(optionsValue={}){
|
|
919
|
+
const options=optionsRecord(optionsValue,'Video capture options');
|
|
920
|
+
this.#assertOpen();
|
|
921
|
+
if(!this.Recorder){
|
|
922
|
+
throw captureError(
|
|
923
|
+
SCREEN_CAPTURE_ERRORS.recorderUnavailable,
|
|
924
|
+
'Video recording is unavailable in this browser.'
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const operation=this.#beginOperation('video',{
|
|
928
|
+
signal:options.signal??null,
|
|
929
|
+
operationId:admittedOperationId(options.operationId)
|
|
930
|
+
});
|
|
931
|
+
let stage='display-selection';
|
|
932
|
+
try{
|
|
933
|
+
const stream=await this.acquire({
|
|
934
|
+
audio:options.audio===undefined?true:Boolean(options.audio),
|
|
935
|
+
signal:operation.controller.signal
|
|
936
|
+
});
|
|
937
|
+
this.#setStream(operation,stream);
|
|
938
|
+
const mimeType=supportedRecorderType(this.Recorder);
|
|
939
|
+
stage='recorder-construction';
|
|
940
|
+
const recorder=new this.Recorder(stream,mimeType?{mimeType}:undefined);
|
|
941
|
+
this.#configureRecorder(operation,recorder,mimeType);
|
|
942
|
+
operation.startedAt=Date.now();
|
|
943
|
+
this.startedAt=operation.startedAt;
|
|
944
|
+
operation.phase='recording';
|
|
945
|
+
stage='recorder-start';
|
|
946
|
+
recorder.start(500);
|
|
947
|
+
if(operation.completed){
|
|
948
|
+
if(operation.terminalError)throw operation.terminalError;
|
|
949
|
+
if(operation.resultPublished)return true;
|
|
950
|
+
throw captureError(
|
|
951
|
+
SCREEN_CAPTURE_ERRORS.recorderStopRejected,
|
|
952
|
+
'The MediaRecorder completed during start without a recorded media result.'
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
this.#assertCurrent(operation);
|
|
956
|
+
this.#publish(
|
|
957
|
+
operation,
|
|
958
|
+
'start',
|
|
959
|
+
Object.freeze({mode:'video'}),
|
|
960
|
+
{
|
|
961
|
+
reason:SCREEN_CAPTURE_REASONS.videoRecordingStarted,
|
|
962
|
+
status:SCREEN_CAPTURE_STATUSES.recording
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
return true;
|
|
966
|
+
}catch(cause){
|
|
967
|
+
const error=this.#operationError(cause,operation,stage);
|
|
968
|
+
if(this.#isCurrent(operation))this.#failOperation(operation,error);
|
|
969
|
+
throw operation.terminalError??error;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
#sampleGifFrame(operation,context){
|
|
974
|
+
context.drawImage(
|
|
975
|
+
operation.video,
|
|
976
|
+
0,
|
|
977
|
+
0,
|
|
978
|
+
operation.canvas.width,
|
|
979
|
+
operation.canvas.height
|
|
980
|
+
);
|
|
981
|
+
operation.frames.push(context.getImageData(
|
|
982
|
+
0,
|
|
983
|
+
0,
|
|
984
|
+
operation.canvas.width,
|
|
985
|
+
operation.canvas.height
|
|
986
|
+
));
|
|
987
|
+
this.frames=operation.frames;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async startGif(optionsValue={}){
|
|
991
|
+
const options=optionsRecord(optionsValue,'GIF capture options');
|
|
992
|
+
const operation=this.#beginOperation('gif',{
|
|
993
|
+
signal:options.signal??null,
|
|
994
|
+
operationId:admittedOperationId(options.operationId)
|
|
995
|
+
});
|
|
996
|
+
let stage='display-selection';
|
|
997
|
+
try{
|
|
998
|
+
const stream=await this.acquire({audio:false,signal:operation.controller.signal});
|
|
999
|
+
this.#setStream(operation,stream);
|
|
1000
|
+
stage='display-preparation';
|
|
1001
|
+
const prepared=await this.prepare(
|
|
1002
|
+
stream,
|
|
1003
|
+
normalizedWidth(options.maxWidth,640),
|
|
1004
|
+
{signal:operation.controller.signal}
|
|
1005
|
+
);
|
|
1006
|
+
this.#setPreparedSurface(operation,prepared);
|
|
1007
|
+
const context=prepared.canvas.getContext?.('2d',{willReadFrequently:true});
|
|
1008
|
+
if(!context){
|
|
1009
|
+
throw captureError(
|
|
1010
|
+
SCREEN_CAPTURE_ERRORS.imageCanvasUnavailable,
|
|
1011
|
+
'The GIF capture canvas does not expose a 2D context.'
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
operation.frames=[];
|
|
1015
|
+
operation.gifDelay=Math.max(100,Number(options.frameDelay)||250);
|
|
1016
|
+
operation.startedAt=Date.now();
|
|
1017
|
+
this.frames=operation.frames;
|
|
1018
|
+
this.gifDelay=operation.gifDelay;
|
|
1019
|
+
this.startedAt=operation.startedAt;
|
|
1020
|
+
stage='gif-frame-read';
|
|
1021
|
+
this.#sampleGifFrame(operation,context);
|
|
1022
|
+
operation.phase='recording';
|
|
1023
|
+
const owner=this;
|
|
1024
|
+
function sampleScreenCaptureGifFrame(){
|
|
1025
|
+
if(!owner.#isCurrent(operation)||operation.phase!=='recording')return;
|
|
1026
|
+
try{owner.#sampleGifFrame(operation,context);}
|
|
1027
|
+
catch(cause){
|
|
1028
|
+
owner.#failOperation(
|
|
1029
|
+
operation,
|
|
1030
|
+
owner.#operationError(cause,operation,'gif-frame-read')
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
operation.timer=setInterval(sampleScreenCaptureGifFrame,operation.gifDelay);
|
|
1035
|
+
this.timer=operation.timer;
|
|
1036
|
+
this.#publish(
|
|
1037
|
+
operation,
|
|
1038
|
+
'start',
|
|
1039
|
+
Object.freeze({mode:'gif'}),
|
|
1040
|
+
{
|
|
1041
|
+
reason:SCREEN_CAPTURE_REASONS.gifRecordingStarted,
|
|
1042
|
+
status:SCREEN_CAPTURE_STATUSES.recording
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
return true;
|
|
1046
|
+
}catch(cause){
|
|
1047
|
+
const error=this.#operationError(cause,operation,stage);
|
|
1048
|
+
if(this.#isCurrent(operation))this.#failOperation(operation,error);
|
|
1049
|
+
throw operation.terminalError??error;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async #stopRecording(operation,options){
|
|
1054
|
+
if(options.signal?.aborted){
|
|
1055
|
+
const error=abortError(options.signal.reason);
|
|
1056
|
+
this.#requestOperationAbort(operation,error,{
|
|
1057
|
+
publishError:true,
|
|
1058
|
+
stopReason:error.reason
|
|
1059
|
+
});
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
this.#linkSignal(options.signal??null,operation.controller,operation.cleanup);
|
|
1063
|
+
if(operation.phase==='requesting'||operation.mode==='image'){
|
|
1064
|
+
const error=captureError(
|
|
1065
|
+
SCREEN_CAPTURE_ERRORS.operationStopped,
|
|
1066
|
+
'The screen capture operation was stopped before completion.'
|
|
1067
|
+
);
|
|
1068
|
+
this.#requestOperationAbort(operation,error,{
|
|
1069
|
+
publishError:false,
|
|
1070
|
+
stopReason:error.reason
|
|
1071
|
+
});
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
operation.phase='stopping';
|
|
1075
|
+
let stage=operation.mode==='video'?'recorder-stop':'gif-encoding';
|
|
1076
|
+
try{
|
|
1077
|
+
let result;
|
|
1078
|
+
if(operation.mode==='video'){
|
|
1079
|
+
const recorder=operation.recorder;
|
|
1080
|
+
if(!recorder){
|
|
1081
|
+
throw captureError(
|
|
1082
|
+
SCREEN_CAPTURE_ERRORS.recorderStopRejected,
|
|
1083
|
+
'The active display recording has no MediaRecorder.'
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if(recorder.state!=='inactive')recorder.stop();
|
|
1087
|
+
const outcome=operation.recorderOutcome??await operation.stopPromise;
|
|
1088
|
+
this.#assertCurrent(operation);
|
|
1089
|
+
if(outcome?.error)throw outcome.error;
|
|
1090
|
+
result=outcome?.result;
|
|
1091
|
+
if(!result){
|
|
1092
|
+
throw captureError(
|
|
1093
|
+
SCREEN_CAPTURE_ERRORS.recorderStopRejected,
|
|
1094
|
+
'The MediaRecorder stopped without a recorded media result.'
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
}else{
|
|
1098
|
+
clearInterval(operation.timer);
|
|
1099
|
+
operation.timer=0;
|
|
1100
|
+
this.timer=0;
|
|
1101
|
+
const encoder=new GifEncoder(operation.canvas.width,operation.canvas.height);
|
|
1102
|
+
for(const frame of operation.frames){
|
|
1103
|
+
encoder.addFrame(frame,{delay:operation.gifDelay});
|
|
1104
|
+
}
|
|
1105
|
+
result={
|
|
1106
|
+
blob:encoder.encode(),
|
|
1107
|
+
mimeType:'image/gif',
|
|
1108
|
+
extension:'gif',
|
|
1109
|
+
duration:Math.max(0,Date.now()-operation.startedAt),
|
|
1110
|
+
width:operation.canvas.width,
|
|
1111
|
+
height:operation.canvas.height
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
this.#assertCurrent(operation);
|
|
1115
|
+
stage='cleanup';
|
|
1116
|
+
const cleanupError=this.#cleanupResources(operation);
|
|
1117
|
+
if(cleanupError)throw cleanupError;
|
|
1118
|
+
const resultReason=operation.mode==='video'
|
|
1119
|
+
?SCREEN_CAPTURE_REASONS.videoCompleted
|
|
1120
|
+
:SCREEN_CAPTURE_REASONS.gifCompleted;
|
|
1121
|
+
this.#publish(
|
|
1122
|
+
operation,
|
|
1123
|
+
'result',
|
|
1124
|
+
result,
|
|
1125
|
+
{reason:resultReason,status:SCREEN_CAPTURE_STATUSES.captureReady}
|
|
1126
|
+
);
|
|
1127
|
+
operation.resultPublished=true;
|
|
1128
|
+
this.#publishStop(
|
|
1129
|
+
operation,
|
|
1130
|
+
operation.mode==='video'
|
|
1131
|
+
?SCREEN_CAPTURE_REASONS.videoRecordingStopped
|
|
1132
|
+
:SCREEN_CAPTURE_REASONS.gifRecordingStopped
|
|
1133
|
+
);
|
|
1134
|
+
this.#releaseOperation(operation);
|
|
1135
|
+
return result;
|
|
1136
|
+
}catch(cause){
|
|
1137
|
+
const error=this.#operationError(cause,operation,stage);
|
|
1138
|
+
if(this.#isCurrent(operation))this.#failOperation(operation,error);
|
|
1139
|
+
throw operation.terminalError??error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async stop(optionsValue={}){
|
|
1144
|
+
this.#assertOpen();
|
|
1145
|
+
const options=optionsRecord(optionsValue,'Screen capture stop options');
|
|
1146
|
+
const operation=this.#operation;
|
|
1147
|
+
if(!operation)return null;
|
|
1148
|
+
if(operation.stopCallPromise)return operation.stopCallPromise;
|
|
1149
|
+
operation.stopCallPromise=this.#stopRecording(operation,options);
|
|
1150
|
+
return operation.stopCallPromise;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async prepare(stream,maxWidth,optionsValue={}){
|
|
1154
|
+
this.#assertOpen();
|
|
1155
|
+
const options=optionsRecord(optionsValue,'Screen capture display options');
|
|
1156
|
+
if(options.signal?.aborted)throw abortError(options.signal.reason);
|
|
1157
|
+
let video;
|
|
1158
|
+
try{
|
|
1159
|
+
video=this.document.createElement('video');
|
|
1160
|
+
video.muted=true;
|
|
1161
|
+
video.playsInline=true;
|
|
1162
|
+
video.srcObject=stream;
|
|
1163
|
+
}catch(cause){
|
|
1164
|
+
throw captureError(
|
|
1165
|
+
SCREEN_CAPTURE_ERRORS.displayMetadataRejected,
|
|
1166
|
+
'The selected display could not be attached to a video surface.',
|
|
1167
|
+
cause
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
try{
|
|
1171
|
+
await new Promise(function waitForDisplayMetadata(resolve,reject){
|
|
1172
|
+
let settled=false;
|
|
1173
|
+
function cleanup(){
|
|
1174
|
+
video.removeEventListener?.('loadedmetadata',receiveDisplayMetadata);
|
|
1175
|
+
video.removeEventListener?.('error',rejectDisplayMetadata);
|
|
1176
|
+
options.signal?.removeEventListener?.('abort',abortDisplayMetadata);
|
|
1177
|
+
}
|
|
1178
|
+
function settle(callback,value){
|
|
1179
|
+
if(settled)return;
|
|
1180
|
+
settled=true;
|
|
1181
|
+
cleanup();
|
|
1182
|
+
callback(value);
|
|
1183
|
+
}
|
|
1184
|
+
function receiveDisplayMetadata(){settle(resolve);}
|
|
1185
|
+
function rejectDisplayMetadata(event){
|
|
1186
|
+
settle(reject,captureError(
|
|
1187
|
+
SCREEN_CAPTURE_ERRORS.displayMetadataRejected,
|
|
1188
|
+
'The selected display metadata could not be read.',
|
|
1189
|
+
event
|
|
1190
|
+
));
|
|
1191
|
+
}
|
|
1192
|
+
function abortDisplayMetadata(){
|
|
1193
|
+
settle(reject,abortError(options.signal?.reason));
|
|
1194
|
+
}
|
|
1195
|
+
video.addEventListener('loadedmetadata',receiveDisplayMetadata,{once:true});
|
|
1196
|
+
video.addEventListener('error',rejectDisplayMetadata,{once:true});
|
|
1197
|
+
options.signal?.addEventListener?.('abort',abortDisplayMetadata,{once:true});
|
|
1198
|
+
if(options.signal?.aborted)abortDisplayMetadata();
|
|
1199
|
+
});
|
|
1200
|
+
if(options.signal?.aborted)throw abortError(options.signal.reason);
|
|
1201
|
+
try{await this.#awaitWithSignal(video.play(),options.signal??null);}
|
|
1202
|
+
catch(cause){
|
|
1203
|
+
if(cause?.code===SCREEN_CAPTURE_ERRORS.operationAborted.code
|
|
1204
|
+
&&cause?.reason===SCREEN_CAPTURE_ERRORS.operationAborted.reason){
|
|
1205
|
+
throw cause;
|
|
1206
|
+
}
|
|
1207
|
+
throw captureError(
|
|
1208
|
+
SCREEN_CAPTURE_ERRORS.displayPlaybackRejected,
|
|
1209
|
+
'The selected display could not start playback for capture.',
|
|
1210
|
+
cause
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
if(options.signal?.aborted)throw abortError(options.signal.reason);
|
|
1214
|
+
const width=Number(video.videoWidth);
|
|
1215
|
+
const height=Number(video.videoHeight);
|
|
1216
|
+
if(!Number.isFinite(width)||width<=0||!Number.isFinite(height)||height<=0){
|
|
1217
|
+
throw captureError(
|
|
1218
|
+
SCREEN_CAPTURE_ERRORS.displayMetadataRejected,
|
|
1219
|
+
'The selected display reported invalid frame dimensions.'
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
const scale=Math.min(1,normalizedWidth(maxWidth,1920)/width);
|
|
1223
|
+
const canvas=this.document.createElement('canvas');
|
|
1224
|
+
canvas.width=Math.max(1,Math.round(width*scale));
|
|
1225
|
+
canvas.height=Math.max(1,Math.round(height*scale));
|
|
1226
|
+
return {video,canvas};
|
|
1227
|
+
}catch(error){
|
|
1228
|
+
let cleanupFailure=null;
|
|
1229
|
+
try{video.pause?.();}catch(cause){cleanupFailure??=cause;}
|
|
1230
|
+
try{video.srcObject=null;}catch(cause){cleanupFailure??=cause;}
|
|
1231
|
+
if(cleanupFailure&&!Object.hasOwn(error,'cleanupCause')){
|
|
1232
|
+
error.cleanupCause=cleanupFailure;
|
|
1233
|
+
}
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
#awaitWithSignal(value,signal){
|
|
1239
|
+
if(signal?.aborted)return Promise.reject(abortError(signal.reason));
|
|
1240
|
+
return new Promise(function waitForAbortableSettlement(resolve,reject){
|
|
1241
|
+
let settled=false;
|
|
1242
|
+
function cleanup(){signal?.removeEventListener?.('abort',abortAwaitedOperation);}
|
|
1243
|
+
function settle(callback,result){
|
|
1244
|
+
if(settled)return;
|
|
1245
|
+
settled=true;
|
|
1246
|
+
cleanup();
|
|
1247
|
+
callback(result);
|
|
1248
|
+
}
|
|
1249
|
+
function abortAwaitedOperation(){settle(reject,abortError(signal?.reason));}
|
|
1250
|
+
signal?.addEventListener?.('abort',abortAwaitedOperation,{once:true});
|
|
1251
|
+
Promise.resolve(value).then(
|
|
1252
|
+
function resolveAwaitedOperation(result){settle(resolve,result);},
|
|
1253
|
+
function rejectAwaitedOperation(error){settle(reject,error);}
|
|
1254
|
+
);
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
#canvasBlob(canvas,type,quality,signal){
|
|
1259
|
+
if(signal?.aborted)return Promise.reject(abortError(signal.reason));
|
|
1260
|
+
return new Promise(function waitForCanvasEncoding(resolve,reject){
|
|
1261
|
+
let settled=false;
|
|
1262
|
+
function cleanup(){signal?.removeEventListener?.('abort',abortImageEncoding);}
|
|
1263
|
+
function settle(callback,value){
|
|
1264
|
+
if(settled)return;
|
|
1265
|
+
settled=true;
|
|
1266
|
+
cleanup();
|
|
1267
|
+
callback(value);
|
|
1268
|
+
}
|
|
1269
|
+
function abortImageEncoding(){settle(reject,abortError(signal?.reason));}
|
|
1270
|
+
signal?.addEventListener?.('abort',abortImageEncoding,{once:true});
|
|
1271
|
+
try{
|
|
1272
|
+
canvas.toBlob(
|
|
1273
|
+
function receiveEncodedCapture(blob){
|
|
1274
|
+
if(blob)settle(resolve,blob);
|
|
1275
|
+
else settle(reject,captureError(
|
|
1276
|
+
SCREEN_CAPTURE_ERRORS.imageEncodingRejected,
|
|
1277
|
+
'The capture canvas returned no encoded image.'
|
|
1278
|
+
));
|
|
1279
|
+
},
|
|
1280
|
+
type,
|
|
1281
|
+
quality
|
|
1282
|
+
);
|
|
1283
|
+
}catch(cause){
|
|
1284
|
+
settle(reject,captureError(
|
|
1285
|
+
SCREEN_CAPTURE_ERRORS.imageEncodingRejected,
|
|
1286
|
+
'The capture canvas rejected image encoding.',
|
|
1287
|
+
cause
|
|
1288
|
+
));
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
stopTracks(stream){
|
|
1294
|
+
let firstError=null;
|
|
1295
|
+
for(const track of stream?.getTracks?.()||[]){
|
|
1296
|
+
try{track.stop();}catch(error){firstError??=error;}
|
|
1297
|
+
}
|
|
1298
|
+
if(firstError)throw firstError;
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
reset(){
|
|
1303
|
+
if(this.#disposed||this.#disposing)return false;
|
|
1304
|
+
const operation=this.#operation;
|
|
1305
|
+
if(!operation)return false;
|
|
1306
|
+
const error=captureError(
|
|
1307
|
+
SCREEN_CAPTURE_ERRORS.reset,
|
|
1308
|
+
'The active screen capture was reset.'
|
|
1309
|
+
);
|
|
1310
|
+
return this.#requestOperationAbort(operation,error,{
|
|
1311
|
+
publishError:false,
|
|
1312
|
+
stopReason:error.reason
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
emit(type,detail={}){
|
|
1317
|
+
this.#assertOpen();
|
|
1318
|
+
if(!EVENT_NAMES[type]){
|
|
1319
|
+
throw captureError(
|
|
1320
|
+
SCREEN_CAPTURE_ERRORS.eventTypeUndeclared,
|
|
1321
|
+
'Screen capture events must be requesting, start, result, error, or stop.',
|
|
1322
|
+
undefined,
|
|
1323
|
+
TypeError
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
const mode=typeof detail?.mode==='string'
|
|
1327
|
+
?detail.mode
|
|
1328
|
+
:(this.#operation?.mode??'capture');
|
|
1329
|
+
const operation=this.#operation??{
|
|
1330
|
+
id:this.#nextOperationId(mode,null),
|
|
1331
|
+
mode
|
|
1332
|
+
};
|
|
1333
|
+
const reason=type==='requesting'
|
|
1334
|
+
?SCREEN_CAPTURE_REASONS.displaySelectionRequested
|
|
1335
|
+
:type==='start'
|
|
1336
|
+
?mode==='gif'
|
|
1337
|
+
?SCREEN_CAPTURE_REASONS.gifRecordingStarted
|
|
1338
|
+
:SCREEN_CAPTURE_REASONS.videoRecordingStarted
|
|
1339
|
+
:type==='result'
|
|
1340
|
+
?mode==='image'
|
|
1341
|
+
?SCREEN_CAPTURE_REASONS.imageCompleted
|
|
1342
|
+
:mode==='gif'
|
|
1343
|
+
?SCREEN_CAPTURE_REASONS.gifCompleted
|
|
1344
|
+
:SCREEN_CAPTURE_REASONS.videoCompleted
|
|
1345
|
+
:type==='stop'
|
|
1346
|
+
?mode==='gif'
|
|
1347
|
+
?SCREEN_CAPTURE_REASONS.gifRecordingStopped
|
|
1348
|
+
:SCREEN_CAPTURE_REASONS.videoRecordingStopped
|
|
1349
|
+
:detail?.error?.reason??SCREEN_CAPTURE_ERRORS.cleanupRejected.reason;
|
|
1350
|
+
const status=type==='requesting'
|
|
1351
|
+
?SCREEN_CAPTURE_STATUSES.selectingDisplay
|
|
1352
|
+
:type==='start'
|
|
1353
|
+
?SCREEN_CAPTURE_STATUSES.recording
|
|
1354
|
+
:type==='result'
|
|
1355
|
+
?SCREEN_CAPTURE_STATUSES.captureReady
|
|
1356
|
+
:type==='error'
|
|
1357
|
+
?SCREEN_CAPTURE_STATUSES.captureRejected
|
|
1358
|
+
:SCREEN_CAPTURE_STATUSES.captureStopped;
|
|
1359
|
+
return this.#publish(operation,type,detail,{
|
|
1360
|
+
code:detail?.error?.code??null,
|
|
1361
|
+
reason,
|
|
1362
|
+
status,
|
|
1363
|
+
cancelable:type==='requesting'
|
|
1364
|
+
})?.occurrence??null;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
destroy(){
|
|
1368
|
+
if(this.#disposed||this.#disposing)return false;
|
|
1369
|
+
this.#disposing=true;
|
|
1370
|
+
let cleanupFailure=null;
|
|
1371
|
+
const operation=this.#operation;
|
|
1372
|
+
if(operation){
|
|
1373
|
+
const error=this.#disposedError();
|
|
1374
|
+
this.#requestOperationAbort(operation,error,{
|
|
1375
|
+
publishError:false,
|
|
1376
|
+
stopReason:error.reason
|
|
1377
|
+
});
|
|
1378
|
+
cleanupFailure=operation.terminalError?.cleanupCause??null;
|
|
1379
|
+
}else{
|
|
1380
|
+
try{this.stopTracks(this.stream);}catch(error){cleanupFailure=error;}
|
|
1381
|
+
clearInterval(this.timer);
|
|
1382
|
+
}
|
|
1383
|
+
for(const acquisition of this.#acquisitions){
|
|
1384
|
+
if(!acquisition.controller.signal.aborted){
|
|
1385
|
+
acquisition.controller.abort(this.#disposedError());
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
this.#disposed=true;
|
|
1389
|
+
this.#disposing=false;
|
|
1390
|
+
this.#events.dispose();
|
|
1391
|
+
if(cleanupFailure){
|
|
1392
|
+
throw captureError(
|
|
1393
|
+
SCREEN_CAPTURE_ERRORS.cleanupRejected,
|
|
1394
|
+
'Screen capture disposal could not release every active resource.',
|
|
1395
|
+
cleanupFailure
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
dispose(){return this.destroy();}
|
|
20
1402
|
}
|