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 ispw-player-shell(PlayerShellComponent). See Inputs & Outputs. - The manifest URL doesn't load. Subscribe to the
erroroutput: a message starting withManifest 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 withAccess-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'spanels+graph.entry/edgesbefore rendering. Theerroroutput 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 needsprovideHttpClient()in your application config. - Missing translations setup: register ngx-translate with
provideTranslateService({ loader: { provide: TranslateLoader, useClass: YourLoader } })and serveassets/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/player1.2.0 (the current npm release) declares Angular^20 || ^21 || ^22, ngx-translate^17 || ^18andrxjs^7.8; 1.1.0 and earlier accept only Angular^20and ngx-translate^17. On Angular 21 or 22, upgrade withnpm install @panelwave/player@latestrather 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/manifestreloads the work.initialChapterId,initialPanelIdandinitialVariablesare only read during a load — to jump to another position in the running work, callnavigateToPanel()instead, or callreload()after changing them. locale,viewModeOverride,showToolbar,autoplay,secondsPerPanelandreducedMotionapply live (from the release after 1.2.0 alsopageFormat); a newentitlementSnapshotre-evaluates the paywall.showCoveris 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 —
UserGestureServicesignals it, andAudioEngineService.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
reducedMotioninput), 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 withentitlementEndpoint+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 ownhasAccess()answer. - Paid panels show with an adapter set: an
entitlementAdapterdecides 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": truestubs stay locked whatever the adapter says) or pass anentitlementSnapshot, 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
chapterrule gates only the chapter named in itsrefId, and anextrasrule 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-verifiedinlocalStorage); private browsing forgets it. To skip the prompt for readers you have verified, passageVerified: trueandagein the snapshot. NullEntitlementAdapterdenies everything: that only concerns the standaloneEntitlementService, not the embedded shell.
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.