Glusoft

Playing a theora video with SDL

Introduction

In this tutorial we will use theoraplay to decode theora video (.ogv) and play a video with SDL.
There is an example of this player for SDL 1.2 in the library theoraplay. For this tutorial, I have modified the code for SDL2 since some things like SDL_Overlay has been deleted.

We want to create a function to play a video file.

The main function is simply a call to that function:

int main(int argc, char **argv) {
	playfile("video.ogv");

	return 0;
}

The first thing to do in the function playfile is to decode the format, to do that we use a THEORAPLAY_Decoder.

THEORAPLAY_Decoder *decoder = THEORAPLAY_startDecodeFile(fname, 30, THEORAPLAY_VIDFMT_IYUV);

The first parameters is the path of the file you want to decode.
The second parameter is the number of frames.
The third parameters is the format to decode, theoraplay can support the formats:

Next we need to create a video and audio buffer:

const THEORAPLAY_VideoFrame *video = NULL;
const THEORAPLAY_AudioPacket *audio = NULL;

The buffers can be fill after the initialization of SDL.

Initialization

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);

while (!audio || !video) {
	if (!audio) audio = THEORAPLAY_getAudio(decoder);
	if (!video) video = THEORAPLAY_getVideo(decoder);
	SDL_Delay(10);
}

We can create the window and renderer with the width and height of the video :

int width = video->width;
int height = video->height;

framems = (video->fps == 0.0) ? 0 : ((Uint32)(1000.0 / video->fps));

screen = SDL_CreateWindow("Video Player SDL2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_OPENGL);

SDL_Renderer* renderer = SDL_CreateRenderer(screen, -1, 0);

After the creation of the renderer we need a texture with the corresponding format to render the video.

SDL_texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, video->width, video->height);

We can start checking for initialization errors.

int quit = 0;
int initfailed = quit = (!screen || !texture);

Playing the audio

To play the audio we need to fill a SDL_AudioSpec:

SDL_AudioSpec spec;
memset(&spec, '\0', sizeof(SDL_AudioSpec));
spec.freq = audio->freq;
spec.format = AUDIO_S16SYS;
spec.channels = audio->channels;
spec.samples = 2048;
spec.callback = audio_callback;

As you can see, we need to have a callback function audio_callback.

The function need to process the audio data :

void SDLCALL audio_callback(void *userdata, Uint8 *stream, int len)

We need to fill the audio stream, for that we create a structure AudioQueue containing a THEORAPLAY_AudioPacket.

typedef struct AudioQueue {
	const THEORAPLAY_AudioPacket *audio;
	int offset;
	struct AudioQueue *next;
} AudioQueue;

static Uint32 baseticks = 0;
static volatile AudioQueue *audio_queue = NULL;
static volatile AudioQueue *audio_queue_tail = NULL;

After, the audio_queue and baseticks are sets later in the program.

Next, from this object we can initialize the buffer stream with the data, then set the length and the offset while processing the data.

static void SDLCALL audio_callback(void *userdata, Uint8 *stream, int len) {
	Sint16 *dst = (Sint16 *)stream;

	while (audio_queue && (len > 0)) {
		volatile AudioQueue *item = audio_queue;
		AudioQueue *next = item->next;
		const int channels = item->audio->channels;

		const float *src = item->audio->samples + (item->offset * channels);
		int cpy = (item->audio->frames - item->offset) * channels;
		int i;

		if (cpy > (len / sizeof(Sint16)))
			cpy = len / sizeof(Sint16);

		for (i = 0; i < cpy; i++) {
			const float val = *(src++);
			if (val < -1.0f)
				*(dst++) = -32768;
			else if (val > 1.0f)
				*(dst++) = 32767;
			else
				*(dst++) = (Sint16)(val * 32767.0f);
		}

		item->offset += (cpy / channels);
		len -= cpy * sizeof(Sint16);

		if (item->offset >= item->audio->frames) {
			THEORAPLAY_freeAudio(item->audio);
			SDL_free((void *)item);
			audio_queue = next;
		}
	}

	if (!audio_queue)
		audio_queue_tail = NULL;

	if (len > 0)
		memset(dst, '\0', len);
}

You do not need to understand this part if you just want to play a video, the audio API of SDL is quite low-level.

