refactor(camera): unify camera devices

This commit is contained in:
lgv 2026-06-30 16:04:59 +08:00
parent 80c8969ce9
commit ca19449545
19 changed files with 1181 additions and 695 deletions

View File

@ -3,14 +3,22 @@ camera {
id: "cam1"
realsense {
serialNumber: "243122074587"
camera_mode: CAMERA_MODE_VIDEO
capture {
width: 640
height: 480
fps: 30
stream_mode: STREAM_MODE_RGBD
}
encoder {
width: 640
height: 480
fps: 30
codec: "H265"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGBD
align_mode: ALIGN_MODE_COLOR
enable_stream_timestamp: true
buffer_size: 30
}
align_mode: ALIGN_MODE_COLOR
sync: false
}
}
@ -19,16 +27,22 @@ camera {
id: "right_hand_cam"
realsense {
serialNumber: "243122072252"
camera_mode: CAMERA_MODE_VIDEO
capture {
width: 1280
height: 720
encode_width: 640
encode_height: 360
fps: 30
stream_mode: STREAM_MODE_RGB
}
encoder {
width: 640
height: 360
fps: 30
codec: "H264"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGB
align_mode: ALIGN_MODE_COLOR
enable_stream_timestamp: true
buffer_size: 30
}
align_mode: ALIGN_MODE_COLOR
sync: false
}
}
@ -37,47 +51,98 @@ camera {
id: "cam3"
realsense {
serialNumber: "243122075614"
camera_mode: CAMERA_MODE_VIDEO
capture {
width: 640
height: 480
fps: 30
stream_mode: STREAM_MODE_RGBD
}
encoder {
width: 640
height: 480
fps: 30
codec: "H265"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGBD
align_mode: ALIGN_MODE_COLOR
enable_stream_timestamp: true
buffer_size: 30
}
align_mode: ALIGN_MODE_COLOR
sync: false
}
}
cameras {
id: "mujoco_hand_cam"
mujoco {
world_id: "mujoco_world"
camera_name: "hand_cam"
render {
width: 1280
height: 720
fps: 30
stream_mode: STREAM_MODE_RGBD
}
encoder {
width: 1280
height: 720
fps: 30
codec: "H264"
enable_stream_timestamp: true
buffer_size: 30
}
consume_new_frame_only: false
viewer_pip {
enable: true
left: -10
bottom: 10
width: 320
height: 180
}
}
}
cameras {
id: "left_eye_cam"
uvc {
usb: "/dev/uvc_left_camera"
camera_mode: CAMERA_MODE_VIDEO
capture {
width: 640
height: 480
fps: 30
stream_mode: STREAM_MODE_RGB
}
encoder {
width: 640
height: 480
fps: 30
codec: "H265"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGB
enable_stream_timestamp: true
buffer_size: 30
}
}
}
cameras {
id: "cam5"
realsense {
serialNumber: "243122070435"
camera_mode: CAMERA_MODE_VIDEO
capture {
width: 1280
height: 720
encode_width: 640
encode_height: 360
fps: 30
stream_mode: STREAM_MODE_RGB
}
encoder {
width: 640
height: 360
fps: 30
codec: "H264"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGB
align_mode: ALIGN_MODE_COLOR
enable_stream_timestamp: true
buffer_size: 30
}
align_mode: ALIGN_MODE_COLOR
sync: false
}
}

View File

@ -1,4 +1,5 @@
#add_subdirectory(mechmind)
add_subdirectory(common)
add_subdirectory(uvc_camera)
add_subdirectory(realsense_camera)
add_subdirectory(mujoco_camera)
@ -11,6 +12,8 @@ target_link_libraries(camera
INTERFACE
cmvr_es::device::uvc_camera
cmvr_es::device::realsense_camera
cmvr_es::device::mujoco_camera
cmvr_es::device::camera_stream_encoder
cmvr_es::proto
)

View File

