/* Theorafile - Ogg Theora Video Decoder Library * * Copyright (c) 2017-2024 Ethan Lee. * Based on TheoraPlay, Copyright (c) 2011-2016 Ryan C. Gordon. * * This software is provided 'as-is', without any express or implied warranty. * In no event will the authors be held liable for any damages arising from * the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software in a * product, an acknowledgment in the product documentation would be * appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. * * Ethan "flibitijibibo" Lee * */ #include "theorafile.h" #include /* fopen and friends */ #include /* realloc */ #include /* memcpy, memset */ #define TF_DEFAULT_BUFFER_SIZE 4096 #ifdef _WIN32 #define inline __inline #endif /* _WIN32 */ static inline int INTERNAL_readOggData(OggTheora_File *file) { long buflen = TF_DEFAULT_BUFFER_SIZE; char *buffer = ogg_sync_buffer(&file->sync, buflen); if (buffer == NULL) { /* If you made it here, you ran out of RAM (wait, what?) */ return -1; } buflen = file->io.read_func(buffer, 1, buflen, file->datasource); if (buflen <= 0) { return 0; } return (ogg_sync_wrote(&file->sync, buflen) == 0) ? 1 : -1; } static inline void INTERNAL_queueOggPage(OggTheora_File *file) { if (file->tpackets) { ogg_stream_pagein(&file->tstream[file->ttrack], &file->page); } if (file->vpackets) { ogg_stream_pagein(&file->vstream[file->vtrack], &file->page); } } static inline int INTERNAL_getNextPacket( OggTheora_File *file, ogg_stream_state *stream, ogg_packet *packet ) { while (ogg_stream_packetout(stream, packet) <= 0) { const int rc = INTERNAL_readOggData(file); if (rc == 0) { file->eos = 1; return 0; } else if (rc < 0) { /* If you made it here, something REALLY bad happened. * * Unfortunately, ogg_sync_wrote does not give out any * codes, so I have no idea what that something is. * * Be sure you're not doing something nasty like * accessing one file via multiple threads at one time. * -flibit */ file->eos = 1; return 0; } else { while (ogg_sync_pageout(&file->sync, &file->page) > 0) { INTERNAL_queueOggPage(file); } } } return 1; } int tf_open_callbacks(void *datasource, OggTheora_File *file, tf_callbacks io) { ogg_packet packet; ogg_stream_state filler; th_setup_info *tsetup = NULL; int pp_level_max = 0; int errcode = TF_EUNKNOWN; vorbis_info vinfo; vorbis_comment vcomment; th_info tinfo; th_comment tcomment; int i; if (datasource == NULL) { return TF_ENODATASOURCE; } memset(file, '\0', sizeof(OggTheora_File)); file->datasource = datasource; file->io = io; #define TF_OPEN_ASSERT(cond) \ if (cond) goto fail; ogg_sync_init(&file->sync); vorbis_info_init(&vinfo); vorbis_comment_init(&vcomment); th_info_init(&tinfo); th_comment_init(&tcomment); /* Is there even data for us to read...? */ TF_OPEN_ASSERT(INTERNAL_readOggData(file) <= 0) /* Read header */ while (ogg_sync_pageout(&file->sync, &file->page) > 0) { if (!ogg_page_bos(&file->page)) { /* Not a header! */ INTERNAL_queueOggPage(file); break; } ogg_stream_init(&filler, ogg_page_serialno(&file->page)); ogg_stream_pagein(&filler, &file->page); ogg_stream_packetout(&filler, &packet); if (th_decode_headerin( &tinfo, &tcomment, &tsetup, &packet ) >= 0) { file->tinfo = realloc(file->tinfo, (file->ttracks + 1) * sizeof(file->tinfo[0])); file->tcomment = realloc(file->tcomment, (file->ttracks + 1) * sizeof(file->tcomment[0])); file->tstream = realloc(file->tstream, (file->ttracks + 1) * sizeof(file->tstream[0])); file->tdec = realloc(file->tdec, (file->ttracks + 1) * sizeof(file->tdec[0])); file->tinfo[file->ttracks] = tinfo; file->tcomment[file->ttracks] = tcomment; memcpy(&file->tstream[file->ttracks], &filler, sizeof(filler)); file->ttracks += 1; file->tpackets += 1; /* Reset this for other possible Theora streams */ th_info_init(&tinfo); th_comment_init(&tcomment); } else if (vorbis_synthesis_headerin( &vinfo, &vcomment, &packet ) >= 0) { file->vinfo = realloc(file->vinfo, (file->vtracks + 1) * sizeof(file->vinfo[0])); file->vcomment = realloc(file->vcomment, (file->vtracks + 1) * sizeof(file->vcomment[0])); file->vstream = realloc(file->vstream, (file->vtracks + 1) * sizeof(file->vstream[0])); file->vinfo[file->vtracks] = vinfo; file->vcomment[file->vtracks] = vcomment; memcpy(&file->vstream[file->vtracks], &filler, sizeof(filler)); file->vtracks += 1; file->vpackets += 1; /* Reset this for other possible Vorbis streams */ vorbis_info_init(&vinfo); vorbis_comment_init(&vcomment); } else { /* Whatever it is, we don't care about it */ ogg_stream_clear(&filler); } } vorbis_comment_clear(&vcomment); vorbis_info_clear(&vinfo); th_info_init(&tinfo); th_comment_init(&tcomment); /* No audio OR video? */ TF_OPEN_ASSERT(!file->tpackets && !file->vpackets) /* Apparently there are 2 more theora and 2 more vorbis headers next. */ #define TPACKETS (file->tpackets && (file->tpackets < (file->ttracks + 2))) #define VPACKETS (file->vpackets && (file->vpackets < (file->vtracks + 2))) while (TPACKETS || VPACKETS) { while (TPACKETS) { if (ogg_stream_packetout( &file->tstream[file->ttrack], &packet ) != 1) { /* Get more data? */ break; } TF_OPEN_ASSERT(!th_decode_headerin( &file->tinfo[0], &file->tcomment[0], &tsetup, &packet )) file->tpackets += 1; } while (VPACKETS) { if (ogg_stream_packetout( &file->vstream[file->vtrack], &packet ) != 1) { /* Get more data? */ break; } TF_OPEN_ASSERT(vorbis_synthesis_headerin( &file->vinfo[0], &file->vcomment[0], &packet )) file->vpackets += 1; } /* Get another page, try again? */ if (ogg_sync_pageout(&file->sync, &file->page) > 0) { INTERNAL_queueOggPage(file); } else { TF_OPEN_ASSERT(INTERNAL_readOggData(file) < 0) } } #undef TPACKETS #undef VPACKETS /* Set up Theora stream */ for (i = 0; i < file->ttracks; i += 1) { /* th_decode_alloc() docs say to check for * insanely large frames yourself. */ TF_OPEN_ASSERT( (file->tinfo[i].frame_width > 99999) || (file->tinfo[i].frame_height > 99999) ) /* FIXME: We treat "unspecified" as NTSC :shrug: */ if ( (file->tinfo[i].colorspace != TH_CS_UNSPECIFIED) && (file->tinfo[i].colorspace != TH_CS_ITU_REC_470M) && (file->tinfo[i].colorspace != TH_CS_ITU_REC_470BG) ) { errcode = TF_EUNSUPPORTED; goto fail; } if ( file->tinfo[i].pixel_fmt != TH_PF_420 && file->tinfo[i].pixel_fmt != TH_PF_422 && file->tinfo[i].pixel_fmt != TH_PF_444 ) { errcode = TF_EUNSUPPORTED; goto fail; } /* The decoder, at last! */ file->tdec[i] = th_decode_alloc(&file->tinfo[i], tsetup); TF_OPEN_ASSERT(!file->tdec[i]) /* Disable all post-processing in the decoder. * FIXME: Maybe an API to set this? * FIXME: Could be TH_DECCTL_GET_PPLEVEL_MAX, for example! * FIXME: Theoretically we could enable post-processing and then * FIXME: drop the quality level if we're not keeping up. */ th_decode_ctl( file->tdec[i], TH_DECCTL_SET_PPLEVEL, &pp_level_max, sizeof(pp_level_max) ); } /* Done with this now */ if (tsetup != NULL) { th_setup_free(tsetup); tsetup = NULL; } /* Set up Vorbis stream */ if (file->vpackets) { file->vdsp_init = vorbis_synthesis_init( &file->vdsp, &file->vinfo[file->vtrack] ) == 0; TF_OPEN_ASSERT(!file->vdsp_init) file->vblock_init = vorbis_block_init( &file->vdsp, &file->vblock ) == 0; TF_OPEN_ASSERT(!file->vblock_init) } #undef TF_OPEN_ASSERT /* Finally. */ return 0; fail: if (tsetup != NULL) { th_setup_free(tsetup); } tf_close(file); return errcode; } static size_t default_read_func(void* ptr, size_t size, size_t nmemb, void* datasource) { return fread(ptr, size, nmemb, datasource); } static int default_seek_func(void* datasource, ogg_int64_t offset, int origin) { return fseek(datasource, offset, origin); } static int default_close_func(void* datasource) { return fclose(datasource); } int tf_fopen(const char *fname, OggTheora_File *file) { tf_callbacks io = { default_read_func, default_seek_func, default_close_func, }; return tf_open_callbacks( fopen(fname, "rb"), file, io ); } void tf_close(OggTheora_File *file) { int i; /* Theora Data */ for (i = 0; i < file->ttracks; i += 1) { if (file->tdec[i] != NULL) { th_decode_free(file->tdec[i]); } } free(file->tdec); /* Vorbis Data */ if (file->vblock_init) { vorbis_block_clear(&file->vblock); } if (file->vdsp_init) { vorbis_dsp_clear(&file->vdsp); } /* Stream Data */ for (i = 0; i < file->ttracks; i += 1) { ogg_stream_clear(&file->tstream[i]); } for (i = 0; i < file->vtracks; i += 1) { ogg_stream_clear(&file->vstream[i]); } /* Metadata */ for (i = 0; i < file->ttracks; i += 1) { th_info_clear(&file->tinfo[i]); th_comment_clear(&file->tcomment[i]); } for (i = 0; i < file->vtracks; i += 1) { vorbis_comment_clear(&file->vcomment[i]); vorbis_info_clear(&file->vinfo[i]); } free(file->tstream); free(file->tcomment); free(file->vstream); free(file->vcomment); free(file->vinfo); free(file->tinfo); /* Current State */ ogg_sync_clear(&file->sync); /* I/O Data */ if (file->io.close_func != NULL) { file->io.close_func(file->datasource); } } int tf_hasvideo(OggTheora_File *file) { return file->tpackets != 0; } int tf_hasaudio(OggTheora_File *file) { return file->vpackets != 0; } void tf_videoinfo( OggTheora_File *file, int *width, int *height, double *fps, th_pixel_fmt *fmt ) { if (width != NULL) { *width = file->tinfo[file->ttrack].pic_width; } if (height != NULL) { *height = file->tinfo[file->ttrack].pic_height; } if (fps != NULL) { if (file->tinfo[file->ttrack].fps_denominator != 0) { *fps = ( ((double) file->tinfo[file->ttrack].fps_numerator) / ((double) file->tinfo[file->ttrack].fps_denominator) ); } else { *fps = 0.0; } } if (fmt != NULL) { *fmt = file->tinfo[file->ttrack].pixel_fmt; } } void tf_audioinfo(OggTheora_File *file, int *channels, int *samplerate) { if (channels != NULL) { *channels = file->vinfo[file->vtrack].channels; } if (samplerate != NULL) { *samplerate = file->vinfo[file->vtrack].rate; } } int tf_setaudiotrack(OggTheora_File *file, int vtrack) { /* Note there may be a slight delay changing track midstream. */ if (vtrack >= 0 && vtrack < file->vtracks) { file->vtrack = vtrack; return 1; } else { return 0; } } int tf_setvideotrack(OggTheora_File *file, int ttrack) { /* Note there may be a slight delay changing track midstream. */ if (ttrack >= 0 && ttrack < file->ttracks) { file->ttrack = ttrack; return 1; } else { return 0; } } int tf_eos(OggTheora_File *file) { return file->eos; } void tf_reset(OggTheora_File *file) { if (file->tpackets) { ogg_stream_reset(&file->tstream[file->ttrack]); } if (file->vpackets) { ogg_stream_reset(&file->vstream[file->vtrack]); } ogg_sync_reset(&file->sync); file->io.seek_func(file->datasource, 0, SEEK_SET); file->eos = 0; } int tf_readvideo(OggTheora_File *file, char *buffer, int numframes) { int i; char *dst = buffer; ogg_int64_t granulepos = 0; ogg_packet packet; th_ycbcr_buffer ycbcr; int rc; int w, h, off; unsigned char *plane; int stride; int retval = 0; for (i = 0; i < numframes; i += 1) { /* Keep trying to get a usable packet */ if (!INTERNAL_getNextPacket(file, &file->tstream[file->ttrack], &packet)) { /* ... unless there's nothing left for us to read. */ if (retval) { break; } return 0; } rc = th_decode_packetin( file->tdec[file->ttrack], &packet, &granulepos ); if (rc == 0) /* New frame! */ { retval = 1; } else if (rc != TH_DUPFRAME) { return 0; /* Why did we get here...? */ } } if (retval) /* New frame! */ { if (th_decode_ycbcr_out(file->tdec[file->ttrack], ycbcr) != 0) { return 0; /* Uhh?! */ } #define TF_COPY_CHANNEL(chan) \ plane = ycbcr[chan].data + off; \ stride = ycbcr[chan].stride; \ for (i = 0; i < h; i += 1, dst += w) \ { \ memcpy( \ dst, \ plane + (stride * i), \ w \ ); \ } /* Y */ w = file->tinfo[file->ttrack].pic_width; h = file->tinfo[file->ttrack].pic_height; off = ( (file->tinfo[file->ttrack].pic_x & ~1) + ycbcr[0].stride * (file->tinfo[file->ttrack].pic_y & ~1) ); TF_COPY_CHANNEL(0) /* U/V */ if (file->tinfo[file->ttrack].pixel_fmt == TH_PF_420) { /* Subsampled in both dimensions */ w /= 2; h /= 2; off = ( (file->tinfo[file->ttrack].pic_x / 2) + (ycbcr[1].stride) * (file->tinfo[file->ttrack].pic_y / 2) ); } else if (file->tinfo[file->ttrack].pixel_fmt == TH_PF_422) { /* Subsampled only horizontally */ w /= 2; off = ( (file->tinfo[file->ttrack].pic_x / 2) + (ycbcr[1].stride) * (file->tinfo[file->ttrack].pic_y & ~1) ); } TF_COPY_CHANNEL(1) TF_COPY_CHANNEL(2) #undef TF_COPY_CHANNEL } return retval; } int tf_readaudio(OggTheora_File *file, float *buffer, int samples) { int offset = 0; int chan, frame; ogg_packet packet; float **pcm = NULL; while (offset < samples) { const int frames = vorbis_synthesis_pcmout(&file->vdsp, &pcm); if (frames > 0) { /* I bet this beats the crap out of the CPU cache... */ for (frame = 0; frame < frames; frame += 1) for (chan = 0; chan < file->vinfo[file->vtrack].channels; chan += 1) { buffer[offset++] = pcm[chan][frame]; if (offset >= samples) { vorbis_synthesis_read( &file->vdsp, frame ); return offset; } } vorbis_synthesis_read(&file->vdsp, frames); } else /* No audio available left in current packet? */ { /* Keep trying to get a usable packet */ if (!INTERNAL_getNextPacket(file, &file->vstream[file->vtrack], &packet)) { /* ... unless there's nothing left for us to read. */ return offset; } if (vorbis_synthesis( &file->vblock, &packet ) == 0) { vorbis_synthesis_blockin( &file->vdsp, &file->vblock ); } } } return offset; }