Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,254 @@
/* SPDX-FileCopyrightText: 2011 Peter Schlaile
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ffmpeg
*
* Compatibility macros to make every FFMPEG installation appear
* like the most current installation (wrapping some functionality sometimes)
* it also includes all FFMPEG header files at once, no need to do it
* separately.
*/
#pragma once
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/cpu.h>
#include <libavutil/display.h>
#include <libswscale/swscale.h>
/* Check if our FFMPEG is new enough, avoids user complaints.
* Minimum supported version is currently 3.2.0 which mean the following library versions:
* `libavutil` > 55.30
* `libavcodec` > 57.60
* `libavformat` > 57.50
*
* We only check for one of these as they are usually updated in tandem.
*/
#if (LIBAVFORMAT_VERSION_MAJOR < 57) || \
((LIBAVFORMAT_VERSION_MAJOR == 57) && (LIBAVFORMAT_VERSION_MINOR <= 50))
# error "FFmpeg 3.2.0 or newer is needed, Upgrade your FFmpeg or disable it"
#endif
/* end sanity check */
/* visual studio 2012 does not define inline for C */
#ifdef _MSC_VER
# define FFMPEG_INLINE static __inline
#else
# define FFMPEG_INLINE static inline
#endif
#if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(58, 29, 100)
/* In FFMPEG 6.1 usage of the "key_frame" variable from "AVFrame" has been deprecated.
* used the new method to query for the "AV_FRAME_FLAG_KEY" flag instead.
*/
# define FFMPEG_OLD_KEY_FRAME_QUERY_METHOD
#endif
#if (LIBAVFORMAT_VERSION_MAJOR < 59)
/* For versions older than FFMPEG 5.0, use the old channel layout variables.
* We intend to only keep this workaround for around two releases (3.5, 3.6).
* If it sticks around any longer, then we should consider refactoring this.
*/
# define FFMPEG_USE_OLD_CHANNEL_VARS
#endif
/* Threaded sws_scale_frame was added in FFMPEG 5.0 (`swscale` version 6.1). */
#if (LIBSWSCALE_VERSION_INT >= AV_VERSION_INT(6, 1, 100))
# define FFMPEG_SWSCALE_THREADING
#endif
/* AV_CODEC_CAP_AUTO_THREADS was renamed to AV_CODEC_CAP_OTHER_THREADS with
* upstream commit
* `github.com/FFmpeg/FFmpeg/commit/7d09579190def3ef7562399489e628f3b65714ce`
* (`lavc` 58.132.100) and removed with commit
* `github.com/FFmpeg/FFmpeg/commit/10c9a0874cb361336237557391d306d26d43f137`
* for FFMPEG 6.0.
*/
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 132, 100)
# define AV_CODEC_CAP_OTHER_THREADS AV_CODEC_CAP_AUTO_THREADS
#endif
#if (LIBAVFORMAT_VERSION_MAJOR < 58) || \
((LIBAVFORMAT_VERSION_MAJOR == 58) && (LIBAVFORMAT_VERSION_MINOR < 76))
# define FFMPEG_USE_DURATION_WORKAROUND 1
/* Before FFMPEG 4.4, package duration calculation used deprecated variables to calculate the
* packet duration. Use the function from commit
* `github.com/FFmpeg/FFmpeg/commit/1c0885334dda9ee8652e60c586fa2e3674056586`
* to calculate the correct frame-rate for FFMPEG < 4.4.
*/
FFMPEG_INLINE
void my_guess_pkt_duration(AVFormatContext *s, AVStream *st, AVPacket *pkt)
{
if (pkt->duration < 0 && st->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
av_log(s,
AV_LOG_WARNING,
"Packet with invalid duration %" PRId64 " in stream %d\n",
pkt->duration,
pkt->stream_index);
pkt->duration = 0;
}
if (pkt->duration) {
return;
}
switch (st->codecpar->codec_type) {
case AVMEDIA_TYPE_VIDEO:
if (st->avg_frame_rate.num > 0 && st->avg_frame_rate.den > 0) {
pkt->duration = av_rescale_q(1, av_inv_q(st->avg_frame_rate), st->time_base);
}
else if (st->time_base.num * 1000LL > st->time_base.den) {
pkt->duration = 1;
}
break;
case AVMEDIA_TYPE_AUDIO: {
int frame_size = av_get_audio_frame_duration2(st->codecpar, pkt->size);
if (frame_size && st->codecpar->sample_rate) {
pkt->duration = av_rescale_q(
frame_size, (AVRational){1, st->codecpar->sample_rate}, st->time_base);
}
break;
}
default:
break;
}
}
#endif
FFMPEG_INLINE
int64_t timestamp_from_pts_or_dts(int64_t pts, int64_t dts)
{
/* Some videos do not have any pts values, use dts instead in those cases if
* possible. Usually when this happens dts can act as pts because as all frames
* should then be presented in their decoded in order. IE pts == dts. */
if (pts == AV_NOPTS_VALUE) {
return dts;
}
return pts;
}
FFMPEG_INLINE
int64_t av_get_pts_from_frame(AVFrame *picture)
{
return timestamp_from_pts_or_dts(picture->pts, picture->pkt_dts);
}
/* Duration of the frame, in the same units as pts. 0 if unknown. */
FFMPEG_INLINE
int64_t av_get_frame_duration_in_pts_units(const AVFrame *picture)
{
#if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(57, 30, 100)
return picture->pkt_duration;
#else
return picture->duration;
#endif
}
FFMPEG_INLINE size_t ffmpeg_get_buffer_alignment()
{
/* NOTE: even if av_frame_get_buffer suggests to pass 0 for alignment,
* as of FFMPEG 6.1/7.0 it does not use correct alignment for AVX512
* CPU (frame.c get_video_buffer ends up always using 32 alignment,
* whereas it should have used 64). Reported upstream:
* https://trac.ffmpeg.org/ticket/11116 and the fix on their code
* side is to use 64 byte alignment as soon as AVX512 is compiled
* in (even if CPU might not support it). So play safe and
* use at least 64 byte alignment here too. Currently larger than
* 64 alignment would not happen anywhere, but keep on querying
* av_cpu_max_align just in case some future platform might. */
size_t align = av_cpu_max_align();
if (align < 64) {
align = 64;
}
return align;
}
FFMPEG_INLINE void ffmpeg_copy_display_matrix(const AVStream *src, AVStream *dst)
{
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 29, 100)
const AVPacketSideData *src_matrix = av_packet_side_data_get(src->codecpar->coded_side_data,
src->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX);
if (src_matrix != nullptr) {
uint8_t *dst_matrix = (uint8_t *)av_memdup(src_matrix->data, src_matrix->size);
av_packet_side_data_add(&dst->codecpar->coded_side_data,
&dst->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX,
dst_matrix,
src_matrix->size,
0);
}
#endif
}
FFMPEG_INLINE int ffmpeg_get_video_rotation(const AVStream *stream)
{
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 29, 100)
const AVPacketSideData *src_matrix = av_packet_side_data_get(
stream->codecpar->coded_side_data,
stream->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX);
if (src_matrix != nullptr) {
/* ffmpeg reports rotation in [-180..+180] range; our image rotation
* uses different direction and [0..360] range. */
double theta = -av_display_rotation_get((const int32_t *)src_matrix->data);
if (theta < 0.0) {
theta += 360.0;
}
return int(theta);
}
#endif
return 0;
}
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 13, 100)
FFMPEG_INLINE const enum AVPixelFormat *ffmpeg_get_pix_fmts(struct AVCodecContext *context,
const AVCodec *codec)
{
const enum AVPixelFormat *pix_fmts = nullptr;
avcodec_get_supported_config(
context, codec, AV_CODEC_CONFIG_PIX_FORMAT, 0, (const void **)&pix_fmts, nullptr);
return pix_fmts;
}
FFMPEG_INLINE const enum AVSampleFormat *ffmpeg_get_sample_fmts(struct AVCodecContext *context,
const AVCodec *codec)
{
const enum AVSampleFormat *sample_fmts = nullptr;
avcodec_get_supported_config(
context, codec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void **)&sample_fmts, nullptr);
return sample_fmts;
}
FFMPEG_INLINE const int *ffmpeg_get_sample_rates(struct AVCodecContext *context,
const AVCodec *codec)
{
const int *sample_rates = nullptr;
avcodec_get_supported_config(
context, codec, AV_CODEC_CONFIG_SAMPLE_RATE, 0, (const void **)&sample_rates, nullptr);
return sample_rates;
}
#else
FFMPEG_INLINE const enum AVPixelFormat *ffmpeg_get_pix_fmts(struct AVCodecContext * /*context*/,
const AVCodec *codec)
{
return codec->pix_fmts;
}
FFMPEG_INLINE const enum AVSampleFormat *ffmpeg_get_sample_fmts(
struct AVCodecContext * /*context*/, const AVCodec *codec)
{
return codec->sample_fmts;
}
FFMPEG_INLINE const int *ffmpeg_get_sample_rates(struct AVCodecContext * /*context*/,
const AVCodec *codec)
{
return codec->supported_samplerates;
}
#endif

View File

@@ -0,0 +1,271 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#ifdef WITH_FFMPEG
# include "ffmpeg_swscale.hh"
# include <cstdint>
# include <mutex>
# include "BLI_mutex.hh"
# include "BLI_vector.hh"
# include "movie_util.hh"
extern "C" {
# include <libavutil/opt.h>
# include <libavutil/pixfmt.h>
# include <libswscale/swscale.h>
# include "ffmpeg_compat.h"
}
namespace blender {
/* libswscale context creation and destruction is expensive.
* Maintain a cache of already created contexts. */
static constexpr int64_t swscale_cache_max_entries = 32;
struct SwscaleContext {
int src_width = 0, src_height = 0;
int dst_width = 0, dst_height = 0;
AVPixelFormat src_format = AV_PIX_FMT_NONE, dst_format = AV_PIX_FMT_NONE;
bool src_full_range = false, dst_full_range = false;
int src_colorspace = -1, dst_colorspace = -1;
int flags = 0;
SwsContext *context = nullptr;
int64_t last_use_timestamp = 0;
bool is_used = false;
};
static Mutex swscale_cache_lock;
static int64_t swscale_cache_timestamp = 0;
static Vector<SwscaleContext> *swscale_cache = nullptr;
static SwsContext *sws_create_context(int src_width,
int src_height,
int av_src_format,
int dst_width,
int dst_height,
int av_dst_format,
int sws_flags)
{
# if defined(FFMPEG_SWSCALE_THREADING)
/* sws_getContext does not allow passing flags that ask for multi-threaded
* scaling context, so do it the hard way. */
SwsContext *c = sws_alloc_context();
if (c == nullptr) {
return nullptr;
}
av_opt_set_int(c, "srcw", src_width, 0);
av_opt_set_int(c, "srch", src_height, 0);
av_opt_set_int(c, "src_format", av_src_format, 0);
av_opt_set_int(c, "dstw", dst_width, 0);
av_opt_set_int(c, "dsth", dst_height, 0);
av_opt_set_int(c, "dst_format", av_dst_format, 0);
av_opt_set_int(c, "sws_flags", sws_flags, 0);
av_opt_set_int(c, "threads", MOV_thread_count(), 0);
if (sws_init_context(c, nullptr, nullptr) < 0) {
sws_freeContext(c);
return nullptr;
}
# else
SwsContext *c = sws_getContext(src_width,
src_height,
AVPixelFormat(av_src_format),
dst_width,
dst_height,
AVPixelFormat(av_dst_format),
sws_flags,
nullptr,
nullptr,
nullptr);
# endif
return c;
}
static void init_swscale_cache_if_needed()
{
if (swscale_cache == nullptr) {
swscale_cache = new Vector<SwscaleContext>();
swscale_cache_timestamp = 0;
}
}
static bool remove_oldest_swscale_context()
{
int64_t oldest_index = -1;
int64_t oldest_time = 0;
for (int64_t index = 0; index < swscale_cache->size(); index++) {
SwscaleContext &ctx = (*swscale_cache)[index];
if (ctx.is_used) {
continue;
}
int64_t time = swscale_cache_timestamp - ctx.last_use_timestamp;
if (time > oldest_time) {
oldest_time = time;
oldest_index = index;
}
}
if (oldest_index >= 0) {
SwscaleContext &ctx = (*swscale_cache)[oldest_index];
sws_freeContext(ctx.context);
swscale_cache->remove_and_reorder(oldest_index);
return true;
}
return false;
}
static void maintain_swscale_cache_size()
{
while (swscale_cache->size() > swscale_cache_max_entries) {
if (!remove_oldest_swscale_context()) {
/* Could not remove anything (all contexts are actively used),
* stop trying. */
break;
}
}
}
SwsContext *ffmpeg_sws_get_context(int src_width,
int src_height,
int av_src_format,
bool src_full_range,
int src_color_space,
int dst_width,
int dst_height,
int av_dst_format,
bool dst_full_range,
int dst_color_space,
int sws_flags)
{
std::lock_guard lock(swscale_cache_lock);
init_swscale_cache_if_needed();
swscale_cache_timestamp++;
/* Search for unused context that has suitable parameters. */
SwsContext *ctx = nullptr;
for (SwscaleContext &c : *swscale_cache) {
if (!c.is_used && c.src_width == src_width && c.src_height == src_height &&
c.src_format == av_src_format && c.src_full_range == src_full_range &&
c.src_colorspace == src_color_space && c.dst_width == dst_width &&
c.dst_height == dst_height && c.dst_format == av_dst_format &&
c.dst_full_range == dst_full_range && c.dst_colorspace == dst_color_space &&
c.flags == sws_flags)
{
ctx = c.context;
/* Mark as used. */
c.is_used = true;
c.last_use_timestamp = swscale_cache_timestamp;
break;
}
}
if (ctx == nullptr) {
/* No free matching context in cache: create a new one. */
ctx = sws_create_context(
src_width, src_height, av_src_format, dst_width, dst_height, av_dst_format, sws_flags);
int src_range, dst_range, brightness, contrast, saturation;
const int *table, *inv_table;
if (sws_getColorspaceDetails(ctx,
(int **)&inv_table,
&src_range,
(int **)&table,
&dst_range,
&brightness,
&contrast,
&saturation) >= 0)
{
if (src_full_range) {
src_range = 1;
}
if (dst_full_range) {
dst_range = 1;
}
if (src_color_space >= 0) {
inv_table = sws_getCoefficients(src_color_space);
}
if (dst_color_space >= 0) {
table = sws_getCoefficients(dst_color_space);
}
sws_setColorspaceDetails(
ctx, (int *)inv_table, src_range, table, dst_range, brightness, contrast, saturation);
}
SwscaleContext c;
c.src_width = src_width;
c.src_height = src_height;
c.dst_width = dst_width;
c.dst_height = dst_height;
c.src_format = AVPixelFormat(av_src_format);
c.dst_format = AVPixelFormat(av_dst_format);
c.src_full_range = src_full_range;
c.dst_full_range = dst_full_range;
c.src_colorspace = src_color_space;
c.dst_colorspace = dst_color_space;
c.flags = sws_flags;
c.context = ctx;
c.is_used = true;
c.last_use_timestamp = swscale_cache_timestamp;
swscale_cache->append(c);
maintain_swscale_cache_size();
}
return ctx;
}
void ffmpeg_sws_release_context(SwsContext *ctx)
{
std::lock_guard lock(swscale_cache_lock);
init_swscale_cache_if_needed();
bool found = false;
for (SwscaleContext &c : *swscale_cache) {
if (c.context == ctx) {
BLI_assert_msg(c.is_used, "Releasing ffmpeg swscale context that is not in use");
c.is_used = false;
found = true;
break;
}
}
BLI_assert_msg(found, "Releasing ffmpeg swscale context that is not in cache");
UNUSED_VARS_NDEBUG(found);
maintain_swscale_cache_size();
}
void ffmpeg_sws_exit()
{
std::lock_guard lock(swscale_cache_lock);
if (swscale_cache != nullptr) {
for (SwscaleContext &c : *swscale_cache) {
sws_freeContext(c.context);
}
delete swscale_cache;
swscale_cache = nullptr;
}
}
void ffmpeg_sws_scale_frame(SwsContext *ctx, AVFrame *dst, const AVFrame *src)
{
# if defined(FFMPEG_SWSCALE_THREADING)
sws_scale_frame(ctx, dst, src);
# else
sws_scale(ctx, src->data, src->linesize, 0, src->height, dst->data, dst->linesize);
# endif
}
#endif /* WITH_FFMPEG */
} // namespace blender

View File

@@ -0,0 +1,53 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup imbuf
*/
#ifdef WITH_FFMPEG
struct AVFrame;
struct SwsContext;
namespace blender {
/**
* Gets a `libswscale` context for given size and format parameters.
* After you're done using the context, call #ffmpeg_sws_release_context
* to release it. Internally the contexts are coming from the context
* pool/cache.
*
* \param src_full_range: whether source uses full (pc/jpeg) range or limited (tv/mpeg) range.
*
* \param src_color_space: -1 for defaults, or AVColorSpace value to override
* sws_setColorspaceDetails `inv_table`.
*
* \param dst_full_range: whether destination uses full (pc/jpeg) range or limited (tv/mpeg) range.
*
* \param dst_color_space: -1 for defaults, or AVColorSpace value to override
* sws_setColorspaceDetails `table`.
*/
SwsContext *ffmpeg_sws_get_context(int src_width,
int src_height,
int av_src_format,
bool src_full_range,
int src_color_space,
int dst_width,
int dst_height,
int av_dst_format,
bool dst_full_range,
int dst_color_space,
int sws_flags);
void ffmpeg_sws_release_context(SwsContext *ctx);
void ffmpeg_sws_scale_frame(SwsContext *ctx, AVFrame *dst, const AVFrame *src);
void ffmpeg_sws_exit();
} // namespace blender
#endif /* WITH_FFMPEG */

View File

