ardour/gtk2_ardour/transcode_ffmpeg.cc
Robin Gareus 4050ca5633
Update GPL boilerplate and (C)
Copyright-holder and year information is extracted from git log.

git history begins in 2005. So (C) from 1998..2005 is lost. Also some
(C) assignment of commits where the committer didn't use --author.
2019-08-03 15:53:15 +02:00

580 lines
17 KiB
C++

/*
* Copyright (C) 2013-2014 Tim Mayberry <mojofunk@gmail.com>
* Copyright (C) 2013-2017 Paul Davis <paul@linuxaudiosystems.com>
* Copyright (C) 2013-2019 Robin Gareus <robin@gareus.org>
* Copyright (C) 2013 John Emmas <john@creativepost.co.uk>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <stdio.h>
#include <string.h>
#include <sstream>
#include <sys/types.h>
#include "pbd/error.h"
#include "pbd/convert.h"
#include "pbd/file_utils.h"
#include "gui_thread.h"
#include "ardour/filesystem_paths.h"
#include "transcode_ffmpeg.h"
#include "utils_videotl.h"
#include "pbd/i18n.h"
using namespace PBD;
using namespace VideoUtils;
TranscodeFfmpeg::TranscodeFfmpeg (std::string f)
: infile(f)
{
probeok = false;
ffexecok = false;
m_duration = 0;
m_avoffset = m_lead_in = m_lead_out = 0;
m_width = m_height = 0;
m_aspect = m_fps = 0;
m_sar = "";
#if 1 /* tentative debug mode */
debug_enable = false;
#endif
if (!ARDOUR::ArdourVideoToolPaths::transcoder_exe(ffmpeg_exe, ffprobe_exe)) {
warning << string_compose(
_(
"ffmpeg installation was not found on this system.\n"
"%1 requires ffmpeg and ffprobe from ffmpeg.org - version 1.1 or newer.\n"
"Video import and export is not possible until you install tools.\n"
"\n"
"The tools are included with the %1 releases from ardour.org "
"and also available with the video-server at http://x42.github.com/harvid/\n"
"\n"
"Important: the files need to be installed in $PATH and named ffmpeg_harvid and ffprobe_harvid.\n"
"If you already have a suitable ffmpeg installation on your system, we recommend creating "
"symbolic links from ffmpeg to ffmpeg_harvid and from ffprobe to ffprobe_harvid.\n"
"\n"
"see also http://manual.ardour.org/video-timeline/setup/"
), PROGRAM_NAME) << endmsg;
return;
}
ffexecok = true;
if (infile.empty() || !probe()) {
return;
}
probeok = true;
}
TranscodeFfmpeg::~TranscodeFfmpeg ()
{
;
}
bool
TranscodeFfmpeg::probe ()
{
ffoutput = "";
char **argp;
argp=(char**) calloc(7,sizeof(char*));
argp[0] = strdup(ffprobe_exe.c_str());
argp[1] = strdup("-print_format");
argp[2] = strdup("csv=nk=0");
argp[3] = strdup("-show_format");
argp[4] = strdup("-show_streams");
argp[5] = strdup(infile.c_str());
argp[6] = 0;
ffcmd = new ARDOUR::SystemExec(ffprobe_exe, argp);
ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffprobeparse, this, _1 ,_2));
ffcmd->Terminated.connect (*this, invalidator (*this), boost::bind (&TranscodeFfmpeg::ffexit, this), gui_context());
if (ffcmd->start (SystemExec::IgnoreAndClose)) {
ffexit();
return false;
}
/* wait for ffprobe process to exit */
ffcmd->wait();
/* wait for interposer thread to copy all data.
* SystemExec::Terminated is emitted and ffcmd set to NULL */
int timeout = 300; // 1.5 sec
while (ffcmd && --timeout > 0) {
Glib::usleep(5000);
ARDOUR::GUIIdle();
}
if (timeout == 0 || ffoutput.empty()) {
return false;
}
/* parse */
std::vector<std::vector<std::string> > lines;
ParseCSV(ffoutput, lines);
double timebase = 0;
m_width = m_height = 0;
m_fps = m_aspect = 0;
m_duration = 0;
m_sar.clear();
m_codec.clear();
m_audio.clear();
#define PARSE_FRACTIONAL_FPS(VAR) \
{ \
std::string::size_type pos; \
VAR = atof(value); \
pos = value.find_first_of('/'); \
if (pos != std::string::npos) { \
VAR = atof(value.substr(0, pos)) / atof(value.substr(pos+1)); \
} \
}
std::string duration_from_format;
for (std::vector<std::vector<std::string> >::iterator i = lines.begin(); i != lines.end(); ++i) {
if (i->at(0) == X_("format")) {
/* format,filename,#streams,format-name,format-long-name,start-time,duration,size,bitrate */
for (std::vector<std::string>::iterator kv = i->begin(); kv != i->end(); ++kv) {
const size_t kvsep = kv->find('=');
if(kvsep == std::string::npos) continue;
std::string key = kv->substr(0, kvsep);
std::string value = kv->substr(kvsep + 1);
if (key == X_("duration")) {
duration_from_format = value;
}
}
} else
if (i->at(0) == X_("stream")) {
if (i->at(5) == X_("codec_type=video") && m_width == 0) {
for (std::vector<std::string>::iterator kv = i->begin(); kv != i->end(); ++kv) {
const size_t kvsep = kv->find('=');
if(kvsep == std::string::npos) continue;
std::string key = kv->substr(0, kvsep);
std::string value = kv->substr(kvsep + 1);
if (key == X_("index")) {
m_videoidx = atoi(value);
} else if (key == X_("width")) {
m_width = atoi(value);
} else if (key == X_("height")) {
m_height = atoi(value);
} else if (key == X_("codec_name")) {
if (!m_codec.empty()) m_codec += " ";
m_codec += value;
} else if (key == X_("codec_long_name")) {
if (!m_codec.empty()) m_codec += " ";
m_codec += "[" + value + "]";
} else if (key == X_("codec_tag_string")) {
if (!m_codec.empty()) m_codec += " ";
m_codec += "(" + value + ")";
} else if (key == X_("r_frame_rate")) {
PARSE_FRACTIONAL_FPS(m_fps)
} else if (key == X_("avg_frame_rate") && m_fps == 0) {
PARSE_FRACTIONAL_FPS(m_fps)
} else if (key == X_("time_base")) {
PARSE_FRACTIONAL_FPS(timebase)
} else if (key == X_("timecode") && m_duration == 0 && m_fps > 0) {
int h,m,s; char f[32];
if (sscanf(i->at(16).c_str(), "%d:%d:%d:%32s",&h,&m,&s,f) == 4) {
m_duration = (ARDOUR::samplecnt_t) floor(m_fps * (
h * 3600.0
+ m * 60.0
+ s * 1.0
+ atoi(f) / pow((double)10, (int)strlen(f))
));
}
} else if (key == X_("duration_ts") && m_fps == 0 && timebase !=0 ) {
m_duration = atof(value) * m_fps * timebase;
} else if (key == X_("duration") && m_fps != 0 && m_duration == 0) {
m_duration = atof(value) * m_fps;
} else if (key == X_("sample_aspect_ratio")) {
std::string::size_type pos;
pos = value.find_first_of(':');
if (pos != std::string::npos && atof(value.substr(pos+1)) != 0) {
m_sar = value;
m_sar.replace(pos, 1, "/");
}
} else if (key == X_("display_aspect_ratio")) {
std::string::size_type pos;
pos = value.find_first_of(':');
if (pos != std::string::npos && atof(value.substr(pos+1)) != 0) {
m_aspect = atof(value.substr(0, pos)) / atof(value.substr(pos+1));
}
}
}
if (m_aspect == 0) {
m_aspect = (double)m_width / (double)m_height;
}
} else if (i->at(5) == X_("codec_type=audio")) { /* new ffprobe */
FFAudioStream as;
for (std::vector<std::string>::iterator kv = i->begin(); kv != i->end(); ++kv) {
const size_t kvsep = kv->find('=');
if(kvsep == std::string::npos) continue;
std::string key = kv->substr(0, kvsep);
std::string value = kv->substr(kvsep + 1);
if (key == X_("channels")) {
as.channels = atoi(value);
} else if (key == X_("index")) {
as.stream_id = value;
} else if (key == X_("codec_long_name")) {
if (!as.name.empty()) as.name += " ";
as.name += value;
} else if (key == X_("codec_name")) {
if (!as.name.empty()) as.name += " ";
as.name += value;
} else if (key == X_("sample_fmt")) {
if (!as.name.empty()) as.name += " ";
as.name += "FMT:" + value;
} else if (key == X_("sample_rate")) {
if (!as.name.empty()) as.name += " ";
as.name += "SR:" + value;
}
}
m_audio.push_back(as);
}
}
}
/* end parse */
if (m_duration == 0 && !duration_from_format.empty() && m_fps > 0) {
warning << "using video-duration from format (container)." << endmsg;
m_duration = atof(duration_from_format) * m_fps;
}
#if 0 /* DEBUG */
printf("FPS: %f\n", m_fps);
printf("Duration: %lu frames\n",(unsigned long)m_duration);
printf("W/H: %ix%i\n",m_width, m_height);
printf("aspect: %f\n",m_aspect);
printf("codec: %s\n",m_codec.c_str());
if (m_audio.size() > 0) {
for (AudioStreams::iterator it = m_audio.begin(); it < m_audio.end(); ++it) {
printf("audio: %s - %i channels\n",(*it).stream_id.c_str(), (*it).channels);
}
} else {
printf("audio: no audio streams in file.\n");
}
#endif
return true;
}
TranscodeFfmpeg::FFSettings
TranscodeFfmpeg::default_encoder_settings ()
{
TranscodeFfmpeg::FFSettings ffs;
ffs.clear();
ffs["-vcodec"] = "mpeg4";
ffs["-acodec"] = "ac3";
ffs["-b:v"] = "5000k";
ffs["-b:a"] = "160k";
return ffs;
}
TranscodeFfmpeg::FFSettings
TranscodeFfmpeg::default_meta_data ()
{
TranscodeFfmpeg::FFSettings ffm;
ffm.clear();
ffm["comment"] = "Created with " PROGRAM_NAME;
return ffm;
}
bool
TranscodeFfmpeg::encode (std::string outfile, std::string inf_a, std::string inf_v, TranscodeFfmpeg::FFSettings ffs, TranscodeFfmpeg::FFSettings meta, bool map)
{
#define MAX_FFMPEG_ENCODER_ARGS (100)
char **argp;
int a=0;
argp=(char**) calloc(MAX_FFMPEG_ENCODER_ARGS,sizeof(char*));
argp[a++] = strdup(ffmpeg_exe.c_str());
if (m_avoffset < 0 || m_avoffset > 0) {
std::ostringstream osstream; osstream << m_avoffset;
argp[a++] = strdup("-itsoffset");
argp[a++] = strdup(osstream.str().c_str());
}
argp[a++] = strdup("-i");
argp[a++] = strdup(inf_v.c_str());
argp[a++] = strdup("-i");
argp[a++] = strdup(inf_a.c_str());
for(TranscodeFfmpeg::FFSettings::const_iterator it = ffs.begin(); it != ffs.end(); ++it) {
argp[a++] = strdup(it->first.c_str());
argp[a++] = strdup(it->second.c_str());
}
for(TranscodeFfmpeg::FFSettings::const_iterator it = meta.begin(); it != meta.end(); ++it) {
argp[a++] = strdup("-metadata");
argp[a++] = SystemExec::format_key_value_parameter (it->first.c_str(), it->second.c_str());
}
if (m_fps > 0) {
m_lead_in = rint (m_lead_in * m_fps) / m_fps;
m_lead_out = rint (m_lead_out * m_fps) / m_fps;
}
if (m_lead_in != 0 && m_lead_out != 0) {
std::ostringstream osstream;
argp[a++] = strdup("-vf");
osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_in;
if (!m_sar.empty()) osstream << X_(":sar=") << m_sar;
osstream << X_(" [pre]; ");
osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_out;
if (!m_sar.empty()) osstream << X_(":sar=") << m_sar;
osstream << X_(" [post]; ");
osstream << X_("[pre] [in] [post] concat=n=3");
argp[a++] = strdup(osstream.str().c_str());
} else if (m_lead_in != 0) {
std::ostringstream osstream;
argp[a++] = strdup("-vf");
osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_in;
if (!m_sar.empty()) osstream << X_(":sar=") << m_sar;
osstream << X_(" [pre]; ");
osstream << X_("[pre] [in] concat=n=2");
argp[a++] = strdup(osstream.str().c_str());
} else if (m_lead_out != 0) {
std::ostringstream osstream;
argp[a++] = strdup("-vf");
osstream << X_("color=c=black:s=") << m_width << X_("x") << m_height << X_(":d=") << m_lead_out;
if (!m_sar.empty()) osstream << X_(":sar=") << m_sar;
osstream << X_(" [post]; ");
osstream << X_("[in] [post] concat=n=2");
argp[a++] = strdup(osstream.str().c_str());
}
if (map) {
std::ostringstream osstream;
argp[a++] = strdup("-map");
osstream << X_("0:") << m_videoidx;
argp[a++] = strdup(osstream.str().c_str());
argp[a++] = strdup("-map");
argp[a++] = strdup("1:0");
}
argp[a++] = strdup("-y");
argp[a++] = strdup(outfile.c_str());
argp[a] = (char *)0;
assert(a<MAX_FFMPEG_ENCODER_ARGS);
/* Note: these are free()d in ~SystemExec */
#if 1 /* DEBUG */
if (debug_enable) { /* tentative debug mode */
printf("EXPORT ENCODE:\n");
for (int i=0; i< a; ++i) {
printf("%s ", argp[i]);
}
printf("\n");
}
#endif
ffcmd = new ARDOUR::SystemExec(ffmpeg_exe, argp);
ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffmpegparse_v, this, _1 ,_2));
ffcmd->Terminated.connect (*this, invalidator (*this), boost::bind (&TranscodeFfmpeg::ffexit, this), gui_context());
if (ffcmd->start (SystemExec::MergeWithStdin)) {
ffexit();
return false;
}
return true;
}
bool
TranscodeFfmpeg::extract_audio (std::string outfile, ARDOUR::samplecnt_t /*samplerate*/, unsigned int stream)
{
if (!probeok) return false;
if (stream >= m_audio.size()) return false;
char **argp;
int i = 0;
argp=(char**) calloc(15,sizeof(char*));
argp[i++] = strdup(ffmpeg_exe.c_str());
argp[i++] = strdup("-i");
argp[i++] = strdup(infile.c_str());
#if 0 /* ffmpeg write original samplerate, use a3/SRC to resample */
argp[i++] = strdup("-ar");
argp[i] = (char*) calloc(7,sizeof(char)); snprintf(argp[i++], 7, "%"PRId64, samplerate);
#endif
argp[i++] = strdup("-ac");
argp[i] = (char*) calloc(3,sizeof(char)); snprintf(argp[i++], 3, "%i", m_audio.at(stream).channels);
argp[i++] = strdup("-map");
argp[i] = (char*) calloc(8,sizeof(char)); snprintf(argp[i++], 8, "0:%s", m_audio.at(stream).stream_id.c_str());
argp[i++] = strdup("-vn");
argp[i++] = strdup("-acodec");
argp[i++] = strdup("pcm_f32le");
argp[i++] = strdup("-y");
argp[i++] = strdup(outfile.c_str());
argp[i++] = (char *)0;
/* Note: argp is free()d in ~SystemExec */
#if 1 /* DEBUG */
if (debug_enable) { /* tentative debug mode */
printf("EXTRACT AUDIO:\n");
for (int i=0; i< 14; ++i) {
printf("%s ", argp[i]);
}
printf("\n");
}
#endif
ffcmd = new ARDOUR::SystemExec(ffmpeg_exe, argp);
ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffmpegparse_a, this, _1 ,_2));
ffcmd->Terminated.connect (*this, invalidator (*this), boost::bind (&TranscodeFfmpeg::ffexit, this), gui_context());
if (ffcmd->start (SystemExec::MergeWithStdin)) {
ffexit();
return false;
}
return true;
}
bool
TranscodeFfmpeg::transcode (std::string outfile, const int outw, const int outh, const int kbitps)
{
if (!probeok) return false;
char **argp;
int bitrate = kbitps;
int width = outw;
int height = outh;
if (width < 1 || width > m_width) { width = m_width; } /* don't allow upscaling */
if (height < 1 || height > m_height) { height = floor(width / m_aspect); }
if (bitrate == 0) {
const double bitperpixel = .7; /* avg quality */
bitrate = floor(m_fps * width * height * bitperpixel / 10000.0);
} else {
bitrate = bitrate / 10;
}
if (bitrate < 10) bitrate = 10;
if (bitrate > 1000) bitrate = 1000;
argp=(char**) calloc(16,sizeof(char*));
argp[0] = strdup(ffmpeg_exe.c_str());
argp[1] = strdup("-i");
argp[2] = strdup(infile.c_str());
argp[3] = strdup("-b:v");
argp[4] = (char*) calloc(7,sizeof(char)); snprintf(argp[4], 7, "%i0k", bitrate);
argp[5] = strdup("-s");
argp[6] = (char*) calloc(10,sizeof(char)); snprintf(argp[6], 10, "%ix%i", width, height);
argp[7] = strdup("-y");
argp[8] = strdup("-vcodec");
argp[9] = strdup("mjpeg");
argp[10] = strdup("-an");
argp[11] = strdup("-intra");
argp[12] = strdup("-g");
argp[13] = strdup("1");
argp[14] = strdup(outfile.c_str());
argp[15] = (char *)0;
/* Note: these are free()d in ~SystemExec */
#if 1 /* DEBUG */
if (debug_enable) { /* tentative debug mode */
printf("TRANSCODE VIDEO:\n");
for (int i=0; i< 15; ++i) {
printf("%s ", argp[i]);
}
printf("\n");
}
#endif
ffcmd = new ARDOUR::SystemExec(ffmpeg_exe, argp);
ffcmd->ReadStdout.connect_same_thread (*this, boost::bind (&TranscodeFfmpeg::ffmpegparse_v, this, _1 ,_2));
ffcmd->Terminated.connect (*this, invalidator (*this), boost::bind (&TranscodeFfmpeg::ffexit, this), gui_context());
if (ffcmd->start (SystemExec::MergeWithStdin)) {
ffexit();
return false;
}
return true;
}
void
TranscodeFfmpeg::cancel ()
{
if (!ffcmd || !ffcmd->is_running()) { return;}
ffcmd->write_to_stdin("q");
#ifdef PLATFORM_WINDOWS
Sleep(1000);
#else
sleep (1);
#endif
if (ffcmd) {
ffcmd->terminate();
}
}
void
TranscodeFfmpeg::ffexit ()
{
delete ffcmd;
ffcmd=0;
Finished(); /* EMIT SIGNAL */
}
void
TranscodeFfmpeg::ffprobeparse (std::string d, size_t /* s */)
{
ffoutput+=d;
}
void
TranscodeFfmpeg::ffmpegparse_a (std::string d, size_t /* s */)
{
const char *t;
int h,m,s; char f[7];
ARDOUR::samplecnt_t p = -1;
if (!(t=strstr(d.c_str(), "time="))) { return; }
if (sscanf(t+5, "%d:%d:%d.%s",&h,&m,&s,f) == 4) {
p = (ARDOUR::samplecnt_t) floor( 100.0 * (
h * 3600.0
+ m * 60.0
+ s * 1.0
+ atoi(f) / pow((double)10, (int)strlen(f))
));
p = p * m_fps / 100.0;
if (p > m_duration ) { p = m_duration; }
Progress(p, m_duration); /* EMIT SIGNAL */
} else {
Progress(0, 0); /* EMIT SIGNAL */
}
}
void
TranscodeFfmpeg::ffmpegparse_v (std::string d, size_t /* s */)
{
if (strstr(d.c_str(), "ERROR") || strstr(d.c_str(), "Error") || strstr(d.c_str(), "error")) {
warning << "ffmpeg-error: " << d << endmsg;
}
if (strncmp(d.c_str(), "frame=",6)) {
#if 1 /* DEBUG */
if (debug_enable) {
d.erase(d.find_last_not_of(" \t\r\n") + 1);
printf("ffmpeg: '%s'\n", d.c_str());
}
#endif
return;
}
ARDOUR::samplecnt_t f = atol(d.substr(6));
if (f == 0) {
Progress(0, 0); /* EMIT SIGNAL */
} else {
Progress(f, m_duration); /* EMIT SIGNAL */
}
}