上传文件至 /

This commit is contained in:
2026-01-10 02:14:32 +08:00
parent c1bc227490
commit 8a456f3253
3 changed files with 815 additions and 0 deletions

467
play.lua Normal file
View File

@@ -0,0 +1,467 @@
-- 简化版视频播放器
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 * 10 -- 仅下载前10秒以节省时间
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 * 10 + 1, totalFramesToPlay + 1) -- 从第 10 秒后开始继续下载
-- 标志:是否还在播放
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, playAudio, 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)

5
speaker_groups.cfg Normal file
View File

@@ -0,0 +1,5 @@
{
main = {}, --混合通道扬声器列表
left = {"speaker_2"}, --左声道扬声器列表
right = {"speaker_3"} --右声道扬声器列表
}

343
speakerlib.lua Normal file
View File

@@ -0,0 +1,343 @@
-- SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
-- 全局控制变量
_G.getPlaymax = 0 -- 总时间(秒)
_G.getPlay = 0 -- 当前播放时间(秒)
_G.setPlay = 0 -- 设置播放进度(秒)
_G.Playopen = true -- 播放开关false停止
_G.Playstop = false -- 暂停控制true暂停false恢复
_G.Playprint = false -- 信息输出开关true开false关
_G.setVolume = 1 -- 音量控制0-3
local mypath = "/"..fs.getDir(shell.getRunningProgram())
-- 扬声器配置
local speakerlist = {
main = {},
left = {},
right = {}
}
local function printlog(...)
if _G.Playprint then
print(...)
end
end
local function loadSpeakerConfig()
-- 默认配置所有扬声器都在main组
speakerlist = {
main = { peripheral.find("speaker") },
left = {},
right = {}
}
local speaker_groups = fs.open(mypath.."/speaker_groups.cfg","r")
if speaker_groups then
local content = speaker_groups.readAll()
speaker_groups.close()
if content then
local success, tableData = pcall(textutils.unserialise, content)
if success and type(tableData) == "table" then
speakerlist = { main = {}, left = {}, right = {} }
for group_name, speakers in pairs(tableData) do
if speakerlist[group_name] then
for _, speaker_name in ipairs(speakers) do
local speaker = peripheral.wrap(speaker_name)
if speaker and peripheral.hasType(speaker_name, "speaker") then
table.insert(speakerlist[group_name], speaker)
end
end
end
end
return
end
end
end
end
local function get_total_duration(url)
if _G.Playprint then printlog("Calculating duration...") end
local handle, err = http.get(url)
if not handle then
error(url .. "Could not get duration: " .. (err or "Unknown error"))
end
local data = handle.readAll()
handle.close()
-- DFPWM: 每字节8个样本48000采样率
local total_length = (#data * 8) / 48000
return total_length, #data
end
local function play_audio_chunk(speakers, buffer)
if #speakers > 0 and buffer and #buffer > 0 then
for _, speaker in pairs(speakers) do
local success = false
while not success and _G.Playopen do
success = speaker.playAudio(buffer, _G.setVolume)
if not success then
os.pullEvent("speaker_audio_empty")
end
end
end
end
end
local cmd = ...
if cmd == "stop" then
local all_speakers = {}
for _, group in pairs(speakerlist) do
for _, speaker in pairs(group) do
table.insert(all_speakers, speaker)
end
end
for _, speaker in pairs(all_speakers) do
speaker.stop()
end
elseif cmd == "play" then
local _, mono_url, left_url, right_url = ...
-- 加载扬声器配置
loadSpeakerConfig()
print("main speaker:"..#speakerlist.main)
print("left speaker:"..#speakerlist.left)
print("right speaker:"..#speakerlist.right)
-- 检查是否有扬声器
local has_speakers = false
for _, group in pairs(speakerlist) do
if #group > 0 then
has_speakers = true
break
end
end
if not has_speakers then
error("No speakers attached", 0)
end
-- 检查是否至少有一个音频URL
if not mono_url and not left_url and not right_url then
error("Usage: speaker play [mono_url] [left_url] [right_url]\nAt least one audio URL is required", 0)
end
-- 计算总时长(使用任意一个通道)
local total_length, total_size
if mono_url then
total_length, total_size = get_total_duration(mono_url)
elseif left_url then
total_length, total_size = get_total_duration(left_url)
elseif right_url then
total_length, total_size = get_total_duration(right_url)
end
-- 设置总时间
_G.getPlaymax = total_length
_G.getPlay = 0
if _G.Playprint then
printlog("Playing audio (" .. math.ceil(total_length) .. "s)")
end
-- 创建HTTP连接
local mono_httpfile, left_httpfile, right_httpfile
if mono_url and #speakerlist.main > 0 then
mono_httpfile = http.get(mono_url)
if not mono_httpfile then
error("Could not open mono audio stream")
end
end
if left_url and #speakerlist.left > 0 then
left_httpfile = http.get(left_url)
if not left_httpfile then
error("Could not open left audio stream")
end
end
if right_url and #speakerlist.right > 0 then
right_httpfile = http.get(right_url)
if not right_httpfile then
error("Could not open right audio stream")
end
end
-- 初始化DFPWM解码器
local decoder = require "cc.audio.dfpwm".make_decoder()
local left_decoder = require "cc.audio.dfpwm".make_decoder()
local right_decoder = require "cc.audio.dfpwm".make_decoder()
-- 每次读取的字节数DFPWM: 每秒6000字节
local chunk_size = 6000
local bytes_read = 0
-- 初始化播放位置
if _G.setPlay > 0 then
local skip_bytes = math.floor(_G.setPlay * 6000)
if skip_bytes < total_size then
-- 跳过指定字节数
local skipped = 0
while skipped < skip_bytes and _G.Playopen do
local to_skip = math.min(8192, skip_bytes - skipped)
if mono_httpfile then mono_httpfile.read(to_skip) end
if left_httpfile then left_httpfile.read(to_skip) end
if right_httpfile then right_httpfile.read(to_skip) end
skipped = skipped + to_skip
bytes_read = bytes_read + to_skip
end
_G.getPlay = _G.setPlay
_G.setPlay = 0
end
end
-- 主播放循环
os.pullEvent("audio_start")
while bytes_read < total_size and _G.Playopen do
-- 检查是否需要设置播放位置
if _G.setPlay > 0 then
-- 重新打开所有连接并跳转
if mono_httpfile then mono_httpfile.close() end
if left_httpfile then left_httpfile.close() end
if right_httpfile then right_httpfile.close() end
if mono_url and #speakerlist.main > 0 then
mono_httpfile = http.get(mono_url)
if not mono_httpfile then error("Could not reopen mono stream") end
end
if left_url and #speakerlist.left > 0 then
left_httpfile = http.get(left_url)
if not left_httpfile then error("Could not reopen left stream") end
end
if right_url and #speakerlist.right > 0 then
right_httpfile = http.get(right_url)
if not right_httpfile then error("Could not reopen right stream") end
end
local skip_bytes = math.floor(_G.setPlay * 6000)
if skip_bytes < total_size then
local skipped = 0
while skipped < skip_bytes and _G.Playopen do
local to_skip = math.min(8192, skip_bytes - skipped)
if mono_httpfile then mono_httpfile.read(to_skip) end
if left_httpfile then left_httpfile.read(to_skip) end
if right_httpfile then right_httpfile.read(to_skip) end
skipped = skipped + to_skip
bytes_read = skip_bytes
end
_G.getPlay = _G.setPlay
_G.setPlay = 0
end
end
-- 检查暂停状态
while _G.Playstop and _G.Playopen do
os.sleep(0.1)
end
-- 检查停止状态
if not _G.Playopen then
break
end
-- 读取音频数据
local mono_chunk, left_chunk, right_chunk
local mono_buffer, left_buffer, right_buffer
if mono_httpfile then
mono_chunk = mono_httpfile.read(chunk_size)
end
if left_httpfile then
left_chunk = left_httpfile.read(chunk_size)
end
if right_httpfile then
right_chunk = right_httpfile.read(chunk_size)
end
-- 检查是否所有通道都没有数据
if (not mono_chunk or #mono_chunk == 0) and
(not left_chunk or #left_chunk == 0) and
(not right_chunk or #right_chunk == 0) then
break
end
-- 解码音频数据
if mono_chunk and #mono_chunk > 0 then
mono_buffer = decoder(mono_chunk)
end
if left_chunk and #left_chunk > 0 then
left_buffer = left_decoder(left_chunk)
end
if right_chunk and #right_chunk > 0 then
right_buffer = right_decoder(right_chunk)
end
-- 并行播放所有通道
parallel.waitForAll(
function()
if mono_buffer and #mono_buffer > 0 then
play_audio_chunk(speakerlist.main, mono_buffer)
end
end,
function()
if right_buffer and #right_buffer > 0 then
play_audio_chunk(speakerlist.right, right_buffer)
end
end,
function()
if left_buffer and #left_buffer > 0 then
play_audio_chunk(speakerlist.left, left_buffer)
end
end
)
-- 更新进度
local max_chunk_size = math.max(
mono_chunk and #mono_chunk or 0,
left_chunk and #left_chunk or 0,
right_chunk and #right_chunk or 0
)
bytes_read = bytes_read + max_chunk_size
_G.getPlay = bytes_read / 6000
if _G.Playprint then
term.setCursorPos(1, term.getCursorPos())
printlog(("Playing: %ds / %ds"):format(math.floor(_G.getPlay), math.ceil(total_length)))
end
end
-- 关闭HTTP连接
if mono_httpfile then mono_httpfile.close() end
if left_httpfile then left_httpfile.close() end
if right_httpfile then right_httpfile.close() end
if _G.Playprint and _G.Playopen then
printlog("Playback finished.")
end
-- 重置播放状态
_G.Playopen = true
_G.Playstop = false
_G.getPlay = 0
else
local programName = arg[0] or fs.getName(shell.getRunningProgram())
printlog("Usage:")
printlog(programName .. " stop")
printlog(programName .. " play [mono_url] [left_url] [right_url]")
printlog("Note: At least one URL is required")
end