From f89d61bd03465265b6ddf4f75032705f3021a77a Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Fri, 7 Jul 2023 08:56:28 +0900 Subject: [PATCH] Update to audioplayers v4 --- include/plugins/audioplayers.h | 2 + src/plugins/audioplayers/player.c | 370 ++++++++++++++++++++---------- src/plugins/audioplayers/plugin.c | 73 +++++- 3 files changed, 315 insertions(+), 130 deletions(-) diff --git a/include/plugins/audioplayers.h b/include/plugins/audioplayers.h index c5a3c547..fb54584a 100644 --- a/include/plugins/audioplayers.h +++ b/include/plugins/audioplayers.h @@ -30,6 +30,8 @@ void audio_player_set_volume(struct audio_player *self, double volume); void audio_player_set_playback_rate(struct audio_player *self, double rate); +void audio_player_set_balance(struct audio_player *self, double balance); + void audio_player_set_position(struct audio_player *self, int64_t position); void audio_player_set_source_url(struct audio_player *self, char *url); diff --git a/src/plugins/audioplayers/player.c b/src/plugins/audioplayers/player.c index c2c44d8f..bc07ceb8 100644 --- a/src/plugins/audioplayers/player.c +++ b/src/plugins/audioplayers/player.c @@ -5,6 +5,7 @@ #include "gst/gstmessage.h" #include "gst/gstsegment.h" #include "platformchannel.h" +#include "pluginregistry.h" #include #include @@ -12,17 +13,24 @@ FILE_DESCR("audioplayers player") struct audio_player { + GstElement *source; GstElement *playbin; GstBus *bus; + GstElement *panorama; + GstElement *audiobin; + GstElement *audiosink; + GstPad *panoramaSinkPad; + bool is_initialized; + bool is_playing; bool is_looping; bool is_seek_completed; double playback_rate; char *url; char *player_id; - char *channel; + char event_channel_name[128]; }; // Private Class functions @@ -31,6 +39,7 @@ static gboolean audio_player_on_refresh(struct audio_player *data); static void audio_player_set_playback(struct audio_player *self, int64_t seekTo, double rate); static void audio_player_on_media_error(struct audio_player *self, GError *error, gchar *debug); static void audio_player_on_media_state_change(struct audio_player *self, GstObject *src, GstState *old_state, GstState *new_state); +static void audio_player_on_prepared(struct audio_player *self, bool value); static void audio_player_on_position_update(struct audio_player *self); static void audio_player_on_duration_update(struct audio_player *self); static void audio_player_on_seek_completed(struct audio_player *self); @@ -57,6 +66,31 @@ static int on_bus_fd_ready(sd_event_source *src, int fd, uint32_t revents, void return 0; } +static void audio_player_source_setup(GstElement *playbin, GstElement *source, GstElement **p_src) { + (void)(playbin); + (void)(p_src); + + if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "ssl-strict") != 0) { + g_object_set(G_OBJECT(source), "ssl-strict", FALSE, NULL); + } +} + +static int on_receive_event_ch(char *channel, struct platch_obj *object, FlutterPlatformMessageResponseHandle *responsehandle) { + //TODO: Do we need any more setup to send events to dart code? + (void)channel; + if (strcmp(object->method, "listen") == 0) { + LOG_DEBUG("%s: listen()\n", channel); + platch_respond_success_std(responsehandle, NULL); + } else if (strcmp(object->method, "cancel") == 0) { + LOG_DEBUG("%s: cancel()\n", channel); + platch_respond_success_std(responsehandle, NULL); + } else { + return platch_respond_not_implemented(responsehandle); + } + + return 0; +} + struct audio_player *audio_player_new(char *player_id, char *channel) { GPollFD fd; sd_event_source *busfd_event_source; @@ -68,17 +102,42 @@ struct audio_player *audio_player_new(char *player_id, char *channel) { self->url = NULL; self->is_initialized = false; + self->is_playing = false; self->is_looping = false; self->is_seek_completed = false; self->playback_rate = 1.0; gst_init(NULL, NULL); - self->playbin = gst_element_factory_make("playbin", "playbin"); + self->playbin = gst_element_factory_make("playbin", NULL); if (!self->playbin) { LOG_ERROR("Could not create gstreamer playbin.\n"); goto deinit_self; } + // Setup stereo balance controller + self->panorama = gst_element_factory_make("audiopanorama", NULL); + if (self->panorama) { + self->audiobin = gst_bin_new(NULL); + self->audiosink = gst_element_factory_make("autoaudiosink", NULL); + + gst_bin_add_many(GST_BIN(self->audiobin), self->panorama, self->audiosink, NULL); + gst_element_link(self->panorama, self->audiosink); + + GstPad *sinkpad = gst_element_get_static_pad(self->panorama, "sink"); + self->panoramaSinkPad = gst_ghost_pad_new("sink", sinkpad); + gst_element_add_pad(self->audiobin, self->panoramaSinkPad); + gst_object_unref(GST_OBJECT(sinkpad)); + + g_object_set(G_OBJECT(self->playbin), "audio-sink", self->audiobin, NULL); + g_object_set(G_OBJECT(self->panorama), "method", 1, NULL); + } else { + self->audiobin = NULL; + self->audiosink = NULL; + self->panoramaSinkPad = NULL; + } + + g_signal_connect(self->playbin, "source-setup", G_CALLBACK(audio_player_source_setup), &self->source); + self->bus = gst_element_get_bus(self->playbin); gst_bus_get_pollfd(self->bus, &fd); @@ -89,12 +148,31 @@ struct audio_player *audio_player_new(char *player_id, char *channel) { g_timeout_add(1000, (GSourceFunc) audio_player_on_refresh, self); self->player_id = strdup(player_id); - if (self->player_id == NULL) + if (self->player_id == NULL) { goto deinit_player; + } - self->channel = strdup(channel); - if (self->channel == NULL) + // audioplayers player event channel clang: + // /events/ + int ok = snprintf( + self->event_channel_name, + sizeof(self->event_channel_name), + "%s/events/%s", + channel, + player_id + ); + DEBUG_ASSERT_MSG(ok < sizeof(self->event_channel_name), "event channel name overflow"); + + // set a receiver on the videoEvents event channel + ok = plugin_registry_set_receiver( + self->event_channel_name, + kStandardMethodCall, + on_receive_event_ch + ); + + if (ok != 0) { goto deinit_player_id; + } return self; @@ -103,9 +181,21 @@ struct audio_player *audio_player_new(char *player_id, char *channel) { free(self->player_id); deinit_player: - free(self->channel); - gst_object_unref(self->bus); + + if (self->panorama != NULL) { + gst_element_set_state(self->audiobin, GST_STATE_NULL); + + gst_element_remove_pad(self->audiobin, self->panoramaSinkPad); + gst_bin_remove(GST_BIN(self->audiobin), self->audiosink); + gst_bin_remove(GST_BIN(self->audiobin), self->panorama); + + self->panorama = NULL; + self->audiosink = NULL; + self->panoramaSinkPad = NULL; + self->audiobin = NULL; + } + gst_element_set_state(self->playbin, GST_STATE_NULL); gst_object_unref(self->playbin); @@ -114,18 +204,6 @@ struct audio_player *audio_player_new(char *player_id, char *channel) { return NULL; } -void audio_player_source_setup(GstElement *playbin, GstElement *source, GstElement **p_src) { - (void) playbin; - (void) source; - (void) p_src; - /** - * Consider if we want to add option to enable strict SSL check. - if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "ssl-strict") != 0) { - g_object_set(G_OBJECT(source), "ssl-strict", FALSE, NULL); - } - */ -} - gboolean audio_player_on_bus_message(GstBus *bus, GstMessage *message, struct audio_player *data) { (void) bus; switch (GST_MESSAGE_TYPE(message)) { @@ -147,14 +225,14 @@ gboolean audio_player_on_bus_message(GstBus *bus, GstMessage *message, struct au break; } case GST_MESSAGE_EOS: - gst_element_set_state(data->playbin, GST_STATE_READY); audio_player_on_playback_ended(data); break; - case GST_MESSAGE_DURATION_CHANGED: audio_player_on_duration_update(data); break; + case GST_MESSAGE_DURATION_CHANGED: + audio_player_on_duration_update(data); + break; case GST_MESSAGE_ASYNC_DONE: if (!data->is_seek_completed) { audio_player_on_seek_completed(data); - data->is_seek_completed = true; } break; default: @@ -167,9 +245,15 @@ gboolean audio_player_on_bus_message(GstBus *bus, GstMessage *message, struct au return TRUE; } -gboolean audio_player_on_refresh(struct audio_player *data) { - if (data->playbin->current_state == GST_STATE_PLAYING) { - audio_player_on_position_update(data); +gboolean audio_player_on_refresh(struct audio_player *self) { + if (self == NULL) { + return FALSE; + } + + GstState playbinState; + gst_element_get_state(self->playbin, &playbinState, NULL, GST_CLOCK_TIME_NONE); + if (playbinState == GST_STATE_PLAYING) { + audio_player_on_position_update(self); } return TRUE; } @@ -190,10 +274,7 @@ void audio_player_set_playback(struct audio_player *self, int64_t seekTo, double audio_player_pause(self); return; } - - if (self->playback_rate != rate) { - self->playback_rate = rate; - } + self->playback_rate = rate; self->is_seek_completed = false; GstEvent *seek_event; @@ -202,6 +283,7 @@ void audio_player_set_playback(struct audio_player *self, int64_t seekTo, double } else { seek_event = gst_event_new_seek(rate, GST_FORMAT_TIME, seek_flags, GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, seekTo * GST_MSECOND); } + if (!gst_element_send_event(self->playbin, seek_event)) { // Not clear how to treat this error? const int64_t seekMs = seekTo * GST_MSECOND; @@ -209,116 +291,114 @@ void audio_player_set_playback(struct audio_player *self, int64_t seekTo, double self->is_seek_completed = true; } } + void audio_player_on_media_error(struct audio_player *self, GError *error, gchar *debug) { - (void) debug; - char error_message[256] = {0}; - snprintf(error_message, sizeof(error_message), "Error: %d; message=%s", error->code, error->message); - if (self->channel) { - // clang-format off - platch_call_std( - self->channel, - "audio.onError", - &STDMAP2( - STDSTRING("playerId"), - STDSTRING(self->player_id), - STDSTRING("value"), - STDSTRING(error_message) - ), - NULL, - NULL - ); - // clang-format on - } + char error_code[16] = {0}; + snprintf(error_code, sizeof(error_code), "%d", error->code); + // clang-format off + platch_send_error_event_std( + self->event_channel_name, + error_code, + error->message, + debug ? &STDSTRING(debug) : NULL + ); + // clang-format on } void audio_player_on_media_state_change(struct audio_player *self, GstObject *src, GstState *old_state, GstState *new_state) { (void) old_state; - if (strcmp(GST_OBJECT_NAME(src), "playbin") == 0) { - if (*new_state >= GST_STATE_READY) { + if (src == GST_OBJECT(self->playbin)) { + LOG_DEBUG("%s: on_media_state_change(old_state=%d, new_state=%d)\n", self->player_id, *old_state, *new_state); + if (*new_state == GST_STATE_READY) { + self->is_initialized = false; + + // Need to set to pause state, in order to make player functional + GstStateChangeReturn ret = gst_element_set_state(self->playbin, GST_STATE_PAUSED); + if (ret == GST_STATE_CHANGE_FAILURE) { + LOG_ERROR("Unable to set the pipeline to the paused state.\n"); + } + } else if (*old_state == GST_STATE_PAUSED && *new_state == GST_STATE_PLAYING) { + audio_player_on_position_update(self); + audio_player_on_duration_update(self); + } else if (*new_state >= GST_STATE_PAUSED) { if (!self->is_initialized) { self->is_initialized = true; - audio_player_pause(self); // Need to set to pause state, in order to get duration + audio_player_on_prepared(self, true); + if (self->is_playing) { + audio_player_resume(self); + } } } else if (self->is_initialized) { self->is_initialized = false; } } } + +void audio_player_on_prepared(struct audio_player *self, bool value) { + // clang-format off + platch_send_success_event_std( + self->event_channel_name, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onPrepared"), + STDSTRING("value"), STDBOOL(value) + ) + ); + // clang-format on +} + void audio_player_on_position_update(struct audio_player *self) { - if (self->channel) { - // clang-format off - platch_call_std( - self->channel, - "audio.onCurrentPosition", - &STDMAP2( - STDSTRING("playerId"), - STDSTRING(self->player_id), - STDSTRING("value"), - STDINT64(audio_player_get_position(self)) - ), - NULL, - NULL - ); - // clang-format on - } + // clang-format off + platch_send_success_event_std( + self->event_channel_name, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onCurrentPosition"), + STDSTRING("value"), STDINT64(audio_player_get_position(self)) + ) + ); + // clang-format on } + void audio_player_on_duration_update(struct audio_player *self) { - if (self->channel) { - // clang-format off - platch_call_std( - self->channel, - "audio.onDuration", - &STDMAP2( - STDSTRING("playerId"), - STDSTRING(self->player_id), - STDSTRING("value"), - STDINT64(audio_player_get_duration(self)) - ), - NULL, - NULL - ); - // clang-format on - } + // clang-format off + platch_send_success_event_std( + self->event_channel_name, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onDuration"), + STDSTRING("value"), STDINT64(audio_player_get_duration(self)) + ) + ); + // clang-format on } void audio_player_on_seek_completed(struct audio_player *self) { - if (self->channel) { - audio_player_on_position_update(self); - // clang-format off - platch_call_std( - self->channel, - "audio.onSeekComplete", - &STDMAP2( - STDSTRING("playerId"), - STDSTRING(self->player_id), - STDSTRING("value"), - STDBOOL(true) - ), - NULL, - NULL - ); - // clang-format on - } + audio_player_on_position_update(self); + // clang-format off + platch_send_success_event_std( + self->event_channel_name, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onSeekComplete"), + STDSTRING("value"), STDBOOL(true) + ) + ); + // clang-format on + self->is_seek_completed = true; } void audio_player_on_playback_ended(struct audio_player *self) { - audio_player_set_position(self, 0); + // clang-format off + platch_send_success_event_std( + self->event_channel_name, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onComplete"), + STDSTRING("value"), STDBOOL(true) + ) + ); + + // clang-format on + if (audio_player_get_looping(self)) { audio_player_play(self); - } - if (self->channel) { - // clang-format off - platch_call_std( - self->channel, - "audio.onComplete", - &STDMAP2( - STDSTRING("playerId"), - STDSTRING(self->player_id), - STDSTRING("value"), - STDBOOL(true) - ), - NULL, - NULL - ); - // clang-format on + } else { + audio_player_pause(self); + audio_player_set_position(self, 0); } } @@ -339,6 +419,12 @@ void audio_player_play(struct audio_player *self) { } void audio_player_pause(struct audio_player *self) { + self->is_playing = false; + + if (!self->is_initialized) { + return; + } + GstStateChangeReturn ret = gst_element_set_state(self->playbin, GST_STATE_PAUSED); if (ret == GST_STATE_CHANGE_FAILURE) { LOG_ERROR("Unable to set the pipeline to the paused state.\n"); @@ -348,15 +434,17 @@ void audio_player_pause(struct audio_player *self) { } void audio_player_resume(struct audio_player *self) { + self->is_playing = true; if (!self->is_initialized) { return; } + GstStateChangeReturn ret = gst_element_set_state(self->playbin, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { LOG_ERROR("Unable to set the pipeline to the playing state.\n"); return; } - // Update duration when start playing, as no event is emitted elsewhere + audio_player_on_position_update(self); audio_player_on_duration_update(self); } @@ -364,9 +452,28 @@ void audio_player_destroy(struct audio_player *self) { if (self->is_initialized) { audio_player_pause(self); } + + if (self->source) { + gst_object_unref(GST_OBJECT(self->source)); + self->source = NULL; + } + gst_object_unref(self->bus); self->bus = NULL; + if (self->panorama != NULL) { + gst_element_set_state(self->audiobin, GST_STATE_NULL); + + gst_element_remove_pad(self->audiobin, self->panoramaSinkPad); + gst_bin_remove(GST_BIN(self->audiobin), self->audiosink); + gst_bin_remove(GST_BIN(self->audiobin), self->panorama); + + self->panorama = NULL; + self->audiosink = NULL; + self->panoramaSinkPad = NULL; + self->audiobin = NULL; + } + gst_element_set_state(self->playbin, GST_STATE_NULL); gst_object_unref(self->playbin); self->playbin = NULL; @@ -378,15 +485,14 @@ void audio_player_destroy(struct audio_player *self) { self->url = NULL; } + if (self->player_id != NULL) { free(self->player_id); self->player_id = NULL; } - if (self->channel != NULL) { - free(self->channel); - self->channel = NULL; - } + plugin_registry_remove_receiver(self->event_channel_name); + self->event_channel_name[0] = 0; free(self); } @@ -418,6 +524,19 @@ void audio_player_set_volume(struct audio_player *self, double volume) { g_object_set(G_OBJECT(self->playbin), "volume", volume, NULL); } +void audio_player_set_balance(struct audio_player *self, double balance) { + if (!self->panorama) { + return; + } + + if (balance > 1.0l) { + balance = 1.0l; + } else if (balance < -1.0l) { + balance = -1.0l; + } + g_object_set(G_OBJECT(self->panorama), "panorama", balance, NULL); +} + void audio_player_set_playback_rate(struct audio_player *self, double rate) { audio_player_set_playback(self, audio_player_get_position(self), rate); } @@ -432,19 +551,24 @@ void audio_player_set_position(struct audio_player *self, int64_t position) { void audio_player_set_source_url(struct audio_player *self, char *url) { DEBUG_ASSERT_NOT_NULL(url); if (self->url == NULL || strcmp(self->url, url)) { + LOG_DEBUG("%s: set source=%s\n", self->player_id, url); if (self->url != NULL) { free(self->url); self->url = NULL; } self->url = strdup(url); gst_element_set_state(self->playbin, GST_STATE_NULL); + self->is_initialized = false; + self->is_playing = false; + if (strlen(self->url) != 0) { g_object_set(self->playbin, "uri", self->url, NULL); if (self->playbin->current_state != GST_STATE_READY) { gst_element_set_state(self->playbin, GST_STATE_READY); } } - self->is_initialized = false; + } else { + audio_player_on_prepared(self, true); } } diff --git a/src/plugins/audioplayers/plugin.c b/src/plugins/audioplayers/plugin.c index e62a264f..cdf90791 100644 --- a/src/plugins/audioplayers/plugin.c +++ b/src/plugins/audioplayers/plugin.c @@ -13,6 +13,7 @@ FILE_DESCR("audioplayers plugin") #define AUDIOPLAYERS_GLOBAL_CHANNEL "xyz.luan/audioplayers.global" static struct audio_player *audioplayers_linux_plugin_get_player(char *player_id, char *mode); +static void audioplayers_linux_plugin_dispose_player(struct audio_player *player); static struct plugin { struct flutterpi *flutterpi; @@ -31,6 +32,8 @@ static int on_local_method_call(char *channel, struct platch_obj *object, Flutte method = object->method; args = &object->std_arg; + LOG_DEBUG("call(method=%s)\n", method); + if (args == NULL || !STDVALUE_IS_MAP(*args)) { return platch_respond_illegal_arg_std(responsehandle, "Expected `arg` to be a map."); } @@ -55,7 +58,9 @@ static int on_local_method_call(char *channel, struct platch_obj *object, Flutte return platch_respond_native_error_std(responsehandle, ENOMEM); } - if (strcmp(method, "pause") == 0) { + if (strcmp(method, "create") == 0) { + //audioplayers_linux_plugin_get_player() creates player if it doesn't exist + } else if (strcmp(method, "pause") == 0) { audio_player_pause(player); } else if (strcmp(method, "resume") == 0) { audio_player_resume(player); @@ -80,9 +85,9 @@ static int on_local_method_call(char *channel, struct platch_obj *object, Flutte } char *url = STDVALUE_AS_STRING(*tmp); - tmp = stdmap_get_str(args, "is_local"); + tmp = stdmap_get_str(args, "isLocal"); if (tmp == NULL || !STDVALUE_IS_BOOL(*tmp)) { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['is_local']` to be a bool."); + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['isLocal']` to be a bool."); } bool is_local = STDVALUE_AS_BOOL(*tmp); @@ -108,24 +113,73 @@ static int on_local_method_call(char *channel, struct platch_obj *object, Flutte } else if (strcmp(method, "getCurrentPosition") == 0) { result = audio_player_get_position(player); } else if (strcmp(method, "setPlaybackRate") == 0) { - tmp = stdmap_get_str(args, "playback_rate"); + tmp = stdmap_get_str(args, "playbackRate"); if (tmp != NULL && STDVALUE_IS_FLOAT(*tmp)) { audio_player_set_playback_rate(player, STDVALUE_AS_FLOAT(*tmp)); } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playback_rate']` to be a float."); + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playbackRate']` to be a float."); } } else if (strcmp(method, "setReleaseMode") == 0) { - tmp = stdmap_get_str(args, "release_mode"); + tmp = stdmap_get_str(args, "releaseMode"); if (tmp != NULL && STDVALUE_IS_STRING(*tmp)) { char *release_mode = STDVALUE_AS_STRING(*tmp); bool looping = strstr(release_mode, "loop") != NULL; audio_player_set_looping(player, looping); } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['release_mode']` to be a string."); + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['releaseMode']` to be a string."); } } else if (strcmp(method, "setPlayerMode") == 0) { // TODO check support for low latency mode: // https://gstreamer.freedesktop.org/documentation/additional/design/latency.html?gi-language=c + } else if (strcmp(method, "setBalance") == 0) { + tmp = stdmap_get_str(args, "balance"); + if (tmp != NULL && STDVALUE_IS_FLOAT(*tmp)) { + audio_player_set_balance(player, STDVALUE_AS_FLOAT(*tmp)); + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['balance']` to be a float."); + } + } else if (strcmp(method, "emitLog") == 0) { + tmp = stdmap_get_str(args, "message"); + char *message; + + if (tmp == NULL) { + message = ""; + } else if (STDVALUE_IS_STRING(*tmp)) { + message = STDVALUE_AS_STRING(*tmp); + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message']` to be a string."); + } + + LOG_DEBUG("%s\n", message); + //TODO: https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers_linux/linux/audio_player.cc#L247 + } else if (strcmp(method, "emitError") == 0) { + tmp = stdmap_get_str(args, "code"); + char *code; + + if (tmp == NULL) { + code = ""; + } else if (STDVALUE_IS_STRING(*tmp)) { + code = STDVALUE_AS_STRING(*tmp); + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['code']` to be a string."); + } + + tmp = stdmap_get_str(args, "message"); + char *message; + + if (tmp == NULL) { + message = ""; + } else if (STDVALUE_IS_STRING(*tmp)) { + message = STDVALUE_AS_STRING(*tmp); + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message']` to be a string."); + } + + LOG_ERROR("Error: %s; message=%s\n", code, message); + //TODO: https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers_linux/linux/audio_player.cc#L144 + } else if (strcmp(method, "dispose") == 0) { + audioplayers_linux_plugin_dispose_player(player); + player = NULL; } else { return platch_respond_not_implemented(responsehandle); } @@ -200,4 +254,9 @@ static struct audio_player *audioplayers_linux_plugin_get_player(char *player_id return player; } +static void audioplayers_linux_plugin_dispose_player(struct audio_player *player) { + audio_player_destroy(player); + cpset_remove_locked(&plugin.players, player); +} + FLUTTERPI_PLUGIN("audioplayers", audioplayers, audioplayers_plugin_init, audioplayers_plugin_deinit)