1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-01-17 02:55:47 -05:00
This commit is contained in:
Jon Beniston 2026-01-01 18:36:46 +00:00
commit fa2f61c8aa
25 changed files with 1403 additions and 37 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 152 KiB

BIN
doc/img/DATVMod_plugin.xcf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

View File

@ -67,7 +67,7 @@ endif()
# Copied from channelrx/CMakeLists.txt - why not in top-level?
find_package(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE)
if (ENABLE_CHANNELTX_MODDATV AND FFMPEG_FOUND)
if (ENABLE_CHANNELTX_MODDATV AND FFMPEG_FOUND AND OpenCV_FOUND)
add_subdirectory(moddatv)
else()
message(STATUS "Not building moddatv (ENABLE_CHANNELTX_MODDATV=${ENABLE_CHANNELTX_MODDATV} FFMPEG_FOUND=${FFMPEG_FOUND})")

View File

@ -8,6 +8,7 @@ set(moddatv_SOURCES
datvmodplugin.cpp
datvmodsettings.cpp
datvmodwebapiadapter.cpp
tsgenerator.cpp
dvb-s/dvb-s.cpp
dvb-s2/DVB2.cpp
dvb-s2/DVBS2.cpp
@ -31,6 +32,7 @@ set(moddatv_HEADERS
datvmodplugin.h
datvmodsettings.h
datvmodwebapiadapter.h
tsgenerator.h
dvb-s/dvb-s.h
dvb-s/dvb-s.h
dvb-s2/DVB2.h
@ -45,6 +47,7 @@ include_directories(
${AVUTIL_INCLUDE_DIRS}
${SWSCALE_INCLUDE_DIRS}
${SWRESAMPLE_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
)
if(NOT SERVER_MODE)
@ -91,6 +94,7 @@ target_link_libraries(${TARGET_NAME} PRIVATE
${AVUTIL_LIBRARIES}
${SWSCALE_LIBRARIES}
${SWRESAMPLE_LIBRARIES}
${OpenCV_LIBS}
)
if(DEFINED FFMPEG_DEPENDS)

View File

@ -245,7 +245,13 @@ void DATVMod::applySettings(const DATVModSettings& settings, bool force)
<< " m_modulation: " << (int) settings.m_modulation
<< " m_fec: " << (int) settings.m_fec
<< " m_symbolRate: " << settings.m_symbolRate
<< " m_rollOff: " << settings.m_rollOff
<< " m_source: " << settings.m_source
<< " m_imageFileName: " << settings.m_imageFileName
<< " m_imageOverlayTimestamp: " << settings.m_imageOverlayTimestamp
<< " m_imageServiceProvider: " << settings.m_imageServiceProvider
<< " m_imageServiceName: " << settings.m_imageServiceName
<< " m_imageCodec: " << (int) settings.m_imageCodec
<< " m_tsFileName: " << settings.m_tsFileName
<< " m_tsFilePlayLoop: " << settings.m_tsFilePlayLoop
<< " m_tsFilePlay: " << settings.m_tsFilePlay
@ -283,6 +289,21 @@ void DATVMod::applySettings(const DATVModSettings& settings, bool force)
if ((settings.m_tsFilePlayLoop != m_settings.m_tsFilePlayLoop) || force) {
reverseAPIKeys.append("tsSource");
}
if ((settings.m_imageFileName != m_settings.m_imageFileName) || force) {
reverseAPIKeys.append("imageFileName");
}
if ((settings.m_imageOverlayTimestamp != m_settings.m_imageOverlayTimestamp) || force) {
reverseAPIKeys.append("imageOverlayTimestamp");
}
if ((settings.m_imageServiceProvider != m_settings.m_imageServiceProvider) || force) {
reverseAPIKeys.append("imageServiceProvider");
}
if ((settings.m_imageServiceName != m_settings.m_imageServiceName) || force) {
reverseAPIKeys.append("imageServiceName");
}
if ((settings.m_imageCodec != m_settings.m_imageCodec) || force) {
reverseAPIKeys.append("imageCodec");
}
if ((settings.m_tsFileName != m_settings.m_tsFileName) || force) {
reverseAPIKeys.append("tsFileName");
}
@ -452,6 +473,21 @@ void DATVMod::webapiUpdateChannelSettings(
if (channelSettingsKeys.contains("tsSource")) {
settings.m_source = (DATVModSettings::DATVSource) response.getDatvModSettings()->getTsSource();
}
if (channelSettingsKeys.contains("imageFileName")) {
settings.m_imageFileName = *response.getDatvModSettings()->getImageFileName();
}
if (channelSettingsKeys.contains("imageOverlayTimestamp")) {
settings.m_imageOverlayTimestamp = response.getDatvModSettings()->getImageOverlayTimestamp() != 0;
}
if (channelSettingsKeys.contains("imageServiceProvider")) {
settings.m_imageServiceProvider = *response.getDatvModSettings()->getImageServiceProvider();
}
if (channelSettingsKeys.contains("imageServiceName")) {
settings.m_imageServiceName = *response.getDatvModSettings()->getImageServiceName();
}
if (channelSettingsKeys.contains("imageCodec")) {
settings.m_imageCodec = (DATVModSettings::DATVCodec) response.getDatvModSettings()->getImageCodec();
}
if (channelSettingsKeys.contains("tsFileName")) {
settings.m_tsFileName = *response.getDatvModSettings()->getTsFileName();
}
@ -523,6 +559,11 @@ void DATVMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& respo
response.getDatvModSettings()->setSymbolRate(settings.m_symbolRate);
response.getDatvModSettings()->setRollOff(settings.m_rollOff);
response.getDatvModSettings()->setTsSource(settings.m_source);
response.getDatvModSettings()->setImageFileName(new QString(settings.m_imageFileName));
response.getDatvModSettings()->setImageOverlayTimestamp(settings.m_imageOverlayTimestamp ? 1 : 0);
response.getDatvModSettings()->setImageServiceProvider(new QString(settings.m_imageServiceProvider));
response.getDatvModSettings()->setImageServiceName(new QString(settings.m_imageServiceName));
response.getDatvModSettings()->setImageCodec((int)settings.m_imageCodec);
response.getDatvModSettings()->setTsFileName(new QString(settings.m_tsFileName));
response.getDatvModSettings()->setTsFilePlayLoop(settings.m_tsFilePlayLoop ? 1 : 0);
response.getDatvModSettings()->setTsFilePlay(settings.m_tsFilePlay ? 1 : 0);
@ -687,6 +728,24 @@ void DATVMod::webapiFormatChannelSettings(
if (channelSettingsKeys.contains("tsSource") || force) {
swgDATVModSettings->setTsSource((int) settings.m_source);
}
if (channelSettingsKeys.contains("rollOff") || force) {
swgDATVModSettings->setRollOff(settings.m_rollOff);
}
if (channelSettingsKeys.contains("imageFileName") || force) {
swgDATVModSettings->setImageFileName(new QString(settings.m_imageFileName));
}
if (channelSettingsKeys.contains("imageOverlayTimestamp") || force) {
swgDATVModSettings->setImageOverlayTimestamp(settings.m_imageOverlayTimestamp ? 1 : 0);
}
if (channelSettingsKeys.contains("imageServiceProvider") || force) {
swgDATVModSettings->setImageServiceProvider(new QString(settings.m_imageServiceProvider));
}
if (channelSettingsKeys.contains("imageServiceName") || force) {
swgDATVModSettings->setImageServiceName(new QString(settings.m_imageServiceName));
}
if (channelSettingsKeys.contains("imageCodec") || force) {
swgDATVModSettings->setImageCodec((int) settings.m_imageCodec);
}
if (channelSettingsKeys.contains("tsFileName") || force) {
swgDATVModSettings->setTsFileName(new QString(settings.m_tsFileName));
}

View File

@ -301,6 +301,7 @@ void DATVModGUI::on_dvbStandard_currentIndexChanged(int index)
on_modulation_currentIndexChanged(idx);
updateFEC();
setImageBitrate();
m_doApplySettings = true;
@ -377,6 +378,7 @@ void DATVModGUI::on_modulation_currentIndexChanged(int index)
m_settings.m_modulation = (DATVModSettings::DATVModulation) (index + 1);
m_doApplySettings = false;
updateFEC();
setImageBitrate();
m_doApplySettings = true;
applySettings();
}
@ -392,12 +394,14 @@ void DATVModGUI::on_fec_currentIndexChanged(int index)
{
(void) index;
m_settings.m_fec = DATVModSettings::mapCodeRate(ui->fec->currentText());
setImageBitrate();
applySettings();
}
void DATVModGUI::on_symbolRate_valueChanged(int value)
{
m_settings.m_symbolRate = value;
setImageBitrate();
applySettings();
}
@ -421,6 +425,7 @@ void DATVModGUI::setChannelMarkerBandwidth()
void DATVModGUI::on_inputSelect_currentIndexChanged(int index)
{
m_settings.m_source = (DATVModSettings::DATVSource) index;
setImageBitrate();
applySettings();
}
@ -430,6 +435,46 @@ void DATVModGUI::on_channelMute_toggled(bool checked)
applySettings();
}
void DATVModGUI::on_imageFileDialog_clicked(bool checked)
{
(void) checked;
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open image file"), m_settings.m_imageFileName, tr("Image Files (*.bmp *.png *.jpg *.jpeg)"),
nullptr, QFileDialog::DontUseNativeDialog);
if (fileName != "")
{
m_settings.m_imageFileName = fileName;
ui->tsImageFileText->setText(m_settings.m_imageFileName);
m_settings.m_imageFileName = fileName;
applySettings();
}
}
void DATVModGUI::on_tsImageTimestamp_toggled(bool checked)
{
m_settings.m_imageOverlayTimestamp = checked;
applySettings();
}
void DATVModGUI::on_imageServiceProvider_editingFinished()
{
m_settings.m_imageServiceProvider = ui->imageServiceProvider->text();
applySettings();
}
void DATVModGUI::on_imageServiceName_editingFinished()
{
m_settings.m_imageServiceName = ui->imageServiceName->text();
applySettings();
}
void DATVModGUI::on_imageCodec_currentIndexChanged(int index)
{
m_settings.m_imageCodec = (DATVModSettings::DATVCodec) index;
applySettings();
}
void DATVModGUI::on_tsFileDialog_clicked(bool checked)
{
(void) checked;
@ -592,10 +637,20 @@ void DATVModGUI::displaySettings()
ui->inputSelect->setCurrentIndex((int) m_settings.m_source);
if (m_settings.m_imageFileName.isEmpty())
ui->tsImageFileText->setText("...");
else
ui->tsImageFileText->setText(m_settings.m_imageFileName);
if (m_settings.m_tsFileName.isEmpty())
ui->tsFileText->setText("...");
else
ui->tsFileText->setText(m_settings.m_tsFileName);
ui->tsImageTimestamp->setChecked(m_settings.m_imageOverlayTimestamp);
ui->imageServiceProvider->setText(m_settings.m_imageServiceProvider);
ui->imageServiceName->setText(m_settings.m_imageServiceName);
ui->imageCodec->setCurrentIndex((int) m_settings.m_imageCodec);
ui->playFile->setChecked(m_settings.m_tsFilePlay);
ui->playLoop->setChecked(m_settings.m_tsFilePlayLoop);
@ -619,6 +674,19 @@ void DATVModGUI::enterEvent(EnterEventType* event)
ChannelGUI::enterEvent(event);
}
void DATVModGUI::setImageBitrate()
{
if (m_settings.m_source == DATVModSettings::SourceImage)
{
int bitrate = static_cast<int>(m_settings.getDVBSDataBitrate() * 1.1f);
ui->tsImageFileBitrate->setText(QString("%1 kb/s").arg(bitrate/1000.0f, 0, 'f', 2));
}
else
{
ui->tsImageFileBitrate->setText("0kb/s");
}
}
void DATVModGUI::tick()
{
double powDb = CalcDb::dbPower(m_datvMod->getMagSq());
@ -688,6 +756,11 @@ void DATVModGUI::makeUIConnections()
QObject::connect(ui->modulation, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DATVModGUI::on_modulation_currentIndexChanged);
QObject::connect(ui->rollOff, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DATVModGUI::on_rollOff_currentIndexChanged);
QObject::connect(ui->inputSelect, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DATVModGUI::on_inputSelect_currentIndexChanged);
QObject::connect(ui->tsImageDialog, &QPushButton::clicked, this, &DATVModGUI::on_imageFileDialog_clicked);
QObject::connect(ui->tsImageTimestamp, &QCheckBox::toggled, this, &DATVModGUI::on_tsImageTimestamp_toggled);
QObject::connect(ui->imageServiceProvider, &QLineEdit::editingFinished, this, &DATVModGUI::on_imageServiceProvider_editingFinished);
QObject::connect(ui->imageServiceName, &QLineEdit::editingFinished, this, &DATVModGUI::on_imageServiceName_editingFinished);
QObject::connect(ui->imageCodec, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DATVModGUI::on_imageCodec_currentIndexChanged);
QObject::connect(ui->tsFileDialog, &QPushButton::clicked, this, &DATVModGUI::on_tsFileDialog_clicked);
QObject::connect(ui->playFile, &ButtonSwitch::toggled, this, &DATVModGUI::on_playFile_toggled);
QObject::connect(ui->playLoop, &ButtonSwitch::toggled, this, &DATVModGUI::on_playLoop_toggled);

View File

@ -119,7 +119,12 @@ private slots:
void on_rollOff_currentIndexChanged(int index);
void on_inputSelect_currentIndexChanged(int index);
void on_imageFileDialog_clicked(bool checked);
void on_tsFileDialog_clicked(bool checked);
void on_tsImageTimestamp_toggled(bool checked);
void on_imageServiceProvider_editingFinished();
void on_imageServiceName_editingFinished();
void on_imageCodec_currentIndexChanged(int index);
void on_playFile_toggled(bool checked);
void on_playLoop_toggled(bool checked);
@ -132,6 +137,7 @@ private slots:
void onMenuDialogCalled(const QPoint& p);
void configureTsFileName();
void setImageBitrate();
void tick();
};

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>543</width>
<height>235</height>
<width>542</width>
<height>371</height>
</rect>
</property>
<property name="sizePolicy">
@ -46,7 +46,7 @@
<x>0</x>
<y>10</y>
<width>541</width>
<height>221</height>
<height>361</height>
</rect>
</property>
<property name="windowTitle">
@ -357,6 +357,11 @@
<property name="toolTip">
<string>Source of MPEG transport stream to transmit</string>
</property>
<item>
<property name="text">
<string>Image</string>
</property>
</item>
<item>
<property name="text">
<string>File</string>
@ -603,6 +608,138 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="tsImageLayout">
<item>
<widget class="QPushButton" name="tsImageDialog">
<property name="minimumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Open still image</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/picture.png</normaloff>:/picture.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="tsImageFileText">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="tsImageTimestamp">
<property name="toolTip">
<string>Show current time on the image</string>
</property>
<property name="text">
<string>time</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_11">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="tsImageFileBitrate">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Image TS bitrate</string>
</property>
<property name="text">
<string>0kb/s</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="imageServiceProviderLabel">
<property name="text">
<string>Prov</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="imageServiceProvider">
<property name="toolTip">
<string>Service provider for the still image transmission</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="imageServiceNameLabel">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="imageServiceName">
<property name="toolTip">
<string>Service name for the still image transmission</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="imageCodec">
<property name="toolTip">
<string>Codec for image encoding</string>
</property>
<item>
<property name="text">
<string>HEVC</string>
</property>
</item>
<item>
<property name="text">
<string>H264</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="tsFileLayout">
<item>

View File

@ -21,9 +21,11 @@
#include "util/simpleserializer.h"
#include "settings/serializable.h"
#include "datvmodsettings.h"
#include "dvb-s/dvb-s.h"
const QStringList DATVModSettings::m_codeRateStrings = {"1/2", "2/3", "3/4", "5/6", "7/8", "4/5", "8/9", "9/10", "1/4", "1/3", "2/5", "3/5"};
const QStringList DATVModSettings::m_modulationStrings = {"BPSK", "QPSK", "8PSK", "16APSK", "32APSK"};
const QStringList DATVModSettings::m_codecStrings = {"HEVC", "H264"};
DATVModSettings::DATVModSettings() :
m_channelMarker(nullptr),
@ -42,6 +44,11 @@ void DATVModSettings::resetToDefaults()
m_symbolRate = 250000;
m_rollOff = 0.35f;
m_source = SourceFile;
m_imageFileName = "";
m_imageOverlayTimestamp = false;
m_imageServiceProvider = "SDRangel";
m_imageServiceName = "SDRangel_TV";
m_imageCodec = CodecHEVC;
m_tsFileName = "";
m_tsFilePlayLoop = false;
m_tsFilePlay = false;
@ -97,6 +104,11 @@ QByteArray DATVModSettings::serialize() const
s.writeS32(30, m_workspaceIndex);
s.writeBlob(31, m_geometryBytes);
s.writeBool(32, m_hidden);
s.writeString(33, m_imageFileName);
s.writeBool(34, m_imageOverlayTimestamp);
s.writeString(35, m_imageServiceProvider);
s.writeString(36, m_imageServiceName);
s.writeS32(37, (int) m_imageCodec);
return s.final();
}
@ -171,6 +183,11 @@ bool DATVModSettings::deserialize(const QByteArray& data)
d.readS32(30, &m_workspaceIndex, 0);
d.readBlob(31, &m_geometryBytes);
d.readBool(32, &m_hidden, false);
d.readString(33, &m_imageFileName, "");
d.readBool(34, &m_imageOverlayTimestamp, false);
d.readString(35, &m_imageServiceProvider, "SDRangel");
d.readString(36, &m_imageServiceName, "SDRangel_TV");
d.readS32(37, (int *)&m_imageCodec, (int)DATVModSettings::CodecHEVC);
return true;
}
@ -210,3 +227,131 @@ QString DATVModSettings::mapModulation(DATVModulation modulation)
{
return m_modulationStrings[modulation];
}
int DATVModSettings::getDVBSDataBitrate() const
{
float fecFactor;
float plFactor;
float bitsPerSymbol;
switch (m_modulation)
{
case DATVModSettings::BPSK:
bitsPerSymbol = 1.0f;
break;
case DATVModSettings::QPSK:
bitsPerSymbol = 2.0f;
break;
case DATVModSettings::PSK8:
bitsPerSymbol = 3.0f;
break;
case DATVModSettings::APSK16:
bitsPerSymbol = 4.0f;
break;
case DATVModSettings::APSK32:
bitsPerSymbol = 5.0f;
break;
}
if (m_standard == DATVModSettings::DVB_S)
{
float rsFactor;
float convFactor;
rsFactor = DVBS::tsPacketLen/(float)DVBS::rsPacketLen;
switch (m_fec)
{
case DATVModSettings::FEC12:
convFactor = 1.0f/2.0f;
break;
case DATVModSettings::FEC23:
convFactor = 2.0f/3.0f;
break;
case DATVModSettings::FEC34:
convFactor = 3.0f/4.0f;
break;
case DATVModSettings::FEC56:
convFactor = 5.0f/6.0f;
break;
case DATVModSettings::FEC78:
convFactor = 7.0f/8.0f;
break;
case DATVModSettings::FEC45:
convFactor = 4.0f/5.0f;
break;
case DATVModSettings::FEC89:
convFactor = 8.0f/9.0f;
break;
case DATVModSettings::FEC910:
convFactor = 9.0f/10.0f;
break;
case DATVModSettings::FEC14:
convFactor = 1.0f/4.0f;
break;
case DATVModSettings::FEC13:
convFactor = 1.0f/3.0f;
break;
case DATVModSettings::FEC25:
convFactor = 2.0f/5.0f;
break;
case DATVModSettings::FEC35:
convFactor = 3.0f/5.0f;
break;
}
fecFactor = rsFactor * convFactor;
plFactor = 1.0f;
}
else
{
// For normal frames
int codedBlockSize = 64800;
int bbHeaderBits = 80;
int uncodedBlockSize = codedBlockSize + bbHeaderBits; // Yields a fec factor of 1.0 if no FEC applied
// See table 5a in DVBS2 spec
switch (m_fec)
{
case DATVModSettings::FEC12:
uncodedBlockSize = 32208;
break;
case DATVModSettings::FEC23:
uncodedBlockSize = 43040;
break;
case DATVModSettings::FEC34:
uncodedBlockSize = 48408;
break;
case DATVModSettings::FEC56:
uncodedBlockSize = 53840;
break;
case DATVModSettings::FEC45:
uncodedBlockSize = 51648;
break;
case DATVModSettings::FEC89:
uncodedBlockSize = 57472;
break;
case DATVModSettings::FEC910:
uncodedBlockSize = 58192;
break;
case DATVModSettings::FEC14:
uncodedBlockSize = 16008;
break;
case DATVModSettings::FEC13:
uncodedBlockSize = 21408;
break;
case DATVModSettings::FEC25:
uncodedBlockSize = 25728;
break;
case DATVModSettings::FEC35:
uncodedBlockSize = 38688;
break;
default:
qWarning("DATVModSettings::getDVBSDataBitrate: Unsupported DVB-S2 code rate");
break;
}
fecFactor = (uncodedBlockSize-bbHeaderBits)/(float)codedBlockSize;
float symbolsPerFrame = codedBlockSize/bitsPerSymbol;
// 90 symbols for PL header
plFactor = symbolsPerFrame / (symbolsPerFrame + 90.0f);
}
return std::round(m_symbolRate * bitsPerSymbol * fecFactor * plFactor);
}

