1 --[==========================================================================[
2 rc.lua: remote control module for VLC
3 --[==========================================================================[
4 Copyright (C) 2007 the VideoLAN team
7 Authors: Antoine Cellerier <dionoea at videolan dot org>
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22 --]==========================================================================]
25 [============================================================================[
26 Remote control interface for VLC
28 This is a modules/control/rc.c look alike (with a bunch of new features)
32 Use on tcp connection:
33 vlc -I luarc --lua-config "rc={host='localhost:4212'}"
34 Use on multiple hosts (term + 2 tcp ports):
35 vlc -I luarc --lua-config "rc={hosts={'*console','localhost:4212','localhost:5678'}}"
38 -I luarc is an alias for -I lua --lua-intf rc
40 Configuration options setable throught the --lua-config option are:
41 * hosts: A list of hosts to listen on.
42 * host: A host to listen on. (won't be used if `hosts' is set)
43 The following can be set using the --lua-config option or in the interface
44 itself using the `set' command:
46 * welcome: The welcome message.
47 * width: The default terminal width (used to format text).
48 * autocompletion: When issuing an unknown command, print a list of
49 possible commands to autocomplete with. (0 to disable,
51 * autoalias: If autocompletion returns only one possibility, use it
52 (0 to disable, 1 to enable).
53 ]============================================================================]
57 skip2 = function(foo) return skip(skip(foo)) end
58 setarg = common.setarg
61 --[[ Setup default environement ]]
62 env = { prompt = "> ";
66 welcome = "Remote control interface initialized. Type `help' for help."
69 --[[ Import custom environement variables from the command line config (if possible) ]]
70 for k,v in pairs(env) do
72 if type(env[k]) == type(config[k]) then
74 vlc.msg.dbg("set environement variable `"..k.."' to "..tonumber(env[k]))
76 vlc.msg.err("environement variable `"..k.."' should be of type "..type(env[k])..". config value will be discarded.")
81 --[[ Command functions ]]
82 function set_env(name,client,value)
84 local var,val = split_input(value)
86 s = string.gsub(val,"\"(.*)\"","%1")
87 if type(client.env[var])==type(1) then
88 client.env[var] = tonumber(s)
93 client:append( tostring(client.env[var]) )
96 for e,v in common.pairs_sorted(client.env) do
97 client:append(e.."="..v)
102 function save_env(name,client,value)
103 env = common.table_copy(client.env)
106 function alias(client,value)
108 local var,val = split_input(value)
109 if commands[var] and type(commands[var]) ~= type("") then
110 client:append("Error: cannot use a primary command as an alias name")
112 if commands[val] then
115 client:append("Error: unknown primary command `"..val.."'.")
119 for c,v in common.pairs_sorted(commands) do
120 if type(v)==type("") then
121 client:append(c.."="..v)
127 function fixme(name,client)
128 client:append( "FIXME: unimplemented command `"..name.."'." )
131 function logout(name,client)
132 if client.type == host.client_type.net then
133 client:send("Bye-bye!")
136 client:append("Error: Can't logout of stdin/stdout. Use quit or shutdown to close VLC.")
140 function shutdown(name,client)
141 client:append("Bye-bye!")
142 h:broadcast("Shutting down.")
143 vlc.msg.info("Requested shutdown.")
147 function quit(name,client)
148 if client.type == host.client_type.net then
151 shutdown(name,client)
155 function add(name,client,arg)
156 -- TODO: parse (and use) options
158 if name == "enqueue" then
159 f = vlc.playlist.enqueue
166 function playlist(name,client,arg)
167 -- TODO: add possibility to filter playlist items using a mask
170 playlist = vlc.playlist.get(true)
171 client:append("+----[ Playlist - Media Library ]")
173 playlist = vlc.playlist.get()
174 client:append("+----[ Playlist ]")
176 for i, item in pairs(playlist) do
177 local str = "| "..tostring(i).." - "..item.name
178 if item.duration > 0 then
179 str = str.." ("..common.durationtostring(item.duration)..")"
181 if item.nb_played > 0 then
182 str = str.." played "..tostring(item.nb_played).." time"
183 if item.nb_played > 1 then
189 client:append("+----[ End of playlist ]")
192 function print_text(label,text)
193 return function(name,client)
194 client:append("+----[ "..label.." ]")
196 for line in string.gmatch(text,".-\r?\n") do
197 client:append("| "..string.gsub(line,"\r?\n",""))
200 client:append("+----[ End of "..string.lower(label).." ]")
204 function help(name,client,arg)
205 local width = client.env.width
206 local long = (name == "longhelp")
208 if arg then extra = "matching `" .. arg .. "' " end
209 client:append("+----[ Remote control commands "..extra.."]")
210 for i, cmd in ipairs(commands_ordered) do
211 if (cmd == "" or not commands[cmd].adv or long)
212 and (not arg or string.match(cmd,arg)) then
213 local str = "| " .. cmd
215 local val = commands[cmd]
217 for _,a in ipairs(val.aliases) do
218 str = str .. ", " .. a
221 if val.args then str = str .. " " .. val.args end
222 if #str%2 == 1 then str = str .. " " end
223 str = str .. string.rep(" .",(width-(#str+#val.help)-1)/2)
224 str = str .. string.rep(" ",width-#str-#val.help) .. val.help
229 client:append("+----[ end of help ]")
232 function input_info(name,client)
233 local categories = vlc.input_info()
234 for cat, infos in pairs(categories) do
235 client:append("+----[ "..cat.." ]")
237 for name, value in pairs(infos) do
238 client:append("| "..name..": "..value)
242 client:append("+----[ end of stream info ]")
245 function playlist_status(name,client)
246 local a,b,c = vlc.playlist.status()
247 client:append( "( new input: " .. tostring(a) .. " )" )
248 client:append( "( audio volume: " .. tostring(b) .. " )")
249 client:append( "( state " .. tostring(c) .. " )")
252 function is_playing(name,client)
253 if vlc.is_playing() then client:append "1" else client:append "0" end
256 function ret_print(foo,start,stop)
257 local start = start or ""
258 local stop = stop or ""
259 return function(discard,client,...) client:append(start..tostring(foo(...))..stop) end
262 function get_time(var,client)
264 local input = vlc.object.input()
265 client:append(math.floor(vlc.var.get( input, var )))
269 function titlechap(name,client,value)
270 local input = vlc.object.input()
271 local var = string.gsub( name, "_.*$", "" )
273 vlc.var.set( input, var, value )
275 local item = vlc.var.get( input, var )
276 -- Todo: add item name conversion
280 function titlechap_offset(client,offset)
281 return function(name,value)
282 local input = vlc.object.input()
283 local var = string.gsub( name, "_.*$", "" )
284 vlc.var.set( input, var, vlc.var.get( input, var )+offset )
288 function seek(name,client,value)
289 local input = vlc.object.input()
290 if string.sub(value,#value)=="%" then
291 vlc.var.set(input,"position",tonumber(string.sub(value,1,#value-1))/100.)
293 vlc.var.set(input,"time",tonumber(value))
297 function volume(name,client,value)
299 vlc.volume.set(value)
301 client:append(tostring(vlc.volume.get()))
305 function rate(name,client)
306 local input = vlc.object.input()
307 if name == "normal" then
308 vlc.var.set(input,"rate",1000) -- FIXME: INPUT_RATE_DEFAULT
310 vlc.var.set(input,"rate-"..name,nil)
314 function listvalue(obj,var)
315 return function(client,value)
316 local o = vlc.object.find(nil,obj,"anywhere")
317 if not o then return end
319 vlc.var.set( o, var, value )
321 local c = vlc.var.get( o, var )
322 local v, l = vlc.var.get_list( o, var )
323 client:append("+----[ "..var.." ]")
324 for i,val in ipairs(v) do
325 local mark = (val==c)and " *" or ""
326 client:append("| "..tostring(val).." - "..tostring(l[i])..mark)
328 client:append("+----[ end of "..var.." ]")
333 function eval(client,val)
334 client:append(loadstring("return "..val)())
337 --[[ Declare commands, register their callback functions and provide
340 "<command name>"; { func = <function>; [ args = "<str>"; ] help = "<str>"; [ adv = <bool>; ] [ aliases = { ["<str>";]* }; ] }
343 { "add"; { func = add; args = "XYZ"; help = "add XYZ to playlist" } };
344 { "enqueue"; { func = add; args = "XYZ"; help = "queue XYZ to playlist" } };
345 { "playlist"; { func = playlist; help = "show items currently in playlist" } };
346 { "play"; { func = skip2(vlc.playlist.play); help = "play stream" } };
347 { "stop"; { func = skip2(vlc.playlist.stop); help = "stop stream" } };
348 { "next"; { func = skip2(vlc.playlist.next); help = "next playlist item" } };
349 { "prev"; { func = skip2(vlc.playlist.prev); help = "previous playlist item" } };
350 { "goto"; { func = skip2(vlc.playlist.goto); help = "goto item at index" } };
351 { "repeat"; { func = skip2(vlc.playlist.repeat_); args = "[on|off]"; help = "toggle playlist repeat" } };
352 { "loop"; { func = skip2(vlc.playlist.loop); args = "[on|off]"; help = "toggle playlist loop" } };
353 { "random"; { func = skip2(vlc.playlist.random); args = "[on|off]"; help = "toggle playlist random" } };
354 { "clear"; { func = skip2(vlc.playlist.clear); help = "clear the playlist" } };
355 { "status"; { func = playlist_status; help = "current playlist status" } };
356 { "title"; { func = titlechap; args = "[X]"; help = "set/get title in current item" } };
357 { "title_n"; { func = titlechap_offset(1); help = "next title in current item" } };
358 { "title_p"; { func = titlechap_offset(-1); help = "previous title in current item" } };
359 { "chapter"; { func = titlechap; args = "[X]"; help = "set/get chapter in current item" } };
360 { "chapter_n"; { func = titlechap_offset(1); help = "next chapter in current item" } };
361 { "chapter_p"; { func = titlechap_offset(-1); help = "previous chapter in current item" } };
363 { "seek"; { func = seek; args = "X"; help = "seek in seconds, for instance `seek 12'" } };
364 { "pause"; { func = setarg(common.hotkey,"key-play-pause"); help = "toggle pause" } };
365 { "fastforward"; { func = setarg(common.hotkey,"key-jump+extrashort"); help = "set to maximum rate" } };
366 { "rewind"; { func = setarg(common.hotkey,"key-jump-extrashort"); help = "set to minimum rate" } };
367 { "faster"; { func = rate; help = "faster playing of stream" } };
368 { "slower"; { func = rate; help = "slower playing of stream" } };
369 { "normal"; { func = rate; help = "normal playing of stream" } };
370 { "fullscreen"; { func = skip2(vlc.fullscreen); args = "[on|off]"; help = "toggle fullscreen"; aliases = { "f", "F" } } };
371 { "info"; { func = input_info; help = "information about the current stream" } };
372 { "get_time"; { func = get_time("time"); help = "seconds elapsed since stream's beginning" } };
373 { "is_playing"; { func = is_playing; help = "1 if a stream plays, 0 otherwise" } };
374 { "get_title"; { func = ret_print(vlc.get_title); help = "the title of the current stream" } };
375 { "get_length"; { func = get_time("length"); help = "the length of the current stream" } };
377 { "volume"; { func = volume; args = "[X]"; help = "set/get audio volume" } };
378 { "volup"; { func = ret_print(vlc.volume.up,"( audio volume: "," )"); args = "[X]"; help = "raise audio volume X steps" } };
379 { "voldown"; { func = ret_print(vlc.volume.down,"( audio volume: "," )"); args = "[X]"; help = "lower audio volume X steps" } };
380 { "adev"; { func = skip(listvalue("aout","audio-device")); args = "[X]"; help = "set/get audio device" } };
381 { "achan"; { func = skip(listvalue("aout","audio-channels")); args = "[X]"; help = "set/get audio channels" } };
382 { "atrack"; { func = skip(listvalue("input","audio-es")); args = "[X]"; help = "set/get audio track" } };
383 { "vtrack"; { func = skip(listvalue("input","video-es")); args = "[X]"; help = "set/get video track" } };
384 { "vratio"; { func = skip(listvalue("vout","aspect-ratio")); args = "[X]"; help = "set/get video aspect ratio" } };
385 { "vcrop"; { func = skip(listvalue("vout","crop")); args = "[X]"; help = "set/get video crop"; aliases = { "crop" } } };
386 { "vzoom"; { func = skip(listvalue("vout","zoom")); args = "[X]"; help = "set/get video zoom"; aliases = { "zoom" } } };
387 { "snapshot"; { func = common.snapshot; help = "take video snapshot" } };
388 { "strack"; { func = skip(listvalue("input","spu-es")); args = "[X]"; help = "set/get subtitles track" } };
389 { "hotkey"; { func = skip(common.hotkey); args = "[hotkey name]"; help = "simulate hotkey press"; adv = true; aliases = { "key" } } };
390 { "menu"; { func = fixme; args = "[on|off|up|down|left|right|select]"; help = "use menu"; adv = true } };
392 { "set"; { func = set_env; args = "[var [value]]"; help = "set/get env var"; adv = true } };
393 { "save_env"; { func = save_env; help = "save env vars (for future clients)"; adv = true } };
394 { "alias"; { func = skip(alias); args = "[cmd]"; help = "set/get command aliases"; adv = true } };
395 { "eval"; { func = skip(eval); help = "eval some lua (*debug*)"; adv =true } }; -- FIXME: comment out if you're not debugging
396 { "description"; { func = print_text("Description",description); help = "describe this module" } };
397 { "license"; { func = print_text("License message",vlc.license()); help = "print VLC's license message"; adv = true } };
398 { "help"; { func = help; args = "[pattern]"; help = "a help message"; aliases = { "?" } } };
399 { "longhelp"; { func = help; args = "[pattern]"; help = "a longer help message" } };
400 { "logout"; { func = logout; help = "exit (if in a socket connection)" } };
401 { "quit"; { func = quit; help = "quit VLC (or logout if in a socket connection)" } };
402 { "shutdown"; { func = shutdown; help = "shutdown VLC" } };
405 for i, cmd in ipairs( commands_ordered ) do
407 commands[cmd[1]]=cmd[2]
408 if cmd[2].aliases then
409 for _,a in ipairs(cmd[2].aliases) do
414 commands_ordered[i]=cmd[1]
416 --[[ From now on commands_ordered is a list of the different command names
417 and commands is a associative array indexed by the command name. ]]
419 -- Compute the column width used when printing a the autocompletion list
421 for c,_ in pairs(commands) do
422 if #c > env.colwidth then env.colwidth = #c end
424 env.coldwidth = env.colwidth + 1
426 -- Count unimplemented functions
430 for c,v in pairs(commands) do
431 if v.func == fixme then
441 env.welcome = env.welcome .. "\r\nWarning: "..count.." functions are still unimplemented "..list.."."
446 function split_input(input)
447 local input = strip(input)
448 local s = string.find(input," ")
450 return string.sub(input,0,s-1), strip(string.sub(input,s))
456 function call_command(cmd,client,arg)
457 if type(commands[cmd]) == type("") then
462 ok, msg = pcall( commands[cmd].func, cmd, client, arg )
464 ok, msg = pcall( commands[cmd].func, cmd, client )
468 if a ~= "" then a = " " .. a end
469 client:append("Error in `"..cmd..a.."' ".. msg)
473 function call_libvlc_command(cmd,client,arg)
474 local ok, vlcerr, vlcmsg = pcall( vlc.libvlc_command, cmd, arg )
477 if a ~= "" then a = " " .. a end
478 client:append("Error in `"..cmd..a.."' ".. vlcerr) -- when pcall fails, the 2nd arg is the error message.
487 h.status_callbacks[host.status.password] = function(client)
488 client.env = common.table_copy( env )
489 client:send( client.env.welcome .. "\r\n")
490 client:switch_status(host.status.read)
492 -- Print prompt when switching a client's status to `read'
493 h.status_callbacks[host.status.read] = function(client)
494 client:send( client.env.prompt )
497 h:listen( config.hosts or config.host or "*console" )
499 --[[ The main loop ]]
500 while not vlc.should_die() do
502 local write, read = h:select(0.1)
504 for _, client in pairs(write) do
505 local len = client:send()
506 client.buffer = string.sub(client.buffer,len+1)
507 if client.buffer == "" then client:switch_status(host.status.read) end
510 for _, client in pairs(read) do
511 local input = client:recv(1000)
513 if string.match(input,"\n$") then
514 client.buffer = string.gsub(client.buffer..input,"\r?\n$","")
516 elseif client.buffer == ""
517 and ((client.type == host.client_type.stdio and input == "")
518 or (client.type == host.client_type.net and input == "\004")) then
520 client.buffer = "quit"
523 client.buffer = client.buffer .. input
526 local cmd,arg = split_input(client.buffer)
528 client:switch_status(host.status.write)
529 if commands[cmd] then
530 call_command(cmd,client,arg)
532 if client.type == host.client_type.stdio
533 and call_libvlc_command(cmd,client,arg) == 0 then
536 if client.env.autocompletion ~= 0 then
537 for v,_ in common.pairs_sorted(commands) do
538 if string.sub(v,0,#cmd)==cmd then
539 table.insert(choices, v)
543 if #choices == 1 and client.env.autoalias ~= 0 then
544 -- client:append("Aliasing to \""..choices[1].."\".")
546 call_command(cmd,client,arg)
548 client:append("Unknown command `"..cmd.."'. Type `help' for help.")
549 if #choices ~= 0 then
550 client:append("Possible choices are:")
551 local cols = math.floor(client.env.width/(client.env.colwidth+1))
552 local fmt = "%-"..client.env.colwidth.."s"
553 for i = 1, #choices do
554 choices[i] = string.format(fmt,choices[i])
556 for i = 1, #choices, cols do
557 local j = i + cols - 1
558 if j > #choices then j = #choices end
559 client:append(" "..table.concat(choices," ",i,j))