mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
@@ -34,7 +34,9 @@ fn build_common(builder: &mut Build) {
|
||||
// system
|
||||
#[cfg(windows)]
|
||||
{
|
||||
["d3d11", "dxgi"].map(|lib| println!("cargo:rustc-link-lib={}", lib));
|
||||
for lib in ["d3d11", "dxgi"] {
|
||||
println!("cargo:rustc-link-lib={}", lib);
|
||||
}
|
||||
}
|
||||
|
||||
builder.include(&common_dir);
|
||||
@@ -99,6 +101,7 @@ mod ffmpeg {
|
||||
link_os();
|
||||
build_ffmpeg_ram(builder);
|
||||
build_ffmpeg_hw(builder);
|
||||
build_ffmpeg_capture(builder);
|
||||
}
|
||||
|
||||
/// Link system FFmpeg using pkg-config or custom path
|
||||
@@ -282,15 +285,24 @@ mod ffmpeg {
|
||||
)
|
||||
);
|
||||
{
|
||||
// Only need avcodec and avutil for encoding
|
||||
// avdevice/avformat are needed by the Windows DirectShow capture bridge.
|
||||
let mut static_libs = vec!["avcodec", "avutil"];
|
||||
if target_os == "windows" {
|
||||
static_libs.push("libmfx");
|
||||
static_libs.extend([
|
||||
"avformat",
|
||||
"avdevice",
|
||||
"avfilter",
|
||||
"swresample",
|
||||
"swscale",
|
||||
"vpx",
|
||||
"libx264",
|
||||
"x265-static",
|
||||
"libmfx",
|
||||
]);
|
||||
}
|
||||
for lib in static_libs {
|
||||
println!("cargo:rustc-link-lib=static={}", lib);
|
||||
}
|
||||
static_libs
|
||||
.iter()
|
||||
.map(|lib| println!("cargo:rustc-link-lib=static={}", lib))
|
||||
.count();
|
||||
}
|
||||
|
||||
let include = path.join("include");
|
||||
@@ -304,7 +316,10 @@ mod ffmpeg {
|
||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||
|
||||
let dyn_libs: Vec<&str> = if target_os == "windows" {
|
||||
["User32", "bcrypt", "ole32", "advapi32"].to_vec()
|
||||
[
|
||||
"User32", "bcrypt", "ole32", "advapi32", "mfuuid", "strmiids",
|
||||
]
|
||||
.to_vec()
|
||||
} else if target_os == "linux" {
|
||||
// Base libraries for all Linux platforms
|
||||
let mut v = vec!["drm", "stdc++"];
|
||||
@@ -375,6 +390,34 @@ mod ffmpeg {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ffmpeg_capture(builder: &mut Build) {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if target_os != "windows" {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let capture_header = manifest_dir
|
||||
.join("cpp")
|
||||
.join("ffmpeg_capture_ffi.h")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
bindgen::builder()
|
||||
.header(capture_header)
|
||||
.rustified_enum("*")
|
||||
.generate()
|
||||
.unwrap()
|
||||
.write_to_file(
|
||||
Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_capture_ffi.rs"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
builder.file(manifest_dir.join("cpp").join("ffmpeg_capture.cpp"));
|
||||
println!("cargo:rustc-link-lib=strmiids");
|
||||
println!("cargo:rustc-link-lib=oleaut32");
|
||||
println!("cargo:rustc-link-lib=quartz");
|
||||
}
|
||||
|
||||
fn build_ffmpeg_hw(builder: &mut Build) {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/opt.h>
|
||||
}
|
||||
|
||||
@@ -99,13 +100,12 @@ void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
||||
c->color_primaries = AVCOL_PRI_SMPTE170M;
|
||||
c->color_trc = AVCOL_TRC_SMPTE170M;
|
||||
|
||||
// Profile selection: use BASELINE for software H264 (faster, simpler)
|
||||
if (is_software_h264(name)) {
|
||||
c->profile = FF_PROFILE_H264_BASELINE; // Simpler profile for real-time
|
||||
} else if (name.find("h264") != std::string::npos) {
|
||||
c->profile = FF_PROFILE_H264_HIGH;
|
||||
// WebRTC SDP advertises constrained baseline. Keep hardware and software
|
||||
// encoders on the same browser-friendly H264 profile.
|
||||
if (name.find("h264") != std::string::npos) {
|
||||
c->profile = AV_PROFILE_H264_CONSTRAINED_BASELINE;
|
||||
} else if (name.find("hevc") != std::string::npos) {
|
||||
c->profile = FF_PROFILE_HEVC_MAIN;
|
||||
c->profile = AV_PROFILE_HEVC_MAIN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +120,7 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
|
||||
}
|
||||
if (name.find("amf") != std::string::npos) {
|
||||
if ((ret = av_opt_set(priv_data, "query_timeout", "1000", 0)) < 0) {
|
||||
LOG_ERROR(std::string("amf set_lantency_free failed, ret = ") + av_err2str(ret));
|
||||
return false;
|
||||
LOG_WARN(std::string("amf query_timeout option is unavailable, ret = ") + av_err2str(ret));
|
||||
}
|
||||
}
|
||||
if (name.find("qsv") != std::string::npos) {
|
||||
|
||||
879
libs/hwcodec/cpp/ffmpeg_capture.cpp
Normal file
879
libs/hwcodec/cpp/ffmpeg_capture.cpp
Normal file
@@ -0,0 +1,879 @@
|
||||
#define NOMINMAX
|
||||
#include "ffmpeg_capture_ffi.h"
|
||||
|
||||
#include <Windows.h>
|
||||
#include <dshow.h>
|
||||
#include <dvdmedia.h>
|
||||
extern "C" {
|
||||
#include <libavcodec/codec_id.h>
|
||||
#include <libavdevice/avdevice.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavutil/error.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
}
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
#pragma comment(lib, "strmiids")
|
||||
|
||||
thread_local std::string g_last_error;
|
||||
|
||||
struct HwcodecDshowCaptureContext {
|
||||
AVFormatContext* format_ctx = nullptr;
|
||||
int stream_index = -1;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int pixel_format = HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||
int stride = 0;
|
||||
int timeout_ms = 2000;
|
||||
std::atomic<long long> deadline_ms{0};
|
||||
std::atomic<int> timed_out{0};
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
namespace {
|
||||
struct DshowCapabilityEntry {
|
||||
std::string format;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<int> fps;
|
||||
};
|
||||
|
||||
const char* requested_pixel_format_name(int requested_format);
|
||||
|
||||
void set_last_error(const std::string& message) {
|
||||
g_last_error = message;
|
||||
}
|
||||
|
||||
std::string ffmpeg_error(int errnum) {
|
||||
char buffer[AV_ERROR_MAX_STRING_SIZE] = {0};
|
||||
av_make_error_string(buffer, sizeof(buffer), errnum);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
long long now_ms() {
|
||||
return static_cast<long long>(GetTickCount64());
|
||||
}
|
||||
|
||||
std::string wide_to_utf8(const wchar_t* value) {
|
||||
if (!value) {
|
||||
return std::string();
|
||||
}
|
||||
int size = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (size <= 1) {
|
||||
return std::string();
|
||||
}
|
||||
std::string result(static_cast<size_t>(size - 1), '\0');
|
||||
WideCharToMultiByte(
|
||||
CP_UTF8,
|
||||
0,
|
||||
value,
|
||||
-1,
|
||||
result.empty() ? nullptr : &result[0],
|
||||
size,
|
||||
nullptr,
|
||||
nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
void add_fps_candidate(std::vector<int>* fps, LONGLONG interval_100ns) {
|
||||
if (!fps || interval_100ns <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
double fps_value = 10000000.0 / static_cast<double>(interval_100ns);
|
||||
int rounded = static_cast<int>(fps_value + 0.5);
|
||||
if (rounded <= 0) {
|
||||
return;
|
||||
}
|
||||
if (std::find(fps->begin(), fps->end(), rounded) == fps->end()) {
|
||||
fps->push_back(rounded);
|
||||
}
|
||||
}
|
||||
|
||||
void normalize_fps(std::vector<int>* fps) {
|
||||
if (!fps) {
|
||||
return;
|
||||
}
|
||||
std::sort(fps->begin(), fps->end(), std::greater<int>());
|
||||
fps->erase(std::unique(fps->begin(), fps->end()), fps->end());
|
||||
}
|
||||
|
||||
const char* media_subtype_to_format(const GUID& subtype) {
|
||||
if (subtype == MEDIASUBTYPE_MJPG) {
|
||||
return "MJPEG";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_YUY2) {
|
||||
return "YUYV";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_UYVY) {
|
||||
return "UYVY";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_YVYU) {
|
||||
return "YVYU";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_NV12) {
|
||||
return "NV12";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_RGB24) {
|
||||
return "RGB24";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_RGB32) {
|
||||
return "BGR24";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_IYUV) {
|
||||
return "YUV420";
|
||||
}
|
||||
if (subtype == MEDIASUBTYPE_YV12) {
|
||||
return "YVU420";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void free_media_type(AM_MEDIA_TYPE* media_type) {
|
||||
if (!media_type) {
|
||||
return;
|
||||
}
|
||||
if (media_type->cbFormat != 0) {
|
||||
CoTaskMemFree(media_type->pbFormat);
|
||||
media_type->cbFormat = 0;
|
||||
media_type->pbFormat = nullptr;
|
||||
}
|
||||
if (media_type->pUnk != nullptr) {
|
||||
media_type->pUnk->Release();
|
||||
media_type->pUnk = nullptr;
|
||||
}
|
||||
CoTaskMemFree(media_type);
|
||||
}
|
||||
|
||||
bool fill_capability_entry(
|
||||
const AM_MEDIA_TYPE* media_type,
|
||||
const VIDEO_STREAM_CONFIG_CAPS* caps,
|
||||
DshowCapabilityEntry* out_entry) {
|
||||
if (!media_type || !out_entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* format = media_subtype_to_format(media_type->subtype);
|
||||
if (!format) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LONG width = 0;
|
||||
LONG height = 0;
|
||||
REFERENCE_TIME avg_time_per_frame = 0;
|
||||
|
||||
if (media_type->formattype == FORMAT_VideoInfo && media_type->pbFormat &&
|
||||
media_type->cbFormat >= sizeof(VIDEOINFOHEADER)) {
|
||||
const auto* info = reinterpret_cast<const VIDEOINFOHEADER*>(media_type->pbFormat);
|
||||
width = info->bmiHeader.biWidth;
|
||||
height = std::abs(info->bmiHeader.biHeight);
|
||||
avg_time_per_frame = info->AvgTimePerFrame;
|
||||
} else if (media_type->formattype == FORMAT_VideoInfo2 && media_type->pbFormat &&
|
||||
media_type->cbFormat >= sizeof(VIDEOINFOHEADER2)) {
|
||||
const auto* info = reinterpret_cast<const VIDEOINFOHEADER2*>(media_type->pbFormat);
|
||||
width = info->bmiHeader.biWidth;
|
||||
height = std::abs(info->bmiHeader.biHeight);
|
||||
avg_time_per_frame = info->AvgTimePerFrame;
|
||||
}
|
||||
|
||||
if ((width <= 0 || height <= 0) && caps) {
|
||||
width = std::max<LONG>(caps->InputSize.cx, caps->MinOutputSize.cx);
|
||||
height = std::max<LONG>(caps->InputSize.cy, caps->MinOutputSize.cy);
|
||||
if (width <= 0 || height <= 0) {
|
||||
width = caps->MaxOutputSize.cx;
|
||||
height = caps->MaxOutputSize.cy;
|
||||
}
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out_entry->format = format;
|
||||
out_entry->width = static_cast<int>(width);
|
||||
out_entry->height = static_cast<int>(height);
|
||||
out_entry->fps.clear();
|
||||
|
||||
add_fps_candidate(&out_entry->fps, avg_time_per_frame);
|
||||
if (caps) {
|
||||
add_fps_candidate(&out_entry->fps, caps->MinFrameInterval);
|
||||
add_fps_candidate(&out_entry->fps, caps->MaxFrameInterval);
|
||||
}
|
||||
normalize_fps(&out_entry->fps);
|
||||
return true;
|
||||
}
|
||||
|
||||
void append_stream_capabilities(IAMStreamConfig* stream_config, std::vector<DshowCapabilityEntry>* entries) {
|
||||
if (!stream_config || !entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
int cap_count = 0;
|
||||
int cap_size = 0;
|
||||
HRESULT hr = stream_config->GetNumberOfCapabilities(&cap_count, &cap_size);
|
||||
if (FAILED(hr) || cap_count <= 0 || cap_size < static_cast<int>(sizeof(VIDEO_STREAM_CONFIG_CAPS))) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<BYTE> caps_buffer(static_cast<size_t>(cap_size));
|
||||
for (int index = 0; index < cap_count; ++index) {
|
||||
AM_MEDIA_TYPE* media_type = nullptr;
|
||||
hr = stream_config->GetStreamCaps(index, &media_type, caps_buffer.data());
|
||||
if (FAILED(hr) || !media_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DshowCapabilityEntry entry;
|
||||
const auto* caps = reinterpret_cast<const VIDEO_STREAM_CONFIG_CAPS*>(caps_buffer.data());
|
||||
if (fill_capability_entry(media_type, caps, &entry)) {
|
||||
entries->push_back(std::move(entry));
|
||||
}
|
||||
|
||||
free_media_type(media_type);
|
||||
}
|
||||
}
|
||||
|
||||
bool find_device_filter(const std::string& device_name, IBaseFilter** out_filter) {
|
||||
if (!out_filter) {
|
||||
return false;
|
||||
}
|
||||
*out_filter = nullptr;
|
||||
|
||||
ICreateDevEnum* dev_enum = nullptr;
|
||||
IEnumMoniker* enum_moniker = nullptr;
|
||||
HRESULT hr = CoCreateInstance(
|
||||
CLSID_SystemDeviceEnum,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_ICreateDevEnum,
|
||||
reinterpret_cast<void**>(&dev_enum));
|
||||
if (FAILED(hr) || !dev_enum) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = dev_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0);
|
||||
dev_enum->Release();
|
||||
if (hr != S_OK || !enum_moniker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
IMoniker* moniker = nullptr;
|
||||
ULONG fetched = 0;
|
||||
while (!found && enum_moniker->Next(1, &moniker, &fetched) == S_OK) {
|
||||
IPropertyBag* bag = nullptr;
|
||||
hr = moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, reinterpret_cast<void**>(&bag));
|
||||
if (SUCCEEDED(hr) && bag) {
|
||||
VARIANT name;
|
||||
VariantInit(&name);
|
||||
if (SUCCEEDED(bag->Read(L"FriendlyName", &name, nullptr)) && name.vt == VT_BSTR) {
|
||||
auto utf8_name = wide_to_utf8(name.bstrVal);
|
||||
if (utf8_name == device_name) {
|
||||
hr = moniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, reinterpret_cast<void**>(out_filter));
|
||||
found = SUCCEEDED(hr) && *out_filter != nullptr;
|
||||
}
|
||||
}
|
||||
VariantClear(&name);
|
||||
bag->Release();
|
||||
}
|
||||
moniker->Release();
|
||||
}
|
||||
enum_moniker->Release();
|
||||
return found;
|
||||
}
|
||||
|
||||
std::string build_capabilities_payload(const std::vector<DshowCapabilityEntry>& entries) {
|
||||
std::string payload;
|
||||
for (size_t i = 0; i < entries.size(); ++i) {
|
||||
const auto& entry = entries[i];
|
||||
payload += entry.format;
|
||||
payload.push_back('|');
|
||||
payload += std::to_string(entry.width);
|
||||
payload.push_back('|');
|
||||
payload += std::to_string(entry.height);
|
||||
payload.push_back('|');
|
||||
for (size_t fps_index = 0; fps_index < entry.fps.size(); ++fps_index) {
|
||||
payload += std::to_string(entry.fps[fps_index]);
|
||||
if (fps_index + 1 < entry.fps.size()) {
|
||||
payload.push_back(',');
|
||||
}
|
||||
}
|
||||
if (i + 1 < entries.size()) {
|
||||
payload.push_back('\n');
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
char* copy_payload(const std::string& payload) {
|
||||
char* out = reinterpret_cast<char*>(std::malloc(payload.size() + 1));
|
||||
if (!out) {
|
||||
set_last_error("Failed to allocate capture payload buffer");
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(out, payload.c_str(), payload.size() + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
int open_dshow_input_with_options(
|
||||
AVFormatContext** format_ctx,
|
||||
const AVInputFormat* input,
|
||||
const std::string& device_name,
|
||||
int width,
|
||||
int height,
|
||||
int fps,
|
||||
int requested_format,
|
||||
bool use_video_size,
|
||||
bool use_framerate,
|
||||
bool use_pixel_format,
|
||||
std::string* attempt_desc) {
|
||||
if (!format_ctx || !input) {
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
|
||||
AVDictionary* options = nullptr;
|
||||
std::vector<std::string> parts;
|
||||
|
||||
if (use_video_size && width > 0 && height > 0) {
|
||||
std::string video_size = std::to_string(width) + "x" + std::to_string(height);
|
||||
av_dict_set(&options, "video_size", video_size.c_str(), 0);
|
||||
parts.push_back("video_size=" + video_size);
|
||||
}
|
||||
if (use_framerate && fps > 0) {
|
||||
std::string framerate = std::to_string(fps);
|
||||
av_dict_set(&options, "framerate", framerate.c_str(), 0);
|
||||
parts.push_back("framerate=" + framerate);
|
||||
}
|
||||
|
||||
av_dict_set(&options, "rtbufsize", "64M", 0);
|
||||
parts.push_back("rtbufsize=64M");
|
||||
|
||||
const char* pixel_format_name = requested_pixel_format_name(requested_format);
|
||||
if (use_pixel_format && pixel_format_name) {
|
||||
av_dict_set(&options, "pixel_format", pixel_format_name, 0);
|
||||
parts.push_back(std::string("pixel_format=") + pixel_format_name);
|
||||
}
|
||||
|
||||
if (attempt_desc) {
|
||||
*attempt_desc = parts.empty() ? "default options" : "options{";
|
||||
if (!parts.empty()) {
|
||||
for (size_t i = 0; i < parts.size(); ++i) {
|
||||
if (i > 0) {
|
||||
attempt_desc->append(", ");
|
||||
}
|
||||
attempt_desc->append(parts[i]);
|
||||
}
|
||||
attempt_desc->append("}");
|
||||
}
|
||||
}
|
||||
|
||||
std::string input_name = "video=" + device_name;
|
||||
int ret = avformat_open_input(format_ctx, input_name.c_str(), input, &options);
|
||||
av_dict_free(&options);
|
||||
return ret;
|
||||
}
|
||||
|
||||
class ScopedComInit {
|
||||
public:
|
||||
ScopedComInit() {
|
||||
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
initialized_ = hr == S_OK || hr == S_FALSE;
|
||||
}
|
||||
|
||||
~ScopedComInit() {
|
||||
if (initialized_) {
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
int capture_stride(int pixel_format, int width) {
|
||||
switch (pixel_format) {
|
||||
case HWCODEC_CAPTURE_FMT_YUYV:
|
||||
case HWCODEC_CAPTURE_FMT_YVYU:
|
||||
case HWCODEC_CAPTURE_FMT_UYVY:
|
||||
return width * 2;
|
||||
case HWCODEC_CAPTURE_FMT_RGB24:
|
||||
case HWCODEC_CAPTURE_FMT_BGR24:
|
||||
return width * 3;
|
||||
case HWCODEC_CAPTURE_FMT_NV24:
|
||||
return width * 2;
|
||||
case HWCODEC_CAPTURE_FMT_NV12:
|
||||
case HWCODEC_CAPTURE_FMT_NV21:
|
||||
case HWCODEC_CAPTURE_FMT_NV16:
|
||||
case HWCODEC_CAPTURE_FMT_YUV420:
|
||||
case HWCODEC_CAPTURE_FMT_YVU420:
|
||||
case HWCODEC_CAPTURE_FMT_GREY:
|
||||
case HWCODEC_CAPTURE_FMT_MJPEG:
|
||||
case HWCODEC_CAPTURE_FMT_JPEG:
|
||||
default:
|
||||
return width;
|
||||
}
|
||||
}
|
||||
|
||||
int map_raw_pixfmt(int format) {
|
||||
switch (format) {
|
||||
case AV_PIX_FMT_YUYV422:
|
||||
return HWCODEC_CAPTURE_FMT_YUYV;
|
||||
case AV_PIX_FMT_UYVY422:
|
||||
return HWCODEC_CAPTURE_FMT_UYVY;
|
||||
#ifdef AV_PIX_FMT_YVYU422
|
||||
case AV_PIX_FMT_YVYU422:
|
||||
return HWCODEC_CAPTURE_FMT_YVYU;
|
||||
#endif
|
||||
case AV_PIX_FMT_NV12:
|
||||
return HWCODEC_CAPTURE_FMT_NV12;
|
||||
case AV_PIX_FMT_NV21:
|
||||
return HWCODEC_CAPTURE_FMT_NV21;
|
||||
#ifdef AV_PIX_FMT_NV16
|
||||
case AV_PIX_FMT_NV16:
|
||||
return HWCODEC_CAPTURE_FMT_NV16;
|
||||
#endif
|
||||
#ifdef AV_PIX_FMT_NV24
|
||||
case AV_PIX_FMT_NV24:
|
||||
return HWCODEC_CAPTURE_FMT_NV24;
|
||||
#endif
|
||||
case AV_PIX_FMT_YUV420P:
|
||||
return HWCODEC_CAPTURE_FMT_YUV420;
|
||||
#ifdef AV_PIX_FMT_YVU420P
|
||||
case AV_PIX_FMT_YVU420P:
|
||||
return HWCODEC_CAPTURE_FMT_YVU420;
|
||||
#endif
|
||||
case AV_PIX_FMT_RGB24:
|
||||
return HWCODEC_CAPTURE_FMT_RGB24;
|
||||
case AV_PIX_FMT_BGR24:
|
||||
return HWCODEC_CAPTURE_FMT_BGR24;
|
||||
case AV_PIX_FMT_GRAY8:
|
||||
return HWCODEC_CAPTURE_FMT_GREY;
|
||||
default:
|
||||
return HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
int map_codec_to_capture_format(const AVCodecParameters* codecpar) {
|
||||
if (!codecpar) {
|
||||
return HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||
}
|
||||
|
||||
switch (codecpar->codec_id) {
|
||||
case AV_CODEC_ID_MJPEG:
|
||||
return HWCODEC_CAPTURE_FMT_MJPEG;
|
||||
case AV_CODEC_ID_JPEG2000:
|
||||
return HWCODEC_CAPTURE_FMT_JPEG;
|
||||
case AV_CODEC_ID_RAWVIDEO:
|
||||
return map_raw_pixfmt(codecpar->format);
|
||||
default:
|
||||
return HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
int interrupt_callback(void* opaque) {
|
||||
auto* ctx = reinterpret_cast<HwcodecDshowCaptureContext*>(opaque);
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
auto deadline = ctx->deadline_ms.load();
|
||||
if (deadline <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (now_ms() > deadline) {
|
||||
ctx->timed_out.store(1);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* requested_pixel_format_name(int requested_format) {
|
||||
switch (requested_format) {
|
||||
case HWCODEC_CAPTURE_FMT_YUYV:
|
||||
return "yuyv422";
|
||||
case HWCODEC_CAPTURE_FMT_UYVY:
|
||||
return "uyvy422";
|
||||
case HWCODEC_CAPTURE_FMT_NV12:
|
||||
return "nv12";
|
||||
case HWCODEC_CAPTURE_FMT_NV21:
|
||||
return "nv21";
|
||||
case HWCODEC_CAPTURE_FMT_RGB24:
|
||||
return "rgb24";
|
||||
case HWCODEC_CAPTURE_FMT_BGR24:
|
||||
return "bgr24";
|
||||
case HWCODEC_CAPTURE_FMT_GREY:
|
||||
return "gray";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" const char* hwcodec_capture_last_error(void) {
|
||||
return g_last_error.c_str();
|
||||
}
|
||||
|
||||
extern "C" char* hwcodec_dshow_list_video_devices(void) {
|
||||
ScopedComInit com;
|
||||
|
||||
ICreateDevEnum* dev_enum = nullptr;
|
||||
IEnumMoniker* enum_moniker = nullptr;
|
||||
HRESULT hr = CoCreateInstance(
|
||||
CLSID_SystemDeviceEnum,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_ICreateDevEnum,
|
||||
reinterpret_cast<void**>(&dev_enum));
|
||||
if (FAILED(hr)) {
|
||||
set_last_error("Failed to create DirectShow device enumerator");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
hr = dev_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0);
|
||||
dev_enum->Release();
|
||||
if (hr != S_OK || !enum_moniker) {
|
||||
char* out = reinterpret_cast<char*>(std::malloc(1));
|
||||
if (out) {
|
||||
out[0] = '\0';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> devices;
|
||||
IMoniker* moniker = nullptr;
|
||||
ULONG fetched = 0;
|
||||
while (enum_moniker->Next(1, &moniker, &fetched) == S_OK) {
|
||||
IPropertyBag* bag = nullptr;
|
||||
hr = moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, reinterpret_cast<void**>(&bag));
|
||||
if (SUCCEEDED(hr) && bag) {
|
||||
VARIANT name;
|
||||
VariantInit(&name);
|
||||
if (SUCCEEDED(bag->Read(L"FriendlyName", &name, nullptr)) && name.vt == VT_BSTR) {
|
||||
auto utf8_name = wide_to_utf8(name.bstrVal);
|
||||
if (!utf8_name.empty()) {
|
||||
devices.push_back(utf8_name);
|
||||
}
|
||||
}
|
||||
VariantClear(&name);
|
||||
bag->Release();
|
||||
}
|
||||
moniker->Release();
|
||||
}
|
||||
enum_moniker->Release();
|
||||
|
||||
std::string payload;
|
||||
for (size_t i = 0; i < devices.size(); ++i) {
|
||||
payload += devices[i];
|
||||
if (i + 1 < devices.size()) {
|
||||
payload.push_back('\n');
|
||||
}
|
||||
}
|
||||
|
||||
return copy_payload(payload);
|
||||
}
|
||||
|
||||
extern "C" char* hwcodec_dshow_list_device_capabilities(const char* device_name) {
|
||||
if (!device_name || device_name[0] == '\0') {
|
||||
set_last_error("DirectShow device name is empty");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ScopedComInit com;
|
||||
IBaseFilter* filter = nullptr;
|
||||
if (!find_device_filter(device_name, &filter) || !filter) {
|
||||
set_last_error("Failed to find DirectShow device filter");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<DshowCapabilityEntry> entries;
|
||||
IEnumPins* enum_pins = nullptr;
|
||||
HRESULT hr = filter->EnumPins(&enum_pins);
|
||||
if (SUCCEEDED(hr) && enum_pins) {
|
||||
IPin* pin = nullptr;
|
||||
ULONG fetched = 0;
|
||||
while (enum_pins->Next(1, &pin, &fetched) == S_OK) {
|
||||
PIN_DIRECTION direction = PINDIR_INPUT;
|
||||
if (SUCCEEDED(pin->QueryDirection(&direction)) && direction == PINDIR_OUTPUT) {
|
||||
IAMStreamConfig* stream_config = nullptr;
|
||||
if (SUCCEEDED(pin->QueryInterface(IID_IAMStreamConfig, reinterpret_cast<void**>(&stream_config))) &&
|
||||
stream_config) {
|
||||
append_stream_capabilities(stream_config, &entries);
|
||||
stream_config->Release();
|
||||
}
|
||||
}
|
||||
pin->Release();
|
||||
}
|
||||
enum_pins->Release();
|
||||
}
|
||||
filter->Release();
|
||||
|
||||
std::sort(entries.begin(), entries.end(), [](const DshowCapabilityEntry& left, const DshowCapabilityEntry& right) {
|
||||
if (left.format != right.format) {
|
||||
return left.format < right.format;
|
||||
}
|
||||
if (left.width != right.width) {
|
||||
return left.width < right.width;
|
||||
}
|
||||
if (left.height != right.height) {
|
||||
return left.height < right.height;
|
||||
}
|
||||
return left.fps > right.fps;
|
||||
});
|
||||
entries.erase(
|
||||
std::unique(entries.begin(), entries.end(), [](const DshowCapabilityEntry& left, const DshowCapabilityEntry& right) {
|
||||
return left.format == right.format && left.width == right.width && left.height == right.height && left.fps == right.fps;
|
||||
}),
|
||||
entries.end());
|
||||
|
||||
return copy_payload(build_capabilities_payload(entries));
|
||||
}
|
||||
|
||||
extern "C" void hwcodec_capture_string_free(char* ptr) {
|
||||
if (ptr) {
|
||||
std::free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" HwcodecDshowCaptureContext* hwcodec_dshow_capture_open(
|
||||
const char* device_name,
|
||||
int width,
|
||||
int height,
|
||||
int fps,
|
||||
int requested_format,
|
||||
int timeout_ms) {
|
||||
if (!device_name || device_name[0] == '\0') {
|
||||
set_last_error("Device name is empty");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
avdevice_register_all();
|
||||
|
||||
const AVInputFormat* input = av_find_input_format("dshow");
|
||||
if (!input) {
|
||||
set_last_error("FFmpeg dshow input format is unavailable");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* ctx = new HwcodecDshowCaptureContext();
|
||||
ctx->timeout_ms = timeout_ms > 0 ? timeout_ms : 2000;
|
||||
ctx->format_ctx = avformat_alloc_context();
|
||||
if (!ctx->format_ctx) {
|
||||
delete ctx;
|
||||
set_last_error("Failed to allocate FFmpeg format context");
|
||||
return nullptr;
|
||||
}
|
||||
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
|
||||
ctx->format_ctx->interrupt_callback.opaque = ctx;
|
||||
|
||||
std::string open_attempt;
|
||||
int ret = open_dshow_input_with_options(
|
||||
&ctx->format_ctx,
|
||||
input,
|
||||
device_name,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
requested_format,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
&open_attempt);
|
||||
|
||||
if (ret < 0) {
|
||||
avformat_free_context(ctx->format_ctx);
|
||||
ctx->format_ctx = avformat_alloc_context();
|
||||
if (!ctx->format_ctx) {
|
||||
delete ctx;
|
||||
set_last_error("Failed to allocate FFmpeg format context for fallback open");
|
||||
return nullptr;
|
||||
}
|
||||
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
|
||||
ctx->format_ctx->interrupt_callback.opaque = ctx;
|
||||
|
||||
std::string fallback_attempt;
|
||||
ret = open_dshow_input_with_options(
|
||||
&ctx->format_ctx,
|
||||
input,
|
||||
device_name,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
requested_format,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
&fallback_attempt);
|
||||
if (ret >= 0) {
|
||||
open_attempt = fallback_attempt;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
avformat_free_context(ctx->format_ctx);
|
||||
ctx->format_ctx = avformat_alloc_context();
|
||||
if (!ctx->format_ctx) {
|
||||
delete ctx;
|
||||
set_last_error("Failed to allocate FFmpeg format context for final fallback open");
|
||||
return nullptr;
|
||||
}
|
||||
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
|
||||
ctx->format_ctx->interrupt_callback.opaque = ctx;
|
||||
|
||||
std::string fallback_attempt;
|
||||
ret = open_dshow_input_with_options(
|
||||
&ctx->format_ctx,
|
||||
input,
|
||||
device_name,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
requested_format,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
&fallback_attempt);
|
||||
if (ret >= 0) {
|
||||
open_attempt = fallback_attempt;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
set_last_error("Failed to open dshow input (" + open_attempt + "): " + ffmpeg_error(ret));
|
||||
avformat_free_context(ctx->format_ctx);
|
||||
delete ctx;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ret = avformat_find_stream_info(ctx->format_ctx, nullptr);
|
||||
if (ret < 0) {
|
||||
set_last_error("Failed to read stream info: " + ffmpeg_error(ret));
|
||||
avformat_close_input(&ctx->format_ctx);
|
||||
delete ctx;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < ctx->format_ctx->nb_streams; ++i) {
|
||||
AVStream* stream = ctx->format_ctx->streams[i];
|
||||
if (stream && stream->codecpar && stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
ctx->stream_index = static_cast<int>(i);
|
||||
ctx->width = stream->codecpar->width > 0 ? stream->codecpar->width : width;
|
||||
ctx->height = stream->codecpar->height > 0 ? stream->codecpar->height : height;
|
||||
ctx->pixel_format = map_codec_to_capture_format(stream->codecpar);
|
||||
ctx->stride = capture_stride(ctx->pixel_format, ctx->width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx->stream_index < 0) {
|
||||
set_last_error("No video stream found on DirectShow device");
|
||||
avformat_close_input(&ctx->format_ctx);
|
||||
delete ctx;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (ctx->pixel_format == HWCODEC_CAPTURE_FMT_UNKNOWN) {
|
||||
set_last_error("DirectShow stream format is unsupported in current Windows backend");
|
||||
avformat_close_input(&ctx->format_ctx);
|
||||
delete ctx;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
extern "C" int hwcodec_dshow_capture_info(
|
||||
HwcodecDshowCaptureContext* ctx,
|
||||
HwcodecCaptureStreamInfo* out_info) {
|
||||
if (!ctx || !out_info) {
|
||||
set_last_error("Invalid capture context");
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_info->width = ctx->width;
|
||||
out_info->height = ctx->height;
|
||||
out_info->pixel_format = ctx->pixel_format;
|
||||
out_info->stride = ctx->stride;
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int hwcodec_dshow_capture_read(
|
||||
HwcodecDshowCaptureContext* ctx,
|
||||
uint8_t** out_data,
|
||||
int* out_len,
|
||||
uint64_t* out_sequence) {
|
||||
if (!ctx || !out_data || !out_len || !out_sequence) {
|
||||
set_last_error("Invalid capture read arguments");
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_data = nullptr;
|
||||
*out_len = 0;
|
||||
*out_sequence = 0;
|
||||
|
||||
AVPacket packet;
|
||||
av_init_packet(&packet);
|
||||
packet.data = nullptr;
|
||||
packet.size = 0;
|
||||
|
||||
while (true) {
|
||||
ctx->timed_out.store(0);
|
||||
ctx->deadline_ms.store(now_ms() + ctx->timeout_ms);
|
||||
int ret = av_read_frame(ctx->format_ctx, &packet);
|
||||
ctx->deadline_ms.store(0);
|
||||
|
||||
if (ret < 0) {
|
||||
if (ctx->timed_out.load() != 0) {
|
||||
set_last_error("Timed out waiting for frame");
|
||||
return -110;
|
||||
}
|
||||
set_last_error("Failed to read frame: " + ffmpeg_error(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (packet.stream_index != ctx->stream_index) {
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (packet.size <= 0 || !packet.data) {
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* buffer = reinterpret_cast<uint8_t*>(std::malloc(static_cast<size_t>(packet.size)));
|
||||
if (!buffer) {
|
||||
av_packet_unref(&packet);
|
||||
set_last_error("Failed to allocate packet buffer");
|
||||
return -12;
|
||||
}
|
||||
|
||||
std::memcpy(buffer, packet.data, static_cast<size_t>(packet.size));
|
||||
*out_data = buffer;
|
||||
*out_len = packet.size;
|
||||
*out_sequence = ctx->sequence++;
|
||||
av_packet_unref(&packet);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void hwcodec_dshow_capture_packet_free(uint8_t* data) {
|
||||
if (data) {
|
||||
std::free(data);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void hwcodec_dshow_capture_close(HwcodecDshowCaptureContext* ctx) {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
if (ctx->format_ctx) {
|
||||
avformat_close_input(&ctx->format_ctx);
|
||||
}
|
||||
delete ctx;
|
||||
}
|
||||
64
libs/hwcodec/cpp/ffmpeg_capture_ffi.h
Normal file
64
libs/hwcodec/cpp/ffmpeg_capture_ffi.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#ifndef HWCODEC_FFMPEG_CAPTURE_FFI_H
|
||||
#define HWCODEC_FFMPEG_CAPTURE_FFI_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct HwcodecDshowCaptureContext HwcodecDshowCaptureContext;
|
||||
|
||||
enum HwcodecCapturePixelFormat {
|
||||
HWCODEC_CAPTURE_FMT_UNKNOWN = 0,
|
||||
HWCODEC_CAPTURE_FMT_MJPEG = 1,
|
||||
HWCODEC_CAPTURE_FMT_JPEG = 2,
|
||||
HWCODEC_CAPTURE_FMT_YUYV = 3,
|
||||
HWCODEC_CAPTURE_FMT_YVYU = 4,
|
||||
HWCODEC_CAPTURE_FMT_UYVY = 5,
|
||||
HWCODEC_CAPTURE_FMT_NV12 = 6,
|
||||
HWCODEC_CAPTURE_FMT_NV21 = 7,
|
||||
HWCODEC_CAPTURE_FMT_NV16 = 8,
|
||||
HWCODEC_CAPTURE_FMT_NV24 = 9,
|
||||
HWCODEC_CAPTURE_FMT_YUV420 = 10,
|
||||
HWCODEC_CAPTURE_FMT_YVU420 = 11,
|
||||
HWCODEC_CAPTURE_FMT_RGB24 = 12,
|
||||
HWCODEC_CAPTURE_FMT_BGR24 = 13,
|
||||
HWCODEC_CAPTURE_FMT_GREY = 14,
|
||||
};
|
||||
|
||||
typedef struct HwcodecCaptureStreamInfo {
|
||||
int width;
|
||||
int height;
|
||||
int pixel_format;
|
||||
int stride;
|
||||
} HwcodecCaptureStreamInfo;
|
||||
|
||||
const char* hwcodec_capture_last_error(void);
|
||||
char* hwcodec_dshow_list_video_devices(void);
|
||||
char* hwcodec_dshow_list_device_capabilities(const char* device_name);
|
||||
void hwcodec_capture_string_free(char* ptr);
|
||||
|
||||
HwcodecDshowCaptureContext* hwcodec_dshow_capture_open(
|
||||
const char* device_name,
|
||||
int width,
|
||||
int height,
|
||||
int fps,
|
||||
int requested_format,
|
||||
int timeout_ms);
|
||||
int hwcodec_dshow_capture_info(
|
||||
HwcodecDshowCaptureContext* ctx,
|
||||
HwcodecCaptureStreamInfo* out_info);
|
||||
int hwcodec_dshow_capture_read(
|
||||
HwcodecDshowCaptureContext* ctx,
|
||||
uint8_t** out_data,
|
||||
int* out_len,
|
||||
uint64_t* out_sequence);
|
||||
void hwcodec_dshow_capture_packet_free(uint8_t* data);
|
||||
void hwcodec_dshow_capture_close(HwcodecDshowCaptureContext* ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
297
libs/hwcodec/src/capture.rs
Normal file
297
libs/hwcodec/src/capture.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/ffmpeg_capture_ffi.rs"));
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CapturePixelFormat {
|
||||
Unknown,
|
||||
Mjpeg,
|
||||
Jpeg,
|
||||
Yuyv,
|
||||
Yvyu,
|
||||
Uyvy,
|
||||
Nv12,
|
||||
Nv21,
|
||||
Nv16,
|
||||
Nv24,
|
||||
Yuv420,
|
||||
Yvu420,
|
||||
Rgb24,
|
||||
Bgr24,
|
||||
Grey,
|
||||
}
|
||||
|
||||
impl CapturePixelFormat {
|
||||
pub fn to_ffi(self) -> c_int {
|
||||
match self {
|
||||
Self::Unknown => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UNKNOWN as c_int,
|
||||
Self::Mjpeg => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_MJPEG as c_int,
|
||||
Self::Jpeg => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_JPEG as c_int,
|
||||
Self::Yuyv => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUYV as c_int,
|
||||
Self::Yvyu => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVYU as c_int,
|
||||
Self::Uyvy => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UYVY as c_int,
|
||||
Self::Nv12 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV12 as c_int,
|
||||
Self::Nv21 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV21 as c_int,
|
||||
Self::Nv16 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV16 as c_int,
|
||||
Self::Nv24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV24 as c_int,
|
||||
Self::Yuv420 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUV420 as c_int,
|
||||
Self::Yvu420 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVU420 as c_int,
|
||||
Self::Rgb24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_RGB24 as c_int,
|
||||
Self::Bgr24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_BGR24 as c_int,
|
||||
Self::Grey => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_GREY as c_int,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_ffi(value: c_int) -> Self {
|
||||
match value {
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_MJPEG as c_int => Self::Mjpeg,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_JPEG as c_int => Self::Jpeg,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUYV as c_int => Self::Yuyv,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVYU as c_int => Self::Yvyu,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UYVY as c_int => Self::Uyvy,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV12 as c_int => Self::Nv12,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV21 as c_int => Self::Nv21,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV16 as c_int => Self::Nv16,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV24 as c_int => Self::Nv24,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUV420 as c_int => {
|
||||
Self::Yuv420
|
||||
}
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVU420 as c_int => {
|
||||
Self::Yvu420
|
||||
}
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_RGB24 as c_int => Self::Rgb24,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_BGR24 as c_int => Self::Bgr24,
|
||||
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_GREY as c_int => Self::Grey,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name.trim().to_ascii_uppercase().as_str() {
|
||||
"MJPEG" | "MJPG" => Some(Self::Mjpeg),
|
||||
"JPEG" => Some(Self::Jpeg),
|
||||
"YUYV" => Some(Self::Yuyv),
|
||||
"YVYU" => Some(Self::Yvyu),
|
||||
"UYVY" => Some(Self::Uyvy),
|
||||
"NV12" => Some(Self::Nv12),
|
||||
"NV21" => Some(Self::Nv21),
|
||||
"NV16" => Some(Self::Nv16),
|
||||
"NV24" => Some(Self::Nv24),
|
||||
"YUV420" | "I420" | "IYUV" => Some(Self::Yuv420),
|
||||
"YVU420" | "YV12" => Some(Self::Yvu420),
|
||||
"RGB24" => Some(Self::Rgb24),
|
||||
"BGR24" => Some(Self::Bgr24),
|
||||
"GREY" | "GRAY" | "Y800" => Some(Self::Grey),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DshowCapability {
|
||||
pub format: CapturePixelFormat,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub fps: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CaptureStreamInfo {
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub pixel_format: CapturePixelFormat,
|
||||
pub stride: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CaptureError {
|
||||
pub code: i32,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CaptureError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CaptureError {}
|
||||
|
||||
fn last_error_message() -> String {
|
||||
unsafe {
|
||||
let ptr = hwcodec_capture_last_error();
|
||||
if ptr.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
CStr::from_ptr(ptr).to_string_lossy().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_dshow_video_devices() -> Result<Vec<String>, CaptureError> {
|
||||
unsafe {
|
||||
let ptr = hwcodec_dshow_list_video_devices();
|
||||
if ptr.is_null() {
|
||||
return Err(CaptureError {
|
||||
code: -1,
|
||||
message: last_error_message(),
|
||||
});
|
||||
}
|
||||
let payload = CStr::from_ptr(ptr).to_string_lossy().to_string();
|
||||
hwcodec_capture_string_free(ptr as *mut _);
|
||||
Ok(payload
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_dshow_device_capabilities(device_name: &str) -> Result<Vec<DshowCapability>, CaptureError> {
|
||||
let device_name = CString::new(device_name).map_err(|_| CaptureError {
|
||||
code: -1,
|
||||
message: "device name contains NUL byte".to_string(),
|
||||
})?;
|
||||
|
||||
unsafe {
|
||||
let ptr = hwcodec_dshow_list_device_capabilities(device_name.as_ptr());
|
||||
if ptr.is_null() {
|
||||
return Err(CaptureError {
|
||||
code: -1,
|
||||
message: last_error_message(),
|
||||
});
|
||||
}
|
||||
|
||||
let payload = CStr::from_ptr(ptr).to_string_lossy().to_string();
|
||||
hwcodec_capture_string_free(ptr as *mut _);
|
||||
|
||||
let capabilities = payload
|
||||
.lines()
|
||||
.filter_map(parse_dshow_capability_line)
|
||||
.collect();
|
||||
Ok(capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_dshow_capability_line(line: &str) -> Option<DshowCapability> {
|
||||
let mut parts = line.split('|');
|
||||
let format = CapturePixelFormat::from_name(parts.next()?.trim())?;
|
||||
let width = parts.next()?.trim().parse::<u32>().ok()?;
|
||||
let height = parts.next()?.trim().parse::<u32>().ok()?;
|
||||
let fps = parts
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.filter_map(|value| value.trim().parse::<u32>().ok())
|
||||
.filter(|value| *value > 0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(DshowCapability {
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct DshowCapture {
|
||||
ctx: *mut HwcodecDshowCaptureContext,
|
||||
}
|
||||
|
||||
unsafe impl Send for DshowCapture {}
|
||||
|
||||
impl DshowCapture {
|
||||
pub fn open(
|
||||
device_name: &str,
|
||||
width: i32,
|
||||
height: i32,
|
||||
fps: i32,
|
||||
requested_format: CapturePixelFormat,
|
||||
timeout_ms: i32,
|
||||
) -> Result<Self, CaptureError> {
|
||||
let device_name = CString::new(device_name).map_err(|_| CaptureError {
|
||||
code: -1,
|
||||
message: "device name contains NUL byte".to_string(),
|
||||
})?;
|
||||
unsafe {
|
||||
let ctx = hwcodec_dshow_capture_open(
|
||||
device_name.as_ptr(),
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
requested_format.to_ffi(),
|
||||
timeout_ms,
|
||||
);
|
||||
if ctx.is_null() {
|
||||
return Err(CaptureError {
|
||||
code: -1,
|
||||
message: last_error_message(),
|
||||
});
|
||||
}
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(&self) -> Result<CaptureStreamInfo, CaptureError> {
|
||||
unsafe {
|
||||
let mut info = HwcodecCaptureStreamInfo {
|
||||
width: 0,
|
||||
height: 0,
|
||||
pixel_format: HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UNKNOWN as c_int,
|
||||
stride: 0,
|
||||
};
|
||||
let ret = hwcodec_dshow_capture_info(self.ctx, &mut info);
|
||||
if ret != 0 {
|
||||
return Err(CaptureError {
|
||||
code: ret,
|
||||
message: last_error_message(),
|
||||
});
|
||||
}
|
||||
Ok(CaptureStreamInfo {
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
pixel_format: CapturePixelFormat::from_ffi(info.pixel_format),
|
||||
stride: info.stride,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_packet(&mut self) -> Result<(Vec<u8>, u64), CaptureError> {
|
||||
unsafe {
|
||||
let mut data = std::ptr::null_mut();
|
||||
let mut len = 0;
|
||||
let mut sequence = 0u64;
|
||||
let ret = hwcodec_dshow_capture_read(self.ctx, &mut data, &mut len, &mut sequence);
|
||||
if ret != 0 {
|
||||
return Err(CaptureError {
|
||||
code: ret,
|
||||
message: last_error_message(),
|
||||
});
|
||||
}
|
||||
if data.is_null() || len <= 0 {
|
||||
return Err(CaptureError {
|
||||
code: -1,
|
||||
message: "empty packet returned by capture backend".to_string(),
|
||||
});
|
||||
}
|
||||
let slice = std::slice::from_raw_parts(data, len as usize);
|
||||
let vec = slice.to_vec();
|
||||
hwcodec_dshow_capture_packet_free(data);
|
||||
Ok((vec, sequence))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DshowCapture {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
hwcodec_dshow_capture_close(self.ctx);
|
||||
}
|
||||
self.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,13 @@ struct ProbePolicy {
|
||||
|
||||
impl ProbePolicy {
|
||||
fn for_codec(codec_name: &str) -> Self {
|
||||
if codec_name.contains("v4l2m2m") {
|
||||
if codec_name.contains("amf") {
|
||||
Self {
|
||||
max_attempts: 5,
|
||||
request_keyframe: true,
|
||||
accept_any_output: true,
|
||||
}
|
||||
} else if codec_name.contains("v4l2m2m") {
|
||||
Self {
|
||||
max_attempts: 5,
|
||||
request_keyframe: true,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(windows)]
|
||||
pub mod capture;
|
||||
pub mod common;
|
||||
pub mod ffmpeg;
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
|
||||
|
||||
Reference in New Issue
Block a user