Chapters

Hide chapters

Android Apprentice

Fourth Edition · Android 11 · Kotlin 1.4 · Android Studio 4.1

Section II: Building a List App

Section 2: 7 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

27. Episode Player
Written by Fuad Kamal

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the last chapter, you succeeded in adding audio playback to the app, but you stopped short of adding any built-in playback features. In this final chapter of this section, you’ll finish up PodPlay by adding a full playback interface and support for videos.

If you’re following along with your own project, the starter project for this chapter includes an additional icon that you’ll need to complete the section. Open your project then copy the following resources from the provided starter project into yours. Be sure to copy the .png files from the various dpi folders (shown below once as “?dpi” but on the file system, they’ll be “hdpi”, “mdpi”, etc). This includes the following resources:

  • res/drawable-?dpi/ic_forward_30_white.png
  • res/drawable-?dpi/ic_replay_10_white.png
  • res/drawable/ic_play_pause_toggle.xml

If you don’t have your own project, don’t worry. Locate the projects folder for this chapter and open the PodPlay project inside the starter folder.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

Getting started

You’ll start by adding a new Fragment to display the details for a single episode. This Fragment gets loaded when the user taps on an episode.

The episode detail screen provides an overview of the episode and playback controls. The design looks like this:

The album art is in the upper-left corner. The episode title is to the right. The description takes up the entire center of the layout and because episode descriptions can be long, the TextView is scrollable so that the user can see the full description.

At the bottom is the player controls area. This area has a black background and the following controls:

  • Play/Pause toggle: starts and stops playback.
  • Skip back: skips back 10 seconds.
  • Skip forward: skips forward 30 seconds.
  • Speed control: allows the playback speed to be increased.
  • Scrubber: displays playback progress and allows scrubbing to any part of the episode.

First up, creating the basic layout.

Episode player layout

Inside res/layout, create a new file and name it fragment_episode_player.xml. Replace its contents with the following:

<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/black">

  <SurfaceView
      android:id="@+id/videoSurfaceView"
      android:layout_width="0dp"
      android:layout_height="0dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintBottom_toBottomOf="parent"
      android:visibility="invisible"/>

  <androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/headerView"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:background="#eeeeee"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">
  </androidx.constraintlayout.widget.ConstraintLayout>

  <TextView
      android:id="@+id/episodeDescTextView"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:background="@android:color/white"
      android:padding="8dp"
      android:scrollbars="vertical"
      app:layout_constraintBottom_toTopOf="@+id/playerControls"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/headerView"
      tools:text="Episode description"/>

  <androidx.constraintlayout.widget.ConstraintLayout
      android:id="@+id/playerControls"
      android:layout_width="0dp"
      android:layout_height="76dp"
      android:background="@android:color/background_dark"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintBottom_toBottomOf="parent">
  </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<string name="episode_thumbnail">episode thumbnail</string>
<string name="replay_button">replay button</string>
<string name="skip_forward">skip forward</string>
<string name="_1x">1x</string>
<string name="_0_00">0:00</string>
<ImageView
    android:id="@+id/episodeImageView"
    android:layout_width="60dp"
    android:layout_height="60dp"
    android:layout_marginStart="8dp"
    android:layout_marginTop="8dp"
    android:src="@android:drawable/ic_menu_report_image"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:contentDescription="@string/episode_thumbnail" />

<TextView
    android:id="@+id/episodeTitleTextView"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_marginEnd="8dp"
    android:layout_marginStart="8dp"
    android:text=""       
    app:layout_constraintBottom_toBottomOf="@+id/episodeImageView"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toEndOf="@+id/episodeImageView"
    app:layout_constraintTop_toTopOf="@+id/episodeImageView"/>
