update
This commit is contained in:
@@ -1,25 +1,15 @@
|
||||
-- modified from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
|
||||
local msg = require('mp.msg')
|
||||
local utils = require("mp.utils")
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local INTERVAL = options.vf_fps and 0.01 or 0.001
|
||||
local osd_width, osd_height, pause = 0, 0, true
|
||||
local time_pos_observer_active = false
|
||||
local overlay = mp.create_osd_overlay('ass-events')
|
||||
|
||||
-- 提取 \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%.]+).*%)")
|
||||
local function realtime_position_text(event, pos, displayarea)
|
||||
if not event.move then
|
||||
local _, current_y = unpack(event.pos)
|
||||
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)
|
||||
@@ -28,9 +18,10 @@ local function parse_comment(event, pos, height, delay)
|
||||
end
|
||||
end
|
||||
|
||||
local x1, y1, x2, y2 = unpack(event.move)
|
||||
-- 计算移动的时间范围
|
||||
local duration = event.end_time - event.start_time --mean: options.scrolltime
|
||||
local progress = (pos - event.start_time - delay) / duration -- 移动进度 [0, 1]
|
||||
local progress = (pos - event.start_time) / duration -- 移动进度 [0, 1]
|
||||
|
||||
-- 计算当前坐标
|
||||
local current_x = tonumber(x1 + (x2 - x1) * progress)
|
||||
@@ -46,60 +37,28 @@ local function parse_comment(event, pos, height, delay)
|
||||
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 文件")
|
||||
function render(pos_arg)
|
||||
if COMMENTS == nil then return end
|
||||
|
||||
local pos, err
|
||||
if pos_arg == nil then
|
||||
pos, err = mp.get_property_number('time-pos')
|
||||
if err ~= nil then
|
||||
return msg.error(err)
|
||||
end
|
||||
else
|
||||
pos = pos_arg
|
||||
end
|
||||
|
||||
if not pos then
|
||||
overlay:remove()
|
||||
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 opacity = tonumber(options.opacity)
|
||||
local alpha = string.format("%02X", (1 - (opacity or 0)) * 255)
|
||||
|
||||
local width, height = 1920, 1080
|
||||
local ratio = osd_width / osd_height
|
||||
@@ -109,23 +68,37 @@ function render()
|
||||
end
|
||||
|
||||
local ass_events = {}
|
||||
local max_display = math.max(options.scrolltime, options.fixtime)
|
||||
local window_start = pos - max_display
|
||||
|
||||
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)
|
||||
-- 跳过已结束的弹幕
|
||||
local lo = binary_search(COMMENTS, window_start, function(item) return item.start_time end)
|
||||
|
||||
local re_entity = "&#%d+;"
|
||||
local re_fs = "\\fs(%d+)"
|
||||
local ass_prefix = string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}",
|
||||
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0")
|
||||
|
||||
for i = lo, #COMMENTS do
|
||||
local event = COMMENTS[i]
|
||||
if not event then break end
|
||||
|
||||
if event.start_time > pos then break end -- 后续弹幕提前退出
|
||||
if event.end_time >= pos then
|
||||
local text = realtime_position_text(event, pos, height * options.displayarea)
|
||||
if text then
|
||||
text = text:gsub("&#%d+;","")
|
||||
text = text:gsub(re_entity, "")
|
||||
end
|
||||
|
||||
if text and text:match("\\fs%d+") then
|
||||
text = text:gsub("\\fs(%d+)", function(size)
|
||||
return string.format("\\fs%d", size * 1.5)
|
||||
if text and text:match(re_fs) then
|
||||
text = text:gsub(re_fs, function(size)
|
||||
local n = tonumber(size) or 0
|
||||
return string.format("\\fs%d", math.floor(n * 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)
|
||||
local ass_text = text and (ass_prefix .. text)
|
||||
|
||||
table.insert(ass_events, ass_text)
|
||||
end
|
||||
@@ -137,27 +110,39 @@ function render()
|
||||
overlay:update()
|
||||
end
|
||||
|
||||
local timer = mp.add_periodic_timer(INTERVAL, render, true)
|
||||
local function time_pos_callback(_, time_pos)
|
||||
if time_pos then
|
||||
render(time_pos)
|
||||
else
|
||||
overlay:remove()
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
local function start_time_observer()
|
||||
if not time_pos_observer_active then
|
||||
mp.observe_property('time-pos', 'number', time_pos_callback)
|
||||
time_pos_observer_active = true
|
||||
end
|
||||
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()
|
||||
local function stop_time_observer()
|
||||
if time_pos_observer_active then
|
||||
mp.unobserve_property(time_pos_callback)
|
||||
time_pos_observer_active = false
|
||||
end
|
||||
end
|
||||
|
||||
function render_danmaku(from_menu, no_osd)
|
||||
if ENABLED and (from_menu or get_danmaku_visibility()) then
|
||||
if not no_osd then
|
||||
show_loaded(true)
|
||||
end
|
||||
end)
|
||||
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
|
||||
show_danmaku_func()
|
||||
else
|
||||
show_message("")
|
||||
hide_danmaku_func()
|
||||
end
|
||||
end
|
||||
|
||||
local function filter_state(label, name)
|
||||
@@ -172,9 +157,11 @@ local function filter_state(label, name)
|
||||
end
|
||||
|
||||
function show_danmaku_func()
|
||||
mp.set_property_bool(HAS_DANMAKU, true)
|
||||
set_danmaku_visibility(true)
|
||||
render()
|
||||
if not pause then
|
||||
timer:resume()
|
||||
start_time_observer()
|
||||
end
|
||||
if options.vf_fps then
|
||||
local display_fps = mp.get_property_number('display-fps')
|
||||
@@ -189,7 +176,9 @@ function show_danmaku_func()
|
||||
end
|
||||
|
||||
function hide_danmaku_func()
|
||||
timer:kill()
|
||||
stop_time_observer()
|
||||
mp.set_property_bool(HAS_DANMAKU, false)
|
||||
set_danmaku_visibility(false)
|
||||
overlay:remove()
|
||||
if filter_state("danmaku") then
|
||||
mp.commandv("vf", "remove", "@danmaku")
|
||||
@@ -221,33 +210,15 @@ 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()
|
||||
stop_time_observer()
|
||||
elseif COMMENTS ~= nil then
|
||||
timer:resume()
|
||||
start_time_observer()
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -263,7 +234,7 @@ end)
|
||||
|
||||
mp.add_hook("on_unload", 50, function()
|
||||
COMMENTS, DELAY = nil, 0
|
||||
timer:kill()
|
||||
stop_time_observer()
|
||||
overlay:remove()
|
||||
mp.set_property_native(DELAY_PROPERTY, 0)
|
||||
if filter_state("danmaku") then
|
||||
@@ -271,13 +242,10 @@ mp.add_hook("on_unload", 50, function()
|
||||
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")
|
||||
file1 = utils.join_path(DANMAKU_PATH, "temp-" .. PID .. ".mp4"),
|
||||
}
|
||||
|
||||
if options.save_danmaku and file_exists(files_to_remove.file2) then
|
||||
if options.save_danmaku then
|
||||
save_danmaku(true)
|
||||
end
|
||||
|
||||
@@ -287,10 +255,5 @@ mp.add_hook("on_unload", 50, function()
|
||||
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)
|
||||
Reference in New Issue
Block a user