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 * flatplaylist: 0 to disable, 1 to enable.
54 ]============================================================================]
58 skip2
= function(foo
) return skip(skip(foo
)) end
59 setarg
= common
.setarg
62 --[[ Setup default environement ]]
63 env
= { prompt
= "> ";
67 welcome
= "Remote control interface initialized. Type `help' for help.";
71 --[[ Import custom environement variables from the command line config (if possible) ]]
72 for k
,v
in pairs(env
) do
74 if type(env
[k
]) == type(config
[k
]) then
76 vlc
.msg
.dbg("set environement variable `"..k
.."' to "..tonumber(env
[k
]))
78 vlc
.msg
.err("environement variable `"..k
.."' should be of type "..type(env
[k
])..". config value will be discarded.")
83 --[[ Command functions ]]
84 function set_env(name
,client
,value
)
86 local var
,val
= split_input(value
)
88 s
= string.gsub(val
,"\"(.*)\"","%1")
89 if type(client
.env
[var
])==type(1) then
90 client
.env
[var
] = tonumber(s
)
95 client
:append( tostring(client
.env
[var
]) )
98 for e
,v
in common
.pairs_sorted(client
.env
) do
99 client
:append(e
.."="..v
)
104 function save_env(name
,client
,value
)
105 env
= common
.table_copy(client
.env
)
108 function alias(client
,value
)
110 local var
,val
= split_input(value
)
111 if commands
[var
] and type(commands
[var
]) ~= type("") then
112 client
:append("Error: cannot use a primary command as an alias name")
114 if commands
[val
] then
117 client
:append("Error: unknown primary command `"..val
.."'.")
121 for c
,v
in common
.pairs_sorted(commands
) do
122 if type(v
)==type("") then
123 client
:append(c
.."="..v
)
129 function fixme(name
,client
)
130 client
:append( "FIXME: unimplemented command `"..name
.."'." )
133 function logout(name
,client
)
134 if client
.type == host
.client_type
.net
then
135 client
:send("Bye-bye!")
138 client
:append("Error: Can't logout of stdin/stdout. Use quit or shutdown to close VLC.")
142 function shutdown(name
,client
)
143 client
:append("Bye-bye!")
144 h
:broadcast("Shutting down.")
145 vlc
.msg
.info("Requested shutdown.")
149 function quit(name
,client
)
150 if client
.type == host
.client_type
.net
then
153 shutdown(name
,client
)
157 function add(name
,client
,arg
)
158 -- TODO: parse (and use) options
160 if name
== "enqueue" then
161 f
= vlc
.playlist
.enqueue
168 function playlist_is_tree( client
)
169 if client
.env
.flatplaylist
== 0 then
176 function playlist(name
,client
,arg
)
177 function playlist0(item
,prefix
)
178 local prefix
= prefix
or ""
179 if not item
.flags
.disabled
then
180 local str
= "| "..prefix
..tostring(item
.id
).." - "..item
.name
181 if item
.duration
> 0 then
182 str
= str
.." ("..common
.durationtostring(item
.duration
)..")"
184 if item
.nb_played
> 0 then
185 str
= str
.." [played "..tostring(item
.nb_played
).." time"
186 if item
.nb_played
> 1 then
193 if item
.children
then
194 for _
, c
in ipairs(item
.children
) do
195 playlist0(c
,prefix
.." ")
200 local tree
= playlist_is_tree(client
)
201 if name
== "search" then
202 playlist
= vlc
.playlist
.search(arg
or "", tree
)
204 if tonumber(arg
) then
205 playlist
= vlc
.playlist
.get(tonumber(arg
), tree
)
207 playlist
= vlc
.playlist
.get(arg
, tree
)
209 playlist
= vlc
.playlist
.get(nil, tree
)
212 if name
== "search" then
213 client
:append("+----[ Search - "..(arg
or "`reset'").." ]")
215 client
:append("+----[ Playlist - "..playlist
.name
.." ]")
217 if playlist
.children
then
218 for _
, item
in ipairs(playlist
.children
) do
224 if name
== "search" then
225 client
:append("+----[ End of search - Use `search' to reset ]")
227 client
:append("+----[ End of playlist ]")
231 function playlist_sort(name
,client
,arg
)
233 client
:append("Valid sort keys are: id, title, artist, genre, random, duration, album.")
235 local tree
= playlist_is_tree(client
)
236 vlc
.playlist
.sort(arg
,false,tree
)
240 function services_discovery(name
,client
,arg
)
242 if vlc
.sd
.is_loaded(arg
) then
244 client
:append(arg
.." disabled.")
247 client
:append(arg
.." enabled.")
250 local sd
= vlc
.sd
.get_services_names()
251 client
:append("+----[ Services discovery ]")
252 for n
,ln
in pairs(sd
) do
254 if vlc
.sd
.is_loaded(n
) then
259 client
:append("| "..n
..": " .. ln
.. " (" .. status
.. ")")
261 client
:append("+----[ End of services discovery ]")
265 function print_text(label
,text
)
266 return function(name
,client
)
267 client
:append("+----[ "..label
.." ]")
269 for line
in string.gmatch(text
,".-\r?\n") do
270 client
:append("| "..string.gsub(line
,"\r?\n",""))
273 client
:append("+----[ End of "..string.lower(label
).." ]")
277 function help(name
,client
,arg
)
278 local width
= client
.env
.width
279 local long
= (name
== "longhelp")
281 if arg
then extra
= "matching `" .. arg
.. "' " end
282 client
:append("+----[ Remote control commands "..extra
.."]")
283 for i
, cmd
in ipairs(commands_ordered
) do
284 if (cmd
== "" or not commands
[cmd
].adv
or long
)
285 and (not arg
or string.match(cmd
,arg
)) then
286 local str
= "| " .. cmd
288 local val
= commands
[cmd
]
290 for _
,a
in ipairs(val
.aliases
) do
291 str
= str
.. ", " .. a
294 if val
.args
then str
= str
.. " " .. val
.args
end
295 if #str
%2 == 1 then str
= str
.. " " end
296 str
= str
.. string.rep(" .",(width
-(#str
+#val
.help
)-1)/2)
297 str
= str
.. string.rep(" ",width
-#str
-#val
.help
) .. val
.help
302 client
:append("+----[ end of help ]")
305 function input_info(name
,client
)
306 local categories
= vlc
.input_info()
307 for cat
, infos
in pairs(categories
) do
308 client
:append("+----[ "..cat
.." ]")
310 for name
, value
in pairs(infos
) do
311 client
:append("| "..name
..": "..value
)
315 client
:append("+----[ end of stream info ]")
318 function playlist_status(name
,client
)
319 local a
,b
,c
= vlc
.playlist
.status()
320 client
:append( "( new input: " .. tostring(a
) .. " )" )
321 client
:append( "( audio volume: " .. tostring(b
) .. " )")
322 client
:append( "( state " .. tostring(c
) .. " )")
325 function is_playing(name
,client
)
326 if vlc
.input
.is_playing() then client
:append
"1" else client
:append
"0" end
329 function ret_print(foo
,start
,stop
)
330 local start
= start
or ""
331 local stop
= stop
or ""
332 return function(discard
,client
,...) client
:append(start
..tostring(foo(...))..stop
) end
335 function get_time(var
,client
)
337 local input
= vlc
.object
.input()
338 client
:append(math
.floor(vlc
.var
.get( input
, var
)))
342 function titlechap(name
,client
,value
)
343 local input
= vlc
.object
.input()
344 local var
= string.gsub( name
, "_.*$", "" )
346 vlc
.var
.set( input
, var
, value
)
348 local item
= vlc
.var
.get( input
, var
)
349 -- Todo: add item name conversion
353 function titlechap_offset(client
,offset
)
354 return function(name
,value
)
355 local input
= vlc
.object
.input()
356 local var
= string.gsub( name
, "_.*$", "" )
357 vlc
.var
.set( input
, var
, vlc
.var
.get( input
, var
)+offset
)
361 function seek(name
,client
,value
)
365 function volume(name
,client
,value
)
367 vlc
.volume
.set(value
)
369 client
:append(tostring(vlc
.volume
.get()))
373 function rate(name
,client
)
374 local input
= vlc
.object
.input()
375 if name
== "normal" then
376 vlc
.var
.set(input
,"rate",1000) -- FIXME: INPUT_RATE_DEFAULT
378 vlc
.var
.set(input
,"rate-"..name
,nil)
382 function listvalue(obj
,var
)
383 return function(client
,value
)
384 local o
= vlc
.object
.find(nil,obj
,"anywhere")
385 if not o
then return end
387 vlc
.var
.set( o
, var
, value
)
389 local c
= vlc
.var
.get( o
, var
)
390 local v
, l
= vlc
.var
.get_list( o
, var
)
391 client
:append("+----[ "..var
.." ]")
392 for i
,val
in ipairs(v
) do
393 local mark
= (val
==c
)and " *" or ""
394 client
:append("| "..tostring(val
).." - "..tostring(l
[i
])..mark
)
396 client
:append("+----[ end of "..var
.." ]")
401 function eval(client
,val
)
402 client
:append(loadstring("return "..val
)())
405 --[[ Declare commands, register their callback functions and provide
408 "<command name>"; { func = <function>; [ args = "<str>"; ] help = "<str>"; [ adv = <bool>; ] [ aliases = { ["<str>";]* }; ] }
411 { "add"; { func
= add
; args
= "XYZ"; help
= "add XYZ to playlist" } };
412 { "enqueue"; { func
= add
; args
= "XYZ"; help
= "queue XYZ to playlist" } };
413 { "playlist"; { func
= playlist
; help
= "show items currently in playlist" } };
414 { "search"; { func
= playlist
; args
= "[string]"; help
= "search for items in playlist (or reset search)" } };
415 { "sort"; { func
= playlist_sort
; args
= "key"; help
= "sort the playlist" } };
416 { "sd"; { func
= services_discovery
; args
= "[sd]"; help
= "show services discovery or toggle" } };
417 { "play"; { func
= skip2(vlc
.playlist
.play
); help
= "play stream" } };
418 { "stop"; { func
= skip2(vlc
.playlist
.stop
); help
= "stop stream" } };
419 { "next"; { func
= skip2(vlc
.playlist
.next); help
= "next playlist item" } };
420 { "prev"; { func
= skip2(vlc
.playlist
.prev
); help
= "previous playlist item" } };
421 { "goto"; { func
= skip2(vlc
.playlist
.goto
); help
= "goto item at index" } };
422 { "repeat"; { func
= skip2(vlc
.playlist
.repeat_
); args
= "[on|off]"; help
= "toggle playlist repeat" } };
423 { "loop"; { func
= skip2(vlc
.playlist
.loop
); args
= "[on|off]"; help
= "toggle playlist loop" } };
424 { "random"; { func
= skip2(vlc
.playlist
.random); args
= "[on|off]"; help
= "toggle playlist random" } };
425 { "clear"; { func
= skip2(vlc
.playlist
.clear
); help
= "clear the playlist" } };
426 { "status"; { func
= playlist_status
; help
= "current playlist status" } };
427 { "title"; { func
= titlechap
; args
= "[X]"; help
= "set/get title in current item" } };
428 { "title_n"; { func
= titlechap_offset(1); help
= "next title in current item" } };
429 { "title_p"; { func
= titlechap_offset(-1); help
= "previous title in current item" } };
430 { "chapter"; { func
= titlechap
; args
= "[X]"; help
= "set/get chapter in current item" } };
431 { "chapter_n"; { func
= titlechap_offset(1); help
= "next chapter in current item" } };
432 { "chapter_p"; { func
= titlechap_offset(-1); help
= "previous chapter in current item" } };
434 { "seek"; { func
= seek; args
= "X"; help
= "seek in seconds, for instance `seek 12'" } };
435 { "pause"; { func
= setarg(common
.hotkey
,"key-play-pause"); help
= "toggle pause" } };
436 { "fastforward"; { func
= setarg(common
.hotkey
,"key-jump+extrashort"); help
= "set to maximum rate" } };
437 { "rewind"; { func
= setarg(common
.hotkey
,"key-jump-extrashort"); help
= "set to minimum rate" } };
438 { "faster"; { func
= rate
; help
= "faster playing of stream" } };
439 { "slower"; { func
= rate
; help
= "slower playing of stream" } };
440 { "normal"; { func
= rate
; help
= "normal playing of stream" } };
441 { "fullscreen"; { func
= skip2(vlc
.video
.fullscreen
); args
= "[on|off]"; help
= "toggle fullscreen"; aliases
= { "f", "F" } } };
442 { "info"; { func
= input_info
; help
= "information about the current stream" } };
443 { "get_time"; { func
= get_time("time"); help
= "seconds elapsed since stream's beginning" } };
444 { "is_playing"; { func
= is_playing
; help
= "1 if a stream plays, 0 otherwise" } };
445 { "get_title"; { func
= ret_print(vlc
.input
.get_title
); help
= "the title of the current stream" } };
446 { "get_length"; { func
= get_time("length"); help
= "the length of the current stream" } };
448 { "volume"; { func
= volume
; args
= "[X]"; help
= "set/get audio volume" } };
449 { "volup"; { func
= ret_print(vlc
.volume
.up
,"( audio volume: "," )"); args
= "[X]"; help
= "raise audio volume X steps" } };
450 { "voldown"; { func
= ret_print(vlc
.volume
.down
,"( audio volume: "," )"); args
= "[X]"; help
= "lower audio volume X steps" } };
451 { "adev"; { func
= skip(listvalue("aout","audio-device")); args
= "[X]"; help
= "set/get audio device" } };
452 { "achan"; { func
= skip(listvalue("aout","audio-channels")); args
= "[X]"; help
= "set/get audio channels" } };
453 { "atrack"; { func
= skip(listvalue("input","audio-es")); args
= "[X]"; help
= "set/get audio track" } };
454 { "vtrack"; { func
= skip(listvalue("input","video-es")); args
= "[X]"; help
= "set/get video track" } };
455 { "vratio"; { func
= skip(listvalue("vout","aspect-ratio")); args
= "[X]"; help
= "set/get video aspect ratio" } };
456 { "vcrop"; { func
= skip(listvalue("vout","crop")); args
= "[X]"; help
= "set/get video crop"; aliases
= { "crop" } } };
457 { "vzoom"; { func
= skip(listvalue("vout","zoom")); args
= "[X]"; help
= "set/get video zoom"; aliases
= { "zoom" } } };
458 { "snapshot"; { func
= common
.snapshot
; help
= "take video snapshot" } };
459 { "strack"; { func
= skip(listvalue("input","spu-es")); args
= "[X]"; help
= "set/get subtitles track" } };
460 { "hotkey"; { func
= skip(common
.hotkey
); args
= "[hotkey name]"; help
= "simulate hotkey press"; adv
= true; aliases
= { "key" } } };
461 { "menu"; { func
= fixme
; args
= "[on|off|up|down|left|right|select]"; help
= "use menu"; adv
= true } };
463 { "set"; { func
= set_env
; args
= "[var [value]]"; help
= "set/get env var"; adv
= true } };
464 { "save_env"; { func
= save_env
; help
= "save env vars (for future clients)"; adv
= true } };
465 { "alias"; { func
= skip(alias
); args
= "[cmd]"; help
= "set/get command aliases"; adv
= true } };
466 { "eval"; { func
= skip(eval
); help
= "eval some lua (*debug*)"; adv
=true } }; -- FIXME: comment out if you're not debugging
467 { "description"; { func
= print_text("Description",description
); help
= "describe this module" } };
468 { "license"; { func
= print_text("License message",vlc
.misc
.license()); help
= "print VLC's license message"; adv
= true } };
469 { "help"; { func
= help
; args
= "[pattern]"; help
= "a help message"; aliases
= { "?" } } };
470 { "longhelp"; { func
= help
; args
= "[pattern]"; help
= "a longer help message" } };
471 { "logout"; { func
= logout
; help
= "exit (if in a socket connection)" } };
472 { "quit"; { func
= quit
; help
= "quit VLC (or logout if in a socket connection)" } };
473 { "shutdown"; { func
= shutdown
; help
= "shutdown VLC" } };
476 for i
, cmd
in ipairs( commands_ordered
) do
478 commands
[cmd
[1]]
=cmd
[2]
479 if cmd
[2].aliases
then
480 for _
,a
in ipairs(cmd
[2].aliases
) do
485 commands_ordered
[i
]=cmd
[1]
487 --[[ From now on commands_ordered is a list of the different command names
488 and commands is a associative array indexed by the command name. ]]
490 -- Compute the column width used when printing a the autocompletion list
492 for c
,_
in pairs(commands
) do
493 if #c
> env
.colwidth
then env
.colwidth
= #c
end
495 env
.coldwidth
= env
.colwidth
+ 1
497 -- Count unimplemented functions
501 for c
,v
in pairs(commands
) do
502 if v
.func
== fixme
then
512 env
.welcome
= env
.welcome
.. "\r\nWarning: "..count
.." functions are still unimplemented "..list
.."."
517 function split_input(input
)
518 local input
= strip(input
)
519 local s
= string.find(input
," ")
521 return string.sub(input
,0,s
-1), strip(string.sub(input
,s
))
527 function call_command(cmd
,client
,arg
)
528 if type(commands
[cmd
]) == type("") then
533 ok
, msg
= pcall( commands
[cmd
].func
, cmd
, client
, arg
)
535 ok
, msg
= pcall( commands
[cmd
].func
, cmd
, client
)
539 if a
~= "" then a
= " " .. a
end
540 client
:append("Error in `"..cmd
..a
.."' ".. msg
)
544 function call_libvlc_command(cmd
,client
,arg
)
545 local ok
, vlcerr
, vlcmsg
= pcall( vlc
.var
.libvlc_command
, cmd
, arg
)
548 if a
~= "" then a
= " " .. a
end
549 client
:append("Error in `"..cmd
..a
.."' ".. vlcerr
) -- when pcall fails, the 2nd arg is the error message.
558 h
.status_callbacks
[host
.status
.password
] = function(client
)
559 client
.env
= common
.table_copy( env
)
560 client
:send( client
.env
.welcome
.. "\r\n")
561 client
:switch_status(host
.status
.read)
563 -- Print prompt when switching a client's status to `read'
564 h
.status_callbacks
[host
.status
.read] = function(client
)
565 client
:send( client
.env
.prompt
)
568 h
:listen( config
.hosts
or config
.host
or "*console" )
570 --[[ The main loop ]]
571 while not vlc
.misc
.should_die() do
573 local write, read = h
:select(0.1)
575 for _
, client
in pairs(write) do
576 local len
= client
:send()
577 client
.buffer
= string.sub(client
.buffer
,len
+1)
578 if client
.buffer
== "" then client
:switch_status(host
.status
.read) end
581 for _
, client
in pairs(read) do
582 local input
= client
:recv(1000)
584 if string.match(input
,"\n$") then
585 client
.buffer
= string.gsub(client
.buffer
..input
,"\r?\n$","")
587 elseif client
.buffer
== ""
588 and ((client
.type == host
.client_type
.stdio
and input
== "")
589 or (client
.type == host
.client_type
.net
and input
== "\004")) then
591 client
.buffer
= "quit"
594 client
.buffer
= client
.buffer
.. input
597 local cmd
,arg
= split_input(client
.buffer
)
599 client
:switch_status(host
.status
.write)
600 if commands
[cmd
] then
601 call_command(cmd
,client
,arg
)
603 if client
.type == host
.client_type
.stdio
604 and call_libvlc_command(cmd
,client
,arg
) == 0 then
607 if client
.env
.autocompletion
~= 0 then
608 for v
,_
in common
.pairs_sorted(commands
) do
609 if string.sub(v
,0,#cmd
)==cmd
then
610 table.insert(choices
, v
)
614 if #choices
== 1 and client
.env
.autoalias
~= 0 then
615 -- client:append("Aliasing to \""..choices[1].."\".")
617 call_command(cmd
,client
,arg
)
619 client
:append("Unknown command `"..cmd
.."'. Type `help' for help.")
620 if #choices
~= 0 then
621 client
:append("Possible choices are:")
622 local cols
= math
.floor(client
.env
.width
/(client
.env
.colwidth
+1))
623 local fmt
= "%-"..client
.env
.colwidth
.."s"
624 for i
= 1, #choices
do
625 choices
[i
] = string.format(fmt
,choices
[i
])
627 for i
= 1, #choices
, cols
do
628 local j
= i
+ cols
- 1
629 if j
> #choices
then j
= #choices
end
630 client
:append(" "..table.concat(choices
," ",i
,j
))