Files
aeris/src/app/api/atc/stream/route.ts
kew eb1103f63f feat: 3D aircraft model overhaul and multi-source flight data proxy (Resolves #15) (#16)
* Refactor aircraft photo and hero banner components to reset loading state on photo change

- Updated Lightbox component to reset image loading state when navigating between photos.
- Modified HeroBanner component to reset loading state when the photo changes.

Clean up control panel search logic

- Removed unnecessary hasResults variable in SearchContent component.

Implement flight API client with fallback mechanism

- Added flight-api-client to handle fetching flight data from multiple sources (airplanes.live, adsb.lol, OpenSky).
- Introduced flight-api-parsing module to convert raw API responses into standardized FlightState objects.
- Created flight-api-types for shared types between API responses.

Refactor useFlights hook to utilize new flight API client

- Updated useFlights hook to fetch flights using the new flight API client.
- Removed credit management logic as it is no longer applicable with the new API structure.

Fix useFlightMonitors to fetch flight data by hex address

- Changed useFlightMonitors to use fetchFlightByHex instead of fetchFlightByIcao24.

Update geo utility function for better readability

- Refactored splitAtAntimeridian function to improve variable naming and clarity.

Enhance OpenSky types with additional fields

- Added typeCode and registration fields to FlightState type for better integration with readsb data.

* fix: correct 6 files that diverged during rebase (iata code, globe mode ref, terrain attribution, cache eviction, opensky parsing)

* fix: improve keyboard shortcuts help focus trapping

feat: add showAirspace option to MapAttribution component

fix: clear hideTimer on ScrollArea cleanup

refactor: change pendingFpvRef to MutableRefObject in useFlightMonitors

fix: handle sessionStorage availability in useFlightTrack

refactor: increase POLL_INTERVAL_MS in useFlights for better performance

fix: optimize keyboard shortcuts dialog check

refactor: optimize useMergedTrails by caching selected flight position

feat: extend Settings type with airspace options

refactor: improve airline logo normalization functions

refactor: enhance flight API client with serialized rate limiting

refactor: optimize registration country lookup with pre-built maps

refactor: enhance logo cache management with size limits

feat: update map attribution to include airspace option

fix: validate rawState in parseStateRow function

refactor: improve utility functions with clamp implementation

* feat: add ATC lookup functionality and GPU memory monitoring

- Implemented ATC lookup functions in `atc-lookup.ts` for converting IATA to ICAO codes, finding nearby ATC feeds, and looking up ATC feeds by code.
- Introduced `atc-types.ts` to define types and priorities for ATC feeds.
- Added GPU memory monitoring in `gpu-memory-monitor.ts` to track WebGL resource allocations and provide memory reports.
- Enhanced trail stitching logic in `trail-stitching.ts` by adding a function to clear the splined track cache and optimizing altitude checks.

* feat: enhance flight data handling and improve API resilience

- Implemented a maximum empty response streak guard in useFlights to prevent data loss during transient API failures.
- Added immediate fetch on network reconnect in useFlights to ensure timely data retrieval.
- Updated useMergedTrails to include timestamps for trail points.
- Removed smoothAnimations setting from useSettings as it is no longer needed.
- Enhanced useTrailHistory to preserve last-known trails during empty flight responses and added dynamic jump detection for tab resume scenarios.
- Improved flight API client with a circuit breaker mechanism to handle provider failures and prevent excessive retries.
- Updated flight API parsing to reject non-JSON responses from OpenSky and other providers.
- Enhanced trail smoothing and stitching logic to ensure better continuity at junctions between historical and live data.

* feat: migrate aircraft models to Cloudinary CDN and update mapping logic

* fix: adjust UI component styles and improve trail smoothing parameters

* fix: adjust base aircraft size for improved rendering

* feat: update changelog with recent enhancements and modify data source attribution

* fix: update model optimization details and remove Draco compression dependency

* feat: update changelog with recent code review fixes and fallback provider adjustments
2026-03-23 01:25:11 +05:30

161 lines
4.8 KiB
TypeScript

import { type NextRequest } from "next/server";
import { VALID_MOUNT_POINTS } from "@/lib/atc-feeds";
/**
* GET /api/atc/stream?mount={mountPoint}
*
* Fallback audio stream proxy for LiveATC Icecast streams.
* Only used when direct browser <audio> playback is blocked.
*
* Security:
* - Mount point validated against static allowlist (SSRF prevention)
* - Connection timeout: 30 seconds
* - Max stream duration: 4 hours
* - Simple per-request rate limiting via headers
*/
/** Maximum stream duration in milliseconds (4 hours). */
const MAX_STREAM_DURATION_MS = 4 * 60 * 60 * 1000;
/** Connection timeout for upstream fetch (30 seconds). */
const CONNECT_TIMEOUT_MS = 30_000;
/**
* Sanitize and validate mount point parameter.
* Only alphanumeric characters, underscores, and hyphens are allowed.
*/
function isValidMountFormat(mount: string): boolean {
return /^[a-z0-9_-]{2,64}$/i.test(mount);
}
export async function GET(request: NextRequest) {
const mount = request.nextUrl.searchParams.get("mount")?.trim();
if (!mount) {
return new Response(
JSON.stringify({ error: "Missing required 'mount' parameter." }),
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}
// Validate mount point format
if (!isValidMountFormat(mount)) {
return new Response(
JSON.stringify({ error: "Invalid mount point format." }),
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}
// SSRF prevention: only allow mount points from our static database
if (!VALID_MOUNT_POINTS.has(mount)) {
return new Response(
JSON.stringify({
error: "Unknown mount point. Only verified feeds are allowed.",
}),
{ status: 403, headers: { "Content-Type": "application/json" } },
);
}
// Construct the upstream URL from the validated mount point
// Using the direct Icecast server URL (d.liveatc.net)
const upstreamUrl = `https://d.liveatc.net/${mount}`;
try {
const controller = new AbortController();
const connectTimer = setTimeout(
() => controller.abort(),
CONNECT_TIMEOUT_MS,
);
const upstream = await fetch(upstreamUrl, {
signal: controller.signal,
headers: {
"User-Agent": "Mozilla/5.0 (compatible; Aeris/1.0)",
Referer: "https://www.liveatc.net/",
Accept: "audio/mpeg, audio/*, */*",
},
});
clearTimeout(connectTimer);
if (!upstream.ok) {
return new Response(
JSON.stringify({
error: "Upstream stream unavailable.",
status: upstream.status,
}),
{ status: 502, headers: { "Content-Type": "application/json" } },
);
}
if (!upstream.body) {
return new Response(
JSON.stringify({ error: "No stream body from upstream." }),
{ status: 502, headers: { "Content-Type": "application/json" } },
);
}
// Set up max duration cutoff
const durationController = new AbortController();
const durationTimer = setTimeout(
() => durationController.abort(),
MAX_STREAM_DURATION_MS,
);
// Pipe the upstream stream through, respecting both abort signals
const reader = upstream.body.getReader();
const stream = new ReadableStream({
async pull(ctrl) {
try {
if (durationController.signal.aborted) {
reader.cancel().catch(() => {});
clearTimeout(durationTimer);
ctrl.close();
return;
}
const { value, done } = await reader.read();
if (done) {
ctrl.close();
} else {
ctrl.enqueue(value);
}
} catch {
reader.cancel().catch(() => {});
ctrl.close();
}
},
cancel() {
clearTimeout(durationTimer);
reader.cancel().catch(() => {});
},
});
// Detect client disconnect via request abort signal
request.signal.addEventListener("abort", () => {
clearTimeout(durationTimer);
reader.cancel().catch(() => {});
});
return new Response(stream, {
status: 200,
headers: {
"Content-Type": upstream.headers.get("Content-Type") ?? "audio/mpeg",
"Cache-Control": "no-cache, no-store, must-revalidate",
"X-Accel-Buffering": "no", // Disable Nginx buffering if behind reverse proxy
"Access-Control-Allow-Origin": "*",
},
});
} catch (err) {
const isAbort = err instanceof Error && err.name === "AbortError";
if (isAbort) {
return new Response(
JSON.stringify({ error: "Connection to upstream timed out." }),
{ status: 504, headers: { "Content-Type": "application/json" } },
);
}
return new Response(
JSON.stringify({ error: "Failed to connect to upstream stream." }),
{ status: 502, headers: { "Content-Type": "application/json" } },
);
}
}