View File

@ -29,6 +29,7 @@ struct DATVModSettings
{
enum DATVSource
{
SourceImage,
SourceFile,
SourceUDP
};
@ -66,8 +67,15 @@ struct DATVModSettings
};
static const QStringList m_codeRateStrings;
enum DATVCodec
{
CodecHEVC,
CodecH264
};
static const QStringList m_codecStrings;
qint64 m_inputFrequencyOffset; //!< Offset from baseband center frequency
Real m_rfBandwidth; //!< Bandwidth of modulated signal
float m_rfBandwidth; //!< Bandwidth of modulated signal
DVBStandard m_standard;
DATVModulation m_modulation; //!< RF modulation type
@ -77,6 +85,11 @@ struct DATVModSettings
DATVSource m_source; //!< Source of transport stream
QString m_imageFileName; //!< Still image file name
bool m_imageOverlayTimestamp; //!< Overlay timestamp on still image
QString m_imageServiceProvider; //!< Service provider name for still image
QString m_imageServiceName; //!< Service name for still image
DATVCodec m_imageCodec; //!< Codec for transport stream encoding of still image
QString m_tsFileName;
bool m_tsFilePlayLoop; //!< Play TS file in a loop
bool m_tsFilePlay; //!< True to play TS file and false to pause
@ -107,6 +120,7 @@ struct DATVModSettings
void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; }
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
int getDVBSDataBitrate() const;
static DATVCodeRate mapCodeRate(const QString& string);
static QString mapCodeRate(DATVCodeRate codeRate);

View File

@ -374,7 +374,36 @@ void DATVModSource::modulateSample()
m_udpByteCount += ba.size();
m_udpAbsByteCount += ba.size();
}
else
else if (m_settings.m_source == DATVModSettings::SourceImage)
{
if (m_frameCount == static_cast<int>(m_tsGenerator.get_buffer_size()/sizeof(m_mpegTS)))
{
int bitrate = static_cast<int>(getDVBSDataBitrate(m_settings) * 1.1f); // Add 10% margin
m_tsGenerator.generate_still_image_ts(m_settings.m_imageFileName.toStdString().c_str(), bitrate, m_settings.m_imageOverlayTimestamp, 1);
m_tsFileOK = true;
m_frameIdx = 0;
m_frameCount = 0;
}
// Read transport stream packet from generated image TS
uint8_t *tsPacket = m_tsGenerator.next_ts_packet();
if (tsPacket != nullptr)
{
memcpy(m_mpegTS, tsPacket, sizeof(m_mpegTS));
m_frameIdx++;
m_frameCount++;
}
else
{
// No more data
memset(m_mpegTS, 0xFF, sizeof(m_mpegTS));
m_mpegTS[0] = 0x47; // Sync byte
m_mpegTS[1] = 0x1F;
m_mpegTS[2] = 0xFF;
m_mpegTS[3] = 0x10;
}
}
else // Unsupported source or no more data
{
// Insert null packet. PID=0x1fff
memset(m_mpegTS, 0xFF, sizeof(m_mpegTS));
@ -670,6 +699,11 @@ void DATVModSource::applySettings(const DATVModSettings& settings, bool force)
<< " m_fec: " << (int) settings.m_fec
<< " m_symbolRate: " << (int) settings.m_symbolRate
<< " m_rollOff: " << (int) settings.m_rollOff
<< " m_imageFileName: " << settings.m_imageFileName
<< " m_imageOverlayTimestamp: " << settings.m_imageOverlayTimestamp
<< " m_imageServiceProvider: " << settings.m_imageServiceProvider
<< " m_imageServiceName: " << settings.m_imageServiceName
<< " m_tsFileName: " << settings.m_tsFileName
<< " m_tsFilePlayLoop: " << settings.m_tsFilePlayLoop
<< " m_tsFilePlay: " << settings.m_tsFilePlay
<< " m_udpAddress: " << settings.m_udpAddress
@ -850,6 +884,18 @@ void DATVModSource::applySettings(const DATVModSettings& settings, bool force)
}
}
if (settings.m_imageServiceProvider != m_settings.m_imageServiceProvider || force) {
m_tsGenerator.set_service_provider(settings.m_imageServiceProvider.toStdString());
}
if (settings.m_imageServiceName != m_settings.m_imageServiceName || force) {
m_tsGenerator.set_service_name(settings.m_imageServiceName.toStdString());
}
if (settings.m_imageCodec != m_settings.m_imageCodec || force) {
m_tsGenerator.set_codec(settings.m_imageCodec);
}
m_settings = settings;
if (m_settings.m_symbolRate > 0)

