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