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:
parent
e9d770a864
commit
22e6a649b7
@ -381,7 +381,6 @@ if(HAVE_BREAKPAD)
|
||||
endif(HAVE_BREAKPAD)
|
||||
|
||||
if(HAVE_SPOTIFY)
|
||||
add_subdirectory(gst/spotifytcpsrc)
|
||||
add_subdirectory(spotifyblob/common)
|
||||
endif(HAVE_SPOTIFY)
|
||||
|
||||
|
@ -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}
|
||||
)
|
@ -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/");
|
||||
}
|
||||
|
@ -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
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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) {
|
||||
|
128
spotifyblob/blob/mediapipeline.cpp
Normal file
128
spotifyblob/blob/mediapipeline.cpp
Normal 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_);
|
||||
}
|
54
spotifyblob/blob/mediapipeline.h
Normal file
54
spotifyblob/blob/mediapipeline.h
Normal 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
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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_;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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:
|
||||
|
@ -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 ""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user