]> git.sesse.net Git - vlc/blob - share/luaintf/rc.lua
d02fb8b4e93f1b34aeec450961d86ce3a7db0448
[vlc] / share / luaintf / rc.lua
1 --[==========================================================================[
2  rc.lua: remote control module for VLC
3 --[==========================================================================[
4  Copyright (C) 2007 the VideoLAN team
5  $Id$
6
7  Authors: Antoine Cellerier <dionoea at videolan dot org>
8
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.
13
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.
18
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 --]==========================================================================]
23
24 description=
25 [============================================================================[
26  Remote control interface for VLC
27
28  This is a modules/control/rc.c look alike (with a bunch of new features)
29  
30  Use on local term:
31     vlc -I luarc
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'}}"
36  
37  Note:
38     -I luarc is an alias for -I lua --lua-intf rc
39
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:
45     * prompt: The prompt.
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,
50                       1 to enable).
51     * autoalias: If autocompletion returns only one possibility, use it
52                  (0 to disable, 1 to enable).
53 ]============================================================================]
54
55 require("common")
56 skip = common.skip
57 skip2 = function(foo) return skip(skip(foo)) end
58 setarg = common.setarg
59 strip = common.strip
60
61 --[[ Setup default environement ]]
62 env = { prompt = "> ";
63         width = 70;
64         autocompletion = 1;
65         autoalias = 1;
66         welcome = "Remote control interface initialized. Type `help' for help."
67       }
68
69 --[[ Import custom environement variables from the command line config (if possible) ]]
70 for k,v in pairs(env) do
71     if config[k] then
72         if type(env[k]) == type(config[k]) then
73             env[k] = config[k]
74             vlc.msg.dbg("set environement variable `"..k.."' to "..tonumber(env[k]))
75         else
76             vlc.msg.err("environement variable `"..k.."' should be of type "..type(env[k])..". config value will be discarded.")
77         end
78     end
79 end
80
81 --[[ Command functions ]]
82 function set_env(name,client,value)
83     if value then
84         local var,val = split_input(value)
85         if val then
86             s = string.gsub(val,"\"(.*)\"","%1")
87             if type(client.env[var])==type(1) then
88                 client.env[var] = tonumber(s)
89             else
90                 client.env[var] = s
91             end
92         else
93             client:append( tostring(client.env[var]) )
94         end
95     else
96         for e,v in common.pairs_sorted(client.env) do
97             client:append(e.."="..v)
98         end
99     end
100 end
101
102 function save_env(name,client,value)
103     env = common.table_copy(client.env)
104 end
105
106 function alias(client,value)
107     if value then
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")
111         else
112             if commands[val] then
113                 commands[var]=val
114             else
115                 client:append("Error: unknown primary command `"..val.."'.")
116             end
117         end
118     else
119         for c,v in common.pairs_sorted(commands) do
120             if type(v)==type("") then
121                 client:append(c.."="..v)
122             end
123         end
124     end
125 end
126
127 function fixme(name,client)
128     client:append( "FIXME: unimplemented command `"..name.."'." )
129 end
130
131 function logout(name,client)
132     if client.type == host.client_type.net then
133         client:send("Bye-bye!")
134         client:del()
135     else
136         client:append("Error: Can't logout of stdin/stdout. Use quit or shutdown to close VLC.")
137     end
138 end
139
140 function shutdown(name,client)
141     client:append("Bye-bye!")
142     h:broadcast("Shutting down.")
143     vlc.msg.info("Requested shutdown.")
144     vlc.quit()
145 end
146
147 function quit(name,client)
148     if client.type == host.client_type.net then
149         logout(name,client)
150     else
151         shutdown(name,client)
152     end
153 end
154
155 function add(name,client,arg)
156     -- TODO: parse (and use) options
157     local f
158     if name == "enqueue" then
159         f = vlc.playlist.enqueue
160     else
161         f = vlc.playlist.add
162     end
163     f({{path=arg}})
164 end
165
166 function playlist(name,client,arg)
167     -- TODO: add possibility to filter playlist items using a mask
168     local playlist
169     if arg == "ml" then
170         playlist = vlc.playlist.get(true)
171         client:append("+----[ Playlist - Media Library ]")
172     else
173         playlist = vlc.playlist.get()
174         client:append("+----[ Playlist ]")
175     end
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)..")"
180         end
181         if item.nb_played > 0 then
182             str = str.." played "..tostring(item.nb_played).." time"
183             if item.nb_played > 1 then
184                 str = str .. "s"
185             end
186         end
187         client:append(str)
188     end
189     client:append("+----[ End of playlist ]")
190 end
191
192 function print_text(label,text)
193     return function(name,client)
194         client:append("+----[ "..label.." ]")
195         client:append "|"
196         for line in string.gmatch(text,".-\r?\n") do
197             client:append("| "..string.gsub(line,"\r?\n",""))
198         end
199         client:append "|"
200         client:append("+----[ End of "..string.lower(label).." ]")
201     end
202 end
203
204 function help(name,client,arg)
205     local width = client.env.width
206     local long = (name == "longhelp")
207     local extra = ""
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
214             if cmd ~= "" then
215                 local val = commands[cmd]
216                 if val.aliases then
217                     for _,a in ipairs(val.aliases) do
218                         str = str .. ", " .. a
219                     end
220                 end
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
225             end
226             client:append(str)
227         end
228     end
229     client:append("+----[ end of help ]")
230 end
231
232 function input_info(name,client)
233     local categories = vlc.input_info()
234     for cat, infos in pairs(categories) do
235         client:append("+----[ "..cat.." ]")
236         client:append("|")
237         for name, value in pairs(infos) do
238             client:append("| "..name..": "..value)
239         end
240         client:append("|")
241     end
242     client:append("+----[ end of stream info ]")
243 end
244
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) .. " )")
250 end
251
252 function is_playing(name,client)
253     if vlc.is_playing() then client:append "1" else client:append "0" end
254 end
255
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
260 end
261
262 function get_time(var,client)
263     return function()
264         local input = vlc.object.input()
265         client:append(math.floor(vlc.var.get( input, var )))
266     end
267 end
268
269 function titlechap(name,client,value)
270     local input = vlc.object.input()
271     local var = string.gsub( name, "_.*$", "" )
272     if value then
273         vlc.var.set( input, var, value )
274     else
275         local item = vlc.var.get( input, var )
276         -- Todo: add item name conversion
277         client:apped(item)
278     end
279 end
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 )
285     end
286 end
287
288 function seek(name,client,value)
289     common.seek(value)
290 end
291
292 function volume(name,client,value)
293     if value then
294         vlc.volume.set(value)
295     else
296         client:append(tostring(vlc.volume.get()))
297     end
298 end
299
300 function rate(name,client)
301     local input = vlc.object.input()
302     if name == "normal" then
303         vlc.var.set(input,"rate",1000) -- FIXME: INPUT_RATE_DEFAULT
304     else
305         vlc.var.set(input,"rate-"..name,nil)
306     end
307 end
308
309 function listvalue(obj,var)
310     return function(client,value)
311         local o = vlc.object.find(nil,obj,"anywhere")
312         if not o then return end
313         if value then
314             vlc.var.set( o, var, value )
315         else
316             local c = vlc.var.get( o, var )
317             local v, l = vlc.var.get_list( o, var )
318             client:append("+----[ "..var.." ]")
319             for i,val in ipairs(v) do
320                 local mark = (val==c)and " *" or ""
321                 client:append("| "..tostring(val).." - "..tostring(l[i])..mark)
322             end
323             client:append("+----[ end of "..var.." ]")
324         end
325     end
326 end
327
328 function eval(client,val)
329     client:append(loadstring("return "..val)())
330 end
331
332 --[[ Declare commands, register their callback functions and provide
333      help strings here.
334      Syntax is:
335      "<command name>"; { func = <function>; [ args = "<str>"; ] help = "<str>"; [ adv = <bool>; ] [ aliases = { ["<str>";]* }; ] }
336      ]]
337 commands_ordered = {
338     { "add"; { func = add; args = "XYZ"; help = "add XYZ to playlist" } };
339     { "enqueue"; { func = add; args = "XYZ"; help = "queue XYZ to playlist" } };
340     { "playlist"; { func = playlist; help = "show items currently in playlist" } };
341     { "play"; { func = skip2(vlc.playlist.play); help = "play stream" } };
342     { "stop"; { func = skip2(vlc.playlist.stop); help = "stop stream" } };
343     { "next"; { func = skip2(vlc.playlist.next); help = "next playlist item" } };
344     { "prev"; { func = skip2(vlc.playlist.prev); help = "previous playlist item" } };
345     { "goto"; { func = skip2(vlc.playlist.goto); help = "goto item at index" } };
346     { "repeat"; { func = skip2(vlc.playlist.repeat_); args = "[on|off]"; help = "toggle playlist repeat" } };
347     { "loop"; { func = skip2(vlc.playlist.loop); args = "[on|off]"; help = "toggle playlist loop" } };
348     { "random"; { func = skip2(vlc.playlist.random); args = "[on|off]"; help = "toggle playlist random" } };
349     { "clear"; { func = skip2(vlc.playlist.clear); help = "clear the playlist" } };
350     { "status"; { func = playlist_status; help = "current playlist status" } };
351     { "title"; { func = titlechap; args = "[X]"; help = "set/get title in current item" } };
352     { "title_n"; { func = titlechap_offset(1); help = "next title in current item" } };
353     { "title_p"; { func = titlechap_offset(-1); help = "previous title in current item" } };
354     { "chapter"; { func = titlechap; args = "[X]"; help = "set/get chapter in current item" } };
355     { "chapter_n"; { func = titlechap_offset(1); help = "next chapter in current item" } };
356     { "chapter_p"; { func = titlechap_offset(-1); help = "previous chapter in current item" } };
357     { "" };
358     { "seek"; { func = seek; args = "X"; help = "seek in seconds, for instance `seek 12'" } };
359     { "pause"; { func = setarg(common.hotkey,"key-play-pause"); help = "toggle pause" } };
360     { "fastforward"; { func = setarg(common.hotkey,"key-jump+extrashort"); help = "set to maximum rate" } };
361     { "rewind"; { func = setarg(common.hotkey,"key-jump-extrashort"); help = "set to minimum rate" } };
362     { "faster"; { func = rate; help = "faster playing of stream" } };
363     { "slower"; { func = rate; help = "slower playing of stream" } };
364     { "normal"; { func = rate; help = "normal playing of stream" } };
365     { "fullscreen"; { func = skip2(vlc.fullscreen); args = "[on|off]"; help = "toggle fullscreen"; aliases = { "f", "F" } } };
366     { "info"; { func = input_info; help = "information about the current stream" } };
367     { "get_time"; { func = get_time("time"); help = "seconds elapsed since stream's beginning" } };
368     { "is_playing"; { func = is_playing; help = "1 if a stream plays, 0 otherwise" } };
369     { "get_title"; { func = ret_print(vlc.get_title); help = "the title of the current stream" } };
370     { "get_length"; { func = get_time("length"); help = "the length of the current stream" } };
371     { "" };
372     { "volume"; { func = volume; args = "[X]"; help = "set/get audio volume" } };
373     { "volup"; { func = ret_print(vlc.volume.up,"( audio volume: "," )"); args = "[X]"; help = "raise audio volume X steps" } };
374     { "voldown"; { func = ret_print(vlc.volume.down,"( audio volume: "," )"); args = "[X]"; help = "lower audio volume X steps" } };
375     { "adev"; { func = skip(listvalue("aout","audio-device")); args = "[X]"; help = "set/get audio device" } };
376     { "achan"; { func = skip(listvalue("aout","audio-channels")); args = "[X]"; help = "set/get audio channels" } };
377     { "atrack"; { func = skip(listvalue("input","audio-es")); args = "[X]"; help = "set/get audio track" } };
378     { "vtrack"; { func = skip(listvalue("input","video-es")); args = "[X]"; help = "set/get video track" } };
379     { "vratio"; { func = skip(listvalue("vout","aspect-ratio")); args = "[X]"; help = "set/get video aspect ratio" } };
380     { "vcrop"; { func = skip(listvalue("vout","crop")); args = "[X]"; help = "set/get video crop"; aliases = { "crop" } } };
381     { "vzoom"; { func = skip(listvalue("vout","zoom")); args = "[X]"; help = "set/get video zoom"; aliases = { "zoom" } } };
382     { "snapshot"; { func = common.snapshot; help = "take video snapshot" } };
383     { "strack"; { func = skip(listvalue("input","spu-es")); args = "[X]"; help = "set/get subtitles track" } };
384     { "hotkey"; { func = skip(common.hotkey); args = "[hotkey name]"; help = "simulate hotkey press"; adv = true; aliases = { "key" } } };
385     { "menu"; { func = fixme; args = "[on|off|up|down|left|right|select]"; help = "use menu"; adv = true } };
386     { "" };
387     { "set"; { func = set_env; args = "[var [value]]"; help = "set/get env var"; adv = true } };
388     { "save_env"; { func = save_env; help = "save env vars (for future clients)"; adv = true } };
389     { "alias"; { func = skip(alias); args = "[cmd]"; help = "set/get command aliases"; adv = true } };
390     { "eval"; { func = skip(eval); help = "eval some lua (*debug*)"; adv =true } }; -- FIXME: comment out if you're not debugging
391     { "description"; { func = print_text("Description",description); help = "describe this module" } };
392     { "license"; { func = print_text("License message",vlc.license()); help = "print VLC's license message"; adv = true } };
393     { "help"; { func = help; args = "[pattern]"; help = "a help message"; aliases = { "?" } } };
394     { "longhelp"; { func = help; args = "[pattern]"; help = "a longer help message" } };
395     { "logout"; { func = logout; help = "exit (if in a socket connection)" } };
396     { "quit"; { func = quit; help = "quit VLC (or logout if in a socket connection)" } };
397     { "shutdown"; { func = shutdown; help = "shutdown VLC" } };
398     }
399 commands = {}
400 for i, cmd in ipairs( commands_ordered ) do
401     if #cmd == 2 then
402         commands[cmd[1]]=cmd[2]
403         if cmd[2].aliases then
404             for _,a in ipairs(cmd[2].aliases) do
405                 commands[a]=cmd[1]
406             end
407         end
408     end
409     commands_ordered[i]=cmd[1]
410 end
411 --[[ From now on commands_ordered is a list of the different command names
412      and commands is a associative array indexed by the command name. ]]
413
414 -- Compute the column width used when printing a the autocompletion list
415 env.colwidth = 0
416 for c,_ in pairs(commands) do
417     if #c > env.colwidth then env.colwidth = #c end
418 end
419 env.coldwidth = env.colwidth + 1
420
421 -- Count unimplemented functions
422 do
423     local count = 0
424     local list = "("
425     for c,v in pairs(commands) do
426         if v.func == fixme then
427             count = count + 1
428             if count ~= 1 then
429                 list = list..","
430             end
431             list = list..c
432         end
433     end
434     list = list..")"
435     if count ~= 0 then
436         env.welcome = env.welcome .. "\r\nWarning: "..count.." functions are still unimplemented "..list.."."
437     end
438 end
439
440 --[[ Utils ]]
441 function split_input(input)
442     local input = strip(input)
443     local s = string.find(input," ")
444     if s then
445         return string.sub(input,0,s-1), strip(string.sub(input,s))
446     else
447         return input
448     end
449 end
450
451 function call_command(cmd,client,arg)
452     if type(commands[cmd]) == type("") then
453         cmd = commands[cmd]
454     end
455     local ok, msg
456     if arg ~= nil then
457         ok, msg = pcall( commands[cmd].func, cmd, client, arg )
458     else
459         ok, msg = pcall( commands[cmd].func, cmd, client )
460     end
461     if not ok then
462         local a = arg or ""
463         if a ~= "" then a = " " .. a end
464         client:append("Error in `"..cmd..a.."' ".. msg)
465     end
466 end
467
468 function call_libvlc_command(cmd,client,arg)
469     local ok, vlcerr, vlcmsg = pcall( vlc.libvlc_command, cmd, arg )
470     if not ok then
471         local a = arg or ""
472         if a ~= "" then a = " " .. a end
473         client:append("Error in `"..cmd..a.."' ".. vlcerr) -- when pcall fails, the 2nd arg is the error message.
474     end
475     return vlcerr
476 end
477
478 --[[ Setup host ]]
479 require("host")
480 h = host.host()
481 -- No auth
482 h.status_callbacks[host.status.password] = function(client)
483     client.env = common.table_copy( env )
484     client:send( client.env.welcome .. "\r\n")
485     client:switch_status(host.status.read)
486 end
487 -- Print prompt when switching a client's status to `read'
488 h.status_callbacks[host.status.read] = function(client)
489     client:send( client.env.prompt )
490 end
491
492 h:listen( config.hosts or config.host or "*console" )
493
494 --[[ The main loop ]]
495 while not vlc.should_die() do
496     h:accept()
497     local write, read = h:select(0.1)
498
499     for _, client in pairs(write) do
500         local len = client:send()
501         client.buffer = string.sub(client.buffer,len+1)
502         if client.buffer == "" then client:switch_status(host.status.read) end
503     end
504
505     for _, client in pairs(read) do
506         local input = client:recv(1000)
507         local done = false
508         if string.match(input,"\n$") then
509             client.buffer = string.gsub(client.buffer..input,"\r?\n$","")
510             done = true
511         elseif client.buffer == ""
512            and ((client.type == host.client_type.stdio and input == "")
513            or  (client.type == host.client_type.net and input == "\004")) then
514             -- Caught a ^D
515             client.buffer = "quit"
516             done = true
517         else
518             client.buffer = client.buffer .. input
519         end
520         if done then
521             local cmd,arg = split_input(client.buffer)
522             client.buffer = ""
523             client:switch_status(host.status.write)
524             if commands[cmd] then
525                 call_command(cmd,client,arg)
526             else
527                 if client.type == host.client_type.stdio 
528                 and call_libvlc_command(cmd,client,arg) == 0 then
529                 else
530                     local choices = {}
531                     if client.env.autocompletion ~= 0 then
532                         for v,_ in common.pairs_sorted(commands) do
533                             if string.sub(v,0,#cmd)==cmd then
534                                 table.insert(choices, v)
535                             end
536                         end
537                     end
538                     if #choices == 1 and client.env.autoalias ~= 0 then
539                         -- client:append("Aliasing to \""..choices[1].."\".")
540                         cmd = choices[1]
541                         call_command(cmd,client,arg)
542                     else
543                         client:append("Unknown command `"..cmd.."'. Type `help' for help.")
544                         if #choices ~= 0 then
545                             client:append("Possible choices are:")
546                             local cols = math.floor(client.env.width/(client.env.colwidth+1))
547                             local fmt = "%-"..client.env.colwidth.."s"
548                             for i = 1, #choices do
549                                 choices[i] = string.format(fmt,choices[i])
550                             end
551                             for i = 1, #choices, cols do
552                                 local j = i + cols - 1
553                                 if j > #choices then j = #choices end
554                                 client:append("  "..table.concat(choices," ",i,j))
555                             end
556                         end
557                     end
558                 end
559             end
560         end
561     end
562 end