From 7d75c5e2bced6e2efa6c432251dfe16bac181859 Mon Sep 17 00:00:00 2001 From: Martin-Vignali Date: Mon, 7 Apr 2025 20:57:44 +0200 Subject: [PATCH] Movie: Add support for writing ProRes codec videos ProRes is a common intra-frame codec in post-production work, supported by a wide range of post-production software. This PR adds support for direct output from Blender using the ProRes codec from FFmpeg. Alpha is supported, along with 8 and 10-bit channel images. Co-authored-by: mvji <33432858+mvji@users.noreply.github.com> Pull Request: https://projects.blender.org/blender/blender/pulls/136405 --- scripts/startup/bl_ui/properties_output.py | 11 +++-- source/blender/imbuf/movie/MOV_enums.hh | 1 + source/blender/imbuf/movie/MOV_util.hh | 5 ++- .../blender/imbuf/movie/intern/movie_util.cc | 12 +++-- .../blender/imbuf/movie/intern/movie_write.cc | 44 +++++++++++++++++++ .../blender/imbuf/movie/intern/movie_write.hh | 1 + source/blender/makesdna/DNA_scene_types.h | 11 +++++ source/blender/makesrna/intern/rna_scene.cc | 21 ++++++++- 8 files changed, 95 insertions(+), 11 deletions(-) diff --git a/scripts/startup/bl_ui/properties_output.py b/scripts/startup/bl_ui/properties_output.py index de93d23c028..e2dfad2f6b3 100644 --- a/scripts/startup/bl_ui/properties_output.py +++ b/scripts/startup/bl_ui/properties_output.py @@ -531,7 +531,7 @@ class RENDER_PT_encoding_video(RenderOutputButtonsPanel, Panel): # Color depth. List of codecs needs to be in sync with # `IMB_ffmpeg_valid_bit_depths` in source code. - use_bpp = needs_codec and ffmpeg.codec in {'H264', 'H265', 'AV1'} + use_bpp = needs_codec and ffmpeg.codec in {'H264', 'H265', 'AV1', 'PRORES'} if use_bpp: image_settings = context.scene.render.image_settings layout.prop(image_settings, "color_depth", expand=True) @@ -539,6 +539,9 @@ class RENDER_PT_encoding_video(RenderOutputButtonsPanel, Panel): if ffmpeg.codec == 'DNXHD': layout.prop(ffmpeg, "use_lossless_output") + if ffmpeg.codec == 'PRORES': + layout.prop(ffmpeg, "ffmpeg_prores_profile") + # Output quality use_crf = needs_codec and ffmpeg.codec in { 'H264', @@ -550,10 +553,10 @@ class RENDER_PT_encoding_video(RenderOutputButtonsPanel, Panel): if use_crf: layout.prop(ffmpeg, "constant_rate_factor") - use_encoding_speed = needs_codec and ffmpeg.codec not in {'DNXHD', 'FFV1', 'HUFFYUV', 'PNG', 'QTRLE'} - use_bitrate = needs_codec and ffmpeg.codec not in {'FFV1', 'HUFFYUV', 'PNG', 'QTRLE'} + use_encoding_speed = needs_codec and ffmpeg.codec not in {'DNXHD', 'FFV1', 'HUFFYUV', 'PNG', 'PRORES', 'QTRLE'} + use_bitrate = needs_codec and ffmpeg.codec not in {'FFV1', 'HUFFYUV', 'PNG', 'PRORES', 'QTRLE'} use_min_max_bitrate = ffmpeg.codec not in {'DNXHD'} - use_gop = needs_codec and ffmpeg.codec not in {'DNXHD', 'HUFFYUV', 'PNG'} + use_gop = needs_codec and ffmpeg.codec not in {'DNXHD', 'HUFFYUV', 'PNG', 'PRORES'} use_b_frames = needs_codec and use_gop and ffmpeg.codec not in {'FFV1', 'QTRLE'} # Encoding speed diff --git a/source/blender/imbuf/movie/MOV_enums.hh b/source/blender/imbuf/movie/MOV_enums.hh index 341e2acede7..72bef7be869 100644 --- a/source/blender/imbuf/movie/MOV_enums.hh +++ b/source/blender/imbuf/movie/MOV_enums.hh @@ -51,6 +51,7 @@ enum IMB_Ffmpeg_Codec_ID { FFMPEG_CODEC_ID_VP9 = 167, FFMPEG_CODEC_ID_H265 = 173, FFMPEG_CODEC_ID_AV1 = 226, + FFMPEG_CODEC_ID_PRORES = 147, FFMPEG_CODEC_ID_PCM_S16LE = 65536, FFMPEG_CODEC_ID_MP2 = 86016, FFMPEG_CODEC_ID_MP3 = 86017, diff --git a/source/blender/imbuf/movie/MOV_util.hh b/source/blender/imbuf/movie/MOV_util.hh index 28e7c2920e9..442fb2c8a67 100644 --- a/source/blender/imbuf/movie/MOV_util.hh +++ b/source/blender/imbuf/movie/MOV_util.hh @@ -8,6 +8,7 @@ #pragma once +struct FFMpegCodecData; struct ImageFormatData; struct RenderData; @@ -26,8 +27,8 @@ void MOV_exit(); */ bool MOV_is_movie_file(const char *filepath); -/** Checks whether given FFMPEG video AVCodecID supports alpha channel (RGBA). */ -bool MOV_codec_supports_alpha(int av_codec_id); +/** Checks whether given FFMpegCodecData supports alpha channel (RGBA). */ +bool MOV_codec_supports_alpha(const FFMpegCodecData &ff_codec_data); /** * Checks whether given FFMPEG video AVCodecID supports CRF (i.e. "quality level") diff --git a/source/blender/imbuf/movie/intern/movie_util.cc b/source/blender/imbuf/movie/intern/movie_util.cc index 99a83f47137..05445502746 100644 --- a/source/blender/imbuf/movie/intern/movie_util.cc +++ b/source/blender/imbuf/movie/intern/movie_util.cc @@ -380,7 +380,7 @@ int MOV_codec_valid_bit_depths(int av_codec_id) int bit_depths = R_IMF_CHAN_DEPTH_8; #ifdef WITH_FFMPEG /* Note: update properties_output.py `use_bpp` when changing this function. */ - if (ELEM(av_codec_id, AV_CODEC_ID_H264, AV_CODEC_ID_H265, AV_CODEC_ID_AV1)) { + if (ELEM(av_codec_id, AV_CODEC_ID_H264, AV_CODEC_ID_H265, AV_CODEC_ID_AV1, AV_CODEC_ID_PRORES)) { bit_depths |= R_IMF_CHAN_DEPTH_10; } if (ELEM(av_codec_id, AV_CODEC_ID_H265, AV_CODEC_ID_AV1)) { @@ -499,17 +499,21 @@ void MOV_validate_output_settings(RenderData *rd, const ImageFormatData *imf) #endif } -bool MOV_codec_supports_alpha(int av_codec_id) +bool MOV_codec_supports_alpha(const FFMpegCodecData &ff_codec_data) { #ifdef WITH_FFMPEG - return ELEM(av_codec_id, + if (ff_codec_data.codec == AV_CODEC_ID_PRORES) { + return ELEM( + ff_codec_data.ffmpeg_prores_profile, FFM_PRORES_PROFILE_4444, FFM_PRORES_PROFILE_4444_XQ); + } + return ELEM(ff_codec_data.codec, AV_CODEC_ID_FFV1, AV_CODEC_ID_QTRLE, AV_CODEC_ID_PNG, AV_CODEC_ID_VP9, AV_CODEC_ID_HUFFYUV); #else - UNUSED_VARS(av_codec_id); + UNUSED_VARS(ff_codec_data); return false; #endif } diff --git a/source/blender/imbuf/movie/intern/movie_write.cc b/source/blender/imbuf/movie/intern/movie_write.cc index d2c9472eec2..0e8819785cc 100644 --- a/source/blender/imbuf/movie/intern/movie_write.cc +++ b/source/blender/imbuf/movie/intern/movie_write.cc @@ -537,6 +537,21 @@ static int remap_crf_to_h265_crf(int crf, bool is_10_or_12_bpp) return crf; } +static const AVCodec *get_prores_encoder(RenderData *rd, int rectx, int recty) +{ + /* prores_aw currently (April 2025) have issue when encoding alpha with high resolution + but in all cases is faster for similar quality use it instead of prores_ks if + possible + https://trac.ffmpeg.org/ticket/11536 + */ + if (rd->im_format.planes == R_IMF_PLANES_RGBA) { + if ((rectx * recty) > (3840 * 2160)) { + return avcodec_find_encoder_by_name("prores_ks"); + } + } + return avcodec_find_encoder_by_name("prores_aw"); +} + /* 10bpp H264: remap 0..51 range to -12..51 range * https://trac.ffmpeg.org/wiki/Encode/H.264#a1.ChooseaCRFvalue */ static int remap_crf_to_h264_10bpp_crf(int crf) @@ -651,6 +666,9 @@ static AVStream *alloc_video_stream(MovieWriter *context, * on given parameters, and also set up opts. */ codec = get_av1_encoder(context, rd, &opts, rectx, recty); } + else if (codec_id == AV_CODEC_ID_PRORES) { + codec = get_prores_encoder(rd, rectx, recty); + } else { codec = avcodec_find_encoder(codec_id); } @@ -817,6 +835,27 @@ static AVStream *alloc_video_stream(MovieWriter *context, c->pix_fmt = AV_PIX_FMT_RGBA; } } + if (codec_id == AV_CODEC_ID_PRORES) { + if ((context->ffmpeg_profile >= FFM_PRORES_PROFILE_422_PROXY) && + (context->ffmpeg_profile <= FFM_PRORES_PROFILE_422_HQ)) + { + c->profile = context->ffmpeg_profile; + c->pix_fmt = AV_PIX_FMT_YUV422P10LE; + } + else if ((context->ffmpeg_profile >= FFM_PRORES_PROFILE_4444) && + (context->ffmpeg_profile <= FFM_PRORES_PROFILE_4444_XQ)) + { + c->profile = context->ffmpeg_profile; + c->pix_fmt = AV_PIX_FMT_YUV444P10LE; + + if (rd->im_format.planes == R_IMF_PLANES_RGBA) { + c->pix_fmt = AV_PIX_FMT_YUVA444P10LE; + } + } + else { + fprintf(stderr, "ffmpeg: invalid profile %d\n", context->ffmpeg_profile); + } + } if (of->oformat->flags & AVFMT_GLOBALHEADER) { FF_DEBUG_PRINT("ffmpeg: using global video header\n"); @@ -945,6 +984,7 @@ static bool start_ffmpeg_impl(MovieWriter *context, context->ffmpeg_autosplit = (rd->ffcodecdata.flags & FFMPEG_AUTOSPLIT_OUTPUT) != 0; context->ffmpeg_crf = rd->ffcodecdata.constant_rate_factor; context->ffmpeg_preset = rd->ffcodecdata.ffmpeg_preset; + context->ffmpeg_profile = 0; if ((rd->ffcodecdata.flags & FFMPEG_USE_MAX_B_FRAMES) != 0) { context->ffmpeg_max_b_frames = rd->ffcodecdata.max_b_frames; @@ -1059,6 +1099,10 @@ static bool start_ffmpeg_impl(MovieWriter *context, } } + if (video_codec == AV_CODEC_ID_PRORES) { + context->ffmpeg_profile = rd->ffcodecdata.ffmpeg_prores_profile; + } + if (video_codec != AV_CODEC_ID_NONE) { context->video_stream = alloc_video_stream( context, rd, video_codec, of, rectx, recty, error, sizeof(error)); diff --git a/source/blender/imbuf/movie/intern/movie_write.hh b/source/blender/imbuf/movie/intern/movie_write.hh index 3bac59a9fe9..3d5d673a995 100644 --- a/source/blender/imbuf/movie/intern/movie_write.hh +++ b/source/blender/imbuf/movie/intern/movie_write.hh @@ -54,6 +54,7 @@ struct MovieWriter { int ffmpeg_crf = 0; /* set to 0 to not use CRF mode; we have another flag for lossless anyway. */ int ffmpeg_preset = 0; /* see eFFMpegPreset */ + int ffmpeg_profile = 0; AVFormatContext *outfile = nullptr; AVCodecContext *video_codec = nullptr; diff --git a/source/blender/makesdna/DNA_scene_types.h b/source/blender/makesdna/DNA_scene_types.h index b3fc7b44db0..10785fc25d5 100644 --- a/source/blender/makesdna/DNA_scene_types.h +++ b/source/blender/makesdna/DNA_scene_types.h @@ -110,6 +110,15 @@ typedef enum eFFMpegAudioChannels { FFM_CHANNELS_SURROUND71 = 8, } eFFMpegAudioChannels; +typedef enum eFFMpegProresProfile { + FFM_PRORES_PROFILE_422_PROXY = 0, /* FF_PROFILE_PRORES_PROXY */ + FFM_PRORES_PROFILE_422_LT = 1, /* FF_PROFILE_PRORES_LT */ + FFM_PRORES_PROFILE_422_STD = 2, /* FF_PROFILE_PRORES_STANDARD */ + FFM_PRORES_PROFILE_422_HQ = 3, /* FF_PROFILE_PRORES_HQ*/ + FFM_PRORES_PROFILE_4444 = 4, /* FF_PROFILE_PRORES_4444 */ + FFM_PRORES_PROFILE_4444_XQ = 5, /* FF_PROFILE_PRORES_XQ */ +} eFFMpegProresProfile; + typedef struct FFMpegCodecData { int type; int codec; @@ -126,12 +135,14 @@ typedef struct FFMpegCodecData { int constant_rate_factor; /** See eFFMpegPreset. */ int ffmpeg_preset; + int ffmpeg_prores_profile; int rc_min_rate; int rc_max_rate; int rc_buffer_size; int mux_packet_size; int mux_rate; + char _pad0[4]; void *_pad1; } FFMpegCodecData; diff --git a/source/blender/makesrna/intern/rna_scene.cc b/source/blender/makesrna/intern/rna_scene.cc index fd5299ce4ba..9cd09609440 100644 --- a/source/blender/makesrna/intern/rna_scene.cc +++ b/source/blender/makesrna/intern/rna_scene.cc @@ -1404,7 +1404,7 @@ static const EnumPropertyItem *rna_ImageFormatSettings_color_mode_itemf(bContext Scene *scene = (Scene *)ptr->owner_id; RenderData *rd = &scene->r; - if (MOV_codec_supports_alpha(rd->ffcodecdata.codec)) { + if (MOV_codec_supports_alpha(rd->ffcodecdata)) { chan_flag |= IMA_CHAN_FLAG_RGBA; } } @@ -6478,6 +6478,7 @@ static void rna_def_scene_ffmpeg_settings(BlenderRNA *brna) {FFMPEG_CODEC_ID_MPEG2VIDEO, "MPEG2", 0, "MPEG-2", ""}, {FFMPEG_CODEC_ID_MPEG4, "MPEG4", 0, "MPEG-4 (divx)", ""}, {FFMPEG_CODEC_ID_PNG, "PNG", 0, "PNG", ""}, + {FFMPEG_CODEC_ID_PRORES, "PRORES", 0, "ProRes", ""}, {FFMPEG_CODEC_ID_QTRLE, "QTRLE", 0, "QuickTime Animation", ""}, {FFMPEG_CODEC_ID_THEORA, "THEORA", 0, "Theora", ""}, {0, nullptr, 0, nullptr, nullptr}, @@ -6497,6 +6498,16 @@ static void rna_def_scene_ffmpeg_settings(BlenderRNA *brna) {0, nullptr, 0, nullptr, nullptr}, }; + static const EnumPropertyItem ffmpeg_prores_profiles_items[] = { + {FFM_PRORES_PROFILE_422_PROXY, "422_PROXY", 0, "ProRes 422 Proxy", ""}, + {FFM_PRORES_PROFILE_422_LT, "422_LT", 0, "ProRes 422 LT", ""}, + {FFM_PRORES_PROFILE_422_STD, "422_STD", 0, "ProRes 422", ""}, + {FFM_PRORES_PROFILE_422_HQ, "422_HQ", 0, "ProRes 422 HQ", ""}, + {FFM_PRORES_PROFILE_4444, "4444", 0, "ProRes 4444", ""}, + {FFM_PRORES_PROFILE_4444_XQ, "4444_XQ", 0, "ProRes 4444 XQ", ""}, + {0, nullptr, 0, nullptr, nullptr}, + }; + static const EnumPropertyItem ffmpeg_crf_items[] = { {FFM_CRF_NONE, "NONE", @@ -6655,6 +6666,14 @@ static void rna_def_scene_ffmpeg_settings(BlenderRNA *brna) prop, "Encoding Speed", "Tradeoff between encoding speed and compression ratio"); RNA_def_property_update(prop, NC_SCENE | ND_RENDER_OPTIONS, nullptr); + prop = RNA_def_property(srna, "ffmpeg_prores_profile", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_bitflag_sdna(prop, nullptr, "ffmpeg_prores_profile"); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_enum_items(prop, ffmpeg_prores_profiles_items); + RNA_def_property_enum_default(prop, FFM_PRORES_PROFILE_422_STD); + RNA_def_property_ui_text(prop, "Profile", "ProRes Profile"); + RNA_def_property_update(prop, NC_SCENE | ND_RENDER_OPTIONS, nullptr); + prop = RNA_def_property(srna, "use_autosplit", PROP_BOOLEAN, PROP_NONE); RNA_def_property_boolean_sdna(prop, nullptr, "flags", FFMPEG_AUTOSPLIT_OUTPUT); RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);