A
AbdulBasit
Guest
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.
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.
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.
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.
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.
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.
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.
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 into nonsense. It cannot be measured from a phone at this frame rate. So we do not show it.
The same goes for whether a bowling action is legal. The ICC threshold is 15 degrees of elbow extension, measured in a laboratory with motion capture. Wrongly telling a fifteen-year-old their action is illegal does real harm.
Three rules came out of the first two weeks, and they are stated to the user inside the app rather than only enforced in code:
Never invent a measurement. If it cannot be derived from what the camera captured, it does not appear.
Publish the uncertainty. Every reading carries an error range, computed from the actual frame delta rather than quoted as a fixed figure. One frame of uncertainty at each end of a 30-frame measurement is about 6 percent.
Show the working. Marked frames, frame delta, frame rate, pixels per metre, travel distance. All of it is inspectable on the result screen.
An instrument that hides its own uncertainty is not an instrument.
The full loop works on a real phone. Record, mark four points, compute, save, export a share card. The first real export read 11.2 km/h with an error range of plus or minus 1, using a three-metre marker calibration.
The app is in closed testing on Google Play. Google requires 12 testers opted in for 14 continuous days before a new developer account can publish anything at all, so the clock that decides whether this ships is not the code. It is fourteen people tapping a link and not un-tapping it for two weeks.
Nineteen days left. The paywall is not built, the animations are not built, and the thing I am most nervous about is not any of the code. It is that we have to film real deliveries on a real pitch this afternoon, and the sample footage that ships inside the app has to be technically correct the first time.
I will write about how that went.
Paceball is being built in public at @paceballpro. The repo is public and MIT-licensed
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 into nonsense. It cannot be measured from a phone at this frame rate. So we do not show it.
The same goes for whether a bowling action is legal. The ICC threshold is 15 degrees of elbow extension, measured in a laboratory with motion capture. Wrongly telling a fifteen-year-old their action is illegal does real harm.
Three rules came out of the first two weeks, and they are stated to the user inside the app rather than only enforced in code:
Never invent a measurement. If it cannot be derived from what the camera captured, it does not appear.
Publish the uncertainty. Every reading carries an error range, computed from the actual frame delta rather than quoted as a fixed figure. One frame of uncertainty at each end of a 30-frame measurement is about 6 percent.
Show the working. Marked frames, frame delta, frame rate, pixels per metre, travel distance. All of it is inspectable on the result screen.
An instrument that hides its own uncertainty is not an instrument.
Where it actually is
The full loop works on a real phone. Record, mark four points, compute, save, export a share card. The first real export read 11.2 km/h with an error range of plus or minus 1, using a three-metre marker calibration.
The app is in closed testing on Google Play. Google requires 12 testers opted in for 14 continuous days before a new developer account can publish anything at all, so the clock that decides whether this ships is not the code. It is fourteen people tapping a link and not un-tapping it for two weeks.
Nineteen days left. The paywall is not built, the animations are not built, and the thing I am most nervous about is not any of the code. It is that we have to film real deliveries on a real pitch this afternoon, and the sample footage that ships inside the app has to be technically correct the first time.
I will write about how that went.
Paceball is being built in public at @paceballpro. The repo is public and MIT-licensed