mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-01-17 02:55:47 -05:00
Merge
This commit is contained in:
commit
fa2f61c8aa
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 152 KiB |
BIN
doc/img/DATVMod_plugin.xcf
Normal file
BIN
doc/img/DATVMod_plugin.xcf
Normal file
Binary file not shown.
BIN
doc/img/DATVMod_plugin_A.png
Normal file
BIN
doc/img/DATVMod_plugin_A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
doc/img/DATVMod_plugin_A.xcf
Normal file
BIN
doc/img/DATVMod_plugin_A.xcf
Normal file
Binary file not shown.
BIN
doc/img/DATVMod_plugin_B.png
Normal file
BIN
doc/img/DATVMod_plugin_B.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
doc/img/DATVMod_plugin_B.xcf
Normal file
BIN
doc/img/DATVMod_plugin_B.xcf
Normal file
Binary file not shown.
@ -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})")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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; }
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -46,80 +46,114 @@ Average total power in dB relative to a ±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>
|
||||
|
||||

|
||||
|
||||
<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>
|
||||
|
||||

|
||||
|
||||
<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.
|
||||
|
||||
|
||||
556
plugins/channeltx/moddatv/tsgenerator.cpp
Normal file
556
plugins/channeltx/moddatv/tsgenerator.cpp
Normal 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};
|
||||
}
|
||||
77
plugins/channeltx/moddatv/tsgenerator.h
Normal file
77
plugins/channeltx/moddatv/tsgenerator.h
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user