Add files via upload
This commit is contained in:
504
play.lua
Normal file
504
play.lua
Normal file
@@ -0,0 +1,504 @@
|
||||
-- play.lua (最终版,帧率显示基于帧间隔)
|
||||
local gpu = peripheral.wrap("tm_gpu_9")
|
||||
gpu.refreshSize()
|
||||
gpu.setSize(64)
|
||||
local w, h = gpu.getSize()
|
||||
|
||||
server_url = "https://newgmapi.liulikeji.cn"
|
||||
|
||||
-- 手动加载 speakerlib.lua 并确保返回模块表
|
||||
local speakerlib
|
||||
local file = fs.open("speakerlib.lua", "r")
|
||||
if not file then
|
||||
error("speakerlib.lua not found")
|
||||
end
|
||||
local content = file.readAll()
|
||||
file.close()
|
||||
|
||||
-- 创建一个新环境,以 _G 为原型,并显式注入 require
|
||||
local env = setmetatable({ require = require }, { __index = _G })
|
||||
local fn, err = load(content, "speakerlib.lua", nil, env)
|
||||
if not fn then
|
||||
error("Failed to compile speakerlib.lua: " .. tostring(err))
|
||||
end
|
||||
local ok, result = pcall(fn)
|
||||
if not ok then
|
||||
error("Error executing speakerlib.lua: " .. tostring(result))
|
||||
end
|
||||
if type(result) ~= "table" then
|
||||
error("speakerlib.lua did not return a table (returned " .. type(result) .. ")")
|
||||
end
|
||||
speakerlib = result
|
||||
print("[OK] speakerlib loaded successfully")
|
||||
|
||||
-- 检查命令行参数
|
||||
local videoUrl = ...
|
||||
if not videoUrl then
|
||||
print("Usage: video_player <video URL>")
|
||||
print("Example: video_player https://example.com/video.mp4")
|
||||
return
|
||||
end
|
||||
|
||||
_G.audio_ready = false
|
||||
local task_id
|
||||
local status_url
|
||||
|
||||
if videoUrl:sub(1,4) ~= "http" then
|
||||
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)
|
||||
|
||||
-- 轮询任务状态直到获取足够帧数(200帧后开始播放)
|
||||
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
|
||||
|
||||
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.result.current_frames and task_info.result.current_frames >= 200 then
|
||||
break
|
||||
end
|
||||
|
||||
sleep(1)
|
||||
end
|
||||
|
||||
-- 获取最终任务信息
|
||||
local response = http.get(status_url, {["Content-Type"] = "application/json"})
|
||||
local finalResp = textutils.unserialiseJSON(response.readAll())
|
||||
response.close()
|
||||
|
||||
videoInfo = finalResp.result
|
||||
videoInfo.fps = 20
|
||||
videoInfo.frame_urls = {}
|
||||
|
||||
for i = 1, videoInfo.total_frames - 10 do
|
||||
videoInfo.frame_urls[i] = "/frames/"..task_id.."/frame_" .. string.format("%06d", i) .. ".png"
|
||||
end
|
||||
|
||||
----------------------------------------------------------------
|
||||
-- 音频播放协程
|
||||
----------------------------------------------------------------
|
||||
local function audioPlayer()
|
||||
print("[AUDIO] audioPlayer started")
|
||||
|
||||
if not videoInfo.audio_urls then
|
||||
print("[AUDIO] No audio URLs provided, skipping playback.")
|
||||
_G.audio_ready = true
|
||||
return
|
||||
end
|
||||
print("[AUDIO] Audio URLs found")
|
||||
|
||||
local mono = videoInfo.audio_urls.audio_dfpwm_url and (server_url .. videoInfo.audio_urls.audio_dfpwm_url) or nil
|
||||
local left = videoInfo.audio_urls.audio_dfpwm_left_url and (server_url .. videoInfo.audio_urls.audio_dfpwm_left_url) or nil
|
||||
local right = videoInfo.audio_urls.audio_dfpwm_right_url and (server_url .. videoInfo.audio_urls.audio_dfpwm_right_url) or nil
|
||||
|
||||
print("[AUDIO] mono: " .. tostring(mono))
|
||||
print("[AUDIO] left: " .. tostring(left))
|
||||
print("[AUDIO] right: " .. tostring(right))
|
||||
|
||||
-- 强制开启 speakerlib 内部日志
|
||||
_G.Playprint = true
|
||||
|
||||
-- 调用播放
|
||||
speakerlib.play(mono, left, right)
|
||||
print("[AUDIO] Playback finished normally")
|
||||
end
|
||||
|
||||
-- 以下为视频播放相关函数
|
||||
local function readU32(s, pos)
|
||||
if pos + 3 > #s then return nil end
|
||||
local b1, b2, b3, b4 = s:byte(pos, pos + 3)
|
||||
return bit32.bor(
|
||||
bit32.lshift(b1, 24),
|
||||
bit32.lshift(b2, 16),
|
||||
bit32.lshift(b3, 8),
|
||||
b4
|
||||
)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
-- 配置参数
|
||||
local BATCH_SIZE = 20
|
||||
local PRELOAD_SECONDS = 20
|
||||
local CACHE_WINDOW_SECONDS = 10
|
||||
local totalFramesToPlay = #videoInfo.frame_urls
|
||||
local preloadFrames = math.min(videoInfo.fps * PRELOAD_SECONDS, totalFramesToPlay)
|
||||
local allFrameData = {}
|
||||
|
||||
-- 分批下载预加载帧
|
||||
print("Pre-caching first " .. preloadFrames .. " frames...")
|
||||
local initBatches = {}
|
||||
for startIdx = 1, preloadFrames, BATCH_SIZE do
|
||||
local endIdx = math.min(startIdx + BATCH_SIZE - 1, preloadFrames)
|
||||
local urls = {}
|
||||
for i = startIdx, endIdx do
|
||||
table.insert(urls, videoInfo.frame_urls[i])
|
||||
end
|
||||
table.insert(initBatches, { start = startIdx, urls = urls })
|
||||
end
|
||||
|
||||
-- 并发下载所有预加载批次
|
||||
local initTasks = {}
|
||||
for _, batch in ipairs(initBatches) do
|
||||
table.insert(initTasks, function()
|
||||
while true do
|
||||
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,
|
||||
binary = true
|
||||
})
|
||||
|
||||
if resp then
|
||||
local binData = resp.readAll()
|
||||
resp.close()
|
||||
|
||||
local batchFrames = unpackFramePack(binData)
|
||||
for idx = 1, #batchFrames do
|
||||
local globalIdx = batch.start + idx - 1
|
||||
allFrameData[globalIdx] = batchFrames[idx]
|
||||
end
|
||||
print("Cached init batch: " .. batch.start .. " - " .. (batch.start + #batchFrames - 1))
|
||||
break
|
||||
else
|
||||
print("Retry init batch starting at " .. batch.start)
|
||||
sleep(0.5)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
parallel.waitForAll(table.unpack(initTasks))
|
||||
print("Initial caching completed.")
|
||||
|
||||
-- 播放循环参数
|
||||
local frameDelay = 1 / videoInfo.fps
|
||||
print("Starting playback (FPS: " .. videoInfo.fps .. ")")
|
||||
|
||||
local starttime1 = os.clock()
|
||||
local running = true
|
||||
local frameIndex = 1
|
||||
|
||||
-- 共享状态
|
||||
local pendingRequests = {}
|
||||
local downloadedFrames = {}
|
||||
|
||||
-- 缓存协程:动态预取未来 CACHE_WINDOW_SECONDS 秒的帧
|
||||
local function cacheAhead()
|
||||
while running do
|
||||
local currentStart = frameIndex
|
||||
local currentEnd = math.min(frameIndex + videoInfo.fps * CACHE_WINDOW_SECONDS - 1, totalFramesToPlay)
|
||||
|
||||
local batches = {}
|
||||
local i = currentStart
|
||||
while i <= currentEnd do
|
||||
if not allFrameData[i] and not downloadedFrames[i] then
|
||||
local batchStart = i
|
||||
local batchEnd = math.min(batchStart + BATCH_SIZE - 1, currentEnd)
|
||||
local urls = {}
|
||||
for j = batchStart, batchEnd do
|
||||
if not allFrameData[j] and not downloadedFrames[j] then
|
||||
table.insert(urls, videoInfo.frame_urls[j])
|
||||
downloadedFrames[j] = true
|
||||
end
|
||||
end
|
||||
if #urls > 0 then
|
||||
table.insert(batches, {
|
||||
start = batchStart,
|
||||
urls = urls,
|
||||
retry = 0
|
||||
})
|
||||
end
|
||||
i = batchEnd + 1
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
for _, batch in ipairs(batches) do
|
||||
local url = server_url .. "/api/framepack?" .. batch.urls[1]
|
||||
local body = textutils.serializeJSON({ urls = batch.urls })
|
||||
http.request({
|
||||
url = url,
|
||||
headers = { ["Content-Type"] = "application/json" },
|
||||
body = body,
|
||||
timeout = 2,
|
||||
binary = true
|
||||
})
|
||||
pendingRequests[url] = batch
|
||||
end
|
||||
|
||||
if next(batches) == nil then
|
||||
sleep(0.5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- HTTP 响应处理协程
|
||||
local function httpResponseHandler()
|
||||
while running do
|
||||
local event, url, handleOrErr = os.pullEvent()
|
||||
|
||||
if event == "http_success" then
|
||||
local batch = pendingRequests[url]
|
||||
if batch then
|
||||
pendingRequests[url] = nil
|
||||
local binData = handleOrErr.readAll()
|
||||
handleOrErr.close()
|
||||
|
||||
local success, batchFrames = pcall(unpackFramePack, binData)
|
||||
if success then
|
||||
for idx = 1, #batchFrames do
|
||||
local globalIdx = batch.start + idx - 1
|
||||
if not allFrameData[globalIdx] then
|
||||
allFrameData[globalIdx] = batchFrames[idx]
|
||||
end
|
||||
end
|
||||
print("[V] Cached batch: " .. batch.start .. " - " .. (batch.start + #batchFrames - 1))
|
||||
else
|
||||
print("[R] Unpack failed for batch " .. batch.start .. ": " .. tostring(batchFrames))
|
||||
for j = batch.start, batch.start + #batch.urls - 1 do
|
||||
downloadedFrames[j] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
elseif event == "http_failure" then
|
||||
local batch = pendingRequests[url]
|
||||
if batch then
|
||||
pendingRequests[url] = nil
|
||||
batch.retry = (batch.retry or 0) + 1
|
||||
if batch.retry < 3 then
|
||||
print("[R] Retrying batch " .. batch.start .. " (attempt " .. (batch.retry + 1) .. ")")
|
||||
for j = batch.start, batch.start + #batch.urls - 1 do
|
||||
downloadedFrames[j] = nil
|
||||
end
|
||||
else
|
||||
print("[X] Giving up on batch " .. batch.start)
|
||||
for j = batch.start, batch.start + #batch.urls - 1 do
|
||||
downloadedFrames[j] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 视频渲染协程(基于帧间隔的实时帧率显示)
|
||||
local function renderVideo()
|
||||
-- 等待音频准备就绪
|
||||
repeat
|
||||
sleep(0.05)
|
||||
until _G.audio_ready
|
||||
os.queueEvent("audio_start")
|
||||
|
||||
local frameDelay = 1 / videoInfo.fps
|
||||
local startTime = os.clock()
|
||||
local totalFrames = #videoInfo.frame_urls
|
||||
local textUpdateCounter = 0
|
||||
local lastFrameTime = os.clock()
|
||||
local stallCount = 0
|
||||
local lastFrameIndex = 0
|
||||
local lastFrameStart = nil -- 上一帧开始时间
|
||||
|
||||
while running and frameIndex <= totalFrames do
|
||||
local frame_start = os.clock()
|
||||
|
||||
-- 检查是否长时间没有新帧(可能卡住)
|
||||
if frame_start - lastFrameTime > 2.0 then
|
||||
print("[RENDER] Warning: No new frame for " .. string.format("%.2f", frame_start - lastFrameTime) .. "s, frameIndex=" .. frameIndex)
|
||||
end
|
||||
if frameIndex == lastFrameIndex then
|
||||
stallCount = stallCount + 1
|
||||
if stallCount > 10 then
|
||||
print("[RENDER] Frame index stuck at " .. frameIndex .. " for too long, forcing advance")
|
||||
frameIndex = frameIndex + 1
|
||||
stallCount = 0
|
||||
goto continue
|
||||
end
|
||||
else
|
||||
stallCount = 0
|
||||
end
|
||||
lastFrameIndex = frameIndex
|
||||
lastFrameTime = frame_start
|
||||
|
||||
-- 获取当前帧数据
|
||||
local data = allFrameData[frameIndex]
|
||||
if not data then
|
||||
-- 如果数据不存在,等待并重试,但不要无限等待
|
||||
local retry = 0
|
||||
while not data and retry < 20 and running do
|
||||
sleep(0.1)
|
||||
data = allFrameData[frameIndex]
|
||||
retry = retry + 1
|
||||
end
|
||||
if not data then
|
||||
print("[RENDER] Frame " .. frameIndex .. " data missing after retries, skipping")
|
||||
frameIndex = frameIndex + 1
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
-- 解码图像
|
||||
local imgBin = { data:byte(1, #data) }
|
||||
local success, image = pcall(gpu.decodeImage, table.unpack(imgBin))
|
||||
|
||||
if success and image then
|
||||
gpu.drawImage(0, 0, image.ref())
|
||||
|
||||
textUpdateCounter = textUpdateCounter + 1
|
||||
if textUpdateCounter % 5 == 1 then
|
||||
-- 计算基于帧间隔的播放帧率
|
||||
local play_fps = videoInfo.fps -- 默认值
|
||||
if lastFrameStart then
|
||||
local interval = frame_start - lastFrameStart
|
||||
if interval > 0 then
|
||||
play_fps = 1 / interval
|
||||
end
|
||||
end
|
||||
gpu.drawText(1, 1,
|
||||
frameIndex .. " / " .. totalFrames ..
|
||||
" fps: " .. string.format("%.1f", play_fps),
|
||||
0xffffff)
|
||||
end
|
||||
|
||||
-- 更新上一帧开始时间(必须在定时器之前,因为下一帧开始时间就是当前帧开始时间)
|
||||
lastFrameStart = frame_start
|
||||
|
||||
-- 等待下一帧(使用定时器,但设置超时保护)
|
||||
if frameIndex > 1 then
|
||||
local event, id
|
||||
local timerStarted = os.startTimer(frameDelay)
|
||||
-- 等待定时器事件,但最多等待 2 倍帧间隔
|
||||
local timeoutTimer = os.startTimer(frameDelay * 2)
|
||||
local gotTimer = false
|
||||
while not gotTimer and running do
|
||||
event, id = os.pullEvent()
|
||||
if event == "timer" and id == timerStarted then
|
||||
gotTimer = true
|
||||
elseif event == "timer" and id == timeoutTimer then
|
||||
print("[RENDER] Timer timeout, continuing anyway")
|
||||
gotTimer = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
gpu.sync()
|
||||
image:free()
|
||||
else
|
||||
print("[RENDER] Failed to decode frame " .. frameIndex .. ": " .. tostring(image))
|
||||
-- 跳过此帧
|
||||
end
|
||||
|
||||
-- 释放已渲染帧内存
|
||||
for i = 1, frameIndex - 3 do
|
||||
allFrameData[i] = nil
|
||||
end
|
||||
|
||||
-- 基于实际播放时间调整帧索引
|
||||
local elapsed = os.clock() - startTime
|
||||
local expectedFrame = math.floor(elapsed / frameDelay) + 1
|
||||
if expectedFrame > frameIndex + 10 then
|
||||
print("[RENDER] Skipping ahead from " .. frameIndex .. " to " .. expectedFrame)
|
||||
frameIndex = expectedFrame
|
||||
else
|
||||
frameIndex = frameIndex + 1
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
-- 视频结束,通知音频停止
|
||||
_G.Playopen = false
|
||||
running = false
|
||||
print("[RENDER] Render loop ended")
|
||||
end
|
||||
|
||||
-- 启动四个协程
|
||||
parallel.waitForAll(renderVideo, cacheAhead, httpResponseHandler, audioPlayer)
|
||||
|
||||
-- 播放结束统计
|
||||
local endtime = os.clock()
|
||||
local time = endtime - starttime1
|
||||
print("Playback finished")
|
||||
print("Play_Time: " .. time)
|
||||
local fps = totalFramesToPlay / time
|
||||
print("Final_FPS: " .. fps)
|
||||
print("Average Frame Interval: " .. 1/fps)
|
||||
324
speakerlib.lua
Normal file
324
speakerlib.lua
Normal file
@@ -0,0 +1,324 @@
|
||||
-- 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
|
||||
_G.Playstop = false
|
||||
_G.Playprint = false
|
||||
_G.setVolume = 1
|
||||
|
||||
-- 扬声器配置
|
||||
local speakerlist = {
|
||||
main = {},
|
||||
left = {},
|
||||
right = {}
|
||||
}
|
||||
|
||||
local function printlog(...)
|
||||
if _G.Playprint then
|
||||
print(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function loadSpeakerConfig()
|
||||
speakerlist = {
|
||||
main = { peripheral.find("speaker") },
|
||||
left = {},
|
||||
right = {}
|
||||
}
|
||||
|
||||
local speaker_groups = fs.open("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,{["Content-Type"] = "application/json"}, true)
|
||||
if not handle then
|
||||
error(url .. " Could not get duration: " .. (err or "Unknown error"))
|
||||
end
|
||||
|
||||
local data = handle.readAll()
|
||||
handle.close()
|
||||
|
||||
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 M = {}
|
||||
|
||||
function M.stop()
|
||||
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
|
||||
end
|
||||
|
||||
function M.play(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")
|
||||
end
|
||||
|
||||
-- 检查是否至少有一个音频URL
|
||||
if not mono_url and not left_url and not right_url then
|
||||
error("At least one audio URL is required")
|
||||
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,{["Content-Type"] = "application/json"}, true)
|
||||
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,{["Content-Type"] = "application/json"}, true)
|
||||
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,{["Content-Type"] = "application/json"}, true)
|
||||
if not right_httpfile then
|
||||
error("Could not open right audio stream")
|
||||
end
|
||||
end
|
||||
|
||||
-- 初始化DFPWM解码器(使用 require 加载)
|
||||
local dfpwm_module = require "cc.audio.dfpwm"
|
||||
local decoder = dfpwm_module.make_decoder()
|
||||
local left_decoder = dfpwm_module.make_decoder()
|
||||
local right_decoder = dfpwm_module.make_decoder()
|
||||
|
||||
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
|
||||
|
||||
-- 主播放循环
|
||||
_G.audio_ready = true
|
||||
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,{["Content-Type"] = "application/json"}, true)
|
||||
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,{["Content-Type"] = "application/json"}, true)
|
||||
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,{["Content-Type"] = "application/json"}, true)
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user