diff --git a/CHANGELOG.md b/CHANGELOG.md index 361d746..75aeb15 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +1,52 @@ -## 0.1.7 - -* Add `DesktopDropX` + `DropTarget` — OS-level drag & drop of files and - images into the app, powered by a vendored desktop_drop engine (v0.7.1, - Apache-2.0): GTK uri-list drag destination on Linux, OLE `IDropTarget` - (`CF_HDROP`) on Windows, dead code on mobile and web. `DesktopDropX` - provides `pathsOf` (dropped file paths) and `readImage` (magic-byte - image sniffing, same table as `ClipboardX.pasteImage`). -* The Linux/Windows plugin entry is now a combined `FoxUtilsPlugin` - registration that wires the vendored pasteboard and desktop_drop engines - from one native entry per platform (the registered plugin class changed - from `PasteboardPlugin` to `FoxUtilsPlugin` — generated registrants - regenerate on the next build). -* New dependencies: `cross_file` (drop items are `XFile`s) and - `universal_platform` (web-safe platform checks), both required by the - vendored engine. - -## 0.1.6 - -* Add `ClipboardX.pasteImage` — reads an image from the clipboard whether it - was copied as image data (browser) or as a file reference (file manager), - on Windows, Linux and Android. Adds a `readFileBytes` channel method for - resolving Android content URIs. - -## 0.1.5 - -* Add `ClipboardX` — clipboard image/files/text copy via a vendored - pasteboard engine (MixinNetwork, Apache-2.0) with modernized builds: - the Android channel is Java (no KGP), the FileProvider is merged from - the plugin manifest (zero per-project setup), image writes are byte-faithful - with magic-byte MIME sniffing, and two upstream bugs are fixed. - -## 0.1.4 - -* Add `AndroidFiles.save` — permission-free file saving to public Android - directories (Pictures/Movies/Music/Download) with an optional media scan. - Android platform channel, no storage permissions required. - -## 0.0.1 - +## 0.1.8 + +* Linux, debug builds only: a Flutter hot restart no longer crashes apps that + use media_kit with live players. Hot restart frees the old isolate's + NativeCallables while the mpv handles live on; media_kit then sends the + leftovers "quit" and mpv calls a dead wakeup callback (SIGSEGV on mpv's + "core" thread). The plugin clears every live handle's wakeup callback on + the engine's `on-pre-engine-restart` signal (handles found through + media_kit's NativeReferenceHolder file). Automatic: depending on fox_utils + is enough. Relies on media_kit internals (checked against 1.2.6). + +## 0.1.7 + +* Add `DesktopDropX` + `DropTarget` — OS-level drag & drop of files and + images into the app, powered by a vendored desktop_drop engine (v0.7.1, + Apache-2.0): GTK uri-list drag destination on Linux, OLE `IDropTarget` + (`CF_HDROP`) on Windows, dead code on mobile and web. `DesktopDropX` + provides `pathsOf` (dropped file paths) and `readImage` (magic-byte + image sniffing, same table as `ClipboardX.pasteImage`). +* The Linux/Windows plugin entry is now a combined `FoxUtilsPlugin` + registration that wires the vendored pasteboard and desktop_drop engines + from one native entry per platform (the registered plugin class changed + from `PasteboardPlugin` to `FoxUtilsPlugin` — generated registrants + regenerate on the next build). +* New dependencies: `cross_file` (drop items are `XFile`s) and + `universal_platform` (web-safe platform checks), both required by the + vendored engine. + +## 0.1.6 + +* Add `ClipboardX.pasteImage` — reads an image from the clipboard whether it + was copied as image data (browser) or as a file reference (file manager), + on Windows, Linux and Android. Adds a `readFileBytes` channel method for + resolving Android content URIs. + +## 0.1.5 + +* Add `ClipboardX` — clipboard image/files/text copy via a vendored + pasteboard engine (MixinNetwork, Apache-2.0) with modernized builds: + the Android channel is Java (no KGP), the FileProvider is merged from + the plugin manifest (zero per-project setup), image writes are byte-faithful + with magic-byte MIME sniffing, and two upstream bugs are fixed. + +## 0.1.4 + +* Add `AndroidFiles.save` — permission-free file saving to public Android + directories (Pictures/Movies/Music/Download) with an optional media scan. + Android platform channel, no storage permissions required. + +## 0.0.1 + * TODO: Describe initial release. \ No newline at end of file diff --git a/README.md b/README.md index 0f2928d..b35da78 100755 --- a/README.md +++ b/README.md @@ -1,183 +1,193 @@ -# fox_utils - -Reusable Flutter utilities shared across projects: permission-free Android -file saving, a cross-platform clipboard wrapper, and desktop drag & drop. - -## Features - -### `AndroidFiles.save` — save files to public Android storage - -Writes any bytes to a public Android directory (`Pictures`, `Movies`, -`Music`, `Download`) **without any storage permission**, with an optional -media scan so the file shows up in gallery apps. - -```dart -final path = await AndroidFiles.save( - bytes, // any file type - fileName: 'image.jpg', - location: AndroidPublicLocation.pictures, // pictures | movies | music | downloads - subPath: 'Imagen', // folder structure under the location - scan: true, // false = saved but not indexed (no gallery) -); -``` - -Notes: - -- Requires **no permissions** on Android 10+ (API 29+): creating new files - inside media collections is permission-free. Legacy devices (< API 29) may - fail — the exception carries the native message. -- `scan: false` writes the file without indexing it. On modern Android the - file can still be picked up by a later system rescan; for a hard - "never in gallery" guarantee, save outside a media collection or add a - `.nomedia` file to the folder. -- Throws `FileSaveException` on failure (logged with stack trace). - -### `ClipboardX` — clipboard copy - -```dart -await ClipboardX.copyImage(bytes); // image bytes as-is, MIME sniffed -await ClipboardX.copyFiles(['/path/a.png']); // desktop: files -ClipboardX.copyText('hello'); // text (Flutter built-in) -final text = await ClipboardX.readText(); -final image = await ClipboardX.readImage(); -final files = await ClipboardX.readFiles(); -``` - -Platform matrix: - -| Operation | Android | Windows | Linux | macOS | iOS | -|---|---|---|---|---|---| -| copy image | ✓ (FileProvider) | ✓ (GDI+) | ✓ (file reference) | ✓ | ✓ | -| copy files | – | ✓ (CF_HDROP) | ✓ (GNOME) | ✓ | – | -| copy/read text | ✓ | ✓ | ✓ | ✓ | ✓ | -| read image | ✓ | ✓ | ✓ | ✓ | ✓ | -| read files | ✓ (URIs) | ✓ | ✓ | ✓ | – | - -Notes: - -- Image bytes are placed **as-is** (no re-encode); the extension is sniffed - from magic bytes (PNG/JPEG/GIF/WebP/BMP), so animated GIFs survive. -- **Linux cannot hold raw image bytes** on the clipboard (GTK limitation), so - `copyImage` stages the bytes to a rotating temp file and copies it as a - file reference — pasted apps receive the image file. The staging file is - replaced on each copy. -- **Android requires no manifest setup**: the FileProvider is declared in - this plugin's manifest and merged automatically (`${applicationId}.provider`). - Images are staged in the app cache dir. -- Throws `ClipboardCopyException` on failure. - -### `ClipboardX.pasteImage` — paste an image, whatever the source - -```dart -final Uint8List? image = await ClipboardX.pasteImage(); -if (image != null) { - // send to server, display, whatever -} -``` - -Resolution order: image data on the clipboard first (browser "copy image"), -then the first file reference (file manager "copy"). The file's first bytes -are sniffed (PNG/JPEG/GIF/WebP/BMP) before the full read, so huge non-image -files are never loaded. Returns `null` when the clipboard holds no image. - -Source matrix: - -| Copied from | Windows | Linux | Android | -|---|---|---|---| -| Browser, "copy image" | CF_DIB → image bytes | `image/png` → image bytes | content URI (image mime) → image bytes | -| File manager, "copy" | CF_HDROP → first file's bytes | `text/uri-list` → first file's bytes | content URI → resolved bytes | - -Android notes: - -- Reading the clipboard works **only while the app is in the foreground** - (Android 10+); background reads return `null`. -- Android 12+ shows a system toast ("app pasted from your clipboard") on - read — cosmetic. -- File references come back as `content://` URIs; `pasteImage` resolves them - through the platform channel (`readFileBytes`), so no manifest setup or - permission is needed. - -### `DesktopDropX` + `DropTarget` — drag & drop files and images - -OS-level drag & drop into the app (desktop only). Wrap any widget in -`DropTarget` to get drag enter/exit/update/drop callbacks; `DesktopDropX` -turns the dropped items into plain paths or image bytes: - -```dart -DropTarget( - onDragEntered: (d) => setState(() => dragging = true), - onDragExited: (d) => setState(() => dragging = false), - onDragDone: (details) async { - final paths = DesktopDropX.pathsOf(details.files); // absolute paths - final Uint8List? image = await DesktopDropX.readImage(details.files); - // image == null when the drop contains no readable image - }, - child: myWidget, -) -``` - -Platform notes: - -- **Linux**: GTK drag destination on the whole window (`text/uri-list` from - GNOME/KDE file managers, browsers, …). Items are absolute paths, delivered - in the order dropped. -- **Windows**: OLE `IDropTarget` on the window (`CF_HDROP` from Explorer, - browsers, …). Same path semantics; `DropDoneDetails.localPosition` is - already DPI-corrected. -- **Android/iOS/web**: no native handler — the widget renders its child and - no drop events ever arrive. **macOS** is not vendored (no macOS target); - re-adding it is a copy of upstream's `macos/` folder behind the same API. -- `DropItem` extends `XFile`: `item.path`, `item.name`, `item.readAsBytes()`, - `item.openRead()`, … are available directly. -- Drop targets receive events for the **whole window**; an invisible - `DropTarget` keeps receiving them, so set `enable: false` when pushing a - page over it. Coordinate-based `DropTarget` bounds filtering is handled by - the widget. -- `readImage` never throws: unreadable files are logged with a stack trace - and skipped; `null` means "no image in this drop". - -## Vendored engines - -The clipboard channels (`pasteboard` method channel) are **vendored from -MixinNetwork's `pasteboard` package (v0.5.0, Apache-2.0)** because upstream's -build applies the legacy Kotlin Gradle Plugin (KGP), which future Flutter -versions will reject. The vendored copy lives in `lib/vendor/` (Dart) and the -native platform folders, with these changes: - -- **Android**: the Kotlin plugin was ported to Java and merged into - `FoxUtilsPlugin` (one plugin class, no KGP, no Kotlin in the module). -- **Byte-faithful image writes**: upstream decodes and re-encodes images as - PNG (destroying GIF animation and metadata); ours writes the original bytes - with magic-byte MIME sniffing. -- **Merged FileProvider**: declared in the plugin manifest — zero per-project - setup (upstream requires a manual manifest entry per app). -- **Two upstream bugs fixed**: clipboard image reads ran synchronously on the - calling thread (now on the executor), and `writeFiles` dropped the first - clip item (now assembles correctly). -- **Linux/Windows**: C++ sources byte-identical, include paths and CMake - targets renamed to the `fox_utils` package name. - -To diff against upstream: `git diff` the folders against -`packages/pasteboard/` at tag `v0.5.0` of -`https://github.com/MixinNetwork/flutter-plugins`. If upstream ever -modernizes their build, un-vendoring is a swap behind the same `ClipboardX` -API. - -The drag & drop channels (`desktop_drop` method channel) are **vendored from -MixinNetwork's `desktop_drop` package (v0.7.1, Apache-2.0)** for the same -reason: its `android/` folder applies KGP. Only the Dart sources and the -Linux/Windows native folders are vendored — the Android/Kotlin, macOS and -web bindings are omitted (Android DnD is out of scope, and there is no macOS -target in the app). Linux/Windows native sources are byte-identical to -upstream except the include path rename (`include/fox_utils/`). Both engines -are registered from one native entry per platform: -`fox_utils_plugin_register_with_registrar` (Linux) / -`FoxUtilsPluginRegisterWithRegistrar` (Windows) — the plugin class in -`pubspec.yaml` is no longer `PasteboardPlugin`. To diff against upstream, use -tag `desktop_drop-v0.7.1` of the same monorepo; un-vendoring is a swap -behind the same `DesktopDropX` API. - -## License - -MIT for this package's own code; the vendored engines are Apache-2.0 -© MixinNetwork (see `lib/vendor/` and the native platform folders). +# fox_utils + +Reusable Flutter utilities shared across projects: permission-free Android +file saving, a cross-platform clipboard wrapper, and desktop drag & drop. + +## Features + +### `AndroidFiles.save` — save files to public Android storage + +Writes any bytes to a public Android directory (`Pictures`, `Movies`, +`Music`, `Download`) **without any storage permission**, with an optional +media scan so the file shows up in gallery apps. + +```dart +final path = await AndroidFiles.save( + bytes, // any file type + fileName: 'image.jpg', + location: AndroidPublicLocation.pictures, // pictures | movies | music | downloads + subPath: 'Imagen', // folder structure under the location + scan: true, // false = saved but not indexed (no gallery) +); +``` + +Notes: + +- Requires **no permissions** on Android 10+ (API 29+): creating new files + inside media collections is permission-free. Legacy devices (< API 29) may + fail — the exception carries the native message. +- `scan: false` writes the file without indexing it. On modern Android the + file can still be picked up by a later system rescan; for a hard + "never in gallery" guarantee, save outside a media collection or add a + `.nomedia` file to the folder. +- Throws `FileSaveException` on failure (logged with stack trace). + +### `ClipboardX` — clipboard copy + +```dart +await ClipboardX.copyImage(bytes); // image bytes as-is, MIME sniffed +await ClipboardX.copyFiles(['/path/a.png']); // desktop: files +ClipboardX.copyText('hello'); // text (Flutter built-in) +final text = await ClipboardX.readText(); +final image = await ClipboardX.readImage(); +final files = await ClipboardX.readFiles(); +``` + +Platform matrix: + +| Operation | Android | Windows | Linux | macOS | iOS | +|---|---|---|---|---|---| +| copy image | ✓ (FileProvider) | ✓ (GDI+) | ✓ (file reference) | ✓ | ✓ | +| copy files | – | ✓ (CF_HDROP) | ✓ (GNOME) | ✓ | – | +| copy/read text | ✓ | ✓ | ✓ | ✓ | ✓ | +| read image | ✓ | ✓ | ✓ | ✓ | ✓ | +| read files | ✓ (URIs) | ✓ | ✓ | ✓ | – | + +Notes: + +- Image bytes are placed **as-is** (no re-encode); the extension is sniffed + from magic bytes (PNG/JPEG/GIF/WebP/BMP), so animated GIFs survive. +- **Linux cannot hold raw image bytes** on the clipboard (GTK limitation), so + `copyImage` stages the bytes to a rotating temp file and copies it as a + file reference — pasted apps receive the image file. The staging file is + replaced on each copy. +- **Android requires no manifest setup**: the FileProvider is declared in + this plugin's manifest and merged automatically (`${applicationId}.provider`). + Images are staged in the app cache dir. +- Throws `ClipboardCopyException` on failure. + +### `ClipboardX.pasteImage` — paste an image, whatever the source + +```dart +final Uint8List? image = await ClipboardX.pasteImage(); +if (image != null) { + // send to server, display, whatever +} +``` + +Resolution order: image data on the clipboard first (browser "copy image"), +then the first file reference (file manager "copy"). The file's first bytes +are sniffed (PNG/JPEG/GIF/WebP/BMP) before the full read, so huge non-image +files are never loaded. Returns `null` when the clipboard holds no image. + +Source matrix: + +| Copied from | Windows | Linux | Android | +|---|---|---|---| +| Browser, "copy image" | CF_DIB → image bytes | `image/png` → image bytes | content URI (image mime) → image bytes | +| File manager, "copy" | CF_HDROP → first file's bytes | `text/uri-list` → first file's bytes | content URI → resolved bytes | + +Android notes: + +- Reading the clipboard works **only while the app is in the foreground** + (Android 10+); background reads return `null`. +- Android 12+ shows a system toast ("app pasted from your clipboard") on + read — cosmetic. +- File references come back as `content://` URIs; `pasteImage` resolves them + through the platform channel (`readFileBytes`), so no manifest setup or + permission is needed. + +### `DesktopDropX` + `DropTarget` — drag & drop files and images + +OS-level drag & drop into the app (desktop only). Wrap any widget in +`DropTarget` to get drag enter/exit/update/drop callbacks; `DesktopDropX` +turns the dropped items into plain paths or image bytes: + +```dart +DropTarget( + onDragEntered: (d) => setState(() => dragging = true), + onDragExited: (d) => setState(() => dragging = false), + onDragDone: (details) async { + final paths = DesktopDropX.pathsOf(details.files); // absolute paths + final Uint8List? image = await DesktopDropX.readImage(details.files); + // image == null when the drop contains no readable image + }, + child: myWidget, +) +``` + +Platform notes: + +- **Linux**: GTK drag destination on the whole window (`text/uri-list` from + GNOME/KDE file managers, browsers, …). Items are absolute paths, delivered + in the order dropped. +- **Windows**: OLE `IDropTarget` on the window (`CF_HDROP` from Explorer, + browsers, …). Same path semantics; `DropDoneDetails.localPosition` is + already DPI-corrected. +- **Android/iOS/web**: no native handler — the widget renders its child and + no drop events ever arrive. **macOS** is not vendored (no macOS target); + re-adding it is a copy of upstream's `macos/` folder behind the same API. +- `DropItem` extends `XFile`: `item.path`, `item.name`, `item.readAsBytes()`, + `item.openRead()`, … are available directly. +- Drop targets receive events for the **whole window**; an invisible + `DropTarget` keeps receiving them, so set `enable: false` when pushing a + page over it. Coordinate-based `DropTarget` bounds filtering is handled by + the widget. +- `readImage` never throws: unreadable files are logged with a stack trace + and skipped; `null` means "no image in this drop". + +### media_kit hot-restart guard (Linux, debug) + +Apps using media_kit crash on a hot restart (`R`) while any `Player` is +alive: mpv calls a wakeup callback that belonged to the discarded isolate. +fox_utils' Linux plugin clears those callbacks just before the engine +restarts, so hot restart works. Nothing to call; release builds compile it +out; it does nothing without media_kit. It reads media_kit's private +NativeReferenceHolder file (checked against media_kit 1.2.6): re-check after +media_kit upgrades. + +## Vendored engines + +The clipboard channels (`pasteboard` method channel) are **vendored from +MixinNetwork's `pasteboard` package (v0.5.0, Apache-2.0)** because upstream's +build applies the legacy Kotlin Gradle Plugin (KGP), which future Flutter +versions will reject. The vendored copy lives in `lib/vendor/` (Dart) and the +native platform folders, with these changes: + +- **Android**: the Kotlin plugin was ported to Java and merged into + `FoxUtilsPlugin` (one plugin class, no KGP, no Kotlin in the module). +- **Byte-faithful image writes**: upstream decodes and re-encodes images as + PNG (destroying GIF animation and metadata); ours writes the original bytes + with magic-byte MIME sniffing. +- **Merged FileProvider**: declared in the plugin manifest — zero per-project + setup (upstream requires a manual manifest entry per app). +- **Two upstream bugs fixed**: clipboard image reads ran synchronously on the + calling thread (now on the executor), and `writeFiles` dropped the first + clip item (now assembles correctly). +- **Linux/Windows**: C++ sources byte-identical, include paths and CMake + targets renamed to the `fox_utils` package name. + +To diff against upstream: `git diff` the folders against +`packages/pasteboard/` at tag `v0.5.0` of +`https://github.com/MixinNetwork/flutter-plugins`. If upstream ever +modernizes their build, un-vendoring is a swap behind the same `ClipboardX` +API. + +The drag & drop channels (`desktop_drop` method channel) are **vendored from +MixinNetwork's `desktop_drop` package (v0.7.1, Apache-2.0)** for the same +reason: its `android/` folder applies KGP. Only the Dart sources and the +Linux/Windows native folders are vendored — the Android/Kotlin, macOS and +web bindings are omitted (Android DnD is out of scope, and there is no macOS +target in the app). Linux/Windows native sources are byte-identical to +upstream except the include path rename (`include/fox_utils/`). Both engines +are registered from one native entry per platform: +`fox_utils_plugin_register_with_registrar` (Linux) / +`FoxUtilsPluginRegisterWithRegistrar` (Windows) — the plugin class in +`pubspec.yaml` is no longer `PasteboardPlugin`. To diff against upstream, use +tag `desktop_drop-v0.7.1` of the same monorepo; un-vendoring is a swap +behind the same `DesktopDropX` API. + +## License + +MIT for this package's own code; the vendored engines are Apache-2.0 +© MixinNetwork (see `lib/vendor/` and the native platform folders). diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 84b5479..e5ab6ed 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(${PLUGIN_NAME} SHARED "fox_utils_plugin.cc" "pasteboard_plugin.cc" "desktop_drop_plugin.cc" + "media_kit_restart_guard.cc" ) apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES @@ -19,6 +20,7 @@ target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${PLUGIN_NAME} PRIVATE ${CMAKE_DL_LIBS}) # List of absolute paths to libraries that should be bundled with the plugin set(fox_utils_bundled_libraries diff --git a/linux/fox_utils_plugin.cc b/linux/fox_utils_plugin.cc index bad689d..0e3d719 100644 --- a/linux/fox_utils_plugin.cc +++ b/linux/fox_utils_plugin.cc @@ -1,8 +1,10 @@ #include "include/fox_utils/fox_utils_plugin.h" #include "include/fox_utils/pasteboard_plugin.h" #include "include/fox_utils/desktop_drop_plugin.h" +#include "include/fox_utils/media_kit_restart_guard.h" void fox_utils_plugin_register_with_registrar(FlPluginRegistrar* registrar) { pasteboard_plugin_register_with_registrar(registrar); desktop_drop_plugin_register_with_registrar(registrar); + media_kit_restart_guard_register_with_registrar(registrar); } \ No newline at end of file diff --git a/linux/include/fox_utils/media_kit_restart_guard.h b/linux/include/fox_utils/media_kit_restart_guard.h new file mode 100644 index 0000000..8abd381 --- /dev/null +++ b/linux/include/fox_utils/media_kit_restart_guard.h @@ -0,0 +1,15 @@ +// Debug builds only: keeps a Flutter hot restart from crashing apps that use +// media_kit on Linux. Hot restart frees the old isolate's NativeCallables +// while mpv handles stay alive; media_kit then "quit"s the leftovers and mpv +// calls a dead wakeup callback (SIGSEGV on mpv's "core" thread). On the +// engine's on-pre-engine-restart signal this clears every live handle's +// wakeup callback (found through media_kit's NativeReferenceHolder file). +// Does nothing without media_kit/libmpv, and nothing in release builds. +#ifndef FOX_UTILS_MEDIA_KIT_RESTART_GUARD_H_ +#define FOX_UTILS_MEDIA_KIT_RESTART_GUARD_H_ + +#include + +void media_kit_restart_guard_register_with_registrar(FlPluginRegistrar* registrar); + +#endif diff --git a/linux/media_kit_restart_guard.cc b/linux/media_kit_restart_guard.cc new file mode 100644 index 0000000..e7b76a7 --- /dev/null +++ b/linux/media_kit_restart_guard.cc @@ -0,0 +1,71 @@ +#include "include/fox_utils/media_kit_restart_guard.h" + +#ifdef NDEBUG + +void media_kit_restart_guard_register_with_registrar(FlPluginRegistrar* registrar) {} + +#else + +#include +#include +#include + +namespace { + +// Mirrors media_kit's NativeReferenceHolder (debug only): a calloc'd buffer +// of kReferenceBufferSize intptr slots holding live mpv_handle addresses, +// its address written in decimal to /. +constexpr int kReferenceBufferSize = 512; +constexpr char kFilePrefix[] = "com.alexmercerind.media_kit.NativeReferenceHolder."; + +using SetWakeupCallback = void (*)(void* ctx, void (*cb)(void*), void* d); + +SetWakeupCallback find_set_wakeup_callback() { + static const char* const kNames[] = {"libmpv.so.2", "libmpv.so", "libmpv.so.1"}; + for (const char* name : kNames) { + void* lib = dlopen(name, RTLD_NOW | RTLD_NOLOAD); + if (lib == nullptr) continue; + auto fn = reinterpret_cast( + dlsym(lib, "mpv_set_wakeup_callback")); + dlclose(lib); // Drops only the reference NOLOAD added. + if (fn != nullptr) return fn; + } + return nullptr; +} + +// Runs on the engine's UI thread before the old isolate shuts down, so the +// buffer is not being written concurrently. Clearing the callback takes the +// same lock mpv calls it under: once this returns, mpv never calls it again. +// The handles stay in the buffer: media_kit's startup in the new isolate +// still sends them "quit". +void on_pre_engine_restart(FlEngine* engine, gpointer user_data) { + g_autofree gchar* name = g_strdup_printf("%s%d", kFilePrefix, getpid()); + g_autofree gchar* path = g_build_filename(g_get_tmp_dir(), name, nullptr); + g_autofree gchar* contents = nullptr; + if (!g_file_get_contents(path, &contents, nullptr, nullptr)) return; + const guint64 address = g_ascii_strtoull(contents, nullptr, 10); + if (address == 0) return; + const SetWakeupCallback set_wakeup = find_set_wakeup_callback(); + if (set_wakeup == nullptr) return; + const auto* slots = reinterpret_cast(address); + int cleared = 0; + for (int i = 0; i < kReferenceBufferSize; i++) { + if (slots[i] == 0) continue; + set_wakeup(reinterpret_cast(slots[i]), nullptr, nullptr); + cleared++; + } + if (cleared > 0) { + g_message("hot restart: unhooked %d mpv wakeup callback(s)", cleared); + } +} + +} // namespace + +void media_kit_restart_guard_register_with_registrar(FlPluginRegistrar* registrar) { + FlView* view = fl_plugin_registrar_get_view(registrar); + if (view == nullptr) return; // headless: no hot restart to guard + g_signal_connect(fl_view_get_engine(view), "on-pre-engine-restart", + G_CALLBACK(on_pre_engine_restart), nullptr); +} + +#endif diff --git a/pubspec.yaml b/pubspec.yaml index ebc1774..bdc3689 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fox_utils description: A collection of reusable Flutter widgets and extensions. -version: 0.1.7 +version: 0.1.8 environment: sdk: ^3.9.2