Skip to content

media: send and receive GIFs like the LINE Chrome Extension - #284

Open
clins1994 wants to merge 4 commits into
beeper:mainfrom
clins1994:feat/send-gifs
Open

clins1994 wants to merge 4 commits into
beeper:mainfrom
clins1994:feat/send-gifs

Conversation

@clins1994

@clins1994 clins1994 commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

GIFs sent from Beeper now reach LINE exactly as the LINE Chrome Extension sends them, and GIFs from LINE arrive in Beeper as GIFs. Review the image send path in pkg/connector/send_message.go and the new pkg/connector/gif.go.

Screenshot 2026-10-25 at 20 48 14
Before After
LINE iOS: a transparent GIF from Beeper shows a black background while stopped The thumbnail is drawn on white, at Chrome's size (verified in the bridge log: thumb_width=384 thumb_height=384).
Beeper iOS: GIFs sent from LINE show as still first frames GIFs from LINE arrive as image/gif with fi.mau.gif and animate (checked in Beeper iOS and Desktop).
LINE iOS: a GIF from Beeper in a plain chat shows "Unable to load this photo as it's no longer available" Plain chats upload the image with cat=original, like Chrome. LINE iOS loads and plays it.

🇬🇧 More details · 5 min read

Summary

Closes #90. This takes the GIF part of #246 by formeo14 (co-author on every commit) and matches it field by field to a network capture of the LINE Chrome Extension 3.7.2.

Why

