diff --git a/options/options.c b/options/options.c index 48baef13ff731..002cd1c624660 100644 --- a/options/options.c +++ b/options/options.c @@ -803,6 +803,7 @@ static const m_option_t mp_opts[] = { {"", OPT_SUBSTRUCT(video_equalizer, mp_csp_equalizer_conf)}, {"use-filedir-conf", OPT_BOOL(use_filedir_conf)}, + {"osd-lua", OPT_BOOL(lua_load_osd)}, {"osd-level", OPT_CHOICE(osd_level, {"0", 0}, {"1", 1}, {"2", 2}, {"3", 3})}, {"osd-on-seek", OPT_CHOICE(osd_on_seek, @@ -1016,6 +1017,7 @@ static const struct MPOpts mp_default_opts = { .osd_on_seek = 1, .osd_duration = 1000, #if HAVE_LUA + .lua_load_osd = false, .lua_load_osc = true, .lua_load_ytdl = true, .lua_ytdl_format = NULL, diff --git a/options/options.h b/options/options.h index 014e41de6d27e..f3030033ff409 100644 --- a/options/options.h +++ b/options/options.h @@ -182,6 +182,7 @@ typedef struct MPOpts { char **script_files; char **script_opts; bool js_memory_report; + bool lua_load_osd; bool lua_load_osc; bool lua_load_ytdl; char *lua_ytdl_format; diff --git a/player/core.h b/player/core.h index b5e2f09ac3e01..7fdfd1e50b52f 100644 --- a/player/core.h +++ b/player/core.h @@ -452,7 +452,7 @@ typedef struct MPContext { struct mp_ipc_ctx *ipc_ctx; - int64_t builtin_script_ids[9]; + int64_t builtin_script_ids[10]; mp_mutex abort_lock; diff --git a/player/lua.c b/player/lua.c index 8ecdeac410fe5..5c64d6c90d8b1 100644 --- a/player/lua.c +++ b/player/lua.c @@ -67,6 +67,9 @@ static const char * const builtin_lua_scripts[][2] = { }, {"mp.options", # include "player/lua/options.lua.inc" + }, + {"@osd.lua", +# include "player/lua/osd.lua.inc" }, {"@osc.lua", # include "player/lua/osc.lua.inc" diff --git a/player/lua/meson.build b/player/lua/meson.build index 72c92261fbaef..e9746321910b7 100644 --- a/player/lua/meson.build +++ b/player/lua/meson.build @@ -1,4 +1,4 @@ -lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua', +lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osd.lua', 'osc.lua', 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua', 'input.lua', 'fzy.lua', 'select.lua', 'positioning.lua', 'commands.lua', 'context_menu.lua'] diff --git a/player/lua/osd.lua b/player/lua/osd.lua new file mode 100644 index 0000000000000..381cb3ed14782 --- /dev/null +++ b/player/lua/osd.lua @@ -0,0 +1,725 @@ +local assdraw = require 'mp.assdraw' +local msg = require 'mp.msg' +local opt = require 'mp.options' + +-- ----------------------------------------------- +-- Customization Options +-- ----------------------------------------------- + +local user_opts = { + -- Time OSD stays on screen after any change + duration = 1.0, + + -- Bar geometry + bar_width = 0.75, -- bar width (as fraction of screen width) + bar_height = 0.04, -- bar height (as fraction of screen height) + bar_x_offset = 0, -- horizontal position for bar counting from center of screen (as fraction of screen width) + bar_y_pos = 0.75, -- vertical position for bar counting from top of screen (as fraction of screen height) + bar_radius = 0, -- corner radius of the bar (as fraction of screen diagonal) + round_fill_end = true, -- toggle for rounding fill bar right corners + show_bar_outline = true, -- toggle for bar outline + outline_gap = 0.003, -- gap between bar and it's outline (as fraction of screen diagonal) + marker_width = 0.005, -- marker width (as fraction of bar width) + marker_height = 0.2, -- marker height (as fraction of bar height) + + -- Colors + bar_body_color = "#FFFFFF", -- color of the bar's section that isn't filled + bar_body_border_color = "#000000", -- color of the outer border of the bar's section that isn't filled + bar_fill_color = "#FFFFFF", -- color of the bar's filled section + bar_fill_border_color = "#000000", -- color of the outer border of the bar's filled section + bar_outline_color = "#FFFFFF", -- color of the bar's outline + bar_outline_border_color= "#000000", -- color of the outer border of the bar's outline + text_color = "#FFFFFF", -- color of the text of the property that is being changed (eg: volume, seek, etc) + text_border_color = "#000000", -- color of the text's border + text_shadow_color = "#000000", -- color of the text's shadow + icon_color = "#FFFFFF", -- color of the icon of the property that is being changed + icon_border_color = "#000000", -- color of the icon's border + text_box_color = "#FFFFFF", -- color of the text's background + text_box_border_color = "#000000", -- color of the text's box + + -- Alphas (aka opacity) + -- goes from 0 (fully opaque) to 255 (fully transparent) + bar_body_alpha = 255, -- alpha of the bar's section that isn't filled + bar_body_border_alpha = 0, -- alpha of the outer border of the bar's section that isn't filled + bar_fill_alpha = 0, -- alpha of the bar's filled section + bar_fill_border_alpha = 0, -- alpha of the outer border of the bar's filled section + bar_outline_alpha = 0, -- alpha of the bar's outline + bar_outline_border_alpha= 0, -- alpha of the outer border of the bar's outline + icon_border_alpha = 0, -- alpha of the icon border + text_alpha = 0, -- opacity of text + text_border_alpha = 0, -- opacity of text outline + text_shadow_alpha = 0, -- opacity of text shadow + icon_alpha = 0, -- opacity of icon + text_box_alpha = 200, -- opacity of text background + text_box_border_alpha = 255, -- opacity of text background border + + -- Text + font_size = 0.030, -- text size (as fraction of screen diagonal) + font = "", -- optional text font name + label_x_offset = 0.02, -- horizontal offset for label counting from left of screen (as fraction of screen width) + label_y_offset = 0.02, -- vertical offset for label counting from top of screen (as fraction of screen height) + + text_border = 0.002, -- size of text outline (as fraction of screen diagonal) + text_shadow = 0.001, -- size of text shadow (as fraction of screen diagonal) + + text_box = false, -- toggle for background square behind label + label_box_size = 0.5, -- controls how wide the background box is compared to the text + label_box_pad_x = 0.04, -- horizontal padding inside the text background box (as fraction of screen width) + label_box_pad_y = 0.02, -- vertical padding inside the text background box (as fraction of screen height) + + text_center = false, -- align label with center of screen + + -- Icons + show_icons_label = false, -- toggle for showing icon at start of label + show_icons_bar = true, -- toggle for showing icon next to bar + icon_size = 0.055, -- size of icon next to bar (as fraction of screen diagonal) + distance_bar = 0.020, -- horizontal distance from bar (as fraction of screen width) + icon_y_offset = -0.005, -- vertical offset from bar level (as fraction of screen height) + icon_volume = "🔈", -- icon for Volume + icon_mute = "🔇", -- icon for Muting + icon_play = "▶", -- icon for Playing + icon_pause = "⏸", -- icon for Pausing + icon_seek = "⏩", -- icon for Seek + icon_brightness = "🔆", -- icon for Brightness + icon_saturation = "◑", -- icon for Saturation + icon_contrast = "◐", -- icon for Contrast + + bar_when_pausing = false, -- toggle for showing seek bar when pausing + + -- Feature on/off switches + show_volume = true, -- toggle for showing changes in Volume + show_seek = true, -- toggle for showing changes when seeking + show_pause = true, -- toggle for showing state when pausing/ playing + show_brightness = true, -- toggle for showing changes in Brightness + show_saturation = true, -- toggle for showing changes in Saturation + show_contrast = true, -- toggle for showing changes in Contrast +} + + +-- 3. Runtime state +local state = { + hide_timer = nil, +} + +-- ----------------------------------------------- +-- Utility Helpers +-- ----------------------------------------------- + +local function clamp(value, min_v, max_v) + if value < min_v then return min_v end + if value > max_v then return max_v end + return value +end + +local function to_ratio(value, max_value) + if max_value == 0 then return 0 end + return clamp(value / max_value, 0, 1) +end + +local function percent_to_ass_alpha(value) + value = clamp(value, 0, 255) + + local p = math.floor(value + 0.5) + + return string.format("&H%02X&", p) +end + +local function scale_value(value, scale) + return value * scale +end + +-- Build the label string, optionally with a leading icon +local function make_label(icon, text, ratio) + local icon_str = "" + if user_opts.show_icons_label and icon and icon ~= "" then + icon_str = icon .. " " + end + + if ratio == -1 then + return string.format("%s%s", icon_str, text) + else + return string.format("%s%s: %d%%", icon_str, text, math.floor(ratio * 100 + 0.5)) + end +end + + +-- Converts colors in HEX formatting to ASS/SSA formatting +local function osd_color_convert(color) + return "&H" .. color:sub(6,7) .. color:sub(4,5) .. color:sub(2,3) .. "&" +end + +local function validate_colors() + for key, value in pairs(user_opts) do + if type(value) == "string" and value:sub(1,1) == "#" then + if value:find("^#%x%x%x%x%x%x$") == nil then + msg.warn("'" .. value .. "' is not a valid color") + end + + -- if invalid, color will fallback to black, same as OSC behaviour + user_opts[key] = osd_color_convert(value) + end + end +end + + + +-- ----------------------------------------------- +-- OSD Helpers +-- ----------------------------------------------- + +local function clear_osd() + local w, h = mp.get_osd_size() + mp.set_osd_ass(w, h, "") +end + +local function schedule_clear() + if state.hide_timer then + state.hide_timer:kill() + end + + state.hide_timer = mp.add_timeout(user_opts.duration, clear_osd) +end + +-- ----------------------------------------------- +-- ASS Style Helpers +-- ----------------------------------------------- + +-- Primary color tag +local function ass_color(color) + return string.format("{\\c%s}", color) +end + +-- Alpha tag +local function ass_alpha(alpha) + return string.format("{\\1a%s}", percent_to_ass_alpha(alpha)) +end + +-- Font size tag +local function ass_fs(size) + return string.format("{\\fs%d}", size) +end + +local function ass_border(size) + return string.format("{\\bord%d}", size) +end + +local function ass_shadow(size) + return string.format("{\\shad%d}", size) +end + +local function ass_border_color(color) + return string.format("{\\3c%s}", color) +end + +local function ass_border_alpha(alpha) + return string.format("{\\3a%s}", percent_to_ass_alpha(alpha)) +end + +local function ass_shadow_color(color) + return string.format("{\\4c%s}", color) +end + +local function remove_border() + return "{\\bord0}" +end + +local function remove_shadow() + return "{\\shad0}" +end + +local function position_center() + return "{\\an5}" +end + +local function position_top_left() + return "{\\an7}" +end + +-- Optional font name tag +local function ass_font_tag() + if user_opts.font and user_opts.font ~= "" then + return string.format("{\\fn%s}", user_opts.font) + end + return "" +end + +-- Reset all overrides +local function ass_reset() + return "{\\r}" +end + +-- ----------------------------------------------- +-- Drawing Functions +-- ----------------------------------------------- + +local function draw_text_center(ass, s_diagonal, s_width, s_height, text, approx_w, approx_h) + + local s_box_pad_x = scale_value(user_opts.label_box_pad_x, s_width) + local s_box_pad_y = scale_value(user_opts.label_box_pad_y, s_height) + + -- background box + if user_opts.text_box then + ass:new_event() + ass:pos(s_width / 2, s_height / 2) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.text_box_color)) + ass:append(ass_alpha(user_opts.text_box_alpha)) + ass:append(ass_border_color(user_opts.text_box_border_color)) + ass:append(ass_border_alpha(user_opts.text_box_border_alpha)) + ass:append(remove_shadow()) + + ass:draw_start() + ass:rect_cw(-approx_w/2 - s_box_pad_x, -approx_h/2 - s_box_pad_y, approx_w/2 + s_box_pad_x, approx_h/2 + s_box_pad_y) + ass:draw_stop() + end + + -- text + ass:new_event() + ass:pos(s_width / 2, s_height / 2) + ass:append(ass_reset()) + ass:append(ass_font_tag()) + ass:append(ass_fs(scale_value(user_opts.font_size, s_diagonal))) + ass:append(ass_color(user_opts.text_color)) + ass:append(ass_alpha(user_opts.text_alpha)) + ass:append(position_center()) + ass:append(text) + +end + +local function draw_text(ass, s_diagonal, s_width, s_height, lx, ly, text, approx_w, approx_h) + + local s_box_pad_x = scale_value(user_opts.label_box_pad_x, s_width) + local s_box_pad_y = scale_value(user_opts.label_box_pad_y, s_height) + + -- background box + if user_opts.text_box then + ass:new_event() + ass:pos(0, 0) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.text_box_color)) + ass:append(ass_alpha(user_opts.text_box_alpha)) + ass:append(ass_border_color(user_opts.text_box_border_color)) + ass:append(ass_border_alpha(user_opts.text_box_border_alpha)) + ass:append(remove_shadow()) + ass:draw_start() + ass:rect_cw(lx - s_box_pad_x, ly - s_box_pad_y, lx + approx_w + s_box_pad_x, ly + approx_h + s_box_pad_y) + ass:draw_stop() + end + + -- text + ass:new_event() + ass:pos(lx, ly) + ass:append(ass_reset()) + ass:append(position_top_left()) + ass:append(ass_font_tag()) + ass:append(ass_fs(scale_value(user_opts.font_size, s_diagonal))) + ass:append(ass_color(user_opts.text_color)) + ass:append(ass_alpha(user_opts.text_alpha)) + ass:append(ass_border(scale_value(user_opts.text_border, s_diagonal))) + ass:append(ass_border_color(user_opts.text_border_color)) + ass:append(ass_shadow(scale_value(user_opts.text_shadow, s_diagonal))) + ass:append(ass_shadow_color(user_opts.text_shadow_color)) + ass:append(ass_border_color(user_opts.text_border_color)) + ass:append(ass_border_alpha(user_opts.text_border_alpha)) + ass:append(text) +end + +local function draw_label(ass, s_diagonal, s_width, s_height, lx, ly, icon, label, ratio) + + local text = make_label(icon, label, ratio) + + -- approximate text size + local approx_h = scale_value(user_opts.font_size, s_diagonal) + local approx_w = #text * approx_h * user_opts.label_box_size + + if user_opts.text_center then + draw_text_center(ass, s_diagonal, s_width, s_height, text, approx_w, approx_h) + else + draw_text(ass, s_diagonal, s_width, s_height, lx, ly, text, approx_w, approx_h) + end + +end + +local function draw_icon(ass, s_diagonal, s_width, s_height, x, y, icon) + if not (user_opts.show_icons_bar and icon and icon ~= "") then return end + + local v_icon_size = scale_value(user_opts.icon_size, s_diagonal) + + ass:new_event() + ass:pos(x - scale_value(user_opts.distance_bar, s_width) - v_icon_size / 2, y + scale_value(user_opts.bar_height, s_height) / 2 - v_icon_size / 2 + scale_value(user_opts.icon_y_offset, s_height)) + ass:append(ass_reset()) + ass:append(position_top_left()) + ass:append(ass_fs(v_icon_size)) + ass:append(ass_color(user_opts.icon_color)) + ass:append(ass_alpha(user_opts.icon_alpha)) + ass:append(ass_border_color(user_opts.icon_border_color)) + ass:append(ass_border_alpha(user_opts.icon_border_alpha)) + ass:append(icon) +end + +local function draw_outline(ass, s_diagonal, s_width, s_height, x, y) + if not user_opts.show_bar_outline then return end + + ass:new_event() + ass:pos(x, y) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.bar_outline_color)) + ass:append(ass_alpha(user_opts.bar_outline_alpha)) + ass:append(ass_border_color(user_opts.bar_outline_border_color)) + ass:append(ass_border_alpha(user_opts.bar_outline_border_alpha)) + + local v_bar_width = scale_value(user_opts.bar_width, s_width) + local v_bar_height = scale_value(user_opts.bar_height, s_height) + + local g = scale_value(user_opts.outline_gap, s_diagonal) + local r = clamp(scale_value(user_opts.bar_radius, v_bar_height), 0, v_bar_height / 2) + local x0 = -g + local y0 = -g + local x1 = v_bar_width + g + local y1 = v_bar_height + g + + ass:draw_start() + + -- outer path + if r > 0 and user_opts.round_fill_end then + ass:move_to(x0 + r, y0) + ass:line_to(x1 - r, y0) + ass:line_to(x1, y0 + r) + ass:line_to(x1, y1 - r) + ass:line_to(x1 - r, y1) + ass:line_to(x0 + r, y1) + ass:line_to(x0, y1 - r) + ass:line_to(x0, y0 + r) + elseif r > 0 then + ass:move_to(x0 + r, y0) + ass:line_to(x1, y0) + ass:line_to(x1, y1) + ass:line_to(x0 + r, y1) + ass:line_to(x0, y1 - r) + ass:line_to(x0, y0 + r) + else + ass:move_to(x0, y0) + ass:line_to(x1, y0) + ass:line_to(x1, y1) + ass:line_to(x0, y1) + end + + -- inner hole + if r > 0 and user_opts.round_fill_end then + ass:move_to(r, 0) + ass:line_to(0, r) + ass:line_to(0, v_bar_height - r) + ass:line_to(r, v_bar_height) + ass:line_to(v_bar_width - r, v_bar_height) + ass:line_to(v_bar_width, v_bar_height - r) + ass:line_to(v_bar_width, r) + ass:line_to(v_bar_width - r, 0) + elseif r > 0 then + ass:move_to(r, 0) + ass:line_to(0, r) + ass:line_to(0, v_bar_height - r) + ass:line_to(r, v_bar_height) + ass:line_to(v_bar_width, v_bar_height) + ass:line_to(v_bar_width, 0) + else + ass:move_to(0, 0) + ass:line_to(0, v_bar_height) + ass:line_to(v_bar_width, v_bar_height) + ass:line_to(v_bar_width, 0) + end + + ass:draw_stop() +end + +local function draw_bar_body(ass, s_diagonal, s_width, s_height, x, y) + + local v_bar_height = scale_value(user_opts.bar_height, s_height) + + ass:new_event() + ass:pos(x, y) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.bar_body_color)) + ass:append(ass_alpha(user_opts.bar_body_alpha)) + ass:append(ass_border_color(user_opts.bar_body_border_color)) + ass:append(ass_border_alpha(user_opts.bar_body_border_alpha)) + + ass:draw_start() + + local r = clamp(scale_value(user_opts.bar_radius, v_bar_height), 0, v_bar_height / 2) + + if r > 0 then + local bw, bh = scale_value(user_opts.bar_width, s_width), v_bar_height + ass:move_to(r, 0) + ass:line_to(bw - r, 0) + ass:line_to(bw, r) + ass:line_to(bw, bh - r) + ass:line_to(bw - r, bh) + ass:line_to(r, bh) + ass:line_to(0, bh - r) + ass:line_to(0, r) + else + ass:rect_cw(0, 0, scale_value(user_opts.bar_width, s_width), v_bar_height) + end + + ass:draw_stop() +end + +local function draw_fill(ass, s_diagonal, s_width, s_height, x, y, filled) + if filled <= 0 then return end + + local v_bar_height = scale_value(user_opts.bar_height, s_height) + + ass:new_event() + ass:pos(x, y) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.bar_fill_color)) + ass:append(ass_alpha(user_opts.bar_fill_alpha)) + ass:append(ass_border_color(user_opts.bar_fill_border_color)) + ass:append(ass_border_alpha(user_opts.bar_fill_border_alpha)) + + ass:draw_start() + + local r = clamp(scale_value(user_opts.bar_radius, v_bar_height), 0, v_bar_height / 2) + + if r > 0 then + local bh = v_bar_height + if user_opts.round_fill_end then + -- rounded on both left and right ends + ass:move_to(r, 0) + ass:line_to(filled - r, 0) + ass:line_to(filled, r) + ass:line_to(filled, bh - r) + ass:line_to(filled - r, bh) + ass:line_to(r, bh) + ass:line_to(0, bh - r) + ass:line_to(0, r) + else + -- rounded only on left, flat on right + ass:move_to(r, 0) + ass:line_to(filled, 0) + ass:line_to(filled, bh) + ass:line_to(r, bh) + ass:line_to(0, bh - r) + ass:line_to(0, r) + end + else + ass:rect_cw(0, 0, filled, v_bar_height) + end + + ass:draw_stop() +end + +local function draw_marker(ass, s_diagonal, s_width, s_height, x, y, marker_position) + + local v_bar_height = scale_value(user_opts.bar_height, s_height) + + local bord = scale_value(user_opts.text_border, s_diagonal) + local mark_x = x + (scale_value(user_opts.bar_width, s_width) * marker_position) + bord / 2 + local half_base = scale_value(user_opts.bar_width, s_width) * user_opts.marker_width + local mark_height = v_bar_height * user_opts.marker_height + + local bar_top = y + local bar_bottom = y + v_bar_height + + local g = scale_value(user_opts.outline_gap, s_diagonal) + + -- triangle to form V shape + ass:new_event() + ass:pos(0, 0) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.bar_body_border_color)) + ass:append(ass_alpha(user_opts.bar_body_border_alpha)) + ass:append(remove_border()) + ass:append(remove_shadow()) + ass:draw_start() + ass:move_to(mark_x - half_base - bord, bar_top) + ass:line_to(mark_x + half_base + bord, bar_top) + ass:line_to(mark_x, bar_top + mark_height + bord) + ass:move_to(mark_x - half_base - bord, bar_bottom) + ass:line_to(mark_x + half_base + bord, bar_bottom) + ass:line_to(mark_x, bar_bottom - mark_height - bord) + ass:draw_stop() + + -- triangle that hides the inside of the other triangle + ass:new_event() + ass:pos(0, 0) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.bar_outline_color)) + ass:append(ass_alpha(user_opts.bar_outline_alpha)) + ass:append(remove_border()) + ass:append(remove_shadow()) + ass:draw_start() + ass:move_to(mark_x - half_base, bar_top - g / 2) + ass:line_to(mark_x + half_base, bar_top - g / 2) + ass:line_to(mark_x, bar_top + mark_height) + ass:move_to(mark_x - half_base, bar_bottom + g / 2) + ass:line_to(mark_x + half_base, bar_bottom + g / 2) + ass:line_to(mark_x, bar_bottom - mark_height) + ass:draw_stop() +end + +local function draw_thin_marker(ass, s_diagonal, s_width, s_height, x, y, marker_position) + + local v_bar_height = scale_value(user_opts.bar_height, s_height) + + local bord = scale_value(user_opts.text_border, s_diagonal) + local mark_x = x + (scale_value(user_opts.bar_width, s_width) * marker_position) + bord / 2 + local mark_height = v_bar_height * user_opts.marker_height + local half_base = scale_value(user_opts.bar_width, s_width) * user_opts.marker_width + + local bar_top = y + local bar_bottom = y + v_bar_height + + ass:new_event() + ass:pos(0, 0) + ass:append(ass_reset()) + ass:append(ass_color(user_opts.bar_outline_color)) + ass:append(ass_alpha(user_opts.bar_outline_alpha)) + ass:append(ass_border_color(user_opts.bar_outline_color)) + ass:append(remove_border()) + ass:append(remove_shadow()) + ass:draw_start() + + ass:move_to(mark_x - half_base, bar_top) + ass:line_to(mark_x + half_base, bar_top) + ass:line_to(mark_x, bar_top + mark_height) + + ass:move_to(mark_x - half_base, bar_bottom) + ass:line_to(mark_x + half_base, bar_bottom) + ass:line_to(mark_x, bar_bottom - mark_height) + + ass:draw_stop() +end + +local function draw_bar(icon, label, value, min_value, max_value, marker_position) + local s_width, s_height = mp.get_osd_size() + local s_diagonal = math.sqrt(s_width * s_width + s_height * s_height) + local is_color_controls = (label == "Brightness" or label == "Saturation" or label == "Contrast") + + local ratio = 0 + if is_color_controls then + ratio = to_ratio(value + 100, math.abs(min_value) + max_value) + else + ratio = to_ratio(value, math.abs(min_value) + max_value) + end + + local filled = scale_value(user_opts.bar_width, s_width) * ratio + + local label_ratio = (label == "Volume" or is_color_controls) and (value / 100) or ratio + + local x = (s_width - scale_value(user_opts.bar_width, s_width)) / 2 + scale_value(user_opts.bar_x_offset, s_width) + local y = s_height * user_opts.bar_y_pos + local lx = scale_value(user_opts.label_x_offset, s_width) + local ly = scale_value(user_opts.label_y_offset, s_height) + + local ass = assdraw.ass_new() + + if label == "Paused" or label == "Playing" then + draw_label(ass, s_diagonal, s_width, s_height, lx, ly, icon, label, -1) + if not user_opts.bar_when_pausing then + mp.set_osd_ass(s_width, s_height, ass.text) + schedule_clear() + return + end + else + draw_label(ass, s_diagonal, s_width, s_height, lx, ly, icon, label, label_ratio) + end + + draw_icon(ass, s_diagonal, s_width, s_height, x, y, icon) + draw_outline(ass, s_diagonal, s_width, s_height, x, y) + draw_bar_body(ass, s_diagonal, s_width, s_height, x, y) + draw_fill(ass, s_diagonal, s_width, s_height, x, y, filled) + + if marker_position then + if user_opts.show_bar_outline then + draw_marker(ass, s_diagonal, s_width, s_height, x, y, marker_position) + else + draw_thin_marker(ass, s_diagonal, s_width, s_height, x, y, marker_position) + end + end + + mp.set_osd_ass(s_width, s_height, ass.text) + schedule_clear() +end + +-- ----------------------------------------------- +-- OSD Actions +-- ----------------------------------------------- + +local function show_volume() + if not user_opts.show_volume then return end + local volume = mp.get_property_number("volume", 0) + local muted = mp.get_property_bool("mute", false) + local icon = (muted or volume == 0) and user_opts.icon_mute or user_opts.icon_volume + draw_bar(icon, "Volume", volume, 0, 130, 100 / 130) +end + +local function show_seek() + if not user_opts.show_seek then return end + local time_pos = mp.get_property_number("time-pos", 0) + local duration = mp.get_property_number("duration", 0) + draw_bar(user_opts.icon_seek, "Seek", time_pos, 0, duration, nil) +end + +local function show_pause_state() + if not user_opts.show_pause then return end + local paused = mp.get_property_bool("pause", false) + local time_pos = mp.get_property_number("time-pos", 0) + local duration = mp.get_property_number("duration", 0) + + local icon = (paused) and user_opts.icon_pause or user_opts.icon_play + local label = (paused) and "Paused" or "Playing" + + draw_bar(icon, label, time_pos, 0, duration, nil) +end + +local function show_brightness() + if not user_opts.show_brightness then return end + local v = mp.get_property_number("brightness", 0) + draw_bar(user_opts.icon_brightness, "Brightness", v, -100, 100, 0.5) +end + +local function show_saturation() + if not user_opts.show_saturation then return end + local v = mp.get_property_number("saturation", 0) + draw_bar(user_opts.icon_saturation, "Saturation", v, -100, 100, 0.5) +end + +local function show_contrast() + if not user_opts.show_contrast then return end + local v = mp.get_property_number("contrast", 0) + draw_bar(user_opts.icon_contrast, "Contrast", v, -100, 100, 0.5) +end + + + +-- Disable C OSD +mp.set_property("osd-level", "0") + + +-- Read options from config file and command-line with callback possibility +opt.read_options(user_opts, "osd", function(changed) + validate_colors() +end) + +validate_colors() + + + + +-- ----------------------------------------------- +-- Hook to mpv +-- ----------------------------------------------- + +mp.observe_property("volume", "number", show_volume) +mp.observe_property("mute", "bool", show_volume) +mp.observe_property("pause", "bool", show_pause_state) +mp.observe_property("brightness", "number", show_brightness) +mp.observe_property("saturation", "number", show_saturation) +mp.observe_property("contrast", "number", show_contrast) + +mp.register_event("seek", show_seek) + +msg.info("Lua OSD loaded") + + diff --git a/player/scripting.c b/player/scripting.c index 6ed582f14265a..edba6a5eee433 100644 --- a/player/scripting.c +++ b/player/scripting.c @@ -271,6 +271,7 @@ void mp_load_builtin_scripts(struct MPContext *mpctx) load_builtin_script(mpctx, 6, mpctx->opts->lua_load_positioning, "@positioning.lua"); load_builtin_script(mpctx, 7, mpctx->opts->lua_load_commands, "@commands.lua"); load_builtin_script(mpctx, 8, mpctx->opts->lua_load_context_menu, "@context_menu.lua"); + load_builtin_script(mpctx, 9, mpctx->opts->lua_load_osd, "@osd.lua"); } bool mp_load_scripts(struct MPContext *mpctx) diff --git a/test/libmpv_test_file_loading_lua_osd.c b/test/libmpv_test_file_loading_lua_osd.c new file mode 100644 index 0000000000000..b53d4ec59e787 --- /dev/null +++ b/test/libmpv_test_file_loading_lua_osd.c @@ -0,0 +1,77 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include "libmpv_common.h" + +static void test_file_loading(char *file) +{ + const char *cmd[] = {"loadfile", file, NULL}; + command(cmd); + bool loaded = false; + bool finished = false; + bool c_osd_suppressed = false; + while (!finished) { + mpv_event *event = wrap_wait_event(); + switch (event->event_id) { + case MPV_EVENT_FILE_LOADED: + // make sure it loads before exiting + loaded = true; + + // make sure we're not using the C OSD and only the Lua one + if (!strcmp(mpv_get_property_string(ctx, "osd-level"), "0")) { + c_osd_suppressed = true; + } + + break; + case MPV_EVENT_END_FILE: + if (loaded) + finished = true; + break; + } + } + + if(!c_osd_suppressed) + fail("Failed to suppress the C OSD"); + + if (!finished) + fail("Unable to load test file!\n"); +} + +int main(int argc, char *argv[]) +{ + if (argc != 2) + return 1; + + ctx = mpv_create(); + if (!ctx) + return 1; + + atexit(exit_cleanup); + + mpv_set_option_string(ctx, "osd-lua", "yes"); + initialize(); + + const char *fmt = "================ TEST: %s ================\n"; + printf(fmt, "test_file_loading for Lua OSD"); + test_file_loading(argv[1]); + printf("================ SHUTDOWN ================\n"); + + command_string("quit"); + while (wrap_wait_event()->event_id != MPV_EVENT_SHUTDOWN) {} + + return 0; +} diff --git a/test/libmpv_test_lua_osd.c b/test/libmpv_test_lua_osd.c new file mode 100644 index 0000000000000..81c0fae0d6d97 --- /dev/null +++ b/test/libmpv_test_lua_osd.c @@ -0,0 +1,343 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include "libmpv_common.h" + + +struct osdTestValues { + char* initial_volume; + char* initial_mute; + char* started_paused; + char* initial_brightness; + char* initial_saturation; + char* initial_contrast; + + char* volume_after_change; + char* second_mute; + char* second_paused; + char* brightness_after_change; + char* saturation_after_change; + char* contrast_after_change; +}; + + +static void set_option_string(const char *name, const char *value) +{ + int ret = mpv_set_option_string(ctx, name, value); + if (ret < 0) + fail("mpv API error while setting option '%s' to '%s' (%s)\n", name, value, mpv_error_string(ret)); +} + + +static void free_osd_values(struct osdTestValues* C_osd_values, struct osdTestValues* Lua_osd_values) { + // Free everything + mpv_free(C_osd_values->initial_volume); + mpv_free(C_osd_values->initial_mute); + mpv_free(C_osd_values->started_paused); + mpv_free(C_osd_values->initial_brightness); + mpv_free(C_osd_values->initial_saturation); + mpv_free(C_osd_values->initial_contrast); + + mpv_free(C_osd_values->volume_after_change); + mpv_free(C_osd_values->second_mute); + mpv_free(C_osd_values->second_paused); + mpv_free(C_osd_values->brightness_after_change); + mpv_free(C_osd_values->saturation_after_change); + mpv_free(C_osd_values->contrast_after_change); + + mpv_free(Lua_osd_values->initial_volume); + mpv_free(Lua_osd_values->initial_mute); + mpv_free(Lua_osd_values->started_paused); + mpv_free(Lua_osd_values->initial_brightness); + mpv_free(Lua_osd_values->initial_saturation); + mpv_free(Lua_osd_values->initial_contrast); + + mpv_free(Lua_osd_values->volume_after_change); + mpv_free(Lua_osd_values->second_mute); + mpv_free(Lua_osd_values->second_paused); + mpv_free(Lua_osd_values->brightness_after_change); + mpv_free(Lua_osd_values->saturation_after_change); + mpv_free(Lua_osd_values->contrast_after_change); +} + + +static void test_c_osd(struct osdTestValues* C_osd_values) +{ + + C_osd_values->initial_volume = mpv_get_property_string(ctx, "volume"); + printf("Initial volume: %s\n", C_osd_values->initial_volume); + + C_osd_values->initial_mute = mpv_get_property_string(ctx, "mute"); + printf("Initial mute flag: %s\n", C_osd_values->initial_mute); + + C_osd_values->started_paused = mpv_get_property_string(ctx, "pause"); + printf("Started paused: %s\n", C_osd_values->started_paused); + + C_osd_values->initial_brightness = mpv_get_property_string(ctx, "brightness"); + printf("Initial brightness: %s\n", C_osd_values->initial_brightness); + + C_osd_values->initial_saturation = mpv_get_property_string(ctx, "saturation"); + printf("Initial saturation: %s\n", C_osd_values->initial_saturation); + + C_osd_values->initial_contrast = mpv_get_property_string(ctx, "contrast"); + printf("Initial contrast: %s\n", C_osd_values->initial_contrast); + + + + mpv_set_property_string(ctx, "volume", "60"); + C_osd_values->volume_after_change = mpv_get_property_string(ctx, "volume"); + printf("Volume after change: %s\n", C_osd_values->volume_after_change); + + mpv_set_property_string(ctx, "mute", "yes"); + C_osd_values->second_mute = mpv_get_property_string(ctx, "mute"); + printf("Mute flag after changing it: %s\n", C_osd_values->second_mute); + + mpv_set_property_string(ctx, "pause", "yes"); + C_osd_values->second_paused = mpv_get_property_string(ctx, "pause"); + printf("Paused after pause command: %s\n", C_osd_values->second_paused); + + mpv_set_property_string(ctx, "brightness", "-70"); + C_osd_values->brightness_after_change = mpv_get_property_string(ctx, "brightness"); + printf("Brightness after change: %s\n", C_osd_values->brightness_after_change); + + mpv_set_property_string(ctx, "saturation", "50"); + C_osd_values->saturation_after_change = mpv_get_property_string(ctx, "saturation"); + printf("Saturation after change: %s\n", C_osd_values->saturation_after_change); + + mpv_set_property_string(ctx, "contrast", "4"); + C_osd_values->contrast_after_change = mpv_get_property_string(ctx, "contrast"); + printf("Contrast after change: %s\n", C_osd_values->contrast_after_change); + + + +} + + + +static void test_lua_osd(struct osdTestValues* Lua_osd_values) +{ + + Lua_osd_values->initial_volume = mpv_get_property_string(ctx, "volume"); + printf("Initial volume: %s\n", Lua_osd_values->initial_volume); + + Lua_osd_values->initial_mute = mpv_get_property_string(ctx, "mute"); + printf("Initial mute flag: %s\n", Lua_osd_values->initial_mute); + + Lua_osd_values->started_paused = mpv_get_property_string(ctx, "pause"); + printf("Started paused: %s\n", Lua_osd_values->started_paused); + + Lua_osd_values->initial_brightness = mpv_get_property_string(ctx, "brightness"); + printf("Initial brightness: %s\n", Lua_osd_values->initial_brightness); + + Lua_osd_values->initial_saturation = mpv_get_property_string(ctx, "saturation"); + printf("Initial saturation: %s\n", Lua_osd_values->initial_saturation); + + Lua_osd_values->initial_contrast = mpv_get_property_string(ctx, "contrast"); + printf("Initial contrast: %s\n", Lua_osd_values->initial_contrast); + + + mpv_set_property_string(ctx, "volume", "60"); + Lua_osd_values->volume_after_change = mpv_get_property_string(ctx, "volume"); + printf("Volume after change: %s\n", Lua_osd_values->volume_after_change); + + mpv_set_property_string(ctx, "mute", "yes"); + Lua_osd_values->second_mute = mpv_get_property_string(ctx, "mute"); + printf("Mute flag after changing it: %s\n", Lua_osd_values->second_mute); + + mpv_set_property_string(ctx, "pause", "yes"); + Lua_osd_values->second_paused = mpv_get_property_string(ctx, "pause"); + printf("Paused after pause command: %s\n", Lua_osd_values->second_paused); + + mpv_set_property_string(ctx, "brightness", "-70"); + Lua_osd_values->brightness_after_change = mpv_get_property_string(ctx, "brightness"); + printf("Brightness after change: %s\n", Lua_osd_values->brightness_after_change); + + mpv_set_property_string(ctx, "saturation", "50"); + Lua_osd_values->saturation_after_change = mpv_get_property_string(ctx, "saturation"); + printf("Saturation after change: %s\n", Lua_osd_values->saturation_after_change); + + mpv_set_property_string(ctx, "contrast", "4"); + Lua_osd_values->contrast_after_change = mpv_get_property_string(ctx, "contrast"); + printf("Luaontrast after change: %s\n", Lua_osd_values->contrast_after_change); + + + + +} + +static void test_compare_values(struct osdTestValues* C_osd_values, struct osdTestValues* Lua_osd_values) +{ + + int number_of_matches = 0; + const char *matched = "%s: Matched.\n"; + const char *no_matched = "%s: DID NOT MATCH (C OSD = %s; Lua OSD = %s)\n"; + + if (strcmp(C_osd_values->initial_volume, Lua_osd_values->initial_volume)) { + printf(no_matched, "Initial volume", C_osd_values->initial_volume, Lua_osd_values->initial_volume); + } else { + printf(matched, "Initial volume"); + number_of_matches++; + } + + if (strcmp(C_osd_values->initial_mute, Lua_osd_values->initial_mute)) { + printf(no_matched, "Initial mute flag", C_osd_values->initial_mute, Lua_osd_values->initial_mute); + } else { + printf(matched, "Initial mute flag"); + number_of_matches++; + } + + if (strcmp(C_osd_values->started_paused, Lua_osd_values->started_paused)) { + printf(no_matched, "Started paused", C_osd_values->started_paused, Lua_osd_values->started_paused); + } else { + printf(matched, "Started paused"); + number_of_matches++; + } + + if (strcmp(C_osd_values->initial_brightness, Lua_osd_values->initial_brightness)) { + printf(no_matched, "Initial brightness", C_osd_values->initial_brightness, Lua_osd_values->initial_brightness); + } else { + printf(matched, "Initial brightness"); + number_of_matches++; + } + + if (strcmp(C_osd_values->initial_saturation, Lua_osd_values->initial_saturation)) { + printf(no_matched, "Initial saturation", C_osd_values->initial_saturation, Lua_osd_values->initial_saturation); + } else { + printf(matched, "Initial saturation"); + number_of_matches++; + } + + if (strcmp(C_osd_values->initial_contrast, Lua_osd_values->initial_contrast)) { + printf(no_matched, "Initial contrast", C_osd_values->initial_contrast, Lua_osd_values->initial_contrast); + } else { + printf(matched, "Initial contrast"); + number_of_matches++; + } + + + + if (strcmp(C_osd_values->volume_after_change, Lua_osd_values->volume_after_change)) { + printf(no_matched, "Volume after change", C_osd_values->volume_after_change, Lua_osd_values->volume_after_change); + } else { + printf(matched, "Volume after change"); + number_of_matches++; + } + + + if (strcmp(C_osd_values->second_mute, Lua_osd_values->second_mute)) { + printf(no_matched, "Mute flag after changing it", C_osd_values->second_mute, Lua_osd_values->second_mute); + } else { + printf(matched, "Mute flag after changing it"); + number_of_matches++; + } + + + if (strcmp(C_osd_values->second_paused, Lua_osd_values->second_paused)) { + printf(no_matched, "Paused after pause command", C_osd_values->second_paused, Lua_osd_values->second_paused); + } else { + printf(matched, "Paused after pause command"); + number_of_matches++; + } + + if (strcmp(C_osd_values->brightness_after_change, Lua_osd_values->brightness_after_change)) { + printf(no_matched, "Brightness after change", C_osd_values->brightness_after_change, Lua_osd_values->brightness_after_change); + } else { + printf(matched, "Brightness after change"); + number_of_matches++; + } + + if (strcmp(C_osd_values->saturation_after_change, Lua_osd_values->saturation_after_change)) { + printf(no_matched, "Saturation after change", C_osd_values->saturation_after_change, Lua_osd_values->saturation_after_change); + } else { + printf(matched, "Saturation after change"); + number_of_matches++; + } + + if (strcmp(C_osd_values->contrast_after_change, Lua_osd_values->contrast_after_change)) { + printf(no_matched, "Contrast after change", C_osd_values->contrast_after_change, Lua_osd_values->contrast_after_change); + } else { + printf(matched, "Contrast after change"); + number_of_matches++; + } + + + fflush(stdout); // to ensure correct order of prints + if (number_of_matches != 12) { + fail("\nSome values of Lua's OSD did not match the intended values from the C OSD.\n"); + } + + + free_osd_values(C_osd_values, Lua_osd_values); +} + + + + +int main(void) +{ + struct osdTestValues C_osd_values; + struct osdTestValues Lua_osd_values; + + + ctx = mpv_create(); + if (!ctx) + return 1; + + atexit(exit_cleanup); + + set_option_string("osd-lua", "no"); + initialize(); + + printf("================ TEST: Lua OSD ================\n"); + + printf("--------------- Running C OSD to register expected values ---------------\n"); + test_c_osd(&C_osd_values); + printf("--------------- Finished running C OSD ---------------\n"); + + command_string("quit"); + + while (wrap_wait_event()->event_id != MPV_EVENT_SHUTDOWN) {} + +// ----------------------------------------------------------------------------------------- + + ctx = mpv_create(); + if (!ctx) + return 1; + + atexit(exit_cleanup); + + set_option_string("osd-lua", "yes"); + initialize(); + + printf("--------------- Running Lua OSD to register values ---------------\n"); + test_lua_osd(&Lua_osd_values); + printf("--------------- Finished running Lua OSD ---------------\n"); + + command_string("quit"); + while (wrap_wait_event()->event_id != MPV_EVENT_SHUTDOWN) {} + + printf("================ SHUTDOWN ================\n"); + +// ----------------------------------------------------------------------------------------- + + printf("--------------- Comparing Lua OSD values with C OSD values ---------------\n"); + test_compare_values(&C_osd_values, &Lua_osd_values); + printf("--------------- Finished comparing values ---------------\n"); + + return 0; +} + diff --git a/test/meson.build b/test/meson.build index 156d2c433b8e4..a87928f2dc7da 100644 --- a/test/meson.build +++ b/test/meson.build @@ -122,6 +122,10 @@ if get_option('libmpv') include_directories: incdir, dependencies: libmpv_dep) test('libmpv-test-file-loading', exe, args: file, suite: 'libmpv') + exe = executable('libmpv-test-file-loading-lua-osd', 'libmpv_test_file_loading_lua_osd.c', + include_directories: incdir, dependencies: libmpv_dep) + test('libmpv-test-file-loading-lua-osd', exe, args: file, suite: 'libmpv') + exe = executable('libmpv-test-lavfi-complex', 'libmpv_test_lavfi_complex.c', include_directories: incdir, dependencies: libmpv_dep) test('libmpv-test-lavfi-complex', exe, args: file, suite: 'libmpv') @@ -130,6 +134,10 @@ if get_option('libmpv') include_directories: incdir, dependencies: libmpv_dep) test('libmpv-test-options', exe, suite: 'libmpv') + exe = executable('libmpv-test-lua-osd', 'libmpv_test_lua_osd.c', + include_directories: incdir, dependencies: libmpv_dep) + test('libmpv-test-lua-osd', exe, suite: 'libmpv') + # Old versions of ffmpeg are bugged when setting forced tracks and older # versions of meson don't support the custom version checking argument. if meson.version().version_compare('>= 1.5.0')