@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/README.md +573 -142
- package/dist/audio-capture-processor.worklet.js +37 -0
- package/dist/index.d.ts +632 -760
- package/dist/index.js +595 -189
- package/dist/vidtreo-recorder.d.ts +55 -0
- package/package.json +15 -16
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 =
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
522
|
+
return null;
|
|
439
523
|
}
|
|
440
|
-
this.audioContext = new AudioContextClass;
|
|
441
524
|
if (config.audioBitrate === undefined || config.audioBitrate === null) {
|
|
442
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1903
|
+
const screenShareStream = await navigator.mediaDevices.getDisplayMedia({
|
|
1783
1904
|
video: true,
|
|
1784
1905
|
audio: true
|
|
1785
1906
|
});
|
|
1786
|
-
this.screenShareStream =
|
|
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.
|
|
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(
|
|
1795
|
-
return
|
|
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
|
-
|
|
1842
|
-
if (!
|
|
1977
|
+
canReuseStream(stream, mustMatchOriginal) {
|
|
1978
|
+
if (!stream) {
|
|
1843
1979
|
return false;
|
|
1844
1980
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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)
|
|
2003
|
+
if (!(managerStream && this.originalCameraStream)) {
|
|
1852
2004
|
return false;
|
|
1853
2005
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
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
|
-
|
|
1864
|
-
|
|
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
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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
|
-
|
|
1887
|
-
|
|
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
|
-
|
|
1903
|
-
if (!stream) {
|
|
2100
|
+
if (!this.originalCameraStream) {
|
|
1904
2101
|
throw new Error("Original camera stream is null");
|
|
1905
2102
|
}
|
|
1906
|
-
return
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2422
|
+
buildDeviceConstraints(deviceId, config) {
|
|
2224
2423
|
if (!deviceId) {
|
|
2225
|
-
return
|
|
2424
|
+
return config;
|
|
2226
2425
|
}
|
|
2227
|
-
if (typeof
|
|
2426
|
+
if (typeof config === "object") {
|
|
2228
2427
|
return {
|
|
2229
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2336
|
-
const
|
|
2337
|
-
if (!
|
|
2338
|
-
|
|
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
|
-
|
|
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
|
|
2348
|
-
if (!
|
|
2551
|
+
const newTrack = trackType === "video" ? newStream.getVideoTracks()[0] : newStream.getAudioTracks()[0];
|
|
2552
|
+
if (!newTrack) {
|
|
2349
2553
|
this.stopStreamTracks(newStream);
|
|
2350
|
-
|
|
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(
|
|
2557
|
+
const useReplaceTrack = await this.tryReplaceTrack(oldTrack, newTrack, newStream);
|
|
2353
2558
|
if (!useReplaceTrack) {
|
|
2354
|
-
|
|
2355
|
-
this.mediaStream = this.recreateStreamWithNewTrack(
|
|
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
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
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:
|
|
2960
|
+
method: "POST",
|
|
2784
2961
|
headers: {
|
|
2785
|
-
|
|
2786
|
-
|
|
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
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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 >=
|
|
3000
|
+
if (xhr.status >= 200 && xhr.status <= 299) {
|
|
2819
3001
|
this.parseSuccessResponse(xhr, resolve, reject);
|
|
2820
|
-
|
|
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(
|
|
2831
|
-
xhr.setRequestHeader(
|
|
2832
|
-
xhr.setRequestHeader(
|
|
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(
|
|
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
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
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
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
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
|
|
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
|
|
2866
|
-
onCountdownUpdate
|
|
2867
|
-
onTimerUpdate
|
|
2868
|
-
onError
|
|
2869
|
-
onRecordingComplete
|
|
2870
|
-
onClearUploadStatus
|
|
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
|
-
|
|
3005
|
-
return
|
|
3211
|
+
switchVideoDevice(deviceId) {
|
|
3212
|
+
return this.streamManager.switchVideoDevice(deviceId);
|
|
3006
3213
|
}
|
|
3007
|
-
|
|
3008
|
-
return
|
|
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
|
-
|
|
3036
|
-
return
|
|
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
|
-
|
|
3091
|
-
return
|
|
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,
|