/////////////////////////////////////////////////////////////////////////////////// // 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 . // /////////////////////////////////////////////////////////////////////////////////// extern "C" { #include #include #include #include #include #include #include #include #include "libswscale/swscale.h" } #include // Add OpenCV for text overlay #include #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) { QString time_str = QDateTime::currentDateTime().toString("HH:mm:ss"); char time_cstr[32]; strncpy(time_cstr, time_str.toStdString().c_str(), sizeof(time_cstr) - 1); time_cstr[sizeof(time_cstr) - 1] = '\0'; // 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_cstr, 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* 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 #if LIBAVFORMAT_VERSION_INT < ((61 << 16) | (0 << 8) | 0) int TSGenerator::write_packet_cb(void* opaque, uint8_t* buf, int buf_size) #else int TSGenerator::write_packet_cb(void* opaque, const uint8_t* buf, int buf_size) #endif { std::vector* buffer = static_cast*>(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 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}; }