@streaming-cdn/rtc-web 1.3.24 → 1.3.26

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 CHANGED
@@ -1,4 +1,4 @@
1
- # RTC Web SDK 1.3.24
1
+ # RTC Web SDK 1.3.26
2
2
 
3
3
  The customer package contains compiled ESM/UMD JavaScript and TypeScript declarations. It intentionally excludes implementation source and source maps. Example source remains available under `examples/`.
4
4
 
@@ -357,6 +357,30 @@ operational diagnostics. Tokens, SDP, media, and chat content are never part of
357
357
  that report. Set `reportSignalingDiagnostics: false` only when replacing this
358
358
  with an application-owned diagnostics pipeline.
359
359
 
360
+ ## 1.3.26 notes
361
+
362
+ - **VRM avatars are framed from the model's own measurements.** The bust
363
+ camera in `avatar-vrm` was fixed at a 0.42 m window aimed at the head
364
+ joint — which sits at the *bottom* of the skull — so characters with tall
365
+ hair or accessories had their crowns cropped, and big-headed models filled
366
+ the whole canvas and rendered far larger than the built-in avatar when
367
+ overlaid. `createVrmStage` now measures each model (head joint from the
368
+ humanoid, crown from the scene bounds, hair included) and frames the bust
369
+ so the skull spans ~38% of the canvas with its centre at 42% from the top —
370
+ the exact contract `vrmAvatarRenderer`'s overlay math expects. Every
371
+ character now appears the same size as the built-in stylized avatar,
372
+ whatever its proportions. The framing rule is exported as `frameVrmBust()`.
373
+
374
+ ## 1.3.25 notes
375
+
376
+ - **B21 — per-peer quality no longer ratchets down on bandwidth-pinned
377
+ links.** The step-up gate required `qualityLimitationReason` to clear; on
378
+ links that pin it at `bandwidth`, upgrades were mathematically impossible.
379
+ Both directions now share `bandwidthSustainRatio` (default 0.7): down
380
+ judges the current tier's target, up judges the next tier's, so promotions
381
+ cannot flap. `bandwidthHeadroomRatio` is retired and ignored, and
382
+ `goodSamplesToStepUp` is effective again in this state.
383
+
360
384
  ## 1.3.24 notes
361
385
 
362
386
  - `callupdated` now carries `actorExternalId`, matching the React Native SDK.
