Skip to content

Letterbox art and remove BMP support - #168

Merged
maximmaxim345 merged 4 commits into
mainfrom
128-artwork-letterboxing
Aug 27, 2026
Merged

Letterbox art and remove BMP support#168
maximmaxim345 merged 4 commits into
mainfrom
128-artwork-letterboxing

Conversation

@kahrendt

@kahrendt kahrendt commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

The previous artwork scaling rule said the server "will scale images to fit within the specified dimensions while preserving aspect ratio", which reads as fit-inside: a square cover requested at 800x480 would arrive 480x480. The spec will adopt what aiosendspin already does: letterbox.

Changes:

  • width/height are the dimensions of the delivered image, not upper bounds (renamed from media_width and media_height)
  • The server MUST pad to those dimensions with black and MUST NOT crop
  • stream/start width/height MUST equal the declared width/height rather than merely not exceeding them
  • Drop 'bmp' from the artwork formats, leaving 'jpeg' and 'png' (see comments in Artwork transfers can starve audio: head-of-line blocking on the shared connection #134)

The equality rule settles the case that blocked scheduled artwork updates: with the delivered size pinned to the declaration, a later image cannot arrive at a different aspect ratio than the resolution stream/start announced.

I'll make a future PR splitting large images across messages, the remaining part of #134.

Closes #128

Breaking Changes

  • BMP is now unsupported
  • media_width/media_height are renamed to width/height
  • Servers must letterbox artwork

The scaling rule said the server "will scale images to fit within the
specified dimensions while preserving aspect ratio", which reads as fit-inside:
a square album cover requested at 800x480 would arrive 480x480. aiosendspin
letterboxes instead, so the delivered image is 800x480 with black bars, and the
spec never said which is correct.

Letterboxing is the behavior to keep. It is what implementations already do, it
keeps clients from having to lay out a variable-size image, and it makes the
dimensions in stream/start match what the client asked for.

- media_width and media_height are the dimensions of the delivered image, not
  upper bounds
- The server MUST pad to those dimensions with black and MUST NOT crop
- stream/start width/height MUST equal the declared media_width/media_height
  rather than merely not exceeding them

The equality rule also settles the case that blocked scheduled artwork updates:
with the delivered size pinned to the declaration, a later image cannot arrive
at a different aspect ratio than the resolution stream/start announced.

Gradient or artwork-derived padding was considered and left out; the padding is
black.
BMP is in the format list because JPEG, PNG and BMP were what ESPHome decoded
out of the box, not because a client needs it. It is the one format whose size
is set by the pixel count rather than the image: an 800x480 24-bit BMP is about
1.1 MB, roughly 450 ms of wire time on a 20 Mbps link, and it has to be held in
memory whole. The devices that would ask for BMP to avoid an image library are
the constrained ones least able to absorb either cost.

Clients keep 'jpeg' and 'png', both of which have decoders available for
microcontrollers.

This removes the largest artwork payloads but not the head-of-line blocking
behind them; splitting large images across messages is still open in #134.

@maximmaxim345 maximmaxim345 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you also rename media_width and media_height to just width and height in this PR? See: #128 (comment)

Comment thread roles/artwork/v1.md Outdated
A stream/request-format the server does not honor leaves the stream in its
previous configuration (messaging.md), but the stream/start rule read as
though every request moved the channel's current capability. That was
harmless while width/height only had to not exceed the declaration; now
that they must equal it, a declined request would put the continuing
stream in violation.

Scope the clause to the changes the server honored. Per-change rather
than per-request, since the artwork request-format fields are each
optional and a server can honor some and decline others.
media_width/media_height meant a maximum while stream/start reported the
actual encoded width/height, so the two names described two quantities.
Delivering at exactly the declared size collapsed that distinction: they
are now the same number, and the prefix no longer disambiguates anything.

Rename them to width and height, matching stream/start and the editorial
rule of one canonical name per term. The stream/start requirement folds
into a single clause now that both sides use the same names.

@maximmaxim345 maximmaxim345 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!

@maximmaxim345
maximmaxim345 merged commit ffca6f4 into main Aug 27, 2026
1 check passed
@maximmaxim345
maximmaxim345 deleted the 128-artwork-letterboxing branch August 27, 2026 11:22
maximmaxim345 added a commit that referenced this pull request Sep 2, 2026
An artwork image larger than one Noise frame (65518 bytes of payload)
currently relies on transport fragmentation, which admits no
interleaving: the sender must finish the fragmented message before any
other frame, so the whole image is an indivisible burst that audio
chunks cannot preempt. A player holding only `min_buffer_ms` of audio
can underrun behind it; on a live stream that truncates audio. With BMP
gone (#168) the worst case shrank, but photographic JPEG still exceeds
one frame beyond roughly 500x500 (the spec's own `stream/start` example,
800x800 JPEG, is 3-5 frames), and PNG exceeds it at almost any artwork
size.

## Changes:

- An image is transferred as a fixed 14-byte announce message carrying
the timestamp and the image's `total_size` (uint32), followed by parts
carrying only image data, all on the channel's message type; a flags
byte after the type byte distinguishes the message kinds, following the
shape #172 gave fragmentation
- The announce carries no image data, so a client can allocate its image
buffer before any image bytes arrive and write each part directly into
it; the transfer completes when the received data reaches `total_size`
- Every artwork message is capped at 65518 bytes after the type byte so
it never needs transport fragmentation; any other messages MAY be sent
between the messages of a transfer, and transfers on different channels
are independent
- The pending image is now the most recently announced image, from its
announce until it becomes current: an announce discards any held pending
image, partly received or complete, and the pending image becomes
current once its transfer completes and its timestamp is reached
- A rejected announce (no active stream, or client not available) still
starts its transfer, so its parts are rejected rather than treated as
malformed
- The clear message becomes an announce with `total_size` 0, which
completes immediately with no parts
- A two-byte cancel message (flags bit 1) discards the channel's pending
image, partly received or complete, leaving the current image showing;
it replaces resend-the-current-image as the cancel recipe in the
scheduled-artwork rules, which under chunking would cost a full image
retransfer
- Malformed sequences (a message under 2 bytes or over the frame cap, an
announce whose length is not 14 bytes, a cancel longer than 2 bytes, a
part with no transfer in flight, data extending past `total_size`, a
nonzero reserved flag bit, both flag bits set) close the connection,
matching fragmentation

The pending-image redefinition keeps the newest-wins rule from #135
applying at the announce, as it did at message arrival before chunking,
and means a client needs at most two image buffers per channel,
receiving the transfer directly into the pending one.

Every image costs one extra small message, including images that fit in
a single frame; the uniform format was judged worth ~31 bytes of
ciphertext per image to give clients flexibility to transfer directly
into a proper sized buffer.

There is no explicit last-part flag; `total_size` already determines
completion, and a redundant end marker would add mismatch states to
define.

Every artwork message changes shape: the flags byte shifts the
timestamp, the header and image data travel in separate messages, and
the empty clear message becomes a bare announce with `total_size` 0.

Closes #134

## Breaking changes:

The wire format for artwork is completely changed. All servers and
clients will have to re-implement to handle:

- the announce binary message
- the artwork specific split chunks versus using the protocol's
generalized noise split
- handling the pending slot; i.e., servers should now send a cancel
message instead of re-sending the current image

---------

Co-authored-by: Maxim Raznatovski <nda.mr43@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Artwork dimensions letterboxing ambiguity

2 participants