arcane-os 0.3.1 → 0.3.2

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 (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,142 +1,142 @@
1
1
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
2
  import GifEncoder from './GifEncoder.js';
3
3
 
4
- export const SCREEN_CAPTURE_EVENT_TYPES=Object.freeze({
4
+ export const SCREEN_CAPTURE_EVENT_TYPES={
5
5
  displaySelectionRequested:'capture-requesting',
6
6
  captureStarted:'capture-start',
7
7
  captureCompleted:'capture-result',
8
8
  captureFailed:'capture-error',
9
9
  captureStopped:'capture-stop'
10
- });
10
+ };
11
11
 
12
- export const SCREEN_CAPTURE_STATUSES=Object.freeze({
12
+ export const SCREEN_CAPTURE_STATUSES={
13
13
  selectingDisplay:'selecting-display',
14
14
  recording:'recording',
15
15
  captureReady:'capture-ready',
16
16
  captureRejected:'capture-rejected',
17
17
  captureStopped:'capture-stopped'
18
- });
18
+ };
19
19
 
20
- export const SCREEN_CAPTURE_IMAGE_TYPE_FALLBACK=Object.freeze({
20
+ export const SCREEN_CAPTURE_IMAGE_TYPE_FALLBACK={
21
21
  when:'blob-type-unreported',
22
22
  mimeType:'image/png',
23
23
  extension:'png'
24
- });
24
+ };
25
25
 
26
- export const SCREEN_CAPTURE_ERRORS=Object.freeze({
27
- active:Object.freeze({
26
+ export const SCREEN_CAPTURE_ERRORS={
27
+ active:{
28
28
  code:'ARCANE_SCREEN_CAPTURE_OPERATION_ACTIVE',
29
29
  reason:'screen-capture-operation-active'
30
- }),
31
- disposed:Object.freeze({
30
+ },
31
+ disposed:{
32
32
  code:'ARCANE_SCREEN_CAPTURE_DISPOSED',
33
33
  reason:'screen-capture-disposed'
34
- }),
35
- optionsRecordRejected:Object.freeze({
34
+ },
35
+ optionsRecordRejected:{
36
36
  code:'ARCANE_SCREEN_CAPTURE_OPTIONS_RECORD_REJECTED',
37
37
  reason:'screen-capture-options-record-rejected'
38
- }),
39
- abortSignalRejected:Object.freeze({
38
+ },
39
+ abortSignalRejected:{
40
40
  code:'ARCANE_SCREEN_CAPTURE_ABORT_SIGNAL_REJECTED',
41
41
  reason:'screen-capture-abort-signal-rejected'
42
- }),
43
- operationIdRejected:Object.freeze({
42
+ },
43
+ operationIdRejected:{
44
44
  code:'ARCANE_SCREEN_CAPTURE_OPERATION_ID_REJECTED',
45
45
  reason:'screen-capture-operation-id-rejected'
46
- }),
47
- cleanupRejected:Object.freeze({
46
+ },
47
+ cleanupRejected:{
48
48
  code:'ARCANE_SCREEN_CAPTURE_CLEANUP_REJECTED',
49
49
  reason:'screen-capture-cleanup-rejected'
50
- }),
51
- displayUnavailable:Object.freeze({
50
+ },
51
+ displayUnavailable:{
52
52
  code:'ARCANE_SCREEN_CAPTURE_DISPLAY_UNAVAILABLE',
53
53
  reason:'screen-capture-display-unavailable'
54
- }),
55
- displayMetadataRejected:Object.freeze({
54
+ },
55
+ displayMetadataRejected:{
56
56
  code:'ARCANE_SCREEN_CAPTURE_DISPLAY_METADATA_REJECTED',
57
57
  reason:'screen-capture-display-metadata-rejected'
58
- }),
59
- displayPlaybackRejected:Object.freeze({
58
+ },
59
+ displayPlaybackRejected:{
60
60
  code:'ARCANE_SCREEN_CAPTURE_DISPLAY_PLAYBACK_REJECTED',
61
61
  reason:'screen-capture-display-playback-rejected'
62
- }),
63
- displaySelectionCancelled:Object.freeze({
62
+ },
63
+ displaySelectionCancelled:{
64
64
  code:'ARCANE_SCREEN_CAPTURE_DISPLAY_SELECTION_CANCELLED',
65
65
  reason:'screen-capture-display-selection-cancelled'
66
- }),
67
- displaySelectionRejected:Object.freeze({
66
+ },
67
+ displaySelectionRejected:{
68
68
  code:'ARCANE_SCREEN_CAPTURE_DISPLAY_SELECTION_REJECTED',
69
69
  reason:'screen-capture-display-selection-rejected'
70
- }),
71
- displayTrackEndedBeforeRecording:Object.freeze({
70
+ },
71
+ displayTrackEndedBeforeRecording:{
72
72
  code:'ARCANE_SCREEN_CAPTURE_DISPLAY_TRACK_ENDED_BEFORE_RECORDING',
73
73
  reason:'screen-capture-display-track-ended-before-recording'
74
- }),
75
- eventTypeUndeclared:Object.freeze({
74
+ },
75
+ eventTypeUndeclared:{
76
76
  code:'ARCANE_SCREEN_CAPTURE_EVENT_TYPE_UNDECLARED',
77
77
  reason:'screen-capture-event-type-undeclared'
78
- }),
79
- gifEncodingRejected:Object.freeze({
78
+ },
79
+ gifEncodingRejected:{
80
80
  code:'ARCANE_SCREEN_CAPTURE_GIF_ENCODING_REJECTED',
81
81
  reason:'screen-capture-gif-encoding-rejected'
82
- }),
83
- gifFrameReadRejected:Object.freeze({
82
+ },
83
+ gifFrameReadRejected:{
84
84
  code:'ARCANE_SCREEN_CAPTURE_GIF_FRAME_READ_REJECTED',
85
85
  reason:'screen-capture-gif-frame-read-rejected'
86
- }),
87
- imageCanvasUnavailable:Object.freeze({
86
+ },
87
+ imageCanvasUnavailable:{
88
88
  code:'ARCANE_SCREEN_CAPTURE_IMAGE_CANVAS_UNAVAILABLE',
89
89
  reason:'screen-capture-image-canvas-unavailable'
90
- }),
91
- imageDrawRejected:Object.freeze({
90
+ },
91
+ imageDrawRejected:{
92
92
  code:'ARCANE_SCREEN_CAPTURE_IMAGE_DRAW_REJECTED',
93
93
  reason:'screen-capture-image-draw-rejected'
94
- }),
95
- imageEncodingRejected:Object.freeze({
94
+ },
95
+ imageEncodingRejected:{
96
96
  code:'ARCANE_SCREEN_CAPTURE_IMAGE_ENCODING_REJECTED',
97
97
  reason:'screen-capture-image-encoding-rejected'
98
- }),
99
- recorderConstructionRejected:Object.freeze({
98
+ },
99
+ recorderConstructionRejected:{
100
100
  code:'ARCANE_SCREEN_CAPTURE_RECORDER_CONSTRUCTION_REJECTED',
101
101
  reason:'screen-capture-recorder-construction-rejected'
102
- }),
103
- recorderErrorReceived:Object.freeze({
102
+ },
103
+ recorderErrorReceived:{
104
104
  code:'ARCANE_SCREEN_CAPTURE_RECORDER_ERROR_RECEIVED',
105
105
  reason:'screen-capture-recorder-error-received'
106
- }),
107
- recorderStartRejected:Object.freeze({
106
+ },
107
+ recorderStartRejected:{
108
108
  code:'ARCANE_SCREEN_CAPTURE_RECORDER_START_REJECTED',
109
109
  reason:'screen-capture-recorder-start-rejected'
110
- }),
111
- recorderStopRejected:Object.freeze({
110
+ },
111
+ recorderStopRejected:{
112
112
  code:'ARCANE_SCREEN_CAPTURE_RECORDER_STOP_REJECTED',
113
113
  reason:'screen-capture-recorder-stop-rejected'
114
- }),
115
- recorderUnavailable:Object.freeze({
114
+ },
115
+ recorderUnavailable:{
116
116
  code:'ARCANE_SCREEN_CAPTURE_RECORDER_UNAVAILABLE',
117
117
  reason:'screen-capture-recorder-unavailable'
118
- }),
119
- operationAborted:Object.freeze({
118
+ },
119
+ operationAborted:{
120
120
  code:'ARCANE_SCREEN_CAPTURE_OPERATION_ABORTED',
121
121
  reason:'screen-capture-operation-aborted'
122
- }),
123
- operationStopped:Object.freeze({
122
+ },
123
+ operationStopped:{
124
124
  code:'ARCANE_SCREEN_CAPTURE_OPERATION_STOPPED',
125
125
  reason:'screen-capture-operation-stopped'
126
- }),
127
- reset:Object.freeze({
126
+ },
127
+ reset:{
128
128
  code:'ARCANE_SCREEN_CAPTURE_OPERATION_RESET',
129
129
  reason:'screen-capture-reset'
130
- })
131
- });
130
+ }
131
+ };
132
132
 
133
- export const SCREEN_CAPTURE_ERROR_CODES=Object.freeze(Object.fromEntries(
133
+ export const SCREEN_CAPTURE_ERROR_CODES=Object.fromEntries(
134
134
  Object.entries(SCREEN_CAPTURE_ERRORS).map(function mapScreenCaptureCode(entry){
135
135
  return [entry[0],entry[1].code];
136
136
  })
137
- ));
137
+ );
138
138
 
139
- export const SCREEN_CAPTURE_REASONS=Object.freeze({
139
+ export const SCREEN_CAPTURE_REASONS={
140
140
  displaySelectionRequested:'screen-capture-display-selection-requested',
141
141
  gifCompleted:'screen-capture-gif-completed',
142
142
  gifRecordingStarted:'screen-capture-gif-recording-started',
@@ -148,15 +148,15 @@ export const SCREEN_CAPTURE_REASONS=Object.freeze({
148
148
  ...Object.fromEntries(Object.entries(SCREEN_CAPTURE_ERRORS).map(
149
149
  function mapScreenCaptureReason(entry){return [entry[0],entry[1].reason];}
150
150
  ))
151
- });
151
+ };
152
152
 
153
- const EVENT_NAMES=Object.freeze({
153
+ const EVENT_NAMES={
154
154
  requesting:SCREEN_CAPTURE_EVENT_TYPES.displaySelectionRequested,
155
155
  start:SCREEN_CAPTURE_EVENT_TYPES.captureStarted,
156
156
  result:SCREEN_CAPTURE_EVENT_TYPES.captureCompleted,
157
157
  error:SCREEN_CAPTURE_EVENT_TYPES.captureFailed,
158
158
  stop:SCREEN_CAPTURE_EVENT_TYPES.captureStopped
159
- });
159
+ };
160
160
 
161
161
  function signalLike(value){
162
162
  return value===undefined
@@ -212,11 +212,10 @@ function admittedOperationId(value){
212
212
  if(value===undefined||value===null)return null;
213
213
  if(typeof value!=='string'
214
214
  ||value.trim()!==value
215
- ||value.length<1
216
- ||value.length>256){
215
+ ||!value){
217
216
  throw captureError(
218
217
  SCREEN_CAPTURE_ERRORS.operationIdRejected,
219
- 'Screen capture operationId must contain 1-256 non-edge-whitespace characters.',
218
+ 'Screen capture operationId must be a non-empty string without edge whitespace.',
220
219
  undefined,
221
220
  TypeError
222
221
  );
@@ -224,10 +223,6 @@ function admittedOperationId(value){
224
223
  return value;
225
224
  }
226
225
 
227
- function normalizedWidth(value,fallback){
228
- return Math.max(1,Number(value)||fallback);
229
- }
230
-
231
226
  function imageResultType(blob){
232
227
  const reported=typeof blob?.type==='string'?blob.type.trim().toLowerCase():'';
233
228
  if(!reported)return SCREEN_CAPTURE_IMAGE_TYPE_FALLBACK;
@@ -240,7 +235,7 @@ function imageResultType(blob){
240
235
  :/^[a-z0-9][a-z0-9.-]*$/u.test(canonicalSubtype)
241
236
  ?canonicalSubtype
242
237
  :'bin';
243
- return Object.freeze({mimeType:reported,extension});
238
+ return {mimeType:reported,extension};
244
239
  }
245
240
 
246
241
  function supportedRecorderType(Recorder){
@@ -260,6 +255,10 @@ function reportDetachedError(error){
260
255
  else globalThis.console?.error?.(error);
261
256
  }
262
257
 
258
+ function monotonicNow(){
259
+ return globalThis.performance?.now?.()??Date.now();
260
+ }
261
+
263
262
  export default class ScreenCapture extends EventTarget{
264
263
  #acquisitions=new Set();
265
264
  #disposed=false;
@@ -289,7 +288,7 @@ export default class ScreenCapture extends EventTarget{
289
288
  this.gifDelay=0;
290
289
  this.#events=createArcaneEventSource(this,{
291
290
  source:'screen-capture',
292
- eventTypes:Object.freeze(Object.values(SCREEN_CAPTURE_EVENT_TYPES))
291
+ eventTypes:Object.values(SCREEN_CAPTURE_EVENT_TYPES)
293
292
  });
294
293
  }
295
294
 
@@ -360,7 +359,7 @@ export default class ScreenCapture extends EventTarget{
360
359
  let pending;
361
360
  try{
362
361
  pending=Promise.resolve(this.mediaDevices.getDisplayMedia({
363
- video:{frameRate:{ideal:15,max:30}},
362
+ video:true,
364
363
  audio:Boolean(options.audio)
365
364
  }));
366
365
  }catch(cause){
@@ -424,30 +423,23 @@ export default class ScreenCapture extends EventTarget{
424
423
  }
425
424
 
426
425
  #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}:{}),
426
+ return {
427
+ ...detail,
436
428
  ...(code?{code}:{}),
437
429
  reason,
438
430
  status
439
- });
431
+ };
440
432
  }
441
433
 
442
434
  #publish(operation,type,detail,{code=null,reason,status,cancelable=false}={}){
443
435
  if(this.#events.disposed)return null;
444
- const compatibilityDetail=Object.freeze({
436
+ const compatibilityDetail={
445
437
  ...detail,
446
438
  mode:typeof detail?.mode==='string'?detail.mode:operation.mode,
447
439
  operationId:operation.id,
448
440
  reason,
449
441
  ...(code?{code}:{})
450
- });
442
+ };
451
443
  return this.#events.dispatch(
452
444
  EVENT_NAMES[type],
453
445
  compatibilityDetail,
@@ -465,6 +457,7 @@ export default class ScreenCapture extends EventTarget{
465
457
  const controller=new AbortController();
466
458
  const operation={
467
459
  abortPublishesError:true,
460
+ cancelFrameSampling:null,
468
461
  cleanup:[],
469
462
  completed:false,
470
463
  controller,
@@ -493,7 +486,7 @@ export default class ScreenCapture extends EventTarget{
493
486
  const publication=this.#publish(
494
487
  operation,
495
488
  'requesting',
496
- Object.freeze({mode}),
489
+ {mode},
497
490
  {
498
491
  reason:SCREEN_CAPTURE_REASONS.displaySelectionRequested,
499
492
  status:SCREEN_CAPTURE_STATUSES.selectingDisplay,
@@ -577,6 +570,15 @@ export default class ScreenCapture extends EventTarget{
577
570
  for(const remove of operation.recorderCleanup.splice(0))remove();
578
571
  }
579
572
 
573
+ #cancelFrameSampling(operation){
574
+ let error=null;
575
+ try{operation.cancelFrameSampling?.();}catch(cause){error=cause;}
576
+ operation.cancelFrameSampling=null;
577
+ operation.timer=0;
578
+ this.timer=0;
579
+ return error;
580
+ }
581
+
580
582
  #settleRecorder(operation,outcome){
581
583
  if(!operation.resolveRecorderOutcome)return false;
582
584
  const resolve=operation.resolveRecorderOutcome;
@@ -589,10 +591,8 @@ export default class ScreenCapture extends EventTarget{
589
591
  #cleanupResources(operation,{settleRecorderError=null}={}){
590
592
  let firstError=null;
591
593
  function remember(error){firstError??=error;}
592
- clearInterval(operation.timer??0);
593
- clearInterval(this.timer);
594
- operation.timer=0;
595
- this.timer=0;
594
+ const frameSamplingError=this.#cancelFrameSampling(operation);
595
+ if(frameSamplingError)remember(frameSamplingError);
596
596
  if(settleRecorderError)this.#settleRecorder(operation,{error:settleRecorderError});
597
597
  const recorder=operation.recorder??this.recorder;
598
598
  if(recorder&&recorder.state!=='inactive'){
@@ -633,7 +633,7 @@ export default class ScreenCapture extends EventTarget{
633
633
  this.#publish(
634
634
  operation,
635
635
  'error',
636
- Object.freeze({error,mode:operation.mode}),
636
+ {error,mode:operation.mode},
637
637
  {
638
638
  code:error.code,
639
639
  reason:error.reason,
@@ -649,7 +649,7 @@ export default class ScreenCapture extends EventTarget{
649
649
  this.#publish(
650
650
  operation,
651
651
  'stop',
652
- Object.freeze({mode:operation.mode}),
652
+ {mode:operation.mode},
653
653
  {code,reason,status:SCREEN_CAPTURE_STATUSES.captureStopped}
654
654
  );
655
655
  return true;
@@ -761,7 +761,6 @@ export default class ScreenCapture extends EventTarget{
761
761
  stage='display-preparation';
762
762
  const prepared=await this.prepare(
763
763
  stream,
764
- normalizedWidth(options.maxWidth,1920),
765
764
  {signal:operation.controller.signal}
766
765
  );
767
766
  this.#setPreparedSurface(operation,prepared);
@@ -833,7 +832,7 @@ export default class ScreenCapture extends EventTarget{
833
832
  this.stopPromise=operation.stopPromise;
834
833
  const owner=this;
835
834
  function receiveRecordedData(event){
836
- if(event.data?.size)chunks.push(event.data);
835
+ if(event.data)chunks.push(event.data);
837
836
  }
838
837
  function receiveRecorderStop(){
839
838
  let outcome;
@@ -844,7 +843,7 @@ export default class ScreenCapture extends EventTarget{
844
843
  blob:new Blob(chunks,{type}),
845
844
  mimeType:type,
846
845
  extension:type.includes('mp4')?'mp4':'webm',
847
- duration:Math.max(0,Date.now()-operation.startedAt)
846
+ duration:Date.now()-operation.startedAt
848
847
  }
849
848
  };
850
849
  }catch(cause){
@@ -956,7 +955,7 @@ export default class ScreenCapture extends EventTarget{
956
955
  this.#publish(
957
956
  operation,
958
957
  'start',
959
- Object.freeze({mode:'video'}),
958
+ {mode:'video'},
960
959
  {
961
960
  reason:SCREEN_CAPTURE_REASONS.videoRecordingStarted,
962
961
  status:SCREEN_CAPTURE_STATUSES.recording
@@ -970,7 +969,7 @@ export default class ScreenCapture extends EventTarget{
970
969
  }
971
970
  }
972
971
 
973
- #sampleGifFrame(operation,context){
972
+ #sampleGifFrame(operation,context,capturedAt=monotonicNow()){
974
973
  context.drawImage(
975
974
  operation.video,
976
975
  0,
@@ -984,9 +983,61 @@ export default class ScreenCapture extends EventTarget{
984
983
  operation.canvas.width,
985
984
  operation.canvas.height
986
985
  ));
986
+ operation.frameTimes.push(capturedAt);
987
987
  this.frames=operation.frames;
988
988
  }
989
989
 
990
+ #startGifFrameSampling(operation,context,frameDelay){
991
+ const requestedDelay=Number(frameDelay);
992
+ const explicitDelay=Number.isFinite(requestedDelay)&&requestedDelay>0
993
+ ?requestedDelay
994
+ :null;
995
+ let active=true;
996
+ let frameHandle=0;
997
+ let requestFrame;
998
+ let cancelFrame;
999
+ if(explicitDelay!==null){
1000
+ requestFrame=callback=>setTimeout(()=>callback(monotonicNow()),explicitDelay);
1001
+ cancelFrame=handle=>clearTimeout(handle);
1002
+ }else if(typeof operation.video?.requestVideoFrameCallback==='function'){
1003
+ requestFrame=callback=>operation.video.requestVideoFrameCallback(callback);
1004
+ cancelFrame=handle=>operation.video.cancelVideoFrameCallback?.(handle);
1005
+ }else if(typeof globalThis.requestAnimationFrame==='function'){
1006
+ requestFrame=callback=>globalThis.requestAnimationFrame(callback);
1007
+ cancelFrame=handle=>globalThis.cancelAnimationFrame?.(handle);
1008
+ }else{
1009
+ throw captureError(
1010
+ SCREEN_CAPTURE_ERRORS.gifFrameReadRejected,
1011
+ 'The browser does not expose a display-frame callback for GIF capture.'
1012
+ );
1013
+ }
1014
+ const owner=this;
1015
+ function schedule(){
1016
+ if(!active||!owner.#isCurrent(operation)||operation.phase!=='recording')return;
1017
+ frameHandle=requestFrame(sampleDisplayFrame);
1018
+ operation.timer=frameHandle;
1019
+ owner.timer=frameHandle;
1020
+ }
1021
+ function sampleDisplayFrame(now){
1022
+ if(!active||!owner.#isCurrent(operation)||operation.phase!=='recording')return;
1023
+ try{owner.#sampleGifFrame(operation,context,now);}
1024
+ catch(cause){
1025
+ owner.#failOperation(
1026
+ operation,
1027
+ owner.#operationError(cause,operation,'gif-frame-read')
1028
+ );
1029
+ return;
1030
+ }
1031
+ schedule();
1032
+ }
1033
+ operation.cancelFrameSampling=function cancelGifFrameSampling(){
1034
+ if(!active)return;
1035
+ active=false;
1036
+ cancelFrame(frameHandle);
1037
+ };
1038
+ schedule();
1039
+ }
1040
+
990
1041
  async startGif(optionsValue={}){
991
1042
  const options=optionsRecord(optionsValue,'GIF capture options');
992
1043
  const operation=this.#beginOperation('gif',{
@@ -1000,7 +1051,6 @@ export default class ScreenCapture extends EventTarget{
1000
1051
  stage='display-preparation';
1001
1052
  const prepared=await this.prepare(
1002
1053
  stream,
1003
- normalizedWidth(options.maxWidth,640),
1004
1054
  {signal:operation.controller.signal}
1005
1055
  );
1006
1056
  this.#setPreparedSurface(operation,prepared);
@@ -1012,7 +1062,11 @@ export default class ScreenCapture extends EventTarget{
1012
1062
  );
1013
1063
  }
1014
1064
  operation.frames=[];
1015
- operation.gifDelay=Math.max(100,Number(options.frameDelay)||250);
1065
+ operation.frameTimes=[];
1066
+ const requestedDelay=Number(options.frameDelay);
1067
+ operation.gifDelay=Number.isFinite(requestedDelay)&&requestedDelay>0
1068
+ ?requestedDelay
1069
+ :0;
1016
1070
  operation.startedAt=Date.now();
1017
1071
  this.frames=operation.frames;
1018
1072
  this.gifDelay=operation.gifDelay;
@@ -1020,23 +1074,11 @@ export default class ScreenCapture extends EventTarget{
1020
1074
  stage='gif-frame-read';
1021
1075
  this.#sampleGifFrame(operation,context);
1022
1076
  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;
1077
+ this.#startGifFrameSampling(operation,context,options.frameDelay);
1036
1078
  this.#publish(
1037
1079
  operation,
1038
1080
  'start',
1039
- Object.freeze({mode:'gif'}),
1081
+ {mode:'gif'},
1040
1082
  {
1041
1083
  reason:SCREEN_CAPTURE_REASONS.gifRecordingStarted,
1042
1084
  status:SCREEN_CAPTURE_STATUSES.recording
@@ -1095,18 +1137,23 @@ export default class ScreenCapture extends EventTarget{
1095
1137
  );
1096
1138
  }
1097
1139
  }else{
1098
- clearInterval(operation.timer);
1099
- operation.timer=0;
1100
- this.timer=0;
1140
+ const frameSamplingError=this.#cancelFrameSampling(operation);
1141
+ if(frameSamplingError)throw frameSamplingError;
1142
+ const stoppedAt=monotonicNow();
1101
1143
  const encoder=new GifEncoder(operation.canvas.width,operation.canvas.height);
1102
- for(const frame of operation.frames){
1103
- encoder.addFrame(frame,{delay:operation.gifDelay});
1144
+ for(let index=0;index<operation.frames.length;index++){
1145
+ const capturedAt=operation.frameTimes[index];
1146
+ const nextCapturedAt=operation.frameTimes[index+1]??stoppedAt;
1147
+ encoder.addFrame(
1148
+ operation.frames[index],
1149
+ {delay:Math.max(0,nextCapturedAt-capturedAt)}
1150
+ );
1104
1151
  }
1105
1152
  result={
1106
1153
  blob:encoder.encode(),
1107
1154
  mimeType:'image/gif',
1108
1155
  extension:'gif',
1109
- duration:Math.max(0,Date.now()-operation.startedAt),
1156
+ duration:Date.now()-operation.startedAt,
1110
1157
  width:operation.canvas.width,
1111
1158
  height:operation.canvas.height
1112
1159
  };
@@ -1150,9 +1197,11 @@ export default class ScreenCapture extends EventTarget{
1150
1197
  return operation.stopCallPromise;
1151
1198
  }
1152
1199
 
1153
- async prepare(stream,maxWidth,optionsValue={}){
1200
+ async prepare(stream,optionsValue={},legacyOptionsValue={}){
1154
1201
  this.#assertOpen();
1155
- const options=optionsRecord(optionsValue,'Screen capture display options');
1202
+ const options=typeof optionsValue==='number'
1203
+ ?optionsRecord(legacyOptionsValue,'Screen capture display options')
1204
+ :optionsRecord(optionsValue,'Screen capture display options');
1156
1205
  if(options.signal?.aborted)throw abortError(options.signal.reason);
1157
1206
  let video;
1158
1207
  try{
@@ -1219,10 +1268,9 @@ export default class ScreenCapture extends EventTarget{
1219
1268
  'The selected display reported invalid frame dimensions.'
1220
1269
  );
1221
1270
  }
1222
- const scale=Math.min(1,normalizedWidth(maxWidth,1920)/width);
1223
1271
  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));
1272
+ canvas.width=width;
1273
+ canvas.height=height;
1226
1274
  return {video,canvas};
1227
1275
  }catch(error){
1228
1276
  let cleanupFailure=null;
@@ -1378,7 +1426,6 @@ export default class ScreenCapture extends EventTarget{
1378
1426
  cleanupFailure=operation.terminalError?.cleanupCause??null;
1379
1427
  }else{
1380
1428
  try{this.stopTracks(this.stream);}catch(error){cleanupFailure=error;}
1381
- clearInterval(this.timer);
1382
1429
  }
1383
1430
  for(const acquisition of this.#acquisitions){
1384
1431
  if(!acquisition.controller.signal.aborted){