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:
committed by
Jesse Yurkovich
parent
7b705d6e54
commit
7d75c5e2bc
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user