<ImageButton
  android:id="@+id/replayButton"
  android:layout_width="34dp"
  android:layout_height="34dp"
  android:layout_marginEnd="24dp"
  android:layout_marginTop="8dp"
  android:background="@android:color/transparent"
  android:scaleType="fitCenter"
  android:src="@drawable/ic_replay_10_white"
  app:layout_constraintEnd_toStartOf="@+id/playToggleButton"
  app:layout_constraintTop_toTopOf="parent"
  android:contentDescription="@string/replay_button" />

<Button
  android:id="@+id/playToggleButton"
  android:layout_width="34dp"
  android:layout_height="34dp"
  android:layout_marginTop="8dp"
  android:background="@drawable/ic_play_pause_toggle"
  android:scaleType="fitCenter"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintHorizontal_bias="0.5"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>

<ImageButton
  android:id="@+id/forwardButton"
  android:layout_width="34dp"
  android:layout_height="34dp"
  android:layout_marginStart="24dp"
  android:layout_marginTop="8dp"
  android:background="@android:color/transparent"
  android:scaleType="fitCenter"
  android:src="@drawable/ic_forward_30_white"
  app:layout_constraintStart_toEndOf="@+id/playToggleButton"
  app:layout_constraintTop_toTopOf="parent"
  android:contentDescription="@string/skip_forward" />

<Button
  android:id="@+id/speedButton"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginEnd="8dp"
  android:layout_marginTop="5dp"
  android:background="@android:color/transparent"
  android:text="@string/_1x"
  android:textAllCaps="false"
  android:textColor="@android:color/white"
  android:textSize="14sp"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintTop_toTopOf="parent" /> 
<TextView
  android:id="@+id/currentTimeTextView"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginBottom="8dp"
  android:layout_marginStart="8dp"
  android:text="@string/_0_00"
  android:textColor="@android:color/white"
  android:textSize="12sp"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="@+id/seekBar"/>

<SeekBar
  android:id="@+id/seekBar"
  android:layout_width="0dp"
  android:layout_height="wrap_content"
  android:layout_marginBottom="8dp"
  android:layout_marginEnd="8dp"
  android:layout_marginStart="8dp"
  android:progressBackgroundTint="@android:color/white"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toStartOf="@+id/endTimeTextView"
  app:layout_constraintStart_toEndOf="@+id/currentTimeTextView"/>

<TextView
  android:id="@+id/endTimeTextView"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginBottom="8dp"
  android:layout_marginEnd="8dp"
  android:text="@string/_0_00"
  android:textColor="@android:color/white"
  android:textSize="12sp"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintTop_toTopOf="@+id/seekBar" />

Episode player fragment

You’re ready to build out the episode player Fragment. This Fragment will display the episode layout and handle all of the playback logic. You’ll move the media-related code from the PodcastDetailsFragment class into this new episode player fragment.

class EpisodePlayerFragment : Fragment() {

  private lateinit var databinding: FragmentEpisodePlayerBinding

  companion object {
    fun newInstance(): EpisodePlayerFragment {
      return EpisodePlayerFragment()
    }
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
  }

  override fun onCreateView(inflater: LayoutInflater,
                            container: ViewGroup?,
                            savedInstanceState: Bundle?): View {
  databinding = FragmentEpisodePlayerBinding.inflate(inflater, container, false)
    return databinding.root
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
  }

  override fun onStart() {
    super.onStart()
  }

  override fun onStop() {
    super.onStop()
  }
}

Episode player navigation

Before finishing the Fragment code, hook up the navigation.

