summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2019-01-16 19:44:28 -0500
committerScott Murray <scott.murray@konsulko.com>2019-01-17 21:47:28 -0500
commitfc00704eafd27305b9ca7e842b0321e0007d3154 (patch)
tree33920f5d07a122474d5187a41575627e4c4d3c2b
parent9fcf22b389c92f3952769ae73750112d1417e5dc (diff)
Replace QtMultimedia usage for ringtone playing with a gstreamer pipeline that uses the provided 4A role ALSA device for output. For now, a "phone" role is assumed to be available, but it does not exist in the current set of 4A policy and HALs, and needs to be added. Testing was done by making the required role changes locally and using some debug QML tweaks to allow triggering the ringtone manually. Bug-AGL: SPEC-1596 Change-Id: I55c2229de1bc5470ee818e5be382b64664fa2d29 Signed-off-by: Scott Murray <scott.murray@konsulko.com>
-rw-r--r--app/Dialer.qml1
-rw-r--r--app/app.pri6
-rw-r--r--app/app.pro11
-rw-r--r--app/fileplayer.cpp290
-rw-r--r--app/fileplayer.h62
-rw-r--r--app/main.cpp7
-rw-r--r--app/phone.cpp19
-rw-r--r--app/phone.h6
-rw-r--r--package/config.xml2
9 files changed, 382 insertions, 22 deletions
diff --git a/app/Dialer.qml b/app/Dialer.qml
index 9dc91db..248bc6c 100644
--- a/app/Dialer.qml
+++ b/app/Dialer.qml
@@ -18,7 +18,6 @@
import QtQuick 2.6
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.0
-import QtMultimedia 5.5
import AGL.Demo.Controls 1.0
Item {
diff --git a/app/app.pri b/app/app.pri
index c3b1fd1..7a60d44 100644
--- a/app/app.pri
+++ b/app/app.pri
@@ -11,3 +11,9 @@ config_libhomescreen {
}
DESTDIR = $${OUT_PWD}/../package/root/bin
+
+copy_ringtone.target = $$DESTDIR/Phone.wav
+copy_ringtone.depends = $$_PRO_FILE_PWD_/Phone.wav
+copy_ringtone.commands = $(COPY_FILE) \"$$replace(copy_ringtone.depends, /, $$QMAKE_DIR_SEP)\" \"$$replace(copy_ringtone.target, /, $$QMAKE_DIR_SEP)\"
+QMAKE_EXTRA_TARGETS += copy_ringtone
+PRE_TARGETDEPS += $$copy_ringtone.target
diff --git a/app/app.pro b/app/app.pro
index d1f19d6..2a6a48b 100644
--- a/app/app.pro
+++ b/app/app.pro
@@ -1,15 +1,14 @@
TARGET = phone
-QT = quickcontrols2 websockets multimedia
+QT = quickcontrols2 websockets
-SOURCES = main.cpp phone.cpp
-HEADERS = phone.h numbertype.h
+SOURCES = main.cpp phone.cpp fileplayer.cpp
+HEADERS = phone.h numbertype.h fileplayer.h
CONFIG += link_pkgconfig
-PKGCONFIG += libhomescreen qlibwindowmanager qtappfw
+PKGCONFIG += libhomescreen qlibwindowmanager qtappfw libafbwsc gstreamer-1.0
RESOURCES += \
phone.qrc \
- images/images.qrc \
- audio.qrc
+ images/images.qrc
include(app.pri)
diff --git a/app/fileplayer.cpp b/app/fileplayer.cpp
new file mode 100644
index 0000000..e26a3e6
--- /dev/null
+++ b/app/fileplayer.cpp
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2018 Konsulko Group
+ *
+ * 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.
+ */
+
+#include "fileplayer.h"
+#include <string>
+#include <cstring>
+#include <iostream>
+#include <mutex>
+#include <condition_variable>
+#include <json-c/json.h>
+
+#undef DEBUG
+
+struct set_role_data
+{
+ bool state;
+ std::string output;
+ std::condition_variable cv;
+};
+
+static void on_hangup(void *closure, struct afb_wsj1 *wsj)
+{
+}
+
+static void on_call(void *closure, const char *api, const char *verb, struct afb_wsj1_msg *msg)
+{
+}
+
+static void on_event(void* closure, const char* event, struct afb_wsj1_msg *msg)
+{
+}
+
+static void on_reply(void *closure, struct afb_wsj1_msg *msg)
+{
+ struct set_role_data *data = (struct set_role_data*) closure;
+ struct json_object* reply;
+
+ if(!(data && data->state))
+ goto reply_done;
+
+ // We opened the role, return the output
+ reply = afb_wsj1_msg_object_j(msg);
+ if(reply) {
+#ifdef DEBUG
+ std::cerr << __FUNCTION__ << ": reply = " << \
+ json_object_to_json_string_ext(reply, JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_PRETTY) << \
+ std::endl;
+#endif
+ struct json_object* response;
+ int rc = json_object_object_get_ex(reply, "response", &response);
+ if(rc) {
+ struct json_object* val;
+ rc = json_object_object_get_ex(response, "device_uri", &val);
+ if (rc && json_object_get_string_len(val)) {
+ const char* jres_pcm = json_object_get_string(val);
+ data->output = jres_pcm;
+#ifdef DEBUG
+ std::cerr << __FUNCTION__ << ": output = " << jres_pcm << std::endl;
+#endif
+ }
+ }
+ }
+reply_done:
+ // Signal reply is done
+ data->cv.notify_one();
+}
+
+static void *afb_loop_thread(struct sd_event* loop)
+{
+ for(;;)
+ sd_event_run(loop, 30000000);
+}
+
+static void *gst_loop_thread(GMainLoop *loop)
+{
+ if(loop)
+ g_main_loop_run(loop);
+}
+
+FilePlayer::FilePlayer(const int port, const std::string &token, const std::string &path, const std::string &role) :
+ m_path(path),
+ m_role(role)
+{
+ std::string uri;
+
+ if(sd_event_new(&m_afb_loop) < 0) {
+ std::cerr << __FUNCTION__ << ": Failed to create event loop" << std::endl;
+ return;
+ }
+
+ // Initialize interface for websocket
+ m_itf.on_hangup = on_hangup;
+ m_itf.on_call = on_call;
+ m_itf.on_event = on_event;
+
+ uri = "ws://localhost:" + std::to_string(port) + "/api?token=" + token;
+#ifdef DEBUG
+ std::cerr << "Using URI: " << uri << std::endl;
+#endif
+ m_ws = afb_ws_client_connect_wsj1(m_afb_loop, uri.c_str(), &m_itf, NULL);
+ if(m_ws) {
+ m_afb_thread = std::thread(afb_loop_thread, m_afb_loop);
+ } else {
+ std::cerr << __FUNCTION__ << ": Failed to create websocket connection" << std::endl;
+ goto error;
+ }
+
+ // Initialize GStreamer
+ gst_init(NULL, NULL);
+
+ // Create elements we need
+ m_playbin = gst_element_factory_make("playbin", "play");
+ m_alsa_sink = gst_element_factory_make("alsasink", NULL);
+ if(!(m_playbin && m_alsa_sink))
+ goto error;
+
+ // Set up bus callback
+ m_bus = gst_pipeline_get_bus(GST_PIPELINE(m_playbin));
+ if(!m_bus)
+ goto error;
+ m_gst_loop = g_main_loop_new(NULL, FALSE);
+ if(!m_gst_loop)
+ goto error;
+ gst_bus_add_watch(m_bus, gstreamer_bus_callback, this);
+
+ // Start thread to run glib main loop for gstreamer bus
+ m_gst_thread = std::thread(gst_loop_thread, m_gst_loop);
+
+ m_valid = true;
+ return;
+error:
+ gst_object_unref(m_playbin);
+ m_playbin = nullptr;
+ gst_object_unref(m_alsa_sink);
+ m_alsa_sink = nullptr;
+ gst_object_unref(m_bus);
+ m_bus = nullptr;
+
+ if(m_gst_loop) {
+ g_main_loop_quit(m_gst_loop);
+ m_gst_loop = nullptr;
+ }
+ if(m_afb_loop) {
+ sd_event_unref(m_afb_loop);
+ m_afb_loop = nullptr;
+ }
+ return;
+}
+
+FilePlayer::~FilePlayer(void)
+{
+ gst_element_set_state(m_playbin, GST_STATE_NULL);
+ gst_object_unref(m_playbin);
+ gst_object_unref(m_alsa_sink);
+ gst_object_unref(m_bus);
+ sd_event_unref(m_afb_loop);
+}
+
+void FilePlayer::play(bool loop)
+{
+ std::string output;
+
+ if(!m_valid || m_playing)
+ return;
+
+ if(set_role_state(true, &output) != 0)
+ return;
+
+ if(output.empty())
+ return;
+
+ m_playing = true;
+ m_looping = loop;
+
+ g_object_set(m_alsa_sink, "device", output.c_str(), NULL);
+ g_object_set(m_playbin, "audio-sink", m_alsa_sink, NULL);
+ std::string uri = "file://" + m_path;
+ g_object_set(m_playbin, "uri", uri.c_str(), NULL);
+
+ // Start playback
+ gst_element_set_state(m_playbin, GST_STATE_PLAYING);
+
+ return;
+}
+
+void FilePlayer::stop(void)
+{
+ if(!(m_valid && m_playing))
+ return;
+
+ // Stop playback
+ gst_element_set_state(m_playbin, GST_STATE_PAUSED);
+}
+
+gboolean FilePlayer::gstreamer_bus_callback(GstBus *bus, GstMessage *msg, gpointer data)
+{
+ if(!data)
+ return TRUE;
+
+ return static_cast<FilePlayer*>(data)->bus_callback(bus, msg);
+}
+
+int FilePlayer::set_role_state(bool state, std::string *output)
+{
+ if(!m_valid)
+ return -1;
+
+ set_role_data data;
+ data.state = state;
+ json_object *jsonData = json_object_new_object();
+ json_object_object_add(jsonData, "action", json_object_new_string(state ? "open" : "close"));
+ int rc = afb_wsj1_call_j(m_ws, "ahl-4a", m_role.c_str(), jsonData, on_reply, (void*) &data);
+ if(rc >= 0) {
+ // Wait for response
+ std::mutex m;
+ std::unique_lock<std::mutex> lk(m);
+ data.cv.wait(lk);
+ if(state && output)
+ *output = data.output;
+ } else {
+ std::cerr << __FUNCTION__ << ": Failed to call ahl-4a/" << m_role.c_str() << std::endl;
+ }
+ return rc;
+}
+
+gboolean FilePlayer::bus_callback(GstBus *bus, GstMessage *msg)
+{
+ switch (GST_MESSAGE_TYPE(msg)) {
+ case GST_MESSAGE_EOS:
+#ifdef DEBUG
+ std::cerr << __FUNCTION__ << ": GST_MESSAGE_EOS" << std::endl;
+#endif
+ if(m_looping) {
+ // restart playback if at end
+ if (!gst_element_seek(m_playbin,
+ 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
+ GST_SEEK_TYPE_SET, 0,
+ GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) {
+ std::cerr << "Seek failed!" << std::endl;
+ }
+ } else {
+ // Move stream to paused state since we're done
+ gst_element_set_state(m_playbin, GST_STATE_PAUSED);
+ }
+ break;
+ case GST_MESSAGE_STATE_CHANGED: {
+ if((GstElement*) GST_MESSAGE_SRC(msg) != m_playbin)
+ break;
+
+ GstState old_state, new_state;
+ gst_message_parse_state_changed(msg, &old_state, &new_state, NULL);
+#ifdef DEBUG
+ std::cerr << __FUNCTION__ << ": GST_MESSAGE_STATE_CHANGE: " << (int) old_state << " to " << (int) new_state << std::endl;
+#endif
+ if(old_state == GST_STATE_PLAYING && new_state == GST_STATE_PAUSED) {
+ set_role_state(false);
+
+ // Seek back to beginning so any subsequent play starts there
+ if (!gst_element_seek(m_playbin,
+ 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
+ GST_SEEK_TYPE_SET, 0,
+ GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) {
+ std::cerr << "Seek failed!" << std::endl;
+ }
+
+ m_playing = false;
+ } else if(old_state == GST_STATE_READY && new_state == GST_STATE_NULL) {
+ // clean up
+ g_main_loop_quit(m_gst_loop);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return TRUE;
+}
diff --git a/app/fileplayer.h b/app/fileplayer.h
new file mode 100644
index 0000000..5f52370
--- /dev/null
+++ b/app/fileplayer.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 Konsulko Group
+ *
+ * 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.
+ */
+
+#ifndef FILEPLAYER_H
+#define FILEPLAYER_H
+
+#include <string>
+#include <thread>
+#include <gst/gst.h>
+
+extern "C"
+{
+#include <afb/afb-wsj1.h>
+#include <afb/afb-ws-client.h>
+#include <systemd/sd-event.h>
+}
+
+class FilePlayer
+{
+public:
+ FilePlayer(int port, const std::string &token, const std::string &path, const std::string &role);
+ ~FilePlayer();
+
+ void play(bool loop = false);
+ void stop(void);
+
+ static gboolean gstreamer_bus_callback(GstBus *bus, GstMessage *msg, gpointer data);
+
+private:
+ std::string m_path;
+ std::string m_role;
+ struct afb_wsj1 *m_ws = nullptr;
+ struct afb_wsj1_itf m_itf;
+ std::thread m_afb_thread;
+ sd_event *m_afb_loop = nullptr;
+ std::thread m_gst_thread;
+ GMainLoop *m_gst_loop = nullptr;
+ GstElement *m_playbin = nullptr;
+ GstElement *m_alsa_sink = nullptr;
+ GstBus *m_bus = nullptr;
+ bool m_valid = false;
+ bool m_playing = false;
+ bool m_looping = false;
+
+ int set_role_state(bool state, std::string *output = nullptr);
+ gboolean bus_callback(GstBus *bus, GstMessage *msg);
+};
+
+#endif // FILEPLAYER_H
diff --git a/app/main.cpp b/app/main.cpp
index 71868d8..91580a5 100644
--- a/app/main.cpp
+++ b/app/main.cpp
@@ -30,6 +30,7 @@
#include <telephony.h>
#include "phone.h"
#include "numbertype.h"
+#include "fileplayer.h"
int main(int argc, char *argv[])
{
@@ -63,11 +64,13 @@ int main(int argc, char *argv[])
context->setContextProperty(QStringLiteral("bindingAddress"), bindingAddress);
Telephony *telephony = new Telephony(bindingAddress);
context->setContextProperty("telephony", telephony);
- Phone *phone = new Phone(telephony);
+ std::string token = secret.toStdString();
+ std::string install_dir = getenv("AFM_APP_INSTALL_DIR");
+ FilePlayer *player = new FilePlayer(port, token, install_dir + "/bin/Phone.wav", std::string("phone"));
+ Phone *phone = new Phone(telephony, player);
context->setContextProperty("phone", phone);
QObject::connect(telephony, &Telephony::callStateChanged, phone, &Phone::onCallStateChanged);
context->setContextProperty("pbap", new Pbap(bindingAddress, context));
- std::string token = secret.toStdString();
LibHomeScreen* hs = new LibHomeScreen();
QLibWindowmanager* qwm = new QLibWindowmanager();
diff --git a/app/phone.cpp b/app/phone.cpp
index 254fe24..8e8edcd 100644
--- a/app/phone.cpp
+++ b/app/phone.cpp
@@ -16,18 +16,14 @@
#include <QDebug>
#include <QObject>
-#include <QSoundEffect>
#include <QTimer>
#include <telephony.h>
#include "phone.h"
-Phone::Phone(Telephony *telephony, QObject *parent) :
- QObject(parent)
+Phone::Phone(Telephony *telephony, FilePlayer *player, QObject *parent) :
+ QObject(parent),
+ m_ringtone(player)
{
- m_ringtone.setSource(QUrl("qrc:./Phone.wav"));
- m_ringtone.setVolume(0.5f);
- m_ringtone.setLoopCount(QSoundEffect::Infinite);
-
QObject::connect(telephony, &Telephony::callStateChanged, this, &Phone::onCallStateChanged);
m_call_timer.setInterval(1000);
@@ -38,15 +34,18 @@ Phone::Phone(Telephony *telephony, QObject *parent) :
void Phone::onCallStateChanged(QString callState)
{
if (callState == "disconnected") {
- m_ringtone.stop();
+ if (m_ringtone)
+ m_ringtone->stop();
m_call_timer.stop();
} else if (callState == "active") {
- m_ringtone.stop();
+ if (m_ringtone)
+ m_ringtone->stop();
m_date_time = m_date_time.fromSecsSinceEpoch(0);
setElapsedTime("00:00:00");
m_call_timer.start();
} else if (callState == "incoming") {
- m_ringtone.play();
+ if (m_ringtone)
+ m_ringtone->play(true);
}
}
diff --git a/app/phone.h b/app/phone.h
index 489f1da..6cdce34 100644
--- a/app/phone.h
+++ b/app/phone.h
@@ -17,10 +17,10 @@
#ifndef PHONE_H
#define PHONE_H
-#include <QSoundEffect>
#include <QTimer>
#include <telephony.h>
+#include "fileplayer.h"
class Phone : public QObject
{
@@ -28,7 +28,7 @@ class Phone : public QObject
Q_PROPERTY(QString elapsedTime READ elapsedTime WRITE setElapsedTime NOTIFY elapsedTimeChanged)
public:
- explicit Phone(Telephony *telephony, QObject *parent = Q_NULLPTR);
+ explicit Phone(Telephony *telephony, FilePlayer *player, QObject *parent = Q_NULLPTR);
void onCallStateChanged(QString);
QString elapsedTime() { return m_elapsed_time; }
@@ -46,7 +46,7 @@ class Phone : public QObject
private:
Telephony *m_telephony;
- QSoundEffect m_ringtone;
+ FilePlayer *m_ringtone;
QTimer m_call_timer;
QDateTime m_date_time;
QString m_elapsed_time;
diff --git a/package/config.xml b/package/config.xml
index d32b0e3..893be6d 100644
--- a/package/config.xml
+++ b/package/config.xml
@@ -11,10 +11,12 @@
<param name="homescreen" value="ws" />
<param name="telephony" value="ws" />
<param name="bluetooth-pbap" value="ws" />
+ <param name="ahl-4a" value="ws" />
</feature>
<feature name="urn:AGL:widget:required-permission">
<param name="urn:AGL:permission::public:no-htdocs" value="required" />
<param name="http://tizen.org/privilege/internal/dbus" value="required" />
+ <param name="urn:AGL:permission:audio:public:audiostream" value="required" />
</feature>
</widget>