|
Language: |
Visual Basic.NET 2005 |
|
Description: |
YCC Cam Cap is a program that automatically
saves both inbound and outbound Yahoo! Messenger
webcam streams to an AVI file. |
|
Sample: |
 |
Introduction
YCC Cam Cap is a program that
automatically saves both inbound and outbound Yahoo!
Messenger webcam streams to an AVI file. YCC Cam Cap
performs this by taking individual screen shots of the
Yahoo! Messenger webcam control and saving them into a
video file on your hard drive. YCC Cam Cap is not
selective in the webcam streams it captures but saves
any webcam control located on the desktop. This process
is carried out automatically behind the scenes so that
when a new webcam window is opened, a new file is
created and when a webcam window is closed the file is
finalized. As long as you have a powerful enough
machine, YCC Cam Cap is capable of saving an almost
unlimited number of webcam windows at the same time and
in real time.
Rational and Background
I was inspired to write this
project from MSN Webcam Recorder which can be found at
http://ml20rc.msnfanatic.com/index.html. The MSN
Webcam Recorder runs as a background application that
passively sniffs the network for MSN webcam traffic.
When a webcam conversation is detected, a file is
created and saved with that packet data. YCC Cam Cap
works differently from MSN Webcam Recorder but to the
user produces the same results.
Early on in the design process I
had to make a decision to either capture data by packets
or screen captures. Each has its advantages and
disadvantages. Packet capture is the most efficient
means to make sure your recoding is an accurate
representation of the actual stream. In this approach
you would sniff the Ethernet traffic for a Yahoo! webcam
stream and then save the raw data into useful data. The
downside to this is you would have to completely reverse
engineer the webcam protocol and use third-party
utilities to gather the data. Another drawback to this
approach was only found after I started investigating
webcam packets. If the user sending data (My Webcam)
does not have any viewers then no actual picture data is
generated. Instead, statistical data that the webcam is
ready to accept viewers is sent. This means that if you
have your cam on and displaying pictures, if no one is
viewing that data can not be captured.
The second approach involves taking
screen shots of the webcam control and then saving this
series of pictures to a file. The advantage to this
approach is an easier program design as there are many
examples of screen capture utilities available. The
disadvantages to this include system performance, stream
representation, and the many quarks of Windows. First
of all is the stream representation. Without looking at
the packets coming form the Yahoo! server, you can not
guess the timing or rate of the stream. To compensate
for this, more data than is needed is pumped into the
capture file. Simply put, you make more screen captures
per second than is needed. Another consideration is how
Windows makes screen captures. Most screen capture APIs
will simply capture an area on the screen where the
control is located. If another window happens to be in
the way or “clips” the control window then the other
application window will also be captured. Starting with
Windows XP, Microsoft introduced a new API called
PrintWindow. This API captures the desired window no
matter where it is on the screen and no mater if it is
covered by another window. Another consideration is
what happens when the control is minimized.
Unfortunately there is no API that can still capture
this window. When a window is minimized, the operating
system stops sending PAINT messages to the window. Even
if an API were to capture the window, it would only get
the last image and would not update the images until the
window is once again set to normal. With PrintWindow
all you get is a black square where the control should
be when it is minimized. This would not be a problem
with the packet system. The last consideration is
system performance. The majority of processing is done
when the picture is converted and compressed into an AVI
file. This process would occur no matter which approach
is used. On the other hand, making pictures from
packets is slightly more efficient than making pictures
from screen captures.
In the end I decided to go with the
screen capture approach. I have previously written
screen capture programs so the learning curve would be
easier. Even though capturing packets has many more
advantages, not being able to capture frames when no was
watching the webcam was a deal breaker. I feel that if
the cam is displaying pictures, it is better to get
those pictures than wait for someone to connect, if they
connect at all. Since I am using screen capture I am
still left with one major disadvantage, YCC Cam Cap will
not be able to capture data when the webcam control is
minimized.
Design
EnumWindows
The first step in capturing screen
shots is finding the window that you want to capture.
The first candidate for the job was the FindWindow API
(http://msdn2.microsoft.com/en-us/library/ms633499.aspx).
The usage for for FindWindow is very straight forward
and easy but I found that it has one fatal flaw.
FindWindow will only return one window handle and if you
have more than one webcam window then you are just out
of luck. Another API called EnumWindows
(http://msdn2.microsoft.com/en-us/library/ms633497.aspx)
does fit the bill but is more complex because it
requires a callback function and it returns all current
window handles.
EnumWindows(New EnumWindowsCallback(AddressOf filterEnum), 0)
In my case the callback function is
filterEnum and it does as the name implies,
filters the results given by EnumWindows. I check the
class name and window text of each window. In the case
of YCC Cam Cap, you need to capture a child window of
the main webcam window so
EnumChildWindows is called for each preliminary
result.
Private
Function filterEnum(ByVal hWnd
As IntPtr,
ByVal lParam As
Integer) As
Boolean
Dim sbWindowText
As New StringBuilder(STRING_BUFFER_LENGTH)
Dim sbClassName
As New StringBuilder(STRING_BUFFER_LENGTH)
GetWindowText(hWnd, sbWindowText, STRING_BUFFER_LENGTH)
GetClassName(hWnd, sbClassName, STRING_BUFFER_LENGTH)
Dim strWindowText
As String = sbWindowText.ToString
Dim strClassName
As String = sbClassName.ToString
'Match partial class name
If strClassName.ToLower.IndexOf(_strClassName.ToLower) <> -1
Then
'Match partial window title
If strWindowText.ToLower.IndexOf(_strWindowText.ToLower) <> -1
Then
'Match visiable windows
If _bListOnlyVisable =
True Then
If IsWindowVisible(hWnd) =
True Then
'go to childern
If _bEnumChild =
True Then
_strParentWindowText = strWindowText
getEnumChildWindows(hWnd)
Else
RaiseEvent ReturnProcess(hWnd, strWindowText)
End If
End If
Else
If _bEnumChild = True Then
_strParentWindowText = strWindowText
getEnumChildWindows(hWnd)
Else
RaiseEvent ReturnProcess(hWnd, strWindowText)
End If
End If
End If
End If
Return True
End Function
The function of
EnumChildWindows is very similar to EnumWindows
with the exception that an event is raised once the
proper window is found. To ensure any newly created
webcam windows are found, EnumWindows is put in a timer
and called every ten seconds by default.
Capture Setup
Once we know that there is a webcam
window to capture, we need to create a new thread for
the capture and AVI file. Since it is possible to
capture muptiple windows at once I created a structure
to hold the variables needed for each window. The
structure is called
CaptureInfo and holds such information as the
window width and height, handle, AVI class, timer, and
various capture objects.
Public Structure CaptureInfo
Dim hWindow As
IntPtr
Dim iWidth As
Integer
Dim iHeight
As
Integer
Dim bWindow
As Bitmap
Dim AVIInstance
As AVICapture.AVICapture
Dim strSavePath
As String
Dim strFileName
As String
Dim uAVICompressOpt
As AVICapture.AVI_APIs.AVICOMPRESSOPTIONS_CLASS
Dim iRate
As
Integer
Dim tProcess
As Threading.Timer
Dim bMyWebCam
As Boolean
Dim strWindowText
As String
Dim bLastGood
As Bitmap
Dim gWindow
As Graphics
Dim GraphicsHdc
As IntPtr
Dim bPrintResult
As
Integer
Dim iFrameCount
As
Integer
Dim oLock
As Object
End Structure
To keep each CaptureInfo object
from being disposed of prematurly I also add it to a
global collection called
_cActiveProcesses.
During setup all the variables are
assigned to the proper CaptureInfo field. The width and
height are calculated by the GetWindowRect API.
'Find the dimensions of the window
Dim rectWindow
As SC_APIs.RECT
SC_APIs.GetWindowRect(hWindow, rectWindow)
cInfo.iWidth = rectWindow.Right - rectWindow.Left
cInfo.iHeight = rectWindow.Bottom - rectWindow.Top
A new bitmap object,
bWindow, is created to hold the screen capture.
Since creating a new bitmap is very resource intensive
and we will be constantly using this bitmap, it is saved
in the structure.
'Create the bitmap
Dim bWindow
As New Bitmap(cInfo.iWidth, cInfo.iHeight)
cInfo.bWindow = bWindow
The name of the AVI is generated
from the webcam name and saved to
strFileName. The rate of capture saved from the
main form. by default this is three frames per second
(fps) because the average Yahoo! webcam usually never
exceeds this rate. Increasing this rate may capture a
few more frames of information over a long amount of
time but the processing and file space needed is cost
prohibitive. The next step is to grab a first frame for
double buffering which I will explain later. The AVICapture object is then set with the desired CODEC.
If _uAVICompressOpt
Is Nothing Then
cInfo.AVIInstance = New AVICapture.AVICapture(cInfo.strSavePath, cInfo.iRate,
cInfo.iWidth, cInfo.iHeight)
Else
cInfo.uAVICompressOpt = _uAVICompressOpt
cInfo.AVIInstance = New AVICapture.AVICapture(cInfo.strSavePath, cInfo.iRate, cInfo.iWidth, cInfo.iHeight, _uAVICompressOpt)
End If
Lastly a new timer is created and
started.
Dim iTimerRate
As Integer = CInt((1 / cInfo.iRate) * 1000)
cInfo.tProcess = New System.Threading.Timer(AddressOf CaptureTimer, cInfo, 0, iTimerRate)
_cActiveProcesses.Add(cInfo, CStr(hWindow))
Timer
By default the timer will fire
three times every second. This is needed to ensure the
AVI file has the proper number of frames and run time.
As discussed later, AVICapture uses Video for Windows
(VfW) which does not have an internal clock. This means
that if the rate is set to 3 fps and you add three
frames, you will only get a one second movie no matter
of the rate that the frames were actually added. In
other words, frame 1 is added, frame 2 added three
seconds later, and frame 3 added five minutes after
that, you will only get a one second movie.
PrintWindow
The rational of using PrintWindow
has already been discussed in the background section so
I will not take much time on it.
Private Function PrintWindowTimer(ByVal cInfo
As CaptureInfo) As Boolean
SyncLock cInfo.oLock
Try
If cInfo.hWindow <> Nothing Then
cInfo.gWindow = Graphics.FromImage(cInfo.bWindow)
cInfo.GraphicsHdc = cInfo.gWindow.GetHdc()
cInfo.bPrintResult = SC_APIs.PrintWindow(cInfo.hWindow, cInfo.GraphicsHdc, 1)
cInfo.gWindow.ReleaseHdc(cInfo.GraphicsHdc)
cInfo.gWindow.Flush()
If cInfo.bPrintResult <> 0
Then
Return True
Else
stopCapture(cInfo.hWindow)
Return False
End If
Else
Return False
End If
Catch ex As Exception
Return False
End Try
End SyncLock
End Function
Double Buffering
It is often said that 90% of your
programming time is consumed by 10% of the code. This
is my 10%. I found that the Yahoo! webcam control
sometimes does not play nice with PrintWindow. I can
only speculate to the cause of this (PAINT messages not
being sent in time?) but the results are clear. Random
frames are not properly rendered and you get either a
black, white, or grey frame. When playing back the AVI
this become very bothersome in a hurry and rapidly
degrades the quality of the movie. To combat this
effect I developed a two pronged attack. The first step
is to test for the offending frame.
Private Function DoubleBufferCalc(ByVal bBitmap
As Bitmap) As
Boolean
'Get a variety of different pixels from the frame
Dim iColor1
As Integer = bBitmap.GetPixel(20, 7).ToArgb
Dim iColor2
As Integer = bBitmap.GetPixel(bBitmap.Width - 40, 40).ToArgb
Dim iColor3
As Integer = bBitmap.GetPixel(bBitmap.Width - 80, bBitmap.Height - 80).ToArgb
Dim iColor4
As Integer = bBitmap.GetPixel(160, bBitmap.Height - 160).ToArgb
Dim iSum As Integer = iColor1 + iColor2 + iColor3 + iColor4
'black screen
If iSum = &HFC000000
Then
Return True
End If
'white screen
If iSum = &HFFFFFFFC
Then
Return True
End If
'grey screen
If iSum = &HFF8B8F68
Then
Return True
End If
End Function
DoubleBufferCalc checks each corner
of the image at progressively greater distances. This is
of course not a fool-proof check but getting the exact
same color for each pixel checked is so rare that a
false positive would be very rare. The sum of these
four pixels are then checked to see if an offending
frames is present.
If DoubleBufferCalc(cInfo.bWindow) =
True Then
'Three trys
If iBufferCount < 3
Then
iBufferCount += 1
GoTo BufferLoop
Else
'Replace with previous frame
bTemp.RotateFlip(RotateFlipType.Rotate180FlipX)
cInfo.AVIInstance.addFrame(bTemp)
cInfo.iFrameCount += 1
End If
Else
'Good frame
cInfo.AVIInstance.addFrame(cInfo.bWindow)
cInfo.iFrameCount += 1
End If
Now that we know we have a bad
frame it is given three chances to retry PrintWindow. I
have found that you have a 50/50 chance of getting a
successful frame here. It is possible to continuously
poll PrintWindow until a good frame is found but that
would take up an amazing amount of processor time and we
want to write the frame before the next timer event
fires. The second, last, and least desirable option is
to simply write the previous frame again. Throughout
the capture process bLastGood has been updated with the
last valid frame. We write bLastGood to the AVI and
keep going. While viewing the AVI this shows up as a
brief pause in the webcam motion but is much less
noticeable than having a black frame. It is necessary
to write a frame every time the timer fires or you will
end up with an incorrect run time.
You will also notice several odd
lines.
Dim bTemp
As New Bitmap(cInfo.bLastGood)
And
bTemp.RotateFlip(RotateFlipType.Rotate180FlipX)
This is because bitmaps are
reference object and are passed throughout the system as
pointers. I had a very hard time getting a
consistent
valid image out of bLastGood so I ended up making a new
bitmap object to hold it. The Rotate180FlipX is needed
because an identical operation is performed in
AVICapture and this is used to undo it.
AVICapture
AVICapture is a specialized purpose
class that I created to create, write, and compress
single frames to an AVI. As stated before AVICapture
uses Video for Windows (VfW) (http://msdn2.microsoft.com/en-us/library/ms713492.aspx).
It is implemented with great help from “A Simple C#
Wrapper for the AviFile Library” by Corinna John which
can be found at
http://www.codeproject.com/cs/media/avifilewrapper.asp.
AVICapture is also open to the
public with source and can be found at
http://ycoderscookbook.com/AVICapture.htm.
Features
- Ability to capture both
inbound and outbound Yahoo! Messenger webcam
streams.
- Automatic capturing of streams
once the program is started.
- AVI files will automatically
be created and finalized when a webcam session is
started or stopped.
- Webcam control search rate,
capture rate, and AVI directories are all
customizable.
- Webcam controls will still be
able to be captured even if the webcam control is
obstructed by other windows or application.
- Ability to select which CODEC
to compress the video stream.
- Notify icon with status
information to reduce desktop footprint.
Usage
- Open YCC Cam Cap
- Set the Search Webcam Windows
and Capture Rate if desired. I have found that the
default settings should rarely be changed.
- Press Start Capture and any
webcam stream will automatically be started or
stopped as needed.
Bugs and Gottchas
- Unable to capture while the
webcam control is minimized. Black frames will be
recorded into the AVI file.
- Some CODECs will fail. This
is due to a design glitch in the underlying AVI
creation code. If you select a CODEC that is not
compatible with YCC Cam Cap, you will be prompted to
enter a new CODEC.
Requirements
License Agreement
The software, YCC Cam Cap, is distributed under
the Creative Commons GNU General Public License
which can be found at
http://creativecommons.org/licenses/GPL/2.0/
The major points follow:
- No commercial distribution without
permission.
- You are allowed to modify the program and
source, all I ask is you keep the original
credit.
- If you do modify the program it will fall
under the same license agreement.
I have chosen this license to allow users to do
pretty much what they want with the program. The big
thing that I ask is please don't redistribute this
software without acknowledgment to YCC and a link
back to http://ycoderscookbook.com. I would love to
hear what you think about the program and if you
have made useful changes. A forum for YCC Cam Cap
can be found at
http://www.ycoderscookbook.com/forums/
under the Programs directory.
As with any piece of freeware, I have made every
effort to make the software useable and friendly.
With that said I take no responsibility for damage
created by this program or the user’s actions. You
may be violating the Yahoo! Terms of Service by
using this program. The user takes full
responsibility for their actions.
Changes
1.0.1
Source
Full source code is provided.
Please feel free to modify YCC Cam Cap as long as
you read and understand the license agreement.
I have created a place on the forum where all users
can discuss YCC Cam Cap at
http://www.ycoderscookbook.com/forums/viewforum.php?f=12.
YCC Cam Cap Executiable
YCC Cam Cap Source
|