原始像素太卡 走ffmpeg

This commit is contained in:
2026-08-29 20:24:11 +08:00
parent 45783201e4
commit 20be348a6a
27 changed files with 1310 additions and 684 deletions
-84
View File
@@ -1,84 +0,0 @@
#pragma once
#include "frame_sampling/Frame_Sampler.hpp"
#include <Frame_Policy/Frame_Scheduler.hpp>
#include <frame_statistics.hpp>
#include <ownership.hpp>
#include <array>
#include <atomic>
#include <chrono>
namespace aethera::web {
struct Gallery_Video_Stream::Private : Prev_Private {
using Object = Gallery_Video_Stream;
struct Source {
Plot_Entry entry; /* 布局槽位关联的实际 Plot。 */
Plot::Stream_Id stream{}; /* Plot 完成帧的唯一订阅标识。 */
};
struct Consumer {
Stream_Id id{}; /* 当前网页媒体连接的订阅身份。 */
std::string connection{}; /* 同一浏览器页重连时保持稳定的协议身份。 */
std::shared_ptr<const Stream_Handler> handler{}; /* 发布期间保持回调存活。 */
std::shared_ptr<const Transport_Readiness> readiness{}; /* 采样前查询对应传输窗口。 */
std::shared_ptr<const Transport_Diagnostics> diagnostics{}; /* 连接自己的传输诊断来源。 */
};
static constexpr std::size_t maximum_consumers{32};
Object* object{}; /* Def 机制所属最终对象;bind_private_crtp 后有效。 */
Sliding_Statistics compose_ms{600}; /* 图集采样与变更槽复制统计计算器。 */
Sliding_Statistics sample_delay_ms{600}; /* 固定 deadline 到实际采样的调度延迟。 */
Sliding_Statistics encode_ms{600}; /* H.264 编码统计计算器。 */
Sliding_Statistics publish_ms{600}; /* H.264 WebSocket 发布统计计算器。 */
std::vector<Source> sources{}; /* 已按业务标识排序的稳定图集来源。 */
std::unique_ptr<frame_sampling::Frame_Sampler> sampler{}; /* latest 图像与 30 FPS deadline 的唯一状态源。 */
std::shared_ptr<Task_Graph> media_graph{}; /* 独立媒体时钟运行的持久 DAG;异步完成回调延长其生命周期。 */
Frame_Scheduler::Timer sample_timer{}; /* 与任意单张 Plot 完成速率解耦的 30 FPS 控制时钟。 */
std::atomic_bool media_busy{}; /* 单个媒体 DAG 的 latest-only 准入。 */
frame_sampling::ffmpeg::FFmpeg_Frame_Transport ffmpeg_transport; /* FFmpeg 子模型的唯一编码上下文。 */
std::optional<frame_sampling::Sampled_Gallery_Frame> active_sample{}; /* 当前 deadline 取得的原生图集视图。 */
std::shared_ptr<const Encoded_Video_Frame> active_video{}; /* 当前编码结果不可变共享所有权。 */
std::atomic_uint64_t next_consumer_id{1}; /* 订阅身份生成器。 */
std::array<std::atomic<std::shared_ptr<const Consumer>>,
maximum_consumers> consumers{}; /* 每个槽位是订阅关系的唯一权威入口;热路径原子查询,不复制集合。 */
std::atomic_bool stopping{}; /* 关闭开始后拒绝新工作。 */
std::atomic_uint64_t skipped_sample_ticks{}; /* 未到 deadline 或无可写消费者的采样触发数。 */
std::atomic_uint64_t sampling_clock_ticks{}; /* 有消费者期间触发的独立媒体时钟 tick。 */
std::atomic_bool failed{}; /* 首次 Unknown Failure 后停止热路径。 */
std::optional<std::string> terminal_failure{}; /* 首次终止失败文本。 */
std::uint64_t encoded_frame_count{}; /* 累计编码成功帧数。 */
std::chrono::steady_clock::time_point metric_started{}; /* 当前指标窗口起点。 */
std::uint64_t metric_encoded_start{}; /* 窗口起点累计编码帧数。 */
std::uint64_t metric_sample_start{}; /* 窗口起点累计采样帧数。 */
std::uint64_t metric_clock_start{}; /* 窗口起点累计时钟数。 */
std::vector<std::uint64_t> metric_completion_starts{}; /* 各 Plot 窗口起点逻辑完成数。 */
std::vector<std::uint64_t> metric_rendered_starts{}; /* 各 Plot 窗口起点真实画面数。 */
static constexpr std::size_t maximum_taskflow_trace_frames{120};
std::atomic_size_t taskflow_trace_remaining{}; /* 尚待捕获的真实媒体 DAG 次数。 */
std::atomic_uint64_t taskflow_trace_control{}; /* 高 32 位 requested,低 32 位 captured。 */
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames> taskflow_trace_slots{};
Private();
~Private();
template <Attached Attached_Object>
void bind_private_crtp(Attached_Object* attached) {
Prev_Private::bind_private_crtp(attached);
object = not_null{static_cast<Object*>(attached)};
}
void initialize(std::vector<Plot_Entry> plots);
[[nodiscard]] bool consumer_accepts() const noexcept;
[[nodiscard]] bool remove_consumer(Stream_Id stream) noexcept;
void publish(std::shared_ptr<const Encoded_Video_Frame> video,
std::string notification) noexcept;
void fail(std::exception_ptr failure) noexcept;
void accept_frame(std::size_t slot,
std::shared_ptr<const Plot_Stream_Frame> frame);
void update_metrics(const frame_sampling::Sampled_Gallery_Frame& sample,
std::size_t encoded_bytes,
std::optional<Video_Encoder_Backend> encoder_backend);
void sample_media_frame();
void encode_media_frame();
void publish_video_frame();
void complete_media_frame();
[[nodiscard]] bool mark_taskflow_trace();
void restore_taskflow_trace() noexcept;
void store_taskflow_trace(const Taskflow_Frame_Trace& trace);
[[nodiscard]] nlohmann::json taskflow_trace_response() const;
};
}
-43
View File
@@ -1,43 +0,0 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <string_view>
#include <vector>
namespace aethera::web {
enum struct Video_Pixel_Layout : std::uint8_t {
bgra,
rgba
};
enum struct Video_Encoder_Backend : std::uint8_t {
libx264
};
[[nodiscard]] std::string_view video_encoder_backend_name(
Video_Encoder_Backend backend) noexcept;
[[nodiscard]] std::string_view h264_profile_level_id() noexcept;
struct Encoded_Video_Frame {
std::vector<std::byte> annex_b{}; /* 带起始码的完整 H.264 access unit,可直接交给 RTP packetizer。 */
std::chrono::microseconds presentation_time{}; /* 从 Plot 帧时钟起点累计的媒体时间戳。 */
std::uint64_t sequence{}; /* 对应 Render_Frame 的单调序号。 */
Video_Encoder_Backend backend{Video_Encoder_Backend::libx264}; /* 实际产生本 access unit 的 FFmpeg 后端。 */
bool key_frame{}; /* 本 access unit 是否可独立解码。 */
};
struct H264_Encoder final {
public:
explicit H264_Encoder(double frame_rate);
~H264_Encoder();
H264_Encoder(const H264_Encoder&) = delete;
H264_Encoder& operator=(const H264_Encoder&) = delete;
void request_key_frame();
[[nodiscard]] std::optional<Encoded_Video_Frame> encode(
std::span<const std::byte> pixels, std::uint32_t width,
std::uint32_t height, Video_Pixel_Layout layout,
std::uint64_t sequence, std::chrono::microseconds presentation_time);
private:
struct Private;
std::unique_ptr<Private> d;
};
}
+13 -10
View File
@@ -1,6 +1,6 @@
#include "Web_Server.hpp"
#include "Gallery_Video_Stream.hpp"
#include "Gallery_WebSocket.hpp"
#include "media/Gallery_Video_Stream.hpp"
#include "media/Gallery_Video_WebSocket.hpp"
#include "Graph_WebSocket.hpp"
#include <mcp/core/runtime/Gallery_Plots.hpp>
#include <mcp/core/Control_Service.hpp>
@@ -20,7 +20,8 @@
namespace aethera::web {
namespace {
using Gallery_Stream_Map =
std::unordered_map<std::string, std::shared_ptr<Gallery_Video_Stream>>;
std::unordered_map<std::string,
std::shared_ptr<media::Gallery_Video_Stream>>;
drogon::HttpResponsePtr json_response(nlohmann::json value) {
auto response = drogon::HttpResponse::newHttpResponse();
@@ -108,8 +109,8 @@ nlohmann::json merge_gallery_media_trace(nlohmann::json plot_trace,
int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) {
auto control = mcp::Control_Service::create();
std::vector<Gallery_Video_Stream::Plot_Entry> gallery_2d;
std::vector<Gallery_Video_Stream::Plot_Entry> gallery_3d;
std::vector<media::Gallery_Video_Stream::Plot_Entry> gallery_2d;
std::vector<media::Gallery_Video_Stream::Plot_Entry> gallery_3d;
gallery_2d.reserve(8);
gallery_3d.reserve(16);
for (const auto& definition : gallery_plot_definitions()) {
@@ -131,7 +132,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
std::unordered_map<std::string, std::string>>();
const auto add_media_group = [&gallery_streams, &plot_media, &plot_media_group](
std::string id,
std::vector<Gallery_Video_Stream::Plot_Entry> entries) {
std::vector<media::Gallery_Video_Stream::Plot_Entry> entries) {
if (entries.empty()) return;
const auto path = "/ws/gallery?group=" + id;
for (const auto& entry : entries) {
@@ -139,18 +140,19 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
plot_media_group->emplace(entry.id, id);
}
gallery_streams->emplace(
std::move(id), Gallery_Video_Stream::create(std::move(entries)));
std::move(id),
media::Gallery_Video_Stream::create(std::move(entries)));
};
constexpr std::size_t plots_per_media_group{4};
const auto add_media_groups = [&add_media_group](
std::string_view prefix,
std::vector<Gallery_Video_Stream::Plot_Entry> entries) {
std::vector<media::Gallery_Video_Stream::Plot_Entry> entries) {
std::size_t group_index{};
for (std::size_t offset = 0; offset < entries.size();
offset += plots_per_media_group) {
const auto end = std::min(
entries.size(), offset + plots_per_media_group);
std::vector<Gallery_Video_Stream::Plot_Entry> group(
std::vector<media::Gallery_Video_Stream::Plot_Entry> group(
std::make_move_iterator(entries.begin() +
static_cast<std::ptrdiff_t>(offset)),
std::make_move_iterator(entries.begin() +
@@ -165,7 +167,8 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
return control->find_plot(id);
};
auto websocket = std::make_shared<Graph_WebSocket_Controller>(resolve_plot);
auto gallery_websocket = std::make_shared<Gallery_WebSocket_Controller>(
auto gallery_websocket =
std::make_shared<media::Gallery_Video_WebSocket_Controller>(
[gallery_streams](std::string_view id) {
const auto found = gallery_streams->find(std::string{id});
return found == gallery_streams->end() ? nullptr : found->second;
@@ -1,144 +0,0 @@
#include "Frame_Sampler.hpp"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <limits>
#include <stdexcept>
namespace aethera::web::frame_sampling {
namespace {
using Clock = std::chrono::steady_clock;
std::int64_t clock_nanoseconds(Clock::time_point value) noexcept {
return std::chrono::duration_cast<std::chrono::nanoseconds>(
value.time_since_epoch()).count();
}
}
struct Frame_Sampler::Private {
detail::Gallery_Frame_Atlas atlas; /* 各 Plot 最近原生帧与持久图集的唯一状态源。 */
std::chrono::nanoseconds period{}; /* 固定采样周期;构造后不变。 */
double frame_rate_fps{}; /* 对外诊断使用的精确目标频率。 */
std::size_t sampling_source_slot{}; /* 为样本提供页面媒体时间的固定业务来源。 */
std::atomic_int64_t next_deadline_ns{}; /* 下一次允许生成样本的单调 deadline。 */
std::atomic_uint64_t accepted_source_frames{}; /* 成功发布到 latest 的源帧计数。 */
std::atomic_uint64_t rejected_source_frames{}; /* 无效或过期源帧计数。 */
std::atomic_uint64_t sampled_frames{}; /* 实际采样计数。 */
std::atomic_uint64_t early_ticks{}; /* deadline 前被合并的触发计数。 */
std::atomic_uint64_t missed_periods{}; /* 因迟到直接跳过的完整周期数。 */
Private(double target_frame_rate_fps,
std::uint32_t tile_width,
std::uint32_t tile_height,
std::uint32_t columns,
std::vector<std::string> source_ids,
std::size_t source_slot)
: atlas(tile_width, tile_height, columns, std::move(source_ids)),
period(static_cast<std::int64_t>(std::llround(
1'000'000'000.0 / target_frame_rate_fps))),
frame_rate_fps(target_frame_rate_fps),
sampling_source_slot(source_slot) {}
};
Frame_Sampler::Frame_Sampler(
double frame_rate_fps, std::uint32_t tile_width,
std::uint32_t tile_height, std::uint32_t columns,
std::vector<std::string> source_ids,
std::size_t sampling_source_slot) {
if (!std::isfinite(frame_rate_fps) || frame_rate_fps <= 0.0)
throw std::invalid_argument(
"frame sampler rate must be finite and positive");
if (sampling_source_slot >= source_ids.size())
throw std::out_of_range("frame sampler source slot is invalid");
d = std::make_unique<Private>(
frame_rate_fps, tile_width, tile_height, columns,
std::move(source_ids), sampling_source_slot);
if (d->period <= std::chrono::nanoseconds::zero())
throw std::invalid_argument("frame sampler period is too small");
}
Frame_Sampler::~Frame_Sampler() = default;
Frame_Sampler::Accept_Frame_Result Frame_Sampler::accept_frame(
std::size_t slot, std::shared_ptr<const Plot_Pixel_Frame> frame) {
const auto result = d->atlas.accept_frame(slot, std::move(frame));
if (result == detail::Gallery_Frame_Atlas::Accept_Frame_Result::accepted) {
d->accepted_source_frames.fetch_add(1, std::memory_order_release);
return Accept_Frame_Result::accepted;
}
d->rejected_source_frames.fetch_add(1, std::memory_order_relaxed);
return result == detail::Gallery_Frame_Atlas::Accept_Frame_Result::stale_frame
? Accept_Frame_Result::stale_frame
: Accept_Frame_Result::invalid_frame;
}
std::optional<Sampled_Gallery_Frame> Frame_Sampler::sample() {
const auto now = Clock::now();
const auto now_ns = clock_nanoseconds(now);
const auto compose_sample = [this, now](double deadline_delay_ms,
std::uint64_t sample_sequence) {
auto composition = d->atlas.compose();
const auto& source = composition.sources[d->sampling_source_slot];
Plot_Render_Tick tick{
now,
source.completion_correlation_id,
static_cast<double>(source.completion_presentation_time.count()) /
1'000.0,
composition.width,
composition.height};
return Sampled_Gallery_Frame{
std::move(composition), tick, sample_sequence,
deadline_delay_ms};
};
/* 首帧以前没有可编码内容;首帧以后每个 deadline 都编码当前图集。
* 这不是 changed-only:静态画面仍持续产生 H.264 帧并维持解码参考链。 */
if (d->accepted_source_frames.load(std::memory_order_acquire) == 0)
return std::nullopt;
auto deadline_ns = d->next_deadline_ns.load(std::memory_order_acquire);
for (;;) {
if (deadline_ns == 0) {
const auto next = now_ns + d->period.count();
if (!d->next_deadline_ns.compare_exchange_weak(
deadline_ns, next, std::memory_order_acq_rel,
std::memory_order_acquire))
continue;
const auto sequence = d->sampled_frames.fetch_add(
1, std::memory_order_relaxed) + 1U;
return compose_sample(0.0, sequence);
}
if (now_ns < deadline_ns) {
d->early_ticks.fetch_add(1, std::memory_order_relaxed);
return std::nullopt;
}
const auto late_ns = now_ns - deadline_ns;
const auto missed = static_cast<std::uint64_t>(
late_ns / d->period.count());
const auto next = deadline_ns +
static_cast<std::int64_t>(missed + 1U) * d->period.count();
if (!d->next_deadline_ns.compare_exchange_weak(
deadline_ns, next, std::memory_order_acq_rel,
std::memory_order_acquire))
continue;
const auto sequence = d->sampled_frames.fetch_add(
1, std::memory_order_relaxed) + 1U;
d->missed_periods.fetch_add(missed, std::memory_order_relaxed);
return compose_sample(static_cast<double>(late_ns) / 1'000'000.0,
sequence);
}
}
detail::Gallery_Atlas_Description Frame_Sampler::describe() const {
return d->atlas.describe();
}
Frame_Sampler_State Frame_Sampler::state() const noexcept {
return {
d->frame_rate_fps,
d->accepted_source_frames.load(std::memory_order_relaxed),
d->rejected_source_frames.load(std::memory_order_relaxed),
d->sampled_frames.load(std::memory_order_relaxed),
d->early_ticks.load(std::memory_order_relaxed),
d->missed_periods.load(std::memory_order_relaxed)};
}
}
@@ -1,63 +0,0 @@
#pragma once
#include <mcp/core/runtime/Plot.hpp>
#include "../detail/Gallery_Frame_Atlas.hpp"
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace aethera::web::frame_sampling {
struct Sampled_Gallery_Frame {
detail::Gallery_Atlas_Composition composition{}; /* 本次采样得到的原生像素图集;下一次成功采样前有效。 */
Plot_Render_Tick tick{}; /* 触发采样的服务端帧时钟。 */
std::uint64_t sample_sequence{}; /* 媒体流自身严格递增的帧序号。 */
double deadline_delay_ms{}; /* 实际采样相对固定周期 deadline 的延迟。 */
};
struct Frame_Sampler_State {
double target_frame_rate_fps{}; /* 采样器固定目标频率。 */
std::uint64_t accepted_source_frames{}; /* 已进入权威 latest 的 Plot 完成帧数。 */
std::uint64_t rejected_source_frames{}; /* 因尺寸、布局或顺序不合法而拒绝的完成帧数。 */
std::uint64_t sampled_frames{}; /* 到达 deadline 并实际生成的图集样本数。 */
std::uint64_t early_ticks{}; /* deadline 前到达、未触发采样的外接图次数。 */
std::uint64_t missed_periods{}; /* 调度延迟跨过的完整采样周期数。 */
};
/*
* Gallery 像素采样的唯一权威入口。Plot 可以按任意帧策略发布,采样器只在
* 固定 deadline 到达时读取每个槽位的 latest,不排队、不补历史帧。
*/
struct Frame_Sampler final {
public:
enum struct Accept_Frame_Result : std::uint8_t {
accepted,
invalid_frame,
stale_frame
};
Frame_Sampler(double frame_rate_fps,
std::uint32_t tile_width,
std::uint32_t tile_height,
std::uint32_t columns,
std::vector<std::string> source_ids,
std::size_t sampling_source_slot);
~Frame_Sampler();
Frame_Sampler(const Frame_Sampler&) = delete;
Frame_Sampler& operator=(const Frame_Sampler&) = delete;
[[nodiscard]] Accept_Frame_Result accept_frame(
std::size_t slot, std::shared_ptr<const Plot_Pixel_Frame> frame);
[[nodiscard]] std::optional<Sampled_Gallery_Frame> sample();
[[nodiscard]] detail::Gallery_Atlas_Description describe() const;
[[nodiscard]] Frame_Sampler_State state() const noexcept;
private:
struct Private;
std::unique_ptr<Private> d;
};
}
@@ -1,26 +0,0 @@
#pragma once
#include "../../H264_Encoder.hpp"
#include "../Frame_Sampler.hpp"
#include <memory>
#include <optional>
namespace aethera::web::frame_sampling::ffmpeg {
/* 固定采样帧到 FFmpeg H.264 access unit 的单一编码位置。 */
struct FFmpeg_Frame_Transport final {
public:
explicit FFmpeg_Frame_Transport(double frame_rate_fps);
~FFmpeg_Frame_Transport();
FFmpeg_Frame_Transport(const FFmpeg_Frame_Transport&) = delete;
FFmpeg_Frame_Transport& operator=(const FFmpeg_Frame_Transport&) = delete;
void request_key_frame() noexcept;
[[nodiscard]] std::optional<Encoded_Video_Frame> encode(
const Sampled_Gallery_Frame& frame);
private:
struct Private;
std::unique_ptr<Private> d;
};
}
@@ -0,0 +1,32 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <vector>
namespace aethera::web::media {
enum struct Video_Pixel_Layout : std::uint8_t {
bgra,
rgba
};
enum struct Video_Encoder_Backend : std::uint8_t {
libx264
};
[[nodiscard]] std::string_view video_encoder_backend_name(
Video_Encoder_Backend backend) noexcept;
[[nodiscard]] std::string_view h264_profile_level_id() noexcept;
struct Encoded_Video_Frame {
std::vector<std::byte> annex_b{}; /* 带起始码的完整 H.264 access unit,可直接交给 WebCodecs。 */
std::chrono::microseconds presentation_time{}; /* 从媒体流起点累计的显示时间戳。 */
std::chrono::nanoseconds source_time_unix{}; /* 对应 Plot 像素进入媒体流水线的 Unix 时间。 */
std::uint64_t sequence{}; /* 图集媒体流的严格单调帧序号。 */
Video_Encoder_Backend backend{Video_Encoder_Backend::libx264}; /* 实际产生本 access unit 的编码后端。 */
bool key_frame{}; /* 本 access unit 是否可独立解码。 */
};
}
@@ -2,7 +2,6 @@
#include <mcp/core/runtime/Taskflow_Trace_Json.hpp>
#include <frame_statistics.hpp>
#include <render_common.hpp>
#include "frame_sampling/Frame_Sampler.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <atomic>
@@ -12,13 +11,13 @@
#include <stdexcept>
#include <string>
#include <utility>
namespace aethera::web {
namespace aethera::web::media {
namespace {
// Scene 的业务视口是 720x420;每组四图形成一行 2880x420 H.264 图集。
constexpr std::uint32_t tile_width{720};
constexpr std::uint32_t tile_height{420};
constexpr std::uint32_t atlas_columns{4};
constexpr double gallery_frame_rate{30.0};
constexpr double encoder_nominal_frame_rate{30.0};
constexpr auto metric_interval{std::chrono::seconds(1)};
nlohmann::json statistic_json(const Statistic_State& value) {
return {
@@ -44,7 +43,7 @@ std::string exception_description(const std::exception_ptr& failure) {
}
}
Gallery_Video_Stream::Private::Private()
: ffmpeg_transport(gallery_frame_rate) {}
: ffmpeg_transport(encoder_nominal_frame_rate) {}
Gallery_Video_Stream::Private::~Private() = default;
void Gallery_Video_Stream::Private::initialize(
std::vector<Plot_Entry> plots) {
@@ -57,9 +56,8 @@ void Gallery_Video_Stream::Private::initialize(
source_ids.push_back(plot.id);
sources.push_back(Source{std::move(plot)});
}
sampler = std::make_unique<frame_sampling::Frame_Sampler>(
gallery_frame_rate, tile_width, tile_height, atlas_columns,
std::move(source_ids), sources.size() - 1U);
atlas = std::make_unique<detail::Gallery_Frame_Atlas>(
tile_width, tile_height, atlas_columns, std::move(source_ids));
metric_completion_starts.resize(sources.size());
metric_rendered_starts.resize(sources.size());
}
@@ -167,7 +165,7 @@ void Gallery_Video_Stream::Private::fail(
const auto notification = nlohmann::json{
{"kind", "gallery_error"},
{"protocol", "aethera.gallery.video"},
{"version", 4},
{"version", 5},
{"message", *terminal_failure}
}.dump();
publish({}, notification);
@@ -177,37 +175,48 @@ void Gallery_Video_Stream::Private::fail(
void Gallery_Video_Stream::Private::accept_frame(
std::size_t slot, std::shared_ptr<const Plot_Stream_Frame> frame) {
if (stopping.load(std::memory_order_acquire) ||
failed.load(std::memory_order_acquire) || !frame)
failed.load(std::memory_order_acquire) || !frame || !frame->pixels ||
!frame->pixels->pixels || !consumer_accepts())
return;
static_cast<void>(sampler->accept_frame(slot, frame->pixels));
Pending_Plot_Frame pending{
slot, std::move(frame->pixels), std::chrono::steady_clock::now(),
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch())};
if (!pending_frames.enqueue(std::move(pending))) throw std::bad_alloc{};
received_frame_count.fetch_add(1, std::memory_order_relaxed);
media_work_generation.fetch_add(1, std::memory_order_release);
arm_media_task(object->weak_from_this());
}
void Gallery_Video_Stream::Private::update_metrics(
const frame_sampling::Sampled_Gallery_Frame& sample,
const Active_Media_Frame& frame,
std::size_t encoded_bytes,
std::optional<Video_Encoder_Backend> encoder_backend) {
const auto& composition = sample.composition;
const auto sampler_state = sampler->state();
const auto& composition = frame.composition;
const auto now = std::chrono::steady_clock::now();
const auto received_total = received_frame_count.load(
std::memory_order_relaxed);
const auto processed_total = processed_frame_count.load(
std::memory_order_relaxed);
const auto encoded_total = encoded_frame_count.load(
std::memory_order_relaxed);
if (metric_started.time_since_epoch().count() == 0) {
metric_started = now;
metric_encoded_start = encoded_frame_count;
metric_sample_start = sampler_state.sampled_frames;
metric_clock_start = sampling_clock_ticks.load(std::memory_order_relaxed);
metric_received_start = received_total;
metric_processed_start = processed_total;
metric_encoded_start = encoded_total;
for (std::size_t slot = 0; slot < composition.sources.size(); ++slot) {
metric_completion_starts[slot] =
composition.sources[slot].completion_count;
composition.sources[slot].completion_count;
metric_rendered_starts[slot] =
composition.sources[slot].rendered_frame_count;
composition.sources[slot].rendered_frame_count;
}
return;
}
const auto elapsed = now - metric_started;
if (elapsed < metric_interval) return;
const double seconds = std::chrono::duration<double>(elapsed).count();
const auto clock_total =
sampling_clock_ticks.load(std::memory_order_relaxed);
nlohmann::json source_metrics = nlohmann::json::object();
const auto description = sampler->describe();
const auto description = atlas->describe();
for (std::size_t slot = 0; slot < composition.sources.size(); ++slot) {
const auto& progress = composition.sources[slot];
const auto completed = progress.completion_count -
@@ -244,38 +253,39 @@ void Gallery_Video_Stream::Private::update_metrics(
metric_rendered_starts[slot] = progress.rendered_frame_count;
}
const auto& compose = compose_ms.state();
const auto& sample_delay = sample_delay_ms.state();
const auto& queue_delay = queue_delay_ms.state();
const auto& encode = encode_ms.state();
const auto& publish_time = publish_ms.state();
auto output = nlohmann::json{
{"kind", "gallery_metrics"},
{"protocol", "aethera.gallery.video"},
{"version", 4},
{"clock_sequence", sample.sample_sequence},
{"sampled_frame_count", sampler_state.sampled_frames},
{"encoded_frame_count", encoded_frame_count},
{"version", 5},
{"media_sequence", frame.sequence},
{"received_frame_count", received_total},
{"processed_frame_count", processed_total},
{"encoded_frame_count", encoded_total},
{"encoder_backend", encoder_backend
? video_encoder_backend_name(*encoder_backend)
: std::string_view{"inactive"}},
{"profile_level_id", h264_profile_level_id()},
{"target_frame_rate_fps", gallery_frame_rate},
{
"clock_delivery_rate_fps",
static_cast<double>(clock_total - metric_clock_start) / seconds
"received_frame_rate_fps",
static_cast<double>(received_total - metric_received_start) /
seconds
},
{
"sampled_frame_rate_fps",
static_cast<double>(sampler_state.sampled_frames -
metric_sample_start) / seconds
"processed_frame_rate_fps",
static_cast<double>(processed_total -
metric_processed_start) / seconds
},
{
"encoded_frame_rate_fps",
static_cast<double>(encoded_frame_count - metric_encoded_start) / seconds
static_cast<double>(encoded_total - metric_encoded_start) / seconds
},
{"compose_average_ms", compose.average},
{"compose_p95_ms", compose.p95},
{"sample_delay_average_ms", sample_delay.average},
{"sample_delay_p95_ms", sample_delay.p95},
{"queue_delay_average_ms", queue_delay.average},
{"queue_delay_p95_ms", queue_delay.p95},
{"encode_average_ms", encode.average},
{"encode_p95_ms", encode.p95},
{"publish_average_ms", publish_time.average},
@@ -283,57 +293,58 @@ void Gallery_Video_Stream::Private::update_metrics(
{
"statistics", {
{"compose_ms", statistic_json(compose)},
{"sample_delay_ms", statistic_json(sample_delay)},
{"queue_delay_ms", statistic_json(queue_delay)},
{"encode_ms", statistic_json(encode)},
{"publish_ms", statistic_json(publish_time)}
}
},
{"pending_frame_count", pending_frames.size_approx()},
{"encoded_bytes", encoded_bytes},
{"fresh_tiles", composition.fresh_tile_count},
{"missing_tiles", composition.missing_tile_count},
{"rejected_frames", composition.rejected_frame_count},
{
"skipped_sample_ticks",
skipped_sample_ticks.load(std::memory_order_relaxed)
},
{"sampler", {
{"accepted_source_frames", sampler_state.accepted_source_frames},
{"rejected_source_frames", sampler_state.rejected_source_frames},
{"early_ticks", sampler_state.early_ticks},
{"missed_periods", sampler_state.missed_periods}}},
{"sources", std::move(source_metrics)}
};
metric_started = now;
metric_encoded_start = encoded_frame_count;
metric_sample_start = sampler_state.sampled_frames;
metric_clock_start = clock_total;
metric_received_start = received_total;
metric_processed_start = processed_total;
metric_encoded_start = encoded_total;
object->template update_state<&State::diagnostics>(std::move(output));
object->template publish_state<Base_Tag>();
}
void Gallery_Video_Stream::Private::sample_media_frame() {
active_sample.reset();
void Gallery_Video_Stream::Private::begin_media_frame() {
active_frame.reset();
active_video.reset();
if (stopping.load(std::memory_order_acquire) ||
failed.load(std::memory_order_acquire) ||
!consumer_accepts()) {
skipped_sample_ticks.fetch_add(1, std::memory_order_relaxed);
return;
}
failed.load(std::memory_order_acquire)) return;
Pending_Plot_Frame pending;
if (!pending_frames.try_dequeue(pending)) return;
++processed_frame_count;
const auto started = std::chrono::steady_clock::now();
active_sample = sampler->sample();
if (!active_sample) {
skipped_sample_ticks.fetch_add(1, std::memory_order_relaxed);
return;
}
const auto accepted = atlas->accept_frame(
pending.slot, std::move(pending.frame));
if (accepted ==
detail::Gallery_Frame_Atlas::Accept_Frame_Result::invalid_frame)
throw std::logic_error(
"Plot published an invalid gallery pixel frame");
auto presentation_time = std::chrono::duration_cast<
std::chrono::microseconds>(started - media_origin);
if (presentation_time <= last_presentation_time)
presentation_time = last_presentation_time +
std::chrono::microseconds{1};
last_presentation_time = presentation_time;
active_frame = Active_Media_Frame{
atlas->compose(), next_media_sequence++, presentation_time,
pending.source_time_unix,
std::chrono::duration<double, std::milli>(
started - pending.enqueued_at).count()};
static_cast<void>(compose_ms.submit(
std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - started).count()));
static_cast<void>(sample_delay_ms.submit(
active_sample->deadline_delay_ms));
static_cast<void>(queue_delay_ms.submit(active_frame->queue_delay_ms));
}
void Gallery_Video_Stream::Private::encode_media_frame() {
if (!active_sample || !consumer_accepts())
return;
if (!active_frame) return;
/*
* FFmpeg.H264.encode Taskflow FFmpeg
* RGB->YUV420P libx264 send/receive
@@ -342,7 +353,14 @@ void Gallery_Video_Stream::Private::encode_media_frame() {
* H264_Encoder mutex 线
*/
const auto started = std::chrono::steady_clock::now();
if (auto encoded = ffmpeg_transport.encode(*active_sample))
if (auto encoded = ffmpeg_transport.encode(
active_frame->composition.pixels,
active_frame->composition.width,
active_frame->composition.height,
active_frame->composition.layout,
active_frame->sequence,
active_frame->presentation_time,
active_frame->source_time_unix))
active_video = std::make_shared<const Encoded_Video_Frame>(
std::move(*encoded));
static_cast<void>(encode_ms.submit(
@@ -359,13 +377,13 @@ void Gallery_Video_Stream::Private::publish_video_frame() {
std::chrono::steady_clock::now() - started).count()));
}
void Gallery_Video_Stream::Private::complete_media_frame() {
if (!active_sample) return;
if (!active_frame) return;
const auto encoded_bytes = active_video ? active_video->annex_b.size() : 0U;
const auto encoder_backend = active_video
? std::optional{active_video->backend}
: std::nullopt;
update_metrics(*active_sample, encoded_bytes, encoder_backend);
active_sample.reset();
update_metrics(*active_frame, encoded_bytes, encoder_backend);
active_frame.reset();
active_video.reset();
}
std::shared_ptr<Gallery_Video_Stream> Gallery_Video_Stream::create(
@@ -383,28 +401,86 @@ Gallery_Video_Stream::Gallery_Video_Stream() = default;
Gallery_Video_Stream::~Gallery_Video_Stream() {
shutdown();
}
void Gallery_Video_Stream::Private::arm_media_task(
std::weak_ptr<Gallery_Video_Stream> lifetime) {
if (stopping.load(std::memory_order_acquire) ||
failed.load(std::memory_order_acquire))
return;
bool idle{};
if (!media_task_scheduled.compare_exchange_strong(
idle, true, std::memory_order_acq_rel,
std::memory_order_acquire))
return;
const auto graph = media_graph;
bool trace_reserved = mark_taskflow_trace();
auto trace_frame = trace_reserved
? std::make_shared<Render_Frame>(Frame_Identity{
received_frame_count.load(std::memory_order_acquire), 0})
: std::shared_ptr<Render_Frame>{};
bool trace_started{};
if (trace_frame) {
trace_frame->request_taskflow_trace();
trace_started = aethera::detail::begin_taskflow_trace(*trace_frame);
if (!trace_started) {
restore_taskflow_trace();
trace_reserved = false;
trace_frame.reset();
}
}
try {
auto completion = [lifetime, graph, trace_frame, trace_started] {
static_cast<void>(graph);
if (trace_started)
aethera::detail::finish_taskflow_trace(*trace_frame);
const auto completed = lifetime.lock();
if (!completed) return;
auto& data = static_cast<Private&>(*completed->d);
if (trace_started)
data.store_taskflow_trace(
trace_frame->take_taskflow_trace());
data.media_task_scheduled.store(false,
std::memory_order_release);
if (data.received_frame_count.load(std::memory_order_acquire) >
data.processed_frame_count.load(std::memory_order_acquire))
data.arm_media_task(lifetime);
};
if (trace_frame)
aethera::detail::run_taskflow(
*graph, *trace_frame, "gallery.media",
std::move(completion));
else
aethera::detail::run_taskflow(*graph, std::move(completion));
}
catch (...) {
if (trace_started)
aethera::detail::finish_taskflow_trace(*trace_frame);
if (trace_reserved) restore_taskflow_trace();
media_task_scheduled.store(false, std::memory_order_release);
fail(std::current_exception());
}
}
void Gallery_Video_Stream::bind_plots() {
auto& data = static_cast<Private&>(*d);
const auto weak = weak_from_this();
if (data.sources.empty())
throw std::invalid_argument("gallery video stream requires a Plot");
/*
* Plot
* latest 30 FPS DAG
* tick Worker
*/
/* 每个 Plot 独立帧策略发布的物理像素帧按到达顺序进入本 DAG。
* Gallery DAG
* FFmpeg */
auto media = std::make_shared<Task_Graph>("gallery.video.frame");
auto sample = media->add("gallery.sample.capture", [weak] {
auto compose = media->add("gallery.atlas.compose", [weak] {
if (const auto owner = weak.lock()) {
auto& owner_data = static_cast<Private&>(*owner->d);
try { owner_data.sample_media_frame(); }
try { owner_data.begin_media_frame(); }
catch (...) { owner_data.fail(std::current_exception()); }
}
});
sample.describe("owner", "gallery")
.describe("stage", "30 FPS latest-frame sampling deadline")
.describe("frame_rate_fps", "30");
compose.describe("owner", "gallery")
.describe("stage", "apply one policy-approved Plot frame")
.describe("admission", "owned by source Plot frame policy");
auto encode = media->add("FFmpeg.H264.encode", [weak] {
if (const auto owner = weak.lock()) {
auto& owner_data = static_cast<Private&>(*owner->d);
@@ -413,7 +489,7 @@ void Gallery_Video_Stream::bind_plots() {
}
});
encode.describe("owner", "gallery")
.describe("stage", "FFmpeg H.264 encode after Plot pixel publish")
.describe("stage", "encode every composed Plot completion")
.describe("backend", "FFmpeg")
.describe("codec", "H.264")
.describe("execution", "Taskflow worker");
@@ -427,7 +503,7 @@ void Gallery_Video_Stream::bind_plots() {
publish_video.describe("owner", "gallery")
.describe("transport", "Drogon WebSocket")
.describe("stage", "H.264 access unit enqueue");
auto complete = media->add("gallery.sample.complete", [weak] {
auto complete = media->add("gallery.media.complete", [weak] {
if (const auto owner = weak.lock()) {
auto& owner_data = static_cast<Private&>(*owner->d);
try { owner_data.complete_media_frame(); }
@@ -435,80 +511,11 @@ void Gallery_Video_Stream::bind_plots() {
}
});
complete.describe("owner", "gallery")
.describe("stage", "sample metrics and ownership release");
sample.precede(encode);
.describe("stage", "metrics and frame ownership release");
compose.precede(encode);
encode.precede(publish_video);
publish_video.precede(complete);
data.media_graph = media;
data.sample_timer = Frame_Scheduler::instance().make_timer(
[weak](Frame_Scheduler::Tick) {
const auto owner = weak.lock();
if (!owner) return;
auto& owner_data = static_cast<Private&>(*owner->d);
if (owner_data.stopping.load(std::memory_order_acquire) ||
owner_data.failed.load(std::memory_order_acquire) ||
!owner_data.consumer_accepts())
return;
owner_data.sampling_clock_ticks.fetch_add(
1, std::memory_order_relaxed);
bool idle{};
if (!owner_data.media_busy.compare_exchange_strong(
idle, true, std::memory_order_acq_rel,
std::memory_order_acquire)) {
owner_data.skipped_sample_ticks.fetch_add(
1, std::memory_order_relaxed);
return;
}
const auto graph = owner_data.media_graph;
bool trace_reserved = owner_data.mark_taskflow_trace();
auto trace_frame = trace_reserved
? std::make_shared<Render_Frame>(Frame_Identity{
owner_data.sampling_clock_ticks.load(
std::memory_order_acquire), 0})
: std::shared_ptr<Render_Frame>{};
bool trace_started{};
if (trace_frame) {
trace_frame->request_taskflow_trace();
trace_started = aethera::detail::begin_taskflow_trace(
*trace_frame);
if (!trace_started) {
owner_data.restore_taskflow_trace();
trace_reserved = false;
trace_frame.reset();
}
}
try {
auto completion = [weak, graph, trace_frame, trace_started] {
static_cast<void>(graph);
if (trace_started)
aethera::detail::finish_taskflow_trace(*trace_frame);
if (const auto completed = weak.lock()) {
auto& completed_data =
static_cast<Private&>(*completed->d);
if (trace_started)
completed_data.store_taskflow_trace(
trace_frame->take_taskflow_trace());
completed_data.media_busy.store(
false, std::memory_order_release);
}
};
if (trace_frame)
aethera::detail::run_taskflow(
*graph, *trace_frame, "gallery.media",
std::move(completion));
else
aethera::detail::run_taskflow(
*graph, std::move(completion));
}
catch (...) {
if (trace_started)
aethera::detail::finish_taskflow_trace(*trace_frame);
if (trace_reserved) owner_data.restore_taskflow_trace();
owner_data.media_busy.store(false,
std::memory_order_release);
owner_data.fail(std::current_exception());
}
});
for (std::size_t slot = 0; slot < data.sources.size(); ++slot) {
auto& source = data.sources[slot];
@@ -518,8 +525,8 @@ void Gallery_Video_Stream::bind_plots() {
if (!owner) return;
auto& owner_data = static_cast<Private&>(*owner->d);
if (owner_data.stopping.load(std::memory_order_acquire)) return;
/* 每个 Plot 只更新自己槽位的权威最新完成帧。 */
owner_data.accept_frame(slot, std::move(frame));
try { owner_data.accept_frame(slot, std::move(frame)); }
catch (...) { owner_data.fail(std::current_exception()); }
});
source.entry.plot->configure_stream(
source.stream, tile_width, tile_height);
@@ -558,7 +565,6 @@ Gallery_Video_Stream::Stream_Id Gallery_Video_Stream::subscribe(
std::memory_order_acquire)) {
if (data.stopping.load(std::memory_order_acquire)) {
static_cast<void>(data.remove_consumer(id));
data.sample_timer.cancel();
throw std::logic_error(
"gallery video stream is shutting down");
}
@@ -579,10 +585,8 @@ Gallery_Video_Stream::Stream_Id Gallery_Video_Stream::subscribe(
if (!inserted)
throw std::runtime_error(
"gallery video stream consumer capacity is exhausted");
data.sample_timer.start_periodic(gallery_frame_rate);
if (data.stopping.load(std::memory_order_acquire)) {
static_cast<void>(data.remove_consumer(id));
data.sample_timer.cancel();
throw std::logic_error("gallery video stream is shutting down");
}
return id;
@@ -600,7 +604,6 @@ void Gallery_Video_Stream::request_video_key_frame() {
void Gallery_Video_Stream::shutdown() noexcept {
auto& data = static_cast<Private&>(*d);
if (data.stopping.exchange(true, std::memory_order_acq_rel)) return;
data.sample_timer.cancel();
for (auto& source : data.sources) {
if (source.stream == 0) continue;
try {
@@ -614,15 +617,14 @@ void Gallery_Video_Stream::shutdown() noexcept {
}
std::string Gallery_Video_Stream::layout_description() const {
const auto& data = static_cast<const Private&>(*d);
const auto description = data.sampler->describe();
const auto description = data.atlas->describe();
nlohmann::json plots = nlohmann::json::object();
for (const auto& source : description.sources) plots[source.id] = {{"column", source.column}, {"row", source.row}};
return nlohmann::json{
{"kind", "gallery_layout"},
{"protocol", "aethera.gallery.video"},
{"version", 4},
{"version", 5},
{"transport", "drogon_h264"},
{"frame_rate_fps", gallery_frame_rate},
{"columns", description.columns},
{"rows", description.rows},
{"tile_width", description.tile_width},
@@ -636,8 +638,8 @@ std::string Gallery_Video_Stream::layout_description() const {
{"profile_level_id", h264_profile_level_id()},
{"transport", "Drogon WebSocket binary"},
{"bitstream", "Annex B"},
{"protocol_version", 1},
{"header_bytes", 40}
{"protocol_version", 2},
{"header_bytes", 48}
}
},
{"plots", std::move(plots)}
@@ -647,13 +649,20 @@ nlohmann::json Gallery_Video_Stream::diagnostics() const {
nlohmann::json output{
{"kind", "gallery_metrics"},
{"protocol", "aethera.gallery.video"},
{"version", 4},
{"version", 5},
{"sources", nlohmann::json::object()}
};
this->template access_state<Base_Tag>([&output](const State& state) {
if (!state.diagnostics.empty()) output = state.diagnostics;
});
const auto& data = static_cast<const Private&>(*d);
output["received_frame_count"] = data.received_frame_count.load(
std::memory_order_relaxed);
output["processed_frame_count"] = data.processed_frame_count.load(
std::memory_order_relaxed);
output["encoded_frame_count"] = data.encoded_frame_count.load(
std::memory_order_relaxed);
output["pending_frame_count"] = data.pending_frames.size_approx();
std::size_t consumer_count{};
for (const auto& slot : data.consumers) {
const auto consumer = slot.load(std::memory_order_acquire);
@@ -1,6 +1,6 @@
#pragma once
#include <mcp/core/runtime/Plot.hpp>
#include "frame_sampling/ffmpeg/FFmpeg_Frame_Transport.hpp"
#include "Encoded_Video_Frame.hpp"
#include <render_common.hpp>
#include <cstdint>
#include <functional>
@@ -9,7 +9,7 @@
#include <string>
#include <vector>
namespace aethera::web {
namespace aethera::web::media {
struct Gallery_Stream_Frame {
std::shared_ptr<const Encoded_Video_Frame> video{}; /* FFmpeg 订阅者共享的 H.264 access unit。 */
std::string notification{}; /* 仅承载终止错误等低频控制通知。 */
@@ -0,0 +1,116 @@
#pragma once
#include "detail/FFmpeg_Frame_Transport.hpp"
#include "detail/Gallery_Frame_Atlas.hpp"
#include <concurrentqueue-1.0.5/concurrentqueue.h>
#include <frame_statistics.hpp>
#include <ownership.hpp>
#include <array>
#include <atomic>
#include <chrono>
#include <optional>
namespace aethera::web::media {
struct Gallery_Video_Stream::Private : Prev_Private {
using Object = Gallery_Video_Stream;
struct Source {
Plot_Entry entry; /* 图集槽位关联的实际 Plot。 */
Plot::Stream_Id stream{}; /* Plot 完成帧的唯一订阅标识。 */
};
struct Consumer {
Stream_Id id{}; /* 当前网页媒体连接的订阅身份。 */
std::string connection{}; /* 同一页面重连期间稳定的协议身份。 */
std::shared_ptr<const Stream_Handler> handler{}; /* 发布期间保持回调存活。 */
std::shared_ptr<const Transport_Readiness> readiness{}; /* 连接和解码器是否可接收媒体。 */
std::shared_ptr<const Transport_Diagnostics> diagnostics{}; /* 连接自身的传输诊断来源。 */
};
struct Pending_Plot_Frame {
std::size_t slot{}; /* 该完成帧所属的稳定图集槽位。 */
std::shared_ptr<const Plot_Pixel_Frame> frame{}; /* 已通过所属 Plot 帧策略的不可变像素帧。 */
std::chrono::steady_clock::time_point enqueued_at{}; /* 进入媒体串行队列的单调时刻。 */
std::chrono::nanoseconds source_time_unix{}; /* 进入媒体串行队列的 Unix 时间。 */
};
struct Active_Media_Frame {
detail::Gallery_Atlas_Composition composition{}; /* 应用一个 Plot 完成帧后的完整图集视图。 */
std::uint64_t sequence{}; /* 媒体流严格递增的 access unit 序号。 */
std::chrono::microseconds presentation_time{}; /* 媒体流起点以来的单调显示时间。 */
std::chrono::nanoseconds source_time_unix{}; /* 对应 Plot 像素进入媒体流水线的 Unix 时间。 */
double queue_delay_ms{}; /* Plot 发布到开始合成的队列时间。 */
};
static constexpr std::size_t maximum_consumers{32};
Object* object{}; /* Def 最终对象的可空非拥有借用;bind_private_crtp 后有效。 */
Sliding_Statistics compose_ms{600}; /* 单个 Plot 帧应用到持久图集的耗时。 */
Sliding_Statistics queue_delay_ms{600}; /* Plot 发布到媒体 Taskflow 开始处理的排队时间。 */
Sliding_Statistics encode_ms{600}; /* H.264 编码耗时。 */
Sliding_Statistics publish_ms{600}; /* H.264 WebSocket 发布调用耗时。 */
std::vector<Source> sources{}; /* 已按业务标识排序的稳定图集来源。 */
std::unique_ptr<detail::Gallery_Frame_Atlas> atlas{}; /* 图集像素和各槽位进度的唯一所有者。 */
moodycamel::ConcurrentQueue<Pending_Plot_Frame> pending_frames{}; /* 各 Plot 已完成像素帧的无损 MPMC 入口。 */
std::shared_ptr<Task_Graph> media_graph{}; /* 每次只消费一个 Plot 完成帧的持久异步 DAG。 */
std::atomic_bool media_task_scheduled{}; /* 唯一媒体 DAG 准入;不承担丢帧策略。 */
std::atomic_uint64_t media_work_generation{}; /* 新完成帧入队后推进,封闭任务退出竞争窗口。 */
detail::FFmpeg_Frame_Transport ffmpeg_transport; /* 仅由媒体 DAG 串行访问的编码上下文。 */
std::optional<Active_Media_Frame> active_frame{}; /* 当前媒体 DAG 独占的图集帧。 */
std::shared_ptr<const Encoded_Video_Frame> active_video{}; /* 当前编码结果的共享所有权。 */
std::atomic_uint64_t next_consumer_id{1}; /* 订阅身份生成器。 */
std::array<std::atomic<std::shared_ptr<const Consumer>>,
maximum_consumers> consumers{}; /* 订阅关系的权威原子槽位。 */
std::atomic_bool stopping{}; /* 关闭开始后拒绝新工作。 */
std::atomic_bool failed{}; /* 首次 Unknown Failure 后停止媒体热路径。 */
std::optional<std::string> terminal_failure{}; /* 首次终止失败文本,仅故障路径访问。 */
std::atomic_uint64_t received_frame_count{}; /* 活跃消费者期间收到的 Plot 像素帧总数。 */
std::atomic_uint64_t processed_frame_count{}; /* 媒体 DAG 已按顺序消费的 Plot 像素帧总数。 */
std::atomic_uint64_t encoded_frame_count{}; /* 成功产生 H.264 access unit 的累计帧数。 */
std::uint64_t next_media_sequence{1}; /* 仅媒体 DAG 访问的 access unit 序号。 */
std::chrono::steady_clock::time_point media_origin{std::chrono::steady_clock::now()}; /* 单调 H.264 PTS 原点。 */
std::chrono::microseconds last_presentation_time{}; /* 仅媒体 DAG 访问的最近 H.264 PTS。 */
std::chrono::steady_clock::time_point metric_started{}; /* 当前统计窗口起点。 */
std::uint64_t metric_received_start{}; /* 窗口起点累计收到帧数。 */
std::uint64_t metric_processed_start{}; /* 窗口起点累计处理帧数。 */
std::uint64_t metric_encoded_start{}; /* 窗口起点累计编码帧数。 */
std::vector<std::uint64_t> metric_completion_starts{}; /* 各 Plot 窗口起点逻辑完成数。 */
std::vector<std::uint64_t> metric_rendered_starts{}; /* 各 Plot 窗口起点真实画面数。 */
static constexpr std::size_t maximum_taskflow_trace_frames{120};
std::atomic_size_t taskflow_trace_remaining{}; /* 尚待捕获的媒体 DAG 次数。 */
std::atomic_uint64_t taskflow_trace_control{}; /* 高 32 位 requested,低 32 位 captured。 */
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames> taskflow_trace_slots{};
Private();
~Private();
template <Attached Attached_Object>
void bind_private_crtp(Attached_Object* attached) {
Prev_Private::bind_private_crtp(attached);
object = not_null{static_cast<Object*>(attached)};
}
void initialize(std::vector<Plot_Entry> plots);
[[nodiscard]] bool consumer_accepts() const noexcept;
[[nodiscard]] bool remove_consumer(Stream_Id stream) noexcept;
void publish(std::shared_ptr<const Encoded_Video_Frame> video,
std::string notification) noexcept;
void fail(std::exception_ptr failure) noexcept;
void accept_frame(std::size_t slot,
std::shared_ptr<const Plot_Stream_Frame> frame);
void arm_media_task(std::weak_ptr<Gallery_Video_Stream> lifetime);
void begin_media_frame();
void encode_media_frame();
void publish_video_frame();
void complete_media_frame();
void update_metrics(const Active_Media_Frame& frame,
std::size_t encoded_bytes,
std::optional<Video_Encoder_Backend> encoder_backend);
[[nodiscard]] bool mark_taskflow_trace();
void restore_taskflow_trace() noexcept;
void store_taskflow_trace(const Taskflow_Frame_Trace& trace);
[[nodiscard]] nlohmann::json taskflow_trace_response() const;
};
}
@@ -1,4 +1,4 @@
#include "Gallery_WebSocket.hpp"
#include "Gallery_Video_WebSocket.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <atomic>
@@ -13,10 +13,9 @@
#include <utility>
#include <vector>
namespace aethera::web {
namespace aethera::web::media {
namespace {
constexpr std::size_t h264_header_bytes{40};
constexpr std::size_t maximum_outstanding_frames{3};
constexpr std::size_t h264_header_bytes{48};
template <class Integer>
void write_little_endian(std::span<std::byte> output,
@@ -39,24 +38,25 @@ std::vector<std::byte> websocket_packet(const Encoded_Video_Frame& frame) {
output[2] = static_cast<std::byte>('H');
output[3] = static_cast<std::byte>('1');
const auto bytes = std::span{output};
write_little_endian(bytes, 4, std::uint16_t{1});
write_little_endian(bytes, 4, std::uint16_t{2});
write_little_endian(bytes, 6,
static_cast<std::uint16_t>(frame.key_frame ? 1U : 0U));
write_little_endian(bytes, 8, frame.sequence);
write_little_endian(bytes, 16, frame.presentation_time.count());
write_little_endian(bytes, 24, frame.source_time_unix.count());
const auto packed_time = std::chrono::duration_cast<
std::chrono::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
write_little_endian(bytes, 24, packed_time);
write_little_endian(bytes, 32,
write_little_endian(bytes, 32, packed_time);
write_little_endian(bytes, 40,
static_cast<std::uint32_t>(frame.annex_b.size()));
write_little_endian(bytes, 36, std::uint32_t{0});
write_little_endian(bytes, 44, std::uint32_t{0});
std::ranges::copy(frame.annex_b, output.begin() + h264_header_bytes);
return output;
}
}
struct Gallery_WebSocket::Private final {
struct Gallery_Video_WebSocket::Private final {
std::weak_ptr<drogon::WebSocketConnection> connection;
std::shared_ptr<Gallery_Video_Stream> stream;
Gallery_Video_Stream::Stream_Id subscription{};
@@ -71,7 +71,7 @@ struct Gallery_WebSocket::Private final {
std::atomic_uint64_t rejected_count{};
};
Gallery_WebSocket::Gallery_WebSocket(
Gallery_Video_WebSocket::Gallery_Video_WebSocket(
drogon::WebSocketConnectionPtr connection,
std::shared_ptr<Gallery_Video_Stream> stream,
std::string connection_id)
@@ -84,9 +84,9 @@ Gallery_WebSocket::Gallery_WebSocket(
d->connection_id = std::move(connection_id);
}
Gallery_WebSocket::~Gallery_WebSocket() { close(); }
Gallery_Video_WebSocket::~Gallery_Video_WebSocket() { close(); }
void Gallery_WebSocket::start() {
void Gallery_Video_WebSocket::start() {
if (d->attached.exchange(true, std::memory_order_acq_rel)) return;
const auto weak = weak_from_this();
d->subscription = d->stream->subscribe(
@@ -102,10 +102,7 @@ void Gallery_WebSocket::start() {
return false;
const auto connection = socket->d->connection.lock();
return connection && connection->connected() &&
socket->d->decoder_ready.load(std::memory_order_acquire) &&
socket->d->outstanding_frames.load(
std::memory_order_acquire) <
maximum_outstanding_frames;
socket->d->decoder_ready.load(std::memory_order_acquire);
},
[weak] {
const auto socket = weak.lock();
@@ -135,7 +132,7 @@ void Gallery_WebSocket::start() {
drogon::WebSocketMessageType::Text);
}
void Gallery_WebSocket::deliver(Gallery_Stream_Frame frame) {
void Gallery_Video_WebSocket::deliver(Gallery_Stream_Frame frame) {
if (!d->attached.load(std::memory_order_acquire)) return;
if (frame.video && !queue_video_frame(std::move(frame.video))) {
d->rejected_count.fetch_add(1, std::memory_order_relaxed);
@@ -149,7 +146,7 @@ void Gallery_WebSocket::deliver(Gallery_Stream_Frame frame) {
}
}
bool Gallery_WebSocket::queue_video_frame(
bool Gallery_Video_WebSocket::queue_video_frame(
std::shared_ptr<const Encoded_Video_Frame> frame) {
if (!frame || !d->attached.load(std::memory_order_acquire))
return false;
@@ -173,7 +170,7 @@ bool Gallery_WebSocket::queue_video_frame(
}
}
void Gallery_WebSocket::acknowledge_video_frame(std::uint64_t sequence) {
void Gallery_Video_WebSocket::acknowledge_video_frame(std::uint64_t sequence) {
auto acknowledged = d->latest_acknowledged_sequence.load(
std::memory_order_acquire);
while (sequence > acknowledged) {
@@ -193,7 +190,7 @@ void Gallery_WebSocket::acknowledge_video_frame(std::uint64_t sequence) {
}
}
void Gallery_WebSocket::receive(std::string_view message) {
void Gallery_Video_WebSocket::receive(std::string_view message) {
const auto json = nlohmann::json::parse(message, nullptr, false);
if (!json.is_object()) return;
const auto kind = json.value("kind", std::string{});
@@ -215,7 +212,7 @@ void Gallery_WebSocket::receive(std::string_view message) {
d->stream->request_video_key_frame();
}
void Gallery_WebSocket::close() noexcept {
void Gallery_Video_WebSocket::close() noexcept {
if (!d->attached.exchange(false, std::memory_order_acq_rel)) return;
try {
if (d->subscription != 0) d->stream->unsubscribe(d->subscription);
@@ -226,19 +223,19 @@ void Gallery_WebSocket::close() noexcept {
d->outstanding_frames.store(0, std::memory_order_release);
}
Gallery_WebSocket_Controller::Gallery_WebSocket_Controller(
Gallery_Video_WebSocket_Controller::Gallery_Video_WebSocket_Controller(
Stream_Resolver resolver) : resolve_stream(std::move(resolver)) {
if (!resolve_stream)
throw std::invalid_argument(
"gallery WebSocket requires a stream resolver");
}
void Gallery_WebSocket_Controller::initPathRouting() {
void Gallery_Video_WebSocket_Controller::initPathRouting() {
drogon::app().registerWebSocketController(
"/ws/gallery", classTypeName());
}
void Gallery_WebSocket_Controller::handleNewConnection(
void Gallery_Video_WebSocket_Controller::handleNewConnection(
const drogon::HttpRequestPtr& request,
const drogon::WebSocketConnectionPtr& connection) {
try {
@@ -249,7 +246,7 @@ void Gallery_WebSocket_Controller::handleNewConnection(
"Invalid gallery H.264 request");
return;
}
auto socket = std::make_shared<Gallery_WebSocket>(
auto socket = std::make_shared<Gallery_Video_WebSocket>(
connection, std::move(stream), connection_id);
connection->setContext(socket);
connection->setPingMessage(
@@ -257,7 +254,7 @@ void Gallery_WebSocket_Controller::handleNewConnection(
socket->start();
}
catch (...) {
if (const auto socket = connection->getContext<Gallery_WebSocket>())
if (const auto socket = connection->getContext<Gallery_Video_WebSocket>())
socket->close();
try {
connection->shutdown(drogon::CloseCode::kViolation,
@@ -267,19 +264,19 @@ void Gallery_WebSocket_Controller::handleNewConnection(
}
}
void Gallery_WebSocket_Controller::handleNewMessage(
void Gallery_Video_WebSocket_Controller::handleNewMessage(
const drogon::WebSocketConnectionPtr& connection, std::string&& message,
const drogon::WebSocketMessageType& type) {
if (type != drogon::WebSocketMessageType::Text ||
message.size() > 4096U)
return;
if (const auto socket = connection->getContext<Gallery_WebSocket>())
if (const auto socket = connection->getContext<Gallery_Video_WebSocket>())
socket->receive(message);
}
void Gallery_WebSocket_Controller::handleConnectionClosed(
void Gallery_Video_WebSocket_Controller::handleConnectionClosed(
const drogon::WebSocketConnectionPtr& connection) {
if (const auto socket = connection->getContext<Gallery_WebSocket>())
if (const auto socket = connection->getContext<Gallery_Video_WebSocket>())
socket->close();
connection->clearContext();
}
@@ -5,17 +5,17 @@
#include <memory>
#include <string_view>
namespace aethera::web {
namespace aethera::web::media {
/* 同一个 Drogon WebSocket 承载布局/背压控制文本和 H.264 Annex-B 二进制帧。 */
struct Gallery_WebSocket final
: std::enable_shared_from_this<Gallery_WebSocket> {
Gallery_WebSocket(drogon::WebSocketConnectionPtr connection,
struct Gallery_Video_WebSocket final
: std::enable_shared_from_this<Gallery_Video_WebSocket> {
Gallery_Video_WebSocket(drogon::WebSocketConnectionPtr connection,
std::shared_ptr<Gallery_Video_Stream> stream,
std::string connection_id);
~Gallery_WebSocket();
Gallery_WebSocket(const Gallery_WebSocket&) = delete;
Gallery_WebSocket& operator=(const Gallery_WebSocket&) = delete;
~Gallery_Video_WebSocket();
Gallery_Video_WebSocket(const Gallery_Video_WebSocket&) = delete;
Gallery_Video_WebSocket& operator=(const Gallery_Video_WebSocket&) = delete;
void start();
void receive(std::string_view message);
@@ -30,11 +30,11 @@ private:
std::unique_ptr<Private> d;
};
struct Gallery_WebSocket_Controller final
: drogon::WebSocketController<Gallery_WebSocket_Controller, false> {
struct Gallery_Video_WebSocket_Controller final
: drogon::WebSocketController<Gallery_Video_WebSocket_Controller, false> {
using Stream_Resolver = std::function<
std::shared_ptr<Gallery_Video_Stream>(std::string_view)>;
explicit Gallery_WebSocket_Controller(Stream_Resolver resolver);
explicit Gallery_Video_WebSocket_Controller(Stream_Resolver resolver);
void handleNewMessage(const drogon::WebSocketConnectionPtr& connection,
std::string&& message,
const drogon::WebSocketMessageType& type) override;
@@ -1,17 +1,14 @@
#include "FFmpeg_Frame_Transport.hpp"
#include <atomic>
#include <cmath>
namespace aethera::web::frame_sampling::ffmpeg {
namespace aethera::web::media::detail {
struct FFmpeg_Frame_Transport::Private {
H264_Encoder encoder; /* FFmpeg 编码上下文的唯一所有者。 */
double frame_rate_fps{}; /* 媒体序号到单调 PTS 的唯一换算基准。 */
std::atomic_bool key_frame_requested{}; /* 下一次实际编码样本是否强制关键帧。 */
explicit Private(double value_frame_rate_fps)
: encoder(value_frame_rate_fps),
frame_rate_fps(value_frame_rate_fps) {}
: encoder(value_frame_rate_fps) {}
};
FFmpeg_Frame_Transport::FFmpeg_Frame_Transport(double frame_rate_fps)
@@ -24,20 +21,21 @@ void FFmpeg_Frame_Transport::request_key_frame() noexcept {
}
std::optional<Encoded_Video_Frame> FFmpeg_Frame_Transport::encode(
const Sampled_Gallery_Frame& frame) {
std::span<const std::byte> pixels,
std::uint32_t width,
std::uint32_t height,
Plot_Pixel_Layout layout,
std::uint64_t sequence,
std::chrono::microseconds presentation_time,
std::chrono::nanoseconds source_time_unix) {
if (d->key_frame_requested.exchange(false, std::memory_order_acq_rel))
d->encoder.request_key_frame();
return d->encoder.encode(
frame.composition.pixels,
frame.composition.width,
frame.composition.height,
frame.composition.layout == Plot_Pixel_Layout::bgra8
pixels, width, height,
layout == Plot_Pixel_Layout::bgra8
? Video_Pixel_Layout::bgra
: Video_Pixel_Layout::rgba,
frame.sample_sequence,
std::chrono::microseconds{static_cast<std::int64_t>(std::llround(
static_cast<double>(frame.sample_sequence) *
(1'000'000.0 / d->frame_rate_fps)))});
sequence, presentation_time, source_time_unix);
}
}
@@ -0,0 +1,36 @@
#pragma once
#include "H264_Encoder.hpp"
#include <mcp/core/runtime/Plot.hpp>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
namespace aethera::web::media::detail {
/* 已通过 Plot 帧策略的图集像素到 H.264 access unit 的单一编码位置。 */
struct FFmpeg_Frame_Transport final {
public:
explicit FFmpeg_Frame_Transport(double frame_rate_fps);
~FFmpeg_Frame_Transport();
FFmpeg_Frame_Transport(const FFmpeg_Frame_Transport&) = delete;
FFmpeg_Frame_Transport& operator=(const FFmpeg_Frame_Transport&) = delete;
void request_key_frame() noexcept;
[[nodiscard]] std::optional<Encoded_Video_Frame> encode(
std::span<const std::byte> pixels,
std::uint32_t width,
std::uint32_t height,
Plot_Pixel_Layout layout,
std::uint64_t sequence,
std::chrono::microseconds presentation_time,
std::chrono::nanoseconds source_time_unix);
private:
struct Private;
std::unique_ptr<Private> d;
};
}
@@ -6,7 +6,7 @@
#include <stdexcept>
#include <utility>
namespace aethera::web::detail {
namespace aethera::web::media::detail {
struct Gallery_Frame_Atlas::Private {
struct Source {
std::atomic<std::shared_ptr<const Plot_Pixel_Frame>>
@@ -96,15 +96,10 @@ Gallery_Frame_Atlas::Accept_Frame_Result Gallery_Frame_Atlas::accept_frame(
}
auto current = source.latest_completion.load(
std::memory_order_acquire);
bool rendered_advanced{};
const bool rendered_advanced = frame->rendered_sequence != 0 &&
(!current ||
frame->rendered_sequence != current->rendered_sequence);
for (;;) {
if (current && frame->sequence <= current->sequence) {
d->rejected_frame_count.fetch_add(1, std::memory_order_relaxed);
return Accept_Frame_Result::stale_frame;
}
rendered_advanced = frame->rendered_sequence != 0 &&
(!current ||
frame->rendered_sequence != current->rendered_sequence);
auto desired = frame;
if (source.latest_completion.compare_exchange_weak(
current, std::move(desired), std::memory_order_release,
@@ -115,18 +110,9 @@ Gallery_Frame_Atlas::Accept_Frame_Result Gallery_Frame_Atlas::accept_frame(
if (rendered_advanced)
source.rendered_frame_count.fetch_add(
1, std::memory_order_release);
if (frame->pixels && frame->rendered_sequence != 0) {
auto pixels = source.latest_pixels.load(std::memory_order_acquire);
while (!pixels ||
frame->rendered_sequence > pixels->rendered_sequence) {
auto desired = frame;
if (source.latest_pixels.compare_exchange_weak(
pixels, std::move(desired),
std::memory_order_release,
std::memory_order_acquire))
break;
}
}
if (frame->pixels && frame->rendered_sequence != 0)
source.latest_pixels.store(std::move(frame),
std::memory_order_release);
return Accept_Frame_Result::accepted;
}
@@ -7,7 +7,7 @@
#include <span>
#include <string>
#include <vector>
namespace aethera::web::detail {
namespace aethera::web::media::detail {
struct Gallery_Atlas_Source_Description {
std::string id; /* 图集槽位对应的 Plot 业务标识。 */
std::size_t slot{}; /* 从左到右、从上到下的稳定槽位序号。 */
@@ -46,8 +46,7 @@ struct Gallery_Frame_Atlas final {
public:
enum struct Accept_Frame_Result : std::uint8_t {
accepted,
invalid_frame,
stale_frame
invalid_frame
};
Gallery_Frame_Atlas(std::uint32_t tile_width,
std::uint32_t tile_height,
@@ -16,7 +16,7 @@ extern "C" {
#include <libswscale/swscale.h>
}
namespace aethera::web {
namespace aethera::web::media {
namespace {
constexpr std::string_view high_profile_level_5_1{"640033"};
@@ -49,10 +49,13 @@ std::string_view h264_profile_level_id() noexcept {
return high_profile_level_5_1;
}
namespace detail {
struct H264_Encoder::Private {
struct Submitted_Frame {
std::uint64_t sequence{};
std::chrono::microseconds presentation_time{};
std::chrono::nanoseconds source_time_unix{};
};
double frame_rate{};
@@ -199,7 +202,8 @@ void H264_Encoder::request_key_frame() {
std::optional<Encoded_Video_Frame> H264_Encoder::encode(
std::span<const std::byte> pixels, std::uint32_t width,
std::uint32_t height, Video_Pixel_Layout layout,
std::uint64_t sequence, std::chrono::microseconds presentation_time) {
std::uint64_t sequence, std::chrono::microseconds presentation_time,
std::chrono::nanoseconds source_time_unix) {
if (width == 0 || height == 0 || (width & 1U) != 0 ||
(height & 1U) != 0 ||
static_cast<std::size_t>(width) >
@@ -218,7 +222,8 @@ std::optional<Encoded_Video_Frame> H264_Encoder::encode(
? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_NONE;
d->key_frame_requested = false;
d->submitted_frames.push_back({sequence, presentation_time});
d->submitted_frames.push_back(
{sequence, presentation_time, source_time_unix});
const auto submitted = avcodec_send_frame(
d->codec_context, d->frame);
if (submitted < 0) {
@@ -247,9 +252,12 @@ std::optional<Encoded_Video_Frame> H264_Encoder::encode(
reinterpret_cast<const std::byte*>(
d->packet->data + d->packet->size));
output.presentation_time = submitted_frame.presentation_time;
output.source_time_unix = submitted_frame.source_time_unix;
output.sequence = submitted_frame.sequence;
output.backend = Video_Encoder_Backend::libx264;
output.key_frame = (d->packet->flags & AV_PKT_FLAG_KEY) != 0;
return output;
}
}
}
@@ -0,0 +1,26 @@
#pragma once
#include "../Encoded_Video_Frame.hpp"
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
namespace aethera::web::media::detail {
struct H264_Encoder final {
public:
explicit H264_Encoder(double frame_rate);
~H264_Encoder();
H264_Encoder(const H264_Encoder&) = delete;
H264_Encoder& operator=(const H264_Encoder&) = delete;
void request_key_frame();
[[nodiscard]] std::optional<Encoded_Video_Frame> encode(
std::span<const std::byte> pixels, std::uint32_t width,
std::uint32_t height, Video_Pixel_Layout layout,
std::uint64_t sequence, std::chrono::microseconds presentation_time,
std::chrono::nanoseconds source_time_unix);
private:
struct Private;
std::unique_ptr<Private> d;
};
}
+10 -9
View File
@@ -1,11 +1,11 @@
#include <gtest/gtest.h>
#include <web_server/src/detail/Gallery_Frame_Atlas.hpp>
#include <web_server/src/media/detail/Gallery_Frame_Atlas.hpp>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <vector>
namespace aethera::web::detail {
namespace aethera::web::media::detail {
namespace {
std::shared_ptr<const Plot_Pixel_Frame> solid_frame(
std::uint64_t sequence, std::uint32_t width,
@@ -70,20 +70,21 @@ TEST(Gallery_Frame_Atlas,
EXPECT_EQ(composition.sources[0].rendered_frame_count, 1U);
}
TEST(Gallery_Frame_Atlas, Rejects_Stale_Completion_Without_Mutating_Progress) {
TEST(Gallery_Frame_Atlas, Applies_Out_Of_Order_Completions_In_Arrival_Order) {
Gallery_Frame_Atlas atlas(2, 2, 1, {"alpha"});
ASSERT_EQ(atlas.accept_frame(
0, solid_frame(2, 2, 2, std::byte{31})),
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
static_cast<void>(atlas.compose());
ASSERT_EQ(atlas.accept_frame(
0, completion_frame(1, 1, 2, 2)),
Gallery_Frame_Atlas::Accept_Frame_Result::stale_frame);
0, solid_frame(1, 2, 2, std::byte{47})),
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
const auto composition = atlas.compose();
ASSERT_EQ(composition.sources.size(), 1U);
EXPECT_EQ(composition.rejected_frame_count, 1U);
EXPECT_EQ(composition.sources[0].completion_sequence, 2U);
EXPECT_EQ(composition.sources[0].completion_count, 1U);
EXPECT_EQ(composition.pixels[0], std::byte{31});
EXPECT_EQ(composition.rejected_frame_count, 0U);
EXPECT_EQ(composition.sources[0].completion_sequence, 1U);
EXPECT_EQ(composition.sources[0].completion_count, 2U);
EXPECT_EQ(composition.pixels[0], std::byte{47});
}
}
@@ -0,0 +1,122 @@
#include <gtest/gtest.h>
#include <mcp/core/runtime/Gallery_Plots.hpp>
#include <render_common.hpp>
#include <web_server/src/media/Gallery_Video_Stream.hpp>
#include <array>
#include <atomic>
#include <chrono>
#include <ranges>
#include <stdexcept>
#include <string>
namespace aethera::web::media {
namespace {
using Clock = std::chrono::steady_clock;
void corun_until_or_throw(std::string_view operation,
const std::function<bool()>& completed) {
std::string failure;
Task_Graph coordinator{"gallery.video.test.await"};
coordinator.add("await", [&] {
const auto deadline = Clock::now() + std::chrono::seconds{10};
Task_Graph::corun_until([&] {
if (completed()) return true;
if (Clock::now() < deadline) return false;
failure = std::string{operation} + " timed out";
return true;
});
});
aethera::detail::run_taskflow(coordinator);
if (!failure.empty()) throw std::runtime_error(failure);
}
void configure_manual_pixel_delivery(const std::shared_ptr<Plot>& plot) {
ASSERT_TRUE(plot);
ASSERT_TRUE(plot->write_prop(
"frame-analysis", "pacing_mode", "manual").value(
"success", false));
ASSERT_TRUE(plot->write_prop(
"frame-analysis", "pixel_delivery_enabled", true).value(
"success", false));
ASSERT_NO_THROW(corun_until_or_throw("manual Plot policy", [&] {
const auto diagnostics = plot->diagnostics();
const auto& configuration = diagnostics.at("frame_policy").at(
"configuration");
return configuration.at("mode") == "manual" &&
configuration.at("pixel_delivery_enabled").get<bool>();
}));
}
void schedule_manual_frame(const std::shared_ptr<Plot>& plot,
std::uint64_t correlation_sequence) {
const auto now = Clock::now();
plot->schedule_render(Plot_Render_Tick{
.issued_at = now,
.sequence = correlation_sequence,
.time_milliseconds =
std::chrono::duration<double, std::milli>(
now.time_since_epoch()).count(),
.width = 720,
.height = 420,
.source = Frame_Request_Source::immediate
});
}
TEST(Gallery_Video_Stream,
Encodes_Each_Plots_Independent_Policy_Frames_Without_Grouping) {
const auto definitions = gallery_plot_definitions();
const auto found = std::ranges::find_if(
definitions, [](const Plot_Definition& definition) {
return definition.dimension == Plot_Dimension::two_d;
});
ASSERT_NE(found, definitions.end());
auto plot_a = found->create();
auto plot_b = found->create();
configure_manual_pixel_delivery(plot_a);
configure_manual_pixel_delivery(plot_b);
auto stream = Gallery_Video_Stream::create({
{"plot-a", plot_a}, {"plot-b", plot_b}});
ASSERT_TRUE(stream);
constexpr std::size_t frame_count{5};
std::array<std::atomic_uint64_t, frame_count> sequences{};
std::atomic_size_t received{};
const auto subscription = stream->subscribe(
"gallery-video-test",
[&](Gallery_Stream_Frame frame) {
if (!frame.video) return;
const auto index = received.fetch_add(
1, std::memory_order_acq_rel);
if (index < sequences.size())
sequences[index].store(
frame.video->sequence, std::memory_order_release);
},
[] { return true; },
[] { return nlohmann::json::object(); });
const std::array<std::shared_ptr<Plot>, frame_count> schedule{
plot_a, plot_a, plot_b, plot_a, plot_b};
for (std::size_t index = 0; index < schedule.size(); ++index) {
schedule_manual_frame(schedule[index], index + 1U);
ASSERT_NO_THROW(corun_until_or_throw("encoded Plot frame", [&] {
return received.load(std::memory_order_acquire) > index;
}));
}
EXPECT_EQ(received.load(std::memory_order_acquire), frame_count);
for (std::size_t index = 0; index < frame_count; ++index)
EXPECT_EQ(sequences[index].load(std::memory_order_acquire),
index + 1U);
const auto diagnostics = stream->diagnostics();
EXPECT_EQ(diagnostics.at("received_frame_count"), frame_count);
EXPECT_EQ(diagnostics.at("processed_frame_count"), frame_count);
EXPECT_EQ(diagnostics.at("encoded_frame_count"), frame_count);
EXPECT_EQ(diagnostics.at("pending_frame_count"), 0U);
stream->unsubscribe(subscription);
stream->shutdown();
}
}
}
+11 -5
View File
@@ -1,12 +1,12 @@
#include <gtest/gtest.h>
#include <web_server/src/H264_Encoder.hpp>
#include <web_server/src/media/detail/H264_Encoder.hpp>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
namespace aethera::web {
namespace aethera::web::media::detail {
namespace {
std::vector<std::byte> test_pattern(std::uint32_t width,
std::uint32_t height) {
@@ -34,13 +34,15 @@ TEST(H264_Encoder, Rejects_Invalid_Input_Before_FFmpeg) {
const auto pixels = test_pattern(320, 180);
EXPECT_THROW(static_cast<void>(encoder.encode(
pixels, 319, 180, Video_Pixel_Layout::rgba,
1, std::chrono::microseconds{33'333})),
1, std::chrono::microseconds{33'333},
std::chrono::nanoseconds{1})),
std::invalid_argument);
EXPECT_THROW(static_cast<void>(encoder.encode(
std::span<const std::byte>{pixels}.first(
pixels.size() - 1U),
320, 180, Video_Pixel_Layout::rgba,
1, std::chrono::microseconds{33'333})),
1, std::chrono::microseconds{33'333},
std::chrono::nanoseconds{1})),
std::invalid_argument);
}
@@ -57,11 +59,15 @@ TEST(H264_Encoder, Libx264_Produces_Ordered_Annex_B_Access_Units) {
auto encoded = encoder.encode(
pixels, width, height, Video_Pixel_Layout::rgba, sequence,
std::chrono::microseconds{
static_cast<std::int64_t>(sequence * 33'333)});
static_cast<std::int64_t>(sequence * 33'333)},
std::chrono::nanoseconds{
static_cast<std::int64_t>(sequence * 1'000)});
if (!encoded) continue;
if (!first) first = encoded;
EXPECT_EQ(encoded->sequence, sequence);
EXPECT_EQ(encoded->backend, Video_Encoder_Backend::libx264);
EXPECT_EQ(encoded->source_time_unix,
std::chrono::nanoseconds{sequence * 1'000});
EXPECT_FALSE(encoded->annex_b.empty());
++produced;
}