#include #include "quakedef.h" #include "client.h" #include "cap_ogg.h" // video capture cvars static cvar_t cl_capturevideo_ogg_theora_vp3compat = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_vp3compat", "1", "make VP3 compatible theora streams"}; static cvar_t cl_capturevideo_ogg_theora_quality = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_quality", "48", "video quality factor (0 to 63), or -1 to use bitrate only; higher is better; setting both to -1 achieves unlimited quality"}; static cvar_t cl_capturevideo_ogg_theora_bitrate = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_bitrate", "-1", "video bitrate (45 to 2000 kbps), or -1 to use quality only; higher is better; setting both to -1 achieves unlimited quality"}; static cvar_t cl_capturevideo_ogg_theora_keyframe_bitrate_multiplier = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_keyframe_bitrate_multiplier", "1.5", "how much more bit rate to use for keyframes, specified as a factor of at least 1"}; static cvar_t cl_capturevideo_ogg_theora_keyframe_maxinterval = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_keyframe_maxinterval", "64", "maximum keyframe interval (1 to 1000)"}; static cvar_t cl_capturevideo_ogg_theora_keyframe_mininterval = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_keyframe_mininterval", "8", "minimum keyframe interval (1 to 1000)"}; static cvar_t cl_capturevideo_ogg_theora_keyframe_auto_threshold = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_keyframe_auto_threshold", "80", "threshold for key frame decision (0 to 100)"}; static cvar_t cl_capturevideo_ogg_theora_noise_sensitivity = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_noise_sensitivity", "1", "video noise sensitivity (0 to 6); lower is better"}; static cvar_t cl_capturevideo_ogg_theora_sharpness = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_theora_sharpness", "0", "sharpness (0 to 2); lower is sharper"}; static cvar_t cl_capturevideo_ogg_vorbis_quality = {CF_CLIENT | CF_ARCHIVE, "cl_capturevideo_ogg_vorbis_quality", "3", "audio quality (-1 to 10); higher is better"}; typedef struct { long endbyte; int endbit; unsigned char *buffer; unsigned char *ptr; long storage; } oggpack_buffer; /* ogg_page is used to encapsulate the data in one Ogg bitstream page *****/ typedef struct { unsigned char *header; long header_len; unsigned char *body; long body_len; } ogg_page; /* ogg_stream_state contains the current encode/decode state of a logical Ogg bitstream **********************************************************/ typedef struct { unsigned char *body_data; /* bytes from packet bodies */ long body_storage; /* storage elements allocated */ long body_fill; /* elements stored; fill mark */ long body_returned; /* elements of fill returned */ int *lacing_vals; /* The values that will go to the segment table */ int64_t *granule_vals; /* granulepos values for headers. Not compact this way, but it is simple coupled to the lacing fifo */ long lacing_storage; long lacing_fill; long lacing_packet; long lacing_returned; unsigned char header[282]; /* working space for header encode */ int header_fill; int e_o_s; /* set when we have buffered the last packet in the logical bitstream */ int b_o_s; /* set after we've written the initial page of a logical bitstream */ long serialno; long pageno; int64_t packetno; /* sequence number for decode; the framing knows where there's a hole in the data, but we need coupling so that the codec (which is in a seperate abstraction layer) also knows about the gap */ int64_t granulepos; } ogg_stream_state; /* ogg_packet is used to encapsulate the data and metadata belonging to a single raw Ogg/Vorbis packet *************************************/ typedef struct { unsigned char *packet; long bytes; long b_o_s; long e_o_s; int64_t granulepos; int64_t packetno; /* sequence number for decode; the framing knows where there's a hole in the data, but we need coupling so that the codec (which is in a seperate abstraction layer) also knows about the gap */ } ogg_packet; typedef struct { unsigned char *data; int storage; int fill; int returned; int unsynced; int headerbytes; int bodybytes; } ogg_sync_state; /* Ogg BITSTREAM PRIMITIVES: encoding **************************/ static int (*qogg_stream_packetin) (ogg_stream_state *os, ogg_packet *op); static int (*qogg_stream_pageout) (ogg_stream_state *os, ogg_page *og); static int (*qogg_stream_flush) (ogg_stream_state *os, ogg_page *og); /* Ogg BITSTREAM PRIMITIVES: general ***************************/ static int (*qogg_stream_init) (ogg_stream_state *os,int serialno); static int (*qogg_stream_clear) (ogg_stream_state *os); static int64_t (*qogg_page_granulepos) (ogg_page *og); // end of ogg.h stuff // vorbis/codec.h stuff typedef struct vorbis_info{ int version; int channels; long rate; /* The below bitrate declarations are *hints*. Combinations of the three values carry the following implications: all three set to the same value: implies a fixed rate bitstream only nominal set: implies a VBR stream that averages the nominal bitrate. No hard upper/lower limit upper and or lower set: implies a VBR bitstream that obeys the bitrate limits. nominal may also be set to give a nominal rate. none set: the coder does not care to speculate. */ long bitrate_upper; long bitrate_nominal; long bitrate_lower; long bitrate_window; void *codec_setup; } vorbis_info; /* vorbis_dsp_state buffers the current vorbis audio analysis/synthesis state. The DSP state belongs to a specific logical bitstream ****************************************************/ typedef struct vorbis_dsp_state{ int analysisp; vorbis_info *vi; float **pcm; float **pcmret; int pcm_storage; int pcm_current; int pcm_returned; int preextrapolate; int eofflag; long lW; long W; long nW; long centerW; int64_t granulepos; int64_t sequence; int64_t glue_bits; int64_t time_bits; int64_t floor_bits; int64_t res_bits; void *backend_state; } vorbis_dsp_state; typedef struct vorbis_block{ /* necessary stream state for linking to the framing abstraction */ float **pcm; /* this is a pointer into local storage */ oggpack_buffer opb; long lW; long W; long nW; int pcmend; int mode; int eofflag; int64_t granulepos; int64_t sequence; vorbis_dsp_state *vd; /* For read-only access of configuration */ /* local storage to avoid remallocing; it's up to the mapping to structure it */ void *localstore; long localtop; long localalloc; long totaluse; struct alloc_chain *reap; /* bitmetrics for the frame */ long glue_bits; long time_bits; long floor_bits; long res_bits; void *internal; } vorbis_block; /* vorbis_block is a single block of data to be processed as part of the analysis/synthesis stream; it belongs to a specific logical bitstream, but is independant from other vorbis_blocks belonging to that logical bitstream. *************************************************/ struct alloc_chain{ void *ptr; struct alloc_chain *next; }; /* vorbis_info contains all the setup information specific to the specific compression/decompression mode in progress (eg, psychoacoustic settings, channel setup, options, codebook etc). vorbis_info and substructures are in backends.h. *********************************************************************/ /* the comments are not part of vorbis_info so that vorbis_info can be static storage */ typedef struct vorbis_comment{ /* unlimited user comment fields. libvorbis writes 'libvorbis' whatever vendor is set to in encode */ char **user_comments; int *comment_lengths; int comments; char *vendor; } vorbis_comment; /* libvorbis encodes in two abstraction layers; first we perform DSP and produce a packet (see docs/analysis.txt). The packet is then coded into a framed OggSquish bitstream by the second layer (see docs/framing.txt). Decode is the reverse process; we sync/frame the bitstream and extract individual packets, then decode the packet back into PCM audio. The extra framing/packetizing is used in streaming formats, such as files. Over the net (such as with UDP), the framing and packetization aren't necessary as they're provided by the transport and the streaming layer is not used */ /* Vorbis PRIMITIVES: general ***************************************/ static void (*qvorbis_info_init) (vorbis_info *vi); static void (*qvorbis_info_clear) (vorbis_info *vi); static void (*qvorbis_comment_init) (vorbis_comment *vc); static void (*qvorbis_comment_clear) (vorbis_comment *vc); static int (*qvorbis_block_init) (vorbis_dsp_state *v, vorbis_block *vb); static int (*qvorbis_block_clear) (vorbis_block *vb); static void (*qvorbis_dsp_clear) (vorbis_dsp_state *v); static double (*qvorbis_granule_time) (vorbis_dsp_state *v, int64_t granulepos); /* Vorbis PRIMITIVES: analysis/DSP layer ****************************/ static int (*qvorbis_analysis_init) (vorbis_dsp_state *v,vorbis_info *vi); static int (*qvorbis_commentheader_out) (vorbis_comment *vc, ogg_packet *op); static int (*qvorbis_analysis_headerout) (vorbis_dsp_state *v, vorbis_comment *vc, ogg_packet *op, ogg_packet *op_comm, ogg_packet *op_code); static float ** (*qvorbis_analysis_buffer) (vorbis_dsp_state *v,int vals); static int (*qvorbis_analysis_wrote) (vorbis_dsp_state *v,int vals); static int (*qvorbis_analysis_blockout) (vorbis_dsp_state *v,vorbis_block *vb); static int (*qvorbis_analysis) (vorbis_block *vb,ogg_packet *op); static int (*qvorbis_bitrate_addblock) (vorbis_block *vb); static int (*qvorbis_bitrate_flushpacket) (vorbis_dsp_state *vd, ogg_packet *op); // end of vorbis/codec.h stuff // vorbisenc.h stuff static int (*qvorbis_encode_init_vbr) (vorbis_info *vi, long channels, long rate, float base_quality /* quality level from 0. (lo) to 1. (hi) */ ); // end of vorbisenc.h stuff // theora.h stuff #define TH_ENCCTL_SET_VP3_COMPATIBLE (10) typedef struct { int y_width; /**< Width of the Y' luminance plane */ int y_height; /**< Height of the luminance plane */ int y_stride; /**< Offset in bytes between successive rows */ int uv_width; /**< Width of the Cb and Cr chroma planes */ int uv_height; /**< Height of the chroma planes */ int uv_stride; /**< Offset between successive chroma rows */ unsigned char *y; /**< Pointer to start of luminance data */ unsigned char *u; /**< Pointer to start of Cb data */ unsigned char *v; /**< Pointer to start of Cr data */ } yuv_buffer; /** * A Colorspace. */ typedef enum { OC_CS_UNSPECIFIED, /**< The colorspace is unknown or unspecified */ OC_CS_ITU_REC_470M, /**< This is the best option for 'NTSC' content */ OC_CS_ITU_REC_470BG, /**< This is the best option for 'PAL' content */ OC_CS_NSPACES /**< This marks the end of the defined colorspaces */ } theora_colorspace; /** * A Chroma subsampling * * These enumerate the available chroma subsampling options supported * by the theora format. See Section 4.4 of the specification for * exact definitions. */ typedef enum { OC_PF_420, /**< Chroma subsampling by 2 in each direction (4:2:0) */ OC_PF_RSVD, /**< Reserved value */ OC_PF_422, /**< Horizonatal chroma subsampling by 2 (4:2:2) */ OC_PF_444 /**< No chroma subsampling at all (4:4:4) */ } theora_pixelformat; /** * Theora bitstream info. * Contains the basic playback parameters for a stream, * corresponding to the initial 'info' header packet. * * Encoded theora frames must be a multiple of 16 in width and height. * To handle other frame sizes, a crop rectangle is specified in * frame_height and frame_width, offset_x and * offset_y. The offset * and size should still be a multiple of 2 to avoid chroma sampling * shifts. Offset values in this structure are measured from the * upper left of the image. * * Frame rate, in frames per second, is stored as a rational * fraction. Aspect ratio is also stored as a rational fraction, and * refers to the aspect ratio of the frame pixels, not of the * overall frame itself. * * See * examples/encoder_example.c for usage examples of the * other paramters and good default settings for the encoder parameters. */ typedef struct { uint32_t width; /**< encoded frame width */ uint32_t height; /**< encoded frame height */ uint32_t frame_width; /**< display frame width */ uint32_t frame_height; /**< display frame height */ uint32_t offset_x; /**< horizontal offset of the displayed frame */ uint32_t offset_y; /**< vertical offset of the displayed frame */ uint32_t fps_numerator; /**< frame rate numerator **/ uint32_t fps_denominator; /**< frame rate denominator **/ uint32_t aspect_numerator; /**< pixel aspect ratio numerator */ uint32_t aspect_denominator; /**< pixel aspect ratio denominator */ theora_colorspace colorspace; /**< colorspace */ int target_bitrate; /**< nominal bitrate in bits per second */ int quality; /**< Nominal quality setting, 0-63 */ int quick_p; /**< Quick encode/decode */ /* decode only */ unsigned char version_major; unsigned char version_minor; unsigned char version_subminor; void *codec_setup; /* encode only */ int dropframes_p; int keyframe_auto_p; uint32_t keyframe_frequency; uint32_t keyframe_frequency_force; /* also used for decode init to get granpos shift correct */ uint32_t keyframe_data_target_bitrate; int32_t keyframe_auto_threshold; uint32_t keyframe_mindistance; int32_t noise_sensitivity; int32_t sharpness; theora_pixelformat pixelformat; /**< chroma subsampling mode to expect */ } theora_info; /** Codec internal state and context. */ typedef struct{ theora_info *i; int64_t granulepos; void *internal_encode; void *internal_decode; } theora_state; /** * Comment header metadata. * * This structure holds the in-stream metadata corresponding to * the 'comment' header packet. * * Meta data is stored as a series of (tag, value) pairs, in * length-encoded string vectors. The first occurence of the * '=' character delimits the tag and value. A particular tag * may occur more than once. The character set encoding for * the strings is always UTF-8, but the tag names are limited * to case-insensitive ASCII. See the spec for details. * * In filling in this structure, qtheora_decode_header() will * null-terminate the user_comment strings for safety. However, * the bitstream format itself treats them as 8-bit clean, * and so the length array should be treated as authoritative * for their length. */ typedef struct theora_comment{ char **user_comments; /**< An array of comment string vectors */ int *comment_lengths; /**< An array of corresponding string vector lengths in bytes */ int comments; /**< The total number of comment string vectors */ char *vendor; /**< The vendor string identifying the encoder, null terminated */ } theora_comment; static int (*qtheora_encode_init) (theora_state *th, theora_info *ti); static int (*qtheora_encode_YUVin) (theora_state *t, yuv_buffer *yuv); static int (*qtheora_encode_packetout) ( theora_state *t, int last_p, ogg_packet *op); static int (*qtheora_encode_header) (theora_state *t, ogg_packet *op); static int (*qtheora_encode_comment) (theora_comment *tc, ogg_packet *op); static int (*qtheora_encode_tables) (theora_state *t, ogg_packet *op); static void (*qtheora_info_init) (theora_info *c); static void (*qtheora_info_clear) (theora_info *c); static void (*qtheora_clear) (theora_state *t); static void (*qtheora_comment_init) (theora_comment *tc); static void (*qtheora_comment_clear) (theora_comment *tc); static double (*qtheora_granule_time) (theora_state *th,int64_t granulepos); static int (*qtheora_control) (theora_state *th,int req,void *buf,size_t buf_sz); // end of theora.h stuff static dllfunction_t oggfuncs[] = { {"ogg_stream_packetin", (void **) &qogg_stream_packetin}, {"ogg_stream_pageout", (void **) &qogg_stream_pageout}, {"ogg_stream_flush", (void **) &qogg_stream_flush}, {"ogg_stream_init", (void **) &qogg_stream_init}, {"ogg_stream_clear", (void **) &qogg_stream_clear}, {"ogg_page_granulepos", (void **) &qogg_page_granulepos}, {NULL, NULL} }; static dllfunction_t vorbisencfuncs[] = { {"vorbis_encode_init_vbr", (void **) &qvorbis_encode_init_vbr}, {NULL, NULL} }; static dllfunction_t vorbisfuncs[] = { {"vorbis_info_init", (void **) &qvorbis_info_init}, {"vorbis_info_clear", (void **) &qvorbis_info_clear}, {"vorbis_comment_init", (void **) &qvorbis_comment_init}, {"vorbis_comment_clear", (void **) &qvorbis_comment_clear}, {"vorbis_block_init", (void **) &qvorbis_block_init}, {"vorbis_block_clear", (void **) &qvorbis_block_clear}, {"vorbis_dsp_clear", (void **) &qvorbis_dsp_clear}, {"vorbis_analysis_init", (void **) &qvorbis_analysis_init}, {"vorbis_commentheader_out", (void **) &qvorbis_commentheader_out}, {"vorbis_analysis_headerout", (void **) &qvorbis_analysis_headerout}, {"vorbis_analysis_buffer", (void **) &qvorbis_analysis_buffer}, {"vorbis_analysis_wrote", (void **) &qvorbis_analysis_wrote}, {"vorbis_analysis_blockout", (void **) &qvorbis_analysis_blockout}, {"vorbis_analysis", (void **) &qvorbis_analysis}, {"vorbis_bitrate_addblock", (void **) &qvorbis_bitrate_addblock}, {"vorbis_bitrate_flushpacket", (void **) &qvorbis_bitrate_flushpacket}, {"vorbis_granule_time", (void **) &qvorbis_granule_time}, {NULL, NULL} }; static dllfunction_t theorafuncs[] = { {"theora_info_init", (void **) &qtheora_info_init}, {"theora_info_clear", (void **) &qtheora_info_clear}, {"theora_comment_init", (void **) &qtheora_comment_init}, {"theora_comment_clear", (void **) &qtheora_comment_clear}, {"theora_encode_init", (void **) &qtheora_encode_init}, {"theora_encode_YUVin", (void **) &qtheora_encode_YUVin}, {"theora_encode_packetout", (void **) &qtheora_encode_packetout}, {"theora_encode_header", (void **) &qtheora_encode_header}, {"theora_encode_comment", (void **) &qtheora_encode_comment}, {"theora_encode_tables", (void **) &qtheora_encode_tables}, {"theora_clear", (void **) &qtheora_clear}, {"theora_granule_time", (void **) &qtheora_granule_time}, {"theora_control", (void **) &qtheora_control}, {NULL, NULL} }; static dllhandle_t og_dll = NULL, vo_dll = NULL, ve_dll = NULL, th_dll = NULL; static qbool SCR_CaptureVideo_Ogg_OpenLibrary(void) { const char* dllnames_og [] = { #if defined(WIN32) "libogg-0.dll", "libogg.dll", "ogg.dll", #elif defined(MACOSX) "libogg.dylib", #else "libogg.so.0", "libogg.so", #endif NULL }; const char* dllnames_vo [] = { #if defined(WIN32) "libvorbis-0.dll", "libvorbis.dll", "vorbis.dll", #elif defined(MACOSX) "libvorbis.dylib", #else "libvorbis.so.0", "libvorbis.so", #endif NULL }; const char* dllnames_ve [] = { #if defined(WIN32) "libvorbisenc-2.dll", "libvorbisenc.dll", "vorbisenc.dll", #elif defined(MACOSX) "libvorbisenc.dylib", #else "libvorbisenc.so.2", "libvorbisenc.so", #endif NULL }; const char* dllnames_th [] = { #if defined(WIN32) "libtheora-0.dll", "libtheora.dll", "theora.dll", #elif defined(MACOSX) "libtheora.dylib", #else "libtheora.so.0", "libtheora.so", #endif NULL }; return Sys_LoadDependency (dllnames_og, &og_dll, oggfuncs) && Sys_LoadDependency (dllnames_th, &th_dll, theorafuncs) && Sys_LoadDependency (dllnames_vo, &vo_dll, vorbisfuncs) && Sys_LoadDependency (dllnames_ve, &ve_dll, vorbisencfuncs); } void SCR_CaptureVideo_Ogg_Init(void) { SCR_CaptureVideo_Ogg_OpenLibrary(); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_vp3compat); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_quality); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_bitrate); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_keyframe_bitrate_multiplier); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_keyframe_maxinterval); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_keyframe_mininterval); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_keyframe_auto_threshold); Cvar_RegisterVariable(&cl_capturevideo_ogg_theora_noise_sensitivity); Cvar_RegisterVariable(&cl_capturevideo_ogg_vorbis_quality); } qbool SCR_CaptureVideo_Ogg_Available(void) { return og_dll && th_dll && vo_dll && ve_dll; } void SCR_CaptureVideo_Ogg_CloseDLL(void) { Sys_FreeLibrary (&ve_dll); Sys_FreeLibrary (&vo_dll); Sys_FreeLibrary (&th_dll); Sys_FreeLibrary (&og_dll); } // this struct should not be needed // however, libogg appears to pull the ogg_page's data element away from our // feet before we get to write the data due to interleaving // so this struct is used to keep the page data around until it actually gets // written typedef struct allocatedoggpage_s { size_t len; double time; unsigned char data[65307]; // this number is from RFC 3533. In case libogg writes more, we'll have to increase this // but we'll get a Host_Error in this case so we can track it down } allocatedoggpage_t; typedef struct capturevideostate_ogg_formatspecific_s { ogg_stream_state to, vo; int serial1, serial2; theora_state ts; vorbis_dsp_state vd; vorbis_block vb; vorbis_info vi; yuv_buffer yuv[2]; int yuvi; int lastnum; int channels; allocatedoggpage_t videopage, audiopage; } capturevideostate_ogg_formatspecific_t; #define LOAD_FORMATSPECIFIC_OGG() capturevideostate_ogg_formatspecific_t *format = (capturevideostate_ogg_formatspecific_t *) cls.capturevideo.formatspecific static void SCR_CaptureVideo_Ogg_Interleave(void) { LOAD_FORMATSPECIFIC_OGG(); ogg_page pg; if(!cls.capturevideo.soundrate) { while(qogg_stream_pageout(&format->to, &pg) > 0) { FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); } return; } for(;;) { // first: make sure we have a page of both types if(!format->videopage.len) if(qogg_stream_pageout(&format->to, &pg) > 0) { format->videopage.len = pg.header_len + pg.body_len; format->videopage.time = qtheora_granule_time(&format->ts, qogg_page_granulepos(&pg)); if(format->videopage.len > sizeof(format->videopage.data)) Sys_Error("video page too long"); memcpy(format->videopage.data, pg.header, pg.header_len); memcpy(format->videopage.data + pg.header_len, pg.body, pg.body_len); } if(!format->audiopage.len) if(qogg_stream_pageout(&format->vo, &pg) > 0) { format->audiopage.len = pg.header_len + pg.body_len; format->audiopage.time = qvorbis_granule_time(&format->vd, qogg_page_granulepos(&pg)); if(format->audiopage.len > sizeof(format->audiopage.data)) Sys_Error("audio page too long"); memcpy(format->audiopage.data, pg.header, pg.header_len); memcpy(format->audiopage.data + pg.header_len, pg.body, pg.body_len); } if(format->videopage.len && format->audiopage.len) { // output the page that ends first if(format->videopage.time < format->audiopage.time) { FS_Write(cls.capturevideo.videofile, format->videopage.data, format->videopage.len); format->videopage.len = 0; } else { FS_Write(cls.capturevideo.videofile, format->audiopage.data, format->audiopage.len); format->audiopage.len = 0; } } else break; } } static void SCR_CaptureVideo_Ogg_FlushInterleaving(void) { LOAD_FORMATSPECIFIC_OGG(); if(cls.capturevideo.soundrate) if(format->audiopage.len) { FS_Write(cls.capturevideo.videofile, format->audiopage.data, format->audiopage.len); format->audiopage.len = 0; } if(format->videopage.len) { FS_Write(cls.capturevideo.videofile, format->videopage.data, format->videopage.len); format->videopage.len = 0; } } static void SCR_CaptureVideo_Ogg_EndVideo(void) { LOAD_FORMATSPECIFIC_OGG(); ogg_page pg; ogg_packet pt; if(format->yuvi >= 0) { // send the previous (and last) frame while(format->lastnum-- > 0) { qtheora_encode_YUVin(&format->ts, &format->yuv[format->yuvi]); while(qtheora_encode_packetout(&format->ts, !format->lastnum, &pt)) qogg_stream_packetin(&format->to, &pt); SCR_CaptureVideo_Ogg_Interleave(); } } if(cls.capturevideo.soundrate) { qvorbis_analysis_wrote(&format->vd, 0); while(qvorbis_analysis_blockout(&format->vd, &format->vb) == 1) { qvorbis_analysis(&format->vb, NULL); qvorbis_bitrate_addblock(&format->vb); while(qvorbis_bitrate_flushpacket(&format->vd, &pt)) qogg_stream_packetin(&format->vo, &pt); SCR_CaptureVideo_Ogg_Interleave(); } } SCR_CaptureVideo_Ogg_FlushInterleaving(); while(qogg_stream_pageout(&format->to, &pg) > 0) { FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); } if(cls.capturevideo.soundrate) { while(qogg_stream_pageout(&format->vo, &pg) > 0) { FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); } } while (1) { int result = qogg_stream_flush (&format->to, &pg); if (result < 0) fprintf (stderr, "Internal Ogg library error.\n"); // TODO Sys_Error if (result <= 0) break; FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); } if(cls.capturevideo.soundrate) { while (1) { int result = qogg_stream_flush (&format->vo, &pg); if (result < 0) fprintf (stderr, "Internal Ogg library error.\n"); // TODO Sys_Error if (result <= 0) break; FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); } qogg_stream_clear(&format->vo); qvorbis_block_clear(&format->vb); qvorbis_dsp_clear(&format->vd); } qogg_stream_clear(&format->to); qtheora_clear(&format->ts); qvorbis_info_clear(&format->vi); Mem_Free(format->yuv[0].y); Mem_Free(format->yuv[0].u); Mem_Free(format->yuv[0].v); Mem_Free(format->yuv[1].y); Mem_Free(format->yuv[1].u); Mem_Free(format->yuv[1].v); Mem_Free(format); FS_Close(cls.capturevideo.videofile); cls.capturevideo.videofile = NULL; } static void SCR_CaptureVideo_Ogg_ConvertFrame_BGRA_to_YUV(void) { LOAD_FORMATSPECIFIC_OGG(); yuv_buffer *yuv; int x, y; int blockr, blockg, blockb; unsigned char *b; int w = cls.capturevideo.width; int h = cls.capturevideo.height; int inpitch = w*4; yuv = &format->yuv[format->yuvi]; for(y = 0; y < h; ++y) { for(b = cls.capturevideo.outbuffer + (h-1-y)*w*4, x = 0; x < w; ++x) { blockr = b[2]; blockg = b[1]; blockb = b[0]; yuv->y[x + yuv->y_stride * y] = cls.capturevideo.yuvnormalizetable[0][cls.capturevideo.rgbtoyuvscaletable[0][0][blockr] + cls.capturevideo.rgbtoyuvscaletable[0][1][blockg] + cls.capturevideo.rgbtoyuvscaletable[0][2][blockb]]; b += 4; } if ((y & 1) == 0 && y/2 < h/2) // if h is odd, this skips the last row { for(b = cls.capturevideo.outbuffer + (h-2-y)*w*4, x = 0; x < w/2; ++x) { blockr = (b[2] + b[6] + b[inpitch+2] + b[inpitch+6]) >> 2; blockg = (b[1] + b[5] + b[inpitch+1] + b[inpitch+5]) >> 2; blockb = (b[0] + b[4] + b[inpitch+0] + b[inpitch+4]) >> 2; yuv->u[x + yuv->uv_stride * (y/2)] = cls.capturevideo.yuvnormalizetable[1][cls.capturevideo.rgbtoyuvscaletable[1][0][blockr] + cls.capturevideo.rgbtoyuvscaletable[1][1][blockg] + cls.capturevideo.rgbtoyuvscaletable[1][2][blockb] + 128]; yuv->v[x + yuv->uv_stride * (y/2)] = cls.capturevideo.yuvnormalizetable[2][cls.capturevideo.rgbtoyuvscaletable[2][0][blockr] + cls.capturevideo.rgbtoyuvscaletable[2][1][blockg] + cls.capturevideo.rgbtoyuvscaletable[2][2][blockb] + 128]; b += 8; } } } } static void SCR_CaptureVideo_Ogg_VideoFrames(int num) { LOAD_FORMATSPECIFIC_OGG(); ogg_packet pt; // data is in cls.capturevideo.outbuffer as BGRA and has size width*height if(format->yuvi >= 0) { // send the previous frame while(format->lastnum-- > 0) { qtheora_encode_YUVin(&format->ts, &format->yuv[format->yuvi]); while(qtheora_encode_packetout(&format->ts, false, &pt)) qogg_stream_packetin(&format->to, &pt); SCR_CaptureVideo_Ogg_Interleave(); } } format->yuvi = (format->yuvi + 1) % 2; SCR_CaptureVideo_Ogg_ConvertFrame_BGRA_to_YUV(); format->lastnum = num; // TODO maybe send num-1 frames from here already } typedef int channelmapping_t[8]; channelmapping_t mapping[8] = { { 0, -1, -1, -1, -1, -1, -1, -1 }, // mono { 0, 1, -1, -1, -1, -1, -1, -1 }, // stereo { 0, 1, 2, -1, -1, -1, -1, -1 }, // L C R { 0, 1, 2, 3, -1, -1, -1, -1 }, // surround40 { 0, 2, 3, 4, 1, -1, -1, -1 }, // FL FC FR RL RR { 0, 2, 3, 4, 1, 5, -1, -1 }, // surround51 { 0, 2, 3, 4, 1, 5, 6, -1 }, // (not defined by vorbis spec) { 0, 2, 3, 4, 1, 5, 6, 7 } // surround71 (not defined by vorbis spec) }; static void SCR_CaptureVideo_Ogg_SoundFrame(const portable_sampleframe_t *paintbuffer, size_t length) { LOAD_FORMATSPECIFIC_OGG(); float **vorbis_buffer; size_t i; int j; ogg_packet pt; int *map = mapping[bound(1, cls.capturevideo.soundchannels, 8) - 1]; vorbis_buffer = qvorbis_analysis_buffer(&format->vd, (int)length); for(j = 0; j < cls.capturevideo.soundchannels; ++j) { float *b = vorbis_buffer[map[j]]; for(i = 0; i < length; ++i) b[i] = paintbuffer[i].sample[j]; } qvorbis_analysis_wrote(&format->vd, (int)length); while(qvorbis_analysis_blockout(&format->vd, &format->vb) == 1) { qvorbis_analysis(&format->vb, NULL); qvorbis_bitrate_addblock(&format->vb); while(qvorbis_bitrate_flushpacket(&format->vd, &pt)) qogg_stream_packetin(&format->vo, &pt); } SCR_CaptureVideo_Ogg_Interleave(); } void SCR_CaptureVideo_Ogg_BeginVideo(void) { char vabuf[1024]; cls.capturevideo.format = CAPTUREVIDEOFORMAT_OGG_VORBIS_THEORA; cls.capturevideo.formatextension = "ogv"; cls.capturevideo.videofile = FS_OpenRealFile(va(vabuf, sizeof(vabuf), "%s.%s", cls.capturevideo.basename, cls.capturevideo.formatextension), "wb", false); cls.capturevideo.endvideo = SCR_CaptureVideo_Ogg_EndVideo; cls.capturevideo.videoframes = SCR_CaptureVideo_Ogg_VideoFrames; cls.capturevideo.soundframe = SCR_CaptureVideo_Ogg_SoundFrame; cls.capturevideo.formatspecific = Mem_Alloc(tempmempool, sizeof(capturevideostate_ogg_formatspecific_t)); { LOAD_FORMATSPECIFIC_OGG(); int num, denom, i; ogg_page pg; ogg_packet pt, pt2, pt3; theora_comment tc; vorbis_comment vc; theora_info ti; int vp3compat; format->serial1 = rand(); qogg_stream_init(&format->to, format->serial1); if(cls.capturevideo.soundrate) { do { format->serial2 = rand(); } while(format->serial1 == format->serial2); qogg_stream_init(&format->vo, format->serial2); } format->videopage.len = format->audiopage.len = 0; qtheora_info_init(&ti); ti.frame_width = cls.capturevideo.width; ti.frame_height = cls.capturevideo.height; ti.width = (ti.frame_width + 15) & ~15; ti.height = (ti.frame_height + 15) & ~15; //ti.offset_x = ((ti.width - ti.frame_width) / 2) & ~1; //ti.offset_y = ((ti.height - ti.frame_height) / 2) & ~1; for(i = 0; i < 2; ++i) { format->yuv[i].y_width = ti.width; format->yuv[i].y_height = ti.height; format->yuv[i].y_stride = ti.width; format->yuv[i].uv_width = ti.width / 2; format->yuv[i].uv_height = ti.height / 2; format->yuv[i].uv_stride = ti.width / 2; format->yuv[i].y = (unsigned char *) Mem_Alloc(tempmempool, format->yuv[i].y_stride * format->yuv[i].y_height); format->yuv[i].u = (unsigned char *) Mem_Alloc(tempmempool, format->yuv[i].uv_stride * format->yuv[i].uv_height); format->yuv[i].v = (unsigned char *) Mem_Alloc(tempmempool, format->yuv[i].uv_stride * format->yuv[i].uv_height); } format->yuvi = -1; // -1: no frame valid yet, write into 0 FindFraction(cls.capturevideo.framerate / cls.capturevideo.framestep, &num, &denom, 1001); ti.fps_numerator = num; ti.fps_denominator = denom; FindFraction(1 / vid_pixelheight.value, &num, &denom, 1000); ti.aspect_numerator = num; ti.aspect_denominator = denom; ti.colorspace = OC_CS_UNSPECIFIED; ti.pixelformat = OC_PF_420; ti.quick_p = true; // http://mlblog.osdir.com/multimedia.ogg.theora.general/2004-07/index.shtml ti.dropframes_p = false; ti.target_bitrate = cl_capturevideo_ogg_theora_bitrate.integer * 1000; ti.quality = cl_capturevideo_ogg_theora_quality.integer; if(ti.target_bitrate <= 0) { ti.target_bitrate = -1; ti.keyframe_data_target_bitrate = (unsigned int)-1; } else { ti.keyframe_data_target_bitrate = (int) (ti.target_bitrate * max(1, cl_capturevideo_ogg_theora_keyframe_bitrate_multiplier.value)); if(ti.target_bitrate < 45000 || ti.target_bitrate > 2000000) Con_DPrintf("WARNING: requesting an odd bitrate for theora (sensible values range from 45 to 2000 kbps)\n"); } if(ti.quality < 0 || ti.quality > 63) { ti.quality = 63; if(ti.target_bitrate <= 0) { ti.target_bitrate = 0x7FFFFFFF; ti.keyframe_data_target_bitrate = 0x7FFFFFFF; } } // this -1 magic is because ti.keyframe_frequency and ti.keyframe_mindistance use different metrics ti.keyframe_frequency = bound(1, cl_capturevideo_ogg_theora_keyframe_maxinterval.integer, 1000); ti.keyframe_mindistance = bound(1, cl_capturevideo_ogg_theora_keyframe_mininterval.integer, (int) ti.keyframe_frequency) - 1; ti.noise_sensitivity = bound(0, cl_capturevideo_ogg_theora_noise_sensitivity.integer, 6); ti.sharpness = bound(0, cl_capturevideo_ogg_theora_sharpness.integer, 2); ti.keyframe_auto_threshold = bound(0, cl_capturevideo_ogg_theora_keyframe_auto_threshold.integer, 100); ti.keyframe_frequency_force = ti.keyframe_frequency; ti.keyframe_auto_p = (ti.keyframe_frequency != ti.keyframe_mindistance + 1); qtheora_encode_init(&format->ts, &ti); qtheora_info_clear(&ti); if(cl_capturevideo_ogg_theora_vp3compat.integer) { vp3compat = 1; qtheora_control(&format->ts, TH_ENCCTL_SET_VP3_COMPATIBLE, &vp3compat, sizeof(vp3compat)); if(!vp3compat) Con_DPrintf("Warning: theora stream is not fully VP3 compatible\n"); } // vorbis? if(cls.capturevideo.soundrate) { qvorbis_info_init(&format->vi); qvorbis_encode_init_vbr(&format->vi, cls.capturevideo.soundchannels, cls.capturevideo.soundrate, bound(-1, cl_capturevideo_ogg_vorbis_quality.value, 10) * 0.099); qvorbis_comment_init(&vc); qvorbis_analysis_init(&format->vd, &format->vi); qvorbis_block_init(&format->vd, &format->vb); } qtheora_comment_init(&tc); /* create the remaining theora headers */ qtheora_encode_header(&format->ts, &pt); qogg_stream_packetin(&format->to, &pt); if (qogg_stream_pageout (&format->to, &pg) != 1) fprintf (stderr, "Internal Ogg library error.\n"); FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); qtheora_encode_comment(&tc, &pt); qogg_stream_packetin(&format->to, &pt); qtheora_encode_tables(&format->ts, &pt); qogg_stream_packetin (&format->to, &pt); qtheora_comment_clear(&tc); if(cls.capturevideo.soundrate) { qvorbis_analysis_headerout(&format->vd, &vc, &pt, &pt2, &pt3); qogg_stream_packetin(&format->vo, &pt); if (qogg_stream_pageout (&format->vo, &pg) != 1) fprintf (stderr, "Internal Ogg library error.\n"); FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); qogg_stream_packetin(&format->vo, &pt2); qogg_stream_packetin(&format->vo, &pt3); qvorbis_comment_clear(&vc); } for(;;) { int result = qogg_stream_flush (&format->to, &pg); if (result < 0) fprintf (stderr, "Internal Ogg library error.\n"); // TODO Sys_Error if (result <= 0) break; FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); } if(cls.capturevideo.soundrate) for(;;) { int result = qogg_stream_flush (&format->vo, &pg); if (result < 0) fprintf (stderr, "Internal Ogg library error.\n"); // TODO Sys_Error if (result <= 0) break; FS_Write(cls.capturevideo.videofile, pg.header, pg.header_len); FS_Write(cls.capturevideo.videofile, pg.body, pg.body_len); } } }