What happened?
The reader makes one HTTP request per 32 MB. On a 7 GB file that is about 200
requests. On a 20 GB file about 600. My origin counts requests and answers 429
when you make too many, so a long episode eventually trips it.
Every other player I have used against the same origin opens one connection and
reads the whole file through it. One request. I have used this kind of host for
years and never seen a 429 until now.
Once the 429s start, playback keeps going for 30 to 50 seconds off the segment
cache, then goes black and stays black. If I leave the player and press play on
the same stream it starts immediately. The source was never dead, it was being
throttled.
Two things I would like.
-
Let the host set how big each request is. persistentRangeBytes is a static
let, and windowHighWater is an AVIOReader init parameter that nothing passes, so
there is nothing to reach from outside the engine. I patched persistentRangeBytes
to 256 MB in my own copy, which is not something I can keep doing.
-
Do not treat a 429 as a dead source. AVIOReader.isRateLimitStatus already
knows the difference, but it is only used inside AVIOReader. It never reaches
handleVODReadErrorExit in HLSVideoEngine+LiveReopen.swift, which spends
readErrorReviveGate (maxAttempts: 2). Two 429s and the session ends with "not
readable in this session". Each of those attempts reopens from offset 0, gets
refused there too, and takes about 35 seconds to fail. Then my host hands off to
its other engine, which asks the same origin and gets the same 429.
I am not asking for the transport to change. I read #220 and #310 and I am not
going to argue with them. I just want a way to make fewer requests.
How I know it is the range size and not the window: consecutive pump generations
advance by exactly 33554432 bytes, every time. That is the range boundary, which
is also the first case your own comment on that constant describes.
Steps to reproduce
No clean repro, sorry. It needs an origin that counts requests, and 10 to 30
minutes of continuous 4K playback before the count gets high enough.
- Play a 7 to 20 GB VOD file over HTTPS from an origin that rate limits by
request count.
- Let it run. The reader issues one ranged GET per 32 MB.
- After a few hundred requests the origin answers 429 to everything, including
HEAD and range probes.
- The pump wedges, the reopen path spends both revive attempts on the refusals,
and the session is declared not readable.
Happens on every long session here. I can run builds with extra logging on real
hardware and get you traces the same day.
AetherEngine version or commit SHA
6.25.0
Host app
Custom / my own integration
Platform
tvOS
OS version
26.6
Device / chip
Apple TV 4K 3rd gen
Playback path
Native AVPlayer
Source media (for playback bugs)
4K HEVC, Dolby Vision profile 7, DTS-HD MA, MKV remuxes. 7 GB and 20 GB. Served
over HTTPS by a commercial proxy that counts requests, 302s to a signed CDN
target, and has no suffix-range support.
Error codes / log lines
One connection per 32 MB:
[AVIOReader] pump conn start gen=91 offset=5577199155 len=32MB
[AVIOReader] pump conn start gen=92 offset=5610753587 len=32MB
[AVIOReader] pump conn start gen=93 offset=5644308019 len=32MB
[AVIOReader] pump conn start gen=94 offset=5677862451 len=32MB
Once rate limited, a reopen attempt fails like this:
[AVIOReader] pump conn start gen=9 offset=0 len=32MB
[AVIOReader] pump gen=9 rejected response status=429 at offset 0
[AVIOReader] pump reconnect exhausted at offset 0 status=429 (7 consecutive rate-limited)
[AVIOReader] slow read: 32533ms at offset=0 detour=0(0ms,0fetch) stallWaits=6(549ms,6signaled) tailWaits=0(0ms) reconnects=6 backoff=31978ms lockWait=0ms connect=3ms staleGenDropped=0b iters=13 unaccounted=0ms gen=3->9
[HLSVideoEngine] restart at idx=130: #169 suspect-dead demuxer; reopen failed (Demuxer: open failed (Operation not permitted (-1))), reusing the demuxer
[HLSVideoEngine] producer restarted at idx=130 (seek=520.85s [absolute source-PTS], restart took 33568ms; stopWait=0ms reopen=33567ms seek=0ms build=0ms)
[AVIOReader] Detour rate-limit gave up at offset 3976056735 (12 consecutive rate-limited)
[HLSSegmentProducer] demuxer.readPacket threw: Demuxer: read failed (Operation not permitted (-1))
[HLSSegmentProducer] pump finished: reason=readError(-1) packetsRead=0 packetsWritten=0 lastError=-1 elapsed=184ms cacheCount=82
[HLSVideoEngine] #169 VOD readError revive cap reached (3 failures, cap 2); giving up (source not readable in this session)
The handoff to my other engine, same origin, same answer:
[PlaybackCoordinator] Aether→MPV handoff: Source read failed (code -1) @ 556.11s
[MPV][ffmpeg] warn: https: HTTP error 429 Too Many Requests
[MPV][stream] error: Failed to open https://<origin>/.../<file>.mkv
[MPV] End file error: loading failed
Starting over does not help, the probes are refused too:
[AVIOReader] pump tail prefetch skipped: this origin declined suffix ranges earlier this session (status=429 (no suffix range support))
[AVIOReader] Range probe (bytes=0-) didn't yield a size
[AVIOReader] HEAD failed (HTTP 429)
[AVIOReader] Range probe (bytes=0-1) didn't yield a size
[AVIOReader] no probe resolved a size, streaming mode (forward-only)
[AVIOReader] size probe empty; reusing cached 20351996087 bytes to keep seekability (#112)
Anything else
Checked against main before filing. winHighWaterDefault is still 16 MB for VOD
against 64 MB for live, readErrorReviveGate is still maxAttempts: 2, and
HLSVideoEngine+LiveReopen.swift has no reference to rate limiting anywhere. No
open PRs touching any of it.
On #307, since it is still open waiting for a retest. Different origin from the
original report, so take it for what it is worth, but the 6.11.0 ladder does what
you said it would:
[AVIOReader] pump gen=6 rejected response status=429 at offset 0
[AVIOReader] pump conn ended at offset 0 status=429, reconnecting (streak=4 retryAfter=0.0s)
[AVIOReader] pump reconnect exhausted at offset 0 status=429 (7 consecutive rate-limited)
The refusal is named, the streak charges, backoff is real (31978ms across six
reconnects), and it ends the read instead of spinning. No sign of the
925-generation spiral. My origin sends no Retry-After, hence retryAfter=0.0s, and
it exhausts at 7 rather than 12 because these are rate-limit statuses on the
shorter streak.
What happened?
The reader makes one HTTP request per 32 MB. On a 7 GB file that is about 200
requests. On a 20 GB file about 600. My origin counts requests and answers 429
when you make too many, so a long episode eventually trips it.
Every other player I have used against the same origin opens one connection and
reads the whole file through it. One request. I have used this kind of host for
years and never seen a 429 until now.
Once the 429s start, playback keeps going for 30 to 50 seconds off the segment
cache, then goes black and stays black. If I leave the player and press play on
the same stream it starts immediately. The source was never dead, it was being
throttled.
Two things I would like.
Let the host set how big each request is. persistentRangeBytes is a static
let, and windowHighWater is an AVIOReader init parameter that nothing passes, so
there is nothing to reach from outside the engine. I patched persistentRangeBytes
to 256 MB in my own copy, which is not something I can keep doing.
Do not treat a 429 as a dead source. AVIOReader.isRateLimitStatus already
knows the difference, but it is only used inside AVIOReader. It never reaches
handleVODReadErrorExit in HLSVideoEngine+LiveReopen.swift, which spends
readErrorReviveGate (maxAttempts: 2). Two 429s and the session ends with "not
readable in this session". Each of those attempts reopens from offset 0, gets
refused there too, and takes about 35 seconds to fail. Then my host hands off to
its other engine, which asks the same origin and gets the same 429.
I am not asking for the transport to change. I read #220 and #310 and I am not
going to argue with them. I just want a way to make fewer requests.
How I know it is the range size and not the window: consecutive pump generations
advance by exactly 33554432 bytes, every time. That is the range boundary, which
is also the first case your own comment on that constant describes.
Steps to reproduce
No clean repro, sorry. It needs an origin that counts requests, and 10 to 30
minutes of continuous 4K playback before the count gets high enough.
request count.
HEAD and range probes.
and the session is declared not readable.
Happens on every long session here. I can run builds with extra logging on real
hardware and get you traces the same day.
AetherEngine version or commit SHA
6.25.0
Host app
Custom / my own integration
Platform
tvOS
OS version
26.6
Device / chip
Apple TV 4K 3rd gen
Playback path
Native AVPlayer
Source media (for playback bugs)
4K HEVC, Dolby Vision profile 7, DTS-HD MA, MKV remuxes. 7 GB and 20 GB. Served
over HTTPS by a commercial proxy that counts requests, 302s to a signed CDN
target, and has no suffix-range support.
Error codes / log lines
Anything else
Checked against main before filing. winHighWaterDefault is still 16 MB for VOD
against 64 MB for live, readErrorReviveGate is still maxAttempts: 2, and
HLSVideoEngine+LiveReopen.swift has no reference to rate limiting anywhere. No
open PRs touching any of it.
On #307, since it is still open waiting for a retest. Different origin from the
original report, so take it for what it is worth, but the 6.11.0 ladder does what
you said it would:
[AVIOReader] pump gen=6 rejected response status=429 at offset 0
[AVIOReader] pump conn ended at offset 0 status=429, reconnecting (streak=4 retryAfter=0.0s)
[AVIOReader] pump reconnect exhausted at offset 0 status=429 (7 consecutive rate-limited)
The refusal is named, the streak charges, backoff is real (31978ms across six
reconnects), and it ends the read instead of spinning. No sign of the
925-generation spiral. My origin sends no Retry-After, hence retryAfter=0.0s, and
it exhausts at 7 rather than 12 because these are rate-limit statuses on the
shorter streak.