Skip to content

Fix EUD/plugin video hairpin-NAT pull + add optional playback HLS URL override - #1645

Open
dfndr13 wants to merge 2 commits into
dfpc-coe:mainfrom
dfndr13:pr1-eud-video-hairpin
Open

dfndr13 wants to merge 2 commits into
dfpc-coe:mainfrom
dfndr13:pr1-eud-video-hairpin

Conversation

@dfndr13

@dfndr13 dfndr13 commented Aug 6, 2026

Copy link
Copy Markdown

Problem

Two related bugs prevent EUD/ATAK-plugin video from playing in some deployments:

1. Hairpin-NAT on the RTSP proxy source. When an off-host EUD or ATAK plugin publishes video, lease.proxy is set to the media service's own public hostname (media::url) — correct, since the EUD needs to reach it publicly. But when the media service itself pulls that same URL as a relay source (source in the MediaMTX path config), it's asking to reach its own public hostname from inside its own network. On networks/NAT setups that don't support hairpinning, that pull just hangs — no error at the MediaMTX layer, no timeout — and surfaces client-side as an HLS manifestLoadTimeOut.

2. No way to route HLS playback through a standard port. The media service's dedicated port (9997) is blocked outright on some networks (guest wifi, restrictive agency networks), with no way to work around it short of changing the media service's own listen port.

Fix

  • A new ingestSource() helper rewrites an RTSP/RTSPS proxy source to an internal alias when its hostname matches media::url's own hostname — but only when an operator has opted in via a new media::ingest_internal_host setting. Left unset (the default), this is a complete no-op.
  • A new optional media::playback_url setting lets operators serve the browser-facing HLS URL through a different host/port (e.g. a standard-port reverse proxy) without touching RTSP/RTMP/SRT/WebRTC URLs or affecting deployments that don't set it.

Both settings default to empty and are strictly additive — behavior for any deployment that doesn't configure them is unchanged.

On the ingestSource() design

The obvious version of this fix is to hardcode the internal alias to rewrite to. We deliberately didn't do that: which internal hostname (if any) is reachable is entirely deployment-specific — it depends on how an operator's media/ingest containers are networked relative to each other, which varies a lot between a plain single-container media-infra deployment, ECS, and setups with a separate ingest tier in front of it. A hardcoded value would be meaningless (or actively wrong) for anyone whose topology differs from ours, so we made it an opt-in setting that's a no-op by default instead. Open to other approaches if maintainers see a better way to express "this deployment needs a hairpin workaround" — this is the smallest one we found that can't silently misbehave for anyone who doesn't ask for it.

Validation

Both issues (the hairpin hang and the port-9997 block) were reproduced and confirmed fixed in an independent deployment running current media-infra main, separately from the environment these patches were originally written against.

Testing

  • tsc --noEmit clean
  • New unit tests for ingestSource() (opt-in rewrite, no-op default, non-matching hosts, non-RTSP sources, malformed URLs)
  • Full existing video-lease/profile-video/connection-video-lease/schema suites pass unchanged

… override

Two related video-playback fixes:

- lease.proxy is set to the media service's own public hostname when an
  off-host EUD/ATAK-plugin is publishing directly to it - correct for the
  EUD, but when this same media service pulls that source to relay it,
  resolving its own public hostname back to itself is a hairpin-NAT path
  many networks/NAT setups can't route, and the pull just hangs (no error,
  surfaces client-side as an HLS manifest load timeout). Adds an
  ingestSource() helper that rewrites the RTSP proxy source to an internal
  alias when its hostname matches media::url's own hostname - but only
  when an operator opts in via the new media::ingest_internal_host
  setting, since which alias (if any) is valid is deployment-specific.
  Unset (the default), this is a complete no-op.

- Some networks block the media server's dedicated port (9997) outright.
  Adds an optional media::playback_url setting so the browser-facing HLS
  URL can be served through a standard-port reverse proxy instead, without
  touching RTSP/RTMP/SRT/WebRTC URLs or changing behavior for deployments
  that don't set it.

Both settings default to empty/unset and are strictly additive - byte-
identical behavior for every deployment that doesn't configure them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ingalls Nick (ingalls) self-assigned this Aug 9, 2026
@ingalls Nick (ingalls) added the video Video streaming and management label Aug 9, 2026
@ingalls

Copy link
Copy Markdown
Member

Thanks for the PR,

One catch to a further review/merge, The Env vars don't appear to actually work as intended from my initial test as _ maps to ::. You'll need to either namespace these config changes or change the actual name of the config value to avoid the namespace replacement from env var => internal value

…internal_host

CLOUDTAK_Config_* env vars translate to Setting keys by replacing every
underscore with `::` (api/common/config.ts), so CLOUDTAK_Config_media_playback_url
resolved to media::playback::url, never matching the media::playback_url key -
the setting silently never applied. Same bug for ingest_internal_host. Renamed
both keys to use `::` per word, matching the translation rule and the
existing login::background::color-style convention elsewhere in FullConfig.
No env var or docker-compose.yml changes needed - only the internal key
spelling had to match what the translation already produces.

Flagged by @ingalls in review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dfndr13

dfndr13 commented Aug 10, 2026

Copy link
Copy Markdown
Author

Fixed the config key names — media::playback::url and media::ingest::internal::host — to match the existing ::-per-word convention in the codebase. The underscores in the original names were getting converted to :: by the config translation, splitting them into extra namespace levels the settings never actually applied to. No env var or docker-compose changes needed. Verified with full build/typecheck/test pass.

dfndr13 pushed a commit to dfndr13/CloudTAK that referenced this pull request Aug 17, 2026
api/common/config.ts's CLOUDTAK_Config_* env var translation replaces
every underscore with ::, so CLOUDTAK_Config_media_internal_url
resolved to media::internal::url while the setting was actually keyed
media::internal_url (and same for media_public_url) - the env vars
could never reach their setting, silently falling back to the legacy
media::url default. Renamed both keys to media::internal::url and
media::public::url to match the translation rule, same fix already
applied to media::playback_url in PR dfpc-coe#1645.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

video Video streaming and management

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants