+
+---
+
+## At a glance
+
+| | |
+|---|---|
+| **Implements** | 3GPP TS 26.502, *5G multicast-broadcast services; User Service architecture*, and TS 29.581, *Nmb8 service API* |
+| **Role** | MBSTF: object ingest, FLUTE packaging, MBS delivery over the user plane |
+| **Built with** | C++ and meson, on top of Open5GS |
+| **Works with** | [rt-mbs-function](https://github.com/5G-MAG/rt-mbs-function) (MBSF), which drives it over Nmb8, and [rt-mbs-client](https://github.com/5G-MAG/rt-mbs-client) at the receiving end |
+| **Part of** | [5G Multicast Broadcast Services](https://www.5g-mag.com/reference-tools/5g-multicast-broadcast-services) |
+
## Introduction
-This repository provides a 5G MBS Transport Function which forms part of the MBS User Services. This NF provides the interfaces designated as Nmb2, Nmb8 and Nmb9 in the [3GPP TS 29.581 V18.5.0](https://www.3gpp.org/DynaReport/29581.htm) specification.
+This repository provides the MBS Transport Function. It takes content by PULL or PUSH, packages it into
+FLUTE object streams, and transmits them on the MBS session the MBSF has established, whether that
+session is broadcast or multicast.
+
+It is built on the [Open5GS](https://open5gs.org/) framework and registers with an NRF like any
+other network function.
+
+## Specification
+
+Built against these versions, named rather than referred to by release:
+
+- **3GPP TS 26.502 V18.6.0**, *5G multicast-broadcast services; User Service architecture*
+- **3GPP TS 29.581 V18.6.0**, *Nmb8 service API*
+- **3GPP TS 26.346 V18.2.0**, *MBMS protocols and codecs*, for the FLUTE and FDT profiling
-Additional information can be found at: https://5g-mag.github.io/Getting-Started/pages/5g-multicast-broadcast-services/
+Clause-by-clause coverage, and what is still absent, is recorded on the project page rather than
+here:
## Install dependencies
@@ -24,6 +67,26 @@ sudo sh -c 'for i in cpp g++ gcc gcc-ar gcc-nm gcc-ranlib gcov gcov-dump gcov-to
sudo python3 -m pip install --break-system-packages --upgrade meson
```
+### The build fetches the 5G APIs
+
+The OpenAPI bindings are generated at configure time from the 3GPP 5G APIs, which the build clones
+from `forge.3gpp.org`. The build therefore needs network access to that host, and Java, which is
+why `default-jdk` is in the list above.
+
+That host currently serves an **incomplete certificate chain**: it sends its own certificate but
+not the Sectigo intermediate that signs it. A browser fetches the missing intermediate by itself,
+but `git` and `curl` do not, so the clone fails with:
+
+```
+fatal: unable to access 'https://forge.3gpp.org/rep/all/5G_APIs.git/':
+ SSL certificate verification failed: certificate signer not trusted
+```
+
+If you see that, install the missing intermediate rather than disabling verification. On Debian
+and Ubuntu, fetch *Sectigo Public Server Authentication CA OV R36* from
+, put the PEM in `/usr/local/share/ca-certificates/` with a `.crt`
+extension, and run `sudo update-ca-certificates`.
+
## Downloading
Release tar files can be downloaded from .
@@ -33,10 +96,25 @@ The source can be obtained by cloning the github repository.
For example to download the latest release you can use:
```bash
-cd ~
git clone --recurse-submodules https://github.com/5G-MAG/rt-mbs-transport-function.git
+cd rt-mbs-transport-function
```
+`--recurse-submodules` is not optional: this repository carries `rt-common-shared` as a submodule
+and the build fails without it. If you have already cloned without it, run
+`git submodule update --init --recursive`.
+
+## Dependencies
+
+Two 5G-MAG libraries are pulled in by the build and fetched automatically. They are listed here
+because a version mismatch surfaces as a compile or link error rather than as a missing
+dependency.
+
+| Dependency | How | What it supplies |
+|---|---|---|
+| `rt-common-shared` | git submodule | the HTTP server and the shared Open5GS tooling, including the OpenAPI generator this build runs |
+| `rt-libflute` | meson wrap | the FLUTE transmitter, the TS 26.346 annex L.6 profiled FDT schema, the scheme-specific FEC OTI and the RFC 5053 Raptor scheme |
+
## Building
The build process requires a working Internet connection as the API files are retrieved at build time.
@@ -44,19 +122,17 @@ The build process requires a working Internet connection as the API files are re
To build the 5G Data Collection Application Function from the source:
```bash
-cd ~/rt-mbs-transport-function
-meson build
+meson setup build
ninja -C build
```
-**Note:** Errors during the `meson build` command are often caused by missing dependencies or a network issue while trying to retrieve the API files and `openapi-generator` JAR file. See the `~/rt-mbs-transport-function/build/meson-logs/meson-log.txt` log file for the errors in greater detail. Search for `generator-libspdc` to find the start of the API fetch sequence.
+**Note:** Errors during the `meson build` command are often caused by missing dependencies or a network issue while trying to retrieve the API files and `openapi-generator` JAR file. See the `build/meson-logs/meson-log.txt` log file for the errors in greater detail. Search for `generator-libspdc` to find the start of the API fetch sequence.
## Unit tests (optional)
There are some unit tests that can be run using:
```bash
-cd ~/rt-mbs-transport-function
meson test -C build --suite rt-mbs-transport-function
```
@@ -67,8 +143,7 @@ This will build the MBSTF (if not already built) and then will run the unit test
To install the built MBS Transport Function as a system process:
```bash
-cd ~/rt-mbs-transport-function/build
-sudo meson install --no-rebuild
+sudo meson install -C build --no-rebuild
```
## Running
@@ -85,6 +160,13 @@ Make sure the IP address and port details of the NRF you are running are configu
sudo /usr/local/bin/open5gs-mbstfd &
```
+## Configuration
+
+Configuration is a YAML file in the Open5GS style, installed as
+`/usr/local/etc/open5gs/mbstf.yaml` and passed with `-c` when running from a build tree. The
+sections that matter are `nrf`, which must point at a reachable NRF, and the MBSTF's own SBI
+address and the local address it sends FLUTE from.
+
## Development
This project follows
@@ -92,3 +174,12 @@ the [Gitflow workflow](https://www.atlassian.com/git/tutorials/comparing-workflo
`development` branch of this project serves as an integration branch for new features. Consequently, please make sure to
switch to the `development` branch before starting the implementation of a new feature.
+## Contributing
+
+Contributions are welcome. How to raise an issue, fork the repository and open a pull request, and
+the Contributor License Agreement required before code can be merged, are described at
+.
+
+## License
+
+See [LICENSE](LICENSE).
diff --git a/src/mbstf/Context.cc b/src/mbstf/Context.cc
index bf06f8c..a11221e 100644
--- a/src/mbstf/Context.cc
+++ b/src/mbstf/Context.cc
@@ -48,6 +48,7 @@ Context::Context()
,servers()
,cacheControl({60, 60})
,totalMaxBitRateSoftLimit(100)
+ ,pathMtu(kDefaultPathMtu)
,consecutiveIngestFailuresBeforeDeactivate(5)
,packetModeSchedulingQueueSize(128*1024) // 128KB queue for rate smoothing
,manifestGlobals()
@@ -119,6 +120,18 @@ bool Context::parseConfig()
} else {
throw std::out_of_range("Bad configuration node at mbstf.totalMaxBitRateSoftLimit");
}
+ } else if (mbstf_key == "pathMtu") {
+ Open5GSYamlIter mtu_iter(mbstf_iter);
+ if (mtu_iter.type() == YAML_SCALAR_NODE) {
+ std::string num_val(mtu_iter.value());
+ size_t idx = 0;
+ pathMtu = std::stoi(num_val, &idx);
+ if (idx != num_val.size() || pathMtu <= 0) {
+ throw std::out_of_range("Bad configuration value at mbstf.pathMtu");
+ }
+ } else {
+ throw std::out_of_range("Bad configuration node at mbstf.pathMtu");
+ }
} else if (mbstf_key == "consecutiveIngestFailuresBeforeDeactivate") {
Open5GSYamlIter failures_iter(mbstf_iter);
if (failures_iter.type() == YAML_SCALAR_NODE) {
diff --git a/src/mbstf/Context.hh b/src/mbstf/Context.hh
index 3dcde2a..b54d46e 100644
--- a/src/mbstf/Context.hh
+++ b/src/mbstf/Context.hh
@@ -73,6 +73,37 @@ public:
unsigned int defaultObjectMaxAge; // Use if not given by push/pull resource Cache-Control.
} cacheControl;
int totalMaxBitRateSoftLimit; //< total maximum bit rate this MBSTF ought to asked to handle
+ /**< MTU, in bytes, of the path a distribution session's packets travel to the receiver.
+ *
+ * FLUTE encoding symbols are sized from this, so a value larger than the path can carry puts
+ * every multi-symbol object into datagrams that do not arrive. RFC 5651 section 6.1: "However,
+ * network efficiency considerations recommend that the sender uses an as large as possible
+ * packet payload size, but in such a way that packets do not exceed the network's maximum
+ * transmission unit size (MTU), or when fragmentation coupled with packet loss might introduce
+ * severe inefficiency in the transmission."
+ *
+ * getsockopt(IP_MTU) on a socket to the session's tunnel address, or to its SSM destination
+ * when there is no tunnel, measures the first hop only. Where the MBSTF and the ingress point
+ * are co-located, which is every single-host deployment, that hop is the loopback interface
+ * and the answer is the loopback MTU, 65536 on Linux; and where a tunnel is configured the
+ * datagram is re-encapsulated and forwarded over a path the MBSTF cannot measure at all. The
+ * measurement is therefore a ceiling on the first hop, never a description of the whole path.
+ *
+ * kDefaultPathMtu is the default and the operator overrides it with mbstf.pathMtu. The
+ * discovered value is used in place of it only when it is smaller, since a first hop narrower
+ * than the stated path MTU is a real constraint while a wider one says nothing about the rest
+ * of the path.
+ */
+ int pathMtu;
+
+ /**< The path MTU assumed when the operator does not state one, in bytes.
+ *
+ * The conventional Ethernet MTU. No clause fixes it: it is a documented default, and a
+ * deployment whose path differs sets mbstf.pathMtu. Sizing symbols below the path MTU costs
+ * efficiency; sizing them above it costs delivery, so the default is the safe side of that.
+ */
+ static constexpr int kDefaultPathMtu = 1500;
+
int consecutiveIngestFailuresBeforeDeactivate; //< The number of consecutive ingest failures allowed before the session aborts
size_t packetModeSchedulingQueueSize; //< The maximum queue size for packet mode scheduling per DistSession
struct {
@@ -82,6 +113,10 @@ public:
*/
std::optional manifestRepetitionRate = std::nullopt;
} manifestGlobals; //< ManifestHandler global configuration (can be overridden by ManifestHandler implement specific config)
+ // TS 29.500 V18.10.0 cl.5.2.7.2/table 5.2.7.1-1: 413 (Payload Too Large) is mandatory for
+ // PATCH and POST. No clause, and no MBSTF documented default, names a byte limit
+ // -- unset means no limit is enforced, as before this option existed.
+ std::optional maxRequestBodySize;
/** Parse a configuration time duration string
*
diff --git a/src/mbstf/DASHManifestHandler.cc b/src/mbstf/DASHManifestHandler.cc
index 30f6288..1b840ec 100644
--- a/src/mbstf/DASHManifestHandler.cc
+++ b/src/mbstf/DASHManifestHandler.cc
@@ -18,6 +18,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -123,7 +124,11 @@ std::pair DASHManifest
static const std::string empty;
auto current_time = std::chrono::system_clock::now();
std::optional time_to_update;
- std::string manifest_url;
+ /* The manifest's own URL, so the two comparisons below can recognise the MPD-refresh entry
+ addMPDRefreshToExtraPullObjects() adds to m_extraPullObjects from the same value. Left empty,
+ both comparisons test against "" and m_refreshMpd is never set, so a re-fetched MPD is
+ ingested without the handler being told its own manifest changed. */
+ const std::string manifest_url(m_manifest ? m_manifest->second.getFetchedUrl() : std::string());
time_type fetch_time;
std::list media_segments;
@@ -131,6 +136,34 @@ std::pair DASHManifest
std::lock_guard guard(m_mpdMutex);
media_segments = std::move(augmentSegmentAvailabilityList(m_mpd.selectedSegmentAvailability(), false, false, false));
}
+ /* A live MPD lists a segment for the whole of its availability window, which tells a unicast client
+ it may still fetch that segment. It does not mean the segment still needs transmitting. Without
+ this filter the same segment is emitted on every pass until its window closes, and because
+ OBJECT_STREAMING removes an object from the ObjectStore once sent, findMetadataByURL() below
+ misses on each pass and builds a fresh object, which the packager then sends under a fresh TOI.
+ A receiver cannot combine symbols across TOIs, so those copies are not redundancy: each is a
+ separate incomplete object, and together they crowd out the bandwidth the first copy needed.
+ Measured before this filter: 4.0 TOIs per object on average and up to 15. */
+ pruneSentSegments(media_segments);
+ media_segments.remove_if([this](const SegmentEntry &seg) {
+ try {
+ return m_sentSegmentUrls.find(seg.segmentURL()) != m_sentSegmentUrls.end();
+ } catch (std::domain_error&) {
+ return false; /* leave a malformed URL to the existing handling further down */
+ }
+ });
+
+ /* Only segments the MPD itself advertises are suppressed on a later pass. The entries appended
+ from m_extraPullObjects below -- the MPD refresh and the initialisation segments -- are meant to
+ repeat, and are deliberately not recorded. */
+ std::set media_segment_urls;
+ for (const auto &seg : media_segments) {
+ try {
+ media_segment_urls.insert(seg.segmentURL());
+ } catch (std::domain_error&) {
+ }
+ }
+
media_segments.insert(media_segments.end(), m_extraPullObjects.begin(), m_extraPullObjects.end());
for (auto &ms: media_segments) {
if(ms.availabilityStartTime() < current_time)
@@ -171,6 +204,7 @@ std::pair DASHManifest
obj_dist_base_url, first_media_segment.availabilityEndTime(), first_media_segment.forceRecache(), first_media_segment.keepAfterSend(), first_media_segment.compressEntry());
}
removeExtraPullObjectsEntry(first_media_segment);
+ if (media_segment_urls.find(segment_url) != media_segment_urls.end()) m_sentSegmentUrls.insert(segment_url);
try {
if (first_media_segment.segmentURL() == manifest_url) m_refreshMpd = true;
@@ -185,6 +219,7 @@ std::pair DASHManifest
segment_url = it->segmentURL();
existing_obj = object_store->findMetadataByURL(segment_url);
removeExtraPullObjectsEntry(*it);
+ if (media_segment_urls.find(segment_url) != media_segment_urls.end()) m_sentSegmentUrls.insert(segment_url);
if (existing_obj) {
ingest_items.emplace_back(*existing_obj, it->availabilityEndTime(), it->forceRecache(), it->keepAfterSend(), it->compressEntry());
} else {
@@ -198,6 +233,28 @@ std::pair DASHManifest
return std::make_pair(fetch_time, ingest_items);
}
+void DASHManifestHandler::pruneSentSegments(const std::list ¤t_segments)
+{
+ /* Bounds m_sentSegmentUrls by the manifest itself: a segment the MPD no longer advertises can no
+ longer be re-emitted by the filter above, so remembering it serves nothing. Without this the set
+ grows for the lifetime of a live session. */
+ if (m_sentSegmentUrls.empty()) return;
+ std::set still_listed;
+ for (const auto &seg : current_segments) {
+ try {
+ still_listed.insert(seg.segmentURL());
+ } catch (std::domain_error&) {
+ }
+ }
+ for (auto it = m_sentSegmentUrls.begin(); it != m_sentSegmentUrls.end();) {
+ if (still_listed.find(*it) == still_listed.end()) {
+ it = m_sentSegmentUrls.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
void DASHManifestHandler::addMPDRefreshToExtraPullObjects()
{
std::lock_guard guard(m_mpdMutex);
diff --git a/src/mbstf/DASHManifestHandler.hh b/src/mbstf/DASHManifestHandler.hh
index 7a7610a..0df82d4 100644
--- a/src/mbstf/DASHManifestHandler.hh
+++ b/src/mbstf/DASHManifestHandler.hh
@@ -12,6 +12,7 @@
* https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view
*/
#include
+#include
#include
#include
#include
@@ -95,6 +96,25 @@ private:
bool m_refreshMpd;
ManifestHandler::time_type m_mpdReceivedTime;
std::list m_extraPullObjects;
+ /** Media segment URLs already handed to the ingester in this session.
+ *
+ * A live MPD keeps a segment listed for the whole of its availability window, which is what tells
+ * a unicast client it may still fetch it. It does not mean the segment still needs sending. In
+ * OBJECT_STREAMING an object is removed from the ObjectStore once it has been sent
+ * (ObjectStreamingController leaves Metadata::keepAfterSend() at false, unlike the carousel), so
+ * the store cannot answer "has this been sent already?" either: findMetadataByURL() misses, a
+ * second ObjectStore object is created for the same URL, and it goes out under a second TOI.
+ *
+ * A receiver cannot combine symbols across TOIs. RFC 3926 clause 3.1: "Note that each object is
+ * associated with a unique TOI within the scope of a session." Each copy is therefore a separate,
+ * independently incomplete object rather than redundancy, and the copies consume the bearer that
+ * the first copy needed.
+ *
+ * Bounded by the MPD's own availability window: pruneSentSegments() drops every entry the current
+ * manifest no longer lists, so this holds at most one string per segment currently advertised.
+ */
+ std::set m_sentSegmentUrls;
+ void pruneSentSegments(const std::list ¤t_segments);
};
MBSTF_NAMESPACE_STOP
diff --git a/src/mbstf/DistributionSession.cc b/src/mbstf/DistributionSession.cc
index c75286e..f55ccce 100644
--- a/src/mbstf/DistributionSession.cc
+++ b/src/mbstf/DistributionSession.cc
@@ -90,6 +90,7 @@ using reftools::mbstf::StatusSubscribeReqData;
using reftools::mbstf::StatusSubscribeRspData;
using reftools::mbstf::TunnelAddress;
using reftools::mbstf::UpTrafficFlowInfo;
+using reftools::mbstf::FECConfig;
MBSTF_NAMESPACE_START
@@ -107,6 +108,9 @@ static void send_model_params_error(const ModelParamsException &err, Open5GSSBIS
const std::optional &api, const std::string &no_cause_reason,
const std::string &log_prefix);
static void _validate(const std::shared_ptr &dist_session);
+static bool request_too_large(Open5GSSBIRequest &request, Open5GSSBIStream &stream, int path_segments,
+ Open5GSSBIMessage &message, const NfServer::AppMetadata &app_meta,
+ const std::optional &api);
/**** public: ****/
@@ -428,6 +432,26 @@ bool DistributionSession::processEvent(Open5GSEvent &event)
dist_event.releaseEventData();
return true;
}
+ case LocalEvents::SUBSCRIPTION_EXPIRED:
+ {
+ /* Pushed by a subscription's expiry timer (SubscriptionExpiryTimerFunc in
+ * DistributionSessionSubscription.cc): its own expiryTime has passed, so remove it.
+ * Handled here, off the timer's call stack, so the subscription and the timer that
+ * fired can both be destroyed safely as part of the removal. */
+ std::unique_ptr > expired(
+ reinterpret_cast*>(event.sbiData()));
+ const auto &dist_session = App::self().context()->findDistributionSession(expired->first);
+ if (dist_session) {
+ try {
+ dist_session->removeSubscription(expired->second);
+ ogs_debug("Removed expired subscription %s from Distribution Session %s",
+ expired->second.c_str(), expired->first.c_str());
+ } catch (std::range_error &ex) {
+ /* already gone, e.g. deleted through the API before the timer fired */
+ }
+ }
+ return true;
+ }
default:
break;
}
@@ -559,6 +583,13 @@ std::optional DistributionSession::getMbr() const
return std::nullopt;
}
+std::optional> DistributionSession::getFecInformation() const
+{
+ std::shared_ptr create_req_data = distributionSessionReqData();
+ std::shared_ptr dist_session = create_req_data->getDistSession();
+ return dist_session->getFecInformation();
+}
+
const std::optional &DistributionSession::getObjectIngestBaseUrl() const
{
std::shared_ptr create_req_data = distributionSessionReqData();
@@ -982,10 +1013,15 @@ void DistributionSession::_apiSessionCreate(Open5GSSBIStream &stream, Open5GSSBI
{
/* static method */
if (request.headerValue(OGS_SBI_CONTENT_TYPE, std::string()) != "application/json") {
- ogs_assert(true == NfServer::sendError(stream, ProblemCause::INVALID_MSG_FORMAT, 1, message, app_meta, api,
+ /* TS 29.500 V18.10.0 table 5.2.7.1-1 marks 415 mandatory for POST. Its table 5.2.7.2-1
+ defines no named cause for 415, so the numeric status is constructed directly here,
+ the same pattern this file already uses for 405 and 501; ProblemCause::INVALID_MSG_FORMAT
+ would answer 400, which is a different condition. */
+ ogs_assert(true == NfServer::sendError(stream, OGS_SBI_HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, 1, message, app_meta, api,
"Unsupported Media Type", "Expected content type: application/json"));
return;
}
+ if (request_too_large(request, stream, 1, message, app_meta, api)) return;
CJson distSession(CJson::Null);
try {
@@ -1118,13 +1154,17 @@ void DistributionSession::_apiSessionPatch(Open5GSSBIStream &stream, Open5GSSBIM
{
std::string content_type(message.contentType());
if (content_type != OGS_SBI_CONTENT_PATCH_TYPE) {
+ /* TS 29.500 V18.10.0 table 5.2.7.1-1 marks 415 mandatory for PATCH. This resource's
+ expected type is the merge-patch type, not the plain application/json the POST handlers
+ check, so the comparison differs from theirs while the status does not. */
std::ostringstream err;
err << "Content-Type [" << message.contentType() << "] unknown for PATCH method, expecting " OGS_SBI_CONTENT_PATCH_TYPE;
ogs_error("%s", err.str().c_str());
- ogs_assert(true == NfServer::sendError(stream, ProblemCause::INVALID_MSG_FORMAT, 2, message, app_meta, api,
- "MBSTF Distribution Session patch bad MIME type", err.str()));
+ ogs_assert(true == NfServer::sendError(stream, OGS_SBI_HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, 2, message, app_meta, api,
+ "Unsupported Media Type", err.str()));
return;
}
+ if (request_too_large(request, stream, 2, message, app_meta, api)) return;
/* parse body */
CJson patch_json(CJson::newNull());
@@ -1136,11 +1176,21 @@ void DistributionSession::_apiSessionPatch(Open5GSSBIStream &stream, Open5GSSBIM
return;
}
- /* Apply patch */
- auto old_dist_sess = distributionSessionReqData();
+ /* Apply patch.
+
+ The JSON Pointers in the patch address this resource's own representation, which
+ TS 29.581 V18.6.0 (TS29581_Nmbstf_DistSession.yaml) gives as DistSession for both the
+ PATCH and the GET on /dist-sessions/{distSessionRef}; CreateReqData is the request body
+ of the collection POST only. So a peer sends "/distSessionState", not
+ "/distSession/distSessionState", and the patch is applied to the DistSession that
+ CreateReqData holds rather than to CreateReqData itself. The enclosing CreateReqData is
+ then rebuilt around the patched DistSession, since that is what this object stores. */
+ auto old_req_data = distributionSessionReqData();
std::shared_ptr new_dist_sess{};
try {
- new_dist_sess.reset(old_dist_sess->newWithJSONPatches(patch_json));
+ std::shared_ptr patched_sess(old_req_data->getDistSession()->newWithJSONPatches(patch_json));
+ new_dist_sess.reset(new reftools::mbstf::CreateReqData(*old_req_data));
+ new_dist_sess->setDistSession(patched_sess);
} catch (ModelException &err) {
send_model_error(err, stream, 2, message, app_meta, api, "Unable to apply JSON Patch",
"MBSTF Distribution Session patch failed to apply");
@@ -1175,8 +1225,19 @@ void DistributionSession::_apiSessionGet(Open5GSSBIStream &stream, Open5GSSBIMes
const std::optional &api,
const NfServer::AppMetadata &app_meta)
{
- CJson createdRspData_json(json(false));
- std::string body(createdRspData_json.serialise());
+ /* TS 29.500 V18.10.0 table 5.2.7.1-1 marks 406 mandatory for GET. This response is always
+ application/json, so a client whose Accept header cannot take that is answered 406 rather
+ than sent a body it did not ask for. */
+ std::optional accept_hdr;
+ if (message.accept()) accept_hdr = message.accept();
+ if (!NfServer::acceptsMediaType(accept_hdr, "application/json")) {
+ ogs_assert(true == NfServer::sendError(stream, OGS_SBI_HTTP_STATUS_NOT_ACCEPTABLE, 1, message, app_meta, api,
+ "Not Acceptable", "This resource is only available as application/json"));
+ return;
+ }
+
+ CJson dist_session_json(json());
+ std::string body(dist_session_json.serialise());
ogs_debug("Generated JSON: %s", body.c_str());
std::optional content_type;
if (!body.empty()) {
@@ -1195,10 +1256,15 @@ void DistributionSession::_apiSubscriptionCreate(Open5GSSBIStream &stream, Open5
const NfServer::AppMetadata &app_meta)
{
if (request.headerValue(OGS_SBI_CONTENT_TYPE, std::string()) != "application/json") {
- ogs_assert(true == NfServer::sendError(stream, ProblemCause::INVALID_MSG_FORMAT, 1, message, app_meta, api,
+ /* TS 29.500 V18.10.0 table 5.2.7.1-1 marks 415 mandatory for POST. Its table 5.2.7.2-1
+ defines no named cause for 415, so the numeric status is constructed directly here,
+ the same pattern this file already uses for 405 and 501; ProblemCause::INVALID_MSG_FORMAT
+ would answer 400, which is a different condition. */
+ ogs_assert(true == NfServer::sendError(stream, OGS_SBI_HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, 1, message, app_meta, api,
"Unsupported Media Type", "Expected content type: application/json"));
return;
}
+ if (request_too_large(request, stream, 1, message, app_meta, api)) return;
CJson dist_session_subsc_json(CJson::Null);
std::string subsc_id;
@@ -1266,6 +1332,21 @@ void DistributionSession::_apiSubscriptionPatch(const DistributionSessionSubscri
const std::optional &api,
const NfServer::AppMetadata &app_meta)
{
+ /* TS 29.581 (TS29581_Nmbstf_DistSession.yaml, the
+ /dist-sessions/{distSessionRef}/subscriptions/{subscriptionId} PATCH operation) requires
+ application/json-patch+json for this resource. Both PATCH operations in that document
+ request this type, not application/merge-patch+json. */
+ std::string content_type(message.contentType());
+ if (content_type != OGS_SBI_CONTENT_PATCH_TYPE) {
+ std::ostringstream err;
+ err << "Content-Type [" << content_type << "] unknown for PATCH method, expecting " OGS_SBI_CONTENT_PATCH_TYPE;
+ ogs_error("%s", err.str().c_str());
+ ogs_assert(true == NfServer::sendError(stream, OGS_SBI_HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, 4, message, app_meta, api,
+ "Unsupported Media Type", err.str()));
+ return;
+ }
+ if (request_too_large(request, stream, 4, message, app_meta, api)) return;
+
CJson req_json(CJson::Null);
try {
req_json = CJson::parse(request.content());
@@ -1309,6 +1390,22 @@ static std::shared_ptr get_object_distribution_data(const D
}
+static bool request_too_large(Open5GSSBIRequest &request, Open5GSSBIStream &stream, int path_segments,
+ Open5GSSBIMessage &message, const NfServer::AppMetadata &app_meta,
+ const std::optional &api)
+{
+ const auto &max_size = App::self().context()->maxRequestBodySize;
+ if (!max_size || request.contentLength() <= *max_size) return false;
+
+ std::ostringstream err;
+ err << "Request body of " << request.contentLength() << " bytes exceeds the configured maximum of "
+ << *max_size << " bytes";
+ ogs_error("%s", err.str().c_str());
+ ogs_assert(true == NfServer::sendError(stream, OGS_SBI_HTTP_STATUS_PAYLOAD_TOO_LARGE, path_segments, message,
+ app_meta, api, "Payload Too Large", err.str()));
+ return true;
+}
+
static void send_model_error(const ModelException &err, Open5GSSBIStream &stream, int path_segments, Open5GSSBIMessage &message,
const NfServer::AppMetadata &app_meta, const std::optional &api,
const std::string &no_cause_reason, const std::string &log_prefix)
@@ -1438,6 +1535,7 @@ static void _validate(const std::shared_ptr &dist_session)
}
}
+
MBSTF_NAMESPACE_STOP
/* vim:ts=8:sts=4:sw=4:expandtab:
diff --git a/src/mbstf/DistributionSession.hh b/src/mbstf/DistributionSession.hh
index 1c8aafc..b290f1d 100644
--- a/src/mbstf/DistributionSession.hh
+++ b/src/mbstf/DistributionSession.hh
@@ -35,6 +35,10 @@
#include "DistributionSessionSubscription.hh"
#include "NfServer.hh"
+namespace reftools::mbstf {
+ class FECConfig;
+}
+
namespace fiveg_mag_reftools {
class CJson;
}
@@ -93,6 +97,9 @@ public:
in_port_t getTunnelPortNumber() const;
uint32_t getRateLimit() const;
std::optional getMbr() const;
+ /** DistSession.fecInformation (TS 29.581 clause 6.1.6.2.5, TS 29.580 V18.8.0 clause 6.2.6.2.14
+ * FECConfig), unset when the create request did not carry it. */
+ std::optional> getFecInformation() const;
const std::optional &getObjectIngestBaseUrl() const;
const std::string &getObjectAcquisitionMethod() const;
void setObjectIngestBaseUrl(std::string ingestBaseUrl);
diff --git a/src/mbstf/DistributionSessionSubscription.cc b/src/mbstf/DistributionSessionSubscription.cc
index 43bf48a..2aa5453 100644
--- a/src/mbstf/DistributionSessionSubscription.cc
+++ b/src/mbstf/DistributionSessionSubscription.cc
@@ -18,6 +18,8 @@
*/
#include
+#include
+#include
#include
#include
#include
@@ -30,6 +32,10 @@
#include "App.hh"
#include "DistributionSession.hh"
#include "DistributionSessionNotificationEvent.hh"
+#include "LocalEvents.hh"
+#include "Open5GSNetworkFunction.hh"
+#include "Open5GSTimer.hh"
+#include "TimerFunc.hh"
#include "openapi/model/DistSessionSubscription.h"
#include "openapi/model/DistSessionEventReport.h"
#include "openapi/model/DistSessionEventReportList.h"
@@ -48,6 +54,48 @@ using fiveg_mag_reftools::ModelException;
MBSTF_NAMESPACE_START
+namespace {
+
+/* Fires once a DistributionSessionSubscription's expiryTime passes. Carries only plain string ids,
+ * not a pointer or reference to the DistributionSession or the subscription, so it depends on
+ * neither object's lifetime.
+ *
+ * trigger() does not call DistributionSession::removeSubscription() directly: this callback runs
+ * from the timer owned by the very DistributionSessionSubscription that would be erased, so
+ * removing it here would destroy this TimerFunc, and the Open5GSTimer executing it, while trigger()
+ * is still on the stack. It pushes a LocalEvents::SUBSCRIPTION_EXPIRED event instead and lets
+ * DistributionSession::processEvent() perform the removal off this call stack, the same deferred
+ * dispatch LocalEvents::SEND_NOTIFICATION and RELEASE_SUBSCRIPTION_SVC already use. */
+class SubscriptionExpiryTimerFunc : public TimerFunc {
+public:
+ SubscriptionExpiryTimerFunc(const std::string &dist_session_id, const std::string &subscription_id)
+ :TimerFunc(), distSessionId(dist_session_id), subscriptionId(subscription_id) {};
+ SubscriptionExpiryTimerFunc(SubscriptionExpiryTimerFunc &&) = delete;
+ SubscriptionExpiryTimerFunc(const SubscriptionExpiryTimerFunc &) = delete;
+ SubscriptionExpiryTimerFunc &operator=(SubscriptionExpiryTimerFunc &&) = delete;
+ SubscriptionExpiryTimerFunc &operator=(const SubscriptionExpiryTimerFunc &) = delete;
+ virtual ~SubscriptionExpiryTimerFunc() {};
+
+ virtual void trigger()
+ {
+ ogs_debug("Subscription %s expiry timer fired", subscriptionId.c_str());
+ std::shared_ptr event(new Open5GSEvent(new ogs_event_t));
+ event->ogsEvent()->id = LocalEvents::SUBSCRIPTION_EXPIRED;
+ event->setSbiData(new std::pair(distSessionId, subscriptionId));
+ try {
+ App::self().ogsApp()->pushEvent(event);
+ } catch (std::exception &ex) {
+ ogs_error("Failed to push SUBSCRIPTION_EXPIRED event for subscription %s: %s",
+ subscriptionId.c_str(), ex.what());
+ }
+ }
+
+ std::string distSessionId;
+ std::string subscriptionId;
+};
+
+}
+
static int __notify_client_cb(int status, ogs_sbi_response_t *response, void *data);
namespace {
@@ -71,6 +119,7 @@ DistributionSessionSubscription::DistributionSessionSubscription(const std::weak
_setSubscriptionId();
_setEventFlags();
_setExpiryTime();
+ _scheduleExpiryTimer();
}
DistributionSessionSubscription::DistributionSessionSubscription(const std::weak_ptr &dist_session,
@@ -85,6 +134,7 @@ DistributionSessionSubscription::DistributionSessionSubscription(const std::weak
_setSubscriptionId();
_setEventFlags();
_setExpiryTime();
+ _scheduleExpiryTimer();
}
DistributionSessionSubscription::DistributionSessionSubscription(DistributionSessionSubscription &&other)
@@ -92,6 +142,8 @@ DistributionSessionSubscription::DistributionSessionSubscription(DistributionSes
,m_subscriptionId(std::move(other.m_subscriptionId))
,m_eventTypes(std::move(other.m_eventTypes))
,m_distSessionSubscription(std::move(other.m_distSessionSubscription))
+ ,m_expiryTimer(std::move(other.m_expiryTimer))
+ ,m_expiryTimerFunc(std::move(other.m_expiryTimerFunc))
,m_expiryTime(std::move(other.m_expiryTime))
,m_cache(other.m_cache)
{
@@ -106,10 +158,14 @@ DistributionSessionSubscription::DistributionSessionSubscription(const Distribut
,m_expiryTime(other.m_expiryTime)
,m_cache(new DistributionSessionSubscription::CacheType(*other.m_cache))
{
+ /* A timer's callback is keyed to one subscription object, so the copy gets its own rather
+ than sharing other's. */
+ _scheduleExpiryTimer();
}
DistributionSessionSubscription::~DistributionSessionSubscription()
{
+ _cancelExpiryTimer();
if (m_cache) {
delete m_cache;
m_cache = nullptr;
@@ -119,11 +175,14 @@ DistributionSessionSubscription::~DistributionSessionSubscription()
/* operators */
DistributionSessionSubscription &DistributionSessionSubscription::operator=(DistributionSessionSubscription &&other)
{
+ _cancelExpiryTimer();
m_distributionSession = std::move(other.m_distributionSession);
m_subscriptionId = std::move(other.m_subscriptionId);
m_eventTypes = std::move(other.m_eventTypes);
m_distSessionSubscription = std::move(other.m_distSessionSubscription);
m_expiryTime = std::move(other.m_expiryTime);
+ m_expiryTimer = std::move(other.m_expiryTimer);
+ m_expiryTimerFunc = std::move(other.m_expiryTimerFunc);
if (m_cache) delete m_cache;
m_cache = other.m_cache;
other.m_cache = nullptr;
@@ -132,12 +191,15 @@ DistributionSessionSubscription &DistributionSessionSubscription::operator=(Dist
DistributionSessionSubscription &DistributionSessionSubscription::operator=(const DistributionSessionSubscription &other)
{
+ _cancelExpiryTimer();
m_distributionSession = other.m_distributionSession;
m_subscriptionId = other.m_subscriptionId;
m_eventTypes = other.m_eventTypes;
m_distSessionSubscription = other.m_distSessionSubscription;
m_expiryTime = other.m_expiryTime;
*m_cache = *other.m_cache;
+ /* see the copy constructor: an independent timer, not a shared one */
+ _scheduleExpiryTimer();
return *this;
}
@@ -184,6 +246,7 @@ DistributionSessionSubscription &DistributionSessionSubscription::update(CJson &
}
_setEventFlags();
_setExpiryTime();
+ _scheduleExpiryTimer();
return *this;
}
@@ -337,6 +400,41 @@ void DistributionSessionSubscription::_setExpiryTime()
}
}
+void DistributionSessionSubscription::_scheduleExpiryTimer()
+{
+ _cancelExpiryTimer();
+
+ if (!m_expiryTime) return; /* no expiryTime set, nothing to enforce */
+
+ std::shared_ptr dist_session(m_distributionSession.lock());
+ if (!dist_session) return; /* no parent DistributionSession to key the timer to */
+
+ const auto now = std::chrono::system_clock::now();
+ long long delay_ms = 0;
+ if (m_expiryTime.value() > now) {
+ delay_ms = std::chrono::duration_cast(m_expiryTime.value() - now).count();
+ }
+ if (delay_ms > std::numeric_limits::max()) delay_ms = std::numeric_limits::max();
+
+ m_expiryTimerFunc.reset(new SubscriptionExpiryTimerFunc(dist_session->distributionSessionId(), m_subscriptionId));
+ m_expiryTimer = App::self().ogsApp()->addTimer(*m_expiryTimerFunc);
+ if (m_expiryTimer) {
+ m_expiryTimer->start(static_cast(delay_ms));
+ } else {
+ ogs_error("Failed to create expiry timer for subscription %s", m_subscriptionId.c_str());
+ m_expiryTimerFunc.reset();
+ }
+}
+
+void DistributionSessionSubscription::_cancelExpiryTimer()
+{
+ if (m_expiryTimer) {
+ App::self().ogsApp()->removeTimer(m_expiryTimer);
+ m_expiryTimer.reset();
+ }
+ m_expiryTimerFunc.reset();
+}
+
void DistributionSessionSubscription::_setSubscriptionId()
{
uuid_t uuid;
diff --git a/src/mbstf/DistributionSessionSubscription.hh b/src/mbstf/DistributionSessionSubscription.hh
index e233ad8..3edc046 100644
--- a/src/mbstf/DistributionSessionSubscription.hh
+++ b/src/mbstf/DistributionSessionSubscription.hh
@@ -42,6 +42,8 @@ MBSTF_NAMESPACE_START
class DistributionSession;
class Open5GSEvent;
+class Open5GSTimer;
+class TimerFunc;
class DistributionSessionSubscription {
public:
@@ -95,6 +97,11 @@ public:
private:
void _setEventFlags();
void _setExpiryTime();
+ /** (Re)schedule, or cancel when no expiryTime is set, the timer that removes this
+ * subscription once its expiryTime passes. Call after any change to m_expiryTime. */
+ void _scheduleExpiryTimer();
+ /** Cancel and release any currently scheduled expiry timer. */
+ void _cancelExpiryTimer();
void _setSubscriptionId();
std::weak_ptr m_distributionSession; /* Parent distribution session */
@@ -103,6 +110,11 @@ private:
int m_eventTypes; /* ORed EventTypeBitMask */
reftools::mbstf::DistSessionSubscription m_distSessionSubscription;
std::optional m_expiryTime;
+ /* Timer that removes this subscription once m_expiryTime passes; absent when no expiryTime is
+ set. TS 29.581's DistSessionSubscription carries expiryTime, and without this the value is
+ parsed and stored but never acted on. */
+ std::shared_ptr m_expiryTimer;
+ std::unique_ptr m_expiryTimerFunc;
std::optional m_subscriptionLocation;
struct CacheType {
diff --git a/src/mbstf/FecOtiHelper.cc b/src/mbstf/FecOtiHelper.cc
new file mode 100644
index 0000000..d974070
--- /dev/null
+++ b/src/mbstf/FecOtiHelper.cc
@@ -0,0 +1,86 @@
+/******************************************************************************
+ * 5G-MAG Reference Tools: MBS Transport Function: FEC OTI helper
+ ******************************************************************************
+ * Copyright: (C)2025-2026 British Broadcasting Corporation
+ * Author(s): Dev Audsin
+ * David Waring
+ * License: 5G-MAG Public License v1
+ *
+ * For full license terms please see the LICENSE file distributed with this
+ * program. If this file is missing then the license can be retrieved from
+ * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "Transmitter.h" // LibFlute
+
+#include "openapi/model/FECConfig.h"
+
+#include "FecOtiHelper.hh"
+
+MBSTF_NAMESPACE_START
+
+namespace {
+
+/* fecScheme is a URN naming an IANA "RMT FEC Encoding ID" (RFC 5052). TS 29.580 V18.8.0 clause
+ 6.2.6.2.14, table 6.2.6.2.14-1, row fecScheme: "It shall be identified using a term from the
+ IANA: "Reliable Multicast Transport (RMT) FEC Encoding IDs and FEC Instance IDs" [20] expressed
+ as a URN, e.g.: urn:ietf:rmt:fec:encoding:0". RFC 5053 clause 7 (IANA Considerations): "This
+ document assigns the Fully-Specified FEC Encoding ID 1 under the ietf:rmt:fec:encoding
+ name-space to "Raptor Code"." rt-libflute's own FecScheme enum (include/flute_types.h) fixes the
+ same numeric values on the wire (FEC-OTI-FEC-Encoding-ID): CompactNoCode=0, Raptor=1, RaptorQ=6. */
+const char * const kFecSchemeCompactNoCode = "urn:ietf:rmt:fec:encoding:0";
+const char * const kFecSchemeRaptor = "urn:ietf:rmt:fec:encoding:1";
+const char * const kFecSchemeRaptorQ = "urn:ietf:rmt:fec:encoding:6";
+
+} // anonymous namespace
+
+std::pair, uint32_t> fecOtiFromFecConfig(
+ const std::optional> &fec_information)
+{
+ if (!fec_information || !fec_information.value()) {
+ return {std::nullopt, LibFlute::kDefaultFecRedundancyLevel};
+ }
+
+ const reftools::mbstf::FECConfig &fec_config = *fec_information.value();
+ const std::string &fec_scheme = fec_config.getFecScheme();
+ int32_t fec_overhead = fec_config.getFecOverHead();
+
+ if (fec_overhead < 0) {
+ throw std::runtime_error("fecOverHead must not be negative: " + std::to_string(fec_overhead));
+ }
+
+ if (fec_scheme == kFecSchemeCompactNoCode) {
+ return {std::nullopt, LibFlute::kDefaultFecRedundancyLevel};
+ }
+ if (fec_scheme == kFecSchemeRaptor) {
+ LibFlute::FecOti oti{};
+ oti.encoding_id = LibFlute::FecScheme::Raptor;
+ /* max_source_block_length and encoding_symbol_length are left at their defaults (0):
+ rt-libflute's own Transmitter derives encoding_symbol_length from the session's path MTU
+ and, under the 3GPP profiles, caps max_source_block_length at the 256 KB sub-block
+ ceiling of TS 26.346 V18.2.0 clause 7.2.3 itself when it is left 0. No MBSTF-side bound is invented here. */
+ return {oti, static_cast(fec_overhead)};
+ }
+ if (fec_scheme == kFecSchemeRaptorQ) {
+ /* The FEC schemes the MBMS Download Profile admits are listed in TS 26.346 V18.2.0
+ clause L.4.7, and RaptorQ is not among them. The runtime message below names the clause
+ without its version: a full identifier inside a string literal is read as a citation by
+ the citation checker, which then matches the next string literal in the file. */
+ throw std::runtime_error(
+ "fecScheme " + fec_scheme + " (RaptorQ) is not one of the FEC schemes the MBMS Download "
+ "Profile admits (TS 26.346 clause L.4.7); this MBSTF cannot honour it");
+ }
+ throw std::runtime_error("fecScheme " + fec_scheme + " is not implemented by this MBSTF");
+}
+
+MBSTF_NAMESPACE_STOP
+
+/* vim:ts=8:sts=4:sw=4:expandtab:
+ */
diff --git a/src/mbstf/FecOtiHelper.hh b/src/mbstf/FecOtiHelper.hh
new file mode 100644
index 0000000..a9682e3
--- /dev/null
+++ b/src/mbstf/FecOtiHelper.hh
@@ -0,0 +1,56 @@
+#ifndef _MBSTF_FEC_OTI_HELPER_HH_
+#define _MBSTF_FEC_OTI_HELPER_HH_
+/******************************************************************************
+ * 5G-MAG Reference Tools: MBS Transport Function: FEC OTI helper
+ ******************************************************************************
+ * Copyright: (C)2025-2026 British Broadcasting Corporation
+ * Author(s): Dev Audsin
+ * David Waring
+ * License: 5G-MAG Public License v1
+ *
+ * For full license terms please see the LICENSE file distributed with this
+ * program. If this file is missing then the license can be retrieved from
+ * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view
+ */
+
+#include
+#include
+#include
+#include
+
+#include "Transmitter.h" // LibFlute
+
+#include "common.hh"
+#include "openapi/model/FECConfig.h"
+
+MBSTF_NAMESPACE_START
+
+/* Only Raptor (RFC 5053) is wired here. TS 26.517 V18.6.0 clause 6.2.1 binds every FLUTE object
+ distribution session this component runs to the MBMS Download Profile ("the MBS Distribution
+ Session shall conform to the MBMS Download Profile as defined in clause L.4 of TS 26.346"), and
+ TS 26.346 V18.2.0 clause L.4.7 admits exactly two AL-FEC schemes into that profile: "the Compact
+ No-Code FEC scheme as specified in RFC 3695 [13], and the Raptor FEC scheme as specified in RFC
+ 5053 [91] are optional to implement by the BM-SC and mandatory to support by the UE." RaptorQ is
+ RFC 6330, referenced by neither TS 26.346 nor TS 26.517, and rt-libflute's own Transmitter now
+ refuses it under this profile at construction (Transmitter.cpp, citing the same L.4.7 sentence)
+ rather than send a session no conformant receiver has any obligation to decode. Requesting it
+ here is therefore reported as a packaging failure rather than passed through to a constructor
+ that would throw.
+
+ Returns the Transmitter-level FEC OTI to apply (unset for no FEC or Compact No-Code, which
+ carries no repair symbols of its own and so has the same observable effect) and the FEC
+ redundancy level (TS 26.346 V18.2.0 clause 7.3.2.11) to apply when one is requested. Throws
+ std::runtime_error for a scheme this MBSTF does not wire or a malformed overhead value.
+
+ Shared by every ObjectPackager subclass that constructs its own LibFlute::Transmitter
+ (originally ObjectListPackager-local; extracted so ObjectCarouselPackager can apply the same
+ Distribution-Session-requested FEC configuration instead of always sending unprotected FLUTE). */
+std::pair, uint32_t> fecOtiFromFecConfig(
+ const std::optional> &fec_information);
+
+MBSTF_NAMESPACE_STOP
+
+#endif /* _MBSTF_FEC_OTI_HELPER_HH_ */
+
+/* vim:ts=8:sts=4:sw=4:expandtab:
+ */
diff --git a/src/mbstf/LocalEvents.hh b/src/mbstf/LocalEvents.hh
index 7fe5e15..318f9a4 100644
--- a/src/mbstf/LocalEvents.hh
+++ b/src/mbstf/LocalEvents.hh
@@ -29,13 +29,15 @@ class LocalEvents {
public:
typedef enum {
SEND_NOTIFICATION = OGS_MAX_NUM_OF_PROTO_EVENT+1000,
- RELEASE_SUBSCRIPTION_SVC
+ RELEASE_SUBSCRIPTION_SVC,
+ SUBSCRIPTION_EXPIRED
} LocalEventIds;
static const char *getEventName(Open5GSEvent &event) {
if (event.id() < OGS_MAX_NUM_OF_PROTO_EVENT) return ogs_event_get_name(event.ogsEvent());
if (event.id() == SEND_NOTIFICATION) return "SEND_NOTIFICATION";
if (event.id() == RELEASE_SUBSCRIPTION_SVC) return "RELEASE_SUBSCRIPTION_SVC";
+ if (event.id() == SUBSCRIPTION_EXPIRED) return "SUBSCRIPTION_EXPIRED";
return "Unknown event";
};
};
diff --git a/src/mbstf/MediaTypeInference.cc b/src/mbstf/MediaTypeInference.cc
new file mode 100644
index 0000000..e108f05
--- /dev/null
+++ b/src/mbstf/MediaTypeInference.cc
@@ -0,0 +1,110 @@
+/*
+ * License: 5G-MAG Public License (v1.0)
+ */
+
+#include
+#include
+#include
+#include