17 #include "movieexporter.h"
24 #include <QApplication>
25 #include <QStandardPaths>
27 #include "layercamera.h"
28 #include "layersound.h"
29 #include "soundclip.h"
31 #define IMAGE_FILENAME "/test_img_%05d.png"
49 int16_t bitsPerSample;
50 char dataChuckID[ 4 ];
53 void InitWithDefaultValues()
55 strncpy( riff,
"RIFF", 4 );
57 strncpy( format,
"WAVE", 4 );
58 strncpy( fmtID,
"fmt ", 4 );
64 blockAlign = ( bitsPerSample * numChannels ) / 8;
65 byteRate = ( sampleRate * bitsPerSample * numChannels ) / 8;
67 strncpy( dataChuckID,
"data", 4 );
72 int16_t safeSumInt16( int16_t a, int16_t b )
74 int32_t a32 =
static_cast<int32_t
>( a );
75 int32_t b32 =
static_cast<int32_t
>( b );
77 if ( ( a32 + b32 ) > INT16_MAX )
81 else if ( ( a32 + b32 ) < INT16_MIN )
91 while ( memcmp( header.dataChuckID,
"data", 4 ) != 0 )
93 int skipByteCount = header.dataSize;
94 std::vector<char> skipData( skipByteCount );
95 file.read( skipData.data(), skipByteCount );
97 file.read( (
char*)&header.dataChuckID, 4 );
98 file.read( (
char*)&header.dataSize, 4 );
102 QString ffmpegLocation()
105 return QApplication::applicationDirPath() +
"/plugins/ffmpeg.exe";
107 return QApplication::applicationDirPath() +
"/plugins/ffmpeg";
109 QString ffmpegPath = QStandardPaths::findExecutable(
112 << QApplication::applicationDirPath() +
"/plugins"
113 << QApplication::applicationDirPath() +
"/../plugins"
115 if ( !ffmpegPath.isEmpty() )
119 return QStandardPaths::findExecutable(
"ffmpeg" );
123 MovieExporter::MovieExporter()
127 MovieExporter::~MovieExporter()
133 std::function<
void(
float )> progress )
137 QString ffmpegPath = ffmpegLocation();
138 qDebug() << ffmpegPath;
139 if ( !QFile::exists( ffmpegPath ) )
142 qDebug() <<
"Please place ffmpeg.exe in " << ffmpegPath <<
" directory";
144 qDebug() <<
"Please place ffmpeg in " << ffmpegPath <<
" directory";
146 qDebug() <<
"Please place ffmpeg in " << ffmpegPath <<
" directory";
148 return Status::ERROR_FFMPEG_NOT_FOUND;
151 STATUS_CHECK( checkInputParameters( desc ) );
154 qDebug() <<
"OutFile: " << mDesc.strFileName;
157 if ( !mTempDir.isValid() )
159 Q_ASSERT(
false &&
"Cannot create temp folder." );
163 mTempWorkDir = mTempDir.path();
166 if ( !desc.strFileName.endsWith(
"gif" ) )
168 STATUS_CHECK( assembleAudio( obj, ffmpegPath, progress ) );
172 STATUS_CHECK( generateImageSequence( obj, progress ) );
175 twoPassEncoding( ffmpegPath, desc.strFileName );
182 QString MovieExporter::error()
189 std::function<
void(
float )> progress )
192 int startFrame = mDesc.startFrame;
193 int endFrame = mDesc.endFrame;
196 Q_ASSERT( startFrame >= 0 );
197 Q_ASSERT( endFrame >= startFrame );
199 float lengthInSec = ( endFrame - startFrame + 1 ) / (
float)fps;
200 qDebug() <<
"Audio Length = " << lengthInSec <<
" seconds";
202 int32_t audioDataSize = 44100 * 2 * 2 * lengthInSec;
204 std::vector<int16_t> audioData( audioDataSize /
sizeof( int16_t ) );
206 bool audioDataValid =
false;
208 QDir dir( mTempWorkDir );
209 Q_ASSERT( dir.exists() );
211 QString tempAudioPath = mTempWorkDir +
"/tmpaudio0.wav";
212 qDebug() <<
"TempAudio=" << tempAudioPath;
214 std::vector< SoundClip* > allSoundClips;
216 std::vector< LayerSound* > allSoundLayers = obj->getLayersByType<
LayerSound>();
219 layer->foreachKeyFrame( [&allSoundClips](
KeyFrame* key )
221 allSoundClips.push_back( static_cast<SoundClip*>( key ) );
231 return Status::CANCELED;
237 strCmd += QString(
"\"%1\"").arg( ffmpegPath );
238 strCmd += QString(
" -i \"%1\" " ).arg( clip->fileName() );
239 strCmd +=
"-ar 44100 -acodec pcm_s16le -ac 2 -y ";
240 strCmd += QString(
"\"%1\"" ).arg( tempAudioPath );
242 executeFFMpegCommand( strCmd );
243 qDebug() <<
"audio file: " + tempAudioPath;
247 QFile file( tempAudioPath );
248 file.open( QIODevice::ReadOnly );
251 skipUselessChucks( header, file );
253 int32_t audioSize = header.dataSize;
255 qDebug() <<
"audio len " << audioSize;
258 std::vector< int16_t > data( audioSize /
sizeof( int16_t ) );
259 file.read( (
char*)data.data(), audioSize );
260 audioDataValid =
true;
262 float fframe = (float)clip->pos() / (float)fps;
263 int delta = fframe * 44100 * 2;
264 qDebug() <<
"audio delta " << delta;
266 int indexMax = std::min( audioSize / 2, audioDataSize / 2 - delta );
269 for (
int i = 0; i < indexMax; i++ )
271 audioData[ i + delta ] = safeSumInt16( audioData[ i + delta ], data[ i ] );
276 float p = ( (float)clipCount / allSoundClips.size() );
277 progress( p * 0.1f );
281 if ( !audioDataValid )
287 QFile file( mTempWorkDir +
"/tmpaudio.wav" );
288 file.open( QIODevice::WriteOnly );
291 outputHeader.InitWithDefaultValues();
292 outputHeader.dataSize = audioDataSize;
293 outputHeader.chuckSize = 36 + audioDataSize;
295 file.write( (
char*)&outputHeader,
sizeof( outputHeader ) );
296 file.write( (
char*)audioData.data(), audioDataSize );
302 Status MovieExporter::generateImageSequence(
304 std::function<
void(
float)> progress )
306 int frameStart = mDesc.startFrame;
307 int frameEnd = mDesc.endFrame;
308 QSize exportSize = mDesc.exportSize;
309 bool transparency =
false;
310 QString strCameraName = mDesc.strCameraName;
312 auto cameraLayer = (
LayerCamera*)obj->findLayerByName( strCameraName, Layer::CAMERA );
313 if ( cameraLayer ==
nullptr )
315 cameraLayer = obj->getLayersByType<
LayerCamera >().front();
318 for (
int currentFrame = frameStart; currentFrame <= frameEnd; currentFrame++ )
322 return Status::CANCELED;
325 QImage imageToExport( exportSize, QImage::Format_ARGB32_Premultiplied );
326 QColor bgColor = Qt::white;
329 bgColor.setAlpha( 0 );
331 imageToExport.fill( bgColor );
333 QPainter painter( &imageToExport );
335 QTransform view = cameraLayer->getViewAtFrame( currentFrame );
337 QSize camSize = cameraLayer->getViewSize();
338 QTransform centralizeCamera;
339 centralizeCamera.translate( camSize.width() / 2, camSize.height() / 2 );
341 painter.setWorldTransform( view * centralizeCamera );
342 painter.setWindow( QRect( 0, 0, camSize.width(), camSize.height() ) );
344 obj->paintImage( painter, currentFrame,
false,
true );
346 QString imageFileWithFrameNumber = QString().sprintf( IMAGE_FILENAME, currentFrame );
348 QString strImgPath = mTempWorkDir + imageFileWithFrameNumber;
349 bool bSave = imageToExport.save( strImgPath );
351 qDebug() <<
"Save img to: " << strImgPath <<
", Success=" << bSave;
353 float fProgressValue = ( currentFrame / (float)( frameEnd - frameStart ) );
354 progress( 0.1f + ( fProgressValue * 0.99f ) );
360 Status MovieExporter::combineVideoAndAudio( QString ffmpegPath, QString strOutputFile )
364 return Status::CANCELED;
368 const QString imgPath = mTempWorkDir + IMAGE_FILENAME;
369 const QString tempAudioPath = mTempWorkDir +
"/tmpaudio.wav";
370 const QSize exportSize = mDesc.exportSize;
372 QString strCmd = QString(
"\"%1\"").arg( ffmpegPath );
373 strCmd += QString(
" -f image2");
374 strCmd += QString(
" -framerate %1" ).arg( mDesc.fps );
375 strCmd += QString(
" -pix_fmt yuv420p" );
376 strCmd += QString(
" -start_number %1" ).arg( mDesc.startFrame );
378 strCmd += QString(
" -i \"%1\" " ).arg( imgPath );
380 if ( QFile::exists( tempAudioPath ) )
382 strCmd += QString(
" -i \"%1\" " ).arg( tempAudioPath );
385 strCmd += QString(
" -s %1x%2" ).arg( exportSize.width() ).arg( exportSize.height() );
387 strCmd += QString(
" \"%1\"" ).arg( strOutputFile );
389 STATUS_CHECK( executeFFMpegCommand( strCmd ) );
394 Status MovieExporter::twoPassEncoding( QString ffmpeg, QString strOutputFile )
396 QString strTempVideo = mTempWorkDir +
"/Temp1.mp4";
397 qDebug() <<
"TempVideo=" << strTempVideo;
399 combineVideoAndAudio( ffmpeg, strTempVideo );
401 if ( strOutputFile.endsWith(
"gif" ) )
403 STATUS_CHECK( convertToGif( ffmpeg, strTempVideo, strOutputFile ) );
407 STATUS_CHECK( convertVideoAgain( ffmpeg, strTempVideo, strOutputFile ) );
413 Status MovieExporter::convertVideoAgain( QString ffmpegPath, QString strIn, QString strOut )
415 QString strCmd = QString(
"\"%1\"").arg( ffmpegPath );
416 strCmd += QString(
" -i \"%1\" " ).arg( strIn );
417 strCmd += QString(
" -pix_fmt yuv420p" );
419 strCmd += QString(
" \"%1\"" ).arg( strOut );
421 STATUS_CHECK( executeFFMpegCommand( strCmd ) );
425 Status MovieExporter::convertToGif( QString ffmpeg, QString strIn, QString strOut )
429 QString strGifPalette = mTempWorkDir +
"/palette.png";
430 QString strCmd1 = QString(
"\"%1\"" ).arg( ffmpeg );
432 strCmd1 += QString(
" -i \"%1\"" ).arg( strIn );
433 strCmd1 +=
" -vf scale=320:-1:flags=lanczos,palettegen";
434 strCmd1 += QString(
" \"%1\"" ).arg( strGifPalette );
436 STATUS_CHECK( executeFFMpegCommand( strCmd1 ) );
439 QString strCmd2 = QString(
"\"%1\"" ).arg( ffmpeg );
441 strCmd2 += QString(
" -i \"%1\"" ).arg( strIn );
442 strCmd2 += QString(
" -i \"%1\"" ).arg( strGifPalette );
443 strCmd2 +=
" -filter_complex \"scale=-1:-1:flags=lanczos[x];[x][1:v]paletteuse\"";
444 strCmd2 += QString(
" \"%1\"" ).arg( strOut );
446 STATUS_CHECK( executeFFMpegCommand( strCmd2 ) );
451 Status MovieExporter::executeFFMpegCommand( QString strCmd )
456 ffmpeg.start( strCmd );
457 if ( ffmpeg.waitForStarted() == true )
459 if ( ffmpeg.waitForFinished() == true )
461 qDebug() <<
"stdout: " + ffmpeg.readAllStandardOutput();
462 qDebug() <<
"stderr: " + ffmpeg.readAllStandardError();
466 qDebug() <<
"ERROR: FFmpeg did not finish executing.";
472 qDebug() <<
"ERROR: Could not execute FFmpeg.";
481 b &= ( !desc.strFileName.isEmpty() );
482 b &= ( desc.startFrame > 0 );
483 b &= ( desc.endFrame >= desc.startFrame );
484 b &= ( desc.fps > 0 );
485 b &= ( !desc.strCameraName.isEmpty() );
487 return b ? Status::OK : Status::INVALID_ARGUMENT;