Building a video link over LoRa
Chapter 01
Sending a video over LoRa
Fitting video into a radio link built for sensor messages: codecs, packet loss, FEC and latency.
Let's start from the basics. LoRa modulation allows us to send data over the air over long distances at low bitrates, but is generally used for small messages from sensors and other low-power devices.
Given the high achievable range of LoRa, we decided it would be interesting to see if we could squeeze a video link over that channel.
Can you send video over LoRa?
-
“Possible? Yes. Feasible: No.”
Reddit, r/Lora, 2022 ·source
-
“Theoretically, yes. Practically, no.”
Reddit, r/cyberDeck, 2026 ·source
-
“What for? It's anyway too slow for any video, and even for voice.”
Reddit, r/cyberDeck, 2026 ·source
-
“LoRa brings Kbits in a problem that needs Mbits”
ST Community, 2024 ·source
-
“Unfortunately Lora is not designed to transmit so much information.”
Reddit, r/Lora, 2021 · asked about 240p at 15 fps ·source
-
“LoRa = Low Rate, Long Range. Text or GPS and a low repetition rate, yes. Video, no.”
Raspberry Pi Forums, 2021 ·source
-
“Not enough bandwidth”
Meshtastic forum (archived), 2022 ·source
-
“I don't think LoRa in the ISM bands can handle video.”
Permies forum ·source
-
“LoRa is useless for video; it's too slow.”
Reddit, r/esp32, 2021 ·source
-
“So: no, not possible.”
The Things Network forum, 2016 · about sending one picture ·source
-
“Not a chance with LoRa, data rate is very low, probably even lower than Xbee.”
Arduino Forum, 2022 ·source
-
“No, it is not possible because a video is too large to send via the LoRa Chirp Spread-Spectrim modulation technique.”
Reddit, r/Lora, 2022 ·source
-
“You can't stream video over LoRa, no.”
Reddit, r/arduino, 2021 ·source
-
“But LoRa will not be able to transmit the video data.”
Arduino Forum, 2023 ·source
-
“For one thing, there is no way you can send video via LoRa. The bandwidth can't support the data rate.”
Hackaday comments, 2019 ·source
-
“using LoRa to transfer your images is out of the question. It is simply not designed for this purpose.”
Pycom forum · about still images ·source
-
“Look at data rates and google what popular streaming video formats need. The comparison should tell you that it's impossible.”
Reddit, r/cyberDeck, 2026 ·source
-
“None of the standard technologies like BT, WiFi, or LORA will do what you want. As you mentioned, cellular is your only hope.”
Arduino Forum, 2026 ·source
-
“Receiving temperature updates from a distant sensor every few minutes, for example, would be possible with LoRa; but, streaming a video feed would be impossible.”
Hackaday.io project page ·source
-
“Don't try this.”
The Things Network forum, 2016 · about sending one picture ·source
-
“Video surveillance is right out.”
Physics Forums, 2021 ·source
-
“It cannot pass video but telemetry and commands should work well even in degraded environments.”
Reddit, r/UkraineWarVideoReport, 2024 ·source
-
“Is like, is possible to send tcp/ip data over a pigeon ? Yes”
Reddit, r/Lora, 2026 ·source
-
“Video - technically it is possible, but it makes no sense it will take months or ages to transfer :)”
Reddit, r/meshtastic, 2026 ·source
-
“it may take DAYS to send an image that looks like it was taken by a 20 year old cell phone.”
Meshtastic on GitHub, 2025 ·source
-
“Video might not finish before OP shuffles off the mortal coil.”
Raspberry Pi Forums, 2019 ·source
-
“BTW, nothing prevents you from buying a couple of LoRa modems and trying them in the field. It's fun, but please forget about video.”
Reddit, r/cyberDeck, 2026 ·source
-
We tried anyway.
This series is how that went. First, the numbers.
We'll first analyze how to make the video small enough while remaining usable, then how we resolve the issues that come up as we deal with the system requirements.
Let's first get familiar with some numbers we'll keep seeing. We are using LR2021 lora chip which can support up to 15.6kB/s transfer speed.
To compare our improvements over time, we'll use the 30s reference video recorded at 360p at 30FPS.
Our baseline recording
How to send a video?
JPEG codec
There are different ways to compress and send a video file. Let's start with a relatively simple approach: We'll take each frame of the video and send it as JPEG file while staying within our LoRa bitrate limit.
Try sending JPEG frames
- Average JPEG
- —
- Needed at 30 fps
- —
So this can work (and would allow something like esp32 cam modules to transmit "video"), but the quality is not usable.
Still, the codec has some benefits. For example, we don't care if a few images get lost during the transmission, we can just render the ones that did manage to arrive.
h265 codec
We'll focus on h265 codec which is a common baseline against which other codecs are compared and is supported by the hardware we are using on this project.
We won't yet go into too much details on how modern video codecs work. However, as we'll see, they can give a much higher video quality than our JPEG codec. They win by predicting the next frame from the previous one.
Now if we have some programming background, we might be concerned, since we've just described a recursive algorithm without a stopping condition. Fortunately, h265 periodically inserts a so called I frame which we can say is essentially JPEG image of the provided input image. The frames where the output is predicted from the previous frame are called P frames.
I frame starts a fresh chain
So now the first tradeoff: how often to send an I frame? Well if we send it on every image from the camera, then we are back at something like a JPEG codec. And if we only send it once, we'll get great bitrate savings, but as soon as a frame is lost, the receiver can't terminate the recursion leaving you with no image.
We'll take inspiration from OpenIPC and set I frame interval to 1s, then encode 360p video with h265 at 15.6kBps.
360p at 30 fps · one keyframe per second
So we are done! We have a compressed video that now fits the lora bitrate budget and we can send it over lora and receive it on long distances.
Ok, there might be some minor issues still to resolve, but how hard could it possibly be to get this into a nice youtube demo...
The last 20%Spoiler: it takes the other 80% of the work.
Packet loss
If the receiver isn't in the same room as the transmitter, some packets will get lost. Let's see how this affects our video. LQ(link quality) tells us how many frames reach the receiver.
LQ and the reference chain
So despite only losing 10% of the frames, the video is mostly useless since losing a frame prevents the decoding of future P frames until the next I frame.
Now I still want my video stream to work even with 10% of packet loss. So if you did some testing, the issue can mostly be resolved by reducing the I frame interval, then frame drops don't matter much. But this brings us back to our JPEG codec which performs terribly...
Forward error correction
Fortunately there is a simple solution: send each frame twice. Then there is only 1% chance of a frame loss at 90% LQ. Easy win right? Well... Let's take a look.
Send every frame twice
This approach is called 1+1 forward error correction. But while the video is much more stable, the quality seems to have degraded? The I frame interval is still 1s, but if you send each frame twice, your budget drops from 15.6kBps to 7.8kBps. And you pay this penalty even at 100% LQ!
At this point in time there is a decision to make. We can either build our system with a two way link where the receiver tells the car that the frame 20 was lost and it should send it again, or we can use forward error correction and I frames and a few other tricks we'll see later.
It turns out two way link is in a certain sense optimal since you can get great video quality when LQ is high while tolerating even very low LQ levels by resending frames multiple times. If reverse communication is free and instant, there is no reason to use FEC.
However the implementation of a two way link turns out to be quite difficult(simple example: what if the packet requesting the frame again gets lost?) so we decided to work on a better one way link instead.
Surviving 90% LQ
So our 1+1 FEC has two main issues we'd like to fix: it reduces the bitrate by 50% and it still lets a few frames get lost.
Now the theory of forward error correction(and linear codes) is interesting, but we won't go into details. For now let's just say that we can "easily" integrate something like 8+4 FEC which sends 8 frames and then 4 more "repeats" that manage to fix any 4 dropped frames(I'm simplifying here since we haven't yet split the frames into equally sized packets). Let's see how that helps us.
Recover any four missing transmissions
So here, to get a failure, 5 or more out of 12 packets must be lost, at 90% LQ this is very unlikely to occur(Nice binomial distribution exercise) with the probability of 0.4% of the block having enough damage to not be fully fixable.
8+4 here wasn't chosen randomly, it is another OpenIPC default.
Now we still have to pay 33% bitrate cost, but you most likely didn't see any I frame waits. so definitely much better than the 1+1 FEC right?
So why not use 100+20 FEC and save even more bitrate with even lower odds of failure? Well there is one issue we are neglecting: latency.
What if the first packet in the 100+20 FEC block is lost? Well we have to wait for 100 frames to be sent(we need data about all 100 frames to compute the repair blocks) to compute what the missing block was. This will take about 4 seconds. So we might as well just wait for the I frame instead!
And even if we have no failures, we spend 20 frames sending FEC repairs causing a latency spike (that we don't simulate yet).
This is not usable for a low latency system so we'll need to be smarter about how we approach things. Still, 8+4 FEC turns out to be quite a useful baseline to work from allowing us to retain the video link even with some error rate though with modest bitrate cost. We'll take this as our new baseline.
Dealing with the real world
With the packet loss simulations we were operating in a world without latency, where every frame can be put into a single packet and forward error correction is easy and just handles varying frame sizes.
Now we actually have to fit our frame data into lora packets, decide how to do that, which parameters to use to set up the communication, how to deal with packet loss on the level where it actually occurs and how to deal with latency.
Where to even start...
LoRa
So semtech promises us 15.6kBps (but hey, if you are from Semtech by any chance, how do I unlock the 25kBps mode? 1.6MHz BW would be really nice to try :) ). Well that is the theoretical bitrate we can achieve, but it doesn't include LoRa overhead.
Let's see what's actually possible. The main two parameters of each LoRa packet are the spreading factor and the bandwidth. To achieve the 15.6kBps data rate we need to use the lowest spreading factor which is 5 for the LR2021 and the 1000kHz bandwidth. These parameters do reduce our range, they in fact represent the shortest range LoRa mode that LR2021 supports, but they still allow for very long range communication especially on lower frequencies.
The problem with 15.6kBps however, is, that it includes the preamble, packet header and CRC check. We'll go through each part
What takes up a packet slot?
Each lora packet requires a preamble, we get to decide how long it is, but for our setup, the general recommendation is to use a 12 symbol preamble. At SF5 and 1000kHz BW, one symbol takes 32 microseconds. Including the additional 6.25 synchronization symbols, the preamble portion takes about 0.584ms.
Header contains the length of the packet and some lora parameters, it also includes a checksum for itself and uses stronger 4/8 error correction. Its 20 information bits are 2.5B, but at SF5 the header takes 8 symbols, or 0.256ms of airtime.
CRC (cyclic redundancy check) is sent after the data, it can be thought of as a hash of the data and is used to ensure there are no errors in the data stream. It is 2B long.
In a lora packet we can send at most 255B of data. With these settings, including the explicit header and CRC, the packet takes 17.319ms on air. That leaves about 14.72kBps of payload before any spacing, about 5.8% below the nominal 15.625kBps. If we allow 0.7ms to prepare the lora module for the next packet, we get about 14.15kBps.
With this setup we send a packet once every 18ms or 55.5 packets/s.
I'm skipping over a lot of details, but let's say this is enough info for now.
Dealing with the latency
For now we'll ignore packet losses and FEC to keep things simple.
h265 encoder takes in a frame from the camera and gives us a payload that we then have to split into 255B packets (or smaller, but then we lose bitrate) to send over LoRa.
Another simplification we'll make, is to always use 255B packets, this reduces the FEC complexity and simplifies frequency hopping (we'll cover later), if the video payload is too small we'll simply use padding to fill the packet(essentially zero bytes).
Now we can simulate latency under these assumptions.
RF latency without frame merging
- Frame
- —
- Type
- —
- Size
- —
- RF latency
- —
Frame merging
Two things stand out in that simulation.
First, latency jumps once per second. Those are the I frames. They can't borrow anything from the previous frame and the encoder tries to keep quality approximately constant, so at 14kBps an average I frame is about 1600B against about 420B for a P frame.
Second, and worse, latency runs away long before the link should be full. Slowly raise the bitrate and somewhere around 9.5kBps the delay stops recovering, even though our link can carry 14.15kBps. So where did the other third go?
Into padding. Each frame must end with a full 255B packet, so on average we waste about 155B per frame. At 30 frames per second that's about 4.7kBps: add the 9.4kBps of video and the link is full.
The fix is simple: if the next frame is already waiting when we start a packet, put its bytes in the remaining space. We are in a fixed packet schedule so this improves our utilization without any latency cost.
With variable packet sizes we could sometimes save a bit of latency without packet merging, but again at a cost of significant code complexity.
The whole clip, packet by packet
One frame per packet
With frame merging
| Padding per frame | Average RF delay | Frames over 200 ms | |
|---|---|---|---|
| One frame per packet | – | – | – |
| With frame merging | – | – | – |
Move the bitrate up, scroll along the clip and compare the two. The top row keeps wasting space and falls further behind the camera, the bottom one only pads when it has caught up. At 14kBps average RF delay in this simulation drops from about 3.2s to about 95ms.
RF latency with frame merging
- Frame
- —
- Type
- —
- Size
- —
- RF latency
- —
We can now use nearly the whole link, but the once per second spikes are still there, reaching about 500ms at 14kBps. Merging makes better use of the packets, it doesn't make I frames any smaller.
Putting it all together
We've ignored link quality in our packet scheme. So let's bring back our 8+4 FEC, this time on actual LoRa packets. Every 12 packet slots now carry 8 packets of video and 4 repair packets, and each packet can be lost on its own. Any 8 out of the 12 are enough to restore the block.
That 8+4 FEC leaves us with about 9.4kBps for video.
Frame merging with 8+4 FEC on packets
- Frame
- —
- Type
- —
- Size
- —
- RF latency
- —
We can see that we get very frequent latency spikes. They are caused by sending the 4 error correction packets consecutively, this takes 72ms during which the receiver doesn't receive any video frames(assuming no packet loss). This is also nicely visible in the RF latency chart as the sawtooth pattern.
So this is our baseline: 360p at 30 frames per second over LoRa, in a simulation that survives 10% packet loss without a return link.
We are not done with the tuning yet though. The I frames still cause spikes of up to 700ms, the repair bursts add a sawtooth to every frame's latency, a lost block can still freeze the picture until the next I frame, and a third of our link goes to repairs even when nothing is lost. That's what we'll be working on from here.