add plugin camera desktop
This commit is contained in:
52
plugins/camera_desktop/windows/CMakeLists.txt
Normal file
52
plugins/camera_desktop/windows/CMakeLists.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(camera_desktop LANGUAGES CXX)
|
||||
cmake_policy(VERSION 3.14...3.25)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PLUGIN_NAME "camera_desktop_plugin")
|
||||
|
||||
add_library(${PLUGIN_NAME} SHARED
|
||||
camera_desktop_plugin.cpp
|
||||
camera.cpp
|
||||
camera_texture.cpp
|
||||
device_enumerator.cpp
|
||||
image_stream_ffi.cpp
|
||||
photo_handler.cpp
|
||||
record_handler.cpp
|
||||
)
|
||||
|
||||
apply_standard_settings(${PLUGIN_NAME})
|
||||
|
||||
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
)
|
||||
|
||||
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
|
||||
|
||||
if(MSVC)
|
||||
# Force MSVC to read source (and emit narrow string literals) as UTF-8,
|
||||
# regardless of the host's ANSI code page. Without this, hosts with a
|
||||
# non-UTF-8 ACP (e.g. CP936 on Simplified Chinese Windows) fail with C4819
|
||||
# → C2220 on our Unicode comments. See chinese-pc-compat.md.
|
||||
target_compile_options(${PLUGIN_NAME} PRIVATE /utf-8)
|
||||
endif()
|
||||
|
||||
target_include_directories(${PLUGIN_NAME} INTERFACE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||
)
|
||||
|
||||
target_link_libraries(${PLUGIN_NAME} PRIVATE
|
||||
flutter
|
||||
flutter_wrapper_plugin
|
||||
mf
|
||||
mfplat
|
||||
mfreadwrite
|
||||
mfuuid
|
||||
ole32
|
||||
strmiids
|
||||
uuid
|
||||
windowscodecs
|
||||
d3d11
|
||||
dxgi
|
||||
)
|
||||
1532
plugins/camera_desktop/windows/camera.cpp
Normal file
1532
plugins/camera_desktop/windows/camera.cpp
Normal file
File diff suppressed because it is too large
Load Diff
220
plugins/camera_desktop/windows/camera.h
Normal file
220
plugins/camera_desktop/windows/camera.h
Normal file
@@ -0,0 +1,220 @@
|
||||
#pragma once
|
||||
|
||||
#include <flutter/encodable_value.h>
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/method_result.h>
|
||||
#include <flutter/texture_registrar.h>
|
||||
#include <d3d11.h>
|
||||
#include <mfapi.h>
|
||||
#include <mfcaptureengine.h>
|
||||
#include <mfidl.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "camera_texture.h"
|
||||
#include "record_handler.h"
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
enum class CameraState {
|
||||
kCreated,
|
||||
kInitializing,
|
||||
kRunning,
|
||||
kPaused,
|
||||
kDisposing,
|
||||
kDisposed,
|
||||
};
|
||||
|
||||
struct CameraConfig {
|
||||
std::wstring symbolic_link;
|
||||
int resolution_preset = 4; // 0=low … 4=max
|
||||
bool enable_audio = false;
|
||||
int target_fps = 30;
|
||||
int target_bitrate = 0; // <=0 means use dynamic default ladder.
|
||||
int audio_bitrate = 0;
|
||||
};
|
||||
|
||||
class Camera : public std::enable_shared_from_this<Camera> {
|
||||
public:
|
||||
using PlatformTaskPoster =
|
||||
std::function<void(std::function<void()>, const char* tag)>;
|
||||
|
||||
Camera(int camera_id, flutter::TextureRegistrar* texture_registrar,
|
||||
flutter::MethodChannel<flutter::EncodableValue>* channel,
|
||||
CameraConfig config,
|
||||
PlatformTaskPoster platform_task_poster);
|
||||
~Camera();
|
||||
|
||||
int64_t RegisterTexture();
|
||||
|
||||
void Initialize(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
|
||||
void TakePicture(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
|
||||
void StartVideoRecording(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void StopVideoRecording(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
|
||||
void StartImageStream();
|
||||
void StopImageStream();
|
||||
|
||||
// FFI image stream access.
|
||||
void* GetImageStreamBuffer();
|
||||
void RegisterImageStreamCallback(void (*callback)(int32_t));
|
||||
void UnregisterImageStreamCallback();
|
||||
|
||||
void PausePreview();
|
||||
void ResumePreview();
|
||||
void DisposeAsync(std::function<void()> on_done);
|
||||
void Dispose();
|
||||
bool IsDisposedOrDisposing() const;
|
||||
|
||||
// Called from COM callbacks, must be public.
|
||||
void OnEngineEvent(IMFMediaEvent* event);
|
||||
void OnPreviewSample(IMFSample* sample);
|
||||
|
||||
private:
|
||||
uint32_t MaxPreviewHeightForPreset() const;
|
||||
uint32_t MaxRecordHeightForPreset() const;
|
||||
int ComputeDefaultBitrate(int width, int height, int fps) const;
|
||||
|
||||
HRESULT CreateCaptureEngine();
|
||||
HRESULT FindBaseMediaTypes();
|
||||
HRESULT StartPreviewInternal();
|
||||
|
||||
void CompleteInit(bool success, const std::string& error,
|
||||
int width = 0, int height = 0);
|
||||
void FailAllPendingResults(const std::string& error);
|
||||
|
||||
void DisposeInternal();
|
||||
void SendError(const std::string& description);
|
||||
int InitElapsedMs() const;
|
||||
|
||||
static void FlipHorizontal(uint8_t* data, int width, int height);
|
||||
static void SwapRBChannels(uint8_t* data, int width, int height);
|
||||
|
||||
void PostImageStreamFrame(const uint8_t* data, int width, int height);
|
||||
void ImageStreamLoop();
|
||||
|
||||
// ── Identity ────────────────────────────────────────────────────────────
|
||||
int camera_id_;
|
||||
int64_t texture_id_ = -1;
|
||||
CameraConfig config_;
|
||||
|
||||
flutter::TextureRegistrar* texture_registrar_;
|
||||
flutter::MethodChannel<flutter::EncodableValue>* channel_;
|
||||
PlatformTaskPoster platform_task_poster_;
|
||||
std::shared_ptr<CameraTexture> texture_;
|
||||
// Guards texture_ against concurrent teardown: preview samples arrive on an
|
||||
// MF callback thread and touch texture_, while DisposeInternal frees it on
|
||||
// the dispose thread. See CRASH.md.
|
||||
std::mutex texture_mutex_;
|
||||
|
||||
// ── Capture engine + D3D11 ─────────────────────────────────────────────
|
||||
ComPtr<IMFCaptureEngine> capture_engine_;
|
||||
ComPtr<IMFCapturePreviewSink> preview_sink_;
|
||||
ComPtr<ID3D11Device> dx11_device_;
|
||||
ComPtr<IMFDXGIDeviceManager> dxgi_device_manager_;
|
||||
UINT dx_device_reset_token_ = 0;
|
||||
|
||||
// Negotiated media types (set in FindBaseMediaTypes before preview starts).
|
||||
ComPtr<IMFMediaType> base_preview_media_type_;
|
||||
ComPtr<IMFMediaType> base_capture_media_type_;
|
||||
int preview_width_ = 0;
|
||||
int preview_height_ = 0;
|
||||
int record_width_ = 0;
|
||||
int record_height_ = 0;
|
||||
int record_fps_ = 0;
|
||||
|
||||
// ── Recording ──────────────────────────────────────────────────────────
|
||||
std::unique_ptr<RecordHandler> record_handler_;
|
||||
std::wstring current_record_path_;
|
||||
std::atomic<bool> is_recording_{false};
|
||||
int active_record_bitrate_ = 0;
|
||||
|
||||
// ── Preview / frame state ───────────────────────────────────────────────
|
||||
std::atomic<bool> first_frame_received_{false};
|
||||
std::atomic<bool> preview_paused_{false};
|
||||
std::atomic<bool> image_streaming_{false};
|
||||
|
||||
// ── Latest frame for photo capture (natural BGRA) ─────────────────────
|
||||
std::vector<uint8_t> latest_frame_;
|
||||
std::mutex latest_frame_mutex_;
|
||||
|
||||
// ── Per-frame working buffer ────────────────────────────────────────────
|
||||
std::vector<uint8_t> packed_frame_;
|
||||
|
||||
// ── Camera state ────────────────────────────────────────────────────────
|
||||
CameraState state_ = CameraState::kCreated;
|
||||
mutable std::mutex state_mutex_;
|
||||
|
||||
// ── Pending async MethodResults ─────────────────────────────────────────
|
||||
mutable std::mutex pending_mutex_;
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>
|
||||
pending_init_;
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>
|
||||
pending_start_record_;
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>
|
||||
pending_stop_record_;
|
||||
|
||||
// ── Init timing (diagnostics) ───────────────────────────────────────────
|
||||
std::chrono::steady_clock::time_point create_start_{};
|
||||
|
||||
// ── Initialisation timeout ──────────────────────────────────────────────
|
||||
std::thread init_timeout_thread_;
|
||||
std::mutex init_timeout_cancel_mutex_;
|
||||
std::condition_variable init_timeout_cancel_cv_;
|
||||
bool init_timeout_cancelled_ = false;
|
||||
|
||||
// ── Image stream delivery ───────────────────────────────────────────────
|
||||
struct ImageStreamBuffer {
|
||||
int64_t sequence;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
int32_t bytes_per_row;
|
||||
int32_t format; // 0=BGRA, 1=RGBA
|
||||
int32_t ready; // 1=Dart may read, 0=native writing
|
||||
int32_t _pad;
|
||||
uint8_t pixels[1];
|
||||
};
|
||||
|
||||
ImageStreamBuffer* image_stream_buffer_ = nullptr;
|
||||
size_t image_stream_buffer_size_ = 0;
|
||||
void (*image_stream_callback_)(int32_t) = nullptr;
|
||||
int64_t image_stream_sequence_ = 0;
|
||||
std::mutex image_stream_ffi_mutex_;
|
||||
|
||||
struct ImageStreamSlot {
|
||||
std::vector<uint8_t> data;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool dirty = false;
|
||||
};
|
||||
std::mutex image_stream_mutex_;
|
||||
std::condition_variable image_stream_cv_;
|
||||
ImageStreamSlot image_stream_slot_;
|
||||
std::thread image_stream_thread_;
|
||||
std::atomic<bool> image_stream_running_{false};
|
||||
std::thread image_stream_join_thread_;
|
||||
std::mutex image_stream_thread_mutex_;
|
||||
|
||||
// ── Async dispose ───────────────────────────────────────────────────────
|
||||
std::thread dispose_thread_;
|
||||
std::mutex dispose_mutex_;
|
||||
std::vector<std::function<void()>> dispose_callbacks_;
|
||||
};
|
||||
|
||||
|
||||
|
||||
549
plugins/camera_desktop/windows/camera_desktop_plugin.cpp
Normal file
549
plugins/camera_desktop/windows/camera_desktop_plugin.cpp
Normal file
@@ -0,0 +1,549 @@
|
||||
#include "camera_desktop_plugin.h"
|
||||
|
||||
#include "include/camera_desktop/camera_desktop_plugin.h"
|
||||
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/plugin_registrar_windows.h>
|
||||
#include <flutter/standard_method_codec.h>
|
||||
#include <mfapi.h>
|
||||
#include <objbase.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <cstdint>
|
||||
|
||||
#include "device_enumerator.h"
|
||||
#include "logging.h"
|
||||
|
||||
CameraDesktopPlugin* CameraDesktopPlugin::instance_ = nullptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TaskDispatcher
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const UINT kWmTask = WM_APP + 100;
|
||||
static const wchar_t kTaskWndClass[] = L"CameraDesktopTaskDispatcher";
|
||||
|
||||
TaskDispatcher::TaskDispatcher() {
|
||||
WNDCLASSEX wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = &TaskDispatcher::WndProc;
|
||||
wc.hInstance = GetModuleHandle(nullptr);
|
||||
wc.lpszClassName = kTaskWndClass;
|
||||
RegisterClassEx(&wc); // Ignore failure; already-registered is fine.
|
||||
|
||||
hwnd_ = CreateWindowEx(0, kTaskWndClass, nullptr, 0,
|
||||
0, 0, 0, 0, HWND_MESSAGE, nullptr,
|
||||
GetModuleHandle(nullptr), nullptr);
|
||||
platform_thread_id_ = GetCurrentThreadId();
|
||||
DebugLog("TaskDispatcher: created on platform thread=" +
|
||||
std::to_string(platform_thread_id_) +
|
||||
" hwnd=" + (hwnd_ ? "ok" : "FAILED"));
|
||||
}
|
||||
|
||||
TaskDispatcher::~TaskDispatcher() {
|
||||
if (hwnd_) {
|
||||
DestroyWindow(hwnd_);
|
||||
hwnd_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
struct TaskDispatcherItem {
|
||||
std::function<void()> task;
|
||||
std::string tag;
|
||||
DWORD caller_thread_id;
|
||||
};
|
||||
|
||||
void TaskDispatcher::Post(std::function<void()> task, const char* tag) {
|
||||
if (!hwnd_) {
|
||||
DebugLog("TaskDispatcher::Post DROPPED (no hwnd) tag=" +
|
||||
std::string(tag ? tag : "?") +
|
||||
" thread=" + std::to_string(GetCurrentThreadId()));
|
||||
return;
|
||||
}
|
||||
auto* item = new TaskDispatcherItem{
|
||||
std::move(task),
|
||||
tag ? tag : "?",
|
||||
GetCurrentThreadId()};
|
||||
if (!PostMessage(hwnd_, kWmTask, 0, reinterpret_cast<LPARAM>(item))) {
|
||||
DebugLog("TaskDispatcher::Post PostMessage FAILED tag=" + item->tag +
|
||||
" thread=" + std::to_string(item->caller_thread_id));
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK TaskDispatcher::WndProc(HWND hwnd, UINT msg,
|
||||
WPARAM wparam, LPARAM lparam) {
|
||||
if (msg == kWmTask) {
|
||||
auto* item = reinterpret_cast<TaskDispatcherItem*>(lparam);
|
||||
DWORD exec_thread = GetCurrentThreadId();
|
||||
// Only log non-imageStreamFrame to avoid 30fps spam.
|
||||
if (item->tag != std::string("imageStreamFrame")) {
|
||||
DebugLog("TaskDispatcher dispatch tag=" + item->tag +
|
||||
" posted-from-thread=" + std::to_string(item->caller_thread_id) +
|
||||
" executing-on-thread=" + std::to_string(exec_thread));
|
||||
}
|
||||
item->task();
|
||||
delete item;
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProc(hwnd, msg, wparam, lparam);
|
||||
}
|
||||
|
||||
int64_t camera_desktop_ffi_register_stream_handle(Camera* camera);
|
||||
void camera_desktop_ffi_release_stream_handle(int64_t stream_handle);
|
||||
void camera_desktop_ffi_release_handles_for_camera(Camera* camera);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C export, called by generated_plugin_registrant.cc
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CameraDesktopPluginRegisterWithRegistrar(
|
||||
FlutterDesktopPluginRegistrarRef registrar) {
|
||||
CameraDesktopPlugin::RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarManager::GetInstance()
|
||||
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// static
|
||||
void CameraDesktopPlugin::RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarWindows* registrar) {
|
||||
// One-time Media Foundation startup (reference-counted internally).
|
||||
MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET);
|
||||
const HRESULT co_hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
auto channel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
||||
registrar->messenger(), "plugins.flutter.io/camera_desktop",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
auto plugin = std::make_unique<CameraDesktopPlugin>(registrar,
|
||||
std::move(channel));
|
||||
plugin->should_co_uninitialize_ = (co_hr == S_OK || co_hr == S_FALSE);
|
||||
instance_ = plugin.get();
|
||||
|
||||
plugin->channel_->SetMethodCallHandler(
|
||||
[plugin_ptr = plugin.get()](const auto& call, auto result) {
|
||||
plugin_ptr->HandleMethodCall(call, std::move(result));
|
||||
});
|
||||
|
||||
registrar->AddPlugin(std::move(plugin));
|
||||
}
|
||||
|
||||
CameraDesktopPlugin::CameraDesktopPlugin(
|
||||
flutter::PluginRegistrarWindows* registrar,
|
||||
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> channel)
|
||||
: registrar_(registrar),
|
||||
channel_(std::move(channel)),
|
||||
task_dispatcher_(std::make_unique<TaskDispatcher>()) {}
|
||||
|
||||
CameraDesktopPlugin::~CameraDesktopPlugin() {
|
||||
shutting_down_ = true;
|
||||
instance_ = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(cameras_mutex_);
|
||||
for (auto& [id, camera] : cameras_) {
|
||||
camera_desktop_ffi_release_handles_for_camera(camera.get());
|
||||
camera->Dispose();
|
||||
}
|
||||
cameras_.clear();
|
||||
}
|
||||
MFShutdown();
|
||||
if (should_co_uninitialize_) {
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Method dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CameraDesktopPlugin::HandleMethodCall(
|
||||
const flutter::MethodCall<flutter::EncodableValue>& call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
const std::string& method = call.method_name();
|
||||
const flutter::EncodableMap* args =
|
||||
std::get_if<flutter::EncodableMap>(call.arguments());
|
||||
const flutter::EncodableMap empty_args;
|
||||
const flutter::EncodableMap& safe_args = args ? *args : empty_args;
|
||||
|
||||
if (method == "availableCameras") {
|
||||
HandleAvailableCameras(std::move(result));
|
||||
} else if (method == "getPlatformCapabilities") {
|
||||
HandleGetPlatformCapabilities(std::move(result));
|
||||
} else if (method == "create") {
|
||||
HandleCreate(safe_args, std::move(result));
|
||||
} else if (method == "initialize") {
|
||||
HandleInitialize(safe_args, std::move(result));
|
||||
} else if (method == "takePicture") {
|
||||
HandleTakePicture(safe_args, std::move(result));
|
||||
} else if (method == "startVideoRecording") {
|
||||
HandleStartVideoRecording(safe_args, std::move(result));
|
||||
} else if (method == "stopVideoRecording") {
|
||||
HandleStopVideoRecording(safe_args, std::move(result));
|
||||
} else if (method == "startImageStream") {
|
||||
HandleStartImageStream(safe_args, std::move(result));
|
||||
} else if (method == "stopImageStream") {
|
||||
HandleStopImageStream(safe_args, std::move(result));
|
||||
} else if (method == "pausePreview") {
|
||||
HandlePausePreview(safe_args, std::move(result));
|
||||
} else if (method == "resumePreview") {
|
||||
HandleResumePreview(safe_args, std::move(result));
|
||||
} else if (method == "setMirror") {
|
||||
HandleSetMirror(safe_args, std::move(result));
|
||||
} else if (method == "dispose") {
|
||||
HandleDispose(safe_args, std::move(result));
|
||||
} else {
|
||||
result->NotImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Individual handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CameraDesktopPlugin::HandleAvailableCameras(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
DebugLog("HandleAvailableCameras: enumerating video devices");
|
||||
auto* raw_result = result.release();
|
||||
std::thread([raw_result]() {
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> async_result(
|
||||
raw_result);
|
||||
|
||||
auto devices = DeviceEnumerator::EnumerateVideoDevices();
|
||||
DebugLog("HandleAvailableCameras: returning " +
|
||||
std::to_string(devices.size()) + " camera(s)");
|
||||
|
||||
flutter::EncodableList list;
|
||||
for (const auto& device : devices) {
|
||||
auto to_utf8 = [](const std::wstring& w) -> std::string {
|
||||
if (w.empty()) return {};
|
||||
int size = WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(),
|
||||
nullptr, 0, nullptr, nullptr);
|
||||
std::string s(size, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), s.data(), size,
|
||||
nullptr, nullptr);
|
||||
return s;
|
||||
};
|
||||
|
||||
std::string display_name = to_utf8(device.friendly_name) + " (" +
|
||||
to_utf8(device.symbolic_link) + ")";
|
||||
|
||||
list.push_back(flutter::EncodableValue(flutter::EncodableMap{
|
||||
{flutter::EncodableValue("name"),
|
||||
flutter::EncodableValue(display_name)},
|
||||
{flutter::EncodableValue("lensDirection"),
|
||||
flutter::EncodableValue(0)},
|
||||
{flutter::EncodableValue("sensorOrientation"),
|
||||
flutter::EncodableValue(0)},
|
||||
}));
|
||||
}
|
||||
|
||||
async_result->Success(flutter::EncodableValue(list));
|
||||
CoUninitialize();
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleGetPlatformCapabilities(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
result->Success(flutter::EncodableValue(flutter::EncodableMap{
|
||||
{flutter::EncodableValue("supportsMirrorControl"),
|
||||
flutter::EncodableValue(false)},
|
||||
{flutter::EncodableValue("supportsVideoFpsControl"),
|
||||
flutter::EncodableValue(true)},
|
||||
{flutter::EncodableValue("supportsVideoBitrateControl"),
|
||||
flutter::EncodableValue(true)},
|
||||
}));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleCreate(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
const std::string* camera_name =
|
||||
std::get_if<std::string>(&args.at(flutter::EncodableValue("cameraName")));
|
||||
if (!camera_name) {
|
||||
result->Error("invalid_args", "cameraName is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const int* resolution_preset_ptr =
|
||||
std::get_if<int>(&args.at(flutter::EncodableValue("resolutionPreset")));
|
||||
int resolution_preset = resolution_preset_ptr ? *resolution_preset_ptr : 4;
|
||||
|
||||
const bool* enable_audio_ptr = nullptr;
|
||||
auto audio_it = args.find(flutter::EncodableValue("enableAudio"));
|
||||
if (audio_it != args.end()) {
|
||||
enable_audio_ptr = std::get_if<bool>(&audio_it->second);
|
||||
}
|
||||
bool enable_audio = enable_audio_ptr ? *enable_audio_ptr : false;
|
||||
|
||||
int target_fps = 30;
|
||||
auto fps_it = args.find(flutter::EncodableValue("fps"));
|
||||
if (fps_it != args.end()) {
|
||||
if (const int* fps_int = std::get_if<int>(&fps_it->second)) {
|
||||
target_fps = *fps_int;
|
||||
} else if (const double* fps_double = std::get_if<double>(&fps_it->second)) {
|
||||
target_fps = static_cast<int>(*fps_double);
|
||||
}
|
||||
}
|
||||
if (target_fps < 5) target_fps = 5;
|
||||
if (target_fps > 60) target_fps = 60;
|
||||
|
||||
int target_bitrate = 0;
|
||||
auto bitrate_it = args.find(flutter::EncodableValue("videoBitrate"));
|
||||
if (bitrate_it != args.end()) {
|
||||
if (const int* bitrate_int = std::get_if<int>(&bitrate_it->second)) {
|
||||
target_bitrate = *bitrate_int;
|
||||
} else if (const int64_t* bitrate_i64 =
|
||||
std::get_if<int64_t>(&bitrate_it->second)) {
|
||||
target_bitrate = static_cast<int>(*bitrate_i64);
|
||||
} else if (const double* bitrate_double =
|
||||
std::get_if<double>(&bitrate_it->second)) {
|
||||
target_bitrate = static_cast<int>(*bitrate_double);
|
||||
}
|
||||
}
|
||||
if (target_bitrate < 0) target_bitrate = 0;
|
||||
|
||||
int audio_bitrate = 0;
|
||||
auto audio_bitrate_it = args.find(flutter::EncodableValue("audioBitrate"));
|
||||
if (audio_bitrate_it != args.end()) {
|
||||
if (const int* vi = std::get_if<int>(&audio_bitrate_it->second)) {
|
||||
audio_bitrate = *vi;
|
||||
} else if (const int64_t* vi64 =
|
||||
std::get_if<int64_t>(&audio_bitrate_it->second)) {
|
||||
audio_bitrate = static_cast<int>(*vi64);
|
||||
} else if (const double* vd =
|
||||
std::get_if<double>(&audio_bitrate_it->second)) {
|
||||
audio_bitrate = static_cast<int>(*vd);
|
||||
}
|
||||
}
|
||||
if (audio_bitrate < 0) audio_bitrate = 0;
|
||||
|
||||
DebugLog("HandleCreate: camera_name=" + *camera_name +
|
||||
" preset=" + std::to_string(resolution_preset) +
|
||||
" audio=" + std::string(enable_audio ? "yes" : "no") +
|
||||
" fps=" + std::to_string(target_fps) +
|
||||
" bitrate=" + std::to_string(target_bitrate));
|
||||
|
||||
std::wstring symbolic_link = DeviceEnumerator::FindSymbolicLink(*camera_name);
|
||||
if (symbolic_link.empty()) {
|
||||
DebugLog("HandleCreate: symbolic link not found for camera: " + *camera_name);
|
||||
result->Error("camera_not_found",
|
||||
"Could not find camera: " + *camera_name);
|
||||
return;
|
||||
}
|
||||
|
||||
CameraConfig config;
|
||||
config.symbolic_link = symbolic_link;
|
||||
config.resolution_preset = resolution_preset;
|
||||
config.enable_audio = enable_audio;
|
||||
config.target_fps = target_fps;
|
||||
config.target_bitrate = target_bitrate;
|
||||
config.audio_bitrate = audio_bitrate;
|
||||
|
||||
int camera_id = next_camera_id_++;
|
||||
DebugLog("HandleCreate: assigning camera_id=" + std::to_string(camera_id));
|
||||
TaskDispatcher* dispatcher = task_dispatcher_.get();
|
||||
Camera::PlatformTaskPoster poster = [dispatcher](std::function<void()> task,
|
||||
const char* tag) {
|
||||
dispatcher->Post(std::move(task), tag);
|
||||
};
|
||||
auto camera = std::make_shared<Camera>(
|
||||
camera_id,
|
||||
registrar_->texture_registrar(),
|
||||
channel_.get(),
|
||||
config,
|
||||
std::move(poster));
|
||||
|
||||
int64_t texture_id = camera->RegisterTexture();
|
||||
if (texture_id < 0) {
|
||||
DebugLog("HandleCreate: texture registration failed for camera_id=" +
|
||||
std::to_string(camera_id));
|
||||
result->Error("texture_registration_failed",
|
||||
"Failed to register Flutter texture");
|
||||
return;
|
||||
}
|
||||
DebugLog("HandleCreate: texture_id=" + std::to_string(texture_id) +
|
||||
" registered for camera_id=" + std::to_string(camera_id));
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(cameras_mutex_);
|
||||
cameras_[camera_id] = std::move(camera);
|
||||
}
|
||||
|
||||
result->Success(flutter::EncodableValue(flutter::EncodableMap{
|
||||
{flutter::EncodableValue("cameraId"),
|
||||
flutter::EncodableValue(camera_id)},
|
||||
{flutter::EncodableValue("textureId"),
|
||||
flutter::EncodableValue(static_cast<int64_t>(texture_id))},
|
||||
}));
|
||||
}
|
||||
|
||||
std::shared_ptr<Camera> CameraDesktopPlugin::FindCamera(
|
||||
const flutter::EncodableMap& args,
|
||||
flutter::MethodResult<flutter::EncodableValue>* result) {
|
||||
auto it = args.find(flutter::EncodableValue("cameraId"));
|
||||
if (it == args.end()) {
|
||||
result->Error("invalid_args", "cameraId is required");
|
||||
return nullptr;
|
||||
}
|
||||
int camera_id = std::get<int>(it->second);
|
||||
std::lock_guard<std::mutex> lk(cameras_mutex_);
|
||||
auto cam_it = cameras_.find(camera_id);
|
||||
if (cam_it == cameras_.end() || cam_it->second->IsDisposedOrDisposing()) {
|
||||
result->Error("camera_not_found", "No camera with id " +
|
||||
std::to_string(camera_id));
|
||||
return {};
|
||||
}
|
||||
return cam_it->second;
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::EraseCameraAfterDispose(int camera_id) {
|
||||
if (shutting_down_) return;
|
||||
std::lock_guard<std::mutex> lk(cameras_mutex_);
|
||||
auto it = cameras_.find(camera_id);
|
||||
if (it != cameras_.end() && it->second->IsDisposedOrDisposing()) {
|
||||
cameras_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleInitialize(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
camera->Initialize(std::move(result));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleTakePicture(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
camera->TakePicture(std::move(result));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleStartVideoRecording(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
camera->StartVideoRecording(std::move(result));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleStopVideoRecording(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
camera->StopVideoRecording(std::move(result));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleStartImageStream(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
camera->StartImageStream();
|
||||
const int64_t stream_handle =
|
||||
camera_desktop_ffi_register_stream_handle(camera.get());
|
||||
result->Success(flutter::EncodableValue(flutter::EncodableMap{
|
||||
{flutter::EncodableValue("streamHandle"),
|
||||
flutter::EncodableValue(stream_handle)},
|
||||
}));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleStopImageStream(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
auto handle_it = args.find(flutter::EncodableValue("streamHandle"));
|
||||
if (handle_it != args.end()) {
|
||||
if (const int64_t* h64 = std::get_if<int64_t>(&handle_it->second)) {
|
||||
camera_desktop_ffi_release_stream_handle(*h64);
|
||||
} else if (const int* h32 = std::get_if<int>(&handle_it->second)) {
|
||||
camera_desktop_ffi_release_stream_handle(static_cast<int64_t>(*h32));
|
||||
} else if (const double* hd = std::get_if<double>(&handle_it->second)) {
|
||||
camera_desktop_ffi_release_stream_handle(static_cast<int64_t>(*hd));
|
||||
}
|
||||
}
|
||||
camera->StopImageStream();
|
||||
result->Success(flutter::EncodableValue(nullptr));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandlePausePreview(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
camera->PausePreview();
|
||||
result->Success(flutter::EncodableValue(nullptr));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleResumePreview(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
camera->ResumePreview();
|
||||
result->Success(flutter::EncodableValue(nullptr));
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleSetMirror(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto camera = FindCamera(args, result.get());
|
||||
if (!camera) return;
|
||||
|
||||
auto it = args.find(flutter::EncodableValue("mirrored"));
|
||||
const bool* mirrored = (it == args.end())
|
||||
? nullptr
|
||||
: std::get_if<bool>(&it->second);
|
||||
if (!mirrored) {
|
||||
result->Error("invalid_args", "mirrored is required");
|
||||
return;
|
||||
}
|
||||
(void)mirrored;
|
||||
|
||||
result->Error("unsupported", "Mirror control is not supported on Windows.");
|
||||
}
|
||||
|
||||
void CameraDesktopPlugin::HandleDispose(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
auto it = args.find(flutter::EncodableValue("cameraId"));
|
||||
if (it != args.end()) {
|
||||
int camera_id = std::get<int>(it->second);
|
||||
DebugLog("HandleDispose: dispose requested for camera_id=" +
|
||||
std::to_string(camera_id));
|
||||
std::shared_ptr<Camera> camera;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(cameras_mutex_);
|
||||
auto cam_it = cameras_.find(camera_id);
|
||||
if (cam_it != cameras_.end()) {
|
||||
camera = cam_it->second;
|
||||
}
|
||||
}
|
||||
if (camera) {
|
||||
camera_desktop_ffi_release_handles_for_camera(camera.get());
|
||||
camera->DisposeAsync([camera_id]() {
|
||||
DebugLog("HandleDispose: async dispose complete for camera_id=" +
|
||||
std::to_string(camera_id));
|
||||
auto* plugin = CameraDesktopPlugin::instance();
|
||||
if (plugin) {
|
||||
plugin->EraseCameraAfterDispose(camera_id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
DebugLog("HandleDispose: camera_id=" + std::to_string(camera_id) +
|
||||
" not found (already disposed?)");
|
||||
}
|
||||
}
|
||||
result->Success(flutter::EncodableValue(nullptr));
|
||||
}
|
||||
104
plugins/camera_desktop/windows/camera_desktop_plugin.h
Normal file
104
plugins/camera_desktop/windows/camera_desktop_plugin.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/plugin_registrar_windows.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "camera.h"
|
||||
|
||||
// Marshals arbitrary work to the Win32 message-loop thread (the Flutter
|
||||
// platform thread) via a hidden message-only HWND. Must be constructed on
|
||||
// the platform thread; Post() is thread-safe.
|
||||
class TaskDispatcher {
|
||||
public:
|
||||
TaskDispatcher();
|
||||
~TaskDispatcher();
|
||||
void Post(std::function<void()> task, const char* tag = nullptr);
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg,
|
||||
WPARAM wparam, LPARAM lparam);
|
||||
HWND hwnd_ = nullptr;
|
||||
DWORD platform_thread_id_ = 0;
|
||||
};
|
||||
|
||||
class CameraDesktopPlugin : public flutter::Plugin {
|
||||
public:
|
||||
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);
|
||||
|
||||
CameraDesktopPlugin(
|
||||
flutter::PluginRegistrarWindows* registrar,
|
||||
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> channel);
|
||||
|
||||
~CameraDesktopPlugin() override;
|
||||
|
||||
void EraseCameraAfterDispose(int camera_id);
|
||||
|
||||
// Global instance for FFI access.
|
||||
static CameraDesktopPlugin* instance() { return instance_; }
|
||||
|
||||
private:
|
||||
void HandleMethodCall(
|
||||
const flutter::MethodCall<flutter::EncodableValue>& call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
|
||||
// Helpers for individual methods.
|
||||
void HandleAvailableCameras(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleGetPlatformCapabilities(
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleCreate(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleInitialize(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleTakePicture(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleStartVideoRecording(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleStopVideoRecording(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleStartImageStream(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleStopImageStream(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandlePausePreview(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleResumePreview(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleSetMirror(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
void HandleDispose(
|
||||
const flutter::EncodableMap& args,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
|
||||
// Returns the camera for |args["cameraId"]| or responds with an error.
|
||||
std::shared_ptr<Camera> FindCamera(
|
||||
const flutter::EncodableMap& args,
|
||||
flutter::MethodResult<flutter::EncodableValue>* result);
|
||||
|
||||
flutter::PluginRegistrarWindows* registrar_;
|
||||
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> channel_;
|
||||
std::unique_ptr<TaskDispatcher> task_dispatcher_;
|
||||
mutable std::mutex cameras_mutex_;
|
||||
std::map<int, std::shared_ptr<Camera>> cameras_;
|
||||
int next_camera_id_ = 1;
|
||||
bool should_co_uninitialize_ = false;
|
||||
bool shutting_down_ = false;
|
||||
|
||||
static CameraDesktopPlugin* instance_;
|
||||
};
|
||||
103
plugins/camera_desktop/windows/camera_texture.cpp
Normal file
103
plugins/camera_desktop/windows/camera_texture.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "camera_texture.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
CameraTexture::CameraTexture(flutter::TextureRegistrar* registrar)
|
||||
: registrar_(registrar) {}
|
||||
|
||||
CameraTexture::~CameraTexture() {
|
||||
Unregister();
|
||||
}
|
||||
|
||||
int64_t CameraTexture::Register() {
|
||||
texture_variant_ = std::make_unique<flutter::TextureVariant>(
|
||||
flutter::PixelBufferTexture(
|
||||
[this](size_t w, size_t h) -> const FlutterDesktopPixelBuffer* {
|
||||
return ObtainPixelBuffer(w, h);
|
||||
}));
|
||||
texture_id_ = registrar_->RegisterTexture(texture_variant_.get());
|
||||
if (texture_id_ >= 0) {
|
||||
DebugLog("CameraTexture::Register: texture_id=" + std::to_string(texture_id_));
|
||||
} else {
|
||||
DebugLog("CameraTexture::Register: RegisterTexture failed (texture_id=" + std::to_string(texture_id_) + ")");
|
||||
}
|
||||
return texture_id_;
|
||||
}
|
||||
|
||||
void CameraTexture::Update(const uint8_t* bgra, int width, int height) {
|
||||
const size_t required = static_cast<size_t>(width) * height * 4;
|
||||
uint8_t* dst = nullptr;
|
||||
int write_idx_snapshot = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
// Reallocate all three buffers when dimensions change.
|
||||
if (width != width_ || height != height_) {
|
||||
for (auto& buf : bufs_) {
|
||||
buf.resize(required);
|
||||
}
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
}
|
||||
write_idx_snapshot = write_idx_;
|
||||
dst = bufs_[write_idx_snapshot].data();
|
||||
}
|
||||
|
||||
// Keep memcpy outside the mutex to minimize render-thread contention.
|
||||
std::memcpy(dst, bgra, required);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (write_idx_ == write_idx_snapshot) {
|
||||
std::swap(write_idx_, ready_idx_);
|
||||
has_new_frame_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FlutterDesktopPixelBuffer* CameraTexture::ObtainPixelBuffer(
|
||||
size_t /*width*/, size_t /*height*/) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (width_ == 0 || height_ == 0) return nullptr;
|
||||
|
||||
// Swap ready ↔ read if a new frame arrived.
|
||||
if (has_new_frame_) {
|
||||
std::swap(ready_idx_, read_idx_);
|
||||
has_new_frame_ = false;
|
||||
}
|
||||
|
||||
pixel_buffer_.buffer = bufs_[read_idx_].data();
|
||||
pixel_buffer_.width = static_cast<size_t>(width_);
|
||||
pixel_buffer_.height = static_cast<size_t>(height_);
|
||||
pixel_buffer_.release_callback = nullptr;
|
||||
pixel_buffer_.release_context = nullptr;
|
||||
|
||||
return &pixel_buffer_;
|
||||
}
|
||||
|
||||
void CameraTexture::Unregister() {
|
||||
if (texture_id_ >= 0 && registrar_) {
|
||||
DebugLog("CameraTexture::Unregister: texture_id=" + std::to_string(texture_id_));
|
||||
registrar_->UnregisterTexture(texture_id_);
|
||||
texture_id_ = -1;
|
||||
}
|
||||
texture_variant_.reset();
|
||||
}
|
||||
|
||||
void CameraTexture::UnregisterAsync(std::function<void()> on_done) {
|
||||
if (texture_id_ >= 0 && registrar_) {
|
||||
DebugLog("CameraTexture::UnregisterAsync: texture_id=" +
|
||||
std::to_string(texture_id_));
|
||||
// Async overload: Flutter removes the texture on the raster thread and only
|
||||
// then invokes |on_done|. Until that point the engine may still call the
|
||||
// pixel-buffer callback, so the object must stay alive (caller holds it).
|
||||
registrar_->UnregisterTexture(texture_id_, std::move(on_done));
|
||||
texture_id_ = -1;
|
||||
} else if (on_done) {
|
||||
on_done();
|
||||
}
|
||||
}
|
||||
62
plugins/camera_desktop/windows/camera_texture.h
Normal file
62
plugins/camera_desktop/windows/camera_texture.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <flutter/texture_registrar.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
// Triple-buffer software texture for Windows.
|
||||
//
|
||||
// The capture thread calls Update() with new BGRA32 pixels.
|
||||
// The Flutter render thread calls the pixel-buffer callback to read frames.
|
||||
// Triple buffering avoids any locking between writer and reader:
|
||||
// - write_idx : capture thread writes here
|
||||
// - ready_idx : swapped by capture thread after write (latest frame)
|
||||
// - read_idx : Flutter render thread reads from here
|
||||
class CameraTexture {
|
||||
public:
|
||||
explicit CameraTexture(flutter::TextureRegistrar* registrar);
|
||||
~CameraTexture();
|
||||
|
||||
// Registers the texture with Flutter and returns the texture ID.
|
||||
int64_t Register();
|
||||
|
||||
// Updates the texture with a new BGRA32 frame.
|
||||
// Called from the capture thread.
|
||||
void Update(const uint8_t* bgra, int width, int height);
|
||||
|
||||
// Unregisters the texture from Flutter.
|
||||
void Unregister();
|
||||
|
||||
// Asynchronously unregisters the texture; |on_done| runs once Flutter has removed
|
||||
// it (on the raster thread). The caller MUST keep this object alive until then,
|
||||
// because Flutter's pixel-buffer callback can still fire on the raster thread after
|
||||
// an ordinary Unregister() returns. See CRASH.md.
|
||||
void UnregisterAsync(std::function<void()> on_done);
|
||||
|
||||
int64_t texture_id() const { return texture_id_; }
|
||||
|
||||
private:
|
||||
const FlutterDesktopPixelBuffer* ObtainPixelBuffer(size_t width,
|
||||
size_t height);
|
||||
|
||||
flutter::TextureRegistrar* registrar_;
|
||||
std::unique_ptr<flutter::TextureVariant> texture_variant_;
|
||||
int64_t texture_id_ = -1;
|
||||
|
||||
// Triple buffer, same pattern as linux/camera_texture.cc.
|
||||
std::vector<uint8_t> bufs_[3];
|
||||
int write_idx_ = 0;
|
||||
int ready_idx_ = 1;
|
||||
int read_idx_ = 2;
|
||||
bool has_new_frame_ = false;
|
||||
std::mutex mutex_;
|
||||
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
|
||||
FlutterDesktopPixelBuffer pixel_buffer_{};
|
||||
};
|
||||
105
plugins/camera_desktop/windows/device_enumerator.cpp
Normal file
105
plugins/camera_desktop/windows/device_enumerator.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "device_enumerator.h"
|
||||
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
#include <string>
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::wstring Utf8ToWide(const std::string& s) {
|
||||
if (s.empty()) return {};
|
||||
int size = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(),
|
||||
nullptr, 0);
|
||||
std::wstring w(size, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(),
|
||||
w.data(), size);
|
||||
return w;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DeviceEnumerator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<DeviceInfo> DeviceEnumerator::EnumerateVideoDevices() {
|
||||
DebugLog("DeviceEnumerator::EnumerateVideoDevices start");
|
||||
std::vector<DeviceInfo> result;
|
||||
|
||||
ComPtr<IMFAttributes> attrs;
|
||||
if (FAILED(MFCreateAttributes(&attrs, 1))) {
|
||||
DebugLog("DeviceEnumerator::EnumerateVideoDevices MFCreateAttributes failed");
|
||||
return result;
|
||||
}
|
||||
if (FAILED(attrs->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
|
||||
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID))) {
|
||||
DebugLog("DeviceEnumerator::EnumerateVideoDevices SetGUID failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
IMFActivate** devices = nullptr;
|
||||
UINT32 count = 0;
|
||||
if (FAILED(MFEnumDeviceSources(attrs.Get(), &devices, &count))) {
|
||||
DebugLog("DeviceEnumerator::EnumerateVideoDevices MFEnumDeviceSources failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
DebugLog("DeviceEnumerator::EnumerateVideoDevices found " +
|
||||
std::to_string(count) + " device(s)");
|
||||
|
||||
for (UINT32 i = 0; i < count; ++i) {
|
||||
WCHAR* friendly_name = nullptr;
|
||||
UINT32 fn_len = 0;
|
||||
WCHAR* symbolic_link = nullptr;
|
||||
UINT32 sl_len = 0;
|
||||
|
||||
devices[i]->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME,
|
||||
&friendly_name, &fn_len);
|
||||
devices[i]->GetAllocatedString(
|
||||
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
|
||||
&symbolic_link, &sl_len);
|
||||
|
||||
if (friendly_name && symbolic_link) {
|
||||
result.push_back({friendly_name, symbolic_link});
|
||||
DebugLog("DeviceEnumerator: device[" + std::to_string(i) + "] name=" +
|
||||
WideToUtf8(friendly_name) +
|
||||
" symlink=" + WideToUtf8(symbolic_link));
|
||||
} else {
|
||||
DebugLog("DeviceEnumerator: device[" + std::to_string(i) +
|
||||
"] skipped (missing friendly_name=" +
|
||||
std::string(friendly_name ? "ok" : "null") +
|
||||
" or symbolic_link=" +
|
||||
std::string(symbolic_link ? "ok" : "null") + ")");
|
||||
}
|
||||
|
||||
if (friendly_name) CoTaskMemFree(friendly_name);
|
||||
if (symbolic_link) CoTaskMemFree(symbolic_link);
|
||||
devices[i]->Release();
|
||||
}
|
||||
|
||||
CoTaskMemFree(devices);
|
||||
DebugLog("DeviceEnumerator::EnumerateVideoDevices returning " +
|
||||
std::to_string(result.size()) + " valid device(s)");
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring DeviceEnumerator::FindSymbolicLink(const std::string& name) {
|
||||
// Name format: "Friendly Name (symbolic_link)"
|
||||
// Extract the part inside the last pair of parentheses.
|
||||
auto last_open = name.rfind('(');
|
||||
auto last_close = name.rfind(')');
|
||||
if (last_open == std::string::npos || last_close == std::string::npos ||
|
||||
last_close < last_open) {
|
||||
return {};
|
||||
}
|
||||
std::string sym_utf8 = name.substr(last_open + 1, last_close - last_open - 1);
|
||||
return Utf8ToWide(sym_utf8);
|
||||
}
|
||||
20
plugins/camera_desktop/windows/device_enumerator.h
Normal file
20
plugins/camera_desktop/windows/device_enumerator.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct DeviceInfo {
|
||||
std::wstring friendly_name;
|
||||
std::wstring symbolic_link;
|
||||
};
|
||||
|
||||
class DeviceEnumerator {
|
||||
public:
|
||||
/// Returns all connected video capture devices.
|
||||
static std::vector<DeviceInfo> EnumerateVideoDevices();
|
||||
|
||||
/// Finds the symbolic link for a camera whose dart-side name is |name|.
|
||||
/// The name format is "Friendly Name (symbolic_link)".
|
||||
/// Returns empty string if not found.
|
||||
static std::wstring FindSymbolicLink(const std::string& name);
|
||||
};
|
||||
85
plugins/camera_desktop/windows/image_stream_ffi.cpp
Normal file
85
plugins/camera_desktop/windows/image_stream_ffi.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include "camera.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex g_stream_handles_mutex;
|
||||
int64_t g_next_stream_handle = 1;
|
||||
std::unordered_map<int64_t, Camera*> g_stream_handles;
|
||||
|
||||
Camera* FindCameraByHandle(int64_t stream_handle) {
|
||||
std::lock_guard<std::mutex> lk(g_stream_handles_mutex);
|
||||
auto it = g_stream_handles.find(stream_handle);
|
||||
if (it == g_stream_handles.end()) {
|
||||
DebugLog("FindCameraByHandle: handle " + std::to_string(stream_handle) + " not found");
|
||||
return nullptr;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int64_t camera_desktop_ffi_register_stream_handle(Camera* camera) {
|
||||
if (!camera) return 0;
|
||||
std::lock_guard<std::mutex> lk(g_stream_handles_mutex);
|
||||
const int64_t handle = g_next_stream_handle++;
|
||||
g_stream_handles.emplace(handle, camera);
|
||||
DebugLog("camera_desktop_ffi_register_stream_handle: handle=" + std::to_string(handle));
|
||||
return handle;
|
||||
}
|
||||
|
||||
void camera_desktop_ffi_release_stream_handle(int64_t stream_handle) {
|
||||
if (stream_handle == 0) return;
|
||||
DebugLog("camera_desktop_ffi_release_stream_handle: handle=" + std::to_string(stream_handle));
|
||||
std::lock_guard<std::mutex> lk(g_stream_handles_mutex);
|
||||
g_stream_handles.erase(stream_handle);
|
||||
}
|
||||
|
||||
void camera_desktop_ffi_release_handles_for_camera(Camera* camera) {
|
||||
if (!camera) return;
|
||||
std::lock_guard<std::mutex> lk(g_stream_handles_mutex);
|
||||
int released = 0;
|
||||
for (auto it = g_stream_handles.begin(); it != g_stream_handles.end();) {
|
||||
if (it->second == camera) {
|
||||
it = g_stream_handles.erase(it);
|
||||
++released;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
DebugLog("camera_desktop_ffi_release_handles_for_camera: released " + std::to_string(released) + " handle(s)");
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
__declspec(dllexport) void camera_desktop_image_stream_noop_callback(
|
||||
int32_t camera_id) {
|
||||
(void)camera_id;
|
||||
}
|
||||
|
||||
__declspec(dllexport) void* camera_desktop_get_image_stream_buffer(
|
||||
int64_t stream_handle) {
|
||||
Camera* camera = FindCameraByHandle(stream_handle);
|
||||
if (!camera) return nullptr;
|
||||
return camera->GetImageStreamBuffer();
|
||||
}
|
||||
|
||||
__declspec(dllexport) void camera_desktop_register_image_stream_callback(
|
||||
int64_t stream_handle, void (*callback)(int32_t)) {
|
||||
Camera* camera = FindCameraByHandle(stream_handle);
|
||||
if (camera) camera->RegisterImageStreamCallback(callback);
|
||||
}
|
||||
|
||||
__declspec(dllexport) void camera_desktop_unregister_image_stream_callback(
|
||||
int64_t stream_handle) {
|
||||
Camera* camera = FindCameraByHandle(stream_handle);
|
||||
if (camera) camera->UnregisterImageStreamCallback();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <flutter_plugin_registrar.h>
|
||||
|
||||
#ifdef FLUTTER_PLUGIN_IMPL
|
||||
#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
FLUTTER_PLUGIN_EXPORT void CameraDesktopPluginRegisterWithRegistrar(
|
||||
FlutterDesktopPluginRegistrarRef registrar);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
48
plugins/camera_desktop/windows/logging.h
Normal file
48
plugins/camera_desktop/windows/logging.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
// Diagnostic logging is opt-in and OFF by default, so the plugin stays quiet in
|
||||
// consumer apps and avoids the cost of OutputDebugString under a debugger. To
|
||||
// capture a trace for a bug report, set the environment variable
|
||||
// CAMERA_DESKTOP_LOG to any value other than "0" before launching, then
|
||||
// reproduce. Real errors still reach the app through the method channel
|
||||
// (result.Error / cameraError) regardless of this setting.
|
||||
inline bool DebugLogEnabled() {
|
||||
static const bool enabled = [] {
|
||||
char buf[16] = {};
|
||||
DWORD n = GetEnvironmentVariableA("CAMERA_DESKTOP_LOG", buf, sizeof(buf));
|
||||
if (n == 0) return false; // not set
|
||||
if (n >= sizeof(buf)) return true; // set to some long value
|
||||
return std::string(buf) != "0";
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
inline void DebugLog(const std::string& msg) {
|
||||
if (!DebugLogEnabled()) return;
|
||||
std::string line = "[camera_desktop/windows] " + msg + "\n";
|
||||
OutputDebugStringA(line.c_str());
|
||||
std::fputs(line.c_str(), stderr);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
inline std::string WideToUtf8(const std::wstring& w) {
|
||||
if (w.empty()) return {};
|
||||
int size = WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(),
|
||||
nullptr, 0, nullptr, nullptr);
|
||||
std::string s(size, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(),
|
||||
s.data(), size, nullptr, nullptr);
|
||||
return s;
|
||||
}
|
||||
|
||||
inline std::string HrToString(HRESULT hr) {
|
||||
std::ostringstream ss;
|
||||
ss << "0x" << std::hex << static_cast<unsigned long>(hr);
|
||||
return ss.str();
|
||||
}
|
||||
140
plugins/camera_desktop/windows/photo_handler.cpp
Normal file
140
plugins/camera_desktop/windows/photo_handler.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "photo_handler.h"
|
||||
|
||||
#include <wincodec.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
bool PhotoHandler::Write(const uint8_t* bgra, int width, int height,
|
||||
const std::wstring& path, std::string* error) {
|
||||
DebugLog("PhotoHandler::Write: " + std::to_string(width) + "x" +
|
||||
std::to_string(height) + " path.length=" + std::to_string(path.size()));
|
||||
|
||||
if (!bgra || width <= 0 || height <= 0) {
|
||||
if (error) *error = "Invalid image buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<IWICImagingFactory> wic;
|
||||
HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr,
|
||||
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wic));
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("PhotoHandler::Write: CoCreateInstance WIC factory failed " + HrToString(hr));
|
||||
if (error) *error = "Failed to create WIC factory";
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<IWICStream> stream;
|
||||
hr = wic->CreateStream(&stream);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("PhotoHandler::Write: CreateStream failed " + HrToString(hr));
|
||||
if (error) *error = "Failed to create WIC stream";
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = stream->InitializeFromFilename(path.c_str(), GENERIC_WRITE);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("PhotoHandler::Write: InitializeFromFilename failed " + HrToString(hr));
|
||||
if (error) *error = "Failed to open output file";
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<IWICBitmapEncoder> encoder;
|
||||
hr = wic->CreateEncoder(GUID_ContainerFormatJpeg, nullptr, &encoder);
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to create JPEG encoder";
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache);
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to initialize encoder";
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<IWICBitmapFrameEncode> frame;
|
||||
hr = encoder->CreateNewFrame(&frame, nullptr);
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to create frame";
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = frame->Initialize(nullptr);
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to initialize frame";
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = frame->SetSize(static_cast<UINT>(width), static_cast<UINT>(height));
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to set frame size";
|
||||
return false;
|
||||
}
|
||||
|
||||
WICPixelFormatGUID fmt = GUID_WICPixelFormat24bppBGR;
|
||||
hr = frame->SetPixelFormat(&fmt);
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to set pixel format";
|
||||
return false;
|
||||
}
|
||||
if (fmt != GUID_WICPixelFormat24bppBGR) {
|
||||
if (error) *error = "JPEG encoder rejected 24bppBGR pixel format";
|
||||
return false;
|
||||
}
|
||||
|
||||
// JPEG does not store alpha. Convert BGRA32 to packed BGR24 explicitly.
|
||||
const UINT stride = static_cast<UINT>(width) * 3;
|
||||
const UINT data_size = stride * static_cast<UINT>(height);
|
||||
std::vector<uint8_t> bgr24(data_size);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const uint8_t* src_row = bgra + static_cast<size_t>(y) * width * 4;
|
||||
uint8_t* dst_row = bgr24.data() + static_cast<size_t>(y) * stride;
|
||||
for (int x = 0; x < width; ++x) {
|
||||
dst_row[x * 3 + 0] = src_row[x * 4 + 0];
|
||||
dst_row[x * 3 + 1] = src_row[x * 4 + 1];
|
||||
dst_row[x * 3 + 2] = src_row[x * 4 + 2];
|
||||
}
|
||||
}
|
||||
|
||||
hr = frame->WritePixels(static_cast<UINT>(height), stride, data_size,
|
||||
bgr24.data());
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to write pixels";
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = frame->Commit();
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to commit frame";
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = encoder->Commit();
|
||||
if (FAILED(hr)) {
|
||||
if (error) *error = "Failed to commit encoder";
|
||||
return false;
|
||||
}
|
||||
|
||||
DebugLog("PhotoHandler::Write: success " + std::to_string(width) + "x" + std::to_string(height));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring PhotoHandler::GeneratePath(int camera_id) {
|
||||
WCHAR temp_dir[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, temp_dir);
|
||||
|
||||
auto now = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
std::wostringstream ss;
|
||||
ss << temp_dir << L"camera_desktop_" << camera_id << L"_" << now << L".jpg";
|
||||
std::wstring path = ss.str();
|
||||
DebugLog("PhotoHandler::GeneratePath: camera_id=" + std::to_string(camera_id) +
|
||||
" path=" + WideToUtf8(path));
|
||||
return path;
|
||||
}
|
||||
15
plugins/camera_desktop/windows/photo_handler.h
Normal file
15
plugins/camera_desktop/windows/photo_handler.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
class PhotoHandler {
|
||||
public:
|
||||
// Writes |bgra| pixels (already flipped by the caller) as a JPEG to |path|.
|
||||
// Returns true on success; sets |error| on failure.
|
||||
static bool Write(const uint8_t* bgra, int width, int height,
|
||||
const std::wstring& path, std::string* error);
|
||||
|
||||
// Generates a unique temp-file path for a photo from |camera_id|.
|
||||
static std::wstring GeneratePath(int camera_id);
|
||||
};
|
||||
215
plugins/camera_desktop/windows/record_handler.cpp
Normal file
215
plugins/camera_desktop/windows/record_handler.cpp
Normal file
@@ -0,0 +1,215 @@
|
||||
#include "record_handler.h"
|
||||
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Queries a typed interface from a collection element.
|
||||
template <class Q>
|
||||
static HRESULT GetCollectionObject(IMFCollection* collection, DWORD index,
|
||||
Q** out) {
|
||||
ComPtr<IUnknown> unk;
|
||||
HRESULT hr = collection->GetElement(index, &unk);
|
||||
if (FAILED(hr)) return hr;
|
||||
return unk->QueryInterface(IID_PPV_ARGS(out));
|
||||
}
|
||||
|
||||
// Builds an AAC audio output media type using the lowest-latency available
|
||||
// encoder configuration (mirrors the approach in camera_windows).
|
||||
static HRESULT BuildAudioOutputType(IMFMediaType** out_type,
|
||||
int audio_bitrate = 0) {
|
||||
DebugLog("BuildAudioOutputType: entry audio_bitrate=" + std::to_string(audio_bitrate));
|
||||
|
||||
ComPtr<IMFAttributes> attrs;
|
||||
HRESULT hr = MFCreateAttributes(&attrs, 1);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildAudioOutputType: MFCreateAttributes failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
hr = attrs->SetUINT32(MF_LOW_LATENCY, TRUE);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildAudioOutputType: SetUINT32 MF_LOW_LATENCY failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
const DWORD flags = (MFT_ENUM_FLAG_ALL & (~MFT_ENUM_FLAG_FIELDOFUSE)) |
|
||||
MFT_ENUM_FLAG_SORTANDFILTER;
|
||||
|
||||
ComPtr<IMFCollection> available_types;
|
||||
hr = MFTranscodeGetAudioOutputAvailableTypes(MFAudioFormat_AAC, flags,
|
||||
attrs.Get(), &available_types);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildAudioOutputType: MFTranscodeGetAudioOutputAvailableTypes failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
DWORD count = 0;
|
||||
hr = available_types->GetElementCount(&count);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildAudioOutputType: GetElementCount failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
if (count == 0) {
|
||||
DebugLog("BuildAudioOutputType: no AAC output types available");
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
ComPtr<IMFMediaType> src_type;
|
||||
hr = GetCollectionObject(available_types.Get(), 0, src_type.GetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildAudioOutputType: GetCollectionObject failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
ComPtr<IMFMediaType> new_type;
|
||||
hr = MFCreateMediaType(&new_type);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildAudioOutputType: MFCreateMediaType failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
hr = src_type->CopyAllItems(new_type.Get());
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildAudioOutputType: CopyAllItems failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
if (audio_bitrate > 0) {
|
||||
new_type->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND,
|
||||
static_cast<UINT32>(audio_bitrate / 8));
|
||||
}
|
||||
|
||||
*out_type = new_type.Detach();
|
||||
DebugLog("BuildAudioOutputType: success");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// Builds an H264 video output type based on the camera's capture media type.
|
||||
static HRESULT BuildVideoOutputType(IMFMediaType* base_type,
|
||||
IMFMediaType** out_type, int fps,
|
||||
int bitrate) {
|
||||
DebugLog("BuildVideoOutputType: fps=" + std::to_string(fps) +
|
||||
" bitrate=" + std::to_string(bitrate));
|
||||
|
||||
ComPtr<IMFMediaType> video_type;
|
||||
HRESULT hr = MFCreateMediaType(&video_type);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildVideoOutputType: MFCreateMediaType failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
hr = base_type->CopyAllItems(video_type.Get());
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildVideoOutputType: CopyAllItems failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
hr = video_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("BuildVideoOutputType: SetGUID H264 failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
video_type->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
|
||||
|
||||
if (fps > 0) {
|
||||
MFSetAttributeRatio(video_type.Get(), MF_MT_FRAME_RATE,
|
||||
static_cast<UINT32>(fps), 1);
|
||||
}
|
||||
if (bitrate > 0) {
|
||||
video_type->SetUINT32(MF_MT_AVG_BITRATE, static_cast<UINT32>(bitrate));
|
||||
}
|
||||
|
||||
*out_type = video_type.Detach();
|
||||
DebugLog("BuildVideoOutputType: success");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RecordHandler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
HRESULT RecordHandler::InitRecordSink(IMFCaptureEngine* capture_engine,
|
||||
IMFMediaType* base_capture_media_type,
|
||||
const std::wstring& path,
|
||||
bool enable_audio, int fps,
|
||||
int video_bitrate, int audio_bitrate) {
|
||||
DebugLog("InitRecordSink: entry enable_audio=" + std::to_string(enable_audio) +
|
||||
" fps=" + std::to_string(fps) +
|
||||
" video_bitrate=" + std::to_string(video_bitrate) +
|
||||
" audio_bitrate=" + std::to_string(audio_bitrate));
|
||||
|
||||
path_ = path;
|
||||
|
||||
ComPtr<IMFCaptureSink> sink;
|
||||
HRESULT hr = capture_engine->GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_RECORD,
|
||||
&sink);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("InitRecordSink: GetSink failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
hr = sink.As(&record_sink_);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("InitRecordSink: sink.As (record sink) failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
hr = record_sink_->RemoveAllStreams();
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("InitRecordSink: RemoveAllStreams failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
// Video stream, H264.
|
||||
ComPtr<IMFMediaType> video_type;
|
||||
hr = BuildVideoOutputType(base_capture_media_type, &video_type, fps,
|
||||
video_bitrate);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("InitRecordSink: BuildVideoOutputType failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
DWORD video_stream_index;
|
||||
hr = record_sink_->AddStream(
|
||||
(DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_RECORD,
|
||||
video_type.Get(), nullptr, &video_stream_index);
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("InitRecordSink: AddStream (video) failed " + HrToString(hr));
|
||||
return hr;
|
||||
}
|
||||
|
||||
// Audio stream, AAC. Non-fatal: record continues without audio on failure.
|
||||
if (enable_audio) {
|
||||
ComPtr<IMFMediaType> audio_type;
|
||||
HRESULT audio_hr = BuildAudioOutputType(&audio_type, audio_bitrate);
|
||||
if (SUCCEEDED(audio_hr)) {
|
||||
DWORD audio_stream_index;
|
||||
HRESULT add_audio_hr = record_sink_->AddStream(
|
||||
(DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_AUDIO,
|
||||
audio_type.Get(), nullptr, &audio_stream_index);
|
||||
if (FAILED(add_audio_hr)) {
|
||||
DebugLog("InitRecordSink: AddStream (audio) failed (non-fatal) " + HrToString(add_audio_hr));
|
||||
}
|
||||
} else {
|
||||
DebugLog("InitRecordSink: BuildAudioOutputType failed (non-fatal), recording without audio " + HrToString(audio_hr));
|
||||
}
|
||||
} else {
|
||||
DebugLog("InitRecordSink: audio disabled, skipping audio stream");
|
||||
}
|
||||
|
||||
hr = record_sink_->SetOutputFileName(path.c_str());
|
||||
if (FAILED(hr)) {
|
||||
DebugLog("InitRecordSink: SetOutputFileName failed " + HrToString(hr));
|
||||
} else {
|
||||
DebugLog("InitRecordSink: success");
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
56
plugins/camera_desktop/windows/record_handler.h
Normal file
56
plugins/camera_desktop/windows/record_handler.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <mfapi.h>
|
||||
#include <mfcaptureengine.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
// Manages the IMFCaptureRecordSink for a single recording session.
|
||||
// The owning Camera calls InitRecordSink() before StartRecord(), then
|
||||
// OnRecordStarted() / OnRecordStopped() as the engine fires events.
|
||||
class RecordHandler {
|
||||
public:
|
||||
RecordHandler() = default;
|
||||
~RecordHandler() = default;
|
||||
|
||||
RecordHandler(const RecordHandler&) = delete;
|
||||
RecordHandler& operator=(const RecordHandler&) = delete;
|
||||
|
||||
// Configures IMFCaptureRecordSink with H264 video + optional AAC audio.
|
||||
// Must be called before IMFCaptureEngine::StartRecord().
|
||||
// fps / video_bitrate ≤ 0 → let engine use source defaults.
|
||||
HRESULT InitRecordSink(IMFCaptureEngine* capture_engine,
|
||||
IMFMediaType* base_capture_media_type,
|
||||
const std::wstring& path, bool enable_audio,
|
||||
int fps, int video_bitrate, int audio_bitrate = 0);
|
||||
|
||||
bool CanStart() const { return state_ == RecordState::kNotStarted; }
|
||||
bool CanStop() const { return state_ == RecordState::kRunning; }
|
||||
|
||||
void SetStarting() {
|
||||
if (state_ == RecordState::kNotStarted) state_ = RecordState::kStarting;
|
||||
}
|
||||
void SetStopping() {
|
||||
if (state_ == RecordState::kRunning) state_ = RecordState::kStopping;
|
||||
}
|
||||
|
||||
void OnRecordStarted() {
|
||||
if (state_ == RecordState::kStarting) state_ = RecordState::kRunning;
|
||||
}
|
||||
void OnRecordStopped() {
|
||||
path_.clear();
|
||||
state_ = RecordState::kNotStarted;
|
||||
}
|
||||
|
||||
std::wstring GetRecordPath() const { return path_; }
|
||||
|
||||
private:
|
||||
enum class RecordState { kNotStarted, kStarting, kRunning, kStopping };
|
||||
|
||||
RecordState state_ = RecordState::kNotStarted;
|
||||
std::wstring path_;
|
||||
ComPtr<IMFCaptureRecordSink> record_sink_;
|
||||
};
|
||||
Reference in New Issue
Block a user