[KPhotoAlbum] restructuring video thumbnail generation with ffmpeg

Andreas Schleth schleth_es at web.de
Fri Sep 8 00:25:26 CEST 2017


Hi!

with friendly help from Tobias, I prepared a patch for video thumbnail 
generation with ffmpeg / ffprobe. In my installation it builds and works 
for me (TM).

Purpose:

     Some strange videos do not have duration info in the stream data. 
These videos produced the length "N/A". Thus, here the duration info is 
taken

     from somewhere else and parsed accordingly. Here is one of the 
offending videos (a short one): https://my.hidrive.com/lnk/3BrpUFZJ

     Then, "short" videos were marked when *any* of the 10 extraction 
processes would produce less than 20 images - this seems strange to me.

     Thus, I moved the markShort-function from ExtractOneVideoFrame.cpp 
to VideoLengthExtractor.cpp and call it for videos of less than 0.5 s 
length.

     At 24 fps this would mean: less 12 frames or less is "short".

I hope, this patch does not break the mplayer functionality. I do not 
have mplayer. Please, could someone test this against mplayer?

I will be a.f.k. for a few days - so discussions next week...

Cheers, Andreas

PS:  I found two conventions for writing the {}-clauses:

a) (my preference)

     if (...) {

         do something

     } else {

         whatever

     }

b)

     if (...)

     {

         do something

     }

Which one is the preferred one in KPA?



-------------- next part --------------
diff --git a/ImageManager/ExtractOneVideoFrame.cpp b/ImageManager/ExtractOneVideoFrame.cpp
index da29a1e8..684d3d79 100644
--- a/ImageManager/ExtractOneVideoFrame.cpp
+++ b/ImageManager/ExtractOneVideoFrame.cpp
@@ -28,15 +28,12 @@
 
 #include <DB/CategoryCollection.h>
 #include <DB/ImageDB.h>
-#include <MainWindow/DirtyIndicator.h>
 #include <MainWindow/FeatureDialog.h>
