diff --git a/common/render_api.h b/common/render_api.h index 8ab2643c..4dd88962 100644 --- a/common/render_api.h +++ b/common/render_api.h @@ -157,6 +157,21 @@ typedef struct decallist_s modelstate_t studio_state; // studio decals only } decallist_t; +enum movie_parms_e +{ + AVI_PARM_LAST = 0, // marker for SetParm to end parse parsing arguments + AVI_RENDER_TEXNUM, // (int) sets texture to draw into, if 0 will draw to screen + AVI_RENDER_X, // (int) when set to screen, sets position where to draw + AVI_RENDER_Y, + AVI_RENDER_W, // (int) sets texture or screen width + AVI_RENDER_H, // set to -1 to draw full screen + AVI_REWIND, // no argument, rewind playback to the beginning + AVI_ENTNUM, // (int) entity number, -1 for no spatialization + AVI_VOLUME, // (int) volume from 0 to 255 + AVI_ATTN, // (float) attenuation value +}; + +struct movie_state_s; struct ref_viewpass_s; typedef struct render_api_s @@ -201,8 +216,8 @@ typedef struct render_api_s void (*AVI_FreeVideo)( void *Avi ); int (*AVI_IsActive)( void *Avi ); void (*AVI_StreamSound)( void *Avi, int entnum, float fvol, float attn, float synctime ); - void (*AVI_Reserved0)( void ); // for potential interface expansion without broken compatibility - void (*AVI_Reserved1)( void ); + qboolean (*AVI_Think)( struct movie_state_s *Avi ); + qboolean (*AVI_SetParm)( struct movie_state_s *Avi, enum movie_parms_e parm, ... ); // glState related calls (must use this instead of normal gl-calls to prevent de-synchornize local states between engine and the client) void (*GL_Bind)( int tmu, unsigned int texnum ); diff --git a/engine/client/avi/avi.h b/engine/client/avi/avi.h index 6bc1c7ad..9adc504a 100644 --- a/engine/client/avi/avi.h +++ b/engine/client/avi/avi.h @@ -34,4 +34,7 @@ movie_state_t *AVI_GetState( int num ); qboolean AVI_Initailize( void ); void AVI_Shutdown( void ); +qboolean AVI_SetParm( movie_state_t *Avi, enum movie_parms_e parm, ... ); +qboolean AVI_Think( movie_state_t *Avi ); + #endif // AVI_H diff --git a/engine/client/avi/avi_ffmpeg.c b/engine/client/avi/avi_ffmpeg.c new file mode 100644 index 00000000..e5ea9329 --- /dev/null +++ b/engine/client/avi/avi_ffmpeg.c @@ -0,0 +1,692 @@ +/* +avi_ffmpreg.c - playing AVI files (ffmpeg backend) +Copyright (C) 2023 Alibek Omarov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "defaults.h" +#if XASH_AVI == AVI_FFMPEG +#include "common.h" +#include "client.h" +#include +#include +#include +#include +#include + +struct movie_state_s +{ + // ffmpeg contexts + AVFormatContext *fmt_ctx; + AVCodecContext *video_ctx; + AVCodecContext *audio_ctx; + struct SwsContext *sws_ctx; + struct SwrContext *swr_ctx; + + AVPacket *pkt; + AVFrame *aframe; + AVFrame *vframe; + AVFrame *vframe_copy; + + int64_t first_time; + int64_t last_time; + + // video stream + byte *dst; + double duration; + int video_stream; + int xres; + int yres; + int dst_linesize; + enum AVPixelFormat pix_fmt; + + // rendering video parameters + int x, y, w, h; // passed to R_DrawStretchRaw + int texture; // passed to R_UploadStretchRaw + + // audio stream + int audio_stream; + int channels; + int rate; + enum AVSampleFormat s_fmt; + + byte *cached_audio; + size_t cached_audio_buf_len; // absolute size of cached_audio array + size_t cached_audio_len; // how many data in bytes we have in cached_audio array + size_t cached_audio_pos; // how far we've read into cached_audio array + + // rendering audio parameters + float attn; + int16_t entnum; // MAX_ENTITY_BITS is 13 + byte volume; + byte active : 1; + byte quiet : 1; +}; + +static qboolean avi_initialized; +static poolhandle_t avi_mempool; +static movie_state_t avi[2]; + +qboolean AVI_SetParm( movie_state_t *Avi, enum movie_parms_e parm, ... ) +{ + qboolean ret = true; + va_list va; + va_start( va, parm ); + + while( parm != AVI_PARM_LAST ) + { + float fval; + int val; + + switch( parm ) + { + case AVI_RENDER_TEXNUM: + Avi->texture = va_arg( va, int ); + break; + case AVI_RENDER_X: + Avi->x = va_arg( va, int ); + break; + case AVI_RENDER_Y: + Avi->y = va_arg( va, int ); + break; + case AVI_RENDER_W: + Avi->w = va_arg( va, int ); + break; + case AVI_RENDER_H: + Avi->h = va_arg( va, int ); + break; + case AVI_REWIND: + if( Avi->audio_ctx ) + avcodec_flush_buffers( Avi->audio_ctx ); + avcodec_flush_buffers( Avi->video_ctx ); + Avi->cached_audio_len = Avi->cached_audio_pos = 0; + Avi->last_time = -1; + Avi->first_time = 0; + av_seek_frame( Avi->fmt_ctx, -1, 0, AVSEEK_FLAG_FRAME | AVSEEK_FLAG_BACKWARD ); + break; + case AVI_ENTNUM: + val = va_arg( va, int ); + Avi->entnum = bound( 0, val, MAX_EDICTS ); + break; + case AVI_VOLUME: + val = va_arg( va, int ); + Avi->volume = bound( 0, val, 255 ); + break; + case AVI_ATTN: + fval = va_arg( va, double ); + Avi->attn = Q_max( 0.0f, fval ); + break; + default: + ret = false; + } + + parm = va_arg( va, enum movie_parms_e ); + } + + va_end( va ); + + return ret; +} + +static void AVI_SpewError( qboolean quiet, const char *fmt, ... ) FORMAT_CHECK( 2 ); +static void AVI_SpewError( qboolean quiet, const char *fmt, ... ) +{ + char buf[MAX_VA_STRING]; + va_list va; + + if( quiet ) + return; + + va_start( va, fmt ); + Q_vsnprintf( buf, sizeof( buf ), fmt, va ); + va_end( va ); + + Con_Printf( S_ERROR "%s", buf ); +} + +static void AVI_SpewAvError( qboolean quiet, const char *func, int numerr ) +{ + if( !quiet ) + Con_Printf( S_ERROR "%s: %s (%d)\n", func, av_err2str( numerr ), numerr ); +} + +static int AVI_OpenCodecContext( AVCodecContext **dst_dec_ctx, AVFormatContext *fmt_ctx, enum AVMediaType type, qboolean quiet ) +{ + const AVCodec *dec; + AVCodecContext *dec_ctx; + AVStream *st; + int idx, ret; + + if(( ret = av_find_best_stream( fmt_ctx, type, -1, -1, NULL, 0 )) < 0 ) + { + AVI_SpewAvError( quiet, "av_find_best_stream", ret ); + return ret; + } + + idx = ret; + st = fmt_ctx->streams[idx]; + + if( !( dec = avcodec_find_decoder( st->codecpar->codec_id ))) + { + AVI_SpewError( quiet, S_ERROR "Failed to find %s codec\n", av_get_media_type_string( type )); + return AVERROR( EINVAL ); + } + + if( !( dec_ctx = avcodec_alloc_context3( dec ))) + { + AVI_SpewError( quiet, S_ERROR "Failed to allocate %s codec context", dec->name ); + return AVERROR( ENOMEM ); + } + + if(( ret = avcodec_parameters_to_context( dec_ctx, st->codecpar )) < 0 ) + { + AVI_SpewAvError( quiet, "avcodec_parameters_to_context", ret ); + avcodec_free_context( &dec_ctx ); + return ret; + } + + dec_ctx->pkt_timebase = st->time_base; + + if(( ret = avcodec_open2( dec_ctx, dec, NULL )) < 0 ) + { + AVI_SpewAvError( quiet, "avcodec_open2", ret ); + + avcodec_free_context( &dec_ctx ); + return ret; + } + + *dst_dec_ctx = dec_ctx; + return idx; // always positive +} + +int AVI_GetVideoFrameNumber( movie_state_t *Avi, float time ) +{ + return 0; +} + +int AVI_TimeToSoundPosition( movie_state_t *Avi, int time ) +{ + return 0; +} + +qboolean AVI_GetVideoInfo( movie_state_t *Avi, int *xres, int *yres, float *duration ) +{ + if( !Avi->active ) + return false; + + if( xres ) + *xres = Avi->xres; + + if( yres ) + *yres = Avi->yres; + + if( duration ) + *duration = Avi->duration; + + return true; +} + +qboolean AVI_GetAudioInfo( movie_state_t *Avi, wavdata_t *snd_info ) +{ + if( !Avi->active || Avi->audio_stream < 0 ) + return false; + + snd_info->rate = Avi->rate; + snd_info->channels = Avi->channels; + snd_info->width = av_get_bytes_per_sample( Avi->s_fmt ); + snd_info->size = (size_t)snd_info->rate * snd_info->width * snd_info->channels; + snd_info->loopStart = 0; + + return true; +} + +// just let it compile, bruh! +byte *AVI_GetVideoFrame( movie_state_t *Avi, int target ) +{ + return Avi->dst; +} + +int AVI_GetAudioChunk( movie_state_t *Avi, char *audiodata, int offset, int length ) +{ + return 0; +} + +static void AVI_StreamAudio( movie_state_t *Avi ) +{ + int buffer_samples, file_samples, file_bytes; + rawchan_t *ch = NULL; + + // keep the same semantics, when S_RAW_SOUND_SOUNDTRACK doesn't play if S_SetStream wasn't enabled + qboolean disable_stream = Avi->entnum == S_RAW_SOUND_SOUNDTRACK ? !s_listener.streaming : false; + + if( !dma.initialized || disable_stream || s_listener.paused || !Avi->cached_audio ) + return; + + ch = S_FindRawChannel( Avi->entnum, true ); + + if( !ch ) + return; + + ch->master_vol = Avi->volume; + ch->dist_mult = (Avi->attn / SND_CLIP_DISTANCE); + + if( ch->s_rawend < soundtime ) + ch->s_rawend = soundtime; + + while( ch->s_rawend < soundtime + ch->max_samples ) + { + size_t copy; + + buffer_samples = ch->max_samples - (ch->s_rawend - soundtime); + + file_samples = buffer_samples * ((float)Avi->rate / SOUND_DMA_SPEED); + if( file_samples <= 1 ) return; // no more samples need + + file_bytes = file_samples * av_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels; + + if( file_bytes > ch->max_samples ) + { + file_bytes = ch->max_samples; + file_samples = file_bytes / ( av_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels ); + } + + copy = Q_min( file_bytes, Q_max( Avi->cached_audio_len - Avi->cached_audio_pos, 0 )); + + if( !copy ) + break; + + if( file_bytes > copy ) + { + file_bytes = copy; + file_samples = file_bytes / ( av_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels ); + } + + ch->s_rawend = S_RawSamplesStereo( ch->rawsamples, ch->s_rawend, ch->max_samples, file_samples, Avi->rate, av_get_bytes_per_sample( Avi->s_fmt ), Avi->channels, Avi->cached_audio + Avi->cached_audio_pos ); + Avi->cached_audio_pos += copy; + } +} + +static void AVI_HandleAudio( movie_state_t *Avi, const AVFrame *frame ) +{ + int samples = frame->nb_samples; + size_t len = samples * av_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels; + int outsamples; + uint8_t *ptr; + + // allocate data + if( !Avi->cached_audio ) + { + Avi->cached_audio_buf_len = len; + Avi->cached_audio_pos = 0; + Avi->cached_audio_len = 0; + Avi->cached_audio = Mem_Malloc( avi_mempool, len ); + } + else + { + if( Avi->cached_audio_pos ) + { + // Con_Printf( "%s: erasing old data of size %d\n", __func__, Avi->cached_audio_pos ); + Avi->cached_audio_len -= Avi->cached_audio_pos; + memmove( Avi->cached_audio, Avi->cached_audio + Avi->cached_audio_pos, Avi->cached_audio_len ); + Avi->cached_audio_pos = 0; + } + + if( len + Avi->cached_audio_len > Avi->cached_audio_buf_len ) + { + // Con_Printf( "%s: resizing old buffer of size %d to size %d\n", __func__, Avi->cached_audio_buf_len, len + Avi->cached_audio_buf_len ); + Avi->cached_audio_buf_len = len + Avi->cached_audio_len; + Avi->cached_audio = Mem_Realloc( avi_mempool, Avi->cached_audio, Avi->cached_audio_buf_len ); + } + } + + ptr = Avi->cached_audio + Avi->cached_audio_len; + outsamples = swr_convert( Avi->swr_ctx, &ptr, samples, (void *)frame->data, samples ); + Avi->cached_audio_len += outsamples * av_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels; + + // Con_Printf( "%s: got audio chunk of size %d samples\n", __func__, outsamples ); +} + +qboolean AVI_Think( movie_state_t *Avi ) +{ + qboolean decoded = false; + qboolean flushing = false; + qboolean redraw = false; + const double timebase = (double)Avi->video_ctx->pkt_timebase.den / Avi->video_ctx->pkt_timebase.num; + int64_t curtime = round( Platform_DoubleTime() * timebase ); + + if( !Avi->first_time ) // always remember at which timestamp we started playing + Avi->first_time = curtime; + + Con_NPrintf( 1, "cached_audio_buf_len = %zu", Avi->cached_audio_buf_len ); + + while( 1 ) // try to get multiple decoded frames to keep up when we're running at low fps + { + int res; + + AVI_StreamAudio( Avi ); // always flush audio buffers + + // recalc time so we always play last possible frame + curtime = round( Platform_DoubleTime() * timebase ); + + if( Avi->last_time > curtime ) + break; + + if(( res = av_read_frame( Avi->fmt_ctx, Avi->pkt )) >= 0 ) + { + if( Avi->pkt->stream_index == Avi->audio_stream ) + { + res = avcodec_send_packet( Avi->audio_ctx, Avi->pkt ); + if( res < 0 ) + AVI_SpewAvError( Avi->quiet, "avcodec_send_packet (audio)", res ); + } + else if( Avi->pkt->stream_index == Avi->video_stream ) + { + res = avcodec_send_packet( Avi->video_ctx, Avi->pkt ); + if( res < 0 ) + AVI_SpewAvError( Avi->quiet, "avcodec_send_packet (audio)", res ); + } + av_packet_unref( Avi->pkt ); + } + else + { + if( res != AVERROR_EOF ) + AVI_SpewAvError( Avi->quiet, "av_read_frame", res ); + + if( Avi->audio_ctx ) + avcodec_flush_buffers( Avi->audio_ctx ); + + avcodec_flush_buffers( Avi->video_ctx ); + flushing = true; + break; + } + + if( Avi->audio_ctx ) + { + while( avcodec_receive_frame( Avi->audio_ctx, Avi->aframe ) == 0 ) + { + AVI_HandleAudio( Avi, Avi->aframe ); + decoded = true; + } + } + + while( avcodec_receive_frame( Avi->video_ctx, Avi->vframe ) == 0 ) + { + Avi->last_time = Avi->first_time + Avi->vframe->best_effort_timestamp; + decoded = true; + + if( FBitSet( Avi->vframe->flags, AV_FRAME_FLAG_CORRUPT|AV_FRAME_FLAG_DISCARD )) + continue; + + if( Avi->vframe->decode_error_flags != 0 ) + continue; + + av_frame_unref( Avi->vframe_copy ); + if( av_frame_ref( Avi->vframe_copy, Avi->vframe ) == 0 ) + redraw = true; + } + } + + if( redraw ) + { + sws_scale( Avi->sws_ctx, (void*)Avi->vframe_copy->data, Avi->vframe_copy->linesize, 0, Avi->video_ctx->height, + &Avi->dst, &Avi->dst_linesize ); + av_frame_unref( Avi->vframe_copy ); + } + + if( Avi->texture == 0 ) + { + int w = Avi->w >= 0 ? Avi->w : refState.width; + int h = Avi->h >= 0 ? Avi->h : refState.height; + + ref.dllFuncs.R_DrawStretchRaw( Avi->x, Avi->y, w, h, Avi->xres, Avi->yres, Avi->dst, redraw ); + } + else if( redraw && Avi->texture > 0 ) + ref.dllFuncs.AVI_UploadRawFrame( Avi->texture, Avi->xres, Avi->yres, Avi->w, Avi->h, Avi->dst ); + + if( flushing && !decoded ) + return false; // probably hit an EOF + + return true; +} + +void AVI_OpenVideo( movie_state_t *Avi, const char *filename, qboolean load_audio, int quiet ) +{ + byte *dst[4]; + int dst_linesize[4]; + int ret; + + if( Avi->active ) + AVI_CloseVideo( Avi ); + + if( !filename || !avi_initialized ) + return; + + Avi->active = false; + Avi->quiet = quiet; + Avi->video_ctx = Avi->audio_ctx = NULL; + Avi->fmt_ctx = NULL; + + if(( ret = avformat_open_input( &Avi->fmt_ctx, filename, NULL, NULL )) < 0 ) + { + AVI_SpewAvError( quiet, "avformat_open_input", ret ); + return; + } + + if(( ret = avformat_find_stream_info( Avi->fmt_ctx, NULL )) < 0 ) + { + AVI_SpewAvError( quiet, "avformat_find_stream_info", ret ); + return; + } + + if( !( Avi->pkt = av_packet_alloc( ))) + { + AVI_SpewAvError( quiet, "av_packet_alloc", 0 ); + return; + } + + if( !( Avi->vframe = av_frame_alloc( ))) + { + AVI_SpewAvError( quiet, "av_frame_alloc (video)", 0 ); + return; + } + + if( !( Avi->vframe_copy = av_frame_alloc( ))) + { + AVI_SpewAvError( quiet, "av_frame_alloc (video)", 0 ); + return; + } + + + Avi->video_stream = AVI_OpenCodecContext( &Avi->video_ctx, Avi->fmt_ctx, AVMEDIA_TYPE_VIDEO, quiet ); + + if( Avi->video_stream < 0 ) + return; + + Avi->xres = Avi->video_ctx->width; + Avi->yres = Avi->video_ctx->height; + Avi->pix_fmt = Avi->video_ctx->pix_fmt; + Avi->duration = Avi->fmt_ctx->duration / (double)AV_TIME_BASE; + Avi->entnum = S_RAW_SOUND_SOUNDTRACK; + Avi->attn = ATTN_NONE; + Avi->volume = 255; + + if( !( Avi->sws_ctx = sws_getContext( Avi->xres, Avi->yres, Avi->pix_fmt, + Avi->xres, Avi->yres, AV_PIX_FMT_BGR0, SWS_POINT, NULL, NULL, NULL ))) + { + AVI_SpewAvError( quiet, "sws_getContext", 0 ); + return; + } + + if(( ret = av_image_alloc( dst, dst_linesize, Avi->xres, Avi->yres, AV_PIX_FMT_BGR0, 1 )) < 0 ) + { + AVI_SpewAvError( quiet, "av_image_alloc", ret ); + return; + } + + Avi->dst = dst[0]; + Avi->dst_linesize = dst_linesize[0]; + + if( load_audio ) + { + if( !( Avi->aframe = av_frame_alloc( ))) + { + AVI_SpewAvError( quiet, "av_frame_alloc (audio)", 0 ); + return; + } + + Avi->audio_stream = AVI_OpenCodecContext( &Avi->audio_ctx, Avi->fmt_ctx, AVMEDIA_TYPE_AUDIO, quiet ); + + // audio stream was requested but it wasn't found + if( Avi->audio_stream < 0 ) + return; + + Avi->channels = Q_min( Avi->audio_ctx->ch_layout.nb_channels, 2 ); + if( Avi->audio_ctx->sample_fmt == AV_SAMPLE_FMT_U8 || Avi->audio_ctx->sample_fmt == AV_SAMPLE_FMT_U8P ) + Avi->s_fmt = AV_SAMPLE_FMT_U8; + else Avi->s_fmt = AV_SAMPLE_FMT_S16; + Avi->rate = Avi->audio_ctx->sample_rate; + + if(( ret = swr_alloc_set_opts2( &Avi->swr_ctx, &Avi->audio_ctx->ch_layout, Avi->s_fmt, Avi->rate, + &Avi->audio_ctx->ch_layout, Avi->audio_ctx->sample_fmt, Avi->audio_ctx->sample_rate, 0, 0 )) < 0 ) + { + AVI_SpewAvError( quiet, "swr_alloc_set_opts2", ret ); + return; + } + + if(( ret = swr_init( Avi->swr_ctx )) < 0 ) + { + AVI_SpewAvError( quiet, "swr_init", ret ); + return; + } + } + + Avi->active = true; +} + +void AVI_CloseVideo( movie_state_t *Avi ) +{ + if( Avi->active ) + { + if( Avi->cached_audio ) + Mem_Free( Avi->cached_audio ); + + swr_free( &Avi->swr_ctx ); + avcodec_free_context( &Avi->audio_ctx ); + av_frame_free( &Avi->aframe ); + + av_free( Avi->dst ); + sws_freeContext( Avi->sws_ctx ); + avcodec_free_context( &Avi->video_ctx ); + av_frame_free( &Avi->vframe ); + av_frame_free( &Avi->vframe_copy ); + + av_packet_free( &Avi->pkt ); + + avformat_close_input( &Avi->fmt_ctx ); + } + + memset( Avi, 0, sizeof( *Avi )); +} + +movie_state_t *AVI_LoadVideo( const char *filename, qboolean load_audio ) +{ + movie_state_t *Avi; + string path; + const char *fullpath; + + // fast reject + if( !avi_initialized ) + return NULL; + + // open cinematic + Q_snprintf( path, sizeof( path ), "media/%s", filename ); + COM_DefaultExtension( path, ".avi", sizeof( path )); + fullpath = FS_GetDiskPath( path, false ); + + if( FS_FileExists( path, false ) && !fullpath ) + { + Con_Printf( "Couldn't load %s from packfile. Please extract it\n", path ); + return NULL; + } + + Avi = Mem_Calloc( avi_mempool, sizeof( movie_state_t )); + AVI_OpenVideo( Avi, fullpath, load_audio, false ); + + if( !AVI_IsActive( Avi )) + { + AVI_FreeVideo( Avi ); // something bad happens + return NULL; + } + + // all done + return Avi; +} + +void AVI_FreeVideo( movie_state_t *Avi ) +{ + if( !Avi ) + return; + + AVI_CloseVideo( Avi ); + + if( Mem_IsAllocatedExt( avi_mempool, Avi )) + Mem_Free( Avi ); +} + +qboolean AVI_IsActive( movie_state_t *Avi ) +{ + return Avi ? Avi->active : false; +} + +movie_state_t *AVI_GetState( int num ) +{ + return &avi[num]; +} + +qboolean AVI_Initailize( void ) +{ + uint ver; + + if( Sys_CheckParm( "-noavi" )) + { + Con_Printf( "AVI: Disabled\n" ); + return false; + } + + // print version we're compiled with and which version we're running with + ver = avutil_version(); + Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBAVUTIL_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver )); + + ver = avformat_version(); + Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBAVFORMAT_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver )); + + ver = avformat_version(); + Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBAVCODEC_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver )); + + ver = swscale_version(); + Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBSWSCALE_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver )); + + ver = swresample_version(); + Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBSWRESAMPLE_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver )); + + avi_initialized = true; + avi_mempool = Mem_AllocPool( "AVI Zone" ); + return true; +} + +void AVI_Shutdown( void ) +{ + Mem_FreePool( &avi_mempool ); + avi_initialized = false; +} + +#endif // XASH_AVI == AVI_NULL diff --git a/engine/client/avi/avi_stub.c b/engine/client/avi/avi_stub.c index a4ff7fb0..c911e3e4 100644 --- a/engine/client/avi/avi_stub.c +++ b/engine/client/avi/avi_stub.c @@ -79,6 +79,8 @@ movie_state_t *AVI_GetState( int num ) qboolean AVI_Initailize( void ) { + Con_Printf( "AVI: Not supported\n" ); + return false; } diff --git a/engine/client/cl_cmds.c b/engine/client/cl_cmds.c index 691574c2..18338fb5 100644 --- a/engine/client/cl_cmds.c +++ b/engine/client/cl_cmds.c @@ -42,7 +42,8 @@ void CL_PlayVideo_f( void ) switch( Cmd_Argc( )) { case 2: // simple user version - Q_snprintf( path, sizeof( path ), "media/%s.avi", Cmd_Argv( 1 )); + Q_snprintf( path, sizeof( path ), "media/%s", Cmd_Argv( 1 )); + COM_DefaultExtension( path, ".avi", sizeof( path )); SCR_PlayCinematic( path ); break; case 3: // sequenced cinematics used this diff --git a/engine/client/cl_gameui.c b/engine/client/cl_gameui.c index 3122e938..95abf99d 100644 --- a/engine/client/cl_gameui.c +++ b/engine/client/cl_gameui.c @@ -302,14 +302,11 @@ void UI_ConnectionProgress_ParseServerInfo( const char *server ) static void GAME_EXPORT UI_DrawLogo( const char *filename, float x, float y, float width, float height ) { - static float cin_time; - static int last_frame = -1; - byte *cin_data = NULL; movie_state_t *cin_state; - int cin_frame; - qboolean redraw = false; - if( !gameui.drawLogo ) return; + if( !gameui.drawLogo ) + return; + cin_state = AVI_GetState( CIN_LOGO ); if( !AVI_IsActive( cin_state )) @@ -336,37 +333,25 @@ static void GAME_EXPORT UI_DrawLogo( const char *filename, float x, float y, flo gameui.drawLogo = false; return; } - - cin_time = 0.0f; - last_frame = -1; } if( width <= 0 || height <= 0 ) { // precache call, don't draw - cin_time = 0.0f; - last_frame = -1; return; } - // advances cinematic time (ignores maxfps and host_framerate settings) - cin_time += host.realframetime; - - // restarts the cinematic - if( cin_time > gameui.logo_length ) - cin_time = 0.0f; + AVI_SetParm( cin_state, + AVI_RENDER_TEXNUM, 0, + AVI_RENDER_X, (int)x, + AVI_RENDER_Y, (int)y, + AVI_RENDER_W, (int)width, + AVI_RENDER_H, (int)height, + AVI_PARM_LAST ); // read the next frame - cin_frame = AVI_GetVideoFrameNumber( cin_state, cin_time ); - - if( cin_frame != last_frame ) - { - cin_data = AVI_GetVideoFrame( cin_state, cin_frame ); - last_frame = cin_frame; - redraw = true; - } - - ref.dllFuncs.R_DrawStretchRaw( x, y, width, height, gameui.logo_xres, gameui.logo_yres, cin_data, redraw ); + if( !AVI_Think( cin_state )) + AVI_SetParm( cin_state, AVI_REWIND, AVI_PARM_LAST ); } static int GAME_EXPORT UI_GetLogoWidth( void ) diff --git a/engine/client/cl_render.c b/engine/client/cl_render.c index 400ed0fb..87feac0e 100644 --- a/engine/client/cl_render.c +++ b/engine/client/cl_render.c @@ -269,8 +269,8 @@ static render_api_t gRenderAPI = (void*)AVI_FreeVideo, (void*)AVI_IsActive, S_StreamAviSamples, - NULL, - NULL, + AVI_Think, + AVI_SetParm, NULL, // GL_Bind, NULL, // GL_SelectTexture, NULL, // GL_LoadTexMatrixExt, diff --git a/engine/client/cl_video.c b/engine/client/cl_video.c index d3ec40aa..a23554d1 100644 --- a/engine/client/cl_video.c +++ b/engine/client/cl_video.c @@ -26,8 +26,6 @@ AVI PLAYING static int xres, yres; static float video_duration; -static float cin_time; -static int cin_frame; static wavdata_t cin_audio; static movie_state_t *cin_state; @@ -88,7 +86,17 @@ void SCR_CheckStartupVids( void ) char *pfile; string token; - if( Sys_CheckParm( "-nointro" ) || host_developer.value || cls.demonum != -1 || GameState->nextstate != STATE_RUNFRAME ) +#if 0 + if( host_developer.value ) + { + // don't run movies where we in developer-mode + cls.movienum = -1; + CL_CheckStartupDemos(); + return; + } +#endif + + if( Sys_CheckParm( "-nointro" ) || cls.demonum != -1 || GameState->nextstate != STATE_RUNFRAME ) { // don't run movies where we in developer-mode cls.movienum = -1; @@ -147,23 +155,9 @@ void SCR_RunCinematic( void ) Key_SetKeyDest( key_menu ); S_StopStreaming(); cls.movienum = -1; - cin_time = 0.0f; cls.signon = 0; return; } - - // advances cinematic time (ignores maxfps and host_framerate settings) - cin_time += host.realframetime; - - // stop the video after it finishes - if( cin_time > video_duration + 0.1f ) - { - SCR_NextMovie( ); - return; - } - - // read the next frame - cin_frame = AVI_GetVideoFrameNumber( cin_state, cin_time ); } /* @@ -176,21 +170,11 @@ should be skipped */ qboolean SCR_DrawCinematic( void ) { - static int last_frame = -1; - qboolean redraw = false; - byte *frame = NULL; - - if( !ref.initialized || cin_time <= 0.0f ) + if( !ref.initialized ) return false; - if( cin_frame != last_frame ) - { - frame = AVI_GetVideoFrame( cin_state, cin_frame ); - last_frame = cin_frame; - redraw = true; - } - - ref.dllFuncs.R_DrawStretchRaw( 0, 0, refState.width, refState.height, xres, yres, frame, redraw ); + if( !AVI_Think( cin_state )) + return SCR_NextMovie(); return true; } @@ -232,10 +216,16 @@ qboolean SCR_PlayCinematic( const char *arg ) S_StartStreaming(); } + AVI_SetParm( cin_state, + AVI_RENDER_X, 0, + AVI_RENDER_Y, 0, + AVI_RENDER_W, -1, + AVI_RENDER_H, -1, + AVI_PARM_LAST ); + UI_SetActiveMenu( false ); cls.state = ca_cinematic; Con_FastClose(); - cin_time = 0.0f; cls.signon = 0; return true; @@ -270,7 +260,6 @@ void SCR_StopCinematic( void ) AVI_CloseVideo( cin_state ); S_StopStreaming(); - cin_time = 0.0f; cls.state = ca_disconnected; cls.signon = 0; diff --git a/engine/wscript b/engine/wscript index 904e797b..f2384f81 100644 --- a/engine/wscript +++ b/engine/wscript @@ -8,6 +8,23 @@ import os top = '.' +FFMPEG_CHECK_FRAGMENT=''' +#include +#include +#include +#include +#include +int main(int argc, char **argv) +{ + swresample_version(); + avformat_version(); + avcodec_version(); + swscale_version(); + avutil_version(); + return 0; +} +''' + def options(opt): grp = opt.add_option_group('Engine options') @@ -35,6 +52,9 @@ def options(opt): grp.add_option('--enable-ffmpeg', action = 'store_true', dest = 'FFMPEG', default = False, help = '') # hidden option, does nothing + grp.add_option('--enable-ffmpeg', action = 'store_true', dest = 'FFMPEG', default = False, + help = 'enable ffmpeg based movie playback') + opt.load('sdl2') def configure(conf): @@ -125,6 +145,20 @@ def configure(conf): conf.define('ENGINE_DLL', 1) + if conf.options.FFMPEG: + # add ffmpeg libraries into single uselib package + conf.check_cfg(package='libswresample', uselib_store='SWRESAMPLE', args='--cflags --libs') + conf.check_cfg(package='libavformat', uselib_store='AVFORMAT', args='--cflags --libs') + conf.check_cfg(package='libavcodec', uselib_store='AVCODEC', args='--cflags --libs') + conf.check_cfg(package='libswscale', uselib_store='SWSCALE', args='--cflags --libs') + conf.check_cfg(package='libavutil', uselib_store='AVUTIL', args='--cflags --libs') + + # validate ffmpeg libs installation + conf.check_cc(fragment=FFMPEG_CHECK_FRAGMENT, use='SWRESAMPLE AVCODEC AVFORMAT AVUTIL SWSCALE', msg='Checking for ffmpeg sanity') + + conf.env.FFMPEG = True + conf.define('HAVE_FFMPEG', True) + conf.define_cond('XASH_ENGINE_TESTS', conf.env.ENGINE_TESTS) conf.define_cond('XASH_STATIC_LIBS', conf.env.STATIC_LINKING) conf.define_cond('XASH_CUSTOM_SWAP', conf.options.CUSTOM_SWAP) @@ -186,6 +220,13 @@ def build(bld): libs.append('SDL3' if bld.env.HAVE_SDL3 else 'SDL2') source += bld.path.ant_glob(['platform/sdl/*.c']) + if bld.get_define('HAVE_FFMPEG'): + libs.append('SWRESAMPLE') + libs.append('AVCODEC') + libs.append('AVFORMAT') + libs.append('AVUTIL') + libs.append('SWSCALE') + if bld.env.MAGX: libs.append('MAGX') is_cxx_link = True