Skip to content

Renderer: play MP3 from live sources, for Spotify MP3 passthrough #227

Description

@madenvel

Summary

The Spotify Connect plugin can only pass Ogg Vorbis to the renderer. Spotify items that exist only as MP3 cannot play in Kalinka, and Spotify skips them as unavailable. Passing MP3 through needs the renderer's MP3 decoder to play live, paced HTTP sources, as the Vorbis decoder has since #225. Today it cannot.

Background

  • The plugin runs librespot with --passthrough: Spotify's compressed audio is served to the renderer unchanged, and the renderer decodes it. librespot's passthrough only supports Ogg Vorbis.
  • At 320 kbps, librespot prefers OGG_VORBIS_320, then MP3_320, MP3_256, then OGG_VORBIS_160. A passthrough load of an MP3 file fails outright, so the plugin now limits librespot to Vorbis files. A track without Vorbis files is skipped.
  • How often Spotify serves MP3-only items is unknown. librespot notes that most podcasts are 96 kbps Vorbis. Worth measuring before building this.

What blocks it in the renderer

Mp3StreamDecoder assumes a finite, seekable file:

  1. Open reads the whole stream. mp3dec_ex_open_cb(..., MP3D_SEEK_TO_SAMPLE) seeks to 0, then scans to EOF to build a seek index unless a Xing/Info tag is found, then seeks back to the first frame. A live source is produced only ~2–3 s ahead of the playback the renderer reports, so open never returns and playback never starts. Unfinished live responses can't seek backwards either.
  2. Reads wait for the full requested size. readCallback calls waitForData(token, size), and Buffer::waitForData waits for min(size, capacity) bytes. minimp3 requests up to 128 KiB (MINIMP3_IO_SIZE): about 3.3 s at 320 kbps, more than the default read-ahead. The producer only releases more after playback advances, so this can block. The live Vorbis path waits for one byte and returns a short read.
  3. Length comes from the total sample count. mp3.samples feeds the start-offset check and streamSize. Neither is known for a live source.

MIME and format mapping already work: audio/mpeg and mp3 select FormatMpeg in NativePlayer.cpp. No server or SDK change is expected.

Proposed change

For live sources (X-Kalinka-Live: 1, unknown length), keeping the finite-file path unchanged:

  • Decode without scanning or seeking back. MP3D_DO_NOT_SCAN alone is not enough, because open still seeks to 0 and back to the first frame. Frame-by-frame mp3dec_decode_frame over a sliding input buffer avoids seeks entirely.
  • Return short reads: wait for at least one byte and read what is available.
  • Report an unknown stream size, and take position from decoded frames. Live sources start at timeline_offset_ms, not startOffsetMs.

Acceptance

  • A paced, lengthless audio/mpeg live response starts after its first frames, without reading ahead of the producer.
  • Pause and resume work without being treated as EOF, and snapshots report advancing position, as for live Vorbis.
  • Finite MP3 files (local files, Jamendo) keep their duration and seeking.
  • Renderer tests cover a paced live MP3 source, alongside the live Vorbis tests from Support live sequential streams for Connect plugins #225.

Follow-up in kalinka-plugin-spotify

Not part of this issue, but needed before Spotify can use it:

  • librespot patch: an MP3 passthrough (upstream PassthroughDecoder rejects non-Ogg formats), and removal of the Vorbis-only file filter.
  • Plugin: MP3 frame parsing for pacing, serving audio/mpeg with TrackSource(format="mp3").

Related: #208

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmedReproduced or confirmed in the codeenhancementNew feature or requestpriority: P2Worth doing; planned by valuetriagedSeen and prioritised

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions