Replace every direct FFmpeg call in the editor and the render worker with the pure C ffmpeg_bridge API, wrapped in thin C++ adapters that preserve the original interfaces: - avframeptr.h: olive::AVFrame adapter around FBFrame handles - ffmpegutils: format conversion helpers on FB_* constants; the int overload is renamed GetCompatibleBridgePixelFormat to avoid a silent overload-resolution trap with the PixelFormat enum - ffmpegdecoder/ffmpegencoder: rewritten as handle-based adapters over FBDecoder/FBProbe/FBEncoder/FBScaler/FBResampler - audioprocessor: FBAudioGraph push/pull adapter - pluginrenderer/OliveClip: sws/pixdesc usage converted to FBScaler and fb_pix_fmt_* queries - AudioParams/channel layouts are plain uint64_t masks everywhere Build integration: the root project no longer links FFMPEG directly; only ffmpeg_bridge does. Binaries resolve the bridge library at runtime via @loader_path inside the macOS app bundle (copied there post-build) and via $ORIGIN/../ffmpeg_bridge/bin on Linux; on Windows the DLL is installed next to the executables, so packages on all three platforms ship the bridge library. ffmpeg_bridge gains the extra API the adapters need: fb_frame_make_writable, fb_decoder_get_format_duration, fb_resampler_convert_frame, FB_PIX_FMT_YUV440P, SRT validation in fb_probe_read_subtitle_stream, packed/planar fixes in fb_encoder_write_audio, and a component-size fix in fb_pix_fmt_component_size.
1126 lines
30 KiB
C++
1126 lines
30 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2022 Olive Team
|
|
Modifications Copyright (C) 2025 mikesolar
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "internal.h"
|
|
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
#endif
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
|
|
AVPixelFormat ConvertJPEGSpaceToRegularSpace(AVPixelFormat f)
|
|
{
|
|
switch (f) {
|
|
case AV_PIX_FMT_YUVJ420P:
|
|
return AV_PIX_FMT_YUV420P;
|
|
case AV_PIX_FMT_YUVJ422P:
|
|
return AV_PIX_FMT_YUV422P;
|
|
case AV_PIX_FMT_YUVJ444P:
|
|
return AV_PIX_FMT_YUV444P;
|
|
case AV_PIX_FMT_YUVJ440P:
|
|
return AV_PIX_FMT_YUV440P;
|
|
case AV_PIX_FMT_YUVJ411P:
|
|
return AV_PIX_FMT_YUV411P;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return f;
|
|
}
|
|
|
|
const AVCodec *FindEncoder(int codec, int sample_format)
|
|
{
|
|
switch (codec) {
|
|
case FB_CODEC_H264:
|
|
return avcodec_find_encoder_by_name("libx264");
|
|
case FB_CODEC_H264RGB:
|
|
return avcodec_find_encoder_by_name("libx264rgb");
|
|
case FB_CODEC_DNXHD:
|
|
return avcodec_find_encoder(AV_CODEC_ID_DNXHD);
|
|
case FB_CODEC_PRORES:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PRORES);
|
|
case FB_CODEC_CINEFORM:
|
|
return avcodec_find_encoder(AV_CODEC_ID_CFHD);
|
|
case FB_CODEC_H265:
|
|
return avcodec_find_encoder(AV_CODEC_ID_HEVC);
|
|
case FB_CODEC_VP9:
|
|
return avcodec_find_encoder(AV_CODEC_ID_VP9);
|
|
case FB_CODEC_AV1: {
|
|
const AVCodec *encoder = avcodec_find_encoder_by_name("libsvtav1");
|
|
if (!encoder) {
|
|
encoder = avcodec_find_encoder(AV_CODEC_ID_AV1);
|
|
}
|
|
return encoder;
|
|
}
|
|
case FB_CODEC_OPENEXR:
|
|
return avcodec_find_encoder(AV_CODEC_ID_EXR);
|
|
case FB_CODEC_PNG:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PNG);
|
|
case FB_CODEC_TIFF:
|
|
return avcodec_find_encoder(AV_CODEC_ID_TIFF);
|
|
case FB_CODEC_MP2:
|
|
return avcodec_find_encoder(AV_CODEC_ID_MP2);
|
|
case FB_CODEC_MP3:
|
|
return avcodec_find_encoder(AV_CODEC_ID_MP3);
|
|
case FB_CODEC_AAC:
|
|
return avcodec_find_encoder(AV_CODEC_ID_AAC);
|
|
case FB_CODEC_PCM:
|
|
switch (sample_format) {
|
|
case FB_SAMPLE_FMT_U8:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PCM_U8);
|
|
case FB_SAMPLE_FMT_S16:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE);
|
|
case FB_SAMPLE_FMT_S32:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PCM_S32LE);
|
|
case FB_SAMPLE_FMT_S64:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PCM_S64LE);
|
|
case FB_SAMPLE_FMT_FLT:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PCM_F32LE);
|
|
case FB_SAMPLE_FMT_DBL:
|
|
return avcodec_find_encoder(AV_CODEC_ID_PCM_F64LE);
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case FB_CODEC_FLAC:
|
|
return avcodec_find_encoder(AV_CODEC_ID_FLAC);
|
|
case FB_CODEC_OPUS:
|
|
return avcodec_find_encoder(AV_CODEC_ID_OPUS);
|
|
case FB_CODEC_VORBIS:
|
|
return avcodec_find_encoder(AV_CODEC_ID_VORBIS);
|
|
case FB_CODEC_SRT:
|
|
return avcodec_find_encoder(AV_CODEC_ID_SUBRIP);
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct FBEncoder {
|
|
// Deep-copied configuration
|
|
std::string filename;
|
|
|
|
int video_enabled = 0;
|
|
int video_codec = FB_CODEC_NONE;
|
|
int video_width = 0;
|
|
int video_height = 0;
|
|
int video_pixel_aspect_num = 1;
|
|
int video_pixel_aspect_den = 1;
|
|
int video_time_base_num = 0;
|
|
int video_time_base_den = 1;
|
|
int video_frame_rate_num = 0;
|
|
int video_frame_rate_den = 1;
|
|
std::string video_pix_fmt;
|
|
int video_src_pix_fmt = FB_PIX_FMT_NONE;
|
|
int video_color_range = FB_COLOR_RANGE_UNSPEC;
|
|
int video_field_order = FB_FIELD_ORDER_PROGRESSIVE;
|
|
int64_t video_bit_rate = 0;
|
|
int64_t video_min_bit_rate = 0;
|
|
int64_t video_max_bit_rate = 0;
|
|
int64_t video_buffer_size = 0;
|
|
int video_threads = 0;
|
|
int video_color_srgb = 0;
|
|
std::vector<std::pair<std::string, std::string>> video_opts;
|
|
|
|
int audio_enabled = 0;
|
|
int audio_codec = FB_CODEC_NONE;
|
|
int audio_sample_rate = 0;
|
|
uint64_t audio_channel_layout_mask = 0;
|
|
int audio_sample_format = FB_SAMPLE_FMT_NONE;
|
|
int64_t audio_bit_rate = 0;
|
|
|
|
int subtitles_enabled = 0;
|
|
int subtitle_codec = FB_CODEC_NONE;
|
|
std::vector<uint8_t> subtitle_header;
|
|
|
|
// Runtime state
|
|
AVFormatContext *fmt_ctx = nullptr;
|
|
|
|
AVStream *video_stream = nullptr;
|
|
AVCodecContext *video_codec_ctx = nullptr;
|
|
AVFilterGraph *video_scale_ctx = nullptr;
|
|
AVFilterContext *video_buffersrc_ctx = nullptr;
|
|
AVFilterContext *video_buffersink_ctx = nullptr;
|
|
|
|
AVStream *audio_stream = nullptr;
|
|
AVCodecContext *audio_codec_ctx = nullptr;
|
|
SwrContext *audio_resample_ctx = nullptr;
|
|
AVFrame *audio_frame = nullptr;
|
|
int audio_max_samples = 0;
|
|
int audio_frame_offset = 0;
|
|
int64_t audio_write_count = 0;
|
|
|
|
AVStream *subtitle_stream = nullptr;
|
|
AVCodecContext *subtitle_codec_ctx = nullptr;
|
|
|
|
bool open = false;
|
|
|
|
char error[1024] = { 0 };
|
|
|
|
void SetError(const char *context, int error_code)
|
|
{
|
|
fb::SetError(error, sizeof(error), context, error_code);
|
|
}
|
|
|
|
void SetError(const char *message)
|
|
{
|
|
snprintf(error, sizeof(error), "%s", message);
|
|
}
|
|
|
|
bool WriteAVFrame(AVFrame *frame, AVCodecContext *codec_ctx,
|
|
AVStream *stream);
|
|
bool InitializeStream(AVMediaType type, AVStream **stream,
|
|
AVCodecContext **codec_ctx, int codec);
|
|
bool InitializeCodecContext(AVStream **stream, AVCodecContext **codec_ctx,
|
|
const AVCodec *codec);
|
|
bool SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx,
|
|
const AVCodec *codec);
|
|
void FlushEncoders();
|
|
void FlushCodecCtx(AVCodecContext *codec_ctx, AVStream *stream);
|
|
bool InitializeResampleContext(int sample_format, int sample_rate,
|
|
uint64_t channel_layout_mask);
|
|
bool WriteAudioData(int sample_format, int sample_rate,
|
|
uint64_t channel_layout_mask, const uint8_t **input_data,
|
|
int input_sample_count);
|
|
};
|
|
|
|
FBEncoder *fb_encoder_create(const FBEncoderConfig *config)
|
|
{
|
|
if (!config || !config->filename) {
|
|
return nullptr;
|
|
}
|
|
|
|
FBEncoder *e = new FBEncoder;
|
|
|
|
e->filename = config->filename;
|
|
|
|
e->video_enabled = config->video_enabled;
|
|
e->video_codec = config->video_codec;
|
|
e->video_width = config->video_width;
|
|
e->video_height = config->video_height;
|
|
e->video_pixel_aspect_num = config->video_pixel_aspect_num;
|
|
e->video_pixel_aspect_den = config->video_pixel_aspect_den;
|
|
e->video_time_base_num = config->video_time_base_num;
|
|
e->video_time_base_den = config->video_time_base_den;
|
|
e->video_frame_rate_num = config->video_frame_rate_num;
|
|
e->video_frame_rate_den = config->video_frame_rate_den;
|
|
if (config->video_pix_fmt) {
|
|
e->video_pix_fmt = config->video_pix_fmt;
|
|
}
|
|
e->video_src_pix_fmt = config->video_src_pix_fmt;
|
|
e->video_color_range = config->video_color_range;
|
|
e->video_field_order = config->video_field_order;
|
|
e->video_bit_rate = config->video_bit_rate;
|
|
e->video_min_bit_rate = config->video_min_bit_rate;
|
|
e->video_max_bit_rate = config->video_max_bit_rate;
|
|
e->video_buffer_size = config->video_buffer_size;
|
|
e->video_threads = config->video_threads;
|
|
e->video_color_srgb = config->video_color_srgb;
|
|
for (int i = 0; i < config->video_opt_count; i++) {
|
|
if (config->video_opt_keys[i] && config->video_opt_values[i]) {
|
|
e->video_opts.emplace_back(config->video_opt_keys[i],
|
|
config->video_opt_values[i]);
|
|
}
|
|
}
|
|
|
|
e->audio_enabled = config->audio_enabled;
|
|
e->audio_codec = config->audio_codec;
|
|
e->audio_sample_rate = config->audio_sample_rate;
|
|
e->audio_channel_layout_mask = config->audio_channel_layout_mask;
|
|
e->audio_sample_format = config->audio_sample_format;
|
|
e->audio_bit_rate = config->audio_bit_rate;
|
|
|
|
e->subtitles_enabled = config->subtitles_enabled;
|
|
e->subtitle_codec = config->subtitle_codec;
|
|
if (config->subtitle_header && config->subtitle_header_size > 0) {
|
|
e->subtitle_header.assign(config->subtitle_header,
|
|
config->subtitle_header +
|
|
config->subtitle_header_size);
|
|
}
|
|
|
|
return e;
|
|
}
|
|
|
|
void fb_encoder_free(FBEncoder **encoder)
|
|
{
|
|
if (encoder && *encoder) {
|
|
fb_encoder_close(*encoder);
|
|
delete *encoder;
|
|
*encoder = nullptr;
|
|
}
|
|
}
|
|
|
|
int fb_encoder_open(FBEncoder *e)
|
|
{
|
|
if (!e) {
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
if (e->open) {
|
|
return 0;
|
|
}
|
|
|
|
int error_code;
|
|
|
|
// Create output format context
|
|
error_code = avformat_alloc_output_context2(&e->fmt_ctx, nullptr, nullptr,
|
|
e->filename.c_str());
|
|
if (error_code < 0) {
|
|
e->SetError("Failed to allocate output context", error_code);
|
|
return error_code;
|
|
}
|
|
|
|
// Initialize a video stream if it's enabled
|
|
if (e->video_enabled) {
|
|
if (!e->InitializeStream(AVMEDIA_TYPE_VIDEO, &e->video_stream,
|
|
&e->video_codec_ctx, e->video_codec)) {
|
|
return AVERROR_EXTERNAL;
|
|
}
|
|
|
|
// This is the pixel format the encoder wants to encode to
|
|
AVPixelFormat encoder_pix_fmt = e->video_codec_ctx->pix_fmt;
|
|
|
|
e->video_scale_ctx = avfilter_graph_alloc();
|
|
if (!e->video_scale_ctx) {
|
|
e->SetError("Failed to allocate filter graph");
|
|
return AVERROR_EXTERNAL;
|
|
}
|
|
|
|
static const int FILTER_ARG_SZ = 1024;
|
|
char filter_args[FILTER_ARG_SZ];
|
|
|
|
snprintf(filter_args, FILTER_ARG_SZ,
|
|
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
|
|
e->video_width, e->video_height, e->video_src_pix_fmt,
|
|
e->video_time_base_num, e->video_time_base_den,
|
|
e->video_pixel_aspect_num, e->video_pixel_aspect_den);
|
|
|
|
avfilter_graph_create_filter(&e->video_buffersrc_ctx,
|
|
avfilter_get_by_name("buffer"), "in",
|
|
filter_args, nullptr, e->video_scale_ctx);
|
|
avfilter_graph_create_filter(&e->video_buffersink_ctx,
|
|
avfilter_get_by_name("buffersink"), "out",
|
|
nullptr, nullptr, e->video_scale_ctx);
|
|
|
|
AVFilterContext *last_filter = e->video_buffersrc_ctx;
|
|
|
|
{
|
|
// Set color range
|
|
AVFilterContext *range_filter;
|
|
|
|
snprintf(filter_args, FILTER_ARG_SZ, "in_range=full:out_range=%s",
|
|
e->video_color_range == FB_COLOR_RANGE_JPEG ? "full" :
|
|
"limited");
|
|
|
|
avfilter_graph_create_filter(&range_filter,
|
|
avfilter_get_by_name("scale"), "range",
|
|
filter_args, nullptr,
|
|
e->video_scale_ctx);
|
|
|
|
avfilter_link(last_filter, 0, range_filter, 0);
|
|
last_filter = range_filter;
|
|
}
|
|
|
|
if (e->video_src_pix_fmt != encoder_pix_fmt) {
|
|
// Transform pixel format
|
|
AVFilterContext *format_filter;
|
|
|
|
snprintf(filter_args, FILTER_ARG_SZ, "pix_fmts=%u", encoder_pix_fmt);
|
|
|
|
avfilter_graph_create_filter(&format_filter,
|
|
avfilter_get_by_name("format"),
|
|
"format", filter_args, nullptr,
|
|
e->video_scale_ctx);
|
|
|
|
avfilter_link(last_filter, 0, format_filter, 0);
|
|
last_filter = format_filter;
|
|
}
|
|
|
|
avfilter_link(last_filter, 0, e->video_buffersink_ctx, 0);
|
|
|
|
if (avfilter_graph_config(e->video_scale_ctx, nullptr) < 0) {
|
|
e->SetError("Failed to configure filter graph");
|
|
return AVERROR_EXTERNAL;
|
|
}
|
|
}
|
|
|
|
// Initialize an audio stream if it's enabled
|
|
if (e->audio_enabled) {
|
|
if (!e->InitializeStream(AVMEDIA_TYPE_AUDIO, &e->audio_stream,
|
|
&e->audio_codec_ctx, e->audio_codec)) {
|
|
return AVERROR_EXTERNAL;
|
|
}
|
|
}
|
|
|
|
// Initialize a subtitle stream if it's enabled
|
|
if (e->subtitles_enabled) {
|
|
if (!e->InitializeStream(AVMEDIA_TYPE_SUBTITLE, &e->subtitle_stream,
|
|
&e->subtitle_codec_ctx, e->subtitle_codec)) {
|
|
return AVERROR_EXTERNAL;
|
|
}
|
|
}
|
|
|
|
av_dump_format(e->fmt_ctx, 0, e->filename.c_str(), 1);
|
|
|
|
// Open output file for writing
|
|
error_code = avio_open(&e->fmt_ctx->pb, e->filename.c_str(), AVIO_FLAG_WRITE);
|
|
if (error_code < 0) {
|
|
e->SetError("Failed to open IO context", error_code);
|
|
return error_code;
|
|
}
|
|
|
|
// Write header
|
|
error_code = avformat_write_header(e->fmt_ctx, nullptr);
|
|
if (error_code < 0) {
|
|
e->SetError("Failed to write format header", error_code);
|
|
return error_code;
|
|
}
|
|
|
|
e->open = true;
|
|
return 0;
|
|
}
|
|
|
|
int fb_encoder_write_video_frame(FBEncoder *e, int width, int height,
|
|
int pix_fmt, const uint8_t *data, int linesize,
|
|
double time_seconds)
|
|
{
|
|
if (!e || !e->open || !data) {
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
// Use the filter graph to convert formats/linesizes
|
|
AVFrame *input_frame = av_frame_alloc();
|
|
if (!input_frame) {
|
|
e->SetError("Failed to allocate input frame");
|
|
return AVERROR(ENOMEM);
|
|
}
|
|
|
|
input_frame->width = width;
|
|
input_frame->height = height;
|
|
input_frame->format = pix_fmt;
|
|
input_frame->data[0] = const_cast<uint8_t *>(data);
|
|
input_frame->linesize[0] = linesize;
|
|
|
|
input_frame->color_primaries = e->video_codec_ctx->color_primaries;
|
|
input_frame->color_trc = e->video_codec_ctx->color_trc;
|
|
input_frame->colorspace = e->video_codec_ctx->colorspace;
|
|
input_frame->color_range = e->video_codec_ctx->color_range;
|
|
|
|
int r = av_buffersrc_add_frame_flags(e->video_buffersrc_ctx, input_frame,
|
|
AV_BUFFERSRC_FLAG_KEEP_REF);
|
|
av_frame_free(&input_frame);
|
|
if (r < 0) {
|
|
e->SetError("Failed to add frame to filter graph", r);
|
|
return r;
|
|
}
|
|
|
|
AVFrame *encoded_frame = av_frame_alloc();
|
|
if (!encoded_frame) {
|
|
e->SetError("Failed to allocate encode frame");
|
|
return AVERROR(ENOMEM);
|
|
}
|
|
|
|
r = av_buffersink_get_frame(e->video_buffersink_ctx, encoded_frame);
|
|
if (r < 0) {
|
|
av_frame_free(&encoded_frame);
|
|
e->SetError("Failed to retrieve frame from buffer sink", r);
|
|
return r;
|
|
}
|
|
|
|
encoded_frame->pts =
|
|
llround(time_seconds / av_q2d(e->video_codec_ctx->time_base));
|
|
|
|
bool result =
|
|
e->WriteAVFrame(encoded_frame, e->video_codec_ctx, e->video_stream);
|
|
|
|
av_frame_free(&encoded_frame);
|
|
|
|
return result ? 0 : AVERROR_EXTERNAL;
|
|
}
|
|
|
|
bool FBEncoder::WriteAudioData(int sample_format, int sample_rate,
|
|
uint64_t channel_layout_mask,
|
|
const uint8_t **input_data,
|
|
int input_sample_count)
|
|
{
|
|
if (!InitializeResampleContext(sample_format, sample_rate,
|
|
channel_layout_mask)) {
|
|
SetError("Failed to initialize resample context");
|
|
return false;
|
|
}
|
|
|
|
bool result = true;
|
|
|
|
// Create output buffer
|
|
int output_sample_count =
|
|
input_sample_count ?
|
|
swr_get_out_samples(audio_resample_ctx, input_sample_count) :
|
|
102400;
|
|
uint8_t **output_data = nullptr;
|
|
int output_linesize;
|
|
av_samples_alloc_array_and_samples(
|
|
&output_data, &output_linesize,
|
|
audio_stream->codecpar->ch_layout.nb_channels, output_sample_count,
|
|
static_cast<AVSampleFormat>(audio_stream->codecpar->format), 0);
|
|
|
|
// Perform conversion
|
|
int converted = swr_convert(audio_resample_ctx, output_data,
|
|
output_sample_count, input_data,
|
|
input_sample_count);
|
|
if (converted > 0) {
|
|
// Split sample buffer into frames
|
|
for (int i = 0; i < converted;) {
|
|
int frame_remaining_samples = audio_max_samples - audio_frame_offset;
|
|
int converted_remaining_samples = converted - i;
|
|
|
|
int copy_length =
|
|
frame_remaining_samples < converted_remaining_samples ?
|
|
frame_remaining_samples :
|
|
converted_remaining_samples;
|
|
|
|
av_samples_copy(audio_frame->data, output_data, audio_frame_offset,
|
|
i, copy_length, audio_frame->ch_layout.nb_channels,
|
|
static_cast<AVSampleFormat>(audio_frame->format));
|
|
|
|
audio_frame_offset += copy_length;
|
|
i += copy_length;
|
|
|
|
if (audio_frame_offset == audio_max_samples ||
|
|
(i == converted && !input_data)) {
|
|
// Got all the samples we needed, write the frame
|
|
audio_frame->pts = av_rescale_q(
|
|
audio_write_count, { 1, audio_codec_ctx->sample_rate },
|
|
audio_codec_ctx->time_base);
|
|
|
|
WriteAVFrame(audio_frame, audio_codec_ctx, audio_stream);
|
|
audio_write_count += audio_frame_offset;
|
|
audio_frame_offset = 0;
|
|
}
|
|
}
|
|
} else if (converted < 0) {
|
|
SetError("Failed to resample audio", converted);
|
|
result = false;
|
|
}
|
|
|
|
if (!input_data && audio_frame_offset > 0) {
|
|
audio_frame->nb_samples = audio_frame_offset;
|
|
audio_frame->pts =
|
|
av_rescale_q(audio_write_count, { 1, audio_codec_ctx->sample_rate },
|
|
audio_codec_ctx->time_base);
|
|
WriteAVFrame(audio_frame, audio_codec_ctx, audio_stream);
|
|
}
|
|
|
|
// Free buffers created
|
|
if (output_data) {
|
|
av_freep(&output_data[0]);
|
|
av_freep(&output_data);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int fb_encoder_write_audio(FBEncoder *e, const uint8_t *const *channel_data,
|
|
int channels, int sample_format, int sample_rate,
|
|
uint64_t channel_layout_mask, int64_t sample_count)
|
|
{
|
|
if (!e || !e->open) {
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
if (!channel_data || sample_count == 0) {
|
|
// Nothing to write (matches the historical empty-buffer early-out)
|
|
return 0;
|
|
}
|
|
|
|
int bytes_per_sample =
|
|
av_get_bytes_per_sample(static_cast<AVSampleFormat>(sample_format));
|
|
int planar =
|
|
av_sample_fmt_is_planar(static_cast<AVSampleFormat>(sample_format));
|
|
|
|
bool result = true;
|
|
|
|
size_t start = 0;
|
|
size_t end = size_t(sample_count);
|
|
const size_t max_frame = 48000;
|
|
|
|
while (result && start < end) {
|
|
// Create input buffer
|
|
uint8_t **input_data = nullptr;
|
|
size_t input_sample_count =
|
|
(end - start) < max_frame ? (end - start) : max_frame;
|
|
int input_linesize;
|
|
|
|
int r = av_samples_alloc_array_and_samples(
|
|
&input_data, &input_linesize, channels, int(input_sample_count),
|
|
static_cast<AVSampleFormat>(sample_format), 0);
|
|
|
|
if (r < 0) {
|
|
e->SetError("Failed to allocate sample array", r);
|
|
return r;
|
|
} else {
|
|
if (planar) {
|
|
for (int i = 0; i < channels; i++) {
|
|
memcpy(input_data[i],
|
|
channel_data[i] + start * size_t(bytes_per_sample),
|
|
input_sample_count * size_t(bytes_per_sample));
|
|
}
|
|
} else {
|
|
size_t stride = size_t(bytes_per_sample) * size_t(channels);
|
|
memcpy(input_data[0], channel_data[0] + start * stride,
|
|
input_sample_count * stride);
|
|
}
|
|
|
|
start += input_sample_count;
|
|
}
|
|
|
|
result = e->WriteAudioData(sample_format, sample_rate,
|
|
channel_layout_mask,
|
|
const_cast<const uint8_t **>(input_data),
|
|
int(input_sample_count));
|
|
|
|
if (input_data) {
|
|
av_freep(&input_data[0]);
|
|
av_freep(&input_data);
|
|
}
|
|
}
|
|
|
|
return result ? 0 : AVERROR_EXTERNAL;
|
|
}
|
|
|
|
int fb_encoder_write_subtitle(FBEncoder *e, const char *utf8_text,
|
|
double in_seconds, double duration_seconds)
|
|
{
|
|
if (!e || !e->open || !utf8_text) {
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
AVPacket *pkt = av_packet_alloc();
|
|
if (!pkt) {
|
|
return AVERROR(ENOMEM);
|
|
}
|
|
|
|
pkt->stream_index = e->subtitle_stream->index;
|
|
pkt->data = reinterpret_cast<uint8_t *>(const_cast<char *>(utf8_text));
|
|
pkt->size = int(strlen(utf8_text));
|
|
|
|
// Convert seconds to the codec timebase, rounding down
|
|
double d = in_seconds / av_q2d(e->subtitle_codec_ctx->time_base);
|
|
const double eps = 0.000000000001;
|
|
int64_t pts;
|
|
if (d > ceil(d) - eps) {
|
|
pts = int64_t(ceil(d));
|
|
} else {
|
|
pts = int64_t(floor(d));
|
|
}
|
|
pkt->pts = pts;
|
|
|
|
pkt->duration = av_rescale_q(llround(duration_seconds * 1000), { 1, 1000 },
|
|
e->subtitle_codec_ctx->time_base);
|
|
pkt->dts = pkt->pts;
|
|
av_packet_rescale_ts(pkt, e->subtitle_codec_ctx->time_base,
|
|
e->subtitle_stream->time_base);
|
|
|
|
int err = av_interleaved_write_frame(e->fmt_ctx, pkt);
|
|
bool ret = true;
|
|
|
|
if (err < 0) {
|
|
e->SetError("Failed to write interleaved packet", err);
|
|
ret = false;
|
|
}
|
|
|
|
av_packet_free(&pkt);
|
|
|
|
return ret ? 0 : err;
|
|
}
|
|
|
|
void fb_encoder_close(FBEncoder *e)
|
|
{
|
|
if (!e) {
|
|
return;
|
|
}
|
|
|
|
if (e->open) {
|
|
// Flush encoders
|
|
e->FlushEncoders();
|
|
|
|
// We've written a header, so we'll write a trailer
|
|
av_write_trailer(e->fmt_ctx);
|
|
avio_closep(&e->fmt_ctx->pb);
|
|
|
|
e->open = false;
|
|
}
|
|
|
|
if (e->audio_resample_ctx) {
|
|
swr_free(&e->audio_resample_ctx);
|
|
e->audio_resample_ctx = nullptr;
|
|
}
|
|
|
|
if (e->audio_frame) {
|
|
av_frame_free(&e->audio_frame);
|
|
e->audio_frame = nullptr;
|
|
}
|
|
|
|
if (e->video_scale_ctx) {
|
|
avfilter_graph_free(&e->video_scale_ctx);
|
|
e->video_scale_ctx = nullptr;
|
|
e->video_buffersrc_ctx = nullptr;
|
|
e->video_buffersink_ctx = nullptr;
|
|
}
|
|
|
|
if (e->video_codec_ctx) {
|
|
avcodec_free_context(&e->video_codec_ctx);
|
|
e->video_codec_ctx = nullptr;
|
|
}
|
|
|
|
if (e->audio_codec_ctx) {
|
|
avcodec_free_context(&e->audio_codec_ctx);
|
|
e->audio_codec_ctx = nullptr;
|
|
}
|
|
|
|
if (e->subtitle_codec_ctx) {
|
|
avcodec_free_context(&e->subtitle_codec_ctx);
|
|
e->subtitle_codec_ctx = nullptr;
|
|
}
|
|
|
|
if (e->fmt_ctx) {
|
|
// NOTE: This also frees the streams
|
|
avformat_free_context(e->fmt_ctx);
|
|
e->fmt_ctx = nullptr;
|
|
e->video_stream = nullptr;
|
|
e->audio_stream = nullptr;
|
|
e->subtitle_stream = nullptr;
|
|
}
|
|
}
|
|
|
|
const char *fb_encoder_get_error(const FBEncoder *encoder)
|
|
{
|
|
return encoder ? encoder->error : "";
|
|
}
|
|
|
|
bool FBEncoder::WriteAVFrame(AVFrame *frame, AVCodecContext *codec_ctx,
|
|
AVStream *stream)
|
|
{
|
|
// Send raw frame to the encoder
|
|
int error_code = avcodec_send_frame(codec_ctx, frame);
|
|
if (error_code < 0) {
|
|
SetError("Failed to send frame to encoder", error_code);
|
|
return false;
|
|
}
|
|
|
|
bool succeeded = false;
|
|
|
|
AVPacket *pkt = av_packet_alloc();
|
|
|
|
// Retrieve packets from encoder
|
|
while (error_code >= 0) {
|
|
error_code = avcodec_receive_packet(codec_ctx, pkt);
|
|
|
|
// EAGAIN just means the encoder wants another frame before encoding
|
|
if (error_code == AVERROR(EAGAIN)) {
|
|
break;
|
|
} else if (error_code < 0) {
|
|
SetError("Failed to receive packet from decoder", error_code);
|
|
goto fail;
|
|
}
|
|
|
|
// Set packet stream index
|
|
pkt->stream_index = stream->index;
|
|
|
|
av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
|
|
|
|
// Write packet to file
|
|
error_code = av_interleaved_write_frame(fmt_ctx, pkt);
|
|
if (error_code < 0) {
|
|
SetError("Failed to write interleaved packet", error_code);
|
|
goto fail;
|
|
}
|
|
|
|
// Unref packet in case we're getting another
|
|
av_packet_unref(pkt);
|
|
}
|
|
|
|
succeeded = true;
|
|
|
|
fail:
|
|
av_packet_free(&pkt);
|
|
|
|
return succeeded;
|
|
}
|
|
|
|
bool FBEncoder::InitializeStream(AVMediaType type, AVStream **stream_ptr,
|
|
AVCodecContext **codec_ctx_ptr, int codec)
|
|
{
|
|
if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO &&
|
|
type != AVMEDIA_TYPE_SUBTITLE) {
|
|
SetError("Cannot initialize a stream that is not a video, audio, or subtitle type");
|
|
return false;
|
|
}
|
|
|
|
// Find encoder
|
|
const AVCodec *encoder = FindEncoder(codec, audio_sample_format);
|
|
if (!encoder) {
|
|
char msg[128];
|
|
snprintf(msg, sizeof(msg), "Failed to find codec for 0x%x", codec);
|
|
SetError(msg);
|
|
return false;
|
|
}
|
|
|
|
if (encoder->type != type) {
|
|
SetError("Retrieved unexpected codec type for codec");
|
|
return false;
|
|
}
|
|
|
|
if (!InitializeCodecContext(stream_ptr, codec_ctx_ptr, encoder)) {
|
|
return false;
|
|
}
|
|
|
|
// Set codec parameters
|
|
AVCodecContext *codec_ctx = *codec_ctx_ptr;
|
|
AVStream *stream = *stream_ptr;
|
|
|
|
if (type == AVMEDIA_TYPE_VIDEO) {
|
|
codec_ctx->width = video_width;
|
|
codec_ctx->height = video_height;
|
|
codec_ctx->sample_aspect_ratio = { video_pixel_aspect_num,
|
|
video_pixel_aspect_den };
|
|
codec_ctx->time_base = { video_time_base_num, video_time_base_den };
|
|
codec_ctx->framerate = { video_frame_rate_num, video_frame_rate_den };
|
|
codec_ctx->pix_fmt = av_get_pix_fmt(video_pix_fmt.c_str());
|
|
codec_ctx->color_range = video_color_range == FB_COLOR_RANGE_JPEG ?
|
|
AVCOL_RANGE_JPEG :
|
|
AVCOL_RANGE_MPEG;
|
|
|
|
if (video_field_order != FB_FIELD_ORDER_PROGRESSIVE) {
|
|
// FIXME: I actually don't know what these flags do, the documentation helpfully doesn't
|
|
// explain them at all. I hope using both of them is the right thing to do.
|
|
codec_ctx->flags |= AV_CODEC_FLAG_INTERLACED_DCT |
|
|
AV_CODEC_FLAG_INTERLACED_ME;
|
|
|
|
if (video_field_order == FB_FIELD_ORDER_TT) {
|
|
codec_ctx->field_order = AV_FIELD_TT;
|
|
} else {
|
|
codec_ctx->field_order = AV_FIELD_BB;
|
|
|
|
if (video_codec == FB_CODEC_H264 ||
|
|
video_codec == FB_CODEC_H264RGB) {
|
|
// For some reason, FFmpeg doesn't set libx264's bff flag so we have to do it ourselves
|
|
av_opt_set(codec_ctx->priv_data, "x264opts", "bff=1",
|
|
AV_OPT_SEARCH_CHILDREN);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set custom options
|
|
for (const auto &opt : video_opts) {
|
|
av_opt_set(codec_ctx->priv_data, opt.first.c_str(),
|
|
opt.second.c_str(), AV_OPT_SEARCH_CHILDREN);
|
|
}
|
|
|
|
if (video_bit_rate > 0) {
|
|
codec_ctx->bit_rate = video_bit_rate;
|
|
}
|
|
|
|
if (video_min_bit_rate > 0) {
|
|
codec_ctx->rc_min_rate = video_min_bit_rate;
|
|
}
|
|
|
|
if (video_max_bit_rate > 0) {
|
|
codec_ctx->rc_max_rate = video_max_bit_rate;
|
|
}
|
|
|
|
if (video_buffer_size > 0) {
|
|
codec_ctx->rc_buffer_size = static_cast<int>(video_buffer_size);
|
|
}
|
|
|
|
// nclc tags. See https://ffmpeg.org/doxygen/4.0/pixfmt_8h.html#ad384ee5a840bafd73daef08e6d9cafe7
|
|
if (video_color_srgb) {
|
|
codec_ctx->color_primaries = AVCOL_PRI_BT709;
|
|
codec_ctx->color_trc = AVCOL_TRC_IEC61966_2_1;
|
|
codec_ctx->colorspace = AVCOL_SPC_BT709;
|
|
} else { // Assume Rec.709
|
|
codec_ctx->color_primaries = AVCOL_PRI_BT709;
|
|
codec_ctx->color_trc = AVCOL_TRC_BT709;
|
|
codec_ctx->colorspace = AVCOL_SPC_BT709;
|
|
}
|
|
|
|
} else if (type == AVMEDIA_TYPE_AUDIO) {
|
|
codec_ctx->sample_rate = audio_sample_rate;
|
|
av_channel_layout_from_mask(&codec_ctx->ch_layout,
|
|
audio_channel_layout_mask);
|
|
codec_ctx->sample_fmt =
|
|
static_cast<AVSampleFormat>(audio_sample_format);
|
|
codec_ctx->time_base = { 1, codec_ctx->sample_rate };
|
|
|
|
if (audio_bit_rate > 0) {
|
|
codec_ctx->bit_rate = audio_bit_rate;
|
|
}
|
|
|
|
} else if (type == AVMEDIA_TYPE_SUBTITLE) {
|
|
codec_ctx->time_base = av_get_time_base_q();
|
|
|
|
if (!subtitle_header.empty()) {
|
|
codec_ctx->subtitle_header =
|
|
new uint8_t[subtitle_header.size()];
|
|
memcpy(codec_ctx->subtitle_header, subtitle_header.data(),
|
|
subtitle_header.size());
|
|
codec_ctx->subtitle_header_size = int(subtitle_header.size());
|
|
}
|
|
}
|
|
|
|
if (!SetupCodecContext(stream, codec_ctx, encoder)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FBEncoder::InitializeCodecContext(AVStream **stream,
|
|
AVCodecContext **codec_ctx,
|
|
const AVCodec *codec)
|
|
{
|
|
*stream = avformat_new_stream(fmt_ctx, nullptr);
|
|
if (!(*stream)) {
|
|
SetError("Failed to allocate AVStream");
|
|
return false;
|
|
}
|
|
|
|
// Allocate a codec context
|
|
*codec_ctx = avcodec_alloc_context3(codec);
|
|
if (!(*codec_ctx)) {
|
|
SetError("Failed to allocate AVCodecContext");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FBEncoder::SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx,
|
|
const AVCodec *codec)
|
|
{
|
|
int error_code;
|
|
|
|
if (fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
|
|
codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
|
}
|
|
|
|
AVDictionary *codec_opts = nullptr;
|
|
|
|
// Set thread count
|
|
if (video_threads == 0) {
|
|
av_dict_set(&codec_opts, "threads", "auto", 0);
|
|
} else {
|
|
char thread_val[16];
|
|
snprintf(thread_val, sizeof(thread_val), "%d", video_threads);
|
|
av_dict_set(&codec_opts, "threads", thread_val, 0);
|
|
}
|
|
|
|
// Try to open encoder
|
|
error_code = avcodec_open2(codec_ctx, codec, &codec_opts);
|
|
av_dict_free(&codec_opts);
|
|
if (error_code < 0) {
|
|
SetError("Failed to open encoder", error_code);
|
|
return false;
|
|
}
|
|
|
|
// Copy context settings to codecpar object
|
|
error_code = avcodec_parameters_from_context(stream->codecpar, codec_ctx);
|
|
if (error_code < 0) {
|
|
SetError("Failed to copy codec parameters to stream", error_code);
|
|
return false;
|
|
}
|
|
|
|
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
|
stream->avg_frame_rate = codec_ctx->framerate;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FBEncoder::FlushEncoders()
|
|
{
|
|
if (video_codec_ctx) {
|
|
FlushCodecCtx(video_codec_ctx, video_stream);
|
|
}
|
|
|
|
if (audio_codec_ctx) {
|
|
FlushCodecCtx(audio_codec_ctx, audio_stream);
|
|
}
|
|
|
|
if (fmt_ctx) {
|
|
if (fmt_ctx->oformat->flags) {
|
|
int r = av_interleaved_write_frame(fmt_ctx, nullptr);
|
|
if (r < 0) {
|
|
SetError("Failed to write interleaved packet", r);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FBEncoder::FlushCodecCtx(AVCodecContext *codec_ctx, AVStream *stream)
|
|
{
|
|
avcodec_send_frame(codec_ctx, nullptr);
|
|
AVPacket *pkt = av_packet_alloc();
|
|
|
|
int error_code;
|
|
do {
|
|
error_code = avcodec_receive_packet(codec_ctx, pkt);
|
|
|
|
if (error_code < 0) {
|
|
break;
|
|
}
|
|
|
|
pkt->stream_index = stream->index;
|
|
av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
|
|
int r = av_interleaved_write_frame(fmt_ctx, pkt);
|
|
if (r < 0) {
|
|
SetError("Failed to write interleaved packet", r);
|
|
break;
|
|
}
|
|
av_packet_unref(pkt);
|
|
} while (error_code >= 0);
|
|
|
|
av_packet_free(&pkt);
|
|
}
|
|
|
|
bool FBEncoder::InitializeResampleContext(int sample_format, int sample_rate,
|
|
uint64_t channel_layout_mask)
|
|
{
|
|
if (audio_resample_ctx) {
|
|
return true;
|
|
}
|
|
|
|
AVChannelLayout layout;
|
|
fb::ChannelLayoutFromMask(&layout, channel_layout_mask, 0);
|
|
|
|
// Create resample context
|
|
swr_alloc_set_opts2(&audio_resample_ctx, &audio_codec_ctx->ch_layout,
|
|
audio_codec_ctx->sample_fmt,
|
|
audio_codec_ctx->sample_rate, &layout,
|
|
static_cast<AVSampleFormat>(sample_format), sample_rate,
|
|
0, nullptr);
|
|
av_channel_layout_uninit(&layout);
|
|
|
|
if (!audio_resample_ctx) {
|
|
return false;
|
|
}
|
|
|
|
int err = swr_init(audio_resample_ctx);
|
|
if (err < 0) {
|
|
SetError("Failed to create resampling context", err);
|
|
return false;
|
|
}
|
|
|
|
audio_max_samples = audio_codec_ctx->frame_size;
|
|
if (!audio_max_samples) {
|
|
// If not set, use another frame size
|
|
if (video_enabled) {
|
|
// If we're encoding video, use enough samples to cover roughly one frame of video
|
|
audio_max_samples =
|
|
int(int64_t(audio_sample_rate) * video_time_base_num /
|
|
video_time_base_den);
|
|
} else {
|
|
// If no video, just use an arbitrary number
|
|
audio_max_samples = 256;
|
|
}
|
|
}
|
|
|
|
audio_frame = av_frame_alloc();
|
|
if (!audio_frame) {
|
|
return false;
|
|
}
|
|
|
|
audio_frame->ch_layout = audio_codec_ctx->ch_layout;
|
|
audio_frame->format = audio_codec_ctx->sample_fmt;
|
|
audio_frame->nb_samples = audio_max_samples;
|
|
|
|
err = av_frame_get_buffer(audio_frame, 0);
|
|
if (err < 0) {
|
|
SetError("Failed to create audio frame", err);
|
|
return false;
|
|
}
|
|
|
|
audio_frame_offset = 0;
|
|
audio_write_count = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
int fb_encoder_codec_get_pixel_formats(int codec, const char **names,
|
|
int max_names)
|
|
{
|
|
const AVCodec *codec_info = FindEncoder(codec, FB_SAMPLE_FMT_NONE);
|
|
if (!codec_info || !codec_info->pix_fmts) {
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
for (int i = 0; codec_info->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
|
|
AVPixelFormat fmt = codec_info->pix_fmts[i];
|
|
if (ConvertJPEGSpaceToRegularSpace(fmt) != fmt) {
|
|
// This is a deprecated "JPEG" space, skip it
|
|
continue;
|
|
}
|
|
|
|
if (names && count < max_names) {
|
|
names[count] = av_get_pix_fmt_name(fmt);
|
|
}
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
int fb_encoder_codec_get_sample_formats(int codec, int *fmts, int max_fmts)
|
|
{
|
|
const AVCodec *codec_info = FindEncoder(codec, FB_SAMPLE_FMT_NONE);
|
|
if (!codec_info || !codec_info->sample_fmts) {
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
for (int i = 0; codec_info->sample_fmts[i] != AV_SAMPLE_FMT_NB; i++) {
|
|
if (fmts && count < max_fmts) {
|
|
fmts[count] = codec_info->sample_fmts[i];
|
|
}
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic pop
|
|
#endif
|