Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
1 change: 1 addition & 0 deletions example/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
2 changes: 1 addition & 1 deletion example/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
1 change: 1 addition & 0 deletions example/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
17 changes: 12 additions & 5 deletions linux/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
73 changes: 2 additions & 71 deletions linux/pro_video_editor_plugin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
#include <sys/utsname.h>

#include <cstring>
#include <memory>
#include <iostream>

#include "pro_video_editor_plugin_private.h"
#include "src/video_metadata.h"
Expand Down Expand Up @@ -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<flutter::EncodableMap>(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<flutter::EncodableMap>(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<flutter::MethodResultFunctions<flutter::EncodableValue>>(
// 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<flutter::MethodResultFunctions<flutter::EncodableValue>>(
[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());
}
Expand Down
93 changes: 57 additions & 36 deletions linux/src/thumbnail_generator.cc
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
#include "thumbnail_generator.h"

#include <flutter/standard_method_codec.h>
#include <flutter_linux/flutter_linux.h>

#include <fstream>
#include <string>
#include <vector>
#include <sstream>
#include <iostream>
#include <chrono>
#include <filesystem>
#include <future>
#include <cstdlib>
#include <iomanip>
#include <cmath>

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);
Expand All @@ -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<uint8_t>& 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<const char*>(bytes.data()), bytes.size());
out.write(reinterpret_cast<const char*>(data), size);
return true;
}

void HandleGenerateThumbnails(
const flutter::EncodableMap& args,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> 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<std::vector<uint8_t>>(&args.at(flutter::EncodableValue("videoBytes")));
const auto* timestampsList = std::get_if<flutter::EncodableList>(&args.at(flutter::EncodableValue("timestamps")));
const auto* formatStr = std::get_if<std::string>(&args.at(flutter::EncodableValue("thumbnailFormat")));
const auto* extensionStr = std::get_if<std::string>(&args.at(flutter::EncodableValue("extension")));
const auto* width = std::get_if<double>(&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<double>(fl_value_get_int(widthVal));
}
int roundedWidth = static_cast<int>(std::round(widthDouble));

int roundedWidth = static_cast<int>(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<std::future<void>> futures;
std::vector<flutter::EncodableValue> thumbnails(timestampsList->size());
std::vector<std::vector<uint8_t>> thumbnails(count);

int index = 0;
for (const auto& tsValue : *timestampsList) {
if (!std::holds_alternative<int>(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<int64_t>(std::get<int>(tsValue));
double tsSec = tsMs / 1000.0;
size_t currentIndex = i;
double tsSec = static_cast<double>(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
Expand All @@ -100,8 +109,9 @@ void HandleGenerateThumbnails(

if (retCode == 0 && fs::exists(tempImagePath)) {
std::ifstream in(tempImagePath, std::ios::binary);
std::vector<uint8_t> imageBytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
thumbnails[currentIndex] = flutter::EncodableValue(imageBytes);
thumbnails[currentIndex] = std::vector<uint8_t>(
(std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
std::remove(tempImagePath.c_str());
}
}));
Expand All @@ -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
14 changes: 5 additions & 9 deletions linux/src/thumbnail_generator.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
// src/video_metadata.h
// src/thumbnail_generator.h
#pragma once

#include <flutter/standard_method_codec.h>
#include <flutter/method_result_functions.h>
#include <flutter_linux/flutter_linux.h>

namespace pro_video_editor {

void HandleGenerateThumbnails(
const flutter::EncodableMap& args,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

} // namespace pro_video_editor

FlMethodResponse* HandleGenerateThumbnails(FlValue* args);

} // namespace pro_video_editor
Loading
Loading