1 /* This file is part of Clementine.
2    Copyright 2010, David Sansome <me@davidsansome.com>
3 
4    Clementine is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    Clementine is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 #include "transcoder.h"
19 
20 #include <algorithm>
21 #include <memory>
22 
23 #include <QCoreApplication>
24 #include <QDir>
25 #include <QFile>
26 #include <QSettings>
27 #include <QThread>
28 #include <QtDebug>
29 
30 #include "core/logging.h"
31 #include "core/signalchecker.h"
32 #include "core/utilities.h"
33 
34 using std::shared_ptr;
35 
36 int Transcoder::JobFinishedEvent::sEventType = -1;
37 
TranscoderPreset(Song::FileType type,const QString & name,const QString & extension,const QString & codec_mimetype,const QString & muxer_mimetype)38 TranscoderPreset::TranscoderPreset(Song::FileType type, const QString& name,
39                                    const QString& extension,
40                                    const QString& codec_mimetype,
41                                    const QString& muxer_mimetype)
42     : type_(type),
43       name_(name),
44       extension_(extension),
45       codec_mimetype_(codec_mimetype),
46       muxer_mimetype_(muxer_mimetype) {}
47 
CreateElement(const QString & factory_name,GstElement * bin,const QString & name)48 GstElement* Transcoder::CreateElement(const QString& factory_name,
49                                       GstElement* bin, const QString& name) {
50   GstElement* ret = gst_element_factory_make(
51       factory_name.toLatin1().constData(),
52       name.isNull() ? factory_name.toLatin1().constData()
53                     : name.toLatin1().constData());
54 
55   if (ret && bin) gst_bin_add(GST_BIN(bin), ret);
56 
57   if (!ret) {
58     emit LogLine(
59         tr("Could not create the GStreamer element \"%1\" -"
60            " make sure you have all the required GStreamer plugins installed")
61             .arg(factory_name));
62   } else {
63     SetElementProperties(factory_name, G_OBJECT(ret));
64   }
65 
66   return ret;
67 }
68 
69 struct SuitableElement {
SuitableElementSuitableElement70   SuitableElement(const QString& name = QString(), int rank = 0)
71       : name_(name), rank_(rank) {}
72 
operator <SuitableElement73   bool operator<(const SuitableElement& other) const {
74     return rank_ < other.rank_;
75   }
76 
77   QString name_;
78   int rank_;
79 };
80 
CreateElementForMimeType(const QString & element_type,const QString & mime_type,GstElement * bin)81 GstElement* Transcoder::CreateElementForMimeType(const QString& element_type,
82                                                  const QString& mime_type,
83                                                  GstElement* bin) {
84   if (mime_type.isEmpty()) return nullptr;
85 
86   // HACK: Force mp4mux because it doesn't set any useful src caps
87   if (mime_type == "audio/mp4") {
88     LogLine(QString("Using '%1' (rank %2)").arg("mp4mux").arg(-1));
89     return CreateElement("mp4mux", bin);
90   }
91 
92   // Keep track of all the suitable elements we find and figure out which
93   // is the best at the end.
94   QList<SuitableElement> suitable_elements_;
95 
96   // The caps we're trying to find
97   GstCaps* target_caps = gst_caps_from_string(mime_type.toUtf8().constData());
98 
99   GstRegistry* registry = gst_registry_get();
100   GList* const features =
101       gst_registry_get_feature_list(registry, GST_TYPE_ELEMENT_FACTORY);
102 
103   for (GList* p = features; p; p = g_list_next(p)) {
104     GstElementFactory* factory = GST_ELEMENT_FACTORY(p->data);
105 
106     // Is this the right type of plugin?
107     if (QString(gst_element_factory_get_klass(factory)).contains(element_type)) {
108       const GList* const templates =
109           gst_element_factory_get_static_pad_templates(factory);
110       for (const GList* p = templates; p; p = g_list_next(p)) {
111         // Only interested in source pads
112         GstStaticPadTemplate* pad_template =
113             reinterpret_cast<GstStaticPadTemplate*>(p->data);
114         if (pad_template->direction != GST_PAD_SRC) continue;
115 
116         // Does this pad support the mime type we want?
117         GstCaps* caps = gst_static_pad_template_get_caps(pad_template);
118         GstCaps* intersection = gst_caps_intersect(caps, target_caps);
119 
120         if (intersection) {
121           if (!gst_caps_is_empty(intersection)) {
122             int rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory));
123             QString name = GST_OBJECT_NAME(factory);
124 
125             if (name.startsWith("ffmux") || name.startsWith("ffenc"))
126               rank = -1;  // ffmpeg usually sucks
127 
128             suitable_elements_ << SuitableElement(name, rank);
129           }
130           gst_caps_unref(intersection);
131         }
132       }
133     }
134   }
135 
136   gst_plugin_feature_list_free(features);
137   gst_caps_unref(target_caps);
138 
139   if (suitable_elements_.isEmpty()) return nullptr;
140 
141   // Sort by rank
142   std::sort(suitable_elements_.begin(), suitable_elements_.end());
143   const SuitableElement& best = suitable_elements_.last();
144 
145   LogLine(QString("Using '%1' (rank %2)").arg(best.name_).arg(best.rank_));
146 
147   if (best.name_ == "lamemp3enc") {
148     // Special case: we need to add xingmux and id3v2mux to the pipeline when
149     // using lamemp3enc because it doesn't write the VBR or ID3v2 headers
150     // itself.
151 
152     LogLine("Adding xingmux and id3v2mux to the pipeline");
153 
154     // Create the bin
155     GstElement* mp3bin = gst_bin_new("mp3bin");
156     gst_bin_add(GST_BIN(bin), mp3bin);
157 
158     // Create the elements
159     GstElement* lame = CreateElement("lamemp3enc", mp3bin);
160     GstElement* xing = CreateElement("xingmux", mp3bin);
161     GstElement* id3v2 = CreateElement("id3v2mux", mp3bin);
162 
163     if (!lame || !xing || !id3v2) {
164       return nullptr;
165     }
166 
167     // Link the elements together
168     gst_element_link_many(lame, xing, id3v2, nullptr);
169 
170     // Link the bin's ghost pads to the elements on each end
171     GstPad* pad = gst_element_get_static_pad(lame, "sink");
172     gst_element_add_pad(mp3bin, gst_ghost_pad_new("sink", pad));
173     gst_object_unref(GST_OBJECT(pad));
174 
175     pad = gst_element_get_static_pad(id3v2, "src");
176     gst_element_add_pad(mp3bin, gst_ghost_pad_new("src", pad));
177     gst_object_unref(GST_OBJECT(pad));
178 
179     return mp3bin;
180   } else {
181     return CreateElement(best.name_, bin);
182   }
183 }
184 
JobFinishedEvent(JobState * state,bool success)185 Transcoder::JobFinishedEvent::JobFinishedEvent(JobState* state, bool success)
186     : QEvent(QEvent::Type(sEventType)), state_(state), success_(success) {}
187 
PostFinished(bool success)188 void Transcoder::JobState::PostFinished(bool success) {
189   if (success) {
190     emit parent_->LogLine(tr("Successfully written %1")
191                               .arg(QDir::toNativeSeparators(job_.output)));
192   }
193 
194   QCoreApplication::postEvent(parent_,
195                               new Transcoder::JobFinishedEvent(this, success));
196 }
197 
Transcoder(QObject * parent,const QString & settings_postfix)198 Transcoder::Transcoder(QObject* parent, const QString& settings_postfix)
199     : QObject(parent),
200       max_threads_(QThread::idealThreadCount()),
201       settings_postfix_(settings_postfix) {
202   if (JobFinishedEvent::sEventType == -1)
203     JobFinishedEvent::sEventType = QEvent::registerEventType();
204 
205   // Initialise some settings for the lamemp3enc element.
206   QSettings s;
207   s.beginGroup("Transcoder/lamemp3enc" + settings_postfix_);
208 
209   if (s.value("target").isNull()) {
210     s.setValue("target", 1);  // 1 == bitrate
211   }
212   if (s.value("cbr").isNull()) {
213     s.setValue("cbr", true);
214   }
215 }
216 
GetAllPresets()217 QList<TranscoderPreset> Transcoder::GetAllPresets() {
218   QList<TranscoderPreset> ret;
219   ret << PresetForFileType(Song::Type_Flac);
220   ret << PresetForFileType(Song::Type_Mp4);
221   ret << PresetForFileType(Song::Type_Mpeg);
222   ret << PresetForFileType(Song::Type_OggVorbis);
223   ret << PresetForFileType(Song::Type_OggFlac);
224   ret << PresetForFileType(Song::Type_OggSpeex);
225   ret << PresetForFileType(Song::Type_Asf);
226   ret << PresetForFileType(Song::Type_Wav);
227   ret << PresetForFileType(Song::Type_OggOpus);
228   return ret;
229 }
230 
PresetForFileType(Song::FileType type)231 TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
232   switch (type) {
233     case Song::Type_Flac:
234       return TranscoderPreset(type, tr("FLAC"), "flac", "audio/x-flac");
235     case Song::Type_Mp4:
236       return TranscoderPreset(type, tr("M4A AAC"), "mp4",
237                               "audio/mpeg, mpegversion=(int)4", "audio/mp4");
238     case Song::Type_Mpeg:
239       return TranscoderPreset(type, tr("MP3"), "mp3",
240                               "audio/mpeg, mpegversion=(int)1, layer=(int)3");
241     case Song::Type_OggVorbis:
242       return TranscoderPreset(type, tr("Ogg Vorbis"), "ogg", "audio/x-vorbis",
243                               "application/ogg");
244     case Song::Type_OggFlac:
245       return TranscoderPreset(type, tr("Ogg Flac"), "ogg", "audio/x-flac",
246                               "application/ogg");
247     case Song::Type_OggSpeex:
248       return TranscoderPreset(type, tr("Ogg Speex"), "spx", "audio/x-speex",
249                               "application/ogg");
250     case Song::Type_OggOpus:
251       return TranscoderPreset(type, tr("Ogg Opus"), "opus", "audio/x-opus",
252                               "application/ogg");
253     case Song::Type_Asf:
254       return TranscoderPreset(type, tr("Windows Media audio"), "wma",
255                               "audio/x-wma", "video/x-ms-asf");
256     case Song::Type_Wav:
257       return TranscoderPreset(type, tr("Wav"), "wav", QString(), "audio/x-wav");
258     default:
259       qLog(Warning) << "Unsupported format in PresetForFileType:" << type;
260       return TranscoderPreset();
261   }
262 }
263 
PickBestFormat(QList<Song::FileType> supported)264 Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
265   if (supported.isEmpty()) return Song::Type_Unknown;
266 
267   QList<Song::FileType> best_formats;
268   best_formats << Song::Type_Mpeg;
269   best_formats << Song::Type_OggVorbis;
270   best_formats << Song::Type_Asf;
271 
272   for (Song::FileType type : best_formats) {
273     if (supported.isEmpty() || supported.contains(type)) return type;
274   }
275 
276   return supported[0];
277 }
278 
AddJob(const QString & input,const TranscoderPreset & preset,const QString & output)279 void Transcoder::AddJob(const QString& input, const TranscoderPreset& preset,
280                         const QString& output) {
281   Job job;
282   job.input = input;
283   job.preset = preset;
284 
285   // Use the supplied filename if there was one, otherwise take the file
286   // extension off the input filename and append the correct one.
287   if (!output.isEmpty())
288     job.output = output;
289   else
290     job.output = input.section('.', 0, -2) + '.' + preset.extension_;
291 
292   // Never overwrite existing files
293   if (QFile::exists(job.output)) {
294     for (int i = 0;; ++i) {
295       QString new_filename =
296           QString("%1.%2.%3").arg(job.output.section('.', 0, -2)).arg(i).arg(
297               preset.extension_);
298       if (!QFile::exists(new_filename)) {
299         job.output = new_filename;
300         break;
301       }
302     }
303   }
304 
305   queued_jobs_ << job;
306 }
307 
AddTemporaryJob(const QString & input,const TranscoderPreset & preset)308 void Transcoder::AddTemporaryJob(const QString &input, const TranscoderPreset &preset) {
309   Job job;
310   job.input = input;
311   job.output = Utilities::GetTemporaryFileName();
312   job.preset = preset;
313 
314   queued_jobs_ << job;
315 }
316 
Start()317 void Transcoder::Start() {
318   emit LogLine(tr("Transcoding %1 files using %2 threads")
319                    .arg(queued_jobs_.count())
320                    .arg(max_threads()));
321 
322   forever {
323     StartJobStatus status = MaybeStartNextJob();
324     if (status == AllThreadsBusy || status == NoMoreJobs) break;
325   }
326 }
327 
MaybeStartNextJob()328 Transcoder::StartJobStatus Transcoder::MaybeStartNextJob() {
329   if (current_jobs_.count() >= max_threads()) return AllThreadsBusy;
330   if (queued_jobs_.isEmpty()) {
331     if (current_jobs_.isEmpty()) {
332       emit AllJobsComplete();
333     }
334 
335     return NoMoreJobs;
336   }
337 
338   Job job = queued_jobs_.takeFirst();
339   if (StartJob(job)) {
340     return StartedSuccessfully;
341   }
342 
343   emit JobComplete(job.input, job.output, false);
344   return FailedToStart;
345 }
346 
NewPadCallback(GstElement *,GstPad * pad,gpointer data)347 void Transcoder::NewPadCallback(GstElement*, GstPad* pad,
348                                 gpointer data) {
349   JobState* state = reinterpret_cast<JobState*>(data);
350   GstPad* const audiopad =
351       gst_element_get_static_pad(state->convert_element_, "sink");
352 
353   if (GST_PAD_IS_LINKED(audiopad)) {
354     qLog(Debug) << "audiopad is already linked, unlinking old pad";
355     gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
356   }
357 
358   gst_pad_link(pad, audiopad);
359   gst_object_unref(audiopad);
360 }
361 
BusCallbackSync(GstBus *,GstMessage * msg,gpointer data)362 GstBusSyncReply Transcoder::BusCallbackSync(GstBus*, GstMessage* msg,
363                                             gpointer data) {
364   JobState* state = reinterpret_cast<JobState*>(data);
365   switch (GST_MESSAGE_TYPE(msg)) {
366     case GST_MESSAGE_EOS:
367       state->PostFinished(true);
368       break;
369 
370     case GST_MESSAGE_ERROR:
371       state->ReportError(msg);
372       state->PostFinished(false);
373       break;
374 
375     default:
376       break;
377   }
378   return GST_BUS_PASS;
379 }
380 
ReportError(GstMessage * msg)381 void Transcoder::JobState::ReportError(GstMessage* msg) {
382   GError* error;
383   gchar* debugs;
384 
385   gst_message_parse_error(msg, &error, &debugs);
386   QString message = QString::fromLocal8Bit(error->message);
387 
388   g_error_free(error);
389   free(debugs);
390 
391   emit parent_->LogLine(tr("Error processing %1: %2").arg(
392       QDir::toNativeSeparators(job_.input), message));
393 }
394 
StartJob(const Job & job)395 bool Transcoder::StartJob(const Job& job) {
396   shared_ptr<JobState> state(new JobState(job, this));
397 
398   emit LogLine(tr("Starting %1").arg(QDir::toNativeSeparators(job.input)));
399 
400   // Create the pipeline.
401   // This should be a scoped_ptr, but scoped_ptr doesn't support custom
402   // destructors.
403   state->pipeline_ = gst_pipeline_new("pipeline");
404   if (!state->pipeline_) return false;
405 
406   // Create all the elements
407   GstElement* src = CreateElement("filesrc", state->pipeline_);
408   GstElement* decode = CreateElement("decodebin", state->pipeline_);
409   GstElement* convert = CreateElement("audioconvert", state->pipeline_);
410   GstElement* resample = CreateElement("audioresample", state->pipeline_);
411   GstElement* codec = CreateElementForMimeType(
412       "Codec/Encoder/Audio", job.preset.codec_mimetype_, state->pipeline_);
413   GstElement* muxer = CreateElementForMimeType(
414       "Codec/Muxer", job.preset.muxer_mimetype_, state->pipeline_);
415   GstElement* sink = CreateElement("filesink", state->pipeline_);
416 
417   if (!src || !decode || !convert || !sink) return false;
418 
419   if (!codec && !job.preset.codec_mimetype_.isEmpty()) {
420     LogLine(tr("Couldn't find an encoder for %1, check you have the correct "
421                "GStreamer plugins installed").arg(job.preset.codec_mimetype_));
422     return false;
423   }
424 
425   if (!muxer && !job.preset.muxer_mimetype_.isEmpty()) {
426     LogLine(tr("Couldn't find a muxer for %1, check you have the correct "
427                "GStreamer plugins installed").arg(job.preset.muxer_mimetype_));
428     return false;
429   }
430 
431   // Join them together
432   gst_element_link(src, decode);
433   if (codec && muxer)
434     gst_element_link_many(convert, resample, codec, muxer, sink, nullptr);
435   else if (codec)
436     gst_element_link_many(convert, resample, codec, sink, nullptr);
437   else if (muxer)
438     gst_element_link_many(convert, resample, muxer, sink, nullptr);
439 
440   // Set properties
441   g_object_set(src, "location", job.input.toUtf8().constData(), nullptr);
442   g_object_set(sink, "location", job.output.toUtf8().constData(), nullptr);
443 
444   // Set callbacks
445   state->convert_element_ = convert;
446 
447   CHECKED_GCONNECT(decode, "pad-added", &NewPadCallback, state.get());
448   gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(state->pipeline_)),
449                            BusCallbackSync, state.get(), nullptr);
450 
451   // Start the pipeline
452   gst_element_set_state(state->pipeline_, GST_STATE_PLAYING);
453 
454   // GStreamer now transcodes in another thread, so we can return now and do
455   // something else.  Keep the JobState object around.  It'll post an event
456   // to our event loop when it finishes.
457   current_jobs_ << state;
458 
459   return true;
460 }
461 
~JobState()462 Transcoder::JobState::~JobState() {
463   if (pipeline_) {
464     gst_element_set_state(pipeline_, GST_STATE_NULL);
465     gst_object_unref(pipeline_);
466   }
467 }
468 
event(QEvent * e)469 bool Transcoder::event(QEvent* e) {
470   if (e->type() == JobFinishedEvent::sEventType) {
471     JobFinishedEvent* finished_event = static_cast<JobFinishedEvent*>(e);
472 
473     // Find this job in the list
474     JobStateList::iterator it = current_jobs_.begin();
475     while (it != current_jobs_.end()) {
476       if (it->get() == finished_event->state_) break;
477       ++it;
478     }
479     if (it == current_jobs_.end()) {
480       // Couldn't find it, maybe GStreamer gave us an event after we'd destroyed
481       // the pipeline?
482       return true;
483     }
484 
485     QString input = (*it)->job_.input;
486     QString output = (*it)->job_.output;
487 
488     // Remove event handlers from the gstreamer pipeline so they don't get
489     // called after the pipeline is shutting down
490     gst_bus_set_sync_handler(
491         gst_pipeline_get_bus(GST_PIPELINE(finished_event->state_->pipeline_)),
492         nullptr, nullptr, nullptr);
493 
494     // Remove it from the list - this will also destroy the GStreamer pipeline
495     current_jobs_.erase(it);
496 
497     // Emit the finished signal
498     emit JobComplete(input, output, finished_event->success_);
499 
500     // Start some more jobs
501     MaybeStartNextJob();
502 
503     return true;
504   }
505 
506   return QObject::event(e);
507 }
508 
Cancel()509 void Transcoder::Cancel() {
510   // Remove all pending jobs
511   queued_jobs_.clear();
512 
513   // Stop the running ones
514   JobStateList::iterator it = current_jobs_.begin();
515   while (it != current_jobs_.end()) {
516     shared_ptr<JobState> state(*it);
517 
518     // Remove event handlers from the gstreamer pipeline so they don't get
519     // called after the pipeline is shutting down
520     gst_bus_set_sync_handler(gst_pipeline_get_bus(
521         GST_PIPELINE(state->pipeline_)), nullptr, nullptr, nullptr);
522 
523     // Stop the pipeline
524     if (gst_element_set_state(state->pipeline_, GST_STATE_NULL) ==
525         GST_STATE_CHANGE_ASYNC) {
526       // Wait for it to finish stopping...
527       gst_element_get_state(state->pipeline_, nullptr, nullptr,
528                             GST_CLOCK_TIME_NONE);
529     }
530 
531     // Remove the job, this destroys the GStreamer pipeline too
532     it = current_jobs_.erase(it);
533   }
534 }
535 
GetProgress() const536 QMap<QString, float> Transcoder::GetProgress() const {
537   QMap<QString, float> ret;
538 
539   for (const auto& state : current_jobs_) {
540     if (!state->pipeline_) continue;
541 
542     gint64 position = 0;
543     gint64 duration = 0;
544 
545     gst_element_query_position(state->pipeline_, GST_FORMAT_TIME, &position);
546     gst_element_query_duration(state->pipeline_, GST_FORMAT_TIME, &duration);
547 
548     ret[state->job_.input] = float(position) / duration;
549   }
550 
551   return ret;
552 }
553 
SetElementProperties(const QString & name,GObject * object)554 void Transcoder::SetElementProperties(const QString& name, GObject* object) {
555   QSettings s;
556   s.beginGroup("Transcoder/" + name + settings_postfix_);
557 
558   guint properties_count = 0;
559   GParamSpec** properties = g_object_class_list_properties(
560       G_OBJECT_GET_CLASS(object), &properties_count);
561 
562   for (int i = 0; i < properties_count; ++i) {
563     GParamSpec* property = properties[i];
564 
565     const QVariant value = s.value(property->name);
566     if (value.isNull()) continue;
567 
568     LogLine(QString("Setting %1 property: %2 = %3")
569                 .arg(name, property->name, value.toString()));
570 
571     switch (property->value_type) {
572       case G_TYPE_DOUBLE:
573         g_object_set(object, property->name, value.toDouble(), nullptr);
574         break;
575       case G_TYPE_FLOAT:
576         g_object_set(object, property->name, value.toFloat(), nullptr);
577         break;
578       case G_TYPE_BOOLEAN:
579         g_object_set(object, property->name, value.toInt(), nullptr);
580         break;
581       case G_TYPE_INT:
582       default:
583         g_object_set(object, property->name, value.toInt(), nullptr);
584         break;
585     }
586   }
587 
588   g_free(properties);
589 }
590