@vidtreo/recorder 0.0.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -179,13 +179,17 @@ function mapPresetToConfig(preset, maxWidth, maxHeight) {
179
179
  // src/core/config/config-service.ts
180
180
  var DEFAULT_CACHE_TIMEOUT = 5 * 60 * 1000;
181
181
  var CONFIG_API_PATH = "/api/v1/videos/config";
182
+ var serviceInstances = new Map;
183
+ function getInstanceKey(backendUrl, apiKey) {
184
+ return `${backendUrl}:${apiKey}`;
185
+ }
182
186
 
183
187
  class ConfigService {
188
+ cacheTimeout;
189
+ options;
184
190
  cachedConfig = null;
185
191
  cacheTimestamp = 0;
186
- cacheTimeout;
187
192
  fetchPromise = null;
188
- options;
189
193
  constructor(options) {
190
194
  this.options = options;
191
195
  if (options.cacheTimeout !== undefined) {
@@ -197,6 +201,15 @@ class ConfigService {
197
201
  this.cacheTimeout = DEFAULT_CACHE_TIMEOUT;
198
202
  }
199
203
  }
204
+ static getInstance(options) {
205
+ const key = getInstanceKey(options.backendUrl, options.apiKey);
206
+ let instance = serviceInstances.get(key);
207
+ if (!instance) {
208
+ instance = new ConfigService(options);
209
+ serviceInstances.set(key, instance);
210
+ }
211
+ return instance;
212
+ }
200
213
  async fetchConfig() {
201
214
  const now = Date.now();
202
215
  if (this.cachedConfig && now - this.cacheTimestamp < this.cacheTimeout) {
@@ -210,16 +223,22 @@ class ConfigService {
210
223
  const config = await this.fetchPromise;
211
224
  this.cachedConfig = config;
212
225
  this.cacheTimestamp = now;
226
+ this.fetchPromise = null;
213
227
  return config;
214
228
  } catch {
215
- return DEFAULT_TRANSCODE_CONFIG;
216
- } finally {
217
229
  this.fetchPromise = null;
230
+ return DEFAULT_TRANSCODE_CONFIG;
218
231
  }
219
232
  }
220
233
  clearCache() {
234
+ const key = getInstanceKey(this.options.backendUrl, this.options.apiKey);
221
235
  this.cachedConfig = null;
222
236
  this.cacheTimestamp = 0;
237
+ this.fetchPromise = null;
238
+ serviceInstances.delete(key);
239
+ }
240
+ static clearAllInstances() {
241
+ serviceInstances.clear();
223
242
  }
224
243
  getCurrentConfig() {
225
244
  if (!this.cachedConfig) {
@@ -251,35 +270,30 @@ class ConfigService {
251
270
  class ConfigManager {
252
271
  configService = null;
253
272
  currentConfig = DEFAULT_TRANSCODE_CONFIG;
254
- configFetchPromise = null;
255
273
  initialize(apiKey, backendUrl) {
274
+ if (this.configService) {
275
+ return;
276
+ }
256
277
  if (!apiKey) {
257
278
  throw new Error("apiKey is required");
258
279
  }
259
280
  if (!backendUrl) {
260
281
  throw new Error("backendUrl is required");
261
282
  }
262
- this.configService = new ConfigService({
263
- apiKey,
264
- backendUrl
283
+ this.configService = ConfigService.getInstance({ apiKey, backendUrl });
284
+ this.configService.fetchConfig().then((config) => {
285
+ this.currentConfig = config;
265
286
  });
266
- this.fetchConfig();
267
287
  }
268
288
  async fetchConfig() {
269
289
  if (!this.configService) {
270
290
  return;
271
291
  }
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;
292
+ this.currentConfig = await this.configService.fetchConfig();
279
293
  }
280
294
  async getConfig() {
281
- if (this.configService && !this.configFetchPromise) {
282
- await this.fetchConfig();
295
+ if (this.configService) {
296
+ this.currentConfig = await this.configService.fetchConfig();
283
297
  }
284
298
  return this.currentConfig;
285
299
  }
@@ -415,6 +429,69 @@ import { CanvasSource } from "mediabunny";
415
429
  // src/core/processor/audio-track-manager.ts
416
430
  import { AudioSample, AudioSampleSource } from "mediabunny";
417
431
 
432
+ // src/core/processor/audio-capture-processor.worklet.ts
433
+ var workletCode = `class AudioCaptureProcessor extends AudioWorkletProcessor {
434
+ process(inputs, outputs) {
435
+ const input = inputs[0];
436
+
437
+ if (!input?.length) {
438
+ return true;
439
+ }
440
+
441
+ const bufferLength = input[0].length;
442
+ const numberOfChannels = input.length;
443
+ const combinedBuffer = new Float32Array(bufferLength * numberOfChannels);
444
+
445
+ for (let channel = 0; channel < numberOfChannels; channel++) {
446
+ combinedBuffer.set(input[channel], channel * bufferLength);
447
+ }
448
+
449
+ const arrayBuffer = combinedBuffer.buffer.slice(
450
+ 0,
451
+ combinedBuffer.length * Float32Array.BYTES_PER_ELEMENT
452
+ );
453
+
454
+ this.port.postMessage(
455
+ {
456
+ type: "audioData",
457
+ data: arrayBuffer,
458
+ sampleRate: sampleRate,
459
+ numberOfChannels,
460
+ duration: bufferLength / sampleRate,
461
+ bufferLength: combinedBuffer.length,
462
+ },
463
+ [arrayBuffer]
464
+ );
465
+
466
+ if (outputs && outputs[0] && outputs[0].length > 0) {
467
+ for (let channel = 0; channel < numberOfChannels; channel++) {
468
+ if (outputs[0][channel] && input[channel]) {
469
+ outputs[0][channel].set(input[channel]);
470
+ }
471
+ }
472
+ }
473
+
474
+ return true;
475
+ }
476
+ }
477
+
478
+ registerProcessor("audio-capture-processor", AudioCaptureProcessor);
479
+ `;
480
+
481
+ // src/core/processor/audio-track-manager.ts
482
+ var workletBlobUrl = null;
483
+ function getWorkletUrl() {
484
+ if (workletBlobUrl) {
485
+ return workletBlobUrl;
486
+ }
487
+ if (typeof Blob === "undefined" || typeof URL === "undefined") {
488
+ throw new Error("Blob and URL APIs are required for worklet loading");
489
+ }
490
+ const blob = new Blob([workletCode], { type: "application/javascript" });
491
+ workletBlobUrl = URL.createObjectURL(blob);
492
+ return workletBlobUrl;
493
+ }
494
+
418
495
  class AudioTrackManager {
419
496
  originalAudioTrack = null;
420
497
  audioContext = null;
@@ -427,7 +504,14 @@ class AudioTrackManager {
427
504
  isPaused = false;
428
505
  onMuteStateChange;
429
506
  async setupAudioTrack(stream, config) {
430
- const audioTrack = stream.getAudioTracks()[0];
507
+ if (!stream) {
508
+ return null;
509
+ }
510
+ const audioTracks = stream.getAudioTracks();
511
+ if (audioTracks.length === 0) {
512
+ return null;
513
+ }
514
+ const audioTrack = audioTracks[0];
431
515
  if (!audioTrack) {
432
516
  return null;
433
517
  }
@@ -435,11 +519,22 @@ class AudioTrackManager {
435
519
  this.lastAudioTimestamp = 0;
436
520
  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
437
521
  if (!AudioContextClass) {
438
- throw new Error("AudioContext is not supported in this browser");
522
+ return null;
439
523
  }
440
- this.audioContext = new AudioContextClass;
441
524
  if (config.audioBitrate === undefined || config.audioBitrate === null) {
442
- throw new Error("audioBitrate is required in config");
525
+ return null;
526
+ }
527
+ if (!config.audioCodec) {
528
+ return null;
529
+ }
530
+ this.audioContext = new AudioContextClass;
531
+ if (this.audioContext.state === "suspended") {
532
+ await this.audioContext.resume();
533
+ }
534
+ if (!this.audioContext.audioWorklet) {
535
+ await this.audioContext.close();
536
+ this.audioContext = null;
537
+ return null;
443
538
  }
444
539
  const audioBitrate = config.audioBitrate;
445
540
  const audioCodec = config.audioCodec;
@@ -447,8 +542,9 @@ class AudioTrackManager {
447
542
  codec: audioCodec,
448
543
  bitrate: audioBitrate
449
544
  });
450
- this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
451
- const processorUrl = new URL("./audio-capture-processor.worklet.js", import.meta.url).href;
545
+ const audioOnlyStream = new MediaStream([this.originalAudioTrack]);
546
+ this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioOnlyStream);
547
+ const processorUrl = getWorkletUrl();
452
548
  await this.audioContext.audioWorklet.addModule(processorUrl);
453
549
  this.audioWorkletNode = new AudioWorkletNode(this.audioContext, "audio-capture-processor");
454
550
  this.audioWorkletNode.port.onmessage = (event) => {
@@ -472,6 +568,9 @@ class AudioTrackManager {
472
568
  this.mediaStreamSource.connect(this.audioWorkletNode);
473
569
  this.audioWorkletNode.connect(this.gainNode);
474
570
  this.gainNode.connect(this.audioContext.destination);
571
+ if (this.audioContext.state === "suspended") {
572
+ await this.audioContext.resume();
573
+ }
475
574
  return this.audioSource;
476
575
  }
477
576
  toggleMute() {
@@ -524,6 +623,7 @@ class AudioTrackManager {
524
623
  this.onMuteStateChange = callback;
525
624
  }
526
625
  cleanup() {
626
+ this.audioSource = null;
527
627
  if (this.audioWorkletNode) {
528
628
  this.audioWorkletNode.disconnect();
529
629
  this.audioWorkletNode.port.close();
@@ -545,7 +645,10 @@ class AudioTrackManager {
545
645
  this.originalAudioTrack.stop();
546
646
  this.originalAudioTrack = null;
547
647
  }
548
- this.audioSource = null;
648
+ if (workletBlobUrl) {
649
+ URL.revokeObjectURL(workletBlobUrl);
650
+ workletBlobUrl = null;
651
+ }
549
652
  this.lastAudioTimestamp = 0;
550
653
  }
551
654
  }
@@ -644,6 +747,10 @@ class FrameCapturer {
644
747
  return this.lastFrameTimestamp;
645
748
  }
646
749
  async waitForPendingFrames() {
750
+ if (!this.isActive && this.pendingFrameAdd) {
751
+ this.pendingFrameAdd = null;
752
+ return;
753
+ }
647
754
  if (this.pendingFrameAdd) {
648
755
  await this.pendingFrameAdd;
649
756
  }
@@ -654,6 +761,7 @@ class FrameCapturer {
654
761
  window.clearTimeout(this.timeoutId);
655
762
  this.timeoutId = null;
656
763
  }
764
+ this.pendingFrameAdd = null;
657
765
  }
658
766
  captureFrame() {
659
767
  if (!this.canCaptureFrame()) {
@@ -1717,14 +1825,19 @@ class SourceSwitchManager {
1717
1825
  getOriginalCameraStream() {
1718
1826
  return this.originalCameraStream;
1719
1827
  }
1720
- stopStreamTracks(stream) {
1721
- const tracks = stream.getTracks();
1828
+ stopLiveTracks(tracks) {
1722
1829
  for (const track of tracks) {
1723
1830
  if (track.readyState === TRACK_READY_STATE_LIVE) {
1724
1831
  track.stop();
1725
1832
  }
1726
1833
  }
1727
1834
  }
1835
+ stopStreamTracks(stream) {
1836
+ this.stopLiveTracks(stream.getTracks());
1837
+ }
1838
+ stopStreamVideoTracks(stream) {
1839
+ this.stopLiveTracks(stream.getVideoTracks());
1840
+ }
1728
1841
  isTrackLive(track) {
1729
1842
  return track !== undefined && track.readyState === TRACK_READY_STATE_LIVE;
1730
1843
  }
@@ -1769,6 +1882,14 @@ class SourceSwitchManager {
1769
1882
  }, delay);
1770
1883
  });
1771
1884
  }
1885
+ combineScreenShareWithOriginalAudio(screenVideoTrack) {
1886
+ const originalAudioTrack = this.originalCameraStream ? this.originalCameraStream.getAudioTracks()[0] : undefined;
1887
+ const combinedTracks = [screenVideoTrack];
1888
+ if (this.isTrackLive(originalAudioTrack)) {
1889
+ combinedTracks.push(originalAudioTrack);
1890
+ }
1891
+ return new MediaStream(combinedTracks);
1892
+ }
1772
1893
  async switchToScreenCapture() {
1773
1894
  const currentStream = this.streamManager.getStream();
1774
1895
  if (currentStream) {
@@ -1779,24 +1900,39 @@ class SourceSwitchManager {
1779
1900
  this.callbacks.onTransitionStart("Select screen to share...");
1780
1901
  }
1781
1902
  try {
1782
- const newStream = await navigator.mediaDevices.getDisplayMedia({
1903
+ const screenShareStream = await navigator.mediaDevices.getDisplayMedia({
1783
1904
  video: true,
1784
1905
  audio: true
1785
1906
  });
1786
- this.screenShareStream = newStream;
1907
+ this.screenShareStream = screenShareStream;
1908
+ const screenVideoTrack = screenShareStream.getVideoTracks()[0];
1909
+ if (!screenVideoTrack) {
1910
+ this.stopStreamTracks(screenShareStream);
1911
+ throw new Error("No video track found in screen share stream");
1912
+ }
1913
+ const combinedStream = this.combineScreenShareWithOriginalAudio(screenVideoTrack);
1787
1914
  if (currentStream && currentStream !== this.originalCameraStream) {
1788
- this.stopStreamTracks(currentStream);
1915
+ this.stopStreamVideoTracks(currentStream);
1916
+ }
1917
+ const screenAudioTracks = screenShareStream.getAudioTracks();
1918
+ for (const track of screenAudioTracks) {
1919
+ track.stop();
1789
1920
  }
1790
1921
  this.currentSourceType = "screen";
1791
1922
  if (this.callbacks.onSourceChange) {
1792
1923
  this.callbacks.onSourceChange(this.currentSourceType);
1793
1924
  }
1794
- this.setupScreenShareTrackHandler(newStream);
1795
- return newStream;
1925
+ this.setupScreenShareTrackHandler(combinedStream);
1926
+ return combinedStream;
1796
1927
  } catch (error) {
1797
1928
  if (this.callbacks.onTransitionEnd) {
1798
1929
  this.callbacks.onTransitionEnd();
1799
1930
  }
1931
+ const errorMessage = extractErrorMessage(error);
1932
+ const isPermissionDenied = errorMessage.includes("NotAllowedError") || errorMessage.includes("AbortError") || errorMessage.toLowerCase().includes("permission denied") || errorMessage.toLowerCase().includes("user denied");
1933
+ if (isPermissionDenied) {
1934
+ return null;
1935
+ }
1800
1936
  throw error;
1801
1937
  }
1802
1938
  }
@@ -1838,30 +1974,62 @@ class SourceSwitchManager {
1838
1974
  }
1839
1975
  this.screenShareTrackEndHandler = null;
1840
1976
  }
1841
- canReuseOriginalStream() {
1842
- if (!this.originalCameraStream) {
1977
+ canReuseStream(stream, mustMatchOriginal) {
1978
+ if (!stream) {
1843
1979
  return false;
1844
1980
  }
1845
- const videoTrack = this.originalCameraStream.getVideoTracks()[0];
1846
- const audioTrack = this.originalCameraStream.getAudioTracks()[0];
1847
- return this.areTracksLive(videoTrack, audioTrack);
1981
+ if (mustMatchOriginal && stream !== this.originalCameraStream) {
1982
+ return false;
1983
+ }
1984
+ const videoTrack = stream.getVideoTracks()[0];
1985
+ const audioTrack = stream.getAudioTracks()[0];
1986
+ if (!this.areTracksLive(videoTrack, audioTrack)) {
1987
+ return false;
1988
+ }
1989
+ if (this.callbacks.getSelectedCameraDeviceId) {
1990
+ const selectedCameraDeviceId = this.callbacks.getSelectedCameraDeviceId();
1991
+ const streamDeviceId = videoTrack.getSettings().deviceId;
1992
+ if (selectedCameraDeviceId !== streamDeviceId) {
1993
+ return false;
1994
+ }
1995
+ }
1996
+ return true;
1997
+ }
1998
+ canReuseOriginalStream() {
1999
+ return this.canReuseStream(this.originalCameraStream, false);
1848
2000
  }
1849
2001
  canReuseManagerStream() {
1850
2002
  const managerStream = this.streamManager.getStream();
1851
- if (!(managerStream && this.originalCameraStream) || managerStream !== this.originalCameraStream) {
2003
+ if (!(managerStream && this.originalCameraStream)) {
1852
2004
  return false;
1853
2005
  }
1854
- const videoTrack = managerStream.getVideoTracks()[0];
1855
- const audioTrack = managerStream.getAudioTracks()[0];
1856
- return this.areTracksLive(videoTrack, audioTrack);
2006
+ return this.canReuseStream(managerStream, true);
2007
+ }
2008
+ getSelectedCameraDeviceId() {
2009
+ if (this.callbacks.getSelectedCameraDeviceId) {
2010
+ return this.callbacks.getSelectedCameraDeviceId();
2011
+ }
2012
+ return this.streamManager.getVideoDevice();
2013
+ }
2014
+ getSelectedMicDeviceId() {
2015
+ if (this.callbacks.getSelectedMicDeviceId) {
2016
+ return this.callbacks.getSelectedMicDeviceId();
2017
+ }
2018
+ return this.streamManager.getAudioDevice();
1857
2019
  }
1858
2020
  buildVideoConstraints(cameraDeviceId) {
1859
2021
  const constraints = {};
2022
+ if (this.originalCameraConstraints) {
2023
+ const { deviceId: _, ...originalConstraintsWithoutDeviceId } = this.originalCameraConstraints;
2024
+ Object.assign(constraints, originalConstraintsWithoutDeviceId);
2025
+ }
1860
2026
  if (cameraDeviceId) {
1861
2027
  constraints.deviceId = { exact: cameraDeviceId };
1862
- }
1863
- if (this.originalCameraConstraints) {
1864
- Object.assign(constraints, this.originalCameraConstraints);
2028
+ } else if (!constraints.deviceId) {
2029
+ const selectedDeviceId = this.getSelectedCameraDeviceId();
2030
+ if (selectedDeviceId) {
2031
+ constraints.deviceId = { exact: selectedDeviceId };
2032
+ }
1865
2033
  }
1866
2034
  return constraints;
1867
2035
  }
@@ -1871,9 +2039,35 @@ class SourceSwitchManager {
1871
2039
  }
1872
2040
  return true;
1873
2041
  }
1874
- async createNewCameraStreamForRecording() {
1875
- const cameraDeviceId = this.callbacks.getSelectedCameraDeviceId ? this.callbacks.getSelectedCameraDeviceId() : null;
1876
- const micDeviceId = this.callbacks.getSelectedMicDeviceId ? this.callbacks.getSelectedMicDeviceId() : null;
2042
+ validateTrack(track, trackType, stream) {
2043
+ if (!this.isTrackLive(track)) {
2044
+ this.stopStreamTracks(stream);
2045
+ const readyState = track ? track.readyState : "undefined";
2046
+ throw new Error(`Failed to get live camera ${trackType} track. ReadyState: ${readyState}`);
2047
+ }
2048
+ }
2049
+ async createCameraStreamWithOriginalAudio(cameraDeviceId) {
2050
+ const originalAudioTrack = this.originalCameraStream ? this.originalCameraStream.getAudioTracks()[0] : undefined;
2051
+ if (!this.isTrackLive(originalAudioTrack)) {
2052
+ return null;
2053
+ }
2054
+ const videoConstraints = this.buildVideoConstraints(cameraDeviceId);
2055
+ const constraints = {
2056
+ video: Object.keys(videoConstraints).length > 0 ? videoConstraints : true,
2057
+ audio: false
2058
+ };
2059
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
2060
+ const videoTrack = newStream.getVideoTracks()[0];
2061
+ this.validateTrack(videoTrack, "video", newStream);
2062
+ const combinedTracks = [videoTrack];
2063
+ combinedTracks.push(originalAudioTrack);
2064
+ const combinedStream = new MediaStream(combinedTracks);
2065
+ this.stopLiveTracks(newStream.getAudioTracks());
2066
+ this.originalCameraStream = combinedStream;
2067
+ return combinedStream;
2068
+ }
2069
+ async createCameraStreamWithNewAudio(cameraDeviceId) {
2070
+ const micDeviceId = this.getSelectedMicDeviceId();
1877
2071
  const videoConstraints = this.buildVideoConstraints(cameraDeviceId);
1878
2072
  const audioConstraints = this.buildAudioConstraints(micDeviceId);
1879
2073
  const constraints = {
@@ -1883,27 +2077,30 @@ class SourceSwitchManager {
1883
2077
  const newStream = await navigator.mediaDevices.getUserMedia(constraints);
1884
2078
  const videoTrack = newStream.getVideoTracks()[0];
1885
2079
  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
- }
2080
+ this.validateTrack(videoTrack, "video", newStream);
2081
+ this.validateTrack(audioTrack, "audio", newStream);
1896
2082
  this.originalCameraStream = newStream;
1897
2083
  return newStream;
1898
2084
  }
2085
+ async createNewCameraStreamForRecording() {
2086
+ const cameraDeviceId = this.getSelectedCameraDeviceId();
2087
+ const streamWithOriginalAudio = await this.createCameraStreamWithOriginalAudio(cameraDeviceId);
2088
+ if (streamWithOriginalAudio) {
2089
+ return streamWithOriginalAudio;
2090
+ }
2091
+ return this.createCameraStreamWithNewAudio(cameraDeviceId);
2092
+ }
1899
2093
  async getCameraStream() {
1900
2094
  const isRecording = this.streamManager.isRecording();
2095
+ const cameraDeviceId = this.getSelectedCameraDeviceId();
2096
+ const micDeviceId = this.getSelectedMicDeviceId();
2097
+ this.streamManager.setVideoDevice(cameraDeviceId);
2098
+ this.streamManager.setAudioDevice(micDeviceId);
1901
2099
  if (this.canReuseOriginalStream()) {
1902
- const stream = this.originalCameraStream;
1903
- if (!stream) {
2100
+ if (!this.originalCameraStream) {
1904
2101
  throw new Error("Original camera stream is null");
1905
2102
  }
1906
- return stream;
2103
+ return this.originalCameraStream;
1907
2104
  }
1908
2105
  if (this.canReuseManagerStream()) {
1909
2106
  const stream = this.streamManager.getStream();
@@ -1920,15 +2117,9 @@ class SourceSwitchManager {
1920
2117
  this.stopStreamTracks(managerStream);
1921
2118
  this.streamManager.setMediaStream(null);
1922
2119
  }
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
2120
  if (isRecording) {
2121
+ this.streamManager.setVideoDevice(this.getSelectedCameraDeviceId());
2122
+ this.streamManager.setAudioDevice(this.getSelectedMicDeviceId());
1932
2123
  return this.createNewCameraStreamForRecording();
1933
2124
  }
1934
2125
  const newStream = await this.streamManager.startStream();
@@ -1964,6 +2155,10 @@ class SourceSwitchManager {
1964
2155
  this.callbacks.onTransitionEnd();
1965
2156
  }
1966
2157
  }
2158
+ stopScreenShareStreamTracks(stream) {
2159
+ this.stopLiveTracks(stream.getVideoTracks());
2160
+ this.stopLiveTracks(stream.getAudioTracks());
2161
+ }
1967
2162
  async handleScreenShareStop() {
1968
2163
  if (this.currentSourceType !== "screen") {
1969
2164
  return;
@@ -1972,11 +2167,11 @@ class SourceSwitchManager {
1972
2167
  const currentStream = this.streamManager.getStream();
1973
2168
  if (screenShareStreamToStop) {
1974
2169
  this.removeScreenShareTrackHandler(screenShareStreamToStop);
1975
- this.stopStreamTracks(screenShareStreamToStop);
2170
+ this.stopScreenShareStreamTracks(screenShareStreamToStop);
1976
2171
  await this.waitForTracksToEnd(SCREEN_SHARE_TRANSITION_DELAY);
1977
2172
  }
1978
2173
  if (currentStream && (!screenShareStreamToStop || currentStream.id !== screenShareStreamToStop.id)) {
1979
- this.stopStreamTracks(currentStream);
2174
+ this.stopStreamVideoTracks(currentStream);
1980
2175
  }
1981
2176
  }
1982
2177
  async applyCameraStream(newStream, isRecording) {
@@ -2009,6 +2204,10 @@ class SourceSwitchManager {
2009
2204
  }
2010
2205
  async switchToScreen() {
2011
2206
  const newStream = await this.switchToScreenCapture();
2207
+ if (!newStream) {
2208
+ this.notifyTransitionEnd();
2209
+ return;
2210
+ }
2012
2211
  this.notifyTransitionStart("Switching to screen...");
2013
2212
  await this.streamManager.switchVideoSource(newStream);
2014
2213
  if (this.callbacks.onPreviewUpdate) {
@@ -2040,7 +2239,7 @@ class SourceSwitchManager {
2040
2239
  const currentStream = this.streamManager.getStream();
2041
2240
  if (currentStream) {
2042
2241
  this.removeScreenShareTrackHandler(currentStream);
2043
- this.stopStreamTracks(currentStream);
2242
+ this.stopStreamVideoTracks(currentStream);
2044
2243
  }
2045
2244
  const newStream = await this.getCameraStream();
2046
2245
  if (!newStream) {
@@ -2065,7 +2264,7 @@ class SourceSwitchManager {
2065
2264
  cleanup() {
2066
2265
  if (this.screenShareStream) {
2067
2266
  this.removeScreenShareTrackHandler(this.screenShareStream);
2068
- this.stopStreamTracks(this.screenShareStream);
2267
+ this.stopScreenShareStreamTracks(this.screenShareStream);
2069
2268
  this.screenShareStream = null;
2070
2269
  }
2071
2270
  const currentStream = this.streamManager.getStream();
@@ -2220,13 +2419,13 @@ class CameraStreamManager {
2220
2419
  throw new Error(`Failed to enumerate devices: ${extractErrorMessage(error)}`);
2221
2420
  }
2222
2421
  }
2223
- buildVideoConstraints(deviceId) {
2422
+ buildDeviceConstraints(deviceId, config) {
2224
2423
  if (!deviceId) {
2225
- return this.streamConfig.video;
2424
+ return config;
2226
2425
  }
2227
- if (typeof this.streamConfig.video === "object") {
2426
+ if (typeof config === "object") {
2228
2427
  return {
2229
- ...this.streamConfig.video,
2428
+ ...config,
2230
2429
  deviceId: { exact: deviceId }
2231
2430
  };
2232
2431
  }
@@ -2234,23 +2433,30 @@ class CameraStreamManager {
2234
2433
  deviceId: { exact: deviceId }
2235
2434
  };
2236
2435
  }
2436
+ buildVideoConstraints(deviceId) {
2437
+ return this.buildDeviceConstraints(deviceId, this.streamConfig.video);
2438
+ }
2237
2439
  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
- };
2440
+ return this.buildDeviceConstraints(deviceId, this.streamConfig.audio);
2250
2441
  }
2251
2442
  async startStream() {
2252
2443
  if (this.mediaStream) {
2253
- return this.mediaStream;
2444
+ const videoTrack = this.mediaStream.getVideoTracks()[0];
2445
+ if (this.selectedVideoDeviceId === null) {
2446
+ const existingDeviceId = videoTrack?.getSettings?.()?.deviceId;
2447
+ if (existingDeviceId) {
2448
+ this.stopStream();
2449
+ } else {
2450
+ return this.mediaStream;
2451
+ }
2452
+ }
2453
+ if (videoTrack?.getSettings) {
2454
+ const trackDeviceId = videoTrack.getSettings().deviceId;
2455
+ if (trackDeviceId === this.selectedVideoDeviceId) {
2456
+ return this.mediaStream;
2457
+ }
2458
+ }
2459
+ this.stopStream();
2254
2460
  }
2255
2461
  this.setState("starting");
2256
2462
  try {
@@ -2328,65 +2534,45 @@ class CameraStreamManager {
2328
2534
  }
2329
2535
  return combinedStream;
2330
2536
  }
2331
- async switchVideoDevice(deviceId) {
2537
+ async switchDeviceTrack(deviceId, trackType, config) {
2332
2538
  if (!this.mediaStream) {
2333
2539
  throw new Error("No active stream to switch device");
2334
2540
  }
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");
2541
+ const oldTrack = trackType === "video" ? this.mediaStream.getVideoTracks()[0] : this.mediaStream.getAudioTracks()[0];
2542
+ const oldOtherTrack = trackType === "video" ? this.mediaStream.getAudioTracks()[0] : this.mediaStream.getVideoTracks()[0];
2543
+ if (!oldTrack) {
2544
+ const trackName = trackType === "video" ? "video" : "audio";
2545
+ throw new Error(`No ${trackName} track in current stream`);
2339
2546
  }
2340
2547
  const constraints = {
2341
- video: deviceId ? {
2342
- ...typeof this.streamConfig.video === "object" ? this.streamConfig.video : {},
2343
- deviceId: { exact: deviceId }
2344
- } : this.streamConfig.video
2548
+ [trackType]: this.buildDeviceConstraints(deviceId, config)
2345
2549
  };
2346
2550
  const newStream = await navigator.mediaDevices.getUserMedia(constraints);
2347
- const newVideoTrack = newStream.getVideoTracks()[0];
2348
- if (!newVideoTrack) {
2551
+ const newTrack = trackType === "video" ? newStream.getVideoTracks()[0] : newStream.getAudioTracks()[0];
2552
+ if (!newTrack) {
2349
2553
  this.stopStreamTracks(newStream);
2350
- throw new Error("Failed to get new video track");
2554
+ const trackName = trackType === "video" ? "video" : "audio";
2555
+ throw new Error(`Failed to get new ${trackName} track`);
2351
2556
  }
2352
- const useReplaceTrack = await this.tryReplaceTrack(oldVideoTrack, newVideoTrack, newStream);
2557
+ const useReplaceTrack = await this.tryReplaceTrack(oldTrack, newTrack, newStream);
2353
2558
  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");
2559
+ oldTrack.stop();
2560
+ this.mediaStream = this.recreateStreamWithNewTrack(newTrack, oldOtherTrack, newStream);
2381
2561
  }
2382
- const useReplaceTrack = await this.tryReplaceTrack(oldAudioTrack, newAudioTrack, newStream);
2383
- if (!useReplaceTrack) {
2384
- oldAudioTrack.stop();
2385
- this.mediaStream = this.recreateStreamWithNewTrack(newAudioTrack, oldVideoTrack, newStream);
2562
+ if (trackType === "video") {
2563
+ this.selectedVideoDeviceId = deviceId;
2564
+ this.emit("videosourcechange", { stream: this.mediaStream });
2565
+ } else {
2566
+ this.selectedAudioDeviceId = deviceId;
2386
2567
  }
2387
- this.selectedAudioDeviceId = deviceId;
2388
2568
  return this.mediaStream;
2389
2569
  }
2570
+ switchVideoDevice(deviceId) {
2571
+ return this.switchDeviceTrack(deviceId, "video", this.streamConfig.video);
2572
+ }
2573
+ switchAudioDevice(deviceId) {
2574
+ return this.switchDeviceTrack(deviceId, "audio", this.streamConfig.audio);
2575
+ }
2390
2576
  startRecordingWithMediaRecorder() {
2391
2577
  if (!this.mediaStream) {
2392
2578
  throw new Error("Stream must be started before recording");
@@ -2732,22 +2918,13 @@ class UploadManager {
2732
2918
 
2733
2919
  // src/core/upload/video-upload-service.ts
2734
2920
  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
2921
 
2745
2922
  class VideoUploadService {
2746
2923
  async uploadVideo(blob, options) {
2747
2924
  if (!options.filename) {
2748
2925
  throw new Error("Filename is required");
2749
2926
  }
2750
- if (!blob.type) {
2927
+ if (!blob.type || blob.type.trim() === "") {
2751
2928
  throw new Error("Blob type is required");
2752
2929
  }
2753
2930
  const initResponse = await this.initVideoUpload({
@@ -2780,10 +2957,10 @@ class VideoUploadService {
2780
2957
  body.userMetadata = data.userMetadata;
2781
2958
  }
2782
2959
  const response = await fetch(url, {
2783
- method: HTTP_METHOD_POST,
2960
+ method: "POST",
2784
2961
  headers: {
2785
- [HEADER_AUTHORIZATION]: `${BEARER_PREFIX}${data.apiKey}`,
2786
- [HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON
2962
+ Authorization: `Bearer ${data.apiKey}`,
2963
+ "Content-Type": "application/json"
2787
2964
  },
2788
2965
  body: JSON.stringify(body)
2789
2966
  });
@@ -2794,14 +2971,19 @@ class VideoUploadService {
2794
2971
  return await response.json();
2795
2972
  }
2796
2973
  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 {}
2974
+ const errorData = await this.parseJsonResponse(response);
2975
+ if (errorData && typeof errorData === "object" && "error" in errorData && typeof errorData.error === "string") {
2976
+ return errorData.error;
2977
+ }
2803
2978
  return `${defaultMessage}: ${response.status} ${response.statusText}`;
2804
2979
  }
2980
+ async parseJsonResponse(response) {
2981
+ const jsonData = await response.json();
2982
+ if (typeof jsonData === "object" && jsonData !== null) {
2983
+ return jsonData;
2984
+ }
2985
+ return null;
2986
+ }
2805
2987
  uploadVideoFile(blob, uploadUrl, options) {
2806
2988
  return new Promise((resolve, reject) => {
2807
2989
  const xhr = new XMLHttpRequest;
@@ -2815,11 +2997,11 @@ class VideoUploadService {
2815
2997
  });
2816
2998
  }
2817
2999
  xhr.addEventListener("load", () => {
2818
- if (xhr.status >= HTTP_STATUS_OK_MIN && xhr.status <= HTTP_STATUS_OK_MAX) {
3000
+ if (xhr.status >= 200 && xhr.status <= 299) {
2819
3001
  this.parseSuccessResponse(xhr, resolve, reject);
2820
- } else {
2821
- this.parseErrorResponse(xhr, reject);
3002
+ return;
2822
3003
  }
3004
+ this.parseErrorResponse(xhr, reject);
2823
3005
  });
2824
3006
  xhr.addEventListener("error", () => {
2825
3007
  reject(new Error("Network error during upload"));
@@ -2827,47 +3009,72 @@ class VideoUploadService {
2827
3009
  xhr.addEventListener("abort", () => {
2828
3010
  reject(new Error("Upload was aborted"));
2829
3011
  });
2830
- xhr.open(HTTP_METHOD_PUT, uploadUrl);
2831
- xhr.setRequestHeader(HEADER_AUTHORIZATION, `${BEARER_PREFIX}${options.apiKey}`);
2832
- xhr.setRequestHeader(HEADER_CONTENT_TYPE, blob.type);
3012
+ xhr.open("PUT", uploadUrl);
3013
+ xhr.setRequestHeader("Authorization", `Bearer ${options.apiKey}`);
3014
+ xhr.setRequestHeader("Content-Type", blob.type);
2833
3015
  if (options.duration !== undefined) {
2834
- xhr.setRequestHeader(HEADER_X_VIDEO_DURATION, options.duration.toString());
3016
+ xhr.setRequestHeader("X-Video-Duration", options.duration.toString());
2835
3017
  }
2836
3018
  xhr.send(blob);
2837
3019
  });
2838
3020
  }
2839
3021
  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}`));
3022
+ const result = this.safeParseJsonFromXhr(xhr);
3023
+ if (!result) {
3024
+ reject(new Error("Failed to parse upload response: invalid JSON"));
3025
+ return;
2846
3026
  }
3027
+ resolve(result);
2847
3028
  }
2848
3029
  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));
3030
+ const errorData = this.safeParseJsonFromXhr(xhr);
3031
+ if (errorData && typeof errorData === "object" && "error" in errorData && typeof errorData.error === "string") {
3032
+ reject(new Error(errorData.error));
3033
+ return;
3034
+ }
3035
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
3036
+ }
3037
+ safeParseJsonFromXhr(xhr) {
3038
+ if (!xhr.responseText || xhr.responseText.trim() === "") {
3039
+ return null;
3040
+ }
3041
+ const trimmed = xhr.responseText.trim();
3042
+ if (!(trimmed.startsWith("{") && trimmed.endsWith("}")) || trimmed.length < 2) {
3043
+ return null;
3044
+ }
3045
+ const parsed = JSON.parse(xhr.responseText);
3046
+ if (typeof parsed === "object" && parsed !== null) {
3047
+ return parsed;
3048
+ }
3049
+ return null;
2857
3050
  }
2858
3051
  }
2859
3052
 
2860
3053
  // src/core/recorder/callback-factory.ts
2861
- var noop = () => {};
3054
+ var noopStateChange = (_state) => {};
3055
+ var noopCountdownUpdate = (_state, _remaining) => {};
3056
+ var noopTimerUpdate = (_formatted) => {};
3057
+ var noopError = (_error) => {};
3058
+ var noopRecordingComplete = (_blob) => {};
3059
+ var noopClearUploadStatus = () => {};
3060
+ function ensureCallback(callback, fallback) {
3061
+ return callback ?? fallback;
3062
+ }
2862
3063
  function createRecordingCallbacks(callbacks, audioLevelAnalyzer, configManager) {
2863
3064
  const recording = callbacks.recording;
3065
+ const onStateChange = ensureCallback(recording?.onStateChange, noopStateChange);
3066
+ const onCountdownUpdate = ensureCallback(recording?.onCountdownUpdate, noopCountdownUpdate);
3067
+ const onTimerUpdate = ensureCallback(recording?.onTimerUpdate, noopTimerUpdate);
3068
+ const onError = ensureCallback(recording?.onError, noopError);
3069
+ const onRecordingComplete = ensureCallback(recording?.onRecordingComplete, noopRecordingComplete);
3070
+ const onClearUploadStatus = ensureCallback(recording?.onClearUploadStatus, noopClearUploadStatus);
2864
3071
  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,
3072
+ onStateChange,
3073
+ onCountdownUpdate,
3074
+ onTimerUpdate,
3075
+ onError,
3076
+ onRecordingComplete,
3077
+ onClearUploadStatus,
2871
3078
  onStopAudioTracking: () => {
2872
3079
  audioLevelAnalyzer.stopTracking();
2873
3080
  },
@@ -3001,11 +3208,11 @@ class RecorderController {
3001
3208
  async stopStream() {
3002
3209
  await this.streamManager.stopStream();
3003
3210
  }
3004
- async switchVideoDevice(deviceId) {
3005
- return await this.streamManager.switchVideoDevice(deviceId);
3211
+ switchVideoDevice(deviceId) {
3212
+ return this.streamManager.switchVideoDevice(deviceId);
3006
3213
  }
3007
- async switchAudioDevice(deviceId) {
3008
- return await this.streamManager.switchAudioDevice(deviceId);
3214
+ switchAudioDevice(deviceId) {
3215
+ return this.streamManager.switchAudioDevice(deviceId);
3009
3216
  }
3010
3217
  async startRecording() {
3011
3218
  await this.recordingManager.startRecording();
@@ -3032,8 +3239,8 @@ class RecorderController {
3032
3239
  setMicDevice(deviceId) {
3033
3240
  this.deviceManager.setMicDevice(deviceId);
3034
3241
  }
3035
- async getAvailableDevices() {
3036
- return await this.deviceManager.getAvailableDevices();
3242
+ getAvailableDevices() {
3243
+ return this.deviceManager.getAvailableDevices();
3037
3244
  }
3038
3245
  muteAudio() {
3039
3246
  this.muteStateManager.mute();
@@ -3087,8 +3294,8 @@ class RecorderController {
3087
3294
  getDeviceManager() {
3088
3295
  return this.deviceManager;
3089
3296
  }
3090
- async getConfig() {
3091
- return await this.configManager.getConfig();
3297
+ getConfig() {
3298
+ return this.configManager.getConfig();
3092
3299
  }
3093
3300
  isRecording() {
3094
3301
  return this.streamManager.isRecording();
@@ -3189,6 +3396,204 @@ function calculateBarColor(position) {
3189
3396
  const t = (position - 0.75) / 0.25;
3190
3397
  return `rgb(0, ${Math.round(128 - (100 - 128) * t)}, ${Math.round(128 + (200 - 128) * t)})`;
3191
3398
  }
3399
+ // src/vidtreo-recorder.ts
3400
+ class VidtreoRecorder {
3401
+ controller;
3402
+ config;
3403
+ uploadService;
3404
+ isInitialized = false;
3405
+ constructor(config) {
3406
+ if (!config.apiKey) {
3407
+ throw new Error("apiKey is required");
3408
+ }
3409
+ if (!config.apiUrl) {
3410
+ throw new Error("apiUrl is required");
3411
+ }
3412
+ this.config = config;
3413
+ this.uploadService = new VideoUploadService;
3414
+ const callbacks = {
3415
+ recording: {
3416
+ onStateChange: (state) => {
3417
+ if (state === "recording" && this.config.onRecordingStart) {
3418
+ this.config.onRecordingStart();
3419
+ }
3420
+ if (state === "idle" && this.config.onRecordingStop) {
3421
+ this.config.onRecordingStop();
3422
+ }
3423
+ },
3424
+ onGetConfig: async () => await this.controller.getConfig()
3425
+ },
3426
+ upload: {
3427
+ onProgress: (progress) => {
3428
+ if (this.config.onUploadProgress) {
3429
+ this.config.onUploadProgress(progress);
3430
+ }
3431
+ },
3432
+ onSuccess: (result) => {
3433
+ if (this.config.onUploadComplete) {
3434
+ this.config.onUploadComplete({
3435
+ recordingId: result.videoId,
3436
+ uploadUrl: result.uploadUrl
3437
+ });
3438
+ }
3439
+ },
3440
+ onError: (error) => {
3441
+ if (this.config.onUploadError) {
3442
+ this.config.onUploadError(error);
3443
+ }
3444
+ },
3445
+ onClearStatus: () => {}
3446
+ },
3447
+ stream: {
3448
+ onError: (error) => {
3449
+ if (this.config.onError) {
3450
+ this.config.onError(error);
3451
+ }
3452
+ }
3453
+ }
3454
+ };
3455
+ this.controller = new RecorderController(callbacks);
3456
+ }
3457
+ async initialize() {
3458
+ if (this.isInitialized) {
3459
+ return;
3460
+ }
3461
+ const recorderConfig = {
3462
+ apiKey: this.config.apiKey,
3463
+ backendUrl: this.config.apiUrl,
3464
+ maxRecordingTime: this.config.maxRecordingTime ?? null,
3465
+ countdownDuration: this.config.countdownDuration,
3466
+ userMetadata: this.config.userMetadata,
3467
+ enableSourceSwitching: this.config.enableSourceSwitching,
3468
+ enableMute: this.config.enableMute,
3469
+ enablePause: this.config.enablePause,
3470
+ enableDeviceChange: this.config.enableDeviceChange
3471
+ };
3472
+ await this.controller.initialize(recorderConfig);
3473
+ this.isInitialized = true;
3474
+ }
3475
+ async startPreview(sourceType = "camera") {
3476
+ await this.ensureInitialized();
3477
+ await this.controller.startStream();
3478
+ if (sourceType === "screen") {
3479
+ await this.controller.switchSource("screen");
3480
+ }
3481
+ const stream = this.controller.getStream();
3482
+ if (!stream) {
3483
+ throw new Error("Failed to start preview stream");
3484
+ }
3485
+ return stream;
3486
+ }
3487
+ async startRecording(_options = {}, sourceType = "camera") {
3488
+ await this.ensureInitialized();
3489
+ if (!this.controller.isActive()) {
3490
+ await this.startPreview(sourceType);
3491
+ }
3492
+ if (sourceType === "screen" && this.controller.getCurrentSourceType() !== "screen") {
3493
+ await this.controller.switchSource("screen");
3494
+ }
3495
+ if (sourceType === "camera" && this.controller.getCurrentSourceType() !== "camera") {
3496
+ await this.controller.switchSource("camera");
3497
+ }
3498
+ await this.controller.startRecording();
3499
+ }
3500
+ async switchSource(sourceType) {
3501
+ await this.ensureInitialized();
3502
+ if (!this.config.enableSourceSwitching) {
3503
+ throw new Error("Source switching is not enabled");
3504
+ }
3505
+ const currentSourceType = this.controller.getCurrentSourceType();
3506
+ if (currentSourceType === sourceType) {
3507
+ return;
3508
+ }
3509
+ if (!this.controller.isRecording()) {
3510
+ if (sourceType === "screen") {
3511
+ await this.startPreview("screen");
3512
+ } else {
3513
+ await this.startPreview("camera");
3514
+ }
3515
+ return;
3516
+ }
3517
+ await this.controller.switchSource(sourceType);
3518
+ }
3519
+ async stopRecording() {
3520
+ await this.ensureInitialized();
3521
+ const blob = await this.controller.stopRecording();
3522
+ const duration = await extractVideoDuration(blob);
3523
+ const timestamp = Date.now();
3524
+ const filename = `recording-${timestamp}.mp4`;
3525
+ const uploadResult = await this.uploadService.uploadVideo(blob, {
3526
+ apiKey: this.config.apiKey,
3527
+ backendUrl: this.config.apiUrl,
3528
+ filename,
3529
+ duration,
3530
+ userMetadata: this.config.userMetadata,
3531
+ onProgress: this.config.onUploadProgress
3532
+ });
3533
+ return {
3534
+ recordingId: uploadResult.videoId,
3535
+ uploadUrl: uploadResult.uploadUrl,
3536
+ blob
3537
+ };
3538
+ }
3539
+ async getAvailableDevices() {
3540
+ if (this.config.enableDeviceChange === false) {
3541
+ throw new Error("Device change functionality is disabled");
3542
+ }
3543
+ await this.ensureInitialized();
3544
+ return await this.controller.getAvailableDevices();
3545
+ }
3546
+ muteAudio() {
3547
+ if (this.config.enableMute === false) {
3548
+ throw new Error("Mute functionality is disabled");
3549
+ }
3550
+ this.controller.muteAudio();
3551
+ }
3552
+ unmuteAudio() {
3553
+ if (this.config.enableMute === false) {
3554
+ throw new Error("Mute functionality is disabled");
3555
+ }
3556
+ this.controller.unmuteAudio();
3557
+ }
3558
+ toggleMute() {
3559
+ if (this.config.enableMute === false) {
3560
+ throw new Error("Mute functionality is disabled");
3561
+ }
3562
+ this.controller.toggleMute();
3563
+ }
3564
+ isMuted() {
3565
+ return this.controller.getIsMuted();
3566
+ }
3567
+ pauseRecording() {
3568
+ if (this.config.enablePause === false) {
3569
+ throw new Error("Pause functionality is disabled");
3570
+ }
3571
+ this.controller.pauseRecording();
3572
+ }
3573
+ resumeRecording() {
3574
+ if (this.config.enablePause === false) {
3575
+ throw new Error("Pause functionality is disabled");
3576
+ }
3577
+ this.controller.resumeRecording();
3578
+ }
3579
+ isPaused() {
3580
+ return this.controller.isPaused();
3581
+ }
3582
+ getRecordingState() {
3583
+ return this.controller.getRecordingState();
3584
+ }
3585
+ getStream() {
3586
+ return this.controller.getStream();
3587
+ }
3588
+ cleanup() {
3589
+ this.controller.cleanup();
3590
+ }
3591
+ async ensureInitialized() {
3592
+ if (!this.isInitialized) {
3593
+ await this.initialize();
3594
+ }
3595
+ }
3596
+ }
3192
3597
  export {
3193
3598
  transcodeVideo,
3194
3599
  mapPresetToConfig,
@@ -3197,6 +3602,7 @@ export {
3197
3602
  extractVideoDuration,
3198
3603
  extractErrorMessage,
3199
3604
  calculateBarColor,
3605
+ VidtreoRecorder,
3200
3606
  VideoUploadService,
3201
3607
  VideoStorageService,
3202
3608
  UploadQueueManager,