-- player.lua -- Воспроизводит .ocv (True Color видео) на экране OpenComputers. -- -- Формат папки .ocv OCV3: -- index.ocv — метаданные видео -- part0001.ocv — кадры чанка 1 -- part0002.ocv — кадры чанка 2 -- ... -- -- Рендер: -- Каждый символ экрана кодирует пару вертикальных пикселей через полу-блок '▄' (U+2584). -- gpu.setBackground(RGB_верхнего_пикселя) -- верхняя половина ячейки -- gpu.setForeground(RGB_нижнего_пикселя) -- нижняя половина ячейки -- gpu.set(x, y, "▄") -- -- Подпиксель получается квадратным (1x1 графический пиксель), без искажений 1:2. -- -- Требования: -- - GPU Tier 3 (для передачи 24-bit RGB в setBackground/setForeground). -- На Tier 2 цвета ограничены палитрой (16/256 цветов), картинка будет в цвете, -- но с палитровыми артефактами. -- - Экран достаточного размера: минимум width x (height//2) символов. -- Для 80x50 -> 80x25 (Tier 2 monitor). -- Для 160x100 -> 160x50 (Tier 3 monitor). -- -- Использование: -- player video.ocv [delay_seconds] -- -- delay_seconds — задержка между кадрами. Если не указана, берётся 1/fps из заголовка. -- -- Память: -- Файл читается по одному кадру целиком, чтобы чтение с диска не смешивалось с отрисовкой. -- В ОЗУ хранится только текущий кадр. local component = require("component") local term = require("term") local event = require("event") local fs = require("filesystem") local computer = require("computer") local gpu = component.gpu -- ---------- аргументы ---------- local args = {...} local path = args[1] local delay = tonumber(args[2]) -- секунды; nil -> взять из заголовка local mode = args[3] local BW_MODE = mode == "bw" or mode == "mono" or mode == "1bit" if not path then io.stderr:write("usage: player [delay_seconds] [bw]\n") return end if not fs.exists(path) then io.stderr:write("file not found: " .. path .. "\n") return end local indexPath = fs.concat(path, "index.ocv") if not fs.exists(indexPath) then io.stderr:write("index not found: " .. indexPath .. "\n") return end -- ---------- открываем индекс ---------- local f, err = io.open(indexPath, "rb") if not f then io.stderr:write("cannot open: " .. tostring(err) .. "\n") return end -- ---------- читаем 32-байт заголовок ---------- local header = f:read(32) if not header or #header < 32 then f:close() io.stderr:write("bad header: file too short\n") return end local MAGIC = header:sub(1, 4) if MAGIC ~= "OCV3" and MAGIC ~= "OCV4" then f:close() io.stderr:write("bad header: expected OCV3/OCV4. Reconvert video with video_to_lua.py\n") return end -- Парсим big-endian uint32 local function u32be(s, off) return s:byte(off) * 0x1000000 + s:byte(off+1) * 0x10000 + s:byte(off+2) * 0x100 + s:byte(off+3) end local SCREEN_H = u32be(header, 5) local SCREEN_W = u32be(header, 9) local FPS = u32be(header, 13) local FRAME_SIZE = u32be(header, 17) local FRAME_COUNT = u32be(header, 21) local CHUNK_COUNT = u32be(header, 25) local FRAMES_PER_CHUNK = u32be(header, 29) f:close() f = nil if SCREEN_H <= 0 or SCREEN_W <= 0 or FRAME_SIZE <= 0 or FRAME_COUNT <= 0 or CHUNK_COUNT <= 0 or FRAMES_PER_CHUNK <= 0 then io.stderr:write("bad header: invalid dimensions\n") return end if FRAME_SIZE ~= SCREEN_H * SCREEN_W * 6 then if FRAME_SIZE ~= SCREEN_H * SCREEN_W * 2 then io.stderr:write("bad header: invalid frame size\n") return end end local COMPACT_RGB332 = FRAME_SIZE == SCREEN_H * SCREEN_W * 2 local DELTA_CODEC = MAGIC == "OCV4" local BYTES_PER_CELL = COMPACT_RGB332 and 2 or 6 local CELL_COUNT = SCREEN_H * SCREEN_W local function chunkPath(index) return fs.concat(path, string.format("part%04d.ocv", index)) end local function loadChunk(index) local p = chunkPath(index) local cf, cerr = io.open(p, "rb") if not cf then return nil, "cannot open chunk: " .. p .. " (" .. tostring(cerr) .. ")" end local data = cf:read("*a") or "" cf:close() return data, nil end local DELTA_OP_BUDGET = 128 local function decodeDeltaFrame(data, offset, target, state) local payloadLen, pos, payloadEnd, cell, parts, partCount if state then payloadLen = state.payloadLen pos = state.pos payloadEnd = state.payloadEnd cell = state.cell parts = state.parts partCount = state.partCount target = state.target else payloadLen = u32be(data, offset) pos = offset + 4 payloadEnd = pos + payloadLen - 1 cell = 0 parts = {} partCount = 0 end local ops = 0 while pos <= payloadEnd and cell < CELL_COUNT do local tag = string.byte(data, pos) local count = string.byte(data, pos + 1) * 0x100 + string.byte(data, pos + 2) pos = pos + 3 if tag == 0 then if not target then return nil, offset + 4 + payloadLen, "bad delta frame" end local bytes = count * BYTES_PER_CELL local start = cell * BYTES_PER_CELL + 1 partCount = partCount + 1 parts[partCount] = target:sub(start, start + bytes - 1) cell = cell + count else local bytes = count * BYTES_PER_CELL partCount = partCount + 1 parts[partCount] = data:sub(pos, pos + bytes - 1) pos = pos + bytes cell = cell + count end ops = ops + 1 if pos <= payloadEnd and cell < CELL_COUNT and ops >= DELTA_OP_BUDGET then return nil, nil, nil, { payloadLen = payloadLen, pos = pos, payloadEnd = payloadEnd, cell = cell, parts = parts, partCount = partCount, target = target } end end if cell ~= CELL_COUNT then return nil, offset + 4 + payloadLen, "bad delta frame" end return table.concat(parts), offset + 4 + payloadLen, nil end if delay == nil then delay = 1 / math.max(FPS, 1) end -- ---------- проверка экрана ---------- local sw, sh = gpu.getResolution() if sw < SCREEN_W or sh < SCREEN_H then io.stderr:write(string.format( "WARNING: screen %dx%d < required %dx%d. Install bigger monitor or lower resolution.\n", sw, sh, SCREEN_W, SCREEN_H)) end -- Сохраняем исходное разрешение, чтобы вернуть после просмотра local old_w, old_h = sw, sh -- Устанавливаем разрешение под видео. -- Tier 3 поддерживает до 160x50. Tier 2 — до 80x25. gpu.setResolution(SCREEN_W, SCREEN_H) term.clear() local backBuffer = nil if gpu.allocateBuffer and gpu.setActiveBuffer and gpu.bitblt then local ok_buf, buf = pcall(gpu.allocateBuffer, SCREEN_W, SCREEN_H) if ok_buf and buf then backBuffer = buf gpu.setActiveBuffer(backBuffer) end end local prevCells = {} local YIELD_ROWS = 8 local gpuSetBackground = gpu.setBackground local gpuSetForeground = gpu.setForeground local gpuSet = gpu.set local dataByte = string.byte local pullSignal = computer.pullSignal local blockCache = {} local runStringCache = {} local function rgb332(byte) local r = math.floor(byte / 32) local g = math.floor(byte / 4) % 8 local b = byte % 4 r = r * 255 / 7 g = g * 255 / 7 b = b * 255 / 3 return math.floor(r + 0.5) * 0x10000 + math.floor(g + 0.5) * 0x100 + math.floor(b + 0.5) end local rgb332Cache = {} for i = 0, 255 do rgb332Cache[i] = rgb332(i) end local bw332Cache = {} for i = 0, 255 do local r = math.floor(i / 32) local g = math.floor(i / 4) % 8 local b = i % 4 bw332Cache[i] = (r * 1092 + g * 1092 + b * 2550 >= 11456) and 0xFFFFFF or 0 end local bw332BitCache = {} for i = 0, 255 do local r = math.floor(i / 32) local g = math.floor(i / 4) % 8 local b = i % 4 bw332BitCache[i] = (r * 1092 + g * 1092 + b * 2550 >= 11456) and 1 or 0 end local function blockString(len) local s = blockCache[len] if not s then s = string.rep("▄", len) blockCache[len] = s end return s end local function runString(ch, len) local cache = runStringCache[ch] if not cache then cache = {} runStringCache[ch] = cache end local s = cache[len] if not s then s = string.rep(ch, len) cache[len] = s end return s end local function drawBWRun(state, x, row, len) if state == 0 then gpuSetBackground(0) gpuSet(x, row, runString(" ", len)) elseif state == 3 then gpuSetBackground(0xFFFFFF) gpuSet(x, row, runString(" ", len)) elseif state == 1 then gpuSetBackground(0) gpuSetForeground(0xFFFFFF) gpuSet(x, row, blockString(len)) else gpuSetBackground(0) gpuSetForeground(0xFFFFFF) gpuSet(x, row, runString("▀", len)) end end local function renderFrameBW(data, frameOffset) if backBuffer then for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W local runX = nil local runLen = 0 local runState = nil for x = 1, SCREEN_W do local cell = cellBase + x local state if COMPACT_RGB332 then local off = frameOffset + (cell - 1) * 2 + 1 local b1, b2 = dataByte(data, off, off + 1) state = bw332BitCache[b1] * 2 + bw332BitCache[b2] else local off = frameOffset + (cell - 1) * 6 + 1 local r1, g1, b1, r2, g2, b2 = dataByte(data, off, off + 5) local top = (r1 * 30 + g1 * 59 + b1 * 11 >= 12800) and 1 or 0 local bottom = (r2 * 30 + g2 * 59 + b2 * 11 >= 12800) and 1 or 0 state = top * 2 + bottom end if prevCells[cell] ~= state then prevCells[cell] = state if runLen > 0 and (runState ~= state or runX + runLen ~= x) then drawBWRun(runState, runX, row, runLen) runX = nil runLen = 0 end if runLen == 0 then runX = x runLen = 1 runState = state else runLen = runLen + 1 end end end if runLen > 0 then drawBWRun(runState, runX, row, runLen) end if row % YIELD_ROWS == 0 then pullSignal(0) end end gpu.bitblt(0, 1, 1, SCREEN_W, SCREEN_H, backBuffer, 1, 1) else for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W local runX = nil local runLen = 0 local runState = nil for x = 1, SCREEN_W do local cell = cellBase + x local state if COMPACT_RGB332 then local off = frameOffset + (cell - 1) * 2 + 1 local b1, b2 = dataByte(data, off, off + 1) state = bw332BitCache[b1] * 2 + bw332BitCache[b2] else local off = frameOffset + (cell - 1) * 6 + 1 local r1, g1, b1, r2, g2, b2 = dataByte(data, off, off + 5) local top = (r1 * 30 + g1 * 59 + b1 * 11 >= 12800) and 1 or 0 local bottom = (r2 * 30 + g2 * 59 + b2 * 11 >= 12800) and 1 or 0 state = top * 2 + bottom end if prevCells[cell] ~= state then prevCells[cell] = state if runLen > 0 and (runState ~= state or runX + runLen ~= x) then drawBWRun(runState, runX, row, runLen) runX = nil runLen = 0 end if runLen == 0 then runX = x runLen = 1 runState = state else runLen = runLen + 1 end end end if runLen > 0 then drawBWRun(runState, runX, row, runLen) end if row % YIELD_ROWS == 0 then pullSignal(0) end end end return true end -- ---------- вспомогательная функция рендера кадра ---------- local function renderFrame(data, frameOffset) local lastBg = nil local lastFg = nil if backBuffer then for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W local runX = nil local runLen = 0 local runBg = nil local runFg = nil for x = 1, SCREEN_W do local cell = cellBase + x local c1, c2 if COMPACT_RGB332 then local off = frameOffset + (cell - 1) * 2 + 1 local b1, b2 = dataByte(data, off, off + 1) if BW_MODE then c1 = bw332Cache[b1] c2 = bw332Cache[b2] else c1 = rgb332Cache[b1] c2 = rgb332Cache[b2] end else local off = frameOffset + (cell - 1) * 6 + 1 local r1, g1, b1, r2, g2, b2 = dataByte(data, off, off + 5) if BW_MODE then c1 = (r1 * 30 + g1 * 59 + b1 * 11 >= 12800) and 0xFFFFFF or 0 c2 = (r2 * 30 + g2 * 59 + b2 * 11 >= 12800) and 0xFFFFFF or 0 else c1 = r1 * 0x10000 + g1 * 0x100 + b1 c2 = r2 * 0x10000 + g2 * 0x100 + b2 end end local packed = c1 * 0x1000000 + c2 if prevCells[cell] ~= packed then prevCells[cell] = packed if runLen > 0 and (runBg ~= c1 or runFg ~= c2 or runX + runLen ~= x) then if runBg ~= lastBg then gpuSetBackground(runBg) lastBg = runBg end if runFg ~= lastFg then gpuSetForeground(runFg) lastFg = runFg end gpuSet(runX, row, blockString(runLen)) runX = nil runLen = 0 end if runLen == 0 then runX = x runLen = 1 runBg = c1 runFg = c2 else runLen = runLen + 1 end end end if runLen > 0 then if runBg ~= lastBg then gpuSetBackground(runBg) lastBg = runBg end if runFg ~= lastFg then gpuSetForeground(runFg) lastFg = runFg end gpuSet(runX, row, blockString(runLen)) end if row % YIELD_ROWS == 0 then pullSignal(0) end end gpu.bitblt(0, 1, 1, SCREEN_W, SCREEN_H, backBuffer, 1, 1) else for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W local runX = nil local runLen = 0 local runBg = nil local runFg = nil for x = 1, SCREEN_W do local cell = cellBase + x local c1, c2 if COMPACT_RGB332 then local off = frameOffset + (cell - 1) * 2 + 1 local b1, b2 = dataByte(data, off, off + 1) if BW_MODE then c1 = bw332Cache[b1] c2 = bw332Cache[b2] else c1 = rgb332Cache[b1] c2 = rgb332Cache[b2] end else local off = frameOffset + (cell - 1) * 6 + 1 local r1, g1, b1, r2, g2, b2 = dataByte(data, off, off + 5) if BW_MODE then c1 = (r1 * 30 + g1 * 59 + b1 * 11 >= 12800) and 0xFFFFFF or 0 c2 = (r2 * 30 + g2 * 59 + b2 * 11 >= 12800) and 0xFFFFFF or 0 else c1 = r1 * 0x10000 + g1 * 0x100 + b1 c2 = r2 * 0x10000 + g2 * 0x100 + b2 end end local packed = c1 * 0x1000000 + c2 if prevCells[cell] ~= packed then prevCells[cell] = packed if runLen > 0 and (runBg ~= c1 or runFg ~= c2 or runX + runLen ~= x) then if runBg ~= lastBg then gpuSetBackground(runBg) lastBg = runBg end if runFg ~= lastFg then gpuSetForeground(runFg) lastFg = runFg end gpuSet(runX, row, blockString(runLen)) runX = nil runLen = 0 end if runLen == 0 then runX = x runLen = 1 runBg = c1 runFg = c2 else runLen = runLen + 1 end end end if runLen > 0 then if runBg ~= lastBg then gpuSetBackground(runBg) lastBg = runBg end if runFg ~= lastFg then gpuSetForeground(runFg) lastFg = runFg end gpuSet(runX, row, blockString(runLen)) end if row % YIELD_ROWS == 0 then pullSignal(0) end end end return true end -- ---------- главный цикл ---------- local n = 0 local rendered = 0 local chunkIndex = 0 local chunkData = nil local chunkStartFrame = 0 local chunkFrameCount = 0 local chunkFrameOffsets = nil local decodedFrame = nil local decodedFrameIndex = -1 local deltaDecodeState = nil local t0 = computer.uptime() local nextFrame = t0 local render = BW_MODE and renderFrameBW or renderFrame local DELTA_DECODE_BUDGET = 4 local function ensureChunk(frameIndex) if chunkData and frameIndex >= chunkStartFrame and frameIndex < chunkStartFrame + chunkFrameCount then return true end local targetChunk = math.floor(frameIndex / FRAMES_PER_CHUNK) + 1 if targetChunk > CHUNK_COUNT then return false end local data, loadErr = loadChunk(targetChunk) if not data then io.stderr:write(loadErr .. "\n") return false end chunkIndex = targetChunk chunkStartFrame = (chunkIndex - 1) * FRAMES_PER_CHUNK chunkData = data chunkFrameOffsets = nil decodedFrame = nil decodedFrameIndex = chunkStartFrame - 1 deltaDecodeState = nil if DELTA_CODEC then chunkFrameOffsets = {} local pos = 1 while pos + 3 <= #chunkData and #chunkFrameOffsets < FRAMES_PER_CHUNK do local payloadLen = u32be(chunkData, pos) if pos + 3 + payloadLen > #chunkData then break end chunkFrameOffsets[#chunkFrameOffsets + 1] = pos pos = pos + 4 + payloadLen end chunkFrameCount = #chunkFrameOffsets else chunkFrameCount = math.floor(#chunkData / FRAME_SIZE) end if chunkFrameCount <= 0 then io.stderr:write("bad chunk: no complete frames in " .. chunkPath(chunkIndex) .. "\n") return false end return true end local function getFrame(frameIndex) if not ensureChunk(frameIndex) then return nil, nil end if not DELTA_CODEC then return chunkData, (frameIndex - chunkStartFrame) * FRAME_SIZE end local localIndex = frameIndex - chunkStartFrame + 1 if localIndex < 1 or localIndex > chunkFrameCount then return nil, nil end local decodedThisCall = 0 while decodedFrameIndex < frameIndex do local nextIndex = decodedFrameIndex + 1 local nextLocalIndex = nextIndex - chunkStartFrame + 1 local offset = chunkFrameOffsets[nextLocalIndex] local frame, _, err, state = decodeDeltaFrame(chunkData, offset, decodedFrame, deltaDecodeState) deltaDecodeState = state if state then return nil, nil, true end if not frame then io.stderr:write(tostring(err) .. "\n") return nil, nil end decodedFrame = frame decodedFrameIndex = nextIndex deltaDecodeState = nil decodedThisCall = decodedThisCall + 1 if decodedFrameIndex < frameIndex and decodedThisCall >= DELTA_DECODE_BUDGET then return nil, nil, true end end return decodedFrame, 0 end while true do if n >= FRAME_COUNT then break end local frameData, frameOffset, pending = getFrame(n) if not frameData then if pending then event.pull(0) else break end else render(frameData, frameOffset) n = n + 1 rendered = rendered + 1 nextFrame = nextFrame + delay local now = computer.uptime() while now > nextFrame + delay and n < FRAME_COUNT do n = n + 1 nextFrame = nextFrame + delay end local wait = nextFrame - now if wait < 0 then wait = 0 end local ev = event.pull(wait, "interrupted") if ev == "interrupted" then break end end end local dt = computer.uptime() - t0 -- ---------- cleanup ---------- if backBuffer then pcall(gpu.setActiveBuffer, 0) if gpu.freeBuffer then pcall(gpu.freeBuffer, backBuffer) end end -- Возвращаем исходное разрешение и чистим экран local ok_res, err_res = pcall(function() gpu.setResolution(old_w, old_h) end) if not ok_res then -- Если old_w/old_h оказались недоступны для текущего GPU, игнорируем end term.clear() print(string.format("Played %d frames in %.2fs (%.1f fps)", rendered, dt, rendered / math.max(dt, 1e-6)))