@vidtreo/recorder 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,27 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __toESM = (mod, isNodeMode, target) => {
7
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
8
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
9
+ for (let key of __getOwnPropNames(mod))
10
+ if (!__hasOwnProp.call(to, key))
11
+ __defProp(to, key, {
12
+ get: () => mod[key],
13
+ enumerable: true
14
+ });
15
+ return to;
16
+ };
17
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
18
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
19
+ }) : x)(function(x) {
20
+ if (typeof require !== "undefined")
21
+ return require.apply(this, arguments);
22
+ throw Error('Dynamic require of "' + x + '" is not supported');
23
+ });
24
+
1
25
  // src/core/utils/error-handler.ts
2
26
  function extractErrorMessage(error) {
3
27
  if (error instanceof Error) {
@@ -127,6 +151,9 @@ class AudioLevelAnalyzer {
127
151
  return isMutedFromCallback || hasDisabledTracks;
128
152
  }
129
153
  }
154
+ // src/core/config/config-constants.ts
155
+ import { QUALITY_HIGH } from "mediabunny";
156
+
130
157
  // src/core/processor/format-codec-mapper.ts
131
158
  var FORMAT_DEFAULT_CODECS = {
132
159
  mp4: "aac",
@@ -149,13 +176,11 @@ var DEFAULT_BACKEND_URL = "https://api.vidtreo.com";
149
176
  var DEFAULT_TRANSCODE_CONFIG = Object.freeze({
150
177
  format: "mp4",
151
178
  fps: 30,
152
- width: 1280,
153
- height: 720,
154
- bitrate: 500000,
155
- audioCodec: undefined,
156
- audioBitrate: 128000,
157
- preset: "medium",
158
- packetCount: 1200
179
+ width: 1920,
180
+ height: 1080,
181
+ bitrate: QUALITY_HIGH,
182
+ audioCodec: "aac",
183
+ audioBitrate: 96000
159
184
  });
160
185
  function getDefaultConfigForFormat(format) {
161
186
  return {
@@ -165,23 +190,21 @@ function getDefaultConfigForFormat(format) {
165
190
  };
166
191
  }
167
192
  // src/core/config/preset-mapper.ts
168
- var BITRATE_MAP = {
169
- sd: 500000,
170
- hd: 1e6,
171
- fhd: 2000000,
172
- "4k": 8000000
193
+ import {
194
+ QUALITY_HIGH as QUALITY_HIGH2,
195
+ QUALITY_LOW,
196
+ QUALITY_MEDIUM,
197
+ QUALITY_VERY_HIGH
198
+ } from "mediabunny";
199
+ var QUALITY_MAP = {
200
+ sd: QUALITY_LOW,
201
+ hd: QUALITY_MEDIUM,
202
+ fhd: QUALITY_HIGH2,
203
+ "4k": QUALITY_VERY_HIGH
173
204
  };
174
205
  var AUDIO_BITRATE = 128000;
175
- var PACKET_COUNT_MAP = {
176
- sd: 800,
177
- hd: 1200,
178
- fhd: 2000,
179
- "4k": 4000
180
- };
181
- var DEFAULT_FPS = 30;
182
- var DEFAULT_PRESET = "medium";
183
206
  function mapPresetToConfig(preset, maxWidth, maxHeight, format = "mp4") {
184
- if (!(preset in BITRATE_MAP)) {
207
+ if (!(preset in QUALITY_MAP)) {
185
208
  throw new Error(`Invalid preset: ${preset}`);
186
209
  }
187
210
  if (typeof maxWidth !== "number" || maxWidth <= 0) {
@@ -193,13 +216,10 @@ function mapPresetToConfig(preset, maxWidth, maxHeight, format = "mp4") {
193
216
  const audioCodec = getDefaultAudioCodecForFormat(format);
194
217
  return {
195
218
  format,
196
- fps: DEFAULT_FPS,
197
219
  width: maxWidth,
198
220
  height: maxHeight,
199
- bitrate: BITRATE_MAP[preset],
221
+ bitrate: QUALITY_MAP[preset],
200
222
  audioCodec,
201
- preset: DEFAULT_PRESET,
202
- packetCount: PACKET_COUNT_MAP[preset],
203
223
  audioBitrate: AUDIO_BITRATE
204
224
  };
205
225
  }
@@ -311,8 +331,12 @@ class ConfigManager {
311
331
  apiKey,
312
332
  backendUrl: normalizedBackendUrl
313
333
  });
314
- this.currentConfig = await this.configService.fetchConfig();
315
- this.configFetched = true;
334
+ this.configService.fetchConfig().then((config) => {
335
+ this.currentConfig = config;
336
+ this.configFetched = true;
337
+ }).catch(() => {
338
+ this.configFetched = false;
339
+ });
316
340
  }
317
341
  async fetchConfig() {
318
342
  if (!this.configService) {
@@ -1379,9 +1403,9 @@ class SourceSwitchManager {
1379
1403
 
1380
1404
  // src/core/stream/stream-constants.ts
1381
1405
  var DEFAULT_CAMERA_CONSTRAINTS = Object.freeze({
1382
- width: { ideal: DEFAULT_TRANSCODE_CONFIG.width },
1383
- height: { ideal: DEFAULT_TRANSCODE_CONFIG.height },
1384
- frameRate: { ideal: DEFAULT_TRANSCODE_CONFIG.fps }
1406
+ width: { ideal: DEFAULT_TRANSCODE_CONFIG.width || 1920 },
1407
+ height: { ideal: DEFAULT_TRANSCODE_CONFIG.height || 1080 },
1408
+ frameRate: { ideal: DEFAULT_TRANSCODE_CONFIG.fps || 30 }
1385
1409
  });
1386
1410
  var DEFAULT_STREAM_CONFIG = Object.freeze({
1387
1411
  video: DEFAULT_CAMERA_CONSTRAINTS,
@@ -2647,6 +2671,30 @@ function isScreenCaptureStream(stream) {
2647
2671
  return "displaySurface" in settings || videoTrack.label.toLowerCase().includes("screen") || videoTrack.label.toLowerCase().includes("display");
2648
2672
  }
2649
2673
 
2674
+ // src/core/processor/codec-detector.ts
2675
+ async function detectBestCodec(width, height, bitrate) {
2676
+ try {
2677
+ const { canEncodeVideo } = await import("mediabunny");
2678
+ if (typeof canEncodeVideo === "function") {
2679
+ const checkOptions = {};
2680
+ if (width !== undefined) {
2681
+ checkOptions.width = width;
2682
+ }
2683
+ if (height !== undefined) {
2684
+ checkOptions.height = height;
2685
+ }
2686
+ if (bitrate !== undefined) {
2687
+ checkOptions.bitrate = bitrate;
2688
+ }
2689
+ const hevcSupported = await canEncodeVideo("hevc", checkOptions);
2690
+ if (hevcSupported) {
2691
+ return "hevc";
2692
+ }
2693
+ }
2694
+ } catch {}
2695
+ return "avc";
2696
+ }
2697
+
2650
2698
  // src/core/processor/worker/recorder-worker.code.ts
2651
2699
  var workerCode = `// ../../node_modules/mediabunny/dist/modules/src/misc.js
2652
2700
  /*!
@@ -8366,6 +8414,7 @@ class Quality {
8366
8414
  return Math.round(finalBitrate / 1000) * 1000;
8367
8415
  }
8368
8416
  }
8417
+ var QUALITY_HIGH = /* @__PURE__ */ new Quality(2);
8369
8418
 
8370
8419
  // ../../node_modules/mediabunny/dist/modules/src/media-source.js
8371
8420
  /*!
@@ -9731,6 +9780,8 @@ class RecorderWorker {
9731
9780
  baseVideoTimestamp = null;
9732
9781
  frameCount = 0;
9733
9782
  config = null;
9783
+ lastKeyFrameTimestamp = 0;
9784
+ forceNextKeyFrame = false;
9734
9785
  videoProcessingActive = false;
9735
9786
  audioProcessingActive = false;
9736
9787
  isStopping = false;
@@ -9819,54 +9870,50 @@ class RecorderWorker {
9819
9870
  }
9820
9871
  this.sendError(new Error(\`Unknown message type: \${message.type}\`));
9821
9872
  };
9822
- async handleStart(videoStream, audioStream, config, overlayConfig) {
9873
+ validateConfig(config) {
9823
9874
  requireDefined(config, "Transcode config is required");
9824
- if (config.width <= 0 || config.height <= 0) {
9825
- throw new Error("Video dimensions must be greater than zero");
9875
+ if (config.width !== undefined && config.width <= 0) {
9876
+ throw new Error("Video width must be greater than zero");
9877
+ }
9878
+ if (config.height !== undefined && config.height <= 0) {
9879
+ throw new Error("Video height must be greater than zero");
9826
9880
  }
9827
- if (config.fps <= 0) {
9881
+ if (config.fps !== undefined && config.fps <= 0) {
9828
9882
  throw new Error("Frame rate must be greater than zero");
9829
9883
  }
9830
- if (config.bitrate <= 0) {
9884
+ if (config.bitrate !== undefined && typeof config.bitrate === "number" && config.bitrate <= 0) {
9831
9885
  throw new Error("Bitrate must be greater than zero");
9832
9886
  }
9833
9887
  if (config.keyFrameInterval <= 0) {
9834
9888
  throw new Error("Key frame interval must be greater than zero");
9835
9889
  }
9836
- logger.debug("[RecorderWorker] handleStart called", {
9837
- hasVideoStream: !!videoStream,
9838
- hasAudioStream: !!audioStream,
9839
- config: {
9840
- width: config.width,
9841
- height: config.height,
9842
- fps: config.fps,
9843
- bitrate: config.bitrate
9844
- },
9845
- hasOverlayConfig: !!overlayConfig,
9846
- overlayConfig
9847
- });
9848
- this.isStopping = false;
9849
- this.isFinalized = false;
9850
- if (this.output) {
9851
- logger.debug("[RecorderWorker] Cleaning up existing output");
9852
- await this.cleanup();
9890
+ }
9891
+ validateFormat(format) {
9892
+ if (format !== "mp4") {
9893
+ throw new Error(\`Format \${format} is not yet supported in worker. Only MP4 is currently supported.\`);
9853
9894
  }
9895
+ }
9896
+ initializeRecordingState(config) {
9854
9897
  this.config = config;
9855
- this.frameRate = config.fps;
9898
+ this.frameRate = config.fps || 30;
9856
9899
  this.isPaused = false;
9857
9900
  this.isMuted = false;
9858
9901
  this.lastVideoTimestamp = 0;
9859
9902
  this.lastAudioTimestamp = 0;
9860
9903
  this.baseVideoTimestamp = null;
9861
9904
  this.frameCount = 0;
9905
+ this.lastKeyFrameTimestamp = 0;
9906
+ this.forceNextKeyFrame = false;
9862
9907
  this.pausedDuration = 0;
9863
9908
  this.pauseStartedAt = null;
9864
- this.overlayConfig = overlayConfig ? { enabled: overlayConfig.enabled, text: overlayConfig.text } : null;
9865
9909
  this.overlayCanvas = null;
9866
9910
  this.hiddenIntervals = [];
9867
9911
  this.currentHiddenIntervalStart = null;
9868
- this.recordingStartTime = overlayConfig?.recordingStartTime !== undefined ? overlayConfig.recordingStartTime / 1000 : performance.now() / 1000;
9869
9912
  this.pendingVisibilityUpdates = [];
9913
+ }
9914
+ setupOverlayConfig(overlayConfig) {
9915
+ this.overlayConfig = overlayConfig ? { enabled: overlayConfig.enabled, text: overlayConfig.text } : null;
9916
+ this.recordingStartTime = overlayConfig?.recordingStartTime !== undefined ? overlayConfig.recordingStartTime / 1000 : performance.now() / 1000;
9870
9917
  const logData = {
9871
9918
  hasOverlayConfig: !!this.overlayConfig,
9872
9919
  overlayEnabled: this.overlayConfig?.enabled,
@@ -9874,15 +9921,13 @@ class RecorderWorker {
9874
9921
  recordingStartTime: this.recordingStartTime
9875
9922
  };
9876
9923
  logger.debug("[RecorderWorker] Overlay config initialized", logData);
9924
+ }
9925
+ createOutput() {
9877
9926
  const writable = new WritableStream({
9878
9927
  write: (chunk) => {
9879
9928
  this.sendChunk(chunk.data, chunk.position);
9880
9929
  }
9881
9930
  });
9882
- const format = config.format || "mp4";
9883
- if (format !== "mp4") {
9884
- throw new Error(\`Format \${format} is not yet supported in worker. Only MP4 is currently supported.\`);
9885
- }
9886
9931
  this.output = new Output({
9887
9932
  format: new Mp4OutputFormat({
9888
9933
  fastStart: "fragmented"
@@ -9892,27 +9937,75 @@ class RecorderWorker {
9892
9937
  chunkSize: CHUNK_SIZE
9893
9938
  })
9894
9939
  });
9895
- this.videoSource = new VideoSampleSource({
9940
+ }
9941
+ createVideoSource(config) {
9942
+ const fps = config.fps || 30;
9943
+ const keyFrameIntervalSeconds = config.keyFrameInterval / fps;
9944
+ const videoSourceOptions = {
9896
9945
  codec: config.codec,
9897
- bitrate: config.bitrate,
9898
- sizeChangeBehavior: "passThrough"
9899
- });
9900
- this.output.addVideoTrack(this.videoSource);
9901
- if (videoStream) {
9902
- this.setupVideoProcessing(videoStream);
9946
+ sizeChangeBehavior: "passThrough",
9947
+ bitrateMode: "variable",
9948
+ latencyMode: "quality",
9949
+ contentHint: "detail",
9950
+ hardwareAcceleration: "prefer-hardware",
9951
+ keyFrameInterval: keyFrameIntervalSeconds,
9952
+ bitrate: config.bitrate ?? QUALITY_HIGH
9953
+ };
9954
+ this.videoSource = new VideoSampleSource(videoSourceOptions);
9955
+ const output = requireNonNull(this.output, "Output must be initialized before adding video track");
9956
+ const trackOptions = {};
9957
+ if (fps !== undefined) {
9958
+ trackOptions.frameRate = fps;
9903
9959
  }
9960
+ output.addVideoTrack(this.videoSource, trackOptions);
9961
+ }
9962
+ setupAudioSource(audioStream, config) {
9904
9963
  if (audioStream && config.audioBitrate && config.audioCodec) {
9905
9964
  if (config.audioBitrate <= 0) {
9906
9965
  throw new Error("Audio bitrate must be greater than zero");
9907
9966
  }
9908
9967
  this.audioSource = new AudioSampleSource({
9909
9968
  codec: config.audioCodec,
9910
- bitrate: config.audioBitrate
9969
+ bitrate: config.audioBitrate,
9970
+ bitrateMode: "variable"
9911
9971
  });
9912
- this.output.addAudioTrack(this.audioSource);
9972
+ const output = requireNonNull(this.output, "Output must be initialized before adding audio track");
9973
+ output.addAudioTrack(this.audioSource);
9913
9974
  this.setupAudioProcessing(audioStream);
9914
9975
  }
9915
- await this.output.start();
9976
+ }
9977
+ async handleStart(videoStream, audioStream, config, overlayConfig) {
9978
+ this.validateConfig(config);
9979
+ logger.debug("[RecorderWorker] handleStart called", {
9980
+ hasVideoStream: !!videoStream,
9981
+ hasAudioStream: !!audioStream,
9982
+ config: {
9983
+ width: config.width,
9984
+ height: config.height,
9985
+ fps: config.fps,
9986
+ bitrate: config.bitrate
9987
+ },
9988
+ hasOverlayConfig: !!overlayConfig,
9989
+ overlayConfig
9990
+ });
9991
+ this.isStopping = false;
9992
+ this.isFinalized = false;
9993
+ if (this.output) {
9994
+ logger.debug("[RecorderWorker] Cleaning up existing output");
9995
+ await this.cleanup();
9996
+ }
9997
+ this.initializeRecordingState(config);
9998
+ this.setupOverlayConfig(overlayConfig);
9999
+ const format = config.format || "mp4";
10000
+ this.validateFormat(format);
10001
+ this.createOutput();
10002
+ this.createVideoSource(config);
10003
+ if (videoStream) {
10004
+ this.setupVideoProcessing(videoStream);
10005
+ }
10006
+ this.setupAudioSource(audioStream, config);
10007
+ const output = requireNonNull(this.output, "Output must be initialized before starting");
10008
+ await output.start();
9916
10009
  this.startBufferUpdates();
9917
10010
  this.sendReady();
9918
10011
  this.sendStateChange("recording");
@@ -10157,8 +10250,6 @@ class RecorderWorker {
10157
10250
  }
10158
10251
  }
10159
10252
  }
10160
- const keyFrameInterval = config.keyFrameInterval > 0 ? config.keyFrameInterval : 5;
10161
- const isKeyFrame = this.frameCount % keyFrameInterval === 0;
10162
10253
  const maxLead = 0.05;
10163
10254
  const maxLag = 0.1;
10164
10255
  const targetAudio = this.lastAudioTimestamp;
@@ -10170,6 +10261,10 @@ class RecorderWorker {
10170
10261
  }
10171
10262
  const monotonicTimestamp = this.lastVideoTimestamp + frameDuration;
10172
10263
  const finalTimestamp = adjustedTimestamp >= monotonicTimestamp ? adjustedTimestamp : monotonicTimestamp;
10264
+ const keyFrameIntervalFrames = config.keyFrameInterval > 0 ? config.keyFrameInterval : 5;
10265
+ const keyFrameIntervalSeconds = keyFrameIntervalFrames / this.frameRate;
10266
+ const timeSinceLastKeyFrame = finalTimestamp - this.lastKeyFrameTimestamp;
10267
+ const isKeyFrame = this.forceNextKeyFrame || timeSinceLastKeyFrame >= keyFrameIntervalSeconds || this.frameCount % keyFrameIntervalFrames === 0;
10173
10268
  this.driftOffset *= 0.5;
10174
10269
  const sample = new VideoSample(frameToProcess, {
10175
10270
  timestamp: finalTimestamp,
@@ -10184,6 +10279,10 @@ class RecorderWorker {
10184
10279
  if (!addError) {
10185
10280
  this.frameCount += 1;
10186
10281
  this.lastVideoTimestamp = finalTimestamp;
10282
+ if (isKeyFrame) {
10283
+ this.lastKeyFrameTimestamp = finalTimestamp;
10284
+ this.forceNextKeyFrame = false;
10285
+ }
10187
10286
  if (this.frameCount % 90 === 0 && this.audioProcessingActive) {
10188
10287
  const avDrift = this.lastAudioTimestamp - this.lastVideoTimestamp;
10189
10288
  logger.debug("[RecorderWorker] AV drift metrics", {
@@ -10460,6 +10559,7 @@ class RecorderWorker {
10460
10559
  const previousVideoTimestamp = this.lastVideoTimestamp;
10461
10560
  this.lastVideoTimestamp = continuationTimestamp;
10462
10561
  this.frameCount = 0;
10562
+ this.forceNextKeyFrame = true;
10463
10563
  logger.debug("[RecorderWorker] handleSwitchSource - preserving baseVideoTimestamp", {
10464
10564
  continuationTimestamp,
10465
10565
  lastVideoTimestamp: this.lastVideoTimestamp,
@@ -10512,6 +10612,8 @@ class RecorderWorker {
10512
10612
  this.lastAudioTimestamp = 0;
10513
10613
  this.baseVideoTimestamp = null;
10514
10614
  this.frameCount = 0;
10615
+ this.lastKeyFrameTimestamp = 0;
10616
+ this.forceNextKeyFrame = false;
10515
10617
  this.totalSize = 0;
10516
10618
  this.pausedDuration = 0;
10517
10619
  this.pauseStartedAt = null;
@@ -10566,7 +10668,6 @@ new RecorderWorker;
10566
10668
 
10567
10669
  // src/core/processor/worker-processor.ts
10568
10670
  var KEY_FRAME_INTERVAL = 5;
10569
- var H264_CODEC = "avc";
10570
10671
  var workerBlobUrl = null;
10571
10672
  function getWorkerUrl() {
10572
10673
  if (workerBlobUrl) {
@@ -10687,20 +10788,21 @@ class WorkerProcessor {
10687
10788
  const format = config.format || "mp4";
10688
10789
  const audioCodec = config.audioCodec || getDefaultAudioCodecForFormat(format);
10689
10790
  const isScreenCapture = isScreenCaptureStream(stream);
10690
- const targetFps = config.fps;
10791
+ const codec = config.codec || await detectBestCodec(config.width, config.height, config.bitrate);
10691
10792
  logger.debug("[WorkerProcessor] Starting processing", {
10692
10793
  isScreenCapture,
10693
- targetFps,
10694
- originalFps: config.fps
10794
+ fps: config.fps,
10795
+ codec,
10796
+ bitrate: config.bitrate
10695
10797
  });
10696
10798
  const workerConfig = {
10697
10799
  width: config.width,
10698
10800
  height: config.height,
10699
- fps: targetFps,
10801
+ fps: config.fps,
10700
10802
  bitrate: config.bitrate,
10701
10803
  audioCodec,
10702
10804
  audioBitrate: config.audioBitrate,
10703
- codec: H264_CODEC,
10805
+ codec,
10704
10806
  keyFrameInterval: KEY_FRAME_INTERVAL,
10705
10807
  format
10706
10808
  };
@@ -11797,13 +11899,24 @@ function getMimeTypeForFormat(format) {
11797
11899
  function createConversionOptions(config) {
11798
11900
  const audioCodec = getAudioCodecForFormat(config.format, config.audioCodec);
11799
11901
  const video = {
11800
- width: config.width,
11801
- height: config.height,
11802
11902
  fit: "contain",
11803
- frameRate: config.fps,
11804
- bitrate: config.bitrate,
11805
11903
  forceTranscode: true
11806
11904
  };
11905
+ if (config.width !== undefined) {
11906
+ video.width = config.width;
11907
+ }
11908
+ if (config.height !== undefined) {
11909
+ video.height = config.height;
11910
+ }
11911
+ if (config.fps !== undefined) {
11912
+ video.frameRate = config.fps;
11913
+ }
11914
+ if (config.bitrate !== undefined) {
11915
+ video.bitrate = config.bitrate;
11916
+ }
11917
+ if (config.codec !== undefined) {
11918
+ video.codec = config.codec;
11919
+ }
11807
11920
  const audio = {
11808
11921
  codec: audioCodec,
11809
11922
  forceTranscode: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "type": "module",
5
5
  "description": "Vidtreo SDK for browser-based video recording and transcoding. Features include camera/screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads. Similar to Ziggeo and Addpipe, Vidtreo provides enterprise-grade video processing capabilities for web applications.",
6
6
  "main": "./dist/index.js",