@voltkit-rtc/client 0.9.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltkit-rtc/client",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Browser SDK for VoltKit RTC: realtime audio/video rooms, chat and live broadcast publishing.",
5
5
  "keywords": [
6
6
  "voltkit",
package/src/adaptive.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * The app attaches the element that shows a video (`attachVideo`). From that
6
6
  * element's size on screen (CSS pixels × devicePixelRatio, at most 2) the
7
- * smallest simulcast layer that fills it sharply is chosen; while no attached
7
+ * smallest simulcast layer that fills it without being stretched is chosen; while no attached
8
8
  * element is visible (scrolled away, `display: none`, the tab in the
9
9
  * background) the video is paused at the media server, so it costs no
10
10
  * bandwidth and no decoding. This is what LiveKit calls adaptiveStream and
@@ -27,12 +27,16 @@ export interface ViewBox {
27
27
 
28
28
  /** Never ask for more than twice the CSS size: a 3× phone screen gains nothing visible from it. */
29
29
  export const MAX_PIXEL_RATIO = 2;
30
- /** A layer shown at most this much bigger than it is still looks sharp: avoids a whole layer for a few pixels. */
31
- export const MAX_UPSCALE = 1.15;
30
+ /**
31
+ * A layer is never stretched on screen: a box bigger than a layer gets the next one up. Up to 0.9.1 a layer could be
32
+ * shown 15% bigger than it is, which looks soft on a face (owner, 2026-10-20: "quality should look good"). The 2% is
33
+ * for rounding only, so a box a pixel or two bigger than a layer keeps it.
34
+ */
35
+ export const MAX_UPSCALE = 1.02;
32
36
 
33
37
  /**
34
- * The smallest layer that fills `box` sharply, or null when the layers are
35
- * not known (an older publisher): then the server chooses, as before.
38
+ * The smallest layer that fills `box` without being stretched, or null when
39
+ * the layers are not known (an older publisher): then the server chooses, as before.
36
40
  *
37
41
  * With `cover` the picture is scaled until it fills the box (a part is
38
42
  * cropped), with `contain` until it fits. Either way the scale is measured
@@ -46,7 +50,7 @@ export function layerForBox(layers: readonly VideoLayer[], box: ViewBox, pixelRa
46
50
  const h = box.height * ratio;
47
51
  for (const layer of usable) {
48
52
  const scale = box.fit === 'cover' ? Math.max(w / layer.width, h / layer.height) : Math.min(w / layer.width, h / layer.height);
49
- // The layer is at most slightly upscaled on screen.
53
+ // The layer is not stretched on screen (see MAX_UPSCALE).
50
54
  if (scale <= MAX_UPSCALE) return layer.spatialLayer;
51
55
  }
52
56
  return usable[usable.length - 1]!.spatialLayer;
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  /** This package's version; kept equal to package.json by a test. */
10
- export const SDK_VERSION = '0.9.1';
10
+ export const SDK_VERSION = '0.9.2';
11
11
 
12
12
  /** How many recent events the log keeps. */
13
13
  export const DIAGNOSTICS_LOG_SIZE = 200;
package/src/index.ts CHANGED
@@ -2337,7 +2337,14 @@ export class StreamClient {
2337
2337
  return parseSendStats(report.values(), { quality: this.videoQuality, baseline, keyPrefix: prefix });
2338
2338
  }
2339
2339
 
2340
- /** Friendly receive ceiling using this producer's real layer sizes; auto restores adaptation. */
2340
+ /**
2341
+ * The quality this viewer gets of one remote video. '1080p', '720p' and '480p' pick the publisher's biggest layer
2342
+ * at or below that size (a 720p camera cannot give 1080p), and the viewer gets it while the video is shown, however
2343
+ * big it is on screen: someone who picks 1080p expects 1080p, not the smaller copy that fits the tile. A connection
2344
+ * that cannot carry it still gets less until it recovers, and a video shown through attachVideo is still paused
2345
+ * while nobody can see it. 'auto' (the default) goes back to the size it is shown at, or the server's choice.
2346
+ * Kept across reconnects.
2347
+ */
2341
2348
  async setReceiveQuality(producerId: string, quality: ReceiveQuality): Promise<void> {
2342
2349
  if (!['auto', '1080p', '720p', '480p'].includes(quality)) throw new RangeError('setReceiveQuality: unsupported quality');
2343
2350
  const consumer = this.consumers.get(producerId);
@@ -3089,26 +3096,31 @@ export class StreamClient {
3089
3096
  * Adaptive stream (like LiveKit's adaptiveStream and Tencent TRTC's small
3090
3097
  * stream with receiveWhenViewVisible): tell the SDK which element shows a
3091
3098
  * remote video. It then receives the smallest layer that fills that element
3092
- * sharply (its size on screen × devicePixelRatio, at most 2) and pauses the
3099
+ * without stretching it (its size on screen × devicePixelRatio, at most 2) and pauses the
3093
3100
  * video at the server while the element is not visible (scrolled away,
3094
3101
  * `display: none`, or the tab in the background), so a grid of small tiles
3095
3102
  * stops decoding full-size video. Attach the same video to several elements
3096
3103
  * and the largest visible one decides. Returns the function that detaches
3097
- * it; detach when the element goes away. A ceiling set with
3098
- * `setReceiveQuality` or `setPreferredLayers` still applies. Works with any
3099
- * server; one without `pauseConsumer` gets the smallest layer instead of a
3100
- * pause.
3104
+ * it; detach when the element goes away. A quality chosen with
3105
+ * `setReceiveQuality` replaces the size it is shown at (the video is still
3106
+ * paused while hidden), and a ceiling set with `setPreferredLayers` still
3107
+ * applies. Works with any server; one without `pauseConsumer` gets the
3108
+ * smallest layer instead of a pause.
3101
3109
  */
3102
3110
  attachVideo(producerId: string, element: Element): () => void {
3103
3111
  return this.adaptive.attach(producerId, element);
3104
3112
  }
3105
3113
 
3106
- /** The layers to ask for: the app's ceiling (setReceiveQuality / setPreferredLayers) and adaptive stream's, the lower. */
3114
+ /**
3115
+ * The layers to ask for. A quality chosen with setReceiveQuality is the layer, whatever the size on screen (owner,
3116
+ * 2026-10-20: viewers who picked 1080p got 540p, because it was only a ceiling). A ceiling set with
3117
+ * setPreferredLayers and adaptive stream's size: the lower of the two.
3118
+ */
3107
3119
  private effectiveLayers(producerId: string, want: AdaptiveWant | null): PreferredLayers {
3108
3120
  let manual: PreferredLayers | undefined = this.preferredLayers.get(producerId);
3109
3121
  const quality = this.receiveQualities.get(producerId);
3110
3122
  if (quality && quality !== 'auto') {
3111
- try { manual = { spatialLayer: receiveSpatialLayer(this.remoteVideoLayers.get(producerId) ?? [], quality) }; } catch { manual = undefined; }
3123
+ try { return { spatialLayer: receiveSpatialLayer(this.remoteVideoLayers.get(producerId) ?? [], quality) }; } catch { manual = undefined; }
3112
3124
  }
3113
3125
  const adaptive = want?.visible ? want.spatialLayer : null;
3114
3126
  if (adaptive === null || adaptive === undefined) return manual ?? { spatialLayer: -1, temporalLayer: -1 };