diff options
Diffstat (limited to 'packages/meshbay-hub/src')
12 files changed, 78 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 6df5a77..b05c8b1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -190,6 +190,7 @@ export default { }, 'music.err_transport': 'Transport nicht verbunden', 'music.err_playback': 'Wiedergabefehler', + 'music.err_transcode': 'Formatumwandlung fehlgeschlagen', 'music.player_play': 'Abspielen', 'music.player_pause': 'Pause', 'music.player_prev': 'Vorheriger', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 4c0db39..db7d048 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -188,6 +188,7 @@ export default { }, 'music.err_transport': 'Transport not connected', 'music.err_playback': 'Playback error', + 'music.err_transcode': 'Format conversion failed', 'music.player_play': 'Play', 'music.player_pause': 'Pause', 'music.player_prev': 'Previous', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 2d795fc..8f29908 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -188,6 +188,7 @@ export default { }, 'music.err_transport': 'Transporte no conectado', 'music.err_playback': 'Error de reproducción', + 'music.err_transcode': 'Error al convertir el formato', 'music.player_play': 'Reproducir', 'music.player_pause': 'Pausa', 'music.player_prev': 'Anterior', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 0ce4c53..5d83517 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -189,6 +189,7 @@ export default { }, 'music.err_transport': 'Transport non connecté', 'music.err_playback': 'Erreur de lecture', + 'music.err_transcode': 'Échec de la conversion du format', 'music.player_play': 'Lecture', 'music.player_pause': 'Pause', 'music.player_prev': 'Précédent', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 2ddea64..fb88cbe 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -189,6 +189,7 @@ export default { }, 'music.err_transport': 'Trasporto non connesso', 'music.err_playback': 'Errore di riproduzione', + 'music.err_transcode': 'Conversione del formato non riuscita', 'music.player_play': 'Riproduci', 'music.player_pause': 'Pausa', 'music.player_prev': 'Precedente', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 82a8fc2..84e9eb5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -186,6 +186,7 @@ export default { }, 'music.err_transport': 'トランスポートが接続されていません', 'music.err_playback': '再生エラー', + 'music.err_transcode': 'フォーマット変換に失敗しました', 'music.player_play': '再生', 'music.player_pause': '一時停止', 'music.player_prev': '前へ', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index be0184a..06097e6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -190,6 +190,7 @@ export default { }, 'music.err_transport': 'Transport niet verbonden', 'music.err_playback': 'Afspeelfout', + 'music.err_transcode': 'Formaatconversie mislukt', 'music.player_play': 'Afspelen', 'music.player_pause': 'Pauzeren', 'music.player_prev': 'Vorige', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 0738cf5..e24fd1e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -199,6 +199,7 @@ export default { }, 'music.err_transport': 'Transport niepołączony', 'music.err_playback': 'Błąd odtwarzania', + 'music.err_transcode': 'Konwersja formatu nie powiodła się', 'music.player_play': 'Odtwórz', 'music.player_pause': 'Pauza', 'music.player_prev': 'Poprzedni', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 1874a5b..1a3069a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -190,6 +190,7 @@ export default { }, 'music.err_transport': 'Transporte não conectado', 'music.err_playback': 'Erro de reprodução', + 'music.err_transcode': 'Falha na conversão de formato', 'music.player_play': 'Reproduzir', 'music.player_pause': 'Pausar', 'music.player_prev': 'Anterior', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 08fd940..236747e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -183,6 +183,7 @@ export default { }, 'music.err_transport': '传输未连接', 'music.err_playback': '播放错误', + 'music.err_transcode': '格式转换失败', 'music.player_play': '播放', 'music.player_pause': '暂停', 'music.player_prev': '上一首', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js index 690efc9..b75d73c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js @@ -16,11 +16,15 @@ import { CHUNK_SIZE, pipelinedDownload } from './file-utils.js'; * calls `onPlayQueue(tracks, startIndex)`, threaded down from group-page.js, * to hand this component a new queue. * - * No streaming, no MSE, no node-side transcode pool: a track is a few - * megabytes, so it is downloaded and decrypted once through the same chunk - * pipeline Files already uses (file-utils.js's pipelinedDownload), then - * played from a blob URL — the deliberate simplification recorded in - * musicbay.md §2.2. + * No streaming, no MSE, no node-side transcode pool for the common case: a + * track is a few megabytes, so it is downloaded and decrypted once through + * the same chunk pipeline Files already uses (file-utils.js's + * pipelinedDownload), then played from a blob URL — the deliberate + * simplification recorded in musicbay.md §2.2. WMA and Musepack are the one + * exception (`NEEDS_TRANSCODE_RE` below): neither decodes in a browser's + * <audio> element at all, tagged correctly or not, so those two go through + * `transport.requestAudioTranscode` first — a node-side, cached-after-once + * AAC/M4A conversion — before the same download/blob path runs. */ const MIME_BY_EXT = { @@ -28,6 +32,11 @@ const MIME_BY_EXT = { wav: 'audio/wav', aac: 'audio/aac', m4a: 'audio/mp4', }; +// Kept in sync with the node's BROWSER_INCOMPATIBLE_AUDIO_EXTS +// (webrtc_server.py) — both name the same two formats no mainstream +// browser's <audio> element decodes natively. +const NEEDS_TRANSCODE_RE = /\.(wma|mpc)$/i; + function guessMime(name) { const ext = (name || '').split('.').pop().toLowerCase(); return MIME_BY_EXT[ext] || 'audio/mpeg'; @@ -186,10 +195,30 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) { if (cached) return cached.url; const transport = transportRef.current; if (!transport || !transport.connected) throw new Error(t('music.err_transport')); - const totalChunks = Math.ceil(entry.size / CHUNK_SIZE); - const chunks = await pipelinedDownload(transport, gekRef.current, entry.id, totalChunks); - const blob = new Blob(chunks, { type: guessMime(entry.name) }); + + let downloadId = entry.id; + let downloadSize = entry.size; + let mime = guessMime(entry.name); + if (NEEDS_TRANSCODE_RE.test(entry.name)) { + let info; + try { + info = await transport.requestAudioTranscode(entry.id); + } catch (err) { + throw new Error(t('music.err_transcode')); + } + downloadId = info.hash; + downloadSize = info.size; + mime = info.mime || 'audio/mp4'; + } + + const totalChunks = Math.ceil(downloadSize / CHUNK_SIZE); + const chunks = await pipelinedDownload(transport, gekRef.current, downloadId, totalChunks); + const blob = new Blob(chunks, { type: mime }); const url = URL.createObjectURL(blob); + // Keyed by the track's own id, not `downloadId` — the transcode cache + // hash is an implementation detail of getting there, and a second play + // of the same track must still hit this cache rather than re-requesting + // a transcode the node already ran once. blobCacheRef.current.set(entry.id, { url, order: blobInsertRef.current++ }); evictOldBlobs(); return url; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index e0e4a64..4f8e3b5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -620,6 +620,23 @@ class MeshBayTransport { } /** + * Server-side transcode of a Music-app file the browser's own <audio> + * element cannot decode at all (WMA, Musepack) into AAC/M4A. Returns + * `{ hash, size, mime }` — the *cache* hash to pull through the normal + * file_req/chunk path (fetchChunk/pipelinedDownload), not the file's own + * id, the same indirection already used for a TMDB poster or a + * MusicBrainz cover. Cached node-side after the first call, but ffmpeg + * still has to run at least once and a transcode slot can be busy, so + * this gets a longer timeout than the metadata lookups above. + */ + async requestAudioTranscode(fileId) { + const msg = await this._sendAndWait( + { type: 'audio_transcode_req', v: '0.9', file_id: fileId }, 120000); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + + /** * Set/clear the node-wide MusicBrainz contact string — the User-Agent * identity MusicBrainz's usage policy asks for, not a credential (there * is none, docs/musicbay.md §3.1). Signed like setTmdbConfig: this turns @@ -1386,6 +1403,9 @@ class MeshBayTransport { // search box can have more than one of these in flight at once. : obj.type === 'season_meta_req' ? `season_meta:${obj.tmdb_id}:${obj.season}` : obj.type === 'tmdb_search_req' ? `tmdb_search:${obj.media_type}:${obj.query}` + // Same reordering hazard as media_meta_req: the player prefetches + // the next track while the current one may still be transcoding. + : obj.type === 'audio_transcode_req' ? `audio_transcode:${obj.file_id}` : null, resolve: (msg) => { clearTimeout(timeout); this._pending.delete(id); resolve(msg); }, reject: (err) => { clearTimeout(timeout); this._pending.delete(id); reject(err); }, @@ -1641,6 +1661,17 @@ class MeshBayTransport { return; } + // Same reasoning as music_meta_resp: keyed, not arrival-order — the + // player can have a transcode of the current track and a prefetch of + // the next one in flight together. + if (msg.type === 'audio_transcode_resp') { + const key = `audio_transcode:${msg.file_id}`; + for (const [, handler] of this._pending) { + if (handler._key === key) { handler.resolve(msg); return; } + } + return; + } + // Same reasoning as media_meta_resp: keyed, not arrival-order, and // "nobody's waiting any more" must not fall through either. if (msg.type === 'season_meta_resp') { |