fun onShowEpisodePlayer(episodeViewData: EpisodeViewData)
listener?.onShowEpisodePlayer(episodeViewData)
override fun onShowEpisodePlayer(episodeViewData: EpisodeViewData) {
}
private const val TAG_PLAYER_FRAGMENT = "PlayerFragment"
private fun createEpisodePlayerFragment(): EpisodePlayerFragment {
  var episodePlayerFragment =
      supportFragmentManager.findFragmentByTag(TAG_PLAYER_FRAGMENT) as
      EpisodePlayerFragment?

  if (episodePlayerFragment == null) {
    episodePlayerFragment = EpisodePlayerFragment.newInstance()
  }

  return episodePlayerFragment
}
var activeEpisodeViewData: EpisodeViewData? = null
private fun showPlayerFragment() {
  val episodePlayerFragment = createEpisodePlayerFragment()
  supportFragmentManager.beginTransaction().replace(R.id.podcastDetailsContainer,
        episodePlayerFragment, TAG_PLAYER_FRAGMENT).addToBackStack("PlayerFragment").commit()
    databinding.podcastRecyclerView.visibility = View.INVISIBLE
    searchMenuItem.isVisible = false
}
podcastViewModel.activeEpisodeViewData = episodeViewData
showPlayerFragment()

Episode player details

It’s time to get some episode data on the player screen. You’ll use the active episode view data from the podcast view model to populate the Views.

private val podcastViewModel: PodcastViewModel by activityViewModels()
private fun updateControls() {
  // 1
  databinding.episodeTitleTextView.text = podcastViewModel.activeEpisodeViewData?.title
  // 2
  val htmlDesc = podcastViewModel.activeEpisodeViewData?.description ?: ""
  val descSpan = HtmlUtils.htmlToSpannable(htmlDesc)
  databinding.episodeDescTextView.text = descSpan
  databinding.episodeDescTextView.movementMethod = ScrollingMovementMethod()
  // 3
  val fragmentActivity = activity as FragmentActivity
  Glide.with(fragmentActivity)
    .load(podcastViewModel.podcastLiveData.value?.imageUrl)
    .into(databinding.episodeImageView)
}
updateControls()

Episode player controls

Now you can turn your attention to the player controls. You’ll get the basic play, pause, and skip controls working first. Then you’ll focus on the seek bar and speed control.

Play/Pause button

Now it’s time to hook up the play/pause button to start and stop playback.

private fun togglePlayPause() {
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  if (controller.playbackState != null) {
    if (controller.playbackState.state ==
        PlaybackStateCompat.STATE_PLAYING) {
      controller.transportControls.pause()
    } else {
      podcastViewModel.activeEpisodeViewData?.let { startPlaying(it) }
    }
  } else {
    podcastViewModel.activeEpisodeViewData?.let { startPlaying(it) }
  }
}
private fun setupControls() {
  databinding.playToggleButton.setOnClickListener {
    togglePlayPause()
  }
}
private fun handleStateChange(state: Int) {
  val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
  databinding.playToggleButton.isActivated = isPlaying
}
val state = state ?: return
handleStateChange(state.getState())
setupControls()

Speed control button

Next, you’ll hook up the speed control button. This button will increase the speed by 0.25x times each time it’s tapped up to a maximum of 2.0x. It will go to 0.75x after reaching the max of 2.0x.

companion object {
  const val CMD_CHANGESPEED = "change_speed"
  const val CMD_EXTRA_SPEED = "speed"
}
private fun setState(state: Int, newSpeed: Float? = null) {
// 1
var speed = 1.0f
// 2
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  if (newSpeed == null) {
    // 3
    speed = mediaPlayer?.getPlaybackParams()?.speed ?: 1.0f
  } else {
    // 4
    speed = newSpeed
  }
  mediaPlayer?.let { mediaPlayer ->
    // 5
    try {
      mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speed)
    }
    catch (e: Exception) {
      // 6
      mediaPlayer.reset()
      mediaUri?.let { mediaUri ->      
        mediaPlayer.setDataSource(context, mediaUri)
      }
      mediaPlayer.prepare()
      // 7
      mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speed)
      // 8
      mediaPlayer.seekTo(position.toInt())
      // 9
      if (state == PlaybackStateCompat.STATE_PLAYING) {
        mediaPlayer.start()
      }
    }
  }
}
.setState(state, position, speed)
private fun changeSpeed(extras: Bundle) {
  var playbackState = PlaybackStateCompat.STATE_PAUSED
  if (mediaSession.controller.playbackState != null) {
    playbackState = mediaSession.controller.playbackState.state
  }
  setState(playbackState, extras.getFloat(CMD_EXTRA_SPEED))
}
override fun onCommand(command: String?, extras: Bundle?,
    cb: ResultReceiver?) {
  super.onCommand(command, extras, cb)
  when (command) {
    CMD_CHANGESPEED -> extras?.let { changeSpeed(it) }
  }
}
private var playerSpeed: Float = 1.0f
private fun changeSpeed() {
  // 1
  playerSpeed += 0.25f
  if (playerSpeed > 2.0f) {
    playerSpeed = 0.75f
  }
  // 2
  val bundle = Bundle()
  bundle.putFloat(CMD_EXTRA_SPEED, playerSpeed)
  // 3    
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  controller.sendCommand(CMD_CHANGESPEED, bundle, null)
  // 4
  val speedButtonText = "${playerSpeed}x"
  databinding.speedButton.text = speedButtonText
}
val speedButtonText = "${playerSpeed}x"
databinding.speedButton.text = speedButtonText
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  databinding.speedButton.setOnClickListener {
    changeSpeed()
  }
} else {
  databinding.speedButton.visibility = View.INVISIBLE
}

Seeking

Before adding the changes to the player Fragment to support skipping or scrubbing to a new position, you need to update the media browser to allow seeking to a specific playback position. This is done by overriding an additional method in PodplayMediaCallback.

override fun onSeekTo(pos: Long) {
  super.onSeekTo(pos)
  // 1
  mediaPlayer?.seekTo(pos.toInt())
  // 2
  val playbackState: PlaybackStateCompat? =
      mediaSession.controller.playbackState
  // 3
  if (playbackState != null) {
    setState(playbackState.state)
  } else {
    setState(PlaybackStateCompat.STATE_PAUSED)
  }
}
private fun seekBy(seconds: Int) {
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  val newPosition = controller.playbackState.position + seconds*1000
  controller.transportControls.seekTo(newPosition)
}

Skip buttons

OK, it’s time to implement the skip forward and back functionality. The media controller allows you to change the playback position directly. To perform a skip, you need to take the current playback position, add a plus or minus offset to get a new position, and then set the new position.

databinding.forwardButton.setOnClickListener {
  seekBy(30)
}
databinding.replayButton.setOnClickListener {
  seekBy(-10)
}

Scrubber control

There are a few steps required to make the scrubber functional:

.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
    mediaPlayer.duration.toLong())
