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
This commit is contained in:
Martin-Vignali
2025-04-07 20:57:44 +02:00
committed by Jesse Yurkovich
parent 7b705d6e54
commit 7d75c5e2bc
8 changed files with 95 additions and 11 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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));

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);