npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jackmorgan/phaser-player

v0.4.0

Published

Embeddable HLS music player for the Phaser streaming network — USDC micropayments on Base, headless and web-component entry points.

Downloads

547

Readme

@jackmorgan/phaser-player

Embeddable HLS music player for the Phaser streaming network. Handles wallet-gated paid streams, USDC micropayments on Base, and HLS playback in one self-contained bundle.

Two entry points: a headless Player class for custom UI, and a <phaser-player> web component that comes with a default themeable UI.

  • Self-contained: hls.js and viem are bundled — no peer dependencies for consumers to wire up.
  • Works in every modern browser (HLS via hls.js, with native Safari fallback).
  • Ships both ESM and IIFE builds plus TypeScript definitions.
  • Bring your own auth: optional AuthAdapter hook, plus ready-made createEip1193Adapter, runSiwe, waitForCoinbaseProvider, and PhaserClient helpers so you don't re-implement the glue.
  • Typed errors: PhaserError with stable code values so apps can branch on payment failures without regex-matching message strings.

Install

npm install @jackmorgan/phaser-player

Or drop in the IIFE build from a CDN:

<script src="https://unpkg.com/@jackmorgan/phaser-player"></script>
<script>
  const player = new Phaser.Player();
</script>

Quick start — web component

<script type="module">
  import "@jackmorgan/phaser-player";
</script>

<phaser-player
  track-id="trk_abc123"
  theme="dark"
></phaser-player>

Quick start — headless

import { Player } from "@jackmorgan/phaser-player";

const player = new Player();

player.on("track:loaded", (e) => console.log("loaded", e.title));
player.on("stream:progress", (e) => console.log(e.currentTime));

await player.load("trk_abc123");
// For paid tracks, connect a wallet first:
await player.connectWallet();
await player.play();

Player API

Constructor

new Player(opts: PlayerOptions)

| Option | Type | Description | |-------------------|-----------------|-------------------------------------------------------------------------------| | apiUrl | string | Override the Phaser API base URL. Default: https://phaser-api.jackmorgan.xyz. | | rpcUrl | string | Override the default Base RPC endpoint used by the built-in wallet module. | | spendAllowance | number | USDC allowance (whole-unit) the default spend permission prompts for. | | spendPeriodDays | number | Rolling window (days) for the spend permission. | | auth | AuthAdapter | Supply an external wallet/auth adapter (see below) to replace the internal one.|

The catalog and streaming endpoints are open — no API key required. Paid streams still require a connected wallet to sign USDC spend permissions.

Methods

| Method | Returns | Notes | |-------------------------------------------|--------------------------|--------------------------------------------------------------------------------------| | load(trackId) | Promise<TrackInfo> | Fetch track metadata and prep for playback. | | play() | Promise<void> | Pays for the stream (if priced) and starts HLS playback. | | pause() | void | Pause the underlying <audio> element. | | resume() | void | Resume after pause(). | | seek(seconds) | void | Seek to an absolute time. | | setVolume(level) | void | level in [0, 1]. | | stop() | void | Stop playback and reset to idle. | | connectWallet() | Promise<string> | Resolves to the connected address. | | disconnectWallet() | void | Forget the current wallet session. | | getBalance() | Promise<string> | Returns USDC balance on Base as a formatted string. | | getEthBalance() | Promise<string> | Returns native ETH balance on Base as a formatted string (5 decimals). | | setAuthToken(jwt) | void | Push a JWT from an external auth adapter. null clears. Preserves audio/queue. | | setAuthAdapter(adapter \| null) | void | Swap the external auth adapter in place (sign-in / sign-out) without rebuilding the Player. Requires construction with opts.auth. | | buildSpendPermission({ amountUsdc? }) | Promise<SpendPermissionState> | Sign and register a spend permission. Use for explicit "Fund listening" UX so the next play() doesn't interrupt for payment. | | setQueue(trackIds) / queue(trackIds) | void | Replace the current queue. | | next() / previous() | Promise<void> | Walk the queue. previous() restarts the current track if >3s elapsed. | | getSpendPermission() | Promise<SpendPermissionState> | Fetch the user's spend permission state from the API. | | on(event, handler) / off(...) | void | Subscribe/unsubscribe to player events (see table below). | | getAudioElement() | HTMLAudioElement | Escape hatch for advanced control. | | destroy() | void | Tear down listeners, audio, and payment state. |