-#include <MainWindow/TokenEditor.h>
 #include <MainWindow/Window.h>
 #include <Utilities/Process.h>
 #include <Utilities/Set.h>
 
 namespace ImageManager {
-QString ExtractOneVideoFrame::s_tokenForShortVideos;
 
 #define STR(x) QString::fromUtf8(x)
 void ExtractOneVideoFrame::extract(const DB::FileName &fileName, double offset, QObject* receiver, const char* slot)
@@ -67,9 +64,18 @@ ExtractOneVideoFrame::ExtractOneVideoFrame(const DB::FileName &fileName, double
     } else {
         QStringList arguments;
         arguments << STR("-ss") << QString::number(offset, 'f', 4) << STR("-i") << fileName.absolute()
-                  << STR("-sn") << STR("-an")
-                  << STR("-vframes") << STR("20")
+                  << STR("-vf") << STR("thumbnail") << STR("-vframes") << STR("20")
                   << m_workingDirectory + STR("/000000%02d.png");
+        // old:
+        //          << STR("-sn") << STR("-an")
+        //          << STR("-vframes") << STR("20")
+        //          << m_workingDirectory + STR("/000000%02d.png");
+        //
+        // The -sn and -an parameters are unnecessary if output is an image
+        // This additional parameter directly finds frames suitable for thumbnails (usually 20 times the same frame): -vf thumbnail
+        // Thus it might be unnecessary to produce 20 pngs and then sift through them.
+        // "Shortness" detection should be done with parsing the duration parameter in VideoLenghtExtractor.cpp
+        //
         //qDebug( "%s %s", qPrintable(MainWindow::FeatureDialog::ffmpegBinary()), qPrintable(arguments.join(QString::fromLatin1(" "))));
 
         m_process->start(MainWindow::FeatureDialog::ffmpegBinary(), arguments);
@@ -78,21 +84,27 @@ ExtractOneVideoFrame::ExtractOneVideoFrame(const DB::FileName &fileName, double
 
 void ExtractOneVideoFrame::frameFetched()
 {
-    if (!QFile::exists(m_workingDirectory + STR("/00000020.png")))
-        markShortVideo(m_fileName);
-
+    // if (!QFile::exists(m_workingDirectory + STR("/00000020.png")))
+    //    markShortVideo(m_fileName);
+    // moved to VideoLengthExtractor.cpp
+    // these 20 files are just there for compatibility with the mplayer-variant
     QString name;
     for (int i = 20; i>0; --i) {
-        name = m_workingDirectory +STR("/000000%1.png").arg(i,2, 10, QChar::fromLatin1('0'));
-        if (QFile::exists(name))
-        {
-            //qDebug() << "Using video frame " << i;
-            break;
-        }
+         name = m_workingDirectory +STR("/000000%1.png").arg(i,2, 10, QChar::fromLatin1('0'));
+         if (QFile::exists(name)) {
+             //qDebug() << "Using video frame " << i;
+             break;
+         }
+    }
+    if (!QFile::exists(name)) {
+        // the old code was missing any error action
+        // is there something, we could emit instead?
+        // This is incomplete!
+        qDebug( "%s", qPrintable(QString::fromUtf8("ExtractOneVideoFrame::fframeFetched(): **no frame found**")));
+    } else {
+        QImage image(name);
+        emit result(image);
     }
-
-    QImage image(name);
-    emit result(image);
     deleteWorkingDirectory();
     deleteLater();
 }
@@ -132,37 +144,5 @@ void ExtractOneVideoFrame::deleteWorkingDirectory()
     dir.rmdir(m_workingDirectory);
 }
 
-void ExtractOneVideoFrame::markShortVideo(const DB::FileName &fileName)
-{
-    if (s_tokenForShortVideos.isNull()) {
-        Utilities::StringSet usedTokens = MainWindow::TokenEditor::tokensInUse().toSet();
-        for ( int ch = 'A'; ch <= 'Z'; ++ch ) {
-            QString token = QChar::fromLatin1( (char) ch );
-            if (!usedTokens.contains(token)) {
-                s_tokenForShortVideos = token;
-                break;
-            }
-        }
-
-        if (s_tokenForShortVideos.isNull()) {
-            // Hmmm, no free token. OK lets just skip setting tokens.
-            return;
-        }
-        KMessageBox::information(MainWindow::Window::theMainWindow(),
-                                 i18n("Unable to extract video thumbnails from some files. "
-                                      "Either the file is damaged in some way, or the video is ultra short. "
-                                      "For your convenience, the token '%1' "
-                                      "has been set on those videos.\n\n"
-                                      "(You might need to wait till the video extraction led in your status bar has stopped blinking, "
-                                      "to see all affected videos.)",
-                                      s_tokenForShortVideos));
-    }
-
-    DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName);
-    DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory);
-    info->addCategoryInfo(tokensCategory->name(), s_tokenForShortVideos);
-    MainWindow::DirtyIndicator::markDirty();
-}
-
 } // namespace ImageManager
 // vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/ImageManager/ExtractOneVideoFrame.h b/ImageManager/ExtractOneVideoFrame.h
index 123f0b96..4e6ad441 100644
--- a/ImageManager/ExtractOneVideoFrame.h
+++ b/ImageManager/ExtractOneVideoFrame.h
@@ -1,5 +1,5 @@
 /* Copyright 2012 Jesper K. Pedersen <blackie at kde.org>
-  
+
    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 2 of
@@ -7,12 +7,12 @@
    accepted by the membership of KDE e.V. (or its successor approved
    by the membership of KDE e.V.), which shall act as a proxy
    defined in Section 14 of version 3 of the license.
-   
+
    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/>.
 */
@@ -52,12 +52,10 @@ private:
     ExtractOneVideoFrame(const DB::FileName& filename, double offset, QObject* receiver, const char* slot);
     void setupWorkingDirectory();
     void deleteWorkingDirectory();
-    void markShortVideo(const DB::FileName& fileName);
 
     QString m_workingDirectory;
     Utilities::Process* m_process;
     DB::FileName m_fileName;
-    static QString s_tokenForShortVideos;
 };
 
 } // namespace ImageManager
diff --git a/ImageManager/VideoLengthExtractor.cpp b/ImageManager/VideoLengthExtractor.cpp
index 06d6c088..5dee33d8 100644
--- a/ImageManager/VideoLengthExtractor.cpp
+++ b/ImageManager/VideoLengthExtractor.cpp
@@ -19,9 +19,17 @@
 
 #include "VideoLengthExtractor.h"
 #include <Utilities/Process.h>
+#include <Utilities/Set.h>
 #include <QDir>
+#include <MainWindow/DirtyIndicator.h>
 #include <MainWindow/FeatureDialog.h>
+#include <MainWindow/TokenEditor.h>
+#include <MainWindow/Window.h>
 #include <QDebug>
+#include <KLocalizedString>
+#include <KMessageBox>
+#include <DB/CategoryCollection.h>
+#include <DB/ImageDB.h>
 
 #define STR(x) QString::fromUtf8(x)
 
@@ -35,6 +43,7 @@ ImageManager::VideoLengthExtractor::VideoLengthExtractor(QObject *parent) :
     QObject(parent), m_process(nullptr)
 {
 }
+QString ImageManager::VideoLengthExtractor::s_tokenForShortVideos;
 
 void ImageManager::VideoLengthExtractor::extract(const DB::FileName &fileName)
 {
@@ -55,8 +64,7 @@ void ImageManager::VideoLengthExtractor::extract(const DB::FileName &fileName)
     m_process->setWorkingDirectory(QDir::tempPath());
     connect( m_process, SIGNAL(finished(int)), this, SLOT(processEnded()));
 
-    if (MainWindow::FeatureDialog::ffmpegBinary().isEmpty())
-    {
+    if (MainWindow::FeatureDialog::ffmpegBinary().isEmpty()) {
         QStringList arguments;
         arguments << STR("-identify") << STR("-frames") << STR("0") << STR("-vc") << STR("null")
                   << STR("-vo") << STR("null") << STR("-ao") << STR("null") <<  fileName.absolute();
@@ -64,11 +72,12 @@ void ImageManager::VideoLengthExtractor::extract(const DB::FileName &fileName)
         m_process->start(MainWindow::FeatureDialog::mplayerBinary(), arguments);
     } else {
         QStringList arguments;
-        arguments << STR("-v") << STR("error") << STR("-select_streams") << STR("v:0")
-                  << STR("-show_entries") << STR("stream=duration")
-                  << STR("-of") << STR("default=noprint_wrappers=1:nokey=1")
-                  <<  fileName.absolute();
-
+        arguments << STR("-hide_banner") << fileName.absolute();
+        // original command was:
+        // ffprobe -v error -select_streams v:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 [filename]
+        // This command fails for some videos, if the stream has no duration data.
+        // The old cmd wrote to stdout, the new writes the interesting things to stderr
+        //
         //qDebug( "%s %s", qPrintable(MainWindow::FeatureDialog::ffprobeBinary()), qPrintable(arguments.join(QString::fromLatin1(" "))));
         m_process->start(MainWindow::FeatureDialog::ffprobeBinary(), arguments);
     }
@@ -80,9 +89,12 @@ void ImageManager::VideoLengthExtractor::processEnded()
         Debug() << m_process->stderr();
 
     QString lenStr;
+    QStringList list;
+    bool ok = false;
+    double length;
     if (MainWindow::FeatureDialog::ffmpegBinary().isEmpty())
     {
-        QStringList list = m_process->stdout().split(QChar::fromLatin1('\n'));
+        list = m_process->stdout().split(QChar::fromLatin1('\n'));
         list = list.filter(STR("ID_LENGTH="));
         if ( list.count() == 0 ) {
             qWarning() << "Unable to find ID_LENGTH in output from MPlayer for file " << m_fileName.absolute() << "\n"
@@ -102,23 +114,46 @@ void ImageManager::VideoLengthExtractor::processEnded()
         }
 
         lenStr = regexp.cap(1);
+        length = lenStr.toDouble(&ok);
     } else {
-        QStringList list = m_process->stdout().split(QChar::fromLatin1('\n'));
-        // one line-break -> 2 parts
-        // some videos with subtitles or other additional streams might have more than one line
-        // in these cases, we just take the first one as both lengths should be the same anyways
-        if ( list.count() < 2 ) {
-            qWarning() << "Unable to parse video length from ffprobe output!"
+        list = m_process->stderr().split(QChar::fromLatin1('\n'));
+        list = list.filter(STR("Duration:"));
+        if ( list.count() == 0 ) {
+            qWarning() << "Unable to find Duration in stderr from ffprobe for file " << m_fileName.absolute() << "\n"
                        << "Output was:\n"
-                       << m_process->stdout();
+                       << m_process->stderr();
+            emit unableToDetermineLength();
+            return;
+        }
+        // The string we have is: "  Duration: 01:28:32.34, somewhat more data"
+        // We split 3 times: first at the blanks, then at the comma and then at the colons
+        // To be independet of the number of blanks, we filter for the first comma
+        list = list[0].split(QChar::fromLatin1(' '));
+        list = list.filter(STR(","));
+        if ( list.count() > 0 ) {
+            lenStr = list[0];
+            list = list[0].split(QChar::fromLatin1(','));
+            list = list[0].split(QChar::fromLatin1(':'));
+        } else {
+            qWarning() << "Unable find sexagesimal time value from ffprobe for file " << m_fileName.absolute() << "\n";
+            emit unableToDetermineLength();
+            return;
+        }
+        const double hh = list[0].toDouble(&ok);
+        const double mm = list[1].toDouble(&ok);
+        const double ss = list[2].toDouble(&ok);
+        if(ok) {
+            length = 3600.*hh + 60.*mm + ss;
+            if(length < 0.5) {
+                markShortVideo(m_fileName);
+            }
+        } else {
+            qWarning() << "Unable assemble double from sexagesimal time value for file " << m_fileName.absolute() << "\n";
             emit unableToDetermineLength();
             return;
         }
-        lenStr = list[0].trimmed();
     }
 
-    bool ok = false;
-    const double length = lenStr.toDouble(&ok);
     if ( !ok ) {
         qWarning() << STR("Unable to convert string \"%1\"to double (for file %2)").arg(lenStr).arg(m_fileName.absolute());
         emit unableToDetermineLength();
@@ -135,4 +170,39 @@ void ImageManager::VideoLengthExtractor::processEnded()
     m_process->deleteLater();
     m_process = nullptr;
 }
+
+// moved here from ExtractOneVideoFrame.cpp and renamed
+// don't know if all dependencies are here (moved it in .h as well)
+void ImageManager::VideoLengthExtractor::markShortVideo(const DB::FileName &fileName)
+{
+    if (s_tokenForShortVideos.isNull()) {
+        Utilities::StringSet usedTokens = MainWindow::TokenEditor::tokensInUse().toSet();
+        for ( int ch = 'A'; ch <= 'Z'; ++ch ) {
+            QString token = QChar::fromLatin1( (char) ch );
+            if (!usedTokens.contains(token)) {
+                s_tokenForShortVideos = token;
+                break;
+            }
+        }
+
+        if (s_tokenForShortVideos.isNull()) {
+            // Hmmm, no free token. OK lets just skip setting tokens.
+            return;
+        }
+        KMessageBox::information(MainWindow::Window::theMainWindow(),
+                                 i18n("Unable to extract video thumbnails from some files. "
+                                      "Either the file is damaged in some way, or the video is ultra short. "
+                                      "For your convenience, the token '%1' "
+                                      "has been set on those videos.\n\n"
+                                      "(You might need to wait till the video extraction led in your status bar has stopped blinking, "
+                                      "to see all affected videos.)",
+                                      s_tokenForShortVideos));
+    }
+
+    DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName);
+    DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory);
+    info->addCategoryInfo(tokensCategory->name(), s_tokenForShortVideos);
+    MainWindow::DirtyIndicator::markDirty();
+}
+
 // vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/ImageManager/VideoLengthExtractor.h b/ImageManager/VideoLengthExtractor.h
index ff925dcb..facdbf11 100644
--- a/ImageManager/VideoLengthExtractor.h
+++ b/ImageManager/VideoLengthExtractor.h
@@ -37,7 +37,7 @@ class VideoLengthExtractor : public QObject
 public:
     explicit VideoLengthExtractor(QObject *parent = nullptr);
     void extract(const DB::FileName& fileName );
-    
+
 signals:
     void lengthFound(int length);
     void unableToDetermineLength();
@@ -48,6 +48,8 @@ private slots:
 private:
     Utilities::Process* m_process;
     DB::FileName m_fileName;
+    void markShortVideo(const DB::FileName& fileName);
+    static QString s_tokenForShortVideos;
 };
 
 }


More information about the KPhotoAlbum mailing list