├── README.md └── mpv ├── lua-settings └── thumbs.conf └── scripts ├── blank.bgra ├── thumbgen.lua └── thumbs.lua /README.md: -------------------------------------------------------------------------------- 1 | # mpv-thumbPreview 2 | mpv thumbnail generator script [windows] 3 | 4 | Generates thumbnail previews for mpv. 5 | 6 | ![Alt text](https://i.imgur.com/SGxtLps.png "Screenshot") 7 | 8 | Tested with shinchiro's compiled mpv only.[mpv](https://sourceforge.net/projects/mpv-player-windows/files/64bit/) 9 | 10 | If compiling mpv, requires luajit module. 11 | 12 | # [Installation] 13 | Clone or download the repo and extract to ~/mpv installation folder. 14 | 15 | # [Info] 16 | Blank.bgra - placeholder 'loading' thumbnail. 17 | 18 | thumbgen.lua - ffmpeg generator script. It uses mp.commandv("script-message-to") to pipe the stdout to thumbs.lua in a non-blocking manner. 19 | 20 | thumbs.lua - main viewer script which handles the input and general behaviour. 21 | 22 | # [Usage] 23 | 24 | By default the thumbnails should be generated anytime a video is shown. However this behaviour can be changed by editing the global variable inside thumbs.lua or by setting them in `lua-settings\thumbs.conf` in your mpv user folder. 25 | 26 | [thumbdir] --The global thumbnail folder. [Only if cache is set to true] 27 | [thumb_width] --thumbnail width. Aspect ratio automatically applied. 28 | [offset_from_seekbar] --The offset y from the seekbar. 29 | [y_offset] --Thumbnail y-pos offset. 30 | [timespan] --The amount of thumbs to be created. IE, every 20 seconds. 31 | [minTime] --The minimum time needed in order to check for thumbs. We don't want thumbnails being created on files less than 5 minutes for example. 32 | [auto] --If true, will automatically create thumbs everytime a video is open. If false, a key will have to be pressed to start the generation. True by default. 33 | [cache] --If true, thumbs will be saved inside the 'thumbdir' so that they do not need to be created again. If false, thumbs will only persist in mpv's memory. If you set this to true, then you must change the default placeholder thumbdir var. False by default. 34 | 35 | 36 | # [Known-Bugs] 37 | - no-keepaspect-window works but has issues at extreme scales. 38 | - cache=true is still wip 39 | - Some videos with odd native resolutions break the mouse region. will be fixed next update 40 | - Doesn't support dragging new videos into the player yet. 41 | 42 | # [ChangeLog] 43 | 0.1 - Initial release 44 | 0.2 - Mouse region improved. 45 | 0.3 - Mouse region adjusted for no-keepaspect-window. 46 | 0.4 - Using built-in mpv ffmpeg. Global settings now applied from thumbs.conf. Timer added for thumb generation 47 | -------------------------------------------------------------------------------- /mpv/lua-settings/thumbs.conf: -------------------------------------------------------------------------------- 1 | #[thumbdir] --The global thumbnail folder. [Only if cache is set to true] 2 | #[thumb_width] --thumbnail width. Aspect ratio automatically applied. 3 | #[offset_from_seekbar] --The offset y from the seekbar. 4 | #[y_offset] --Thumbnail y-pos offset. 5 | #[timespan] --The amount of thumbs to be created. IE, every 20 seconds. 6 | #[minTime] --The minimum time needed in order to check for thumbs. We don't want thumbnails being created on files less than 5 minutes for example. 7 | #[auto] --If true, will automatically create thumbs everytime a video is open. If false, a key will have to be pressed to start the generation. True by default. 8 | #[cache] --If true, thumbs will be saved inside the 'thumbdir' so that they do not need to be created again. If false, thumbs will only persist in mpv's memory. If you set this to true, then you must change the default placeholder thumbdir var. False by default. 9 | 10 | 11 | thumbdir="D:\\\\mpv\\\\cache\\\\" 12 | thumb_width=150 13 | offset_from_seekbar=120 14 | y_offset=0 15 | timespan=20 16 | minTime=300 17 | auto=yes 18 | cache=no -------------------------------------------------------------------------------- /mpv/scripts/blank.bgra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun-dev/mpv-thumbPreview/73548ccb10f315d97e71e74d2c8ed19126fdc82c/mpv/scripts/blank.bgra -------------------------------------------------------------------------------- /mpv/scripts/thumbgen.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Thumbnail Generator script for thumbs.lua. 3 | --]] 4 | 5 | local utils = require "mp.utils" 6 | local ffi = require("ffi") 7 | 8 | -- Get memory address from ffmpeg's stdout. 9 | local function extract_address(s) 10 | local addr = tostring(ffi.cast("char*",s)) 11 | local _, loc = string.find(addr, ": 0x") 12 | return tonumber(string.sub(addr,loc+1,-1),16) 13 | end 14 | 15 | -- Generate thumbnails in stdout. 16 | local function generate(timespan, input, size,maxThumbs) 17 | local thumbs = { 18 | data = {}, 19 | addr = {} 20 | } 21 | 22 | local command = {} 23 | 24 | command.args = { 25 | "mpv", 26 | "--msg-level","all=no", 27 | "--ss", "", 28 | input, 29 | "--hr-seek=no", 30 | "--frames","1", 31 | "--no-audio", 32 | "--vf-add", size, 33 | "--ovc=rawvideo", 34 | "--vf-add=format=bgra", 35 | "--of", "image2", 36 | "--o=-" 37 | } 38 | 39 | local start = mp.get_time() 40 | for i=0, maxThumbs do 41 | curtime=(i*timespan) 42 | command.args[5] = curtime 43 | 44 | -- mp.osd_message("Running: " .. table.concat(command.args,' '),3) --[debug] 45 | 46 | local process = utils.subprocess(command) 47 | 48 | --Check if process was successful. 49 | if process.status ~=0 then 50 | mp.msg.warn("mpv subprocess failed.") 51 | return 52 | end 53 | 54 | thumbs.data[i] = process.stdout 55 | thumbs.addr[i] = extract_address(thumbs.data[i]) 56 | 57 | --Send thumb table to thumbs.lua 58 | mp.commandv("script-message-to", "thumbs", "add_thumb", i, thumbs.addr[i]) 59 | 60 | if curtime > (maxThumbs-timespan) then 61 | local stop = mp.get_time() 62 | mp.msg.debug(i+1 .. " thumbs created in " .. stop-start .. " seconds") 63 | mp.unregister_script_message("generate") 64 | thumbs = {} 65 | return 66 | end 67 | 68 | end 69 | 70 | end 71 | 72 | --Generate thumbnails in cache folder. 73 | local function generateLocal(...) 74 | local arg={...} 75 | local init = false 76 | local timespan = tonumber(arg[1]) 77 | local input = arg[2] 78 | local size = arg[3] 79 | local maxThumbs = tonumber(arg[4]) 80 | local output = arg[5] 81 | local regen = tonumber(arg[6]) 82 | 83 | 84 | local command = {} 85 | local mkcmd = {} 86 | 87 | mkcmd.args = {"mkdir", "-p",output} 88 | 89 | command.args = { 90 | "mpv", 91 | "--msg-level","all=no", 92 | "--ss", "", 93 | input, 94 | "--hr-seek=no", 95 | "--frames","1", 96 | "--no-audio", 97 | "--vf-add", size, 98 | "--ovc=rawvideo", 99 | "--vf-add=format=bgra", 100 | "--of", "image2", 101 | "--o", "out" 102 | } 103 | 104 | if regen == 0 then 105 | local folder_process = utils.subprocess(mkcmd) 106 | if folder_process.status == 0 then 107 | init = true 108 | end 109 | else 110 | init = true 111 | end 112 | 113 | if init then 114 | local start = mp.get_time() 115 | for i=0, maxThumbs do 116 | curtime=(i*timespan) 117 | command.args[5] = curtime 118 | command.args[18] = output.."\\\\thumb"..tostring(i)..".bgra" 119 | -- mp.osd_message("Running: " .. table.concat(command.args,' '),3) --[debug] 120 | 121 | local process = utils.subprocess(command) 122 | --Check if process was successful. 123 | if process.status ~=0 then 124 | mp.msg.warn("mpv subprocess failed.") 125 | return 126 | end 127 | 128 | 129 | 130 | if curtime > (maxThumbs-timespan) then 131 | local stop = mp.get_time() 132 | mp.msg.debug(i+1 .. " thumbs created in " .. stop-start .. " seconds") 133 | mp.unregister_script_message("generateLocal") 134 | return 135 | end 136 | 137 | end 138 | 139 | end 140 | 141 | end 142 | 143 | 144 | mp.register_script_message("generate", generate) 145 | mp.register_script_message("generateLocal", generateLocal) 146 | -------------------------------------------------------------------------------- /mpv/scripts/thumbs.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MPV Thumbnail Preview 3 | Version: 0.4 4 | 5 | Generates preview thumbnails for mpv either locally or dynamically using overlay-add. 6 | Special thanks to various anons in https://boards.4chan.org/g/catalog#s=mpv 7 | --]] 8 | 9 | local msg = require 'mp.msg' 10 | local utils = require "mp.utils" 11 | local options = require "mp.options" 12 | 13 | local _global = { 14 | thumbdir="", 15 | thumb_width=0, 16 | offset_from_seekbar=0, 17 | y_offset=0, 18 | timespan=0, 19 | minTime=0, 20 | auto=false, 21 | cache=false 22 | } 23 | 24 | options.read_options(_global) 25 | 26 | local vid_w,vid_h = 0 27 | local osd_w,osd_h = 0 28 | local input,outpath = "" 29 | local mpath = "" 30 | local hash = "" 31 | local init,init2 = false 32 | local regen = 0 33 | local oldSize= {} 34 | local thumb_height=0 35 | local duration = 0 36 | local thumbaddr = {} 37 | local offset_x_seekbar = (_global.thumb_width/2) --Center of thumbnail width. 38 | local zRect = {} 39 | local sw,sh = 0 40 | 41 | local function exists(file) 42 | local ok, err, code = os.rename(file, file) 43 | if not ok then 44 | if code == 13 then 45 | return true 46 | end 47 | end 48 | return ok, err 49 | end 50 | 51 | local function isdir(path) 52 | return exists(path.."/") 53 | end 54 | 55 | local function script_path() 56 | local str = debug.getinfo(2, "S").source:sub(2) 57 | return str:match("(.*/)") 58 | end 59 | 60 | function GetFileExtension(strFilename) 61 | return string.match(strFilename,"^.+(%..+)$") 62 | end 63 | 64 | local function escape(str) 65 | return str:gsub("\\", "\\\\"):gsub("'", "'\\''") 66 | end 67 | 68 | local function osd(str) 69 | return mp.osd_message(str, 2) 70 | end 71 | 72 | -- modified from https://github.com/occivink/mpv-scripts/blob/master/drag-to-pan.lua 73 | local function compute_video_dimensions() 74 | local video_params = mp.get_property_native("video-out-params") 75 | local h = video_params["h"] 76 | local w = video_params["w"] 77 | local dw = video_params["dw"] 78 | local dh = video_params["dh"] 79 | 80 | local fwidth = osd_w 81 | local fheight = math.floor(osd_w / dw * dh) 82 | 83 | if fheight > osd_h or fheight < h then 84 | local tmpw = math.floor(osd_h / dh * dw) 85 | 86 | if tmpw <= osd_w then 87 | fheight = osd_h 88 | fwidth = tmpw 89 | end 90 | end 91 | 92 | if fheight < osd_h then 93 | fheight = osd_h 94 | end 95 | 96 | sw = fwidth + math.floor( fwidth / fheight) 97 | sh = fheight +math.floor( fwidth / fheight) 98 | 99 | end 100 | 101 | local function resized() 102 | oldSize.x,oldSize.y = osd_w,osd_h 103 | compute_video_dimensions() 104 | local osc = mp.get_property("osc") 105 | 106 | -- osd(tostring(offset)) 107 | if osc then 108 | zRect = { 109 | aY = sh * 94/ 100, 110 | aY2 = sh * 1/ 100, 111 | bX = sw * 18/ 100, 112 | bX2 = sw * 32 / 100 113 | } 114 | else 115 | zRect = { 116 | aY = sh * 94/ 100, 117 | aY2 = sh * 1/ 100, 118 | bX = sw* 1/ 100, 119 | bX2 = sw * 1 / 100 120 | } 121 | end 122 | end 123 | 124 | -- https://github.com/wiiaboo/mpv-scripts/blob/master/zones.lua 125 | local function zone(p) 126 | local v = {0, 1, 2} 127 | local h = {0, 1, 2} 128 | 129 | local y = (p.y < zRect.aY) and v[1] or (p.y < (osd_h - zRect.aY2)) and v[2] or v[3] 130 | local x = (p.x < zRect.bX) and h[1] or (p.x < (osd_w - zRect.bX2)) and h[2] or h[3] 131 | 132 | return y, x 133 | end 134 | 135 | local function on_seek() 136 | if init2 then 137 | osd_w,osd_h = mp.get_osd_size() 138 | local point = {} 139 | point.x, point.y = mp.get_mouse_pos() 140 | 141 | --Setup video w/h once. 142 | if vid_w == 0 then 143 | vid_w,vid_h = mp.get_property("width"),mp.get_property("height") 144 | 145 | resized() 146 | 147 | --calculate thumb_height based on given width. 148 | thumb_height = math.floor(_global.thumb_width/ (vid_w/vid_h)) 149 | end 150 | 151 | --calculate new sizes when resized. 152 | if oldSize.x ~= osd_w or oldSize.y ~= osd_h then 153 | resized() 154 | end 155 | 156 | local zoneY, zoneX = zone(point) 157 | 158 | -- osd(tostring(point.x)..","..tostring(point.y)) --[Debug] 159 | -- osd(tostring(zoneX).."-"..tostring(zoneY)) --[Debug] 160 | 161 | local posx = (point.x-offset_x_seekbar) 162 | local posy = math.floor(zRect.aY-(thumb_height+_global.y_offset)) 163 | 164 | local region = zoneX ==1 and zoneY == 1 --mouse zone(seekbar) 165 | local norm = math.floor(((point.x - zRect.bX)/ ((osd_w - zRect.bX2) - zRect.bX))*duration) --normalized range of mouse x(ssp) 166 | local index = math.floor(norm/_global.timespan) 167 | local thumbpath = nil 168 | 169 | if init then 170 | --non-negative index. doesn't serve any particular purpose. 171 | if index < 0 then 172 | index = 0 173 | end 174 | 175 | --cap index at table max. 176 | if index > #thumbaddr+1 then 177 | index = #thumbaddr 178 | end 179 | 180 | if _global.cache then 181 | local tmp = string.format("%s%s",outpath,"\\thumb") 182 | thumbpath = tmp..tostring(index)..".bgra" 183 | thumbaddr[index] = 0 --dummy value 184 | else 185 | thumbpath = "&"..tostring(thumbaddr[index]) 186 | end 187 | 188 | if region then 189 | if thumbaddr[index] ~= nil then 190 | mp.commandv("overlay-add",1,posx,posy,thumbpath, 0, "bgra",_global.thumb_width, thumb_height, (4*_global.thumb_width)) 191 | else 192 | mp.commandv("overlay-add",1,posx,posy,mpath.."blank.bgra", 0, "bgra",_global.thumb_width, thumb_height, (4*_global.thumb_width)) 193 | end 194 | else 195 | mp.commandv("overlay-remove",1) 196 | end 197 | end 198 | end 199 | end 200 | 201 | local function createThumbs() 202 | 203 | local size = "lavfi=[scale=$thumbwidth:-1]" 204 | size = size:gsub("$thumbwidth", _global.thumb_width) 205 | init2 = true 206 | 207 | if not _global.cache then 208 | mp.msg.debug("Generating thumbnails dynamically") 209 | mp.commandv("script-message-to", "thumbgen", "generate",_global.timespan, input, size,duration) 210 | else 211 | mp.msg.debug("Generating thumbnails locally") 212 | mp.commandv("script-message-to", "thumbgen", "generateLocal",_global.timespan, input, size,duration,outpath,regen) 213 | end 214 | end 215 | 216 | local function checkThumbs() 217 | if duration < _global.minTime then 218 | if not _global.auto then 219 | osd("Video duration less than ".. tostring(_global.minTime) .. " seconds. Cancelled") 220 | end 221 | return end 222 | 223 | --Check if a local folder exists or not if caching, else use regular streaming method. 224 | if _global.cache then 225 | 226 | --Get MD5 227 | local cmd = {} 228 | 229 | cmd.args = { 230 | "mpv", 231 | "--msg-level","all=no", 232 | input, 233 | "--frames","1", 234 | "--t","0.1", 235 | "--of", "md5", 236 | "--o=-" 237 | } 238 | 239 | local process = utils.subprocess(cmd) 240 | local tmp = process.stdout 241 | hash = tostring(tmp:gsub("MD5=", "")) 242 | hash = tostring(hash:gsub("\n", "")) 243 | tmp = _global.thumbdir:gsub("\"", "") 244 | outpath = tmp .. hash 245 | 246 | local status = isdir(outpath) 247 | 248 | if status then 249 | if _global.auto then 250 | init2 = true 251 | else 252 | osd("Thumbnails exist. Recreate? Y/N",25) 253 | addBinding() 254 | end 255 | 256 | else 257 | mp.msg.warn("Hash for video not found. Generating cache thumbs.") 258 | createThumbs() 259 | end 260 | else 261 | if not init2 then 262 | createThumbs() 263 | end 264 | end 265 | end 266 | 267 | -- Gets data from generate function from thumbgen.lua 268 | local function add_thumb(index, addr) 269 | local i = tonumber(index) 270 | thumbaddr[i] = tonumber(addr) 271 | end 272 | 273 | local function unsave() 274 | osd("Cancelled.",1) 275 | removeBinding() 276 | end 277 | 278 | local function regenThumb() 279 | regen = 1 280 | createThumbs() 281 | removeBinding() 282 | end 283 | 284 | -- Temporarily force y as a keybinding for confirmation. 285 | local function addBinding() 286 | mp.add_forced_key_binding("y", "save", regenThumb) 287 | mp.add_forced_key_binding("Y", "save2", regenThumb) 288 | mp.add_forced_key_binding("n", "unsave", unsave) 289 | mp.add_forced_key_binding("N", "unsave2", unsave) 290 | end 291 | 292 | -- Temporarily force n as a keybinding for confirmation. 293 | local function removeBinding() 294 | mp.remove_key_binding("unsave") 295 | mp.remove_key_binding("unsave2") 296 | mp.remove_key_binding("save") 297 | mp.remove_key_binding("save2") 298 | end 299 | 300 | local function unregister() 301 | init = false 302 | init2 = false 303 | mp.remove_key_binding("checkThumbs") 304 | mp.unregister_script_message("add_thumb") 305 | end 306 | 307 | local check 308 | check = function() 309 | mp.unregister_event(check) 310 | 311 | --check if valid format for making previews 312 | local validlist = '.avi|.divx|flv|.mkv|.mov|.mp4|.mpeg|.mpg|.rm|.rmvb|.ts|.vob|.webm|.wmv|.m2ts' 313 | local ext = GetFileExtension(mp.get_property("filename")) 314 | 315 | mpath = script_path() 316 | mpath=mpath:gsub([[/]],[[\\]]) 317 | 318 | --if the video isn't in the valid list, then it's ignored. 319 | if not string.match(validlist, ext) then 320 | mp.msg.warn("invalid file") 321 | unregister() 322 | return end 323 | 324 | --check if video length is sane 325 | if duration == 0 then 326 | duration = math.floor(mp.get_property_number("duration")) 327 | 328 | --if the video length is too small, don't create thumbs. 329 | if duration < _global.minTime then 330 | mp.msg.warn("duration too small") 331 | unregister() 332 | return end 333 | end 334 | 335 | oldSize.x,oldSize.y = mp.get_osd_size() 336 | input = escape(utils.join_path(utils.getcwd(),mp.get_property("path"))) 337 | 338 | if _global.auto then 339 | checkThumbs() 340 | end 341 | 342 | init = true 343 | mp.msg.debug("mpv thumbs initial check performed") 344 | end 345 | 346 | --keybindings 347 | mp.add_key_binding("k", "checkThumbs", checkThumbs) 348 | 349 | --registers 350 | mp.register_script_message("add_thumb", add_thumb) 351 | mp.register_event("tick", on_seek) 352 | 353 | local fileLoaded 354 | fileLoaded = function() 355 | mp.unregister_event(fileLoaded) 356 | return mp.register_event('playback-restart', check) 357 | end 358 | return mp.register_event('file-loaded', fileLoaded) 359 | --------------------------------------------------------------------------------