Read-only state

player.state      // Readonly<PlayerState>  — full snapshot (status, wallet, track, permission, …)
player.track      // TrackInfo | null       — the currently loaded track

Events

player.on("stream:progress", ({ currentTime, duration }) => { … });

| Event | Payload | |------------------------|---------------------------------------------------------------------------| | wallet:connected | { address: string } | | wallet:disconnected | {} | | payment:permission | { status: SpendPermissionState["status"]; remainingUsdc: string \| null } | | payment:required | { trackId: string; price: string } | | payment:processing | { trackId: string; txHash?: string } | | payment:confirmed | { trackId: string; chargeId: string; amount: string } | | payment:failed | { trackId: string; error: string; code: PhaserErrorCode } — branch on code, see Typed errors | | track:loaded | { trackId: string; title: string; artist: string; duration: number; price: string } | | stream:started | { trackId: string; title: string } | | stream:progress | { currentTime: number; duration: number } | | stream:paused | { trackId: string } | | stream:resumed | { trackId: string } | | stream:ended | { trackId: string } | | error | { code: string; message: string } |

AuthAdapter — bring-your-own wallet

Pass an object that satisfies this interface (e.g. a wrapper around @phaser/auth, wagmi, RainbowKit, or any other wallet stack) to skip the built-in viem wallet flow:

interface AuthAdapter {
  readonly address: string | null;
  readonly isConnected: boolean;
  readonly jwt: string | null;
  signMessage(message: string): Promise<string>;
  signTypedData(typedData: any): Promise<string>;
  sendTransaction(tx: { to: `0x${string}`; data: `0x${string}`; value?: bigint }): Promise<string>;
  getBalance(): Promise<string>;
  on(event: string, handler: (...args: any[]) => void): void;
  off(event: string, handler: (...args: any[]) => void): void;
}

const player = new Player({
  auth: myAdapter,
});

If auth.jwt is already populated, the player skips the SIWE round-trip and uses it directly to authorize paid streams. Push updates later without tearing down the Player:

player.setAuthToken(newJwt);  // after token refresh
player.setAuthToken(null);    // on sign-out

createEip1193Adapter — drop-in adapter for any EIP-1193 provider

Most apps already have an EIP-1193 provider (window.ethereum, Coinbase Smart Wallet SDK, Rabby, WalletConnect, …). Wrap it in one line:

import { Player, createEip1193Adapter } from "@jackmorgan/phaser-player";

const adapter = createEip1193Adapter({ provider, address, jwt });
const player = new Player({ auth: adapter });

The adapter handles the three things consumers otherwise get wrong:

  • personal_sign messages are hex-encoded (Coinbase Smart Wallet requires it)
  • eth_signTypedData_v4 payloads have bigints serialized as decimal strings
  • EIP712Domain is injected into types so strict wallets don't reject it

It also exposes getBalance() (USDC) and getEthBalance() (native ETH) so your UI can render a wallet view without hitting a public RPC yourself.

runSiwe — Sign-In-With-Ethereum helper

The Phaser API issues JWTs via a four-step handshake (connect → challenge → sign → verify). runSiwe runs it end-to-end and fires onStep for each transition so you can drive a progress UI:

import { runSiwe } from "@jackmorgan/phaser-player";

const { jwt, address } = await runSiwe({
  provider,                                          // EIP-1193 provider
  apiUrl: "https://phaser-api.jackmorgan.xyz",
  onStep: ({ step, status }) => updateUI(step, status),
});

Steps: connect, challenge, sign, verify. Statuses: active, done, err. If eth_requestAccounts has already returned, pass address and runSiwe skips the prompt.

waitForCoinbaseProvider — eager-load synchronization

Coinbase Wallet SDK loads as an async module; browsers drop the user-gesture context if you await import between a click and eth_requestAccounts, which blocks the popup. The pattern is to load the SDK eagerly on page start, attach the provider to window.__coinbaseProvider, then synchronize with this helper when the user actually clicks sign-in:

import { waitForCoinbaseProvider } from "@jackmorgan/phaser-player";

const provider = await waitForCoinbaseProvider(); // default 3.5s timeout
if (!provider) return toast("Coinbase SDK failed to load");

PhaserClient — typed HTTP client

