1
0
mirror of https://github.com/clementine-player/Clementine synced 2025-01-27 17:49:19 +01:00

Remove the custom gstspotifytcpsrc and use tcpserversrc ! gdpdepay to receive data. Use a gstreamer pipeline on the other end as well to send data.

This commit is contained in:
David Sansome 2011-11-28 18:11:09 +00:00
parent e9d770a864
commit 22e6a649b7
22 changed files with 360 additions and 617 deletions

View File

@ -381,7 +381,6 @@ if(HAVE_BREAKPAD)
endif(HAVE_BREAKPAD)
if(HAVE_SPOTIFY)
add_subdirectory(gst/spotifytcpsrc)
add_subdirectory(spotifyblob/common)
endif(HAVE_SPOTIFY)

View File

@ -1,35 +0,0 @@
cmake_minimum_required(VERSION 2.6)
set(CMAKE_C_FLAGS "-Wall")
set(CMAKE_CXX_FLAGS "-Woverloaded-virtual -Wall")
include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${CMAKE_SOURCE_DIR})
include_directories(${CMAKE_BINARY_DIR})
include_directories(${CMAKE_SOURCE_DIR}/src)
include_directories(${CMAKE_BINARY_DIR}/src)
include_directories(${GLIB_INCLUDE_DIRS})
include_directories(${GOBJECT_INCLUDE_DIRS})
include_directories(${GSTREAMER_INCLUDE_DIRS})
if(HAVE_LIBGPOD)
include_directories(${LIBGPOD_INCLUDE_DIRS})
endif(HAVE_LIBGPOD)
set(SOURCES
gstspotifytcpsrc.cpp
)
add_library(gstspotifytcpsrc STATIC
${SOURCES}
)
target_link_libraries(gstspotifytcpsrc
clementine_lib
${GOBJECT_LIBRARIES}
${GLIB_LIBRARIES}
${GSTREAMER_LIBRARIES}
${GSTREAMER_BASE_LIBRARIES}
${IMOBILEDEVICE_LIBRARIES}
)

View File