The function to fill the audio_queue and audio_queue_tail is queue_audio.

static void queue_audio(const THEORAPLAY_AudioPacket *audio) {
	AudioQueue *item = (AudioQueue *)SDL_malloc(sizeof(AudioQueue));
	if (!item) {
		THEORAPLAY_freeAudio(audio);
		return;
	}

	item->audio = audio;
	item->offset = 0;
	item->next = NULL;

	SDL_LockAudio();
	if (audio_queue_tail)
		audio_queue_tail->next = item;
	else
		audio_queue = item;
	audio_queue_tail = item;
	SDL_UnlockAudio();
}

We can loop on the audio packet and call queue_audio to process it.

After the audio packet, we process the next audio packet.

while (audio) {
	queue_audio(audio);
	audio = THEORAPLAY_getAudio(decoder);
}

Process the video data

The next thing to do is get and process the video data. For that in the main loop we get the video from the decoder.

void *pixels = NULL;
int pitch = 0;
baseticks = SDL_GetTicks();
	
while (!quit && THEORAPLAY_isDecoding(decoder)) {
	const Uint32 now = SDL_GetTicks() - baseticks;

	if (!video)
		video = THEORAPLAY_getVideo(decoder);

Each frame is rendered during a certain amount of time. The time is specified in video->playms. If the time elapsed is greater thant this we go to the next frame.

	if (video && (video->playms <= now)) {
		if (framems && ((now - video->playms) >= framems)) {
			const THEORAPLAY_VideoFrame *last = video;
			while ((video = THEORAPLAY_getVideo(decoder)) != NULL) {
				THEORAPLAY_freeVideo(last);
				last = video;
				if ((now - video->playms) < framems)
					break;
			}

			if (!video)
				video = last;
		}

After getting the right frame we update the texture, in the right format:

	SDL_LockTexture(texture, NULL, &pixels, &pitch);
	const int w = video->width;
	const int h = video->height;
	const Uint8 *y = (const Uint8 *)video->pixels;
	const Uint8 *u = y + (w * h);
	const Uint8 *v = u + ((w / 2) * (h / 2));
	Uint8 *dst = (Uint8*)pixels;
	int i;

	//memcpy(pixels, video->pixels, video->height * pitch); // For RGBA texture

	for (i = 0; i < h; i++, y += w, dst += pitch) {
		memcpy(dst, y, w);
	}

	for (i = 0; i < h / 2; i++,	u += w / 2, dst += pitch / 2) {
		memcpy(dst, u, w / 2);
	}

	for (i = 0; i < h / 2; i++,	v += w / 2,	dst += pitch / 2) {
		memcpy(dst, v, w / 2);
	}

	SDL_UnlockTexture(texture);

After the texture update we can free the video frame, if there is nothing to render we add a small delay.

	THEORAPLAY_freeVideo(video);
		video = NULL;
	}
	else {
		SDL_Delay(10);
	}

For each frame the audio needs to be processed again:

while ((audio = THEORAPLAY_getAudio(decoder)) != NULL)
queue_audio(audio);

We need a small event loop and we can render the texture.

	while (screen && SDL_PollEvent(&event)) {
		switch (event.type) {

		case SDL_QUIT:
			quit = 1;
			break;

		case SDL_KEYDOWN:
			if (event.key.keysym.sym == SDLK_ESCAPE)
				quit = 1;
			break;
		}
	}

	SDL_RenderClear(renderer);
	SDL_RenderCopy(renderer, texture, NULL, NULL);
	SDL_RenderPresent(renderer);
}

Finally, we display the error and free everything:

while (!quit) {
	SDL_LockAudio();
	quit = (audio_queue == NULL);
	SDL_UnlockAudio();
	if (!quit)
		SDL_Delay(100);
}

if (initfailed)
	printf("Initialization failed!\n");
else if (THEORAPLAY_decodingError(decoder))
	printf("There was an error decoding this file!\n");
else
	printf("done with this file!\n");

if (texture) SDL_DestroyTexture(texture);
if (video) THEORAPLAY_freeVideo(video);
if (audio) THEORAPLAY_freeAudio(audio);
if (decoder) THEORAPLAY_stopDecode(decoder);
SDL_CloseAudio();
SDL_Quit();

You can download the project: TheoPlay.7z