This commit is contained in:
2026-03-27 07:06:16 +01:00
commit 1541961403
340 changed files with 151916 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
-- modified from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
local msg = require('mp.msg')
local utils = require("mp.utils")
local INTERVAL = options.vf_fps and 0.01 or 0.001
local osd_width, osd_height, pause = 0, 0, true
-- 提取 \move 参数 (x1, y1, x2, y2) 并返回
local function parse_move_tag(text)
-- 匹配包括小数和负数在内的坐标值
local x1, y1, x2, y2 = text:match("\\move%((%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
if x1 and y1 and x2 and y2 then
return tonumber(x1), tonumber(y1), tonumber(x2), tonumber(y2)
end
return nil
end
local function parse_comment(event, pos, height, delay)
local x1, y1, x2, y2 = parse_move_tag(event.text)
local displayarea = tonumber(height * options.displayarea)
if not x1 then
local current_x, current_y = event.text:match("\\pos%((%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
if not current_y or tonumber(current_y) > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\an8}%s", event.text)
else
return string.format("{\\an7}%s", event.text)
end
end
-- 计算移动的时间范围
local duration = event.end_time - event.start_time --mean: options.scrolltime
local progress = (pos - event.start_time - delay) / duration -- 移动进度 [0, 1]
-- 计算当前坐标
local current_x = tonumber(x1 + (x2 - x1) * progress)
local current_y = tonumber(y1 + (y2 - y1) * progress)
-- 移除 \move 标签并应用当前坐标
local clean_text = event.text:gsub("\\move%(.-%)", "")
if current_y > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\pos(%.1f,%.1f)\\an8}%s", current_x, current_y, clean_text)
else
return string.format("{\\pos(%.1f,%.1f)\\an7}%s", current_x, current_y, clean_text)
end
end
-- 从 ASS 文件中解析样式和事件
local function parse_ass_events(ass_path, callback)
local ass_file = io.open(ass_path, "r")
if not ass_file then
callback("无法打开 ASS 文件")
return
end
local events = {}
local time_tolerance = options.merge_tolerance
for line in ass_file:lines() do
if line:match("^Dialogue:") then
local start_time, end_time, style, text = line:match("Dialogue:%s*[^,]*,%s*([^,]*),%s*([^,]*),%s*([^,]*),[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,(.*)")
if start_time and end_time and text then
local event = {
start_time = time_to_seconds(start_time),
end_time = time_to_seconds(end_time),
style = style,
text = text:gsub("%s+$", ""),
clean_text = text:gsub("\\h+", " "):gsub("{[\\=].-}", ""):gsub("^%s*(.-)%s*$", "%1"),
pos = text:match("\\pos"),
move = text:match("\\move"),
}
table.insert(events, event)
end
end
end
table.sort(events, function(a, b)
return a.start_time < b.start_time
end)
ass_file:close()
callback(nil, events)
end
local overlay = mp.create_osd_overlay('ass-events')
function render()
if COMMENTS == nil then return end
local pos, err = mp.get_property_number('time-pos')
if err ~= nil then
return msg.error(err)
end
local delay = get_delay_for_time(DELAYS, pos)
local fontname = options.fontname
local fontsize = options.fontsize
local alpha = string.format("%02X", (1 - tonumber(options.opacity)) * 255)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
if width / height < ratio then
height = width / ratio
fontsize = options.fontsize - ratio * 2
end
local ass_events = {}
for _, event in ipairs(COMMENTS) do
if pos >= event.start_time + delay and pos <= event.end_time + delay then
local text = parse_comment(event, pos, height, delay)
if text then
text = text:gsub("&#%d+;","")
end
if text and text:match("\\fs%d+") then
text = text:gsub("\\fs(%d+)", function(size)
return string.format("\\fs%d", size * 1.5)
end)
end
-- 构建 ASS 字符串
local ass_text = text and string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}%s",
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0", text)
table.insert(ass_events, ass_text)
end
end
overlay.res_x = width
overlay.res_y = height
overlay.data = table.concat(ass_events, '\n')
overlay:update()
end
local timer = mp.add_periodic_timer(INTERVAL, render, true)
function parse_danmaku(ass_file_path, from_menu, no_osd)
parse_ass_events(ass_file_path, function(err, events)
COMMENTS = events
if err then
msg.error("ASS 解析错误: " .. err)
return
end
if ENABLED and (from_menu or get_danmaku_visibility()) then
if not no_osd then
show_loaded(true)
end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
show_danmaku_func()
else
show_message("")
hide_danmaku_func()
end
end)
end
local function filter_state(label, name)
local filters = mp.get_property_native("vf")
for _, filter in pairs(filters) do
if filter.label == label or filter.name == name
or filter.params[name] ~= nil then
return true
end
end
return false
end
function show_danmaku_func()
render()
if not pause then
timer:resume()
end
if options.vf_fps then
local display_fps = mp.get_property_number('display-fps')
local video_fps = mp.get_property_number('estimated-vf-fps')
if (display_fps and display_fps < 58) or (video_fps and video_fps > 58) then
return
end
if not filter_state("danmaku", "fps") then
mp.commandv("vf", "append", string.format("@danmaku:fps=fps=%s", options.fps))
end
end
end
function hide_danmaku_func()
timer:kill()
overlay:remove()
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
end
end
local message_overlay = mp.create_osd_overlay('ass-events')
local message_timer = mp.add_timeout(3, function()
message_overlay:remove()
end, true)
function show_message(text, time)
message_timer.timeout = time or 3
message_timer:kill()
message_overlay:remove()
local message = string.format("{\\an%d\\pos(%d,%d)}%s", options.message_anlignment,
options.message_x, options.message_y, text)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
if width / height < ratio then
height = width / ratio
end
message_overlay.res_x = width
message_overlay.res_y = height
message_overlay.data = message
message_overlay:update()
message_timer:resume()
end
mp.observe_property('osd-width', 'number', function(_, value) osd_width = value or osd_width end)
mp.observe_property('osd-height', 'number', function(_, value) osd_height = value or osd_height end)
mp.observe_property('display-fps', 'number', function(_, value)
if value ~= nil then
local interval = 1 / value / 10
if interval > INTERVAL then
timer:kill()
timer = mp.add_periodic_timer(interval, render, true)
if ENABLED then
timer:resume()
end
else
timer:kill()
timer = mp.add_periodic_timer(INTERVAL, render, true)
if ENABLED then
timer:resume()
end
end
end
end)
mp.observe_property('pause', 'bool', function(_, value)
if value ~= nil then
pause = value
end
if ENABLED then
if pause then
timer:kill()
elseif COMMENTS ~= nil then
timer:resume()
end
end
end)
mp.register_event('playback-restart', function(event)
if event.error then
return msg.error(event.error)
end
if ENABLED and COMMENTS ~= nil then
render()
end
end)
mp.add_hook("on_unload", 50, function()
COMMENTS, DELAY = nil, 0
timer:kill()
overlay:remove()
mp.set_property_native(DELAY_PROPERTY, 0)
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
end
local files_to_remove = {
file1 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".json"),
file2 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".ass"),
file3 = utils.join_path(DANMAKU_PATH, "temp-" .. PID .. ".mp4"),
file4 = utils.join_path(DANMAKU_PATH, "bahamut-" .. PID .. ".json")
}
if options.save_danmaku and file_exists(files_to_remove.file2) then
save_danmaku(true)
end
for _, file in pairs(files_to_remove) do
if file_exists(file) then
os.remove(file)
end
end
for _, source in pairs(DANMAKU.sources) do
if source.fname and source.from and source.from ~= "user_local" and file_exists(source.fname) then
os.remove(source.fname)
end
end
DANMAKU = {sources = {}, count = 1}
end)