At first glance, downloading media from X (Twitter) sounds simple.
You have a post URL. You find the media URL. You download the file.
That works — until you realize that a video, an animated GIF, a photo, and a recorded Space are handled in completely different ways.
While working on FromX.app, I had to deal with all of these formats. What surprised me most was how much complexity can sit behind a single input field that just says:
Paste an X link.
Here is what I learned about how X delivers its media.
One post can contain several different kinds of media
A normal X post can contain photos, video, or animated content.
But those media types aren't just different file extensions pointing to the same storage system.
The processing pipeline can look more like this:
X URL
│
├── Video
│ ├── MP4 variant
│ ├── MP4 variant
│ └── HLS representation
│
├── Animated GIF
│ └── MP4 video
│ └── optional conversion back to .gif
│
├── Photos
│ ├── resized timeline version
│ └── largest available version
│
└── Space
└── HLS audio stream
├── segment
├── segment
├── segment
└── ...
↓
M4A
So even if the user experience is always "paste a URL and download", the work happening behind that button can be very different.
Let's look at each type.
1. Videos: one video can have multiple representations
When you watch a video on X, you aren't necessarily looking at one universal video file.
The same video can be available in several representations with different resolutions and bitrates.
For example, you might find versions roughly corresponding to:
320x568
480x852
720x1280
1080x1920
The exact set depends on the source video.
This leads to an important rule for a downloader:
Don't invent quality that doesn't exist.
If the largest version X provides is 720p, displaying a shiny "1080p download" button doesn't magically create Full HD.
You could upscale the video, of course, but that would only create a larger file. It wouldn't restore detail that wasn't present in the source.
For FromX.app, I decided to show the actual qualities available for the post instead.
If X provides 1080p, 720p, and 360p, those are the options.
If it only provides 720p and lower, that's what the user sees.
Direct files vs streaming
Another complication is that video delivery isn't limited to simple direct file URLs.
Depending on the media, you can encounter both directly downloadable variants and HLS playlists.
HLS uses an .m3u8 manifest that describes media streams rather than pointing to one traditional file.
Conceptually:
master.m3u8
│
├── low quality stream
├── medium quality stream
└── high quality stream
This is why media extraction is more interesting than simply searching the HTML for something ending in .mp4.
2. Twitter GIFs aren't really GIFs
This was probably the most amusing part.
Someone uploads an animated GIF to Twitter.
You open the post.
It looks like a GIF.
It loops like a GIF.
But what you're actually watching is typically a video.
X converts animated GIF content into a short looping MP4.
There are good reasons for this.
GIF is an extremely old format. Among other limitations, it uses a limited color palette and is very inefficient for storing longer animations.
A short animation might look like this:
animation.mp4 → 2 MB
animation.gif → 15 MB
The exact difference varies enormously, but the general pattern is the same: video compression is dramatically more efficient.
So when somebody asks:
"Why does my Twitter GIF download as an MP4?"
The answer is:
Because the MP4 is effectively the media X is serving for that animation.
Converting it back into a real GIF
Sometimes users specifically need a .gif file.
Maybe they're uploading it to an old CMS, a forum, presentation software, or another system that expects GIF rather than MP4.
In that case you need another pipeline:
X animated post
↓
MP4
↓
decode frames
↓
generate palette
↓
encode animated GIF
↓
.gif
Palette generation matters more than I initially expected.
A naive MP4 → GIF conversion can produce ugly banding and poor colors because GIF frames are restricted to a small color palette.
Generating a palette based on the actual animation generally gives much better results.
There is still a trade-off:
the real GIF will usually be much larger than the MP4.
That's not a bug in the downloader. It's simply one of the disadvantages of the GIF format.
So FromX gives both options: keep the smaller original MP4 or convert it into a real animated GIF when you actually need one.
3. Photos: the image in your timeline isn't necessarily the largest one
Photos seem much easier than videos.
And, comparatively, they are.
But there's still one detail worth knowing.
The image displayed inside the timeline doesn't need to be the largest version available.
Social platforms generate resized versions because sending a huge image to everyone scrolling through a feed would waste bandwidth.
Conceptually:
Uploaded image
│
├── small preview
├── medium version
├── large version
└── largest available version
For a downloader, the goal shouldn't be:
Download the exact image currently displayed in the browser.
It should be:
Find the largest version X makes available.
That's the version FromX requests.
There is also an important distinction here.
When I say "original", I mean the largest version stored by X, not necessarily the untouched byte-for-byte file that existed on the uploader's computer before it was posted.
X may already have processed the image.
A downloader cannot recover information that the platform itself discarded.
This is another case where it's better not to promise something technically impossible.
Multiple photos introduce another small problem
A post can contain several photos.
Downloading one file is trivial.
Downloading four photos isn't difficult either, but asking the user to click four separate buttons isn't a great experience.
So there are really two useful outputs:
Post
├── photo_1.jpg
├── photo_2.jpg
├── photo_3.jpg
└── photo_4.jpg
or:
post_photos.zip
├── photo_1.jpg
├── photo_2.jpg
├── photo_3.jpg
└── photo_4.jpg
The ZIP itself doesn't need to modify the images.
It can simply package the existing files together.
It's a tiny feature technically, but it makes multi-photo posts much more convenient to save.
4. Spaces are completely different
Then there are X Spaces.
A recorded Space looks like one long audio file from the user's point of view.
Technically, that's not necessarily how it's delivered.
Recorded Spaces are served through streaming infrastructure using HLS.
Instead of:
space.m4a
you may be dealing with something closer to:
playlist.m3u8
segment0001
segment0002
segment0003
segment0004
...
segment2500
A long Space can contain thousands of small audio fragments.
To produce something useful for offline listening, those fragments need to be retrieved in the correct order and combined.
The simplified process looks like this:
Space URL
↓
recording metadata
↓
HLS playlist
↓
audio segments
↓
assemble in order
↓
M4A file
This is fundamentally different from downloading a photo or grabbing a direct MP4 URL.
Why M4A?
The Space audio is based on AAC.
Instead of decoding that audio and encoding it again into MP3, you can preserve the existing audio stream and package it inside an M4A container.
That has a major advantage:
no unnecessary re-encoding.
Every lossy re-encode has the potential to reduce quality.
If the source is already AAC and your goal is simply to make it easy to save and play, keeping that audio makes sense.
The result works well for long recordings too.
A two-hour discussion doesn't need to become an enormous video file when the user only wants the audio.
Avoid conversion whenever possible
This became one of the main principles I followed while building the service.
If the platform already provides a useful file, don't convert it just because you can.
For example:
X MP4 → Download MP4
is better than:
X MP4
↓
decode
↓
re-encode MP4
↓
download
The second approach:
- consumes CPU;
- takes longer;
- can reduce quality;
- creates temporary files;
- increases server bandwidth and storage requirements;
- gives the user almost nothing in return.
The same applies to Spaces.
If you can preserve the original AAC audio, there is little reason to encode the entire recording again unless the user specifically asks for another format.
Conversion should solve a real compatibility problem.
It shouldn't be the default.
A simple UI can hide very different backend jobs
This was probably my biggest takeaway.
From the user's perspective, the workflow for every format is identical:
1. Copy URL
2. Paste URL
3. Download
But internally the jobs can be completely different.
A photo might only require locating the correct image URL.
A video requires discovering available representations.
A GIF requires recognizing that the "GIF" is really a video and optionally running a conversion pipeline.
A recorded Space requires processing a streaming playlist containing potentially thousands of segments.
Yet all of them begin with the same thing:
https://x.com/username/status/...
or:
https://x.com/i/spaces/...
That's one of the things I enjoy about building small tools: sometimes the simplest interfaces sit on top of the most interesting technical problems.
What I ended up building
These experiments eventually became FromX.app.
It handles public X media through one input and currently supports:
- videos in the qualities X makes available;
- animated posts as their MP4 or converted GIF;
- photos in the largest available size;
- multiple photos as a ZIP;
- audio extracted from videos;
- recorded Spaces as M4A;
- profile pictures;
- profile headers.
There is no account required.
The more interesting part for me, though, wasn't building another download button.
It was learning that "download media from Twitter" isn't really one problem.
It's several unrelated media pipelines hidden behind one URL.
Final thought
Modern websites increasingly hide storage and delivery details behind players, dynamic APIs, manifests, CDNs, and streaming protocols.
What looks like:
[ Download ]
might mean:
find metadata
→ identify media type
→ discover variants
→ parse a playlist
→ fetch hundreds of fragments
→ remux streams
→ optionally convert formats
→ send the result
And sometimes the animated GIF isn't even a GIF.
That part is still my favorite.

Top comments (0)