Home Android & Kotlin Books Android Apprentice

27
Episode Player Written by Tom Blankenship

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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"
  tools:context="com.raywenderlich.podplay.ui.EpisodePlayerFragment">

  <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>
<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"/>

<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"
    tools:text="Episode Title"/>
<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"/>

<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"/>

<Button
    android:id="@+id/speedButton"
    android:layout_width="54dp"
    android:layout_height="34dp"
    android:layout_marginEnd="8dp"
    android:layout_marginTop="8dp"
    android:background="@android:color/transparent"
    android:text="1x"
    android:textColor="@android:color/white"
    android:textSize="14sp"
    android:textAllCaps="false"
    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="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="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() {

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

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

  override fun onCreateView(inflater: LayoutInflater,
                            container: ViewGroup?,
                            savedInstanceState: Bundle?): View?{
    return inflater.inflate(R.layout.fragment_episode_player,
        container, false)
  }

  override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(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()
  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
  episodeTitleTextView.text =
      podcastViewModel.activeEpisodeViewData?.title

  // 2
  val htmlDesc =
      podcastViewModel.activeEpisodeViewData?.description ?: ""
  val descSpan = HtmlUtils.htmlToSpannable(htmlDesc)
  episodeDescTextView.text = descSpan
  episodeDescTextView.movementMethod = ScrollingMovementMethod()

  // 3
  val fragmentActivity = activity as FragmentActivity
  Glide.with(fragmentActivity)
    .load(podcastViewModel.activePodcastViewData?.imageUrl)
    .into(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() {
  playToggleButton.setOnClickListener {
    togglePlayPause()
  }
}
private fun handleStateChange(state: Int) {
  val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
  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
  speedButton.text = "${playerSpeed}x"
}
speedButton.text = "${playerSpeed}x"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  speedButton.setOnClickListener {
    changeSpeed()
  }
} else {
  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.

forwardButton.setOnClickListener {
  seekBy(30)
}
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)
  endTimeTextView.text = DateUtils.formatElapsedTime(
      episodeDuration / 1000)
}
metadata?.let { updateControlsFromMetadata(it) }
seekBar.max = episodeDuration.toInt()
private var draggingScrubber: Boolean = false
// 1
seekBar.setOnSeekBarChangeListener(
    object : SeekBar.OnSeekBarChangeListener {
  override fun onProgressChanged(seekBar: SeekBar, progress: Int,
      fromUser: Boolean) {
    // 2
    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
        seekBar.progress = animator.animatedValue as Int
      }
    }
    // 10
    animator.start()
  }
}
private fun handleStateChange(state: Int, position: Long, speed: Float) {
val progress = position.toInt()
seekBar.progress = progress
speedButton.text = "${playerSpeed}x"

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)
  }
  registerMediaController(mediaSession!!.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 = 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 = videoSurfaceView.layoutParams
  // 6
  if (videoAspectRatio > layoutAspectRatio) {
    layoutParams.height = 
        (containerWidth / videoAspectRatio).toInt()
  } else {
    layoutParams.width = 
        (containerHeight * videoAspectRatio).toInt()
  }
  // 7
  videoSurfaceView.layoutParams = layoutParams
}
private fun initMediaPlayer() {
  if (mediaPlayer == null) {
    // 1
    mediaPlayer = MediaPlayer()
    mediaPlayer?.let {
      // 2
      it.setAudioStreamType(AudioManager.STREAM_MUSIC)
      // 3
      it.setDataSource(
          podcastViewModel.activeEpisodeViewData?.mediaUrl)
      // 4
      it.setOnPreparedListener {
        // 5
        val fragmentActivity = activity as FragmentActivity
        val episodeMediaCallback = PodplayMediaCallback(
            fragmentActivity, mediaSession!!, it)
        mediaSession!!.setCallback(episodeMediaCallback)
        // 6
        setSurfaceSize()
        // 7
        if (playOnPrepare) {
          togglePlayPause()
        }
      }
      // 8
      it.prepareAsync()
    }
  } else {
    // 9
    setSurfaceSize()
  }
}
playOnPrepare = true
private fun initVideoPlayer() {
  // 1
  videoSurfaceView.visibility = View.VISIBLE
  // 2
  val surfaceHolder = 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) {
    }
  })
}

SurfaceView overview

This method warrants some explanation on how surface views interact with the media player. To display videos, the MediaPlayer object requires access to a SurfaceView. Surface views provide a dedicated drawing surface within your view hierarchy.

private var isVideo: Boolean = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  isVideo = podcastViewModel.activeEpisodeViewData?.isVideo ?: false
} else {
  isVideo = 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() {
  episodeDescTextView.visibility = View.INVISIBLE
  headerView.visibility = View.INVISIBLE
  val activity = activity as AppCompatActivity
  activity.supportActionBar?.hide()
  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()
}

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.