Replies: 1 comment 4 replies
-
|
I have spent the last couple of days working on a (very early draft) Lua API that can be loaded by UI scripts to abstract away some of the more complex challenges of such an API, which I have been playing around with in this branch. It is just for testing purposes, so mpv isn't configured to compile with it, and there is no JavaScript version, but it is working and can be tested by manually loading thumbnail.lua. I have configured a version of osc.lua to use the API to perform the equivalent of your patches in #17518. I have also configured a version of thumbfast to make use of the API for testing purposes. So far I have currently only tested using this API to replicate the behaviour of the "active" model in #17518. This is because thumbfast's code architecture is not designed to allow multiple requests at once, and I haven't had the time to try and add support for that. The API, as implemented, should allow such thumbnailers to be written, we just need to spend the time to do so, though it's possible that we'll realise a flaw in the design once we actually try. For discussion's sake, and to report what I've learned, I'll briefly go through the API, compare it with the considerations and minimum requirements you've presented. Obviously this may not be the direction we go with the API, but even then, it should be useful for testing and experimentation. Especially relevant pings: @N-R-K @po5. Edit: this ended up not being brief at all, sorry if it's too long. The Async API
I figure that for a first draft, we want the API to guarantee one response for each request. To guarantee that, we can do something like function thumbnail.generate(opts, cb)
opts.id = opts.id or ''
local thumbnailer = choose_thumbnailer(opts.path, opts.t)
if not thumbnailer then
return false
end
-- registers a unique script message to handle the reply
local response_handler = register_response_handler(opts, cb)
mp.commandv('script-message-to', thumbnailer, 'generate-thumbnail', utils.format_json({
t = opts.t, w = opts.w, h = opts.h,
path = opts.path, id = mp.get_script_name()..'/'..(opts.id or ''),
client_name = mp.get_script_name(),
response_handler = response_handler,
}))
return true
endThere is an extra
Currently I'm returning all of this except the
Stride seems to always be w*4 right? So it doesn't really seem necessary. If any non-bgra format support (with a different stride calculation) were added to local function register_response_handler(opts, cb)
local handler_id = mp.get_script_name().."/"..handle_counter
handle_counter = handle_counter + 1
mp.register_script_message(handler_id, function (response, err)
mp.unregister_script_message(handler_id)
response = utils.parse_json(response or '')
if not response then
return cb(nil, err)
end
local thumb = {
w = response.w,
h = response.h,
_thumbnail = response.thumbnail,
_status = 'available',
_id = opts.id,
_uid = handler_id
}
unfreed_thumbnails[handler_id] = thumb
cb(setmetatable(thumb, thumbnail_mt), err)
end)
return handler_id
endI don't know about these error messages, I suspect they won't be useful. Cancelling requests
I haven't implemented this (thumbfast returns thumbnails so fast it hasn't been noticeable), but it wouldn't be too difficult. In the context of this API, it probably makes more sense to cancel all requests of a particular local uid = thumbnail.generate({...})
thumbnail.abort(uid)
-- or:
uid:abort()It's worth remembering though that aborted requests may still receive responses, if the responses were sent before the thumbnailer received the abort command. That is something that we ourselves could detect and block if we wished. Resource ManagementThumbnail Data Transfer
So far, I have been experimenting with doing memory management on the UI side; I have provided a method to free the data (calling thumbnail.generate({id = 2, t = math.random(0, mp.get_property_number('duration') or 0)}, function(thumb)
if not thumb then return end
thumb:draw({x = 500, y = 300})
thumb:free()
end)I had to modify thumbfast to write each thumbnail to a new temporary file (using Additional Considerationsoverlay-idsOne consideration that has not been mentioned is how to handle overlay-id conflicts. In the passive model, each UI script is drawing its own overlays, so there is a risk of multiple selecting the same ids and interfering with each other. Unless each client is given its own id namespace in the future (#17534), this is an issue for all thumbnail users. Currently, to avoid overlay id conflicts, I see three options:
This third option seems the simplest and is the one I've implemented it in the mp.thumbnail API: -- Assigns overlay ids in such a way as to minimise the risk of conflicts with
-- other scripts. If the overlay ids were ever made client-specific, this function
-- could be simplified.
local function assign_overlay_id(thumb)
if overlay_ids[thumb._id] then
local reservation = mp.get_property_native('user-data/mpv/overlay-ids/'..overlay_ids[thumb._id])
if reservation == mp.get_script_name() then
return true
else
overlay_ids[thumb._id] = nil
end
end
for i = OVERLAY_ID_MIN, OVERLAY_ID_MAX, 1 do
local overlay_reservation = 'user-data/mpv/overlay-ids/'..i
if not mp.get_property(overlay_reservation) then
-- This is intended to (as far as possible) avoid two scripts from setting the same
-- overlay id reservation at the same time. Still needs testing to determine if it works.
mp.commandv('expand-properties', 'set', overlay_reservation,
('${%s:%s}'):format(overlay_reservation, mp.get_script_name()))
if mp.get_property_native(overlay_reservation) == mp.get_script_name() then
overlay_ids[thumb._id] = i
return true
end
end
end
-- If we reach here then we ran out of available overlay IDs
return false
endI am calling this for every However, I suspect (though have not been able to confirm this in practice) that this operation is racy; if two scripts try and set the same id at the exact same time, they might end up reserving the same id, which is why I tried to set the property in a strange way and why I've got guard clauses on both sides of the assignment. If we were to do this, it might require some testing. Multiple ThumbnailersAnother consideration that was brought up in the previous thread was support for multiple thumbnailers. I still think it is a good idea, and I have been experimenting with an interface for doing so. There are a few ways of doing it that I've considered, but this post is getting very long and I'm getting tired, so I'll just talk about the method I actually implemented. Essentially, I have configured it so that the UI script chooses a valid thumbnailer for the specific thumbnail being requested. This can all be done internally by the mp.set_property_native('user-data/mpv/thumbnailers/'..mp.get_script_name(), {
priority = 50,
paths = {
"^/", -- linux root
"^[A--Z]:/", -- windows drives
"^https?://youtube.com/",
"^https?://[^/]+.youtube.com/", -- any youtube.com subdomain
"^https?://yt.be/"
},
ranges = {
{0, 60},
{600, 99999}
},
current_file_only = true,
})The priority value allows
This has actually worked very well in my testing. Another advantage of this, is that it can provide some immediate (synchronous) feedback to the UI script about whether to expect a thumbnail by returning String Pattern ProblemsHowever, there is one big challenge to this, and that is Lua patterns and JavaScript regex do not use the same syntax and are not compatible with each other. So to handle this we would need to agree on some sort of custom/cut down version of either lua patterns or regex (or a hybrid or something else entirely) and convert it into a valid Lua pattern in Lua clients, and a valid regex pattern for javascript clients (or implement a custom parser but that's waaaaay out of scope). I spent some time on this, since I wanted to make sure it was possible to avoid going down a dead end, and it is actually possible to take a cut-down version of regex, and have it work on both lua and js after some processing, without it being too complex, code-wise (javascript version is identical in length): for i, path in ipairs(config.paths or {}) do
-- We need to use a custom pattern format that can work in both Lua and JS
config.paths[i] = path:lower()
:gsub("\\[\\%-%*%?%[%]]", { -- escape (our) special chars
["\\\\"] = "\0a", ["\\-"] = "\0b",
["\\*"] = "\0c", ["\\?"] = "\0d",
["\\["] = "\0e", ["\\]"] = "\0f",
["\\^"] = "\0g", ["\\$"] = "\0h",
["\\+"] = "\0i"
})
:gsub("\\", "") -- remove backslashes from anywhere else
:gsub("([%(%)%%%.%-])", "%%%1") -- escape lua special chars
:gsub("%%%-%%%-", "-")
:gsub("%*%?", "-")
:gsub("([+?])%?", "%1%%?")
:gsub("%[^%]", ".")
:gsub("%z%a", { -- Re-add our escaped characters
["\0a"] = "\\", ["\0b"] = "%-",
["\0c"] = "%*", ["\0d"] = "%?",
["\0e"] = "%[", ["\0f"] = "%]",
["\0g"] = "%^", ["\0h"] = "%$",
["\0i"] = "%+"
})
endSo it's possible, and it doesn't take all that much code to implement, but is it actually something we would want to do for an API like this? "Active" API WrapperAnother thing I mentioned on the previous thread was that you could actually provide a wrapper API to achieve the "draw here" behaviour of the "active" model using the "passive" model on the backend. I wanted to test this and compare it with the active model, as I figured it would be a good way of testing the performance hit/latency of the passive model. I initially wrote something that looked like this: -- If a client clears the thumbnail, we don't want an in-transit thumbnail
-- response to redraw the image.
local blocked_draws = {}
function thumbnail.draw(opts)
opts.id = opts.id or ''
if not opts.x and not opts.y then
clear_thumbnail(opts.id)
blocked_draws[opts.id] = true
return
end
blocked_draws[opts.id] = false
return thumbnail.generate(opts, function(thumb)
if not thumb then return end
if not blocked_draws[opts.id] then
thumb:draw(opts)
end
thumb:free()
end)
endAnd I tested it against the thumbfast branch of the OSC, which uses the base thumbfast API. Interestingly I was noticing major reductions in perceived latency of the image, the thumbfast version followed my mouse really smoothly, while this version was always a little behind. I did various tests and eventually realised that the difference was that thumbfast was updating the position of existing thumbnails while generating new ones, so I modified the draw API to cache the latest thumbnail and do this as well with an additional option: -- If a client clears the thumbnail, we don't want an in-transit thumbnail
-- response to redraw the image.
local blocked_draws = {}
local latest_coords = {}
local latest_thumbnails = {}
function thumbnail.draw(opts)
opts.id = opts.id or ''
if not opts.x and not opts.y then
clear_thumbnail(opts.id)
blocked_draws[opts.id] = true
latest_coords[opts.id] = nil
if latest_thumbnails[opts.id] then
latest_thumbnails[opts.id]:free()
latest_thumbnails[opts.id] = nil
end
return
end
blocked_draws[opts.id] = false
latest_coords[opts.id] = {x = opts.x, y = opts.y}
if opts.redraw_immediately and latest_thumbnails[opts.id] then
latest_thumbnails[opts.id]:draw({
x = opts.x, y = opts.y,
w = opts.w, h = opts.h,
})
end
return thumbnail.generate(opts, function(thumb)
if not thumb then return end
if latest_thumbnails[opts.id] then
latest_thumbnails[opts.id]:free()
end
latest_thumbnails[opts.id] = thumb
if not blocked_draws[opts.id] then
thumb:draw({
x = opts.redraw_immediately and latest_coords[opts.id].x or opts.x,
y = opts.redraw_immediately and latest_coords[opts.id].y or opts.y,
w = opts.w, h = opts.h,
})
end
end)
endOnce I made this change, there was no perceivable difference between thumbfast and this API. In addition, since the draw call still happens on the UI side, it is actually properly in sync with the rendered border: if thumbnail.draw({
t = mp.get_property_number("duration", 0) * (sliderpos / 100),
w = math.floor(thumb_w + 0.5), h = math.floor(thumb_h + 0.5),
x = math.floor(thumb_x + 0.5), y = math.floor(thumb_y + 0.5),
redraw_immediately = true,
}) then
elem_ass:new_event()
elem_ass:pos(thumb_x * r_w, thumb_y * r_h)
elem_ass:an(7)
elem_ass:append(osc_styles.timePosBar)
elem_ass:append("{\\1a&H20&}")
elem_ass:draw_start()
elem_ass:rect_cw(-thumb_pad * r_w, -thumb_pad * r_h, (thumb_w+ thumb_pad) * r_w, (thumb_h + thumb_pad) * r_h)
elem_ass:draw_stop()
else
thumbnail.draw({})
endRemaining ConsiderationsObviously, the key advantages of the "passive" model (multiple requests simultaneously, different files) remains untested. So the next steps will be to modify some thumbnail scripts to use this API and see how it works in practice. I'll be out of town for a few days though, so I won't be able to make any progress for at least a week. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Moving the discussion from #17518 to not pollute the PR.
TL;DR a passive model where a client can request a thumbnail and the thumbnailer simply sends it back (asynchronously) rather than drawing the thumbnail itself would be better as it would:
At a minimum such api should support the following "inputs":
And reply back with the following:
Depending on the storage used to send the thumbnail (e.g filesystem) there might be some need for the client to inform the thumbnailer when it's done with the thumbnail and the thumbnailer is free to remove the thumbnail (if it wishes).
Additionally, you'd want to cancel in-flight requests as well, e.g user changed hover position so the old request is not needed anymore. (Note that the thumbnailer might still end up generating the thumbnail anyways, for caching purposes or otherwise, point is to inform the thumbnailer so that it can avoid generating the thumbnail if it doesn't need/want to).
Considerations
fdandmemory addressbut these are not easy (?) to deal with in lua.Testing
Because this would be a new api (as opposed to #17518 which is based on thumbfast api) this would require implementing it on 3rd party scripts first and refining the api based on real world usage before it can be considered for inclusion in mpv.
From the UI side, I am willing to have branches in mfpbar for testing purposes.
From non UI usecases, porting scripts like mpv-gallery-view to such api could provide good insights.
As for thumbnailer, we can either modify thumbfast, the yt-thumbnailer in #17518 or mpv_thumbnail_script.
Feel free to add anything else I missed here.
Beta Was this translation helpful? Give feedback.
All reactions