Skip to content

Limit the source-completeness check to HTTP pulls - #496

Merged
erikhortsch merged 4 commits into
mainfrom
erik/gate-source-complete-to-pull-inputs
Sep 10, 2026
Merged

Limit the source-completeness check to HTTP pulls#496
erikhortsch merged 4 commits into
mainfrom
erik/gate-source-complete-to-pull-inputs

Conversation

@erikhortsch

@erikhortsch erikhortsch commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #480, which added checkSourceComplete.

The problem

checkSourceComplete compares playback position against the duration the pipeline reports and fails the ingress when it falls short. #480 scoped this to HLS, and reasoned that everything else is safe because a source with no duration is complete anyway:

A source with no duration is complete too: live HLS, RTMP and WHIP.

Live HLS does report none. That -1 comes from hlsdemux2 answering get_duration for a playlist with no declared end. A push input has no hlsdemux2 in it — RTMP is appsrcflvdemuxdecodebin3 — and on that chain the duration query is answered from the timestamps that have arrived. It returns a positive value, the d <= 0 guard falls through, and the comparison runs on a source that has nothing to fall short of.

What the comparison measures there is the queue depth at teardown. That is roughly constant, so as a share of the duration it grows the shorter the session is:

Session length 1.24s gap as share vs 5% tolerance
24s 5.1% fails
60s 2.1% passes
5 min 0.4% passes

An observed live RTMP session was failed on source ended after 23.1115s of 24.355s — a 1.2435s gap, 5.106%, 26ms past the tolerance. Break-even is around 25s, so the exposure is short push sessions, where an ordinary publisher disconnect is reported as a truncated source. Short sessions skew towards people testing an encoder setup for the first time, which is a poor audience for a spurious ENDPOINT_ERROR.

The fix

Exclude everything that is not a pull over HTTP:

if p.InputType != livekit.IngressInput_URL_INPUT ||
    !(strings.HasPrefix(p.Url, "http://") || strings.HasPrefix(p.Url, "https://")) {
    return nil
}

The input type alone is not enough, which came out of review: a URL pull is not necessarily a pull over HTTP. NewURLSource also builds srtclientsrc for srt:// and udpsrc for udp://, and both are live. They arrive as URL_INPUT and would still reach the comparison.

The prefix test is deliberately the same expression NewURLSource selects the source element with, and that sourceLatencyReductionEnabled already uses, so the gate and the element selection cannot drift apart over what counts as HTTP. A url.Parse version would be tidier and could disagree with it.

Live sources are excluded by what they are rather than by the duration query being expected to decline for them. SRT probably does answer -1 — but "this live source reports no duration" is the assumption that put RTMP here, and it is not worth relying on a second time for a source that carries MPEG-TS timestamps over a live link.

The d <= 0 guard stays. It still carries live HLS, which reaches it as an HTTP pull.

Tolerance floor

Second, smaller thing, and separable if you would rather it were: under roughly 20s the percentage collapses to a few tens of milliseconds, which is narrower than the overshoot #480 describes on a final segment. A 3s asset is currently held to 150ms. The tolerance now takes the greater of the percentage and a one second floor.

The floor is a guess, and commented as one. Open question 2 on #480 said the same of the 5%; neither has production data behind it yet. One second means a 3s asset tolerates a 33% shortfall, so if catching truncation on very short assets matters, this wants to be smaller.

Tests

  • TestPushInputWithADurationIsComplete — the reported case, over RTMP and WHIP.
  • TestLiveURLSchemesAreComplete — the same for srt:// and udp:// under URL_INPUT.
  • TestShortSourceIsHeldToTheToleranceFloor — asserts the shortfall clears the percentage and sits under the floor before asserting the verdict, so it tests the floor rather than the percentage.

The first two assert the fixture answers a positive duration, so neither can pass by the fixture happening to answer none. All three are mutation tested: removing the input-type gate fails the first, removing the scheme clause fails the second, removing the floor fails the third.

TestSourceWithoutDurationIsComplete is renamed TestLivePullWithoutDurationIsComplete. It builds audiotestsrc is-live=true, which is the live HLS shape, and its comment claimed it covered RTMP and WHIP. It never did, and that is roughly how this got through.

The fixtures now carry a params.Params declaring the input type and url they model. They previously carried neither, which is why the gate needed them touched.

Verification

  • go build ./...: clean
  • go vet ./pkg/media/, gofmt: clean
  • go test -race ./pkg/media/: all pass except the two below

