1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2017-05-25
7 * Description : a tool to generate video slideshow from images.
8 *
9 * Copyright (C) 2017-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10 *
11 * This program is free software; you can redistribute it
12 * and/or modify it under the terms of the GNU General
13 * Public License as published by the Free Software Foundation;
14 * either version 2, or (at your option)
15 * any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * ============================================================ */
23
24 #include "vidslidetask.h"
25
26 // C++ includes
27
28 #include <cmath>
29
30 // Qt includes
31
32 #include <QImage>
33 #include <QSize>
34 #include <QPainter>
35 #include <QFileInfo>
36
37 // KDE includes
38
39 #include <klocalizedstring.h>
40
41 // QtAv includes
42
43 #include <QtAV/VideoFrame.h> // krazy:exclude=includes
44 #include <QtAV/AudioFrame.h> // krazy:exclude=includes
45 #include <QtAV/VideoEncoder.h> // krazy:exclude=includes
46 #include <QtAV/AudioEncoder.h> // krazy:exclude=includes
47 #include <QtAV/AudioDecoder.h> // krazy:exclude=includes
48 #include <QtAV/AVMuxer.h> // krazy:exclude=includes
49 #include <QtAV/AVDemuxer.h> // krazy:exclude=includes
50
51 // Local includes
52
53 #include "frameutils.h"
54 #include "dfileoperations.h"
55 #include "transitionmngr.h"
56 #include "effectmngr.h"
57 #include "digikam_debug.h"
58 #include "digikam_config.h"
59
60 using namespace QtAV;
61
62 namespace Digikam
63 {
64
65 class Q_DECL_HIDDEN VidSlideTask::Private
66 {
67 public:
68
Private()69 explicit Private()
70 : settings(nullptr),
71 astream (0),
72 adec (AudioDecoder::create("FFmpeg"))
73 {
74 }
75
~Private()76 ~Private()
77 {
78 adec->close();
79 }
80
81 bool encodeFrame(VideoFrame& vframe,
82 VideoEncoder* const venc,
83 AudioEncoder* const aenc,
84 AVMuxer& mux);
85
86 AudioFrame nextAudioFrame(const AudioFormat& afmt);
87
88 public:
89
90 VidSlideSettings* settings;
91 AVDemuxer demuxer;
92 Packet apkt;
93 int astream;
94 AudioDecoder* adec;
95 QList<QUrl>::const_iterator curAudioFile;
96 };
97
encodeFrame(VideoFrame & vframe,VideoEncoder * const venc,AudioEncoder * const aenc,AVMuxer & mux)98 bool VidSlideTask::Private::encodeFrame(VideoFrame& vframe,
99 VideoEncoder* const venc,
100 AudioEncoder* const aenc,
101 AVMuxer& mux)
102 {
103 Packet apkt;
104 Packet vpkt;
105
106 if (curAudioFile != settings->inputAudio.constEnd())
107 {
108 AudioFrame aframe = nextAudioFrame(aenc->audioFormat());
109
110 if (!apkt.isValid())
111 {
112 qCWarning(DIGIKAM_GENERAL_LOG) << "Invalid audio frame";
113 }
114 else
115 {
116 if (aenc->encode(aframe))
117 {
118 apkt = aenc->encoded();
119 }
120 else
121 {
122 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to encode audio frame";
123 }
124 }
125 }
126
127 if (vframe.pixelFormat() != venc->pixelFormat())
128 {
129 vframe = vframe.to(venc->pixelFormat());
130 }
131
132 if (venc->encode(vframe))
133 {
134 vpkt = venc->encoded();
135
136 if (vpkt.isValid())
137 {
138 mux.writeVideo(vpkt);
139 }
140
141 if (apkt.isValid())
142 {
143 mux.writeAudio(apkt);
144 }
145
146 return true;
147 }
148
149 return false;
150 }
151
nextAudioFrame(const AudioFormat & afmt)152 AudioFrame VidSlideTask::Private::nextAudioFrame(const AudioFormat& afmt)
153 {
154 if (curAudioFile == settings->inputAudio.constEnd())
155 {
156 return AudioFrame();
157 }
158
159 if (demuxer.atEnd() || demuxer.fileName().isEmpty())
160 {
161 if (demuxer.fileName().isEmpty())
162 {
163 curAudioFile = settings->inputAudio.constBegin();
164 }
165 else
166 {
167 ++curAudioFile;
168 }
169
170 if (curAudioFile != settings->inputAudio.constEnd())
171 {
172 demuxer.setMedia((*curAudioFile).toLocalFile());
173
174 if (!demuxer.load())
175 {
176 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to open audio file" << demuxer.fileName();
177 return AudioFrame();
178 }
179
180 adec->setCodecContext(demuxer.audioCodecContext());
181
182 if (!adec->open())
183 {
184 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to open audio stream in decode"
185 << demuxer.fileName();
186 return AudioFrame();
187 }
188 }
189 else
190 {
191 return AudioFrame();
192 }
193 }
194
195 while (!demuxer.atEnd())
196 {
197 if (!apkt.isValid())
198 {
199 if (!demuxer.readFrame() || (demuxer.stream() != astream))
200 {
201 continue;
202 }
203
204 apkt = demuxer.packet();
205 }
206
207 if (!adec->decode(apkt))
208 {
209 apkt = Packet();
210 continue;
211 }
212
213 apkt.data = QByteArray::fromRawData(apkt.data.constData() + apkt.data.size() -
214 adec->undecodedSize(), adec->undecodedSize());
215
216 AudioFrame aframe = adec->frame();
217
218 if (aframe.format() != afmt)
219 {
220 qCDebug(DIGIKAM_GENERAL_LOG) << "Audio transcoding:";
221 qCDebug(DIGIKAM_GENERAL_LOG) << "current format =" << aframe.format();
222 qCDebug(DIGIKAM_GENERAL_LOG) << "target format =" << afmt;
223 /*
224 adec->resampler()->setOutAudioFormat(afmt);
225 adec->resampler()->prepare();
226 aframe.setAudioResampler(adec->resampler());
227 */
228 aframe = aframe.to(afmt);
229 }
230
231 return aframe;
232 }
233
234 return AudioFrame();
235 }
236
237 // -------------------------------------------------------
238
VidSlideTask(VidSlideSettings * const settings)239 VidSlideTask::VidSlideTask(VidSlideSettings* const settings)
240 : ActionJob(),
241 d (new Private)
242 {
243 d->settings = settings;
244
245 if (d->settings->inputAudio.isEmpty())
246 {
247 d->curAudioFile = d->settings->inputAudio.constEnd();
248 }
249 }
250
~VidSlideTask()251 VidSlideTask::~VidSlideTask()
252 {
253 cancel();
254 delete d;
255 }
256
run()257 void VidSlideTask::run()
258 {
259 // ---------------------------------------------
260 // Setup output video file
261
262 QUrl dest = d->settings->outputDir;
263 dest = dest.adjusted(QUrl::StripTrailingSlash);
264 dest.setPath(dest.path() + QLatin1String("/videoslideshow.") + d->settings->videoFormat());
265 QString outFile = dest.toLocalFile();
266 QFileInfo fi(outFile);
267
268 if (fi.exists() && (d->settings->conflictRule != FileSaveConflictBox::OVERWRITE))
269 {
270 outFile = DFileOperations::getUniqueFileUrl(dest).toLocalFile();
271 }
272
273 // ---------------------------------------------
274 // Setup Video Encoder
275
276 VideoEncoder* const venc = VideoEncoder::create("FFmpeg");
277 venc->setCodecName(d->settings->videoCodec());
278 venc->setBitRate(d->settings->videoBitRate());
279 venc->setFrameRate(d->settings->videoFrameRate());
280
281 QSize osize = d->settings->videoSize();
282 venc->setWidth(osize.width());
283 venc->setHeight(osize.height());
284
285 if (!venc->open())
286 {
287 emit signalMessage(i18n("Failed to open video encoder"), true);
288 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to open video encoder";
289 emit signalDone(false);
290
291 return;
292 }
293
294 // ---------------------------------------------
295 // Setup Audio Encoder
296
297 AudioEncoder* const aenc = AudioEncoder::create("FFmpeg");
298 aenc->setCodecName(QLatin1String("mp2"));
299 aenc->setBitRate(d->settings->abitRate);
300
301 if (!aenc->open())
302 {
303 emit signalMessage(i18n("Failed to open audio encoder"), true);
304 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to open audio encoder";
305 emit signalDone(false);
306
307 return;
308 }
309
310 // ---------------------------------------------
311 // Setup Muxer
312
313 AVMuxer mux;
314 mux.setMedia(outFile);
315 mux.copyProperties(venc); // Setup video encoder
316 mux.copyProperties(aenc); // Setup audio encoder
317 /*
318 // Segments muxer ffmpeg options. See : https://www.ffmpeg.org/ffmpeg-formats.html#Options-11
319
320 QVariantHash avfopt;
321 avfopt[QLatin1String("segment_time")] = 4;
322 avfopt[QLatin1String("segment_list_size")] = 0;
323 avfopt[QLatin1String("segment_format")] = QLatin1String("mpegts");
324 avfopt[QLatin1String("segment_list")] = outFile.left(outFile.lastIndexOf(QLatin1Char('/'))+1)
325 .append(QLatin1String("index.m3u8"));
326 QVariantHash muxopt;
327 muxopt[QLatin1String("avformat")] = avfopt;
328
329 mux.setOptions(muxopt);
330 */
331 if (!mux.open())
332 {
333 emit signalMessage(i18n("Failed to open muxer"), true);
334 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to open muxer";
335 emit signalDone(false);
336
337 return;
338 }
339
340 QImage qiimg;
341
342 // ---------------------------------------------
343 // Loop to encode frames with images list
344
345 TransitionMngr transmngr;
346 transmngr.setOutputSize(osize);
347
348 EffectMngr effmngr;
349 effmngr.setOutputSize(osize);
350 effmngr.setFrames(d->settings->imgFrames);
351
352 for (int i = 0 ; ((i < d->settings->inputImages.count() + 1) && !m_cancel) ; ++i)
353 {
354 if (i == 0)
355 {
356 qiimg = FrameUtils::makeFramedImage(QString(), osize);
357 }
358
359 QString ofile;
360
361 if (i < d->settings->inputImages.count())
362 {
363 ofile = d->settings->inputImages[i].toLocalFile();
364 }
365
366 QImage qoimg = FrameUtils::makeFramedImage(ofile, osize);
367
368 // -- Transition encoding ----------
369
370 transmngr.setInImage(qiimg);
371 transmngr.setOutImage(qoimg);
372 transmngr.setTransition(d->settings->transition);
373
374 int ttmout = 0;
375
376 do
377 {
378 VideoFrame frame(transmngr.currentFrame(ttmout));
379
380 if (!d->encodeFrame(frame, venc, aenc, mux))
381 {
382 qCWarning(DIGIKAM_GENERAL_LOG) << "Cannot encode transition frame";
383 }
384 }
385 while ((ttmout != -1) && !m_cancel);
386
387 // -- Images encoding ----------
388
389 if (i < d->settings->inputImages.count())
390 {
391 VideoFrame frame;
392 int count = 0;
393 int itmout = 0;
394 effmngr.setImage(qoimg);
395 effmngr.setEffect(d->settings->vEffect);
396
397 do
398 {
399 qiimg = effmngr.currentFrame(itmout);
400 frame = VideoFrame(qiimg);
401
402 if (d->encodeFrame(frame, venc, aenc, mux))
403 {
404
405 ++count;
406 /*
407 qCDebug(DIGIKAM_GENERAL_LOG) << ofile
408 << " => encode count:" << count
409 << "frame size:" << frame.width()
410 << "x" << frame.height();
411 */
412 }
413 }
414 while ((count < d->settings->imgFrames) && !m_cancel);
415 }
416
417 qCDebug(DIGIKAM_GENERAL_LOG) << "Encoded image" << i << "done";
418
419 emit signalMessage(i18n("Encoding %1 Done", ofile), false);
420 emit signalProgress(i);
421 }
422
423 // ---------------------------------------------
424 // Get delayed frames
425
426 qCDebug(DIGIKAM_GENERAL_LOG) << "Encode delayed frames...";
427
428 while (venc->encode() && !m_cancel)
429 {
430 Packet vpkt(venc->encoded());
431
432 if (vpkt.isValid())
433 {
434 mux.writeVideo(vpkt);
435 }
436
437 Packet apkt(aenc->encoded());
438
439 if (apkt.isValid())
440 {
441 mux.writeAudio(apkt);
442 }
443 }
444
445 // ---------------------------------------------
446 // Cleanup
447
448 venc->close();
449 aenc->close();
450 mux.close();
451
452 if (!m_cancel)
453 {
454 emit signalMessage(i18n("Output video is %1", outFile), false);
455 d->settings->outputVideo = outFile;
456 }
457
458 emit signalDone(!m_cancel);
459 }
460
461 } // namespace Digikam
462