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

@cyberscaling/secure-audio-stream-client

v0.3.1

Published

JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.

Downloads

65

Readme

@cyberscaling/secure-audio-stream-client

JavaScript SDK pour le lecteur audio chiffré musicme / Cyberscaling. Charge un morceau identifié par (cb, disc, track), ouvre une session sur le worker de streaming, télécharge les chunks chiffrés en Range, déchiffre AES-CTR au vol, alimente une MediaSource (ou ManagedMediaSource sur iOS Safari 17.1+) via mp4box.js. Expose un <audio> HTML standard.

Installation

bun add @cyberscaling/secure-audio-stream-client
# ou: pnpm add / npm install / yarn add

Usage minimal

import { SecureAudioPlayer } from '@cyberscaling/secure-audio-stream-client'

const player = new SecureAudioPlayer({
  workerUrl: 'https://stream.musicme.cc',
  getToken: async () => {
    const r = await fetch('/api/player-token', { method: 'POST', credentials: 'include' })
    const { token } = await r.json()
    return token
  },
  mode: 'mse', // 'blob' uniquement pour debug / fallback explicite
})

document.querySelector('#player-container')!.append(player.audio)

await player.load({ cb: 5400863209100, disc: 1, track: 1 })
await player.play()

Plateformes & modes de lecture

Le partenaire passe toujours mode: 'mse'. Le SDK choisit le backend au runtime selon les capacités du navigateur :

| Plateforme | Backend résolu | Comportement | |---|---|---| | iOS Safari 17.1+, macOS Safari 17.1+ | ManagedMediaSource (MMS) | Streaming progressif, économe en cellulaire. Pause/reprise du fetch loop sur endstreaming/startstreaming. | | Chrome / Firefox / Edge / Safari desktop pré-17.1 | MediaSource (MSE classique) | Streaming progressif standard, comportement inchangé. | | iOS Safari pré-17.1 | blob (fallback auto) | Téléchargement + déchiffrement intégral avant lecture. onError reçoit un avertissement non-fatal mms_fallback: no_media_source. | | Browser sans MediaSource ni ManagedMediaSource | blob (fallback auto) | Idem ci-dessus. |

Aucune modif du code partenaire pour activer iOS. Le SDK auto-détecte ManagedMediaSource et choisit le bon chemin.

Prefetch & cache warm-up

Le play→canplay froid coûte typiquement 1.5-2s (cold DO, cold KV catalogue, cold edge cache). Deux helpers réduisent radicalement cette latence dans le scénario réel partenaire — un user qui ouvre une page album puis joue un morceau.

prefetchAlbum(workerUrl, token, cb)

À appeler dès le mount d'une page album, en fire-and-forget. Le SDK orchestre une série de calls /warmup-album côté worker — chaque call traite jusqu'à 8 tracks en pools parallèles :

  1. Résolution catalogue — walk de l'index tracks-db pour le cb, écrit dans album:<cb> KV (TTL 24h). Tous les (disc, track) → mid du cb sont alors instantanément disponibles cross-isolate.
  2. Métadonnées objetHEAD Scaleway pour chaque mid en parallèle, écrit dans head:<mid> KV (TTL 24h). Évite la HEAD upstream (~150ms) à chaque /init-stream.
  3. Cache edge block 0Range 0-1048575 pour chaque mid en parallèle, await caches.default.put. Cloudflare Cache API persistant 7 jours par PoP. Le premier /stream Range du listener final est alors un HIT (~80ms au lieu de ~400-700ms).
import { prefetchAlbum } from '@cyberscaling/secure-audio-stream-client'

// Appel typique sur ouverture d'une page album
useEffect(() => {
  void prefetchAlbum(workerUrl, token, cb).catch(() => {})  // non-fatal
}, [cb])

Pourquoi le SDK chunke: la limite Cloudflare Workers de 50 subrequests par invocation (Bundled plan) est atteinte dès qu'un album dépasse ~10 tracks (chaque track ≈ 4 subrequests : KV head get/put + Scaleway HEAD + Scaleway GET). prefetchAlbum issue donc le 1er batch synchrone (qui retourne tracks = compte total), puis fanout les batches restants en parallèle. Chaque batch HTTP = une invocation Worker séparée avec son propre budget de 50. Albums de toute taille marchent de manière transparente.

Retourne AlbumWarmupReport agrégé ({tracks, head_cached, edge_filled, edge_errors, batches, phases_ms}).

prefetchSession(workerUrl, token, ref)