private var episodeDuration: Long = 0
private fun updateControlsFromMetadata(metadata: MediaMetadataCompat) {
  episodeDuration = metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
  databinding.endTimeTextView.text = DateUtils.formatElapsedTime((episodeDuration / 1000))
}
metadata?.let { updateControlsFromMetadata(it) }
databinding.seekBar.max = episodeDuration.toInt()
private var draggingScrubber: Boolean = false
// 1
databinding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
  override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
        // 2
        databinding.currentTimeTextView.text = DateUtils.formatElapsedTime((progress / 1000).toLong())
  }

    override fun onStartTrackingTouch(seekBar: SeekBar) {
        // 3
        draggingScrubber = true
    }

    override fun onStopTrackingTouch(seekBar: SeekBar) {
        // 4
        draggingScrubber = false
        // 5
        val fragmentActivity = activity as FragmentActivity
        val controller = MediaControllerCompat.getMediaController(fragmentActivity)
        if (controller.playbackState != null) {
            // 6
            controller.transportControls.seekTo(seekBar.progress.toLong())
        } else {
            // 7
            seekBar.progress = 0
        }
    }
})
private var progressAnimator: ValueAnimator? = null
// 1
private fun animateScrubber(progress: Int, speed: Float) {
  // 2
  val timeRemaining = ((episodeDuration - progress) / speed).toInt()
  // 3
  if (timeRemaining < 0) {
    return;
  }
  // 4
  progressAnimator = ValueAnimator.ofInt(
      progress, episodeDuration.toInt())
  progressAnimator?.let { animator ->
    // 5
    animator.duration = timeRemaining.toLong()
    // 6
    animator.interpolator = LinearInterpolator()
    // 7
    animator.addUpdateListener {
      if (draggingScrubber) {
        // 8
        animator.cancel()
      } else {
        // 9
        databinding.seekBar.progress = animator.animatedValue as Int
      }
    }
    // 10
    animator.start()
  }
}
private fun handleStateChange(state: Int, position: Long, speed: Float) {
val progress = position.toInt()
databinding.seekBar.progress = progress
val speedButtonText = "${playerSpeed}x"
databinding.speedButton.text = speedButtonText

if (isPlaying) {
  animateScrubber(progress, speed)
}
progressAnimator?.let {
  it.cancel()
  progressAnimator = null
}
handleStateChange(state.state, state.position, state.playbackSpeed)
progressAnimator?.cancel()
private fun updateControlsFromController() {
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  if (controller != null) {
    val metadata = controller.metadata
    if (metadata != null) {
      handleStateChange(controller.playbackState.state,
          controller.playbackState.position, playerSpeed)
      updateControlsFromMetadata(controller.metadata)
    }
  }
}
updateControlsFromController()
updateControlsFromController()

Video playback

The last feature you’ll implement is video playback. If you try to play a video podcast with PodPlay now, only the audio part will play.

Identifying videos

The first thing you need is a means to identify if the episode media is a video. Open PodcastViewModel.kt and add the following to EpisodeViewData:

  var isVideo: Boolean = false
data class EpisodeViewData (
    var guid: String? = "",
    var title: String? = "",
    var description: String? = "",
    var mediaUrl: String? = "",
    var releaseDate: Date? = null,
    var duration: String? = "",
    var isVideo: Boolean = false      
)
return episodes.map {
  val isVideo = it.mimeType.startsWith("video")
  EpisodeViewData(it.guid, it.title, it.description, 
      it.mediaUrl, it.releaseDate, it.duration, isVideo)
}

Media session

You need a MediaSession object to manage the video playback.

private var mediaSession: MediaSessionCompat? = null
private fun initMediaSession() {
  if (mediaSession == null) {
    // 1
    mediaSession = MediaSessionCompat(activity as Context, 
        "EpisodePlayerFragment")
    // 2
    mediaSession?.setMediaButtonReceiver(null)
  }
  mediaSession?.let {
    registerMediaController(it.sessionToken)
  }
}

Media player

You also need a MediaPlayer object just like you did with the MediaBrowserService. Add the following property to EpisodePlayerFragment:

private var mediaPlayer: MediaPlayer? = null
private var playOnPrepare: Boolean = false
private fun setSurfaceSize() {
  // 1
  val mediaPlayer = mediaPlayer ?: return
  // 2
  val videoWidth = mediaPlayer.videoWidth
  val videoHeight = mediaPlayer.videoHeight
  // 3
  val parent = databinding.videoSurfaceView.parent as View
  val containerWidth = parent.width
  val containerHeight = parent.height
  // 4
  val layoutAspectRatio = containerWidth.toFloat() / 
      containerHeight
  val videoAspectRatio = videoWidth.toFloat() / videoHeight
  // 5
  val layoutParams = databinding.videoSurfaceView.layoutParams
  // 6
  if (videoAspectRatio > layoutAspectRatio) {
    layoutParams.height = 
        (containerWidth / videoAspectRatio).toInt()
  } else {
    layoutParams.width = 
        (containerHeight * videoAspectRatio).toInt()
  }
  // 7
  databinding.videoSurfaceView.layoutParams = layoutParams
}
private fun initMediaPlayer() {
  if (mediaPlayer == null) {
        // 1
        mediaPlayer = MediaPlayer()
        mediaPlayer?.let { mediaPlayer ->
          // 2
          mediaPlayer.setAudioAttributes(
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                .build()
          )
          // 3
          mediaPlayer.setDataSource(podcastViewModel.activeEpisodeViewData?.mediaUrl)
          // 4
          mediaPlayer.setOnPreparedListener {
            // 5
            val fragmentActivity = activity as FragmentActivity
            mediaSession?.let { mediaSession ->
                val episodeMediaCallback = PodplayMediaCallback(fragmentActivity, mediaSession, it)
                mediaSession.setCallback(episodeMediaCallback)
            }
            // 6
            setSurfaceSize()
            // 7
            if (playOnPrepare) {
                togglePlayPause()
            }
          }
          // 8
            mediaPlayer.prepareAsync()
        }
  } else {
        // 9
        setSurfaceSize()
  }
}
playOnPrepare = true

SurfaceView overview

Finally, add the following method to initialize the video surface and call the new initMediaPlayer method:

private fun initVideoPlayer() {
  // 1
  databinding.videoSurfaceView.visibility = View.VISIBLE
  // 2
  val surfaceHolder = databinding.videoSurfaceView.holder
  // 3
  surfaceHolder.addCallback(object: SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
      // 4
      initMediaPlayer()
      mediaPlayer?.setDisplay(holder)
    }
    override fun surfaceChanged(var1: SurfaceHolder, var2: Int,
        var3: Int, var4: Int) {
    }
    override fun surfaceDestroyed(var1: SurfaceHolder) {
    }
  })
}
private var isVideo: Boolean = false
isVideo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  podcastViewModel.activeEpisodeViewData?.isVideo ?: false
} else {
  false
}
if (!isVideo) {
  initMediaBrowser()
}
if (!isVideo) {
  if (mediaBrowser.isConnected) {
    val fragmentActivity = activity as FragmentActivity
    if (MediaControllerCompat.getMediaController(fragmentActivity) == null) {
      registerMediaController(mediaBrowser.sessionToken)
    }
    updateControlsFromController()
  } else {
    mediaBrowser.connect()
  }
}
if (isVideo) {
  mediaPlayer?.setDisplay(null)
}
if (isVideo) {
  initMediaSession()
  initVideoPlayer()
}
private fun setupVideoUI() {
  databinding.episodeDescTextView.visibility = View.INVISIBLE
  databinding.headerView.visibility = View.INVISIBLE
  val activity = activity as AppCompatActivity
  activity.supportActionBar?.hide()
  databinding.playerControls.setBackgroundColor(Color.argb(255/2, 0, 0, 0))
}
if (isVideo) {
  setupVideoUI()
}
if (!fragmentActivity.isChangingConfigurations) {
  mediaPlayer?.release()
  mediaPlayer = null
}
mediaPlayer?.let {
  updateControlsFromController()
}
private var mediaNeedsPrepare: Boolean = false
mediaNeedsPrepare = true
mediaPlayer.reset()
mediaPlayer.setDataSource(context, mediaUri)
mediaPlayer.prepare()
if (mediaNeedsPrepare) {
  mediaPlayer.reset()
  mediaPlayer.setDataSource(context, mediaUri)
  mediaPlayer.prepare()
}

Key Points

  • You used a number of built-in overrides for media session to control playback features such play / pause and seeking to various points in the media timeline.
  • You created custom commands to add other features such as changing playback speed.
  • Custom commands have a name and a Bundle object with the command parameters. onCommand() is called by the media session when a custom command is received.
  • You learned how to identify and playback video podcasts, making for a truly dynamic podcast experience!

Where to go from here?

Congratulations, you now have a fully functional podcast player worthy of praise and bragging rights! Pat yourself on the back because you’ve accomplished a lot.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now