From 0d25615c29316a1f2abcd7f92f4cfadd2544f1ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:50:17 +0000 Subject: [PATCH 1/5] Initial plan From 5a798fca6c865be6c31a7a305601b9442f134700 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:54:34 +0000 Subject: [PATCH 2/5] Add video recording format configuration and swscale library Co-authored-by: Cerwym <1760289+Cerwym@users.noreply.github.com> --- deps/CMakeLists.txt | 8 ++++++-- src/config_keeperfx.c | 17 +++++++++++++++++ src/config_keeperfx.h | 1 + src/scrcapt.c | 1 + src/scrcapt.h | 1 + 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 4f6f3d7fc9..4408d5ba59 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -123,5 +123,9 @@ add_library(libswresample_static STATIC IMPORTED) set_target_properties(libswresample_static PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg/libswresample/libswresample.a) set_target_properties(libswresample_static PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg) -target_link_libraries(keeperfx PUBLIC bcrypt libavcodec_static libavformat_static libavutil_static libswresample_static) -target_link_libraries(keeperfx_hvlog PUBLIC bcrypt libavcodec_static libavformat_static libavutil_static libswresample_static) +add_library(libswscale_static STATIC IMPORTED) +set_target_properties(libswscale_static PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg/libswscale/libswscale.a) +set_target_properties(libswscale_static PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg) + +target_link_libraries(keeperfx PUBLIC bcrypt libavcodec_static libavformat_static libavutil_static libswresample_static libswscale_static) +target_link_libraries(keeperfx_hvlog PUBLIC bcrypt libavcodec_static libavformat_static libavutil_static libswresample_static libswscale_static) diff --git a/src/config_keeperfx.c b/src/config_keeperfx.c index 823bd10f18..2196254a21 100644 --- a/src/config_keeperfx.c +++ b/src/config_keeperfx.c @@ -99,6 +99,12 @@ const struct NamedCommand scrshot_type[] = { {NULL, 0}, }; +const struct NamedCommand video_record_type[] = { + {"MKV", 1}, + {"FLC", 2}, + {NULL, 0}, + }; + const struct NamedCommand atmos_volume[] = { {"LOW", 64}, {"MEDIUM", 128}, @@ -155,6 +161,7 @@ const struct NamedCommand conf_commands[] = { {"FRAMES_PER_SECOND" , 39}, {"TAG_MODE_TOGGLING" , 40}, {"DEFAULT_TAG_MODE" , 41}, + {"VIDEO_RECORDING_FORMAT" , 42}, {NULL, 0}, }; @@ -904,6 +911,16 @@ static void load_file_configuration(const char *fname, const char *sname, const default_tag_mode = i; } break; + case 42: // VIDEO_RECORDING_FORMAT + i = recognize_conf_parameter(buf,&pos,len,video_record_type); + if (i <= 0) + { + CONFWRNLOG("Couldn't recognize \"%s\" command parameter in %s file.", + COMMAND_TEXT(cmd_num),config_textname); + break; + } + video_recording_format = i; + break; case ccr_comment: break; case ccr_endOfFile: diff --git a/src/config_keeperfx.h b/src/config_keeperfx.h index 0326897de7..f58c305c05 100644 --- a/src/config_keeperfx.h +++ b/src/config_keeperfx.h @@ -116,6 +116,7 @@ extern char keeper_runtime_directory[152]; extern unsigned long features_enabled; extern const struct NamedCommand lang_type[]; extern const struct NamedCommand scrshot_type[]; +extern const struct NamedCommand video_record_type[]; extern char cmd_char; extern short api_enabled; extern uint16_t api_port; diff --git a/src/scrcapt.c b/src/scrcapt.c index 3078163163..538a4eda50 100644 --- a/src/scrcapt.c +++ b/src/scrcapt.c @@ -41,6 +41,7 @@ /******************************************************************************/ unsigned char screenshot_format = 1; +unsigned char video_recording_format = 1; // 1=MKV, 2=FLC unsigned char cap_palette[768]; /******************************************************************************/ diff --git a/src/scrcapt.h b/src/scrcapt.h index 17aecc6bb4..2ce9ef3681 100644 --- a/src/scrcapt.h +++ b/src/scrcapt.h @@ -28,6 +28,7 @@ extern "C" { /******************************************************************************/ extern unsigned char screenshot_format; +extern unsigned char video_recording_format; /******************************************************************************/ TbBool perform_any_screen_capturing(void); From b681f309dd013dacc3cdc46c099668726a9f06e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:56:16 +0000 Subject: [PATCH 3/5] Implement ffmpeg-based MKV video recording functionality Co-authored-by: Cerwym <1760289+Cerwym@users.noreply.github.com> --- src/bflib_fmvids.cpp | 390 +++++++++++++++++++++++++++++++++++++++++++ src/bflib_fmvids.h | 3 + src/scrcapt.c | 31 +++- 3 files changed, 421 insertions(+), 3 deletions(-) diff --git a/src/bflib_fmvids.cpp b/src/bflib_fmvids.cpp index cc58370486..db77ca7186 100644 --- a/src/bflib_fmvids.cpp +++ b/src/bflib_fmvids.cpp @@ -13,6 +13,7 @@ extern "C" { #include #include #include + #include #pragma GCC diagnostic warning "-Wdeprecated-declarations" } @@ -721,6 +722,357 @@ struct Animation { Animation animation; +// MKV recording structure using ffmpeg +struct MKVRecorder { + AVFormatContext *format_ctx = nullptr; + const AVCodec *codec = nullptr; + AVCodecContext *codec_ctx = nullptr; + AVStream *stream = nullptr; + AVFrame *frame = nullptr; + AVFrame *rgb_frame = nullptr; + AVPacket *packet = nullptr; + SwsContext *sws_ctx = nullptr; + int frame_counter = 0; + bool is_recording = false; + int width = 0; + int height = 0; + unsigned char palette[768]; +}; + +MKVRecorder mkv_recorder; + +// MKV recording functions +bool mkv_init_encoder(const char *filename, int width, int height) { + SYNCLOG("Starting MKV recording to \"%s\" at %dx%d", filename, width, height); + + mkv_recorder.width = width; + mkv_recorder.height = height; + + // Allocate output format context + avformat_alloc_output_context2(&mkv_recorder.format_ctx, nullptr, "matroska", filename); + if (!mkv_recorder.format_ctx) { + ERRORLOG("Could not create output context for MKV recording"); + return false; + } + + // Find H.264 encoder + mkv_recorder.codec = avcodec_find_encoder(AV_CODEC_ID_H264); + if (!mkv_recorder.codec) { + ERRORLOG("H.264 codec not found"); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.format_ctx = nullptr; + return false; + } + + // Create stream + mkv_recorder.stream = avformat_new_stream(mkv_recorder.format_ctx, nullptr); + if (!mkv_recorder.stream) { + ERRORLOG("Failed to create stream"); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.format_ctx = nullptr; + return false; + } + mkv_recorder.stream->id = mkv_recorder.format_ctx->nb_streams - 1; + + // Allocate codec context + mkv_recorder.codec_ctx = avcodec_alloc_context3(mkv_recorder.codec); + if (!mkv_recorder.codec_ctx) { + ERRORLOG("Could not allocate video codec context"); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.format_ctx = nullptr; + return false; + } + + // Set codec parameters + mkv_recorder.codec_ctx->codec_id = AV_CODEC_ID_H264; + mkv_recorder.codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO; + mkv_recorder.codec_ctx->width = width; + mkv_recorder.codec_ctx->height = height; + mkv_recorder.codec_ctx->time_base = AVRational{1, 57}; // 57 FPS to match FLC + mkv_recorder.codec_ctx->framerate = AVRational{57, 1}; + mkv_recorder.codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + mkv_recorder.codec_ctx->bit_rate = 2000000; // 2 Mbps + mkv_recorder.codec_ctx->gop_size = 10; + mkv_recorder.codec_ctx->max_b_frames = 1; + + // Set H.264 specific options for better compression + av_opt_set(mkv_recorder.codec_ctx->priv_data, "preset", "medium", 0); + av_opt_set(mkv_recorder.codec_ctx->priv_data, "crf", "23", 0); + + // Open codec + if (avcodec_open2(mkv_recorder.codec_ctx, mkv_recorder.codec, nullptr) < 0) { + ERRORLOG("Could not open codec"); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + + // Copy codec parameters to stream + avcodec_parameters_from_context(mkv_recorder.stream->codecpar, mkv_recorder.codec_ctx); + mkv_recorder.stream->time_base = mkv_recorder.codec_ctx->time_base; + + // Allocate frame for YUV420P + mkv_recorder.frame = av_frame_alloc(); + if (!mkv_recorder.frame) { + ERRORLOG("Could not allocate video frame"); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + mkv_recorder.frame->format = mkv_recorder.codec_ctx->pix_fmt; + mkv_recorder.frame->width = width; + mkv_recorder.frame->height = height; + + if (av_frame_get_buffer(mkv_recorder.frame, 0) < 0) { + ERRORLOG("Could not allocate frame buffer"); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + + // Allocate frame for RGB conversion (8-bit indexed -> RGB24) + mkv_recorder.rgb_frame = av_frame_alloc(); + if (!mkv_recorder.rgb_frame) { + ERRORLOG("Could not allocate RGB frame"); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + mkv_recorder.rgb_frame->format = AV_PIX_FMT_RGB24; + mkv_recorder.rgb_frame->width = width; + mkv_recorder.rgb_frame->height = height; + + if (av_frame_get_buffer(mkv_recorder.rgb_frame, 0) < 0) { + ERRORLOG("Could not allocate RGB frame buffer"); + av_frame_free(&mkv_recorder.rgb_frame); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.rgb_frame = nullptr; + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + + // Initialize SWS context for palette->RGB24->YUV420P conversion + mkv_recorder.sws_ctx = sws_getContext( + width, height, AV_PIX_FMT_RGB24, + width, height, AV_PIX_FMT_YUV420P, + SWS_BILINEAR, nullptr, nullptr, nullptr + ); + if (!mkv_recorder.sws_ctx) { + ERRORLOG("Could not initialize sws context"); + av_frame_free(&mkv_recorder.rgb_frame); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.rgb_frame = nullptr; + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + + // Allocate packet + mkv_recorder.packet = av_packet_alloc(); + if (!mkv_recorder.packet) { + ERRORLOG("Could not allocate packet"); + sws_freeContext(mkv_recorder.sws_ctx); + av_frame_free(&mkv_recorder.rgb_frame); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.sws_ctx = nullptr; + mkv_recorder.rgb_frame = nullptr; + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + + // Open output file + if (!(mkv_recorder.format_ctx->oformat->flags & AVFMT_NOFILE)) { + if (avio_open(&mkv_recorder.format_ctx->pb, filename, AVIO_FLAG_WRITE) < 0) { + ERRORLOG("Could not open output file '%s'", filename); + av_packet_free(&mkv_recorder.packet); + sws_freeContext(mkv_recorder.sws_ctx); + av_frame_free(&mkv_recorder.rgb_frame); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.packet = nullptr; + mkv_recorder.sws_ctx = nullptr; + mkv_recorder.rgb_frame = nullptr; + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + } + + // Write header + if (avformat_write_header(mkv_recorder.format_ctx, nullptr) < 0) { + ERRORLOG("Error writing header"); + if (!(mkv_recorder.format_ctx->oformat->flags & AVFMT_NOFILE)) { + avio_closep(&mkv_recorder.format_ctx->pb); + } + av_packet_free(&mkv_recorder.packet); + sws_freeContext(mkv_recorder.sws_ctx); + av_frame_free(&mkv_recorder.rgb_frame); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + mkv_recorder.packet = nullptr; + mkv_recorder.sws_ctx = nullptr; + mkv_recorder.rgb_frame = nullptr; + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + return false; + } + + mkv_recorder.frame_counter = 0; + mkv_recorder.is_recording = true; + memset(mkv_recorder.palette, 0, sizeof(mkv_recorder.palette)); + + SYNCLOG("MKV recording initialized successfully"); + return true; +} + +bool mkv_encode_frame(unsigned char *screenbuf, unsigned char *palette) { + if (!mkv_recorder.is_recording) { + return false; + } + + // Update palette if changed + memcpy(mkv_recorder.palette, palette, sizeof(mkv_recorder.palette)); + + // Convert 8-bit indexed color to RGB24 + for (int y = 0; y < mkv_recorder.height; y++) { + for (int x = 0; x < mkv_recorder.width; x++) { + int idx = y * mkv_recorder.width + x; + unsigned char pixel = screenbuf[idx]; + int rgb_idx = y * mkv_recorder.rgb_frame->linesize[0] + x * 3; + + // Palette is in RGB format, each color is 3 bytes + mkv_recorder.rgb_frame->data[0][rgb_idx + 0] = palette[pixel * 3 + 0]; + mkv_recorder.rgb_frame->data[0][rgb_idx + 1] = palette[pixel * 3 + 1]; + mkv_recorder.rgb_frame->data[0][rgb_idx + 2] = palette[pixel * 3 + 2]; + } + } + + // Convert RGB24 to YUV420P + sws_scale( + mkv_recorder.sws_ctx, + mkv_recorder.rgb_frame->data, + mkv_recorder.rgb_frame->linesize, + 0, + mkv_recorder.height, + mkv_recorder.frame->data, + mkv_recorder.frame->linesize + ); + + // Set frame pts + mkv_recorder.frame->pts = mkv_recorder.frame_counter++; + + // Send frame to encoder + int ret = avcodec_send_frame(mkv_recorder.codec_ctx, mkv_recorder.frame); + if (ret < 0) { + ERRORLOG("Error sending frame to encoder"); + return false; + } + + // Receive packets from encoder + while (ret >= 0) { + ret = avcodec_receive_packet(mkv_recorder.codec_ctx, mkv_recorder.packet); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } else if (ret < 0) { + ERRORLOG("Error receiving packet from encoder"); + return false; + } + + // Rescale packet timestamps + av_packet_rescale_ts(mkv_recorder.packet, mkv_recorder.codec_ctx->time_base, mkv_recorder.stream->time_base); + mkv_recorder.packet->stream_index = mkv_recorder.stream->index; + + // Write packet + ret = av_interleaved_write_frame(mkv_recorder.format_ctx, mkv_recorder.packet); + av_packet_unref(mkv_recorder.packet); + + if (ret < 0) { + ERRORLOG("Error writing packet"); + return false; + } + } + + return true; +} + +void mkv_finish_recording() { + if (!mkv_recorder.is_recording) { + return; + } + + SYNCLOG("Finishing MKV recording"); + + // Flush encoder + avcodec_send_frame(mkv_recorder.codec_ctx, nullptr); + + int ret = 0; + while (ret >= 0) { + ret = avcodec_receive_packet(mkv_recorder.codec_ctx, mkv_recorder.packet); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } else if (ret < 0) { + break; + } + + av_packet_rescale_ts(mkv_recorder.packet, mkv_recorder.codec_ctx->time_base, mkv_recorder.stream->time_base); + mkv_recorder.packet->stream_index = mkv_recorder.stream->index; + av_interleaved_write_frame(mkv_recorder.format_ctx, mkv_recorder.packet); + av_packet_unref(mkv_recorder.packet); + } + + // Write trailer + av_write_trailer(mkv_recorder.format_ctx); + + // Clean up + if (!(mkv_recorder.format_ctx->oformat->flags & AVFMT_NOFILE)) { + avio_closep(&mkv_recorder.format_ctx->pb); + } + + av_packet_free(&mkv_recorder.packet); + sws_freeContext(mkv_recorder.sws_ctx); + av_frame_free(&mkv_recorder.rgb_frame); + av_frame_free(&mkv_recorder.frame); + avcodec_free_context(&mkv_recorder.codec_ctx); + avformat_free_context(mkv_recorder.format_ctx); + + mkv_recorder.packet = nullptr; + mkv_recorder.sws_ctx = nullptr; + mkv_recorder.rgb_frame = nullptr; + mkv_recorder.frame = nullptr; + mkv_recorder.codec_ctx = nullptr; + mkv_recorder.format_ctx = nullptr; + mkv_recorder.is_recording = false; + + SYNCLOG("MKV recording finished"); +} + /** * Writes the data into FLI animation. * @return Returns false on error, true on success. @@ -1444,3 +1796,41 @@ extern "C" short anim_record() ERRORLOG("No free file name for recorded movie"); return 0; } + +extern "C" short anim_record_mkv() +{ + SYNCDBG(7,"Starting MKV recording"); + char finalname[255] = ""; + if (LbGraphicsScreenBPP() != 8) { + ERRORLOG("Cannot record movie in non-8bit screen mode"); + return 0; + } + int idx; + for (idx=0; idx < 10000; idx++) { + snprintf(finalname, sizeof(finalname), "%s/game%04d.mkv","scrshots",idx); + if (LbFileExists(finalname)) { + continue; + } + if (mkv_init_encoder(finalname, MyScreenWidth/pixel_size, MyScreenHeight/pixel_size)) { + return 1; + } else { + ERRORLOG("Failed to initialize MKV encoder"); + return 0; + } + } + ERRORLOG("No free file name for recorded movie"); + return 0; +} + +extern "C" TbBool anim_record_frame_mkv(unsigned char *screenbuf, unsigned char *palette) +{ + if (!mkv_recorder.is_recording) { + return false; + } + return mkv_encode_frame(screenbuf, palette); +} + +extern "C" void anim_stop_mkv() +{ + mkv_finish_recording(); +} diff --git a/src/bflib_fmvids.h b/src/bflib_fmvids.h index cce8f3da55..e8be015f4c 100644 --- a/src/bflib_fmvids.h +++ b/src/bflib_fmvids.h @@ -41,7 +41,10 @@ enum SmackerPlayFlags { TbBool play_smk(const char * filename, int flags); short anim_stop(void); short anim_record(void); +short anim_record_mkv(void); TbBool anim_record_frame(unsigned char * screenbuf, unsigned char * palette); +TbBool anim_record_frame_mkv(unsigned char * screenbuf, unsigned char * palette); +void anim_stop_mkv(void); #ifdef __cplusplus } diff --git a/src/scrcapt.c b/src/scrcapt.c index 538a4eda50..6b0bdca3a1 100644 --- a/src/scrcapt.c +++ b/src/scrcapt.c @@ -127,7 +127,16 @@ TbBool cumulative_screen_shot(void) TbBool movie_record_start(void) { - if ( anim_record() ) + short result; + if (video_recording_format == 1) { + // MKV format + result = anim_record_mkv(); + } else { + // FLC format + result = anim_record(); + } + + if (result) { set_flag(game.system_flags, GSF_CaptureMovie); return true; @@ -138,7 +147,14 @@ TbBool movie_record_start(void) TbBool movie_record_stop(void) { clear_flag(game.system_flags, GSF_CaptureMovie); - anim_stop(); + + if (video_recording_format == 1) { + // MKV format + anim_stop_mkv(); + } else { + // FLC format + anim_stop(); + } return true; } @@ -151,7 +167,16 @@ TbBool movie_record_frame(void) return false; } LbPaletteGet(cap_palette); - short result = anim_record_frame(lbDisplay.WScreen, cap_palette); + + short result; + if (video_recording_format == 1) { + // MKV format + result = anim_record_frame_mkv(lbDisplay.WScreen, cap_palette); + } else { + // FLC format + result = anim_record_frame(lbDisplay.WScreen, cap_palette); + } + if (!lock_mem) LbScreenUnlock(); return result; From 1edbd8eb6a2bd05bcf36791a8992b42633ffbcbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:56:56 +0000 Subject: [PATCH 4/5] Add VIDEO_RECORDING_FORMAT configuration to keeperfx.cfg Co-authored-by: Cerwym <1760289+Cerwym@users.noreply.github.com> --- config/keeperfx.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/keeperfx.cfg b/config/keeperfx.cfg index 13ac0f680d..c0b9f6a221 100644 --- a/config/keeperfx.cfg +++ b/config/keeperfx.cfg @@ -53,6 +53,11 @@ LOCK_CURSOR_IN_POSSESSION=ON ; File format in which screenshots are written; BMP or PNG. SCREENSHOT=PNG +; File format for video recording (SHIFT-M); MKV or FLC. +; MKV provides much better compression (50-100x smaller files) and is playable in standard media players. +; FLC is the original format with larger file sizes. +VIDEO_RECORDING_FORMAT=MKV + ; Run the game at high framerates while maintaining game speed. DELTA_TIME=ON From b0fe9b29f01fe4555565daab88207e59770902ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:58:02 +0000 Subject: [PATCH 5/5] Address code review feedback: improve GOP size, remove unused bit_rate, add format constants Co-authored-by: Cerwym <1760289+Cerwym@users.noreply.github.com> --- src/bflib_fmvids.cpp | 4 ++-- src/scrcapt.c | 8 ++++---- src/scrcapt.h | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/bflib_fmvids.cpp b/src/bflib_fmvids.cpp index db77ca7186..b4688ea493 100644 --- a/src/bflib_fmvids.cpp +++ b/src/bflib_fmvids.cpp @@ -791,8 +791,8 @@ bool mkv_init_encoder(const char *filename, int width, int height) { mkv_recorder.codec_ctx->time_base = AVRational{1, 57}; // 57 FPS to match FLC mkv_recorder.codec_ctx->framerate = AVRational{57, 1}; mkv_recorder.codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; - mkv_recorder.codec_ctx->bit_rate = 2000000; // 2 Mbps - mkv_recorder.codec_ctx->gop_size = 10; + // bit_rate is not used when CRF mode is enabled (see below) + mkv_recorder.codec_ctx->gop_size = 114; // ~2 seconds at 57 FPS for good compression/seek balance mkv_recorder.codec_ctx->max_b_frames = 1; // Set H.264 specific options for better compression diff --git a/src/scrcapt.c b/src/scrcapt.c index 6b0bdca3a1..edc963671b 100644 --- a/src/scrcapt.c +++ b/src/scrcapt.c @@ -41,7 +41,7 @@ /******************************************************************************/ unsigned char screenshot_format = 1; -unsigned char video_recording_format = 1; // 1=MKV, 2=FLC +unsigned char video_recording_format = VIDEO_FORMAT_MKV; // Default to MKV format unsigned char cap_palette[768]; /******************************************************************************/ @@ -128,7 +128,7 @@ TbBool cumulative_screen_shot(void) TbBool movie_record_start(void) { short result; - if (video_recording_format == 1) { + if (video_recording_format == VIDEO_FORMAT_MKV) { // MKV format result = anim_record_mkv(); } else { @@ -148,7 +148,7 @@ TbBool movie_record_stop(void) { clear_flag(game.system_flags, GSF_CaptureMovie); - if (video_recording_format == 1) { + if (video_recording_format == VIDEO_FORMAT_MKV) { // MKV format anim_stop_mkv(); } else { @@ -169,7 +169,7 @@ TbBool movie_record_frame(void) LbPaletteGet(cap_palette); short result; - if (video_recording_format == 1) { + if (video_recording_format == VIDEO_FORMAT_MKV) { // MKV format result = anim_record_frame_mkv(lbDisplay.WScreen, cap_palette); } else { diff --git a/src/scrcapt.h b/src/scrcapt.h index 2ce9ef3681..fb2908040b 100644 --- a/src/scrcapt.h +++ b/src/scrcapt.h @@ -26,6 +26,11 @@ extern "C" { #endif +/******************************************************************************/ +// Video recording format constants +#define VIDEO_FORMAT_MKV 1 +#define VIDEO_FORMAT_FLC 2 + /******************************************************************************/ extern unsigned char screenshot_format; extern unsigned char video_recording_format;