Audiobooks for Android
I built a Kotlin and Jetpack Compose audiobook player that turns authorized YouTube recordings into a local listening library. It preserves progress and playback speed per book, continues through a Media3 background service, and supports sharing, skip controls, and sleep timers.
Technologies used
- Kotlin
- Jetpack Compose
- Media3 ExoPlayer
- Room
- WorkManager
- yt-dlp
- FFmpeg
- Coil
Why I built my own audiobook app
I live far from my university, so I spend a lot of time in the car and often use that commute to listen to audiobooks. I couldn't find a free audiobook app that matched the way I wanted to listen, so I decided to build my own.
I wanted each downloaded book to remain useful across repeated drives, with progress that resumes where I left off, background playback, offline access, per-book speed, skip controls, and a sleep timer. I chose native Kotlin and Jetpack Compose because I wanted the playback and background-service behavior to fit Android directly.
I kept the workflow to two main screens: the library handles downloads and progress, while the player provides the controls I need to resume a book without rebuilding the listening session.
Moving a shared link into a local library
I put downloads behind a WorkManager CoroutineWorker so they can continue after the user leaves the app. Inside that worker, I use the bundled yt-dlp runtime to read video metadata and extract the best available audio, then FFmpeg converts it to M4A before I save it in app-private external storage. Foreground execution also gives Android a visible progress notification during longer downloads.
I route both pasted links and links shared from the YouTube app through that same worker. This keeps download behavior, progress reporting, retry decisions, and database insertion inside one boundary.
Playback that survives leaving the screen
I moved Media3 ExoPlayer into a MediaSessionService instead of tying it to the activity. That lets playback continue with the screen locked or the app in the background, while Android exposes play, pause, and skip actions through the media notification, Bluetooth devices, and headset buttons.
I save the current position to Room every five seconds and again when playback pauses or stops. Opening a book restores that checkpoint and its saved playback speed. When a book finishes, I reset its next session to the beginning instead of resuming from the final seconds.
Keeping controls and state at book level
I model each downloaded book as its own Room record, including its file path, duration, position, speed, completion state, source URL, artwork URL, and recency timestamps. The library can then order books by recent listening activity and calculate progress without requiring the player to be active.
When an existing book is downloaded again, I update its file and metadata while preserving the listener's position, speed, and finished state. I separated file replacement from listening history because recovering a missing or incomplete file shouldn't erase progress.
I also keep the sleep timer inside the playback service. The Compose player sends session commands to set or query the deadline, so the countdown remains independent from the screen's lifecycle.
Designing for files that can disappear
I treat local audio as fallible state because downloaded files can disappear or become incomplete. Before playback, I check that the stored file still exists and contains data. Missing, malformed, or undecodable files produce an actionable message instead of leaving an unresponsive play button.
I classify common download failures so the app can separate conditions that may succeed on retry from errors that need a different link or user action. I keep yt-dlp behind the download boundary because YouTube can change its player formats, and I refresh the bundled extractor on a throttled schedule to reduce that compatibility gap.
What this project taught me
I had to think about what happens after the screen disappears: whether a download survives, whether playback continues, and whether the app remembers the exact point where I stopped.
The project began with a routine part of my life. My commute gave me a specific problem to solve, and turning that frustration into an app of my own is what makes it meaningful to me.
Tags
Explore other projects
Web
Dros
A local-first React study app that turns AI-generated and imported MCQs into repeatable practice with immediate feedback.
University
AI Workflow DSL
A Python DSL and table-driven LL(1) parser for multi-agent workflows, with generated grammar docs and source-aware syntax errors.
Web
Notion Study Reminder
A dependency-free Node.js service that turns a Notion study schedule into Beirut-time email reminders.