TestTruncatedHLSPullIsAnError and TestCompleteHLSPullIsComplete fail locally, both at the require.Equal(gst.MessageEOS, msgType) assertion that precedes any of this code — the pull raises a bus error instead of an EOS. They fail identically on unmodified main on this machine, so this is environmental rather than a regression here: local is Homebrew GStreamer 1.28.6, and #480 recorded its green full-suite run on 1.26.7, the version CI pins. Worth a look on its own, but please treat CI as the arbiter for these two.

Not verified against a real FLV, SRT or UDP chain in a test. The new gate tests use the seekable wav fixture, because what they pin is that the gate turns the check away before it reads any query. The 24.355s figure above is from an observed session, not from a test.

🤖 Generated with Claude Code

checkSourceComplete compares playback position against the duration the
pipeline reports and fails the ingress when it falls short. #480 scoped
that to HLS on the basis that a source with no duration is complete
anyway, and listed RTMP and WHIP alongside live HLS as sources that
report none.

RTMP reports one. Live HLS gets its -1 from hlsdemux2 answering
get_duration for a playlist with no end; a push input has no hlsdemux2
in it. On a live FLV chain the query is answered from the timestamps
that have arrived, so it returns a positive value and the guard falls
through to the comparison.

What that comparison then measures is the queue depth at teardown, not
missing source, and as a share of the whole it grows the shorter the
session is. A live RTMP session was failed on a 1.2435s gap over
24.355s: 5.106% against a 5% tolerance, 26ms past it. The same gap on a
five minute session is 0.4% and passes. So the exposure is short push
sessions, where an ordinary publisher disconnect reads as truncation.

Gate on the input type instead of on the duration query declining, and
give the tolerance a floor so a short pull is not held to a few tens of
milliseconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erikhortsch
erikhortsch marked this pull request as ready for review September 9, 2026 18:42
@erikhortsch
erikhortsch requested a review from a team as a code owner September 9, 2026 18:42

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread pkg/media/pipeline.go
Comment on lines +376 to +379
tolerance := max(
time.Duration(float64(duration)*durationTolerancePercent/100),
durationToleranceFloor,
)

@devin-ai-integration devin-ai-integration Bot Sep 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Short HLS truncations report success

For HLS assets under 20 seconds, durationToleranceFloor accepts missing content beyond the five-percent allowance. Assets at or below one second pass after any positive playback, so truncated ingresses finish successfully.

Learn more

The fixed one-second tolerance dominates the percentage for every source shorter than 20 seconds. It can classify a large relative shortfall as complete, and it effectively disables truncation detection for assets at or below one second once position is positive. Rework the short-asset tolerance so it accommodates normal final-segment timestamp variance without accepting an arbitrary one-second loss. Add a short HLS regression fixture where a real segment fetch fails and verify that the ingress remains an error.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is on purpose. 1 second is short enough that it shouldn't really be noticed.

@milos-lk

milos-lk commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

it would be interesting to check srt input..

@milos-lk

milos-lk commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

I think SRT source should return -1 to the duration query so we should be ok - but querying a live source doesn't make sense - maybe we should do:

if p.InputType != livekit.IngressInput_URL_INPUT ||
      !(strings.HasPrefix(p.Url, "http://") || strings.HasPrefix(p.Url, "https://")) {
      return nil
}

A URL pull is not necessarily a pull over HTTP. NewURLSource also builds
srtclientsrc for srt:// and udpsrc for udp://, and both are live, so the
input type alone leaves two live sources reaching the check.

Use the prefix test NewURLSource selects the source element with, so the
gate and the element selection cannot disagree about what HTTP is.

Assuming the live schemes answer no duration would be the assumption
that put RTMP here to begin with, so they are excluded by what they are
rather than by what a query is expected to return.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erikhortsch erikhortsch changed the title Limit the source-completeness check to pull inputs Limit the source-completeness check to HTTP pulls Sep 10, 2026
erikhortsch and others added 2 commits September 10, 2026 09:08
…lete-to-pull-inputs

# Conflicts:
#	pkg/media/pipeline_test.go
staticcheck flagged the negated disjunction. De Morgan would split the
prefix test into two negated halves, which stops it being the same
expression NewURLSource picks the source element with, so name the
positive condition and negate that instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erikhortsch
erikhortsch merged commit 136c4db into main Sep 10, 2026
7 checks passed
@erikhortsch
erikhortsch deleted the erik/gate-source-complete-to-pull-inputs branch September 10, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants