Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ the public-API contract.

### Fixed

- **The HLS fixture reads its redirect port under the lifecycle lock.** A request worker could read
the bound port while `stop()` reset it under the lock. Host-only redirects now take the same lock,
and the port stays private to the server. Explicit `--redirect-port` targets are unchanged.

- **Source failures keep their cause and request ownership.** Initial live probes preserve HTTP,
URL and TLS failures without changing the live-start `liveSourceUnavailable` kind (HTTP and
certificate classifications stay specific). A transport failure at open (a reset or a timeout from
Expand Down
4 changes: 2 additions & 2 deletions Sources/aetherctl/HLSFixture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ final class HLSFixtureServer: @unchecked Sendable {
private var shouldStop = false
private let lock = NSLock()
private var clientFds = Set<Int32>()
private(set) var port: UInt16 = 0
private var port: UInt16 = 0

private var startTime: Date = Date()

Expand Down Expand Up @@ -596,7 +596,7 @@ final class HLSFixtureServer: @unchecked Sendable {
// which is what a tokenized IPTV portal does (portal answers, CDN edge serves). 127.0.0.1 and
// localhost are distinct origins to any HTTP client that decides header forwarding by host.
if let redirectHost = config.redirectEntryToHost, path == "/entry.m3u8" {
let targetPort = config.redirectEntryToPort ?? port
let targetPort = config.redirectEntryToPort ?? lock.withLock { port }
let target = "http://\(redirectHost):\(targetPort)/\(config.withMaster ? "master.m3u8" : "media.m3u8")"
print("[HLSFixture] 302 /entry.m3u8 -> \(target)")
send302(fd: fd, location: target)
Expand Down
2 changes: 2 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,8 @@ aetherctl play --live --header "User-Agent: Mozilla/5.0 (QtEmbedded; TestSTB)" \

The REQ log then carries the verdict per request (`auth=ok` / `auth=MISSING -> 403`), which is what makes "who lost the header, and on which hop" a measurement. Knobs: `--require-header "Name: Value"`, `--deny-status N` (401 and 403 reach the engine as different `NSURLError` codes), `--deny-segments-only` (refuse after readyToPlay rather than at the master), `--deny-user-agent S` (refuse `AppleCoreMedia` and serve everyone else, the one origin shape that tells the AVPlayer bypass and the engine's own ingest fetcher apart), `--redirect-entry` / `--redirect-host` / `--redirect-port` (portal-to-edge 302, cross-origin by host name and port while staying on loopback), `--media-origin H:P` (master's variants point at a second origin absolutely).

Without `--redirect-port`, a redirect uses the fixture's bound port, read under the same lifecycle lock that updates it on start and stop.

Measured with it before AE#363 was written, and worth knowing before suspecting the engine: `LoadOptions.httpHeaders` survive BOTH shapes on the AVPlayer bypass on macOS, the cross-origin 302 and the absolutely referenced second origin. Every request arrived with the header and the session played through.

`--codecs` / `--resolution` write `CODECS=` / `RESOLUTION=` onto both `EXT-X-STREAM-INF` lines. Without them AVFoundation reports no `videoAttributes` for the variants, so everything that reads master evidence (the #168 watchdog, the #293 probe gate) sees a master advertising no video at all and the fixture quietly stops carrying the case under test.
Expand Down