move ff* structs & typedef into dedicated namespace
[ardour.git] / gtk2_ardour / transcode_ffmpeg.cc
1 /*
2     Copyright (C) 2010-2013 Paul Davis
3     Author: Robin Gareus <robin@gareus.org>
4
5     This program is free software; you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation; either version 2 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with this program; if not, write to the Free Software
17     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 */
20 #include <stdio.h>
21 #include <string.h>
22 #include <sstream>
23 #include <sys/types.h>
24
25 #include "pbd/error.h"
26 #include "pbd/convert.h"
27 #include "pbd/file_utils.h"
28 #include "gui_thread.h"
29
30 #include "transcode_ffmpeg.h"
31 #include "utils_videotl.h"
32
33 #include "i18n.h"
34
35 using namespace PBD;
36
37 TranscodeFfmpeg::TranscodeFfmpeg (std::string f)
38         : infile(f)
39 {
40         probeok = false;
41         ffexecok = false;
42         ffmpeg_exe = "";
43         ffprobe_exe = "";
44         m_duration = 0;
45         m_avoffset = m_lead_in = m_lead_out = 0;
46         m_width = m_height = 0;
47         m_aspect = m_fps = 0;
48 #if 1 /* tentative debug mode */
49         debug_enable = false;
50 #endif
51
52         std::string ff_file_path;
53         if (find_file_in_search_path (SearchPath(Glib::getenv("PATH")), X_("ffmpeg_harvid"), ff_file_path)) { ffmpeg_exe = ff_file_path; }
54         else if (Glib::file_test(X_("C:\\Program Files\\harvid\\ffmpeg.exe"), Glib::FILE_TEST_EXISTS)) {
55                 ffmpeg_exe = X_("C:\\Program Files\\ffmpeg\\ffmpeg.exe");
56         }
57         else if (Glib::file_test(X_("C:\\Program Files\\ffmpeg\\ffmpeg.exe"), Glib::FILE_TEST_EXISTS)) {
58                 ffmpeg_exe = X_("C:\\Program Files\\ffmpeg\\ffmpeg.exe");
59         }
60
61         if (find_file_in_search_path (SearchPath(Glib::getenv("PATH")), X_("ffprobe_harvid"), ff_file_path)) { ffprobe_exe = ff_file_path; }
62         else if (Glib::file_test(X_("C:\\Program Files\\harvid\\ffprobe.exe"), Glib::FILE_TEST_EXISTS)) {
63                 ffprobe_exe = X_("C:\\Program Files\\ffmpeg\\ffprobe.exe");
64         }
65         else if (Glib::file_test(X_("C:\\Program Files\\ffmpeg\\ffprobe.exe"), Glib::FILE_TEST_EXISTS)) {
66                 ffprobe_exe = X_("C:\\Program Files\\ffmpeg\\ffprobe.exe");
67         }
68
69         if (ffmpeg_exe.empty() || ffprobe_exe.empty()) {
70                 warning << _(
71                                 "No ffprobe or ffmpeg executables could be found on this system.\n"
72                                 "Video import and export is not possible until you install those tools.\n"
73                                 "Ardour requires ffmpeg and ffprobe from ffmpeg.org - version 1.1 or newer.\n"
74                                 "\n"
75                                 "The tools are included with the Ardour releases from ardour.org "
76                                 "and also available with the video-server at http://x42.github.com/harvid/\n"
77                                 "\n"
78                                 "Important: the files need to be installed in $PATH and named ffmpeg_harvid and ffprobe_harvid.\n"
79                                 "If you already have a suitable ffmpeg installation on your system, we recommend creating "
80                                 "symbolic links from ffmpeg to ffmpeg_harvid and from ffprobe to ffprobe_harvid.\n"
81                                 ) << endmsg;
82                 return;
83         }
84         ffexecok = true;
85
86         if (infile.empty() || !probe()) {
87                 return;
88         }
89         probeok = true;
90 }
91
92 TranscodeFfmpeg::~TranscodeFfmpeg ()
93 {
94   ;
95 }
96
97 bool
98 TranscodeFfmpeg::probe ()
99 {
100         ffoutput = "";
101         char **argp;
102         argp=(char**) calloc(7,sizeof(char*));
103         argp[0] = strdup(ffprobe_exe.c_str());
104         argp[1] = strdup("-print_format");
105         argp[2] = strdup("csv=nk=0");
106         argp[3] = strdup("-show_format");
107         argp[4] = strdup("-show_streams");
108         argp[5] = strdup(infile.c_str());
109         argp[6] = 0;
110         ffcmd = new SystemExec(ffprobe_exe, argp);
111         ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffprobeparse, this, _1 ,_2));
112         ffcmd->Terminated.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffexit, this));
113         if (ffcmd->start(1)) {
114                 ffexit();
115                 return false;
116         }
117
118         /* wait for ffprobe process to exit */
119         ffcmd->wait();
120
121         /* wait for interposer thread to copy all data.
122          * SystemExec::Terminated is emitted and ffcmd set to NULL */
123         int timeout = 300; // 1.5 sec
124         while (ffcmd && --timeout > 0) {
125                 usleep(5000);
126         }
127         if (timeout == 0 || ffoutput.empty()) {
128                 return false;
129         }
130
131         /* parse */
132
133         std::vector<std::vector<std::string> > lines;
134         ParseCSV(ffoutput, lines);
135         double timebase = 0;
136         m_width = m_height = 0;
137         m_fps = m_aspect = 0;
138         m_duration = 0;
139         m_codec.clear();
140         m_audio.clear();
141
142 #define PARSE_FRACTIONAL_FPS(VAR) \
143         { \
144                 std::string::size_type pos; \
145                 VAR = atof(value); \
146                 pos = value.find_first_of('/'); \
147                 if (pos != std::string::npos) { \
148                         VAR = atof(value.substr(0, pos)) / atof(value.substr(pos+1)); \
149                 } \
150         }
151
152         for (std::vector<std::vector<std::string> >::iterator i = lines.begin(); i != lines.end(); ++i) {
153                 if (i->at(0) == X_("format")) {
154                         /* format,filename,#streams,format-name,format-long-name,start-time,duration,size,bitrate */
155                 } else
156                 if (i->at(0) == X_("stream")) {
157                         if (i->at(5) == X_("codec_type=video") && m_width == 0) {
158
159                                 for (std::vector<std::string>::iterator kv = i->begin(); kv != i->end(); ++kv) {
160                                         const size_t kvsep = kv->find('=');
161                                         if(kvsep == std::string::npos) continue;
162                                         std::string key = kv->substr(0, kvsep);
163                                         std::string value = kv->substr(kvsep + 1);
164
165                                         if (key == X_("index")) {
166                                                 m_videoidx = atoi(value);
167                                         } else if (key == X_("width")) {
168                                                 m_width = atoi(value);
169                                         } else if (key == X_("height")) {
170                                                 m_height = atoi(value);
171                                         } else if (key == X_("codec_name")) {
172                                                 if (!m_codec.empty()) m_codec += " ";
173                                                 m_codec += value;
174                                         } else if (key == X_("codec_long_name")) {
175                                                 if (!m_codec.empty()) m_codec += " ";
176                                                 m_codec += "[" + value + "]";
177                                         } else if (key == X_("codec_tag_string")) {
178                                                 if (!m_codec.empty()) m_codec += " ";
179                                                 m_codec += "(" + value + ")";
180                                         } else if (key == X_("r_frame_rate")) {
181                                                 PARSE_FRACTIONAL_FPS(m_fps)
182                                         } else if (key == X_("avg_frame_rate") && m_fps == 0) {
183                                                 PARSE_FRACTIONAL_FPS(m_fps)
184                                         } else if (key == X_("time_base")) {
185                                                 PARSE_FRACTIONAL_FPS(timebase)
186                                         } else if (key == X_("timecode") && m_duration == 0) {
187                                                 int h,m,s; char f[7];
188                                                 if (sscanf(i->at(16).c_str(), "%d:%d:%d:%s",&h,&m,&s,f) == 4) {
189                                                         m_duration = (ARDOUR::framecnt_t) floor(m_fps * (
190                                                                         h * 3600.0
191                                                                 + m * 60.0
192                                                                 + s * 1.0
193                                                                 + atoi(f) / pow(10, strlen(f))
194                                                         ));
195                                                 }
196                                         } else if (key == X_("duration_ts") && m_fps == 0 && timebase !=0 ) {
197                                                 m_duration = atof(value) * m_fps * timebase;
198                                         } else if (key == X_("duration") && m_fps != 0 && m_duration == 0) {
199                                                 m_duration = atof(value) * m_fps;
200                                         } else if (key == X_("display_aspect_ratio")) {
201                                                 std::string::size_type pos;
202                                                 pos = value.find_first_of(':');
203                                                 if (pos != std::string::npos && atof(value.substr(pos+1)) != 0) {
204                                                         m_aspect = atof(value.substr(0, pos)) / atof(value.substr(pos+1));
205                                                 }
206                                         }
207                                 }
208
209                                 if (m_aspect == 0) {
210                                         m_aspect = (double)m_width / (double)m_height;
211                                 }
212
213                         } else if (i->at(5) == X_("codec_type=audio")) { /* new ffprobe */
214                                 FFAudioStream as;
215                                 for (std::vector<std::string>::iterator kv = i->begin(); kv != i->end(); ++kv) {
216                                         const size_t kvsep = kv->find('=');
217                                         if(kvsep == std::string::npos) continue;
218                                         std::string key = kv->substr(0, kvsep);
219                                         std::string value = kv->substr(kvsep + 1);
220
221                                         if (key == X_("channels")) {
222                                                 as.channels   = atoi(value);
223                                         } else if (key == X_("index")) {
224                                                 as.stream_id  = value;
225                                         } else if (key == X_("codec_long_name")) {
226                                                 if (!as.name.empty()) as.name += " ";
227                                                 as.name += value;
228                                         } else if (key == X_("codec_name")) {
229                                                 if (!as.name.empty()) as.name += " ";
230                                                 as.name += value;
231                                         } else if (key == X_("sample_fmt")) {
232                                                 if (!as.name.empty()) as.name += " ";
233                                                 as.name += "FMT:" + value;
234                                         } else if (key == X_("sample_rate")) {
235                                                 if (!as.name.empty()) as.name += " ";
236                                                 as.name += "SR:" + value;
237                                         }
238
239                                 }
240                                 m_audio.push_back(as);
241                         }
242                 }
243         }
244         /* end parse */
245
246 #if 0 /* DEBUG */
247         printf("FPS: %f\n", m_fps);
248         printf("Duration: %lu frames\n",(unsigned long)m_duration);
249         printf("W/H: %ix%i\n",m_width, m_height);
250         printf("aspect: %f\n",m_aspect);
251         printf("codec: %s\n",m_codec.c_str());
252         if (m_audio.size() > 0) {
253                 for (AudioStreams::iterator it = m_audio.begin(); it < m_audio.end(); ++it) {
254                         printf("audio: %s - %i channels\n",(*it).stream_id.c_str(), (*it).channels);
255                 }
256         } else {
257           printf("audio: no audio streams in file.\n");
258         }
259 #endif
260
261         return true;
262 }
263
264 TranscodeFfmpeg::FFSettings
265 TranscodeFfmpeg::default_encoder_settings ()
266 {
267         TranscodeFfmpeg::FFSettings ffs;
268         ffs.clear();
269         ffs["-vcodec"] = "mpeg4";
270         ffs["-acodec"] = "ac3";
271         ffs["-b:v"] = "5000k";
272         ffs["-b:a"] = "160k";
273         return ffs;
274 }
275
276 TranscodeFfmpeg::FFSettings
277 TranscodeFfmpeg::default_meta_data ()
278 {
279         TranscodeFfmpeg::FFSettings ffm;
280         ffm.clear();
281         ffm["comment"] = "Created with ardour";
282         return ffm;
283 }
284
285 char *
286 TranscodeFfmpeg::format_metadata (std::string key, std::string value)
287 {
288         size_t start_pos = 0;
289         std::string v1 = value;
290         while((start_pos = v1.find_first_not_of(
291                         "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(),.\"'",
292                         start_pos)) != std::string::npos)
293         {
294                 v1.replace(start_pos, 1, "_");
295                 start_pos += 1;
296         }
297
298         start_pos = 0;
299         while((start_pos = v1.find("\"", start_pos)) != std::string::npos) {
300                 v1.replace(start_pos, 1, "\\\"");
301                 start_pos += 2;
302         }
303
304         size_t len = key.length() + v1.length() + 4;
305         char *mds = (char*) calloc(len, sizeof(char));
306         snprintf(mds, len, "%s=\"%s\"", key.c_str(), v1.c_str());
307         return mds;
308 }
309
310 bool
311 TranscodeFfmpeg::encode (std::string outfile, std::string inf_a, std::string inf_v, TranscodeFfmpeg::FFSettings ffs, TranscodeFfmpeg::FFSettings meta, bool map)
312 {
313 #define MAX_FFMPEG_ENCODER_ARGS (100)
314         char **argp;
315         int a=0;
316
317         argp=(char**) calloc(MAX_FFMPEG_ENCODER_ARGS,sizeof(char*));
318         argp[a++] = strdup(ffmpeg_exe.c_str());
319         if (m_avoffset < 0 || m_avoffset > 0) {
320                 std::ostringstream osstream; osstream << m_avoffset;
321                 argp[a++] = strdup("-itsoffset");
322                 argp[a++] = strdup(osstream.str().c_str());
323         }
324         argp[a++] = strdup("-i");
325         argp[a++] = strdup(inf_v.c_str());
326
327         argp[a++] = strdup("-i");
328         argp[a++] = strdup(inf_a.c_str());
329
330         for(TranscodeFfmpeg::FFSettings::const_iterator it = ffs.begin(); it != ffs.end(); ++it) {
331                 argp[a++] = strdup(it->first.c_str());
332                 argp[a++] = strdup(it->second.c_str());
333         }
334         for(TranscodeFfmpeg::FFSettings::const_iterator it = meta.begin(); it != meta.end(); ++it) {
335                 argp[a++] = strdup("-metadata");
336                 argp[a++] = format_metadata(it->first.c_str(), it->second.c_str());
337         }
338         if (m_lead_in != 0 && m_lead_out != 0) {
339                 std::ostringstream osstream;
340                 argp[a++] = strdup("-vf");
341                 osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_in << X_(" [pre]; ");
342                 osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_out << X_(" [post]; ");
343                 osstream << X_("[pre] [in] [post] concat=n=3");
344                 argp[a++] = strdup(osstream.str().c_str());
345         } else if (m_lead_in != 0) {
346                 std::ostringstream osstream;
347                 argp[a++] = strdup("-vf");
348                 osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_in << X_(" [pre]; ");
349                 osstream << X_("[pre] [in] concat=n=2");
350                 argp[a++] = strdup(osstream.str().c_str());
351         } else if (m_lead_out != 0) {
352                 std::ostringstream osstream;
353                 argp[a++] = strdup("-vf");
354                 osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_out << X_(" [post]; ");
355                 osstream << X_("[in] [post] concat=n=2");
356                 argp[a++] = strdup(osstream.str().c_str());
357         }
358
359         if (map) {
360                 std::ostringstream osstream;
361                 argp[a++] = strdup("-map");
362                 osstream << X_("0:") << m_videoidx;
363                 argp[a++] = strdup(osstream.str().c_str());
364                 argp[a++] = strdup("-map");
365                 argp[a++] = strdup("1:0");
366         }
367
368         argp[a++] = strdup("-y");
369         argp[a++] = strdup(outfile.c_str());
370         argp[a] = (char *)0;
371         assert(a<MAX_FFMPEG_ENCODER_ARGS);
372         /* Note: these are free()d in ~SystemExec */
373 #if 1 /* DEBUG */
374         if (debug_enable) { /* tentative debug mode */
375         printf("EXPORT ENCODE:\n");
376         for (int i=0; i< a; ++i) {
377           printf("%s ", argp[i]);
378         }
379         printf("\n");
380         }
381 #endif
382
383         ffcmd = new SystemExec(ffmpeg_exe, argp);
384         ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffmpegparse_v, this, _1 ,_2));
385         ffcmd->Terminated.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffexit, this));
386         if (ffcmd->start(2)) {
387                 ffexit();
388                 return false;
389         }
390         return true;
391 }
392
393 bool
394 TranscodeFfmpeg::extract_audio (std::string outfile, ARDOUR::framecnt_t samplerate, unsigned int stream)
395 {
396         if (!probeok) return false;
397   if (stream >= m_audio.size()) return false;
398
399         char **argp;
400         int i = 0;
401
402         argp=(char**) calloc(15,sizeof(char*));
403         argp[i++] = strdup(ffmpeg_exe.c_str());
404         argp[i++] = strdup("-i");
405         argp[i++] = strdup(infile.c_str());
406 #if 0 /* ffmpeg write original samplerate, use a3/SRC to resample */
407         argp[i++] = strdup("-ar");
408         argp[i] = (char*) calloc(7,sizeof(char)); snprintf(argp[i++], 7, "%"PRId64, samplerate);
409 #endif
410         argp[i++] = strdup("-ac");
411         argp[i] = (char*) calloc(3,sizeof(char)); snprintf(argp[i++], 3, "%i", m_audio.at(stream).channels);
412         argp[i++] = strdup("-map");
413         argp[i] = (char*) calloc(8,sizeof(char)); snprintf(argp[i++], 8, "0:%s", m_audio.at(stream).stream_id.c_str());
414         argp[i++] = strdup("-vn");
415         argp[i++] = strdup("-acodec");
416         argp[i++] = strdup("pcm_f32le");
417         argp[i++] = strdup("-y");
418         argp[i++] = strdup(outfile.c_str());
419         argp[i++] = (char *)0;
420         /* Note: argp is free()d in ~SystemExec */
421 #if 1 /* DEBUG */
422         if (debug_enable) { /* tentative debug mode */
423         printf("EXTRACT AUDIO:\n");
424         for (int i=0; i< 14; ++i) {
425           printf("%s ", argp[i]);
426         }
427         printf("\n");
428         }
429 #endif
430
431         ffcmd = new SystemExec(ffmpeg_exe, argp);
432         ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffmpegparse_a, this, _1 ,_2));
433         ffcmd->Terminated.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffexit, this));
434         if (ffcmd->start(2)) {
435                 ffexit();
436                 return false;
437         }
438         return true;
439 }
440
441
442 bool
443 TranscodeFfmpeg::transcode (std::string outfile, const int outw, const int outh, const int kbitps)
444 {
445         if (!probeok) return false;
446
447         char **argp;
448         int bitrate = kbitps;
449         int width = outw;
450         int height = outh;
451
452         if (width < 1 || width > m_width) { width = m_width; } /* don't allow upscaling */
453         if (height < 1 || height > m_height) { height = floor(width / m_aspect); }
454
455         if (bitrate == 0) {
456                 const double bitperpixel = .7; /* avg quality */
457                 bitrate = floor(m_fps * width * height * bitperpixel / 10000.0);
458         } else {
459                 bitrate = bitrate / 10;
460         }
461         if (bitrate < 10)  bitrate = 10;
462         if (bitrate > 1000) bitrate = 1000;
463
464         argp=(char**) calloc(16,sizeof(char*));
465         argp[0] = strdup(ffmpeg_exe.c_str());
466         argp[1] = strdup("-i");
467         argp[2] = strdup(infile.c_str());
468         argp[3] = strdup("-b:v");
469         argp[4] = (char*) calloc(7,sizeof(char)); snprintf(argp[4], 7, "%i0k", bitrate);
470         argp[5] = strdup("-s");
471         argp[6] = (char*) calloc(10,sizeof(char)); snprintf(argp[6], 10, "%ix%i", width, height);
472         argp[7] = strdup("-y");
473         argp[8] = strdup("-vcodec");
474         argp[9] = strdup("mjpeg");
475         argp[10] = strdup("-an");
476         argp[11] = strdup("-intra");
477         argp[12] = strdup("-g");
478         argp[13] = strdup("1");
479         argp[14] = strdup(outfile.c_str());
480         argp[15] = (char *)0;
481         /* Note: these are free()d in ~SystemExec */
482 #if 1 /* DEBUG */
483         if (debug_enable) { /* tentative debug mode */
484         printf("TRANSCODE VIDEO:\n");
485         for (int i=0; i< 15; ++i) {
486           printf("%s ", argp[i]);
487         }
488         printf("\n");
489         }
490 #endif
491         ffcmd = new SystemExec(ffmpeg_exe, argp);
492         ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffmpegparse_v, this, _1 ,_2));
493         ffcmd->Terminated.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffexit, this));
494         if (ffcmd->start(2)) {
495                 ffexit();
496                 return false;
497         }
498         return true;
499 }
500
501 void
502 TranscodeFfmpeg::cancel ()
503 {
504         if (!ffcmd || !ffcmd->is_running()) { return;}
505         ffcmd->write_to_stdin("q");
506         sleep (1);
507         if (ffcmd) {
508           ffcmd->terminate();
509         }
510 }
511
512 void
513 TranscodeFfmpeg::ffexit ()
514 {
515         delete ffcmd;
516         ffcmd=0;
517         Finished(); /* EMIT SIGNAL */
518 }
519
520 void
521 TranscodeFfmpeg::ffprobeparse (std::string d, size_t /* s */)
522 {
523         ffoutput+=d;
524 }
525
526 void
527 TranscodeFfmpeg::ffmpegparse_a (std::string d, size_t /* s */)
528 {
529         const char *t;
530         int h,m,s; char f[7];
531         ARDOUR::framecnt_t p = -1;
532
533         if (!(t=strstr(d.c_str(), "time="))) { return; }
534
535         if (sscanf(t+5, "%d:%d:%d.%s",&h,&m,&s,f) == 4) {
536                 p = (ARDOUR::framecnt_t) floor( 100.0 * (
537                       h * 3600.0
538                     + m * 60.0
539                     + s * 1.0
540                     + atoi(f) / pow(10, strlen(f))
541                 ));
542                 p = p * m_fps / 100.0;
543                 if (p > m_duration ) { p = m_duration; }
544                 Progress(p, m_duration); /* EMIT SIGNAL */
545         } else {
546                 Progress(0, 0); /* EMIT SIGNAL */
547         }
548 }
549
550 void
551 TranscodeFfmpeg::ffmpegparse_v (std::string d, size_t /* s */)
552 {
553         if (strstr(d.c_str(), "ERROR") || strstr(d.c_str(), "Error") || strstr(d.c_str(), "error")) {
554                 warning << "ffmpeg-error: " << d << endmsg;
555         }
556         if (strncmp(d.c_str(), "frame=",6)) {
557 #if 1 /* DEBUG */
558                 if (debug_enable) {
559                         d.erase(d.find_last_not_of(" \t\r\n") + 1);
560                   printf("ffmpeg: '%s'\n", d.c_str());
561                 }
562 #endif
563                 Progress(0, 0); /* EMIT SIGNAL */
564                 return;
565         }
566         ARDOUR::framecnt_t f = atol(d.substr(6));
567         if (f == 0) {
568                 Progress(0, 0); /* EMIT SIGNAL */
569         } else {
570                 Progress(f, m_duration); /* EMIT SIGNAL */
571         }
572 }