summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--binding/CMakeLists.txt25
-rw-r--r--binding/radio-binding.c35
-rw-r--r--binding/radio_impl.h2
-rw-r--r--binding/radio_impl_kingfisher.c115
-rw-r--r--binding/radio_impl_rtlsdr.c8
-rw-r--r--binding/radio_output_gstreamer.c218
-rw-r--r--conf.d/cmake/config.cmake6
-rw-r--r--conf.d/wgt/config-4a.xml.in29
9 files changed, 382 insertions, 57 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b25c15b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*~
diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt
index d279945..c295eb0 100644
--- a/binding/CMakeLists.txt
+++ b/binding/CMakeLists.txt
@@ -26,6 +26,11 @@ PROJECT_TARGET_ADD(radio-binding)
radio-binding.c
radio_impl_kingfisher.c
radio_impl_rtlsdr.c)
+ if(HAVE_4A_FRAMEWORK)
+ add_definitions(-DHAVE_4A_FRAMEWORK)
+ endif()
+
+ PKG_CHECK_MODULES(SOUND REQUIRED gstreamer-1.0)
add_library(${TARGET_NAME} MODULE ${radio_SOURCES})
@@ -38,7 +43,8 @@ PROJECT_TARGET_ADD(radio-binding)
)
# Library dependencies (include updates automatically)
- TARGET_LINK_LIBRARIES(${TARGET_NAME} ${link_libraries})
+ TARGET_COMPILE_OPTIONS(${TARGET_NAME} PUBLIC ${SOUND_CFLAGS})
+ TARGET_LINK_LIBRARIES(${TARGET_NAME} ${SOUND_LIBRARIES} ${link_libraries})
# installation directory
INSTALL(TARGETS ${TARGET_NAME}
@@ -51,9 +57,16 @@ PROJECT_TARGET_ADD(rtl_fm_helper)
set(helper_SOURCES
${TARGET_NAME}.c
rtl_fm.c
- convenience/convenience.c
- radio_output.c)
- PKG_CHECK_MODULES(SOUND REQUIRED libpulse-simple)
+ convenience/convenience.c)
+ if(HAVE_4A_FRAMEWORK)
+ set(helper_SOURCES ${helper_SOURCES} radio_output_gstreamer.c)
+ PKG_CHECK_MODULES(helper_SOUND REQUIRED gstreamer-1.0)
+ else()
+ set(helper_SOURCES ${helper_SOURCES} radio_output_pulse.c)
+ PKG_CHECK_MODULES(helper_SOUND REQUIRED libpulse-simple)
+ endif()
+
+ PKG_CHECK_MODULES(helper_RTLSDR REQUIRED librtlsdr)
add_executable(${TARGET_NAME} ${helper_SOURCES})
@@ -62,6 +75,6 @@ PROJECT_TARGET_ADD(rtl_fm_helper)
OUTPUT_NAME ${TARGET_NAME}
)
- PKG_CHECK_MODULES(RTLSDR REQUIRED librtlsdr)
+ TARGET_COMPILE_OPTIONS(${TARGET_NAME} PUBLIC ${helper_SOUND_CFLAGS})
TARGET_LINK_LIBRARIES(${TARGET_NAME}
- ${RTLSDR_LIBRARIES} ${SOUND_LIBRARIES} ${link_libraries} m)
+ ${helper_RTLSDR_LIBRARIES} ${helper_SOUND_LIBRARIES} ${link_libraries} m)
diff --git a/binding/radio-binding.c b/binding/radio-binding.c
index 4a61011..a652ff1 100644
--- a/binding/radio-binding.c
+++ b/binding/radio-binding.c
@@ -467,22 +467,53 @@ static const struct afb_verb_v2 verbs[]= {
static int init()
{
int rc;
+ char *output = NULL;
+
+#ifdef HAVE_4A_FRAMEWORK
+ json_object *response;
+
+ json_object *jsonData = json_object_new_object();
+ json_object_object_add(jsonData, "audio_role", json_object_new_string("Radio"));
+ json_object_object_add(jsonData, "endpoint_type", json_object_new_string("sink"));
+ rc = afb_service_call_sync("ahl-4a", "stream_open", jsonData, &response);
+ if(!rc) {
+ json_object *valJson = NULL;
+ json_object *val = NULL;
+
+ rc = json_object_object_get_ex(response, "response", &valJson);
+ if(rc) {
+ rc = json_object_object_get_ex(valJson, "device_uri", &val);
+ if(rc) {
+ const char *jres_pcm = json_object_get_string(val);
+ char *p;
+ if((p = strchr(jres_pcm, ':'))) {
+ output = strdup(p + 1);
+ }
+ }
+ }
+ } else {
+ AFB_ERROR("afb_service_call_sync failed\n");
+ return rc;
+ }
+#endif
+ // Initialize event structures
freq_event = afb_daemon_make_event("frequency");
scan_event = afb_daemon_make_event("station_found");
// Look for RTL-SDR USB adapter
radio_impl_ops = &rtlsdr_impl_ops;
- rc = radio_impl_ops->init();
+ rc = radio_impl_ops->init(output);
if(rc != 0) {
// Look for Kingfisher Si4689
radio_impl_ops = &kf_impl_ops;
- rc = radio_impl_ops->init();
+ rc = radio_impl_ops->init(output);
}
if(rc == 0) {
printf("%s found\n", radio_impl_ops->name);
radio_impl_ops->set_frequency_callback(freq_callback, NULL);
}
+ free(output);
return rc;
}
diff --git a/binding/radio_impl.h b/binding/radio_impl.h
index 4b4a2f7..f745f12 100644
--- a/binding/radio_impl.h
+++ b/binding/radio_impl.h
@@ -41,7 +41,7 @@ typedef enum {
typedef struct {
char *name;
- int (*init)(void);
+ int (*init)(const char *output);
uint32_t (*get_frequency)(void);
diff --git a/binding/radio_impl_kingfisher.c b/binding/radio_impl_kingfisher.c
index ac7ec7f..3612424 100644
--- a/binding/radio_impl_kingfisher.c
+++ b/binding/radio_impl_kingfisher.c
@@ -22,6 +22,9 @@
#include <glib.h>
#include <fcntl.h>
#include <sys/stat.h>
+#include <gst/gst.h>
+#define AFB_BINDING_VERSION 2
+#include <afb/afb-binding.h>
#include "radio_impl.h"
@@ -29,6 +32,9 @@
#define SI_INIT "/usr/bin/si_init"
#define SI_CTL "/usr/bin/si_ctl"
+#define GST_SINK_OPT_LEN 128
+#define GST_PIPELINE_LEN 256
+
// Structure to describe FM band plans, all values in Hz.
typedef struct {
char *name;
@@ -47,19 +53,22 @@ static fm_band_plan_t known_fm_band_plans[5] = {
static unsigned int bandplan = 0;
static bool present;
-static bool active;
static uint32_t current_frequency;
static int scan_valid_snr_threshold = 128;
static int scan_valid_rssi_threshold = 128;
static bool scanning = false;
+// GStreamer state
+static GstElement *pipeline;
+static bool running;
+
static void (*freq_callback)(uint32_t, void*);
static void *freq_callback_data;
static uint32_t kf_get_min_frequency(radio_band_t band);
static void kf_scan_stop(void);
-static int kf_init(void)
+static int kf_init(const char *output)
{
GKeyFile* conf_file;
int conf_file_present = 0;
@@ -67,7 +76,8 @@ static int kf_init(void)
char *value_str;
char cmd[128];
int rc;
- char *output_sink;
+ char output_sink_opt[GST_SINK_OPT_LEN];
+ char gst_pipeline_str[GST_PIPELINE_LEN];
if(present)
return 0;
@@ -122,7 +132,7 @@ static int kf_init(void)
"scan_valid_snr_threshold",
&error);
if(!error) {
- fprintf(stderr, "Scan valid SNR level set to %d\n", n);
+ AFB_INFO("Scan valid SNR level set to %d", n);
scan_valid_snr_threshold = n;
}
@@ -132,7 +142,7 @@ static int kf_init(void)
"scan_valid_rssi_threshold",
&error);
if(!error) {
- fprintf(stderr, "Scan valid SNR level set to %d\n", n);
+ AFB_INFO("Scan valid SNR level set to %d", n);
scan_valid_rssi_threshold = n;
}
@@ -141,11 +151,11 @@ static int kf_init(void)
rc = system(SI_INIT);
if(rc != 0) {
- fprintf(stderr, "si_init failed, rc = %d", rc);
+ AFB_ERROR("si_init failed, rc = %d", rc);
return -1;
}
- fprintf(stderr, "Using FM Bandplan: %s\n", known_fm_band_plans[bandplan].name);
+ AFB_INFO("Using FM Bandplan: %s", known_fm_band_plans[bandplan].name);
current_frequency = kf_get_min_frequency(BAND_FM);
sprintf(cmd,
"%s /dev/i2c-11 0x65 -b fm -p %s -t %d -u %d -c %d",
@@ -156,39 +166,50 @@ static int kf_init(void)
current_frequency / 1000);
rc = system(cmd);
if(rc != 0) {
- fprintf(stderr, "%s failed, rc = %d", SI_CTL, rc);
+ AFB_ERROR("%s failed, rc = %d", SI_CTL, rc);
return -1;
}
- // Handle 4A disabling PA udev module
- if(system("pactl list sources short |grep -q alsa_input.radio") != 0) {
- // Set up radio source
- if(system("pactl load-module module-alsa-source device=hw:radio,0 name=radio") != 0) {
- fprintf(stderr, "radio PA source creation failed!\n");
+ // Initialize GStreamer
+ gst_init(NULL, NULL);
+
+ if(output) {
+ AFB_INFO("Using output device %s", output);
+ rc = snprintf(output_sink_opt, GST_SINK_OPT_LEN, " device=%s", output);
+ if(rc >= GST_SINK_OPT_LEN) {
+ AFB_ERROR("output device string too long");
return -1;
}
+ } else {
+ output_sink_opt[0] = '\0';
}
-
- // Set initial state to muted
- rc = system("pactl set-source-mute alsa_input.radio 1");
- if(rc != 0) {
- fprintf(stderr, "pactl failed, rc = %d", rc);
+ // NOTE: If muting without pausing is desired, it can likely be done
+ // by adding a "volume" element to the pipeline before the sink,
+ // and setting the volume to 0 to mute.
+#ifdef HAVE_4A_FRAMEWORK
+ rc = snprintf(gst_pipeline_str,
+ GST_PIPELINE_LEN,
+ "alsasrc device=hw:radio ! queue ! audioconvert ! audioresample ! alsasink%s",
+ output_sink_opt);
+#else
+ // Use PulseAudio output for compatibility with audiomanager / module_router
+ rc = snprintf(gst_pipeline_str,
+ GST_PIPELINE_LEN,
+ "alsasrc device=hw:radio ! queue ! audioconvert ! audioresample ! pulsesink stream-properties=\"props,media.role=radio\"");
+#endif
+ if(rc >= GST_PIPELINE_LEN) {
+ AFB_ERROR("pipeline string too long");
return -1;
}
-
- // Set up loopback to output sink
- output_sink = getenv("PULSE_SINK");
- if(!output_sink) {
- // On non-4A, loopback to the sink for the on-board Starter Kit M3/H3 audio
- output_sink = "1";
- }
- sprintf(cmd, "pactl load-module module-loopback source=alsa_input.radio sink=%s", output_sink);
- rc = system(cmd);
- if(rc != 0) {
- fprintf(stderr, "pactl failed, rc = %d", rc);
+ pipeline = gst_parse_launch(gst_pipeline_str, NULL);
+ if(!pipeline) {
+ AFB_ERROR("pipeline construction failed!");
return -1;
}
+ // Start pipeline in paused state
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
present = true;
return 0;
}
@@ -281,26 +302,28 @@ static void kf_start(void)
if(!present)
return;
- if(!active) {
- rc = system("pactl set-source-mute alsa_input.radio 0");
- if(rc != 0)
- fprintf(stderr, "pactl set-source-mute failed!\n");
- active = true;
+ if(!running) {
+ // Start pipeline
+ gst_element_set_state(pipeline, GST_STATE_PLAYING);
+ running = true;
}
}
static void kf_stop(void)
{
- int rc;
-
- if(!present)
- return;
-
- if(active) {
- active = false;
- rc = system("pactl set-source-mute alsa_input.radio 1");
- if(rc != 0)
- fprintf(stderr, "pactl set-source-mute failed!\n");
+ GstEvent *event;
+
+ if(present && running) {
+ // Stop pipeline
+ running = false;
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
+ // Flush pipeline
+ // This seems required to avoid stutters on starts after a stop
+ event = gst_event_new_flush_start();
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
+ event = gst_event_new_flush_stop(TRUE);
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
}
}
@@ -324,14 +347,14 @@ static void kf_scan_start(radio_scan_direction_t direction,
sprintf(cmd, "%s /dev/i2c-11 0x65 -l %s", SI_CTL, direction == SCAN_FORWARD ? "up" : "down");
fp = popen(cmd, "r");
if(fp == NULL) {
- fprintf(stderr, "Could not run: %s!\n", cmd);
+ AFB_ERROR("Could not run: %s!", cmd);
return;
}
// Look for "Frequency:" in output
while(fgets(line, 128, fp) != NULL) {
if(strncmp("Frequency:", line, 10) == 0) {
new_frequency = atoi(line + 10);
- //fprintf(stderr, "%s: got new_frequency = %d\n", __FUNCTION__, new_frequency);
+ //AFB_DEBUG("%s: got new_frequency = %d", __FUNCTION__, new_frequency);
break;
}
}
diff --git a/binding/radio_impl_rtlsdr.c b/binding/radio_impl_rtlsdr.c
index 7fd4d69..b8b2454 100644
--- a/binding/radio_impl_rtlsdr.c
+++ b/binding/radio_impl_rtlsdr.c
@@ -125,7 +125,7 @@ static pid_t popen2(char *command, int *in_fd, int *out_fd)
return pid;
}
-static int rtlsdr_init(void)
+static int rtlsdr_init(const char *output)
{
GKeyFile *conf_file;
char *value_str;
@@ -183,6 +183,12 @@ static int rtlsdr_init(void)
return -1;
}
+ if(output) {
+ // Indicate desired output to helper
+ AFB_INFO("Setting RADIO_OUTPUT=%s", output);
+ setenv("RADIO_OUTPUT", output, 1);
+ }
+
// Run helper
if(snprintf(helper_path, PATH_MAX, "%s/bin/%s", rootdir, HELPER_NAME) == PATH_MAX) {
AFB_ERROR("Could not create path to %s", HELPER_NAME);
diff --git a/binding/radio_output_gstreamer.c b/binding/radio_output_gstreamer.c
new file mode 100644
index 0000000..698b81b
--- /dev/null
+++ b/binding/radio_output_gstreamer.c
@@ -0,0 +1,218 @@
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <gst/gst.h>
+
+#include "radio_output.h"
+#include "rtl_fm.h"
+
+// Output buffer
+static unsigned int extra;
+static int16_t extra_buf[1];
+static unsigned char *output_buf;
+
+// GStreamer state
+static GstElement *pipeline, *appsrc;
+static bool running;
+
+int radio_output_open()
+{
+ unsigned int rate = 24000;
+ GstElement *queue, *convert, *sink, *resample;
+ char *p;
+
+ if(pipeline)
+ return 0;
+
+ // Initialize GStreamer
+#ifdef DEBUG
+ unsigned int argc = 2;
+ char **argv = malloc(2 * sizeof(char*));
+ argv[0] = strdup("test");
+ argv[1] = strdup("--gst-debug-level=5");
+ gst_init(&argc, &argv);
+#else
+ gst_init(NULL, NULL);
+#endif
+
+ // Setup pipeline
+ // NOTE: With our use of the simple buffer pushing mode, there seems to
+ // be no need for a mainloop, so currently not instantiating one.
+ pipeline = gst_pipeline_new("pipeline");
+ appsrc = gst_element_factory_make("appsrc", "source");
+ queue = gst_element_factory_make("queue", "queue");
+ convert = gst_element_factory_make("audioconvert", "convert");
+ resample = gst_element_factory_make("audioresample", "resample");
+ sink = gst_element_factory_make("alsasink", "sink");
+ if(!(pipeline && appsrc && queue && convert && resample && sink)) {
+ fprintf(stderr, "pipeline element construction failed!\n");
+ }
+ g_object_set(G_OBJECT(appsrc), "caps",
+ gst_caps_new_simple("audio/x-raw",
+ "format", G_TYPE_STRING, "S16LE",
+ "rate", G_TYPE_INT, rate,
+ "channels", G_TYPE_INT, 2,
+ "layout", G_TYPE_STRING, "interleaved",
+ "channel-mask", G_TYPE_UINT64, 3,
+ NULL), NULL);
+
+ if((p = getenv("RADIO_OUTPUT"))) {
+ fprintf(stderr, "Using output device %s\n", p);
+ g_object_set(sink, "device", p, NULL);
+ }
+ gst_bin_add_many(GST_BIN(pipeline), appsrc, queue, convert, resample, sink, NULL);
+ gst_element_link_many(appsrc, queue, convert, resample, sink, NULL);
+ //gst_bin_add_many(GST_BIN(pipeline), appsrc, convert, resample, sink, NULL);
+ //gst_element_link_many(appsrc, convert, resample, sink, NULL);
+
+ // Set up appsrc
+ // NOTE: Radio seems like it matches the use case the "is-live" property
+ // is for, but setting it seems to require a lot more work with
+ // respect to latency settings to make the pipeline work smoothly.
+ // For now, leave it unset since the result seems to work
+ // reasonably well.
+ g_object_set(G_OBJECT(appsrc),
+ "stream-type", 0,
+ "format", GST_FORMAT_TIME,
+ NULL);
+
+ // Start pipeline in paused state
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
+ // Set up output buffer
+ extra = 0;
+ output_buf = malloc(sizeof(unsigned char) * RTL_FM_MAXIMUM_BUF_LENGTH);
+
+ return 0;
+}
+
+int radio_output_start(void)
+{
+ int rc = 0;
+
+ if(!pipeline) {
+ rc = radio_output_open();
+ if(rc)
+ return rc;
+ }
+
+ // Start pipeline
+ running = true;
+ gst_element_set_state(pipeline, GST_STATE_PLAYING);
+
+ return rc;
+}
+
+void radio_output_stop(void)
+{
+ GstEvent *event;
+
+ if(pipeline && running) {
+ // Stop pipeline
+ running = false;
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
+ // Flush pipeline
+ // This seems required to avoid stutters on starts after a stop
+ event = gst_event_new_flush_start();
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
+ event = gst_event_new_flush_stop(TRUE);
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
+ }
+}
+
+void radio_output_suspend(int state)
+{
+ // Placeholder
+}
+
+void radio_output_close(void)
+{
+ radio_output_stop();
+
+ if(pipeline) {
+ // Tear down pipeline
+ gst_element_set_state(pipeline, GST_STATE_NULL);
+ gst_object_unref(GST_OBJECT(pipeline));
+ pipeline = NULL;
+ running = false;
+ }
+
+ free(output_buf);
+ output_buf = NULL;
+}
+
+int radio_output_write(void *buf, int len)
+{
+ int rc = -EINVAL;
+ size_t n = len;
+ int samples = len / 2;
+ unsigned char *p;
+ GstBuffer *buffer;
+ GstFlowReturn ret;
+
+ if(!(pipeline && buf)) {
+ return rc;
+ }
+
+ // Don't bother pushing samples if output hasn't started
+ if(!running)
+ return 0;
+
+ /*
+ * Handle the rtl_fm code giving us an odd number of samples.
+ * This extra buffer copying approach is not particularly efficient,
+ * but works for now. It looks feasible to hack in something in the
+ * demod and output thread routines in rtl_fm.c to handle it there
+ * if more performance is required.
+ */
+ p = output_buf;
+ if(extra) {
+ memcpy(output_buf, extra_buf, sizeof(int16_t));
+ if((extra + samples) % 2) {
+ // We still have an extra sample, n remains the same, store the extra
+ memcpy(output_buf + sizeof(int16_t), buf, n - 2);
+ memcpy(extra_buf, ((unsigned char*) buf) + n - 2, sizeof(int16_t));
+ } else {
+ // We have an even number of samples, no extra
+ memcpy(output_buf + sizeof(int16_t), buf, n);
+ n += 2;
+ extra = 0;
+ }
+ } else if(samples % 2) {
+ // We have an extra sample, store it, and decrease n
+ n -= 2;
+ memcpy(output_buf + sizeof(int16_t), buf, n);
+ memcpy(extra_buf, ((unsigned char*) buf) + n, sizeof(int16_t));
+ extra = 1;
+ } else {
+ p = buf;
+ }
+
+ // Push buffer into pipeline
+ buffer = gst_buffer_new_allocate(NULL, n, NULL);
+ gst_buffer_fill(buffer, 0, p, n);
+ g_signal_emit_by_name(appsrc, "push-buffer", buffer, &ret);
+ gst_buffer_unref(buffer);
+ rc = n;
+
+ return rc;
+}
diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake
index 4fa7666..86588d4 100644
--- a/conf.d/cmake/config.cmake
+++ b/conf.d/cmake/config.cmake
@@ -91,7 +91,11 @@ set(LD_LIBRARY_PATH ${CMAKE_INSTALL_PREFIX}/lib64 ${CMAKE_INSTALL_PREFIX}/lib)
# Optional location for config.xml.in
# -----------------------------------
-set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config.xml.in)
+if(HAVE_4A_FRAMEWORK)
+ set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config-4a.xml.in)
+else()
+ set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config.xml.in)
+endif()
# Mandatory widget Mimetype specification of the main unit
# --------------------------------------------------------------------------
diff --git a/conf.d/wgt/config-4a.xml.in b/conf.d/wgt/config-4a.xml.in
new file mode 100644
index 0000000..42ad037
--- /dev/null
+++ b/conf.d/wgt/config-4a.xml.in
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<widget xmlns="http://www.w3.org/ns/widgets" id="@PROJECT_NAME@" version="@PROJECT_VERSION@">
+ <name>@PROJECT_NAME@</name>
+ <icon src="@PROJECT_ICON@"/>
+ <content src="@WIDGET_ENTRY_POINT@" type="@WIDGET_TYPE@"/>
+ <description>@PROJECT_DESCRIPTION@</description>
+ <author>@PROJECT_AUTHOR@ &lt;@PROJECT_AUTHOR_MAIL@&gt;</author>
+ <license>@PROJECT_LICENSE@</license>
+
+ <feature name="urn:AGL:widget:required-permission">
+ <param name="urn:AGL:permission::public:hidden" value="required" />
+ <param name="urn:AGL:permission::public:no-htdocs" value="required" />
+ <param name="urn:AGL:permission:audio:public:audiostream" value="required" />
+ </feature>
+
+ <feature name="urn:AGL:widget:file-properties">
+ <param name="bin/rtl_fm_helper" value="executable" />
+ </feature>
+
+ <feature name="urn:AGL:widget:provided-api">
+ <param name="radio" value="ws" />
+ </feature>
+
+ <feature name="urn:AGL:widget:required-api">
+ <param name="ahl-4a" value="ws" />
+ <param name="@WIDGET_ENTRY_POINT@" value="local" />
+ </feature>
+
+</widget>