-- 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") -- доступ к компонентам OpenComputers 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] -- путь к папке с index.ocv и partXXXX.ocv local delay = tonumber(args[2]) -- задержка между кадрами; nil -> взять из FPS заголовка local mode = args[3] -- режим вывода, например bw/mono/1bit local BW_MODE = mode == "bw" or mode == "mono" or mode == "1bit" -- true: рисовать чёрно-белую картинку 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") -- f: handle индексного файла; err: ошибка открытия if not f then io.stderr:write("cannot open: " .. tostring(err) .. "\n") return end -- ---------- читаем 32-байт заголовок ---------- local header = f:read(32) -- бинарный заголовок index.ocv с метаданными видео 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) -- сигнатура формата: OCV3 или OCV4 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) -- FPS из файла local FRAME_SIZE = u32be(header, 17) -- размер одного полного кадра в байтах local FRAME_COUNT = u32be(header, 21) -- общее количество кадров local CHUNK_COUNT = u32be(header, 25) -- количество partXXXX.ocv файлов 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 -- true: цвет хранится компактно по 1 байту на подпиксель local DELTA_CODEC = MAGIC == "OCV4" -- true: кадры хранятся как delta от предыдущего 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) -- путь к нужному partXXXX.ocv local cf, cerr = io.open(p, "rb") -- cf: handle чанка; cerr: ошибка открытия 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 -- максимум delta-run операций за один заход декодера до yield local deltaPartsBuf = {} -- переиспользуемый буфер кусков для сборки delta-кадра; -- живёт на весь ролик, чтобы не плодить мусор на каждый кадр для GC local function decodeDeltaFrame(data, offset, target, state, ctxChunk, ctxFrame) local payloadLen, pos, payloadEnd, cell, parts, partCount -- состояние сборки одного delta-кадра if state then payloadLen = state.payloadLen -- длина payload текущего delta-кадра pos = state.pos -- текущая позиция чтения внутри payload payloadEnd = state.payloadEnd -- последний байт payload cell = state.cell -- номер текущей экранной ячейки parts = deltaPartsBuf -- общий буфер кусков (тот же, что и при первом заходе на этот кадр) partCount = state.partCount -- количество элементов в parts target = state.target -- предыдущий полный кадр, откуда копируются неизменённые области else payloadLen = u32be(data, offset) -- длина delta-payload после 4-байтного префикса pos = offset + 4 -- начало payload после длины payloadEnd = pos + payloadLen - 1 -- конец payload cell = 0 -- сколько ячеек кадра уже восстановлено parts = deltaPartsBuf -- переиспользуем буфер вместо {} — старый хвост игнорируется по partCount partCount = 0 -- текущий индекс записи в parts end local ops = 0 -- сколько delta-run операций обработано за этот вызов while pos <= payloadEnd and cell < CELL_COUNT do local tag = string.byte(data, pos) -- тип run: 0 копировать из прошлого кадра, иначе читать новые байты local count = string.byte(data, pos + 1) * 0x100 + string.byte(data, pos + 2) -- длина run в ячейках pos = pos + 3 if tag == 0 then if not target then return nil, offset + 4 + payloadLen, string.format( "bad delta frame: chunk %s frame %s references previous frame, but none decoded yet (chunk must start with a keyframe)", tostring(ctxChunk), tostring(ctxFrame)) 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 -- сколько новых байт прочитать из delta-payload 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, partCount = partCount, target = target } end end if cell ~= CELL_COUNT then return nil, offset + 4 + payloadLen, string.format( "bad delta frame: chunk %s frame %s decoded %d/%d cells (payload ended early or is corrupt)", tostring(ctxChunk), tostring(ctxFrame), cell, CELL_COUNT) end -- table.concat с явным диапазоном 1..partCount: не задеваем "хвост" буфера, -- оставшийся от кадра большей сложности, декодированного ранее. return table.concat(parts, "", 1, partCount), 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. local ok_setres, err_setres = pcall(gpu.setResolution, SCREEN_W, SCREEN_H) -- ok_setres: удалось ли применить разрешение видео if not ok_setres then io.stderr:write(string.format( "cannot set resolution %dx%d: %s\n", SCREEN_W, SCREEN_H, tostring(err_setres))) return end term.clear() local backBuffer = nil -- id GPU-буфера для двойной буферизации, если доступен if gpu.allocateBuffer and gpu.setActiveBuffer and gpu.bitblt then local ok_buf, buf = pcall(gpu.allocateBuffer, SCREEN_W, SCREEN_H) -- ok_buf: успех выделения; buf: id буфера if ok_buf and buf then backBuffer = buf gpu.setActiveBuffer(backBuffer) end end -- Веса яркости (приближение ITU-R BT.601: Y = 0.30R + 0.59G + 0.11B). -- Используются в двух шкалах: LUMA_* для 8-бит RGB888 (порог из 255*100), -- LUMA332_* для RGB332-компонент (порог заранее умножен на диапазон компоненты). local LUMA_R, LUMA_G, LUMA_B = 30, 59, 11 local LUMA_THRESHOLD = 12800 -- порог "светлый/тёмный" для суммы r*30+g*59+b*11 (RGB888) local LUMA332_R, LUMA332_G, LUMA332_B = 1092, 1092, 2550 -- те же веса, перенормированные под шкалы RGB332 (3/3/2 бита) local LUMA332_THRESHOLD = 11456 -- порог "светлый/тёмный" для RGB332-компонент local prevCells = {} -- состояние ячеек прошлого отрисованного кадра для пропуска неизменённых мест local YIELD_ROWS = 8 -- как часто отдавать управление при рендере строк local gpuSetBackground = gpu.setBackground -- локальная ссылка на gpu.setBackground для скорости local gpuSetForeground = gpu.setForeground -- локальная ссылка на gpu.setForeground для скорости local gpuSet = gpu.set -- локальная ссылка на gpu.set для скорости local dataByte = string.byte -- локальная ссылка на string.byte для скорости local pullSignal = computer.pullSignal -- локальная ссылка на computer.pullSignal для yield local blockCache = {} -- кэш строк из символа "▄" разной длины local runStringCache = {} -- кэш строк из пробелов/верхних блоков разной длины local function rgb332(byte) local r = math.floor(byte / 32) -- красная компонента RGB332: 3 бита local g = math.floor(byte / 4) % 8 -- зелёная компонента RGB332: 3 бита local b = byte % 4 -- синяя компонента RGB332: 2 бита 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 = {} -- таблица перевода RGB332 байта в 24-bit RGB for i = 0, 255 do rgb332Cache[i] = rgb332(i) end local bw332BitCache = {} -- таблица перевода RGB332 байта в бит яркости 0/1 for i = 0, 255 do local r = math.floor(i / 32) -- красная компонента RGB332 local g = math.floor(i / 4) % 8 -- зелёная компонента RGB332 local b = i % 4 -- синяя компонента RGB332 bw332BitCache[i] = (r * LUMA332_R + g * LUMA332_G + b * LUMA332_B >= LUMA332_THRESHOLD) and 1 or 0 end -- Pair-кэши для RGB332 (256x256 = 65536 комбинаций верх+низ, это дёшево по памяти): -- на "горячем" пути видео большинство ячеек кадр-к-кадру не меняются, и сейчас -- для сравнения с prevCells приходится делать 2 lookup'а (rgb332Cache[b1], rgb332Cache[b2]) -- и умножение/сложение, чтобы собрать packed-значение. С pair-кэшем сравнение — -- один lookup по объединённому индексу b1*256+b2; сами c1/c2 достаются из -- rgb332Cache только для реально изменившихся ячеек (меньшинство кадра). local pairPacked332 = {} -- b1*256+b2 -> packed 48-bit (c1<<24 | c2), для быстрого diff с prevCells local pairState332 = {} -- b1*256+b2 -> 2-битное ч/б состояние (для renderFrameBW_332) for b1 = 0, 255 do local base = b1 * 256 local c1 = rgb332Cache[b1] local bit1 = bw332BitCache[b1] for b2 = 0, 255 do pairPacked332[base + b2] = c1 * 0x1000000 + rgb332Cache[b2] pairState332[base + b2] = bit1 * 2 + bw332BitCache[b2] end 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] -- готовая строка ch нужной длины из кэша 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_332(data, frameOffset) for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W -- индекс первой ячейки текущей строки минус 1 local runX = nil -- X начала текущего непрерывного участка local runLen = 0 -- длина текущего непрерывного участка local runState = nil -- чёрно-белое состояние текущего участка for x = 1, SCREEN_W do local cell = cellBase + x -- линейный номер ячейки кадра local off = frameOffset + (cell - 1) * 2 + 1 -- позиция двух RGB332 байт ячейки local b1, b2 = dataByte(data, off, off + 1) -- b1: верхний подпиксель; b2: нижний подпиксель local state = pairState332[b1 * 256 + b2] -- один lookup вместо двух + арифметики 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 if backBuffer then gpu.bitblt(0, 1, 1, SCREEN_W, SCREEN_H, backBuffer, 1, 1) end return true end local function renderFrameBW_888(data, frameOffset) for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W -- индекс первой ячейки текущей строки минус 1 local runX = nil -- X начала текущего непрерывного участка local runLen = 0 -- длина текущего непрерывного участка local runState = nil -- чёрно-белое состояние текущего участка for x = 1, SCREEN_W do local cell = cellBase + x -- линейный номер ячейки кадра local off = frameOffset + (cell - 1) * 6 + 1 -- позиция шести RGB888 байт ячейки local r1, g1, b1, r2, g2, b2 = dataByte(data, off, off + 5) -- RGB верхнего и нижнего подпикселей local top = (r1 * LUMA_R + g1 * LUMA_G + b1 * LUMA_B >= LUMA_THRESHOLD) and 1 or 0 -- яркость верхнего подпикселя local bottom = (r2 * LUMA_R + g2 * LUMA_G + b2 * LUMA_B >= LUMA_THRESHOLD) and 1 or 0 -- яркость нижнего подпикселя local state = top * 2 + bottom 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 if backBuffer then gpu.bitblt(0, 1, 1, SCREEN_W, SCREEN_H, backBuffer, 1, 1) end return true end -- Эти функции вызываются только когда BW_MODE == false (см. выбор `render` ниже) — -- чёрно-белый путь целиком в renderFrameBW_332/renderFrameBW_888. local function renderFrame_332(data, frameOffset) local lastBg = nil -- последний установленный цвет фона GPU local lastFg = nil -- последний установленный цвет текста GPU for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W -- индекс первой ячейки текущей строки минус 1 local runX = nil -- X начала текущего цветового участка local runLen = 0 -- длина текущего цветового участка local runBg = nil -- фон текущего участка: цвет верхнего подпикселя local runFg = nil -- цвет текста текущего участка: цвет нижнего подпикселя for x = 1, SCREEN_W do local cell = cellBase + x -- линейный номер ячейки кадра local off = frameOffset + (cell - 1) * 2 + 1 -- позиция двух RGB332 байт ячейки local b1, b2 = dataByte(data, off, off + 1) -- b1: верхний подпиксель; b2: нижний подпиксель local packed = pairPacked332[b1 * 256 + b2] -- один lookup вместо двух + умножения if prevCells[cell] ~= packed then prevCells[cell] = packed local c1 = rgb332Cache[b1] -- реальный RGB нужен только для изменившихся ячеек local c2 = rgb332Cache[b2] 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 if backBuffer then gpu.bitblt(0, 1, 1, SCREEN_W, SCREEN_H, backBuffer, 1, 1) end return true end local function renderFrame_888(data, frameOffset) local lastBg = nil -- последний установленный цвет фона GPU local lastFg = nil -- последний установленный цвет текста GPU for row = 1, SCREEN_H do local cellBase = (row - 1) * SCREEN_W -- индекс первой ячейки текущей строки минус 1 local runX = nil -- X начала текущего цветового участка local runLen = 0 -- длина текущего цветового участка local runBg = nil -- фон текущего участка: цвет верхнего подпикселя local runFg = nil -- цвет текста текущего участка: цвет нижнего подпикселя for x = 1, SCREEN_W do local cell = cellBase + x -- линейный номер ячейки кадра local off = frameOffset + (cell - 1) * 6 + 1 -- позиция шести RGB888 байт ячейки local r1, g1, b1, r2, g2, b2 = dataByte(data, off, off + 5) -- RGB верхнего и нижнего подпикселей local c1 = r1 * 0x10000 + g1 * 0x100 + b1 local c2 = r2 * 0x10000 + g2 * 0x100 + b2 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 if backBuffer then gpu.bitblt(0, 1, 1, SCREEN_W, SCREEN_H, backBuffer, 1, 1) end return true end -- ---------- главный цикл ---------- local n = 0 -- индекс следующего кадра, который нужно получить/отрисовать local rendered = 0 -- сколько кадров реально было отрисовано на экран local chunkIndex = 0 -- номер текущего загруженного чанка partXXXX.ocv local chunkData = nil -- бинарные данные текущего чанка local chunkStartFrame = 0 -- глобальный индекс первого кадра в текущем чанке local chunkFrameCount = 0 -- количество кадров, найденных в текущем чанке local chunkFrameOffsets = nil -- для OCV4: offsets каждого delta-кадра внутри chunkData local decodedFrame = nil -- последний полностью восстановленный OCV4 кадр local decodedFrameIndex = -1 -- глобальный индекс decodedFrame local deltaDecodeState = nil -- состояние незавершённого пошагового delta-декодирования local t0 = computer.uptime() -- время старта воспроизведения local nextFrame = t0 -- запланированное время следующего кадра local render -- выбранная функция рендера: комбинация ч/б-цвет x формат кадра, без веток внутри цикла if BW_MODE then render = COMPACT_RGB332 and renderFrameBW_332 or renderFrameBW_888 else render = COMPACT_RGB332 and renderFrame_332 or renderFrame_888 end local DELTA_DECODE_BUDGET = 4 -- максимум готовых delta-кадров за один заход getFrame 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 -- номер чанка, где должен лежать frameIndex if targetChunk > CHUNK_COUNT then return false end local data, loadErr = loadChunk(targetChunk) -- data: содержимое чанка; loadErr: текст ошибки загрузки 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 -- текущая позиция сканирования delta-кадров внутри чанка while pos + 3 <= #chunkData and #chunkFrameOffsets < FRAMES_PER_CHUNK do local payloadLen = u32be(chunkData, pos) -- длина очередного delta-payload 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 -- индекс кадра внутри текущего чанка, начиная с 1 if localIndex < 1 or localIndex > chunkFrameCount then return nil, nil end local decodedThisCall = 0 -- сколько полных delta-кадров восстановлено за этот вызов getFrame while decodedFrameIndex < frameIndex do local nextIndex = decodedFrameIndex + 1 -- следующий глобальный кадр, который надо восстановить local nextLocalIndex = nextIndex - chunkStartFrame + 1 -- индекс этого кадра внутри чанка local offset = chunkFrameOffsets[nextLocalIndex] -- байтовая позиция delta-кадра в chunkData local frame, _, err, state = decodeDeltaFrame(chunkData, offset, decodedFrame, deltaDecodeState, chunkIndex, nextIndex) -- frame: готовый кадр; state: незавершённый декодер 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) -- frameData: данные кадра; frameOffset: смещение; pending: декодер ещё не закончил 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() -- текущее время для расчёта опоздания и ожидания -- Для OCV4 (DELTA_CODEC) пропуск кадра здесь экономит только рендер, но не декодирование: -- getFrame() всё равно последовательно восстановит все промежуточные delta-кадры, -- чтобы не сломать цепочку target. Реальной экономии CPU при отставании на delta-видео нет. 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() -- ok_res: успех возврата разрешения; err_res: ошибка pcall 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)))