TroubleshootingPlayer Embedding

Player Embedding Issues

Fix the most common problems when integrating the PanelWave Player into an Angular app — blank screens, load errors, untranslated labels, fonts, audio, and paywalls.

Work through the symptom that matches what you see. The full integration guide is Quickstart: Embed; to rule out your manifest, open it in the live demo first (Open file… or ?manifest=<url>) — if it reads fine there, the problem is in the embed.

The player renders nothing (or just placeholder text)

  • You embedded pw-player. That selector belongs to a legacy stub component that renders static text. The real embed target is pw-player-shell (PlayerShellComponent). See Inputs & Outputs.
  • The manifest URL doesn't load. Subscribe to the error output: a message starting with Manifest load failed: means the request failed (wrong path, 404, or a cross-origin URL without CORS headers) or the response was not a valid manifest. Open the URL in the browser to check, and serve cross-origin manifests with Access-Control-Allow-Origin.
  • The manifest fails the shell's load-time checks. The shell validates panelwave.version, meta.id/title/locales/default_locale, and each chapter's panels + graph.entry/edges before rendering. The error output tells you what was rejected; run the manifest through the CLI first.
  • The host element has no height. The player fills its container. Give the element (or its parent) a height, e.g. :host { display: block; height: 100dvh; }.

Build or injection errors on startup

  • Missing HttpClient: the player needs provideHttpClient() in your application config.
  • Missing translations setup: register ngx-translate with provideTranslateService({ loader: { provide: TranslateLoader, useClass: YourLoader } }) and serve assets/i18n/<lang>.json — the player's UI strings come from @ngx-translate. See Installation.
  • "Class constructor … cannot be invoked without 'new'" in a production build: you registered the loader with ngx-translate 18's provideTranslateLoader(SomeClass). Its class detection fails on minified code — pass the loader as an explicit { provide: TranslateLoader, useClass } provider instead.
  • Peer dependency conflicts on npm install: @panelwave/player 1.2.0 (the current npm release) declares Angular ^20 || ^21 || ^22, ngx-translate ^17 || ^18 and rxjs ^7.8; 1.1.0 and earlier accept only Angular ^20 and ngx-translate ^17. On Angular 21 or 22, upgrade with npm install @panelwave/player@latest rather than forcing the install.

Labels show as keys (toolbar.like, paywall.sign_in, …)

The UI translation files are missing or outdated. Copy them from the package into your build output — node_modules/@panelwave/player/src/assets/i18n → assets/i18n via the assets entry in angular.json — instead of keeping your own copy, which misses keys added in newer releases (most recently player.locked.*, navigation.* and toolbar.author_timing* from the release after 1.2.0, and before them age_gate.*, branch_chooser.* and paywall.reason_*). See Installation.

The table of contents or thumbnail strip is empty

Both list the loaded work whether you pass manifestUrl or [manifest]. If they stay empty with manifestUrl, you are on @panelwave/player 1.0.1 or earlier, where both read only the manifest input — upgrade to 1.1.0 or later, or load the JSON yourself and bind [manifest] (option B).

Changing an input doesn't do anything

The shell reacts to input changes after initialization:

  • A new manifestUrl / manifest reloads the work. initialChapterId, initialPanelId and initialVariables are only read during a load — to jump to another position in the running work, call navigateToPanel() instead, or call reload() after changing them.
  • locale, viewModeOverride, showToolbar, autoplay, secondsPerPanel and reducedMotion apply live (from the release after 1.2.0 also pageFormat); a new entitlementSnapshot re-evaluates the paywall. showCover is only read during a load.

If nothing reacts, check that you bind a new value (a mutated object with the same reference is not a change for Angular), and that you are on player 1.1.0 or later — 1.0.1 and earlier read their inputs only once. See Inputs & Outputs.

Speech balloons use the wrong font

Include the bundled balloon font CSS once in your application styles (node_modules/@panelwave/player/src/assets/fonts/balloon/balloon-fonts.css). Ames Pro is a commercial font and is not bundled — without a license, balloons fall back to Comic Neue. That is also why balloons set in Ames look different in your own embed than in the CMS preview or on read.panelwave.org: PanelWave's own players load Ames under PanelWave's license, which covers PanelWave's domains only, and CMS exports don't include it. To use Ames on your site, license it from blambot.com and add your own @font-face. See Fonts and Ames Pro.

Audio or video won't start

Browsers block audible playback before a user gesture (hover doesn't count):

  • On-view videos start muted with an unmute affordance until the reader interacts.
  • Start audible audio from the first real interaction — UserGestureService signals it, and AudioEngineService.resumeContext() resumes the WebAudio context.
  • Only one unmuted video plays at a time; starting another preempts the first.
  • With reduced motion on (OS setting, the reader's Settings switch, or the reducedMotion input), on-view videos wait for a click by design.

See Audio.

Gated content is locked (or nothing is)

  • Everything past the preview is locked: the player treats the reader as anonymous until you tell it otherwise. Pass what they own via [entitlementSnapshot], or let the player fetch it with entitlementEndpoint + readerToken. If the endpoint request fails, the reader stays anonymous on purpose.
  • Nothing is locked: check that the manifest has paywall.rules, and that you haven't set [entitlementAdapter] — an adapter replaces the manifest rules with its own hasAccess() answer.
  • Paid panels show with an adapter set: an entitlementAdapter decides navigation, not rendering. A panel it refuses is still rendered — the paywall opens over a refused entry panel, and page and canvas view show refused panels in full. Don't rely on an adapter to hide content: strip it on the server ("x-locked": true stubs stay locked whatever the adapter says) or pass an entitlementSnapshot, which locks rendering too.
  • Gated panels appear in page or canvas view (1.2.0 and earlier): these versions checked the paywall only when the reader moved panel by panel. From the release after 1.2.0 every view shows a locked placeholder for gated panels, and the entry panel is gated on load. A chapter rule gates only the chapter named in its refId, and an extras rule never gates panels — it only locks its extras block. (In 1.0.1 and earlier, chapter rules were ignored and extras rules locked the whole work.)
  • The age gate keeps appearing / never appears: a passed age gate is remembered per device (pw-age-verified in localStorage); private browsing forgets it. To skip the prompt for readers you have verified, pass ageVerified: true and age in the snapshot.
  • NullEntitlementAdapter denies everything: that only concerns the standalone EntitlementService, not the embedded shell.

See Paywall & Entitlement.

Reader progress isn't saved

Persistent-scope variables are saved automatically (pw-variables-persistent), and a bookmark the reader sets from the toolbar is resumed on the next visit — unless you pass initialChapterId/initialPanelId, which always win. Automatic "continue where you stopped" is the host's job: record panelChange events and restore via the initial-position inputs. A complete example is on Saving Progress.