Coverage Report

Created: 2026-01-25 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ffmpegthumbs/ffmpegthumbnailer/videothumbnailer.cpp
Line
Count
Source
1
/*
2
    SPDX-FileCopyrightText: 2010 Dirk Vanden Boer <dirk.vdb@gmail.com>
3
4
    SPDX-License-Identifier: GPL-2.0-or-later
5
*/
6
7
#include "videothumbnailer.h"
8
9
#include "moviedecoder.h"
10
#include "filmstripfilter.h"
11
#include "imagewriter.h"
12
13
#include <QtGlobal>
14
#include <QTime>
15
16
#include <iostream>
17
#include <cfloat>
18
#include <cmath>
19
#include <sys/stat.h>
20
21
22
using namespace std;
23
24
namespace ffmpegthumbnailer
25
{
26
27
static const int SMART_FRAME_ATTEMPTS = 25;
28
29
VideoThumbnailer::VideoThumbnailer()
30
682
        : m_ThumbnailSize(128)
31
682
        , m_SeekPercentage(10)
32
682
        , m_OverlayFilmStrip(false)
33
682
        , m_WorkAroundIssues(false)
34
682
        , m_MaintainAspectRatio(true)
35
682
        , m_SmartFrameSelection(false)
36
682
{
37
682
}
38
39
VideoThumbnailer::VideoThumbnailer(int thumbnailSize, bool workaroundIssues, bool maintainAspectRatio, bool smartFrameSelection)
40
0
        : m_ThumbnailSize(thumbnailSize)
41
0
        , m_SeekPercentage(10)
42
0
        , m_WorkAroundIssues(workaroundIssues)
43
0
        , m_MaintainAspectRatio(maintainAspectRatio)
44
0
        , m_SmartFrameSelection(smartFrameSelection)
45
0
{
46
0
}
47
48
VideoThumbnailer::~VideoThumbnailer()
49
682
{
50
682
}
51
52
void VideoThumbnailer::setSeekPercentage(int percentage)
53
682
{
54
682
    m_SeekTime.clear();
55
682
    m_SeekPercentage = percentage > 95 ? 95 : percentage;
56
682
}
57
58
void VideoThumbnailer::setSeekTime(const QString& seekTime)
59
0
{
60
0
    m_SeekTime = seekTime;
61
0
}
62
63
void VideoThumbnailer::setThumbnailSize(int size)
64
682
{
65
682
    m_ThumbnailSize = size;
66
682
}
67
68
void VideoThumbnailer::setWorkAroundIssues(bool workAround)
69
0
{
70
0
    m_WorkAroundIssues = workAround;
71
0
}
72
73
void VideoThumbnailer::setMaintainAspectRatio(bool enabled)
74
0
{
75
0
    m_MaintainAspectRatio = enabled;
76
0
}
77
78
void VideoThumbnailer::setSmartFrameSelection(bool enabled)
79
0
{
80
0
    m_SmartFrameSelection = enabled;
81
0
}
82
83
int timeToSeconds(const QString& time)
84
0
{
85
0
    return QTime::fromString(time, QLatin1String("hh:mm:ss")).secsTo(QTime(0, 0, 0));
86
0
}
87
88
void VideoThumbnailer::generateThumbnail(const QString& videoFile, ImageWriter& imageWriter, QImage &image)
89
682
{
90
682
    MovieDecoder movieDecoder(videoFile, nullptr);
91
682
    if (movieDecoder.getInitialized()) {
92
0
        if (!movieDecoder.decodeVideoFrame()) { //before seeking, a frame has to be decoded
93
0
            return;
94
0
        }
95
        
96
0
        if ((!m_WorkAroundIssues) || (movieDecoder.getCodec() != QLatin1String("h264"))) { //workaround for bug in older ffmpeg (100% cpu usage when seeking in h264 files)
97
0
            int secondToSeekTo = m_SeekTime.isEmpty() ? movieDecoder.getDuration() * m_SeekPercentage / 100 : timeToSeconds(m_SeekTime);
98
0
            movieDecoder.seek(secondToSeekTo);
99
0
        }
100
    
101
0
        VideoFrame videoFrame;
102
        
103
0
        if (m_SmartFrameSelection) {
104
0
            generateSmartThumbnail(movieDecoder, videoFrame);
105
0
        } else {
106
0
            movieDecoder.getScaledVideoFrame(m_ThumbnailSize, m_MaintainAspectRatio, videoFrame);
107
0
        }
108
        
109
0
        applyFilters(videoFrame);
110
0
        imageWriter.writeFrame(videoFrame, image, movieDecoder.transformations());
111
0
    }
112
682
}
113
114
void VideoThumbnailer::generateSmartThumbnail(MovieDecoder& movieDecoder, VideoFrame& videoFrame)
115
0
{
116
0
    vector<VideoFrame> videoFrames(SMART_FRAME_ATTEMPTS);
117
0
    vector<Histogram<int> > histograms(SMART_FRAME_ATTEMPTS);
118
119
0
    for (int i = 0; i < SMART_FRAME_ATTEMPTS; ++i) {
120
0
        movieDecoder.decodeVideoFrame();
121
0
        movieDecoder.getScaledVideoFrame(m_ThumbnailSize, m_MaintainAspectRatio, videoFrames[i]);
122
0
        generateHistogram(videoFrames[i], histograms[i]);
123
0
    }
124
125
0
    int bestFrame = getBestThumbnailIndex(videoFrames, histograms);
126
127
0
    Q_ASSERT(bestFrame != -1);
128
0
    videoFrame = videoFrames[bestFrame];
129
0
}
130
131
void VideoThumbnailer::generateThumbnail(const QString& videoFile, QImage &image)
132
682
{
133
682
    ImageWriter* imageWriter = new  ImageWriter();
134
682
    generateThumbnail(videoFile, *imageWriter, image);
135
682
    delete imageWriter;
136
682
}
137
138
void VideoThumbnailer::addFilter(IFilter* filter)
139
682
{
140
682
    m_Filters.push_back(filter);
141
682
}
142
143
void VideoThumbnailer::removeFilter(IFilter* filter)
144
0
{
145
0
    for (vector<IFilter*>::iterator iter = m_Filters.begin();
146
0
            iter != m_Filters.end();
147
0
            ++iter) {
148
0
        if (*iter == filter) {
149
0
            m_Filters.erase(iter);
150
0
            break;
151
0
        }
152
0
    }
153
0
}
154
155
void VideoThumbnailer::clearFilters()
156
0
{
157
0
    m_Filters.clear();
158
0
}
159
160
void VideoThumbnailer::applyFilters(VideoFrame& videoFrame)
161
0
{
162
0
    for (vector<IFilter*>::iterator iter = m_Filters.begin();
163
0
            iter != m_Filters.end();
164
0
            ++iter) {
165
0
        (*iter)->process(videoFrame);
166
0
    }
167
0
}
168
169
void VideoThumbnailer::generateHistogram(const VideoFrame& videoFrame, Histogram<int>& histogram)
170
0
{
171
0
    for (quint32 i = 0; i < videoFrame.height; ++i) {
172
0
        int pixelIndex = i * videoFrame.lineSize;
173
0
        for (quint32 j = 0; j < videoFrame.width * 3; j += 3) {
174
0
            ++histogram.r[videoFrame.frameData[pixelIndex + j]];
175
0
            ++histogram.g[videoFrame.frameData[pixelIndex + j + 1]];
176
0
            ++histogram.b[videoFrame.frameData[pixelIndex + j + 2]];
177
0
        }
178
0
    }
179
0
}
180
181
int VideoThumbnailer::getBestThumbnailIndex(vector<VideoFrame>& videoFrames, const vector<Histogram<int> >& histograms)
182
0
{
183
0
    Q_UNUSED(videoFrames);
184
0
    Histogram<float> avgHistogram;
185
0
    for (size_t i = 0; i < histograms.size(); ++i) {
186
0
        for (int j = 0; j < 255; ++j) {
187
0
            avgHistogram.r[j] += static_cast<float>(histograms[i].r[j]) / histograms.size();
188
0
            avgHistogram.g[j] += static_cast<float>(histograms[i].g[j]) / histograms.size();
189
0
            avgHistogram.b[j] += static_cast<float>(histograms[i].b[j]) / histograms.size();
190
0
        }
191
0
    }
192
193
0
    int bestFrame = -1;
194
0
    float minRMSE = FLT_MAX;
195
0
    for (size_t i = 0; i < histograms.size(); ++i) {
196
        //calculate root mean squared error
197
0
        float rmse = 0.0;
198
0
        for (int j = 0; j < 255; ++j) {
199
0
            float error = fabsf(avgHistogram.r[j] - histograms[i].r[j])
200
0
                          + fabsf(avgHistogram.g[j] - histograms[i].g[j])
201
0
                          + fabsf(avgHistogram.b[j] - histograms[i].b[j]);
202
0
            rmse += (error * error) / 255;
203
0
        }
204
205
0
        rmse = sqrtf(rmse);
206
0
        if (rmse < minRMSE) {
207
0
            minRMSE = rmse;
208
0
            bestFrame = i;
209
0
        }
210
0
    }
211
#ifdef DEBUG_MODE
212
    cout << "Best frame was: " << bestFrame << "(RMSE: " << minRMSE << ")" << endl;
213
#endif
214
0
    return bestFrame;
215
0
}
216
217
}