@ -8,6 +8,8 @@
#include "cmvr/config/camera_config/camera_config.pb.h"
namespace cmvr::device {
enum CameraMode {PHOTO_MODE, VIDEO_MODE};
struct Rs2Intrinsics
{
float cx;

View File

@ -8,6 +8,7 @@
#include "cmvr/config/camera_config/camera_config.pb.h"
#include "common/base/logging/logger.h"
#include "devices/camera/abstract_camera.h"
#include "devices/camera/mujoco_camera/include/mujoco_camera.h"
#include "devices/camera/realsense_camera/include/realsense_camera.h"
#include "devices/camera/uvc_camera/include/uvc_camera.h"
@ -40,6 +41,10 @@ public:
return nullptr;
}
case config::CameraDeviceConfig::kMujoco:
return std::make_shared<MujocoCamera>(
backendWithId_(cfg.id(), cfg.mujoco()));
case config::CameraDeviceConfig::BACKEND_NOT_SET:
default:
{

View File

@ -0,0 +1,18 @@
add_library(camera_stream_encoder SHARED src/camera_stream_encoder.cpp)
target_include_directories(camera_stream_encoder PUBLIC ${CMAKE_SOURCE_DIR}/cmvr-es)
target_link_libraries(camera_stream_encoder
PUBLIC
opencv_core
opencv_imgproc
avcodec
avformat
avutil
swscale
swresample
)
add_library(cmvr_es::device::camera_stream_encoder ALIAS camera_stream_encoder)
install(TARGETS camera_stream_encoder LIBRARY DESTINATION lib)

View File

@ -0,0 +1,47 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <opencv2/opencv.hpp>
#include "speaker/ffmpeg_speaker/include/ffmpeg_ptr.h"
namespace cmvr::device {
struct FfmpegEncoderInfo {
std::string codec_name;
int width = 0;
int height = 0;
int fps = 0;
int64_t frame_pts = 0;
bool bRunning = false;
AVCodecContext* codec_context = nullptr;
AVFrame* frame = nullptr;
AVPacket* packet = nullptr;
SwsContext* sws_context = nullptr;
~FfmpegEncoderInfo();
};
struct CameraStreamEncodeOptions {
bool draw_timestamp = false;
};
class CameraStreamEncoder {
public:
static bool init(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const std::string& codec_name,
int width,
int height,
int fps);
static bool encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const cv::Mat& frame,
std::vector<uint8_t>& encoded_frame,
bool& is_key,
const CameraStreamEncodeOptions& options = {});
};
} // namespace cmvr::device

View File

@ -0,0 +1,297 @@
#include "devices/camera/common/include/camera_stream_encoder.h"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <libavutil/opt.h>
#include <opencv2/imgproc.hpp>
#include "common/base/logging/logger.h"
namespace cmvr::device {
namespace {
std::string getCurrentTimeString()
{
const auto now = std::chrono::system_clock::now();
const auto now_sec = std::chrono::time_point_cast<std::chrono::seconds>(now);
const std::time_t now_time = std::chrono::system_clock::to_time_t(now_sec);
const std::tm* tm_ptr = std::localtime(&now_time);
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - now_sec).count();
std::stringstream ss;
ss << std::put_time(tm_ptr, "%Y-%m-%d %H:%M:%S")
<< "." << std::setw(3) << std::setfill('0') << ms;
return ss.str();
}
void drawTimeStamp(cv::Mat& image)
{
if (image.empty()) {
return;
}
const std::string time_str = getCurrentTimeString();
constexpr int font_face = cv::FONT_HERSHEY_SIMPLEX;
constexpr double font_scale = 0.8;
constexpr int thickness = 2;
int baseline = 0;
const cv::Size text_size = cv::getTextSize(time_str, font_face, font_scale, thickness, &baseline);
const cv::Point text_pos(image.cols - text_size.width - 10, text_size.height + 10);
cv::putText(image, time_str, text_pos, font_face, font_scale, cv::Scalar(0, 0, 0), thickness + 2);
cv::putText(image, time_str, text_pos, font_face, font_scale, cv::Scalar(255, 255, 255), thickness);
}
const AVCodec* findEncoder(const std::string& codec_name)
{
if (codec_name == "h264" || codec_name == "H264") {
const AVCodec* codec = avcodec_find_encoder_by_name("libx264");
return codec ? codec : avcodec_find_encoder(AV_CODEC_ID_H264);
}
if (codec_name == "h265" || codec_name == "HEVC" || codec_name == "H265") {
const AVCodec* codec = avcodec_find_encoder_by_name("libx265");
return codec ? codec : avcodec_find_encoder(AV_CODEC_ID_HEVC);
}
return nullptr;
}
AVPixelFormat sourcePixelFormat(const cv::Mat& frame)
{
if (frame.channels() == 3) {
return AV_PIX_FMT_BGR24;
}
if (frame.channels() == 4) {
return AV_PIX_FMT_BGRA;
}
if (frame.channels() == 1) {
return AV_PIX_FMT_GRAY8;
}
return AV_PIX_FMT_NONE;
}
} // namespace
FfmpegEncoderInfo::~FfmpegEncoderInfo()
{
if (frame) {
av_frame_free(&frame);
frame = nullptr;
}
if (packet) {
av_packet_free(&packet);
packet = nullptr;
}
if (codec_context) {
avcodec_close(codec_context);
avcodec_free_context(&codec_context);
codec_context = nullptr;
}
if (sws_context) {
sws_freeContext(sws_context);
sws_context = nullptr;
}
}
bool CameraStreamEncoder::init(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const std::string& codec_name,
const int width,
const int height,
const int fps)
{
encoder = std::make_shared<FfmpegEncoderInfo>();
encoder->codec_name = codec_name;
encoder->width = width;
encoder->height = height;
encoder->fps = fps;
const AVCodec* codec = findEncoder(codec_name);
if (!codec) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to find encoder: " << codec_name;
return false;
}
encoder->codec_context = avcodec_alloc_context3(codec);
if (!encoder->codec_context) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to allocate codec context";
return false;
}
AVCodecContext* ctx = encoder->codec_context;
ctx->codec_type = AVMEDIA_TYPE_VIDEO;
ctx->width = (width + 1) & ~1;
ctx->height = (height + 1) & ~1;
ctx->time_base = {1, fps};
ctx->framerate = {fps, 1};
ctx->max_b_frames = 0;
ctx->gop_size = 10;
if (codec->id == AV_CODEC_ID_H264) {
av_opt_set(ctx->priv_data, "preset", "ultrafast", 0);
av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);
av_opt_set(ctx->priv_data, "profile", "baseline", 0);
av_opt_set(ctx->priv_data, "repeat-headers", "1", 0);
av_opt_set(ctx->priv_data, "annexb", "1", 0);
} else if (codec->id == AV_CODEC_ID_HEVC) {
av_opt_set(ctx->priv_data, "x265-params",
"keyint=10:min-keyint=10:no-open-gop=1:bframes=0:rc-lookahead=0:log-level=none",
0);
av_opt_set_int(ctx->priv_data, "keyint", 10, 0);
av_opt_set_int(ctx->priv_data, "min-keyint", 10, 0);
av_opt_set(ctx->priv_data, "no-open-gop", "1", 0);
}
const AVPixelFormat* pix_fmts = codec->pix_fmts;
if (!pix_fmts) {
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
} else {
ctx->pix_fmt = pix_fmts[0];
}
const int open_ret = avcodec_open2(ctx, codec, nullptr);
if (open_ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(open_ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to open " << codec_name << " encoder: " << errbuf;
return false;
}
encoder->frame = av_frame_alloc();
if (!encoder->frame) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to allocate AVFrame";
return false;
}
encoder->frame->format = ctx->pix_fmt;
encoder->frame->width = ctx->width;
encoder->frame->height = ctx->height;
if (av_frame_get_buffer(encoder->frame, 0) < 0) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to allocate AVFrame buffer";
av_frame_free(&encoder->frame);
return false;
}
encoder->packet = av_packet_alloc();
if (!encoder->packet) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to allocate AVPacket";
return false;
}
CMVR_LOG(INFO) << "[CameraStreamEncoder] initialized " << codec_name
<< " encoder, size=" << width << "x" << height
<< ", fps=" << fps;
return true;
}
bool CameraStreamEncoder::encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const cv::Mat& frame,
std::vector<uint8_t>& encoded_frame,
bool& is_key,
const CameraStreamEncodeOptions& options)
{
encoded_frame.clear();
is_key = false;
if (!encoder || !encoder->codec_context || !encoder->frame || !encoder->packet) {
return false;
}
if (frame.cols != encoder->width || frame.rows != encoder->height) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] frame size mismatch"
<< ", frame=" << frame.cols << "x" << frame.rows
<< ", encoder=" << encoder->width << "x" << encoder->height;
return false;
}
cv::Mat frame_to_encode = frame;
if (options.draw_timestamp) {
frame_to_encode = frame.clone();
drawTimeStamp(frame_to_encode);
}
const AVPixelFormat src_pix_fmt = sourcePixelFormat(frame_to_encode);
if (src_pix_fmt == AV_PIX_FMT_NONE) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] unsupported channels: " << frame_to_encode.channels();
return false;
}
if (encoder->sws_context) {
sws_freeContext(encoder->sws_context);
}
encoder->sws_context = sws_getContext(frame_to_encode.cols,
frame_to_encode.rows,
src_pix_fmt,
encoder->codec_context->width,
encoder->codec_context->height,
encoder->codec_context->pix_fmt,
SWS_BILINEAR,
nullptr,
nullptr,
nullptr);
if (!encoder->sws_context) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to create SwsContext";
return false;
}
const uint8_t* src_data[AV_NUM_DATA_POINTERS] = {frame_to_encode.data};
int src_linesize[AV_NUM_DATA_POINTERS] = {static_cast<int>(frame_to_encode.step)};
const int scale_ret = sws_scale(encoder->sws_context,
src_data,
src_linesize,
0,
frame_to_encode.rows,
encoder->frame->data,
encoder->frame->linesize);
if (scale_ret < 0) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] error scaling frame";
return false;
}
encoder->frame->pts = encoder->frame_pts++;
int ret = avcodec_send_frame(encoder->codec_context, encoder->frame);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] error sending frame to encoder: " << errbuf;
return false;
}
while (true) {
ret = avcodec_receive_packet(encoder->codec_context, encoder->packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] error receiving packet from encoder: " << errbuf;
break;
}
if (encoder->packet->flags & AV_PKT_FLAG_KEY) {
is_key = true;
}
encoded_frame.reserve(encoded_frame.size() + encoder->packet->size);
encoded_frame.insert(encoded_frame.end(),
encoder->packet->data,
encoder->packet->data + encoder->packet->size);
av_packet_unref(encoder->packet);
}
if (encoded_frame.size() < 4) {
return false;
}
const bool has_start_code =
(encoded_frame[0] == 0 && encoded_frame[1] == 0 && encoded_frame[2] == 1) ||
(encoded_frame[0] == 0 && encoded_frame[1] == 0 && encoded_frame[2] == 0 && encoded_frame[3] == 1);
if (!has_start_code) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] invalid frame: no NALU start code";
return false;
}
return true;
}
} // namespace cmvr::device

View File

@ -4,8 +4,30 @@ add_library(mujoco_camera SHARED src/mujoco_camera.cpp)
target_include_directories(mujoco_camera PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(mujoco_camera PUBLIC ${OpenCV_LIBS})
target_link_libraries(mujoco_camera
PUBLIC
${OpenCV_LIBS}
cmvr_es::proto
cmvr_es::device::camera_stream_encoder
cmvr_es::mujoco_world
cmvr_es::device::motor_manager
glfw
mujoco
)
add_library(cmvr_es::device::mujoco_camera ALIAS mujoco_camera)
add_executable(mujoco_camera_test
src/mujoco_camera_test.cpp
)
target_link_libraries(mujoco_camera_test
PRIVATE
cmvr_es::device::mujoco_camera
cmvr_es::mujoco_world
gtest
gtest_main
pthread
)
install(TARGETS mujoco_camera LIBRARY DESTINATION lib)

View File

@ -6,10 +6,19 @@
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include <mujoco/mujoco.h>
#include "cmvr/config/camera_config/camera_config.pb.h"
#include "devices/camera/abstract_camera.h"
#include "devices/camera/common/include/camera_stream_encoder.h"
#include "simulate/mujoco/mujoco_world/include/mujoco_world.h"
struct GLFWwindow;
namespace cmvr::device {
@ -22,27 +31,67 @@ public:
uint64_t& frame_id)>;
explicit MujocoCamera(FetchRgbdFn fetch_rgbd_fn);
~MujocoCamera() override = default;
explicit MujocoCamera(config::MujocoCameraConfig config);
~MujocoCamera() override;
std::string typeName() const override { return "MujocoCamera"; }
const config::MujocoCameraConfig& config() const { return config_; }
bool init() override;
bool start() override;
bool stop() override;
void setFetchRgbdFn(FetchRgbdFn fetch_rgbd_fn);
void setFovyDeg(double fovy_deg);
void setConsumeNewFrameOnly(bool enable);
void getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) override;
void getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) override;
void getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics) override;
bool startStreaming() override;
void stopStreaming() override;
bool getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index) override;
private:
bool fetch(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics);
void fillIntrinsics(int width, int height, Rs2Intrinsics& intrinsics) const;
bool initOffscreen_();
void destroyOffscreen_();
bool renderOffscreen_(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics);
bool ensureEncoder_(int width, int height, int fps);
void setError_(const std::string& error);
static void flipRgbAndDepth_(std::vector<unsigned char>& rgb,
std::vector<float>& depth,
int width,
int height);
static void linearizeDepth_(const mjModel* model, std::vector<float>& depth);
private:
FetchRgbdFn fetch_rgbd_fn_;
mutable std::mutex mtx_;
config::MujocoCameraConfig config_;
std::weak_ptr<simulate::MujocoWorld> world_;
GLFWwindow* window_{nullptr};
mjvCamera camera_{};
mjvOption option_{};
mjvPerturb perturb_{};
mjvScene scene_{};
mjrContext context_{};
bool scene_initialized_{false};
bool context_initialized_{false};
int camera_id_{-1};
int width_{640};
int height_{480};
int encode_width_{640};
int encode_height_{480};
std::string codec_{"H264"};
bool enable_stream_timestamp_{false};
double fovy_deg_{60.0};
bool consume_new_frame_only_{true};
uint64_t last_frame_id_{0};
bool has_last_frame_id_{false};
size_t stream_frame_index_{0};
bool streaming_{false};
std::shared_ptr<FfmpegEncoderInfo> rgb_encoder_;
};
} // namespace cmvr::device

