feat: add camera audio and QUIC streaming

This commit is contained in:
linbo 2026-09-20 13:58:20 +08:00
parent 9d27f903cd
commit b9b6088800
630 changed files with 379734 additions and 1101 deletions

View File

@ -28,6 +28,38 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(FindExternalLib)
set(ARCH "x86")
setup_external_libs(${ARCH})
# Intel oneVPL / VA-API runtime. The shared libraries in lib/ are installed
# by setup_external_libs(); the VA-API driver plugin directory is installed
# separately because it must retain its dri layout.
set(INTEL_MEDIA_STACK_ROOT
"${PROJECT_SOURCE_DIR}/dependency/${ARCH}/third_party/intel-media-stack/vpl-2.17"
)
set(INTEL_MEDIA_DRIVER_DIR "${INTEL_MEDIA_STACK_ROOT}/lib/dri")
set(INTEL_IHD_DRIVER "${INTEL_MEDIA_DRIVER_DIR}/iHD_drv_video.so")
if(NOT EXISTS "${INTEL_IHD_DRIVER}")
message(FATAL_ERROR "Intel iHD VA-API driver not found: ${INTEL_IHD_DRIVER}")
endif()
message(STATUS "Intel media stack: ${INTEL_MEDIA_STACK_ROOT}")
install(
DIRECTORY "${INTEL_MEDIA_DRIVER_DIR}/"
DESTINATION lib/dri
)
install(CODE [=[
find_program(CMVR_PATCHELF_EXECUTABLE patchelf REQUIRED)
set(_cmvr_ihd_driver
"${CMAKE_INSTALL_PREFIX}/lib/dri/iHD_drv_video.so"
)
execute_process(
COMMAND "${CMVR_PATCHELF_EXECUTABLE}"
--set-rpath "$ORIGIN/.."
"${_cmvr_ihd_driver}"
COMMAND_ERROR_IS_FATAL ANY
)
]=])
# 在调用 setup_external_libs 之后
message(STATUS "CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}")
message(STATUS "CMAKE_SHARED_LINKER_FLAGS: ${CMAKE_SHARED_LINKER_FLAGS}")
@ -108,6 +140,7 @@ target_link_libraries(cmvr_es PRIVATE
cmvr_es::proto
cmvr_es::logging
service
cmvr_es::quic_edge_task
${GLOG_LIBRARIES}
jsoncpp
cmvr_es::service

View File

@ -0,0 +1,16 @@
# MsQuic's QuicTLS custom build asks CMake's ProcessorCount module for a
# nested `make -jN` value. Route that query through the build script's
# --jobs setting so the dependency build cannot silently oversubscribe the
# host.
if(NOT DEFINED CMVR_MSQUIC_PROCESSOR_COUNT OR
NOT CMVR_MSQUIC_PROCESSOR_COUNT MATCHES "^[1-9][0-9]*$")
message(FATAL_ERROR
"CMVR_MSQUIC_PROCESSOR_COUNT must be a positive integer")
endif()
function(ProcessorCount result_variable)
set("${result_variable}"
"${CMVR_MSQUIC_PROCESSOR_COUNT}"
PARENT_SCOPE)
endfunction()

View File

@ -7,7 +7,10 @@ add_subdirectory(algorithms)
add_subdirectory(simulate)
add_subdirectory(devices)
add_subdirectory(manager/device_manager)
add_subdirectory(manager/media_source_hub)
add_subdirectory(service/quic_edge)
add_subdirectory(task)
add_subdirectory(task/quic_edge_task)
add_subdirectory(manager/task_manager)
add_subdirectory(service)
add_subdirectory(runtime)

View File

@ -6,16 +6,25 @@
#define CMVR_ES_RING_BUFFER_H
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <memory>
#include <mutex>
#include <vector>
#include <atomic>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
template<typename T>
class RingBuffer {
public:
explicit RingBuffer(size_t capacity) : capacity_(capacity) {}
explicit RingBuffer(size_t capacity) : capacity_(capacity) {
if (capacity_ == 0) {
throw std::invalid_argument("RingBuffer capacity must be greater than zero");
}
}
void push(const T& item) {
std::lock_guard<std::mutex> lock(mutex_);
@ -50,47 +59,49 @@ template<typename T>
class SPMCRingBuffer {
public:
explicit SPMCRingBuffer(size_t capacity)
: buffer_(capacity), capacity_(capacity),
head_(0), tail_(0) {}
: buffer_(capacity), capacity_(capacity) {
if (capacity_ == 0) {
throw std::invalid_argument("SPMCRingBuffer capacity must be greater than zero");
}
}
// 写入操作(仅支持单个生产者)
void push(const T& item) {
size_t head = head_.load(std::memory_order_relaxed);
size_t tail = tail_.load(std::memory_order_acquire);
buffer_[head % capacity_] = item;
head = head + 1;
head_.store(head, std::memory_order_release);
if (head - tail >= capacity_) {
// 队列满,覆盖最旧的数据
tail_.store(tail + 1, std::memory_order_release);
{
std::lock_guard<std::mutex> lock(mutex_);
buffer_[head_ % capacity_] = item;
++head_;
if (head_ - tail_ > capacity_) {
// 队列满,覆盖最旧的数据
tail_ = head_ - capacity_;
}
}
condition_.notify_all();
}
// 单消费者使用(内部 tail_)
std::optional<T> pop() {
size_t tail = tail_.load(std::memory_order_relaxed);
size_t head = head_.load(std::memory_order_acquire);
if (tail >= head) return std::nullopt;
T value = buffer_[tail % capacity_];
tail_.store(tail + 1, std::memory_order_release);
std::lock_guard<std::mutex> lock(mutex_);
if (tail_ >= head_) return std::nullopt;
T value = buffer_[tail_ % capacity_];
++tail_;
return value;
}
std::optional<T> getLast() {
size_t tail = tail_.load(std::memory_order_relaxed);
size_t head = head_.load(std::memory_order_acquire);
if (tail >= head) return std::nullopt;
T value = buffer_[head_ % capacity_];
return value;
std::optional<T> getLast() const {
std::lock_guard<std::mutex> lock(mutex_);
if (tail_ >= head_) return std::nullopt;
return buffer_[(head_ - 1) % capacity_];
}
// 多消费者使用(每个读者独立维护 reader_tail)
// 多消费者使用(每个读者独立维护 reader_tail)。同一个 reader_tail 只能由
// 一个消费线程拥有,且不要把该游标与无参 pop() 的共享 tail_ 混合作为同一路读取。
std::optional<T> pop(size_t& reader_tail) const {
size_t head = head_.load(std::memory_order_acquire);
if (reader_tail >= head) return std::nullopt;
if (head > reader_tail + capacity_) {
std::lock_guard<std::mutex> lock(mutex_);
if (reader_tail >= head_) return std::nullopt;
if (reader_tail < tail_) {
// 数据已被覆盖,跳过无效读取区间
reader_tail = head - capacity_;
reader_tail = tail_;
return std::nullopt;
}
T value = buffer_[reader_tail % capacity_];
@ -98,16 +109,52 @@ public:
return value;
}
// 在同一次加锁中把独立读游标跳到当前最新元素并读取,避免先 getHead()
// 再 pop() 时被高速覆盖造成的检查/读取竞态。
std::optional<T> getLatest(size_t& reader_tail) const {
std::lock_guard<std::mutex> lock(mutex_);
if (tail_ >= head_) {
return std::nullopt;
}
reader_tail = head_ - 1;
T value = buffer_[reader_tail % capacity_];
++reader_tail;
return value;
}
template<class Rep, class Period>
std::optional<T> waitPop(
size_t& reader_tail,
const std::chrono::duration<Rep, Period>& timeout) const {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait_for(lock, timeout, [&] { return reader_tail < head_; });
if (reader_tail >= head_) {
return std::nullopt;
}
if (reader_tail < tail_) {
reader_tail = tail_;
}
if (reader_tail >= head_) {
return std::nullopt;
}
T value = buffer_[reader_tail % capacity_];
++reader_tail;
return value;
}
size_t size() const {
return head_.load(std::memory_order_acquire) - tail_.load(std::memory_order_acquire);
std::lock_guard<std::mutex> lock(mutex_);
return head_ - tail_;
}
size_t getHead() const {
return head_.load(std::memory_order_acquire);
std::lock_guard<std::mutex> lock(mutex_);
return head_;
}
size_t getTail() const {
return tail_.load(std::memory_order_acquire);
std::lock_guard<std::mutex> lock(mutex_);
return tail_;
}
bool empty() const {
@ -119,16 +166,278 @@ public:
}
void clear() {
head_.store(0, std::memory_order_release);
tail_.store(0, std::memory_order_release);
{
std::lock_guard<std::mutex> lock(mutex_);
// Keep sequence numbers monotonic so cursors created before clear()
// cannot alias newly published slots after the reset.
tail_ = head_;
}
condition_.notify_all();
}
private:
std::vector<T> buffer_;
mutable std::vector<T> buffer_;
const size_t capacity_;
mutable std::mutex mutex_;
mutable std::condition_variable condition_;
size_t head_{0}; // 单调写序号;clear() 仅推进 tail_,避免旧游标 ABA。
size_t tail_{0}; // 当前仍保留的最旧序号,同时也是 pop() 的共享读指针。
};
std::atomic<size_t> head_; // 共享写指针
std::atomic<size_t> tail_; // 共享读指针(仅用于 SPSC 模式)
// 线程安全的多消费者广播缓冲区。缓冲区只保存不可变共享对象,消费者通过各自
// 的 Cursor 独立前进;慢消费者被覆盖的数据会累计到 Cursor::dropped_count。
// Cursor 是单线程所有权对象,不可由多个线程同时读写;每个消费者应创建自己的 Cursor。
template<typename T>
class BroadcastFrameRing {
public:
using ValuePtr = std::shared_ptr<const T>;
enum class StartPosition {
NEXT_PUBLISHED,
OLDEST_AVAILABLE,
LATEST_AVAILABLE
};
struct Cursor {
uint64_t generation{0};
uint64_t next_sequence{0};
uint64_t dropped_count{0};
StartPosition start_position{StartPosition::NEXT_PUBLISHED};
private:
bool generation_changed{false};
uint64_t reported_dropped_count{0};
friend class BroadcastFrameRing<T>;
};
struct ReadResult {
ValuePtr value;
uint64_t generation{0};
uint64_t sequence{0};
uint64_t dropped_count{0};
uint64_t dropped_since_last_read{0};
bool generation_changed{false};
};
struct Stats {
size_t capacity{0};
size_t size{0};
uint64_t generation{0};
uint64_t next_sequence{0};
uint64_t dropped_count{0};
bool closed{false};
};
explicit BroadcastFrameRing(const size_t capacity)
: capacity_(capacity) {
if (capacity_ == 0) {
throw std::invalid_argument("BroadcastFrameRing capacity must be greater than zero");
}
}
BroadcastFrameRing(const BroadcastFrameRing&) = delete;
BroadcastFrameRing& operator=(const BroadcastFrameRing&) = delete;
Cursor makeCursor(const StartPosition start_position = StartPosition::NEXT_PUBLISHED) const {
std::lock_guard<std::mutex> lock(mutex_);
Cursor cursor;
cursor.generation = generation_;
cursor.start_position = start_position;
cursor.next_sequence = startSequenceLocked_(start_position);
return cursor;
}
std::optional<uint64_t> publish(ValuePtr value) {
if (!value) {
return std::nullopt;
}
std::optional<uint64_t> published_sequence;
{
std::lock_guard<std::mutex> lock(mutex_);
if (closed_) {
return std::nullopt;
}
const uint64_t sequence = next_sequence_++;
if (entries_.size() == capacity_) {
entries_.pop_front();
++dropped_count_;
}
entries_.push_back(Entry{generation_, sequence, std::move(value)});
published_sequence = sequence;
}
condition_.notify_all();
return published_sequence;
}
std::optional<ReadResult> tryRead(Cursor& cursor) const {
std::lock_guard<std::mutex> lock(mutex_);
return tryReadLocked_(cursor);
}
// Low-latency consumers can use this before reading to abandon an excessive
// backlog atomically. When the number of currently readable entries exceeds
// maximum_pending_frames, every pending entry is discarded and the cursor is
// advanced to the next sequence that will be published. Frames already
// overwritten by the ring and frames actively discarded here are both
// reflected in Cursor::dropped_count; the next successful read reports their
// sum through ReadResult::dropped_since_last_read.
uint64_t discardPendingIfExceeds(
Cursor& cursor,
const size_t maximum_pending_frames) const {
std::lock_guard<std::mutex> lock(mutex_);
synchronizeCursorGenerationLocked_(cursor);
if (!entries_.empty()) {
const uint64_t oldest_sequence = entries_.front().sequence;
if (cursor.next_sequence < oldest_sequence) {
cursor.dropped_count += oldest_sequence - cursor.next_sequence;
cursor.next_sequence = oldest_sequence;
}
}
const uint64_t pending =
cursor.next_sequence < next_sequence_
? next_sequence_ - cursor.next_sequence
: 0;
if (pending <= static_cast<uint64_t>(maximum_pending_frames)) {
return 0;
}
cursor.next_sequence = next_sequence_;
cursor.dropped_count += pending;
return pending;
}
template<class Rep, class Period>
std::optional<ReadResult> waitRead(
Cursor& cursor,
const std::chrono::duration<Rep, Period>& timeout) const {
const auto deadline = std::chrono::steady_clock::now() + timeout;
std::unique_lock<std::mutex> lock(mutex_);
while (true) {
if (auto result = tryReadLocked_(cursor)) {
return result;
}
if (closed_) {
return std::nullopt;
}
if (condition_.wait_until(lock, deadline) == std::cv_status::timeout) {
return tryReadLocked_(cursor);
}
}
}
// 开始一个新的发布代次。旧 Cursor 会在下一次成功读取时收到
// generation_changed=true,序号从 0 重新开始。
uint64_t reset() {
uint64_t generation = 0;
{
std::lock_guard<std::mutex> lock(mutex_);
entries_.clear();
++generation_;
next_sequence_ = 0;
closed_ = false;
generation = generation_;
}
condition_.notify_all();
return generation;
}
void close() {
{
std::lock_guard<std::mutex> lock(mutex_);
closed_ = true;
}
condition_.notify_all();
}
bool closed() const {
std::lock_guard<std::mutex> lock(mutex_);
return closed_;
}
Stats stats() const {
std::lock_guard<std::mutex> lock(mutex_);
return Stats{capacity_, entries_.size(), generation_, next_sequence_, dropped_count_, closed_};
}
private:
struct Entry {
uint64_t generation;
uint64_t sequence;
ValuePtr value;
};
uint64_t startSequenceLocked_(const StartPosition start_position) const {
if (entries_.empty()) {
return next_sequence_;
}
switch (start_position) {
case StartPosition::OLDEST_AVAILABLE:
return entries_.front().sequence;
case StartPosition::LATEST_AVAILABLE:
return entries_.back().sequence;
case StartPosition::NEXT_PUBLISHED:
default:
return next_sequence_;
}
}
void synchronizeCursorGenerationLocked_(Cursor& cursor) const {
if (cursor.generation == generation_) {
return;
}
cursor.generation = generation_;
cursor.next_sequence = startSequenceLocked_(cursor.start_position);
cursor.generation_changed = true;
}
std::optional<ReadResult> tryReadLocked_(Cursor& cursor) const {
synchronizeCursorGenerationLocked_(cursor);
if (entries_.empty()) {
return std::nullopt;
}
const uint64_t oldest_sequence = entries_.front().sequence;
if (cursor.next_sequence < oldest_sequence) {
cursor.dropped_count += oldest_sequence - cursor.next_sequence;
cursor.next_sequence = oldest_sequence;
}
if (cursor.next_sequence >= next_sequence_) {
return std::nullopt;
}
const size_t index = static_cast<size_t>(cursor.next_sequence - oldest_sequence);
if (index >= entries_.size()) {
return std::nullopt;
}
const Entry& entry = entries_[index];
++cursor.next_sequence;
const uint64_t dropped_since_last = cursor.dropped_count - cursor.reported_dropped_count;
cursor.reported_dropped_count = cursor.dropped_count;
ReadResult result;
result.value = entry.value;
result.generation = entry.generation;
result.sequence = entry.sequence;
result.dropped_count = cursor.dropped_count;
result.dropped_since_last_read = dropped_since_last;
result.generation_changed = cursor.generation_changed;
cursor.generation_changed = false;
return result;
}
const size_t capacity_;
mutable std::mutex mutex_;
mutable std::condition_variable condition_;
std::deque<Entry> entries_;
uint64_t generation_{1};
uint64_t next_sequence_{0};
uint64_t dropped_count_{0};
bool closed_{false};
};

View File

@ -62,14 +62,14 @@ int CameraCapture::initialize(const Config& config) {
}
int CameraCapture::init_device() {
AVInputFormat* input_fmt = nullptr;
// AVInputFormat* input_fmt = nullptr;
std::string device_path;
#ifdef _WIN32
input_fmt = av_find_input_format("dshow");
auto input_fmt = av_find_input_format("dshow");
device_path = "video=" + config_.device_name;
#else
input_fmt = av_find_input_format("v4l2");
auto input_fmt = av_find_input_format("v4l2");
device_path = config_.device_name;
#endif

View File

@ -0,0 +1,244 @@
#ifndef CMVR_ES_COMMON_MEDIA_MEDIA_FRAME_H
#define CMVR_ES_COMMON_MEDIA_MEDIA_FRAME_H
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace cmvr::media {
enum class MediaKind : uint8_t {
UNKNOWN = 0,
VIDEO = 1,
AUDIO = 2
};
enum class Codec : uint8_t {
UNKNOWN = 0,
H264 = 1,
H265 = 2,
OPUS = 3,
PCM_S16LE = 4,
AAC = 5
};
enum class PayloadFormat : uint8_t {
UNKNOWN = 0,
ANNEX_B = 1,
AVCC = 2,
RAW = 3,
OPUS_PACKET = 4,
AAC_ADTS = 5
};
struct Rational {
int32_t numerator{0};
int32_t denominator{1};
constexpr bool valid() const noexcept {
return numerator > 0 && denominator > 0;
}
};
constexpr bool operator==(const Rational lhs, const Rational rhs) noexcept {
return lhs.numerator == rhs.numerator && lhs.denominator == rhs.denominator;
}
constexpr bool operator!=(const Rational lhs, const Rational rhs) noexcept {
return !(lhs == rhs);
}
// A TrackDescriptor is immutable after construction. Reconfiguration is represented by
// publishing a new descriptor with a larger generation and attaching it to later frames.
class TrackDescriptor final {
public:
struct Config {
std::string id;
std::string source_id;
MediaKind kind{MediaKind::UNKNOWN};
Codec codec{Codec::UNKNOWN};
PayloadFormat payload_format{PayloadFormat::UNKNOWN};
Rational time_base{};
uint32_t width{0};
uint32_t height{0};
uint32_t sample_rate{0};
uint32_t channels{0};
uint32_t nominal_rate{0};
float fx{0.0F};
float fy{0.0F};
float cx{0.0F};
float cy{0.0F};
std::vector<float> distortion;
uint64_t generation{1};
std::vector<uint8_t> codec_config;
};
explicit TrackDescriptor(Config config)
: id(std::move(config.id)),
source_id(std::move(config.source_id)),
kind(config.kind),
codec(config.codec),
payload_format(config.payload_format),
time_base(config.time_base),
width(config.width),
height(config.height),
sample_rate(config.sample_rate),
channels(config.channels),
nominal_rate(config.nominal_rate),
fx(config.fx),
fy(config.fy),
cx(config.cx),
cy(config.cy),
distortion(std::move(config.distortion)),
generation(config.generation),
codec_config(std::move(config.codec_config)) {
if (id.empty()) {
throw std::invalid_argument("TrackDescriptor id must not be empty");
}
if (source_id.empty()) {
throw std::invalid_argument("TrackDescriptor source_id must not be empty");
}
if (kind == MediaKind::UNKNOWN) {
throw std::invalid_argument("TrackDescriptor kind must not be UNKNOWN");
}
if (!time_base.valid()) {
throw std::invalid_argument("TrackDescriptor time_base is invalid");
}
if (generation == 0) {
throw std::invalid_argument("TrackDescriptor generation must be greater than zero");
}
}
const std::string id;
const std::string source_id;
const MediaKind kind;
const Codec codec;
const PayloadFormat payload_format;
const Rational time_base;
const uint32_t width;
const uint32_t height;
const uint32_t sample_rate;
const uint32_t channels;
const uint32_t nominal_rate;
const float fx;
const float fy;
const float cx;
const float cy;
const std::vector<float> distortion;
const uint64_t generation;
const std::vector<uint8_t> codec_config;
};
using TrackDescriptorPtr = std::shared_ptr<const TrackDescriptor>;
inline bool equivalentTrackDescriptor(
const TrackDescriptor& lhs,
const TrackDescriptor& rhs) noexcept {
return lhs.id == rhs.id &&
lhs.source_id == rhs.source_id &&
lhs.kind == rhs.kind &&
lhs.codec == rhs.codec &&
lhs.payload_format == rhs.payload_format &&
lhs.time_base == rhs.time_base &&
lhs.width == rhs.width &&
lhs.height == rhs.height &&
lhs.sample_rate == rhs.sample_rate &&
lhs.channels == rhs.channels &&
lhs.nominal_rate == rhs.nominal_rate &&
lhs.fx == rhs.fx &&
lhs.fy == rhs.fy &&
lhs.cx == rhs.cx &&
lhs.cy == rhs.cy &&
lhs.distortion == rhs.distortion &&
lhs.generation == rhs.generation &&
lhs.codec_config == rhs.codec_config;
}
// MediaFrame and its payload are immutable and therefore safe to share across all protocol
// adapters and consumers without copying. PTS/DTS use TrackDescriptor::time_base;
// capture_time_ns is monotonic for pacing, while capture_utc_ns is optional wall time.
class MediaFrame final {
public:
using Payload = std::vector<uint8_t>;
using PayloadPtr = std::shared_ptr<const Payload>;
struct Config {
TrackDescriptorPtr descriptor;
Payload payload;
uint64_t sequence{0};
// Opaque producer-native timing/counter values. Their units and epoch
// are source-defined; zero means that the source did not provide them.
uint64_t source_timestamp{0};
uint64_t source_frame_number{0};
int64_t pts{0};
int64_t dts{0};
int64_t duration{0};
uint64_t capture_time_ns{0};
int64_t capture_utc_ns{0};
bool key_frame{false};
bool discontinuity{false};
};
explicit MediaFrame(Config config)
: descriptor(std::move(config.descriptor)),
payload(std::make_shared<const Payload>(std::move(config.payload))),
sequence(config.sequence),
source_timestamp(config.source_timestamp),
source_frame_number(config.source_frame_number),
pts(config.pts),
dts(config.dts),
duration(config.duration),
capture_time_ns(config.capture_time_ns),
capture_utc_ns(config.capture_utc_ns),
key_frame(config.key_frame),
discontinuity(config.discontinuity) {
if (!descriptor) {
throw std::invalid_argument("MediaFrame descriptor must not be null");
}
}
const uint8_t* data() const noexcept {
return payload->empty() ? nullptr : payload->data();
}
size_t size() const noexcept {
return payload->size();
}
bool empty() const noexcept {
return payload->empty();
}
const TrackDescriptorPtr descriptor;
const PayloadPtr payload;
const uint64_t sequence;
const uint64_t source_timestamp;
const uint64_t source_frame_number;
const int64_t pts;
const int64_t dts;
const int64_t duration;
const uint64_t capture_time_ns;
const int64_t capture_utc_ns;
const bool key_frame;
const bool discontinuity;
};
using MediaFramePtr = std::shared_ptr<const MediaFrame>;
inline TrackDescriptorPtr makeTrackDescriptor(TrackDescriptor::Config config) {
return std::make_shared<const TrackDescriptor>(std::move(config));
}
inline MediaFramePtr makeMediaFrame(MediaFrame::Config config) {
return std::make_shared<const MediaFrame>(std::move(config));
}
} // namespace cmvr::media
#endif // CMVR_ES_COMMON_MEDIA_MEDIA_FRAME_H

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEKzCCApOgAwIBAgIUTtZCyKM8INYxZKkfwbKdpHKLjn4wDQYJKoZIhvcNAQEL
BQAwHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENBMB4XDTI2MDcyNDA2NDQy
MFoXDTM2MDcyMTA2NDQyMFowHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENB
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmq2rHldOobaemqNfWggS
OVj3inKy6AYjfgtcXUfKs48DDbpZ9gyEd/YPJXA8C2jGPXpxzgnc7a4UCUVQZ8ah
ddoJtFcC+Q6BgjeMVqUdUubu5Y9HpkfU3lvnp4KhzvOeFnkKtrCzYIPa2nK3zLc7
uCiuLlB+91KQSRXPFbc6N7H/EAfGmUHIwlZGysAkRN7b2TAoR4C7E96JLVtuUQsS
VtlEGpunSfuefFzeeZCMS6avLbB+a8Q6yUzLt6pqnheNsDB+jCCXodlJs5XS1AOB
W2GOpGFMj7dLoTD+eBIlAlrhWFcwKjzFmtp6LGl/Jy0O+E99X4TL72oNSOdTrO2O
iavXABz9IvWR2BrAyo5AKlTJqO6tmZw77iVti8jYi+HsXIVGQKMYwvv5k0jdHeIZ
FaDToUbFPP/zj0m8ZraMy+8eNAhScnx6Zs56fcncBuDti6pT+zKisjV1rH/sFvZY
wO1UvJlOEbvXrfYLPp58Aqe/toG2nV45a2Q4+x6AETV1AgMBAAGjYzBhMB0GA1Ud
DgQWBBRdHz+g2vVmqghDZi2kEHtZfoyHZzAfBgNVHSMEGDAWgBRdHz+g2vVmqghD
Zi2kEHtZfoyHZzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAYEASuEoMFiVcg4iTMxO2kshFTJ6LIrqqGXBn+1j+yQ1ennG
mqPMo5fBOe/Kp3YWCnREQWu0+EEPEC9qWgIDOIm3v7ch4mZiW31GUOqae6bjprBe
er7ySElKGZ5GefKAq++we19A6WHnNxtNAT9BE1VSKUmkxEsnIkuwd+QMmQ9eaIRm
8RvzshWdUcyiBJg07sI3rPzPD/YpnfcYlAa2a0+oXJ3o3zlhdbs2S+9fIl+Y/zFN
wBaUV6ZNk0RFzOCA+jUq2jU5Y1pODcot3Mp2jCHzp4uTFyVqsoqc5USRB2TUILCP
acSbEjD9GnKaNU4miTcftnC85fnBAiZ5btZkXIs72ij+J3VW7QnbcFQG8bElngoR
mWizh6ByPQrsvyT0evJjdjpuOzILsVTAn64m0cYpnlR4g5orcEkIJHv4DzMhpXc2
CvpcWwk2vJrUuws/LGxDATTAcqtIUIxAkxffqUsdB/6j8fdykNCvXFwNh/uPcawd
4kzh6dr+RFzHSMQ6oN16
-----END CERTIFICATE-----

View File

@ -35,8 +35,8 @@ camera {
stream_mode: STREAM_MODE_RGB
}
encoder {
width: 480
height: 320
width: 640
height: 360
fps: 30
codec: "H264"
enable_stream_timestamp: true
@ -48,21 +48,21 @@ camera {
}
cameras {
id: "left_hand_cam"
id: "cam3"
realsense {
serialNumber: "243122075614"
camera_mode: CAMERA_MODE_VIDEO
capture {
width: 1280
height: 720
width: 640
height: 480
fps: 30
stream_mode: STREAM_MODE_RGB
stream_mode: STREAM_MODE_RGBD
}
encoder {
width: 480
height: 320
width: 640
height: 480
fps: 30
codec: "H264"
codec: "H265"
enable_stream_timestamp: true
buffer_size: 30
}
@ -83,8 +83,8 @@ camera {
stream_mode: STREAM_MODE_RGBD
}
encoder {
width: 480
height: 320
width: 1280
height: 720
fps: 30
codec: "H264"
enable_stream_timestamp: true
@ -92,7 +92,7 @@ camera {
}
consume_new_frame_only: false
viewer_pip {
enable: false
enable: true
left: -10
bottom: 10
width: 320
@ -101,57 +101,7 @@ camera {
}
}
cameras {
id: "mujoco_external_touch_cam"
mujoco {
world_id: "mujoco_world"
camera_name: "external_touch_cam"
render {
width: 1280
height: 720
fps: 30
stream_mode: STREAM_MODE_RGBD
}
encoder {
width: 480
height: 320
fps: 30
codec: "H264"
enable_stream_timestamp: true
buffer_size: 30
}
consume_new_frame_only: false
viewer_pip {
enable: false
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"
enable_stream_timestamp: true
buffer_size: 30
}
}
}
cameras {
id: "cam5"
@ -176,4 +126,89 @@ camera {
sync: false
}
}
cameras {
id: "hikvision_cam"
hikvision {
ip: "192.168.192.64"
port: 8000
username: "admin"
password: "okwy1688"
channel: 1
stream_type: 0
link_mode: 0
width: 1920
height: 1080
fps: 25
codec: "H264"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGB
buffer_size: 30
}
}
cameras {
id: "hikvision_thermal_cam"
hikvision {
ip: "192.168.192.65"
port: 8000
username: "admin"
password: "okwy1688"
channel: 1
stream_type: 0
link_mode: 0
width: 384
height: 288
fps: 50
codec: "H264"
camera_mode: CAMERA_MODE_VIDEO
stream_mode: STREAM_MODE_RGB
buffer_size: 30
}
}
cameras {
id: "real_cam1"
realsense {
serialNumber: "332522076896"
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_qsv"
enable_stream_timestamp: true
buffer_size: 30
}
align_mode: ALIGN_MODE_COLOR
sync: false
}
}
cameras {
id: "usb_cam1"
uvc {
usb: "/dev/video0"
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_qsv"
enable_stream_timestamp: true
buffer_size: 30
}
}
}
}

View File

@ -3,9 +3,9 @@ microphone {
id: "mic1"
ffmpeg {
channels: 2
sampleRate: 44100
sampleRate: 48000
volume: 100
input_device: "default"
input_device: "plughw:CARD=XFMDPV0018,DEV=0"
}
}
}

View File

@ -22,4 +22,11 @@ task_manager {
config_file: "tasks/self_collision_task/self_collision_task.pb.txt"
enable: false
}
tasks {
id: "quic_edge"
type: TASK_TYPE_QUIC_EDGE
run_mode: TASK_RUN_MODE_BLOCKING_SERVICE
config_file: "tasks/quic_edge_task/quic_edge_task.pb.txt"
enable: true
}
}

View File

@ -0,0 +1,79 @@
quic_edge {
id: "quic_edge"
# Task enablement is controlled by manager/task_manager.pb.txt. Configure a
# reachable QUIC Gateway and TLS policy before enabling the task there.
server_host: "192.168.0.222"
server_port: 4433
alpn: "cmvr-quic-edge/1"
node_id: "cmvr-edge"
robot_id: "CN-CMVR-MBLRV1-AIMA-20260806-001"
software_version: "0.1"
# The existing cmvr-es gRPC server remains the robot-control endpoint. "auto"
# selects a usable address from the interface snapshot sent at registration
# and on every heartbeat.
grpc_endpoint_host: "auto"
grpc_endpoint_port: 50052
grpc_endpoint_tls: false
include_loopback_interfaces: false
# Local heartbeat period. The Gateway keeps this value when its registration
# response returns heartbeat_interval_ms=0; a non-zero response overrides it.
heartbeat_interval_ms: 5000
control_response_timeout_ms: 1000
tls {
ca_file: "certs/cmvr-quic-ca.crt"
server_name: "192.168.0.222"
allow_insecure: false
}
# Additional platforms run concurrently with the primary endpoint above.
# Each one has an independent connection, registration, heartbeat ACK state
# and reconnect loop. Media forwarding is opt-in for additional platforms.
# platforms {
# id: "operations_backup"
# server_host: "192.168.0.223"
# server_port: 4433
# enable_media: false
# tls {
# ca_file: "certs/cmvr-quic-ca.crt"
# server_name: "192.168.0.223"
# allow_insecure: false
# }
# }
reconnect {
initial_delay_ms: 500
maximum_delay_ms: 30000
multiplier: 2.0
jitter_percent: 20
connect_timeout_ms: 5000
}
maximum_datagram_bytes: 1200
maximum_control_frame_bytes: 1048576
# With 1200-byte DATAGRAMs and a 512-entry queue, 524288 stays below
# the atomic batch capacity while reserving slots for control messages.
maximum_frame_bytes: 524288
datagram_send_queue_depth: 512
media_poll_interval_ms: 2
# Zero media tracks is valid and keeps registration, IP reporting and
# heartbeat active. Add tracks only for devices enabled in DeviceManager.
# tracks {
# track_id: 1
# source_kind: SOURCE_KIND_CAMERA
# device_id: "right_hand_cam"
# source_track_id: "right_hand_cam/video/color"
# enable: true
# }
# tracks {
# track_id: 2
# source_kind: SOURCE_KIND_MICROPHONE
# device_id: "mic1"
# source_track_id: "mic1/audio/main"
# enable: true
# }
}

View File

@ -3,7 +3,7 @@ add_subdirectory(common)
add_subdirectory(uvc_camera)
add_subdirectory(realsense_camera)
add_subdirectory(mujoco_camera)
add_subdirectory(hikvision_camera)
add_library(camera INTERFACE)
target_include_directories(camera INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
@ -14,6 +14,7 @@ target_link_libraries(camera
cmvr_es::device::realsense_camera
cmvr_es::device::mujoco_camera
cmvr_es::device::camera_stream_encoder
cmvr_es::device::hikvision_camera
cmvr_es::proto
)

View File

@ -2,6 +2,11 @@
#define CMVR_ES_ABSTRACT_CAMERA_H
#pragma once
#include <cstdint>
#include <chrono>
#include <string>
#include <vector>
#include <opencv2/opencv.hpp>
#include <mutex>
#include "../abstract_device.h"
@ -33,13 +38,50 @@ namespace cmvr::device {
std::vector<uint8_t> depthFrame;
//编码格式
std::string codec = ".h264";
Rs2Intrinsics intrinsics;
int width;
int height;
int fps;
bool bKey;
bool depthKey;
Rs2Intrinsics intrinsics{};
int width = 0;
int height = 0;
// Depth frames may use a different resolution from the encoded color frame.
int depth_width = 0;
int depth_height = 0;
int fps = 0;
bool bKey = false;
bool depthKey = false;
// Protocol-neutral real-time metadata. The producer fills these values
// when a complete encoded access unit is published.
uint64_t stream_epoch = 0;
uint64_t sequence = 0;
// Opaque producer-native timing/counter values. Their units and epoch
// are source-defined; zero means that the source did not provide them.
uint64_t source_timestamp = 0;
uint64_t source_frame_number = 0;
int64_t capture_monotonic_ns = 0;
int64_t capture_utc_ns = 0;
int64_t pts = 0;
int64_t dts = 0;
int32_t time_base_num = 1;
int32_t time_base_den = 1;
int64_t duration = 0;
bool discontinuity = false;
uint32_t codec_config_generation = 0;
std::vector<uint8_t> codec_config;
};
enum class PtzCommand {
TiltUp,
TiltDown,
PanLeft,
PanRight,
UpLeft,
UpRight,
DownLeft,
DownRight,
ZoomIn,
ZoomOut,
PanAuto
};
class AbstractCamera : public AbstractDevice {
public:
// 录制状态
@ -53,7 +95,10 @@ namespace cmvr::device {
~AbstractCamera() override = default;
DeviceKind kind() const noexcept override { return DeviceKind::Camera; }
inline void getState(CameraState &state) {state = state_;}
// Every implementation must take the same lock used by its state_
// writers; CameraState contains std::string and cannot be snapshotted
// safely while another thread mutates it.
virtual void getState(CameraState &state) = 0;
virtual void getRGBImage(cv::Mat &color, Rs2Intrinsics& intrinsics) {}
virtual void getDepthImage(cv::Mat &depth, Rs2Intrinsics& intrinsics) {}
virtual void getRGBDImages(cv::Mat &color, cv::Mat &depth, Rs2Intrinsics& intrinsics) {}
@ -62,6 +107,14 @@ namespace cmvr::device {
virtual void pauseRecording() {}
virtual void resumeRecording() {}
virtual void getEncodedFrame(StreamFrameData& frame_data, size_t& index) {}
virtual bool waitEncodedFrame(
StreamFrameData& frame_data,
size_t& index,
std::chrono::milliseconds timeout) {
(void)timeout;
getEncodedFrame(frame_data, index);
return !frame_data.rgbFrame.empty() || !frame_data.depthFrame.empty();
}
virtual bool getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index) {
return false;
}
@ -81,6 +134,13 @@ namespace cmvr::device {
virtual bool startStreaming() {return true;}
virtual void stopStreaming() {}
virtual bool controlPtz(PtzCommand command, bool stop, int speed) {
(void)command;
(void)stop;
(void)speed;
return false;
}
virtual bool requestKeyFrame() { return false; }
virtual Eigen::Vector3f get3DPointFromPixel(int u, int v) {return {0,0,0};}
protected:
CameraState state_{};

View File

@ -9,6 +9,7 @@
#include "common/base/logging/logger.h"
#include "devices/camera/abstract_camera.h"
#include "devices/camera/mujoco_camera/include/mujoco_camera.h"
#include "devices/camera/hikvision_camera/include/hikvision_camera.h"
#include "devices/camera/realsense_camera/include/realsense_camera.h"
#include "devices/camera/uvc_camera/include/uvc_camera.h"
@ -41,6 +42,10 @@ public:
return nullptr;
}
case config::CameraDeviceConfig::kHikvision:
return std::make_shared<HikvisionCamera>(
backendWithId_(cfg.id(), cfg.hikvision()));
case config::CameraDeviceConfig::kMujoco:
return std::make_shared<MujocoCamera>(
backendWithId_(cfg.id(), cfg.mujoco()));

View File

@ -22,6 +22,7 @@ struct FfmpegEncoderInfo {
bool bRunning = false;
AVCodecContext* codec_context = nullptr;
AVFrame* frame = nullptr;
AVFrame* transfer_frame = nullptr;
AVPacket* packet = nullptr;
SwsContext* sws_context = nullptr;
@ -68,6 +69,12 @@ public:
std::vector<uint8_t>& encoded_frame,
bool& is_key,
const CameraStreamEncodeOptions& options = {});
static bool encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const AVFrame* frame,
std::vector<uint8_t>& encoded_frame,
bool& is_key,
const CameraStreamEncodeOptions& options = {});
};
} // namespace cmvr::device

View File

@ -7,6 +7,8 @@
#include <sstream>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
#include <libavutil/hwcontext.h>
#include <opencv2/imgproc.hpp>
#include "common/base/logging/logger.h"
@ -85,6 +87,13 @@ void drawOutlinedText(cv::Mat& image,
const AVCodec* findEncoder(const std::string& codec_name)
{
if (codec_name == "h264_qsv" || codec_name == "H264_QSV") {
return avcodec_find_encoder_by_name("h264_qsv");
}
if (codec_name == "hevc_qsv" || codec_name == "h265_qsv" ||
codec_name == "HEVC_QSV" || codec_name == "H265_QSV") {
return avcodec_find_encoder_by_name("hevc_qsv");
}
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);
@ -110,6 +119,55 @@ AVPixelFormat sourcePixelFormat(const cv::Mat& frame)
return AV_PIX_FMT_NONE;
}
bool encodePreparedFrame(FfmpegEncoderInfo& encoder,
std::vector<uint8_t>& encoded_frame,
bool& is_key)
{
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;
return false;
}
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
void drawCoordinateFrame(cv::Mat& image,
@ -189,6 +247,10 @@ FfmpegEncoderInfo::~FfmpegEncoderInfo()
av_frame_free(&frame);
frame = nullptr;
}
if (transfer_frame) {
av_frame_free(&transfer_frame);
transfer_frame = nullptr;
}
if (packet) {
av_packet_free(&packet);
packet = nullptr;
@ -237,7 +299,17 @@ bool CameraStreamEncoder::init(std::shared_ptr<FfmpegEncoderInfo>& encoder,
ctx->max_b_frames = 0;
ctx->gop_size = 10;
if (codec->id == AV_CODEC_ID_H264) {
const bool is_qsv = codec->name &&
(std::string(codec->name) == "h264_qsv" || std::string(codec->name) == "hevc_qsv");
if (is_qsv) {
// Feed system-memory NV12 frames to oneVPL/QSV. The QSV encoder
// performs the upload and hardware encoding internally.
ctx->pix_fmt = AV_PIX_FMT_NV12;
ctx->bit_rate = 4'000'000;
av_opt_set_int(ctx->priv_data, "async_depth", 1, 0);
av_opt_set_int(ctx->priv_data, "repeat_pps", 1, 0);
} else 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);
@ -252,11 +324,13 @@ bool CameraStreamEncoder::init(std::shared_ptr<FfmpegEncoderInfo>& encoder,
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];
if (!is_qsv) {
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);
@ -287,9 +361,11 @@ bool CameraStreamEncoder::init(std::shared_ptr<FfmpegEncoderInfo>& encoder,
return false;
}
CMVR_LOG(INFO) << "[CameraStreamEncoder] initialized " << codec_name
const char* pixel_format_name = av_get_pix_fmt_name(ctx->pix_fmt);
CMVR_LOG(INFO) << "[CameraStreamEncoder] initialized " << codec->name
<< " encoder, size=" << width << "x" << height
<< ", fps=" << fps;
<< ", fps=" << fps
<< ", pixel_format=" << (pixel_format_name ? pixel_format_name : "unknown");
return true;
}
@ -343,24 +419,30 @@ bool CameraStreamEncoder::encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,
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);
encoder->sws_context = sws_getCachedContext(encoder->sws_context,
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 int writable_ret = av_frame_make_writable(encoder->frame);
if (writable_ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(writable_ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] frame is not writable: " << errbuf;
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)};
@ -376,49 +458,125 @@ bool CameraStreamEncoder::encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,
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 encodePreparedFrame(*encoder, encoded_frame, is_key);
}
bool CameraStreamEncoder::encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,
const AVFrame* 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 || !frame) {
return false;
}
while (true) {
ret = avcodec_receive_packet(encoder->codec_context, encoder->packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
const AVFrame* source_frame = frame;
if (frame->hw_frames_ctx) {
if (!encoder->transfer_frame) {
encoder->transfer_frame = av_frame_alloc();
}
if (ret < 0) {
if (!encoder->transfer_frame) {
return false;
}
av_frame_unref(encoder->transfer_frame);
const int transfer_ret = av_hwframe_transfer_data(encoder->transfer_frame, frame, 0);
if (transfer_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;
av_strerror(transfer_ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to download hardware frame: " << errbuf;
return false;
}
if (encoder->packet->flags & AV_PKT_FLAG_KEY) {
is_key = true;
const int props_ret = av_frame_copy_props(encoder->transfer_frame, frame);
if (props_ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(props_ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to copy hardware frame properties: " << errbuf;
return false;
}
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);
source_frame = encoder->transfer_frame;
}
if (encoded_frame.size() < 4) {
const auto source_format = static_cast<AVPixelFormat>(source_frame->format);
if (source_format == AV_PIX_FMT_NONE) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] source frame has no pixel format";
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";
const int writable_ret = av_frame_make_writable(encoder->frame);
if (writable_ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(writable_ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] frame is not writable: " << errbuf;
return false;
}
return true;
int copy_ret = 0;
if (source_frame->width == encoder->codec_context->width
&& source_frame->height == encoder->codec_context->height
&& source_format == encoder->codec_context->pix_fmt) {
copy_ret = av_frame_copy(encoder->frame, source_frame);
} else {
encoder->sws_context = sws_getCachedContext(
encoder->sws_context,
source_frame->width,
source_frame->height,
source_format,
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 native frame conversion context";
return false;
}
const int scaled_rows = sws_scale(
encoder->sws_context,
source_frame->data,
source_frame->linesize,
0,
source_frame->height,
encoder->frame->data,
encoder->frame->linesize);
copy_ret = scaled_rows == encoder->codec_context->height
? 0
: scaled_rows < 0 ? scaled_rows : AVERROR_INVALIDDATA;
}
if (copy_ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(copy_ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to copy native frame: " << errbuf;
return false;
}
if (options.draw_timestamp) {
const auto output_format = static_cast<AVPixelFormat>(encoder->frame->format);
if (output_format != AV_PIX_FMT_NV12 && output_format != AV_PIX_FMT_YUV420P) {
const char* format_name = av_get_pix_fmt_name(output_format);
CMVR_LOG(ERROR) << "[CameraStreamEncoder] cannot draw timestamp on pixel format "
<< (format_name ? format_name : "unknown");
return false;
}
if (!encoder->frame->data[0]
|| encoder->frame->linesize[0] < encoder->frame->width) {
CMVR_LOG(ERROR) << "[CameraStreamEncoder] invalid luma plane for timestamp";
return false;
}
cv::Mat luma_plane(
encoder->frame->height,
encoder->frame->width,
CV_8UC1,
encoder->frame->data[0],
static_cast<size_t>(encoder->frame->linesize[0]));
drawTimeStamp(luma_plane);
}
return encodePreparedFrame(*encoder, encoded_frame, is_key);
}
bool CameraStreamEncoder::encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,

View File

@ -0,0 +1,122 @@
add_library(hikvision_camera SHARED src/hikvision_camera.cpp)
target_include_directories(hikvision_camera PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set(HIKVISION_SDK_ROOT "${CMAKE_SOURCE_DIR}/dependency/${ARCH}/third_party/hikvision_sdk/v6.1.11.5")
set(HIKVISION_SDK_LIB_DIR "${HIKVISION_SDK_ROOT}/lib")
set(HIKVISION_SDK_COM_DIR "${HIKVISION_SDK_LIB_DIR}/HCNetSDKCom")
file(GLOB HIKVISION_SDK_LIBS CONFIGURE_DEPENDS
"${HIKVISION_SDK_LIB_DIR}/*.so"
"${HIKVISION_SDK_LIB_DIR}/*.so.*"
)
file(GLOB HIKVISION_SDK_COM_LIBS CONFIGURE_DEPENDS
"${HIKVISION_SDK_COM_DIR}/*.so"
"${HIKVISION_SDK_COM_DIR}/*.so.*"
)
set(HIKVISION_HCNETSDK_LIB "${HIKVISION_SDK_LIB_DIR}/libhcnetsdk.so")
if(NOT HIKVISION_SDK_LIBS)
message(FATAL_ERROR "Hikvision SDK libraries not found: ${HIKVISION_SDK_LIB_DIR}")
endif()
if(NOT HIKVISION_SDK_COM_LIBS)
message(FATAL_ERROR "Hikvision SDK component libraries not found: ${HIKVISION_SDK_COM_DIR}")
endif()
if(NOT EXISTS "${HIKVISION_HCNETSDK_LIB}")
message(FATAL_ERROR "Hikvision HCNetSDK library not found: ${HIKVISION_HCNETSDK_LIB}")
endif()
target_include_directories(hikvision_camera PRIVATE "${HIKVISION_SDK_ROOT}/include")
target_link_directories(hikvision_camera PRIVATE "${HIKVISION_SDK_LIB_DIR}" "${HIKVISION_SDK_COM_DIR}")
set(HIKVISION_JSONCPP_LINK jsoncpp)
if(TARGET jsoncpp)
get_target_property(HIKVISION_JSONCPP_IMPORTED_LOCATION jsoncpp IMPORTED_LOCATION)
get_target_property(HIKVISION_JSONCPP_IMPORTED_LOCATION_RELEASE jsoncpp IMPORTED_LOCATION_RELEASE)
get_target_property(HIKVISION_JSONCPP_INTERFACE_INCLUDES jsoncpp INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "[hikvision_camera] jsoncpp target: jsoncpp")
message(STATUS "[hikvision_camera] jsoncpp IMPORTED_LOCATION: ${HIKVISION_JSONCPP_IMPORTED_LOCATION}")
message(STATUS "[hikvision_camera] jsoncpp IMPORTED_LOCATION_RELEASE: ${HIKVISION_JSONCPP_IMPORTED_LOCATION_RELEASE}")
message(STATUS "[hikvision_camera] jsoncpp INTERFACE_INCLUDE_DIRECTORIES: ${HIKVISION_JSONCPP_INTERFACE_INCLUDES}")
else()
find_library(HIKVISION_JSONCPP_LIBRARY NAMES jsoncpp)
if(HIKVISION_JSONCPP_LIBRARY)
set(HIKVISION_JSONCPP_LINK "${HIKVISION_JSONCPP_LIBRARY}")
message(STATUS "[hikvision_camera] jsoncpp library: ${HIKVISION_JSONCPP_LIBRARY}")
else()
message(WARNING "[hikvision_camera] jsoncpp library not found by CMake; linker will resolve -ljsoncpp")
endif()
endif()
find_path(HIKVISION_JSONCPP_INCLUDE_DIR NAMES json/json.h PATH_SUFFIXES jsoncpp)
message(STATUS "[hikvision_camera] jsoncpp include dir: ${HIKVISION_JSONCPP_INCLUDE_DIR}")
if(HIKVISION_JSONCPP_INCLUDE_DIR)
target_include_directories(hikvision_camera PRIVATE "${HIKVISION_JSONCPP_INCLUDE_DIR}")
endif()
target_link_libraries(hikvision_camera
PUBLIC
glog
opencv_core
cmvr_es::proto
PRIVATE
"${HIKVISION_HCNETSDK_LIB}"
${HIKVISION_JSONCPP_LINK}
pthread
)
set_target_properties(hikvision_camera PROPERTIES
BUILD_RPATH "${HIKVISION_SDK_LIB_DIR};${HIKVISION_SDK_COM_DIR}"
INSTALL_RPATH "$ORIGIN;$ORIGIN/HCNetSDKCom"
)
add_library(cmvr_es::device::hikvision_camera ALIAS hikvision_camera)
install(TARGETS hikvision_camera LIBRARY DESTINATION lib)
install(FILES ${HIKVISION_SDK_LIBS} DESTINATION lib)
install(DIRECTORY "${HIKVISION_SDK_COM_DIR}" DESTINATION lib)
if(BUILD_TESTING)
add_executable(hikvision_camera_callback_test
tests/hikvision_camera_callback_test.cpp
src/hikvision_camera.cpp
)
target_include_directories(hikvision_camera_callback_test
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
"${HIKVISION_SDK_ROOT}/include"
)
if(HIKVISION_JSONCPP_INCLUDE_DIR)
target_include_directories(
hikvision_camera_callback_test
PRIVATE
"${HIKVISION_JSONCPP_INCLUDE_DIR}"
)
endif()
target_compile_definitions(hikvision_camera_callback_test
PRIVATE
HIKVISION_SDK_LIB_DIR="${HIKVISION_SDK_LIB_DIR}/"
)
# The test supplies a small in-process HCNetSDK fake, so it exercises the
# callback/stop ordering without requiring a camera or loading hcnetsdk.
target_link_libraries(hikvision_camera_callback_test
PRIVATE
glog
opencv_core
cmvr_es::proto
${HIKVISION_JSONCPP_LINK}
pthread
)
add_test(
NAME hikvision_camera_callback_test
COMMAND hikvision_camera_callback_test
)
set_tests_properties(hikvision_camera_callback_test PROPERTIES TIMEOUT 10)
if(UNIX AND NOT APPLE)
get_property(_hikvision_test_library_dirs DIRECTORY PROPERTY LINK_DIRECTORIES)
list(PREPEND _hikvision_test_library_dirs "${CMAKE_BINARY_DIR}/cmvr_compiler_runtime")
list(JOIN _hikvision_test_library_dirs ":" _hikvision_test_library_path)
set_tests_properties(hikvision_camera_callback_test PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${_hikvision_test_library_path}"
)
endif()
endif()

View File

@ -0,0 +1,117 @@
#ifndef CMVR_ES_HIKVISION_CAMERA_H
#define CMVR_ES_HIKVISION_CAMERA_H
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "common/base/ring_buffer.h"
#include "devices/camera/abstract_camera.h"
namespace cmvr::device {
class HikvisionCamera final : public AbstractCamera {
public:
explicit HikvisionCamera(const cmvr::config::HikvisionCameraConfig& camera);
~HikvisionCamera() override;
std::string typeName() const override { return "HikvisionCamera"; }
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
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;
void startRecording(const std::string& video_path) override;
void stopRecording() override;
void pauseRecording() override;
void resumeRecording() override;
void getEncodedFrame(StreamFrameData& frame_data, size_t& index) override;
bool waitEncodedFrame(StreamFrameData& frame_data, size_t& index, std::chrono::milliseconds timeout) override;
bool getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index) override;
bool startStreaming() override;
void stopStreaming() override;
bool controlPtz(PtzCommand command, bool stop, int speed) override;
bool executeJsonCommand(const std::string& request_json, std::string& response_json);
bool requestKeyFrame() override;
void onEsData(long real_handle,
unsigned int packet_type,
unsigned char* buffer,
unsigned int buffer_size,
unsigned int width,
unsigned int height,
uint64_t source_timestamp,
uint64_t source_frame_number,
unsigned int source_frame_rate,
unsigned int source_packet_mode);
private:
bool initSdk_();
void releaseSdk_();
bool login_();
bool startPreview_();
void stopPreview_();
bool requestKeyFrame_();
void stopRecordingUnlocked_();
void fillIntrinsics_(Rs2Intrinsics& intrinsics) const;
void setError_(const std::string& message);
std::string sdkError_(const std::string& action) const;
void pushEncodedFrame_(const unsigned char* buffer,
unsigned int buffer_size,
bool is_key_frame,
unsigned int width,
unsigned int height,
uint64_t source_timestamp,
uint64_t source_frame_number,
unsigned int source_frame_rate,
unsigned int source_packet_mode);
void resetStreamState_();
config::HikvisionCameraConfig camera_;
std::string ip_;
std::string username_;
std::string password_;
std::string sdk_path_;
int port_ = 8000;
int channel_ = 1;
int stream_type_ = 0;
int link_mode_ = 0;
int fps_ = 25;
int width_ = 0;
int height_ = 0;
size_t buffer_size_ = 30;
std::string codec_ = "H264";
int user_id_ = -1;
int real_handle_ = -1;
bool sdk_acquired_ = false;
std::shared_ptr<SPMCRingBuffer<StreamFrameData>> stream_frame_buffer_;
std::string current_video_path_;
mutable std::mutex ctrl_mtx_;
// SDK callbacks must never take ctrl_mtx_: NET_DVR_StopRealPlay may wait
// for an in-flight callback while stop() owns that mutex. This mutex is the
// single synchronization domain for callback publication and stream state.
mutable std::mutex callback_mtx_;
bool callback_publishing_enabled_ = false;
long callback_preview_handle_ = -1;
std::vector<uint8_t> es_stream_header_;
bool has_es_stream_header_ = false;
bool awaiting_key_frame_ = false;
int stream_count_ = 0;
uint64_t stream_epoch_ = 0;
uint64_t stream_sequence_ = 0;
uint32_t codec_config_generation_ = 0;
};
} // namespace cmvr::device
#endif // CMVR_ES_HIKVISION_CAMERA_H

View File

@ -0,0 +1,971 @@
#include "../include/hikvision_camera.h"
#include <algorithm>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <limits.h>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#if defined(__linux__)
#include <unistd.h>
#endif
#include "HCNetSDK.h"
#include "common/base/logging/logger.h"
#include "json/json.h"
namespace {
std::mutex g_sdk_mutex;
int g_sdk_ref_count = 0;
bool g_sdk_initialized = false;
std::atomic<int> g_ignored_data_type_log_count{0};
std::atomic<int> g_es_video_log_count{0};
std::atomic<int> g_unexpected_packet_mode_log_count{0};
constexpr DWORD kHikvisionPacketFileHeader = 0;
constexpr DWORD kHikvisionPacketVideoIFrame = 1;
constexpr DWORD kHikvisionPacketVideoBFrame = 2;
constexpr DWORD kHikvisionPacketVideoPFrame = 3;
std::string normalizeSdkPath(std::string path)
{
if (path.empty()) {
return path;
}
const char last = path.back();
if (last != '/' && last != '\\') {
path.push_back('/');
}
return path;
}
std::string defaultRuntimeSdkPath()
{
#if defined(__linux__)
char executable_path[PATH_MAX] = {0};
const ssize_t count = readlink("/proc/self/exe", executable_path, PATH_MAX - 1);
if (count > 0) {
executable_path[count] = '\0';
const auto lib_path =
std::filesystem::path(executable_path).parent_path() / ".." / "lib";
return normalizeSdkPath(std::filesystem::weakly_canonical(lib_path).string());
}
#endif
return normalizeSdkPath((std::filesystem::current_path() / ".." / "lib").string());
}
void copyCString(char* dest, size_t dest_size, const std::string& source)
{
if (dest_size == 0) {
return;
}
std::snprintf(dest, dest_size, "%s", source.c_str());
}
DWORD toHikvisionPtzCommand(cmvr::device::PtzCommand command)
{
switch (command) {
case cmvr::device::PtzCommand::TiltUp:
return TILT_UP;
case cmvr::device::PtzCommand::TiltDown:
return TILT_DOWN;
case cmvr::device::PtzCommand::PanLeft:
return PAN_LEFT;
case cmvr::device::PtzCommand::PanRight:
return PAN_RIGHT;
case cmvr::device::PtzCommand::UpLeft:
return UP_LEFT;
case cmvr::device::PtzCommand::UpRight:
return UP_RIGHT;
case cmvr::device::PtzCommand::DownLeft:
return DOWN_LEFT;
case cmvr::device::PtzCommand::DownRight:
return DOWN_RIGHT;
case cmvr::device::PtzCommand::ZoomIn:
return ZOOM_IN;
case cmvr::device::PtzCommand::ZoomOut:
return ZOOM_OUT;
case cmvr::device::PtzCommand::PanAuto:
return PAN_AUTO;
}
return 0;
}
DWORD normalizePtzSpeed(int speed)
{
if (speed <= 0) {
return 4;
}
if (speed > 7) {
return 7;
}
return static_cast<DWORD>(speed);
}
std::string lowerString(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return value;
}
std::string jsonEscape(const std::string& value)
{
std::ostringstream out;
for (const char c : value) {
switch (c) {
case '\\':
out << "\\\\";
break;
case '"':
out << "\\\"";
break;
case '\n':
out << "\\n";
break;
case '\r':
out << "\\r";
break;
case '\t':
out << "\\t";
break;
default:
out << c;
break;
}
}
return out.str();
}
std::string jsonStringField(const Json::Value& value, const char* name)
{
const Json::Value* member = value.find(name, name + std::strlen(name));
return member && member->isString() ? member->asString() : "";
}
bool jsonBoolField(const Json::Value& value, const char* name, bool& out)
{
const Json::Value* member = value.find(name, name + std::strlen(name));
if (!member || !member->isBool()) {
return false;
}
out = member->asBool();
return true;
}
int jsonIntField(const Json::Value& value, const char* name, int default_value)
{
const Json::Value* member = value.find(name, name + std::strlen(name));
return member && member->isInt() ? member->asInt() : default_value;
}
bool parseJsonCommand(const std::string& request_json, Json::Value& root, std::string& error)
{
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
return reader->parse(
request_json.data(), request_json.data() + request_json.size(), &root, &error);
}
bool parsePtzCommandName(const std::string& command, cmvr::device::PtzCommand& out)
{
const std::string normalized = lowerString(command);
if (normalized == "tilt_up" || normalized == "up") {
out = cmvr::device::PtzCommand::TiltUp;
} else if (normalized == "tilt_down" || normalized == "down") {
out = cmvr::device::PtzCommand::TiltDown;
} else if (normalized == "pan_left" || normalized == "left") {
out = cmvr::device::PtzCommand::PanLeft;
} else if (normalized == "pan_right" || normalized == "right") {
out = cmvr::device::PtzCommand::PanRight;
} else if (normalized == "up_left") {
out = cmvr::device::PtzCommand::UpLeft;
} else if (normalized == "up_right") {
out = cmvr::device::PtzCommand::UpRight;
} else if (normalized == "down_left") {
out = cmvr::device::PtzCommand::DownLeft;
} else if (normalized == "down_right") {
out = cmvr::device::PtzCommand::DownRight;
} else if (normalized == "zoom_in") {
out = cmvr::device::PtzCommand::ZoomIn;
} else if (normalized == "zoom_out") {
out = cmvr::device::PtzCommand::ZoomOut;
} else if (normalized == "pan_auto" || normalized == "auto") {
out = cmvr::device::PtzCommand::PanAuto;
} else {
return false;
}
return true;
}
std::string makeJsonResult(bool success, const std::string& error_message = "")
{
std::ostringstream out;
out << "{\"success\":" << (success ? "true" : "false");
if (!error_message.empty()) {
out << ",\"error_message\":\"" << jsonEscape(error_message) << "\"";
}
out << "}";
return out.str();
}
void CALLBACK hikvisionEsRealPlayCallback(
LONG real_handle, NET_DVR_PACKET_INFO_EX* packet_info, void* user)
{
auto* camera = static_cast<cmvr::device::HikvisionCamera*>(user);
if (!camera || !packet_info) {
return;
}
const uint64_t source_timestamp =
(static_cast<uint64_t>(packet_info->dwTimeStampHigh) << 32U) |
static_cast<uint64_t>(packet_info->dwTimeStamp);
camera->onEsData(real_handle,
packet_info->dwPacketType,
packet_info->pPacketBuffer,
packet_info->dwPacketSize,
packet_info->wWidth,
packet_info->wHeight,
source_timestamp,
packet_info->dwFrameNum,
packet_info->dwFrameRate,
packet_info->dwPacketMode);
}
} // namespace
namespace cmvr::device {
HikvisionCamera::HikvisionCamera(const config::HikvisionCameraConfig& camera)
: camera_(camera)
{
id_ = camera_.id();
ip_ = camera_.ip();
username_ = camera_.username().empty() ? "admin" : camera_.username();
password_ = camera_.password();
port_ = camera_.port() > 0 ? camera_.port() : 8000;
channel_ = camera_.channel() > 0 ? camera_.channel() : 1;
stream_type_ = camera_.stream_type() >= 0 ? camera_.stream_type() : 0;
link_mode_ = camera_.link_mode() >= 0 ? camera_.link_mode() : 0;
fps_ = camera_.fps() > 0 ? camera_.fps() : 25;
width_ = camera_.width();
height_ = camera_.height();
buffer_size_ = camera_.buffer_size() > 0 ? static_cast<size_t>(camera_.buffer_size()) : 30;
codec_ = camera_.codec().empty() ? "H264" : camera_.codec();
sdk_path_ = normalizeSdkPath(
camera_.sdk_path().empty() ? defaultRuntimeSdkPath() : camera_.sdk_path());
// Keep the shared_ptr itself immutable after construction. Readers and the
// SDK callback may use it concurrently; the ring buffer owns its locking.
stream_frame_buffer_ = std::make_shared<SPMCRingBuffer<StreamFrameData>>(buffer_size_);
state_.fps = fps_;
state_.width = width_;
state_.height = height_;
if (id_.empty()) {
setError_("[HikvisionCamera] camera id is empty");
} else if (ip_.empty()) {
setError_("[HikvisionCamera] ip is empty");
} else if (password_.empty()) {
setError_("[HikvisionCamera] password is empty");
}
}
HikvisionCamera::~HikvisionCamera()
{
stop();
}
bool HikvisionCamera::init()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
state_.is_initialized = false;
if (ip_.empty() || username_.empty() || password_.empty()) {
setError_("ip, username or password is empty");
return false;
}
stream_frame_buffer_->clear();
resetStreamState_();
if (!initSdk_()) {
return false;
}
state_.is_initialized = true;
state_.is_error = false;
CMVR_LOG(INFO) << "[HikvisionCamera] initialized: id=" << id_
<< ", ip=" << ip_
<< ", channel=" << channel_
<< ", stream_type=" << stream_type_;
return true;
}
bool HikvisionCamera::start()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_initialized) {
setError_("camera not initialized");
return false;
}
if (state_.is_opened) {
return true;
}
if (!login_()) {
return false;
}
if (!startPreview_()) {
if (user_id_ >= 0) {
NET_DVR_Logout(user_id_);
user_id_ = -1;
}
return false;
}
state_.is_opened = true;
return true;
}
bool HikvisionCamera::stop()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (state_.is_recording) {
stopRecordingUnlocked_();
}
state_.is_streaming = false;
stream_count_ = 0;
resetStreamState_();
stopPreview_();
if (user_id_ >= 0) {
NET_DVR_Logout(user_id_);
user_id_ = -1;
}
state_.is_opened = false;
releaseSdk_();
return true;
}
void HikvisionCamera::getState(CameraState& state)
{
std::lock_guard lock(ctrl_mtx_);
state = state_;
}
void HikvisionCamera::getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics)
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
fillIntrinsics_(intrinsics);
setError_("getRGBImage unsupported: HikvisionCamera only forwards stream data");
color.release();
}
void HikvisionCamera::getDepthImage(cv::Mat& depth, Rs2Intrinsics&)
{
std::lock_guard lock(ctrl_mtx_);
setError_("getDepthImage unsupported usage");
depth.release();
}
void HikvisionCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics& intrinsics)
{
getRGBImage(color, intrinsics);
depth.release();
}
void HikvisionCamera::startRecording(const std::string& video_path)
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened || real_handle_ < 0) {
setError_("camera not opened");
return;
}
if (state_.is_recording) {
setError_("already recording");
return;
}
if (video_path.empty()) {
setError_("video path is empty");
return;
}
current_video_path_ = video_path;
if (!NET_DVR_SaveRealData(real_handle_, const_cast<char*>(current_video_path_.c_str()))) {
setError_(sdkError_("NET_DVR_SaveRealData"));
current_video_path_.clear();
return;
}
state_.is_recording = true;
}
void HikvisionCamera::stopRecording()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
stopRecordingUnlocked_();
}
void HikvisionCamera::stopRecordingUnlocked_()
{
clear_error_();
if (!state_.is_recording) {
return;
}
if (real_handle_ >= 0 && !NET_DVR_StopSaveRealData(real_handle_)) {
setError_(sdkError_("NET_DVR_StopSaveRealData"));
return;
}
state_.is_recording = false;
current_video_path_.clear();
}
void HikvisionCamera::pauseRecording()
{
std::lock_guard lock(ctrl_mtx_);
setError_("pauseRecording unsupported by Hikvision SDK recording");
}
void HikvisionCamera::resumeRecording()
{
std::lock_guard lock(ctrl_mtx_);
setError_("resumeRecording unsupported by Hikvision SDK recording");
}
void HikvisionCamera::getEncodedFrame(StreamFrameData& frame_data, size_t& index)
{
if (!stream_frame_buffer_) {
return;
}
auto frame = stream_frame_buffer_->pop(index);
if (frame) {
frame_data = std::move(*frame);
}
}
bool HikvisionCamera::waitEncodedFrame(
StreamFrameData& frame_data,
size_t& index,
const std::chrono::milliseconds timeout)
{
if (!stream_frame_buffer_) {
return false;
}
const auto deadline = std::chrono::steady_clock::now() + timeout;
do {
auto frame = stream_frame_buffer_->pop(index);
if (frame) {
frame_data = std::move(*frame);
return !frame_data.rgbFrame.empty() || !frame_data.depthFrame.empty();
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
} while (std::chrono::steady_clock::now() < deadline);
return false;
}
bool HikvisionCamera::getLatestEncodedFrame(StreamFrameData& frame_data, size_t& next_index)
{
if (!stream_frame_buffer_) {
return false;
}
const size_t head = stream_frame_buffer_->getHead();
if (head == 0) {
return false;
}
next_index = head - 1;
auto frame = stream_frame_buffer_->pop(next_index);
if (!frame.has_value()) {
return false;
}
frame_data = std::move(*frame);
return true;
}
bool HikvisionCamera::startStreaming()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened) {
setError_("camera not opened");
return false;
}
const bool first_stream = stream_count_ == 0;
if (first_stream) {
if (stream_frame_buffer_) {
stream_frame_buffer_->clear();
}
std::lock_guard callback_lock(callback_mtx_);
if (callback_preview_handle_ < 0 ||
callback_preview_handle_ != static_cast<long>(real_handle_)) {
setError_("preview callback is not active");
return false;
}
++stream_epoch_;
stream_sequence_ = 0;
++codec_config_generation_;
awaiting_key_frame_ = true;
callback_publishing_enabled_ = true;
}
state_.is_streaming = true;
++stream_count_;
if (first_stream && !requestKeyFrame_()) {
// Keep waiting for the next natural I-frame. Publishing P/B frames
// immediately would make a newly attached decoder start corrupted.
CMVR_LOG(WARNING) << "[HikvisionCamera] key-frame request failed; "
<< "waiting for the next natural I-frame";
}
return true;
}
void HikvisionCamera::stopStreaming()
{
std::lock_guard lock(ctrl_mtx_);
if (stream_count_ > 0) {
--stream_count_;
}
if (stream_count_ == 0) {
// Wait for an already-running callback to finish its publication, then
// prevent both queued and future callbacks from publishing. No frame
// can be pushed after this critical section has completed.
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
awaiting_key_frame_ = false;
state_.is_streaming = false;
}
}
bool HikvisionCamera::controlPtz(PtzCommand command, bool stop, int speed)
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened || user_id_ < 0) {
setError_("camera not opened");
return false;
}
const DWORD sdk_command = toHikvisionPtzCommand(command);
if (sdk_command == 0) {
setError_("unsupported PTZ command");
return false;
}
const DWORD sdk_stop = stop ? 1 : 0;
const DWORD sdk_speed = normalizePtzSpeed(speed);
if (!NET_DVR_PTZControlWithSpeed_Other(user_id_, channel_, sdk_command, sdk_stop, sdk_speed)) {
setError_(sdkError_("NET_DVR_PTZControlWithSpeed_Other"));
return false;
}
return true;
}
bool HikvisionCamera::requestKeyFrame()
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened || user_id_ < 0) {
setError_("camera not opened");
return false;
}
if (!requestKeyFrame_()) {
setError_(sdkError_(
stream_type_ == 0 ? "NET_DVR_MakeKeyFrame" : "NET_DVR_MakeKeyFrameSub"));
return false;
}
return true;
}
bool HikvisionCamera::requestKeyFrame_()
{
if (user_id_ < 0) {
CMVR_LOG(WARNING) << "[HikvisionCamera] skip key frame request: camera not logged in";
return false;
}
const bool is_sub_stream = stream_type_ != 0;
const BOOL success = is_sub_stream
? NET_DVR_MakeKeyFrameSub(user_id_, channel_)
: NET_DVR_MakeKeyFrame(user_id_, channel_);
if (!success) {
CMVR_LOG(WARNING) << "[HikvisionCamera] "
<< (is_sub_stream ? "NET_DVR_MakeKeyFrameSub" : "NET_DVR_MakeKeyFrame")
<< " failed, error_code=" << NET_DVR_GetLastError();
return false;
}
CMVR_LOG(INFO) << "[HikvisionCamera] requested key frame"
<< ", channel=" << channel_
<< ", stream_type=" << stream_type_;
return true;
}
bool HikvisionCamera::executeJsonCommand(const std::string& request_json, std::string& response_json)
{
Json::Value root;
std::string parse_error;
if (!parseJsonCommand(request_json, root, parse_error) || !root.isObject()) {
response_json = makeJsonResult(false, "invalid json: " + parse_error);
return false;
}
const std::string command_type = lowerString(
!jsonStringField(root, "command").empty() ? jsonStringField(root, "command") :
!jsonStringField(root, "type").empty() ? jsonStringField(root, "type") :
jsonStringField(root, "action"));
if (command_type != "ptz") {
response_json = makeJsonResult(false, "unsupported json command: " + command_type);
return false;
}
const std::string ptz_command_name =
!jsonStringField(root, "direction").empty() ? jsonStringField(root, "direction") :
!jsonStringField(root, "ptz_command").empty() ? jsonStringField(root, "ptz_command") :
jsonStringField(root, "operation");
PtzCommand ptz_command{};
if (!parsePtzCommandName(ptz_command_name, ptz_command)) {
response_json = makeJsonResult(false, "invalid ptz command: " + ptz_command_name);
return false;
}
bool stop = false;
if (!jsonBoolField(root, "stop", stop)) {
const std::string ptz_action = lowerString(jsonStringField(root, "ptz_action"));
const std::string action = lowerString(jsonStringField(root, "action"));
stop = ptz_action == "stop" || action == "stop";
}
const int speed = jsonIntField(root, "speed", 0);
if (!controlPtz(ptz_command, stop, speed)) {
CameraState state;
getState(state);
response_json = makeJsonResult(
false,
state.error_message.empty() ? "failed to control PTZ" : state.error_message);
return false;
}
std::ostringstream result;
result << "{\"success\":true"
<< ",\"command\":\"ptz\""
<< ",\"direction\":\"" << jsonEscape(ptz_command_name) << "\""
<< ",\"stop\":" << (stop ? "true" : "false")
<< ",\"speed\":" << static_cast<int>(normalizePtzSpeed(speed))
<< "}";
response_json = result.str();
return true;
}
void HikvisionCamera::onEsData(
const long real_handle,
const unsigned int packet_type,
unsigned char* buffer,
const unsigned int buffer_size,
const unsigned int packet_width,
const unsigned int packet_height,
const uint64_t source_timestamp,
const uint64_t source_frame_number,
const unsigned int source_frame_rate,
const unsigned int source_packet_mode)
{
if (!buffer || buffer_size == 0) {
return;
}
// Never take ctrl_mtx_ from an SDK callback. NET_DVR_StopRealPlay may wait
// for this callback while stop() owns ctrl_mtx_. Holding callback_mtx_
// through publication also preserves the ring's single-producer contract.
std::lock_guard callback_lock(callback_mtx_);
if (callback_preview_handle_ != real_handle || !stream_frame_buffer_) {
return;
}
if (packet_type == kHikvisionPacketFileHeader) {
const bool changed =
!has_es_stream_header_ ||
es_stream_header_.size() != buffer_size ||
!std::equal(es_stream_header_.begin(), es_stream_header_.end(), buffer);
if (changed) {
es_stream_header_.assign(buffer, buffer + buffer_size);
has_es_stream_header_ = true;
++codec_config_generation_;
if (callback_publishing_enabled_) {
awaiting_key_frame_ = true;
}
CMVR_LOG(INFO) << "[HikvisionCamera] received ES stream header, size=" << buffer_size;
}
return;
}
const bool is_video_packet =
packet_type == kHikvisionPacketVideoIFrame ||
packet_type == kHikvisionPacketVideoBFrame ||
packet_type == kHikvisionPacketVideoPFrame;
if (!is_video_packet) {
const int log_count = g_ignored_data_type_log_count.fetch_add(1);
if (log_count < 10) {
CMVR_LOG(INFO) << "[HikvisionCamera] ignore ES packet_type="
<< packet_type << ", size=" << buffer_size;
}
return;
}
const int video_log_count = g_es_video_log_count.fetch_add(1);
if (video_log_count < 10) {
CMVR_LOG(INFO) << "[HikvisionCamera] ES video packet_type="
<< packet_type << ", size=" << buffer_size
<< ", source_timestamp=" << source_timestamp
<< ", source_frame_number=" << source_frame_number
<< ", source_frame_rate=" << source_frame_rate
<< ", source_packet_mode=" << source_packet_mode;
}
if (!callback_publishing_enabled_) {
return;
}
const bool is_key_frame = packet_type == kHikvisionPacketVideoIFrame;
if (awaiting_key_frame_) {
if (!is_key_frame) {
return;
}
awaiting_key_frame_ = false;
CMVR_LOG(INFO) << "[HikvisionCamera] received first key frame after stream start";
}
pushEncodedFrame_(buffer,
buffer_size,
is_key_frame,
packet_width,
packet_height,
source_timestamp,
source_frame_number,
source_frame_rate,
source_packet_mode);
}
void HikvisionCamera::pushEncodedFrame_(
const unsigned char* buffer,
unsigned int buffer_size,
bool is_key_frame,
unsigned int packet_width,
unsigned int packet_height,
uint64_t source_timestamp,
uint64_t source_frame_number,
unsigned int source_frame_rate,
unsigned int source_packet_mode)
{
if (!buffer || buffer_size == 0 || !stream_frame_buffer_) {
return;
}
StreamFrameData frame_data;
const auto capture_monotonic = std::chrono::steady_clock::now();
const auto capture_utc = std::chrono::system_clock::now();
frame_data.rgbFrame.assign(buffer, buffer + buffer_size);
frame_data.codec = codec_;
frame_data.fps =
source_frame_rate >= 1U && source_frame_rate <= 1000U
? static_cast<int>(source_frame_rate)
: fps_;
frame_data.width = packet_width > 0 ? static_cast<int>(packet_width) : width_;
frame_data.height = packet_height > 0 ? static_cast<int>(packet_height) : height_;
frame_data.bKey = is_key_frame;
frame_data.stream_epoch = stream_epoch_;
frame_data.sequence = stream_sequence_++;
frame_data.source_timestamp = source_timestamp;
frame_data.source_frame_number = source_frame_number;
frame_data.capture_monotonic_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
capture_monotonic.time_since_epoch()).count();
frame_data.capture_utc_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
capture_utc.time_since_epoch()).count();
frame_data.pts = static_cast<int64_t>(frame_data.sequence);
frame_data.dts = frame_data.pts;
frame_data.time_base_num = 1;
frame_data.time_base_den = std::max(1, frame_data.fps);
frame_data.duration = 1;
frame_data.codec_config_generation = codec_config_generation_;
// Hikvision labels packet type 0 as a "file header", but the bundled SDK
// does not guarantee that it is a decoder-ready VPS/SPS/PPS blob. Keep it
// only for change detection until its format is verified on real hardware.
fillIntrinsics_(frame_data.intrinsics);
if (source_packet_mode > 1U) {
const int log_count = g_unexpected_packet_mode_log_count.fetch_add(1);
if (log_count < 5) {
CMVR_LOG(WARNING) << "[HikvisionCamera] unexpected ES source_packet_mode="
<< source_packet_mode
<< ", source_frame_number=" << source_frame_number;
}
}
stream_frame_buffer_->push(frame_data);
}
void HikvisionCamera::resetStreamState_()
{
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = -1;
awaiting_key_frame_ = false;
es_stream_header_.clear();
has_es_stream_header_ = false;
}
bool HikvisionCamera::initSdk_()
{
if (sdk_acquired_) {
return true;
}
std::lock_guard<std::mutex> lock(g_sdk_mutex);
if (g_sdk_ref_count == 0) {
if (!NET_DVR_Init()) {
setError_(sdkError_("NET_DVR_Init"));
return false;
}
NET_DVR_SetConnectTime(3000, 3);
NET_DVR_SetReconnect(10000, TRUE);
NET_DVR_SetLogToFile(3, nullptr, TRUE);
g_sdk_initialized = true;
}
++g_sdk_ref_count;
sdk_acquired_ = true;
return g_sdk_initialized;
}
void HikvisionCamera::releaseSdk_()
{
if (!sdk_acquired_) {
return;
}
std::lock_guard<std::mutex> lock(g_sdk_mutex);
sdk_acquired_ = false;
if (g_sdk_ref_count > 0) {
--g_sdk_ref_count;
}
if (g_sdk_ref_count == 0 && g_sdk_initialized) {
NET_DVR_Cleanup();
g_sdk_initialized = false;
}
}
bool HikvisionCamera::login_()
{
NET_DVR_USER_LOGIN_INFO login_info{};
NET_DVR_DEVICEINFO_V40 device_info{};
copyCString(login_info.sDeviceAddress, sizeof(login_info.sDeviceAddress), ip_);
copyCString(login_info.sUserName, sizeof(login_info.sUserName), username_);
copyCString(login_info.sPassword, sizeof(login_info.sPassword), password_);
login_info.wPort = static_cast<WORD>(port_);
login_info.bUseAsynLogin = FALSE;
user_id_ = NET_DVR_Login_V40(&login_info, &device_info);
if (user_id_ < 0) {
setError_(sdkError_("NET_DVR_Login_V40"));
return false;
}
return true;
}
bool HikvisionCamera::startPreview_()
{
NET_DVR_PREVIEWINFO preview_info{};
preview_info.lChannel = channel_;
preview_info.dwStreamType = static_cast<DWORD>(stream_type_);
preview_info.dwLinkMode = static_cast<DWORD>(link_mode_);
preview_info.hPlayWnd = 0;
preview_info.bBlocked = 1;
preview_info.dwDisplayBufNum = 1;
preview_info.byReconnect = 1;
real_handle_ = NET_DVR_RealPlay_V40(user_id_, &preview_info, nullptr, nullptr);
if (real_handle_ < 0) {
setError_(sdkError_("NET_DVR_RealPlay_V40"));
return false;
}
{
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = static_cast<long>(real_handle_);
awaiting_key_frame_ = false;
}
if (!NET_DVR_SetESRealPlayCallBack(real_handle_, hikvisionEsRealPlayCallback, this)) {
setError_(sdkError_("NET_DVR_SetESRealPlayCallBack"));
{
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = -1;
awaiting_key_frame_ = false;
}
NET_DVR_StopRealPlay(real_handle_);
real_handle_ = -1;
return false;
}
return true;
}
void HikvisionCamera::stopPreview_()
{
const int preview_handle = real_handle_;
{
// Invalidate before asking the SDK to stop. We intentionally release
// callback_mtx_ before NET_DVR_StopRealPlay because that function may
// wait for an SDK callback to return.
std::lock_guard callback_lock(callback_mtx_);
callback_publishing_enabled_ = false;
callback_preview_handle_ = -1;
awaiting_key_frame_ = false;
}
if (preview_handle >= 0) {
NET_DVR_StopRealPlay(preview_handle);
real_handle_ = -1;
}
}
void HikvisionCamera::fillIntrinsics_(Rs2Intrinsics& intrinsics) const
{
intrinsics.fx = camera_.fx();
intrinsics.fy = camera_.fy();
intrinsics.cx = camera_.cx() > 0.0f ? camera_.cx() : static_cast<float>(width_) * 0.5f;
intrinsics.cy = camera_.cy() > 0.0f ? camera_.cy() : static_cast<float>(height_) * 0.5f;
for (int i = 0; i < 5; ++i) {
intrinsics.coeffs[i] = i < camera_.coeffs_size() ? camera_.coeffs(i) : 0.0f;
}
}
void HikvisionCamera::setError_(const std::string& message)
{
state_.is_error = true;
state_.error_message = message;
CMVR_LOG(ERROR) << "[HikvisionCamera] " << message;
}
std::string HikvisionCamera::sdkError_(const std::string& action) const
{
return action + " failed, error_code=" + std::to_string(NET_DVR_GetLastError());
}
} // namespace cmvr::device

View File

@ -0,0 +1,425 @@
#include "../include/hikvision_camera.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "HCNetSDK.h"
namespace {
using EsDataCallback =
void(CALLBACK*)(LONG, NET_DVR_PACKET_INFO_EX*, void*);
constexpr DWORD kFileHeader = 0;
constexpr DWORD kVideoIFrame = 1;
constexpr DWORD kVideoPFrame = 3;
std::mutex g_fake_sdk_mutex;
EsDataCallback g_es_data_callback = nullptr;
void* g_es_data_user = nullptr;
LONG g_real_handle = 42;
std::atomic<int> g_stop_callback_count{0};
std::atomic<int> g_key_frame_request_count{0};
std::atomic<int> g_ptz_call_count{0};
std::atomic<DWORD> g_last_ptz_command{0};
std::atomic<DWORD> g_last_ptz_stop{0};
std::atomic<DWORD> g_last_ptz_speed{0};
void resetFakeSdk()
{
std::lock_guard lock(g_fake_sdk_mutex);
g_es_data_callback = nullptr;
g_es_data_user = nullptr;
g_stop_callback_count = 0;
g_key_frame_request_count = 0;
g_ptz_call_count = 0;
g_last_ptz_command = 0;
g_last_ptz_stop = 0;
g_last_ptz_speed = 0;
}
void emitEsPacket(
const DWORD packet_type,
const LONG real_handle,
std::vector<BYTE> payload,
const WORD width = 640,
const WORD height = 360,
const DWORD timestamp_low = 0,
const DWORD timestamp_high = 0,
const DWORD frame_number = 0,
const DWORD frame_rate = 0,
const DWORD packet_mode = 0)
{
EsDataCallback callback = nullptr;
void* user = nullptr;
{
std::lock_guard lock(g_fake_sdk_mutex);
callback = g_es_data_callback;
user = g_es_data_user;
}
if (!callback) {
return;
}
NET_DVR_PACKET_INFO_EX packet{};
packet.wWidth = width;
packet.wHeight = height;
packet.dwTimeStamp = timestamp_low;
packet.dwTimeStampHigh = timestamp_high;
packet.dwFrameNum = frame_number;
packet.dwFrameRate = frame_rate;
packet.dwPacketType = packet_type;
packet.dwPacketSize = static_cast<DWORD>(payload.size());
packet.pPacketBuffer = payload.data();
packet.dwPacketMode = packet_mode;
callback(real_handle, &packet, user);
}
void emitIFrame(
const LONG real_handle = g_real_handle,
const DWORD timestamp_low = 0,
const DWORD timestamp_high = 0,
const DWORD frame_number = 0,
const DWORD frame_rate = 0,
const DWORD packet_mode = 0)
{
emitEsPacket(
kVideoIFrame,
real_handle,
{0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x21},
640,
360,
timestamp_low,
timestamp_high,
frame_number,
frame_rate,
packet_mode);
}
void emitPFrame(const LONG real_handle = g_real_handle)
{
emitEsPacket(
kVideoPFrame,
real_handle,
{0x00, 0x00, 0x00, 0x01, 0x41, 0x9A, 0x20});
}
bool check(const bool condition, const char* expression, const int line)
{
if (condition) {
return true;
}
std::cerr << "CHECK failed at line " << line << ": " << expression << '\n';
return false;
}
#define CHECK_TRUE(expression) \
do { \
if (!check(static_cast<bool>(expression), #expression, __LINE__)) { \
return false; \
} \
} while (false)
cmvr::config::HikvisionCameraConfig makeConfig()
{
cmvr::config::HikvisionCameraConfig config;
config.set_id("hikvision_callback_test");
config.set_ip("127.0.0.1");
config.set_username("admin");
config.set_password("test-only");
config.set_port(8000);
config.set_channel(1);
config.set_stream_type(0);
config.set_link_mode(0);
config.set_width(1920);
config.set_height(1080);
config.set_fps(25);
config.set_codec("H264");
config.set_buffer_size(32);
return config;
}
bool testCallbackPublicationLifecycle()
{
resetFakeSdk();
cmvr::device::HikvisionCamera camera(makeConfig());
CHECK_TRUE(camera.init());
CHECK_TRUE(camera.start());
CHECK_TRUE(camera.startStreaming());
CHECK_TRUE(g_key_frame_request_count.load() == 1);
size_t cursor = 0;
cmvr::device::StreamFrameData frame;
// A new subscriber must not receive an undecodable inter frame, and a
// delayed callback from an older preview handle must also be rejected.
emitPFrame();
emitIFrame(g_real_handle - 1);
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
emitIFrame(
g_real_handle,
0x89ABCDEFU,
0x01234567U,
42U,
30U,
1U);
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.stream_epoch == 1);
CHECK_TRUE(frame.sequence == 0);
CHECK_TRUE(frame.codec_config_generation == 1);
CHECK_TRUE(frame.bKey);
CHECK_TRUE(frame.width == 640);
CHECK_TRUE(frame.height == 360);
CHECK_TRUE(frame.source_timestamp == 0x0123456789ABCDEFULL);
CHECK_TRUE(frame.source_frame_number == 42U);
CHECK_TRUE(frame.fps == 30);
CHECK_TRUE(frame.time_base_den == 30);
CHECK_TRUE(camera.requestKeyFrame());
CHECK_TRUE(g_key_frame_request_count.load() == 2);
CHECK_TRUE(camera.controlPtz(
cmvr::device::PtzCommand::PanLeft, false, 99));
CHECK_TRUE(g_ptz_call_count.load() == 1);
CHECK_TRUE(g_last_ptz_command.load() == PAN_LEFT);
CHECK_TRUE(g_last_ptz_stop.load() == 0);
CHECK_TRUE(g_last_ptz_speed.load() == 7);
std::string json_response;
CHECK_TRUE(camera.executeJsonCommand(
R"({"command":"ptz","direction":"zoom_in","speed":3})",
json_response));
CHECK_TRUE(json_response.find(R"("success":true)") != std::string::npos);
CHECK_TRUE(g_ptz_call_count.load() == 2);
CHECK_TRUE(g_last_ptz_command.load() == ZOOM_IN);
CHECK_TRUE(g_last_ptz_speed.load() == 3);
// A changed SDK file header invalidates the descriptor generation, but is
// not exposed as decoder config until its vendor-specific format is known.
emitEsPacket(kFileHeader, g_real_handle, {0x01, 0x02, 0x03});
emitPFrame();
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
emitIFrame(
g_real_handle,
0x76543210U,
0xFEDCBA98U,
99U,
1001U,
0U);
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.sequence == 1);
CHECK_TRUE(frame.codec_config_generation == 2);
CHECK_TRUE(frame.codec_config.empty());
CHECK_TRUE(frame.source_timestamp == 0xFEDCBA9876543210ULL);
CHECK_TRUE(frame.source_frame_number == 99U);
CHECK_TRUE(frame.fps == 25);
CHECK_TRUE(frame.time_base_den == 25);
constexpr int kConcurrentCallbacks = 8;
std::vector<std::thread> producers;
producers.reserve(kConcurrentCallbacks);
for (int i = 0; i < kConcurrentCallbacks; ++i) {
producers.emplace_back(emitPFrame, g_real_handle);
}
for (auto& producer : producers) {
producer.join();
}
size_t latest_cursor = 0;
cmvr::device::StreamFrameData latest;
CHECK_TRUE(camera.getLatestEncodedFrame(latest, latest_cursor));
CHECK_TRUE(latest.stream_epoch == 1);
CHECK_TRUE(latest.sequence == static_cast<uint64_t>(kConcurrentCallbacks + 1));
CHECK_TRUE(latest_cursor == static_cast<size_t>(kConcurrentCallbacks + 2));
for (int sequence = 2; sequence <= kConcurrentCallbacks + 1; ++sequence) {
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.stream_epoch == 1);
CHECK_TRUE(frame.sequence == static_cast<uint64_t>(sequence));
}
camera.stopStreaming();
emitIFrame();
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
CHECK_TRUE(camera.startStreaming());
CHECK_TRUE(g_key_frame_request_count.load() == 3);
emitPFrame();
emitIFrame(g_real_handle - 1);
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
emitIFrame();
CHECK_TRUE(camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(50)));
CHECK_TRUE(frame.stream_epoch == 2);
CHECK_TRUE(frame.sequence == 0);
CHECK_TRUE(frame.codec_config_generation == 3);
// The fake StopRealPlay invokes the SDK callback synchronously. stop()
// owns ctrl_mtx_ here, proving the callback neither takes that mutex nor
// publishes after the preview handle has been invalidated.
CHECK_TRUE(camera.stop());
CHECK_TRUE(g_stop_callback_count.load() == 1);
CHECK_TRUE(!camera.waitEncodedFrame(
frame, cursor, std::chrono::milliseconds(10)));
return true;
}
} // namespace
extern "C" {
BOOL NET_DVR_Init()
{
return TRUE;
}
BOOL NET_DVR_Cleanup()
{
return TRUE;
}
BOOL NET_DVR_SetConnectTime(DWORD, DWORD)
{
return TRUE;
}
BOOL NET_DVR_SetReconnect(DWORD, BOOL)
{
return TRUE;
}
BOOL NET_DVR_SetLogToFile(DWORD, char*, BOOL)
{
return TRUE;
}
LONG NET_DVR_Login_V40(
LPNET_DVR_USER_LOGIN_INFO,
LPNET_DVR_DEVICEINFO_V40)
{
return 7;
}
BOOL NET_DVR_Logout(LONG)
{
return TRUE;
}
DWORD NET_DVR_GetLastError()
{
return 0;
}
LONG NET_DVR_RealPlay_V40(
LONG,
LPNET_DVR_PREVIEWINFO,
REALDATACALLBACK,
void*)
{
return g_real_handle;
}
BOOL NET_DVR_SetESRealPlayCallBack(
LONG,
EsDataCallback callback,
void* user)
{
std::lock_guard lock(g_fake_sdk_mutex);
g_es_data_callback = callback;
g_es_data_user = user;
return TRUE;
}
BOOL NET_DVR_StopRealPlay(const LONG real_handle)
{
EsDataCallback callback = nullptr;
void* user = nullptr;
{
std::lock_guard lock(g_fake_sdk_mutex);
callback = g_es_data_callback;
user = g_es_data_user;
}
if (callback) {
std::vector<BYTE> idr{
0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x21
};
NET_DVR_PACKET_INFO_EX packet{};
packet.wWidth = 640;
packet.wHeight = 360;
packet.dwPacketType = kVideoIFrame;
packet.dwPacketSize = static_cast<DWORD>(idr.size());
packet.pPacketBuffer = idr.data();
callback(real_handle, &packet, user);
++g_stop_callback_count;
}
{
std::lock_guard lock(g_fake_sdk_mutex);
g_es_data_callback = nullptr;
g_es_data_user = nullptr;
}
return TRUE;
}
BOOL NET_DVR_SaveRealData(LONG, char*)
{
return TRUE;
}
BOOL NET_DVR_StopSaveRealData(LONG)
{
return TRUE;
}
BOOL NET_DVR_MakeKeyFrame(LONG, LONG)
{
++g_key_frame_request_count;
return TRUE;
}
BOOL NET_DVR_MakeKeyFrameSub(LONG, LONG)
{
++g_key_frame_request_count;
return TRUE;
}
BOOL NET_DVR_PTZControlWithSpeed_Other(
LONG,
LONG,
const DWORD command,
const DWORD stop,
const DWORD speed)
{
++g_ptz_call_count;
g_last_ptz_command = command;
g_last_ptz_stop = stop;
g_last_ptz_speed = speed;
return TRUE;
}
} // extern "C"
int main()
{
if (!testCallbackPublicationLifecycle()) {
return 1;
}
std::cout << "hikvision_camera_callback_test: PASS\n";
return 0;
}

View File

@ -30,6 +30,7 @@ namespace cmvr::device
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
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;

View File

@ -50,6 +50,11 @@ bool MechmindCamera::init() {
return true;
}
void MechmindCamera::getState(CameraState& state) {
std::lock_guard lock(dev_mtx_);
state = state_;
}
bool MechmindCamera::start() {
try {
std::lock_guard lock(dev_mtx_);
@ -68,6 +73,7 @@ bool MechmindCamera::start() {
return true;
}
catch (const std::exception& e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (start): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -85,6 +91,7 @@ bool MechmindCamera::stop() {
return true;
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (stop): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -141,6 +148,7 @@ void MechmindCamera::getRGBImage(cv::Mat& color, Rs2Intrinsics& intrinsics) {
}
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (getRGBImage): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -175,6 +183,7 @@ void MechmindCamera::getDepthImage(cv::Mat& depth, Rs2Intrinsics& intrinsics) {
depth = cv::Mat(depthMap.height(), depthMap.width(), CV_32FC1, depthMap.data());
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (getDepthImage): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -258,6 +267,7 @@ void MechmindCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics
textured_plc_to_rgbd_(textured_pcl, color, depth);
}
catch (const exception &e) {
std::lock_guard lock(dev_mtx_);
const string error_msg = "[MechmindCamera] (getRGBDImages): " + string(e.what());
CMVR_LOG(ERROR) << error_msg;
state_.is_error = true;
@ -269,12 +279,14 @@ void MechmindCamera::getRGBDImages(cv::Mat& color, cv::Mat& depth, Rs2Intrinsics
}
void MechmindCamera::startRecording(const std::string& video_path) {
std::lock_guard lock(dev_mtx_);
state_.is_error = true;
state_.error_message = "startRecording is not implemented";
CMVR_LOG(ERROR) << "[MechmindCamera] (startRecording): " << state_.error_message;
}
void MechmindCamera::stopRecording() {
std::lock_guard lock(dev_mtx_);
state_.is_error = true;
state_.error_message = "stopRecording is not implemented";
CMVR_LOG(ERROR) << "[MechmindCamera] (stopRecording): " << state_.error_message;

View File

@ -42,6 +42,7 @@ public:
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
void setFetchRgbdFn(FetchRgbdFn fetch_rgbd_fn);
void setFovyDeg(double fovy_deg);

View File

@ -59,6 +59,12 @@ MujocoCamera::~MujocoCamera()
stop();
}
void MujocoCamera::getState(CameraState& state)
{
std::lock_guard<std::mutex> lock(mtx_);
state = state_;
}
bool MujocoCamera::init()
{
std::lock_guard<std::mutex> lock(mtx_);

View File

@ -24,6 +24,7 @@ namespace cmvr::device{
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
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;

View File

@ -5,6 +5,8 @@
#include "../include/realsense_camera.h"
#include <cstring>
using namespace std;
using namespace cmvr::device;
@ -143,6 +145,12 @@ RealsenseCamera::~RealsenseCamera() {
}
}
void RealsenseCamera::getState(CameraState& state)
{
std::lock_guard lock(ctrl_mtx_);
state = state_;
}
bool RealsenseCamera::init() {
try {
std::lock_guard lock(ctrl_mtx_);
@ -174,8 +182,9 @@ bool RealsenseCamera::init() {
//初始化编码器
// 初始化RGB编码器(示例参数:640x480,30fps,H.264)
if (!CameraStreamEncoder::init(rgbEncoder_, codec_, encode_width_, encode_height_, fps_)) {
CMVR_LOG(ERROR) << "[RealsenseCamera] (start): Failed to init RGB encoder!";
if (stream_mode_ != DEPTH_MODE &&
!CameraStreamEncoder::init(rgbEncoder_, codec_, encode_width_, encode_height_, fps_)) {
CMVR_LOG(ERROR) << "[RealsenseCamera] (init): Failed to init RGB encoder!";
state_.is_initialized = false;
state_.is_error = true;
state_.error_message = "Failed to init RGB encoder!";
@ -247,16 +256,20 @@ bool RealsenseCamera::start() {
intrinsics_ = Kd_;
}
if (align_mode_ == "color") {
align_ = std::make_shared<rs2::align>(RS2_STREAM_COLOR);
}
else if (align_mode_ == "depth") {
align_ = std::make_shared<rs2::align>(RS2_STREAM_DEPTH);
if (stream_mode_ == RGBD_MODE) {
if (align_mode_ == "color") {
align_ = std::make_shared<rs2::align>(RS2_STREAM_COLOR);
}
else if (align_mode_ == "depth") {
align_ = std::make_shared<rs2::align>(RS2_STREAM_DEPTH);
}
}
// 启动后做一次首帧探测,避免“start 成功但长期无帧”假阳性。
rs2::frameset probe = pipe_.wait_for_frames(kProbeTimeoutMs);
if (!probe.get_color_frame()) {
const bool needs_color = stream_mode_ == COLOR_MODE || stream_mode_ == RGBD_MODE;
const bool needs_depth = stream_mode_ == DEPTH_MODE || stream_mode_ == RGBD_MODE;
if (needs_color && !probe.get_color_frame()) {
last_error = "no color frame after start";
CMVR_LOG(WARNING) << "[RealsenseCamera] (start): " << last_error;
pipe_.stop();
@ -264,7 +277,7 @@ bool RealsenseCamera::start() {
std::this_thread::sleep_for(std::chrono::milliseconds(kRetrySleepMs));
continue;
}
if (stream_mode_ == RGBD_MODE && !probe.get_depth_frame()) {
if (needs_depth && !probe.get_depth_frame()) {
last_error = "no depth frame after start";
CMVR_LOG(WARNING) << "[RealsenseCamera] (start): " << last_error;
pipe_.stop();
@ -308,8 +321,20 @@ bool RealsenseCamera::stop() {
CMVR_LOG(WARNING) << "[RealsenseCamera] (stop): stopRecording failed: " << e.what();
}
}
std::shared_ptr<std::thread> stream_thread_to_join;
{
std::lock_guard lock(ctrl_mtx_);
clear_error_();
stream_count_ = 0;
state_.is_streaming = false;
state_.is_recording = false;
stream_thread_to_join = stream_thread_;
stream_thread_.reset();
}
if (stream_thread_to_join && stream_thread_to_join->joinable()) {
stream_thread_to_join->join();
}
std::lock_guard lock(ctrl_mtx_);
clear_error_();
if (!state_.is_opened || !state_.is_initialized) {
state_.is_opened = false;
return true;
@ -518,9 +543,12 @@ void RealsenseCamera::startRecording(const std::string &video_path) {
// 1. 确定编码格式对应的AVCodecID(H.264/H.265)
AVCodecID codec_id;
if (codec_ == "H264" || codec_ == "h264") {
if (codec_ == "H264" || codec_ == "h264" ||
codec_ == "H264_QSV" || codec_ == "h264_qsv") {
codec_id = AV_CODEC_ID_H264;
} else if (codec_ == "H265" || codec_ == "hevc") {
} else if (codec_ == "H265" || codec_ == "h265" || codec_ == "HEVC" || codec_ == "hevc" ||
codec_ == "H265_QSV" || codec_ == "h265_qsv" ||
codec_ == "HEVC_QSV" || codec_ == "hevc_qsv") {
codec_id = AV_CODEC_ID_HEVC;
} else {
state_.is_error = true;
@ -528,7 +556,8 @@ void RealsenseCamera::startRecording(const std::string &video_path) {
CMVR_LOG(ERROR) << "[RealsenseCamera] (startRecording): " << state_.error_message;
return;
}
const char* format_name = (codec_ == "H265" || codec_ == "h265" || codec_ == "HEVC" || codec_ == "hevc") ? "mp4" : "avi";
const bool is_hevc = codec_id == AV_CODEC_ID_HEVC;
const char* format_name = is_hevc ? "mp4" : "avi";
auto ret = avformat_alloc_output_context2(&format_context_, nullptr, format_name, output_path_.c_str());
// 2. 创建输出格式上下文(封装器核心)
if (ret < 0) {
@ -713,25 +742,31 @@ void RealsenseCamera::streaming_worker_() {
bool success = false;
is_streaming_running = true;
uint64_t frame_sequence = 0;
const uint64_t stream_epoch = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
// 处于流传输或者录像状态时就不退出线程
while (state_.is_streaming || state_.is_recording) {
// 记录当前帧处理开始时间
auto frame_start_time = std::chrono::high_resolution_clock::now();
const auto frame_start_time = std::chrono::steady_clock::now();
rs2::frameset frames;
frames = get_frameset(true);
rs2::frame color_frame = frames.get_color_frame();
rs2::frame depth_frame = frames.get_depth_frame();
if (!color_frame) {
rs2::video_frame color_frame = frames.get_color_frame();
rs2::depth_frame depth_frame = frames.get_depth_frame();
const bool needs_color = stream_mode_ == COLOR_MODE || stream_mode_ == RGBD_MODE;
const bool needs_depth = stream_mode_ == DEPTH_MODE || stream_mode_ == RGBD_MODE;
if (needs_color && !color_frame) {
state_.is_error = true;
state_.error_message = "missing color frame";
CMVR_LOG(ERROR) << "[RealsenseCamera]streaming_worker_: " << state_.error_message;
break;
}
if (stream_mode_ == RGBD_MODE && !depth_frame) {
if (needs_depth && !depth_frame) {
state_.is_error = true;
state_.error_message = "missing depth frame in RGBD mode";
state_.error_message = "missing depth frame";
CMVR_LOG(ERROR) << "[RealsenseCamera]streaming_worker_: " << state_.error_message;
break;
}
@ -747,56 +782,78 @@ void RealsenseCamera::streaming_worker_() {
}
// 保存图像数据
{
cv::Mat temp(cv::Size(width_, height_), CV_8UC3, const_cast<void*>(color_frame.get_data()), cv::Mat::AUTO_STEP);
if (color_frame) {
cv::Mat temp(cv::Size(color_frame.get_width(), color_frame.get_height()),
CV_8UC3, const_cast<void*>(color_frame.get_data()), cv::Mat::AUTO_STEP);
temp.copyTo(frame_data.rgbImage); // 执行深拷贝
}
if (depth_frame) {
auto temp = cv::Mat(cv::Size(width_, height_), CV_16UC1, const_cast<void*>(depth_frame.get_data()));
cv::Mat temp(cv::Size(depth_frame.get_width(), depth_frame.get_height()),
CV_16UC1, const_cast<void*>(depth_frame.get_data()), cv::Mat::AUTO_STEP);
temp.copyTo(frame_data.depthImage); // 执行深拷贝
}
// 记录编码开始时间
if (depth_frame) {
frame_data.depth_width = frame_data.depthImage.cols;
frame_data.depth_height = frame_data.depthImage.rows;
frame_data.depthKey = true;
const size_t depth_bytes = frame_data.depthImage.total() * frame_data.depthImage.elemSize();
frame_data.depthFrame.resize(depth_bytes);
std::memcpy(frame_data.depthFrame.data(), frame_data.depthImage.data, depth_bytes);
}
auto encode_start_time = std::chrono::high_resolution_clock::now();
// rgb图像编码
cv::Mat rgb_to_encode = frame_data.rgbImage;
if (encode_width_ > 0 && encode_height_ > 0 &&
(frame_data.rgbImage.cols != encode_width_ || frame_data.rgbImage.rows != encode_height_)) {
cv::resize(frame_data.rgbImage,
rgb_to_encode,
cv::Size(encode_width_, encode_height_),
0.0,
0.0,
cv::INTER_LINEAR);
success = true;
if (needs_color) {
cv::Mat rgb_to_encode = frame_data.rgbImage;
if (encode_width_ > 0 && encode_height_ > 0 &&
(frame_data.rgbImage.cols != encode_width_ || frame_data.rgbImage.rows != encode_height_)) {
cv::resize(frame_data.rgbImage,
rgb_to_encode,
cv::Size(encode_width_, encode_height_),
0.0,
0.0,
cv::INTER_LINEAR);
}
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);
}
CameraStreamEncodeOptions encode_options;
encode_options.draw_timestamp = enable_stream_timestamp_;
encode_options.overlay = streamOverlay();
const Rs2Intrinsics encode_intrinsics = scaleIntrinsics(
frame_data.intrinsics,
frame_data.rgbImage.cols,
frame_data.rgbImage.rows,
rgb_to_encode.cols,
rgb_to_encode.rows);
success = CameraStreamEncoder::encode(rgbEncoder_,
rgb_to_encode,
frame_data.rgbFrame,
frame_data.bKey,
encode_intrinsics,
encode_options);
// 深度图编码
// success = encodeFrameWithEncoder(depthEncoder_, frame_data.depthImage, frame_data.depthFrame, frame_data.depthKey);
if (needs_depth && frame_data.depthFrame.empty())
success = false;
if (success) {
const uint64_t sequence = frame_sequence++;
frame_data.stream_epoch = stream_epoch;
frame_data.sequence = sequence;
const rs2::frame source_frame = color_frame ? color_frame : depth_frame;
frame_data.source_timestamp = source_frame
? static_cast<uint64_t>(source_frame.get_timestamp()) : 0;
frame_data.source_frame_number = source_frame ? source_frame.get_frame_number() : 0;
frame_data.capture_monotonic_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
frame_start_time.time_since_epoch()).count();
frame_data.capture_utc_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
frame_data.pts = static_cast<int64_t>(sequence);
frame_data.dts = frame_data.pts;
frame_data.time_base_num = 1;
frame_data.time_base_den = fps_;
frame_data.duration = 1;
frame_data.fps = fps_;
frame_data.width = encode_width_;
frame_data.height = encode_height_;
frame_data.codec = codec_;
frame_data.width = needs_color ? encode_width_ : frame_data.depth_width;
frame_data.height = needs_color ? encode_height_ : frame_data.depth_height;
frame_data.codec = needs_color ? codec_ : "none";
stream_frame_buffer_->push(frame_data);
}
// 计算从帧开始到现在的总耗时
auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - frame_start_time
std::chrono::steady_clock::now() - frame_start_time
).count();
// 计算需要休眠的时间(确保总耗时达到frame_interval)
@ -963,12 +1020,24 @@ bool RealsenseCamera::startStreaming()
void RealsenseCamera::stopStreaming()
{
std::lock_guard lock(ctrl_mtx_);
stream_count_--;
if (stream_count_ == 0)
std::shared_ptr<std::thread> stream_thread_to_join;
{
std::lock_guard lock(ctrl_mtx_);
if (stream_count_ > 0) {
stream_count_--;
}
if (stream_count_ == 0)
{
// 当前已经没有正在使用的流了,编码采集线程状态修改
state_.is_streaming = false;
state_.is_streaming = false;
if (!state_.is_recording) {
stream_thread_to_join = stream_thread_;
stream_thread_.reset();
}
}
}
if (stream_thread_to_join && stream_thread_to_join->joinable()) {
stream_thread_to_join->join();
}
}

View File

@ -2,7 +2,18 @@ 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 cmvr_es::device::camera_stream_encoder)
target_link_libraries(uvc_camera PUBLIC
glog
opencv_core
opencv_imgproc
avcodec
avdevice
avformat
avutil
swscale
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

@ -5,6 +5,9 @@
#ifndef CMVR_ES_UVC_CAMERA_H
#define CMVR_ES_UVC_CAMERA_H
#include <atomic>
#include <condition_variable>
#include "common/base/ring_buffer.h"
#include "camera/abstract_camera.h"
#include "devices/camera/common/include/camera_stream_encoder.h"
@ -29,6 +32,7 @@ namespace cmvr::device {
bool init() override;
bool start() override;
bool stop() override;
void getState(CameraState& state) override;
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;
@ -44,6 +48,16 @@ namespace cmvr::device {
private:
void streaming_worker_();
void recording_worker_();
void cleanup_recording_resources_();
bool open_capture_();
void close_capture_();
void capture_worker_();
bool wait_for_capture_frame_(AVFrame* frame,
uint64_t& last_sequence,
int64_t& capture_monotonic_ns,
int64_t& capture_utc_ns);
bool convert_capture_frame_to_bgr_(const AVFrame* frame, cv::Mat& bgr_frame);
static int interrupt_capture_(void* opaque);
int fps_;
int width_;
@ -51,7 +65,6 @@ namespace cmvr::device {
int encode_width_;
int encode_height_;
std::string serial_;
cv::VideoCapture cap_;
size_t buffer_size_;
std::string codec_;
bool enable_stream_timestamp_{false};
@ -62,13 +75,24 @@ namespace cmvr::device {
std::string current_video_path_;
std::mutex ctrl_mtx_{};
std::unique_ptr<cv::VideoWriter> video_writer_;
std::shared_ptr<std::thread> stream_thread_;
std::shared_ptr<std::thread> recording_thread_;
std::shared_ptr<std::thread> capture_thread_;
cv::Mat latest_frame_; // 存储最新帧
std::mutex frame_mutex_; // 保护最新帧的访问
AVFormatContext* capture_format_context_ = nullptr;
AVCodecContext* capture_decoder_context_ = nullptr;
AVBufferRef* capture_hw_device_context_ = nullptr;
SwsContext* capture_sws_context_ = nullptr;
int capture_video_stream_index_ = -1;
std::atomic<bool> capture_running_{false};
std::mutex capture_frame_mutex_;
std::mutex capture_conversion_mutex_;
std::condition_variable capture_frame_cv_;
AVFrame* latest_capture_frame_ = nullptr;
uint64_t latest_capture_sequence_ = 0;
int64_t latest_capture_monotonic_ns_ = 0;
int64_t latest_capture_utc_ns_ = 0;
std::string capture_error_;
std::string output_path_;
bool is_recording_ = false;

File diff suppressed because it is too large Load Diff

View File

@ -10,4 +10,4 @@ target_link_libraries(microphone
cmvr_es::proto
)
add_library(cmvr_es::device::microphone ALIAS microphone)
add_library(cmvr_es::device::microphone ALIAS microphone)

View File

@ -6,6 +6,9 @@
#define CMVR_ES_ABSTRACT_MICROPHONE_H
#pragma once
#include <chrono>
#include <cstddef>
#include "devices/abstract_device.h"
#include "cmvr/config/microphone_config/microphone_config.pb.h"
namespace cmvr::device{
@ -22,6 +25,20 @@ namespace cmvr::device{
virtual void resume() {}
virtual void setVolume(const int volume) {}
virtual int getVolume() {return 0;}
virtual bool startStreaming() {return true;}
virtual void stopStreaming() {}
virtual void getEncodedFrame(AudioStreamFrameData& frame_data, size_t& index) {}
virtual bool waitEncodedFrame(
AudioStreamFrameData& frame_data,
size_t& index,
std::chrono::milliseconds timeout) {
(void)timeout;
getEncodedFrame(frame_data, index);
return !frame_data.data.empty();
}
virtual bool getLatestEncodedFrame(AudioStreamFrameData& frame_data, size_t& next_index) {
return false;
}
protected:
MicrophoneState state_{};

View File

@ -3,26 +3,30 @@
#ifndef CMVR_ES_FFMPEG_MICROPHONE_H
#define CMVR_ES_FFMPEG_MICROPHONE_H
#include <thread>
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <queue>
#include <condition_variable>
#include "microphone/abstract_microphone.h"
#include <thread>
#include <boost/lockfree/spsc_queue.hpp>
#include "common/base/ring_buffer.h"
#include "microphone/abstract_microphone.h"
#include "speaker/ffmpeg_speaker/include/ffmpeg_ptr.h"
namespace cmvr::device {
class ffmpegMicroPhone final : public AbstractMicrophone {
public:
// 录制状态
enum class RecordingState {
STOPPED,
RECORDING,
PAUSED
};
public:
ffmpegMicroPhone(const config::FFMpegMicroPhoneConfig& cfg);
};
explicit ffmpegMicroPhone(const config::FFMpegMicroPhoneConfig& cfg);
~ffmpegMicroPhone() override;
std::string typeName() const override { return "FFMpegMicroPhone"; }
bool init() override;
@ -36,49 +40,64 @@ namespace cmvr::device {
void setVolume(const int volume) override;
int getVolume() override;
bool startStreaming() override;
void stopStreaming() override;
void getEncodedFrame(AudioStreamFrameData& frame_data, size_t& index) override;
bool waitEncodedFrame(AudioStreamFrameData& frame_data, size_t& index, std::chrono::milliseconds timeout) override;
bool getLatestEncodedFrame(AudioStreamFrameData& frame_data, size_t& next_index) override;
private:
// 音频采集和编码线程
void audioThread();
// 初始化FFmpeg采集和编码
bool initFFmpeg();
// 关闭FFmpeg资源
void closeFFmpeg();
bool startCapture_(bool write_file);
void stopCapture_();
bool recoverStreamingCapture_(int read_error);
static int interruptCallback_(void* opaque);
AudioStreamFormat currentStreamFormat_() const;
void pushEncodedPacket_(const AVPacket* packet);
private:
// FFmpeg采集相关
AVFormatContext* input_fmt_ctx;
AVCodecContext* input_codec_ctx;
AVFrame* input_frame;
// FFmpeg编码相关
AVFormatContext* output_fmt_ctx;
AVOutputFormat* output_fmt;
const AVOutputFormat* output_fmt;
AVStream* audio_st;
AVCodecContext* audio_codec_ctx;
AVCodec* audio_codec;
const AVCodec* audio_codec;
SwrContext* swr_ctx;
AVFrame* audio_frame;
std::string output_file;
std::string format_name;
std::atomic<bool> is_recording;
std::atomic<bool> is_capturing_{false};
std::atomic<bool> interrupt_requested_{false};
std::atomic<bool> write_output_file_{false};
std::atomic<bool> is_paused;
std::mutex pause_mutex;
std::condition_variable pause_cv;
// 音频参数
int sample_rate_;
int channels_;
int64_t next_pts;
private:
std::string input_device_;
std::shared_ptr<std::thread> audio_thread_;
// Serializes FFmpeg context start/stop and keeps a new shared lease from
// starting until the previous capture thread has exited and been joined.
std::mutex capture_mutex_;
std::mutex mtx_;
std::shared_ptr<SPMCRingBuffer<AudioStreamFrameData>> stream_frame_buffer_;
// Protected by mtx_. The audio thread snapshots these values while it
// assigns metadata to an encoded frame.
int stream_count_ = 0;
size_t buffer_size_ = 256;
uint64_t stream_epoch_ = 0;
uint64_t stream_sequence_ = 0;
uint32_t codec_config_generation_ = 0;
config::FFMpegMicroPhoneConfig config_;
};

View File

@ -21,6 +21,8 @@ namespace cmvr::device{
virtual int getVolume() const {return 0;}
virtual void pause() {}
virtual void resume() {}
virtual bool pushAudioFrame(const AudioStreamFrameData& frame_data) { return false; }
virtual void stopStreaming() {}
protected:
SpeakerState state_{};

View File

@ -114,291 +114,6 @@ enum class ImageType {
DEPTH, // 深度图像
GRAYSCALE // 灰度图像
};
class HEVCEncoder {
public:
HEVCEncoder(int width, int height, int fps, int bitrate,
const std::string& deviceName = "/dev/dri/renderD128",
ImageType imageType = ImageType::COLOR)
: width_(width), height_(height), fps_(fps), bitrate_(bitrate),
deviceName_(deviceName), imageType_(imageType) {
initialized_ = init();
}
bool encodeFrame(const cv::Mat& image) {
if (!initialized_ || !codec_ctx_ || !frame_) {
CMVR_LOG(ERROR) << "HEVC encoder is not initialized";
return false;
}
if (!prepareFrame(image)) {
CMVR_LOG(ERROR) << "Failed to prepare frame";
return false;
}
if (avcodec_send_frame(codec_ctx_.get(), frame_.get()) < 0) {
CMVR_LOG(ERROR) << "Error sending frame to encoder";
return false;
}
return true;
}
std::vector<uint8_t> receivePacket(bool& is_key) {
std::vector<uint8_t> packetData;
if (!initialized_ || !codec_ctx_) {
CMVR_LOG(ERROR) << "HEVC encoder is not initialized";
return packetData;
}
FFmpegPtr<AVPacket> packet(av_packet_alloc());
while (true)
{
int ret = avcodec_receive_packet(codec_ctx_.get(), packet.get());
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
CMVR_LOG(ERROR) << "Error during encoding";
break;
}
is_key = (packet->flags & AV_PKT_FLAG_KEY) != 0;
// 预留足够空间,避免多次内存分配
packetData.reserve(packetData.size() + packet->size);
// 复制数据包内容到输出向量
packetData.insert(packetData.end(),
packet->data,
packet->data + packet->size);
av_packet_unref(packet.get());
}
return packetData;
}
private:
bool init() {
// 1. 创建硬件设备上下文
AVBufferRef* hw_device_ctx = nullptr;
int ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VAAPI,
deviceName_.c_str(), nullptr, 0);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
CMVR_LOG(ERROR) << "Failed to create VAAPI device context: " << errbuf;
return false;
}
hw_device_ctx_.reset(hw_device_ctx);
// 2. 查找编码器
const AVCodec* codec = avcodec_find_encoder_by_name("hevc_vaapi");
if (!codec) {
CMVR_LOG(ERROR) << "HEVC VAAPI encoder not found";
return false;
}
// 3. 设置编码器上下文
codec_ctx_.reset(avcodec_alloc_context3(codec));
codec_ctx_->width = width_;
codec_ctx_->height = height_;
codec_ctx_->time_base = {1, fps_};
codec_ctx_->pix_fmt = AV_PIX_FMT_VAAPI;
codec_ctx_->bit_rate = bitrate_;
codec_ctx_->gop_size = 1;
codec_ctx_->hw_device_ctx = av_buffer_ref(hw_device_ctx_.get());
// 4. 创建硬件帧上下文
hw_frames_ctx_.reset(av_hwframe_ctx_alloc(hw_device_ctx_.get()));
if (!hw_frames_ctx_) {
CMVR_LOG(ERROR) << "Failed to allocate hardware frame context";
return false;
}
AVHWFramesContext* frames_ctx = (AVHWFramesContext*)hw_frames_ctx_->data;
frames_ctx->format = AV_PIX_FMT_VAAPI;
frames_ctx->sw_format = AV_PIX_FMT_NV12;
frames_ctx->width = width_;
frames_ctx->height = height_;
frames_ctx->initial_pool_size = 20;
if (av_hwframe_ctx_init(hw_frames_ctx_.get()) < 0) {
CMVR_LOG(ERROR) << "Failed to initialize hardware frame context";
return false;
}
codec_ctx_->hw_frames_ctx = av_buffer_ref(hw_frames_ctx_.get());
// 5. 打开编码器
if (avcodec_open2(codec_ctx_.get(), codec, nullptr) < 0) {
CMVR_LOG(ERROR) << "Cannot open encoder";
return false;
}
// 6. 初始化硬件帧
frame_.reset(av_frame_alloc());
frame_->format = AV_PIX_FMT_VAAPI;
frame_->width = width_;
frame_->height = height_;
if (av_hwframe_get_buffer(hw_frames_ctx_.get(), frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Could not allocate hardware frame data";
return false;
}
// 7. 初始化软件帧
sw_frame_.reset(av_frame_alloc());
sw_frame_->width = width_;
sw_frame_->height = height_;
sw_frame_->format = (imageType_ == ImageType::DEPTH) ? AV_PIX_FMT_GRAY8 : AV_PIX_FMT_NV12;
if (av_frame_get_buffer(sw_frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Could not allocate software frame buffer";
return false;
}
return true;
}
bool prepareFrame(const cv::Mat& image) {
if (imageType_ == ImageType::DEPTH) {
return prepareDepthFrame(image);
} else {
return prepareColorOrGrayscaleFrame(image);
}
}
bool prepareDepthFrame(const cv::Mat& depthImage) {
cv::Mat processedDepth;
if (depthImage.channels() != 1) {
CMVR_LOG(ERROR) << "Depth image must be single-channel";
return false;
}
if (depthImage.depth() != CV_8U) {
if (depthImage.depth() == CV_16U) {
depthImage.convertTo(processedDepth, CV_8U, 255.0 / 65535.0);
} else {
CMVR_LOG(ERROR) << "Unsupported depth image format";
return false;
}
} else {
processedDepth = depthImage.clone();
}
// 确保图像尺寸匹配
if (processedDepth.cols != width_ || processedDepth.rows != height_) {
CMVR_LOG(ERROR) << "Depth image size mismatch";
return false;
}
// 设置软件帧属性
sw_frame_->pts = frame_counter_++;
sw_frame_->format = AV_PIX_FMT_GRAY8;
av_frame_make_writable(sw_frame_.get());
// 使用av_image_fill_arrays处理行对齐
av_image_fill_arrays(sw_frame_->data, sw_frame_->linesize,
processedDepth.data,
AV_PIX_FMT_GRAY8,
width_, height_, 1);
// 上传到硬件帧
if (av_hwframe_transfer_data(frame_.get(), sw_frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Error transferring depth data to hardware frame";
return false;
}
frame_->pts = sw_frame_->pts;
return true;
}
bool prepareColorOrGrayscaleFrame(const cv::Mat& image) {
if (image.cols != width_ || image.rows != height_) {
CMVR_LOG(ERROR) << "Image size mismatch: " << image.cols << "x" << image.rows
<< " vs " << width_ << "x" << height_;
return false;
}
// 1. 确定输入图像的像素格式(OpenCV的Mat格式)
AVPixelFormat src_pix_fmt;
if (image.channels() == 3) {
src_pix_fmt = AV_PIX_FMT_BGR24; // OpenCV默认是BGR格式(3通道)
} else if (image.channels() == 1) {
src_pix_fmt = AV_PIX_FMT_GRAY8; // 灰度图(1通道)
} else {
CMVR_LOG(ERROR) << "Unsupported channel count: " << image.channels();
return false;
}
// 2. 初始化格式转换上下文(swscale)
SwsContext* sws_ctx = sws_getContext(
width_, height_, src_pix_fmt, // 源宽高和格式
width_, height_, AV_PIX_FMT_NV12, // 目标宽高和格式(NV12)
SWS_BILINEAR, // 缩放算法(可根据需求调整)
nullptr, nullptr, nullptr
);
if (!sws_ctx) {
CMVR_LOG(ERROR) << "Failed to create sws context";
return false;
}
// 3. 配置源数据(OpenCV的Mat)
uint8_t* src_data[4] = {nullptr};
int src_linesize[4] = {0};
if (image.channels() == 3) {
src_data[0] = image.data; // BGR数据起始地址
src_linesize[0] = image.step; // 每行字节数(含OpenCV的对齐填充)
} else {
src_data[0] = image.data; // 灰度数据起始地址
src_linesize[0] = image.step; // 灰度图每行字节数
}
// 4. 配置目标数据(NV12格式的AVFrame)
av_frame_make_writable(sw_frame_.get());
sw_frame_->format = AV_PIX_FMT_NV12;
sw_frame_->width = width_;
sw_frame_->height = height_;
// 5. 执行格式转换(BGR/GRAY -> NV12)
int ret = sws_scale(
sws_ctx,
src_data, // 源图像数据
src_linesize, // 源图像每行字节数
0, // 起始行
height_, // 转换的行数
sw_frame_->data, // 目标图像数据(sw_frame_的data指针)
sw_frame_->linesize // 目标图像每行字节数
);
if (ret <= 0) {
CMVR_LOG(ERROR) << "sws_scale failed, ret=" << ret;
sws_freeContext(sws_ctx);
return false;
}
// 6. 释放转换上下文
sws_freeContext(sws_ctx);
// 7. 设置帧属性并上传到硬件
sw_frame_->pts = frame_counter_++;
if (av_hwframe_transfer_data(frame_.get(), sw_frame_.get(), 0) < 0) {
CMVR_LOG(ERROR) << "Error transferring data to hardware frame";
return false;
}
frame_->pts = sw_frame_->pts;
return true;
}
int width_;
int height_;
int fps_;
int bitrate_;
int frame_counter_ = 0;
std::string deviceName_;
ImageType imageType_;
bool initialized_{false};
FFmpegPtr<AVCodecContext> codec_ctx_;
FFmpegPtr<AVFrame> frame_;
FFmpegPtr<AVFrame> sw_frame_;
FFmpegPtr<AVBufferRef> hw_device_ctx_;
FFmpegPtr<AVBufferRef> hw_frames_ctx_;
};
} // namespace ffmpeg
#endif //FFMPEG_PTR_H

View File

@ -3,9 +3,14 @@
#ifndef CMVR_ES_FFMPEG_SPEAKER_H
#define CMVR_ES_FFMPEG_SPEAKER_H
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "speaker/abstract_speaker.h"
#include <boost/lockfree/spsc_queue.hpp>
#include <pulse/simple.h>
@ -33,6 +38,8 @@ namespace cmvr::device {
void pause() override;
void resume() override;
void getState(SpeakerState& state) override;
bool pushAudioFrame(const AudioStreamFrameData& frame_data) override;
void stopStreaming() override;
void resetPlayState();
bool initPulseDevice_();
@ -40,6 +47,12 @@ namespace cmvr::device {
private:
void decode_audio_();
void play_audio_();
bool startStreamingPlayback_(const AudioStreamFrameData& frame_data);
bool pushPcmFrame_(const AudioStreamFrameData& frame_data);
bool decodeStreamFrame_(const AudioStreamFrameData& frame_data);
bool initStreamDecoder_(const AudioStreamFrameData& frame_data);
void releaseStreamDecoder_();
void releaseStreamDecoderUnlocked_();
pa_simple* pulse_simple_ = nullptr; // 修改:PulseAudio 简单 API 句柄
pa_sample_spec sample_spec_{}; // 新增:PulseAudio 采样规格
@ -51,6 +64,7 @@ namespace cmvr::device {
std::shared_ptr<std::thread> decode_thread_;
std::shared_ptr<std::thread> play_thread_;
std::mutex mtx_;
std::mutex stop_mtx_;
std::string audio_path_;
// Boost 单生产者单消费者无锁队列
@ -60,10 +74,20 @@ namespace cmvr::device {
mutable std::mutex mtx_pause_;
std::condition_variable cv_pause_;
std::atomic<bool> is_paused_{false};
std::atomic<bool> is_stopping_{false};
// 音频时钟同步
std::atomic<int64_t> audio_pts_{0};
std::chrono::time_point<std::chrono::steady_clock> playback_start_time_;
std::mutex stream_decode_mtx_;
AVCodecContext* stream_decoder_ctx_ = nullptr;
SwrContext* stream_swr_ctx_ = nullptr;
AVFrame* stream_decode_frame_ = nullptr;
AVPacket* stream_decode_packet_ = nullptr;
std::string stream_codec_;
int stream_input_sample_rate_ = 0;
int stream_input_channels_ = 0;
bool is_streaming_input_ = false;
config::FFMpegSpeakerConfig config_;
};

View File

@ -4,6 +4,9 @@
//
#include <filesystem>
#include <algorithm>
#include <climits>
#include <cstring>
#include "../include/ffmpeg_speaker.h"
@ -18,7 +21,7 @@ ffmpegSpeaker::ffmpegSpeaker(const config::FFMpegSpeakerConfig& cfg):config_(cfg
try {
id_ = config_.id();
memset(&sample_spec_, 0, sizeof(sample_spec_));
// state_.volume = config_.volume();
state_.volume = 100;
}
catch (const exception& e) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] ([ffmpegSpeaker]): Failed to parse config: " << e.what();
@ -28,6 +31,7 @@ ffmpegSpeaker::ffmpegSpeaker(const config::FFMpegSpeakerConfig& cfg):config_(cfg
ffmpegSpeaker::~ffmpegSpeaker() {
is_stopping_ = true;
{
std::lock_guard<std::mutex> lock(mtx_);
state_.is_running = false;
@ -76,6 +80,8 @@ bool ffmpegSpeaker::start() {
void ffmpegSpeaker::resetPlayState()
{
releaseStreamDecoder_();
if (pulse_simple_) {
pa_simple_flush(pulse_simple_, nullptr);
pa_simple_free(pulse_simple_);
@ -89,19 +95,24 @@ void ffmpegSpeaker::resetPlayState()
}
state_.is_initialized = false;
is_streaming_input_ = false;
audio_path_.clear();
CMVR_LOG(INFO) << "[ffmpegSpeaker] (resetPlayState): Success, id=" << id_;
}
bool ffmpegSpeaker::stop() {
std::lock_guard<std::mutex> stop_lock(stop_mtx_);
is_stopping_ = true;
{
lock_guard lock(mtx_);
state_.is_running = false;
state_.is_decoding = false;
state_.is_paused = false;
is_streaming_input_ = false;
}
cv_pause_.notify_all();
// 等待线程结束
if (decode_thread_ && decode_thread_->joinable()) {
decode_thread_->join();
@ -114,6 +125,7 @@ bool ffmpegSpeaker::stop() {
}
resetPlayState();
is_stopping_ = false;
return true;
}
@ -150,6 +162,7 @@ void ffmpegSpeaker::resume() {
return;
}
state_.is_paused = false;
cv_pause_.notify_all();
CMVR_LOG(INFO) << "[ffmpegSpeaker] (resume): Success, id=" << id_;
}
catch (const exception& e) {
@ -291,7 +304,7 @@ void ffmpegSpeaker::decode_audio_() {
return;
}
AVCodec* decoder = nullptr;
const AVCodec* decoder = nullptr;
const int stream_index = av_find_best_stream(fmt_ctx.get(), AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0);
if (stream_index < 0) {
{
@ -531,6 +544,310 @@ void ffmpegSpeaker::play_audio_() {
CMVR_LOG(INFO) << "[ffmpegSpeaker] Playback finished";
}
bool ffmpegSpeaker::pushAudioFrame(const AudioStreamFrameData& frame_data)
{
if (is_stopping_) {
return false;
}
if (frame_data.data.empty()) {
return true;
}
bool need_start = false;
bool need_restart = false;
{
std::lock_guard<std::mutex> lock(mtx_);
need_start = !state_.is_running;
need_restart = state_.is_running && is_streaming_input_ &&
(frame_data.sample_rate != sample_rate_ || frame_data.channels != channels_);
if (state_.is_running && !is_streaming_input_) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] cannot stream audio while file playback is active";
return false;
}
}
if (need_restart) {
stopStreaming();
need_start = true;
}
if (need_start && !startStreamingPlayback_(frame_data)) {
return false;
}
if (frame_data.format == AudioStreamFormat::PCM ||
frame_data.codec == "pcm_s16le" ||
frame_data.codec == "pcm") {
return pushPcmFrame_(frame_data);
}
return decodeStreamFrame_(frame_data);
}
void ffmpegSpeaker::stopStreaming()
{
std::lock_guard<std::mutex> stop_lock(stop_mtx_);
is_stopping_ = true;
{
std::lock_guard<std::mutex> lock(mtx_);
if (!is_streaming_input_) {
is_stopping_ = false;
return;
}
state_.is_decoding = false;
state_.is_paused = false;
is_streaming_input_ = false;
}
cv_pause_.notify_all();
if (play_thread_ && play_thread_->joinable()) {
play_thread_->join();
play_thread_.reset();
}
resetPlayState();
is_stopping_ = false;
}
bool ffmpegSpeaker::startStreamingPlayback_(const AudioStreamFrameData& frame_data)
{
std::lock_guard<std::mutex> lock(mtx_);
sample_rate_ = frame_data.sample_rate > 0 ? frame_data.sample_rate : 44100;
channels_ = frame_data.channels > 0 ? frame_data.channels : 2;
AudioFrame queued_frame;
while (audio_queue_.pop(queued_frame)) {
}
if (!initPulseDevice_()) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] PulseAudio device initialization failed";
return false;
}
state_.is_running = true;
state_.is_decoding = true;
state_.is_paused = false;
is_streaming_input_ = true;
play_thread_ = std::make_shared<std::thread>([this]() {
try {
this->play_audio_();
} catch (const std::exception& e) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] Stream play thread exception: " << e.what();
std::lock_guard<std::mutex> lock(mtx_);
state_.is_running = false;
state_.is_decoding = false;
}
});
return true;
}
bool ffmpegSpeaker::pushPcmFrame_(const AudioStreamFrameData& frame_data)
{
const size_t sample_count = frame_data.data.size() / sizeof(int16_t);
if (sample_count == 0) {
return true;
}
auto buffer = std::make_shared<std::vector<int16_t>>(sample_count);
std::memcpy(buffer->data(), frame_data.data.data(), sample_count * sizeof(int16_t));
float volume_scale = 1.0f;
{
std::lock_guard<std::mutex> lock(mtx_);
volume_scale = static_cast<float>(state_.volume) / 100.0f;
}
for (int16_t& sample : *buffer) {
const float scaled = static_cast<float>(sample) * volume_scale;
sample = static_cast<int16_t>(std::clamp(scaled, -32768.f, 32767.f));
}
while (true) {
{
std::lock_guard<std::mutex> lock(mtx_);
if (!state_.is_running || is_stopping_) {
return false;
}
}
if (audio_queue_.push(buffer)) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return true;
}
bool ffmpegSpeaker::decodeStreamFrame_(const AudioStreamFrameData& frame_data)
{
std::lock_guard<std::mutex> decode_lock(stream_decode_mtx_);
if (!stream_decoder_ctx_ ||
stream_codec_ != frame_data.codec ||
stream_input_sample_rate_ != frame_data.sample_rate ||
stream_input_channels_ != frame_data.channels) {
releaseStreamDecoderUnlocked_();
if (!initStreamDecoder_(frame_data)) {
return false;
}
}
av_packet_unref(stream_decode_packet_);
if (av_new_packet(stream_decode_packet_, static_cast<int>(frame_data.data.size())) < 0) {
return false;
}
std::memcpy(stream_decode_packet_->data, frame_data.data.data(), frame_data.data.size());
stream_decode_packet_->pts = frame_data.pts;
int ret = avcodec_send_packet(stream_decoder_ctx_, stream_decode_packet_);
av_packet_unref(stream_decode_packet_);
if (ret < 0) {
return false;
}
while ((ret = avcodec_receive_frame(stream_decoder_ctx_, stream_decode_frame_)) == 0) {
if (!stream_swr_ctx_) {
stream_swr_ctx_ = swr_alloc_set_opts(nullptr,
av_get_default_channel_layout(channels_),
AV_SAMPLE_FMT_S16,
sample_rate_,
av_get_default_channel_layout(stream_decode_frame_->channels),
static_cast<AVSampleFormat>(stream_decode_frame_->format),
stream_decode_frame_->sample_rate,
0,
nullptr);
if (!stream_swr_ctx_ || swr_init(stream_swr_ctx_) < 0) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] Failed to init stream resampler";
av_frame_unref(stream_decode_frame_);
return false;
}
}
const int64_t max_samples64 = av_rescale_rnd(
swr_get_delay(stream_swr_ctx_, stream_decode_frame_->sample_rate) + stream_decode_frame_->nb_samples,
sample_rate_,
stream_decode_frame_->sample_rate,
AV_ROUND_UP);
const int max_samples = static_cast<int>(std::min<int64_t>(max_samples64, INT_MAX));
auto buffer = std::make_shared<std::vector<int16_t>>(max_samples * channels_);
uint8_t* out[] = {reinterpret_cast<uint8_t*>(buffer->data()), nullptr};
const int out_samples = swr_convert(stream_swr_ctx_,
out,
max_samples,
const_cast<const uint8_t**>(stream_decode_frame_->data),
stream_decode_frame_->nb_samples);
av_frame_unref(stream_decode_frame_);
if (out_samples <= 0) {
continue;
}
buffer->resize(out_samples * channels_);
float volume_scale = 1.0f;
{
std::lock_guard<std::mutex> lock(mtx_);
volume_scale = static_cast<float>(state_.volume) / 100.0f;
}
for (int16_t& sample : *buffer) {
const float scaled = static_cast<float>(sample) * volume_scale;
sample = static_cast<int16_t>(std::clamp(scaled, -32768.f, 32767.f));
}
while (true) {
{
std::lock_guard<std::mutex> lock(mtx_);
if (!state_.is_running || is_stopping_) {
return false;
}
}
if (audio_queue_.push(buffer)) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
return ret == AVERROR(EAGAIN) || ret == AVERROR_EOF;
}
bool ffmpegSpeaker::initStreamDecoder_(const AudioStreamFrameData& frame_data)
{
const AVCodec* decoder = nullptr;
if (!frame_data.codec.empty()) {
decoder = avcodec_find_decoder_by_name(frame_data.codec.c_str());
}
if (!decoder) {
AVCodecID codec_id = AV_CODEC_ID_NONE;
if (frame_data.format == AudioStreamFormat::AAC) {
codec_id = AV_CODEC_ID_AAC;
} else if (frame_data.format == AudioStreamFormat::MP3) {
codec_id = AV_CODEC_ID_MP3;
}
if (codec_id != AV_CODEC_ID_NONE) {
decoder = avcodec_find_decoder(codec_id);
}
}
if (!decoder) {
CMVR_LOG(ERROR) << "[ffmpegSpeaker] Unsupported stream audio codec: " << frame_data.codec;
return false;
}
stream_decoder_ctx_ = avcodec_alloc_context3(decoder);
if (!stream_decoder_ctx_) {
return false;
}
stream_decoder_ctx_->sample_rate = frame_data.sample_rate > 0 ? frame_data.sample_rate : sample_rate_;
stream_decoder_ctx_->channels = frame_data.channels > 0 ? frame_data.channels : channels_;
stream_decoder_ctx_->channel_layout =
stream_decoder_ctx_->channels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
if (avcodec_open2(stream_decoder_ctx_, decoder, nullptr) < 0) {
releaseStreamDecoderUnlocked_();
return false;
}
stream_decode_frame_ = av_frame_alloc();
stream_decode_packet_ = av_packet_alloc();
if (!stream_decode_frame_ || !stream_decode_packet_) {
releaseStreamDecoderUnlocked_();
return false;
}
stream_codec_ = frame_data.codec;
stream_input_sample_rate_ = frame_data.sample_rate;
stream_input_channels_ = frame_data.channels;
return true;
}
void ffmpegSpeaker::releaseStreamDecoder_()
{
std::lock_guard<std::mutex> decode_lock(stream_decode_mtx_);
releaseStreamDecoderUnlocked_();
}
void ffmpegSpeaker::releaseStreamDecoderUnlocked_()
{
if (stream_swr_ctx_) {
swr_free(&stream_swr_ctx_);
}
if (stream_decode_frame_) {
av_frame_free(&stream_decode_frame_);
}
if (stream_decode_packet_) {
av_packet_free(&stream_decode_packet_);
}
if (stream_decoder_ctx_) {
avcodec_free_context(&stream_decoder_ctx_);
}
stream_codec_.clear();
stream_input_sample_rate_ = 0;
stream_input_channels_ = 0;
}
bool ffmpegSpeaker::initPulseDevice_() {
// 验证参数
if (sample_rate_ <= 0 || channels_ <= 0) {

View File

@ -6,12 +6,12 @@
#define CMVR_ES_STATE_DEFINE_H
#include <atomic>
#include <cstdint>
#include <cmath>
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <set>
#include <vector>
#include "common/types/agv/agv_types.h"
#include "common/types/geometry_types.h"
@ -36,6 +36,36 @@ namespace cmvr::device{
UNKNOWN
};
enum class AudioStreamFormat {
PCM = 0,
MP3 = 1,
AAC = 2,
WAV = 3,
OPUS = 4,
UNKNOWN = 99
};
struct AudioStreamFrameData {
std::vector<uint8_t> data;
int sample_rate = 44100;
int channels = 2;
AudioStreamFormat format = AudioStreamFormat::PCM;
std::string codec = "pcm_s16le";
int64_t pts = 0;
int64_t dts = 0;
int nb_samples = 0;
uint64_t stream_epoch = 0;
uint64_t sequence = 0;
int64_t capture_monotonic_ns = 0;
int64_t capture_utc_ns = 0;
int32_t time_base_num = 1;
int32_t time_base_den = 44100;
int64_t duration = 0;
bool discontinuity = false;
uint32_t codec_config_generation = 0;
std::vector<uint8_t> codec_config;
};
// ------------------------------------- robot -------------------------------------
typedef enum {
FORWARD, BACKWARD,

View File

@ -1,9 +1,15 @@
#include <csignal>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <limits.h>
#include <unistd.h>
#include <pthread.h>
#include "common/base/logging/logger.h"
#include "runtime/include/cmvr_runtime.h"
namespace {
bool blockShutdownSignals(sigset_t& shutdown_signals)
@ -16,8 +22,57 @@ bool blockShutdownSignals(sigset_t& shutdown_signals)
} // namespace
namespace fs = std::filesystem;
static bool setupIntelMediaEnvironment()
{
char exe_path_buffer[PATH_MAX] = {};
const ssize_t length = readlink(
"/proc/self/exe",
exe_path_buffer,
sizeof(exe_path_buffer) - 1);
if (length <= 0) {
std::cerr << "Failed to resolve /proc/self/exe\n";
return false;
}
exe_path_buffer[length] = '\0';
const fs::path executable_path(exe_path_buffer);
const fs::path bin_directory = executable_path.parent_path();
const fs::path install_directory = bin_directory.parent_path();
const fs::path library_directory = install_directory / "lib";
const fs::path va_driver_directory = library_directory / "dri";
const fs::path ihd_driver = va_driver_directory / "iHD_drv_video.so";
if (!fs::exists(ihd_driver)) {
std::cerr << "Intel media driver not found: "
<< ihd_driver << '\n';
return false;
}
// overwrite=1:强制 cmvr-es 使用随项目发布的媒体运行时。
setenv("LIBVA_DRIVER_NAME", "iHD", 1);
setenv(
"LIBVA_DRIVERS_PATH",
va_driver_directory.c_str(),
1);
setenv(
"ONEVPL_SEARCH_PATH",
library_directory.c_str(),
1);
return true;
}
int main()
{
if (!setupIntelMediaEnvironment()) {
return EXIT_FAILURE;
}
sigset_t shutdown_signals;
if (!blockShutdownSignals(shutdown_signals)) {
return 1;

View File

@ -0,0 +1,61 @@
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
cmake_minimum_required(VERSION 3.22)
project(cmvr_media_source_hub LANGUAGES CXX)
enable_testing()
endif()
add_library(media_source_hub STATIC
src/media_source_hub.cpp
)
target_compile_features(media_source_hub PUBLIC cxx_std_17)
target_include_directories(media_source_hub
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../..
)
add_library(cmvr_es::media_source_hub ALIAS media_source_hub)
if(NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_library(device_media_source_adapter STATIC
src/device_media_source_adapter.cpp
)
target_compile_features(device_media_source_adapter PUBLIC cxx_std_17)
target_include_directories(device_media_source_adapter
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../..
)
target_link_libraries(device_media_source_adapter
PUBLIC
cmvr_es::media_source_hub
cmvr_es::common
cmvr_es::proto
cmvr_es::logging
)
add_library(cmvr_es::device_media_source_adapter ALIAS device_media_source_adapter)
endif()
option(CMVR_MEDIA_SOURCE_HUB_BUILD_TESTS
"Build the standalone MediaSourceHub self-test"
${PROJECT_IS_TOP_LEVEL})
if(CMVR_MEDIA_SOURCE_HUB_BUILD_TESTS)
find_package(Threads REQUIRED)
add_executable(media_source_hub_test
tests/media_source_hub_test.cpp
)
target_compile_features(media_source_hub_test PRIVATE cxx_std_17)
target_link_libraries(media_source_hub_test
PRIVATE
cmvr_es::media_source_hub
Threads::Threads
)
# This self-test only links the static Hub and pthreads. In the root build,
# the project-wide third-party RUNPATH can otherwise make the loader pick up
# a vendor libstdc++.so (for example from the AUBO SDK), even though the test
# has no dependency on that SDK.
set_target_properties(media_source_hub_test PROPERTIES
SKIP_BUILD_RPATH TRUE
)
add_test(NAME media_source_hub_test COMMAND media_source_hub_test)
endif()

View File

@ -0,0 +1,38 @@
#ifndef CMVR_ES_DEVICE_MEDIA_SOURCE_ADAPTER_H
#define CMVR_ES_DEVICE_MEDIA_SOURCE_ADAPTER_H
#pragma once
#include <cstddef>
#include <memory>
#include <string>
#include "devices/camera/abstract_camera.h"
#include "devices/microphone/abstract_microphone.h"
#include "manager/media_source_hub/include/media_source_hub.h"
namespace cmvr::media {
// Process-wide protocol-neutral media hub shared by gRPC and QUIC services.
MediaSourceHub& globalMediaSourceHub();
std::string cameraColorTrackId(const std::string& device_id);
std::string microphoneTrackId(const std::string& device_id);
// Registration is idempotent for an already registered track. The adapter owns a
// short-lived pump thread and one startStreaming()/stopStreaming() lease only while
// at least one Hub subscription is active. It ensures start() succeeds but deliberately
// does not call stop(), because the base device lifecycle can also be owned by control RPCs.
bool ensureCameraMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractCamera>& camera,
size_t ring_capacity = 64);
bool ensureMicrophoneMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractMicrophone>& microphone,
size_t ring_capacity = 256);
} // namespace cmvr::media
#endif // CMVR_ES_DEVICE_MEDIA_SOURCE_ADAPTER_H

View File

@ -0,0 +1,132 @@
#ifndef CMVR_ES_MANAGER_MEDIA_SOURCE_HUB_H
#define CMVR_ES_MANAGER_MEDIA_SOURCE_HUB_H
#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "common/base/ring_buffer.h"
#include "common/media/media_frame.h"
namespace cmvr::media {
// MediaSourceHub owns no protocol-specific state. A device or capture adapter registers
// start/stop callbacks and receives a sink callback when the first consumer subscribes.
class MediaSourceHub final {
public:
using FrameRing = BroadcastFrameRing<MediaFrame>;
using FrameReadResult = FrameRing::ReadResult;
using StartPosition = FrameRing::StartPosition;
using FrameSink = std::function<void(MediaFramePtr)>;
// Cancellation checks run while MediaSourceHub protects source lifecycle
// state. Predicates must therefore be fast, non-blocking and must not call
// back into the same hub.
using CancelPredicate = std::function<bool()>;
struct SourceCallbacks {
// start() may run asynchronously. It must observe cancelled during any
// potentially blocking startup work and return false promptly once set.
// MediaSourceHub retains the callback state until a non-cooperative start
// eventually returns, so late completion cannot access destroyed state.
std::function<bool(
const FrameSink& sink,
const CancelPredicate& cancelled)> start;
// stop() is the synchronous publication barrier for the last lease and
// must unblock and join the source producer before returning.
std::function<void()> stop;
std::function<bool()> request_key_frame;
};
private:
struct SourceState;
public:
class Subscription final {
public:
Subscription() = default;
~Subscription();
Subscription(const Subscription&) = delete;
Subscription& operator=(const Subscription&) = delete;
Subscription(Subscription&& other) noexcept;
Subscription& operator=(Subscription&& other) noexcept;
// A Subscription owns one reader cursor and is single-consumer. Moving,
// resetting, or reading the same object concurrently is unsupported; use
// one independent subscription per consumer thread.
bool valid() const;
explicit operator bool() const { return valid(); }
// Returns the most recently observed immutable descriptor. A callback source may
// replace the initially registered UNKNOWN codec/config descriptor with the first
// real frame descriptor without invalidating existing subscriptions.
TrackDescriptorPtr descriptor() const;
std::optional<FrameReadResult> tryRead();
std::optional<FrameReadResult> waitRead(std::chrono::milliseconds timeout);
uint64_t discardPendingIfExceeds(size_t maximum_pending_frames);
uint64_t droppedCount() const noexcept;
void reset();
private:
friend class MediaSourceHub;
Subscription(std::shared_ptr<SourceState> source, FrameRing::Cursor cursor);
std::shared_ptr<SourceState> source_;
FrameRing::Cursor cursor_;
bool active_{false};
};
MediaSourceHub();
~MediaSourceHub();
MediaSourceHub(const MediaSourceHub&) = delete;
MediaSourceHub& operator=(const MediaSourceHub&) = delete;
bool registerSource(
TrackDescriptorPtr initial_descriptor,
SourceCallbacks callbacks,
size_t ring_capacity = 64);
// Active sources cannot be unregistered. Destroy/reset their subscriptions first.
bool unregisterSource(const std::string& track_id);
bool hasSource(const std::string& track_id) const;
std::vector<TrackDescriptorPtr> listTracks() const;
size_t subscriberCount(const std::string& track_id) const;
// Protocol adapters can request an IDR after a discontinuity without knowing the
// concrete camera implementation. Returns false when unsupported or not running.
bool requestKeyFrame(const std::string& track_id) const;
Subscription subscribe(
const std::string& track_id,
StartPosition start_position = StartPosition::NEXT_PUBLISHED,
CancelPredicate cancelled = {});
// Stops all registered sources and invalidates outstanding subscriptions. The
// subscriptions remain destructible and their waitRead calls are awakened.
// A cooperative in-progress start is cancelled; a callback that violates the
// cancellation contract is quarantined with retained state rather than blocking
// shutdown or risking a use-after-free.
void shutdown();
private:
struct Impl;
std::shared_ptr<Impl> impl_;
};
// Compatibility alias for older protocol tests and integrations. New code should
// use MediaSourceHub directly.
using MediaSourceManager = MediaSourceHub;
} // namespace cmvr::media
#endif // CMVR_ES_MANAGER_MEDIA_SOURCE_HUB_H

View File

@ -0,0 +1,687 @@
#include "manager/media_source_hub/include/device_media_source_adapter.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <exception>
#include <iterator>
#include <limits>
#include <mutex>
#include <thread>
#include <utility>
#include <vector>
#include "common/base/logging/logger.h"
namespace cmvr::media {
namespace {
std::string normalizedCodec(std::string codec) {
codec.erase(
std::remove_if(codec.begin(), codec.end(), [](const unsigned char c) {
return !std::isalnum(c);
}),
codec.end());
std::transform(codec.begin(), codec.end(), codec.begin(), [](const unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return codec;
}
Codec videoCodec(const std::string& value) {
const std::string codec = normalizedCodec(value);
if (codec == "h264" || codec == "avc" || codec == "avc1" ||
codec == "libx264" || codec == "h264qsv") {
return Codec::H264;
}
if (codec == "h265" || codec == "hevc" || codec == "hvc1" ||
codec == "libx265" || codec == "h265qsv" || codec == "hevcqsv") {
return Codec::H265;
}
return Codec::UNKNOWN;
}
PayloadFormat videoPayloadFormat(
const Codec codec,
const std::vector<uint8_t>& payload) noexcept {
if (codec != Codec::H264 && codec != Codec::H265) {
return PayloadFormat::UNKNOWN;
}
const bool three_byte_start_code = payload.size() >= 3 &&
payload[0] == 0U && payload[1] == 0U && payload[2] == 1U;
const bool four_byte_start_code = payload.size() >= 4 &&
payload[0] == 0U && payload[1] == 0U && payload[2] == 0U && payload[3] == 1U;
return three_byte_start_code || four_byte_start_code
? PayloadFormat::ANNEX_B
: PayloadFormat::UNKNOWN;
}
Codec audioCodec(const std::string& value) {
const std::string codec = normalizedCodec(value);
if (codec == "opus") return Codec::OPUS;
if (codec == "aac") return Codec::AAC;
if (codec == "pcms16le") return Codec::PCM_S16LE;
return Codec::UNKNOWN;
}
Codec audioCodec(const device::AudioStreamFrameData& source) {
const Codec codec = audioCodec(source.codec);
if (codec != Codec::UNKNOWN || !normalizedCodec(source.codec).empty()) {
return codec;
}
switch (source.format) {
case device::AudioStreamFormat::PCM:
return Codec::PCM_S16LE;
case device::AudioStreamFormat::AAC:
return Codec::AAC;
case device::AudioStreamFormat::OPUS:
return Codec::OPUS;
default:
return Codec::UNKNOWN;
}
}
PayloadFormat audioPayloadFormat(
const Codec codec,
const std::vector<uint8_t>& payload) noexcept {
switch (codec) {
case Codec::OPUS:
return PayloadFormat::OPUS_PACKET;
case Codec::PCM_S16LE:
return PayloadFormat::RAW;
case Codec::AAC: {
// FFmpeg encoders commonly expose raw AAC access units plus AudioSpecificConfig;
// only advertise ADTS when the sync word and layer bits are actually present.
const bool has_adts_header = payload.size() >= 2 && payload[0] == 0xFFU &&
(payload[1] & 0xF6U) == 0xF0U;
return has_adts_header ? PayloadFormat::AAC_ADTS : PayloadFormat::RAW;
}
default:
return PayloadFormat::UNKNOWN;
}
}
Rational sanitizedTimeBase(
const int32_t numerator,
const int32_t denominator,
const int32_t fallback_denominator) noexcept {
return Rational{
numerator > 0 ? numerator : 1,
denominator > 0 ? denominator : std::max(1, fallback_denominator)};
}
uint64_t descriptorGeneration(
const uint64_t stream_epoch,
const uint32_t codec_generation) {
const uint64_t generation = (stream_epoch << 32U) | codec_generation;
return generation == 0 ? 1 : generation;
}
template<typename DeviceT>
struct PumpState : public std::enable_shared_from_this<PumpState<DeviceT>> {
explicit PumpState(std::shared_ptr<DeviceT> device_ptr)
: device(std::move(device_ptr)) {}
virtual ~PumpState() {
stop();
}
bool begin(
const MediaSourceHub::FrameSink& frame_sink,
const MediaSourceHub::CancelPredicate& cancelled) {
if (!frame_sink || !device) {
return false;
}
const auto cancellation_requested = [&cancelled] {
if (!cancelled) return false;
try {
return cancelled();
} catch (...) {
return true;
}
};
if (cancellation_requested()) {
return false;
}
std::unique_lock<std::mutex> lock(mutex);
if (running.load(std::memory_order_acquire)) {
return true;
}
if (worker.joinable()) {
// A previous worker must always be collected before a new capture lease starts.
std::thread stale_worker = std::move(worker);
lock.unlock();
collectThread(std::move(stale_worker));
lock.lock();
}
sink = frame_sink;
bool streaming_attempted = false;
try {
if (!device->start()) {
sink = {};
return false;
}
if (cancellation_requested()) {
sink = {};
return false;
}
streaming_attempted = true;
if (!device->startStreaming()) {
sink = {};
lock.unlock();
stopDeviceStreaming();
return false;
}
streaming_started = true;
if (cancellation_requested()) {
streaming_started = false;
sink = {};
lock.unlock();
stopDeviceStreaming();
return false;
}
running.store(true, std::memory_order_release);
try {
// The worker owns the pump while run() is active. This also
// makes the defensive self-stop/detach path lifetime-safe.
const auto self = this->shared_from_this();
worker = std::thread([self] { self->run(); });
} catch (...) {
running.store(false, std::memory_order_release);
streaming_started = false;
sink = {};
lock.unlock();
stopDeviceStreaming();
return false;
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to start media source: "
<< error.what();
sink = {};
lock.unlock();
if (streaming_attempted) stopDeviceStreaming();
return false;
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to start media source";
sink = {};
lock.unlock();
if (streaming_attempted) stopDeviceStreaming();
return false;
}
return true;
}
void stop() noexcept {
std::thread thread;
bool stop_streaming = false;
{
std::lock_guard<std::mutex> lock(mutex);
running.store(false, std::memory_order_release);
stop_streaming = streaming_started;
streaming_started = false;
sink = {};
thread = std::move(worker);
}
if (stop_streaming) {
stopDeviceStreaming();
}
if (thread.joinable()) {
collectThread(std::move(thread));
}
}
static void collectThread(std::thread thread) noexcept {
if (!thread.joinable()) {
return;
}
try {
if (thread.get_id() == std::this_thread::get_id()) {
thread.detach();
} else {
thread.join();
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to collect media pump: "
<< error.what();
if (thread.joinable()) {
try {
thread.detach();
} catch (...) {
// std::thread's destructor would terminate if this extremely rare
// platform error occurred; there is no recoverable ownership path.
}
}
}
}
virtual void run() = 0;
void stopDeviceStreaming() noexcept {
try {
if (device) {
device->stopStreaming();
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to stop media source: "
<< error.what();
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to stop media source";
}
}
std::shared_ptr<DeviceT> device;
std::atomic<bool> running{false};
std::mutex mutex;
std::thread worker;
MediaSourceHub::FrameSink sink;
bool streaming_started{false};
};
struct CameraPump final : PumpState<device::AbstractCamera> {
CameraPump(std::shared_ptr<device::AbstractCamera> camera, std::string id)
: PumpState(std::move(camera)), track_id(std::move(id)) {}
~CameraPump() override { stop(); }
void run() override {
size_t cursor = 0;
uint64_t last_epoch = 0;
uint64_t last_sequence = 0;
uint64_t cached_config_generation = 0;
std::vector<uint8_t> cached_codec_config;
TrackDescriptorPtr last_descriptor;
bool have_previous = false;
bool have_cached_config_generation = false;
bool waiting_for_key_frame = true;
bool pending_discontinuity = true;
bool requested_key_frame = false;
std::chrono::steady_clock::time_point last_key_frame_request;
const auto request_key_frame = [&] {
const auto now = std::chrono::steady_clock::now();
if (requested_key_frame &&
now - last_key_frame_request < std::chrono::milliseconds(250)) {
return;
}
requested_key_frame = true;
last_key_frame_request = now;
try {
device->requestKeyFrame();
} catch (...) {
// Unsupported/failed key-frame requests fall back to the encoder's GOP.
}
};
request_key_frame();
while (running.load(std::memory_order_acquire)) {
device::StreamFrameData source;
try {
if (!device->waitEncodedFrame(source, cursor, std::chrono::milliseconds(50))) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Camera frame read failed for "
<< track_id << ": " << error.what();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Camera frame read failed for "
<< track_id;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (source.rgbFrame.empty()) {
continue;
}
const uint64_t descriptor_generation = descriptorGeneration(
source.stream_epoch, source.codec_config_generation);
if (!have_cached_config_generation ||
cached_config_generation != descriptor_generation) {
cached_codec_config.clear();
cached_config_generation = descriptor_generation;
have_cached_config_generation = true;
}
if (!source.codec_config.empty()) {
cached_codec_config = source.codec_config;
}
TrackDescriptor::Config track;
track.id = track_id;
track.source_id = device->id();
track.kind = MediaKind::VIDEO;
track.codec = videoCodec(source.codec);
track.payload_format = videoPayloadFormat(track.codec, source.rgbFrame);
track.time_base = sanitizedTimeBase(
source.time_base_num,
source.time_base_den,
source.fps);
track.width = static_cast<uint32_t>(std::max(0, source.width));
track.height = static_cast<uint32_t>(std::max(0, source.height));
track.nominal_rate = static_cast<uint32_t>(std::max(0, source.fps));
track.fx = source.intrinsics.fx;
track.fy = source.intrinsics.fy;
track.cx = source.intrinsics.cx;
track.cy = source.intrinsics.cy;
track.distortion.assign(
std::begin(source.intrinsics.coeffs),
std::end(source.intrinsics.coeffs));
track.generation = descriptor_generation;
track.codec_config = cached_codec_config;
const bool epoch_changed = have_previous && source.stream_epoch != last_epoch;
const bool sequence_wrapped = have_previous &&
last_sequence == std::numeric_limits<uint64_t>::max() && source.sequence != 0;
const bool sequence_gap = have_previous && !epoch_changed &&
(sequence_wrapped ||
(last_sequence != std::numeric_limits<uint64_t>::max() &&
source.sequence != last_sequence + 1));
TrackDescriptorPtr descriptor;
try {
descriptor = makeTrackDescriptor(std::move(track));
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid camera descriptor for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
have_previous = true;
continue;
}
const bool descriptor_changed = last_descriptor &&
!equivalentTrackDescriptor(*last_descriptor, *descriptor);
const bool discontinuity = !have_previous || source.discontinuity || epoch_changed ||
sequence_gap || descriptor_changed;
const bool inter_frame_codec = descriptor->codec == Codec::H264 ||
descriptor->codec == Codec::H265;
if (discontinuity) {
pending_discontinuity = true;
if (inter_frame_codec) {
waiting_for_key_frame = true;
request_key_frame();
} else {
waiting_for_key_frame = false;
}
}
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
last_descriptor = descriptor;
have_previous = true;
if (waiting_for_key_frame && !source.bKey) {
request_key_frame();
continue;
}
waiting_for_key_frame = false;
MediaFrame::Config frame;
frame.descriptor = std::move(descriptor);
frame.payload = std::move(source.rgbFrame);
frame.sequence = source.sequence;
frame.source_timestamp = source.source_timestamp;
frame.source_frame_number = source.source_frame_number;
frame.pts = source.pts;
frame.dts = source.dts;
frame.duration = source.duration;
frame.capture_time_ns = source.capture_monotonic_ns > 0
? static_cast<uint64_t>(source.capture_monotonic_ns)
: 0;
frame.capture_utc_ns = source.capture_utc_ns;
frame.key_frame = source.bKey;
frame.discontinuity = pending_discontinuity;
MediaSourceHub::FrameSink current_sink;
{
std::lock_guard<std::mutex> lock(mutex);
current_sink = sink;
}
if (current_sink) {
try {
current_sink(makeMediaFrame(std::move(frame)));
pending_discontinuity = false;
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid camera frame for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
}
}
}
}
std::string track_id;
};
struct MicrophonePump final : PumpState<device::AbstractMicrophone> {
MicrophonePump(std::shared_ptr<device::AbstractMicrophone> microphone, std::string id)
: PumpState(std::move(microphone)), track_id(std::move(id)) {}
~MicrophonePump() override { stop(); }
void run() override {
size_t cursor = 0;
uint64_t last_epoch = 0;
uint64_t last_sequence = 0;
uint64_t cached_config_generation = 0;
std::vector<uint8_t> cached_codec_config;
TrackDescriptorPtr last_descriptor;
bool have_previous = false;
bool have_cached_config_generation = false;
bool pending_discontinuity = true;
while (running.load(std::memory_order_acquire)) {
device::AudioStreamFrameData source;
try {
if (!device->waitEncodedFrame(source, cursor, std::chrono::milliseconds(50))) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Microphone frame read failed for "
<< track_id << ": " << error.what();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
} catch (...) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Microphone frame read failed for "
<< track_id;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (source.data.empty()) {
continue;
}
const uint64_t descriptor_generation = descriptorGeneration(
source.stream_epoch, source.codec_config_generation);
if (!have_cached_config_generation ||
cached_config_generation != descriptor_generation) {
cached_codec_config.clear();
cached_config_generation = descriptor_generation;
have_cached_config_generation = true;
}
if (!source.codec_config.empty()) {
cached_codec_config = source.codec_config;
}
TrackDescriptor::Config track;
track.id = track_id;
track.source_id = device->id();
track.kind = MediaKind::AUDIO;
track.codec = audioCodec(source);
track.payload_format = audioPayloadFormat(track.codec, source.data);
track.time_base = sanitizedTimeBase(
source.time_base_num,
source.time_base_den,
source.sample_rate);
track.sample_rate = static_cast<uint32_t>(std::max(0, source.sample_rate));
track.channels = static_cast<uint32_t>(std::max(0, source.channels));
// Packet sample counts belong to MediaFrame::duration, not immutable track metadata.
track.nominal_rate = 0;
track.generation = descriptor_generation;
track.codec_config = cached_codec_config;
const bool epoch_changed = have_previous && source.stream_epoch != last_epoch;
const bool sequence_wrapped = have_previous &&
last_sequence == std::numeric_limits<uint64_t>::max() && source.sequence != 0;
const bool sequence_gap = have_previous && !epoch_changed &&
(sequence_wrapped ||
(last_sequence != std::numeric_limits<uint64_t>::max() &&
source.sequence != last_sequence + 1));
TrackDescriptorPtr descriptor;
try {
descriptor = makeTrackDescriptor(std::move(track));
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid microphone descriptor for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
have_previous = true;
continue;
}
const bool descriptor_changed = last_descriptor &&
!equivalentTrackDescriptor(*last_descriptor, *descriptor);
if (!have_previous || source.discontinuity || epoch_changed || sequence_gap ||
descriptor_changed) {
pending_discontinuity = true;
}
last_epoch = source.stream_epoch;
last_sequence = source.sequence;
last_descriptor = descriptor;
have_previous = true;
MediaFrame::Config frame;
frame.descriptor = std::move(descriptor);
frame.payload = std::move(source.data);
frame.sequence = source.sequence;
frame.pts = source.pts;
frame.dts = source.dts;
frame.duration = source.duration > 0 ? source.duration : source.nb_samples;
frame.capture_time_ns = source.capture_monotonic_ns > 0
? static_cast<uint64_t>(source.capture_monotonic_ns)
: 0;
frame.capture_utc_ns = source.capture_utc_ns;
frame.key_frame = true;
frame.discontinuity = pending_discontinuity;
MediaSourceHub::FrameSink current_sink;
{
std::lock_guard<std::mutex> lock(mutex);
current_sink = sink;
}
if (current_sink) {
try {
current_sink(makeMediaFrame(std::move(frame)));
pending_discontinuity = false;
} catch (const std::exception& error) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Invalid microphone frame for "
<< track_id << ": " << error.what();
pending_discontinuity = true;
}
}
}
}
std::string track_id;
};
TrackDescriptorPtr initialTrack(
std::string track_id,
std::string source_id,
const MediaKind kind) {
TrackDescriptor::Config config;
config.id = std::move(track_id);
config.source_id = std::move(source_id);
config.kind = kind;
// A placeholder descriptor is never emitted as a media sample, but it
// still carries a mathematically valid neutral time base.
config.time_base = Rational{1, 1};
config.generation = 1;
return makeTrackDescriptor(std::move(config));
}
} // namespace
MediaSourceHub& globalMediaSourceHub() {
static MediaSourceHub hub;
return hub;
}
std::string cameraColorTrackId(const std::string& device_id) {
return device_id + "/video/color";
}
std::string microphoneTrackId(const std::string& device_id) {
return device_id + "/audio/main";
}
bool ensureCameraMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractCamera>& camera,
const size_t ring_capacity) {
if (!camera || camera->id().empty()) {
return false;
}
const std::string track_id = cameraColorTrackId(camera->id());
if (hub.hasSource(track_id)) {
return true;
}
const auto pump = std::make_shared<CameraPump>(camera, track_id);
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [pump](
const MediaSourceHub::FrameSink& sink,
const MediaSourceHub::CancelPredicate& cancelled) {
return pump->begin(sink, cancelled);
};
callbacks.stop = [pump] { pump->stop(); };
callbacks.request_key_frame = [camera] { return camera->requestKeyFrame(); };
const bool registered = hub.registerSource(
initialTrack(track_id, camera->id(), MediaKind::VIDEO),
std::move(callbacks),
ring_capacity);
if (!registered && !hub.hasSource(track_id)) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to register camera track: " << track_id;
return false;
}
return true;
}
bool ensureMicrophoneMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractMicrophone>& microphone,
const size_t ring_capacity) {
if (!microphone || microphone->id().empty()) {
return false;
}
const std::string track_id = microphoneTrackId(microphone->id());
if (hub.hasSource(track_id)) {
return true;
}
const auto pump = std::make_shared<MicrophonePump>(microphone, track_id);
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [pump](
const MediaSourceHub::FrameSink& sink,
const MediaSourceHub::CancelPredicate& cancelled) {
return pump->begin(sink, cancelled);
};
callbacks.stop = [pump] { pump->stop(); };
const bool registered = hub.registerSource(
initialTrack(track_id, microphone->id(), MediaKind::AUDIO),
std::move(callbacks),
ring_capacity);
if (!registered && !hub.hasSource(track_id)) {
CMVR_LOG(ERROR) << "[DeviceMediaSourceAdapter] Failed to register microphone track: " << track_id;
return false;
}
return true;
}
} // namespace cmvr::media

View File

@ -0,0 +1,596 @@
#include "manager/media_source_hub/include/media_source_hub.h"
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <utility>
namespace cmvr::media {
struct MediaSourceHub::SourceState final : public std::enable_shared_from_this<SourceState> {
enum class Lifecycle {
STOPPED,
STARTING,
RUNNING,
STOPPING
};
struct StartAttempt {
size_t waiters{0};
bool completed{false};
bool succeeded{false};
std::atomic<bool> cancel_requested{false};
};
SourceState(
TrackDescriptorPtr initial_descriptor,
SourceCallbacks source_callbacks,
const size_t ring_capacity)
: track_id(initial_descriptor->id),
descriptor(std::move(initial_descriptor)),
callbacks(std::move(source_callbacks)),
ring(ring_capacity) {}
FrameSink makeSink() {
const std::weak_ptr<SourceState> weak_source = shared_from_this();
return [weak_source](MediaFramePtr frame) {
if (const auto source = weak_source.lock()) {
source->acceptFrame(std::move(frame));
}
};
}
void acceptFrame(MediaFramePtr frame) {
if (!frame || !frame->descriptor || frame->descriptor->id != track_id) {
return;
}
{
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (!registered ||
(lifecycle != Lifecycle::STARTING && lifecycle != Lifecycle::RUNNING)) {
return;
}
}
const TrackDescriptorPtr current = std::atomic_load(&descriptor);
if (!current || !equivalentTrackDescriptor(*current, *frame->descriptor)) {
// C++17 atomic shared_ptr free functions provide an atomic descriptor snapshot to
// all subscriptions while frames remain immutable.
std::atomic_store(&descriptor, frame->descriptor);
}
ring.publish(std::move(frame));
}
static bool isCancelled(const CancelPredicate& cancelled) noexcept {
if (!cancelled) return false;
try {
return cancelled();
} catch (...) {
return true;
}
}
bool invokeStart(
const FrameSink& sink,
const CancelPredicate& cancelled) noexcept {
std::lock_guard<std::mutex> callback_lock(callback_mutex);
try {
return callbacks.start && callbacks.start(sink, cancelled);
} catch (...) {
return false;
}
}
void invokeStop() noexcept {
std::lock_guard<std::mutex> callback_lock(callback_mutex);
try {
if (callbacks.stop) callbacks.stop();
} catch (...) {
}
}
void completeStart(
const std::shared_ptr<StartAttempt>& attempt,
const bool started) {
bool stop_abandoned_start = false;
{
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (start_attempt != attempt || lifecycle != Lifecycle::STARTING) {
return;
}
attempt->completed = true;
attempt->succeeded = started;
if (started && registered && attempt->waiters != 0U) {
lifecycle = Lifecycle::RUNNING;
} else if (started) {
lifecycle = Lifecycle::STOPPING;
ring.close();
stop_abandoned_start = true;
} else {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
if (stop_abandoned_start) {
invokeStop();
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
}
bool acquire(
const StartPosition start_position,
FrameRing::Cursor& cursor,
const CancelPredicate& cancelled) {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
while (lifecycle == Lifecycle::STOPPING) {
if (!registered || isCancelled(cancelled)) return false;
lifecycle_condition.wait_for(lock, std::chrono::milliseconds(10));
}
if (!registered || isCancelled(cancelled)) return false;
if (lifecycle == Lifecycle::RUNNING) {
++subscriber_count;
lock.unlock();
cursor = ring.makeCursor(start_position);
return true;
}
std::shared_ptr<StartAttempt> attempt;
if (lifecycle == Lifecycle::STOPPED) {
lifecycle = Lifecycle::STARTING;
ring.reset();
const FrameSink sink = makeSink();
attempt = std::make_shared<StartAttempt>();
attempt->waiters = 1U;
start_attempt = attempt;
const auto self = shared_from_this();
try {
std::thread([self, attempt, sink]() {
const CancelPredicate cancelled = [attempt] {
return attempt->cancel_requested.load(
std::memory_order_acquire);
};
const bool started = self->invokeStart(sink, cancelled);
self->completeStart(attempt, started);
}).detach();
} catch (...) {
start_attempt.reset();
lifecycle = Lifecycle::STOPPED;
lifecycle_condition.notify_all();
return false;
}
} else if (lifecycle == Lifecycle::STARTING) {
attempt = start_attempt;
if (!attempt || attempt->cancel_requested.load(std::memory_order_acquire)) {
return false;
}
++attempt->waiters;
} else {
return false;
}
while (registered && !attempt->completed) {
if (isCancelled(cancelled)) {
if (attempt->waiters != 0U) --attempt->waiters;
if (attempt->waiters == 0U) {
attempt->cancel_requested.store(true, std::memory_order_release);
}
lifecycle_condition.notify_all();
return false;
}
lifecycle_condition.wait_for(lock, std::chrono::milliseconds(10));
}
const bool caller_cancelled = isCancelled(cancelled);
const bool acquired = !caller_cancelled && registered && attempt->completed &&
attempt->succeeded &&
lifecycle == Lifecycle::RUNNING;
if (attempt->waiters != 0U) --attempt->waiters;
if (!acquired) {
if (attempt->waiters == 0U) {
attempt->cancel_requested.store(true, std::memory_order_release);
}
const bool stop_unclaimed_source =
start_attempt == attempt && attempt->waiters == 0U &&
subscriber_count == 0U &&
lifecycle == Lifecycle::RUNNING;
if (stop_unclaimed_source) {
lifecycle = Lifecycle::STOPPING;
ring.close();
lock.unlock();
invokeStop();
lock.lock();
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
return false;
}
++subscriber_count;
lock.unlock();
cursor = ring.makeCursor(start_position);
return true;
}
void release() {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
if (subscriber_count == 0) {
return;
}
--subscriber_count;
if (subscriber_count != 0 || lifecycle != Lifecycle::RUNNING) {
return;
}
lifecycle = Lifecycle::STOPPING;
ring.close();
lock.unlock();
invokeStop();
lock.lock();
lifecycle = Lifecycle::STOPPED;
lifecycle_condition.notify_all();
}
bool deactivateIfUnused() {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
if (subscriber_count != 0) {
return false;
}
registered = false;
ring.close();
if (lifecycle == Lifecycle::STARTING) {
if (start_attempt) {
start_attempt->cancel_requested.store(true, std::memory_order_release);
}
lifecycle_condition.notify_all();
return true;
}
if (lifecycle == Lifecycle::STOPPING || lifecycle == Lifecycle::STOPPED) {
lifecycle_condition.notify_all();
return true;
}
lifecycle = Lifecycle::STOPPING;
lock.unlock();
invokeStop();
lock.lock();
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
return true;
}
void shutdown() {
std::unique_lock<std::mutex> lock(lifecycle_mutex);
registered = false;
ring.close();
if (lifecycle == Lifecycle::STARTING) {
if (start_attempt) {
start_attempt->cancel_requested.store(true, std::memory_order_release);
}
lifecycle_condition.notify_all();
return;
}
if (lifecycle == Lifecycle::STOPPING) {
lifecycle_condition.notify_all();
return;
}
if (lifecycle == Lifecycle::STOPPED) {
lifecycle_condition.notify_all();
return;
}
lifecycle = Lifecycle::STOPPING;
lock.unlock();
invokeStop();
lock.lock();
if (lifecycle == Lifecycle::STOPPING) {
lifecycle = Lifecycle::STOPPED;
}
lifecycle_condition.notify_all();
}
bool validForSubscription() const {
std::lock_guard<std::mutex> lock(lifecycle_mutex);
return registered && lifecycle == Lifecycle::RUNNING;
}
size_t subscriberCount() const {
std::lock_guard<std::mutex> lock(lifecycle_mutex);
return subscriber_count;
}
bool requestKeyFrame() const {
// Serialize with stop first, then re-check lifecycle. A stop that has
// already begun rejects the request; a stop that begins afterwards
// waits for this callback before invoking the device stop barrier.
std::lock_guard<std::mutex> callback_lock(callback_mutex);
{
std::lock_guard<std::mutex> lock(lifecycle_mutex);
if (!registered || lifecycle != Lifecycle::RUNNING || !callbacks.request_key_frame) {
return false;
}
}
try {
return callbacks.request_key_frame();
} catch (...) {
return false;
}
}
TrackDescriptorPtr currentDescriptor() const {
return std::atomic_load(&descriptor);
}
const std::string track_id;
mutable TrackDescriptorPtr descriptor;
const SourceCallbacks callbacks;
FrameRing ring;
mutable std::mutex callback_mutex;
mutable std::mutex lifecycle_mutex;
std::condition_variable lifecycle_condition;
Lifecycle lifecycle{Lifecycle::STOPPED};
size_t subscriber_count{0};
bool registered{true};
std::shared_ptr<StartAttempt> start_attempt;
};
struct MediaSourceHub::Impl final {
mutable std::mutex mutex;
std::unordered_map<std::string, std::shared_ptr<SourceState>> sources;
};
MediaSourceHub::Subscription::Subscription(
std::shared_ptr<SourceState> source,
FrameRing::Cursor cursor)
: source_(std::move(source)),
cursor_(std::move(cursor)),
active_(static_cast<bool>(source_)) {}
MediaSourceHub::Subscription::~Subscription() {
reset();
}
MediaSourceHub::Subscription::Subscription(Subscription&& other) noexcept
: source_(std::move(other.source_)),
cursor_(other.cursor_),
active_(other.active_) {
other.active_ = false;
}
MediaSourceHub::Subscription& MediaSourceHub::Subscription::operator=(Subscription&& other) noexcept {
if (this == &other) {
return *this;
}
reset();
source_ = std::move(other.source_);
cursor_ = other.cursor_;
active_ = other.active_;
other.active_ = false;
return *this;
}
bool MediaSourceHub::Subscription::valid() const {
return active_ && source_ && source_->validForSubscription();
}
TrackDescriptorPtr MediaSourceHub::Subscription::descriptor() const {
return source_ ? source_->currentDescriptor() : nullptr;
}
std::optional<MediaSourceHub::FrameReadResult> MediaSourceHub::Subscription::tryRead() {
if (!active_ || !source_) {
return std::nullopt;
}
return source_->ring.tryRead(cursor_);
}
std::optional<MediaSourceHub::FrameReadResult> MediaSourceHub::Subscription::waitRead(
const std::chrono::milliseconds timeout) {
if (!active_ || !source_) {
return std::nullopt;
}
return source_->ring.waitRead(cursor_, timeout);
}
uint64_t MediaSourceHub::Subscription::discardPendingIfExceeds(
const size_t maximum_pending_frames) {
if (!active_ || !source_) {
return 0;
}
return source_->ring.discardPendingIfExceeds(cursor_, maximum_pending_frames);
}
uint64_t MediaSourceHub::Subscription::droppedCount() const noexcept {
return cursor_.dropped_count;
}
void MediaSourceHub::Subscription::reset() {
if (active_ && source_) {
source_->release();
}
active_ = false;
source_.reset();
}
MediaSourceHub::MediaSourceHub()
: impl_(std::make_shared<Impl>()) {}
MediaSourceHub::~MediaSourceHub() {
shutdown();
}
bool MediaSourceHub::registerSource(
TrackDescriptorPtr initial_descriptor,
SourceCallbacks callbacks,
const size_t ring_capacity) {
if (!impl_ || !initial_descriptor || initial_descriptor->id.empty() ||
!callbacks.start || ring_capacity == 0) {
return false;
}
std::shared_ptr<SourceState> source;
try {
source = std::make_shared<SourceState>(
std::move(initial_descriptor), std::move(callbacks), ring_capacity);
} catch (...) {
return false;
}
std::lock_guard<std::mutex> lock(impl_->mutex);
return impl_->sources.emplace(source->track_id, std::move(source)).second;
}
bool MediaSourceHub::unregisterSource(const std::string& track_id) {
if (!impl_ || track_id.empty()) {
return false;
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return false;
}
source = it->second;
}
if (!source->deactivateIfUnused()) {
return false;
}
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it != impl_->sources.end() && it->second == source) {
impl_->sources.erase(it);
return true;
}
return false;
}
bool MediaSourceHub::hasSource(const std::string& track_id) const {
if (!impl_) {
return false;
}
std::lock_guard<std::mutex> lock(impl_->mutex);
return impl_->sources.find(track_id) != impl_->sources.end();
}
std::vector<TrackDescriptorPtr> MediaSourceHub::listTracks() const {
std::vector<std::shared_ptr<SourceState>> sources;
if (!impl_) {
return {};
}
{
std::lock_guard<std::mutex> lock(impl_->mutex);
sources.reserve(impl_->sources.size());
for (const auto& [track_id, source] : impl_->sources) {
(void)track_id;
sources.push_back(source);
}
}
std::vector<TrackDescriptorPtr> descriptors;
descriptors.reserve(sources.size());
for (const auto& source : sources) {
descriptors.push_back(source->currentDescriptor());
}
std::sort(descriptors.begin(), descriptors.end(), [](const auto& lhs, const auto& rhs) {
if (!lhs) return static_cast<bool>(rhs);
if (!rhs) return false;
return lhs->id < rhs->id;
});
return descriptors;
}
size_t MediaSourceHub::subscriberCount(const std::string& track_id) const {
if (!impl_) {
return 0;
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return 0;
}
source = it->second;
}
return source->subscriberCount();
}
bool MediaSourceHub::requestKeyFrame(const std::string& track_id) const {
if (!impl_) {
return false;
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return false;
}
source = it->second;
}
return source->requestKeyFrame();
}
MediaSourceHub::Subscription MediaSourceHub::subscribe(
const std::string& track_id,
const StartPosition start_position,
CancelPredicate cancelled) {
if (!impl_) {
return {};
}
std::shared_ptr<SourceState> source;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
const auto it = impl_->sources.find(track_id);
if (it == impl_->sources.end()) {
return {};
}
source = it->second;
}
FrameRing::Cursor cursor;
if (!source->acquire(start_position, cursor, cancelled)) {
return {};
}
return Subscription(std::move(source), std::move(cursor));
}
void MediaSourceHub::shutdown() {
if (!impl_) {
return;
}
std::vector<std::shared_ptr<SourceState>> sources;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
sources.reserve(impl_->sources.size());
for (auto& [track_id, source] : impl_->sources) {
(void)track_id;
sources.push_back(std::move(source));
}
impl_->sources.clear();
}
for (const auto& source : sources) {
source->shutdown();
}
}
} // namespace cmvr::media

View File

@ -0,0 +1,683 @@
#include "manager/media_source_hub/include/media_source_hub.h"
#include <atomic>
#include <chrono>
#include <future>
#include <iostream>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
namespace {
using namespace std::chrono_literals;
using cmvr::media::Codec;
using cmvr::media::MediaFrame;
using cmvr::media::MediaFramePtr;
using cmvr::media::MediaKind;
using cmvr::media::MediaSourceHub;
using cmvr::media::PayloadFormat;
using cmvr::media::Rational;
using cmvr::media::TrackDescriptor;
using cmvr::media::TrackDescriptorPtr;
static_assert(!std::is_copy_assignable<MediaFrame>::value, "MediaFrame must be immutable");
static_assert(!std::is_copy_assignable<TrackDescriptor>::value, "TrackDescriptor must be immutable");
static_assert(std::is_same<MediaFramePtr::element_type, const MediaFrame>::value,
"MediaFramePtr must share const frames");
int failures = 0;
#define CHECK_TRUE(expression) \
do { \
if (!(expression)) { \
std::cerr << __FILE__ << ':' << __LINE__ << " check failed: " #expression << '\n'; \
++failures; \
} \
} while (false)
TrackDescriptorPtr makeVideoDescriptor(
const Codec codec,
const uint64_t generation,
std::vector<uint8_t> codec_config = {}) {
TrackDescriptor::Config config;
config.id = "camera.front.video";
config.source_id = "camera.front";
config.kind = MediaKind::VIDEO;
config.codec = codec;
config.payload_format = codec == Codec::UNKNOWN ? PayloadFormat::UNKNOWN : PayloadFormat::ANNEX_B;
config.time_base = Rational{1, 90000};
config.width = 640;
config.height = 360;
config.nominal_rate = 30;
config.generation = generation;
config.codec_config = std::move(codec_config);
return cmvr::media::makeTrackDescriptor(std::move(config));
}
MediaFramePtr makeFrame(
TrackDescriptorPtr descriptor,
const uint64_t sequence,
const uint8_t marker) {
MediaFrame::Config config;
config.descriptor = std::move(descriptor);
config.payload = {marker, static_cast<uint8_t>(marker + 1)};
config.sequence = sequence;
config.pts = static_cast<int64_t>(sequence * 3000);
config.dts = config.pts;
config.duration = 3000;
config.capture_time_ns = sequence * 1000000;
config.capture_utc_ns = 1700000000000000000LL + static_cast<int64_t>(sequence);
config.key_frame = sequence == 0;
return cmvr::media::makeMediaFrame(std::move(config));
}
void testMediaMetadataValidation() {
TrackDescriptor::Config invalid;
invalid.id = "invalid.video";
invalid.source_id = "invalid";
invalid.kind = MediaKind::VIDEO;
invalid.time_base = Rational{0, 1};
bool rejected = false;
try {
(void)cmvr::media::makeTrackDescriptor(std::move(invalid));
} catch (const std::invalid_argument&) {
rejected = true;
}
CHECK_TRUE(rejected);
const auto frame = makeFrame(makeVideoDescriptor(Codec::H264, 1), 7, 1);
CHECK_TRUE(frame->capture_time_ns == 7000000);
CHECK_TRUE(frame->capture_utc_ns == 1700000000000000007LL);
CHECK_TRUE(frame->duration == 3000);
}
void testLegacySpmcCompatibility() {
bool ring_zero_capacity_rejected = false;
try {
RingBuffer<int> invalid_ring(0);
} catch (const std::invalid_argument&) {
ring_zero_capacity_rejected = true;
}
CHECK_TRUE(ring_zero_capacity_rejected);
bool spmc_zero_capacity_rejected = false;
try {
SPMCRingBuffer<int> invalid_ring(0);
} catch (const std::invalid_argument&) {
spmc_zero_capacity_rejected = true;
}
CHECK_TRUE(spmc_zero_capacity_rejected);
SPMCRingBuffer<int> ring(2);
ring.push(10);
ring.push(20);
CHECK_TRUE(ring.size() == 2);
CHECK_TRUE(ring.getHead() == 2);
CHECK_TRUE(ring.getTail() == 0);
CHECK_TRUE(ring.getLast().has_value() && *ring.getLast() == 20);
size_t reader = 0;
CHECK_TRUE(ring.pop(reader).has_value());
ring.push(30);
ring.push(40);
CHECK_TRUE(!ring.pop(reader).has_value());
CHECK_TRUE(reader == ring.getTail());
CHECK_TRUE(ring.pop(reader).has_value());
ring.clear();
CHECK_TRUE(ring.empty());
CHECK_TRUE(ring.getHead() == 4);
ring.push(50);
CHECK_TRUE(!ring.pop(reader).has_value());
const auto after_clear = ring.pop(reader);
CHECK_TRUE(after_clear.has_value() && *after_clear == 50);
}
void testBroadcastFrameRing() {
using Ring = BroadcastFrameRing<MediaFrame>;
Ring ring(2);
const auto descriptor = makeVideoDescriptor(Codec::H264, 1, {1, 2, 3});
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
CHECK_TRUE(ring.publish(makeFrame(descriptor, 0, 10)).value() == 0);
CHECK_TRUE(ring.publish(makeFrame(descriptor, 1, 20)).value() == 1);
CHECK_TRUE(ring.publish(makeFrame(descriptor, 2, 30)).value() == 2);
const auto first = ring.tryRead(cursor);
CHECK_TRUE(first.has_value());
CHECK_TRUE(first->sequence == 1);
CHECK_TRUE(first->value->sequence == 1);
CHECK_TRUE(first->dropped_count == 1);
CHECK_TRUE(first->dropped_since_last_read == 1);
CHECK_TRUE(ring.stats().dropped_count == 1);
const auto second = ring.tryRead(cursor);
CHECK_TRUE(second.has_value() && second->sequence == 2);
CHECK_TRUE(second->dropped_since_last_read == 0);
auto waiting_cursor = ring.makeCursor(Ring::StartPosition::NEXT_PUBLISHED);
auto waiting_read = std::async(std::launch::async, [&ring, &waiting_cursor] {
return ring.waitRead(waiting_cursor, 1s);
});
std::this_thread::sleep_for(10ms);
ring.publish(makeFrame(descriptor, 3, 40));
CHECK_TRUE(waiting_read.wait_for(500ms) == std::future_status::ready);
CHECK_TRUE(waiting_read.get().has_value());
const uint64_t next_generation = ring.reset();
CHECK_TRUE(next_generation == 2);
ring.publish(makeFrame(descriptor, 4, 50));
const auto after_reset = ring.tryRead(cursor);
CHECK_TRUE(after_reset.has_value());
CHECK_TRUE(after_reset->generation == 2);
CHECK_TRUE(after_reset->sequence == 0);
CHECK_TRUE(after_reset->generation_changed);
auto close_cursor = ring.makeCursor(Ring::StartPosition::NEXT_PUBLISHED);
auto close_wait = std::async(std::launch::async, [&ring, &close_cursor] {
return ring.waitRead(close_cursor, 2s);
});
ring.close();
CHECK_TRUE(close_wait.wait_for(500ms) == std::future_status::ready);
CHECK_TRUE(!close_wait.get().has_value());
CHECK_TRUE(!ring.publish(makeFrame(descriptor, 5, 60)).has_value());
}
void testBroadcastDiscardPending() {
using Ring = BroadcastFrameRing<MediaFrame>;
const auto descriptor = makeVideoDescriptor(Codec::H264, 1, {1, 2, 3});
{
Ring ring(8);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
ring.publish(makeFrame(descriptor, 0, 10));
ring.publish(makeFrame(descriptor, 1, 20));
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 2) == 0);
const auto first = ring.tryRead(cursor);
CHECK_TRUE(first.has_value());
CHECK_TRUE(first->sequence == 0);
CHECK_TRUE(first->dropped_since_last_read == 0);
}
{
Ring ring(8);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
ring.publish(makeFrame(descriptor, 0, 10));
ring.publish(makeFrame(descriptor, 1, 20));
ring.publish(makeFrame(descriptor, 2, 30));
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 2) == 3);
CHECK_TRUE(!ring.tryRead(cursor).has_value());
ring.publish(makeFrame(descriptor, 3, 40));
const auto after_discard = ring.tryRead(cursor);
CHECK_TRUE(after_discard.has_value());
CHECK_TRUE(after_discard->sequence == 3);
CHECK_TRUE(after_discard->dropped_count == 3);
CHECK_TRUE(after_discard->dropped_since_last_read == 3);
}
{
// Two frames are overwritten before the explicit three-frame discard.
// Both kinds of loss must be reported by the next successful read.
Ring ring(3);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
for (uint64_t sequence = 0; sequence < 5; ++sequence) {
ring.publish(makeFrame(
descriptor,
sequence,
static_cast<uint8_t>(sequence)));
}
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 2) == 3);
CHECK_TRUE(cursor.dropped_count == 5);
ring.publish(makeFrame(descriptor, 5, 50));
const auto after_overwrite_and_discard = ring.tryRead(cursor);
CHECK_TRUE(after_overwrite_and_discard.has_value());
CHECK_TRUE(after_overwrite_and_discard->sequence == 5);
CHECK_TRUE(after_overwrite_and_discard->dropped_count == 5);
CHECK_TRUE(after_overwrite_and_discard->dropped_since_last_read == 5);
}
{
// An old-generation OLDEST_AVAILABLE cursor adopts the reset generation
// before deciding whether that generation's pending frames are excessive.
Ring ring(4);
auto cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
ring.publish(makeFrame(descriptor, 0, 10));
ring.reset();
ring.publish(makeFrame(descriptor, 1, 20));
ring.publish(makeFrame(descriptor, 2, 30));
CHECK_TRUE(ring.discardPendingIfExceeds(cursor, 1) == 2);
ring.publish(makeFrame(descriptor, 3, 40));
const auto after_reset = ring.tryRead(cursor);
CHECK_TRUE(after_reset.has_value());
CHECK_TRUE(after_reset->generation == 2);
CHECK_TRUE(after_reset->sequence == 2);
CHECK_TRUE(after_reset->generation_changed);
CHECK_TRUE(after_reset->dropped_since_last_read == 2);
}
}
void testBroadcastDiscardConcurrentPublish() {
using Ring = BroadcastFrameRing<int>;
constexpr uint64_t frame_count = 4000;
Ring ring(64);
auto cursor = ring.makeCursor(Ring::StartPosition::NEXT_PUBLISHED);
std::atomic<bool> start{false};
std::atomic<bool> publisher_done{false};
std::thread publisher([&] {
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
for (uint64_t sequence = 0; sequence < frame_count; ++sequence) {
ring.publish(std::make_shared<const int>(static_cast<int>(sequence)));
if ((sequence & 7U) == 0U) {
std::this_thread::yield();
}
}
publisher_done.store(true, std::memory_order_release);
});
uint64_t read_count = 0;
uint64_t actively_discarded = 0;
start.store(true, std::memory_order_release);
while (true) {
actively_discarded += ring.discardPendingIfExceeds(cursor, 8);
if (ring.tryRead(cursor)) {
++read_count;
continue;
}
if (publisher_done.load(std::memory_order_acquire)) {
actively_discarded += ring.discardPendingIfExceeds(cursor, 8);
if (ring.tryRead(cursor)) {
++read_count;
continue;
}
break;
}
std::this_thread::yield();
}
publisher.join();
CHECK_TRUE(read_count + cursor.dropped_count == frame_count);
CHECK_TRUE(actively_discarded <= cursor.dropped_count);
}
void testBroadcastConcurrency() {
using Ring = BroadcastFrameRing<MediaFrame>;
constexpr uint64_t frame_count = 500;
Ring ring(frame_count);
const auto descriptor = makeVideoDescriptor(Codec::H264, 1, {1, 2, 3});
auto first_cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
auto second_cursor = ring.makeCursor(Ring::StartPosition::OLDEST_AVAILABLE);
auto consume = [&ring](Ring::Cursor& cursor) {
uint64_t expected = 0;
while (expected < frame_count) {
const auto result = ring.waitRead(cursor, 1s);
if (!result || result->sequence != expected || result->value->sequence != expected) {
return false;
}
++expected;
}
return cursor.dropped_count == 0;
};
auto first_consumer = std::async(std::launch::async, consume, std::ref(first_cursor));
auto second_consumer = std::async(std::launch::async, consume, std::ref(second_cursor));
std::thread producer([&ring, &descriptor] {
for (uint64_t sequence = 0; sequence < frame_count; ++sequence) {
ring.publish(makeFrame(descriptor, sequence, static_cast<uint8_t>(sequence)));
}
});
producer.join();
CHECK_TRUE(first_consumer.get());
CHECK_TRUE(second_consumer.get());
}
void testHubLifecycleAndDescriptorRefresh() {
MediaSourceHub hub;
const auto initial_descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<int> start_count{0};
std::atomic<int> stop_count{0};
std::atomic<int> key_frame_requests{0};
std::mutex sink_mutex;
MediaSourceHub::FrameSink sink;
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink& callback_sink,
const MediaSourceHub::CancelPredicate&) {
{
std::lock_guard<std::mutex> lock(sink_mutex);
sink = callback_sink;
}
++start_count;
return true;
};
callbacks.stop = [&] {
++stop_count;
std::lock_guard<std::mutex> lock(sink_mutex);
sink = {};
};
callbacks.request_key_frame = [&] {
++key_frame_requests;
return true;
};
CHECK_TRUE(hub.registerSource(initial_descriptor, callbacks, 4));
CHECK_TRUE(!hub.registerSource(initial_descriptor, callbacks, 4));
CHECK_TRUE(hub.hasSource(initial_descriptor->id));
CHECK_TRUE(hub.listTracks().size() == 1);
auto first = hub.subscribe(initial_descriptor->id);
auto second = hub.subscribe(initial_descriptor->id);
CHECK_TRUE(first.valid() && second.valid());
CHECK_TRUE(hub.requestKeyFrame(initial_descriptor->id));
CHECK_TRUE(key_frame_requests == 1);
CHECK_TRUE(start_count == 1);
CHECK_TRUE(hub.subscriberCount(initial_descriptor->id) == 2);
CHECK_TRUE(first.descriptor()->codec == Codec::UNKNOWN);
CHECK_TRUE(!hub.unregisterSource(initial_descriptor->id));
// Content changes at the same generation must atomically replace the initial descriptor.
const auto actual_descriptor = makeVideoDescriptor(Codec::H264, 1, {0, 0, 0, 1, 0x67});
MediaSourceHub::FrameSink producer;
{
std::lock_guard<std::mutex> lock(sink_mutex);
producer = sink;
}
CHECK_TRUE(static_cast<bool>(producer));
const auto shared_frame = makeFrame(actual_descriptor, 0, 70);
producer(shared_frame);
const auto first_read = first.waitRead(500ms);
const auto second_read = second.waitRead(500ms);
CHECK_TRUE(first_read.has_value() && first_read->value == shared_frame);
CHECK_TRUE(second_read.has_value() && second_read->value == shared_frame);
CHECK_TRUE(first.descriptor() == actual_descriptor);
CHECK_TRUE(second.descriptor()->codec_config == actual_descriptor->codec_config);
first.reset();
CHECK_TRUE(stop_count == 0);
CHECK_TRUE(hub.subscriberCount(initial_descriptor->id) == 1);
second.reset();
CHECK_TRUE(stop_count == 1);
CHECK_TRUE(!hub.requestKeyFrame(initial_descriptor->id));
CHECK_TRUE(hub.subscriberCount(initial_descriptor->id) == 0);
{
auto restarted = hub.subscribe(initial_descriptor->id);
CHECK_TRUE(restarted.valid());
CHECK_TRUE(start_count == 2);
}
CHECK_TRUE(stop_count == 2);
CHECK_TRUE(hub.unregisterSource(initial_descriptor->id));
CHECK_TRUE(!hub.hasSource(initial_descriptor->id));
}
void testSubscriptionDiscardPending() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::H264, 1);
std::mutex sink_mutex;
MediaSourceHub::FrameSink sink;
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink& callback_sink,
const MediaSourceHub::CancelPredicate&) {
std::lock_guard<std::mutex> lock(sink_mutex);
sink = callback_sink;
return true;
};
callbacks.stop = [&] {
std::lock_guard<std::mutex> lock(sink_mutex);
sink = {};
};
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 8));
auto subscription = hub.subscribe(descriptor->id);
CHECK_TRUE(subscription.valid());
MediaSourceHub::FrameSink producer;
{
std::lock_guard<std::mutex> lock(sink_mutex);
producer = sink;
}
CHECK_TRUE(static_cast<bool>(producer));
producer(makeFrame(descriptor, 0, 10));
producer(makeFrame(descriptor, 1, 20));
producer(makeFrame(descriptor, 2, 30));
CHECK_TRUE(subscription.discardPendingIfExceeds(2) == 3);
CHECK_TRUE(!subscription.tryRead().has_value());
producer(makeFrame(descriptor, 3, 40));
const auto next = subscription.tryRead();
CHECK_TRUE(next.has_value());
CHECK_TRUE(next->value->sequence == 3);
CHECK_TRUE(next->dropped_since_last_read == 3);
CHECK_TRUE(subscription.droppedCount() == 3);
}
void testHubFailedStartAndShutdown() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<int> start_attempts{0};
std::atomic<int> retry_stop_count{0};
MediaSourceHub::SourceCallbacks failed_callbacks;
failed_callbacks.start = [&](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
return ++start_attempts >= 2;
};
failed_callbacks.stop = [&] { ++retry_stop_count; };
CHECK_TRUE(hub.registerSource(descriptor, std::move(failed_callbacks), 2));
auto failed = hub.subscribe(descriptor->id);
CHECK_TRUE(!failed.valid());
auto retry = hub.subscribe(descriptor->id);
CHECK_TRUE(retry.valid());
retry.reset();
CHECK_TRUE(start_attempts == 2);
CHECK_TRUE(retry_stop_count == 1);
CHECK_TRUE(hub.unregisterSource(descriptor->id));
std::atomic<int> stop_count{0};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
return true;
};
callbacks.stop = [&] { ++stop_count; };
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto live = hub.subscribe(descriptor->id);
CHECK_TRUE(live.valid());
hub.shutdown();
CHECK_TRUE(stop_count == 1);
CHECK_TRUE(!live.valid());
CHECK_TRUE(!live.waitRead(50ms).has_value());
}
void testKeyFrameRequestIsOrderedBeforeStop() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::H264, 1);
std::atomic<bool> key_frame_entered{false};
std::atomic<bool> release_key_frame{false};
std::atomic<int> stop_count{0};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
return true;
};
callbacks.stop = [&] { ++stop_count; };
callbacks.request_key_frame = [&] {
key_frame_entered.store(true, std::memory_order_release);
while (!release_key_frame.load(std::memory_order_acquire)) {
std::this_thread::sleep_for(1ms);
}
return true;
};
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto subscription = hub.subscribe(descriptor->id);
CHECK_TRUE(subscription.valid());
auto key_frame = std::async(std::launch::async, [&] {
return hub.requestKeyFrame(descriptor->id);
});
const auto enter_deadline = std::chrono::steady_clock::now() + 500ms;
while (!key_frame_entered.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < enter_deadline) {
std::this_thread::sleep_for(1ms);
}
CHECK_TRUE(key_frame_entered.load(std::memory_order_acquire));
auto stop = std::async(std::launch::async, [&] { subscription.reset(); });
CHECK_TRUE(stop.wait_for(20ms) == std::future_status::timeout);
CHECK_TRUE(stop_count.load(std::memory_order_acquire) == 0);
release_key_frame.store(true, std::memory_order_release);
CHECK_TRUE(key_frame.get());
CHECK_TRUE(stop.wait_for(500ms) == std::future_status::ready);
stop.get();
CHECK_TRUE(stop_count.load(std::memory_order_acquire) == 1);
CHECK_TRUE(!hub.requestKeyFrame(descriptor->id));
}
void testHubCancelsBlockedStartWithoutBlockingShutdown() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<bool> start_entered{false};
std::atomic<bool> start_exited{false};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate& cancelled) {
start_entered.store(true, std::memory_order_release);
while (!cancelled()) {
std::this_thread::sleep_for(2ms);
}
start_exited.store(true, std::memory_order_release);
return false;
};
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto subscription_future = std::async(std::launch::async, [&] {
return hub.subscribe(descriptor->id);
});
const auto start_deadline = std::chrono::steady_clock::now() + 500ms;
while (!start_entered.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < start_deadline) {
std::this_thread::sleep_for(2ms);
}
auto shutdown_future = std::async(std::launch::async, [&] { hub.shutdown(); });
const bool shutdown_completed = shutdown_future.wait_for(500ms) ==
std::future_status::ready;
if (shutdown_completed) shutdown_future.get();
const bool subscribe_completed = subscription_future.wait_for(500ms) ==
std::future_status::ready;
bool invalid_subscription = false;
if (subscribe_completed) {
invalid_subscription = !subscription_future.get().valid();
}
const auto exit_deadline = std::chrono::steady_clock::now() + 500ms;
while (!start_exited.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < exit_deadline) {
std::this_thread::sleep_for(2ms);
}
CHECK_TRUE(start_entered.load(std::memory_order_acquire));
CHECK_TRUE(shutdown_completed);
CHECK_TRUE(subscribe_completed);
CHECK_TRUE(invalid_subscription);
CHECK_TRUE(start_exited.load(std::memory_order_acquire));
}
void testHubQuarantinesNonCooperativeStart() {
MediaSourceHub hub;
const auto descriptor = makeVideoDescriptor(Codec::UNKNOWN, 1);
std::atomic<bool> start_entered{false};
std::atomic<bool> release_start{false};
std::atomic<bool> start_exited{false};
std::atomic<int> stop_count{0};
MediaSourceHub::SourceCallbacks callbacks;
callbacks.start = [&](const MediaSourceHub::FrameSink&,
const MediaSourceHub::CancelPredicate&) {
start_entered.store(true, std::memory_order_release);
while (!release_start.load(std::memory_order_acquire)) {
std::this_thread::sleep_for(2ms);
}
start_exited.store(true, std::memory_order_release);
return true;
};
callbacks.stop = [&] { ++stop_count; };
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 2));
auto subscription_future = std::async(std::launch::async, [&] {
return hub.subscribe(descriptor->id);
});
const auto start_deadline = std::chrono::steady_clock::now() + 500ms;
while (!start_entered.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < start_deadline) {
std::this_thread::sleep_for(2ms);
}
const auto shutdown_started = std::chrono::steady_clock::now();
hub.shutdown();
const bool shutdown_was_bounded =
std::chrono::steady_clock::now() - shutdown_started < 500ms;
const bool subscribe_completed = subscription_future.wait_for(500ms) ==
std::future_status::ready;
bool invalid_subscription = false;
if (subscribe_completed) {
invalid_subscription = !subscription_future.get().valid();
}
// Release the deliberately non-cooperative test callback before its stack
// captures go out of scope. Late successful startup must be stopped once.
release_start.store(true, std::memory_order_release);
const auto exit_deadline = std::chrono::steady_clock::now() + 500ms;
while ((!start_exited.load(std::memory_order_acquire) || stop_count.load() != 1) &&
std::chrono::steady_clock::now() < exit_deadline) {
std::this_thread::sleep_for(2ms);
}
CHECK_TRUE(start_entered.load(std::memory_order_acquire));
CHECK_TRUE(shutdown_was_bounded);
CHECK_TRUE(subscribe_completed);
CHECK_TRUE(invalid_subscription);
CHECK_TRUE(start_exited.load(std::memory_order_acquire));
CHECK_TRUE(stop_count.load() == 1);
}
} // namespace
int main() {
testMediaMetadataValidation();
testLegacySpmcCompatibility();
testBroadcastFrameRing();
testBroadcastDiscardPending();
testBroadcastDiscardConcurrentPublish();
testBroadcastConcurrency();
testHubLifecycleAndDescriptorRefresh();
testSubscriptionDiscardPending();
testHubFailedStartAndShutdown();
testKeyFrameRequestIsOrderedBeforeStop();
testHubCancelsBlockedStartWithoutBlockingShutdown();
testHubQuarantinesNonCooperativeStart();
if (failures != 0) {
std::cerr << failures << " media_source_hub checks failed\n";
return 1;
}
std::cout << "media_source_hub self-test passed\n";
return 0;
}

View File

@ -38,6 +38,8 @@ const char* taskConfigTypeToString(const config::TaskConfigEntry::TaskType type)
return "TASK_TYPE_GRPC_SERVER";
case config::TaskConfigEntry::TASK_TYPE_SELF_COLLISION:
return "TASK_TYPE_SELF_COLLISION";
case config::TaskConfigEntry::TASK_TYPE_QUIC_EDGE:
return "TASK_TYPE_QUIC_EDGE";
case config::TaskConfigEntry::TASK_TYPE_UNKNOWN:
default:
return "TASK_TYPE_UNKNOWN";

View File

@ -12,6 +12,7 @@ target_link_libraries(cmvr_runtime PUBLIC
cmvr_es::device_manager
cmvr_es::task_manager
cmvr_es::service
cmvr_es::quic_edge_task
cmvr_es::mujoco_viewer
)

View File

@ -11,6 +11,7 @@
#include "common/config/config_files.h"
#include "common/io/proto_file_io.h"
#include "task/grpc_server_task/include/grpc_server_task.h"
#include "task/quic_edge_task/include/quic_edge_task.h"
namespace cmvr {
namespace {
@ -102,6 +103,7 @@ bool Runtime::init_(const std::string& config_path,
device::DeviceManager::getInstance(device_manager_root.device_manager());
task::registerGrpcServerTaskFactory();
task::registerQuicEdgeTaskFactory();
if (app_config.task_manager_config_file().empty()) {
CMVR_LOG(ERROR) << "TaskManager config file is empty";

View File

@ -23,6 +23,8 @@ target_link_libraries(service PRIVATE
cmvr_es::task_manager
cmvr_es::algorithms::controller
cmvr_es::task
cmvr_es::media_source_hub
cmvr_es::device_media_source_adapter
protobuf::libprotobuf
)

View File

@ -9,12 +9,14 @@
#include "common/base/grpc_utils.h"
#include "manager/device_manager/include/device_manager.h"
#include "devices/camera/abstract_camera.h"
#include "service/grpc/include/grpc_camera_stream_policy.h"
namespace cmvr::service {
class gRPCCameraServiceImpl final: public api::CameraService::Service {
public:
gRPCCameraServiceImpl();
explicit gRPCCameraServiceImpl(
CameraStreamLowLatencyConfig stream_config = {});
~gRPCCameraServiceImpl() override = default;
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetCameraStateCommand_Request* request, api::GetCameraStateCommand_Feedback* response) override;
grpc::Status StartCamera(grpc::ServerContext* context, const api::StartCameraCommand_Request* request, api::StartCameraCommand_Feedback* response) override;
@ -24,11 +26,13 @@ namespace cmvr::service {
grpc::Status GetRGBDImages(grpc::ServerContext* context, const api::GetRGBDImagesCommand_Request* request, api::GetRGBDImagesCommand_Feedback* response) override;
grpc::Status StartRecording(grpc::ServerContext* context, const api::StartCameraRecordingCommand_Request* request, api::StartCameraRecordingCommand_Feedback* response) override;
grpc::Status StopRecording(grpc::ServerContext* context, const api::StopCameraRecordingCommand_Request* request, api::StopCameraRecordingCommand_Feedback* response) override;
grpc::Status ControlPtz(grpc::ServerContext* context, const api::ControlPtzCommand_Request* request, api::ControlPtzCommand_Feedback* response) override;
grpc::Status GetDepthImageStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetDepthImageStreamCommand_Feedback, cmvr::api::GetDepthImageStreamCommand_Request>* stream) override;
grpc::Status GetRGBDImagesStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetRGBDImagesStreamCommand_Feedback, cmvr::api::GetRGBDImagesStreamCommand_Request>* stream) override;
grpc::Status GetRGBImageStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetRGBImageStreamCommand_Feedback, cmvr::api::GetRGBImageStreamCommand_Request>* stream) override;
private:
device::DeviceManager& dmgr_;
CameraStreamLowLatencyConfig stream_config_;
//双向流读写线程
std::shared_ptr<std::thread> read_thread_ = nullptr;

View File

@ -0,0 +1,60 @@
#ifndef CMVR_ES_GRPC_CAMERA_STREAM_POLICY_H
#define CMVR_ES_GRPC_CAMERA_STREAM_POLICY_H
#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <optional>
namespace cmvr::service {
inline constexpr size_t kDefaultCameraStreamMaxPendingFrames = 2;
inline constexpr uint32_t kDefaultCameraStreamMaxFrameAgeMs = 250;
struct CameraStreamLowLatencyConfig {
size_t max_pending_frames{kDefaultCameraStreamMaxPendingFrames};
std::chrono::milliseconds max_frame_age{
kDefaultCameraStreamMaxFrameAgeMs};
};
inline CameraStreamLowLatencyConfig makeCameraStreamLowLatencyConfig(
const uint32_t max_pending_frames,
const uint32_t max_frame_age_ms) noexcept {
CameraStreamLowLatencyConfig config;
config.max_pending_frames = max_pending_frames == 0
? kDefaultCameraStreamMaxPendingFrames
: static_cast<size_t>(max_pending_frames);
config.max_frame_age = std::chrono::milliseconds(
max_frame_age_ms == 0
? kDefaultCameraStreamMaxFrameAgeMs
: max_frame_age_ms);
return config;
}
inline std::optional<uint64_t> cameraFrameAgeNs(
const uint64_t capture_time_ns,
const uint64_t now_ns) noexcept {
if (capture_time_ns == 0 || now_ns < capture_time_ns) {
return std::nullopt;
}
return now_ns - capture_time_ns;
}
inline bool cameraFrameExceedsAgeLimit(
const uint64_t capture_time_ns,
const uint64_t now_ns,
const std::chrono::milliseconds max_frame_age) noexcept {
const auto age_ns = cameraFrameAgeNs(capture_time_ns, now_ns);
if (!age_ns || max_frame_age.count() <= 0) {
return false;
}
const auto max_age_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
max_frame_age).count();
return *age_ns > static_cast<uint64_t>(max_age_ns);
}
} // namespace cmvr::service
#endif // CMVR_ES_GRPC_CAMERA_STREAM_POLICY_H

View File

@ -22,6 +22,7 @@ namespace cmvr::service
grpc::Status StopRecord(grpc::ServerContext* context, const api::StopMicRecordingCommand_Request* request,api::StopMicRecordingCommand_Feedback* response) override;
grpc::Status PauseRecord(grpc::ServerContext* context, const api::PauseMicRecordingCommand_Request* request,api::PauseMicRecordingCommand_Feedback* response) override;
grpc::Status ResumeRecord(grpc::ServerContext* context, const api::ResumeMicRecordingCommand_Request* request,api::ResumeMicRecordingCommand_Feedback* response) override;
grpc::Status StreamAudio(grpc::ServerContext* context, const api::StreamMicAudioCommand_Request* request, grpc::ServerWriter<api::StreamMicAudioCommand_Feedback>* writer) override;
grpc::Status SetVolume(grpc::ServerContext* context, const api::SetMicPhoneVolumeCommand_Request* request,api::SetMicPhoneVolumeCommand_Feedback* response) override;
grpc::Status GetVolume(grpc::ServerContext* context, const api::GetMicPhoneVolumeCommand_Request* request,api::GetMicPhoneVolumeCommand_Feedback* response) override;
private:

View File

@ -17,6 +17,7 @@ namespace cmvr::service {
~gRPCSpeakerServiceImpl() override = default;
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetSpeakerStateCommand_Request* request,api::GetSpeakerStateCommand_Feedback* response) override;
grpc::Status PlayAudio(grpc::ServerContext* context, const api::PlayAudioCommand_Request* request,api::PlayAudioCommand_Feedback* response) override;
grpc::Status StreamAudio(grpc::ServerContext* context, grpc::ServerReader<api::StreamSpeakerAudioCommand_Request>* reader, api::StreamSpeakerAudioCommand_Feedback* response) override;
grpc::Status StopPlayback(grpc::ServerContext* context, const api::StopSpeakerCommand_Request* request,api::StopSpeakerCommand_Feedback* response) override;
grpc::Status PausePlayback(grpc::ServerContext* context, const api::PauseSpeakerCommand_Request* request,api::PauseSpeakerCommand_Feedback* response) override;
grpc::Status ResumePlayback(grpc::ServerContext* context, const api::ResumeSpeakerCommand_Request* request,api::ResumeSpeakerCommand_Feedback* response) override;

View File

@ -1,11 +1,17 @@
#include "common/base/logging/logger.h"
#include "manager/media_source_hub/include/device_media_source_adapter.h"
//
// Created by xtkuang on 2025/6/1.
//
#include "../include/grpc_camera_service.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <limits>
#include <thread>
using namespace std;
using namespace cmvr::service;
@ -20,9 +26,88 @@ grpc::Status failResponse(ResponseT* response, const std::string& message) {
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
bool toPtzCommand(cmvr::api::ControlPtzCommand_Command command, PtzCommand& out)
{
switch (command) {
case cmvr::api::ControlPtzCommand_Command_TILT_UP:
out = PtzCommand::TiltUp;
return true;
case cmvr::api::ControlPtzCommand_Command_TILT_DOWN:
out = PtzCommand::TiltDown;
return true;
case cmvr::api::ControlPtzCommand_Command_PAN_LEFT:
out = PtzCommand::PanLeft;
return true;
case cmvr::api::ControlPtzCommand_Command_PAN_RIGHT:
out = PtzCommand::PanRight;
return true;
case cmvr::api::ControlPtzCommand_Command_UP_LEFT:
out = PtzCommand::UpLeft;
return true;
case cmvr::api::ControlPtzCommand_Command_UP_RIGHT:
out = PtzCommand::UpRight;
return true;
case cmvr::api::ControlPtzCommand_Command_DOWN_LEFT:
out = PtzCommand::DownLeft;
return true;
case cmvr::api::ControlPtzCommand_Command_DOWN_RIGHT:
out = PtzCommand::DownRight;
return true;
case cmvr::api::ControlPtzCommand_Command_ZOOM_IN:
out = PtzCommand::ZoomIn;
return true;
case cmvr::api::ControlPtzCommand_Command_ZOOM_OUT:
out = PtzCommand::ZoomOut;
return true;
case cmvr::api::ControlPtzCommand_Command_PAN_AUTO:
out = PtzCommand::PanAuto;
return true;
default:
return false;
}
}
gRPCCameraServiceImpl::gRPCCameraServiceImpl(): dmgr_(DeviceManager::getInstance()) {}
// The legacy depth/RGBD RPCs acquire the camera's shared producer directly
// instead of going through MediaSourceHub. Keep that lease exception-safe:
// cancellation, a failed Write(), or any conversion error must release exactly
// the one startStreaming() reference acquired by this call.
class CameraStreamingLease final {
public:
explicit CameraStreamingLease(std::shared_ptr<AbstractCamera> camera)
: camera_(std::move(camera)) {
active_ = camera_ && camera_->startStreaming();
}
~CameraStreamingLease() {
if (!active_ || !camera_) {
return;
}
try {
camera_->stopStreaming();
} catch (const std::exception& e) {
CMVR_LOG(ERROR) << "[gRPCCameraServiceImpl] failed to release camera stream lease: "
<< e.what();
} catch (...) {
CMVR_LOG(ERROR) << "[gRPCCameraServiceImpl] failed to release camera stream lease";
}
}
CameraStreamingLease(const CameraStreamingLease&) = delete;
CameraStreamingLease& operator=(const CameraStreamingLease&) = delete;
explicit operator bool() const noexcept { return active_; }
private:
std::shared_ptr<AbstractCamera> camera_;
bool active_{false};
};
}
gRPCCameraServiceImpl::gRPCCameraServiceImpl(
CameraStreamLowLatencyConfig stream_config)
: dmgr_(DeviceManager::getInstance()),
stream_config_(stream_config) {}
grpc::Status gRPCCameraServiceImpl::GetStatus(grpc::ServerContext* context,
const api::GetCameraStateCommand_Request* request, api::GetCameraStateCommand_Feedback* response)
@ -47,14 +132,6 @@ grpc::Status gRPCCameraServiceImpl::GetStatus(grpc::ServerContext* context,
response->mutable_state()->set_fps(state.fps);
response->mutable_state()->set_width(state.width);
response->mutable_state()->set_height(state.height);
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (GetStatus): success, id=" << dev_id
<< ", initialized=" << state.is_initialized
<< ", opened=" << state.is_opened
<< ", streaming=" << state.is_streaming
<< ", recording=" << state.is_recording
<< ", error=" << state.is_error
<< ", size=" << state.width << "x" << state.height
<< ", fps=" << state.fps;
return grpc::Status::OK;
}
catch(const exception &e) {
@ -80,7 +157,6 @@ grpc::Status gRPCCameraServiceImpl::StartCamera(grpc::ServerContext* context,
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (StartCamera): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (const exception &e) {
@ -106,7 +182,6 @@ grpc::Status gRPCCameraServiceImpl::StopCamera(grpc::ServerContext* context,
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (StopCamera): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (exception &e) {
@ -130,7 +205,9 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImage(grpc::ServerContext* context,
}
Rs2Intrinsics intrinsics = {0};
dev->getRGBImage(image,intrinsics);
response->mutable_header()->set_success(true);
if (image.empty()) {
return failResponse(response, "Camera returned an empty RGB image: " + dev_id);
}
response->mutable_intrinsics()->set_fx(intrinsics.fx);
response->mutable_intrinsics()->set_fy(intrinsics.fy);
@ -160,10 +237,6 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImage(grpc::ServerContext* context,
response->mutable_color_frame()->set_height(image.rows);
response->mutable_color_frame()->set_width(image.cols);
response->mutable_color_frame()->set_codec("none");
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (GetRGBImage): success, id=" << dev_id
<< ", size=" << image.cols << "x" << image.rows
<< ", cv_type=" << image.type()
<< ", bytes=" << image.total() * image.elemSize();
return grpc::Status::OK;
}
catch (exception &e) {
@ -187,6 +260,9 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImage(grpc::ServerContext* context,
}
Rs2Intrinsics intrinsics = {0};
dev->getDepthImage(image,intrinsics);
if (image.empty()) {
return failResponse(response, "Camera returned an empty depth image: " + dev_id);
}
response->mutable_header()->set_success(true);
response->mutable_intrinsics()->set_fx(intrinsics.fx);
@ -220,10 +296,6 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImage(grpc::ServerContext* context,
response->mutable_depth_frame()->set_height(image.rows);
response->mutable_depth_frame()->set_width(image.cols);
response->mutable_depth_frame()->set_codec("none");
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (GetDepthImage): success, id=" << dev_id
<< ", size=" << image.cols << "x" << image.rows
<< ", cv_type=" << image.type()
<< ", bytes=" << image.total() * image.elemSize();
return grpc::Status::OK;
}
catch (exception &e) {
@ -247,6 +319,12 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
}
Rs2Intrinsics intrinsics = {0};
dev->getRGBDImages(color_image,depth_image, intrinsics);
if (color_image.empty()) {
return failResponse(response, "Camera returned an empty RGB image: " + dev_id);
}
if (depth_image.empty()) {
return failResponse(response, "Camera returned an empty depth image: " + dev_id);
}
response->mutable_header()->set_success(true);
response->mutable_intrinsics()->set_fx(intrinsics.fx);
@ -259,7 +337,8 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
// color image
// color image is serialized in the existing OpenCV BGR byte order;
// clients convert it once when constructing an RGB image.
if (color_image.type() == CV_8UC3) {
response->mutable_color_frame()->set_type(api::FrameData::U8C3);
}
@ -288,16 +367,14 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
else if (depth_image.type() == CV_32FC1) {
response->mutable_depth_frame()->set_type(api::FrameData::F32C1);
}
else {
return failResponse(response, "unsupported depth image type");
}
response->mutable_depth_frame()->set_data(
reinterpret_cast<const char*>(depth_image.data), depth_image.total() * depth_image.elemSize());
response->mutable_depth_frame()->set_height(depth_image.rows);
response->mutable_depth_frame()->set_width(depth_image.cols);
response->mutable_depth_frame()->set_codec("none");
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (GetRGBDImages): success, id=" << dev_id
<< ", color_size=" << color_image.cols << "x" << color_image.rows
<< ", color_type=" << color_image.type()
<< ", depth_size=" << depth_image.cols << "x" << depth_image.rows
<< ", depth_type=" << depth_image.type();
return grpc::Status::OK;
}
catch (exception &e) {
@ -321,8 +398,6 @@ grpc::Status gRPCCameraServiceImpl::StartRecording(grpc::ServerContext* context,
dev->startRecording(request->video_path());
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (StartRecording): success, id=" << dev_id
<< ", path=" << request->video_path();
return grpc::Status::OK;
}
catch (exception &e) {
@ -346,7 +421,50 @@ grpc::Status gRPCCameraServiceImpl::StopRecording(grpc::ServerContext* context,
dev->stopRecording();
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (StopRecording): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (exception &e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
}
grpc::Status gRPCCameraServiceImpl::ControlPtz(grpc::ServerContext* context,
const api::ControlPtzCommand_Request* request, api::ControlPtzCommand_Feedback* response)
{
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (ControlPtz): id=" << dev_id
<< ", command=" << request->command()
<< ", action=" << request->action()
<< ", speed=" << request->speed();
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
if (!dev) {
return failResponse(response, "Camera device not found: " + dev_id);
}
PtzCommand command{};
if (!toPtzCommand(request->command(), command)) {
return failResponse(response, "Invalid PTZ command");
}
if (request->action() != api::ControlPtzCommand_Action_START &&
request->action() != api::ControlPtzCommand_Action_STOP) {
return failResponse(response, "Invalid PTZ action");
}
const bool stop = request->action() == api::ControlPtzCommand_Action_STOP;
if (!dev->controlPtz(command, stop, static_cast<int>(request->speed()))) {
CameraState state{};
dev->getState(state);
const std::string error_message =
state.error_message.empty() ? "Failed to control PTZ: " + dev_id : state.error_message;
return failResponse(response, error_message);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
catch (exception &e) {
@ -363,9 +481,11 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* con
try {
//读取首次传递的数据,获取设备id
api::GetDepthImageStreamCommand_Request request;
stream->Read(&request);
if (!stream->Read(&request)) {
return grpc::Status::OK;
}
string dev_id = request.header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): start,id=" << dev_id;
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetDepthImageStream): start,id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
if (!dev) {
api::GetDepthImageStreamCommand_Feedback response;
@ -375,9 +495,17 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* con
stream->Write(response);
return grpc::Status::OK;
}
CameraStreamingLease stream_lease(dev);
if (!stream_lease) {
api::GetDepthImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message("Failed to start camera stream: " + dev_id);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);
return grpc::Status::OK;
}
int nFrameCount = 0;
dev->startStreaming();
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (GetDepthImageStream): start streaming success, id=" << dev_id;
size_t index = 0;
while (true)
{
@ -389,39 +517,39 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* con
api::GetDepthImageStreamCommand_Feedback response;
cmvr::device::StreamFrameData frame_data;
dev->getEncodedFrame(frame_data,index);
if (!frame_data.rgbFrame.empty()) {
if (dev->waitEncodedFrame(frame_data, index, std::chrono::milliseconds(100)) &&
!frame_data.depthFrame.empty()) {
response.mutable_header()->set_success(true);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
response.mutable_depth_frame()->set_type(api::FrameData::U16C1);
response.mutable_depth_frame()->set_data(frame_data.depthFrame.data(), frame_data.depthFrame.size());
response.mutable_depth_frame()->set_is_key_frame(frame_data.depthKey);
response.mutable_depth_frame()->set_codec(frame_data.codec);
response.mutable_depth_frame()->set_width(frame_data.width);
response.mutable_depth_frame()->set_height(frame_data.height);
response.mutable_depth_frame()->set_codec("none");
response.mutable_depth_frame()->set_width(frame_data.depth_width > 0 ? frame_data.depth_width : frame_data.width);
response.mutable_depth_frame()->set_height(frame_data.depth_height > 0 ? frame_data.depth_height : frame_data.height);
response.mutable_intrinsics()->set_fx(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_fy(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_cx(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_cy(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_fy(frame_data.intrinsics.fy);
response.mutable_intrinsics()->set_cx(frame_data.intrinsics.cx);
response.mutable_intrinsics()->set_cy(frame_data.intrinsics.cy);
for (int i = 0; i < 5 ; i++) {
response.mutable_intrinsics()->add_coeffs(frame_data.intrinsics.coeffs[i]);
}
response.set_seq_no(nFrameCount++);
grpc::WriteOptions options;
options.set_last_message();
if (!stream->Write(response)) {
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (stream->Write) failed,id=" << dev_id;
break;
}
}
}
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): end,id=" << dev_id;
dev->stopStreaming();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetDepthImageStream): end,id=" << dev_id;
return grpc::Status::OK;
}
catch (exception &e) {
catch (const exception &e) {
api::GetDepthImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);
@ -433,9 +561,11 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* con
try {
//读取首次传递的数据,获取设备id
api::GetRGBDImagesStreamCommand_Request request;
stream->Read(&request);
if (!stream->Read(&request)) {
return grpc::Status::OK;
}
string dev_id = request.header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): start,id=" << dev_id;
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBDImagesStream): start,id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
if (!dev) {
api::GetRGBDImagesStreamCommand_Feedback response;
@ -445,9 +575,17 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* con
stream->Write(response);
return grpc::Status::OK;
}
CameraStreamingLease stream_lease(dev);
if (!stream_lease) {
api::GetRGBDImagesStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message("Failed to start camera stream: " + dev_id);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);
return grpc::Status::OK;
}
int nFrameCount = 0;
dev->startStreaming();
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (GetRGBDImagesStream): start streaming success, id=" << dev_id;
size_t index = 0;
while (true)
{
@ -459,33 +597,34 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* con
api::GetRGBDImagesStreamCommand_Feedback response;
cmvr::device::StreamFrameData frame_data;
dev->getEncodedFrame(frame_data,index);
if (!frame_data.rgbFrame.empty()) {
if (dev->waitEncodedFrame(frame_data, index, std::chrono::milliseconds(100)) &&
!frame_data.rgbFrame.empty() &&
!frame_data.depthFrame.empty()) {
response.mutable_header()->set_success(true);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
response.mutable_color_frame()->set_data(frame_data.rgbFrame.data(), frame_data.rgbFrame.size());
response.mutable_color_frame()->set_is_key_frame(frame_data.bKey);
response.mutable_color_frame()->set_codec(frame_data.codec);
response.mutable_color_frame()->set_width(frame_data.width);
response.mutable_color_frame()->set_height(frame_data.height);
response.mutable_depth_frame()->set_type(api::FrameData::U16C1);
response.mutable_depth_frame()->set_data(frame_data.depthFrame.data(), frame_data.depthFrame.size());
response.mutable_depth_frame()->set_is_key_frame(frame_data.depthKey);
response.mutable_depth_frame()->set_codec(frame_data.codec);
response.mutable_depth_frame()->set_width(frame_data.width);
response.mutable_depth_frame()->set_height(frame_data.height);
response.mutable_depth_frame()->set_codec("none");
response.mutable_depth_frame()->set_width(frame_data.depth_width > 0 ? frame_data.depth_width : frame_data.width);
response.mutable_depth_frame()->set_height(frame_data.depth_height > 0 ? frame_data.depth_height : frame_data.height);
response.mutable_intrinsics()->set_fx(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_fy(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_cx(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_cy(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_fy(frame_data.intrinsics.fy);
response.mutable_intrinsics()->set_cx(frame_data.intrinsics.cx);
response.mutable_intrinsics()->set_cy(frame_data.intrinsics.cy);
for (int i = 0; i < 5 ; i++) {
response.mutable_intrinsics()->add_coeffs(frame_data.intrinsics.coeffs[i]);
}
response.set_seq_no(nFrameCount++);
grpc::WriteOptions options;
options.set_last_message();
if (!stream->Write(response)) {
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (stream->Write) failed,id=" << dev_id;
break;
@ -493,11 +632,11 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* con
}
}
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBDImagesStream): end,id=" << dev_id;
dev->stopStreaming();
return grpc::Status::OK;
}
catch (exception &e) {
catch (const exception &e) {
api::GetRGBDImagesStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);
@ -508,9 +647,13 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* conte
try {
//读取首次传递的数据,获取设备id
api::GetRGBImageStreamCommand_Request request;
stream->Read(&request);
if (!stream->Read(&request)) {
return grpc::Status::OK;
}
string dev_id = request.header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): start,id=" << dev_id;
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): start,id=" << dev_id
<< ", max_pending_frames=" << stream_config_.max_pending_frames
<< ", max_frame_age_ms=" << stream_config_.max_frame_age.count();
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
if (!dev) {
api::GetRGBImageStreamCommand_Feedback response;
@ -520,57 +663,259 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* conte
stream->Write(response);
return grpc::Status::OK;
}
int nFrameCount = 0;
dev->startStreaming();
CMVR_LOG(DEBUG) << "[gRPCCameraServiceImpl] (GetRGBImageStream): start streaming success, id=" << dev_id;
size_t last_sent_index = std::numeric_limits<size_t>::max();
auto& media_hub = cmvr::media::globalMediaSourceHub();
const std::string track_id = cmvr::media::cameraColorTrackId(dev_id);
if (!cmvr::media::ensureCameraMediaSource(media_hub, dev)) {
api::GetRGBImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message("Failed to register camera media source: " + dev_id);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);
return grpc::Status::OK;
}
auto subscription = media_hub.subscribe(
track_id,
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
[context] { return context->IsCancelled(); });
if (!subscription) {
api::GetRGBImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message("Failed to subscribe camera media source: " + dev_id);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);
return grpc::Status::OK;
}
std::atomic<bool> client_eof_requested{false};
std::atomic<bool> request_stream_closed{false};
std::atomic<uint64_t> control_requests_read{1};
std::thread request_reader([&] {
api::GetRGBImageStreamCommand_Request control_request;
while (stream->Read(&control_request)) {
++control_requests_read;
if (control_request.eof()) {
client_eof_requested.store(true, std::memory_order_release);
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] client requested RGB stream EOF"
<< ", id=" << dev_id
<< ", peer=" << context->peer()
<< ", control_requests=" << control_requests_read.load();
break;
}
}
request_stream_closed.store(true, std::memory_order_release);
});
struct RequestReaderJoiner {
std::thread& thread;
~RequestReaderJoiner() {
if (thread.joinable()) {
thread.join();
}
}
} request_reader_joiner{request_reader};
const auto join_request_reader = [&] {
if (request_reader.joinable()) {
request_reader.join();
}
};
bool waiting_for_key_frame = true;
const char* exit_reason = "unknown";
auto last_key_frame_request = std::chrono::steady_clock::now();
auto last_latency_log = std::chrono::steady_clock::time_point{};
uint64_t discarded_since_log = 0;
std::chrono::microseconds last_write_duration{0};
media_hub.requestKeyFrame(track_id);
const auto request_key_frame_if_due = [&] {
const auto now = std::chrono::steady_clock::now();
if (now - last_key_frame_request >= std::chrono::milliseconds(500)) {
media_hub.requestKeyFrame(track_id);
last_key_frame_request = now;
}
};
const auto request_key_frame_now = [&] {
waiting_for_key_frame = true;
media_hub.requestKeyFrame(track_id);
last_key_frame_request = std::chrono::steady_clock::now();
};
const auto log_latency_event = [&](
const char* reason,
const uint64_t discarded,
const uint64_t frame_age_ns,
const std::chrono::microseconds write_duration) {
discarded_since_log += discarded;
const auto now = std::chrono::steady_clock::now();
if (last_latency_log != std::chrono::steady_clock::time_point{} &&
now - last_latency_log < std::chrono::seconds(1)) {
return;
}
const double frame_age_ms = static_cast<double>(frame_age_ns) / 1'000'000.0;
const double write_ms = static_cast<double>(write_duration.count()) / 1'000.0;
CMVR_LOG(WARNING) << "[gRPCCameraServiceImpl] low-latency camera stream event"
<< ", id=" << dev_id
<< ", reason=" << reason
<< ", discarded=" << discarded_since_log
<< ", age_ms=" << frame_age_ms
<< ", write_ms=" << write_ms
<< ", max_pending_frames="
<< stream_config_.max_pending_frames
<< ", max_frame_age_ms="
<< stream_config_.max_frame_age.count();
discarded_since_log = 0;
last_latency_log = now;
};
while (true)
{
if (client_eof_requested.load(std::memory_order_acquire)) {
exit_reason = "client_eof";
break;
}
if (context->IsCancelled())
{
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl](GetRGBImageStream) context is cancelled,id=" << dev_id;
exit_reason = "context_cancelled";
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl](GetRGBImageStream) context is cancelled"
<< ", id=" << dev_id
<< ", peer=" << context->peer()
<< ", client_eof=" << client_eof_requested.load()
<< ", request_stream_closed=" << request_stream_closed.load();
break;
}
api::GetRGBImageStreamCommand_Feedback response;
cmvr::device::StreamFrameData frame_data;
size_t next_index = last_sent_index;
if (!dev->getLatestEncodedFrame(frame_data, next_index) ||
frame_data.rgbFrame.empty() ||
next_index == last_sent_index) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
const auto read = subscription.waitRead(std::chrono::milliseconds(100));
if (!read || !read->value || read->value->empty()) {
if (!subscription.valid()) {
exit_reason = "subscription_invalid";
break;
}
if (waiting_for_key_frame) {
request_key_frame_if_due();
}
continue;
}
response.mutable_header()->set_success(true);
response.mutable_color_frame()->set_data(frame_data.rgbFrame.data(), frame_data.rgbFrame.size());
response.mutable_color_frame()->set_is_key_frame(frame_data.bKey);
response.mutable_color_frame()->set_codec(frame_data.codec);
response.mutable_color_frame()->set_width(frame_data.width);
response.mutable_color_frame()->set_height(frame_data.height);
response.mutable_intrinsics()->set_fx(frame_data.intrinsics.fx);
response.mutable_intrinsics()->set_fy(frame_data.intrinsics.fy);
response.mutable_intrinsics()->set_cx(frame_data.intrinsics.cx);
response.mutable_intrinsics()->set_cy(frame_data.intrinsics.cy);
for (int i = 0; i < 5 ; i++) {
response.mutable_intrinsics()->add_coeffs(frame_data.intrinsics.coeffs[i]);
const auto& frame = *read->value;
const auto descriptor = frame.descriptor;
if (!descriptor) {
continue;
}
response.set_seq_no(nFrameCount++);
if (!stream->Write(response)) {
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (stream->Write) failed,id=" << dev_id;
const uint64_t now_ns = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
const auto frame_age = cameraFrameAgeNs(frame.capture_time_ns, now_ns);
if (frame_age &&
cameraFrameExceedsAgeLimit(
frame.capture_time_ns,
now_ns,
stream_config_.max_frame_age)) {
// This frame is already outside the latency budget. Flush all
// currently queued frames and wait for a fresh IDR; sending any
// P/B frame after an intentional gap would break decoder continuity.
const uint64_t discarded =
1 + subscription.discardPendingIfExceeds(0);
request_key_frame_now();
log_latency_event(
"stale_frame",
discarded,
*frame_age,
last_write_duration);
continue;
}
const bool inter_frame_codec = descriptor->codec == cmvr::media::Codec::H264 ||
descriptor->codec == cmvr::media::Codec::H265;
if (!inter_frame_codec || descriptor->payload_format != cmvr::media::PayloadFormat::ANNEX_B) {
api::GetRGBImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message(
"Unsupported camera stream codec or payload format: " + dev_id);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);
exit_reason = "unsupported_stream";
break;
}
last_sent_index = next_index;
if (read->dropped_since_last_read > 0 || read->generation_changed || frame.discontinuity) {
request_key_frame_now();
}
if (waiting_for_key_frame && !frame.key_frame) {
request_key_frame_if_due();
continue;
}
waiting_for_key_frame = false;
api::GetRGBImageStreamCommand_Feedback response;
response.mutable_header()->set_success(true);
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
response.mutable_color_frame()->set_data(frame.data(), frame.size());
response.mutable_color_frame()->set_is_key_frame(frame.key_frame);
response.mutable_color_frame()->set_codec(
descriptor->codec == cmvr::media::Codec::H264 ? "h264" :
descriptor->codec == cmvr::media::Codec::H265 ? "h265" : "unknown");
response.mutable_color_frame()->set_width(static_cast<int32_t>(descriptor->width));
response.mutable_color_frame()->set_height(static_cast<int32_t>(descriptor->height));
response.mutable_color_frame()->set_capture_utc_ns(frame.capture_utc_ns);
response.mutable_color_frame()->set_source_sequence(frame.sequence);
response.mutable_color_frame()->set_pts(frame.pts);
response.mutable_color_frame()->set_dts(frame.dts);
response.mutable_color_frame()->set_source_fps(descriptor->nominal_rate);
response.mutable_color_frame()->set_source_timestamp(frame.source_timestamp);
response.mutable_color_frame()->set_source_frame_number(frame.source_frame_number);
response.mutable_intrinsics()->set_fx(descriptor->fx);
response.mutable_intrinsics()->set_fy(descriptor->fy);
response.mutable_intrinsics()->set_cx(descriptor->cx);
response.mutable_intrinsics()->set_cy(descriptor->cy);
for (const float coefficient : descriptor->distortion) {
response.mutable_intrinsics()->add_coeffs(coefficient);
}
response.set_seq_no(static_cast<int32_t>(std::min<uint64_t>(
frame.sequence,
static_cast<uint64_t>(std::numeric_limits<int32_t>::max()))));
const auto write_started = std::chrono::steady_clock::now();
if (!stream->Write(response)) {
exit_reason = "write_failed";
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (stream->Write) failed"
<< ", id=" << dev_id
<< ", peer=" << context->peer()
<< ", context_cancelled=" << context->IsCancelled()
<< ", client_eof=" << client_eof_requested.load()
<< ", request_stream_closed=" << request_stream_closed.load()
<< ", control_requests=" << control_requests_read.load()
<< ", write_ms="
<< std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - write_started).count() / 1000.0;
break;
}
last_write_duration = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - write_started);
// A successful synchronous Write may have been flow-controlled long
// enough for the source to outpace this consumer. Once the pending
// count crosses the configured trigger, discard the whole pending
// batch and require a fresh key frame before resuming.
const uint64_t discarded = subscription.discardPendingIfExceeds(
stream_config_.max_pending_frames);
if (discarded > 0) {
request_key_frame_now();
log_latency_event(
"write_backpressure",
discarded,
frame_age.value_or(0),
last_write_duration);
}
}
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): end,id=" << dev_id;
dev->stopStreaming();
join_request_reader();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): end"
<< ", id=" << dev_id
<< ", reason=" << exit_reason
<< ", peer=" << context->peer()
<< ", context_cancelled=" << context->IsCancelled()
<< ", client_eof=" << client_eof_requested.load()
<< ", request_stream_closed=" << request_stream_closed.load()
<< ", control_requests=" << control_requests_read.load();
return grpc::Status::OK;
}
catch (exception &e) {
catch (const exception &e) {
api::GetRGBImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
response.mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
stream->Write(response);

View File

@ -1,4 +1,9 @@
#include "common/base/logging/logger.h"
#include "manager/media_source_hub/include/device_media_source_adapter.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <limits>
//
// Created by linbo on 2025/6/13.
// Created by xtkuang on 2025/6/13.
@ -18,6 +23,7 @@ grpc::Status failResponse(ResponseT* response, const std::string& message) {
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
}
gRPCMicroPhoneServiceImpl::gRPCMicroPhoneServiceImpl(): dmgr_(DeviceManager::getInstance()) {}
@ -151,6 +157,111 @@ grpc::Status gRPCMicroPhoneServiceImpl::ResumeRecord(grpc::ServerContext* contex
}
}
grpc::Status gRPCMicroPhoneServiceImpl::StreamAudio(grpc::ServerContext* context,
const api::StreamMicAudioCommand_Request* request,
grpc::ServerWriter<api::StreamMicAudioCommand_Feedback>* writer) {
try {
const string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (StreamAudio): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
if (!dev) {
api::StreamMicAudioCommand_Feedback feedback;
feedback.mutable_header()->set_success(false);
feedback.mutable_header()->set_error_message("Microphone device not found: " + dev_id);
setCurrentTimestamp(feedback.mutable_header()->mutable_timestamp());
writer->Write(feedback);
return grpc::Status::OK;
}
auto& media_hub = cmvr::media::globalMediaSourceHub();
const std::string track_id = cmvr::media::microphoneTrackId(dev_id);
if (!cmvr::media::ensureMicrophoneMediaSource(media_hub, dev)) {
api::StreamMicAudioCommand_Feedback feedback;
feedback.mutable_header()->set_success(false);
feedback.mutable_header()->set_error_message("Failed to register microphone media source: " + dev_id);
setCurrentTimestamp(feedback.mutable_header()->mutable_timestamp());
writer->Write(feedback);
return grpc::Status::OK;
}
auto subscription = media_hub.subscribe(
track_id,
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
[context] { return context->IsCancelled(); });
if (!subscription) {
api::StreamMicAudioCommand_Feedback feedback;
feedback.mutable_header()->set_success(false);
feedback.mutable_header()->set_error_message("Failed to subscribe microphone media source: " + dev_id);
setCurrentTimestamp(feedback.mutable_header()->mutable_timestamp());
writer->Write(feedback);
return grpc::Status::OK;
}
while (!context->IsCancelled()) {
const auto read = subscription.waitRead(std::chrono::milliseconds(100));
if (!read || !read->value || read->value->empty()) {
if (!subscription.valid()) {
break;
}
continue;
}
const auto& frame = *read->value;
const auto descriptor = frame.descriptor;
if (!descriptor) {
continue;
}
api::StreamMicAudioCommand_Feedback feedback;
feedback.mutable_header()->set_success(true);
setCurrentTimestamp(feedback.mutable_header()->mutable_timestamp());
auto* audio = feedback.mutable_audio();
audio->set_data(frame.data(), frame.size());
audio->set_sample_rate(static_cast<int32_t>(std::min<uint32_t>(
descriptor->sample_rate,
static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))));
audio->set_channels(static_cast<int32_t>(std::min<uint32_t>(
descriptor->channels,
static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))));
if (descriptor->codec == cmvr::media::Codec::PCM_S16LE) {
audio->set_format(cmvr::api::AudioData_AudioFormat_PCM);
audio->set_codec("pcm_s16le");
} else if (descriptor->codec == cmvr::media::Codec::OPUS) {
audio->set_format(cmvr::api::AudioData_AudioFormat_OPUS);
audio->set_codec("opus");
} else if (descriptor->codec == cmvr::media::Codec::AAC) {
audio->set_format(cmvr::api::AudioData_AudioFormat_AAC);
audio->set_codec("aac");
} else {
feedback.mutable_header()->set_success(false);
feedback.mutable_header()->set_error_message(
"Unsupported microphone stream codec: " + dev_id);
feedback.clear_audio();
writer->Write(feedback);
break;
}
audio->set_pts(frame.pts);
const int64_t sample_count = frame.duration > 0
? frame.duration
: static_cast<int64_t>(descriptor->nominal_rate);
audio->set_nb_samples(static_cast<int32_t>(std::clamp<int64_t>(
sample_count,
0,
std::numeric_limits<int32_t>::max())));
if (!writer->Write(feedback)) {
break;
}
}
return grpc::Status::OK;
} catch (const std::exception& error) {
api::StreamMicAudioCommand_Feedback feedback;
feedback.mutable_header()->set_success(false);
feedback.mutable_header()->set_error_message(error.what());
setCurrentTimestamp(feedback.mutable_header()->mutable_timestamp());
writer->Write(feedback);
return grpc::Status::OK;
}
}
grpc::Status gRPCMicroPhoneServiceImpl::SetVolume(grpc::ServerContext* context,
const api::SetMicPhoneVolumeCommand_Request* request, api::SetMicPhoneVolumeCommand_Feedback* response) {
try {

View File

@ -1,4 +1,5 @@
#include "common/base/logging/logger.h"
#include <memory>
//
// Created by xtkuang on 2025/6/10.
//
@ -18,6 +19,33 @@ grpc::Status failResponse(ResponseT* response, const std::string& message) {
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
AudioStreamFormat fromProtoAudioFormat(cmvr::api::AudioData_AudioFormat format) {
switch (format) {
case cmvr::api::AudioData_AudioFormat_PCM:
return AudioStreamFormat::PCM;
case cmvr::api::AudioData_AudioFormat_MP3:
return AudioStreamFormat::MP3;
case cmvr::api::AudioData_AudioFormat_AAC:
return AudioStreamFormat::AAC;
case cmvr::api::AudioData_AudioFormat_WAV:
return AudioStreamFormat::WAV;
default:
return AudioStreamFormat::UNKNOWN;
}
}
AudioStreamFrameData fromProtoAudioData(const cmvr::api::AudioData& audio) {
AudioStreamFrameData frame;
frame.data.assign(audio.data().begin(), audio.data().end());
frame.sample_rate = audio.sample_rate() > 0 ? audio.sample_rate() : 44100;
frame.channels = audio.channels() > 0 ? audio.channels() : 2;
frame.format = fromProtoAudioFormat(audio.format());
frame.codec = audio.codec();
frame.pts = audio.pts();
frame.nb_samples = audio.nb_samples();
return frame;
}
}
gRPCSpeakerServiceImpl::gRPCSpeakerServiceImpl(): dmgr_(DeviceManager::getInstance()) {}
@ -81,6 +109,48 @@ grpc::Status gRPCSpeakerServiceImpl::PlayAudio(grpc::ServerContext* context,
}
}
grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
grpc::ServerReader<api::StreamSpeakerAudioCommand_Request>* reader,
api::StreamSpeakerAudioCommand_Feedback* response) {
try {
api::StreamSpeakerAudioCommand_Request request;
std::shared_ptr<AbstractSpeaker> dev;
std::string dev_id;
while (reader->Read(&request)) {
if (!dev) {
dev_id = request.header().device_id();
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (StreamAudio): id=" << dev_id;
dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
if (!dev) {
return failResponse(response, "Speaker device not found: " + dev_id);
}
if (!dev->start()) {
return failResponse(response, "Failed to start speaker: " + dev_id);
}
}
if (!dev->pushAudioFrame(fromProtoAudioData(request.audio()))) {
dev->stopStreaming();
return failResponse(response, "Failed to push speaker audio frame: " + dev_id);
}
}
if (dev) {
dev->stopStreaming();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
}
grpc::Status gRPCSpeakerServiceImpl::StopPlayback(grpc::ServerContext* context,
const api::StopSpeakerCommand_Request* request, api::StopSpeakerCommand_Feedback* response) {
try {

View File

@ -0,0 +1,54 @@
#include "service/grpc/include/grpc_camera_stream_policy.h"
#include <chrono>
#include <cstdint>
#include <iostream>
namespace {
bool check(const bool condition, const char* expression, const int line) {
if (condition) {
return true;
}
std::cerr << "CHECK failed at line " << line << ": " << expression << '\n';
return false;
}
#define CHECK_TRUE(expression) \
do { \
if (!check(static_cast<bool>(expression), #expression, __LINE__)) { \
return 1; \
} \
} while (false)
} // namespace
int main() {
using namespace std::chrono_literals;
using cmvr::service::cameraFrameAgeNs;
using cmvr::service::cameraFrameExceedsAgeLimit;
using cmvr::service::makeCameraStreamLowLatencyConfig;
const auto defaults = makeCameraStreamLowLatencyConfig(0, 0);
CHECK_TRUE(defaults.max_pending_frames == 2);
CHECK_TRUE(defaults.max_frame_age == 250ms);
const auto configured = makeCameraStreamLowLatencyConfig(7, 900);
CHECK_TRUE(configured.max_pending_frames == 7);
CHECK_TRUE(configured.max_frame_age == 900ms);
CHECK_TRUE(!cameraFrameAgeNs(0, 1'000'000'000ULL).has_value());
CHECK_TRUE(!cameraFrameAgeNs(2'000'000'000ULL, 1'000'000'000ULL).has_value());
CHECK_TRUE(cameraFrameAgeNs(1'000'000'000ULL, 1'250'000'000ULL).value() ==
250'000'000ULL);
CHECK_TRUE(!cameraFrameExceedsAgeLimit(
1'000'000'000ULL, 1'250'000'000ULL, 250ms));
CHECK_TRUE(cameraFrameExceedsAgeLimit(
1'000'000'000ULL, 1'250'000'001ULL, 250ms));
CHECK_TRUE(!cameraFrameExceedsAgeLimit(
1'000'000'000ULL, 2'000'000'000ULL, 0ms));
std::cout << "grpc_camera_stream_policy_test: PASS\n";
return 0;
}

View File

@ -0,0 +1,49 @@
find_package(Threads REQUIRED)
add_library(quic_edge_service STATIC
src/control_framing.cpp
src/datagram_packetizer.cpp
src/quic_edge_types.cpp
src/quic_transport.cpp
src/msquic_transport.cpp
src/quic_edge_service.cpp
src/quic_edge_device_adapter.cpp
)
target_compile_features(quic_edge_service PUBLIC cxx_std_17)
target_include_directories(quic_edge_service PUBLIC ${PROJECT_SOURCE_DIR}/cmvr-es)
target_link_libraries(quic_edge_service
PUBLIC
cmvr_es::proto
cmvr_es::media_source_hub
PRIVATE
cmvr_es::device_media_source_adapter
cmvr_es::device_manager
Threads::Threads
msquic
)
add_library(cmvr_es::quic_edge_service ALIAS quic_edge_service)
if(BUILD_TESTING)
add_executable(quic_edge_protocol_test tests/quic_edge_protocol_test.cpp)
target_compile_features(quic_edge_protocol_test PRIVATE cxx_std_17)
target_link_libraries(quic_edge_protocol_test PRIVATE
cmvr_es::quic_edge_service
cmvr_es::media_source_hub
Threads::Threads
)
add_test(NAME quic_edge_protocol_test COMMAND quic_edge_protocol_test)
set(_quic_edge_test_environment
"LD_LIBRARY_PATH=${CMVR_TEST_EXTERNAL_LIBRARY_PATH}")
if(CMVR_TEST_SYSTEM_LIBSTDCXX)
list(APPEND _quic_edge_test_environment
"LD_PRELOAD=${CMVR_TEST_SYSTEM_LIBSTDCXX}")
endif()
set_tests_properties(quic_edge_protocol_test PROPERTIES
TIMEOUT 15
ENVIRONMENT "${_quic_edge_test_environment}")
endif()
install(TARGETS quic_edge_service
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib)

View File

@ -0,0 +1,54 @@
#ifndef CMVR_ES_QUIC_EDGE_CONTROL_FRAMING_H
#define CMVR_ES_QUIC_EDGE_CONTROL_FRAMING_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace cmvr::quic_edge {
class ControlFrameEncoder {
public:
static bool encode(const std::uint8_t* payload,
std::size_t payload_size,
std::size_t maximum_payload_size,
std::vector<std::uint8_t>* framed,
std::string* error);
static bool encode(const std::vector<std::uint8_t>& payload,
std::size_t maximum_payload_size,
std::vector<std::uint8_t>* framed,
std::string* error)
{
return encode(payload.data(), payload.size(), maximum_payload_size, framed, error);
}
};
class ControlFrameDecoder {
public:
explicit ControlFrameDecoder(std::size_t maximum_payload_size);
bool push(const std::uint8_t* data,
std::size_t size,
std::vector<std::vector<std::uint8_t>>* decoded_frames,
std::string* error);
bool push(const std::vector<std::uint8_t>& data,
std::vector<std::vector<std::uint8_t>>* decoded_frames,
std::string* error)
{
return push(data.data(), data.size(), decoded_frames, error);
}
void reset();
std::size_t bufferedBytes() const { return buffer_.size(); }
private:
std::size_t maximum_payload_size_;
std::vector<std::uint8_t> buffer_;
};
} // namespace cmvr::quic_edge
#endif // CMVR_ES_QUIC_EDGE_CONTROL_FRAMING_H

View File

@ -0,0 +1,47 @@
#ifndef CMVR_ES_QUIC_EDGE_DATAGRAM_PACKETIZER_H
#define CMVR_ES_QUIC_EDGE_DATAGRAM_PACKETIZER_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "service/quic_edge/include/quic_edge_types.h"
namespace cmvr::quic_edge {
class DatagramPacketizer {
public:
explicit DatagramPacketizer(std::uint64_t initial_packet_sequence = 0);
bool packetize(const media::MediaFrame& frame,
std::uint32_t wire_track_id,
std::uint32_t codec_generation_token,
std::uint64_t session_epoch,
std::uint64_t wire_frame_sequence,
bool force_discontinuity,
std::size_t maximum_datagram_bytes,
std::vector<DatagramPacket>* packets,
std::string* error);
static bool decodeHeader(const std::uint8_t* data,
std::size_t size,
DatagramHeader* header,
std::string* error);
static bool decodeHeader(const std::vector<std::uint8_t>& packet,
DatagramHeader* header,
std::string* error)
{
return decodeHeader(packet.data(), packet.size(), header, error);
}
std::uint64_t nextPacketSequence() const { return next_packet_sequence_; }
private:
std::uint64_t next_packet_sequence_;
};
} // namespace cmvr::quic_edge
#endif // CMVR_ES_QUIC_EDGE_DATAGRAM_PACKETIZER_H

View File

@ -0,0 +1,205 @@
#ifndef CMVR_ES_QUIC_EDGE_SERVICE_H
#define CMVR_ES_QUIC_EDGE_SERVICE_H
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "cmvr/config/quic_edge_config/quic_edge_config.pb.h"
#include "devices/device_types.h"
#include "manager/media_source_hub/include/media_source_hub.h"
#include "service/quic_edge/include/control_framing.h"
#include "service/quic_edge/include/datagram_packetizer.h"
#include "service/quic_edge/include/quic_transport.h"
namespace cmvr::quic_edge {
enum class QuicEdgeServiceState {
UNINITIALIZED,
STOPPED,
CONNECTING,
REGISTERING,
ONLINE,
BACKOFF,
FAILED,
};
const char* toString(QuicEdgeServiceState state);
struct QuicEdgeStats {
std::uint64_t connection_attempts{0};
std::uint64_t successful_connections{0};
std::uint64_t reconnects{0};
std::uint64_t registrations_sent{0};
std::uint64_t registrations_accepted{0};
std::uint64_t registrations_rejected{0};
std::uint64_t heartbeats_sent{0};
std::uint64_t heartbeats_acknowledged{0};
std::uint64_t heartbeat_timeouts{0};
std::uint64_t protocol_errors{0};
std::uint64_t media_sessions_opened{0};
std::uint64_t frames_queued{0};
std::uint64_t datagrams_queued{0};
std::uint64_t frames_dropped_source{0};
std::uint64_t frames_dropped_backpressure{0};
std::uint64_t frames_dropped_oversize{0};
std::uint64_t frames_dropped_no_datagram{0};
std::uint64_t frames_skipped_waiting_keyframe{0};
std::uint64_t source_errors{0};
};
struct QuicEdgeStatus {
bool registered{false};
std::string node_id;
std::string boot_id;
std::string session_id;
std::string observed_source_ip;
std::string last_media_error;
std::uint64_t heartbeat_sequence{0};
std::uint64_t last_heartbeat_ack_unix_ms{0};
std::size_t active_media_tracks{0};
};
class QuicEdgeService {
public:
explicit QuicEdgeService(config::QuicEdgeConfig config);
QuicEdgeService(config::QuicEdgeConfig config,
std::unique_ptr<QuicTransport> transport,
media::MediaSourceHub& media_hub);
~QuicEdgeService();
QuicEdgeService(const QuicEdgeService&) = delete;
QuicEdgeService& operator=(const QuicEdgeService&) = delete;
static bool validateConfig(const config::QuicEdgeConfig& config,
std::string* error);
bool initialize(std::string* error);
bool start(std::string* error);
// Releases the current media subscriptions without stopping the QUIC
// transport, node registration, heartbeat loop, or service worker. The
// media worker automatically subscribes again when admission permits it.
bool interruptMediaActivities();
void stop();
QuicEdgeServiceState state() const;
std::string lastError() const;
QuicEdgeStats stats() const;
QuicEdgeStatus status() const;
private:
using SourceRegistrar = std::function<bool(
const config::QuicEdgeTrackConfig&, const std::string&, std::string*)>;
struct ActiveTrack {
config::QuicEdgeTrackConfig config;
std::string source_track_id;
media::MediaSourceHub::Subscription subscription;
std::optional<MediaTrackDescription> last_description;
bool waiting_for_keyframe{false};
bool keyframe_requested{false};
bool next_frame_discontinuous{false};
};
void run();
void runMedia();
bool startMediaWorker(std::string* error);
void stopMediaWorker();
void recordMediaConnectionFailure(const std::string& error);
bool performRegistration(ControlFrameDecoder* decoder, std::string* error);
bool sendNodeRegistration(std::string* error);
bool sendHeartbeat(std::uint64_t sequence, std::string* error);
bool receiveAndDispatchControl(ControlFrameDecoder* decoder,
std::chrono::milliseconds timeout,
bool* received,
std::string* error);
bool dispatchControlFrame(const std::vector<std::uint8_t>& frame,
std::string* error);
bool openMediaSession(std::vector<ActiveTrack>* tracks,
std::uint64_t activity_generation,
std::string* error);
void refreshMediaTracks(std::vector<ActiveTrack>* tracks,
std::uint64_t activity_generation);
bool hasEnabledMediaTracks() const;
bool ensureSourceRegistered(const config::QuicEdgeTrackConfig& track,
const std::string& source_track_id,
std::string* error);
bool sendSessionOpen(std::string* error);
bool sendTrackDescription(const MediaTrackDescription& description,
std::string* error);
bool sendControlEnvelope(const std::string& serialized,
std::string* error);
bool processTrack(ActiveTrack* track, bool* sent_anything,
std::string* error);
std::uint64_t shedBufferedFrames(ActiveTrack* track);
void requestKeyFrame(ActiveTrack* track);
std::uint32_t maximumFrameBytes(const ActiveTrack& track) const;
void initializeIdentity();
void resetConnectionStatus();
void recordMediaError(const std::string& error);
void setState(QuicEdgeServiceState state, const std::string& error = {});
bool waitForStop(std::chrono::milliseconds duration);
std::chrono::milliseconds nextBackoff(std::chrono::milliseconds current);
std::chrono::milliseconds jittered(std::chrono::milliseconds base);
std::uint64_t nextSessionEpoch();
config::QuicEdgeConfig config_;
std::unique_ptr<QuicTransport> transport_;
media::MediaSourceHub* media_hub_{nullptr};
bool using_global_media_hub_{false};
SourceRegistrar source_registrar_;
std::mutex lifecycle_mutex_;
std::mutex control_send_mutex_;
mutable std::mutex mutex_;
std::condition_variable stop_cv_;
std::condition_variable media_stop_cv_;
std::condition_variable media_activity_cv_;
QuicEdgeServiceState state_{QuicEdgeServiceState::UNINITIALIZED};
std::string last_error_;
std::string last_media_error_;
QuicEdgeStats stats_;
bool registered_{false};
std::string session_id_;
std::string observed_source_ip_;
std::uint64_t heartbeat_sequence_{0};
std::uint64_t outstanding_heartbeat_sequence_{0};
std::uint64_t last_heartbeat_ack_unix_ms_{0};
std::size_t active_media_tracks_{0};
bool stop_requested_{false};
bool media_stop_requested_{false};
bool media_worker_running_{false};
std::uint64_t media_activity_generation_{1U};
std::uint64_t media_activity_quiesced_generation_{1U};
bool media_connection_failed_{false};
std::string media_connection_error_;
std::thread worker_;
std::thread media_worker_;
std::string node_id_;
std::string boot_id_;
std::string software_version_;
DatagramPacketizer packetizer_;
std::uint64_t session_epoch_{0};
std::unordered_map<std::uint32_t, std::uint64_t> next_wire_frame_sequence_;
std::uint64_t control_message_sequence_{0};
std::uint64_t inbound_message_sequence_{0};
bool has_inbound_message_sequence_{false};
std::uint32_t effective_heartbeat_interval_ms_{0};
std::chrono::steady_clock::time_point heartbeat_deadline_{};
std::chrono::steady_clock::time_point next_heartbeat_{};
std::chrono::steady_clock::time_point next_media_source_retry_{};
bool media_session_announced_{false};
};
} // namespace cmvr::quic_edge
#endif // CMVR_ES_QUIC_EDGE_SERVICE_H

View File

@ -0,0 +1,118 @@
#ifndef CMVR_ES_QUIC_EDGE_TYPES_H
#define CMVR_ES_QUIC_EDGE_TYPES_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "common/media/media_frame.h"
namespace cmvr::quic_edge {
constexpr std::uint32_t kDatagramMagic = 0x434d5144U; // "CMQD"
constexpr std::uint8_t kProtocolVersion = 1U;
constexpr std::size_t kDatagramHeaderBytes = 64U;
enum DatagramFlag : std::uint16_t {
DATAGRAM_FLAG_NONE = 0,
DATAGRAM_FLAG_KEY_FRAME = 1U << 0U,
DATAGRAM_FLAG_RESERVED_1 = 1U << 1U,
DATAGRAM_FLAG_DISCONTINUITY = 1U << 2U,
};
// All integer fields are serialized in network byte order. codec_generation
// is a compact token for the full 64-bit MediaSourceHub descriptor generation
// announced on the reliable control stream.
struct DatagramHeader {
std::uint8_t protocol_version{kProtocolVersion};
media::MediaKind kind{media::MediaKind::UNKNOWN};
std::uint16_t flags{DATAGRAM_FLAG_NONE};
std::uint16_t fragment_index{0};
std::uint16_t fragment_count{0};
std::uint16_t payload_size{0};
std::uint32_t track_id{0};
std::uint32_t codec_generation{0};
std::uint64_t session_epoch{0};
std::uint64_t packet_sequence{0};
std::uint64_t frame_sequence{0};
std::uint64_t capture_timestamp_us{0};
std::uint32_t frame_size{0};
std::uint32_t fragment_offset{0};
};
struct DatagramPacket {
DatagramHeader header;
std::vector<std::uint8_t> bytes;
};
struct MediaTrackDescription {
std::uint32_t track_id{0};
std::uint32_t codec_generation_token{0};
std::string source_track_id;
std::string source_id;
media::MediaKind kind{media::MediaKind::UNKNOWN};
media::Codec codec{media::Codec::UNKNOWN};
media::PayloadFormat payload_format{media::PayloadFormat::UNKNOWN};
std::uint64_t codec_generation{0};
std::uint32_t width{0};
std::uint32_t height{0};
std::uint32_t nominal_rate{0};
std::uint32_t sample_rate{0};
std::uint32_t channels{0};
std::vector<std::uint8_t> codec_config;
bool operator==(const MediaTrackDescription& other) const
{
return track_id == other.track_id &&
codec_generation_token == other.codec_generation_token &&
source_track_id == other.source_track_id &&
source_id == other.source_id &&
kind == other.kind &&
codec == other.codec &&
payload_format == other.payload_format &&
codec_generation == other.codec_generation &&
width == other.width &&
height == other.height &&
nominal_rate == other.nominal_rate &&
sample_rate == other.sample_rate &&
channels == other.channels &&
codec_config == other.codec_config;
}
bool operator!=(const MediaTrackDescription& other) const
{
return !(*this == other);
}
};
std::uint32_t descriptorGenerationToken(std::uint64_t generation);
const char* codecName(media::Codec codec);
const char* payloadFormatName(media::PayloadFormat format);
inline MediaTrackDescription describeTrack(
const std::uint32_t wire_track_id,
const media::TrackDescriptor& descriptor)
{
MediaTrackDescription description;
description.track_id = wire_track_id;
description.codec_generation_token =
descriptorGenerationToken(descriptor.generation);
description.source_track_id = descriptor.id;
description.source_id = descriptor.source_id;
description.kind = descriptor.kind;
description.codec = descriptor.codec;
description.payload_format = descriptor.payload_format;
description.codec_generation = descriptor.generation;
description.width = descriptor.width;
description.height = descriptor.height;
description.nominal_rate = descriptor.nominal_rate;
description.sample_rate = descriptor.sample_rate;
description.channels = descriptor.channels;
description.codec_config = descriptor.codec_config;
return description;
}
} // namespace cmvr::quic_edge
#endif // CMVR_ES_QUIC_EDGE_TYPES_H

View File

@ -0,0 +1,73 @@
#ifndef CMVR_ES_QUIC_EDGE_QUIC_TRANSPORT_H
#define CMVR_ES_QUIC_EDGE_QUIC_TRANSPORT_H
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "cmvr/config/quic_edge_config/quic_edge_config.pb.h"
#include "service/quic_edge/include/quic_edge_types.h"
namespace cmvr::quic_edge {
enum class TransportSendResult {
QUEUED,
WOULD_BLOCK,
DISCONNECTED,
ERROR,
};
enum class TransportReceiveResult {
DATA,
TIMEOUT,
DISCONNECTED,
ERROR,
};
class QuicTransport {
public:
virtual ~QuicTransport() = default;
virtual bool connect(const config::QuicEdgeConfig& config,
std::chrono::milliseconds timeout,
std::string* error) = 0;
virtual void disconnect() = 0;
virtual bool isConnected() const = 0;
virtual std::size_t maximumDatagramBytes() const = 0;
virtual std::size_t maximumDatagramBatchPackets() const = 0;
// QUEUED means ownership was accepted. WOULD_BLOCK and synchronous ERROR
// mean no bytes were accepted, so the caller may safely retry the complete
// framed message. DISCONNECTED means the connection is no longer usable.
virtual TransportSendResult sendControl(
std::vector<std::uint8_t> framed_message,
std::string* error) = 0;
// Returns one raw byte chunk from the reliable control stream. QUIC stream
// callback boundaries are not message boundaries; callers must feed all
// DATA chunks into their framing decoder in order.
//
// The default implementation reports DISCONNECTED so existing transports
// that only support sending remain source-compatible.
virtual TransportReceiveResult receiveControl(
std::vector<std::uint8_t>* chunk,
std::chrono::milliseconds timeout,
std::string* error);
// Local admission is atomic for the complete batch. A native QUIC API may
// still fail after accepting earlier fragments; in that case the transport
// must reset the connection so the receiver discards the partial session.
virtual TransportSendResult sendDatagramBatch(
std::vector<DatagramPacket> packets,
std::string* error) = 0;
};
std::unique_ptr<QuicTransport> createDefaultQuicTransport(
std::size_t send_queue_depth);
} // namespace cmvr::quic_edge
#endif // CMVR_ES_QUIC_EDGE_QUIC_TRANSPORT_H

View File

@ -0,0 +1,115 @@
#include "service/quic_edge/include/control_framing.h"
#include <algorithm>
#include <limits>
namespace cmvr::quic_edge {
namespace {
void setError(std::string* error, const std::string& message)
{
if (error) {
*error = message;
}
}
std::uint32_t readLength(const std::vector<std::uint8_t>& buffer)
{
return (static_cast<std::uint32_t>(buffer[0]) << 24U) |
(static_cast<std::uint32_t>(buffer[1]) << 16U) |
(static_cast<std::uint32_t>(buffer[2]) << 8U) |
static_cast<std::uint32_t>(buffer[3]);
}
} // namespace
bool ControlFrameEncoder::encode(const std::uint8_t* payload,
const std::size_t payload_size,
const std::size_t maximum_payload_size,
std::vector<std::uint8_t>* framed,
std::string* error)
{
if (!framed) {
setError(error, "control-frame output is null");
return false;
}
framed->clear();
if (!payload || payload_size == 0U) {
setError(error, "control-frame payload is empty");
return false;
}
if (payload_size > maximum_payload_size ||
payload_size > std::numeric_limits<std::uint32_t>::max()) {
setError(error, "control-frame payload exceeds configured limit");
return false;
}
const auto length = static_cast<std::uint32_t>(payload_size);
framed->resize(4U + payload_size);
(*framed)[0] = static_cast<std::uint8_t>(length >> 24U);
(*framed)[1] = static_cast<std::uint8_t>(length >> 16U);
(*framed)[2] = static_cast<std::uint8_t>(length >> 8U);
(*framed)[3] = static_cast<std::uint8_t>(length);
std::copy_n(payload, payload_size, framed->data() + 4U);
return true;
}
ControlFrameDecoder::ControlFrameDecoder(const std::size_t maximum_payload_size)
: maximum_payload_size_(maximum_payload_size)
{
buffer_.reserve(std::min<std::size_t>(maximum_payload_size + 4U, 4096U));
}
bool ControlFrameDecoder::push(const std::uint8_t* data,
const std::size_t size,
std::vector<std::vector<std::uint8_t>>* decoded_frames,
std::string* error)
{
if (!decoded_frames) {
setError(error, "decoded control-frame output is null");
return false;
}
if (size != 0U && !data) {
setError(error, "control-frame input is null");
return false;
}
std::size_t consumed = 0U;
while (consumed < size) {
if (buffer_.size() < 4U) {
const std::size_t prefix_bytes =
std::min<std::size_t>(4U - buffer_.size(), size - consumed);
buffer_.insert(buffer_.end(), data + consumed,
data + consumed + prefix_bytes);
consumed += prefix_bytes;
if (buffer_.size() < 4U) {
break;
}
const std::uint32_t declared = readLength(buffer_);
if (declared == 0U || declared > maximum_payload_size_) {
reset();
setError(error, "control-frame declared length is invalid");
return false;
}
}
const std::size_t total_size = 4U + readLength(buffer_);
const std::size_t bytes_needed = total_size - buffer_.size();
const std::size_t copied =
std::min<std::size_t>(bytes_needed, size - consumed);
buffer_.insert(buffer_.end(), data + consumed, data + consumed + copied);
consumed += copied;
if (buffer_.size() == total_size) {
decoded_frames->emplace_back(buffer_.begin() + 4, buffer_.end());
buffer_.clear();
}
}
return true;
}
void ControlFrameDecoder::reset()
{
buffer_.clear();
}
} // namespace cmvr::quic_edge

View File

@ -0,0 +1,270 @@
#include "service/quic_edge/include/datagram_packetizer.h"
#include <algorithm>
#include <limits>
namespace cmvr::quic_edge {
namespace {
void setError(std::string* error, const std::string& message)
{
if (error) {
*error = message;
}
}
void writeU16(std::vector<std::uint8_t>* out, const std::size_t offset,
const std::uint16_t value)
{
(*out)[offset] = static_cast<std::uint8_t>(value >> 8U);
(*out)[offset + 1U] = static_cast<std::uint8_t>(value);
}
void writeU32(std::vector<std::uint8_t>* out, const std::size_t offset,
const std::uint32_t value)
{
(*out)[offset] = static_cast<std::uint8_t>(value >> 24U);
(*out)[offset + 1U] = static_cast<std::uint8_t>(value >> 16U);
(*out)[offset + 2U] = static_cast<std::uint8_t>(value >> 8U);
(*out)[offset + 3U] = static_cast<std::uint8_t>(value);
}
void writeU64(std::vector<std::uint8_t>* out, const std::size_t offset,
const std::uint64_t value)
{
for (std::size_t i = 0; i < 8U; ++i) {
(*out)[offset + i] = static_cast<std::uint8_t>(value >> (56U - i * 8U));
}
}
std::uint16_t readU16(const std::uint8_t* data, const std::size_t offset)
{
return static_cast<std::uint16_t>(
(static_cast<std::uint16_t>(data[offset]) << 8U) |
static_cast<std::uint16_t>(data[offset + 1U]));
}
std::uint32_t readU32(const std::uint8_t* data, const std::size_t offset)
{
return (static_cast<std::uint32_t>(data[offset]) << 24U) |
(static_cast<std::uint32_t>(data[offset + 1U]) << 16U) |
(static_cast<std::uint32_t>(data[offset + 2U]) << 8U) |
static_cast<std::uint32_t>(data[offset + 3U]);
}
std::uint64_t readU64(const std::uint8_t* data, const std::size_t offset)
{
std::uint64_t value = 0;
for (std::size_t i = 0; i < 8U; ++i) {
value = (value << 8U) | static_cast<std::uint64_t>(data[offset + i]);
}
return value;
}
bool validKind(const media::MediaKind kind)
{
return kind == media::MediaKind::VIDEO || kind == media::MediaKind::AUDIO;
}
} // namespace
DatagramPacketizer::DatagramPacketizer(const std::uint64_t initial_packet_sequence)
: next_packet_sequence_(initial_packet_sequence)
{
}
bool DatagramPacketizer::packetize(const media::MediaFrame& frame,
const std::uint32_t wire_track_id,
const std::uint32_t codec_generation_token,
const std::uint64_t session_epoch,
const std::uint64_t wire_frame_sequence,
const bool force_discontinuity,
const std::size_t maximum_datagram_bytes,
std::vector<DatagramPacket>* packets,
std::string* error)
{
if (!packets) {
setError(error, "packet output is null");
return false;
}
packets->clear();
if (wire_track_id == 0U) {
setError(error, "track_id must be non-zero");
return false;
}
if (!frame.descriptor || !validKind(frame.descriptor->kind)) {
setError(error, "unsupported media kind");
return false;
}
if (codec_generation_token == 0U) {
setError(error, "codec generation token must be non-zero");
return false;
}
if (session_epoch == 0U) {
setError(error, "session_epoch must be non-zero");
return false;
}
if (wire_frame_sequence == 0U) {
setError(error, "wire_frame_sequence must be non-zero");
return false;
}
if (frame.empty()) {
setError(error, "media frame payload is empty");
return false;
}
if (frame.size() > std::numeric_limits<std::uint32_t>::max()) {
setError(error, "media frame is larger than the v1 frame_size field");
return false;
}
if (maximum_datagram_bytes <= kDatagramHeaderBytes) {
setError(error, "maximum DATAGRAM size does not leave room for payload");
return false;
}
const std::size_t payload_capacity = std::min<std::size_t>(
maximum_datagram_bytes - kDatagramHeaderBytes,
std::numeric_limits<std::uint16_t>::max());
const std::size_t fragment_count =
(frame.size() + payload_capacity - 1U) / payload_capacity;
if (fragment_count == 0U ||
fragment_count > std::numeric_limits<std::uint16_t>::max()) {
setError(error, "media frame requires too many DATAGRAM fragments");
return false;
}
if (next_packet_sequence_ >
std::numeric_limits<std::uint64_t>::max() - fragment_count) {
setError(error, "DATAGRAM packet sequence exhausted");
return false;
}
std::uint16_t flags = DATAGRAM_FLAG_NONE;
if (frame.key_frame) {
flags |= DATAGRAM_FLAG_KEY_FRAME;
}
if (frame.discontinuity || force_discontinuity) {
flags |= DATAGRAM_FLAG_DISCONTINUITY;
}
std::vector<DatagramPacket> encoded_packets;
encoded_packets.reserve(fragment_count);
for (std::size_t i = 0; i < fragment_count; ++i) {
const std::size_t offset = i * payload_capacity;
const std::size_t payload_size =
std::min(payload_capacity, frame.size() - offset);
DatagramPacket packet;
packet.header.protocol_version = kProtocolVersion;
packet.header.kind = frame.descriptor->kind;
packet.header.flags = flags;
packet.header.fragment_index = static_cast<std::uint16_t>(i);
packet.header.fragment_count = static_cast<std::uint16_t>(fragment_count);
packet.header.payload_size = static_cast<std::uint16_t>(payload_size);
packet.header.track_id = wire_track_id;
packet.header.codec_generation = codec_generation_token;
packet.header.session_epoch = session_epoch;
packet.header.packet_sequence = next_packet_sequence_ + i;
packet.header.frame_sequence = wire_frame_sequence;
packet.header.capture_timestamp_us = frame.capture_time_ns / 1000U;
packet.header.frame_size = static_cast<std::uint32_t>(frame.size());
packet.header.fragment_offset = static_cast<std::uint32_t>(offset);
packet.bytes.resize(kDatagramHeaderBytes + payload_size);
writeU32(&packet.bytes, 0U, kDatagramMagic);
packet.bytes[4U] = packet.header.protocol_version;
packet.bytes[5U] = static_cast<std::uint8_t>(packet.header.kind);
writeU16(&packet.bytes, 6U, packet.header.flags);
writeU16(&packet.bytes, 8U, static_cast<std::uint16_t>(kDatagramHeaderBytes));
writeU16(&packet.bytes, 10U, packet.header.fragment_index);
writeU16(&packet.bytes, 12U, packet.header.fragment_count);
writeU16(&packet.bytes, 14U, packet.header.payload_size);
writeU32(&packet.bytes, 16U, packet.header.track_id);
writeU32(&packet.bytes, 20U, packet.header.codec_generation);
writeU64(&packet.bytes, 24U, packet.header.session_epoch);
writeU64(&packet.bytes, 32U, packet.header.packet_sequence);
writeU64(&packet.bytes, 40U, packet.header.frame_sequence);
writeU64(&packet.bytes, 48U, packet.header.capture_timestamp_us);
writeU32(&packet.bytes, 56U, packet.header.frame_size);
writeU32(&packet.bytes, 60U, packet.header.fragment_offset);
std::copy_n(frame.data() + offset, payload_size,
packet.bytes.data() + kDatagramHeaderBytes);
encoded_packets.push_back(std::move(packet));
}
next_packet_sequence_ += fragment_count;
*packets = std::move(encoded_packets);
return true;
}
bool DatagramPacketizer::decodeHeader(const std::uint8_t* data,
const std::size_t size,
DatagramHeader* header,
std::string* error)
{
if (!data || !header) {
setError(error, "DATAGRAM input or header output is null");
return false;
}
if (size < kDatagramHeaderBytes) {
setError(error, "DATAGRAM is shorter than the v1 header");
return false;
}
if (readU32(data, 0U) != kDatagramMagic) {
setError(error, "DATAGRAM magic mismatch");
return false;
}
if (data[4U] != kProtocolVersion) {
setError(error, "unsupported DATAGRAM protocol version");
return false;
}
const auto kind = static_cast<media::MediaKind>(data[5U]);
if (!validKind(kind)) {
setError(error, "unsupported DATAGRAM media kind");
return false;
}
if (readU16(data, 8U) != kDatagramHeaderBytes) {
setError(error, "unsupported DATAGRAM header size");
return false;
}
DatagramHeader decoded;
decoded.protocol_version = data[4U];
decoded.kind = kind;
decoded.flags = readU16(data, 6U);
decoded.fragment_index = readU16(data, 10U);
decoded.fragment_count = readU16(data, 12U);
decoded.payload_size = readU16(data, 14U);
decoded.track_id = readU32(data, 16U);
decoded.codec_generation = readU32(data, 20U);
decoded.session_epoch = readU64(data, 24U);
decoded.packet_sequence = readU64(data, 32U);
decoded.frame_sequence = readU64(data, 40U);
decoded.capture_timestamp_us = readU64(data, 48U);
decoded.frame_size = readU32(data, 56U);
decoded.fragment_offset = readU32(data, 60U);
if (decoded.track_id == 0U || decoded.codec_generation == 0U ||
decoded.session_epoch == 0U) {
setError(error, "DATAGRAM has a zero track, generation, or session identifier");
return false;
}
if (decoded.fragment_count == 0U ||
decoded.fragment_index >= decoded.fragment_count) {
setError(error, "DATAGRAM fragment index/count is invalid");
return false;
}
if (decoded.payload_size != size - kDatagramHeaderBytes) {
setError(error, "DATAGRAM payload size does not match packet length");
return false;
}
if (decoded.frame_size == 0U || decoded.payload_size == 0U ||
decoded.fragment_offset > decoded.frame_size ||
decoded.payload_size > decoded.frame_size - decoded.fragment_offset) {
setError(error, "DATAGRAM fragment is outside the declared frame");
return false;
}
*header = decoded;
return true;
}
} // namespace cmvr::quic_edge

View File

@ -0,0 +1,856 @@
#include "service/quic_edge/include/quic_transport.h"
#include <msquic.h>
#include <algorithm>
#include <condition_variable>
#include <deque>
#include <iomanip>
#include <limits>
#include <mutex>
#include <sstream>
#include <unordered_set>
#include <utility>
#include "common/base/logging/logger.h"
namespace cmvr::quic_edge {
namespace {
std::string statusText(const char* operation, const QUIC_STATUS status)
{
std::ostringstream output;
output << operation << " failed with QUIC_STATUS 0x" << std::hex
<< static_cast<std::uint64_t>(status);
return output.str();
}
constexpr std::size_t kControlFramePrefixBytes = sizeof(std::uint32_t);
constexpr std::size_t kMinimumControlReceiveQueueBytes = 64U * 1024U;
constexpr std::size_t kMaximumControlReceiveQueueBytes =
32U * 1024U * 1024U + 2U * kControlFramePrefixBytes;
constexpr std::size_t kMaximumControlReceiveQueueChunks = 4096U;
constexpr std::size_t kMaximumReservedControlSends = 8U;
constexpr std::uint64_t kMaximumHeartbeatIntervalMs = 60ULL * 60ULL * 1000ULL;
std::size_t reservedControlSends(const std::size_t queue_depth)
{
if (queue_depth <= 1U) return 0U;
return std::min<std::size_t>(
kMaximumReservedControlSends,
std::max<std::size_t>(1U, queue_depth / 16U));
}
std::size_t controlReceiveQueueBytes(const config::QuicEdgeConfig& config)
{
// Keep enough room for two maximum-sized framed control messages while
// enforcing a transport-level hard bound independent of peer behavior.
const std::uint64_t desired =
2ULL * (static_cast<std::uint64_t>(
config.maximum_control_frame_bytes()) +
kControlFramePrefixBytes);
return static_cast<std::size_t>(std::clamp<std::uint64_t>(
desired, kMinimumControlReceiveQueueBytes,
kMaximumControlReceiveQueueBytes));
}
class MsQuicTransport final : public QuicTransport {
public:
explicit MsQuicTransport(const std::size_t send_queue_depth)
: send_queue_depth_(std::max<std::size_t>(1U, send_queue_depth)),
reserved_control_sends_(reservedControlSends(send_queue_depth_))
{
}
~MsQuicTransport() override
{
disconnect();
if (registration_ && api_) api_->RegistrationClose(registration_);
if (api_) MsQuicClose(api_);
}
bool connect(const config::QuicEdgeConfig& config,
const std::chrono::milliseconds timeout,
std::string* error) override
{
disconnect();
QUIC_STATUS status = QUIC_STATUS_SUCCESS;
{
// Configuration creation and connection publication are one API
// operation. A concurrent disconnect can therefore never close a
// just-created configuration before ConnectionStart consumes it.
std::lock_guard operation_lock(api_operation_mutex_);
if (!openApi(error) || !openConfiguration(config, error)) {
return false;
}
{
std::lock_guard lock(mutex_);
shutdown_complete_ = false;
connected_ = false;
datagram_send_enabled_ = false;
maximum_datagram_bytes_ = 0U;
control_stream_start_completed_ = false;
control_stream_started_ = false;
control_stream_shutdown_complete_ = false;
control_stream_receive_closed_ = true;
control_receive_queue_.clear();
control_receive_queue_bytes_ = 0U;
maximum_control_receive_queue_bytes_ =
controlReceiveQueueBytes(config);
control_receive_error_.clear();
connect_error_.clear();
}
HQUIC opened_connection = nullptr;
status = api_->ConnectionOpen(
registration_, &MsQuicTransport::connectionCallback, this,
&opened_connection);
if (QUIC_SUCCEEDED(status)) {
{
std::lock_guard state_lock(mutex_);
connection_ = opened_connection;
}
const std::string& server_name =
config.tls().allow_insecure() || config.tls().server_name().empty()
? config.server_host() : config.tls().server_name();
CMVR_LOG(INFO) << "[MsQuicTransport] ConnectionStart"
<< ", connect_host=" << server_name
<< ", configured_server_host=" << config.server_host()
<< ", port=" << config.server_port()
<< ", ca_file=" << config.tls().ca_file()
<< ", allow_insecure=" << config.tls().allow_insecure();
status = api_->ConnectionStart(
opened_connection, configuration_, QUIC_ADDRESS_FAMILY_UNSPEC,
server_name.c_str(),
static_cast<std::uint16_t>(config.server_port()));
}
}
if (QUIC_FAILED(status)) {
setError(error, statusText("ConnectionOpen/Start", status));
closeConnectionHandles();
return false;
}
{
std::unique_lock lock(mutex_);
if (!condition_.wait_for(lock, timeout, [this]() {
return connected_ || shutdown_complete_;
}) || !connected_) {
const std::string message = connect_error_.empty()
? "MsQuic connect timed out" : connect_error_;
lock.unlock();
disconnect();
setError(error, message);
return false;
}
}
bool connection_available = false;
{
std::lock_guard operation_lock(api_operation_mutex_);
HQUIC connection = nullptr;
HQUIC opened_stream = nullptr;
{
std::lock_guard state_lock(mutex_);
connection_available = connected_ && connection_;
connection = connection_;
}
if (connection_available) {
status = api_->StreamOpen(
connection, QUIC_STREAM_OPEN_FLAG_NONE,
&MsQuicTransport::streamCallback, this, &opened_stream);
if (QUIC_SUCCEEDED(status)) {
{
std::lock_guard state_lock(mutex_);
control_stream_ = opened_stream;
control_stream_start_completed_ = false;
control_stream_started_ = false;
control_stream_shutdown_complete_ = false;
control_stream_receive_closed_ = false;
}
status = api_->StreamStart(
opened_stream, QUIC_STREAM_START_FLAG_IMMEDIATE);
}
}
}
if (!connection_available) {
setError(error, "QUIC connection closed before control stream setup");
disconnect();
return false;
}
if (QUIC_FAILED(status)) {
{
std::lock_guard lock(mutex_);
control_stream_start_completed_ = true;
control_stream_started_ = false;
}
setError(error, statusText("control StreamOpen/Start", status));
disconnect();
return false;
}
{
std::unique_lock lock(mutex_);
const bool completed = condition_.wait_for(lock, timeout, [this]() {
return control_stream_start_completed_ || !connected_;
});
if (!completed || !connected_ || !control_stream_started_) {
const std::string message = connect_error_.empty()
? "QUIC control stream start did not complete successfully"
: connect_error_;
lock.unlock();
disconnect();
setError(error, message);
return false;
}
}
return true;
}
void disconnect() override
{
HQUIC connection = nullptr;
HQUIC stream = nullptr;
bool stream_started = false;
{
// Resolve and use the handle while holding the API-operation lock.
// This prevents a concurrent closeConnectionHandles() call from
// closing the handle between the state read and ConnectionShutdown.
std::lock_guard operation_lock(api_operation_mutex_);
{
std::lock_guard lock(mutex_);
connection = connection_;
stream = control_stream_;
stream_started = control_stream_started_;
connected_ = false;
control_stream_receive_closed_ = true;
++control_receive_epoch_;
condition_.notify_all();
}
if (stream && stream_started && api_) {
const auto flags = static_cast<QUIC_STREAM_SHUTDOWN_FLAGS>(
QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND |
QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE |
QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE);
(void)api_->StreamShutdown(stream, flags, 0);
}
if (connection && api_) {
api_->ConnectionShutdown(
connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
}
}
if (stream && stream_started && api_) {
std::unique_lock lock(mutex_);
// A started stream handle may only be closed after MsQuic delivers
// SHUTDOWN_COMPLETE. IMMEDIATE schedules that completion without a
// graceful network wait, but it is not guaranteed to run inline.
condition_.wait(lock, [this, stream]() {
return control_stream_ != stream ||
control_stream_shutdown_complete_;
});
}
closeConnectionHandles();
}
bool isConnected() const override
{
std::lock_guard lock(mutex_);
return connected_;
}
std::size_t maximumDatagramBytes() const override
{
std::lock_guard lock(mutex_);
return maximum_datagram_bytes_;
}
std::size_t maximumDatagramBatchPackets() const override
{
// Static empty-queue limit. sendDatagramBatch performs the real-time
// admission check under mutex and may still return WOULD_BLOCK.
return send_queue_depth_ - reserved_control_sends_;
}
TransportSendResult sendControl(std::vector<std::uint8_t> message,
std::string* error) override
{
std::lock_guard operation_lock(api_operation_mutex_);
HQUIC stream = nullptr;
{
std::lock_guard lock(mutex_);
if (!connected_ || !control_stream_) return TransportSendResult::DISCONNECTED;
if (pending_send_count_ >= send_queue_depth_) {
return TransportSendResult::WOULD_BLOCK;
}
++pending_send_count_;
stream = control_stream_;
}
auto* context = new SendContext(this, std::move(message));
registerSend(context);
const QUIC_STATUS status = api_->StreamSend(
stream, &context->buffer, 1, QUIC_SEND_FLAG_NONE, context);
if (QUIC_FAILED(status)) {
finishSend(context);
setError(error, statusText("StreamSend", status));
return TransportSendResult::ERROR;
}
return TransportSendResult::QUEUED;
}
TransportReceiveResult receiveControl(
std::vector<std::uint8_t>* chunk,
const std::chrono::milliseconds timeout,
std::string* error) override
{
if (!chunk) {
setError(error, "control receive output must not be null");
return TransportReceiveResult::ERROR;
}
chunk->clear();
if (error) error->clear();
if (timeout.count() < 0) {
setError(error, "control receive timeout must not be negative");
return TransportReceiveResult::ERROR;
}
std::unique_lock lock(mutex_);
const std::uint64_t receive_epoch = control_receive_epoch_;
const auto ready = [this, receive_epoch]() {
return !control_receive_error_.empty() ||
!control_receive_queue_.empty() ||
control_stream_receive_closed_ || !connected_ ||
control_receive_epoch_ != receive_epoch;
};
if (!ready()) {
bool signaled = false;
if (timeout == std::chrono::milliseconds::max()) {
condition_.wait(lock, ready);
signaled = true;
} else {
signaled = condition_.wait_for(lock, timeout, ready);
}
if (!signaled) return TransportReceiveResult::TIMEOUT;
}
// A disconnect/reconnect boundary invalidates the old stream decoder
// even if a new connection became ready before this waiter ran.
if (control_receive_epoch_ != receive_epoch) {
setError(error, "QUIC control stream session changed");
return TransportReceiveResult::DISCONNECTED;
}
if (!control_receive_error_.empty()) {
setError(error, control_receive_error_);
return TransportReceiveResult::ERROR;
}
// Preserve bytes already delivered by MsQuic even when shutdown raced
// with the consumer; report DISCONNECTED only after draining them.
if (!control_receive_queue_.empty()) {
*chunk = std::move(control_receive_queue_.front());
control_receive_queue_.pop_front();
control_receive_queue_bytes_ -= chunk->size();
return TransportReceiveResult::DATA;
}
setError(error, connect_error_.empty()
? "QUIC control stream is disconnected" : connect_error_);
return TransportReceiveResult::DISCONNECTED;
}
TransportSendResult sendDatagramBatch(
std::vector<DatagramPacket> packets,
std::string* error) override
{
std::lock_guard operation_lock(api_operation_mutex_);
if (packets.empty()) {
setError(error, "empty MsQuic DATAGRAM batch");
return TransportSendResult::ERROR;
}
HQUIC connection = nullptr;
{
std::lock_guard lock(mutex_);
if (!connected_ || !connection_) return TransportSendResult::DISCONNECTED;
if (!datagram_send_enabled_) {
setError(error, "peer/path disabled QUIC DATAGRAM sending");
return TransportSendResult::ERROR;
}
for (const auto& packet : packets) {
if (packet.bytes.empty() ||
packet.bytes.size() > maximum_datagram_bytes_) {
setError(error,
"QUIC DATAGRAM exceeds the current path limit");
return TransportSendResult::ERROR;
}
}
const std::size_t datagram_capacity =
send_queue_depth_ - reserved_control_sends_;
if (pending_send_count_ >= datagram_capacity ||
packets.size() > datagram_capacity - pending_send_count_) {
return TransportSendResult::WOULD_BLOCK;
}
pending_send_count_ += packets.size();
connection = connection_;
}
std::size_t submitted = 0;
for (auto& packet : packets) {
auto* context = new SendContext(this, std::move(packet.bytes));
registerSend(context);
const QUIC_STATUS status = api_->DatagramSend(
connection, &context->buffer, 1, QUIC_SEND_FLAG_NONE, context);
if (QUIC_FAILED(status)) {
finishSend(context);
releaseReservedSends(packets.size() - submitted - 1U);
setError(error, statusText("DatagramSend", status));
// A submission error after earlier fragments is terminal. The
// reconnect/session epoch prevents receivers from combining a
// partial old frame with new traffic.
api_->ConnectionShutdown(connection,
QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 1);
return TransportSendResult::ERROR;
}
++submitted;
}
return TransportSendResult::QUEUED;
}
private:
struct SendContext {
SendContext(MsQuicTransport* value_owner, std::vector<std::uint8_t> value)
: owner(value_owner), bytes(std::move(value))
{
buffer.Buffer = bytes.data();
buffer.Length = static_cast<std::uint32_t>(bytes.size());
}
MsQuicTransport* owner;
std::vector<std::uint8_t> bytes;
QUIC_BUFFER buffer{};
};
static QUIC_STATUS QUIC_API connectionCallback(
HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event)
{
auto* self = static_cast<MsQuicTransport*>(context);
switch (event->Type) {
case QUIC_CONNECTION_EVENT_CONNECTED: {
std::lock_guard lock(self->mutex_);
if (connection != self->connection_) break;
self->connected_ = true;
self->condition_.notify_all();
break;
}
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: {
std::lock_guard lock(self->mutex_);
if (connection != self->connection_) break;
self->connected_ = false;
self->control_stream_receive_closed_ = true;
self->connect_error_ = statusText(
"peer/transport shutdown",
event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status);
self->condition_.notify_all();
break;
}
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: {
std::lock_guard lock(self->mutex_);
if (connection != self->connection_) break;
self->connected_ = false;
self->control_stream_receive_closed_ = true;
self->connect_error_ = "peer closed the QUIC connection";
self->condition_.notify_all();
break;
}
case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: {
std::lock_guard lock(self->mutex_);
if (connection != self->connection_) break;
self->connected_ = false;
self->control_stream_receive_closed_ = true;
self->shutdown_complete_ = true;
self->condition_.notify_all();
break;
}
case QUIC_CONNECTION_EVENT_DATAGRAM_SEND_STATE_CHANGED:
switch (event->DATAGRAM_SEND_STATE_CHANGED.State) {
case QUIC_DATAGRAM_SEND_LOST_DISCARDED:
case QUIC_DATAGRAM_SEND_ACKNOWLEDGED:
case QUIC_DATAGRAM_SEND_ACKNOWLEDGED_SPURIOUS:
case QUIC_DATAGRAM_SEND_CANCELED:
self->finishSend(static_cast<SendContext*>(
event->DATAGRAM_SEND_STATE_CHANGED.ClientContext));
break;
default:
break;
}
break;
case QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED: {
std::lock_guard lock(self->mutex_);
if (connection != self->connection_) break;
self->datagram_send_enabled_ =
event->DATAGRAM_STATE_CHANGED.SendEnabled != FALSE;
self->maximum_datagram_bytes_ = self->datagram_send_enabled_
? event->DATAGRAM_STATE_CHANGED.MaxSendLength : 0U;
self->condition_.notify_all();
break;
}
default:
break;
}
return QUIC_STATUS_SUCCESS;
}
static QUIC_STATUS QUIC_API streamCallback(
HQUIC stream, void* context, QUIC_STREAM_EVENT* event)
{
auto* self = static_cast<MsQuicTransport*>(context);
switch (event->Type) {
case QUIC_STREAM_EVENT_START_COMPLETE: {
std::lock_guard lock(self->mutex_);
if (stream != self->control_stream_) break;
self->control_stream_start_completed_ = true;
self->control_stream_started_ =
QUIC_SUCCEEDED(event->START_COMPLETE.Status);
if (!self->control_stream_started_) {
self->control_stream_receive_closed_ = true;
self->connect_error_ = statusText(
"control stream start", event->START_COMPLETE.Status);
}
self->condition_.notify_all();
break;
}
case QUIC_STREAM_EVENT_RECEIVE:
return self->handleControlReceive(stream, event);
case QUIC_STREAM_EVENT_SEND_COMPLETE:
if (event->SEND_COMPLETE.Canceled != FALSE) {
std::lock_guard lock(self->mutex_);
if (stream == self->control_stream_ && self->connected_) {
if (self->connect_error_.empty()) {
self->connect_error_ =
"QUIC control stream send was canceled";
}
self->control_stream_receive_closed_ = true;
self->connected_ = false;
self->condition_.notify_all();
}
}
self->finishSend(static_cast<SendContext*>(
event->SEND_COMPLETE.ClientContext));
break;
case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: {
std::lock_guard lock(self->mutex_);
if (stream != self->control_stream_ || !self->connected_) break;
if (self->connect_error_.empty()) {
self->connect_error_ =
"peer aborted the QUIC control stream receive direction";
}
self->control_stream_receive_closed_ = true;
self->connected_ = false;
self->condition_.notify_all();
break;
}
case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: {
std::lock_guard lock(self->mutex_);
if (stream != self->control_stream_) break;
self->control_stream_receive_closed_ = true;
self->condition_.notify_all();
break;
}
case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: {
std::lock_guard lock(self->mutex_);
if (stream != self->control_stream_) break;
self->control_stream_shutdown_complete_ = true;
self->control_stream_receive_closed_ = true;
self->condition_.notify_all();
break;
}
default:
break;
}
return QUIC_STATUS_SUCCESS;
}
QUIC_STATUS handleControlReceive(
HQUIC stream, const QUIC_STREAM_EVENT* event)
{
if (event->RECEIVE.BufferCount != 0U && !event->RECEIVE.Buffers) {
return failControlReceive(
stream, "MsQuic supplied a null control receive buffer",
QUIC_STATUS_INVALID_PARAMETER);
}
std::size_t received_bytes = 0U;
for (std::uint32_t index = 0U;
index < event->RECEIVE.BufferCount; ++index) {
const QUIC_BUFFER& buffer = event->RECEIVE.Buffers[index];
if (buffer.Length != 0U && !buffer.Buffer) {
return failControlReceive(
stream, "MsQuic supplied a null control receive payload",
QUIC_STATUS_INVALID_PARAMETER);
}
if (buffer.Length >
std::numeric_limits<std::size_t>::max() - received_bytes) {
return failControlReceive(
stream, "QUIC control receive byte count overflow",
QUIC_STATUS_BUFFER_TOO_SMALL);
}
received_bytes += buffer.Length;
}
if (received_bytes == 0U) return QUIC_STATUS_SUCCESS;
std::size_t queue_limit = 0U;
{
std::lock_guard lock(mutex_);
if (stream != control_stream_) return QUIC_STATUS_SUCCESS;
queue_limit = maximum_control_receive_queue_bytes_;
}
if (received_bytes > queue_limit) {
return failControlReceive(
stream, "QUIC control receive chunk exceeds the bounded queue",
QUIC_STATUS_BUFFER_TOO_SMALL);
}
std::vector<std::uint8_t> chunk;
try {
chunk.resize(received_bytes);
std::size_t offset = 0U;
for (std::uint32_t index = 0U;
index < event->RECEIVE.BufferCount; ++index) {
const QUIC_BUFFER& buffer = event->RECEIVE.Buffers[index];
std::copy_n(buffer.Buffer, buffer.Length,
chunk.begin() + static_cast<std::ptrdiff_t>(offset));
offset += buffer.Length;
}
} catch (...) {
return failControlReceive(
stream, "unable to allocate QUIC control receive storage",
QUIC_STATUS_OUT_OF_MEMORY);
}
{
std::lock_guard lock(mutex_);
if (stream != control_stream_ ||
control_stream_receive_closed_ || !connected_) {
return QUIC_STATUS_SUCCESS;
}
if (control_receive_queue_.size() >=
kMaximumControlReceiveQueueChunks ||
control_receive_queue_bytes_ >
maximum_control_receive_queue_bytes_ - received_bytes) {
control_receive_error_ =
"QUIC control receive queue capacity exceeded";
control_stream_receive_closed_ = true;
connected_ = false;
condition_.notify_all();
return QUIC_STATUS_BUFFER_TOO_SMALL;
}
try {
control_receive_queue_.push_back(std::move(chunk));
} catch (...) {
control_receive_error_ =
"unable to enqueue QUIC control receive bytes";
control_stream_receive_closed_ = true;
connected_ = false;
condition_.notify_all();
return QUIC_STATUS_OUT_OF_MEMORY;
}
control_receive_queue_bytes_ += received_bytes;
condition_.notify_all();
}
// Returning success consumes every buffer synchronously. Their bytes
// are now owned by control_receive_queue_.
return QUIC_STATUS_SUCCESS;
}
QUIC_STATUS failControlReceive(
HQUIC stream, const char* message, const QUIC_STATUS status)
{
std::lock_guard lock(mutex_);
if (stream != control_stream_) return QUIC_STATUS_SUCCESS;
control_receive_error_ = message;
control_stream_receive_closed_ = true;
connected_ = false;
condition_.notify_all();
return status;
}
bool openApi(std::string* error)
{
if (api_) return true;
QUIC_STATUS status = MsQuicOpen2(&api_);
if (QUIC_FAILED(status)) {
setError(error, statusText("MsQuicOpen2", status));
return false;
}
const QUIC_REGISTRATION_CONFIG registration_config = {
"cmvr-es-quic-edge", QUIC_EXECUTION_PROFILE_LOW_LATENCY};
status = api_->RegistrationOpen(&registration_config, &registration_);
if (QUIC_FAILED(status)) {
setError(error, statusText("RegistrationOpen", status));
MsQuicClose(api_);
api_ = nullptr;
return false;
}
return true;
}
bool openConfiguration(const config::QuicEdgeConfig& config,
std::string* error)
{
QUIC_BUFFER alpn{};
alpn.Buffer = reinterpret_cast<std::uint8_t*>(
const_cast<char*>(config.alpn().data()));
alpn.Length = static_cast<std::uint32_t>(config.alpn().size());
QUIC_SETTINGS settings{};
settings.IsSet.DatagramReceiveEnabled = TRUE;
settings.DatagramReceiveEnabled = TRUE;
settings.IsSet.IdleTimeoutMs = TRUE;
const std::uint64_t heartbeat_safe_idle_timeout =
kMaximumHeartbeatIntervalMs +
2ULL * config.control_response_timeout_ms() + 60000ULL;
settings.IdleTimeoutMs = std::max<std::uint64_t>(
heartbeat_safe_idle_timeout,
std::max<std::uint64_t>(
30000ULL, config.reconnect().connect_timeout_ms() * 3ULL));
QUIC_STATUS status = api_->ConfigurationOpen(
registration_, &alpn, 1, &settings, sizeof(settings),
nullptr, &configuration_);
if (QUIC_FAILED(status)) {
setError(error, statusText("ConfigurationOpen", status));
return false;
}
QUIC_CREDENTIAL_CONFIG credential{};
credential.Flags = QUIC_CREDENTIAL_FLAG_CLIENT;
if (config.tls().allow_insecure()) {
credential.Flags = static_cast<QUIC_CREDENTIAL_FLAGS>(
credential.Flags | QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION);
} else {
credential.Flags = static_cast<QUIC_CREDENTIAL_FLAGS>(
credential.Flags | QUIC_CREDENTIAL_FLAG_SET_CA_CERTIFICATE_FILE);
credential.CaCertificateFile = config.tls().ca_file().c_str();
}
QUIC_CERTIFICATE_FILE certificate{};
if (!config.tls().certificate_file().empty()) {
certificate.CertificateFile = config.tls().certificate_file().c_str();
certificate.PrivateKeyFile = config.tls().private_key_file().c_str();
credential.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE;
credential.CertificateFile = &certificate;
} else {
credential.Type = QUIC_CREDENTIAL_TYPE_NONE;
}
status = api_->ConfigurationLoadCredential(configuration_, &credential);
if (QUIC_FAILED(status)) {
setError(error, statusText("ConfigurationLoadCredential", status));
api_->ConfigurationClose(configuration_);
configuration_ = nullptr;
return false;
}
return true;
}
void closeConnectionHandles()
{
std::lock_guard operation_lock(api_operation_mutex_);
HQUIC stream = nullptr;
HQUIC connection = nullptr;
HQUIC configuration = nullptr;
{
std::lock_guard lock(mutex_);
stream = std::exchange(control_stream_, nullptr);
connection = std::exchange(connection_, nullptr);
configuration = std::exchange(configuration_, nullptr);
connected_ = false;
datagram_send_enabled_ = false;
maximum_datagram_bytes_ = 0U;
control_stream_start_completed_ = false;
control_stream_started_ = false;
control_stream_shutdown_complete_ = false;
control_stream_receive_closed_ = true;
++control_receive_epoch_;
condition_.notify_all();
}
if (!api_) return;
if (stream) api_->StreamClose(stream);
if (connection) api_->ConnectionClose(connection);
if (configuration) api_->ConfigurationClose(configuration);
std::vector<SendContext*> abandoned_sends;
{
// ConnectionClose/StreamClose are the final handle calls. Once
// they return, no send callback may still reference these
// contexts, so any missing completion can be reclaimed safely.
std::lock_guard lock(mutex_);
abandoned_sends.assign(outstanding_sends_.begin(),
outstanding_sends_.end());
outstanding_sends_.clear();
pending_send_count_ = 0U;
}
for (auto* context : abandoned_sends) delete context;
}
void finishSend(SendContext* context)
{
if (!context) return;
bool owned = false;
{
std::lock_guard lock(mutex_);
owned = outstanding_sends_.erase(context) != 0U;
if (owned && pending_send_count_ != 0U) --pending_send_count_;
condition_.notify_all();
}
if (owned) delete context;
}
void registerSend(SendContext* context)
{
std::lock_guard lock(mutex_);
outstanding_sends_.insert(context);
}
void releaseReservedSends(const std::size_t count)
{
std::lock_guard lock(mutex_);
pending_send_count_ = count > pending_send_count_
? 0U : pending_send_count_ - count;
condition_.notify_all();
}
static void setError(std::string* error, const std::string& message)
{
if (error) *error = message;
}
const std::size_t send_queue_depth_;
const std::size_t reserved_control_sends_;
const QUIC_API_TABLE* api_{nullptr};
HQUIC registration_{nullptr};
HQUIC configuration_{nullptr};
HQUIC connection_{nullptr};
HQUIC control_stream_{nullptr};
mutable std::mutex api_operation_mutex_;
mutable std::mutex mutex_;
std::condition_variable condition_;
bool connected_{false};
bool shutdown_complete_{false};
bool datagram_send_enabled_{false};
bool control_stream_start_completed_{false};
bool control_stream_started_{false};
bool control_stream_shutdown_complete_{false};
bool control_stream_receive_closed_{true};
std::size_t maximum_datagram_bytes_{0};
std::size_t maximum_control_receive_queue_bytes_{
kMinimumControlReceiveQueueBytes};
std::size_t control_receive_queue_bytes_{0};
std::uint64_t control_receive_epoch_{0};
std::size_t pending_send_count_{0};
std::deque<std::vector<std::uint8_t>> control_receive_queue_;
std::unordered_set<SendContext*> outstanding_sends_;
std::string control_receive_error_;
std::string connect_error_;
};
} // namespace
std::unique_ptr<QuicTransport> createMsQuicTransport(
const std::size_t send_queue_depth)
{
return std::make_unique<MsQuicTransport>(send_queue_depth);
}
} // namespace cmvr::quic_edge

View File

@ -0,0 +1,66 @@
#include "service/quic_edge/include/quic_edge_service.h"
#include <utility>
#include "manager/device_manager/include/device_manager.h"
#include "manager/media_source_hub/include/device_media_source_adapter.h"
namespace cmvr::quic_edge {
QuicEdgeService::QuicEdgeService(config::QuicEdgeConfig config)
: config_(std::move(config)),
transport_(createDefaultQuicTransport(config_.datagram_send_queue_depth())),
media_hub_(&media::globalMediaSourceHub()),
using_global_media_hub_(true)
{
initializeIdentity();
source_registrar_ = [this](
const config::QuicEdgeTrackConfig& track,
const std::string& source_track_id,
std::string* error) {
auto& manager = device::DeviceManager::getInstance();
bool registered = false;
switch (track.source_kind()) {
case config::QuicEdgeTrackConfig::SOURCE_KIND_CAMERA: {
auto camera =
manager.getDevice<device::AbstractCamera>(track.device_id());
if (!camera) {
if (error) {
*error = "camera unavailable for QUIC edge: " +
track.device_id();
}
return false;
}
registered = media::ensureCameraMediaSource(*media_hub_, camera);
break;
}
case config::QuicEdgeTrackConfig::SOURCE_KIND_MICROPHONE: {
auto microphone = manager.getDevice<device::AbstractMicrophone>(
track.device_id());
if (!microphone) {
if (error) {
*error = "microphone unavailable for QUIC edge: " +
track.device_id();
}
return false;
}
registered = media::ensureMicrophoneMediaSource(
*media_hub_, microphone);
break;
}
case config::QuicEdgeTrackConfig::SOURCE_KIND_UNSPECIFIED:
if (error) *error = "unsupported QUIC edge source kind";
return false;
}
if (!registered && !media_hub_->hasSource(source_track_id)) {
if (error) {
*error = "failed to register MediaSourceHub source: " +
source_track_id;
}
return false;
}
return true;
};
}
} // namespace cmvr::quic_edge

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
#include "service/quic_edge/include/quic_edge_types.h"
namespace cmvr::quic_edge {
std::uint32_t descriptorGenerationToken(const std::uint64_t generation)
{
// Fold both halves because the shared device adapter embeds its stream
// epoch in the upper 32 bits and its codec generation in the lower half.
std::uint32_t token = static_cast<std::uint32_t>(generation) ^
static_cast<std::uint32_t>(generation >> 32U);
return token == 0U ? 1U : token;
}
const char* codecName(const media::Codec codec)
{
switch (codec) {
case media::Codec::H264: return "h264";
case media::Codec::H265: return "h265";
case media::Codec::OPUS: return "opus";
case media::Codec::PCM_S16LE: return "pcm_s16le";
case media::Codec::AAC: return "aac";
case media::Codec::UNKNOWN: return "unknown";
}
return "unknown";
}
const char* payloadFormatName(const media::PayloadFormat format)
{
switch (format) {
case media::PayloadFormat::ANNEX_B: return "annex_b";
case media::PayloadFormat::AVCC: return "avcc";
case media::PayloadFormat::RAW: return "raw";
case media::PayloadFormat::OPUS_PACKET: return "opus_packet";
case media::PayloadFormat::AAC_ADTS: return "aac_adts";
case media::PayloadFormat::UNKNOWN: return "unknown";
}
return "unknown";
}
} // namespace cmvr::quic_edge

View File

@ -0,0 +1,31 @@
#include "service/quic_edge/include/quic_transport.h"
#include <utility>
namespace cmvr::quic_edge {
std::unique_ptr<QuicTransport> createMsQuicTransport(
std::size_t send_queue_depth);
TransportReceiveResult QuicTransport::receiveControl(
std::vector<std::uint8_t>* chunk,
std::chrono::milliseconds,
std::string* error)
{
if (!chunk) {
if (error) *error = "control receive output must not be null";
return TransportReceiveResult::ERROR;
}
chunk->clear();
if (error) {
*error = "control stream receive is not supported by this transport";
}
return TransportReceiveResult::DISCONNECTED;
}
std::unique_ptr<QuicTransport> createDefaultQuicTransport(
const std::size_t send_queue_depth)
{
return createMsQuicTransport(send_queue_depth);
}
} // namespace cmvr::quic_edge

View File

@ -0,0 +1,980 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <future>
#include <iostream>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "cmvr/quic_edge/v1/quic_edge.pb.h"
#include "manager/media_source_hub/include/media_source_hub.h"
#include "service/quic_edge/include/control_framing.h"
#include "service/quic_edge/include/datagram_packetizer.h"
#include "service/quic_edge/include/quic_edge_service.h"
namespace {
#define CHECK_TRUE(expression) \
do { \
if (!(expression)) { \
std::cerr << "CHECK failed at line " << __LINE__ << ": " \
<< #expression << '\n'; \
return false; \
} \
} while (false)
using namespace cmvr;
class FakeTransport final : public quic_edge::QuicTransport {
public:
explicit FakeTransport(const bool accept_registration = true,
const bool acknowledge_heartbeats = true,
const bool valid_heartbeat_session = true,
const std::size_t media_session_would_block_count = 0U,
const std::uint32_t registration_heartbeat_interval_ms =
250U,
const std::uint32_t heartbeat_ack_delay_ms = 0U)
: accept_registration_(accept_registration),
acknowledge_heartbeats_(acknowledge_heartbeats),
valid_heartbeat_session_(valid_heartbeat_session),
media_session_would_block_count_(media_session_would_block_count),
registration_heartbeat_interval_ms_(
registration_heartbeat_interval_ms),
heartbeat_ack_delay_ms_(heartbeat_ack_delay_ms)
{
}
bool connect(const config::QuicEdgeConfig&,
std::chrono::milliseconds,
std::string*) override
{
connected_.store(true);
++connect_count_;
{
std::lock_guard lock(mutex_);
connect_times_.push_back(std::chrono::steady_clock::now());
}
condition_.notify_all();
return true;
}
void disconnect() override
{
connected_.store(false);
{
std::lock_guard lock(mutex_);
delayed_heartbeat_ack_.reset();
}
condition_.notify_all();
}
bool isConnected() const override { return connected_.load(); }
std::size_t maximumDatagramBytes() const override { return 1200U; }
std::size_t maximumDatagramBatchPackets() const override { return 32U; }
quic_edge::TransportSendResult sendControl(
std::vector<std::uint8_t> message, std::string*) override
{
if (!connected_.load()) return quic_edge::TransportSendResult::DISCONNECTED;
std::vector<std::vector<std::uint8_t>> frames;
std::string decode_error;
quic_edge::ControlFrameDecoder decoder(1024U * 1024U);
if (!decoder.push(message, &frames, &decode_error) || frames.size() != 1U) {
return quic_edge::TransportSendResult::ERROR;
}
cmvr::quic_edge::v1::EdgeControlEnvelope envelope;
if (!envelope.ParseFromArray(frames.front().data(),
static_cast<int>(frames.front().size()))) {
return quic_edge::TransportSendResult::ERROR;
}
{
std::lock_guard lock(mutex_);
if (envelope.has_media_session_open() &&
media_session_would_block_count_ != 0U) {
--media_session_would_block_count_;
return quic_edge::TransportSendResult::WOULD_BLOCK;
}
edge_message_sequences_.push_back(envelope.message_sequence());
controls_.push_back(std::move(message));
if (envelope.has_node_register_request()) {
const auto& request = envelope.node_register_request();
last_registered_node_id_ = request.node().node_id();
last_registered_robot_id_ = request.node().robot_id();
last_grpc_endpoint_port_ = request.node().grpc_endpoint().port();
last_interface_count_ = request.node().local_interfaces_size();
cmvr::quic_edge::v1::EdgeControlEnvelope response;
response.set_protocol_version(quic_edge::kProtocolVersion);
response.set_message_sequence(server_message_sequence_++);
auto* registration = response.mutable_node_register_response();
registration->set_accepted(accept_registration_);
registration->set_session_id(
accept_registration_ ? "test-session" : "");
registration->set_message(
accept_registration_ ? "accepted" : "rejected for test");
registration->set_heartbeat_interval_ms(
registration_heartbeat_interval_ms_);
registration->set_observed_source_ip("203.0.113.10");
enqueueEnvelopeLocked(response);
} else if (envelope.has_node_heartbeat()) {
const auto& heartbeat = envelope.node_heartbeat();
last_heartbeat_ = heartbeat;
has_last_heartbeat_ = true;
heartbeat_times_.push_back(
std::chrono::steady_clock::now());
if (!acknowledge_heartbeats_) {
condition_.notify_all();
return quic_edge::TransportSendResult::QUEUED;
}
cmvr::quic_edge::v1::EdgeControlEnvelope response;
response.set_protocol_version(quic_edge::kProtocolVersion);
response.set_message_sequence(server_message_sequence_++);
auto* ack = response.mutable_node_heartbeat_ack();
ack->set_accepted(true);
ack->set_acknowledged_sequence(heartbeat.sequence());
ack->set_session_id(valid_heartbeat_session_
? heartbeat.session_id() : "");
ack->set_observed_source_ip("203.0.113.11");
if (heartbeat_ack_delay_ms_ == 0U) {
enqueueEnvelopeLocked(response);
} else {
delayed_heartbeat_ack_ = std::move(response);
delayed_heartbeat_ack_ready_at_ =
std::chrono::steady_clock::now() +
std::chrono::milliseconds(
heartbeat_ack_delay_ms_);
}
}
}
condition_.notify_all();
return quic_edge::TransportSendResult::QUEUED;
}
quic_edge::TransportReceiveResult receiveControl(
std::vector<std::uint8_t>* chunk,
const std::chrono::milliseconds timeout,
std::string*) override
{
if (!chunk) return quic_edge::TransportReceiveResult::ERROR;
std::unique_lock lock(mutex_);
releaseDelayedHeartbeatAckLocked();
if (control_receive_queue_.empty() && connected_.load()) {
auto wait_duration = timeout;
if (delayed_heartbeat_ack_) {
const auto now = std::chrono::steady_clock::now();
if (now < delayed_heartbeat_ack_ready_at_) {
wait_duration = std::min(
wait_duration,
std::chrono::duration_cast<std::chrono::milliseconds>(
delayed_heartbeat_ack_ready_at_ - now) +
std::chrono::milliseconds(1));
}
}
condition_.wait_for(lock, wait_duration, [this]() {
return !control_receive_queue_.empty() ||
!connected_.load();
});
releaseDelayedHeartbeatAckLocked();
}
if (!control_receive_queue_.empty()) {
*chunk = std::move(control_receive_queue_.front());
control_receive_queue_.pop_front();
return quic_edge::TransportReceiveResult::DATA;
}
return connected_.load()
? quic_edge::TransportReceiveResult::TIMEOUT
: quic_edge::TransportReceiveResult::DISCONNECTED;
}
quic_edge::TransportSendResult sendDatagramBatch(
std::vector<quic_edge::DatagramPacket> packets, std::string*) override
{
if (!connected_.load()) return quic_edge::TransportSendResult::DISCONNECTED;
std::lock_guard lock(mutex_);
datagrams_.push_back(std::move(packets));
return quic_edge::TransportSendResult::QUEUED;
}
std::size_t datagramBatchCount() const
{
std::lock_guard lock(mutex_);
return datagrams_.size();
}
std::size_t controlCount() const
{
std::lock_guard lock(mutex_);
return controls_.size();
}
bool controlSequencesStrictlyIncreasing() const
{
std::lock_guard lock(mutex_);
for (std::size_t index = 1U;
index < edge_message_sequences_.size(); ++index) {
if (edge_message_sequences_[index] <=
edge_message_sequences_[index - 1U]) {
return false;
}
}
return !edge_message_sequences_.empty();
}
std::vector<std::uint64_t> datagramFrameSequences() const
{
std::lock_guard lock(mutex_);
std::vector<std::uint64_t> sequences;
for (const auto& batch : datagrams_) {
if (!batch.empty()) sequences.push_back(batch.front().header.frame_sequence);
}
return sequences;
}
std::uint64_t connectCount() const { return connect_count_.load(); }
std::vector<std::chrono::milliseconds> connectIntervals() const
{
std::lock_guard lock(mutex_);
std::vector<std::chrono::milliseconds> intervals;
for (std::size_t index = 1U; index < connect_times_.size(); ++index) {
intervals.push_back(std::chrono::duration_cast<std::chrono::milliseconds>(
connect_times_[index] - connect_times_[index - 1U]));
}
return intervals;
}
std::string lastRegisteredNodeId() const
{
std::lock_guard lock(mutex_);
return last_registered_node_id_;
}
std::string lastRegisteredRobotId() const
{
std::lock_guard lock(mutex_);
return last_registered_robot_id_;
}
std::uint32_t lastGrpcEndpointPort() const
{
std::lock_guard lock(mutex_);
return last_grpc_endpoint_port_;
}
int lastInterfaceCount() const
{
std::lock_guard lock(mutex_);
return last_interface_count_;
}
std::size_t heartbeatCount() const
{
std::lock_guard lock(mutex_);
return heartbeat_times_.size();
}
std::vector<std::chrono::milliseconds> heartbeatIntervals() const
{
std::lock_guard lock(mutex_);
std::vector<std::chrono::milliseconds> intervals;
for (std::size_t index = 1U;
index < heartbeat_times_.size(); ++index) {
intervals.push_back(
std::chrono::duration_cast<std::chrono::milliseconds>(
heartbeat_times_[index] -
heartbeat_times_[index - 1U]));
}
return intervals;
}
cmvr::quic_edge::v1::NodeHeartbeat lastHeartbeat() const
{
std::lock_guard lock(mutex_);
return has_last_heartbeat_
? last_heartbeat_
: cmvr::quic_edge::v1::NodeHeartbeat{};
}
private:
void releaseDelayedHeartbeatAckLocked()
{
if (!delayed_heartbeat_ack_ ||
std::chrono::steady_clock::now() <
delayed_heartbeat_ack_ready_at_) {
return;
}
enqueueEnvelopeLocked(*delayed_heartbeat_ack_);
delayed_heartbeat_ack_.reset();
}
void enqueueEnvelopeLocked(
const cmvr::quic_edge::v1::EdgeControlEnvelope& envelope)
{
std::string serialized;
if (!envelope.SerializeToString(&serialized)) return;
std::vector<std::uint8_t> framed;
std::string error;
if (quic_edge::ControlFrameEncoder::encode(
reinterpret_cast<const std::uint8_t*>(serialized.data()),
serialized.size(), 1024U * 1024U, &framed, &error)) {
const auto split = framed.size() / 2U;
control_receive_queue_.emplace_back(
framed.begin(), framed.begin() + static_cast<std::ptrdiff_t>(split));
control_receive_queue_.emplace_back(
framed.begin() + static_cast<std::ptrdiff_t>(split), framed.end());
}
}
mutable std::mutex mutex_;
std::condition_variable condition_;
std::atomic<bool> connected_{false};
std::atomic<std::uint64_t> connect_count_{0};
bool accept_registration_{true};
bool acknowledge_heartbeats_{true};
bool valid_heartbeat_session_{true};
std::size_t media_session_would_block_count_{0U};
std::uint32_t registration_heartbeat_interval_ms_{250U};
std::uint32_t heartbeat_ack_delay_ms_{0U};
std::uint64_t server_message_sequence_{0};
std::string last_registered_node_id_;
std::string last_registered_robot_id_;
std::uint32_t last_grpc_endpoint_port_{0};
int last_interface_count_{0};
std::vector<std::vector<std::uint8_t>> controls_;
std::deque<std::vector<std::uint8_t>> control_receive_queue_;
std::vector<std::vector<quic_edge::DatagramPacket>> datagrams_;
std::vector<std::uint64_t> edge_message_sequences_;
std::vector<std::chrono::steady_clock::time_point> connect_times_;
std::vector<std::chrono::steady_clock::time_point> heartbeat_times_;
bool has_last_heartbeat_{false};
cmvr::quic_edge::v1::NodeHeartbeat last_heartbeat_;
std::optional<cmvr::quic_edge::v1::EdgeControlEnvelope>
delayed_heartbeat_ack_;
std::chrono::steady_clock::time_point
delayed_heartbeat_ack_ready_at_{};
};
config::QuicEdgeConfig validConfig(const std::string& source_track_id)
{
config::QuicEdgeConfig config;
config.set_id("quic-test");
config.set_server_host("127.0.0.1");
config.set_server_port(4433);
config.set_alpn("cmvr-quic-edge/1");
config.set_node_id("test-node");
config.set_robot_id("CN-CMVR-MBLRV1-CHAGAN-20260731-001");
config.set_software_version("test-version");
config.set_grpc_endpoint_host("auto");
config.set_grpc_endpoint_port(50052U);
config.set_grpc_endpoint_tls(false);
config.set_heartbeat_interval_ms(250U);
config.set_control_response_timeout_ms(100U);
config.set_include_loopback_interfaces(true);
config.mutable_tls()->set_allow_insecure(true);
config.mutable_reconnect()->set_initial_delay_ms(5);
config.mutable_reconnect()->set_maximum_delay_ms(20);
config.mutable_reconnect()->set_multiplier(2.0);
config.mutable_reconnect()->set_connect_timeout_ms(50);
config.set_maximum_datagram_bytes(1200);
config.set_maximum_control_frame_bytes(4096);
config.set_maximum_frame_bytes(32 * 1024);
config.set_datagram_send_queue_depth(32);
config.set_media_poll_interval_ms(1);
auto* track = config.add_tracks();
track->set_track_id(7);
track->set_source_kind(config::QuicEdgeTrackConfig::SOURCE_KIND_CAMERA);
track->set_device_id("camera-test");
track->set_source_track_id(source_track_id);
track->set_enable(true);
return config;
}
config::QuicEdgeConfig validPresenceOnlyConfig()
{
auto config = validConfig("unused/video/color");
config.clear_tracks();
return config;
}
media::TrackDescriptorPtr videoDescriptor(const std::string& id)
{
media::TrackDescriptor::Config config;
config.id = id;
config.source_id = "camera-test";
config.kind = media::MediaKind::VIDEO;
config.codec = media::Codec::H264;
config.payload_format = media::PayloadFormat::ANNEX_B;
config.time_base = {1, 90000};
config.width = 640;
config.height = 480;
config.nominal_rate = 30;
config.generation = 0x0000000200000003ULL;
return media::makeTrackDescriptor(std::move(config));
}
bool waitUntil(const std::function<bool()>& predicate)
{
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (std::chrono::steady_clock::now() < deadline) {
if (predicate()) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
return predicate();
}
bool testControlFraming()
{
const std::vector<std::uint8_t> payload{1, 2, 3, 4, 5};
std::vector<std::uint8_t> encoded;
std::string error;
CHECK_TRUE(quic_edge::ControlFrameEncoder::encode(
payload, 64, &encoded, &error));
quic_edge::ControlFrameDecoder decoder(64);
std::vector<std::vector<std::uint8_t>> decoded;
CHECK_TRUE(decoder.push(encoded.data(), 2, &decoded, &error));
CHECK_TRUE(decoded.empty());
CHECK_TRUE(decoder.push(encoded.data() + 2, encoded.size() - 2,
&decoded, &error));
CHECK_TRUE(decoded.size() == 1 && decoded.front() == payload);
return true;
}
bool testPacketizer()
{
auto descriptor = videoDescriptor("camera-test/video/color");
media::MediaFrame::Config frame_config;
frame_config.descriptor = descriptor;
frame_config.payload.resize(2500, 0x5a);
frame_config.sequence = 11;
frame_config.capture_time_ns = 1234567000ULL;
frame_config.key_frame = true;
auto frame = media::makeMediaFrame(std::move(frame_config));
quic_edge::DatagramPacketizer packetizer(100);
std::vector<quic_edge::DatagramPacket> packets;
std::string error;
const std::uint32_t token =
quic_edge::descriptorGenerationToken(descriptor->generation);
CHECK_TRUE(packetizer.packetize(*frame, 7, token, 99, 11, true, 1200,
&packets, &error));
CHECK_TRUE(packets.size() == 3);
std::size_t payload_bytes = 0;
for (std::size_t index = 0; index < packets.size(); ++index) {
quic_edge::DatagramHeader header;
CHECK_TRUE(quic_edge::DatagramPacketizer::decodeHeader(
packets[index].bytes, &header, &error));
CHECK_TRUE(header.track_id == 7 && header.session_epoch == 99);
CHECK_TRUE(header.fragment_index == index && header.fragment_count == 3);
CHECK_TRUE((header.flags & quic_edge::DATAGRAM_FLAG_KEY_FRAME) != 0);
CHECK_TRUE((header.flags & quic_edge::DATAGRAM_FLAG_DISCONTINUITY) != 0);
payload_bytes += header.payload_size;
}
CHECK_TRUE(payload_bytes == frame->size());
return true;
}
bool testServiceWithSharedHub()
{
const std::string track_id = "camera-test/video/color";
media::MediaSourceManager hub;
media::MediaSourceManager::FrameSink sink;
std::mutex sink_mutex;
std::atomic<bool> source_started{false};
std::atomic<std::uint64_t> keyframe_requests{0};
media::MediaSourceManager::SourceCallbacks callbacks;
callbacks.start = [&](const media::MediaSourceManager::FrameSink& value,
const media::MediaSourceManager::CancelPredicate&) {
std::lock_guard lock(sink_mutex);
sink = value;
source_started.store(true);
return true;
};
callbacks.stop = [&]() { source_started.store(false); };
callbacks.request_key_frame = [&]() {
++keyframe_requests;
return true;
};
auto descriptor = videoDescriptor(track_id);
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 8));
auto transport = std::make_unique<FakeTransport>(true, true, true, 1U);
FakeTransport* transport_view = transport.get();
quic_edge::QuicEdgeService service(
validConfig(track_id), std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() { return source_started.load(); }));
CHECK_TRUE(waitUntil([&]() {
return service.stats().registrations_accepted == 1U &&
service.stats().heartbeats_acknowledged >= 1U;
}));
CHECK_TRUE(transport_view->lastRegisteredNodeId() == "test-node");
CHECK_TRUE(transport_view->lastRegisteredRobotId() ==
"CN-CMVR-MBLRV1-CHAGAN-20260731-001");
CHECK_TRUE(transport_view->lastGrpcEndpointPort() == 50052U);
CHECK_TRUE(transport_view->connectCount() == 1U);
media::MediaFrame::Config frame_config;
frame_config.descriptor = descriptor;
frame_config.payload.resize(1800, 0x11);
frame_config.sequence = 1;
frame_config.capture_time_ns = 1000000;
frame_config.key_frame = true;
media::MediaSourceManager::FrameSink publisher;
{
std::lock_guard lock(sink_mutex);
publisher = sink;
}
CHECK_TRUE(static_cast<bool>(publisher));
publisher(media::makeMediaFrame(std::move(frame_config)));
CHECK_TRUE(waitUntil([&]() { return transport_view->datagramBatchCount() == 1; }));
CHECK_TRUE(service.stats().frames_queued == 1);
CHECK_TRUE(keyframe_requests.load() != 0);
// Codec initialization bytes may be learned at the first encoder IDR
// without changing the source generation. The reliable descriptor must be
// refreshed rather than treating this legal enrichment as a source error.
auto enriched_descriptor_config = media::TrackDescriptor::Config{};
enriched_descriptor_config.id = track_id;
enriched_descriptor_config.source_id = "camera-test";
enriched_descriptor_config.kind = media::MediaKind::VIDEO;
enriched_descriptor_config.codec = media::Codec::H264;
enriched_descriptor_config.payload_format = media::PayloadFormat::ANNEX_B;
enriched_descriptor_config.time_base = {1, 90000};
enriched_descriptor_config.width = 640;
enriched_descriptor_config.height = 480;
enriched_descriptor_config.nominal_rate = 30;
enriched_descriptor_config.generation = descriptor->generation;
enriched_descriptor_config.codec_config = {0, 0, 0, 1, 0x67};
auto enriched_descriptor =
media::makeTrackDescriptor(std::move(enriched_descriptor_config));
media::MediaFrame::Config enriched_frame;
enriched_frame.descriptor = std::move(enriched_descriptor);
enriched_frame.payload.resize(256, 0x22);
// Simulate a source restart that resets its device-local sequence. The
// QUIC wire sequence must remain monotonic inside the media epoch.
enriched_frame.sequence = 1;
enriched_frame.capture_time_ns = 2000000;
enriched_frame.key_frame = true;
enriched_frame.discontinuity = true;
publisher(media::makeMediaFrame(std::move(enriched_frame)));
CHECK_TRUE(waitUntil([&]() { return transport_view->datagramBatchCount() == 2; }));
const auto wire_sequences = transport_view->datagramFrameSequences();
CHECK_TRUE(wire_sequences.size() == 2U &&
wire_sequences[0] == 1U && wire_sequences[1] == 2U);
CHECK_TRUE(transport_view->controlCount() >= 3);
CHECK_TRUE(transport_view->controlSequencesStrictlyIncreasing());
CHECK_TRUE(service.state() != quic_edge::QuicEdgeServiceState::FAILED);
service.stop();
return true;
}
bool testMediaActivityInterruptPreservesPresenceAndResumes()
{
const std::string track_id = "camera-stop-all/video/color";
media::MediaSourceHub hub;
media::MediaSourceManager::FrameSink sink;
std::mutex sink_mutex;
std::atomic<bool> source_started{false};
std::atomic<std::uint64_t> source_starts{0U};
std::atomic<std::uint64_t> source_stops{0U};
media::MediaSourceManager::SourceCallbacks callbacks;
callbacks.start = [&](const media::MediaSourceManager::FrameSink& value,
const media::MediaSourceManager::CancelPredicate&) {
{
std::lock_guard lock(sink_mutex);
sink = value;
}
source_started.store(true);
++source_starts;
return true;
};
callbacks.stop = [&]() {
source_started.store(false);
++source_stops;
};
callbacks.request_key_frame = [] { return true; };
const auto descriptor = videoDescriptor(track_id);
CHECK_TRUE(hub.registerSource(descriptor, std::move(callbacks), 8U));
auto transport = std::make_unique<FakeTransport>();
FakeTransport* transport_view = transport.get();
quic_edge::QuicEdgeService edge_service(
validConfig(track_id), std::move(transport), hub);
std::string error;
CHECK_TRUE(edge_service.initialize(&error));
CHECK_TRUE(edge_service.start(&error));
CHECK_TRUE(waitUntil([&] {
return edge_service.state() == quic_edge::QuicEdgeServiceState::ONLINE &&
edge_service.status().registered && source_started.load() &&
hub.subscriberCount(track_id) == 1U;
}));
auto publish = [&](const std::uint64_t sequence) {
media::MediaSourceManager::FrameSink publisher;
{
std::lock_guard lock(sink_mutex);
publisher = sink;
}
if (!publisher) return false;
media::MediaFrame::Config frame;
frame.descriptor = descriptor;
frame.payload.resize(256U, 0x5aU);
frame.sequence = sequence;
frame.capture_time_ns = sequence * 1000000U;
frame.key_frame = true;
publisher(media::makeMediaFrame(std::move(frame)));
return true;
};
CHECK_TRUE(publish(1U));
CHECK_TRUE(waitUntil([&] {
return transport_view->datagramBatchCount() == 1U;
}));
const auto heartbeat_before_stop = transport_view->heartbeatCount();
CHECK_TRUE(edge_service.interruptMediaActivities());
CHECK_TRUE(hub.subscriberCount(track_id) == 0U);
CHECK_TRUE(!source_started.load());
CHECK_TRUE(source_stops.load() == 1U);
CHECK_TRUE(edge_service.state() == quic_edge::QuicEdgeServiceState::ONLINE);
CHECK_TRUE(edge_service.status().registered);
CHECK_TRUE(transport_view->connectCount() == 1U);
// A publisher retained by the stopped generation must no longer enqueue
// frames while StopAll admission is closed.
CHECK_TRUE(publish(2U));
std::this_thread::sleep_for(std::chrono::milliseconds(30));
CHECK_TRUE(transport_view->datagramBatchCount() == 1U);
CHECK_TRUE(waitUntil([&] {
return source_starts.load() == 2U && source_started.load() &&
hub.subscriberCount(track_id) == 1U;
}));
CHECK_TRUE(publish(3U));
CHECK_TRUE(waitUntil([&] {
return transport_view->datagramBatchCount() == 2U;
}));
CHECK_TRUE(waitUntil([&] {
return transport_view->heartbeatCount() > heartbeat_before_stop;
}));
CHECK_TRUE(edge_service.stats().registrations_accepted == 1U);
CHECK_TRUE(transport_view->connectCount() == 1U);
CHECK_TRUE(edge_service.state() == quic_edge::QuicEdgeServiceState::ONLINE);
edge_service.stop();
return true;
}
bool testMediaActivityInterruptCancelsStartingSubscription()
{
const std::string track_id = "slow-stop-all/video/color";
media::MediaSourceHub hub;
std::atomic<bool> start_entered{false};
std::atomic<bool> start_cancelled{false};
media::MediaSourceManager::SourceCallbacks callbacks;
callbacks.start = [&](const media::MediaSourceManager::FrameSink&,
const media::MediaSourceManager::CancelPredicate& cancelled) {
start_entered.store(true);
while (!cancelled()) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
start_cancelled.store(true);
return false;
};
callbacks.stop = [] {};
CHECK_TRUE(hub.registerSource(
videoDescriptor(track_id), std::move(callbacks), 8U));
auto transport = std::make_unique<FakeTransport>();
FakeTransport* transport_view = transport.get();
quic_edge::QuicEdgeService edge_service(
validConfig(track_id), std::move(transport), hub);
std::string error;
CHECK_TRUE(edge_service.initialize(&error));
CHECK_TRUE(edge_service.start(&error));
CHECK_TRUE(waitUntil([&] {
return start_entered.load() && edge_service.status().registered;
}));
auto interrupt = std::async(std::launch::async, [&edge_service] {
return edge_service.interruptMediaActivities();
});
CHECK_TRUE(interrupt.wait_for(std::chrono::milliseconds(500)) ==
std::future_status::ready);
CHECK_TRUE(interrupt.get());
CHECK_TRUE(start_cancelled.load());
CHECK_TRUE(hub.subscriberCount(track_id) == 0U);
CHECK_TRUE(edge_service.state() == quic_edge::QuicEdgeServiceState::ONLINE);
CHECK_TRUE(edge_service.status().registered);
CHECK_TRUE(transport_view->connectCount() == 1U);
edge_service.stop();
return true;
}
bool testMissingInjectedSourceRetriesSafely()
{
media::MediaSourceManager hub;
auto transport = std::make_unique<FakeTransport>();
FakeTransport* transport_view = transport.get();
quic_edge::QuicEdgeService service(
validConfig("missing/video/color"), std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() { return service.stats().source_errors >= 2; }));
CHECK_TRUE(service.state() == quic_edge::QuicEdgeServiceState::ONLINE);
CHECK_TRUE(service.status().registered);
CHECK_TRUE(service.status().active_media_tracks == 0U);
CHECK_TRUE(transport_view->connectCount() == 1U);
service.stop();
return true;
}
bool testPresenceOnlyWithoutMedia()
{
media::MediaSourceManager hub;
auto transport = std::make_unique<FakeTransport>();
FakeTransport* transport_view = transport.get();
quic_edge::QuicEdgeService service(
validPresenceOnlyConfig(), std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() {
return service.stats().registrations_accepted == 1U &&
service.stats().heartbeats_acknowledged >= 1U;
}));
const auto status = service.status();
CHECK_TRUE(status.registered);
CHECK_TRUE(status.session_id == "test-session");
CHECK_TRUE(status.observed_source_ip == "203.0.113.11");
CHECK_TRUE(status.active_media_tracks == 0U);
CHECK_TRUE(transport_view->datagramBatchCount() == 0U);
CHECK_TRUE(transport_view->connectCount() == 1U);
CHECK_TRUE(service.state() == quic_edge::QuicEdgeServiceState::ONLINE);
service.stop();
return true;
}
bool testConfiguredHeartbeatIntervalWithoutGatewayOverride()
{
media::MediaSourceManager hub;
auto transport = std::make_unique<FakeTransport>(
true, true, true, 0U, 0U);
FakeTransport* transport_view = transport.get();
auto config = validPresenceOnlyConfig();
config.set_heartbeat_interval_ms(400U);
quic_edge::QuicEdgeService service(
config, std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() {
return transport_view->heartbeatCount() >= 3U;
}));
const auto intervals = transport_view->heartbeatIntervals();
service.stop();
CHECK_TRUE(intervals.size() >= 2U);
CHECK_TRUE(intervals[0].count() >= 350);
CHECK_TRUE(intervals[1].count() >= 350);
CHECK_TRUE(intervals[0].count() <= 900);
CHECK_TRUE(intervals[1].count() <= 900);
return true;
}
bool testHeartbeatTimeoutReconnectsWithoutTaskFailure()
{
media::MediaSourceManager hub;
auto transport = std::make_unique<FakeTransport>(true, false);
FakeTransport* transport_view = transport.get();
auto config = validPresenceOnlyConfig();
config.set_control_response_timeout_ms(20U);
quic_edge::QuicEdgeService service(config, std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() {
return service.stats().heartbeat_timeouts >= 1U &&
transport_view->connectCount() >= 2U;
}));
CHECK_TRUE(service.state() != quic_edge::QuicEdgeServiceState::FAILED);
service.stop();
return true;
}
bool testRegistrationRejectionBacksOff()
{
media::MediaSourceManager hub;
auto transport = std::make_unique<FakeTransport>(false, true);
FakeTransport* transport_view = transport.get();
auto config = validPresenceOnlyConfig();
config.mutable_reconnect()->set_initial_delay_ms(20U);
config.mutable_reconnect()->set_maximum_delay_ms(80U);
quic_edge::QuicEdgeService service(
config, std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() {
return service.stats().registrations_rejected >= 3U &&
transport_view->connectCount() >= 4U;
}));
CHECK_TRUE(service.stats().heartbeats_sent == 0U);
const auto intervals = transport_view->connectIntervals();
CHECK_TRUE(intervals.size() >= 3U);
CHECK_TRUE(intervals[0].count() >= 15);
CHECK_TRUE(intervals[1].count() >= 35);
CHECK_TRUE(intervals[2].count() >= 70);
CHECK_TRUE(service.state() != quic_edge::QuicEdgeServiceState::FAILED);
service.stop();
return true;
}
bool testHeartbeatAckRequiresSessionId()
{
media::MediaSourceManager hub;
auto transport = std::make_unique<FakeTransport>(true, true, false);
FakeTransport* transport_view = transport.get();
quic_edge::QuicEdgeService service(
validPresenceOnlyConfig(), std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() {
return transport_view->connectCount() >= 2U &&
service.stats().heartbeats_sent >= 2U;
}));
CHECK_TRUE(service.stats().heartbeats_acknowledged == 0U);
CHECK_TRUE(service.state() != quic_edge::QuicEdgeServiceState::FAILED);
service.stop();
return true;
}
bool testSlowMediaStartDoesNotBlockHeartbeat()
{
const std::string track_id = "slow-camera/video/color";
media::MediaSourceManager hub;
std::atomic<bool> start_entered{false};
std::atomic<bool> start_exited{false};
std::atomic<bool> release_start{false};
media::MediaSourceManager::SourceCallbacks callbacks;
callbacks.start = [&](const media::MediaSourceManager::FrameSink&,
const media::MediaSourceManager::CancelPredicate& cancelled) {
start_entered.store(true);
while (!release_start.load() && !cancelled()) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
start_exited.store(true);
return !cancelled();
};
callbacks.stop = []() {};
CHECK_TRUE(hub.registerSource(
videoDescriptor(track_id), std::move(callbacks), 8U));
auto transport = std::make_unique<FakeTransport>();
FakeTransport* transport_view = transport.get();
quic_edge::QuicEdgeService service(
validConfig(track_id), std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
const bool entered = waitUntil([&]() { return start_entered.load(); });
const bool heartbeat_while_blocked = entered && waitUntil([&]() {
return service.stats().heartbeats_acknowledged >= 1U;
});
const bool registered_while_blocked = service.status().registered;
auto stop_future = std::async(std::launch::async, [&service] {
service.stop();
});
const bool stop_while_start_blocked =
stop_future.wait_for(std::chrono::milliseconds(500)) ==
std::future_status::ready;
release_start.store(true);
stop_future.get();
const bool start_cancelled = waitUntil([&]() { return start_exited.load(); });
CHECK_TRUE(entered);
CHECK_TRUE(heartbeat_while_blocked);
CHECK_TRUE(registered_while_blocked);
CHECK_TRUE(stop_while_start_blocked);
CHECK_TRUE(start_cancelled);
CHECK_TRUE(transport_view->connectCount() == 1U);
CHECK_TRUE(transport_view->controlSequencesStrictlyIncreasing());
return true;
}
bool testLifecycleStateGuards()
{
media::MediaSourceManager hub;
{
auto transport = std::make_unique<FakeTransport>();
quic_edge::QuicEdgeService service(
validPresenceOnlyConfig(), std::move(transport), hub);
service.stop();
CHECK_TRUE(service.state() ==
quic_edge::QuicEdgeServiceState::UNINITIALIZED);
std::string error;
CHECK_TRUE(!service.start(&error));
}
{
auto transport = std::make_unique<FakeTransport>();
quic_edge::QuicEdgeService service(
validPresenceOnlyConfig(), std::move(transport), hub);
std::string error;
CHECK_TRUE(service.initialize(&error));
CHECK_TRUE(service.start(&error));
CHECK_TRUE(waitUntil([&]() {
return service.stats().registrations_accepted == 1U;
}));
error.clear();
CHECK_TRUE(!service.initialize(&error));
CHECK_TRUE(!error.empty());
service.stop();
}
return true;
}
bool testRobotIdIsRequired()
{
media::MediaSourceManager hub;
auto config = validPresenceOnlyConfig();
config.clear_robot_id();
auto transport = std::make_unique<FakeTransport>();
quic_edge::QuicEdgeService service(
std::move(config), std::move(transport), hub);
std::string error;
CHECK_TRUE(!service.initialize(&error));
CHECK_TRUE(error.find("robot_id") != std::string::npos);
return true;
}
} // namespace
int main()
{
if (!testControlFraming() || !testPacketizer() ||
!testServiceWithSharedHub() ||
!testMediaActivityInterruptPreservesPresenceAndResumes() ||
!testMediaActivityInterruptCancelsStartingSubscription() ||
!testMissingInjectedSourceRetriesSafely() ||
!testPresenceOnlyWithoutMedia() ||
!testConfiguredHeartbeatIntervalWithoutGatewayOverride() ||
!testHeartbeatTimeoutReconnectsWithoutTaskFailure() ||
!testRegistrationRejectionBacksOff() ||
!testHeartbeatAckRequiresSessionId() ||
!testSlowMediaStartDoesNotBlockHeartbeat() ||
!testLifecycleStateGuards() || !testRobotIdIsRequired()) {
return 1;
}
std::cout << "quic_edge_protocol_test: PASS\n";
return 0;
}

View File

@ -0,0 +1,36 @@
add_library(quic_edge_task STATIC src/quic_edge_task.cpp)
target_compile_features(quic_edge_task PUBLIC cxx_std_17)
target_include_directories(quic_edge_task PUBLIC ${PROJECT_SOURCE_DIR}/cmvr-es)
target_link_libraries(quic_edge_task
PUBLIC
cmvr_es::task
cmvr_es::quic_edge_service
PRIVATE
cmvr_es::proto
cmvr_es::logging
cmvr_es::device_manager
)
add_library(cmvr_es::quic_edge_task ALIAS quic_edge_task)
if(BUILD_TESTING)
add_executable(quic_edge_task_test tests/quic_edge_task_test.cpp)
target_compile_features(quic_edge_task_test PRIVATE cxx_std_17)
target_link_libraries(quic_edge_task_test PRIVATE
cmvr_es::quic_edge_task
)
add_test(NAME quic_edge_task_test COMMAND quic_edge_task_test)
if(UNIX AND NOT APPLE)
get_property(_quic_task_test_library_dirs DIRECTORY PROPERTY LINK_DIRECTORIES)
list(PREPEND _quic_task_test_library_dirs "${CMAKE_BINARY_DIR}/cmvr_compiler_runtime")
list(JOIN _quic_task_test_library_dirs ":" _quic_task_test_library_path)
set(_quic_task_test_environment
"LD_LIBRARY_PATH=${_quic_task_test_library_path}")
if(CMVR_TEST_SYSTEM_LIBSTDCXX)
list(APPEND _quic_task_test_environment
"LD_PRELOAD=${CMVR_TEST_SYSTEM_LIBSTDCXX}")
endif()
set_tests_properties(quic_edge_task_test PROPERTIES
TIMEOUT 10
ENVIRONMENT "${_quic_task_test_environment}")
endif()
endif()

View File

@ -0,0 +1,65 @@
#ifndef CMVR_ES_QUIC_EDGE_TASK_H
#define CMVR_ES_QUIC_EDGE_TASK_H
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "cmvr/config/quic_edge_config/quic_edge_config.pb.h"
#include "service/quic_edge/include/quic_edge_service.h"
#include "task/task.h"
namespace cmvr::task {
class QuicEdgeTask final : public Task {
public:
using ServiceFactory = std::function<std::unique_ptr<quic_edge::QuicEdgeService>(
config::QuicEdgeConfig)>;
explicit QuicEdgeTask(const config::QuicEdgeConfig& config);
QuicEdgeTask(const config::QuicEdgeConfig& config,
ServiceFactory service_factory);
~QuicEdgeTask() override;
const std::string& id() const override { return id_; }
TaskRunMode runMode() const override { return TaskRunMode::BLOCKING_SERVICE; }
bool init() override;
bool start() override;
bool step(double dt) override;
void stop() override;
bool stopActivity();
TaskState state() const override;
bool isBusy() const override;
bool isFinished() const override;
bool isFailed() const override;
std::string stateString() const override;
std::string detailStatusString() const override;
private:
struct PlatformService {
std::string id;
bool media_enabled{false};
config::QuicEdgeConfig config;
std::unique_ptr<quic_edge::QuicEdgeService> service;
};
TaskState mappedState() const;
config::QuicEdgeConfig config_;
std::string id_;
ServiceFactory service_factory_;
std::vector<PlatformService> services_;
mutable std::mutex mutex_;
TaskState state_{TaskState::UNINITIALIZED};
std::string last_error_;
};
void registerQuicEdgeTaskFactory();
} // namespace cmvr::task
#endif // CMVR_ES_QUIC_EDGE_TASK_H

View File

@ -0,0 +1,372 @@
#include "task/quic_edge_task/include/quic_edge_task.h"
#include <set>
#include <sstream>
#include <unordered_set>
#include <utility>
#include <vector>
#include "cmvr/config/task_manager_config/task_manager_config.pb.h"
#include "common/base/logging/logger.h"
#include "common/config/config_files.h"
#include "manager/device_manager/include/device_manager.h"
#include "task/task_factory.h"
namespace cmvr::task {
namespace {
struct PlatformConfig {
std::string id;
bool media_enabled{false};
config::QuicEdgeConfig config;
};
void resolveTlsFiles(config::QuicEdgeTlsConfig* tls)
{
if (!tls) return;
if (!tls->ca_file().empty()) {
tls->set_ca_file(ConfigHelper::resolveConfigFile(tls->ca_file()));
}
if (!tls->certificate_file().empty()) {
tls->set_certificate_file(
ConfigHelper::resolveConfigFile(tls->certificate_file()));
}
if (!tls->private_key_file().empty()) {
tls->set_private_key_file(
ConfigHelper::resolveConfigFile(tls->private_key_file()));
}
}
bool appendPlatformConfig(
const config::QuicEdgeConfig& root_config,
const std::string& platform_id,
const std::string& server_host,
const std::uint32_t server_port,
const config::QuicEdgeTlsConfig& tls,
const bool media_enabled,
std::unordered_set<std::string>* platform_ids,
std::set<std::pair<std::string, std::uint32_t>>* endpoints,
std::vector<PlatformConfig>* platforms,
std::string* error)
{
if (platform_id.empty()) {
if (error) *error = "QUIC edge platform id is empty";
return false;
}
if (!platform_ids->insert(platform_id).second) {
if (error) *error = "duplicate QUIC edge platform id: " + platform_id;
return false;
}
if (!endpoints->emplace(server_host, server_port).second) {
if (error) {
*error = "duplicate QUIC edge platform endpoint: " + server_host +
':' + std::to_string(server_port);
}
return false;
}
PlatformConfig platform;
platform.id = platform_id;
platform.media_enabled = media_enabled;
platform.config = root_config;
platform.config.set_server_host(server_host);
platform.config.set_server_port(server_port);
*platform.config.mutable_tls() = tls;
platform.config.clear_platforms();
if (!media_enabled) platform.config.clear_tracks();
platforms->push_back(std::move(platform));
return true;
}
bool expandPlatformConfigs(const config::QuicEdgeConfig& config,
std::vector<PlatformConfig>* platforms,
std::string* error)
{
if (!platforms) {
if (error) *error = "QUIC edge platform output is null";
return false;
}
platforms->clear();
std::unordered_set<std::string> platform_ids;
std::set<std::pair<std::string, std::uint32_t>> endpoints;
const bool has_primary_endpoint = !config.server_host().empty() ||
config.server_port() != 0U || config.has_tls();
if (has_primary_endpoint &&
!appendPlatformConfig(
config, config.id(), config.server_host(), config.server_port(),
config.tls(), true, &platform_ids, &endpoints, platforms, error)) {
return false;
}
for (const auto& configured_platform : config.platforms()) {
if (!appendPlatformConfig(
config, configured_platform.id(),
configured_platform.server_host(),
configured_platform.server_port(), configured_platform.tls(),
configured_platform.enable_media(), &platform_ids, &endpoints,
platforms, error)) {
return false;
}
}
if (platforms->empty()) {
if (error) *error = "QUIC edge has no platform endpoint";
return false;
}
return true;
}
std::shared_ptr<Task> createQuicEdgeTask(const config::TaskConfigEntry& entry)
{
if (entry.id().empty() || entry.config_file().empty()) {
CMVR_LOG(ERROR) << "[QuicEdgeTask] Task id or config_file is empty";
return nullptr;
}
config::QuicEdgeRootConfig root;
if (!ConfigHelper::loadConfigFile(entry.config_file(), root)) {
CMVR_LOG(ERROR) << "[QuicEdgeTask] Failed to load config: "
<< entry.config_file();
return nullptr;
}
config::QuicEdgeConfig config = root.quic_edge();
if (config.id().empty() || config.id() != entry.id()) {
CMVR_LOG(ERROR) << "[QuicEdgeTask] Task ID mismatch: manager=" << entry.id()
<< ", config=" << config.id();
return nullptr;
}
if (config.has_tls()) resolveTlsFiles(config.mutable_tls());
for (auto& platform : *config.mutable_platforms()) {
if (platform.has_tls()) resolveTlsFiles(platform.mutable_tls());
}
if (config.software_version().empty()) {
config.set_software_version(device::DeviceManager::getInstance().version());
}
return std::make_shared<QuicEdgeTask>(config);
}
} // namespace
QuicEdgeTask::QuicEdgeTask(const config::QuicEdgeConfig& config)
: QuicEdgeTask(
config,
[](config::QuicEdgeConfig platform_config) {
return std::make_unique<quic_edge::QuicEdgeService>(
std::move(platform_config));
})
{
}
QuicEdgeTask::QuicEdgeTask(const config::QuicEdgeConfig& config,
ServiceFactory service_factory)
: config_(config), id_(config.id()),
service_factory_(std::move(service_factory))
{
}
QuicEdgeTask::~QuicEdgeTask()
{
stop();
}
bool QuicEdgeTask::init()
{
std::lock_guard lock(mutex_);
if (id_.empty()) {
last_error_ = "QUIC edge task id is empty";
state_ = TaskState::FAILED;
return false;
}
std::vector<PlatformConfig> platform_configs;
std::string error;
if (!service_factory_) {
last_error_ = "QUIC edge service factory is empty";
state_ = TaskState::FAILED;
return false;
}
if (!expandPlatformConfigs(config_, &platform_configs, &error)) {
last_error_ = std::move(error);
state_ = TaskState::FAILED;
return false;
}
std::vector<PlatformService> initialized_services;
initialized_services.reserve(platform_configs.size());
for (auto& platform_config : platform_configs) {
auto service = service_factory_(platform_config.config);
if (!service) {
last_error_ = "failed to create QUIC edge service for platform " +
platform_config.id;
state_ = TaskState::FAILED;
return false;
}
if (!service->initialize(&error)) {
last_error_ = "platform " + platform_config.id + ": " + error;
state_ = TaskState::FAILED;
return false;
}
initialized_services.push_back(PlatformService{
std::move(platform_config.id), platform_config.media_enabled,
std::move(platform_config.config), std::move(service)});
}
services_ = std::move(initialized_services);
last_error_.clear();
state_ = TaskState::IDLE;
return true;
}
bool QuicEdgeTask::start()
{
std::lock_guard lock(mutex_);
if (state_ == TaskState::RUNNING) {
return true;
}
if ((state_ != TaskState::IDLE && state_ != TaskState::STOPPED) ||
services_.empty()) {
last_error_ = "QUIC edge task is not initialized";
state_ = TaskState::FAILED;
return false;
}
std::size_t started_services = 0U;
for (auto& platform : services_) {
std::string error;
if (!platform.service->start(&error)) {
for (std::size_t index = 0U;
index < started_services; ++index) {
services_[index].service->stop();
}
last_error_ = "platform " + platform.id + ": " + error;
state_ = TaskState::FAILED;
return false;
}
++started_services;
}
last_error_.clear();
state_ = TaskState::RUNNING;
CMVR_LOG(INFO) << "[QuicEdgeTask] Started, id=" << id_
<< ", platforms=" << services_.size();
return true;
}
bool QuicEdgeTask::step(const double dt)
{
(void)dt;
return !isFailed();
}
void QuicEdgeTask::stop()
{
std::lock_guard lock(mutex_);
// stop() is the task lifecycle terminator used by TaskManager shutdown.
// Unlike StopAll's stopActivity(), it intentionally tears down transport.
for (auto& platform : services_) platform.service->stop();
if (state_ != TaskState::FAILED) state_ = TaskState::STOPPED;
}
bool QuicEdgeTask::stopActivity()
{
std::lock_guard lock(mutex_);
if (services_.empty() || state_ == TaskState::FAILED) {
return false;
}
// System StopAll must preserve the QUIC presence channel. Only old media
// subscriptions are fenced; registration and heartbeat stay online.
bool all_stopped = true;
for (auto& platform : services_) {
if (!platform.service->interruptMediaActivities()) {
all_stopped = false;
}
}
return all_stopped;
}
TaskState QuicEdgeTask::mappedState() const
{
if (services_.empty() || state_ != TaskState::RUNNING) return state_;
for (const auto& platform : services_) {
if (platform.service->state() ==
quic_edge::QuicEdgeServiceState::FAILED) {
return TaskState::FAILED;
}
}
return state_;
}
TaskState QuicEdgeTask::state() const
{
std::lock_guard lock(mutex_);
return mappedState();
}
bool QuicEdgeTask::isBusy() const
{
return state() == TaskState::RUNNING;
}
bool QuicEdgeTask::isFinished() const
{
return state() == TaskState::STOPPED;
}
bool QuicEdgeTask::isFailed() const
{
return state() == TaskState::FAILED;
}
std::string QuicEdgeTask::stateString() const
{
return taskStateToString(state());
}
std::string QuicEdgeTask::detailStatusString() const
{
std::lock_guard lock(mutex_);
std::ostringstream output;
output << taskStateToString(mappedState());
if (!services_.empty()) {
output << " platforms=" << services_.size();
for (const auto& platform : services_) {
const auto stats = platform.service->stats();
const auto service_status = platform.service->status();
output << " platform[" << platform.id << "]={"
<< "service=" << quic_edge::toString(platform.service->state())
<< ",target=" << platform.config.server_host() << ':'
<< platform.config.server_port()
<< ",media_enabled=" << platform.media_enabled
<< ",node_id=" << service_status.node_id
<< ",registered=" << service_status.registered
<< ",connections=" << stats.successful_connections
<< ",registrations=" << stats.registrations_accepted
<< ",heartbeat_sequence=" << service_status.heartbeat_sequence
<< ",heartbeat_acks=" << stats.heartbeats_acknowledged
<< ",media_tracks=" << service_status.active_media_tracks
<< ",frames=" << stats.frames_queued
<< ",datagrams=" << stats.datagrams_queued;
if (!service_status.session_id.empty()) {
output << ",session=" << service_status.session_id;
}
if (!service_status.observed_source_ip.empty()) {
output << ",observed_ip=" << service_status.observed_source_ip;
}
if (!service_status.last_media_error.empty()) {
output << ",media_error=" << service_status.last_media_error;
}
const std::string service_error = platform.service->lastError();
if (!service_error.empty()) output << ",error=" << service_error;
output << '}';
}
} else if (!last_error_.empty()) {
output << " error=" << last_error_;
}
return output.str();
}
void registerQuicEdgeTaskFactory()
{
TaskFactory::registerCreator(
config::TaskConfigEntry::TASK_TYPE_QUIC_EDGE,
createQuicEdgeTask);
}
} // namespace cmvr::task

View File

@ -0,0 +1,340 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "cmvr/quic_edge/v1/quic_edge.pb.h"
#include "manager/media_source_hub/include/media_source_hub.h"
#include "service/quic_edge/include/control_framing.h"
#include "task/quic_edge_task/include/quic_edge_task.h"
namespace {
using Clock = std::chrono::steady_clock;
using QuicEdgeService = cmvr::quic_edge::QuicEdgeService;
class HeartbeatTransport final : public cmvr::quic_edge::QuicTransport {
public:
explicit HeartbeatTransport(std::string platform_id)
: platform_id_(std::move(platform_id))
{
}
bool connect(const cmvr::config::QuicEdgeConfig&,
std::chrono::milliseconds,
std::string*) override
{
connected_.store(true);
return true;
}
void disconnect() override
{
connected_.store(false);
condition_.notify_all();
}
bool isConnected() const override { return connected_.load(); }
std::size_t maximumDatagramBytes() const override { return 1200U; }
std::size_t maximumDatagramBatchPackets() const override { return 32U; }
cmvr::quic_edge::TransportSendResult sendControl(
std::vector<std::uint8_t> framed_message,
std::string*) override
{
if (!connected_.load()) {
return cmvr::quic_edge::TransportSendResult::DISCONNECTED;
}
std::vector<std::vector<std::uint8_t>> frames;
std::string error;
cmvr::quic_edge::ControlFrameDecoder decoder(1024U * 1024U);
if (!decoder.push(framed_message, &frames, &error) ||
frames.size() != 1U) {
return cmvr::quic_edge::TransportSendResult::ERROR;
}
cmvr::quic_edge::v1::EdgeControlEnvelope request;
if (!request.ParseFromArray(
frames.front().data(),
static_cast<int>(frames.front().size()))) {
return cmvr::quic_edge::TransportSendResult::ERROR;
}
cmvr::quic_edge::v1::EdgeControlEnvelope response;
response.set_protocol_version(cmvr::quic_edge::kProtocolVersion);
{
std::lock_guard lock(mutex_);
response.set_message_sequence(server_message_sequence_++);
if (request.has_node_register_request()) {
++registrations_;
auto* registration = response.mutable_node_register_response();
registration->set_accepted(true);
registration->set_session_id("session-" + platform_id_);
registration->set_heartbeat_interval_ms(250U);
} else if (request.has_node_heartbeat()) {
const auto& heartbeat = request.node_heartbeat();
++heartbeats_;
last_heartbeat_sequence_.store(heartbeat.sequence());
auto* ack = response.mutable_node_heartbeat_ack();
ack->set_accepted(true);
ack->set_session_id(heartbeat.session_id());
ack->set_acknowledged_sequence(heartbeat.sequence());
} else {
return cmvr::quic_edge::TransportSendResult::ERROR;
}
if (!enqueueResponse(response)) {
return cmvr::quic_edge::TransportSendResult::ERROR;
}
}
condition_.notify_all();
return cmvr::quic_edge::TransportSendResult::QUEUED;
}
cmvr::quic_edge::TransportReceiveResult receiveControl(
std::vector<std::uint8_t>* chunk,
const std::chrono::milliseconds timeout,
std::string*) override
{
if (!chunk) return cmvr::quic_edge::TransportReceiveResult::ERROR;
std::unique_lock lock(mutex_);
condition_.wait_for(lock, timeout, [this] {
return !responses_.empty() || !connected_.load();
});
if (!responses_.empty()) {
*chunk = std::move(responses_.front());
responses_.pop_front();
return cmvr::quic_edge::TransportReceiveResult::DATA;
}
return connected_.load()
? cmvr::quic_edge::TransportReceiveResult::TIMEOUT
: cmvr::quic_edge::TransportReceiveResult::DISCONNECTED;
}
cmvr::quic_edge::TransportSendResult sendDatagramBatch(
std::vector<cmvr::quic_edge::DatagramPacket>,
std::string*) override
{
return connected_.load()
? cmvr::quic_edge::TransportSendResult::QUEUED
: cmvr::quic_edge::TransportSendResult::DISCONNECTED;
}
std::uint64_t registrations() const { return registrations_.load(); }
std::uint64_t heartbeats() const { return heartbeats_.load(); }
std::uint64_t lastHeartbeatSequence() const
{
return last_heartbeat_sequence_.load();
}
private:
bool enqueueResponse(
const cmvr::quic_edge::v1::EdgeControlEnvelope& response)
{
std::string serialized;
if (!response.SerializeToString(&serialized)) return false;
std::vector<std::uint8_t> framed;
std::string error;
if (!cmvr::quic_edge::ControlFrameEncoder::encode(
reinterpret_cast<const std::uint8_t*>(serialized.data()),
serialized.size(), 1024U * 1024U, &framed, &error)) {
return false;
}
responses_.push_back(std::move(framed));
return true;
}
std::string platform_id_;
std::atomic<bool> connected_{false};
std::atomic<std::uint64_t> registrations_{0U};
std::atomic<std::uint64_t> heartbeats_{0U};
std::atomic<std::uint64_t> last_heartbeat_sequence_{0U};
mutable std::mutex mutex_;
std::condition_variable condition_;
std::deque<std::vector<std::uint8_t>> responses_;
std::uint64_t server_message_sequence_{0U};
};
template <typename Predicate>
bool waitUntil(const std::chrono::milliseconds timeout, Predicate predicate)
{
const auto deadline = Clock::now() + timeout;
while (Clock::now() < deadline) {
if (predicate()) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return predicate();
}
cmvr::config::QuicEdgeConfig validConfig()
{
cmvr::config::QuicEdgeConfig config;
config.set_id("quic-admission-test");
config.set_server_host("127.0.0.1");
config.set_server_port(4433U);
config.set_alpn("cmvr-quic-edge/1");
config.set_node_id("test-node");
config.set_robot_id("test-robot");
config.set_software_version("test-version");
config.set_grpc_endpoint_host("127.0.0.1");
config.set_grpc_endpoint_port(50052U);
config.set_heartbeat_interval_ms(250U);
config.set_control_response_timeout_ms(100U);
config.mutable_tls()->set_allow_insecure(true);
config.mutable_reconnect()->set_initial_delay_ms(5U);
config.mutable_reconnect()->set_maximum_delay_ms(20U);
config.mutable_reconnect()->set_multiplier(2.0);
config.mutable_reconnect()->set_connect_timeout_ms(20U);
config.set_maximum_datagram_bytes(1200U);
config.set_maximum_control_frame_bytes(4096U);
config.set_maximum_frame_bytes(32U * 1024U);
config.set_datagram_send_queue_depth(32U);
config.set_media_poll_interval_ms(1U);
return config;
}
cmvr::config::QuicEdgeConfig multiPlatformConfig()
{
auto config = validConfig();
config.set_id("quic-multi-platform-test");
config.clear_server_host();
config.clear_server_port();
config.clear_tls();
auto* platform_a = config.add_platforms();
platform_a->set_id("platform-a");
platform_a->set_server_host("192.0.2.10");
platform_a->set_server_port(4433U);
platform_a->mutable_tls()->set_allow_insecure(true);
auto* platform_b = config.add_platforms();
platform_b->set_id("platform-b");
platform_b->set_server_host("198.51.100.20");
platform_b->set_server_port(4434U);
platform_b->mutable_tls()->set_allow_insecure(true);
platform_b->set_enable_media(true);
auto* disabled_track = config.add_tracks();
disabled_track->set_track_id(1U);
disabled_track->set_source_kind(
cmvr::config::QuicEdgeTrackConfig::SOURCE_KIND_CAMERA);
disabled_track->set_device_id("disabled-test-camera");
disabled_track->set_enable(false);
return config;
}
} // namespace
int main()
{
cmvr::config::QuicEdgeConfig config;
config.set_id("quic-invalid-config-test");
cmvr::task::QuicEdgeTask task(config);
if (task.init() || task.state() != cmvr::task::TaskState::FAILED) {
std::cerr << "invalid QUIC task config was not rejected\n";
return 1;
}
task.stop();
if (task.state() != cmvr::task::TaskState::FAILED) {
std::cerr << "failed QUIC task did not preserve its failure state\n";
return 1;
}
if (task.stopActivity()) {
std::cerr << "failed QUIC task reported a confirmed activity stop\n";
return 1;
}
cmvr::media::MediaSourceHub media_hub;
std::vector<HeartbeatTransport*> transports;
std::vector<QuicEdgeService*> services;
std::vector<cmvr::config::QuicEdgeConfig> expanded_configs;
cmvr::task::QuicEdgeTask multi_platform_task(
multiPlatformConfig(),
[&](cmvr::config::QuicEdgeConfig platform_config) {
expanded_configs.push_back(platform_config);
auto transport = std::make_unique<HeartbeatTransport>(
platform_config.server_host());
transports.push_back(transport.get());
auto service = std::make_unique<QuicEdgeService>(
std::move(platform_config), std::move(transport), media_hub);
services.push_back(service.get());
return service;
});
if (!multi_platform_task.init() || expanded_configs.size() != 2U ||
transports.size() != 2U || services.size() != 2U) {
std::cerr << "multi-platform QUIC task did not create two services: "
<< multi_platform_task.detailStatusString() << '\n';
return 1;
}
if (expanded_configs[0].platforms_size() != 0 ||
expanded_configs[1].platforms_size() != 0 ||
expanded_configs[0].server_host() != "192.0.2.10" ||
expanded_configs[1].server_host() != "198.51.100.20" ||
expanded_configs[0].tracks_size() != 0 ||
expanded_configs[1].tracks_size() != 1) {
std::cerr << "multi-platform QUIC task expanded invalid service configs\n";
return 1;
}
if (!multi_platform_task.start()) {
std::cerr << "multi-platform QUIC task did not start: "
<< multi_platform_task.detailStatusString() << '\n';
return 1;
}
const bool both_platforms_online = waitUntil(
std::chrono::seconds(2), [&] {
return transports[0]->registrations() >= 1U &&
transports[1]->registrations() >= 1U &&
transports[0]->heartbeats() >= 1U &&
transports[1]->heartbeats() >= 1U &&
services[0]->stats().heartbeats_acknowledged >= 1U &&
services[1]->stats().heartbeats_acknowledged >= 1U;
});
if (!both_platforms_online ||
transports[0]->lastHeartbeatSequence() != 1U ||
transports[1]->lastHeartbeatSequence() != 1U) {
std::cerr << "both platforms did not independently ACK a heartbeat: "
<< multi_platform_task.detailStatusString() << '\n';
return 1;
}
const std::string multi_status = multi_platform_task.detailStatusString();
if (multi_status.find("platforms=2") == std::string::npos ||
multi_status.find("platform[platform-a]") == std::string::npos ||
multi_status.find("platform[platform-b]") == std::string::npos) {
std::cerr << "multi-platform QUIC status is incomplete: "
<< multi_status << '\n';
return 1;
}
multi_platform_task.stop();
if (multi_platform_task.state() != cmvr::task::TaskState::STOPPED) {
std::cerr << "multi-platform QUIC task did not stop cleanly\n";
return 1;
}
auto duplicate_endpoint_config = validConfig();
auto* duplicate_platform = duplicate_endpoint_config.add_platforms();
duplicate_platform->set_id("duplicate-primary");
duplicate_platform->set_server_host(
duplicate_endpoint_config.server_host());
duplicate_platform->set_server_port(
duplicate_endpoint_config.server_port());
duplicate_platform->mutable_tls()->set_allow_insecure(true);
cmvr::task::QuicEdgeTask duplicate_endpoint_task(
duplicate_endpoint_config);
if (duplicate_endpoint_task.init() ||
duplicate_endpoint_task.detailStatusString().find(
"duplicate QUIC edge platform endpoint") == std::string::npos) {
std::cerr << "duplicate QUIC platform endpoint was not rejected\n";
return 1;
}
std::cout << "quic_edge_task_test: PASS\n";
return 0;
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,36 @@
/*
* AC-3 parser prototypes
* Copyright (c) 2003 Fabrice Bellard
* Copyright (c) 2003 Michael Niedermayer
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_AC3_PARSER_H
#define AVCODEC_AC3_PARSER_H
#include <stddef.h>
#include <stdint.h>
/**
* Extract the bitstream ID and the frame size from AC-3 data.
*/
int av_ac3_parse_header(const uint8_t *buf, size_t size,
uint8_t *bitstream_id, uint16_t *frame_size);
#endif /* AVCODEC_AC3_PARSER_H */

View File

@ -0,0 +1,37 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_ADTS_PARSER_H
#define AVCODEC_ADTS_PARSER_H
#include <stddef.h>
#include <stdint.h>
#define AV_AAC_ADTS_HEADER_SIZE 7
/**
* Extract the number of samples and frames from AAC data.
* @param[in] buf pointer to AAC data buffer
* @param[out] samples Pointer to where number of samples is written
* @param[out] frames Pointer to where number of frames is written
* @return Returns 0 on success, error code on failure.
*/
int av_adts_header_parse(const uint8_t *buf, uint32_t *samples,
uint8_t *frames);
#endif /* AVCODEC_ADTS_PARSER_H */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_AVDCT_H
#define AVCODEC_AVDCT_H
#include "libavutil/opt.h"
/**
* AVDCT context.
* @note function pointers can be NULL if the specific features have been
* disabled at build time.
*/
typedef struct AVDCT {
const AVClass *av_class;
void (*idct)(int16_t *block /* align 16 */);
/**
* IDCT input permutation.
* Several optimized IDCTs need a permutated input (relative to the
* normal order of the reference IDCT).
* This permutation must be performed before the idct_put/add.
* Note, normally this can be merged with the zigzag/alternate scan<br>
* An example to avoid confusion:
* - (->decode coeffs -> zigzag reorder -> dequant -> reference IDCT -> ...)
* - (x -> reference DCT -> reference IDCT -> x)
* - (x -> reference DCT -> simple_mmx_perm = idct_permutation
* -> simple_idct_mmx -> x)
* - (-> decode coeffs -> zigzag reorder -> simple_mmx_perm -> dequant
* -> simple_idct_mmx -> ...)
*/
uint8_t idct_permutation[64];
void (*fdct)(int16_t *block /* align 16 */);
/**
* DCT algorithm.
* must use AVOptions to set this field.
*/
int dct_algo;
/**
* IDCT algorithm.
* must use AVOptions to set this field.
*/
int idct_algo;
void (*get_pixels)(int16_t *block /* align 16 */,
const uint8_t *pixels /* align 8 */,
ptrdiff_t line_size);
int bits_per_sample;
void (*get_pixels_unaligned)(int16_t *block /* align 16 */,
const uint8_t *pixels,
ptrdiff_t line_size);
} AVDCT;
/**
* Allocates a AVDCT context.
* This needs to be initialized with avcodec_dct_init() after optionally
* configuring it with AVOptions.
*
* To free it use av_free()
*/
AVDCT *avcodec_dct_alloc(void);
int avcodec_dct_init(AVDCT *);
const AVClass *avcodec_dct_get_class(void);
#endif /* AVCODEC_AVDCT_H */

View File

@ -0,0 +1,149 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_AVFFT_H
#define AVCODEC_AVFFT_H
#include "libavutil/attributes.h"
#include "version_major.h"
#if FF_API_AVFFT
/**
* @file
* @ingroup lavc_fft
* FFT functions
*/
/**
* @defgroup lavc_fft FFT functions
* @ingroup lavc_misc
*
* @{
*/
typedef float FFTSample;
typedef struct FFTComplex {
FFTSample re, im;
} FFTComplex;
typedef struct FFTContext FFTContext;
/**
* Set up a complex FFT.
* @param nbits log2 of the length of the input array
* @param inverse if 0 perform the forward transform, if 1 perform the inverse
* @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_FFT
*/
attribute_deprecated
FFTContext *av_fft_init(int nbits, int inverse);
/**
* Do the permutation needed BEFORE calling ff_fft_calc().
* @deprecated without replacement
*/
attribute_deprecated
void av_fft_permute(FFTContext *s, FFTComplex *z);
/**
* Do a complex FFT with the parameters defined in av_fft_init(). The
* input data must be permuted before. No 1.0/sqrt(n) normalization is done.
* @deprecated use the av_tx_fn value returned by av_tx_init, which also does permutation
*/
attribute_deprecated
void av_fft_calc(FFTContext *s, FFTComplex *z);
attribute_deprecated
void av_fft_end(FFTContext *s);
/**
* @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_MDCT,
* with a flag of AV_TX_FULL_IMDCT for a replacement to av_imdct_calc.
*/
attribute_deprecated
FFTContext *av_mdct_init(int nbits, int inverse, double scale);
attribute_deprecated
void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
attribute_deprecated
void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
attribute_deprecated
void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
attribute_deprecated
void av_mdct_end(FFTContext *s);
/* Real Discrete Fourier Transform */
enum RDFTransformType {
DFT_R2C,
IDFT_C2R,
IDFT_R2C,
DFT_C2R,
};
typedef struct RDFTContext RDFTContext;
/**
* Set up a real FFT.
* @param nbits log2 of the length of the input array
* @param trans the type of transform
*
* @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_RDFT
*/
attribute_deprecated
RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
attribute_deprecated
void av_rdft_calc(RDFTContext *s, FFTSample *data);
attribute_deprecated
void av_rdft_end(RDFTContext *s);
/* Discrete Cosine Transform */
typedef struct DCTContext DCTContext;
enum DCTTransformType {
DCT_II = 0,
DCT_III,
DCT_I,
DST_I,
};
/**
* Set up DCT.
*
* @param nbits size of the input array:
* (1 << nbits) for DCT-II, DCT-III and DST-I
* (1 << nbits) + 1 for DCT-I
* @param type the type of transform
*
* @note the first element of the input of DST-I is ignored
*
* @deprecated use av_tx_init from libavutil/tx.h with an appropriate type of AV_TX_FLOAT_DCT
*/
attribute_deprecated
DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
attribute_deprecated
void av_dct_calc(DCTContext *s, FFTSample *data);
attribute_deprecated
void av_dct_end (DCTContext *s);
/**
* @}
*/
#endif /* FF_API_AVFFT */
#endif /* AVCODEC_AVFFT_H */

View File

@ -0,0 +1,332 @@
/*
* Bitstream filters public API
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_BSF_H
#define AVCODEC_BSF_H
#include "libavutil/dict.h"
#include "libavutil/log.h"
#include "libavutil/rational.h"
#include "codec_id.h"
#include "codec_par.h"
#include "packet.h"
/**
* @defgroup lavc_bsf Bitstream filters
* @ingroup libavc
*
* Bitstream filters transform encoded media data without decoding it. This
* allows e.g. manipulating various header values. Bitstream filters operate on
* @ref AVPacket "AVPackets".
*
* The bitstream filtering API is centered around two structures:
* AVBitStreamFilter and AVBSFContext. The former represents a bitstream filter
* in abstract, the latter a specific filtering process. Obtain an
* AVBitStreamFilter using av_bsf_get_by_name() or av_bsf_iterate(), then pass
* it to av_bsf_alloc() to create an AVBSFContext. Fill in the user-settable
* AVBSFContext fields, as described in its documentation, then call
* av_bsf_init() to prepare the filter context for use.
*
* Submit packets for filtering using av_bsf_send_packet(), obtain filtered
* results with av_bsf_receive_packet(). When no more input packets will be
* sent, submit a NULL AVPacket to signal the end of the stream to the filter.
* av_bsf_receive_packet() will then return trailing packets, if any are
* produced by the filter.
*
* Finally, free the filter context with av_bsf_free().
* @{
*/
/**
* The bitstream filter state.
*
* This struct must be allocated with av_bsf_alloc() and freed with
* av_bsf_free().
*
* The fields in the struct will only be changed (by the caller or by the
* filter) as described in their documentation, and are to be considered
* immutable otherwise.
*/
typedef struct AVBSFContext {
/**
* A class for logging and AVOptions
*/
const AVClass *av_class;
/**
* The bitstream filter this context is an instance of.
*/
const struct AVBitStreamFilter *filter;
/**
* Opaque filter-specific private data. If filter->priv_class is non-NULL,
* this is an AVOptions-enabled struct.
*/
void *priv_data;
/**
* Parameters of the input stream. This field is allocated in
* av_bsf_alloc(), it needs to be filled by the caller before
* av_bsf_init().
*/
AVCodecParameters *par_in;
/**
* Parameters of the output stream. This field is allocated in
* av_bsf_alloc(), it is set by the filter in av_bsf_init().
*/
AVCodecParameters *par_out;
/**
* The timebase used for the timestamps of the input packets. Set by the
* caller before av_bsf_init().
*/
AVRational time_base_in;
/**
* The timebase used for the timestamps of the output packets. Set by the
* filter in av_bsf_init().
*/
AVRational time_base_out;
} AVBSFContext;
typedef struct AVBitStreamFilter {
const char *name;
/**
* A list of codec ids supported by the filter, terminated by
* AV_CODEC_ID_NONE.
* May be NULL, in that case the bitstream filter works with any codec id.
*/
const enum AVCodecID *codec_ids;
/**
* A class for the private data, used to declare bitstream filter private
* AVOptions. This field is NULL for bitstream filters that do not declare
* any options.
*
* If this field is non-NULL, the first member of the filter private data
* must be a pointer to AVClass, which will be set by libavcodec generic
* code to this class.
*/
const AVClass *priv_class;
} AVBitStreamFilter;
/**
* @return a bitstream filter with the specified name or NULL if no such
* bitstream filter exists.
*/
const AVBitStreamFilter *av_bsf_get_by_name(const char *name);
/**
* Iterate over all registered bitstream filters.
*
* @param opaque a pointer where libavcodec will store the iteration state. Must
* point to NULL to start the iteration.
*
* @return the next registered bitstream filter or NULL when the iteration is
* finished
*/
const AVBitStreamFilter *av_bsf_iterate(void **opaque);
/**
* Allocate a context for a given bitstream filter. The caller must fill in the
* context parameters as described in the documentation and then call
* av_bsf_init() before sending any data to the filter.
*
* @param filter the filter for which to allocate an instance.
* @param[out] ctx a pointer into which the pointer to the newly-allocated context
* will be written. It must be freed with av_bsf_free() after the
* filtering is done.
*
* @return 0 on success, a negative AVERROR code on failure
*/
int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx);
/**
* Prepare the filter for use, after all the parameters and options have been
* set.
*
* @param ctx a AVBSFContext previously allocated with av_bsf_alloc()
*/
int av_bsf_init(AVBSFContext *ctx);
/**
* Submit a packet for filtering.
*
* After sending each packet, the filter must be completely drained by calling
* av_bsf_receive_packet() repeatedly until it returns AVERROR(EAGAIN) or
* AVERROR_EOF.
*
* @param ctx an initialized AVBSFContext
* @param pkt the packet to filter. The bitstream filter will take ownership of
* the packet and reset the contents of pkt. pkt is not touched if an error occurs.
* If pkt is empty (i.e. NULL, or pkt->data is NULL and pkt->side_data_elems zero),
* it signals the end of the stream (i.e. no more non-empty packets will be sent;
* sending more empty packets does nothing) and will cause the filter to output
* any packets it may have buffered internally.
*
* @return
* - 0 on success.
* - AVERROR(EAGAIN) if packets need to be retrieved from the filter (using
* av_bsf_receive_packet()) before new input can be consumed.
* - Another negative AVERROR value if an error occurs.
*/
int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
/**
* Retrieve a filtered packet.
*
* @param ctx an initialized AVBSFContext
* @param[out] pkt this struct will be filled with the contents of the filtered
* packet. It is owned by the caller and must be freed using
* av_packet_unref() when it is no longer needed.
* This parameter should be "clean" (i.e. freshly allocated
* with av_packet_alloc() or unreffed with av_packet_unref())
* when this function is called. If this function returns
* successfully, the contents of pkt will be completely
* overwritten by the returned data. On failure, pkt is not
* touched.
*
* @return
* - 0 on success.
* - AVERROR(EAGAIN) if more packets need to be sent to the filter (using
* av_bsf_send_packet()) to get more output.
* - AVERROR_EOF if there will be no further output from the filter.
* - Another negative AVERROR value if an error occurs.
*
* @note one input packet may result in several output packets, so after sending
* a packet with av_bsf_send_packet(), this function needs to be called
* repeatedly until it stops returning 0. It is also possible for a filter to
* output fewer packets than were sent to it, so this function may return
* AVERROR(EAGAIN) immediately after a successful av_bsf_send_packet() call.
*/
int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
/**
* Reset the internal bitstream filter state. Should be called e.g. when seeking.
*/
void av_bsf_flush(AVBSFContext *ctx);
/**
* Free a bitstream filter context and everything associated with it; write NULL
* into the supplied pointer.
*/
void av_bsf_free(AVBSFContext **ctx);
/**
* Get the AVClass for AVBSFContext. It can be used in combination with
* AV_OPT_SEARCH_FAKE_OBJ for examining options.
*
* @see av_opt_find().
*/
const AVClass *av_bsf_get_class(void);
/**
* Structure for chain/list of bitstream filters.
* Empty list can be allocated by av_bsf_list_alloc().
*/
typedef struct AVBSFList AVBSFList;
/**
* Allocate empty list of bitstream filters.
* The list must be later freed by av_bsf_list_free()
* or finalized by av_bsf_list_finalize().
*
* @return Pointer to @ref AVBSFList on success, NULL in case of failure
*/
AVBSFList *av_bsf_list_alloc(void);
/**
* Free list of bitstream filters.
*
* @param lst Pointer to pointer returned by av_bsf_list_alloc()
*/
void av_bsf_list_free(AVBSFList **lst);
/**
* Append bitstream filter to the list of bitstream filters.
*
* @param lst List to append to
* @param bsf Filter context to be appended
*
* @return >=0 on success, negative AVERROR in case of failure
*/
int av_bsf_list_append(AVBSFList *lst, AVBSFContext *bsf);
/**
* Construct new bitstream filter context given it's name and options
* and append it to the list of bitstream filters.
*
* @param lst List to append to
* @param bsf_name Name of the bitstream filter
* @param options Options for the bitstream filter, can be set to NULL
*
* @return >=0 on success, negative AVERROR in case of failure
*/
int av_bsf_list_append2(AVBSFList *lst, const char * bsf_name, AVDictionary **options);
/**
* Finalize list of bitstream filters.
*
* This function will transform @ref AVBSFList to single @ref AVBSFContext,
* so the whole chain of bitstream filters can be treated as single filter
* freshly allocated by av_bsf_alloc().
* If the call is successful, @ref AVBSFList structure is freed and lst
* will be set to NULL. In case of failure, caller is responsible for
* freeing the structure by av_bsf_list_free()
*
* @param lst Filter list structure to be transformed
* @param[out] bsf Pointer to be set to newly created @ref AVBSFContext structure
* representing the chain of bitstream filters
*
* @return >=0 on success, negative AVERROR in case of failure
*/
int av_bsf_list_finalize(AVBSFList **lst, AVBSFContext **bsf);
/**
* Parse string describing list of bitstream filters and create single
* @ref AVBSFContext describing the whole chain of bitstream filters.
* Resulting @ref AVBSFContext can be treated as any other @ref AVBSFContext freshly
* allocated by av_bsf_alloc().
*
* @param str String describing chain of bitstream filters in format
* `bsf1[=opt1=val1:opt2=val2][,bsf2]`
* @param[out] bsf Pointer to be set to newly created @ref AVBSFContext structure
* representing the chain of bitstream filters
*
* @return >=0 on success, negative AVERROR in case of failure
*/
int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf);
/**
* Get null/pass-through bitstream filter.
*
* @param[out] bsf Pointer to be set to new instance of pass-through bitstream filter
*
* @return
*/
int av_bsf_get_null_filter(AVBSFContext **bsf);
/**
* @}
*/
#endif // AVCODEC_BSF_H

View File

@ -0,0 +1,378 @@
/*
* AVCodec public API
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_CODEC_H
#define AVCODEC_CODEC_H
#include <stdint.h>
#include "libavutil/avutil.h"
#include "libavutil/hwcontext.h"
#include "libavutil/log.h"
#include "libavutil/pixfmt.h"
#include "libavutil/rational.h"
#include "libavutil/samplefmt.h"
#include "libavcodec/codec_id.h"
#include "libavcodec/version_major.h"
/**
* @addtogroup lavc_core
* @{
*/
/**
* Decoder can use draw_horiz_band callback.
*/
#define AV_CODEC_CAP_DRAW_HORIZ_BAND (1 << 0)
/**
* Codec uses get_buffer() or get_encode_buffer() for allocating buffers and
* supports custom allocators.
* If not set, it might not use get_buffer() or get_encode_buffer() at all, or
* use operations that assume the buffer was allocated by
* avcodec_default_get_buffer2 or avcodec_default_get_encode_buffer.
*/
#define AV_CODEC_CAP_DR1 (1 << 1)
/**
* Encoder or decoder requires flushing with NULL input at the end in order to
* give the complete and correct output.
*
* NOTE: If this flag is not set, the codec is guaranteed to never be fed with
* with NULL data. The user can still send NULL data to the public encode
* or decode function, but libavcodec will not pass it along to the codec
* unless this flag is set.
*
* Decoders:
* The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
* avpkt->size=0 at the end to get the delayed data until the decoder no longer
* returns frames.
*
* Encoders:
* The encoder needs to be fed with NULL data at the end of encoding until the
* encoder no longer returns data.
*
* NOTE: For encoders implementing the AVCodec.encode2() function, setting this
* flag also means that the encoder must set the pts and duration for
* each output packet. If this flag is not set, the pts and duration will
* be determined by libavcodec from the input frame.
*/
#define AV_CODEC_CAP_DELAY (1 << 5)
/**
* Codec can be fed a final frame with a smaller size.
* This can be used to prevent truncation of the last audio samples.
*/
#define AV_CODEC_CAP_SMALL_LAST_FRAME (1 << 6)
#if FF_API_SUBFRAMES
/**
* Codec can output multiple frames per AVPacket
* Normally demuxers return one frame at a time, demuxers which do not do
* are connected to a parser to split what they return into proper frames.
* This flag is reserved to the very rare category of codecs which have a
* bitstream that cannot be split into frames without timeconsuming
* operations like full decoding. Demuxers carrying such bitstreams thus
* may return multiple frames in a packet. This has many disadvantages like
* prohibiting stream copy in many cases thus it should only be considered
* as a last resort.
*/
#define AV_CODEC_CAP_SUBFRAMES (1 << 8)
#endif
/**
* Codec is experimental and is thus avoided in favor of non experimental
* encoders
*/
#define AV_CODEC_CAP_EXPERIMENTAL (1 << 9)
/**
* Codec should fill in channel configuration and samplerate instead of container
*/
#define AV_CODEC_CAP_CHANNEL_CONF (1 << 10)
/**
* Codec supports frame-level multithreading.
*/
#define AV_CODEC_CAP_FRAME_THREADS (1 << 12)
/**
* Codec supports slice-based (or partition-based) multithreading.
*/
#define AV_CODEC_CAP_SLICE_THREADS (1 << 13)
/**
* Codec supports changed parameters at any point.
*/
#define AV_CODEC_CAP_PARAM_CHANGE (1 << 14)
/**
* Codec supports multithreading through a method other than slice- or
* frame-level multithreading. Typically this marks wrappers around
* multithreading-capable external libraries.
*/
#define AV_CODEC_CAP_OTHER_THREADS (1 << 15)
/**
* Audio encoder supports receiving a different number of samples in each call.
*/
#define AV_CODEC_CAP_VARIABLE_FRAME_SIZE (1 << 16)
/**
* Decoder is not a preferred choice for probing.
* This indicates that the decoder is not a good choice for probing.
* It could for example be an expensive to spin up hardware decoder,
* or it could simply not provide a lot of useful information about
* the stream.
* A decoder marked with this flag should only be used as last resort
* choice for probing.
*/
#define AV_CODEC_CAP_AVOID_PROBING (1 << 17)
/**
* Codec is backed by a hardware implementation. Typically used to
* identify a non-hwaccel hardware decoder. For information about hwaccels, use
* avcodec_get_hw_config() instead.
*/
#define AV_CODEC_CAP_HARDWARE (1 << 18)
/**
* Codec is potentially backed by a hardware implementation, but not
* necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the
* implementation provides some sort of internal fallback.
*/
#define AV_CODEC_CAP_HYBRID (1 << 19)
/**
* This encoder can reorder user opaque values from input AVFrames and return
* them with corresponding output packets.
* @see AV_CODEC_FLAG_COPY_OPAQUE
*/
#define AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE (1 << 20)
/**
* This encoder can be flushed using avcodec_flush_buffers(). If this flag is
* not set, the encoder must be closed and reopened to ensure that no frames
* remain pending.
*/
#define AV_CODEC_CAP_ENCODER_FLUSH (1 << 21)
/**
* The encoder is able to output reconstructed frame data, i.e. raw frames that
* would be produced by decoding the encoded bitstream.
*
* Reconstructed frame output is enabled by the AV_CODEC_FLAG_RECON_FRAME flag.
*/
#define AV_CODEC_CAP_ENCODER_RECON_FRAME (1 << 22)
/**
* AVProfile.
*/
typedef struct AVProfile {
int profile;
const char *name; ///< short name for the profile
} AVProfile;
/**
* AVCodec.
*/
typedef struct AVCodec {
/**
* Name of the codec implementation.
* The name is globally unique among encoders and among decoders (but an
* encoder and a decoder can share the same name).
* This is the primary way to find a codec from the user perspective.
*/
const char *name;
/**
* Descriptive name for the codec, meant to be more human readable than name.
* You should use the NULL_IF_CONFIG_SMALL() macro to define it.
*/
const char *long_name;
enum AVMediaType type;
enum AVCodecID id;
/**
* Codec capabilities.
* see AV_CODEC_CAP_*
*/
int capabilities;
uint8_t max_lowres; ///< maximum value for lowres supported by the decoder
const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
#if FF_API_OLD_CHANNEL_LAYOUT
/**
* @deprecated use ch_layouts instead
*/
attribute_deprecated
const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0
#endif
const AVClass *priv_class; ///< AVClass for the private context
const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {AV_PROFILE_UNKNOWN}
/**
* Group name of the codec implementation.
* This is a short symbolic name of the wrapper backing this codec. A
* wrapper uses some kind of external implementation for the codec, such
* as an external library, or a codec implementation provided by the OS or
* the hardware.
* If this field is NULL, this is a builtin, libavcodec native codec.
* If non-NULL, this will be the suffix in AVCodec.name in most cases
* (usually AVCodec.name will be of the form "<codec_name>_<wrapper_name>").
*/
const char *wrapper_name;
/**
* Array of supported channel layouts, terminated with a zeroed layout.
*/
const AVChannelLayout *ch_layouts;
} AVCodec;
/**
* Iterate over all registered codecs.
*
* @param opaque a pointer where libavcodec will store the iteration state. Must
* point to NULL to start the iteration.
*
* @return the next registered codec or NULL when the iteration is
* finished
*/
const AVCodec *av_codec_iterate(void **opaque);
/**
* Find a registered decoder with a matching codec ID.
*
* @param id AVCodecID of the requested decoder
* @return A decoder if one was found, NULL otherwise.
*/
const AVCodec *avcodec_find_decoder(enum AVCodecID id);
/**
* Find a registered decoder with the specified name.
*
* @param name name of the requested decoder
* @return A decoder if one was found, NULL otherwise.
*/
const AVCodec *avcodec_find_decoder_by_name(const char *name);
/**
* Find a registered encoder with a matching codec ID.
*
* @param id AVCodecID of the requested encoder
* @return An encoder if one was found, NULL otherwise.
*/
const AVCodec *avcodec_find_encoder(enum AVCodecID id);
/**
* Find a registered encoder with the specified name.
*
* @param name name of the requested encoder
* @return An encoder if one was found, NULL otherwise.
*/
const AVCodec *avcodec_find_encoder_by_name(const char *name);
/**
* @return a non-zero number if codec is an encoder, zero otherwise
*/
int av_codec_is_encoder(const AVCodec *codec);
/**
* @return a non-zero number if codec is a decoder, zero otherwise
*/
int av_codec_is_decoder(const AVCodec *codec);
/**
* Return a name for the specified profile, if available.
*
* @param codec the codec that is searched for the given profile
* @param profile the profile value for which a name is requested
* @return A name for the profile if found, NULL otherwise.
*/
const char *av_get_profile_name(const AVCodec *codec, int profile);
enum {
/**
* The codec supports this format via the hw_device_ctx interface.
*
* When selecting this format, AVCodecContext.hw_device_ctx should
* have been set to a device of the specified type before calling
* avcodec_open2().
*/
AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01,
/**
* The codec supports this format via the hw_frames_ctx interface.
*
* When selecting this format for a decoder,
* AVCodecContext.hw_frames_ctx should be set to a suitable frames
* context inside the get_format() callback. The frames context
* must have been created on a device of the specified type.
*
* When selecting this format for an encoder,
* AVCodecContext.hw_frames_ctx should be set to the context which
* will be used for the input frames before calling avcodec_open2().
*/
AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX = 0x02,
/**
* The codec supports this format by some internal method.
*
* This format can be selected without any additional configuration -
* no device or frames context is required.
*/
AV_CODEC_HW_CONFIG_METHOD_INTERNAL = 0x04,
/**
* The codec supports this format by some ad-hoc method.
*
* Additional settings and/or function calls are required. See the
* codec-specific documentation for details. (Methods requiring
* this sort of configuration are deprecated and others should be
* used in preference.)
*/
AV_CODEC_HW_CONFIG_METHOD_AD_HOC = 0x08,
};
typedef struct AVCodecHWConfig {
/**
* For decoders, a hardware pixel format which that decoder may be
* able to decode to if suitable hardware is available.
*
* For encoders, a pixel format which the encoder may be able to
* accept. If set to AV_PIX_FMT_NONE, this applies to all pixel
* formats supported by the codec.
*/
enum AVPixelFormat pix_fmt;
/**
* Bit set of AV_CODEC_HW_CONFIG_METHOD_* flags, describing the possible
* setup methods which can be used with this configuration.
*/
int methods;
/**
* The device type associated with the configuration.
*
* Must be set for AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX and
* AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, otherwise unused.
*/
enum AVHWDeviceType device_type;
} AVCodecHWConfig;
/**
* Retrieve supported hardware configurations for a codec.
*
* Values of index from zero to some maximum return the indexed configuration
* descriptor; all other values return NULL. If the codec does not support
* any hardware configurations then it will always return NULL.
*/
const AVCodecHWConfig *avcodec_get_hw_config(const AVCodec *codec, int index);
/**
* @}
*/
#endif /* AVCODEC_CODEC_H */

View File

@ -0,0 +1,134 @@
/*
* Codec descriptors public API
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_CODEC_DESC_H
#define AVCODEC_CODEC_DESC_H
#include "libavutil/avutil.h"
#include "codec_id.h"
/**
* @addtogroup lavc_core
* @{
*/
/**
* This struct describes the properties of a single codec described by an
* AVCodecID.
* @see avcodec_descriptor_get()
*/
typedef struct AVCodecDescriptor {
enum AVCodecID id;
enum AVMediaType type;
/**
* Name of the codec described by this descriptor. It is non-empty and
* unique for each codec descriptor. It should contain alphanumeric
* characters and '_' only.
*/
const char *name;
/**
* A more descriptive name for this codec. May be NULL.
*/
const char *long_name;
/**
* Codec properties, a combination of AV_CODEC_PROP_* flags.
*/
int props;
/**
* MIME type(s) associated with the codec.
* May be NULL; if not, a NULL-terminated array of MIME types.
* The first item is always non-NULL and is the preferred MIME type.
*/
const char *const *mime_types;
/**
* If non-NULL, an array of profiles recognized for this codec.
* Terminated with AV_PROFILE_UNKNOWN.
*/
const struct AVProfile *profiles;
} AVCodecDescriptor;
/**
* Codec uses only intra compression.
* Video and audio codecs only.
*/
#define AV_CODEC_PROP_INTRA_ONLY (1 << 0)
/**
* Codec supports lossy compression. Audio and video codecs only.
* @note a codec may support both lossy and lossless
* compression modes
*/
#define AV_CODEC_PROP_LOSSY (1 << 1)
/**
* Codec supports lossless compression. Audio and video codecs only.
*/
#define AV_CODEC_PROP_LOSSLESS (1 << 2)
/**
* Codec supports frame reordering. That is, the coded order (the order in which
* the encoded packets are output by the encoders / stored / input to the
* decoders) may be different from the presentation order of the corresponding
* frames.
*
* For codecs that do not have this property set, PTS and DTS should always be
* equal.
*/
#define AV_CODEC_PROP_REORDER (1 << 3)
/**
* Video codec supports separate coding of fields in interlaced frames.
*/
#define AV_CODEC_PROP_FIELDS (1 << 4)
/**
* Subtitle codec is bitmap based
* Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field.
*/
#define AV_CODEC_PROP_BITMAP_SUB (1 << 16)
/**
* Subtitle codec is text based.
* Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field.
*/
#define AV_CODEC_PROP_TEXT_SUB (1 << 17)
/**
* @return descriptor for given codec ID or NULL if no descriptor exists.
*/
const AVCodecDescriptor *avcodec_descriptor_get(enum AVCodecID id);
/**
* Iterate over all codec descriptors known to libavcodec.
*
* @param prev previous descriptor. NULL to get the first descriptor.
*
* @return next descriptor or NULL after the last descriptor
*/
const AVCodecDescriptor *avcodec_descriptor_next(const AVCodecDescriptor *prev);
/**
* @return codec descriptor with the given name or NULL if no such descriptor
* exists.
*/
const AVCodecDescriptor *avcodec_descriptor_get_by_name(const char *name);
/**
* @}
*/
#endif // AVCODEC_CODEC_DESC_H

View File

@ -0,0 +1,668 @@
/*
* Codec IDs
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_CODEC_ID_H
#define AVCODEC_CODEC_ID_H
#include "libavutil/avutil.h"
#include "libavutil/samplefmt.h"
#include "version_major.h"
/**
* @addtogroup lavc_core
* @{
*/
/**
* Identify the syntax and semantics of the bitstream.
* The principle is roughly:
* Two decoders with the same ID can decode the same streams.
* Two encoders with the same ID can encode compatible streams.
* There may be slight deviations from the principle due to implementation
* details.
*
* If you add a codec ID to this list, add it so that
* 1. no value of an existing codec ID changes (that would break ABI),
* 2. it is as close as possible to similar codecs
*
* After adding new codec IDs, do not forget to add an entry to the codec
* descriptor list and bump libavcodec minor version.
*/
enum AVCodecID {
AV_CODEC_ID_NONE,
/* video codecs */
AV_CODEC_ID_MPEG1VIDEO,
AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
AV_CODEC_ID_H261,
AV_CODEC_ID_H263,
AV_CODEC_ID_RV10,
AV_CODEC_ID_RV20,
AV_CODEC_ID_MJPEG,
AV_CODEC_ID_MJPEGB,
AV_CODEC_ID_LJPEG,
AV_CODEC_ID_SP5X,
AV_CODEC_ID_JPEGLS,
AV_CODEC_ID_MPEG4,
AV_CODEC_ID_RAWVIDEO,
AV_CODEC_ID_MSMPEG4V1,
AV_CODEC_ID_MSMPEG4V2,
AV_CODEC_ID_MSMPEG4V3,
AV_CODEC_ID_WMV1,
AV_CODEC_ID_WMV2,
AV_CODEC_ID_H263P,
AV_CODEC_ID_H263I,
AV_CODEC_ID_FLV1,
AV_CODEC_ID_SVQ1,
AV_CODEC_ID_SVQ3,
AV_CODEC_ID_DVVIDEO,
AV_CODEC_ID_HUFFYUV,
AV_CODEC_ID_CYUV,
AV_CODEC_ID_H264,
AV_CODEC_ID_INDEO3,
AV_CODEC_ID_VP3,
AV_CODEC_ID_THEORA,
AV_CODEC_ID_ASV1,
AV_CODEC_ID_ASV2,
AV_CODEC_ID_FFV1,
AV_CODEC_ID_4XM,
AV_CODEC_ID_VCR1,
AV_CODEC_ID_CLJR,
AV_CODEC_ID_MDEC,
AV_CODEC_ID_ROQ,
AV_CODEC_ID_INTERPLAY_VIDEO,
AV_CODEC_ID_XAN_WC3,
AV_CODEC_ID_XAN_WC4,
AV_CODEC_ID_RPZA,
AV_CODEC_ID_CINEPAK,
AV_CODEC_ID_WS_VQA,
AV_CODEC_ID_MSRLE,
AV_CODEC_ID_MSVIDEO1,
AV_CODEC_ID_IDCIN,
AV_CODEC_ID_8BPS,
AV_CODEC_ID_SMC,
AV_CODEC_ID_FLIC,
AV_CODEC_ID_TRUEMOTION1,
AV_CODEC_ID_VMDVIDEO,
AV_CODEC_ID_MSZH,
AV_CODEC_ID_ZLIB,
AV_CODEC_ID_QTRLE,
AV_CODEC_ID_TSCC,
AV_CODEC_ID_ULTI,
AV_CODEC_ID_QDRAW,
AV_CODEC_ID_VIXL,
AV_CODEC_ID_QPEG,
AV_CODEC_ID_PNG,
AV_CODEC_ID_PPM,
AV_CODEC_ID_PBM,
AV_CODEC_ID_PGM,
AV_CODEC_ID_PGMYUV,
AV_CODEC_ID_PAM,
AV_CODEC_ID_FFVHUFF,
AV_CODEC_ID_RV30,
AV_CODEC_ID_RV40,
AV_CODEC_ID_VC1,
AV_CODEC_ID_WMV3,
AV_CODEC_ID_LOCO,
AV_CODEC_ID_WNV1,
AV_CODEC_ID_AASC,
AV_CODEC_ID_INDEO2,
AV_CODEC_ID_FRAPS,
AV_CODEC_ID_TRUEMOTION2,
AV_CODEC_ID_BMP,
AV_CODEC_ID_CSCD,
AV_CODEC_ID_MMVIDEO,
AV_CODEC_ID_ZMBV,
AV_CODEC_ID_AVS,
AV_CODEC_ID_SMACKVIDEO,
AV_CODEC_ID_NUV,
AV_CODEC_ID_KMVC,
AV_CODEC_ID_FLASHSV,
AV_CODEC_ID_CAVS,
AV_CODEC_ID_JPEG2000,
AV_CODEC_ID_VMNC,
AV_CODEC_ID_VP5,
AV_CODEC_ID_VP6,
AV_CODEC_ID_VP6F,
AV_CODEC_ID_TARGA,
AV_CODEC_ID_DSICINVIDEO,
AV_CODEC_ID_TIERTEXSEQVIDEO,
AV_CODEC_ID_TIFF,
AV_CODEC_ID_GIF,
AV_CODEC_ID_DXA,
AV_CODEC_ID_DNXHD,
AV_CODEC_ID_THP,
AV_CODEC_ID_SGI,
AV_CODEC_ID_C93,
AV_CODEC_ID_BETHSOFTVID,
AV_CODEC_ID_PTX,
AV_CODEC_ID_TXD,
AV_CODEC_ID_VP6A,
AV_CODEC_ID_AMV,
AV_CODEC_ID_VB,
AV_CODEC_ID_PCX,
AV_CODEC_ID_SUNRAST,
AV_CODEC_ID_INDEO4,
AV_CODEC_ID_INDEO5,
AV_CODEC_ID_MIMIC,
AV_CODEC_ID_RL2,
AV_CODEC_ID_ESCAPE124,
AV_CODEC_ID_DIRAC,
AV_CODEC_ID_BFI,
AV_CODEC_ID_CMV,
AV_CODEC_ID_MOTIONPIXELS,
AV_CODEC_ID_TGV,
AV_CODEC_ID_TGQ,
AV_CODEC_ID_TQI,
AV_CODEC_ID_AURA,
AV_CODEC_ID_AURA2,
AV_CODEC_ID_V210X,
AV_CODEC_ID_TMV,
AV_CODEC_ID_V210,
AV_CODEC_ID_DPX,
AV_CODEC_ID_MAD,
AV_CODEC_ID_FRWU,
AV_CODEC_ID_FLASHSV2,
AV_CODEC_ID_CDGRAPHICS,
AV_CODEC_ID_R210,
AV_CODEC_ID_ANM,
AV_CODEC_ID_BINKVIDEO,
AV_CODEC_ID_IFF_ILBM,
#define AV_CODEC_ID_IFF_BYTERUN1 AV_CODEC_ID_IFF_ILBM
AV_CODEC_ID_KGV1,
AV_CODEC_ID_YOP,
AV_CODEC_ID_VP8,
AV_CODEC_ID_PICTOR,
AV_CODEC_ID_ANSI,
AV_CODEC_ID_A64_MULTI,
AV_CODEC_ID_A64_MULTI5,
AV_CODEC_ID_R10K,
AV_CODEC_ID_MXPEG,
AV_CODEC_ID_LAGARITH,
AV_CODEC_ID_PRORES,
AV_CODEC_ID_JV,
AV_CODEC_ID_DFA,
AV_CODEC_ID_WMV3IMAGE,
AV_CODEC_ID_VC1IMAGE,
AV_CODEC_ID_UTVIDEO,
AV_CODEC_ID_BMV_VIDEO,
AV_CODEC_ID_VBLE,
AV_CODEC_ID_DXTORY,
AV_CODEC_ID_V410,
AV_CODEC_ID_XWD,
AV_CODEC_ID_CDXL,
AV_CODEC_ID_XBM,
AV_CODEC_ID_ZEROCODEC,
AV_CODEC_ID_MSS1,
AV_CODEC_ID_MSA1,
AV_CODEC_ID_TSCC2,
AV_CODEC_ID_MTS2,
AV_CODEC_ID_CLLC,
AV_CODEC_ID_MSS2,
AV_CODEC_ID_VP9,
AV_CODEC_ID_AIC,
AV_CODEC_ID_ESCAPE130,
AV_CODEC_ID_G2M,
AV_CODEC_ID_WEBP,
AV_CODEC_ID_HNM4_VIDEO,
AV_CODEC_ID_HEVC,
#define AV_CODEC_ID_H265 AV_CODEC_ID_HEVC
AV_CODEC_ID_FIC,
AV_CODEC_ID_ALIAS_PIX,
AV_CODEC_ID_BRENDER_PIX,
AV_CODEC_ID_PAF_VIDEO,
AV_CODEC_ID_EXR,
AV_CODEC_ID_VP7,
AV_CODEC_ID_SANM,
AV_CODEC_ID_SGIRLE,
AV_CODEC_ID_MVC1,
AV_CODEC_ID_MVC2,
AV_CODEC_ID_HQX,
AV_CODEC_ID_TDSC,
AV_CODEC_ID_HQ_HQA,
AV_CODEC_ID_HAP,
AV_CODEC_ID_DDS,
AV_CODEC_ID_DXV,
AV_CODEC_ID_SCREENPRESSO,
AV_CODEC_ID_RSCC,
AV_CODEC_ID_AVS2,
AV_CODEC_ID_PGX,
AV_CODEC_ID_AVS3,
AV_CODEC_ID_MSP2,
AV_CODEC_ID_VVC,
#define AV_CODEC_ID_H266 AV_CODEC_ID_VVC
AV_CODEC_ID_Y41P,
AV_CODEC_ID_AVRP,
AV_CODEC_ID_012V,
AV_CODEC_ID_AVUI,
#if FF_API_AYUV_CODECID
AV_CODEC_ID_AYUV,
#endif
AV_CODEC_ID_TARGA_Y216,
AV_CODEC_ID_V308,
AV_CODEC_ID_V408,
AV_CODEC_ID_YUV4,
AV_CODEC_ID_AVRN,
AV_CODEC_ID_CPIA,
AV_CODEC_ID_XFACE,
AV_CODEC_ID_SNOW,
AV_CODEC_ID_SMVJPEG,
AV_CODEC_ID_APNG,
AV_CODEC_ID_DAALA,
AV_CODEC_ID_CFHD,
AV_CODEC_ID_TRUEMOTION2RT,
AV_CODEC_ID_M101,
AV_CODEC_ID_MAGICYUV,
AV_CODEC_ID_SHEERVIDEO,
AV_CODEC_ID_YLC,
AV_CODEC_ID_PSD,
AV_CODEC_ID_PIXLET,
AV_CODEC_ID_SPEEDHQ,
AV_CODEC_ID_FMVC,
AV_CODEC_ID_SCPR,
AV_CODEC_ID_CLEARVIDEO,
AV_CODEC_ID_XPM,
AV_CODEC_ID_AV1,
AV_CODEC_ID_BITPACKED,
AV_CODEC_ID_MSCC,
AV_CODEC_ID_SRGC,
AV_CODEC_ID_SVG,
AV_CODEC_ID_GDV,
AV_CODEC_ID_FITS,
AV_CODEC_ID_IMM4,
AV_CODEC_ID_PROSUMER,
AV_CODEC_ID_MWSC,
AV_CODEC_ID_WCMV,
AV_CODEC_ID_RASC,
AV_CODEC_ID_HYMT,
AV_CODEC_ID_ARBC,
AV_CODEC_ID_AGM,
AV_CODEC_ID_LSCR,
AV_CODEC_ID_VP4,
AV_CODEC_ID_IMM5,
AV_CODEC_ID_MVDV,
AV_CODEC_ID_MVHA,
AV_CODEC_ID_CDTOONS,
AV_CODEC_ID_MV30,
AV_CODEC_ID_NOTCHLC,
AV_CODEC_ID_PFM,
AV_CODEC_ID_MOBICLIP,
AV_CODEC_ID_PHOTOCD,
AV_CODEC_ID_IPU,
AV_CODEC_ID_ARGO,
AV_CODEC_ID_CRI,
AV_CODEC_ID_SIMBIOSIS_IMX,
AV_CODEC_ID_SGA_VIDEO,
AV_CODEC_ID_GEM,
AV_CODEC_ID_VBN,
AV_CODEC_ID_JPEGXL,
AV_CODEC_ID_QOI,
AV_CODEC_ID_PHM,
AV_CODEC_ID_RADIANCE_HDR,
AV_CODEC_ID_WBMP,
AV_CODEC_ID_MEDIA100,
AV_CODEC_ID_VQC,
AV_CODEC_ID_PDV,
AV_CODEC_ID_EVC,
AV_CODEC_ID_RTV1,
AV_CODEC_ID_VMIX,
/* various PCM "codecs" */
AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
AV_CODEC_ID_PCM_S16LE = 0x10000,
AV_CODEC_ID_PCM_S16BE,
AV_CODEC_ID_PCM_U16LE,
AV_CODEC_ID_PCM_U16BE,
AV_CODEC_ID_PCM_S8,
AV_CODEC_ID_PCM_U8,
AV_CODEC_ID_PCM_MULAW,
AV_CODEC_ID_PCM_ALAW,
AV_CODEC_ID_PCM_S32LE,
AV_CODEC_ID_PCM_S32BE,
AV_CODEC_ID_PCM_U32LE,
AV_CODEC_ID_PCM_U32BE,
AV_CODEC_ID_PCM_S24LE,
AV_CODEC_ID_PCM_S24BE,
AV_CODEC_ID_PCM_U24LE,
AV_CODEC_ID_PCM_U24BE,
AV_CODEC_ID_PCM_S24DAUD,
AV_CODEC_ID_PCM_ZORK,
AV_CODEC_ID_PCM_S16LE_PLANAR,
AV_CODEC_ID_PCM_DVD,
AV_CODEC_ID_PCM_F32BE,
AV_CODEC_ID_PCM_F32LE,
AV_CODEC_ID_PCM_F64BE,
AV_CODEC_ID_PCM_F64LE,
AV_CODEC_ID_PCM_BLURAY,
AV_CODEC_ID_PCM_LXF,
AV_CODEC_ID_S302M,
AV_CODEC_ID_PCM_S8_PLANAR,
AV_CODEC_ID_PCM_S24LE_PLANAR,
AV_CODEC_ID_PCM_S32LE_PLANAR,
AV_CODEC_ID_PCM_S16BE_PLANAR,
AV_CODEC_ID_PCM_S64LE,
AV_CODEC_ID_PCM_S64BE,
AV_CODEC_ID_PCM_F16LE,
AV_CODEC_ID_PCM_F24LE,
AV_CODEC_ID_PCM_VIDC,
AV_CODEC_ID_PCM_SGA,
/* various ADPCM codecs */
AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
AV_CODEC_ID_ADPCM_IMA_WAV,
AV_CODEC_ID_ADPCM_IMA_DK3,
AV_CODEC_ID_ADPCM_IMA_DK4,
AV_CODEC_ID_ADPCM_IMA_WS,
AV_CODEC_ID_ADPCM_IMA_SMJPEG,
AV_CODEC_ID_ADPCM_MS,
AV_CODEC_ID_ADPCM_4XM,
AV_CODEC_ID_ADPCM_XA,
AV_CODEC_ID_ADPCM_ADX,
AV_CODEC_ID_ADPCM_EA,
AV_CODEC_ID_ADPCM_G726,
AV_CODEC_ID_ADPCM_CT,
AV_CODEC_ID_ADPCM_SWF,
AV_CODEC_ID_ADPCM_YAMAHA,
AV_CODEC_ID_ADPCM_SBPRO_4,
AV_CODEC_ID_ADPCM_SBPRO_3,
AV_CODEC_ID_ADPCM_SBPRO_2,
AV_CODEC_ID_ADPCM_THP,
AV_CODEC_ID_ADPCM_IMA_AMV,
AV_CODEC_ID_ADPCM_EA_R1,
AV_CODEC_ID_ADPCM_EA_R3,
AV_CODEC_ID_ADPCM_EA_R2,
AV_CODEC_ID_ADPCM_IMA_EA_SEAD,
AV_CODEC_ID_ADPCM_IMA_EA_EACS,
AV_CODEC_ID_ADPCM_EA_XAS,
AV_CODEC_ID_ADPCM_EA_MAXIS_XA,
AV_CODEC_ID_ADPCM_IMA_ISS,
AV_CODEC_ID_ADPCM_G722,
AV_CODEC_ID_ADPCM_IMA_APC,
AV_CODEC_ID_ADPCM_VIMA,
AV_CODEC_ID_ADPCM_AFC,
AV_CODEC_ID_ADPCM_IMA_OKI,
AV_CODEC_ID_ADPCM_DTK,
AV_CODEC_ID_ADPCM_IMA_RAD,
AV_CODEC_ID_ADPCM_G726LE,
AV_CODEC_ID_ADPCM_THP_LE,
AV_CODEC_ID_ADPCM_PSX,
AV_CODEC_ID_ADPCM_AICA,
AV_CODEC_ID_ADPCM_IMA_DAT4,
AV_CODEC_ID_ADPCM_MTAF,
AV_CODEC_ID_ADPCM_AGM,
AV_CODEC_ID_ADPCM_ARGO,
AV_CODEC_ID_ADPCM_IMA_SSI,
AV_CODEC_ID_ADPCM_ZORK,
AV_CODEC_ID_ADPCM_IMA_APM,
AV_CODEC_ID_ADPCM_IMA_ALP,
AV_CODEC_ID_ADPCM_IMA_MTF,
AV_CODEC_ID_ADPCM_IMA_CUNNING,
AV_CODEC_ID_ADPCM_IMA_MOFLEX,
AV_CODEC_ID_ADPCM_IMA_ACORN,
AV_CODEC_ID_ADPCM_XMD,
/* AMR */
AV_CODEC_ID_AMR_NB = 0x12000,
AV_CODEC_ID_AMR_WB,
/* RealAudio codecs*/
AV_CODEC_ID_RA_144 = 0x13000,
AV_CODEC_ID_RA_288,
/* various DPCM codecs */
AV_CODEC_ID_ROQ_DPCM = 0x14000,
AV_CODEC_ID_INTERPLAY_DPCM,
AV_CODEC_ID_XAN_DPCM,
AV_CODEC_ID_SOL_DPCM,
AV_CODEC_ID_SDX2_DPCM,
AV_CODEC_ID_GREMLIN_DPCM,
AV_CODEC_ID_DERF_DPCM,
AV_CODEC_ID_WADY_DPCM,
AV_CODEC_ID_CBD2_DPCM,
/* audio codecs */
AV_CODEC_ID_MP2 = 0x15000,
AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
AV_CODEC_ID_AAC,
AV_CODEC_ID_AC3,
AV_CODEC_ID_DTS,
AV_CODEC_ID_VORBIS,
AV_CODEC_ID_DVAUDIO,
AV_CODEC_ID_WMAV1,
AV_CODEC_ID_WMAV2,
AV_CODEC_ID_MACE3,
AV_CODEC_ID_MACE6,
AV_CODEC_ID_VMDAUDIO,
AV_CODEC_ID_FLAC,
AV_CODEC_ID_MP3ADU,
AV_CODEC_ID_MP3ON4,
AV_CODEC_ID_SHORTEN,
AV_CODEC_ID_ALAC,
AV_CODEC_ID_WESTWOOD_SND1,
AV_CODEC_ID_GSM, ///< as in Berlin toast format
AV_CODEC_ID_QDM2,
AV_CODEC_ID_COOK,
AV_CODEC_ID_TRUESPEECH,
AV_CODEC_ID_TTA,
AV_CODEC_ID_SMACKAUDIO,
AV_CODEC_ID_QCELP,
AV_CODEC_ID_WAVPACK,
AV_CODEC_ID_DSICINAUDIO,
AV_CODEC_ID_IMC,
AV_CODEC_ID_MUSEPACK7,
AV_CODEC_ID_MLP,
AV_CODEC_ID_GSM_MS, /* as found in WAV */
AV_CODEC_ID_ATRAC3,
AV_CODEC_ID_APE,
AV_CODEC_ID_NELLYMOSER,
AV_CODEC_ID_MUSEPACK8,
AV_CODEC_ID_SPEEX,
AV_CODEC_ID_WMAVOICE,
AV_CODEC_ID_WMAPRO,
AV_CODEC_ID_WMALOSSLESS,
AV_CODEC_ID_ATRAC3P,
AV_CODEC_ID_EAC3,
AV_CODEC_ID_SIPR,
AV_CODEC_ID_MP1,
AV_CODEC_ID_TWINVQ,
AV_CODEC_ID_TRUEHD,
AV_CODEC_ID_MP4ALS,
AV_CODEC_ID_ATRAC1,
AV_CODEC_ID_BINKAUDIO_RDFT,
AV_CODEC_ID_BINKAUDIO_DCT,
AV_CODEC_ID_AAC_LATM,
AV_CODEC_ID_QDMC,
AV_CODEC_ID_CELT,
AV_CODEC_ID_G723_1,
AV_CODEC_ID_G729,
AV_CODEC_ID_8SVX_EXP,
AV_CODEC_ID_8SVX_FIB,
AV_CODEC_ID_BMV_AUDIO,
AV_CODEC_ID_RALF,
AV_CODEC_ID_IAC,
AV_CODEC_ID_ILBC,
AV_CODEC_ID_OPUS,
AV_CODEC_ID_COMFORT_NOISE,
AV_CODEC_ID_TAK,
AV_CODEC_ID_METASOUND,
AV_CODEC_ID_PAF_AUDIO,
AV_CODEC_ID_ON2AVC,
AV_CODEC_ID_DSS_SP,
AV_CODEC_ID_CODEC2,
AV_CODEC_ID_FFWAVESYNTH,
AV_CODEC_ID_SONIC,
AV_CODEC_ID_SONIC_LS,
AV_CODEC_ID_EVRC,
AV_CODEC_ID_SMV,
AV_CODEC_ID_DSD_LSBF,
AV_CODEC_ID_DSD_MSBF,
AV_CODEC_ID_DSD_LSBF_PLANAR,
AV_CODEC_ID_DSD_MSBF_PLANAR,
AV_CODEC_ID_4GV,
AV_CODEC_ID_INTERPLAY_ACM,
AV_CODEC_ID_XMA1,
AV_CODEC_ID_XMA2,
AV_CODEC_ID_DST,
AV_CODEC_ID_ATRAC3AL,
AV_CODEC_ID_ATRAC3PAL,
AV_CODEC_ID_DOLBY_E,
AV_CODEC_ID_APTX,
AV_CODEC_ID_APTX_HD,
AV_CODEC_ID_SBC,
AV_CODEC_ID_ATRAC9,
AV_CODEC_ID_HCOM,
AV_CODEC_ID_ACELP_KELVIN,
AV_CODEC_ID_MPEGH_3D_AUDIO,
AV_CODEC_ID_SIREN,
AV_CODEC_ID_HCA,
AV_CODEC_ID_FASTAUDIO,
AV_CODEC_ID_MSNSIREN,
AV_CODEC_ID_DFPWM,
AV_CODEC_ID_BONK,
AV_CODEC_ID_MISC4,
AV_CODEC_ID_APAC,
AV_CODEC_ID_FTR,
AV_CODEC_ID_WAVARC,
AV_CODEC_ID_RKA,
AV_CODEC_ID_AC4,
AV_CODEC_ID_OSQ,
/* subtitle codecs */
AV_CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
AV_CODEC_ID_DVD_SUBTITLE = 0x17000,
AV_CODEC_ID_DVB_SUBTITLE,
AV_CODEC_ID_TEXT, ///< raw UTF-8 text
AV_CODEC_ID_XSUB,
AV_CODEC_ID_SSA,
AV_CODEC_ID_MOV_TEXT,
AV_CODEC_ID_HDMV_PGS_SUBTITLE,
AV_CODEC_ID_DVB_TELETEXT,
AV_CODEC_ID_SRT,
AV_CODEC_ID_MICRODVD,
AV_CODEC_ID_EIA_608,
AV_CODEC_ID_JACOSUB,
AV_CODEC_ID_SAMI,
AV_CODEC_ID_REALTEXT,
AV_CODEC_ID_STL,
AV_CODEC_ID_SUBVIEWER1,
AV_CODEC_ID_SUBVIEWER,
AV_CODEC_ID_SUBRIP,
AV_CODEC_ID_WEBVTT,
AV_CODEC_ID_MPL2,
AV_CODEC_ID_VPLAYER,
AV_CODEC_ID_PJS,
AV_CODEC_ID_ASS,
AV_CODEC_ID_HDMV_TEXT_SUBTITLE,
AV_CODEC_ID_TTML,
AV_CODEC_ID_ARIB_CAPTION,
/* other specific kind of codecs (generally used for attachments) */
AV_CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
AV_CODEC_ID_TTF = 0x18000,
AV_CODEC_ID_SCTE_35, ///< Contain timestamp estimated through PCR of program stream.
AV_CODEC_ID_EPG,
AV_CODEC_ID_BINTEXT,
AV_CODEC_ID_XBIN,
AV_CODEC_ID_IDF,
AV_CODEC_ID_OTF,
AV_CODEC_ID_SMPTE_KLV,
AV_CODEC_ID_DVD_NAV,
AV_CODEC_ID_TIMED_ID3,
AV_CODEC_ID_BIN_DATA,
AV_CODEC_ID_SMPTE_2038,
AV_CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf should attempt to identify it
AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
* stream (only used by libavformat) */
AV_CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
* stream (only used by libavformat) */
AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
AV_CODEC_ID_WRAPPED_AVFRAME = 0x21001, ///< Passthrough codec, AVFrames wrapped in AVPacket
/**
* Dummy null video codec, useful mainly for development and debugging.
* Null encoder/decoder discard all input and never return any output.
*/
AV_CODEC_ID_VNULL,
/**
* Dummy null audio codec, useful mainly for development and debugging.
* Null encoder/decoder discard all input and never return any output.
*/
AV_CODEC_ID_ANULL,
};
/**
* Get the type of the given codec.
*/
enum AVMediaType avcodec_get_type(enum AVCodecID codec_id);
/**
* Get the name of a codec.
* @return a static string identifying the codec; never NULL
*/
const char *avcodec_get_name(enum AVCodecID id);
/**
* Return codec bits per sample.
*
* @param[in] codec_id the codec
* @return Number of bits per sample or zero if unknown for the given codec.
*/
int av_get_bits_per_sample(enum AVCodecID codec_id);
/**
* Return codec bits per sample.
* Only return non-zero if the bits per sample is exactly correct, not an
* approximation.
*
* @param[in] codec_id the codec
* @return Number of bits per sample or zero if unknown for the given codec.
*/
int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
/**
* Return a name for the specified profile, if available.
*
* @param codec_id the ID of the codec to which the requested profile belongs
* @param profile the profile value for which a name is requested
* @return A name for the profile if found, NULL otherwise.
*
* @note unlike av_get_profile_name(), which searches a list of profiles
* supported by a specific decoder or encoder implementation, this
* function searches the list of profiles from the AVCodecDescriptor
*/
const char *avcodec_profile_name(enum AVCodecID codec_id, int profile);
/**
* Return the PCM codec associated with a sample format.
* @param be endianness, 0 for little, 1 for big,
* -1 (or anything else) for native
* @return AV_CODEC_ID_PCM_* or AV_CODEC_ID_NONE
*/
enum AVCodecID av_get_pcm_codec(enum AVSampleFormat fmt, int be);
/**
* @}
*/
#endif // AVCODEC_CODEC_ID_H

View File

@ -0,0 +1,262 @@
/*
* Codec parameters public API
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_CODEC_PAR_H
#define AVCODEC_CODEC_PAR_H
#include <stdint.h>
#include "libavutil/avutil.h"
#include "libavutil/channel_layout.h"
#include "libavutil/rational.h"
#include "libavutil/pixfmt.h"
#include "codec_id.h"
#include "defs.h"
#include "packet.h"
/**
* @addtogroup lavc_core
* @{
*/
/**
* This struct describes the properties of an encoded stream.
*
* sizeof(AVCodecParameters) is not a part of the public ABI, this struct must
* be allocated with avcodec_parameters_alloc() and freed with
* avcodec_parameters_free().
*/
typedef struct AVCodecParameters {
/**
* General type of the encoded data.
*/
enum AVMediaType codec_type;
/**
* Specific type of the encoded data (the codec used).
*/
enum AVCodecID codec_id;
/**
* Additional information about the codec (corresponds to the AVI FOURCC).
*/
uint32_t codec_tag;
/**
* Extra binary data needed for initializing the decoder, codec-dependent.
*
* Must be allocated with av_malloc() and will be freed by
* avcodec_parameters_free(). The allocated size of extradata must be at
* least extradata_size + AV_INPUT_BUFFER_PADDING_SIZE, with the padding
* bytes zeroed.
*/
uint8_t *extradata;
/**
* Size of the extradata content in bytes.
*/
int extradata_size;
/**
* - video: the pixel format, the value corresponds to enum AVPixelFormat.
* - audio: the sample format, the value corresponds to enum AVSampleFormat.
*/
int format;
/**
* The average bitrate of the encoded data (in bits per second).
*/
int64_t bit_rate;
/**
* The number of bits per sample in the codedwords.
*
* This is basically the bitrate per sample. It is mandatory for a bunch of
* formats to actually decode them. It's the number of bits for one sample in
* the actual coded bitstream.
*
* This could be for example 4 for ADPCM
* For PCM formats this matches bits_per_raw_sample
* Can be 0
*/
int bits_per_coded_sample;
/**
* This is the number of valid bits in each output sample. If the
* sample format has more bits, the least significant bits are additional
* padding bits, which are always 0. Use right shifts to reduce the sample
* to its actual size. For example, audio formats with 24 bit samples will
* have bits_per_raw_sample set to 24, and format set to AV_SAMPLE_FMT_S32.
* To get the original sample use "(int32_t)sample >> 8"."
*
* For ADPCM this might be 12 or 16 or similar
* Can be 0
*/
int bits_per_raw_sample;
/**
* Codec-specific bitstream restrictions that the stream conforms to.
*/
int profile;
int level;
/**
* Video only. The dimensions of the video frame in pixels.
*/
int width;
int height;
/**
* Video only. The aspect ratio (width / height) which a single pixel
* should have when displayed.
*
* When the aspect ratio is unknown / undefined, the numerator should be
* set to 0 (the denominator may have any value).
*/
AVRational sample_aspect_ratio;
/**
* Video only. The order of the fields in interlaced video.
*/
enum AVFieldOrder field_order;
/**
* Video only. Additional colorspace characteristics.
*/
enum AVColorRange color_range;
enum AVColorPrimaries color_primaries;
enum AVColorTransferCharacteristic color_trc;
enum AVColorSpace color_space;
enum AVChromaLocation chroma_location;
/**
* Video only. Number of delayed frames.
*/
int video_delay;
#if FF_API_OLD_CHANNEL_LAYOUT
/**
* Audio only. The channel layout bitmask. May be 0 if the channel layout is
* unknown or unspecified, otherwise the number of bits set must be equal to
* the channels field.
* @deprecated use ch_layout
*/
attribute_deprecated
uint64_t channel_layout;
/**
* Audio only. The number of audio channels.
* @deprecated use ch_layout.nb_channels
*/
attribute_deprecated
int channels;
#endif
/**
* Audio only. The number of audio samples per second.
*/
int sample_rate;
/**
* Audio only. The number of bytes per coded audio frame, required by some
* formats.
*
* Corresponds to nBlockAlign in WAVEFORMATEX.
*/
int block_align;
/**
* Audio only. Audio frame size, if known. Required by some formats to be static.
*/
int frame_size;
/**
* Audio only. The amount of padding (in samples) inserted by the encoder at
* the beginning of the audio. I.e. this number of leading decoded samples
* must be discarded by the caller to get the original audio without leading
* padding.
*/
int initial_padding;
/**
* Audio only. The amount of padding (in samples) appended by the encoder to
* the end of the audio. I.e. this number of decoded samples must be
* discarded by the caller from the end of the stream to get the original
* audio without any trailing padding.
*/
int trailing_padding;
/**
* Audio only. Number of samples to skip after a discontinuity.
*/
int seek_preroll;
/**
* Audio only. The channel layout and number of channels.
*/
AVChannelLayout ch_layout;
/**
* Video only. Number of frames per second, for streams with constant frame
* durations. Should be set to { 0, 1 } when some frames have differing
* durations or if the value is not known.
*
* @note This field correponds to values that are stored in codec-level
* headers and is typically overridden by container/transport-layer
* timestamps, when available. It should thus be used only as a last resort,
* when no higher-level timing information is available.
*/
AVRational framerate;
/**
* Additional data associated with the entire stream.
*/
AVPacketSideData *coded_side_data;
/**
* Amount of entries in @ref coded_side_data.
*/
int nb_coded_side_data;
} AVCodecParameters;
/**
* Allocate a new AVCodecParameters and set its fields to default values
* (unknown/invalid/0). The returned struct must be freed with
* avcodec_parameters_free().
*/
AVCodecParameters *avcodec_parameters_alloc(void);
/**
* Free an AVCodecParameters instance and everything associated with it and
* write NULL to the supplied pointer.
*/
void avcodec_parameters_free(AVCodecParameters **par);
/**
* Copy the contents of src to dst. Any allocated fields in dst are freed and
* replaced with newly allocated duplicates of the corresponding fields in src.
*
* @return >= 0 on success, a negative AVERROR code on failure.
*/
int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);
/**
* This function is the same as av_get_audio_frame_duration(), except it works
* with AVCodecParameters instead of an AVCodecContext.
*/
int av_get_audio_frame_duration2(AVCodecParameters *par, int frame_bytes);
/**
* @}
*/
#endif // AVCODEC_CODEC_PAR_H

View File

@ -0,0 +1,112 @@
/*
* Direct3D11 HW acceleration
*
* copyright (c) 2009 Laurent Aimar
* copyright (c) 2015 Steve Lhomme
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_D3D11VA_H
#define AVCODEC_D3D11VA_H
/**
* @file
* @ingroup lavc_codec_hwaccel_d3d11va
* Public libavcodec D3D11VA header.
*/
#if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0602
#undef _WIN32_WINNT
#define _WIN32_WINNT 0x0602
#endif
#include <stdint.h>
#include <d3d11.h>
/**
* @defgroup lavc_codec_hwaccel_d3d11va Direct3D11
* @ingroup lavc_codec_hwaccel
*
* @{
*/
#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for Direct3D11 and old UVD/UVD+ ATI video cards
#define FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO 2 ///< Work around for Direct3D11 and old Intel GPUs with ClearVideo interface
/**
* This structure is used to provides the necessary configurations and data
* to the Direct3D11 FFmpeg HWAccel implementation.
*
* The application must make it available as AVCodecContext.hwaccel_context.
*
* Use av_d3d11va_alloc_context() exclusively to allocate an AVD3D11VAContext.
*/
typedef struct AVD3D11VAContext {
/**
* D3D11 decoder object
*/
ID3D11VideoDecoder *decoder;
/**
* D3D11 VideoContext
*/
ID3D11VideoContext *video_context;
/**
* D3D11 configuration used to create the decoder
*/
D3D11_VIDEO_DECODER_CONFIG *cfg;
/**
* The number of surface in the surface array
*/
unsigned surface_count;
/**
* The array of Direct3D surfaces used to create the decoder
*/
ID3D11VideoDecoderOutputView **surface;
/**
* A bit field configuring the workarounds needed for using the decoder
*/
uint64_t workaround;
/**
* Private to the FFmpeg AVHWAccel implementation
*/
unsigned report_id;
/**
* Mutex to access video_context
*/
HANDLE context_mutex;
} AVD3D11VAContext;
/**
* Allocate an AVD3D11VAContext.
*
* @return Newly-allocated AVD3D11VAContext or NULL on failure.
*/
AVD3D11VAContext *av_d3d11va_alloc_context(void);
/**
* @}
*/
#endif /* AVCODEC_D3D11VA_H */

View File

@ -0,0 +1,335 @@
/*
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_DEFS_H
#define AVCODEC_DEFS_H
/**
* @file
* @ingroup libavc
* Misc types and constants that do not belong anywhere else.
*/
#include <stdint.h>
#include <stdlib.h>
/**
* @ingroup lavc_decoding
* Required number of additionally allocated bytes at the end of the input bitstream for decoding.
* This is mainly needed because some optimized bitstream readers read
* 32 or 64 bit at once and could read over the end.<br>
* Note: If the first 23 bits of the additional bytes are not 0, then damaged
* MPEG bitstreams could cause overread and segfault.
*/
#define AV_INPUT_BUFFER_PADDING_SIZE 64
/**
* Verify checksums embedded in the bitstream (could be of either encoded or
* decoded data, depending on the format) and print an error message on mismatch.
* If AV_EF_EXPLODE is also set, a mismatching checksum will result in the
* decoder/demuxer returning an error.
*/
#define AV_EF_CRCCHECK (1<<0)
#define AV_EF_BITSTREAM (1<<1) ///< detect bitstream specification deviations
#define AV_EF_BUFFER (1<<2) ///< detect improper bitstream length
#define AV_EF_EXPLODE (1<<3) ///< abort decoding on minor error detection
#define AV_EF_IGNORE_ERR (1<<15) ///< ignore errors and continue
#define AV_EF_CAREFUL (1<<16) ///< consider things that violate the spec, are fast to calculate and have not been seen in the wild as errors
#define AV_EF_COMPLIANT (1<<17) ///< consider all spec non compliances as errors
#define AV_EF_AGGRESSIVE (1<<18) ///< consider things that a sane encoder/muxer should not do as an error
#define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software.
#define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences.
#define FF_COMPLIANCE_NORMAL 0
#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
#define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things.
#define AV_PROFILE_UNKNOWN -99
#define AV_PROFILE_RESERVED -100
#define AV_PROFILE_AAC_MAIN 0
#define AV_PROFILE_AAC_LOW 1
#define AV_PROFILE_AAC_SSR 2
#define AV_PROFILE_AAC_LTP 3
#define AV_PROFILE_AAC_HE 4
#define AV_PROFILE_AAC_HE_V2 28
#define AV_PROFILE_AAC_LD 22
#define AV_PROFILE_AAC_ELD 38
#define AV_PROFILE_MPEG2_AAC_LOW 128
#define AV_PROFILE_MPEG2_AAC_HE 131
#define AV_PROFILE_DNXHD 0
#define AV_PROFILE_DNXHR_LB 1
#define AV_PROFILE_DNXHR_SQ 2
#define AV_PROFILE_DNXHR_HQ 3
#define AV_PROFILE_DNXHR_HQX 4
#define AV_PROFILE_DNXHR_444 5
#define AV_PROFILE_DTS 20
#define AV_PROFILE_DTS_ES 30
#define AV_PROFILE_DTS_96_24 40
#define AV_PROFILE_DTS_HD_HRA 50
#define AV_PROFILE_DTS_HD_MA 60
#define AV_PROFILE_DTS_EXPRESS 70
#define AV_PROFILE_DTS_HD_MA_X 61
#define AV_PROFILE_DTS_HD_MA_X_IMAX 62
#define AV_PROFILE_EAC3_DDP_ATMOS 30
#define AV_PROFILE_TRUEHD_ATMOS 30
#define AV_PROFILE_MPEG2_422 0
#define AV_PROFILE_MPEG2_HIGH 1
#define AV_PROFILE_MPEG2_SS 2
#define AV_PROFILE_MPEG2_SNR_SCALABLE 3
#define AV_PROFILE_MPEG2_MAIN 4
#define AV_PROFILE_MPEG2_SIMPLE 5
#define AV_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag
#define AV_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag
#define AV_PROFILE_H264_BASELINE 66
#define AV_PROFILE_H264_CONSTRAINED_BASELINE (66|AV_PROFILE_H264_CONSTRAINED)
#define AV_PROFILE_H264_MAIN 77
#define AV_PROFILE_H264_EXTENDED 88
#define AV_PROFILE_H264_HIGH 100
#define AV_PROFILE_H264_HIGH_10 110
#define AV_PROFILE_H264_HIGH_10_INTRA (110|AV_PROFILE_H264_INTRA)
#define AV_PROFILE_H264_MULTIVIEW_HIGH 118
#define AV_PROFILE_H264_HIGH_422 122
#define AV_PROFILE_H264_HIGH_422_INTRA (122|AV_PROFILE_H264_INTRA)
#define AV_PROFILE_H264_STEREO_HIGH 128
#define AV_PROFILE_H264_HIGH_444 144
#define AV_PROFILE_H264_HIGH_444_PREDICTIVE 244
#define AV_PROFILE_H264_HIGH_444_INTRA (244|AV_PROFILE_H264_INTRA)
#define AV_PROFILE_H264_CAVLC_444 44
#define AV_PROFILE_VC1_SIMPLE 0
#define AV_PROFILE_VC1_MAIN 1
#define AV_PROFILE_VC1_COMPLEX 2
#define AV_PROFILE_VC1_ADVANCED 3
#define AV_PROFILE_MPEG4_SIMPLE 0
#define AV_PROFILE_MPEG4_SIMPLE_SCALABLE 1
#define AV_PROFILE_MPEG4_CORE 2
#define AV_PROFILE_MPEG4_MAIN 3
#define AV_PROFILE_MPEG4_N_BIT 4
#define AV_PROFILE_MPEG4_SCALABLE_TEXTURE 5
#define AV_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
#define AV_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
#define AV_PROFILE_MPEG4_HYBRID 8
#define AV_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
#define AV_PROFILE_MPEG4_CORE_SCALABLE 10
#define AV_PROFILE_MPEG4_ADVANCED_CODING 11
#define AV_PROFILE_MPEG4_ADVANCED_CORE 12
#define AV_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
#define AV_PROFILE_MPEG4_SIMPLE_STUDIO 14
#define AV_PROFILE_MPEG4_ADVANCED_SIMPLE 15
#define AV_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 1
#define AV_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 2
#define AV_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 32768
#define AV_PROFILE_JPEG2000_DCINEMA_2K 3
#define AV_PROFILE_JPEG2000_DCINEMA_4K 4
#define AV_PROFILE_VP9_0 0
#define AV_PROFILE_VP9_1 1
#define AV_PROFILE_VP9_2 2
#define AV_PROFILE_VP9_3 3
#define AV_PROFILE_HEVC_MAIN 1
#define AV_PROFILE_HEVC_MAIN_10 2
#define AV_PROFILE_HEVC_MAIN_STILL_PICTURE 3
#define AV_PROFILE_HEVC_REXT 4
#define AV_PROFILE_HEVC_SCC 9
#define AV_PROFILE_VVC_MAIN_10 1
#define AV_PROFILE_VVC_MAIN_10_444 33
#define AV_PROFILE_AV1_MAIN 0
#define AV_PROFILE_AV1_HIGH 1
#define AV_PROFILE_AV1_PROFESSIONAL 2
#define AV_PROFILE_MJPEG_HUFFMAN_BASELINE_DCT 0xc0
#define AV_PROFILE_MJPEG_HUFFMAN_EXTENDED_SEQUENTIAL_DCT 0xc1
#define AV_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT 0xc2
#define AV_PROFILE_MJPEG_HUFFMAN_LOSSLESS 0xc3
#define AV_PROFILE_MJPEG_JPEG_LS 0xf7
#define AV_PROFILE_SBC_MSBC 1
#define AV_PROFILE_PRORES_PROXY 0
#define AV_PROFILE_PRORES_LT 1
#define AV_PROFILE_PRORES_STANDARD 2
#define AV_PROFILE_PRORES_HQ 3
#define AV_PROFILE_PRORES_4444 4
#define AV_PROFILE_PRORES_XQ 5
#define AV_PROFILE_ARIB_PROFILE_A 0
#define AV_PROFILE_ARIB_PROFILE_C 1
#define AV_PROFILE_KLVA_SYNC 0
#define AV_PROFILE_KLVA_ASYNC 1
#define AV_PROFILE_EVC_BASELINE 0
#define AV_PROFILE_EVC_MAIN 1
#define AV_LEVEL_UNKNOWN -99
enum AVFieldOrder {
AV_FIELD_UNKNOWN,
AV_FIELD_PROGRESSIVE,
AV_FIELD_TT, ///< Top coded_first, top displayed first
AV_FIELD_BB, ///< Bottom coded first, bottom displayed first
AV_FIELD_TB, ///< Top coded first, bottom displayed first
AV_FIELD_BT, ///< Bottom coded first, top displayed first
};
/**
* @ingroup lavc_decoding
*/
enum AVDiscard{
/* We leave some space between them for extensions (drop some
* keyframes for intra-only or drop just some bidir frames). */
AVDISCARD_NONE =-16, ///< discard nothing
AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi
AVDISCARD_NONREF = 8, ///< discard all non reference
AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
AVDISCARD_NONINTRA= 24, ///< discard all non intra frames
AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
AVDISCARD_ALL = 48, ///< discard all
};
enum AVAudioServiceType {
AV_AUDIO_SERVICE_TYPE_MAIN = 0,
AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
AV_AUDIO_SERVICE_TYPE_NB , ///< Not part of ABI
};
/**
* Pan Scan area.
* This specifies the area which should be displayed.
* Note there may be multiple such areas for one frame.
*/
typedef struct AVPanScan {
/**
* id
* - encoding: Set by user.
* - decoding: Set by libavcodec.
*/
int id;
/**
* width and height in 1/16 pel
* - encoding: Set by user.
* - decoding: Set by libavcodec.
*/
int width;
int height;
/**
* position of the top left corner in 1/16 pel for up to 3 fields/frames
* - encoding: Set by user.
* - decoding: Set by libavcodec.
*/
int16_t position[3][2];
} AVPanScan;
/**
* This structure describes the bitrate properties of an encoded bitstream. It
* roughly corresponds to a subset the VBV parameters for MPEG-2 or HRD
* parameters for H.264/HEVC.
*/
typedef struct AVCPBProperties {
/**
* Maximum bitrate of the stream, in bits per second.
* Zero if unknown or unspecified.
*/
int64_t max_bitrate;
/**
* Minimum bitrate of the stream, in bits per second.
* Zero if unknown or unspecified.
*/
int64_t min_bitrate;
/**
* Average bitrate of the stream, in bits per second.
* Zero if unknown or unspecified.
*/
int64_t avg_bitrate;
/**
* The size of the buffer to which the ratecontrol is applied, in bits.
* Zero if unknown or unspecified.
*/
int64_t buffer_size;
/**
* The delay between the time the packet this structure is associated with
* is received and the time when it should be decoded, in periods of a 27MHz
* clock.
*
* UINT64_MAX when unknown or unspecified.
*/
uint64_t vbv_delay;
} AVCPBProperties;
/**
* Allocate a CPB properties structure and initialize its fields to default
* values.
*
* @param size if non-NULL, the size of the allocated struct will be written
* here. This is useful for embedding it in side data.
*
* @return the newly allocated struct or NULL on failure
*/
AVCPBProperties *av_cpb_properties_alloc(size_t *size);
/**
* This structure supplies correlation between a packet timestamp and a wall clock
* production time. The definition follows the Producer Reference Time ('prft')
* as defined in ISO/IEC 14496-12
*/
typedef struct AVProducerReferenceTime {
/**
* A UTC timestamp, in microseconds, since Unix epoch (e.g, av_gettime()).
*/
int64_t wallclock;
int flags;
} AVProducerReferenceTime;
/**
* Encode extradata length to a buffer. Used by xiph codecs.
*
* @param s buffer to write to; must be at least (v/255+1) bytes long
* @param v size of extradata in bytes
* @return number of bytes written to the buffer.
*/
unsigned int av_xiphlacing(unsigned char *s, unsigned int v);
#endif // AVCODEC_DEFS_H

View File

@ -0,0 +1,135 @@
/*
* Copyright (C) 2007 Marco Gerards <marco@gnu.org>
* Copyright (C) 2009 David Conrad
* Copyright (C) 2011 Jordi Ortiz
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_DIRAC_H
#define AVCODEC_DIRAC_H
/**
* @file
* Interface to Dirac Decoder/Encoder
* @author Marco Gerards <marco@gnu.org>
* @author David Conrad
* @author Jordi Ortiz
*/
#include <stddef.h>
#include <stdint.h>
#include "libavutil/pixfmt.h"
#include "libavutil/rational.h"
/**
* The spec limits the number of wavelet decompositions to 4 for both
* level 1 (VC-2) and 128 (long-gop default).
* 5 decompositions is the maximum before >16-bit buffers are needed.
* Schroedinger allows this for DD 9,7 and 13,7 wavelets only, limiting
* the others to 4 decompositions (or 3 for the fidelity filter).
*
* We use this instead of MAX_DECOMPOSITIONS to save some memory.
*/
#define MAX_DWT_LEVELS 5
/**
* Parse code values:
*
* Dirac Specification ->
* 9.6.1 Table 9.1
*
* VC-2 Specification ->
* 10.4.1 Table 10.1
*/
enum DiracParseCodes {
DIRAC_PCODE_SEQ_HEADER = 0x00,
DIRAC_PCODE_END_SEQ = 0x10,
DIRAC_PCODE_AUX = 0x20,
DIRAC_PCODE_PAD = 0x30,
DIRAC_PCODE_PICTURE_CODED = 0x08,
DIRAC_PCODE_PICTURE_RAW = 0x48,
DIRAC_PCODE_PICTURE_LOW_DEL = 0xC8,
DIRAC_PCODE_PICTURE_HQ = 0xE8,
DIRAC_PCODE_INTER_NOREF_CO1 = 0x0A,
DIRAC_PCODE_INTER_NOREF_CO2 = 0x09,
DIRAC_PCODE_INTER_REF_CO1 = 0x0D,
DIRAC_PCODE_INTER_REF_CO2 = 0x0E,
DIRAC_PCODE_INTRA_REF_CO = 0x0C,
DIRAC_PCODE_INTRA_REF_RAW = 0x4C,
DIRAC_PCODE_INTRA_REF_PICT = 0xCC,
DIRAC_PCODE_MAGIC = 0x42424344,
};
typedef struct DiracVersionInfo {
int major;
int minor;
} DiracVersionInfo;
typedef struct AVDiracSeqHeader {
unsigned width;
unsigned height;
uint8_t chroma_format; ///< 0: 444 1: 422 2: 420
uint8_t interlaced;
uint8_t top_field_first;
uint8_t frame_rate_index; ///< index into dirac_frame_rate[]
uint8_t aspect_ratio_index; ///< index into dirac_aspect_ratio[]
uint16_t clean_width;
uint16_t clean_height;
uint16_t clean_left_offset;
uint16_t clean_right_offset;
uint8_t pixel_range_index; ///< index into dirac_pixel_range_presets[]
uint8_t color_spec_index; ///< index into dirac_color_spec_presets[]
int profile;
int level;
AVRational framerate;
AVRational sample_aspect_ratio;
enum AVPixelFormat pix_fmt;
enum AVColorRange color_range;
enum AVColorPrimaries color_primaries;
enum AVColorTransferCharacteristic color_trc;
enum AVColorSpace colorspace;
DiracVersionInfo version;
int bit_depth;
} AVDiracSeqHeader;
/**
* Parse a Dirac sequence header.
*
* @param dsh this function will allocate and fill an AVDiracSeqHeader struct
* and write it into this pointer. The caller must free it with
* av_free().
* @param buf the data buffer
* @param buf_size the size of the data buffer in bytes
* @param log_ctx if non-NULL, this function will log errors here
* @return 0 on success, a negative AVERROR code on failure
*/
int av_dirac_parse_sequence_header(AVDiracSeqHeader **dsh,
const uint8_t *buf, size_t buf_size,
void *log_ctx);
#endif /* AVCODEC_DIRAC_H */

View File

@ -0,0 +1,82 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_DV_PROFILE_H
#define AVCODEC_DV_PROFILE_H
#include <stdint.h>
#include "libavutil/pixfmt.h"
#include "libavutil/rational.h"
/* minimum number of bytes to read from a DV stream in order to
* determine the profile */
#define DV_PROFILE_BYTES (6 * 80) /* 6 DIF blocks */
/*
* AVDVProfile is used to express the differences between various
* DV flavors. For now it's primarily used for differentiating
* 525/60 and 625/50, but the plans are to use it for various
* DV specs as well (e.g. SMPTE314M vs. IEC 61834).
*/
typedef struct AVDVProfile {
int dsf; /* value of the dsf in the DV header */
int video_stype; /* stype for VAUX source pack */
int frame_size; /* total size of one frame in bytes */
int difseg_size; /* number of DIF segments per DIF channel */
int n_difchan; /* number of DIF channels per frame */
AVRational time_base; /* 1/framerate */
int ltc_divisor; /* FPS from the LTS standpoint */
int height; /* picture height in pixels */
int width; /* picture width in pixels */
AVRational sar[2]; /* sample aspect ratios for 4:3 and 16:9 */
enum AVPixelFormat pix_fmt; /* picture pixel format */
int bpm; /* blocks per macroblock */
const uint8_t *block_sizes; /* AC block sizes, in bits */
int audio_stride; /* size of audio_shuffle table */
int audio_min_samples[3]; /* min amount of audio samples */
/* for 48kHz, 44.1kHz and 32kHz */
int audio_samples_dist[5]; /* how many samples are supposed to be */
/* in each frame in a 5 frames window */
const uint8_t (*audio_shuffle)[9]; /* PCM shuffling table */
} AVDVProfile;
/**
* Get a DV profile for the provided compressed frame.
*
* @param sys the profile used for the previous frame, may be NULL
* @param frame the compressed data buffer
* @param buf_size size of the buffer in bytes
* @return the DV profile for the supplied data or NULL on failure
*/
const AVDVProfile *av_dv_frame_profile(const AVDVProfile *sys,
const uint8_t *frame, unsigned buf_size);
/**
* Get a DV profile for the provided stream parameters.
*/
const AVDVProfile *av_dv_codec_profile(int width, int height, enum AVPixelFormat pix_fmt);
/**
* Get a DV profile for the provided stream parameters.
* The frame rate is used as a best-effort parameter.
*/
const AVDVProfile *av_dv_codec_profile2(int width, int height, enum AVPixelFormat pix_fmt, AVRational frame_rate);
#endif /* AVCODEC_DV_PROFILE_H */

View File

@ -0,0 +1,93 @@
/*
* DXVA2 HW acceleration
*
* copyright (c) 2009 Laurent Aimar
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_DXVA2_H
#define AVCODEC_DXVA2_H
/**
* @file
* @ingroup lavc_codec_hwaccel_dxva2
* Public libavcodec DXVA2 header.
*/
#if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0602
#undef _WIN32_WINNT
#define _WIN32_WINNT 0x0602
#endif
#include <stdint.h>
#include <d3d9.h>
#include <dxva2api.h>
/**
* @defgroup lavc_codec_hwaccel_dxva2 DXVA2
* @ingroup lavc_codec_hwaccel
*
* @{
*/
#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
#define FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO 2 ///< Work around for DXVA2 and old Intel GPUs with ClearVideo interface
/**
* This structure is used to provides the necessary configurations and data
* to the DXVA2 FFmpeg HWAccel implementation.
*
* The application must make it available as AVCodecContext.hwaccel_context.
*/
struct dxva_context {
/**
* DXVA2 decoder object
*/
IDirectXVideoDecoder *decoder;
/**
* DXVA2 configuration used to create the decoder
*/
const DXVA2_ConfigPictureDecode *cfg;
/**
* The number of surface in the surface array
*/
unsigned surface_count;
/**
* The array of Direct3D surfaces used to create the decoder
*/
LPDIRECT3DSURFACE9 *surface;
/**
* A bit field configuring the workarounds needed for using the decoder
*/
uint64_t workaround;
/**
* Private to the FFmpeg AVHWAccel implementation
*/
unsigned report_id;
};
/**
* @}
*/
#endif /* AVCODEC_DXVA2_H */

View File

@ -0,0 +1,46 @@
/*
* JNI public API functions
*
* Copyright (c) 2015-2016 Matthieu Bouron <matthieu.bouron stupeflix.com>
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_JNI_H
#define AVCODEC_JNI_H
/*
* Manually set a Java virtual machine which will be used to retrieve the JNI
* environment. Once a Java VM is set it cannot be changed afterwards, meaning
* you can call multiple times av_jni_set_java_vm with the same Java VM pointer
* however it will error out if you try to set a different Java VM.
*
* @param vm Java virtual machine
* @param log_ctx context used for logging, can be NULL
* @return 0 on success, < 0 otherwise
*/
int av_jni_set_java_vm(void *vm, void *log_ctx);
/*
* Get the Java virtual machine which has been set with av_jni_set_java_vm.
*
* @param vm Java virtual machine
* @return a pointer to the Java virtual machine
*/
void *av_jni_get_java_vm(void *log_ctx);
#endif /* AVCODEC_JNI_H */

View File

@ -0,0 +1,103 @@
/*
* Android MediaCodec public API
*
* Copyright (c) 2016 Matthieu Bouron <matthieu.bouron stupeflix.com>
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_MEDIACODEC_H
#define AVCODEC_MEDIACODEC_H
#include "libavcodec/avcodec.h"
/**
* This structure holds a reference to a android/view/Surface object that will
* be used as output by the decoder.
*
*/
typedef struct AVMediaCodecContext {
/**
* android/view/Surface object reference.
*/
void *surface;
} AVMediaCodecContext;
/**
* Allocate and initialize a MediaCodec context.
*
* When decoding with MediaCodec is finished, the caller must free the
* MediaCodec context with av_mediacodec_default_free.
*
* @return a pointer to a newly allocated AVMediaCodecContext on success, NULL otherwise
*/
AVMediaCodecContext *av_mediacodec_alloc_context(void);
/**
* Convenience function that sets up the MediaCodec context.
*
* @param avctx codec context
* @param ctx MediaCodec context to initialize
* @param surface reference to an android/view/Surface
* @return 0 on success, < 0 otherwise
*/
int av_mediacodec_default_init(AVCodecContext *avctx, AVMediaCodecContext *ctx, void *surface);
/**
* This function must be called to free the MediaCodec context initialized with
* av_mediacodec_default_init().
*
* @param avctx codec context
*/
void av_mediacodec_default_free(AVCodecContext *avctx);
/**
* Opaque structure representing a MediaCodec buffer to render.
*/
typedef struct MediaCodecBuffer AVMediaCodecBuffer;
/**
* Release a MediaCodec buffer and render it to the surface that is associated
* with the decoder. This function should only be called once on a given
* buffer, once released the underlying buffer returns to the codec, thus
* subsequent calls to this function will have no effect.
*
* @param buffer the buffer to render
* @param render 1 to release and render the buffer to the surface or 0 to
* discard the buffer
* @return 0 on success, < 0 otherwise
*/
int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
/**
* Release a MediaCodec buffer and render it at the given time to the surface
* that is associated with the decoder. The timestamp must be within one second
* of the current `java/lang/System#nanoTime()` (which is implemented using
* `CLOCK_MONOTONIC` on Android). See the Android MediaCodec documentation
* of [`android/media/MediaCodec#releaseOutputBuffer(int,long)`][0] for more details.
*
* @param buffer the buffer to render
* @param time timestamp in nanoseconds of when to render the buffer
* @return 0 on success, < 0 otherwise
*
* [0]: https://developer.android.com/reference/android/media/MediaCodec#releaseOutputBuffer(int,%20long)
*/
int av_mediacodec_render_buffer_at_time(AVMediaCodecBuffer *buffer, int64_t time);
#endif /* AVCODEC_MEDIACODEC_H */

View File

@ -0,0 +1,846 @@
/*
* AVPacket public API
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_PACKET_H
#define AVCODEC_PACKET_H
#include <stddef.h>
#include <stdint.h>
#include "libavutil/attributes.h"
#include "libavutil/buffer.h"
#include "libavutil/dict.h"
#include "libavutil/rational.h"
#include "libavutil/version.h"
#include "libavcodec/version_major.h"
/**
* @defgroup lavc_packet_side_data AVPacketSideData
*
* Types and functions for working with AVPacketSideData.
* @{
*/
enum AVPacketSideDataType {
/**
* An AV_PKT_DATA_PALETTE side data packet contains exactly AVPALETTE_SIZE
* bytes worth of palette. This side data signals that a new palette is
* present.
*/
AV_PKT_DATA_PALETTE,
/**
* The AV_PKT_DATA_NEW_EXTRADATA is used to notify the codec or the format
* that the extradata buffer was changed and the receiving side should
* act upon it appropriately. The new extradata is embedded in the side
* data buffer and should be immediately used for processing the current
* frame or packet.
*/
AV_PKT_DATA_NEW_EXTRADATA,
/**
* An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
* @code
* u32le param_flags
* if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
* s32le channel_count
* if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
* u64le channel_layout
* if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
* s32le sample_rate
* if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
* s32le width
* s32le height
* @endcode
*/
AV_PKT_DATA_PARAM_CHANGE,
/**
* An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of
* structures with info about macroblocks relevant to splitting the
* packet into smaller packets on macroblock edges (e.g. as for RFC 2190).
* That is, it does not necessarily contain info about all macroblocks,
* as long as the distance between macroblocks in the info is smaller
* than the target payload size.
* Each MB info structure is 12 bytes, and is laid out as follows:
* @code
* u32le bit offset from the start of the packet
* u8 current quantizer at the start of the macroblock
* u8 GOB number
* u16le macroblock address within the GOB
* u8 horizontal MV predictor
* u8 vertical MV predictor
* u8 horizontal MV predictor for block number 3
* u8 vertical MV predictor for block number 3
* @endcode
*/
AV_PKT_DATA_H263_MB_INFO,
/**
* This side data should be associated with an audio stream and contains
* ReplayGain information in form of the AVReplayGain struct.
*/
AV_PKT_DATA_REPLAYGAIN,
/**
* This side data contains a 3x3 transformation matrix describing an affine
* transformation that needs to be applied to the decoded video frames for
* correct presentation.
*
* See libavutil/display.h for a detailed description of the data.
*/
AV_PKT_DATA_DISPLAYMATRIX,
/**
* This side data should be associated with a video stream and contains
* Stereoscopic 3D information in form of the AVStereo3D struct.
*/
AV_PKT_DATA_STEREO3D,
/**
* This side data should be associated with an audio stream and corresponds
* to enum AVAudioServiceType.
*/
AV_PKT_DATA_AUDIO_SERVICE_TYPE,
/**
* This side data contains quality related information from the encoder.
* @code
* u32le quality factor of the compressed frame. Allowed range is between 1 (good) and FF_LAMBDA_MAX (bad).
* u8 picture type
* u8 error count
* u16 reserved
* u64le[error count] sum of squared differences between encoder in and output
* @endcode
*/
AV_PKT_DATA_QUALITY_STATS,
/**
* This side data contains an integer value representing the stream index
* of a "fallback" track. A fallback track indicates an alternate
* track to use when the current track can not be decoded for some reason.
* e.g. no decoder available for codec.
*/
AV_PKT_DATA_FALLBACK_TRACK,
/**
* This side data corresponds to the AVCPBProperties struct.
*/
AV_PKT_DATA_CPB_PROPERTIES,
/**
* Recommmends skipping the specified number of samples
* @code
* u32le number of samples to skip from start of this packet
* u32le number of samples to skip from end of this packet
* u8 reason for start skip
* u8 reason for end skip (0=padding silence, 1=convergence)
* @endcode
*/
AV_PKT_DATA_SKIP_SAMPLES,
/**
* An AV_PKT_DATA_JP_DUALMONO side data packet indicates that
* the packet may contain "dual mono" audio specific to Japanese DTV
* and if it is true, recommends only the selected channel to be used.
* @code
* u8 selected channels (0=main/left, 1=sub/right, 2=both)
* @endcode
*/
AV_PKT_DATA_JP_DUALMONO,
/**
* A list of zero terminated key/value strings. There is no end marker for
* the list, so it is required to rely on the side data size to stop.
*/
AV_PKT_DATA_STRINGS_METADATA,
/**
* Subtitle event position
* @code
* u32le x1
* u32le y1
* u32le x2
* u32le y2
* @endcode
*/
AV_PKT_DATA_SUBTITLE_POSITION,
/**
* Data found in BlockAdditional element of matroska container. There is
* no end marker for the data, so it is required to rely on the side data
* size to recognize the end. 8 byte id (as found in BlockAddId) followed
* by data.
*/
AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
/**
* The optional first identifier line of a WebVTT cue.
*/
AV_PKT_DATA_WEBVTT_IDENTIFIER,
/**
* The optional settings (rendering instructions) that immediately
* follow the timestamp specifier of a WebVTT cue.
*/
AV_PKT_DATA_WEBVTT_SETTINGS,
/**
* A list of zero terminated key/value strings. There is no end marker for
* the list, so it is required to rely on the side data size to stop. This
* side data includes updated metadata which appeared in the stream.
*/
AV_PKT_DATA_METADATA_UPDATE,
/**
* MPEGTS stream ID as uint8_t, this is required to pass the stream ID
* information from the demuxer to the corresponding muxer.
*/
AV_PKT_DATA_MPEGTS_STREAM_ID,
/**
* Mastering display metadata (based on SMPTE-2086:2014). This metadata
* should be associated with a video stream and contains data in the form
* of the AVMasteringDisplayMetadata struct.
*/
AV_PKT_DATA_MASTERING_DISPLAY_METADATA,
/**
* This side data should be associated with a video stream and corresponds
* to the AVSphericalMapping structure.
*/
AV_PKT_DATA_SPHERICAL,
/**
* Content light level (based on CTA-861.3). This metadata should be
* associated with a video stream and contains data in the form of the
* AVContentLightMetadata struct.
*/
AV_PKT_DATA_CONTENT_LIGHT_LEVEL,
/**
* ATSC A53 Part 4 Closed Captions. This metadata should be associated with
* a video stream. A53 CC bitstream is stored as uint8_t in AVPacketSideData.data.
* The number of bytes of CC data is AVPacketSideData.size.
*/
AV_PKT_DATA_A53_CC,
/**
* This side data is encryption initialization data.
* The format is not part of ABI, use av_encryption_init_info_* methods to
* access.
*/
AV_PKT_DATA_ENCRYPTION_INIT_INFO,
/**
* This side data contains encryption info for how to decrypt the packet.
* The format is not part of ABI, use av_encryption_info_* methods to access.
*/
AV_PKT_DATA_ENCRYPTION_INFO,
/**
* Active Format Description data consisting of a single byte as specified
* in ETSI TS 101 154 using AVActiveFormatDescription enum.
*/
AV_PKT_DATA_AFD,
/**
* Producer Reference Time data corresponding to the AVProducerReferenceTime struct,
* usually exported by some encoders (on demand through the prft flag set in the
* AVCodecContext export_side_data field).
*/
AV_PKT_DATA_PRFT,
/**
* ICC profile data consisting of an opaque octet buffer following the
* format described by ISO 15076-1.
*/
AV_PKT_DATA_ICC_PROFILE,
/**
* DOVI configuration
* ref:
* dolby-vision-bitstreams-within-the-iso-base-media-file-format-v2.1.2, section 2.2
* dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2, section 3.3
* Tags are stored in struct AVDOVIDecoderConfigurationRecord.
*/
AV_PKT_DATA_DOVI_CONF,
/**
* Timecode which conforms to SMPTE ST 12-1:2014. The data is an array of 4 uint32_t
* where the first uint32_t describes how many (1-3) of the other timecodes are used.
* The timecode format is described in the documentation of av_timecode_get_smpte_from_framenum()
* function in libavutil/timecode.h.
*/
AV_PKT_DATA_S12M_TIMECODE,
/**
* HDR10+ dynamic metadata associated with a video frame. The metadata is in
* the form of the AVDynamicHDRPlus struct and contains
* information for color volume transform - application 4 of
* SMPTE 2094-40:2016 standard.
*/
AV_PKT_DATA_DYNAMIC_HDR10_PLUS,
/**
* The number of side data types.
* This is not part of the public API/ABI in the sense that it may
* change when new side data types are added.
* This must stay the last enum value.
* If its value becomes huge, some code using it
* needs to be updated as it assumes it to be smaller than other limits.
*/
AV_PKT_DATA_NB
};
#define AV_PKT_DATA_QUALITY_FACTOR AV_PKT_DATA_QUALITY_STATS //DEPRECATED
/**
* This structure stores auxiliary information for decoding, presenting, or
* otherwise processing the coded stream. It is typically exported by demuxers
* and encoders and can be fed to decoders and muxers either in a per packet
* basis, or as global side data (applying to the entire coded stream).
*
* Global side data is handled as follows:
* - During demuxing, it may be exported through
* @ref AVStream.codecpar.side_data "AVStream's codec parameters", which can
* then be passed as input to decoders through the
* @ref AVCodecContext.coded_side_data "decoder context's side data", for
* initialization.
* - For muxing, it can be fed through @ref AVStream.codecpar.side_data
* "AVStream's codec parameters", typically the output of encoders through
* the @ref AVCodecContext.coded_side_data "encoder context's side data", for
* initialization.
*
* Packet specific side data is handled as follows:
* - During demuxing, it may be exported through @ref AVPacket.side_data
* "AVPacket's side data", which can then be passed as input to decoders.
* - For muxing, it can be fed through @ref AVPacket.side_data "AVPacket's
* side data", typically the output of encoders.
*
* Different modules may accept or export different types of side data
* depending on media type and codec. Refer to @ref AVPacketSideDataType for a
* list of defined types and where they may be found or used.
*/
typedef struct AVPacketSideData {
uint8_t *data;
size_t size;
enum AVPacketSideDataType type;
} AVPacketSideData;
/**
* Allocate a new packet side data.
*
* @param sd pointer to an array of side data to which the side data should
* be added. *sd may be NULL, in which case the array will be
* initialized.
* @param nb_sd pointer to an integer containing the number of entries in
* the array. The integer value will be increased by 1 on success.
* @param type side data type
* @param size desired side data size
* @param flags currently unused. Must be zero
*
* @return pointer to freshly allocated side data on success, or NULL otherwise.
*/
AVPacketSideData *av_packet_side_data_new(AVPacketSideData **psd, int *pnb_sd,
enum AVPacketSideDataType type,
size_t size, int flags);
/**
* Wrap existing data as packet side data.
*
* @param sd pointer to an array of side data to which the side data should
* be added. *sd may be NULL, in which case the array will be
* initialized
* @param nb_sd pointer to an integer containing the number of entries in
* the array. The integer value will be increased by 1 on success.
* @param type side data type
* @param data a data array. It must be allocated with the av_malloc() family
* of functions. The ownership of the data is transferred to the
* side data array on success
* @param size size of the data array
* @param flags currently unused. Must be zero
*
* @return pointer to freshly allocated side data on success, or NULL otherwise
* On failure, the side data array is unchanged and the data remains
* owned by the caller.
*/
AVPacketSideData *av_packet_side_data_add(AVPacketSideData **sd, int *nb_sd,
enum AVPacketSideDataType type,
void *data, size_t size, int flags);
/**
* Get side information from a side data array.
*
* @param sd the array from which the side data should be fetched
* @param nb_sd value containing the number of entries in the array.
* @param type desired side information type
*
* @return pointer to side data if present or NULL otherwise
*/
const AVPacketSideData *av_packet_side_data_get(const AVPacketSideData *sd,
int nb_sd,
enum AVPacketSideDataType type);
/**
* Remove side data of the given type from a side data array.
*
* @param sd the array from which the side data should be removed
* @param nb_sd pointer to an integer containing the number of entries in
* the array. Will be reduced by the amount of entries removed
* upon return
* @param type side information type
*/
void av_packet_side_data_remove(AVPacketSideData *sd, int *nb_sd,
enum AVPacketSideDataType type);
/**
* Convenience function to free all the side data stored in an array, and
* the array itself.
*
* @param sd pointer to array of side data to free. Will be set to NULL
* upon return.
* @param nb_sd pointer to an integer containing the number of entries in
* the array. Will be set to 0 upon return.
*/
void av_packet_side_data_free(AVPacketSideData **sd, int *nb_sd);
const char *av_packet_side_data_name(enum AVPacketSideDataType type);
/**
* @}
*/
/**
* @defgroup lavc_packet AVPacket
*
* Types and functions for working with AVPacket.
* @{
*/
/**
* This structure stores compressed data. It is typically exported by demuxers
* and then passed as input to decoders, or received as output from encoders and
* then passed to muxers.
*
* For video, it should typically contain one compressed frame. For audio it may
* contain several compressed frames. Encoders are allowed to output empty
* packets, with no compressed data, containing only side data
* (e.g. to update some stream parameters at the end of encoding).
*
* The semantics of data ownership depends on the buf field.
* If it is set, the packet data is dynamically allocated and is
* valid indefinitely until a call to av_packet_unref() reduces the
* reference count to 0.
*
* If the buf field is not set av_packet_ref() would make a copy instead
* of increasing the reference count.
*
* The side data is always allocated with av_malloc(), copied by
* av_packet_ref() and freed by av_packet_unref().
*
* sizeof(AVPacket) being a part of the public ABI is deprecated. once
* av_init_packet() is removed, new packets will only be able to be allocated
* with av_packet_alloc(), and new fields may be added to the end of the struct
* with a minor bump.
*
* @see av_packet_alloc
* @see av_packet_ref
* @see av_packet_unref
*/
typedef struct AVPacket {
/**
* A reference to the reference-counted buffer where the packet data is
* stored.
* May be NULL, then the packet data is not reference-counted.
*/
AVBufferRef *buf;
/**
* Presentation timestamp in AVStream->time_base units; the time at which
* the decompressed packet will be presented to the user.
* Can be AV_NOPTS_VALUE if it is not stored in the file.
* pts MUST be larger or equal to dts as presentation cannot happen before
* decompression, unless one wants to view hex dumps. Some formats misuse
* the terms dts and pts/cts to mean something different. Such timestamps
* must be converted to true pts/dts before they are stored in AVPacket.
*/
int64_t pts;
/**
* Decompression timestamp in AVStream->time_base units; the time at which
* the packet is decompressed.
* Can be AV_NOPTS_VALUE if it is not stored in the file.
*/
int64_t dts;
uint8_t *data;
int size;
int stream_index;
/**
* A combination of AV_PKT_FLAG values
*/
int flags;
/**
* Additional packet data that can be provided by the container.
* Packet can contain several types of side information.
*/
AVPacketSideData *side_data;
int side_data_elems;
/**
* Duration of this packet in AVStream->time_base units, 0 if unknown.
* Equals next_pts - this_pts in presentation order.
*/
int64_t duration;
int64_t pos; ///< byte position in stream, -1 if unknown
/**
* for some private data of the user
*/
void *opaque;
/**
* AVBufferRef for free use by the API user. FFmpeg will never check the
* contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when
* the packet is unreferenced. av_packet_copy_props() calls create a new
* reference with av_buffer_ref() for the target packet's opaque_ref field.
*
* This is unrelated to the opaque field, although it serves a similar
* purpose.
*/
AVBufferRef *opaque_ref;
/**
* Time base of the packet's timestamps.
* In the future, this field may be set on packets output by encoders or
* demuxers, but its value will be by default ignored on input to decoders
* or muxers.
*/
AVRational time_base;
} AVPacket;
#if FF_API_INIT_PACKET
attribute_deprecated
typedef struct AVPacketList {
AVPacket pkt;
struct AVPacketList *next;
} AVPacketList;
#endif
#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
/**
* Flag is used to discard packets which are required to maintain valid
* decoder state but are not required for output and should be dropped
* after decoding.
**/
#define AV_PKT_FLAG_DISCARD 0x0004
/**
* The packet comes from a trusted source.
*
* Otherwise-unsafe constructs such as arbitrary pointers to data
* outside the packet may be followed.
*/
#define AV_PKT_FLAG_TRUSTED 0x0008
/**
* Flag is used to indicate packets that contain frames that can
* be discarded by the decoder. I.e. Non-reference frames.
*/
#define AV_PKT_FLAG_DISPOSABLE 0x0010
enum AVSideDataParamChangeFlags {
#if FF_API_OLD_CHANNEL_LAYOUT
/**
* @deprecated those are not used by any decoder
*/
AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
#endif
AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
};
/**
* Allocate an AVPacket and set its fields to default values. The resulting
* struct must be freed using av_packet_free().
*
* @return An AVPacket filled with default values or NULL on failure.
*
* @note this only allocates the AVPacket itself, not the data buffers. Those
* must be allocated through other means such as av_new_packet.
*
* @see av_new_packet
*/
AVPacket *av_packet_alloc(void);
/**
* Create a new packet that references the same data as src.
*
* This is a shortcut for av_packet_alloc()+av_packet_ref().
*
* @return newly created AVPacket on success, NULL on error.
*
* @see av_packet_alloc
* @see av_packet_ref
*/
AVPacket *av_packet_clone(const AVPacket *src);
/**
* Free the packet, if the packet is reference counted, it will be
* unreferenced first.
*
* @param pkt packet to be freed. The pointer will be set to NULL.
* @note passing NULL is a no-op.
*/
void av_packet_free(AVPacket **pkt);
#if FF_API_INIT_PACKET
/**
* Initialize optional fields of a packet with default values.
*
* Note, this does not touch the data and size members, which have to be
* initialized separately.
*
* @param pkt packet
*
* @see av_packet_alloc
* @see av_packet_unref
*
* @deprecated This function is deprecated. Once it's removed,
sizeof(AVPacket) will not be a part of the ABI anymore.
*/
attribute_deprecated
void av_init_packet(AVPacket *pkt);
#endif
/**
* Allocate the payload of a packet and initialize its fields with
* default values.
*
* @param pkt packet
* @param size wanted payload size
* @return 0 if OK, AVERROR_xxx otherwise
*/
int av_new_packet(AVPacket *pkt, int size);
/**
* Reduce packet size, correctly zeroing padding
*
* @param pkt packet
* @param size new size
*/
void av_shrink_packet(AVPacket *pkt, int size);
/**
* Increase packet size, correctly zeroing padding
*
* @param pkt packet
* @param grow_by number of bytes by which to increase the size of the packet
*/
int av_grow_packet(AVPacket *pkt, int grow_by);
/**
* Initialize a reference-counted packet from av_malloc()ed data.
*
* @param pkt packet to be initialized. This function will set the data, size,
* and buf fields, all others are left untouched.
* @param data Data allocated by av_malloc() to be used as packet data. If this
* function returns successfully, the data is owned by the underlying AVBuffer.
* The caller may not access the data through other means.
* @param size size of data in bytes, without the padding. I.e. the full buffer
* size is assumed to be size + AV_INPUT_BUFFER_PADDING_SIZE.
*
* @return 0 on success, a negative AVERROR on error
*/
int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size);
/**
* Allocate new information of a packet.
*
* @param pkt packet
* @param type side information type
* @param size side information size
* @return pointer to fresh allocated data or NULL otherwise
*/
uint8_t* av_packet_new_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
size_t size);
/**
* Wrap an existing array as a packet side data.
*
* @param pkt packet
* @param type side information type
* @param data the side data array. It must be allocated with the av_malloc()
* family of functions. The ownership of the data is transferred to
* pkt.
* @param size side information size
* @return a non-negative number on success, a negative AVERROR code on
* failure. On failure, the packet is unchanged and the data remains
* owned by the caller.
*/
int av_packet_add_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
uint8_t *data, size_t size);
/**
* Shrink the already allocated side data buffer
*
* @param pkt packet
* @param type side information type
* @param size new side information size
* @return 0 on success, < 0 on failure
*/
int av_packet_shrink_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
size_t size);
/**
* Get side information from packet.
*
* @param pkt packet
* @param type desired side information type
* @param size If supplied, *size will be set to the size of the side data
* or to zero if the desired side data is not present.
* @return pointer to data if present or NULL otherwise
*/
uint8_t* av_packet_get_side_data(const AVPacket *pkt, enum AVPacketSideDataType type,
size_t *size);
/**
* Pack a dictionary for use in side_data.
*
* @param dict The dictionary to pack.
* @param size pointer to store the size of the returned data
* @return pointer to data if successful, NULL otherwise
*/
uint8_t *av_packet_pack_dictionary(AVDictionary *dict, size_t *size);
/**
* Unpack a dictionary from side_data.
*
* @param data data from side_data
* @param size size of the data
* @param dict the metadata storage dictionary
* @return 0 on success, < 0 on failure
*/
int av_packet_unpack_dictionary(const uint8_t *data, size_t size,
AVDictionary **dict);
/**
* Convenience function to free all the side data stored.
* All the other fields stay untouched.
*
* @param pkt packet
*/
void av_packet_free_side_data(AVPacket *pkt);
/**
* Setup a new reference to the data described by a given packet
*
* If src is reference-counted, setup dst as a new reference to the
* buffer in src. Otherwise allocate a new buffer in dst and copy the
* data from src into it.
*
* All the other fields are copied from src.
*
* @see av_packet_unref
*
* @param dst Destination packet. Will be completely overwritten.
* @param src Source packet
*
* @return 0 on success, a negative AVERROR on error. On error, dst
* will be blank (as if returned by av_packet_alloc()).
*/
int av_packet_ref(AVPacket *dst, const AVPacket *src);
/**
* Wipe the packet.
*
* Unreference the buffer referenced by the packet and reset the
* remaining packet fields to their default values.
*
* @param pkt The packet to be unreferenced.
*/
void av_packet_unref(AVPacket *pkt);
/**
* Move every field in src to dst and reset src.
*
* @see av_packet_unref
*
* @param src Source packet, will be reset
* @param dst Destination packet
*/
void av_packet_move_ref(AVPacket *dst, AVPacket *src);
/**
* Copy only "properties" fields from src to dst.
*
* Properties for the purpose of this function are all the fields
* beside those related to the packet data (buf, data, size)
*
* @param dst Destination packet
* @param src Source packet
*
* @return 0 on success AVERROR on failure.
*/
int av_packet_copy_props(AVPacket *dst, const AVPacket *src);
/**
* Ensure the data described by a given packet is reference counted.
*
* @note This function does not ensure that the reference will be writable.
* Use av_packet_make_writable instead for that purpose.
*
* @see av_packet_ref
* @see av_packet_make_writable
*
* @param pkt packet whose data should be made reference counted.
*
* @return 0 on success, a negative AVERROR on error. On failure, the
* packet is unchanged.
*/
int av_packet_make_refcounted(AVPacket *pkt);
/**
* Create a writable reference for the data described by a given packet,
* avoiding data copy if possible.
*
* @param pkt Packet whose data should be made writable.
*
* @return 0 on success, a negative AVERROR on failure. On failure, the
* packet is unchanged.
*/
int av_packet_make_writable(AVPacket *pkt);
/**
* Convert valid timing fields (timestamps / durations) in a packet from one
* timebase to another. Timestamps with unknown values (AV_NOPTS_VALUE) will be
* ignored.
*
* @param pkt packet on which the conversion will be performed
* @param tb_src source timebase, in which the timing fields in pkt are
* expressed
* @param tb_dst destination timebase, to which the timing fields will be
* converted
*/
void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);
/**
* @}
*/
#endif // AVCODEC_PACKET_H

View File

@ -0,0 +1,109 @@
/*
* Intel MediaSDK QSV public API
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_QSV_H
#define AVCODEC_QSV_H
#include <mfxvideo.h>
#include "libavutil/buffer.h"
/**
* This struct is used for communicating QSV parameters between libavcodec and
* the caller. It is managed by the caller and must be assigned to
* AVCodecContext.hwaccel_context.
* - decoding: hwaccel_context must be set on return from the get_format()
* callback
* - encoding: hwaccel_context must be set before avcodec_open2()
*/
typedef struct AVQSVContext {
/**
* If non-NULL, the session to use for encoding or decoding.
* Otherwise, libavcodec will try to create an internal session.
*/
mfxSession session;
/**
* The IO pattern to use.
*/
int iopattern;
/**
* Extra buffers to pass to encoder or decoder initialization.
*/
mfxExtBuffer **ext_buffers;
int nb_ext_buffers;
/**
* Encoding only. If this field is set to non-zero by the caller, libavcodec
* will create an mfxExtOpaqueSurfaceAlloc extended buffer and pass it to
* the encoder initialization. This only makes sense if iopattern is also
* set to MFX_IOPATTERN_IN_OPAQUE_MEMORY.
*
* The number of allocated opaque surfaces will be the sum of the number
* required by the encoder and the user-provided value nb_opaque_surfaces.
* The array of the opaque surfaces will be exported to the caller through
* the opaque_surfaces field.
*
* The caller must set this field to zero for oneVPL (MFX_VERSION >= 2.0)
*/
int opaque_alloc;
/**
* Encoding only, and only if opaque_alloc is set to non-zero. Before
* calling avcodec_open2(), the caller should set this field to the number
* of extra opaque surfaces to allocate beyond what is required by the
* encoder.
*
* On return from avcodec_open2(), this field will be set by libavcodec to
* the total number of allocated opaque surfaces.
*/
int nb_opaque_surfaces;
/**
* Encoding only, and only if opaque_alloc is set to non-zero. On return
* from avcodec_open2(), this field will be used by libavcodec to export the
* array of the allocated opaque surfaces to the caller, so they can be
* passed to other parts of the pipeline.
*
* The buffer reference exported here is owned and managed by libavcodec,
* the callers should make their own reference with av_buffer_ref() and free
* it with av_buffer_unref() when it is no longer needed.
*
* The buffer data is an nb_opaque_surfaces-sized array of mfxFrameSurface1.
*/
AVBufferRef *opaque_surfaces;
/**
* Encoding only, and only if opaque_alloc is set to non-zero. On return
* from avcodec_open2(), this field will be set to the surface type used in
* the opaque allocation request.
*/
int opaque_alloc_type;
} AVQSVContext;
/**
* Allocate a new context.
*
* It must be freed by the caller with av_free().
*/
AVQSVContext *av_qsv_alloc_context(void);
#endif /* AVCODEC_QSV_H */

View File

@ -0,0 +1,157 @@
/*
* The Video Decode and Presentation API for UNIX (VDPAU) is used for
* hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
*
* Copyright (C) 2008 NVIDIA
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VDPAU_H
#define AVCODEC_VDPAU_H
/**
* @file
* @ingroup lavc_codec_hwaccel_vdpau
* Public libavcodec VDPAU header.
*/
/**
* @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
* @ingroup lavc_codec_hwaccel
*
* VDPAU hardware acceleration has two modules
* - VDPAU decoding
* - VDPAU presentation
*
* The VDPAU decoding module parses all headers using FFmpeg
* parsing mechanisms and uses VDPAU for the actual decoding.
*
* As per the current implementation, the actual decoding
* and rendering (API calls) are done as part of the VDPAU
* presentation (vo_vdpau.c) module.
*
* @{
*/
#include <vdpau/vdpau.h>
#include "libavutil/avconfig.h"
#include "libavutil/attributes.h"
#include "avcodec.h"
struct AVCodecContext;
struct AVFrame;
typedef int (*AVVDPAU_Render2)(struct AVCodecContext *, struct AVFrame *,
const VdpPictureInfo *, uint32_t,
const VdpBitstreamBuffer *);
/**
* This structure is used to share data between the libavcodec library and
* the client video application.
* The user shall allocate the structure via the av_alloc_vdpau_hwaccel
* function and make it available as
* AVCodecContext.hwaccel_context. Members can be set by the user once
* during initialization or through each AVCodecContext.get_buffer()
* function call. In any case, they must be valid prior to calling
* decoding functions.
*
* The size of this structure is not a part of the public ABI and must not
* be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an
* AVVDPAUContext.
*/
typedef struct AVVDPAUContext {
/**
* VDPAU decoder handle
*
* Set by user.
*/
VdpDecoder decoder;
/**
* VDPAU decoder render callback
*
* Set by the user.
*/
VdpDecoderRender *render;
AVVDPAU_Render2 render2;
} AVVDPAUContext;
/**
* @brief allocation function for AVVDPAUContext
*
* Allows extending the struct without breaking API/ABI
*/
AVVDPAUContext *av_alloc_vdpaucontext(void);
AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *);
void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2);
/**
* Associate a VDPAU device with a codec context for hardware acceleration.
* This function is meant to be called from the get_format() codec callback,
* or earlier. It can also be called after avcodec_flush_buffers() to change
* the underlying VDPAU device mid-stream (e.g. to recover from non-transparent
* display preemption).
*
* @note get_format() must return AV_PIX_FMT_VDPAU if this function completes
* successfully.
*
* @param avctx decoding context whose get_format() callback is invoked
* @param device VDPAU device handle to use for hardware acceleration
* @param get_proc_address VDPAU device driver
* @param flags zero of more OR'd AV_HWACCEL_FLAG_* flags
*
* @return 0 on success, an AVERROR code on failure.
*/
int av_vdpau_bind_context(AVCodecContext *avctx, VdpDevice device,
VdpGetProcAddress *get_proc_address, unsigned flags);
/**
* Gets the parameters to create an adequate VDPAU video surface for the codec
* context using VDPAU hardware decoding acceleration.
*
* @note Behavior is undefined if the context was not successfully bound to a
* VDPAU device using av_vdpau_bind_context().
*
* @param avctx the codec context being used for decoding the stream
* @param type storage space for the VDPAU video surface chroma type
* (or NULL to ignore)
* @param width storage space for the VDPAU video surface pixel width
* (or NULL to ignore)
* @param height storage space for the VDPAU video surface pixel height
* (or NULL to ignore)
*
* @return 0 on success, a negative AVERROR code on failure.
*/
int av_vdpau_get_surface_parameters(AVCodecContext *avctx, VdpChromaType *type,
uint32_t *width, uint32_t *height);
/**
* Allocate an AVVDPAUContext.
*
* @return Newly-allocated AVVDPAUContext or NULL on failure.
*/
AVVDPAUContext *av_vdpau_alloc_context(void);
/** @} */
#endif /* AVCODEC_VDPAU_H */

View File

@ -0,0 +1,45 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VERSION_H
#define AVCODEC_VERSION_H
/**
* @file
* @ingroup libavc
* Libavcodec version macros.
*/
#include "libavutil/version.h"
#include "version_major.h"
#define LIBAVCODEC_VERSION_MINOR 31
#define LIBAVCODEC_VERSION_MICRO 102
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
LIBAVCODEC_VERSION_MINOR, \
LIBAVCODEC_VERSION_MICRO)
#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
LIBAVCODEC_VERSION_MINOR, \
LIBAVCODEC_VERSION_MICRO)
#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
#endif /* AVCODEC_VERSION_H */

View File

@ -0,0 +1,59 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VERSION_MAJOR_H
#define AVCODEC_VERSION_MAJOR_H
/**
* @file
* @ingroup libavc
* Libavcodec version macros.
*/
#define LIBAVCODEC_VERSION_MAJOR 60
/**
* FF_API_* defines may be placed below to indicate public API that will be
* dropped at a future version bump. The defines themselves are not part of
* the public API and may change, break or disappear at any time.
*
* @note, when bumping the major version it is recommended to manually
* disable each FF_API_* in its own commit instead of disabling them all
* at once through the bump. This improves the git bisect-ability of the change.
*/
#define FF_API_INIT_PACKET (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_IDCT_NONE (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_SVTAV1_OPTS (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_AYUV_CODECID (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_VT_OUTPUT_CALLBACK (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_AVCODEC_CHROMA_POS (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_VT_HWACCEL_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_AVCTX_FRAME_NUMBER (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_SLICE_OFFSET (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_SUBFRAMES (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_TICKS_PER_FRAME (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_DROPCHANGED (LIBAVCODEC_VERSION_MAJOR < 61)
#define FF_API_AVFFT (LIBAVCODEC_VERSION_MAJOR < 62)
#define FF_API_FF_PROFILE_LEVEL (LIBAVCODEC_VERSION_MAJOR < 62)
// reminder to remove CrystalHD decoders on next major bump
#define FF_CODEC_CRYSTAL_HD (LIBAVCODEC_VERSION_MAJOR < 61)
#endif /* AVCODEC_VERSION_MAJOR_H */

View File

@ -0,0 +1,150 @@
/*
* Videotoolbox hardware acceleration
*
* copyright (c) 2012 Sebastien Zwickert
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VIDEOTOOLBOX_H
#define AVCODEC_VIDEOTOOLBOX_H
/**
* @file
* @ingroup lavc_codec_hwaccel_videotoolbox
* Public libavcodec Videotoolbox header.
*/
/**
* @defgroup lavc_codec_hwaccel_videotoolbox VideoToolbox Decoder
* @ingroup lavc_codec_hwaccel
*
* Hardware accelerated decoding using VideoToolbox on Apple Platforms
*
* @{
*/
#include <stdint.h>
#define Picture QuickdrawPicture
#include <VideoToolbox/VideoToolbox.h>
#undef Picture
#include "libavcodec/avcodec.h"
#include "libavutil/attributes.h"
/**
* This struct holds all the information that needs to be passed
* between the caller and libavcodec for initializing Videotoolbox decoding.
* Its size is not a part of the public ABI, it must be allocated with
* av_videotoolbox_alloc_context() and freed with av_free().
*/
typedef struct AVVideotoolboxContext {
/**
* Videotoolbox decompression session object.
*/
VTDecompressionSessionRef session;
#if FF_API_VT_OUTPUT_CALLBACK
/**
* The output callback that must be passed to the session.
* Set by av_videottoolbox_default_init()
*/
attribute_deprecated
VTDecompressionOutputCallback output_callback;
#endif
/**
* CVPixelBuffer Format Type that Videotoolbox will use for decoded frames.
* set by the caller. If this is set to 0, then no specific format is
* requested from the decoder, and its native format is output.
*/
OSType cv_pix_fmt_type;
/**
* CoreMedia Format Description that Videotoolbox will use to create the decompression session.
*/
CMVideoFormatDescriptionRef cm_fmt_desc;
/**
* CoreMedia codec type that Videotoolbox will use to create the decompression session.
*/
int cm_codec_type;
} AVVideotoolboxContext;
#if FF_API_VT_HWACCEL_CONTEXT
/**
* Allocate and initialize a Videotoolbox context.
*
* This function should be called from the get_format() callback when the caller
* selects the AV_PIX_FMT_VIDETOOLBOX format. The caller must then create
* the decoder object (using the output callback provided by libavcodec) that
* will be used for Videotoolbox-accelerated decoding.
*
* When decoding with Videotoolbox is finished, the caller must destroy the decoder
* object and free the Videotoolbox context using av_free().
*
* @return the newly allocated context or NULL on failure
* @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead.
*/
attribute_deprecated
AVVideotoolboxContext *av_videotoolbox_alloc_context(void);
/**
* This is a convenience function that creates and sets up the Videotoolbox context using
* an internal implementation.
*
* @param avctx the corresponding codec context
*
* @return >= 0 on success, a negative AVERROR code on failure
* @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead.
*/
attribute_deprecated
int av_videotoolbox_default_init(AVCodecContext *avctx);
/**
* This is a convenience function that creates and sets up the Videotoolbox context using
* an internal implementation.
*
* @param avctx the corresponding codec context
* @param vtctx the Videotoolbox context to use
*
* @return >= 0 on success, a negative AVERROR code on failure
* @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead.
*/
attribute_deprecated
int av_videotoolbox_default_init2(AVCodecContext *avctx, AVVideotoolboxContext *vtctx);
/**
* This function must be called to free the Videotoolbox context initialized with
* av_videotoolbox_default_init().
*
* @param avctx the corresponding codec context
* @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead.
*/
attribute_deprecated
void av_videotoolbox_default_free(AVCodecContext *avctx);
#endif /* FF_API_VT_HWACCEL_CONTEXT */
/**
* @}
*/
#endif /* AVCODEC_VIDEOTOOLBOX_H */

Some files were not shown because too many files have changed in this diff Show More