// This player code is based largely on the player example in the libtheora package // and so they get a fair bit of credit for this code, even if in ideas/comments only #ifdef SCP_UNIX #include #include #endif #include "globalincs/pstypes.h" #include "globalincs/def_files.h" #include "cfile/cfile.h" #include "graphics/gropengl.h" #include "graphics/gropengltexture.h" #include "graphics/gropenglextension.h" #include "graphics/gropenglshader.h" #include "graphics/gropenglstate.h" #include "graphics/gropengldraw.h" #include "graphics/2d.h" #include "io/key.h" #include "osapi/osapi.h" #include "sound/sound.h" #include "sound/openal.h" #include "bmpman/bmpman.h" #include "cutscene/oggplayer.h" #include "io/timer.h" #include "theora/theora.h" #include "vorbis/codec.h" extern int Cmdline_noscalevid; static int hp2, wp2; static int video_inited = 0; static int scale_video = 0; static int playing = 1; static ubyte *pixelbuf = NULL; static uint g_screenWidth = 0; static uint g_screenHeight = 0; static int g_screenX = 0; static int g_screenY = 0; static GLuint GLtex = 0; static GLuint ytex = 0; static GLuint utex = 0; static GLuint vtex = 0; static GLint gl_screenYH = 0; static GLint gl_screenXW = 0; static GLfloat gl_screenU = 0; static GLfloat gl_screenV = 0; static GLfloat glVertices[4][4] = {{0}}; // video externs from API graphics functions extern void opengl_tcache_get_adjusted_texture_size(int w_in, int h_in, int *w_out, int *h_out); static int timer_started = 0; static longlong base_time = -1; #ifdef SCP_UNIX static struct timeval timer_expire = { 0, 0 }; #else static int timer_expire; #endif static int audio_inited = 0; static int audio_buffer_tail = 0; static ALuint audio_sid = 0; #define OGG_AUDIO_BUFFERS 15 static ALuint audio_buffers[OGG_AUDIO_BUFFERS]; // single audio fragment audio buffering static int audiobuf_fill = 0; static short *audiobuf = NULL; static longlong audiobuf_granulepos = 0; // time position of last sample static int audiofd_fragsize = 0; // single frame video buffering static longlong videobuf_granulepos = -1; static double videobuf_time = 0; static bool use_shaders = true; // ----------------------------------------------------------------------------- // Utility items // // helper; just grab some more compressed bitstream and sync it for page extraction static int OGG_buffer_data(THEORAFILE *movie) { char *buffer = ogg_sync_buffer(&movie->osyncstate, 8192); // Doubled read size to fix choppy audio with high bitrate movies - Valathil int bytes = cfread(buffer, 1, 8192, movie->cfp); ogg_sync_wrote(&movie->osyncstate, bytes); return bytes; } // helper: push a page into the appropriate steam // this can be done blindly; a stream won't accept a page that doesn't belong to it static void OGG_queue_page(THEORAFILE *movie) { Assert( movie->theora_p ); ogg_stream_pagein(&movie->t_osstate, &movie->opage); if (movie->vorbis_p) ogg_stream_pagein(&movie->v_osstate, &movie->opage); } // get relative time since beginning playback, compensating for A/V drift static double OGG_get_time() { #ifdef SCP_UNIX longlong now; struct timeval tv; gettimeofday(&tv, NULL); now = tv.tv_sec * 1000 + tv.tv_usec / 1000; if (base_time == -1) base_time = now; return (now - base_time) * 0.001; #else if (base_time == -1) base_time = timer_get_milliseconds(); return (timer_get_milliseconds() - base_time) * 0.001; #endif } static void OGG_timer_init() { #if SCP_UNIX int nsec = 0; gettimeofday(&timer_expire, NULL); timer_expire.tv_usec += (int)((videobuf_time - OGG_get_time()) * 1000000.0); if (timer_expire.tv_usec > 1000000) { nsec = timer_expire.tv_usec / 1000000; timer_expire.tv_sec += nsec; timer_expire.tv_usec -= nsec * 1000000; } #else timer_expire = timer_get_microseconds(); timer_expire += (int)((videobuf_time - OGG_get_time()) * 1000000.0); #endif timer_started = 1; } static void OGG_timer_do_wait() { if (!timer_started) OGG_timer_init(); #if SCP_UNIX int nsec = 0; struct timespec ts, tsRem; struct timeval tv; gettimeofday(&tv, NULL); if (tv.tv_sec > timer_expire.tv_sec) goto end; if ( (tv.tv_sec == timer_expire.tv_sec) && (tv.tv_usec >= timer_expire.tv_usec) ) goto end; ts.tv_sec = timer_expire.tv_sec - tv.tv_sec; ts.tv_nsec = 1000 * (timer_expire.tv_usec - tv.tv_usec); if (ts.tv_nsec < 0) { ts.tv_nsec += 1000000000UL; --ts.tv_sec; } if ( (nanosleep(&ts, &tsRem) == -1) && (errno == EINTR) ) { // so we got an error that was a signal interupt, try to sleep again with remainder of time if ( (nanosleep(&tsRem, NULL) == -1) && (errno == EINTR) ) { mprintf(("MVE: Timer error! Aborting movie playback!\n")); return; } } end: timer_expire.tv_usec += (int)((videobuf_time - OGG_get_time()) * 1000000.0); if (timer_expire.tv_usec > 1000000) { nsec = timer_expire.tv_usec / 1000000; timer_expire.tv_sec += nsec; timer_expire.tv_usec -= nsec * 1000000; } #else int tv, ts, ts2; tv = timer_get_microseconds(); if (tv > timer_expire) goto end; ts = timer_expire - tv; ts2 = ts/1000; Sleep(ts2); end: timer_expire += (int)((videobuf_time - OGG_get_time()) * 1000000.0); #endif } // // End Utility items // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // That wonderful Audio // static void OGG_audio_init(vorbis_info *vinfo) { if (audio_inited) return; Assert( vinfo != NULL ); audio_inited = 1; OpenAL_ErrorCheck( alGenSources(1, &audio_sid), return ); OpenAL_ErrorPrint( alSourcef(audio_sid, AL_GAIN, 1.0f) ); memset( &audio_buffers, 0, sizeof(ALuint) * OGG_AUDIO_BUFFERS ); audiofd_fragsize = (((vinfo->channels * 16) / 8) * vinfo->rate); audiobuf = (short *) vm_malloc(audiofd_fragsize); } static void OGG_audio_close() { if ( !audio_inited ) return; audio_inited = 0; ALint p = 0; OpenAL_ErrorPrint( alSourceStop(audio_sid) ); OpenAL_ErrorPrint( alGetSourcei(audio_sid, AL_BUFFERS_PROCESSED, &p) ); OpenAL_ErrorPrint( alSourceUnqueueBuffers(audio_sid, p, audio_buffers) ); OpenAL_ErrorPrint( alDeleteSources(1, &audio_sid) ); for (int i = 0; i < OGG_AUDIO_BUFFERS; i++) { // make sure that the buffer is real before trying to delete, it could crash for some otherwise if ( (audio_buffers[i] != 0) && alIsBuffer(audio_buffers[i]) ) { OpenAL_ErrorPrint( alDeleteBuffers(1, &audio_buffers[i]) ); } } audio_sid = 0; audio_buffer_tail = 0; audiobuf_fill = 0; audiobuf_granulepos = 0; audiofd_fragsize = 0; if (audiobuf != NULL) { vm_free(audiobuf); audiobuf = NULL; } } static void OGG_audio_write(vorbis_info *vorbis, bool *ready) { ALint status, queued, processed = 0; ALuint bid = 0; if ( !audio_inited || !(*ready) ) return; OpenAL_ErrorCheck( alGetSourcei(audio_sid, AL_BUFFERS_PROCESSED, &processed), return ); while (processed-- > 2) OpenAL_ErrorPrint( alSourceUnqueueBuffers(audio_sid, 1, &bid) ); if ( !(*ready) ) return; OpenAL_ErrorCheck( alGetSourcei(audio_sid, AL_BUFFERS_QUEUED, &queued), return ); if ( audiobuf_fill && (queued < OGG_AUDIO_BUFFERS) ) { if ( !audio_buffers[audio_buffer_tail] ) OpenAL_ErrorCheck( alGenBuffers(1, &audio_buffers[audio_buffer_tail]), return ); ALenum format = (vorbis->channels == 2) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; OpenAL_ErrorCheck( alBufferData(audio_buffers[audio_buffer_tail], format, audiobuf, audiobuf_fill, vorbis->rate), return ); OpenAL_ErrorCheck( alSourceQueueBuffers(audio_sid, 1, &audio_buffers[audio_buffer_tail]), return ); if (++audio_buffer_tail == OGG_AUDIO_BUFFERS) audio_buffer_tail = 0; audiobuf_fill = 0; *ready = false; } else { // mprintf(("Theora WARN: Buffer overrun: Queue full\n")); } OpenAL_ErrorCheck( alGetSourcei(audio_sid, AL_SOURCE_STATE, &status), return ); OpenAL_ErrorCheck( alGetSourcei(audio_sid, AL_BUFFERS_QUEUED, &queued), return ); if ( (status != AL_PLAYING) && (queued > 0) ) OpenAL_ErrorPrint( alSourcePlay(audio_sid) ); } // // End Audio stuff // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // Video related stuff // static void OGG_video_init(theora_info *tinfo) { float scale_by = 0.0f; if (video_inited) return; Assert( tinfo != NULL ); g_screenWidth = tinfo->frame_width; g_screenHeight = tinfo->frame_height; if (gr_screen.mode == GR_OPENGL) { opengl_set_texture_target(GL_TEXTURE_2D); opengl_tcache_get_adjusted_texture_size(g_screenWidth, g_screenHeight, &wp2, &hp2); if(!Use_GLSL) use_shaders = false; if(use_shaders) { glGenTextures(1, &ytex); glGenTextures(1, &utex); glGenTextures(1, &vtex); Assert( ytex != 0 ); Assert( utex != 0 ); Assert( vtex != 0 ); if ( ytex + utex + vtex == 0 ) { nprintf(("MOVIE", "ERROR: Can't create a GL texture")); video_inited = 1; return; } int sdr_handle = gr_maybe_create_shader(SDR_TYPE_VIDEO_PROCESS, 0); if ( sdr_handle >= 0) opengl_shader_set_current(sdr_handle); else use_shaders = false; } if(!use_shaders) { glGenTextures(1, &GLtex); Assert( GLtex != 0 ); if ( GLtex == 0 ) { nprintf(("MOVIE", "ERROR: Can't create a GL texture")); video_inited = 1; return; } } gr_set_lighting(false, false); GL_state.Texture.DisableAll(); if(!use_shaders) { GL_state.Texture.SetActiveUnit(0); GL_state.Texture.SetTarget(GL_texture_target); GL_state.Texture.Enable(GLtex); glTexParameteri(GL_texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_state.Texture.GetTarget(), 0, GL_RGB8, wp2, hp2, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL); } GL_state.SetTextureSource(TEXTURE_SOURCE_DECAL); GL_state.SetAlphaBlendMode(ALPHA_BLEND_NONE); GL_state.SetZbufferType(ZBUFFER_TYPE_NONE); if(use_shaders) { GL_state.Texture.SetActiveUnit(0); GL_state.Texture.SetTarget(GL_texture_target); GL_state.Texture.Enable(ytex); glTexParameteri(GL_texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // NOTE: using NULL instead of pixelbuf crashes some drivers, but then so does pixelbuf glTexImage2D(GL_state.Texture.GetTarget(), 0, GL_LUMINANCE8, 2048, 2048, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); GL_state.Texture.SetActiveUnit(1); GL_state.Texture.SetTarget(GL_texture_target); GL_state.Texture.Enable(utex); glTexParameteri(GL_texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // NOTE: using NULL instead of pixelbuf crashes some drivers, but then so does pixelbuf glTexImage2D(GL_state.Texture.GetTarget(), 0, GL_LUMINANCE8, 1024, 1024, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); GL_state.Texture.SetActiveUnit(2); GL_state.Texture.SetTarget(GL_texture_target); GL_state.Texture.Enable(vtex); glTexParameteri(GL_texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // NOTE: using NULL instead of pixelbuf crashes some drivers, but then so does pixelbuf glTexImage2D(GL_state.Texture.GetTarget(), 0, GL_LUMINANCE8, 1024, 1024, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); } float screen_ratio = (float)gr_screen.max_w / (float)gr_screen.max_h; float movie_ratio = (float)g_screenWidth / (float)g_screenHeight; if (screen_ratio > movie_ratio) scale_by = (float)gr_screen.max_h / (float)g_screenHeight; else scale_by = (float)gr_screen.max_w / (float)g_screenWidth; // don't bother setting anything if we aren't going to need it if (!Cmdline_noscalevid && (scale_by != 1.0f)) { glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); glScalef( scale_by, scale_by, -1.0f ); scale_video = 1; } // set our color so that we can make sure that it's correct GL_state.Color(255, 255, 255, 255); } if(!use_shaders) { pixelbuf = (ubyte *) vm_malloc_q(g_screenWidth * g_screenHeight * 3); if (pixelbuf == NULL) { nprintf(("MOVIE", "ERROR: Can't allocate memory for pixelbuf")); video_inited = 1; return; } memset( pixelbuf, 0, g_screenWidth * g_screenHeight * 3 ); } if (scale_video) { g_screenX = ((fl2i(gr_screen.max_w / scale_by + 0.5f) - g_screenWidth) / 2); g_screenY = ((fl2i(gr_screen.max_h / scale_by + 0.5f) - g_screenHeight) / 2); } else { // centers on 1024x768, fills on 640x480 g_screenX = ((gr_screen.max_w - g_screenWidth) / 2); g_screenY = ((gr_screen.max_h - g_screenHeight) / 2); } // set additional values for screen width/height and UV coords if (gr_screen.mode == GR_OPENGL) { gl_screenYH = g_screenY + g_screenHeight; gl_screenXW = g_screenX + g_screenWidth; gl_screenU = i2fl(g_screenWidth) / i2fl(wp2); gl_screenV = i2fl(g_screenHeight) / i2fl(hp2); if(use_shaders) { gl_screenU = i2fl(tinfo->frame_width-1) / i2fl(2048) ; gl_screenV = i2fl(tinfo->frame_height-1) / i2fl(2048); GL_state.Texture.SetShaderMode(GL_TRUE); GL_state.Uniform.setUniformi("ytex", 0); GL_state.Uniform.setUniformi("utex", 1); GL_state.Uniform.setUniformi("vtex", 2); } glVertices[0][0] = (GLfloat)g_screenX; glVertices[0][1] = (GLfloat)g_screenY; glVertices[0][2] = 0.0f; glVertices[0][3] = 0.0f; glVertices[1][0] = (GLfloat)g_screenX; glVertices[1][1] = (GLfloat)gl_screenYH; glVertices[1][2] = 0.0f; glVertices[1][3] = gl_screenV; glVertices[2][0] = (GLfloat)gl_screenXW; glVertices[2][1] = (GLfloat)g_screenY; glVertices[2][2] = gl_screenU; glVertices[2][3] = 0.0f; glVertices[3][0] = (GLfloat)gl_screenXW; glVertices[3][1] = (GLfloat)gl_screenYH; glVertices[3][2] = gl_screenU; glVertices[3][3] = gl_screenV; GL_state.Array.BindArrayBuffer(0); vertex_layout vert_def; vert_def.add_vertex_component(vertex_format_data::POSITION2, sizeof(glVertices[0]), glVertices); vert_def.add_vertex_component(vertex_format_data::TEX_COORD, sizeof(glVertices[0]), &(glVertices[0][2])); opengl_bind_vertex_layout(vert_def); } if(!use_shaders && tinfo->frame_height > 450) { mprintf(("VIDEO: No shader support and hd video is beeing played this can get choppy.")); } video_inited = 1; } static void OGG_video_close() { if ( !video_inited ) { return; } if (gr_screen.mode == GR_OPENGL) { if (scale_video) { glMatrixMode(GL_MODELVIEW); glPopMatrix(); } GL_state.Texture.Disable(); if(use_shaders) { GL_state.Texture.Delete(ytex); GL_state.Texture.Delete(utex); GL_state.Texture.Delete(vtex); glDeleteTextures(1, &ytex); glDeleteTextures(1, &utex); glDeleteTextures(1, &vtex); } else { GL_state.Texture.Delete(GLtex); glDeleteTextures(1, &GLtex); } opengl_set_texture_target(); ytex = utex = vtex = 0; GLtex = 0; GL_state.Texture.SetShaderMode(GL_FALSE); opengl_shader_set_current(); } if (pixelbuf != NULL) { vm_free(pixelbuf); pixelbuf = NULL; } video_inited = 0; scale_video = 0; pixelbuf = NULL; g_screenWidth = 0; g_screenHeight = 0; g_screenX = 0; g_screenY = 0; gl_screenYH = 0; gl_screenXW = 0; gl_screenU = 0; gl_screenV = 0; } static void convert_YUV_to_RGB(yuv_buffer *yuv) { int Y1, Y2, U, V; int R = 0, G = 0, B = 0; int C, D, E; uint x, y; uint width_2 = g_screenWidth/2; ubyte *pix = &pixelbuf[0]; ubyte *y_ptr = (ubyte *)yuv->y; ubyte *u_ptr = (ubyte *)yuv->u; ubyte *v_ptr = (ubyte *)yuv->v; for (y = 0; y < g_screenHeight; y++) { for (x = 0; x < width_2; x++) { // we need two pixels of Y Y1 = *y_ptr; y_ptr++; Y2 = *y_ptr; y_ptr++; // only one pixel of U and V (half the size of Y) U = u_ptr[x]; V = v_ptr[x]; D = (U - 128); E = (V - 128); // first pixel C = (Y1 - 16) * 298; R = ((C + 409 * E + 128) >> 8); G = ((C - 100 * D - 208 * E + 128) >> 8); B = ((C + 516 * D + 128) >> 8); CLAMP(R, 0, 255); CLAMP(G, 0, 255); CLAMP(B, 0, 255); *pix++ = (ubyte)B; *pix++ = (ubyte)G; *pix++ = (ubyte)R; // second pixel (U and V values are resused) C = (Y2 - 16) * 298; R = ((C + 409 * E + 128) >> 8); G = ((C - 100 * D - 208 * E + 128) >> 8); B = ((C + 516 * D + 128) >> 8); CLAMP(R, 0, 255); CLAMP(G, 0, 255); CLAMP(B, 0, 255); *pix++ = (ubyte)B; *pix++ = (ubyte)G; *pix++ = (ubyte)R; } y_ptr += (yuv->y_stride - yuv->y_width); // u and v have to be done every other row (it's a 2x2 block) if (y % 2) { u_ptr += yuv->uv_stride; v_ptr += yuv->uv_stride; } } } extern int Mouse_hidden; static void OGG_video_draw(theora_state *tstate) { yuv_buffer yuv; theora_decode_YUVout(tstate, &yuv); if(!use_shaders) convert_YUV_to_RGB(&yuv); if (gr_screen.mode == GR_OPENGL) { if(use_shaders) { GL_state.Texture.SetActiveUnit(0); glTexSubImage2D(GL_state.Texture.GetTarget(), 0, 0, 0, yuv.y_stride, yuv.y_height, GL_LUMINANCE, GL_UNSIGNED_BYTE, yuv.y); GL_state.Texture.SetActiveUnit(1); glTexSubImage2D(GL_state.Texture.GetTarget(), 0, 0, 0, yuv.uv_stride, yuv.uv_height, GL_LUMINANCE, GL_UNSIGNED_BYTE, yuv.u); GL_state.Texture.SetActiveUnit(2); glTexSubImage2D(GL_state.Texture.GetTarget(), 0, 0, 0, yuv.uv_stride, yuv.uv_height, GL_LUMINANCE, GL_UNSIGNED_BYTE, yuv.v); } else { glTexSubImage2D(GL_state.Texture.GetTarget(), 0, 0, 0, g_screenWidth, g_screenHeight, GL_BGR, GL_UNSIGNED_BYTE, pixelbuf); } glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } Mouse_hidden = 1; gr_flip(); os_poll(); int k = key_inkey(); switch (k) { case KEY_ESC: case KEY_ENTER: case KEY_SPACEBAR: playing = 0; } } // // End Video related stuff // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // The global stuff... // // close out a theora movie and free it's memory void theora_close(THEORAFILE *movie) { Assert( movie != NULL ); timer_started = 0; base_time = -1; videobuf_granulepos = -1; videobuf_time = 0; if (movie->vorbis_p) { ogg_stream_clear(&movie->v_osstate); vorbis_block_clear(&movie->vblock); vorbis_dsp_clear(&movie->vstate); vorbis_comment_clear(&movie->vcomment); vorbis_info_clear(&movie->vinfo); } if (movie->theora_p) { ogg_stream_clear(&movie->t_osstate); theora_clear(&movie->tstate); theora_comment_clear(&movie->tcomment); theora_info_clear(&movie->tinfo); } ogg_sync_clear(&movie->osyncstate); // free the stream if (movie->cfp) cfclose(movie->cfp); // free the struct vm_free(movie); playing = 1; } // opens a theora file, verifies we can use it, initializes everything needed // to decode and use the file THEORAFILE *theora_open(char *filename) { char lower_name[MAX_FILENAME_LEN]; THEORAFILE *movie; int stateflag = 0; // create the file movie = (THEORAFILE *) vm_malloc_q( sizeof(THEORAFILE) ); if (movie == NULL) return NULL; memset( movie, 0, sizeof(THEORAFILE) ); // lower case filename for checking memset( lower_name, 0, sizeof(lower_name) ); strncpy( lower_name, filename, sizeof(lower_name)-1 ); strlwr(lower_name); char *p = strchr( lower_name, '.' ); if ( p ) *p = 0; strcat_s( lower_name, ".ogg" ); // NOTE: Because the .ogg extension is used for both movies and sounds, we have to // be a bit more specific about our search locations, so we look only in the // two places that Theora movies might exist. movie->cfp = cfopen(lower_name, "rb", CFILE_NORMAL, CF_TYPE_ROOT); if (movie->cfp == NULL) movie->cfp = cfopen(lower_name, "rb", CFILE_NORMAL, CF_TYPE_MOVIES); if (movie->cfp == NULL) { mprintf(("Theora ERROR: Unable to find and open movie file named '%s'\n", lower_name)); goto Error; } // start up Ogg stream synchronization layer ogg_sync_init(&movie->osyncstate); // init supporting Vorbis structures needed in header parsing vorbis_info_init(&movie->vinfo); vorbis_comment_init(&movie->vcomment); // init supporting Theora structures needed in header parsing theora_comment_init(&movie->tcomment); theora_info_init(&movie->tinfo); // ogg file open so parse the headers // we are only interested in Vorbis/Theora streams while ( !stateflag ) { int ret = OGG_buffer_data(movie); if (ret == 0) break; while (ogg_sync_pageout(&movie->osyncstate, &movie->opage) > 0) { ogg_stream_state test; // is this a mandated initial header? If not, stop parsing if ( !ogg_page_bos(&movie->opage) ) { // don't leak the page; get it into the appropriate stream OGG_queue_page(movie); stateflag = 1; break; } ogg_stream_init(&test, ogg_page_serialno(&movie->opage)); ogg_stream_pagein(&test, &movie->opage); ogg_stream_packetout(&test, &movie->opacket); // identify the codec: try theora if ( !movie->theora_p && (theora_decode_header(&movie->tinfo, &movie->tcomment, &movie->opacket) >= 0) ) { // it is theora memcpy( &movie->t_osstate, &test, sizeof(test) ); movie->theora_p = 1; } else if ( !movie->vorbis_p && (vorbis_synthesis_headerin(&movie->vinfo, &movie->vcomment, &movie->opacket) >= 0) ) { // it is vorbis memcpy( &movie->v_osstate, &test, sizeof(test) ); movie->vorbis_p = 1; } else { // whatever it is, we don't care about it ogg_stream_clear(&test); } } // fall through to non-bos page parsing } // if we don't have usable video then bail out now if ( !movie->theora_p ) { mprintf(("Theora ERROR: Unable to find video data in '%s'\n", lower_name)); goto Error; } // go ahead and do some audio stream error checking... if ( (movie->vinfo.channels < 0) || (movie->vinfo.channels > 2) ) { mprintf(("Theora ERROR: Unsupported number of audio channels!\n")); movie->vorbis_p = 0; } // if we don't have usable audio then close out it's partial setup and just use the video if ( !Sound_enabled || !movie->vorbis_p ) { vorbis_info_clear(&movie->vinfo); vorbis_comment_clear(&movie->vcomment); movie->vorbis_p = 0; } int ret; // we're expecting more header packets. while ( (movie->theora_p < 3) || (movie->vorbis_p && (movie->vorbis_p < 3)) ) { ret = ogg_stream_packetout(&movie->t_osstate, &movie->opacket); // look for further theora headers while ( (movie->theora_p < 3) && (ret) ){ if ( (ret < 0) || theora_decode_header(&movie->tinfo, &movie->tcomment, &movie->opacket) ) { mprintf(("Theora ERROR: Error parsing Theora stream headers on '%s'! Corrupt stream?\n", lower_name)); goto Error; } if (++movie->theora_p == 3) break; ret = ogg_stream_packetout(&movie->t_osstate, &movie->opacket); } ret = ogg_stream_packetout(&movie->v_osstate, &movie->opacket); // look for more vorbis header packets while ( movie->vorbis_p && (movie->vorbis_p < 3) && (ret) ) { if ( (ret < 0) || vorbis_synthesis_headerin(&movie->vinfo, &movie->vcomment, &movie->opacket) ) { mprintf(("Theora ERROR: Error parsing Vorbis stream headers on '%s'! Corrupt stream?\n", lower_name)); goto Error; } if (++movie->vorbis_p == 3) break; ret = ogg_stream_packetout(&movie->v_osstate, &movie->opacket); } // The header pages/packets will arrive before anything else we care about, or the stream is not obeying spec if ( ogg_sync_pageout(&movie->osyncstate, &movie->opage) > 0 ) { OGG_queue_page(movie); // demux into the appropriate stream } else { ret = OGG_buffer_data(movie); // someone needs more data if ( ret == 0 ) { mprintf(("Theora ERROR: End of file while searching for codec headers in '%s'!\n", lower_name)); goto Error; } } } // initialize video decoder theora_decode_init(&movie->tstate, &movie->tinfo); // and now for video stream error checking... if (movie->tinfo.pixelformat != OC_PF_420) { mprintf(("Theora ERROR: Only the yuv420p chroma is supported!\n")); goto Error; } if (movie->tinfo.offset_x || movie->tinfo.offset_y) { mprintf(("Theora ERROR: Player does not support frame offsets!\n")); goto Error; } // initialize audio decoder, if there is audio if (movie->vorbis_p) { vorbis_synthesis_init(&movie->vstate, &movie->vinfo); vorbis_block_init(&movie->vstate, &movie->vblock); } return movie; Error: theora_close(movie); return NULL; } // play that funky music... err, movie! void theora_play(THEORAFILE *movie) { int i, j; bool stateflag = false; bool videobuf_ready = false; bool audiobuf_ready = false; double last_time = 0.0; // for frame skipper if ( (movie == NULL) || !movie->theora_p ) return; // only OGL is supported if (gr_screen.mode != GR_OPENGL) return; // open audio if (movie->vorbis_p) OGG_audio_init(&movie->vinfo); // open video OGG_video_init(&movie->tinfo); // on to the main decode loop. We assume in this example that audio // and video start roughly together, and don't begin playback until // we have a start frame for both. This is not necessarily a valid // assumption in Ogg A/V streams! It will always be true of the // example_encoder (and most streams) though. while (playing) { // we want a video and audio frame ready to go at all times. If // we have to buffer incoming, buffer the compressed data (ie, let // ogg do the buffering) while ( movie->vorbis_p && !audiobuf_ready ) { int ret; float **pcm; // if there's pending, decoded audio, grab it if ( (ret = vorbis_synthesis_pcmout(&movie->vstate, &pcm)) > 0 ) { int count = (audiobuf_fill / 2); int maxsamples = (audiofd_fragsize - audiobuf_fill) / 2 / movie->vinfo.channels; for (i = 0; (i < ret) && (i < maxsamples); i++) { for (j = 0; j < movie->vinfo.channels; j++) { float val = pcm[j][i] * 32767.0f + 0.5f; if (val > 32767.0f) val = 32767.0f; if (val < -32768.0f) val = -32768.0f; audiobuf[count++] = (short)val; } } vorbis_synthesis_read(&movie->vstate, i); audiobuf_fill += (i * movie->vinfo.channels * 2); if (audiobuf_fill == audiofd_fragsize) audiobuf_ready = true; if (movie->vstate.granulepos >= 0) audiobuf_granulepos = movie->vstate.granulepos - ret + i; else audiobuf_granulepos += i; } else { // no pending audio; is there a pending packet to decode? if ( ogg_stream_packetout(&movie->v_osstate, &movie->opacket) > 0 ) { if ( vorbis_synthesis(&movie->vblock, &movie->opacket) == 0 ) vorbis_synthesis_blockin(&movie->vstate, &movie->vblock); } // we need more data; break out to suck in another page else { break; } } } while ( !videobuf_ready ) { // theora is one in, one out... if ( ogg_stream_packetout(&movie->t_osstate, &movie->opacket) <= 0 ) break; theora_decode_packetin(&movie->tstate, &movie->opacket); videobuf_time = theora_granule_time(&movie->tstate, movie->tstate.granulepos); double now_time = OGG_get_time(); double delay = videobuf_time - OGG_get_time(); // if we are running slow then skip the frame, otherwise to go into ready mode if ( (delay >= 0.0) || (now_time - last_time >= 1.0) ) videobuf_ready = true; } if ( !videobuf_ready && (movie->vorbis_p && !audiobuf_ready) && cfeof(movie->cfp) ) break; if ( !videobuf_ready || (movie->vorbis_p && !audiobuf_ready) ) { // no data yet for somebody. Grab another page OGG_buffer_data(movie); while (ogg_sync_pageout(&movie->osyncstate, &movie->opage) > 0) OGG_queue_page(movie); } // If playback has begun, top audio buffer off immediately. if ( stateflag ) OGG_audio_write(&movie->vinfo, &audiobuf_ready); // are we at or past time for this video frame? if ( stateflag && videobuf_ready && (videobuf_time <= OGG_get_time()) ) { OGG_video_draw(&movie->tstate); last_time = OGG_get_time(); videobuf_ready = false; } // if our buffers either don't exist or are ready to go, we can begin playback if ( videobuf_ready && (audiobuf_ready || !movie->vorbis_p) ) stateflag = true; // bail if we run into eof // if ( cfeof(movie->cfp) ) // break; if (videobuf_ready) OGG_timer_do_wait(); } // tear it all down OGG_audio_close(); OGG_video_close(); }