mirror of
https://github.com/cemu-project/Cemu.git
synced 2026-08-22 18:18:12 -05:00
Switch from openpnp-capture to SDL_Camera and make camera support build optional
Had to change because `openpnp-capture` converts all output to RGB (performance problem) doesn't re-enumerate after initialization (usability problem).
This commit is contained in:
3
BUILD.md
3
BUILD.md
@@ -242,11 +242,12 @@ Example usage: `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DENABLE_SDL=ON -
|
||||
|--------------------|:--|-----------------------------------------------------------------------------|---------|--------------------|
|
||||
| ALLOW_PORTABLE | | Allow Cemu to use the `portable` directory to store configs and data | ON | |
|
||||
| CEMU_CXX_FLAGS | | Flags passed straight to the compiler, e.g. `-march=native`, `-Wall`, `/W3` | "" | |
|
||||
| ENABLE_CAMERA | | Enable camera support | ON | |
|
||||
| ENABLE_CUBEB | | Enable cubeb audio backend | ON | |
|
||||
| ENABLE_DISCORD_RPC | | Enable Discord Rich presence support | ON | |
|
||||
| ENABLE_OPENGL | | Enable OpenGL graphics backend | ON | |
|
||||
| ENABLE_HIDAPI | | Enable HIDAPI (used for Wiimote controller API) | ON | |
|
||||
| ENABLE_SDL | | Enable SDLController controller API | ON | |
|
||||
| ENABLE_SDL | | Enable SDLController controller API/SDLCamera implementation | ON | |
|
||||
| ENABLE_VCPKG | | Use VCPKG package manager to obtain dependencies | ON | |
|
||||
| ENABLE_VULKAN | | Enable the Vulkan graphics backend | ON | |
|
||||
| ENABLE_WXWIDGETS | | Enable wxWidgets UI | ON | Currently required |
|
||||
|
||||
@@ -135,6 +135,7 @@ endif()
|
||||
option(ENABLE_HIDAPI "Build with HIDAPI" ON)
|
||||
option(ENABLE_SDL "Enables the SDLController backend" ON)
|
||||
option(ENABLE_LIBUSB "Build with libusb support" ON)
|
||||
option(ENABLE_CAMERA "Build with support for camera access" ON)
|
||||
|
||||
if (ENABLE_LIBUSB)
|
||||
add_compile_definitions(HAS_LIBUSB)
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#include <wx/dcbuffer.h>
|
||||
#include <wx/rawbmp.h>
|
||||
|
||||
struct wxCameraDeviceUniqueId : public wxClientData
|
||||
{
|
||||
std::string id;
|
||||
explicit wxCameraDeviceUniqueId(std::string id) : id(std::move(id)) {}
|
||||
};
|
||||
|
||||
CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent)
|
||||
: wxDialog(parent, wxID_ANY, _("Camera settings"), wxDefaultPosition),
|
||||
m_imageBitmap(CameraManager::CAMERA_WIDTH, CameraManager::CAMERA_HEIGHT, 24),
|
||||
@@ -55,21 +61,35 @@ void CameraSettingsWindow::OnSelectCameraChoice(wxCommandEvent&)
|
||||
if (selection == 0)
|
||||
CameraManager::ResetDevice();
|
||||
else
|
||||
CameraManager::SetDevice(selection - 1);
|
||||
{
|
||||
auto id = static_cast<wxCameraDeviceUniqueId*>(m_cameraComboBox->GetClientData(selection))->id;
|
||||
CameraManager::SetDevice(id);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraSettingsWindow::OnRefreshPressed(wxCommandEvent&)
|
||||
{
|
||||
wxArrayString choices = {_("None")};
|
||||
for (const auto& entry : CameraManager::EnumerateDevices())
|
||||
const auto devices = CameraManager::EnumerateDevices();
|
||||
const auto currentDevice = CameraManager::GetCurrentDevice();
|
||||
auto currentIndex = 0;
|
||||
for (auto i = 0; i < devices.size(); ++i)
|
||||
{
|
||||
choices.push_back(fmt::format("{} ({})", entry.name, entry.uniqueId));
|
||||
auto& device = devices[i];
|
||||
choices.push_back(fmt::format("{} ({})", device.name, device.uniqueId));
|
||||
if (currentDevice && device.uniqueId == *currentDevice)
|
||||
currentIndex = i + 1;
|
||||
}
|
||||
|
||||
m_cameraComboBox->Set(choices);
|
||||
if (auto currentDevice = CameraManager::GetCurrentDevice())
|
||||
m_cameraComboBox->SetSelection(static_cast<int>(*currentDevice) + 1);
|
||||
else
|
||||
m_cameraComboBox->SetSelection(0);
|
||||
m_cameraComboBox->SetSelection(currentIndex);
|
||||
// Client data can only be set when there are entries
|
||||
for (auto i = 0; i < devices.size(); ++i)
|
||||
{
|
||||
auto& device = devices[i];
|
||||
const auto choiceIndex = i + 1;
|
||||
m_cameraComboBox->SetClientData(choiceIndex, new wxCameraDeviceUniqueId{device.uniqueId});
|
||||
}
|
||||
}
|
||||
|
||||
void CameraSettingsWindow::UpdateImage(const wxTimerEvent&)
|
||||
|
||||
@@ -19,10 +19,7 @@ add_library(CemuInput
|
||||
api/Keyboard/KeyboardControllerProvider.cpp
|
||||
api/Keyboard/KeyboardController.cpp
|
||||
api/Keyboard/KeyboardController.h
|
||||
camera/CameraManager.cpp
|
||||
camera/CameraManager.h
|
||||
camera/Rgb2Nv12.cpp
|
||||
camera/Rgb2Nv12.h
|
||||
emulated/ProController.cpp
|
||||
emulated/EmulatedController.h
|
||||
emulated/EmulatedController.cpp
|
||||
@@ -102,12 +99,21 @@ if(ENABLE_SDL)
|
||||
target_link_libraries(CemuInput PRIVATE SDL3::SDL3)
|
||||
endif()
|
||||
|
||||
if (NOT ENABLE_CAMERA)
|
||||
target_sources(CemuInput PRIVATE
|
||||
camera/DummyCameraManager.cpp
|
||||
)
|
||||
elseif (ENABLE_SDL)
|
||||
target_sources(CemuInput PRIVATE
|
||||
camera/SDLCameraManager.cpp
|
||||
)
|
||||
endif ()
|
||||
|
||||
target_include_directories(CemuInput PUBLIC "../")
|
||||
|
||||
target_link_libraries(CemuInput PRIVATE
|
||||
CemuCommon
|
||||
CemuGui
|
||||
openpnp-capture
|
||||
)
|
||||
|
||||
if (ENABLE_BLUEZ)
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
#include "CameraManager.h"
|
||||
|
||||
#include "config/CemuConfig.h"
|
||||
#include "util/helpers/helpers.h"
|
||||
#include "Rgb2Nv12.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include <openpnp-capture.h>
|
||||
|
||||
namespace CameraManager
|
||||
{
|
||||
static std::mutex s_mutex;
|
||||
static std::mutex s_bufferMutex;
|
||||
static CapContext s_ctx;
|
||||
static std::optional<CapDeviceID> s_device;
|
||||
static std::optional<CapStream> s_stream;
|
||||
static uint8_t* s_rgbBufferOut;
|
||||
static uint8_t* s_nv12BufferOut;
|
||||
static int s_refCount = 0;
|
||||
static std::thread s_captureThread;
|
||||
static std::atomic_bool s_capturing = false;
|
||||
static std::atomic_bool s_running = false;
|
||||
|
||||
static std::string FourCC(uint32le value)
|
||||
{
|
||||
return {
|
||||
static_cast<char>((value >> 0) & 0xFF),
|
||||
static_cast<char>((value >> 8) & 0xFF),
|
||||
static_cast<char>((value >> 16) & 0xFF),
|
||||
static_cast<char>((value >> 24) & 0xFF)
|
||||
};
|
||||
}
|
||||
|
||||
static void CaptureLogFunction(uint32_t level, const char* string)
|
||||
{
|
||||
cemuLog_log(LogType::InputAPI, "openpnp-capture: {}: {}", level, string);
|
||||
}
|
||||
|
||||
static std::optional<CapFormatID> FindCorrectFormat()
|
||||
{
|
||||
const auto device = *s_device;
|
||||
cemuLog_log(LogType::InputAPI, "Video capture device '{}' available formats:",
|
||||
Cap_getDeviceName(s_ctx, device));
|
||||
const auto formatCount = Cap_getNumFormats(s_ctx, device);
|
||||
for (int32_t formatId = 0; formatId < formatCount; ++formatId)
|
||||
{
|
||||
CapFormatInfo formatInfo;
|
||||
if (Cap_getFormatInfo(s_ctx, device, formatId, &formatInfo) != CAPRESULT_OK)
|
||||
continue;
|
||||
cemuLog_log(LogType::InputAPI, "{}: {} {}x{} @ {} fps, {} bpp", formatId, FourCC(formatInfo.fourcc),
|
||||
formatInfo.width, formatInfo.height, formatInfo.fps, formatInfo.bpp);
|
||||
if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT)
|
||||
{
|
||||
cemuLog_log(LogType::InputAPI, "Selected video format {}", formatId);
|
||||
return formatId;
|
||||
}
|
||||
}
|
||||
cemuLog_log(LogType::InputAPI, "Failed to find suitable video format");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static void CaptureWorkerFunc()
|
||||
{
|
||||
SetThreadName("CameraManager");
|
||||
auto rgbBuffer = new uint8[CAMERA_RGB_BUFFER_SIZE];
|
||||
auto nv12Buffer = new uint8[CAMERA_NV12_BUFFER_SIZE];
|
||||
while (s_running)
|
||||
{
|
||||
while (s_capturing)
|
||||
{
|
||||
if (s_mutex.try_lock())
|
||||
{
|
||||
if (s_stream && Cap_hasNewFrame(s_ctx, *s_stream) &&
|
||||
Cap_captureFrame(s_ctx, *s_stream, rgbBuffer, CAMERA_RGB_BUFFER_SIZE) == CAPRESULT_OK)
|
||||
{
|
||||
Rgb2Nv12(rgbBuffer, CAMERA_WIDTH, CAMERA_HEIGHT, nv12Buffer, CAMERA_PITCH);
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
std::swap(s_rgbBufferOut, rgbBuffer);
|
||||
std::swap(s_nv12BufferOut, nv12Buffer);
|
||||
}
|
||||
s_mutex.unlock();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
std::this_thread::yield();
|
||||
}
|
||||
delete[] rgbBuffer;
|
||||
delete[] nv12Buffer;
|
||||
}
|
||||
|
||||
static void OpenStream()
|
||||
{
|
||||
const auto formatId = FindCorrectFormat();
|
||||
if (!formatId)
|
||||
return;
|
||||
const auto stream = Cap_openStream(s_ctx, *s_device, *formatId);
|
||||
if (stream == -1)
|
||||
return;
|
||||
s_capturing = true;
|
||||
s_stream = stream;
|
||||
}
|
||||
|
||||
static void CloseStream()
|
||||
{
|
||||
s_capturing = false;
|
||||
if (s_stream)
|
||||
{
|
||||
Cap_closeStream(s_ctx, *s_stream);
|
||||
s_stream = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static void ResetBuffers()
|
||||
{
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
std::fill_n(s_rgbBufferOut, CAMERA_RGB_BUFFER_SIZE, 0);
|
||||
constexpr static auto PIXEL_COUNT = CAMERA_HEIGHT * CAMERA_PITCH;
|
||||
std::ranges::fill_n(s_nv12BufferOut, PIXEL_COUNT, 16);
|
||||
std::ranges::fill_n(s_nv12BufferOut + PIXEL_COUNT, (PIXEL_COUNT / 2), 128);
|
||||
}
|
||||
|
||||
static std::vector<DeviceInfo> InternalEnumerateDevices()
|
||||
{
|
||||
std::vector<DeviceInfo> infos;
|
||||
const auto deviceCount = Cap_getDeviceCount(s_ctx);
|
||||
cemuLog_log(LogType::InputAPI, "Available video capture devices:");
|
||||
for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo)
|
||||
{
|
||||
const auto uniqueId = Cap_getDeviceUniqueID(s_ctx, deviceNo);
|
||||
const auto name = Cap_getDeviceName(s_ctx, deviceNo);
|
||||
DeviceInfo info;
|
||||
info.uniqueId = uniqueId;
|
||||
info.name = name ? name : "";
|
||||
infos.push_back(info);
|
||||
cemuLog_log(LogType::InputAPI, "{}", info.name);
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
static void Init()
|
||||
{
|
||||
s_running = true;
|
||||
s_ctx = Cap_createContext();
|
||||
Cap_setLogLevel(4);
|
||||
Cap_installCustomLogFunction(CaptureLogFunction);
|
||||
s_rgbBufferOut = new uint8[CAMERA_RGB_BUFFER_SIZE];
|
||||
s_nv12BufferOut = new uint8[CAMERA_NV12_BUFFER_SIZE];
|
||||
|
||||
s_captureThread = std::thread(&CaptureWorkerFunc);
|
||||
|
||||
const auto deviceName = GetConfig().camera_id.GetValue();
|
||||
if (!deviceName.empty())
|
||||
{
|
||||
const auto devices = InternalEnumerateDevices();
|
||||
for (CapDeviceID deviceId = 0; deviceId < devices.size(); ++deviceId)
|
||||
{
|
||||
if (devices[deviceId].name == deviceName)
|
||||
{
|
||||
s_device = deviceId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ResetBuffers();
|
||||
}
|
||||
|
||||
static void Deinit()
|
||||
{
|
||||
CloseStream();
|
||||
Cap_releaseContext(s_ctx);
|
||||
s_running = false;
|
||||
s_captureThread.join();
|
||||
delete[] s_rgbBufferOut;
|
||||
delete[] s_nv12BufferOut;
|
||||
}
|
||||
|
||||
void FillNV12Buffer(std::span<uint8, CAMERA_NV12_BUFFER_SIZE> nv12Buffer)
|
||||
{
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
std::ranges::copy_n(s_nv12BufferOut, CAMERA_NV12_BUFFER_SIZE, nv12Buffer.data());
|
||||
}
|
||||
|
||||
void FillRGBBuffer(std::span<uint8, CAMERA_RGB_BUFFER_SIZE> rgbBuffer)
|
||||
{
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
std::ranges::copy_n(s_rgbBufferOut, CAMERA_RGB_BUFFER_SIZE, rgbBuffer.data());
|
||||
}
|
||||
|
||||
void SetDevice(uint32 deviceNo)
|
||||
{
|
||||
std::scoped_lock lock(s_mutex);
|
||||
CloseStream();
|
||||
s_device = deviceNo;
|
||||
if (s_refCount != 0)
|
||||
OpenStream();
|
||||
}
|
||||
|
||||
void ResetDevice()
|
||||
{
|
||||
std::scoped_lock lock(s_mutex);
|
||||
CloseStream();
|
||||
s_device = std::nullopt;
|
||||
ResetBuffers();
|
||||
}
|
||||
|
||||
void Open()
|
||||
{
|
||||
std::scoped_lock lock(s_mutex);
|
||||
if (!s_running)
|
||||
{
|
||||
Init();
|
||||
}
|
||||
if (s_device && s_refCount == 0)
|
||||
{
|
||||
OpenStream();
|
||||
}
|
||||
s_refCount += 1;
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
std::scoped_lock lock(s_mutex);
|
||||
if (s_refCount == 0)
|
||||
return;
|
||||
s_refCount -= 1;
|
||||
if (s_refCount != 0)
|
||||
return;
|
||||
CloseStream();
|
||||
Deinit();
|
||||
}
|
||||
|
||||
std::vector<DeviceInfo> EnumerateDevices()
|
||||
{
|
||||
std::scoped_lock lock(s_mutex);
|
||||
return InternalEnumerateDevices();
|
||||
}
|
||||
|
||||
void SaveDevice()
|
||||
{
|
||||
std::scoped_lock lock(s_mutex);
|
||||
const std::string cameraId = s_device ? Cap_getDeviceName(s_ctx, *s_device) : "";
|
||||
GetConfig().camera_id.SetValue(cameraId);
|
||||
GetConfigHandle().Save();
|
||||
}
|
||||
|
||||
std::optional<uint32> GetCurrentDevice()
|
||||
{
|
||||
return s_device;
|
||||
}
|
||||
} // namespace CameraManager
|
||||
@@ -21,9 +21,9 @@ namespace CameraManager
|
||||
void FillNV12Buffer(std::span<uint8, CAMERA_NV12_BUFFER_SIZE> nv12Buffer);
|
||||
void FillRGBBuffer(std::span<uint8, CAMERA_RGB_BUFFER_SIZE> rgbBuffer);
|
||||
|
||||
void SetDevice(uint32 deviceNo);
|
||||
void SetDevice(std::string_view deviceId);
|
||||
void ResetDevice();
|
||||
std::vector<DeviceInfo> EnumerateDevices();
|
||||
void SaveDevice();
|
||||
std::optional<uint32> GetCurrentDevice();
|
||||
std::optional<std::string> GetCurrentDevice();
|
||||
} // namespace CameraManager
|
||||
|
||||
46
src/input/camera/DummyCameraManager.cpp
Normal file
46
src/input/camera/DummyCameraManager.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "CameraManager.h"
|
||||
|
||||
namespace CameraManager
|
||||
{
|
||||
void FillNV12Buffer(std::span<uint8, CAMERA_NV12_BUFFER_SIZE> nv12Buffer)
|
||||
{
|
||||
constexpr static auto PIXEL_COUNT = CAMERA_HEIGHT * CAMERA_PITCH;
|
||||
std::ranges::fill_n(nv12Buffer.data(), PIXEL_COUNT, 16);
|
||||
std::ranges::fill_n(nv12Buffer.data() + PIXEL_COUNT, (PIXEL_COUNT / 2), 128);
|
||||
}
|
||||
|
||||
void FillRGBBuffer(std::span<uint8, CAMERA_RGB_BUFFER_SIZE> rgbBuffer)
|
||||
{
|
||||
std::ranges::fill(rgbBuffer, 0);
|
||||
}
|
||||
|
||||
void SetDevice(std::string_view deviceId)
|
||||
{
|
||||
}
|
||||
|
||||
void ResetDevice()
|
||||
{
|
||||
}
|
||||
|
||||
void Open()
|
||||
{
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<DeviceInfo> EnumerateDevices()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void SaveDevice()
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<std::string> GetCurrentDevice()
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace CameraManager
|
||||
@@ -1,65 +0,0 @@
|
||||
// Based on https://github.com/cohenrotem/Rgb2NV12
|
||||
#include "Rgb2Nv12.h"
|
||||
|
||||
constexpr static glm::mat3x3 COEFFICIENT_MATRIX =
|
||||
{
|
||||
+0.257f, -0.148f, 0.439f,
|
||||
+0.504f, -0.291f, -0.368f,
|
||||
+0.098f, +0.439f, -0.071f};
|
||||
|
||||
constexpr static glm::mat4x3 OFFSET_MATRIX = {
|
||||
16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f,
|
||||
16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f,
|
||||
16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f,
|
||||
16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f};
|
||||
|
||||
static void Rgb2Nv12TwoRows(const uint8* topLine,
|
||||
const uint8* bottomLine,
|
||||
unsigned imageWidth,
|
||||
uint8* topLineY,
|
||||
uint8* bottomLineY,
|
||||
uint8* uv)
|
||||
{
|
||||
auto* topIn = reinterpret_cast<const glm::u8vec3*>(topLine);
|
||||
auto* botIn = reinterpret_cast<const glm::u8vec3*>(bottomLine);
|
||||
|
||||
for (auto x = 0u; x < imageWidth; x += 2)
|
||||
{
|
||||
const glm::mat4x3 rgbMatrix{
|
||||
topIn[x],
|
||||
topIn[x + 1],
|
||||
botIn[x],
|
||||
botIn[x + 1],
|
||||
};
|
||||
|
||||
const auto result = COEFFICIENT_MATRIX * rgbMatrix + OFFSET_MATRIX;
|
||||
|
||||
topLineY[x + 0] = result[0].s;
|
||||
topLineY[x + 1] = result[1].s;
|
||||
bottomLineY[x + 0] = result[2].s;
|
||||
bottomLineY[x + 1] = result[3].s;
|
||||
|
||||
uv[x + 0] = (result[0].t + result[1].t + result[2].t + result[3].t) * 0.25f;
|
||||
uv[x + 1] = (result[0].p + result[1].p + result[2].p + result[3].p) * 0.25f;
|
||||
}
|
||||
}
|
||||
|
||||
void Rgb2Nv12(const uint8* rgbImage,
|
||||
unsigned imageWidth,
|
||||
unsigned imageHeight,
|
||||
uint8* outNv12Image,
|
||||
unsigned nv12Pitch)
|
||||
{
|
||||
cemu_assert_debug(!((imageWidth | imageHeight) & 1));
|
||||
unsigned char* UV = outNv12Image + nv12Pitch * imageHeight;
|
||||
|
||||
for (auto row = 0u; row < imageHeight; row += 2)
|
||||
{
|
||||
Rgb2Nv12TwoRows(&rgbImage[row * imageWidth * 3],
|
||||
&rgbImage[(row + 1) * imageWidth * 3],
|
||||
imageWidth,
|
||||
&outNv12Image[row * nv12Pitch],
|
||||
&outNv12Image[(row + 1) * nv12Pitch],
|
||||
&UV[(row / 2) * nv12Pitch]);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void Rgb2Nv12(const uint8* rgbImage,
|
||||
unsigned imageWidth,
|
||||
unsigned imageHeight,
|
||||
uint8* outNv12Image,
|
||||
unsigned nv12Pitch);
|
||||
278
src/input/camera/SDLCameraManager.cpp
Normal file
278
src/input/camera/SDLCameraManager.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#include "CameraManager.h"
|
||||
|
||||
#include "config/CemuConfig.h"
|
||||
#include "util/helpers/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include <SDL3/SDL_camera.h>
|
||||
#include <SDL3/SDL_init.h>
|
||||
|
||||
namespace CameraManager
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct InternalDeviceInfo
|
||||
{
|
||||
SDL_CameraID id;
|
||||
std::string name;
|
||||
};
|
||||
std::mutex s_deviceMutex;
|
||||
std::mutex s_bufferMutex;
|
||||
std::optional<SDL_CameraID> s_deviceId;
|
||||
SDL_Camera* s_camera;
|
||||
std::unique_ptr<uint8[]> s_rgbBufferOut;
|
||||
std::unique_ptr<uint8[]> s_nv12BufferOut;
|
||||
int s_refCount = 0;
|
||||
std::thread s_captureThread;
|
||||
std::atomic_bool s_capturing = false;
|
||||
std::atomic_bool s_running = false;
|
||||
}
|
||||
|
||||
static void CaptureWorkerFunc()
|
||||
{
|
||||
SetThreadName("CameraManager");
|
||||
auto nv12Buffer = std::unique_ptr<uint8[]>(new uint8[CAMERA_NV12_BUFFER_SIZE]);
|
||||
while (s_running)
|
||||
{
|
||||
while (s_capturing)
|
||||
{
|
||||
if (auto deviceLock = std::unique_lock(s_deviceMutex, std::try_to_lock); deviceLock && s_camera)
|
||||
{
|
||||
if (auto frame = SDL_AcquireCameraFrame(s_camera, nullptr))
|
||||
{
|
||||
if (frame->format != SDL_PIXELFORMAT_NV12)
|
||||
return;
|
||||
if (SDL_MUSTLOCK(frame))
|
||||
SDL_LockSurface(frame);
|
||||
const auto byteRowCount = (frame->h * 3) >> 1;
|
||||
for (auto row = 0; row < byteRowCount; ++row)
|
||||
{
|
||||
const auto lineIn = static_cast<const uint8*>(frame->pixels) + frame->pitch * row;
|
||||
const auto lineOut = nv12Buffer.get() + CAMERA_PITCH * row;
|
||||
std::memcpy(lineOut, lineIn, frame->w);
|
||||
}
|
||||
if (SDL_MUSTLOCK(frame))
|
||||
SDL_UnlockSurface(frame);
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
std::swap(s_nv12BufferOut, nv12Buffer);
|
||||
SDL_ReleaseCameraFrame(s_camera, frame);
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
static void OpenStream()
|
||||
{
|
||||
SDL_CameraSpec cameraSpec;
|
||||
cameraSpec.format = SDL_PIXELFORMAT_NV12;
|
||||
cameraSpec.colorspace = SDL_COLORSPACE_BT601_LIMITED;
|
||||
cameraSpec.framerate_numerator = 30;
|
||||
cameraSpec.framerate_denominator = 1;
|
||||
cameraSpec.width = CAMERA_WIDTH;
|
||||
cameraSpec.height = CAMERA_HEIGHT;
|
||||
const auto camera = SDL_OpenCamera(*s_deviceId, &cameraSpec);
|
||||
if (camera == nullptr)
|
||||
return;
|
||||
|
||||
if (SDL_GetCameraFormat(camera, &cameraSpec) && cameraSpec.format != SDL_PIXELFORMAT_NV12)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Camera output format is NV12");
|
||||
SDL_CloseCamera(camera);
|
||||
return;
|
||||
}
|
||||
s_capturing = true;
|
||||
s_camera = camera;
|
||||
}
|
||||
|
||||
static void CloseStream()
|
||||
{
|
||||
s_capturing = false;
|
||||
if (s_camera)
|
||||
{
|
||||
SDL_CloseCamera(s_camera);
|
||||
s_camera = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void ResetBuffers()
|
||||
{
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
std::fill_n(s_rgbBufferOut.get(), CAMERA_RGB_BUFFER_SIZE, 0);
|
||||
constexpr static auto PIXEL_COUNT = CAMERA_HEIGHT * CAMERA_PITCH;
|
||||
std::ranges::fill_n(s_nv12BufferOut.get(), PIXEL_COUNT, 16);
|
||||
std::ranges::fill_n(s_nv12BufferOut.get() + PIXEL_COUNT, (PIXEL_COUNT / 2), 128);
|
||||
}
|
||||
|
||||
static std::vector<InternalDeviceInfo> InternalEnumerateDevices()
|
||||
{
|
||||
std::vector<InternalDeviceInfo> infos;
|
||||
int deviceCount = 0;
|
||||
auto devices = SDL_GetCameras(&deviceCount);
|
||||
if (devices == nullptr)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "{}", SDL_GetError());
|
||||
return {};
|
||||
}
|
||||
cemuLog_log(LogType::InputAPI, "Available video capture devices:");
|
||||
for (auto cameraId : std::span(devices, deviceCount))
|
||||
{
|
||||
const auto name = SDL_GetCameraName(cameraId);
|
||||
infos.emplace_back(cameraId, name ? name : "");
|
||||
}
|
||||
SDL_free(devices);
|
||||
return infos;
|
||||
}
|
||||
|
||||
static void Init()
|
||||
{
|
||||
SDL_InitSubSystem(SDL_INIT_CAMERA);
|
||||
s_running = true;
|
||||
s_rgbBufferOut.reset(new uint8[CAMERA_RGB_BUFFER_SIZE]);
|
||||
s_nv12BufferOut.reset(new uint8[CAMERA_NV12_BUFFER_SIZE]);
|
||||
|
||||
s_captureThread = std::thread(&CaptureWorkerFunc);
|
||||
|
||||
const auto deviceName = GetConfig().camera_id.GetValue();
|
||||
if (!deviceName.empty())
|
||||
{
|
||||
for (auto device : InternalEnumerateDevices())
|
||||
{
|
||||
if (device.name == deviceName)
|
||||
{
|
||||
s_deviceId = device.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
ResetBuffers();
|
||||
}
|
||||
|
||||
static void Deinit()
|
||||
{
|
||||
CloseStream();
|
||||
s_running = false;
|
||||
s_captureThread.join();
|
||||
s_nv12BufferOut.reset();
|
||||
s_rgbBufferOut.reset();
|
||||
SDL_QuitSubSystem(SDL_INIT_CAMERA);
|
||||
}
|
||||
|
||||
void FillNV12Buffer(std::span<uint8, CAMERA_NV12_BUFFER_SIZE> nv12Buffer)
|
||||
{
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
std::ranges::copy_n(s_nv12BufferOut.get(), CAMERA_NV12_BUFFER_SIZE, nv12Buffer.data());
|
||||
}
|
||||
|
||||
static uint8 ClampU8(int v)
|
||||
{
|
||||
v = (v > 255) ? 255 : v;
|
||||
v = (v < 0) ? 0 : v;
|
||||
return v;
|
||||
}
|
||||
|
||||
void FillRGBBuffer(std::span<uint8, CAMERA_RGB_BUFFER_SIZE> rgbBuffer)
|
||||
{
|
||||
std::scoped_lock lock(s_bufferMutex);
|
||||
const auto* yPtr = s_nv12BufferOut.get();
|
||||
const auto* uvPtr = s_nv12BufferOut.get() + CAMERA_PITCH * CAMERA_HEIGHT;
|
||||
auto* rgbPtr = rgbBuffer.data();
|
||||
for (auto row = 0; row < CAMERA_HEIGHT; ++row)
|
||||
{
|
||||
const auto yRow = yPtr + row * CAMERA_PITCH;
|
||||
const auto uvRow = uvPtr + (row >> 1) * CAMERA_PITCH;
|
||||
const auto rgbRow = rgbPtr + row * CAMERA_WIDTH * 3;
|
||||
for (auto col = 0; col < CAMERA_WIDTH; ++col)
|
||||
{
|
||||
const auto _y = 19 * (yRow[col] - 16);
|
||||
const auto uvCol = col >> 1;
|
||||
const auto u = uvRow[(uvCol << 1) + 0];
|
||||
const auto v = uvRow[(uvCol << 1) + 1];
|
||||
rgbRow[col * 3 + 0] = ClampU8((_y + 26 * (v - 128)) >> 4);
|
||||
rgbRow[col * 3 + 1] = ClampU8((_y - 13 * (v - 128) - 6 * (u - 128)) >> 4);
|
||||
rgbRow[col * 3 + 2] = ClampU8((_y + 32 * (u - 128)) >> 4);;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetDevice(std::string_view deviceId)
|
||||
{
|
||||
std::scoped_lock lock(s_deviceMutex);
|
||||
CloseStream();
|
||||
SDL_CameraID id;
|
||||
auto [it, ec] = std::from_chars(deviceId.begin(), deviceId.end(), id);
|
||||
if (ec != std::errc{})
|
||||
return;
|
||||
s_deviceId = id;
|
||||
if (s_refCount != 0)
|
||||
OpenStream();
|
||||
}
|
||||
|
||||
void ResetDevice()
|
||||
{
|
||||
std::scoped_lock lock(s_deviceMutex);
|
||||
CloseStream();
|
||||
s_deviceId = std::nullopt;
|
||||
ResetBuffers();
|
||||
}
|
||||
|
||||
|
||||
void Open()
|
||||
{
|
||||
std::scoped_lock lock(s_deviceMutex);
|
||||
if (!s_running)
|
||||
{
|
||||
Init();
|
||||
}
|
||||
if (s_deviceId && s_refCount == 0)
|
||||
{
|
||||
OpenStream();
|
||||
}
|
||||
s_refCount += 1;
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
std::scoped_lock lock(s_deviceMutex);
|
||||
if (s_refCount == 0)
|
||||
return;
|
||||
s_refCount -= 1;
|
||||
if (s_refCount != 0)
|
||||
return;
|
||||
CloseStream();
|
||||
Deinit();
|
||||
}
|
||||
|
||||
std::vector<DeviceInfo> EnumerateDevices()
|
||||
{
|
||||
std::scoped_lock lock(s_deviceMutex);
|
||||
std::vector<DeviceInfo> deviceInfos;
|
||||
for (const auto& [id, name] : InternalEnumerateDevices())
|
||||
{
|
||||
deviceInfos.emplace_back(fmt::to_string(id), name);
|
||||
}
|
||||
return deviceInfos;
|
||||
}
|
||||
|
||||
void SaveDevice()
|
||||
{
|
||||
std::scoped_lock lock(s_deviceMutex);
|
||||
const std::string cameraId = s_deviceId ? SDL_GetCameraName(*s_deviceId) : "";
|
||||
GetConfig().camera_id.SetValue(cameraId);
|
||||
GetConfigHandle().Save();
|
||||
}
|
||||
|
||||
std::optional<std::string> GetCurrentDevice()
|
||||
{
|
||||
if (s_deviceId)
|
||||
return std::to_string(*s_deviceId);
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace CameraManager
|
||||
Reference in New Issue
Block a user