@vizor-vr/player 0.2.0 → 0.2.1
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 +4 -0
- package/dist/__tests__/autoplay.test.d.ts +2 -0
- package/dist/__tests__/autoplay.test.d.ts.map +1 -0
- package/dist/__tests__/chapter-logic.test.d.ts +2 -0
- package/dist/__tests__/chapter-logic.test.d.ts.map +1 -0
- package/dist/__tests__/chapter-manager.test.d.ts +2 -0
- package/dist/__tests__/chapter-manager.test.d.ts.map +1 -0
- package/dist/__tests__/chapter-menu.test.d.ts +2 -0
- package/dist/__tests__/chapter-menu.test.d.ts.map +1 -0
- package/dist/__tests__/dom-drm.test.d.ts +2 -0
- package/dist/__tests__/dom-drm.test.d.ts.map +1 -0
- package/dist/__tests__/drm-gate.test.d.ts +2 -0
- package/dist/__tests__/drm-gate.test.d.ts.map +1 -0
- package/dist/__tests__/fairplay.test.d.ts +2 -0
- package/dist/__tests__/fairplay.test.d.ts.map +1 -0
- package/dist/__tests__/hub-controller.test.d.ts +2 -0
- package/dist/__tests__/hub-controller.test.d.ts.map +1 -0
- package/dist/__tests__/hub-layout.test.d.ts +2 -0
- package/dist/__tests__/hub-layout.test.d.ts.map +1 -0
- package/dist/__tests__/hub-model.test.d.ts +2 -0
- package/dist/__tests__/hub-model.test.d.ts.map +1 -0
- package/dist/__tests__/hub-panel-renderer.test.d.ts +2 -0
- package/dist/__tests__/hub-panel-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/lens-angle.test.d.ts +2 -0
- package/dist/__tests__/lens-angle.test.d.ts.map +1 -0
- package/dist/__tests__/media-element-drm-gate.test.d.ts +2 -0
- package/dist/__tests__/media-element-drm-gate.test.d.ts.map +1 -0
- package/dist/__tests__/media-element-gating.test.d.ts +2 -0
- package/dist/__tests__/media-element-gating.test.d.ts.map +1 -0
- package/dist/__tests__/player-version-lock.test.d.ts +2 -0
- package/dist/__tests__/player-version-lock.test.d.ts.map +1 -0
- package/dist/__tests__/source-manager-drm.test.d.ts +2 -0
- package/dist/__tests__/source-manager-drm.test.d.ts.map +1 -0
- package/dist/__tests__/vz-chapter.test.d.ts +2 -0
- package/dist/__tests__/vz-chapter.test.d.ts.map +1 -0
- package/dist/__tests__/vz-cinema-gating.test.d.ts +2 -0
- package/dist/__tests__/vz-cinema-gating.test.d.ts.map +1 -0
- package/dist/__tests__/vz-hub-item.test.d.ts +2 -0
- package/dist/__tests__/vz-hub-item.test.d.ts.map +1 -0
- package/dist/__tests__/vz-tour-gating.test.d.ts +2 -0
- package/dist/__tests__/vz-tour-gating.test.d.ts.map +1 -0
- package/dist/__tests__/watch-time.test.d.ts +2 -0
- package/dist/__tests__/watch-time.test.d.ts.map +1 -0
- package/dist/analytics/index.d.ts.map +1 -1
- package/dist/annotations/annotation-popup.d.ts.map +1 -1
- package/dist/{annotations-C0ugfCn4.js → annotations-Du6ZWrqC.js} +82 -13
- package/dist/annotations-Du6ZWrqC.js.map +1 -0
- package/dist/captions/caption-renderer.d.ts.map +1 -1
- package/dist/{captions-BP0H2E03.js → captions-YvnHAwLY.js} +4 -4
- package/dist/captions-YvnHAwLY.js.map +1 -0
- package/dist/chapters/chapter-logic.d.ts +47 -0
- package/dist/chapters/chapter-logic.d.ts.map +1 -0
- package/dist/chapters/chapter-manager.d.ts +42 -0
- package/dist/chapters/chapter-manager.d.ts.map +1 -0
- package/dist/chapters/chapter-menu.d.ts +24 -0
- package/dist/chapters/chapter-menu.d.ts.map +1 -0
- package/dist/{collab-BXPomrS0.js → collab-BLpllHtb.js} +20 -14
- package/dist/collab-BLpllHtb.js.map +1 -0
- package/dist/collaborative/chat-button.d.ts.map +1 -1
- package/dist/collaborative/voice-button.d.ts.map +1 -1
- package/dist/controls/cc-button.d.ts.map +1 -1
- package/dist/controls/compass.d.ts.map +1 -1
- package/dist/controls/controls-overlay.d.ts.map +1 -1
- package/dist/controls/fullscreen-button.d.ts.map +1 -1
- package/dist/controls/icons.d.ts +1 -0
- package/dist/controls/icons.d.ts.map +1 -1
- package/dist/controls/live-indicator.d.ts.map +1 -1
- package/dist/controls/play-button.d.ts.map +1 -1
- package/dist/controls/playlist-controls.d.ts.map +1 -1
- package/dist/controls/quality-menu.d.ts.map +1 -1
- package/dist/controls/seek-bar.d.ts +9 -0
- package/dist/controls/seek-bar.d.ts.map +1 -1
- package/dist/controls/volume-control.d.ts.map +1 -1
- package/dist/controls/vr-button.d.ts.map +1 -1
- package/dist/core/base-element.d.ts +8 -0
- package/dist/core/base-element.d.ts.map +1 -1
- package/dist/core/media-element.d.ts +70 -1
- package/dist/core/media-element.d.ts.map +1 -1
- package/dist/{dash-adapter-tid9ZyhO.js → dash-adapter-BMpHWcBA.js} +9 -2
- package/dist/dash-adapter-BMpHWcBA.js.map +1 -0
- package/dist/delivery/__tests__/delivery-client.test.d.ts +2 -0
- package/dist/delivery/__tests__/delivery-client.test.d.ts.map +1 -0
- package/dist/delivery/__tests__/delivery-hydrate.test.d.ts +2 -0
- package/dist/delivery/__tests__/delivery-hydrate.test.d.ts.map +1 -0
- package/dist/delivery/delivery-client.d.ts +27 -0
- package/dist/delivery/delivery-client.d.ts.map +1 -0
- package/dist/delivery/delivery-hydrate.d.ts +18 -0
- package/dist/delivery/delivery-hydrate.d.ts.map +1 -0
- package/dist/delivery/dom-drm.d.ts +9 -0
- package/dist/delivery/dom-drm.d.ts.map +1 -0
- package/dist/elements/vz-chapter.d.ts +27 -0
- package/dist/elements/vz-chapter.d.ts.map +1 -0
- package/dist/elements/vz-cinema.d.ts +20 -0
- package/dist/elements/vz-cinema.d.ts.map +1 -1
- package/dist/elements/vz-hub-item.d.ts +23 -0
- package/dist/elements/vz-hub-item.d.ts.map +1 -0
- package/dist/elements/vz-hub.d.ts +57 -0
- package/dist/elements/vz-hub.d.ts.map +1 -0
- package/dist/elements/vz-playlist.d.ts +14 -0
- package/dist/elements/vz-playlist.d.ts.map +1 -1
- package/dist/elements/vz-tour.d.ts +16 -0
- package/dist/elements/vz-tour.d.ts.map +1 -1
- package/dist/hls-adapter-C_a65zia.js +161 -0
- package/dist/hls-adapter-C_a65zia.js.map +1 -0
- package/dist/hub/hub-controller.d.ts +31 -0
- package/dist/hub/hub-controller.d.ts.map +1 -0
- package/dist/hub/hub-layout.d.ts +23 -0
- package/dist/hub/hub-layout.d.ts.map +1 -0
- package/dist/hub/hub-model.d.ts +27 -0
- package/dist/hub/hub-model.d.ts.map +1 -0
- package/dist/hub/hub-panel-manager.d.ts +16 -0
- package/dist/hub/hub-panel-manager.d.ts.map +1 -0
- package/dist/hub/hub-panel-renderer.d.ts +26 -0
- package/dist/hub/hub-panel-renderer.d.ts.map +1 -0
- package/dist/license/__tests__/cache.test.d.ts +2 -0
- package/dist/license/__tests__/cache.test.d.ts.map +1 -0
- package/dist/license/__tests__/feature-gate.test.d.ts +2 -0
- package/dist/license/__tests__/feature-gate.test.d.ts.map +1 -0
- package/dist/license/__tests__/format-gate.test.d.ts +2 -0
- package/dist/license/__tests__/format-gate.test.d.ts.map +1 -0
- package/dist/license/__tests__/license-checker.test.d.ts.map +1 -0
- package/dist/license/__tests__/master-key.test.d.ts.map +1 -0
- package/dist/license/__tests__/verify-no-ed25519.test.d.ts +2 -0
- package/dist/license/__tests__/verify-no-ed25519.test.d.ts.map +1 -0
- package/dist/license/__tests__/verify.test.d.ts +2 -0
- package/dist/license/__tests__/verify.test.d.ts.map +1 -0
- package/dist/license/cache.d.ts +4 -0
- package/dist/license/cache.d.ts.map +1 -0
- package/dist/license/drm-gate.d.ts +15 -0
- package/dist/license/drm-gate.d.ts.map +1 -0
- package/dist/license/feature-gate.d.ts +27 -0
- package/dist/license/feature-gate.d.ts.map +1 -0
- package/dist/license/format-gate.d.ts +23 -0
- package/dist/license/format-gate.d.ts.map +1 -0
- package/dist/license/license-checker.d.ts.map +1 -1
- package/dist/license/master-key.d.ts +11 -7
- package/dist/license/master-key.d.ts.map +1 -1
- package/dist/license/trusted-key.d.ts +7 -0
- package/dist/license/trusted-key.d.ts.map +1 -0
- package/dist/license/verify.d.ts +7 -0
- package/dist/license/verify.d.ts.map +1 -0
- package/dist/{register-CwnmiGEs.js → register-BmZt_300.js} +1482 -163
- package/dist/register-BmZt_300.js.map +1 -0
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +1 -1
- package/dist/renderer/camera-controller.d.ts +1 -1
- package/dist/renderer/camera-controller.d.ts.map +1 -1
- package/dist/renderer/projections/fisheye-projection.d.ts +4 -0
- package/dist/renderer/projections/fisheye-projection.d.ts.map +1 -1
- package/dist/renderer/projections/spherical-projection.d.ts +26 -0
- package/dist/renderer/projections/spherical-projection.d.ts.map +1 -0
- package/dist/renderer/renderer.d.ts.map +1 -1
- package/dist/renderer/shaders/fisheye.glsl.d.ts +10 -1
- package/dist/renderer/shaders/fisheye.glsl.d.ts.map +1 -1
- package/dist/sri.json +8 -8
- package/dist/streaming/dash-adapter.d.ts +2 -2
- package/dist/streaming/dash-adapter.d.ts.map +1 -1
- package/dist/streaming/fairplay.d.ts +21 -0
- package/dist/streaming/fairplay.d.ts.map +1 -0
- package/dist/streaming/hls-adapter.d.ts +2 -2
- package/dist/streaming/hls-adapter.d.ts.map +1 -1
- package/dist/streaming/progressive-adapter.d.ts +2 -2
- package/dist/streaming/progressive-adapter.d.ts.map +1 -1
- package/dist/streaming/source-manager.d.ts +3 -1
- package/dist/streaming/source-manager.d.ts.map +1 -1
- package/dist/streaming/types.d.ts +9 -3
- package/dist/streaming/types.d.ts.map +1 -1
- package/dist/styles/controls.d.ts +1 -1
- package/dist/styles/controls.d.ts.map +1 -1
- package/dist/utils/static-html.d.ts +12 -0
- package/dist/utils/static-html.d.ts.map +1 -0
- package/dist/vizor.js +4 -4
- package/package.json +8 -5
- package/dist/__tests__/license-checker.test.d.ts.map +0 -1
- package/dist/__tests__/master-key.test.d.ts.map +0 -1
- package/dist/annotations-C0ugfCn4.js.map +0 -1
- package/dist/captions-BP0H2E03.js.map +0 -1
- package/dist/collab-BXPomrS0.js.map +0 -1
- package/dist/dash-adapter-tid9ZyhO.js.map +0 -1
- package/dist/hls-adapter-BivH46L_.js +0 -97
- package/dist/hls-adapter-BivH46L_.js.map +0 -1
- package/dist/register-CwnmiGEs.js.map +0 -1
- /package/dist/{__tests__ → license/__tests__}/license-checker.test.d.ts +0 -0
- /package/dist/{__tests__ → license/__tests__}/master-key.test.d.ts +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { D as DEFAULT_CONFIG, g as getFeaturesForTier, s as sanitizeUrl, e as parseFormat, f as CAMERA_DEFAULTS, S as SPHERE_DEFAULTS, h as clamp, n as normalizeAngle, i as degToRad, j as getFormatMeta, V as VOLUME_STEP, k as SEEK_STEP, m as ANALYTICS_BATCH_INTERVAL, G as GAZE_SAMPLE_INTERVAL, o as GAZE_BATCH_INTERVAL, A as AnnotationManager, M as MEDIA_EVENTS, l as latLonToSpherical
|
|
1
|
+
import { D as DEFAULT_CONFIG, g as getFeaturesForTier, s as sanitizeUrl, e as parseFormat, f as CAMERA_DEFAULTS, S as SPHERE_DEFAULTS, h as clamp, n as normalizeAngle, i as degToRad, j as getFormatMeta, V as VOLUME_STEP, k as SEEK_STEP, m as ANALYTICS_BATCH_INTERVAL, G as GAZE_SAMPLE_INTERVAL, o as GAZE_BATCH_INTERVAL, q as sanitizeAnnotationHtml, r as VzFormat, A as AnnotationManager, M as MEDIA_EVENTS, l as latLonToSpherical } from "./annotations-Du6ZWrqC.js";
|
|
2
2
|
import { b as SphereGeometry, c as MeshBasicMaterial, d as Mesh, e as MathUtils, P as PlaneGeometry, B as BoxGeometry, f as ShaderMaterial, g as FrontSide, h as BufferAttribute, V as Vector2, i as Scene, j as Color, k as PerspectiveCamera, W as WebGLRenderer, l as VideoTexture, m as SRGBColorSpace, L as LinearFilter, T as TextureLoader, C as CanvasTexture, S as SpriteMaterial, a as Sprite, R as Raycaster } from "./three-Bci7aSeI.js";
|
|
3
|
-
import { I as ICON_PLAY, e as ICON_PAUSE, f as
|
|
3
|
+
import { s as setStaticHtml, I as ICON_PLAY, e as ICON_PAUSE, f as ICON_VOLUME_MUTED, g as ICON_VOLUME_HIGH, h as ICON_VOLUME_LOW, i as ICON_FULLSCREEN, j as ICON_FULLSCREEN_EXIT, k as ICON_VR, l as ICON_COMPASS, m as ICON_SETTINGS, n as ICON_LIVE_DOT, o as ICON_CHAPTERS, p as ICON_CC, G as GazeCursorManager, a as ChatPanel, C as ChatButton, d as VoiceManager, V as VoiceButton, b as CollaborativeClient, c as GazeSender, q as ICON_PLAYLIST_PREV, r as ICON_PLAYLIST_NEXT } from "./collab-BLpllHtb.js";
|
|
4
4
|
import { A as AudioManager } from "./spatial-BeadwNJA.js";
|
|
5
|
-
import { C as CaptionRenderer } from "./captions-
|
|
5
|
+
import { C as CaptionRenderer } from "./captions-YvnHAwLY.js";
|
|
6
6
|
const HTMLElementBase = typeof HTMLElement !== "undefined" ? HTMLElement : class {
|
|
7
7
|
};
|
|
8
8
|
const INITIAL_STATE = {
|
|
@@ -207,23 +207,68 @@ function resolveConfig(elementAttrs, metaOverrides) {
|
|
|
207
207
|
...elementAttrs
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
|
-
function
|
|
210
|
+
async function verifyLicenseKey(licenseKey, trustedPublicKeyHex) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
async function checkMasterKey(licenseKey) {
|
|
214
|
+
const payload = await verifyLicenseKey();
|
|
215
|
+
if (!payload || payload.tier !== "master") return null;
|
|
216
|
+
return {
|
|
217
|
+
valid: true,
|
|
218
|
+
tier: "enterprise",
|
|
219
|
+
features: getFeaturesForTier("enterprise"),
|
|
220
|
+
domains: payload.domains ?? [],
|
|
221
|
+
expiresAt: payload.expiresAt
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const CACHE_PREFIX = "__vz_lk_";
|
|
225
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
226
|
+
function hashString(input) {
|
|
227
|
+
let h = 5381;
|
|
228
|
+
for (let i = 0; i < input.length; i++) {
|
|
229
|
+
h = h * 33 ^ input.charCodeAt(i);
|
|
230
|
+
}
|
|
231
|
+
return (h >>> 0).toString(36);
|
|
232
|
+
}
|
|
233
|
+
function cacheKeyFor(licenseKey, domain) {
|
|
234
|
+
return CACHE_PREFIX + hashString(`${licenseKey}|${domain}`);
|
|
235
|
+
}
|
|
236
|
+
function integrityTag(licenseKey, domain, result, ts) {
|
|
237
|
+
return hashString(`${licenseKey}|${domain}|${JSON.stringify(result)}|${ts}`);
|
|
238
|
+
}
|
|
239
|
+
function writeLicenseCache(licenseKey, domain, result, now = Date.now()) {
|
|
240
|
+
if (typeof localStorage === "undefined") return;
|
|
211
241
|
try {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const envelope = JSON.parse(decoded);
|
|
217
|
-
const payload = JSON.parse(envelope.payload);
|
|
218
|
-
if (payload.tier !== "master") return null;
|
|
219
|
-
if (payload.publicKey !== masterPubKey) return null;
|
|
220
|
-
return {
|
|
221
|
-
valid: true,
|
|
222
|
-
tier: "enterprise",
|
|
223
|
-
features: getFeaturesForTier("enterprise"),
|
|
224
|
-
domains: payload.domains,
|
|
225
|
-
expiresAt: payload.expiresAt
|
|
242
|
+
const envelope = {
|
|
243
|
+
result,
|
|
244
|
+
ts: now,
|
|
245
|
+
tag: integrityTag(licenseKey, domain, result, now)
|
|
226
246
|
};
|
|
247
|
+
localStorage.setItem(cacheKeyFor(licenseKey, domain), JSON.stringify(envelope));
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function readLicenseCache(licenseKey, domain, now = Date.now()) {
|
|
252
|
+
if (typeof localStorage === "undefined") return null;
|
|
253
|
+
const key = cacheKeyFor(licenseKey, domain);
|
|
254
|
+
try {
|
|
255
|
+
const raw = localStorage.getItem(key);
|
|
256
|
+
if (!raw) return null;
|
|
257
|
+
const parsed = JSON.parse(raw);
|
|
258
|
+
if (!parsed || typeof parsed.ts !== "number" || typeof parsed.tag !== "string" || !parsed.result) {
|
|
259
|
+
localStorage.removeItem(key);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (now - parsed.ts > MAX_AGE_MS) {
|
|
263
|
+
localStorage.removeItem(key);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const expected = integrityTag(licenseKey, domain, parsed.result, parsed.ts);
|
|
267
|
+
if (parsed.tag !== expected) {
|
|
268
|
+
localStorage.removeItem(key);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return parsed.result;
|
|
227
272
|
} catch {
|
|
228
273
|
return null;
|
|
229
274
|
}
|
|
@@ -232,7 +277,7 @@ const DEFAULT_API_ENDPOINT = "https://api.vizor.io";
|
|
|
232
277
|
async function checkLicense(config) {
|
|
233
278
|
const mode = config.licenseMode ?? (config.licenseKey ? "standalone" : "saas");
|
|
234
279
|
if (mode === "standalone" && config.licenseKey) {
|
|
235
|
-
const masterResult = checkMasterKey(config.licenseKey);
|
|
280
|
+
const masterResult = await checkMasterKey(config.licenseKey);
|
|
236
281
|
if (masterResult) return masterResult;
|
|
237
282
|
return checkStandaloneLicense(config.licenseKey, config.apiEndpoint, config.skipPhoneHome);
|
|
238
283
|
}
|
|
@@ -261,56 +306,43 @@ async function checkSaasLicense(apiKey, apiEndpoint) {
|
|
|
261
306
|
return freeTierResult("Network error during license validation");
|
|
262
307
|
}
|
|
263
308
|
}
|
|
264
|
-
|
|
309
|
+
function currentHostname() {
|
|
310
|
+
return typeof window !== "undefined" ? window.location.hostname : "localhost";
|
|
311
|
+
}
|
|
265
312
|
async function checkStandaloneLicense(licenseKey, apiEndpoint, skipPhoneHome) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
tier,
|
|
296
|
-
features: getFeaturesForTier(tier),
|
|
297
|
-
domains: payload.domains,
|
|
298
|
-
expiresAt: payload.expiresAt
|
|
299
|
-
};
|
|
300
|
-
if (!skipPhoneHome && typeof window !== "undefined") {
|
|
301
|
-
phoneHome(licenseKey, apiEndpoint, localResult);
|
|
302
|
-
}
|
|
303
|
-
const cached = getCachedResult();
|
|
304
|
-
if (cached) {
|
|
305
|
-
return cached;
|
|
306
|
-
}
|
|
307
|
-
return localResult;
|
|
308
|
-
} catch (err) {
|
|
309
|
-
console.warn("[Vizor] Standalone license decode failed:", err);
|
|
310
|
-
return freeTierResult("Invalid license key format");
|
|
313
|
+
const payload = await verifyLicenseKey();
|
|
314
|
+
if (!payload) {
|
|
315
|
+
return freeTierResult("Invalid or unverifiable license key");
|
|
316
|
+
}
|
|
317
|
+
if (payload.expiresAt) {
|
|
318
|
+
const expires = new Date(payload.expiresAt);
|
|
319
|
+
if (expires < /* @__PURE__ */ new Date()) {
|
|
320
|
+
return freeTierResult("License expired");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const currentDomain = currentHostname();
|
|
324
|
+
const domains = payload.domains ?? [];
|
|
325
|
+
if (domains.length > 0 && !matchesDomain(currentDomain, domains)) {
|
|
326
|
+
return freeTierResult("Domain not authorized");
|
|
327
|
+
}
|
|
328
|
+
const tier = payload.tier;
|
|
329
|
+
const localResult = {
|
|
330
|
+
valid: true,
|
|
331
|
+
tier,
|
|
332
|
+
features: getFeaturesForTier(tier),
|
|
333
|
+
domains,
|
|
334
|
+
expiresAt: payload.expiresAt
|
|
335
|
+
};
|
|
336
|
+
if (!skipPhoneHome && typeof window !== "undefined") {
|
|
337
|
+
phoneHome(licenseKey, currentDomain, apiEndpoint);
|
|
338
|
+
}
|
|
339
|
+
const cached = readLicenseCache(licenseKey, currentDomain);
|
|
340
|
+
if (cached) {
|
|
341
|
+
return cached;
|
|
311
342
|
}
|
|
343
|
+
return localResult;
|
|
312
344
|
}
|
|
313
|
-
function phoneHome(licenseKey,
|
|
345
|
+
function phoneHome(licenseKey, domain, apiEndpoint) {
|
|
314
346
|
const sessionKey = "__vz_ph_done";
|
|
315
347
|
if (typeof sessionStorage !== "undefined") {
|
|
316
348
|
try {
|
|
@@ -320,7 +352,6 @@ function phoneHome(licenseKey, apiEndpoint, fallback) {
|
|
|
320
352
|
}
|
|
321
353
|
}
|
|
322
354
|
const endpoint = apiEndpoint || DEFAULT_API_ENDPOINT;
|
|
323
|
-
const domain = typeof window !== "undefined" ? window.location.hostname : "localhost";
|
|
324
355
|
fetch(`${endpoint}/api/v1/license/validate-standalone`, {
|
|
325
356
|
method: "POST",
|
|
326
357
|
headers: { "Content-Type": "application/json" },
|
|
@@ -329,31 +360,11 @@ function phoneHome(licenseKey, apiEndpoint, fallback) {
|
|
|
329
360
|
if (!res.ok) throw new Error(`Status ${res.status}`);
|
|
330
361
|
return res.json();
|
|
331
362
|
}).then((serverResult) => {
|
|
332
|
-
|
|
333
|
-
localStorage.setItem(
|
|
334
|
-
PHONE_HOME_CACHE_KEY,
|
|
335
|
-
JSON.stringify({ result: serverResult, ts: Date.now() })
|
|
336
|
-
);
|
|
337
|
-
} catch {
|
|
338
|
-
}
|
|
363
|
+
writeLicenseCache(licenseKey, domain, serverResult);
|
|
339
364
|
}).catch(() => {
|
|
340
365
|
console.debug("[Vizor] Phone-home failed, using local license result");
|
|
341
366
|
});
|
|
342
367
|
}
|
|
343
|
-
function getCachedResult() {
|
|
344
|
-
try {
|
|
345
|
-
const raw = localStorage.getItem(PHONE_HOME_CACHE_KEY);
|
|
346
|
-
if (!raw) return null;
|
|
347
|
-
const { result, ts } = JSON.parse(raw);
|
|
348
|
-
if (Date.now() - ts > 24 * 60 * 60 * 1e3) {
|
|
349
|
-
localStorage.removeItem(PHONE_HOME_CACHE_KEY);
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
return result;
|
|
353
|
-
} catch {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
368
|
function freeTierResult(message) {
|
|
358
369
|
return {
|
|
359
370
|
valid: false,
|
|
@@ -371,13 +382,6 @@ function matchesDomain(hostname, allowedDomains) {
|
|
|
371
382
|
return hostname === pattern;
|
|
372
383
|
});
|
|
373
384
|
}
|
|
374
|
-
function hexToBytes(hex) {
|
|
375
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
376
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
377
|
-
bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
378
|
-
}
|
|
379
|
-
return bytes;
|
|
380
|
-
}
|
|
381
385
|
let observer = null;
|
|
382
386
|
let guardActive = false;
|
|
383
387
|
const HARDENED_WATERMARK_CSS = `
|
|
@@ -580,8 +584,10 @@ const ATTR_MAP = {
|
|
|
580
584
|
author: "author",
|
|
581
585
|
"author-href": "authorHref",
|
|
582
586
|
poster: "poster",
|
|
587
|
+
preview: "preview",
|
|
583
588
|
loop: "loop",
|
|
584
589
|
muted: "muted",
|
|
590
|
+
autoplay: "autoplay",
|
|
585
591
|
preload: "preload",
|
|
586
592
|
fps: "fps",
|
|
587
593
|
crossorigin: "crossorigin",
|
|
@@ -596,6 +602,7 @@ const ATTR_MAP = {
|
|
|
596
602
|
"start-lat": "startLat",
|
|
597
603
|
"start-lon": "startLon",
|
|
598
604
|
"focal-length": "focalLength",
|
|
605
|
+
"lens-angle": "lensAngle",
|
|
599
606
|
"min-focal-length": "minFocalLength",
|
|
600
607
|
"max-focal-length": "maxFocalLength",
|
|
601
608
|
"license-key": "licenseKey",
|
|
@@ -626,6 +633,7 @@ const ATTR_MAP = {
|
|
|
626
633
|
const BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
627
634
|
"loop",
|
|
628
635
|
"muted",
|
|
636
|
+
"autoplay",
|
|
629
637
|
"seekable",
|
|
630
638
|
"cors-fallback-redirect-confirmation",
|
|
631
639
|
"hide-compass",
|
|
@@ -641,6 +649,7 @@ const NUMBER_ATTRS = /* @__PURE__ */ new Set([
|
|
|
641
649
|
"fps",
|
|
642
650
|
"start-lat",
|
|
643
651
|
"start-lon",
|
|
652
|
+
"lens-angle",
|
|
644
653
|
"focal-length",
|
|
645
654
|
"min-focal-length",
|
|
646
655
|
"max-focal-length"
|
|
@@ -672,6 +681,27 @@ class VzBaseElement extends HTMLElementBase {
|
|
|
672
681
|
this.resizeObserver = new ResizeObserver(() => this.onResize());
|
|
673
682
|
this.resizeObserver.observe(this);
|
|
674
683
|
this.injectBaseStyles();
|
|
684
|
+
const pending = this.beforeConnect();
|
|
685
|
+
if (pending && typeof pending.then === "function") {
|
|
686
|
+
pending.then(
|
|
687
|
+
() => this.finishConnect(),
|
|
688
|
+
() => this.finishConnect()
|
|
689
|
+
);
|
|
690
|
+
} else {
|
|
691
|
+
this.finishConnect();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Optional async hook run after config/styles are resolved but BEFORE
|
|
696
|
+
* onConnected(). Return a promise to defer onConnected() until it settles
|
|
697
|
+
* (e.g. to inject fetched DOM children); return void (default) to keep the
|
|
698
|
+
* connect path synchronous. Subclasses override.
|
|
699
|
+
*/
|
|
700
|
+
beforeConnect() {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
finishConnect() {
|
|
704
|
+
if (!this.isConnected) return;
|
|
675
705
|
this.onConnected();
|
|
676
706
|
this.checkLicenseAsync();
|
|
677
707
|
}
|
|
@@ -1310,6 +1340,7 @@ uniform float uScaleX;
|
|
|
1310
1340
|
uniform float uScaleY;
|
|
1311
1341
|
uniform float uOffsetX;
|
|
1312
1342
|
uniform float uOffsetY;
|
|
1343
|
+
uniform float uLensFov; // capture-lens angle in RADIANS (WS-F, 120-250 deg)
|
|
1313
1344
|
|
|
1314
1345
|
varying vec3 vWorldPosition;
|
|
1315
1346
|
|
|
@@ -1328,9 +1359,12 @@ void main() {
|
|
|
1328
1359
|
// Azimuthal angle around the hemisphere pole
|
|
1329
1360
|
float phi = atan(dir.y, dir.x * faceSign);
|
|
1330
1361
|
|
|
1331
|
-
// Map to circular fisheye UV using equidistant projection
|
|
1332
|
-
// r
|
|
1333
|
-
|
|
1362
|
+
// Map to circular fisheye UV using equidistant projection.
|
|
1363
|
+
// r = 1 at the lens edge (uLensFov/2 from the pole). For lenses wider than
|
|
1364
|
+
// 180 deg the hemisphere uses only the inner part of the circle; for
|
|
1365
|
+
// narrower lenses r can exceed 1 — clamp to the edge (looks better than
|
|
1366
|
+
// holes; decided by visual test).
|
|
1367
|
+
float r = min(theta / (uLensFov * 0.5), 1.0);
|
|
1334
1368
|
|
|
1335
1369
|
// Convert polar (r, phi) to cartesian circle coordinates, centered at (0.5, 0.5)
|
|
1336
1370
|
vec2 circleUV = vec2(r * cos(phi), r * sin(phi)) * 0.5 + 0.5;
|
|
@@ -1347,6 +1381,41 @@ void main() {
|
|
|
1347
1381
|
}
|
|
1348
1382
|
`
|
|
1349
1383
|
);
|
|
1384
|
+
const sphericalFragmentShader = (
|
|
1385
|
+
/* glsl */
|
|
1386
|
+
`
|
|
1387
|
+
uniform sampler2D map;
|
|
1388
|
+
uniform float uScaleX;
|
|
1389
|
+
uniform float uScaleY;
|
|
1390
|
+
uniform float uOffsetX;
|
|
1391
|
+
uniform float uOffsetY;
|
|
1392
|
+
uniform float uLensFov; // capture-lens angle in RADIANS
|
|
1393
|
+
|
|
1394
|
+
varying vec3 vWorldPosition;
|
|
1395
|
+
|
|
1396
|
+
void main() {
|
|
1397
|
+
vec3 dir = normalize(vWorldPosition);
|
|
1398
|
+
|
|
1399
|
+
// Angle from the forward optical axis (0 straight ahead). The camera
|
|
1400
|
+
// controller's default view (lat 0, lon 0) looks along +x, so that is the
|
|
1401
|
+
// content center for VR180 footage.
|
|
1402
|
+
float theta = acos(clamp(dir.x, -1.0, 1.0));
|
|
1403
|
+
|
|
1404
|
+
// Azimuth around the axis: +z is screen-right at the default view, +y up.
|
|
1405
|
+
float phi = atan(dir.y, dir.z);
|
|
1406
|
+
|
|
1407
|
+
// Equidistant mapping; clamp beyond the lens edge.
|
|
1408
|
+
float r = min(theta / (uLensFov * 0.5), 1.0);
|
|
1409
|
+
|
|
1410
|
+
vec2 circleUV = vec2(r * cos(phi), r * sin(phi)) * 0.5 + 0.5;
|
|
1411
|
+
|
|
1412
|
+
// Stereo region selection (full texture for mono).
|
|
1413
|
+
vec2 uv = vec2(circleUV.x * uScaleX + uOffsetX, circleUV.y * uScaleY + uOffsetY);
|
|
1414
|
+
|
|
1415
|
+
gl_FragColor = texture2D(map, uv);
|
|
1416
|
+
}
|
|
1417
|
+
`
|
|
1418
|
+
);
|
|
1350
1419
|
function stereoUniforms(stereoMode, eye) {
|
|
1351
1420
|
switch (stereoMode) {
|
|
1352
1421
|
case "sbs":
|
|
@@ -1374,8 +1443,16 @@ function stereoUniforms(stereoMode, eye) {
|
|
|
1374
1443
|
};
|
|
1375
1444
|
}
|
|
1376
1445
|
}
|
|
1446
|
+
function clampLensAngle(deg) {
|
|
1447
|
+
if (deg == null || Number.isNaN(deg)) return 180;
|
|
1448
|
+
return Math.min(250, Math.max(120, deg));
|
|
1449
|
+
}
|
|
1377
1450
|
class FisheyeProjection {
|
|
1378
1451
|
meshes = [];
|
|
1452
|
+
lensFovRad;
|
|
1453
|
+
constructor(lensAngleDeg) {
|
|
1454
|
+
this.lensFovRad = clampLensAngle(lensAngleDeg) * Math.PI / 180;
|
|
1455
|
+
}
|
|
1379
1456
|
createMeshes(texture, stereoMode) {
|
|
1380
1457
|
this.dispose();
|
|
1381
1458
|
if (stereoMode === "mono") {
|
|
@@ -1419,7 +1496,8 @@ class FisheyeProjection {
|
|
|
1419
1496
|
uScaleX: { value: uniforms.uScaleX },
|
|
1420
1497
|
uScaleY: { value: uniforms.uScaleY },
|
|
1421
1498
|
uOffsetX: { value: uniforms.uOffsetX },
|
|
1422
|
-
uOffsetY: { value: uniforms.uOffsetY }
|
|
1499
|
+
uOffsetY: { value: uniforms.uOffsetY },
|
|
1500
|
+
uLensFov: { value: this.lensFovRad }
|
|
1423
1501
|
},
|
|
1424
1502
|
vertexShader: fisheyeVertexShader,
|
|
1425
1503
|
fragmentShader: fisheyeFragmentShader,
|
|
@@ -1428,6 +1506,72 @@ class FisheyeProjection {
|
|
|
1428
1506
|
return new Mesh(geometry, material);
|
|
1429
1507
|
}
|
|
1430
1508
|
}
|
|
1509
|
+
class SphericalProjection {
|
|
1510
|
+
meshes = [];
|
|
1511
|
+
lensFovRad;
|
|
1512
|
+
constructor(lensAngleDeg) {
|
|
1513
|
+
this.lensFovRad = clampLensAngle(lensAngleDeg) * Math.PI / 180;
|
|
1514
|
+
}
|
|
1515
|
+
createMeshes(texture, stereoMode) {
|
|
1516
|
+
this.dispose();
|
|
1517
|
+
if (stereoMode === "mono") {
|
|
1518
|
+
const mesh = this.createSphere(texture, { uScaleX: 1, uScaleY: 1, uOffsetX: 0, uOffsetY: 0 });
|
|
1519
|
+
mesh.layers.set(0);
|
|
1520
|
+
this.meshes = [mesh];
|
|
1521
|
+
return this.meshes;
|
|
1522
|
+
}
|
|
1523
|
+
const effectiveMode = stereoMode === "separate" ? "tb" : stereoMode;
|
|
1524
|
+
const rects = effectiveMode === "sbs" ? {
|
|
1525
|
+
left: { uScaleX: 0.5, uScaleY: 1, uOffsetX: 0, uOffsetY: 0 },
|
|
1526
|
+
right: { uScaleX: 0.5, uScaleY: 1, uOffsetX: 0.5, uOffsetY: 0 }
|
|
1527
|
+
} : {
|
|
1528
|
+
// TB: left eye top half, right eye bottom half (matches fisheye).
|
|
1529
|
+
left: { uScaleX: 1, uScaleY: 0.5, uOffsetX: 0, uOffsetY: 0.5 },
|
|
1530
|
+
right: { uScaleX: 1, uScaleY: 0.5, uOffsetX: 0, uOffsetY: 0 }
|
|
1531
|
+
};
|
|
1532
|
+
const meshL = this.createSphere(texture, rects.left);
|
|
1533
|
+
const meshR = this.createSphere(texture, rects.right);
|
|
1534
|
+
meshL.layers.set(1);
|
|
1535
|
+
meshR.layers.set(2);
|
|
1536
|
+
this.meshes = [meshL, meshR];
|
|
1537
|
+
return this.meshes;
|
|
1538
|
+
}
|
|
1539
|
+
configureCamera(camera) {
|
|
1540
|
+
camera.fov = CAMERA_DEFAULTS.fov;
|
|
1541
|
+
camera.near = CAMERA_DEFAULTS.near;
|
|
1542
|
+
camera.far = CAMERA_DEFAULTS.far;
|
|
1543
|
+
camera.position.set(0, 0, 0);
|
|
1544
|
+
camera.updateProjectionMatrix();
|
|
1545
|
+
}
|
|
1546
|
+
update(_camera, _time) {
|
|
1547
|
+
}
|
|
1548
|
+
dispose() {
|
|
1549
|
+
for (const mesh of this.meshes) {
|
|
1550
|
+
mesh.geometry.dispose();
|
|
1551
|
+
mesh.material.dispose();
|
|
1552
|
+
}
|
|
1553
|
+
this.meshes = [];
|
|
1554
|
+
}
|
|
1555
|
+
createSphere(texture, rect) {
|
|
1556
|
+
const { radius, widthSegments, heightSegments } = SPHERE_DEFAULTS;
|
|
1557
|
+
const geometry = new SphereGeometry(radius, widthSegments, heightSegments);
|
|
1558
|
+
geometry.scale(-1, 1, 1);
|
|
1559
|
+
const material = new ShaderMaterial({
|
|
1560
|
+
uniforms: {
|
|
1561
|
+
map: { value: texture },
|
|
1562
|
+
uScaleX: { value: rect.uScaleX },
|
|
1563
|
+
uScaleY: { value: rect.uScaleY },
|
|
1564
|
+
uOffsetX: { value: rect.uOffsetX },
|
|
1565
|
+
uOffsetY: { value: rect.uOffsetY },
|
|
1566
|
+
uLensFov: { value: this.lensFovRad }
|
|
1567
|
+
},
|
|
1568
|
+
vertexShader: fisheyeVertexShader,
|
|
1569
|
+
fragmentShader: sphericalFragmentShader,
|
|
1570
|
+
side: FrontSide
|
|
1571
|
+
});
|
|
1572
|
+
return new Mesh(geometry, material);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1431
1575
|
class CameraController {
|
|
1432
1576
|
camera;
|
|
1433
1577
|
state;
|
|
@@ -1686,7 +1830,10 @@ class VzRenderer {
|
|
|
1686
1830
|
minFocalLength: config.minFocalLength,
|
|
1687
1831
|
maxFocalLength: config.maxFocalLength,
|
|
1688
1832
|
isPanoramic,
|
|
1689
|
-
|
|
1833
|
+
// Spherical (VR180-family) content pans as far as the capture lens
|
|
1834
|
+
// covers — the lens angle IS the horizontal FOV (WS-F). Other formats
|
|
1835
|
+
// keep their registry fovH (fisheye duals are 360).
|
|
1836
|
+
fovH: formatMeta?.projection === "spherical" ? clampLensAngle(this.config.lensAngle) : formatMeta?.fovH
|
|
1690
1837
|
});
|
|
1691
1838
|
this.resize();
|
|
1692
1839
|
}
|
|
@@ -1845,8 +1992,9 @@ class VzRenderer {
|
|
|
1845
1992
|
createProjection(formatMeta) {
|
|
1846
1993
|
switch (formatMeta.projection) {
|
|
1847
1994
|
case "equirect":
|
|
1848
|
-
case "spherical":
|
|
1849
1995
|
return new EquirectProjection(formatMeta.fovH);
|
|
1996
|
+
case "spherical":
|
|
1997
|
+
return new SphericalProjection(this.config.lensAngle);
|
|
1850
1998
|
case "flat":
|
|
1851
1999
|
return new FlatProjection();
|
|
1852
2000
|
case "cubemap":
|
|
@@ -1854,7 +2002,7 @@ class VzRenderer {
|
|
|
1854
2002
|
case "eac":
|
|
1855
2003
|
return new EacProjection();
|
|
1856
2004
|
case "fisheye":
|
|
1857
|
-
return new FisheyeProjection();
|
|
2005
|
+
return new FisheyeProjection(this.config.lensAngle);
|
|
1858
2006
|
default:
|
|
1859
2007
|
return new EquirectProjection(360);
|
|
1860
2008
|
}
|
|
@@ -1911,7 +2059,7 @@ class ProgressiveAdapter {
|
|
|
1911
2059
|
this.videoElement = videoElement;
|
|
1912
2060
|
this._isAttached = true;
|
|
1913
2061
|
}
|
|
1914
|
-
load(url) {
|
|
2062
|
+
load(url, _drm) {
|
|
1915
2063
|
if (!this.videoElement) return;
|
|
1916
2064
|
this.videoElement.src = url;
|
|
1917
2065
|
}
|
|
@@ -1931,11 +2079,11 @@ class ProgressiveAdapter {
|
|
|
1931
2079
|
async function createAdapter(type) {
|
|
1932
2080
|
switch (type) {
|
|
1933
2081
|
case "hls": {
|
|
1934
|
-
const { HlsAdapter } = await import("./hls-adapter-
|
|
2082
|
+
const { HlsAdapter } = await import("./hls-adapter-C_a65zia.js");
|
|
1935
2083
|
return new HlsAdapter();
|
|
1936
2084
|
}
|
|
1937
2085
|
case "dash": {
|
|
1938
|
-
const { DashAdapter } = await import("./dash-adapter-
|
|
2086
|
+
const { DashAdapter } = await import("./dash-adapter-BMpHWcBA.js");
|
|
1939
2087
|
return new DashAdapter();
|
|
1940
2088
|
}
|
|
1941
2089
|
case "progressive":
|
|
@@ -1949,20 +2097,22 @@ class SourceManager {
|
|
|
1949
2097
|
state = null;
|
|
1950
2098
|
events = null;
|
|
1951
2099
|
sources = [];
|
|
2100
|
+
drm;
|
|
1952
2101
|
/**
|
|
1953
2102
|
* Load sources into the video element using the appropriate adapter.
|
|
1954
2103
|
*/
|
|
1955
|
-
async loadSources(sources, videoElement, state, events) {
|
|
2104
|
+
async loadSources(sources, videoElement, state, events, drm) {
|
|
1956
2105
|
this.sources = sources;
|
|
1957
2106
|
this.videoElement = videoElement;
|
|
1958
2107
|
this.state = state;
|
|
1959
2108
|
this.events = events;
|
|
2109
|
+
this.drm = drm;
|
|
1960
2110
|
if (sources.length === 0) return;
|
|
1961
2111
|
this.adapter?.destroy();
|
|
1962
2112
|
const { adapterType, source } = this.selectSource(sources);
|
|
1963
2113
|
this.adapter = await createAdapter(adapterType);
|
|
1964
2114
|
this.adapter.attach(videoElement);
|
|
1965
|
-
this.adapter.load(source.url);
|
|
2115
|
+
this.adapter.load(source.url, drm);
|
|
1966
2116
|
this.pollQualityLevels();
|
|
1967
2117
|
}
|
|
1968
2118
|
/** Get available quality levels from the current adapter */
|
|
@@ -1993,7 +2143,7 @@ class SourceManager {
|
|
|
1993
2143
|
if (!this.videoElement || !this.adapter || index >= this.sources.length) return;
|
|
1994
2144
|
const currentTime = this.videoElement.currentTime;
|
|
1995
2145
|
const wasPlaying = !this.videoElement.paused;
|
|
1996
|
-
this.adapter.load(this.sources[index].url);
|
|
2146
|
+
this.adapter.load(this.sources[index].url, this.drm);
|
|
1997
2147
|
const onLoaded = () => {
|
|
1998
2148
|
this.videoElement.currentTime = currentTime;
|
|
1999
2149
|
if (wasPlaying) this.videoElement.play().catch(() => {
|
|
@@ -2220,6 +2370,24 @@ const CONTROLS_CSS = `
|
|
|
2220
2370
|
height: 5px;
|
|
2221
2371
|
}
|
|
2222
2372
|
|
|
2373
|
+
.vz-seek-chapters {
|
|
2374
|
+
position: absolute;
|
|
2375
|
+
inset: 0;
|
|
2376
|
+
pointer-events: none;
|
|
2377
|
+
z-index: 3;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
.vz-seek-chapter-marker {
|
|
2381
|
+
position: absolute;
|
|
2382
|
+
top: 50%;
|
|
2383
|
+
width: 2px;
|
|
2384
|
+
height: 100%;
|
|
2385
|
+
transform: translate(-50%, -50%);
|
|
2386
|
+
background: rgba(0, 0, 0, 0.55);
|
|
2387
|
+
box-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.5);
|
|
2388
|
+
pointer-events: none;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2223
2391
|
.vz-seek-buffered {
|
|
2224
2392
|
position: absolute;
|
|
2225
2393
|
top: 0;
|
|
@@ -2702,6 +2870,74 @@ const CONTROLS_CSS = `
|
|
|
2702
2870
|
font-size: 14px;
|
|
2703
2871
|
}
|
|
2704
2872
|
|
|
2873
|
+
/* ------------------------------------------------------------------ */
|
|
2874
|
+
/* Chapters menu */
|
|
2875
|
+
/* ------------------------------------------------------------------ */
|
|
2876
|
+
|
|
2877
|
+
.vz-chapter-menu {
|
|
2878
|
+
position: relative;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
.vz-chapter-dropdown {
|
|
2882
|
+
position: absolute;
|
|
2883
|
+
bottom: 100%;
|
|
2884
|
+
right: 0;
|
|
2885
|
+
margin-bottom: 8px;
|
|
2886
|
+
background: var(--vz-secondary, rgba(20, 20, 30, 0.95));
|
|
2887
|
+
backdrop-filter: blur(16px) saturate(1.3);
|
|
2888
|
+
-webkit-backdrop-filter: blur(16px) saturate(1.3);
|
|
2889
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
2890
|
+
border-radius: 8px;
|
|
2891
|
+
padding: 4px 0;
|
|
2892
|
+
min-width: 200px;
|
|
2893
|
+
max-height: 260px;
|
|
2894
|
+
overflow-y: auto;
|
|
2895
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
2896
|
+
display: none;
|
|
2897
|
+
z-index: 20;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
.vz-chapter-dropdown.open {
|
|
2901
|
+
display: block;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
.vz-chapter-option {
|
|
2905
|
+
display: flex;
|
|
2906
|
+
align-items: baseline;
|
|
2907
|
+
gap: 10px;
|
|
2908
|
+
padding: 8px 16px;
|
|
2909
|
+
cursor: pointer;
|
|
2910
|
+
font-size: 13px;
|
|
2911
|
+
color: rgba(255, 255, 255, 0.7);
|
|
2912
|
+
white-space: nowrap;
|
|
2913
|
+
transition: color 0.12s, background 0.12s;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
.vz-chapter-option:hover {
|
|
2917
|
+
background: color-mix(in srgb, var(--vz-primary, #f43f5e) 8%, transparent);
|
|
2918
|
+
color: #fff;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
.vz-chapter-option.active {
|
|
2922
|
+
color: var(--vz-primary, #f43f5e);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
.vz-chapter-time {
|
|
2926
|
+
font-variant-numeric: tabular-nums;
|
|
2927
|
+
font-size: 12px;
|
|
2928
|
+
color: rgba(255, 255, 255, 0.45);
|
|
2929
|
+
flex-shrink: 0;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
.vz-chapter-option.active .vz-chapter-time {
|
|
2933
|
+
color: var(--vz-primary, #f43f5e);
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
.vz-chapter-label {
|
|
2937
|
+
overflow: hidden;
|
|
2938
|
+
text-overflow: ellipsis;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2705
2941
|
/* ------------------------------------------------------------------ */
|
|
2706
2942
|
/* Chat badge */
|
|
2707
2943
|
/* ------------------------------------------------------------------ */
|
|
@@ -2779,7 +3015,7 @@ class ControlsOverlay {
|
|
|
2779
3015
|
containerEl.appendChild(this.wrapper);
|
|
2780
3016
|
this.loadingEl = document.createElement("div");
|
|
2781
3017
|
this.loadingEl.className = "vz-loading hidden";
|
|
2782
|
-
this.loadingEl
|
|
3018
|
+
setStaticHtml(this.loadingEl, '<div class="vz-loading-spinner"></div>');
|
|
2783
3019
|
containerEl.appendChild(this.loadingEl);
|
|
2784
3020
|
const onActivity = () => this.show();
|
|
2785
3021
|
containerEl.addEventListener("pointermove", onActivity);
|
|
@@ -2883,11 +3119,11 @@ class PlayButton {
|
|
|
2883
3119
|
this.button = document.createElement("button");
|
|
2884
3120
|
this.button.className = "vz-btn vz-play-btn";
|
|
2885
3121
|
this.button.setAttribute("aria-label", "Play");
|
|
2886
|
-
this.button
|
|
3122
|
+
setStaticHtml(this.button, ICON_PLAY);
|
|
2887
3123
|
this.bigPlay = document.createElement("button");
|
|
2888
3124
|
this.bigPlay.className = "vz-big-play";
|
|
2889
3125
|
this.bigPlay.setAttribute("aria-label", "Play");
|
|
2890
|
-
this.bigPlay
|
|
3126
|
+
setStaticHtml(this.bigPlay, ICON_PLAY);
|
|
2891
3127
|
containerEl.appendChild(this.bigPlay);
|
|
2892
3128
|
const toggle = () => {
|
|
2893
3129
|
if (state.get("paused") || state.get("ended")) {
|
|
@@ -2901,14 +3137,14 @@ class PlayButton {
|
|
|
2901
3137
|
this.bigPlay.addEventListener("click", toggle);
|
|
2902
3138
|
this.bigPlay.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
2903
3139
|
const unsubPlaying = state.subscribe("playing", (playing) => {
|
|
2904
|
-
this.button
|
|
3140
|
+
setStaticHtml(this.button, playing ? ICON_PAUSE : ICON_PLAY);
|
|
2905
3141
|
this.button.setAttribute("aria-label", playing ? "Pause" : "Play");
|
|
2906
3142
|
this.bigPlay.classList.toggle("hidden", playing);
|
|
2907
3143
|
});
|
|
2908
3144
|
this.unsubscribes.push(unsubPlaying);
|
|
2909
3145
|
const unsubEnded = state.subscribe("ended", (ended) => {
|
|
2910
3146
|
if (ended) {
|
|
2911
|
-
this.button
|
|
3147
|
+
setStaticHtml(this.button, ICON_PLAY);
|
|
2912
3148
|
this.button.setAttribute("aria-label", "Play");
|
|
2913
3149
|
this.bigPlay.classList.remove("hidden");
|
|
2914
3150
|
}
|
|
@@ -2932,6 +3168,7 @@ class SeekBar {
|
|
|
2932
3168
|
tooltip;
|
|
2933
3169
|
input;
|
|
2934
3170
|
track;
|
|
3171
|
+
chapterLayer = null;
|
|
2935
3172
|
unsubscribes = [];
|
|
2936
3173
|
constructor(state, mediaElement) {
|
|
2937
3174
|
this.timeDisplay = document.createElement("span");
|
|
@@ -3005,6 +3242,27 @@ class SeekBar {
|
|
|
3005
3242
|
getElement() {
|
|
3006
3243
|
return this.container;
|
|
3007
3244
|
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Render chapter tick markers on the seek track. Markers are purely visual
|
|
3247
|
+
* (pointer-events: none) so they never interfere with the range input; the
|
|
3248
|
+
* native `title` shows the chapter label on hover. Re-render on duration
|
|
3249
|
+
* change by calling again; pass [] to clear.
|
|
3250
|
+
*/
|
|
3251
|
+
setChapterMarkers(markers) {
|
|
3252
|
+
if (!this.chapterLayer) {
|
|
3253
|
+
this.chapterLayer = document.createElement("div");
|
|
3254
|
+
this.chapterLayer.className = "vz-seek-chapters";
|
|
3255
|
+
this.track.appendChild(this.chapterLayer);
|
|
3256
|
+
}
|
|
3257
|
+
this.chapterLayer.replaceChildren();
|
|
3258
|
+
for (const m of markers) {
|
|
3259
|
+
const tick = document.createElement("div");
|
|
3260
|
+
tick.className = "vz-seek-chapter-marker";
|
|
3261
|
+
tick.style.left = `${m.position * 100}%`;
|
|
3262
|
+
tick.title = m.title;
|
|
3263
|
+
this.chapterLayer.appendChild(tick);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3008
3266
|
getTimeDisplay() {
|
|
3009
3267
|
return this.timeDisplay;
|
|
3010
3268
|
}
|
|
@@ -3041,7 +3299,7 @@ class VolumeControl {
|
|
|
3041
3299
|
const muteBtn = document.createElement("button");
|
|
3042
3300
|
muteBtn.className = "vz-btn vz-mute-btn";
|
|
3043
3301
|
muteBtn.setAttribute("aria-label", "Mute");
|
|
3044
|
-
muteBtn
|
|
3302
|
+
setStaticHtml(muteBtn, ICON_VOLUME_HIGH);
|
|
3045
3303
|
muteBtn.addEventListener("click", () => {
|
|
3046
3304
|
mediaElement.muted = !mediaElement.muted;
|
|
3047
3305
|
});
|
|
@@ -3074,12 +3332,12 @@ class VolumeControl {
|
|
|
3074
3332
|
slider.value = String(pct);
|
|
3075
3333
|
updateSliderTrack(pct);
|
|
3076
3334
|
if (!state.get("muted")) {
|
|
3077
|
-
muteBtn
|
|
3335
|
+
setStaticHtml(muteBtn, volume > 0.5 ? ICON_VOLUME_HIGH : volume > 0 ? ICON_VOLUME_LOW : ICON_VOLUME_MUTED);
|
|
3078
3336
|
}
|
|
3079
3337
|
});
|
|
3080
3338
|
this.unsubscribes.push(unsubVolume);
|
|
3081
3339
|
const unsubMuted = state.subscribe("muted", (muted) => {
|
|
3082
|
-
muteBtn
|
|
3340
|
+
setStaticHtml(muteBtn, muted ? ICON_VOLUME_MUTED : state.get("volume") > 0.5 ? ICON_VOLUME_HIGH : ICON_VOLUME_LOW);
|
|
3083
3341
|
muteBtn.setAttribute("aria-label", muted ? "Unmute" : "Mute");
|
|
3084
3342
|
});
|
|
3085
3343
|
this.unsubscribes.push(unsubMuted);
|
|
@@ -3098,12 +3356,12 @@ class FullscreenButton {
|
|
|
3098
3356
|
this.button = document.createElement("button");
|
|
3099
3357
|
this.button.className = "vz-btn vz-fullscreen-btn";
|
|
3100
3358
|
this.button.setAttribute("aria-label", "Enter fullscreen");
|
|
3101
|
-
this.button
|
|
3359
|
+
setStaticHtml(this.button, ICON_FULLSCREEN);
|
|
3102
3360
|
this.button.addEventListener("click", () => {
|
|
3103
3361
|
mediaElement.toggleFullscreen();
|
|
3104
3362
|
});
|
|
3105
3363
|
const unsubFs = state.subscribe("fullscreen", (fs) => {
|
|
3106
|
-
this.button
|
|
3364
|
+
setStaticHtml(this.button, fs ? ICON_FULLSCREEN_EXIT : ICON_FULLSCREEN);
|
|
3107
3365
|
this.button.setAttribute("aria-label", fs ? "Exit fullscreen" : "Enter fullscreen");
|
|
3108
3366
|
});
|
|
3109
3367
|
this.unsubscribes.push(unsubFs);
|
|
@@ -3122,7 +3380,7 @@ class VrButton {
|
|
|
3122
3380
|
this.button = document.createElement("button");
|
|
3123
3381
|
this.button.className = "vz-btn vz-vr-btn";
|
|
3124
3382
|
this.button.setAttribute("aria-label", "Enter VR");
|
|
3125
|
-
this.button
|
|
3383
|
+
setStaticHtml(this.button, ICON_VR);
|
|
3126
3384
|
this.button.style.display = "none";
|
|
3127
3385
|
this.button.addEventListener("click", () => {
|
|
3128
3386
|
onToggleXR?.();
|
|
@@ -3155,7 +3413,7 @@ class Compass {
|
|
|
3155
3413
|
this.button = document.createElement("button");
|
|
3156
3414
|
this.button.className = "vz-btn vz-compass";
|
|
3157
3415
|
this.button.setAttribute("aria-label", "Reset to north");
|
|
3158
|
-
this.button
|
|
3416
|
+
setStaticHtml(this.button, ICON_COMPASS);
|
|
3159
3417
|
this.button.addEventListener("click", () => {
|
|
3160
3418
|
const controller = renderer.getCameraController();
|
|
3161
3419
|
controller.setOrientation(state.get("lat"), 0);
|
|
@@ -3186,7 +3444,7 @@ class QualityMenu {
|
|
|
3186
3444
|
const btn = document.createElement("button");
|
|
3187
3445
|
btn.className = "vz-btn vz-quality-btn";
|
|
3188
3446
|
btn.setAttribute("aria-label", "Quality");
|
|
3189
|
-
btn
|
|
3447
|
+
setStaticHtml(btn, ICON_SETTINGS);
|
|
3190
3448
|
this.dropdown = document.createElement("div");
|
|
3191
3449
|
this.dropdown.className = "vz-quality-dropdown";
|
|
3192
3450
|
this.container.appendChild(btn);
|
|
@@ -3220,10 +3478,13 @@ class QualityMenu {
|
|
|
3220
3478
|
renderOptions(state, sourceManager) {
|
|
3221
3479
|
const levels = state.get("availableQualities");
|
|
3222
3480
|
const currentQuality = state.get("currentQuality");
|
|
3223
|
-
this.dropdown.
|
|
3481
|
+
this.dropdown.replaceChildren();
|
|
3224
3482
|
const autoOption = document.createElement("div");
|
|
3225
3483
|
autoOption.className = `vz-quality-option ${currentQuality === "auto" ? "active" : ""}`;
|
|
3226
|
-
|
|
3484
|
+
const autoCheck = document.createElement("span");
|
|
3485
|
+
autoCheck.className = "vz-quality-check";
|
|
3486
|
+
autoCheck.textContent = currentQuality === "auto" ? "✓" : "";
|
|
3487
|
+
autoOption.append(autoCheck, document.createTextNode("Auto"));
|
|
3227
3488
|
autoOption.addEventListener("click", () => {
|
|
3228
3489
|
sourceManager.setQualityLevel(-1);
|
|
3229
3490
|
this.isOpen = false;
|
|
@@ -3234,7 +3495,10 @@ class QualityMenu {
|
|
|
3234
3495
|
const option = document.createElement("div");
|
|
3235
3496
|
const isActive = level.label === currentQuality;
|
|
3236
3497
|
option.className = `vz-quality-option ${isActive ? "active" : ""}`;
|
|
3237
|
-
|
|
3498
|
+
const check = document.createElement("span");
|
|
3499
|
+
check.className = "vz-quality-check";
|
|
3500
|
+
check.textContent = isActive ? "✓" : "";
|
|
3501
|
+
option.append(check, document.createTextNode(level.label));
|
|
3238
3502
|
option.addEventListener("click", () => {
|
|
3239
3503
|
sourceManager.setQualityLevel(index);
|
|
3240
3504
|
this.isOpen = false;
|
|
@@ -3485,7 +3749,7 @@ class LiveIndicator {
|
|
|
3485
3749
|
this.container.title = "Jump to live";
|
|
3486
3750
|
this.dotEl = document.createElement("span");
|
|
3487
3751
|
this.dotEl.className = "vz-live-dot at-edge";
|
|
3488
|
-
this.dotEl
|
|
3752
|
+
setStaticHtml(this.dotEl, ICON_LIVE_DOT);
|
|
3489
3753
|
this.labelEl = document.createElement("span");
|
|
3490
3754
|
this.labelEl.className = "vz-live-label";
|
|
3491
3755
|
this.labelEl.textContent = "LIVE";
|
|
@@ -3708,16 +3972,55 @@ function initAnalytics(state, events, options) {
|
|
|
3708
3972
|
apiEndpoint: options.apiEndpoint,
|
|
3709
3973
|
apiKey: options.apiKey
|
|
3710
3974
|
});
|
|
3975
|
+
const WATCH_TIME_REPORT_SECONDS = 60;
|
|
3976
|
+
let playStartMs = null;
|
|
3977
|
+
let accumulatedMs = 0;
|
|
3978
|
+
const reportWatchTime = () => {
|
|
3979
|
+
if (playStartMs != null) {
|
|
3980
|
+
accumulatedMs += Date.now() - playStartMs;
|
|
3981
|
+
playStartMs = state.get("playing") ? Date.now() : null;
|
|
3982
|
+
}
|
|
3983
|
+
const seconds = Math.round(accumulatedMs / 1e3);
|
|
3984
|
+
if (seconds >= 1) {
|
|
3985
|
+
const quality = state.get("currentQuality");
|
|
3986
|
+
collector.track("watch_time", { seconds, ...quality ? { quality } : {} });
|
|
3987
|
+
accumulatedMs = 0;
|
|
3988
|
+
}
|
|
3989
|
+
};
|
|
3990
|
+
const watchTimer = setInterval(() => {
|
|
3991
|
+
const liveMs = accumulatedMs + (playStartMs != null ? Date.now() - playStartMs : 0);
|
|
3992
|
+
if (liveMs >= WATCH_TIME_REPORT_SECONDS * 1e3) reportWatchTime();
|
|
3993
|
+
}, 1e4);
|
|
3994
|
+
cleanups.push(() => clearInterval(watchTimer));
|
|
3995
|
+
if (typeof window !== "undefined") {
|
|
3996
|
+
const watchVisibilityHandler = () => {
|
|
3997
|
+
if (document.visibilityState === "hidden") {
|
|
3998
|
+
reportWatchTime();
|
|
3999
|
+
void collector.flush();
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
window.addEventListener("visibilitychange", watchVisibilityHandler);
|
|
4003
|
+
cleanups.push(() => window.removeEventListener("visibilitychange", watchVisibilityHandler));
|
|
4004
|
+
}
|
|
3711
4005
|
cleanups.push(
|
|
3712
4006
|
state.subscribe("playing", (playing) => {
|
|
3713
4007
|
collector.track(playing ? "play" : "pause");
|
|
4008
|
+
if (playing) {
|
|
4009
|
+
playStartMs = Date.now();
|
|
4010
|
+
} else {
|
|
4011
|
+
reportWatchTime();
|
|
4012
|
+
}
|
|
3714
4013
|
})
|
|
3715
4014
|
);
|
|
3716
4015
|
cleanups.push(
|
|
3717
4016
|
state.subscribe("ended", (ended) => {
|
|
3718
|
-
if (ended)
|
|
4017
|
+
if (ended) {
|
|
4018
|
+
collector.track("ended");
|
|
4019
|
+
reportWatchTime();
|
|
4020
|
+
}
|
|
3719
4021
|
})
|
|
3720
4022
|
);
|
|
4023
|
+
cleanups.push(() => reportWatchTime());
|
|
3721
4024
|
cleanups.push(
|
|
3722
4025
|
state.subscribe("seeking", (seeking) => {
|
|
3723
4026
|
if (seeking) collector.track("seek", { time: state.get("currentTime") });
|
|
@@ -3799,21 +4102,313 @@ function initAnalytics(state, events, options) {
|
|
|
3799
4102
|
}
|
|
3800
4103
|
};
|
|
3801
4104
|
}
|
|
3802
|
-
|
|
4105
|
+
function trimTrailingSlash(value) {
|
|
4106
|
+
return value.replace(/\/+$/, "");
|
|
4107
|
+
}
|
|
4108
|
+
async function getJson(url, apiKey, fetchImpl) {
|
|
4109
|
+
try {
|
|
4110
|
+
const headers = {};
|
|
4111
|
+
if (apiKey) headers["x-api-key"] = apiKey;
|
|
4112
|
+
const res = await fetchImpl(url, { headers });
|
|
4113
|
+
if (!res.ok) return null;
|
|
4114
|
+
return await res.json();
|
|
4115
|
+
} catch {
|
|
4116
|
+
return null;
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
async function fetchDeliveryContent(opts, fetchImpl = fetch) {
|
|
4120
|
+
const url = `${trimTrailingSlash(opts.apiEndpoint)}/api/v1/delivery/content/${encodeURIComponent(
|
|
4121
|
+
opts.contentId
|
|
4122
|
+
)}`;
|
|
4123
|
+
return getJson(url, opts.apiKey, fetchImpl);
|
|
4124
|
+
}
|
|
4125
|
+
function vttDataUri(vtt) {
|
|
4126
|
+
return `data:text/vtt;charset=utf-8,${encodeURIComponent(vtt)}`;
|
|
4127
|
+
}
|
|
4128
|
+
function injectSources(host, config) {
|
|
4129
|
+
for (const source of config.sources) {
|
|
4130
|
+
const el = document.createElement("source");
|
|
4131
|
+
el.setAttribute("src", source.url);
|
|
4132
|
+
el.setAttribute("type", source.type);
|
|
4133
|
+
if (source.quality) el.setAttribute("quality", source.quality);
|
|
4134
|
+
if (source.fps != null) el.setAttribute("fps", String(source.fps));
|
|
4135
|
+
if (source.format) el.setAttribute("format", source.format);
|
|
4136
|
+
if (source.audioFormat) el.setAttribute("audio-format", source.audioFormat);
|
|
4137
|
+
host.appendChild(el);
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
function injectAnnotations(host, config) {
|
|
4141
|
+
for (const a of config.annotations) {
|
|
4142
|
+
const el = document.createElement("vz-annotation");
|
|
4143
|
+
el.setAttribute("lat", String(a.lat));
|
|
4144
|
+
el.setAttribute("lon", String(a.lon));
|
|
4145
|
+
el.setAttribute("title", a.title);
|
|
4146
|
+
if (a.icon) el.setAttribute("icon", a.icon);
|
|
4147
|
+
if (a.timeStart != null) el.setAttribute("time-start", String(a.timeStart));
|
|
4148
|
+
if (a.timeEnd != null) el.setAttribute("time-end", String(a.timeEnd));
|
|
4149
|
+
el.setAttribute("sort-order", String(a.sortOrder));
|
|
4150
|
+
if (a.htmlContent) el.innerHTML = sanitizeAnnotationHtml(a.htmlContent);
|
|
4151
|
+
host.appendChild(el);
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
function injectChapters(host, config) {
|
|
4155
|
+
for (const c of config.chapters) {
|
|
4156
|
+
const el = document.createElement("vz-chapter");
|
|
4157
|
+
el.setAttribute("start", String(c.startTime));
|
|
4158
|
+
el.setAttribute("title", c.title);
|
|
4159
|
+
el.setAttribute("sort-order", String(c.sortOrder));
|
|
4160
|
+
el.setAttribute("chapter-id", c.id);
|
|
4161
|
+
host.appendChild(el);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
function injectCaptions(host, config) {
|
|
4165
|
+
for (const c of config.captions) {
|
|
4166
|
+
const el = document.createElement("track");
|
|
4167
|
+
el.setAttribute("kind", "captions");
|
|
4168
|
+
el.setAttribute("srclang", c.srclang);
|
|
4169
|
+
el.setAttribute("label", c.label);
|
|
4170
|
+
el.setAttribute("src", vttDataUri(c.vttContent));
|
|
4171
|
+
host.appendChild(el);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
function applyDeliveryConfigToDom(host, config) {
|
|
4175
|
+
if (config.lensAngle != null && !host.hasAttribute("lens-angle")) {
|
|
4176
|
+
host.setAttribute("lens-angle", String(config.lensAngle));
|
|
4177
|
+
}
|
|
4178
|
+
if (config.previewUrl && !host.hasAttribute("preview")) {
|
|
4179
|
+
host.setAttribute("preview", config.previewUrl);
|
|
4180
|
+
}
|
|
4181
|
+
injectSources(host, config);
|
|
4182
|
+
injectAnnotations(host, config);
|
|
4183
|
+
injectCaptions(host, config);
|
|
4184
|
+
injectChapters(host, config);
|
|
4185
|
+
}
|
|
4186
|
+
const MONO_BY_PROJECTION = {
|
|
4187
|
+
flat: VzFormat.MONO_FLAT,
|
|
4188
|
+
equirect: VzFormat.MONO_360,
|
|
4189
|
+
cubemap: VzFormat.MONO_CUBEMAP,
|
|
4190
|
+
eac: VzFormat.MONO_EAC,
|
|
4191
|
+
fisheye: VzFormat.MONO_FISHEYE
|
|
4192
|
+
};
|
|
4193
|
+
function monoEquivalent(meta) {
|
|
4194
|
+
if (meta.fovH === 180) return void 0;
|
|
4195
|
+
return MONO_BY_PROJECTION[meta.projection];
|
|
4196
|
+
}
|
|
4197
|
+
function resolveFormatGate(format, features) {
|
|
4198
|
+
const meta = getFormatMeta(format);
|
|
4199
|
+
if (!meta) return { action: "allow" };
|
|
4200
|
+
const is180 = meta.fovH === 180;
|
|
4201
|
+
const isStereo = meta.stereoMode !== "mono";
|
|
4202
|
+
if (is180 && !features.formats180) return { action: "block", requires: "formats180" };
|
|
4203
|
+
if (isStereo && !features.stereoFormats) {
|
|
4204
|
+
const mono = monoEquivalent(meta);
|
|
4205
|
+
return mono ? { action: "downgrade", to: mono } : { action: "block", requires: "stereoFormats" };
|
|
4206
|
+
}
|
|
4207
|
+
return { action: "allow" };
|
|
4208
|
+
}
|
|
4209
|
+
function resolveFeatureGate(_feature, enabled) {
|
|
4210
|
+
return enabled ? { action: "allow" } : { action: "degrade" };
|
|
4211
|
+
}
|
|
4212
|
+
function resolveDrmGate(hasDrmConfig, drmFeatureEnabled) {
|
|
4213
|
+
if (!hasDrmConfig) return { action: "none" };
|
|
4214
|
+
return drmFeatureEnabled ? { action: "allow" } : { action: "block" };
|
|
4215
|
+
}
|
|
4216
|
+
function parseDomDrmConfig(host) {
|
|
4217
|
+
const keyId = host.getAttribute("drm-key-id");
|
|
4218
|
+
const widevine = host.getAttribute("drm-widevine-url");
|
|
4219
|
+
const playready = host.getAttribute("drm-playready-url");
|
|
4220
|
+
const fairplay = host.getAttribute("drm-fairplay-url");
|
|
4221
|
+
if (!keyId || !widevine || !playready || !fairplay) return null;
|
|
4222
|
+
return {
|
|
4223
|
+
scheme: "cbcs",
|
|
4224
|
+
keyId,
|
|
4225
|
+
licenseUrls: { widevine, playready, fairplay },
|
|
4226
|
+
fairplayCertUrl: host.getAttribute("drm-fairplay-cert-url")
|
|
4227
|
+
};
|
|
4228
|
+
}
|
|
4229
|
+
function normalizeChapters(raw) {
|
|
4230
|
+
const withDefaults = raw.map((c, i) => ({
|
|
4231
|
+
id: c.id ?? "",
|
|
4232
|
+
title: c.title,
|
|
4233
|
+
startTime: c.startTime,
|
|
4234
|
+
sortOrder: c.sortOrder ?? i
|
|
4235
|
+
}));
|
|
4236
|
+
return sortChapters(withDefaults).map((c, i) => ({
|
|
4237
|
+
...c,
|
|
4238
|
+
id: c.id || `chapter-${i}`
|
|
4239
|
+
}));
|
|
4240
|
+
}
|
|
4241
|
+
function sortChapters(chapters) {
|
|
4242
|
+
return [...chapters].sort((a, b) => a.startTime - b.startTime);
|
|
4243
|
+
}
|
|
4244
|
+
function resolveActiveChapter(chapters, currentTime) {
|
|
4245
|
+
let active = null;
|
|
4246
|
+
for (const c of sortChapters(chapters)) {
|
|
4247
|
+
if (c.startTime <= currentTime) active = c;
|
|
4248
|
+
else break;
|
|
4249
|
+
}
|
|
4250
|
+
return active;
|
|
4251
|
+
}
|
|
4252
|
+
function chapterMarkerPositions(chapters, duration) {
|
|
4253
|
+
if (!(duration > 0)) return [];
|
|
4254
|
+
return sortChapters(chapters).map((c) => ({
|
|
4255
|
+
id: c.id,
|
|
4256
|
+
title: c.title,
|
|
4257
|
+
startTime: c.startTime,
|
|
4258
|
+
position: Math.max(0, Math.min(1, c.startTime / duration))
|
|
4259
|
+
}));
|
|
4260
|
+
}
|
|
4261
|
+
function formatTimestamp(seconds) {
|
|
4262
|
+
if (isNaN(seconds) || !isFinite(seconds) || seconds < 0) return "0:00";
|
|
4263
|
+
const s = Math.floor(seconds);
|
|
4264
|
+
const h = Math.floor(s / 3600);
|
|
4265
|
+
const m = Math.floor(s % 3600 / 60);
|
|
4266
|
+
const sec = s % 60;
|
|
4267
|
+
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
|
4268
|
+
return `${m}:${String(sec).padStart(2, "0")}`;
|
|
4269
|
+
}
|
|
4270
|
+
class ChapterMenu {
|
|
3803
4271
|
container;
|
|
3804
|
-
dropdown;
|
|
3805
4272
|
btn;
|
|
3806
|
-
|
|
3807
|
-
|
|
4273
|
+
dropdown;
|
|
4274
|
+
chapters;
|
|
4275
|
+
onSelect;
|
|
4276
|
+
activeId = null;
|
|
3808
4277
|
isOpen = false;
|
|
3809
|
-
|
|
3810
|
-
|
|
4278
|
+
unsubscribes = [];
|
|
4279
|
+
constructor(chapters, onSelect) {
|
|
4280
|
+
this.chapters = chapters;
|
|
4281
|
+
this.onSelect = onSelect;
|
|
4282
|
+
this.container = document.createElement("div");
|
|
4283
|
+
this.container.className = "vz-chapter-menu";
|
|
4284
|
+
this.btn = document.createElement("button");
|
|
4285
|
+
this.btn.className = "vz-btn vz-chapter-btn";
|
|
4286
|
+
this.btn.setAttribute("aria-label", "Chapters");
|
|
4287
|
+
this.btn.title = "Chapters";
|
|
4288
|
+
setStaticHtml(this.btn, ICON_CHAPTERS);
|
|
4289
|
+
this.dropdown = document.createElement("div");
|
|
4290
|
+
this.dropdown.className = "vz-chapter-dropdown";
|
|
4291
|
+
this.container.appendChild(this.btn);
|
|
4292
|
+
this.container.appendChild(this.dropdown);
|
|
4293
|
+
this.btn.addEventListener("click", (e) => {
|
|
4294
|
+
e.stopPropagation();
|
|
4295
|
+
this.isOpen = !this.isOpen;
|
|
4296
|
+
this.dropdown.classList.toggle("open", this.isOpen);
|
|
4297
|
+
});
|
|
4298
|
+
const onDocClick = () => {
|
|
4299
|
+
if (this.isOpen) {
|
|
4300
|
+
this.isOpen = false;
|
|
4301
|
+
this.dropdown.classList.remove("open");
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4304
|
+
document.addEventListener("click", onDocClick);
|
|
4305
|
+
this.unsubscribes.push(() => document.removeEventListener("click", onDocClick));
|
|
4306
|
+
this.renderOptions();
|
|
4307
|
+
}
|
|
4308
|
+
getElement() {
|
|
4309
|
+
return this.container;
|
|
4310
|
+
}
|
|
4311
|
+
/** Highlight the chapter with the given id (or clear when null). */
|
|
4312
|
+
setActive(id) {
|
|
4313
|
+
this.activeId = id;
|
|
4314
|
+
const rows = this.dropdown.querySelectorAll(".vz-chapter-option");
|
|
4315
|
+
rows.forEach((row) => {
|
|
4316
|
+
row.classList.toggle("active", row.dataset.chapterId === id);
|
|
4317
|
+
});
|
|
4318
|
+
}
|
|
4319
|
+
dispose() {
|
|
4320
|
+
for (const unsub of this.unsubscribes) unsub();
|
|
4321
|
+
this.unsubscribes = [];
|
|
4322
|
+
this.container.remove();
|
|
4323
|
+
}
|
|
4324
|
+
renderOptions() {
|
|
4325
|
+
this.dropdown.replaceChildren();
|
|
4326
|
+
for (const chapter of this.chapters) {
|
|
4327
|
+
const row = document.createElement("div");
|
|
4328
|
+
row.className = "vz-chapter-option";
|
|
4329
|
+
row.dataset.chapterId = chapter.id;
|
|
4330
|
+
if (chapter.id === this.activeId) row.classList.add("active");
|
|
4331
|
+
const time = document.createElement("span");
|
|
4332
|
+
time.className = "vz-chapter-time";
|
|
4333
|
+
time.textContent = formatTimestamp(chapter.startTime);
|
|
4334
|
+
const label = document.createElement("span");
|
|
4335
|
+
label.className = "vz-chapter-label";
|
|
4336
|
+
label.textContent = chapter.title;
|
|
4337
|
+
row.appendChild(time);
|
|
4338
|
+
row.appendChild(label);
|
|
4339
|
+
row.addEventListener("click", (e) => {
|
|
4340
|
+
e.stopPropagation();
|
|
4341
|
+
this.isOpen = false;
|
|
4342
|
+
this.dropdown.classList.remove("open");
|
|
4343
|
+
this.onSelect(chapter.startTime);
|
|
4344
|
+
});
|
|
4345
|
+
this.dropdown.appendChild(row);
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
class ChapterManager {
|
|
4350
|
+
chapters;
|
|
4351
|
+
state;
|
|
4352
|
+
events;
|
|
4353
|
+
seek;
|
|
4354
|
+
seekBar;
|
|
4355
|
+
menu = null;
|
|
4356
|
+
activeId = null;
|
|
4357
|
+
unsubscribes = [];
|
|
4358
|
+
constructor(opts) {
|
|
4359
|
+
this.chapters = opts.chapters;
|
|
4360
|
+
this.state = opts.state;
|
|
4361
|
+
this.events = opts.events;
|
|
4362
|
+
this.seek = opts.seek;
|
|
4363
|
+
this.seekBar = opts.seekBar ?? null;
|
|
4364
|
+
if (opts.controlsOverlay) {
|
|
4365
|
+
this.menu = new ChapterMenu(this.chapters, (t) => this.seek(t));
|
|
4366
|
+
opts.controlsOverlay.addControl(this.menu.getElement(), "right");
|
|
4367
|
+
}
|
|
4368
|
+
this.renderMarkers(this.state.get("duration"));
|
|
4369
|
+
this.unsubscribes.push(this.state.subscribe("duration", (d) => this.renderMarkers(d)));
|
|
4370
|
+
this.updateActive(this.state.get("currentTime"));
|
|
4371
|
+
this.unsubscribes.push(this.state.subscribe("currentTime", (t) => this.updateActive(t)));
|
|
4372
|
+
}
|
|
4373
|
+
renderMarkers(duration) {
|
|
4374
|
+
this.seekBar?.setChapterMarkers(chapterMarkerPositions(this.chapters, duration));
|
|
4375
|
+
}
|
|
4376
|
+
updateActive(currentTime) {
|
|
4377
|
+
const active = resolveActiveChapter(this.chapters, currentTime);
|
|
4378
|
+
const id = active?.id ?? null;
|
|
4379
|
+
if (id === this.activeId) return;
|
|
4380
|
+
this.activeId = id;
|
|
4381
|
+
this.menu?.setActive(id);
|
|
4382
|
+
if (active) {
|
|
4383
|
+
this.events.emit("vz-chapter-change", {
|
|
4384
|
+
chapterId: active.id,
|
|
4385
|
+
title: active.title,
|
|
4386
|
+
startTime: active.startTime
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
dispose() {
|
|
4391
|
+
for (const u of this.unsubscribes) u();
|
|
4392
|
+
this.unsubscribes = [];
|
|
4393
|
+
this.menu?.dispose();
|
|
4394
|
+
this.menu = null;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
class CcButton {
|
|
4398
|
+
container;
|
|
4399
|
+
dropdown;
|
|
4400
|
+
btn;
|
|
4401
|
+
captionRenderer;
|
|
4402
|
+
unsubscribes = [];
|
|
4403
|
+
isOpen = false;
|
|
4404
|
+
constructor(state, captionRenderer) {
|
|
4405
|
+
this.captionRenderer = captionRenderer;
|
|
3811
4406
|
this.container = document.createElement("div");
|
|
3812
4407
|
this.container.className = "vz-cc-menu";
|
|
3813
4408
|
this.btn = document.createElement("button");
|
|
3814
4409
|
this.btn.className = "vz-btn vz-cc-btn";
|
|
3815
4410
|
this.btn.setAttribute("aria-label", "Captions");
|
|
3816
|
-
this.btn
|
|
4411
|
+
setStaticHtml(this.btn, ICON_CC);
|
|
3817
4412
|
this.dropdown = document.createElement("div");
|
|
3818
4413
|
this.dropdown.className = "vz-cc-dropdown";
|
|
3819
4414
|
this.container.appendChild(this.btn);
|
|
@@ -3854,12 +4449,15 @@ class CcButton {
|
|
|
3854
4449
|
}
|
|
3855
4450
|
}
|
|
3856
4451
|
renderOptions() {
|
|
3857
|
-
this.dropdown.
|
|
4452
|
+
this.dropdown.replaceChildren();
|
|
3858
4453
|
const tracks = this.captionRenderer.getTracks();
|
|
3859
4454
|
const activeIndex = this.captionRenderer.getActiveTrackIndex();
|
|
3860
4455
|
const offOption = document.createElement("div");
|
|
3861
4456
|
offOption.className = `vz-cc-option ${activeIndex === -1 ? "active" : ""}`;
|
|
3862
|
-
|
|
4457
|
+
const offCheck = document.createElement("span");
|
|
4458
|
+
offCheck.className = "vz-cc-check";
|
|
4459
|
+
offCheck.textContent = activeIndex === -1 ? "✓" : "";
|
|
4460
|
+
offOption.append(offCheck, document.createTextNode("Off"));
|
|
3863
4461
|
offOption.addEventListener("click", () => {
|
|
3864
4462
|
this.captionRenderer.setActiveTrack(-1);
|
|
3865
4463
|
this.isOpen = false;
|
|
@@ -3886,6 +4484,21 @@ class CcButton {
|
|
|
3886
4484
|
});
|
|
3887
4485
|
}
|
|
3888
4486
|
}
|
|
4487
|
+
function applyAutoplayPolicy(video, config) {
|
|
4488
|
+
if (!config.autoplay) return;
|
|
4489
|
+
if (!video.muted) {
|
|
4490
|
+
console.info("[Vizor] autoplay requires muted playback — muting (browser policy)");
|
|
4491
|
+
video.muted = true;
|
|
4492
|
+
}
|
|
4493
|
+
let attempted = false;
|
|
4494
|
+
video.addEventListener("canplay", () => {
|
|
4495
|
+
if (attempted) return;
|
|
4496
|
+
attempted = true;
|
|
4497
|
+
const p = video.play();
|
|
4498
|
+
if (p && typeof p.catch === "function") p.catch(() => {
|
|
4499
|
+
});
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
3889
4502
|
class VzMediaElement extends VzBaseElement {
|
|
3890
4503
|
renderer;
|
|
3891
4504
|
videoElement;
|
|
@@ -3909,6 +4522,8 @@ class VzMediaElement extends VzBaseElement {
|
|
|
3909
4522
|
liveIndicator = null;
|
|
3910
4523
|
analyticsHandle = null;
|
|
3911
4524
|
annotationManager = null;
|
|
4525
|
+
chapterManager = null;
|
|
4526
|
+
pendingChapters = [];
|
|
3912
4527
|
captionRenderer = null;
|
|
3913
4528
|
ccButton = null;
|
|
3914
4529
|
collabClient = null;
|
|
@@ -3918,6 +4533,30 @@ class VzMediaElement extends VzBaseElement {
|
|
|
3918
4533
|
chatButton = null;
|
|
3919
4534
|
voiceManager = null;
|
|
3920
4535
|
voiceButton = null;
|
|
4536
|
+
drmConfig = null;
|
|
4537
|
+
deferredDrmSources = [];
|
|
4538
|
+
/**
|
|
4539
|
+
* A content-id embed (`<vz-video content-id api-key>` with no DOM `<source>`)
|
|
4540
|
+
* fetches its config from the delivery API and injects the equivalent
|
|
4541
|
+
* `<source>`/`<vz-annotation>`/`<track>` DOM children before onConnected() runs
|
|
4542
|
+
* — so authored content actually reaches the viewer. A DOM-authored embed
|
|
4543
|
+
* (any `<source>` child) wins and skips the fetch entirely.
|
|
4544
|
+
*/
|
|
4545
|
+
beforeConnect() {
|
|
4546
|
+
const { contentId, apiKey } = this.config;
|
|
4547
|
+
if (!contentId || !apiKey) return;
|
|
4548
|
+
if (this.querySelector("source")) return;
|
|
4549
|
+
return this.hydrateFromDeliveryApi(contentId, apiKey);
|
|
4550
|
+
}
|
|
4551
|
+
async hydrateFromDeliveryApi(contentId, apiKey) {
|
|
4552
|
+
const apiEndpoint = this.config.apiEndpoint ?? window.location.origin;
|
|
4553
|
+
const config = await fetchDeliveryContent({ apiEndpoint, contentId, apiKey });
|
|
4554
|
+
if (!config) return;
|
|
4555
|
+
if (!this.hasAttribute("format")) this.config.format = config.format;
|
|
4556
|
+
if (!this.config.poster && config.poster) this.config.poster = config.poster;
|
|
4557
|
+
applyDeliveryConfigToDom(this, config);
|
|
4558
|
+
if (config.drm) this.drmConfig = config.drm;
|
|
4559
|
+
}
|
|
3921
4560
|
onConnected() {
|
|
3922
4561
|
this.containerEl = document.createElement("div");
|
|
3923
4562
|
this.containerEl.className = "vz-container";
|
|
@@ -3931,6 +4570,16 @@ class VzMediaElement extends VzBaseElement {
|
|
|
3931
4570
|
});
|
|
3932
4571
|
posterEl.classList.add("hidden");
|
|
3933
4572
|
});
|
|
4573
|
+
if (this.config.preview) {
|
|
4574
|
+
posterEl.addEventListener("mouseenter", () => {
|
|
4575
|
+
if (this.hasFeature("animatedPreviews")) {
|
|
4576
|
+
posterEl.style.backgroundImage = `url(${this.config.preview})`;
|
|
4577
|
+
}
|
|
4578
|
+
});
|
|
4579
|
+
posterEl.addEventListener("mouseleave", () => {
|
|
4580
|
+
posterEl.style.backgroundImage = `url(${this.config.poster})`;
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
3934
4583
|
this.shadow.appendChild(posterEl);
|
|
3935
4584
|
}
|
|
3936
4585
|
this.videoElement = this.createVideoElement();
|
|
@@ -3947,14 +4596,8 @@ class VzMediaElement extends VzBaseElement {
|
|
|
3947
4596
|
}
|
|
3948
4597
|
});
|
|
3949
4598
|
this.sourceManager = new SourceManager();
|
|
4599
|
+
this.drmConfig = this.drmConfig ?? parseDomDrmConfig(this);
|
|
3950
4600
|
this.loadSources();
|
|
3951
|
-
if (this.config.audioFormat === "ambix") {
|
|
3952
|
-
this.audioManager = new AudioManager(this.state, this.events);
|
|
3953
|
-
this.audioManager.init(this.videoElement, "ambix").catch((err) => {
|
|
3954
|
-
console.warn("[Vizor] Audio init error:", err);
|
|
3955
|
-
});
|
|
3956
|
-
this.renderer.setAudioManager(this.audioManager);
|
|
3957
|
-
}
|
|
3958
4601
|
this.cleanupFullscreen = onFullscreenChange(() => {
|
|
3959
4602
|
const fs = isFullscreen();
|
|
3960
4603
|
this.state.set("fullscreen", fs);
|
|
@@ -4032,6 +4675,7 @@ class VzMediaElement extends VzBaseElement {
|
|
|
4032
4675
|
}
|
|
4033
4676
|
this.initAnnotations();
|
|
4034
4677
|
this.initCaptions();
|
|
4678
|
+
this.initChapters();
|
|
4035
4679
|
this.initCollab();
|
|
4036
4680
|
this.keyboardShortcuts = new KeyboardShortcuts(this, this.state, this, this.captionRenderer, this.chatPanel, this.voiceButton);
|
|
4037
4681
|
this.renderer.start();
|
|
@@ -4060,6 +4704,8 @@ class VzMediaElement extends VzBaseElement {
|
|
|
4060
4704
|
this.analyticsHandle = null;
|
|
4061
4705
|
this.annotationManager?.dispose();
|
|
4062
4706
|
this.annotationManager = null;
|
|
4707
|
+
this.chapterManager?.dispose();
|
|
4708
|
+
this.chapterManager = null;
|
|
4063
4709
|
this.ccButton?.dispose();
|
|
4064
4710
|
this.ccButton = null;
|
|
4065
4711
|
this.captionRenderer?.dispose();
|
|
@@ -4083,16 +4729,127 @@ class VzMediaElement extends VzBaseElement {
|
|
|
4083
4729
|
this.videoElement.removeAttribute("src");
|
|
4084
4730
|
this.videoElement.load();
|
|
4085
4731
|
this.renderer.dispose();
|
|
4732
|
+
this.drmConfig = null;
|
|
4733
|
+
this.deferredDrmSources = [];
|
|
4086
4734
|
this.onMediaDisconnected();
|
|
4087
4735
|
}
|
|
4088
4736
|
onResize() {
|
|
4089
4737
|
this.renderer?.resize();
|
|
4090
4738
|
}
|
|
4091
|
-
/** After license resolves, enforce theme
|
|
4739
|
+
/** After license resolves, enforce theme + format/feature tier gating */
|
|
4092
4740
|
onLicenseResolved() {
|
|
4093
4741
|
if (this.controlsOverlay && this.license) {
|
|
4094
4742
|
this.controlsOverlay.updateTheme(this.config, this.license);
|
|
4095
4743
|
}
|
|
4744
|
+
this.applyFormatGate();
|
|
4745
|
+
this.applyWebxrGate();
|
|
4746
|
+
this.applySpatialAudioGate();
|
|
4747
|
+
this.applyChaptersGate();
|
|
4748
|
+
this.applyDrmGate();
|
|
4749
|
+
}
|
|
4750
|
+
/**
|
|
4751
|
+
* Manual chapters are a paid feature. Once the license is known, wire the
|
|
4752
|
+
* chapter UI (seek-bar markers + menu) and active-chapter tracking that fires
|
|
4753
|
+
* `vz-chapter-change`, but only when the tier includes chapters. Unlicensed
|
|
4754
|
+
* tiers degrade gracefully: no markers, no menu, no event (the video plays
|
|
4755
|
+
* unaffected).
|
|
4756
|
+
*/
|
|
4757
|
+
applyChaptersGate() {
|
|
4758
|
+
if (this.chapterManager || this.pendingChapters.length === 0) return;
|
|
4759
|
+
if (resolveFeatureGate("chapters", this.hasFeature("chapters")).action !== "allow") return;
|
|
4760
|
+
this.chapterManager = new ChapterManager({
|
|
4761
|
+
chapters: this.pendingChapters,
|
|
4762
|
+
state: this.state,
|
|
4763
|
+
events: this.events,
|
|
4764
|
+
seek: (t) => this.seek(t),
|
|
4765
|
+
seekBar: this.seekBar,
|
|
4766
|
+
controlsOverlay: this.controlsOverlay
|
|
4767
|
+
});
|
|
4768
|
+
}
|
|
4769
|
+
/**
|
|
4770
|
+
* WebXR (Enter-VR) is a paid feature. Once the license is known, if the tier
|
|
4771
|
+
* doesn't include it, remove the VR button so it can't be used — flat/pointer
|
|
4772
|
+
* viewing continues unaffected (graceful downgrade). Licensed users keep the
|
|
4773
|
+
* button, which reveals itself only when the device reports XR support.
|
|
4774
|
+
*/
|
|
4775
|
+
applyWebxrGate() {
|
|
4776
|
+
if (!this.license) return;
|
|
4777
|
+
if (resolveFeatureGate("webxr", this.hasFeature("webxr")).action === "allow") return;
|
|
4778
|
+
this.vrButton?.dispose();
|
|
4779
|
+
this.vrButton?.getElement().remove();
|
|
4780
|
+
this.vrButton = null;
|
|
4781
|
+
}
|
|
4782
|
+
/**
|
|
4783
|
+
* Spatial (ambisonic) audio is a paid feature. When the source is authored as
|
|
4784
|
+
* ambix and the tier includes it, wire up the AudioManager here (license is
|
|
4785
|
+
* now known). When the tier lacks it, we do nothing — the hidden <video>'s
|
|
4786
|
+
* own audio track plays as ordinary stereo, the graceful fallback.
|
|
4787
|
+
*/
|
|
4788
|
+
applySpatialAudioGate() {
|
|
4789
|
+
if (this.config.audioFormat !== "ambix") return;
|
|
4790
|
+
if (this.audioManager) return;
|
|
4791
|
+
if (resolveFeatureGate("spatialAudio", this.hasFeature("spatialAudio")).action !== "allow") {
|
|
4792
|
+
return;
|
|
4793
|
+
}
|
|
4794
|
+
this.audioManager = new AudioManager(this.state, this.events);
|
|
4795
|
+
this.audioManager.init(this.videoElement, "ambix").catch((err) => {
|
|
4796
|
+
console.warn("[Vizor] Audio init error:", err);
|
|
4797
|
+
});
|
|
4798
|
+
this.renderer.setAudioManager(this.audioManager);
|
|
4799
|
+
}
|
|
4800
|
+
/**
|
|
4801
|
+
* Enforce format tier gating once the license is known. A stereo format the
|
|
4802
|
+
* tier doesn't include is downgraded to its mono equivalent (re-projecting if
|
|
4803
|
+
* the renderer already set up the ungated one); a format with no fallback (a
|
|
4804
|
+
* 180 half-sphere) is blocked behind an upgrade overlay. Prevents free users
|
|
4805
|
+
* from silently receiving paid formats.
|
|
4806
|
+
*/
|
|
4807
|
+
applyFormatGate() {
|
|
4808
|
+
if (!this.license || !this.config.format) return;
|
|
4809
|
+
const decision = resolveFormatGate(this.config.format, this.license.features);
|
|
4810
|
+
if (decision.action === "downgrade") {
|
|
4811
|
+
this.config.format = decision.to;
|
|
4812
|
+
if (this.renderer && this.videoElement.readyState >= 2) {
|
|
4813
|
+
this.renderer.setupVideo(this.videoElement, decision.to);
|
|
4814
|
+
}
|
|
4815
|
+
} else if (decision.action === "block") {
|
|
4816
|
+
this.suppressAutoSetup = true;
|
|
4817
|
+
this.videoElement.pause();
|
|
4818
|
+
this.showFormatUpgradeOverlay(decision.requires);
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
showFormatUpgradeOverlay(requires) {
|
|
4822
|
+
const label = requires === "formats180" ? "180° video" : "Stereoscopic 3D";
|
|
4823
|
+
this.showUpgradeOverlay("vz-format-gate", `${label} playback requires a paid Vizor plan.`);
|
|
4824
|
+
}
|
|
4825
|
+
showUpgradeOverlay(className, message) {
|
|
4826
|
+
if (!this.containerEl || this.shadow.querySelector("." + className)) return;
|
|
4827
|
+
const overlay = document.createElement("div");
|
|
4828
|
+
overlay.className = className;
|
|
4829
|
+
overlay.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;padding:1rem;background:rgba(0,0,0,0.85);color:#fff;z-index:10;font:500 14px/1.5 system-ui,-apple-system,sans-serif;";
|
|
4830
|
+
overlay.textContent = message;
|
|
4831
|
+
this.containerEl.appendChild(overlay);
|
|
4832
|
+
}
|
|
4833
|
+
/**
|
|
4834
|
+
* DRM assets are encrypted-only (no plain fallback). Once the license is known:
|
|
4835
|
+
* a tier WITHOUT `drm` is blocked behind an upgrade overlay (and NO license
|
|
4836
|
+
* request is made); a licensed tier performs the deferred encrypted source load
|
|
4837
|
+
* with the DRM context (license URLs + api-key) so the adapter can drive EME.
|
|
4838
|
+
* EME/CDM errors during playback surface via the adapter's existing error events.
|
|
4839
|
+
*/
|
|
4840
|
+
applyDrmGate() {
|
|
4841
|
+
const decision = resolveDrmGate(this.drmConfig !== null, this.hasFeature("drm"));
|
|
4842
|
+
if (decision.action === "none") return;
|
|
4843
|
+
if (decision.action === "block") {
|
|
4844
|
+
this.videoElement.pause();
|
|
4845
|
+
this.showUpgradeOverlay("vz-drm-gate", "DRM-protected playback requires a Vizor Enterprise plan.");
|
|
4846
|
+
return;
|
|
4847
|
+
}
|
|
4848
|
+
const drm = { config: this.drmConfig, apiKey: this.config.apiKey };
|
|
4849
|
+
this.sourceManager.loadSources(this.deferredDrmSources, this.videoElement, this.state, this.events, drm).catch((err) => {
|
|
4850
|
+
console.error("[Vizor] DRM source loading error:", err);
|
|
4851
|
+
this.events.emit("vz-error", { code: "DRM_LOAD_ERROR", message: String(err) });
|
|
4852
|
+
});
|
|
4096
4853
|
}
|
|
4097
4854
|
onAttributeChanged(name, _oldVal, _newVal) {
|
|
4098
4855
|
if (name === "format" && this.config.format && this.renderer) {
|
|
@@ -4178,6 +4935,23 @@ class VzMediaElement extends VzBaseElement {
|
|
|
4178
4935
|
this.annotationManager?.update(time);
|
|
4179
4936
|
});
|
|
4180
4937
|
}
|
|
4938
|
+
/**
|
|
4939
|
+
* Parse `<vz-chapter>` children into a normalized pending list. The chapter UI
|
|
4940
|
+
* (markers/menu/event) is wired later in applyChaptersGate() once the license
|
|
4941
|
+
* is known, since chapters are tier-gated.
|
|
4942
|
+
*/
|
|
4943
|
+
initChapters() {
|
|
4944
|
+
const chapterEls = this.querySelectorAll("vz-chapter");
|
|
4945
|
+
if (chapterEls.length === 0) return;
|
|
4946
|
+
const raw = [];
|
|
4947
|
+
chapterEls.forEach((el) => {
|
|
4948
|
+
const vzEl = el;
|
|
4949
|
+
if (typeof vzEl.getData === "function") {
|
|
4950
|
+
raw.push(vzEl.getData());
|
|
4951
|
+
}
|
|
4952
|
+
});
|
|
4953
|
+
this.pendingChapters = normalizeChapters(raw);
|
|
4954
|
+
}
|
|
4181
4955
|
/** Initialize collaborative viewing if collab attributes are set */
|
|
4182
4956
|
initCollab() {
|
|
4183
4957
|
const { collabServer, collabRoom, collabRole, collabDisplayName } = this.config;
|
|
@@ -4296,6 +5070,7 @@ class VzMediaElement extends VzBaseElement {
|
|
|
4296
5070
|
video.preload = this.config.preload === "none" ? "metadata" : this.config.preload;
|
|
4297
5071
|
video.loop = this.config.loop;
|
|
4298
5072
|
video.muted = this.config.muted;
|
|
5073
|
+
applyAutoplayPolicy(video, this.config);
|
|
4299
5074
|
video.style.display = "none";
|
|
4300
5075
|
video.addEventListener("error", () => {
|
|
4301
5076
|
const err = video.error;
|
|
@@ -4336,6 +5111,10 @@ class VzMediaElement extends VzBaseElement {
|
|
|
4336
5111
|
}
|
|
4337
5112
|
}
|
|
4338
5113
|
if (sources.length === 0) return;
|
|
5114
|
+
if (this.drmConfig) {
|
|
5115
|
+
this.deferredDrmSources = sources;
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
4339
5118
|
this.sourceManager.loadSources(sources, this.videoElement, this.state, this.events).catch((err) => {
|
|
4340
5119
|
console.error("[Vizor] Source loading error:", err);
|
|
4341
5120
|
this.events.emit("vz-error", { code: "SOURCE_LOAD_ERROR", message: String(err) });
|
|
@@ -4480,8 +5259,8 @@ function registerImg() {
|
|
|
4480
5259
|
const HOTSPOT_DISTANCE = 200;
|
|
4481
5260
|
const HOTSPOT_SCALE_X = 30;
|
|
4482
5261
|
const HOTSPOT_SCALE_Y = 15;
|
|
4483
|
-
const CANVAS_W = 256;
|
|
4484
|
-
const CANVAS_H = 128;
|
|
5262
|
+
const CANVAS_W$1 = 256;
|
|
5263
|
+
const CANVAS_H$1 = 128;
|
|
4485
5264
|
class HotspotRenderer {
|
|
4486
5265
|
sprites = [];
|
|
4487
5266
|
scene;
|
|
@@ -4516,22 +5295,22 @@ class HotspotRenderer {
|
|
|
4516
5295
|
}
|
|
4517
5296
|
createSprite(portal) {
|
|
4518
5297
|
const canvas = document.createElement("canvas");
|
|
4519
|
-
canvas.width = CANVAS_W;
|
|
4520
|
-
canvas.height = CANVAS_H;
|
|
5298
|
+
canvas.width = CANVAS_W$1;
|
|
5299
|
+
canvas.height = CANVAS_H$1;
|
|
4521
5300
|
const ctx = canvas.getContext("2d");
|
|
4522
5301
|
const pad = 8;
|
|
4523
5302
|
ctx.fillStyle = "rgba(0, 160, 255, 0.85)";
|
|
4524
5303
|
ctx.beginPath();
|
|
4525
|
-
this.roundRect(ctx, pad, pad, CANVAS_W - pad * 2, CANVAS_H - pad * 2, 16);
|
|
5304
|
+
this.roundRect(ctx, pad, pad, CANVAS_W$1 - pad * 2, CANVAS_H$1 - pad * 2, 16);
|
|
4526
5305
|
ctx.fill();
|
|
4527
5306
|
const label = portal.title ?? portal.to;
|
|
4528
5307
|
ctx.fillStyle = "#fff";
|
|
4529
5308
|
ctx.font = "bold 24px sans-serif";
|
|
4530
5309
|
ctx.textAlign = "center";
|
|
4531
5310
|
ctx.textBaseline = "middle";
|
|
4532
|
-
ctx.fillText(label, CANVAS_W / 2, CANVAS_H / 2 - 4, CANVAS_W - 32);
|
|
5311
|
+
ctx.fillText(label, CANVAS_W$1 / 2, CANVAS_H$1 / 2 - 4, CANVAS_W$1 - 32);
|
|
4533
5312
|
ctx.font = "16px sans-serif";
|
|
4534
|
-
ctx.fillText("→", CANVAS_W / 2, CANVAS_H / 2 + 20);
|
|
5313
|
+
ctx.fillText("→", CANVAS_W$1 / 2, CANVAS_H$1 / 2 + 20);
|
|
4535
5314
|
const texture = new CanvasTexture(canvas);
|
|
4536
5315
|
texture.minFilter = LinearFilter;
|
|
4537
5316
|
texture.magFilter = LinearFilter;
|
|
@@ -4645,6 +5424,12 @@ class VzTourElement extends VzBaseElement {
|
|
|
4645
5424
|
xrManager = null;
|
|
4646
5425
|
cleanupFullscreen = null;
|
|
4647
5426
|
loader = new TextureLoader();
|
|
5427
|
+
/**
|
|
5428
|
+
* True once the license resolves without the `tours` feature. In that state
|
|
5429
|
+
* the tour degrades to a single static panorama (the start probe): portal
|
|
5430
|
+
* navigation between probes is disabled.
|
|
5431
|
+
*/
|
|
5432
|
+
toursLocked = false;
|
|
4648
5433
|
onConnected() {
|
|
4649
5434
|
this.containerEl = document.createElement("div");
|
|
4650
5435
|
this.containerEl.className = "vz-container";
|
|
@@ -4719,6 +5504,26 @@ class VzTourElement extends VzBaseElement {
|
|
|
4719
5504
|
onResize() {
|
|
4720
5505
|
this.renderer?.resize();
|
|
4721
5506
|
}
|
|
5507
|
+
/**
|
|
5508
|
+
* Interactive tours are a paid feature, and WebXR (Enter-VR) is gated
|
|
5509
|
+
* separately. Once the license resolves, enforce both.
|
|
5510
|
+
*
|
|
5511
|
+
* - tours not licensed → graceful downgrade to a single static panorama: the
|
|
5512
|
+
* start probe stays visible but portal navigation is disabled (existing
|
|
5513
|
+
* portals cleared, future ones suppressed).
|
|
5514
|
+
* - webxr not licensed → remove the VR button (flat viewing continues).
|
|
5515
|
+
*/
|
|
5516
|
+
onLicenseResolved() {
|
|
5517
|
+
if (resolveFeatureGate("webxr", this.hasFeature("webxr")).action !== "allow") {
|
|
5518
|
+
this.vrButton?.dispose();
|
|
5519
|
+
this.vrButton?.getElement().remove();
|
|
5520
|
+
this.vrButton = null;
|
|
5521
|
+
}
|
|
5522
|
+
if (resolveFeatureGate("tours", this.hasFeature("tours")).action !== "allow") {
|
|
5523
|
+
this.toursLocked = true;
|
|
5524
|
+
this.hotspotManager?.clear();
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
4722
5527
|
/** Navigate to a specific probe by ID */
|
|
4723
5528
|
async navigateTo(probeId) {
|
|
4724
5529
|
const probe = this.probes.get(probeId);
|
|
@@ -4743,7 +5548,9 @@ class VzTourElement extends VzBaseElement {
|
|
|
4743
5548
|
if (probe.rotationY !== void 0) {
|
|
4744
5549
|
this.renderer.getCameraController().setOrientation(0, probe.rotationY);
|
|
4745
5550
|
}
|
|
4746
|
-
this.
|
|
5551
|
+
if (!this.toursLocked) {
|
|
5552
|
+
this.hotspotManager?.setPortals(probe.portals);
|
|
5553
|
+
}
|
|
4747
5554
|
this.state.batch({
|
|
4748
5555
|
currentProbeId: probeId
|
|
4749
5556
|
});
|
|
@@ -4959,6 +5766,9 @@ class CinemaProjection {
|
|
|
4959
5766
|
}
|
|
4960
5767
|
class VzCinemaElement extends VzMediaElement {
|
|
4961
5768
|
cinemaProjection = null;
|
|
5769
|
+
videoDataReady = false;
|
|
5770
|
+
/** Guard so the projection (cinema or flat fallback) is only built once. */
|
|
5771
|
+
projectionSetup = false;
|
|
4962
5772
|
static get observedAttributes() {
|
|
4963
5773
|
return [
|
|
4964
5774
|
...super.observedAttributes,
|
|
@@ -4973,15 +5783,51 @@ class VzCinemaElement extends VzMediaElement {
|
|
|
4973
5783
|
onMediaConnected() {
|
|
4974
5784
|
this.suppressAutoSetup = true;
|
|
4975
5785
|
if (this.videoElement.readyState >= 2) {
|
|
4976
|
-
this.
|
|
5786
|
+
this.videoDataReady = true;
|
|
5787
|
+
this.maybeSetupProjection();
|
|
4977
5788
|
} else {
|
|
4978
5789
|
this.videoElement.addEventListener("loadeddata", () => {
|
|
4979
|
-
this.
|
|
5790
|
+
this.videoDataReady = true;
|
|
5791
|
+
this.maybeSetupProjection();
|
|
4980
5792
|
}, { once: true });
|
|
4981
5793
|
}
|
|
4982
5794
|
}
|
|
5795
|
+
/**
|
|
5796
|
+
* Cinema is a paid feature. The base class runs its own gates (format, webxr,
|
|
5797
|
+
* spatial audio) here; we add the cinema gate and (re)attempt projection
|
|
5798
|
+
* setup now that the license is known.
|
|
5799
|
+
*/
|
|
5800
|
+
onLicenseResolved() {
|
|
5801
|
+
super.onLicenseResolved();
|
|
5802
|
+
this.maybeSetupProjection();
|
|
5803
|
+
}
|
|
5804
|
+
/**
|
|
5805
|
+
* Build the projection once BOTH the video data is ready AND the license has
|
|
5806
|
+
* resolved. Licensed → the cinema room projection. Unlicensed → graceful
|
|
5807
|
+
* downgrade to flat (mono) playback of the same clip.
|
|
5808
|
+
*/
|
|
5809
|
+
maybeSetupProjection() {
|
|
5810
|
+
if (this.projectionSetup) return;
|
|
5811
|
+
if (!this.videoDataReady) return;
|
|
5812
|
+
if (!this.license) return;
|
|
5813
|
+
this.projectionSetup = true;
|
|
5814
|
+
if (resolveFeatureGate("cinema", this.hasFeature("cinema")).action === "allow") {
|
|
5815
|
+
this.setupCinemaProjection();
|
|
5816
|
+
} else {
|
|
5817
|
+
this.setupFlatFallback();
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
/**
|
|
5821
|
+
* Graceful downgrade when the tier lacks cinema: render the clip as an
|
|
5822
|
+
* ordinary flat video instead of projecting it onto a screen inside a room.
|
|
5823
|
+
*/
|
|
5824
|
+
setupFlatFallback() {
|
|
5825
|
+
this.renderer.setupVideo(this.videoElement, VzFormat.MONO_FLAT);
|
|
5826
|
+
}
|
|
4983
5827
|
onMediaDisconnected() {
|
|
4984
5828
|
this.cinemaProjection = null;
|
|
5829
|
+
this.projectionSetup = false;
|
|
5830
|
+
this.videoDataReady = false;
|
|
4985
5831
|
}
|
|
4986
5832
|
setupCinemaProjection() {
|
|
4987
5833
|
this.cinemaProjection = new CinemaProjection({
|
|
@@ -5089,7 +5935,7 @@ class PlaylistControls {
|
|
|
5089
5935
|
this.injectedRight = controlsRight;
|
|
5090
5936
|
this.prevBtn = document.createElement("button");
|
|
5091
5937
|
this.prevBtn.className = "vz-btn vz-playlist-prev";
|
|
5092
|
-
this.prevBtn
|
|
5938
|
+
setStaticHtml(this.prevBtn, ICON_PLAYLIST_PREV);
|
|
5093
5939
|
this.prevBtn.title = "Previous";
|
|
5094
5940
|
this.prevBtn.addEventListener("click", (e) => {
|
|
5095
5941
|
e.stopPropagation();
|
|
@@ -5097,7 +5943,7 @@ class PlaylistControls {
|
|
|
5097
5943
|
});
|
|
5098
5944
|
this.nextBtn = document.createElement("button");
|
|
5099
5945
|
this.nextBtn.className = "vz-btn vz-playlist-next";
|
|
5100
|
-
this.nextBtn
|
|
5946
|
+
setStaticHtml(this.nextBtn, ICON_PLAYLIST_NEXT);
|
|
5101
5947
|
this.nextBtn.title = "Next";
|
|
5102
5948
|
this.nextBtn.addEventListener("click", (e) => {
|
|
5103
5949
|
e.stopPropagation();
|
|
@@ -5119,7 +5965,7 @@ class PlaylistControls {
|
|
|
5119
5965
|
if (onTogglePanel && controlsRight) {
|
|
5120
5966
|
this.panelBtn = document.createElement("button");
|
|
5121
5967
|
this.panelBtn.className = "vz-btn vz-playlist-panel-toggle";
|
|
5122
|
-
this.panelBtn
|
|
5968
|
+
setStaticHtml(this.panelBtn, ICON_PLAYLIST_LIST);
|
|
5123
5969
|
this.panelBtn.title = "Playlist";
|
|
5124
5970
|
this.panelBtn.addEventListener("click", (e) => {
|
|
5125
5971
|
e.stopPropagation();
|
|
@@ -5166,7 +6012,7 @@ class PlaylistPanel {
|
|
|
5166
6012
|
title.textContent = "Playlist";
|
|
5167
6013
|
const closeBtn = document.createElement("button");
|
|
5168
6014
|
closeBtn.className = "vz-playlist-panel-close";
|
|
5169
|
-
closeBtn.
|
|
6015
|
+
closeBtn.textContent = "×";
|
|
5170
6016
|
closeBtn.title = "Close";
|
|
5171
6017
|
closeBtn.addEventListener("click", (e) => {
|
|
5172
6018
|
e.stopPropagation();
|
|
@@ -5217,7 +6063,7 @@ class PlaylistPanel {
|
|
|
5217
6063
|
this.panel.remove();
|
|
5218
6064
|
}
|
|
5219
6065
|
renderItems(items, activeIndex) {
|
|
5220
|
-
this.listEl.
|
|
6066
|
+
this.listEl.replaceChildren();
|
|
5221
6067
|
for (let i = 0; i < items.length; i++) {
|
|
5222
6068
|
const item = items[i];
|
|
5223
6069
|
const row = document.createElement("div");
|
|
@@ -5392,6 +6238,14 @@ class VzPlaylistElement extends HTMLElementBase {
|
|
|
5392
6238
|
playlistPanel = null;
|
|
5393
6239
|
endedHandler = null;
|
|
5394
6240
|
connected = false;
|
|
6241
|
+
/**
|
|
6242
|
+
* True once a child player reports a license tier without the `playlist`
|
|
6243
|
+
* feature. The queue degrades to playing only the first item as a single
|
|
6244
|
+
* clip: auto-advance and prev/next navigation are disabled and the multi-item
|
|
6245
|
+
* controls/panel are removed.
|
|
6246
|
+
*/
|
|
6247
|
+
playlistLocked = false;
|
|
6248
|
+
licenseListener = null;
|
|
5395
6249
|
constructor() {
|
|
5396
6250
|
super();
|
|
5397
6251
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
@@ -5437,6 +6291,15 @@ class VzPlaylistElement extends HTMLElementBase {
|
|
|
5437
6291
|
const slot = document.createElement("slot");
|
|
5438
6292
|
wrapper.appendChild(slot);
|
|
5439
6293
|
this.parseItems();
|
|
6294
|
+
this.licenseListener = (e) => {
|
|
6295
|
+
const detail = e.detail;
|
|
6296
|
+
if (!detail?.tier) return;
|
|
6297
|
+
const features = getFeaturesForTier(detail.tier);
|
|
6298
|
+
if (resolveFeatureGate("playlist", features.playlist).action !== "allow") {
|
|
6299
|
+
this.applyPlaylistLock();
|
|
6300
|
+
}
|
|
6301
|
+
};
|
|
6302
|
+
this.addEventListener("vz-license", this.licenseListener);
|
|
5440
6303
|
if (this.panelMode === "expandable") {
|
|
5441
6304
|
this.playlistPanel = new PlaylistPanel(
|
|
5442
6305
|
this.shadow,
|
|
@@ -5459,6 +6322,10 @@ class VzPlaylistElement extends HTMLElementBase {
|
|
|
5459
6322
|
disconnectedCallback() {
|
|
5460
6323
|
this.connected = false;
|
|
5461
6324
|
this.detachEndedListener();
|
|
6325
|
+
if (this.licenseListener) {
|
|
6326
|
+
this.removeEventListener("vz-license", this.licenseListener);
|
|
6327
|
+
this.licenseListener = null;
|
|
6328
|
+
}
|
|
5462
6329
|
this.playlistControls?.dispose();
|
|
5463
6330
|
this.playlistControls = null;
|
|
5464
6331
|
this.playlistPanel?.dispose();
|
|
@@ -5470,6 +6337,7 @@ class VzPlaylistElement extends HTMLElementBase {
|
|
|
5470
6337
|
/** Navigate to item at index */
|
|
5471
6338
|
goTo(index) {
|
|
5472
6339
|
if (index < 0 || index >= this.items.length) return;
|
|
6340
|
+
if (this.playlistLocked && index !== 0) return;
|
|
5473
6341
|
const prevIndex = this.currentIndex;
|
|
5474
6342
|
this.currentIndex = index;
|
|
5475
6343
|
for (let i = 0; i < this.items.length; i++) {
|
|
@@ -5513,6 +6381,7 @@ class VzPlaylistElement extends HTMLElementBase {
|
|
|
5513
6381
|
}
|
|
5514
6382
|
/** Go to next item */
|
|
5515
6383
|
next() {
|
|
6384
|
+
if (this.playlistLocked) return;
|
|
5516
6385
|
if (this.currentIndex < this.items.length - 1) {
|
|
5517
6386
|
this.goTo(this.currentIndex + 1);
|
|
5518
6387
|
} else if (this.loopPlaylist) {
|
|
@@ -5521,12 +6390,27 @@ class VzPlaylistElement extends HTMLElementBase {
|
|
|
5521
6390
|
}
|
|
5522
6391
|
/** Go to previous item */
|
|
5523
6392
|
previous() {
|
|
6393
|
+
if (this.playlistLocked) return;
|
|
5524
6394
|
if (this.currentIndex > 0) {
|
|
5525
6395
|
this.goTo(this.currentIndex - 1);
|
|
5526
6396
|
} else if (this.loopPlaylist) {
|
|
5527
6397
|
this.goTo(this.items.length - 1);
|
|
5528
6398
|
}
|
|
5529
6399
|
}
|
|
6400
|
+
/**
|
|
6401
|
+
* Graceful downgrade when the tier lacks the playlist feature: keep the first
|
|
6402
|
+
* item playing as a single clip and disable everything queue-specific —
|
|
6403
|
+
* auto-advance, prev/next, the injected controls, and the side panel.
|
|
6404
|
+
*/
|
|
6405
|
+
applyPlaylistLock() {
|
|
6406
|
+
if (this.playlistLocked) return;
|
|
6407
|
+
this.playlistLocked = true;
|
|
6408
|
+
this.detachEndedListener();
|
|
6409
|
+
this.playlistControls?.dispose();
|
|
6410
|
+
this.playlistControls = null;
|
|
6411
|
+
this.playlistPanel?.dispose();
|
|
6412
|
+
this.playlistPanel = null;
|
|
6413
|
+
}
|
|
5530
6414
|
/** Shuffle the playlist order */
|
|
5531
6415
|
shuffle() {
|
|
5532
6416
|
for (let i = this.items.length - 1; i > 0; i--) {
|
|
@@ -5604,8 +6488,9 @@ class VzPlaylistElement extends HTMLElementBase {
|
|
|
5604
6488
|
}
|
|
5605
6489
|
injectControls() {
|
|
5606
6490
|
this.playlistControls?.dispose();
|
|
6491
|
+
this.playlistControls = null;
|
|
6492
|
+
if (this.playlistLocked) return;
|
|
5607
6493
|
if (this.items.length <= 1) {
|
|
5608
|
-
this.playlistControls = null;
|
|
5609
6494
|
return;
|
|
5610
6495
|
}
|
|
5611
6496
|
const activeEl = this.items[this.currentIndex]?.element;
|
|
@@ -5688,17 +6573,451 @@ function registerAnnotation() {
|
|
|
5688
6573
|
customElements.define("vz-annotation", VzAnnotationElement);
|
|
5689
6574
|
}
|
|
5690
6575
|
}
|
|
6576
|
+
class VzChapterElement extends HTMLElementBase {
|
|
6577
|
+
static get observedAttributes() {
|
|
6578
|
+
return ["start", "title", "sort-order", "chapter-id"];
|
|
6579
|
+
}
|
|
6580
|
+
/** Parse this element's attributes into a RawChapter. */
|
|
6581
|
+
getData() {
|
|
6582
|
+
const startTime = this.hasAttribute("start") ? parseFloat(this.getAttribute("start")) : 0;
|
|
6583
|
+
const title = this.getAttribute("title") ?? "";
|
|
6584
|
+
const sortOrder = this.hasAttribute("sort-order") ? parseInt(this.getAttribute("sort-order"), 10) : void 0;
|
|
6585
|
+
const id = this.getAttribute("chapter-id") ?? void 0;
|
|
6586
|
+
return {
|
|
6587
|
+
id,
|
|
6588
|
+
title,
|
|
6589
|
+
startTime: isNaN(startTime) ? 0 : startTime,
|
|
6590
|
+
sortOrder: sortOrder != null && !isNaN(sortOrder) ? sortOrder : void 0
|
|
6591
|
+
};
|
|
6592
|
+
}
|
|
6593
|
+
}
|
|
6594
|
+
function registerChapter() {
|
|
6595
|
+
if (typeof customElements === "undefined") return;
|
|
6596
|
+
if (!customElements.get("vz-chapter")) {
|
|
6597
|
+
customElements.define("vz-chapter", VzChapterElement);
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
class VzHubItemElement extends HTMLElementBase {
|
|
6601
|
+
static get observedAttributes() {
|
|
6602
|
+
return ["src", "poster", "title", "format", "content-id"];
|
|
6603
|
+
}
|
|
6604
|
+
/** Parse this element's attributes into a RawHubItem. */
|
|
6605
|
+
getData() {
|
|
6606
|
+
return {
|
|
6607
|
+
src: this.getAttribute("src"),
|
|
6608
|
+
poster: this.getAttribute("poster"),
|
|
6609
|
+
title: this.getAttribute("title"),
|
|
6610
|
+
format: this.getAttribute("format"),
|
|
6611
|
+
contentId: this.getAttribute("content-id")
|
|
6612
|
+
};
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
function registerHubItem() {
|
|
6616
|
+
if (typeof customElements === "undefined") return;
|
|
6617
|
+
if (!customElements.get("vz-hub-item")) {
|
|
6618
|
+
customElements.define("vz-hub-item", VzHubItemElement);
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
function normalizeLon(lon) {
|
|
6622
|
+
return ((lon + 180) % 360 + 360) % 360 - 180;
|
|
6623
|
+
}
|
|
6624
|
+
function hubPanelLayout(count, opts = {}) {
|
|
6625
|
+
if (count <= 0) return [];
|
|
6626
|
+
const spacing = opts.spacingDeg ?? 30;
|
|
6627
|
+
const lat = opts.lat ?? 0;
|
|
6628
|
+
const span = (count - 1) * spacing;
|
|
6629
|
+
if (span > 330) {
|
|
6630
|
+
const step = 360 / count;
|
|
6631
|
+
return Array.from({ length: count }, (_, i) => ({ lat, lon: normalizeLon(i * step - 180) }));
|
|
6632
|
+
}
|
|
6633
|
+
const start = -span / 2;
|
|
6634
|
+
return Array.from({ length: count }, (_, i) => ({ lat, lon: normalizeLon(start + i * spacing) }));
|
|
6635
|
+
}
|
|
6636
|
+
const PANEL_DISTANCE = 200;
|
|
6637
|
+
const PANEL_SCALE_X = 60;
|
|
6638
|
+
const PANEL_SCALE_Y = 34;
|
|
6639
|
+
const CANVAS_W = 320;
|
|
6640
|
+
const CANVAS_H = 180;
|
|
6641
|
+
class HubPanelRenderer {
|
|
6642
|
+
sprites = [];
|
|
6643
|
+
posterTextures = [];
|
|
6644
|
+
scene;
|
|
6645
|
+
loader = new TextureLoader();
|
|
6646
|
+
constructor(scene) {
|
|
6647
|
+
this.scene = scene;
|
|
6648
|
+
}
|
|
6649
|
+
/** Build a panel sprite per item and add them to the scene. */
|
|
6650
|
+
setItems(items) {
|
|
6651
|
+
this.clear();
|
|
6652
|
+
const positions = hubPanelLayout(items.length);
|
|
6653
|
+
items.forEach((item, index) => {
|
|
6654
|
+
const sprite = this.createPanelSprite(item, positions[index], index);
|
|
6655
|
+
this.sprites.push(sprite);
|
|
6656
|
+
this.scene.add(sprite);
|
|
6657
|
+
});
|
|
6658
|
+
}
|
|
6659
|
+
/** Sprites for raycasting (consumed by HotspotInteraction). */
|
|
6660
|
+
getSprites() {
|
|
6661
|
+
return this.sprites;
|
|
6662
|
+
}
|
|
6663
|
+
clear() {
|
|
6664
|
+
for (const sprite of this.sprites) {
|
|
6665
|
+
this.scene.remove(sprite);
|
|
6666
|
+
sprite.material.map?.dispose();
|
|
6667
|
+
sprite.material.dispose();
|
|
6668
|
+
}
|
|
6669
|
+
this.sprites = [];
|
|
6670
|
+
for (const tex of this.posterTextures) tex.dispose();
|
|
6671
|
+
this.posterTextures = [];
|
|
6672
|
+
}
|
|
6673
|
+
dispose() {
|
|
6674
|
+
this.clear();
|
|
6675
|
+
}
|
|
6676
|
+
createPanelSprite(item, pos, index) {
|
|
6677
|
+
const canvas = document.createElement("canvas");
|
|
6678
|
+
canvas.width = CANVAS_W;
|
|
6679
|
+
canvas.height = CANVAS_H;
|
|
6680
|
+
const ctx = canvas.getContext("2d");
|
|
6681
|
+
ctx.fillStyle = "rgba(15, 15, 25, 0.9)";
|
|
6682
|
+
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
|
|
6683
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.25)";
|
|
6684
|
+
ctx.lineWidth = 3;
|
|
6685
|
+
ctx.strokeRect(2, 2, CANVAS_W - 4, CANVAS_H - 4);
|
|
6686
|
+
ctx.fillStyle = "#fff";
|
|
6687
|
+
ctx.font = "bold 26px sans-serif";
|
|
6688
|
+
ctx.textAlign = "center";
|
|
6689
|
+
ctx.textBaseline = "middle";
|
|
6690
|
+
ctx.fillText(item.title || "Play", CANVAS_W / 2, CANVAS_H / 2, CANVAS_W - 40);
|
|
6691
|
+
const texture = new CanvasTexture(canvas);
|
|
6692
|
+
texture.minFilter = LinearFilter;
|
|
6693
|
+
texture.magFilter = LinearFilter;
|
|
6694
|
+
const material = new SpriteMaterial({ map: texture, depthTest: false, sizeAttenuation: true });
|
|
6695
|
+
const sprite = new Sprite(material);
|
|
6696
|
+
sprite.userData.portalTo = String(index);
|
|
6697
|
+
sprite.userData.hubIndex = index;
|
|
6698
|
+
const { phi, theta } = latLonToSpherical(pos.lat, pos.lon);
|
|
6699
|
+
sprite.position.set(
|
|
6700
|
+
PANEL_DISTANCE * Math.sin(phi) * Math.cos(theta),
|
|
6701
|
+
PANEL_DISTANCE * Math.cos(phi),
|
|
6702
|
+
PANEL_DISTANCE * Math.sin(phi) * Math.sin(theta)
|
|
6703
|
+
);
|
|
6704
|
+
sprite.scale.set(PANEL_SCALE_X, PANEL_SCALE_Y, 1);
|
|
6705
|
+
if (item.poster) {
|
|
6706
|
+
this.loader.load(
|
|
6707
|
+
item.poster,
|
|
6708
|
+
(tex) => {
|
|
6709
|
+
tex.colorSpace = SRGBColorSpace;
|
|
6710
|
+
tex.minFilter = LinearFilter;
|
|
6711
|
+
tex.magFilter = LinearFilter;
|
|
6712
|
+
material.map?.dispose();
|
|
6713
|
+
material.map = tex;
|
|
6714
|
+
material.needsUpdate = true;
|
|
6715
|
+
this.posterTextures.push(tex);
|
|
6716
|
+
},
|
|
6717
|
+
void 0,
|
|
6718
|
+
() => {
|
|
6719
|
+
}
|
|
6720
|
+
);
|
|
6721
|
+
}
|
|
6722
|
+
return sprite;
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
class HubPanelManager {
|
|
6726
|
+
renderer;
|
|
6727
|
+
interaction;
|
|
6728
|
+
constructor(scene, camera, domElement, onSelect) {
|
|
6729
|
+
this.renderer = new HubPanelRenderer(scene);
|
|
6730
|
+
this.interaction = new HotspotInteraction(
|
|
6731
|
+
camera,
|
|
6732
|
+
domElement,
|
|
6733
|
+
() => this.renderer.getSprites(),
|
|
6734
|
+
(portalTo) => onSelect(Number(portalTo))
|
|
6735
|
+
);
|
|
6736
|
+
this.interaction.attach();
|
|
6737
|
+
}
|
|
6738
|
+
setItems(items) {
|
|
6739
|
+
this.renderer.setItems(items);
|
|
6740
|
+
}
|
|
6741
|
+
clear() {
|
|
6742
|
+
this.renderer.clear();
|
|
6743
|
+
}
|
|
6744
|
+
dispose() {
|
|
6745
|
+
this.interaction.dispose();
|
|
6746
|
+
this.renderer.dispose();
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
class HubController {
|
|
6750
|
+
items;
|
|
6751
|
+
events;
|
|
6752
|
+
onShowItem;
|
|
6753
|
+
onShowLobby;
|
|
6754
|
+
activeIndex = -1;
|
|
6755
|
+
constructor(opts) {
|
|
6756
|
+
this.items = opts.items;
|
|
6757
|
+
this.events = opts.events;
|
|
6758
|
+
this.onShowItem = opts.onShowItem;
|
|
6759
|
+
this.onShowLobby = opts.onShowLobby;
|
|
6760
|
+
}
|
|
6761
|
+
isInLobby() {
|
|
6762
|
+
return this.activeIndex === -1;
|
|
6763
|
+
}
|
|
6764
|
+
getActiveIndex() {
|
|
6765
|
+
return this.activeIndex;
|
|
6766
|
+
}
|
|
6767
|
+
/** Select an item by index: play it and fire vz-hub-select. No-op if invalid or already active. */
|
|
6768
|
+
select(index) {
|
|
6769
|
+
if (index < 0 || index >= this.items.length) return;
|
|
6770
|
+
if (index === this.activeIndex) return;
|
|
6771
|
+
this.activeIndex = index;
|
|
6772
|
+
const item = this.items[index];
|
|
6773
|
+
this.onShowItem(item, index);
|
|
6774
|
+
this.events.emit("vz-hub-select", {
|
|
6775
|
+
id: item.id,
|
|
6776
|
+
src: item.src,
|
|
6777
|
+
title: item.title,
|
|
6778
|
+
index
|
|
6779
|
+
});
|
|
6780
|
+
}
|
|
6781
|
+
/** Return to the lobby. No-op if already there. */
|
|
6782
|
+
back() {
|
|
6783
|
+
if (this.activeIndex === -1) return;
|
|
6784
|
+
this.activeIndex = -1;
|
|
6785
|
+
this.onShowLobby();
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
function isImageSource(src) {
|
|
6789
|
+
const clean = (src.split("?")[0] ?? "").split("#")[0] ?? "";
|
|
6790
|
+
return /\.(jpe?g|png|webp|avif|gif|bmp)$/i.test(clean);
|
|
6791
|
+
}
|
|
6792
|
+
function normalizeHubItems(raw) {
|
|
6793
|
+
const items = [];
|
|
6794
|
+
raw.forEach((r, i) => {
|
|
6795
|
+
const src = r.src?.trim();
|
|
6796
|
+
if (!src) return;
|
|
6797
|
+
items.push({
|
|
6798
|
+
id: r.contentId?.trim() || `item-${i}`,
|
|
6799
|
+
src,
|
|
6800
|
+
poster: r.poster?.trim() || null,
|
|
6801
|
+
title: r.title ?? "",
|
|
6802
|
+
format: r.format?.trim() || "MONO_360"
|
|
6803
|
+
});
|
|
6804
|
+
});
|
|
6805
|
+
return items;
|
|
6806
|
+
}
|
|
6807
|
+
class VzHubElement extends VzBaseElement {
|
|
6808
|
+
renderer;
|
|
6809
|
+
containerEl;
|
|
6810
|
+
items = [];
|
|
6811
|
+
panelManager = null;
|
|
6812
|
+
controller = null;
|
|
6813
|
+
controlsOverlay = null;
|
|
6814
|
+
compass = null;
|
|
6815
|
+
fullscreenButton = null;
|
|
6816
|
+
vrButton = null;
|
|
6817
|
+
xrManager = null;
|
|
6818
|
+
cleanupFullscreen = null;
|
|
6819
|
+
loader = new TextureLoader();
|
|
6820
|
+
bgTexture = null;
|
|
6821
|
+
playingEl = null;
|
|
6822
|
+
hubLocked = false;
|
|
6823
|
+
onConnected() {
|
|
6824
|
+
this.containerEl = document.createElement("div");
|
|
6825
|
+
this.containerEl.className = "vz-container";
|
|
6826
|
+
this.shadow.appendChild(this.containerEl);
|
|
6827
|
+
this.renderer = new VzRenderer(this.shadow, this.containerEl, this.state, this.config);
|
|
6828
|
+
this.items = this.parseItems();
|
|
6829
|
+
this.xrManager = new XRManager(
|
|
6830
|
+
this.renderer.getThreeRenderer(),
|
|
6831
|
+
this.state,
|
|
6832
|
+
this.events,
|
|
6833
|
+
this.renderer.getCameraController()
|
|
6834
|
+
);
|
|
6835
|
+
if (!this.config.hideControls) {
|
|
6836
|
+
this.controlsOverlay = new ControlsOverlay(
|
|
6837
|
+
this.shadow,
|
|
6838
|
+
this.containerEl,
|
|
6839
|
+
this.state,
|
|
6840
|
+
this.config,
|
|
6841
|
+
this.events
|
|
6842
|
+
);
|
|
6843
|
+
this.compass = new Compass(this.state, this.renderer);
|
|
6844
|
+
this.controlsOverlay.addControl(this.compass.getElement(), "right");
|
|
6845
|
+
this.vrButton = new VrButton(this.state, () => {
|
|
6846
|
+
this.xrManager?.toggleXR().catch((err) => {
|
|
6847
|
+
this.events.emit("vz-error", { code: "XR_ERROR", message: String(err) });
|
|
6848
|
+
});
|
|
6849
|
+
});
|
|
6850
|
+
this.controlsOverlay.addControl(this.vrButton.getElement(), "right");
|
|
6851
|
+
this.fullscreenButton = new FullscreenButton(this.state, this);
|
|
6852
|
+
this.controlsOverlay.addControl(this.fullscreenButton.getElement(), "right");
|
|
6853
|
+
}
|
|
6854
|
+
this.cleanupFullscreen = onFullscreenChange(() => {
|
|
6855
|
+
this.state.set("fullscreen", isFullscreen());
|
|
6856
|
+
this.renderer.resize();
|
|
6857
|
+
});
|
|
6858
|
+
this.controller = new HubController({
|
|
6859
|
+
items: this.items,
|
|
6860
|
+
events: this.events,
|
|
6861
|
+
onShowItem: (item) => this.playItem(item),
|
|
6862
|
+
onShowLobby: () => this.returnToLobby()
|
|
6863
|
+
});
|
|
6864
|
+
this.panelManager = new HubPanelManager(
|
|
6865
|
+
this.renderer.getScene(),
|
|
6866
|
+
this.renderer.getCamera(),
|
|
6867
|
+
this.containerEl,
|
|
6868
|
+
(index) => this.controller?.select(index)
|
|
6869
|
+
);
|
|
6870
|
+
this.showLobby();
|
|
6871
|
+
this.renderer.start();
|
|
6872
|
+
this.events.emit("vz-ready");
|
|
6873
|
+
}
|
|
6874
|
+
onDisconnected() {
|
|
6875
|
+
this.cleanupFullscreen?.();
|
|
6876
|
+
this.playingEl?.remove();
|
|
6877
|
+
this.playingEl = null;
|
|
6878
|
+
this.compass?.dispose();
|
|
6879
|
+
this.fullscreenButton?.dispose();
|
|
6880
|
+
this.vrButton?.dispose();
|
|
6881
|
+
this.controlsOverlay?.dispose();
|
|
6882
|
+
this.xrManager?.dispose();
|
|
6883
|
+
this.panelManager?.dispose();
|
|
6884
|
+
this.panelManager = null;
|
|
6885
|
+
this.bgTexture?.dispose();
|
|
6886
|
+
this.bgTexture = null;
|
|
6887
|
+
this.renderer?.dispose();
|
|
6888
|
+
}
|
|
6889
|
+
onAttributeChanged() {
|
|
6890
|
+
}
|
|
6891
|
+
onResize() {
|
|
6892
|
+
this.renderer?.resize();
|
|
6893
|
+
}
|
|
6894
|
+
/**
|
|
6895
|
+
* Hub is an Enterprise feature; WebXR is gated separately. On license resolve:
|
|
6896
|
+
* - hub not licensed → lock to a static lobby panorama (panels removed, an
|
|
6897
|
+
* upgrade note shown) — graceful downgrade, the background still renders.
|
|
6898
|
+
* - webxr not licensed → remove the VR button.
|
|
6899
|
+
*/
|
|
6900
|
+
onLicenseResolved() {
|
|
6901
|
+
if (resolveFeatureGate("webxr", this.hasFeature("webxr")).action !== "allow") {
|
|
6902
|
+
this.vrButton?.dispose();
|
|
6903
|
+
this.vrButton?.getElement().remove();
|
|
6904
|
+
this.vrButton = null;
|
|
6905
|
+
}
|
|
6906
|
+
if (resolveFeatureGate("hub", this.hasFeature("hub")).action !== "allow") {
|
|
6907
|
+
this.hubLocked = true;
|
|
6908
|
+
this.panelManager?.clear();
|
|
6909
|
+
this.playingEl?.remove();
|
|
6910
|
+
this.playingEl = null;
|
|
6911
|
+
this.showUpgradeOverlay();
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
toggleFullscreen() {
|
|
6915
|
+
if (isFullscreen()) exitFullscreen();
|
|
6916
|
+
else requestFullscreen(this);
|
|
6917
|
+
}
|
|
6918
|
+
/** Show the lobby: background panorama + clickable panels (unless locked). */
|
|
6919
|
+
showLobby() {
|
|
6920
|
+
const bg = this.getAttribute("background");
|
|
6921
|
+
if (bg) {
|
|
6922
|
+
const format = parseFormat(this.getAttribute("background-format") ?? "MONO_360") ?? "MONO_360";
|
|
6923
|
+
this.state.set("loading", true);
|
|
6924
|
+
this.loader.load(
|
|
6925
|
+
bg,
|
|
6926
|
+
(texture) => {
|
|
6927
|
+
texture.colorSpace = SRGBColorSpace;
|
|
6928
|
+
texture.minFilter = LinearFilter;
|
|
6929
|
+
this.bgTexture?.dispose();
|
|
6930
|
+
this.bgTexture = texture;
|
|
6931
|
+
this.renderer.setupImage(texture, format);
|
|
6932
|
+
this.state.set("loading", false);
|
|
6933
|
+
},
|
|
6934
|
+
void 0,
|
|
6935
|
+
() => this.state.set("loading", false)
|
|
6936
|
+
);
|
|
6937
|
+
}
|
|
6938
|
+
if (!this.hubLocked) {
|
|
6939
|
+
this.panelManager?.setItems(this.items);
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
/** Play a selected item inline via a child vz-video/vz-img + a back control. */
|
|
6943
|
+
playItem(item) {
|
|
6944
|
+
if (this.hubLocked) return;
|
|
6945
|
+
this.panelManager?.clear();
|
|
6946
|
+
const overlay = document.createElement("div");
|
|
6947
|
+
overlay.className = "vz-hub-playing";
|
|
6948
|
+
overlay.style.cssText = "position:absolute;inset:0;z-index:15;background:#000;";
|
|
6949
|
+
const child = document.createElement(isImageSource(item.src) ? "vz-img" : "vz-video");
|
|
6950
|
+
child.setAttribute("src", item.src);
|
|
6951
|
+
child.setAttribute("format", item.format);
|
|
6952
|
+
child.setAttribute("crossorigin", "anonymous");
|
|
6953
|
+
if (isImageSource(item.src) === false) {
|
|
6954
|
+
child.setAttribute("muted", "");
|
|
6955
|
+
child.setAttribute("autoplay", "");
|
|
6956
|
+
}
|
|
6957
|
+
if (this.config.apiKey) child.setAttribute("api-key", this.config.apiKey);
|
|
6958
|
+
if (this.config.apiEndpoint) child.setAttribute("api-endpoint", this.config.apiEndpoint);
|
|
6959
|
+
if (this.config.licenseKey) child.setAttribute("license-key", this.config.licenseKey);
|
|
6960
|
+
child.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
|
|
6961
|
+
const back = document.createElement("button");
|
|
6962
|
+
back.className = "vz-hub-back";
|
|
6963
|
+
back.type = "button";
|
|
6964
|
+
back.textContent = "← Lobby";
|
|
6965
|
+
back.style.cssText = "position:absolute;top:12px;left:12px;z-index:20;padding:8px 14px;border:none;border-radius:8px;background:rgba(0,0,0,0.6);color:#fff;font:500 13px system-ui;cursor:pointer;backdrop-filter:blur(8px);";
|
|
6966
|
+
back.addEventListener("click", (e) => {
|
|
6967
|
+
e.stopPropagation();
|
|
6968
|
+
this.controller?.back();
|
|
6969
|
+
});
|
|
6970
|
+
overlay.appendChild(child);
|
|
6971
|
+
overlay.appendChild(back);
|
|
6972
|
+
this.shadow.appendChild(overlay);
|
|
6973
|
+
this.playingEl = overlay;
|
|
6974
|
+
}
|
|
6975
|
+
/** Return to the lobby: tear down the inline player, re-render panels. */
|
|
6976
|
+
returnToLobby() {
|
|
6977
|
+
this.playingEl?.remove();
|
|
6978
|
+
this.playingEl = null;
|
|
6979
|
+
if (!this.hubLocked) {
|
|
6980
|
+
this.panelManager?.setItems(this.items);
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
showUpgradeOverlay() {
|
|
6984
|
+
if (this.shadow.querySelector(".vz-hub-gate")) return;
|
|
6985
|
+
const overlay = document.createElement("div");
|
|
6986
|
+
overlay.className = "vz-hub-gate";
|
|
6987
|
+
overlay.style.cssText = "position:absolute;left:0;right:0;bottom:0;padding:10px;text-align:center;z-index:12;background:rgba(0,0,0,0.7);color:#fff;font:500 13px system-ui;";
|
|
6988
|
+
overlay.textContent = "The content hub requires a Vizor Enterprise plan.";
|
|
6989
|
+
this.containerEl.appendChild(overlay);
|
|
6990
|
+
}
|
|
6991
|
+
/** Parse <vz-hub-item> children into a normalized item list. */
|
|
6992
|
+
parseItems() {
|
|
6993
|
+
const raw = [];
|
|
6994
|
+
this.querySelectorAll("vz-hub-item").forEach((el) => {
|
|
6995
|
+
const vzEl = el;
|
|
6996
|
+
if (typeof vzEl.getData === "function") raw.push(vzEl.getData());
|
|
6997
|
+
});
|
|
6998
|
+
return normalizeHubItems(raw);
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
function registerHub() {
|
|
7002
|
+
if (typeof customElements === "undefined") return;
|
|
7003
|
+
if (!customElements.get("vz-hub")) {
|
|
7004
|
+
customElements.define("vz-hub", VzHubElement);
|
|
7005
|
+
}
|
|
7006
|
+
}
|
|
5691
7007
|
function registerAll() {
|
|
5692
7008
|
if (typeof customElements === "undefined") return;
|
|
5693
|
-
|
|
5694
|
-
|
|
7009
|
+
registerAnnotation();
|
|
7010
|
+
registerChapter();
|
|
7011
|
+
registerHubItem();
|
|
5695
7012
|
registerTourPortal();
|
|
5696
7013
|
registerTourImg();
|
|
7014
|
+
registerVideo();
|
|
7015
|
+
registerImg();
|
|
5697
7016
|
registerTour();
|
|
5698
7017
|
registerCinema();
|
|
5699
7018
|
registerPlaylist();
|
|
5700
7019
|
registerLive();
|
|
5701
|
-
|
|
7020
|
+
registerHub();
|
|
5702
7021
|
}
|
|
5703
7022
|
registerAll();
|
|
5704
7023
|
export {
|
|
@@ -5737,4 +7056,4 @@ export {
|
|
|
5737
7056
|
registerTourPortal as y,
|
|
5738
7057
|
registerVideo as z
|
|
5739
7058
|
};
|
|
5740
|
-
//# sourceMappingURL=register-
|
|
7059
|
+
//# sourceMappingURL=register-BmZt_300.js.map
|