/*** 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 . ***/ #include "ffmpegdecoder.h" extern "C" { #include #include #include #include } #include #include #include #include #include #include #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(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); // For YUV formats, force the output texture to F32 RGBA for maximum precision 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: vp.set_format(PixelFormat::F32); vp.set_channel_count(VideoParams::kRGBAChannelCount); break; default: break; } // 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; case AV_PIX_FMT_RGBAF32: // RGBA F32 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(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; } // Diagnostic: check if decoded frame is all black bool all_black = true; if (f->data[0]) { int check_rows = std::min(f->height, 8); int check_bytes = check_rows * f->linesize[0]; for (int i = 0; i < check_bytes; ++i) { if (f->data[0][i] != 0) { all_black = false; break; } } } // 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(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( 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 &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(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(data.to_raw_ptrs().data()), nb_samples, const_cast(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( reinterpret_cast(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; case AV_PIX_FMT_RGBF32: case AV_PIX_FMT_RGBAF32: return PixelFormat::F32; default: return PixelFormat::INVALID; } } int FFmpegDecoder::GetNativeChannelCount(AVPixelFormat pix_fmt) { switch (pix_fmt) { case AV_PIX_FMT_RGB24: case AV_PIX_FMT_RGB48: case AV_PIX_FMT_RGBF32: return VideoParams::kRGBChannelCount; case AV_PIX_FMT_RGBA: case AV_PIX_FMT_RGBA64: case AV_PIX_FMT_RGBAF32: 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: case AV_PIX_FMT_RGBAF32: 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(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(dest->format))) { dest->format = FFmpegUtils::GetCompatiblePixelFormat( static_cast(dest->format), p.maximum_format); } // swscale does not support RGBAF32 as output, fallback to RGBA64 if (dest->format == AV_PIX_FMT_RGBAF32) { dest->format = AV_PIX_FMT_RGBA64; } 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(f->format); sws_dst_width_ = dest->width; sws_dst_height_ = dest->height; sws_dst_format_ = static_cast(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(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); } }