History always repeats itself and sometimes that's good. Here's a second chance to be part of it: tmdtc.com

Sunday, 19 August 2007

Building a webcam recorder with VLC, Ion and Ruby

I promised to build a system to let people leave video messages at a party. The requirements were:
  • people have to see the stream while it is recorded, so they're sure they are in the video
  • the system has to be extremely simple to use, possibly requiring the use of only one keyboard keypress to start and stop recording
  • the recorded video have a maximum length, so that we avoid recording when someone forgot to stop the recording after his/her message
After looking at several options, including GStreamer and Ffmpeg, there was no easy solution in sight. And then I took a look at Videolan, also known as VLC. Here's how I got to a working solution. Note however this is a quick hack.

With VLC's WxWidgets interface, it is easy to open a video4linux device (I'm using QuickCam Express II): File > Open Capture Device:



In this dialog, check that the correct device is opened (I open /dev/video0), and things should work as expected: when you click "OK", you see the images captured by your webcam.


In the dialog above, you might have seen the checkbox "Stream/Save". Checking it enables to save the stream from your webcam to a file, the spec of this file being set in the dialog appearing when you click on the Settings Button:


From top to bottom:
  1. Check "Play locally", as this shows the stream captured by the webcam on your screen
  2. check "File", as this will save the stream coming from the webcam into the filename you enter in the textbox.
  3. choose MP4 as "Encapsulated Method". You need to choose this carefully as not all encapsulation methods accept all format. Check the VLC streaming features page for more info.
  4. According to the Encapsulation Method you have chosen, choose the right video and audio codec. For MP4 it's respectively mp4v and mpga. If you only want to dump the video, only the video checkbox has to be ticked.
Clicking ok in each dialog will show you the stream in VLC's window, and dump it in the file you entered in the configuration dialog.

Now that it is working with the interface, the next step is to get it working from the command line. As you may have noticed, when you set options in the configuration dialogs above, a text field is filled accordingly. For example, when setting the configuration in the "Save/Stream" options dialog, the top text field has its value set to

which contain all configuration option set in the dialog.

And in the "Open Capture Device", the bottom text field has its value set to

v4l:// :v4l-vdev="/dev/video0" :v4l-adev="/dev/dsp" :v4l-norm=3 :v4l-frequency=-1


These configuration strings can be used on the command line when launching vlc. Each v4l corresponds to a long option passed to vlc. :v4l-vdev="/dev/video0" becomes --v4l-vdev /dev/video0 on the command line.

We end up with this command:



The next step is to start and stop the recording from a script. This is possible thanks to the RC interface offered by vlc. the RC interface, launched by passing the option -I RC to vlc, gives you a command prompt to control VLC. With the option "--rc-host localhost:4444", this command interface is even reachable by telnet on port 4444.

Typing help at the command prompt will give all commands available, but we'll only need:
  • add v4l:// to start the recording. It opens the v4l device passed in the options of the command line, and outputs the stream to what we configured on the command line as well: the file and the screen.
  • stop to end the recording
Before building an interface on this, I had to correct a little problem: the image displayed was way too dark. In the vlc interface, in Settings > Extended GUI , you have access to Image adjustments, where you can activate and set Hue, Contrast, Brightness, Saturation and Gamma correction. These can be set and activated from the command line with the corresponding option. Here are the options I had to use:

--contrast 1.9 --brightness 1.7 --saturation 2.3


I've had problem sometimes for the options to be taken into account. Going through the vlc extended GUI and enabling the image adjustements usually fixed it.

Building a little interface on this is quite straight-forward. I used Ruby and Tk. The GUI is made of.... 1 button used to start and stop the recording. It is triggered by pressing the "Enter" key. The control GUI also shows how long, in seconds, the person still has before reaching the maximum lenght of the recording.



The script starts VLC with the RC interface and the telnet connections accepted on port 4444. All commands are passed to VLC through the telnet connection. VLC always stores the dumped stream in the same location, and at the end of the recording session, the script moves it over to a definitive location, with a unique name based on the timestamp.

