Merge R05A04 (r05a04_dev) into master (#114)
* rebuild r05a04_dev from nasty Git merge bullshit; implement handling of SNDCP on the FNE instead of dvmhost; add quick sanity Catch2 testcases; BUGFIX: NXDN SACCH was incorrectly handling the RAN and structure causing the structure value to become overwritten; correct badly set example IP range in FNE config; add AI generated documentation for the network statck, FNE REST and DVMHost REST; update version number for next dev version; * remove old documents; * BUGFIX: fix some unbounded array checking; * enhance InfluxDB implementation: fix bad handling on Windows; fix bad use of free() for address info freeaddrinfo(); fix partial data writes for bigger payloads; enhance getting responses back from InfluxDB (this makes it so that InfluxDB cannot fail silently easily); * EXPERIMENTAL: add an adaptive jitter buffer implementation to the FNE to better deal with peers on bad connections that may send packets out-of-sequence; * remove unnecessary garbage in AI generated doc; use references for autos when looping peers; normalize jitter buffer log messages; * cleanup doc; * properly move hastily erected debug YAML config to the peer ACL file instead; * update jitter buffer doc; * BUGFIX: correct naive implementation that did not consider the length of the packet being sent, which could result in a nullptr after casting the ip struct; BUGFIX: send actual bytes read from VTUN vs the default MTU size; * BUGFIX: correct similar ip struct casting problem; * implement a very preliminary DMR to VTUN IP dispatcher based on the P25 implementation (while I am sure this is probably going to be quite iffy, I am quite sure the VTUN TAP interface method we are using *is* the right direction; even though the P25 implementation is iffy, with iteration over time I suspect it is the correct approach, as such I have implemented a DMR equiviliant based on the P25 implementation); * use 3/4 rate instead of 1 rate; add some dev notes future me; * implement preliminary positive response to KMM HELLO (this still will fail with No-Service due to missing implementation for per-SU UKEK implementation from the crypto container); * add some extra logging for VTUN PDU; * better implement V(R) V(S) checking; * add prereq logging of last load time for various lookup tables; * add fne-stats; add reload peers and crypto; * update FNE REST API doc; * WIN32: fix ctime_r and add Win32 compat for ctime_s so we can compile on Win32 again; * Win32: FNE's P25PacketData clock() in its entirety should do nothing on Windows; * shut the compiler up about order of constructor initializers; * EXPERIMENTAL: add option for 1 second delayed network TDU when a subscriber dekeys; * slight TUI color palette change; * implement support to configure adaptive jitter buffer from peered; * initialize FinalCut palette earlier; * document that key IDs are hex not dec; * BUGFIX: lets not memset past the possible end of the key array....; * track active call counts (this might be slightly error prone, so we will at best call this an approximate number); * attempt to validate source IP/port during a RPTL, RPTK or RPTC event when RPTL, RPTK or RPTC is sent while not in the correct login state; * BUGFIX: dvmpatch did not properly release the network stream ID at the termination of a call, resulting in a hang up; * BUGFIX: for R05A04s 1s delayed TDU, make sure we have valid IDs before allowing a delayed TDU; * BUGFIX: add more stream ID lockout reset handling; * BUGFIX: add more stream ID lockout reset handling; * add some experiemental support to dvmpatch to support clear to enc one-way patching; * BUGFIX: only consider jitter buffer for RTP frames not carrying the end of call sequence (65535); * BUGFIX: always allow the network stack to process and pass RTP frames carrying the end of call sequence (65535); * BUGFIX: dvmbridge, ignore UDP frame timeouts when using RTP or USRP UDP streams; * add dropped call support to dvmpatch; refactor/rewrite dvmpatch encryption support; * hide debug messages; * BUGFIX: validate destination matches before handling TDU; * report ssrc for late packets; * prevent issue with 0 callDstId on dvmpatch; reject TDUs with no destination (these are invalid and can fuck off); * add separate packet dumping option to the network stack, to allow for less-verbose debugging messaging (debugigng where packet dump/trace is not required); * BUGFIX: fix condition in P25 call handler where if the TSBK was not decoded it could result in a crash; * add network debug trace around stream ID reset calls; * better handle framequeue debugging when not performing packet dumping; * implement proper packet reordering for P25 PDU data; BUGFIX: add boundary checking for P25 PDU disassembly; * BUGFIX: fix startup crash of dvmbridge when using analog audio mode; add transmit voice frame status for analog audio; correct core network issue determining length of analog audio frames; * bump copyright dates; * BUGFIX: for unauthorized, unconnected failures the NAK response was not sending the appropriate RTP sequence; * enhance REST API logging; add dev REST API commands to force send NAK packets; * add PDU block reordering support to dvmhost; * minor bugfix, correct incorrect log module for host sourced analog audio; * very experimental fix for #111, along with additional enhancements to repeat parrot traffic to all VCs of a trunk site to ensure parrot traffic repeats; * update bridge config and expressly document the need to properly configure timing parameters; * make the program status variable m_running a static s_running for dvmpatch; ensure PeerNetwork for dvmpatch uses m_packetDump instead of m_debug for packet dumping; * (we're still hunting the cause of bridge ceasing UDP socket operations after a period of time, it *seems* to occur on Win32 only though); make the program status variable m_running a static s_running for dvmbridge; ensure PeerNetwork for dvmbridge uses m_packetDump instead of m_debug for packet dumping; fix shutdown issue with local audio causing a crash for dvmbridge; refactor how RTP and uLaw frames work; remove and deprecate the no include length configuration parameter; refactor and centralize UDP audio writing functionality; add verbose log messages for if the s_running flag becomes non-true during operation; * fix concurrency lockup for DMR granting; * remove main thread sleep control this causes WinSock on Win32 to lose its mind when polled too fast; * move sample level dumping option to CLI; * always reset the call streams at the end of a call; * increase packet processing latency warning to 250ms (1/4 second); * begin adding support for P25P2 network transiting; * add some verbose debug trace for checkNetTrafficCollision(); * begin defining P25 Phase 2 primitives; * P2 abbreviated/explicit partition MCOs are essentially just TSBK opcodes; * add MFID specific partition type; * refactor MAC MCO defines; * correct comment; * make slot numerical; * whoops meant == not =; * implement RTP audio pacing by timestamp sent by source; * for maintainability split modes into separate source CPP files; BUGFIX: fix buffer overflow when copying PCM data; allow uLaw encoded RTP frames to also carry source and dest metadata; * preamble tones are not supported for UDP audio; * BUGFIX: dvmpatch do not send LDU2s until network is in non-idle state; * convert direct MMDVM TDU to a timed TDU; correct some handling of LDU1 srcId and dstId; * remove dvmmon, R05A02 will be the last version to support dvmmon, R05A04+ will not support it in favor of using sysview from the FNE for monitoring; * add more P25 Phase 2 primitives for S-OEMI sync and Hamming (8,4,4) for P2 DUID handling; * more P25 Phase 2 constants; * extreme preliminary work to handle P25 Phase 2 MAC messages (nothing uses this yet this is for future proofing); * continued work on preliminary instrumentation for Phase 2 MAC handling; * fix missing parens; * split the decodeVCH_MACPDU into two functions, one for IEMI the other for OEMI; * bugfix and correct encode/decode for RS 52,30,23, 46,26,21, 45,26,20, 44,16,29; implement catch2 cases for P25 Phase 2 RS codes; correct bad EC check for RS 24,12,13, 24,16,9 and 36,20,17 that could cause false positive failures in edge case conditions; * implement and add more EDAC verification test; * add DMR EMB and QR 16,7,6 tests; correct issue where QR 16,7,6 decode() was correcting and returning the wrong number of bits; correct issue where DMR EMB would not actually use the corrected QR 16,7,6 codeword; * finally what I was trying to get to -- add testcases for the new P2 MAC PDU logic; correct implementation problems with the P2 MAC PDU handling for OEMI and IEMI (the implementation still requires handling scrambling); * add missing Hamming FEC test cases; * finish out the base crypto test suite and include the DES algo; * (this first part is subject to Git revert if it becomes problematic, but because this is a dev branch I am gonna roll with it) revert FrameQueue timestamp list/map changes back to pre-R04J32, the original implementation was far cleaner and faster with O(1) speed for timestamp lookup vs the O(n) lookup, additionally, the newer R04J32+ implementation introduces a pointer that can be dereferenced incorrectly causing a edge case crash; fix the implementation for handling the mutex for timestamp map locking, the implementation used a incorrect instance mutex which could in high-traffic conditions cause a race condition and ultimately a crash; * for condition where a downstream peer is trying to connect to us, only validate the IP instead of IP/port; * EXPERIMENTAL (and untested): allow source ID overriding for parrot playback, this feature will rewrite the source ID to a static ID configured in the FNE configuration, for P25, NXDN and Analog this rewrite is striaghtfoward, for DMR this will likely work oddly due to the way DMR has source ID data embedded in the transited data frames; * validate the RPC data length includes the message; * BUGFIX: handle edge case where a opcode $27 is not an ISP_EMERG_ALARM_REQ but rather its a network sourced OSP_DENY_RSP; BUGFIX: handle incorrect decoding of OSP_DENY_RSP by SysView; BUGFIX: for DVRS operation properly set MFG_MOT for DENY and QUEUE outbounds; * do not use [] access for grantTimers and unitRegTimers, use find() lookups these are safer; * issue a notify CC channel release on a ChanRelease TDULC; * add explicit TDU TG release option, this option allows a CC to process incoming TDUs to determine if a channel grant should be released; add more explicit TDU process logging to the FNE, ensure TDUs being sent outside a call are being logged properly; during a TG unpermit on a non-authoritative VC, transmit a burst of TDUs on the outbound VC RF interface; add extra verbosity to the active TG logging; * add option displayModemDebugMessages to optionally disable or enable debug log messages coming *from* the modem; properly check if we're operating in DFSI or not and if we are do not attempt to set the FIFO buffer lengths or clear buffers related to DMR or NXDN; correct deletion of array types in the DFSICallData structure; correct bad length of the VHDR1 during DFSICallData initialization, the length defaulted to TIA mode which would cause a crash when trying to delete and deallocate VHDR1 when used in V.24 mode; * correct TSBK, TDULC and PDU transmission using V.24 DFSI, timing must be applied like IMBE; lengthen the high-level CC generation timer by 10ms, this has no impact on regular air and hotspot modems, but better times CC frames for V.24 DFSI modems; refactor naming to be clearer for the V.24 jitter buffer transmit types; because V.24 uses OSP_TSBK_RAW and LC_TDULC_RAW correct several off by 2 shift bugs; remove unnecessary V.24 start of stream and end of stream calls; * allow TDUs to be sent regardless of whether a V.24 call was started or not; * do work on V.24 PDU support (inbound should be working pretty much perfect now, outbound -- is still broken); * V.24 PDU work; * allow the user to adjust the frame timeout length when using frame timing at the bridge; ensure audio frames aren't greater then x2 the size of an expected audio frame; * after consideration entirely refactor how UDP frame timing is generated internally when we're using raw PCM; fix a variety of bugs related to locking and handling of the deque for UDP audio frames; better insert silence at the start and end of calls; * disable this debug trace, its too noisy; * do not consider active call or call counts for parrot; * add call collisions to the stats counters; add REST APIs to reset and zero call counters; * log non-call DMR terminators like P25; * correct endpoints for counter reset APIs; * reset active call count on a maintainence loop; * increase V.24 outbound rate for TSDUs; * properly set the V.24 Tx buffer size to the FIFO length like done for air and hotspot modems; pass whether or not the frame being written to the modem is an immediate frame; modify V.24 modem to properly maintain two independant buffers, one for background/normal priority frames, and one for immediate priority frames (this helps with CC mode delays, there are more buffering issues, likely at the modem to mitgate but thats a future project); * update gitignore to include some python stuff; add very very preliminary Python tool that helps generate dvmhost configurations; * add option to disable deny responses in conventional for radios that are braindead and interpret an OSP_DENY as a ISP_EMERG because of opcode reuse (thanks TIA-102); * delete array instead of delete; * add protections to prevent the network mutex from locking too long; add proper network watchdog to prevent unterminated calls from hanging bridge; * ICC and GRANT shouldnt NAK, as this will cause a straight disconnect/reconnect, ignore them instead; * fix issue where in-call control might start looping requests; * lets make sure RID0 is never used, treat as WUID_FNE instead; * only send ICC upstream when the ICC request originates on the local FNE; * Revise cross-compilation instructions in README Updated cross-compilation instructions for ARM 32-bit and ARM 64-bit, including examples and required packages. * relabel some source files in the FNE, FNENetwork -> TrafficNetwork, DiagNetwork -> MetadataNetwork; remove support for disabling the alternate port, this feature is mandatory now; * identify a software SDR; * add support to dvmcfggen for logging configuration; add support to dvmcfggen for user supplied answers files to automate skipping certain wizard questions; * typo; * HIGHLY_UNECESSARY_DISCLAIMER_FOR_THE_MENTAL; * the typo of that define was driving my OCD nuts; * include new statement in README.md; * enforce a users usage understanding by adding a startup configuration parameter that must be set; * these flags are root level; * make not be stupid error more clear; * copy legalese from config.yml to README.md; * policy updates * Update usage_guidelines.md * create issue & pr templates * remove dvmmon from bug & pr template * revert how timestamping is generated; * BUGFIX: fix incorrectly set length for RTP packets, we were incorrectly adding 4U bytes which does not apply to RTP frames; * enrage the entire community by cleaning up the bridge configuration to be more sane -- a lot of these parameters had nothing to do with FNE network configuration; * code cleanup (no functional change); * fix missing marker bit on RTP seq 0 to make jsb stop complaining; fix missing CTS controller teardown; * properly set the FIRST seq (when seq is reset to 0) as a marker; because JSB is *REALLY* hell bent on this, implement continuous RTP seq to make him happy; * wtf; * cleanup typo and bad spacing due to bad VSCode configuration; correct memory leak when handling buffer drop due to new stream packet wraparound; * correct issue where RTP sequences were not stepped appropriately; * ignore m_audioDetect flag for UDP audio outbound to the FNE; attempt to correct becoming stuck on incoming RTP timestamps; * whoops remove debug trace line; * remove incorrect shift; * ensure network watchdog resets all states; ignore TDU/TDULC when local audio or UDP traffic is running; * reset callStartTime for TGs who receive Non-Call TDU/Terminators; * remove duration log from Non-Call TDU; * disable the expected TS code for RTP frames; * add mutex locking when making a copy of the RadioId table to prevent thread-based race conditions; --------- Co-authored-by: Dev_Ranger <30966416+DevRanger@users.noreply.github.com>fix_nid_array_cleanup 2026-03-02
parent
49ff1a461b
commit
274b805517
@ -0,0 +1,139 @@
|
||||
name: Bug report
|
||||
description: Report a reproducible bug in dvmhost or related components
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Issues are for reproducible bugs only.
|
||||
Feature requests and roadmap suggestions will be closed.**
|
||||
|
||||
Please provide enough detail for us to reproduce the issue.
|
||||
Full logs are strongly preferred over screenshots or partial snippets.
|
||||
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Select the affected module
|
||||
options:
|
||||
- dvmhost
|
||||
- dvmfne
|
||||
- dvmbridge
|
||||
- dvmpatch
|
||||
- dvmcmd
|
||||
- sysview
|
||||
- tged
|
||||
- peered
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: component_other
|
||||
attributes:
|
||||
label: If "Other", specify component
|
||||
placeholder: "Describe the affected component"
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version / Commit
|
||||
description: Provide the exact commit SHA or tag
|
||||
placeholder: "ex: v1.2.3 or a1b2c3d4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: build_type
|
||||
attributes:
|
||||
label: Build type
|
||||
options:
|
||||
- Built from source (native)
|
||||
- Cross-compiled
|
||||
- Custom packaging
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: compiler
|
||||
attributes:
|
||||
label: Compiler version
|
||||
description: Output of `gcc --version` or `clang --version`
|
||||
placeholder: "ex: GCC 13.2.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: environment
|
||||
attributes:
|
||||
label: Operating system / architecture
|
||||
placeholder: "ex: Debian 12 x86_64"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: build_flags
|
||||
attributes:
|
||||
label: Build flags / CMake options
|
||||
description: Include any special flags used (ex: cross-compile options)
|
||||
placeholder: |
|
||||
Example:
|
||||
-DCROSS_COMPILE_ARM=1
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Brief description of the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Include exact error messages if applicable
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Full logs
|
||||
description: Paste complete logs or attach files. Redact secrets.
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Relevant config (redacted)
|
||||
description: Include only relevant sections with secrets removed
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
@ -0,0 +1,6 @@
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: DVMProject Community (Discord)
|
||||
url: https://discord.dvmproject.io/
|
||||
about: For questions, discussion, and general support, please join our Discord community.
|
||||
@ -0,0 +1,49 @@
|
||||
## Summary
|
||||
Describe the change and why it is needed.
|
||||
|
||||
## Component(s)
|
||||
Which part(s) of the project does this affect?
|
||||
|
||||
- [ ] dvmhost
|
||||
- [ ] dvmfne
|
||||
- [ ] dvmbridge
|
||||
- [ ] dvmpatch
|
||||
- [ ] dvmcmd
|
||||
- [ ] sysview
|
||||
- [ ] tged
|
||||
- [ ] peered
|
||||
- [ ] Other (explain below)
|
||||
|
||||
## Type of change
|
||||
- [ ] Bug fix
|
||||
- [ ] Refactor / cleanup
|
||||
- [ ] Performance improvement
|
||||
- [ ] Documentation update
|
||||
- [ ] Build / tooling change
|
||||
|
||||
## Related issues
|
||||
Link any related bug reports.
|
||||
|
||||
Closes #
|
||||
Refs #
|
||||
|
||||
## Build & test notes
|
||||
Explain how this was built and tested.
|
||||
|
||||
Include:
|
||||
- OS / distro
|
||||
- Compiler version
|
||||
- Any special build flags
|
||||
|
||||
## Logs / output (if applicable)
|
||||
Paste relevant output demonstrating the fix or behavior change.
|
||||
|
||||
## Checklist
|
||||
- [ ] Change is scoped and focused
|
||||
- [ ] Existing functionality verified
|
||||
- [ ] No unrelated refactors included
|
||||
- [ ] Documentation updated if needed
|
||||
- [ ] No secrets or credentials included
|
||||
|
||||
## Notes for maintainers
|
||||
Anything reviewers should be aware of?
|
||||
@ -0,0 +1,190 @@
|
||||
# Adaptive Jitter Buffer Configuration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The FNE (Fixed Network Equipment) includes an adaptive jitter buffer system that can automatically reorder out-of-sequence RTP packets from peers experiencing network issues such as:
|
||||
|
||||
- **Satellite links** with high latency and variable jitter
|
||||
- **Cellular connections** with packet reordering
|
||||
- **Congested network paths** causing sporadic delays
|
||||
- **Multi-path routing** leading to out-of-order delivery
|
||||
|
||||
The jitter buffer operates with **zero latency for perfect networks** - if packets arrive in order, they pass through immediately without buffering. Only out-of-order packets trigger the adaptive buffering mechanism.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Zero-Latency Fast Path
|
||||
When packets arrive in perfect sequence order, they are processed immediately with **no additional latency**. The jitter buffer is effectively transparent.
|
||||
|
||||
### Adaptive Reordering
|
||||
When an out-of-order packet is detected:
|
||||
1. The jitter buffer holds the packet temporarily
|
||||
2. Waits for missing packets to arrive
|
||||
3. Delivers frames in correct sequence order
|
||||
4. Times out after a configurable period if gaps persist
|
||||
|
||||
### Per-Peer, Per-Stream Isolation
|
||||
- Each peer connection can have independent jitter buffer settings
|
||||
- Within each peer, each call/stream has its own isolated buffer
|
||||
- This prevents one problematic stream from affecting others
|
||||
|
||||
## Configuration
|
||||
|
||||
### Location
|
||||
|
||||
Jitter buffer configuration is defined in the FNE configuration file (typically `fne-config.yml`) under the `master` section:
|
||||
|
||||
```yaml
|
||||
master:
|
||||
# ... other master configuration ...
|
||||
|
||||
jitterBuffer:
|
||||
enabled: false
|
||||
defaultMaxSize: 4
|
||||
defaultMaxWait: 40000
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
#### Global Settings
|
||||
|
||||
- **enabled** (boolean, default: `false`)
|
||||
- Master enable/disable switch for jitter buffering
|
||||
- When `false`, all peers operate with zero-latency pass-through
|
||||
- When `true`, peers use jitter buffering with default parameters
|
||||
|
||||
- **defaultMaxSize** (integer, range: 2-8, default: `4`)
|
||||
- Maximum number of frames to buffer per stream
|
||||
- Larger values provide more reordering capability but add latency
|
||||
- **Recommended values:**
|
||||
- `4` - Standard networks (LAN, stable WAN)
|
||||
- `6` - High-jitter networks (cellular, congested paths)
|
||||
- `8` - Extreme conditions (satellite, very poor links)
|
||||
|
||||
- **defaultMaxWait** (integer, range: 10000-200000 microseconds, default: `40000`)
|
||||
- Maximum time to wait for missing packets
|
||||
- Frames older than this are delivered even with gaps
|
||||
- **Recommended values:**
|
||||
- `40000` (40ms) - Terrestrial networks
|
||||
- `60000` (60ms) - Cellular networks
|
||||
- `80000` (80ms) - Satellite links
|
||||
|
||||
Per-Peer overrides occur with the jitter buffer parameters within the peer ACL file. The same global parameters, apply
|
||||
there but on a per-peer basis. Global jitter buffer parameters take precedence over per-peer.
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Example 1: Disabled (Default)
|
||||
|
||||
For networks with reliable connectivity:
|
||||
|
||||
```yaml
|
||||
master:
|
||||
jitterBuffer:
|
||||
enabled: false
|
||||
defaultMaxSize: 4
|
||||
defaultMaxWait: 40000
|
||||
```
|
||||
|
||||
All peers operate with zero-latency pass-through. Best for:
|
||||
- Local area networks
|
||||
- Stable dedicated connections
|
||||
- Networks with minimal packet loss/reordering
|
||||
|
||||
### Example 2: Global Enable with Defaults
|
||||
|
||||
Enable jitter buffering for all peers with conservative settings:
|
||||
|
||||
```yaml
|
||||
master:
|
||||
jitterBuffer:
|
||||
enabled: true
|
||||
defaultMaxSize: 4
|
||||
defaultMaxWait: 40000
|
||||
```
|
||||
|
||||
Good starting point for:
|
||||
- Mixed network environments
|
||||
- Networks with occasional jitter
|
||||
- General purpose deployments
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### CPU Impact
|
||||
|
||||
- **Zero-latency path:** Negligible overhead (~1 comparison per packet)
|
||||
- **Buffering path:** Minimal overhead (~map lookup + timestamp check)
|
||||
- **Memory:** ~500 bytes per active stream buffer
|
||||
|
||||
### Latency Impact
|
||||
|
||||
- **In-order packets:** 0ms additional latency
|
||||
- **Out-of-order packets:** Buffered until:
|
||||
- Missing packets arrive, OR
|
||||
- `maxWait` timeout expires
|
||||
- **Typical latency:** 10-40ms for reordered packets on terrestrial networks
|
||||
|
||||
### Effectiveness
|
||||
|
||||
Based on the adaptive jitter buffer design:
|
||||
- **100% pass-through** for perfect networks (zero latency)
|
||||
- **~95-99% recovery** of out-of-order packets within timeout window
|
||||
- **Automatic timeout delivery** prevents indefinite stalling
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Symptom: Audio/Data Gaps Despite Jitter Buffer
|
||||
|
||||
**Possible Causes:**
|
||||
1. `maxWait` timeout too short for network conditions
|
||||
2. `maxSize` buffer too small for reordering depth
|
||||
3. Actual packet loss (not just reordering)
|
||||
|
||||
**Solutions:**
|
||||
- Increase `maxWait` by 20-40ms increments
|
||||
- Increase `maxSize` by 1-2 frames
|
||||
- Verify network packet loss with diagnostics
|
||||
|
||||
### Symptom: Excessive Latency
|
||||
|
||||
**Possible Causes:**
|
||||
1. Jitter buffer enabled on stable connections
|
||||
2. `maxWait` set too high
|
||||
3. `maxSize` set too large
|
||||
|
||||
**Solutions:**
|
||||
- Disable jitter buffer for known-good peers using overrides
|
||||
- Reduce `maxWait` in 10-20ms decrements
|
||||
- Reduce `maxSize` to minimum (2-4 frames)
|
||||
|
||||
### Symptom: No Improvement
|
||||
|
||||
**Possible Causes:**
|
||||
1. Jitter buffer not actually enabled for the problematic peer
|
||||
2. Issues beyond reordering (e.g., corruption, auth failures)
|
||||
3. Problems at application layer, not transport layer
|
||||
|
||||
**Solutions:**
|
||||
- Verify peer override configuration is correct
|
||||
- Check FNE logs for peer-specific configuration messages
|
||||
- Enable verbose and debug logging to trace packet flow
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start Disabled**: Begin with jitter buffering disabled and enable only as needed
|
||||
2. **Target Specific Peers**: Use per-peer overrides rather than global enable when possible
|
||||
3. **Conservative Tuning**: Start with default parameters and adjust incrementally
|
||||
4. **Monitor Performance**: Watch for signs of latency or audio quality issues
|
||||
5. **Document Changes**: Keep records of which peers need special configuration
|
||||
6. **Test Thoroughly**: Validate changes don't introduce unintended latency
|
||||
|
||||
## Reference
|
||||
|
||||
### Configuration Schema
|
||||
|
||||
```yaml
|
||||
jitterBuffer:
|
||||
enabled: <boolean> # false
|
||||
defaultMaxSize: <2-8> # 4
|
||||
defaultMaxWait: <10000-200000> # 40000
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,247 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Bridge
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
|
||||
* Copyright (C) 2025 Caleb, K4PHP
|
||||
* Copyright (C) 2025 Lorenzo L Romero, K2LLR
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/analog/AnalogDefines.h"
|
||||
#include "common/analog/AnalogAudio.h"
|
||||
#include "common/analog/data/NetData.h"
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
#include "bridge/ActivityLog.h"
|
||||
#include "HostBridge.h"
|
||||
#include "BridgeMain.h"
|
||||
|
||||
using namespace analog;
|
||||
using namespace analog::defines;
|
||||
using namespace network;
|
||||
using namespace network::frame;
|
||||
using namespace network::udp;
|
||||
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Class Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
** Analog
|
||||
*/
|
||||
|
||||
/* Helper to process analog network traffic. */
|
||||
|
||||
void HostBridge::processAnalogNetwork(uint8_t* buffer, uint32_t length)
|
||||
{
|
||||
assert(buffer != nullptr);
|
||||
using namespace analog;
|
||||
using namespace analog::defines;
|
||||
|
||||
if (m_txMode != TX_MODE_ANALOG)
|
||||
return;
|
||||
|
||||
// process network message header
|
||||
uint8_t seqNo = buffer[4U];
|
||||
|
||||
uint32_t srcId = GET_UINT24(buffer, 5U);
|
||||
uint32_t dstId = GET_UINT24(buffer, 8U);
|
||||
|
||||
bool individual = (buffer[15] & 0x40U) == 0x40U;
|
||||
|
||||
AudioFrameType::E frameType = (AudioFrameType::E)(buffer[15U] & 0x0FU);
|
||||
|
||||
data::NetData analogData;
|
||||
analogData.setSeqNo(seqNo);
|
||||
analogData.setSrcId(srcId);
|
||||
analogData.setDstId(dstId);
|
||||
analogData.setFrameType(frameType);
|
||||
|
||||
analogData.setAudio(buffer + 20U);
|
||||
|
||||
uint8_t frame[AUDIO_SAMPLES_LENGTH_BYTES];
|
||||
analogData.getAudio(frame);
|
||||
|
||||
if (m_debug) {
|
||||
LogDebug(LOG_NET, "Analog, seqNo = %u, srcId = %u, dstId = %u, len = %u", seqNo, srcId, dstId, length);
|
||||
}
|
||||
|
||||
if (!individual) {
|
||||
if (srcId == 0)
|
||||
return;
|
||||
|
||||
// ensure destination ID matches and slot matches
|
||||
if (dstId != m_dstId)
|
||||
return;
|
||||
|
||||
m_networkWatchdog.start();
|
||||
|
||||
// is this a new call stream?
|
||||
if (m_network->getAnalogStreamId() != m_rxStreamId && !m_callInProgress) {
|
||||
m_callInProgress = true;
|
||||
m_callAlgoId = 0U;
|
||||
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
m_rxStartTime = now;
|
||||
|
||||
LogInfoEx(LOG_HOST, "Analog, call start, srcId = %u, dstId = %u", srcId, dstId);
|
||||
if (m_preambleLeaderTone)
|
||||
generatePreambleTone();
|
||||
}
|
||||
|
||||
// process call termination
|
||||
if (frameType == AudioFrameType::TERMINATOR) {
|
||||
m_callInProgress = false;
|
||||
m_networkWatchdog.stop();
|
||||
m_ignoreCall = false;
|
||||
m_callAlgoId = 0U;
|
||||
|
||||
if (m_rxStartTime > 0U) {
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
uint64_t diff = now - m_rxStartTime;
|
||||
|
||||
LogInfoEx(LOG_HOST, "Analog, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
|
||||
}
|
||||
|
||||
m_rxStartTime = 0U;
|
||||
m_rxStreamId = 0U;
|
||||
|
||||
if (!m_udpRTPContinuousSeq) {
|
||||
m_rtpInitialFrame = false;
|
||||
m_rtpSeqNo = 0U;
|
||||
}
|
||||
m_rtpTimestamp = INVALID_TS;
|
||||
m_network->resetAnalog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ignoreCall && m_callAlgoId == 0U)
|
||||
m_ignoreCall = false;
|
||||
|
||||
if (m_ignoreCall)
|
||||
return;
|
||||
|
||||
// decode audio frames
|
||||
if (frameType == AudioFrameType::VOICE_START || frameType == AudioFrameType::VOICE) {
|
||||
LogInfoEx(LOG_NET, ANO_VOICE ", audio, srcId = %u, dstId = %u, seqNo = %u", srcId, dstId, analogData.getSeqNo());
|
||||
|
||||
short samples[AUDIO_SAMPLES_LENGTH];
|
||||
int smpIdx = 0;
|
||||
for (uint32_t pcmIdx = 0; pcmIdx < AUDIO_SAMPLES_LENGTH; pcmIdx++) {
|
||||
samples[smpIdx] = AnalogAudio::decodeMuLaw(frame[pcmIdx]);
|
||||
smpIdx++;
|
||||
}
|
||||
|
||||
// post-process: apply gain to decoded audio frames
|
||||
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain);
|
||||
|
||||
if (m_localAudio) {
|
||||
m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH);
|
||||
}
|
||||
|
||||
if (m_udpAudio) {
|
||||
int pcmIdx = 0;
|
||||
uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U];
|
||||
if (m_udpUseULaw) {
|
||||
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
|
||||
pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
|
||||
}
|
||||
|
||||
if (m_trace)
|
||||
Utils::dump(1U, "HostBridge()::processAnalogNetwork(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH);
|
||||
|
||||
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U);
|
||||
} else {
|
||||
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
|
||||
pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
|
||||
pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
|
||||
pcmIdx += 2;
|
||||
}
|
||||
|
||||
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_rxStreamId = m_network->getAnalogStreamId();
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper to encode analog network traffic audio frames. */
|
||||
|
||||
void HostBridge::encodeAnalogAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId)
|
||||
{
|
||||
assert(pcm != nullptr);
|
||||
using namespace analog;
|
||||
using namespace analog::defines;
|
||||
using namespace analog::data;
|
||||
|
||||
if (m_analogN == 254U)
|
||||
m_analogN = 0;
|
||||
|
||||
int smpIdx = 0;
|
||||
short samples[AUDIO_SAMPLES_LENGTH];
|
||||
for (uint32_t pcmIdx = 0; pcmIdx < (AUDIO_SAMPLES_LENGTH * 2U); pcmIdx += 2) {
|
||||
samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
|
||||
smpIdx++;
|
||||
}
|
||||
|
||||
// pre-process: apply gain to PCM audio frames
|
||||
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain);
|
||||
|
||||
uint32_t srcId = m_srcId;
|
||||
if (m_srcIdOverride != 0 && (m_overrideSrcIdFromMDC))
|
||||
srcId = m_srcIdOverride;
|
||||
if (m_overrideSrcIdFromUDP)
|
||||
srcId = m_udpSrcId;
|
||||
if (forcedSrcId > 0 && forcedSrcId != m_srcId)
|
||||
srcId = forcedSrcId;
|
||||
uint32_t dstId = m_dstId;
|
||||
if (forcedDstId > 0 && forcedDstId != m_dstId)
|
||||
dstId = forcedDstId;
|
||||
|
||||
// never allow a source ID of 0
|
||||
if (srcId == 0U)
|
||||
srcId = m_srcId;
|
||||
|
||||
data::NetData analogData;
|
||||
analogData.setSeqNo(m_analogN);
|
||||
analogData.setSrcId(srcId);
|
||||
analogData.setDstId(dstId);
|
||||
analogData.setControl(0U);
|
||||
analogData.setFrameType(AudioFrameType::VOICE);
|
||||
if (m_txStreamId <= 1U) {
|
||||
analogData.setFrameType(AudioFrameType::VOICE_START);
|
||||
|
||||
if (m_grantDemand) {
|
||||
analogData.setControl(0x80U); // analog remote grant demand flag
|
||||
}
|
||||
}
|
||||
|
||||
int pcmIdx = 0;
|
||||
uint8_t outPcm[AUDIO_SAMPLES_LENGTH * 2U];
|
||||
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
|
||||
outPcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
|
||||
}
|
||||
|
||||
if (m_trace)
|
||||
Utils::dump(1U, "HostBridge()::encodeAnalogAudioFrame(), Encoded uLaw Audio", outPcm, AUDIO_SAMPLES_LENGTH);
|
||||
|
||||
analogData.setAudio(outPcm);
|
||||
|
||||
if (analogData.getFrameType() == AudioFrameType::VOICE) {
|
||||
LogInfoEx(LOG_HOST, ANO_VOICE ", audio, srcId = %u, dstId = %u, seqNo = %u", srcId, dstId, analogData.getSeqNo());
|
||||
}
|
||||
|
||||
m_network->writeAnalog(analogData);
|
||||
m_txStreamId = m_network->getAnalogStreamId();
|
||||
m_analogN++;
|
||||
}
|
||||
@ -0,0 +1,472 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Bridge
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
|
||||
* Copyright (C) 2025 Caleb, K4PHP
|
||||
* Copyright (C) 2025 Lorenzo L Romero, K2LLR
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/analog/AnalogDefines.h"
|
||||
#include "common/analog/AnalogAudio.h"
|
||||
#include "common/dmr/DMRDefines.h"
|
||||
#include "common/dmr/data/EMB.h"
|
||||
#include "common/dmr/data/NetData.h"
|
||||
#include "common/dmr/lc/FullLC.h"
|
||||
#include "common/dmr/SlotType.h"
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
#include "bridge/ActivityLog.h"
|
||||
#include "HostBridge.h"
|
||||
#include "BridgeMain.h"
|
||||
|
||||
using namespace analog;
|
||||
using namespace analog::defines;
|
||||
using namespace network;
|
||||
using namespace network::frame;
|
||||
using namespace network::udp;
|
||||
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#endif // !defined(_WIN32)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Class Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
** Digital Mobile Radio
|
||||
*/
|
||||
|
||||
/* Helper to process DMR network traffic. */
|
||||
|
||||
void HostBridge::processDMRNetwork(uint8_t* buffer, uint32_t length)
|
||||
{
|
||||
assert(buffer != nullptr);
|
||||
using namespace dmr;
|
||||
using namespace dmr::defines;
|
||||
|
||||
if (m_txMode != TX_MODE_DMR) {
|
||||
m_network->resetDMR(1U);
|
||||
m_network->resetDMR(2U);
|
||||
return;
|
||||
}
|
||||
|
||||
// process network message header
|
||||
uint8_t seqNo = buffer[4U];
|
||||
|
||||
uint32_t srcId = GET_UINT24(buffer, 5U);
|
||||
uint32_t dstId = GET_UINT24(buffer, 8U);
|
||||
|
||||
FLCO::E flco = (buffer[15U] & 0x40U) == 0x40U ? FLCO::PRIVATE : FLCO::GROUP;
|
||||
|
||||
uint32_t slotNo = (buffer[15U] & 0x80U) == 0x80U ? 2U : 1U;
|
||||
|
||||
if (slotNo > 3U) {
|
||||
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
||||
m_network->resetDMR(1U);
|
||||
m_network->resetDMR(2U);
|
||||
return;
|
||||
}
|
||||
|
||||
// DMO mode slot disabling
|
||||
if (slotNo == 1U && !m_network->getDuplex()) {
|
||||
LogError(LOG_DMR, "DMR/DMO, invalid slot, slotNo = %u", slotNo);
|
||||
m_network->resetDMR(1U);
|
||||
return;
|
||||
}
|
||||
|
||||
// Individual slot disabling
|
||||
if (slotNo == 1U && !m_network->getSlot1()) {
|
||||
LogError(LOG_DMR, "DMR, invalid slot, slot 1 disabled, slotNo = %u", slotNo);
|
||||
m_network->resetDMR(1U);
|
||||
return;
|
||||
}
|
||||
if (slotNo == 2U && !m_network->getSlot2()) {
|
||||
LogError(LOG_DMR, "DMR, invalid slot, slot 2 disabled, slotNo = %u", slotNo);
|
||||
m_network->resetDMR(2U);
|
||||
return;
|
||||
}
|
||||
|
||||
bool dataSync = (buffer[15U] & 0x20U) == 0x20U;
|
||||
bool voiceSync = (buffer[15U] & 0x10U) == 0x10U;
|
||||
|
||||
if (m_debug) {
|
||||
LogDebug(LOG_NET, "DMR, seqNo = %u, srcId = %u, dstId = %u, flco = $%02X, slotNo = %u, len = %u", seqNo, srcId, dstId, flco, slotNo, length);
|
||||
}
|
||||
|
||||
// process raw DMR data bytes
|
||||
UInt8Array data = std::unique_ptr<uint8_t[]>(new uint8_t[DMR_FRAME_LENGTH_BYTES]);
|
||||
::memset(data.get(), 0x00U, DMR_FRAME_LENGTH_BYTES);
|
||||
DataType::E dataType = DataType::VOICE_SYNC;
|
||||
uint8_t n = 0U;
|
||||
if (dataSync) {
|
||||
dataType = (DataType::E)(buffer[15U] & 0x0FU);
|
||||
::memcpy(data.get(), buffer + 20U, DMR_FRAME_LENGTH_BYTES);
|
||||
}
|
||||
else if (voiceSync) {
|
||||
::memcpy(data.get(), buffer + 20U, DMR_FRAME_LENGTH_BYTES);
|
||||
}
|
||||
else {
|
||||
n = buffer[15U] & 0x0FU;
|
||||
dataType = DataType::VOICE;
|
||||
::memcpy(data.get(), buffer + 20U, DMR_FRAME_LENGTH_BYTES);
|
||||
}
|
||||
|
||||
if (flco == FLCO::GROUP) {
|
||||
if (srcId == 0) {
|
||||
m_network->resetDMR(slotNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure destination ID matches and slot matches
|
||||
if (dstId != m_dstId) {
|
||||
m_network->resetDMR(slotNo);
|
||||
return;
|
||||
}
|
||||
if (slotNo != m_slot) {
|
||||
m_network->resetDMR(slotNo);
|
||||
return;
|
||||
}
|
||||
|
||||
m_networkWatchdog.start();
|
||||
|
||||
// is this a new call stream?
|
||||
if (m_network->getDMRStreamId(slotNo) != m_rxStreamId && !m_callInProgress) {
|
||||
m_callInProgress = true;
|
||||
m_callAlgoId = 0U;
|
||||
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
m_rxStartTime = now;
|
||||
|
||||
LogInfoEx(LOG_HOST, "DMR, call start, srcId = %u, dstId = %u, slot = %u", srcId, dstId, slotNo);
|
||||
if (m_preambleLeaderTone)
|
||||
generatePreambleTone();
|
||||
|
||||
// if we can, use the LC from the voice header as to keep all options intact
|
||||
if (dataSync && (dataType == DataType::VOICE_LC_HEADER)) {
|
||||
lc::LC lc = lc::LC();
|
||||
lc::FullLC fullLC = lc::FullLC();
|
||||
lc = *fullLC.decode(data.get(), DataType::VOICE_LC_HEADER);
|
||||
|
||||
m_rxDMRLC = lc;
|
||||
}
|
||||
else {
|
||||
// if we don't have a voice header; don't wait to decode it, just make a dummy header
|
||||
m_rxDMRLC = lc::LC();
|
||||
m_rxDMRLC.setDstId(dstId);
|
||||
m_rxDMRLC.setSrcId(srcId);
|
||||
}
|
||||
|
||||
m_rxDMRPILC = lc::PrivacyLC();
|
||||
}
|
||||
|
||||
// if we can, use the PI LC from the PI voice header as to keep all options intact
|
||||
if (dataSync && (dataType == DataType::VOICE_PI_HEADER)) {
|
||||
lc::PrivacyLC lc = lc::PrivacyLC();
|
||||
lc::FullLC fullLC = lc::FullLC();
|
||||
lc = *fullLC.decodePI(data.get());
|
||||
|
||||
m_rxDMRPILC = lc;
|
||||
m_callAlgoId = lc.getAlgId();
|
||||
}
|
||||
|
||||
// process call termination
|
||||
if (dataSync && (dataType == DataType::TERMINATOR_WITH_LC)) {
|
||||
m_callInProgress = false;
|
||||
m_networkWatchdog.stop();
|
||||
m_ignoreCall = false;
|
||||
m_callAlgoId = 0U;
|
||||
|
||||
if (m_rxStartTime > 0U) {
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
uint64_t diff = now - m_rxStartTime;
|
||||
|
||||
LogInfoEx(LOG_HOST, "DMR, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
|
||||
}
|
||||
|
||||
m_rxDMRLC = lc::LC();
|
||||
m_rxDMRPILC = lc::PrivacyLC();
|
||||
m_rxStartTime = 0U;
|
||||
m_rxStreamId = 0U;
|
||||
|
||||
if (!m_udpRTPContinuousSeq) {
|
||||
m_rtpInitialFrame = false;
|
||||
m_rtpSeqNo = 0U;
|
||||
}
|
||||
m_rtpTimestamp = INVALID_TS;
|
||||
m_network->resetDMR(slotNo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ignoreCall && m_callAlgoId == 0U)
|
||||
m_ignoreCall = false;
|
||||
|
||||
if (m_ignoreCall) {
|
||||
m_network->resetDMR(slotNo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_callAlgoId != 0U) {
|
||||
if (m_callInProgress) {
|
||||
m_callInProgress = false;
|
||||
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
uint64_t diff = now - m_rxStartTime;
|
||||
|
||||
// send USRP end of transmission
|
||||
if (m_udpUsrp)
|
||||
sendUsrpEot();
|
||||
|
||||
LogInfoEx(LOG_HOST, "P25, call end (T), srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
|
||||
}
|
||||
|
||||
m_ignoreCall = true;
|
||||
m_network->resetDMR(slotNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// process audio frames
|
||||
if (dataType == DataType::VOICE_SYNC || dataType == DataType::VOICE) {
|
||||
uint8_t ambe[27U];
|
||||
::memcpy(ambe, data.get(), 14U);
|
||||
ambe[13] &= 0xF0;
|
||||
ambe[13] |= (uint8_t)(data[19] & 0x0F);
|
||||
::memcpy(ambe + 14U, data.get() + 20U, 13U);
|
||||
|
||||
LogInfoEx(LOG_NET, DMR_DT_VOICE ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = %u", slotNo, srcId, dstId, n);
|
||||
decodeDMRAudioFrame(ambe, srcId, dstId, n);
|
||||
}
|
||||
|
||||
m_rxStreamId = m_network->getDMRStreamId(slotNo);
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper to decode DMR network traffic audio frames. */
|
||||
|
||||
void HostBridge::decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dstId, uint8_t dmrN)
|
||||
{
|
||||
assert(ambe != nullptr);
|
||||
using namespace dmr;
|
||||
using namespace dmr::defines;
|
||||
|
||||
for (uint32_t n = 0; n < AMBE_PER_SLOT; n++) {
|
||||
uint8_t ambePartial[RAW_AMBE_LENGTH_BYTES];
|
||||
for (uint32_t i = 0; i < RAW_AMBE_LENGTH_BYTES; i++)
|
||||
ambePartial[i] = ambe[i + (n * 9)];
|
||||
|
||||
short samples[AUDIO_SAMPLES_LENGTH];
|
||||
int errs = 0;
|
||||
#if defined(_WIN32)
|
||||
if (m_useExternalVocoder) {
|
||||
ambeDecode(ambePartial, RAW_AMBE_LENGTH_BYTES, samples);
|
||||
}
|
||||
else {
|
||||
#endif // defined(_WIN32)
|
||||
m_decoder->decode(ambePartial, samples);
|
||||
#if defined(_WIN32)
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
if (m_debug)
|
||||
LogInfoEx(LOG_HOST, DMR_DT_VOICE ", Frame, VC%u.%u, srcId = %u, dstId = %u, errs = %u", dmrN, n, srcId, dstId, errs);
|
||||
|
||||
// post-process: apply gain to decoded audio frames
|
||||
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain);
|
||||
|
||||
if (m_localAudio) {
|
||||
m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH);
|
||||
// Assert RTS PTT when audio is being sent to output
|
||||
assertRtsPtt();
|
||||
}
|
||||
|
||||
if (m_udpAudio) {
|
||||
int pcmIdx = 0;
|
||||
uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U];
|
||||
// are we sending uLaw encoded audio?
|
||||
if (m_udpUseULaw) {
|
||||
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
|
||||
pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
|
||||
}
|
||||
|
||||
if (m_trace)
|
||||
Utils::dump(1U, "HostBridge()::decodeDMRAudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH);
|
||||
|
||||
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U);
|
||||
} else {
|
||||
// raw PCM audio
|
||||
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
|
||||
pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
|
||||
pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
|
||||
pcmIdx += 2;
|
||||
}
|
||||
|
||||
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper to encode DMR network traffic audio frames. */
|
||||
|
||||
void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId)
|
||||
{
|
||||
assert(pcm != nullptr);
|
||||
using namespace dmr;
|
||||
using namespace dmr::defines;
|
||||
using namespace dmr::data;
|
||||
|
||||
uint32_t srcId = m_srcId;
|
||||
if (m_srcIdOverride != 0 && (m_overrideSrcIdFromMDC))
|
||||
srcId = m_srcIdOverride;
|
||||
if (m_overrideSrcIdFromUDP)
|
||||
srcId = m_udpSrcId;
|
||||
if (forcedSrcId > 0 && forcedSrcId != m_srcId)
|
||||
srcId = forcedSrcId;
|
||||
uint32_t dstId = m_dstId;
|
||||
if (forcedDstId > 0 && forcedDstId != m_dstId)
|
||||
dstId = forcedDstId;
|
||||
|
||||
// never allow a source ID of 0
|
||||
if (srcId == 0U)
|
||||
srcId = m_srcId;
|
||||
|
||||
uint8_t* data = nullptr;
|
||||
m_dmrN = (uint8_t)(m_dmrSeqNo % 6);
|
||||
if (m_ambeCount == AMBE_PER_SLOT) {
|
||||
// is this the intitial sequence?
|
||||
if (m_dmrSeqNo == 0) {
|
||||
// send DMR voice header
|
||||
data = new uint8_t[DMR_FRAME_LENGTH_BYTES];
|
||||
|
||||
// generate DMR LC
|
||||
lc::LC dmrLC = lc::LC();
|
||||
dmrLC.setFLCO(FLCO::GROUP);
|
||||
dmrLC.setSrcId(srcId);
|
||||
dmrLC.setDstId(dstId);
|
||||
m_dmrEmbeddedData.setLC(dmrLC);
|
||||
|
||||
// generate the Slot TYpe
|
||||
SlotType slotType = SlotType();
|
||||
slotType.setDataType(DataType::VOICE_LC_HEADER);
|
||||
slotType.encode(data);
|
||||
|
||||
lc::FullLC fullLC = lc::FullLC();
|
||||
fullLC.encode(dmrLC, data, DataType::VOICE_LC_HEADER);
|
||||
|
||||
// generate DMR network frame
|
||||
NetData dmrData;
|
||||
dmrData.setSlotNo(m_slot);
|
||||
dmrData.setDataType(DataType::VOICE_LC_HEADER);
|
||||
dmrData.setSrcId(srcId);
|
||||
dmrData.setDstId(dstId);
|
||||
dmrData.setFLCO(FLCO::GROUP);
|
||||
|
||||
uint8_t controlByte = 0U;
|
||||
if (m_grantDemand)
|
||||
controlByte = network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag
|
||||
controlByte |= network::NET_CTRL_SWITCH_OVER;
|
||||
dmrData.setControl(controlByte);
|
||||
|
||||
dmrData.setN(m_dmrN);
|
||||
dmrData.setSeqNo(m_dmrSeqNo);
|
||||
dmrData.setBER(0U);
|
||||
dmrData.setRSSI(0U);
|
||||
|
||||
dmrData.setData(data);
|
||||
|
||||
LogInfoEx(LOG_HOST, DMR_DT_VOICE_LC_HEADER ", slot = %u, srcId = %u, dstId = %u, FLCO = $%02X", m_slot,
|
||||
dmrLC.getSrcId(), dmrLC.getDstId(), dmrData.getFLCO());
|
||||
|
||||
m_network->writeDMR(dmrData, false);
|
||||
m_txStreamId = m_network->getDMRStreamId(m_slot);
|
||||
|
||||
m_dmrSeqNo++;
|
||||
delete[] data;
|
||||
}
|
||||
|
||||
// send DMR voice
|
||||
data = new uint8_t[DMR_FRAME_LENGTH_BYTES];
|
||||
|
||||
::memcpy(data, m_ambeBuffer, 13U);
|
||||
data[13U] = (uint8_t)(m_ambeBuffer[13U] & 0xF0);
|
||||
data[19U] = (uint8_t)(m_ambeBuffer[13U] & 0x0F);
|
||||
::memcpy(data + 20U, m_ambeBuffer + 14U, 13U);
|
||||
|
||||
DataType::E dataType = DataType::VOICE_SYNC;
|
||||
if (m_dmrN == 0)
|
||||
dataType = DataType::VOICE_SYNC;
|
||||
else {
|
||||
dataType = DataType::VOICE;
|
||||
|
||||
uint8_t lcss = m_dmrEmbeddedData.getData(data, m_dmrN);
|
||||
|
||||
// generated embedded signalling
|
||||
EMB emb = EMB();
|
||||
emb.setColorCode(0U);
|
||||
emb.setLCSS(lcss);
|
||||
emb.encode(data);
|
||||
}
|
||||
|
||||
LogInfoEx(LOG_HOST, DMR_DT_VOICE ", srcId = %u, dstId = %u, slot = %u, seqNo = %u", srcId, dstId, m_slot, m_dmrN);
|
||||
|
||||
// generate DMR network frame
|
||||
NetData dmrData;
|
||||
dmrData.setSlotNo(m_slot);
|
||||
dmrData.setDataType(dataType);
|
||||
dmrData.setSrcId(srcId);
|
||||
dmrData.setDstId(dstId);
|
||||
dmrData.setFLCO(FLCO::GROUP);
|
||||
dmrData.setN(m_dmrN);
|
||||
dmrData.setSeqNo(m_dmrSeqNo);
|
||||
dmrData.setBER(0U);
|
||||
dmrData.setRSSI(0U);
|
||||
|
||||
dmrData.setData(data);
|
||||
|
||||
m_network->writeDMR(dmrData, false);
|
||||
m_txStreamId = m_network->getDMRStreamId(m_slot);
|
||||
|
||||
m_dmrSeqNo++;
|
||||
::memset(m_ambeBuffer, 0x00U, 27U);
|
||||
m_ambeCount = 0U;
|
||||
}
|
||||
|
||||
int smpIdx = 0;
|
||||
short samples[AUDIO_SAMPLES_LENGTH];
|
||||
for (uint32_t pcmIdx = 0; pcmIdx < (AUDIO_SAMPLES_LENGTH * 2U); pcmIdx += 2) {
|
||||
samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
|
||||
smpIdx++;
|
||||
}
|
||||
|
||||
// pre-process: apply gain to PCM audio frames
|
||||
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain);
|
||||
|
||||
// encode PCM samples into AMBE codewords
|
||||
uint8_t ambe[RAW_AMBE_LENGTH_BYTES];
|
||||
::memset(ambe, 0x00U, RAW_AMBE_LENGTH_BYTES);
|
||||
#if defined(_WIN32)
|
||||
if (m_useExternalVocoder) {
|
||||
ambeEncode(samples, AUDIO_SAMPLES_LENGTH, ambe);
|
||||
}
|
||||
else {
|
||||
#endif // defined(_WIN32)
|
||||
m_encoder->encode(samples, ambe);
|
||||
#if defined(_WIN32)
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
// Utils::dump(1U, "HostBridge::encodeDMRAudioFrame(), Encoded AMBE", ambe, RAW_AMBE_LENGTH_BYTES);
|
||||
|
||||
::memcpy(m_ambeBuffer + (m_ambeCount * 9U), ambe, RAW_AMBE_LENGTH_BYTES);
|
||||
m_ambeCount++;
|
||||
}
|
||||
@ -0,0 +1,713 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Bridge
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
|
||||
* Copyright (C) 2025 Caleb, K4PHP
|
||||
* Copyright (C) 2025 Lorenzo L Romero, K2LLR
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/analog/AnalogDefines.h"
|
||||
#include "common/analog/AnalogAudio.h"
|
||||
#include "common/p25/P25Defines.h"
|
||||
#include "common/p25/data/LowSpeedData.h"
|
||||
#include "common/p25/dfsi/DFSIDefines.h"
|
||||
#include "common/p25/dfsi/LC.h"
|
||||
#include "common/p25/lc/LC.h"
|
||||
#include "common/p25/P25Utils.h"
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
#include "bridge/ActivityLog.h"
|
||||
#include "HostBridge.h"
|
||||
#include "BridgeMain.h"
|
||||
|
||||
using namespace analog;
|
||||
using namespace analog::defines;
|
||||
using namespace network;
|
||||
using namespace network::frame;
|
||||
using namespace network::udp;
|
||||
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#endif // !defined(_WIN32)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Class Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
** Project 25
|
||||
*/
|
||||
|
||||
/* Helper to process P25 network traffic. */
|
||||
|
||||
void HostBridge::processP25Network(uint8_t* buffer, uint32_t length)
|
||||
{
|
||||
assert(buffer != nullptr);
|
||||
using namespace p25;
|
||||
using namespace p25::defines;
|
||||
using namespace p25::dfsi::defines;
|
||||
using namespace p25::data;
|
||||
|
||||
if (m_txMode != TX_MODE_P25) {
|
||||
m_network->resetP25();
|
||||
return;
|
||||
}
|
||||
|
||||
bool grantDemand = (buffer[14U] & network::NET_CTRL_GRANT_DEMAND) == network::NET_CTRL_GRANT_DEMAND;
|
||||
bool grantDenial = (buffer[14U] & network::NET_CTRL_GRANT_DENIAL) == network::NET_CTRL_GRANT_DENIAL;
|
||||
bool unitToUnit = (buffer[14U] & network::NET_CTRL_U2U) == network::NET_CTRL_U2U;
|
||||
|
||||
// process network message header
|
||||
DUID::E duid = (DUID::E)buffer[22U];
|
||||
uint8_t MFId = buffer[15U];
|
||||
|
||||
if (duid == DUID::HDU || duid == DUID::TSDU || duid == DUID::PDU)
|
||||
return;
|
||||
|
||||
// process raw P25 data bytes
|
||||
UInt8Array data;
|
||||
uint8_t frameLength = buffer[23U];
|
||||
if (duid == DUID::PDU) {
|
||||
frameLength = length;
|
||||
data = std::unique_ptr<uint8_t[]>(new uint8_t[length]);
|
||||
::memset(data.get(), 0x00U, length);
|
||||
::memcpy(data.get(), buffer, length);
|
||||
}
|
||||
else {
|
||||
if (frameLength <= 24) {
|
||||
data = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
|
||||
::memset(data.get(), 0x00U, frameLength);
|
||||
}
|
||||
else {
|
||||
data = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
|
||||
::memset(data.get(), 0x00U, frameLength);
|
||||
::memcpy(data.get(), buffer + 24U, frameLength);
|
||||
}
|
||||
}
|
||||
|
||||
// handle LDU, TDU or TSDU frame
|
||||
uint8_t lco = buffer[4U];
|
||||
|
||||
uint32_t srcId = GET_UINT24(buffer, 5U);
|
||||
uint32_t dstId = GET_UINT24(buffer, 8U);
|
||||
|
||||
uint8_t lsd1 = buffer[20U];
|
||||
uint8_t lsd2 = buffer[21U];
|
||||
|
||||
lc::LC control;
|
||||
LowSpeedData lsd;
|
||||
|
||||
control.setLCO(lco);
|
||||
control.setSrcId(srcId);
|
||||
control.setDstId(dstId);
|
||||
control.setMFId(MFId);
|
||||
|
||||
if (!control.isStandardMFId()) {
|
||||
control.setLCO(LCO::GROUP);
|
||||
}
|
||||
else {
|
||||
if (control.getLCO() == LCO::GROUP_UPDT || control.getLCO() == LCO::RFSS_STS_BCAST) {
|
||||
control.setLCO(LCO::GROUP);
|
||||
}
|
||||
}
|
||||
|
||||
lsd.setLSD1(lsd1);
|
||||
lsd.setLSD2(lsd2);
|
||||
|
||||
if (control.getLCO() == LCO::GROUP) {
|
||||
if ((duid == DUID::TDU) || (duid == DUID::TDULC)) {
|
||||
// ignore TDU/TDULC entirely when local audio detect or
|
||||
// traffic from UDP is running
|
||||
if (m_audioDetect || m_trafficFromUDP)
|
||||
return;
|
||||
|
||||
// ignore TDU's that are grant demands
|
||||
if (grantDemand) {
|
||||
m_network->resetP25();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (srcId == 0) {
|
||||
m_network->resetP25();
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure destination ID matches
|
||||
if (dstId != m_dstId) {
|
||||
m_network->resetP25();
|
||||
return;
|
||||
}
|
||||
|
||||
m_networkWatchdog.start();
|
||||
|
||||
// is this a new call stream?
|
||||
uint16_t callKID = 0U;
|
||||
if (m_network->getP25StreamId() != m_rxStreamId && ((duid != DUID::TDU) && (duid != DUID::TDULC)) && !m_callInProgress) {
|
||||
m_callInProgress = true;
|
||||
m_callAlgoId = ALGO_UNENCRYPT;
|
||||
|
||||
// if this is the beginning of a call and we have a valid HDU frame, extract the algo ID
|
||||
uint8_t frameType = buffer[180U];
|
||||
if (frameType == FrameType::HDU_VALID) {
|
||||
m_callAlgoId = buffer[181U];
|
||||
if (m_callAlgoId != ALGO_UNENCRYPT) {
|
||||
callKID = GET_UINT16(buffer, 182U);
|
||||
|
||||
if (m_callAlgoId != m_tekAlgoId && callKID != m_tekKeyId) {
|
||||
m_callAlgoId = ALGO_UNENCRYPT;
|
||||
m_callInProgress = false;
|
||||
m_ignoreCall = true;
|
||||
|
||||
LogWarning(LOG_HOST, "P25, call ignored, using different encryption parameters, callAlgoId = $%02X, callKID = $%04X, tekAlgoId = $%02X, tekKID = $%04X", m_callAlgoId, callKID, m_tekAlgoId, m_tekKeyId);
|
||||
m_network->resetP25();
|
||||
return;
|
||||
} else {
|
||||
uint8_t mi[MI_LENGTH_BYTES];
|
||||
::memset(mi, 0x00U, MI_LENGTH_BYTES);
|
||||
for (uint8_t i = 0; i < MI_LENGTH_BYTES; i++) {
|
||||
mi[i] = buffer[184U + i];
|
||||
}
|
||||
|
||||
m_p25Crypto->setMI(mi);
|
||||
m_p25Crypto->generateKeystream();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
m_rxStartTime = now;
|
||||
|
||||
LogInfoEx(LOG_HOST, "P25, call start, srcId = %u, dstId = %u, callAlgoId = $%02X, callKID = $%04X", srcId, dstId, m_callAlgoId, callKID);
|
||||
if (m_preambleLeaderTone)
|
||||
generatePreambleTone();
|
||||
}
|
||||
|
||||
// process call termination
|
||||
if ((duid == DUID::TDU) || (duid == DUID::TDULC)) {
|
||||
m_callInProgress = false;
|
||||
m_networkWatchdog.stop();
|
||||
m_ignoreCall = false;
|
||||
m_callAlgoId = ALGO_UNENCRYPT;
|
||||
|
||||
if (m_rxStartTime > 0U) {
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
uint64_t diff = now - m_rxStartTime;
|
||||
|
||||
// send USRP end of transmission
|
||||
if (m_udpUsrp) {
|
||||
sendUsrpEot();
|
||||
}
|
||||
|
||||
LogInfoEx(LOG_HOST, "P25, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
|
||||
}
|
||||
|
||||
m_rxP25LC = lc::LC();
|
||||
m_rxStartTime = 0U;
|
||||
m_rxStreamId = 0U;
|
||||
|
||||
if (!m_udpRTPContinuousSeq) {
|
||||
m_rtpInitialFrame = false;
|
||||
m_rtpSeqNo = 0U;
|
||||
}
|
||||
m_rtpTimestamp = INVALID_TS;
|
||||
m_network->resetP25();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ignoreCall && m_callAlgoId == ALGO_UNENCRYPT)
|
||||
m_ignoreCall = false;
|
||||
if (m_ignoreCall && m_callAlgoId == m_tekAlgoId)
|
||||
m_ignoreCall = false;
|
||||
|
||||
if (duid == DUID::LDU2 && !m_ignoreCall) {
|
||||
m_callAlgoId = data[88U];
|
||||
callKID = GET_UINT16(buffer, 89U);
|
||||
}
|
||||
|
||||
if (m_callAlgoId != ALGO_UNENCRYPT) {
|
||||
if (m_callAlgoId == m_tekAlgoId)
|
||||
m_ignoreCall = false;
|
||||
else
|
||||
m_ignoreCall = true;
|
||||
}
|
||||
|
||||
if (m_ignoreCall) {
|
||||
m_network->resetP25();
|
||||
return;
|
||||
}
|
||||
|
||||
// unsupported change of encryption parameters during call
|
||||
if (m_callAlgoId != ALGO_UNENCRYPT && m_callAlgoId != m_tekAlgoId && callKID != m_tekKeyId) {
|
||||
if (m_callInProgress) {
|
||||
m_callInProgress = false;
|
||||
|
||||
if (m_callAlgoId != m_tekAlgoId && callKID != m_tekKeyId) {
|
||||
LogWarning(LOG_HOST, "P25, unsupported change of encryption parameters during call, callAlgoId = $%02X, callKID = $%04X, tekAlgoId = $%02X, tekKID = $%04X", m_callAlgoId, callKID, m_tekAlgoId, m_tekKeyId);
|
||||
}
|
||||
|
||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
uint64_t diff = now - m_rxStartTime;
|
||||
|
||||
LogInfoEx(LOG_HOST, "P25, call end (T), srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
|
||||
}
|
||||
|
||||
m_ignoreCall = true;
|
||||
m_network->resetP25();
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
switch (duid)
|
||||
{
|
||||
case DUID::LDU1:
|
||||
if ((data[0U] == DFSIFrameType::LDU1_VOICE1) && (data[22U] == DFSIFrameType::LDU1_VOICE2) &&
|
||||
(data[36U] == DFSIFrameType::LDU1_VOICE3) && (data[53U] == DFSIFrameType::LDU1_VOICE4) &&
|
||||
(data[70U] == DFSIFrameType::LDU1_VOICE5) && (data[87U] == DFSIFrameType::LDU1_VOICE6) &&
|
||||
(data[104U] == DFSIFrameType::LDU1_VOICE7) && (data[121U] == DFSIFrameType::LDU1_VOICE8) &&
|
||||
(data[138U] == DFSIFrameType::LDU1_VOICE9)) {
|
||||
|
||||
dfsi::LC dfsiLC = dfsi::LC(control, lsd);
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE1);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 10U);
|
||||
count += DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE2);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 26U);
|
||||
count += DFSI_LDU1_VOICE2_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE3);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 55U);
|
||||
count += DFSI_LDU1_VOICE3_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE4);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 80U);
|
||||
count += DFSI_LDU1_VOICE4_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE5);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 105U);
|
||||
count += DFSI_LDU1_VOICE5_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE6);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 130U);
|
||||
count += DFSI_LDU1_VOICE6_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE7);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 155U);
|
||||
count += DFSI_LDU1_VOICE7_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE8);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 180U);
|
||||
count += DFSI_LDU1_VOICE8_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE9);
|
||||
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 204U);
|
||||
count += DFSI_LDU1_VOICE9_FRAME_LENGTH_BYTES;
|
||||
|
||||
LogInfoEx(LOG_NET, P25_LDU1_STR " audio, srcId = %u, dstId = %u", srcId, dstId);
|
||||
|
||||
// decode 9 IMBE codewords into PCM samples
|
||||
decodeP25AudioFrame(m_netLDU1, srcId, dstId, 1U);
|
||||
}
|
||||
break;
|
||||
case DUID::LDU2:
|
||||
if ((data[0U] == DFSIFrameType::LDU2_VOICE10) && (data[22U] == DFSIFrameType::LDU2_VOICE11) &&
|
||||
(data[36U] == DFSIFrameType::LDU2_VOICE12) && (data[53U] == DFSIFrameType::LDU2_VOICE13) &&
|
||||
(data[70U] == DFSIFrameType::LDU2_VOICE14) && (data[87U] == DFSIFrameType::LDU2_VOICE15) &&
|
||||
(data[104U] == DFSIFrameType::LDU2_VOICE16) && (data[121U] == DFSIFrameType::LDU2_VOICE17) &&
|
||||
(data[138U] == DFSIFrameType::LDU2_VOICE18)) {
|
||||
|
||||
dfsi::LC dfsiLC = dfsi::LC(control, lsd);
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE10);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 10U);
|
||||
count += DFSI_LDU2_VOICE10_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE11);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 26U);
|
||||
count += DFSI_LDU2_VOICE11_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE12);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 55U);
|
||||
count += DFSI_LDU2_VOICE12_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE13);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 80U);
|
||||
count += DFSI_LDU2_VOICE13_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE14);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 105U);
|
||||
count += DFSI_LDU2_VOICE14_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE15);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 130U);
|
||||
count += DFSI_LDU2_VOICE15_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE16);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 155U);
|
||||
count += DFSI_LDU2_VOICE16_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE17);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 180U);
|
||||
count += DFSI_LDU2_VOICE17_FRAME_LENGTH_BYTES;
|
||||
|
||||
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE18);
|
||||
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 204U);
|
||||
count += DFSI_LDU2_VOICE18_FRAME_LENGTH_BYTES;
|
||||
|
||||
LogInfoEx(LOG_NET, P25_LDU2_STR " audio, algo = $%02X, kid = $%04X", dfsiLC.control()->getAlgId(), dfsiLC.control()->getKId());
|
||||
|
||||
// decode 9 IMBE codewords into PCM samples
|
||||
decodeP25AudioFrame(m_netLDU2, srcId, dstId, 2U);
|
||||
|
||||
// copy out the MI for the next super frame
|
||||
if (dfsiLC.control()->getAlgId() == m_tekAlgoId && dfsiLC.control()->getKId() == m_tekKeyId) {
|
||||
uint8_t mi[MI_LENGTH_BYTES];
|
||||
dfsiLC.control()->getMI(mi);
|
||||
|
||||
m_p25Crypto->setMI(mi);
|
||||
m_p25Crypto->generateKeystream();
|
||||
} else {
|
||||
m_p25Crypto->clearMI();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DUID::HDU:
|
||||
case DUID::PDU:
|
||||
case DUID::TDU:
|
||||
case DUID::TDULC:
|
||||
case DUID::TSDU:
|
||||
case DUID::VSELP1:
|
||||
case DUID::VSELP2:
|
||||
default:
|
||||
// this makes GCC happy
|
||||
break;
|
||||
}
|
||||
|
||||
m_rxStreamId = m_network->getP25StreamId();
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper to decode P25 network traffic audio frames. */
|
||||
|
||||
void HostBridge::decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstId, uint8_t p25N)
|
||||
{
|
||||
assert(ldu != nullptr);
|
||||
using namespace p25;
|
||||
using namespace p25::defines;
|
||||
|
||||
if (m_debug) {
|
||||
uint8_t mi[MI_LENGTH_BYTES];
|
||||
::memset(mi, 0x00U, MI_LENGTH_BYTES);
|
||||
m_p25Crypto->getMI(mi);
|
||||
|
||||
LogInfoEx(LOG_NET, "Crypto, Enc Sync, MI = %02X %02X %02X %02X %02X %02X %02X %02X %02X",
|
||||
mi[0U], mi[1U], mi[2U], mi[3U], mi[4U], mi[5U], mi[6U], mi[7U], mi[8U]);
|
||||
}
|
||||
|
||||
// decode 9 IMBE codewords into PCM samples
|
||||
for (int n = 0; n < 9; n++) {
|
||||
uint8_t imbe[RAW_IMBE_LENGTH_BYTES];
|
||||
switch (n) {
|
||||
case 0:
|
||||
::memcpy(imbe, ldu + 10U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 1:
|
||||
::memcpy(imbe, ldu + 26U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 2:
|
||||
::memcpy(imbe, ldu + 55U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 3:
|
||||
::memcpy(imbe, ldu + 80U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 4:
|
||||
::memcpy(imbe, ldu + 105U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 5:
|
||||
::memcpy(imbe, ldu + 130U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 6:
|
||||
::memcpy(imbe, ldu + 155U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 7:
|
||||
::memcpy(imbe, ldu + 180U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 8:
|
||||
::memcpy(imbe, ldu + 204U, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
}
|
||||
|
||||
// Utils::dump(1U, "HostBridge::decodeP25AudioFrame(), IMBE", imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
|
||||
if (m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U && m_p25Crypto->getTEKLength() > 0U) {
|
||||
switch (m_tekAlgoId) {
|
||||
case P25DEF::ALGO_AES_256:
|
||||
m_p25Crypto->cryptAES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2);
|
||||
break;
|
||||
case P25DEF::ALGO_ARC4:
|
||||
m_p25Crypto->cryptARC4_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2);
|
||||
break;
|
||||
case P25DEF::ALGO_DES:
|
||||
m_p25Crypto->cryptDES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2);
|
||||
break;
|
||||
default:
|
||||
LogError(LOG_HOST, "unsupported TEK algorithm, tekAlgoId = $%02X", m_tekAlgoId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Utils::dump(1U, "HostBridge::decodeP25AudioFrame(), Decrypted IMBE", imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
|
||||
short samples[AUDIO_SAMPLES_LENGTH];
|
||||
int errs = 0;
|
||||
#if defined(_WIN32)
|
||||
if (m_useExternalVocoder) {
|
||||
ambeDecode(imbe, RAW_IMBE_LENGTH_BYTES, samples);
|
||||
}
|
||||
else {
|
||||
#endif // defined(_WIN32)
|
||||
m_decoder->decode(imbe, samples);
|
||||
#if defined(_WIN32)
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
if (m_debug)
|
||||
LogDebug(LOG_HOST, "P25, LDU (Logical Link Data Unit), Frame, VC%u.%u, srcId = %u, dstId = %u, errs = %u", p25N, n, srcId, dstId, errs);
|
||||
|
||||
// post-process: apply gain to decoded audio frames
|
||||
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain);
|
||||
|
||||
if (m_localAudio) {
|
||||
m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH);
|
||||
// Assert RTS PTT when audio is being sent to output
|
||||
assertRtsPtt();
|
||||
}
|
||||
|
||||
if (m_udpAudio) {
|
||||
int pcmIdx = 0;
|
||||
uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U];
|
||||
if (m_udpUseULaw) {
|
||||
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
|
||||
pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
|
||||
}
|
||||
|
||||
if (m_trace)
|
||||
Utils::dump(1U, "HostBridge()::decodeP25AudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH);
|
||||
|
||||
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U);
|
||||
} else {
|
||||
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
|
||||
pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
|
||||
pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
|
||||
pcmIdx += 2;
|
||||
}
|
||||
|
||||
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper to encode P25 network traffic audio frames. */
|
||||
|
||||
void HostBridge::encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId)
|
||||
{
|
||||
assert(pcm != nullptr);
|
||||
using namespace p25;
|
||||
using namespace p25::defines;
|
||||
using namespace p25::data;
|
||||
|
||||
if (m_p25N > 17)
|
||||
m_p25N = 0;
|
||||
if (m_p25N == 0)
|
||||
::memset(m_netLDU1, 0x00U, 9U * 25U);
|
||||
if (m_p25N == 9)
|
||||
::memset(m_netLDU2, 0x00U, 9U * 25U);
|
||||
|
||||
int smpIdx = 0;
|
||||
short samples[AUDIO_SAMPLES_LENGTH];
|
||||
for (uint32_t pcmIdx = 0; pcmIdx < (AUDIO_SAMPLES_LENGTH * 2U); pcmIdx += 2) {
|
||||
samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
|
||||
smpIdx++;
|
||||
}
|
||||
|
||||
// pre-process: apply gain to PCM audio frames
|
||||
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain);
|
||||
|
||||
// encode PCM samples into IMBE codewords
|
||||
uint8_t imbe[RAW_IMBE_LENGTH_BYTES];
|
||||
::memset(imbe, 0x00U, RAW_IMBE_LENGTH_BYTES);
|
||||
#if defined(_WIN32)
|
||||
if (m_useExternalVocoder) {
|
||||
ambeEncode(samples, AUDIO_SAMPLES_LENGTH, imbe);
|
||||
}
|
||||
else {
|
||||
#endif // defined(_WIN32)
|
||||
m_encoder->encode(samples, imbe);
|
||||
#if defined(_WIN32)
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
// Utils::dump(1U, "HostBridge::encodeP25AudioFrame(), Encoded IMBE", imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
|
||||
if (m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U && m_p25Crypto->getTEKLength() > 0U) {
|
||||
// generate initial MI for the HDU
|
||||
if (m_p25N == 0U && !m_p25Crypto->hasValidKeystream()) {
|
||||
if (!m_p25Crypto->hasValidMI()) {
|
||||
m_p25Crypto->generateMI();
|
||||
m_p25Crypto->generateKeystream();
|
||||
}
|
||||
}
|
||||
|
||||
// perform crypto
|
||||
switch (m_tekAlgoId) {
|
||||
case P25DEF::ALGO_AES_256:
|
||||
m_p25Crypto->cryptAES_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2);
|
||||
break;
|
||||
case P25DEF::ALGO_ARC4:
|
||||
m_p25Crypto->cryptARC4_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2);
|
||||
break;
|
||||
case P25DEF::ALGO_DES:
|
||||
m_p25Crypto->cryptDES_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2);
|
||||
break;
|
||||
default:
|
||||
LogError(LOG_HOST, "unsupported TEK algorithm, tekAlgoId = $%02X", m_tekAlgoId);
|
||||
break;
|
||||
}
|
||||
|
||||
// if we're on the last block of the LDU2 -- generate the next MI
|
||||
if (m_p25N == 17U) {
|
||||
m_p25Crypto->generateNextMI();
|
||||
|
||||
// generate new keystream
|
||||
m_p25Crypto->generateKeystream();
|
||||
}
|
||||
}
|
||||
|
||||
// fill the LDU buffers appropriately
|
||||
switch (m_p25N) {
|
||||
// LDU1
|
||||
case 0:
|
||||
::memcpy(m_netLDU1 + 10U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 1:
|
||||
::memcpy(m_netLDU1 + 26U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 2:
|
||||
::memcpy(m_netLDU1 + 55U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 3:
|
||||
::memcpy(m_netLDU1 + 80U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 4:
|
||||
::memcpy(m_netLDU1 + 105U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 5:
|
||||
::memcpy(m_netLDU1 + 130U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 6:
|
||||
::memcpy(m_netLDU1 + 155U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 7:
|
||||
::memcpy(m_netLDU1 + 180U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 8:
|
||||
::memcpy(m_netLDU1 + 204U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
|
||||
// LDU2
|
||||
case 9:
|
||||
::memcpy(m_netLDU2 + 10U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 10:
|
||||
::memcpy(m_netLDU2 + 26U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 11:
|
||||
::memcpy(m_netLDU2 + 55U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 12:
|
||||
::memcpy(m_netLDU2 + 80U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 13:
|
||||
::memcpy(m_netLDU2 + 105U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 14:
|
||||
::memcpy(m_netLDU2 + 130U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 15:
|
||||
::memcpy(m_netLDU2 + 155U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 16:
|
||||
::memcpy(m_netLDU2 + 180U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
case 17:
|
||||
::memcpy(m_netLDU2 + 204U, imbe, RAW_IMBE_LENGTH_BYTES);
|
||||
break;
|
||||
}
|
||||
|
||||
uint32_t srcId = m_srcId;
|
||||
if (m_srcIdOverride != 0 && (m_overrideSrcIdFromMDC))
|
||||
srcId = m_srcIdOverride;
|
||||
if (m_overrideSrcIdFromUDP)
|
||||
srcId = m_udpSrcId;
|
||||
if (forcedSrcId > 0 && forcedSrcId != m_srcId)
|
||||
srcId = forcedSrcId;
|
||||
uint32_t dstId = m_dstId;
|
||||
if (forcedDstId > 0 && forcedDstId != m_dstId)
|
||||
dstId = forcedDstId;
|
||||
|
||||
// never allow a source ID of 0
|
||||
if (srcId == 0U)
|
||||
srcId = m_srcId;
|
||||
|
||||
lc::LC lc = lc::LC();
|
||||
lc.setLCO(LCO::GROUP);
|
||||
lc.setGroup(true);
|
||||
lc.setPriority(4U);
|
||||
lc.setDstId(dstId);
|
||||
lc.setSrcId(srcId);
|
||||
|
||||
lc.setAlgId(m_tekAlgoId);
|
||||
lc.setKId(m_tekKeyId);
|
||||
|
||||
uint8_t mi[MI_LENGTH_BYTES];
|
||||
m_p25Crypto->getMI(mi);
|
||||
lc.setMI(mi);
|
||||
|
||||
LowSpeedData lsd = LowSpeedData();
|
||||
|
||||
uint8_t controlByte = network::NET_CTRL_SWITCH_OVER;
|
||||
|
||||
// send P25 LDU1
|
||||
if (m_p25N == 8U) {
|
||||
LogInfoEx(LOG_HOST, P25_LDU1_STR " audio, srcId = %u, dstId = %u", srcId, dstId);
|
||||
m_network->writeP25LDU1(lc, lsd, m_netLDU1, FrameType::HDU_VALID, controlByte);
|
||||
m_txStreamId = m_network->getP25StreamId();
|
||||
}
|
||||
|
||||
// send P25 LDU2
|
||||
if (m_p25N == 17U) {
|
||||
LogInfoEx(LOG_HOST, P25_LDU2_STR " audio, algo = $%02X, kid = $%04X", m_tekAlgoId, m_tekKeyId);
|
||||
m_network->writeP25LDU2(lc, lsd, m_netLDU2, controlByte);
|
||||
}
|
||||
|
||||
m_p25SeqNo++;
|
||||
m_p25N++;
|
||||
|
||||
// if N is >17 reset sequence
|
||||
if (m_p25N > 17)
|
||||
m_p25N = 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,279 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Common Library
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "common/Log.h"
|
||||
#include "network/AdaptiveJitterBuffer.h"
|
||||
|
||||
using namespace network;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define RTP_SEQ_MOD (1U << 16) // 65536
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Class Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/* Initializes a new instance of the AdaptiveJitterBuffer class. */
|
||||
|
||||
AdaptiveJitterBuffer::AdaptiveJitterBuffer(uint16_t maxBufferSize, uint32_t maxWaitTime) :
|
||||
m_buffer(),
|
||||
m_mutex(),
|
||||
m_nextExpectedSeq(0U),
|
||||
m_maxBufferSize(maxBufferSize),
|
||||
m_maxWaitTime(maxWaitTime),
|
||||
m_totalFrames(0ULL),
|
||||
m_reorderedFrames(0ULL),
|
||||
m_droppedFrames(0ULL),
|
||||
m_timedOutFrames(0ULL),
|
||||
m_initialized(false)
|
||||
{
|
||||
assert(maxBufferSize > 0U);
|
||||
assert(maxWaitTime > 0U);
|
||||
}
|
||||
|
||||
/* Finalizes a instance of the AdaptiveJitterBuffer class. */
|
||||
|
||||
AdaptiveJitterBuffer::~AdaptiveJitterBuffer()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// clean up any buffered frames
|
||||
for (auto& pair : m_buffer) {
|
||||
if (pair.second != nullptr) {
|
||||
delete pair.second;
|
||||
}
|
||||
}
|
||||
m_buffer.clear();
|
||||
}
|
||||
|
||||
/* Processes an incoming RTP frame. */
|
||||
|
||||
bool AdaptiveJitterBuffer::processFrame(uint16_t seq, const uint8_t* data, uint32_t length,
|
||||
std::vector<BufferedFrame*>& readyFrames)
|
||||
{
|
||||
if (data == nullptr || length == 0U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_totalFrames++;
|
||||
|
||||
// initialize on first frame
|
||||
if (!m_initialized) {
|
||||
m_nextExpectedSeq = seq;
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
// zero-latency fast path: in-order packet
|
||||
if (seq == m_nextExpectedSeq) {
|
||||
// create frame and add to ready list
|
||||
BufferedFrame* frame = new BufferedFrame(seq, data, length);
|
||||
readyFrames.push_back(frame);
|
||||
|
||||
// advance expected sequence
|
||||
m_nextExpectedSeq = (m_nextExpectedSeq + 1) & 0xFFFF;
|
||||
|
||||
// flush any subsequent sequential frames from buffer
|
||||
flushSequentialFrames(readyFrames);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t diff = seqDiff(seq, m_nextExpectedSeq);
|
||||
|
||||
// frame is in the past (duplicate or very late)
|
||||
if (diff < 0) {
|
||||
// check if it's severely out of order (> 1000 packets behind)
|
||||
if (diff < -1000) {
|
||||
// likely a sequence wraparound with new stream - reset
|
||||
m_nextExpectedSeq = seq;
|
||||
|
||||
// cleanup any buffered frames, delete and clear list
|
||||
for (auto& pair : m_buffer) {
|
||||
if (pair.second != nullptr) {
|
||||
delete pair.second;
|
||||
}
|
||||
}
|
||||
m_buffer.clear();
|
||||
|
||||
BufferedFrame* frame = new BufferedFrame(seq, data, length);
|
||||
readyFrames.push_back(frame);
|
||||
m_nextExpectedSeq = (m_nextExpectedSeq + 1) & 0xFFFF;
|
||||
return true;
|
||||
}
|
||||
|
||||
// drop duplicate/late frame
|
||||
m_droppedFrames++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// frame is in the future - buffer it
|
||||
m_reorderedFrames++;
|
||||
|
||||
// check buffer capacity
|
||||
if (m_buffer.size() >= m_maxBufferSize) {
|
||||
// buffer is full - drop oldest frame to make room
|
||||
auto oldestIt = m_buffer.begin();
|
||||
delete oldestIt->second;
|
||||
m_buffer.erase(oldestIt);
|
||||
m_droppedFrames++;
|
||||
}
|
||||
|
||||
// add frame to buffer
|
||||
BufferedFrame* frame = new BufferedFrame(seq, data, length);
|
||||
m_buffer[seq] = frame;
|
||||
|
||||
// check if we now have the next expected frame
|
||||
flushSequentialFrames(readyFrames);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Checks for timed-out buffered frames and forces their delivery. */
|
||||
|
||||
void AdaptiveJitterBuffer::checkTimeouts(std::vector<BufferedFrame*>& timedOutFrames,
|
||||
uint64_t currentTime)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_buffer.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get current time if not provided
|
||||
if (currentTime == 0ULL) {
|
||||
currentTime = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
// find frames that have exceeded the wait time
|
||||
std::vector<uint16_t> toRemove;
|
||||
for (auto& pair : m_buffer) {
|
||||
BufferedFrame* frame = pair.second;
|
||||
if (frame != nullptr) {
|
||||
uint64_t age = currentTime - frame->timestamp;
|
||||
|
||||
if (age >= m_maxWaitTime) {
|
||||
toRemove.push_back(pair.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove and deliver timed-out frames in sequence order
|
||||
if (!toRemove.empty()) {
|
||||
// sort by sequence number
|
||||
std::sort(toRemove.begin(), toRemove.end(), [this](uint16_t a, uint16_t b) {
|
||||
return seqDiff(a, b) < 0;
|
||||
});
|
||||
|
||||
for (uint16_t seq : toRemove) {
|
||||
auto it = m_buffer.find(seq);
|
||||
if (it != m_buffer.end() && it->second != nullptr) {
|
||||
timedOutFrames.push_back(it->second);
|
||||
m_buffer.erase(it);
|
||||
m_timedOutFrames++;
|
||||
|
||||
// update next expected sequence to skip the gap
|
||||
int32_t diff = seqDiff(seq, m_nextExpectedSeq);
|
||||
if (diff >= 0) {
|
||||
m_nextExpectedSeq = (seq + 1) & 0xFFFF;
|
||||
|
||||
// try to flush any sequential frames after this one
|
||||
flushSequentialFrames(timedOutFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Resets the jitter buffer state. */
|
||||
|
||||
void AdaptiveJitterBuffer::reset(bool clearStats)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// clean up buffered frames
|
||||
for (auto& pair : m_buffer) {
|
||||
if (pair.second != nullptr) {
|
||||
delete pair.second;
|
||||
}
|
||||
}
|
||||
m_buffer.clear();
|
||||
|
||||
m_initialized = false;
|
||||
m_nextExpectedSeq = 0U;
|
||||
|
||||
if (clearStats) {
|
||||
m_totalFrames = 0ULL;
|
||||
m_reorderedFrames = 0ULL;
|
||||
m_droppedFrames = 0ULL;
|
||||
m_timedOutFrames = 0ULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gets statistics about jitter buffer performance. */
|
||||
|
||||
void AdaptiveJitterBuffer::getStatistics(uint64_t& totalFrames, uint64_t& reorderedFrames,
|
||||
uint64_t& droppedFrames, uint64_t& timedOutFrames) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
totalFrames = m_totalFrames;
|
||||
reorderedFrames = m_reorderedFrames;
|
||||
droppedFrames = m_droppedFrames;
|
||||
timedOutFrames = m_timedOutFrames;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Class Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/* Delivers all sequential frames from the buffer. */
|
||||
|
||||
void AdaptiveJitterBuffer::flushSequentialFrames(std::vector<BufferedFrame*>& readyFrames)
|
||||
{
|
||||
while (!m_buffer.empty()) {
|
||||
auto it = m_buffer.find(m_nextExpectedSeq);
|
||||
if (it == m_buffer.end()) {
|
||||
// gap in sequence - stop flushing
|
||||
break;
|
||||
}
|
||||
|
||||
// found next sequential frame
|
||||
BufferedFrame* frame = it->second;
|
||||
readyFrames.push_back(frame);
|
||||
m_buffer.erase(it);
|
||||
|
||||
// advance to next expected sequence
|
||||
m_nextExpectedSeq = (m_nextExpectedSeq + 1) & 0xFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculates sequence number difference handling wraparound. */
|
||||
|
||||
int32_t AdaptiveJitterBuffer::seqDiff(uint16_t seq1, uint16_t seq2) const
|
||||
{
|
||||
// handle RTP sequence number wraparound (RFC 3550)
|
||||
int32_t diff = (int32_t)seq1 - (int32_t)seq2;
|
||||
|
||||
// adjust for wraparound
|
||||
if (diff > (int32_t)(RTP_SEQ_MOD / 2)) {
|
||||
diff -= (int32_t)RTP_SEQ_MOD;
|
||||
} else if (diff < -(int32_t)(RTP_SEQ_MOD / 2)) {
|
||||
diff += (int32_t)RTP_SEQ_MOD;
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
@ -0,0 +1,220 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Common Library
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @file AdaptiveJitterBuffer.h
|
||||
* @ingroup network_core
|
||||
* @file AdaptiveJitterBuffer.cpp
|
||||
* @ingroup network_core
|
||||
*/
|
||||
#if !defined(__ADAPTIVE_JITTER_BUFFER_H__)
|
||||
#define __ADAPTIVE_JITTER_BUFFER_H__
|
||||
|
||||
#include "common/Defines.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
|
||||
namespace network
|
||||
{
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define DEFAULT_JITTER_MAX_SIZE 4U
|
||||
#define DEFAULT_JITTER_MAX_WAIT 40000U
|
||||
|
||||
#define MIN_JITTER_MAX_SIZE 2U
|
||||
#define MAX_JITTER_MAX_SIZE 8U
|
||||
|
||||
#define MIN_JITTER_MAX_WAIT 10000U
|
||||
#define MAX_JITTER_MAX_WAIT 200000U
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Structure Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Represents a buffered frame in the jitter buffer.
|
||||
* @ingroup network_core
|
||||
*/
|
||||
struct BufferedFrame {
|
||||
uint16_t seq; //<! RTP sequence number
|
||||
uint8_t* data; //<! Frame data
|
||||
uint32_t length; //<! Frame length
|
||||
uint64_t timestamp; //<! Reception timestamp (microseconds)
|
||||
|
||||
/**
|
||||
* @brief Initializes a new instance of the BufferedFrame struct.
|
||||
*/
|
||||
BufferedFrame() : seq(0U), data(nullptr), length(0U), timestamp(0ULL) { /* stub */ }
|
||||
|
||||
/**
|
||||
* @brief Initializes a new instance of the BufferedFrame struct.
|
||||
* @param sequence RTP sequence number.
|
||||
* @param buffer Frame data buffer.
|
||||
* @param len Frame length.
|
||||
*/
|
||||
BufferedFrame(uint16_t sequence, const uint8_t* buffer, uint32_t len) :
|
||||
seq(sequence),
|
||||
data(nullptr),
|
||||
length(len),
|
||||
timestamp(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count())
|
||||
{
|
||||
if (len > 0U && buffer != nullptr) {
|
||||
data = new uint8_t[len];
|
||||
::memcpy(data, buffer, len);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finalizes a instance of the BufferedFrame struct.
|
||||
*/
|
||||
~BufferedFrame()
|
||||
{
|
||||
if (data != nullptr) {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Implements an adaptive jitter buffer for RTP streams.
|
||||
* @ingroup network_core
|
||||
*
|
||||
* This class provides minimal-latency jitter buffering with a zero-latency
|
||||
* fast path for in-order packets. Out-of-order packets are buffered briefly
|
||||
* to allow reordering, with adaptive timeout based on observed jitter.
|
||||
*/
|
||||
class HOST_SW_API AdaptiveJitterBuffer {
|
||||
public:
|
||||
/**
|
||||
* @brief Initializes a new instance of the AdaptiveJitterBuffer class.
|
||||
* @param maxBufferSize Maximum number of frames to buffer (default: 4).
|
||||
* @param maxWaitTime Maximum time to wait for out-of-order frames in microseconds (default: 40000 = 40ms).
|
||||
*/
|
||||
AdaptiveJitterBuffer(uint16_t maxBufferSize = 4U, uint32_t maxWaitTime = 40000U);
|
||||
|
||||
/**
|
||||
* @brief Finalizes a instance of the AdaptiveJitterBuffer class.
|
||||
*/
|
||||
~AdaptiveJitterBuffer();
|
||||
|
||||
/**
|
||||
* @brief Processes an incoming RTP frame.
|
||||
* @param seq RTP sequence number.
|
||||
* @param data Frame data.
|
||||
* @param length Frame length.
|
||||
* @param[out] readyFrames Vector of frames ready for delivery (in sequence order).
|
||||
* @returns bool True if frame was processed successfully, otherwise false.
|
||||
*
|
||||
* This method implements a zero-latency fast path for in-order packets.
|
||||
* Out-of-order packets are buffered and returned when they become sequential.
|
||||
*/
|
||||
bool processFrame(uint16_t seq, const uint8_t* data, uint32_t length,
|
||||
std::vector<BufferedFrame*>& readyFrames);
|
||||
|
||||
/**
|
||||
* @brief Checks for timed-out buffered frames and forces their delivery.
|
||||
* @param[out] timedOutFrames Vector of frames that have exceeded the wait time.
|
||||
* @param currentTime Current time in microseconds (0 = use system clock).
|
||||
*
|
||||
* This should be called periodically (e.g., every 10-20ms) to ensure
|
||||
* buffered frames are delivered even if missing packets never arrive.
|
||||
*/
|
||||
void checkTimeouts(std::vector<BufferedFrame*>& timedOutFrames,
|
||||
uint64_t currentTime = 0ULL);
|
||||
|
||||
/**
|
||||
* @brief Resets the jitter buffer state.
|
||||
* @param clearStats If true, also resets statistics (default: false).
|
||||
*
|
||||
* This should be called when a stream ends or restarts.
|
||||
*/
|
||||
void reset(bool clearStats = false);
|
||||
|
||||
/**
|
||||
* @brief Gets the current buffer occupancy.
|
||||
* @returns size_t Number of frames currently buffered.
|
||||
*/
|
||||
size_t getBufferSize() const { return m_buffer.size(); }
|
||||
|
||||
/**
|
||||
* @brief Gets the next expected sequence number.
|
||||
* @returns uint16_t Next expected sequence number.
|
||||
*/
|
||||
uint16_t getNextExpectedSeq() const { return m_nextExpectedSeq; }
|
||||
|
||||
/**
|
||||
* @brief Gets statistics about jitter buffer performance.
|
||||
* @param[out] totalFrames Total frames processed.
|
||||
* @param[out] reorderedFrames Frames that were out-of-order but successfully reordered.
|
||||
* @param[out] droppedFrames Frames dropped due to buffer overflow or severe reordering.
|
||||
* @param[out] timedOutFrames Frames delivered due to timeout (missing packets).
|
||||
*/
|
||||
void getStatistics(uint64_t& totalFrames, uint64_t& reorderedFrames,
|
||||
uint64_t& droppedFrames, uint64_t& timedOutFrames) const;
|
||||
|
||||
/**
|
||||
* @brief Sets the maximum buffer size.
|
||||
* @param maxBufferSize Maximum number of frames to buffer.
|
||||
*/
|
||||
void setMaxBufferSize(uint16_t maxBufferSize) { m_maxBufferSize = maxBufferSize; }
|
||||
|
||||
/**
|
||||
* @brief Sets the maximum wait time for out-of-order frames.
|
||||
* @param maxWaitTime Maximum wait time in microseconds.
|
||||
*/
|
||||
void setMaxWaitTime(uint32_t maxWaitTime) { m_maxWaitTime = maxWaitTime; }
|
||||
|
||||
private:
|
||||
std::map<uint16_t, BufferedFrame*> m_buffer;
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
uint16_t m_nextExpectedSeq;
|
||||
uint16_t m_maxBufferSize;
|
||||
uint32_t m_maxWaitTime;
|
||||
|
||||
uint64_t m_totalFrames;
|
||||
uint64_t m_reorderedFrames;
|
||||
uint64_t m_droppedFrames;
|
||||
uint64_t m_timedOutFrames;
|
||||
|
||||
bool m_initialized;
|
||||
|
||||
/**
|
||||
* @brief Delivers all sequential frames from the buffer.
|
||||
* @param[out] readyFrames Vector to append ready frames to.
|
||||
*
|
||||
* Internal helper that flushes all frames starting from m_nextExpectedSeq
|
||||
* until a gap is encountered.
|
||||
*/
|
||||
void flushSequentialFrames(std::vector<BufferedFrame*>& readyFrames);
|
||||
|
||||
/**
|
||||
* @brief Calculates sequence number difference handling wraparound.
|
||||
* @param seq1 First sequence number.
|
||||
* @param seq2 Second sequence number.
|
||||
* @returns int32_t Signed difference (seq1 - seq2).
|
||||
*/
|
||||
int32_t seqDiff(uint16_t seq1, uint16_t seq2) const;
|
||||
};
|
||||
} // namespace network
|
||||
|
||||
#endif // __ADAPTIVE_JITTER_BUFFER_H__
|
||||
@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Converged FNE Software
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "common/Log.h"
|
||||
#include "network/FNEPeerConnection.h"
|
||||
|
||||
using namespace network;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Class Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/* Gets or creates a jitter buffer for the specified stream. */
|
||||
|
||||
AdaptiveJitterBuffer* FNEPeerConnection::getOrCreateJitterBuffer(uint64_t streamId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_jitterMutex);
|
||||
|
||||
if (m_jitterBuffers.find(streamId) == m_jitterBuffers.end()) {
|
||||
m_jitterBuffers[streamId] = new AdaptiveJitterBuffer(m_jitterMaxSize, m_jitterMaxWait);
|
||||
}
|
||||
|
||||
return m_jitterBuffers[streamId];
|
||||
}
|
||||
|
||||
/* Cleans up jitter buffer for the specified stream. */
|
||||
|
||||
void FNEPeerConnection::cleanupJitterBuffer(uint64_t streamId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_jitterMutex);
|
||||
|
||||
auto it = m_jitterBuffers.find(streamId);
|
||||
if (it != m_jitterBuffers.end()) {
|
||||
delete it->second;
|
||||
m_jitterBuffers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/* Checks for timed-out buffered frames across all streams. */
|
||||
|
||||
void FNEPeerConnection::checkJitterTimeouts()
|
||||
{
|
||||
if (!m_jitterBufferEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_jitterMutex);
|
||||
|
||||
// check timeouts for all active jitter buffers
|
||||
for (auto& pair : m_jitterBuffers) {
|
||||
AdaptiveJitterBuffer* buffer = pair.second;
|
||||
if (buffer != nullptr) {
|
||||
std::vector<BufferedFrame*> timedOutFrames;
|
||||
buffer->checkTimeouts(timedOutFrames);
|
||||
|
||||
// note: timed-out frames are handled by the calling context
|
||||
// this method just ensures the buffers are checked periodically
|
||||
// the frames themselves are cleaned up by the caller
|
||||
for (BufferedFrame* frame : timedOutFrames) {
|
||||
delete frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue