From 1038ee33d209622717daf444ac6ce8fab05446a0 Mon Sep 17 00:00:00 2001 From: tibbi Date: Fri, 29 Jun 2018 12:00:12 +0200 Subject: [PATCH] fix #274, replace video MediaPlayer with ExoPlayer --- app/build.gradle | 1 + .../gallery/fragments/VideoFragment.kt | 289 ++++++++++-------- app/src/main/res/layout/pager_video_item.xml | 5 +- 3 files changed, 158 insertions(+), 137 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 047cd8b1a..43298c558 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,7 @@ dependencies { implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12' implementation 'com.github.chrisbanes:PhotoView:2.1.3' implementation 'com.android.support.constraint:constraint-layout:1.1.2' + implementation 'com.google.android.exoplayer:exoplayer-core:2.8.2' kapt "android.arch.persistence.room:compiler:1.1.1" implementation "android.arch.persistence.room:runtime:1.1.1" diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt index af7024ff4..6c476a8c2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt @@ -1,21 +1,31 @@ package com.simplemobiletools.gallery.fragments +import android.annotation.TargetApi import android.content.res.Configuration +import android.graphics.Point +import android.graphics.SurfaceTexture import android.media.AudioManager -import android.media.MediaPlayer import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler -import android.support.annotation.RequiresApi import android.util.DisplayMetrics import android.view.* import android.view.animation.AnimationUtils import android.widget.SeekBar import android.widget.TextView +import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory +import com.google.android.exoplayer2.source.ExtractorMediaSource +import com.google.android.exoplayer2.source.TrackGroupArray +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector +import com.google.android.exoplayer2.trackselection.TrackSelectionArray +import com.google.android.exoplayer2.upstream.DataSource +import com.google.android.exoplayer2.upstream.DataSpec +import com.google.android.exoplayer2.upstream.FileDataSource +import com.google.android.exoplayer2.video.VideoListener import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.isJellyBean1Plus -import com.simplemobiletools.gallery.BuildConfig import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.VideoActivity import com.simplemobiletools.gallery.extensions.* @@ -24,32 +34,28 @@ import com.simplemobiletools.gallery.helpers.MediaSideScroll import com.simplemobiletools.gallery.models.Medium import kotlinx.android.synthetic.main.pager_video_item.view.* import java.io.File -import java.io.IOException -class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSeekBarChangeListener { +class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, SeekBar.OnSeekBarChangeListener { private val PROGRESS = "progress" private val MIN_SKIP_LENGTH = 2000 + private val PLAY_PAUSE_VISIBLE_ALPHA = 0.8f - private var mMediaPlayer: MediaPlayer? = null - private var mSurfaceView: SurfaceView? = null - private var mSurfaceHolder: SurfaceHolder? = null + private var mTextureView: TextureView? = null private var mCurrTimeView: TextView? = null private var mTimerHandler: Handler? = null private var mSeekBar: SeekBar? = null private var mTimeHolder: View? = null private var mView: View? = null + private var mExoPlayer: SimpleExoPlayer? = null + private var mVideoSize = Point(0, 0) private var mIsPlaying = false private var mIsDragged = false private var mIsFullscreen = false private var mIsFragmentVisible = false - private var mPlayOnPrepare = false - private var wasEncoded = false - private var wasInit = false - private var isPrepared = false + private var mWasInit = false private var mCurrTime = 0 private var mDuration = 0 - private var mEncodedPath = "" private var mStoredShowExtendedDetails = false private var mStoredHideExtendedDetails = false @@ -77,7 +83,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } mIsFullscreen = activity!!.window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_FULLSCREEN == View.SYSTEM_UI_FLAG_FULLSCREEN - mView!!.video_play_outline.alpha = if (mIsFullscreen) 0f else 1f + mView!!.video_play_outline.alpha = if (mIsFullscreen) 0f else PLAY_PAUSE_VISIBLE_ALPHA setupPlayer() if (savedInstanceState != null) { @@ -85,7 +91,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } checkFullscreen() - wasInit = true + mWasInit = true mView!!.apply { brightnessSideScroll = video_brightness_controller @@ -102,6 +108,65 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee video_duration.setOnClickListener { skip(true) } } + mExoPlayer = ExoPlayerFactory.newSimpleInstance(context, DefaultTrackSelector()) + mExoPlayer!!.addListener(object : Player.EventListener { + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {} + + override fun onSeekProcessed() {} + + override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {} + + override fun onPlayerError(error: ExoPlaybackException?) { + activity?.showErrorToast(error.toString()) + } + + override fun onLoadingChanged(isLoading: Boolean) {} + + override fun onPositionDiscontinuity(reason: Int) {} + + override fun onRepeatModeChanged(repeatMode: Int) {} + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {} + + override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {} + + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { + when (playbackState) { + Player.STATE_READY -> videoPrepared() + Player.STATE_ENDED -> videoCompleted() + } + } + }) + + mExoPlayer!!.addVideoListener(object : VideoListener { + override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) { + mVideoSize.x = width + mVideoSize.y = height + setVideoSize() + } + + override fun onRenderedFirstFrame() {} + }) + + val uri = Uri.fromFile(File(medium.path)) + val dataSpec = DataSpec(uri) + val fileDataSource = FileDataSource() + try { + fileDataSource.open(dataSpec) + } catch (e: Exception) { + activity?.showErrorToast(e) + } + + val factory = DataSource.Factory { fileDataSource } + val audioSource = ExtractorMediaSource(fileDataSource.uri, factory, DefaultExtractorsFactory(), null, null) + mExoPlayer!!.audioStreamType = AudioManager.STREAM_MUSIC + mExoPlayer!!.prepare(audioSource) + medium.path.getVideoResolution()?.apply { + mVideoSize.x = x + mVideoSize.y = y + setVideoSize() + } + return mView } @@ -157,26 +222,23 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee mView!!.video_play_outline.setOnClickListener { togglePlayPause() } - mSurfaceView = mView!!.video_surface - mSurfaceHolder = mSurfaceView!!.holder - mSurfaceHolder!!.addCallback(this) - mSurfaceView!!.setOnClickListener { toggleFullscreen() } + mTextureView = mView!!.video_surface + mTextureView!!.setOnClickListener { toggleFullscreen() } + mTextureView!!.surfaceTextureListener = this mView!!.video_holder.setOnClickListener { toggleFullscreen() } initTimeHolder() checkExtendedDetails() - initMediaPlayer() } override fun setMenuVisibility(menuVisible: Boolean) { super.setMenuVisibility(menuVisible) if (mIsFragmentVisible && !menuVisible) { pauseVideo() - releaseMediaPlayer() } + mIsFragmentVisible = menuVisible - if (menuVisible && wasInit) { - initMediaPlayer() + if (menuVisible && mWasInit) { if (context?.config?.autoplayVideos == true) { playVideo() } @@ -259,8 +321,8 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee private fun setupTimer() { activity!!.runOnUiThread(object : Runnable { override fun run() { - if (mMediaPlayer != null && !mIsDragged && mIsPlaying) { - mCurrTime = mMediaPlayer!!.currentPosition / 1000 + if (mExoPlayer != null && !mIsDragged && mIsPlaying) { + mCurrTime = (mExoPlayer!!.currentPosition / 1000).toInt() mSeekBar!!.progress = mCurrTime mCurrTimeView!!.text = mCurrTime.getFormattedDuration() } @@ -299,8 +361,6 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee if (activity == null || !isAdded) return - initMediaPlayer() - mIsPlaying = !mIsPlaying if (mIsPlaying) { playVideo() @@ -310,102 +370,59 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } fun playVideo() { - if (mMediaPlayer != null && isPrepared) { - mIsPlaying = true - mMediaPlayer?.start() - } else { - mPlayOnPrepare = true + if (mExoPlayer == null) { + return } + if (videoEnded()) { + setProgress(0) + } + + mIsPlaying = true + mExoPlayer?.playWhenReady = true mView!!.video_play_outline.setImageDrawable(resources.getDrawable(R.drawable.img_pause_outline_big)) activity!!.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } private fun pauseVideo() { + if (mExoPlayer == null) { + return + } + mIsPlaying = false - mMediaPlayer?.pause() + if (!videoEnded()) { + mExoPlayer?.playWhenReady = false + } mView?.video_play_outline?.setImageDrawable(resources.getDrawable(R.drawable.img_play_outline_big)) activity!!.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } - private fun initMediaPlayer() { - if (mMediaPlayer != null || !mIsFragmentVisible) { - return - } - - val mediumPath = if (wasEncoded) mEncodedPath else getPathToLoad(medium) - - // this workaround is needed for example if the filename contains a colon - val fileUri = if (mediumPath.startsWith("/")) context!!.getFilePublicUri(File(mediumPath), BuildConfig.APPLICATION_ID) else Uri.parse(mediumPath) - try { - mMediaPlayer = MediaPlayer().apply { - setDataSource(context, fileUri) - setDisplay(mSurfaceHolder) - setOnCompletionListener { videoCompleted() } - setOnVideoSizeChangedListener { mediaPlayer, width, height -> setVideoSize() } - setOnPreparedListener { videoPrepared(it) } - setAudioStreamType(AudioManager.STREAM_MUSIC) - prepare() - } - } catch (e: IOException) { - mEncodedPath = Uri.encode(getPathToLoad(medium)) - if (wasEncoded) { - releaseMediaPlayer() - } else { - wasEncoded = true - mMediaPlayer = null - initMediaPlayer() - } - } catch (e: Exception) { - releaseMediaPlayer() - } - } + private fun videoEnded() = mExoPlayer!!.currentPosition >= mExoPlayer!!.duration private fun setProgress(seconds: Int) { - mMediaPlayer!!.seekTo(seconds * 1000) + mExoPlayer!!.seekTo(seconds * 1000L) mSeekBar!!.progress = seconds mCurrTimeView!!.text = seconds.getFormattedDuration() } - private fun addPreviewImage() { - mMediaPlayer!!.start() - mMediaPlayer!!.pause() - } + private fun videoPrepared() { + if (mDuration == 0) { + mDuration = (mExoPlayer!!.duration / 1000).toInt() + setupTimeHolder() + setProgress(mCurrTime) - private fun cleanup() { - pauseVideo() - mCurrTimeView?.text = 0.getFormattedDuration() - releaseMediaPlayer() - mSeekBar?.progress = 0 - mTimerHandler?.removeCallbacksAndMessages(null) - mSurfaceView = null - mSurfaceHolder?.removeCallback(this) - mSurfaceHolder = null - } - - private fun releaseMediaPlayer() { - mMediaPlayer?.setSurface(null) - mMediaPlayer?.release() - mMediaPlayer = null - } - - private fun videoPrepared(mediaPlayer: MediaPlayer) { - isPrepared = true - mDuration = mediaPlayer.duration / 1000 - addPreviewImage() - setupTimeHolder() - setProgress(mCurrTime) - - if (mIsFragmentVisible && (context!!.config.autoplayVideos || mPlayOnPrepare)) { - playVideo() + if (mIsFragmentVisible && (context!!.config.autoplayVideos)) { + playVideo() + } } } private fun videoCompleted() { - if (!isAdded) { + if (!isAdded || mExoPlayer == null) { return } + mCurrTime = (mExoPlayer!!.duration / 1000).toInt() if (listener?.videoEnded() == false && context!!.config.loopVideos) { playVideo() } else { @@ -415,37 +432,42 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } } - override fun surfaceCreated(holder: SurfaceHolder) { - mSurfaceHolder = holder - if (mIsFragmentVisible) { - initMediaPlayer() - } - } - - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - if (width != 0 && height != 0 && mSurfaceView != null) { - setVideoSize() - } - } - - override fun surfaceDestroyed(holder: SurfaceHolder) { + private fun cleanup() { + pauseVideo() + mCurrTimeView?.text = 0.getFormattedDuration() releaseMediaPlayer() + mSeekBar?.progress = 0 + mTimerHandler?.removeCallbacksAndMessages(null) + mTextureView = null } - @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private fun releaseMediaPlayer() { + mExoPlayer?.stop() + mExoPlayer?.release() + mExoPlayer = null + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) { + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) { + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean { + releaseMediaPlayer() + return false + } + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) { + mExoPlayer?.setVideoSurface(Surface(mTextureView!!.surfaceTexture)) + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private fun setVideoSize() { - if (mSurfaceHolder == null) - mSurfaceHolder = mSurfaceView!!.holder - - if (activity == null || mSurfaceHolder == null || !mSurfaceHolder!!.surface.isValid) + if (activity == null || mTextureView == null) return - initMediaPlayer() - if (mMediaPlayer == null) { - return - } - - val videoProportion = mMediaPlayer!!.videoWidth.toFloat() / mMediaPlayer!!.videoHeight.toFloat() + val videoProportion = mVideoSize.x.toFloat() / mVideoSize.y.toFloat() val display = activity!!.windowManager.defaultDisplay val screenWidth: Int val screenHeight: Int @@ -462,7 +484,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee val screenProportion = screenWidth.toFloat() / screenHeight.toFloat() - mSurfaceView!!.layoutParams.apply { + mTextureView!!.layoutParams.apply { if (videoProportion > screenProportion) { width = screenWidth height = (screenWidth.toFloat() / videoProportion).toInt() @@ -470,7 +492,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee width = (videoProportion * screenHeight.toFloat()).toInt() height = screenHeight } - mSurfaceView!!.layoutParams = this + mTextureView!!.layoutParams = this } } @@ -496,15 +518,15 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } private fun skip(forward: Boolean) { - if (mMediaPlayer == null) { + if (mExoPlayer == null) { return } - val curr = mMediaPlayer!!.currentPosition - val twoPercents = Math.max(mMediaPlayer!!.duration / 50, MIN_SKIP_LENGTH) + val curr = mExoPlayer!!.currentPosition + val twoPercents = Math.max((mExoPlayer!!.duration / 50).toInt(), MIN_SKIP_LENGTH) val newProgress = if (forward) curr + twoPercents else curr - twoPercents val roundProgress = Math.round(newProgress / 1000f) - val limitedProgress = Math.max(Math.min(mMediaPlayer!!.duration / 1000, roundProgress), 0) + val limitedProgress = Math.max(Math.min(mExoPlayer!!.duration.toInt(), roundProgress), 0) setProgress(limitedProgress) if (!mIsPlaying) { togglePlayPause() @@ -512,17 +534,16 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - if (mMediaPlayer != null && fromUser) { + if (mExoPlayer != null && fromUser) { setProgress(progress) } } override fun onStartTrackingTouch(seekBar: SeekBar) { - initMediaPlayer() - if (mMediaPlayer == null) + if (mExoPlayer == null) return - mMediaPlayer!!.pause() + mExoPlayer!!.playWhenReady = false mIsDragged = true } @@ -530,7 +551,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee if (!mIsPlaying) { togglePlayPause() } else { - mMediaPlayer?.start() + mExoPlayer!!.playWhenReady = true } mIsDragged = false @@ -549,7 +570,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } } - mView!!.video_play_outline.animate().alpha(if (isFullscreen) 0f else 1f).start() + mView!!.video_play_outline.animate().alpha(if (isFullscreen) 0f else PLAY_PAUSE_VISIBLE_ALPHA).start() } private fun getExtendedDetailsY(height: Int): Float { diff --git a/app/src/main/res/layout/pager_video_item.xml b/app/src/main/res/layout/pager_video_item.xml index b7658ca01..03014aa82 100644 --- a/app/src/main/res/layout/pager_video_item.xml +++ b/app/src/main/res/layout/pager_video_item.xml @@ -6,12 +6,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_centerInParent="true"/>