]> git.sesse.net Git - nageru/commitdiff
Make number of cards flexible at runtime.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sat, 30 May 2020 16:29:38 +0000 (18:29 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 1 Jun 2020 08:03:08 +0000 (10:03 +0200)
This is a cleanup that should really have been done when we added
hotplug in the first place, but it's becoming even more relevant
now that SRT “cards” are supported.

Basically, empty slots can now be filled with nothing instead of
fake capture cards (which generate frames and take a little bit
of CPU time); we only instantiate fake capture cards if the slot is
below some certain minimum index or has been used by the theme.
(Cards that are unused are now “inactive” and will generally not
show up in the UI.) This means that the --num-cards parameter is now
largely irrelevant; it is only there for guaranteeing a minimum
amount of fake cards, for testing. Most users will be happy just using the
default of 2. There's also a new --max-num-cards in the unlikely case
that you want to leave some cards unused, e.g. for other applications
on the same machine.

This also unifies handling of regular capture cards, FFmpeg “cards”
and CEF “cards”; they are indexed pretty much the same way.

21 files changed:
nageru/alsa_pool.cpp
nageru/audio_mixer.cpp
nageru/audio_mixer.h
nageru/benchmark_audio_mixer.cpp
nageru/card_type.h [new file with mode: 0644]
nageru/context_menus.cpp
nageru/flags.cpp
nageru/flags.h
nageru/glwidget.cpp
nageru/input_mapping.cpp
nageru/input_mapping.h
nageru/input_mapping_dialog.cpp
nageru/kaeru.cpp
nageru/mixer.cpp
nageru/mixer.h
nageru/print_latency.cpp
nageru/resampling_queue.cpp
nageru/resampling_queue.h
nageru/state.proto
nageru/theme.cpp
nageru/theme.h

index bdff5af09917b0c13a79bc5453b3e255ffd02ff4..70f533e32fafcfcbbb50a5718352cfc9a23d0930 100644 (file)
@@ -271,7 +271,7 @@ ALSAPool::ProbeResult ALSAPool::probe_device_once(unsigned card_index, unsigned
        reset_device(internal_dev_index);  // Restarts it if it is held (ie., we just replaced a dead card).
 
        DeviceSpec spec{InputSourceType::ALSA_INPUT, internal_dev_index};
-       global_audio_mixer->set_display_name(spec, display_name);
+       global_audio_mixer->set_device_parameters(spec, display_name, CardType::LIVE_CARD, /*num_channels=*/0, /*active=*/true);  // Type and channels are ignored.
        global_audio_mixer->trigger_state_changed_callback();
 
        return ALSAPool::ProbeResult::SUCCESS;
index cef3c3c87fb31416b78e50c7ac601797265f41f3..402131f558aa630de5eece413a2c0727dcd7df08 100644 (file)
@@ -229,11 +229,8 @@ void deinterleave_samples(const vector<float> &in, vector<float> *out_l, vector<
 
 }  // namespace
 
-AudioMixer::AudioMixer(unsigned num_capture_cards, unsigned num_ffmpeg_inputs)
-       : num_capture_cards(num_capture_cards),
-         num_ffmpeg_inputs(num_ffmpeg_inputs),
-         ffmpeg_inputs(new AudioDevice[num_ffmpeg_inputs]),
-         limiter(OUTPUT_FREQUENCY),
+AudioMixer::AudioMixer()
+       : limiter(OUTPUT_FREQUENCY),
          correlation(OUTPUT_FREQUENCY)
 {
        for (unsigned bus_index = 0; bus_index < MAX_BUSES; ++bus_index) {
@@ -301,7 +298,7 @@ void AudioMixer::reset_resampler_mutex_held(DeviceSpec device_spec)
                device->resampling_queue.reset();
        } else {
                device->resampling_queue.reset(new ResamplingQueue(
-                       device_spec, device->capture_frequency, OUTPUT_FREQUENCY, device->interesting_channels.size(),
+                       spec_to_string(device_spec), device->capture_frequency, OUTPUT_FREQUENCY, device->interesting_channels.size(),
                        global_flags.audio_queue_length_ms * 0.001));
        }
 }
@@ -491,8 +488,6 @@ AudioMixer::AudioDevice *AudioMixer::find_audio_device(DeviceSpec device)
                return &video_cards[device.index];
        case InputSourceType::ALSA_INPUT:
                return &alsa_inputs[device.index];
-       case InputSourceType::FFMPEG_VIDEO_INPUT:
-               return &ffmpeg_inputs[device.index];
        case InputSourceType::SILENCE:
        default:
                assert(false);
@@ -531,8 +526,7 @@ void AudioMixer::fill_audio_bus(const map<DeviceSpec, vector<float>> &samples_ca
                memset(output, 0, num_samples * 2 * sizeof(*output));
        } else {
                assert(bus.device.type == InputSourceType::CAPTURE_CARD ||
-                      bus.device.type == InputSourceType::ALSA_INPUT ||
-                      bus.device.type == InputSourceType::FFMPEG_VIDEO_INPUT);
+                      bus.device.type == InputSourceType::ALSA_INPUT);
                const float *lsrc, *rsrc;
                unsigned lstride, rstride;
                float *dptr = output;
@@ -598,12 +592,6 @@ vector<DeviceSpec> AudioMixer::get_active_devices() const
                        ret.push_back(device_spec);
                }
        }
-       for (unsigned card_index = 0; card_index < num_ffmpeg_inputs; ++card_index) {
-               const DeviceSpec device_spec{InputSourceType::FFMPEG_VIDEO_INPUT, card_index};
-               if (!find_audio_device(device_spec)->interesting_channels.empty()) {
-                       ret.push_back(device_spec);
-               }
-       }
        return ret;
 }
 
@@ -1027,12 +1015,12 @@ map<DeviceSpec, DeviceInfo> AudioMixer::get_devices()
        lock_guard<timed_mutex> lock(audio_mutex);
 
        map<DeviceSpec, DeviceInfo> devices;
-       for (unsigned card_index = 0; card_index < num_capture_cards; ++card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                const DeviceSpec spec{ InputSourceType::CAPTURE_CARD, card_index };
                const AudioDevice *device = &video_cards[card_index];
                DeviceInfo info;
                info.display_name = device->display_name;
-               info.num_channels = 8;
+               info.num_channels = device->num_channels;
                devices.insert(make_pair(spec, info));
        }
        vector<ALSAPool::Device> available_alsa_devices = alsa_pool.get_devices();
@@ -1047,23 +1035,27 @@ map<DeviceSpec, DeviceInfo> AudioMixer::get_devices()
                info.alsa_address = device.address;
                devices.insert(make_pair(spec, info));
        }
-       for (unsigned card_index = 0; card_index < num_ffmpeg_inputs; ++card_index) {
-               const DeviceSpec spec{ InputSourceType::FFMPEG_VIDEO_INPUT, card_index };
-               const AudioDevice *device = &ffmpeg_inputs[card_index];
-               DeviceInfo info;
-               info.display_name = device->display_name;
-               info.num_channels = 2;
-               devices.insert(make_pair(spec, info));
-       }
        return devices;
 }
 
-void AudioMixer::set_display_name(DeviceSpec device_spec, const string &name)
+void AudioMixer::set_device_parameters(DeviceSpec device_spec, const std::string &display_name, CardType card_type, unsigned num_channels, bool active)
 {
        AudioDevice *device = find_audio_device(device_spec);
 
        lock_guard<timed_mutex> lock(audio_mutex);
-       device->display_name = name;
+       if (active || device->display_name.empty()) {
+               device->display_name = display_name;
+       }
+       device->card_type = card_type;
+       device->active = active;
+}
+
+bool AudioMixer::get_active(DeviceSpec device_spec)
+{
+       AudioDevice *device = find_audio_device(device_spec);
+
+       lock_guard<timed_mutex> lock(audio_mutex);
+       return device->active;
 }
 
 void AudioMixer::serialize_device(DeviceSpec device_spec, DeviceSpecProto *device_spec_proto)
@@ -1081,25 +1073,16 @@ void AudioMixer::serialize_device(DeviceSpec device_spec, DeviceSpecProto *devic
                case InputSourceType::ALSA_INPUT:
                        alsa_pool.serialize_device(device_spec.index, device_spec_proto);
                        break;
-               case InputSourceType::FFMPEG_VIDEO_INPUT:
-                       device_spec_proto->set_type(DeviceSpecProto::FFMPEG_VIDEO_INPUT);
-                       device_spec_proto->set_index(device_spec.index);
-                       device_spec_proto->set_display_name(ffmpeg_inputs[device_spec.index].display_name);
-                       break;
        }
 }
 
 void AudioMixer::set_simple_input(unsigned card_index)
 {
-       assert(card_index < num_capture_cards + num_ffmpeg_inputs);
+       assert(card_index < MAX_VIDEO_CARDS);
        InputMapping new_input_mapping;
        InputMapping::Bus input;
        input.name = "Main";
-       if (card_index >= num_capture_cards) {
-               input.device = DeviceSpec{InputSourceType::FFMPEG_VIDEO_INPUT, card_index - num_capture_cards};
-       } else {
-               input.device = DeviceSpec{InputSourceType::CAPTURE_CARD, card_index};
-       }
+       input.device = DeviceSpec{InputSourceType::CAPTURE_CARD, card_index};
        input.source_channel[0] = 0;
        input.source_channel[1] = 1;
 
@@ -1119,11 +1102,6 @@ unsigned AudioMixer::get_simple_input() const
            input_mapping.buses[0].source_channel[0] == 0 &&
            input_mapping.buses[0].source_channel[1] == 1) {
                return input_mapping.buses[0].device.index;
-       } else if (input_mapping.buses.size() == 1 &&
-                  input_mapping.buses[0].device.type == InputSourceType::FFMPEG_VIDEO_INPUT &&
-                  input_mapping.buses[0].source_channel[0] == 0 &&
-                  input_mapping.buses[0].source_channel[1] == 1) {
-               return input_mapping.buses[0].device.index + num_capture_cards;
        } else {
                return numeric_limits<unsigned>::max();
        }
