/* Theorafile - Ogg Theora Video Decoder Library * * Copyright (c) 2017-2020 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 /* 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->page); } if (file->vpackets) { ogg_stream_pagein(&file->vstream, &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; 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(&file->vinfo); vorbis_comment_init(&file->vcomment); th_info_init(&file->tinfo); th_comment_init(&file->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 (!file->tpackets && (th_decode_headerin( &file->tinfo, &file->tcomment, &tsetup, &packet ) >= 0)) { memcpy(&file->tstream, &filler, sizeof(filler)); file->tpackets = 1; } else if (!file->vpackets && (vorbis_synthesis_headerin( &file->vinfo, &file->vcomment, &packet ) >= 0)) { memcpy(&file->vstream, &filler, sizeof(filler)); file->vpackets = 1; } else { /* Whatever it is, we don't care about it */ ogg_stream_clear(&filler); } } /* 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 < 3)) #define VPACKETS (file->vpackets && (file->vpackets < 3)) while (TPACKETS || VPACKETS) { while (TPACKETS) { if (ogg_stream_packetout( &file->tstream, &packet ) != 1) { /* Get more data? */ break; } TF_OPEN_ASSERT(!th_decode_headerin( &file->tinfo, &file->tcomment, &tsetup, &packet )) file->tpackets += 1; } while (VPACKETS) { if (ogg_stream_packetout( &file->vstream, &packet ) != 1) { /* Get more data? */ break; } TF_OPEN_ASSERT(vorbis_synthesis_headerin( &file->vinfo, &file->vcomment, &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 */ if (file->tpackets) { /* th_decode_alloc() docs say to check for * insanely large frames yourself. */ TF_OPEN_ASSERT( (file->tinfo.frame_width > 99999) || (file->tinfo.frame_height > 99999) ) /* FIXME: We treat "unspecified" as NTSC :shrug: */ if ( (file->tinfo.colorspace != TH_CS_UNSPECIFIED) && (file->tinfo.colorspace != TH_CS_ITU_REC_470M) && (file->tinfo.colorspace != TH_CS_ITU_REC_470BG) ) { errcode = TF_EUNSUPPORTED; goto fail; } if ( file->tinfo.pixel_fmt != TH_PF_420 && file->tinfo.pixel_fmt != TH_PF_422 && file->tinfo.pixel_fmt != TH_PF_444 ) { errcode = TF_EUNSUPPORTED; goto fail; } /* The decoder, at last! */ file->tdec = th_decode_alloc(&file->tinfo, tsetup); TF_OPEN_ASSERT(!file->tdec) /* 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, 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 ) == 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; } int tf_fopen(const char *fname, OggTheora_File *file) { tf_callbacks io = { (size_t (*) (void*, size_t, size_t, void*)) fread, (int (*) (void*, ogg_int64_t, int)) fseek, (int (*) (void*)) fclose, }; return tf_open_callbacks( fopen(fname, "rb"), file, io ); } void tf_close(OggTheora_File *file) { /* Theora Data */ if (file->tdec != NULL) { th_decode_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 */ if (file->tpackets) { ogg_stream_clear(&file->tstream); } if (file->vpackets) { ogg_stream_clear(&file->vstream); } /* Metadata */ th_info_clear(&file->tinfo); th_comment_clear(&file->tcomment); vorbis_comment_clear(&file->vcomment); vorbis_info_clear(&file->vinfo); /* 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.pic_width; } if (height != NULL) { *height = file->tinfo.pic_height; } if (fps != NULL) { if (file->tinfo.fps_denominator != 0) { *fps = ( ((double) file->tinfo.fps_numerator) / ((double) file->tinfo.fps_denominator) ); } else { *fps = 0.0; } } if (fmt != NULL) { *fmt = file->tinfo.pixel_fmt; } } void tf_audioinfo(OggTheora_File *file, int *channels, int *samplerate) { if (channels != NULL) { *channels = file->vinfo.channels; } if (samplerate != NULL) { *samplerate = file->vinfo.rate; } } int tf_eos(OggTheora_File *file) { return file->eos; } void tf_reset(OggTheora_File *file) { if (file->tpackets) { ogg_stream_reset(&file->tstream); } if (file->vpackets) { ogg_stream_reset(&file->vstream); } 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, &packet)) { /* ... unless there's nothing left for us to read. */ if (retval) { break; } return 0; } rc = th_decode_packetin( file->tdec, &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, 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.pic_width; h = file->tinfo.pic_height; off = ( (file->tinfo.pic_x & ~1) + ycbcr[0].stride * (file->tinfo.pic_y & ~1) ); TF_COPY_CHANNEL(0) /* U/V */ if (file->tinfo.pixel_fmt == TH_PF_420) { /* Subsampled in both dimensions */ w /= 2; h /= 2; off = ( (file->tinfo.pic_x / 2) + (ycbcr[1].stride) * (file->tinfo.pic_y / 2) ); } else if (file->tinfo.pixel_fmt == TH_PF_422) { /* Subsampled only horizontally */ w /= 2; off = ( (file->tinfo.pic_x / 2) + (ycbcr[1].stride) * (file->tinfo.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.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, &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; }