diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index fef84518..e7f0ea21 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -21,14 +21,14 @@ jobs: steps: - uses: actions/checkout@v2 - + - name: Install dependencies - run: sudo apt-get install -y cmake libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev ttf-mscorefonts-installer fontconfig libsystemd-dev libinput-dev libudev-dev libxkbcommon-dev ninja-build + run: sudo apt-get install -y cmake libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev ttf-mscorefonts-installer fontconfig libsystemd-dev libinput-dev libudev-dev libxkbcommon-dev ninja-build libgstreamer-plugins-base1.0-dev - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/build -DBUILD_OMXPLAYER_VIDEO_PLAYER_PLUGIN=On -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -GNinja + run: cmake -B ${{github.workspace}}/build -DBUILD_OMXPLAYER_VIDEO_PLAYER_PLUGIN=On -DBUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN=On -DBUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN=On -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -GNinja - name: Build # Build your program with the given configuration @@ -37,7 +37,7 @@ jobs: - name: Test if: false working-directory: ${{github.workspace}}/build - # Execute tests defined by the CMake configuration. + # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest -C ${{env.BUILD_TYPE}} - + diff --git a/CMakeLists.txt b/CMakeLists.txt index cd51cf1d..40efee21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,8 +40,10 @@ option(BUILD_TEXT_INPUT_PLUGIN "Include the text input plugin in the finished bi option(BUILD_RAW_KEYBOARD_PLUGIN "Include the raw keyboard plugin in the finished binary. Enables raw keycode listening in flutter via the flutter RawKeyboard interface." ON) option(BUILD_TEST_PLUGIN "Include the test plugin in the finished binary. Allows testing platform channel communication." OFF) option(BUILD_OMXPLAYER_VIDEO_PLAYER_PLUGIN "Include the omxplayer_video_player plugin in the finished binary. Allows for hardware accelerated video playback in flutter using omxplayer." ON) -option(BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN "Include the gstreamer_video_player plugin in the finished binary. Allows for more stable, hardware accelerated video playback in flutter using gstreamer." ON) +option(BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN "Include the gstreamer based video plugins in the finished binary. Allows for more stable, hardware accelerated video playback in flutter using gstreamer." ON) +option(BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN "Include the gstreamer based audio plugins in the finished binary." ON) option(TRY_BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN "Don't throw an error if the gstreamer libs aren't found, instead just don't build the gstreamer video player plugin in that case." ON) +option(TRY_BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN "Don't throw an error if the gstreamer libs aren't found, instead just don't build gstreamer audio plugin." ON) option(DUMP_ENGINE_LAYERS "True if flutter-pi should dump the list of rendering layers that the flutter engine sends to flutter-pi on each draw." OFF) option(ENABLE_TSAN "True to build & link with -fsanitize=thread" OFF) option(ENABLE_ASAN "True to build & link with -fsanitize=address" OFF) @@ -130,7 +132,7 @@ target_link_libraries(flutter-pi ${GBM_LDFLAGS} ${EGL_LDFLAGS} ${GLESV2_LDFLAGS} - EGL + EGL systemd #${LIBSYSTEMD_LDFLAGS} input #${LIBINPUT_LDFLAGS} xkbcommon #${LIBUDEV_LDFLAGS} @@ -153,7 +155,7 @@ target_include_directories(flutter-pi PRIVATE ) target_compile_options(flutter-pi PRIVATE - ${DRM_CFLAGS} + ${DRM_CFLAGS} ${GBM_CFLAGS} ${EGL_CFLAGS} ${GLESV2_CFLAGS} @@ -166,7 +168,7 @@ target_compile_options(flutter-pi PRIVATE $<$:-O2 -Wall -Wextra -Wno-unused-function -Wno-sign-compare -Wno-missing-field-initializers -ggdb> ) -# TODO: Just unconditionally define those, make them optional later +# TODO: Just unconditionally define those, make them optional later target_compile_definitions(flutter-pi PRIVATE HAS_KMS HAS_EGL HAS_GBM HAS_FBDEV) # TODO: We actually don't need the compile definitions anymore, except for @@ -219,6 +221,7 @@ if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) ${LIBGSTREAMER_APP_LDFLAGS} ${LIBGSTREAMER_ALLOCATORS_LDFLAGS} ${LIBGSTREAMER_VIDEO_LDFLAGS} + ${LIBGSTREAMER_AUDIO_LIBRARY_DIRS} ) target_include_directories(flutter-pi PRIVATE ${LIBGSTREAMER_INCLUDE_DIRS} @@ -226,6 +229,7 @@ if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) ${LIBGSTREAMER_APP_INCLUDE_DIRS} ${LIBGSTREAMER_ALLOCATORS_INCLUDE_DIRS} ${LIBGSTREAMER_VIDEO_INCLUDE_DIRS} + ${LIBGSTREAMER_AUDIO_INCLUDE_DIRS} ) target_compile_options(flutter-pi PRIVATE ${LIBGSTREAMER_CFLAGS} @@ -233,12 +237,50 @@ if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) ${LIBGSTREAMER_APP_CFLAGS} ${LIBGSTREAMER_ALLOCATORS_CFLAGS} ${LIBGSTREAMER_VIDEO_CFLAGS} + ${LIBGSTREAMER_AUDIO_CFLAGS} ) else() message(NOTICE "Couldn't find gstreamer libraries. Gstreamer video player plugin won't be build.") endif() endif() +if (BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN) + if (TRY_BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN) + pkg_check_modules(LIBGSTREAMER gstreamer-1.0) + pkg_check_modules(LIBGSTREAMER_APP gstreamer-app-1.0) + pkg_check_modules(LIBGSTREAMER_AUDIO gstreamer-audio-1.0) + else() + pkg_check_modules(LIBGSTREAMER REQUIRED gstreamer-1.0) + pkg_check_modules(LIBGSTREAMER_APP REQUIRED gstreamer-app-1.0) + pkg_check_modules(LIBGSTREAMER_AUDIO REQUIRED gstreamer-audio-1.0) + endif() + + if (LIBGSTREAMER_FOUND AND LIBGSTREAMER_APP_FOUND AND LIBGSTREAMER_AUDIO_FOUND) + target_sources(flutter-pi PRIVATE + src/plugins/audioplayers/plugin.c + src/plugins/audioplayers/player.c + ) + target_compile_definitions(flutter-pi PRIVATE "BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN") + target_link_libraries(flutter-pi + ${LIBGSTREAMER_LDFLAGS} + ${LIBGSTREAMER_APP_LDFLAGS} + ${LIBGSTREAMER_AUDIO_LIBRARY_DIRS} + ) + target_include_directories(flutter-pi PRIVATE + ${LIBGSTREAMER_INCLUDE_DIRS} + ${LIBGSTREAMER_APP_INCLUDE_DIRS} + ${LIBGSTREAMER_AUDIO_INCLUDE_DIRS} + ) + target_compile_options(flutter-pi PRIVATE + ${LIBGSTREAMER_CFLAGS} + ${LIBGSTREAMER_APP_CFLAGS} + ${LIBGSTREAMER_AUDIO_CFLAGS} + ) + else() + message(NOTICE "Couldn't find gstreamer libraries. Gstreamer audio player plugin won't be build.") + endif() +endif() + # Needed so dart VM can actually resolve symbols in the same # executable. target_link_options(flutter-pi PRIVATE diff --git a/include/plugins/audioplayers.h b/include/plugins/audioplayers.h new file mode 100644 index 00000000..c5a3c547 --- /dev/null +++ b/include/plugins/audioplayers.h @@ -0,0 +1,39 @@ +#ifndef AUDIOPLAYERS_H_ +#define AUDIOPLAYERS_H_ + +#include +#include + +struct audio_player; + +struct audio_player *audio_player_new(char *playerId, char *channel); + +// Instance function + +int64_t audio_player_get_position(struct audio_player *self); + +int64_t audio_player_get_duration(struct audio_player *self); + +bool audio_player_get_looping(struct audio_player *self); + +void audio_player_play(struct audio_player *self); + +void audio_player_pause(struct audio_player *self); + +void audio_player_resume(struct audio_player *self); + +void audio_player_destroy(struct audio_player *self); + +void audio_player_set_looping(struct audio_player *self, bool isLooping); + +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_position(struct audio_player *self, int64_t position); + +void audio_player_set_source_url(struct audio_player *self, char *url); + +bool audio_player_is_id(struct audio_player *self, char *id); + +#endif // AUDIOPLAYERS_H_ diff --git a/src/plugins/audioplayers/player.c b/src/plugins/audioplayers/player.c new file mode 100644 index 00000000..20f847aa --- /dev/null +++ b/src/plugins/audioplayers/player.c @@ -0,0 +1,453 @@ +#include + +#include "gst/gst.h" +#include "gst/gstelementfactory.h" +#include "gst/gstmessage.h" +#include "gst/gstsegment.h" +#include "platformchannel.h" + +#include +#include + +FILE_DESCR("audioplayers player") + +struct audio_player { + GstElement *playbin; + GstBus *bus; + + bool is_initialized; + bool is_looping; + bool is_seek_completed; + double playback_rate; + + char *url; + char *player_id; + char *channel; +}; + +// Private Class functions +static gboolean audio_player_on_bus_message(GstBus *bus, GstMessage *message, struct audio_player *data); +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_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); +static void audio_player_on_playback_ended(struct audio_player *self); + +static int on_bus_fd_ready(sd_event_source *src, int fd, uint32_t revents, void *userdata) { + struct audio_player *player = userdata; + GstMessage *msg; + + (void) src; + (void) fd; + (void) revents; + + /* DEBUG_TRACE_BEGIN(player, "on_bus_fd_ready"); */ + + msg = gst_bus_pop(player->bus); + if (msg != NULL) { + audio_player_on_bus_message(player->bus, msg, player); + gst_message_unref(msg); + } + + /* DEBUG_TRACE_END(player, "on_bus_fd_ready"); */ + + return 0; +} + +struct audio_player *audio_player_new(char *player_id, char *channel) { + GPollFD fd; + sd_event_source *busfd_event_source; + + struct audio_player *self = malloc(sizeof(struct audio_player)); + if (self == NULL) { + return NULL; + } + + self->url = NULL; + self->is_initialized = 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"); + if (!self->playbin) { + LOG_ERROR("Could not create gstreamer playbin.\n"); + goto deinit_self; + } + + self->bus = gst_element_get_bus(self->playbin); + + gst_bus_get_pollfd(self->bus, &fd); + + flutterpi_sd_event_add_io(&busfd_event_source, fd.fd, EPOLLIN, on_bus_fd_ready, self); + + // Refresh continuously to emit recurring events + g_timeout_add(1000, (GSourceFunc) audio_player_on_refresh, self); + + self->player_id = strdup(player_id); + if (self->player_id == NULL) + goto deinit_player; + + self->channel = strdup(channel); + if (self->channel == NULL) + goto deinit_player_id; + + return self; + + //Deinit doesn't require to NULL, as we just delete player. +deinit_player_id: + free(self->player_id); + +deinit_player: + free(self->channel); + + gst_object_unref(self->bus); + gst_element_set_state(self->playbin, GST_STATE_NULL); + gst_object_unref(self->playbin); + +deinit_self: + free(self); + 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)) { + case GST_MESSAGE_ERROR: { + GError *err; + gchar *debug; + + gst_message_parse_error(message, &err, &debug); + audio_player_on_media_error(data, err, debug); + g_error_free(err); + g_free(debug); + break; + } + case GST_MESSAGE_STATE_CHANGED: { + GstState old_state, new_state; + + gst_message_parse_state_changed(message, &old_state, &new_state, NULL); + audio_player_on_media_state_change(data, message->src, &old_state, &new_state); + 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_ASYNC_DONE: + if (!data->is_seek_completed) { + audio_player_on_seek_completed(data); + data->is_seek_completed = true; + } + break; + default: + // For more GstMessage types see: + // https://gstreamer.freedesktop.org/documentation/gstreamer/gstmessage.html?gi-language=c#enumerations + break; + } + + // Continue watching for messages + 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); + } + return TRUE; +} + +void audio_player_set_playback(struct audio_player *self, int64_t seekTo, double rate) { + const GstSeekFlags seek_flags = GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE; + + if (!self->is_initialized) { + return; + } + // See: + // https://gstreamer.freedesktop.org/documentation/tutorials/basic/playback-speed.html?gi-language=c + if (!self->is_seek_completed) { + return; + } + if (rate == 0) { + // Do not set rate if it's 0, rather pause. + audio_player_pause(self); + return; + } + + if (self->playback_rate != rate) { + self->playback_rate = rate; + } + self->is_seek_completed = false; + + GstEvent *seek_event; + if (rate > 0) { + seek_event = gst_event_new_seek(rate, GST_FORMAT_TIME, seek_flags, GST_SEEK_TYPE_SET, seekTo * GST_MSECOND, GST_SEEK_TYPE_NONE, -1); + } 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; + LOG_ERROR("Could not set playback to position " GST_STIME_FORMAT " and rate %f.\n", GST_TIME_ARGS(seekMs), rate); + 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("player_id"), + STDSTRING(self->player_id), + STDSTRING("value"), + STDSTRING(error_message) + ), + NULL, + 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 (!self->is_initialized) { + self->is_initialized = true; + audio_player_pause(self); // Need to set to pause state, in order to get duration + } + } else if (self->is_initialized) { + self->is_initialized = false; + } + } +} +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("player_id"), + STDSTRING(self->player_id), + STDSTRING("value"), + STDINT64(audio_player_get_position(self)) + ), + NULL, + NULL + ); + // 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("player_id"), + STDSTRING(self->player_id), + STDSTRING("value"), + STDINT64(audio_player_get_duration(self)) + ), + NULL, + NULL + ); + // 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("player_id"), + STDSTRING(self->player_id), + STDSTRING("value"), + STDBOOL(true) + ), + NULL, + NULL + ); + // clang-format on + } +} +void audio_player_on_playback_ended(struct audio_player *self) { + audio_player_set_position(self, 0); + 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("player_id"), + STDSTRING(self->player_id), + STDSTRING("value"), + STDBOOL(true) + ), + NULL, + NULL + ); + // clang-format on + } +} + +void audio_player_set_looping(struct audio_player *self, bool is_looping) { + self->is_looping = is_looping; +} + +bool audio_player_get_looping(struct audio_player *self) { + return self->is_looping; +} + +void audio_player_play(struct audio_player *self) { + if (!self->is_initialized) { + return; + } + audio_player_set_position(self, 0); + audio_player_resume(self); +} + +void audio_player_pause(struct audio_player *self) { + 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"); + return; + } + audio_player_on_position_update(self); // Update to exact position when pausing +} + +void audio_player_resume(struct audio_player *self) { + 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_duration_update(self); +} + +void audio_player_destroy(struct audio_player *self) { + if (self->is_initialized) { + audio_player_pause(self); + } + gst_object_unref(self->bus); + self->bus = NULL; + + gst_element_set_state(self->playbin, GST_STATE_NULL); + gst_object_unref(self->playbin); + self->playbin = NULL; + + self->is_initialized = false; + + if (self->url != NULL) { + free(self->url); + 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; + } + + free(self); +} + +int64_t audio_player_get_position(struct audio_player *self) { + gint64 current = 0; + if (!gst_element_query_position(self->playbin, GST_FORMAT_TIME, ¤t)) { + LOG_ERROR("Could not query current position.\n"); + return 0; + } + return current / 1000000; +} + +int64_t audio_player_get_duration(struct audio_player *self) { + gint64 duration = 0; + if (!gst_element_query_duration(self->playbin, GST_FORMAT_TIME, &duration)) { + LOG_ERROR("Could not query current duration.\n"); + return 0; + } + return duration / 1000000; +} + +void audio_player_set_volume(struct audio_player *self, double volume) { + if (volume > 1) { + volume = 1; + } else if (volume < 0) { + volume = 0; + } + g_object_set(G_OBJECT(self->playbin), "volume", volume, NULL); +} + +void audio_player_set_playback_rate(struct audio_player *self, double rate) { + audio_player_set_playback(self, audio_player_get_position(self), rate); +} + +void audio_player_set_position(struct audio_player *self, int64_t position) { + if (!self->is_initialized) { + return; + } + audio_player_set_playback(self, position, self->playback_rate); +} + +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)) { + if (self->url != NULL) { + free(self->url); + self->url = NULL; + } + self->url = strdup(url); + gst_element_set_state(self->playbin, GST_STATE_NULL); + 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; + } +} + +bool audio_player_is_id(struct audio_player *self, char *player_id) { + return strcmp(self->player_id, player_id) == 0; +} diff --git a/src/plugins/audioplayers/plugin.c b/src/plugins/audioplayers/plugin.c new file mode 100644 index 00000000..17499abc --- /dev/null +++ b/src/plugins/audioplayers/plugin.c @@ -0,0 +1,203 @@ +#define _GNU_SOURCE + +#include "plugins/audioplayers.h" + +#include +#include +#include +#include + +FILE_DESCR("audioplayers plugin") + +#define AUDIOPLAYERS_LOCAL_CHANNEL "xyz.luan/audioplayers" +#define AUDIOPLAYERS_GLOBAL_CHANNEL "xyz.luan/audioplayers.global" + +static struct audio_player *audioplayers_linux_plugin_get_player(char *player_id, char *mode); + +static struct plugin { + struct flutterpi *flutterpi; + bool initialized; + struct concurrent_pointer_set players; +} plugin; + +static int on_local_method_call(char *channel, struct platch_obj *object, FlutterPlatformMessageResponseHandle *responsehandle) { + struct audio_player *player; + struct std_value *args, *tmp; + const char *method; + char *player_id, *mode; + int result = 1; + (void) responsehandle; + (void) channel; + method = object->method; + args = &object->std_arg; + + if (args == NULL || !STDVALUE_IS_MAP(*args)) { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg` to be a map."); + } + + tmp = stdmap_get_str(&object->std_arg, "player_id"); + if (tmp == NULL || !STDVALUE_IS_STRING(*tmp)) { + LOG_ERROR("Call missing mandatory parameter player_id.\n"); + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['player_id'] to be a string."); + } + player_id = STDVALUE_AS_STRING(*tmp); + tmp = stdmap_get_str(args, "mode"); + if (tmp == NULL) { + mode = ""; + } else if (STDVALUE_IS_STRING(*tmp)) { + mode = STDVALUE_AS_STRING(*tmp); + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['mode']` to be a string or null."); + } + + player = audioplayers_linux_plugin_get_player(player_id, mode); + if (player == NULL) { + return platch_respond_native_error_std(responsehandle, ENOMEM); + } + + if (strcmp(method, "pause") == 0) { + audio_player_pause(player); + } else if (strcmp(method, "resume") == 0) { + audio_player_resume(player); + } else if (strcmp(method, "stop") == 0) { + audio_player_pause(player); + audio_player_set_position(player, 0); + } else if (strcmp(method, "release") == 0) { + audio_player_pause(player); + audio_player_set_position(player, 0); + } else if (strcmp(method, "seek") == 0) { + tmp = stdmap_get_str(args, "position"); + if (tmp == NULL || !STDVALUE_IS_INT(*tmp)) { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['position']` to be an int."); + } + + int64_t position = STDVALUE_AS_INT(*tmp); + audio_player_set_position(player, position); + } else if (strcmp(method, "setSourceUrl") == 0) { + tmp = stdmap_get_str(args, "url"); + if (tmp == NULL || !STDVALUE_IS_STRING(*tmp)) { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['url']` to be a string."); + } + char *url = STDVALUE_AS_STRING(*tmp); + + tmp = stdmap_get_str(args, "is_local"); + if (tmp == NULL || !STDVALUE_IS_BOOL(*tmp)) { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['is_local']` to be a bool."); + } + + bool is_local = STDVALUE_AS_BOOL(*tmp); + if (is_local) { + char *local_url = NULL; + asprintf(&local_url, "file://%s", url); + if (local_url == NULL) { + return platch_respond_native_error_std(responsehandle, ENOMEM); + } + url = local_url; + } + + audio_player_set_source_url(player, url); + } else if (strcmp(method, "getDuration") == 0) { + result = audio_player_get_duration(player); + } else if (strcmp(method, "setVolume") == 0) { + tmp = stdmap_get_str(args, "volume"); + if (tmp != NULL && STDVALUE_IS_FLOAT(*tmp)) { + audio_player_set_volume(player, STDVALUE_AS_FLOAT(*tmp)); + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['volume']` to be a float."); + } + } 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"); + 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."); + } + } else if (strcmp(method, "setReleaseMode") == 0) { + tmp = stdmap_get_str(args, "release_mode"); + 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."); + } + } 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 { + return platch_respond_not_implemented(responsehandle); + } + + return platch_respond_success_std(responsehandle, &STDINT64(result)); +} + +static int on_global_method_call(char *channel, struct platch_obj *object, FlutterPlatformMessageResponseHandle *responsehandle) { + (void) responsehandle; + (void) channel; + (void) object; + + return platch_respond_success_std(responsehandle, &STDBOOL(true)); +} + +enum plugin_init_result audioplayers_plugin_init(struct flutterpi *flutterpi, void **userdata_out) { + (void) userdata_out; + int ok; + plugin.flutterpi = flutterpi; + plugin.initialized = false; + + ok = cpset_init(&plugin.players, CPSET_DEFAULT_MAX_SIZE); + if (ok != 0) + return kError_PluginInitResult; + + ok = plugin_registry_set_receiver(AUDIOPLAYERS_GLOBAL_CHANNEL, kStandardMethodCall, on_global_method_call); + if (ok != 0) { + goto fail_deinit_cpset; + } + + ok = plugin_registry_set_receiver(AUDIOPLAYERS_LOCAL_CHANNEL, kStandardMethodCall, on_local_method_call); + if (ok != 0) { + goto fail_remove_global_receiver; + } + + return kInitialized_PluginInitResult; + +fail_remove_global_receiver: + plugin_registry_remove_receiver(AUDIOPLAYERS_GLOBAL_CHANNEL); + +fail_deinit_cpset: + cpset_deinit(&plugin.players); + + return kError_PluginInitResult; +} + +void audioplayers_plugin_deinit(struct flutterpi *flutterpi, void *userdata) { + (void) flutterpi; + (void) userdata; + plugin_registry_remove_receiver(AUDIOPLAYERS_GLOBAL_CHANNEL); + plugin_registry_remove_receiver(AUDIOPLAYERS_LOCAL_CHANNEL); + + struct audio_player *ptr; + for_each_pointer_in_cpset(&plugin.players, ptr) { + audio_player_destroy(ptr); + } + + cpset_deinit(&plugin.players); +} + +static struct audio_player *audioplayers_linux_plugin_get_player(char *player_id, char *mode) { + (void) mode; + struct audio_player *player; + for_each_pointer_in_cpset(&plugin.players, player) { + if (audio_player_is_id(player, player_id)) { + return player; + } + } + + player = audio_player_new(player_id, AUDIOPLAYERS_LOCAL_CHANNEL); + cpset_put_locked(&plugin.players, player); + return player; +} + +FLUTTERPI_PLUGIN("audioplayers", audioplayers, audioplayers_plugin_init, audioplayers_plugin_deinit)