fix #274, replace video MediaPlayer with ExoPlayer

This commit is contained in:
tibbi 2018-06-29 12:00:12 +02:00
parent ebaf64af70
commit 1038ee33d2
3 changed files with 158 additions and 137 deletions

View file

@ -54,6 +54,7 @@ dependencies {
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12'
implementation 'com.github.chrisbanes:PhotoView:2.1.3' implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2' 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" kapt "android.arch.persistence.room:compiler:1.1.1"
implementation "android.arch.persistence.room:runtime:1.1.1" implementation "android.arch.persistence.room:runtime:1.1.1"

View file

@ -1,21 +1,31 @@
package com.simplemobiletools.gallery.fragments package com.simplemobiletools.gallery.fragments
import android.annotation.TargetApi
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Point
import android.graphics.SurfaceTexture
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.support.annotation.RequiresApi
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.* import android.view.*
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.TextView 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.extensions.*
import com.simplemobiletools.commons.helpers.isJellyBean1Plus import com.simplemobiletools.commons.helpers.isJellyBean1Plus
import com.simplemobiletools.gallery.BuildConfig
import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.VideoActivity import com.simplemobiletools.gallery.activities.VideoActivity
import com.simplemobiletools.gallery.extensions.* import com.simplemobiletools.gallery.extensions.*
@ -24,32 +34,28 @@ import com.simplemobiletools.gallery.helpers.MediaSideScroll
import com.simplemobiletools.gallery.models.Medium import com.simplemobiletools.gallery.models.Medium
import kotlinx.android.synthetic.main.pager_video_item.view.* import kotlinx.android.synthetic.main.pager_video_item.view.*
import java.io.File 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 PROGRESS = "progress"
private val MIN_SKIP_LENGTH = 2000 private val MIN_SKIP_LENGTH = 2000
private val PLAY_PAUSE_VISIBLE_ALPHA = 0.8f
private var mMediaPlayer: MediaPlayer? = null private var mTextureView: TextureView? = null
private var mSurfaceView: SurfaceView? = null
private var mSurfaceHolder: SurfaceHolder? = null
private var mCurrTimeView: TextView? = null private var mCurrTimeView: TextView? = null
private var mTimerHandler: Handler? = null private var mTimerHandler: Handler? = null
private var mSeekBar: SeekBar? = null private var mSeekBar: SeekBar? = null
private var mTimeHolder: View? = null private var mTimeHolder: View? = null
private var mView: View? = null private var mView: View? = null
private var mExoPlayer: SimpleExoPlayer? = null
private var mVideoSize = Point(0, 0)
private var mIsPlaying = false private var mIsPlaying = false
private var mIsDragged = false private var mIsDragged = false
private var mIsFullscreen = false private var mIsFullscreen = false
private var mIsFragmentVisible = false private var mIsFragmentVisible = false
private var mPlayOnPrepare = false private var mWasInit = false
private var wasEncoded = false
private var wasInit = false
private var isPrepared = false
private var mCurrTime = 0 private var mCurrTime = 0
private var mDuration = 0 private var mDuration = 0
private var mEncodedPath = ""
private var mStoredShowExtendedDetails = false private var mStoredShowExtendedDetails = false
private var mStoredHideExtendedDetails = 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 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() setupPlayer()
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -85,7 +91,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
} }
checkFullscreen() checkFullscreen()
wasInit = true mWasInit = true
mView!!.apply { mView!!.apply {
brightnessSideScroll = video_brightness_controller brightnessSideScroll = video_brightness_controller
@ -102,6 +108,65 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
video_duration.setOnClickListener { skip(true) } 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 return mView
} }
@ -157,26 +222,23 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
mView!!.video_play_outline.setOnClickListener { togglePlayPause() } mView!!.video_play_outline.setOnClickListener { togglePlayPause() }
mSurfaceView = mView!!.video_surface mTextureView = mView!!.video_surface
mSurfaceHolder = mSurfaceView!!.holder mTextureView!!.setOnClickListener { toggleFullscreen() }
mSurfaceHolder!!.addCallback(this) mTextureView!!.surfaceTextureListener = this
mSurfaceView!!.setOnClickListener { toggleFullscreen() }
mView!!.video_holder.setOnClickListener { toggleFullscreen() } mView!!.video_holder.setOnClickListener { toggleFullscreen() }
initTimeHolder() initTimeHolder()
checkExtendedDetails() checkExtendedDetails()
initMediaPlayer()
} }
override fun setMenuVisibility(menuVisible: Boolean) { override fun setMenuVisibility(menuVisible: Boolean) {
super.setMenuVisibility(menuVisible) super.setMenuVisibility(menuVisible)
if (mIsFragmentVisible && !menuVisible) { if (mIsFragmentVisible && !menuVisible) {
pauseVideo() pauseVideo()
releaseMediaPlayer()
} }
mIsFragmentVisible = menuVisible mIsFragmentVisible = menuVisible
if (menuVisible && wasInit) { if (menuVisible && mWasInit) {
initMediaPlayer()
if (context?.config?.autoplayVideos == true) { if (context?.config?.autoplayVideos == true) {
playVideo() playVideo()
} }
@ -259,8 +321,8 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
private fun setupTimer() { private fun setupTimer() {
activity!!.runOnUiThread(object : Runnable { activity!!.runOnUiThread(object : Runnable {
override fun run() { override fun run() {
if (mMediaPlayer != null && !mIsDragged && mIsPlaying) { if (mExoPlayer != null && !mIsDragged && mIsPlaying) {
mCurrTime = mMediaPlayer!!.currentPosition / 1000 mCurrTime = (mExoPlayer!!.currentPosition / 1000).toInt()
mSeekBar!!.progress = mCurrTime mSeekBar!!.progress = mCurrTime
mCurrTimeView!!.text = mCurrTime.getFormattedDuration() mCurrTimeView!!.text = mCurrTime.getFormattedDuration()
} }
@ -299,8 +361,6 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
if (activity == null || !isAdded) if (activity == null || !isAdded)
return return
initMediaPlayer()
mIsPlaying = !mIsPlaying mIsPlaying = !mIsPlaying
if (mIsPlaying) { if (mIsPlaying) {
playVideo() playVideo()
@ -310,102 +370,59 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
} }
fun playVideo() { fun playVideo() {
if (mMediaPlayer != null && isPrepared) { if (mExoPlayer == null) {
mIsPlaying = true return
mMediaPlayer?.start()
} else {
mPlayOnPrepare = true
} }
if (videoEnded()) {
setProgress(0)
}
mIsPlaying = true
mExoPlayer?.playWhenReady = true
mView!!.video_play_outline.setImageDrawable(resources.getDrawable(R.drawable.img_pause_outline_big)) mView!!.video_play_outline.setImageDrawable(resources.getDrawable(R.drawable.img_pause_outline_big))
activity!!.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) activity!!.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
private fun pauseVideo() { private fun pauseVideo() {
if (mExoPlayer == null) {
return
}
mIsPlaying = false mIsPlaying = false
mMediaPlayer?.pause() if (!videoEnded()) {
mExoPlayer?.playWhenReady = false
}
mView?.video_play_outline?.setImageDrawable(resources.getDrawable(R.drawable.img_play_outline_big)) mView?.video_play_outline?.setImageDrawable(resources.getDrawable(R.drawable.img_play_outline_big))
activity!!.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) activity!!.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
private fun initMediaPlayer() { private fun videoEnded() = mExoPlayer!!.currentPosition >= mExoPlayer!!.duration
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 setProgress(seconds: Int) { private fun setProgress(seconds: Int) {
mMediaPlayer!!.seekTo(seconds * 1000) mExoPlayer!!.seekTo(seconds * 1000L)
mSeekBar!!.progress = seconds mSeekBar!!.progress = seconds
mCurrTimeView!!.text = seconds.getFormattedDuration() mCurrTimeView!!.text = seconds.getFormattedDuration()
} }
private fun addPreviewImage() { private fun videoPrepared() {
mMediaPlayer!!.start() if (mDuration == 0) {
mMediaPlayer!!.pause() mDuration = (mExoPlayer!!.duration / 1000).toInt()
} setupTimeHolder()
setProgress(mCurrTime)
private fun cleanup() { if (mIsFragmentVisible && (context!!.config.autoplayVideos)) {
pauseVideo() playVideo()
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()
} }
} }
private fun videoCompleted() { private fun videoCompleted() {
if (!isAdded) { if (!isAdded || mExoPlayer == null) {
return return
} }
mCurrTime = (mExoPlayer!!.duration / 1000).toInt()
if (listener?.videoEnded() == false && context!!.config.loopVideos) { if (listener?.videoEnded() == false && context!!.config.loopVideos) {
playVideo() playVideo()
} else { } else {
@ -415,37 +432,42 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
} }
} }
override fun surfaceCreated(holder: SurfaceHolder) { private fun cleanup() {
mSurfaceHolder = holder pauseVideo()
if (mIsFragmentVisible) { mCurrTimeView?.text = 0.getFormattedDuration()
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) {
releaseMediaPlayer() 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() { private fun setVideoSize() {
if (mSurfaceHolder == null) if (activity == null || mTextureView == null)
mSurfaceHolder = mSurfaceView!!.holder
if (activity == null || mSurfaceHolder == null || !mSurfaceHolder!!.surface.isValid)
return return
initMediaPlayer() val videoProportion = mVideoSize.x.toFloat() / mVideoSize.y.toFloat()
if (mMediaPlayer == null) {
return
}
val videoProportion = mMediaPlayer!!.videoWidth.toFloat() / mMediaPlayer!!.videoHeight.toFloat()
val display = activity!!.windowManager.defaultDisplay val display = activity!!.windowManager.defaultDisplay
val screenWidth: Int val screenWidth: Int
val screenHeight: Int val screenHeight: Int
@ -462,7 +484,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
val screenProportion = screenWidth.toFloat() / screenHeight.toFloat() val screenProportion = screenWidth.toFloat() / screenHeight.toFloat()
mSurfaceView!!.layoutParams.apply { mTextureView!!.layoutParams.apply {
if (videoProportion > screenProportion) { if (videoProportion > screenProportion) {
width = screenWidth width = screenWidth
height = (screenWidth.toFloat() / videoProportion).toInt() height = (screenWidth.toFloat() / videoProportion).toInt()
@ -470,7 +492,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
width = (videoProportion * screenHeight.toFloat()).toInt() width = (videoProportion * screenHeight.toFloat()).toInt()
height = screenHeight height = screenHeight
} }
mSurfaceView!!.layoutParams = this mTextureView!!.layoutParams = this
} }
} }
@ -496,15 +518,15 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
} }
private fun skip(forward: Boolean) { private fun skip(forward: Boolean) {
if (mMediaPlayer == null) { if (mExoPlayer == null) {
return return
} }
val curr = mMediaPlayer!!.currentPosition val curr = mExoPlayer!!.currentPosition
val twoPercents = Math.max(mMediaPlayer!!.duration / 50, MIN_SKIP_LENGTH) val twoPercents = Math.max((mExoPlayer!!.duration / 50).toInt(), MIN_SKIP_LENGTH)
val newProgress = if (forward) curr + twoPercents else curr - twoPercents val newProgress = if (forward) curr + twoPercents else curr - twoPercents
val roundProgress = Math.round(newProgress / 1000f) 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) setProgress(limitedProgress)
if (!mIsPlaying) { if (!mIsPlaying) {
togglePlayPause() togglePlayPause()
@ -512,17 +534,16 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
} }
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (mMediaPlayer != null && fromUser) { if (mExoPlayer != null && fromUser) {
setProgress(progress) setProgress(progress)
} }
} }
override fun onStartTrackingTouch(seekBar: SeekBar) { override fun onStartTrackingTouch(seekBar: SeekBar) {
initMediaPlayer() if (mExoPlayer == null)
if (mMediaPlayer == null)
return return
mMediaPlayer!!.pause() mExoPlayer!!.playWhenReady = false
mIsDragged = true mIsDragged = true
} }
@ -530,7 +551,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee
if (!mIsPlaying) { if (!mIsPlaying) {
togglePlayPause() togglePlayPause()
} else { } else {
mMediaPlayer?.start() mExoPlayer!!.playWhenReady = true
} }
mIsDragged = false 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 { private fun getExtendedDetailsY(height: Int): Float {

View file

@ -6,12 +6,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<SurfaceView <TextureView
android:id="@+id/video_surface" android:id="@+id/video_surface"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"/>
android:background="@android:color/transparent"/>
<com.simplemobiletools.gallery.helpers.MediaSideScroll <com.simplemobiletools.gallery.helpers.MediaSideScroll
android:id="@+id/video_volume_controller" android:id="@+id/video_volume_controller"