@@ -0,0 +1,959 @@
/* SPDX-FileCopyrightText: 2011 Peter Schlaile <peter [at] schlaile [dot] de>.
* SPDX-FileCopyrightText: 2024-2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#include <cstdlib>
#include "MEM_guardedalloc.h"
#include "BLI_endian_switch.h"
#include "BLI_fileops.h"
#include "BLI_math_base.h"
#include "BLI_math_base.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_string_utils.hh"
#include "BLI_time.h"
#include "BLI_utildefines.h"
#include "CLG_log.h"
#include "MOV_read.hh"
#include "ffmpeg_swscale.hh"
#include "movie_proxy_indexer.hh"
#include "movie_read.hh"
#include "movie_util.hh"
#ifdef WITH_FFMPEG
extern "C" {
# include "ffmpeg_compat.h"
# include <libavutil/imgutils.h>
}
#endif
namespace blender {
static CLG_LogRef LOG = {"video.proxy"};
static const IMB_Proxy_Size proxy_sizes[] = {
IMB_PROXY_25, IMB_PROXY_50, IMB_PROXY_75, IMB_PROXY_100};
static const float proxy_fac[] = {0.25, 0.50, 0.75, 1.00};
static int proxy_size_to_array_index(IMB_Proxy_Size pr_size)
{
switch (pr_size) {
case IMB_PROXY_NONE:
return -1;
case IMB_PROXY_25:
return 0;
case IMB_PROXY_50:
return 1;
case IMB_PROXY_75:
return 2;
case IMB_PROXY_100:
return 3;
default:
BLI_assert_msg(0, "Unhandled proxy size enum!");
return -1;
}
}
static void get_proxy_dir(const MovieReader *anim, char *proxy_dir, size_t proxy_dir_maxncpy)
{
if (!anim->proxy_dir[0]) {
char filename[FILE_MAXFILE];
char dirname[FILE_MAXDIR];
BLI_path_split_dir_file(anim->filepath, dirname, sizeof(dirname), filename, sizeof(filename));
BLI_path_join(proxy_dir, proxy_dir_maxncpy, dirname, "BL_proxy", filename);
}
else {
BLI_strncpy(proxy_dir, anim->proxy_dir, proxy_dir_maxncpy);
}
}
static bool get_proxy_filepath(const MovieReader *anim,
IMB_Proxy_Size preview_size,
char *filepath,
bool temp)
{
char proxy_dir[FILE_MAXDIR];
int i = proxy_size_to_array_index(preview_size);
BLI_assert(i >= 0);
char proxy_name[FILE_MAXFILE];
char stream_suffix[20];
const char *name = (temp) ? "proxy_%d%s_part.avi" : "proxy_%d%s.avi";
stream_suffix[0] = 0;
if (anim->streamindex > 0) {
SNPRINTF(stream_suffix, "_st%d", anim->streamindex);
}
SNPRINTF(proxy_name, name, int(proxy_fac[i] * 100), stream_suffix, anim->suffix);
get_proxy_dir(anim, proxy_dir, sizeof(proxy_dir));
if (BLI_path_ncmp(anim->filepath, proxy_dir, FILE_MAXDIR) == 0) {
return false;
}
BLI_path_join(filepath, FILE_MAXFILE + FILE_MAXDIR, proxy_dir, proxy_name);
return true;
}
/* ----------------------------------------------------------------------
* - ffmpeg rebuilder
* ---------------------------------------------------------------------- */
#ifdef WITH_FFMPEG
struct proxy_output_ctx {
AVFormatContext *of;
AVStream *st;
AVCodecContext *c;
const AVCodec *codec;
SwsContext *sws_ctx;
AVFrame *frame;
int cfra;
AVRational output_timebase;
IMB_Proxy_Size proxy_size;
int orig_height;
MovieReader *anim;
};
static proxy_output_ctx *alloc_proxy_output_ffmpeg(MovieReader *anim,
AVCodecContext *codec_ctx,
AVStream *st,
IMB_Proxy_Size proxy_size,
int width,
int height,
int quality)
{
proxy_output_ctx *rv = MEM_new_zeroed<proxy_output_ctx>("alloc_proxy_output");
char filepath[FILE_MAX];
rv->proxy_size = proxy_size;
rv->anim = anim;
get_proxy_filepath(rv->anim, rv->proxy_size, filepath, true);
if (!BLI_file_ensure_parent_dir_exists(filepath)) {
MEM_delete(rv);
return nullptr;
}
rv->of = avformat_alloc_context();
/* Note: we keep on using .avi extension for proxies,
* but actual container can not be AVI, since it does not support
* video rotation metadata. */
rv->of->oformat = av_guess_format("mp4", nullptr, nullptr);
rv->of->url = av_strdup(filepath);
rv->st = avformat_new_stream(rv->of, nullptr);
rv->st->id = 0;
rv->codec = avcodec_find_encoder(AV_CODEC_ID_H264);
rv->c = avcodec_alloc_context3(rv->codec);
if (!rv->codec) {
CLOG_ERROR(&LOG, "Could not build proxy '%s': failed to create video encoder", filepath);
avcodec_free_context(&rv->c);
avformat_free_context(rv->of);
MEM_delete(rv);
return nullptr;
}
rv->c->width = width;
rv->c->height = height;
rv->c->gop_size = 10;
rv->c->max_b_frames = 0;
const enum AVPixelFormat *pix_fmts = ffmpeg_get_pix_fmts(rv->c, rv->codec);
if (pix_fmts) {
rv->c->pix_fmt = pix_fmts[0];
}
else {
rv->c->pix_fmt = AV_PIX_FMT_YUVJ420P;
}
rv->c->sample_aspect_ratio = rv->st->sample_aspect_ratio = st->sample_aspect_ratio;
/* Use same output timebase as input: we seek within the proxy file
* using exact same frame numbers as if it was original file. So we want to
* match original frame-rate, plus any variable frames in the source file. */
rv->output_timebase = st->time_base;
rv->c->time_base = st->time_base;
rv->st->time_base = st->time_base;
rv->st->avg_frame_rate = st->avg_frame_rate;
/* This range matches #eFFMpegCrf. `crf_range_min` corresponds to lowest quality,
* `crf_range_max` to highest quality. */
const int crf_range_min = 32;
const int crf_range_max = 17;
int crf = round_fl_to_int((quality / 100.0f) * (crf_range_max - crf_range_min) + crf_range_min);
AVDictionary *codec_opts = nullptr;
/* High quality preset value. */
av_dict_set_int(&codec_opts, "crf", crf, 0);
/* Prefer smaller file-size. Presets from `veryslow` to `veryfast` produce output with very
* similar file-size, but there is big difference in performance.
* In some cases `veryfast` preset will produce smallest file-size. */
av_dict_set(&codec_opts, "preset", "veryfast", 0);
av_dict_set(&codec_opts, "tune", "fastdecode", 0);
if (rv->codec->capabilities & AV_CODEC_CAP_OTHER_THREADS) {
rv->c->thread_count = 0;
}
else {
rv->c->thread_count = MOV_thread_count();
}
if (rv->codec->capabilities & AV_CODEC_CAP_FRAME_THREADS) {
rv->c->thread_type = FF_THREAD_FRAME;
}
else if (rv->codec->capabilities & AV_CODEC_CAP_SLICE_THREADS) {
rv->c->thread_type = FF_THREAD_SLICE;
}
if (rv->of->oformat->flags & AVFMT_GLOBALHEADER) {
rv->c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
rv->c->color_range = codec_ctx->color_range;
rv->c->color_primaries = codec_ctx->color_primaries;
rv->c->color_trc = codec_ctx->color_trc;
rv->c->colorspace = codec_ctx->colorspace;
int ret = avio_open(&rv->of->pb, filepath, AVIO_FLAG_WRITE);
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG,
"Could not build proxy '%s': failed to create output file (%s)",
filepath,
error_str);
avcodec_free_context(&rv->c);
avformat_free_context(rv->of);
MEM_delete(rv);
return nullptr;
}
ret = avcodec_open2(rv->c, rv->codec, &codec_opts);
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(
&LOG, "Could not build proxy '%s': failed to open video codec (%s)", filepath, error_str);
avcodec_free_context(&rv->c);
avformat_free_context(rv->of);
MEM_delete(rv);
return nullptr;
}
avcodec_parameters_from_context(rv->st->codecpar, rv->c);
ffmpeg_copy_display_matrix(st, rv->st);
rv->orig_height = st->codecpar->height;
if (st->codecpar->width != width || st->codecpar->height != height ||
st->codecpar->format != rv->c->pix_fmt)
{
const size_t align = ffmpeg_get_buffer_alignment();
rv->frame = av_frame_alloc();
rv->frame->format = rv->c->pix_fmt;
rv->frame->width = width;
rv->frame->height = height;
av_frame_get_buffer(rv->frame, align);
rv->sws_ctx = ffmpeg_sws_get_context(st->codecpar->width,
rv->orig_height,
AVPixelFormat(st->codecpar->format),
codec_ctx->color_range == AVCOL_RANGE_JPEG,
-1,
width,
height,
rv->c->pix_fmt,
codec_ctx->color_range == AVCOL_RANGE_JPEG,
-1,
SWS_FAST_BILINEAR);
}
ret = avformat_write_header(rv->of, nullptr);
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(
&LOG, "Could not build proxy '%s': failed to write header (%s)", filepath, error_str);
if (rv->frame) {
av_frame_free(&rv->frame);
}
avcodec_free_context(&rv->c);
avformat_free_context(rv->of);
MEM_delete(rv);
return nullptr;
}
return rv;
}
static void add_to_proxy_output_ffmpeg(proxy_output_ctx *ctx,
AVFrame *frame,
AVRational input_timebase)
{
if (!ctx) {
return;
}
const int64_t src_pts = frame ? frame->pts : AV_NOPTS_VALUE;
if (ctx->sws_ctx && frame &&
(frame->data[0] || frame->data[1] || frame->data[2] || frame->data[3]))
{
ffmpeg_sws_scale_frame(ctx->sws_ctx, ctx->frame, frame);
}
frame = ctx->sws_ctx ? (frame ? ctx->frame : nullptr) : frame;
if (frame) {
if (src_pts != AV_NOPTS_VALUE) {
frame->pts = av_rescale_q(src_pts, input_timebase, ctx->output_timebase);
}
else {
frame->pts = ctx->cfra;
}
ctx->cfra++;
}
int ret = avcodec_send_frame(ctx->c, frame);
if (ret < 0) {
/* Can't send frame to encoder. This shouldn't happen. */
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(
&LOG, "Building proxy '%s': failed to send video frame (%s)", ctx->of->url, error_str);
return;
}
AVPacket *packet = av_packet_alloc();
while (ret >= 0) {
ret = avcodec_receive_packet(ctx->c, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
/* No more packets to flush. */
break;
}
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG,
"Building proxy '%s': error encoding frame #%i (%s)",
ctx->of->url,
ctx->cfra - 1,
error_str);
break;
}
packet->stream_index = ctx->st->index;
av_packet_rescale_ts(packet, ctx->c->time_base, ctx->st->time_base);
# ifdef FFMPEG_USE_DURATION_WORKAROUND
my_guess_pkt_duration(ctx->of, ctx->st, packet);
# endif
int write_ret = av_interleaved_write_frame(ctx->of, packet);
if (write_ret != 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, write_ret);
CLOG_ERROR(&LOG,
"Building proxy '%s': error writing frame #%i (%s)",
ctx->of->url,
ctx->cfra - 1,
error_str);
break;
}
}
av_packet_free(&packet);
}
static void free_proxy_output_ffmpeg(proxy_output_ctx *ctx, int rollback)
{
char filepath[FILE_MAX];
char filepath_tmp[FILE_MAX];
if (!ctx) {
return;
}
if (!rollback) {
/* Flush the remaining packets. */
add_to_proxy_output_ffmpeg(ctx, nullptr, {1, 1});
}
av_write_trailer(ctx->of);
if (ctx->of->oformat) {
if (!(ctx->of->oformat->flags & AVFMT_NOFILE)) {
avio_close(ctx->of->pb);
}
}
avcodec_free_context(&ctx->c);
avformat_free_context(ctx->of);
if (ctx->sws_ctx) {
ffmpeg_sws_release_context(ctx->sws_ctx);
ctx->sws_ctx = nullptr;
}
if (ctx->frame) {
av_frame_free(&ctx->frame);
}
get_proxy_filepath(ctx->anim, ctx->proxy_size, filepath_tmp, true);
if (rollback) {
BLI_delete(filepath_tmp, false, false);
}
else {
get_proxy_filepath(ctx->anim, ctx->proxy_size, filepath, false);
BLI_rename_overwrite(filepath_tmp, filepath);
}
MEM_delete(ctx);
}
struct MovieProxyBuilder {
AVFormatContext *iFormatCtx;
AVCodecContext *iCodecCtx;
const AVCodec *iCodec;
AVStream *iStream;
int videoStream;
int num_proxy_sizes;
proxy_output_ctx *proxy_ctx[IMB_PROXY_MAX_SLOT];
int proxy_sizes_in_use;
bool build_only_on_bad_performance;
bool building_cancelled;
};
static MovieProxyBuilder *proxy_builder_create(MovieReader *anim,
int proxy_sizes_in_use,
int quality,
bool build_only_on_bad_performance)
{
/* Never build proxies for un-seekable single frame files. */
if (anim->never_seek_decode_one_frame) {
return nullptr;
}
MovieProxyBuilder *context = MEM_new_zeroed<MovieProxyBuilder>(__func__);
int num_proxy_sizes = IMB_PROXY_MAX_SLOT;
int i, streamcount;
context->proxy_sizes_in_use = proxy_sizes_in_use;
context->num_proxy_sizes = IMB_PROXY_MAX_SLOT;
context->build_only_on_bad_performance = build_only_on_bad_performance;
memset(context->proxy_ctx, 0, sizeof(context->proxy_ctx));
if (avformat_open_input(&context->iFormatCtx, anim->filepath, nullptr, nullptr) != 0) {
MEM_delete(context);
return nullptr;
}
if (avformat_find_stream_info(context->iFormatCtx, nullptr) < 0) {
avformat_close_input(&context->iFormatCtx);
MEM_delete(context);
return nullptr;
}
streamcount = anim->streamindex;
/* Find the video stream */
context->videoStream = -1;
for (i = 0; i < context->iFormatCtx->nb_streams; i++) {
if (context->iFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
if (streamcount > 0) {
streamcount--;
continue;
}
context->videoStream = i;
break;
}
}
if (context->videoStream == -1) {
avformat_close_input(&context->iFormatCtx);
MEM_delete(context);
return nullptr;
}
context->iStream = context->iFormatCtx->streams[context->videoStream];
context->iCodec = avcodec_find_decoder(context->iStream->codecpar->codec_id);
if (context->iCodec == nullptr) {
avformat_close_input(&context->iFormatCtx);
MEM_delete(context);
return nullptr;
}
context->iCodecCtx = avcodec_alloc_context3(nullptr);
avcodec_parameters_to_context(context->iCodecCtx, context->iStream->codecpar);
context->iCodecCtx->workaround_bugs = FF_BUG_AUTODETECT;
if (context->iCodec->capabilities & AV_CODEC_CAP_OTHER_THREADS) {
context->iCodecCtx->thread_count = 0;
}
else {
context->iCodecCtx->thread_count = MOV_thread_count();
}
if (context->iCodec->capabilities & AV_CODEC_CAP_FRAME_THREADS) {
context->iCodecCtx->thread_type = FF_THREAD_FRAME;
}
else if (context->iCodec->capabilities & AV_CODEC_CAP_SLICE_THREADS) {
context->iCodecCtx->thread_type = FF_THREAD_SLICE;
}
if (avcodec_open2(context->iCodecCtx, context->iCodec, nullptr) < 0) {
avformat_close_input(&context->iFormatCtx);
avcodec_free_context(&context->iCodecCtx);
MEM_delete(context);
return nullptr;
}
for (i = 0; i < num_proxy_sizes; i++) {
if (proxy_sizes_in_use & proxy_sizes[i]) {
int width = context->iCodecCtx->width * proxy_fac[i];
int height = context->iCodecCtx->height * proxy_fac[i];
width += width % 2;
height += height % 2;
context->proxy_ctx[i] = alloc_proxy_output_ffmpeg(
anim, context->iCodecCtx, context->iStream, proxy_sizes[i], width, height, quality);
if (!context->proxy_ctx[i]) {
proxy_sizes_in_use &= ~int(proxy_sizes[i]);
}
}
}
if (context->proxy_ctx[0] == nullptr && context->proxy_ctx[1] == nullptr &&
context->proxy_ctx[2] == nullptr && context->proxy_ctx[3] == nullptr)
{
avformat_close_input(&context->iFormatCtx);
avcodec_free_context(&context->iCodecCtx);
MEM_delete(context);
return nullptr; /* Nothing to transcode. */
}
return context;
}
static void proxy_builder_finish(MovieProxyBuilder *context, const bool stop)
{
const bool do_rollback = stop || context->building_cancelled;
for (int i = 0; i < context->num_proxy_sizes; i++) {
if (context->proxy_sizes_in_use & proxy_sizes[i]) {
free_proxy_output_ffmpeg(context->proxy_ctx[i], do_rollback);
}
}
avcodec_free_context(&context->iCodecCtx);
avformat_close_input(&context->iFormatCtx);
MEM_delete(context);
}
static void proxy_builder_proc_decoded_frame(MovieProxyBuilder *context, AVFrame *in_frame)
{
for (int i = 0; i < context->num_proxy_sizes; i++) {
add_to_proxy_output_ffmpeg(context->proxy_ctx[i], in_frame, context->iStream->time_base);
}
}
static int proxy_builder_process(MovieProxyBuilder *context,
const bool *stop,
bool *do_update,
const FunctionRef<void(float progress)> set_progress_fn)
{
AVFrame *in_frame = av_frame_alloc();
AVPacket *next_packet = av_packet_alloc();
uint64_t stream_size = avio_size(context->iFormatCtx->pb);
float progress = 0.0f;
while (av_read_frame(context->iFormatCtx, next_packet) >= 0) {
float next_progress =
float(int(floor(double(next_packet->pos) * 100 / double(stream_size) + 0.5))) / 100;
if (progress != next_progress) {
progress = next_progress;
*do_update = true;
if (set_progress_fn) {
set_progress_fn(progress);
}
}
if (*stop) {
break;
}
if (next_packet->stream_index == context->videoStream) {
int ret = avcodec_send_packet(context->iCodecCtx, next_packet);
while (ret >= 0) {
ret = avcodec_receive_frame(context->iCodecCtx, in_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
/* No more frames to flush. */
break;
}
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG, "Error decoding proxy frame: %s", error_str);
break;
}
proxy_builder_proc_decoded_frame(context, in_frame);
}
}
av_packet_unref(next_packet);
}
/* process pictures still stuck in decoder engine after EOF
* according to ffmpeg docs using nullptr packets.
*
* At least, if we haven't already stopped... */
if (!*stop) {
int ret = avcodec_send_packet(context->iCodecCtx, nullptr);
while (ret >= 0) {
ret = avcodec_receive_frame(context->iCodecCtx, in_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
/* No more frames to flush. */
break;
}
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG, "Error flushing proxy frame: %s", error_str);
break;
}
proxy_builder_proc_decoded_frame(context, in_frame);
}
}
av_packet_free(&next_packet);
av_free(in_frame);
return 1;
}
/* Get number of frames, that can be decoded in specified time period. */
static int performance_get_decode_rate(MovieProxyBuilder *context, const double time_period)
{
AVFrame *in_frame = av_frame_alloc();
AVPacket *packet = av_packet_alloc();
const double start = BLI_time_now_seconds();
int frames_decoded = 0;
while (av_read_frame(context->iFormatCtx, packet) >= 0) {
if (packet->stream_index != context->videoStream) {
av_packet_unref(packet);
continue;
}
int ret = avcodec_send_packet(context->iCodecCtx, packet);
while (ret >= 0) {
ret = avcodec_receive_frame(context->iCodecCtx, in_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG, "Error decoding proxy frame: %s", error_str);
break;
}
frames_decoded++;
}
const double end = BLI_time_now_seconds();
if (end > start + time_period) {
break;
}
av_packet_unref(packet);
}
av_packet_free(&packet);
av_frame_free(&in_frame);
avcodec_flush_buffers(context->iCodecCtx);
av_seek_frame(context->iFormatCtx, -1, 0, AVSEEK_FLAG_BACKWARD);
return frames_decoded;
}
/* Read up to 10k movie packets and return max GOP size detected.
* Number of packets is arbitrary. It should be as large as possible, but processed within
* reasonable time period, so detected GOP size is as close to real as possible. */
static int performance_get_max_gop_size(MovieProxyBuilder *context)
{
AVPacket *packet = av_packet_alloc();
const int packets_max = 10000;
int packet_index = 0;
int max_gop = 0;
int cur_gop = 0;
while (av_read_frame(context->iFormatCtx, packet) >= 0) {
if (packet->stream_index != context->videoStream) {
av_packet_unref(packet);
continue;
}
packet_index++;
cur_gop++;
if (packet->flags & AV_PKT_FLAG_KEY) {
max_gop = max_ii(max_gop, cur_gop);
cur_gop = 0;
}
if (packet_index > packets_max) {
break;
}
av_packet_unref(packet);
}
av_packet_free(&packet);
av_seek_frame(context->iFormatCtx, -1, 0, AVSEEK_FLAG_BACKWARD);
return max_gop;
}
/* Assess scrubbing performance of provided file. This function is not meant to be very exact.
* It compares number of frames decoded in reasonable time with largest detected GOP size.
* Because seeking happens in single GOP, it means, that maximum seek time can be detected this
* way.
* Since proxies use GOP size of 10 frames, skip building if detected GOP size is less or
* equal.
*/
static bool need_to_build_proxy(MovieProxyBuilder *context)
{
if (!context->build_only_on_bad_performance) {
return true;
}
/* Make sure, that file is not cold read. */
performance_get_decode_rate(context, 0.1);
/* Get decode rate per 100ms. This is arbitrary, but seems to be good baseline cadence of
* seeking. */
const int decode_rate = performance_get_decode_rate(context, 0.1);
const int max_gop_size = performance_get_max_gop_size(context);
if (max_gop_size <= 10 || max_gop_size < decode_rate) {
CLOG_INFO_NOCHECK(&LOG,
"Skipping proxy building for %s: Decoding performance is already good.",
context->iFormatCtx->url);
context->building_cancelled = true;
return false;
}
return true;
}
#endif
/* ----------------------------------------------------------------------
* - public API
* ---------------------------------------------------------------------- */
MovieProxyBuilder *MOV_proxy_builder_start(MovieReader *anim,
int proxy_sizes_in_use,
int quality,
const bool overwrite,
Set<std::string> *processed_paths,
bool build_only_on_bad_performance)
{
int proxy_sizes_to_build = proxy_sizes_in_use;
/* Check which proxies are going to be generated in this session already. */
if (processed_paths != nullptr) {
for (int i = 0; i < IMB_PROXY_MAX_SLOT; i++) {
IMB_Proxy_Size proxy_size = proxy_sizes[i];
if ((proxy_size & proxy_sizes_to_build) == 0) {
continue;
}
char filepath[FILE_MAX];
if (!get_proxy_filepath(anim, proxy_size, filepath, false)) {
return nullptr;
}
if (!processed_paths->add(filepath)) {
proxy_sizes_to_build &= ~int(proxy_size);
}
}
}
/* When not overwriting existing proxies, skip the ones that already exist. */
if (!overwrite) {
int built_proxies = MOV_get_existing_proxies(anim);
if (built_proxies != 0) {
for (int i = 0; i < IMB_PROXY_MAX_SLOT; i++) {
IMB_Proxy_Size proxy_size = proxy_sizes[i];
if (proxy_size & built_proxies) {
char filepath[FILE_MAX];
if (!get_proxy_filepath(anim, proxy_size, filepath, false)) {
return nullptr;
}
CLOG_INFO_NOCHECK(&LOG, "Skipping proxy: %s", filepath);
}
}
}
proxy_sizes_to_build &= ~built_proxies;
}
if (proxy_sizes_to_build == 0) {
return nullptr;
}
MovieProxyBuilder *context = nullptr;
#ifdef WITH_FFMPEG
if (anim->state == MovieReader::State::Valid) {
context = proxy_builder_create(
anim, proxy_sizes_to_build, quality, build_only_on_bad_performance);
}
#else
UNUSED_VARS(build_only_on_bad_performance);
#endif
return context;
UNUSED_VARS(proxy_sizes_in_use, quality);
}
void MOV_proxy_builder_process(MovieProxyBuilder *context,
/* NOLINTNEXTLINE: readability-non-const-parameter. */
const bool *stop,
/* NOLINTNEXTLINE: readability-non-const-parameter. */
bool *do_update,
const FunctionRef<void(float progress)> set_progress_fn)
{
#ifdef WITH_FFMPEG
if (context != nullptr) {
if (need_to_build_proxy(context)) {
proxy_builder_process(context, stop, do_update, set_progress_fn);
}
}
#endif
UNUSED_VARS(context, stop, do_update, set_progress_fn);
}
void MOV_proxy_builder_finish(MovieProxyBuilder *context, const bool stop)
{
#ifdef WITH_FFMPEG
if (context != nullptr) {
proxy_builder_finish(context, stop);
}
#endif
/* static defined at top of the file */
UNUSED_VARS(context, stop, proxy_sizes);
}
void MOV_close_proxies(MovieReader *anim)
{
if (anim == nullptr) {
return;
}
for (int i = 0; i < IMB_PROXY_MAX_SLOT; i++) {
if (anim->proxy_anim[i]) {
MOV_close(anim->proxy_anim[i]);
anim->proxy_anim[i] = nullptr;
}
}
anim->proxies_tried = 0;
}
void MOV_set_custom_proxy_dir(MovieReader *anim, const char *dir)
{
if (STREQ(anim->proxy_dir, dir)) {
return;
}
STRNCPY(anim->proxy_dir, dir);
MOV_close_proxies(anim);
}
MovieReader *movie_open_proxy(MovieReader *anim, IMB_Proxy_Size preview_size)
{
char filepath[FILE_MAX];
int i = proxy_size_to_array_index(preview_size);
if (i < 0) {
return nullptr;
}
if (anim->proxy_anim[i]) {
return anim->proxy_anim[i];
}
if (anim->proxies_tried & preview_size) {
return nullptr;
}
get_proxy_filepath(anim, preview_size, filepath, false);
/* Proxies are generated in the same color space as animation itself.
*
* Also skip any colorspace conversion to the color pipeline design as it helps performance and
* the image buffers from the proxy builder are not used anywhere else in Blender. */
anim->proxy_anim[i] = MOV_open_file(filepath, ImBufFlags::Zero, 0, true, anim->colorspace);
anim->proxies_tried |= preview_size;
return anim->proxy_anim[i];
}
int MOV_get_existing_proxies(const MovieReader *anim)
{
const int num_proxy_sizes = IMB_PROXY_MAX_SLOT;
int existing = IMB_PROXY_NONE;
for (int i = 0; i < num_proxy_sizes; i++) {
IMB_Proxy_Size proxy_size = proxy_sizes[i];
char filepath[FILE_MAX];
get_proxy_filepath(anim, proxy_size, filepath, false);
if (BLI_exists(filepath)) {
existing |= int(proxy_size);
}
}
return existing;
}
} // namespace blender