Pre-crée la session N+1 pendant que N joue. Renvoie un PrefetchedSession activable via player.loadPrefetched(...) → switch instantané sur fin de track (auto-advance gapless).

import { prefetchSession } from '@cyberscaling/secure-audio-stream-client'

// Pendant la lecture de la track N, dès que `currentTime > duration - 5s` :
const next = await prefetchSession(workerUrl, token, { cb, disc: 1, track: N + 1 })

// Sur audio.ended :
await player.loadPrefetched(next)

Combiné avec prefetchAlbum (déjà appelé sur mount), prefetchSession se contente d'un /init-stream warm — ~50ms.

Caches serveur (transparents)

Aucune action client requise, à connaître pour le debug :

| Cache | Clé | Couverture | TTL | Effet | |---|---|---|---|---| | album:<cb> (KV) | code-barres | cross-isolate | 24h | Skip b-tree walk (~300ms cold) | | head:<mid> (KV) | mid | cross-isolate | 24h | Skip Scaleway HEAD (~150ms cold) | | scaleway/full/<mid> (Cache API) | mid | per-PoP | 7 jours | Fichiers ≤10 MiB cached whole — toute Range = HIT | | scaleway/blk/<mid>/<n> (Cache API) | mid+blockIdx | per-PoP | 7 jours | Fichiers >10 MiB cached en blocks 1 MiB | | SessionPoolDO warmth alarm | 4 pool instances | global | self-rearm 15s | putSession reste warm (~6-10ms) malgré inactivité | | lookupCache (module-scoped) | (cb,disc,track) | per-isolate | 5min | Skip KV/R2 sur replay même isolate |

X-Cache: HIT|MISS|PARTIAL est exposé sur les responses /stream pour observer le edge cache. Le header Server-Timing détaille les phases serveur de chaque endpoint (/init-stream, /key, /stream, /warmup-album).

Télémétrie

Active la collecte de métriques côté worker (dataset play_performance) :

new SecureAudioPlayer({
  workerUrl: 'https://stream.musicme.cc',
  getToken,
  mode: 'mse',
  metrics: { enabled: true, sampleRate: 1.0 },
  onMetrics: (report) => {
    // report.mode = 'mse' | 'mms' | 'blob' (le backend réellement utilisé)
    // report.outcome = 'canplay' | 'error' | 'aborted'
    // report.phases_ms.* = breakdown latence (get_token, init_session, fetch_key, mse_setup, mp4box_ready, canplay, total)
    console.info('[player] metrics', report)
  },
})

report.mode permet de slicer les latences par plateforme côté Analytics Engine.

Plateformes non-web

Le SDK est web-only — il dépend de MediaSource/ManagedMediaSource, fetch, et crypto.subtle du navigateur. Pour les apps mobiles natives ou React Native, voir le guide d'intégration partenaire (docs/integration-guide.md du repo musicme-onboarding-mcp), section "Plateformes non-web". En résumé :

  • React Native : recommandé via react-native-webview qui héberge la webapp partenaire — l'engine WebKit d'iOS fournit MMS, le SDK marche tel quel. Alternative lourde : bridge natif réimplémentant le flow init/key/stream + déchiffrement.
  • Native iOS (Swift) : AVPlayer + AVAssetResourceLoaderDelegate qui appelle /init-stream, intercepte les range requests AVPlayer et déchiffre AES-CTR via CryptoKit. Réimplémente le SDK en ~150 lignes Swift.
  • Native Android (Kotlin) : ExoPlayer + DataSource.Factory custom, déchiffrement via javax.crypto.Cipher. Mêmes endpoints HTTP que le SDK web.

Playlist (auto-advance + dynamic queue)

import { Playlist } from '@cyberscaling/secure-audio-stream-client'

const playlist = new Playlist({
  workerUrl: 'https://stream.musicme.cc',
  getToken: async () => (await fetch('/api/player-token', { method: 'POST', credentials: 'include' }).then(r => r.json())).token,
  items: [
    { cb: 5400863209100, disc: 1, track: 1 },
    { cb: 5400863209100, disc: 1, track: 2 },
    { cb: 3663729427441, disc: 1, track: 7 },
  ],
  onCurrentChange: (curr, prev) => console.info('[playlist] now playing', curr?.ref),
})

document.querySelector('#player')!.append(playlist.audio)
await playlist.play()

// Mutate live:
playlist.insert({ cb: 5400863209100, disc: 1, track: 5 }, /* position */ 1)
playlist.move(playlist.items[2].id, 0)
playlist.remove(playlist.items[1].id)

