libflute
flute-transmitter.cpp
Go to the documentation of this file.
1 // libflute - FLUTE/ALC library
2 //
3 // Copyright (C) 2021 Klaus Kühnhammer (Österreichische Rundfunksender GmbH & Co KG)
4 // 2025 British Broadcasting Corporation (David Waring <david.waring2@bbc.co.uk>)
5 //
6 // Licensed under the License terms and conditions for use, reproduction, and
7 // distribution of 5G-MAG software (the “License”). You may not use this file
8 // except in compliance with the License. You may obtain a copy of the License at
9 // https://www.5g-mag.com/reference-tools. Unless required by applicable law or
10 // agreed to in writing, software distributed under the License is distributed on
11 // an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12 // or implied.
13 //
14 // See the License for the specific language governing permissions and limitations
15 // under the License.
16 //
17 #include <argp.h>
18 
19 #include <cstdio>
20 #include <cstdlib>
21 
22 #include <chrono>
23 #include <iostream>
24 #include <list>
25 #include <vector>
26 #include <fstream>
27 #include <string>
28 #include <filesystem>
29 
30 #include <libconfig.h++>
31 #include <boost/asio.hpp>
32 #include <openssl/sha.h>
33 
34 #include "spdlog/async.h"
35 #include "spdlog/spdlog.h"
36 #include "spdlog/sinks/syslog_sink.h"
37 
38 #include "Version.h"
39 #include "../utils/base64.h"
40 #include "Transmitter.h"
41 
42 
43 using libconfig::Config;
44 using libconfig::FileIOException;
45 using libconfig::ParseException;
46 
47 using namespace std::literals::chrono_literals;
48 
49 static void print_version(FILE *stream, struct argp_state *state);
50 void (*argp_program_version_hook)(FILE *, struct argp_state *) = print_version;
51 const char *argp_program_bug_address = "Austrian Broadcasting Services <obeca@ors.at>";
52 static char doc[] = "FLUTE/ALC transmitter demo"; // NOLINT
53 
54 static struct argp_option options[] = { // NOLINT
55  {"target", 'm', "IP", 0, "Target multicast address (default: 238.1.1.95)", 0},
56  {"port", 'p', "PORT", 0, "Target port (default: 40085)", 0},
57  {"mtu", 't', "BYTES", 0, "Path MTU to size ALC packets for (default: 1500)", 0},
58  {"rate-limit", 'r', "KBPS", 0, "Transmit rate limit (kbps), 0 = no limit, default: 1000 (1 Mbps)", 0},
59  {"ipsec-key", 'k', "KEY", 0, "To enable IPSec/ESP encryption of packets, provide a hex-encoded AES key here", 0},
60  {"log-level", 'l', "LEVEL", 0,
61  "Log verbosity: 0 = trace, 1 = debug, 2 = info, 3 = warn, 4 = error, 5 = "
62  "critical, 6 = none (default: 2)",
63  0},
64  {"gzip", 'g', nullptr, 0, "Use gzip to compress the contents, implies -n option", 0},
65  {"tsi", 'T', "ID", 0, "The TSI to use for the FLUTE session (default: 16)", 0},
66  {"new-api", 'n', nullptr, 0, "Use the new FileDescription API", 0},
67  {"retransmit", 'R', "COUNT", 0, "Number of times to repeatedly transmit a file, implies -n option (default: 1)", 0},
68  {"etags", 'e', nullptr, 0, "Enable generation of ETag values for each file, implies -n option (default: no ETags)", 0},
69  {nullptr, 0, nullptr, 0, nullptr, 0}};
70 
74 struct ft_arguments {
75  const char *mcast_target = {};
76  bool enable_ipsec = false;
77  bool use_gzip = false;
78  bool new_api = false;
79  bool gen_etags = false;
80  const char *aes_key = {};
81  unsigned short mcast_port = 40085;
82  unsigned short mtu = 1500;
83  uint32_t rate_limit = 1000;
84  uint64_t tsi = 16;
85  size_t retransmit_count = 1;
86  unsigned log_level = 2;
87  char **files;
88 };
89 
93 static auto parse_opt(int key, char *arg, struct argp_state *state) -> error_t {
94  auto arguments = static_cast<struct ft_arguments *>(state->input);
95  switch (key) {
96  case 'e':
97  arguments->gen_etags = true;
98  arguments->new_api = true;
99  break;
100  case 'm':
101  arguments->mcast_target = arg;
102  break;
103  case 'k':
104  arguments->aes_key = arg;
105  arguments->enable_ipsec = true;
106  break;
107  case 'p':
108  arguments->mcast_port = static_cast<unsigned short>(strtoul(arg, nullptr, 10));
109  break;
110  case 't':
111  arguments->mtu = static_cast<unsigned short>(strtoul(arg, nullptr, 10));
112  break;
113  case 'r':
114  arguments->rate_limit = static_cast<uint32_t>(strtoul(arg, nullptr, 10));
115  break;
116  case 'l':
117  arguments->log_level = static_cast<unsigned>(strtoul(arg, nullptr, 10));
118  break;
119  case 'g':
120  arguments->use_gzip = true;
121  arguments->new_api = true;
122  break;
123  case 'T':
124  arguments->tsi = static_cast<uint64_t>(strtoul(arg, nullptr, 10));
125  break;
126  case 'n':
127  arguments->new_api = true;
128  break;
129  case 'R':
130  arguments->retransmit_count = static_cast<size_t>(strtoul(arg, nullptr, 10));
131  arguments->new_api = true;
132  break;
133  case ARGP_KEY_NO_ARGS:
134  argp_usage (state);
135  case ARGP_KEY_ARG:
136  arguments->files = &state->argv[state->next-1];
137  state->next = state->argc;
138  break;
139  default:
140  return ARGP_ERR_UNKNOWN;
141  }
142  return 0;
143 }
144 
145 static char args_doc[] = "[FILE...]"; //NOLINT
146 static struct argp argp = {options, parse_opt, args_doc, doc,
147  nullptr, nullptr, nullptr};
148 
152 void print_version(FILE *stream, struct argp_state * /*state*/) {
153  fprintf(stream, "%s.%s.%s\n", std::to_string(VERSION_MAJOR).c_str(),
154  std::to_string(VERSION_MINOR).c_str(),
155  std::to_string(VERSION_PATCH).c_str());
156 }
157 
158 static void send_with_new_api(struct ft_arguments &arguments)
159 {
160  struct fileEntry {
161  fileEntry(LibFlute::Transmitter::FileDescription *fd, size_t init_count = 0) :file(fd), transmitted_count(init_count) {};
162 
163  std::shared_ptr<LibFlute::Transmitter::FileDescription> file;
164  size_t transmitted_count;
165  };
166 
167  std::list<fileEntry> files;
168 
169  for (int j = 0; arguments.files[j]; j++) {
170  auto fd = new LibFlute::Transmitter::FileDescription(arguments.files[j], arguments.files[j]);
171  fd->set_content_type("application/octet-stream");
172  fd->set_expiry_time(std::chrono::system_clock::now() + 60s);
173  if (arguments.use_gzip) {
175  }
176  if (arguments.gen_etags) {
177  std::array<unsigned char, SHA_DIGEST_LENGTH> digest;
178  SHA1(reinterpret_cast<const unsigned char*>(fd->data()), fd->data_length(), digest.data());
179  fd->set_etag(base64_encode(digest.data(), SHA_DIGEST_LENGTH));
180  }
181  files.emplace_back(fd);
182  }
183 
184  // Create a Boost io_context
185  boost::asio::io_context io;
186 
187  // Construct the transmitter class
188  LibFlute::Transmitter transmitter(
189  arguments.mcast_target,
190  (short)arguments.mcast_port,
191  arguments.tsi,
192  arguments.mtu,
193  arguments.rate_limit,
195 
196  // Configure IPSEC ESP, if enabled
197  if (arguments.enable_ipsec)
198  {
199  transmitter.enable_ipsec(1, arguments.aes_key);
200  }
201 
202  // Register a completion callback
203  transmitter.register_completion_callback(
204  [&files, &arguments, &transmitter](uint32_t toi) -> void {
205  for (auto& f : files) {
206  if (f.file->toi() == toi) {
207  spdlog::info("{} (TOI {}) has been transmitted", f.file->file_entry().content_location, f.file->toi());
208  f.transmitted_count++;
209  if (f.transmitted_count < arguments.retransmit_count) {
210  transmitter.send(f.file);
211  }
212  }
213  }
214  });
215 
216  // Queue all the files
217  for (const auto& file : files) {
218  auto toi = transmitter.send( file.file );
219  const auto &file_entry = file.file->file_entry();
220  spdlog::info("Queued {} ({} bytes ({} bytes transmitted)) for transmission, TOI is {}",
221  file_entry.content_location, file_entry.content_length, file_entry.fec_oti.transfer_length, toi);
222  }
223 
224  // Start the io_context, and thus sending data
225  io.run();
226 }
227 
228 static void send_with_old_api(struct ft_arguments &arguments)
229 {
230  // We're responsible for buffer management, so create a vector of structs that
231  // are going to hold the data buffers
232  struct FsFile {
233  std::string location;
234  char* buffer;
235  size_t len;
236  uint32_t toi;
237  };
238  std::vector<FsFile> files;
239 
240  // read the file contents into the buffers
241  for (int j = 0; arguments.files[j]; j++) {
242  const std::string &location = arguments.files[j];
243  std::ifstream file(location, std::ios::binary | std::ios::ate);
244  std::streamsize size = file.tellg();
245  file.seekg(0, std::ios::beg);
246 
247  char* buffer = (char*)malloc(size);
248  file.read(buffer, size);
249  files.push_back(FsFile{ location, buffer, (size_t)size});
250  }
251 
252  // Create a Boost io_context
253  boost::asio::io_context io;
254 
255  // Construct the transmitter class
256  LibFlute::Transmitter transmitter(
257  arguments.mcast_target,
258  (short)arguments.mcast_port,
259  arguments.tsi,
260  arguments.mtu,
261  arguments.rate_limit,
263 
264  // Configure IPSEC ESP, if enabled
265  if (arguments.enable_ipsec)
266  {
267  transmitter.enable_ipsec(1, arguments.aes_key);
268  }
269 
270  // Register a completion callback
271  transmitter.register_completion_callback(
272  [&files](uint32_t toi) -> void {
273  for (auto& file : files) {
274  if (file.toi == toi) {
275  spdlog::info("{} (TOI {}) has been transmitted", file.location, file.toi);
276  // could free() the buffer here
277  }
278  }
279  });
280 
281  // Queue all the files
282  for (auto& file : files) {
283  file.toi = transmitter.send( file.location,
284  "application/octet-stream",
285  transmitter.seconds_since_epoch() + 60, // 1 minute from now
286  file.buffer,
287  file.len
288  );
289  spdlog::info("Queued {} ({} bytes) for transmission, TOI is {}",
290  file.location, file.len, file.toi);
291  }
292 
293  // Start the io_context, and thus sending data
294  io.run();
295 }
296 
304 auto main(int argc, char **argv) -> int {
305  struct ft_arguments arguments;
306  /* Default values */
307  arguments.mcast_target = "238.1.1.95";
308 
309  argp_parse(&argp, argc, argv, 0, nullptr, &arguments);
310 
311  // Set up logging
312  std::string ident = "flute-transmitter";
313  auto syslog_logger = spdlog::syslog_logger_mt("syslog", ident, LOG_PID | LOG_PERROR | LOG_CONS );
314 
315  spdlog::set_level(
316  static_cast<spdlog::level::level_enum>(arguments.log_level));
317  spdlog::set_pattern("[%H:%M:%S.%f %z] [%^%l%$] [thr %t] %v");
318 
319  spdlog::set_default_logger(syslog_logger);
320  spdlog::info("FLUTE transmitter demo starting up");
321 
322  try {
323  if (arguments.new_api) {
324  send_with_new_api(arguments);
325  } else {
326  send_with_old_api(arguments);
327  }
328  } catch (std::exception ex ) {
329  spdlog::error("Exiting on unhandled exception: %s", ex.what());
330  }
331 
332 exit:
333  return 0;
334 }
File Description object.
Definition: Transmitter.h:51
FLUTE transmitter class.
Definition: Transmitter.h:40
uint64_t seconds_since_epoch()
Convenience function to get the current timestamp for expiry calculation.
uint16_t send(const std::string &content_location, const std::string &content_type, uint32_t expires, char *data, size_t length)
Transmit a file (deprecated).
void enable_ipsec(uint32_t spi, const std::string &aes_key)
Enable IPSEC ESP encryption of FLUTE payloads.
void register_completion_callback(completion_callback_t cb)
Register a callback for file transmission completion notifications.
Definition: Transmitter.h:609
static void send_with_new_api(struct ft_arguments &arguments)
static void print_version(FILE *stream, struct argp_state *state)
Print the program version in MAJOR.MINOR.PATCH format.
auto main(int argc, char **argv) -> int
Main entry point for the program.
static char args_doc[]
const char * argp_program_bug_address
static struct argp argp
static struct argp_option options[]
void(* argp_program_version_hook)(FILE *, struct argp_state *)
static auto parse_opt(int key, char *arg, struct argp_state *state) -> error_t
Parses the command line options into the arguments struct.
static void send_with_old_api(struct ft_arguments &arguments)
static char doc[]
Holds all options passed on the command line.
unsigned log_level
log level
const char * mcast_target
unsigned short mcast_port
const char * aes_key
unsigned short mtu