View File

@@ -0,0 +1,19 @@
/* SPDX-FileCopyrightText: 2023-2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#pragma once
#include "IMB_imbuf_enums.h"
namespace blender {
struct MovieReader;
MovieReader *movie_open_proxy(MovieReader *anim, IMB_Proxy_Size preview_size);
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2024-2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#pragma once
#include <cstdint>
#include "IMB_imbuf_enums.h"
struct AVFormatContext;
struct AVCodecContext;
struct AVCodec;
struct AVFrame;
struct AVPacket;
struct SwsContext;
#ifdef WITH_FFMPEG
extern "C" {
# include <libavutil/rational.h>
}
#endif
namespace blender {
struct IDProperty;
struct MovieReader {
enum class State { Uninitialized, Failed, Valid };
ImBufFlags ib_flags = ImBufFlags::Zero;
State state = State::Uninitialized;
int cur_position = 0; /* index 0 = 1e, 1 = 2e, enz. */
int duration_in_frames = 0;
int frs_sec = 0;
double frs_sec_base = 0.0;
double start_offset = 0.0;
int x = 0;
int y = 0;
int video_rotation = 0;
/* for number */
char filepath[/*FILE_MAX*/ 1024] = {};
int streamindex = 0;
bool keep_original_colorspace = false;
#ifdef WITH_FFMPEG
AVFormatContext *pFormatCtx = nullptr;
AVCodecContext *pCodecCtx = nullptr;
const AVCodec *pCodec = nullptr;
AVFrame *pFrameRGB = nullptr;
AVFrame *pFrameDeinterlaced = nullptr;
SwsContext *img_convert_ctx = nullptr;
int videoStream = 0;
AVFrame *pFrame = nullptr;
bool pFrame_complete = false;
AVFrame *pFrame_backup = nullptr;
bool pFrame_backup_complete = false;
int64_t cur_pts = 0;
int64_t cur_key_frame_pts = 0;
AVPacket *cur_packet = nullptr;
AVRational frame_rate = {1, 1};
bool seek_before_decode = false;
bool is_float = false;
/* When set, never seek within the video, and only ever decode one frame.
* This is a workaround for some Ogg files that have full audio but only
* one frame of "album art" as a video stream in non-Theora format.
* ffmpeg crashes/aborts when trying to seek within them
* (https://trac.ffmpeg.org/ticket/10755). */
bool never_seek_decode_one_frame = false;
#endif
char proxy_dir[768] = {};
int proxies_tried = 0;
MovieReader *proxy_anim[IMB_PROXY_MAX_SLOT] = {};
char colorspace[/*MAX_COLORSPACE_NAME*/ 64] = {};
/** The maximum name from multi-view. */
char suffix[/*MAX_NAME*/ 64] = {};
IDProperty *metadata = nullptr;
};
} // namespace blender

