Forum

Tutorial: Adding AVI capture to stock GLQuake

Post tutorials on how to do certain tasks within game or engine code here.

Moderator: InsideQC Admins

Tutorial: Adding AVI capture to stock GLQuake

Postby Baker » Mon Nov 24, 2008 8:05 am

Foreword: This tutorial is how to port the the fine upgraded AVI capture work of Jozsef Szalontai from JoeQuake 1862 of October, 2007.

His AVI capture code is used in several clients, like Qrack and ezQuake.

He did a silent version update of JoeQuake last year and he re-wrote the AVI capture to be even better!


Notes in Advance: This code only works on Windows, unlike how DarkPlaces video capture is operating system neutral to the best of my knowledge.

However, with this code, you can capture using Xvid or other codecs for far smaller video sizes.

XVid is an open source codec, but unlike DarkPlaces video capture, you'll need to download the Xvid separately.

In addition, this code can be used to capture in WinQuake, but this tutorial doesn't cover that because it's already going to be a long one ...


Adding AVI Capture to GLQuake

First, you really need to see JoeQuake 1862's video capture in action and try it once.

Then when we starting adding code, we will be lightly touching 12 files and adding 4. This tutorial is way easier than that sounds.

Capturing a demo in JoeQuake Build 1862

1. Download JoeQuake Build 1862 for Windows.(download) and install Xvid Installer (download) [Xvid is open source, it won't hurt you].

2. Start JoeQuake and type (or paste this) in console:

capture_codec divx; capture_fps 15; capturedemo demo1


Then look in quake\capture ... you have a demo1.avi and if you are using 640x480 resolution, you'll see that it is only 18 MB. Very nice! You could upload it to YouTube or whatever.

The Code

Files to edit = 12 = cl_demo.c; client.h; common.c; common.h; gl_screen.c; host.c; mathlib.h; snd_dma.c; snd_mix.c; zone.c; zone.h

F1. cl_demo.c

(a) Below #include "quakedef.h" add:

Code: Select all
#ifdef _WIN32
#include "movie.h"
#endif


Below this:

Code: Select all
   if (cls.timedemo)
      CL_FinishTimeDemo ();


(b) Add:

Code: Select all
#ifdef _WIN32
   Movie_StopPlayback ();
#endif


We are making it so AVI capture stops when a demo is done playing.



F2. client.h

(a) Below sizebuf_t message; in our client_static_t definition add:

Code: Select all
qboolean   capturedemo;


We are defining a client state of AVI capturing.


F3. common.c

(a) For simplicity, just paste this at the bottom of common.c:

Code: Select all
void Q_strncpyz (char *dest, char *src, size_t size)
{
   strncpy (dest, src, size - 1);
   dest[size-1] = 0;
}

void Q_snprintfz (char *dest, size_t size, char *fmt, ...)
{
   va_list      argptr;
   va_start (argptr, fmt);
   vsnprintf (dest, size, fmt, argptr);
   va_end (argptr);

   dest[size - 1] = 0;
}

/*
==================
COM_ForceExtension

If path doesn't have an extension or has a different extension, append(!) specified extension
Extension should include the .
==================
*/
void COM_ForceExtension (char *path, char *extension)
{
   char    *src;

   src = path + strlen(path) - 1;

   while (*src != '/' && src != path)
   {
      if (*src-- == '.')
      {
         COM_StripExtension (path, path);
         strcat (path, extension);
         return;
      }
   }

   strncat (path, extension, MAX_OSPATH);
}


We are adding this only because the files we will be adding later use them a lot.


F4. common.h

(a) Again, for reasons of simplicity just add the below to the bottom of common.h:

Code: Select all
#ifdef _WIN32
#define   vsnprintf _vsnprintf
#endif

#define bound(a, b, c) ((a) >= (c) ? (a) : (b) < (a) ? (a) : (b) > (c) ? (c) : (b))

void Q_strncpyz (char *dest, char *src, size_t size);
void Q_snprintfz (char *dest, size_t size, char *fmt, ...);

void COM_ForceExtension (char *path, char *extension);


Adding for same reason as before. Tidy up the location of these if you wish, but pasting them to the bottom will work.


F5. gl_screen.c

(a) Below #include "quakedef.h" add:

Code: Select all
#ifdef _WIN32
#include "movie.h"
#endif


Below this:

Code: Select all
scr_turtle = Draw_PicFromWad ("turtle");


(b) Add:

Code: Select all
#ifdef _WIN32
   Movie_Init ();
#endif


Above this near very bottom of our file:

Code: Select all
GL_EndRendering ();


(c) Add:

Code: Select all
#ifdef _WIN32
   Movie_UpdateScreen ();
#endif


This does the initialization of the AVI capture module and part C is part of the capture process.



F6. host.c

(a) Below #include "quakedef.h" add (look familiar?):

Code: Select all
#ifdef _WIN32
#include "movie.h"
#endif


Above this:

Code: Select all
host_frametime = realtime - oldrealtime;


(b) Add:

Code: Select all
#ifdef _WIN32
   if (Movie_IsActive())
      host_frametime = Movie_FrameTime ();
   else
#endif


Regulates the host time so video capture can work, otherwise you'd have big problems trying to capture.


F7. mathlib.h
(a) Add at bottom for simplicity:

Code: Select all
#define Q_rint(x) ((x) > 0 ? (int)((x) + 0.5) : (int)((x) - 0.5))


The files we add use this function.


F8. snd_dma.c

(a) Below #include "quakedef.h" add:

Code: Select all
#ifdef _WIN32
#include "movie.h"
#endif


Below this:

Code: Select all
int      fullsamples;


(b) Add:

Code: Select all
#ifdef _WIN32
   if (Movie_GetSoundtime())
      return;
#endif


Further down, below this:

Code: Select all
void S_ExtraUpdate (void)
{

#ifdef _WIN32


(c) Add as very next line:

Code: Select all
   if (Movie_IsActive())
      return;


Helps capture the sound and keep it in synchronized.


F9. snd_mix.c

(a) Below #include "quakedef.h" add:

Code: Select all
#ifdef _WIN32
#include "movie.h"
#endif


Below this:

Code: Select all
lpaintedtime += (snd_linear_count>>1);


(b) Add:

Code: Select all
#ifdef _WIN32
      Movie_TransferStereo16 ();
#endif


More sound capturing.



F10. zone.c

(a) You can paste this at the bottom:

Code: Select all
/*
===================
Q_malloc

Use it instead of malloc so that if memory allocation fails,
the program exits with a message saying there's not enough memory
instead of crashing after trying to use a NULL pointer
===================
*/
void *Q_malloc (size_t size)
{
   void   *p;

   if (!(p = malloc(size)))
      Sys_Error ("Not enough memory free; check disk space");

   return p;
}

/*
===================
Q_calloc
===================
*/
void *Q_calloc (size_t n, size_t size)
{
   void   *p;

   if (!(p = calloc(n, size)))
      Sys_Error ("Not enough memory free; check disk space");

   return p;
}

/*
===================
Q_realloc
===================
*/
void *Q_realloc (void *ptr, size_t size)
{
   void   *p;

   if (!(p = realloc(ptr, size)))
      Sys_Error ("Not enough memory free; check disk space");

   return p;
}

/*
===================
Q_strdup
===================
*/
void *Q_strdup (const char *str)
{
   char   *p;

   if (!(p = _strdup(str)))
      Sys_Error ("Not enough memory free; check disk space");

   return p;
}


Memory allocation goodness that the files we will be adding use so we must have them.


F11. zone.h

(a) You can paste this at the bottom:

Code: Select all
void *Q_malloc (size_t size);         // joe
void *Q_calloc (size_t n, size_t size);      //
void *Q_realloc (void *ptr, size_t size);   //
void *Q_strdup (const char *str);      //


Same reason as previous step.


F12. Now obtain the files movie.c, movie.h, movie_avi.c, movie_avi.h from JoeQuake Build 1862 Windows source (download) and add them to your project.

F13. movie.c

We have to make some changes to movie.c ...

(a) Locate this code:

Code: Select all
cvar_t   capture_dir   = {"capture_dir", "capture", 0, OnChange_capture_dir};

cvar_t   capture_codec   = {"capture_codec", "0"};


Now GLQuake doesn't have cvar callback, so replace with this, deleting the capture_dir and forcing capture_codec to save to config:

Code: Select all
cvar_t   capture_codec   = {"capture_codec", "0", true};


Locate this code:

Code: Select all
void Movie_Start_f (void)
{
   char   name[MAX_FILELENGTH], path[256];


(b) Change MAX_FILELENGTH to MAX_OSPATH because GLQUAKE doesn't have MAX_FILELENGTH.

Now, we aren't going to bother with the flexibility of specifying a custom capture directory, for the sake of keeping this simple.

Locate this code:

Code: Select all
   hack_ctr = capture_hack.value;

   Q_snprintfz (path, sizeof(path), "%s/%s", capture_dir.string, name);
   if (!(moviefile = fopen(path, "wb")))


(c) Change to this, unforunately removing the ability for the user to choose a directory themselves but making this tutorial easier to write:

Code: Select all
   hack_ctr = capture_hack.value;

   Q_snprintfz (path, sizeof(path), "%s/id1/%s", host_parms.basedir, name);
   if (!(moviefile = fopen(path, "wb")))




Locate this code:

Code: Select all
   Cvar_Register (&capture_codec);
   Cvar_Register (&capture_fps);
   Cvar_Register (&capture_dir);
   Cvar_Register (&capture_console);
   Cvar_Register (&capture_hack);

   Cvar_Set (&capture_dir, va("%s/%s", com_basedir, capture_dir.string));

   ACM_LoadLibrary ();
   if (!acm_loaded)
      return;

   Cvar_Register (&capture_mp3);
   Cvar_Register (&capture_mp3_kbps);


GLQuake doesn't use Cvar_Register, it uses Cvar_RegisterVariable. Furthermore, for simplicity, your recorded demos are going to be saved to quake\id1 instead of a custom user capturedir.

(d) So replace the above code with this:

Code: Select all
   Cvar_RegisterVariable (&capture_codec);
   Cvar_RegisterVariable (&capture_fps);
   Cvar_RegisterVariable (&capture_dir);
   Cvar_RegisterVariable (&capture_console);
   Cvar_RegisterVariable (&capture_hack);

   ACM_LoadLibrary ();
   if (!acm_loaded)
      return;

   Cvar_RegisterVariable (&capture_mp3);
   Cvar_RegisterVariable (&capture_mp3_kbps);


Next, GLQuake doesn't have hardware gamma so find this code:

Code: Select all
#ifdef GLQUAKE
   buffer = Q_malloc (size);
   glReadPixels (glx, gly, glwidth, glheight, GL_RGB, GL_UNSIGNED_BYTE, buffer);
   ApplyGamma (buffer, size);


(e) And comment out or delete the line "ApplyGamma (buffer, size);"

And, again, GLQuake doesn't have hardware gamma so locate this code:

Code: Select all
         *p++ = current_pal[vid.buffer[rowp]*3+2];
         *p++ = current_pal[vid.buffer[rowp]*3+1];
         *p++ = current_pal[vid.buffer[rowp]*3+0];


And replace with this:

Code: Select all
         *p++ = host_basepal[vid.buffer[rowp]*3+2];
         *p++ = host_basepal[vid.buffer[rowp]*3+1];
         *p++ = host_basepal[vid.buffer[rowp]*3+0];


With any luck and assuming this big long, but straightforward tutorial doesn't have any typos -- I was careful, but there is a chance it could have happened -- you are now the proud owner of an engine with AVI capturing capability.

So this means you don't need to use some goofy application like Fraps to capture your demos with your engine, you can do it easy and whenever you want.
User avatar
Baker
 
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Postby qbism » Fri Jan 29, 2010 2:41 am

Thanks, Baker. This tute works for swquake (Makaqu)!

Comments, looking at movie.c:
Change this line-
Code: Select all
Q_snprintfz (path, sizeof(path), "%s/id1/%s", host_parms.basedir, name);


to this-
Code: Select all
Q_snprintfz (path, sizeof(path), "%s/%s", com_gamedir, name);

...so that movie will save in gamedir rather than hard-coded to id1.

Delete this-
Code: Select all
Cvar_RegisterVariable (&capture_dir);


My computer is too slow to encode the avi without dropping a few frames and getting out-of-sync with audio. Anyone, is there a way to make the engine more "patient"?

BTW, I found that the GSpot utility that comes with K-Lite codec pack will list available codecs and fourcc codes.
User avatar
qbism
 
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am

Postby Baker » Fri Jan 29, 2010 3:03 am

qbism wrote:My computer is too slow to encode the avi without dropping a few frames and getting out-of-sync with audio. Anyone, is there a way to make the engine more "patient"?

BTW, I found that the GSpot utility that comes with K-Lite codec pack will list available codecs and fourcc codes.


What codec are you using? DivX, while certainly not the best, is in my opinion the easiest to setup for initial testing of capture and is rather fast.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
User avatar
Baker
 
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Postby Spike » Fri Jan 29, 2010 3:44 am

the general recommendation is to use a really poor codec that takes little cpu usage purely to reduce disk usage, and to then compress it properly in an external application, where it can benefit from knowing how it will become, rather than just how it changed. Depends on the app.

the sound system generally needs some sort of tweek to ensure it mixes the correct quantities and blocks of sound. one way is to grab the dma position from the recording code instead of the sound card - it'll sound terrible while recording, but the sound that is actually mixed should then be reasonably correct. The sound driver should have a GetDMAPosition or something, which you can just make return some multiple of the running count of frames written, based on the sample rate.
Spike
 
Posts: 2892
Joined: Fri Nov 05, 2004 3:12 am
Location: UK

Postby frag.machine » Fri Jan 29, 2010 4:34 pm

There's an alternating (not as well documented as yours, of course) method to do AVI capture, from the QdQ team custom engine. It's a really straightforward recipe to add a couple ready to use source files to any vanilla-based engine, copy & paste some calls in strategic points and bang, you get essentially the same features from your solution. I'll post it later in a separated thread.
I know FrikaC made a cgi-bin version of the quakec interpreter once and wrote part of his website in QuakeC :) (LordHavoc)
User avatar
frag.machine
 
Posts: 2090
Joined: Sat Nov 25, 2006 1:49 pm

Postby frag.machine » Fri Jan 29, 2010 4:44 pm

And some more useful AVI capture tips:
1- record a demo first, then play it back to capture the AVI;
2 - if your engine allow to set the AVI fps, use something as low as the host ticrate (usually 10Hz when model interpolation is disabled, 15 if enabled);
3 - also, set low screen dimensions (my default is 512 x 384, which gives a more than enough good resolution for YouTube videos).
I know FrikaC made a cgi-bin version of the quakec interpreter once and wrote part of his website in QuakeC :) (LordHavoc)
User avatar
frag.machine
 
Posts: 2090
Joined: Sat Nov 25, 2006 1:49 pm

Postby qbism » Fri Jan 29, 2010 5:56 pm

Working great now in swquake with some settings tweaks!

1. Performance is greatly improved by switching to windowed rather than full-screen mode. Would like to figure out why... May need to look at where Movie_UpdateScreen () is placed, or maybe it's just because the laptop video chip is stretching output to "fake" low-res modes.

2.
the general recommendation is to use a really poor codec that takes little cpu usage purely to reduce disk usage
Experimenting with the codecs available with K-lite installed, FFDS seemed to be the fastest. DIVX was slightly slower but much more compressed in size.

3. Setting CAPTURE_HACK to 1 solved the audio sync issue. I plan to try other values of CAPTURE_HACK also. ( Try 30! )
it'll sound terrible while recording, but the sound that is actually mixed should then be reasonably correct
That is exactly what happens- audio is way off during record, but correct in the video.

record a demo first, then play it back to capture the AVI
This is good advice, partly because mouse input will not be very smooth during avi recording.
User avatar
qbism
 
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am


Return to Programming Tutorials

Who is online

Users browsing this forum: No registered users and 1 guest