@ -1,365 +0,0 @@
/* 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 "gstspotifytcpsrc.h"
#include "core/logging.h"
#include "internet/internetmodel.h"
#include "internet/spotifyserver.h"
#include "internet/spotifyservice.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <stdlib.h>
#include <string.h>
#include <gst/gst.h>
#ifdef Q_OS_UNIX
# include <sys/types.h>
# include <sys/socket.h>
#endif
static const int kPollTimeoutMsec = 100;
static const int kMaxConnectionWaits = 50; // each one takes kPollTimeoutMsec
// This is about one second of audio at spotify's bitrate.
static const int kSocketBufferSize = 176400;
// Signals
enum {
LAST_SIGNAL
};
// Properties
enum {
PROP_0,
PROP_LOCATION,
};
GST_DEBUG_CATEGORY_STATIC(gst_spotifytcp_src_debug);
#define GST_CAT_DEFAULT gst_spotifytcp_src_debug
static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS_ANY
);
static void gst_spotifytcp_src_interface_init(GType type);
static void gst_spotifytcp_src_uri_handler_init(gpointer iface, gpointer data);
static void gst_spotifytcp_src_set_property(GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec);
static void gst_spotifytcp_src_get_property(GObject * object, guint prop_id, GValue * value, GParamSpec * pspec);
static void gst_spotifytcp_src_finalize(GObject* object);
static gboolean gst_spotifytcp_src_start(GstBaseSrc* src);
static gboolean gst_spotifytcp_src_stop(GstBaseSrc* src);
static GstFlowReturn gst_spotifytcp_src_create(GstPushSrc* src, GstBuffer** buffer);
static gboolean gst_spotifytcp_src_is_seekable(GstBaseSrc* src);
static gboolean gst_spotifytcp_src_do_seek(GstBaseSrc* src, GstSegment* segment);
static gboolean gst_spotifytcp_src_unlock(GstBaseSrc* src);
static gboolean gst_spotifytcp_src_unlock_stop(GstBaseSrc* src);
GST_BOILERPLATE_FULL(GstSpotifyTcpSrc, gst_spotifytcp_src, GstPushSrc,
GST_TYPE_PUSH_SRC, gst_spotifytcp_src_interface_init);
static void gst_spotifytcp_src_interface_init(GType type) {
static const GInterfaceInfo urihandler_info = {
gst_spotifytcp_src_uri_handler_init,
NULL,
NULL
};
GST_DEBUG_CATEGORY_INIT (gst_spotifytcp_src_debug, "spotifytcpsrc", 0,
"libspotify source");
g_type_add_interface_static(type, GST_TYPE_URI_HANDLER, &urihandler_info);
}
static void gst_spotifytcp_src_base_init(gpointer gclass) {
GstElementClass *element_class = GST_ELEMENT_CLASS (gclass);
gst_element_class_set_details_simple(element_class,
"libspotify source",
"Source/SpotifyTcp",
"Receive audio data from an external libspotify process",
"David Sansome <me@davidsansome.com>");
gst_element_class_add_pad_template (element_class,
gst_static_pad_template_get (&src_factory));
}
static void gst_spotifytcp_src_class_init (GstSpotifyTcpSrcClass* klass) {
GObjectClass* gobject_class = (GObjectClass*) klass;
GstBaseSrcClass* gstbasesrc_class = (GstBaseSrcClass*) klass;
GstPushSrcClass* gstpushsrc_class = (GstPushSrcClass*) klass;
gobject_class->set_property = gst_spotifytcp_src_set_property;
gobject_class->get_property = gst_spotifytcp_src_get_property;
gobject_class->finalize = gst_spotifytcp_src_finalize;
g_object_class_install_property(gobject_class, PROP_LOCATION,
g_param_spec_string(
"location", "URI",
"The URI of the file to read, must be of the form afc://uuid/filename", NULL,
static_cast<GParamFlags>(
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | GST_PARAM_MUTABLE_READY)));
gstpushsrc_class->create = gst_spotifytcp_src_create;
gstbasesrc_class->start = gst_spotifytcp_src_start;
gstbasesrc_class->stop = gst_spotifytcp_src_stop;
gstbasesrc_class->is_seekable = gst_spotifytcp_src_is_seekable;
gstbasesrc_class->do_seek = gst_spotifytcp_src_do_seek;
gstbasesrc_class->unlock = gst_spotifytcp_src_unlock;
gstbasesrc_class->unlock_stop = gst_spotifytcp_src_unlock_stop;
}
static GstURIType gst_spotifytcp_src_uri_get_type() {
return GST_URI_SRC;
}
static gchar** gst_spotifytcp_src_uri_get_protocols() {
static const gchar* protocols[] = { "spotify", NULL };
return (char**) protocols;
}
static const gchar* gst_spotifytcp_src_uri_get_uri(GstURIHandler* handler) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(handler);
return self->uri_->constData();
}
static gboolean gst_spotifytcp_src_uri_set_uri(GstURIHandler* handler, const gchar* uri) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(handler);
*(self->uri_) = uri;
return TRUE;
}
static void gst_spotifytcp_src_uri_handler_init(gpointer g_iface, gpointer data) {
GstURIHandlerInterface* iface = (GstURIHandlerInterface*) g_iface;
iface->get_type = gst_spotifytcp_src_uri_get_type;
iface->get_protocols = gst_spotifytcp_src_uri_get_protocols;
iface->set_uri = gst_spotifytcp_src_uri_set_uri;
iface->get_uri = gst_spotifytcp_src_uri_get_uri;
}
static void gst_spotifytcp_src_init(GstSpotifyTcpSrc* element, GstSpotifyTcpSrcClass* gclass) {
element->service_ = NULL;
element->uri_ = new QByteArray;
element->server_ = NULL;
element->socket_ = NULL;
element->unlock_ = false;
}
static void gst_spotifytcp_src_finalize(GObject* object) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(object);
delete self->uri_;
delete self->socket_;
delete self->server_;
G_OBJECT_CLASS(parent_class)->finalize(object);
}
static void gst_spotifytcp_src_set_property(
GObject* object, guint prop_id, const GValue* value, GParamSpec* pspec) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(object);
switch (prop_id) {
case PROP_LOCATION:
gst_spotifytcp_src_uri_set_uri(reinterpret_cast<GstURIHandler*>(self),
g_value_get_string(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void gst_spotifytcp_src_get_property(
GObject* object, guint prop_id, GValue* value, GParamSpec* pspec) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(object);
switch (prop_id) {
case PROP_LOCATION:
g_value_set_string(value, gst_spotifytcp_src_uri_get_uri(
reinterpret_cast<GstURIHandler*>(self)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static gboolean gst_spotifytcp_src_start(GstBaseSrc* src) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(src);
if (self->server_)
return TRUE;
self->service_ = InternetModel::Service<SpotifyService>();
if (!self->service_)
return FALSE;
SpotifyServer* server = self->service_->server();
if (!server)
return FALSE;
self->server_ = new QTcpServer;
if (!self->server_->listen(QHostAddress::LocalHost, 0)) {
delete self->server_;
self->server_ = NULL;
return FALSE;
}
qLog(Debug) << "Listening for media on port" << self->server_->serverPort();
server->StartPlayback(QString::fromAscii(*self->uri_), self->server_->serverPort());
// Wait for a client to connect
int attempts = 0;
while (!self->server_->waitForNewConnection(kPollTimeoutMsec)) {
if (self->unlock_) {
qLog(Warning) << "Unlock while waiting for connection";
return FALSE;
}
++attempts;
if (attempts > kMaxConnectionWaits) {
qLog(Warning) << "Timed out waiting for connection";
return FALSE;
}
}
// Take the socket
self->socket_ = self->server_->nextPendingConnection();
if (!self->socket_) {
qLog(Warning) << "Failed to get pending connection";
return FALSE;
}
qLog(Info) << "Media socket connected";
self->socket_->setReadBufferSize(kSocketBufferSize);
self->socket_->setSocketOption(QAbstractSocket::LowDelayOption, true);
#ifdef Q_OS_UNIX
const int size = kSocketBufferSize;
setsockopt(self->socket_->socketDescriptor(), SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
#endif
return TRUE;
}
static gboolean gst_spotifytcp_src_stop(GstBaseSrc* src) {
return TRUE;
}
static GstFlowReturn gst_spotifytcp_src_create(GstPushSrc* src, GstBuffer** buffer) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(src);
if (self->socket_->state() != QAbstractSocket::ConnectedState) {
qLog(Info) << "Media socket disconnected";
return GST_FLOW_UNEXPECTED;
}
while (!self->socket_->waitForReadyRead(kPollTimeoutMsec)) {
if (self->unlock_) {
qLog(Warning) << "Unlock while reading data";
return GST_FLOW_WRONG_STATE;
}
if (self->socket_->state() != QAbstractSocket::ConnectedState) {
qLog(Info) << "Media socket disconnected2";
return GST_FLOW_UNEXPECTED;
}
}
qint64 length = self->socket_->bytesAvailable();
GstBuffer* buf = gst_buffer_try_new_and_alloc(length);
if (buf == NULL) {
qLog(Error) << "Failed to allocate buffer";
return GST_FLOW_ERROR;
}
qint64 bytes_read = self->socket_->read(
reinterpret_cast<char*>(GST_BUFFER_DATA(buf)), length);
if (bytes_read < 0) {
gst_buffer_unref(buf);
return GST_FLOW_UNEXPECTED;
}
GST_BUFFER_SIZE(buf) = bytes_read;
*buffer = buf;
return GST_FLOW_OK;
}
static gboolean gst_spotifytcp_src_unlock(GstBaseSrc* src) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(src);
self->unlock_ = true;
return TRUE;
}
static gboolean gst_spotifytcp_src_unlock_stop(GstBaseSrc* src) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(src);
self->unlock_ = false;
return TRUE;
}
static gboolean gst_spotifytcp_src_is_seekable(GstBaseSrc* src) {
return TRUE;
}
static gboolean gst_spotifytcp_src_do_seek(GstBaseSrc* src, GstSegment* segment) {
GstSpotifyTcpSrc* self = GST_SPOTIFYTCPSRC(src);
SpotifyServer* server = self->service_->server();
if (!server)
return FALSE;
// Tell the spotify client to seek.
server->metaObject()->invokeMethod(server, "Seek", Q_ARG(qint64, segment->start));
// Throw away any old data that's sitting in the socket's read buffer.
self->socket_->waitForReadyRead(kPollTimeoutMsec);
self->socket_->read(self->socket_->bytesAvailable());
return TRUE;
}
#define PACKAGE "Clementine"
static gboolean spotifytcpsrc_init(GstPlugin* spotifytcpsrc) {
return gst_element_register(spotifytcpsrc, "spotifytcpsrc", GST_RANK_PRIMARY, GST_TYPE_SPOTIFYTCPSRC);
}
void spotifytcpsrc_register_static() {
gst_plugin_register_static(
GST_VERSION_MAJOR,
GST_VERSION_MINOR,
"spotifytcpsrc",
const_cast<gchar*>("libspotify source"),
spotifytcpsrc_init,
"0.1",
"GPL",
"Clementine",
"Clementine",
"http://www.clementine-player.org/");
}

View File

@ -1,72 +0,0 @@
/* 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/>.
*/
#ifndef __GST_SPOTIFYTCPSRC_H__
#define __GST_SPOTIFYTCPSRC_H__
#include <gst/gst.h>
#include <gst/base/gstpushsrc.h>
class QByteArray;
class QEventLoop;
class QTcpServer;
class QTcpSocket;
class SpotifyService;
extern "C" {
void spotifytcpsrc_register_static();
}
G_BEGIN_DECLS
#define GST_TYPE_SPOTIFYTCPSRC \
(gst_spotifytcp_src_get_type())
#define GST_SPOTIFYTCPSRC(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_SPOTIFYTCPSRC,GstSpotifyTcpSrc))
#define GST_SPOTIFYTCPSRC_CLASS(klass) \
(G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_SPOTIFYTCPSRC,GstSpotifyTcpSrcClass))
#define GST_IS_SPOTIFYTCPSRC(obj) \
(G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_SPOTIFYTCPSRC))
#define GST_IS_SPOTIFYTCPSRC_CLASS(klass) \
(G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_SPOTIFYTCPSRC))
typedef struct _GstSpotifyTcpSrc GstSpotifyTcpSrc;
typedef struct _GstSpotifyTcpSrcClass GstSpotifyTcpSrcClass;
struct _GstSpotifyTcpSrc {
GstPushSrc element;
SpotifyService* service_;
QByteArray* uri_;
QTcpServer* server_;
QTcpSocket* socket_;
bool unlock_;
};
struct _GstSpotifyTcpSrcClass {
GstPushSrcClass parent_class;
};
GType gst_spotifytcp_src_get_type (void);
G_END_DECLS
#endif