View File

@@ -0,0 +1,656 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#include "BLI_path_utils.hh"
#include "BLI_threads.h"
#include "BLI_utildefines.h"
#include "CLG_log.h"
#include "DNA_scene_types.h"
#include "MOV_enums.hh"
#include "MOV_util.hh"
#include "ffmpeg_swscale.hh"
#include "movie_util.hh"
#include <mutex>
#ifdef WITH_FFMPEG
# include "BLI_string.h"
extern "C" {
# include "ffmpeg_compat.h"
# include <libavcodec/avcodec.h>
# include <libavdevice/avdevice.h>
# include <libavformat/avformat.h>
# include <libavutil/log.h>
}
#endif
namespace blender {
#ifdef WITH_FFMPEG
static CLG_LogRef LOG = {"video.ffmpeg"};
static char ffmpeg_last_error_buffer[1024];
/* BLI_vsnprintf in ffmpeg_log_callback() causes invalid warning */
# ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wmissing-format-attribute"
# endif
static size_t ffmpeg_log_to_buffer(char *buffer,
const size_t buffer_size,
const char *format,
va_list arg)
{
va_list args_cpy;
size_t n;
va_copy(args_cpy, arg);
n = BLI_vsnprintf(buffer, buffer_size, format, args_cpy);
va_end(args_cpy);
return n;
}
static void ffmpeg_log_callback(void * /*ptr*/, int level, const char *format, va_list arg)
{
CLG_Level clg_level;
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
/* ffmpeg "fatal" should not quit whole Blender; report as error. */
clg_level = CLG_LEVEL_ERROR;
break;
case AV_LOG_ERROR:
case AV_LOG_WARNING:
case AV_LOG_INFO:
/* Note: most ffmpeg internal errors/warnings are not actionable; treat them as "info"
* log level. */
clg_level = CLG_LEVEL_INFO;
break;
case AV_LOG_VERBOSE:
case AV_LOG_DEBUG:
clg_level = CLG_LEVEL_DEBUG;
break;
case AV_LOG_TRACE:
default:
clg_level = CLG_LEVEL_TRACE;
break;
}
static std::mutex mutex;
std::scoped_lock lock(mutex);
if (ELEM(level, AV_LOG_PANIC, AV_LOG_FATAL, AV_LOG_ERROR)) {
const size_t n = ffmpeg_log_to_buffer(
ffmpeg_last_error_buffer, sizeof(ffmpeg_last_error_buffer), format, arg);
/* Strip trailing \n. */
ffmpeg_last_error_buffer[n - 1] = '\0';
}
if (CLOG_CHECK(&LOG, clg_level)) {
/* FFmpeg calls this multiple times without a line ending, so accumulate until
* we reach a line ending. This will not work well with multithreading, but the
* output would be garbled either way. */
static char buffer[1024];
static int buffer_used = 0;
buffer_used += ffmpeg_log_to_buffer(
buffer + buffer_used, sizeof(buffer) - buffer_used, format, arg);
if (buffer_used >= sizeof(buffer) || (buffer_used > 0 && buffer[buffer_used - 1] == '\n')) {
if (buffer[buffer_used - 1] == '\n') {
buffer[buffer_used - 1] = '\0';
}
CLOG_STR_AT_LEVEL(&LOG, clg_level, buffer);
buffer_used = 0;
}
}
}
# ifdef __GNUC__
# pragma GCC diagnostic pop
# endif
const char *ffmpeg_last_error()
{
return ffmpeg_last_error_buffer;
}
static int isffmpeg(const char *filepath)
{
AVFormatContext *pFormatCtx = nullptr;
uint i;
int videoStream;
const AVCodec *pCodec;
if (BLI_path_extension_check_n(filepath,
".swf",
".jpg",
".jp2",
".j2c",
".png",
".dds",
".tga",
".bmp",
".tif",
".exr",
".cin",
".wav",
nullptr))
{
return 0;
}
if (avformat_open_input(&pFormatCtx, filepath, nullptr, nullptr) != 0) {
return 0;
}
if (avformat_find_stream_info(pFormatCtx, nullptr) < 0) {
avformat_close_input(&pFormatCtx);
return 0;
}
/* Find the first video stream */
videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i] && pFormatCtx->streams[i]->codecpar &&
(pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO))
{
videoStream = i;
break;
}
}
if (videoStream == -1) {
avformat_close_input(&pFormatCtx);
return 0;
}
AVCodecParameters *codec_par = pFormatCtx->streams[videoStream]->codecpar;
/* Find the decoder for the video stream */
pCodec = avcodec_find_decoder(codec_par->codec_id);
if (pCodec == nullptr) {
avformat_close_input(&pFormatCtx);
return 0;
}
avformat_close_input(&pFormatCtx);
return 1;
}
/* -------------------------------------------------------------------- */
/* AVFrame de-interlacing. Code for this was originally based on FFMPEG 2.6.4 (LGPL). */
# define MAX_NEG_CROP 1024
# define times4(x) x, x, x, x
# define times256(x) times4(times4(times4(times4(times4(x)))))
static const uint8_t ff_compat_crop_tab[256 + 2 * MAX_NEG_CROP] = {
times256(0x00), 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22,
0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E,
0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46,
0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E,
0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A,
0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82,
0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E,
0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A,
0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6,
0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2,
0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE,
0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA,
0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6,
0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2,
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE,
0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA,
0xFB, 0xFC, 0xFD, 0xFE, 0xFF, times256(0xFF)};
# undef times4
# undef times256
/* filter parameters: [-1 4 2 4 -1] // 8 */
FFMPEG_INLINE void deinterlace_line(uint8_t *dst,
const uint8_t *lum_m4,
const uint8_t *lum_m3,
const uint8_t *lum_m2,
const uint8_t *lum_m1,
const uint8_t *lum,
int size)
{
const uint8_t *cm = ff_compat_crop_tab + MAX_NEG_CROP;
int sum;
for (; size > 0; size--) {
sum = -lum_m4[0];
sum += lum_m3[0] << 2;
sum += lum_m2[0] << 1;
sum += lum_m1[0] << 2;
sum += -lum[0];
dst[0] = cm[(sum + 4) >> 3];
lum_m4++;
lum_m3++;
lum_m2++;
lum_m1++;
lum++;
dst++;
}
}
FFMPEG_INLINE void deinterlace_line_inplace(
uint8_t *lum_m4, uint8_t *lum_m3, uint8_t *lum_m2, uint8_t *lum_m1, uint8_t *lum, int size)
{
const uint8_t *cm = ff_compat_crop_tab + MAX_NEG_CROP;
int sum;
for (; size > 0; size--) {
sum = -lum_m4[0];
sum += lum_m3[0] << 2;
sum += lum_m2[0] << 1;
lum_m4[0] = lum_m2[0];
sum += lum_m1[0] << 2;
sum += -lum[0];
lum_m2[0] = cm[(sum + 4) >> 3];
lum_m4++;
lum_m3++;
lum_m2++;
lum_m1++;
lum++;
}
}
/**
* De-interlacing: 2 temporal taps, 3 spatial taps linear filter.
* The top field is copied as is, but the bottom field is de-interlaced against the top field.
*/
FFMPEG_INLINE void deinterlace_bottom_field(
uint8_t *dst, int dst_wrap, const uint8_t *src1, int src_wrap, int width, int height)
{
const uint8_t *src_m2, *src_m1, *src_0, *src_p1, *src_p2;
int y;
src_m2 = src1;
src_m1 = src1;
src_0 = &src_m1[src_wrap];
src_p1 = &src_0[src_wrap];
src_p2 = &src_p1[src_wrap];
for (y = 0; y < (height - 2); y += 2) {
memcpy(dst, src_m1, width);
dst += dst_wrap;
deinterlace_line(dst, src_m2, src_m1, src_0, src_p1, src_p2, width);
src_m2 = src_0;
src_m1 = src_p1;
src_0 = src_p2;
src_p1 += 2 * src_wrap;
src_p2 += 2 * src_wrap;
dst += dst_wrap;
}
memcpy(dst, src_m1, width);
dst += dst_wrap;
/* do last line */
deinterlace_line(dst, src_m2, src_m1, src_0, src_0, src_0, width);
}
FFMPEG_INLINE int deinterlace_bottom_field_inplace(uint8_t *src1,
int src_wrap,
int width,
int height)
{
uint8_t *src_m1, *src_0, *src_p1, *src_p2;
int y;
uint8_t *buf = (uint8_t *)av_malloc(width);
if (!buf) {
return AVERROR(ENOMEM);
}
src_m1 = src1;
memcpy(buf, src_m1, width);
src_0 = &src_m1[src_wrap];
src_p1 = &src_0[src_wrap];
src_p2 = &src_p1[src_wrap];
for (y = 0; y < (height - 2); y += 2) {
deinterlace_line_inplace(buf, src_m1, src_0, src_p1, src_p2, width);
src_m1 = src_p1;
src_0 = src_p2;
src_p1 += 2 * src_wrap;
src_p2 += 2 * src_wrap;
}
/* do last line */
deinterlace_line_inplace(buf, src_m1, src_0, src_0, src_0, width);
av_free(buf);
return 0;
}
int ffmpeg_deinterlace(
AVFrame *dst, const AVFrame *src, enum AVPixelFormat pix_fmt, int width, int height)
{
int i, ret;
if (!ELEM(pix_fmt,
AV_PIX_FMT_YUV420P,
AV_PIX_FMT_YUVJ420P,
AV_PIX_FMT_YUV422P,
AV_PIX_FMT_YUVJ422P,
AV_PIX_FMT_YUV444P,
AV_PIX_FMT_YUV411P,
AV_PIX_FMT_GRAY8))
{
return -1;
}
if ((width & 3) != 0 || (height & 3) != 0) {
return -1;
}
for (i = 0; i < 3; i++) {
if (i == 1) {
switch (pix_fmt) {
case AV_PIX_FMT_YUVJ420P:
case AV_PIX_FMT_YUV420P:
width >>= 1;
height >>= 1;
break;
case AV_PIX_FMT_YUV422P:
case AV_PIX_FMT_YUVJ422P:
width >>= 1;
break;
case AV_PIX_FMT_YUV411P:
width >>= 2;
break;
default:
break;
}
if (pix_fmt == AV_PIX_FMT_GRAY8) {
break;
}
}
if (src == dst) {
ret = deinterlace_bottom_field_inplace(dst->data[i], dst->linesize[i], width, height);
if (ret < 0) {
return ret;
}
}
else {
deinterlace_bottom_field(
dst->data[i], dst->linesize[i], src->data[i], src->linesize[i], width, height);
}
}
return 0;
}
AVCodecID mov_av_codec_id_get(IMB_Ffmpeg_Codec_ID id)
{
switch (id) {
case FFMPEG_CODEC_ID_NONE:
return AV_CODEC_ID_NONE;
case FFMPEG_CODEC_ID_MPEG1VIDEO:
return AV_CODEC_ID_MPEG1VIDEO;
case FFMPEG_CODEC_ID_MPEG2VIDEO:
return AV_CODEC_ID_MPEG2VIDEO;
case FFMPEG_CODEC_ID_MPEG4:
return AV_CODEC_ID_MPEG4;
case FFMPEG_CODEC_ID_FLV1:
return AV_CODEC_ID_FLV1;
case FFMPEG_CODEC_ID_DVVIDEO:
return AV_CODEC_ID_DVVIDEO;
case FFMPEG_CODEC_ID_HUFFYUV:
return AV_CODEC_ID_HUFFYUV;
case FFMPEG_CODEC_ID_H264:
return AV_CODEC_ID_H264;
case FFMPEG_CODEC_ID_THEORA:
return AV_CODEC_ID_THEORA;
case FFMPEG_CODEC_ID_FFV1:
return AV_CODEC_ID_FFV1;
case FFMPEG_CODEC_ID_QTRLE:
return AV_CODEC_ID_QTRLE;
case FFMPEG_CODEC_ID_PNG:
return AV_CODEC_ID_PNG;
case FFMPEG_CODEC_ID_DNXHD:
return AV_CODEC_ID_DNXHD;
case FFMPEG_CODEC_ID_VP9:
return AV_CODEC_ID_VP9;
case FFMPEG_CODEC_ID_H265:
return AV_CODEC_ID_H265;
case FFMPEG_CODEC_ID_AV1:
return AV_CODEC_ID_AV1;
case FFMPEG_CODEC_ID_PRORES:
return AV_CODEC_ID_PRORES;
case FFMPEG_CODEC_ID_PCM_S16LE:
return AV_CODEC_ID_PCM_S16LE;
case FFMPEG_CODEC_ID_MP2:
return AV_CODEC_ID_MP2;
case FFMPEG_CODEC_ID_MP3:
return AV_CODEC_ID_MP3;
case FFMPEG_CODEC_ID_AAC:
return AV_CODEC_ID_AAC;
case FFMPEG_CODEC_ID_AC3:
return AV_CODEC_ID_AC3;
case FFMPEG_CODEC_ID_VORBIS:
return AV_CODEC_ID_VORBIS;
case FFMPEG_CODEC_ID_FLAC:
return AV_CODEC_ID_FLAC;
case FFMPEG_CODEC_ID_OPUS:
return AV_CODEC_ID_OPUS;
}
BLI_assert_unreachable();
return AV_CODEC_ID_NONE;
}
static void ffmpeg_preset_set(RenderData *rd, int preset)
{
bool is_ntsc = (rd->frs_sec != 25);
switch (preset) {
case FFMPEG_PRESET_H264:
rd->ffcodecdata.type = FFMPEG_AVI;
rd->ffcodecdata.codec_id_set(FFMPEG_CODEC_ID_H264);
rd->ffcodecdata.video_bitrate = 6000;
rd->ffcodecdata.gop_size = is_ntsc ? 18 : 15;
rd->ffcodecdata.rc_max_rate = 9000;
rd->ffcodecdata.rc_min_rate = 0;
rd->ffcodecdata.rc_buffer_size = 224 * 8;
rd->ffcodecdata.mux_packet_size = 2048;
rd->ffcodecdata.mux_rate = 10080000;
break;
case FFMPEG_PRESET_THEORA:
case FFMPEG_PRESET_XVID:
if (preset == FFMPEG_PRESET_XVID) {
rd->ffcodecdata.type = FFMPEG_AVI;
rd->ffcodecdata.codec_id_set(FFMPEG_CODEC_ID_MPEG4);
}
else if (preset == FFMPEG_PRESET_THEORA) {
rd->ffcodecdata.type = FFMPEG_OGG; /* XXX broken */
rd->ffcodecdata.codec_id_set(FFMPEG_CODEC_ID_THEORA);
}
rd->ffcodecdata.video_bitrate = 6000;
rd->ffcodecdata.gop_size = is_ntsc ? 18 : 15;
rd->ffcodecdata.rc_max_rate = 9000;
rd->ffcodecdata.rc_min_rate = 0;
rd->ffcodecdata.rc_buffer_size = 224 * 8;
rd->ffcodecdata.mux_packet_size = 2048;
rd->ffcodecdata.mux_rate = 10080000;
break;
case FFMPEG_PRESET_AV1:
rd->ffcodecdata.type = FFMPEG_AV1;
rd->ffcodecdata.codec_id_set(FFMPEG_CODEC_ID_AV1);
rd->ffcodecdata.video_bitrate = 6000;
rd->ffcodecdata.gop_size = is_ntsc ? 18 : 15;
rd->ffcodecdata.rc_max_rate = 9000;
rd->ffcodecdata.rc_min_rate = 0;
rd->ffcodecdata.rc_buffer_size = 224 * 8;
rd->ffcodecdata.mux_packet_size = 2048;
rd->ffcodecdata.mux_rate = 10080000;
break;
}
}
eImageFormatDepth MOV_codec_valid_bit_depths(AVCodecID av_codec_id)
{
eImageFormatDepth bit_depths = R_IMF_CHAN_DEPTH_8;
/* 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,
AV_CODEC_ID_PRORES,
AV_CODEC_ID_FFV1))
{
bit_depths |= R_IMF_CHAN_DEPTH_10;
}
if (ELEM(av_codec_id, AV_CODEC_ID_H265, AV_CODEC_ID_AV1, AV_CODEC_ID_FFV1)) {
bit_depths |= R_IMF_CHAN_DEPTH_12;
}
if (ELEM(av_codec_id, AV_CODEC_ID_FFV1)) {
bit_depths |= R_IMF_CHAN_DEPTH_16;
}
return bit_depths;
}
bool MOV_codec_supports_alpha(AVCodecID av_codec_id, int ffmpeg_profile)
{
if (av_codec_id == AV_CODEC_ID_PRORES) {
return ELEM(ffmpeg_profile, FFM_PRORES_PROFILE_4444, FFM_PRORES_PROFILE_4444_XQ);
}
return ELEM(av_codec_id,
AV_CODEC_ID_FFV1,
AV_CODEC_ID_QTRLE,
AV_CODEC_ID_PNG,
AV_CODEC_ID_VP9,
AV_CODEC_ID_HUFFYUV);
}
bool MOV_codec_supports_crf(AVCodecID av_codec_id)
{
return ELEM(av_codec_id,
AV_CODEC_ID_H264,
AV_CODEC_ID_H265,
AV_CODEC_ID_MPEG4,
AV_CODEC_ID_VP9,
AV_CODEC_ID_AV1);
}
int MOV_thread_count()
{
/* ffmpeg does not recommend thread counts above 16. */
return std::min(BLI_system_thread_count(), 16);
}
#endif /* WITH_FFMPEG */
bool MOV_is_movie_file(const char *filepath)
{
BLI_assert(!BLI_path_is_rel(filepath));
#ifdef WITH_FFMPEG
if (isffmpeg(filepath)) {
return true;
}
#else
UNUSED_VARS(filepath);
#endif
return false;
}
void MOV_init()
{
#ifdef WITH_FFMPEG
avdevice_register_all();
ffmpeg_last_error_buffer[0] = '\0';
if (CLOG_CHECK(&LOG, CLG_LEVEL_INFO)) {
av_log_set_level(AV_LOG_INFO);
}
else if (CLOG_CHECK(&LOG, CLG_LEVEL_DEBUG)) {
av_log_set_level(AV_LOG_DEBUG);
}
else if (CLOG_CHECK(&LOG, CLG_LEVEL_TRACE)) {
av_log_set_level(AV_LOG_TRACE);
}
/* set separate callback which could store last error to report to UI */
av_log_set_callback(ffmpeg_log_callback);
#endif
}
void MOV_exit()
{
#ifdef WITH_FFMPEG
ffmpeg_sws_exit();
#endif
}
void MOV_validate_output_settings(RenderData *rd, const ImageFormatData *imf)
{
#ifdef WITH_FFMPEG
if (imf->imtype == R_IMF_IMTYPE_FFMPEG) {
if (rd->ffcodecdata.type <= 0 || rd->ffcodecdata.codec_id_get() <= 0 ||
rd->ffcodecdata.video_bitrate <= 1)
{
ffmpeg_preset_set(rd, FFMPEG_PRESET_H264);
rd->ffcodecdata.constant_rate_factor = FFM_CRF_MEDIUM;
rd->ffcodecdata.ffmpeg_preset = FFM_PRESET_GOOD;
rd->ffcodecdata.type = FFMPEG_MKV;
}
if (rd->ffcodecdata.type == FFMPEG_OGG) {
rd->ffcodecdata.type = FFMPEG_MPEG2;
}
}
#else
UNUSED_VARS(rd, imf);
#endif
}
eImageFormatDepth MOV_codec_valid_bit_depths(IMB_Ffmpeg_Codec_ID codec_id)
{
#ifdef WITH_FFMPEG
return MOV_codec_valid_bit_depths(mov_av_codec_id_get(codec_id));
#else
UNUSED_VARS(codec_id);
return R_IMF_CHAN_DEPTH_8;
#endif
}
bool MOV_codec_supports_alpha(IMB_Ffmpeg_Codec_ID codec_id, int ffmpeg_profile)
{
#ifdef WITH_FFMPEG
return MOV_codec_supports_alpha(mov_av_codec_id_get(codec_id), ffmpeg_profile);
#else
UNUSED_VARS(codec_id, ffmpeg_profile);
return false;
#endif
}
bool MOV_codec_supports_crf(IMB_Ffmpeg_Codec_ID codec_id)
{
#ifdef WITH_FFMPEG
return MOV_codec_supports_crf(mov_av_codec_id_get(codec_id));
#else
UNUSED_VARS(codec_id);
return false;
#endif
}
} // namespace blender

