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