#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
-#include <sys/resource.h>
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include "DeckLinkAPI.h"
#include "LinuxCOM.h"
#include "alsa_output.h"
+#include "basic_stats.h"
#include "bmusb/bmusb.h"
#include "bmusb/fake_capture.h"
#include "chroma_subsampler.h"
#include "v210_converter.h"
#include "video_encoder.h"
+#undef Status
+#include <google/protobuf/util/json_util.h>
+#include "json.pb.h"
+
class IDeckLink;
class QOpenGLContext;
using namespace bmusb;
Mixer *global_mixer = nullptr;
-bool uses_mlock = false;
namespace {
} // namespace
-void QueueLengthPolicy::update_policy(unsigned queue_length)
+void JitterHistory::register_metrics(const vector<pair<string, string>> &labels)
+{
+ global_metrics.add("input_underestimated_jitter_frames", labels, &metric_input_underestimated_jitter_frames);
+ global_metrics.add("input_estimated_max_jitter_seconds", labels, &metric_input_estimated_max_jitter_seconds, Metrics::TYPE_GAUGE);
+}
+
+void JitterHistory::unregister_metrics(const vector<pair<string, string>> &labels)
+{
+ global_metrics.remove("input_underestimated_jitter_frames", labels);
+ global_metrics.remove("input_estimated_max_jitter_seconds", labels);
+}
+
+void JitterHistory::frame_arrived(steady_clock::time_point now, int64_t frame_duration, size_t dropped_frames)
{
- if (queue_length == 0) { // Starvation.
- if (been_at_safe_point_since_last_starvation && safe_queue_length < unsigned(global_flags.max_input_queue_frames)) {
- ++safe_queue_length;
- fprintf(stderr, "Card %u: Starvation, increasing safe limit to %u frame(s)\n",
- card_index, safe_queue_length);
+ if (expected_timestamp > steady_clock::time_point::min()) {
+ expected_timestamp += dropped_frames * nanoseconds(frame_duration * 1000000000 / TIMEBASE);
+ double jitter_seconds = fabs(duration<double>(expected_timestamp - now).count());
+ history.push_back(orders.insert(jitter_seconds));
+ if (jitter_seconds > estimate_max_jitter()) {
+ ++metric_input_underestimated_jitter_frames;
}
- frames_with_at_least_one = 0;
- been_at_safe_point_since_last_starvation = false;
- return;
+
+ metric_input_estimated_max_jitter_seconds = estimate_max_jitter();
+
+ if (history.size() > history_length) {
+ orders.erase(history.front());
+ history.pop_front();
+ }
+ assert(history.size() <= history_length);
}
- if (queue_length >= safe_queue_length) {
- been_at_safe_point_since_last_starvation = true;
+ expected_timestamp = now + nanoseconds(frame_duration * 1000000000 / TIMEBASE);
+}
+
+double JitterHistory::estimate_max_jitter() const
+{
+ if (orders.empty()) {
+ return 0.0;
}
- if (++frames_with_at_least_one >= 1000 && safe_queue_length > 1) {
- --safe_queue_length;
- fprintf(stderr, "Card %u: Spare frames for more than 1000 frames, reducing safe limit to %u frame(s)\n",
- card_index, safe_queue_length);
- frames_with_at_least_one = 0;
+ size_t elem_idx = lrint((orders.size() - 1) * percentile);
+ if (percentile <= 0.5) {
+ return *next(orders.begin(), elem_idx) * multiplier;
+ } else {
+ return *prev(orders.end(), orders.size() - elem_idx) * multiplier;
}
}
+void QueueLengthPolicy::register_metrics(const vector<pair<string, string>> &labels)
+{
+ global_metrics.add("input_queue_safe_length_frames", labels, &metric_input_queue_safe_length_frames, Metrics::TYPE_GAUGE);
+}
+
+void QueueLengthPolicy::unregister_metrics(const vector<pair<string, string>> &labels)
+{
+ global_metrics.remove("input_queue_safe_length_frames", labels);
+}
+
+void QueueLengthPolicy::update_policy(steady_clock::time_point now,
+ steady_clock::time_point expected_next_frame,
+ int64_t input_frame_duration,
+ int64_t master_frame_duration,
+ double max_input_card_jitter_seconds,
+ double max_master_card_jitter_seconds)
+{
+ double input_frame_duration_seconds = input_frame_duration / double(TIMEBASE);
+ double master_frame_duration_seconds = master_frame_duration / double(TIMEBASE);
+
+ // Figure out when we can expect the next frame for this card, assuming
+ // worst-case jitter (ie., the frame is maximally late).
+ double seconds_until_next_frame = max(duration<double>(expected_next_frame - now).count() + max_input_card_jitter_seconds, 0.0);
+
+ // How many times are the master card expected to tick in that time?
+ // We assume the master clock has worst-case jitter but not any rate
+ // discrepancy, ie., it ticks as early as possible every time, but not
+ // cumulatively.
+ double frames_needed = (seconds_until_next_frame + max_master_card_jitter_seconds) / master_frame_duration_seconds;
+
+ // As a special case, if the master card ticks faster than the input card,
+ // we expect the queue to drain by itself even without dropping. But if
+ // the difference is small (e.g. 60 Hz master and 59.94 input), it would
+ // go slowly enough that the effect wouldn't really be appreciable.
+ // We account for this by looking at the situation five frames ahead,
+ // assuming everything else is the same.
+ double frames_allowed;
+ if (master_frame_duration < input_frame_duration) {
+ frames_allowed = frames_needed + 5 * (input_frame_duration_seconds - master_frame_duration_seconds) / master_frame_duration_seconds;
+ } else {
+ frames_allowed = frames_needed;
+ }
+
+ safe_queue_length = max<int>(floor(frames_allowed), 0);
+ metric_input_queue_safe_length_frames = safe_queue_length;
+}
+
Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
: httpd(),
num_cards(num_cards),
mixer_surface(create_surface(format)),
h264_encoder_surface(create_surface(format)),
decklink_output_surface(create_surface(format)),
- ycbcr_interpretation(global_flags.ycbcr_interpretation),
audio_mixer(num_cards)
{
+ memcpy(ycbcr_interpretation, global_flags.ycbcr_interpretation, sizeof(ycbcr_interpretation));
CHECK(init_movit(MOVIT_SHADER_DIR, MOVIT_DEBUG_OFF));
check_error();
// Must be instantiated after VideoEncoder has initialized global_flags.use_zerocopy.
theme.reset(new Theme(global_flags.theme_filename, global_flags.theme_dirs, resource_pool.get(), num_cards));
+ httpd.add_endpoint("/channels", bind(&Mixer::get_channels_json, this), HTTPD::ALLOW_ALL_ORIGINS);
+ for (int channel_idx = 2; channel_idx < theme->get_num_channels(); ++channel_idx) {
+ char url[256];
+ snprintf(url, sizeof(url), "/channels/%d/color", channel_idx);
+ httpd.add_endpoint(url, bind(&Mixer::get_channel_color_http, this, unsigned(channel_idx)), HTTPD::ALLOW_ALL_ORIGINS);
+ }
+
// Start listening for clients only once VideoEncoder has written its header, if any.
- httpd.start(9095);
+ httpd.start(global_flags.http_port);
// First try initializing the then PCI devices, then USB, then
// fill up with fake cards until we have the desired number of cards.
DeckLinkCapture *capture = new DeckLinkCapture(decklink, card_index);
DeckLinkOutput *output = new DeckLinkOutput(resource_pool.get(), decklink_output_surface, global_flags.width, global_flags.height, card_index);
- output->set_device(decklink);
+ if (!output->set_device(decklink)) {
+ delete output;
+ output = nullptr;
+ }
configure_card(card_index, capture, CardType::LIVE_CARD, output);
++num_pci_devices;
}
set_output_card_internal(global_flags.output_card);
}
- global_metrics.register_int_metric("num_frames", &metrics_num_frames);
- global_metrics.register_int_metric("dropped_frames", &metrics_dropped_frames);
- global_metrics.register_double_metric("uptime", &metrics_uptime);
+ output_jitter_history.register_metrics({{ "card", "output" }});
}
Mixer::~Mixer()
audio_mixer.reset_resampler(device);
audio_mixer.set_display_name(device, card->capture->get_description());
audio_mixer.trigger_state_changed_callback();
+
+ // Unregister old metrics, if any.
+ if (!card->labels.empty()) {
+ const vector<pair<string, string>> &labels = card->labels;
+ card->jitter_history.unregister_metrics(labels);
+ card->queue_length_policy.unregister_metrics(labels);
+ global_metrics.remove("input_received_frames", labels);
+ global_metrics.remove("input_dropped_frames_jitter", labels);
+ global_metrics.remove("input_dropped_frames_error", labels);
+ global_metrics.remove("input_dropped_frames_resets", labels);
+ global_metrics.remove("input_queue_length_frames", labels);
+ global_metrics.remove("input_queue_duped_frames", labels);
+
+ global_metrics.remove("input_has_signal_bool", labels);
+ global_metrics.remove("input_is_connected_bool", labels);
+ global_metrics.remove("input_interlaced_bool", labels);
+ global_metrics.remove("input_width_pixels", labels);
+ global_metrics.remove("input_height_pixels", labels);
+ global_metrics.remove("input_frame_rate_nom", labels);
+ global_metrics.remove("input_frame_rate_den", labels);
+ global_metrics.remove("input_sample_rate_hz", labels);
+ }
+
+ // Register metrics.
+ vector<pair<string, string>> labels;
+ char card_name[64];
+ snprintf(card_name, sizeof(card_name), "%d", card_index);
+ labels.emplace_back("card", card_name);
+
+ switch (card_type) {
+ case CardType::LIVE_CARD:
+ labels.emplace_back("cardtype", "live");
+ break;
+ case CardType::FAKE_CAPTURE:
+ labels.emplace_back("cardtype", "fake");
+ break;
+ case CardType::FFMPEG_INPUT:
+ labels.emplace_back("cardtype", "ffmpeg");
+ break;
+ default:
+ assert(false);
+ }
+ card->jitter_history.register_metrics(labels);
+ card->queue_length_policy.register_metrics(labels);
+ global_metrics.add("input_received_frames", labels, &card->metric_input_received_frames);
+ global_metrics.add("input_dropped_frames_jitter", labels, &card->metric_input_dropped_frames_jitter);
+ global_metrics.add("input_dropped_frames_error", labels, &card->metric_input_dropped_frames_error);
+ global_metrics.add("input_dropped_frames_resets", labels, &card->metric_input_resets);
+ global_metrics.add("input_queue_length_frames", labels, &card->metric_input_queue_length_frames, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_queue_duped_frames", labels, &card->metric_input_duped_frames);
+
+ global_metrics.add("input_has_signal_bool", labels, &card->metric_input_has_signal_bool, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_is_connected_bool", labels, &card->metric_input_is_connected_bool, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_interlaced_bool", labels, &card->metric_input_interlaced_bool, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_width_pixels", labels, &card->metric_input_width_pixels, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_height_pixels", labels, &card->metric_input_height_pixels, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_frame_rate_nom", labels, &card->metric_input_frame_rate_nom, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_frame_rate_den", labels, &card->metric_input_frame_rate_den, Metrics::TYPE_GAUGE);
+ global_metrics.add("input_sample_rate_hz", labels, &card->metric_input_sample_rate_hz, Metrics::TYPE_GAUGE);
+ card->labels = labels;
}
void Mixer::set_output_card_internal(int card_index)
lock.unlock();
fake_capture->stop_dequeue_thread();
lock.lock();
- old_card->capture = move(old_card->parked_capture);
+ old_card->capture = move(old_card->parked_capture); // TODO: reset the metrics
old_card->is_fake_capture = false;
old_card->capture->start_bm_capture();
}
card->output->start_output(desired_output_video_mode, pts_int);
}
output_card_index = card_index;
+ output_jitter_history.clear();
}
namespace {
DeviceSpec device{InputSourceType::CAPTURE_CARD, card_index};
CaptureCard *card = &cards[card_index];
+ ++card->metric_input_received_frames;
+ card->metric_input_has_signal_bool = video_format.has_signal;
+ card->metric_input_is_connected_bool = video_format.is_connected;
+ card->metric_input_interlaced_bool = video_format.interlaced;
+ card->metric_input_width_pixels = video_format.width;
+ card->metric_input_height_pixels = video_format.height;
+ card->metric_input_frame_rate_nom = video_format.frame_rate_nom;
+ card->metric_input_frame_rate_den = video_format.frame_rate_den;
+ card->metric_input_sample_rate_hz = audio_format.sample_rate;
+
if (is_mode_scanning[card_index]) {
if (video_format.has_signal) {
// Found a stable signal, so stop scanning.
card_index, card->last_timecode, timecode);
audio_mixer.reset_resampler(device);
dropped_frames = 0;
+ ++card->metric_input_resets;
} else if (dropped_frames > 0) {
// Insert silence as needed.
fprintf(stderr, "Card %d dropped %d frame(s) (before timecode 0x%04x), inserting silence.\n",
card_index, dropped_frames, timecode);
+ card->metric_input_dropped_frames_error += dropped_frames;
bool success;
do {
} while (!success);
}
- audio_mixer.add_audio(device, audio_frame.data + audio_offset, num_samples, audio_format, frame_length, audio_frame.received_timestamp);
+ if (num_samples > 0) {
+ audio_mixer.add_audio(device, audio_frame.data + audio_offset, num_samples, audio_format, frame_length, audio_frame.received_timestamp);
+ }
// Done with the audio, so release it.
if (audio_frame.owner) {
new_frame.dropped_frames = dropped_frames;
new_frame.received_timestamp = video_frame.received_timestamp;
card->new_frames.push_back(move(new_frame));
- card->new_frames_changed.notify_all();
+ card->jitter_history.frame_arrived(video_frame.received_timestamp, frame_length, dropped_frames);
}
+ card->new_frames_changed.notify_all();
return;
}
new_frame.dropped_frames = dropped_frames;
new_frame.received_timestamp = video_frame.received_timestamp; // Ignore the audio timestamp.
card->new_frames.push_back(move(new_frame));
- card->new_frames_changed.notify_all();
+ card->jitter_history.frame_arrived(video_frame.received_timestamp, frame_length, dropped_frames);
}
+ card->new_frames_changed.notify_all();
}
}
}
}
- steady_clock::time_point start, now;
- start = steady_clock::now();
-
+ BasicStats basic_stats(/*verbose=*/true);
int stats_dropped_frames = 0;
while (!should_quit) {
++frame_num;
pts_int += frame_duration;
- now = steady_clock::now();
- double elapsed = duration<double>(now - start).count();
-
- metrics_num_frames = frame_num;
- metrics_dropped_frames = stats_dropped_frames;
- metrics_uptime = elapsed;
-
- if (frame_num % 100 == 0) {
- printf("%d frames (%d dropped) in %.3f seconds = %.1f fps (%.1f ms/frame)",
- frame_num, stats_dropped_frames, elapsed, frame_num / elapsed,
- 1e3 * elapsed / frame_num);
- // chain->print_phase_timing();
-
- // Check our memory usage, to see if we are close to our mlockall()
- // limit (if at all set).
- rusage used;
- if (getrusage(RUSAGE_SELF, &used) == -1) {
- perror("getrusage(RUSAGE_SELF)");
- assert(false);
- }
-
- if (uses_mlock) {
- rlimit limit;
- if (getrlimit(RLIMIT_MEMLOCK, &limit) == -1) {
- perror("getrlimit(RLIMIT_MEMLOCK)");
- assert(false);
- }
-
- if (limit.rlim_cur == 0) {
- printf(", using %ld MB memory (locked)",
- long(used.ru_maxrss / 1024));
- } else {
- printf(", using %ld / %ld MB lockable memory (%.1f%%)",
- long(used.ru_maxrss / 1024),
- long(limit.rlim_cur / 1048576),
- float(100.0 * (used.ru_maxrss * 1024.0) / limit.rlim_cur));
- }
- } else {
- printf(", using %ld MB memory (not locked)",
- long(used.ru_maxrss / 1024));
- }
-
- printf("\n");
- }
-
+ basic_stats.update(frame_num, stats_dropped_frames);
+ // if (frame_num % 100 == 0) chain->print_phase_timing();
if (should_cut.exchange(false)) { // Test and clear.
video_encoder->do_cut(frame_num);
return (card_index == master_card_index);
}
-void Mixer::trim_queue(CaptureCard *card, unsigned card_index)
+void Mixer::trim_queue(CaptureCard *card, size_t safe_queue_length)
{
// Count the number of frames in the queue, including any frames
// we dropped. It's hard to know exactly how we should deal with
for (const CaptureCard::NewFrame &frame : card->new_frames) {
queue_length += frame.dropped_frames + 1;
}
- card->queue_length_policy.update_policy(queue_length);
// If needed, drop frames until the queue is below the safe limit.
// We prefer to drop from the head, because all else being equal,
// we'd like more recent frames (less latency).
unsigned dropped_frames = 0;
- while (queue_length > card->queue_length_policy.get_safe_queue_length()) {
+ while (queue_length > safe_queue_length) {
assert(!card->new_frames.empty());
assert(queue_length > card->new_frames.front().dropped_frames);
queue_length -= card->new_frames.front().dropped_frames;
- if (queue_length <= card->queue_length_policy.get_safe_queue_length()) {
+ if (queue_length <= safe_queue_length) {
// No need to drop anything.
break;
}
++dropped_frames;
}
+ card->metric_input_dropped_frames_jitter += dropped_frames;
+ card->metric_input_queue_length_frames = queue_length;
+
#if 0
if (dropped_frames > 0) {
fprintf(stderr, "Card %u dropped %u frame(s) to keep latency down.\n",
#endif
}
+pair<string, string> Mixer::get_channels_json()
+{
+ Channels ret;
+ for (int channel_idx = 2; channel_idx < theme->get_num_channels(); ++channel_idx) {
+ Channel *channel = ret.add_channel();
+ channel->set_index(channel_idx);
+ channel->set_name(theme->get_channel_name(channel_idx));
+ channel->set_color(theme->get_channel_color(channel_idx));
+ }
+ string contents;
+ google::protobuf::util::MessageToJsonString(ret, &contents); // Ignore any errors.
+ return make_pair(contents, "text/json");
+}
+
+pair<string, string> Mixer::get_channel_color_http(unsigned channel_idx)
+{
+ return make_pair(theme->get_channel_color(channel_idx), "text/plain");
+}
Mixer::OutputFrameInfo Mixer::get_one_frame_from_each_card(unsigned master_card_index, bool master_card_is_output, CaptureCard::NewFrame new_frames[MAX_VIDEO_CARDS], bool has_new_frame[MAX_VIDEO_CARDS])
{
// and then restart.
assert(cards[master_card_index].capture->get_disconnected());
handle_hotplugged_cards();
+ lock.unlock();
goto start;
}
- if (!master_card_is_output) {
- output_frame_info.frame_timestamp =
- cards[master_card_index].new_frames.front().received_timestamp;
- }
-
for (unsigned card_index = 0; card_index < num_cards + num_video_inputs; ++card_index) {
CaptureCard *card = &cards[card_index];
- if (input_card_is_master_clock(card_index, master_card_index)) {
- // We don't use the queue length policy for the master card,
- // but we will if it stops being the master. Thus, clear out
- // the policy in case we switch in the future.
- card->queue_length_policy.reset(card_index);
- assert(!card->new_frames.empty());
+ if (card->new_frames.empty()) { // Starvation.
+ ++card->metric_input_duped_frames;
} else {
- trim_queue(card, card_index);
- }
- if (!card->new_frames.empty()) {
new_frames[card_index] = move(card->new_frames.front());
has_new_frame[card_index] = true;
card->new_frames.pop_front();
}
if (!master_card_is_output) {
+ output_frame_info.frame_timestamp = new_frames[master_card_index].received_timestamp;
output_frame_info.dropped_frames = new_frames[master_card_index].dropped_frames;
output_frame_info.frame_duration = new_frames[master_card_index].length;
}
+ if (!output_frame_info.is_preroll) {
+ output_jitter_history.frame_arrived(output_frame_info.frame_timestamp, output_frame_info.frame_duration, output_frame_info.dropped_frames);
+ }
+
+ for (unsigned card_index = 0; card_index < num_cards + num_video_inputs; ++card_index) {
+ CaptureCard *card = &cards[card_index];
+ if (has_new_frame[card_index] &&
+ !input_card_is_master_clock(card_index, master_card_index) &&
+ !output_frame_info.is_preroll) {
+ card->queue_length_policy.update_policy(
+ output_frame_info.frame_timestamp,
+ card->jitter_history.get_expected_next_frame(),
+ new_frames[master_card_index].length,
+ output_frame_info.frame_duration,
+ card->jitter_history.estimate_max_jitter(),
+ output_jitter_history.estimate_max_jitter());
+ trim_queue(card, min<int>(global_flags.max_input_queue_frames,
+ card->queue_length_policy.get_safe_queue_length()));
+ }
+ }
+
// This might get off by a fractional sample when changing master card
// between ones with different frame rates, but that's fine.
int num_samples_times_timebase = OUTPUT_FREQUENCY * output_frame_info.frame_duration + fractional_samples;
// The theme can't (or at least shouldn't!) call connect_signal() on
// each FFmpeg input, so we'll do it here.
for (const pair<LiveInputWrapper *, FFmpegCapture *> &conn : theme->get_signal_connections()) {
- conn.first->connect_signal_raw(conn.second->get_card_index());
+ conn.first->connect_signal_raw(conn.second->get_card_index(), input_state);
}
// If HDMI/SDI output is active and the user has requested auto mode,
live_frame.ready_fence = fence;
live_frame.input_frames = {};
live_frame.temp_textures = { y_display_tex, cbcr_display_tex };
- output_channel[OUTPUT_LIVE].output_frame(live_frame);
+ output_channel[OUTPUT_LIVE].output_frame(move(live_frame));
// Set up preview and any additional channels.
for (int i = 1; i < theme->get_num_channels() + 2; ++i) {
DisplayFrame display_frame;
Theme::Chain chain = theme->get_chain(i, pts(), global_flags.width, global_flags.height, input_state); // FIXME: dimensions
- display_frame.chain = chain.chain;
- display_frame.setup_chain = chain.setup_chain;
+ display_frame.chain = move(chain.chain);
+ display_frame.setup_chain = move(chain.setup_chain);
display_frame.ready_fence = fence;
- display_frame.input_frames = chain.input_frames;
+ display_frame.input_frames = move(chain.input_frames);
display_frame.temp_textures = {};
- output_channel[i].output_frame(display_frame);
+ output_channel[i].output_frame(move(display_frame));
}
}
}
}
-void Mixer::OutputChannel::output_frame(DisplayFrame frame)
+void Mixer::OutputChannel::output_frame(DisplayFrame &&frame)
{
// Store this frame for display. Remove the ready frame if any
// (it was seemingly never used).
if (has_ready_frame) {
parent->release_display_frame(&ready_frame);
}
- ready_frame = frame;
+ ready_frame = move(frame);
has_ready_frame = true;
// Call the callbacks under the mutex (they should be short),
}
if (has_ready_frame) {
assert(!has_current_frame);
- current_frame = ready_frame;
+ current_frame = move(ready_frame);
ready_frame.ready_fence.reset(); // Drop the refcount.
ready_frame.input_frames.clear(); // Drop the refcounts.
has_current_frame = true;