Add the following line to your app's build.gradle dependencies:
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
Replace 2.X.X with the latest version.
Initialize ExoPlayer in your activity as follows:
private lateinit var exoPlayer: SimpleExoPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
exoPlayer = SimpleExoPlayer.Builder(this).build()
}
Create a MediaItem and set it to the player:
val mediaItem = MediaItem.fromUri("https://www.example.com/media.mp4")
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
Implement the Player.Listener interface and override onPlayerError():
exoPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
// Handle the error
Log.e("ExoPlayer", "Error: ${'$'}{error.message}")
}
})
Use the onIsLoadingChanged() callback:
exoPlayer.addListener(object : Player.Listener {
override fun onIsLoadingChanged(isLoading: Boolean) {
if (isLoading) {
// Show loading indicator
} else {
// Hide loading indicator
}
}
})
Add multiple MediaItem objects to the player:
val mediaItems = listOf(
MediaItem.fromUri("https://www.example.com/media1.mp4"),
MediaItem.fromUri("https://www.example.com/media2.mp4")
)
exoPlayer.setMediaItems(mediaItems)
exoPlayer.prepare()
exoPlayer.play()
Add the following method to your activity to trigger Picture-in-Picture mode:
override fun onUserLeaveHint() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder().build()
enterPictureInPictureMode(params)
}
}
Also, add PiP support in your AndroidManifest.xml file:
<activity
android:name=".YourActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>
To customize PiP behavior, handle the activity lifecycle callbacks, such as onPictureInPictureModeChanged:
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (isInPictureInPictureMode) {
// Hide UI controls for PiP mode
} else {
// Restore UI controls
}
}
Use a foreground service to keep the player alive:
// Start a foreground service with a notification
val intent = Intent(this, PlayerService::class.java)
ContextCompat.startForegroundService(this, intent)
Create a PlayerService class extending Service.
Use ExoPlayer's DownloadService and DownloadManager:
// Initialize the DownloadManager
val downloadManager = DownloadManager(
this,
DefaultDatabaseProvider(this),
DefaultHttpDataSource.Factory(),
DefaultRenderersFactory(this)
)
// Create a DownloadRequest
val downloadRequest = DownloadRequest.Builder("media_id", uri).build()
// Enqueue the download
downloadManager.addDownload(downloadRequest)
Adjust the LoadControl settings:
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(
minBufferMs = 15000,
maxBufferMs = 50000,
bufferForPlaybackMs = 2500,
bufferForPlaybackAfterRebufferMs = 5000
)
.build()
exoPlayer = SimpleExoPlayer.Builder(this)
.setLoadControl(loadControl)
.build()
Set the player's playWhenReady property to true:
exoPlayer.playWhenReady = true
exoPlayer.prepare()
Save the playback position and restore it:
// Saving position
val playbackPosition = exoPlayer.currentPosition
// Restoring position
exoPlayer.seekTo(playbackPosition)
exoPlayer.play()
Use the ExoPlayer's UI components or create custom ones. For example, you can modify the PlayerView XML layout to include additional buttons like shuffle or subtitle:
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:show_shuffle_button="true"
app:show_subtitle_button="true"
/>
If you want to fully customize the controls, you can define a custom controller layout and reference it in the PlayerView:
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:controller_layout_id="@layout/custom_controls"
/>
Here, custom_controls.xml would be your custom controller layout defined in the res/layout directory. In your custom layout, you can design the buttons, progress bars, or any other controls you need.
Play the live stream URL as you would with any media:
val mediaItem = MediaItem.fromUri("https://www.example.com/live.m3u8")
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
ExoPlayer will handle the live stream appropriately.
Create a DrmSessionManager and set it in the player:
// Create a MediaDrmCallback
val mediaDrmCallback = HttpMediaDrmCallback(
licenseUrl,
DefaultHttpDataSource.Factory()
)
// Create a DefaultDrmSessionManager
val drmSessionManager = DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(C.WIDEVINE_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(mediaDrmCallback)
// Build the player with DRM support
exoPlayer = SimpleExoPlayer.Builder(this)
.setMediaSourceFactory(DefaultMediaSourceFactory(this).setDrmSessionManagerProvider { drmSessionManager })
.build()
Create a MediaItem.SubtitleConfiguration and add it to the MediaItem:
val subtitle = MediaItem.SubtitleConfiguration.Builder(subtitleUri)
.setMimeType(MimeTypes.APPLICATION_SUBRIP)
.build()
val mediaItem = MediaItem.Builder()
.setUri(videoUri)
.setSubtitleConfigurations(listOf(subtitle))
.build()
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
Use the appropriate media source factory:
val mediaItem = MediaItem.fromUri("https://www.example.com/stream.mpd") // For DASH
// Or for HLS: "https://www.example.com/stream.m3u8"
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
ExoPlayer automatically handles adaptive bitrate streaming.
Implement the Player.Listener interface:
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
when (state) {
Player.STATE_READY -> {
// Player is ready
}
Player.STATE_ENDED -> {
// Playback ended
}
}
}
})
Use a DefaultTrackSelector and set parameters:
val trackSelector = DefaultTrackSelector(this)
trackSelector.parameters = trackSelector.buildUponParameters()
.setMaxVideoSizeSd()
.build()
exoPlayer = SimpleExoPlayer.Builder(this)
.setTrackSelector(trackSelector)
.build()
Implement an AudioManager.OnAudioFocusChangeListener:
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> exoPlayer.pause()
AudioManager.AUDIOFOCUS_GAIN -> exoPlayer.play()
}
}
val result = audioManager.requestAudioFocus(
focusChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
Create a custom renderer and add it to the player:
val renderersFactory = DefaultRenderersFactory(this)
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
exoPlayer = SimpleExoPlayer.Builder(this, renderersFactory).build()
This allows you to use custom or extended renderers.
Initialize a player for each item or manage a shared player:
// In your RecyclerView adapter
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val exoPlayer = SimpleExoPlayer.Builder(context).build()
holder.playerView.player = exoPlayer
// Set media and prepare
}
Be cautious with resource management to prevent memory leaks.
Adjust the DefaultTrackSelector parameters:
trackSelector.parameters = trackSelector.buildUponParameters()
.setMaxVideoBitrate(800_000) // 800 kbps
.build()
This limits the maximum video bitrate.
Create a MediaSessionCompat and connect it to the player:
val mediaSession = MediaSessionCompat(this, "TAG")
mediaSession.isActive = true
val mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(exoPlayer)
This enables integration with system media controls.
Use AnalyticsListener:
exoPlayer.addAnalyticsListener(object : AnalyticsListener {
override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, state: Int) {
// Collect analytics
}
})
Use the player's seekTo() method:
// Seek to 30 seconds
exoPlayer.seekTo(30_000)
You can create custom UI controls to trigger this method.
Retry playback in the onPlayerError() callback:
override fun onPlayerError(error: PlaybackException) {
exoPlayer.retry()
}
You might want to limit the number of retries.
Use ConcatenatingMediaSource for seamless transitions:
val mediaSource1 = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri1))
val mediaSource2 = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri2))
val concatenatedSource = ConcatenatingMediaSource(mediaSource1, mediaSource2)
exoPlayer.setMediaSource(concatenatedSource)
exoPlayer.prepare()
exoPlayer.play()
ExoPlayer supports various formats out of the box:
// Just provide the URI, ExoPlayer will handle the format
val mediaItem = MediaItem.fromUri("https://www.example.com/media.mp4") // MP4
// Or "media.webm", "media.mp3", "media.ogg", etc.
Add the FFmpeg extension to your dependencies:
implementation 'com.google.android.exoplayer:extension-ffmpeg:2.X.X'
Initialize the player with the extension:
val renderersFactory = DefaultRenderersFactory(this)
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
exoPlayer = SimpleExoPlayer.Builder(this, renderersFactory).build()
Use interfaces and callbacks provided by the library:
For example, integrating with a caching library:
// Initialize cache
val cache = SimpleCache(cacheDir, NoOpCacheEvictor())
// Use cache data source factory
val cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(DefaultDataSourceFactory(this))
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri))
Release the player when not in use:
override fun onPause() {
super.onPause()
exoPlayer.release()
}
Avoid memory leaks by nullifying references.
Adjust the player's playback parameters:
exoPlayer.setPlaybackParameters(PlaybackParameters(1.5f)) // 1.5x speed
Set the repeat mode:
exoPlayer.repeatMode = Player.REPEAT_MODE_ONE
Implement a preview loader using ImageView and seek position:
// Use a library or custom implementation to fetch frames at specific positions
This requires generating or fetching preview images.
Adjust the player's volume:
// Mute
exoPlayer.volume = 0f
// Unmute
exoPlayer.volume = 1f
Listen to buffering events and show/hide your indicator:
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_BUFFERING) {
bufferingIndicator.visibility = View.VISIBLE
} else {
bufferingIndicator.visibility = View.GONE
}
}
})
Use DefaultTrackSelector to select specific tracks:
// Get the available tracks
val mappedTrackInfo = trackSelector.currentMappedTrackInfo
val videoRendererIndex = 0 // Typically 0 for video
// Build track selection parameters
val parameters = trackSelector.buildUponParameters()
.setSelectionOverride(
videoRendererIndex,
mappedTrackInfo?.getTrackGroups(videoRendererIndex),
SelectionOverride(groupIndex, trackIndex)
)
.build()
trackSelector.parameters = parameters
This allows manual selection of video quality.
Play downloaded media using its local URI:
val mediaItem = MediaItem.fromUri(downloadedMediaUri)
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
Ensure you handle storage permissions appropriately.
Use the onPlaybackStateChanged() callback:
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_READY) {
// Player is ready
}
}
Use SimpleCache to cache media:
// Create a cache
val cache = SimpleCache(cacheDir, LeastRecentlyUsedCacheEvictor(cacheSize))
// Create a cache data source factory
val cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(DefaultDataSourceFactory(context))
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
// Use the factory in your player
val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory)
.createMediaSource(MediaItem.fromUri(mediaUri))
exoPlayer.setMediaSource(mediaSource)
exoPlayer.prepare()
Use a Handler to periodically update a SeekBar:
val handler = Handler(Looper.getMainLooper())
val runnable = object : Runnable {
override fun run() {
val currentPosition = exoPlayer.currentPosition
seekBar.progress = currentPosition.toInt()
handler.postDelayed(this, 1000)
}
}
handler.post(runnable)
Implement retry logic in the onPlayerError callback:
exoPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
if (error.isRecoverable) {
exoPlayer.retry()
} else {
// Notify the user about the issue
}
}
})
Use ExoPlayer's control methods:
// Play
exoPlayer.play()
// Pause
exoPlayer.pause()
// Stop
exoPlayer.stop()
Listen for Player.STATE_ENDED in the playback state:
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED) {
// Playback ended
}
}
})
Seek to a specific position:
// Forward 10 seconds
exoPlayer.seekTo(exoPlayer.currentPosition + 10_000)
// Rewind 10 seconds
exoPlayer.seekTo(exoPlayer.currentPosition - 10_000)
Use the AudioAttributes API:
val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MOVIE)
.build()
exoPlayer.setAudioAttributes(audioAttributes, true)
Fetch the required information from ExoPlayer:
val duration = exoPlayer.duration
val currentPosition = exoPlayer.currentPosition
val bufferedPercentage = exoPlayer.bufferedPercentage
Enable shuffling:
exoPlayer.shuffleModeEnabled = true
Use AnalyticsListener:
exoPlayer.addAnalyticsListener(object : AnalyticsListener {
override fun onVideoInputFormatChanged(eventTime: AnalyticsListener.EventTime, format: Format) {
val bitrate = format.bitrate
val resolution = "${'$'}{format.width}x${'$'}{format.height}"
}
})
Use the TrackSelector to switch audio tracks:
val mappedTrackInfo = trackSelector.currentMappedTrackInfo
val audioRendererIndex = 1 // Typically index 1 is for audio tracks
val parameters = trackSelector.buildUponParameters()
.setSelectionOverride(
audioRendererIndex,
mappedTrackInfo?.getTrackGroups(audioRendererIndex),
SelectionOverride(groupIndex, trackIndex)
)
.build()
trackSelector.parameters = parameters
Create a MediaItem with metadata:
val mediaMetadata = MediaMetadata.Builder()
.setTitle("Custom Title")
.setArtist("Artist Name")
.build()
val mediaItem = MediaItem.Builder()
.setUri("https://www.example.com/media.mp4")
.setMediaMetadata(mediaMetadata)
.build()
exoPlayer.setMediaItem(mediaItem)
Use a ConcatenatingMediaSource to dynamically add items:
val concatenatingMediaSource = ConcatenatingMediaSource()
// Add items dynamically
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri("https://www.example.com/media.mp4"))
concatenatingMediaSource.addMediaSource(mediaSource)
exoPlayer.setMediaSource(concatenatingMediaSource)
exoPlayer.prepare()
Create a MediaItem with subtitle configurations:
val subtitle = MediaItem.SubtitleConfiguration.Builder(Uri.parse("https://www.example.com/subtitles.vtt"))
.setMimeType(MimeTypes.TEXT_VTT)
.setLanguage("en")
.build()
val mediaItem = MediaItem.Builder()
.setUri("https://www.example.com/video.m3u8")
.setSubtitleConfigurations(listOf(subtitle))
.build()
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
Save and restore playback positions using Room:
// Save position
val position = exoPlayer.currentPosition
val playbackState = PlaybackState(mediaId, position)
playbackDao.insert(playbackState)
// Restore position
val savedState = playbackDao.getPlaybackState(mediaId)
exoPlayer.seekTo(savedState.position)
Use PrepareMediaSource:
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare() // Load without playing
Use the ImaAdsLoader for server-side ad integration:
val imaAdsLoader = ImaAdsLoader.Builder(context).build()
val adsMediaSource = AdsMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem, imaAdsLoader)
exoPlayer.setMediaSource(adsMediaSource)
exoPlayer.prepare()
Use a custom VideoFrameProcessor:
// Implement a custom renderer to capture frames
// Use ExoPlayer's FrameProcessor API
Integrate with MediaSessionConnector and notifications:
val mediaSession = MediaSessionCompat(this, "MediaSessionTag")
mediaSession.isActive = true
val mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(exoPlayer)
// Build and display notifications
val notificationManager = PlayerNotificationManager.Builder(this, 1, "channel_id")
.setMediaDescriptionAdapter(DescriptionAdapter())
.build()
notificationManager.setPlayer(exoPlayer)
Listen for DrmSessionManager.DrmSessionException:
exoPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
if (error is DrmSessionManager.DrmSessionException) {
Log.e("DRM", "Error: ${'$'}{error.message}")
}
}
})
Set the media item and ensure adaptive playback is enabled:
val mediaItem = MediaItem.Builder()
.setUri("https://www.example.com/live-stream.m3u8")
.setLiveConfiguration(MediaItem.LiveConfiguration.Builder().build())
.build()
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
Download media using DownloadService:
// Initialize the DownloadRequest
val downloadRequest = DownloadRequest.Builder("video_id", Uri.parse("https://www.example.com/video.mpd"))
.setStreamKeys(listOf(StreamKey(0, 0))) // Optional
.build()
// Add the request to the download manager
downloadManager.addDownload(downloadRequest)
Seek to the default live position:
exoPlayer.seekToDefaultPosition()
exoPlayer.play()
Use a single ExoPlayer instance where possible, and carefully release instances not in use:
override fun onPause() {
super.onPause()
if (isFinishing) exoPlayer.release()
}
Enable PiP mode for live streams as follows:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder().build()
enterPictureInPictureMode(params)
}
Ensure you handle activity lifecycle events properly.
Listen for Player.Listener events:
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
loadingSpinner.visibility = if (state == Player.STATE_BUFFERING) View.VISIBLE else View.GONE
}
})
Extract metadata after the player is prepared:
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_READY) {
val duration = exoPlayer.duration
val videoFormat = exoPlayer.videoFormat
val resolution = "${'$'}{videoFormat?.width}x${'$'}{videoFormat?.height}"
}
}
})
Use ConcatenatingMediaSource for preloading:
val concatenatingSource = ConcatenatingMediaSource()
val videoUris = listOf("video1.mp4", "video2.mp4")
videoUris.forEach { uri ->
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri))
concatenatingSource.addMediaSource(mediaSource)
}
exoPlayer.setMediaSource(concatenatingSource)
exoPlayer.prepare()
Integrate an analytics listener:
exoPlayer.addAnalyticsListener(object : AnalyticsListener {
override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, state: Int) {
Log.d("Analytics", "Playback state: ${'$'}state at time ${'$'}{eventTime.currentPlaybackPositionMs}")
}
})
Reduce buffer sizes and optimize video tracks:
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(10000, 20000, 1500, 2000)
.build()
exoPlayer = SimpleExoPlayer.Builder(context)
.setLoadControl(loadControl)
.build()
Set an audio file URI as a media item:
val audioMediaItem = MediaItem.fromUri("https://www.example.com/audio.mp3")
exoPlayer.setMediaItem(audioMediaItem)
exoPlayer.prepare()
exoPlayer.play()
Use setPlaybackParameters:
exoPlayer.setPlaybackParameters(PlaybackParameters(1.5f)) // 1.5x speed
Listen for Player.STATE_BUFFERING:
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_BUFFERING) {
// Handle buffering state
}
}
})
Log playback events using Firebase:
exoPlayer.addAnalyticsListener(object : AnalyticsListener {
override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, state: Int) {
val bundle = Bundle().apply {
putString("state", state.toString())
putLong("position", eventTime.currentPlaybackPositionMs)
}
FirebaseAnalytics.getInstance(context).logEvent("playback_state", bundle)
}
})
Set the repeat mode:
exoPlayer.repeatMode = Player.REPEAT_MODE_ONE
exoPlayer.setMediaItem(MediaItem.fromUri("background_video.mp4"))
exoPlayer.prepare()
exoPlayer.play()
Use ConcatenatingMediaSource to manage the playlist:
// Add an item
val newItem = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri("new_video.mp4"))
concatenatingSource.addMediaSource(newItem)
// Remove an item
concatenatingSource.removeMediaSource(index)
You can customize the PlayerView UI by defining a custom layout for the controls. Follow these steps:
1. Create a custom controller layout (e.g., res/layout/custom_controls.xml):
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#99000000"
android:padding="8dp">
<ImageButton
android:id="@+id/exo_play_pause"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_play"
android:background="@null"
android:contentDescription="@string/play_pause" />
<SeekBar
android:id="@+id/exo_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:textSize="14sp" />
</LinearLayout>
2. Reference the custom controller in your PlayerView in your layout file:
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:controller_layout_id="@layout/custom_controls" />
3. Customize behavior by interacting with the controls in your Activity or Fragment:
val playerView: PlayerView = findViewById(R.id.player_view)
playerView.player = exoPlayer
// Optionally, customize listeners for your custom controls
val playPauseButton: ImageButton = playerView.findViewById(R.id.exo_play_pause)
playPauseButton.setOnClickListener {
if (exoPlayer.isPlaying) {
exoPlayer.pause()
playPauseButton.setImageResource(R.drawable.ic_play)
} else {
exoPlayer.play()
playPauseButton.setImageResource(R.drawable.ic_pause)
}
}
val durationTextView: TextView = playerView.findViewById(R.id.exo_duration)
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_READY) {
durationTextView.text = formatTime(exoPlayer.duration)
}
}
})
// Format time utility function
fun formatTime(durationMs: Long): String {
val minutes = (durationMs / 1000) / 60
val seconds = (durationMs / 1000) % 60
return String.format("%02d:%02d", minutes, seconds)
}
This approach allows you to fully customize the look and behavior of the PlayerView's controls.
Use a GestureDetector to handle custom gestures:
val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
if (Math.abs(distanceX) > Math.abs(distanceY)) {
val seekPosition = exoPlayer.currentPosition - (distanceX * 10).toLong()
exoPlayer.seekTo(seekPosition)
return true
}
return false
}
})
playerView.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
Use a custom method to toggle the visibility of the system UI:
fun toggleFullscreen() {
if (isFullscreen) {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
isFullscreen = false
} else {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
isFullscreen = true
}
}
Implement a shared ExoPlayer instance and bind it to the RecyclerView items:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val mediaItem = MediaItem.fromUri(videoList[position])
holder.playerView.player = exoPlayer
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
}
Use a DrmSessionManager with live media sources:
val drmSessionManager = DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(C.WIDEVINE_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(HttpMediaDrmCallback(licenseUrl, DefaultHttpDataSource.Factory()))
exoPlayer = SimpleExoPlayer.Builder(context)
.setMediaSourceFactory(DefaultMediaSourceFactory(context).setDrmSessionManagerProvider { drmSessionManager })
.build()
Use DefaultTrackSelector to dynamically switch video quality:
val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters()
.setMaxVideoSize(1280, 720)
.build()
exoPlayer = SimpleExoPlayer.Builder(context)
.setTrackSelector(trackSelector)
.build()
Use the ConnectivityManager to monitor network changes:
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
exoPlayer.pause()
}
override fun onAvailable(network: Network) {
exoPlayer.play()
}
})
Use the ConcatenatingMediaSource with transition animations:
// Apply custom animations between items (handled at UI layer)
val concatenatingSource = ConcatenatingMediaSource()
val mediaSource1 = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri("video1.mp4"))
val mediaSource2 = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri("video2.mp4"))
concatenatingSource.addMediaSource(mediaSource1)
concatenatingSource.addMediaSource(mediaSource2)
exoPlayer.setMediaSource(concatenatingSource)
exoPlayer.prepare()
Set the minimum bitrate using DefaultTrackSelector:
val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters()
.setForceLowestBitrate(true)
.build()
exoPlayer = SimpleExoPlayer.Builder(context)
.setTrackSelector(trackSelector)
.build()
Use AnalyticsListener:
exoPlayer.addAnalyticsListener(object : AnalyticsListener {
override fun onVideoFrameProcessingOffset(eventTime: AnalyticsListener.EventTime, totalOffsetUs: Long, frameCount: Int) {
Log.d("Stats", "Frame drops: ${'$'}frameCount, Total Offset: ${'$'}totalOffsetUs")
}
})
Set repeat mode on a concatenating media source:
val concatenatingSource = ConcatenatingMediaSource()
concatenatingSource.addMediaSource(mediaSource1)
concatenatingSource.addMediaSource(mediaSource2)
exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
exoPlayer.setMediaSource(concatenatingSource)
exoPlayer.prepare()
Add multiple subtitle configurations to the MediaItem:
val englishSubtitle = MediaItem.SubtitleConfiguration.Builder(subtitleUriEn)
.setMimeType(MimeTypes.TEXT_VTT)
.setLanguage("en")
.build()
val frenchSubtitle = MediaItem.SubtitleConfiguration.Builder(subtitleUriFr)
.setMimeType(MimeTypes.TEXT_VTT)
.setLanguage("fr")
.build()
val mediaItem = MediaItem.Builder()
.setUri(videoUri)
.setSubtitleConfigurations(listOf(englishSubtitle, frenchSubtitle))
.build()
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
Listen for Player.STATE_ENDED and ensure no further items are queued:
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED && !exoPlayer.hasNextMediaItem()) {
Log.d("Playlist", "Playback completed")
}
}
})
Use the Google Cast SDK along with ExoPlayer:
// Setup CastContext
val castContext = CastContext.getSharedInstance(context)
// Use CastPlayer
val castPlayer = CastPlayer(castContext)
castPlayer.setMediaItem(mediaItem)
castPlayer.prepare()
castPlayer.play()
Use the DefaultTrackSelector to disable specific tracks:
val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters()
.setRendererDisabled(C.TRACK_TYPE_VIDEO, true) // Disable video
.build()
exoPlayer = SimpleExoPlayer.Builder(context)
.setTrackSelector(trackSelector)
.build()
Use an ad-enabled MediaSource with a provided ad tag:
val adsLoader = ImaAdsLoader.Builder(context).build()
val adsMediaSource = AdsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri("https://example.com/video.m3u8"), adsLoader)
exoPlayer.setMediaSource(adsMediaSource)
exoPlayer.prepare()
exoPlayer.play()
Use Firebase Realtime Database or WebSockets to sync playback positions:
// Update playback position on server
val position = exoPlayer.currentPosition
database.reference.child("sync/playback").setValue(position)
// Listen for changes from the server
database.reference.child("sync/playback").addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val syncedPosition = snapshot.getValue(Long::class.java) ?: 0
exoPlayer.seekTo(syncedPosition)
}
})
Track playback positions periodically and log data:
val handler = Handler(Looper.getMainLooper())
val playbackTracker = object : Runnable {
override fun run() {
val currentPosition = exoPlayer.currentPosition
logPlaybackHeatmap(currentPosition)
handler.postDelayed(this, 1000)
}
}
handler.post(playbackTracker)
Use a visualization library to create heatmaps from the tracked data.
Use ExoPlayer’s AudioProcessor for custom effects:
class CustomAudioProcessor : BaseAudioProcessor() {
override fun onProcessInputBuffer(inputBuffer: ByteBuffer) {
// Apply custom audio transformations here
}
}
exoPlayer = SimpleExoPlayer.Builder(context)
.setAudioProcessors(listOf(CustomAudioProcessor()))
.build()
Set the aspect ratio of the PlayerView dynamically:
playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
For custom handling, override the layout parameters programmatically.
Combine ExoPlayer with backend APIs for managing content:
// Fetch media information from the server
val videoList = api.getVideoList()
// Use ExoPlayer to stream video on demand
videoList.forEach { video ->
val mediaItem = MediaItem.fromUri(video.url)
exoPlayer.addMediaItem(mediaItem)
}
exoPlayer.prepare()
Include features like search, recommendations, and user watchlists in your VOD system.
Use ExoPlayer's VR extension for 360-degree video support:
implementation 'com.google.android.exoplayer:extension-vr:2.X.X'
// Configure a spherical view with PlayerView
playerView.setUseSphericalProjection(true)
exoPlayer.setMediaItem(MediaItem.fromUri("https://example.com/360video.mp4"))
exoPlayer.prepare()
exoPlayer.play()
Use separate tracks for each camera angle and switch dynamically:
val mappedTrackInfo = trackSelector.currentMappedTrackInfo
val cameraAngleGroup = mappedTrackInfo?.getTrackGroups(C.TRACK_TYPE_VIDEO)?.get(cameraAngleIndex)
val parameters = trackSelector.buildUponParameters()
.setSelectionOverride(
C.TRACK_TYPE_VIDEO,
cameraAngleGroup,
SelectionOverride(cameraAngleGroupIndex, cameraAngleTrackIndex)
)
trackSelector.parameters = parameters
Encapsulate common functionality in a library module:
// Create a custom ExoPlayer wrapper
class CustomPlayer(context: Context) {
private val exoPlayer: SimpleExoPlayer = SimpleExoPlayer.Builder(context).build()
fun playVideo(url: String) {
val mediaItem = MediaItem.fromUri(url)
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
}
fun release() {
exoPlayer.release()
}
}
// Publish the module as a library for other projects









