The dependencies
implementation 'com.google.android.exoplayer:exoplayer-core:2.10.6'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.10.6'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.10.6'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.10.6'
The layout files
custom_playback_controller.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#CC000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal">
<!--<ImageButton android:id="@id/exo_prev"-->
<!--style="@style/ExoMediaButton.Previous"/>-->
<ImageButton android:id="@id/exo_rew"
android:tint="#FF00A6FF"
style="@style/ExoMediaButton.Rewind"/>
<ImageButton android:id="@id/exo_shuffle"
android:tint="#FF00A6FF"
style="@style/ExoMediaButton.Shuffle"/>
<ImageButton android:id="@id/exo_repeat_toggle"
android:tint="#FF00A6FF"
style="@style/ExoMediaButton"/>
<ImageButton android:id="@id/exo_play"
android:tint="#FF00A6FF"
style="@style/ExoMediaButton.Play"/>
<ImageButton android:id="@id/exo_pause"
android:tint="#FF00A6FF"
style="@style/ExoMediaButton.Pause"/>
<ImageButton android:id="@id/exo_ffwd"
android:tint="#FF00A6FF"
style="@style/ExoMediaButton.FastForward"/>
<!--<ImageButton android:id="@id/exo_next"-->
<!--style="@style/ExoMediaButton.Next"/>-->
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FF00A6FF"/>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="26dp"/>
<TextView android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FF00A6FF"/>
</LinearLayout>
</LinearLayout>
activity_player.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#000000">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:controller_layout_id="@layout/custom_playback_control"/>
</FrameLayout>
The Kotlin class files
ComponentListener.kt
import android.util.Log
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.analytics.AnalyticsListener
import com.google.android.exoplayer2.audio.AudioRendererEventListener
import com.google.android.exoplayer2.decoder.DecoderCounters
import com.google.android.exoplayer2.video.VideoRendererEventListener
class ComponentListener : Player.EventListener, VideoRendererEventListener, AudioRendererEventListener, AnalyticsListener {
private val TAG = "ComponentListener"
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
val stateString: String
when (playbackState) {
Player.STATE_IDLE -> stateString = "ExoPlayer.STATE_IDLE -"
Player.STATE_BUFFERING -> stateString = "ExoPlayer.STATE_BUFFERING -"
Player.STATE_READY -> stateString = "ExoPlayer.STATE_READY -"
Player.STATE_ENDED -> stateString = "ExoPlayer.STATE_ENDED -"
else -> stateString = "UNKNOWN_STATE -"
}
Log.d(TAG, "changed state to $stateString playWhenReady: $playWhenReady")
}
// Implementing VideoRendererEventListener.
override fun onVideoEnabled(counters: DecoderCounters) {
// Do nothing.
}
override fun onVideoDecoderInitialized(decoderName: String, initializedTimestampMs: Long, initializationDurationMs: Long) {
// Do nothing.
}
override fun onVideoInputFormatChanged(format: Format) {
// Do nothing.
}
override fun onDroppedFrames(count: Int, elapsedMs: Long) {
// Do nothing.
}
override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
// Do nothing.
}
override fun onVideoDisabled(counters: DecoderCounters) {
// Do nothing.
}
// Implementing AudioRendererEventListener.
override fun onAudioEnabled(counters: DecoderCounters) {
// Do nothing.
}
override fun onAudioSessionId(audioSessionId: Int) {
// Do nothing.
}
override fun onAudioDecoderInitialized(decoderName: String, initializedTimestampMs: Long, initializationDurationMs: Long) {
// Do nothing.
}
override fun onAudioInputFormatChanged(format: Format) {
// Do nothing.
}
override fun onAudioSinkUnderrun(bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long) {
// Do nothing.
}
override fun onAudioDisabled(counters: DecoderCounters) {
// Do nothing.
}
}
PlayerActivity.kt
import android.annotation.SuppressLint
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import android.view.View
import com.example.exoplayer.MainActivity.Companion.MEDIA_URI
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.ExoPlayerFactory
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.audio.AudioRendererEventListener
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.util.Util
import com.google.android.exoplayer2.video.VideoRendererEventListener
/**
* A fullscreen activity to play audio or video streams.
*/
class PlayerActivity : AppCompatActivity() {
companion object {
private val BANDWIDTH_METER = DefaultBandwidthMeter()
}
private var playerView: PlayerView? = null
private lateinit var player: SimpleExoPlayer
private var mediaUri = ""
private var playbackPosition: Long = 0
private var currentWindow: Int = 0
private var playWhenReady = true
private var componentListener: ComponentListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
mediaUri = intent.getStringExtra(MEDIA_URI)
if (mediaUri.isEmpty()) {
AlertDialog.Builder(this)
.setTitle("Invalid media uri")
.setMessage("The uri is empty, please provide a valid media uri")
.setPositiveButton("OK") { _, _ ->
finish()
}
.show()
}
playerView = findViewById(R.id.video_view)
componentListener = ComponentListener()
}
public override fun onStart() {
super.onStart()
if (Util.SDK_INT > 23) {
initializePlayer(mediaUri)
}
}
public override fun onResume() {
super.onResume()
hideSystemUi()
if (Util.SDK_INT <= 23 || !::player.isInitialized) {
initializePlayer(mediaUri)
}
}
public override fun onPause() {
super.onPause()
if (Util.SDK_INT <= 23 && ::player.isInitialized) {
releasePlayer()
}
}
public override fun onStop() {
super.onStop()
if (Util.SDK_INT > 23 && ::player.isInitialized) {
releasePlayer()
}
}
private fun initializePlayer(uri: String) {
if (uri.isEmpty()) {
return
}
if (!::player.isInitialized) {
// a factory to create an AdaptiveVideoTrackSelection
val adaptiveTrackSelectionFactory = AdaptiveTrackSelection.Factory(BANDWIDTH_METER)
player = ExoPlayerFactory.newSimpleInstance(
this,
DefaultRenderersFactory(this),
DefaultTrackSelector(adaptiveTrackSelectionFactory),
DefaultLoadControl())
player.addListener(componentListener)
player.addAnalyticsListener(componentListener)
player.addAnalyticsListener(componentListener)
}
playerView?.player = player
player.playWhenReady = playWhenReady
player.seekTo(currentWindow, playbackPosition)
val mediaSource = buildMediaSource(Uri.parse(uri))
player.prepare(mediaSource, true, false)
}
private fun releasePlayer() {
playbackPosition = player.currentPosition
currentWindow = player.currentWindowIndex
playWhenReady = player.playWhenReady
player.removeListener(componentListener)
player.setVideoListener(null)
player.removeAnalyticsListener(componentListener)
player.removeAnalyticsListener(componentListener)
player.release()
}
private fun buildMediaSource(uri: Uri): MediaSource {
val userAgent = "exoplayer-codelab"
if (uri.getLastPathSegment().contains("mp3") || uri.getLastPathSegment().contains("mp4")) {
return ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory(userAgent))
.createMediaSource(uri)
} else if (uri.getLastPathSegment().contains("m3u8")) {
return HlsMediaSource.Factory(DefaultHttpDataSourceFactory(userAgent))
.createMediaSource(uri)
} else {
val dashChunkSourceFactory = DefaultDashChunkSource.Factory(
DefaultHttpDataSourceFactory("ua", BANDWIDTH_METER))
val manifestDataSourceFactory = DefaultHttpDataSourceFactory(userAgent)
return DashMediaSource.Factory(dashChunkSourceFactory, manifestDataSourceFactory).createMediaSource(uri)
}
}
@SuppressLint("InlinedApi")
private fun hideSystemUi() {
playerView!!.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
}
}
The resource files
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FFFFFF</color>
<color name="colorPrimaryDark">#000000</color>
<color name="colorAccent">#FF4081</color>
</resources>
strings.xml
<resources>
<string name="app_name">ExoPlayer code lab</string>
<string name="player_activity_name">Media player</string>
<string name="media_url_mp3">http://storage.googleapis.com/exoplayer-test-media-0/play.mp3</string>
<string name="media_url_mp4">http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4</string>
<string name="media_url_dash"><![CDATA[http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0]]></string>
<string name="logo">Google logo</string>
</resources>
styles.xml
<resources>
<style name="PlayerTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@null</item>
</style>
</resources>
Player activity class field variables, these class field variables are used within the player class.
private var playerView: PlayerView? = null
private var player: SimpleExoPlayer? = null
private var playbackPosition: Long = 0
private var currentWindow: Int = 0
private var playWhenReady = true
private var componentListener: ComponentListener? = null
Initialize the playerView and componentListener in the onCreate function.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
playerView = findViewById(R.id.video_view)
componentListener = ComponentListener()
}
The function for initializing the Exoplayer
private fun initializePlayer(uri: String) {
if (uri.isEmpty()) {
return
}
if (!::player.isInitialized) {
// a factory to create an AdaptiveVideoTrackSelection
val adaptiveTrackSelectionFactory = AdaptiveTrackSelection.Factory(BANDWIDTH_METER)
player = ExoPlayerFactory.newSimpleInstance(
this,
DefaultRenderersFactory(this),
DefaultTrackSelector(adaptiveTrackSelectionFactory),
DefaultLoadControl())
player.addListener(componentListener)
player.addAnalyticsListener(componentListener)
player.addAnalyticsListener(componentListener)
}
playerView?.player = player
player.playWhenReady = playWhenReady
player.seekTo(currentWindow, playbackPosition)
val mediaSource = buildMediaSource(Uri.parse(uri))
player.prepare(mediaSource, true, false)
}
The function for releasing the Exoplayer.
private fun releasePlayer() {
playbackPosition = player.currentPosition
currentWindow = player.currentWindowIndex
playWhenReady = player.playWhenReady
player.removeListener(componentListener)
player.setVideoListener(null)
player.removeAnalyticsListener(componentListener)
player.removeAnalyticsListener(componentListener)
player.release()
}
The function for building the media source to be played in the Exoplayer.
private fun buildMediaSource(uri: Uri): MediaSource {
val userAgent = "exoplayer-codelab"
if (uri.getLastPathSegment().contains("mp3") || uri.getLastPathSegment().contains("mp4")) {
return ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory(userAgent))
.createMediaSource(uri)
} else if (uri.getLastPathSegment().contains("m3u8")) {
return HlsMediaSource.Factory(DefaultHttpDataSourceFactory(userAgent))
.createMediaSource(uri)
} else {
val dashChunkSourceFactory = DefaultDashChunkSource.Factory(
DefaultHttpDataSourceFactory("ua", BANDWIDTH_METER))
val manifestDataSourceFactory = DefaultHttpDataSourceFactory(userAgent)
return DashMediaSource.Factory(dashChunkSourceFactory, manifestDataSourceFactory).createMediaSource(uri)
}
}
The activity life cycle functions (onStart and onResume) that calls the function for initializing the exoplayer.
public override fun onStart() {
super.onStart()
if (Util.SDK_INT > 23) {
initializePlayer(mediaUri)
}
}
public override fun onResume() {
super.onResume()
hideSystemUi()
if (Util.SDK_INT <= 23 || !::player.isInitialized) {
initializePlayer(mediaUri)
}
}
The activity life cycle functions (onPause and onStop) that calls the function for releasing the exoplayer.
public override fun onPause() {
super.onPause()
if (Util.SDK_INT <= 23 && ::player.isInitialized) {
releasePlayer()
}
}
public override fun onStop() {
super.onStop()
if (Util.SDK_INT > 23 && ::player.isInitialized) {
releasePlayer()
}
}