Streaming
A music provider usually gives every song a plain URL. When that’s not enough,
give songs a uri like myservice:SONG_ID and handle it in a background thread, which
keeps working while Android has the app in the background.
Stream resolvers
Section titled “Stream resolvers”Stream URLs that expire, or that cost a request each, shouldn’t be built for a whole list in playable. Register a
resolver instead:
-- started with nari.thread("resolver.lua")nari.resolver("myservice", function(id) local response = nari.http({ url = "https://example.com/stream/" .. id }) return nari.json_decode(response.body).urlend)The player calls it right before it opens each song. Raise an error, or return nil, when there’s no stream: the
player skips to the next song.
Byte sources
Section titled “Byte sources”When a song can’t be reached with a URL at all (your own protocol, a file inside an archive, a service that signs every chunk), hand the player the bytes yourself:
-- started with nari.thread("source.lua")nari.source("myproto", function(id, out) local response = nari.http({ url = "https://example.com/files/" .. id }) out:size(#response.body) -- optional: the total size lets the player seek out:write(response.body) -- call it as many times as you likeend)out method |
|
|---|---|
out:write(bytes) |
Sends bytes to the player. Returns false once the player stopped listening, like when the song changed: stop then. |
out:size(bytes) |
The total size, when you know it. Call it before the first write. |
out:live() |
Marks an endless stream. The player keeps a few megabytes and write waits while that’s full. |
out:closed() |
true once the player stopped listening. |
out:write(bytes, offset) |
Sends bytes for that position of the song. |
out:wanted() |
The position the player is waiting for after a seek, or nil. Check it between writes and jump there to make seeking instant. |
The song ends when the handler returns. Raise an error to skip it. The format is detected from the bytes, and the
song’s file_name helps.
Downloads
Section titled “Downloads”Nari can download songs from a source by copying the whole stream to a file (not live streams). Pass a third argument to change that:
{ save = false }when the service doesn’t allow keeping its songs: Nari won’t download them.{ save = "sealed" }keeps downloads encrypted, the way streaming apps do. Nari saves what the handler writes formyproto:SONG_ID?saveto a.sealedfile, and plays it by openingmyproto:SONG_ID?offline=<percent-encoded path>. Your handler reads and decrypts the file, so it decides when a download stops playing. Keep the keys in private storage so the files can’t be opened on another device.
The handler runs on its thread, so give long or endless streams a thread of their own. The Spotify plugin plays all its songs through a source.
Live radio
Section titled “Live radio”Endless streams play live: Icecast and Shoutcast stations, .m3u and .pls playlists, and HLS (.m3u8 with AAC,
MP3 or MPEG-TS segments). Nari notices them by itself; set live = true in a playable entry to say so up front.
While a stream is live:
- Now Playing shows LIVE instead of the progress bar, and seeking does nothing
- the station’s “now playing” title (usually
Artist - Song) replaces the song’s title and artist everywhere, and the station’s name becomes the album - the
trackevent fires again every time the title changes, withlive = true - songs heard for 30 seconds are scrobbled
- dropped connections are retried a few times before giving up
Encrypted HLS streams and fragmented MP4 segments aren’t supported.