youtube-quality.lua (8902B)
1 -- youtube-quality.lua 2 -- https://github.com/jgreco/mpv-youtube-quality 3 -- Change youtube video quality on the fly. 4 -- 5 -- Diplays a menu that lets you switch to different ytdl-format settings while 6 -- you're in the middle of a video (just like you were using the web player). 7 -- 8 -- Bound to ctrl-f by default. 9 10 local mp = require 'mp' 11 local utils = require 'mp.utils' 12 local msg = require 'mp.msg' 13 local assdraw = require 'mp.assdraw' 14 15 local opts = { 16 --key bindings 17 toggle_menu_binding = "ctrl+f", 18 up_binding = "UP", 19 down_binding = "DOWN", 20 select_binding = "ENTER", 21 ytdl_format = "bestvideo+bestaudio/best", 22 23 --formatting / cursors 24 selected_and_active = "▶ - ", 25 selected_and_inactive = "● - ", 26 unselected_and_active = "▷ - ", 27 unselected_and_inactive = "○ - ", 28 29 --font size scales by window, if false requires larger font and padding sizes 30 scale_playlist_by_window=false, 31 32 --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua 33 --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 34 --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags 35 --undeclared tags will use default osd settings 36 --these styles will be used for the whole playlist. More specific styling will need to be hacked in 37 -- 38 --(a monospaced font is recommended but not required) 39 style_ass_tags = "{\\fnmonospace}", 40 41 --paddings for top left corner 42 text_padding_x = 5, 43 text_padding_y = 5, 44 45 --other 46 menu_timeout = 10, 47 48 --use youtube-dl to fetch a list of available formats (overrides quality_strings) 49 fetch_formats = true, 50 51 --default menu entries 52 quality_strings=[[ 53 [ 54 {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"}, 55 {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"}, 56 {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"}, 57 {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"}, 58 {"720p" : "bestvideo[height<=?720]+bestaudio/best"}, 59 {"480p" : "bestvideo[height<=?480]+bestaudio/best"}, 60 {"360p" : "bestvideo[height<=?360]+bestaudio/best"}, 61 {"240p" : "bestvideo[height<=?240]+bestaudio/best"}, 62 {"144p" : "bestvideo[height<=?144]+bestaudio/best"} 63 ] 64 ]], 65 } 66 (require 'mp.options').read_options(opts, "youtube-quality") 67 opts.quality_strings = utils.parse_json(opts.quality_strings) 68 69 local destroyer = nil 70 71 mp.set_property("ytdl-format",opts.ytdl_format) 72 73 function show_menu() 74 local selected = 1 75 local active = 0 76 local current_ytdl_format = mp.get_property("ytdl-format") 77 msg.verbose("current ytdl-format: "..current_ytdl_format) 78 local num_options = 0 79 local options = {} 80 81 82 if opts.fetch_formats then 83 options, num_options = download_formats() 84 end 85 86 if next(options) == nil then 87 for i,v in ipairs(opts.quality_strings) do 88 num_options = num_options + 1 89 for k,v2 in pairs(v) do 90 options[i] = {label = k, format=v2} 91 if v2 == current_ytdl_format then 92 active = i 93 selected = active 94 end 95 end 96 end 97 end 98 99 --set the cursor to the currently format 100 for i,v in ipairs(options) do 101 if v.format == current_ytdl_format then 102 active = i 103 selected = active 104 break 105 end 106 end 107 108 function selected_move(amt) 109 selected = selected + amt 110 if selected < 1 then selected = num_options 111 elseif selected > num_options then selected = 1 end 112 timeout:kill() 113 timeout:resume() 114 draw_menu() 115 end 116 function choose_prefix(i) 117 if i == selected and i == active then return opts.selected_and_active 118 elseif i == selected then return opts.selected_and_inactive end 119 120 if i ~= selected and i == active then return opts.unselected_and_active 121 elseif i ~= selected then return opts.unselected_and_inactive end 122 return "> " --shouldn't get here. 123 end 124 125 function draw_menu() 126 local ass = assdraw.ass_new() 127 128 ass:pos(opts.text_padding_x, opts.text_padding_y) 129 ass:append(opts.style_ass_tags) 130 131 for i,v in ipairs(options) do 132 ass:append(choose_prefix(i)..v.label.."\\N") 133 end 134 135 local w, h = mp.get_osd_size() 136 if opts.scale_playlist_by_window then w,h = 0, 0 end 137 mp.set_osd_ass(w, h, ass.text) 138 end 139 140 function destroy() 141 timeout:kill() 142 mp.set_osd_ass(0,0,"") 143 mp.remove_key_binding("move_up") 144 mp.remove_key_binding("move_down") 145 mp.remove_key_binding("select") 146 mp.remove_key_binding("escape") 147 destroyer = nil 148 end 149 timeout = mp.add_periodic_timer(opts.menu_timeout, destroy) 150 destroyer = destroy 151 152 mp.add_forced_key_binding(opts.up_binding, "move_up", function() selected_move(-1) end, {repeatable=true}) 153 mp.add_forced_key_binding(opts.down_binding, "move_down", function() selected_move(1) end, {repeatable=true}) 154 mp.add_forced_key_binding(opts.select_binding, "select", function() 155 destroy() 156 mp.set_property("ytdl-format", options[selected].format) 157 reload_resume() 158 end) 159 mp.add_forced_key_binding(opts.toggle_menu_binding, "escape", destroy) 160 161 draw_menu() 162 return 163 end 164 165 local ytdl = { 166 path = "youtube-dl", 167 searched = false, 168 blacklisted = {} 169 } 170 171 format_cache={} 172 function download_formats() 173 local function exec(args) 174 local ret = utils.subprocess({args = args}) 175 return ret.status, ret.stdout, ret 176 end 177 178 local function table_size(t) 179 s = 0 180 for i,v in ipairs(t) do 181 s = s+1 182 end 183 return s 184 end 185 186 local url = mp.get_property("path") 187 188 url = string.gsub(url, "ytdl://", "") -- Strip possible ytdl:// prefix. 189 190 -- don't fetch the format list if we already have it 191 if format_cache[url] ~= nil then 192 local res = format_cache[url] 193 return res, table_size(res) 194 end 195 mp.osd_message("fetching available formats with youtube-dl...", 60) 196 197 if not (ytdl.searched) then 198 local ytdl_mcd = mp.find_config_file("youtube-dl") 199 if not (ytdl_mcd == nil) then 200 msg.verbose("found youtube-dl at: " .. ytdl_mcd) 201 ytdl.path = ytdl_mcd 202 end 203 ytdl.searched = true 204 end 205 206 local command = {ytdl.path, "--no-warnings", "--no-playlist", "-J"} 207 table.insert(command, url) 208 local es, json, result = exec(command) 209 210 if (es < 0) or (json == nil) or (json == "") then 211 mp.osd_message("fetching formats failed...", 1) 212 msg.error("failed to get format list: " .. err) 213 return {}, 0 214 end 215 216 local json, err = utils.parse_json(json) 217 218 if (json == nil) then 219 mp.osd_message("fetching formats failed...", 1) 220 msg.error("failed to parse JSON data: " .. err) 221 return {}, 0 222 end 223 224 res = {} 225 msg.verbose("youtube-dl succeeded!") 226 for i,v in ipairs(json.formats) do 227 if v.vcodec ~= "none" then 228 local fps = v.fps and v.fps.."fps" or "" 229 local resolution = string.format("%sx%s", v.width, v.height) 230 local l = string.format("%-9s %-5s (%-4s / %s)", resolution, fps, v.ext, v.vcodec) 231 local f = string.format("%s+bestaudio/best", v.format_id) 232 table.insert(res, {label=l, format=f, width=v.width }) 233 end 234 end 235 236 table.sort(res, function(a, b) return a.width > b.width end) 237 238 mp.osd_message("", 0) 239 format_cache[url] = res 240 return res, table_size(res) 241 end 242 243 244 -- register script message to show menu 245 mp.register_script_message("toggle-quality-menu", 246 function() 247 if destroyer ~= nil then 248 destroyer() 249 else 250 show_menu() 251 end 252 end) 253 254 -- keybind to launch menu 255 mp.add_key_binding(opts.toggle_menu_binding, "quality-menu", show_menu) 256 257 -- special thanks to reload.lua (https://github.com/4e6/mpv-reload/) 258 function reload_resume() 259 local playlist_pos = mp.get_property_number("playlist-pos") 260 local reload_duration = mp.get_property_native("duration") 261 local time_pos = mp.get_property("time-pos") 262 263 mp.set_property_number("playlist-pos", playlist_pos) 264 265 -- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero 266 -- duration property. When reloading VOD, to keep the current time position 267 -- we should provide offset from the start. Stream doesn't have fixed start. 268 -- Decent choice would be to reload stream from it's current 'live' positon. 269 -- That's the reason we don't pass the offset when reloading streams. 270 if reload_duration and reload_duration > 0 then 271 local function seeker() 272 mp.commandv("seek", time_pos, "absolute") 273 mp.unregister_event(seeker) 274 end 275 mp.register_event("file-loaded", seeker) 276 end 277 end