Hope Game
Logo Glusoft
Glusoft
04/06/2019

Playing a theora video

In this tutorial we will use theoraplay to decode theora video (.ogv).
There is an example of this player for SDL 1.2 in the library theoraplay. I adapted 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:

After that 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.
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);

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;

The audio_queue and baseticks will be filled later in the program.

From this object we can initialize the buffer stream with the data, set the length and the offset while processing the data.
Then free the audio data, after processing.

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 is processed we process the next audio packet so on and so forth.
while (audio) {
	queue_audio(audio);
	audio = THEORAPLAY_getAudio(decoder);
}

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 need to be 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 is updated 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 need to be processed again:
while ((audio = THEORAPLAY_getAudio(decoder)) != NULL)
queue_audio(audio);

Most of the work is done, 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);
}

The last thing to do is 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

Dark theme