@vidtreo/recorder 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,3218 @@
1
+ // src/core/utils/error-handler.ts
2
+ function extractErrorMessage(error) {
3
+ if (error instanceof Error) {
4
+ return error.message;
5
+ }
6
+ return String(error);
7
+ }
8
+
9
+ // src/core/audio/audio-level-analyzer.ts
10
+ var AUDIO_LEVEL_INTERVAL = 100;
11
+ var FFT_SIZE = 2048;
12
+ var SMOOTHING_TIME_CONSTANT = 0.8;
13
+ var AUDIO_MIDPOINT = 128;
14
+ var AUDIO_NORMALIZATION_DIVISOR = 128;
15
+ var MIN_DB = -60;
16
+ var DB_OFFSET = 50;
17
+ var DB_RANGE = 50;
18
+ var POWER_CURVE_EXPONENT = 0.6;
19
+ var MAX_AUDIO_LEVEL = 100;
20
+ var LEVEL_MULTIPLIER = 110;
21
+
22
+ class AudioLevelAnalyzer {
23
+ audioContext = null;
24
+ analyser = null;
25
+ audioLevelIntervalId = null;
26
+ audioLevel = 0;
27
+ getMutedState = null;
28
+ currentStream = null;
29
+ startTracking(stream, callbacks, getMutedState) {
30
+ if (!stream) {
31
+ throw new Error("Stream is required");
32
+ }
33
+ if (!getMutedState) {
34
+ throw new Error("getMutedState callback is required");
35
+ }
36
+ this.stopTracking();
37
+ this.currentStream = stream;
38
+ this.getMutedState = getMutedState;
39
+ const audioTracks = stream.getAudioTracks();
40
+ if (audioTracks.length === 0) {
41
+ throw new Error("Stream has no audio tracks");
42
+ }
43
+ const AudioContextClass = this.getAudioContextClass();
44
+ if (!AudioContextClass) {
45
+ throw new Error("AudioContext is not supported in this browser");
46
+ }
47
+ try {
48
+ this.audioContext = new AudioContextClass;
49
+ } catch (error) {
50
+ throw new Error(`Failed to create AudioContext: ${extractErrorMessage(error)}`);
51
+ }
52
+ const source = this.audioContext.createMediaStreamSource(stream);
53
+ this.analyser = this.audioContext.createAnalyser();
54
+ this.analyser.fftSize = FFT_SIZE;
55
+ this.analyser.smoothingTimeConstant = SMOOTHING_TIME_CONSTANT;
56
+ source.connect(this.analyser);
57
+ const dataArray = new Uint8Array(this.analyser.fftSize);
58
+ this.audioLevelIntervalId = window.setInterval(() => {
59
+ if (!this.analyser) {
60
+ return;
61
+ }
62
+ this.analyser.getByteTimeDomainData(dataArray);
63
+ const audioLevel = this.calculateAudioLevel(dataArray);
64
+ this.audioLevel = audioLevel;
65
+ const isMuted = this.checkMutedState();
66
+ const displayLevel = isMuted ? 0 : audioLevel;
67
+ callbacks.onLevelUpdate(displayLevel, isMuted);
68
+ }, AUDIO_LEVEL_INTERVAL);
69
+ }
70
+ stopTracking() {
71
+ if (this.audioLevelIntervalId !== null) {
72
+ clearInterval(this.audioLevelIntervalId);
73
+ this.audioLevelIntervalId = null;
74
+ }
75
+ if (this.analyser) {
76
+ this.analyser.disconnect();
77
+ this.analyser = null;
78
+ }
79
+ if (this.audioContext) {
80
+ this.audioContext.close();
81
+ this.audioContext = null;
82
+ }
83
+ this.audioLevel = 0;
84
+ this.getMutedState = null;
85
+ this.currentStream = null;
86
+ }
87
+ getAudioLevel() {
88
+ return this.audioLevel;
89
+ }
90
+ getAudioContextClass() {
91
+ if (window.AudioContext) {
92
+ return window.AudioContext;
93
+ }
94
+ const webkitAudioContext = window.webkitAudioContext;
95
+ if (webkitAudioContext) {
96
+ return webkitAudioContext;
97
+ }
98
+ return null;
99
+ }
100
+ calculateAudioLevel(dataArray) {
101
+ let sumSquares = 0;
102
+ for (const value of dataArray) {
103
+ const normalized = (value - AUDIO_MIDPOINT) / AUDIO_NORMALIZATION_DIVISOR;
104
+ sumSquares += normalized * normalized;
105
+ }
106
+ const rms = Math.sqrt(sumSquares / dataArray.length);
107
+ const db = rms > 0 ? 20 * Math.log10(rms) : MIN_DB;
108
+ const normalizedDb = Math.max(0, Math.min(1, (db + DB_OFFSET) / DB_RANGE));
109
+ const powerCurve = normalizedDb ** POWER_CURVE_EXPONENT;
110
+ return Math.min(MAX_AUDIO_LEVEL, powerCurve * LEVEL_MULTIPLIER);
111
+ }
112
+ checkMutedState() {
113
+ if (!this.getMutedState) {
114
+ throw new Error("getMutedState callback is not set");
115
+ }
116
+ const isMutedFromCallback = this.getMutedState();
117
+ if (!this.currentStream) {
118
+ throw new Error("Current stream is not set");
119
+ }
120
+ const audioTracks = this.currentStream.getAudioTracks();
121
+ const hasDisabledTracks = audioTracks.length > 0 && audioTracks.some((track) => !track.enabled);
122
+ return isMutedFromCallback || hasDisabledTracks;
123
+ }
124
+ }
125
+ // src/core/config/default-config.ts
126
+ var DEFAULT_TRANSCODE_CONFIG = Object.freeze({
127
+ format: "mp4",
128
+ fps: 30,
129
+ width: 1280,
130
+ height: 720,
131
+ bitrate: 500000,
132
+ audioCodec: "aac",
133
+ audioBitrate: 128000,
134
+ preset: "medium",
135
+ packetCount: 1200
136
+ });
137
+
138
+ // src/core/config/preset-mapper.ts
139
+ var BITRATE_MAP = {
140
+ sd: 500000,
141
+ hd: 1e6,
142
+ fhd: 2000000,
143
+ "4k": 8000000
144
+ };
145
+ var AUDIO_BITRATE = 128000;
146
+ var PACKET_COUNT_MAP = {
147
+ sd: 800,
148
+ hd: 1200,
149
+ fhd: 2000,
150
+ "4k": 4000
151
+ };
152
+ var DEFAULT_FPS = 30;
153
+ var DEFAULT_FORMAT = "mp4";
154
+ var DEFAULT_AUDIO_CODEC = "aac";
155
+ var DEFAULT_PRESET = "medium";
156
+ function mapPresetToConfig(preset, maxWidth, maxHeight) {
157
+ if (!(preset in BITRATE_MAP)) {
158
+ throw new Error(`Invalid preset: ${preset}`);
159
+ }
160
+ if (typeof maxWidth !== "number" || maxWidth <= 0) {
161
+ throw new Error("maxWidth must be a positive number");
162
+ }
163
+ if (typeof maxHeight !== "number" || maxHeight <= 0) {
164
+ throw new Error("maxHeight must be a positive number");
165
+ }
166
+ return {
167
+ format: DEFAULT_FORMAT,
168
+ fps: DEFAULT_FPS,
169
+ width: maxWidth,
170
+ height: maxHeight,
171
+ bitrate: BITRATE_MAP[preset],
172
+ audioCodec: DEFAULT_AUDIO_CODEC,
173
+ preset: DEFAULT_PRESET,
174
+ packetCount: PACKET_COUNT_MAP[preset],
175
+ audioBitrate: AUDIO_BITRATE
176
+ };
177
+ }
178
+
179
+ // src/core/config/config-service.ts
180
+ var DEFAULT_CACHE_TIMEOUT = 5 * 60 * 1000;
181
+ var CONFIG_API_PATH = "/api/v1/videos/config";
182
+
183
+ class ConfigService {
184
+ cachedConfig = null;
185
+ cacheTimestamp = 0;
186
+ cacheTimeout;
187
+ fetchPromise = null;
188
+ options;
189
+ constructor(options) {
190
+ this.options = options;
191
+ if (options.cacheTimeout !== undefined) {
192
+ if (typeof options.cacheTimeout !== "number" || options.cacheTimeout <= 0) {
193
+ throw new Error("cacheTimeout must be a positive number");
194
+ }
195
+ this.cacheTimeout = options.cacheTimeout;
196
+ } else {
197
+ this.cacheTimeout = DEFAULT_CACHE_TIMEOUT;
198
+ }
199
+ }
200
+ async fetchConfig() {
201
+ const now = Date.now();
202
+ if (this.cachedConfig && now - this.cacheTimestamp < this.cacheTimeout) {
203
+ return this.cachedConfig;
204
+ }
205
+ if (this.fetchPromise) {
206
+ return this.fetchPromise;
207
+ }
208
+ this.fetchPromise = this.fetchConfigFromBackend();
209
+ try {
210
+ const config = await this.fetchPromise;
211
+ this.cachedConfig = config;
212
+ this.cacheTimestamp = now;
213
+ return config;
214
+ } catch {
215
+ return DEFAULT_TRANSCODE_CONFIG;
216
+ } finally {
217
+ this.fetchPromise = null;
218
+ }
219
+ }
220
+ clearCache() {
221
+ this.cachedConfig = null;
222
+ this.cacheTimestamp = 0;
223
+ }
224
+ getCurrentConfig() {
225
+ if (!this.cachedConfig) {
226
+ throw new Error("No cached config available. Call fetchConfig() first.");
227
+ }
228
+ return this.cachedConfig;
229
+ }
230
+ async fetchConfigFromBackend() {
231
+ const url = `${this.options.backendUrl}${CONFIG_API_PATH}`;
232
+ const response = await fetch(url, {
233
+ method: "GET",
234
+ headers: {
235
+ Authorization: `Bearer ${this.options.apiKey}`,
236
+ "Content-Type": "application/json"
237
+ }
238
+ });
239
+ if (!response.ok) {
240
+ throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
241
+ }
242
+ const data = await response.json();
243
+ if (!data.presetEncoding || typeof data.max_width !== "number" || typeof data.max_height !== "number") {
244
+ throw new Error("Invalid config response from backend");
245
+ }
246
+ return mapPresetToConfig(data.presetEncoding, data.max_width, data.max_height);
247
+ }
248
+ }
249
+
250
+ // src/core/config/config-manager.ts
251
+ class ConfigManager {
252
+ configService = null;
253
+ currentConfig = DEFAULT_TRANSCODE_CONFIG;
254
+ configFetchPromise = null;
255
+ initialize(apiKey, backendUrl) {
256
+ if (!apiKey) {
257
+ throw new Error("apiKey is required");
258
+ }
259
+ if (!backendUrl) {
260
+ throw new Error("backendUrl is required");
261
+ }
262
+ this.configService = new ConfigService({
263
+ apiKey,
264
+ backendUrl
265
+ });
266
+ this.fetchConfig();
267
+ }
268
+ async fetchConfig() {
269
+ if (!this.configService) {
270
+ return;
271
+ }
272
+ if (this.configFetchPromise) {
273
+ this.currentConfig = await this.configFetchPromise;
274
+ return;
275
+ }
276
+ this.configFetchPromise = this.configService.fetchConfig();
277
+ this.currentConfig = await this.configFetchPromise;
278
+ this.configFetchPromise = null;
279
+ }
280
+ async getConfig() {
281
+ if (this.configService && !this.configFetchPromise) {
282
+ await this.fetchConfig();
283
+ }
284
+ return this.currentConfig;
285
+ }
286
+ clearCache() {
287
+ if (!this.configService) {
288
+ throw new Error("ConfigService is not initialized");
289
+ }
290
+ this.configService.clearCache();
291
+ }
292
+ }
293
+ // src/core/device/device-manager.ts
294
+ class DeviceManager {
295
+ streamManager;
296
+ callbacks;
297
+ availableDevices = {
298
+ audioinput: [],
299
+ videoinput: []
300
+ };
301
+ selectedCameraDeviceId = null;
302
+ selectedMicDeviceId = null;
303
+ constructor(streamManager, callbacks) {
304
+ this.streamManager = streamManager;
305
+ this.callbacks = callbacks;
306
+ }
307
+ async getAvailableDevices() {
308
+ this.availableDevices = await this.streamManager.getAvailableDevices();
309
+ if (this.callbacks?.onDevicesChanged) {
310
+ this.callbacks.onDevicesChanged(this.availableDevices);
311
+ }
312
+ return this.availableDevices;
313
+ }
314
+ setCameraDevice(deviceId) {
315
+ this.selectedCameraDeviceId = deviceId;
316
+ this.streamManager.setVideoDevice(deviceId);
317
+ if (this.callbacks?.onDeviceSelected) {
318
+ this.callbacks.onDeviceSelected("camera", deviceId);
319
+ }
320
+ }
321
+ setMicDevice(deviceId) {
322
+ this.selectedMicDeviceId = deviceId;
323
+ this.streamManager.setAudioDevice(deviceId);
324
+ if (this.callbacks?.onDeviceSelected) {
325
+ this.callbacks.onDeviceSelected("mic", deviceId);
326
+ }
327
+ }
328
+ getSelectedCameraDeviceId() {
329
+ return this.selectedCameraDeviceId;
330
+ }
331
+ getSelectedMicDeviceId() {
332
+ return this.selectedMicDeviceId;
333
+ }
334
+ getAvailableDevicesList() {
335
+ return this.availableDevices;
336
+ }
337
+ }
338
+ // src/core/processor/processor.ts
339
+ import {
340
+ BlobSource,
341
+ BufferTarget,
342
+ Conversion,
343
+ FilePathSource,
344
+ Input,
345
+ MP4,
346
+ Mp4OutputFormat,
347
+ Output
348
+ } from "mediabunny";
349
+ function createSource(input) {
350
+ if (typeof input === "string") {
351
+ return new FilePathSource(input);
352
+ }
353
+ if (input instanceof Blob) {
354
+ return new BlobSource(input);
355
+ }
356
+ throw new Error("Invalid input type. Expected Blob, File, or file path string.");
357
+ }
358
+ function createConversionOptions(config) {
359
+ const video = {
360
+ width: config.width,
361
+ height: config.height,
362
+ fit: "contain",
363
+ frameRate: config.fps,
364
+ bitrate: config.bitrate,
365
+ forceTranscode: true
366
+ };
367
+ const audio = {
368
+ codec: config.audioCodec,
369
+ forceTranscode: true
370
+ };
371
+ return { video, audio };
372
+ }
373
+ function validateConversion(conversion) {
374
+ if (!conversion.isValid) {
375
+ const reasons = conversion.discardedTracks.map((track) => track.reason).join(", ");
376
+ throw new Error(`Conversion is invalid. Discarded tracks: ${reasons}`);
377
+ }
378
+ }
379
+ async function transcodeVideo(input, config = {}, onProgress) {
380
+ const finalConfig = {
381
+ ...DEFAULT_TRANSCODE_CONFIG,
382
+ ...config
383
+ };
384
+ const source = createSource(input);
385
+ const mediabunnyInput = new Input({
386
+ formats: [MP4],
387
+ source
388
+ });
389
+ const output = new Output({
390
+ format: new Mp4OutputFormat,
391
+ target: new BufferTarget
392
+ });
393
+ const conversion = await Conversion.init({
394
+ input: mediabunnyInput,
395
+ output,
396
+ ...createConversionOptions(finalConfig)
397
+ });
398
+ validateConversion(conversion);
399
+ if (onProgress) {
400
+ conversion.onProgress = onProgress;
401
+ }
402
+ await conversion.execute();
403
+ const buffer = output.target.buffer;
404
+ if (!buffer) {
405
+ throw new Error("Transcoding completed but no output buffer was generated");
406
+ }
407
+ return {
408
+ buffer,
409
+ blob: new Blob([buffer], { type: "video/mp4" })
410
+ };
411
+ }
412
+ // src/core/processor/stream-processor.ts
413
+ import { CanvasSource } from "mediabunny";
414
+
415
+ // src/core/processor/audio-track-manager.ts
416
+ import { AudioSample, AudioSampleSource } from "mediabunny";
417
+
418
+ class AudioTrackManager {
419
+ originalAudioTrack = null;
420
+ audioContext = null;
421
+ audioWorkletNode = null;
422
+ mediaStreamSource = null;
423
+ gainNode = null;
424
+ audioSource = null;
425
+ lastAudioTimestamp = 0;
426
+ isMuted = false;
427
+ isPaused = false;
428
+ onMuteStateChange;
429
+ async setupAudioTrack(stream, config) {
430
+ const audioTrack = stream.getAudioTracks()[0];
431
+ if (!audioTrack) {
432
+ return null;
433
+ }
434
+ this.originalAudioTrack = audioTrack;
435
+ this.lastAudioTimestamp = 0;
436
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
437
+ if (!AudioContextClass) {
438
+ throw new Error("AudioContext is not supported in this browser");
439
+ }
440
+ this.audioContext = new AudioContextClass;
441
+ if (config.audioBitrate === undefined || config.audioBitrate === null) {
442
+ throw new Error("audioBitrate is required in config");
443
+ }
444
+ const audioBitrate = config.audioBitrate;
445
+ const audioCodec = config.audioCodec;
446
+ this.audioSource = new AudioSampleSource({
447
+ codec: audioCodec,
448
+ bitrate: audioBitrate
449
+ });
450
+ this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
451
+ const processorUrl = new URL("./audio-capture-processor.worklet.js", import.meta.url).href;
452
+ await this.audioContext.audioWorklet.addModule(processorUrl);
453
+ this.audioWorkletNode = new AudioWorkletNode(this.audioContext, "audio-capture-processor");
454
+ this.audioWorkletNode.port.onmessage = (event) => {
455
+ if (this.isPaused || this.isMuted || !this.audioSource) {
456
+ return;
457
+ }
458
+ const { data, sampleRate, numberOfChannels, duration, bufferLength } = event.data;
459
+ const float32Data = new Float32Array(data, 0, bufferLength);
460
+ const audioSample = new AudioSample({
461
+ data: float32Data,
462
+ format: "f32-planar",
463
+ numberOfChannels,
464
+ sampleRate,
465
+ timestamp: this.lastAudioTimestamp
466
+ });
467
+ this.audioSource.add(audioSample);
468
+ this.lastAudioTimestamp += duration;
469
+ };
470
+ this.gainNode = this.audioContext.createGain();
471
+ this.gainNode.gain.value = 0;
472
+ this.mediaStreamSource.connect(this.audioWorkletNode);
473
+ this.audioWorkletNode.connect(this.gainNode);
474
+ this.gainNode.connect(this.audioContext.destination);
475
+ return this.audioSource;
476
+ }
477
+ toggleMute() {
478
+ if (!this.audioSource) {
479
+ throw new Error("Audio source not initialized");
480
+ }
481
+ this.isMuted = !this.isMuted;
482
+ if (this.onMuteStateChange) {
483
+ this.onMuteStateChange(this.isMuted);
484
+ }
485
+ }
486
+ isMutedState() {
487
+ return this.isMuted;
488
+ }
489
+ pause() {
490
+ if (!this.audioSource || this.isPaused) {
491
+ return;
492
+ }
493
+ this.isPaused = true;
494
+ }
495
+ resume() {
496
+ if (!(this.isPaused && this.audioSource)) {
497
+ return null;
498
+ }
499
+ this.isPaused = false;
500
+ return null;
501
+ }
502
+ isPausedState() {
503
+ return this.isPaused;
504
+ }
505
+ getClonedAudioTrack() {
506
+ return this.originalAudioTrack;
507
+ }
508
+ getAudioSource() {
509
+ return this.audioSource;
510
+ }
511
+ getAudioStreamForAnalysis() {
512
+ if (!this.originalAudioTrack) {
513
+ return null;
514
+ }
515
+ if (this.originalAudioTrack.readyState !== "live") {
516
+ return null;
517
+ }
518
+ return new MediaStream([this.originalAudioTrack]);
519
+ }
520
+ getLastAudioTimestamp() {
521
+ return this.lastAudioTimestamp;
522
+ }
523
+ setOnMuteStateChange(callback) {
524
+ this.onMuteStateChange = callback;
525
+ }
526
+ cleanup() {
527
+ if (this.audioWorkletNode) {
528
+ this.audioWorkletNode.disconnect();
529
+ this.audioWorkletNode.port.close();
530
+ this.audioWorkletNode = null;
531
+ }
532
+ if (this.gainNode) {
533
+ this.gainNode.disconnect();
534
+ this.gainNode = null;
535
+ }
536
+ if (this.mediaStreamSource) {
537
+ this.mediaStreamSource.disconnect();
538
+ this.mediaStreamSource = null;
539
+ }
540
+ if (this.audioContext) {
541
+ this.audioContext.close();
542
+ this.audioContext = null;
543
+ }
544
+ if (this.originalAudioTrack) {
545
+ this.originalAudioTrack.stop();
546
+ this.originalAudioTrack = null;
547
+ }
548
+ this.audioSource = null;
549
+ this.lastAudioTimestamp = 0;
550
+ }
551
+ }
552
+
553
+ // src/core/processor/canvas-renderer.ts
554
+ var BLACK_BACKGROUND = "#000000";
555
+
556
+ class CanvasRenderer {
557
+ canvasContext;
558
+ constructor(canvasContext) {
559
+ this.canvasContext = canvasContext;
560
+ }
561
+ clear() {
562
+ this.canvasContext.clearRect(0, 0, this.canvasContext.canvas.width, this.canvasContext.canvas.height);
563
+ }
564
+ drawFrame(videoElement) {
565
+ if (videoElement.videoWidth === 0 || videoElement.videoHeight === 0) {
566
+ return;
567
+ }
568
+ const videoAspectRatio = videoElement.videoWidth / videoElement.videoHeight;
569
+ const canvasAspectRatio = this.canvasContext.canvas.width / this.canvasContext.canvas.height;
570
+ let drawWidth;
571
+ let drawHeight;
572
+ let drawX;
573
+ let drawY;
574
+ if (videoAspectRatio > canvasAspectRatio) {
575
+ drawWidth = this.canvasContext.canvas.width;
576
+ drawHeight = this.canvasContext.canvas.width / videoAspectRatio;
577
+ drawX = 0;
578
+ drawY = (this.canvasContext.canvas.height - drawHeight) / 2;
579
+ } else {
580
+ drawHeight = this.canvasContext.canvas.height;
581
+ drawWidth = this.canvasContext.canvas.height * videoAspectRatio;
582
+ drawX = (this.canvasContext.canvas.width - drawWidth) / 2;
583
+ drawY = 0;
584
+ }
585
+ this.canvasContext.fillStyle = BLACK_BACKGROUND;
586
+ this.canvasContext.fillRect(0, 0, this.canvasContext.canvas.width, this.canvasContext.canvas.height);
587
+ this.canvasContext.drawImage(videoElement, drawX, drawY, drawWidth, drawHeight);
588
+ }
589
+ getContext() {
590
+ return this.canvasContext;
591
+ }
592
+ }
593
+
594
+ // src/core/processor/frame-capturer.ts
595
+ var READY_STATE_HAVE_CURRENT_DATA = 2;
596
+ var DEFAULT_FRAME_RATE = 30;
597
+
598
+ class FrameCapturer {
599
+ timeoutId = null;
600
+ isActive = false;
601
+ isPaused = false;
602
+ frameCount = 0;
603
+ lastFrameTimestamp = 0;
604
+ currentFrameRate = DEFAULT_FRAME_RATE;
605
+ canvasSource = null;
606
+ canvasRenderer = null;
607
+ videoElement = null;
608
+ pendingFrameAdd = null;
609
+ start(canvasSource, canvasRenderer, videoElement, frameRate) {
610
+ this.canvasSource = canvasSource;
611
+ this.canvasRenderer = canvasRenderer;
612
+ this.videoElement = videoElement;
613
+ this.isActive = true;
614
+ this.isPaused = false;
615
+ this.frameCount = 0;
616
+ this.lastFrameTimestamp = 0;
617
+ this.currentFrameRate = frameRate;
618
+ this.captureFrame();
619
+ }
620
+ pause() {
621
+ if (!this.isActive || this.isPaused) {
622
+ return;
623
+ }
624
+ this.isPaused = true;
625
+ if (this.timeoutId !== null) {
626
+ window.clearTimeout(this.timeoutId);
627
+ this.timeoutId = null;
628
+ }
629
+ }
630
+ resume() {
631
+ if (!(this.isActive && this.isPaused)) {
632
+ return;
633
+ }
634
+ if (!(this.videoElement && this.canvasSource)) {
635
+ throw new Error("Frame capturer not properly initialized");
636
+ }
637
+ this.isPaused = false;
638
+ this.captureFrame();
639
+ }
640
+ isPausedState() {
641
+ return this.isPaused;
642
+ }
643
+ getLastFrameTimestamp() {
644
+ return this.lastFrameTimestamp;
645
+ }
646
+ async waitForPendingFrames() {
647
+ if (this.pendingFrameAdd) {
648
+ await this.pendingFrameAdd;
649
+ }
650
+ }
651
+ stop() {
652
+ this.isActive = false;
653
+ if (this.timeoutId !== null) {
654
+ window.clearTimeout(this.timeoutId);
655
+ this.timeoutId = null;
656
+ }
657
+ }
658
+ captureFrame() {
659
+ if (!this.canCaptureFrame()) {
660
+ return;
661
+ }
662
+ if (!this.isVideoReady()) {
663
+ this.scheduleNextFrame();
664
+ return;
665
+ }
666
+ if (this.isPaused) {
667
+ return;
668
+ }
669
+ this.frameCount += 1;
670
+ const frameDuration = 1 / this.currentFrameRate;
671
+ const frameTimestamp = this.lastFrameTimestamp + frameDuration;
672
+ if (this.isPaused) {
673
+ this.frameCount -= 1;
674
+ return;
675
+ }
676
+ this.renderFrame();
677
+ this.addFrameToSource(frameTimestamp, frameDuration);
678
+ if (this.isPaused) {
679
+ this.frameCount -= 1;
680
+ return;
681
+ }
682
+ this.lastFrameTimestamp = frameTimestamp;
683
+ this.scheduleNextFrame();
684
+ }
685
+ canCaptureFrame() {
686
+ if (!this.isActive || this.isPaused || !this.videoElement || !this.canvasSource || !this.canvasRenderer) {
687
+ return false;
688
+ }
689
+ return true;
690
+ }
691
+ isVideoReady() {
692
+ if (!this.videoElement) {
693
+ return false;
694
+ }
695
+ if (this.videoElement.readyState < READY_STATE_HAVE_CURRENT_DATA) {
696
+ return false;
697
+ }
698
+ if (this.videoElement.videoWidth === 0 || this.videoElement.videoHeight === 0) {
699
+ return false;
700
+ }
701
+ return true;
702
+ }
703
+ renderFrame() {
704
+ if (!(this.canvasRenderer && this.videoElement)) {
705
+ return;
706
+ }
707
+ this.canvasRenderer.clear();
708
+ this.canvasRenderer.drawFrame(this.videoElement);
709
+ }
710
+ addFrameToSource(timestamp, duration) {
711
+ if (!this.canvasSource || this.isPaused) {
712
+ return;
713
+ }
714
+ const isKeyFrame = this.frameCount === 1;
715
+ this.pendingFrameAdd = this.canvasSource.add(timestamp, duration, isKeyFrame ? { keyFrame: true } : undefined).then(() => {
716
+ this.pendingFrameAdd = null;
717
+ }).catch(() => {
718
+ this.pendingFrameAdd = null;
719
+ });
720
+ }
721
+ scheduleNextFrame() {
722
+ if (this.isPaused) {
723
+ return;
724
+ }
725
+ const frameInterval = 1000 / this.currentFrameRate;
726
+ this.timeoutId = window.setTimeout(() => {
727
+ this.captureFrame();
728
+ }, frameInterval);
729
+ }
730
+ }
731
+
732
+ // src/core/processor/output-manager.ts
733
+ import {
734
+ Mp4OutputFormat as Mp4OutputFormat2,
735
+ Output as Output2,
736
+ StreamTarget
737
+ } from "mediabunny";
738
+ var CHUNK_SIZE = 16 * 1024 * 1024;
739
+
740
+ class OutputManager {
741
+ output = null;
742
+ chunks = [];
743
+ totalSize = 0;
744
+ create() {
745
+ const writable = new WritableStream({
746
+ write: (chunk) => {
747
+ this.chunks.push({
748
+ data: chunk.data,
749
+ position: chunk.position
750
+ });
751
+ this.totalSize = Math.max(this.totalSize, chunk.position + chunk.data.length);
752
+ }
753
+ });
754
+ this.output = new Output2({
755
+ format: new Mp4OutputFormat2({
756
+ fastStart: "fragmented"
757
+ }),
758
+ target: new StreamTarget(writable, {
759
+ chunked: true,
760
+ chunkSize: CHUNK_SIZE
761
+ })
762
+ });
763
+ return this.output;
764
+ }
765
+ getOutput() {
766
+ if (!this.output) {
767
+ throw new Error("Output not initialized");
768
+ }
769
+ return this.output;
770
+ }
771
+ getChunks() {
772
+ return this.chunks;
773
+ }
774
+ async finalize() {
775
+ if (!this.output) {
776
+ throw new Error("Output not initialized");
777
+ }
778
+ await this.output.finalize();
779
+ const sortedChunks = [...this.chunks].sort((a, b) => a.position - b.position);
780
+ const buffer = new ArrayBuffer(this.totalSize);
781
+ const view = new Uint8Array(buffer);
782
+ for (const chunk of sortedChunks) {
783
+ view.set(chunk.data, chunk.position);
784
+ }
785
+ const blob = new Blob([buffer], { type: "video/mp4" });
786
+ return {
787
+ blob,
788
+ totalSize: this.totalSize
789
+ };
790
+ }
791
+ async cancel() {
792
+ if (this.output) {
793
+ await this.output.cancel();
794
+ }
795
+ }
796
+ getTotalSize() {
797
+ return this.totalSize;
798
+ }
799
+ }
800
+
801
+ // src/core/processor/video-element-manager.ts
802
+ var READY_STATE_HAVE_CURRENT_DATA2 = 2;
803
+ var VIDEO_LOAD_TIMEOUT = 5000;
804
+
805
+ class VideoElementManager {
806
+ videoElement = null;
807
+ isActive = false;
808
+ isIntentionallyPaused = false;
809
+ create(stream) {
810
+ const videoElement = document.createElement("video");
811
+ videoElement.srcObject = stream;
812
+ videoElement.autoplay = true;
813
+ videoElement.playsInline = true;
814
+ videoElement.muted = true;
815
+ videoElement.addEventListener("pause", () => {
816
+ if (this.isActive && this.videoElement && !this.isIntentionallyPaused) {
817
+ this.videoElement.play();
818
+ }
819
+ });
820
+ this.videoElement = videoElement;
821
+ return videoElement;
822
+ }
823
+ async waitForReady() {
824
+ if (!this.videoElement) {
825
+ throw new Error("Video element not created");
826
+ }
827
+ await this.waitForVideoReady(true);
828
+ }
829
+ async switchSource(newStream) {
830
+ if (!this.videoElement) {
831
+ throw new Error("Video element not initialized");
832
+ }
833
+ this.videoElement.srcObject = newStream;
834
+ await this.waitForVideoReady(false);
835
+ await this.videoElement.play();
836
+ }
837
+ getElement() {
838
+ return this.videoElement;
839
+ }
840
+ setActive(active) {
841
+ this.isActive = active;
842
+ }
843
+ pause() {
844
+ if (!(this.videoElement && this.isActive)) {
845
+ return;
846
+ }
847
+ this.isIntentionallyPaused = true;
848
+ this.videoElement.pause();
849
+ }
850
+ resume() {
851
+ if (!(this.videoElement && this.isActive)) {
852
+ return;
853
+ }
854
+ this.isIntentionallyPaused = false;
855
+ this.videoElement.play();
856
+ }
857
+ cleanup() {
858
+ if (this.videoElement) {
859
+ this.videoElement.srcObject = null;
860
+ this.videoElement = null;
861
+ }
862
+ }
863
+ waitForVideoReady(shouldPlay) {
864
+ if (!this.videoElement) {
865
+ throw new Error("Video element not available");
866
+ }
867
+ const videoElement = this.videoElement;
868
+ return new Promise((resolve, reject) => {
869
+ const cleanup = () => {
870
+ videoElement.removeEventListener("loadedmetadata", onLoadedMetadata);
871
+ videoElement.removeEventListener("error", onError);
872
+ };
873
+ const onLoadedMetadata = () => {
874
+ cleanup();
875
+ if (shouldPlay) {
876
+ videoElement.play().then(resolve).catch(reject);
877
+ } else {
878
+ resolve();
879
+ }
880
+ };
881
+ const onError = () => {
882
+ cleanup();
883
+ reject(new Error("Failed to load video metadata"));
884
+ };
885
+ if (videoElement.readyState >= READY_STATE_HAVE_CURRENT_DATA2) {
886
+ if (shouldPlay) {
887
+ videoElement.play().then(resolve).catch(reject);
888
+ } else {
889
+ resolve();
890
+ }
891
+ return;
892
+ }
893
+ videoElement.addEventListener("loadedmetadata", onLoadedMetadata);
894
+ videoElement.addEventListener("error", onError);
895
+ if (!shouldPlay) {
896
+ videoElement.play().catch(reject);
897
+ }
898
+ setTimeout(() => {
899
+ cleanup();
900
+ reject(new Error("Timeout waiting for video to load"));
901
+ }, VIDEO_LOAD_TIMEOUT);
902
+ });
903
+ }
904
+ }
905
+
906
+ // src/core/processor/stream-processor.ts
907
+ var KEY_FRAME_INTERVAL = 5;
908
+ var H264_CODEC = "avc";
909
+ var REALTIME_LATENCY = "realtime";
910
+
911
+ class StreamProcessor {
912
+ outputManager;
913
+ canvasSource = null;
914
+ offscreenCanvas = null;
915
+ videoElementManager;
916
+ canvasRenderer = null;
917
+ frameCapturer;
918
+ audioTrackManager;
919
+ currentVideoStream = null;
920
+ onSourceChange;
921
+ constructor() {
922
+ this.outputManager = new OutputManager;
923
+ this.videoElementManager = new VideoElementManager;
924
+ this.frameCapturer = new FrameCapturer;
925
+ this.audioTrackManager = new AudioTrackManager;
926
+ }
927
+ async startProcessing(stream, config) {
928
+ this.offscreenCanvas = new OffscreenCanvas(config.width, config.height);
929
+ const ctx = this.offscreenCanvas.getContext("2d", {
930
+ alpha: false,
931
+ desynchronized: true,
932
+ willReadFrequently: false
933
+ });
934
+ if (!ctx) {
935
+ throw new Error("Failed to get OffscreenCanvas context");
936
+ }
937
+ this.canvasRenderer = new CanvasRenderer(ctx);
938
+ this.videoElementManager.create(stream);
939
+ this.videoElementManager.setActive(true);
940
+ await this.videoElementManager.waitForReady();
941
+ this.currentVideoStream = stream;
942
+ const output = this.outputManager.create();
943
+ if (typeof config.fps !== "number" || config.fps <= 0) {
944
+ throw new Error("fps must be a positive number");
945
+ }
946
+ const frameRate = config.fps;
947
+ if (!this.offscreenCanvas) {
948
+ throw new Error("OffscreenCanvas not initialized");
949
+ }
950
+ if (!this.offscreenCanvas) {
951
+ throw new Error("Cannot create CanvasSource: not initialized");
952
+ }
953
+ this.canvasSource = new CanvasSource(this.offscreenCanvas, {
954
+ codec: H264_CODEC,
955
+ bitrate: config.bitrate,
956
+ keyFrameInterval: KEY_FRAME_INTERVAL,
957
+ latencyMode: REALTIME_LATENCY
958
+ });
959
+ output.addVideoTrack(this.canvasSource);
960
+ const audioSource = await this.audioTrackManager.setupAudioTrack(stream, config);
961
+ if (audioSource) {
962
+ output.addAudioTrack(audioSource);
963
+ }
964
+ await output.start();
965
+ if (!this.canvasRenderer) {
966
+ throw new Error("CanvasRenderer not initialized");
967
+ }
968
+ const videoEl = this.videoElementManager.getElement();
969
+ if (!videoEl) {
970
+ throw new Error("Video element not available");
971
+ }
972
+ if (!this.canvasSource) {
973
+ throw new Error("CanvasSource not initialized");
974
+ }
975
+ this.frameCapturer.start(this.canvasSource, this.canvasRenderer, videoEl, frameRate);
976
+ }
977
+ pause() {
978
+ this.frameCapturer.pause();
979
+ this.audioTrackManager.pause();
980
+ this.videoElementManager.pause();
981
+ }
982
+ resume() {
983
+ if (!this.canvasSource) {
984
+ throw new Error("CanvasSource not initialized - cannot resume");
985
+ }
986
+ this.frameCapturer.resume();
987
+ this.audioTrackManager.resume();
988
+ this.videoElementManager.resume();
989
+ }
990
+ isPausedState() {
991
+ return this.frameCapturer.isPausedState();
992
+ }
993
+ async finalize() {
994
+ this.frameCapturer.stop();
995
+ await this.frameCapturer.waitForPendingFrames();
996
+ this.videoElementManager.cleanup();
997
+ this.audioTrackManager.cleanup();
998
+ if (this.canvasSource) {
999
+ this.canvasSource.close();
1000
+ this.canvasSource = null;
1001
+ }
1002
+ return await this.outputManager.finalize();
1003
+ }
1004
+ toggleMute() {
1005
+ this.audioTrackManager.toggleMute();
1006
+ }
1007
+ isMutedState() {
1008
+ return this.audioTrackManager.isMutedState();
1009
+ }
1010
+ getClonedAudioTrack() {
1011
+ return this.audioTrackManager.getClonedAudioTrack();
1012
+ }
1013
+ getAudioStreamForAnalysis() {
1014
+ return this.audioTrackManager.getAudioStreamForAnalysis();
1015
+ }
1016
+ async switchVideoSource(newStream) {
1017
+ await this.videoElementManager.switchSource(newStream);
1018
+ this.currentVideoStream = newStream;
1019
+ if (this.onSourceChange) {
1020
+ this.onSourceChange(newStream);
1021
+ }
1022
+ }
1023
+ getCurrentVideoSource() {
1024
+ return this.currentVideoStream;
1025
+ }
1026
+ getBufferSize() {
1027
+ return this.outputManager.getTotalSize();
1028
+ }
1029
+ setOnMuteStateChange(callback) {
1030
+ this.audioTrackManager.setOnMuteStateChange(callback);
1031
+ }
1032
+ setOnSourceChange(callback) {
1033
+ this.onSourceChange = callback;
1034
+ }
1035
+ async cancel() {
1036
+ this.frameCapturer.stop();
1037
+ await this.outputManager.cancel();
1038
+ this.videoElementManager.cleanup();
1039
+ this.audioTrackManager.cleanup();
1040
+ if (this.canvasSource) {
1041
+ this.canvasSource.close();
1042
+ this.canvasSource = null;
1043
+ }
1044
+ }
1045
+ }
1046
+ // src/core/utils/formatters.ts
1047
+ var FILE_SIZE_UNITS = ["Bytes", "KB", "MB", "GB"];
1048
+ var FILE_SIZE_BASE = 1024;
1049
+ function formatFileSize(bytes) {
1050
+ if (bytes === 0) {
1051
+ return "0 Bytes";
1052
+ }
1053
+ const index = Math.floor(Math.log(bytes) / Math.log(FILE_SIZE_BASE));
1054
+ const size = Math.round(bytes / FILE_SIZE_BASE ** index * 100) / 100;
1055
+ return `${size} ${FILE_SIZE_UNITS[index]}`;
1056
+ }
1057
+ function formatTime(totalSeconds) {
1058
+ const hours = Math.floor(totalSeconds / 3600);
1059
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1060
+ const secs = totalSeconds % 60;
1061
+ if (hours > 0) {
1062
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
1063
+ }
1064
+ return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
1065
+ }
1066
+
1067
+ // src/core/recording/recording-manager.ts
1068
+ var DEFAULT_COUNTDOWN_DURATION = 5000;
1069
+ var MILLISECONDS_PER_SECOND = 1000;
1070
+ var COUNTDOWN_UPDATE_INTERVAL = 100;
1071
+ var RECORDING_TIMER_INTERVAL = 1000;
1072
+ var RECORDING_STATE_RECORDING = "recording";
1073
+ var RECORDING_STATE_IDLE = "idle";
1074
+ var RECORDING_STATE_COUNTDOWN = "countdown";
1075
+
1076
+ class RecordingManager {
1077
+ recordingState = RECORDING_STATE_IDLE;
1078
+ countdownDuration = DEFAULT_COUNTDOWN_DURATION;
1079
+ countdownRemaining = 0;
1080
+ countdownTimeoutId = null;
1081
+ countdownIntervalId = null;
1082
+ countdownStartTime = null;
1083
+ isPaused = false;
1084
+ maxRecordingTime = null;
1085
+ maxTimeTimer = null;
1086
+ recordingSeconds = 0;
1087
+ recordingIntervalId = null;
1088
+ pauseStartTime = null;
1089
+ totalPausedTime = 0;
1090
+ streamManager;
1091
+ callbacks;
1092
+ streamProcessor = null;
1093
+ originalCameraStream = null;
1094
+ constructor(streamManager, callbacks) {
1095
+ this.streamManager = streamManager;
1096
+ this.callbacks = callbacks;
1097
+ }
1098
+ setCountdownDuration(duration) {
1099
+ this.countdownDuration = duration;
1100
+ }
1101
+ setMaxRecordingTime(maxTime) {
1102
+ this.maxRecordingTime = maxTime;
1103
+ }
1104
+ getRecordingState() {
1105
+ return this.recordingState;
1106
+ }
1107
+ isPausedState() {
1108
+ return this.isPaused;
1109
+ }
1110
+ getRecordingSeconds() {
1111
+ return this.recordingSeconds;
1112
+ }
1113
+ getStreamProcessor() {
1114
+ return this.streamProcessor;
1115
+ }
1116
+ setOriginalCameraStream(stream) {
1117
+ this.originalCameraStream = stream;
1118
+ }
1119
+ getOriginalCameraStream() {
1120
+ return this.originalCameraStream;
1121
+ }
1122
+ async startRecording() {
1123
+ try {
1124
+ this.callbacks.onClearUploadStatus();
1125
+ if (this.countdownDuration > 0) {
1126
+ this.startCountdown();
1127
+ } else {
1128
+ await this.doStartRecording();
1129
+ }
1130
+ } catch (error) {
1131
+ this.handleError(error);
1132
+ this.recordingState = RECORDING_STATE_IDLE;
1133
+ this.cancelCountdown();
1134
+ }
1135
+ }
1136
+ startCountdown() {
1137
+ this.recordingState = RECORDING_STATE_COUNTDOWN;
1138
+ this.countdownRemaining = Math.ceil(this.countdownDuration / MILLISECONDS_PER_SECOND);
1139
+ this.countdownStartTime = Date.now();
1140
+ this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
1141
+ this.countdownIntervalId = window.setInterval(() => {
1142
+ if (!this.countdownStartTime) {
1143
+ return;
1144
+ }
1145
+ const elapsed = Date.now() - this.countdownStartTime;
1146
+ const remaining = Math.max(0, Math.ceil((this.countdownDuration - elapsed) / MILLISECONDS_PER_SECOND));
1147
+ this.countdownRemaining = remaining;
1148
+ this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
1149
+ }, COUNTDOWN_UPDATE_INTERVAL);
1150
+ this.countdownTimeoutId = window.setTimeout(async () => {
1151
+ await this.doStartRecording();
1152
+ }, this.countdownDuration);
1153
+ }
1154
+ async doStartRecording() {
1155
+ try {
1156
+ this.cancelCountdown();
1157
+ this.recordingState = RECORDING_STATE_RECORDING;
1158
+ this.callbacks.onStateChange(this.recordingState);
1159
+ this.resetRecordingState();
1160
+ const currentStream = this.streamManager.getStream();
1161
+ if (!currentStream) {
1162
+ throw new Error("No stream available for recording");
1163
+ }
1164
+ this.originalCameraStream = currentStream;
1165
+ this.streamProcessor = new StreamProcessor;
1166
+ const config = await this.callbacks.onGetConfig();
1167
+ await this.streamManager.startRecording(this.streamProcessor, config);
1168
+ this.startRecordingTimer();
1169
+ if (this.maxRecordingTime && this.maxRecordingTime > 0) {
1170
+ this.maxTimeTimer = window.setTimeout(async () => {
1171
+ if (this.recordingState === RECORDING_STATE_RECORDING) {
1172
+ await this.stopRecording();
1173
+ }
1174
+ }, this.maxRecordingTime);
1175
+ }
1176
+ } catch (error) {
1177
+ this.handleError(error);
1178
+ this.recordingState = RECORDING_STATE_IDLE;
1179
+ this.callbacks.onStateChange(this.recordingState);
1180
+ }
1181
+ }
1182
+ async stopRecording() {
1183
+ try {
1184
+ this.cancelCountdown();
1185
+ this.clearTimer(this.recordingIntervalId, clearInterval);
1186
+ this.recordingIntervalId = null;
1187
+ this.clearTimer(this.maxTimeTimer, clearTimeout);
1188
+ this.maxTimeTimer = null;
1189
+ this.resetPauseState();
1190
+ this.callbacks.onStopAudioTracking();
1191
+ const blob = await this.streamManager.stopRecording();
1192
+ this.recordingState = RECORDING_STATE_IDLE;
1193
+ this.callbacks.onStateChange(this.recordingState);
1194
+ this.recordingSeconds = 0;
1195
+ this.streamProcessor = null;
1196
+ this.callbacks.onRecordingComplete(blob);
1197
+ return blob;
1198
+ } catch (error) {
1199
+ this.handleError(error);
1200
+ this.recordingState = RECORDING_STATE_IDLE;
1201
+ this.callbacks.onStateChange(this.recordingState);
1202
+ throw error;
1203
+ }
1204
+ }
1205
+ pauseRecording() {
1206
+ if (this.recordingState !== RECORDING_STATE_RECORDING || this.isPaused) {
1207
+ return;
1208
+ }
1209
+ this.streamManager.pauseRecording();
1210
+ this.isPaused = true;
1211
+ this.clearTimer(this.recordingIntervalId, clearInterval);
1212
+ this.recordingIntervalId = null;
1213
+ this.pauseStartTime = Date.now();
1214
+ }
1215
+ resumeRecording() {
1216
+ if (this.recordingState !== RECORDING_STATE_RECORDING || !this.isPaused) {
1217
+ return;
1218
+ }
1219
+ this.streamManager.resumeRecording();
1220
+ this.isPaused = false;
1221
+ this.updatePausedDuration();
1222
+ this.startRecordingTimer();
1223
+ }
1224
+ cancelCountdown() {
1225
+ this.clearTimer(this.countdownTimeoutId, clearTimeout);
1226
+ this.countdownTimeoutId = null;
1227
+ this.clearTimer(this.countdownIntervalId, clearInterval);
1228
+ this.countdownIntervalId = null;
1229
+ this.recordingState = RECORDING_STATE_IDLE;
1230
+ this.countdownRemaining = 0;
1231
+ this.countdownStartTime = null;
1232
+ this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
1233
+ }
1234
+ cleanup() {
1235
+ this.cancelCountdown();
1236
+ this.clearTimer(this.recordingIntervalId, clearInterval);
1237
+ this.recordingIntervalId = null;
1238
+ this.clearTimer(this.maxTimeTimer, clearTimeout);
1239
+ this.maxTimeTimer = null;
1240
+ }
1241
+ resetRecordingState() {
1242
+ this.isPaused = false;
1243
+ this.recordingSeconds = 0;
1244
+ this.totalPausedTime = 0;
1245
+ this.pauseStartTime = null;
1246
+ }
1247
+ resetPauseState() {
1248
+ this.isPaused = false;
1249
+ this.pauseStartTime = null;
1250
+ this.totalPausedTime = 0;
1251
+ }
1252
+ updatePausedDuration() {
1253
+ if (this.pauseStartTime === null) {
1254
+ throw new Error("Pause start time not set");
1255
+ }
1256
+ const pausedDuration = Date.now() - this.pauseStartTime;
1257
+ this.totalPausedTime += pausedDuration;
1258
+ this.pauseStartTime = null;
1259
+ }
1260
+ startRecordingTimer() {
1261
+ if (this.recordingIntervalId !== null) {
1262
+ return;
1263
+ }
1264
+ this.recordingIntervalId = window.setInterval(() => {
1265
+ this.recordingSeconds += 1;
1266
+ this.callbacks.onTimerUpdate(formatTime(this.recordingSeconds));
1267
+ }, RECORDING_TIMER_INTERVAL);
1268
+ }
1269
+ clearTimer(timerId, clearFn) {
1270
+ if (timerId !== null) {
1271
+ clearFn(timerId);
1272
+ }
1273
+ }
1274
+ handleError(error) {
1275
+ const errorMessage = error instanceof Error ? error : new Error(extractErrorMessage(error));
1276
+ this.callbacks.onError(errorMessage);
1277
+ }
1278
+ }
1279
+
1280
+ // src/core/upload/upload-queue-manager.ts
1281
+ var MAX_RETRIES = 10;
1282
+ var INITIAL_RETRY_DELAY = 2000;
1283
+ var MAX_RETRY_DELAY = 300000;
1284
+ var RETRY_MULTIPLIER = 1.5;
1285
+ var PROCESSING_INTERVAL = 5000;
1286
+
1287
+ class UploadQueueManager {
1288
+ storageService;
1289
+ uploadService;
1290
+ processingIntervalId;
1291
+ networkOnlineHandler;
1292
+ isProcessing = false;
1293
+ retryTimeoutId = null;
1294
+ callbacks = {};
1295
+ constructor(storageService, uploadService) {
1296
+ this.storageService = storageService;
1297
+ this.uploadService = uploadService;
1298
+ this.networkOnlineHandler = () => {
1299
+ this.processQueue().catch((error) => {
1300
+ const errorMessage = extractErrorMessage(error);
1301
+ this.callbacks.onUploadError?.("network-recovery", new Error(errorMessage));
1302
+ });
1303
+ };
1304
+ window.addEventListener("online", this.networkOnlineHandler);
1305
+ this.processingIntervalId = window.setInterval(() => {
1306
+ this.processQueue().catch((error) => {
1307
+ const errorMessage = extractErrorMessage(error);
1308
+ this.callbacks.onUploadError?.("processing-loop", new Error(errorMessage));
1309
+ });
1310
+ }, PROCESSING_INTERVAL);
1311
+ }
1312
+ destroy() {
1313
+ this.clearTimer(this.processingIntervalId, clearInterval);
1314
+ this.clearTimer(this.retryTimeoutId, clearTimeout);
1315
+ window.removeEventListener("online", this.networkOnlineHandler);
1316
+ }
1317
+ setCallbacks(callbacks) {
1318
+ this.callbacks = callbacks;
1319
+ }
1320
+ async queueUpload(upload) {
1321
+ const id = await this.storageService.savePendingUpload(upload);
1322
+ this.processQueue();
1323
+ return id;
1324
+ }
1325
+ async processQueue() {
1326
+ if (this.isProcessing) {
1327
+ return;
1328
+ }
1329
+ this.isProcessing = true;
1330
+ try {
1331
+ if (!this.storageService.isInitialized()) {
1332
+ throw new Error("Database not initialized");
1333
+ }
1334
+ const pendingUploads = await this.storageService.getPendingUploads("pending");
1335
+ if (pendingUploads.length > 0) {
1336
+ const upload = this.getOldestUpload(pendingUploads);
1337
+ await this.processUpload(upload);
1338
+ this.isProcessing = false;
1339
+ return;
1340
+ }
1341
+ const failedUploads = await this.storageService.getPendingUploads("failed");
1342
+ const retryableUploads = failedUploads.filter((upload) => upload.retryCount < MAX_RETRIES);
1343
+ if (retryableUploads.length > 0) {
1344
+ const upload = this.getOldestFailedUpload(retryableUploads);
1345
+ const delay = this.calculateRetryDelay(upload.retryCount);
1346
+ const timeSinceLastAttempt = Date.now() - upload.updatedAt;
1347
+ if (timeSinceLastAttempt >= delay) {
1348
+ await this.storageService.updateUploadStatus(upload.id, {
1349
+ status: "pending",
1350
+ retryCount: upload.retryCount
1351
+ });
1352
+ await this.processUpload(upload);
1353
+ } else {
1354
+ const remainingDelay = delay - timeSinceLastAttempt;
1355
+ this.scheduleRetry(remainingDelay);
1356
+ }
1357
+ }
1358
+ this.isProcessing = false;
1359
+ } catch (error) {
1360
+ this.isProcessing = false;
1361
+ throw new Error(`Error processing upload queue: ${extractErrorMessage(error)}`);
1362
+ }
1363
+ }
1364
+ getPendingUploads() {
1365
+ return this.storageService.getPendingUploads();
1366
+ }
1367
+ async getStats() {
1368
+ const all = await this.storageService.getPendingUploads();
1369
+ const stats = {
1370
+ pending: 0,
1371
+ uploading: 0,
1372
+ failed: 0,
1373
+ total: all.length
1374
+ };
1375
+ for (const upload of all) {
1376
+ if (upload.status === "pending") {
1377
+ stats.pending += 1;
1378
+ } else if (upload.status === "uploading") {
1379
+ stats.uploading += 1;
1380
+ } else if (upload.status === "failed") {
1381
+ stats.failed += 1;
1382
+ }
1383
+ }
1384
+ return stats;
1385
+ }
1386
+ getOldestUpload(uploads) {
1387
+ if (uploads.length === 0) {
1388
+ throw new Error("Cannot get oldest upload from empty array");
1389
+ }
1390
+ return uploads.sort((a, b) => a.createdAt - b.createdAt)[0];
1391
+ }
1392
+ getOldestFailedUpload(uploads) {
1393
+ if (uploads.length === 0) {
1394
+ throw new Error("Cannot get oldest failed upload from empty array");
1395
+ }
1396
+ return uploads.sort((a, b) => a.updatedAt - b.updatedAt)[0];
1397
+ }
1398
+ async processUpload(upload) {
1399
+ try {
1400
+ await this.storageService.updateUploadStatus(upload.id, {
1401
+ status: "uploading"
1402
+ });
1403
+ const result = await this.uploadService.uploadVideo(upload.blob, {
1404
+ apiKey: upload.apiKey,
1405
+ backendUrl: upload.backendUrl,
1406
+ filename: upload.filename,
1407
+ duration: upload.duration,
1408
+ metadata: upload.metadata,
1409
+ userMetadata: upload.userMetadata,
1410
+ onProgress: (progress) => {
1411
+ this.callbacks.onUploadProgress?.(upload.id, progress);
1412
+ }
1413
+ });
1414
+ await this.storageService.deleteUpload(upload.id);
1415
+ this.callbacks.onUploadComplete?.(upload.id, result);
1416
+ } catch (error) {
1417
+ const errorMessage = extractErrorMessage(error);
1418
+ const retryCount = upload.retryCount + 1;
1419
+ await this.storageService.updateUploadStatus(upload.id, {
1420
+ status: "failed",
1421
+ retryCount,
1422
+ lastError: errorMessage
1423
+ });
1424
+ if (retryCount >= MAX_RETRIES) {
1425
+ this.callbacks.onUploadError?.(upload.id, new Error(`Upload failed after ${MAX_RETRIES} attempts: ${errorMessage}`));
1426
+ } else {
1427
+ const delay = this.calculateRetryDelay(retryCount);
1428
+ this.scheduleRetry(delay);
1429
+ }
1430
+ }
1431
+ }
1432
+ calculateRetryDelay(retryCount) {
1433
+ const delay = INITIAL_RETRY_DELAY * RETRY_MULTIPLIER ** (retryCount - 1);
1434
+ return Math.min(delay, MAX_RETRY_DELAY);
1435
+ }
1436
+ scheduleRetry(delay) {
1437
+ this.clearTimer(this.retryTimeoutId, clearTimeout);
1438
+ this.retryTimeoutId = window.setTimeout(() => {
1439
+ this.retryTimeoutId = null;
1440
+ this.processQueue();
1441
+ }, delay);
1442
+ }
1443
+ clearTimer(timerId, clearFn) {
1444
+ if (timerId !== null) {
1445
+ clearFn(timerId);
1446
+ }
1447
+ }
1448
+ }
1449
+
1450
+ // src/core/storage/video-storage.ts
1451
+ var DB_NAME = "vidtreo-recorder";
1452
+ var DB_VERSION = 1;
1453
+ var STORE_NAME = "pending-uploads";
1454
+ var STATUS_INDEX = "status";
1455
+ var CREATED_AT_INDEX = "createdAt";
1456
+ var DEFAULT_RETENTION_HOURS = 24;
1457
+ var MAX_RETRIES2 = 10;
1458
+ var MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
1459
+ var ID_PREFIX = "upload-";
1460
+ var ID_RANDOM_LENGTH = 9;
1461
+
1462
+ class VideoStorageService {
1463
+ db = null;
1464
+ init() {
1465
+ if (this.db) {
1466
+ return Promise.resolve();
1467
+ }
1468
+ return new Promise((resolve, reject) => {
1469
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
1470
+ request.onerror = () => {
1471
+ if (request.error) {
1472
+ reject(request.error);
1473
+ } else {
1474
+ reject(new Error("Failed to open database"));
1475
+ }
1476
+ };
1477
+ request.onsuccess = () => {
1478
+ if (!request.result) {
1479
+ reject(new Error("Database result is null"));
1480
+ return;
1481
+ }
1482
+ this.db = request.result;
1483
+ resolve();
1484
+ };
1485
+ request.onupgradeneeded = (event) => {
1486
+ const db = event.target.result;
1487
+ if (!db) {
1488
+ reject(new Error("Database upgrade result is null"));
1489
+ return;
1490
+ }
1491
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
1492
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
1493
+ store.createIndex(STATUS_INDEX, STATUS_INDEX, { unique: false });
1494
+ store.createIndex(CREATED_AT_INDEX, CREATED_AT_INDEX, {
1495
+ unique: false
1496
+ });
1497
+ }
1498
+ };
1499
+ });
1500
+ }
1501
+ isInitialized() {
1502
+ return this.db !== null;
1503
+ }
1504
+ savePendingUpload(upload) {
1505
+ const id = this.generateUploadId();
1506
+ const pendingUpload = {
1507
+ ...upload,
1508
+ id,
1509
+ status: "pending",
1510
+ retryCount: 0,
1511
+ createdAt: Date.now(),
1512
+ updatedAt: Date.now()
1513
+ };
1514
+ return this.executeTransaction("readwrite", (store) => {
1515
+ const request = store.add(pendingUpload);
1516
+ return new Promise((resolve, reject) => {
1517
+ request.onsuccess = () => resolve(id);
1518
+ request.onerror = () => {
1519
+ if (request.error) {
1520
+ if (request.error.name === "QuotaExceededError") {
1521
+ reject(new Error("Storage quota exceeded. Please free up space or delete old uploads."));
1522
+ } else {
1523
+ reject(request.error);
1524
+ }
1525
+ } else {
1526
+ reject(new Error("Failed to save upload"));
1527
+ }
1528
+ };
1529
+ });
1530
+ });
1531
+ }
1532
+ getPendingUploads(status) {
1533
+ return this.executeTransaction("readonly", (store) => {
1534
+ const request = status ? store.index(STATUS_INDEX).getAll(status) : store.getAll();
1535
+ return new Promise((resolve, reject) => {
1536
+ request.onsuccess = () => {
1537
+ if (request.result === undefined) {
1538
+ reject(new Error("Failed to get uploads: result is undefined"));
1539
+ return;
1540
+ }
1541
+ resolve(request.result);
1542
+ };
1543
+ request.onerror = () => {
1544
+ if (request.error) {
1545
+ reject(request.error);
1546
+ } else {
1547
+ reject(new Error("Failed to get uploads"));
1548
+ }
1549
+ };
1550
+ });
1551
+ });
1552
+ }
1553
+ updateUploadStatus(id, updates) {
1554
+ return this.executeTransaction("readwrite", (store) => {
1555
+ const getRequest = store.get(id);
1556
+ return new Promise((resolve, reject) => {
1557
+ getRequest.onsuccess = () => {
1558
+ const upload = getRequest.result;
1559
+ if (!upload) {
1560
+ reject(new Error("Upload not found"));
1561
+ return;
1562
+ }
1563
+ const updated = { ...upload, ...updates, updatedAt: Date.now() };
1564
+ const putRequest = store.put(updated);
1565
+ putRequest.onsuccess = () => resolve();
1566
+ putRequest.onerror = () => {
1567
+ if (putRequest.error) {
1568
+ reject(putRequest.error);
1569
+ } else {
1570
+ reject(new Error("Failed to update upload"));
1571
+ }
1572
+ };
1573
+ };
1574
+ getRequest.onerror = () => {
1575
+ if (getRequest.error) {
1576
+ reject(getRequest.error);
1577
+ } else {
1578
+ reject(new Error("Failed to get upload"));
1579
+ }
1580
+ };
1581
+ });
1582
+ });
1583
+ }
1584
+ deleteUpload(id) {
1585
+ return this.executeTransaction("readwrite", (store) => {
1586
+ const request = store.delete(id);
1587
+ return new Promise((resolve, reject) => {
1588
+ request.onsuccess = () => resolve();
1589
+ request.onerror = () => {
1590
+ if (request.error) {
1591
+ reject(request.error);
1592
+ } else {
1593
+ reject(new Error("Failed to delete upload"));
1594
+ }
1595
+ };
1596
+ });
1597
+ });
1598
+ }
1599
+ async cleanupPermanentlyFailedUploads(retentionHours) {
1600
+ const actualRetentionHours = retentionHours !== undefined ? retentionHours : DEFAULT_RETENTION_HOURS;
1601
+ if (typeof actualRetentionHours !== "number" || actualRetentionHours < 0) {
1602
+ throw new Error("retentionHours must be a non-negative number");
1603
+ }
1604
+ const cutoffTime = Date.now() - actualRetentionHours * MILLISECONDS_PER_HOUR;
1605
+ const allUploads = await this.getPendingUploads();
1606
+ const toDelete = allUploads.filter((upload) => upload.status === "failed" && upload.retryCount >= MAX_RETRIES2 && upload.updatedAt < cutoffTime);
1607
+ for (const upload of toDelete) {
1608
+ await this.deleteUpload(upload.id);
1609
+ }
1610
+ return toDelete.length;
1611
+ }
1612
+ async getTotalStorageSize() {
1613
+ const uploads = await this.getPendingUploads();
1614
+ return uploads.reduce((total, upload) => total + upload.blob.size, 0);
1615
+ }
1616
+ generateUploadId() {
1617
+ return `${ID_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 2 + ID_RANDOM_LENGTH)}`;
1618
+ }
1619
+ executeTransaction(mode, operation) {
1620
+ if (!this.db) {
1621
+ throw new Error("Database not initialized");
1622
+ }
1623
+ const transaction = this.db.transaction([STORE_NAME], mode);
1624
+ const store = transaction.objectStore(STORE_NAME);
1625
+ return operation(store);
1626
+ }
1627
+ }
1628
+
1629
+ // src/core/storage/storage-manager.ts
1630
+ var CLEANUP_INTERVAL = 60 * 60 * 1000;
1631
+ var CLEANUP_HOURS = 24;
1632
+
1633
+ class StorageManager {
1634
+ storageService = null;
1635
+ uploadQueueManager = null;
1636
+ cleanupIntervalId = null;
1637
+ async initialize(uploadService, callbacks, onCleanupError) {
1638
+ if (!this.storageService) {
1639
+ this.storageService = new VideoStorageService;
1640
+ }
1641
+ if (!this.storageService.isInitialized()) {
1642
+ await this.storageService.init();
1643
+ }
1644
+ if (!uploadService) {
1645
+ return;
1646
+ }
1647
+ if (this.uploadQueueManager) {
1648
+ this.uploadQueueManager.setCallbacks(callbacks);
1649
+ if (this.cleanupIntervalId === null) {
1650
+ this.cleanupIntervalId = window.setInterval(() => {
1651
+ this.performCleanup().catch((error) => {
1652
+ onCleanupError(extractErrorMessage(error));
1653
+ });
1654
+ }, CLEANUP_INTERVAL);
1655
+ }
1656
+ return;
1657
+ }
1658
+ this.uploadQueueManager = new UploadQueueManager(this.storageService, uploadService);
1659
+ this.uploadQueueManager.setCallbacks(callbacks);
1660
+ if (this.cleanupIntervalId === null) {
1661
+ this.cleanupIntervalId = window.setInterval(() => {
1662
+ this.performCleanup().catch((error) => {
1663
+ onCleanupError(extractErrorMessage(error));
1664
+ });
1665
+ }, CLEANUP_INTERVAL);
1666
+ }
1667
+ }
1668
+ async checkPendingUploads() {
1669
+ if (!this.uploadQueueManager) {
1670
+ throw new Error("UploadQueueManager not initialized");
1671
+ }
1672
+ return await this.uploadQueueManager.getStats();
1673
+ }
1674
+ async performCleanup() {
1675
+ if (!this.storageService) {
1676
+ throw new Error("StorageService not initialized");
1677
+ }
1678
+ await this.storageService.cleanupPermanentlyFailedUploads(CLEANUP_HOURS);
1679
+ }
1680
+ getUploadQueueManager() {
1681
+ return this.uploadQueueManager;
1682
+ }
1683
+ getStorageService() {
1684
+ return this.storageService;
1685
+ }
1686
+ destroy() {
1687
+ if (this.uploadQueueManager) {
1688
+ this.uploadQueueManager.destroy();
1689
+ this.uploadQueueManager = null;
1690
+ }
1691
+ if (this.cleanupIntervalId !== null) {
1692
+ clearInterval(this.cleanupIntervalId);
1693
+ this.cleanupIntervalId = null;
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ // src/core/stream/source-switch.ts
1699
+ var SCREEN_SHARE_TRANSITION_DELAY = 100;
1700
+ var TRACK_READY_STATE_LIVE = "live";
1701
+
1702
+ class SourceSwitchManager {
1703
+ currentSourceType = "camera";
1704
+ originalCameraStream = null;
1705
+ originalCameraConstraints = null;
1706
+ screenShareStream = null;
1707
+ screenShareTrackEndHandler = null;
1708
+ streamManager;
1709
+ callbacks;
1710
+ constructor(streamManager, callbacks = {}) {
1711
+ this.streamManager = streamManager;
1712
+ this.callbacks = callbacks;
1713
+ }
1714
+ getCurrentSourceType() {
1715
+ return this.currentSourceType;
1716
+ }
1717
+ getOriginalCameraStream() {
1718
+ return this.originalCameraStream;
1719
+ }
1720
+ stopStreamTracks(stream) {
1721
+ const tracks = stream.getTracks();
1722
+ for (const track of tracks) {
1723
+ if (track.readyState === TRACK_READY_STATE_LIVE) {
1724
+ track.stop();
1725
+ }
1726
+ }
1727
+ }
1728
+ isTrackLive(track) {
1729
+ return track !== undefined && track.readyState === TRACK_READY_STATE_LIVE;
1730
+ }
1731
+ areTracksLive(videoTrack, audioTrack) {
1732
+ return this.isTrackLive(videoTrack) && this.isTrackLive(audioTrack);
1733
+ }
1734
+ storeOriginalCameraConstraints(stream) {
1735
+ const videoTrack = stream.getVideoTracks()[0];
1736
+ if (!videoTrack) {
1737
+ return;
1738
+ }
1739
+ const settings = videoTrack.getSettings();
1740
+ this.originalCameraConstraints = {
1741
+ width: settings.width,
1742
+ height: settings.height,
1743
+ aspectRatio: settings.aspectRatio,
1744
+ frameRate: settings.frameRate,
1745
+ deviceId: settings.deviceId,
1746
+ facingMode: settings.facingMode
1747
+ };
1748
+ }
1749
+ storeOriginalCameraStream(stream) {
1750
+ const videoTrack = stream.getVideoTracks()[0];
1751
+ const audioTrack = stream.getAudioTracks()[0];
1752
+ if (this.areTracksLive(videoTrack, audioTrack)) {
1753
+ this.originalCameraStream = new MediaStream([videoTrack, audioTrack]);
1754
+ } else {
1755
+ this.originalCameraStream = stream;
1756
+ }
1757
+ }
1758
+ createError(error) {
1759
+ if (error instanceof Error) {
1760
+ return error;
1761
+ }
1762
+ return new Error(extractErrorMessage(error));
1763
+ }
1764
+ waitForTracksToEnd(delay) {
1765
+ return new Promise((resolve) => {
1766
+ setTimeout(() => {
1767
+ this.screenShareStream = null;
1768
+ resolve();
1769
+ }, delay);
1770
+ });
1771
+ }
1772
+ async switchToScreenCapture() {
1773
+ const currentStream = this.streamManager.getStream();
1774
+ if (currentStream) {
1775
+ this.storeOriginalCameraConstraints(currentStream);
1776
+ this.storeOriginalCameraStream(currentStream);
1777
+ }
1778
+ if (this.callbacks.onTransitionStart) {
1779
+ this.callbacks.onTransitionStart("Select screen to share...");
1780
+ }
1781
+ try {
1782
+ const newStream = await navigator.mediaDevices.getDisplayMedia({
1783
+ video: true,
1784
+ audio: true
1785
+ });
1786
+ this.screenShareStream = newStream;
1787
+ if (currentStream && currentStream !== this.originalCameraStream) {
1788
+ this.stopStreamTracks(currentStream);
1789
+ }
1790
+ this.currentSourceType = "screen";
1791
+ if (this.callbacks.onSourceChange) {
1792
+ this.callbacks.onSourceChange(this.currentSourceType);
1793
+ }
1794
+ this.setupScreenShareTrackHandler(newStream);
1795
+ return newStream;
1796
+ } catch (error) {
1797
+ if (this.callbacks.onTransitionEnd) {
1798
+ this.callbacks.onTransitionEnd();
1799
+ }
1800
+ throw error;
1801
+ }
1802
+ }
1803
+ setupScreenShareTrackHandler(newStream) {
1804
+ const videoTrack = newStream.getVideoTracks()[0];
1805
+ if (!videoTrack) {
1806
+ throw new Error("No video track found in screen share stream");
1807
+ }
1808
+ const handler = this.screenShareTrackEndHandler;
1809
+ if (handler) {
1810
+ const oldStream = this.streamManager.getStream();
1811
+ if (oldStream) {
1812
+ const oldVideoTrack = oldStream.getVideoTracks()[0];
1813
+ if (oldVideoTrack) {
1814
+ oldVideoTrack.removeEventListener("ended", handler);
1815
+ }
1816
+ }
1817
+ }
1818
+ this.screenShareTrackEndHandler = async () => {
1819
+ if (this.currentSourceType === "screen") {
1820
+ try {
1821
+ await this.switchToCamera();
1822
+ } catch (error) {
1823
+ if (this.callbacks.onError) {
1824
+ this.callbacks.onError(this.createError(error));
1825
+ }
1826
+ }
1827
+ }
1828
+ };
1829
+ videoTrack.addEventListener("ended", this.screenShareTrackEndHandler);
1830
+ }
1831
+ removeScreenShareTrackHandler(stream) {
1832
+ if (!(this.screenShareTrackEndHandler && stream)) {
1833
+ return;
1834
+ }
1835
+ const videoTrack = stream.getVideoTracks()[0];
1836
+ if (videoTrack) {
1837
+ videoTrack.removeEventListener("ended", this.screenShareTrackEndHandler);
1838
+ }
1839
+ this.screenShareTrackEndHandler = null;
1840
+ }
1841
+ canReuseOriginalStream() {
1842
+ if (!this.originalCameraStream) {
1843
+ return false;
1844
+ }
1845
+ const videoTrack = this.originalCameraStream.getVideoTracks()[0];
1846
+ const audioTrack = this.originalCameraStream.getAudioTracks()[0];
1847
+ return this.areTracksLive(videoTrack, audioTrack);
1848
+ }
1849
+ canReuseManagerStream() {
1850
+ const managerStream = this.streamManager.getStream();
1851
+ if (!(managerStream && this.originalCameraStream) || managerStream !== this.originalCameraStream) {
1852
+ return false;
1853
+ }
1854
+ const videoTrack = managerStream.getVideoTracks()[0];
1855
+ const audioTrack = managerStream.getAudioTracks()[0];
1856
+ return this.areTracksLive(videoTrack, audioTrack);
1857
+ }
1858
+ buildVideoConstraints(cameraDeviceId) {
1859
+ const constraints = {};
1860
+ if (cameraDeviceId) {
1861
+ constraints.deviceId = { exact: cameraDeviceId };
1862
+ }
1863
+ if (this.originalCameraConstraints) {
1864
+ Object.assign(constraints, this.originalCameraConstraints);
1865
+ }
1866
+ return constraints;
1867
+ }
1868
+ buildAudioConstraints(micDeviceId) {
1869
+ if (micDeviceId) {
1870
+ return { deviceId: { exact: micDeviceId } };
1871
+ }
1872
+ return true;
1873
+ }
1874
+ async createNewCameraStreamForRecording() {
1875
+ const cameraDeviceId = this.callbacks.getSelectedCameraDeviceId ? this.callbacks.getSelectedCameraDeviceId() : null;
1876
+ const micDeviceId = this.callbacks.getSelectedMicDeviceId ? this.callbacks.getSelectedMicDeviceId() : null;
1877
+ const videoConstraints = this.buildVideoConstraints(cameraDeviceId);
1878
+ const audioConstraints = this.buildAudioConstraints(micDeviceId);
1879
+ const constraints = {
1880
+ video: Object.keys(videoConstraints).length > 0 ? videoConstraints : true,
1881
+ audio: audioConstraints
1882
+ };
1883
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
1884
+ const videoTrack = newStream.getVideoTracks()[0];
1885
+ const audioTrack = newStream.getAudioTracks()[0];
1886
+ if (!this.isTrackLive(videoTrack)) {
1887
+ this.stopStreamTracks(newStream);
1888
+ const readyState = videoTrack ? videoTrack.readyState : "undefined";
1889
+ throw new Error(`Failed to get live camera video track. ReadyState: ${readyState}`);
1890
+ }
1891
+ if (!this.isTrackLive(audioTrack)) {
1892
+ this.stopStreamTracks(newStream);
1893
+ const readyState = audioTrack ? audioTrack.readyState : "undefined";
1894
+ throw new Error(`Failed to get live camera audio track. ReadyState: ${readyState}`);
1895
+ }
1896
+ this.originalCameraStream = newStream;
1897
+ return newStream;
1898
+ }
1899
+ async getCameraStream() {
1900
+ const isRecording = this.streamManager.isRecording();
1901
+ if (this.canReuseOriginalStream()) {
1902
+ const stream = this.originalCameraStream;
1903
+ if (!stream) {
1904
+ throw new Error("Original camera stream is null");
1905
+ }
1906
+ return stream;
1907
+ }
1908
+ if (this.canReuseManagerStream()) {
1909
+ const stream = this.streamManager.getStream();
1910
+ if (!stream) {
1911
+ throw new Error("Manager stream is null");
1912
+ }
1913
+ return stream;
1914
+ }
1915
+ if (this.originalCameraStream) {
1916
+ this.originalCameraStream = null;
1917
+ }
1918
+ const managerStream = this.streamManager.getStream();
1919
+ if (!isRecording && managerStream && managerStream !== this.originalCameraStream) {
1920
+ this.stopStreamTracks(managerStream);
1921
+ this.streamManager.setMediaStream(null);
1922
+ }
1923
+ if (this.callbacks.getSelectedCameraDeviceId) {
1924
+ const cameraDeviceId = this.callbacks.getSelectedCameraDeviceId();
1925
+ this.streamManager.setVideoDevice(cameraDeviceId);
1926
+ }
1927
+ if (this.callbacks.getSelectedMicDeviceId) {
1928
+ const micDeviceId = this.callbacks.getSelectedMicDeviceId();
1929
+ this.streamManager.setAudioDevice(micDeviceId);
1930
+ }
1931
+ if (isRecording) {
1932
+ return this.createNewCameraStreamForRecording();
1933
+ }
1934
+ const newStream = await this.streamManager.startStream();
1935
+ this.originalCameraStream = newStream;
1936
+ return newStream;
1937
+ }
1938
+ async switchToCamera() {
1939
+ const isRecording = this.streamManager.isRecording();
1940
+ if (!isRecording && this.currentSourceType === "camera") {
1941
+ return;
1942
+ }
1943
+ try {
1944
+ this.notifyTransitionStart("Switching to camera...");
1945
+ await this.handleScreenShareStop();
1946
+ const newStream = await this.getCameraStream();
1947
+ if (!newStream) {
1948
+ throw new Error("Failed to get camera stream");
1949
+ }
1950
+ await this.applyCameraStream(newStream, isRecording);
1951
+ this.notifyTransitionEnd();
1952
+ } catch (error) {
1953
+ this.notifyTransitionEnd();
1954
+ throw error;
1955
+ }
1956
+ }
1957
+ notifyTransitionStart(message) {
1958
+ if (this.callbacks.onTransitionStart) {
1959
+ this.callbacks.onTransitionStart(message);
1960
+ }
1961
+ }
1962
+ notifyTransitionEnd() {
1963
+ if (this.callbacks.onTransitionEnd) {
1964
+ this.callbacks.onTransitionEnd();
1965
+ }
1966
+ }
1967
+ async handleScreenShareStop() {
1968
+ if (this.currentSourceType !== "screen") {
1969
+ return;
1970
+ }
1971
+ const screenShareStreamToStop = this.screenShareStream;
1972
+ const currentStream = this.streamManager.getStream();
1973
+ if (screenShareStreamToStop) {
1974
+ this.removeScreenShareTrackHandler(screenShareStreamToStop);
1975
+ this.stopStreamTracks(screenShareStreamToStop);
1976
+ await this.waitForTracksToEnd(SCREEN_SHARE_TRANSITION_DELAY);
1977
+ }
1978
+ if (currentStream && (!screenShareStreamToStop || currentStream.id !== screenShareStreamToStop.id)) {
1979
+ this.stopStreamTracks(currentStream);
1980
+ }
1981
+ }
1982
+ async applyCameraStream(newStream, isRecording) {
1983
+ this.streamManager.setMediaStream(newStream);
1984
+ this.currentSourceType = "camera";
1985
+ if (this.callbacks.onSourceChange) {
1986
+ this.callbacks.onSourceChange(this.currentSourceType);
1987
+ }
1988
+ if (isRecording) {
1989
+ await this.streamManager.switchVideoSource(newStream);
1990
+ }
1991
+ if (this.callbacks.onPreviewUpdate) {
1992
+ await this.callbacks.onPreviewUpdate(newStream);
1993
+ }
1994
+ }
1995
+ async toggleSource() {
1996
+ const isRecording = this.streamManager.isRecording();
1997
+ if (!isRecording) {
1998
+ return;
1999
+ }
2000
+ try {
2001
+ if (this.currentSourceType === "camera") {
2002
+ await this.switchToScreen();
2003
+ } else {
2004
+ await this.switchToCamera();
2005
+ }
2006
+ } catch (error) {
2007
+ this.handleToggleError(error);
2008
+ }
2009
+ }
2010
+ async switchToScreen() {
2011
+ const newStream = await this.switchToScreenCapture();
2012
+ this.notifyTransitionStart("Switching to screen...");
2013
+ await this.streamManager.switchVideoSource(newStream);
2014
+ if (this.callbacks.onPreviewUpdate) {
2015
+ await this.callbacks.onPreviewUpdate(newStream);
2016
+ }
2017
+ this.notifyTransitionEnd();
2018
+ }
2019
+ handleToggleError(error) {
2020
+ this.notifyTransitionEnd();
2021
+ const errorMessage = extractErrorMessage(error);
2022
+ if (errorMessage.includes("NotAllowedError") || errorMessage.includes("AbortError")) {
2023
+ if (this.currentSourceType === "screen") {
2024
+ this.switchToCamera().catch((switchError) => {
2025
+ if (this.callbacks.onError) {
2026
+ this.callbacks.onError(this.createError(switchError));
2027
+ }
2028
+ });
2029
+ }
2030
+ } else if (this.callbacks.onError) {
2031
+ this.callbacks.onError(this.createError(error));
2032
+ }
2033
+ }
2034
+ async handleRecordingStop() {
2035
+ if (this.currentSourceType !== "screen") {
2036
+ this.cleanup();
2037
+ return;
2038
+ }
2039
+ try {
2040
+ const currentStream = this.streamManager.getStream();
2041
+ if (currentStream) {
2042
+ this.removeScreenShareTrackHandler(currentStream);
2043
+ this.stopStreamTracks(currentStream);
2044
+ }
2045
+ const newStream = await this.getCameraStream();
2046
+ if (!newStream) {
2047
+ throw new Error("Failed to get camera stream");
2048
+ }
2049
+ this.streamManager.setMediaStream(newStream);
2050
+ this.currentSourceType = "camera";
2051
+ if (this.callbacks.onSourceChange) {
2052
+ this.callbacks.onSourceChange(this.currentSourceType);
2053
+ }
2054
+ if (this.callbacks.onPreviewUpdate) {
2055
+ await this.callbacks.onPreviewUpdate(newStream);
2056
+ }
2057
+ } catch (error) {
2058
+ if (this.callbacks.onError) {
2059
+ this.callbacks.onError(this.createError(error));
2060
+ }
2061
+ throw error;
2062
+ }
2063
+ this.cleanup();
2064
+ }
2065
+ cleanup() {
2066
+ if (this.screenShareStream) {
2067
+ this.removeScreenShareTrackHandler(this.screenShareStream);
2068
+ this.stopStreamTracks(this.screenShareStream);
2069
+ this.screenShareStream = null;
2070
+ }
2071
+ const currentStream = this.streamManager.getStream();
2072
+ if (currentStream) {
2073
+ this.removeScreenShareTrackHandler(currentStream);
2074
+ }
2075
+ this.screenShareTrackEndHandler = null;
2076
+ this.originalCameraStream = null;
2077
+ this.originalCameraConstraints = null;
2078
+ }
2079
+ setCallbacks(callbacks) {
2080
+ this.callbacks = { ...this.callbacks, ...callbacks };
2081
+ }
2082
+ }
2083
+
2084
+ // src/core/stream/config.ts
2085
+ var DEFAULT_CAMERA_CONSTRAINTS = Object.freeze({
2086
+ width: { ideal: DEFAULT_TRANSCODE_CONFIG.width },
2087
+ height: { ideal: DEFAULT_TRANSCODE_CONFIG.height },
2088
+ frameRate: { ideal: DEFAULT_TRANSCODE_CONFIG.fps }
2089
+ });
2090
+ var DEFAULT_STREAM_CONFIG = Object.freeze({
2091
+ video: DEFAULT_CAMERA_CONSTRAINTS,
2092
+ audio: true
2093
+ });
2094
+ var DEFAULT_RECORDING_OPTIONS = Object.freeze({
2095
+ mimeType: "video/webm;codecs=vp9,opus"
2096
+ });
2097
+
2098
+ // src/core/stream/stream.ts
2099
+ var TIMER_INTERVAL = 1000;
2100
+ var SECONDS_PER_MINUTE = 60;
2101
+ var MILLISECONDS_PER_SECOND2 = 1000;
2102
+ var TRACK_READY_STATE_LIVE2 = "live";
2103
+ var MEDIA_RECORDER_STATE_RECORDING = "recording";
2104
+ var MEDIA_RECORDER_STATE_PAUSED = "paused";
2105
+
2106
+ class CameraStreamManager {
2107
+ mediaStream = null;
2108
+ mediaRecorder = null;
2109
+ recordedChunks = [];
2110
+ recordedMimeType = null;
2111
+ state = "idle";
2112
+ recordingStartTime = 0;
2113
+ recordingTimer = null;
2114
+ pauseStartTime = null;
2115
+ totalPausedTime = 0;
2116
+ eventListeners = new Map;
2117
+ streamConfig;
2118
+ recordingOptions;
2119
+ streamProcessor = null;
2120
+ bufferSizeUpdateInterval = null;
2121
+ selectedAudioDeviceId = null;
2122
+ selectedVideoDeviceId = null;
2123
+ constructor(streamConfig = {}, recordingOptions = {}) {
2124
+ this.streamConfig = { ...DEFAULT_STREAM_CONFIG, ...streamConfig };
2125
+ this.recordingOptions = {
2126
+ ...DEFAULT_RECORDING_OPTIONS,
2127
+ ...recordingOptions
2128
+ };
2129
+ }
2130
+ getState() {
2131
+ return this.state;
2132
+ }
2133
+ getStream() {
2134
+ return this.mediaStream;
2135
+ }
2136
+ getAudioStreamForAnalysis() {
2137
+ if (this.streamProcessor) {
2138
+ const audioStream = this.streamProcessor.getAudioStreamForAnalysis();
2139
+ if (audioStream) {
2140
+ return audioStream;
2141
+ }
2142
+ }
2143
+ if (this.mediaStream && this.mediaStream.getAudioTracks().length > 0) {
2144
+ return this.mediaStream;
2145
+ }
2146
+ return null;
2147
+ }
2148
+ getRecorder() {
2149
+ return this.mediaRecorder;
2150
+ }
2151
+ isRecording() {
2152
+ return this.state === "recording";
2153
+ }
2154
+ isActive() {
2155
+ return this.state === "active" || this.state === "recording";
2156
+ }
2157
+ on(event, listener) {
2158
+ if (!this.eventListeners.has(event)) {
2159
+ this.eventListeners.set(event, new Set);
2160
+ }
2161
+ const listeners = this.eventListeners.get(event);
2162
+ if (listeners) {
2163
+ listeners.add(listener);
2164
+ }
2165
+ return () => {
2166
+ this.off(event, listener);
2167
+ };
2168
+ }
2169
+ off(event, listener) {
2170
+ const listeners = this.eventListeners.get(event);
2171
+ if (listeners) {
2172
+ listeners.delete(listener);
2173
+ }
2174
+ }
2175
+ once(event, listener) {
2176
+ const wrappedListener = (data) => {
2177
+ listener(data);
2178
+ this.off(event, wrappedListener);
2179
+ };
2180
+ return this.on(event, wrappedListener);
2181
+ }
2182
+ emit(event, data) {
2183
+ const listeners = this.eventListeners.get(event);
2184
+ if (listeners) {
2185
+ for (const listener of listeners) {
2186
+ try {
2187
+ listener(data);
2188
+ } catch {}
2189
+ }
2190
+ }
2191
+ }
2192
+ setState(newState) {
2193
+ if (this.state === newState) {
2194
+ return;
2195
+ }
2196
+ const previousState = this.state;
2197
+ this.state = newState;
2198
+ this.emit("statechange", { state: newState, previousState });
2199
+ }
2200
+ setAudioDevice(deviceId) {
2201
+ this.selectedAudioDeviceId = deviceId;
2202
+ }
2203
+ setVideoDevice(deviceId) {
2204
+ this.selectedVideoDeviceId = deviceId;
2205
+ }
2206
+ getAudioDevice() {
2207
+ return this.selectedAudioDeviceId;
2208
+ }
2209
+ getVideoDevice() {
2210
+ return this.selectedVideoDeviceId;
2211
+ }
2212
+ async getAvailableDevices() {
2213
+ try {
2214
+ const devices = await navigator.mediaDevices.enumerateDevices();
2215
+ return {
2216
+ audioinput: devices.filter((d) => d.kind === "audioinput"),
2217
+ videoinput: devices.filter((d) => d.kind === "videoinput")
2218
+ };
2219
+ } catch (error) {
2220
+ throw new Error(`Failed to enumerate devices: ${extractErrorMessage(error)}`);
2221
+ }
2222
+ }
2223
+ buildVideoConstraints(deviceId) {
2224
+ if (!deviceId) {
2225
+ return this.streamConfig.video;
2226
+ }
2227
+ if (typeof this.streamConfig.video === "object") {
2228
+ return {
2229
+ ...this.streamConfig.video,
2230
+ deviceId: { exact: deviceId }
2231
+ };
2232
+ }
2233
+ return {
2234
+ deviceId: { exact: deviceId }
2235
+ };
2236
+ }
2237
+ buildAudioConstraints(deviceId) {
2238
+ if (!deviceId) {
2239
+ return this.streamConfig.audio;
2240
+ }
2241
+ if (typeof this.streamConfig.audio === "object") {
2242
+ return {
2243
+ ...this.streamConfig.audio,
2244
+ deviceId: { exact: deviceId }
2245
+ };
2246
+ }
2247
+ return {
2248
+ deviceId: { exact: deviceId }
2249
+ };
2250
+ }
2251
+ async startStream() {
2252
+ if (this.mediaStream) {
2253
+ return this.mediaStream;
2254
+ }
2255
+ this.setState("starting");
2256
+ try {
2257
+ const constraints = {
2258
+ video: this.buildVideoConstraints(this.selectedVideoDeviceId),
2259
+ audio: this.buildAudioConstraints(this.selectedAudioDeviceId)
2260
+ };
2261
+ this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
2262
+ this.setState("active");
2263
+ this.emit("streamstart", { stream: this.mediaStream });
2264
+ return this.mediaStream;
2265
+ } catch (error) {
2266
+ const err = error instanceof Error ? error : new Error(extractErrorMessage(error));
2267
+ this.setState("error");
2268
+ this.emit("error", { error: err });
2269
+ throw err;
2270
+ }
2271
+ }
2272
+ stopStream() {
2273
+ if (this.mediaStream) {
2274
+ for (const track of this.mediaStream.getTracks()) {
2275
+ track.stop();
2276
+ }
2277
+ this.mediaStream = null;
2278
+ }
2279
+ if (this.state !== "idle") {
2280
+ this.setState("idle");
2281
+ this.emit("streamstop", undefined);
2282
+ }
2283
+ }
2284
+ stopStreamTracks(stream) {
2285
+ for (const track of stream.getTracks()) {
2286
+ track.stop();
2287
+ }
2288
+ }
2289
+ isTrackLive(track) {
2290
+ return track !== undefined && track.readyState === TRACK_READY_STATE_LIVE2;
2291
+ }
2292
+ async tryReplaceTrack(oldTrack, newTrack, newStream) {
2293
+ const replaceTrackMethod = oldTrack.replaceTrack;
2294
+ if (typeof replaceTrackMethod !== "function") {
2295
+ return false;
2296
+ }
2297
+ try {
2298
+ await replaceTrackMethod.call(oldTrack, newTrack);
2299
+ oldTrack.stop();
2300
+ for (const track of newStream.getTracks()) {
2301
+ if (track !== newTrack) {
2302
+ track.stop();
2303
+ }
2304
+ }
2305
+ newTrack.stop();
2306
+ return true;
2307
+ } catch {
2308
+ return false;
2309
+ }
2310
+ }
2311
+ recreateStreamWithNewTrack(newTrack, oldOtherTrack, newStream) {
2312
+ for (const track of newStream.getTracks()) {
2313
+ if (track !== newTrack) {
2314
+ track.stop();
2315
+ }
2316
+ }
2317
+ const tracks = [newTrack];
2318
+ if (this.isTrackLive(oldOtherTrack) && oldOtherTrack) {
2319
+ tracks.push(oldOtherTrack);
2320
+ }
2321
+ const combinedStream = new MediaStream(tracks);
2322
+ if (this.mediaStream) {
2323
+ for (const track of this.mediaStream.getTracks()) {
2324
+ if (track !== oldOtherTrack) {
2325
+ track.stop();
2326
+ }
2327
+ }
2328
+ }
2329
+ return combinedStream;
2330
+ }
2331
+ async switchVideoDevice(deviceId) {
2332
+ if (!this.mediaStream) {
2333
+ throw new Error("No active stream to switch device");
2334
+ }
2335
+ const oldVideoTrack = this.mediaStream.getVideoTracks()[0];
2336
+ const oldAudioTrack = this.mediaStream.getAudioTracks()[0];
2337
+ if (!oldVideoTrack) {
2338
+ throw new Error("No video track in current stream");
2339
+ }
2340
+ const constraints = {
2341
+ video: deviceId ? {
2342
+ ...typeof this.streamConfig.video === "object" ? this.streamConfig.video : {},
2343
+ deviceId: { exact: deviceId }
2344
+ } : this.streamConfig.video
2345
+ };
2346
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
2347
+ const newVideoTrack = newStream.getVideoTracks()[0];
2348
+ if (!newVideoTrack) {
2349
+ this.stopStreamTracks(newStream);
2350
+ throw new Error("Failed to get new video track");
2351
+ }
2352
+ const useReplaceTrack = await this.tryReplaceTrack(oldVideoTrack, newVideoTrack, newStream);
2353
+ if (!useReplaceTrack) {
2354
+ oldVideoTrack.stop();
2355
+ this.mediaStream = this.recreateStreamWithNewTrack(newVideoTrack, oldAudioTrack, newStream);
2356
+ }
2357
+ this.selectedVideoDeviceId = deviceId;
2358
+ this.emit("videosourcechange", { stream: this.mediaStream });
2359
+ return this.mediaStream;
2360
+ }
2361
+ async switchAudioDevice(deviceId) {
2362
+ if (!this.mediaStream) {
2363
+ throw new Error("No active stream to switch device");
2364
+ }
2365
+ const oldAudioTrack = this.mediaStream.getAudioTracks()[0];
2366
+ const oldVideoTrack = this.mediaStream.getVideoTracks()[0];
2367
+ if (!oldAudioTrack) {
2368
+ throw new Error("No audio track in current stream");
2369
+ }
2370
+ const constraints = {
2371
+ audio: deviceId ? {
2372
+ ...typeof this.streamConfig.audio === "object" ? this.streamConfig.audio : {},
2373
+ deviceId: { exact: deviceId }
2374
+ } : this.streamConfig.audio
2375
+ };
2376
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
2377
+ const newAudioTrack = newStream.getAudioTracks()[0];
2378
+ if (!newAudioTrack) {
2379
+ this.stopStreamTracks(newStream);
2380
+ throw new Error("Failed to get new audio track");
2381
+ }
2382
+ const useReplaceTrack = await this.tryReplaceTrack(oldAudioTrack, newAudioTrack, newStream);
2383
+ if (!useReplaceTrack) {
2384
+ oldAudioTrack.stop();
2385
+ this.mediaStream = this.recreateStreamWithNewTrack(newAudioTrack, oldVideoTrack, newStream);
2386
+ }
2387
+ this.selectedAudioDeviceId = deviceId;
2388
+ return this.mediaStream;
2389
+ }
2390
+ startRecordingWithMediaRecorder() {
2391
+ if (!this.mediaStream) {
2392
+ throw new Error("Stream must be started before recording");
2393
+ }
2394
+ if (this.isRecording()) {
2395
+ return;
2396
+ }
2397
+ this.recordedChunks = [];
2398
+ this.recordedMimeType = null;
2399
+ try {
2400
+ this.mediaRecorder = new MediaRecorder(this.mediaStream, this.recordingOptions);
2401
+ } catch {
2402
+ this.mediaRecorder = new MediaRecorder(this.mediaStream);
2403
+ }
2404
+ this.mediaRecorder.ondataavailable = (event) => {
2405
+ if (event.data && event.data.size > 0) {
2406
+ this.recordedChunks.push(event.data);
2407
+ this.emit("recordingdata", { data: event.data });
2408
+ }
2409
+ };
2410
+ this.mediaRecorder.onstop = () => {
2411
+ if (!this.mediaRecorder) {
2412
+ throw new Error("MediaRecorder is missing in onstop handler");
2413
+ }
2414
+ const mimeType = this.mediaRecorder.mimeType;
2415
+ if (!mimeType) {
2416
+ throw new Error("MediaRecorder mimeType is missing");
2417
+ }
2418
+ this.recordedMimeType = mimeType;
2419
+ const blob = new Blob(this.recordedChunks, {
2420
+ type: mimeType
2421
+ });
2422
+ this.setState("active");
2423
+ this.emit("recordingstop", {
2424
+ blob,
2425
+ mimeType
2426
+ });
2427
+ this.mediaRecorder = null;
2428
+ this.recordedChunks = [];
2429
+ };
2430
+ this.mediaRecorder.start();
2431
+ this.resetRecordingState();
2432
+ this.setState("recording");
2433
+ this.emit("recordingstart", { recorder: this.mediaRecorder });
2434
+ this.startRecordingTimer();
2435
+ }
2436
+ stopRecordingWithMediaRecorder() {
2437
+ if (!(this.mediaRecorder && this.isRecording())) {
2438
+ return;
2439
+ }
2440
+ this.setState("stopping");
2441
+ this.clearRecordingTimer();
2442
+ this.resetPauseState();
2443
+ this.mediaRecorder.stop();
2444
+ }
2445
+ async startRecording(processor, config) {
2446
+ if (!this.mediaStream) {
2447
+ throw new Error("Stream must be started before recording");
2448
+ }
2449
+ if (this.isRecording()) {
2450
+ return;
2451
+ }
2452
+ this.streamProcessor = processor;
2453
+ await processor.startProcessing(this.mediaStream, config);
2454
+ this.bufferSizeUpdateInterval = window.setInterval(() => {
2455
+ if (!this.streamProcessor) {
2456
+ return;
2457
+ }
2458
+ const size = this.streamProcessor.getBufferSize();
2459
+ const formatted = formatFileSize(size);
2460
+ this.emit("recordingbufferupdate", { size, formatted });
2461
+ }, TIMER_INTERVAL);
2462
+ processor.setOnMuteStateChange((muted) => {
2463
+ this.emit("audiomutetoggle", { muted });
2464
+ });
2465
+ processor.setOnSourceChange((stream) => {
2466
+ this.emit("videosourcechange", { stream });
2467
+ });
2468
+ this.resetRecordingState();
2469
+ this.setState("recording");
2470
+ this.emit("recordingstart", { recorder: null });
2471
+ this.startRecordingTimer();
2472
+ }
2473
+ async stopRecording() {
2474
+ if (!(this.streamProcessor && this.isRecording())) {
2475
+ throw new Error("Not currently recording");
2476
+ }
2477
+ this.setState("stopping");
2478
+ this.clearRecordingTimer();
2479
+ this.clearBufferSizeInterval();
2480
+ this.resetPauseState();
2481
+ const result = await this.streamProcessor.finalize();
2482
+ this.setState("active");
2483
+ this.emit("recordingstop", {
2484
+ blob: result.blob,
2485
+ mimeType: "video/mp4"
2486
+ });
2487
+ this.streamProcessor = null;
2488
+ return result.blob;
2489
+ }
2490
+ pauseRecording() {
2491
+ this.clearRecordingTimer();
2492
+ if (this.pauseStartTime === null) {
2493
+ this.pauseStartTime = Date.now();
2494
+ }
2495
+ if (this.streamProcessor && this.isRecording()) {
2496
+ this.streamProcessor.pause();
2497
+ } else if (this.mediaRecorder && this.isRecording() && this.mediaRecorder.state === MEDIA_RECORDER_STATE_RECORDING) {
2498
+ this.mediaRecorder.pause();
2499
+ }
2500
+ }
2501
+ resumeRecording() {
2502
+ if (this.pauseStartTime !== null) {
2503
+ const pausedDuration = Date.now() - this.pauseStartTime;
2504
+ this.totalPausedTime += pausedDuration;
2505
+ this.pauseStartTime = null;
2506
+ }
2507
+ this.startRecordingTimer();
2508
+ if (this.streamProcessor && this.isRecording()) {
2509
+ this.streamProcessor.resume();
2510
+ } else if (this.mediaRecorder && this.isRecording() && this.mediaRecorder.state === MEDIA_RECORDER_STATE_PAUSED) {
2511
+ this.mediaRecorder.resume();
2512
+ }
2513
+ }
2514
+ toggleMute() {
2515
+ if (!this.streamProcessor) {
2516
+ throw new Error("StreamProcessor is required to toggle mute");
2517
+ }
2518
+ this.streamProcessor.toggleMute();
2519
+ }
2520
+ setAudioTracksEnabled(enabled) {
2521
+ if (!this.mediaStream) {
2522
+ return;
2523
+ }
2524
+ const audioTracks = this.mediaStream.getAudioTracks();
2525
+ for (const track of audioTracks) {
2526
+ track.enabled = enabled;
2527
+ }
2528
+ }
2529
+ muteAudio() {
2530
+ if (this.streamProcessor) {
2531
+ if (!this.streamProcessor.isMutedState()) {
2532
+ this.streamProcessor.toggleMute();
2533
+ }
2534
+ this.setAudioTracksEnabled(false);
2535
+ } else if (this.mediaStream) {
2536
+ this.setAudioTracksEnabled(false);
2537
+ this.emit("audiomutetoggle", { muted: true });
2538
+ }
2539
+ }
2540
+ unmuteAudio() {
2541
+ if (this.streamProcessor) {
2542
+ if (this.streamProcessor.isMutedState()) {
2543
+ this.streamProcessor.toggleMute();
2544
+ }
2545
+ this.setAudioTracksEnabled(true);
2546
+ } else if (this.mediaStream) {
2547
+ this.setAudioTracksEnabled(true);
2548
+ this.emit("audiomutetoggle", { muted: false });
2549
+ }
2550
+ }
2551
+ isMuted() {
2552
+ if (this.streamProcessor) {
2553
+ return this.streamProcessor.isMutedState();
2554
+ }
2555
+ if (this.mediaStream) {
2556
+ const audioTracks = this.mediaStream.getAudioTracks();
2557
+ return audioTracks.length > 0 && audioTracks.every((track) => !track.enabled);
2558
+ }
2559
+ return false;
2560
+ }
2561
+ async switchVideoSource(newStream) {
2562
+ if (!this.streamProcessor) {
2563
+ throw new Error("StreamProcessor is required to switch video source");
2564
+ }
2565
+ await this.streamProcessor.switchVideoSource(newStream);
2566
+ }
2567
+ setMediaStream(stream) {
2568
+ this.mediaStream = stream;
2569
+ }
2570
+ getCurrentVideoSource() {
2571
+ if (!this.streamProcessor) {
2572
+ throw new Error("StreamProcessor is required to get current video source");
2573
+ }
2574
+ const source = this.streamProcessor.getCurrentVideoSource();
2575
+ if (!source) {
2576
+ throw new Error("Current video source is not available");
2577
+ }
2578
+ return source;
2579
+ }
2580
+ formatTimeElapsed(elapsedSeconds) {
2581
+ const mins = Math.floor(elapsedSeconds / SECONDS_PER_MINUTE);
2582
+ const secs = Math.floor(elapsedSeconds % SECONDS_PER_MINUTE);
2583
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
2584
+ }
2585
+ startRecordingTimer() {
2586
+ this.recordingTimer = window.setInterval(() => {
2587
+ const elapsed = (Date.now() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND2;
2588
+ const formatted = this.formatTimeElapsed(elapsed);
2589
+ this.emit("recordingtimeupdate", { elapsed, formatted });
2590
+ }, TIMER_INTERVAL);
2591
+ }
2592
+ clearRecordingTimer() {
2593
+ if (this.recordingTimer !== null) {
2594
+ clearInterval(this.recordingTimer);
2595
+ this.recordingTimer = null;
2596
+ }
2597
+ }
2598
+ clearBufferSizeInterval() {
2599
+ if (this.bufferSizeUpdateInterval !== null) {
2600
+ clearInterval(this.bufferSizeUpdateInterval);
2601
+ this.bufferSizeUpdateInterval = null;
2602
+ }
2603
+ }
2604
+ resetRecordingState() {
2605
+ this.recordingStartTime = Date.now();
2606
+ this.totalPausedTime = 0;
2607
+ this.pauseStartTime = null;
2608
+ }
2609
+ resetPauseState() {
2610
+ this.totalPausedTime = 0;
2611
+ this.pauseStartTime = null;
2612
+ }
2613
+ getRecordedBlob() {
2614
+ if (this.recordedChunks.length === 0) {
2615
+ throw new Error("No recorded chunks available");
2616
+ }
2617
+ if (!this.recordedMimeType) {
2618
+ throw new Error("Recorded mimeType is missing");
2619
+ }
2620
+ return new Blob(this.recordedChunks, {
2621
+ type: this.recordedMimeType
2622
+ });
2623
+ }
2624
+ destroy() {
2625
+ this.stopRecordingWithMediaRecorder();
2626
+ if (this.streamProcessor) {
2627
+ this.streamProcessor.cancel().catch(() => {});
2628
+ this.streamProcessor = null;
2629
+ }
2630
+ this.stopStream();
2631
+ this.clearRecordingTimer();
2632
+ this.clearBufferSizeInterval();
2633
+ this.eventListeners.clear();
2634
+ this.setState("idle");
2635
+ }
2636
+ }
2637
+
2638
+ // src/core/upload/duration-extractor.ts
2639
+ import { BlobSource as BlobSource2, Input as Input2, MP4 as MP42 } from "mediabunny";
2640
+ async function extractVideoDuration(blob) {
2641
+ try {
2642
+ const source = new BlobSource2(blob);
2643
+ const input = new Input2({
2644
+ formats: [MP42],
2645
+ source
2646
+ });
2647
+ if (typeof input.computeDuration !== "function") {
2648
+ throw new Error("computeDuration method is not available");
2649
+ }
2650
+ const duration = await input.computeDuration();
2651
+ if (!duration) {
2652
+ throw new Error("Duration is missing from computeDuration");
2653
+ }
2654
+ if (duration <= 0) {
2655
+ throw new Error("Invalid duration: must be greater than 0");
2656
+ }
2657
+ return duration;
2658
+ } catch {
2659
+ return extractDurationWithVideoElement(blob);
2660
+ }
2661
+ }
2662
+ function extractDurationWithVideoElement(blob) {
2663
+ return new Promise((resolve, reject) => {
2664
+ const video = document.createElement("video");
2665
+ const url = URL.createObjectURL(blob);
2666
+ const cleanup = () => {
2667
+ URL.revokeObjectURL(url);
2668
+ };
2669
+ video.addEventListener("loadedmetadata", () => {
2670
+ cleanup();
2671
+ const duration = video.duration;
2672
+ if (!Number.isFinite(duration) || duration <= 0) {
2673
+ reject(new Error("Invalid video duration"));
2674
+ return;
2675
+ }
2676
+ resolve(duration);
2677
+ });
2678
+ video.addEventListener("error", () => {
2679
+ cleanup();
2680
+ reject(new Error("Failed to load video metadata"));
2681
+ });
2682
+ video.src = url;
2683
+ video.load();
2684
+ });
2685
+ }
2686
+
2687
+ // src/core/upload/upload-manager.ts
2688
+ class UploadManager {
2689
+ callbacks;
2690
+ uploadQueueManager = null;
2691
+ constructor(callbacks) {
2692
+ this.callbacks = callbacks;
2693
+ }
2694
+ setUploadQueueManager(uploadQueueManager) {
2695
+ this.uploadQueueManager = uploadQueueManager;
2696
+ }
2697
+ async uploadVideo(blob, apiKey, backendUrl, userMetadata) {
2698
+ if (!this.uploadQueueManager) {
2699
+ throw new Error("Upload queue manager not initialized");
2700
+ }
2701
+ try {
2702
+ this.callbacks.onClearStatus();
2703
+ const duration = await extractVideoDuration(blob);
2704
+ const metadata = Object.keys(userMetadata).length > 0 ? userMetadata : undefined;
2705
+ await this.uploadQueueManager.queueUpload({
2706
+ blob,
2707
+ apiKey,
2708
+ backendUrl,
2709
+ filename: `recording-${Date.now()}.mp4`,
2710
+ duration,
2711
+ metadata: undefined,
2712
+ userMetadata: metadata
2713
+ });
2714
+ } catch (error) {
2715
+ this.callbacks.onError(error instanceof Error ? error : new Error(extractErrorMessage(error)));
2716
+ throw error;
2717
+ }
2718
+ }
2719
+ updateProgress(progress) {
2720
+ this.callbacks.onProgress(progress);
2721
+ }
2722
+ showSuccess(result) {
2723
+ this.callbacks.onSuccess(result);
2724
+ }
2725
+ showError(message) {
2726
+ this.callbacks.onError(new Error(message));
2727
+ }
2728
+ clearStatus() {
2729
+ this.callbacks.onClearStatus();
2730
+ }
2731
+ }
2732
+
2733
+ // src/core/upload/video-upload-service.ts
2734
+ var API_INIT_PATH = "/api/v1/videos/init";
2735
+ var HTTP_STATUS_OK_MIN = 200;
2736
+ var HTTP_STATUS_OK_MAX = 299;
2737
+ var BEARER_PREFIX = "Bearer ";
2738
+ var CONTENT_TYPE_JSON = "application/json";
2739
+ var HEADER_AUTHORIZATION = "Authorization";
2740
+ var HEADER_CONTENT_TYPE = "Content-Type";
2741
+ var HEADER_X_VIDEO_DURATION = "X-Video-Duration";
2742
+ var HTTP_METHOD_POST = "POST";
2743
+ var HTTP_METHOD_PUT = "PUT";
2744
+
2745
+ class VideoUploadService {
2746
+ async uploadVideo(blob, options) {
2747
+ if (!options.filename) {
2748
+ throw new Error("Filename is required");
2749
+ }
2750
+ if (!blob.type) {
2751
+ throw new Error("Blob type is required");
2752
+ }
2753
+ const initResponse = await this.initVideoUpload({
2754
+ apiKey: options.apiKey,
2755
+ backendUrl: options.backendUrl,
2756
+ filename: options.filename,
2757
+ fileSize: blob.size,
2758
+ mimeType: blob.type,
2759
+ metadata: options.metadata,
2760
+ userMetadata: options.userMetadata
2761
+ });
2762
+ return this.uploadVideoFile(blob, initResponse.uploadUrl, {
2763
+ apiKey: options.apiKey,
2764
+ duration: options.duration,
2765
+ onProgress: options.onProgress
2766
+ });
2767
+ }
2768
+ async initVideoUpload(data) {
2769
+ const url = `${data.backendUrl}${API_INIT_PATH}`;
2770
+ const body = {
2771
+ filename: data.filename,
2772
+ fileSize: data.fileSize,
2773
+ mimeType: data.mimeType,
2774
+ preProcessed: true
2775
+ };
2776
+ if (data.metadata) {
2777
+ body.metadata = data.metadata;
2778
+ }
2779
+ if (data.userMetadata) {
2780
+ body.userMetadata = data.userMetadata;
2781
+ }
2782
+ const response = await fetch(url, {
2783
+ method: HTTP_METHOD_POST,
2784
+ headers: {
2785
+ [HEADER_AUTHORIZATION]: `${BEARER_PREFIX}${data.apiKey}`,
2786
+ [HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON
2787
+ },
2788
+ body: JSON.stringify(body)
2789
+ });
2790
+ if (!response.ok) {
2791
+ const errorMessage = await this.extractErrorFromResponse(response, "Failed to initialize video upload");
2792
+ throw new Error(errorMessage);
2793
+ }
2794
+ return await response.json();
2795
+ }
2796
+ async extractErrorFromResponse(response, defaultMessage) {
2797
+ try {
2798
+ const errorData = await response.json();
2799
+ if (errorData.error && typeof errorData.error === "string") {
2800
+ return errorData.error;
2801
+ }
2802
+ } catch {}
2803
+ return `${defaultMessage}: ${response.status} ${response.statusText}`;
2804
+ }
2805
+ uploadVideoFile(blob, uploadUrl, options) {
2806
+ return new Promise((resolve, reject) => {
2807
+ const xhr = new XMLHttpRequest;
2808
+ if (options.onProgress) {
2809
+ const onProgress = options.onProgress;
2810
+ xhr.upload.addEventListener("progress", (event) => {
2811
+ if (event.lengthComputable) {
2812
+ const progress = event.loaded / event.total;
2813
+ onProgress(progress);
2814
+ }
2815
+ });
2816
+ }
2817
+ xhr.addEventListener("load", () => {
2818
+ if (xhr.status >= HTTP_STATUS_OK_MIN && xhr.status <= HTTP_STATUS_OK_MAX) {
2819
+ this.parseSuccessResponse(xhr, resolve, reject);
2820
+ } else {
2821
+ this.parseErrorResponse(xhr, reject);
2822
+ }
2823
+ });
2824
+ xhr.addEventListener("error", () => {
2825
+ reject(new Error("Network error during upload"));
2826
+ });
2827
+ xhr.addEventListener("abort", () => {
2828
+ reject(new Error("Upload was aborted"));
2829
+ });
2830
+ xhr.open(HTTP_METHOD_PUT, uploadUrl);
2831
+ xhr.setRequestHeader(HEADER_AUTHORIZATION, `${BEARER_PREFIX}${options.apiKey}`);
2832
+ xhr.setRequestHeader(HEADER_CONTENT_TYPE, blob.type);
2833
+ if (options.duration !== undefined) {
2834
+ xhr.setRequestHeader(HEADER_X_VIDEO_DURATION, options.duration.toString());
2835
+ }
2836
+ xhr.send(blob);
2837
+ });
2838
+ }
2839
+ parseSuccessResponse(xhr, resolve, reject) {
2840
+ try {
2841
+ const result = JSON.parse(xhr.responseText);
2842
+ resolve(result);
2843
+ } catch (error) {
2844
+ const errorMessage = extractErrorMessage(error);
2845
+ reject(new Error(`Failed to parse upload response: ${errorMessage}`));
2846
+ }
2847
+ }
2848
+ parseErrorResponse(xhr, reject) {
2849
+ let errorMessage = `Upload failed: ${xhr.status} ${xhr.statusText}`;
2850
+ try {
2851
+ const errorData = JSON.parse(xhr.responseText);
2852
+ if (errorData.error && typeof errorData.error === "string") {
2853
+ errorMessage = errorData.error;
2854
+ }
2855
+ } catch {}
2856
+ reject(new Error(errorMessage));
2857
+ }
2858
+ }
2859
+
2860
+ // src/core/recorder/callback-factory.ts
2861
+ var noop = () => {};
2862
+ function createRecordingCallbacks(callbacks, audioLevelAnalyzer, configManager) {
2863
+ const recording = callbacks.recording;
2864
+ return {
2865
+ onStateChange: recording?.onStateChange ?? noop,
2866
+ onCountdownUpdate: recording?.onCountdownUpdate ?? noop,
2867
+ onTimerUpdate: recording?.onTimerUpdate ?? noop,
2868
+ onError: recording?.onError ?? noop,
2869
+ onRecordingComplete: recording?.onRecordingComplete ?? noop,
2870
+ onClearUploadStatus: recording?.onClearUploadStatus ?? noop,
2871
+ onStopAudioTracking: () => {
2872
+ audioLevelAnalyzer.stopTracking();
2873
+ },
2874
+ onGetConfig: () => configManager.getConfig()
2875
+ };
2876
+ }
2877
+ function createSourceSwitchCallbacks(callbacks, deviceManager) {
2878
+ const sourceSwitch = callbacks.sourceSwitch;
2879
+ return {
2880
+ onSourceChange: sourceSwitch?.onSourceChange,
2881
+ onPreviewUpdate: sourceSwitch?.onPreviewUpdate,
2882
+ onError: sourceSwitch?.onError,
2883
+ onTransitionStart: sourceSwitch?.onTransitionStart,
2884
+ onTransitionEnd: sourceSwitch?.onTransitionEnd,
2885
+ getSelectedCameraDeviceId: () => deviceManager.getSelectedCameraDeviceId(),
2886
+ getSelectedMicDeviceId: () => deviceManager.getSelectedMicDeviceId()
2887
+ };
2888
+ }
2889
+
2890
+ // src/core/recorder/mute-state-manager.ts
2891
+ class MuteStateManager {
2892
+ isMuted = false;
2893
+ streamManager;
2894
+ constructor(streamManager) {
2895
+ this.streamManager = streamManager;
2896
+ }
2897
+ mute() {
2898
+ this.streamManager.muteAudio();
2899
+ this.isMuted = true;
2900
+ }
2901
+ unmute() {
2902
+ this.streamManager.unmuteAudio();
2903
+ this.isMuted = false;
2904
+ }
2905
+ toggle() {
2906
+ const currentMuted = this.streamManager.isMuted();
2907
+ if (currentMuted) {
2908
+ this.unmute();
2909
+ } else {
2910
+ this.mute();
2911
+ }
2912
+ }
2913
+ getIsMuted() {
2914
+ const streamMuted = this.streamManager.isMuted();
2915
+ if (this.isMuted !== streamMuted) {
2916
+ this.isMuted = streamMuted;
2917
+ }
2918
+ return this.isMuted;
2919
+ }
2920
+ getMutedStateCallback() {
2921
+ return () => {
2922
+ const muted = this.streamManager.isMuted();
2923
+ if (this.isMuted !== muted) {
2924
+ this.isMuted = muted;
2925
+ }
2926
+ return muted;
2927
+ };
2928
+ }
2929
+ }
2930
+
2931
+ // src/core/recorder/recorder-controller.ts
2932
+ function createDefaultUploadCallbacks() {
2933
+ return {
2934
+ onProgress: () => {},
2935
+ onSuccess: () => {},
2936
+ onError: () => {},
2937
+ onClearStatus: () => {}
2938
+ };
2939
+ }
2940
+
2941
+ class RecorderController {
2942
+ streamManager;
2943
+ configManager;
2944
+ storageManager;
2945
+ deviceManager;
2946
+ audioLevelAnalyzer;
2947
+ uploadManager;
2948
+ recordingManager;
2949
+ sourceSwitchManager;
2950
+ uploadService = null;
2951
+ muteStateManager;
2952
+ isInitialized = false;
2953
+ constructor(callbacks = {}) {
2954
+ this.streamManager = new CameraStreamManager;
2955
+ this.configManager = new ConfigManager;
2956
+ this.storageManager = new StorageManager;
2957
+ this.deviceManager = new DeviceManager(this.streamManager, callbacks.device);
2958
+ this.audioLevelAnalyzer = new AudioLevelAnalyzer;
2959
+ this.uploadService = new VideoUploadService;
2960
+ const uploadCallbacks = callbacks.upload ? callbacks.upload : createDefaultUploadCallbacks();
2961
+ this.uploadManager = new UploadManager(uploadCallbacks);
2962
+ const recordingCallbacks = createRecordingCallbacks(callbacks, this.audioLevelAnalyzer, this.configManager);
2963
+ this.recordingManager = new RecordingManager(this.streamManager, recordingCallbacks);
2964
+ const sourceSwitchCallbacks = createSourceSwitchCallbacks(callbacks, this.deviceManager);
2965
+ this.sourceSwitchManager = new SourceSwitchManager(this.streamManager, sourceSwitchCallbacks);
2966
+ this.muteStateManager = new MuteStateManager(this.streamManager);
2967
+ }
2968
+ async initialize(config) {
2969
+ if (this.isInitialized) {
2970
+ return;
2971
+ }
2972
+ if (config.apiKey && config.backendUrl) {
2973
+ this.configManager.initialize(config.apiKey, config.backendUrl);
2974
+ }
2975
+ if (config.countdownDuration !== undefined) {
2976
+ this.recordingManager.setCountdownDuration(config.countdownDuration);
2977
+ }
2978
+ if (config.maxRecordingTime !== undefined) {
2979
+ this.recordingManager.setMaxRecordingTime(config.maxRecordingTime);
2980
+ }
2981
+ await this.storageManager.initialize(this.uploadService, {
2982
+ onUploadProgress: (_id, progress) => {
2983
+ this.uploadManager.updateProgress(progress);
2984
+ },
2985
+ onUploadComplete: (_id, result) => {
2986
+ this.uploadManager.showSuccess(result);
2987
+ },
2988
+ onUploadError: (_id, error) => {
2989
+ this.uploadManager.showError(error.message);
2990
+ }
2991
+ }, () => {
2992
+ throw new Error("Storage cleanup error");
2993
+ });
2994
+ const uploadQueueManager = this.storageManager.getUploadQueueManager();
2995
+ this.uploadManager.setUploadQueueManager(uploadQueueManager);
2996
+ this.isInitialized = true;
2997
+ }
2998
+ async startStream() {
2999
+ await this.streamManager.startStream();
3000
+ }
3001
+ async stopStream() {
3002
+ await this.streamManager.stopStream();
3003
+ }
3004
+ async switchVideoDevice(deviceId) {
3005
+ return await this.streamManager.switchVideoDevice(deviceId);
3006
+ }
3007
+ async switchAudioDevice(deviceId) {
3008
+ return await this.streamManager.switchAudioDevice(deviceId);
3009
+ }
3010
+ async startRecording() {
3011
+ await this.recordingManager.startRecording();
3012
+ }
3013
+ async stopRecording() {
3014
+ const blob = await this.recordingManager.stopRecording();
3015
+ await this.sourceSwitchManager.handleRecordingStop().catch(() => {
3016
+ throw new Error("Source switch cleanup failed");
3017
+ });
3018
+ return blob;
3019
+ }
3020
+ pauseRecording() {
3021
+ this.recordingManager.pauseRecording();
3022
+ }
3023
+ resumeRecording() {
3024
+ this.recordingManager.resumeRecording();
3025
+ }
3026
+ async switchSource(_sourceType) {
3027
+ await this.sourceSwitchManager.toggleSource();
3028
+ }
3029
+ setCameraDevice(deviceId) {
3030
+ this.deviceManager.setCameraDevice(deviceId);
3031
+ }
3032
+ setMicDevice(deviceId) {
3033
+ this.deviceManager.setMicDevice(deviceId);
3034
+ }
3035
+ async getAvailableDevices() {
3036
+ return await this.deviceManager.getAvailableDevices();
3037
+ }
3038
+ muteAudio() {
3039
+ this.muteStateManager.mute();
3040
+ }
3041
+ unmuteAudio() {
3042
+ this.muteStateManager.unmute();
3043
+ }
3044
+ toggleMute() {
3045
+ this.muteStateManager.toggle();
3046
+ }
3047
+ getIsMuted() {
3048
+ return this.muteStateManager.getIsMuted();
3049
+ }
3050
+ startAudioLevelTracking(stream, callbacks) {
3051
+ if (!callbacks) {
3052
+ throw new Error("Audio level callbacks are required");
3053
+ }
3054
+ this.audioLevelAnalyzer.startTracking(stream, callbacks, this.muteStateManager.getMutedStateCallback());
3055
+ return Promise.resolve();
3056
+ }
3057
+ stopAudioLevelTracking() {
3058
+ this.audioLevelAnalyzer.stopTracking();
3059
+ }
3060
+ getAudioLevel() {
3061
+ return this.audioLevelAnalyzer.getAudioLevel();
3062
+ }
3063
+ async uploadVideo(blob, apiKey, backendUrl, metadata) {
3064
+ await this.uploadManager.uploadVideo(blob, apiKey, backendUrl, metadata);
3065
+ }
3066
+ getStream() {
3067
+ return this.streamManager.getStream();
3068
+ }
3069
+ getRecordingState() {
3070
+ return this.recordingManager.getRecordingState();
3071
+ }
3072
+ isPaused() {
3073
+ return this.recordingManager.isPausedState();
3074
+ }
3075
+ getCurrentSourceType() {
3076
+ return this.sourceSwitchManager.getCurrentSourceType();
3077
+ }
3078
+ getOriginalCameraStream() {
3079
+ return this.sourceSwitchManager.getOriginalCameraStream();
3080
+ }
3081
+ getStreamManager() {
3082
+ return this.streamManager;
3083
+ }
3084
+ getAudioStreamForAnalysis() {
3085
+ return this.streamManager.getAudioStreamForAnalysis();
3086
+ }
3087
+ getDeviceManager() {
3088
+ return this.deviceManager;
3089
+ }
3090
+ async getConfig() {
3091
+ return await this.configManager.getConfig();
3092
+ }
3093
+ isRecording() {
3094
+ return this.streamManager.isRecording();
3095
+ }
3096
+ isActive() {
3097
+ return this.streamManager.isActive();
3098
+ }
3099
+ cleanup() {
3100
+ this.storageManager.destroy();
3101
+ this.recordingManager.cleanup();
3102
+ this.audioLevelAnalyzer.stopTracking();
3103
+ this.sourceSwitchManager.cleanup();
3104
+ }
3105
+ }
3106
+ // src/core/storage/quota-manager.ts
3107
+ var PERCENTAGE_MULTIPLIER = 100;
3108
+ var DEFAULT_WARNING_THRESHOLD = 80;
3109
+ var DEFAULT_CRITICAL_THRESHOLD = 95;
3110
+
3111
+ class QuotaManager {
3112
+ async getQuota() {
3113
+ if (!(("storage" in navigator) && ("estimate" in navigator.storage))) {
3114
+ throw new Error("Storage API not supported");
3115
+ }
3116
+ const estimate = await navigator.storage.estimate();
3117
+ if (estimate.usage === undefined) {
3118
+ throw new Error("Storage usage estimate not available");
3119
+ }
3120
+ if (estimate.quota === undefined) {
3121
+ throw new Error("Storage quota estimate not available");
3122
+ }
3123
+ const usage = estimate.usage;
3124
+ const quota = estimate.quota;
3125
+ const available = quota - usage;
3126
+ const percentage = quota > 0 ? usage / quota * PERCENTAGE_MULTIPLIER : 0;
3127
+ return {
3128
+ usage,
3129
+ quota,
3130
+ available,
3131
+ percentage
3132
+ };
3133
+ }
3134
+ async hasSpaceFor(sizeInBytes) {
3135
+ if (typeof sizeInBytes !== "number" || sizeInBytes < 0) {
3136
+ throw new Error("sizeInBytes must be a non-negative number");
3137
+ }
3138
+ const quota = await this.getQuota();
3139
+ return quota.available >= sizeInBytes;
3140
+ }
3141
+ async requestPersistentStorage() {
3142
+ if (!(("storage" in navigator) && ("persist" in navigator.storage))) {
3143
+ throw new Error("Persistent storage API not supported");
3144
+ }
3145
+ return await navigator.storage.persist();
3146
+ }
3147
+ async isPersistent() {
3148
+ if (!(("storage" in navigator) && ("persisted" in navigator.storage))) {
3149
+ throw new Error("Persistent storage check API not supported");
3150
+ }
3151
+ return await navigator.storage.persisted();
3152
+ }
3153
+ formatBytes(bytes) {
3154
+ if (typeof bytes !== "number" || bytes < 0) {
3155
+ throw new Error("bytes must be a non-negative number");
3156
+ }
3157
+ return formatFileSize(bytes);
3158
+ }
3159
+ async shouldWarn(threshold) {
3160
+ const actualThreshold = threshold !== undefined ? threshold : DEFAULT_WARNING_THRESHOLD;
3161
+ return await this.checkThreshold(actualThreshold);
3162
+ }
3163
+ async isCritical(threshold) {
3164
+ const actualThreshold = threshold !== undefined ? threshold : DEFAULT_CRITICAL_THRESHOLD;
3165
+ return await this.checkThreshold(actualThreshold);
3166
+ }
3167
+ async checkThreshold(threshold) {
3168
+ if (typeof threshold !== "number" || threshold < 0 || threshold > 100) {
3169
+ throw new Error("threshold must be a number between 0 and 100");
3170
+ }
3171
+ const quota = await this.getQuota();
3172
+ return quota.percentage >= threshold;
3173
+ }
3174
+ }
3175
+ // src/core/utils/audio-utils.ts
3176
+ function calculateBarColor(position) {
3177
+ if (position < 0.25) {
3178
+ const t2 = position / 0.25;
3179
+ return `rgb(255, ${Math.round(165 + (215 - 165) * t2)}, 0)`;
3180
+ }
3181
+ if (position < 0.5) {
3182
+ const t2 = (position - 0.25) / 0.25;
3183
+ return `rgb(${Math.round(255 - (50 - 255) * t2)}, ${Math.round(215 + (205 - 215) * t2)}, ${Math.round(0 + (50 - 0) * t2)})`;
3184
+ }
3185
+ if (position < 0.75) {
3186
+ const t2 = (position - 0.5) / 0.25;
3187
+ return `rgb(${Math.round(50 - (0 - 50) * t2)}, ${Math.round(205 + (128 - 205) * t2)}, ${Math.round(50 + (128 - 50) * t2)})`;
3188
+ }
3189
+ const t = (position - 0.75) / 0.25;
3190
+ return `rgb(0, ${Math.round(128 - (100 - 128) * t)}, ${Math.round(128 + (200 - 128) * t)})`;
3191
+ }
3192
+ export {
3193
+ transcodeVideo,
3194
+ mapPresetToConfig,
3195
+ formatTime,
3196
+ formatFileSize,
3197
+ extractVideoDuration,
3198
+ extractErrorMessage,
3199
+ calculateBarColor,
3200
+ VideoUploadService,
3201
+ VideoStorageService,
3202
+ UploadQueueManager,
3203
+ UploadManager,
3204
+ StreamProcessor,
3205
+ StorageManager,
3206
+ RecordingManager,
3207
+ RecorderController,
3208
+ QuotaManager,
3209
+ DeviceManager,
3210
+ DEFAULT_TRANSCODE_CONFIG,
3211
+ DEFAULT_STREAM_CONFIG,
3212
+ DEFAULT_RECORDING_OPTIONS,
3213
+ DEFAULT_CAMERA_CONSTRAINTS,
3214
+ ConfigService,
3215
+ ConfigManager,
3216
+ CameraStreamManager,
3217
+ AudioLevelAnalyzer
3218
+ };