{"id":1291,"date":"2026-09-08T09:48:15","date_gmt":"2026-09-08T02:48:15","guid":{"rendered":"https:\/\/liveapi.com\/blog\/exoplayer\/"},"modified":"2026-09-08T09:48:57","modified_gmt":"2026-09-08T02:48:57","slug":"exoplayer","status":"publish","type":"post","link":"https:\/\/liveapi.com\/blog\/exoplayer\/","title":{"rendered":"ExoPlayer: What It Is, How It Works, and How to Use It on Android"},"content":{"rendered":"<span class=\"rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">Reading Time: <\/span> <span class=\"rt-time\">12<\/span> <span class=\"rt-label rt-postfix\">minutes<\/span><\/span><p>Search for ExoPlayer and the top result is a GitHub repo with a deprecation notice on it. No wonder so many developers arrive unsure whether the library is dead.<\/p>\n<p>It isn&#8217;t. ExoPlayer is still the media player inside YouTube, and it&#8217;s still what most serious Android video apps run on.<\/p>\n<p>It just moved house. The code now lives in AndroidX Media3 under a new package name, and the old standalone project stopped taking commits in April 2024.<\/p>\n<p>That single move explains almost every outdated tutorial you&#8217;ll find, and it&#8217;s where most migration pain comes from.<\/p>\n<h2>What Is ExoPlayer?<\/h2>\n<p>ExoPlayer is an open-source media player library for Android that plays audio and video from local files or network streams, with support for adaptive streaming protocols the platform&#8217;s built-in player can&#8217;t handle.<\/p>\n<p>It&#8217;s the <a href=\"https:\/\/developer.android.com\/media\/media3\/exoplayer\" target=\"_blank\" rel=\"nofollow\">default implementation of the <code>Player<\/code> interface<\/a> in Jetpack Media3. Google builds it, YouTube uses it, and it ships as an app dependency rather than as part of the operating system.<\/p>\n<p>That last detail matters more than it sounds. Android&#8217;s built-in <code>MediaPlayer<\/code> is baked into the OS, so its behavior shifts across devices and Android versions, and you can&#8217;t patch a bug on a user&#8217;s phone.<\/p>\n<p>ExoPlayer travels with your APK. Fix a playback bug, ship an app update, done.<\/p>\n<p>Here&#8217;s how the two compare:<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>ExoPlayer (Media3)<\/th>\n<th>Android MediaPlayer<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Ships with<\/strong><\/td>\n<td>Your app<\/td>\n<td>The operating system<\/td>\n<\/tr>\n<tr>\n<td><strong>Bug fixes<\/strong><\/td>\n<td>App update<\/td>\n<td>OS update (or never)<\/td>\n<\/tr>\n<tr>\n<td><strong>Adaptive streaming<\/strong><\/td>\n<td>HLS, DASH, SmoothStreaming<\/td>\n<td>Limited HLS, partial DASH<\/td>\n<\/tr>\n<tr>\n<td><strong>Customization<\/strong><\/td>\n<td>Every component is swappable<\/td>\n<td>Nearly none<\/td>\n<\/tr>\n<tr>\n<td><strong>DRM<\/strong><\/td>\n<td>Widevine, PlayReady, ClearKey<\/td>\n<td>Widevine only, device-dependent<\/td>\n<\/tr>\n<tr>\n<td><strong>Playlists<\/strong><\/td>\n<td>Built in, with gapless playback<\/td>\n<td>Manual<\/td>\n<\/tr>\n<tr>\n<td><strong>APK size cost<\/strong><\/td>\n<td>~1 to 3 MB<\/td>\n<td>Zero<\/td>\n<\/tr>\n<tr>\n<td><strong>Behavior across devices<\/strong><\/td>\n<td>Consistent<\/td>\n<td>Varies by vendor<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The tradeoff is size and complexity. You&#8217;re adding a library where the platform already hands you something free.<\/p>\n<p>For a single MP4 on an onboarding screen, that&#8217;s a bad deal. For live streams, adaptive bitrates, DRM, or precise playback control, it&#8217;s the only sane choice.<\/p>\n<h2>ExoPlayer vs Media3: Why the Library Moved<\/h2>\n<p>ExoPlayer didn&#8217;t get replaced. It got relocated.<\/p>\n<p>The library used to ship standalone as <code>com.google.android.exoplayer2<\/code>. In 2023 Google folded it into Jetpack Media3, an AndroidX library that unifies media playback, media sessions, transformation, and UI under one set of packages. Same player, new name: <code>androidx.media3.exoplayer<\/code>.<\/p>\n<p>The old project is finished. Version 2.19.0 marked the deprecation, <a href=\"https:\/\/github.com\/google\/ExoPlayer\" target=\"_blank\" rel=\"nofollow\">ExoPlayer 2.19.1<\/a> was the final artifact, and the team stopped pushing commits to the <code>dev-v2<\/code> branch on April 3, 2024.<\/p>\n<p>No security patches. No new codec support. No bug fixes.<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>Old ExoPlayer<\/th>\n<th>Media3 ExoPlayer<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Package<\/strong><\/td>\n<td><code>com.google.android.exoplayer2<\/code><\/td>\n<td><code>androidx.media3.exoplayer<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Gradle group<\/strong><\/td>\n<td><code>com.google.android.exoplayer<\/code><\/td>\n<td><code>androidx.media3<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Last version<\/strong><\/td>\n<td>2.19.1 (2023)<\/td>\n<td>1.11.0 (August 2026)<\/td>\n<\/tr>\n<tr>\n<td><strong>Status<\/strong><\/td>\n<td>Deprecated, no commits since April 2024<\/td>\n<td>Actively developed<\/td>\n<\/tr>\n<tr>\n<td><strong>Minimum SDK<\/strong><\/td>\n<td>16<\/td>\n<td>23 (Android 6.0), since Media3 1.9.0<\/td>\n<\/tr>\n<tr>\n<td><strong>Repository<\/strong><\/td>\n<td>google\/ExoPlayer<\/td>\n<td>androidx\/media<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Two things catch teams off guard during migration.<\/p>\n<p>The version number went backwards. You upgrade from ExoPlayer 2.19.1 to Media3 1.x and it looks like a downgrade. It isn&#8217;t. Media3 restarted versioning at 1.0 as a new library.<\/p>\n<p>And the minimum SDK jumped. Media3 1.9.0 raised <code>minSdk<\/code> to 23, matching the rest of AndroidX. If you&#8217;re still supporting Android 5.0, you&#8217;re pinned to Media3 1.8.1 or older.<\/p>\n<p>For the migration itself, Google publishes a shell script that rewrites package names across Gradle files, Java, Kotlin, and XML layouts. It handles the mechanical renaming. Anything where you subclassed an ExoPlayer internal still needs a human.<\/p>\n<h2>How Does ExoPlayer Work?<\/h2>\n<p>ExoPlayer isn&#8217;t one component. It&#8217;s a pipeline of swappable pieces that move bytes from a URL to pixels on a screen.<\/p>\n<p>Know those pieces and you graduate from &#8220;I got a video to play&#8221; to &#8220;I can debug why this stream stalls.&#8221;<\/p>\n<h3>The core components<\/h3>\n<ul>\n<li><strong><code>ExoPlayer<\/code><\/strong> is the top-level object that holds playback state and coordinates everything below it. You build it with <code>ExoPlayer.Builder(context).build()<\/code>.<\/li>\n<li><strong><code>MediaSource<\/code><\/strong> knows how to load a particular kind of media. <code>ProgressiveMediaSource<\/code> handles a plain MP4, <code>HlsMediaSource<\/code> handles an HLS playlist, <code>DashMediaSource<\/code> handles a DASH manifest.<\/li>\n<li><strong><code>DataSource<\/code><\/strong> does the actual byte fetching over HTTP, from disk, or from a cache.<\/li>\n<li><strong><code>Extractor<\/code><\/strong> demuxes a container into separate audio, video, and text sample streams.<\/li>\n<li><strong><code>TrackSelector<\/code><\/strong> picks which tracks play, including which quality rendition to use at any moment. This is where adaptive bitrate logic lives.<\/li>\n<li><strong><code>LoadControl<\/code><\/strong> decides when to buffer more data and when to start playing.<\/li>\n<li><strong><code>Renderer<\/code><\/strong> decodes samples and pushes them to output. <code>MediaCodecVideoRenderer<\/code> drives the hardware decoder to a <code>Surface<\/code>, <code>MediaCodecAudioRenderer<\/code> drives an <code>AudioTrack<\/code>.<\/li>\n<li><strong><code>PlayerView<\/code><\/strong> is the UI layer: the surface, the playback controls, the subtitle overlay.<\/li>\n<\/ul>\n<p>Every one of those is an interface with a default implementation you can replace.<\/p>\n<h3>The playback pipeline, step by step<\/h3>\n<ol>\n<li><strong>You set a media item.<\/strong> <code>player.setMediaItem(MediaItem.fromUri(url))<\/code> tells the player what to play, and a <code>MediaSource.Factory<\/code> inspects the URI to pick the right <code>MediaSource<\/code>.<\/li>\n<li><strong>The manifest loads.<\/strong> For adaptive streams, ExoPlayer fetches the HLS playlist or DASH manifest first and reads what renditions exist, what codecs they use, and how long the segments are.<\/li>\n<li><strong><code>LoadControl<\/code> requests data.<\/strong> The player starts pulling segments through the <code>DataSource<\/code> chain, filling buffers ahead of the playhead.<\/li>\n<li><strong><code>Extractor<\/code> demuxes.<\/strong> Each downloaded chunk gets parsed into audio, video, and subtitle samples, which land in per-track sample queues.<\/li>\n<li><strong><code>TrackSelector<\/code> chooses.<\/strong> Based on measured bandwidth, buffer health, and device capability, it picks the rendition to keep pulling. This runs continuously, not once.<\/li>\n<li><strong>Renderers decode.<\/strong> Samples feed into <code>MediaCodec<\/code>, which hands the work to the device&#8217;s hardware decoder wherever possible.<\/li>\n<li><strong>Output syncs.<\/strong> Decoded video frames go to the <code>Surface<\/code> and audio goes to the <code>AudioTrack<\/code>, timed against the player&#8217;s internal clock so lips match voices.<\/li>\n<\/ol>\n<p>This architecture matters because it localizes bugs. When a stream misbehaves, the fix almost always sits in one specific component.<\/p>\n<p>Stalling on a slow network is a <code>LoadControl<\/code> and <code>TrackSelector<\/code> problem. Audio playing over a black screen is a <code>Renderer<\/code> and codec problem. A stream that won&#8217;t start at all is usually a <code>DataSource<\/code> or manifest problem.<\/p>\n<p>The pipeline turns a vague bug report into a short list of suspects.<\/p>\n<h2>What Formats Does ExoPlayer Support?<\/h2>\n<p>ExoPlayer&#8217;s format coverage is the widest of any Android player, partly because it falls back on the device&#8217;s own decoders and partly because it ships optional software decoders for the gaps.<\/p>\n<h3>Adaptive streaming protocols<\/h3>\n<table>\n<thead>\n<tr>\n<th>Protocol<\/th>\n<th>Containers<\/th>\n<th>Content protection<\/th>\n<th>Live<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong><a href=\"https:\/\/liveapi.com\/blog\/what-is-hls-streaming\/\" target=\"_blank\">HLS<\/a><\/strong><\/td>\n<td>MPEG-TS, fMP4\/CMAF, ADTS, MP3<\/td>\n<td>AES-128, Widevine, PlayReady SL2000<\/td>\n<td>Yes, including low-latency HLS<\/td>\n<\/tr>\n<tr>\n<td><strong><a href=\"https:\/\/liveapi.com\/blog\/mpeg-dash\/\" target=\"_blank\">MPEG-DASH<\/a><\/strong><\/td>\n<td>fMP4, WebM, Matroska<\/td>\n<td>Widevine, PlayReady SL2000, ClearKey<\/td>\n<td>Yes, including ultra low-latency CMAF<\/td>\n<\/tr>\n<tr>\n<td><strong>SmoothStreaming<\/strong><\/td>\n<td>fMP4<\/td>\n<td>PlayReady SL2000 (Android TV)<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><strong><a href=\"https:\/\/liveapi.com\/blog\/what-is-real-time-streaming-protocol\/\" target=\"_blank\">RTSP<\/a><\/strong><\/td>\n<td>H.264, AAC, AC-3 payloads<\/td>\n<td>None<\/td>\n<td>Yes, RTP over UDP or TCP<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>HLS and DASH both work best with <a href=\"https:\/\/liveapi.com\/blog\/fragmented-mp4\/\" target=\"_blank\">fragmented MP4<\/a> segments, and if you package once as <a href=\"https:\/\/liveapi.com\/blog\/what-is-cmaf\/\" target=\"_blank\">CMAF<\/a> you can serve both protocols from a single set of files.<\/p>\n<h3>Video codecs<\/h3>\n<p>ExoPlayer uses the device&#8217;s hardware decoders by default and ships software extensions for the rest.<\/p>\n<table>\n<thead>\n<tr>\n<th>Codec<\/th>\n<th>Support<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>H.264 (AVC)<\/td>\n<td>Hardware, universal<\/td>\n<\/tr>\n<tr>\n<td>H.265 (HEVC)<\/td>\n<td>Hardware on most devices since Android 5.0<\/td>\n<\/tr>\n<tr>\n<td>VP9<\/td>\n<td>Hardware on many devices, software extension available<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/liveapi.com\/blog\/av1-codec\/\" target=\"_blank\">AV1<\/a><\/td>\n<td>Hardware on newer chipsets, software extension available<\/td>\n<\/tr>\n<tr>\n<td>Dolby Vision<\/td>\n<td>Device-dependent<\/td>\n<\/tr>\n<tr>\n<td>HDR10+<\/td>\n<td>In Matroska and WebM<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>H.264 remains the safe default for reach. If you&#8217;re weighing <a href=\"https:\/\/liveapi.com\/blog\/hevc-vs-h264\/\" target=\"_blank\">H.265 against H.264<\/a> for bandwidth savings, ExoPlayer will play either, so the decision comes down to encoding cost and licensing rather than playback support. For <a href=\"https:\/\/liveapi.com\/blog\/hdr-video\/\" target=\"_blank\">HDR<\/a> content, check the specific device&#8217;s capabilities at runtime instead of assuming.<\/p>\n<h3>Audio codecs<\/h3>\n<p>AAC, HE-AAC, AAC-ELD, MP3, FLAC, <a href=\"https:\/\/liveapi.com\/blog\/opus-codec\/\" target=\"_blank\">Opus<\/a>, Vorbis, AMR-NB, AMR-WB, AC-3, and E-AC-3 all decode through platform decoders. Optional extensions add FFmpeg (which brings DTS, TrueHD, ALAC, and PCM variants), IAMF, and MPEG-H.<\/p>\n<h3>Containers and subtitles<\/h3>\n<p>Progressive containers: MP4, M4A, fMP4, WebM, Matroska, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV, ADTS, FLAC, and AMR. A few of those, including MP3, ADTS, and AMR, only support constant bitrate seeking.<\/p>\n<p>Subtitle formats: WebVTT, TTML, SMPTE-TT, SubRip, SubStationAlpha, plus CEA-608 and CEA-708 for embedded broadcast captions. The full matrix lives in the <a href=\"https:\/\/developer.android.com\/media\/media3\/exoplayer\/supported-formats\" target=\"_blank\" rel=\"nofollow\">Android format documentation<\/a>.<\/p>\n<h2>Advantages of ExoPlayer<\/h2>\n<h3>It updates with your app<\/h3>\n<p>Ship a fix in your next release instead of waiting for device manufacturers. This is the single biggest practical difference from <code>MediaPlayer<\/code>, and it&#8217;s why every large Android video app made the switch years ago.<\/p>\n<h3>Adaptive streaming actually works<\/h3>\n<p>Built-in HLS, DASH, and SmoothStreaming support with real <a href=\"https:\/\/liveapi.com\/blog\/adaptive-bitrate-streaming\/\" target=\"_blank\">adaptive bitrate streaming<\/a> logic. The <code>TrackSelector<\/code> measures throughput, watches buffer health, and switches renditions mid-playback. You can tune the thresholds or write your own selection algorithm.<\/p>\n<h3>Behavior is consistent across devices<\/h3>\n<p>Because the player logic lives in your APK rather than in vendor firmware, a stream that works on a Pixel behaves the same way on a five-year-old budget phone with the same decoder capabilities. Device fragmentation still bites at the hardware decoder layer, but everything above it is yours.<\/p>\n<h3>Every component is replaceable<\/h3>\n<p>Custom <code>DataSource<\/code> to add request signing. Custom <code>LoadControl<\/code> to buffer aggressively on Wi-Fi and conservatively on cellular. Custom <code>TrackSelector<\/code> to cap resolution on metered connections. The extension points are interfaces, not hacks.<\/p>\n<h3>Playlists and gapless playback come free<\/h3>\n<p><code>player.setMediaItems(list)<\/code> gives you queue management, transitions, and gapless audio playback without writing state machines. Clipping and merging media items is built in too.<\/p>\n<h3>The instrumentation is genuinely good<\/h3>\n<p><code>AnalyticsListener<\/code> exposes buffering events, dropped frames, bandwidth estimates, decoder initialization, and playback errors with enough detail to build real quality-of-experience dashboards. Most teams wire it straight into their analytics pipeline.<\/p>\n<h2>Disadvantages of ExoPlayer<\/h2>\n<h3>It adds meaningful APK size<\/h3>\n<p>Expect roughly 1 to 3 MB depending on which modules you include, and more if you add software decoder extensions. R8 shrinking helps, but you can&#8217;t get to zero. Include only the modules you use: pulling in <code>media3-exoplayer-dash<\/code> when you only serve HLS is wasted bytes.<\/p>\n<h3>The learning curve is real<\/h3>\n<p><code>MediaPlayer<\/code> has maybe six methods worth knowing. ExoPlayer has a component graph, a threading model, and a lifecycle you have to respect.<\/p>\n<p>Getting a video playing takes fifteen minutes. Getting it to behave across rotation, backgrounding, audio focus changes, and network transitions takes considerably longer.<\/p>\n<h3>Lifecycle management is on you<\/h3>\n<p>Forget to call <code>player.release()<\/code> and you leak the codec, the surface, and the audio session. Do it in the wrong lifecycle callback and playback dies when the user rotates the screen. This is the most common source of ExoPlayer bugs in production apps.<\/p>\n<h3>Android only<\/h3>\n<p>There&#8217;s no iOS build. Cross-platform teams end up pairing ExoPlayer with AVPlayer on iOS and something like <a href=\"https:\/\/liveapi.com\/blog\/shaka-player\/\" target=\"_blank\">hls.js or Shaka Player<\/a> on web, which means three playback implementations and three sets of quirks. Wrappers exist for React Native and Flutter, but they&#8217;re thin layers over ExoPlayer that expose a fraction of the API.<\/p>\n<h3>Hardware decoder fragmentation still exists<\/h3>\n<p>ExoPlayer normalizes the software side, not the silicon. A device that reports AV1 support and then drops frames at 1080p is still a device you have to detect and work around. <code>MediaCodecInfo<\/code> capability checks and a fallback rendition ladder remain necessary.<\/p>\n<p>The architecture reads heavier than the setup actually is. Here&#8217;s the code.<\/p>\n<h2>How to Add ExoPlayer to an Android App<\/h2>\n<h3>Step 1: Add the Gradle dependencies<\/h3>\n<p>Every Media3 module has to use the same version number. Mixing versions produces confusing runtime crashes.<\/p>\n<pre><code class=\"language-kotlin\">dependencies {\n    implementation(&quot;androidx.media3:media3-exoplayer:1.11.0&quot;)\n    implementation(&quot;androidx.media3:media3-ui:1.11.0&quot;)\n\n    \/\/ Add only the protocols you actually serve\n    implementation(&quot;androidx.media3:media3-exoplayer-hls:1.11.0&quot;)\n    implementation(&quot;androidx.media3:media3-exoplayer-dash:1.11.0&quot;)\n}\n<\/code><\/pre>\n<h3>Step 2: Request network permission<\/h3>\n<pre><code class=\"language-xml\">&lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; \/&gt;\n<\/code><\/pre>\n<h3>Step 3: Add PlayerView to your layout<\/h3>\n<pre><code class=\"language-xml\">&lt;androidx.media3.ui.PlayerView\n    android:id=&quot;@+id\/player_view&quot;\n    android:layout_width=&quot;match_parent&quot;\n    android:layout_height=&quot;wrap_content&quot;\n    android:keepScreenOn=&quot;true&quot;\n    app:resize_mode=&quot;fit&quot;\n    app:show_buffering=&quot;when_playing&quot; \/&gt;\n<\/code><\/pre>\n<p><code>keepScreenOn<\/code> stops the display sleeping mid-video. <code>show_buffering<\/code> gives users a spinner instead of a frozen frame.<\/p>\n<h3>Step 4: Build the player and start playback<\/h3>\n<pre><code class=\"language-kotlin\">class PlayerActivity : AppCompatActivity() {\n\n    private var player: ExoPlayer? = null\n    private lateinit var playerView: PlayerView\n\n    private fun initializePlayer() {\n        player = ExoPlayer.Builder(this)\n            .build()\n            .also { exoPlayer -&gt;\n                playerView.player = exoPlayer\n\n                val mediaItem = MediaItem.fromUri(\n                    &quot;https:\/\/your-stream-host.example.com\/live\/stream.m3u8&quot;\n                )\n                exoPlayer.setMediaItem(mediaItem)\n                exoPlayer.playWhenReady = true\n                exoPlayer.prepare()\n            }\n    }\n}\n<\/code><\/pre>\n<p>That&#8217;s the whole playback path. ExoPlayer reads the <code>.m3u8<\/code> extension, picks <code>HlsMediaSource<\/code>, fetches the playlist, and starts adaptive playback on its own.<\/p>\n<h3>Step 5: Handle the lifecycle properly<\/h3>\n<p>This is the step people skip, and it&#8217;s the one that causes leaked codecs and dead playback after rotation.<\/p>\n<pre><code class=\"language-kotlin\">override fun onStart() {\n    super.onStart()\n    if (Build.VERSION.SDK_INT &gt; 23) initializePlayer()\n}\n\noverride fun onResume() {\n    super.onResume()\n    if (Build.VERSION.SDK_INT &lt;= 23 || player == null) initializePlayer()\n}\n\noverride fun onPause() {\n    super.onPause()\n    if (Build.VERSION.SDK_INT &lt;= 23) releasePlayer()\n}\n\noverride fun onStop() {\n    super.onStop()\n    if (Build.VERSION.SDK_INT &gt; 23) releasePlayer()\n}\n\nprivate fun releasePlayer() {\n    player?.let { exoPlayer -&gt;\n        playbackPosition = exoPlayer.currentPosition\n        playWhenReady = exoPlayer.playWhenReady\n        exoPlayer.release()\n    }\n    player = null\n}\n<\/code><\/pre>\n<p>Save the position before releasing and restore it on the next <code>initializePlayer()<\/code> call. Users notice when a rotation restarts the video.<\/p>\n<p>The URL in Step 4 is the hard part.<\/p>\n<p>ExoPlayer plays an HLS or DASH stream, but something has to produce that stream: ingest <a href=\"https:\/\/liveapi.com\/blog\/what-is-rtmp\/\" target=\"_blank\">RTMP<\/a> or SRT from a broadcaster, transcode it into a rendition ladder, package it into segments, and serve it from a <a href=\"https:\/\/liveapi.com\/blog\/cdn-for-video-streaming\/\" target=\"_blank\">CDN<\/a>. A streaming API like LiveAPI returns a ready HLS URL you can drop straight into <code>MediaItem.fromUri()<\/code>, which is why most teams pair a client player with a hosted backend rather than building both.<\/p>\n<h2>ExoPlayer Features Worth Configuring in Production<\/h2>\n<p>The defaults get you playing. These get you shipping.<\/p>\n<h3>Tune the buffering policy<\/h3>\n<p><code>DefaultLoadControl<\/code> buffers 50 seconds by default, which is generous for VOD and terrible for live. Cut it down for live streams:<\/p>\n<pre><code class=\"language-kotlin\">val loadControl = DefaultLoadControl.Builder()\n    .setBufferDurationsMs(\n        \/* minBufferMs = *\/ 2_000,\n        \/* maxBufferMs = *\/ 8_000,\n        \/* bufferForPlaybackMs = *\/ 1_000,\n        \/* bufferForPlaybackAfterRebufferMs = *\/ 2_000\n    )\n    .build()\n\nval player = ExoPlayer.Builder(context)\n    .setLoadControl(loadControl)\n    .build()\n<\/code><\/pre>\n<p>Shorter buffers cut startup time and latency but make rebuffering more likely on unstable connections. Test against real network conditions, not office Wi-Fi.<\/p>\n<h3>Constrain track selection<\/h3>\n<p>Cap resolution on cellular, or lock to a maximum bitrate to control data costs:<\/p>\n<pre><code class=\"language-kotlin\">player.trackSelectionParameters = player.trackSelectionParameters\n    .buildUpon()\n    .setMaxVideoSize(1280, 720)\n    .setMaxVideoBitrate(2_500_000)\n    .build()\n<\/code><\/pre>\n<h3>Add DRM for protected content<\/h3>\n<p>ExoPlayer handles <a href=\"https:\/\/liveapi.com\/blog\/widevine-drm\/\" target=\"_blank\">Widevine<\/a> natively, which covers Android. Apple devices need <a href=\"https:\/\/liveapi.com\/blog\/fairplay-drm\/\" target=\"_blank\">FairPlay<\/a> instead, so multi-platform catalogs typically license both.<\/p>\n<pre><code class=\"language-kotlin\">val mediaItem = MediaItem.Builder()\n    .setUri(streamUrl)\n    .setDrmConfiguration(\n        MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)\n            .setLicenseUri(licenseServerUrl)\n            .build()\n    )\n    .build()\n<\/code><\/pre>\n<p>Widevine has three security levels. L1 does decryption in the device&#8217;s trusted execution environment and is what studios require for HD and 4K playback. L3 runs in software and most studios cap it at 480p or 720p. Check the level at runtime before promising a resolution.<\/p>\n<h3>Cache segments for offline and repeat views<\/h3>\n<p><code>SimpleCache<\/code> backed by a <code>CacheDataSource.Factory<\/code> stores downloaded segments on disk. Pair it with <code>DownloadManager<\/code> for full offline downloads with progress tracking and background continuation.<\/p>\n<h3>Cut latency on live streams<\/h3>\n<p>For <a href=\"https:\/\/liveapi.com\/blog\/what-is-low-latency-streaming\/\" target=\"_blank\">low-latency HLS<\/a>, ExoPlayer reads the <code>#EXT-X-SERVER-CONTROL<\/code> and partial segment tags automatically. You control how aggressively it chases the live edge:<\/p>\n<pre><code class=\"language-kotlin\">val mediaItem = MediaItem.Builder()\n    .setUri(liveStreamUrl)\n    .setLiveConfiguration(\n        MediaItem.LiveConfiguration.Builder()\n            .setTargetOffsetMs(3_000)\n            .setMinPlaybackSpeed(0.97f)\n            .setMaxPlaybackSpeed(1.03f)\n            .build()\n    )\n    .build()\n<\/code><\/pre>\n<p>The speed range lets the player subtly adjust playback rate to hold its target offset instead of pausing or skipping.<\/p>\n<h3>Wire up captions and ads<\/h3>\n<p>Sideloaded <a href=\"https:\/\/liveapi.com\/blog\/closed-captioning-vs-subtitles\/\" target=\"_blank\">captions and subtitles<\/a> attach through <code>MediaItem.SubtitleConfiguration<\/code>, and embedded CEA-608 and CEA-708 tracks show up in the track selector automatically. For monetization, the <code>media3-exoplayer-ima<\/code> module handles client-side ads through Google IMA, while <a href=\"https:\/\/liveapi.com\/blog\/server-side-ad-insertion\/\" target=\"_blank\">server-side ad insertion<\/a> stitches ads into the manifest before ExoPlayer ever sees them, which sidesteps ad blockers entirely.<\/p>\n<h2>ExoPlayer vs VLC and Other Android Players<\/h2>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>ExoPlayer (Media3)<\/th>\n<th>libVLC for Android<\/th>\n<th>Android MediaPlayer<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Best for<\/strong><\/td>\n<td>Streaming apps, OTT, VOD<\/td>\n<td>Playing any file a user throws at it<\/td>\n<td>Simple local playback<\/td>\n<\/tr>\n<tr>\n<td><strong>Adaptive streaming<\/strong><\/td>\n<td>Excellent<\/td>\n<td>Basic<\/td>\n<td>Poor<\/td>\n<\/tr>\n<tr>\n<td><strong>Format breadth<\/strong><\/td>\n<td>Wide, tied to device decoders<\/td>\n<td>Widest, bundles its own decoders<\/td>\n<td>Narrow<\/td>\n<\/tr>\n<tr>\n<td><strong>DRM<\/strong><\/td>\n<td>Widevine, PlayReady, ClearKey<\/td>\n<td>Limited<\/td>\n<td>Widevine only<\/td>\n<\/tr>\n<tr>\n<td><strong>APK size<\/strong><\/td>\n<td>1 to 3 MB<\/td>\n<td>20 to 40 MB<\/td>\n<td>0<\/td>\n<\/tr>\n<tr>\n<td><strong>License<\/strong><\/td>\n<td>Apache 2.0<\/td>\n<td>LGPL \/ GPL<\/td>\n<td>Platform<\/td>\n<\/tr>\n<tr>\n<td><strong>Maintained by<\/strong><\/td>\n<td>Google<\/td>\n<td>VideoLAN<\/td>\n<td>Google (platform)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The split is straightforward. libVLC bundles its own decoders, so it plays obscure formats ExoPlayer refuses, which is why media center and IPTV apps favor it. That breadth costs you 20 to 40 MB and an LGPL obligation.<\/p>\n<p>ExoPlayer wins for anything that streams commercial content: better adaptive logic, proper DRM, smaller footprint, and a permissive license.<\/p>\n<h2>What ExoPlayer Doesn&#8217;t Handle: The Server Side<\/h2>\n<p>ExoPlayer is a client. It plays streams. It doesn&#8217;t make them.<\/p>\n<p>Ship a video feature and the player is maybe 20% of the work. The rest sits on the server:<\/p>\n<ul>\n<li><strong>Ingest.<\/strong> Accepting RTMP or SRT from encoders, mobile broadcasters, or IP cameras.<\/li>\n<li><strong>Transcoding.<\/strong> Turning one input into a rendition ladder so adaptive selection has something to choose between. One 1080p source needs 4 or 5 renditions to serve users on bad connections.<\/li>\n<li><strong>Packaging.<\/strong> Segmenting into HLS or DASH with correct manifests, keyframe alignment, and byte-range indexes.<\/li>\n<li><strong>Delivery.<\/strong> Global CDN distribution, because a stream served from one region buffers everywhere else.<\/li>\n<li><strong>Recording.<\/strong> Turning <a href=\"https:\/\/liveapi.com\/blog\/live-to-vod\/\" target=\"_blank\">live to VOD<\/a> so viewers can watch after the broadcast ends.<\/li>\n<li><strong>Storage, access control, and analytics.<\/strong> Signed URLs, geo restrictions, viewer metrics.<\/li>\n<\/ul>\n<p>Building that stack is months of infrastructure work plus a permanent operations burden.<\/p>\n<p>LiveAPI handles the whole chain behind an API: RTMP and SRT ingest, instant encoding into adaptive renditions up to 4K, HLS output, delivery across Akamai, Cloudflare, and Fastly, and automatic live-to-VOD recording. Your Android app calls the API, gets back an HLS URL, and hands it to ExoPlayer.<\/p>\n<p>That&#8217;s the practical division of labor. ExoPlayer owns playback on the device. A streaming API owns everything upstream of the URL.<\/p>\n<h2>Is ExoPlayer Right for Your App?<\/h2>\n<p>Use ExoPlayer if you&#8217;re doing any of these:<\/p>\n<ul>\n<li>Playing live streams or adaptive HLS and DASH content<\/li>\n<li>Distributing DRM-protected content<\/li>\n<li>Building an OTT, video-on-demand, or Android TV app<\/li>\n<li>Needing consistent playback behavior across the device landscape<\/li>\n<li>Tracking playback quality metrics in production<\/li>\n<li>Supporting offline downloads or background audio<\/li>\n<\/ul>\n<p>Stick with <code>MediaPlayer<\/code> or a lighter option if:<\/p>\n<ul>\n<li>You play short local MP4 or MP3 files and nothing else<\/li>\n<li>APK size is tightly constrained and video is a minor feature<\/li>\n<li>You need one line of code and zero configuration<\/li>\n<\/ul>\n<p>For most apps where video is a core feature rather than a decoration, ExoPlayer is the right answer. The APK cost buys you control, and the control is what you need the first time a stream breaks on a specific device in a specific market.<\/p>\n<h2>ExoPlayer FAQ<\/h2>\n<h3>Is ExoPlayer deprecated?<\/h3>\n<p>The standalone <code>com.google.android.exoplayer2<\/code> library is deprecated. ExoPlayer itself is not. The code moved to AndroidX Media3 as <code>androidx.media3.exoplayer<\/code> and is actively developed, with 1.11.0 released in August 2026. Only the old GitHub project and package name are dead.<\/p>\n<h3>What is ExoPlayer used for?<\/h3>\n<p>Playing audio and video in Android apps, especially streaming content. It handles adaptive protocols like HLS and DASH, DRM-protected playback, playlists, offline downloads, live streams, and background audio. YouTube and most major Android streaming apps run on it.<\/p>\n<h3>What&#8217;s the difference between ExoPlayer and Media3?<\/h3>\n<p>Media3 is the AndroidX library that contains ExoPlayer. ExoPlayer is the player implementation inside it. Media3 also includes media sessions, UI components, and a transformation library. In practice &#8220;Media3 ExoPlayer&#8221; and &#8220;ExoPlayer&#8221; refer to the same player.<\/p>\n<h3>Is ExoPlayer better than VLC?<\/h3>\n<p>For streaming apps, yes. ExoPlayer has stronger adaptive bitrate logic, proper DRM support, a much smaller footprint, and a permissive Apache 2.0 license. libVLC plays a wider set of exotic file formats because it bundles its own decoders, which makes it a better fit for general-purpose media player apps.<\/p>\n<h3>What is the latest ExoPlayer version?<\/h3>\n<p>AndroidX Media3 1.11.0, released in August 2026. The last standalone ExoPlayer release was 2.19.1, and the version numbering restarted at 1.0 when the library moved to Media3, so a &#8220;lower&#8221; number is still newer.<\/p>\n<h3>What minimum SDK does ExoPlayer require?<\/h3>\n<p>Media3 1.9.0 and later require API 23 (Android 6.0). Media3 1.8.1 and earlier support API 21. The old ExoPlayer 2.19.1 supported API 16.<\/p>\n<h3>Does ExoPlayer support RTMP?<\/h3>\n<p>Yes, through the optional <code>media3-datasource-rtmp<\/code> module, though RTMP playback is uncommon in modern apps. RTMP is typically used for ingest from encoder to server, with HLS or DASH delivered to viewers. ExoPlayer also supports RTSP natively for IP camera feeds.<\/p>\n<h3>Can ExoPlayer play 4K and HDR video?<\/h3>\n<p>Yes, subject to the device&#8217;s hardware decoder. ExoPlayer supports Dolby Vision and HDR10+, and exposes device capabilities through <code>MediaCodecInfo<\/code> so you can check before selecting a rendition. On DRM content, 4K generally requires Widevine L1.<\/p>\n<h2>Getting Started with ExoPlayer<\/h2>\n<p>ExoPlayer is the default choice for video playback on Android, and the Media3 move didn&#8217;t change that. It changed the package name, the versioning, and the minimum SDK. If you&#8217;re starting a new project, add <code>androidx.media3:media3-exoplayer<\/code> and skip the old library entirely.<\/p>\n<p>Once playback works, the harder problem is upstream: producing the adaptive streams ExoPlayer expects, encoding them fast enough to be useful, and delivering them worldwide without buffering.<\/p>\n<p><a href=\"https:\/\/liveapi.com\/\" target=\"_blank\">Try LiveAPI free<\/a> and get an HLS URL your Android app can play in minutes, with RTMP and SRT ingest, instant encoding, and multi-CDN delivery handled for you.<\/p>\n","protected":false},"excerpt":{"rendered":"<p><span class=\"rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">Reading Time: <\/span> <span class=\"rt-time\">12<\/span> <span class=\"rt-label rt-postfix\">minutes<\/span><\/span> Search for ExoPlayer and the top result is a GitHub repo with a deprecation notice on it. No wonder so many developers arrive unsure whether the library is dead. It isn&#8217;t. ExoPlayer is still the media player inside YouTube, and it&#8217;s still what most serious Android video apps run on. It just moved house. The [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1292,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_yoast_wpseo_title":"ExoPlayer: What It Is, How It Works, and How to Use It %%sep%% %%sitename%%","_yoast_wpseo_metadesc":"Learn what ExoPlayer is, how Media3 ExoPlayer works, which formats it supports, and how to add it to your Android app with working code examples.","inline_featured_image":false,"footnotes":""},"categories":[4],"tags":[],"class_list":["post-1291","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-video-player"],"jetpack_featured_media_url":"https:\/\/liveapi.com\/blog\/wp-content\/uploads\/2026\/09\/exoplayer.jpg","yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v15.6.2 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<meta name=\"description\" content=\"Learn what ExoPlayer is, how Media3 ExoPlayer works, which formats it supports, and how to add it to your Android app with working code examples.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/liveapi.com\/blog\/exoplayer\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"ExoPlayer: What It Is, How It Works, and How to Use It - LiveAPI Blog\" \/>\n<meta property=\"og:description\" content=\"Learn what ExoPlayer is, how Media3 ExoPlayer works, which formats it supports, and how to add it to your Android app with working code examples.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/liveapi.com\/blog\/exoplayer\/\" \/>\n<meta property=\"og:site_name\" content=\"LiveAPI Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-08T02:48:15+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-08T02:48:57+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\">\n\t<meta name=\"twitter:data1\" content=\"17 minutes\">\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebSite\",\"@id\":\"https:\/\/liveapi.com\/blog\/#website\",\"url\":\"https:\/\/liveapi.com\/blog\/\",\"name\":\"LiveAPI Blog\",\"description\":\"Live Video Streaming API Blog\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":\"https:\/\/liveapi.com\/blog\/?s={search_term_string}\",\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/liveapi.com\/blog\/exoplayer\/#primaryimage\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/liveapi.com\/blog\/wp-content\/uploads\/2026\/09\/exoplayer.jpg\",\"width\":1880,\"height\":1253,\"caption\":\"Photo by Zain Ali on Pexels\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/liveapi.com\/blog\/exoplayer\/#webpage\",\"url\":\"https:\/\/liveapi.com\/blog\/exoplayer\/\",\"name\":\"ExoPlayer: What It Is, How It Works, and How to Use It - LiveAPI Blog\",\"isPartOf\":{\"@id\":\"https:\/\/liveapi.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/liveapi.com\/blog\/exoplayer\/#primaryimage\"},\"datePublished\":\"2026-09-08T02:48:15+00:00\",\"dateModified\":\"2026-09-08T02:48:57+00:00\",\"author\":{\"@id\":\"https:\/\/liveapi.com\/blog\/#\/schema\/person\/98f2ee8b3a0bd93351c0d9e8ce490e4a\"},\"description\":\"Learn what ExoPlayer is, how Media3 ExoPlayer works, which formats it supports, and how to add it to your Android app with working code examples.\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/liveapi.com\/blog\/exoplayer\/\"]}]},{\"@type\":\"Person\",\"@id\":\"https:\/\/liveapi.com\/blog\/#\/schema\/person\/98f2ee8b3a0bd93351c0d9e8ce490e4a\",\"name\":\"govz\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/liveapi.com\/blog\/#personlogo\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/ab5cbe0543c0a44dc944c720159323bd001fc39a8ba5b1f137cd22e7578e84c9?s=96&d=mm&r=g\",\"caption\":\"govz\"},\"sameAs\":[\"https:\/\/liveapi.com\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","_links":{"self":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts\/1291","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/comments?post=1291"}],"version-history":[{"count":1,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts\/1291\/revisions"}],"predecessor-version":[{"id":1293,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts\/1291\/revisions\/1293"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/media\/1292"}],"wp:attachment":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/media?parent=1291"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/categories?post=1291"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/tags?post=1291"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}