Implement Graphics.play_movie

Render video frames and play audio for Graphics.play_movie
Added theora dependency for theora video format playback
Added icculus's theoraplay to project
This commit is contained in:
Eblo 2022-01-02 18:05:17 -05:00 committed by Struma
parent 950e62dff1
commit 286b2b7658
10 changed files with 1238 additions and 5 deletions

View file

@ -46,6 +46,24 @@ CMAKE := $(CONFIGURE_ENV) cmake .. $(CMAKE_ARGS)
default: everything
# Theora
libtheora: init_dirs libvorbis libogg $(LIBDIR)/libtheora.a
$(LIBDIR)/libtheora.a: $(LIBDIR)/libogg.a $(DOWNLOADS)/theora/Makefile
cd $(DOWNLOADS)/theora; \
make -j$(NPROC); make install
$(DOWNLOADS)/theora/Makefile: $(DOWNLOADS)/theora/configure
cd $(DOWNLOADS)/theora; \
$(CONFIGURE) --with-ogg=$(BUILD_PREFIX) --enable-shared=false --enable-static=true --disable-examples
$(DOWNLOADS)/theora/configure: $(DOWNLOADS)/theora/autogen.sh
cd $(DOWNLOADS)/theora; \
./autogen.sh
$(DOWNLOADS)/theora/autogen.sh:
$(CLONE) $(GITHUB)/xiph/theora $(DOWNLOADS)/theora
# Vorbis
libvorbis: init_dirs libogg $(LIBDIR)/libvorbis.a
@ -337,5 +355,5 @@ powerwash: clean-downloads
clean-downloads:
-rm -rf downloads
deps-core: libvorbis pixman libpng libjpeg physfs sdl2 sdl2image sdlsound sdl2ttf openal openssl fluidsynth uchardet iconv
deps-core: libtheora libvorbis pixman libpng libjpeg physfs sdl2 sdl2image sdlsound sdl2ttf openal openssl fluidsynth uchardet iconv
everything: deps-core ruby

View file

@ -59,6 +59,24 @@ CMAKE := $(CONFIGURE_ENV) cmake .. $(CMAKE_ARGS)
default:
# Theora
libtheora: init_dirs libvorbis libogg $(LIBDIR)/libtheora.a
$(LIBDIR)/libtheora.a: $(LIBDIR)/libogg.a $(DOWNLOADS)/theora/Makefile
cd $(DOWNLOADS)/theora; \
make -j$(NPROC); make install
$(DOWNLOADS)/theora/Makefile: $(DOWNLOADS)/theora/configure
cd $(DOWNLOADS)/theora; \
$(CONFIGURE) --with-ogg=$(BUILD_PREFIX) --enable-shared=false --enable-static=true --disable-examples
$(DOWNLOADS)/theora/configure: $(DOWNLOADS)/theora/autogen.sh
cd $(DOWNLOADS)/theora; \
./autogen.sh
$(DOWNLOADS)/theora/autogen.sh:
$(CLONE) $(GITHUB)/xiph/theora $(DOWNLOADS)/theora
# Vorbis
libvorbis: init_dirs libogg $(LIBDIR)/libvorbis.a
@ -329,5 +347,5 @@ clean-downloads:
clean-compiled:
-rm -rf build-$(SDK)-$(ARCH)
deps-core: libvorbis pixman libpng libjpeg physfs uchardet sdl2 sdl2image sdlsound sdl2ttf openal openssl
deps-core: libtheora libvorbis pixman libpng libjpeg physfs uchardet sdl2 sdl2image sdlsound sdl2ttf openal openssl
everything: deps-core ruby

View file

