Implementing Audio Player in Mobile Applications
When user minimizes the app, audio should keep playing — and lock screen should show control buttons. This is not "extra feature" but expected behavior. Exactly here most implementations break.
Background Playback
iOS. Add UIBackgroundModes: audio to Info.plist. AVAudioSession:
try AVAudioSession.sharedInstance().setCategory(
.playback,
mode: .default,
options: []
)
try AVAudioSession.sharedInstance().setActive(true)
.playback — category for player. Without it, iOS stops playback 3 seconds after background transition. But more important: AVAudioSession must be activated before playback starts, not after.
Android. MediaSessionCompat + MediaBrowserServiceCompat or, for new projects, media3 ExoPlayer with MediaSessionService. Declare service in AndroidManifest.xml with android:foregroundServiceType="mediaPlayback". Without foreground service, Android 8+ kills process.
ExoPlayer vs AVPlayer
ExoPlayer (androidx.media3:media3-exoplayer) — standard for Android. Supports MP3, AAC, FLAC, OGG, WAV, M4A, OPUS out of the box. Control via Player.Listener. Playlist — MediaItem.Builder().setUri(uri).setMediaMetadata(metadata).build().
AVPlayer (iOS) — for single track. For queue — AVQueuePlayer with AVPlayerItem. Add observation via AVPlayerItem.status KVO and AVPlayerItemDidPlayToEndTimeNotification.
Seeking
AVQueuePlayer on iOS: seek(to: CMTime, toleranceBefore: .zero, toleranceAfter: .zero) — precise seek. toleranceBefore/After: .zero slower but precisely hits target frame. For slider with drag, CMTime(seconds: 0.5, ...) suffices — faster.
ExoPlayer: player.seekTo(positionMs). With SEEK_PRECISE strategy — precise but more CPU-intensive.
Track Queue and Metadata
Pass track metadata (title, artist, artwork) to Lock Screen via MPNowPlayingInfoCenter (iOS) or MediaSession.setMetadata() (Android).
MPNowPlayingInfoCenter.default().nowPlayingInfo = [
MPMediaItemPropertyTitle: track.title,
MPMediaItemPropertyArtist: track.artist,
MPMediaItemPropertyPlaybackDuration: track.duration,
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime,
MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: artworkSize) { _ in artworkImage }
]
Without MPMediaItemPropertyArtwork, Lock Screen displays gray rectangle.
Handling Interruptions
Phone call, another app with audio — interruptions happen. iOS: AVAudioSession.interruptionNotification. Android: AudioFocusRequest with OnAudioFocusChangeListener. On AUDIOFOCUS_LOSS_TRANSIENT — pause with auto-resume. On AUDIOFOCUS_LOSS — pause without resume (another app hijacked focus long-term).
Crossfade Between Tracks
Smooth track transition — detail that music app users notice. On iOS: create two AVAudioPlayer (or AVPlayerNode in AVAudioEngine), apply fade out to current and fade in to next via AVAudioEngine.mainMixerNode.volume with AVAudioTime. On Android: ExoPlayer doesn't natively support crossfade — implement via AudioMixer (API 31+) or parallel ExoPlayer with gradual volume decrease/increase via Handler.postDelayed.
Playback Speed
Podcasts and audiobooks often play at 1.25x or 1.5x. iOS: AVPlayer.rate = 1.5. Android/ExoPlayer: player.playbackParameters = PlaybackParameters(1.5f). Above 1.5x, audio without processing sounds unnatural (chipmunk effect) — ExoPlayer automatically applies pitch correction via SonicAudioProcessor. On iOS above 2.0x, explicitly set AVAudioTimePitchAlgorithm.timeDomain on AVPlayerItem.
Flutter: just_audio
just_audio (pub.dev) — most full-featured audio player for Flutter. Supports queue, loops, shuffle, speed, background playback via audio_service. AudioPlayer.createProgressiveAudioSource() for progressive URLs, AudioPlayer.createHlsAudioSource() for HLS streams.
Timeline
Audio player with queue, background playback and media controls — 2–3 days. Custom UI with waveform progress bar and animation — plus 1–2 days.