For the endpoints consumers hit outside of player.* — listening history, likes, playlists, spend-permission queries — use the bundled client. JWT lifecycle is stateful so you set it once after sign-in:

import { PhaserClient } from "@jackmorgan/phaser-player";

const client = new PhaserClient({ apiUrl });
client.setJwt(jwt);                       // after sign-in
client.setJwt(null);                      // on sign-out

const likes = await client.me.likes();
await client.me.like("trk_abc123");
const history = await client.me.history();
const pl = await client.me.createPlaylist("Late night", true);
const perm = await client.payments.getPermission();

Non-2xx responses throw PhaserApiError with .status and .body, so apps can branch on err.status === 401 to trigger a sign-out.

Typed errors

payment:failed carries a stable code: PhaserErrorCode alongside the human-readable error message. Errors thrown from player.play() / player.buildSpendPermission() are PhaserError instances with the same code. Branch on the code, not the message:

import type { PhaserErrorCode } from "@jackmorgan/phaser-player";

try {
  await player.play();
} catch (err) {
  switch (err.code as PhaserErrorCode) {
    case "permission_missing":
    case "permission_pending_onchain":
    case "balance_low":
    case "payment_required":   openFundModal(); break;
    case "timeout":            toast("Payment timed out — try again"); break;
    case "signature_declined": toast("You cancelled the signature"); break;
    default:                   toast(`Stream: ${err.message}`, "err");
  }
}

Full code list: wallet_disconnected | payment_required | permission_missing | permission_pending_onchain | balance_low | timeout | charge_failed | authorization_failed | signature_declined | unknown.

Gotchas — read this before shipping

  1. Counterfactual Coinbase Smart Wallets. An account that hasn't sent any UserOp has no deployed bytecode, so SpendPermissionManager.isValid reverts via its isValidSignature call. @base-org/account's multicall fails with "Failed to fetch valid status" / "Failed to fetch current period". The Player tolerates this — the locally-verified signature is trusted until the first UserOp deploys the account — and rewrites the multicall revert to code: "permission_pending_onchain" on payment:failed. Your UI should either retry after a short delay or tell the user "wallet deploying, try again in a moment".
  2. Clock skew. The Player backdates spend-permission start by 5 minutes to dodge Base block-time drift. If you build permissions yourself instead of using player.buildSpendPermission(), do the same — otherwise getCurrentPeriod() reverts when the user's PC clock is even a few seconds ahead of chain time.
  3. Payment poll timeout. player.play() polls the charge endpoint for up to 60 seconds before emitting payment:failed with code: "timeout". This is a safety net — backend dispatch issues surface as a clean timeout rather than an indefinite "processing payment…" spinner.
  4. Do not emit auth:connected from a custom adapter's constructor, even when constructing in an already-connected state. The Player's WalletModuleAdapter already handles that synchronously via auth.isConnected && auth.address. Emitting the event churns internal listeners.
  5. Token refresh. opts.auth.jwt survives wallet reconnects from 0.3.0 onwards. Push updates via player.setAuthToken(jwt | null) or swap the whole adapter via player.setAuthAdapter(newAdapter | null) — do not destroy and rebuild the Player just to refresh a token.

For a production-grade reference implementation, see the phaser-app listener and the wallet integration guide.

Web component

Registering the element is a side-effect of importing the package:

import "@jackmorgan/phaser-player";

Attributes map to PlayerOptions:

<phaser-player
  track-id="trk_abc123"
  api-url="https://phaser-api.jackmorgan.xyz"
  theme="dark"                 <!-- or "light" -->
  spend-allowance="5"
  spend-period-days="30"
></phaser-player>

The underlying Player instance is available as element.player once the component connects.

Compatibility

  • Phaser API v1 — the player targets the /v1/* routes on phaser-api.jackmorgan.xyz. A minimum API version will be pinned here if breaking changes land.
  • Browsers — any evergreen browser with MediaSource support, plus Safari (uses native HLS).
  • Node — the package is browser-only; do not require() from a Node server.

Bundling notes

hls.js and viem are bundled in. If you already ship viem in your app and want to dedupe, import from @jackmorgan/phaser-player as usual — the bundled copy is tree-shakable at the Player level but intentionally not peer-deped to keep the drop-in contract simple. Open an issue if you need a viem-peer build.

License

MIT