View File

@ -24,9 +24,6 @@
#include <iostream>
#include <cstdint>
#include <QObject>
#include <QMutex>
#include "dsp/channelsamplesource.h"
#include "dsp/nco.h"
#include "dsp/interpolator.h"
@ -37,6 +34,7 @@
#include "dvb-s/dvb-s.h"
#include "dvb-s2/DVBS2.h"
#include "tsgenerator.h"
class MessageQueue;
class QUdpSocket;
@ -126,6 +124,7 @@ private:
Real m_levelSum;
bool m_tsFileOK;
TSGenerator m_tsGenerator;
MessageQueue *m_messageQueueToGUI;
@ -141,7 +140,6 @@ private:
void updateUDPBufferUtilization();
MessageQueue *getMessageQueueToGUI() { return m_messageQueueToGUI; }
};

View File

@ -46,80 +46,114 @@ Average total power in dB relative to a &#177;1.0 amplitude signal generated in
Use this button to toggle mute for this channel. The radio waves on the icon are toggled on (active) and off (muted) accordingly. Default is channel active.
<h3>6: Standard</h3>
<h3>A. DATV transmission details</h3>
![DATV Modulator plugin GUI A](../../../doc/img/DATVMod_plugin_A.png)
<h3>A.1: Standard</h3>
Select the DVB standard to use for channel coding and modulation. This can be either DVB-S or DVB-S2.
<h3>7: Symbol rate</h3>
<h3>A.2: Symbol rate</h3>
Specifies the symbol rate in symbols per second. Higher symbol rates allow for higher bitrate transport streams to be transmitted, but require a greater bandwidth.
<h3>8: Bandwidth</h3>
<h3>A.3: Bandwidth</h3>
Specifies the bandwidth of a filter applied to the modulated output signal when interpolation takes place (i.e. when the sample rate (2) is not equal to the baseband sample rate). Otherwise the full baseband bandwidth is used.
<h3>9: Transport Stream Source</h3>
<h3>A.4: Transport Stream Source</h3>
This combo box lets you choose the source of the MPEG transport stream:
- Image: still image
- File: transport stream file read from the file selected with button (16).
- UDP: transport stream received via UDP port (14).
When using UDP, the packet size should be an integer multiple of the MPEG transport stream packet size, which is 188 bytes. 1316 bytes is a common value.
<h3>10: FEC</h3>
<h3>A.5: FEC</h3>
Forward error correction code rate. This controls the number of bits sent to help the receiver to correct errors.
A code rate of 1/2 has the highest overhead (corresponding to a lower data rate), but allows the most amount of errors to be correct.
7/8 (DVB-S) or 9/10 (DVB-S2) has the least overhead (corresponding to higher data rates), but will allow the fewest amount of errors to be corrected.
<h3>11: Modulation</h3>
<h3>A.6: Modulation</h3>
Select the modulation to be used. For DVB-S, this can either be BPSK or QPSK. For DVB-S2, this can be QPSK, 8PSK, 16APSK or 32PSK.
BPSK transmits a single bit per symbol, whereas QPSK transmits two bits per symbol, so has twice the bitrate. Similar, 8PSK is 3 bits per symbol, 16APSK 4 and 32PSK 5. BPSK, QPSK and 8PSK only modulate phase. 16APSK and 32APKS modulate both phase and amplitude.
<h3>12: Roll off</h3>
<h3>A.7: Roll off</h3>
Roll-off for the root raised cosine filter. For DVB-S, this should be 0.35. For DVB-S2 this can be 0.2, 0.25 or 0.35.
<h3>13: UDP IP address</h3>
<h3>6: UDP IP address</h3>
Set the IP address of the network interface/adaptor to bind the UDP socket to.
<h3>14: UDP port</h3>
<h3>7: UDP port</h3>
Set the UDP port number the UDP socket will be opened on. This is the port the transport stream will need to be sent to.
<h3>15: UDP bitrate</h3>
<h3>8: UDP bitrate</h3>
This displays the bitrate at which data is being received via the UDP port.
<h3>16: Transport stream file select</h3>
<h3>B. Still image details</h3>
![DATV Modulator plugin GUI B](../../../doc/img/DATVMod_plugin_B.png)
<h3>B.1: Select image</h3>
This button opens a file dialog that lets you select an image file
<h3>B.2: Path to the image file</h3>
<h3>B.3: Time display overlay</h3>
When checked show the time in the top left corner updating approximately every second
<h3>B.4: Transport Stream bitrate</h3>
<h3>B.5: Service provider</h3>
<h3>B.6: Service name</h3>
<h3>B.7: Codec</h3>
You have the choice between two codecs to encode the image:
- **HEVC**: a.k.a. H265
- **H264**
<h3>9: Transport stream file select</h3>
Clicking on this button will open a file dialog to let you choose an MPEG transport stream file to transmit. When the dialog is closed and the choice is validated the name of the file will appear on the space at the right of the button.
<h3>17: Play loop</h3>
<h3>10: Path to the transport stream file</h3>
Use this button to toggle on/off transmitting of the transport stream file in a loop.
<h3>18: Play/Pause</h3>
Use this button to play or pause the transport stream file.
<h3>19: Current transport stream file position</h3>
This is the current transport stream file position in time units relative to the start.
<h3>20: Transport stream file bitrate</h3>
<h3>11: Transport stream bitrate</h3>
This is the bitrate in kb/s of the transport stream file. This should be less or equal to the DVB data rate (3).
<h3>21: Transport stream file length</h3>
<h3>12: Play loop</h3>
Use this button to toggle on/off transmitting of the transport stream file in a loop.
<h3>13: Play/Pause</h3>
Use this button to play or pause the transport stream file.
<h3>14: Current transport stream file position</h3>
This is the current transport stream file position in time units relative to the start.
<h3>15: Transport stream file length</h3>
This is the length of the transport stream file in time units
<h3>22: Transport stream file position slider</h3>
<h3>16: Transport stream file position slider</h3>
This slider can be used to randomly set the current position in the file when file play is in pause state (button 18). When the transport stream is transmitted, the slider moves according to the current position.

