PrismCore / API reference / Loopback server
Actor
LoopbackHTTPServer
actor LoopbackHTTPServerDeclared in Sources/PrismCore/Loopback/LoopbackHTTPServer.swift · PrismCore 3.5.4
A minimal HTTP/1.1 server bound to 127.0.0.1 on an ephemeral port — the loopback half of the remux pipeline. It serves exactly one namespace (the remux session's segment dir by default): the growing playlist, the init segment, and the media segments.
Still deliberately small, but no longer naive about what AVPlayer does to a server at scale:
GETandHEAD; anything else is405.- Whole-payload responses with
Content-Length(Rangeis ignored — HLS clients fetch whole segments). - Keep-alive per HTTP/1.1 default: after a response the connection loops back to reading the next request, bounded by
Limits.maxRequestsPerConnectionandLimits.idleTimeoutso abandoned sockets can't pile up.Connection: close(and HTTP/1.0 without an explicit keep-alive) is honored. - A provider seam (
SegmentProvider) instead of direct file reads, so a payload that is still being produced can say so.
The reason the seam exists is an expensive AVPlayer lesson: AVPlayer's media watchdog logs -12889 "No response for media file" after roughly 3.5 s without response HEADERS — holding the socket open silently buys nothing — and a few of those in a row fail the item outright. So when a provider hasn't answered within Limits.slowServeThreshold (2 s, comfortably inside the window), the server commits to an early 200 with Transfer-Encoding: chunked and sends the payload as a single chunk when it lands. If the serve ultimately misses, the connection is aborted mid-body: a truncated transfer is something AVPlayer retries, whereas a well-framed empty 200 is something it caches as the truth about that segment.
Fast serves keep the byte-identical Content-Length shape — the chunked path is the exception, not the norm.
Reachability
The default (Reachability.loopbackOnly) is the whole story above: bound to 127.0.0.1, unreachable from anywhere else, no token, byte-identical to what shipped before this mode existed.
.localNetworkUnencryptedForAirPlay is the opt-in exception, and it exists for exactly one reason: when the host AirPlays to an external receiver, the receiver fetches the playlist and every segment itself, and 127.0.0.1 resolves to the receiver. See that case's documentation for what it costs.
Initializers
convenience init(root: URL, limits: Limits = Limits(), reachability: Reachability = .loopbackOnly)Serve a directory from disk — the v0 shape, unchanged in behavior.
convenience init(provider: SegmentProvider, limits: Limits = Limits(), reachability: Reachability = .loopbackOnly)No documentation comment in the source.
Instance properties
var serviceAddress: ServiceAddress { get }Where this server is reachable, and whether it still is.
var accessToken: SessionAccessToken? { get }The gate on every request in LAN mode; nil on loopback, where there is nobody to gate against.
Instance methods
@discardableResult func start() async throws -> URLBind and listen. Returns the base URL — http://127.0.0.1:<port>/ by default, and http://<lan-ip>:<port>/<token>/ in LAN mode, where every relative reference inside the served playlists inherits the token prefix for free.
func revalidateServiceAddress()Re-check whether the machine still holds the address this server published, and flip serviceAddress accordingly. Self-healing on purpose: a Wi-Fi blip that comes back with the same address goes back to serving instead of staying broken.
func stop()Stop listening and tear down every connection, including ones with a response in flight.
Cancelling the per-connection task alone would not be enough: a task parked in a receive continuation only wakes when Network tells it something, so the connection is cancelled too — that delivers an error to the pending callback and the task unwinds.
Structure
LoopbackHTTPServer.FailedToStart
struct FailedToStartNo documentation comment in the source.
Structure
LoopbackHTTPServer.Limits
struct LimitsResource bounds. Defaults are tuned for one AVPlayer talking to one remux session; tests dial them down.
Initializers
init(maxRequestsPerConnection: Int = 100, idleTimeout: Duration = .seconds(10), slowServeThreshold: Duration = .seconds(2), maxRequestLineBytes: Int = 8 * 1024, maxHeaderBytes: Int = 64 * 1024)No documentation comment in the source.
Instance properties
var maxRequestsPerConnection: IntRequests served on one connection before the server says Connection: close. Bounds per-connection state growth without costing AVPlayer anything — it just opens a new socket.
var idleTimeout: DurationHow long a connection may sit between requests before it's dropped.
var slowServeThreshold: DurationHow long a provider may take before the server switches to early headers + chunked. Must stay well under AVPlayer's ~3.5 s watchdog.
var maxRequestLineBytes: IntCap on the request line (method + target + version).
Structure
LoopbackHTTPServer.NoLocalNetworkInterface
struct NoLocalNetworkInterfaceNo interface to serve from: Wi-Fi off, cable out, or nothing left after the tunnels and peer-to-peer radios are excluded. Thrown from start() in LAN mode, because the alternative is publishing a URL nobody can reach and letting the host find out from a playback timeout.
Enumeration
LoopbackHTTPServer.Reachability
enum ReachabilityWho can reach this server. Opt in with care; the default is the safe one.
Cases
case loopbackOnlyBound to 127.0.0.1. Nothing off-device can connect, and no token is issued or required.
case localNetworkUnencryptedForAirPlayOpt-in, and named at length on purpose. Binds the preferred LAN IPv4 interface so an AirPlay receiver can fetch the playlist, and gates every request on a per-session token (see SessionAccessToken).
The residual risk, stated plainly: this is cleartext HTTP on the local network. Anyone on that LAN who can observe the traffic sees the token, the playlist and the media bytes — and anyone who holds the token can fetch the session's segments for as long as it runs. The token makes the server unguessable, not private. Enable it for the duration of an AirPlay route on a network the user trusts, and stop the session when the route ends.
Enumeration
LoopbackHTTPServer.ServiceAddress
enum ServiceAddressWhere the server is currently reachable.
Cases
case addressLost(String)Bound to this LAN address and the machine no longer has it — a Wi-Fi-to-Ethernet swap, a DHCP change, the radio going down.
The server does not re-bind: the URL AVPlayer and the receiver were handed is baked into the item and every segment reference, so a new address would need a new session anyway. It refuses every request with 503 instead, so the failure is an honest error rather than a URL that hangs, and the host can see it on serviceAddress.
Generated from the public symbols of PrismCore 3.5.4. All types