/** * TheoraPlay; multithreaded Ogg Theora/Ogg Vorbis decoding. * * Please see the file LICENSE.txt in the source's root directory. * * This file written by Ryan C. Gordon. */ // I wrote this with a lot of peeking at the Theora example code in // libtheora-1.1.1/examples/player_example.c, but this is all my own // code. #include "rc_os_defines.h" #include #include #include #include #ifdef RC_ANDROID_BUILD #include "SDL.h" #else #include "SDL2/SDL.h" #endif #ifdef _WIN32 #include #define THEORAPLAY_THREAD_T HANDLE #define THEORAPLAY_MUTEX_T HANDLE #define sleepms(x) Sleep(x) #else #include #include #define sleepms(x) usleep((x) * 1000) #define THEORAPLAY_THREAD_T pthread_t #define THEORAPLAY_MUTEX_T pthread_mutex_t #endif #include "theoraplay.h" #include "theora/theoradec.h" #include "vorbis/codec.h" #define THEORAPLAY_INTERNAL 1 typedef THEORAPLAY_VideoFrame VideoFrame; typedef THEORAPLAY_AudioPacket AudioPacket; // !!! FIXME: these all count on the pixel format being TH_PF_420 for now. typedef unsigned char *(*ConvertVideoFrameFn)(const th_info *tinfo, const th_ycbcr_buffer ycbcr); static unsigned char *ConvertVideoFrame420ToYUVPlanar( const th_info *tinfo, const th_ycbcr_buffer ycbcr, const int p0, const int p1, const int p2) { int i; const int w = tinfo->pic_width; const int h = tinfo->pic_height; const int yoff = (tinfo->pic_x & ~1) + ycbcr[0].stride * (tinfo->pic_y & ~1); const int uvoff = (tinfo->pic_x / 2) + (ycbcr[1].stride) * (tinfo->pic_y / 2); unsigned char *yuv = (unsigned char *) malloc(w * h * 2); if (yuv) { unsigned char *dst = yuv; for (i = 0; i < h; i++, dst += w) memcpy(dst, ycbcr[p0].data + yoff + ycbcr[p0].stride * i, w); for (i = 0; i < (h / 2); i++, dst += w/2) memcpy(dst, ycbcr[p1].data + uvoff + ycbcr[p1].stride * i, w / 2); for (i = 0; i < (h / 2); i++, dst += w/2) memcpy(dst, ycbcr[p2].data + uvoff + ycbcr[p2].stride * i, w / 2); } // if return yuv; } // ConvertVideoFrame420ToYUVPlanar static unsigned char *ConvertVideoFrame420ToYV12(const th_info *tinfo, const th_ycbcr_buffer ycbcr) { return ConvertVideoFrame420ToYUVPlanar(tinfo, ycbcr, 0, 2, 1); } // ConvertVideoFrame420ToYV12 static unsigned char *ConvertVideoFrame420ToIYUV(const th_info *tinfo, const th_ycbcr_buffer ycbcr) { return ConvertVideoFrame420ToYUVPlanar(tinfo, ycbcr, 0, 1, 2); } // ConvertVideoFrame420ToIYUV // RGB #define THEORAPLAY_CVT_FNNAME_420 ConvertVideoFrame420ToRGB #define THEORAPLAY_CVT_RGB_ALPHA 0 #include "theoraplay_cvtrgb.h" #undef THEORAPLAY_CVT_RGB_ALPHA #undef THEORAPLAY_CVT_FNNAME_420 // RGBA #define THEORAPLAY_CVT_FNNAME_420 ConvertVideoFrame420ToRGBA #define THEORAPLAY_CVT_RGB_ALPHA 1 #include "theoraplay_cvtrgb.h" #undef THEORAPLAY_CVT_RGB_ALPHA #undef THEORAPLAY_CVT_FNNAME_420 typedef struct TheoraDecoder { // Thread wrangling... int thread_created; THEORAPLAY_MUTEX_T lock; volatile int halt; int thread_done; THEORAPLAY_THREAD_T worker; // API state... THEORAPLAY_Io *io; unsigned int maxframes; // Max video frames to buffer. volatile unsigned int prepped; volatile unsigned int videocount; // currently buffered frames. volatile unsigned int audioms; // currently buffered audio samples. volatile int hasvideo; volatile int hasaudio; volatile int decode_error; THEORAPLAY_VideoFormat vidfmt; ConvertVideoFrameFn vidcvt; VideoFrame *videolist; VideoFrame *videolisttail; AudioPacket *audiolist; AudioPacket *audiolisttail; } TheoraDecoder; #ifdef _WIN32 static inline int Thread_Create(TheoraDecoder *ctx, void *(*routine) (void*)) { ctx->worker = CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE) routine, (LPVOID) ctx, 0, NULL ); return (ctx->worker == NULL); } static inline void Thread_Join(THEORAPLAY_THREAD_T thread) { WaitForSingleObject(thread, INFINITE); CloseHandle(thread); } static inline int Mutex_Create(TheoraDecoder *ctx) { ctx->lock = CreateMutex(NULL, FALSE, NULL); return (ctx->lock == NULL); } static inline void Mutex_Destroy(THEORAPLAY_MUTEX_T mutex) { CloseHandle(mutex); } static inline void Mutex_Lock(THEORAPLAY_MUTEX_T mutex) { WaitForSingleObject(mutex, INFINITE); } static inline void Mutex_Unlock(THEORAPLAY_MUTEX_T mutex) { ReleaseMutex(mutex); } #else static inline int Thread_Create(TheoraDecoder *ctx, void *(*routine) (void*)) { return pthread_create(&ctx->worker, NULL, routine, ctx); } static inline void Thread_Join(THEORAPLAY_THREAD_T thread) { pthread_join(thread, NULL); } static inline int Mutex_Create(TheoraDecoder *ctx) { return pthread_mutex_init(&ctx->lock, NULL); } static inline void Mutex_Destroy(THEORAPLAY_MUTEX_T mutex) { pthread_mutex_destroy(&mutex); } static inline void Mutex_Lock(THEORAPLAY_MUTEX_T mutex) { pthread_mutex_lock(&mutex); } static inline void Mutex_Unlock(THEORAPLAY_MUTEX_T mutex) { pthread_mutex_unlock(&mutex); } #endif static int FeedMoreOggData(THEORAPLAY_Io *io, ogg_sync_state *sync) { long buflen = 4096; char *buffer = ogg_sync_buffer(sync, buflen); if (buffer == NULL) return -1; buflen = io->read(io, buffer, buflen); if (buflen <= 0) return 0; return (ogg_sync_wrote(sync, buflen) == 0) ? 1 : -1; } // FeedMoreOggData // This massive function is where all the effort happens. static void WorkerThread(TheoraDecoder *ctx) { // make sure we initialized the stream before using pagein, but the stream // will know to ignore pages that aren't meant for it, so pass to both. #define queue_ogg_page(ctx) do { \ if (tpackets) ogg_stream_pagein(&tstream, &page); \ if (vpackets) ogg_stream_pagein(&vstream, &page); \ } while (0) unsigned long audioframes = 0; unsigned long videoframes = 0; double fps = 0.0; int was_error = 1; // resets to 0 at the end. int eos = 0; // end of stream flag. // Too much Ogg/Vorbis/Theora state... ogg_packet packet; ogg_sync_state sync; ogg_page page; int vpackets = 0; vorbis_info vinfo; vorbis_comment vcomment; ogg_stream_state vstream; int vdsp_init = 0; vorbis_dsp_state vdsp; int tpackets = 0; th_info tinfo; th_comment tcomment; ogg_stream_state tstream; int vblock_init = 0; vorbis_block vblock; th_dec_ctx *tdec = NULL; th_setup_info *tsetup = NULL; ogg_sync_init(&sync); vorbis_info_init(&vinfo); vorbis_comment_init(&vcomment); th_comment_init(&tcomment); th_info_init(&tinfo); int bos = 1; while (!ctx->halt && bos) { if (FeedMoreOggData(ctx->io, &sync) <= 0) goto cleanup; // parse out the initial header. while ( (!ctx->halt) && (ogg_sync_pageout(&sync, &page) > 0) ) { ogg_stream_state test; if (!ogg_page_bos(&page)) // not a header. { queue_ogg_page(ctx); bos = 0; break; } // if ogg_stream_init(&test, ogg_page_serialno(&page)); ogg_stream_pagein(&test, &page); ogg_stream_packetout(&test, &packet); if (!tpackets && (th_decode_headerin(&tinfo, &tcomment, &tsetup, &packet) >= 0)) { memcpy(&tstream, &test, sizeof (test)); tpackets = 1; } // if else if (!vpackets && (vorbis_synthesis_headerin(&vinfo, &vcomment, &packet) >= 0)) { memcpy(&vstream, &test, sizeof (test)); vpackets = 1; } // else if else { // whatever it is, we don't care about it ogg_stream_clear(&test); } // else } // while } // while // no audio OR video? if (ctx->halt || (!vpackets && !tpackets)) goto cleanup; // apparently there are two more theora and two more vorbis headers next. while ((!ctx->halt) && ((tpackets && (tpackets < 3)) || (vpackets && (vpackets < 3)))) { while (!ctx->halt && tpackets && (tpackets < 3)) { if (ogg_stream_packetout(&tstream, &packet) != 1) break; // get more data? if (!th_decode_headerin(&tinfo, &tcomment, &tsetup, &packet)) goto cleanup; tpackets++; } // while while (!ctx->halt && vpackets && (vpackets < 3)) { if (ogg_stream_packetout(&vstream, &packet) != 1) break; // get more data? if (vorbis_synthesis_headerin(&vinfo, &vcomment, &packet)) goto cleanup; vpackets++; } // while // get another page, try again? if (ogg_sync_pageout(&sync, &page) > 0) queue_ogg_page(ctx); else if (FeedMoreOggData(ctx->io, &sync) <= 0) goto cleanup; } // while // okay, now we have our streams, ready to set up decoding. if (!ctx->halt && tpackets) { // th_decode_alloc() docs say to check for insanely large frames yourself. if ((tinfo.frame_width > 99999) || (tinfo.frame_height > 99999)) goto cleanup; // We treat "unspecified" as NTSC. *shrug* if ( (tinfo.colorspace != TH_CS_UNSPECIFIED) && (tinfo.colorspace != TH_CS_ITU_REC_470M) && (tinfo.colorspace != TH_CS_ITU_REC_470BG) ) { assert(0 && "Unsupported colorspace."); // !!! FIXME goto cleanup; } // if if (tinfo.pixel_fmt != TH_PF_420) { assert(0); goto cleanup; } // !!! FIXME if (tinfo.fps_denominator != 0) fps = ((double) tinfo.fps_numerator) / ((double) tinfo.fps_denominator); tdec = th_decode_alloc(&tinfo, tsetup); if (!tdec) goto cleanup; // Set decoder to maximum post-processing level. // Theoretically we could try dropping this level if we're not keeping up. int pp_level_max = 0; // !!! FIXME: maybe an API to set this? //th_decode_ctl(tdec, TH_DECCTL_GET_PPLEVEL_MAX, &pp_level_max, sizeof(pp_level_max)); th_decode_ctl(tdec, TH_DECCTL_SET_PPLEVEL, &pp_level_max, sizeof(pp_level_max)); } // if // Done with this now. if (tsetup != NULL) { th_setup_free(tsetup); tsetup = NULL; } // if if (!ctx->halt && vpackets) { vdsp_init = (vorbis_synthesis_init(&vdsp, &vinfo) == 0); if (!vdsp_init) goto cleanup; vblock_init = (vorbis_block_init(&vdsp, &vblock) == 0); if (!vblock_init) goto cleanup; } // if // Now we can start the actual decoding! // Note that audio and video don't _HAVE_ to start simultaneously. Mutex_Lock(ctx->lock); ctx->prepped = 1; ctx->hasvideo = (tpackets != 0); ctx->hasaudio = (vpackets != 0); Mutex_Unlock(ctx->lock); while (!ctx->halt && !eos) { int need_pages = 0; // need more Ogg pages? int saw_video_frame = 0; // Try to read as much audio as we can at once. We limit the outer // loop to one video frame and as much audio as we can eat. while (!ctx->halt && vpackets) { float **pcm = NULL; const int frames = vorbis_synthesis_pcmout(&vdsp, &pcm); if (frames > 0) { const int channels = vinfo.channels; int chanidx, frameidx; float *samples; AudioPacket *item = (AudioPacket *) malloc(sizeof (AudioPacket)); if (item == NULL) goto cleanup; item->playms = (unsigned long) ((((double) audioframes) / ((double) vinfo.rate)) * 1000.0); item->channels = channels; item->freq = vinfo.rate; item->frames = frames; item->samples = (float *) malloc(sizeof (float) * frames * channels); item->next = NULL; if (item->samples == NULL) { free(item); goto cleanup; } // if // I bet this beats the crap out of the CPU cache... samples = item->samples; for (frameidx = 0; frameidx < frames; frameidx++) { for (chanidx = 0; chanidx < channels; chanidx++) *(samples++) = pcm[chanidx][frameidx]; } // for vorbis_synthesis_read(&vdsp, frames); // we ate everything. audioframes += frames; //printf("Decoded %d frames of audio.\n", (int) frames); Mutex_Lock(ctx->lock); ctx->audioms += item->playms; if (ctx->audiolisttail) { assert(ctx->audiolist); ctx->audiolisttail->next = item; } // if else { assert(!ctx->audiolist); ctx->audiolist = item; } // else ctx->audiolisttail = item; Mutex_Unlock(ctx->lock); } // if else // no audio available left in current packet? { // try to feed another packet to the Vorbis stream... if (ogg_stream_packetout(&vstream, &packet) <= 0) { if (!tpackets) need_pages = 1; // no video, get more pages now. break; // we'll get more pages when the video catches up. } // if else { if (vorbis_synthesis(&vblock, &packet) == 0) vorbis_synthesis_blockin(&vdsp, &vblock); } // else } // else } // while if (!ctx->halt && tpackets) { // Theora, according to example_player.c, is // "one [packet] in, one [frame] out." if (ogg_stream_packetout(&tstream, &packet) <= 0) need_pages = 1; else { ogg_int64_t granulepos = 0; const int rc = th_decode_packetin(tdec, &packet, &granulepos); if (rc == TH_DUPFRAME) videoframes++; // nothing else to do. else if (rc == 0) // new frame! { th_ycbcr_buffer ycbcr; if (th_decode_ycbcr_out(tdec, ycbcr) == 0) { VideoFrame *item = (VideoFrame *) malloc(sizeof (VideoFrame)); if (item == NULL) goto cleanup; item->playms = (fps == 0) ? 0 : (unsigned int) ((((double) videoframes) / fps) * 1000.0); item->fps = fps; item->width = tinfo.pic_width; item->height = tinfo.pic_height; item->format = ctx->vidfmt; item->pixels = ctx->vidcvt(&tinfo, ycbcr); item->next = NULL; if (item->pixels == NULL) { free(item); goto cleanup; } // if //printf("Decoded another video frame.\n"); Mutex_Lock(ctx->lock); if (ctx->videolisttail) { assert(ctx->videolist); ctx->videolisttail->next = item; } // if else { assert(!ctx->videolist); ctx->videolist = item; } // else ctx->videolisttail = item; ctx->videocount++; Mutex_Unlock(ctx->lock); saw_video_frame = 1; } // if videoframes++; } // if } // else } // if if (!ctx->halt && need_pages) { const int rc = FeedMoreOggData(ctx->io, &sync); if (rc == 0) eos = 1; // end of stream else if (rc < 0) goto cleanup; // i/o error, etc. else { while (!ctx->halt && (ogg_sync_pageout(&sync, &page) > 0)) queue_ogg_page(ctx); } // else } // if // Sleep the process until we have space for more frames. if (saw_video_frame) { int go_on = !ctx->halt; //printf("Sleeping.\n"); while (go_on) { // !!! FIXME: This is stupid. I should use a semaphore for this. Mutex_Lock(ctx->lock); go_on = !ctx->halt && (ctx->videocount >= ctx->maxframes); Mutex_Unlock(ctx->lock); if (go_on) sleepms(10); } // while //printf("Awake!\n"); } // if } // while was_error = 0; cleanup: ctx->decode_error = (!ctx->halt && was_error); if (tdec != NULL) th_decode_free(tdec); if (tsetup != NULL) th_setup_free(tsetup); if (vblock_init) vorbis_block_clear(&vblock); if (vdsp_init) vorbis_dsp_clear(&vdsp); if (tpackets) ogg_stream_clear(&tstream); if (vpackets) ogg_stream_clear(&vstream); th_info_clear(&tinfo); th_comment_clear(&tcomment); vorbis_comment_clear(&vcomment); vorbis_info_clear(&vinfo); ogg_sync_clear(&sync); ctx->io->close(ctx->io); ctx->thread_done = 1; } // WorkerThread static void *WorkerThreadEntry(void *_this) { TheoraDecoder *ctx = (TheoraDecoder *) _this; WorkerThread(ctx); //printf("Worker thread is done.\n"); return NULL; } // WorkerThreadEntry static long IoFopenRead(THEORAPLAY_Io *io, void *buf, long buflen) { SDL_RWops *f = (SDL_RWops *) io->userdata; const size_t br = SDL_RWread(f, buf, 1, buflen); if (br == 0) return -1; return (long) br; } // IoFopenRead static void IoFopenClose(THEORAPLAY_Io *io) { SDL_RWops *f = (SDL_RWops *) io->userdata; SDL_RWclose(f); free(io); } // IoFopenClose THEORAPLAY_Decoder *THEORAPLAY_startDecodeFile(const char *fname, const unsigned int maxframes, THEORAPLAY_VideoFormat vidfmt) { THEORAPLAY_Io *io = (THEORAPLAY_Io *) malloc(sizeof (THEORAPLAY_Io)); if (io == NULL) return NULL; SDL_RWops *f = SDL_RWFromFile(fname, "rb"); if (f == NULL) { free(io); return NULL; } // if io->read = IoFopenRead; io->close = IoFopenClose; io->userdata = f; return THEORAPLAY_startDecode(io, maxframes, vidfmt); } // THEORAPLAY_startDecodeFile THEORAPLAY_Decoder *THEORAPLAY_startDecode(THEORAPLAY_Io *io, const unsigned int maxframes, THEORAPLAY_VideoFormat vidfmt) { TheoraDecoder *ctx = NULL; ConvertVideoFrameFn vidcvt = NULL; switch (vidfmt) { // !!! FIXME: current expects TH_PF_420. #define VIDCVT(t) case THEORAPLAY_VIDFMT_##t: vidcvt = ConvertVideoFrame420To##t; break; VIDCVT(YV12) VIDCVT(IYUV) VIDCVT(RGB) VIDCVT(RGBA) #undef VIDCVT default: goto startdecode_failed; // invalid/unsupported format. } // switch ctx = (TheoraDecoder *) malloc(sizeof (TheoraDecoder)); if (ctx == NULL) goto startdecode_failed; memset(ctx, '\0', sizeof (TheoraDecoder)); ctx->maxframes = maxframes; ctx->vidfmt = vidfmt; ctx->vidcvt = vidcvt; ctx->io = io; if (Mutex_Create(ctx) == 0) { ctx->thread_created = (Thread_Create(ctx, WorkerThreadEntry) == 0); if (ctx->thread_created) return (THEORAPLAY_Decoder *) ctx; } // if Mutex_Destroy(ctx->lock); startdecode_failed: io->close(io); free(ctx); return NULL; } // THEORAPLAY_startDecode void THEORAPLAY_stopDecode(THEORAPLAY_Decoder *decoder) { TheoraDecoder *ctx = (TheoraDecoder *) decoder; if (!ctx) return; if (ctx->thread_created) { ctx->halt = 1; Thread_Join(ctx->worker); Mutex_Destroy(ctx->lock); } // if VideoFrame *videolist = ctx->videolist; while (videolist) { VideoFrame *next = videolist->next; free(videolist->pixels); free(videolist); videolist = next; } // while AudioPacket *audiolist = ctx->audiolist; while (audiolist) { AudioPacket *next = audiolist->next; free(audiolist->samples); free(audiolist); audiolist = next; } // while free(ctx); } // THEORAPLAY_stopDecode int THEORAPLAY_isDecoding(THEORAPLAY_Decoder *decoder) { TheoraDecoder *ctx = (TheoraDecoder *) decoder; int retval = 0; if (ctx) { Mutex_Lock(ctx->lock); retval = ( ctx && (ctx->audiolist || ctx->videolist || (ctx->thread_created && !ctx->thread_done)) ); Mutex_Unlock(ctx->lock); } // if return retval; } // THEORAPLAY_isDecoding #define GET_SYNCED_VALUE(typ, defval, decoder, member) \ TheoraDecoder *ctx = (TheoraDecoder *) decoder; \ typ retval = defval; \ if (ctx) { \ Mutex_Lock(ctx->lock); \ retval = ctx->member; \ Mutex_Unlock(ctx->lock); \ } \ return retval; int THEORAPLAY_isInitialized(THEORAPLAY_Decoder *decoder) { GET_SYNCED_VALUE(int, 0, decoder, prepped); } // THEORAPLAY_isInitialized int THEORAPLAY_hasVideoStream(THEORAPLAY_Decoder *decoder) { GET_SYNCED_VALUE(int, 0, decoder, hasvideo); } // THEORAPLAY_hasVideoStream int THEORAPLAY_hasAudioStream(THEORAPLAY_Decoder *decoder) { GET_SYNCED_VALUE(int, 0, decoder, hasaudio); } // THEORAPLAY_hasAudioStream unsigned int THEORAPLAY_availableVideo(THEORAPLAY_Decoder *decoder) { GET_SYNCED_VALUE(unsigned int, 0, decoder, videocount); } // THEORAPLAY_hasAudioStream unsigned int THEORAPLAY_availableAudio(THEORAPLAY_Decoder *decoder) { GET_SYNCED_VALUE(unsigned int, 0, decoder, audioms); } // THEORAPLAY_hasAudioStream int THEORAPLAY_decodingError(THEORAPLAY_Decoder *decoder) { GET_SYNCED_VALUE(int, 0, decoder, decode_error); } // THEORAPLAY_decodingError const THEORAPLAY_AudioPacket *THEORAPLAY_getAudio(THEORAPLAY_Decoder *decoder) { TheoraDecoder *ctx = (TheoraDecoder *) decoder; AudioPacket *retval; Mutex_Lock(ctx->lock); retval = ctx->audiolist; if (retval) { ctx->audioms -= retval->playms; ctx->audiolist = retval->next; retval->next = NULL; if (ctx->audiolist == NULL) ctx->audiolisttail = NULL; } // if Mutex_Unlock(ctx->lock); return retval; } // THEORAPLAY_getAudio void THEORAPLAY_freeAudio(const THEORAPLAY_AudioPacket *_item) { THEORAPLAY_AudioPacket *item = (THEORAPLAY_AudioPacket *) _item; if (item != NULL) { assert(item->next == NULL); free(item->samples); free(item); } // if } // THEORAPLAY_freeAudio const THEORAPLAY_VideoFrame *THEORAPLAY_getVideo(THEORAPLAY_Decoder *decoder) { TheoraDecoder *ctx = (TheoraDecoder *) decoder; VideoFrame *retval; Mutex_Lock(ctx->lock); retval = ctx->videolist; if (retval) { ctx->videolist = retval->next; retval->next = NULL; if (ctx->videolist == NULL) ctx->videolisttail = NULL; assert(ctx->videocount > 0); ctx->videocount--; } // if Mutex_Unlock(ctx->lock); return retval; } // THEORAPLAY_getVideo void THEORAPLAY_freeVideo(const THEORAPLAY_VideoFrame *_item) { THEORAPLAY_VideoFrame *item = (THEORAPLAY_VideoFrame *) _item; if (item != NULL) { assert(item->next == NULL); free(item->pixels); free(item); } // if } // THEORAPLAY_freeVideo // end of theoraplay.cpp ...