We Built a Cricket Speed Gun From a Phone Camera and Got the Physics Wrong
Every cricketer has asked the same question after a good spell. How fast was that?
The honest answer has always been a guess. A coach with a stopwatch, a teammate saying "that looked quick", or nothing at all. A Pocket Radar costs around $400. A Stalker, the kind professional scouts carry, runs up to $1,200. For most of the 2.5 billion people who follow this sport, that is not a purchase. It is a fantasy.
Baseball has a dozen consumer tools for measuring pitch speed. Cricket has effectively none.
So we are building one. My name is Abdulbasit. I am 19, and I am building Paceball alongside Mustafa Asim for RevenueCat Shipaton 2026. Neither of us has shipped a mobile app before. We are both web developers. This is day 14 of 33.
The following is an account of what the first two weeks actually looked like, including the part where we found out we had been wrong.
The idea is simple, which is the trap
Film a delivery side-on. Mark the frame where the ball leaves the hand and the frame where it pitches. Count the frames between them, divide by the frame rate, and you have time. The pitch is 20.12 metres, fixed by the laws of cricket at every earth ground, so you have distance. Distance over time is speed.
No radar. No hardware. Nothing that leaves the phone.
That is the pitch, and it is roughly what we told people in week one. It is also wrong in a way that took us four days to notice.
More on that shortly. First, the part that had to work before anything else mattered.
Can a phone even record fast enough?
A cricket ball at 140 km/h covers about 39 metres per second. At 30 frames per second, you get one frame every 1.3 metres of travel, which is not enough to measure anything. At 60 you get roughly double that resolution, which is workable.
So the first test was not code. It was nine recordings on a Samsung S25 checking whether the camera holds a locked 60 fps or drifts.
Locked matters. Many phones advertise 60 fps and then quietly drop frames in low light to keep the exposure up. A frame rate that varies with the light is useless for measurement, because every frame you lose is time you cannot account for. The test came back at a hard 60 across all nine recordings.
If that had come back red, there would be no app.
Two findings from that week that cost us time:
The frame rate is a float, and you have to read it per file. Our recordings come back between 59.8 and 60.05. Never exactly 60. We never hardcode it, we read the actual value out of every file. One 633 millisecond test clip reported 55.29 fps because frame count divided by duration amplifies rounding on short samples, which is why the app now enforces a three second minimum recording.
A fast ball does not record as a ball. It records as a smear. At default exposure, the camera picks a slower shutter; the ball travels during the time the shutter is open, and you get a streak instead of an object. If you cannot see where the ball is, you cannot measure how fast it is moving. Dropping exposure to minus 4 forces a faster shutter, and the ball comes back as a ball. We also turn stabilisation off, because stabilisation warps frames to smooth the image, and warped frames mean corrupted pixel positions.
The library everyone recommends has been dead since January 2025
To measure anything, we need individual frames out of a video file, addressed by index.
Every tutorial for doing this in React Native points to FFmpegKit. It was the standard. It wrapped FFmpeg for mobile, and everyone used it.
It was retired on 6 January 2025. Binaries were pulled from Maven Central, CocoaPods, and npm. Not deprecated, removed. Builds that depend on it simply fail.
The tutorials are all still online. Still ranking. Still confidently telling you to install a package that no longer exists. npm does not warn you. You find out when the build breaks.
Android has had the right API the whole time. MediaMetadataRetriever.getFramesAtIndex(), available since API 28, native, no external binary. It gives you frames by index rather than by timestamp, which turns out to matter a lot. Timestamp seeking on Android is approximate and varies by codec. Approximate frames mean wrong measurements.
We wrote about a hundred lines of Kotlin in a local Expo module instead of pulling in a 40MB dependency.
The lesson is not that FFmpeg is bad. It is that when a library dies, the internet does not update. The tutorials stay, the SEO stays, and the top result stays wrong. Check the last publish date before you install anything.
Then we found out the whole measurement was wrong
Here is the part I would rather not write.
The plan was to time the ball across the pitch. A pitch is 20.12 metres. Time it, divide, done.
Except the ball does not travel 20.12 metres.
It is released roughly two metres past the bowler's crease, and it pitches six to eight metres short of the far stumps. Actual travel is closer to eleven metres. We would have been dividing the right time into nearly twice the right distance. Every reading, roughly doubled. A 70 km/h delivery reporting as 140.
And it would have looked plausible. That is the part that gets you. A teenager bowling in a street in Rawalpindi sees 140 on the screen and has no way to know it is nonsense. Nobody has a radar gun to check against. That is the entire reason the app exists, and it is also the reason a wrong number would never be caught.
The fix reframes the whole thing. The pitch is not the distance. The pitch is the ruler.
Mark both sets of stumps and the app knows how many pixels equal one metre. Then mark where the ball actually left the hand and where it actually landed, and convert that gap into real metres. Four taps instead of two. Correct instead of confidently wrong.
I would rather find that in a test than in someone's speed reading.
And then the ruler broke too
A few days later we ran a test indoors, throwing a ball across a room at maybe 8 km/h.
The app returned 302.
The maths was correct. Every step of the calculation did exactly what it should. The app had simply been told that the two calibration marks were 20.12 metres apart, because that is what a full pitch is, when in reality they were about four metres apart in a room. Every distance came out five times too large.
No amount of clever code fixes that. Absolute distance cannot be recovered from a single 2D image without a reference of known size. That is geometry, not a limitation of our approach.
So calibration became a user choice. Before marking, you say what you are measuring against: a full pitch at 20.12 metres, two markers you paced out yourself, the diameter of the ball at 7.2 centimetres, or your own height from your profile.
The app caught the bad reading itself. There is a warning that fires when the ball's travel distance approaches the calibration distance, because that combination is almost always a marking error. In the 302 case it read: the ball reads as travelling 27.0 metres before bouncing, which is the length of the whole pitch. Check your marks.
Most apps would have shown 302 and said nothing.
What weโre leaving out
We could display spin rate. It would look impressive on the result screen, and almost nobody would check it.
At 1280 RPM, a cricket ball turns about 21 times per second. At 60 fps, that is roughly three frames per rotation, and the direction of the rotation aliases in