The problem with this setup in a "standard" window manager, is that recording starts, a window is opened to display the image coming in from the camera. this window covers the command unterface, which makes it impossible for the user to simply press on Enter to stop the recording, and the focus is on the vlc output window.

For Ion 3 users, there a quick solution here: split your workspace in two, and make the GUI appear in the left frame, and the vlc output window in the right. What's even cooler is that when the vlc output window is opened when the user starts the recording, the focus stays with the command GUI.

I will assume that you have installed Ion3, and copied the standard config files to ~/.ion3, as explained in the Ion3 documentation.

I created a new user, for which I set Ion3 as the window manager to use. I'm using kdm, so I edit .xsession with this content

exec /usr/local/ion3/bin/ion3


and at log in I choose the "Default" window manager.

When you log in, you get a workspace with one frame. To split it vertically, use Meta+K S. You can bind the key you want to Meta in ~/.ion3/cfg_ion.lua. I set it to Meta4, which is the Windows key on my laptop.

To identify the frame, it is necessary to give them a name. Focus the frame you want to rename, and press Meta+F3. This brings up the Lua code interpreter. Type the following command:

mod_query.query_renameframe(_)


and press "Enter". You are then promted for a name. Give the name app to the left frame, and vlc to the right one.
Ion automatically saves your workspaces when you log out. As the app is running under a specific user, this will not have to be repeated.

To make a window appear in a specific frame is achieved with the defwinprop directive in the user configuration of Ion. To identify the properties of a window, the xprop command can be used, or, even better when using the Ion WM, use the frame context menu (left click on the title bar of the frame, of Meta+M) to get the "Window Info":



Once you have this info, you can configure the GUI window to appear in the left frame.

The class of the command GUI is Webcamrecorder.rb, the instance is webcamrecorder.rb. It should go in the app frame, and focus should go to this window. This gives this configuration line in cfg_user.lua:



The problem with the vlc output window is that it has no class nor instance property set... Only the title is set to "VLC XVideo ouput", which is an identification information not supported by defwinprop.... I set this in the cfg_user.lua file, so that all windows except the control GUI appear in the right frame:

defwinprop { target = "vlc"}


I set the last line of the cfg_user.lua file to

exec('xterm -e /usr/local/bin/webcamrecorder.rb')

to start the webcamrecorder immediately when logging in.

Note that I first wanted to place this in the .xsession file, but it didn't work. Apparently, webcamrecorder.rb has to be started from a terminal, or it won't work. I suspect this is due to the RC interface we request from vlc.

Once you have your solution up, you can clean your Ion bindings to avoid users switching frames, creating workspaces or starting terminals. I also cleaned up cfg_ion.lua to limit the number of modules loaded. I end up with these config files: cfg_ion.lua, cfg_ionws.lua, cfg_menu.lua, cfg_query.lua. Together with the webcamrecorder.rb script, it's all what makes up this quick hack.

Update: Richard took the script and made a GTK version. He posted it in the comments, together with information on how he's using v4l2. I put the script in a file webcamrecordergtk.rb that you can download. Thanks Richard!

21 comments:

Jeff Barczewski said...

Thanks for sharing your work, this could be useful for many others.

Benno said...

I can't download the files:
Not found
Error 404

raphinou said...

links have been corrected. I think Blogger messed up the links as I had tested it before publishing....

Richard said...

This is exactly the write up I was looking for (application will be in a carputer to use a webcam for recording autocross runs...I need it to be as simple as possible to operate). Thanks!

Anonymous said...

VLC doesn't show my webcam :-(

I can get the webcam to work (and capture) using luvcview with no problem.

The thing was that I need to record audio and luvcview doesn't support it.

raphinou said...

anonymous, are you sure you open the correct device?

Anonymous said...

Yes I'm sure because I use the same device for luvcview.

