Fix streaming playback and HLS lifetime in gr.Audio and gr.Video - #13808
Fix streaming playback and HLS lifetime in gr.Audio and gr.Video#13808hysts wants to merge 19 commits into
gr.Audio and gr.Video#13808Conversation
`stream_active` keeps load_stream() from re-attaching a source for every chunk of a run, since each chunk re-sends the same playlist URL. Its only reset lived in load_audio(), which runs from an effect gated on `waveform_ready || !show_recording_waveform`. A stream never renders the waveform branch, so waveform_ready can never become true, and with the default show_recording_waveform=True neither side of the guard holds. The flag stayed true after the first run, so every later run attached nothing and played silence. Reset it in a small effect keyed on the URL instead, the way Video.svelte already does. Each run streams to its own playlist URL, so a change of URL is exactly what signals a new stream to attach. The previous Hls instance is now destroyed too, instead of being left bound to the same audio element.
🪼 branch checks and previews
Install Gradio from this PR pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/ba5eb705409a3e9202995bbe4425bb17d941c4b2/gradio-6.26.0-py3-none-any.whlInstall Gradio Python Client from this PR pip install "gradio-client @ git+https://github.com/gradio-app/gradio@ba5eb705409a3e9202995bbe4425bb17d941c4b2#subdirectory=client/python"Import Gradio JS Client from this PR via CDN import { Client } from "https://huggingface.co/buckets/gradio/npm-previews/resolve/ba5eb705409a3e9202995bbe4425bb17d941c4b2/browser.js"; |
🦄 change detectedThis Pull Request includes changes to the following packages.
|
There was a problem hiding this comment.
Pull request overview
Fixes a frontend regression where gr.Audio(streaming=True) only plays the first streaming run by ensuring streaming state is reset when a new playlist URL indicates a new run, and by preventing HLS instances from accumulating across runs.
Changes:
- Track the currently attached
Hlsinstance inAudioPlayerand resetstream_activebased on the derivedurl(new run == new URL). - Destroy the previous HLS instance before attaching a new one to avoid leaking
MediaSourcebindings. - Add a unit test to verify a new streaming run triggers a new HLS source attachment.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| js/audio/player/AudioPlayer.svelte | Resets streaming state on URL changes and manages HLS instance lifecycle for streaming audio outputs. |
| js/audio/audio.test.ts | Adds a regression test ensuring consecutive streaming runs reattach a new HLS source. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| beforeEach(() => { | ||
| load_source = vi | ||
| .spyOn(Hls.prototype, "loadSource") | ||
| .mockImplementation(() => {}); | ||
| }); |
There was a problem hiding this comment.
The vitest browser matrix in this repo is pinned to Chromium (js/spa/vite.config.ts, instances: [{ browser: "chromium" }]), so there is no CI-provider variance here and Hls.isSupported() is always true. If it were false the test would fail on the waitFor assertion rather than hang. Mocking it would also hide a real environment regression instead of surfacing it, and the non-HLS branch it would mask carries the same bug this PR fixes, so it deserves a real test rather than a mock.
Destroying it only when attaching the next stream left an instance behind whenever the value stopped being a stream, and clearing the value unmounts AudioPlayer entirely, so nothing ran at all there. Move the teardown into the URL effect, above the load effect so the detach cannot clear a source that effect has just set, and destroy it in the onMount teardown for the unmount path.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.changeset/icy-cups-join.md:6
- Repository policy says not to hand-author
.changeset/*.mdfiles because a GitHub Action generates them from the PR title, and an existing changeset will override the changelog entry. This file should be removed from the PR to avoid publishing an unintended/duplicated changelog entry (see AGENTS.md Pull Request Rules #7).
---
"@gradio/audio": patch
"gradio": patch
---
fix:Fix `gr.Audio(streaming=True)` playing only the first streaming run
The new-run path is where the leak fix actually matters, but only the clear-to-null path asserted on it. The native branch that runs when Hls is unsupported carries the same reset bug and was untested. Chromium always supports HLS, so the only way in is to force isSupported() false; that is the opposite of pinning it true, which would have hidden a regression on the path the other tests already cover.
The Hls setup was duplicated verbatim in AudioPlayer.svelte and Video.svelte, on two different hls.js versions (1.5.13 and 1.6.15), so the bundle carried two copies and the teardown fix had landed in only one of them. Both now go through attach_hls_stream in @gradio/utils/hls, pinned to 1.6.15, and Video destroys its instance on a source change and on unmount the way Audio already did. The helper sits behind a subpath export so packages that do not stream still never pull hls.js in. The helper reports an unrecoverable error back to the caller. The old code destroyed the instance there but left stream_active true and the caller's reference pointing at the dead object, so the rest of that run was silent and the next teardown destroyed it a second time. stream_active becomes $state so the attach effect tracks it, matching Video. As a plain let it was untracked, and correctness rested on the reset effect being declared before the attach effect, which no test would have caught. A stale waveform is now torn down when its branch unmounts, and neither load path feeds a stream URL to wavesurfer. Going from a file value to a stream left waveform_ready true, so the playlist was loaded into wavesurfer, and the async decode failure then assigned that playlist to audio_player.src, replacing the MediaSource HLS had just attached.
gr.Audio(streaming=True) playing only the first streaming rungr.Audio and gr.Video
| --- | ||
| "@gradio/audio": patch | ||
| "@gradio/utils": patch | ||
| "@gradio/video": patch | ||
| "gradio": patch | ||
| --- | ||
|
|
||
| fix:Fix streaming playback and HLS lifetime in `gr.Audio` and `gr.Video` |
The Hls and native stream sources were coordinated by manual flags (stream_active, active_hls) written from an attach function, a URL-keyed reset effect, an error callback and unmount teardown; every regression in review was an interaction between two of those sites. A single $effect now owns the stream in both components: it attaches one instance per playlist URL and its teardown destroys it on a new run, on clearing and on unmount. The effect depends only on equality-stable deriveds, since the value object is new every chunk. The helper loses the lifetime callbacks, registers its listeners before loading (which can emit synchronously), and exports Hls and is_hls_supported so the components no longer pin hls.js themselves. A fatal unrecoverable error just destroys the instance; destroy() is idempotent in hls.js 1.6.15, so the later teardown stays safe. On the waveform side, the duplicate wavesurfer load in load_audio() is gone (every file was fetched and decoded twice), the waveform teardown clears the subtitle handlers registered on it, and handle_waveform_error ignores errors while the value is a stream so a late rejection cannot steer the native player onto the playlist.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
.changeset/icy-cups-join.md:8
- This repo’s PR rules explicitly say not to hand-author
.changeset/*.mdfiles because a GitHub Action generates them from the PR title; committing one will override the generated changelog entry. Please remove this changeset file from the PR.
---
"@gradio/audio": patch
"@gradio/utils": patch
"@gradio/video": patch
"gradio": patch
---
fix:Fix streaming playback and HLS lifetime in `gr.Audio` and `gr.Video`
The non-MSE fallback branch registered no teardown, so clearing the value left the previous stream playing behind the empty state on the always-mounted native element. The teardown now pauses and detaches the source, but only while the src is still the one this effect set: effects run in declaration order, so the next value's source may already be in place (hls.js guards its detach the same way). handle_waveform_error acted on the current value rather than the URL whose load failed, so a slow file's late decode rejection could push a newer, valid file onto the native fallback. The load effect now captures the URL and the handler discards rejections the value has moved past. The manifest-parsed callback also captures autoplay via untrack up front instead of reading waveform_settings inside the callback, and the remaining value?.* reads moved onto the equality-stable deriveds. Test fixtures now use an unroutable host instead of example.com, since those URLs land on real media elements in browser mode.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
.changeset/icy-cups-join.md:5
- This repository’s PR rules say not to hand-author
.changeset/*.mdfiles because a GitHub Action generates them from the PR title; keeping this file will override the autogenerated changelog entry (and the summary line currently has afix:Fixtypo). Please delete this changeset file and let the bot generate it.
---
"@gradio/audio": patch
"@gradio/utils": patch
"@gradio/video": patch
"gradio": patch
| } | ||
| media.src = url; | ||
| if (untrack(() => waveform_settings.autoplay)) media.play(); | ||
| }); |
gradio 6 changed gr.Video's value model from the {video, subtitles}
envelope to a plain FileData (subtitles became a component prop), but
stream_output kept emitting the old envelope. The frontend reads
value.url off the value directly, finds undefined, and renders the
empty state, so a streaming gr.Video output has shown nothing since
the change; the e2e specs covering it are skipped, so CI never saw
it. This also un-breaks the desired_output_format lookup in
handle_streaming_outputs, which reads orig_name from the top level of
the payload.
Match gr.Audio's stream_output shape and type the payload as
FileDataDict.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
.changeset/icy-cups-join.md:5
- This repository’s contributor guidelines explicitly say not to hand-author
.changeset/*.mdfiles because a GitHub Action generates them from the PR title, and an existing changeset will override the generated changelog entry (AGENTS.md:39). This changeset file should be removed from the PR so the automation can generate the correct entry.
---
"@gradio/audio": patch
"@gradio/utils": patch
"@gradio/video": patch
"gradio": patch
The stale-URL guard added in the previous round never ran on the path that actually fires. wavesurfer's load() emits `error` synchronously before it rejects, and the event carries no URL, so the listener always won the race and dropped the current file into the native fallback. Load failures now go through the load promise, which knows which URL failed, and the event listener is left to the media element errors it is the only source for. The fallback also kept the failed file attached to the native element, so a later file that decoded fine played behind the restored waveform with no way to stop it. The element is released when the waveform recovers. Two smaller consequences of the previous round: the stream teardown paused the media element, which dispatches a `pause` the app never caused, where `load()` stops playback on its own; and autoplay was snapshotted at attach time instead of read when the manifest parses. Also clear the subtitle handlers when create_waveform() replaces the instance, and keep the HLS console output for fatal errors only.
There was a problem hiding this comment.
🔵 Needs a closer look
A hand-authored .changeset/*.md file is included, but repository policy requires leaving changesets to the automated Action generation.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
.changeset/icy-cups-join.md:5
- Repository policy says not to add hand-authored
.changeset/*.mdfiles because an Action generates them from the PR title, and a manual one will override the changelog entry. This changeset should be removed so the bot-generated one is used instead (AGENTS.md:39).
---
"@gradio/audio": patch
"@gradio/utils": patch
"@gradio/video": patch
"gradio": patch
- Files reviewed: 12/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
Two reviews of the streaming work converged on the wavesurfer error
routing, and one found that the HLS teardown can discard the source
that replaces a stream.
The `error` listener was gated on a count of in-flight loads, on the
premise that a load failure reaches the load promise and the event
handles the rest. A media element error reaches neither. wavesurfer
reads the duration by waiting for `loadedmetadata`, an errored element
never fires it, and that wait has no reject path, so the load hangs:
the promise never rejects and the listener returns on the counter. A
file whose bytes fetch but which the element cannot parse therefore
left a blank waveform with no fallback, where main fell back to the
native player. Worse, the decrement lived in that hung promise's
`finally`, so the first such file disabled the listener for the rest of
the mount. Errors are now separated by type: `MediaError` is a media
element error and the only signal for one, everything else belongs to
the load promise, which knows which URL failed. The counter is gone.
The stream effect and the effect assigning a plain file both wrote the
native element's source, and correctness rested on hls.js skipping its
own cleanup when `src` no longer holds its object URL. That guard reads
`(media.querySelector('source') || media).src`, and when hls.js
attaches through a ManagedMediaSource (Safari 17+, iOS 17+, the default
whenever the constructor exists) it keeps its object URL in a `source`
child, so the guard matches and the detach clears the element even
though `src` moved on. With `show_recording_waveform=False`, a stream
followed by a file left the player empty. The two effects are now one,
since they own one resource, and Svelte runs a teardown before the next
body, so the release and the next assignment cannot happen out of
order. `gr.Video` is not exposed: `VideoPreview` keys the player on the
URL, so its teardown always runs on an element being discarded, and its
other consumers never stream.
`play()` also gets a `catch` at the three streaming call sites. The
teardown's `load()` rejects a pending play promise by spec, so every
stream teardown logged an unhandled rejection.
All three tests were confirmed to fail before the fix.
There was a problem hiding this comment.
🟡 Changes recommended
The PR adds a hand-authored .changeset file (against repo rules) and the updated Audio player effect still leaves the native file src attached when the value is cleared in the no-waveform path.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
.changeset/icy-cups-join.md:8
- This repository’s PR rules prohibit hand-authoring
.changeset/*.mdfiles because a GitHub Action generates them from the PR title; committing one can silently override the intended changelog entry (see AGENTS.md:39). Please remove this changeset file from the PR.
---
"@gradio/audio": patch
"@gradio/utils": patch
"@gradio/video": patch
"gradio": patch
---
fix:Fix streaming playback and HLS lifetime in `gr.Audio` and `gr.Video`
- Files reviewed: 12/13 changed files
- Comments generated: 1
- Review effort level: Lite
| if (!use_waveform) { | ||
| media.src = url; | ||
| } |
A media element error carries no URL, so the listener attributes it to
whatever is attached. The residue is that an error belonging to a file
the value has moved past can still land on its successor: wavesurfer
keeps the old file on its element for the whole of the next file's
blob fetch, and the load effect has already cleared the flag by then,
so the handler runs to the end and points the native element at a file
that is loading fine. Nothing reset it afterwards, because the waveform
branch is hidden rather than unmounted while the fallback is active, so
the component stayed on the bare native element until the value
changed. The load promise's success branch is the only place that still
knows which URL loaded, so it now releases a fallback that belongs to
the URL it just loaded. A hung load never resolves, so it cannot
release the fallback it caused.
The `loaded` action in js/video/shared/utils.ts awaits `node.play()`
with nothing attached to the promise, on the same element the stream
effect owns, so it had the unhandled rejection this PR just fixed at
the other three call sites.
`Video.test.ts` stubbed `play()` with a `vi.fn()` returning undefined,
which now breaks on the `catch` the callers attach. It only passed
because `loadSource` is mocked and the manifest never parses.
Also two comment corrections: the media element error path cannot see
wavesurfer's `new Error("Media error")` substitute, since a real
element has its `error` set before the event fires, and only the stream
branches of the source-owning effect need a teardown.
The new test was confirmed to fail before the fix.
Streaming `gr.Video` played on its own regardless of the prop, because
the manifest callback called `play()` unconditionally. The call predates
this PR, but streamed video rendered nothing at all until the payload
shape was fixed here, so nothing about it was observable and there is no
behaviour anyone can be relying on. The element already carries the
`autoplay` attribute and `use:loaded` already acts on the prop, so the
callback was the one place left that could override it. It is now gated
the way the audio player's is.
The rest are review follow-ups in the same code:
Non-fatal HLS errors were dropped entirely. Both pre-PR copies logged
every error, and the non-fatal details are the symptom of a stream that
connects and plays nothing, which is the bug class this PR is about.
They go to `console.debug`, which keeps them out of the way at a
one-second buffer while leaving them reachable.
wavesurfer substitutes `new Error("Media error")` when the element
reports no error object. A real element sets `error` before firing, so
that should be unreachable, but the guard added here dropped it into
the same gap the guard exists to close: no fallback, no rejection, and
nothing in the console. Both shapes now route to the handler.
The non-MSE stream branch called `play()` although the element carries
`autoplay={waveform_settings.autoplay}`, so assigning the source is
already enough. The remaining three call sites go through `play_media`,
which absorbs the teardown's `AbortError` and an autoplay-policy block
and lets anything else reach the console, rather than each swallowing
every rejection.
`stream_output`'s payload shape was only pinned on the `value is None`
call, which returns before any chunk is converted. It is now pinned on
the chunk path as well.
Two test-hygiene fixes: both streaming suites restored their spies
before `cleanup()`, which is what unmounts and therefore what runs the
teardowns, so nothing could assert on unmount behaviour; and the
ManagedMediaSource stub replaced `destroy` without calling through,
leaving a live instance behind for the rest of the run.
The autoplay and substitute-error tests were confirmed to fail before
the fix.
The test added in the previous commit fed a real mp4 through the chunk path, so it converted with ffmpeg and read the duration with ffprobe. ffprobe died with SIGSEGV on the converted file on CI's build, and no other test in the suite depends on either tool. What the test is for is the payload shape and the chunk it returns, so it now passes a `.ts` path, which needs no conversion, and patches the duration lookup.
`stream_output` built the `.ts` path with `value.replace(".mp4", ".ts")`,
which rewrites every occurrence, so a chunk under a directory whose name
ends in .mp4 was handed to ffmpeg as a path in a directory that does not
exist. `Path(value).with_suffix(".ts")` is what `Audio` already uses for
the same job.
`play_media`'s doc comment said unexpected rejections reach the console
while the code logged them at `console.debug`, which both Chrome and
Firefox hide by default, so a `NotSupportedError` produced the silent
failure the helper exists to prevent. It logs at `warn` now, and wraps
the call in `Promise.resolve`, since `play()` predates promises and can
return nothing in an old or embedded implementation.
The source-owning effect's comment explained the missing teardown on
the file branch with "the player unmounts with the value", which a
reader cannot check from that file. It now names the two parents that
make it true: `StaticAudio`'s `{#if value !== null}` and
`InteractiveAudio`'s else branch.
The path test was confirmed to fail before the fix.
Description
A
gr.Audio(streaming=True)output plays the first run and is silent on every run after it. Settingwaveform_options=gr.WaveformOptions(show_recording_waveform=False)works around it.stream_activewas the flag that kept the player from re-attaching a source for every chunk of a run, since each chunk re-sends the same playlist URL. Its only reset lived inload_audio(), which is called from an effect gated onwaveform_ready || !waveform_options.show_recording_waveform. A stream deliberately never renders the waveform branch, sowaveform_readycan never become true, and with the defaultshow_recording_waveform=Trueneither side of that guard holds.load_audio()never ran,stream_activestayedtrueafter the first run, and every later run returned early without attaching anything.The guard picked up
waveform_readyin #12779 (the Svelte 5 migration). Before that the load ran on every URL change, which is what keptstream_activein sync. #13728 later added the!show_recording_waveformescape hatch for the disabled-waveform player, which is why that setting works around the bug today.The underlying problem is that the stream's lifetime was coordinated by manual flags (
stream_active,active_hls) written from four separate places: an attach function, a URL-keyed reset effect, an error callback, and unmount teardown. This bug was one missed interaction between those sites, and review of earlier revisions of this PR kept finding others. So instead of patching the flag, the fix makes a single$effectown the media element's source in bothgr.Audioandgr.Video: it attaches one HLS instance per playlist URL, and its teardown destroys that instance when the URL changes, when the value is cleared, and when the component unmounts. Ingr.Audiothe same effect assigns a plain file value too, since a stream and a file are the same resource. Since a run re-sends its own URL with every chunk and thevalueobject is new on each chunk, the effect depends only on equality-stable values (url,is_stream), so chunks of the same run never restart the stream. A fatal unrecoverable HLS error destroys the instance and playback stays stopped for that run, as before; the next run brings a new URL and the effect re-attaches naturally (Hls.destroy()is idempotent in hls.js 1.6.15, so the teardown destroying it again is safe, and a test covers that).One caveat on that run boundary: the playlist URL carries a per-run key which is currently
id(iterator), and #13811 fixes sequential runs colliding on a reused address. Where they do collide the URL is identical between runs, so this effect correctly keeps the source it already has and the second run is silent for a backend reason this branch cannot see. #13807 needs both changes to be closed for good.Related fixes in the same lifecycle, kept in this PR because they share the defect surface and the reviewing context:
Video.sveltecarried a verbatim copy of the HLS setup, on a different hls.js version:@gradio/audiopinned 1.5.13 exactly and@gradio/videoasked for^1.6.13, so the bundle shipped two copies and any fix had to be applied twice. Both components now stream throughcreate_hls_streamfrom@gradio/utils/hls, and hls.js is pinned once, in@gradio/utils, at 1.6.15. That means the audio player moves up a minor version, which is worth a look from anyone who knows that range; theManagedMediaSourceattach described below is not new in it, since 1.5.13 has the sameappendSourcepath. The helper is a subpath export, so packages that do not stream never pull hls.js into their bundles; the tradeoff is that hls.js now sits in@gradio/utils's install footprint for every dependent package.waveform_readywas never reset when the waveform branch unmounted, so a component that had shown a normal file and then received a stream pushed the.m3u8playlist into wavesurfer; the async decode failure then assigned that playlist toaudio_player.src, replacing the MediaSource HLS had just attached (waveform_readygoes stale when a streaming value follows a regular file ingr.Audio#13810). The teardown now resets that state and clears the subtitle handlers registered on the destroyed instance, andhandle_waveform_errorignores errors while the value is a stream.errorsynchronously before it rejects, and the event carries no URL, so a failure belonging to a file the value had already moved past still dropped the current file into the native fallback. And once the fallback was active, the failed file stayed attached to the native element, so a later file that decoded fine played behind the restored waveform with no way to stop it. Load failures now go through the load promise, the only path that knows which URL failed, and the native element is released when the waveform recovers. A media element error needs the event instead: it interrupts wavesurfer's wait forloadedmetadata, which has no reject path, so that load never settles and the promise never sees the error. The two are told apart by type, since wavesurfer passes the element's ownMediaErrorstraight through. Until this, a file whose bytes fetched but which the element could not parse reached neither path and left a blank waveform with no fallback and nothing in the console.srcno longer holds its own object URL, but that check reads(media.querySelector('source') || media).src, and when it attaches through aManagedMediaSource(Safari 17+ and iOS 17+, which is the default wherever the constructor exists) it keeps its object URL in asourcechild. The check matches there even aftersrchas moved on, so the detach clears the element, and withshow_recording_waveform=Falsea stream followed by a plain file left the player with nothing to play. One effect owning both removes the problem, because Svelte runs a teardown before the next body.gr.Videois not exposed to it:VideoPreviewkeys the player on the URL, so its teardown always runs on an element that is being discarded anyway.load()rejects a pendingplay()promise by spec, and the streaming call sites did not catch it. The same catch also covers playback blocked by an autoplay policy.load_audio()duplicated the wavesurfer load that the URL effect already performs. The wavesurfer load now happens in one place, and the other effect only assigns the native<audio>source when the waveform is off.gr.Videooutput has shown nothing at all since the gradio 6 value-model change: the value moved from the{video, subtitles}envelope to a plainFileData(subtitles became a component prop), butVideo.stream_outputkept emitting the old envelope, so the frontend readvalue.urlasundefinedand rendered the empty state. The e2e specs covering streamed video are skipped, so CI never caught it.stream_outputnow returns a flatFileDatathe waygr.Audio's does. That also changes what agradio_clientcaller of a streaminggr.Videoendpoint receives, from{"video": "<path>"}to the bare path, which is the shapepostprocess()already returns for non-streaming values.gr.Videoplayed by itself whateverautoplaysaid, because the manifest callback calledplay()unconditionally. That had no visible effect while the payload shape above kept the player empty, so fixing the shape is what would have turned it into a regression people could see. The callback is gated on the prop now, the waygr.Audio's is; the element already carried theautoplayattribute and theloadedaction already acted on the prop, so this was the one place left that could override it.demo/stream_video_outsetsautoplay=Trueexplicitly, so nothing in the repo relied on the forced play.play()rejection was swallowed, and non-fatal HLS errors stopped being logged once both components started routing through the shared helper, although both earlier copies logged them. Rejections now go through aplay_mediahelper in@gradio/utilsthat absorbs the two that are not the app's doing, a teardown'sAbortErrorand an autoplay-policy block, and logs anything else; non-fatal HLS details go toconsole.debug, which keeps the symptom of a stream that connects and plays nothing reachable without filling the console at a one-second buffer.Deferred as follow-ups rather than grown into this PR: a rewrite of the waveform's own lifetime management (it is entangled with the controls, subtitles, trim, and native-fallback flows), and bounding hls.js's fatal-error recovery, which retries a failing playlist or segment without a limit.
Closes: #13807
Before / after Spaces
Both Spaces stream audio and video. On the before Space the audio's second run goes silent and the video output never displays anything (the streamed-video value shape bug above).
AI Disclosure
We encourage the use of AI tooling in creating PRs, but the any non-trivial use of AI needs be disclosed. E.g. if you used Claude to write a first draft, you should mention that. Trivial tab-completion doesn't need to be disclosed. You should self-review all PRs, especially if they were generated with AI.
🎯 PRs Should Target Issues
Before your create a PR, please check to see if there is an existing issue for this change. If not, please create an issue before you create this PR, unless the fix is very small.
Not adhering to this guideline will result in the PR being closed.
Testing and Formatting Your Code
PRs will only be merged if tests pass on CI. We recommend at least running the backend tests locally, please set up your Gradio environment locally and run the backed tests:
bash scripts/run_backend_tests.shPlease run these bash scripts to automatically format your code:
bash scripts/format_backend.sh, and (if you made any changes to non-Python files)bash scripts/format_frontend.sh