View File

@@ -0,0 +1,52 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup imbuf
*/
#ifdef WITH_FFMPEG
extern "C" {
# include <libavcodec/avcodec.h>
# include <libavutil/pixfmt.h>
}
# include "DNA_scene_types.h"
struct AVFrame;
namespace blender {
int ffmpeg_deinterlace(
AVFrame *dst, const AVFrame *src, enum AVPixelFormat pix_fmt, int width, int height);
const char *ffmpeg_last_error();
AVCodecID mov_av_codec_id_get(IMB_Ffmpeg_Codec_ID id);
/** Checks whether given FFMPEG codec and profile combination supports alpha channel (RGBA). */
bool MOV_codec_supports_alpha(AVCodecID codec_id, int ffmpeg_profile);
/**
* Checks whether given FFMPEG video AVCodecID supports CRF (i.e. "quality level")
* setting. For codecs that do not support constant quality, only target bit-rate
* can be specified.
*/
bool MOV_codec_supports_crf(AVCodecID codec_id);
/**
* Which pixel bit depths are supported by a given FFMPEG video CodecID.
* Returns bit-mask of `R_IMF_CHAN_DEPTH_` flags.
*/
eImageFormatDepth MOV_codec_valid_bit_depths(AVCodecID codec_id);
/**
* Returns thread count to be used for ffmpeg.
*/
int MOV_thread_count();
} // namespace blender
#endif /* WITH_FFMPEG */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#pragma once
#ifdef WITH_FFMPEG
# include <cstdint>
/* Note: include cmath before ffmpeg headers, since both of them define
* M_PI and other macros. This is to avoid warnings about macro redefinition
* if later including cmath (MSVC 2019). */
# if defined(_MSC_VER) && !defined(_USE_MATH_DEFINES)
# define _USE_MATH_DEFINES
# endif
# include <cmath> // IWYU pragma: keep
extern "C" {
# include <libavcodec/codec_id.h>
# include <libavformat/avformat.h>
# include <libavutil/buffer.h>
# include <libavutil/channel_layout.h>
# include <libavutil/imgutils.h>
# include <libavutil/mastering_display_metadata.h>
# include <libavutil/opt.h>
# include <libavutil/rational.h>
# include <libavutil/samplefmt.h>
# include <libavutil/spherical.h>
# include <libavutil/stereo3d.h>
# include "ffmpeg_compat.h"
}
# ifdef WITH_AUDASPACE
# include "BKE_sound_types.hh"
# endif
namespace blender {
struct Scene;
struct ReportList;
struct StampData;
struct MovieWriter {
int ffmpeg_type = 0;
AVCodecID ffmpeg_codec = {};
AVCodecID ffmpeg_audio_codec = {};
int ffmpeg_video_bitrate = 0;
int ffmpeg_audio_bitrate = 0;
int ffmpeg_gop_size = 0;
int ffmpeg_max_b_frames = 0;
int ffmpeg_autosplit_count = 0;
bool ffmpeg_autosplit = false;
bool ffmpeg_preview = false;
int ffmpeg_crf = 0; /* set to 0 to not use CRF mode; we have another flag for lossless anyway. */
bool custom_crf = false;
int ffmpeg_preset = 0; /* see eFFMpegPreset */
int ffmpeg_profile = 0;
AVFormatContext *outfile = nullptr;
AVCodecContext *video_codec = nullptr;
AVCodecContext *audio_codec = nullptr;
AVStream *video_stream = nullptr;
AVStream *audio_stream = nullptr;
AVFrame *current_frame = nullptr; /* Image frame in output pixel format. */
int video_time = 0;
/* Image frame in Blender's own pixel format, may need conversion to the output pixel format. */
AVFrame *img_convert_frame = nullptr;
SwsContext *img_convert_ctx = nullptr;
uint8_t *audio_input_buffer = nullptr;
uint8_t *audio_deinterleave_buffer = nullptr;
int audio_input_samples = 0;
double audio_time = 0.0;
double audio_time_total = 0.0;
bool audio_deinterleave = false;
int audio_sample_size = 0;
StampData *stamp_data = nullptr;
# ifdef WITH_AUDASPACE
AUD_Device audio_mixdown_device;
# endif
};
bool movie_audio_open(MovieWriter *context,
const Scene *scene,
int start_frame,
int mixrate,
float volume,
ReportList *reports);
void movie_audio_close(MovieWriter *context, bool is_autosplit);
AVStream *alloc_audio_stream(MovieWriter *context,
int audio_mixrate,
int audio_channels,
AVCodecID codec_id,
AVFormatContext *of,
char *error,
int error_size,
ReportList *reports);
void write_audio_frames(MovieWriter *context, double to_pts);
} // namespace blender
#endif /* WITH_FFMPEG */

