PrismCore / API reference / Loopback server

Actor

LoopbackHTTPServer

actor LoopbackHTTPServer

Declared 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:

  • GET and HEAD; anything else is 405.
  • Whole-payload responses with Content-Length (Range is 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.maxRequestsPerConnection and Limits.idleTimeout so 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 port: UInt16 { get }

The bound port, available after start().

var host: String { get }

The host in the published base URL, available after start().

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 -> URL

Bind 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 FailedToStart

No documentation comment in the source.

Structure

LoopbackHTTPServer.Limits

struct Limits

Resource 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: Int

Requests 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: Duration

How long a connection may sit between requests before it's dropped.

var slowServeThreshold: Duration

How 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: Int

Cap on the request line (method + target + version).

var maxHeaderBytes: Int

Cap on the whole request head.

Structure

LoopbackHTTPServer.NoLocalNetworkInterface

struct NoLocalNetworkInterface

No 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 Reachability

Who can reach this server. Opt in with care; the default is the safe one.

Cases

case loopbackOnly

Bound to 127.0.0.1. Nothing off-device can connect, and no token is issued or required.

case localNetworkUnencryptedForAirPlay

Opt-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 ServiceAddress

Where the server is currently reachable.

Cases

case loopback

No documentation comment in the source.

case localNetwork(String)

Bound to this LAN address and still holding it.

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