local args = { ... } local dfpwm = require("cc.audio.dfpwm") local ccpi = require("ccpi") 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")) local speakers = { l = peripheral.wrap(settings.get("video.speaker.left")), r = nil } local r_spk_id = settings.get("video.speaker.right") if r_spk_id then speakers.r = peripheral.wrap(r_spk_id) end local delay = 0 local loading_concurrency = 8 local buffer_size = 8192 local wait_until_input = false local n_frames, video_url, audio_url_l, audio_url_r 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.r = 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)) 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()) delay = info.frame_time n_frames = info.frame_count video_url = info.video audio_url_l = info.audio.l audio_url_r = info.audio.r req.close() 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 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] [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 end print(string.format("Using monitor %s", peripheral.getName(monitor))) if speakers.r then print(string.format("Stereo sound: L=%s R=%s", peripheral.getName(speakers.l), peripheral.getName(speakers.r))) 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() 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_done = false table.insert(subthreads, function() 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 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)) if #frames > 60 and #audio_frames.l >= n_audio_samples and not wait_until_input then break end end os.queueEvent("playback_ready") 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 os.pullEvent("playback_ready") 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 os.pullEvent("playback_ready") 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() os.pullEvent("playback_ready") 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(frame >= #frames and colors.red or colors.gray) term.clearLine() term.write(string.format("Playing frame: %d/%d", frame + 1, #frames)) if frames[frame] then ccpi.draw(frames[frame + 1], 1, 1, monitor) end os.sleep(delay) end end) parallel.waitForAll(table.unpack(subthreads))