diff options
Diffstat (limited to 'pica-client/dialogs/settingsdialog.cpp')
| -rw-r--r-- | pica-client/dialogs/settingsdialog.cpp | 400 |
1 files changed, 384 insertions, 16 deletions
diff --git a/pica-client/dialogs/settingsdialog.cpp b/pica-client/dialogs/settingsdialog.cpp index 8685936..15c7c96 100644 --- a/pica-client/dialogs/settingsdialog.cpp +++ b/pica-client/dialogs/settingsdialog.cpp @@ -19,25 +19,28 @@ #include "../globals.h" #include "../audiodevice.h" #include "../videodevice.h" +#include "../callsettings.h" #include "../../PICA_netconf.h" #include "../../PICA_proto.h" #include <QGroupBox> #include <QVBoxLayout> #include <QHBoxLayout> +#include <QFormLayout> #include <QLabel> #include <QDebug> #include <QVariant> #include <QListWidget> #include <QNetworkInterface> #include <QHostAddress> +#include <QSize> #include <QTabWidget> #include <QTimer> -// Audio pipeline test: how long the microphone is recorded for, and the -// sample rate to record at - the same rate a call negotiates. +// Audio pipeline test: how long the microphone is recorded for. The codec and +// the rate come from the "Call Settings" tab, so the test runs the same path a +// call would - see toggleAudioTest(). static const int kAudioTestRecordMs = 5000; -static const int kAudioTestSampleRate = 48000; // One encoded packet covers this much audio (the Opus encoder produces // 20 ms frames at 48 kHz), so feeding one packet per this many milliseconds @@ -55,14 +58,41 @@ static const int kAudioTestStartTimeoutMs = 4000; // buffer and the sound card's own buffer to drain before closing the device. static const int kAudioTestDrainMs = 500; -// Size requested from the camera by the video pipeline test, matching what a -// call asks for. -static const int kVideoTestWidth = 640; -static const int kVideoTestHeight = 480; +// Sizes and frame rates offered when the camera will not say what it supports +// - a platform where they cannot be queried, or a camera that will not answer. +// The camera is free to refuse any of them, the same way it may refuse +// anything else it was asked for; VideoDevice::Capture() loosens the request +// rather than failing. +static const struct +{ + int width; + int height; +} kFallbackResolutions[] = +{ + { 1920, 1080 }, { 1280, 720 }, { 800, 600 }, { 640, 480 }, + { 640, 360 }, { 352, 288 }, { 320, 240 }, { 160, 120 } +}; + +static const int kFallbackFrameRates[] = { 30, 25, 24, 20, 15, 10, 5 }; + +// Range the bitrate spin boxes accept, in kbit/s. Wide enough not to argue +// with anyone: the low end is where a codec still produces something, the high +// end well past what a call over a home connection can carry. +static const int kMinVideoBitrateKbps = 50; +static const int kMaxVideoBitrateKbps = 20000; +static const int kMinAudioBitrateKbps = 6; +static const int kMaxAudioBitrateKbps = 510; SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) { + // The video tab's format lists follow the camera selection, and + // fillVideoDevices() below changes that before they have been built - so + // they have to be readable as "not there yet" from the moment anything can + // reach them. + videoRes = nullptr; + videoFps = nullptr; + QVBoxLayout *settingsLayout = new QVBoxLayout(); QTabWidget *tabW = new QTabWidget(this); @@ -71,11 +101,13 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QWidget* multilogintab = new QWidget(0); QWidget* audiodevtab = new QWidget(0); QWidget* videodevtab = new QWidget(0); + QWidget* calltab = new QWidget(0); QVBoxLayout *directc2cLayout = new QVBoxLayout(); QVBoxLayout *multiloginlayout = new QVBoxLayout(); QVBoxLayout *audiodevlayout = new QVBoxLayout(); QVBoxLayout *videodevLayout = new QVBoxLayout(); + QVBoxLayout *callLayout = new QVBoxLayout(); //Direct connections rbDisableDirectConns = new QRadioButton(tr("Disable direct connections"), this); @@ -305,6 +337,29 @@ SettingsDialog::SettingsDialog(QWidget *parent) : videoDevRefresh = new QPushButton(tr("Refresh 🔄"), this); connect(videoDevRefresh, SIGNAL(clicked()), this, SLOT(fillVideoDevices())); + QLabel *lbVideoRes = new QLabel(tr("Resolution"), this); + videoRes = new QComboBox(this); + videoRes->setToolTip(tr("Picture sizes the selected camera offers. A size the camera can " + "deliver already compressed lists the formats in brackets; those " + "are the ones the \"prefer compressed formats\" setting below can " + "forward untouched, without decoding and re-encoding them here.")); + + QLabel *lbVideoFps = new QLabel(tr("Frame rate"), this); + videoFps = new QComboBox(this); + videoFps->setToolTip(tr("Frame rates the camera offers at the selected size.")); + + // Filled from the camera the device list above has selected, so the two + // have to follow it - and each other. + fillVideoResolutions(); + connect(videoDev, SIGNAL(currentIndexChanged(int)), this, SLOT(fillVideoResolutions())); + connect(videoRes, SIGNAL(currentIndexChanged(int)), this, SLOT(fillVideoFrameRates())); + + QHBoxLayout *videoFormatLayout = new QHBoxLayout(); + videoFormatLayout->addWidget(lbVideoRes); + videoFormatLayout->addWidget(videoRes, 1); + videoFormatLayout->addWidget(lbVideoFps); + videoFormatLayout->addWidget(videoFps); + cbPreferCompressed = new QCheckBox(tr("Prefer compressed formats if provided by the camera"), this); #ifdef HAVE_VAAPI @@ -336,6 +391,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) : videodevLayout->addWidget(videoDev); videodevLayout->addWidget(videoDevRefresh); + videodevLayout->addLayout(videoFormatLayout); videodevLayout->addWidget(cbPreferCompressed); #ifdef HAVE_VAAPI videodevLayout->addWidget(cbVaapiEncoding); @@ -376,10 +432,73 @@ SettingsDialog::SettingsDialog(QWidget *parent) : #endif testDecoderThread.start(); +//Call Settings + // What a call encodes with. The codec names stored here are FFmpeg names, + // which is also how the 0x74 and 0x75 messages name them - see + // callsettings.h. + callVideoCodec = new QComboBox(this); + + for (int i = 0; i < kCallVideoCodecCount; i++) + callVideoCodec->addItem(QLatin1String(kCallVideoCodecs[i].displayName), + QLatin1String(kCallVideoCodecs[i].name)); + + callVideoCodec->setToolTip(tr("What outgoing video is encoded with. The peer is told which " + "codec is in use, so it does not have to be one it was " + "configured for.\n\n" + "This applies to video encoded here. With \"prefer compressed " + "formats\" switched on and a camera that delivers a compressed " + "stream itself, that stream is forwarded as it comes and the " + "camera's format is what the peer receives - this setting then " + "only decides which of the camera's formats is preferred.")); + + callAudioCodec = new QComboBox(this); + + for (int i = 0; i < kCallAudioCodecCount; i++) + callAudioCodec->addItem(QLatin1String(kCallAudioCodecs[i].displayName), + QLatin1String(kCallAudioCodecs[i].name)); + + callAudioCodec->setToolTip(tr("What outgoing audio is encoded with. Opus is the one to want " + "for a voice call; G.722 and G.711 A-law are the telephony " + "codecs, fixed at 64 kbit/s and there for compatibility.")); + + callVideoBitrate = new QSpinBox(this); + callVideoBitrate->setRange(kMinVideoBitrateKbps, kMaxVideoBitrateKbps); + callVideoBitrate->setSuffix(tr(" kbps")); + callVideoBitrate->setValue(kDefaultVideoBitrateKbps); + + callAudioBitrate = new QSpinBox(this); + callAudioBitrate->setRange(kMinAudioBitrateKbps, kMaxAudioBitrateKbps); + callAudioBitrate->setSuffix(tr(" kbps")); + callAudioBitrate->setValue(kDefaultAudioBitrateKbps); + audioBitrateKbps = kDefaultAudioBitrateKbps; + + connect(callAudioCodec, SIGNAL(currentIndexChanged(int)), this, SLOT(callAudioCodecChanged())); + + QFormLayout *callFormLayout = new QFormLayout(); + callFormLayout->addRow(tr("Video Codec"), callVideoCodec); + callFormLayout->addRow(tr("Video Bitrate"), callVideoBitrate); + callFormLayout->addRow(tr("Audio Codec"), callAudioCodec); + callFormLayout->addRow(tr("Audio Bitrate"), callAudioBitrate); + + // The other half of what a call is started with lives on the video tab, + // next to the camera that has to deliver it; say so rather than leaving + // this tab looking like the whole story. + QLabel *lbCallHint = new QLabel(tr("The picture size and frame rate a call is started with are " + "on the Video Devices tab, next to the camera that has to " + "deliver them."), this); + lbCallHint->setWordWrap(true); + + callLayout->addLayout(callFormLayout); + callLayout->addWidget(lbCallHint); + callLayout->addStretch(1); + + calltab->setLayout(callLayout); + tabW->addTab(directc2ctab, tr("Direct Connections")); tabW->addTab(multilogintab, tr("Multiple logins")); tabW->addTab(audiodevtab, tr("Audio Devices")); tabW->addTab(videodevtab, tr("Video Devices")); + tabW->addTab(calltab, tr("Call Settings")); tabW->addTab(soundstab, tr("Sounds")); settingsLayout->addWidget(tabW); @@ -511,9 +630,16 @@ void SettingsDialog::toggleAudioTest() btAudioTest->setText(tr("Stop ⏹")); audioTestStatus->setText(tr("Recording, say something...")); + // The codec selected on the "Call Settings" tab, at the rate a call with + // it would run at - same as the video test, this exercises what the + // current selections do rather than a fixed pipeline of its own. + const QString codec = callAudioCodec->itemData(callAudioCodec->currentIndex()).toString(); + const int rate = callAudioSampleRate(codec); + const int bitrate = callAudioBitrateAdjustable(codec) ? callAudioBitrate->value() * 1000 : 0; + QMetaObject::invokeMethod(testMic, "configureCapture", Qt::QueuedConnection, - Q_ARG(QString, dev), Q_ARG(QString, QStringLiteral("opus")), - Q_ARG(int, kAudioTestSampleRate)); + Q_ARG(QString, dev), Q_ARG(QString, codec), + Q_ARG(int, rate), Q_ARG(int, bitrate)); QMetaObject::invokeMethod(testMic, "Capture", Qt::QueuedConnection); // Only a watchdog for the device failing to open at all. The recording @@ -589,9 +715,13 @@ void SettingsDialog::startAudioTestPlayback() audioTestStatus->setText(summary); + // Decoded with what it was encoded with, exactly as the receiving end of a + // call decodes with whatever the peer announced. + const QString codec = callAudioCodec->itemData(callAudioCodec->currentIndex()).toString(); + QMetaObject::invokeMethod(testSpeaker, "configurePlayback", Qt::QueuedConnection, - Q_ARG(QString, dev), Q_ARG(QString, QStringLiteral("opus")), - Q_ARG(int, kAudioTestSampleRate)); + Q_ARG(QString, dev), Q_ARG(QString, codec), + Q_ARG(int, callAudioSampleRate(codec))); QMetaObject::invokeMethod(testSpeaker, "Play", Qt::QueuedConnection); // The recording has to be handed over at the rate it plays at, not all at @@ -715,10 +845,16 @@ void SettingsDialog::toggleVideoTest() videoTestPathReport.clear(); + // The settings as they stand in the dialog, not as they were last stored: + // the point of the test is to see what the current selections do before + // committing to them. QMetaObject::invokeMethod(testCam, "configureCapture", Qt::QueuedConnection, Q_ARG(QString, dev), - Q_ARG(int, kVideoTestWidth), Q_ARG(int, kVideoTestHeight), + Q_ARG(int, selectedVideoWidth()), Q_ARG(int, selectedVideoHeight()), + Q_ARG(int, selectedVideoFrameRate()), Q_ARG(bool, cbPreferCompressed->isChecked()), + Q_ARG(QString, callVideoCodec->itemData(callVideoCodec->currentIndex()).toString()), + Q_ARG(int, callVideoBitrate->value() * 1000), Q_ARG(bool, vaapiEncoding)); QMetaObject::invokeMethod(testCam, "Capture", Qt::QueuedConnection); @@ -898,10 +1034,11 @@ void SettingsDialog::fillDevicesComboBox(QComboBox *cb, MediaDevice *dev, enum M .arg(md.at(i).device) .arg(md.at(i).humanReadable); - // Cameras that can deliver a compressed stream themselves say so, - // which is what the "prefer compressed formats" setting acts on. - if (!md.at(i).compressedFormats.isEmpty()) - item += QString(QLatin1String(" [%1]")).arg(md.at(i).compressedFormats.join(QLatin1String(", "))); + // A camera's compressed formats are not listed here but in the + // resolution list, which is where they actually mean something: a + // camera offers them at some of its sizes and not at others, so + // naming them against the camera as a whole said less than it + // appeared to. cb->addItem(item, md.at(i).device); } @@ -910,7 +1047,193 @@ void SettingsDialog::fillDevicesComboBox(QComboBox *cb, MediaDevice *dev, enum M void SettingsDialog::fillVideoDevices() { VideoDevice vd; + + // Refilling walks the current index through -1 and back, and each step + // of that would otherwise ask a camera what it can do - which is not + // free, on Windows it means instantiating the capture filter. So the + // format lists below are rebuilt once, at the end. + const QString previous = videoDev->itemData(videoDev->currentIndex()).toString(); + + videoDev->blockSignals(true); fillDevicesComboBox(videoDev, &vd, CAPTURE); + + // Refresh is for picking up a camera that was plugged in, not for + // moving the selection off the one already chosen. + int item = videoDev->findData(previous); + if (item >= 0) + videoDev->setCurrentIndex(item); + + videoDev->blockSignals(false); + + fillVideoResolutions(); +} + +void SettingsDialog::fillVideoResolutions() +{ + // Nothing to fill into yet while the constructor is still building the + // tab - fillVideoDevices() runs before these exist. + if (!videoRes || !videoFps) + return; + + const QString dev = videoDev->itemData(videoDev->currentIndex()).toString(); + + videoFormats = dev.isEmpty() ? QList<VideoCaptureFormat>() + : VideoDevice::CaptureFormats(dev); + + // Refilling moves the current index about; the frame rate list is rebuilt + // once at the end rather than on every one of those intermediate changes. + videoRes->blockSignals(true); + videoRes->clear(); + + if (videoFormats.isEmpty()) + { + // Either the camera would not say or this platform cannot ask. Offer + // the common sizes rather than an empty list - a camera that cannot + // produce one of them is handled the same way as any other request it + // will not honour, see VideoDevice::Capture(). + for (unsigned int i = 0; i < sizeof(kFallbackResolutions) / sizeof(kFallbackResolutions[0]); i++) + { + const int w = kFallbackResolutions[i].width; + const int h = kFallbackResolutions[i].height; + + videoRes->addItem(QString(QLatin1String("%1x%2")).arg(w).arg(h), QSize(w, h)); + } + } + else + { + for (int i = 0; i < videoFormats.size(); i++) + { + const VideoCaptureFormat &f = videoFormats.at(i); + QString item = QString(QLatin1String("%1x%2")).arg(f.width).arg(f.height); + + // The formats this camera can hand over ready-made at this size, + // which is what "prefer compressed formats" below acts on. + if (!f.compressedFormats.isEmpty()) + item += QString(QLatin1String(" [%1]")).arg(f.compressedFormats.join(QLatin1String(", "))); + + videoRes->addItem(item, QSize(f.width, f.height)); + } + } + + // Keep the configured size selected across a device change where the new + // camera offers it too, rather than silently moving the setting to + // whatever happens to be first in the new list. + Settings st(config_dbname); + QSize wanted(st.loadValue("video.capture_width", kDefaultCaptureWidth).toInt(), + st.loadValue("video.capture_height", kDefaultCaptureHeight).toInt()); + + int item = videoRes->findData(wanted); + + if (item < 0) + item = videoRes->findData(QSize(kDefaultCaptureWidth, kDefaultCaptureHeight)); + + if (item >= 0) + videoRes->setCurrentIndex(item); + + videoRes->blockSignals(false); + + fillVideoFrameRates(); +} + +void SettingsDialog::fillVideoFrameRates() +{ + if (!videoFps) + return; + + const QSize size = videoRes->itemData(videoRes->currentIndex()).toSize(); + QList<int> rates; + + for (int i = 0; i < videoFormats.size(); i++) + { + if (videoFormats.at(i).width == size.width() && videoFormats.at(i).height == size.height()) + { + rates = videoFormats.at(i).frameRates; + break; + } + } + + videoFps->blockSignals(true); + videoFps->clear(); + + if (rates.isEmpty()) + { + // Same reasoning as for the sizes: a camera that named its sizes but + // not its rates, or one we could not ask at all. + for (unsigned int i = 0; i < sizeof(kFallbackFrameRates) / sizeof(kFallbackFrameRates[0]); i++) + rates << kFallbackFrameRates[i]; + } + + for (int i = 0; i < rates.size(); i++) + videoFps->addItem(tr("%1 fps").arg(rates.at(i)), rates.at(i)); + + Settings st(config_dbname); + int wanted = st.loadValue("video.capture_framerate", kDefaultCaptureFrameRate).toInt(); + + int item = videoFps->findData(wanted); + + if (item < 0) + item = videoFps->findData(kDefaultCaptureFrameRate); + + // Neither the configured rate nor the default is on offer at this size - + // take the highest the camera does offer, which is the first in the list. + if (item < 0 && videoFps->count() > 0) + item = 0; + + if (item >= 0) + videoFps->setCurrentIndex(item); + + videoFps->blockSignals(false); +} + +int SettingsDialog::selectedVideoWidth() const +{ + const QSize size = videoRes->itemData(videoRes->currentIndex()).toSize(); + + return size.isValid() ? size.width() : kDefaultCaptureWidth; +} + +int SettingsDialog::selectedVideoHeight() const +{ + const QSize size = videoRes->itemData(videoRes->currentIndex()).toSize(); + + return size.isValid() ? size.height() : kDefaultCaptureHeight; +} + +int SettingsDialog::selectedVideoFrameRate() const +{ + bool ok = false; + const int fps = videoFps->itemData(videoFps->currentIndex()).toInt(&ok); + + return (ok && fps > 0) ? fps : kDefaultCaptureFrameRate; +} + +void SettingsDialog::callAudioCodecChanged() +{ + const QString codec = callAudioCodec->itemData(callAudioCodec->currentIndex()).toString(); + + if (callAudioBitrateAdjustable(codec)) + { + // Coming back from a fixed rate codec, whose rate is showing in the + // box; the user's own choice is in audioBitrateKbps. + if (!callAudioBitrate->isEnabled()) + { + callAudioBitrate->setEnabled(true); + callAudioBitrate->setValue(audioBitrateKbps); + callAudioBitrate->setToolTip(QString()); + } + + return; + } + + if (callAudioBitrate->isEnabled()) + audioBitrateKbps = callAudioBitrate->value(); + + // Show what the codec will actually run at rather than leaving a number + // behind that has nothing to do with it. + callAudioBitrate->setEnabled(false); + callAudioBitrate->setValue(callAudioFixedBitrateKbps(codec)); + callAudioBitrate->setToolTip(tr("This codec codes a fixed number of bits per sample, so its " + "bitrate follows from the sample rate and cannot be chosen.")); } void SettingsDialog::audioDriverChanged(int index) @@ -1099,8 +1422,38 @@ void SettingsDialog::loadSettings() if (videoDevItem >= 0) videoDev->setCurrentIndex(videoDevItem); + // The resolution and frame rate lists pick the stored size and rate out of + // what the selected camera offers as they are filled, which the line above + // has just triggered where it changed the camera - see + // fillVideoResolutions(). + cbPreferCompressed->setChecked(st.loadValue("video.prefer_compressed", 0).toBool()); + // Call encoding parameters. Read through CallSettings rather than from st + // directly, so that the dialog and a call that is started without ever + // opening it agree on what "not configured" means. + const CallSettings cs = CallSettings::load(); + + int callVideoCodecItem = callVideoCodec->findData(cs.videoCodec); + if (callVideoCodecItem >= 0) + callVideoCodec->setCurrentIndex(callVideoCodecItem); + + callVideoBitrate->setValue(qBound(kMinVideoBitrateKbps, cs.videoBitrateKbps, kMaxVideoBitrateKbps)); + + // Before the codec is selected: selecting one whose bitrate is fixed puts + // that codec's rate in the box and keeps this as the value to come back + // to, see callAudioCodecChanged(). + audioBitrateKbps = qBound(kMinAudioBitrateKbps, cs.audioBitrateKbps, kMaxAudioBitrateKbps); + callAudioBitrate->setValue(audioBitrateKbps); + + int callAudioCodecItem = callAudioCodec->findData(cs.audioCodec); + if (callAudioCodecItem >= 0) + callAudioCodec->setCurrentIndex(callAudioCodecItem); + + // Explicitly, because setCurrentIndex() above only emits when the index + // actually changes - and the stored codec is often the one already showing. + callAudioCodecChanged(); + #ifdef HAVE_VAAPI cbVaapiEncoding->setChecked(st.loadValue("video.vaapi_encoding", 0).toBool()); cbVaapiDecoding->setChecked(st.loadValue("video.vaapi_decoding", 0).toBool()); @@ -1182,7 +1535,22 @@ void SettingsDialog::storeSettings() st.storeValue("multiple_logins.state", QString::number(mlpstate)); st.storeValue("video.capture_device", videoDev->itemData(videoDev->currentIndex()).toString()); + st.storeValue("video.capture_width", QString::number(selectedVideoWidth())); + st.storeValue("video.capture_height", QString::number(selectedVideoHeight())); + st.storeValue("video.capture_framerate", QString::number(selectedVideoFrameRate())); st.storeValue("video.prefer_compressed", cbPreferCompressed->isChecked() ? "1" : "0"); + + st.storeValue("call.video_codec", callVideoCodec->itemData(callVideoCodec->currentIndex()).toString()); + st.storeValue("call.audio_codec", callAudioCodec->itemData(callAudioCodec->currentIndex()).toString()); + st.storeValue("call.video_bitrate_kbps", QString::number(callVideoBitrate->value())); + + // The box shows a fixed rate codec's own bitrate while such a codec is + // selected; what gets stored is the user's choice either way, so that + // switching back to Opus finds it again on the next visit. + if (callAudioBitrate->isEnabled()) + audioBitrateKbps = callAudioBitrate->value(); + + st.storeValue("call.audio_bitrate_kbps", QString::number(audioBitrateKbps)); #ifdef HAVE_VAAPI st.storeValue("video.vaapi_encoding", cbVaapiEncoding->isChecked() ? "1" : "0"); st.storeValue("video.vaapi_decoding", cbVaapiDecoding->isChecked() ? "1" : "0"); |