View File

@ -1,87 +1,460 @@
//
// Created by lgv on 2026/2/26.
//
#include "../include/mujoco_camera.h"
#include "devices/camera/mujoco_camera/include/mujoco_camera.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <utility>
#include <GLFW/glfw3.h>
#include <opencv2/imgproc.hpp>
#include "common/base/logging/logger.h"
#include "devices/motor/manager/include/motor_manager.h"
namespace cmvr::device {
namespace {
constexpr int kDefaultWidth = 640;
constexpr int kDefaultHeight = 480;
constexpr int kMaxGeom = 100000;
int positiveOrDefault(const int value, const int fallback)
{
return value > 0 ? value : fallback;
}
} // namespace
MujocoCamera::MujocoCamera(FetchRgbdFn fetch_rgbd_fn)
: fetch_rgbd_fn_(std::move(fetch_rgbd_fn)) {}
: fetch_rgbd_fn_(std::move(fetch_rgbd_fn))
{
id_ = "mujoco_camera";
}
void MujocoCamera::setFovyDeg(double fovy_deg) {
MujocoCamera::MujocoCamera(config::MujocoCameraConfig config)
: config_(config)
{
id_ = config_.id();
const auto& render = config_.render();
const auto& encoder = config_.encoder();
width_ = positiveOrDefault(render.width(), kDefaultWidth);
height_ = positiveOrDefault(render.height(), kDefaultHeight);
encode_width_ = positiveOrDefault(encoder.width(), width_);
encode_height_ = positiveOrDefault(encoder.height(), height_);
codec_ = encoder.codec().empty() ? "H264" : encoder.codec();
enable_stream_timestamp_ = encoder.enable_stream_timestamp();
consume_new_frame_only_ = config_.consume_new_frame_only();
}
MujocoCamera::~MujocoCamera()
{
stop();
}
bool MujocoCamera::init()
{
std::lock_guard<std::mutex> lock(mtx_);
if (fetch_rgbd_fn_) {
state_.is_initialized = true;
state_.is_opened = true;
state_.fps = positiveOrDefault(config_.render().fps(), 30);
state_.width = width_;
state_.height = height_;
return true;
}
if (id_.empty()) {
setError_("[MujocoCamera] id is empty");
return false;
}
if (config_.world_id().empty()) {
setError_("[MujocoCamera] world_id is empty: " + id_);
return false;
}
if (config_.camera_name().empty()) {
setError_("[MujocoCamera] camera_name is empty: " + id_);
return false;
}
auto world = simulate::MujocoWorldDevice::worldFor(config_.world_id());
if (!world) {
world = MotorManager::mujocoWorldFor(config_.world_id());
}
if (!world) {
setError_("[MujocoCamera] MuJoCo world not found: " + config_.world_id());
return false;
}
if (!world->isLoaded()) {
setError_("[MujocoCamera] MuJoCo world is not loaded: " + config_.world_id());
return false;
}
world_ = world;
{
std::lock_guard<std::mutex> world_lock(world->mutex());
const mjModel* model = world->model();
if (model == nullptr) {
setError_("[MujocoCamera] world model is null: " + config_.world_id());
return false;
}
camera_id_ = mj_name2id(model, mjOBJ_CAMERA, config_.camera_name().c_str());
if (camera_id_ < 0) {
setError_("[MujocoCamera] camera not found in MuJoCo model: " + config_.camera_name());
return false;
}
fovy_deg_ = model->cam_fovy[camera_id_];
}
if (!initOffscreen_()) {
return false;
}
state_.is_initialized = true;
state_.is_opened = true;
state_.fps = positiveOrDefault(config_.render().fps(), 30);
state_.width = width_;
state_.height = height_;
clear_error_();
CMVR_LOG(INFO) << "[MujocoCamera] initialized, id=" << id_
<< ", world_id=" << config_.world_id()
<< ", camera=" << config_.camera_name()
<< ", size=" << width_ << "x" << height_;
return true;
}
bool MujocoCamera::start()
{
if (!state_.is_initialized) {
if (!init()) {
return false;
}
}
std::lock_guard<std::mutex> lock(mtx_);
auto world = world_.lock();
if (world && !world->isRunning() && !world->start()) {
setError_("[MujocoCamera] failed to start MuJoCo world: " + world->lastError());
return false;
}
state_.is_streaming = true;
state_.is_opened = true;
return true;
}
bool MujocoCamera::stop()
{
std::lock_guard<std::mutex> lock(mtx_);
state_.is_streaming = false;
state_.is_opened = false;
destroyOffscreen_();
return true;
}
void MujocoCamera::setFetchRgbdFn(FetchRgbdFn fetch_rgbd_fn)
{
std::lock_guard<std::mutex> lock(mtx_);
fetch_rgbd_fn_ = std::move(fetch_rgbd_fn);
if (fetch_rgbd_fn_) {
destroyOffscreen_();
state_.is_initialized = true;
state_.is_opened = true;
state_.fps = positiveOrDefault(config_.render().fps(), 30);
state_.width = width_;
state_.height = height_;
clear_error_();
}
}
void MujocoCamera::setFovyDeg(const double fovy_deg)
{
std::lock_guard<std::mutex> lock(mtx_);
fovy_deg_ = fovy_deg;
}
void MujocoCamera::setConsumeNewFrameOnly(bool enable) {
void MujocoCamera::setConsumeNewFrameOnly(const bool enable)
{
std::lock_guard<std::mutex> lock(mtx_);
consume_new_frame_only_ = enable;
}
void MujocoCamera::getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) {
void MujocoCamera::getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics)
{
cv::Mat depth;
if (!fetch(color, depth, intrinsics)) {
color.release();
}
}
void MujocoCamera::getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) {
void MujocoCamera::getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics)
{
cv::Mat color;
if (!fetch(color, depth, intrinsics)) {
depth.release();
}
}
void MujocoCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics) {
void MujocoCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics)
{
if (!fetch(color, depth, intrinsics)) {
color.release();
depth.release();
}
}
bool MujocoCamera::fetch(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics) {
bool MujocoCamera::startStreaming()
{
if (!state_.is_initialized && !init()) {
return false;
}
std::lock_guard<std::mutex> lock(mtx_);
if (!fetch_rgbd_fn_) return false;
streaming_ = true;
state_.is_streaming = true;
return true;
}
void MujocoCamera::stopStreaming()
{
std::lock_guard<std::mutex> lock(mtx_);
streaming_ = false;
state_.is_streaming = false;
stream_frame_index_ = 0;
rgb_encoder_.reset();
}
bool MujocoCamera::getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index)
{
if (!streaming_) {
return false;
}
cv::Mat color;
cv::Mat depth;
Rs2Intrinsics intrinsics{};
if (!fetch(color, depth, intrinsics) || color.empty()) {
return false;
}
cv::Mat color_to_encode = color;
if (encode_width_ > 0 && encode_height_ > 0 &&
(color.cols != encode_width_ || color.rows != encode_height_)) {
cv::resize(color, color_to_encode, cv::Size(encode_width_, encode_height_), 0.0, 0.0, cv::INTER_LINEAR);
}
const int fps = positiveOrDefault(config_.encoder().fps(), positiveOrDefault(config_.render().fps(), 30));
if (!ensureEncoder_(color_to_encode.cols, color_to_encode.rows, fps)) {
return false;
}
frame_data.rgbImage = color.clone();
CameraStreamEncodeOptions encode_options;
encode_options.draw_timestamp = enable_stream_timestamp_;
if (!CameraStreamEncoder::encode(rgb_encoder_,
color_to_encode,
frame_data.rgbFrame,
frame_data.bKey,
encode_options)) {
return false;
}
if (!depth.empty()) {
frame_data.depthImage = depth.clone();
const auto* depth_begin = reinterpret_cast<const uint8_t*>(depth.data);
const auto* depth_end = depth_begin + depth.total() * depth.elemSize();
frame_data.depthFrame.assign(depth_begin, depth_end);
}
frame_data.intrinsics = intrinsics;
frame_data.width = color_to_encode.cols;
frame_data.height = color_to_encode.rows;
frame_data.fps = fps;
frame_data.codec = codec_;
frame_data.depthKey = true;
next_index = ++stream_frame_index_;
return true;
}
bool MujocoCamera::fetch(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics)
{
std::lock_guard<std::mutex> lock(mtx_);
if (fetch_rgbd_fn_) {
std::vector<unsigned char> rgb_raw;
std::vector<float> depth_raw;
int width = 0;
int height = 0;
uint64_t frame_id = 0;
std::uint64_t frame_id = 0;
if (!fetch_rgbd_fn_(rgb_raw, depth_raw, width, height, frame_id)) {
return false;
}
if (width <= 0 || height <= 0) return false;
if ((int)rgb_raw.size() != width * height * 3) return false;
if (!depth_raw.empty() && (int)depth_raw.size() != width * height) return false;
if (consume_new_frame_only_) {
if (has_last_frame_id_ && frame_id == last_frame_id_) {
if (width <= 0 || height <= 0) {
return false;
}
if (static_cast<int>(rgb_raw.size()) != width * height * 3) {
return false;
}
if (!depth_raw.empty() && static_cast<int>(depth_raw.size()) != width * height) {
return false;
}
if (consume_new_frame_only_ && has_last_frame_id_ && frame_id == last_frame_id_) {
return false;
}
last_frame_id_ = frame_id;
has_last_frame_id_ = true;
cv::Mat rgb(height, width, CV_8UC3, rgb_raw.data());
color = rgb.clone();
cv::cvtColor(rgb, color, cv::COLOR_RGB2BGR);
if (!depth_raw.empty()) {
cv::Mat dep(height, width, CV_32FC1, depth_raw.data());
depth = dep.clone();
} else {
depth.release();
}
fillIntrinsics(width, height, intrinsics);
return true;
}
return renderOffscreen_(color, depth, intrinsics);
}
bool MujocoCamera::initOffscreen_()
{
if (window_ != nullptr && context_initialized_ && scene_initialized_) {
return true;
}
if (!glfwInit()) {
setError_("[MujocoCamera] glfwInit failed");
return false;
}
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_FALSE);
window_ = glfwCreateWindow(width_, height_, ("mujoco_camera_" + id_).c_str(), nullptr, nullptr);
if (window_ == nullptr) {
setError_("[MujocoCamera] glfwCreateWindow failed");
return false;
}
glfwMakeContextCurrent(window_);
mjv_defaultCamera(&camera_);
mjv_defaultOption(&option_);
mjv_defaultPerturb(&perturb_);
mjv_defaultScene(&scene_);
mjr_defaultContext(&context_);
auto world = world_.lock();
if (!world) {
setError_("[MujocoCamera] world expired");
return false;
}
std::lock_guard<std::mutex> world_lock(world->mutex());
const mjModel* model = world->model();
if (model == nullptr) {
setError_("[MujocoCamera] world model is null");
return false;
}
mjv_makeScene(model, &scene_, kMaxGeom);
scene_initialized_ = true;
mjr_makeContext(model, &context_, mjFONTSCALE_150);
context_initialized_ = true;
mjr_setBuffer(mjFB_OFFSCREEN, &context_);
if (context_.currentBuffer != mjFB_OFFSCREEN) {
setError_("[MujocoCamera] MuJoCo offscreen buffer is not available");
return false;
}
return true;
}
void MujocoCamera::fillIntrinsics(int width, int height, Rs2Intrinsics& intrinsics) const {
void MujocoCamera::destroyOffscreen_()
{
if (window_ != nullptr) {
glfwMakeContextCurrent(window_);
}
if (context_initialized_) {
mjr_freeContext(&context_);
context_initialized_ = false;
}
if (scene_initialized_) {
mjv_freeScene(&scene_);
scene_initialized_ = false;
}
if (window_ != nullptr) {
glfwDestroyWindow(window_);
window_ = nullptr;
}
}
bool MujocoCamera::renderOffscreen_(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics)
{
if (!state_.is_initialized) {
setError_("[MujocoCamera] camera is not initialized: " + id_);
return false;
}
if (!initOffscreen_()) {
return false;
}
auto world = world_.lock();
if (!world) {
setError_("[MujocoCamera] world expired");
return false;
}
glfwMakeContextCurrent(window_);
std::vector<unsigned char> rgb(static_cast<std::size_t>(width_) * height_ * 3);
std::vector<float> depth_raw(static_cast<std::size_t>(width_) * height_);
{
std::lock_guard<std::mutex> world_lock(world->mutex());
mjModel* model = world->model();
mjData* data = world->data();
if (model == nullptr || data == nullptr) {
setError_("[MujocoCamera] world model/data is null");
return false;
}
camera_.type = mjCAMERA_FIXED;
camera_.fixedcamid = camera_id_;
camera_.trackbodyid = -1;
mjrRect viewport;
viewport.left = 0;
viewport.bottom = 0;
viewport.width = width_;
viewport.height = height_;
mjv_updateScene(model, data, &option_, &perturb_, &camera_, mjCAT_ALL, &scene_);
mjr_render(viewport, &scene_, &context_);
mjr_readPixels(rgb.data(), depth_raw.data(), viewport, &context_);
flipRgbAndDepth_(rgb, depth_raw, width_, height_);
linearizeDepth_(model, depth_raw);
}
cv::Mat rgb_mat(height_, width_, CV_8UC3, rgb.data());
color = rgb_mat.clone();
cv::Mat depth_mat(height_, width_, CV_32FC1, depth_raw.data());
depth = depth_mat.clone();
fillIntrinsics(width_, height_, intrinsics);
++last_frame_id_;
has_last_frame_id_ = true;
clear_error_();
return true;
}
bool MujocoCamera::ensureEncoder_(const int width, const int height, const int fps)
{
if (rgb_encoder_ &&
rgb_encoder_->width == width &&
rgb_encoder_->height == height &&
rgb_encoder_->fps == fps &&
rgb_encoder_->codec_name == codec_) {
return true;
}
rgb_encoder_.reset();
return CameraStreamEncoder::init(rgb_encoder_, codec_, width, height, fps);
}
void MujocoCamera::fillIntrinsics(const int width, const int height, Rs2Intrinsics& intrinsics) const
{
const double fovy = fovy_deg_ * M_PI / 180.0;
const double fy = (height * 0.5) / std::tan(fovy * 0.5);
const double fx = fy;
@ -95,4 +468,56 @@ void MujocoCamera::fillIntrinsics(int width, int height, Rs2Intrinsics& intrinsi
}
}
void MujocoCamera::setError_(const std::string& error)
{
state_.is_error = true;
state_.error_message = error;
CMVR_LOG(ERROR) << error;
}
void MujocoCamera::flipRgbAndDepth_(std::vector<unsigned char>& rgb,
std::vector<float>& depth,
const int width,
const int height)
{
for (int row = 0; row < height / 2; ++row) {
auto* rgb_top = rgb.data() + 3 * width * row;
auto* rgb_bottom = rgb.data() + 3 * width * (height - 1 - row);
std::swap_ranges(rgb_top, rgb_top + 3 * width, rgb_bottom);
auto* depth_top = depth.data() + width * row;
auto* depth_bottom = depth.data() + width * (height - 1 - row);
std::swap_ranges(depth_top, depth_top + width, depth_bottom);
}
}
void MujocoCamera::linearizeDepth_(const mjModel* model, std::vector<float>& depth)
{
if (model == nullptr) {
return;
}
const double znear = static_cast<double>(model->vis.map.znear) *
static_cast<double>(model->stat.extent);
const double zfar = static_cast<double>(model->vis.map.zfar) *
static_cast<double>(model->stat.extent);
if (znear <= 0.0 || zfar <= znear) {
return;
}
const double two_nf = 2.0 * znear * zfar;
const double f_plus_n = zfar + znear;
const double f_minus_n = zfar - znear;
for (float& item : depth) {
if (!std::isfinite(item) || item <= 0.0f || item >= 1.0f) {
item = std::numeric_limits<float>::infinity();
continue;
}
const double z_ndc = 2.0 * static_cast<double>(item) - 1.0;
const double denom = f_plus_n - z_ndc * f_minus_n;
item = denom <= 1e-12
? std::numeric_limits<float>::infinity()
: static_cast<float>(two_nf / denom);
}
}
} // namespace cmvr::device

View File

@ -0,0 +1,56 @@
#include <gtest/gtest.h>
#include "devices/camera/mujoco_camera/include/mujoco_camera.h"
#include "simulate/mujoco/mujoco_world/include/mujoco_world.h"
namespace {
TEST(MujocoCameraTest, CapturesOffscreenRgbdFrame)
{
cmvr::config::MujocoWorldConfig world_config;
world_config.set_id("mujoco_camera_test_world");
world_config.set_model_path("model/xiaoyan_description/dual_arm.xml");
world_config.set_timestep_s(0.001);
world_config.set_realtime_factor(1.0);
world_config.set_require_actuator(false);
auto world_device = std::make_shared<cmvr::simulate::MujocoWorldDevice>(world_config);
ASSERT_TRUE(world_device->init());
ASSERT_TRUE(world_device->start());
cmvr::config::MujocoCameraConfig camera_config;
camera_config.set_id("mujoco_camera_test_cam");
camera_config.set_world_id(world_config.id());
camera_config.set_camera_name("hand_cam");
auto* render = camera_config.mutable_render();
render->set_width(320);
render->set_height(240);
render->set_fps(30);
render->set_stream_mode(cmvr::config::STREAM_MODE_RGBD);
auto* encoder = camera_config.mutable_encoder();
encoder->set_width(320);
encoder->set_height(240);
encoder->set_fps(30);
encoder->set_codec("H264");
encoder->set_enable_stream_timestamp(true);
cmvr::device::MujocoCamera camera(camera_config);
ASSERT_TRUE(camera.init());
ASSERT_TRUE(camera.start());
cv::Mat color;
cv::Mat depth;
cmvr::device::Rs2Intrinsics intrinsics{};
camera.getRGBDImages(color, depth, intrinsics);
EXPECT_EQ(color.cols, 320);
EXPECT_EQ(color.rows, 240);
EXPECT_EQ(color.type(), CV_8UC3);
EXPECT_EQ(depth.cols, 320);
EXPECT_EQ(depth.rows, 240);
EXPECT_EQ(depth.type(), CV_32FC1);
EXPECT_GT(intrinsics.fx, 0.0f);
EXPECT_GT(intrinsics.fy, 0.0f);
}
} // namespace

View File

@ -5,6 +5,7 @@ target_include_directories(realsense_camera PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_library(cmvr_es::device::realsense_camera ALIAS realsense_camera) # OK
# librealsense2
target_link_libraries(realsense_camera PUBLIC cmvr_es::device::camera_stream_encoder)
target_link_libraries(realsense_camera PRIVATE realsense2 opencv_core opencv_imgproc opencv_videoio cmvr_es::proto)
target_link_libraries(realsense_camera PRIVATE
realsense2

View File

@ -5,7 +5,9 @@
#ifndef REALSENSE_CAMERA_H
#define REALSENSE_CAMERA_H
#include "../../uvc_camera/include/uvc_camera.h"
#include "camera/abstract_camera.h"
#include "common/base/ring_buffer.h"
#include "devices/camera/common/include/camera_stream_encoder.h"
#include <librealsense2/rs.hpp>
#include <librealsense2/hpp/rs_internal.hpp>
@ -30,11 +32,6 @@ namespace cmvr::device{
void pauseRecording() override;
void resumeRecording() override;
// 初始化单个编码器的通用函数(复用逻辑,避免重复代码)
static bool initSingleEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const std::string& codec_name,
int width, int height, int fps);
static bool encodeFrameWithEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder,const cv::Mat& frame,std::vector<uint8_t>& encoded_frame,bool& is_key);
void getEncodedFrame(StreamFrameData& frame_data, size_t& index) override;
bool getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index) override;
@ -58,6 +55,7 @@ namespace cmvr::device{
cv::VideoCapture cap_;
size_t buffer_size_;
std::string codec_;
bool enable_stream_timestamp_{false};
CameraMode mode_;
StreamMode stream_mode_;

View File

@ -76,12 +76,15 @@ RealsenseCamera::RealsenseCamera(const config::RealSenseCameraConfig& camera):ca
state_.error_message = "empty device serial number";
return;
}
fps_ = camera_.fps();
width_ = camera_.width();
height_ = camera_.height();
encode_width_ = camera_.encode_width() > 0 ? camera_.encode_width() : width_;
encode_height_ = camera_.encode_height() > 0 ? camera_.encode_height() : height_;
buffer_size_ = camera_.buffer_size();
const auto& capture = camera_.capture();
const auto& encoder = camera_.encoder();
fps_ = capture.fps();
width_ = capture.width();
height_ = capture.height();
encode_width_ = encoder.width() > 0 ? encoder.width() : width_;
encode_height_ = encoder.height() > 0 ? encoder.height() : height_;
buffer_size_ = encoder.buffer_size() > 0 ? encoder.buffer_size() : 30;
enable_stream_timestamp_ = encoder.enable_stream_timestamp();
state_.fps = fps_;
state_.width = width_;
@ -100,7 +103,7 @@ RealsenseCamera::RealsenseCamera(const config::RealSenseCameraConfig& camera):ca
return;
}
auto stream_mode = camera_.stream_mode();
auto stream_mode = capture.stream_mode();
if (stream_mode == config::STREAM_MODE_RGB)
{
stream_mode_ = COLOR_MODE;
@ -126,7 +129,7 @@ RealsenseCamera::RealsenseCamera(const config::RealSenseCameraConfig& camera):ca
align_mode_ = "color";
}
codec_ = camera_.codec();
codec_ = encoder.codec().empty() ? "H264" : encoder.codec();
}
RealsenseCamera::~RealsenseCamera() {
@ -171,7 +174,7 @@ bool RealsenseCamera::init() {
//初始化编码器
// 初始化RGB编码器示例参数640x48030fpsH.264
if (!initSingleEncoder(rgbEncoder_, codec_, encode_width_, encode_height_, fps_)) {
if (!CameraStreamEncoder::init(rgbEncoder_, codec_, encode_width_, encode_height_, fps_)) {
CMVR_LOG(ERROR) << "[RealsenseCamera] (start): Failed to init RGB encoder!";
state_.is_initialized = false;
state_.is_error = true;
@ -767,7 +770,13 @@ void RealsenseCamera::streaming_worker_() {
0.0,
cv::INTER_LINEAR);
}
success = encodeFrameWithEncoder(rgbEncoder_, rgb_to_encode, frame_data.rgbFrame, frame_data.bKey);
CameraStreamEncodeOptions encode_options;
encode_options.draw_timestamp = enable_stream_timestamp_;
success = CameraStreamEncoder::encode(rgbEncoder_,
rgb_to_encode,
frame_data.rgbFrame,
frame_data.bKey,
encode_options);
// 深度图编码
// success = encodeFrameWithEncoder(depthEncoder_, frame_data.depthImage, frame_data.depthFrame, frame_data.depthKey);
if (success) {
@ -890,297 +899,6 @@ void RealsenseCamera::recording_worker_() {
state_.is_recording = false;
}
// 初始化单个编码器的通用函数
bool RealsenseCamera::initSingleEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const std::string& codec_name,
int width, int height, int fps) {
// 1. 创建编码器实例(不变)
encoder = std::make_shared<FfmpegEncoderInfo>();
encoder->codec_name = codec_name;
encoder->width = width;
encoder->height = height;
encoder->fps = fps;
// 2. 查找编码器(不变)
const AVCodec* codec = nullptr;
if (codec_name == "h264" || codec_name == "H264") {
codec = avcodec_find_encoder_by_name("libx264");
if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_H264);
} else if (codec_name == "h265" || codec_name == "HEVC" || codec_name == "H265") {
codec = avcodec_find_encoder_by_name("libx265");
if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
}
if (!codec) {
CMVR_LOG(ERROR) << "Failed to find " << codec_name << " encoder!";
return false;
}
// 3. 初始化编码器上下文(不变)
encoder->codec_context = avcodec_alloc_context3(codec);
if (!encoder->codec_context) {
CMVR_LOG(ERROR) << "Failed to allocate codec context!";
return false;
}
// 4. 设置编码器参数(不变)
AVCodecContext* ctx = encoder->codec_context;
ctx->codec_type = AVMEDIA_TYPE_VIDEO;
ctx->width = (width + 1) & ~1; // 确保宽度为偶数
ctx->height = (height + 1) & ~1;// 确保高度为偶数
ctx->time_base = {1, fps}; // 时间基1/fps
ctx->framerate = {fps, 1}; // 帧率
ctx->max_b_frames = 0; // 禁用B帧降低延迟
ctx->gop_size = 10;//I帧间隔
// 5. 设置编码器私有参数
if (codec->id == AV_CODEC_ID_H264) {
av_opt_set(ctx->priv_data, "preset", "ultrafast", 0);
av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);
av_opt_set(ctx->priv_data, "profile", "baseline", 0);
av_opt_set(ctx->priv_data, "repeat-headers", "1", 0);
av_opt_set(ctx->priv_data, "annexb", "1", 0);
} else if (codec->id == AV_CODEC_ID_HEVC) {
// 降低 x265 控制台日志噪声(如 "encoded 0 frames")。
av_opt_set(ctx->priv_data, "x265-params", "log-level=none", 0);
// H.265 的强制设置
// 使用 x265-params 字符串设置所有参数
char x265_params[256];
snprintf(x265_params, sizeof(x265_params),
"keyint=%d:" // 关键帧间隔
"min-keyint=%d:" // 最小关键帧间隔
"no-open-gop=1:" // 禁用开放GOP
"bframes=0:" // 禁用B帧
"rc-lookahead=0:"
"log-level=none", // 日志级别
10, 10); // 设置keyint和min-keyint为10
av_opt_set(ctx->priv_data, "x265-params", x265_params, 0);
// 或者分开设置(如果支持)
av_opt_set_int(ctx->priv_data, "keyint", 10, 0);
av_opt_set_int(ctx->priv_data, "min-keyint", 10, 0);
av_opt_set(ctx->priv_data, "no-open-gop", "1", 0);
}
// 6. 设置像素格式(不变)
const enum AVPixelFormat* pix_fmts = codec->pix_fmts;
if (!pix_fmts) {
CMVR_LOG(INFO) << "Using default pixel format: YUV420P";
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
} else {
ctx->pix_fmt = pix_fmts[0];
CMVR_LOG(INFO) << "Selected pixel format: " << av_get_pix_fmt_name(ctx->pix_fmt);
}
// 7. 打开编码器(不变)
int ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Failed to open " << codec_name << " encoder: " << errbuf;
return false;
}
// 8. 分配AVFrame不变
encoder->frame = av_frame_alloc();
if (!encoder->frame) {
CMVR_LOG(ERROR) << "Failed to allocate AVFrame!";
return false;
}
encoder->frame->format = ctx->pix_fmt;
encoder->frame->width = ctx->width;
encoder->frame->height = ctx->height;
if (av_frame_get_buffer(encoder->frame, 0) < 0) {
CMVR_LOG(ERROR) << "Failed to allocate AVFrame buffer!";
av_frame_free(&encoder->frame);
return false;
}
// 9. 分配AVPacket不变
encoder->packet = av_packet_alloc();
if (!encoder->packet) {
CMVR_LOG(ERROR) << "Failed to allocate AVPacket!";
return false;
}
// 添加调试信息,确认设置生效
CMVR_LOG(INFO) << "Encoder settings:";
CMVR_LOG(INFO) << " GOP size: " << ctx->gop_size;
if (codec->id == AV_CODEC_ID_HEVC) {
char* params = nullptr;
if (av_opt_get(ctx->priv_data, "x265-params", 0, (uint8_t**)&params) >= 0) {
CMVR_LOG(INFO) << " x265-params: " << params;
av_free(params);
}
}
CMVR_LOG(INFO) << "Successfully initialized " << codec_name << " encoder ( "
<< width << "x" << height << "@" << fps << "fps )";
return true;
}
// 获取当前时间并格式化为字符串
std::string getCurrentTimeString() {
// 获取当前时间(精确到毫秒)
auto now = std::chrono::system_clock::now();
// 转换为秒级时间
auto now_sec = std::chrono::time_point_cast<std::chrono::seconds>(now);
std::time_t now_time = std::chrono::system_clock::to_time_t(now_sec);
std::tm* tm_ptr = std::localtime(&now_time);
// 计算毫秒部分
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now - now_sec
).count();
// 格式化时间字符串
std::stringstream ss;
ss << std::put_time(tm_ptr, "%Y-%m-%d %H:%M:%S")
<< "." << std::setw(3) << std::setfill('0') << ms;
return ss.str();
}
// 在cv::Mat右上角绘制时间戳
void drawTimeStamp(cv::Mat& image) {
if (image.empty()) return;
std::string time_str = getCurrentTimeString();
cv::Point text_pos;
cv::Scalar text_color(255, 255, 255); // 白色文字
int font_face = cv::FONT_HERSHEY_SIMPLEX;
double font_scale = 0.8;
int thickness = 2;
// 计算文本尺寸,用于确定右上角位置
int baseline = 0;
cv::Size text_size = cv::getTextSize(time_str, font_face, font_scale, thickness, &baseline);
// 设置右上角坐标留出10像素边距
text_pos.x = image.cols - text_size.width - 10;
text_pos.y = text_size.height + 10;
// 绘制文字(先画黑色背景增加可读性)
cv::putText(image, time_str, text_pos, font_face, font_scale, cv::Scalar(0, 0, 0), thickness + 2);
cv::putText(image, time_str, text_pos, font_face, font_scale, text_color, thickness);
}
bool RealsenseCamera::encodeFrameWithEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder, const cv::Mat& frame,std::vector<uint8_t>& encoded_frame,bool& is_key) {
if (!encoder || !encoder->codec_context || !encoder->frame || !encoder->packet) {
return false;
}
// 确保输入帧尺寸匹配(不变)
if (frame.cols != encoder->width || frame.rows != encoder->height) {
CMVR_LOG(ERROR) << "Frame size does not match encoder dimensions";
return false;
}
cv::Mat dateImage;
frame.copyTo(dateImage);
drawTimeStamp(dateImage);
// 【修复3设置递增的PTS确保编码器正确处理I帧请求】
encoder->frame->pts = encoder->frame_pts++; // 分配唯一PTS
// 根据cv::Mat的类型设置源格式不变
AVPixelFormat src_pix_fmt;
if (dateImage.channels() == 3) {
src_pix_fmt = AV_PIX_FMT_BGR24; // OpenCV默认BGR
} else if (dateImage.channels() == 4) {
src_pix_fmt = AV_PIX_FMT_BGRA; // 4通道为BGRA
} else if (dateImage.channels() == 1) {
src_pix_fmt = AV_PIX_FMT_GRAY8; // 单通道灰度图
} else {
CMVR_LOG(ERROR) << "Unsupported number of channels: " << dateImage.channels();
return false;
}
// 【修复4动态创建sws_context匹配当前输入格式
if (encoder->sws_context) {
sws_freeContext(encoder->sws_context); // 释放旧上下文
}
encoder->sws_context = sws_getContext(
dateImage.cols, dateImage.rows, src_pix_fmt, // 输入格式由当前frame决定
encoder->codec_context->width, encoder->codec_context->height, encoder->codec_context->pix_fmt,
SWS_BILINEAR, nullptr, nullptr, nullptr
);
if (!encoder->sws_context) {
CMVR_LOG(ERROR) << "Failed to create SwsContext";
return false;
}
// 转换输入帧格式为编码器所需格式(不变)
const uint8_t* src_data[AV_NUM_DATA_POINTERS] = {dateImage.data};
int src_linesize[AV_NUM_DATA_POINTERS] = {static_cast<int>(dateImage.step)};
int ret = sws_scale(encoder->sws_context, src_data, src_linesize, 0, dateImage.rows,
encoder->frame->data, encoder->frame->linesize);
if (ret < 0) {
CMVR_LOG(ERROR) << "Error scaling frame";
return false;
}
// 发送帧到编码器(不变)
ret = avcodec_send_frame(encoder->codec_context, encoder->frame);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Error sending frame to encoder: " << errbuf;
return false;
}
while (true)
{
// 接收编码后的数据(不变)
ret = avcodec_receive_packet(encoder->codec_context, encoder->packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Error receiving packet from encoder: " << errbuf;
break;
}
// 调试打印帧类型I帧/P帧
if (encoder->packet->flags & AV_PKT_FLAG_KEY) {
is_key = true;
//CMVR_LOG(INFO) << "Encoded I frame (size: " << encoder->packet->size << " bytes)";
} else {
is_key = false;
//CMVR_LOG(INFO) << "Encoded P frame (size: " << encoder->packet->size << " bytes)";
}
// 预留足够空间,避免多次内存分配
encoded_frame.reserve(encoded_frame.size() + encoder->packet->size);
// 复制数据包内容到输出向量
encoded_frame.insert(encoded_frame.end(),
encoder->packet->data,
encoder->packet->data + encoder->packet->size);
av_packet_unref(encoder->packet);
}
// 验证帧有效性NALU起始码、元数据
if (!encoded_frame.empty()) {
// 检查NALU起始码
bool has_start_code = false;
if ((encoded_frame[0] == 0 && encoded_frame[1] == 0 && encoded_frame[2] == 1) ||
(encoded_frame[0] == 0 && encoded_frame[1] == 0 && encoded_frame[2] == 0 && encoded_frame[3] == 1)) {
has_start_code = true;
}
if (!has_start_code) {
CMVR_LOG(ERROR) << "Invalid frame: no NALU start code!";
return false;
}
} else {
//CMVR_LOG(ERROR) << "Encoded frame is empty!";
return false;
}
return true;
}
void RealsenseCamera::getEncodedFrame(StreamFrameData& frame_data, size_t& index) {
// 环形队列取数据的index由接口传入
auto frame = stream_frame_buffer_->pop(index);

View File

@ -70,14 +70,20 @@ cmvr::config::RealSenseCameraConfig makeRsConfig() {
cmvr::config::RealSenseCameraConfig cfg;
cfg.set_id("realsense_health_check");
cfg.set_serialnumber(kRsSerialForHealthCheck);
cfg.set_width(kRsWidth);
cfg.set_height(kRsHeight);
cfg.set_fps(kRsFps);
cfg.set_codec("H265");
cfg.set_camera_mode(cmvr::config::CAMERA_MODE_PHOTO);
cfg.set_stream_mode(cmvr::config::STREAM_MODE_RGBD);
auto* capture = cfg.mutable_capture();
capture->set_width(kRsWidth);
capture->set_height(kRsHeight);
capture->set_fps(kRsFps);
capture->set_stream_mode(cmvr::config::STREAM_MODE_RGBD);
auto* encoder = cfg.mutable_encoder();
encoder->set_width(kRsWidth);
encoder->set_height(kRsHeight);
encoder->set_fps(kRsFps);
encoder->set_codec("H265");
encoder->set_enable_stream_timestamp(true);
encoder->set_buffer_size(30);
cfg.set_align_mode(cmvr::config::ALIGN_MODE_COLOR);
cfg.set_buffer_size(30);
cfg.set_sync(true);
return cfg;
}

View File

@ -2,7 +2,7 @@ add_library(uvc_camera SHARED src/uvc_camera.cpp)
target_include_directories(uvc_camera PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(uvc_camera PUBLIC glog opencv_core opencv_imgproc cmvr_es::proto)
target_link_libraries(uvc_camera PUBLIC glog opencv_core opencv_imgproc cmvr_es::proto cmvr_es::device::camera_stream_encoder)
add_library(cmvr_es::device::uvc_camera ALIAS uvc_camera)
install(TARGETS uvc_camera LIBRARY DESTINATION lib)

View File

@ -7,17 +7,9 @@
#include "common/base/ring_buffer.h"
#include "camera/abstract_camera.h"
//使用ffmpeg来编码保存视频文件
#define USE_FFMPEG_ENCODER 1
#if USE_FFMPEG_ENCODER
#include "speaker/ffmpeg_speaker/include/ffmpeg_ptr.h"
#endif
#include "devices/camera/common/include/camera_stream_encoder.h"
namespace cmvr::device {
enum CameraMode {PHOTO_MODE, VIDEO_MODE};
struct ImageData
{
@ -27,45 +19,6 @@ namespace cmvr::device {
cv::Mat depthImage;
};
struct FfmpegEncoderInfo {
std::string codec_name; // 编码器名称(如"h264"、"hevc"
int width = 0; // 图像宽度
int height = 0; // 图像高度
int fps = 0; // 帧率
int64_t frame_pts = 0;
bool bRunning = false; // 是否进行编码
AVCodecContext* codec_context = nullptr; // 编码器上下文
AVFrame* frame = nullptr; // 输入帧
AVPacket* packet = nullptr; // 输出包
SwsContext* sws_context = nullptr; // 图像转换上下文(如果需要格式转换)
// 析构函数释放FFmpeg资源核心避免内存泄漏
~FfmpegEncoderInfo() {
// 释放编码帧
if (frame) {
av_frame_free(&frame);
frame = nullptr;
}
// 释放编码包
if (packet) {
av_packet_free(&packet);
packet = nullptr;
}
// 释放编码器上下文
if (codec_context) {
avcodec_close(codec_context); // 关闭编码器
avcodec_free_context(&codec_context); // 释放上下文
codec_context = nullptr;
}
// 释放格式转换上下文
if (sws_context) {
sws_freeContext(sws_context);
sws_context = nullptr;
}
//std::cout << "FfmpegEncoderInfo resources released." << std::endl;
}
};
//USB摄像机
class UVCCamera final : public AbstractCamera {
public:
@ -83,11 +36,6 @@ namespace cmvr::device {
void stopRecording() override;
void pauseRecording() override;
void resumeRecording() override;
// 初始化单个编码器的通用函数(复用逻辑,避免重复代码)
static bool initSingleEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const std::string& codec_name,
int width, int height, int fps);
static bool encodeFrameWithEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder,const cv::Mat& frame,std::vector<uint8_t>& encoded_frame,bool& is_key);
void getEncodedFrame(StreamFrameData& frame_data, size_t& index) override;
bool getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index) override;
@ -106,6 +54,7 @@ namespace cmvr::device {
cv::VideoCapture cap_;
size_t buffer_size_;
std::string codec_;
bool enable_stream_timestamp_{false};
CameraMode mode_;
// std::shared_ptr<cv::Mat> current_image_;

View File

@ -20,12 +20,15 @@ UVCCamera::UVCCamera(const config::UVCCameraConfig& camera):camera_(camera)
state_.error_message = "empty device serial number";
return;
}
fps_ = camera_.fps();
width_ = camera_.width();
height_ = camera_.height();
encode_width_ = camera_.encode_width() > 0 ? camera_.encode_width() : width_;
encode_height_ = camera_.encode_height() > 0 ? camera_.encode_height() : height_;
buffer_size_ = camera_.buffer_size();
const auto& capture = camera_.capture();
const auto& encoder = camera_.encoder();
fps_ = capture.fps();
width_ = capture.width();
height_ = capture.height();
encode_width_ = encoder.width() > 0 ? encoder.width() : width_;
encode_height_ = encoder.height() > 0 ? encoder.height() : height_;
buffer_size_ = encoder.buffer_size() > 0 ? encoder.buffer_size() : 30;
enable_stream_timestamp_ = encoder.enable_stream_timestamp();
state_.fps = fps_;
state_.width = width_;
@ -44,7 +47,7 @@ UVCCamera::UVCCamera(const config::UVCCameraConfig& camera):camera_(camera)
return;
}
codec_ = camera_.codec();
codec_ = encoder.codec().empty() ? "H264" : encoder.codec();
}
UVCCamera::~UVCCamera() {
stop();
@ -100,7 +103,7 @@ bool UVCCamera::init() {
}
//初始化编码器
// 初始化RGB编码器示例参数640x48030fpsH.264
if (!initSingleEncoder(rgbEncoder_, codec_, encode_width_, encode_height_, fps_)) {
if (!CameraStreamEncoder::init(rgbEncoder_, codec_, encode_width_, encode_height_, fps_)) {
CMVR_LOG(ERROR) << "[UVCCamera] (start): Failed to init RGB encoder!";
state_.is_error = true;
state_.error_message = "Failed to init RGB encoder!";
@ -499,7 +502,13 @@ void UVCCamera::streaming_worker_() {
0.0,
cv::INTER_LINEAR);
}
success = encodeFrameWithEncoder(rgbEncoder_, rgb_to_encode, frame_data.rgbFrame, frame_data.bKey);
CameraStreamEncodeOptions encode_options;
encode_options.draw_timestamp = enable_stream_timestamp_;
success = CameraStreamEncoder::encode(rgbEncoder_,
rgb_to_encode,
frame_data.rgbFrame,
frame_data.bKey,
encode_options);
if (success) {
frame_data.fps = fps_;
frame_data.width = encode_width_;
@ -621,216 +630,6 @@ void UVCCamera::recording_worker_() {
state_.is_recording = false;
}
// 初始化单个编码器的通用函数
bool UVCCamera::initSingleEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const std::string& codec_name,
int width, int height, int fps) {
// 1. 创建编码器实例(不变)
encoder = std::make_shared<FfmpegEncoderInfo>();
encoder->codec_name = codec_name;
encoder->width = width;
encoder->height = height;
encoder->fps = fps;
// 2. 查找编码器(不变)
const AVCodec* codec = nullptr;
if (codec_name == "h264" || codec_name == "H264") {
codec = avcodec_find_encoder_by_name("libx264");
if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_H264);
} else if (codec_name == "h265" || codec_name == "HEVC" || codec_name == "H265") {
codec = avcodec_find_encoder_by_name("libx265");
if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
}
if (!codec) {
CMVR_LOG(ERROR) << "Failed to find " << codec_name << " encoder!";
return false;
}
// 3. 初始化编码器上下文(不变)
encoder->codec_context = avcodec_alloc_context3(codec);
if (!encoder->codec_context) {
CMVR_LOG(ERROR) << "Failed to allocate codec context!";
return false;
}
// 4. 设置编码器参数(不变)
AVCodecContext* ctx = encoder->codec_context;
ctx->codec_type = AVMEDIA_TYPE_VIDEO;
ctx->width = (width + 1) & ~1; // 确保宽度为偶数
ctx->height = (height + 1) & ~1;// 确保高度为偶数
ctx->time_base = {1, fps}; // 时间基1/fps
ctx->framerate = {fps, 1}; // 帧率
ctx->max_b_frames = 0; // 禁用B帧降低延迟
ctx->gop_size = 10;//I帧间隔1每一帧都是I帧
// 5. 设置编码器私有参数
if (codec->id == AV_CODEC_ID_H264) {
av_opt_set(ctx->priv_data, "preset", "ultrafast", 0);
av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);
av_opt_set(ctx->priv_data, "profile", "baseline", 0);
av_opt_set(ctx->priv_data, "repeat-headers", "1", 0);
av_opt_set(ctx->priv_data, "annexb", "1", 0);
} else if (codec->id == AV_CODEC_ID_HEVC) {
// 直接使用默认参数,不自定义
// av_opt_set(ctx->priv_data, "preset", "ultrafast", 0);
// av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);
}
// 6. 设置像素格式(不变)
const enum AVPixelFormat* pix_fmts = codec->pix_fmts;
if (!pix_fmts) {
CMVR_LOG(INFO) << "Using default pixel format: YUV420P";
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
} else {
ctx->pix_fmt = pix_fmts[0];
CMVR_LOG(INFO) << "Selected pixel format: " << av_get_pix_fmt_name(ctx->pix_fmt);
}
// 7. 打开编码器(不变)
int ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Failed to open " << codec_name << " encoder: " << errbuf;
return false;
}
// 8. 分配AVFrame不变
encoder->frame = av_frame_alloc();
if (!encoder->frame) {
CMVR_LOG(ERROR) << "Failed to allocate AVFrame!";
return false;
}
encoder->frame->format = ctx->pix_fmt;
encoder->frame->width = ctx->width;
encoder->frame->height = ctx->height;
if (av_frame_get_buffer(encoder->frame, 0) < 0) {
CMVR_LOG(ERROR) << "Failed to allocate AVFrame buffer!";
av_frame_free(&encoder->frame);
return false;
}
// 9. 分配AVPacket不变
encoder->packet = av_packet_alloc();
if (!encoder->packet) {
CMVR_LOG(ERROR) << "Failed to allocate AVPacket!";
return false;
}
CMVR_LOG(INFO) << "Successfully initialized " << codec_name << " encoder ( "
<< width << "x" << height << "@" << fps << "fps )";
return true;
}
bool UVCCamera::encodeFrameWithEncoder(std::shared_ptr<FfmpegEncoderInfo>& encoder, const cv::Mat& frame,std::vector<uint8_t>& encoded_frame,bool& is_key) {
if (!encoder || !encoder->codec_context || !encoder->frame || !encoder->packet) {
return false;
}
// 确保输入帧尺寸匹配(不变)
if (frame.cols != encoder->width || frame.rows != encoder->height) {
CMVR_LOG(ERROR) << "Frame size does not match encoder dimensions";
return false;
}
// 【修复3设置递增的PTS确保编码器正确处理I帧请求】
encoder->frame->pts = encoder->frame_pts++; // 分配唯一PTS
// 根据cv::Mat的类型设置源格式不变
AVPixelFormat src_pix_fmt;
if (frame.channels() == 3) {
src_pix_fmt = AV_PIX_FMT_BGR24; // OpenCV默认BGR
} else if (frame.channels() == 4) {
src_pix_fmt = AV_PIX_FMT_BGRA; // 4通道为BGRA
} else if (frame.channels() == 1) {
src_pix_fmt = AV_PIX_FMT_GRAY8; // 单通道灰度图
} else {
CMVR_LOG(ERROR) << "Unsupported number of channels: " << frame.channels();
return false;
}
// 【修复4动态创建sws_context匹配当前输入格式
if (encoder->sws_context) {
sws_freeContext(encoder->sws_context); // 释放旧上下文
}
encoder->sws_context = sws_getContext(
frame.cols, frame.rows, src_pix_fmt, // 输入格式由当前frame决定
encoder->codec_context->width, encoder->codec_context->height, encoder->codec_context->pix_fmt,
SWS_BILINEAR, nullptr, nullptr, nullptr
);
if (!encoder->sws_context) {
CMVR_LOG(ERROR) << "Failed to create SwsContext";
return false;
}
// 转换输入帧格式为编码器所需格式(不变)
const uint8_t* src_data[AV_NUM_DATA_POINTERS] = {frame.data};
int src_linesize[AV_NUM_DATA_POINTERS] = {static_cast<int>(frame.step)};
int ret = sws_scale(encoder->sws_context, src_data, src_linesize, 0, frame.rows,
encoder->frame->data, encoder->frame->linesize);
if (ret < 0) {
CMVR_LOG(ERROR) << "Error scaling frame";
return false;
}
// 发送帧到编码器(不变)
ret = avcodec_send_frame(encoder->codec_context, encoder->frame);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Error sending frame to encoder: " << errbuf;
return false;
}
while (true)
{
// 接收编码后的数据(不变)
ret = avcodec_receive_packet(encoder->codec_context, encoder->packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Error receiving packet from encoder: " << errbuf;
break;
}
// 调试打印帧类型I帧/P帧
if (encoder->packet->flags & AV_PKT_FLAG_KEY) {
is_key = true;
//CMVR_LOG(INFO) << "Encoded I frame (size: " << encoder->packet->size << " bytes)";
} else {
is_key = false;
//CMVR_LOG(INFO) << "Encoded P frame (size: " << encoder->packet->size << " bytes)";
}
// 预留足够空间,避免多次内存分配
encoded_frame.reserve(encoded_frame.size() + encoder->packet->size);
// 复制数据包内容到输出向量
encoded_frame.insert(encoded_frame.end(),
encoder->packet->data,
encoder->packet->data + encoder->packet->size);
av_packet_unref(encoder->packet);
}
// 验证帧有效性NALU起始码、元数据
if (!encoded_frame.empty()) {
// 检查NALU起始码
bool has_start_code = false;
if ((encoded_frame[0] == 0 && encoded_frame[1] == 0 && encoded_frame[2] == 1) ||
(encoded_frame[0] == 0 && encoded_frame[1] == 0 && encoded_frame[2] == 0 && encoded_frame[3] == 1)) {
has_start_code = true;
}
if (!has_start_code) {
CMVR_LOG(ERROR) << "Invalid frame: no NALU start code!";
return false;
}
} else {
//CMVR_LOG(ERROR) << "Encoded frame is empty!";
return false;
}
return true;
}
void UVCCamera::getEncodedFrame(StreamFrameData& frame_data, size_t& index) {
// 环形队列取数据的index由接口传入
auto frame = stream_frame_buffer_->pop(index);

View File

@ -19,40 +19,55 @@ enum AlignMode {
ALIGN_MODE_DEPTH = 1;
}
message CameraCaptureConfig {
int32 width = 1;
int32 height = 2;
int32 fps = 3;
StreamMode stream_mode = 4;
}
message CameraRenderConfig {
int32 width = 1;
int32 height = 2;
int32 fps = 3;
StreamMode stream_mode = 4;
}
message CameraEncoderConfig {
int32 width = 1;
int32 height = 2;
int32 fps = 3;
string codec = 4;
bool enable_stream_timestamp = 5;
int32 buffer_size = 6;
}
message MujocoViewerPipConfig {
bool enable = 1;
int32 left = 2;
int32 bottom = 3;
int32 width = 4;
int32 height = 5;
}
// RealSense摄像头
message RealSenseCameraConfig {
reserved 12;
string id = 1;
string serialNumber = 2;
int32 width = 3;
int32 height = 4;
int32 fps = 5;
string codec = 6;
CameraMode camera_mode = 7;
StreamMode stream_mode = 8;
AlignMode align_mode = 9;
int32 buffer_size = 10;
bool sync = 11;
int32 encode_width = 13;
int32 encode_height = 14;
CameraMode camera_mode = 3;
CameraCaptureConfig capture = 4;
CameraEncoderConfig encoder = 5;
AlignMode align_mode = 6;
bool sync = 7;
}
// UVC摄像头
message UVCCameraConfig {
reserved 10;
string id = 1;
string usb = 2; // device_path
int32 width = 3;
int32 height = 4;
int32 fps = 5;
string codec = 6;
CameraMode camera_mode = 7;
StreamMode stream_mode = 8;
int32 buffer_size = 9;
int32 encode_width = 11;
int32 encode_height = 12;
CameraMode camera_mode = 3;
CameraCaptureConfig capture = 4;
CameraEncoderConfig encoder = 5;
}
// MechMind工业相机
@ -65,6 +80,16 @@ message MechMindCameraConfig {
string image2d_type = 4;
}
message MujocoCameraConfig {
string id = 1;
string world_id = 2;
string camera_name = 3;
CameraRenderConfig render = 4;
CameraEncoderConfig encoder = 5;
bool consume_new_frame_only = 6;
MujocoViewerPipConfig viewer_pip = 7;
}
message CameraDeviceConfig {
string id = 1;
reserved 2;
@ -73,6 +98,7 @@ message CameraDeviceConfig {
UVCCameraConfig uvc = 10;
RealSenseCameraConfig realsense = 11;
MechMindCameraConfig mechmind = 12;
MujocoCameraConfig mujoco = 13;
}
}