diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index fef84518..3c598377 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -13,6 +13,8 @@ env: jobs: build: + name: build (raspbian bullseye, pi4) + # The CMake configure and build commands are platform agnostic and should work equally # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. @@ -21,14 +23,18 @@ 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 +43,46 @@ 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}} - + + build-buster: + name: build (debian buster, x64) + runs-on: ubuntu-latest + container: + image: debian:buster + env: + DEBIAN_FRONTEND: noninteractive + steps: + - uses: actions/checkout@v2 + + - name: Install dependencies + run: | + apt-get update && apt-get install -y \ + cmake libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev \ + fonts-liberation 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 \ + -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 + run: cmake --build ${{ github.workspace }}/build --config ${{ env.BUILD_TYPE }} + + - name: Test + if: false + working-directory: ${{github.workspace}}/build + # 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/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 00000000..9f71ddde --- /dev/null +++ b/.github/workflows/no-response.yml @@ -0,0 +1,36 @@ +name: No Response + +# Both `issue_comment` and `scheduled` event types are required for this Action +# to work properly. +on: + issue_comment: + types: [created] + schedule: + # Schedule for five minutes after the hour, every hour + - cron: '5 * * * *' + +# By specifying the access of one of the scopes, all of those that are not +# specified are set to 'none'. +permissions: + issues: write + +jobs: + noResponse: + runs-on: ubuntu-latest + steps: + - uses: godofredoc/no-response@0ce2dc0e63e1c7d2b87752ceed091f6d32c9df09 + with: + token: ${{ github.token }} + # Comment to post when closing an Issue for lack of response. Set to `false` to disable + closeComment: > + Without additional information, we are unfortunately not sure how to + resolve this issue. We are therefore reluctantly going to close this + bug for now. + If you find this problem please file a new issue with the same description, + what happens and logs. All system setups can be slightly different so it's + always better to open new issues and reference the related ones. + Thanks for your contribution. + # Number of days of inactivity before an issue is closed for lack of response. + daysUntilClose: 21 + # Label requiring a response. + responseRequiredLabel: "waiting for response" diff --git a/CMakeLists.txt b/CMakeLists.txt index ab9dd4a4..41bd4067 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ if(NOT CMAKE_BUILD_TYPE) message(STATUS "CMAKE_BUILD_TYPE not set, defaulting to Release.") endif() -project(flutter-pi LANGUAGES C CXX VERSION "1.0.0") +project(flutter-pi LANGUAGES C VERSION "1.0.0") message(STATUS "Generator .............. ${CMAKE_GENERATOR}") message(STATUS "Build Type ............. ${CMAKE_BUILD_TYPE}") @@ -40,13 +40,20 @@ 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) option(ENABLE_UBSAN "True to build & link with -fsanitize=undefined" OFF) option(ENABLE_MTRACE "True if flutter-pi should call GNU mtrace() on startup." OFF) + +set(FILESYSTEM_LAYOUTS default meta-flutter) +set(FILESYSTEM_LAYOUT "default" CACHE STRING "Where to look for the icudtl.dat, app.so/libapp.so, flutter asset bundle.") +set_property(CACHE FILESYSTEM_LAYOUT PROPERTY STRINGS ${FILESYSTEM_LAYOUTS}) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(NOT FLUTTER_EMBEDDER_HEADER) @@ -98,9 +105,6 @@ endif() include(ExternalProject) include(CheckCCompilerFlag) -set(ENV{PKG_CONFIG_PATH} ${PKG_CONFIG_PATH}) -message(STATUS "PKG_CONFIG_PATH ........ $ENV{PKG_CONFIG_PATH}") - include(FindPkgConfig) pkg_check_modules(DRM REQUIRED libdrm) pkg_check_modules(GBM REQUIRED gbm) @@ -125,6 +129,7 @@ add_executable(flutter-pi src/locales.c src/notifier_listener.c src/pixel_format.c + src/filesystem_layout.c src/plugins/services.c ) @@ -133,10 +138,11 @@ target_link_libraries(flutter-pi ${GBM_LDFLAGS} ${EGL_LDFLAGS} ${GLESV2_LDFLAGS} - ${LIBSYSTEMD_LDFLAGS} - ${LIBINPUT_LDFLAGS} - ${LIBUDEV_LDFLAGS} - ${LIBXKBCOMMON_LDFLAGS} + EGL + systemd #${LIBSYSTEMD_LDFLAGS} + input #${LIBINPUT_LDFLAGS} + xkbcommon #${LIBUDEV_LDFLAGS} + udev #${LIBXKBCOMMON_LDFLAGS} pthread dl rt m atomic ) @@ -155,7 +161,7 @@ target_include_directories(flutter-pi PRIVATE ) target_compile_options(flutter-pi PRIVATE - ${DRM_CFLAGS} + ${DRM_CFLAGS} ${GBM_CFLAGS} ${EGL_CFLAGS} ${GLESV2_CFLAGS} @@ -163,14 +169,39 @@ target_compile_options(flutter-pi PRIVATE ${LIBINPUT_CFLAGS} ${LIBUDEV_CFLAGS} ${LIBXKBCOMMON_CFLAGS} - $<$:-O0 -Wall -Wextra -Wno-unused-function -Wno-sign-compare -Wno-missing-field-initializers -Werror -ggdb -DDEBUG> + $<$:-O0 -Wall -Wextra -Wno-unused-function -Wno-sign-compare -Wno-missing-field-initializers -Werror -ggdb -U_FORTIFY_SOURCE -DDEBUG> $<$:-O2 -Wall -Wextra -Wno-unused-function -Wno-sign-compare -Wno-missing-field-initializers -ggdb> $<$:-O2 -Wall -Wextra -Wno-unused-function -Wno-sign-compare -Wno-missing-field-initializers -ggdb> ) -# TODO: Just unconditionally define those, make them optional later +# There's no other way to query the libinput version (in code) somehow. +# So we need to roll our own libinput version macro +string(REPLACE "." ";" LIBINPUT_VERSION_AS_LIST ${LIBINPUT_VERSION}) +list(GET LIBINPUT_VERSION_AS_LIST 0 LIBINPUT_VERSION_MAJOR) +list(GET LIBINPUT_VERSION_AS_LIST 1 LIBINPUT_VERSION_MINOR) +list(GET LIBINPUT_VERSION_AS_LIST 2 LIBINPUT_VERSION_PATCH) + +target_compile_definitions(flutter-pi PRIVATE + LIBINPUT_VERSION_MAJOR=${LIBINPUT_VERSION_MAJOR} + LIBINPUT_VERSION_MINOR=${LIBINPUT_VERSION_MINOR} + LIBINPUT_VERSION_PATCH=${LIBINPUT_VERSION_PATCH} +) + +# TODO: Just unconditionally define those, make them optional later target_compile_definitions(flutter-pi PRIVATE HAS_KMS HAS_EGL HAS_GBM HAS_FBDEV) +if(NOT FILESYSTEM_LAYOUT IN_LIST FILESYSTEM_LAYOUTS) + message(FATAL_ERROR "FILESYSTEM_LAYOUT must be one of ${FILESYSTEM_LAYOUTS}") +endif() + +message(STATUS "Filesystem Layout ...... ${FILESYSTEM_LAYOUT}") + +if(FILESYSTEM_LAYOUT STREQUAL default) + target_compile_definitions(flutter-pi PRIVATE "FILESYSTEM_LAYOUT_DEFAULT") +elseif(FILESYSTEM_LAYOUT STREQUAL meta-flutter) + target_compile_definitions(flutter-pi PRIVATE "FILESYSTEM_LAYOUT_METAFLUTTER") +endif() + # TODO: We actually don't need the compile definitions anymore, except for # text input and raw keyboard plugin (because those have special treatment # in flutter-pi.c) @@ -221,6 +252,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} @@ -228,6 +260,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} @@ -235,12 +268,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/GETTING_STARTED.md b/GETTING_STARTED.md index 53dd4a42..30261860 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -69,7 +69,7 @@ In each step below with Bash commands, the commands start with a set of `export` --sdk-root ~/dev/flutter-for-pi/bin/cache/artifacts/engine/common/flutter_patched_sdk_product \ --target=flutter \ --aot --tfa -Ddart.vm.product=true \ - --packages .packages --output-dill build/kernel_snapshot.dill --depfile build/kernel_snapshot.d \ + --packages .dart_tool\package_config.json --output-dill build/kernel_snapshot.dill --depfile build/kernel_snapshot.d \ package:$APPNAME/main.dart ../engine-binaries/$ARM/gen_snapshot_linux_x64_release \ --deterministic --snapshot_kind=app-aot-elf \ diff --git a/README.md b/README.md index e02fcae6..ae16ee21 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## 📰 NEWS - There's now a new video player based on gstreamer. See [gstreamer video player](#gstreamer-video-player) section. -- The new latest flutter gallery commit for flutter 2.10 is `5da082d` +- The new latest flutter gallery commit for flutter 3.7 is `9776b9fd916635e10a32bd426fcd7a20c3841faf` # flutter-pi A light-weight Flutter Engine Embedder for Raspberry Pi. Inspired by https://github.com/chinmaygarde/flutter_from_scratch. @@ -109,7 +109,7 @@ If you encounter issues running flutter-pi on any of the supported platforms lis 2. Switch to console mode: `System Options -> Boot / Auto Login` and select `Console` or `Console (Autologin)`. -3. *Raspbian buster only, skip this if you're on bullseye* +3. *You can skip this if you're on Raspberry Pi 4 with Raspbian Bullseye* Enable the V3D graphics driver: `Advanced Options -> GL Driver -> GL (Fake KMS)` @@ -123,7 +123,7 @@ If you encounter issues running flutter-pi on any of the supported platforms lis usermod -a -G render pi ``` -5. Finish and reboot. +7. Finish and reboot.
More information @@ -164,7 +164,7 @@ If you encounter issues running flutter-pi on any of the supported platforms lis ```bash git clone https://github.com/flutter/gallery.git flutter_gallery cd flutter_gallery -git checkout 5da082d +git checkout 9776b9fd916635e10a32bd426fcd7a20c3841faf flutter build bundle rsync -a ./build/flutter_assets/ pi@raspberrypi:/home/pi/flutter_gallery/ ``` @@ -173,7 +173,7 @@ rsync -a ./build/flutter_assets/ pi@raspberrypi:/home/pi/flutter_gallery/
More information - - flutter_gallery is developed against flutter master. `5da082d82e2da9f57e396b5a1302dc924c81f83d` is currently the latest flutter gallery + - flutter_gallery is developed against flutter master. `9776b9fd916635e10a32bd426fcd7a20c3841faf` is currently the latest flutter gallery commit working with flutter stable.
@@ -195,13 +195,19 @@ rsync -a ./build/flutter_assets/ pi@raspberrypi:/home/pi/flutter_gallery/ --aot ^ --tfa ^ -Ddart.vm.product=true ^ - --packages .packages ^ + --packages .dart_tool\package_config.json ^ --output-dill build\kernel_snapshot.dill ^ --verbose ^ --depfile build\kernel_snapshot.d ^ package:my_app_name/main.dart ``` +
+ More information + + - In versions prior to Flutter 3.3.0 the `--packages` argument should be set to `.packages`. In versions greater than or equal to 3.3.0 the `--packages` argument should be set to `.dart_tool\package_config.json`. +
+ 5. Fetch the latest `gen_snapshot_linux_x64_release` I provide in the [engine binaries repo](https://github.com/ardera/flutter-engine-binaries-for-arm). 6. The following steps must be executed on a linux x64 machine. If you're on windows, you can use [WSL](https://docs.microsoft.com/de-de/windows/wsl/install-win10). If you're on macOS, you can use a linux VM. 7. Build the `app.so`. If you're building for _arm64_, you need to omit the `--sim-use-hardfp` flag. @@ -231,7 +237,7 @@ rsync -a ./build/flutter_assets/ pi@raspberrypi:/home/pi/flutter_gallery/ git clone https://github.com/flutter/gallery.git flutter_gallery git clone --depth 1 https://github.com/ardera/flutter-engine-binaries-for-arm.git engine-binaries cd flutter_gallery - git checkout 5da082d82e2da9f57e396b5a1302dc924c81f83d + git checkout 9776b9fd916635e10a32bd426fcd7a20c3841faf flutter build bundle C:\flutter\bin\cache\dart-sdk\bin\dart.exe ^ C:\flutter\bin\cache\dart-sdk\bin\snapshots\frontend_server.dart.snapshot ^ @@ -240,7 +246,7 @@ rsync -a ./build/flutter_assets/ pi@raspberrypi:/home/pi/flutter_gallery/ --aot ^ --tfa ^ -Ddart.vm.product=true ^ - --packages .packages ^ + --packages .dart_tool\package_config.json ^ --output-dill build\kernel_snapshot.dill ^ --verbose ^ --depfile build\kernel_snapshot.d ^ diff --git a/include/filesystem_layout.h b/include/filesystem_layout.h new file mode 100644 index 00000000..533cd240 --- /dev/null +++ b/include/filesystem_layout.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +/* + * Filesystem Layout + * + * - implements different filesystem layouts for flutter artifacts + * + * Copyright (c) 2022, Hannes Winkler + */ + + +#ifndef _FLUTTERPI_INCLUDE_FILESYSTEM_LAYOUT_H +#define _FLUTTERPI_INCLUDE_FILESYSTEM_LAYOUT_H + +#include + +typedef struct flutter_paths *(*resolve_paths_t)(const char *app_bundle_path, enum flutter_runtime_mode runtime_mode); + +struct flutter_paths *fs_layout_flutterpi_resolve(const char *app_bundle_path, enum flutter_runtime_mode runtime_mode); +struct flutter_paths *fs_layout_metaflutter_resolve(const char *app_bundle_path, enum flutter_runtime_mode runtime_mode); + +#endif // _FLUTTERPI_INCLUDE_FILESYSTEM_LAYOUT_H diff --git a/include/flutter-pi.h b/include/flutter-pi.h index 2b6e3140..c0f0f7b2 100644 --- a/include/flutter-pi.h +++ b/include/flutter-pi.h @@ -215,12 +215,26 @@ struct frame { struct compositor; enum flutter_runtime_mode { - kDebug, kRelease + kDebug, kProfile, kRelease }; +#define FLUTTER_RUNTIME_MODE_IS_JIT(runtime_mode) ((runtime_mode) == kDebug) +#define FLUTTER_RUNTIME_MODE_IS_AOT(runtime_mode) ((runtime_mode) == kProfile || (runtime_mode) == kRelease) + struct plugin_registry; struct texture_registry; +struct flutter_paths { + char *app_bundle_path; + char *asset_bundle_path; + char *app_elf_path; + char *icudtl_path; + char *kernel_blob_path; + char *flutter_engine_path; + char *flutter_engine_dlopen_name; + char *flutter_engine_dlopen_name_fallback; +}; + struct flutterpi { /// graphics stuff struct { @@ -336,11 +350,9 @@ struct flutterpi { /// flutter stuff struct { - char *asset_bundle_path; - char *kernel_blob_path; - char *app_elf_path; + char *bundle_path; + struct flutter_paths *paths; void *app_elf_handle; - char *icu_data_path; FlutterLocale **locales; size_t n_locales; diff --git a/include/locales.h b/include/locales.h index 7a328be6..babbcab6 100644 --- a/include/locales.h +++ b/include/locales.h @@ -34,4 +34,6 @@ int locales_add_to_fl_engine(struct locales *locales, FlutterEngine engine, Flut const FlutterLocale *locales_on_compute_platform_resolved_locale(struct locales *locales, const FlutterLocale **fl_locales, size_t n_fl_locales); +void locales_print(const struct locales *locales); + #endif diff --git a/include/pixel_format.h b/include/pixel_format.h index cf03fc1c..7314ceb2 100644 --- a/include/pixel_format.h +++ b/include/pixel_format.h @@ -30,24 +30,24 @@ struct fbdev_pixfmt { * */ enum pixfmt { - kRGB565, - kARGB8888, - kXRGB8888, - kBGRA8888, - kRGBA8888, - kMax_PixFmt = kRGBA8888, + kRGB565_FpiPixelFormat, + kARGB8888_FpiPixelFormat, + kXRGB8888_FpiPixelFormat, + kBGRA8888_FpiPixelFormat, + kRGBA8888_FpiPixelFormat, + kMax_PixFmt = kRGBA8888_FpiPixelFormat, kCount_PixFmt = kMax_PixFmt + 1 }; // Just a pedantic check so we don't update the pixfmt enum without changing kMax_PixFmt -COMPILE_ASSERT(kMax_PixFmt == kRGBA8888); +COMPILE_ASSERT(kMax_PixFmt == kRGBA8888_FpiPixelFormat); #define PIXFMT_LIST(V) \ - V( "RGB 5:6:5", "RGB565", kRGB565, /*bpp*/ 16, /*opaque*/ true, /*R*/ 5, 11, /*G*/ 6, 5, /*B*/ 5, 0, /*A*/ 0, 0, /*GBM fourcc*/ GBM_FORMAT_RGB565, /*DRM fourcc*/ DRM_FORMAT_RGB565) \ - V("ARGB 8:8:8:8", "ARGB8888", kARGB8888, /*bpp*/ 32, /*opaque*/ false, /*R*/ 8, 16, /*G*/ 8, 8, /*B*/ 8, 0, /*A*/ 8, 24, /*GBM fourcc*/ GBM_FORMAT_ARGB8888, /*DRM fourcc*/ DRM_FORMAT_RGB565) \ - V("XRGB 8:8:8:8", "XRGB8888", kXRGB8888, /*bpp*/ 32, /*opaque*/ true, /*R*/ 8, 16, /*G*/ 8, 8, /*B*/ 8, 0, /*A*/ 0, 24, /*GBM fourcc*/ GBM_FORMAT_XRGB8888, /*DRM fourcc*/ DRM_FORMAT_XRGB8888) \ - V("BGRA 8:8:8:8", "BGRA8888", kBGRA8888, /*bpp*/ 32, /*opaque*/ false, /*R*/ 8, 8, /*G*/ 8, 16, /*B*/ 8, 24, /*A*/ 8, 0, /*GBM fourcc*/ GBM_FORMAT_BGRA8888, /*DRM fourcc*/ DRM_FORMAT_BGRA8888) \ - V("RGBA 8:8:8:8", "RGBA8888", kRGBA8888, /*bpp*/ 32, /*opaque*/ false, /*R*/ 8, 24, /*G*/ 8, 16, /*B*/ 8, 8, /*A*/ 8, 0, /*GBM fourcc*/ GBM_FORMAT_RGBA8888, /*DRM fourcc*/ DRM_FORMAT_RGBA8888) + V( "RGB 5:6:5", "RGB565", kRGB565_FpiPixelFormat, /*bpp*/ 16, /*opaque*/ true, /*R*/ 5, 11, /*G*/ 6, 5, /*B*/ 5, 0, /*A*/ 0, 0, /*GBM fourcc*/ GBM_FORMAT_RGB565, /*DRM fourcc*/ DRM_FORMAT_RGB565) \ + V("ARGB 8:8:8:8", "ARGB8888", kARGB8888_FpiPixelFormat, /*bpp*/ 32, /*opaque*/ false, /*R*/ 8, 16, /*G*/ 8, 8, /*B*/ 8, 0, /*A*/ 8, 24, /*GBM fourcc*/ GBM_FORMAT_ARGB8888, /*DRM fourcc*/ DRM_FORMAT_RGB565) \ + V("XRGB 8:8:8:8", "XRGB8888", kXRGB8888_FpiPixelFormat, /*bpp*/ 32, /*opaque*/ true, /*R*/ 8, 16, /*G*/ 8, 8, /*B*/ 8, 0, /*A*/ 0, 24, /*GBM fourcc*/ GBM_FORMAT_XRGB8888, /*DRM fourcc*/ DRM_FORMAT_XRGB8888) \ + V("BGRA 8:8:8:8", "BGRA8888", kBGRA8888_FpiPixelFormat, /*bpp*/ 32, /*opaque*/ false, /*R*/ 8, 8, /*G*/ 8, 16, /*B*/ 8, 24, /*A*/ 8, 0, /*GBM fourcc*/ GBM_FORMAT_BGRA8888, /*DRM fourcc*/ DRM_FORMAT_BGRA8888) \ + V("RGBA 8:8:8:8", "RGBA8888", kRGBA8888_FpiPixelFormat, /*bpp*/ 32, /*opaque*/ false, /*R*/ 8, 24, /*G*/ 8, 16, /*B*/ 8, 8, /*A*/ 8, 0, /*GBM fourcc*/ GBM_FORMAT_RGBA8888, /*DRM fourcc*/ DRM_FORMAT_RGBA8888) // make sure the macro list we defined has as many elements as the pixfmt enum. #define __COUNT(...) +1 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/compositor.c b/src/compositor.c index 3015a552..e95e5237 100644 --- a/src/compositor.c +++ b/src/compositor.c @@ -50,7 +50,7 @@ struct compositor compositor = { static struct view_cb_data *get_cbs_for_view_id_locked(int64_t view_id) { struct view_cb_data *data; - + for_each_pointer_in_cpset(&compositor.cbs, data) { if (data->view_id == view_id) { return data; @@ -62,11 +62,11 @@ static struct view_cb_data *get_cbs_for_view_id_locked(int64_t view_id) { static struct view_cb_data *get_cbs_for_view_id(int64_t view_id) { struct view_cb_data *data; - + cpset_lock(&compositor.cbs); data = get_cbs_for_view_id_locked(view_id); cpset_unlock(&compositor.cbs); - + return data; } @@ -98,7 +98,7 @@ static void destroy_gbm_bo( if (fb && fb->fb_id) drmModeRmFB(flutterpi.drm.drmdev->fd, fb->fb_id); - + free(fb); } @@ -142,7 +142,7 @@ static uint32_t gbm_bo_get_drm_fb_id(struct gbm_bo *bo) { if (ok) { if (flags) LOG_DEBUG("drm_fb_get_from_bo: modifiers failed!\n"); - + memcpy(handles, (uint32_t [4]){gbm_bo_get_handle(bo).u32,0,0,0}, 16); memcpy(strides, (uint32_t [4]){gbm_bo_get_stride(bo),0,0,0}, 16); memset(offsets, 0, 16); @@ -238,7 +238,7 @@ static int create_drm_rbo( */ glBindRenderbuffer(GL_RENDERBUFFER, 0); - + // glBindFramebuffer(GL_FRAMEBUFFER, 0); ok = drmModeAddFB2( @@ -391,7 +391,7 @@ static int rendertarget_gbm_present( printed = true; } } - + ok = drmdev_plane_supports_setting_zpos_value(atomic_req->drmdev, drm_plane_id, zpos, &supported); if (ok != 0) return ok; @@ -400,7 +400,7 @@ static int rendertarget_gbm_present( } else { static bool printed = false; - if (!printed) { + if (!printed) { LOG_ERROR( "GPU does not supported the desired HW plane order.\n" " Some UI layers may be invisible.\n" @@ -491,7 +491,7 @@ static int rendertarget_gbm_present_legacy( ((uint16_t) flutterpi.display.height) << 16 ); } - + // TODO: move this to the page flip handler. // We can only be sure the buffer can be released when the buffer swap // ocurred. @@ -505,10 +505,10 @@ static int rendertarget_gbm_present_legacy( /** * @brief Create a type of rendertarget that is backed by a GBM Surface, used for rendering into the DRM primary plane. - * + * * @param[out] out A pointer to the pointer of the created rendertarget. * @param[in] compositor The compositor which this rendertarget should be associated with. - * + * * @see rendertarget_gbm */ static int rendertarget_gbm_new( @@ -583,10 +583,10 @@ static int rendertarget_nogbm_present( drmdev_atomic_req_put_plane_property(req, drm_plane_id, "CRTC_Y", 0); drmdev_atomic_req_put_plane_property(req, drm_plane_id, "CRTC_W", flutterpi.display.width); drmdev_atomic_req_put_plane_property(req, drm_plane_id, "CRTC_H", flutterpi.display.height); - + ok = drmdev_plane_supports_setting_rotation_value(req->drmdev, drm_plane_id, DRM_MODE_ROTATE_0 | DRM_MODE_REFLECT_Y, &supported); if (ok != 0) return ok; - + if (supported) { drmdev_atomic_req_put_plane_property(req, drm_plane_id, "rotation", DRM_MODE_ROTATE_0 | DRM_MODE_REFLECT_Y); } else { @@ -604,13 +604,13 @@ static int rendertarget_nogbm_present( ok = drmdev_plane_supports_setting_zpos_value(req->drmdev, drm_plane_id, zpos, &supported); if (ok != 0) return ok; - + if (supported) { drmdev_atomic_req_put_plane_property(req, drm_plane_id, "zpos", zpos); } else { static bool printed = false; - if (!printed) { + if (!printed) { LOG_ERROR( "GPU does not supported the desired HW plane order.\n" " Some UI layers may be invisible.\n" @@ -681,10 +681,10 @@ static int rendertarget_nogbm_present_legacy( ((uint16_t) flutterpi.display.height) << 16 ); } - + ok = drmdev_plane_supports_setting_rotation_value(drmdev, drm_plane_id, DRM_MODE_ROTATE_0 | DRM_MODE_REFLECT_Y, &supported); if (ok != 0) return ok; - + if (supported) { drmdev_legacy_set_plane_property(drmdev, drm_plane_id, "rotation", DRM_MODE_ROTATE_0 | DRM_MODE_REFLECT_Y); } else { @@ -702,13 +702,13 @@ static int rendertarget_nogbm_present_legacy( ok = drmdev_plane_supports_setting_zpos_value(drmdev, drm_plane_id, zpos, &supported); if (ok != 0) return ok; - + if (supported) { drmdev_legacy_set_plane_property(drmdev, drm_plane_id, "zpos", zpos); } else { static bool printed = false; - if (!printed) { + if (!printed) { LOG_ERROR( "GPU does not supported the desired HW plane order.\n" " Some UI layers may be invisible.\n" @@ -722,10 +722,10 @@ static int rendertarget_nogbm_present_legacy( /** * @brief Create a type of rendertarget that is not backed by a GBM-Surface, used for rendering into DRM overlay planes. - * + * * @param[out] out A pointer to the pointer of the created rendertarget. * @param[in] compositor The compositor which this rendertarget should be associated with. - * + * * @see rendertarget_nogbm */ static int rendertarget_nogbm_new( @@ -805,13 +805,13 @@ static int rendertarget_nogbm_new( * @brief Called by flutter when the OpenGL FBO of a backing store should be destroyed. * Called on an internal engine-managed thread. This is actually called after the engine * calls @ref on_collect_backing_store. - * + * * @param[in] userdata The pointer to the struct flutterpi_backing_store that should be destroyed. */ static void on_destroy_backing_store_gl_fb(void *userdata) { struct flutterpi_backing_store *store; struct compositor *compositor; - + store = userdata; compositor = store->target->compositor; @@ -828,7 +828,7 @@ static void on_destroy_backing_store_gl_fb(void *userdata) { * @brief A callback invoked by the engine to release the backing store. The embedder may * collect any resources associated with the backing store. Invoked on an internal * engine-managed thread. This is actually called before the engine calls @ref on_destroy_backing_store_gl_fb. - * + * * @param[in] backing_store The backing store to be collected. * @param[in] userdata A pointer to the flutterpi compositor. */ @@ -840,7 +840,7 @@ static bool on_collect_backing_store( struct compositor *compositor; (void) userdata; - + store = backing_store->user_data; compositor = store->target->compositor; @@ -858,7 +858,7 @@ static bool on_collect_backing_store( /** * @brief A callback invoked by the engine to obtain a FlutterBackingStore for a specific FlutterLayer. * Called on an internal engine-managed thread. - * + * * @param[in] config The dimensions of the backing store to be created, post transform. * @param[out] backing_store_out The created backing store. * @param[in] userdata A pointer to the flutterpi compositor. @@ -1033,7 +1033,7 @@ static void fill_platform_view_params( for (int i = n_mutations - 1; i >= 0; i--) { if (mutations[i]->type == kFlutterPlatformViewMutationTypeTransformation) { apply_transform_to_quad(mutations[i]->transformation, &quad); - + double rotz = atan2(mutations[i]->transformation.skewX, mutations[i]->transformation.scaleX) * 180.0 / M_PI; if (rotz < 0) { rotz += 360; @@ -1087,7 +1087,7 @@ static bool on_present_layers( LOG_DEBUG(" backing store (offset: %f, %f. size: %f, %f)\n", layers[i]->offset.x, layers[i]->offset.y, layers[i]->size.width, layers[i]->size.height); } else { DEBUG_ASSERT(layers[i]->type == kFlutterLayerContentTypePlatformView); - + LOG_DEBUG(" platform view (id: %"PRId64", offset: %f, %f, size: %f, %f) mutations:\n", layers[i]->platform_view->identifier, layers[i]->offset.x, layers[i]->offset.y, layers[i]->size.width, layers[i]->size.height); for (size_t j = 0; j < layers[i]->platform_view->mutations_count; j++) { const FlutterPlatformViewMutation *mut = layers[i]->platform_view->mutations[j]; @@ -1183,7 +1183,7 @@ static bool on_present_layers( LOG_ERROR("Could not move cursor to front. Mouse cursor may be invisible. drmdev_plane_get_max_zpos_value: %s\n", strerror(ok)); continue; } - + ok = drmdev_plane_supports_setting_zpos_value(req->drmdev, plane->plane->plane_id, max_zpos, &supported); if (ok != 0) { LOG_ERROR("Could not move cursor to front. Mouse cursor may be invisible. drmdev_plane_supports_setting_zpos_value: %s\n", strerror(ok)); @@ -1210,7 +1210,7 @@ static bool on_present_layers( LOG_ERROR("Could not move cursor to front. Mouse cursor may be invisible. drmdev_plane_get_max_zpos_value: %s\n", strerror(ok)); continue; } - + ok = drmdev_plane_supports_setting_zpos_value(drmdev, plane->plane->plane_id, max_zpos, &supported); if (ok != 0) { LOG_ERROR("Could not move cursor to front. Mouse cursor may be invisible. drmdev_plane_supports_setting_zpos_value: %s\n", strerror(ok)); @@ -1226,10 +1226,10 @@ static bool on_present_layers( } } } - + compositor->has_applied_modeset = true; } - + // first, the state machine phase. // go through the layers, update // all platform views accordingly. @@ -1246,7 +1246,7 @@ static bool on_present_layers( void *updated_views_storage[layers_count]; memset(updated_views_storage, 0, layers_count * sizeof(void*)); struct pointer_set updated_views = PSET_INITIALIZER_STATIC(updated_views_storage, layers_count); - + for_each_pointer_in_cpset(&compositor->cbs, cb_data) { const FlutterLayer *layer = NULL; bool is_present = false; @@ -1304,7 +1304,7 @@ static bool on_present_layers( for_each_pointer_in_pset(&updated_views, cb_data) { const FlutterLayer *layer = NULL; - int zpos; + int zpos = 0; for (int i = 0; i < layers_count; i++) { if (layers[i]->type == kFlutterLayerContentTypePlatformView && @@ -1351,7 +1351,7 @@ static bool on_present_layers( for_each_pointer_in_pset(&mounted_views, cb_data) { const FlutterLayer *layer = NULL; - int zpos; + int zpos = 0; for (int i = 0; i < layers_count; i++) { if (layers[i]->type == kFlutterLayerContentTypePlatformView && @@ -1399,7 +1399,7 @@ static bool on_present_layers( } } } - + int64_t min_zpos; if (use_atomic_modesetting) { for_each_unreserved_plane_in_atomic_req(req, plane) { @@ -1529,7 +1529,7 @@ static bool on_present_layers( } eglMakeCurrent(stored_display, stored_read_surface, stored_write_surface, stored_context); - + if (use_atomic_modesetting) { do_commit: if (compositor->do_blocking_atomic_commits) { @@ -1537,7 +1537,7 @@ static bool on_present_layers( } else { req_flags |= DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT; } - + ok = drmdev_atomic_req_commit(req, req_flags, NULL); if ((compositor->do_blocking_atomic_commits == false) && (ok == EBUSY)) { LOG_ERROR( @@ -1556,7 +1556,7 @@ static bool on_present_layers( return false; } - drmdev_destroy_atomic_req(req); + drmdev_destroy_atomic_req(req); } cpset_unlock(&compositor->cbs); @@ -1719,7 +1719,7 @@ static int create_cursor_buffer(int width, int height, int bpp) { ok = drmModeAddFB(compositor.drmdev->fd, create_req.width, create_req.height, 32, create_req.bpp, create_req.pitch, create_req.handle, &drm_fb_id); if (ok < 0) { ok = errno; - LOG_ERROR("Could not make a DRM FB out of the hardware cursor buffer. drmModeAddFB: %s", strerror(errno)); + LOG_ERROR("Could not make a DRM FB out of the hardware cursor buffer. drmModeAddFB: %s\n", strerror(errno)); goto fail_destroy_dumb_buffer; } @@ -1729,14 +1729,14 @@ static int create_cursor_buffer(int width, int height, int bpp) { ok = ioctl(compositor.drmdev->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_req); if (ok < 0) { ok = errno; - LOG_ERROR("Could not prepare dumb buffer mmap for uploading the hardware cursor icon. ioctl: %s", strerror(errno)); + LOG_ERROR("Could not prepare dumb buffer mmap for uploading the hardware cursor icon. ioctl: %s\n", strerror(errno)); goto fail_rm_drm_fb; } buffer = mmap(0, create_req.size, PROT_READ | PROT_WRITE, MAP_SHARED, compositor.drmdev->fd, map_req.offset); if (buffer == MAP_FAILED) { ok = errno; - LOG_ERROR("Could not mmap dumb buffer for uploading the hardware cursor icon. mmap: %s", strerror(errno)); + LOG_ERROR("Could not mmap dumb buffer for uploading the hardware cursor icon. mmap: %s\n", strerror(errno)); goto fail_rm_drm_fb; } @@ -1749,7 +1749,7 @@ static int create_cursor_buffer(int width, int height, int bpp) { compositor.cursor.buffer_size = create_req.size; compositor.cursor.drm_fb_id = drm_fb_id; compositor.cursor.buffer = buffer; - + return 0; @@ -1801,7 +1801,7 @@ int compositor_apply_cursor_state( if ((compositor.cursor.is_enabled == false) || (compositor.cursor.current_rotation != rotation) || (compositor.cursor.current_cursor != cursor)) { int rotated_hot_x, rotated_hot_y; - + if (rotation == 0) { memcpy(compositor.cursor.buffer, cursor->data, compositor.cursor.buffer_size); rotated_hot_x = cursor->hot_x; @@ -1820,10 +1820,10 @@ int compositor_apply_cursor_state( buffer_x = y; buffer_y = cursor->width - x - 1; } - + int buffer_offset = compositor.cursor.buffer_pitch * buffer_y + (compositor.cursor.buffer_depth / 8) * buffer_x; int cursor_offset = cursor->width * y + x; - + compositor.cursor.buffer[buffer_offset / 4] = cursor->data[cursor_offset]; } } @@ -1843,13 +1843,6 @@ int compositor_apply_cursor_state( return EINVAL; } - compositor.cursor.current_rotation = rotation; - compositor.cursor.current_cursor = cursor; - compositor.cursor.cursor_size = cursor->width; - compositor.cursor.hot_x = rotated_hot_x; - compositor.cursor.hot_y = rotated_hot_y; - compositor.cursor.is_enabled = true; - ok = drmModeSetCursor2( compositor.drmdev->fd, compositor.drmdev->selected_crtc->crtc->crtc_id, @@ -1860,7 +1853,11 @@ int compositor_apply_cursor_state( rotated_hot_y ); if (ok < 0) { - LOG_ERROR("Could not set the mouse cursor buffer. drmModeSetCursor: %s", strerror(errno)); + if (errno == ENXIO) { + LOG_ERROR("Could not configure cursor. Hardware cursor is not supported by device.\n"); + } else { + LOG_ERROR("Could not set the mouse cursor buffer. drmModeSetCursor: %s\n", strerror(errno)); + } return errno; } @@ -1871,11 +1868,18 @@ int compositor_apply_cursor_state( compositor.cursor.y - compositor.cursor.hot_y ); if (ok < 0) { - LOG_ERROR("Could not move cursor. drmModeMoveCursor: %s", strerror(errno)); + LOG_ERROR("Could not move cursor. drmModeMoveCursor: %s\n", strerror(errno)); return errno; } + + compositor.cursor.current_rotation = rotation; + compositor.cursor.current_cursor = cursor; + compositor.cursor.cursor_size = cursor->width; + compositor.cursor.hot_x = rotated_hot_x; + compositor.cursor.hot_y = rotated_hot_y; + compositor.cursor.is_enabled = true; } - + return 0; } else if ((is_enabled == false) && (compositor.cursor.is_enabled == true)) { drmModeSetCursor( @@ -1905,7 +1909,7 @@ int compositor_set_cursor_pos(int x, int y) { int ok; if (compositor.cursor.is_enabled == false) { - return EINVAL; + return 0; } ok = drmModeMoveCursor(compositor.drmdev->fd, compositor.drmdev->selected_crtc->crtc->crtc_id, x - compositor.cursor.hot_x, y - compositor.cursor.hot_y); @@ -1915,7 +1919,7 @@ int compositor_set_cursor_pos(int x, int y) { } compositor.cursor.x = x; - compositor.cursor.y = y; + compositor.cursor.y = y; return 0; } @@ -1926,4 +1930,4 @@ const FlutterCompositor flutter_compositor = { .collect_backing_store_callback = on_collect_backing_store, .present_layers_callback = on_present_layers, .user_data = &compositor -}; \ No newline at end of file +}; diff --git a/src/filesystem_layout.c b/src/filesystem_layout.c new file mode 100644 index 00000000..bb150aa5 --- /dev/null +++ b/src/filesystem_layout.c @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT +/* + * Filesystem Layout + * + * - implements different filesystem layouts for flutter artifacts + * (libflutter_engine, icudtl, asset bundle, etc) + * + * Copyright (c) 2022, Hannes Winkler + */ + +#define _GNU_SOURCE +#include +#include +#include + +#include +#include + +FILE_DESCR("fs layout") + +static bool path_exists(const char *path) { + return access(path, R_OK) == 0; +} + +static struct flutter_paths *resolve( + const char *app_bundle_path, + enum flutter_runtime_mode runtime_mode, + const char *asset_bundle_subpath, + const char *icudtl_subpath, + const char *icudtl_system_path, + const char *icudtl_system_path_fallback, + const char *kernel_blob_subpath, + const char *app_elf_subpath, + const char *app_engine_subpath, + const char *engine_dlopen_name, + const char *engine_dlopen_name_fallback +) { + struct flutter_paths *paths; + char *dlopen_name_fallback_duped; + char *app_bundle_path_real; + char *dlopen_name_duped; + char *asset_bundle_path; + char *kernel_blob_path; + char *app_elf_path; + char *icudtl_path; + char *engine_path; + int ok; + + DEBUG_ASSERT_NOT_NULL(app_bundle_path); + DEBUG_ASSERT(icudtl_subpath || icudtl_system_path || icudtl_system_path_fallback); + DEBUG_ASSERT_MSG(!icudtl_system_path_fallback || icudtl_system_path, "icudtl.dat fallback system path is given, but no non-fallback system path."); + DEBUG_ASSERT_NOT_NULL(asset_bundle_subpath); + DEBUG_ASSERT_NOT_NULL(kernel_blob_subpath); + DEBUG_ASSERT_NOT_NULL(app_elf_subpath); + DEBUG_ASSERT(app_engine_subpath || engine_dlopen_name || engine_dlopen_name_fallback); + DEBUG_ASSERT_MSG(!engine_dlopen_name_fallback || engine_dlopen_name, "flutter engine fallback dlopen name is given, but no non-fallback dlopen name."); + + paths = malloc(sizeof *paths); + if (paths == NULL) { + return NULL; + } + + if (path_exists(app_bundle_path) == false) { + LOG_ERROR("App bundle directory \"%s\" does not exist.\n", app_bundle_path); + goto fail_free_paths; + } + + // Seems like the realpath will always not end with a slash. + app_bundle_path_real = realpath(app_bundle_path, NULL); + if (app_bundle_path_real == NULL) { + goto fail_free_paths; + } + + DEBUG_ASSERT(path_exists(app_bundle_path_real)); + + // Asset bundle path is the same as the app bundle path in the default filesystem layout, + // or /flutter_assets in meta-flutter dunfell / kirkstone layout. + ok = asprintf(&asset_bundle_path, "%s/%s", app_bundle_path_real, asset_bundle_subpath); + if (ok == -1) { + goto fail_free_app_bundle_path_real; + } + + if (path_exists(asset_bundle_path) == false) { + LOG_ERROR("Asset bundle directory \"%s\" does not exist.\n", asset_bundle_path); + goto fail_free_asset_bundle_path; + } + + // Find the icudtl.dat file. + // Mostly we look in /icudtl.dat or /data/icudtl.dat, /usr/share/flutter/icudtl.dat + // or /usr/lib/icudtl.dat. + icudtl_path = NULL; + + if (icudtl_subpath != NULL) { + ok = asprintf(&icudtl_path, "%s/%s", app_bundle_path_real, icudtl_subpath); + if (ok == -1) { + goto fail_free_asset_bundle_path; + } + } + + if (icudtl_system_path != NULL && (icudtl_path == NULL || path_exists(icudtl_path) == false)) { + if (icudtl_path != NULL) { + LOG_DEBUG("icudtl file not found at %s.\n", icudtl_path); + free(icudtl_path); + } + + icudtl_path = strdup(icudtl_system_path); + if (icudtl_path == NULL) { + goto fail_free_asset_bundle_path; + } + } + + DEBUG_ASSERT_NOT_NULL(icudtl_path); + + if (icudtl_system_path_fallback != NULL && path_exists(icudtl_path) == false) { + LOG_DEBUG("icudtl file not found at %s.\n", icudtl_path); + free(icudtl_path); + + icudtl_path = strdup(icudtl_system_path_fallback); + if (icudtl_path == NULL) { + goto fail_free_asset_bundle_path; + } + } + + DEBUG_ASSERT_NOT_NULL(icudtl_path); + + // We still haven't found it. Fail because we need it to run flutter. + if (path_exists(icudtl_path) == false) { + LOG_DEBUG("icudtl file not found at %s.\n", icudtl_path); + free(icudtl_path); + + LOG_ERROR("icudtl file not found!\n"); + goto fail_free_asset_bundle_path; + } + + // Find the kernel_blob.bin file. Only necessary for JIT (debug) mode. + ok = asprintf(&kernel_blob_path, "%s/%s", app_bundle_path_real, kernel_blob_subpath); + if (ok == -1) { + goto fail_free_asset_bundle_path; + } + + if (FLUTTER_RUNTIME_MODE_IS_JIT(runtime_mode) && !path_exists(kernel_blob_path)) { + LOG_ERROR("kernel blob file \"%s\" does not exist, but is necessary for debug mode.\n", kernel_blob_path); + goto fail_free_kernel_blob_path; + } + + // Find the app.so/libapp.so file. Only necessary for AOT (release/profile) mode. + ok = asprintf(&app_elf_path, "%s/%s", app_bundle_path_real, app_elf_subpath); + if (ok == -1) { + goto fail_free_kernel_blob_path; + } + + if (FLUTTER_RUNTIME_MODE_IS_AOT(runtime_mode) && !path_exists(app_elf_path)) { + LOG_ERROR("app elf file \"%s\" does not exist, but is necessary for release/profile mode.\n", app_elf_path); + goto fail_free_app_elf_path; + } + + // Try to find the engine inside the asset bundle. If we don't find it, that's not an error because + // it could still be inside /usr/lib and we can just dlopen it using the filename. + ok = asprintf(&engine_path, "%s/%s", app_bundle_path_real, app_engine_subpath); + if (ok == -1) { + goto fail_free_app_elf_path; + } + + if (path_exists(engine_path) == false) { + if (engine_dlopen_name == NULL && engine_dlopen_name_fallback == NULL) { + LOG_ERROR("flutter engine file \"%s\" does not exist.\n", engine_path); + goto fail_maybe_free_engine_path; + } + + free(engine_path); + engine_path = NULL; + } + + if (engine_dlopen_name != NULL) { + dlopen_name_duped = strdup(engine_dlopen_name); + if (dlopen_name_duped == NULL) { + goto fail_maybe_free_engine_path; + } + } else { + dlopen_name_duped = NULL; + } + + if (engine_dlopen_name_fallback != NULL) { + dlopen_name_fallback_duped = strdup(engine_dlopen_name_fallback); + if (dlopen_name_fallback_duped == NULL) { + goto fail_free_dlopen_name_duped; + } + } else { + dlopen_name_fallback_duped = NULL; + } + + + paths->app_bundle_path = app_bundle_path_real; + paths->asset_bundle_path = asset_bundle_path; + paths->icudtl_path = icudtl_path; + paths->kernel_blob_path = kernel_blob_path; + paths->app_elf_path = app_elf_path; + paths->flutter_engine_path = engine_path; + paths->flutter_engine_dlopen_name = dlopen_name_duped; + paths->flutter_engine_dlopen_name_fallback = dlopen_name_fallback_duped; + return paths; + + + fail_free_dlopen_name_duped: + free(dlopen_name_duped); + + fail_maybe_free_engine_path: + if (engine_path != NULL) { + free(engine_path); + } + + fail_free_app_elf_path: + free(app_elf_path); + + fail_free_kernel_blob_path: + free(kernel_blob_path); + + fail_free_asset_bundle_path: + free(asset_bundle_path); + + fail_free_app_bundle_path_real: + free(app_bundle_path_real); + + fail_free_paths: + free(paths); + return NULL; +} + +void flutter_paths_free(struct flutter_paths *paths) { + free(paths->app_bundle_path); + free(paths->asset_bundle_path); + free(paths->icudtl_path); + free(paths->kernel_blob_path); + free(paths->app_elf_path); + if (paths->flutter_engine_path != NULL) { + free(paths->flutter_engine_path); + } + if (paths->flutter_engine_dlopen_name != NULL) { + free(paths->flutter_engine_dlopen_name); + } + if (paths->flutter_engine_dlopen_name_fallback != NULL) { + free(paths->flutter_engine_dlopen_name_fallback); + } + free(paths); +} + +struct flutter_paths *fs_layout_flutterpi_resolve(const char *app_bundle_path, enum flutter_runtime_mode runtime_mode) { + return resolve( + app_bundle_path, + runtime_mode, + /* asset bundle subpath */ "", + /* icudtl subpath */ "icudtl.dat", + /* icudtl system path */ "/usr/share/flutter/icudtl.dat", + /* icudtl system path fallback */ "/usr/lib/icudtl.dat", + /* kernel blob subpath */ "kernel_blob.bin", + /* app elf subpath */ "app.so", + /* flutter engine subpath */ "libflutter_engine.so", + /* engine dlopen name */ runtime_mode == kDebug ? "libflutter_engine.so.debug" : + runtime_mode == kProfile ? "libflutter_engine.so.profile" : + "libflutter_engine.so.release", + /* engine dlopen name fallback */ "libflutter_engine.so" + ); +} + +struct flutter_paths *fs_layout_metaflutter_resolve(const char *app_bundle_path, enum flutter_runtime_mode runtime_mode) { + return resolve( + app_bundle_path, + runtime_mode, + /* asset bundle subpath */ "data/flutter_assets/", + /* icudtl subpath */ "data/icudtl.dat", + /* icudtl system path */ "/usr/share/flutter/icudtl.dat", + /* icudtl system path fallback */ NULL, + /* kernel blob subpath */ "data/flutter_assets/kernel_blob.bin", + /* app elf subpath */ "lib/libapp.so", + /* flutter engine subpath */ "lib/libflutter_engine.so", + /* engine dlopen name */ "libflutter_engine.so", + /* engine dlopen name fallback */ NULL + ); +} diff --git a/src/flutter-pi.c b/src/flutter-pi.c index 01f4c6bb..6fb473ff 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -55,6 +55,7 @@ #include #include #include +#include #ifdef ENABLE_MTRACE # include @@ -66,15 +67,19 @@ const char *const usage ="\ flutter-pi - run flutter apps on your Raspberry Pi.\n\ \n\ USAGE:\n\ - flutter-pi [options] [flutter engine options]\n\ + flutter-pi [options] [flutter engine options]\n\ \n\ OPTIONS:\n\ --release Run the app in release mode. The AOT snapshot\n\ - of the app (\"app.so\") must be located inside the\n\ - asset bundle directory.\n\ + of the app must be located inside the bundle directory.\n\ This also requires a libflutter_engine.so that was\n\ built with --runtime-mode=release.\n\ -\n\ + \n\ + --profile Run the app in profile mode. The AOT snapshot\n\ + of the app must be located inside the bundle directory.\n\ + This also requires a libflutter_engine.so that was\n\ + built with --runtime-mode=profile.\n\ + \n\ -o, --orientation Start the app in this orientation. Valid\n\ for are: portrait_up, landscape_left,\n\ portrait_down, landscape_right.\n\ @@ -867,7 +872,7 @@ struct texture *flutterpi_create_texture(struct flutterpi *flutterpi) { const char *flutterpi_get_asset_bundle_path( struct flutterpi *flutterpi ) { - return flutterpi->flutter.asset_bundle_path; + return flutterpi->flutter.paths->asset_bundle_path; } /// TODO: Make this refcounted if we're gonna use it from multiple threads. @@ -1243,6 +1248,16 @@ static int init_display(void) { continue; } + for_each_connector_in_drmdev(flutterpi.drm.drmdev, connector) { + if (connector->connector->connection == DRM_MODE_CONNECTED) { + goto found_connected_connector; + } + } + LOG_ERROR("Device \"%s\" doesn't have a display connected. Skipping.\n", device->nodes[DRM_NODE_PRIMARY]); + continue; + + + found_connected_connector: break; } @@ -1420,6 +1435,7 @@ static int init_display(void) { return -ok; } + locales_print(flutterpi.locales); printf( "===================================\n" "display mode:\n" @@ -1444,8 +1460,14 @@ static int init_display(void) { flutterpi.gbm.surface = gbm_surface_create_with_modifiers(flutterpi.gbm.device, flutterpi.display.width, flutterpi.display.height, flutterpi.gbm.format, &flutterpi.gbm.modifier, 1); if (flutterpi.gbm.surface == NULL) { - perror("[flutter-pi] Could not create GBM Surface. gbm_surface_create_with_modifiers"); - return errno; + perror("[flutter-pi] Could not create GBM Surface. gbm_surface_create_with_modifiers. Will attempt with gbm_surface_create"); + + flutterpi.gbm.surface = gbm_surface_create(flutterpi.gbm.device, flutterpi.display.width, flutterpi.display.height, flutterpi.gbm.format, GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR); + + if (flutterpi.gbm.surface == NULL) { + perror("[flutter-pi] Could not create GBM Surface even with gbm_surface_create"); + return errno; + } } /********************** @@ -1658,6 +1680,7 @@ static int init_display(void) { **************************/ static int init_application(void) { FlutterEngineAOTDataSource aot_source; + enum flutter_runtime_mode runtime_mode; struct libflutter_engine *libflutter_engine; struct texture_registry *texture_registry; struct plugin_registry *plugin_registry; @@ -1665,36 +1688,55 @@ static int init_application(void) { FlutterEngineAOTData aot_data; FlutterEngineResult engine_result; FlutterProjectArgs project_args = {0}; - void *libflutter_engine_handle; + void *engine_handle; int ok; - libflutter_engine_handle = NULL; - if (flutterpi.flutter.runtime_mode == kRelease) { - libflutter_engine_handle = dlopen("libflutter_engine.so.release", RTLD_LOCAL | RTLD_NOW); - if (libflutter_engine_handle == NULL) { - LOG_ERROR("[flutter-pi] Warning: Could not load libflutter_engine.so.release: %s. Trying to open libflutter_engine.so...\n", dlerror()); + runtime_mode = flutterpi.flutter.runtime_mode; + + engine_handle = NULL; + if (flutterpi.flutter.paths->flutter_engine_path != NULL) { + engine_handle = dlopen(flutterpi.flutter.paths->flutter_engine_path, RTLD_LOCAL | RTLD_NOW); + if (engine_handle == NULL) { + LOG_DEBUG( + "Info: Could not load flutter engine from app bundle. dlopen(\"%s\"): %s.\n", + flutterpi.flutter.paths->flutter_engine_path, + dlerror() + ); } - } else if (flutterpi.flutter.runtime_mode == kDebug) { - libflutter_engine_handle = dlopen("libflutter_engine.so.debug", RTLD_LOCAL | RTLD_NOW); - if (libflutter_engine_handle == NULL) { - LOG_ERROR("[flutter-pi] Warning: Could not load libflutter_engine.so.debug: %s. Trying to open libflutter_engine.so...\n", dlerror()); + } + + if (engine_handle == NULL && flutterpi.flutter.paths->flutter_engine_dlopen_name != NULL) { + engine_handle = dlopen(flutterpi.flutter.paths->flutter_engine_dlopen_name, RTLD_LOCAL | RTLD_NOW); + if (engine_handle == NULL) { + LOG_DEBUG( + "Info: Could not load flutter engine. dlopen(\"%s\"): %s.\n", + flutterpi.flutter.paths->flutter_engine_dlopen_name, + dlerror() + ); } } - if (libflutter_engine_handle == NULL) { - libflutter_engine_handle = dlopen("libflutter_engine.so", RTLD_LOCAL | RTLD_NOW); - if (libflutter_engine_handle == NULL) { - LOG_ERROR("Could not load libflutter_engine.so. dlopen: %s", dlerror()); - LOG_ERROR("Could not find a fitting libflutter_engine.\n"); - return EINVAL; + if (engine_handle == NULL && flutterpi.flutter.paths->flutter_engine_dlopen_name_fallback != NULL) { + engine_handle = dlopen(flutterpi.flutter.paths->flutter_engine_dlopen_name_fallback, RTLD_LOCAL | RTLD_NOW); + if (engine_handle == NULL) { + LOG_DEBUG( + "Info: Could not load flutter engine. dlopen(\"%s\"): %s.\n", + flutterpi.flutter.paths->flutter_engine_dlopen_name_fallback, + dlerror() + ); } } + if (engine_handle == NULL) { + LOG_ERROR("Error: Could not load flutter engine from any location. Make sure you have installed the engine binaries.\n"); + return EINVAL; + } + libflutter_engine = &flutterpi.flutter.libflutter_engine; # define LOAD_LIBFLUTTER_ENGINE_PROC(name) \ do { \ - libflutter_engine->name = dlsym(libflutter_engine_handle, #name); \ + libflutter_engine->name = dlsym(engine_handle, #name); \ if (!libflutter_engine->name) {\ perror("[flutter-pi] Could not resolve libflutter_engine procedure " #name ". dlsym"); \ return EINVAL; \ @@ -1777,8 +1819,8 @@ static int init_application(void) { // configure the project project_args = (FlutterProjectArgs) { .struct_size = sizeof(FlutterProjectArgs), - .assets_path = flutterpi.flutter.asset_bundle_path, - .icu_data_path = flutterpi.flutter.icu_data_path, + .assets_path = flutterpi.flutter.paths->asset_bundle_path, + .icu_data_path = flutterpi.flutter.paths->icudtl_path, .command_line_argc = flutterpi.flutter.engine_argc, .command_line_argv = (const char * const*) flutterpi.flutter.engine_argv, .platform_message_callback = on_platform_message, @@ -1805,7 +1847,8 @@ static int init_application(void) { .runs_task_on_current_thread_callback = runs_platform_tasks_on_current_thread, .post_task_callback = on_post_flutter_task }, - .render_task_runner = NULL + .render_task_runner = NULL, + .thread_priority_setter = NULL }, .shutdown_dart_vm_when_done = true, .compositor = &flutter_compositor, @@ -1818,14 +1861,14 @@ static int init_application(void) { }; bool engine_is_aot = libflutter_engine->FlutterEngineRunsAOTCompiledDartCode(); - if ((engine_is_aot == true) && (flutterpi.flutter.runtime_mode != kRelease)) { + if (engine_is_aot == true && runtime_mode == kDebug) { LOG_ERROR( - "The flutter engine was built for release (AOT) mode, but flutter-pi was not started up in release mode.\n" + "The flutter engine was built for release or profile (AOT) mode, but flutter-pi was not started up in release or profile mode.\n" "Either you swap out the libflutter_engine.so with one that was built for debug mode, or you start" - "flutter-pi with the --release flag and make sure a valid \"app.so\" is located inside the asset bundle directory.\n" + "flutter-pi with the --release or --profile flag and make sure a valid \"app.so\" is located inside the asset bundle directory.\n" ); return EINVAL; - } else if ((engine_is_aot == false) && (flutterpi.flutter.runtime_mode != kDebug)) { + } else if (engine_is_aot == false && runtime_mode != kDebug) { LOG_ERROR( "The flutter engine was built for debug mode, but flutter-pi was started up in release mode.\n" "Either you swap out the libflutter_engine.so with one that was built for release mode," @@ -1834,9 +1877,9 @@ static int init_application(void) { return EINVAL; } - if (flutterpi.flutter.runtime_mode == kRelease) { + if (flutterpi.flutter.runtime_mode != kDebug) { aot_source = (FlutterEngineAOTDataSource) { - .elf_path = flutterpi.flutter.app_elf_path, + .elf_path = flutterpi.flutter.paths->app_elf_path, .type = kFlutterEngineAOTDataSourceTypeElfPath }; @@ -1900,7 +1943,13 @@ static int init_application(void) { .struct_size = sizeof(FlutterWindowMetricsEvent), .width = flutterpi.view.width, .height = flutterpi.view.height, - .pixel_ratio = flutterpi.display.pixel_ratio + .pixel_ratio = flutterpi.display.pixel_ratio, + .left = 0, + .top = 0, + .physical_view_inset_top = 0, + .physical_view_inset_right = 0, + .physical_view_inset_bottom = 0, + .physical_view_inset_left = 0 } ); if (engine_result != kSuccess) { @@ -2105,44 +2154,19 @@ static int init_user_input(void) { return 0; } +static bool path_exists(const char *path) { + return access(path, R_OK) == 0; +} -static bool setup_paths(void) { - char *kernel_blob_path, *icu_data_path, *app_elf_path; - #define PATH_EXISTS(path) (access((path),R_OK)==0) - - if (!PATH_EXISTS(flutterpi.flutter.asset_bundle_path)) { - LOG_ERROR("Asset Bundle Directory \"%s\" does not exist\n", flutterpi.flutter.asset_bundle_path); - return false; - } - - asprintf(&kernel_blob_path, "%s/kernel_blob.bin", flutterpi.flutter.asset_bundle_path); - asprintf(&app_elf_path, "%s/app.so", flutterpi.flutter.asset_bundle_path); - - if (flutterpi.flutter.runtime_mode == kDebug) { - if (!PATH_EXISTS(kernel_blob_path)) { - LOG_ERROR("Could not find \"kernel.blob\" file inside \"%s\", which is required for debug mode.\n", flutterpi.flutter.asset_bundle_path); - return false; - } - } else if (flutterpi.flutter.runtime_mode == kRelease) { - if (!PATH_EXISTS(app_elf_path)) { - LOG_ERROR("Could not find \"app.so\" file inside \"%s\", which is required for release and profile mode.\n", flutterpi.flutter.asset_bundle_path); - return false; - } - } - - asprintf(&icu_data_path, "/usr/lib/icudtl.dat"); - if (!PATH_EXISTS(icu_data_path)) { - LOG_ERROR("Could not find \"icudtl.dat\" file inside \"/usr/lib/\".\n"); - return false; - } - - flutterpi.flutter.kernel_blob_path = kernel_blob_path; - flutterpi.flutter.icu_data_path = icu_data_path; - flutterpi.flutter.app_elf_path = app_elf_path; - - return true; - - #undef PATH_EXISTS +static struct flutter_paths *setup_paths(enum flutter_runtime_mode runtime_mode, const char *app_bundle_path) { +#if defined(FILESYSTEM_LAYOUT_DEFAULT) + return fs_layout_flutterpi_resolve(app_bundle_path, runtime_mode); +#elif defined(FILESYSTEM_LAYOUT_METAFLUTTER) + return fs_layout_metaflutter_resolve(app_bundle_path, runtime_mode); +#else + #error "Exactly one of FILESYSTEM_LAYOUT_DEFAULT or FILESYSTEM_LAYOUT_METAFLUTTER must be defined." + return NULL; +#endif } static bool parse_cmd_args(int argc, char **argv) { @@ -2153,6 +2177,7 @@ static bool parse_cmd_args(int argc, char **argv) { struct option long_options[] = { {"release", no_argument, &runtime_mode_int, kRelease}, + {"profile", no_argument, &runtime_mode_int, kProfile}, {"input", required_argument, NULL, 'i'}, {"orientation", required_argument, NULL, 'o'}, {"rotation", required_argument, NULL, 'r'}, @@ -2274,7 +2299,7 @@ static bool parse_cmd_args(int argc, char **argv) { return false; } - flutterpi.flutter.asset_bundle_path = strdup(argv[optind]); + flutterpi.flutter.bundle_path = realpath(argv[optind], NULL); flutterpi.flutter.runtime_mode = runtime_mode_int; argv[optind] = argv[0]; @@ -2285,6 +2310,7 @@ static bool parse_cmd_args(int argc, char **argv) { } int init(int argc, char **argv) { + struct flutter_paths *paths; int ok; #ifdef ENABLE_MTRACE @@ -2296,11 +2322,13 @@ int init(int argc, char **argv) { return EINVAL; } - ok = setup_paths(); - if (ok == false) { + paths = setup_paths(flutterpi.flutter.runtime_mode, flutterpi.flutter.bundle_path); + if (paths == NULL) { return EINVAL; } + flutterpi.flutter.paths = paths; + ok = init_main_loop(); if (ok != 0) { return ok; diff --git a/src/locales.c b/src/locales.c index a6773da8..3d3aabb8 100644 --- a/src/locales.c +++ b/src/locales.c @@ -169,7 +169,8 @@ static int add_locale_variants(struct concurrent_pointer_set *locales, const cha at = strchr(dot ? dot : (underscore ? underscore : locale_description), '@'); if (at != NULL) { - modifier = strdup(at); + //exclude @ + modifier = strdup(at + 1); if (modifier == NULL) { ok = ENOMEM; goto fail_return_ok; @@ -179,7 +180,8 @@ static int add_locale_variants(struct concurrent_pointer_set *locales, const cha } if (dot != NULL) { - codeset = strndup(dot, next_delim - dot); + //exclude . and @ + codeset = strndup(dot + 1, next_delim - dot - 1); if (codeset == NULL) { ok = ENOMEM; goto fail_free_modifier; @@ -188,7 +190,8 @@ static int add_locale_variants(struct concurrent_pointer_set *locales, const cha } if (underscore != NULL) { - territory = strndup(underscore, next_delim - underscore); + //exclude _ and . + territory = strndup(underscore + 1, next_delim - underscore - 1); if (territory == NULL) { ok = ENOMEM; goto fail_free_codeset; @@ -196,6 +199,7 @@ static int add_locale_variants(struct concurrent_pointer_set *locales, const cha next_delim = underscore; } + //nothing to exclude language = strndup(locale_description, next_delim - locale_description); if (language == NULL) { ok = ENOMEM; @@ -397,4 +401,44 @@ const FlutterLocale *locales_on_compute_platform_resolved_locale(struct locales (void) n_fl_locales; return fl_locales[0]; -} \ No newline at end of file +} + +void locales_print(const struct locales *locales) { + DEBUG_ASSERT(locales != NULL); + + printf("==============Locale==============\n"); + printf("Flutter locale:\n"); + if (locales->default_flutter_locale != NULL) { + printf(" default: %s", locales->default_flutter_locale->language_code); + if (locales->default_flutter_locale->country_code != NULL) { + printf("_%s", locales->default_flutter_locale->country_code); + } + if (locales->default_flutter_locale->script_code != NULL) { + printf(".%s", locales->default_flutter_locale->script_code); + } + if (locales->default_flutter_locale->variant_code != NULL) { + printf("@%s", locales->default_flutter_locale->variant_code); + } + + printf("\n"); + } else { + printf(" default: NULL\n"); + } + + printf(" locales:"); + for (size_t idx = 0; idx < locales->n_locales; idx++) { + const FlutterLocale *locale = locales->flutter_locales[idx]; + printf(" %s", locale->language_code); + if (locale->country_code != NULL) { + printf("_%s", locale->country_code); + } + if (locale->script_code != NULL) { + printf(".%s", locale->script_code); + } + if (locale->variant_code != NULL) { + printf("@%s", locale->variant_code); + } + } + + printf("\n===================================\n"); +} diff --git a/src/pluginregistry.c b/src/pluginregistry.c index 5210ac1c..73ca0c54 100644 --- a/src/pluginregistry.c +++ b/src/pluginregistry.c @@ -143,7 +143,9 @@ void plugin_registry_destroy(struct plugin_registry *registry) { plugin_registry_ensure_plugins_deinitialized(registry); for_each_pointer_in_cpset(®istry->plugins, instance) { + cpset_remove_locked(®istry->plugins, instance); free(instance); + instance = NULL; } cpset_deinit(®istry->callbacks); cpset_deinit(®istry->plugins); 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) diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index b9a37af5..b2830d98 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -328,7 +328,7 @@ static enum listener_return on_video_info_notify(void *arg, void *userdata) { return kNoAction; } - LOG_ERROR( + LOG_DEBUG( "Got video info: stream? %s, w x h: % 4d x % 4d, duration: %" GST_TIME_FORMAT "\n", !info->can_seek ? "yes" : "no", info->width, info->height, @@ -386,7 +386,7 @@ static int on_receive_evch( method = object->method; - LOG_ERROR("on_receive_evch\n"); + LOG_DEBUG("on_receive_evch\n"); player = get_player_by_evch(channel); if (player == NULL) { @@ -442,7 +442,7 @@ static int on_initialize( return respond_init_failed(responsehandle); } - LOG_ERROR("on_initialize\n"); + LOG_DEBUG("on_initialize\n"); // what do we even do here? @@ -702,7 +702,7 @@ static int on_create( goto fail_remove_receiver; } - LOG_ERROR("respond success on_create\n"); + LOG_DEBUG("respond success on_create\n"); return platch_respond_success_pigeon( responsehandle, diff --git a/src/plugins/omxplayer_video_player.c b/src/plugins/omxplayer_video_player.c index 5a2b729f..d6050c18 100644 --- a/src/plugins/omxplayer_video_player.c +++ b/src/plugins/omxplayer_video_player.c @@ -1148,7 +1148,7 @@ static int on_create( player->player_id = omxpvidpp.next_unused_player_id++; player->mgr = mgr; if (asset != NULL) { - snprintf(player->video_uri, sizeof(player->video_uri), "%s/%s", flutterpi.flutter.asset_bundle_path, asset); + snprintf(player->video_uri, sizeof(player->video_uri), "%s/%s", flutterpi.flutter.paths->asset_bundle_path, asset); } else { strncpy(player->video_uri, uri, sizeof(player->video_uri)); } diff --git a/src/plugins/text_input.c b/src/plugins/text_input.c index 9a295f10..65a3ca0f 100644 --- a/src/plugins/text_input.c +++ b/src/plugins/text_input.c @@ -32,20 +32,21 @@ struct text_input { * UTF8 utility functions */ static inline uint8_t utf8_symbol_length(uint8_t c) { - if (!(c & 0b10000000)) { - return 1; - } else if (!(c & 0b01000000)) { - // we are in a follow byte - return 0; - } else if (c & 0b00100000) { - return 2; - } else if (c & 0b00010000) { - return 3; - } else if (c & 0b00001000) { + if ((c & 0b11110000) == 0b11110000) { return 4; } - - return 0; + if ((c & 0b11100000) == 0b11100000) { + return 3; + } + if ((c & 0b11000000) == 0b11000000) { + return 2; + } + if ((c & 0b10000000) == 0b10000000) { + // XXX should we return 1 and don't care here? + DEBUG_ASSERT_MSG(false, "Invalid UTF-8 character"); + return 0; + } + return 1; } static inline uint8_t *symbol_at(unsigned int symbol_index) { @@ -875,23 +876,6 @@ static int sync_editing_state(void) { int textin_on_utf8_char(uint8_t *c) { if (text_input.connection_id == -1) return 0; - - switch (text_input.input_type) { - case kInputTypeNumber: - if (isdigit(*c)) { - break; - } else { - return 0; - } - case kInputTypePhone: - if (isdigit(*c) || *c == '*' || *c == '#' || *c == '+') { - break; - } else { - return 0; - } - default: - break; - } if (model_add_utf8_char(c)) return sync_editing_state(); diff --git a/src/texture_registry.c b/src/texture_registry.c index 43f91d22..ef7e7ef7 100644 --- a/src/texture_registry.c +++ b/src/texture_registry.c @@ -45,6 +45,13 @@ struct texture { /// The frame that's scheduled to be displayed next. The texture holds a reference to this frame. /// If a new frame is scheduled using @ref texture_push_frame, the reference will be dropped. struct counted_texture_frame *next_frame; + + /** + * @brief True if next_frame was not yet fetched by the engine. So if @ref texture_push_frame is called, + * we can just replace @ref next_frame with the new frame and don't need to call mark frame available again. + * + */ + bool dirty; }; struct texture_registry *texture_registry_new( @@ -210,6 +217,7 @@ struct texture *texture_new(struct texture_registry *reg) { texture->registry = reg; texture->id = id; texture->next_frame = NULL; + texture->dirty = false; ok = texture_registry_register_texture(reg, texture); if (ok != 0) { @@ -244,10 +252,18 @@ int texture_push_frame(struct texture *texture, const struct texture_frame *fram } texture->next_frame = counted_frame; + + if (texture->dirty == false) { + ok = texture_registry_engine_notify_frame_available(texture->registry, texture->id); + if (ok != 0) { + /// TODO: Don't really know what do to here. + } - ok = texture_registry_engine_notify_frame_available(texture->registry, texture->id); - if (ok != 0) { - /// TODO: Don't really know what do to here. + /// We have now called @ref texture_registry_engine_notify_frame_available available. + /// If a new frame is pushed with @ref texture_push_frame, and the engine + /// hasn't yet fetched the frame with the @ref texture_gl_external_texture_frame_callback, + /// we can just replace the next frame without notifying the engine again. + texture->dirty = true; } texture_unlock(texture); @@ -289,6 +305,7 @@ static bool texture_gl_external_texture_frame_callback( DEBUG_ASSERT_NOT_NULL(texture_out); texture_lock(texture); + if (texture->next_frame != NULL) { /// TODO: If acquiring the texture frame fails, flutter will destroy the texture frame two times. /// So we'll probably have a segfault if that happens. @@ -296,6 +313,11 @@ static bool texture_gl_external_texture_frame_callback( } else { frame = NULL; } + + /// flutter has now fetched the texture, so if we want to present a new frame + /// we need to call @ref texture_registry_engine_notify_frame_available again. + texture->dirty = false; + texture_unlock(texture); // only actually fill out the frame info when we have a frame. diff --git a/src/user_input.c b/src/user_input.c index 87c1e73d..e1e5ec29 100644 --- a/src/user_input.c +++ b/src/user_input.c @@ -17,6 +17,9 @@ FILE_DESCR("user input") +#define LIBINPUT_VER(major, minor, patch) ((((major) & 0xFF) << 16) | (((minor) & 0xFF) << 8) | ((patch) & 0xFF)) +#define THIS_LIBINPUT_VER LIBINPUT_VER(LIBINPUT_VERSION_MAJOR, LIBINPUT_VERSION_MINOR, LIBINPUT_VERSION_PATCH) + struct input_device_data { int64_t flutter_device_id_offset; struct keyboard_state *keyboard_state; @@ -31,6 +34,12 @@ struct input_device_data { * Only applies to devices which have LIBINPUT_DEVICE_CAP_POINTER. */ bool has_emitted_pointer_events; + + /** + * @brief Only applies to tablets. True if the tablet tool is in contact with the screen right now. + * + */ + bool tip; }; struct user_input { @@ -335,6 +344,7 @@ static int on_device_added(struct user_input *input, struct libinput_event *even data->buttons = 0; data->timestamp = timestamp; data->has_emitted_pointer_events = false; + data->tip = false; libinput_device_set_user_data(device, data); @@ -366,6 +376,19 @@ static int on_device_added(struct user_input *input, struct libinput_event *even // If we don't have a keyboard config data->keyboard_state = NULL; } + } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TABLET_TOOL)) { + device_id = input->next_unused_flutter_device_id++; + + /// TODO: Use kFlutterPointerDeviceKindStylus here + emit_pointer_events( + input, + &FLUTTER_POINTER_TOUCH_ADD_EVENT( + timestamp, + 0.0, 0.0, + device_id + ), + 1 + ); } else { // We don't handle this device, so we don't need the data. libinput_device_set_user_data(device, NULL); @@ -935,6 +958,135 @@ static int on_touch_frame(struct user_input *input, struct libinput_event *event return 0; } +static int on_tablet_tool_axis(struct user_input *input, struct libinput_event *event) { + struct libinput_event_tablet_tool *tablet_event; + struct input_device_data *data; + uint64_t timestamp; + int64_t device_id; + double x, y; + + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + data = libinput_device_get_user_data(libinput_event_get_device(event)); + DEBUG_ASSERT_NOT_NULL(data); + + tablet_event = libinput_event_get_tablet_tool_event(event); + timestamp = libinput_event_tablet_tool_get_time_usec(tablet_event); + + device_id = data->flutter_device_id_offset; + + // Only report down events when the tool is in contact with the tablet. + /// TODO: Maybe report hover events when it's not in contact? + /// FIXME: Use kFlutterPointerDeviceKindStylus here + if (data->tip) { + x = libinput_event_tablet_tool_get_x_transformed(tablet_event, input->display_width - 1); + y = libinput_event_tablet_tool_get_y_transformed(tablet_event, input->display_height - 1); + + apply_flutter_transformation(input->display_to_view_transform, &x, &y); + + emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_MOVE_EVENT(timestamp, x, y, device_id), 1); + } + + return 0; +} + +static int on_tablet_tool_proximity(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + (void) input; + (void) event; + + return 0; +} + +static int on_tablet_tool_tip(struct user_input *input, struct libinput_event *event) { + struct libinput_event_tablet_tool *tablet_event; + struct input_device_data *data; + uint64_t timestamp; + int64_t device_id; + double x, y; + + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + data = libinput_device_get_user_data(libinput_event_get_device(event)); + DEBUG_ASSERT_NOT_NULL(data); + + tablet_event = libinput_event_get_tablet_tool_event(event); + timestamp = libinput_event_tablet_tool_get_time_usec(tablet_event); + + device_id = data->flutter_device_id_offset; + + x = libinput_event_tablet_tool_get_x_transformed(tablet_event, input->display_width - 1); + y = libinput_event_tablet_tool_get_y_transformed(tablet_event, input->display_height - 1); + + apply_flutter_transformation(input->display_to_view_transform, &x, &y); + + /// FIXME: Use kFlutterPointerDeviceKindStylus here + if (libinput_event_tablet_tool_get_tip_state(tablet_event) == LIBINPUT_TABLET_TOOL_TIP_DOWN) { + data->tip = true; + emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_DOWN_EVENT(timestamp, x, y, device_id), 1); + } else { + data->tip = false; + emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_UP_EVENT(timestamp, x, y, device_id), 1); + } + + return 0; +} + +static int on_tablet_tool_button(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + (void) input; + (void) event; + + return 0; +} + +static int on_tablet_pad_button(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + (void) input; + (void) event; + + return 0; +} + +static int on_tablet_pad_ring(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + (void) input; + (void) event; + + return 0; +} + +static int on_tablet_pad_strip(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + (void) input; + (void) event; + + return 0; +} + +#if THIS_LIBINPUT_VER >= LIBINPUT_VER(1, 15, 0) +static int on_tablet_pad_key(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT_NOT_NULL(input); + DEBUG_ASSERT_NOT_NULL(event); + + (void) input; + (void) event; + + return 0; +} +#endif static int process_libinput_events(struct user_input *input, uint64_t timestamp) { enum libinput_event_type event_type; @@ -1020,6 +1172,56 @@ static int process_libinput_events(struct user_input *input, uint64_t timestamp) goto fail_destroy_event; } break; + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + ok = on_tablet_tool_axis(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: + ok = on_tablet_tool_proximity(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TABLET_TOOL_TIP: + ok = on_tablet_tool_tip(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TABLET_TOOL_BUTTON: + ok = on_tablet_tool_button(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TABLET_PAD_BUTTON: + ok = on_tablet_pad_button(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TABLET_PAD_RING: + ok = on_tablet_pad_ring(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TABLET_PAD_STRIP: + ok = on_tablet_pad_strip(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; +#if THIS_LIBINPUT_VER >= LIBINPUT_VER(1, 15, 0) + case LIBINPUT_EVENT_TABLET_PAD_KEY: + ok = on_tablet_pad_key(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; +#endif default: break; }