From f6b91af2eeb4a6f60e869b6af442271cdb5154fb Mon Sep 17 00:00:00 2001 From: hkc Date: Sun, 15 Sep 2024 02:34:16 +0300 Subject: [PATCH] Added video+audio player --- video.lua | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 video.lua diff --git a/video.lua b/video.lua new file mode 100644 index 0000000..e06fc85 --- /dev/null +++ b/video.lua @@ -0,0 +1,222 @@ +local args = { ... } +local dfpwm = require("cc.audio.dfpwm") +local ccpi = require("ccpi") + +local monitor = peripheral.find("monitor") +local speakers = { + l = peripheral.find("speaker"), + r = nil +} + +local delay = 0 +local loading_concurrency = 8 +local buffer_size = 8192 + +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 + speakers.l = peripheral.wrap(table.remove(args, 1)) + 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)) + 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 + +if #args < 3 then + printError("Usage: video [-m MONITOR] [-l SPK_L] [-r SPK_R] [-d FRAME_T] [RIGHT_CHANNEL]") + return +end + +print(string.format("Using monitor %s", peripheral.getName(monitor))) +if speakers.r then + print("Stereo sound: L=%s R=%s", peripheral.getName(speakers.l), peripheral.getName(speakers.r)) +else + print("Mono sound: " .. peripheral.getName(speakers.l)) +end + +local n_frames = tonumber(table.remove(args, 1)) +local video_url = table.remove(args, 1) +local audio_url_l = table.remove(args, 1) +local audio_url_r = #args > 0 and table.remove(args, 1) or nil + +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() + while #frames ~= n_frames or #audio_frames.l < n_audio_samples do + 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) + end + print() +end) + +local playback_locked = true +local playback_done = false + +table.insert(subthreads, function() + while #frames < 60 or #audio_frames.l < n_audio_samples do + 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)) + os.sleep(0.25) + end + playback_locked = false +end) + +table.insert(subthreads, function() + while playback_locked do os.sleep(0) end -- spin +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 + while playback_locked do os.sleep(0) end -- spin + + 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 + + while playback_locked do os.sleep(0) end -- spin + + 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() + while playback_locked do os.sleep(0) end -- spin + + 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) + term.setBackgroundColor(colors.gray) + term.clearLine() + term.write(string.format("Playing frame: %d/%d", frame + 1, #frames)) + ccpi.draw(frames[frame + 1], 1, 1, monitor) + os.sleep(delay) + end +end) + +parallel.waitForAll(table.unpack(subthreads)) +