- clone this gist:
git clone https://gist.github.com/5af90be72b98bce36a3e.git mpv-url-overlay-example
- copy (or symlink)
url-overlay.lua
to your mpv scripts directory (~/.config/mpv/scripts
or~/.mpv/scripts
) mpv --no-osc url-overlay-example.mkv
- Hold
tab
to highlight all on-screen urls. If you already usetab
for something, bind some key to the commandmp_url_overlay_show
. - Click urls to launch them. On Linux
xdg-open
is required. Will not work if you haveMOUSE_BTN0
bound to another command.
Last active
December 6, 2020 01:32
-
-
Save torque/5af90be72b98bce36a3e to your computer and use it in GitHub Desktop.
mpv url-overlay example.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"resX": 1280, | |
"resY": 720, | |
"events": [ | |
{ | |
"start": 2, | |
"stop": 300, | |
"bounds": { | |
"r": 735, "t": 314, "l": 1084, "b": 408 | |
}, | |
"string": "https://github.com" | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local msg = require('mp.msg') | |
local log = { | |
debug = function(format, ...) | |
return msg.debug(format:format(...)) | |
end, | |
info = function(format, ...) | |
return msg.info(format:format(...)) | |
end, | |
warn = function(format, ...) | |
return msg.warn(format:format(...)) | |
end, | |
dump = function(item, ignore) | |
local level = 2 | |
if "table" ~= type(item) then | |
msg.info(tostring(item)) | |
return | |
end | |
local count = 1 | |
local tablecount = 1 | |
local result = { | |
"{ @" .. tostring(tablecount) | |
} | |
local seen = { | |
[item] = tablecount | |
} | |
local recurse | |
recurse = function(item, space) | |
for key, value in pairs(item) do | |
if not (key == ignore) then | |
if "table" == type(value) then | |
if not (seen[value]) then | |
tablecount = tablecount + 1 | |
seen[value] = tablecount | |
count = count + 1 | |
result[count] = space .. tostring(key) .. ": { @" .. tostring(tablecount) | |
recurse(value, space .. " ") | |
count = count + 1 | |
result[count] = space .. "}" | |
else | |
count = count + 1 | |
result[count] = space .. tostring(key) .. ": @" .. tostring(seen[value]) | |
end | |
else | |
if "string" == type(value) then | |
value = ("%q"):format(value) | |
end | |
count = count + 1 | |
result[count] = space .. tostring(key) .. ": " .. tostring(value) | |
end | |
end | |
end | |
end | |
recurse(item, " ") | |
count = count + 1 | |
result[count] = "}" | |
return msg.info(table.concat(result, "\n")) | |
end | |
} | |
local Bounds | |
do | |
local ASS | |
local _base_0 = { | |
scale = function(self, factor) | |
if 1 == factor then | |
return | |
end | |
self.l = self.l * factor | |
self.t = self.t * factor | |
self.r = self.r * factor | |
self.b = self.b * factor | |
end, | |
toASS = function(self) | |
ASS[2] = ([[%d %d l %d %d %d %d %d %d]]):format(self.l, self.t, self.r, self.t, self.r, self.b, self.l, self.b) | |
return table.concat(ASS) | |
end, | |
containsPoint = function(self, x, y) | |
return ((x >= self.l) and (y >= self.t) and (x < self.r) and (y < self.b)) | |
end | |
} | |
_base_0.__index = _base_0 | |
local _class_0 = setmetatable({ | |
__init = function(self, l, t, r, b) | |
self.l, self.t, self.r, self.b = l, t, r, b | |
if self.r < self.l then | |
self.l, self.r = self.r, self.l | |
end | |
if self.b < self.t then | |
self.t, self.b = self.b, self.t | |
end | |
end, | |
__base = _base_0, | |
__name = "Bounds" | |
}, { | |
__index = _base_0, | |
__call = function(cls, ...) | |
local _self_0 = setmetatable({}, _base_0) | |
cls.__init(_self_0, ...) | |
return _self_0 | |
end | |
}) | |
_base_0.__class = _class_0 | |
local self = _class_0 | |
self.fromBounds = function(self, bounds) | |
return self(bounds.l, bounds.t, bounds.r, bounds.b) | |
end | |
ASS = { | |
[[{\an7\pos(0,0)\bord3\3c&H0000FF&\1a&HFF&\p1}m ]], | |
[[]] | |
} | |
Bounds = _class_0 | |
end | |
local TimeRange | |
do | |
local _base_0 = { | |
timeInRange = function(self, time) | |
if time <= self.start then | |
return -1 | |
elseif time > self.finish then | |
return 1 | |
else | |
return 0 | |
end | |
end | |
} | |
_base_0.__index = _base_0 | |
local _class_0 = setmetatable({ | |
__init = function(self, start, finish) | |
self.start, self.finish = start, finish | |
end, | |
__base = _base_0, | |
__name = "TimeRange" | |
}, { | |
__index = _base_0, | |
__call = function(cls, ...) | |
local _self_0 = setmetatable({}, _base_0) | |
cls.__init(_self_0, ...) | |
return _self_0 | |
end | |
}) | |
_base_0.__class = _class_0 | |
TimeRange = _class_0 | |
end | |
local CoordinateTranslator | |
do | |
local _base_0 = { | |
windowPointToVideo = function(self, x, y) | |
local newX = (x - self.offsetX) / self.scaleX | |
local newY = (y - self.offsetY) / self.scaleY | |
return newX, newY | |
end, | |
windowBoundsToVideo = function(self, bounds) | |
local l = (bounds.l - self.offsetX) / self.scaleX | |
local t = (bounds.t - self.offsetY) / self.scaleY | |
local r = (bounds.r - self.offsetX) / self.scaleX | |
local b = (bounds.b - self.offsetY) / self.scaleY | |
return Bounds(l, t, r, b) | |
end, | |
unscaledMousePosition = function(self) | |
local x, y = mp.get_mouse_pos() | |
return x * self.mScaleX, y * self.mScaleY | |
end, | |
mouseOverVideo = function(self, x, y) | |
return (x >= self.offsetX) and (x < self.edgeX) and (y >= self.offsetY) and (y < self.edgeY) | |
end, | |
videoPointToWindow = function(self, x, y) | |
local newX = x * self.scaleX + self.offsetX | |
local newY = y * self.scaleY + self.offsetY | |
return newX, newY | |
end, | |
videoBoundsToWindow = function(self, bounds) | |
local l = bounds.l * self.scaleX + self.offsetX | |
local t = bounds.t * self.scaleY + self.offsetY | |
local r = bounds.r * self.scaleX + self.offsetX | |
local b = bounds.b * self.scaleY + self.offsetY | |
return Bounds(l, t, r, b) | |
end, | |
setMouseScale = function(self, winW, winH) | |
self.mScaleX = winW / self.osdResX | |
self.mScaleY = winH / self.osdResY | |
end, | |
update = function(self, winW, winH) | |
self:setMouseScale(winW, winH) | |
local ml, mt, mr, mb = mp.get_screen_margins() | |
local vidW = mp.get_property_number("video-params/dw", 1) | |
local vidH = mp.get_property_number("video-params/dh", 1) | |
local dispW = winW - (ml + mr) | |
local dispH = winH - (mt + mb) | |
self.scaleX = dispW / vidW | |
self.scaleY = dispH / vidH | |
self.offsetX = ml | |
self.offsetY = mt | |
self.edgeX = winW - mr | |
self.edgeY = winH - mb | |
end | |
} | |
_base_0.__index = _base_0 | |
local _class_0 = setmetatable({ | |
__init = function() end, | |
__base = _base_0, | |
__name = "CoordinateTranslator" | |
}, { | |
__index = _base_0, | |
__call = function(cls, ...) | |
local _self_0 = setmetatable({}, _base_0) | |
cls.__init(_self_0, ...) | |
return _self_0 | |
end | |
}) | |
_base_0.__class = _class_0 | |
CoordinateTranslator = _class_0 | |
end | |
local utils = require("mp.utils") | |
local open_osx | |
open_osx = function(urlString) | |
return utils.subprocess({ | |
args = { | |
"open", | |
urlString | |
} | |
}) | |
end | |
local open_windows | |
open_windows = function(urlString) | |
return utils.subprocess({ | |
args = { | |
"start", | |
urlString | |
} | |
}) | |
end | |
local open_linux | |
open_linux = function(urlString) | |
return utils.subprocess({ | |
args = { | |
"xdg-open", | |
urlString | |
} | |
}) | |
end | |
local URL | |
do | |
local clickAction, hoverASS | |
local _base_0 = { | |
toHoverASS = function(self, winW, winH) | |
hoverASS[2] = ("%g,%g"):format(10, winH - 10) | |
hoverASS[4] = self.urlString | |
return table.concat(hoverASS) | |
end, | |
click = function(self) | |
return clickAction(self.urlString) | |
end | |
} | |
_base_0.__index = _base_0 | |
local _class_0 = setmetatable({ | |
__init = function(self, start, stop, bounds, urlString) | |
self.bounds, self.urlString = bounds, urlString | |
self.time = TimeRange(start, stop) | |
end, | |
__base = _base_0, | |
__name = "URL" | |
}, { | |
__index = _base_0, | |
__call = function(cls, ...) | |
local _self_0 = setmetatable({}, _base_0) | |
cls.__init(_self_0, ...) | |
return _self_0 | |
end | |
}) | |
_base_0.__class = _class_0 | |
local self = _class_0 | |
clickAction = nil | |
if true then | |
if "Windows_NT" == os.getenv("OS") then | |
clickAction = open_windows | |
else | |
local uname = utils.subprocess({ | |
args = { | |
"uname", | |
"-s" | |
} | |
}) | |
if uname.stdout:match("Darwin") then | |
clickAction = open_osx | |
elseif uname.stdout:match("Linux") then | |
clickAction = open_linux | |
end | |
end | |
end | |
hoverASS = { | |
[[{\an1\fs40\\pos(]], | |
[[-100,-100]], | |
[[)}]], | |
[[]] | |
} | |
URL = _class_0 | |
end | |
local URLManager | |
do | |
local _base_0 = { | |
updateOSD = function(self, w, h, string) | |
self.translator.osdResX = w | |
self.translator.osdResY = h | |
self.translator:setMouseScale(w, h) | |
return mp.set_osd_ass(w, h, string) | |
end, | |
getUrlsForTime = function(self, time) | |
local startIndex = false | |
local endIndex = false | |
for x = self.lastIndex, #self.urls do | |
local url = self.urls[x] | |
local _exp_0 = url.time:timeInRange(time) | |
if 0 == _exp_0 then | |
if not (startIndex) then | |
startIndex = x | |
end | |
elseif -1 == _exp_0 then | |
endIndex = x - 1 | |
break | |
end | |
end | |
if startIndex and not endIndex then | |
endIndex = #self.urls | |
end | |
if startIndex then | |
self.lastIndex = startIndex | |
end | |
return startIndex, endIndex | |
end, | |
update = function(self) | |
local winW, winH = mp.get_screen_size() | |
local changed = self.urlBoxesShown ~= self.showUrlBoxes | |
if (winW ~= self.winW or winH ~= self.winH) then | |
changed = true | |
self.translator:update(winW, winH) | |
for x = 1, self.activeCount do | |
self.activeWindowBounds[x] = self.translator:videoBoundsToWindow(self.activeURLs[x].bounds) | |
end | |
self.winW, self.winH = winW, winH | |
end | |
if not self.paused then | |
self.currentTime = mp.get_property_number("time-pos", 0) | |
local startIndex, endIndex = self:getUrlsForTime(self.currentTime) | |
if startIndex and (startIndex ~= self.lastStart or endIndex ~= self.lastEnd) then | |
changed = true | |
self.lastStart, self.lastEnd = startIndex, endIndex | |
self.activeCount = endIndex - startIndex + 1 | |
self.activeURLs = { } | |
self.activeWindowBounds = { } | |
for x = 1, self.activeCount do | |
local url = self.urls[x + startIndex - 1] | |
self.activeURLs[x] = url | |
self.activeWindowBounds[x] = self.translator:videoBoundsToWindow(url.bounds) | |
end | |
elseif self.activeCount > 0 and not startIndex then | |
changed = true | |
self.activeCount = 0 | |
self.lastStart = false | |
end | |
end | |
local ass = { } | |
local hovered = self:handleHover() | |
if hovered then | |
self.hovered = true | |
table.insert(ass, hovered) | |
elseif self.hovered then | |
self.hovered = false | |
changed = true | |
end | |
if changed or self.hovered then | |
self.urlBoxesShown = self.showUrlBoxes | |
if self.showUrlBoxes then | |
for x = 1, self.activeCount do | |
table.insert(ass, self.activeWindowBounds[x]:toASS()) | |
end | |
end | |
return self:updateOSD(self.winW, self.winH, table.concat(ass, '\n')) | |
end | |
end, | |
handleHover = function(self) | |
local mX, mY = self.translator:unscaledMousePosition() | |
for x = self.activeCount, 1, -1 do | |
if self.activeWindowBounds[x]:containsPoint(mX, mY) then | |
return self.activeURLs[x]:toHoverASS(self.winW, self.winH) | |
end | |
end | |
return false | |
end, | |
handleClick = function(self, mX, mY) | |
for x = self.activeCount, 1, -1 do | |
if self.activeWindowBounds[x]:containsPoint(mX, mY) then | |
self.activeURLs[x]:click() | |
break | |
end | |
end | |
end | |
} | |
_base_0.__index = _base_0 | |
local _class_0 = setmetatable({ | |
__init = function(self, URLData) | |
self.urls = { } | |
self.showUrlBoxes = false | |
self.urlBoxesShown = false | |
self.currentTime = 0 | |
self.lastIndex = 1 | |
self.activeURLs = { } | |
self.activeWindowBounds = { } | |
self.activeCount = 0 | |
local vidW = mp.get_property_number("video-params/dw", 1) | |
local vidH = mp.get_property_number("video-params/dh", 1) | |
assert(vidW / vidH == URLData.resX / URLData.resY, "The video aspect ratio does not match that of the URLData. Dying.") | |
local scale = vidW / URLData.resX | |
local winW, winH = mp.get_screen_size() | |
self.translator = CoordinateTranslator() | |
self:updateOSD(winW, winH, "") | |
self.translator:update(winW, winH) | |
for x = 1, #URLData.events do | |
local url = URLData.events[x] | |
local bounds = Bounds:fromBounds(url.bounds) | |
bounds:scale(scale) | |
self.urls[x] = URL(url.start, url.stop, bounds, url.string) | |
end | |
local timer = mp.add_periodic_timer(0.05, (function() | |
local _base_1 = self | |
local _fn_0 = _base_1.update | |
return function(...) | |
return _fn_0(_base_1, ...) | |
end | |
end)()) | |
self.paused = mp.get_property_bool('pause', false) | |
mp.observe_property('pause', 'bool', function(event, paused) | |
self.paused = paused | |
end) | |
mp.add_key_binding('TAB', 'mp_url_overlay_show', function(event) | |
local _exp_0 = event.event | |
if "up" == _exp_0 then | |
self.showUrlBoxes = false | |
elseif "down" == _exp_0 then | |
self.showUrlBoxes = true | |
end | |
end, { | |
complex = true | |
}) | |
mp.add_key_binding("MOUSE_BTN0", "mp_url_overlay_click", function() | |
self:update() | |
local mX, mY = self.translator:unscaledMousePosition() | |
if self.translator:mouseOverVideo(mX, mY) then | |
return self:handleClick(mX, mY) | |
end | |
end) | |
return mp.register_event("seek", function() | |
local time = mp.get_property_number("time-pos", 0) | |
if time < self.currentTime then | |
self.currentTime = time | |
self.lastIndex = 1 | |
end | |
end) | |
end, | |
__base = _base_0, | |
__name = "URLManager" | |
}, { | |
__index = _base_0, | |
__call = function(cls, ...) | |
local _self_0 = setmetatable({}, _base_0) | |
cls.__init(_self_0, ...) | |
return _self_0 | |
end | |
}) | |
_base_0.__class = _class_0 | |
local self = _class_0 | |
self.fromJSON = function(self, json) | |
local result = utils.parse_json(json) | |
if result then | |
return self(result) | |
else | |
msg.warn("JSON parse error.") | |
return nil, "JSON parse error." | |
end | |
end | |
URLManager = _class_0 | |
end | |
local initDraw | |
initDraw = function() | |
mp.unregister_event(initDraw) | |
local videoPath = mp.get_property("path", "") | |
local jsonPath = videoPath:sub(1, -4) .. "json" | |
local suburls = io.open(jsonPath) | |
if not suburls then | |
log.warn("Could not find suburls: %s", jsonPath) | |
return | |
end | |
local json = suburls:read("*a") | |
suburls:close() | |
local manager = URLManager:fromJSON(json) | |
if not manager then | |
return log.warn("Failed to create URLManager. Malformed JSON?") | |
end | |
end | |
local fileLoaded | |
fileLoaded = function() | |
return mp.register_event('playback-restart', initDraw) | |
end | |
return mp.register_event('file-loaded', fileLoaded) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment