-- ocv.lua — плеер видео OCV5 для OpenComputers (Tier 3 GPU + Tier 3 Screen). -- -- Видео готовится на ПК через ocv_encode.py: файл уже содержит готовые -- GPU-команды, плеер только исполняет их в back buffer и выводит кадр по таймеру. -- -- Использование: ocv <папка> [-l] -- -l зациклить -- Управление: Space — пауза, Q / Ctrl+C — выход local component = require("component") local computer = require("computer") local event = require("event") local fs = require("filesystem") local shell = require("shell") local term = require("term") local gpu = component.gpu local byte, concat, floor = string.byte, table.concat, math.floor local uptime = computer.uptime local HEADER_SIZE = 64 local MAX_SKIP = 4 -- максимум подряд пропущенных выводов кадра при отставании local KEY_SPACE, KEY_Q = 32, 113 local OP_BG, OP_FG, OP_BOTH, OP_TEXT = 1, 2, 3, 4 -- ============================================================ утилиты local function u16(s, o) return byte(s, o) * 0x100 + byte(s, o + 1) end local function u32(s, o) return u16(s, o) * 0x10000 + u16(s, o + 2) end local function fail(msg) error(msg, 0) end -- ============================================================ палитра Tier 3 -- 0..15 — изменяемая палитра (берётся из index.ocv), 16..255 — куб 6x8x5 local function buildColorTable(header) local rgb = {} for i = 0, 15 do local o = 17 + i * 3 rgb[i] = byte(header, o) * 0x10000 + byte(header, o + 1) * 0x100 + byte(header, o + 2) end for i = 0, 239 do local r = floor(floor(i / 40) * 255 / 5 + 0.5) local g = floor(floor(i / 5) % 8 * 255 / 7 + 0.5) local b = floor(i % 5 * 255 / 4 + 0.5) rgb[16 + i] = r * 0x10000 + g * 0x100 + b end return rgb end -- ============================================================ символы -- GLYPHS[n][b]: строка из первых n кодов байта b (2 бита на ячейку, старшие первыми) local function buildGlyphTable() local chars = { [0] = " ", "▄", "▀", "█" } local t = {} for n = 1, 4 do t[n] = {} for b = 0, 255 do local s = {} for i = 1, n do s[i] = chars[floor(b / 4 ^ (4 - i)) % 4] end t[n][b] = concat(s) end end return t end -- ============================================================ видео local function readIndex(dir) local f, err = io.open(fs.concat(dir, "index.ocv"), "rb") if not f then fail("cannot open index.ocv: " .. tostring(err)) end local h = f:read(HEADER_SIZE) f:close() if not h or #h < HEADER_SIZE or h:sub(1, 4) ~= "OCV5" then fail("bad index.ocv (нужен OCV5, перекодируй через ocv_encode.py)") end local v = { dir = dir, width = u16(h, 5), height = u16(h, 7), fps = u16(h, 9) / 100, frames = u32(h, 11), parts = u16(h, 15), header = h, } if v.width < 1 or v.height < 1 or v.fps <= 0 or v.frames < 1 or v.parts < 1 then fail("bad index.ocv: пустое или повреждённое видео") end return v end -- последовательное чтение кадров через все part-файлы local function openStream(video) local part, file = 0, nil local stream = {} function stream.next() while true do if not file then part = part + 1 if part > video.parts then return nil end local path = fs.concat(video.dir, string.format("part%04d.ocv", part)) local err file, err = io.open(path, "rb") if not file then fail("cannot open " .. path .. ": " .. tostring(err)) end end local head = file:read(4) if head and #head == 4 then local len = u32(head, 1) if len == 0 then return "" end local data = file:read(len) if not data or #data < len then fail("truncated frame in part " .. part) end return data end file:close() file = nil end end function stream.close() if file then file:close(); file = nil end end return stream end -- ============================================================ GPU local Display = {} Display.__index = Display function Display.open(w, h, palette) if gpu.maxDepth() < 8 then fail("нужен GPU и экран Tier 3") end local mw, mh = gpu.maxResolution() if mw < w or mh < h then fail(string.format("экран %dx%d меньше видео %dx%d", mw, mh, w, h)) end local self = setmetatable({ w = w, h = h }, Display) self.saved = { depth = gpu.getDepth(), fg = gpu.getForeground(), bg = gpu.getBackground(), palette = {}, } self.saved.w, self.saved.h = gpu.getResolution() gpu.setDepth(8) gpu.setResolution(w, h) for i = 0, 15 do self.saved.palette[i] = gpu.getPaletteColor(i) end self:_applyPalette(palette) -- back buffer: рисование в него не тратит call budget, на экран — один bitblt if gpu.allocateBuffer then local ok, buf = pcall(gpu.allocateBuffer, w, h) if ok and buf then self.buffer = buf gpu.setActiveBuffer(buf) self:_applyPalette(palette) end end self:clear() return self end function Display:_applyPalette(palette) for i = 0, 15 do gpu.setPaletteColor(i, palette[i]) end end -- начальное состояние, которое предполагает энкодер: всё чёрное function Display:clear() gpu.setBackground(0x000000) gpu.fill(1, 1, self.w, self.h, " ") self:present() end function Display:present() if self.buffer then gpu.bitblt(0, 1, 1, self.w, self.h, self.buffer, 1, 1) end end function Display:close() local s = self.saved pcall(function() if self.buffer then gpu.setActiveBuffer(0) gpu.freeBuffer(self.buffer) end for i = 0, 15 do gpu.setPaletteColor(i, s.palette[i]) end gpu.setDepth(s.depth) gpu.setResolution(s.w, s.h) gpu.setForeground(s.fg) gpu.setBackground(s.bg) end) term.clear() end -- ============================================================ декодер кадра local function makeRenderer(colors, glyphs) local setBg, setFg, set = gpu.setBackground, gpu.setForeground, gpu.set local g4 = glyphs[4] local parts = {} return function(data) local p, n = 1, #data while p <= n do local op = byte(data, p) if op == OP_TEXT then local x, y, len = byte(data, p + 1, p + 3) local q = p + 4 if len <= 4 then set(x, y, glyphs[len][byte(data, q)]) p = q + 1 else local full = floor(len / 4) for i = 1, full do parts[i] = g4[byte(data, q + i - 1)] end local cnt, rest = full, len - full * 4 if rest > 0 then cnt = cnt + 1 parts[cnt] = glyphs[rest][byte(data, q + full)] end set(x, y, concat(parts, "", 1, cnt)) p = q + cnt end elseif op == OP_BOTH then setBg(colors[byte(data, p + 1)]) setFg(colors[byte(data, p + 2)]) p = p + 3 elseif op == OP_BG then setBg(colors[byte(data, p + 1)]) p = p + 2 elseif op == OP_FG then setFg(colors[byte(data, p + 1)]) p = p + 2 else error(string.format("corrupted frame: op %d at %d", op, p), 0) end end end end -- ============================================================ ввод local QUIT, PAUSE = "quit", "pause" local function poll(timeout) local name, _, char = event.pull(timeout) if name == "interrupted" then return QUIT end if name == "key_down" then if char == KEY_Q then return QUIT end if char == KEY_SPACE then return PAUSE end end end local function waitUntil(deadline) repeat local cmd = poll(math.max(0, deadline - uptime())) if cmd then return cmd end until uptime() >= deadline end -- ============================================================ воспроизведение local function play(video, display, render, stats) local stream = openStream(video) local interval = 1 / video.fps local t0 = uptime() local skipped = 0 local ok, result = pcall(function() for k = 0, video.frames - 1 do local data = stream.next() if not data then return end local c0 = os.clock() render(data) stats.cpu = stats.cpu + (os.clock() - c0) stats.drawn = stats.drawn + 1 local due = t0 + k * interval local now = uptime() local cmd if display.buffer and now > due + interval and skipped < MAX_SKIP and k < video.frames - 1 then -- отстаём: рисуем следующий кадр без вывода на экран skipped = skipped + 1 stats.late = stats.late + 1 cmd = poll(0) else cmd = waitUntil(due) display:present() skipped = 0 end if cmd == PAUSE then local p0 = uptime() repeat cmd = poll(math.huge) until cmd t0 = t0 + (uptime() - p0) end if cmd == QUIT then return QUIT end end end) stream.close() if not ok then error(result, 0) end return result end -- ============================================================ main local function main(...) local args, opts = shell.parse(...) local dir = args[1] if not dir then print("usage: ocv [-l]") print(" -l зациклить | Space — пауза, Q / Ctrl+C — выход") return end dir = shell.resolve(dir) if not fs.isDirectory(dir) then fail("папка не найдена: " .. dir) end local video = readIndex(dir) local colors = buildColorTable(video.header) local render = makeRenderer(colors, buildGlyphTable()) local display = Display.open(video.width, video.height, colors) local stats = { drawn = 0, late = 0, cpu = 0 } local started = uptime() local ok, err = pcall(function() repeat if play(video, display, render, stats) == QUIT then break end if opts.l then display:clear() end until not opts.l end) display:close() if not ok and err ~= "interrupted" then fail(err) end local elapsed = math.max(uptime() - started, 1e-6) print(string.format("%d кадров за %.1f с (%.1f fps), пропущено выводов: %d, CPU на кадр: %.1f мс", stats.drawn, elapsed, (stats.drawn - stats.late) / elapsed, stats.late, stats.cpu / math.max(stats.drawn, 1) * 1000)) if stats.late > stats.drawn / 10 then print("Не успевает: перекодируй с --max-sets 800 (или меньше) либо --fps 10") end end local ok, err = pcall(main, ...) if not ok then io.stderr:write("ocv: " .. tostring(err) .. "\n") end