@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
Maintainers
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 addUsage 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 :
- Résolution catalogue — walk de l'index
tracks-dbpour lecb, écrit dansalbum:<cb>KV (TTL 24h). Tous les(disc, track) → midducbsont alors instantanément disponibles cross-isolate. - Métadonnées objet —
HEADScaleway pour chaquemiden parallèle, écrit danshead:<mid>KV (TTL 24h). Évite la HEAD upstream (~150ms) à chaque/init-stream. - Cache edge block 0 —
Range 0-1048575pour chaquemiden parallèle,await caches.default.put. Cloudflare Cache API persistant 7 jours par PoP. Le premier/streamRange 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-webviewqui 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+AVAssetResourceLoaderDelegatequi appelle/init-stream, intercepte les range requests AVPlayer et déchiffre AES-CTR viaCryptoKit. Réimplémente le SDK en ~150 lignes Swift. - Native Android (Kotlin) : ExoPlayer +
DataSource.Factorycustom, déchiffrement viajavax.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 viaprefetchSession. Track-to-track latency is ~50ms.kvLookahead = 5— the next 5 tracks have their mid + head + edge cache pre-warmed via the new/warmup-tracksendpoint. Cross-album playlists feel as fast as in-album ones.prefetchLeadSeconds = 8— how early beforeaudio.endedthe next-track session prefetch fires.
Events (all optional):
onItemsChange(items)— fired after every mutation.onCurrentChange(curr, prev)— fired onplay(),next(),prev(), auto-advance.onPrefetchState(e)—{itemId, ref, layer, state}wherelayer ∈ {session, kv}andstate ∈ {pending, ready, error, invalidated}. Use for observability.onError(err, ctx)—ctx?.itemIdcorrelates 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)— constructeurplayer.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— leHTMLAudioElementà insérer dans le DOMplayer.destroy()— release session, audio, listeners (réutilisable ensuite viaload())prefetchSession(workerUrl, token, ref) : PrefetchedSession— pre-crée la session N+1 pourplayer.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-clientReleases 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 automatiquementLe workflow vérifie que le tag matche package.json (refuse de publier si mismatch).
License
MIT — voir LICENSE.