View File

@ -0,0 +1,556 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Edouard Griffiths, F4EXB. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include <libavutil/channel_layout.h>
#include <libavutil/common.h>
#include <libavutil/imgutils.h>
#include <libavutil/mathematics.h>
#include <libavutil/opt.h>
#include <libavutil/samplefmt.h>
#include "libswscale/swscale.h"
}
#include <opencv2/opencv.hpp> // Add OpenCV for text overlay
#include "tsgenerator.h"
const double TSGenerator::rate_qpsk[11][4] = {{1.0, 4.0, 12.0, 2}, {1.0, 3.0, 12.0, 2}, {2.0, 5.0, 12.0, 2}, {1.0, 2.0, 12.0, 2}, {3.0, 5.0, 12.0, 2}, {2.0, 3.0, 10.0, 2}, {3.0, 4.0, 12.0, 2}, {4.0, 5.0, 12.0, 2}, {5.0, 6.0, 10.0, 2}, {8.0, 9.0, 8.0, 2}, {9.0, 10.0, 8.0, 1}};
const double TSGenerator::rate_8psk[6][4] = {{3.0, 5.0, 12.0, 2}, {2.0, 3.0, 10.0, 2}, {3.0, 4.0, 12.0, 2}, {5.0, 6.0, 10.0, 2}, {8.0, 9.0, 8.0, 2}, {9.0, 10.0, 8.0, 1}};
const double TSGenerator::rate_16apsk[6][4] = {{2.0, 3.0, 10.0, 2}, {3.0, 4.0, 12.0, 2}, {4.0, 5.0, 12.0, 2}, {5.0, 6.0, 10.0, 2}, {8.0, 9.0, 8.0, 2}, {9.0, 10.0, 8.0, 1}};
const double TSGenerator::rate_32apsk[5][4] = {{3.0, 4.0, 12.0, 2}, {4.0, 5.0, 12.0, 2}, {5.0, 6.0, 10.0, 2}, {8.0, 9.0, 8.0, 2}, {9.0, 10.0, 8.0, 1}};
uint8_t TSGenerator::continuity_counters[8192] = {0};
TSGenerator::TSGenerator()
{
}
void TSGenerator::generate_still_image_ts(const char* image_path, int bitrate, bool overlay_timestamp, int duration_sec)
{
printf("TSGenerator::generate_still_image_ts: Generating TS from image %s at bitrate %d bps for %d seconds\n",
image_path, bitrate, duration_sec);
ts_buffer.clear();
// 1. Setup (one-time)
AVFrame* frame = load_image_to_yuv_with_opencv(image_path, 1280, 720, overlay_timestamp);
// 1. SELECT CODEC
auto [codec, ctx] = create_codec_context(25, bitrate, 1280, 720, duration_sec);
if (!codec || !ctx)
return;
avcodec_open2(ctx, codec, nullptr);
// 2. In-memory TS context
AVFormatContext* oc = nullptr;
int stream_idx = setup_ts_context(&oc, ctx);
avformat_write_header(oc, nullptr);
// 3. Generate fixed duration TS packets
int64_t pts = 0;
int64_t total_frames = 25 * duration_sec; // 25fps
for (int64_t i = 0; i < total_frames; i++)
{
frame->pts = pts++;
encode_frame_to_ts(oc, ctx, frame, stream_idx);
}
av_write_trailer(oc); // Finalize TS (PAT/PMT)
// ts_buffer now contains complete, seekable TS packets
printf("TSGenerator::generate_still_image_ts: Generated %zu bytes TS buffer\n", ts_buffer.size());
buffer_size = ts_buffer.size();
// 4. Cleanup
if (frame) av_frame_free(&frame);
if (ctx) avcodec_free_context(&ctx);
if (oc) avformat_free_context(oc);
}
AVFrame* TSGenerator::load_image_to_yuv(const char* filename, int width, int height)
{
AVFormatContext* fmt_ctx = nullptr;
AVCodecContext* codec_ctx = nullptr;
AVFrame* frame = nullptr;
AVFrame* yuv_frame = nullptr;
struct SwsContext* sws_ctx = nullptr;
AVPacket pkt;
int stream_idx = -1;
AVStream* stream = nullptr;
const AVCodec* codec = nullptr;
// NOW all gotos are safe - no declarations bypassed
if (avformat_open_input(&fmt_ctx, filename, nullptr, nullptr) < 0) {
fprintf(stderr, "TSGenerator::load_image_to_yuv Could not open image %s\n", filename);
return nullptr;
}
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
goto cleanup;
}
stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (stream_idx < 0) goto cleanup;
stream = fmt_ctx->streams[stream_idx];
codec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!codec) goto cleanup;
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx || avcodec_parameters_to_context(codec_ctx, stream->codecpar) < 0) {
goto cleanup;
}
if (avcodec_open2(codec_ctx, codec, nullptr) < 0) goto cleanup;
// Decode single frame
av_init_packet(&pkt);
if (av_read_frame(fmt_ctx, &pkt) < 0) goto cleanup;
frame = av_frame_alloc();
if (frame && avcodec_send_packet(codec_ctx, &pkt) >= 0) {
avcodec_receive_frame(codec_ctx, frame);
}
av_packet_unref(&pkt);
if (!frame) goto cleanup;
// Convert to YUV420P
yuv_frame = av_frame_alloc();
if (!yuv_frame) goto cleanup;
yuv_frame->format = AV_PIX_FMT_YUV420P;
yuv_frame->width = width ? width : frame->width;
yuv_frame->height = height ? height : frame->height;
if (av_frame_get_buffer(yuv_frame, 32) < 0) goto cleanup;
sws_ctx = sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format,
yuv_frame->width, yuv_frame->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (sws_ctx) {
sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height,
yuv_frame->data, yuv_frame->linesize);
sws_freeContext(sws_ctx);
}
cleanup:
if (frame) av_frame_free(&frame);
if (codec_ctx) avcodec_free_context(&codec_ctx);
if (fmt_ctx) avformat_close_input(&fmt_ctx);
return yuv_frame;
}
AVFrame* TSGenerator::load_image_to_yuv_with_opencv(const char* filename, int width, int height, bool overlay_timestamp) {
// 1. Load image with OpenCV (simpler, supports text overlay)
cv::Mat rgb_image = cv::imread(filename);
if (rgb_image.empty()) {
fprintf(stderr, "TSGenerator::load_image_to_yuv_with_opencv Failed to load image: %s\n", filename);
return nullptr;
}
// 2. OPTIONAL: Overlay timestamp
if (overlay_timestamp) {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
char time_str[32];
struct tm time_buffer; // Your own buffer
std::strftime(time_str, sizeof(time_str), "%H:%M:%S", localtime_r(&time_t, &time_buffer));
// Draw black background box first
cv::rectangle(rgb_image, cv::Point(15, 28), cv::Point(170, 65),
cv::Scalar(0, 0, 0), -1);
// Draw white timestamp text
cv::putText(rgb_image, time_str, cv::Point(20, 55),
cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(255, 255, 255), 2);
}
// 3. BGR → RGB24
cv::Mat rgb24;
cv::cvtColor(rgb_image, rgb24, cv::COLOR_BGR2RGB);
// 4. Create FFmpeg RGB frame
AVFrame* rgb_frame = av_frame_alloc();
rgb_frame->format = AV_PIX_FMT_RGB24;
rgb_frame->width = rgb24.cols;
rgb_frame->height = rgb24.rows;
if (av_frame_get_buffer(rgb_frame, 32) < 0) {
av_frame_free(&rgb_frame);
return nullptr;
}
// FIXED pixel copy with proper alignment
int linesize = rgb_frame->linesize[0]; // FFmpeg padded linesize
int src_width_bytes = rgb24.cols * 3; // OpenCV tight-packed
for (int y = 0; y < rgb24.rows; y++) {
uint8_t* dst_line = rgb_frame->data[0] + y * linesize;
const uint8_t* src_line = rgb24.data + y * rgb24.step;
// Copy exactly src_width_bytes, pad rest with black
memcpy(dst_line, src_line, src_width_bytes);
memset(dst_line + src_width_bytes, 0, linesize - src_width_bytes);
}
// 5. Convert RGB24 → YUV420P
AVFrame* yuv_frame = av_frame_alloc();
yuv_frame->format = AV_PIX_FMT_YUV420P;
yuv_frame->width = width ? width : rgb_frame->width;
yuv_frame->height = height ? height : rgb_frame->height;
if (av_frame_get_buffer(yuv_frame, 32) < 0) {
av_frame_free(&rgb_frame);
av_frame_free(&yuv_frame);
return nullptr;
}
SwsContext* sws_ctx = sws_getContext(rgb_frame->width, rgb_frame->height, AV_PIX_FMT_RGB24,
yuv_frame->width, yuv_frame->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (sws_ctx) {
sws_scale(sws_ctx, rgb_frame->data, rgb_frame->linesize, 0, rgb_frame->height,
yuv_frame->data, yuv_frame->linesize);
sws_freeContext(sws_ctx);
}
av_frame_free(&rgb_frame);
return yuv_frame;
}
AVCodecContext* TSGenerator::configure_hevc_context(const AVCodec* codec, int fps, int bitrate, int width, int height, int duration_sec)
{
AVCodecContext* ctx = avcodec_alloc_context3(codec);
if (!ctx) return nullptr;
// Basic video parameters
ctx->width = width;
ctx->height = height;
ctx->pix_fmt = AV_PIX_FMT_YUV420P; // Required for HEVC
ctx->time_base = {1, fps}; // 1/fps
ctx->framerate = {fps, 1}; // fps/1
// Encoding parameters (matching your CLI)
ctx->bit_rate = bitrate; // 1000k = 1Mbps
ctx->gop_size = 25 * duration_sec;
ctx->max_b_frames = 0; // Low latency (no B-frames)
ctx->rc_buffer_size = bitrate * 2; // Buffer size
// HEVC-specific tuning
av_opt_set(ctx->priv_data, "preset", "fast", 0);
av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);
return ctx;
}
AVCodecContext* TSGenerator::configure_h264_context(const AVCodec* codec, int fps, int bitrate, int width, int height, int duration_sec)
{
AVCodecContext* ctx = avcodec_alloc_context3(codec);
if (!ctx) return nullptr;
// Basic video parameters
ctx->width = width;
ctx->height = height;
ctx->pix_fmt = AV_PIX_FMT_YUV420P; // Required for H.264
ctx->time_base = {1, fps}; // 1/fps
ctx->framerate = {fps, 1}; // fps/1
// Encoding parameters
ctx->bit_rate = bitrate;
ctx->gop_size = 25 * duration_sec;
ctx->max_b_frames = 0; // Low latency
ctx->rc_buffer_size = bitrate * 2;
// H.264-specific tuning
av_opt_set(ctx->priv_data, "preset", "ultrafast", 0);
av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);
return ctx;
}
int TSGenerator::setup_ts_context(AVFormatContext** oc, AVCodecContext* codec_ctx)
{
// 1. Allocate MPEG-TS format context
if (avformat_alloc_output_context2(oc, nullptr, "mpegts", nullptr) < 0) {
return -1;
}
// Set TS metadata (optional)
av_dict_set(&(*oc)->metadata, "service_provider", service_provider.c_str(), 0);
av_dict_set(&(*oc)->metadata, "service_name", service_name.c_str(), 0);
// 2. Create in-memory buffer for TS packets
unsigned char* iobuf = (unsigned char*)av_mallocz(32768);
if (!iobuf) return -1;
AVIOContext* avio_ctx = avio_alloc_context(
iobuf, 32768, // Buffer size
1, // Write mode
&ts_buffer, // Your std::vector<uint8_t>*
read_packet_cb, // Read callback (unused)
write_packet_cb, // Write callback → your memory buffer
seek_cb // Seek callback (unused)
);
if (!avio_ctx) {
av_free(iobuf);
return -1;
}
(*oc)->pb = avio_ctx;
// 3. Add video stream
AVStream* stream = avformat_new_stream(*oc, nullptr);
if (!stream) return -1;
stream->id = (*oc)->nb_streams - 1;
stream->time_base = codec_ctx->time_base;
// Copy codec parameters to stream
if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) {
return -1;
}
return stream->id;
}
void TSGenerator::encode_frame_to_ts(AVFormatContext* oc, AVCodecContext* codec_ctx, AVFrame* frame, int stream_idx)
{
AVPacket pkt = {nullptr};
int ret;
// 1. Send frame to HEVC encoder
ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0) {
fprintf(stderr, "TSGenerator::encode_frame_to_ts Error sending frame to encoder\n");
return;
}
// 2. Receive encoded packets (may produce multiple per frame)
while (ret >= 0) {
ret = avcodec_receive_packet(codec_ctx, &pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break; // No more packets ready
} else if (ret < 0) {
fprintf(stderr, "TSGenerator::encode_frame_to_ts Error receiving packet from encoder\n");
break;
}
// 3. Rescale PTS to stream timebase
pkt.stream_index = stream_idx;
av_packet_rescale_ts(&pkt, codec_ctx->time_base, oc->streams[stream_idx]->time_base);
// 4. Write TS packet to your memory buffer
ret = av_interleaved_write_frame(oc, &pkt);
if (ret < 0) {
fprintf(stderr, "TSGenerator::encode_frame_to_ts Error writing frame to TS\n");
}
av_packet_unref(&pkt);
}
}
// Write callback - stores TS packets in memory
int TSGenerator::write_packet_cb(void* opaque, uint8_t* buf, int buf_size)
{
std::vector<uint8_t>* buffer = static_cast<std::vector<uint8_t>*>(opaque);
buffer->insert(buffer->end(), buf, buf + buf_size);
return buf_size;
}
int TSGenerator::read_packet_cb(void* opaque, unsigned char* buf, int buf_size)
{
(void) opaque;
(void) buf;
(void) buf_size;
return AVERROR_EOF;
}
int64_t TSGenerator::seek_cb(void* opaque, int64_t offset, int whence)
{
(void) opaque;
(void) offset;
(void) whence;
return -1;
}
double TSGenerator::get_dvbs2_rate(double symbol_rate, DATVModSettings::DATVModulation modulation, DATVModSettings::DATVCodeRate code_rate)
{
const double (*rate_table)[4] = nullptr;
int table_size = 0;
double fec_num, fec_den, bits;
switch(code_rate)
{
case DATVModSettings::FEC12:
fec_num = 1.0;
fec_den = 2.0;
break;
case DATVModSettings::FEC23:
fec_num = 2.0;
fec_den = 3.0;
break;
case DATVModSettings::FEC34:
fec_num = 3.0;
fec_den = 4.0;
break;
case DATVModSettings::FEC45:
fec_num = 4.0;
fec_den = 5.0;
break;
case DATVModSettings::FEC56:
fec_num = 5.0;
fec_den = 6.0;
break;
case DATVModSettings::FEC78:
fec_num = 7.0;
fec_den = 8.0;
break;
case DATVModSettings::FEC89:
fec_num = 8.0;
fec_den = 9.0;
break;
case DATVModSettings::FEC910:
fec_num = 9.0;
fec_den = 10.0;
break;
case DATVModSettings::FEC14:
fec_num = 1.0;
fec_den = 4.0;
break;
case DATVModSettings::FEC13:
fec_num = 1.0;
fec_den = 3.0;
break;
case DATVModSettings::FEC25:
fec_num = 2.0;
fec_den = 5.0;
break;
case DATVModSettings::FEC35:
fec_num = 3.0;
fec_den = 5.0;
break;
default:
return symbol_rate * (fec_num / fec_den); // others
}
switch (modulation) {
case DATVModSettings::QPSK:
bits = 2.0;
rate_table = TSGenerator::rate_qpsk;
table_size = 11;
break;
case DATVModSettings::PSK8:
bits = 3.0;
rate_table = TSGenerator::rate_8psk;
table_size = 6;
break;
case DATVModSettings::APSK16:
bits = 4.0;
rate_table = TSGenerator::rate_16apsk;
table_size = 6;
break;
case DATVModSettings::APSK32:
bits = 5.0;
rate_table = TSGenerator::rate_32apsk;
table_size = 5;
break;
default:
return symbol_rate * (fec_num / fec_den); // BPSK and others
}
for (int i = 0; i < table_size; i++) {
if (rate_table[i][0] == fec_num && rate_table[i][1] == fec_den) {
return TSGenerator::calc_dvbs2_rate(symbol_rate, bits, fec_num, fec_den, rate_table[i][2], 0.0);
}
}
return symbol_rate * (fec_num / fec_den); // others
}
double TSGenerator::calc_dvbs2_rate(double symbol_rate, double bits, double fec_num, double fec_den, double bch, double pilots)
{
double fec_frame = 64800.0;
double tsrate;
tsrate = symbol_rate / (fec_frame / bits + 90 + ceil(fec_frame/ bits / 90 / 16 - 1) * pilots) * (fec_frame * (fec_num / fec_den) - (16 * bch) - 80);
return (tsrate);
}
uint8_t *TSGenerator::next_ts_packet()
{
static size_t read_pos = 0;
const size_t ts_packet_size = 188;
if (read_pos + ts_packet_size > buffer_size) {
read_pos = 0; // Loop back to start
}
uint8_t* packet = ts_buffer.data() + read_pos;
read_pos += ts_packet_size;
if (packet[0] != 0x47) { // no sync byte
return packet;
}
// PATCH continuity counter (byte 3, bits 0-3)
uint16_t pid = ((packet[1] & 0x1F) << 8) | packet[2];
uint8_t& cc = continuity_counters[pid];
// Clear old CC, set new CC
packet[3] = (packet[3] & 0xF0) | cc;
// Increment CC (rolls over 0-15)
cc = (cc + 1) & 0x0F;
return packet;
}
std::pair<const AVCodec*, AVCodecContext*> TSGenerator::create_codec_context(int fps, int bitrate, int width, int height, int duration_sec)
{
const AVCodec* codec = nullptr;
AVCodecContext* ctx = nullptr;
switch (m_codecType)
{
case DATVModSettings::CodecH264:
codec = avcodec_find_encoder_by_name("libx264");
ctx = configure_h264_context(codec, fps, bitrate, width, height, duration_sec);
break;
case DATVModSettings::CodecHEVC:
default:
codec = avcodec_find_encoder_by_name("libx265");
ctx = configure_hevc_context(codec, fps, bitrate, width, height, duration_sec);
break;
}
if (!codec || !ctx) {
fprintf(stderr, "TSGenerator::create_codec_context: Codec not available\n");
return {nullptr, nullptr};
}
return {codec, ctx};
}

View File

@ -0,0 +1,77 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Edouard Griffiths, F4EXB. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_ATVMOD_TSGENERATOR_H
#define INCLUDE_ATVMOD_TSGENERATOR_H
#include <vector>
#include <cstdint>
#include <string>
#include "datvmodsettings.h"
struct AVFrame;
struct AVFormatContext;
struct AVCodecContext;
struct AVCodec;
class TSGenerator
{
public:
enum class CodecType {
HEVC, // libx265
H264, // libx264
};
TSGenerator();
~TSGenerator() = default;
void generate_still_image_ts(const char* image_path, int bitrate, bool overlay_timestamp, int duration_sec = 1);
void set_service_provider(const std::string& provider) { service_provider = provider; }
void set_service_name(const std::string& name) { service_name = name; }
uint8_t *next_ts_packet();
int get_buffer_size() const { return static_cast<int>(buffer_size); }
void set_codec(DATVModSettings::DATVCodec codec) { m_codecType = codec; }
private:
std::vector<uint8_t> ts_buffer; // Complete TS packets (e.g., 10s worth)
size_t buffer_size = 0;
size_t write_pos = 0;
std::string service_provider = "SDRangel";
std::string service_name = "SDRangel_TV";
bool m_generateImage = false;
DATVModSettings::DATVCodec m_codecType = DATVModSettings::CodecHEVC;
static const double rate_qpsk[11][4];
static const double rate_8psk[6][4];
static const double rate_16apsk[6][4];
static const double rate_32apsk[5][4];
static uint8_t continuity_counters[8192]; // One per PID (0-8191)
AVFrame* load_image_to_yuv(const char* filename, int width, int height);
AVFrame* load_image_to_yuv_with_opencv(const char* filename, int width, int height, bool overlay_timestamp = false);
AVCodecContext* configure_hevc_context(const AVCodec* codec, int fps, int bitrate, int width, int height, int duration_sec);
AVCodecContext* configure_h264_context(const AVCodec* codec, int fps, int bitrate, int width, int height, int duration_sec);
int setup_ts_context(AVFormatContext** oc, AVCodecContext* codec_ctx);
void encode_frame_to_ts(AVFormatContext* oc, AVCodecContext* codec_ctx, AVFrame* frame, int stream_idx = 0);
std::pair<const AVCodec*, AVCodecContext*> create_codec_context(int fps, int bitrate, int width, int height, int duration_sec);
static int write_packet_cb(void* opaque, uint8_t* buf, int buf_size);
static int read_packet_cb(void* opaque, uint8_t* buf, int buf_size);
static int64_t seek_cb(void* opaque, int64_t offset, int whence);
static double get_dvbs2_rate(double symbol_rate, DATVModSettings::DATVModulation modulation, DATVModSettings::DATVCodeRate code_rate);
static double calc_dvbs2_rate(double symbol_rate, double bits, double fec_num, double fec_den, double bch, double pilots);
};
#endif // INCLUDE_ATVMOD_TSGENERATOR_H

View File

@ -5107,6 +5107,25 @@ margin-bottom: 20px;
"type" : "integer",
"description" : "Transport stream source (File=0 UDP=1)"
},
"imageFileName" : {
"type" : "string"
},
"imageOverlayTimestamp" : {
"type" : "integer",
"description" : "Overlay timestamp on still image (1 for yes, 0 for no)"
},
"imageServiceProvider" : {
"type" : "string",
"description" : "Service provider name for image overlay"
},
"imageServiceName" : {
"type" : "string",
"description" : "Service name for image overlay"
},
"imageCodec" : {
"type" : "integer",
"description" : "Image codec (HEVC=0 H264=1)"
},
"tsFileName" : {
"type" : "string"
},
@ -59715,7 +59734,7 @@ except ApiException as e:
</div>
<div id="generator">
<div class="content">
Generated 2025-12-21T17:33:51.672+01:00
Generated 2025-12-31T21:12:50.336+01:00
</div>
</div>
</div>

View File

@ -26,6 +26,20 @@ DATVModSettings:
tsSource:
description: "Transport stream source (File=0 UDP=1)"
type: integer
imageFileName:
type: string
imageOverlayTimestamp:
description: "Overlay timestamp on still image (1 for yes, 0 for no)"
type: integer
imageServiceProvider:
description: "Service provider name for image overlay"
type: string
imageServiceName:
description: "Service name for image overlay"
type: string
imageCodec:
description: "Image codec (HEVC=0 H264=1)"
type: integer
tsFileName:
type: string
tsFilePlayLoop:

View File

@ -26,6 +26,20 @@ DATVModSettings:
tsSource:
description: "Transport stream source (File=0 UDP=1)"
type: integer
imageFileName:
type: string
imageOverlayTimestamp:
description: "Overlay timestamp on still image (1 for yes, 0 for no)"
type: integer
imageServiceProvider:
description: "Service provider name for image overlay"
type: string
imageServiceName:
description: "Service name for image overlay"
type: string
imageCodec:
description: "Image codec (HEVC=0 H264=1)"
type: integer
tsFileName:
type: string
tsFilePlayLoop:

View File

@ -5107,6 +5107,25 @@ margin-bottom: 20px;
"type" : "integer",
"description" : "Transport stream source (File=0 UDP=1)"
},
"imageFileName" : {
"type" : "string"
},
"imageOverlayTimestamp" : {
"type" : "integer",
"description" : "Overlay timestamp on still image (1 for yes, 0 for no)"
},
"imageServiceProvider" : {
"type" : "string",
"description" : "Service provider name for image overlay"
},
"imageServiceName" : {
"type" : "string",
"description" : "Service name for image overlay"
},
"imageCodec" : {
"type" : "integer",
"description" : "Image codec (HEVC=0 H264=1)"
},
"tsFileName" : {
"type" : "string"
},
@ -59715,7 +59734,7 @@ except ApiException as e:
</div>
<div id="generator">
<div class="content">
Generated 2025-12-21T17:33:51.672+01:00
Generated 2025-12-31T21:12:50.336+01:00
</div>
</div>
</div>

View File

@ -44,6 +44,16 @@ SWGDATVModSettings::SWGDATVModSettings() {
m_roll_off_isSet = false;
ts_source = 0;
m_ts_source_isSet = false;
image_file_name = nullptr;
m_image_file_name_isSet = false;
image_overlay_timestamp = 0;
m_image_overlay_timestamp_isSet = false;
image_service_provider = nullptr;
m_image_service_provider_isSet = false;
image_service_name = nullptr;
m_image_service_name_isSet = false;
image_codec = 0;
m_image_codec_isSet = false;
ts_file_name = nullptr;
m_ts_file_name_isSet = false;
ts_file_play_loop = 0;
@ -100,6 +110,16 @@ SWGDATVModSettings::init() {
m_roll_off_isSet = false;
ts_source = 0;
m_ts_source_isSet = false;
image_file_name = new QString("");
m_image_file_name_isSet = false;
image_overlay_timestamp = 0;
m_image_overlay_timestamp_isSet = false;
image_service_provider = new QString("");
m_image_service_provider_isSet = false;
image_service_name = new QString("");
m_image_service_name_isSet = false;
image_codec = 0;
m_image_codec_isSet = false;
ts_file_name = new QString("");
m_ts_file_name_isSet = false;
ts_file_play_loop = 0;
@ -144,6 +164,17 @@ SWGDATVModSettings::cleanup() {
if(image_file_name != nullptr) {
delete image_file_name;
}
if(image_service_provider != nullptr) {
delete image_service_provider;
}
if(image_service_name != nullptr) {
delete image_service_name;
}
if(ts_file_name != nullptr) {
delete ts_file_name;
}
@ -201,6 +232,16 @@ SWGDATVModSettings::fromJsonObject(QJsonObject &pJson) {
::SWGSDRangel::setValue(&ts_source, pJson["tsSource"], "qint32", "");
::SWGSDRangel::setValue(&image_file_name, pJson["imageFileName"], "QString", "QString");
::SWGSDRangel::setValue(&image_overlay_timestamp, pJson["imageOverlayTimestamp"], "qint32", "");
::SWGSDRangel::setValue(&image_service_provider, pJson["imageServiceProvider"], "QString", "QString");
::SWGSDRangel::setValue(&image_service_name, pJson["imageServiceName"], "QString", "QString");
::SWGSDRangel::setValue(&image_codec, pJson["imageCodec"], "qint32", "");
::SWGSDRangel::setValue(&ts_file_name, pJson["tsFileName"], "QString", "QString");
::SWGSDRangel::setValue(&ts_file_play_loop, pJson["tsFilePlayLoop"], "qint32", "");
@ -273,6 +314,21 @@ SWGDATVModSettings::asJsonObject() {
if(m_ts_source_isSet){
obj->insert("tsSource", QJsonValue(ts_source));
}
if(image_file_name != nullptr && *image_file_name != QString("")){
toJsonValue(QString("imageFileName"), image_file_name, obj, QString("QString"));
}
if(m_image_overlay_timestamp_isSet){
obj->insert("imageOverlayTimestamp", QJsonValue(image_overlay_timestamp));
}
if(image_service_provider != nullptr && *image_service_provider != QString("")){
toJsonValue(QString("imageServiceProvider"), image_service_provider, obj, QString("QString"));
}
if(image_service_name != nullptr && *image_service_name != QString("")){
toJsonValue(QString("imageServiceName"), image_service_name, obj, QString("QString"));
}
if(m_image_codec_isSet){
obj->insert("imageCodec", QJsonValue(image_codec));
}
if(ts_file_name != nullptr && *ts_file_name != QString("")){
toJsonValue(QString("tsFileName"), ts_file_name, obj, QString("QString"));
}
@ -405,6 +461,56 @@ SWGDATVModSettings::setTsSource(qint32 ts_source) {
this->m_ts_source_isSet = true;
}
QString*
SWGDATVModSettings::getImageFileName() {
return image_file_name;
}
void
SWGDATVModSettings::setImageFileName(QString* image_file_name) {
this->image_file_name = image_file_name;
this->m_image_file_name_isSet = true;
}
qint32
SWGDATVModSettings::getImageOverlayTimestamp() {
return image_overlay_timestamp;
}
void
SWGDATVModSettings::setImageOverlayTimestamp(qint32 image_overlay_timestamp) {
this->image_overlay_timestamp = image_overlay_timestamp;
this->m_image_overlay_timestamp_isSet = true;
}
QString*
SWGDATVModSettings::getImageServiceProvider() {
return image_service_provider;
}
void
SWGDATVModSettings::setImageServiceProvider(QString* image_service_provider) {
this->image_service_provider = image_service_provider;
this->m_image_service_provider_isSet = true;
}
QString*
SWGDATVModSettings::getImageServiceName() {
return image_service_name;
}
void
SWGDATVModSettings::setImageServiceName(QString* image_service_name) {
this->image_service_name = image_service_name;
this->m_image_service_name_isSet = true;
}
qint32
SWGDATVModSettings::getImageCodec() {
return image_codec;
}
void
SWGDATVModSettings::setImageCodec(qint32 image_codec) {
this->image_codec = image_codec;
this->m_image_codec_isSet = true;
}
QString*
SWGDATVModSettings::getTsFileName() {
return ts_file_name;
@ -594,6 +700,21 @@ SWGDATVModSettings::isSet(){
if(m_ts_source_isSet){
isObjectUpdated = true; break;
}
if(image_file_name && *image_file_name != QString("")){
isObjectUpdated = true; break;
}
if(m_image_overlay_timestamp_isSet){
isObjectUpdated = true; break;
}
if(image_service_provider && *image_service_provider != QString("")){
isObjectUpdated = true; break;
}
if(image_service_name && *image_service_name != QString("")){
isObjectUpdated = true; break;
}
if(m_image_codec_isSet){
isObjectUpdated = true; break;
}
if(ts_file_name && *ts_file_name != QString("")){
isObjectUpdated = true; break;
}

View File

@ -68,6 +68,21 @@ public:
qint32 getTsSource();
void setTsSource(qint32 ts_source);
QString* getImageFileName();
void setImageFileName(QString* image_file_name);
qint32 getImageOverlayTimestamp();
void setImageOverlayTimestamp(qint32 image_overlay_timestamp);
QString* getImageServiceProvider();
void setImageServiceProvider(QString* image_service_provider);
QString* getImageServiceName();
void setImageServiceName(QString* image_service_name);
qint32 getImageCodec();
void setImageCodec(qint32 image_codec);
QString* getTsFileName();
void setTsFileName(QString* ts_file_name);
@ -144,6 +159,21 @@ private:
qint32 ts_source;
bool m_ts_source_isSet;
QString* image_file_name;
bool m_image_file_name_isSet;
qint32 image_overlay_timestamp;
bool m_image_overlay_timestamp_isSet;
QString* image_service_provider;
bool m_image_service_provider_isSet;
QString* image_service_name;
bool m_image_service_name_isSet;
qint32 image_codec;
bool m_image_codec_isSet;
QString* ts_file_name;
bool m_ts_file_name_isSet;