@@ -1147,8 +1125,7 @@ void AudioMixer::set_input_mapping_lock_held(const InputMapping &new_input_mappi
        map<DeviceSpec, set<unsigned>> interesting_channels;
        for (const InputMapping::Bus &bus : new_input_mapping.buses) {
                if (bus.device.type == InputSourceType::CAPTURE_CARD ||
-                   bus.device.type == InputSourceType::ALSA_INPUT ||
-                   bus.device.type == InputSourceType::FFMPEG_VIDEO_INPUT) {
+                   bus.device.type == InputSourceType::ALSA_INPUT) {
                        for (unsigned channel = 0; channel < 2; ++channel) {
                                if (bus.source_channel[channel] != -1) {
                                        interesting_channels[bus.device].insert(bus.source_channel[channel]);
@@ -1192,11 +1169,14 @@ void AudioMixer::set_input_mapping_lock_held(const InputMapping &new_input_mappi
                if (bus.device.type == InputSourceType::SILENCE) {
                        metrics.labels.emplace_back("source_type", "silence");
                } else if (bus.device.type == InputSourceType::CAPTURE_CARD) {
-                       metrics.labels.emplace_back("source_type", "capture_card");
+                       AudioDevice *device = find_audio_device(bus.device);
+                       if (device->card_type == CardType::FFMPEG_INPUT) {
+                               metrics.labels.emplace_back("source_type", "ffmpeg_video_input");
+                       } else {
+                               metrics.labels.emplace_back("source_type", "capture_card");
+                       }
                } else if (bus.device.type == InputSourceType::ALSA_INPUT) {
                        metrics.labels.emplace_back("source_type", "alsa_input");
-               } else if (bus.device.type == InputSourceType::FFMPEG_VIDEO_INPUT) {
-                       metrics.labels.emplace_back("source_type", "ffmpeg_video_input");
                } else {
                        assert(false);
                }
@@ -1240,14 +1220,6 @@ void AudioMixer::set_input_mapping_lock_held(const InputMapping &new_input_mappi
                        reset_resampler_mutex_held(device_spec);
                }
        }
-       for (unsigned card_index = 0; card_index < num_ffmpeg_inputs; ++card_index) {
-               const DeviceSpec device_spec{InputSourceType::FFMPEG_VIDEO_INPUT, card_index};
-               AudioDevice *device = find_audio_device(device_spec);
-               if (device->interesting_channels != interesting_channels[device_spec]) {
-                       device->interesting_channels = interesting_channels[device_spec];
-                       reset_resampler_mutex_held(device_spec);
-               }
-       }
 
        input_mapping = new_input_mapping;
 }
@@ -1285,10 +1257,37 @@ bool AudioMixer::is_mono(unsigned bus_index)
                return true;
        } else {
                assert(bus.device.type == InputSourceType::CAPTURE_CARD ||
-                      bus.device.type == InputSourceType::ALSA_INPUT ||
-                      bus.device.type == InputSourceType::FFMPEG_VIDEO_INPUT);
+                      bus.device.type == InputSourceType::ALSA_INPUT);
                return bus.source_channel[0] == bus.source_channel[1];
        }
 }
 
+// This is perhaps not the most user-friendly output, but it's at least better
+// than the raw index. It would be nice to have it identical to
+// Mixer::description_for_card for capture cards, though.
+string AudioMixer::spec_to_string(DeviceSpec device_spec) const
+{
+       char buf[256];
+
+       switch (device_spec.type) {
+               case InputSourceType::SILENCE:
+                       return "<silence>";
+               case InputSourceType::CAPTURE_CARD: {
+                       const AudioDevice *device = find_audio_device(device_spec);
+                       if (device->card_type == CardType::FFMPEG_INPUT) {
+                               snprintf(buf, sizeof(buf), "Virtual capture card %u (%s)", device_spec.index, device->display_name.c_str());
+                       } else {
+                               snprintf(buf, sizeof(buf), "Capture card %u (%s)", device_spec.index, device->display_name.c_str());
+                       }
+                       return buf;
+               }
+               case InputSourceType::ALSA_INPUT:
+                       snprintf(buf, sizeof(buf), "ALSA input %u", device_spec.index);
+                       return buf;
+               default:
+                       assert(false);
+       }
+}
+
+
 AudioMixer *global_audio_mixer = nullptr;
index 14e7e85d098065aabc5cd39d70ed5b31a8713041..a1d81e200b3a7fefd3e569229295e8667c1ac0e5 100644 (file)
@@ -22,6 +22,7 @@
 #include <vector>
 
 #include "alsa_pool.h"
+#include "card_type.h"
 #include "correlation_measurer.h"
 #include "decibel.h"
 #include "defs.h"
@@ -50,7 +51,7 @@ enum EQBand {
 
 class AudioMixer {
 public:
-       AudioMixer(unsigned num_capture_cards, unsigned num_ffmpeg_inputs);
+       AudioMixer();
        void reset_resampler(DeviceSpec device_spec);
        void reset_meters();
 
@@ -95,7 +96,9 @@ public:
                return DeviceSpec{InputSourceType::ALSA_INPUT, dead_card_index};
        }
 
-       void set_display_name(DeviceSpec device_spec, const std::string &name);
+       // NOTE: The display name is not overridden if active == false.
+       void set_device_parameters(DeviceSpec device_spec, const std::string &display_name, CardType card_type, unsigned num_channels, bool active);
+       bool get_active(DeviceSpec device_spec);
 
        // Note: The card should be held (currently this isn't enforced, though).
        void serialize_device(DeviceSpec device_spec, DeviceSpecProto *device_spec_proto);
@@ -324,6 +327,9 @@ private:
                // Which channels we consider interesting (ie., are part of some input_mapping).
                std::set<unsigned> interesting_channels;
                bool silenced = false;
+               CardType card_type;
+               unsigned num_channels = 2;  // Ignored for ALSA cards, which check the device directly.
+               bool active = false;  // Only really relevant for capture cards (not ALSA cards).
        };
 
        const AudioDevice *find_audio_device(DeviceSpec device_spec) const
@@ -343,15 +349,13 @@ private:
        void send_audio_level_callback();
        std::vector<DeviceSpec> get_active_devices() const;
        void set_input_mapping_lock_held(const InputMapping &input_mapping);
-
-       unsigned num_capture_cards, num_ffmpeg_inputs;
+       std::string spec_to_string(DeviceSpec device_spec) const;
 
        mutable std::timed_mutex audio_mutex;
 
        ALSAPool alsa_pool;
        AudioDevice video_cards[MAX_VIDEO_CARDS];  // Under audio_mutex.
        AudioDevice alsa_inputs[MAX_ALSA_CARDS];  // Under audio_mutex.
-       std::unique_ptr<AudioDevice[]> ffmpeg_inputs;  // Under audio_mutex.
 
        std::atomic<float> locut_cutoff_hz{120};
        StereoFilter locut[MAX_BUSES];  // Default cutoff 120 Hz, 24 dB/oct.
index 98bc4e987d346d84bb3cba89fde5a682f1119998..3f1ed7c1552b7251e975778d0658054a3e966a48 100644 (file)
@@ -101,7 +101,7 @@ void init_mapping(AudioMixer *mixer)
 
 void do_test(const char *filename)
 {
-       AudioMixer mixer(NUM_BENCHMARK_CARDS, 0);
+       AudioMixer mixer;
        mixer.set_audio_level_callback(callback);
        init_mapping(&mixer);
 
@@ -140,7 +140,7 @@ void do_test(const char *filename)
 
 void do_benchmark()
 {
-       AudioMixer mixer(NUM_BENCHMARK_CARDS, 0);
+       AudioMixer mixer;
        mixer.set_audio_level_callback(callback);
        init_mapping(&mixer);
 
diff --git a/nageru/card_type.h b/nageru/card_type.h
new file mode 100644 (file)
index 0000000..bbbf520
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef _CARD_TYPE_H
+#define _CARD_TYPE_H 1
+
+enum class CardType {
+       LIVE_CARD,
+       FAKE_CAPTURE,
+       FFMPEG_INPUT,
+       CEF_INPUT,
+};
+
+#endif  // !defined(_CARD_TYPE_H)
index 790de9412a610b392b3b837734ffd6d646a774ff..ee666e251a78c501a8f0a62a4839dd0da5752c97 100644 (file)
@@ -22,8 +22,7 @@ void fill_hdmi_sdi_output_device_menu(QMenu *menu)
        QObject::connect(none_action, &QAction::triggered, []{ global_mixer->set_output_card(-1); });
        menu->addAction(none_action);
 
-       unsigned num_cards = global_mixer->get_num_cards();
-       for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                if (!global_mixer->card_can_be_used_as_output(card_index)) {
                        continue;
                }
index 9c58ad9c0d7bf4aa0d0bfebe46e81a3b8b29b191..402e33d7eca7a0be7d05d6ae87a4025f78ad9d0f 100644 (file)
@@ -15,6 +15,7 @@ Flags global_flags;
 enum LongOption {
        OPTION_HELP = 1000,
        OPTION_FULLSCREEN,
+       OPTION_MAX_NUM_CARDS,
        OPTION_MULTICHANNEL,
        OPTION_MIDI_MAPPING,
        OPTION_DEFAULT_HDMI_INPUT,
@@ -93,13 +94,13 @@ map<unsigned, unsigned> parse_mjpeg_export_cards(char *optarg)
                        fprintf(stderr, "ERROR: Invalid range %u-%u in --mjpeg-export-cards=\n", range_begin, range_end);
                        exit(1);
                }
-               if (range_end >= unsigned(global_flags.num_cards)) {
+               if (range_end >= unsigned(global_flags.max_num_cards)) {
                        // There are situations where we could possibly want to
                        // include FFmpeg inputs (CEF inputs are unlikely),
                        // but they're not necessarily in 4:2:2 Y'CbCr, so it would
                        // require more functionality the the JPEG encoder.
                        fprintf(stderr, "ERROR: Asked for (zero-indexed) card %u in --mjpeg-export-cards=, but there are only %u cards\n",
-                               range_end, global_flags.num_cards);
+                               range_end, global_flags.max_num_cards);
                        exit(1);
                }
                for (unsigned card_idx = range_begin; card_idx <= range_end; ++card_idx) {
@@ -133,7 +134,8 @@ void usage(Program program)
        fprintf(stderr, "  -w, --width                     output width in pixels (default 1280)\n");
        fprintf(stderr, "  -h, --height                    output height in pixels (default 720)\n");
        if (program == PROGRAM_NAGERU) {
-               fprintf(stderr, "  -c, --num-cards                 set number of input cards (default 2)\n");
+               fprintf(stderr, "  -c, --num-cards                 set minimum number of input cards (default 2)\n");
+               fprintf(stderr, "      --max-num-cards             set maximum number of input cards (default %d)\n", MAX_VIDEO_CARDS);
                fprintf(stderr, "  -o, --output-card=CARD          also output signal to the given card (default none)\n");
                fprintf(stderr, "  -t, --theme=FILE                choose theme (default theme.lua)\n");
                fprintf(stderr, "  -I, --theme-dir=DIR             search for theme in this directory (can be given multiple times)\n");
@@ -233,6 +235,7 @@ void parse_flags(Program program, int argc, char * const argv[])
                { "width", required_argument, 0, 'w' },
                { "height", required_argument, 0, 'h' },
                { "num-cards", required_argument, 0, 'c' },
+               { "max-num-cards", required_argument, 0, OPTION_MAX_NUM_CARDS },
                { "output-card", required_argument, 0, 'o' },
                { "theme", required_argument, 0, 't' },
                { "theme-dir", required_argument, 0, 'I' },
@@ -312,7 +315,10 @@ void parse_flags(Program program, int argc, char * const argv[])
                        global_flags.height = atoi(optarg);
                        break;
                case 'c':
-                       global_flags.num_cards = atoi(optarg);
+                       global_flags.min_num_cards = atoi(optarg);
+                       break;
+               case OPTION_MAX_NUM_CARDS:
+                       global_flags.max_num_cards = atoi(optarg);
                        break;
                case 'o':
                        global_flags.output_card = atoi(optarg);
@@ -587,12 +593,20 @@ void parse_flags(Program program, int argc, char * const argv[])
                fprintf(stderr, "ERROR: --http-uncompressed-video and --http-x264-video are mutually incompatible\n");
                exit(1);
        }
-       if (global_flags.num_cards <= 0) {
+       if (global_flags.min_num_cards <= 0) {
                fprintf(stderr, "ERROR: --num-cards must be at least 1\n");
                exit(1);
        }
+       if (global_flags.max_num_cards <= 0) {
+               fprintf(stderr, "ERROR: --max-num-cards must be at least 1\n");
+               exit(1);
+       }
+       if (global_flags.max_num_cards < global_flags.min_num_cards) {
+               fprintf(stderr, "ERROR: --max-num-cards can not be lower than --num-cards\n");
+               exit(1);
+       }
        if (global_flags.output_card < -1 ||
-           global_flags.output_card >= global_flags.num_cards) {
+           global_flags.output_card >= global_flags.max_num_cards) {
                fprintf(stderr, "ERROR: --output-card points to a nonexistant card\n");
                exit(1);
        }
@@ -624,8 +638,8 @@ void parse_flags(Program program, int argc, char * const argv[])
        }
 
        for (pair<int, int> mapping : global_flags.default_stream_mapping) {
-               if (mapping.second >= global_flags.num_cards) {
-                       fprintf(stderr, "ERROR: Signal %d mapped to card %d, which doesn't exist (try adjusting --num-cards)\n",
+               if (mapping.second >= global_flags.max_num_cards) {
+                       fprintf(stderr, "ERROR: Signal %d mapped to card %d, which doesn't exist (try adjusting --max-num-cards)\n",
                                mapping.first, mapping.second);
                        exit(1);
                }
@@ -694,7 +708,7 @@ void parse_flags(Program program, int argc, char * const argv[])
 
        if (!card_to_mjpeg_stream_export_set) {
                // Fill in the default mapping (export all cards, in order).
-               for (unsigned card_idx = 0; card_idx < unsigned(global_flags.num_cards); ++card_idx) {
+               for (unsigned card_idx = 0; card_idx < unsigned(global_flags.max_num_cards); ++card_idx) {
                        global_flags.card_to_mjpeg_stream_export[card_idx] = card_idx;
                }
        }
index c1fcdff01f4b60731162c3a7b7c6452f09b6aa4e..ad812b938431ba778286af7997245594fdda9444 100644 (file)
@@ -12,7 +12,8 @@
 
 struct Flags {
        int width = 1280, height = 720;
-       int num_cards = 2;
+       int min_num_cards = 2;
+       int max_num_cards = MAX_VIDEO_CARDS;
        std::string va_display;
        bool fake_cards_audio = false;
        bool uncompressed_video_to_http = false;
index 62aeebc9cc14a0a7de7ca47a2be03076d1d79537..f5f350a6865188194f7bf58c7a778521cd0dad92 100644 (file)
@@ -88,7 +88,7 @@ void GLWidget::initializeGL()
 {
        static once_flag flag;
        call_once(flag, [this]{
-               global_mixer = new Mixer(QGLFormat::toSurfaceFormat(format()), global_flags.num_cards);
+               global_mixer = new Mixer(QGLFormat::toSurfaceFormat(format()));
                global_audio_mixer = global_mixer->get_audio_mixer();
                global_mainwindow->mixer_created(global_mixer);
                global_mixer->start();
@@ -227,12 +227,13 @@ void GLWidget::show_preview_context_menu(unsigned signal_num, const QPoint &pos)
        QMenu mode_submenu;
        QActionGroup mode_group(&mode_submenu);
 
-       unsigned num_cards = global_mixer->get_num_cards();
        unsigned current_card = global_mixer->map_signal_to_card(signal_num);
        bool is_ffmpeg = global_mixer->card_is_ffmpeg(current_card);
 
        if (!is_ffmpeg) {  // FFmpeg inputs are not connected to any card; they're locked to a given input and have a given Y'CbCr interpretatio and have a given Y'CbCr interpretation.
-               for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+               for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
+                       if (!global_mixer->card_is_active(card_index)) continue;
+                       if (global_mixer->card_is_cef(card_index) || global_mixer->card_is_ffmpeg(card_index)) continue;
                        QString description(QString::fromStdString(global_mixer->get_card_description(card_index)));
                        QAction *action = new QAction(description, &card_group);
                        action->setCheckable(true);
index dfcc97ce5e6613cd8c51f12d55a1be49433daf5f..cad723a7a92b9c8dee18809e4525b9011d2ade30 100644 (file)
 using namespace std;
 using namespace google::protobuf;
 
-string spec_to_string(DeviceSpec device_spec)
-{
-       char buf[256];
-
-       switch (device_spec.type) {
-       case InputSourceType::SILENCE:
-               return "<silence>";
-       case InputSourceType::CAPTURE_CARD:
-               snprintf(buf, sizeof(buf), "Capture card %u", device_spec.index);
-               return buf;
-       case InputSourceType::ALSA_INPUT:
-               snprintf(buf, sizeof(buf), "ALSA input %u", device_spec.index);
-               return buf;
-       case InputSourceType::FFMPEG_VIDEO_INPUT:
-               snprintf(buf, sizeof(buf), "FFmpeg input %u", device_spec.index);
-               return buf;
-       default:
-               assert(false);
-       }
-}
-
 bool save_input_mapping_to_file(const map<DeviceSpec, DeviceInfo> &devices, const InputMapping &input_mapping, const string &filename)
 {
        InputMappingProto mapping_proto;
@@ -83,12 +62,10 @@ bool load_input_mapping_from_file(const map<DeviceSpec, DeviceInfo> &devices, co
                case DeviceSpecProto::SILENCE:
                        device_mapping.push_back(DeviceSpec{InputSourceType::SILENCE, 0});
                        break;
-               case DeviceSpecProto::FFMPEG_VIDEO_INPUT:
                case DeviceSpecProto::CAPTURE_CARD: {
                        // First see if there's a card that matches on both index and name.
                        DeviceSpec spec;
-                       spec.type = (device_proto.type() == DeviceSpecProto::CAPTURE_CARD) ?
-                               InputSourceType::CAPTURE_CARD : InputSourceType::FFMPEG_VIDEO_INPUT;
+                       spec.type = InputSourceType::CAPTURE_CARD;
                        spec.index = unsigned(device_proto.index());
                        assert(devices.count(spec));
 
index 67af0f489ee3aaf465b8c6c32aa554796c788cf1..10c3ca2901be0afe870ec9cbefc40ec9c31e0a60 100644 (file)
@@ -6,7 +6,7 @@
 #include <string>
 #include <vector>
 
-enum class InputSourceType { SILENCE, CAPTURE_CARD, ALSA_INPUT, FFMPEG_VIDEO_INPUT };
+enum class InputSourceType { SILENCE, CAPTURE_CARD, ALSA_INPUT };
 struct DeviceSpec {
        InputSourceType type;
        unsigned index;
@@ -47,10 +47,6 @@ struct InputMapping {
        std::vector<Bus> buses;
 };
 
-// This is perhaps not the most user-friendly output, but it's at least better
-// than the raw index.
-std::string spec_to_string(DeviceSpec device_spec);
-
 bool save_input_mapping_to_file(const std::map<DeviceSpec, DeviceInfo> &devices,
                                 const InputMapping &mapping,
                                 const std::string &filename);
index b4565152a4af01b290941025633e7224b8c4d4dd..831d4873468f488283b6e8d0546982264606efd4 100644 (file)
 using namespace std;
 using namespace std::placeholders;
 
+namespace {
+
+bool uses_device(const InputMapping &mapping, DeviceSpec device)
+{
+       for (const InputMapping::Bus &bus : mapping.buses) {
+               if (bus.device == device) {
+                       return true;
+               }
+       }
+       return false;
+}
+
+}  // namespace
+
 InputMappingDialog::InputMappingDialog()
        : ui(new Ui::InputMappingDialog),
          mapping(global_audio_mixer->get_input_mapping()),
@@ -113,6 +127,15 @@ void InputMappingDialog::fill_row_from_bus(unsigned row, const InputMapping::Bus
                        } else if (state == ALSAPool::Device::State::DEAD) {
                                label += " (dead)";
                        }
+               } else if (!global_audio_mixer->get_active(spec_and_info.first)) {
+                       // Should nominally be skipped, but if we used it before it went away,
+                       // we'll need to allow the user to still see it.
+                       if (uses_device(mapping, spec_and_info.first) ||
+                           uses_device(old_mapping, spec_and_info.first)) {
+                               label += " (dead)";
+                       } else {
+                               continue;
+                       }
                }
                ++current_index;
                if (unsigned(card_combo->count()) > current_index) {
@@ -148,8 +171,7 @@ void InputMappingDialog::setup_channel_choices_from_bus(unsigned row, const Inpu
                QComboBox *channel_combo = new QComboBox;
                channel_combo->addItem(QString("(none)"));
                if (bus.device.type == InputSourceType::CAPTURE_CARD ||
-                   bus.device.type == InputSourceType::ALSA_INPUT ||
-                   bus.device.type == InputSourceType::FFMPEG_VIDEO_INPUT) {
+                   bus.device.type == InputSourceType::ALSA_INPUT) {
                        auto device_it = devices.find(bus.device);
                        assert(device_it != devices.end());
                        unsigned num_device_channels = device_it->second.num_channels;
index cf14bf35d404f7f117798bc8c6d69c3500ef2419..e4fba7f03630e399f0000c5b42c2ef2e837754f6 100644 (file)
@@ -181,7 +181,7 @@ int main(int argc, char *argv[])
                usage(PROGRAM_KAERU);
                abort();
        }
-       global_flags.num_cards = 1;  // For latency metrics.
+       global_flags.max_num_cards = 1;  // For latency metrics.
 
 #if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
        av_register_all();
index 3f5119a43d74c7de00a58d1152290315837e77b8..2911dae1d19cc553846fbe66b8871b7c4dd6673c 100644 (file)
@@ -312,9 +312,8 @@ void QueueLengthPolicy::update_policy(steady_clock::time_point now,
        metric_input_queue_safe_length_frames = safe_queue_length;
 }
 
-Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
+Mixer::Mixer(const QSurfaceFormat &format)
        : httpd(),
-         num_cards(num_cards),
          mixer_surface(create_surface(format)),
          h264_encoder_surface(create_surface(format)),
          decklink_output_surface(create_surface(format)),
@@ -381,11 +380,11 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        }
 
        // 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));
+       theme.reset(new Theme(global_flags.theme_filename, global_flags.theme_dirs, resource_pool.get()));
 
        // Must be instantiated after the theme, as the theme decides the number of FFmpeg inputs.
        std::vector<FFmpegCapture *> video_inputs = theme->get_video_inputs();
-       audio_mixer.reset(new AudioMixer(num_cards, video_inputs.size()));
+       audio_mixer.reset(new AudioMixer);
 
        httpd.add_endpoint("/channels", bind(&Mixer::get_channels_json, this), HTTPD::ALLOW_ALL_ORIGINS);
        for (int channel_idx = 0; channel_idx < theme->get_num_channels(); ++channel_idx) {
@@ -405,7 +404,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        {
                IDeckLinkIterator *decklink_iterator = CreateDeckLinkIteratorInstance();
                if (decklink_iterator != nullptr) {
-                       for ( ; card_index < num_cards; ++card_index) {
+                       for ( ; card_index < unsigned(global_flags.max_num_cards); ++card_index) {
                                IDeckLink *decklink;
                                if (decklink_iterator->Next(&decklink) != S_OK) {
                                        break;
@@ -417,7 +416,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
                                        delete output;
                                        output = nullptr;
                                }
-                               configure_card(card_index, capture, CardType::LIVE_CARD, output);
+                               configure_card(card_index, capture, CardType::LIVE_CARD, output, /*is_srt_card=*/false);
                                ++num_pci_devices;
                        }
                        decklink_iterator->Release();
@@ -428,31 +427,44 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        }
 
        unsigned num_usb_devices = BMUSBCapture::num_cards();
-       for (unsigned usb_card_index = 0; usb_card_index < num_usb_devices && card_index < num_cards; ++usb_card_index, ++card_index) {
+       for (unsigned usb_card_index = 0; usb_card_index < num_usb_devices && card_index < unsigned(global_flags.max_num_cards); ++usb_card_index, ++card_index) {
                BMUSBCapture *capture = new BMUSBCapture(usb_card_index);
                capture->set_card_disconnected_callback(bind(&Mixer::bm_hotplug_remove, this, card_index));
-               configure_card(card_index, capture, CardType::LIVE_CARD, /*output=*/nullptr);
+               configure_card(card_index, capture, CardType::LIVE_CARD, /*output=*/nullptr, /*is_srt_card=*/false);
        }
        fprintf(stderr, "Found %u USB card(s).\n", num_usb_devices);
 
+       // Fill up with fake cards for as long as we can, so that the FFmpeg
+       // and HTML cards always come last.
        unsigned num_fake_cards = 0;
-       for ( ; card_index < num_cards; ++card_index, ++num_fake_cards) {
-               FakeCapture *capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
-               configure_card(card_index, capture, CardType::FAKE_CAPTURE, /*output=*/nullptr);
+#ifdef HAVE_CEF
+       size_t num_html_inputs = theme->get_html_inputs().size();
+#else
+       size_t num_html_inputs = 0;
+#endif
+       for ( ; card_index < MAX_VIDEO_CARDS - video_inputs.size() - num_html_inputs; ++card_index) {
+               // Only bother to activate fake capture cards to satisfy the minimum.
+               bool is_active = card_index < unsigned(global_flags.min_num_cards) || cards[card_index].force_active;
+               if (is_active) {
+                       FakeCapture *capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
+                       configure_card(card_index, capture, CardType::FAKE_CAPTURE, /*output=*/nullptr, /*is_srt_card=*/false);
+                       ++num_fake_cards;
+               } else {
+                       configure_card(card_index, nullptr, CardType::FAKE_CAPTURE, /*output=*/nullptr, /*is_srt_card=*/false);
+               }
        }
 
        if (num_fake_cards > 0) {
                fprintf(stderr, "Initialized %u fake cards.\n", num_fake_cards);
        }
 
-       // Initialize all video inputs the theme asked for. Note that these are
-       // all put _after_ the regular cards, which stop at <num_cards> - 1.
+       // Initialize all video inputs the theme asked for.
        for (unsigned video_card_index = 0; video_card_index < video_inputs.size(); ++card_index, ++video_card_index) {
                if (card_index >= MAX_VIDEO_CARDS) {
                        fprintf(stderr, "ERROR: Not enough card slots available for the videos the theme requested.\n");
                        abort();
                }
-               configure_card(card_index, video_inputs[video_card_index], CardType::FFMPEG_INPUT, /*output=*/nullptr);
+               configure_card(card_index, video_inputs[video_card_index], CardType::FFMPEG_INPUT, /*output=*/nullptr, /*is_srt_card=*/false);
                video_inputs[video_card_index]->set_card_index(card_index);
        }
        num_video_inputs = video_inputs.size();
@@ -465,7 +477,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
                        fprintf(stderr, "ERROR: Not enough card slots available for the HTML inputs the theme requested.\n");
                        abort();
                }
-               configure_card(card_index, html_inputs[html_card_index], CardType::CEF_INPUT, /*output=*/nullptr);
+               configure_card(card_index, html_inputs[html_card_index], CardType::CEF_INPUT, /*output=*/nullptr, /*is_srt_card=*/false);
                html_inputs[html_card_index]->set_card_index(card_index);
        }
        num_html_inputs = html_inputs.size();
@@ -480,7 +492,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        }
 #endif
 
-       for (unsigned card_index = 0; card_index < num_cards + num_video_inputs + num_html_inputs; ++card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                cards[card_index].queue_length_policy.reset(card_index);
        }
 
@@ -538,8 +550,10 @@ Mixer::~Mixer()
        httpd.stop();
        BMUSBCapture::stop_bm_thread();
 
-       for (unsigned card_index = 0; card_index < num_cards + num_video_inputs + num_html_inputs; ++card_index) {
-               cards[card_index].capture->stop_dequeue_thread();
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
+               if (cards[card_index].capture != nullptr) {  // Active.
+                       cards[card_index].capture->stop_dequeue_thread();
+               }
                if (cards[card_index].output) {
                        cards[card_index].output->end_output();
                        cards[card_index].output.reset();
@@ -551,7 +565,12 @@ Mixer::~Mixer()
 
 void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, CardType card_type, DeckLinkOutput *output, bool is_srt_card)
 {
-       printf("Configuring card %d...\n", card_index);
+       bool is_active = capture != nullptr;
+       if (is_active) {
+               printf("Configuring card %d...\n", card_index);
+       } else {
+               assert(card_type == CardType::FAKE_CAPTURE);
+       }
 
        CaptureCard *card = &cards[card_index];
        if (card->capture != nullptr) {
@@ -580,42 +599,48 @@ void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, CardT
                pixel_format = PixelFormat_8BitYCbCr;
        }
 
-       card->capture->set_frame_callback(bind(&Mixer::bm_frame, this, card_index, _1, _2, _3, _4, _5, _6, _7));
-       if (card->frame_allocator == nullptr) {
-               card->frame_allocator.reset(new PBOFrameAllocator(pixel_format, 8 << 20, global_flags.width, global_flags.height, card_index, mjpeg_encoder.get()));  // 8 MB.
-       } else {
-               // The format could have changed, but we cannot reset the allocator
-               // and create a new one from scratch, since there may be allocated
-               // frames from it that expect to call release_frame() on it.
-               // Instead, ask the allocator to create new frames for us and discard
-               // any old ones as they come back. This takes the mutex while
-               // allocating, but nothing should really be sending frames in there
-               // right now anyway (start_bm_capture() has not been called yet).
-               card->frame_allocator->reconfigure(pixel_format, 8 << 20, global_flags.width, global_flags.height, card_index, mjpeg_encoder.get());
-       }
-       card->capture->set_video_frame_allocator(card->frame_allocator.get());
-       if (card->surface == nullptr) {
-               card->surface = create_surface_with_same_format(mixer_surface);
-       }
-       while (!card->new_frames.empty()) card->new_frames.pop_front();
-       card->last_timecode = -1;
-       card->capture->set_pixel_format(pixel_format);
-       card->capture->configure_card();
-
-       // NOTE: start_bm_capture() happens in thread_func().
+       if (is_active) {
+               card->capture->set_frame_callback(bind(&Mixer::bm_frame, this, card_index, _1, _2, _3, _4, _5, _6, _7));
+               if (card->frame_allocator == nullptr) {
+                       card->frame_allocator.reset(new PBOFrameAllocator(pixel_format, 8 << 20, global_flags.width, global_flags.height, card_index, mjpeg_encoder.get()));  // 8 MB.
+               } else {
+                       // The format could have changed, but we cannot reset the allocator
+                       // and create a new one from scratch, since there may be allocated
+                       // frames from it that expect to call release_frame() on it.
+                       // Instead, ask the allocator to create new frames for us and discard
+                       // any old ones as they come back. This takes the mutex while
+                       // allocating, but nothing should really be sending frames in there
+                       // right now anyway (start_bm_capture() has not been called yet).
+                       card->frame_allocator->reconfigure(pixel_format, 8 << 20, global_flags.width, global_flags.height, card_index, mjpeg_encoder.get());
+               }
+               card->capture->set_video_frame_allocator(card->frame_allocator.get());
+               if (card->surface == nullptr) {
+                       card->surface = create_surface_with_same_format(mixer_surface);
+               }
+               while (!card->new_frames.empty()) card->new_frames.pop_front();
+               card->last_timecode = -1;
+               card->capture->set_pixel_format(pixel_format);
+               card->capture->configure_card();
+
+               // NOTE: start_bm_capture() happens in thread_func().
+       }
 
        if (is_srt_card) {
                assert(card_type == CardType::FFMPEG_INPUT);
        }
 
        DeviceSpec device;
-       if (card_type == CardType::FFMPEG_INPUT && !is_srt_card) {
-               device = DeviceSpec{InputSourceType::FFMPEG_VIDEO_INPUT, card_index - num_cards};
+       device = DeviceSpec{InputSourceType::CAPTURE_CARD, card_index};
+       audio_mixer->reset_resampler(device);
+       unsigned num_channels = card_type == CardType::LIVE_CARD ? 8 : 2;
+       if (is_active) {
+               audio_mixer->set_device_parameters(device, card->capture->get_description(), card_type, num_channels, /*active=*/true);
        } else {
-               device = DeviceSpec{InputSourceType::CAPTURE_CARD, card_index};
+               // Note: Keeps the previous name, if any.
+               char name[32];
+               snprintf(name, sizeof(name), "Fake card %u", card_index + 1);
+               audio_mixer->set_device_parameters(device, name, card_type, num_channels, /*active=*/false);
        }
-       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.
@@ -706,117 +731,121 @@ void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, CardT
                global_metrics.remove_if_exists("srt_receiver_delivery_delay_seconds", 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);
+       if (is_active) {
+               // 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:
-               if (is_srt_card) {
-                       labels.emplace_back("cardtype", "srt");
-               } else {
-                       labels.emplace_back("cardtype", "ffmpeg");
+               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:
+                       if (is_srt_card) {
+                               labels.emplace_back("cardtype", "srt");
+                       } else {
+                               labels.emplace_back("cardtype", "ffmpeg");
+                       }
+                       break;
+               case CardType::CEF_INPUT:
+                       labels.emplace_back("cardtype", "cef");
+                       break;
+               default:
+                       assert(false);
                }
-               break;
-       case CardType::CEF_INPUT:
-               labels.emplace_back("cardtype", "cef");
-               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);
-
-       if (is_srt_card) {
-               // Global measurements (counters).
-               global_metrics.add("srt_uptime_seconds", labels, &card->metric_srt_uptime_seconds);
-               global_metrics.add("srt_send_duration_seconds", labels, &card->metric_srt_send_duration_seconds);
-               global_metrics.add("srt_sent_bytes", labels, &card->metric_srt_sent_bytes);
-               global_metrics.add("srt_received_bytes", labels, &card->metric_srt_received_bytes);
-
-               vector<pair<string, string>> packet_labels = labels;
-               packet_labels.emplace_back("type", "normal");
-               global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_normal);
-               global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_normal);
-
-               packet_labels.back().second = "lost";
-               global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_lost);
-               global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_lost);
-
-               packet_labels.back().second = "retransmitted";
-               global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_retransmitted);
-               global_metrics.add("srt_sent_bytes", packet_labels, &card->metric_srt_sent_bytes_retransmitted);
-
-               packet_labels.back().second = "ack";
-               global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_ack);
-               global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_ack);
+               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);
 
-               packet_labels.back().second = "nak";
-               global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_nak);
-               global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_nak);
-
-               packet_labels.back().second = "dropped";
-               global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_dropped);
-               global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_dropped);
-               global_metrics.add("srt_sent_bytes", packet_labels, &card->metric_srt_sent_bytes_dropped);
-               global_metrics.add("srt_received_bytes", packet_labels, &card->metric_srt_received_bytes_dropped);
-
-               packet_labels.back().second = "undecryptable";
-               global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_undecryptable);
-               global_metrics.add("srt_received_bytes", packet_labels, &card->metric_srt_received_bytes_undecryptable);
-
-               global_metrics.add("srt_filter_sent_extra_packets", labels, &card->metric_srt_filter_sent_packets);
-               global_metrics.add("srt_filter_received_extra_packets", labels, &card->metric_srt_filter_received_extra_packets);
-               global_metrics.add("srt_filter_received_rebuilt_packets", labels, &card->metric_srt_filter_received_rebuilt_packets);
-               global_metrics.add("srt_filter_received_lost_packets", labels, &card->metric_srt_filter_received_lost_packets);
+               if (is_srt_card) {
+                       // Global measurements (counters).
+                       global_metrics.add("srt_uptime_seconds", labels, &card->metric_srt_uptime_seconds);
+                       global_metrics.add("srt_send_duration_seconds", labels, &card->metric_srt_send_duration_seconds);
+                       global_metrics.add("srt_sent_bytes", labels, &card->metric_srt_sent_bytes);
+                       global_metrics.add("srt_received_bytes", labels, &card->metric_srt_received_bytes);
+
+                       vector<pair<string, string>> packet_labels = labels;
+                       packet_labels.emplace_back("type", "normal");
+                       global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_normal);
+                       global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_normal);
+
+                       packet_labels.back().second = "lost";
+                       global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_lost);
+                       global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_lost);
+
+                       packet_labels.back().second = "retransmitted";
+                       global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_retransmitted);
+                       global_metrics.add("srt_sent_bytes", packet_labels, &card->metric_srt_sent_bytes_retransmitted);
+
+                       packet_labels.back().second = "ack";
+                       global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_ack);
+                       global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_ack);
+
+                       packet_labels.back().second = "nak";
+                       global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_nak);
+                       global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_nak);
+
+                       packet_labels.back().second = "dropped";
+                       global_metrics.add("srt_sent_packets", packet_labels, &card->metric_srt_sent_packets_dropped);
+                       global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_dropped);
+                       global_metrics.add("srt_sent_bytes", packet_labels, &card->metric_srt_sent_bytes_dropped);
+                       global_metrics.add("srt_received_bytes", packet_labels, &card->metric_srt_received_bytes_dropped);
+
+                       packet_labels.back().second = "undecryptable";
+                       global_metrics.add("srt_received_packets", packet_labels, &card->metric_srt_received_packets_undecryptable);
+                       global_metrics.add("srt_received_bytes", packet_labels, &card->metric_srt_received_bytes_undecryptable);
+
+                       global_metrics.add("srt_filter_sent_extra_packets", labels, &card->metric_srt_filter_sent_packets);
+                       global_metrics.add("srt_filter_received_extra_packets", labels, &card->metric_srt_filter_received_extra_packets);
+                       global_metrics.add("srt_filter_received_rebuilt_packets", labels, &card->metric_srt_filter_received_rebuilt_packets);
+                       global_metrics.add("srt_filter_received_lost_packets", labels, &card->metric_srt_filter_received_lost_packets);
+
+                       // Instant measurements (gauges).
+                       global_metrics.add("srt_packet_sending_period_seconds", labels, &card->metric_srt_packet_sending_period_seconds, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_flow_window_packets", labels, &card->metric_srt_flow_window_packets, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_congestion_window_packets", labels, &card->metric_srt_congestion_window_packets, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_flight_size_packets", labels, &card->metric_srt_flight_size_packets, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_rtt_seconds", labels, &card->metric_srt_rtt_seconds, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_estimated_bandwidth_bits_per_second", labels, &card->metric_srt_estimated_bandwidth_bits_per_second, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_bandwidth_ceiling_bits_per_second", labels, &card->metric_srt_bandwidth_ceiling_bits_per_second, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_send_buffer_available_bytes", labels, &card->metric_srt_send_buffer_available_bytes, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_receive_buffer_available_bytes", labels, &card->metric_srt_receive_buffer_available_bytes, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_mss_bytes", labels, &card->metric_srt_mss_bytes, Metrics::TYPE_GAUGE);
+
+                       global_metrics.add("srt_sender_unacked_packets", labels, &card->metric_srt_sender_unacked_packets, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_sender_unacked_bytes", labels, &card->metric_srt_sender_unacked_bytes, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_sender_unacked_timespan_seconds", labels, &card->metric_srt_sender_unacked_timespan_seconds, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_sender_delivery_delay_seconds", labels, &card->metric_srt_sender_delivery_delay_seconds, Metrics::TYPE_GAUGE);
+
+                       global_metrics.add("srt_receiver_unacked_packets", labels, &card->metric_srt_receiver_unacked_packets, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_receiver_unacked_bytes", labels, &card->metric_srt_receiver_unacked_bytes, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_receiver_unacked_timespan_seconds", labels, &card->metric_srt_receiver_unacked_timespan_seconds, Metrics::TYPE_GAUGE);
+                       global_metrics.add("srt_receiver_delivery_delay_seconds", labels, &card->metric_srt_receiver_delivery_delay_seconds, Metrics::TYPE_GAUGE);
+               }
 
-               // Instant measurements (gauges).
-               global_metrics.add("srt_packet_sending_period_seconds", labels, &card->metric_srt_packet_sending_period_seconds, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_flow_window_packets", labels, &card->metric_srt_flow_window_packets, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_congestion_window_packets", labels, &card->metric_srt_congestion_window_packets, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_flight_size_packets", labels, &card->metric_srt_flight_size_packets, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_rtt_seconds", labels, &card->metric_srt_rtt_seconds, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_estimated_bandwidth_bits_per_second", labels, &card->metric_srt_estimated_bandwidth_bits_per_second, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_bandwidth_ceiling_bits_per_second", labels, &card->metric_srt_bandwidth_ceiling_bits_per_second, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_send_buffer_available_bytes", labels, &card->metric_srt_send_buffer_available_bytes, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_receive_buffer_available_bytes", labels, &card->metric_srt_receive_buffer_available_bytes, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_mss_bytes", labels, &card->metric_srt_mss_bytes, Metrics::TYPE_GAUGE);
-
-               global_metrics.add("srt_sender_unacked_packets", labels, &card->metric_srt_sender_unacked_packets, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_sender_unacked_bytes", labels, &card->metric_srt_sender_unacked_bytes, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_sender_unacked_timespan_seconds", labels, &card->metric_srt_sender_unacked_timespan_seconds, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_sender_delivery_delay_seconds", labels, &card->metric_srt_sender_delivery_delay_seconds, Metrics::TYPE_GAUGE);
-
-               global_metrics.add("srt_receiver_unacked_packets", labels, &card->metric_srt_receiver_unacked_packets, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_receiver_unacked_bytes", labels, &card->metric_srt_receiver_unacked_bytes, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_receiver_unacked_timespan_seconds", labels, &card->metric_srt_receiver_unacked_timespan_seconds, Metrics::TYPE_GAUGE);
-               global_metrics.add("srt_receiver_delivery_delay_seconds", labels, &card->metric_srt_receiver_delivery_delay_seconds, Metrics::TYPE_GAUGE);
-       }
-
-       card->labels = labels;
+               card->labels = labels;
+       } else {
+               card->labels.clear();
+       }
 }
 
 void Mixer::set_output_card_internal(int card_index)
@@ -852,7 +881,7 @@ void Mixer::set_output_card_internal(int card_index)
                lock.lock();
                card->parked_capture = move(card->capture);
                CaptureInterface *fake_capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
-               configure_card(card_index, fake_capture, CardType::FAKE_CAPTURE, card->output.release());
+               configure_card(card_index, fake_capture, CardType::FAKE_CAPTURE, card->output.release(), /*is_srt_card=*/false);
                card->queue_length_policy.reset(card_index);
                card->capture->start_bm_capture();
                desired_output_video_mode = output_video_mode = card->output->pick_video_mode(desired_output_video_mode);
@@ -874,22 +903,13 @@ int unwrap_timecode(uint16_t current_wrapped, int last)
        }
 }
 
-DeviceSpec card_index_to_device(unsigned card_index, unsigned num_cards)
-{
-       if (card_index >= num_cards) {
-               return DeviceSpec{InputSourceType::FFMPEG_VIDEO_INPUT, card_index - num_cards};
-       } else {
-               return DeviceSpec{InputSourceType::CAPTURE_CARD, card_index};
-       }
-}
-
 }  // namespace
 
 void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
                      FrameAllocator::Frame video_frame, size_t video_offset, VideoFormat video_format,
                     FrameAllocator::Frame audio_frame, size_t audio_offset, AudioFormat audio_format)
 {
-       DeviceSpec device = card_index_to_device(card_index, num_cards);
+       DeviceSpec device{InputSourceType::CAPTURE_CARD, card_index};
        CaptureCard *card = &cards[card_index];
 
        ++card->metric_input_received_frames;
@@ -926,7 +946,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
        size_t num_samples = (audio_frame.len > audio_offset) ? (audio_frame.len - audio_offset) / audio_format.num_channels / (audio_format.bits_per_sample / 8) : 0;
        if (num_samples > OUTPUT_FREQUENCY / 10 && card->type != CardType::FFMPEG_INPUT) {
                printf("%s: Dropping frame with implausible audio length (len=%d, offset=%d) [timecode=0x%04x video_len=%d video_offset=%d video_format=%x)\n",
-                       spec_to_string(device).c_str(), int(audio_frame.len), int(audio_offset),
+                       description_for_card(card_index).c_str(), int(audio_frame.len), int(audio_offset),
                        timecode, int(video_frame.len), int(video_offset), video_format.id);
                if (video_frame.owner) {
                        video_frame.owner->release_frame(video_frame);
@@ -948,14 +968,14 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
 
        if (dropped_frames > MAX_FPS * 2) {
                fprintf(stderr, "%s lost more than two seconds (or time code jumping around; from 0x%04x to 0x%04x), resetting resampler\n",
-                       spec_to_string(device).c_str(), card->last_timecode, timecode);
+                       description_for_card(card_index).c_str(), 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, "%s dropped %d frame(s) (before timecode 0x%04x), inserting silence.\n",
-                       spec_to_string(device).c_str(), dropped_frames, timecode);
+                       description_for_card(card_index).c_str(), dropped_frames, timecode);
                card->metric_input_dropped_frames_error += dropped_frames;
 
                bool success;
@@ -1039,7 +1059,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
            video_frame.len - video_offset != expected_length) {
                if (video_frame.len != 0) {
                        printf("%s: Dropping video frame with wrong length (%zu; expected %zu)\n",
-                               spec_to_string(device).c_str(), video_frame.len - video_offset, expected_length);
+                               description_for_card(card_index).c_str(), video_frame.len - video_offset, expected_length);
                }
                if (video_frame.owner) {
                        video_frame.owner->release_frame(video_frame);
@@ -1234,8 +1254,8 @@ void Mixer::thread_func()
 
        // Start the actual capture. (We don't want to do it before we're actually ready
        // to process output frames.)
-       for (unsigned card_index = 0; card_index < num_cards + num_video_inputs + num_html_inputs; ++card_index) {
-               if (int(card_index) != output_card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
+               if (int(card_index) != output_card_index && cards[card_index].capture != nullptr) {
                        cards[card_index].capture->start_bm_capture();
                }
        }
@@ -1266,7 +1286,7 @@ void Mixer::thread_func()
                } else {
                        master_card_is_output = false;
                        master_card_index = theme->map_signal_to_card(master_clock_channel);
-                       assert(master_card_index < num_cards + num_video_inputs);
+                       assert(master_card_index < MAX_VIDEO_CARDS);
                }
 
                handle_hotplugged_cards();
@@ -1276,8 +1296,7 @@ void Mixer::thread_func()
                schedule_audio_resampling_tasks(output_frame_info.dropped_frames, output_frame_info.num_samples, output_frame_info.frame_duration, output_frame_info.is_preroll, output_frame_info.frame_timestamp);
                stats_dropped_frames += output_frame_info.dropped_frames;
 
-               for (unsigned card_index = 0; card_index < num_cards + num_video_inputs + num_html_inputs; ++card_index) {
-                       DeviceSpec device = card_index_to_device(card_index, num_cards);
+               for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                        if (card_index == master_card_index || !has_new_frame[card_index]) {
                                continue;
                        }
@@ -1286,7 +1305,7 @@ void Mixer::thread_func()
                        }
                        if (new_frames[card_index].dropped_frames > 0) {
                                printf("%s dropped %d frames before this\n",
-                                       spec_to_string(device).c_str(), int(new_frames[card_index].dropped_frames));
+                                       description_for_card(card_index).c_str(), int(new_frames[card_index].dropped_frames));
                        }
                }
 
@@ -1298,7 +1317,7 @@ void Mixer::thread_func()
                        continue;
                }
 
-               for (unsigned card_index = 0; card_index < num_cards + num_video_inputs + num_html_inputs; ++card_index) {
+               for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                        if (!has_new_frame[card_index] || new_frames[card_index].frame->len == 0)
                                continue;
 
@@ -1471,7 +1490,7 @@ start:
                goto start;
        }
 
-       for (unsigned card_index = 0; card_index < num_cards + num_video_inputs + num_html_inputs; ++card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                CaptureCard *card = &cards[card_index];
                if (card->new_frames.empty()) {  // Starvation.
                        ++card->metric_input_duped_frames;
@@ -1505,7 +1524,7 @@ start:
                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 + num_html_inputs; ++card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                CaptureCard *card = &cards[card_index];
                if (has_new_frame[card_index] &&
                    !input_card_is_master_clock(card_index, master_card_index) &&
@@ -1535,17 +1554,28 @@ start:
 void Mixer::handle_hotplugged_cards()
 {
        // Check for cards that have been disconnected since last frame.
-       for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                CaptureCard *card = &cards[card_index];
-               if (card->capture->get_disconnected()) {
+               if (card->capture != nullptr && card->capture->get_disconnected()) {
                        fprintf(stderr, "Card %u went away, replacing with a fake card.\n", card_index);
                        FakeCapture *capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
-                       configure_card(card_index, capture, CardType::FAKE_CAPTURE, /*output=*/nullptr);
+                       configure_card(card_index, capture, CardType::FAKE_CAPTURE, /*output=*/nullptr, /*is_srt_card=*/false);
                        card->queue_length_policy.reset(card_index);
                        card->capture->start_bm_capture();
                }
        }
 
+       // Count how many active cards we already have. Used below to check that we
+       // don't go past the max_cards limit set by the user. Note that (non-SRT) video
+       // and HTML “cards” don't count towards this limit.
+       int num_video_cards = 0;
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
+               CaptureCard *card = &cards[card_index];
+               if (card->type == CardType::LIVE_CARD || is_srt_card(card)) {
+                       ++num_video_cards;
+               }
+       }
+
        // Check for cards that have been connected since last frame.
        vector<libusb_device *> hotplugged_cards_copy;
 #ifdef HAVE_SRT
@@ -1561,14 +1591,14 @@ void Mixer::handle_hotplugged_cards()
        for (libusb_device *new_dev : hotplugged_cards_copy) {
                // Look for a fake capture card where we can stick this in.
                int free_card_index = -1;
-               for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+               for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                        if (cards[card_index].is_fake_capture) {
                                free_card_index = card_index;
                                break;
                        }
                }
 
-               if (free_card_index == -1) {
+               if (free_card_index == -1 || num_video_cards >= global_flags.max_num_cards) {
                        fprintf(stderr, "New card plugged in, but no free slots -- ignoring.\n");
                        libusb_unref_device(new_dev);
                } else {
@@ -1576,7 +1606,7 @@ void Mixer::handle_hotplugged_cards()
                        fprintf(stderr, "New card plugged in, choosing slot %d.\n", free_card_index);
                        CaptureCard *card = &cards[free_card_index];
                        BMUSBCapture *capture = new BMUSBCapture(free_card_index, new_dev);
-                       configure_card(free_card_index, capture, CardType::LIVE_CARD, /*output=*/nullptr);
+                       configure_card(free_card_index, capture, CardType::LIVE_CARD, /*output=*/nullptr, /*is_srt_card=*/false);
                        card->queue_length_policy.reset(free_card_index);
                        capture->set_card_disconnected_callback(bind(&Mixer::bm_hotplug_remove, this, free_card_index));
                        capture->start_bm_capture();
@@ -1597,7 +1627,7 @@ void Mixer::handle_hotplugged_cards()
                // same stream ID, if any exist -- and it multiple exist,
                // take the one that disconnected the last.
                int first_free_card_index = -1, last_matching_free_card_index = -1;
-               for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+               for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                        CaptureCard *card = &cards[card_index];
                        if (!card->is_fake_capture) {
                                continue;
@@ -1615,7 +1645,7 @@ void Mixer::handle_hotplugged_cards()
 
                const int free_card_index = (last_matching_free_card_index != -1)
                        ? last_matching_free_card_index : first_free_card_index;
-               if (free_card_index == -1) {
+               if (free_card_index == -1 || num_video_cards >= global_flags.max_num_cards) {
                        if (stream_id.empty()) {
                                stream_id = "no name";
                        }
@@ -1631,7 +1661,7 @@ void Mixer::handle_hotplugged_cards()
                        CaptureCard *card = &cards[free_card_index];
                        FFmpegCapture *capture = new FFmpegCapture(sock, stream_id);
                        capture->set_card_index(free_card_index);
-                       configure_card(free_card_index, capture, CardType::FFMPEG_INPUT, /*output=*/nullptr, /*override_card_as_live=*/true);
+                       configure_card(free_card_index, capture, CardType::FFMPEG_INPUT, /*output=*/nullptr, /*is_srt_card=*/true);
                        update_srt_stats(sock, card);  // Initial zero stats.
                        card->last_srt_stream_id = stream_id;
                        card->queue_length_policy.reset(free_card_index);
@@ -1640,13 +1670,23 @@ void Mixer::handle_hotplugged_cards()
                }
        }
 #endif
+
+       // Finally, newly forced-to-active fake capture cards.
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
+               CaptureCard *card = &cards[card_index];
+               if (card->capture == nullptr && card->force_active) {
+                       FakeCapture *capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
+                       configure_card(card_index, capture, CardType::FAKE_CAPTURE, /*output=*/nullptr, /*is_srt_card=*/false);
+                       card->queue_length_policy.reset(card_index);
+                       card->capture->start_bm_capture();
+               }
+       }
 }
 
 
 void Mixer::schedule_audio_resampling_tasks(unsigned dropped_frames, int num_samples_per_frame, int length_per_frame, bool is_preroll, steady_clock::time_point frame_timestamp)
 {
        // Resample the audio as needed, including from previously dropped frames.
-       assert(num_cards > 0);
        for (unsigned frame_num = 0; frame_num < dropped_frames + 1; ++frame_num) {
                const bool dropped_frame = (frame_num != dropped_frames);
                {
@@ -1688,7 +1728,7 @@ void Mixer::render_one_frame(int64_t duration)
        // Update Y'CbCr settings for all cards.
        {
                lock_guard<mutex> lock(card_mutex);
-               for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+               for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                        YCbCrInterpretation *interpretation = &ycbcr_interpretation[card_index];
                        input_state.ycbcr_coefficients_auto[card_index] = interpretation->ycbcr_coefficients_auto;
                        input_state.ycbcr_coefficients[card_index] = interpretation->ycbcr_coefficients;
@@ -1922,7 +1962,11 @@ void Mixer::set_input_ycbcr_interpretation(unsigned card_index, const YCbCrInter
 
 void Mixer::start_mode_scanning(unsigned card_index)
 {
-       assert(card_index < num_cards);
+       assert(card_index < MAX_VIDEO_CARDS);
+       if (cards[card_index].capture != nullptr) {
+               // Inactive card. Should never happen.
+               return;
+       }
        if (is_mode_scanning[card_index]) {
                return;
        }
@@ -1946,12 +1990,14 @@ map<uint32_t, VideoMode> Mixer::get_available_output_video_modes() const
 
 string Mixer::get_ffmpeg_filename(unsigned card_index) const
 {
-       assert(card_index >= num_cards && card_index < num_cards + num_video_inputs);
+       assert(card_index < MAX_VIDEO_CARDS);
+       assert(cards[card_index].type == CardType::FFMPEG_INPUT);
        return ((FFmpegCapture *)(cards[card_index].capture.get()))->get_filename();
 }
 
 void Mixer::set_ffmpeg_filename(unsigned card_index, const string &filename) {
-       assert(card_index >= num_cards && card_index < num_cards + num_video_inputs);
+       assert(card_index < MAX_VIDEO_CARDS);
+       assert(cards[card_index].type == CardType::FFMPEG_INPUT);
        ((FFmpegCapture *)(cards[card_index].capture.get()))->change_filename(filename);
 }
 
@@ -2175,4 +2221,44 @@ void Mixer::update_srt_stats(int srt_sock, Mixer::CaptureCard *card)
 }
 #endif
 
+string Mixer::description_for_card(unsigned card_index)
+{
+       CaptureCard *card = &cards[card_index];
+       if (card->capture == nullptr) {
+               // Should never be called for inactive cards, but OK.
+               char buf[256];
+               snprintf(buf, sizeof(buf), "Inactive capture card %u", card_index);
+               return buf;
+       }
+       if (card->type != CardType::FFMPEG_INPUT) {
+               char buf[256];
+               snprintf(buf, sizeof(buf), "Capture card %u (%s)", card_index, card->capture->get_description().c_str());
+               return buf;
+       }
+
+       // Number (non-SRT) FFmpeg inputs from zero, separately from the capture cards,
+       // since it's not too obvious for the user that they are “cards”.
+       unsigned ffmpeg_index = 0;
+       for (unsigned i = 0; i < card_index; ++i) {
+               CaptureCard *other_card = &cards[i];
+               if (other_card->type == CardType::FFMPEG_INPUT && !is_srt_card(other_card)) {
+                       ++ffmpeg_index;
+               }
+       }
+       char buf[256];
+       snprintf(buf, sizeof(buf), "Video input %u (%s)", ffmpeg_index, card->capture->get_description().c_str());
+       return buf;
+}
+
+bool Mixer::is_srt_card(const Mixer::CaptureCard *card)
+{
+#ifdef HAVE_SRT
+       if (card->type == CardType::FFMPEG_INPUT) {
+               int srt_sock = static_cast<FFmpegCapture *>(card->capture.get())->get_srt_sock();
+               return srt_sock != -1;
+       }
+#endif
+       return false;
+}
+
 mutex RefCountedGLsync::fence_lock;
index 1852e48ed8196fa79938a5d4527e9e55f7f7a14a..e382f7755ed31f642513118c8a2b41dbf6c6756e 100644 (file)
@@ -29,6 +29,7 @@
 #include "audio_mixer.h"
 #include "bmusb/bmusb.h"
 #include "defs.h"
+#include "ffmpeg_capture.h"
 #include "shared/httpd.h"
 #include "input_state.h"
 #include "libusb.h"
@@ -158,7 +159,7 @@ private:
 class Mixer {
 public:
        // The surface format is used for offscreen destinations for OpenGL contexts we need.
-       Mixer(const QSurfaceFormat &format, unsigned num_cards);
+       Mixer(const QSurfaceFormat &format);
        ~Mixer();
        void start();
        void quit();
@@ -303,10 +304,8 @@ public:
                should_cut = true;
        }
 
-       unsigned get_num_cards() const { return num_cards; }
-
        std::string get_card_description(unsigned card_index) const {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                return cards[card_index].capture->get_description();
        }
 
@@ -316,7 +315,7 @@ public:
        // the card's actual name.
        std::string get_output_card_description(unsigned card_index) const {
                assert(card_can_be_used_as_output(card_index));
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                if (cards[card_index].parked_capture) {
                        return cards[card_index].parked_capture->get_description();
                } else {
@@ -325,65 +324,87 @@ public:
        }
 
        bool card_can_be_used_as_output(unsigned card_index) const {
-               assert(card_index < num_cards);
-               return cards[card_index].output != nullptr;
+               assert(card_index < MAX_VIDEO_CARDS);
+               return cards[card_index].output != nullptr && cards[card_index].capture != nullptr;
+       }
+
+       bool card_is_cef(unsigned card_index) const {
+               assert(card_index < MAX_VIDEO_CARDS);
+               return cards[card_index].type == CardType::CEF_INPUT;
        }
 
        bool card_is_ffmpeg(unsigned card_index) const {
-               assert(card_index < num_cards + num_video_inputs);
-               if (card_index < num_cards) {
-                       // SRT inputs are more like regular inputs than FFmpeg inputs,
-                       // so show them as such. (This allows the user to right-click
-                       // to select a different input.)
+               assert(card_index < MAX_VIDEO_CARDS);
+               if (cards[card_index].type != CardType::FFMPEG_INPUT) {
                        return false;
                }
-               return cards[card_index].type == CardType::FFMPEG_INPUT;
+#ifdef HAVE_SRT
+               // SRT inputs are more like regular inputs than FFmpeg inputs,
+               // so show them as such. (This allows the user to right-click
+               // to select a different input.)
+               return static_cast<FFmpegCapture *>(cards[card_index].capture.get())->get_srt_sock() == -1;
+#else
+               return true;
+#endif
+       }
+
+       bool card_is_active(unsigned card_index) const {
+               assert(card_index < MAX_VIDEO_CARDS);
+               std::lock_guard<std::mutex> lock(card_mutex);
+               return cards[card_index].capture != nullptr;
+       }
+
+       void force_card_active(unsigned card_index)
+       {
+               // handle_hotplugged_cards() will pick this up.
+               std::lock_guard<std::mutex> lock(card_mutex);
+               cards[card_index].force_active = true;
        }
 
        std::map<uint32_t, bmusb::VideoMode> get_available_video_modes(unsigned card_index) const {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                return cards[card_index].capture->get_available_video_modes();
        }
 
        uint32_t get_current_video_mode(unsigned card_index) const {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                return cards[card_index].capture->get_current_video_mode();
        }
 
        void set_video_mode(unsigned card_index, uint32_t mode) {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                cards[card_index].capture->set_video_mode(mode);
        }
 
        void start_mode_scanning(unsigned card_index);
 
        std::map<uint32_t, std::string> get_available_video_inputs(unsigned card_index) const {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                return cards[card_index].capture->get_available_video_inputs();
        }
 
        uint32_t get_current_video_input(unsigned card_index) const {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                return cards[card_index].capture->get_current_video_input();
        }
 
        void set_video_input(unsigned card_index, uint32_t input) {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                cards[card_index].capture->set_video_input(input);
        }
 
        std::map<uint32_t, std::string> get_available_audio_inputs(unsigned card_index) const {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                return cards[card_index].capture->get_available_audio_inputs();
        }
 
        uint32_t get_current_audio_input(unsigned card_index) const {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                return cards[card_index].capture->get_current_audio_input();
        }
 
        void set_audio_input(unsigned card_index, uint32_t input) {
-               assert(card_index < num_cards);
+               assert(card_index < MAX_VIDEO_CARDS);
                cards[card_index].capture->set_audio_input(input);
        }
 
@@ -439,13 +460,7 @@ public:
 private:
        struct CaptureCard;
 
-       enum class CardType {
-               LIVE_CARD,
-               FAKE_CAPTURE,
-               FFMPEG_INPUT,
-               CEF_INPUT,
-       };
-       void configure_card(unsigned card_index, bmusb::CaptureInterface *capture, CardType card_type, DeckLinkOutput *output, bool is_srt_card = false);
+       void configure_card(unsigned card_index, bmusb::CaptureInterface *capture, CardType card_type, DeckLinkOutput *output, bool is_srt_card);
        void set_output_card_internal(int card_index);  // Should only be called from the mixer thread.
        void bm_frame(unsigned card_index, uint16_t timecode,
                bmusb::FrameAllocator::Frame video_frame, size_t video_offset, bmusb::VideoFormat video_format,
@@ -473,7 +488,7 @@ private:
        std::pair<std::string, std::string> get_channel_color_http(unsigned channel_idx);
 
        HTTPD httpd;
-       unsigned num_cards, num_video_inputs, num_html_inputs = 0;
+       unsigned num_video_inputs, num_html_inputs = 0;
 
        QSurface *mixer_surface, *h264_encoder_surface, *decklink_output_surface, *image_update_surface;
        std::unique_ptr<movit::ResourcePool> resource_pool;
@@ -521,7 +536,12 @@ private:
        mutable std::mutex card_mutex;
        bool has_bmusb_thread = false;
        struct CaptureCard {
+               // If nullptr, the card is inactive, and will be hidden in the UI.
+               // Only fake capture cards can be inactive.
                std::unique_ptr<bmusb::CaptureInterface> capture;
+               // If true, card must always be active (typically because it's one of the
+               // first cards, or because the theme has explicitly asked for it).
+               bool force_active = false;
                bool is_fake_capture;
                // If is_fake_capture is true, contains a monotonic timer value for when
                // it was last changed. Otherwise undefined. Used for SRT re-plugging.
@@ -660,6 +680,9 @@ private:
        void update_srt_stats(int srt_sock, Mixer::CaptureCard *card);
 #endif
 
+       std::string description_for_card(unsigned card_index);
+       static bool is_srt_card(const CaptureCard *card);
+
        InputState input_state;
 
        // Cards we have been noticed about being hotplugged, but haven't tried adding yet.
index 21098a7a5997fac8743008fe285c412566ae0e57..76f05b7ce33acc8044e88c8e007268e9820a5482 100644 (file)
@@ -14,11 +14,10 @@ using namespace std::chrono;
 
 ReceivedTimestamps find_received_timestamp(const vector<RefCountedFrame> &input_frames)
 {
-       unsigned num_cards = global_mixer->get_num_cards();
-       assert(input_frames.size() == num_cards * FRAME_HISTORY_LENGTH);
+       assert(input_frames.size() == MAX_VIDEO_CARDS * FRAME_HISTORY_LENGTH);
 
        ReceivedTimestamps ts;
-       for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                for (unsigned frame_index = 0; frame_index < FRAME_HISTORY_LENGTH; ++frame_index) {
                        const RefCountedFrame &input_frame = input_frames[card_index * FRAME_HISTORY_LENGTH + frame_index];
                        if (input_frame == nullptr ||
@@ -34,9 +33,8 @@ ReceivedTimestamps find_received_timestamp(const vector<RefCountedFrame> &input_
 
 void LatencyHistogram::init(const string &measuring_point)
 {
-       unsigned num_cards = global_flags.num_cards;  // The mixer might not be ready yet.
-       summaries.resize(num_cards * FRAME_HISTORY_LENGTH * 2);
-       for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+       summaries.resize(MAX_VIDEO_CARDS * FRAME_HISTORY_LENGTH * 2);
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                char card_index_str[64];
                snprintf(card_index_str, sizeof(card_index_str), "%u", card_index);
                summaries[card_index].resize(FRAME_HISTORY_LENGTH);
@@ -55,7 +53,7 @@ void LatencyHistogram::init(const string &measuring_point)
                                 { "frame_age", frame_index_str },
                                 { "frame_type", "i/p" }},
                                 &summaries[card_index][frame_index][0],
-                               (frame_index == 0) ? Metrics::PRINT_ALWAYS : Metrics::PRINT_WHEN_NONEMPTY);
+                               Metrics::PRINT_WHEN_NONEMPTY);
                        global_metrics.add("latency_seconds",
                                {{ "measuring_point", measuring_point },
                                 { "card", card_index_str },
@@ -69,7 +67,7 @@ void LatencyHistogram::init(const string &measuring_point)
                                 { "frame_age", frame_index_str },
                                 { "frame_type", "total" }},
                                 &summaries[card_index][frame_index][2],
-                               (frame_index == 0) ? Metrics::PRINT_ALWAYS : Metrics::PRINT_WHEN_NONEMPTY);
+                               Metrics::PRINT_WHEN_NONEMPTY);
                }
        }
 }
@@ -91,9 +89,8 @@ void print_latency(const char *header, const ReceivedTimestamps &received_ts, bo
                        histogram->summaries[0][0][2].count_event(latency.count());
                }
        } else {
-               unsigned num_cards = global_mixer->get_num_cards();
-               assert(received_ts.ts.size() == num_cards * FRAME_HISTORY_LENGTH);
-               for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+               assert(received_ts.ts.size() == MAX_VIDEO_CARDS * FRAME_HISTORY_LENGTH);
+               for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                        for (unsigned frame_index = 0; frame_index < FRAME_HISTORY_LENGTH; ++frame_index) {
                                steady_clock::time_point ts = received_ts.ts[card_index * FRAME_HISTORY_LENGTH + frame_index];
                                if (ts == steady_clock::time_point::min()) {
index 711c1c5239f44aad7df9000711556bc2ff39c2d6..045d5c7b0f28460db1700e47b10aff339bbbed66 100644 (file)
@@ -30,8 +30,8 @@
 using namespace std;
 using namespace std::chrono;
 
-ResamplingQueue::ResamplingQueue(DeviceSpec device_spec, unsigned freq_in, unsigned freq_out, unsigned num_channels, double expected_delay_seconds)
-       : device_spec(device_spec), freq_in(freq_in), freq_out(freq_out), num_channels(num_channels),
+ResamplingQueue::ResamplingQueue(const std::string &debug_description, unsigned freq_in, unsigned freq_out, unsigned num_channels, double expected_delay_seconds)
+       : debug_description(debug_description), freq_in(freq_in), freq_out(freq_out), num_channels(num_channels),
          current_estimated_freq_in(freq_in),
          ratio(double(freq_out) / double(freq_in)), expected_delay(expected_delay_seconds * OUTPUT_FREQUENCY)
 {
@@ -62,7 +62,7 @@ void ResamplingQueue::add_input_samples(steady_clock::time_point ts, const float
                current_estimated_freq_in = (a1.input_samples_received - a0.input_samples_received) / duration<double>(a1.ts - a0.ts).count();
                if (!(current_estimated_freq_in >= 0.0)) {
                        fprintf(stderr, "%s: PANIC: Input audio clock going backwards, ignoring.\n",
-                               spec_to_string(device_spec).c_str());
+                               debug_description.c_str());
                        current_estimated_freq_in = freq_in;
                }
 
@@ -170,7 +170,7 @@ bool ResamplingQueue::get_output_samples(steady_clock::time_point ts, float *sam
                        // This should never happen unless delay is set way too low,
                        // or we're dropping a lot of data.
                        fprintf(stderr, "%s: PANIC: Out of input samples to resample, still need %d output samples! (correction factor is %f)\n",
-                               spec_to_string(device_spec).c_str(), int(vresampler.out_count), rcorr);
+                               debug_description.c_str(), int(vresampler.out_count), rcorr);
                        memset(vresampler.out_data, 0, vresampler.out_count * num_channels * sizeof(float));
 
                        // Reset the loop filter.
index f0e2499cbf3300e9b4340430b85bf33f25862006..dd55d4c62a85695f6f7b737ec46eb27321f70433 100644 (file)
@@ -49,8 +49,8 @@
 
 class ResamplingQueue {
 public:
-       // device_spec is for debugging outputs only.
-       ResamplingQueue(DeviceSpec device_spec, unsigned freq_in, unsigned freq_out, unsigned num_channels, double expected_delay_seconds);
+       // debug_description is for diagnostic output only.
+       ResamplingQueue(const std::string &debug_description, unsigned freq_in, unsigned freq_out, unsigned num_channels, double expected_delay_seconds);
 
        // If policy is DO_NOT_ADJUST_RATE, the resampling rate will not be changed.
        // This is primarily useful if you have an extraordinary situation, such as
@@ -69,7 +69,7 @@ private:
 
        VResampler vresampler;
 
-       DeviceSpec device_spec;
+       std::string debug_description;
        unsigned freq_in, freq_out, num_channels;
 
        bool first_output = true;
index 6372e61b5cc4b22575888c47c3f38cc91324649e..8ea6b9723b864e79357a41cc754c92d09c17e11b 100644 (file)
@@ -8,7 +8,7 @@ syntax = "proto2";
 // to the right device even if the devices have moved around.
 message DeviceSpecProto {
        // Members from DeviceSpec itself.
-       enum InputSourceType { SILENCE = 0; CAPTURE_CARD = 1; ALSA_INPUT = 2; FFMPEG_VIDEO_INPUT = 3; };
+       enum InputSourceType { SILENCE = 0; CAPTURE_CARD = 1; ALSA_INPUT = 2; };
        optional InputSourceType type = 1;
        optional int32 index = 2;
 
index 2bb9e807495e32a2d316ee5287a6f25b45208dc8..bb7613c9871e71df19db23feb39baccabf6ae993 100644 (file)
@@ -1466,8 +1466,8 @@ int Nageru_set_audio_bus_eq_level_db(lua_State *L)
        return 0;
 }
 
-Theme::Theme(const string &filename, const vector<string> &search_dirs, ResourcePool *resource_pool, unsigned num_cards)
-       : resource_pool(resource_pool), num_cards(num_cards), signal_to_card_mapping(global_flags.default_stream_mapping)
+Theme::Theme(const string &filename, const vector<string> &search_dirs, ResourcePool *resource_pool)
+       : resource_pool(resource_pool), signal_to_card_mapping(global_flags.default_stream_mapping)
 {
        // Defaults.
        channel_names[0] = "Live";
@@ -1748,8 +1748,8 @@ Theme::Chain Theme::get_chain(unsigned num, float t, unsigned width, unsigned he
 
        // TODO: Can we do better, e.g. by running setup_chain() and seeing what it references?
        // Actually, setup_chain does maybe hold all the references we need now anyway?
-       chain.input_frames.reserve(num_cards * FRAME_HISTORY_LENGTH);
-       for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+       chain.input_frames.reserve(MAX_VIDEO_CARDS * FRAME_HISTORY_LENGTH);
+       for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
                for (unsigned frame_num = 0; frame_num < FRAME_HISTORY_LENGTH; ++frame_num) {
                        chain.input_frames.push_back(input_state.buffered_frames[card_index][frame_num].frame);
                }
@@ -1959,24 +1959,25 @@ int Theme::map_signal_to_card(int signal_num)
        }
 
        int card_index;
-       if (global_flags.output_card != -1 && num_cards > 1) {
+       if (global_flags.output_card != -1) {
                // Try to exclude the output card from the default card_index.
-               card_index = signal_num % (num_cards - 1);
+               card_index = signal_num % (global_flags.max_num_cards - 1);
                if (card_index >= global_flags.output_card) {
                         ++card_index;
                }
-               if (signal_num >= int(num_cards - 1)) {
+               if (signal_num >= int(global_flags.max_num_cards - 1)) {
                        print_warning(L, "Theme asked for input %d, but we only have %u input card(s) (card %d is busy with output).\n",
-                               signal_num, num_cards - 1, global_flags.output_card);
+                               signal_num, global_flags.max_num_cards - 1, global_flags.output_card);
                        fprintf(stderr, "Mapping to card %d instead.\n", card_index);
                }
        } else {
-               card_index = signal_num % num_cards;
-               if (signal_num >= int(num_cards)) {
-                       print_warning(L, "Theme asked for input %d, but we only have %u card(s).\n", signal_num, num_cards);
+               card_index = signal_num % global_flags.max_num_cards;
+               if (signal_num >= int(global_flags.max_num_cards)) {
+                       print_warning(L, "Theme asked for input %d, but we only have %u card(s).\n", signal_num, global_flags.max_num_cards);
                        fprintf(stderr, "Mapping to card %d instead.\n", card_index);
                }
        }
+       global_mixer->force_card_active(card_index);
        signal_to_card_mapping[signal_num] = card_index;
        return card_index;
 }
@@ -1984,7 +1985,7 @@ int Theme::map_signal_to_card(int signal_num)
 void Theme::set_signal_mapping(int signal_num, int card_idx)
 {
        lock_guard<mutex> lock(map_m);
-       assert(card_idx < int(num_cards));
+       assert(card_idx < MAX_VIDEO_CARDS);
        signal_to_card_mapping[signal_num] = card_idx;
 }
 
index 507dc01828cafb3fe2524f8187b9f56a9f279e5e..ce232fcfa913acb8d9eaef44c5889b4d42f62c79 100644 (file)
@@ -85,7 +85,7 @@ struct InputStateInfo {
 
 class Theme {
 public:
-       Theme(const std::string &filename, const std::vector<std::string> &search_dirs, movit::ResourcePool *resource_pool, unsigned num_cards);
+       Theme(const std::string &filename, const std::vector<std::string> &search_dirs, movit::ResourcePool *resource_pool);
        ~Theme();
 
        struct Chain {
@@ -206,7 +206,6 @@ private:
        const InputState *input_state = nullptr;  // Protected by <m>. Only set temporarily, during chain setup.
        movit::ResourcePool *resource_pool;
        int num_channels = -1;
-       unsigned num_cards;
        bool startup_finished = false;
 
        std::mutex map_m;