Amethyst Manual - Chapter 3: Media
- The whole thing in one breath
- Your photos, at the size you choose
- The upload quality slider
- Privacy metadata, hashing and captions
- Ultra HDR photos
- HLS Upload: adaptive HD video
- My Blossom Files: your stored media
- Anonymising your voice
- Audio posts as cards
- Now Playing: your music as a status
- Configuring the video player buttons
- Video credits and divine.video streams
- Casting, briefly
The whole thing in one breath
The default upload is a quality-60 JPEG at half the width and half the height; the slider that changes it starts at Medium in every composer, so Uncompressed is a choice you make per photo. “High” is the video stop that keeps 1080p at 1080p. The GIF and H.265 switches apply in the short-note and long-form composers, and an Ultra HDR photo keeps its gain map through all of it. Adaptive HD video lives in the side drawer under HLS Upload, and that’s the one way in. A Blossom upload tries your picked server, then the rest of the list top down, with one signer prompt; a failed thumbnail’s Error badge tells you why and lets you remove it. Settings → Media Servers → My Blossom Files is where you see what you’ve stored. Anonymize on a voice note is a pitch shift, labelled by the app as reversible — a disguise, not a shield. Audio posts are cards, with PiP from ten minutes up. Now Playing is off until you turn it on and clears itself 30 seconds after the music stops. Captions are on by default, per account. And Save to Phone, Share and PiP are in the player’s overflow waiting to be promoted, with PiP the way to keep video playing once you leave.
Go and slide something to Uncompressed — your camera’s full resolution is one scroll away.
Your photos, at the size you choose
Your phone’s camera shoots four thousand pixels across. Attach that photo to a post, hit send, and by default Amethyst publishes it at two thousand — half the width, half the height, a quarter of the pixels — re-encoded as a quality-60 JPEG.
That default is called “Medium”, and here is exactly what it does: the image is halved, and halved again, for as long as it stays above roughly 640×816, and whatever is left is encoded as JPEG regardless of the source format. A PNG with transparency comes out as a JPEG without it, because JPEG has no transparency. A small image skips the resize and still gets the re-encode. The thumbnail in the composer looks the same either way, because it’s a thumbnail.
Here’s how to keep the full original — and then a tour of everything else the media layer does for you.
The upload quality slider
Attach anything to a post and the attachment card shows a quality slider: Low, Medium, High, Uncompressed. You’ll find it in the short-note composer, the Pictures/Video/Shorts/Longs composer, and the chat file-upload dialog.
For an ordinary photo, “Uncompressed” is the stop that keeps your original resolution and format. Low, Medium and High share the same pipeline: the same power-of-two downscale — a 12-megapixel photo comes out at about 2016×1512 on all three — and the same JPEG encode. What the three steps change is the JPEG quality number: 50, 60, 80. So those three pick how finely to compress an image that has already been reduced to a quarter of its pixels; Uncompressed is the one that skips both the downscale and the re-encode.
The one thing to know: the slider is per-upload. There is no media-quality field in your account settings, and every composer starts at Medium, every time — the single exception is an attachment that isn’t media at all, which starts at Uncompressed. Set Uncompressed today and tomorrow’s photo starts at Medium again. Make it part of your posting routine: scroll down to the slider on the attachment card and set it before you hit send.
Video has its own ladder. Medium brings a 1080p clip to 720p, and Low takes the same clip to 480p. “High” keeps 1080p at 1080p, and it is the stop that leaves a 4K source at 4K — though even High brings 1440p down to 1080p. Compression has a 60-second timeout: a long video that doesn’t finish in time uploads uncompressed, and if the compressed file comes out bigger than the original the app uses the original and shows a toast saying so.
Three things bypass the slider altogether:
- GIF, SVG and AVIF are never compressed. They upload as-is, whatever the slider says.
- Voice notes are always uncompressed. Both the composer and the voice-reply screen force it; the slider never applies.
- “Convert GIF to MP4”, the switch that appears once you’ve attached a GIF, runs even at Uncompressed — its check sits before the uncompressed short-circuit. Turning it on also hides the quality slider.
On the same card, “Use H.265/HEVC Codec” appears when the first attachment is a video and quality isn’t Uncompressed. It’s off by default and gives better compression when on; the thing to weigh is your audience, because a viewer whose phone has no H.265 decoder gets nothing. (Keep that in mind for the casting section — H.265 is also the format TVs tend to struggle with.)
Both switches are worth a closer look across composers. The same attachment card is used by seven composers, and the switches are drawn in all of them, but the short-note and long-form composers are the two that apply them. In comments, classifieds, public messages, the edit-post screen and the new-DM composer the switch value isn’t used: “Convert GIF to MP4” there uploads the GIF as a GIF, and H.265 there produces H.264. The new-DM composer also doesn’t use the card’s quality slider — that upload goes out at Medium wherever you’ve set the thumb.
Privacy metadata, hashing and captions
“Remove private metadata” is on by default, and it is thorough: all GPS fields, camera make/model/software, artist and copyright, camera-owner name, body and lens serial numbers, every DateTime field with its UTC offset — 48 tags in total. Images get an EXIF rewrite, video a remux, MP3s lose their ID3 tags.
The switch is hidden and force-disabled whenever a video is being compressed, because re-encoding drops the metadata anyway. For a format the app can’t strip, the upload pauses mid-flight to ask “Upload anyway?” — cancel and that upload is abandoned.
AVIF is the one format where the privacy check can stop the upload. Amethyst doesn’t rewrite an AVIF container, so it only inspects it: a clean file passes through untouched, and one containing any sensitive tag is refused, with no “upload anyway” option. Convert it to JPEG or PNG if you need it published.
A failed upload wears an Error badge on its thumbnail; since v1.17.0, tapping it opens an Upload Error dialog with the reason — the AVIF refusal, the server’s message, payment required — and Close / Remove. Remove drops that attachment from the post on the spot.
After every upload Amethyst re-downloads the file from the server and re-hashes it. That’s what the “Downloading” and “Hashing” stages are, and it’s why a small image can still take a few seconds. Here’s what that hash represents: it is the hash of the file the server handed back — it isn’t compared with the file you sent — and it is what gets published, so your post describes the file people will actually fetch.
The description field — “Description of the contents” in the short-note composer, “Add a Caption” in the Pictures/Video one, the same alt tag underneath — can fill itself in. On the Play build, when the first attachment is a still image, an on-device describer (Gemini Nano) writes a suggestion and tags it with an “AI-suggested, edit me” chip. The image never leaves your phone. F-Droid ships a stub that always returns nothing, and on Play a device without Gemini Nano returns nothing and shows no error. The describer runs in the short-note-style composers only — not the Pictures/Video one, not the chat dialog.
Ultra HDR photos
A JPEG with a gain map — what recent phone cameras write by default — shows in HDR, brighter than SDR white, on an HDR-capable display running Android 14 or later (since v1.17.0). It’s automatic, with no setting. In the feed the headroom is capped at 2× so the text around the photo doesn’t read grey; the fullscreen viewer lifts the cap. (The cap needs Android 15; on Android 14 the window goes HDR uncapped.) The useful fact for uploaders: the gain map survives Amethyst’s own pipeline — metadata stripping, the Medium downscale and the caches all keep it — so a photo posted at Medium still shows in HDR.
HLS Upload: adaptive HD video
Side drawer → HLS Upload. That’s where it lives.
It transcodes your video on the phone into a ladder of resolutions and publishes a NIP-71 video event plus a plain kind:1 sibling note carrying the poster, dimensions and hashes — so clients that don’t understand NIP-71 still render something rather than a dead link. Viewers get adaptive streaming instead of one enormous MP4.
The one thing to know: HLS is its own entrance, and a normal upload never goes through it. There’s no file-size threshold, no duration threshold, no server-capability probe. Attach a 400MB video to a normal post and it goes out as a 400MB video. For adaptive HD, open the drawer and start from HLS Upload.
Details worth knowing before you get there:
- A title is required. Without one the Publish button stays greyed out, and the same goes for a missing server or an empty rendition list — if the button is grey, check all three. Until you’ve picked a video the button isn’t drawn at all.
- H.265 is on by default here, the opposite of the regular composer’s default. If your device has no H.265 encoder it falls back to H.264 and tells you. Untick it if the video is destined for a TV — see the casting section.
- The rendition checkboxes start all-selected, and anything above your source resolution is shown, disabled and unchecked. No upscaling.
- A poster frame is extracted automatically, because an
.m3u8playlist is just text. If that fails, publishing continues without one.
My Blossom Files: your stored media
Settings → Media Servers → “My Blossom Files”.
This is your inventory of uploaded media — the one place in the app that lists it. (If you find yourself coming back to it, pin it to the bottom bar from Settings → Bottom Navigation Bar.) It’s a gallery of every blob you hold across your Blossom servers, with a zoomable viewer, video thumbnails and a per-file details sheet. Per blob you get Copy URL, Share, Open, Mirror to missing servers, Report, and Delete from one specific host — deletion is per-server, so removing a file everywhere means deleting it from each server that holds it.
Each file carries an “On all servers” / “Not on all servers” badge, and when things are uneven a banner offers Sync all. That sweep runs as a foreground service with its own cancellable notification, so it survives navigating away or backgrounding the app. One sweep runs at a time, and any server that demands payment for the copy is skipped and counted as a failure. Cancelling stops the sweep and clears the banner; the banner’s own dismiss takes effect once the sweep has finished.
One level deeper, overflow (⋮) → Import files scans other Blossom servers for blobs you uploaded from other clients and copies them into yours. Toggle which sources to scan, add a custom URL, press “Check servers”, and import what comes back.
The parent screen matters too. Servers are cards you drag to reorder, under a caption that reads “uploads try each server from the top down”. Here’s how a Blossom upload picks its servers (since v1.17.0): it starts with the one in the attachment card’s picker — whichever you used last, or the top of this list if you’ve never chosen — and if that fails it tries the rest of the list top down until one stores the file. Every server gets the same bytes, so they share one signed upload token: your signer is asked once, not per server, and a signer that refuses, times out or is read-only stops the fallback there. When every server fails, the error shown is the selected server’s. Only Blossom servers fall back; a NIP-96 or NIP-95 upload still has a single target. There is no Save button: every add, remove and reorder publishes a new list event immediately, and there’s no undo, so make each change deliberately.
Two switches under “Upload behaviour” are worth knowing: Mirror uploads is ON by default and runs fire-and-forget in the background, targeting only servers you explicitly configured — never the built-in fallbacks. Optimize media on the server is OFF by default: switching it on routes uploads through the server’s re-encoding endpoint, so the stored file may differ from the one you sent — which also means the published hash describes the server’s version rather than your original bytes. Leave it off unless you specifically want the server to re-encode.
Also on this screen, On-device cache (on by default) serves media from a local Blossom cache when one is running. Since v1.17.0 a cached Blossom image loads straight from disk — offline too — and when the cache has been closed the request falls back to the original URL. One rule holds: media routed through Tor never touches the local cache, because the cache would fetch the origin outside Tor; it helps while Tor is on for other traffic, not for the media itself.
Anonymising your voice
Record a voice note and, under the preview, there’s a section labelled Anonymize with four chips: None, Deep, High, Neutral. The default is None. It appears in the composer and on the dedicated voice-reply screen, only while a recording is pending.
What it does: an on-device pitch shift, with a spinner on the chip while it works and the waveform preview updating to the processed audio. Duration is preserved — this is not a chipmunk speed-up. Deep and High add a ±10% random jitter on every run, so the same preset never produces the same pitch twice; Neutral doesn’t.
It is a pitch shift only — no formant shifting, no speaker anonymisation — and the app says so in its own caption: basic pitch changes “can potentially be reversed by determined listeners”. Use it as a disguise from a casual scroller; if your safety depends on not being recognised, reach for a dedicated tool rather than this feature.
While you’re in audio: Settings → Audio Visualizer offers seven options — Classic Waveform, Spectrum Bars, Color Waves, Radial Ring, Aurora Glow, Static Image, Off. The four live styles are genuine real-time FFT of the decoded track, not canned animation, and no microphone is involved. A voice note that ships its own precomputed waveform always renders that instead.
Audio posts as cards
An audio file in a post gets a card rather than a square video player (since v1.17.0): a waveform card — play button, the waveform as the seek bar, “MP3 · 4.8 MB · 3:28” — or, when the file declares a cover image, a square cover card with artwork, title, artist and a scrubber. The waveform is the file’s own when it ships one, otherwise drawn from its hash. Tap or drag the bars to seek. Audio cards never autoplay and never loop; when nothing declares the type — an HLS playlist, a bare Blossom hash — a neutral Media card holds the place until the player has probed the tracks.
A track of ten minutes or longer gets its own Picture-in-Picture button, at the right end of the waveform card or the top-right of the cover card. The PiP window shows cover, title, artist and the waveform as progress; it never starts muted and doesn’t hold the screen on, so a podcast keeps playing with the screen off.
Now Playing: your music as a status
Settings → Now Playing, directly after Audio Visualizer (since v1.17.0). It publishes the track you’re hearing as a NIP-38 music status — “Title - Artist” under your name in feeds, with a music-note icon — and a “How others see it” preview card shows you exactly that before you switch anything on.
Two switches, two sources. Amethyst covers music tracks, audio tracks and podcast episodes you play in the app — never feed videos, voice messages, live streams or a muted autoplay. Other apps covers Spotify, YouTube Music and any app with media controls; it needs notification access, the only way Android shares another app’s media session, and the screen offers a Grant access button until you have — Amethyst reads what’s playing, never your notifications’ content. An Apps list then shows each app once it has played something, with a switch to stop sharing it. Both switches are off by default, and the choice is stored on this device per account; it doesn’t sync.
The timing is built to be quiet. A track is published only after it has played for 5 seconds, so skipping through a playlist signs nothing; the status expires at the track’s end; and when playback stops it is cleared 30 seconds later, so a short pause or a buffering stall doesn’t clear and re-post it. The same 30-second wait applies when you turn a source off. Statuses go to your write relays and private outbox; a read-only login publishes nothing.
Configuring the video player buttons
Settings → Video Player Buttons lists eight player actions and lets you decide, per action, whether it lives on the top bar or in the overflow (⋮) menu — plus drag to reorder. There’s no on/off; the choice is where.
The defaults: the top bar has Fullscreen, Mute, Video Quality, Cast and, since v1.17.0, Captions; the overflow has Share or Save, Save to Phone, Picture-in-Picture. If you use one of those three often, promoting it to the top bar is a single tap, and the layout syncs across your devices. An older account gets the Captions row appended in that default slot; the saved order of the rest is kept.
Three of them do more than their names suggest:
- Save to Phone files media into an “Amethyst” subfolder —
Pictures/,Movies/,Music/, orDownload/for anything else. The file keeps the name it had in the URL, which for most media servers is a 64-character hash, so it won’t be named after what’s in it. It is hidden, not greyed out, on anything HLS — the test is the format, not liveness, so an on-demand.m3u8upload has no button, exactly as a live stream does. To save a photo, use the viewer rather than the player: tap the image to open it fullscreen and use the download arrow in the viewer’s chrome — a separate control from the Share button beside it, carrying the same “Save to Phone” label, and the route toPictures/Amethyst/. On Android 9 and below it asks for storage permission first; from Android 10 it just saves. - Share on a video downloads the actual file first (you get a “Downloading video…” toast) and then hands the real file to the Android share sheet — so you can send the video itself to WhatsApp or Signal, not just the URL. The menu stays open while that download runs. If you only want the link, there’s a separate “Copy URL to clipboard” row.
- Picture-in-Picture is the opt-in for real background playback. Every other player is released 30 seconds after you pause the app — by design. The PiP controller is explicitly exempt from that timer and stays alive when the screen locks, so audio keeps going. It’s hidden entirely on a device without the PiP system feature.
Captions is the CC button for NIP-71 videos that carry text tracks (divine.video publishes them). One track and it’s a plain on/off toggle; more than one language and it opens a popup listing each plus Off. Captions are on by default, and the switch is per account, not per video: it syncs across your devices like the layout, so turning them off on one video turns them off everywhere.
A button you’ve placed on the top bar still follows its own guard conditions: Quality shows only when the stream has more than one video track, Cast shows only for an http(s) URL, and Captions only when the video carries text tracks — on one without, it is hidden entirely, whatever slot you gave it.
Playback speed — 0.5× to 2.0× in seven steps — is the “1.0x” button at the bottom right of the controls, next to the time readout. It isn’t part of the configurable button set, so it doesn’t appear on that settings screen, and it hides with the rest of the chrome.
Feed looping works like this: feed videos stop looping after five automatic plays. The player pauses itself and shows the play button. Press play, or scroll it back to centre, for a fresh allowance — it’s a battery and decoder guard. Relatedly, mute isn’t a saved preference — unmuting one video sets a session default that carries to the next, and it resets to muted every time you return to the app.
Two things to know about media menus. Media in a feed has no long-press menu of its own, with one exception: a PDF preview card, where a long-press opens the share/copy/save dialog and a plain tap opens the PDF. Everywhere else that dialog — “Media Actions” — comes from an explicit Share button: the fullscreen viewer’s chrome, the player’s overflow, or a voice track. And “Add Media to Gallery” in that dialog publishes the item into your Nostr profile gallery — it isn’t a save to your phone — and it appears only when the media belongs to a post. The dialog’s own Save to Phone row applies to PDFs; for a photo use the download arrow above, for a video the player action.
On audio routing: the media player plays through the normal output, so raising a voice note to your ear doesn’t switch to the earpiece — proximity routing lives in the calling stack. The player’s one piece of routing awareness is spotting a Bluetooth output so it can delay the visualiser to stay in sync with what you hear.
Video credits and divine.video streams
Under a NIP-71 video, below the description, a line of chips names who and what it credits (since v1.17.0): “Inspired by @name”, “Featuring @name”, “Audio from ”, “References ”, or a role the author typed — “Director @name”. Each chip links to the profile or note; a profile that hasn’t loaded prints nothing, so you’ll never see a hex key. A role-credited collaborator who has published an acceptance for that video gets a check-circle (“Credit accepted”), and the acceptance renders as its own card in feeds with the video quoted.
divine.video shorts adapt too: divine hangs its HLS manifest off an hls property on the single imeta, and Amethyst expands that into a variant of its own and prefers it over the progressive MP4, so you get adaptive streaming and the Quality menu. More generally the player now picks which file to load: audio-only tracks and posters are skipped, HLS beats progressive, and the largest declared size wins among HLS entries.
Casting, briefly
Play build only. The F-Droid build has no caster: the player button and the Settings → Video Player Buttons row aren’t rendered — not greyed out, not disabled, absent. F-Droid users configure seven player actions rather than eight.
On Play, Cast sits in the top bar by default. Your phone isn’t mirroring — it hands the TV the video’s link and the TV fetches it directly, which is why it’s light on battery and particular about formats. H.264 with AAC-LC is the happy path; HEVC and HE-AAC tend to stall on a spinner. There are no in-app playback controls yet: use the TV remote or the Android cast notification. The standalone guide, Cast videos to your TV in Amethyst, has the full format matrix.
Two additions since v1.17.0. On Android 17 the first tap on Cast asks for the local network permission — Android 17 gates the Wi-Fi discovery the Cast SDK uses behind it; grant it and the picker opens as before, deny it and a “Permission needed” dialog offers Open settings. Android 16 and below ask nothing. And a receiver that refuses now says so instead of leaving the picker on “Casting”: “
Describes Amethyst for Android v1.17.0. Updated 2026-10-08. This post is republished as each part goes out, so the links below fill in over time. Bookmark it and come back.
Start here: the Amethyst manual
Write a comment