feat: add camera audio and QUIC streaming
This commit is contained in:
parent
9d27f903cd
commit
b9b6088800
@ -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
|
||||
|
||||
16
cmake/msquic/ProcessorCount.cmake
Normal file
16
cmake/msquic/ProcessorCount.cmake
Normal 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()
|
||||
@ -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)
|
||||
|
||||
@ -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_) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
buffer_[head_ % capacity_] = item;
|
||||
++head_;
|
||||
if (head_ - tail_ > capacity_) {
|
||||
// 队列满,覆盖最旧的数据
|
||||
tail_.store(tail + 1, std::memory_order_release);
|
||||
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};
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
244
cmvr-es/common/media/media_frame.h
Normal file
244
cmvr-es/common/media/media_frame.h
Normal 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
|
||||
25
cmvr-es/config/certs/cmvr-quic-ca.crt
Normal file
25
cmvr-es/config/certs/cmvr-quic-ca.crt
Normal 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-----
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
79
cmvr-es/config/tasks/quic_edge_task/quic_edge_task.pb.txt
Normal file
79
cmvr-es/config/tasks/quic_edge_task/quic_edge_task.pb.txt
Normal 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
|
||||
# }
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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_{};
|
||||
|
||||
@ -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()));
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,12 +324,14 @@ bool CameraStreamEncoder::init(std::shared_ptr<FfmpegEncoderInfo>& encoder,
|
||||
av_opt_set(ctx->priv_data, "no-open-gop", "1", 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);
|
||||
if (open_ret < 0) {
|
||||
@ -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,10 +419,8 @@ 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,
|
||||
encoder->sws_context = sws_getCachedContext(encoder->sws_context,
|
||||
frame_to_encode.cols,
|
||||
frame_to_encode.rows,
|
||||
src_pix_fmt,
|
||||
encoder->codec_context->width,
|
||||
@ -361,6 +435,14 @@ bool CameraStreamEncoder::encode(std::shared_ptr<FfmpegEncoderInfo>& encoder,
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
const AVFrame* source_frame = frame;
|
||||
if (frame->hw_frames_ctx) {
|
||||
if (!encoder->transfer_frame) {
|
||||
encoder->transfer_frame = av_frame_alloc();
|
||||
}
|
||||
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 sending frame to encoder: " << errbuf;
|
||||
av_strerror(transfer_ret, errbuf, sizeof(errbuf));
|
||||
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to download hardware frame: " << 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) {
|
||||
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(ret, errbuf, sizeof(errbuf));
|
||||
CMVR_LOG(ERROR) << "[CameraStreamEncoder] error receiving packet from encoder: " << errbuf;
|
||||
break;
|
||||
av_strerror(props_ret, errbuf, sizeof(errbuf));
|
||||
CMVR_LOG(ERROR) << "[CameraStreamEncoder] failed to copy hardware frame properties: " << errbuf;
|
||||
return false;
|
||||
}
|
||||
source_frame = encoder->transfer_frame;
|
||||
}
|
||||
|
||||
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) {
|
||||
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,
|
||||
|
||||
122
cmvr-es/devices/camera/hikvision_camera/CMakeLists.txt
Normal file
122
cmvr-es/devices/camera/hikvision_camera/CMakeLists.txt
Normal 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()
|
||||
@ -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
|
||||
971
cmvr-es/devices/camera/hikvision_camera/src/hikvision_camera.cpp
Normal file
971
cmvr-es/devices/camera/hikvision_camera/src/hikvision_camera.cpp
Normal 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
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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_);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (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_);
|
||||
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,19 +782,32 @@ 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图像编码
|
||||
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_)) {
|
||||
@ -772,31 +820,40 @@ void RealsenseCamera::streaming_worker_() {
|
||||
}
|
||||
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::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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
@ -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_{};
|
||||
|
||||
@ -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_;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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_{};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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_;
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
61
cmvr-es/manager/media_source_hub/CMakeLists.txt
Normal file
61
cmvr-es/manager/media_source_hub/CMakeLists.txt
Normal 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()
|
||||
@ -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
|
||||
132
cmvr-es/manager/media_source_hub/include/media_source_hub.h
Normal file
132
cmvr-es/manager/media_source_hub/include/media_source_hub.h
Normal 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
|
||||
@ -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
|
||||
596
cmvr-es/manager/media_source_hub/src/media_source_hub.cpp
Normal file
596
cmvr-es/manager/media_source_hub/src/media_source_hub.cpp
Normal 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
|
||||
683
cmvr-es/manager/media_source_hub/tests/media_source_hub_test.cpp
Normal file
683
cmvr-es/manager/media_source_hub/tests/media_source_hub_test.cpp
Normal 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;
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
60
cmvr-es/service/grpc/include/grpc_camera_stream_policy.h
Normal file
60
cmvr-es/service/grpc/include/grpc_camera_stream_policy.h
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,165 +517,16 @@ 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_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);
|
||||
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();
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
catch (exception &e) {
|
||||
api::GetDepthImageStreamCommand_Feedback response;
|
||||
response.mutable_header()->set_error_message(e.what());
|
||||
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
|
||||
stream->Write(response);
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
}
|
||||
grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* context
|
||||
, grpc::ServerReaderWriter<cmvr::api::GetRGBDImagesStreamCommand_Feedback, cmvr::api::GetRGBDImagesStreamCommand_Request>* stream){
|
||||
try {
|
||||
//读取首次传递的数据,获取设备id
|
||||
api::GetRGBDImagesStreamCommand_Request request;
|
||||
stream->Read(&request);
|
||||
string dev_id = request.header().device_id();
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): start,id=" << dev_id;
|
||||
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
||||
if (!dev) {
|
||||
api::GetRGBDImagesStreamCommand_Feedback response;
|
||||
response.mutable_header()->set_success(false);
|
||||
response.mutable_header()->set_error_message("Camera device not found: " + 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)
|
||||
{
|
||||
if (context->IsCancelled())
|
||||
{
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl](GetRGBDImagesStream) context is cancelled,id=" << dev_id;
|
||||
break;
|
||||
}
|
||||
|
||||
api::GetRGBDImagesStreamCommand_Feedback response;
|
||||
cmvr::device::StreamFrameData frame_data;
|
||||
dev->getEncodedFrame(frame_data,index);
|
||||
if (!frame_data.rgbFrame.empty()) {
|
||||
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_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_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);
|
||||
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] (GetRGBDImagesStream): end,id=" << dev_id;
|
||||
dev->stopStreaming();
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
catch (exception &e) {
|
||||
api::GetRGBDImagesStreamCommand_Feedback response;
|
||||
response.mutable_header()->set_error_message(e.what());
|
||||
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
|
||||
stream->Write(response);
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
}
|
||||
grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetRGBImageStreamCommand_Feedback, cmvr::api::GetRGBImageStreamCommand_Request>* stream){
|
||||
try {
|
||||
//读取首次传递的数据,获取设备id
|
||||
api::GetRGBImageStreamCommand_Request request;
|
||||
stream->Read(&request);
|
||||
string dev_id = request.header().device_id();
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBImageStream): start,id=" << dev_id;
|
||||
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
||||
if (!dev) {
|
||||
api::GetRGBImageStreamCommand_Feedback response;
|
||||
response.mutable_header()->set_success(false);
|
||||
response.mutable_header()->set_error_message("Camera device not found: " + dev_id);
|
||||
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
|
||||
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();
|
||||
while (true)
|
||||
{
|
||||
if (context->IsCancelled())
|
||||
{
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl](GetRGBImageStream) context is cancelled,id=" << dev_id;
|
||||
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));
|
||||
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_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.fy);
|
||||
@ -563,14 +542,380 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* conte
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (stream->Write) failed,id=" << dev_id;
|
||||
break;
|
||||
}
|
||||
last_sent_index = next_index;
|
||||
}
|
||||
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) {
|
||||
api::GetRGBImageStreamCommand_Feedback response;
|
||||
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);
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
}
|
||||
grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* context
|
||||
, grpc::ServerReaderWriter<cmvr::api::GetRGBDImagesStreamCommand_Feedback, cmvr::api::GetRGBDImagesStreamCommand_Request>* stream){
|
||||
try {
|
||||
//读取首次传递的数据,获取设备id
|
||||
api::GetRGBDImagesStreamCommand_Request request;
|
||||
if (!stream->Read(&request)) {
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
string dev_id = request.header().device_id();
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBDImagesStream): start,id=" << dev_id;
|
||||
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
||||
if (!dev) {
|
||||
api::GetRGBDImagesStreamCommand_Feedback response;
|
||||
response.mutable_header()->set_success(false);
|
||||
response.mutable_header()->set_error_message("Camera device not found: " + dev_id);
|
||||
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
|
||||
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;
|
||||
size_t index = 0;
|
||||
while (true)
|
||||
{
|
||||
if (context->IsCancelled())
|
||||
{
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl](GetRGBDImagesStream) context is cancelled,id=" << dev_id;
|
||||
break;
|
||||
}
|
||||
|
||||
api::GetRGBDImagesStreamCommand_Feedback response;
|
||||
cmvr::device::StreamFrameData frame_data;
|
||||
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("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.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++);
|
||||
|
||||
if (!stream->Write(response)) {
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (stream->Write) failed,id=" << dev_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetRGBDImagesStream): end,id=" << dev_id;
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
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);
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
}
|
||||
grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetRGBImageStreamCommand_Feedback, cmvr::api::GetRGBImageStreamCommand_Request>* stream){
|
||||
try {
|
||||
//读取首次传递的数据,获取设备id
|
||||
api::GetRGBImageStreamCommand_Request 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
|
||||
<< ", 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;
|
||||
response.mutable_header()->set_success(false);
|
||||
response.mutable_header()->set_error_message("Camera device not found: " + dev_id);
|
||||
setCurrentTimestamp(response.mutable_header()->mutable_timestamp());
|
||||
stream->Write(response);
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
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())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const auto& frame = *read->value;
|
||||
const auto descriptor = frame.descriptor;
|
||||
if (!descriptor) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 (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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
49
cmvr-es/service/quic_edge/CMakeLists.txt
Normal file
49
cmvr-es/service/quic_edge/CMakeLists.txt
Normal 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)
|
||||
54
cmvr-es/service/quic_edge/include/control_framing.h
Normal file
54
cmvr-es/service/quic_edge/include/control_framing.h
Normal 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
|
||||
47
cmvr-es/service/quic_edge/include/datagram_packetizer.h
Normal file
47
cmvr-es/service/quic_edge/include/datagram_packetizer.h
Normal 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
|
||||
205
cmvr-es/service/quic_edge/include/quic_edge_service.h
Normal file
205
cmvr-es/service/quic_edge/include/quic_edge_service.h
Normal 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
|
||||
118
cmvr-es/service/quic_edge/include/quic_edge_types.h
Normal file
118
cmvr-es/service/quic_edge/include/quic_edge_types.h
Normal 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
|
||||
73
cmvr-es/service/quic_edge/include/quic_transport.h
Normal file
73
cmvr-es/service/quic_edge/include/quic_transport.h
Normal 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
|
||||
115
cmvr-es/service/quic_edge/src/control_framing.cpp
Normal file
115
cmvr-es/service/quic_edge/src/control_framing.cpp
Normal 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
|
||||
270
cmvr-es/service/quic_edge/src/datagram_packetizer.cpp
Normal file
270
cmvr-es/service/quic_edge/src/datagram_packetizer.cpp
Normal 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
|
||||
856
cmvr-es/service/quic_edge/src/msquic_transport.cpp
Normal file
856
cmvr-es/service/quic_edge/src/msquic_transport.cpp
Normal 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(®istration_config, ®istration_);
|
||||
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
|
||||
66
cmvr-es/service/quic_edge/src/quic_edge_device_adapter.cpp
Normal file
66
cmvr-es/service/quic_edge/src/quic_edge_device_adapter.cpp
Normal 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
|
||||
1806
cmvr-es/service/quic_edge/src/quic_edge_service.cpp
Normal file
1806
cmvr-es/service/quic_edge/src/quic_edge_service.cpp
Normal file
File diff suppressed because it is too large
Load Diff
40
cmvr-es/service/quic_edge/src/quic_edge_types.cpp
Normal file
40
cmvr-es/service/quic_edge/src/quic_edge_types.cpp
Normal 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
|
||||
31
cmvr-es/service/quic_edge/src/quic_transport.cpp
Normal file
31
cmvr-es/service/quic_edge/src/quic_transport.cpp
Normal 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
|
||||
980
cmvr-es/service/quic_edge/tests/quic_edge_protocol_test.cpp
Normal file
980
cmvr-es/service/quic_edge/tests/quic_edge_protocol_test.cpp
Normal 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;
|
||||
}
|
||||
36
cmvr-es/task/quic_edge_task/CMakeLists.txt
Normal file
36
cmvr-es/task/quic_edge_task/CMakeLists.txt
Normal 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()
|
||||
65
cmvr-es/task/quic_edge_task/include/quic_edge_task.h
Normal file
65
cmvr-es/task/quic_edge_task/include/quic_edge_task.h
Normal 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
|
||||
372
cmvr-es/task/quic_edge_task/src/quic_edge_task.cpp
Normal file
372
cmvr-es/task/quic_edge_task/src/quic_edge_task.cpp
Normal 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
|
||||
340
cmvr-es/task/quic_edge_task/tests/quic_edge_task_test.cpp
Normal file
340
cmvr-es/task/quic_edge_task/tests/quic_edge_task_test.cpp
Normal 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;
|
||||
}
|
||||
BIN
dependency/x86/third_party/ffmpeg/v6.1/bin/ffmpeg
vendored
Executable file
BIN
dependency/x86/third_party/ffmpeg/v6.1/bin/ffmpeg
vendored
Executable file
Binary file not shown.
BIN
dependency/x86/third_party/ffmpeg/v6.1/bin/ffprobe
vendored
Executable file
BIN
dependency/x86/third_party/ffmpeg/v6.1/bin/ffprobe
vendored
Executable file
Binary file not shown.
36
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/ac3_parser.h
vendored
Normal file
36
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/ac3_parser.h
vendored
Normal 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 */
|
||||
37
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/adts_parser.h
vendored
Normal file
37
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/adts_parser.h
vendored
Normal 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 */
|
||||
3158
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/avcodec.h
vendored
Normal file
3158
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/avcodec.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
88
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/avdct.h
vendored
Normal file
88
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/avdct.h
vendored
Normal 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 */
|
||||
149
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/avfft.h
vendored
Normal file
149
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/avfft.h
vendored
Normal 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 */
|
||||
332
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/bsf.h
vendored
Normal file
332
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/bsf.h
vendored
Normal 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
|
||||
378
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec.h
vendored
Normal file
378
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec.h
vendored
Normal 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 */
|
||||
134
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec_desc.h
vendored
Normal file
134
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec_desc.h
vendored
Normal 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
|
||||
668
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec_id.h
vendored
Normal file
668
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec_id.h
vendored
Normal 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
|
||||
262
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec_par.h
vendored
Normal file
262
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/codec_par.h
vendored
Normal 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
|
||||
112
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/d3d11va.h
vendored
Normal file
112
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/d3d11va.h
vendored
Normal 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 */
|
||||
335
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/defs.h
vendored
Normal file
335
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/defs.h
vendored
Normal 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
|
||||
135
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/dirac.h
vendored
Normal file
135
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/dirac.h
vendored
Normal 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 */
|
||||
82
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/dv_profile.h
vendored
Normal file
82
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/dv_profile.h
vendored
Normal 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 */
|
||||
93
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/dxva2.h
vendored
Normal file
93
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/dxva2.h
vendored
Normal 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 */
|
||||
46
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/jni.h
vendored
Normal file
46
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/jni.h
vendored
Normal 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 */
|
||||
103
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/mediacodec.h
vendored
Normal file
103
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/mediacodec.h
vendored
Normal 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 */
|
||||
846
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/packet.h
vendored
Normal file
846
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/packet.h
vendored
Normal 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
|
||||
109
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/qsv.h
vendored
Normal file
109
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/qsv.h
vendored
Normal 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 */
|
||||
157
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/vdpau.h
vendored
Normal file
157
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/vdpau.h
vendored
Normal 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 */
|
||||
45
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/version.h
vendored
Normal file
45
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/version.h
vendored
Normal 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 */
|
||||
59
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/version_major.h
vendored
Normal file
59
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/version_major.h
vendored
Normal 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 */
|
||||
150
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/videotoolbox.h
vendored
Normal file
150
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/videotoolbox.h
vendored
Normal 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 */
|
||||
74
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/vorbis_parser.h
vendored
Normal file
74
dependency/x86/third_party/ffmpeg/v6.1/include/libavcodec/vorbis_parser.h
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file
|
||||
* A public API for Vorbis parsing
|
||||
*
|
||||
* Determines the duration for each packet.
|
||||
*/
|
||||
|
||||
#ifndef AVCODEC_VORBIS_PARSER_H
|
||||
#define AVCODEC_VORBIS_PARSER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct AVVorbisParseContext AVVorbisParseContext;
|
||||
|
||||
/**
|
||||
* Allocate and initialize the Vorbis parser using headers in the extradata.
|
||||
*/
|
||||
AVVorbisParseContext *av_vorbis_parse_init(const uint8_t *extradata,
|
||||
int extradata_size);
|
||||
|
||||
/**
|
||||
* Free the parser and everything associated with it.
|
||||
*/
|
||||
void av_vorbis_parse_free(AVVorbisParseContext **s);
|
||||
|
||||
#define VORBIS_FLAG_HEADER 0x00000001
|
||||
#define VORBIS_FLAG_COMMENT 0x00000002
|
||||
#define VORBIS_FLAG_SETUP 0x00000004
|
||||
|
||||
/**
|
||||
* Get the duration for a Vorbis packet.
|
||||
*
|
||||
* If @p flags is @c NULL,
|
||||
* special frames are considered invalid.
|
||||
*
|
||||
* @param s Vorbis parser context
|
||||
* @param buf buffer containing a Vorbis frame
|
||||
* @param buf_size size of the buffer
|
||||
* @param flags flags for special frames
|
||||
*/
|
||||
int av_vorbis_parse_frame_flags(AVVorbisParseContext *s, const uint8_t *buf,
|
||||
int buf_size, int *flags);
|
||||
|
||||
/**
|
||||
* Get the duration for a Vorbis packet.
|
||||
*
|
||||
* @param s Vorbis parser context
|
||||
* @param buf buffer containing a Vorbis frame
|
||||
* @param buf_size size of the buffer
|
||||
*/
|
||||
int av_vorbis_parse_frame(AVVorbisParseContext *s, const uint8_t *buf,
|
||||
int buf_size);
|
||||
|
||||
void av_vorbis_parse_reset(AVVorbisParseContext *s);
|
||||
|
||||
#endif /* AVCODEC_VORBIS_PARSER_H */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user