fix: fetch camera and image sources from their still endpoint - #115
Merged
g4bri3lDev merged 1 commit intoAug 23, 2026
Merged
Conversation
media_source.async_resolve_media() hands the camera and image domains a *_proxy_stream endpoint, because it resolves on behalf of media players, which want something to keep playing. Those endpoints serve an endless multipart/x-mixed-replace body while declaring a still mime type, so the accept: image/* selector offers every camera and image entity and every one of them stalled _async_download_image() for the full 30 s timeout. Measured: 5.19 MB from a camera and 652 KB from an image entity in ten seconds, neither response ending. The failure was also illegible. TimeoutError is not an aiohttp.ClientError, so the handler below the read never caught it and the user got a raw traceback instead of media_download_error. Naming it in the except is the second half of this change; the parametrized download_error test pins it, since no line-coverage metric can. Both domains already serve the frame we want from the endpoint sitting next to the stream -- /api/camera_proxy/ and /api/image_proxy/, the same ones their own media-source thumbnails point at. Rewriting the id to that path and letting the existing download path sign and fetch it keeps one code path for every remote source, and brings that path's relative-URL signing branch under test for the first time. Calling camera.async_get_image()/image.async_get_image() instead was built first and rejected. It reaches the same code -- CameraImageView is a thin wrapper over the same _async_get_image() -- but importing those components costs a guard so a cold import never lands on the event loop, two branches because the two functions differ in their optional parameters, and PyTurboJPEG in the test groups, since components/camera imports it at module scope and no HA is present to install its requirements. Going through _async_get_image() either way is also what makes this work for cameras that have no still of their own: it pulls a keyframe from the stream when use_stream_for_stills is set. Untested here -- no such camera on hand. Special-casing only camera was the original scope. Measuring the other sources showed image fails identically, and unconditionally -- its media source has no branch at all -- and it is the domain listing this integration's own image entities.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #114.
What it does
Both domains serve the frame we want from the endpoint next to the stream, so
rewrite the media-source id to that path and let the existing download path
sign and fetch it:
Plus naming
TimeoutErrorin theexcept, so a timeout reports asmedia_download_errorrather than a raw traceback.This is not a shortcut around the entity API:
CameraImageViewis a thinwrapper over the same
_async_get_image()thatcamera.async_get_image()calls, which also means cameras with no still of their own work, via
use_stream_for_stills.Calling
async_get_image()directly was built first and dropped -- samebehaviour, but importing those components costs a cold-import guard, two
branches (the two functions differ in their optional parameters), and
PyTurboJPEGin the test groups.Verified
Live, HA 2026.8.3, Waveshare 7.3" BWGBRY over BLE:
media-source://camera/..., no stream supportTimeoutErrormedia-source://camera/..., stream only (RTSP/H264).m3u8,UnidentifiedImageErrormedia-source://image/...TimeoutErrormedia-source://media_source/local/*.jpgmedia-source://ai_task/...The stream-only case is a
genericcamera overrtsp://.../h264.sdp, H2641920x1080, no
still_image_url, souse_stream_for_stillsis what produces theframe.
/api/camera_proxy/on it returns a real 1920x1080 JPEG keyframe.Worth noting for that row: because its media source declares
application/vnd.apple.mpegurl, anaccept: image/*picker filters it out, soit is only reachable by writing the
media_content_idby hand.Not verified:
image_upload(none present, unaffected either way).Notes
_async_upload_image()reaches the changed code;drawcustomdoes not.pyproject.toml,uv.lockor translation change.