Each item gets a stable id (auto-generated on insert if you pass a bare TrackRef). Mutations are synchronous; the Playlist re-computes its lookahead window on every change and adapts the prefetch state transparently.

Lookahead policy (defaults — override via constructor opts):

  • sessionLookahead = 2 — the next 2 tracks have their session + key pre-created via prefetchSession. Track-to-track latency is ~50ms.
  • kvLookahead = 5 — the next 5 tracks have their mid + head + edge cache pre-warmed via the new /warmup-tracks endpoint. Cross-album playlists feel as fast as in-album ones.
  • prefetchLeadSeconds = 8 — how early before audio.ended the next-track session prefetch fires.

Events (all optional):

  • onItemsChange(items) — fired after every mutation.
  • onCurrentChange(curr, prev) — fired on play(), next(), prev(), auto-advance.
  • onPrefetchState(e){itemId, ref, layer, state} where layer ∈ {session, kv} and state ∈ {pending, ready, error, invalidated}. Use for observability.
  • onError(err, ctx)ctx?.itemId correlates the failure to the offending track.

Backwards compatibility: SecureAudioPlayer, prefetchSession, prefetchAlbum, prefetchTracks remain exported and unchanged. Playlist is purely additive.

API

Voir src/index.ts pour la surface complète :

  • SecureAudioPlayer(options) — constructeur
  • player.load(ref) — ouvre une session pour (cb, disc, track)
  • player.loadPrefetched(prefetched) — active une session pré-créée (gapless)
  • player.play() / player.pause() / player.seek(time) — wrappers <audio>
  • player.audio — le HTMLAudioElement à insérer dans le DOM
  • player.destroy() — release session, audio, listeners (réutilisable ensuite via load())
  • prefetchSession(workerUrl, token, ref) : PrefetchedSession — pre-crée la session N+1 pour player.loadPrefetched (gapless auto-advance)
  • prefetchAlbum(workerUrl, token, cb) : AlbumWarmupReport — fire-and-forget sur mount d'album, warm les caches album KV + head KV + edge block 0 de toutes les tracks

Tests

bun run test       # vitest, helpers purs (detectMediaSourceCtor, WaitGate)
bun run typecheck  # tsc --noEmit
bun run build      # tsup → dist/ (lib publish artifacts: ESM + CJS + .d.ts)
bun run build:demo # vite build → ../dist-demo/ (demo SPA, dev only)

Les chemins DOM-heavy (loadMse, MMS event wiring) sont validés manuellement sur la matrice browser documentée dans docs/superpowers/plans/2026-05-10-ios-mms-streaming.md du repo.

Publication (mainteneurs)

Le SDK est publié sur npm public sous le scope @cyberscaling. Le workflow GitHub Actions .github/workflows/publish-client.yml se déclenche sur tag client-v<version> et publie automatiquement après typecheck + tests + build.

Première publication manuelle (one-shot)

L'org @cyberscaling doit exister sur npm et un NPM_TOKEN (Granular Access Token, scope @cyberscaling, write access) doit être configuré côté GitHub.

# 1. Vérifier que l'org @cyberscaling existe sur npmjs.com
#    Sinon : npmjs.com → Sign in → Add organization → Free (public packages)

# 2. Générer un Granular Access Token
#    npmjs.com → Account → Access Tokens → Generate New Token → Granular
#    - Expiration : 1 an
#    - Permissions : Read and write
#    - Packages and scopes : @cyberscaling
#    Copier la valeur (commence par `npm_…`).

# 3. Ajouter le secret côté repo GitHub
#    https://github.com/cyberscaling/secure-audio-stream/settings/secrets/actions
#    Name: NPM_TOKEN, Value: <le token copié>

# 4. Premier publish manuel (depuis client/) pour réserver le nom
cd client
npm login --scope=@cyberscaling
bun run build
npm publish --access public

# 5. Vérifier
npm view @cyberscaling/secure-audio-stream-client

Releases suivantes

# 1. Bump la version dans client/package.json
#    (semver : patch pour fix, minor pour feature, major pour breaking)

# 2. Commit le bump
git add client/package.json
git commit -m "chore(client): release vX.Y.Z"

# 3. Tag + push
git tag client-vX.Y.Z
git push origin main --tags

# 4. Le workflow GH Actions construit + publie automatiquement

Le workflow vérifie que le tag matche package.json (refuse de publier si mismatch).

License

MIT — voir LICENSE.