add plugin camera desktop

This commit is contained in:
kyo
2026-06-22 09:39:31 +07:00
parent 92daeef0c5
commit 5407afbd6e
144 changed files with 15900 additions and 1 deletions

View 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
)

File diff suppressed because it is too large Load Diff

View 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_;
};

View 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));
}

View 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_;
};

View 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();
}
}

View 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_{};
};

View 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);
}

View 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);
};

View 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"

View File

@@ -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

View 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();
}

View 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;
}

View 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);
};

View 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;
}

View 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_;
};