@@ -0,0 +1,82 @@
1
+ import { type ZipEntry } from "./zip-reader";
2
+ /**
3
+ * Custom avatar model library.
4
+ *
5
+ * A browser cannot look inside the user's Downloads folder — no web API
6
+ * enumerates the file system. So a model obtained from a third-party site is
7
+ * handed over once, explicitly (file picker or drag-and-drop), and kept here so
8
+ * every later call finds it already imported.
9
+ *
10
+ * Models never leave the device: the avatar is rendered locally into the
11
+ * outgoing video track, so remote participants receive video, never the model
12
+ * file. Nothing here uploads, and nothing here depends on three.js — reading a
13
+ * VRM's own metadata only needs its glTF JSON chunk.
14
+ */
15
+ export interface AvatarModelLicense {
16
+ /** Character name declared by the model, when present. */
17
+ title: string | null;
18
+ author: string | null;
19
+ /** Who the model may be used by, verbatim from the file. */
20
+ allowedUser: string | null;
21
+ /** Whether the author permits commercial use, verbatim from the file. */
22
+ commercialUse: string | null;
23
+ licenseName: string | null;
24
+ licenseUrl: string | null;
25
+ }
26
+ export interface AvatarModelEntry {
27
+ id: string;
28
+ /** Display name: the model's own title when it declares one, else the file name. */
29
+ name: string;
30
+ bytes: number;
31
+ importedAt: string;
32
+ /** The author's terms as declared inside the file; null when it declares none. */
33
+ license: AvatarModelLicense | null;
34
+ }
35
+ export interface AvatarModelImportOptions {
36
+ /** Overrides the name taken from the model metadata or file name. */
37
+ name?: string;
38
+ /** Rejects models above this size. Defaults to AVATAR_MODEL_MAX_BYTES. */
39
+ maxBytes?: number;
40
+ /**
41
+ * Which character to take when the source is an archive holding several.
42
+ * Comes from AvatarModelError.choices on a previous "archive_many_models".
43
+ */
44
+ entryPath?: string;
45
+ }
46
+ /**
47
+ * Lists the characters inside a downloaded pack, so a caller can offer a
48
+ * choice before importing. Returns an empty list for a bare .vrm.
49
+ */
50
+ export declare function listArchiveModels(source: File | ArrayBuffer): Promise<string[]>;
51
+ /** Well past any sensible VRM character, far below anything that would wedge a tab. */
52
+ export declare const AVATAR_MODEL_MAX_BYTES: number;
53
+ export type AvatarModelErrorCode = "model_too_large" | "model_not_glb" | "model_not_vrm" | "model_unreadable" | "storage_unavailable" | "archive_no_model" | "archive_many_models" | "archive_unreadable" | "archive_encrypted" | "archive_unsupported";
54
+ export declare class AvatarModelError extends Error {
55
+ readonly code: AvatarModelErrorCode;
56
+ /**
57
+ * The characters found inside an archive. Set only for
58
+ * "archive_many_models", so the caller can ask which one to import and pass
59
+ * it back as importAvatarModel's entryPath.
60
+ */
61
+ readonly choices: string[];
62
+ constructor(code: AvatarModelErrorCode, message: string, choices?: string[]);
63
+ }
64
+ /** Entries a character pack holds that are actually characters. */
65
+ export declare function findArchiveModels(entries: ZipEntry[]): ZipEntry[];
66
+ /**
67
+ * Reads the VRM metadata a model declares about itself. Supports VRM 0.x
68
+ * (extensions.VRM.meta) and VRM 1.0 (extensions.VRMC_vrm.meta), whose field
69
+ * names differ. Returns null for a glTF that carries neither.
70
+ */
71
+ export declare function readAvatarModelMetadata(buffer: ArrayBuffer): AvatarModelLicense | null;
72
+ /**
73
+ * Validates a model the user supplied and keeps it for later calls. The bytes
74
+ * are checked before anything renders them: an oversized or non-VRM file is
75
+ * rejected with a code the UI can explain, never handed to the renderer.
76
+ */
77
+ export declare function importAvatarModel(source: File | ArrayBuffer, options?: AvatarModelImportOptions): Promise<AvatarModelEntry>;
78
+ /** Every model this device has imported, newest first. */
79
+ export declare function listAvatarModels(): Promise<AvatarModelEntry[]>;
80
+ /** The stored bytes for a model, or null when it is no longer present. */
81
+ export declare function readAvatarModel(id: string): Promise<ArrayBuffer | null>;
82
+ export declare function removeAvatarModel(id: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import type { AvatarRig } from "./avatar-rig";
2
+ import type { AvatarRenderer } from "./avatar";
3
+ export interface VrmRenderOptions {
4
+ /** Multiplier on head joint rotation; 1 mirrors the person exactly. */
5
+ headGain?: number;
6
+ }
7
+ export interface VrmStage {
8
+ canvas: HTMLCanvasElement;
9
+ applyAndRender(rig: AvatarRig, options?: VrmRenderOptions): void;
10
+ dispose(): void;
11
+ }
12
+ /**
13
+ * Bust framing computed from the model's own measurements.
14
+ *
15
+ * Characters do not share proportions: the head joint sits at the bottom of
16
+ * the skull, and between a chibi and a tall model the joint-to-crown distance
17
+ * (hair and accessories included) spans several fold. A fixed camera cropped
18
+ * tall hair off and let big heads fill the whole canvas, which the overlay
19
+ * then multiplied into an avatar larger than the screen.
20
+ *
21
+ * The contract with `vrmAvatarRenderer` is proportional: the skull spans
22
+ * roughly 38% of the canvas (2 of the 5.2 head radii the overlay paints) with
23
+ * its centre at 42% from the top — the same size and seat as the built-in
24
+ * stylized head, so switching characters never changes how big the avatar
25
+ * appears over the person.
26
+ */
27
+ export declare function frameVrmBust(measure: {
28
+ headY: number;
29
+ crownY: number;
30
+ }): {
31
+ windowHeight: number;
32
+ lookY: number;
33
+ distance: number;
34
+ };
35
+ /**
36
+ * Adapts a VrmStage to the SDK's pluggable `AvatarRenderer`, so
37
+ * `createAvatarPipeline` can publish a VRM character instead of the built-in
38
+ * stylized avatar. In overlay mode the bust is framed around the head, so it
39
+ * spans a few head radii over the tracked anchor.
40
+ */
41
+ export declare function vrmAvatarRenderer(stage: VrmStage, options?: {
42
+ headGain?: number;
43
+ }): AvatarRenderer;
44
+ export declare function createVrmStage(buffer: ArrayBuffer, size?: number): Promise<VrmStage>;
45
+ /**
46
+ * One-call helper: bytes in, pipeline renderer out. Pair it with
47
+ * readAvatarModel() from the model library to render a character the user
48
+ * imported from a third-party site.
49
+ */
50
+ export declare function createVrmAvatarRenderer(buffer: ArrayBuffer, options?: {
51
+ size?: number;
52
+ headGain?: number;
53
+ }): Promise<AvatarRenderer>;
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ export type { BackgroundPreset, OverlayEffect, Segmenter, VideoEffectsOptions, V
9
9
  export { AVATAR_TUNING_DEFAULTS, createAvatarPipeline, stylizedAvatarRenderer } from "./avatar";
10
10
  export type { AvatarMode, AvatarPipeline, AvatarPipelineOptions, AvatarRenderer, AvatarTuning, FaceTracker, FaceTrackerResult } from "./avatar";
11
11
  export { drawAvatar, headPoseFromMatrix, neutralRig, rigFromBlendshapes, smoothRig } from "./avatar-rig";
12
+ export { AVATAR_MODEL_MAX_BYTES, AvatarModelError, importAvatarModel, listArchiveModels, listAvatarModels, readAvatarModel, readAvatarModelMetadata, removeAvatarModel } from "./avatar-library";
13
+ export type { AvatarModelEntry, AvatarModelErrorCode, AvatarModelImportOptions, AvatarModelLicense } from "./avatar-library";
12
14
  export type { AvatarAnchor, AvatarRig, BlendshapeCategory } from "./avatar-rig";
13
15
  export type RtcMode = "voice" | "video" | "meeting";
14
16
  /**
@@ -393,4 +395,4 @@ export declare class RtcIncomingClient extends EventTarget {
393
395
  private emit;
394
396
  }
395
397
  export declare function createRtcIncomingClient(options: RtcIncomingClientOptions): RtcIncomingClient;
396
- export declare const version = "1.3.24";
398
+ export declare const version = "1.3.25";
@@ -37,8 +37,20 @@ export interface PeerQualityThresholds {
37
37
  goodLossPct: number;
38
38
  goodRttMs: number;
39
39
  sustainRatio: number;
40
- /** "bandwidth" counts only when the uplink estimate is below this multiple of the tier target. */
41
- bandwidthHeadroomRatio: number;
40
+ /**
41
+ * One ratio governs both directions of the bandwidth-limited path (B21).
42
+ * Down: "bandwidth" counts as congestion only below this fraction of the
43
+ * CURRENT tier's target. Up: a bandwidth-limited but otherwise clean sample
44
+ * counts as good once the uplink covers this fraction of the NEXT tier's
45
+ * target — a capped encoder never lets estimation probe much past its send
46
+ * rate, so demanding the full next-tier target keeps the gate shut exactly
47
+ * when the cap is what throttles discovery. The default (0.7) exceeds every
48
+ * adjacent-tier cap ratio, so a promotion never yields fewer usable bits
49
+ * than the tier being left was allowed at full cap.
50
+ */
51
+ bandwidthSustainRatio: number;
52
+ /** @deprecated No longer used; superseded by bandwidthSustainRatio (B21). */
53
+ bandwidthHeadroomRatio?: number;
42
54
  }
43
55
  export declare const defaultPeerQualityThresholds: PeerQualityThresholds;
44
56
  export interface PeerQualitySample {
@@ -0,0 +1,68 @@
1
+ import * as d from "three";
2
+ import { GLTFLoader as L } from "three/examples/jsm/loaders/GLTFLoader.js";
3
+ import { VRMLoaderPlugin as g, VRMUtils as w } from "@pixiv/three-vrm";
4
+ function A(n) {
5
+ const o = n.crownY - n.headY, t = o > 0.05 && o < 1.5 ? o : 0.25, r = t * 2.1, p = n.headY + t * 0.45 - r * 0.08, s = r / 2 / Math.tan(28 / 2 * (Math.PI / 180));
6
+ return { windowHeight: r, lookY: p, distance: s };
7
+ }
8
+ function C(n, o) {
9
+ return {
10
+ render(t, r, c, p, s) {
11
+ if (n.applyAndRender(r, o), s) {
12
+ const a = s.radius * 5.2;
13
+ t.drawImage(n.canvas, s.x - a / 2, s.y - a * 0.42, a, a);
14
+ } else {
15
+ const a = Math.min(c, p);
16
+ t.drawImage(n.canvas, (c - a) / 2, (p - a) / 2, a, a);
17
+ }
18
+ },
19
+ dispose() {
20
+ n.dispose();
21
+ }
22
+ };
23
+ }
24
+ async function G(n, o = 512) {
25
+ const t = document.createElement("canvas");
26
+ t.width = o, t.height = o;
27
+ const r = new d.WebGLRenderer({ canvas: t, alpha: !0, antialias: !0 });
28
+ r.setClearColor(0, 0);
29
+ const c = new d.Scene();
30
+ c.add(new d.AmbientLight(16777215, 1.1));
31
+ const p = new d.DirectionalLight(16777215, 1.4);
32
+ p.position.set(0.5, 1.2, 1.5), c.add(p);
33
+ const s = new d.PerspectiveCamera(28, 1, 0.1, 20), a = new L();
34
+ a.register((e) => new g(e));
35
+ const k = await a.parseAsync(n, ""), i = k.userData.vrm;
36
+ w.removeUnnecessaryVertices(k.scene), w.combineSkeletons(k.scene), w.rotateVRM0(i), c.add(i.scene);
37
+ const u = i.humanoid?.getNormalizedBoneNode("head"), h = new d.Vector3(0, 1.35, 0);
38
+ u && u.getWorldPosition(h), i.scene.updateWorldMatrix(!0, !0);
39
+ const R = new d.Box3().setFromObject(i.scene), V = Number.isFinite(R.max.y) ? R.max.y : h.y + 0.25, y = A({ headY: h.y, crownY: V });
40
+ s.position.set(0, y.lookY, y.distance), s.lookAt(0, y.lookY, 0);
41
+ const f = new d.Object3D();
42
+ f.position.set(0, h.y, 2), c.add(f), i.lookAt && (i.lookAt.target = f);
43
+ const M = new d.Clock();
44
+ return {
45
+ canvas: t,
46
+ applyAndRender(e, Y) {
47
+ const b = Y?.headGain ?? 0.9, l = i.expressionManager;
48
+ if (l) {
49
+ const v = l.expressionMap ?? {}, m = (x) => !!v[x];
50
+ m("blinkLeft") || m("blinkRight") ? (l.setValue("blinkLeft", e.blinkLeft), l.setValue("blinkRight", e.blinkRight)) : m("blink") && l.setValue("blink", (e.blinkLeft + e.blinkRight) / 2), m("aa") && l.setValue("aa", Math.max(0, e.jawOpen)), m("happy") && l.setValue("happy", Math.max(0, e.smile)), m("angry") && l.setValue("angry", Math.max(0, -e.smile) * 0.6);
51
+ }
52
+ u && u.rotation.set(-e.headPitch * b, e.headYaw * b, -e.headRoll * b), f.position.set(e.pupilX * 1.2, h.y - e.pupilY * 0.8, 2), i.update(M.getDelta()), r.render(c, s);
53
+ },
54
+ dispose() {
55
+ w.deepDispose(i.scene), r.dispose();
56
+ }
57
+ };
58
+ }
59
+ async function B(n, o = {}) {
60
+ const t = await G(n, o.size ?? 640);
61
+ return C(t, { headGain: o.headGain });
62
+ }
63
+ export {
64
+ B as createVrmAvatarRenderer,
65
+ G as createVrmStage,
66
+ A as frameVrmBust,
67
+ C as vrmAvatarRenderer
68
+ };
@@ -0,0 +1 @@
1
+ (function(s,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("three"),require("three/examples/jsm/loaders/GLTFLoader.js"),require("@pixiv/three-vrm")):typeof define=="function"&&define.amd?define(["exports","three","three/examples/jsm/loaders/GLTFLoader.js","@pixiv/three-vrm"],f):(s=typeof globalThis<"u"?globalThis:s||self,f(s.StreamingCdnRtcAvatarVrm={},s.THREE,s.THREE,s.THREE_VRM))})(this,(function(s,f,j,h){"use strict";function x(e){const n=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const t in e)if(t!=="default"){const o=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(n,t,o.get?o:{enumerable:!0,get:()=>e[t]})}}return n.default=e,Object.freeze(n)}const d=x(f);function M(e){const n=e.crownY-e.headY,t=n>.05&&n<1.5?n:.25,o=t*2.1,p=e.headY+t*.45-o*.08,i=o/2/Math.tan(28/2*(Math.PI/180));return{windowHeight:o,lookY:p,distance:i}}function V(e,n){return{render(t,o,l,p,i){if(e.applyAndRender(o,n),i){const r=i.radius*5.2;t.drawImage(e.canvas,i.x-r/2,i.y-r*.42,r,r)}else{const r=Math.min(l,p);t.drawImage(e.canvas,(l-r)/2,(p-r)/2,r,r)}},dispose(){e.dispose()}}}async function g(e,n=512){const t=document.createElement("canvas");t.width=n,t.height=n;const o=new d.WebGLRenderer({canvas:t,alpha:!0,antialias:!0});o.setClearColor(0,0);const l=new d.Scene;l.add(new d.AmbientLight(16777215,1.1));const p=new d.DirectionalLight(16777215,1.4);p.position.set(.5,1.2,1.5),l.add(p);const i=new d.PerspectiveCamera(28,1,.1,20),r=new j.GLTFLoader;r.register(a=>new h.VRMLoaderPlugin(a));const w=await r.parseAsync(e,""),c=w.userData.vrm;h.VRMUtils.removeUnnecessaryVertices(w.scene),h.VRMUtils.combineSkeletons(w.scene),h.VRMUtils.rotateVRM0(c),l.add(c.scene);const R=c.humanoid?.getNormalizedBoneNode("head"),y=new d.Vector3(0,1.35,0);R&&R.getWorldPosition(y),c.scene.updateWorldMatrix(!0,!0);const L=new d.Box3().setFromObject(c.scene),T=Number.isFinite(L.max.y)?L.max.y:y.y+.25,k=M({headY:y.y,crownY:T});i.position.set(0,k.lookY,k.distance),i.lookAt(0,k.lookY,0);const b=new d.Object3D;b.position.set(0,y.y,2),l.add(b),c.lookAt&&(c.lookAt.target=b);const Y=new d.Clock;return{canvas:t,applyAndRender(a,S){const v=S?.headGain??.9,u=c.expressionManager;if(u){const E=u.expressionMap??{},m=O=>!!E[O];m("blinkLeft")||m("blinkRight")?(u.setValue("blinkLeft",a.blinkLeft),u.setValue("blinkRight",a.blinkRight)):m("blink")&&u.setValue("blink",(a.blinkLeft+a.blinkRight)/2),m("aa")&&u.setValue("aa",Math.max(0,a.jawOpen)),m("happy")&&u.setValue("happy",Math.max(0,a.smile)),m("angry")&&u.setValue("angry",Math.max(0,-a.smile)*.6)}R&&R.rotation.set(-a.headPitch*v,a.headYaw*v,-a.headRoll*v),b.position.set(a.pupilX*1.2,y.y-a.pupilY*.8,2),c.update(Y.getDelta()),o.render(l,i)},dispose(){h.VRMUtils.deepDispose(c.scene),o.dispose()}}}async function A(e,n={}){const t=await g(e,n.size??640);return V(t,{headGain:n.headGain})}s.createVrmAvatarRenderer=A,s.createVrmStage=g,s.frameVrmBust=M,s.vrmAvatarRenderer=V,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})}));