Tutorial: Adding AVI capture to stock GLQuake

Post tutorials on how to do certain tasks within game or engine code here.
Post Reply
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Tutorial: Adding AVI capture to stock GLQuake

Post by Baker »

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.
qbism
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am
Contact:

Post by qbism »

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.
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Post by Baker »

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 ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Post by Spike »

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.
frag.machine
Posts: 2126
Joined: Sat Nov 25, 2006 1:49 pm

Post by frag.machine »

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)
frag.machine
Posts: 2126
Joined: Sat Nov 25, 2006 1:49 pm

Post by frag.machine »

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)
qbism
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am
Contact:

Post by qbism »

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.
Post Reply