@ -21,6 +21,8 @@
#include "graphics.h"
#include "theoraplay/theoraplay.h"
#include "audio.h"
#include "binding.h"
#include "bitmap.h"
#include "config.h"
@ -63,6 +65,8 @@
#define DEF_FRAMERATE (rgssVer == 1 ? 40 : 60)
#define DEF_MAX_VIDEO_FRAMES 30
struct PingPong {
TEXFBO rt[2];
uint8_t srcInd, dstInd;
@ -912,7 +916,146 @@ void Graphics::resizeScreen(int width, int height) {
}
void Graphics::playMovie(const char *filename) {
Debug() << "Graphics.playMovie(" << filename << ") not implemented";
const size_t bufferSize = 256;
char filePath[bufferSize] = "";
snprintf(filePath, bufferSize, "%s.ogg", filename);
// Try adding the .ogg and .ogv extensions
if (!shState->fileSystem().exists(filePath)) {
snprintf(filePath, bufferSize, "%s.ogg", filename);
}
if (!shState->fileSystem().exists(filePath)) {
snprintf(filePath, bufferSize, "%s.ogv", filename);
}
if (!shState->fileSystem().exists(filePath)) {
// Movie file not found
Debug() << "Unable to open movie file: " << filePath;
return;
}
THEORAPLAY_Decoder *decoder = THEORAPLAY_startDecodeFile(filePath, DEF_MAX_VIDEO_FRAMES, THEORAPLAY_VIDFMT_RGBA);
if (!decoder) {
Debug() << "Failed to start decoding movie file: " << filePath;
return;
}
// Wait until the decoder has parsed out some basic truths from the file.
while (!THEORAPLAY_isInitialized(decoder)) {
SDL_Delay(10);
}
// Wait until we have video
const THEORAPLAY_VideoFrame *video = NULL;
while ((video = THEORAPLAY_getVideo(decoder)) == NULL) {
SDL_Delay(10);
}
/* Capture new scene */
p->checkSyncLock();
p->screen.composite();
TEXFBO &videoBuffer = p->screen.getPP().backBuffer();
Bitmap *videoBitmap = new Bitmap(video->width, video->height);
TransShader &shader = shState->shaders().trans;
shader.bind();
shader.applyViewportProj();
shader.setTransMap(videoBitmap->getGLTypes().tex);
shader.setVague(256.0f);
shader.setTexSize(p->scRes);
glState.blend.pushSet(false);
// We can just play the audio stream directly using the movie file
shState->audio().bgmPlay(filePath, 100, 100, 0);
// Flags on what to do upon video end
bool shutDown = false;
bool scriptReset = false;
// Assuming every frame has the same duration.
Uint32 frameMs = (video->fps == 0.0) ? 0 : ((Uint32) (1000.0 / video->fps));
Uint32 baseTicks = SDL_GetTicks();
while(THEORAPLAY_isDecoding(decoder)) {
if (p->threadData->rqTerm) {
shutDown = true;
break;
}
if (p->threadData->rqReset) {
break;
}
if (!video) {
video = THEORAPLAY_getVideo(decoder);
}
const Uint32 now = SDL_GetTicks() - baseTicks;
if (video && (video->playms <= now)) {
if (frameMs && ((now - video->playms) >= frameMs)) {
// Skip frames to catch up, but keep track of the last one
// in case we catch up to a series of dupe frames, which
// means we'd have to draw that final frame and then wait for
// more.
const THEORAPLAY_VideoFrame *previous = video;
while ((video = THEORAPLAY_getVideo(decoder)) != NULL) {
THEORAPLAY_freeVideo(previous);
previous = video;
if ((now - video->playms) < frameMs) {
break;
}
}
if (!video) {
video = previous;
}
}
// Application is too far behind
if (!video) {
Debug() << "WARNING: Video playback cannot keep up!";
break;
}
// Got a video frame, now draw it
p->checkSyncLock();
videoBitmap->replaceRaw(video->pixels, video->width * video->height * 4);
shader.bind();
FBO::bind(videoBuffer.fbo);
FBO::clear();
p->screenQuad.draw();
p->checkResize();
/* Then blit it flipped and scaled to the screen */
FBO::unbind();
FBO::clear();
GLMeta::blitBeginScreen(Vec2i(p->winSize));
GLMeta::blitSource(videoBuffer);
p->metaBlitBufferFlippedScaled();
GLMeta::blitEnd();
p->swapGLBuffer();
THEORAPLAY_freeVideo(video);
video = NULL;
} else {
// Next video frame not yet ready, let the CPU breathe
SDL_Delay(10);
}
}
if (video) THEORAPLAY_freeVideo(video);
if (decoder) THEORAPLAY_stopDecode(decoder);
glState.blend.pop();
videoBitmap->dispose();
delete videoBitmap;
shState->audio().bgmStop();
if(scriptReset) scriptBinding->reset();
if(shutDown) p->shutdown();
}
void Graphics::screenshot(const char *filename) {

View file

@ -1,5 +1,6 @@
physfs = dependency('physfs', version: '>=2.1', static: build_static)
openal = dependency('openal', static: build_static, method: 'pkg-config')
theora = dependency('theora', static: build_static)
vorbisfile = dependency('vorbisfile', static: build_static)
vorbis = dependency('vorbis', static: build_static)
ogg = dependency('ogg', static: build_static)
@ -82,7 +83,7 @@ global_include_dirs += include_directories('.',
'util', 'util/sigslot', 'util/sigslot/adapter'
)
global_dependencies += [openal, zlib, bz2, sdl2, sdl_sound, pixman, physfs, vorbisfile, vorbis, ogg, sdl2_ttf, freetype, sdl2_image, png, jpeg, iconv, uchardet]
global_dependencies += [openal, zlib, bz2, sdl2, sdl_sound, pixman, physfs, theora, vorbisfile, vorbis, ogg, sdl2_ttf, freetype, sdl2_image, png, jpeg, iconv, uchardet]
if host_system == 'windows'
global_dependencies += compilers['cpp'].find_library('wsock32')
endif
@ -115,6 +116,7 @@ main_source = files(
'audio/sdlsoundsource.cpp',
'audio/soundemitter.cpp',
'audio/vorbissource.cpp',
'theoraplay/theoraplay.c',
'crypto/rgssad.cpp',

View file

@ -0,0 +1,17 @@
Copyright (c) 2011-2021 Ryan C. Gordon <icculus@icculus.org>.
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.

15
src/theoraplay/README.md Normal file
View file

@ -0,0 +1,15 @@
# TheoraPlay
A small C library to make Ogg Theora decoding easier.
A tiny example to pull data out of an .ogv file is about
[50 lines of C code](https://github.com/icculus/theoraplay/blob/main/test/testtheoraplay.c),
and a complete SDL-based media player is about
[300 lines of code](https://github.com/icculus/theoraplay/blob/main/test/simplesdl.c).
TheoraPlay is optimized for multicore CPUs, and is designed to be
programmer-friendly. You will need libogg, libvorbis, and libtheora,
of course, but then you just drop a .c file and two headers into your
project and you're ready to hook up video decoding, without worrying
about Ogg pages, Vorbis blocks, or Theora decoder state.

827
src/theoraplay/theoraplay.c Normal file
View file

@ -0,0 +1,827 @@
/**
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#ifdef _WIN32
#include <windows.h>
#define THEORAPLAY_THREAD_T HANDLE
#define THEORAPLAY_MUTEX_T HANDLE
#define sleepms(x) Sleep(x)
#else
#include <pthread.h>
#include <unistd.h>
#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);
const unsigned char *p0data = ycbcr[p0].data + yoff;
const int p0stride = ycbcr[p0].stride;
const unsigned char *p1data = ycbcr[p1].data + uvoff;
const int p1stride = ycbcr[p1].stride;
const unsigned char *p2data = ycbcr[p2].data + uvoff;
const int p2stride = ycbcr[p2].stride;
if (yuv)
{
unsigned char *dst = yuv;
for (i = 0; i < h; i++, dst += w)
memcpy(dst, p0data + (p0stride * i), w);
for (i = 0; i < (h / 2); i++, dst += w/2)
memcpy(dst, p1data + (p1stride * i), w / 2);
for (i = 0; i < (h / 2); i++, dst += w/2)
memcpy(dst, p2data + (p2stride * 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;
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;
// you have to guide the Theora decoder to get meaningful timestamps, apparently. :/
if (packet.granulepos >= 0)
th_decode_ctl(tdec, TH_DECCTL_SET_GRANPOS, &packet.granulepos, sizeof(packet.granulepos));
if (th_decode_packetin(tdec, &packet, &granulepos) == 0) // new frame!
{
th_ycbcr_buffer ycbcr;
if (th_decode_ycbcr_out(tdec, ycbcr) == 0)
{
const double videotime = th_granule_time(tdec, granulepos);
VideoFrame *item = (VideoFrame *) malloc(sizeof (VideoFrame));
if (item == NULL) goto cleanup;
item->playms = (unsigned int) (videotime * 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
} // 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)
{
FILE *f = (FILE *) io->userdata;
const size_t br = fread(buf, 1, buflen, f);
if ((br == 0) && ferror(f))
return -1;
return (long) br;
} // IoFopenRead
static void IoFopenClose(THEORAPLAY_Io *io)
{
FILE *f = (FILE *) io->userdata;
fclose(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;
FILE *f = fopen(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_availableVideo
unsigned int THEORAPLAY_availableAudio(THEORAPLAY_Decoder *decoder)
{
GET_SYNCED_VALUE(unsigned int, 0, decoder, audioms);
} // THEORAPLAY_availableAudio
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 ...

View file

@ -0,0 +1,85 @@
/**
* 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.
*/
#ifndef _INCL_THEORAPLAY_H_
#define _INCL_THEORAPLAY_H_
#ifdef __cplusplus
extern "C" {
#endif
typedef struct THEORAPLAY_Io THEORAPLAY_Io;
struct THEORAPLAY_Io
{
long (*read)(THEORAPLAY_Io *io, void *buf, long buflen);
void (*close)(THEORAPLAY_Io *io);
void *userdata;
};
typedef struct THEORAPLAY_Decoder THEORAPLAY_Decoder;
/* YV12 is YCrCb, not YCbCr; that's what SDL uses for YV12 overlays. */
typedef enum THEORAPLAY_VideoFormat
{
THEORAPLAY_VIDFMT_YV12, /* NTSC colorspace, planar YCrCb 4:2:0 */
THEORAPLAY_VIDFMT_IYUV, /* NTSC colorspace, planar YCbCr 4:2:0 */
THEORAPLAY_VIDFMT_RGB, /* 24 bits packed pixel RGB */
THEORAPLAY_VIDFMT_RGBA /* 32 bits packed pixel RGBA (full alpha). */
} THEORAPLAY_VideoFormat;
typedef struct THEORAPLAY_VideoFrame
{
unsigned int playms;
double fps;
unsigned int width;
unsigned int height;
THEORAPLAY_VideoFormat format;
unsigned char *pixels;
struct THEORAPLAY_VideoFrame *next;
} THEORAPLAY_VideoFrame;
typedef struct THEORAPLAY_AudioPacket
{
unsigned int playms; /* playback start time in milliseconds. */
int channels;
int freq;
int frames;
float *samples; /* frames * channels float32 samples. */
struct THEORAPLAY_AudioPacket *next;
} THEORAPLAY_AudioPacket;
THEORAPLAY_Decoder *THEORAPLAY_startDecodeFile(const char *fname,
const unsigned int maxframes,
THEORAPLAY_VideoFormat vidfmt);
THEORAPLAY_Decoder *THEORAPLAY_startDecode(THEORAPLAY_Io *io,
const unsigned int maxframes,
THEORAPLAY_VideoFormat vidfmt);
void THEORAPLAY_stopDecode(THEORAPLAY_Decoder *decoder);
int THEORAPLAY_isDecoding(THEORAPLAY_Decoder *decoder);
int THEORAPLAY_decodingError(THEORAPLAY_Decoder *decoder);
int THEORAPLAY_isInitialized(THEORAPLAY_Decoder *decoder);
int THEORAPLAY_hasVideoStream(THEORAPLAY_Decoder *decoder);
int THEORAPLAY_hasAudioStream(THEORAPLAY_Decoder *decoder);
unsigned int THEORAPLAY_availableVideo(THEORAPLAY_Decoder *decoder);
unsigned int THEORAPLAY_availableAudio(THEORAPLAY_Decoder *decoder);
const THEORAPLAY_AudioPacket *THEORAPLAY_getAudio(THEORAPLAY_Decoder *decoder);
void THEORAPLAY_freeAudio(const THEORAPLAY_AudioPacket *item);
const THEORAPLAY_VideoFrame *THEORAPLAY_getVideo(THEORAPLAY_Decoder *decoder);
void THEORAPLAY_freeVideo(const THEORAPLAY_VideoFrame *item);
#ifdef __cplusplus
}
#endif
#endif /* include-once blocker. */
/* end of theoraplay.h ... */

View file

@ -0,0 +1,90 @@
/**
* 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.
*/
#if !THEORAPLAY_INTERNAL
#error Do not include this in your app. It is used internally by TheoraPlay.
#endif
static unsigned char *THEORAPLAY_CVT_FNNAME_420(const th_info *tinfo,
const th_ycbcr_buffer ycbcr)
{
const int w = tinfo->pic_width;
const int h = tinfo->pic_height;
const int halfw = w / 2;
unsigned char *pixels = (unsigned char *) malloc(w * h * 4);
// http://www.theora.org/doc/Theora.pdf, 1.1 spec,
// chapter 4.2 (Y'CbCr -> Y'PbPr -> R'G'B')
// These constants apparently work for NTSC _and_ PAL/SECAM.
const float yoffset = 16.0f;
const float yexcursion = 219.0f;
const float cboffset = 128.0f;
const float cbexcursion = 224.0f;
const float croffset = 128.0f;
const float crexcursion = 224.0f;
const float kr = 0.299f;
const float kb = 0.114f;
if (pixels)
{
unsigned char *dst = pixels;
const int ystride = ycbcr[0].stride;
const int cbstride = ycbcr[1].stride;
const int crstride = ycbcr[2].stride;
const int yoff = (tinfo->pic_x & ~1) + ystride * (tinfo->pic_y & ~1);
const int cboff = (tinfo->pic_x / 2) + (cbstride) * (tinfo->pic_y / 2);
const unsigned char *py = ycbcr[0].data + yoff;
const unsigned char *pcb = ycbcr[1].data + cboff;
const unsigned char *pcr = ycbcr[2].data + cboff;
int posy;
for (posy = 0; posy < h; posy++)
{
int posx, poshalfx;
posx = 0;
for (poshalfx = 0; poshalfx < halfw; poshalfx++, posx += 2)
{
const float y1 = (((float) py[posx]) - yoffset) / yexcursion;
const float y2 = (((float) py[posx+1]) - yoffset) / yexcursion;
const float pb = (((float) pcb[poshalfx]) - cboffset) / cbexcursion;
const float pr = (((float) pcr[poshalfx]) - croffset) / crexcursion;
const float r1 = (y1 + (2.0f * (1.0f - kr) * pr)) * 255.0f;
const float g1 = (y1 - ((2.0f * (((1.0f - kb) * kb) / ((1.0f - kb) - kr))) * pb) - ((2.0f * (((1.0f - kr) * kr) / ((1.0f - kb) - kr))) * pr)) * 255.0f;
const float b1 = (y1 + (2.0f * (1.0f - kb) * pb)) * 255.0f;
const float r2 = (y2 + (2.0f * (1.0f - kr) * pr)) * 255.0f;
const float g2 = (y2 - ((2.0f * (((1.0f - kb) * kb) / ((1.0f - kb) - kr))) * pb) - ((2.0f * (((1.0f - kr) * kr) / ((1.0f - kb) - kr))) * pr)) * 255.0f;
const float b2 = (y2 + (2.0f * (1.0f - kb) * pb)) * 255.0f;
*(dst++) = (unsigned char) ((r1 < 0.0f) ? 0.0f : (r1 > 255.0f) ? 255.0f : r1);
*(dst++) = (unsigned char) ((g1 < 0.0f) ? 0.0f : (g1 > 255.0f) ? 255.0f : g1);
*(dst++) = (unsigned char) ((b1 < 0.0f) ? 0.0f : (b1 > 255.0f) ? 255.0f : b1);
#if THEORAPLAY_CVT_RGB_ALPHA
*(dst++) = 0xFF;
#endif
*(dst++) = (unsigned char) ((r2 < 0.0f) ? 0.0f : (r2 > 255.0f) ? 255.0f : r2);
*(dst++) = (unsigned char) ((g2 < 0.0f) ? 0.0f : (g2 > 255.0f) ? 255.0f : g2);
*(dst++) = (unsigned char) ((b2 < 0.0f) ? 0.0f : (b2 > 255.0f) ? 255.0f : b2);
#if THEORAPLAY_CVT_RGB_ALPHA
*(dst++) = 0xFF;
#endif
} // for
// adjust to the start of the next line.
py += ystride;
pcb += cbstride * (posy % 2);
pcr += crstride * (posy % 2);
} // for
} // if
return pixels;
} // THEORAPLAY_CVT_FNNAME_420
// end of theoraplay_cvtrgb.h ...

View file

@ -48,6 +48,24 @@ CMAKE := $(CONFIGURE_ENV) cmake .. $(CMAKE_ARGS)
default: everything
# Theora
libtheora: init_dirs libvorbis libogg $(LIBDIR)/libtheora.a
$(LIBDIR)/libtheora.a: $(LIBDIR)/libogg.a $(DOWNLOADS)/theora/Makefile
cd $(DOWNLOADS)/theora; \
make -j$(NPROC); make install
$(DOWNLOADS)/theora/Makefile: $(DOWNLOADS)/theora/configure
cd $(DOWNLOADS)/theora; \
$(CONFIGURE) --with-ogg=$(BUILD_PREFIX) --enable-shared=false --enable-static=true --disable-examples
$(DOWNLOADS)/theora/configure: $(DOWNLOADS)/theora/autogen.sh
cd $(DOWNLOADS)/theora; \
./autogen.sh
$(DOWNLOADS)/theora/autogen.sh:
$(CLONE) $(GITHUB)/xiph/theora $(DOWNLOADS)/theora
# Vorbis
libvorbis: init_dirs libogg $(LIBDIR)/libvorbis.a
@ -320,5 +338,5 @@ powerwash: clean-downloads
clean-downloads:
-rm -rf downloads
deps-core: libvorbis pixman libpng libjpeg physfs sdl2 sdl2image sdlsound sdl2ttf openal openssl fluidsynth uchardet
deps-core: libtheora libvorbis pixman libpng libjpeg physfs sdl2 sdl2image sdlsound sdl2ttf openal openssl fluidsynth uchardet
everything: deps-core ruby