Clementine-audio-player-Mac.../src/transcoder/transcoder.cpp

575 lines
18 KiB
C++

/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
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"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QSettings>
#include <QThread>
#include <QtDebug>
#include <boost/bind.hpp>
using boost::shared_ptr;
int Transcoder::JobFinishedEvent::sEventType = -1;
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) {
GstElement* ret = gst_element_factory_make(
factory_name.toAscii().constData(),
name.isNull() ? factory_name.toAscii().constData() : name.toAscii().constData());
if (ret && bin)
gst_bin_add(GST_BIN(bin), ret);
if (!ret) {
emit LogLine(
tr("Could not create the GStreamer element \"%1\" -"
" make sure you have all the required GStreamer plugins installed")
.arg(factory_name));
} else {
SetElementProperties(factory_name, G_OBJECT(ret));
}
return ret;
}
struct SuitableElement {
SuitableElement(const QString& name = QString(), int rank = 0)
: name_(name), rank_(rank) {}
bool operator <(const SuitableElement& other) const { return rank_ < other.rank_; }
QString name_;
int rank_;
};
GstElement* Transcoder::CreateElementForMimeType(const QString& element_type,
const QString& mime_type,
GstElement* bin) {
if (mime_type.isEmpty())
return NULL;
// HACK: Force ffmux_mp4 because it doesn't set any useful src caps
if (mime_type == "audio/mp4") {
LogLine(QString("Using '%1' (rank %2)").arg("ffmux_mp4").arg(-1));
return CreateElement("ffmux_mp4", bin);
}
// 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());
GstRegistry* registry = gst_registry_get_default();
GList* const features =
gst_registry_get_feature_list(registry, GST_TYPE_ELEMENT_FACTORY);
for (GList* p = features ; p ; p = g_list_next(p)) {
GstElementFactory* factory = GST_ELEMENT_FACTORY(p->data);
// Is this the right type of plugin?
if (QString(factory->details.klass).contains(element_type)) {
const GList* const templates =
gst_element_factory_get_static_pad_templates(factory);
for (const GList* p = templates ; p ; p = g_list_next(p)) {
// Only interested in source pads
GstStaticPadTemplate* pad_template = reinterpret_cast<GstStaticPadTemplate*>(p->data);
if (pad_template->direction != GST_PAD_SRC)
continue;
// 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));
QString name = GST_PLUGIN_FEATURE_NAME(factory);
if (name.startsWith("ffmux") || name.startsWith("ffenc"))
rank = -1; // ffmpeg usually sucks
suitable_elements_ << SuitableElement(name, rank);
}
gst_caps_unref(intersection);
}
}
}
}
gst_plugin_feature_list_free(features);
gst_caps_unref(target_caps);
if (suitable_elements_.isEmpty())
return NULL;
// Sort by rank
qSort(suitable_elements_);
const SuitableElement& best = suitable_elements_.last();
LogLine(QString("Using '%1' (rank %2)").arg(best.name_).arg(best.rank_));
if (best.name_ == "lamemp3enc") {
// 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.
LogLine("Adding xingmux and id3v2mux to the pipeline");
// Create the bin
GstElement* mp3bin = gst_bin_new("mp3bin");
gst_bin_add(GST_BIN(bin), mp3bin);
// Create the elements
GstElement* lame = CreateElement("lamemp3enc", mp3bin);
GstElement* xing = CreateElement("xingmux", mp3bin);
GstElement* id3v2 = CreateElement("id3v2mux", mp3bin);
if (!lame || !xing || !id3v2) {
return NULL;
}
// Link the elements together
gst_element_link_many(lame, xing, id3v2, NULL);
// Link the bin's ghost pads to the elements on each end
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);
}
}
Transcoder::JobFinishedEvent::JobFinishedEvent(JobState *state, bool success)
: QEvent(QEvent::Type(sEventType)),
state_(state),
success_(success)
{
}
void Transcoder::JobState::PostFinished(bool success) {
if (success) {
emit parent_->LogLine(
tr("Successfully written %1").arg(QDir::toNativeSeparators(job_.output)));
}
QCoreApplication::postEvent(parent_, new Transcoder::JobFinishedEvent(this, success));
}
Transcoder::Transcoder(QObject* parent)
: QObject(parent),
max_threads_(QThread::idealThreadCount())
{
if (JobFinishedEvent::sEventType == -1)
JobFinishedEvent::sEventType = QEvent::registerEventType();
// Initialise some settings for the lamemp3enc element.
QSettings s;
s.beginGroup("Transcoder/lamemp3enc");
if (s.value("target").isNull()) {
s.setValue("target", 1); // 1 == bitrate
}
if (s.value("cbr").isNull()) {
s.setValue("cbr", true);
}
}
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);
return ret;
}
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
switch (type) {
case Song::Type_Flac:
return TranscoderPreset(type, "Flac", "flac", "audio/x-flac");
case Song::Type_Mp4:
return TranscoderPreset(type, "M4A AAC", "mp4", "audio/mpeg, mpegversion=(int)4", "audio/mp4");
case Song::Type_Mpeg:
return TranscoderPreset(type, "MP3", "mp3", "audio/mpeg, mpegversion=(int)1, layer=(int)3");
case Song::Type_OggVorbis:
return TranscoderPreset(type, "Ogg Vorbis", "ogg", "audio/x-vorbis", "application/ogg");
case Song::Type_OggFlac:
return TranscoderPreset(type, "Ogg Flac", "ogg", "audio/x-flac", "application/ogg");
case Song::Type_OggSpeex:
return TranscoderPreset(type, "Ogg Speex", "spx", "audio/x-speex", "application/ogg");
case Song::Type_Asf:
return TranscoderPreset(type, "Windows Media audio", "wma", "audio/x-wma", "video/x-ms-asf");
case Song::Type_Wav:
return TranscoderPreset(type, "Wav", "wav", QString(), "audio/x-wav");
default:
qWarning() << "Unsupported format in Transcoder::PresetForFileType:" << type;
return TranscoderPreset();
}
}
Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
if (supported.isEmpty())
return Song::Type_Unknown;
QList<Song::FileType> best_formats;
best_formats << Song::Type_Mpeg;
best_formats << Song::Type_OggVorbis;
best_formats << Song::Type_Asf;
foreach (Song::FileType type, best_formats) {
if (supported.isEmpty() || supported.contains(type))
return type;
}
return supported[0];
}
void Transcoder::AddJob(const QString& input,
const TranscoderPreset& preset,
const QString& output) {
Job job;
job.input = input;
job.preset = preset;
// Use the supplied filename if there was one, otherwise take the file
// extension off the input filename and append the correct one.
if (!output.isEmpty())
job.output = output;
else
job.output = input.section('.', 0, -2) + '.' + preset.extension_;
// Never overwrite existing files
if (QFile::exists(job.output)) {
for (int i=0 ; ; ++i) {
QString new_filename = QString("%1.%2").arg(job.output).arg(i);
if (!QFile::exists(new_filename)) {
job.output = new_filename;
break;
}
}
}
queued_jobs_ << job;
}
void Transcoder::Start() {
emit LogLine(tr("Transcoding %1 files using %2 threads")
.arg(queued_jobs_.count()).arg(max_threads()));
forever {
StartJobStatus status = MaybeStartNextJob();
if (status == AllThreadsBusy || status == NoMoreJobs)
break;
}
}
Transcoder::StartJobStatus Transcoder::MaybeStartNextJob() {
if (current_jobs_.count() >= max_threads())
return AllThreadsBusy;
if (queued_jobs_.isEmpty()) {
if (current_jobs_.isEmpty()) {
emit AllJobsComplete();
}
return NoMoreJobs;
}
Job job = queued_jobs_.takeFirst();
if (StartJob(job))
return StartedSuccessfully;
emit JobComplete(job.input, false);
return FailedToStart;
}
void Transcoder::NewPadCallback(GstElement*, GstPad* pad, gboolean, gpointer data) {
JobState* state = reinterpret_cast<JobState*>(data);
GstPad* const audiopad = gst_element_get_pad(state->convert_element_, "sink");
if (GST_PAD_IS_LINKED(audiopad)) {
qDebug() << "audiopad is already linked. Unlinking old pad.";
gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
}
gst_pad_link(pad, audiopad);
gst_object_unref(audiopad);
}
gboolean Transcoder::BusCallback(GstBus*, GstMessage* msg, gpointer data) {
JobState* state = reinterpret_cast<JobState*>(data);
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_ERROR:
state->ReportError(msg);
state->PostFinished(false);
break;
default:
break;
}
return GST_BUS_DROP;
}
GstBusSyncReply Transcoder::BusCallbackSync(GstBus*, GstMessage* msg, gpointer data) {
JobState* state = reinterpret_cast<JobState*>(data);
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
state->PostFinished(true);
break;
case GST_MESSAGE_ERROR:
state->ReportError(msg);
state->PostFinished(false);
break;
default:
break;
}
return GST_BUS_PASS;
}
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);
emit parent_->LogLine(
tr("Error processing %1: %2").arg(QDir::toNativeSeparators(job_.input), message));
}
bool Transcoder::StartJob(const Job &job) {
shared_ptr<JobState> state(new JobState(job, this));
emit LogLine(tr("Starting %1").arg(QDir::toNativeSeparators(job.input)));
// Create the pipeline.
// This should be a scoped_ptr, but scoped_ptr doesn't support custom
// destructors.
state->pipeline_.reset(gst_pipeline_new("pipeline"),
boost::bind(gst_object_unref, _1));
if (!state->pipeline_) return false;
// Create all the elements
GstElement* src = CreateElement("filesrc", state->pipeline_.get());
GstElement* decode = CreateElement("decodebin2", state->pipeline_.get());
GstElement* convert = CreateElement("audioconvert", state->pipeline_.get());
GstElement* codec = CreateElementForMimeType("Codec/Encoder/Audio", job.preset.codec_mimetype_, state->pipeline_.get());
GstElement* muxer = CreateElementForMimeType("Codec/Muxer", job.preset.muxer_mimetype_, state->pipeline_.get());
GstElement* sink = CreateElement("filesink", state->pipeline_.get());
if (!src || !decode || !convert || !sink)
return false;
if (!codec && !job.preset.codec_mimetype_.isEmpty()) {
LogLine(tr("Couldn't find an encoder for %1, check you have the correct GStreamer plugins installed"
).arg(job.preset.codec_mimetype_));
return false;
}
if (!muxer && !job.preset.muxer_mimetype_.isEmpty()) {
LogLine(tr("Couldn't find a muxer for %1, check you have the correct GStreamer plugins installed"
).arg(job.preset.muxer_mimetype_));
return false;
}
// Join them together
gst_element_link(src, decode);
if (codec && muxer)
gst_element_link_many(convert, codec, muxer, sink, NULL);
else if (codec)
gst_element_link_many(convert, codec, sink, NULL);
else if (muxer)
gst_element_link_many(convert, muxer, sink, NULL);
// Set properties
g_object_set(src, "location", job.input.toLocal8Bit().constData(), NULL);
g_object_set(sink, "location", job.output.toLocal8Bit().constData(), NULL);
// Set callbacks
state->convert_element_ = convert;
g_signal_connect(decode, "new-decoded-pad", G_CALLBACK(NewPadCallback), state.get());
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(state->pipeline_.get())), BusCallbackSync, state.get());
state->bus_callback_id_ = gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(state->pipeline_.get())), BusCallback, state.get());
// Start the pipeline
gst_element_set_state(state->pipeline_.get(), GST_STATE_PLAYING);
// 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;
return true;
}
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()) {
if (it->get() == finished_event->state_)
break;
++it;
}
if (it == current_jobs_.end()) {
// Couldn't find it, maybe GStreamer gave us an event after we'd destroyed
// the pipeline?
return true;
}
QString filename = (*it)->job_.input;
// Remove event handlers from the gstreamer pipeline so they don't get
// called after the pipeline is shutting down
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(
finished_event->state_->pipeline_.get())), NULL, NULL);
g_source_remove(finished_event->state_->bus_callback_id_);
// Remove it from the list - this will also destroy the GStreamer pipeline
current_jobs_.erase(it);
// Emit the finished signal
emit JobComplete(filename, finished_event->success_);
// Start some more jobs
MaybeStartNextJob();
return true;
}
return QObject::event(e);
}
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);
// Remove event handlers from the gstreamer pipeline so they don't get
// called after the pipeline is shutting down
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(
state->pipeline_.get())), NULL, NULL);
g_source_remove(state->bus_callback_id_);
// Stop the pipeline
if (gst_element_set_state(state->pipeline_.get(), GST_STATE_NULL) == GST_STATE_CHANGE_ASYNC) {
// Wait for it to finish stopping...
gst_element_get_state(state->pipeline_.get(), NULL, NULL, GST_CLOCK_TIME_NONE);
}
// Remove the job, this destroys the GStreamer pipeline too
it = current_jobs_.erase(it);
}
}
QMap<QString, float> Transcoder::GetProgress() const {
QMap<QString, float> ret;
foreach (boost::shared_ptr<JobState> state, current_jobs_) {
if (!state->pipeline_)
continue;
gint64 position = 0;
gint64 duration = 0;
GstFormat format = GST_FORMAT_TIME;
gst_element_query_position(state->pipeline_.get(), &format, &position);
gst_element_query_duration(state->pipeline_.get(), &format, &duration);
ret[state->job_.input] = float(position) / duration;
}
return ret;
}
void Transcoder::SetElementProperties(const QString& name, GObject* object) {
QSettings s;
s.beginGroup("Transcoder/" + name);
guint properties_count = 0;
GParamSpec** properties = g_object_class_list_properties(
G_OBJECT_GET_CLASS(object), &properties_count);
for (int i=0 ; i<properties_count ; ++i) {
GParamSpec* property = properties[i];
const QVariant value = s.value(property->name);
if (value.isNull())
continue;
LogLine(QString("Setting %1 property: %2 = %3").arg(name, property->name, value.toString()));
switch (property->value_type) {
case G_TYPE_DOUBLE: g_object_set(object, property->name, value.toDouble(), NULL); break;
case G_TYPE_FLOAT: g_object_set(object, property->name, value.toFloat(), NULL); break;
case G_TYPE_BOOLEAN: g_object_set(object, property->name, value.toInt(), NULL); break;
case G_TYPE_INT:
default: g_object_set(object, property->name, value.toInt(), NULL); break;
}
}
g_free(properties);
}