cc-stuff/video.lua

276 lines
8.0 KiB
Lua
Raw Normal View History

2024-09-15 02:34:16 +03:00
local args = { ... }
local dfpwm = require("cc.audio.dfpwm")
local ccpi = require("ccpi")
2024-09-16 16:26:36 +03:00
local EV_NONCE = math.floor(0xFFFFFFFF * math.random())
2024-09-15 03:41:47 +03:00
settings.define("video.speaker.left", {
description = "Speaker ID for left audio channel",
default = peripheral.getName(peripheral.find("speaker")),
type = "string"
})
settings.define("video.speaker.right", {
description = "Speaker ID for right audio channel",
default = nil,
type = "string"
})
settings.define("video.monitor", {
description = "Monitor to draw frames on",
default = peripheral.getName(peripheral.find("monitor")),
type = "string"
})
local monitor = peripheral.wrap(settings.get("video.monitor"))
2024-09-15 02:34:16 +03:00
local speakers = {
2024-09-15 03:41:47 +03:00
l = peripheral.wrap(settings.get("video.speaker.left")),
2024-09-15 02:34:16 +03:00
r = nil
}
2024-09-15 03:41:47 +03:00
local r_spk_id = settings.get("video.speaker.right")
if r_spk_id then
speakers.r = peripheral.wrap(r_spk_id)
end
2024-09-15 02:34:16 +03:00
local delay = 0
local loading_concurrency = 8
local buffer_size = 8192
2024-09-15 03:08:14 +03:00
local wait_until_input = false
local n_frames, video_url, audio_url_l, audio_url_r
2024-09-15 02:34:16 +03:00
while args[1] ~= nil and string.sub(args[1], 1, 1) == "-" do
local k = table.remove(args, 1):sub(2)
if k == "m" or k == "monitor" then
monitor = peripheral.wrap(table.remove(args, 1))
elseif k == "l" or k == "speaker-left" then
speakers.l = peripheral.wrap(table.remove(args, 1))
elseif k == "r" or k == "speaker-right" then
2024-09-15 02:37:52 +03:00
speakers.r = peripheral.wrap(table.remove(args, 1))
2024-09-15 02:34:16 +03:00
elseif k == "d" or k == "delay" then
delay = tonumber(table.remove(args, 1))
elseif k == "t" or k == "threads" then
loading_concurrency = tonumber(table.remove(args, 1))
elseif k == "b" or k == "buffer-size" then
buffer_size = tonumber(table.remove(args, 1))
2024-09-15 03:08:14 +03:00
elseif k == "w" or k == "wait" then
wait_until_input = true
elseif k == "J" or k == "info-json" then
local req = assert(http.get(table.remove(args, 1)))
local info = textutils.unserializeJSON(req.readAll())
2024-09-15 03:36:14 +03:00
delay = info.frame_time
n_frames = info.frame_count
video_url = info.video
audio_url_l = info.audio.l
2024-09-15 03:36:14 +03:00
audio_url_r = info.audio.r
req.close()
2024-09-15 02:34:16 +03:00
end
end
if not monitor then
printError("No monitor connected or invalid one specified")
return
end
if not speakers.l then
printError("No speaker connected or invalid one specified")
return
end
2024-09-15 03:36:14 +03:00
if not n_frames and not video_url and not audio_url_l then
if #args < 3 then
printError("Usage: video [-w] [-b BUFSZ] [-t THREADS] [-J URL] [-m MONITOR] [-l SPK_L] [-r SPK_R] [-d FRAME_T] <N_FRAMES> <VIDEO_TEMPLATE> <LEFT_CHANNEL> [RIGHT_CHANNEL]")
return
else
n_frames = tonumber(table.remove(args, 1))
video_url = table.remove(args, 1)
audio_url_l = table.remove(args, 1)
audio_url_r = #args > 0 and table.remove(args, 1) or nil
end
2024-09-15 02:34:16 +03:00
end
2024-09-16 16:26:36 +03:00
local mon_w, mon_h = monitor.getSize()
print(string.format("Using monitor %s (%dx%d)", peripheral.getName(monitor), mon_w, mon_h))
2024-09-15 02:34:16 +03:00
if speakers.r then
2024-09-15 03:08:14 +03:00
print(string.format("Stereo sound: L=%s R=%s", peripheral.getName(speakers.l), peripheral.getName(speakers.r)))
2024-09-15 02:34:16 +03:00
else
print("Mono sound: " .. peripheral.getName(speakers.l))
end
if not speakers.r and audio_url_r then
printError("No right speaker found but right audio channel was specified")
printError("Right channel will not be played")
elseif speakers.r and not audio_url_r then
printError("No URL for right channel was specified but right speaker is set")
printError("Right speaker will remain silent")
end
print("\n\n")
local _, ty = term.getCursorPos()
local tw, _ = term.getSize()
local function draw_bar(y, c1, c2, p, fmt, ...)
local str = string.format(fmt, ...)
local w1 = math.ceil(p * tw)
local w2 = tw - w1
term.setCursorPos(1, y)
term.setBackgroundColor(c1)
term.write(str:sub(1, w1))
local rem = w1 - #str
if rem > 0 then
term.write(string.rep(" ", rem))
end
term.setBackgroundColor(c2)
term.write(str:sub(w1 + 1, w1 + w2))
rem = math.min(tw - #str, w2)
if rem > 0 then
term.write(string.rep(" ", rem))
end
end
local function decode_s8(data)
local buffer = {}
for i = 1, #data do
local v = string.byte(data, i)
if bit32.band(v, 0x80) then
v = bit32.bxor(v, 0x7F) - 128
end
buffer[i] = v
end
return buffer
end
local frames = {}
local audio_frames = { l = {}, r = {} }
local subthreads = {}
local dl_channels = {
l = assert(http.get(audio_url_l, nil, true)),
r = audio_url_r and assert(http.get(audio_url_r, nil, true)) or nil,
}
local n_audio_samples = math.ceil(dl_channels.l.seek("end") / buffer_size)
dl_channels.l.seek("set", 0)
table.insert(subthreads, function()
local chunk
repeat
chunk = dl_channels.l.read(buffer_size)
table.insert(audio_frames.l, chunk or {})
if (#audio_frames.l % 8) == 0 then os.sleep(0) end
until not chunk or #chunk == 0
end)
if dl_channels.r then
table.insert(subthreads, function()
local chunk
repeat
chunk = dl_channels.r.read(buffer_size)
table.insert(audio_frames.r, chunk or {})
if (#audio_frames.r % 8) == 0 then os.sleep(0) end
until not chunk or #chunk == 0
end)
end
for i = 1, loading_concurrency do
table.insert(subthreads, function()
for ndx = i, n_frames, loading_concurrency do
local req = assert(http.get(string.format(video_url, ndx), nil, true))
local img = assert(ccpi.parse(req))
frames[ndx] = img
req.close()
end
end)
end
table.insert(subthreads, function()
2024-09-16 16:26:36 +03:00
repeat
2024-09-15 02:34:16 +03:00
draw_bar(ty - 3, colors.blue, colors.gray, #frames / n_frames, "Loading video [%5d / %5d]", #frames, n_frames)
draw_bar(ty - 2, colors.red, colors.gray, #audio_frames.l / n_audio_samples, "Loading audio [%5d / %5d]", #audio_frames.l, n_audio_samples)
os.sleep(0.25)
2024-09-16 16:26:36 +03:00
until #frames >= n_frames or #audio_frames.l >= n_audio_samples
2024-09-15 02:34:16 +03:00
print()
end)
local playback_done = false
table.insert(subthreads, function()
2024-09-15 03:08:14 +03:00
local tmr = os.startTimer(0.25)
while true do
local ev = { os.pullEvent() }
if ev[1] == "key" and ev[2] == keys.enter then
break
end
2024-09-15 02:34:16 +03:00
term.setCursorPos(1, ty - 1)
term.setBackgroundColor(colors.gray)
term.clearLine()
term.write(string.format("Waiting for frames... (V:%d, A:%d)", #frames, #audio_frames.l))
2024-09-15 03:12:15 +03:00
if #frames > 60 and #audio_frames.l >= n_audio_samples and not wait_until_input then
2024-09-15 03:08:14 +03:00
break
end
2024-09-15 02:34:16 +03:00
end
2024-09-16 16:26:36 +03:00
os.queueEvent("playback_ready", EV_NONCE)
2024-09-15 02:34:16 +03:00
end)
table.insert(subthreads, function()
local is_dfpwm = ({ audio_url_l:find("%.dfpwm") })[2] == #audio_url_l
local decode = use_dfpwm and dfpwm.make_decoder() or decode_s8
2024-09-16 16:26:36 +03:00
repeat
local _, nonce = os.pullEvent("playback_ready")
until nonce == EV_NONCE
2024-09-15 02:34:16 +03:00
for i = 1, n_audio_samples do
local buffer = decode(audio_frames.l[i])
while not speakers.l.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
playback_done = true
end)
table.insert(subthreads, function()
if not audio_url_r then return end
local is_dfpwm = ({ audio_url_r:find("%.dfpwm") })[2] == #audio_url_r
local decode = use_dfpwm and dfpwm.make_decoder() or decode_s8
2024-09-16 16:26:36 +03:00
repeat
local _, nonce = os.pullEvent("playback_ready")
until nonce == EV_NONCE
2024-09-15 02:34:16 +03:00
for i = 1, n_audio_samples do
local buffer = decode(audio_frames.r[i])
while not speakers.r.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
playback_done = true
end)
table.insert(subthreads, function()
2024-09-16 16:26:36 +03:00
repeat
local _, nonce = os.pullEvent("playback_ready")
until nonce == EV_NONCE
2024-09-15 02:34:16 +03:00
local start_t = os.clock()
while not playback_done do
local frame = math.floor((os.clock() - start_t) / math.max(0.05, delay))
term.setCursorPos(1, ty - 1)
2024-09-15 03:08:14 +03:00
term.setBackgroundColor(frame >= #frames and colors.red or colors.gray)
2024-09-15 02:34:16 +03:00
term.clearLine()
term.write(string.format("Playing frame: %d/%d", frame + 1, #frames))
2024-09-16 16:26:36 +03:00
local img = frames[frame + 1]
if img ~= nil then
local x = math.max(math.floor((mon_w - img.w) / 2), 0)
local y = math.max(math.floor((mon_h - img.h) / 2), 0)
ccpi.draw(img, x, y, monitor)
2024-09-15 03:08:14 +03:00
end
2024-09-15 02:34:16 +03:00
os.sleep(delay)
end
end)
parallel.waitForAll(table.unpack(subthreads))