467 lines
13 KiB
Lua
467 lines
13 KiB
Lua
-- 简化版视频播放器
|
||
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) |