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