@unboundcx/video-sdk-client 2.0.4 → 2.0.6
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.
|
@@ -366,6 +366,14 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
366
366
|
// Get display media
|
|
367
367
|
// Note: Audio is only available when sharing a Chrome Tab or Window (not Screen)
|
|
368
368
|
// Chrome will show an audio checkbox for Tab and Window sharing if audio is requested
|
|
369
|
+
//
|
|
370
|
+
// Keep video constraints minimal — only the `cursor` hint. Some
|
|
371
|
+
// Chrome versions reject frameRate / width / height on
|
|
372
|
+
// getDisplayMedia and abort the capture with an opaque
|
|
373
|
+
// "Error starting tab capture", so we let the browser pick the
|
|
374
|
+
// source's native resolution + framerate. Bandwidth is bounded
|
|
375
|
+
// downstream by the encoder profile in MediasoupManager
|
|
376
|
+
// (single-layer, max 8 Mbps, maintain-resolution preference).
|
|
369
377
|
const constraints = {
|
|
370
378
|
video: {
|
|
371
379
|
cursor: 'always',
|
|
@@ -509,6 +509,77 @@ export class MediasoupManager extends EventEmitter {
|
|
|
509
509
|
const baseWidth = settings.width || 1920;
|
|
510
510
|
const baseHeight = settings.height || 1080;
|
|
511
511
|
|
|
512
|
+
const isScreenShare = options.appData?.type === "screenShare";
|
|
513
|
+
|
|
514
|
+
// ───────────────────────────────────────────────────────────
|
|
515
|
+
// Screen-share encoder profile
|
|
516
|
+
//
|
|
517
|
+
// Screen content is fundamentally different from camera:
|
|
518
|
+
// - Mostly static (text, UI, diagrams) with occasional motion
|
|
519
|
+
// - Text legibility is binary — readable or not, no in-between
|
|
520
|
+
// - Receivers either need to read it or they're zoomed out
|
|
521
|
+
//
|
|
522
|
+
// So: single high-quality layer (not 3-tier simulcast), much
|
|
523
|
+
// higher max bitrate per pixel, lower framerate target (the
|
|
524
|
+
// encoder handles this via degradationPreference + the actual
|
|
525
|
+
// capture frame rate, which Chrome lowers automatically for
|
|
526
|
+
// static content). Also set contentHint='detail' on the
|
|
527
|
+
// track itself so the encoder favours sharpness over motion.
|
|
528
|
+
// ───────────────────────────────────────────────────────────
|
|
529
|
+
if (isScreenShare) {
|
|
530
|
+
try {
|
|
531
|
+
track.contentHint = "detail";
|
|
532
|
+
} catch (err) {
|
|
533
|
+
this.logger.warn("Could not set contentHint on screenshare track:", err);
|
|
534
|
+
}
|
|
535
|
+
// If the captured source is bigger than 1920×1080 (4K monitor,
|
|
536
|
+
// Retina display, etc.) scale it down before encoding so we
|
|
537
|
+
// don't waste CPU on pixels nobody will see. Encode at the
|
|
538
|
+
// larger of the two dimension ratios so neither dimension
|
|
539
|
+
// exceeds 1920×1080.
|
|
540
|
+
const maxW = 1920;
|
|
541
|
+
const maxH = 1080;
|
|
542
|
+
const scaleDown = Math.max(
|
|
543
|
+
1.0,
|
|
544
|
+
baseWidth / maxW,
|
|
545
|
+
baseHeight / maxH,
|
|
546
|
+
);
|
|
547
|
+
const encWidth = Math.round(baseWidth / scaleDown);
|
|
548
|
+
const encHeight = Math.round(baseHeight / scaleDown);
|
|
549
|
+
|
|
550
|
+
// Bitrate budget tuned for text crispness: ~5 Mbps at 1080p,
|
|
551
|
+
// ~3 Mbps at 720p, scaled linearly for unusual sizes. Cap
|
|
552
|
+
// upper at 8 Mbps so a huge external monitor doesn't blow
|
|
553
|
+
// through user bandwidth.
|
|
554
|
+
const encPixels = encWidth * encHeight;
|
|
555
|
+
const bitrate = Math.min(
|
|
556
|
+
8_000_000,
|
|
557
|
+
Math.max(2_000_000, Math.round(encPixels * 2.5)),
|
|
558
|
+
);
|
|
559
|
+
produceOptions.encodings = [
|
|
560
|
+
{
|
|
561
|
+
rid: "h",
|
|
562
|
+
maxBitrate: bitrate,
|
|
563
|
+
scaleResolutionDownBy: scaleDown,
|
|
564
|
+
},
|
|
565
|
+
];
|
|
566
|
+
// degradationPreference="maintain-resolution" tells WebRTC
|
|
567
|
+
// to drop framerate before resolution if bandwidth tightens
|
|
568
|
+
// — exactly what we want for text.
|
|
569
|
+
produceOptions.degradationPreference = "maintain-resolution";
|
|
570
|
+
this.logger.info("VIDEO_QUALITY :: screen-share encoder profile", {
|
|
571
|
+
sourceResolution: `${baseWidth}×${baseHeight}`,
|
|
572
|
+
encodedResolution: `${encWidth}×${encHeight}`,
|
|
573
|
+
scaleDown: scaleDown.toFixed(2),
|
|
574
|
+
bitrate: `${(bitrate / 1_000_000).toFixed(1)}Mbps`,
|
|
575
|
+
contentHint: track.contentHint,
|
|
576
|
+
degradationPreference: "maintain-resolution",
|
|
577
|
+
});
|
|
578
|
+
// Skip the camera simulcast branches below.
|
|
579
|
+
// (Falls through to the produceOptions logging.)
|
|
580
|
+
// No `return` — we still want to call transport.produce().
|
|
581
|
+
} else {
|
|
582
|
+
|
|
512
583
|
// Check if user has set a max resolution preference
|
|
513
584
|
const maxResolution = options.maxResolution || "1080p"; // Default to 1080p
|
|
514
585
|
|
|
@@ -584,6 +655,7 @@ export class MediasoupManager extends EventEmitter {
|
|
|
584
655
|
bitrate: "800kbps",
|
|
585
656
|
});
|
|
586
657
|
}
|
|
658
|
+
} // end camera-profile else (matches `if (isScreenShare)`)
|
|
587
659
|
}
|
|
588
660
|
|
|
589
661
|
this.logger.info("Calling transport.produce with options:", {
|