View File

@ -11,6 +11,7 @@ set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(SOURCES
main.cpp
mediapipeline.cpp
spotifyclient.cpp
spotify_utilities.cpp
@ -41,6 +42,8 @@ target_link_libraries(clementine-spotifyblob
${SPOTIFY_LIBRARIES} ${SPOTIFY_LDFLAGS}
${QT_QTCORE_LIBRARY}
${QT_QTNETWORK_LIBRARY}
${GSTREAMER_BASE_LIBRARIES}
${GSTREAMER_APP_LIBRARIES}
clementine-spotifyblob-messages
)

View File

@ -22,6 +22,8 @@
#include <QCoreApplication>
#include <QStringList>
#include <gst/gst.h>
#include "spotifyclient.h"
#include "core/logging.h"
@ -33,6 +35,8 @@ int main(int argc, char** argv) {
logging::Init();
gst_init(NULL, NULL);
const QStringList arguments(a.arguments());
if (arguments.length() != 2) {

View File

@ -0,0 +1,128 @@
/* 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)
: port_(port),
length_msec_(length_msec),
pipeline_(NULL),
appsrc_(NULL),
byte_rate_(1),
offset_bytes_(0)
{
}
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) {
if (is_initialised())
return false;
pipeline_ = gst_pipeline_new("pipeline");
// Create elements
appsrc_ = GST_APP_SRC(gst_element_factory_make("appsrc", NULL));
GstElement* gdppay = gst_element_factory_make("gdppay", NULL);
tcpsink_ = gst_element_factory_make("tcpclientsink", NULL);
if (!pipeline_ || !appsrc_ || !tcpsink_) {
if (pipeline_) { gst_object_unref(GST_OBJECT(pipeline_)); pipeline_ = NULL; }
if (appsrc_) { gst_object_unref(GST_OBJECT(appsrc_)); appsrc_ = NULL; }
if (gdppay) { gst_object_unref(GST_OBJECT(gdppay)); }
if (tcpsink_) { gst_object_unref(GST_OBJECT(tcpsink_)); tcpsink_ = NULL; }
return false;
}
// Add elements to the pipelin and link them
gst_bin_add(GST_BIN(pipeline_), GST_ELEMENT(appsrc_));
gst_bin_add(GST_BIN(pipeline_), gdppay);
gst_bin_add(GST_BIN(pipeline_), tcpsink_);
gst_element_link_many(GST_ELEMENT(appsrc_), gdppay, tcpsink_, NULL);
// Set the sink's port
g_object_set(G_OBJECT(tcpsink_), "host", "127.0.0.1", NULL);
g_object_set(G_OBJECT(tcpsink_), "port", port_, NULL);
g_object_set(G_OBJECT(appsrc_), "format", GST_FORMAT_TIME, NULL);
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
const int endianness = G_BIG_ENDIAN;
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
const int endianness = G_LITTLE_ENDIAN;
#endif
// Set caps
GstCaps* caps = gst_caps_new_simple("audio/x-raw-int",
"endianness", G_TYPE_INT, endianness,
"signed", G_TYPE_BOOLEAN, TRUE,
"width", G_TYPE_INT, 16,
"depth", G_TYPE_INT, 16,
"rate", G_TYPE_INT, sample_rate,
"channels", G_TYPE_INT, channels,
NULL);
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
return gst_element_set_state(pipeline_, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE;
}
void MediaPipeline::WriteData(const char* data, qint64 length) {
if (!is_initialised())
return;
GstBuffer* buffer = gst_buffer_new_and_alloc(length);
memcpy(GST_BUFFER_DATA(buffer), data, length);
GST_BUFFER_OFFSET(buffer) = offset_bytes_;
GST_BUFFER_TIMESTAMP(buffer) = offset_bytes_ * kNsecPerSec / byte_rate_;
GST_BUFFER_DURATION(buffer) = length * kNsecPerSec / byte_rate_;
//qLog(Debug) << GST_BUFFER_OFFSET(buffer) << GST_BUFFER_TIMESTAMP(buffer) << GST_BUFFER_DURATION(buffer);
offset_bytes_ += length;
GST_BUFFER_OFFSET_END(buffer) = offset_bytes_;
gst_app_src_push_buffer(appsrc_, buffer);
}
void MediaPipeline::EndStream() {
if (!is_initialised())
return;
gst_app_src_end_of_stream(appsrc_);
}

View File

@ -0,0 +1,54 @@
/* 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.
#ifndef MEDIAPIPELINE_H
#define MEDIAPIPELINE_H
#include <QtGlobal>
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
class MediaPipeline {
public:
MediaPipeline(int port, quint64 length_msec);
~MediaPipeline();
bool is_initialised() const { return pipeline_; }
bool Init(int sample_rate, int channels);
void WriteData(const char* data, qint64 length);
void EndStream();
private:
Q_DISABLE_COPY(MediaPipeline)
const int port_;
const quint64 length_msec_;
GstElement* pipeline_;
GstAppSrc* appsrc_;
GstElement* tcpsink_;
quint64 byte_rate_;
quint64 offset_bytes_;
};
#endif // MEDIAPIPELINE_H

View File

@ -1,3 +1,23 @@
/* 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 "spotify_utilities.h"
#include <stdlib.h>

View File

@ -1,3 +1,23 @@
/* 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.
#ifndef SPOTIFY_UTILITIES_H
#define SPOTIFY_UTILITIES_H

View File

@ -19,6 +19,7 @@
// compatible.
#include "mediapipeline.h"
#include "spotifyclient.h"
#include "spotifykey.h"
#include "spotifymessagehandler.h"
@ -40,12 +41,9 @@ SpotifyClient::SpotifyClient(QObject* parent)
: QObject(parent),
api_key_(QByteArray::fromBase64(kSpotifyApiKey)),
protocol_socket_(new QTcpSocket(this)),
media_socket_(NULL),
handler_(new SpotifyMessageHandler(protocol_socket_, this)),
session_(NULL),
events_timer_(new QTimer(this)),
media_length_msec_(-1),
byte_rate_(0) {
events_timer_(new QTimer(this)) {
memset(&spotify_callbacks_, 0, sizeof(spotify_callbacks_));
memset(&spotify_config_, 0, sizeof(spotify_config_));
memset(&playlistcontainer_callbacks_, 0, sizeof(playlistcontainer_callbacks_));
@ -62,7 +60,6 @@ SpotifyClient::SpotifyClient(QObject* parent)
spotify_callbacks_.offline_status_updated = &OfflineStatusUpdatedCallback;
spotify_callbacks_.connection_error = &ConnectionErrorCallback;
spotify_callbacks_.message_to_user = &UserMessageCallback;
spotify_callbacks_.get_audio_buffer_stats = &GetAudioBufferStatsCallback;
spotify_callbacks_.start_playback = &StartPlaybackCallback;
spotify_callbacks_.stop_playback = &StopPlaybackCallback;
@ -637,7 +634,7 @@ int SpotifyClient::MusicDeliveryCallback(
const void* frames, int num_frames) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
if (!me->media_socket_) {
if (!me->media_pipeline_) {
return 0;
}
@ -645,76 +642,32 @@ int SpotifyClient::MusicDeliveryCallback(
return 0;
}
// Write the WAVE header if it hasn't been written yet.
if (me->media_length_msec_ != -1) {
qLog(Debug) << "Sending WAVE header";
QDataStream s(me->media_socket_);
s.setByteOrder(QDataStream::LittleEndian);
const int bytes_per_sample = 2;
const int byte_rate = format->sample_rate * format->channels * bytes_per_sample;
const quint32 data_size = quint64(me->media_length_msec_) * byte_rate / 1000;
qLog(Debug) << "length" << me->media_length_msec_ << "byte_rate" << byte_rate
<< "data_size" << data_size;
// RIFF header
s.writeRawData("RIFF", 4);
s << quint32(32 + data_size);
s.writeRawData("WAVE", 4);
// WAVE fmt sub-chunk
s.writeRawData("fmt ", 4);
s << quint32(16); // Subchunk1Size
s << quint16(1); // AudioFormat
s << quint16(format->channels); // NumChannels
s << quint32(format->sample_rate); // SampleRate
s << quint32(byte_rate); // ByteRate
s << quint16(format->channels * bytes_per_sample); // BlockAlign
s << quint16(bytes_per_sample * 8); // BitsPerSample
// Data sub-chunk
s.writeRawData("data", 4);
s << quint32(data_size);
me->media_length_msec_ = -1;
me->byte_rate_ = byte_rate;
if (!me->media_pipeline_->is_initialised()) {
if (!me->media_pipeline_->Init(format->sample_rate, format->channels)) {
qLog(Warning) << "Failed to intitialise media pipeline";
sp_session_player_unload(me->session_);
me->media_pipeline_.reset();
return 0;
}
}
#ifdef Q_OS_DARWIN
// For some reason, the data doesn't reliably get pushed to the underlying socket on Mac.
me->media_socket_->flush();
#endif
if (me->media_socket_->bytesToWrite() >= 8192) {
return 0;
}
// Write the audio data.
qint64 bytes_written = me->media_socket_->write(
me->media_pipeline_->WriteData(
reinterpret_cast<const char*>(frames),
num_frames * format->channels * 2);
return bytes_written / (format->channels * 2);
return num_frames;
}
void SpotifyClient::EndOfTrackCallback(sp_session* session) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
// Close the socket - it will get deleted when the other side has
// disconnected
me->media_socket_->close();
me->media_socket_ = NULL;
me->media_pipeline_.reset();
}
void SpotifyClient::StreamingErrorCallback(sp_session* session, sp_error error) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
// Close the socket - it will get deleted when the other side has
// disconnected
me->media_socket_->close();
me->media_socket_ = NULL;
me->media_pipeline_.reset();
// Send the error
me->SendPlaybackError(QString::fromUtf8(sp_error_message(error)));
@ -728,14 +681,6 @@ void SpotifyClient::UserMessageCallback(sp_session* session, const char* message
qLog(Debug) << Q_FUNC_INFO << message;
}
void SpotifyClient::GetAudioBufferStatsCallback(
sp_session* session,
sp_audio_buffer_stats* stats) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
stats->stutter = 0;
stats->samples = me->media_socket_ ? me->media_socket_->bytesToWrite() / 2 : 0;
}
void SpotifyClient::StartPlaybackCallback(sp_session* session) {
qLog(Debug) << Q_FUNC_INFO;
}
@ -841,11 +786,8 @@ void SpotifyClient::StartPlayback(const spotify_pb::PlaybackRequest& req) {
}
void SpotifyClient::Seek(qint64 offset_bytes) {
if (byte_rate_) {
const int msec = ((offset_bytes - kWaveHeaderSize) * 1000) / byte_rate_;
qLog(Debug) << "Seeking to time" << msec << "ms";
sp_session_player_seek(session_, msec);
}
// TODO
qLog(Error) << "TODO seeking";
}
void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
@ -868,23 +810,12 @@ void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
}
// Create the media socket
QTcpSocket* old_media_socket = media_socket_;
media_socket_ = new QTcpSocket(this);
media_socket_->connectToHost(QHostAddress::LocalHost, req.request_.media_port());
media_socket_->setSocketOption(QAbstractSocket::LowDelayOption, true);
connect(media_socket_, SIGNAL(disconnected()), SLOT(MediaSocketDisconnected()));
if (old_media_socket) {
old_media_socket->close();
}
media_pipeline_.reset(new MediaPipeline(req.request_.media_port(),
sp_track_duration(req.track_)));
qLog(Info) << "Starting playback of uri" << req.request_.track_uri().c_str()
<< "to port" << req.request_.media_port();
// Set the track length - this will trigger MusicDeliveryCallback to send
// a WAVE header.
media_length_msec_ = sp_track_duration(req.track_);
// Start playback
sp_session_player_play(session_, true);
@ -899,22 +830,6 @@ void SpotifyClient::SendPlaybackError(const QString& error) {
handler_->SendMessage(message);
}
void SpotifyClient::MediaSocketDisconnected() {
QTcpSocket* socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) {
return;
}
qLog(Info) << "Media socket disconnected";
socket->deleteLater();
if (socket == media_socket_) {
sp_session_player_unload(session_);
media_socket_ = NULL;
}
}
void SpotifyClient::LoadImage(const QString& id_b64) {
QByteArray id = QByteArray::fromBase64(id_b64.toAscii());
if (id.length() != kSpotifyImageIDSize) {

View File

@ -32,6 +32,7 @@
class QTcpSocket;
class QTimer;
class MediaPipeline;
class ResponseMessage;
class SpotifyMessageHandler;
@ -50,7 +51,6 @@ public:
private slots:
void HandleMessage(const spotify_pb::SpotifyMessage& message);
void ProcessEvents();
void MediaSocketDisconnected();
private:
void SendLoginCompleted(bool success, const QString& error,
@ -72,9 +72,6 @@ private:
static void SP_CALLCONV OfflineStatusUpdatedCallback(sp_session* session);
static void SP_CALLCONV ConnectionErrorCallback(sp_session* session, sp_error error);
static void SP_CALLCONV UserMessageCallback(sp_session* session, const char* message);
static void SP_CALLCONV GetAudioBufferStatsCallback(
sp_session* session,
sp_audio_buffer_stats* stats);
static void SP_CALLCONV StartPlaybackCallback(sp_session* session);
static void SP_CALLCONV StopPlaybackCallback(sp_session* session);
@ -158,7 +155,6 @@ private:
QByteArray api_key_;
QTcpSocket* protocol_socket_;
QTcpSocket* media_socket_;
SpotifyMessageHandler* handler_;
sp_session_config spotify_config_;
@ -180,8 +176,7 @@ private:
QMap<sp_search*, QList<sp_albumbrowse*> > pending_search_album_browses_;
QMap<sp_albumbrowse*, sp_search*> pending_search_album_browse_responses_;
int media_length_msec_;
int byte_rate_;
QScopedPointer<MediaPipeline> media_pipeline_;
};
#endif // SPOTIFYCLIENT_H

View File

@ -1035,7 +1035,6 @@ endif(HAVE_BREAKPAD)
if(HAVE_SPOTIFY)
target_link_libraries(clementine_lib
clementine-spotifyblob-messages
gstspotifytcpsrc
${QCA_LIBRARIES}
)
link_directories(${QCA_LIBRARY_DIRS})

View File

@ -1,20 +1,23 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Copyright 2011, 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.
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
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.
http://www.apache.org/licenses/LICENSE-2.0
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
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.
#ifndef TIMECONSTANTS_H
#define TIMECONSTANTS_H

View File

@ -28,6 +28,7 @@
#include <QIODevice>
#include <QMouseEvent>
#include <QStringList>
#include <QTcpServer>
#include <QTemporaryFile>
#include <QUrl>
#include <QWidget>
@ -371,6 +372,17 @@ bool IsMouseEventInWidget(const QMouseEvent* e, const QWidget* widget) {
return widget->rect().contains(widget->mapFromGlobal(e->globalPos()));
}
quint16 PickUnusedPort() {
forever {
const quint16 port = 49152 + qrand() % 16384;
QTcpServer server;
if (server.listen(QHostAddress::Any, port)) {
return port;
}
}
}
} // namespace Utilities

View File

@ -62,6 +62,11 @@ namespace Utilities {
QByteArray Sha256(const QByteArray& data);
// Picks an unused ephemeral port number. Doesn't hold the port open so
// there's the obvious race condition
quint16 PickUnusedPort();
// Forwards a mouse event to a different widget, remapping the event's widget
// coordinates relative to those of the target widget.
void ForwardMouseEvent(const QMouseEvent* e, QWidget* target);

View File

@ -31,10 +31,6 @@
# include "gst/afcsrc/gstafcsrc.h"
#endif
#ifdef HAVE_SPOTIFY
# include "gst/spotifytcpsrc/gstspotifytcpsrc.h"
#endif
#include <math.h>
#include <unistd.h>
#include <vector>
@ -151,10 +147,6 @@ void GstEngine::InitialiseGstreamer() {
#ifdef HAVE_IMOBILEDEVICE
afcsrc_register_static();
#endif
#ifdef HAVE_SPOTIFY
spotifytcpsrc_register_static();
#endif
}
void GstEngine::ReloadSettings() {

View File

@ -22,6 +22,10 @@
#include "gstengine.h"
#include "gstenginepipeline.h"
#include "core/logging.h"
#include "core/utilities.h"
#include "internet/internetmodel.h"
#include "internet/spotifyserver.h"
#include "internet/spotifyservice.h"
#include <QtConcurrentRun>
@ -121,14 +125,43 @@ bool GstEnginePipeline::ReplaceDecodeBin(GstElement* new_bin) {
}
bool GstEnginePipeline::ReplaceDecodeBin(const QUrl& url) {
GstElement* new_bin = engine_->CreateElement("uridecodebin");
g_object_set(G_OBJECT(new_bin), "uri", url.toEncoded().constData(), NULL);
g_object_set(G_OBJECT(new_bin), "buffer-duration", buffer_duration_nanosec_, NULL);
g_object_set(G_OBJECT(new_bin), "download", true, NULL);
g_object_set(G_OBJECT(new_bin), "use-buffering", true, NULL);
g_signal_connect(G_OBJECT(new_bin), "drained", G_CALLBACK(SourceDrainedCallback), this);
g_signal_connect(G_OBJECT(new_bin), "pad-added", G_CALLBACK(NewPadCallback), this);
g_signal_connect(G_OBJECT(new_bin), "notify::source", G_CALLBACK(SourceSetupCallback), this);
GstElement* new_bin = NULL;
if (url.scheme() == "spotify") {
new_bin = gst_bin_new("spotify_bin");
// Create elements
GstElement* src = engine_->CreateElement("tcpserversrc", new_bin);
GstElement* gdp = engine_->CreateElement("gdpdepay", new_bin);
if (!src || !gdp)
return false;
// Pick a port number
const int port = Utilities::PickUnusedPort();
g_object_set(G_OBJECT(src), "host", "127.0.0.1", NULL);
g_object_set(G_OBJECT(src), "port", port, NULL);
// Link the elements
gst_element_link(src, gdp);
// Add a ghost pad
GstPad* pad = gst_element_get_static_pad(gdp, "src");
gst_element_add_pad(GST_ELEMENT(new_bin), gst_ghost_pad_new("src", pad));
gst_object_unref(GST_OBJECT(pad));
// Tell spotify to start sending data to us.
InternetModel::Service<SpotifyService>()->server()->StartPlaybackLater(url.toString(), port);
} else {
new_bin = engine_->CreateElement("uridecodebin");
g_object_set(G_OBJECT(new_bin), "uri", url.toEncoded().constData(), NULL);
g_object_set(G_OBJECT(new_bin), "buffer-duration", buffer_duration_nanosec_, NULL);
g_object_set(G_OBJECT(new_bin), "download", true, NULL);
g_object_set(G_OBJECT(new_bin), "use-buffering", true, NULL);
g_signal_connect(G_OBJECT(new_bin), "drained", G_CALLBACK(SourceDrainedCallback), this);
g_signal_connect(G_OBJECT(new_bin), "pad-added", G_CALLBACK(NewPadCallback), this);
g_signal_connect(G_OBJECT(new_bin), "notify::source", G_CALLBACK(SourceSetupCallback), this);
}
return ReplaceDecodeBin(new_bin);
}
@ -304,9 +337,24 @@ bool GstEnginePipeline::Init() {
gst_pad_add_buffer_probe(gst_element_get_pad(probe_converter, "src"), G_CALLBACK(HandoffCallback), this);
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallbackSync, this);
bus_cb_id_ = gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallback, this);
MaybeLinkDecodeToAudio();
return true;
}
void GstEnginePipeline::MaybeLinkDecodeToAudio() {
if (!uridecodebin_ || !audiobin_)
return;
GstPad* pad = gst_element_get_static_pad(uridecodebin_, "src");
if (!pad)
return;
gst_object_unref(pad);
gst_element_link(uridecodebin_, audiobin_);
}
bool GstEnginePipeline::InitFromString(const QString& pipeline) {
pipeline_ = gst_pipeline_new("pipeline");
@ -640,6 +688,7 @@ void GstEnginePipeline::TransitionToNext() {
ReplaceDecodeBin(next_url_);
gst_element_set_state(uridecodebin_, GST_STATE_PLAYING);
MaybeLinkDecodeToAudio();
url_ = next_url_;
end_offset_nanosec_ = next_end_offset_nanosec_;

View File

@ -135,6 +135,10 @@ class GstEnginePipeline : public QObject {
void TransitionToNext();
// If the decodebin is special (ie. not really a uridecodebin) then it'll have
// a src pad immediately and we can link it after everything's created.
void MaybeLinkDecodeToAudio();
private slots:
void FaderTimelineFinished();

View File

@ -16,6 +16,7 @@
*/
#include "spotifyserver.h"
#include "core/closure.h"
#include "core/logging.h"
#include "spotifyblob/common/spotifymessages.pb.h"
@ -23,6 +24,7 @@
#include <QTcpServer>
#include <QTcpSocket>
#include <QTimer>
SpotifyServer::SpotifyServer(QObject* parent)
: QObject(parent),
@ -205,6 +207,16 @@ void SpotifyServer::LoadUserPlaylist(int index) {
LoadPlaylist(spotify_pb::UserPlaylist, index);
}
void SpotifyServer::StartPlaybackLater(const QString& uri, quint16 port) {
QTimer* timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), timer, SLOT(deleteLater()));
timer->start(100); // lol
NewClosure(timer, SIGNAL(timeout()),
this, SLOT(StartPlayback(QString,quint16)),
uri, port);
}
void SpotifyServer::StartPlayback(const QString& uri, quint16 port) {
spotify_pb::SpotifyMessage message;
spotify_pb::PlaybackRequest* req = message.mutable_playback_request();

View File

@ -44,7 +44,7 @@ public:
void SyncInbox();
void LoadUserPlaylist(int index);
void SyncUserPlaylist(int index);
void StartPlayback(const QString& uri, quint16 port);
void StartPlaybackLater(const QString& uri, quint16 port);
void Search(const QString& text, int limit, int limit_album = 0);
void LoadImage(const QString& id);
void AlbumBrowse(const QString& uri);
@ -53,6 +53,7 @@ public:
int server_port() const;
public slots:
void StartPlayback(const QString& uri, quint16 port);
void Seek(qint64 offset_bytes);
signals:

View File

@ -44,12 +44,12 @@ msgstr ""
msgid "%1 albums"
msgstr ""
#: core/utilities.cpp:89
#: core/utilities.cpp:90
#, qt-format
msgid "%1 days"
msgstr ""
#: core/utilities.cpp:110
#: core/utilities.cpp:111
#, qt-format
msgid "%1 days ago"
msgstr ""
@ -211,7 +211,7 @@ msgstr ""
msgid "0:00:00"
msgstr ""
#: core/utilities.cpp:89
#: core/utilities.cpp:90
msgid "1 day"
msgstr ""
@ -4040,7 +4040,7 @@ msgstr ""
msgid "Title"
msgstr ""
#: core/utilities.cpp:106
#: core/utilities.cpp:107
msgid "Today"
msgstr ""
@ -4402,7 +4402,7 @@ msgstr ""
msgid "Years"
msgstr ""
#: core/utilities.cpp:108
#: core/utilities.cpp:109
msgid "Yesterday"
msgstr ""