This is the error messages I get in the shell:
[00000377] main input error: refcount is 1, delaying again (id=377,type=-7)
[00000377] main input error: waited too long, cancelling destruction (id=377,type=-7)
[00000454] v4l demuxer error: cannot get channel infos (Invalid argument)

(.:7960): Gtk-CRITICAL **: gtk_container_remove: assertion `GTK_IS_TOOLBAR (container) || widget->parent == GTK_WIDGET (container)' failed

(.:7960): Gtk-CRITICAL **: gtk_container_remove: assertion `GTK_IS_TOOLBAR (container) || widget->parent == GTK_WIDGET (container)' failed

(.:7960): Gtk-CRITICAL **: gtk_container_remove: assertion `GTK_IS_TOOLBAR (container) || widget->parent == GTK_WIDGET (container)' failed

(.:7960): Gtk-CRITICAL **: gtk_container_remove: assertion `GTK_IS_TOOLBAR (container) || widget->parent == GTK_WIDGET (container)' failed

(.:7960): Gtk-CRITICAL **: gtk_container_remove: assertion `GTK_IS_TOOLBAR (container) || widget->parent == GTK_WIDGET (container)' failed

(.:7960): Gtk-CRITICAL **: gtk_container_remove: assertion `GTK_IS_TOOLBAR (container) || widget->parent == GTK_WIDGET (container)' failed
[00000287] main playlist: stopping playback
[00000287] main playlist error: refcount is -2, delaying again (id=287,type=-5)
[00000287] main playlist error: waited too long, cancelling destruction (id=287,type=-5)

Anonymous said...

I don't know if this matters or not, but I can get video and audio working with aMSN

Anonymous said...

I think linux-uvc has a v4l2 driver. vlc doesn't support v4l2 yet.

Richard said...

I took your program and translated it to ruby GTK2/gnome2. It requires ruby-gnome2 libraries to run. This is what it looks like:



#!/usr/bin/ruby1.8

require 'gtk2'
require 'open3'
require 'net/telnet'
require 'fileutils'
require 'date'


DESTINATION_DIR = "/tmp"
MAXIMUM_DURATION= 5
UVC_STREAM_COMMAND=%{uvc_stream -r 640x480 -f 30 -p 5555}
VLC_COMMAND=%{vlc -IRC --rc-host localhost:4444 --sout '#transcode{vcodec=mp4v,vb=3072,scale=1,acodec=mpga,ab=192,channels=2}:duplicate{dst=display,dst=std{access=file,mux=mp4,dst="/tmp/test.mpg"}}'}



