Files
computer_craft_video_play/play.lua
2026-01-10 02:18:57 +08:00

467 lines
13 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 简化版视频播放器
local gpu = peripheral.wrap("tm_gpu_2")
gpu.refreshSize()
gpu.setSize(64)
local w, h = gpu.getSize()
server_url = "https://newgmapi.liulikeji.cn"
-- 检查命令行参数
local videoUrl = ...
if not videoUrl then
print("Usage: video_player <video URL>")
print("Example: video_player https://example.com/video.mp4")
return
end
-- 假设 videoUrl, w, h 已定义
local task_id
local status_url
if videoUrl:sub(1,4) ~= "http" then
-- 如果不是 URL则直接当作 task_id 使用(兼容模式?)
task_id = videoUrl
status_url = server_url .. "/api/task/" .. task_id
else
-- 创建新任务
print("Submitting video frame extraction task...")
local requestData = {
video_url = videoUrl,
w = w,
h = h,
force_resolution = false,
pad_to_target = true,
fps = 20
}
local response = http.post(
server_url .. "/api/video_frame/async",
textutils.serializeJSON(requestData),
{["Content-Type"] = "application/json"}
)
if not response then
error("Failed to connect to API server")
end
local respBody = response.readAll()
response.close()
local createResult = textutils.unserialiseJSON(respBody)
if not createResult or createResult.status ~= "success" then
error("Task creation failed: " .. (createResult and createResult.message or "Unknown error"))
end
task_id = createResult.task_id
status_url = createResult.status_url
end
term.clear()
term.setCursorPos(1,1)
print("task_id: " .. task_id)
-- 用于记录已打印的日志数量,避免重复打印
local total_logs_printed = 0
while true do
local response = http.get(status_url, {["Content-Type"] = "application/json"})
if not response then
error("Failed to fetch task status")
end
local respBody = response.readAll()
response.close()
local task_info = textutils.unserialiseJSON(respBody)
term.clear()
term.setCursorPos(1,1)
-- 打印基本信息
print("task_id: " .. task_id)
print("Status: " .. (task_info.status or "unknown"))
print("Progress: " .. (task_info.progress or 0) .. "%")
if task_info.message then
print("Message: " .. task_info.message)
end
-- 打印新增日志new_logs 是数组)
if task_info.new_logs and #task_info.new_logs > 0 then
for i = 1, #task_info.new_logs do
print(task_info.new_logs[i])
end
total_logs_printed = total_logs_printed + #task_info.new_logs
end
-- 检查是否完成
if task_info.status == "completed" then
break
end
sleep(1)
end
-- 获取最终结果
local response = http.get(status_url, {["Content-Type"] = "application/json"})
local finalResp = textutils.unserialiseJSON(response.readAll())
response.close()
-- 注意:最终结果在 result.result 中
videoInfo = finalResp.result.result
-- 此时 videoInfo 包含 frame_urls, audio_dfpwm_url 等字段
-- 可根据需要进一步处理
print("\nTask completed! Total frames: " .. #videoInfo.frame_urls)
-- 播放音频 单通道
--shell.run("bg speaker play", server_url .. videoInfo.audio_dfpwm_url)
-- server_url .. videoInfo.audio_dfpwm_url -- 音频单通道文件 URL
-- server_url .. videoInfo.audio_dfpwm_right_url -- 音频右通道文件 URL
-- server_url .. videoInfo.audio_dfpwm_left_url -- 音频左通道文件 URL
shell.run("bg speakerlib play ".. server_url .. videoInfo.audio_dfpwm_url .." ".. server_url .. videoInfo.audio_dfpwm_left_url .." ".. server_url .. videoInfo.audio_dfpwm_right_url)
-- 下载和播放函数
frames = {}
local frameCount = 0
frameData = {}
local get_task = {}
local function readU32(s, pos)
if pos + 3 > #s then return nil end
local b1, b2, b3, b4 = s:byte(pos, pos + 3)
-- 使用 bit32.lshift 和 bit32.bor
return bit32.bor(
bit32.lshift(b1, 24),
bit32.lshift(b2, 16),
bit32.lshift(b3, 8),
b4
)
end
-- 解析 FramePack 二进制数据
local function unpackFramePack(data)
local frames = {}
local pos = 1
local frameCount = readU32(data, pos)
if not frameCount then error("Invalid framepack header") end
pos = pos + 4
for i = 1, frameCount do
local size = readU32(data, pos)
if not size then error("Truncated framepack at frame " .. i) end
pos = pos + 4
if pos + size - 1 > #data then error("Frame " .. i .. " out of bounds") end
local frameData = data:sub(pos, pos + size - 1)
frames[i] = frameData
pos = pos + size
end
return frames
end
-- 分批下载(每批 50 帧)
local BATCH_SIZE = 20
-- local totalFrames = #videoInfo.frame_urls
local totalFrames = videoInfo.fps * 5 -- 仅下载前5秒以节省时间
local allFrameData = {}
for startIdx = 1, totalFrames, BATCH_SIZE do
local endIdx = math.min(startIdx + BATCH_SIZE - 1, totalFrames)
print("Downloading batch: " .. startIdx .. " - " .. endIdx)
-- 构造要下载的 URL 列表(相对路径)
local urls = {}
for i = startIdx, endIdx do
-- 假设 videoInfo.frame_urls 是 "/frames/xxx/frame_xxx.png" 这样的相对路径
table.insert(urls, videoInfo.frame_urls[i])
end
-- 请求打包
local resp,err = http.post(
server_url .. "/api/framepack",
textutils.serializeJSON({ urls = urls }),
{ ["Content-Type"] = "application/json" }
)
if not resp then error("Failed to download framepack "..err) end
local binData = resp.readAll()
print(#binData .. " bytes received")
resp.close()
-- 解包
local batchFrames = unpackFramePack(binData)
for i = 1, #batchFrames do
allFrameData[startIdx + i - 1] = batchFrames[i]
end
print("Loaded batch: " .. #batchFrames .. " frames")
sleep(0.1)
end
-- 后续播放时直接使用 allFrameData[frameIndex]
-- -- 预加载前N帧
-- for i = 1, math.min(videoInfo.fps, #videoInfo.frame_urls) do
-- if frameData[i] then
-- local data = frameData[i]
-- local imgBin = {}
-- for j = 1, #data do
-- imgBin[#imgBin + 1] = data:byte(j)
-- end
-- local success, image = pcall(function()
-- return gpu.decodeImage(table.unpack(imgBin))
-- end)
-- if success then
-- print("Loaded frame "..i .."/"..#videoInfo.frame_urls)
-- frames[i] = image
-- frameData[i] = nil
-- data = nil
-- frameCount = frameCount + 1
-- else
-- print(image)
-- end
-- end
-- end
-- 播放循环
local frameDelay = tonumber(string.format("%.3f",1 / videoInfo.fps))
print("Starting playback (FPS: " .. videoInfo.fps .. ")")
print(frameDelay * #videoInfo.frame_urls)
local starttime1 = os.clock()
sleep(1)
-- 播放前已缓存 10 秒
local totalFramesToPlay = #videoInfo.frame_urls
local maxCachedFrames = math.min(videoInfo.fps * 20, totalFramesToPlay) -- 最多缓存 20 秒
local nextDownloadIndex = math.min(videoInfo.fps * 5 + 1, totalFramesToPlay + 1) -- 从第 5 秒后开始继续下载
-- 标志:是否还在播放
local running = true
-- 全局帧索引(由播放线程更新)
local frameIndex = 1
-- 缓存协程:边播边下(并发下载多个 framepack
local function cacheAhead()
while running do
-- 计算当前应缓存的范围 [frameIndex, frameIndex + maxCachedFrames)
local currentStart = frameIndex
local currentEnd = math.min(frameIndex + videoInfo.fps * 20 - 1, totalFramesToPlay)
-- 找出尚未缓存的帧段,按 BATCH_SIZE 切分
local batches = {}
local i = currentStart
while i <= currentEnd do
if not allFrameData[i] then
local batchStart = i
local batchEnd = math.min(batchStart + BATCH_SIZE - 1, currentEnd)
-- 构造 URL 列表(相对路径)
local urls = {}
for j = batchStart, batchEnd do
if not allFrameData[j] then
table.insert(urls, videoInfo.frame_urls[j])
end
end
if #urls > 0 then
table.insert(batches, {
start = batchStart,
urls = urls
})
end
i = batchEnd + 1
else
i = i + 1
end
end
-- 如果没有需要下载的批次,休眠后重试
if #batches == 0 then
sleep(0.5)
end
-- 并发下载所有缺失的批次
downloadTasks = {}
for i, batch in ipairs(batches) do
table.insert(downloadTasks, function()
print("Downloading batch: " .. batch.start .. " - " .. (batch.start + #batch.urls - 1))
while true do
-- local resp = http.post(
-- server_url .. "/api/framepack?"..batch.urls[1],
-- textutils.serializeJSON({ urls = batch.urls }),
-- { ["Content-Type"] = "application/json" }
-- )
local resp = http.post({
url = server_url .. "/api/framepack?"..batch.urls[1],
headers = { ["Content-Type"] = "application/json" },
body = textutils.serializeJSON({ urls = batch.urls } ),
timeout = 3
}
)
if resp then
local binData = resp.readAll()
resp.close()
local batchFrames = unpackFramePack(binData)
for idx = 1, #batchFrames do
local globalIdx = batch.start + idx - 1
if not allFrameData[globalIdx] then
allFrameData[globalIdx] = batchFrames[idx]
end
end
print("Cached batch: " .. batch.start .. " - " .. (batch.start + #batchFrames - 1))
break
else
print("Failed to cache batch starting at " .. batch.start)
end
end
end)
end
-- 并发执行所有下载任务
parallel.waitForAll(table.unpack(downloadTasks))
-- 检查是否已缓存到最后一帧
if currentEnd >= #videoInfo.frame_urls then
break
end
-- 小睡避免过于频繁轮询
sleep(0.2)
end
end
-- -- 音频播放协程(保持原样)
-- local function playAudio()
-- os.queueEvent("audio_start") -- 触发同步
-- shell.run("bg speakerlib play " ..
-- server_url .. videoInfo.audio_dfpwm_url .. " " ..
-- server_url .. videoInfo.audio_dfpwm_left_url .. " " ..
-- server_url .. videoInfo.audio_dfpwm_right_url)
-- end
-- 视频渲染协程(稍作优化:及时清理旧帧)
local function renderVideo()
local frameIndex2 = 0
local play_fps = videoInfo.fps
local starttime = os.clock()
os.queueEvent("audio_start")
while running and frameIndex <= #videoInfo.frame_urls do
local frame_start_time = os.clock()
local data = allFrameData[frameIndex]
local imgBin = {}
for j = 1, #data do
imgBin[#imgBin + 1] = data:byte(j)
end
for i = 1, frameIndex - 5 do
allFrameData[i] = nil
end
data = nil
local success, image = pcall(function()
return gpu.decodeImage(table.unpack(imgBin))
end)
--local image = frames[frameIndex]
-- 播放当前帧
if image then
gpu.drawImage(0, 0, image.ref())
--gpu.drawText(1,1,frameIndex .. " / "..#videoInfo.frame_urls.." fps: "..math.ceil(play_fps),0xffffff)
gpu.drawText(1,1,frameIndex .. " / "..#videoInfo.frame_urls.." fps: "..math.ceil(play_fps),0xffffff)
local event, id
if frameIndex > 1 then
repeat
event, id = os.pullEvent("timer")
until id == timer_id
end
timer_id = os.startTimer(frameDelay)
gpu.sync()
-- for i = 1, frameIndex - 4 do
-- local image = frames[i]
-- image:free()
-- end
image:free()
else
error(frameIndex.. "Frame does not exist, possibly due to full memory or insufficient performance")
end
local frame_end_time = os.clock()
local frame_time = frame_end_time - frame_start_time
play_fps = 1 / frame_time
local play_time = tonumber(string.format("%.3f",os.clock() - starttime))
frameIndex2 = math.ceil(play_time / frameDelay)
if frameIndex2 + (frameIndex - frameIndex2) * 0.5 >= 4 then
frameIndex = frameIndex2
else
frameIndex = frameIndex+1
end
end
end
-- 启动三个协程
parallel.waitForAll(renderVideo, cacheAhead)
local endtime = os.clock()
local time = endtime-starttime1
print("Playback finished")
print("Play_Time: ".. time)
local fps = #videoInfo.frame_urls / time
print("Final_FPS: "..fps)
print("Average Frame Interval: "..1/fps)