Implementing Offline Media Playback in Mobile Applications
Download series for airplane — basic user scenario. Implementing it correctly is non-trivial: need storage management, download progress display, pause/resume support, and if content is protected — DRM with offline license.
Download: Approaches and Tools
iOS. AVAssetDownloadURLSession — native API for HLS download. Saves not separate files but HLS stream structure as AVURLAsset to disk:
let configuration = URLSessionConfiguration.background(withIdentifier: "com.app.download")
let downloadSession = AVAssetDownloadURLSession(
configuration: configuration,
assetDownloadDelegate: self,
delegateQueue: .main
)
let task = downloadSession.makeAssetDownloadTask(
asset: asset,
assetTitle: "Episode 1",
assetArtworkData: nil,
options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 2_000_000]
)
task.resume()
background configuration — download continues when app is in background or closed. Progress via URLSessionTaskDelegate.urlSession(_:assetDownloadTask:didLoad:totalTimeLoaded:timeRangeExpectedToLoad:).
Android. media3 DownloadManager + DownloadService. Service keeps downloads alive in background:
val downloadManager = DownloadManager(
context, databaseProvider, downloadCache, HttpDataSource.Factory(), Executor.Main
)
val downloadRequest = DownloadRequest.Builder(contentId, uri)
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()
DownloadService.sendAddDownload(context, MyDownloadService::class.java, downloadRequest, false)
Progress via DownloadManager.Listener.onDownloadChanged. For progressive files (MP4, MP3) without HLS — standard WorkManager + OkHttp with Range header support for resume.
Storage Management
Offline content accumulates. User doesn't always remember what they downloaded.
Show size of each downloaded item. iOS: AVURLAsset.assetCache?.isPlayableOffline — readiness flag, size via FileManager.attributesOfItem(atPath:)[.size]. Android: DownloadHelper.getDownloadedBytes(download).
Manual deletion and auto cleanup of old files (not played > N days). Quota: warn if less than 500 MB free (UIDevice.current.freeDiskSpaceInBytes / StatFs).
DRM: Offline Licenses
Without DRM this section simplifies. With DRM — significant integration layer added.
FairPlay (iOS). Offline playback requires offline license: AVContentKeyRequest with makeStreamingContentKeyRequestData(forApp:contentIdentifier:options:). License downloaded from server (KSM) and saved in protected storage. On offline playback — AVContentKeySession uses saved license.
Widevine (Android). ExoPlayer + DefaultDrmSessionManager. Offline license: OfflineLicenseHelper.downloadLicense(drmInitData), save keySetId. On offline playback — setLicenseUri + keySetId in MediaItem.DrmConfiguration.
Playing Downloaded Content
After download on iOS: AVURLAsset(url: localHlsURL) — path to saved HLS. asset.assetCache?.isPlayableOffline must be true before creating AVPlayerItem. Opening asset without this check — player tries to access network.
On Android with media3 DownloadManager: get DownloadRequest from database, pass to ExoPlayer via DownloadHelper.getDownloadedBytesForRequest(). CacheDataSource.Factory automatically substitutes local data instead of network requests.
Download Progress in UI
Multiple downloads simultaneously — standard situation. Each download needs separate ProgressBar with percent. On iOS: URLSession.progress.fractionCompleted via KVO, update @Published in ViewModel. On Android: DownloadManager.Listener called on background thread — switch to Main via withContext(Dispatchers.Main).
Timeline
Download without DRM with storage management — 3–4 days. With DRM (FairPlay or Widevine) — plus 3–4 days for license server integration. Full implementation for iOS + Android — 7–10 days.