View File

@@ -0,0 +1,450 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#ifdef _MSC_VER
/* This needs to be included first to prevent ffmpegs headers adding defines for various math
* constants leading to duplicate definitions. */
# include <cmath>
#endif
#include "movie_util.hh"
#include "movie_write.hh"
#ifdef WITH_FFMPEG
# include <cstdio>
# include <cstring>
# include "DNA_scene_types.h"
# include "BLI_string.h"
# include "BLI_utildefines.h"
# include "BKE_report.hh"
# include "BKE_sound.hh"
# include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"video.write"};
/* If any of these codecs, we prefer the float sample format (if supported) */
static bool request_float_audio_buffer(int codec_id)
{
return ELEM(codec_id, AV_CODEC_ID_AAC, AV_CODEC_ID_AC3, AV_CODEC_ID_VORBIS);
}
# ifdef WITH_AUDASPACE
static int write_audio_frame(MovieWriter *context)
{
AVFrame *frame = nullptr;
AVCodecContext *c = context->audio_codec;
bke::sound_device_read(
context->audio_mixdown_device, context->audio_input_buffer, context->audio_input_samples);
frame = av_frame_alloc();
frame->pts = context->audio_time / av_q2d(c->time_base);
frame->nb_samples = context->audio_input_samples;
frame->format = c->sample_fmt;
# ifdef FFMPEG_USE_OLD_CHANNEL_VARS
frame->channels = c->channels;
frame->channel_layout = c->channel_layout;
const int num_channels = c->channels;
# else
av_channel_layout_copy(&frame->ch_layout, &c->ch_layout);
const int num_channels = c->ch_layout.nb_channels;
# endif
if (context->audio_deinterleave) {
int channel, i;
uint8_t *temp;
for (channel = 0; channel < num_channels; channel++) {
for (i = 0; i < frame->nb_samples; i++) {
memcpy(context->audio_deinterleave_buffer +
(i + channel * frame->nb_samples) * context->audio_sample_size,
context->audio_input_buffer +
(num_channels * i + channel) * context->audio_sample_size,
context->audio_sample_size);
}
}
temp = context->audio_deinterleave_buffer;
context->audio_deinterleave_buffer = context->audio_input_buffer;
context->audio_input_buffer = temp;
}
avcodec_fill_audio_frame(frame,
num_channels,
c->sample_fmt,
context->audio_input_buffer,
context->audio_input_samples * num_channels *
context->audio_sample_size,
1);
int success = 1;
char error_str[AV_ERROR_MAX_STRING_SIZE];
int ret = avcodec_send_frame(c, frame);
if (ret < 0) {
/* Can't send frame to encoder. This shouldn't happen. */
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG, "Can't send audio frame: %s", error_str);
success = -1;
}
AVPacket *pkt = av_packet_alloc();
while (ret >= 0) {
ret = avcodec_receive_packet(c, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG, "Error encoding audio frame: %s", error_str);
success = -1;
}
pkt->stream_index = context->audio_stream->index;
av_packet_rescale_ts(pkt, c->time_base, context->audio_stream->time_base);
# ifdef FFMPEG_USE_DURATION_WORKAROUND
my_guess_pkt_duration(context->outfile, context->audio_stream, pkt);
# endif
pkt->flags |= AV_PKT_FLAG_KEY;
int write_ret = av_interleaved_write_frame(context->outfile, pkt);
if (write_ret != 0) {
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG, "Error writing audio packet: %s", error_str);
success = -1;
break;
}
}
av_packet_free(&pkt);
av_frame_free(&frame);
return success;
}
# endif /* #ifdef WITH_AUDASPACE */
bool movie_audio_open(MovieWriter *context,
const Scene *scene,
int start_frame,
int mixrate,
float volume,
ReportList *reports)
{
bool success = true;
# ifdef WITH_AUDASPACE
if (context->audio_stream) {
AVCodecContext *c = context->audio_codec;
aud::DeviceSpecs specs;
# ifdef FFMPEG_USE_OLD_CHANNEL_VARS
specs.channels = aud::Channels(c->channels);
# else
specs.channels = aud::Channels(c->ch_layout.nb_channels);
# endif
switch (av_get_packed_sample_fmt(c->sample_fmt)) {
case AV_SAMPLE_FMT_U8:
specs.format = aud::FORMAT_U8;
break;
case AV_SAMPLE_FMT_S16:
specs.format = aud::FORMAT_S16;
break;
case AV_SAMPLE_FMT_S32:
specs.format = aud::FORMAT_S32;
break;
case AV_SAMPLE_FMT_FLT:
specs.format = aud::FORMAT_FLOAT32;
break;
case AV_SAMPLE_FMT_DBL:
specs.format = aud::FORMAT_FLOAT64;
break;
default:
BKE_report(reports, RPT_ERROR, "Audio sample format unsupported");
success = false;
break;
}
specs.rate = mixrate;
if (success) {
context->audio_mixdown_device = BKE_sound_mixdown(scene, specs, start_frame, volume);
}
}
# else
UNUSED_VARS(context, scene, start_frame, mixrate, volume, reports);
# endif
return success;
}
void movie_audio_close(MovieWriter *context, bool is_autosplit)
{
# ifdef WITH_AUDASPACE
if (!is_autosplit) {
context->audio_mixdown_device.reset();
}
# else
UNUSED_VARS(context, is_autosplit);
# endif
}
AVStream *alloc_audio_stream(MovieWriter *context,
int audio_mixrate,
int audio_channels,
AVCodecID codec_id,
AVFormatContext *of,
char *error,
int error_size,
ReportList *reports)
{
AVStream *st;
const AVCodec *codec;
error[0] = '\0';
st = avformat_new_stream(of, nullptr);
if (!st) {
return nullptr;
}
st->id = 1;
codec = avcodec_find_encoder(codec_id);
if (!codec) {
CLOG_ERROR(&LOG, "Couldn't find valid audio codec");
context->audio_codec = nullptr;
return nullptr;
}
int channel_layout_mask = 0;
int channel_count = 0;
switch (audio_channels) {
case FFM_CHANNELS_MONO:
channel_layout_mask = AV_CH_LAYOUT_MONO;
channel_count = 1;
break;
case FFM_CHANNELS_STEREO:
channel_layout_mask = AV_CH_LAYOUT_STEREO;
channel_count = 2;
break;
case FFM_CHANNELS_SURROUND4:
channel_layout_mask = AV_CH_LAYOUT_QUAD;
channel_count = 4;
break;
case FFM_CHANNELS_SURROUND51:
channel_layout_mask = AV_CH_LAYOUT_5POINT1_BACK;
channel_count = 6;
break;
case FFM_CHANNELS_SURROUND71:
channel_layout_mask = AV_CH_LAYOUT_7POINT1;
channel_count = 8;
break;
default:
BLI_assert(false);
break;
}
/* Clamp audio bitrate and report info if bitrate is set higher than the maximum bitrate of the
* codec. */
switch (codec_id) {
case AV_CODEC_ID_MP2:
if (context->ffmpeg_audio_bitrate > 384) {
context->ffmpeg_audio_bitrate = 384;
BKE_report(reports,
RPT_INFO,
"The audio is rendered with a bitrate of 384kbit/s, the maximum bitrate MP2 "
"supports.");
}
break;
case AV_CODEC_ID_MP3:
if (context->ffmpeg_audio_bitrate > 320) {
context->ffmpeg_audio_bitrate = 320;
BKE_report(reports,
RPT_INFO,
"The audio is rendered with a bitrate of 320kbit/s, the maximum bitrate MP3 "
"supports.");
}
break;
case AV_CODEC_ID_AAC:
if (context->ffmpeg_audio_bitrate > 250 * channel_count) {
/* AAC doesn't specify a maximum bitrate. Instead, the maximum bitrate is dependent on the
* encoder used. Clamping of the bitrate is therefore left to the encoder. */
BKE_report(
reports,
RPT_INFO,
"The audio is rendered with a bitrate of roughly 250kbit/s per channel, the maximum "
"bitrate AAC supports.");
}
break;
case AV_CODEC_ID_AC3:
if (context->ffmpeg_audio_bitrate > 640) {
context->ffmpeg_audio_bitrate = 640;
BKE_report(reports,
RPT_INFO,
"The audio is rendered with a bitrate of 640kbit/s, the maximum bitrate AC3 "
"supports.");
}
break;
case AV_CODEC_ID_OPUS:
if (context->ffmpeg_audio_bitrate > 256 * channel_count) {
context->ffmpeg_audio_bitrate = 256 * channel_count;
BKE_report(reports,
RPT_INFO,
"The audio is rendered with a bitrate of 256kbit/s per channel, the maximum "
"bitrate Opus supports.");
}
break;
case AV_CODEC_ID_VORBIS:
if (context->ffmpeg_audio_bitrate > 240 * channel_count) {
context->ffmpeg_audio_bitrate = 240 * channel_count;
BKE_report(reports,
RPT_INFO,
"The audio is rendered with a bitrate of 240kbit/s per channel, the maximum "
"bitrate Vorbis supports.");
}
break;
default:
/* Default case for suppressing compiler warnings. */
break;
}
context->audio_codec = avcodec_alloc_context3(codec);
AVCodecContext *c = context->audio_codec;
c->thread_count = MOV_thread_count();
c->thread_type = FF_THREAD_SLICE;
c->sample_rate = audio_mixrate;
c->bit_rate = context->ffmpeg_audio_bitrate * 1000;
c->sample_fmt = AV_SAMPLE_FMT_S16;
# ifdef FFMPEG_USE_OLD_CHANNEL_VARS
c->channels = audio_channels;
c->channel_layout = channel_layout_mask;
# else
av_channel_layout_from_mask(&c->ch_layout, channel_layout_mask);
# endif
if (request_float_audio_buffer(codec_id)) {
/* mainly for AAC codec which is experimental */
c->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
c->sample_fmt = AV_SAMPLE_FMT_FLT;
}
const enum AVSampleFormat *sample_fmts = ffmpeg_get_sample_fmts(c, codec);
if (sample_fmts) {
/* Check if the preferred sample format for this codec is supported.
* this is because, depending on the version of LIBAV,
* and with the whole FFMPEG/LIBAV fork situation,
* you have various implementations around.
* Float samples in particular are not always supported. */
const enum AVSampleFormat *p = sample_fmts;
for (; *p != -1; p++) {
if (*p == c->sample_fmt) {
break;
}
}
if (*p == -1) {
/* sample format incompatible with codec. Defaulting to a format known to work */
c->sample_fmt = sample_fmts[0];
}
}
const int *supported_samplerates = ffmpeg_get_sample_rates(c, codec);
if (supported_samplerates) {
const int *p = supported_samplerates;
int best = 0;
int best_dist = INT_MAX;
for (; *p; p++) {
int dist = abs(c->sample_rate - *p);
if (dist < best_dist) {
best_dist = dist;
best = *p;
}
}
/* best is the closest supported sample rate (same as selected if best_dist == 0) */
c->sample_rate = best;
}
if (of->oformat->flags & AVFMT_GLOBALHEADER) {
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
int ret = avcodec_open2(c, codec, nullptr);
if (ret < 0) {
char error_str[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret);
CLOG_ERROR(&LOG, "Couldn't initialize audio codec: %s", error_str);
BLI_strncpy(error, ffmpeg_last_error(), error_size);
avcodec_free_context(&c);
context->audio_codec = nullptr;
return nullptr;
}
/* Need to prevent floating point exception when using VORBIS audio codec,
* initialize this value in the same way as it's done in FFMPEG itself (sergey) */
c->time_base.num = 1;
c->time_base.den = c->sample_rate;
if (c->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) {
/* If the audio format has a variable frame size, default to 1024.
* This is because we won't try to encode any variable frame size.
* 1024 seems to be a good compromise between size and speed.
*/
context->audio_input_samples = 1024;
}
else {
context->audio_input_samples = c->frame_size;
}
context->audio_deinterleave = av_sample_fmt_is_planar(c->sample_fmt);
context->audio_sample_size = av_get_bytes_per_sample(c->sample_fmt);
context->audio_input_buffer = (uint8_t *)av_malloc(context->audio_input_samples *
audio_channels * context->audio_sample_size);
if (context->audio_deinterleave) {
context->audio_deinterleave_buffer = (uint8_t *)av_malloc(
context->audio_input_samples * audio_channels * context->audio_sample_size);
}
context->audio_time = 0.0f;
avcodec_parameters_from_context(st->codecpar, c);
return st;
}
void write_audio_frames(MovieWriter *context, double to_pts)
{
# ifdef WITH_AUDASPACE
AVCodecContext *c = context->audio_codec;
while (context->audio_stream) {
if ((context->audio_time_total >= to_pts) || !write_audio_frame(context)) {
break;
}
context->audio_time_total += double(context->audio_input_samples) / double(c->sample_rate);
context->audio_time += double(context->audio_input_samples) / double(c->sample_rate);
}
# else
UNUSED_VARS(context, to_pts);
# endif
}
} // namespace blender
#endif /* WITH_FFMPEG */