class Recorder
def delete_event( widget, event )
Gtk.main_quit
return false
end
def initialize
start_uvc_stream
start_vlc
Gtk.init
window = Gtk::Window.new( Gtk::Window::TOPLEVEL )
window.signal_connect( 'delete_event' ) { |w,e| delete_event( w, e ) }
window.title=( "VLC/webcam controller" )
window.border_width = ( 10 )
window.set_default_size(200,100)
window.set_keep_above(true)
window.focus_on_map=(true)
Gdk::Window::GRAVITY_SOUTH_EAST
window.move(Gdk.screen_width - 200, Gdk.screen_height - 100)
tcontainer = Gtk::HBox.new( false, 0 )
window.add( tcontainer )
ttable = Gtk::Table.new( 2, 2, true )
tcontainer.pack_start( ttable, true, true, 0 )
tbutton = Gtk::Button.new("Start Recording")
@button = tbutton
tbutton.signal_connect("clicked") {
start_stop
}
ttable.attach( tbutton, 0, 2, 0, 1 )
tframe = Gtk::Frame.new( "Status:" )
tlabel = Gtk::Label.new
tlabel.text = 'Stopped'
@label = tlabel
tframe.add( tlabel )
ttable.attach( tframe, 0, 2, 1, 2 )
tlabel.show
tframe.show
tbutton.show
ttable.show
tcontainer.show
window.show
@recording = false
#@root = TkRoot.new { title "Ex1" }
@telnet = nil
begin
@telnet = Net::Telnet.new( "Host" => "localhost", "Port" => 4444, "TelnetMode" => false, "Prompt" => //)
rescue Exception => e
sleep 1
retry
end
sleep 2
start_recording
#bind('Return') { start_stop }
Gtk.main
end


def update_button
if !@recording
start_recording
else
stop_recording
end
end
def start_vlc
@vlc_thread = Thread.new do
system VLC_COMMAND
end
@vlc_started = true
end
def start_uvc_stream
@uvc_stream_thread = Thread.new do
system UVC_STREAM_COMMAND
end
@uvc_stream_started = true
end

def start_recording
@recording = true
@button.label = 'Stop Recording'
@label.text = 'Recording...'
@telnet.cmd("add http://localhost:5555")
#sleep 1
#@telnet.cmd("f")
# @start_time = Time.now
# @counter_thread = Thread.new(@label, @start_time) do |rlabel, rstart|
# while ((duration = (Time.now - rstart).to_i)<5)
# sleep 1
# rlabel.text = %{and #{MAXIMUM_DURATION - duration - 1}s are left }
# end
# end
# stop_recording
end
def stop_recording
@recording = false
#@telnet.cmd("f")
#sleep 1
@telnet.cmd("stop")
@button.label = 'Start Recording'
file = DateTime.now.strftime("%Y-%m-%dT%H:%M:%S")
FileUtils.mv("/tmp/test.mpg", DESTINATION_DIR+"/"+file+".mpg")
# @counter_thread.kill
# @label.text = %{#{MAXIMUM_DURATION}}
@label.text = 'Stopped'
end

def start_stop
update_button
end
def terminate
@telnet.cmd("quit")
end
end



r = Recorder.new
r.terminate

Richard said...

I forgot...I launch uvcStream so I can use a V4L2 webcam, and that is what I send to VLC so it can capture it. Also, that code probably isn't readable because of the format of this blog commenting system, but copying and pasting it into a text editor should yield more usable code.

raphinou said...

Thanks Richard! I updated my original post, mentioning your script, and I put it in a downloadable file.

Anonymous said...

You can test and check your webcam it is easy.
Test my webcam

Anonymous said...

interesting take a look at
http://arcanesentiment.blogspot.com/2008/08/why-i-havent-looked-at-rebol-much.html

and you will find they have been talking about this Rebol cross platofrm GUI scripting language.

a long time rebol coder (Gregg Irwin ,thanks for taking the time BTW Greg, im begining to be convinced ;) )took a quick lok at your ruby code and produced a rebol flavour to replace all but the core vlc as the engine, so if it works (he didnt test it so it may not run!) perhaps people can learn from this and start producing more detailed and functional cross platform GUI's

it seems this rebol view is a very good self contained GUI front end maker if you can follow its programming logic....and make the time to write the code and make it available to all OC

Padmanabh said...

Can you tell how to go for VLC on windows? I have tried the steps but am not able to see the output on VLC.

raphinou said...

Sorry, I have no windows to test it on....

Raphaël

Webcam Recorder said...

great work...

Thanks for sharing.

Cricket said...

What versions of what distro did this work on? I am trying to set something up very similar but I'm not familiar with ion3. Your VLC menus also look very different and seem to have different options than the modern releases. Do you remember anything else about your system that may point me in the right direction.

Thank you for posting your setup, it may have saved me many hours in building my own.

Streaming Recorder said...

hi very ecnomical tips thanks dude you make the work easier

pnt said...

2, thanks for your post, but when i try, there no sound display on my recording, please give me some help, thanks

personyoga said...

Easily, the article is actually the best topic on this registry related issue. I fit in with your conclusions and will eagerly look forward to your next updates. Just saying thanks will not just be sufficient, for the fantasti c lucidity in your writing. I will instantly grab your rss feed to stay informed of any updates. Alena