Compare commits

..

7 Commits

Author SHA1 Message Date
nnwang
13276d00ee 在当前帧前的数据包不处理 2026-02-22 07:41:17 +08:00
nnwang
06f9013090 添加自动调整屏幕大小功能 2026-02-22 07:11:56 +08:00
nnwang
f7acd8ce9a 添加error报错等 2026-02-22 06:29:50 +08:00
nnwang
cc513092ca 添加http错误提示 2026-02-22 05:48:45 +08:00
nnwang
e8aba71e70 修复audiourl定义错位 2026-02-22 05:23:30 +08:00
nnwang
50a74935f1 修改speakerlib的创建方式 2026-02-22 05:20:35 +08:00
nnwang
13ec00766e 更改保留帧数量 2026-02-22 05:00:26 +08:00

173
play.lua
View File

@@ -1,9 +1,72 @@
-- 简化版视频播放器
local gpu = peripheral.wrap("tm_gpu_0")
local gpu_Size_Auto = true --自动分辨率设置
local gpu_Size = 64 --默认屏幕分辨率
if not gpu then gpu = peripheral.find("tm_gpu") end
gpu.refreshSize()
gpu.setSize(64)
local w, h = gpu.getSize()
gpu.setSize(gpu_Size)
local w, h, w_black, h_black, Size = gpu.getSize()
-- 分批下载(每批 10 帧)
local BATCH_SIZE = 10
local function computeOptimalSize(w_black, h_black)
local w_target, h_target = 640, 360
local max_size = 64
-- 避免除零
if w_black <= 0 or h_black <= 0 then
return 1
end
-- 计算理想缩放比例(不能让任一边超过目标)
local size_by_w = w_target / w_black
local size_by_h = h_target / h_black
local ideal_size = math.min(size_by_w, size_by_h)
-- 如果理想值 >= 64直接用 64
if ideal_size >= max_size then
return max_size
end
-- 否则,在 floor 和 ceil 中选更优的(但不能超过 64
local size_floor = math.floor(ideal_size)
local size_ceil = math.ceil(ideal_size)
-- 确保不越界
if size_floor < 1 then size_floor = 1 end
if size_ceil > max_size then size_ceil = max_size end
-- 计算两个候选的误差(欧氏距离或曼哈顿距离均可,这里用平方和)
local function error(size)
local w = w_black * size
local h = h_black * size
return (w - w_target)^2 + (h - h_target)^2
end
local err_floor = error(size_floor)
local err_ceil = error(size_ceil)
if err_floor <= err_ceil then
return size_floor
else
return size_ceil
end
end
if gpu_Size_Auto then
local optimal_Size = computeOptimalSize(w_black, h_black)
print("[I] gpu_Size: "..optimal_Size)
gpu.refreshSize()
gpu.setSize(optimal_Size)
end
local w, h, w_black, h_black, Size = gpu.getSize()
local mypath = "/"..fs.getDir(shell.getRunningProgram())
if not fs.exists(mypath.."/speakerlib.lua") then shell.run("wget https://git.liulikeji.cn/xingluo/computer_craft_video_play/raw/branch/main/speakerlib.lua") end
@@ -74,7 +137,8 @@ print("task_id: " .. task_id)
-- 用于记录已打印的日志数量,避免重复打印
local total_logs_printed = 0
speakerrun = true
local speakerrun = true
while true do
@@ -105,10 +169,17 @@ while true do
end
total_logs_printed = total_logs_printed + #task_info.new_logs
end
if task_info.result.audio_urls and speakerrun then
local main_audio_url = server_url .. task_info.result.audio_urls.audio_dfpwm_url
local left_audio_url = server_url .. task_info.result.audio_urls.audio_dfpwm_left_url
local right_audio_url = server_url .. task_info.result.audio_urls.audio_dfpwm_right_url
speakerrun = false
shell.run("bg speakerlib play ".. server_url .. task_info.result.audio_urls.audio_dfpwm_url .." ".. server_url .. task_info.result.audio_urls.audio_dfpwm_left_url .." ".. server_url .. task_info.result.audio_urls.audio_dfpwm_right_url)
local new_tab_id = shell.openTab(mypath.."/speakerlib","play",main_audio_url,left_audio_url,right_audio_url)
local tab_id = multishell.getCurrent()
-- 切换一次进程,让其可以自然关闭
multishell.setFocus(new_tab_id)
multishell.setFocus(tab_id)
end
-- 检查是否完成
@@ -132,18 +203,11 @@ videoInfo.fps = 20
videoInfo.frame_urls = {}
-- /frames/20116713/frame_000001.png
for i = 1, videoInfo.total_frames - 10 do
videoInfo.frame_urls[i] = "/frames/"..task_id.."/frame_" .. string.format("%06d", i) .. ".png"
end
-- 播放音频 单通道
--shell.run("bg speaker play", server_url .. videoInfo.audio_dfpwm_url)
-- server_url .. videoInfo.audio_urls.audio_dfpwm_url -- 音频单通道文件 URL
-- server_url .. videoInfo.audio_urls.audio_dfpwm_right_url -- 音频右通道文件 URL
-- server_url .. videoInfo.audio_urls.audio_dfpwm_left_url -- 音频左通道文件 URL
-- 下载和播放函数
frames = {}
local frameCount = 0
@@ -185,8 +249,6 @@ local function unpackFramePack(data)
return frames
end
-- 分批下载(每批 50 帧)
local BATCH_SIZE = 20
-- local totalFrames = #videoInfo.frame_urls
local totalFrames = videoInfo.fps * 20 -- 仅下载前20秒以节省时间
local allFrameData = {}
@@ -212,11 +274,11 @@ local initTasks = {}
for _, batch in ipairs(initBatches) do
table.insert(initTasks, function()
while true do
local resp = http.post({
local resp,http_error = http.post({
url = server_url .. "/api/framepack?" .. batch.urls[1],
headers = { ["Content-Type"] = "application/json" },
body = textutils.serializeJSON({ urls = batch.urls }),
timeout = 3,
timeout = 2,
binary = true
})
@@ -230,10 +292,15 @@ for _, batch in ipairs(initBatches) do
allFrameData[globalIdx] = batchFrames[idx]
end
print("Cached init batch: " .. batch.start .. " - " .. (batch.start + #batchFrames - 1))
print("[V] Cached init batch: " .. batch.start .. " - " .. (batch.start + #batchFrames - 1))
break
else
print("Retry init batch starting at " .. batch.start)
print("[E] " .. batch.start .." "..http_error)
if http_error == "Response is too large" then
error("\n[E] Frame packet size exceeds 16MB. \nPlease try adjusting the screen size or resolution.")
end
sleep(0.5)
end
end
@@ -257,7 +324,7 @@ until _G.audio_ready
local starttime1 = os.clock()
-- 播放前已缓存 20 秒
local totalFramesToPlay = #videoInfo.frame_urls
-- 标志:是否还在播放
@@ -333,6 +400,15 @@ local function httpResponseHandler()
local batch = pendingRequests[url]
if batch then
pendingRequests[url] = nil
-- [优化] 检查整个批次是否已经完全过期待(最晚的一帧都小于 frameIndex - 10
-- 虽然 unpacking 很快,但在极端延迟下可以直接跳过解包
local batchEndIdx = batch.start + #batch.urls - 1
if batchEndIdx < frameIndex - 10 then
-- 批次过旧,直接丢弃,仅释放句柄
handleOrErr.close()
print("[X] Drop old batch (too late): " .. batch.start)
else
local binData = handleOrErr.readAll()
handleOrErr.close()
@@ -340,27 +416,52 @@ local function httpResponseHandler()
if success then
for idx = 1, #batchFrames do
local globalIdx = batch.start + idx - 1
-- [核心修改] 只有当帧号 >= 当前帧 - 10 时才存入内存
-- 防止旧数据占用内存或被错误地重新缓存
if globalIdx >= frameIndex - 10 then
if not allFrameData[globalIdx] then
allFrameData[globalIdx] = batchFrames[idx]
end
end
print("[V] Cached batch: " .. batch.start .. " - " .. (batch.start + #batchFrames - 1))
end
-- 仅计算大致大小用于日志,逻辑不变
local sizeInMB = #binData / (1024 * 1024)
print("[V] Cached batch: " .. batch.start .. " - " .. (batch.start + #batchFrames - 1) .." ("..tonumber(string.format("%.1f", sizeInMB)).." Mb)")
else
print("[R] Unpack failed for batch " .. batch.start .. ": " .. tostring(batchFrames))
-- 标记这些帧可重试
-- 这里如果解包失败,如果数据还算新才许重试
if batchEndIdx >= frameIndex - 10 then
for j = batch.start, batch.start + #batch.urls - 1 do
downloadedFrames[j] = nil
end
end
end
end
end
elseif event == "http_failure" then
local batch = pendingRequests[url]
if batch then
pendingRequests[url] = nil
-- [核心修改] 检查批次时效性
-- 如果该批次最晚的一帧都比 (frameIndex - 10) 还旧,直接丢弃,不再重试
local batchEndIdx = batch.start + #batch.urls - 1
if batchEndIdx < frameIndex - 10 then
-- 这里的 print 可以注释掉,或者保留用于调试
print("[X] Drop old batch (too late): " .. batch.start)
-- 标记已下载(尽管是失败),防止 cacheAhead 再次死循环请求它
for j = batch.start, batch.start + #batch.urls - 1 do
downloadedFrames[j] = true
end
else
-- 只有数据还比较“新鲜”时,才进行重试逻辑
batch.retry = (batch.retry or 0) + 1
if batch.retry < 3 then
print("[R] Retrying batch " .. batch.start .. " (attempt " .. (batch.retry + 1) .. ")")
print("[R] Retrying batch " .. batch.start .. " (attempt " .. (batch.retry + 1) .. ") Error: "..tostring(handleOrErr))
-- 允许重试
for j = batch.start, batch.start + #batch.urls - 1 do
downloadedFrames[j] = nil
@@ -368,13 +469,18 @@ local function httpResponseHandler()
-- 重新触发 cacheAhead 下一轮会重发
else
print("[X] Giving up on batch " .. batch.start)
print("[E] " .. batch.start.." "..tostring(handleOrErr))
-- 永久失败,不再重试
for j = batch.start, batch.start + #batch.urls - 1 do
downloadedFrames[j] = true
end
end
end
end
elseif event == "timer" then
os.queueEvent("time",url, handleOrErr)
end
end
end
@@ -390,6 +496,7 @@ local function renderVideo()
local fps = videoInfo.fps
local frameDelay = 1 / fps
local startTime = os.clock()
local nil_e = 0
os.queueEvent("audio_start") -- 通知音频开始播放
@@ -409,7 +516,7 @@ local function renderVideo()
local frameStart = frameEnd
-- 1. 内存清理
for i = 1, math.max(0, frameIndex - 5) do
for i = 1, math.max(0, frameIndex - 10) do
allFrameData[i] = nil
end
@@ -433,15 +540,16 @@ local function renderVideo()
if debug then
-- 格式:帧进度 | 平均FPS(平均耗时) | 最低FPS(波峰耗时)
local statusText = string.format("%d/%d Avg:%d(%dms) Low:%d(%dms)",
local statusText = string.format("%d/%d Avg:%d(%dms) Low:%d(%dms) %dx%d gpuSize:%d",
frameIndex,
#videoInfo.frame_urls,
math.floor(ui_avgFps),
math.floor(ui_avgMs),
math.floor(ui_lowFps),
math.floor(ui_lowMs)
math.floor(ui_lowMs),
w,h,Size
)
gpu.drawText(1, 1, statusText, 0xFFFFFF)
gpu.drawText(1, 1, statusText, 0xFD452A)
end
-- 这里的 Timer 逻辑移出去了,确保跳帧时也能同步
@@ -458,16 +566,17 @@ local function renderVideo()
image:free()
-- === 修改点:只有成功渲染并同步后,才更新 frameEnd ===
frameEnd = os.clock()
nil_e = 0
else
print("Decode failed frame " .. frameIndex)
end
else
if frameIndex > 1 and timer_id then
repeat
local event, id = os.pullEvent("timer")
until id == timer_id
print("[E] "..frameIndex.." is nil")
nil_e = nil_e + 1
if nil_e >= 5 then
error("\n[E] No available video data for 10 seconds \nDownload speed is below the minimum threshold! \nPlease try checking your network bandwidth or adjusting the resolution.")
end
sleep(2)
timer_id = os.startTimer(frameDelay)
-- === 修改点:数据为空时,不做任何操作,直接进入下方的同步逻辑 ===
-- 此时 frameEnd 依然等于 frameStart计算出的 costTime 为 0