diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index e7ac869e9..824f23579 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -21,6 +21,7 @@ extern "C" { #include #include +#include #include } @@ -43,6 +44,44 @@ 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) @@ -339,7 +378,12 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, // Open file in a format context AVFormatContext *fmt_ctx = nullptr; - error_code = avformat_open_input(&fmt_ctx, filename_c, nullptr, 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) { @@ -901,6 +945,7 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational &time, int ret; AVFramePtr return_frame = nullptr; AVFramePtr filtered = nullptr; + bool retried_after_eof = false; while (true) { // Break out of loop if we've cancelled @@ -948,6 +993,15 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational &time, 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 { @@ -1064,7 +1118,12 @@ FFmpegDecoder::Instance::Instance() bool FFmpegDecoder::Instance::Open(const char *filename, int stream_index) { // Open file in a format context - int error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr); + 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) { diff --git a/app/core.cpp b/app/core.cpp index d4aba7559..b273f8d49 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -74,6 +74,72 @@ #include "widget/menu/menushared.h" #include "window/mainwindow/mainwindow.h" +namespace { + +QStringList FootageVideoExtensions() +{ + return QStringList{ + QStringLiteral("mp4"), QStringLiteral("mov"), QStringLiteral("m4v"), + QStringLiteral("avi"), QStringLiteral("mpg"), QStringLiteral("mpeg"), + QStringLiteral("m2ts"), QStringLiteral("mts"), QStringLiteral("ts"), + QStringLiteral("webm"), QStringLiteral("wmv"), QStringLiteral("flv"), + QStringLiteral("3gp"), QStringLiteral("3g2"), QStringLiteral("mxf") + }; +} + +QStringList FootageAudioExtensions() +{ + return QStringList{ + QStringLiteral("wav"), QStringLiteral("mp3"), QStringLiteral("flac"), + QStringLiteral("aac"), QStringLiteral("ogg"), QStringLiteral("opus"), + QStringLiteral("m4a"), QStringLiteral("alac"), QStringLiteral("aif"), + QStringLiteral("aiff"), QStringLiteral("aifc"), QStringLiteral("wma") + }; +} + +QStringList FootageImageExtensions() +{ + return QStringList{ + QStringLiteral("png"), QStringLiteral("jpg"), QStringLiteral("jpeg"), + QStringLiteral("tif"), QStringLiteral("tiff"), QStringLiteral("bmp"), + QStringLiteral("gif"), QStringLiteral("exr"), QStringLiteral("dpx"), + QStringLiteral("webp") + }; +} + +QString BuildFootageFilterGroup(const QString &label, + const QStringList &extensions) +{ + QStringList patterns; + patterns.reserve(extensions.size()); + for (const QString &ext : extensions) { + patterns.append(QStringLiteral("*.%1").arg(ext)); + } + + return QStringLiteral("%1 (%2)") + .arg(label, patterns.join(QLatin1Char(' '))); +} + +QString BuildFootageFileDialogFilter() +{ + QStringList all = FootageVideoExtensions() + FootageAudioExtensions() + + FootageImageExtensions(); + all.removeDuplicates(); + + QStringList groups; + groups << BuildFootageFilterGroup(QObject::tr("Common Media Files"), all); + groups << BuildFootageFilterGroup(QObject::tr("Video Files"), + FootageVideoExtensions()); + groups << BuildFootageFilterGroup(QObject::tr("Audio Files"), + FootageAudioExtensions()); + groups << BuildFootageFilterGroup(QObject::tr("Image Files"), + FootageImageExtensions()); + + return groups.join(QStringLiteral(";;")); +} + +} // namespace + namespace olive { @@ -102,6 +168,29 @@ Core *Core::instance() return instance_; } +QString Core::FootageFileDialogFilter() +{ + return BuildFootageFileDialogFilter(); +} + +QStringList Core::AllowedFootageExtensions() +{ + QStringList all = FootageVideoExtensions() + FootageAudioExtensions() + + FootageImageExtensions(); + all.removeDuplicates(); + return all; +} + +bool Core::IsFootageExtensionAllowed(const QString &path) +{ + const QString ext = QFileInfo(path).suffix().toLower(); + if (ext.isEmpty()) { + return false; + } + + return AllowedFootageExtensions().contains(ext); +} + void Core::DeclareTypesForQt() { qRegisterMetaType(); @@ -240,7 +329,31 @@ void Core::ImportFiles(const QStringList &urls, Folder *parent) return; } - ProjectImportTask *pim = new ProjectImportTask(parent, urls); + QStringList filtered_urls; + QStringList rejected_urls; + filtered_urls.reserve(urls.size()); + + for (const QString &url : urls) { + if (IsFootageExtensionAllowed(url)) { + filtered_urls.append(url); + } else { + rejected_urls.append(url); + } + } + + if (!rejected_urls.isEmpty()) { + QMessageBox::warning( + main_window_, tr("Unsupported media"), + tr("Skipped %1 file(s) that are not allowed by the current media " + "type filter.") + .arg(rejected_urls.size())); + } + + if (filtered_urls.isEmpty()) { + return; + } + + ProjectImportTask *pim = new ProjectImportTask(parent, filtered_urls); if (!pim->GetFileCount()) { // No files to import @@ -333,8 +446,9 @@ void Core::DialogAboutShow() void Core::DialogImportShow() { // Open dialog for user to select files - QStringList files = - QFileDialog::getOpenFileNames(main_window_, tr("Import footage...")); + QStringList files = QFileDialog::getOpenFileNames( + main_window_, tr("Import footage..."), QString(), + FootageFileDialogFilter()); // Check if the user actually selected files to import if (!files.isEmpty()) { diff --git a/app/core.h b/app/core.h index 4efadbe93..2869c6a61 100644 --- a/app/core.h +++ b/app/core.h @@ -132,6 +132,10 @@ public: */ static Core *instance(); + static QString FootageFileDialogFilter(); + static QStringList AllowedFootageExtensions(); + static bool IsFootageExtensionAllowed(const QString &path); + const CoreParams &core_params() const { return core_params_; diff --git a/app/dialog/footagerelink/footagerelinkdialog.cpp b/app/dialog/footagerelink/footagerelinkdialog.cpp index e93caf051..1ee2b5327 100644 --- a/app/dialog/footagerelink/footagerelinkdialog.cpp +++ b/app/dialog/footagerelink/footagerelinkdialog.cpp @@ -18,11 +18,14 @@ #include "footagerelinkdialog.h" +#include "core.h" + #include #include #include #include #include +#include #include #include #include @@ -104,7 +107,8 @@ void FootageRelinkDialog::BrowseForFootage() QFileInfo info(f->filename()); QString new_fn = QFileDialog::getOpenFileName( - this, tr("Relink \"%1\"").arg(f->GetLabel()), info.absolutePath()); + this, tr("Relink \"%1\"").arg(f->GetLabel()), info.absolutePath(), + Core::FootageFileDialogFilter()); // Originally, this function would attempt to filter to the exact filename of the missing file. // However, this would break on Windows if the filename had any spaces in it. The reason is @@ -117,6 +121,13 @@ void FootageRelinkDialog::BrowseForFootage() // We received a new filename if (!new_fn.isEmpty()) { + if (!Core::IsFootageExtensionAllowed(new_fn)) { + QMessageBox::warning( + this, tr("Unsupported media"), + tr("This file type is not allowed by the current media type " + "filter.")); + return; + } // Store original dir since we might be able to use this to find other files QDir original_dir = info.dir(); QDir new_dir = QFileInfo(new_fn).dir(); diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 39b59caa7..7dd7cadc4 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -506,8 +506,18 @@ void ProjectExplorer::ReplaceSelectedFootage() { Footage *footage = static_cast(context_menu_items_.first()); - QString file = QFileDialog::getOpenFileName(this, tr("Replace Footage")); + QString file = QFileDialog::getOpenFileName( + this, tr("Replace Footage"), QString(), + Core::FootageFileDialogFilter()); if (!file.isEmpty()) { + if (!Core::IsFootageExtensionAllowed(file)) { + QMessageBox::warning( + this, tr("Unsupported media"), + tr("This file type is not allowed by the current media type " + "filter.")); + return; + } + auto p = new MultiUndoCommand(); // Change filename parameter