1293 lines
35 KiB
C++
1293 lines
35 KiB
C++
/*
|
|
* Oak Video Editor - Non-Linear Video Editor
|
|
* Copyright (C) 2025 Olive CE Team
|
|
*
|
|
* 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 "ffmpegdecoder.h"
|
|
|
|
extern "C" {
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavformat/avformat.h>
|
|
#include <libavutil/avutil.h>
|
|
#include <libavutil/frame.h>
|
|
}
|
|
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QString>
|
|
#include <QtMath>
|
|
#include <QThread>
|
|
|
|
#include "codec/planarfiledevice.h"
|
|
#include "common/ffmpegutils.h"
|
|
#include "common/filefunctions.h"
|
|
#include "render/renderer.h"
|
|
#include "render/subtitleparams.h"
|
|
|
|
namespace olive
|
|
{
|
|
|
|
QVariant Yuv2RgbShader;
|
|
QVariant DeinterlaceShader;
|
|
|
|
namespace {
|
|
|
|
constexpr int64_t kAnalyzeDurationUs = 5000000;
|
|
constexpr int64_t kProbeSizeBytes = 20000000;
|
|
|
|
void ApplyFormatOpenOptions(AVDictionary **opts)
|
|
{
|
|
av_dict_set_int(opts, "analyzeduration", kAnalyzeDurationUs, 0);
|
|
av_dict_set_int(opts, "probesize", kProbeSizeBytes, 0);
|
|
}
|
|
|
|
void TuneFormatContext(AVFormatContext *ctx)
|
|
{
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
ctx->probesize = kProbeSizeBytes;
|
|
ctx->max_analyze_duration = kAnalyzeDurationUs;
|
|
}
|
|
|
|
void DiscardSubtitleStreams(AVFormatContext *ctx)
|
|
{
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
for (unsigned int i = 0; i < ctx->nb_streams; i++) {
|
|
AVStream *stream = ctx->streams[i];
|
|
if (stream && stream->codecpar &&
|
|
stream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
|
|
stream->discard = AVDISCARD_ALL;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
FFmpegDecoder::FFmpegDecoder()
|
|
: sws_ctx_(nullptr)
|
|
, working_packet_(nullptr)
|
|
, cache_at_zero_(false)
|
|
, cache_at_eof_(false)
|
|
{
|
|
}
|
|
|
|
bool FFmpegDecoder::OpenInternal()
|
|
{
|
|
if (instance_.Open(stream().filename().toUtf8(), stream().stream())) {
|
|
AVStream *s = instance_.avstream();
|
|
|
|
// Store one second in the source's timebase
|
|
second_ts_ = qRound64(av_q2d(av_inv_q(s->time_base)));
|
|
|
|
working_packet_ = av_packet_alloc();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
TexturePtr FFmpegDecoder::ProcessFrameIntoTexture(AVFramePtr f,
|
|
const RetrieveVideoParams &p,
|
|
const AVFramePtr original)
|
|
{
|
|
// Determine native format
|
|
AVPixelFormat ideal_fmt = FFmpegUtils::GetCompatiblePixelFormat(
|
|
static_cast<AVPixelFormat>(f->format));
|
|
PixelFormat native_fmt = GetNativePixelFormat(ideal_fmt);
|
|
int native_channels = GetNativeChannelCount(ideal_fmt);
|
|
|
|
// Set up video params
|
|
VideoParams vp(original->width, original->height, native_fmt,
|
|
native_channels,
|
|
av_guess_sample_aspect_ratio(instance_.fmt_ctx(),
|
|
instance_.avstream(), nullptr),
|
|
VideoParams::kInterlaceNone, p.divider);
|
|
|
|
// Create texture
|
|
TexturePtr tex = p.renderer->CreateTexture(vp);
|
|
|
|
switch (f->format) {
|
|
case AV_PIX_FMT_YUV420P:
|
|
case AV_PIX_FMT_YUV422P:
|
|
case AV_PIX_FMT_YUV444P:
|
|
case AV_PIX_FMT_YUV420P10LE:
|
|
case AV_PIX_FMT_YUV422P10LE:
|
|
case AV_PIX_FMT_YUV444P10LE:
|
|
case AV_PIX_FMT_YUV420P12LE:
|
|
case AV_PIX_FMT_YUV422P12LE:
|
|
case AV_PIX_FMT_YUV444P12LE: {
|
|
// Run through YUV to RGB shader
|
|
if (Yuv2RgbShader.isNull()) {
|
|
// Compile shader
|
|
Yuv2RgbShader = p.renderer->CreateNativeShader(
|
|
ShaderCode(FileFunctions::ReadFileAsString(
|
|
QStringLiteral(":/shaders/yuv2rgb.frag"))));
|
|
if (Yuv2RgbShader.isNull()) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
int px_size;
|
|
int bits_per_pixel;
|
|
switch (f->format) {
|
|
case AV_PIX_FMT_YUV420P:
|
|
case AV_PIX_FMT_YUV422P:
|
|
case AV_PIX_FMT_YUV444P:
|
|
default:
|
|
px_size = 1;
|
|
bits_per_pixel = 8;
|
|
break;
|
|
case AV_PIX_FMT_YUV420P10LE:
|
|
case AV_PIX_FMT_YUV422P10LE:
|
|
case AV_PIX_FMT_YUV444P10LE:
|
|
px_size = 2;
|
|
bits_per_pixel = 10;
|
|
break;
|
|
case AV_PIX_FMT_YUV420P12LE:
|
|
case AV_PIX_FMT_YUV422P12LE:
|
|
case AV_PIX_FMT_YUV444P12LE:
|
|
px_size = 2;
|
|
bits_per_pixel = 12;
|
|
break;
|
|
}
|
|
|
|
AVFramePtr hw_in = f;
|
|
|
|
VideoParams plane_params = vp;
|
|
plane_params.set_channel_count(1);
|
|
plane_params.set_format(native_fmt);
|
|
|
|
TexturePtr y_plane = p.renderer->CreateTexture(
|
|
plane_params, hw_in->data[0], hw_in->linesize[0] / px_size);
|
|
y_plane->handleFrame(hw_in);
|
|
switch (f->format) {
|
|
case AV_PIX_FMT_YUV420P:
|
|
case AV_PIX_FMT_YUV422P:
|
|
case AV_PIX_FMT_YUV420P10LE:
|
|
case AV_PIX_FMT_YUV422P10LE:
|
|
case AV_PIX_FMT_YUV420P12LE:
|
|
case AV_PIX_FMT_YUV422P12LE:
|
|
plane_params.set_width(plane_params.width() / 2);
|
|
break;
|
|
}
|
|
|
|
switch (f->format) {
|
|
case AV_PIX_FMT_YUV420P:
|
|
case AV_PIX_FMT_YUV420P10LE:
|
|
case AV_PIX_FMT_YUV420P12LE:
|
|
plane_params.set_height(plane_params.height() / 2);
|
|
break;
|
|
}
|
|
|
|
TexturePtr u_plane = p.renderer->CreateTexture(
|
|
plane_params, hw_in->data[1], hw_in->linesize[1] / px_size);
|
|
u_plane->handleFrame(hw_in);
|
|
|
|
TexturePtr v_plane = p.renderer->CreateTexture(
|
|
plane_params, hw_in->data[2], hw_in->linesize[2] / px_size);
|
|
v_plane->handleFrame(hw_in);
|
|
|
|
ShaderJob job;
|
|
job.Insert(QStringLiteral("y_channel"),
|
|
NodeValue(NodeValue::kTexture,
|
|
QVariant::fromValue(y_plane)));
|
|
job.Insert(QStringLiteral("u_channel"),
|
|
NodeValue(NodeValue::kTexture,
|
|
QVariant::fromValue(u_plane)));
|
|
job.Insert(QStringLiteral("v_channel"),
|
|
NodeValue(NodeValue::kTexture,
|
|
QVariant::fromValue(v_plane)));
|
|
job.Insert(QStringLiteral("bits_per_pixel"),
|
|
NodeValue(NodeValue::kInt, bits_per_pixel));
|
|
job.Insert(QStringLiteral("full_range"),
|
|
NodeValue(NodeValue::kBoolean,
|
|
hw_in->color_range == AVCOL_RANGE_JPEG));
|
|
|
|
const int *yuv_coeffs = sws_getCoefficients(
|
|
FFmpegUtils::GetSwsColorspaceFromAVColorSpace(hw_in->colorspace));
|
|
job.Insert(QStringLiteral("yuv_crv"),
|
|
NodeValue(NodeValue::kFloat, yuv_coeffs[0] / 65536.0));
|
|
job.Insert(QStringLiteral("yuv_cgu"),
|
|
NodeValue(NodeValue::kFloat, yuv_coeffs[2] / 65536.0));
|
|
job.Insert(QStringLiteral("yuv_cgv"),
|
|
NodeValue(NodeValue::kFloat, yuv_coeffs[3] / 65536.0));
|
|
job.Insert(QStringLiteral("yuv_cbu"),
|
|
NodeValue(NodeValue::kFloat, yuv_coeffs[1] / 65536.0));
|
|
|
|
tex = p.renderer->CreateTexture(vp);
|
|
p.renderer->BlitToTexture(Yuv2RgbShader, job, tex.get(), false);
|
|
break;
|
|
}
|
|
case AV_PIX_FMT_RGBA:
|
|
case AV_PIX_FMT_RGBA64LE:
|
|
// RGBA can be uploaded directly to the texture
|
|
tex->handleFrame(f);
|
|
tex->Upload(f->data[0], f->linesize[0] / vp.GetBytesPerPixel());
|
|
break;
|
|
}
|
|
|
|
// Deinterlace if necessary
|
|
if (p.src_interlacing != VideoParams::kInterlaceNone) {
|
|
if (DeinterlaceShader.isNull()) {
|
|
// Compile shader
|
|
DeinterlaceShader = p.renderer->CreateNativeShader(
|
|
ShaderCode(FileFunctions::ReadFileAsString(
|
|
QStringLiteral(":/shaders/deinterlace2.frag"))));
|
|
if (DeinterlaceShader.isNull()) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
rational frame_rate_tb = av_guess_frame_rate(
|
|
instance_.fmt_ctx(), instance_.avstream(), original.get());
|
|
|
|
// Double frame rate for interlaced fields
|
|
frame_rate_tb *= 2;
|
|
|
|
// Flip frame rate so it can be used as a timebase
|
|
frame_rate_tb.flip();
|
|
|
|
int64_t req = Timecode::time_to_timestamp(
|
|
p.time + rational(instance_.fmt_ctx()->start_time, AV_TIME_BASE),
|
|
frame_rate_tb);
|
|
int64_t frm = Timecode::rescale_timestamp(
|
|
original->pts, instance_.avstream()->time_base, frame_rate_tb);
|
|
|
|
bool first = (req == frm);
|
|
bool top_first =
|
|
(p.src_interlacing == VideoParams::kInterlacedTopFirst);
|
|
|
|
int interlacing = (first == top_first) ? 1 : 2;
|
|
|
|
TexturePtr deinterlaced = p.renderer->CreateTexture(tex->params());
|
|
|
|
ShaderJob job;
|
|
job.Insert(QStringLiteral("ove_maintex"),
|
|
NodeValue(NodeValue::kTexture, tex));
|
|
job.Insert(QStringLiteral("interlacing"),
|
|
NodeValue(NodeValue::kInt, interlacing));
|
|
job.Insert(QStringLiteral("pixel_height"),
|
|
NodeValue(NodeValue::kInt, original->height));
|
|
|
|
p.renderer->BlitToTexture(DeinterlaceShader, job, deinterlaced.get(),
|
|
false);
|
|
|
|
tex = deinterlaced;
|
|
}
|
|
|
|
return tex;
|
|
}
|
|
|
|
TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p)
|
|
{
|
|
if (AVFramePtr f = RetrieveFrame(p.time, p.cancelled)) {
|
|
if (p.cancelled && p.cancelled->IsCancelled()) {
|
|
return nullptr;
|
|
}
|
|
|
|
AVFramePtr original = f;
|
|
|
|
// Disregard "JPEG" pixel formats because we allow the user to override that
|
|
f->format = FFmpegUtils::ConvertJPEGSpaceToRegularSpace(
|
|
static_cast<AVPixelFormat>(f->format));
|
|
|
|
// Force frame's color range to whatever it's set to in Olive
|
|
f->color_range = p.force_range == VideoParams::kColorRangeFull ?
|
|
AVCOL_RANGE_JPEG :
|
|
AVCOL_RANGE_MPEG;
|
|
|
|
// Perform any CPU processing required
|
|
AVFramePtr ptr = PreProcessFrame(f, p);
|
|
f=std::move(ptr);
|
|
if (!f) {
|
|
// Error occurred while software scaling
|
|
return nullptr;
|
|
}
|
|
|
|
// Finally, perform any GPU processing required
|
|
TexturePtr texture = ProcessFrameIntoTexture(f, p, original);
|
|
|
|
return texture;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void FFmpegDecoder::CloseInternal()
|
|
{
|
|
if (working_packet_) {
|
|
av_packet_free(&working_packet_);
|
|
working_packet_ = nullptr;
|
|
}
|
|
|
|
ClearFrameCache();
|
|
FreeScaler();
|
|
|
|
instance_.Close();
|
|
}
|
|
|
|
rational FFmpegDecoder::GetAudioStartOffset() const
|
|
{
|
|
auto f = instance_.fmt_ctx();
|
|
if (f) {
|
|
rational fmt_start =
|
|
rational(instance_.fmt_ctx()->start_time, AV_TIME_BASE);
|
|
rational str_start = rational(instance_.avstream()->time_base) *
|
|
instance_.avstream()->start_time;
|
|
return str_start - fmt_start;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
QString FFmpegDecoder::id() const
|
|
{
|
|
return QStringLiteral("ffmpeg");
|
|
}
|
|
|
|
FootageDescription FFmpegDecoder::Probe(const QString &filename,
|
|
CancelAtom *cancelled) const
|
|
{
|
|
// Return value
|
|
FootageDescription desc(id());
|
|
|
|
// Variable for receiving errors from FFmpeg
|
|
int error_code;
|
|
|
|
// Convert QString to a C string
|
|
QByteArray filename_c = filename.toUtf8();
|
|
|
|
// Open file in a format context
|
|
AVFormatContext *fmt_ctx = nullptr;
|
|
AVDictionary *format_opts = nullptr;
|
|
ApplyFormatOpenOptions(&format_opts);
|
|
error_code = avformat_open_input(&fmt_ctx, filename_c, nullptr, &format_opts);
|
|
av_dict_free(&format_opts);
|
|
TuneFormatContext(fmt_ctx);
|
|
DiscardSubtitleStreams(fmt_ctx);
|
|
|
|
// Handle format context error
|
|
if (error_code == 0) {
|
|
// Retrieve metadata about the media
|
|
avformat_find_stream_info(fmt_ctx, nullptr);
|
|
|
|
int64_t footage_duration = fmt_ctx->duration;
|
|
|
|
bool duration_guessed_from_bitrate =
|
|
(fmt_ctx->duration_estimation_method ==
|
|
AVFMT_DURATION_FROM_BITRATE);
|
|
if (duration_guessed_from_bitrate) {
|
|
qWarning()
|
|
<< "Unreliable duration detected - we will manually determine it ourselves (this may take some time)";
|
|
}
|
|
|
|
// Dump it into the Footage object
|
|
int video_streams = 0, audio_streams = 0, still_streams = 0;
|
|
|
|
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
|
|
// FFmpeg AVStream
|
|
AVStream *avstream = fmt_ctx->streams[i];
|
|
|
|
// Find decoder for this stream, if it exists we can proceed
|
|
const AVCodec *decoder =
|
|
avcodec_find_decoder(avstream->codecpar->codec_id);
|
|
|
|
if (decoder &&
|
|
(avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
|
|
avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO ||
|
|
avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE)) {
|
|
if (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
|
AVPixelFormat compatible_pix_fmt = AV_PIX_FMT_NONE;
|
|
|
|
bool image_is_still = false;
|
|
rational pixel_aspect_ratio;
|
|
rational frame_rate;
|
|
VideoParams::Interlacing interlacing =
|
|
VideoParams::kInterlaceNone;
|
|
|
|
{
|
|
// Read at least two frames to get more information about this video stream
|
|
AVPacket *pkt = av_packet_alloc();
|
|
AVFrame *frame = av_frame_alloc();
|
|
|
|
VideoParams::Interlacing interlacing = VideoParams::kInterlaceNone;
|
|
AVRational pixel_aspect_ratio = {1, 1};
|
|
AVRational frame_rate = avstream->avg_frame_rate;
|
|
AVPixelFormat compatible_pix_fmt =
|
|
FFmpegUtils::GetCompatiblePixelFormat(
|
|
static_cast<AVPixelFormat>(avstream->codecpar->format));
|
|
bool image_is_still = false;
|
|
|
|
{
|
|
Instance instance;
|
|
if (instance.Open(filename_c, avstream->index) != 0)
|
|
goto cleanup;
|
|
|
|
AVCodecContext *avctx = instance.codec_ctx();
|
|
interlacing = FFmpegFieldOrderToOlive(avctx->field_order);
|
|
|
|
if (instance.GetFrame(pkt, frame) >= 0) {
|
|
pixel_aspect_ratio =
|
|
av_guess_sample_aspect_ratio(instance.fmt_ctx(),
|
|
instance.avstream(), frame);
|
|
frame_rate =
|
|
av_guess_frame_rate(instance.fmt_ctx(),
|
|
instance.avstream(), frame);
|
|
}
|
|
|
|
int ret = instance.GetFrame(pkt, frame);
|
|
if (ret == AVERROR_EOF) {
|
|
image_is_still = true;
|
|
} else if (avstream->duration == AV_NOPTS_VALUE ||
|
|
duration_guessed_from_bitrate) {
|
|
int64_t last_ts = frame->best_effort_timestamp;
|
|
while (instance.GetFrame(pkt, frame) >= 0 &&
|
|
(!cancelled || !cancelled->IsCancelled()))
|
|
last_ts = frame->best_effort_timestamp;
|
|
avstream->duration = last_ts;
|
|
}
|
|
|
|
instance.Close();
|
|
}
|
|
|
|
cleanup:
|
|
av_frame_free(&frame);
|
|
av_packet_free(&pkt);
|
|
|
|
VideoParams stream;
|
|
stream.set_stream_index(i);
|
|
stream.set_width(avstream->codecpar->width);
|
|
stream.set_height(avstream->codecpar->height);
|
|
stream.set_video_type(image_is_still ? VideoParams::kVideoTypeStill
|
|
: VideoParams::kVideoTypeVideo);
|
|
stream.set_format(GetNativePixelFormat(compatible_pix_fmt));
|
|
stream.set_channel_count(GetNativeChannelCount(compatible_pix_fmt));
|
|
stream.set_interlacing(interlacing); // <-- 已正确填充
|
|
stream.set_pixel_aspect_ratio(pixel_aspect_ratio);
|
|
stream.set_frame_rate(frame_rate);
|
|
stream.set_start_time(avstream->start_time);
|
|
stream.set_time_base(avstream->time_base);
|
|
stream.set_duration(avstream->duration);
|
|
stream.set_color_range(avstream->codecpar->color_range == AVCOL_RANGE_JPEG
|
|
? VideoParams::kColorRangeFull
|
|
: VideoParams::kColorRangeLimited);
|
|
stream.set_premultiplied_alpha(false);
|
|
|
|
desc.AddVideoStream(stream);
|
|
image_is_still ? still_streams++ : video_streams++;
|
|
}
|
|
|
|
|
|
} else if (avstream->codecpar->codec_type ==
|
|
AVMEDIA_TYPE_AUDIO) {
|
|
// Create an audio stream object
|
|
AVChannelLayout &channel_layout =
|
|
avstream->codecpar->ch_layout;
|
|
if (!av_channel_layout_check(&channel_layout)) {
|
|
av_channel_layout_default(
|
|
&channel_layout,
|
|
avstream->codecpar->ch_layout.nb_channels);
|
|
}
|
|
|
|
if (avstream->duration == AV_NOPTS_VALUE ||
|
|
duration_guessed_from_bitrate) {
|
|
// Loop through stream until we get the whole duration
|
|
if (footage_duration == AV_NOPTS_VALUE ||
|
|
duration_guessed_from_bitrate) {
|
|
Instance instance;
|
|
instance.Open(filename_c, avstream->index);
|
|
|
|
AVPacket *pkt = av_packet_alloc();
|
|
AVFrame *frame = av_frame_alloc();
|
|
|
|
int64_t new_dur;
|
|
|
|
do {
|
|
new_dur = frame->best_effort_timestamp;
|
|
} while (instance.GetFrame(pkt, frame) >= 0 &&
|
|
(!cancelled || !cancelled->IsCancelled()));
|
|
|
|
avstream->duration = new_dur;
|
|
|
|
av_frame_free(&frame);
|
|
av_packet_free(&pkt);
|
|
|
|
instance.Close();
|
|
} else {
|
|
avstream->duration =
|
|
Timecode::rescale_timestamp_ceil(
|
|
footage_duration, rational(1, AV_TIME_BASE),
|
|
avstream->time_base);
|
|
}
|
|
}
|
|
|
|
AudioParams stream;
|
|
stream.set_stream_index(i);
|
|
stream.set_channel_layout(channel_layout);
|
|
stream.set_sample_rate(avstream->codecpar->sample_rate);
|
|
stream.set_format(FFmpegUtils::GetNativeSampleFormat(
|
|
static_cast<AVSampleFormat>(
|
|
avstream->codecpar->format)));
|
|
stream.set_time_base(avstream->time_base);
|
|
stream.set_duration(avstream->duration);
|
|
desc.AddAudioStream(stream);
|
|
|
|
audio_streams++;
|
|
|
|
} else if (avstream->codecpar->codec_type ==
|
|
AVMEDIA_TYPE_SUBTITLE) {
|
|
// Limit to SRT for now...
|
|
if (avstream->codecpar->codec_id == AV_CODEC_ID_SUBRIP) {
|
|
SubtitleParams sub;
|
|
|
|
AVPacket *pkt = av_packet_alloc();
|
|
{
|
|
Instance instance;
|
|
instance.Open(filename_c, avstream->index);
|
|
|
|
while (instance.GetPacket(pkt) >= 0) {
|
|
TimeRange time(Timecode::timestamp_to_time(
|
|
pkt->pts,
|
|
avstream->time_base),
|
|
Timecode::timestamp_to_time(
|
|
pkt->pts + pkt->duration,
|
|
avstream->time_base));
|
|
|
|
QString text = QString::fromUtf8(
|
|
(const char *)pkt->data, pkt->size);
|
|
|
|
sub.push_back(Subtitle(time, text));
|
|
}
|
|
|
|
instance.Close();
|
|
}
|
|
av_packet_free(&pkt);
|
|
|
|
desc.AddSubtitleStream(sub);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
desc.SetStreamCount(fmt_ctx->nb_streams);
|
|
|
|
if (video_streams == 0 && audio_streams > 0 && still_streams > 0) {
|
|
// This footage has no video streams, but has audio and image streams. We've probably
|
|
// imported a song with embedded album art that most people don't care about. We'll keep the
|
|
// stills referenced in case users do, but we'll default them to disabled so they're
|
|
// easier to work with.
|
|
for (VideoParams &vp : desc.GetVideoStreams()) {
|
|
vp.set_enabled(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Free all memory
|
|
avformat_close_input(&fmt_ctx);
|
|
|
|
return desc;
|
|
}
|
|
|
|
QString FFmpegDecoder::FFmpegError(int error_code)
|
|
{
|
|
char err[1024];
|
|
av_strerror(error_code, err, 512);
|
|
return QStringLiteral("%1 %2").arg(QString::number(error_code), err);
|
|
}
|
|
|
|
bool FFmpegDecoder::ConformAudioInternal(const QVector<QString> &filenames,
|
|
const AudioParams ¶ms,
|
|
CancelAtom *cancelled)
|
|
{
|
|
// Iterate through each audio frame and extract the PCM data
|
|
|
|
// Seek to starting point
|
|
instance_.Seek(0);
|
|
|
|
// Handle NULL channel layout
|
|
AVChannelLayout channel_layout =
|
|
ValidateChannelLayout(instance_.avstream());
|
|
if (!av_channel_layout_check(&channel_layout)) {
|
|
qCritical()
|
|
<< "Failed to determine channel layout of audio file, could not conform";
|
|
return false;
|
|
}
|
|
// Create resampling context
|
|
AVChannelLayout layout = params.channel_layout();
|
|
SwrContext *resampler=NULL;
|
|
swr_alloc_set_opts2(
|
|
&resampler, &layout,
|
|
FFmpegUtils::GetFFmpegSampleFormat(params.format()),
|
|
params.sample_rate(), &channel_layout,
|
|
static_cast<AVSampleFormat>(instance_.avstream()->codecpar->format),
|
|
instance_.avstream()->codecpar->sample_rate, 0, nullptr);
|
|
av_channel_layout_uninit(&layout);
|
|
swr_init(resampler);
|
|
|
|
AVPacket *pkt = av_packet_alloc();
|
|
AVFrame *frame = av_frame_alloc();
|
|
int ret;
|
|
|
|
bool success = false;
|
|
|
|
int64_t duration = instance_.avstream()->duration;
|
|
if (duration == 0 || duration == AV_NOPTS_VALUE) {
|
|
duration = instance_.fmt_ctx()->duration;
|
|
if (!(duration == 0 || duration == AV_NOPTS_VALUE)) {
|
|
// Rescale from AVFormatContext timebase to AVStream timebase
|
|
duration = av_rescale_q_rnd(duration, { 1, AV_TIME_BASE },
|
|
instance_.avstream()->time_base,
|
|
AV_ROUND_UP);
|
|
}
|
|
}
|
|
|
|
PlanarFileDevice wave_out;
|
|
if (wave_out.open(filenames, QFile::WriteOnly)) {
|
|
int nb_channels = params.channel_count();
|
|
SampleBuffer data;
|
|
data.set_audio_params(params);
|
|
|
|
while (true) {
|
|
// Check if we have a `cancelled` ptr and its value
|
|
if (cancelled && cancelled->IsCancelled()) {
|
|
break;
|
|
}
|
|
|
|
ret = instance_.GetFrame(pkt, frame);
|
|
|
|
if (ret < 0) {
|
|
if (ret == AVERROR_EOF) {
|
|
success = true;
|
|
} else {
|
|
char err_str[512];
|
|
av_strerror(ret, err_str, 512);
|
|
qWarning() << "Failed to conform:" << ret << err_str;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Allocate buffers
|
|
int nb_samples = swr_get_out_samples(resampler, frame->nb_samples);
|
|
int nb_bytes_per_channel =
|
|
params.samples_to_bytes(nb_samples) / nb_channels;
|
|
data.set_sample_count(nb_bytes_per_channel);
|
|
data.allocate();
|
|
|
|
// Resample audio to our destination parameters
|
|
nb_samples = swr_convert(
|
|
resampler,
|
|
reinterpret_cast<uint8_t **>(data.to_raw_ptrs().data()),
|
|
nb_samples, const_cast<const uint8_t **>(frame->data),
|
|
frame->nb_samples);
|
|
|
|
// If no error, write to files
|
|
if (nb_samples > 0) {
|
|
// Update byte count for the number of samples we actually received
|
|
nb_bytes_per_channel =
|
|
params.samples_to_bytes(nb_samples) / nb_channels;
|
|
|
|
// Write to files
|
|
wave_out.write(
|
|
const_cast<const char **>(
|
|
reinterpret_cast<char **>(data.to_raw_ptrs().data())),
|
|
nb_bytes_per_channel);
|
|
}
|
|
|
|
// Free buffer
|
|
data.destroy();
|
|
|
|
// Handle error now after freeing
|
|
if (nb_samples < 0) {
|
|
char err_str[512];
|
|
av_strerror(nb_samples, err_str, 512);
|
|
qWarning() << "libswresample failed with error:" << nb_samples
|
|
<< err_str;
|
|
break;
|
|
}
|
|
|
|
SignalProcessingProgress(frame->best_effort_timestamp, duration);
|
|
}
|
|
|
|
wave_out.close();
|
|
} else {
|
|
qWarning() << "Failed to open WAVE output for indexing";
|
|
}
|
|
|
|
swr_free(&resampler);
|
|
|
|
av_frame_free(&frame);
|
|
av_packet_free(&pkt);
|
|
|
|
return success;
|
|
}
|
|
|
|
PixelFormat FFmpegDecoder::GetNativePixelFormat(AVPixelFormat pix_fmt)
|
|
{
|
|
switch (pix_fmt) {
|
|
case AV_PIX_FMT_RGB24:
|
|
case AV_PIX_FMT_RGBA:
|
|
return PixelFormat::U8;
|
|
case AV_PIX_FMT_RGB48:
|
|
case AV_PIX_FMT_RGBA64:
|
|
return PixelFormat::U16;
|
|
default:
|
|
return PixelFormat::INVALID;
|
|
}
|
|
}
|
|
|
|
int FFmpegDecoder::GetNativeChannelCount(AVPixelFormat pix_fmt)
|
|
{
|
|
switch (pix_fmt) {
|
|
case AV_PIX_FMT_RGB24:
|
|
case AV_PIX_FMT_RGB48:
|
|
return VideoParams::kRGBChannelCount;
|
|
case AV_PIX_FMT_RGBA:
|
|
case AV_PIX_FMT_RGBA64:
|
|
return VideoParams::kRGBAChannelCount;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
AVChannelLayout FFmpegDecoder::ValidateChannelLayout(AVStream *stream)
|
|
{
|
|
if (av_channel_layout_check(&stream->codecpar->ch_layout)) {
|
|
return stream->codecpar->ch_layout;
|
|
}
|
|
AVChannelLayout layout;
|
|
av_channel_layout_default(&layout, stream->codecpar->ch_layout.nb_channels);
|
|
return layout;
|
|
}
|
|
|
|
const char *
|
|
FFmpegDecoder::GetInterlacingModeInFFmpeg(VideoParams::Interlacing interlacing)
|
|
{
|
|
if (interlacing == VideoParams::kInterlacedTopFirst) {
|
|
return "tff";
|
|
} else {
|
|
return "bff";
|
|
}
|
|
}
|
|
|
|
bool FFmpegDecoder::IsPixelFormatGLSLCompatible(AVPixelFormat f)
|
|
{
|
|
// NOTE: We don't include RGB24 or RGB48 here because those are slow on the GPU and performance
|
|
// should be better if we convert to RGBA on the CPU beforehand
|
|
switch (f) {
|
|
case AV_PIX_FMT_YUV420P:
|
|
case AV_PIX_FMT_YUV422P:
|
|
case AV_PIX_FMT_YUV444P:
|
|
case AV_PIX_FMT_YUV420P10LE:
|
|
case AV_PIX_FMT_YUV422P10LE:
|
|
case AV_PIX_FMT_YUV444P10LE:
|
|
case AV_PIX_FMT_YUV420P12LE:
|
|
case AV_PIX_FMT_YUV422P12LE:
|
|
case AV_PIX_FMT_YUV444P12LE:
|
|
case AV_PIX_FMT_RGBA:
|
|
case AV_PIX_FMT_RGBA64LE:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void FFmpegDecoder::ClearFrameCache()
|
|
{
|
|
if (!cached_frames_.empty()) {
|
|
cached_frames_.clear();
|
|
cache_at_eof_ = false;
|
|
cache_at_zero_ = false;
|
|
}
|
|
}
|
|
|
|
AVFramePtr FFmpegDecoder::PreProcessFrame(AVFramePtr f,
|
|
const RetrieveVideoParams &p)
|
|
{
|
|
// In pre-processing, we try to achieve the following:
|
|
// - If a divider is being used, scale down the image
|
|
// - If a pixel format is not compatible with the GLSL shader, convert it to RGBA ourselves
|
|
|
|
if (p.divider == 1 &&
|
|
IsPixelFormatGLSLCompatible(static_cast<AVPixelFormat>(f->format))) {
|
|
// No CPU processing required, the user wants this in full resolution and the pixel format can
|
|
// be converted on the GPU
|
|
return f;
|
|
}
|
|
|
|
// Some scaling and/or format conversion needs to be done
|
|
AVFramePtr dest = CreateAVFramePtr();
|
|
|
|
dest->width = f->width;
|
|
dest->height = f->height;
|
|
dest->format = f->format;
|
|
dest->color_range = f->color_range;
|
|
dest->colorspace = f->colorspace;
|
|
dest->hw_frames_ctx = nullptr;
|
|
if (p.divider > 1) {
|
|
dest->width = VideoParams::GetScaledDimension(dest->width, p.divider);
|
|
dest->height = VideoParams::GetScaledDimension(dest->height, p.divider);
|
|
}
|
|
|
|
if (!IsPixelFormatGLSLCompatible(
|
|
static_cast<AVPixelFormat>(dest->format))) {
|
|
dest->format = FFmpegUtils::GetCompatiblePixelFormat(
|
|
static_cast<AVPixelFormat>(dest->format), p.maximum_format);
|
|
}
|
|
|
|
int r = av_frame_get_buffer(dest.get(), 0);
|
|
if (r < 0) {
|
|
FFmpegError(r);
|
|
return nullptr;
|
|
}
|
|
|
|
if (!sws_ctx_ || sws_src_width_ != f->width ||
|
|
sws_src_height_ != f->height || sws_src_format_ != f->format ||
|
|
sws_dst_width_ != dest->width || sws_dst_height_ != dest->height ||
|
|
sws_dst_format_ != dest->format || sws_colrange_ != dest->color_range ||
|
|
sws_colspace_ != dest->colorspace) {
|
|
// SwsContext must be recreated, destroy current if it exists
|
|
FreeScaler();
|
|
|
|
// Cache info
|
|
sws_src_width_ = f->width;
|
|
sws_src_height_ = f->height;
|
|
sws_src_format_ = static_cast<AVPixelFormat>(f->format);
|
|
sws_dst_width_ = dest->width;
|
|
sws_dst_height_ = dest->height;
|
|
sws_dst_format_ = static_cast<AVPixelFormat>(dest->format);
|
|
sws_colrange_ = dest->color_range;
|
|
sws_colspace_ = dest->colorspace;
|
|
|
|
// Create new scaler
|
|
sws_ctx_ = sws_getContext(sws_src_width_, sws_src_height_,
|
|
sws_src_format_, sws_dst_width_,
|
|
sws_dst_height_, sws_dst_format_, SWS_POINT,
|
|
nullptr, nullptr, nullptr);
|
|
|
|
// Set swscale's colorspace details
|
|
sws_setColorspaceDetails(
|
|
sws_ctx_,
|
|
sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(
|
|
dest->colorspace)),
|
|
dest->color_range == AVCOL_RANGE_JPEG ? 1 : 0,
|
|
sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(
|
|
dest->colorspace)),
|
|
dest->color_range == AVCOL_RANGE_JPEG ? 1 : 0, 0, 0x10000, 0x10000);
|
|
}
|
|
|
|
r = sws_scale_frame(sws_ctx_, dest.get(), f.get());
|
|
|
|
if (r < 0) {
|
|
FFmpegError(r);
|
|
return nullptr;
|
|
}
|
|
|
|
return dest;
|
|
}
|
|
|
|
AVFramePtr FFmpegDecoder::RetrieveFrame(const rational &time,
|
|
CancelAtom *cancelled)
|
|
{
|
|
int64_t target_ts =
|
|
Timecode::time_to_timestamp(time, instance_.avstream()->time_base);
|
|
|
|
if (instance_.fmt_ctx()->start_time != AV_NOPTS_VALUE) {
|
|
target_ts += av_rescale_q(instance_.fmt_ctx()->start_time,
|
|
{ 1, AV_TIME_BASE },
|
|
instance_.avstream()->time_base);
|
|
}
|
|
|
|
const int64_t min_seek = 0;
|
|
int64_t seek_ts = std::max(min_seek, target_ts - MaximumQueueSize());
|
|
bool still_seeking = false;
|
|
|
|
if (time != kAnyTimecode) {
|
|
// If the frame wasn't in the frame cache, see if this frame cache is too old to use
|
|
if (cached_frames_.empty() ||
|
|
(target_ts < cached_frames_.front()->pts ||
|
|
target_ts > cached_frames_.back()->pts + 2 * second_ts_)) {
|
|
ClearFrameCache();
|
|
|
|
instance_.Seek(seek_ts);
|
|
if (seek_ts == min_seek) {
|
|
cache_at_zero_ = true;
|
|
}
|
|
|
|
still_seeking = true;
|
|
} else {
|
|
// Search cache for frame
|
|
AVFramePtr cached_frame = GetFrameFromCache(target_ts);
|
|
if (cached_frame) {
|
|
return cached_frame;
|
|
}
|
|
}
|
|
}
|
|
|
|
int ret;
|
|
AVFramePtr return_frame = nullptr;
|
|
AVFramePtr filtered = nullptr;
|
|
bool retried_after_eof = false;
|
|
|
|
while (true) {
|
|
// Break out of loop if we've cancelled
|
|
if (cancelled && cancelled->IsCancelled()) {
|
|
break;
|
|
}
|
|
|
|
if (!filtered) {
|
|
filtered = CreateAVFramePtr();
|
|
}
|
|
|
|
// Pull from the decoder
|
|
ret = instance_.GetFrame(working_packet_, filtered.get());
|
|
|
|
if (cancelled && cancelled->IsCancelled()) {
|
|
break;
|
|
}
|
|
|
|
// Handle any errors that aren't EOF (EOF is handled later on)
|
|
if (ret < 0 && ret != AVERROR_EOF) {
|
|
qCritical() << "Failed to retrieve frame:" << ret;
|
|
break;
|
|
}
|
|
|
|
if (still_seeking) {
|
|
// Handle a failure to seek (occurs on some media)
|
|
// We'll only be here if the frame cache was emptied earlier
|
|
if (!cache_at_zero_ &&
|
|
(ret == AVERROR_EOF ||
|
|
filtered->best_effort_timestamp > target_ts)) {
|
|
seek_ts = qMax(min_seek, seek_ts - second_ts_);
|
|
instance_.Seek(seek_ts);
|
|
if (seek_ts == min_seek) {
|
|
cache_at_zero_ = true;
|
|
}
|
|
continue;
|
|
|
|
} else {
|
|
still_seeking = false;
|
|
}
|
|
}
|
|
|
|
if (ret == AVERROR_EOF) {
|
|
// Handle an "expected" EOF by using the last frame of our cache
|
|
cache_at_eof_ = true;
|
|
|
|
if (cached_frames_.empty()) {
|
|
if (!retried_after_eof) {
|
|
retried_after_eof = true;
|
|
ClearFrameCache();
|
|
instance_.Seek(min_seek);
|
|
cache_at_zero_ = true;
|
|
still_seeking = true;
|
|
continue;
|
|
}
|
|
|
|
qCritical()
|
|
<< "Unexpected codec EOF - unable to retrieve frame";
|
|
} else {
|
|
return_frame = cached_frames_.back();
|
|
}
|
|
|
|
break;
|
|
|
|
} else {
|
|
// Cut down to thread count - 1 before we acquire a new frame
|
|
if (cached_frames_.size() > size_t(MaximumQueueSize())) {
|
|
RemoveFirstFrame();
|
|
}
|
|
|
|
// Store frame before just in case
|
|
AVFramePtr previous;
|
|
if (cached_frames_.empty()) {
|
|
previous = nullptr;
|
|
} else {
|
|
previous = cached_frames_.back();
|
|
}
|
|
|
|
// Append this frame and signal to other threads that a new frame has arrived
|
|
cached_frames_.push_back(filtered);
|
|
|
|
// If this is a valid frame, see if this or the frame before it are the one we need
|
|
if (filtered->pts == target_ts || time == kAnyTimecode) {
|
|
return_frame = filtered;
|
|
break;
|
|
} else if (filtered->pts > target_ts) {
|
|
if (!previous && cache_at_zero_) {
|
|
return_frame = filtered;
|
|
break;
|
|
} else {
|
|
return_frame = previous;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
filtered = nullptr;
|
|
}
|
|
|
|
av_packet_unref(working_packet_);
|
|
|
|
return return_frame;
|
|
}
|
|
|
|
void FFmpegDecoder::FreeScaler()
|
|
{
|
|
if (sws_ctx_) {
|
|
sws_freeContext(sws_ctx_);
|
|
sws_ctx_ = nullptr;
|
|
}
|
|
}
|
|
|
|
AVFramePtr FFmpegDecoder::GetFrameFromCache(const int64_t &t) const
|
|
{
|
|
if (t < cached_frames_.front()->pts) {
|
|
if (cache_at_zero_) {
|
|
return cached_frames_.front();
|
|
}
|
|
|
|
} else if (t > cached_frames_.back()->pts) {
|
|
if (cache_at_eof_) {
|
|
return cached_frames_.back();
|
|
}
|
|
|
|
} else {
|
|
// We already have this frame in the cache, find it
|
|
for (auto it = cached_frames_.cbegin(); it != cached_frames_.cend();
|
|
it++) {
|
|
AVFramePtr this_frame = *it;
|
|
|
|
auto next = it;
|
|
next++;
|
|
|
|
if (this_frame->pts == t // Test for an exact match
|
|
||
|
|
(next != cached_frames_.cend() &&
|
|
(*next)->pts > t)) { // Or for this frame to be the "closest"
|
|
|
|
return this_frame;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void FFmpegDecoder::RemoveFirstFrame()
|
|
{
|
|
cached_frames_.pop_front();
|
|
cache_at_zero_ = false;
|
|
}
|
|
|
|
int FFmpegDecoder::MaximumQueueSize()
|
|
{
|
|
// Fairly arbitrary size. This used to need to be the number of current threads to ensure any
|
|
// thread that arrived would have its frame available, but if we only have one render thread,
|
|
// that's no longer a concern. Now, this value could technically be 1, but some memory cache
|
|
// may be useful for reversing. This value may be tweaked over time.
|
|
return 2;
|
|
}
|
|
|
|
FFmpegDecoder::Instance::Instance()
|
|
: fmt_ctx_(nullptr)
|
|
, codec_ctx_(nullptr)
|
|
, avstream_(nullptr)
|
|
, opts_(nullptr)
|
|
{
|
|
}
|
|
|
|
bool FFmpegDecoder::Instance::Open(const char *filename, int stream_index)
|
|
{
|
|
// Open file in a format context
|
|
AVDictionary *format_opts = nullptr;
|
|
ApplyFormatOpenOptions(&format_opts);
|
|
int error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, &format_opts);
|
|
av_dict_free(&format_opts);
|
|
TuneFormatContext(fmt_ctx_);
|
|
DiscardSubtitleStreams(fmt_ctx_);
|
|
|
|
// Handle format context error
|
|
if (error_code != 0) {
|
|
qCritical()
|
|
<< "Failed to open input:" << filename << FFmpegError(error_code);
|
|
return false;
|
|
}
|
|
|
|
// Get stream information from format
|
|
error_code = avformat_find_stream_info(fmt_ctx_, nullptr);
|
|
|
|
// Handle get stream information error
|
|
if (error_code < 0) {
|
|
qCritical() << "Failed to find stream info:" << FFmpegError(error_code);
|
|
return false;
|
|
}
|
|
|
|
// Get reference to correct AVStream
|
|
avstream_ = fmt_ctx_->streams[stream_index];
|
|
|
|
// Find decoder
|
|
const AVCodec *codec = avcodec_find_decoder(avstream_->codecpar->codec_id);
|
|
|
|
// Handle failure to find decoder
|
|
if (codec == nullptr) {
|
|
qCritical()
|
|
<< "Failed to find appropriate decoder for this codec:" << filename
|
|
<< stream_index << avstream_->codecpar->codec_id;
|
|
return false;
|
|
}
|
|
|
|
// Allocate context for the decoder
|
|
codec_ctx_ = avcodec_alloc_context3(codec);
|
|
if (codec_ctx_ == nullptr) {
|
|
qCritical() << "Failed to allocate codec context";
|
|
return false;
|
|
}
|
|
|
|
// Copy parameters from the AVStream to the AVCodecContext
|
|
error_code = avcodec_parameters_to_context(codec_ctx_, avstream_->codecpar);
|
|
|
|
// Handle failure to copy parameters
|
|
if (error_code < 0) {
|
|
qCritical()
|
|
<< "Failed to copy parameters from AVStream to AVCodecContext";
|
|
return false;
|
|
}
|
|
|
|
// Set multithreading setting
|
|
error_code = av_dict_set(&opts_, "threads", "auto", 0);
|
|
|
|
// Handle failure to set multithreaded decoding
|
|
if (error_code < 0) {
|
|
qCritical() << "Failed to set codec options, performance may suffer";
|
|
}
|
|
|
|
// Open codec
|
|
error_code = avcodec_open2(codec_ctx_, codec, &opts_);
|
|
if (error_code < 0) {
|
|
char buf[512];
|
|
av_strerror(error_code, buf, 512);
|
|
qCritical() << "Failed to open codec" << codec->id << error_code << buf;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FFmpegDecoder::Instance::Close()
|
|
{
|
|
if (opts_) {
|
|
av_dict_free(&opts_);
|
|
opts_ = nullptr;
|
|
}
|
|
|
|
if (codec_ctx_) {
|
|
avcodec_free_context(&codec_ctx_);
|
|
codec_ctx_ = nullptr;
|
|
}
|
|
|
|
if (fmt_ctx_) {
|
|
avformat_close_input(&fmt_ctx_);
|
|
fmt_ctx_ = nullptr;
|
|
}
|
|
}
|
|
|
|
int FFmpegDecoder::Instance::GetFrame(AVPacket *pkt, AVFrame *frame)
|
|
{
|
|
bool eof = false;
|
|
|
|
int ret;
|
|
|
|
// Clear any previous frames
|
|
av_frame_unref(frame);
|
|
|
|
while ((ret = avcodec_receive_frame(codec_ctx_, frame)) ==
|
|
AVERROR(EAGAIN) &&
|
|
!eof) {
|
|
// Find next packet in the correct stream index
|
|
ret = GetPacket(pkt);
|
|
|
|
if (ret == AVERROR_EOF) {
|
|
// Don't break so that receive gets called again, but don't try to read again
|
|
eof = true;
|
|
|
|
// Send a null packet to signal end of
|
|
avcodec_send_packet(codec_ctx_, nullptr);
|
|
} else if (ret < 0) {
|
|
// Handle other error by breaking loop and returning the code we received
|
|
break;
|
|
} else {
|
|
// Successful read, send the packet
|
|
ret = avcodec_send_packet(codec_ctx_, pkt);
|
|
|
|
// We don't need the packet anymore, so free it
|
|
av_packet_unref(pkt);
|
|
|
|
if (ret < 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
const char *FFmpegDecoder::Instance::GetSubtitleHeader() const
|
|
{
|
|
return reinterpret_cast<const char *>(codec_ctx_->subtitle_header);
|
|
}
|
|
|
|
int FFmpegDecoder::Instance::GetSubtitle(AVPacket *pkt, AVSubtitle *sub)
|
|
{
|
|
int ret = GetPacket(pkt);
|
|
|
|
if (ret >= 0) {
|
|
int got_sub;
|
|
ret = avcodec_decode_subtitle2(codec_ctx_, sub, &got_sub, pkt);
|
|
if (!got_sub) {
|
|
ret = -1;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
int FFmpegDecoder::Instance::GetPacket(AVPacket *pkt)
|
|
{
|
|
int ret;
|
|
|
|
do {
|
|
av_packet_unref(pkt);
|
|
|
|
ret = av_read_frame(fmt_ctx_, pkt);
|
|
} while (pkt->stream_index != avstream_->index && ret >= 0);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void FFmpegDecoder::Instance::Seek(int64_t timestamp)
|
|
{
|
|
avcodec_flush_buffers(codec_ctx_);
|
|
av_seek_frame(fmt_ctx_, avstream_->index, timestamp, AVSEEK_FLAG_BACKWARD);
|
|
}
|
|
|
|
}
|