-- A pretend music player: it shows its "track" in the OS media controls (the
-- volume flyout on Windows, the MPRIS applet on Linux, Control Center on
-- macOS) and follows the play/pause, next and previous keys, wherever they
-- are pressed. Nothing is actually played.
--
-- neyuki examples/media.nyk [artwork.png]
local ui = require("@neyuki/ui")
local os = require("@neyuki/os")
local math = require("@neyuki/math")
local tracks = {
{ title = "Morning Light", artist = "The Placeholders", album = "Demo Tapes", duration = 184 },
{ title = "Second Wind", artist = "The Placeholders", album = "Demo Tapes", duration = 212 },
{ title = "Long Way Home", artist = "Sample Rate", album = "Loops", duration = 245 },
}
local current = 1
local position = 0
local playing = true
local log = {}
local artworkPath = os.args()[1]
local artwork = nil
if (artworkPath != nil) then artwork = ui.loadImage(artworkPath) end
local win = ui.open({ title = "Neyuki Player", width = 480, height = 260 })
function publish()
local track = tracks[current]
local ok, err = ui.mediaSession({
title = track.title,
artist = track.artist,
album = track.album,
artwork = artwork,
duration = track.duration,
position = position,
playing = playing,
})
if (not ok) then note("no media controls: " .. tostring(err)) end
end
function skip(by: number)
current = (current - 1 + by) % #tracks + 1
position = 0
publish()
end
function note(line: string)
table.insert(log, line)
if (#log > 5) then table.remove(log, 1) end
end
publish()
ui.run(function(events: {Event}, dt: number)
for _, event in events do
if (event.kind == "keydown" and event.key == "escape") then
win:close()
return
elseif (event.kind == "mediakey") then
note("mediakey " .. event.key)
if (event.key == "playpause") then playing = not playing
elseif (event.key == "play") then playing = true
elseif (event.key == "pause" or event.key == "stop") then playing = false
elseif (event.key == "next") then skip(1)
elseif (event.key == "previous") then skip(-1)
end
publish()
elseif (event.kind == "mediasession" and event.action == "seek") then
local track = tracks[current]
local target = event.position
if (target == nil) then target = position + event.offset end
position = math.clamp(target, 0, track.duration)
note("seek to " .. tostring(math.floor(position)))
publish()
end
end
local track = tracks[current]
if (playing) then
position += dt
if (position >= track.duration) then skip(1) end
end
win:clear("#1e1e2e")
win:text(24, 24, track.title, { size = 26, color = "#cdd6f4" })
win:text(24, 60, track.artist .. " - " .. track.album, { size = 16, color = "#a6adc8" })
local w, _ = win:size()
local barWidth = w - 48
win:fillRect(24, 100, barWidth, 6, "#313244", { radius = 3 })
win:fillRect(24, 100, barWidth * position / track.duration, 6, "#89b4fa", { radius = 3 })
local status = "paused"
if (playing) then status = "playing" end
win:text(24, 116, status .. " " .. tostring(math.floor(position)) .. " / " .. tostring(track.duration) .. " s",
{ size = 14, color = "#a6adc8" })
for i, line in log do
win:text(24, 140 + i * 18, line, { size = 13, color = "#6c7086" })
end
win:present()
end)