diff --git a/CHANGELOG.md b/CHANGELOG.md index 332d4cd6..303e9db6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.16.3 +- **FIX**(linux): Fix Linux build by replacing the unavailable C++ Flutter embedder API (`flutter::EncodableValue`) with the GLib-based `FlValue` API. Update CMakeLists to use GStreamer (`gstreamer-1.0`, `gstreamer-pbutils-1.0`) instead of the unused libavformat/libavcodec/libavutil pkg-config targets. Fix source file extensions (`.cc` instead of `.cpp`). + ## 1.16.2 - **FIX**(android, iOS, macOS): Eliminate audible clicks/gaps at custom audio loop boundaries and clip transitions. Each custom audio track is now pre-rendered to a single gap-less PCM WAV file before being inserted into the composition, avoiding AAC encoder frame realignment (Android Media3) and codec priming/padding artifacts (AVFoundation `insertTimeRange` per loop iteration on iOS/macOS). Volume control remains in the mixer layer for post-hoc adjustments without re-decoding. diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 4938690b..559b4ef0 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 768b999d..aba9da97 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -26,7 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ProImageEditorPlugin.register(with: registry.registrar(forPlugin: "ProImageEditorPlugin")) ProVideoEditorPlugin.register(with: registry.registrar(forPlugin: "ProVideoEditorPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 98161824..361febbd 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 7fb4ab0c..5d37f41d 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -14,8 +14,8 @@ set(PLUGIN_NAME "pro_video_editor_plugin") # Any new source files that you add to the plugin should be added here. list(APPEND PLUGIN_SOURCES "pro_video_editor_plugin.cc" - "src/video_metadata.cpp" - "src/thumbnail_generator.cpp" + "src/video_metadata.cc" + "src/thumbnail_generator.cc" ) # Define the plugin library target. Its name must not be changed (see comment @@ -40,11 +40,16 @@ target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) # dependencies here. target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GST REQUIRED IMPORTED_TARGET gstreamer-1.0) +pkg_check_modules(GST_PBUTILS REQUIRED IMPORTED_TARGET gstreamer-pbutils-1.0) + target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) -target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::AVFORMAT) -target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::AVCODEC) -target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::AVUTIL) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GST) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GST_PBUTILS) # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an @@ -89,6 +94,8 @@ apply_standard_settings(${TEST_RUNNER}) target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(${TEST_RUNNER} PRIVATE flutter) target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK) +target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GST) +target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GST_PBUTILS) target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) # Enable automatic test discovery. diff --git a/linux/pro_video_editor_plugin.cc b/linux/pro_video_editor_plugin.cc index 912b9b34..f263f0f5 100644 --- a/linux/pro_video_editor_plugin.cc +++ b/linux/pro_video_editor_plugin.cc @@ -5,8 +5,6 @@ #include #include -#include -#include #include "pro_video_editor_plugin_private.h" #include "src/video_metadata.h" @@ -51,86 +49,19 @@ FlMethodResponse* get_platform_version() { return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } -// Utility to convert FlValue* to EncodableValue -flutter::EncodableValue ConvertFlValueToEncodable(FlValue* value); - -// Utility to convert EncodableValue to FlValue* -FlValue* ConvertEncodableToFlValue(const flutter::EncodableValue& value); - static void pro_video_editor_plugin_handle_method_call( ProVideoEditorPlugin* self, FlMethodCall* method_call) { g_autoptr(FlMethodResponse) response = nullptr; const gchar* method = fl_method_call_get_name(method_call); - FlValue* args = fl_method_call_get_args(method_call); - flutter::EncodableValue encodable_args = ConvertFlValueToEncodable(args); - - if (!std::holds_alternative(encodable_args)) { - response = FL_METHOD_RESPONSE(fl_method_error_response_new( - "InvalidArgument", "Expected a map", nullptr)); - fl_method_call_respond(method_call, response, nullptr); - return; - } - - auto args_map = std::get(encodable_args); if (strcmp(method, "getPlatformVersion") == 0) { response = get_platform_version(); - } else if (strcmp(method, "getMetadata") == 0) { - pro_video_editor::HandleGetMetadata( - args_map, - std::make_unique>( - // onSuccess - [method_call](const flutter::EncodableValue* result) { - FlValue* fl_result = ConvertEncodableToFlValue(*result); - g_autoptr(FlMethodResponse) response = - FL_METHOD_RESPONSE(fl_method_success_response_new(fl_result)); - fl_method_call_respond(method_call, response, nullptr); - }, - // onError - [method_call](const std::string& code, - const std::string& message, - const flutter::EncodableValue* details) { - g_autoptr(FlMethodResponse) response = - FL_METHOD_RESPONSE(fl_method_error_response_new(code.c_str(), message.c_str(), nullptr)); - fl_method_call_respond(method_call, response, nullptr); - }, - // onNotImplemented - [method_call]() { - g_autoptr(FlMethodResponse) response = - FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); - fl_method_call_respond(method_call, response, nullptr); - })); - - return; // Don't respond here — async will handle it - + response = pro_video_editor::HandleGetMetadata(args); } else if (strcmp(method, "createVideoThumbnails") == 0) { - pro_video_editor::HandleGenerateThumbnails( - args_map, - std::make_unique>( - [method_call](const flutter::EncodableValue* result) { - FlValue* fl_result = ConvertEncodableToFlValue(*result); - g_autoptr(FlMethodResponse) response = - FL_METHOD_RESPONSE(fl_method_success_response_new(fl_result)); - fl_method_call_respond(method_call, response, nullptr); - }, - [method_call](const std::string& code, - const std::string& message, - const flutter::EncodableValue* details) { - g_autoptr(FlMethodResponse) response = - FL_METHOD_RESPONSE(fl_method_error_response_new(code.c_str(), message.c_str(), nullptr)); - fl_method_call_respond(method_call, response, nullptr); - }, - [method_call]() { - g_autoptr(FlMethodResponse) response = - FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); - fl_method_call_respond(method_call, response, nullptr); - })); - - return; - + response = pro_video_editor::HandleGenerateThumbnails(args); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } diff --git a/linux/src/thumbnail_generator.cc b/linux/src/thumbnail_generator.cc index 94aac632..047a113f 100644 --- a/linux/src/thumbnail_generator.cc +++ b/linux/src/thumbnail_generator.cc @@ -1,22 +1,22 @@ #include "thumbnail_generator.h" -#include +#include #include #include #include #include -#include #include #include #include #include #include +#include namespace fs = std::filesystem; namespace pro_video_editor { -std::string GenerateTempFilename(const std::string& prefix, const std::string& extension) { +static std::string GenerateTempFilename(const std::string& prefix, const std::string& extension) { std::stringstream filename; auto now = std::chrono::system_clock::now(); auto time = std::chrono::system_clock::to_time_t(now); @@ -31,63 +31,72 @@ std::string GenerateTempFilename(const std::string& prefix, const std::string& e return filename.str(); } -bool WriteBytesToFile(const std::string& path, const std::vector& bytes) { +static bool WriteBytesToFile(const std::string& path, const uint8_t* data, size_t size) { std::ofstream out(path, std::ios::binary); if (!out.is_open()) return false; - out.write(reinterpret_cast(bytes.data()), bytes.size()); + out.write(reinterpret_cast(data), size); return true; } -void HandleGenerateThumbnails( - const flutter::EncodableMap& args, - std::unique_ptr> result) { +FlMethodResponse* HandleGenerateThumbnails(FlValue* args) { + FlValue* videoBytesVal = fl_value_lookup_string(args, "videoBytes"); + FlValue* timestampsVal = fl_value_lookup_string(args, "timestamps"); + FlValue* formatVal = fl_value_lookup_string(args, "thumbnailFormat"); + FlValue* extensionVal = fl_value_lookup_string(args, "extension"); + FlValue* widthVal = fl_value_lookup_string(args, "imageWidth"); + + if (!videoBytesVal || fl_value_get_type(videoBytesVal) != FL_VALUE_TYPE_UINT8_LIST || + !timestampsVal || fl_value_get_type(timestampsVal) != FL_VALUE_TYPE_LIST || + !formatVal || fl_value_get_type(formatVal) != FL_VALUE_TYPE_STRING || + !extensionVal || fl_value_get_type(extensionVal) != FL_VALUE_TYPE_STRING || + !widthVal) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "InvalidArgument", "Missing required parameters", nullptr)); + } - const auto* videoBytes = std::get_if>(&args.at(flutter::EncodableValue("videoBytes"))); - const auto* timestampsList = std::get_if(&args.at(flutter::EncodableValue("timestamps"))); - const auto* formatStr = std::get_if(&args.at(flutter::EncodableValue("thumbnailFormat"))); - const auto* extensionStr = std::get_if(&args.at(flutter::EncodableValue("extension"))); - const auto* width = std::get_if(&args.at(flutter::EncodableValue("imageWidth"))); + const uint8_t* videoData = fl_value_get_uint8_list(videoBytesVal); + size_t videoDataSize = fl_value_get_length(videoBytesVal); - if (!videoBytes || !timestampsList || !formatStr || !extensionStr || !width) { - result->Error("InvalidArgument", "Missing required parameters"); - return; + double widthDouble = 0.0; + if (fl_value_get_type(widthVal) == FL_VALUE_TYPE_FLOAT) { + widthDouble = fl_value_get_float(widthVal); + } else if (fl_value_get_type(widthVal) == FL_VALUE_TYPE_INT) { + widthDouble = static_cast(fl_value_get_int(widthVal)); } + int roundedWidth = static_cast(std::round(widthDouble)); - int roundedWidth = static_cast(std::round(*width)); - std::string videoExt = *extensionStr; + std::string videoExt = fl_value_get_string(extensionVal); if (videoExt.empty() || videoExt[0] != '.') videoExt = "." + videoExt; - std::string imageExt = *formatStr; + std::string imageExt = fl_value_get_string(formatVal); if (imageExt.empty() || imageExt[0] != '.') imageExt = "." + imageExt; std::string tempVideoPath = GenerateTempFilename("video_temp", videoExt); - if (!WriteBytesToFile(tempVideoPath, *videoBytes)) { - result->Error("FileError", "Failed to write temp video file"); - return; + if (!WriteBytesToFile(tempVideoPath, videoData, videoDataSize)) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "FileError", "Failed to write temp video file", nullptr)); } - // Assume `ffmpeg` is in system PATH on Linux std::string ffmpegPath = "ffmpeg"; - + size_t count = fl_value_get_length(timestampsVal); std::vector> futures; - std::vector thumbnails(timestampsList->size()); + std::vector> thumbnails(count); - int index = 0; - for (const auto& tsValue : *timestampsList) { - if (!std::holds_alternative(tsValue)) { - ++index; + for (size_t i = 0; i < count; i++) { + FlValue* tsVal = fl_value_get_list_value(timestampsVal, i); + if (!tsVal || fl_value_get_type(tsVal) != FL_VALUE_TYPE_INT) { continue; } - int currentIndex = index++; - int64_t tsMs = static_cast(std::get(tsValue)); - double tsSec = tsMs / 1000.0; + size_t currentIndex = i; + double tsSec = static_cast(fl_value_get_int(tsVal)) / 1000.0; futures.push_back(std::async(std::launch::async, [=, &thumbnails]() { std::ostringstream timestampStream; timestampStream << std::fixed << std::setprecision(3) << tsSec; - std::string tempImagePath = GenerateTempFilename("thumb_" + std::to_string(currentIndex), imageExt); + std::string tempImagePath = GenerateTempFilename( + "thumb_" + std::to_string(currentIndex), imageExt); std::ostringstream cmd; cmd << ffmpegPath @@ -100,8 +109,9 @@ void HandleGenerateThumbnails( if (retCode == 0 && fs::exists(tempImagePath)) { std::ifstream in(tempImagePath, std::ios::binary); - std::vector imageBytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); - thumbnails[currentIndex] = flutter::EncodableValue(imageBytes); + thumbnails[currentIndex] = std::vector( + (std::istreambuf_iterator(in)), + std::istreambuf_iterator()); std::remove(tempImagePath.c_str()); } })); @@ -112,7 +122,18 @@ void HandleGenerateThumbnails( } std::remove(tempVideoPath.c_str()); - result->Success(thumbnails); + + g_autoptr(FlValue) result_list = fl_value_new_list(); + for (size_t i = 0; i < count; i++) { + if (!thumbnails[i].empty()) { + fl_value_append_take(result_list, + fl_value_new_uint8_list(thumbnails[i].data(), thumbnails[i].size())); + } else { + fl_value_append_take(result_list, fl_value_new_null()); + } + } + + return FL_METHOD_RESPONSE(fl_method_success_response_new(result_list)); } } // namespace pro_video_editor diff --git a/linux/src/thumbnail_generator.h b/linux/src/thumbnail_generator.h index a0a09a74..c8b01549 100644 --- a/linux/src/thumbnail_generator.h +++ b/linux/src/thumbnail_generator.h @@ -1,14 +1,10 @@ -// src/video_metadata.h +// src/thumbnail_generator.h #pragma once -#include -#include +#include namespace pro_video_editor { - void HandleGenerateThumbnails( - const flutter::EncodableMap& args, - std::unique_ptr> result); - - } // namespace pro_video_editor - \ No newline at end of file + FlMethodResponse* HandleGenerateThumbnails(FlValue* args); + +} // namespace pro_video_editor \ No newline at end of file diff --git a/linux/src/video_metadata.cc b/linux/src/video_metadata.cc index 163f9b7c..9ea13f75 100644 --- a/linux/src/video_metadata.cc +++ b/linux/src/video_metadata.cc @@ -1,57 +1,44 @@ #include "video_metadata.h" -#include +#include #include #include -#include #include #include -#include #include -#include -#include #include namespace pro_video_editor { -void HandleGetMetadata( - const flutter::EncodableMap& args, - std::unique_ptr> result) { - - auto itPath = args.find(flutter::EncodableValue("inputPath")); - if (itPath == args.end()) { - result->Error("InvalidArgument", "Missing inputPath"); - return; - } - const auto* pathStr = std::get_if(&itPath->second); - if (!pathStr) { - result->Error("InvalidArgument", "Invalid inputPath format"); - return; +FlMethodResponse* HandleGetMetadata(FlValue* args) { + FlValue* inputPathValue = fl_value_lookup_string(args, "inputPath"); + if (!inputPathValue || fl_value_get_type(inputPathValue) != FL_VALUE_TYPE_STRING) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "InvalidArgument", "Missing or invalid inputPath", nullptr)); } - std::string inputPath = *pathStr; + std::string inputPath = fl_value_get_string(inputPathValue); struct stat file_stat; int64_t fileSize = 0; std::string dateStr; if (stat(inputPath.c_str(), &file_stat) == 0) { fileSize = file_stat.st_size; - char buffer[64]; std::tm* tm = std::localtime(&file_stat.st_ctime); std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm); dateStr = buffer; } else { - result->Error("FileError", "Failed to stat file"); - return; + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "FileError", "Failed to stat file", nullptr)); } gst_init(nullptr, nullptr); GstDiscoverer* discoverer = gst_discoverer_new(5 * GST_SECOND, nullptr); if (!discoverer) { - result->Error("GStreamerError", "Failed to create discoverer"); - return; + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "GStreamerError", "Failed to create discoverer", nullptr)); } std::string uri = "file://" + inputPath; @@ -59,12 +46,12 @@ void HandleGetMetadata( if (!info) { g_object_unref(discoverer); - result->Error("GStreamerError", "Failed to get metadata"); - return; + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "GStreamerError", "Failed to get metadata", nullptr)); } - const GstDiscovererStreamInfo* streamInfo = gst_discoverer_info_get_stream_info(info); - const GstCaps* caps = gst_discoverer_stream_info_get_caps(streamInfo); + GstDiscovererStreamInfo* streamInfo = gst_discoverer_info_get_stream_info(info); + GstCaps* caps = gst_discoverer_stream_info_get_caps(streamInfo); int width = 0, height = 0, rotation = 0; double duration_ms = 0.0; @@ -89,25 +76,25 @@ void HandleGetMetadata( gst_tag_list_get_string(tags, GST_TAG_TITLE, &title); } - flutter::EncodableMap result_map; - result_map[flutter::EncodableValue("fileSize")] = flutter::EncodableValue(static_cast(fileSize)); - result_map[flutter::EncodableValue("duration")] = flutter::EncodableValue(duration_ms); - result_map[flutter::EncodableValue("width")] = flutter::EncodableValue(width); - result_map[flutter::EncodableValue("height")] = flutter::EncodableValue(height); - result_map[flutter::EncodableValue("rotation")] = flutter::EncodableValue(rotation); // Rotation not available via GStreamer tags directly - result_map[flutter::EncodableValue("bitrate")] = flutter::EncodableValue(bitrate); - result_map[flutter::EncodableValue("title")] = flutter::EncodableValue(title ? title : ""); - result_map[flutter::EncodableValue("artist")] = flutter::EncodableValue(""); - result_map[flutter::EncodableValue("author")] = flutter::EncodableValue(""); - result_map[flutter::EncodableValue("album")] = flutter::EncodableValue(""); - result_map[flutter::EncodableValue("albumArtist")] = flutter::EncodableValue(""); - result_map[flutter::EncodableValue("date")] = flutter::EncodableValue(dateStr); + g_autoptr(FlValue) result_map = fl_value_new_map(); + fl_value_set_string_take(result_map, "fileSize", fl_value_new_int(fileSize)); + fl_value_set_string_take(result_map, "duration", fl_value_new_float(duration_ms)); + fl_value_set_string_take(result_map, "width", fl_value_new_int(width)); + fl_value_set_string_take(result_map, "height", fl_value_new_int(height)); + fl_value_set_string_take(result_map, "rotation", fl_value_new_int(rotation)); + fl_value_set_string_take(result_map, "bitrate", fl_value_new_int(bitrate)); + fl_value_set_string_take(result_map, "title", fl_value_new_string(title ? title : "")); + fl_value_set_string_take(result_map, "artist", fl_value_new_string("")); + fl_value_set_string_take(result_map, "author", fl_value_new_string("")); + fl_value_set_string_take(result_map, "album", fl_value_new_string("")); + fl_value_set_string_take(result_map, "albumArtist", fl_value_new_string("")); + fl_value_set_string_take(result_map, "date", fl_value_new_string(dateStr.c_str())); if (title) g_free(title); gst_discoverer_info_unref(info); g_object_unref(discoverer); - result->Success(flutter::EncodableValue(result_map)); + return FL_METHOD_RESPONSE(fl_method_success_response_new(result_map)); } } // namespace pro_video_editor diff --git a/linux/src/video_metadata.h b/linux/src/video_metadata.h index 00f4933d..5b30d753 100644 --- a/linux/src/video_metadata.h +++ b/linux/src/video_metadata.h @@ -1,13 +1,10 @@ // src/video_metadata.h #pragma once -#include -#include +#include namespace pro_video_editor { - void HandleGetMetadata( - const flutter::EncodableMap& args, - std::unique_ptr> result); + FlMethodResponse* HandleGetMetadata(FlValue* args); } // namespace pro_video_editor diff --git a/pubspec.yaml b/pubspec.yaml index f0c06e35..ba5e9eca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pro_video_editor description: "A Flutter video editor: Seamlessly enhance your videos with user-friendly editing features." -version: 1.16.2 +version: 1.16.3 homepage: https://github.com/hm21/pro_video_editor/ repository: https://github.com/hm21/pro_video_editor/ documentation: https://github.com/hm21/pro_video_editor/