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