Define call video capabilities and bounded frame assembly
This commit is contained in:
1 parent
5fbea21482
commit
c8bbce6a94
2 files changed
+238
No files matched your search
@@ -0,0 +1,175 @@
|
||||
#include "call_video.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include <string.h>
|
||||
|
||||
static uint64_t read_be(const uint8_t *p, unsigned count) {
|
||||
uint64_t value = 0;
|
||||
for (unsigned i = 0; i < count; i++) value = (value << 8) | p[i];
|
||||
return value;
|
||||
}
|
||||
static void write_be(uint8_t *p, uint64_t value, unsigned count) {
|
||||
for (unsigned i = count; i; i--) { p[i - 1] = (uint8_t)value; value >>= 8; }
|
||||
}
|
||||
static int limit_valid(const struct call_video_limit *l) {
|
||||
if (!l->width && !l->height && !l->fps && !l->pixels_per_second) return 1;
|
||||
return l->width >= 16 && l->width <= 4096 && l->height >= 16 && l->height <= 4096
|
||||
&& l->fps >= 1 && l->fps <= 60 && l->pixels_per_second >= 256 && l->pixels_per_second <= 4096u * 4096u * 60u;
|
||||
}
|
||||
int call_video_caps_valid(const struct call_video_caps *caps) {
|
||||
if (!caps) return 0;
|
||||
for (unsigned i = 0; i < 2; i++) if (!limit_valid(&caps->encode[i]) || !limit_valid(&caps->decode[i])) return 0;
|
||||
return 1;
|
||||
}
|
||||
static int fits(const struct call_video_limit *l, uint16_t width, uint16_t height, uint16_t fps) {
|
||||
return width <= l->width && height <= l->height && fps <= l->fps && (uint64_t)width * height * fps <= l->pixels_per_second;
|
||||
}
|
||||
int call_video_choose(const struct call_video_caps *sender, const struct call_video_caps *receiver,
|
||||
int requested, uint16_t width, uint16_t height, uint16_t fps) {
|
||||
if (!call_video_caps_valid(sender) || !call_video_caps_valid(receiver) || requested < 0 || requested > 2
|
||||
|| width < 16 || height < 16 || width > 4096 || height > 4096 || width % 2 || height % 2 || !fps || fps > 60) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video choose invalid request codec=%d size=%ux%u fps=%u", requested, width, height, fps);
|
||||
return -1;
|
||||
}
|
||||
for (int codec = 2; codec >= 1; codec--) {
|
||||
if (requested && codec != requested) continue;
|
||||
if (fits(&sender->encode[codec - 1], width, height, fps) && fits(&receiver->decode[codec - 1], width, height, fps)) return codec;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video no common mode codec=%d size=%ux%u fps=%u", requested, width, height, fps);
|
||||
return 0;
|
||||
}
|
||||
int call_video_format_valid(const struct call_video_format *f) {
|
||||
return f && f->generation && f->codec >= 1 && f->codec <= 2 && f->width >= 16 && f->height >= 16
|
||||
&& f->width <= 4096 && f->height <= 4096 && !(f->width % 2) && !(f->height % 2)
|
||||
&& f->fps >= 1 && f->fps <= 60 && (f->rotation == 0 || f->rotation == 90 || f->rotation == 180 || f->rotation == 270);
|
||||
}
|
||||
int call_video_same_format(const struct call_video_format *a, const struct call_video_format *b) {
|
||||
return a->generation == b->generation && a->width == b->width && a->height == b->height
|
||||
&& a->fps == b->fps && a->rotation == b->rotation && a->codec == b->codec;
|
||||
}
|
||||
int call_video_caps_encode(const struct call_video_caps *caps, uint8_t out[CALL_VIDEO_CAPS_BYTES]) {
|
||||
if (!out || !call_video_caps_valid(caps)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video capabilities encode invalid arguments"); return -1;
|
||||
}
|
||||
for (unsigned i = 0; i < 4; i++) {
|
||||
const struct call_video_limit *l = i < 2 ? &caps->encode[i] : &caps->decode[i - 2];
|
||||
write_be(out + i * 10, l->width, 2); write_be(out + i * 10 + 2, l->height, 2);
|
||||
write_be(out + i * 10 + 4, l->fps, 2); write_be(out + i * 10 + 6, l->pixels_per_second, 4);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int call_video_caps_parse(const uint8_t *data, size_t length, struct call_video_caps *out) {
|
||||
if (!data || !out || length != CALL_VIDEO_CAPS_BYTES) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "video capabilities invalid size=%zu", length); return -1;
|
||||
}
|
||||
struct call_video_caps caps = {0};
|
||||
for (unsigned i = 0; i < 4; i++) {
|
||||
struct call_video_limit *l = i < 2 ? &caps.encode[i] : &caps.decode[i - 2];
|
||||
l->width = read_be(data + i * 10, 2); l->height = read_be(data + i * 10 + 2, 2);
|
||||
l->fps = read_be(data + i * 10 + 4, 2); l->pixels_per_second = read_be(data + i * 10 + 6, 4);
|
||||
}
|
||||
if (!call_video_caps_valid(&caps)) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "video capabilities invalid limits"); return -1; }
|
||||
*out = caps; return 0;
|
||||
}
|
||||
static int fragment_valid(const struct call_video_frame *f) {
|
||||
return f && f->call_id && f->frame_id && f->pts_us >= 0 && f->pts_us <= INT64_MAX / 2 && call_video_format_valid(&f->format)
|
||||
&& !(f->flags & ~CALL_VIDEO_KEY) && f->data && f->size && f->size <= CALL_VIDEO_MAX_FRAME
|
||||
&& f->offset < f->size && f->length && f->length <= CALL_VIDEO_FRAGMENT_BYTES && f->length <= f->size - f->offset;
|
||||
}
|
||||
int call_video_fragment_encode(const struct call_video_frame *f, uint8_t *out, size_t capacity, size_t *length) {
|
||||
if (!fragment_valid(f) || !out || !length || capacity < CALL_VIDEO_HEADER_BYTES + f->length) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video fragment encode invalid arguments"); return -1;
|
||||
}
|
||||
write_be(out, f->call_id, 8); write_be(out + 8, f->format.generation, 8);
|
||||
write_be(out + 16, f->frame_id, 8); write_be(out + 24, f->pts_us, 8);
|
||||
write_be(out + 32, f->format.width, 2); write_be(out + 34, f->format.height, 2);
|
||||
write_be(out + 36, f->format.fps, 2); write_be(out + 38, f->format.rotation, 2);
|
||||
out[40] = f->format.codec; out[41] = f->flags; out[42] = out[43] = 0;
|
||||
write_be(out + 44, f->size, 4); write_be(out + 48, f->offset, 4);
|
||||
memcpy(out + CALL_VIDEO_HEADER_BYTES, f->data, f->length);
|
||||
*length = CALL_VIDEO_HEADER_BYTES + f->length; return 0;
|
||||
}
|
||||
int call_video_fragment_parse(const uint8_t *data, size_t length, struct call_video_frame *out) {
|
||||
if (!data || !out || length <= CALL_VIDEO_HEADER_BYTES || length > CALL_VIDEO_HEADER_BYTES + CALL_VIDEO_FRAGMENT_BYTES) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "video fragment invalid size=%zu", length); return -1;
|
||||
}
|
||||
struct call_video_frame f = {0};
|
||||
f.call_id = read_be(data, 8); f.format.generation = read_be(data + 8, 8);
|
||||
f.frame_id = read_be(data + 16, 8); f.pts_us = (int64_t)read_be(data + 24, 8);
|
||||
f.format.width = read_be(data + 32, 2); f.format.height = read_be(data + 34, 2);
|
||||
f.format.fps = read_be(data + 36, 2); f.format.rotation = read_be(data + 38, 2);
|
||||
f.format.codec = data[40]; f.flags = data[41];
|
||||
f.size = read_be(data + 44, 4); f.offset = read_be(data + 48, 4);
|
||||
f.length = length - CALL_VIDEO_HEADER_BYTES; f.data = data + CALL_VIDEO_HEADER_BYTES;
|
||||
if (data[42] || data[43] || !fragment_valid(&f)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "video fragment invalid fields frame=%llu size=%u offset=%u bytes=%u",
|
||||
(unsigned long long)f.frame_id, f.size, f.offset, f.length); return -1;
|
||||
}
|
||||
*out = f; return 0;
|
||||
}
|
||||
|
||||
struct call_video_receiver {
|
||||
struct call_video_frame frame;
|
||||
uint8_t *data;
|
||||
uint32_t capacity, received;
|
||||
uint64_t started_us, last_us, last_id;
|
||||
int64_t last_pts;
|
||||
int need_key, active;
|
||||
};
|
||||
struct call_video_receiver *call_video_receiver_create(void) {
|
||||
struct call_video_receiver *r = u_calloc(1, sizeof(*r));
|
||||
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video receiver allocation failed"); return NULL; }
|
||||
r->need_key = 1; r->last_pts = -1; return r;
|
||||
}
|
||||
void call_video_receiver_destroy(struct call_video_receiver *r) { if (r) { u_free(r->data); u_free(r); } }
|
||||
void call_video_receiver_reset(struct call_video_receiver *r) {
|
||||
if (!r) return;
|
||||
u_free(r->data); memset(r, 0, sizeof(*r)); r->need_key = 1; r->last_pts = -1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video receiver reset");
|
||||
}
|
||||
int call_video_receiver_needs_key(const struct call_video_receiver *r) { return !r || r->need_key; }
|
||||
static int receiver_fail(struct call_video_receiver *r, const char *reason) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "video assembly rejected reason=%s frame=%llu received=%u total=%u",
|
||||
reason, (unsigned long long)r->frame.frame_id, r->received, r->frame.size);
|
||||
r->active = 0; r->received = 0; r->need_key = 1; return -1;
|
||||
}
|
||||
int call_video_receiver_push(struct call_video_receiver *r, const struct call_video_frame *f,
|
||||
uint64_t now_us, struct call_video_frame *complete) {
|
||||
if (!r || !complete || !fragment_valid(f)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "video receiver invalid arguments"); return -1;
|
||||
}
|
||||
if (now_us < r->last_us) return receiver_fail(r, "clock_regression");
|
||||
r->last_us = now_us;
|
||||
if (r->active && now_us - r->started_us > 2000000) receiver_fail(r, "assembly_timeout");
|
||||
if (!f->offset) {
|
||||
if (f->frame_id <= r->last_id || f->pts_us <= r->last_pts || f->format.generation < r->frame.format.generation)
|
||||
return receiver_fail(r, "stale_frame");
|
||||
if (r->active) receiver_fail(r, "incomplete_frame");
|
||||
if (r->last_id && f->frame_id != r->last_id + 1) r->need_key = 1;
|
||||
int changed = !call_video_same_format(&f->format, &r->frame.format);
|
||||
if (changed && f->format.generation == r->frame.format.generation) return receiver_fail(r, "format_without_generation");
|
||||
if (changed) r->need_key = 1;
|
||||
if (r->need_key && !(f->flags & CALL_VIDEO_KEY)) return 0;
|
||||
if (r->capacity < f->size) {
|
||||
uint8_t *data = u_realloc(r->data, f->size);
|
||||
if (!data) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video frame allocation failed bytes=%u", f->size); return receiver_fail(r, "allocation"); }
|
||||
r->data = data; r->capacity = f->size;
|
||||
}
|
||||
r->frame = *f; r->frame.data = r->data; r->received = 0; r->started_us = now_us; r->active = 1;
|
||||
}
|
||||
if (!r->active) return 0;
|
||||
if (f->call_id != r->frame.call_id || f->frame_id != r->frame.frame_id || f->pts_us != r->frame.pts_us
|
||||
|| f->size != r->frame.size || f->flags != r->frame.flags || !call_video_same_format(&f->format, &r->frame.format)
|
||||
|| f->offset != r->received) return receiver_fail(r, "fragment_mismatch");
|
||||
memcpy(r->data + r->received, f->data, f->length); r->received += f->length;
|
||||
if (r->received != r->frame.size) return 0;
|
||||
r->active = 0; r->last_id = f->frame_id; r->last_pts = f->pts_us;
|
||||
if (f->flags & CALL_VIDEO_KEY) {
|
||||
if (f->size < 5 || read_be(r->data, 4) > 65536 || read_be(r->data, 4) >= f->size - 4)
|
||||
return receiver_fail(r, "key_envelope");
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video KEY assembled generation=%llu frame=%llu pts=%lld bytes=%u codec=%u size=%ux%u",
|
||||
(unsigned long long)f->format.generation, (unsigned long long)f->frame_id, (long long)f->pts_us,
|
||||
f->size, f->format.codec, f->format.width, f->format.height);
|
||||
}
|
||||
r->need_key = 0; r->frame.offset = 0; r->frame.length = r->frame.size; *complete = r->frame; return 1;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* Общий контракт видеотрека. Все операции выполняет владелец в uasync-потоке.
|
||||
* Кодеки: H.264 Constrained Baseline / H.265 Main, 8 bit, 4:2:0, без B-кадров.
|
||||
* Поколение меняется только при смене формата/камеры; bitrate не меняет поколение.
|
||||
* KEY: config_length:4BE + Annex B configuration + Annex B access unit (как uAV2).
|
||||
* Приём хранит один частичный кадр. Готовый view заимствован до следующего push/reset.
|
||||
* Очередью готовых кадров, decoder и показом по audio clock владеет вызывающий. */
|
||||
#ifndef CALL_VIDEO_H
|
||||
#define CALL_VIDEO_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CALL_VIDEO_H264 1
|
||||
#define CALL_VIDEO_H265 2
|
||||
#define CALL_VIDEO_KEY 1
|
||||
#define CALL_VIDEO_MAX_FRAME (2u * 1024u * 1024u)
|
||||
#define CALL_VIDEO_FRAGMENT_BYTES 1200u
|
||||
#define CALL_VIDEO_HEADER_BYTES 52u
|
||||
#define CALL_VIDEO_CAPS_BYTES 40u
|
||||
|
||||
/* Нулевой limit означает отсутствие проверенного encoder/decoder данного профиля.
|
||||
* Индексы 0=H.264, 1=H.265. Лимиты независимы для каждого направления. */
|
||||
struct call_video_limit { uint16_t width, height, fps; uint32_t pixels_per_second; };
|
||||
struct call_video_caps { struct call_video_limit encode[2], decode[2]; };
|
||||
struct call_video_format { uint64_t generation; uint16_t width, height, fps, rotation; uint8_t codec; };
|
||||
struct call_video_frame {
|
||||
uint64_t call_id, frame_id;
|
||||
int64_t pts_us;
|
||||
struct call_video_format format;
|
||||
uint32_t size, offset, length;
|
||||
uint8_t flags;
|
||||
const uint8_t *data;
|
||||
};
|
||||
|
||||
int call_video_caps_valid(const struct call_video_caps *caps);
|
||||
/* requested=0: H.265, затем H.264. 0 — нет совместимого режима; -1 — неверные аргументы. */
|
||||
int call_video_choose(const struct call_video_caps *sender, const struct call_video_caps *receiver,
|
||||
int requested, uint16_t width, uint16_t height, uint16_t fps);
|
||||
int call_video_format_valid(const struct call_video_format *format);
|
||||
int call_video_same_format(const struct call_video_format *a, const struct call_video_format *b);
|
||||
int call_video_caps_encode(const struct call_video_caps *caps, uint8_t out[CALL_VIDEO_CAPS_BYTES]);
|
||||
int call_video_caps_parse(const uint8_t *data, size_t length, struct call_video_caps *out);
|
||||
/* Весь header проверяется до записи/публикации view. Отказ: -1, выход не меняется. */
|
||||
int call_video_fragment_encode(const struct call_video_frame *frame, uint8_t *out, size_t capacity, size_t *length);
|
||||
int call_video_fragment_parse(const uint8_t *data, size_t length, struct call_video_frame *out);
|
||||
|
||||
struct call_video_receiver;
|
||||
struct call_video_receiver *call_video_receiver_create(void);
|
||||
void call_video_receiver_destroy(struct call_video_receiver *receiver);
|
||||
void call_video_receiver_reset(struct call_video_receiver *receiver);
|
||||
/* 1 — полный кадр, 0 — частичный/пропущенный, -1 — неверный поток; после пропуска нужен KEY.
|
||||
* bytes — строго один пакет, уже проверенный на принадлежность call_id/peer/group владельцем.
|
||||
* now_us — монотонное время для ограничения жизни частичной сборки (2с).
|
||||
* Новый формат принимается только с KEY. Старые готовые кадры вызывающий сохраняет до их PTS. */
|
||||
int call_video_receiver_push(struct call_video_receiver *receiver, const struct call_video_frame *fragment,
|
||||
uint64_t now_us, struct call_video_frame *complete);
|
||||
int call_video_receiver_needs_key(const struct call_video_receiver *receiver);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
Reference in new issue
Block a user