@netless/app-presentation 0.1.13-beta.4 → 0.1.14

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.
@@ -7,10 +7,10 @@ import type {
7
7
  ReadonlyTeleBox,
8
8
  Room,
9
9
  SceneDefinition,
10
- Size,
11
10
  View,
12
11
  WindowManager,
13
12
  } from "@netless/window-manager";
13
+ import type { Size } from "white-web-sdk";
14
14
 
15
15
  import { disposableStore } from "@wopjs/disposable";
16
16
  import { listen } from "@wopjs/dom";
@@ -263,12 +263,11 @@ export const setupBlurThumbnailDegradation = (config: {
263
263
  const { context, view, pages, pageIndex$, app, dispose } = config;
264
264
  const box = context.getBox();
265
265
  const boxBlurClass = box.wrapClassName("blur");
266
- // Two separate flags: `blurEpisode` covers the whole blurred period (pages
267
- // without a thumbnail don't end it, so a later page degrades again), while
268
- // `reDegradeEnabled` only turns off when the degraded thumbnail itself
269
- // fails, so we don't fight the SDK's background onError retry.
266
+ // A failed thumbnail is pinned only for its page during this blur episode.
270
267
  let blurEpisode = false;
271
- let reDegradeEnabled = false;
268
+ let failedThumbnailPageIndex: number | null = null;
269
+ const canReDegrade = (): boolean =>
270
+ blurEpisode && failedThumbnailPageIndex !== pageIndex$.value;
272
271
  // Duplicate focus(false) deliveries are unavoidable (multiple sources in
273
272
  // the host). Preview-resource clearing is the only non-idempotent DOM op
274
273
  // in the blur path, so gate it on this flag until focus restores it.
@@ -340,15 +339,18 @@ export const setupBlurThumbnailDegradation = (config: {
340
339
  app.suspendPreviewResources();
341
340
  }
342
341
  const page = pages[pageIndex$.value];
343
- if (!page || !hasUsableThumbnail(page) || reDegradeEnabled) {
342
+ if (!page || !hasUsableThumbnail(page) || canReDegrade()) {
344
343
  degrade();
345
344
  }
346
345
  };
347
346
 
348
347
  const restoreFocusedResources = (): void => {
348
+ // TeleBox can restore before the host delivers its activity/focus event.
349
+ // Keep the applied state aligned so the next blur starts a new episode.
350
+ appliedFocusState = true;
349
351
  if (!blurEpisode) return;
350
352
  blurEpisode = false;
351
- reDegradeEnabled = false;
353
+ failedThumbnailPageIndex = null;
352
354
  previewSuspended = false;
353
355
  restore();
354
356
  app.preload.resume(pageIndex$.value);
@@ -374,18 +376,23 @@ export const setupBlurThumbnailDegradation = (config: {
374
376
  };
375
377
 
376
378
  const onImgLoad = (ev: Event): void => {
377
- if (!blurEpisode || !reDegradeEnabled) return;
379
+ if (!canReDegrade()) return;
378
380
  if (!ownsTarget(ev.target)) return;
379
381
  degrade();
380
382
  };
381
383
 
382
- // The degraded thumbnail failed to load: stop re-degrading for this blur
383
- // episode so the SDK's onError retry can settle on the full image. Focus
384
- // still force-restores afterwards.
384
+ // Stop re-degrading this page when its thumbnail fails, so the SDK's
385
+ // onError retry can settle on the full image. Other pages remain eligible.
385
386
  const onImgError = (ev: Event): void => {
386
- if (!blurEpisode || !reDegradeEnabled) return;
387
+ if (!canReDegrade()) return;
387
388
  if (!ownsTarget(ev.target)) return;
388
- reDegradeEnabled = false;
389
+ const thumbnailURL = pages[pageIndex$.value]?.thumbnail;
390
+ if (
391
+ thumbnailURL &&
392
+ resolveImageUrl(ev.target.src) === resolveImageUrl(thumbnailURL)
393
+ ) {
394
+ failedThumbnailPageIndex = pageIndex$.value;
395
+ }
389
396
  };
390
397
 
391
398
  const applyActivity = (): void => {
@@ -397,7 +404,7 @@ export const setupBlurThumbnailDegradation = (config: {
397
404
  restoreFocusedResources();
398
405
  } else {
399
406
  blurEpisode = true;
400
- reDegradeEnabled = true;
407
+ failedThumbnailPageIndex = null;
401
408
  degrade();
402
409
  app.preload.pause();
403
410
  suspendCachedResources();
@@ -447,27 +454,16 @@ export const setupBlurThumbnailDegradation = (config: {
447
454
  });
448
455
  };
449
456
 
450
- const createLogger = (room: Room | undefined): Logger => {
451
- if (room && (room as any).logger) {
452
- return (...args) => (room as any).logger.info(...args);
453
- } else {
454
- return (...args) => console.log(...args);
455
- }
457
+ const getLogger = (context: AppContext) =>
458
+ (context.getWindowManager() as any)?.Logger ?? (context.getRoom() as any)?.logger;
459
+
460
+ const createLogger = (context: AppContext): Logger => {
461
+ return (...args) => getLogger(context)?.info?.(...args);
456
462
  };
457
463
 
458
- const createDiagnosticLogger = (
464
+ export const createDiagnosticLogger = (
459
465
  context: AppContext,
460
466
  ): PresentationDiagnosticLogger => {
461
- const createAppLogger = (context as any).createLogger;
462
- if (typeof createAppLogger === "function") {
463
- return createAppLogger.call(context, "camera", {
464
- debounceTime: 300,
465
- maxWaitTime: 2000,
466
- });
467
- }
468
-
469
- // Compatibility fallback for WindowManager versions without AppContext.createLogger().
470
- const roomLogger = (context.getRoom() as any)?.logger;
471
467
  const prefix = `[Presentation][${context.appId}][camera]`;
472
468
  const emit = (
473
469
  level: "info" | "warn" | "error",
@@ -475,6 +471,7 @@ const createDiagnosticLogger = (
475
471
  ...data: unknown[]
476
472
  ) => {
477
473
  try {
474
+ const roomLogger = getLogger(context);
478
475
  const printer = roomLogger?.[level];
479
476
  if (typeof printer === "function")
480
477
  printer.call(roomLogger, `${prefix}[${event}]`, ...data);
@@ -554,10 +551,8 @@ export const NetlessAppPresentation: NetlessApp<
554
551
 
555
552
  const options = context.getAppOptions() || {};
556
553
  const room = context.getRoom();
557
- const log = options.log || createLogger(room);
558
- const roomLogger = (room as any)?.logger;
559
- const warn: Logger = (...data) =>
560
- roomLogger?.warn ? roomLogger.warn(...data) : log(...data);
554
+ const log = options.log || createLogger(context);
555
+ const warn: Logger = (...data) => getLogger(context)?.warn?.(...data);
561
556
  const configuredOriginSize = context.storage.state.originSize;
562
557
  const originSize = isValidSize(configuredOriginSize)
563
558
  ? {
@@ -927,6 +922,7 @@ export const NetlessAppPresentation: NetlessApp<
927
922
  view,
928
923
  options.thumbnail,
929
924
  options.useClipView,
925
+ error => getLogger(context)?.error?.("[Presentation] thumbnail URL parsing failed", context.appId, error),
930
926
  ),
931
927
  );
932
928
  app.contentDOM.dataset.appPresentationVersion = __VERSION__;
@@ -1522,8 +1518,8 @@ export const NetlessAppPresentation: NetlessApp<
1522
1518
  // `SetupResult | Promise<SetupResult>` declaration is the minimum version.
1523
1519
  const setupReadyTimeout = options.setupReadyTimeout ?? DEFAULT_SETUP_READY_TIMEOUT;
1524
1520
  return prepareScenesPromise.then(async () => {
1525
- // Keep the serial setup queue honest: resolve only once the current
1526
- // page image is actually visible (or the bounded wait gives up).
1521
+ // Wait for the current page image or its first failure. The SDK owns
1522
+ // background-image retries, so a failed request must not fail setup.
1527
1523
  const result = await waitForCurrentPageImage(
1528
1524
  () => findBackgroundImage(view), setupReadyTimeout, () => disposed,
1529
1525
  {
@@ -1532,10 +1528,7 @@ export const NetlessAppPresentation: NetlessApp<
1532
1528
  },
1533
1529
  );
1534
1530
  if (disposed) throw new Error("[Presentation] disposed before setup ready");
1535
- if (result === "error" && (context as any).waitForActualSetupReady) {
1536
- throw new Error("[Presentation] setup page image failed to load");
1537
- }
1538
- if (result === "error" && !disposed) {
1531
+ if (result === "error") {
1539
1532
  warn("[Presentation] setup page image failed to load; continuing setup");
1540
1533
  }
1541
1534
  return controller;
@@ -1597,6 +1590,7 @@ function createPresentation(
1597
1590
  view: View,
1598
1591
  thumbnail?: (src: string) => string,
1599
1592
  useClipView?: boolean,
1593
+ onThumbnailError?: (error: unknown) => void,
1600
1594
  ): AppPresentation {
1601
1595
  box.mountStyles(styles);
1602
1596
 
@@ -1605,6 +1599,7 @@ function createPresentation(
1605
1599
  readonly: box.readonly,
1606
1600
  jumpPage,
1607
1601
  thumbnail,
1602
+ onThumbnailError,
1608
1603
  });
1609
1604
  app.box = box;
1610
1605
  box.mountContent(app.contentDOM);
@@ -1,4 +1,4 @@
1
- import type { Camera, Rectangle, Size } from "@netless/window-manager"
1
+ import type { Camera, Rectangle, Size } from "white-web-sdk"
2
2
 
3
3
  export function isValidSize(size: Size | null | undefined): size is Size {
4
4
  return Boolean(
@@ -18,6 +18,7 @@ export interface PresentationConfig {
18
18
  readonly pages: PresentationPage[]
19
19
  readonly readonly?: boolean
20
20
  readonly thumbnail?: (src: string) => string
21
+ readonly onThumbnailError?: (error: unknown) => void
21
22
  }
22
23
 
23
24
  /**
@@ -59,6 +60,7 @@ export class Presentation implements IDisposable<void> {
59
60
  readonly dispose = disposableStore()
60
61
  readonly pages: PresentationPage[]
61
62
  readonly preload: Preload
63
+ readonly onThumbnailError?: (error: unknown) => void
62
64
 
63
65
  dom: Element | DocumentFragment
64
66
  contentDOM: HTMLDivElement
@@ -81,6 +83,7 @@ export class Presentation implements IDisposable<void> {
81
83
  constructor(config: PresentationConfig) {
82
84
  this.pages = config.pages
83
85
  this.preload = new Preload(this.pages)
86
+ this.onThumbnailError = config.onThumbnailError
84
87
  if (config.thumbnail) this.thumbnail = config.thumbnail
85
88
  this.dispose.add(this.preload)
86
89
  this.readonly = config.readonly ?? false
@@ -329,7 +332,11 @@ export class Presentation implements IDisposable<void> {
329
332
  url.searchParams.set('x-oss-process', 'image/resize,l_50')
330
333
  return url.toString()
331
334
  } catch (err) {
332
- console.error(err)
335
+ try {
336
+ this.onThumbnailError?.(err)
337
+ } catch {
338
+ // A diagnostic failure must not change the thumbnail fallback.
339
+ }
333
340
  return src
334
341
  }
335
342
  }