← Back to Projects
Mobile

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.

Android audiobook player showing cover art, saved progress, skip controls, playback speed, and a sleep timer
The player restores each book's position and speed, with ten-second rewind, thirty-second forward, and sleep-timer controls.

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.

Android audiobook library showing a foreground download being prepared after receiving a shared YouTube link
A shared link enters the same foreground WorkManager download flow as a URL pasted into the library.
Android audiobook library with a YouTube link field and a downloaded Alice in Wonderland book showing listening progress
The library keeps downloaded books, cover art, listening progress, and remaining time in one local view.

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.

Playback speed dialog with choices from 0.75 to 3 times speed and 1.25 times selected
Playback speed is stored per book and can be changed from 0.75x to 3x.

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.

Sleep timer dialog with options from five to sixty minutes
The playback service owns the sleep timer, so it continues to count down while the app is in the background.

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

AndroidOffline audioBackground playbackMediaPersistence

Explore other projects

Dros practice session showing an incorrect answer, the correct option, and an explanation

Web

React TypeScript Vite

Dros

A local-first React study app that turns AI-generated and imported MCQs into repeatable practice with immediate feedback.

AI Workflow DSL overview showing a workflow program beside the lexer, generated LL(1) table, and stack parser

University

Python LP PT

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.

Notion Study Reminder email open in a desktop inbox, showing overdue tasks, today’s checklist, and upcoming deadlines

Web

Node.js Notion API Gmail API

Notion Study Reminder

A dependency-free Node.js service that turns a Notion study schedule into Beirut-time email reminders.