1

I have the following fragment handling a ViewPager2 which creates fragments (VideoFragment) on which a video is shown via ExoPlayer:

private const val IMMERSIVE_FLAG_TIMEOUT = 500L
class VideoGalleryFragment : Fragment() {

    private lateinit var binding: FragmentVideoGalleryBinding
    private lateinit var mediaList: MutableList<File>
    private lateinit var mediaViewPager: ViewPager2

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private val videoGalleryFragmentViewModel by viewModels<VideoGalleryFragmentViewModel> { viewModelFactory }

    private val args by navArgs<VideoGalleryFragmentArgs>()

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

        // Mark this as a retain fragment, so the lifecycle does not get restarted on config change
        retainInstance = true

        // Get root directory of media
        //outputDirectory = getOutputDirectory(requireContext())
        val rootDirectory = File(args.rootDirectory)


        // Walk through all files in the root directory
        // We reverse the order of the list to present the last photos first
        mediaList = rootDirectory.listFiles { file ->
            VIDEO_EXTENSION_WHITELIST.contains(file.extension.toUpperCase(Locale.ROOT))
        }?.sortedDescending()?.toMutableList() ?: mutableListOf()

    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        // Inflate the layout for this fragment
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_video_gallery, container, false)
        binding.apply {
            lifecycleOwner = viewLifecycleOwner
            viewModel = videoGalleryFragmentViewModel
        }

        return binding.root
    }

    // HERE IS MY VIEWPAGER2 SETUP
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mediaViewPager = binding.videoViewPager.apply {
            offscreenPageLimit = 2
            adapter = object: FragmentStateAdapter(this@VideoGalleryFragment) {
                override fun getItemCount(): Int = mediaList.size

                override fun createFragment(position: Int): Fragment =
                    VideoFragment.create(
                        mediaList[position]
                    )

            }
            setPageTransformer(DepthPageTransformer())
        }


        }
    }



    override fun onResume() {
        super.onResume()
        /*
        * Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may
        * be trying to set app to immersive mode before it's ready and the flags do not stick
        * */
        binding.container.postDelayed({
            binding.container.systemUiVisibility =
                FLAGS_FULLSCREEN
        }, IMMERSIVE_FLAG_TIMEOUT)
    }
}

And here is the Fragment class (used by the ViewPager2) showing the videos via ExoPlayer2:

class VideoFragment : Fragment() {

    private var _binding: FragmentVideoBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    private var player: SimpleExoPlayer? = null

    private var playWhenReady: Boolean? = true
    private var currentWindow: Int? = 0
    private var playbackPosition : Long? = 0
    private var resource: String? = null


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

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

        val args = arguments ?: return
        args.getString(VideoFragment.VIDEO_FILE_NAME_KEY)?.let {
            resource = it
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        releasePlayer()
        _binding = null
    }

    override fun onStart() {
        super.onStart()
        if(Util.SDK_INT >= 24){
            initializePlayer(resource)
        }
    }

    override fun onResume() {
        super.onResume()
        if((Util.SDK_INT < 24 || player == null)){
            initializePlayer(resource)
        }
    }

    private fun initializePlayer(videoFilePath: String?){

        player = ExoPlayerFactory.newSimpleInstance(activity)




        binding.videoViewGallery.player = player


        videoFilePath?.let {
            val mediaSource: MediaSource =
                ProgressiveMediaSource.Factory(DefaultDataSourceFactory(activity, "exoplayer-mylim"))
                    .createMediaSource(Uri.parse(it))


            playWhenReady?.let { playWhenReady ->
                (player as SimpleExoPlayer).playWhenReady = playWhenReady
            }



            currentWindow?.let { currentWindow ->
                playbackPosition?.let {playbackPosition ->
                    (player as SimpleExoPlayer).seekTo(currentWindow, playbackPosition)
                }
            }



            (player as SimpleExoPlayer).prepare(mediaSource, false, false)
        }
    }

    private fun releasePlayer(){
        player?.stop()
        player?.release()
        player = null
    }

    override fun onPause() {
        super.onPause()
        if(Util.SDK_INT < 24){
            releasePlayer()
        }
    }

    override fun onStop() {
        super.onStop()
        if(Util.SDK_INT >= 24){
            releasePlayer()
        }
    }
    companion object {
        private const val VIDEO_FILE_NAME_KEY = "video_file_name"

        fun create(video: File) = VideoFragment().apply {
            arguments = Bundle().apply {
                putString(VIDEO_FILE_NAME_KEY, video.absolutePath)
            }
        }

    }
}

My problem is that when I switch between the VideoFragments by swiping, the previous Exoplayer is not released properly so that I can still hear the audio of the first video after starting a 2nd video.

Although I have put a releasePlayer() method for releasing the player into the appropriate lifecycle methods, it seems to me that ViewPager does not care about that.

How can I stop/release an ExoPlayer instance when used with ViewPager?

halfer
  • 19,824
  • 17
  • 99
  • 186
ebeninki
  • 909
  • 1
  • 12
  • 34

2 Answers2

1

With ViewPager2, your Fragment can be pre-loaded before it is actually displayed to the user. One way to solve your problem is to listen for page changes and releasePlayer() from there, rather than waiting for the Fragment to be destroyed. See the documentation for ViewPager2.OnPageChangeCallback here

zkovar
  • 11
  • 1
0

I know it is late But Might help someone, I also ran into the same issue. The solution lies in using StatePagerAdapter.

    class ViewPagerAdapter extends FragmentStatePagerAdapter {

    public ViewPagerAdapter(FragmentManager manager) {
        super(manager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public void addFragment(Fragment fragment) {
        mFragmentList.add(fragment);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragmentTitleList.get(position);
    }
}

Old That I was Using

    class ViewPagerAdapter extends FragmentPagerAdapter {

    public ViewPagerAdapter(FragmentManager manager) {
        super(manager);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public void addFragment(Fragment fragment) {
        mFragmentList.add(fragment);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragmentTitleList.get(position);
    }
} 

The Difference is on Parent Classes

FragmentStatePagerAdapter <== use this Now

 class ViewPagerAdapter extends FragmentStatePagerAdapter {

public ViewPagerAdapter(FragmentManager manager) {
    super(manager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}

FragmentPagerAdapter <== I was using

class ViewPagerAdapter extends FragmentPagerAdapter {

public ViewPagerAdapter(FragmentManager manager) {
    super(manager);
}

FragmentPagerAdapter this provides a method which is now deprecated can be seen below:

         @Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);

    if (isVisibleToUser){
        if (!loaded){
            loadStatus();
        }
    }
}

this was used to manage resources/play/pause based on if a fragment is visible to the user in the View Pager. Deprecated

Now If you want to handle resources/state of the fragment you should use State Pager Adapter as I have described above. Implementing StatePagerAdapter calls onPause() and onResume() methods of a fragment Based on Its Visibility to User.

So answer to the question is Implement Adapter Like StatePagerAdapter and pause the player on Fragment's onPause() method and Resume it in the onResume() method.

MRamzan
  • 161
  • 2
  • 7