2010-05-03 20:52:35 +02:00
|
|
|
/* This file is part of Clementine.
|
2010-11-20 14:27:10 +01:00
|
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
2010-05-03 20:52:35 +02:00
|
|
|
|
|
|
|
Clementine 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 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "transcoder.h"
|
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
#include <QCoreApplication>
|
2010-08-30 14:43:49 +02:00
|
|
|
#include <QDir>
|
|
|
|
#include <QFile>
|
2011-04-17 01:04:15 +02:00
|
|
|
#include <QSettings>
|
2010-08-30 14:43:49 +02:00
|
|
|
#include <QThread>
|
|
|
|
#include <QtDebug>
|
2020-09-18 16:15:19 +02:00
|
|
|
#include <algorithm>
|
|
|
|
#include <memory>
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2012-06-08 15:34:00 +02:00
|
|
|
#include "core/logging.h"
|
|
|
|
#include "core/signalchecker.h"
|
2014-11-13 22:31:49 +01:00
|
|
|
#include "core/utilities.h"
|
2012-06-08 15:34:00 +02:00
|
|
|
|
2014-02-06 14:48:00 +01:00
|
|
|
using std::shared_ptr;
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2021-05-16 14:03:36 +02:00
|
|
|
static QString UrlToLocalFileIfPossible(QUrl url) {
|
|
|
|
if (url.isLocalFile()) return url.toLocalFile();
|
|
|
|
return url.toString();
|
|
|
|
}
|
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
int Transcoder::JobFinishedEvent::sEventType = -1;
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
TranscoderPreset::TranscoderPreset(Song::FileType type, const QString& name,
|
|
|
|
const QString& extension,
|
|
|
|
const QString& codec_mimetype,
|
|
|
|
const QString& muxer_mimetype)
|
|
|
|
: type_(type),
|
|
|
|
name_(name),
|
|
|
|
extension_(extension),
|
|
|
|
codec_mimetype_(codec_mimetype),
|
|
|
|
muxer_mimetype_(muxer_mimetype) {}
|
|
|
|
|
|
|
|
GstElement* Transcoder::CreateElement(const QString& factory_name,
|
|
|
|
GstElement* bin, const QString& name) {
|
2010-05-03 20:52:35 +02:00
|
|
|
GstElement* ret = gst_element_factory_make(
|
2015-04-11 22:52:31 +02:00
|
|
|
factory_name.toLatin1().constData(),
|
|
|
|
name.isNull() ? factory_name.toLatin1().constData()
|
|
|
|
: name.toLatin1().constData());
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (ret && bin) gst_bin_add(GST_BIN(bin), ret);
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2010-05-08 17:36:12 +02:00
|
|
|
if (!ret) {
|
|
|
|
emit LogLine(
|
|
|
|
tr("Could not create the GStreamer element \"%1\" -"
|
|
|
|
" make sure you have all the required GStreamer plugins installed")
|
2014-02-07 16:34:20 +01:00
|
|
|
.arg(factory_name));
|
2011-04-17 01:04:15 +02:00
|
|
|
} else {
|
|
|
|
SetElementProperties(factory_name, G_OBJECT(ret));
|
2010-05-08 17:36:12 +02:00
|
|
|
}
|
|
|
|
|
2010-05-03 20:52:35 +02:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2010-08-22 02:27:14 +02:00
|
|
|
struct SuitableElement {
|
|
|
|
SuitableElement(const QString& name = QString(), int rank = 0)
|
2014-02-07 16:34:20 +01:00
|
|
|
: name_(name), rank_(rank) {}
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
bool operator<(const SuitableElement& other) const {
|
|
|
|
return rank_ < other.rank_;
|
|
|
|
}
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2010-08-22 02:27:14 +02:00
|
|
|
QString name_;
|
|
|
|
int rank_;
|
|
|
|
};
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2021-01-31 07:05:21 +01:00
|
|
|
SuitableElement Transcoder::FindBestElementForMimeType(
|
|
|
|
const QString& element_type, const QString& mime_type) {
|
|
|
|
if (element_type.isEmpty() || mime_type.isEmpty()) return SuitableElement();
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2016-01-12 15:59:23 +01:00
|
|
|
// HACK: Force mp4mux because it doesn't set any useful src caps
|
2010-08-22 02:27:14 +02:00
|
|
|
if (mime_type == "audio/mp4") {
|
2021-01-31 07:05:21 +01:00
|
|
|
return SuitableElement("mp4mux", -1);
|
2010-05-03 20:52:35 +02:00
|
|
|
}
|
|
|
|
|
2010-08-22 02:27:14 +02:00
|
|
|
// Keep track of all the suitable elements we find and figure out which
|
|
|
|
// is the best at the end.
|
|
|
|
QList<SuitableElement> suitable_elements_;
|
|
|
|
|
|
|
|
// The caps we're trying to find
|
|
|
|
GstCaps* target_caps = gst_caps_from_string(mime_type.toUtf8().constData());
|
|
|
|
|
2013-09-25 15:42:13 +02:00
|
|
|
GstRegistry* registry = gst_registry_get();
|
2010-08-22 02:27:14 +02:00
|
|
|
GList* const features =
|
|
|
|
gst_registry_get_feature_list(registry, GST_TYPE_ELEMENT_FACTORY);
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
for (GList* p = features; p; p = g_list_next(p)) {
|
2010-08-22 02:27:14 +02:00
|
|
|
GstElementFactory* factory = GST_ELEMENT_FACTORY(p->data);
|
|
|
|
|
|
|
|
// Is this the right type of plugin?
|
2020-09-18 16:15:19 +02:00
|
|
|
if (QString(gst_element_factory_get_klass(factory))
|
|
|
|
.contains(element_type)) {
|
2010-08-22 02:27:14 +02:00
|
|
|
const GList* const templates =
|
|
|
|
gst_element_factory_get_static_pad_templates(factory);
|
2014-02-07 16:34:20 +01:00
|
|
|
for (const GList* p = templates; p; p = g_list_next(p)) {
|
2010-08-22 02:27:14 +02:00
|
|
|
// Only interested in source pads
|
2014-02-07 16:34:20 +01:00
|
|
|
GstStaticPadTemplate* pad_template =
|
|
|
|
reinterpret_cast<GstStaticPadTemplate*>(p->data);
|
|
|
|
if (pad_template->direction != GST_PAD_SRC) continue;
|
2010-08-22 02:27:14 +02:00
|
|
|
|
|
|
|
// Does this pad support the mime type we want?
|
|
|
|
GstCaps* caps = gst_static_pad_template_get_caps(pad_template);
|
|
|
|
GstCaps* intersection = gst_caps_intersect(caps, target_caps);
|
|
|
|
|
|
|
|
if (intersection) {
|
|
|
|
if (!gst_caps_is_empty(intersection)) {
|
|
|
|
int rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory));
|
2013-09-25 15:42:13 +02:00
|
|
|
QString name = GST_OBJECT_NAME(factory);
|
2010-08-22 02:27:14 +02:00
|
|
|
|
|
|
|
if (name.startsWith("ffmux") || name.startsWith("ffenc"))
|
2014-02-07 16:34:20 +01:00
|
|
|
rank = -1; // ffmpeg usually sucks
|
2010-08-22 02:27:14 +02:00
|
|
|
|
|
|
|
suitable_elements_ << SuitableElement(name, rank);
|
|
|
|
}
|
|
|
|
gst_caps_unref(intersection);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
gst_plugin_feature_list_free(features);
|
|
|
|
gst_caps_unref(target_caps);
|
|
|
|
|
2021-01-31 07:05:21 +01:00
|
|
|
if (suitable_elements_.isEmpty()) return SuitableElement();
|
2010-08-22 02:27:14 +02:00
|
|
|
|
|
|
|
// Sort by rank
|
2018-10-05 17:19:05 +02:00
|
|
|
std::sort(suitable_elements_.begin(), suitable_elements_.end());
|
2021-01-31 07:05:21 +01:00
|
|
|
return suitable_elements_.last();
|
|
|
|
}
|
|
|
|
|
|
|
|
GstElement* Transcoder::CreateElementForMimeType(const QString& element_type,
|
|
|
|
const QString& mime_type,
|
|
|
|
GstElement* bin) {
|
|
|
|
SuitableElement best = FindBestElementForMimeType(element_type, mime_type);
|
|
|
|
if (best.name_.isEmpty()) {
|
2021-07-14 00:57:48 +02:00
|
|
|
emit LogLine(tr("Suitable element not found"));
|
2021-01-31 07:05:21 +01:00
|
|
|
return nullptr;
|
|
|
|
}
|
2010-08-22 02:27:14 +02:00
|
|
|
|
2021-07-14 00:57:48 +02:00
|
|
|
emit LogLine(QString("Using '%1' (rank %2)").arg(best.name_).arg(best.rank_));
|
2010-11-21 00:46:30 +01:00
|
|
|
|
|
|
|
if (best.name_ == "lamemp3enc") {
|
|
|
|
// Special case: we need to add xingmux and id3v2mux to the pipeline when
|
2014-02-07 16:34:20 +01:00
|
|
|
// using lamemp3enc because it doesn't write the VBR or ID3v2 headers
|
|
|
|
// itself.
|
2010-11-21 13:03:22 +01:00
|
|
|
|
2021-07-14 00:57:48 +02:00
|
|
|
emit LogLine("Adding xingmux and id3v2mux to the pipeline");
|
2010-11-21 00:46:30 +01:00
|
|
|
|
2010-11-21 13:03:22 +01:00
|
|
|
// Create the bin
|
2010-11-21 00:46:30 +01:00
|
|
|
GstElement* mp3bin = gst_bin_new("mp3bin");
|
|
|
|
gst_bin_add(GST_BIN(bin), mp3bin);
|
|
|
|
|
2010-11-21 13:03:22 +01:00
|
|
|
// Create the elements
|
2014-02-07 16:34:20 +01:00
|
|
|
GstElement* lame = CreateElement("lamemp3enc", mp3bin);
|
|
|
|
GstElement* xing = CreateElement("xingmux", mp3bin);
|
2010-11-21 00:46:30 +01:00
|
|
|
GstElement* id3v2 = CreateElement("id3v2mux", mp3bin);
|
|
|
|
|
2010-11-21 13:03:22 +01:00
|
|
|
if (!lame || !xing || !id3v2) {
|
2014-02-06 16:49:49 +01:00
|
|
|
return nullptr;
|
2010-11-21 13:03:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Link the elements together
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_element_link_many(lame, xing, id3v2, nullptr);
|
2010-11-21 00:46:30 +01:00
|
|
|
|
2010-11-21 13:03:22 +01:00
|
|
|
// Link the bin's ghost pads to the elements on each end
|
2010-11-21 00:46:30 +01:00
|
|
|
GstPad* pad = gst_element_get_static_pad(lame, "sink");
|
|
|
|
gst_element_add_pad(mp3bin, gst_ghost_pad_new("sink", pad));
|
|
|
|
gst_object_unref(GST_OBJECT(pad));
|
|
|
|
|
|
|
|
pad = gst_element_get_static_pad(id3v2, "src");
|
|
|
|
gst_element_add_pad(mp3bin, gst_ghost_pad_new("src", pad));
|
|
|
|
gst_object_unref(GST_OBJECT(pad));
|
|
|
|
|
|
|
|
return mp3bin;
|
|
|
|
} else {
|
|
|
|
return CreateElement(best.name_, bin);
|
|
|
|
}
|
2010-05-03 20:52:35 +02:00
|
|
|
}
|
|
|
|
|
2021-01-31 09:10:17 +01:00
|
|
|
QString Transcoder::GetEncoderFactoryForMimeType(const QString& mime_type) {
|
|
|
|
SuitableElement best =
|
|
|
|
FindBestElementForMimeType("Codec/Encoder/Audio", mime_type);
|
|
|
|
return best.name_;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
Transcoder::JobFinishedEvent::JobFinishedEvent(JobState* state, bool success)
|
|
|
|
: QEvent(QEvent::Type(sEventType)), state_(state), success_(success) {}
|
2010-05-06 18:28:19 +02:00
|
|
|
|
|
|
|
void Transcoder::JobState::PostFinished(bool success) {
|
2010-05-08 17:36:12 +02:00
|
|
|
if (success) {
|
2014-02-07 16:34:20 +01:00
|
|
|
emit parent_->LogLine(tr("Successfully written %1")
|
|
|
|
.arg(QDir::toNativeSeparators(job_.output)));
|
2010-05-08 17:36:12 +02:00
|
|
|
}
|
2010-08-30 15:03:21 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
QCoreApplication::postEvent(parent_,
|
|
|
|
new Transcoder::JobFinishedEvent(this, success));
|
2010-05-06 18:28:19 +02:00
|
|
|
}
|
|
|
|
|
2021-01-24 09:18:03 +01:00
|
|
|
QString Transcoder::JobState::GetDisplayName() {
|
2021-05-16 14:03:36 +02:00
|
|
|
return job_.input.fileName() + " => " + QFileInfo(job_.output).fileName();
|
2021-01-24 09:18:03 +01:00
|
|
|
}
|
|
|
|
|
2014-11-13 22:31:49 +01:00
|
|
|
Transcoder::Transcoder(QObject* parent, const QString& settings_postfix)
|
|
|
|
: QObject(parent),
|
|
|
|
max_threads_(QThread::idealThreadCount()),
|
2021-01-24 09:18:03 +01:00
|
|
|
settings_postfix_(settings_postfix),
|
|
|
|
model_(new GstPipelineModel(this)) {
|
2010-05-06 18:28:19 +02:00
|
|
|
if (JobFinishedEvent::sEventType == -1)
|
|
|
|
JobFinishedEvent::sEventType = QEvent::registerEventType();
|
2011-04-17 01:04:15 +02:00
|
|
|
|
|
|
|
// Initialise some settings for the lamemp3enc element.
|
|
|
|
QSettings s;
|
2014-11-13 22:31:49 +01:00
|
|
|
s.beginGroup("Transcoder/lamemp3enc" + settings_postfix_);
|
2011-04-17 01:04:15 +02:00
|
|
|
|
|
|
|
if (s.value("target").isNull()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
s.setValue("target", 1); // 1 == bitrate
|
2011-04-17 01:04:15 +02:00
|
|
|
}
|
|
|
|
if (s.value("cbr").isNull()) {
|
|
|
|
s.setValue("cbr", true);
|
|
|
|
}
|
2010-08-29 17:32:36 +02:00
|
|
|
}
|
2010-05-06 18:28:19 +02:00
|
|
|
|
2010-08-29 17:32:36 +02:00
|
|
|
QList<TranscoderPreset> Transcoder::GetAllPresets() {
|
|
|
|
QList<TranscoderPreset> ret;
|
|
|
|
ret << PresetForFileType(Song::Type_Flac);
|
|
|
|
ret << PresetForFileType(Song::Type_Mp4);
|
|
|
|
ret << PresetForFileType(Song::Type_Mpeg);
|
|
|
|
ret << PresetForFileType(Song::Type_OggVorbis);
|
|
|
|
ret << PresetForFileType(Song::Type_OggFlac);
|
|
|
|
ret << PresetForFileType(Song::Type_OggSpeex);
|
|
|
|
ret << PresetForFileType(Song::Type_Asf);
|
|
|
|
ret << PresetForFileType(Song::Type_Wav);
|
2013-01-28 14:08:34 +01:00
|
|
|
ret << PresetForFileType(Song::Type_OggOpus);
|
2010-08-29 17:32:36 +02:00
|
|
|
return ret;
|
2010-05-03 20:52:35 +02:00
|
|
|
}
|
|
|
|
|
2021-02-05 08:31:24 +01:00
|
|
|
QString Transcoder::MimeType(CodecType type) {
|
|
|
|
switch (type) {
|
|
|
|
case Codec_None:
|
|
|
|
return QString();
|
|
|
|
case Codec_Flac:
|
|
|
|
return "audio/x-flac";
|
|
|
|
case Codec_Mp4:
|
|
|
|
return "audio/mpeg, mpegversion=(int)4";
|
|
|
|
case Codec_Mp3:
|
|
|
|
return "audio/mpeg, mpegversion=(int)1, layer=(int)3";
|
|
|
|
case Codec_Vorbis:
|
|
|
|
return "audio/x-vorbis";
|
|
|
|
case Codec_Speex:
|
|
|
|
return "audio/x-speex";
|
|
|
|
case Codec_Opus:
|
|
|
|
return "audio/x-opus";
|
|
|
|
case Codec_Wma:
|
|
|
|
return "audio/x-wma";
|
|
|
|
}
|
|
|
|
return "unknown";
|
|
|
|
}
|
|
|
|
|
2010-08-29 16:25:33 +02:00
|
|
|
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
|
|
|
|
switch (type) {
|
|
|
|
case Song::Type_Flac:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("FLAC"), "flac", MimeType(Codec_Flac));
|
2010-08-29 16:25:33 +02:00
|
|
|
case Song::Type_Mp4:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("M4A AAC"), "mp4", MimeType(Codec_Mp4),
|
|
|
|
"audio/mp4");
|
2010-08-29 16:25:33 +02:00
|
|
|
case Song::Type_Mpeg:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("MP3"), "mp3", MimeType(Codec_Mp3));
|
2010-08-29 16:25:33 +02:00
|
|
|
case Song::Type_OggVorbis:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("Ogg Vorbis"), "ogg",
|
|
|
|
MimeType(Codec_Vorbis), "application/ogg");
|
2010-08-29 16:25:33 +02:00
|
|
|
case Song::Type_OggFlac:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("Ogg Flac"), "ogg", MimeType(Codec_Flac),
|
2014-02-07 16:34:20 +01:00
|
|
|
"application/ogg");
|
2010-08-29 16:25:33 +02:00
|
|
|
case Song::Type_OggSpeex:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("Ogg Speex"), "spx",
|
|
|
|
MimeType(Codec_Speex), "application/ogg");
|
2013-01-28 14:08:34 +01:00
|
|
|
case Song::Type_OggOpus:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("Ogg Opus"), "opus",
|
|
|
|
MimeType(Codec_Opus), "application/ogg");
|
2010-08-29 16:25:33 +02:00
|
|
|
case Song::Type_Asf:
|
2014-02-07 16:34:20 +01:00
|
|
|
return TranscoderPreset(type, tr("Windows Media audio"), "wma",
|
2021-02-05 08:31:24 +01:00
|
|
|
MimeType(Codec_Wma), "video/x-ms-asf");
|
2010-08-29 16:25:33 +02:00
|
|
|
case Song::Type_Wav:
|
2021-02-05 08:31:24 +01:00
|
|
|
return TranscoderPreset(type, tr("Wav"), "wav", MimeType(Codec_None),
|
|
|
|
"audio/x-wav");
|
2010-08-29 16:25:33 +02:00
|
|
|
default:
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Warning) << "Unsupported format in PresetForFileType:" << type;
|
2010-08-29 16:25:33 +02:00
|
|
|
return TranscoderPreset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-08-29 21:22:21 +02:00
|
|
|
Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (supported.isEmpty()) return Song::Type_Unknown;
|
2010-08-29 21:22:21 +02:00
|
|
|
|
|
|
|
QList<Song::FileType> best_formats;
|
|
|
|
best_formats << Song::Type_Mpeg;
|
|
|
|
best_formats << Song::Type_OggVorbis;
|
|
|
|
best_formats << Song::Type_Asf;
|
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (Song::FileType type : best_formats) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (supported.isEmpty() || supported.contains(type)) return type;
|
2010-08-29 21:22:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return supported[0];
|
|
|
|
}
|
|
|
|
|
2021-05-16 14:03:36 +02:00
|
|
|
void Transcoder::AddJob(const QUrl& input, const TranscoderPreset& preset,
|
2021-08-22 13:35:35 +02:00
|
|
|
const QString& output, bool overwrite_existing) {
|
2010-05-03 20:52:35 +02:00
|
|
|
Job job;
|
|
|
|
job.input = input;
|
2010-08-22 02:27:14 +02:00
|
|
|
job.preset = preset;
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2010-05-03 23:24:43 +02:00
|
|
|
// Use the supplied filename if there was one, otherwise take the file
|
|
|
|
// extension off the input filename and append the correct one.
|
2010-05-03 20:52:35 +02:00
|
|
|
if (!output.isEmpty())
|
|
|
|
job.output = output;
|
|
|
|
else
|
2021-05-16 14:03:36 +02:00
|
|
|
job.output = UrlToLocalFileIfPossible(input).section('.', 0, -2) + '.' +
|
|
|
|
preset.extension_;
|
2010-05-03 20:52:35 +02:00
|
|
|
|
2021-08-22 13:35:35 +02:00
|
|
|
// Don't overwrite existing files if overwrite_existing is not set
|
|
|
|
if (!overwrite_existing && QFile::exists(job.output)) {
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0;; ++i) {
|
2020-09-18 16:15:19 +02:00
|
|
|
QString new_filename = QString("%1.%2.%3")
|
|
|
|
.arg(job.output.section('.', 0, -2))
|
|
|
|
.arg(i)
|
|
|
|
.arg(preset.extension_);
|
2010-05-03 23:24:43 +02:00
|
|
|
if (!QFile::exists(new_filename)) {
|
|
|
|
job.output = new_filename;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
queued_jobs_ << job;
|
2010-05-03 20:52:35 +02:00
|
|
|
}
|
|
|
|
|
2021-05-16 14:03:36 +02:00
|
|
|
void Transcoder::AddTemporaryJob(const QUrl& input,
|
2020-09-18 16:15:19 +02:00
|
|
|
const TranscoderPreset& preset) {
|
2021-05-16 14:03:36 +02:00
|
|
|
AddJob(input, preset, Utilities::GetTemporaryFileName());
|
2014-11-13 22:31:49 +01:00
|
|
|
}
|
|
|
|
|
2010-05-03 20:52:35 +02:00
|
|
|
void Transcoder::Start() {
|
2010-05-08 17:36:12 +02:00
|
|
|
emit LogLine(tr("Transcoding %1 files using %2 threads")
|
2014-02-07 16:34:20 +01:00
|
|
|
.arg(queued_jobs_.count())
|
|
|
|
.arg(max_threads()));
|
2010-05-08 17:36:12 +02:00
|
|
|
|
2021-01-28 07:49:08 +01:00
|
|
|
// Kick off worker threads.
|
2010-05-06 18:28:19 +02:00
|
|
|
forever {
|
|
|
|
StartJobStatus status = MaybeStartNextJob();
|
2014-02-07 16:34:20 +01:00
|
|
|
if (status == AllThreadsBusy || status == NoMoreJobs) break;
|
2010-05-06 18:28:19 +02:00
|
|
|
}
|
2010-05-03 20:52:35 +02:00
|
|
|
}
|
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
Transcoder::StartJobStatus Transcoder::MaybeStartNextJob() {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (current_jobs_.count() >= max_threads()) return AllThreadsBusy;
|
2010-05-06 18:28:19 +02:00
|
|
|
if (queued_jobs_.isEmpty()) {
|
2010-05-08 17:36:12 +02:00
|
|
|
if (current_jobs_.isEmpty()) {
|
2010-05-06 18:28:19 +02:00
|
|
|
emit AllJobsComplete();
|
2010-05-08 17:36:12 +02:00
|
|
|
}
|
2010-05-06 18:28:19 +02:00
|
|
|
|
|
|
|
return NoMoreJobs;
|
|
|
|
}
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
Job job = queued_jobs_.takeFirst();
|
2014-01-11 05:47:49 +01:00
|
|
|
if (StartJob(job)) {
|
2010-08-22 02:27:14 +02:00
|
|
|
return StartedSuccessfully;
|
2014-01-11 05:47:49 +01:00
|
|
|
}
|
2010-08-22 02:27:14 +02:00
|
|
|
|
2014-11-13 22:31:49 +01:00
|
|
|
emit JobComplete(job.input, job.output, false);
|
2010-08-22 02:27:14 +02:00
|
|
|
return FailedToStart;
|
2010-05-03 21:56:21 +02:00
|
|
|
}
|
|
|
|
|
2020-09-18 16:15:19 +02:00
|
|
|
void Transcoder::NewPadCallback(GstElement*, GstPad* pad, gpointer data) {
|
2010-05-03 21:56:21 +02:00
|
|
|
JobState* state = reinterpret_cast<JobState*>(data);
|
2014-02-07 16:34:20 +01:00
|
|
|
GstPad* const audiopad =
|
|
|
|
gst_element_get_static_pad(state->convert_element_, "sink");
|
2010-05-03 21:56:21 +02:00
|
|
|
|
|
|
|
if (GST_PAD_IS_LINKED(audiopad)) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "audiopad is already linked, unlinking old pad";
|
2010-05-03 21:56:21 +02:00
|
|
|
gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
|
|
|
|
}
|
|
|
|
|
|
|
|
gst_pad_link(pad, audiopad);
|
|
|
|
gst_object_unref(audiopad);
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
GstBusSyncReply Transcoder::BusCallbackSync(GstBus*, GstMessage* msg,
|
|
|
|
gpointer data) {
|
2010-05-03 21:56:21 +02:00
|
|
|
JobState* state = reinterpret_cast<JobState*>(data);
|
|
|
|
switch (GST_MESSAGE_TYPE(msg)) {
|
|
|
|
case GST_MESSAGE_EOS:
|
2010-05-06 18:28:19 +02:00
|
|
|
state->PostFinished(true);
|
2010-05-03 21:56:21 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case GST_MESSAGE_ERROR:
|
2010-05-08 17:36:12 +02:00
|
|
|
state->ReportError(msg);
|
2010-05-06 18:28:19 +02:00
|
|
|
state->PostFinished(false);
|
2010-05-03 21:56:21 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return GST_BUS_PASS;
|
|
|
|
}
|
|
|
|
|
2010-05-08 17:36:12 +02:00
|
|
|
void Transcoder::JobState::ReportError(GstMessage* msg) {
|
|
|
|
GError* error;
|
|
|
|
gchar* debugs;
|
|
|
|
|
|
|
|
gst_message_parse_error(msg, &error, &debugs);
|
|
|
|
QString message = QString::fromLocal8Bit(error->message);
|
|
|
|
|
|
|
|
g_error_free(error);
|
|
|
|
free(debugs);
|
|
|
|
|
2021-10-30 12:44:45 +02:00
|
|
|
// clean up output file if it was already created
|
|
|
|
if (QFile::exists(job_.output)) {
|
|
|
|
QFile::remove(job_.output);
|
|
|
|
}
|
|
|
|
|
2020-09-18 16:15:19 +02:00
|
|
|
emit parent_->LogLine(
|
|
|
|
tr("Error processing %1: %2")
|
2021-05-16 14:03:36 +02:00
|
|
|
.arg(UrlToLocalFileIfPossible(job_.input), message));
|
2010-05-08 17:36:12 +02:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
bool Transcoder::StartJob(const Job& job) {
|
2010-05-06 18:28:19 +02:00
|
|
|
shared_ptr<JobState> state(new JobState(job, this));
|
|
|
|
|
2021-05-16 14:03:36 +02:00
|
|
|
emit LogLine(tr("Starting %1").arg(UrlToLocalFileIfPossible(job.input)));
|
2010-05-08 17:36:12 +02:00
|
|
|
|
2010-05-03 22:10:55 +02:00
|
|
|
// Create the pipeline.
|
|
|
|
// This should be a scoped_ptr, but scoped_ptr doesn't support custom
|
|
|
|
// destructors.
|
2021-01-13 07:49:02 +01:00
|
|
|
if (!state->Init()) return false;
|
2010-05-03 21:56:21 +02:00
|
|
|
|
|
|
|
// Create all the elements
|
2021-05-16 14:03:36 +02:00
|
|
|
GstElement* decode = CreateElement("uridecodebin", state->Pipeline());
|
2021-01-07 07:50:08 +01:00
|
|
|
GstElement* convert = CreateElement("audioconvert", state->Pipeline());
|
|
|
|
GstElement* resample = CreateElement("audioresample", state->Pipeline());
|
2014-02-07 16:34:20 +01:00
|
|
|
GstElement* codec = CreateElementForMimeType(
|
2021-01-07 07:50:08 +01:00
|
|
|
"Codec/Encoder/Audio", job.preset.codec_mimetype_, state->Pipeline());
|
2014-02-07 16:34:20 +01:00
|
|
|
GstElement* muxer = CreateElementForMimeType(
|
2021-01-07 07:50:08 +01:00
|
|
|
"Codec/Muxer", job.preset.muxer_mimetype_, state->Pipeline());
|
|
|
|
GstElement* sink = CreateElement("filesink", state->Pipeline());
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2021-05-16 14:03:36 +02:00
|
|
|
if (!decode || !convert || !sink) return false;
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2010-08-29 16:25:33 +02:00
|
|
|
if (!codec && !job.preset.codec_mimetype_.isEmpty()) {
|
2021-07-14 00:57:48 +02:00
|
|
|
emit LogLine(
|
|
|
|
tr("Couldn't find an encoder for %1, check you have the correct "
|
|
|
|
"GStreamer plugins installed")
|
|
|
|
.arg(job.preset.codec_mimetype_));
|
2010-08-22 02:27:14 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!muxer && !job.preset.muxer_mimetype_.isEmpty()) {
|
2021-07-14 00:57:48 +02:00
|
|
|
emit LogLine(tr("Couldn't find a muxer for %1, check you have the correct "
|
|
|
|
"GStreamer plugins installed")
|
|
|
|
.arg(job.preset.muxer_mimetype_));
|
2010-08-22 02:27:14 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2010-05-03 21:56:21 +02:00
|
|
|
// Join them together
|
2010-08-29 16:25:33 +02:00
|
|
|
if (codec && muxer)
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_element_link_many(convert, resample, codec, muxer, sink, nullptr);
|
2010-08-29 16:25:33 +02:00
|
|
|
else if (codec)
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_element_link_many(convert, resample, codec, sink, nullptr);
|
2010-08-29 16:25:33 +02:00
|
|
|
else if (muxer)
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_element_link_many(convert, resample, muxer, sink, nullptr);
|
2010-05-03 21:56:21 +02:00
|
|
|
|
|
|
|
// Set properties
|
2021-05-16 14:03:36 +02:00
|
|
|
g_object_set(decode, "uri", job.input.toString().toUtf8().constData(),
|
|
|
|
nullptr);
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(sink, "location", job.output.toUtf8().constData(), nullptr);
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2021-08-20 20:34:51 +02:00
|
|
|
// Create target directory, if it does not exist
|
|
|
|
QFileInfo output_file_path(job.output);
|
|
|
|
output_file_path.dir().mkpath(".");
|
|
|
|
|
2010-05-03 21:56:21 +02:00
|
|
|
// Set callbacks
|
2010-05-06 18:28:19 +02:00
|
|
|
state->convert_element_ = convert;
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2014-11-13 22:31:49 +01:00
|
|
|
CHECKED_GCONNECT(decode, "pad-added", &NewPadCallback, state.get());
|
2021-01-07 07:50:08 +01:00
|
|
|
gst_bus_set_sync_handler(
|
|
|
|
gst_pipeline_get_bus(GST_PIPELINE(state->Pipeline())), BusCallbackSync,
|
|
|
|
state.get(), nullptr);
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
// Start the pipeline
|
2021-01-07 07:50:08 +01:00
|
|
|
gst_element_set_state(state->Pipeline(), GST_STATE_PLAYING);
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
// GStreamer now transcodes in another thread, so we can return now and do
|
|
|
|
// something else. Keep the JobState object around. It'll post an event
|
|
|
|
// to our event loop when it finishes.
|
|
|
|
current_jobs_ << state;
|
2021-01-24 09:18:03 +01:00
|
|
|
model_->AddPipeline(state->id(), state->GetDisplayName());
|
2010-05-03 21:56:21 +02:00
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
return true;
|
2010-05-03 20:52:35 +02:00
|
|
|
}
|
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
bool Transcoder::event(QEvent* e) {
|
|
|
|
if (e->type() == JobFinishedEvent::sEventType) {
|
|
|
|
JobFinishedEvent* finished_event = static_cast<JobFinishedEvent*>(e);
|
|
|
|
|
|
|
|
// Find this job in the list
|
|
|
|
JobStateList::iterator it = current_jobs_.begin();
|
|
|
|
while (it != current_jobs_.end()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (it->get() == finished_event->state_) break;
|
2010-05-06 18:28:19 +02:00
|
|
|
++it;
|
|
|
|
}
|
|
|
|
if (it == current_jobs_.end()) {
|
|
|
|
// Couldn't find it, maybe GStreamer gave us an event after we'd destroyed
|
|
|
|
// the pipeline?
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-05-16 14:03:36 +02:00
|
|
|
QUrl input = (*it)->job_.input;
|
2014-11-13 22:31:49 +01:00
|
|
|
QString output = (*it)->job_.output;
|
2010-05-06 18:28:19 +02:00
|
|
|
|
2010-05-08 17:36:12 +02:00
|
|
|
// Remove event handlers from the gstreamer pipeline so they don't get
|
|
|
|
// called after the pipeline is shutting down
|
2014-02-07 16:34:20 +01:00
|
|
|
gst_bus_set_sync_handler(
|
2021-01-07 07:50:08 +01:00
|
|
|
gst_pipeline_get_bus(GST_PIPELINE(finished_event->state_->Pipeline())),
|
2014-06-09 08:20:24 +02:00
|
|
|
nullptr, nullptr, nullptr);
|
2010-05-08 17:36:12 +02:00
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
// Remove it from the list - this will also destroy the GStreamer pipeline
|
2021-01-24 09:18:03 +01:00
|
|
|
model_->RemovePipeline((*it)->id());
|
2010-05-06 18:28:19 +02:00
|
|
|
current_jobs_.erase(it);
|
|
|
|
|
2010-08-30 15:03:21 +02:00
|
|
|
// Emit the finished signal
|
2014-11-13 22:31:49 +01:00
|
|
|
emit JobComplete(input, output, finished_event->success_);
|
2010-08-30 15:03:21 +02:00
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
// Start some more jobs
|
|
|
|
MaybeStartNextJob();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return QObject::event(e);
|
2010-05-03 20:52:35 +02:00
|
|
|
}
|
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
void Transcoder::Cancel() {
|
|
|
|
// Remove all pending jobs
|
|
|
|
queued_jobs_.clear();
|
|
|
|
|
|
|
|
// Stop the running ones
|
|
|
|
JobStateList::iterator it = current_jobs_.begin();
|
|
|
|
while (it != current_jobs_.end()) {
|
|
|
|
shared_ptr<JobState> state(*it);
|
|
|
|
|
2010-05-08 17:36:12 +02:00
|
|
|
// Remove event handlers from the gstreamer pipeline so they don't get
|
|
|
|
// called after the pipeline is shutting down
|
2020-09-18 16:15:19 +02:00
|
|
|
gst_bus_set_sync_handler(
|
2021-01-07 07:50:08 +01:00
|
|
|
gst_pipeline_get_bus(GST_PIPELINE(state->Pipeline())), nullptr, nullptr,
|
2020-09-18 16:15:19 +02:00
|
|
|
nullptr);
|
2010-05-08 17:36:12 +02:00
|
|
|
|
2010-05-06 18:28:19 +02:00
|
|
|
// Stop the pipeline
|
2021-01-07 07:50:08 +01:00
|
|
|
if (gst_element_set_state(state->Pipeline(), GST_STATE_NULL) ==
|
2014-02-07 16:34:20 +01:00
|
|
|
GST_STATE_CHANGE_ASYNC) {
|
2010-05-06 18:28:19 +02:00
|
|
|
// Wait for it to finish stopping...
|
2021-01-07 07:50:08 +01:00
|
|
|
gst_element_get_state(state->Pipeline(), nullptr, nullptr,
|
2014-02-07 16:34:20 +01:00
|
|
|
GST_CLOCK_TIME_NONE);
|
2010-05-06 18:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the job, this destroys the GStreamer pipeline too
|
2021-01-24 09:18:03 +01:00
|
|
|
model_->RemovePipeline((*it)->id());
|
2010-05-06 18:28:19 +02:00
|
|
|
it = current_jobs_.erase(it);
|
|
|
|
}
|
|
|
|
}
|
2010-08-30 13:36:40 +02:00
|
|
|
|
2021-01-27 07:41:56 +01:00
|
|
|
void Transcoder::DumpGraph(int id) {
|
|
|
|
for (JobStateList::iterator it = current_jobs_.begin();
|
|
|
|
it != current_jobs_.end(); it++) {
|
|
|
|
if ((*it)->id() == id) {
|
|
|
|
(*it)->DumpGraph();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-16 14:03:36 +02:00
|
|
|
QMap<QUrl, float> Transcoder::GetProgress() const {
|
|
|
|
QMap<QUrl, float> ret;
|
2010-08-30 13:36:40 +02:00
|
|
|
|
2014-02-06 14:48:00 +01:00
|
|
|
for (const auto& state : current_jobs_) {
|
2021-01-07 07:50:08 +01:00
|
|
|
if (!state->Pipeline()) continue;
|
2010-08-30 13:36:40 +02:00
|
|
|
|
|
|
|
gint64 position = 0;
|
|
|
|
gint64 duration = 0;
|
|
|
|
|
2021-01-07 07:50:08 +01:00
|
|
|
gst_element_query_position(state->Pipeline(), GST_FORMAT_TIME, &position);
|
|
|
|
gst_element_query_duration(state->Pipeline(), GST_FORMAT_TIME, &duration);
|
2010-08-30 13:36:40 +02:00
|
|
|
|
|
|
|
ret[state->job_.input] = float(position) / duration;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2011-04-17 01:04:15 +02:00
|
|
|
|
|
|
|
void Transcoder::SetElementProperties(const QString& name, GObject* object) {
|
|
|
|
QSettings s;
|
2014-11-13 22:31:49 +01:00
|
|
|
s.beginGroup("Transcoder/" + name + settings_postfix_);
|
2011-04-17 01:04:15 +02:00
|
|
|
|
|
|
|
guint properties_count = 0;
|
|
|
|
GParamSpec** properties = g_object_class_list_properties(
|
2014-02-07 16:34:20 +01:00
|
|
|
G_OBJECT_GET_CLASS(object), &properties_count);
|
2011-04-17 01:04:15 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0; i < properties_count; ++i) {
|
2011-04-17 01:04:15 +02:00
|
|
|
GParamSpec* property = properties[i];
|
|
|
|
|
|
|
|
const QVariant value = s.value(property->name);
|
2014-02-07 16:34:20 +01:00
|
|
|
if (value.isNull()) continue;
|
2011-04-17 01:04:15 +02:00
|
|
|
|
2021-07-14 00:57:48 +02:00
|
|
|
emit LogLine(QString("Setting %1 property: %2 = %3")
|
|
|
|
.arg(name, property->name, value.toString()));
|
2011-04-17 01:04:15 +02:00
|
|
|
|
|
|
|
switch (property->value_type) {
|
2014-02-07 16:34:20 +01:00
|
|
|
case G_TYPE_DOUBLE:
|
|
|
|
g_object_set(object, property->name, value.toDouble(), nullptr);
|
|
|
|
break;
|
|
|
|
case G_TYPE_FLOAT:
|
|
|
|
g_object_set(object, property->name, value.toFloat(), nullptr);
|
|
|
|
break;
|
|
|
|
case G_TYPE_BOOLEAN:
|
|
|
|
g_object_set(object, property->name, value.toInt(), nullptr);
|
|
|
|
break;
|
2011-04-17 01:04:15 +02:00
|
|
|
case G_TYPE_INT:
|
2014-02-07 16:34:20 +01:00
|
|
|
default:
|
|
|
|
g_object_set(object, property->name, value.toInt(), nullptr);
|
|
|
|
break;
|
2011-04-17 01:04:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
g_free(properties);
|
|
|
|
}
|