2011-11-28 19:11:09 +01:00
|
|
|
/* This file is part of Clementine.
|
|
|
|
Copyright 2011, David Sansome <me@davidsansome.com>
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Note: this file is licensed under the Apache License instead of GPL because
|
|
|
|
// it is used by the Spotify blob which links against libspotify and is not GPL
|
|
|
|
// compatible.
|
|
|
|
|
|
|
|
#include "mediapipeline.h"
|
|
|
|
#include "core/logging.h"
|
|
|
|
#include "core/timeconstants.h"
|
|
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
|
|
MediaPipeline::MediaPipeline(int port, quint64 length_msec)
|
2014-02-07 16:34:20 +01:00
|
|
|
: port_(port),
|
|
|
|
length_msec_(length_msec),
|
|
|
|
accepting_data_(true),
|
|
|
|
pipeline_(nullptr),
|
|
|
|
appsrc_(nullptr),
|
|
|
|
byte_rate_(1),
|
|
|
|
offset_bytes_(0) {}
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
MediaPipeline::~MediaPipeline() {
|
|
|
|
if (pipeline_) {
|
|
|
|
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
|
|
|
gst_object_unref(GST_OBJECT(pipeline_));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool MediaPipeline::Init(int sample_rate, int channels) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (is_initialised()) return false;
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
pipeline_ = gst_pipeline_new("pipeline");
|
|
|
|
|
|
|
|
// Create elements
|
2014-02-06 16:49:49 +01:00
|
|
|
appsrc_ = GST_APP_SRC(gst_element_factory_make("appsrc", nullptr));
|
|
|
|
GstElement* gdppay = gst_element_factory_make("gdppay", nullptr);
|
|
|
|
tcpsink_ = gst_element_factory_make("tcpclientsink", nullptr);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
if (!pipeline_ || !appsrc_ || !tcpsink_) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (pipeline_) {
|
|
|
|
gst_object_unref(GST_OBJECT(pipeline_));
|
|
|
|
pipeline_ = nullptr;
|
|
|
|
}
|
|
|
|
if (appsrc_) {
|
|
|
|
gst_object_unref(GST_OBJECT(appsrc_));
|
|
|
|
appsrc_ = nullptr;
|
|
|
|
}
|
|
|
|
if (gdppay) {
|
|
|
|
gst_object_unref(GST_OBJECT(gdppay));
|
|
|
|
}
|
|
|
|
if (tcpsink_) {
|
|
|
|
gst_object_unref(GST_OBJECT(tcpsink_));
|
|
|
|
tcpsink_ = nullptr;
|
|
|
|
}
|
2011-11-28 19:11:09 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2011-11-29 12:10:04 +01:00
|
|
|
// Add elements to the pipeline and link them
|
2011-11-28 19:11:09 +01:00
|
|
|
gst_bin_add(GST_BIN(pipeline_), GST_ELEMENT(appsrc_));
|
|
|
|
gst_bin_add(GST_BIN(pipeline_), gdppay);
|
|
|
|
gst_bin_add(GST_BIN(pipeline_), tcpsink_);
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_element_link_many(GST_ELEMENT(appsrc_), gdppay, tcpsink_, nullptr);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
// Set the sink's port
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(tcpsink_), "host", "127.0.0.1", nullptr);
|
|
|
|
g_object_set(G_OBJECT(tcpsink_), "port", port_, nullptr);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
2012-06-09 18:52:39 +02:00
|
|
|
// Try to send 5 seconds of audio in advance to initially fill Clementine's
|
|
|
|
// buffer.
|
2014-10-14 10:25:39 +02:00
|
|
|
g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec),
|
|
|
|
nullptr);
|
2012-06-09 18:52:39 +02:00
|
|
|
|
2011-11-28 19:28:28 +01:00
|
|
|
// We know the time of each buffer
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(appsrc_), "format", GST_FORMAT_TIME, nullptr);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
2011-11-29 12:10:04 +01:00
|
|
|
// Spotify only pushes data to us every 100ms, so keep the appsrc half full
|
|
|
|
// to prevent tiny stalls.
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(appsrc_), "min-percent", 50, nullptr);
|
2011-11-29 12:10:04 +01:00
|
|
|
|
2011-11-28 19:28:28 +01:00
|
|
|
// Set callbacks for when to start/stop pushing data
|
|
|
|
GstAppSrcCallbacks callbacks;
|
|
|
|
callbacks.enough_data = EnoughDataCallback;
|
|
|
|
callbacks.need_data = NeedDataCallback;
|
|
|
|
callbacks.seek_data = SeekDataCallback;
|
|
|
|
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_app_src_set_callbacks(appsrc_, &callbacks, this, nullptr);
|
2011-11-28 19:28:28 +01:00
|
|
|
|
2011-11-28 19:11:09 +01:00
|
|
|
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
|
2013-09-25 15:42:13 +02:00
|
|
|
static const char* format = "S16BE";
|
2011-11-28 19:11:09 +01:00
|
|
|
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
2013-09-25 15:42:13 +02:00
|
|
|
static const char* format = "S16LE";
|
2011-11-28 19:11:09 +01:00
|
|
|
#endif
|
|
|
|
|
|
|
|
// Set caps
|
2014-10-15 21:57:57 +02:00
|
|
|
GstCaps* caps = gst_caps_new_simple(
|
|
|
|
"audio/x-raw", "format", G_TYPE_STRING, format, "rate", G_TYPE_INT,
|
|
|
|
sample_rate, "channels", G_TYPE_INT, channels, "layout", G_TYPE_STRING,
|
|
|
|
"interleaved", nullptr);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
gst_app_src_set_caps(appsrc_, caps);
|
|
|
|
gst_caps_unref(caps);
|
|
|
|
|
|
|
|
// Set size
|
|
|
|
byte_rate_ = quint64(sample_rate) * channels * 2;
|
|
|
|
const quint64 bytes = byte_rate_ * length_msec_ / 1000;
|
|
|
|
gst_app_src_set_size(appsrc_, bytes);
|
|
|
|
|
|
|
|
// Ready to go
|
2014-02-07 16:34:20 +01:00
|
|
|
return gst_element_set_state(pipeline_, GST_STATE_PLAYING) !=
|
|
|
|
GST_STATE_CHANGE_FAILURE;
|
2011-11-28 19:11:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void MediaPipeline::WriteData(const char* data, qint64 length) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!is_initialised()) return;
|
2011-11-28 19:11:09 +01:00
|
|
|
|
2014-09-21 14:39:30 +02:00
|
|
|
GstBuffer* buffer = gst_buffer_new_allocate(nullptr, length, nullptr);
|
2013-09-25 15:42:13 +02:00
|
|
|
GstMapInfo map_info;
|
|
|
|
gst_buffer_map(buffer, &map_info, GST_MAP_WRITE);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
2013-09-25 15:42:13 +02:00
|
|
|
memcpy(map_info.data, data, length);
|
|
|
|
|
|
|
|
gst_buffer_unmap(buffer, &map_info);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
2014-09-21 14:39:30 +02:00
|
|
|
GST_BUFFER_PTS(buffer) = offset_bytes_ * kNsecPerSec / byte_rate_;
|
2011-11-28 19:11:09 +01:00
|
|
|
GST_BUFFER_DURATION(buffer) = length * kNsecPerSec / byte_rate_;
|
|
|
|
|
|
|
|
offset_bytes_ += length;
|
|
|
|
|
|
|
|
gst_app_src_push_buffer(appsrc_, buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MediaPipeline::EndStream() {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!is_initialised()) return;
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
gst_app_src_end_of_stream(appsrc_);
|
|
|
|
}
|
2011-11-28 19:28:28 +01:00
|
|
|
|
|
|
|
void MediaPipeline::NeedDataCallback(GstAppSrc* src, guint length, void* data) {
|
|
|
|
MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
|
|
|
|
me->accepting_data_ = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void MediaPipeline::EnoughDataCallback(GstAppSrc* src, void* data) {
|
|
|
|
MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
|
|
|
|
me->accepting_data_ = false;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
gboolean MediaPipeline::SeekDataCallback(GstAppSrc* src, guint64 offset,
|
|
|
|
void* data) {
|
|
|
|
// MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
|
2011-11-28 19:28:28 +01:00
|
|
|
|
|
|
|
qLog(Debug) << "Gstreamer wants seek to" << offset;
|
|
|
|
return false;
|
|
|
|
}
|