The bridge sent GIFs that differed from what LINE's own clients send, and labelled every received image as image.jpg:

  • MEDIA_CONTENT_INFO had no width or height, and fileSize and FILE_SIZE held the encrypted size (the file plus 32 bytes).
  • Image metadata carried FILE_NAME and a contentType key, which the extension never sends.
  • In plain chats the bridge uploaded a thumbnail that failed with a 400 error. It also uploaded the image without cat=original, and LINE iOS then failed to load it.
  • Thumbnails were up to 1280 px, and transparent areas turned black.
  • Beeper Desktop had no GIF picker in LINE chats and refused .gif files (This chat doesn't support sending files), because the bridge did not advertise fi.mau.gif.
  • The animated-GIF check scanned for the byte 0x2C, which also appears in compressed pixel data. Some single-frame GIFs were marked animated.

Changes

  • pkg/connector/gif.go: originalImageInfo and imageMediaMetadata build the image metadata the extension sends. convertVideoToGIF turns GIF-picker MP4 and WebM into GIF (from GIF, sticker and emoji support with media #246).
  • pkg/connector/send_message.go:
    • Images send real width/height, the original size, and animated for GIFs, with no FILE_NAME or contentType key.
    • Plain images send MEDIA_CONTENT_INFO only and upload no thumbnail.
    • fi.mau.gif videos are converted to GIF and sent as images.
    • GIFs over 20 MB are refused with ErrMediaTooLarge.
  • pkg/connector/media.go:
    • Thumbnails have a longest side of 384 px, are never scaled up, and are drawn on white.
    • isAnimatedGif walks the GIF block structure.
  • pkg/connector/userinfo.go: advertises fi.mau.gif with a 20 MB MaxSize, and bumps the capabilities ID. It offers image/gif, plus video/mp4 and video/webm (converted) only when ffmpeg and ffprobe are installed.
  • README.md: lists FFmpeg as optional for native installs.
  • pkg/line/client.go: UploadOBSPlainOriginalImage uploads plain images with cat=original and the file name.
  • pkg/connector/handlers/image.go: received images keep their real MIME type, size and dimensions, also with direct media, and animated GIFs get fi.mau.gif.

ENC_KM is left as it is. The extension never puts it in the metadata, but changing it affects every media type, so it belongs in its own PR.

How it was tested

We wrote a 77-step test plan with line-test-plan, then ran it in three phases. The skills used here are in clins1994/beeper-line-skills. The full plan with every result, and the field-by-field findings, are in the first comment.

  1. Before, on the current code: Beeper iOS and Desktop sent GIFs, and LINE iOS sent the same files as the reference.
  2. Capture: the LINE Chrome Extension sent the same files, a size ladder (4.7, 10.2, 19.7 and 27 MB) and edge cases. The CDP network capture was recorded by line-capture and parsed and redacted by line-analyze.
  3. After, on this branch, deployed to a self-hosted bridge: we repeated the sends. The chats were an encrypted group, a plain chat with a LINE business account, a plain 1:1 and an encrypted 1:1.

Each row compares the bridge on this branch with the capture:

Check Chrome extension (capture) Bridge, this branch (log)
width/height 480×480, 960×360, 360×960 the same
fileSize / FILE_SIZE original size original size
Thumbnail 384×384, 384×144, 144×384 the same
Single-frame GIF animated: false animated: false
Plain chat MEDIA_CONTENT_INFO only, upload with cat: original the same, no thumbnail upload, no 400
GIF over 20 MB 27 MB sent as a file refused as too large (Desktop: "file too large")

Checks:

  • go build ./...
  • go test $(go list ./... | grep -v /ltsm)
  • go vet $(go list ./... | grep -v /ltsm)
  • staticcheck $(go list ./... | grep -v /ltsm)
  • goimports -local github.com/beeper/line -l pkg

New tests: pkg/connector/gif_test.go and pkg/connector/handlers/image_info_test.go.

Not tested (and why)

  • A second person receiving a bridge-sent GIF. Only one test account was available. LINE iOS does not autoplay a GIF sent from another device of the same account, whether from the bridge or from the Chrome extension. So autoplay for a real receiver is still unverified.
  • Whether LINE turns a GIF over 20 MB into a file by default. The 27 MB send in the capture came after choosing "always send as archive" for an APNG. The bridge refuses GIFs over 20 MB either way.
  • APNG and animated WebP. The extension calls them unsupported and offers to send them as an archive. This PR keeps the bridge's current behaviour for them.

Found while testing, not changed here

Screenshots

The images at the top are LINE iOS and Beeper iOS screenshots from the "before" phase. The "after" side comes from the bridge's debug log on this branch.

Related

🤖 Generated with Claude Code using clins:create-pr

clins1994 and others added 4 commits October 25, 2026 19:07
Match what the LINE Chrome Extension 3.7.2 sends for images, as captured
on 2026-10-25 (beeper#90):

- MEDIA_CONTENT_INFO carries the image's real width and height, the
  original file size, and animated true/false for GIFs.
- FILE_SIZE is the original size, not the encrypted upload size.
- Images no longer send FILE_NAME or a contentType metadata key.
- Plain chats send MEDIA_CONTENT_INFO only and upload no thumbnail, which
  also stops the failing (400) plain thumbnail upload.
- Thumbnails are at most 384 px on the longest side, never upscaled, with
  transparent areas on white instead of black.
- Detect animated GIFs by walking the GIF blocks; the old byte scan marked
  some single-frame GIFs as animated.

Advertise fi.mau.gif with LINE's 20 MB GIF limit, convert GIF-picker
videos (MP4/WebM) to GIF, and refuse GIFs over 20 MB as too large.

Receive LINE images with their real type and size, and mark animated
GIFs fi.mau.gif, instead of labelling every image image.jpg.

Co-authored-by: formeo14 <maiki2004@gmx.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In chats without end-to-end encryption (for example with a LINE business
account), the message says MEDIA_CONTENT_INFO category "original", but the
image was uploaded without cat=original. LINE iOS then showed the preview
frame and "Unable to load this photo as it's no longer available".

Upload plain images with cat=original and the file name, as the LINE
Chrome Extension does: {"ver":"2.0","name":"<file>","type":"image","cat":"original"}.

Co-authored-by: formeo14 <maiki2004@gmx.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With direct media on, generateDirectMedia replaced the type and file name
from MEDIA_CONTENT_INFO with image/jpeg and image.jpg. The saved metadata
also made the download response say image/jpeg, so GIFs from LINE still
arrived as JPEGs on that path.

Set the type, file name and (when FILE_SIZE is missing) the size from
lineImageInfo on the direct media metadata before the URI is made.

Answers the Greptile review comment "Direct GIFs still claim JPEG".

Co-authored-by: formeo14 <maiki2004@gmx.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picker videos are converted to GIF with ffprobe and ffmpeg. A native
install without them still offered video/mp4 and video/webm for
fi.mau.gif, so every picker GIF failed instead of sending.

Offer those types only when both programs are on PATH, add
"+no_gif_video" to the capability ID when they are not, and list FFmpeg as
an optional native prerequisite in the README.

Answers the Greptile review comment "Picker support ignores missing
programs".

Co-authored-by: formeo14 <maiki2004@gmx.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@clins1994

clins1994 commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor Author

The full test plan with every result, and the field-by-field findings from the LINE Chrome Extension capture. All IDs, keys and tokens are left out.


🇬🇧 More details · 30 min read

Test plan and results

Test plan: send GIFs from Beeper to LINE (#90)

Goal: a GIF sent from Beeper (the GIF picker, a .gif file, an animated sticker) arrives in LINE as an animated GIF. It behaves exactly like a GIF sent from LINE itself, on every LINE client and under every LINE app setting. Split out of #246 (commit 6241f35, "feat: support GIF sending with transparency"), with formeo14 as co-author.

Scope

Copy what LINE does, nothing extra. Anything LINE's own clients cannot do is out of scope. Write down that they cannot.

Known before testing:

  • Beeper iOS can send GIFs today (its GIF picker in the message box). Beeper Desktop cannot: it offers no GIF picker in LINE chats.
  • The bridge tells Beeper it accepts files up to 100 MB (BeeperMaxFileSize). LINE's own limit for images is unknown. Section E finds it.

Out of scope, noted on purpose:

  • LINE iOS Send Live Photos: about iOS Live Photos, not GIFs.
  • LINE iOS Autoplay videos: videos only. In scope is checking that a GIF from Beeper's GIF picker does not arrive as a video, because then this setting controls it instead of Autoplay GIFs.

Not testable, a known gap:

  • A real second person receiving a bridge-sent GIF (for example the other member of beeper/line - basic checking A3 vs C1 on their phone). Not possible during this run. Worth testing later. Not a blocker.
  • Another Beeper user receiving the GIF. There is only one test account. The receiver's side is covered by the bot relay: the bot gets the GIF and posts it back, so LINE iOS and Beeper see it as a received message.

Platforms

  • Beeper iOS
  • Beeper Desktop
  • LINE iOS
  • Chrome extension (LINE Chrome Extension 3.7.2)

Test chats

  • beeper/line - basic: you and one other person. End-to-end encrypted.
  • beeper/line - bot: you and your bot (a LINE business account). The bot uses the LINE Messaging API: relay on makes it post back everything it receives, relay off stops it. Business accounts often do not support end-to-end encryption, so this chat can use the plain upload path. D7 shows which.

Every message is visible to all members.

Before A: AirDrop test/gif-send/samples/ to the iPhone and save it in Files, so Beeper iOS and LINE iOS can attach the same files.

Test data

Every dummy string used in the tests. line-evidence treats these as safe text.

  • beeper/line - basic
  • beeper/line - bot
  • relay on
  • relay off
  • GIF test
  • 09-static
  • 10-animated
  • 11-animated
  • 12-animated
  • 13-h264
  • 21-animated-large
  • 22-animated-transparent
  • 23-animated-tiny
  • 24-animated-27mb
  • 25-animated-wide
  • 26-animated-tall
  • 27-animated-square
  • 28-animated-5mb
  • 29-animated-10mb
  • 30-animated-20mb
  • Photos and videos
  • Photo quality
  • Standard
  • High
  • Auto-download images
  • Autoplay GIFs
  • Autoplay videos
  • Send Live Photos

Run order

LINE allows one Chrome Extension session, so the capture logs the bridge out.

  1. before: bridge logged in on the self-hosted bridge, current code: A, B, C
  2. capture: log the bridge out, start line-capture, then: D, E, F. Quit Chrome when done.
  3. after: deploy, log the bridge in again: G, H

Settings record: before A, write down the LINE iOS values under Settings → Photos and videos (recorded 2026-10-25: Photo quality Standard, Auto-download images off, Autoplay GIFs on). B, C and H say when to change one, and their last step puts them back.

Relay: keep the bot's relay off except in sections B, C and H, so other sends do not get doubled.

Pacing: wait about 10 s between steps (about 15 s after a relay). Write what happened in the Result column: ✅ worked, ❌ rejected (copy the error text), n/a with the reason, plus notes (animated or frozen, cropped, quality, transparent background colour, delay).

A. Beeper today: sending

Current code. Check each one on LINE iOS with Autoplay GIFs on.

# Platform Phase Action Checks Evidence Result
A1 Beeper iOS before In beeper/line - basic, send a GIF from the GIF picker animates on LINE iOS? quality? shot: Beeper iOS; shot: LINE iOS ❌ not autoplayed on LINE iOS (sender's view). No bridge log: logging was at warn, raised to debug after this step
A2 Beeper iOS before Send the same picker GIF again same result and quality both times? shot: LINE iOS Not autoplayed on LINE iOS (sender's view); plays on tap. Log: sent as image, extension: gif, animated: true, 1.9 MB, bridge thumbnail 124×126. On LINE it shows the same width as A3; in Beeper iOS it shows at about 1/3 screen width (small pixel size)
A3 Beeper iOS before Attach 10-animated.gif from Files and send file attachment animates like the picker? shot: LINE iOS Not autoplayed on LINE iOS (sender's view); plays on tap. Log: animated: true, 57 KB, thumbnail 480×480. In Beeper iOS about 2/3 screen width. Idea: LINE scales small GIFs up to a minimum width; consider matching that in Beeper
A4 Beeper iOS before Attach 25-animated-wide.gif cropped or letterboxed? (watch the corner markers) shot: LINE iOS Not autoplayed (tap to play). Same width as the square GIFs; not cropped. Log: no width/height in MEDIA_CONTENT_INFO; thumbnail 960×360; FILE_SIZE 61024 (+32 bytes)
A5 Beeper iOS before Attach 26-animated-tall.gif cropped or letterboxed? shot: LINE iOS Not autoplayed. Width a little under half of the wide one; not cropped. Log: thumbnail 360×960, no width/height
A6 Beeper iOS before Attach 27-animated-square.gif square baseline shot: LINE iOS Not autoplayed. Looks fine. Log: thumbnail 600×600, no width/height
A7 Beeper iOS before Attach 22-animated-transparent.gif transparency kept or black/white background? shot: LINE iOS Not autoplayed. Black background while stopped, transparent once played (the bridge's thumbnail loses transparency). In Beeper a faint border shows on a light background (probably the image's edge). Log: no width/height
A8 Beeper Desktop before Open the message box and look for a GIF picker expected: none offered shot: Beeper Desktop message box ✅ as expected: the GIF picker is disabled in LINE chats
A9 Beeper Desktop before Attach 10-animated.gif as a file and send Desktop file path animates on LINE? shot: Beeper Desktop; shot: LINE iOS ❌ Beeper Desktop shows "This chat doesn't support sending files" and nothing reaches the bridge (no log entry). Desktop refuses the GIF before sending; check what the room advertises
A10 Beeper iOS before In beeper/line - bot, attach 10-animated.gif (relay off) bot chat same as basic? shot: LINE iOS Covered by B2: the bot chat uses the plain media path; GIF uploaded, thumbnail upload failed (400)

B. Beeper today: LINE settings, through the relay

The bot's relayed copy arrives as a received message, so the LINE iOS settings apply to it. Send from Beeper iOS in beeper/line - bot.

# Platform Phase Action Checks Evidence Result
B1 Beeper iOS before Send relay on bot confirms relay is on shot: Beeper iOS ✅ relay turned on
B2 Beeper iOS before LINE iOS: Autoplay GIFs on, Auto-download images on. Send 10-animated.gif relayed copy plays by itself on LINE iOS? animates in Beeper? shot: LINE iOS; shot: Beeper iOS ❌ Relayed copy shows one frame on LINE iOS (the Messaging API flattened it). Nothing in Beeper: the bridge dropped the bot's image (public OBS resource must be an absolute path). The bridge's own send took the plain path, and its thumbnail upload failed (400)
B3 Beeper iOS before LINE iOS: Autoplay GIFs off. Send it again relayed copy stays still until tapped? shot: LINE iOS n/a: the bot relay flattens GIFs to one frame and replaces the bridge's metadata with the Messaging API's, so it cannot compare Beeper with LINE (see Observations)
B4 Beeper iOS before LINE iOS: Autoplay GIFs on, Auto-download images off. Send it again placeholder until tapped? animates after tapping? shot: LINE iOS before and after tap n/a: the bot relay flattens GIFs to one frame and replaces the bridge's metadata with the Messaging API's, so it cannot compare Beeper with LINE (see Observations)
B5 Beeper iOS before LINE iOS: both off. Send it again placeholder, then still after tapping? shot: LINE iOS n/a: the bot relay flattens GIFs to one frame and replaces the bridge's metadata with the Messaging API's, so it cannot compare Beeper with LINE (see Observations)
B6 Beeper iOS before LINE iOS: Autoplay GIFs off, Autoplay videos on. Send a GIF from the GIF picker stays still (a GIF) or plays (sent as a video)? shot: LINE iOS n/a via relay. Answered by the logs instead: the Beeper iOS picker sends a real GIF (extension: gif, animated: true), not a video

C. The same GIFs sent from LINE iOS (the reference)

Same files and settings as A and B, sent from LINE iOS. Also check how each one arrives in Beeper iOS and Desktop (today's receive path).

# Platform Phase Action Checks Evidence Result
C1 LINE iOS before Photo quality Standard: in beeper/line - basic, send 10-animated.gif from Files or Photos reference quality; animates in Beeper? shot: LINE iOS; shot: Beeper iOS ✅ autoplays on LINE iOS even as the sender (bridge-sent A2/A3 did not). Log: MEDIA_CONTENT_INFO has width: 480, height: 480 and attribute: {}, no MEDIA_THUMB_INFO, no FILE_NAME, FILE_SIZE = original 56571. ❌ In Beeper iOS it arrives as a still image (first frame) with no GIF badge; plays only on tap
C2 LINE iOS before Photo quality High: send it again does quality change what arrives? shot: LINE iOS; shot: Beeper iOS ✅ Same as C1: Photo quality High does not change GIFs. Log: identical fields and FILE_SIZE 56571 (the original file)
C3 LINE iOS before Send 25-animated-wide.gif reference for A4 shot: LINE iOS ✅ Autoplays and keeps playing. Same width as A4. Log: MEDIA_CONTENT_INFO has width: 960, height: 360, attribute: {}; FILE_SIZE 60992 (the file's real size), no thumbnail info. Sent together with C4–C6: Beeper shows the four as a cropped 2×2 gallery
C4 LINE iOS before Send 26-animated-tall.gif reference for A5 shot: LINE iOS ✅ Autoplays. Width a little over half of the wide one. Log: width: 360, height: 960
C5 LINE iOS before Send 27-animated-square.gif reference for A6 shot: LINE iOS ✅ Autoplays. Log: width: 600, height: 600
C6 LINE iOS before Send 22-animated-transparent.gif reference for A7 shot: LINE iOS; shot: Beeper iOS ✅ Autoplays, transparent. In the Beeper gallery: black background until clicked, then animates fine. Log: width: 480, height: 480
C7 LINE iOS before In beeper/line - bot: Autoplay GIFs on, Auto-download on. Send 10-animated.gif reference for B2 shot: LINE iOS n/a: the bot relay flattens GIFs to one frame and replaces the bridge's metadata with the Messaging API's, so it cannot compare Beeper with LINE (see Observations)
C8 LINE iOS before Autoplay GIFs off. Send it again reference for B3 shot: LINE iOS n/a: the bot relay flattens GIFs to one frame and replaces the bridge's metadata with the Messaging API's, so it cannot compare Beeper with LINE (see Observations)
C9 LINE iOS before Autoplay GIFs on, Auto-download off. Send it again reference for B4 shot: LINE iOS before and after tap n/a: the bot relay flattens GIFs to one frame and replaces the bridge's metadata with the Messaging API's, so it cannot compare Beeper with LINE (see Observations)
C10 LINE iOS before Both off. Send it again reference for B5 shot: LINE iOS n/a: the bot relay flattens GIFs to one frame and replaces the bridge's metadata with the Messaging API's, so it cannot compare Beeper with LINE (see Observations)
C11 Beeper iOS before Send relay off; put the LINE iOS settings back to the recorded values tidy shot: settings Relay turned off. LINE iOS settings left as: Photo quality Standard, Auto-download images on (restore to off at the end), Autoplay GIFs on

D. Sending from Chrome

# Platform Phase Action Checks Evidence Result
D1 Chrome extension capture Open the extension's Settings and look for any photo, GIF or quality option does the extension have media settings at all? shot: extension settings ✅ Settings → Chats has: Photo resolution Original (note: "Photos larger than 20 MB can't be sent in their original resolution"), Animated GIFs "Auto-play animated GIFs in chats" (on), File previews, URL previews. Notifications tab: pop-up, sound, preview, rules, "Turn off mobile notifications when logged in to the Chrome version". Opening Settings froze the extension until the release helper ran (recorder bug)
D2 Chrome extension capture In beeper/line - basic, send 10-animated.gif, then look at it on LINE iOS upload call, content type, animated flag, encrypted path; does it autoplay on LINE iOS without a tap? (decides whether the bridge's missing width/height matter) shot: Chrome message; req: sendMessage + OBS upload Autoplays in Chrome. ❌ Does not autoplay on LINE iOS (needs a tap), the same as the bridge-sent A3–A7. So LINE iOS does not autoplay GIFs sent from another device of the same account; only its own local sends (C1, C3–C6) autoplay. No bridge autoplay bug. The width/height/attribute difference stays a parity point to compare with the capture. The attachment picker allows every file type (the group-photo picker allowed only JPEG/PNG/GIF/BMP)
D3 Chrome extension capture Send 09-static.gif single-frame GIF: still marked animated? req: sendMessage + OBS upload ✅ Shown as a still image. Log: animated: false, 800×800
D4 Chrome extension capture Send 22-animated-transparent.gif does Chrome convert it or keep transparency? req: OBS upload ✅ Transparent. Log: animated: true, 480×480, thumbnail 384×384
D5 Chrome extension capture Try 12-animated.apng and 11-animated.webp greyed out in the picker, converted, or sent as a file? shot: file picker; req: OBS upload Both "unsupported": a popup offered to send as an archive; chose "always send as archive". The second file then went without a prompt. Log: both sent as files (contentType: 14)
D6 Chrome extension capture Look for a GIF or sticker search in the message box does the extension offer GIF search? shot: message box No GIF search in the extension. Pasted a GIF copied from Raycast's GIF extension instead: worked. Log: 8 MB, 480×480, animated: true, sent as an image
D7 Chrome extension capture In beeper/line - bot (relay off), send 10-animated.gif encrypted or plain upload path in a chat with a business account req: sendMessage + OBS upload ✅ Works. Log: plain path, message first then upload to r/talk/m/<id>, MEDIA_CONTENT_INFO only, no thumbnail
D8 Chrome extension capture Send 25-animated-wide.gif does the extension resize or record dimensions? req: OBS upload ✅ Works. Log: 960×360, thumbnail 384×144
D9 Chrome extension capture Send 26-animated-tall.gif same for tall req: OBS upload ✅ Works. Log: 360×960, thumbnail 144×384
D10 Chrome extension capture Before sending anything, open beeper/line - basic and scroll to the GIFs from phase 1: A3/A4–A7 (sent from Beeper) and C1/C3–C6 (sent from LINE iOS) receiver view with no cached file: does LINE's GIF autoplay here and the bridge's not? shot: Chrome chat; req: OBS downloads ✅ All autoplay in Chrome, both the Beeper-sent (A3–A7) and the LINE-sent (C1, C3–C6). Chrome does not need width/height to autoplay. Open question for D2: does a Chrome-sent GIF autoplay on LINE iOS without a tap?

E. Size limit and other edge cases

Send each from Chrome in beeper/line - basic, smallest first. Stop the ladder at the first rejection.

# Platform Phase Action Checks Evidence Result
E1 Chrome extension capture Send 28-animated-5mb.gif accepted? resized? shot: Chrome; req: OBS upload ✅ Works (animated image, 4.7 MB)
E2 Chrome extension capture Send 29-animated-10mb.gif accepted? shot: Chrome; req: OBS upload ✅ Works (animated image, 10.2 MB)
E3 Chrome extension capture Send 30-animated-20mb.gif accepted? shot: Chrome; req: OBS upload ✅ Works (animated image, 19.7 MB)
E4 Chrome extension capture Send 24-animated-27mb.gif rejected in the UI, by the server, or resized? copy the error shot: error or message; req: OBS upload Sent as a file with no prompt, probably because of the "always send as archive" choice in D5. Log: contentType: 14, FILE_SIZE equal to the file (not zipped). Whether a GIF over 20 MB becomes a file without that choice is still open
E5 Chrome extension capture Send 21-animated-large.gif many frames still animate? shot: Chrome ✅ Fine. Log: 1080×1080
E6 Chrome extension capture Send 23-animated-tiny.gif tiny image upscaled or shown small? shot: Chrome ✅ Fine. On LINE iOS it shows at the same width as the others (scaled up for display). Log: 32×32, thumbnail 32×32
E7 Chrome extension capture Pick 10-animated.gif, then cancel before sending nothing is sent shot: picker n/a: not done
E8 Chrome extension capture Send 13-h264.mp4 sent as video: the shape a GIF must not take req: OBS upload ✅ Fine. Log: video, contentType: 2, DURATION: 3000

F. GIFs from the phone during the capture

Keep the extension open on beeper/line - basic. This records the events the bridge receives.

# Platform Phase Action Checks Evidence Result
F1 LINE iOS capture Photo quality Standard: send 10-animated.gif event fields, size req: operation + OBS download ✅ Done. Log: width/height 480×480, attribute: {}, 56571 bytes
F2 LINE iOS capture Photo quality High: send it again do size or fields change? req: operation + OBS download ✅ OK. Same as F1: quality does not change GIFs
F3 LINE iOS capture Send a GIF from LINE's own GIF search, if it exists different content type or source? req: operation LINE iOS has no GIF search. Pasted a GIF from Raycast's iOS keyboard instead: worked. Log: 3.1 MB, 480×400, animated: true
F4 LINE iOS capture Unsend F1–F3 unsend events for media req: operation n/a: forgot to unsend; unsend events for media not captured

G. Beeper after deploy: sending

Repeat A on the new build. Each row must match LINE's own behaviour from C, D and E.

# Platform Phase Action Checks Evidence Result
G1 Beeper iOS after Same as A1 (GIF picker) animates, quality matches C1 shot: Beeper iOS; shot: LINE iOS ✅ Log: width: 124, height: 126, original size 1904017 (no +32), thumbnail 124×126
G2 Beeper iOS after Same as A3 (10-animated.gif) matches C1 shot: LINE iOS ✅ Log: 480×480, size 56571, thumbnail 384×384 (matches Chrome D2)
G3 Beeper iOS after Same as A4 (wide) matches C3 shot: LINE iOS ✅ Log: 960×360, thumbnail 384×144 (matches D8)
G4 Beeper iOS after Same as A5 (tall) matches C4 shot: LINE iOS ✅ Log: 360×960, thumbnail 144×384 (matches D9)
G5 Beeper iOS after Same as A6 (square) matches C5 shot: LINE iOS ✅ Log: 600×600, thumbnail 384×384
G6 Beeper iOS after Same as A7 (transparent) matches C6 and D4 shot: LINE iOS ✅ Looks fine; thumbnail now drawn on white
G7 Beeper Desktop after Look for a GIF picker now offered? shot: Beeper Desktop message box ✅ GIF picker now enabled in Beeper Desktop
G8 Beeper Desktop after Send a GIF from the picker, if offered arrives as a GIF, not a video shot: LINE iOS; log: conversion ✅ Picker GIF converted and sent as a GIF image. Log: 480×368, 2.7 MB
G9 Beeper Desktop after Same as A9 (file) animates on LINE shot: LINE iOS ✅ Sends now (A9 "This chat doesn't support sending files" is fixed)
G10 Beeper iOS after Same as A10 (beeper/line - bot) upload path matches D7 shot: LINE iOS Not confirmed: no Beeper send to the bot chat in the log. The bot-chat GIFs came from LINE iOS and showed the bridge's Image unavailable notice in Beeper: the bridge downloaded the plain media at once and got LINE OBS object not found (the sender uploads after the message). Receive bug in plain chats, not caused by this change
G11 Beeper iOS after Send 09-static.gif matches D3 shot: LINE iOS ✅ Log: animated: false, 800×800 (matches D3)
G12 Beeper iOS after Send 29-animated-10mb.gif, then 24-animated-27mb.gif same limit and message as E2 and E4 shot: Beeper iOS error or message ✅ 10 MB sent. 27 MB: Beeper Desktop refused ("file too large"); Beeper iOS shrank it itself to 16.7 MB, 640×640 before sending, so it went through under the limit
G13 LINE iOS after In beeper/line - basic, send 10-animated.gif from LINE iOS arrives in Beeper iOS as a GIF (GIF badge, animates), not a still image.jpg (was C1) shot: Beeper iOS ✅ basic chat fine. In the bot chat: same as G10 (plain receive bug)
G14 Beeper Desktop after Look at the G13 GIF in Beeper Desktop animates in Desktop too shot: Beeper Desktop
G15 Beeper iOS after In beeper/line - bot, attach 10-animated.gif again (fix: plain upload with cat=original) LINE iOS loads and plays it, no "no longer available" shot: LINE iOS ✅ Loads and plays on LINE iOS. Log: plain upload of the original (56571 bytes), no thumbnail, no 400
G16 Beeper Desktop after In beeper/line - bot, send a GIF from the picker same as G15 shot: LINE iOS ✅ Log: plain, 4.2 MB, 498×370
G17 Beeper iOS after In a 1:1 chat with a person (DM), attach 10-animated.gif DM (1:1 encryption) path: plays after a tap, right size shot: LINE iOS ✅ This DM partner has no E2EE keys (inactive account), so the DM used the plain path. Log: plain upload, 480×480
G18 Beeper iOS after In the DM, send a GIF from the picker, then 22-animated-transparent.gif both arrive as GIFs; transparent shows white while stopped shot: LINE iOS ✅ Picker GIF (124×126) and transparent GIF (480×480) sent on the plain DM path
G19 Beeper Desktop after In the DM, send a GIF from the picker Desktop picker works in DMs too shot: LINE iOS ✅ Desktop picker GIF in the DM (480×368, 2.7 MB)
G20 LINE iOS after In the DM, send 10-animated.gif from LINE iOS arrives in Beeper iOS and Desktop as an animated GIF shot: Beeper iOS; shot: Beeper Desktop ✅ With a 1:1 encrypted DM: arrived in Beeper. On the plain DM: failed once (LINE OBS object not found after 101 ms → "Image unavailable"), succeeded once (downloaded in 144 ms) — a timing race in plain receives
G21 Beeper iOS after In a 1:1 chat with the bot (if it has one), attach 10-animated.gif plain DM path: loads on LINE iOS shot: LINE iOS ✅ Plain DM with the bot: sent and loads

H. Beeper after deploy: LINE settings, through the relay

Repeat B on the new build. Each row must match its C row (LINE's own reference).

# Platform Phase Action Checks Evidence Result
H1 Beeper iOS after In beeper/line - bot, send relay on bot confirms relay is on shot: Beeper iOS n/a: the bot relay cannot test this (it flattens GIFs and replaces the bridge's metadata, and the bridge still drops bot images, a separate issue). G8 covers "a picker GIF arrives as a GIF, not a video"
H2 Beeper iOS after Autoplay GIFs on, Auto-download on. Send 10-animated.gif matches C7; relayed copy animates in Beeper shot: LINE iOS; shot: Beeper iOS n/a: the bot relay cannot test this (it flattens GIFs and replaces the bridge's metadata, and the bridge still drops bot images, a separate issue). G8 covers "a picker GIF arrives as a GIF, not a video"
H3 Beeper iOS after Autoplay GIFs off. Send it again matches C8 shot: LINE iOS n/a: the bot relay cannot test this (it flattens GIFs and replaces the bridge's metadata, and the bridge still drops bot images, a separate issue). G8 covers "a picker GIF arrives as a GIF, not a video"
H4 Beeper iOS after Autoplay GIFs on, Auto-download off. Send it again matches C9 shot: LINE iOS before and after tap n/a: the bot relay cannot test this (it flattens GIFs and replaces the bridge's metadata, and the bridge still drops bot images, a separate issue). G8 covers "a picker GIF arrives as a GIF, not a video"
H5 Beeper iOS after Both off. Send it again matches C10 shot: LINE iOS n/a: the bot relay cannot test this (it flattens GIFs and replaces the bridge's metadata, and the bridge still drops bot images, a separate issue). G8 covers "a picker GIF arrives as a GIF, not a video"
H6 Beeper iOS after Autoplay GIFs off, Autoplay videos on. Send a GIF from the GIF picker stays still: it is a GIF, not a video shot: LINE iOS n/a: the bot relay cannot test this (it flattens GIFs and replaces the bridge's metadata, and the bridge still drops bot images, a separate issue). G8 covers "a picker GIF arrives as a GIF, not a video"
H7 Beeper iOS after Send relay off; put the LINE iOS settings back to the recorded values tidy shot: settings n/a: the bot relay cannot test this (it flattens GIFs and replaces the bridge's metadata, and the bridge still drops bot images, a separate issue). G8 covers "a picker GIF arrives as a GIF, not a video"

Observations during the run

  • 2026-10-25, after A3 and C1, LINE iOS with Auto-download images off and Autoplay GIFs on:
    • The bridge-sent 10-animated.gif (A3, 57 KB) first showed a play button. After one tap it animated, and it kept autoplaying even after force-closing and reopening LINE. Likely cause: with Auto-download off, LINE does not download media that arrived from another device until tapped. Once downloaded and cached, the GIF autoplays.
    • The LINE iOS-sent 10-animated.gif (C1) autoplayed at once. It was sent from this phone, so the file was already local.
    • The bridge-sent picker GIFs (A1, A2, 1.9 MB) go back to a stopped state even after being played. The cause is still open: a size limit for autoplay, or caching.
    • So C1 vs A3 does not prove that the bridge's metadata is wrong. Auto-download confounds it. The metadata differences stay a lead to test: LINE puts width/height and attribute: {} in MEDIA_CONTENT_INFO, and the bridge does not.
  • A3b / A3c results (Auto-download images on), both sent from Beeper iOS: 10-animated.gif (57 KB) and 28-animated-5mb.gif (4.7 MB). Neither autoplayed: both needed a tap on the play button, and after that both kept animating, also after leaving and reopening. So:
    • Auto-download is not the cause. Bridge-sent GIFs need a tap even with it on, while LINE's own GIF (C1) autoplayed. The metadata difference (no width/height/attribute in MEDIA_CONTENT_INFO) is the main lead again. C1 was sent from the same phone, so the bot relay (B vs C) is the fair receiver-side comparison.
    • Size is not the cause of GIFs falling back to stopped: the 4.7 MB file keeps animating. Only the GIF picker GIFs (A1, A2: 1.9 MB, 124×126 pixels) go back to stopped after playing. It is picker-specific, maybe their tiny pixel size or how the picker encodes them.
  • Correction after D2 (2026-10-25): a GIF sent from the Chrome extension also needs a tap on LINE iOS. The autoplay difference below is LINE iOS's local-file behaviour, not a bridge bug. Keep the metadata notes as parity points only.
  • A4–A7 vs C3–C6 (Auto-download on), the pattern holds for every GIF:
    • LINE iOS always puts width, height and attribute: {} in MEDIA_CONTENT_INFO, sends no thumbnail info, and reports the file's real size. Its GIFs autoplay.
    • The bridge never sends width/height/attribute, adds MEDIA_THUMB_INFO, and reports the encrypted size (the file plus 32 bytes). Its GIFs need a tap.
    • The bridge's thumbnail drops transparency, so a stopped transparent GIF shows a black background.
  • B1/B2 (relay on, 10-animated.gif from Beeper iOS in beeper/line - bot):
    • The bot chat uses the plain (not end-to-end encrypted) media path: Server indicates plain media flow for this chat. This answers D7.
    • Bridge bug, sending on the plain path: the GIF uploaded (56571 bytes), but the thumbnail upload failed: WRN Failed to upload plain media thumbnail … OBS upload failed (400).
    • Relay limit: the relayed copy on LINE iOS shows only one frame. The bot re-sends through the Messaging API, which flattened it. Relayed copies cannot show GIF animation, and they carry the bot's metadata, not the bridge's. So B and C cannot compare bridge vs LINE on the receiving side.
    • Bridge bug, receiving images from business accounts: the relayed image never appeared in Beeper. WRN Failed to download image from OBS error="public OBS resource must be an absolute path", then Remote message handling was cancelled and the message was dropped. Images from official accounts and bots are stored as public resources, and the bridge rejects their path. This probably affects every image a bot sends, not only GIFs. Out of scope for the GIF PR: needs its own issue.
  • Fair receiver-side checks instead of the relay: ask the other member of beeper/line - basic whether A3 (from Beeper) and C1 (from LINE) autoplay on their phone. During the capture, open beeper/line - basic in the Chrome extension, where both are received messages with no cached file.
  • Check before deciding (A3b, A3c), as planned: with Auto-download images on, send 10-animated.gif again from Beeper iOS (A3b), and send 28-animated-5mb.gif from Beeper iOS and from LINE iOS (A3c). If A3b autoplays without a tap, the 57 KB case is only about auto-download. A3c tests the size rule.

Evidence note: phase 2 (D, E, F) has the full network capture but no Chrome extension screenshots. Nothing looked unusual in Chrome during the run. Phases 1 and 3 have LINE iOS and Beeper screenshots.

  • Plain receive is a race (G10, G13, G20): in chats without E2EE, LINE delivers the message before the sender's upload finishes. The bridge downloads at once and gives up on "not found" (failed after 30–101 ms, succeeded once at 144 ms). Fix: retry plain downloads on "not found" for a few seconds. Not a regression: on unchanged main (c5a432d) the same receive failed 4 of 6 times (LINE OBS object not found 100–200 ms after the message), vs 3 of 4 on the GIF branch. The download code is not changed by this PR. Filed as a separate issue.

Findings

the Findings section below (written by the agent in the analyze phase, after line-analyze parses the capture). Capture: 2026-10-25, 349 requests, 76 SSE events.

Findings

Findings: sending GIFs (#90)

Capture: 2026-10-25, LINE Chrome Extension 3.7.2, line-capture from beeper-line-skills 0.3.0
(349 requests, 76 SSE events). Steps and results: the Test plan section above. All IDs, OIDs, keys and tokens are left out.

What the Chrome extension sends for a GIF

End-to-end encrypted chat (beeper/line - basic)

Two uploads to OBS r/talk/emi/, then sendMessage with contentType: 1 (image):

  1. the encrypted file (reqid-…): plaintext size + 32 bytes
  2. an encrypted thumbnail (preview path)
"contentMetadata": {
  "MEDIA_CONTENT_INFO": "{\"category\":\"original\",\"fileSize\":56571,\"extension\":\"gif\",\"animated\":true,\"width\":480,\"height\":480}",
  "MEDIA_THUMB_INFO": "{\"width\":384,\"height\":384}",
  "SID": "emi",
  "FILE_SIZE": "56571",
  "e2eeVersion": "2"
}
  • width/height: the GIF's real pixel size (D2 480×480, D8 960×360, D9 360×960, E6 32×32).
  • fileSize/FILE_SIZE: the plaintext size of the file, not the encrypted upload.
  • animated: false for a single-frame GIF (D3), true otherwise.
  • Thumbnail: longest side 384, aspect kept (D8 384×144, D9 144×384), never upscaled (E6 32×32).
  • No FILE_NAME, no contentType key in the metadata.
Plain chat (beeper/line - bot, a chat with a business account; D7)

sendMessage first, then the GIF is uploaded to r/talk/m/<messageId>. No thumbnail upload.

"contentMetadata": {
  "MEDIA_CONTENT_INFO": "{\"category\":\"original\",\"fileSize\":56571,\"extension\":\"gif\",\"animated\":true,\"width\":480,\"height\":480}"
}
What is not sent as an image
Input Sent as
12-animated.apng (D5) the extension called it unsupported and offered to send it as an archive (user chose "always"); sent as a file, contentType: 14, SID: emf, FILE_SIZE 130 bytes larger than the file (archive wrapper)
11-animated.webp (D5) sent as a file without a prompt (after "always"), contentType: 14, FILE_SIZE equal to the file
24-animated-27mb.gif (E4) sent as a file without a prompt, contentType: 14, FILE_SIZE equal to the file. Possibly caused by the "always send as archive" choice in D5; whether a GIF over 20 MB becomes a file by default is still open (Settings: "Photos larger than 20 MB can't be sent in their original resolution")
13-h264.mp4 (E8) video, contentType: 2, DURATION: 3000

GIFs of 4.7, 10.2 and 19.7 MB (E1–E3) were sent as animated images.

GIFs from LINE iOS (F1–F3)

MEDIA_CONTENT_INFO with width, height, animated, extension, category, fileSize and attribute: {}, plus FILE_SIZE, with no thumbnail info.
Photo quality Standard and High give the same result for a GIF (F1, F2). LINE iOS has no GIF search: F3 pasted a GIF copied with Raycast's iOS keyboard (3.1 MB, 480×400, animated: true), and it was sent as a normal animated image.

Bridge today vs the extension

Field / behaviour Chrome extension Bridge today Change
MEDIA_CONTENT_INFO.width / height real pixel size missing add
fileSize / FILE_SIZE (encrypted path) plaintext size encrypted size (+32 bytes) fix
FILE_NAME not sent sent drop for images
contentType key in metadata not sent sent drop
Thumbnail size (MEDIA_THUMB_INFO) longest side 384, never upscaled up to 480 match 384
Thumbnail transparency (not checked) black background while stopped keep alpha or match Chrome
Plain path: thumbnail none uploaded, fails with 400 don't upload one
Plain path: upload parameters cat: original + file name no cat, timestamp name send cat: original (found in G10)
Plain path: metadata MEDIA_CONTENT_INFO only adds FILE_NAME, FILE_SIZE, MEDIA_THUMB_INFO, contentType match
GIF over 20 MB sent as a file (maybe only after "always send as archive") sent as an image (bridge advertises 100 MB) confirm the default, then match
APNG / WebP "unsupported": offered as an archive, sent as files (to check) send as files
Single-frame GIF animated: false animated from frame count already matches

Autoplay on LINE iOS

A GIF sent from another device of the same account needs a tap on LINE iOS, whether it came from the Chrome extension (D2) or the bridge (A3–A7). Only GIFs sent from the phone itself autoplay there (C1, C3–C6). In the Chrome extension every GIF autoplays (D10). No bridge bug here.

Receiving in Beeper (separate from sending)

  • GIFs from LINE arrive in Beeper as still images with no GIF badge (C1, C6). The incoming MEDIA_CONTENT_INFO has animated: true, so the bridge can mark them as GIFs.
  • Images from business accounts are dropped: public OBS resource must be an absolute path (B2). Separate issue.

Beeper side

  • Beeper iOS can send GIFs (picker and files). The picker sends real GIFs (extension: gif, animated: true). They are small in pixels (124×126) and large in bytes (1.9 MB).
  • Beeper Desktop: no GIF picker in LINE chats (A8), and sending a .gif file fails with This chat doesn't support sending files before the bridge sees it (A9). Check the room's advertised features.

Found during the after phase (2026-10-25)

Plain image uploads need cat=original (fixed)

In plain chats the bridge's GIF reached LINE, but LINE iOS showed only the preview frame and
Unable to load this photo as it's no longer available (G10). The Chrome extension's plain upload (D7):

POST r/talk/m/<messageId>
x-obs-params: {"ver":"2.0","name":"10-animated.gif","type":"image","cat":"original"}

The bridge sent {"ver":"2.0","name":"<timestamp>","type":"image"} with no cat, while the message says
category: original. Fixed by uploading plain images with cat=original and the file name. G15–G21 load on LINE iOS.

Plain receives race the sender's upload (not a regression, separate issue)

In plain chats LINE delivers the message before the sender's upload finishes. The bridge downloads at once from
r/talk/m/<messageId> (tid original), gets LINE OBS object not found 100–200 ms after the message, and posts
the bridge's Image unavailable notice. Right after, LINE sends an op 43 for the same
image with "onObs":true in MEDIA_CONTENT_INFO, which the bridge ignores.

Build Plain receives from LINE iOS Failed
main (c5a432d) 6 4
GIF branch 4 3

The download code is not changed by this PR. Next step: capture the Chrome extension receiving a plain image to see
whether it waits for op 43 / onObs, retries, or both.

Other results on the new build
  • Beeper Desktop's GIF picker is enabled, and .gif files send (A9 fixed by advertising fi.mau.gif).
  • Thumbnails match Chrome exactly: 384×384, 384×144, 144×384.
  • GIFs over 20 MB: Beeper Desktop refuses them ("file too large"). Beeper iOS shrank a 27 MB GIF to 16.7 MB, 640×640 itself before sending.
  • A single-frame GIF is sent with animated: false. The old byte-scan check marked some such GIFs as animated. The new check walks the GIF blocks and matches Chrome on every sample.

Open

  • Neither the extension nor LINE iOS has a GIF search. A GIF pasted from the clipboard (Raycast GIF search, D6: 8 MB, 480×480) is sent as a normal animated image.
  • Whether a GIF over 20 MB becomes a file by default (E4 possibly reflects the D5 "always send as archive" choice). Retest with that choice reset.
  • A second person receiving a bridge-sent GIF: untested.

🤖 Generated with Claude Code using clins:github

@highesttt highesttt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

looks ok, I dont see anything wrong with it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

feat: ✨ send GIFs from beeper

2 participants