Skip to content

Instantly share code, notes, and snippets.

@torque
Last active December 6, 2020 01:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save torque/5af90be72b98bce36a3e to your computer and use it in GitHub Desktop.
Save torque/5af90be72b98bce36a3e to your computer and use it in GitHub Desktop.
mpv url-overlay example.

Instructions

  1. clone this gist: git clone https://gist.github.com/5af90be72b98bce36a3e.git mpv-url-overlay-example
  2. copy (or symlink) url-overlay.lua to your mpv scripts directory (~/.config/mpv/scripts or ~/.mpv/scripts)
  3. mpv --no-osc url-overlay-example.mkv
  4. Hold tab to highlight all on-screen urls. If you already use tab for something, bind some key to the command mp_url_overlay_show.
  5. Click urls to launch them. On Linux xdg-open is required. Will not work if you have MOUSE_BTN0 bound to another command.
{
"resX": 1280,
"resY": 720,
"events": [
{
"start": 2,
"stop": 300,
"bounds": {
"r": 735, "t": 314, "l": 1084, "b": 408
},
"string": "https://github.com"
}
]
}
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