382 lines
13 KiB
Kotlin
382 lines
13 KiB
Kotlin
package org.mariotaku.twidere.fragment
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
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.util.Pair
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.widget.MediaController
|
|
import android.widget.ProgressBar
|
|
import android.widget.SeekBar
|
|
import android.widget.TextView
|
|
import com.commonsware.cwac.layouts.AspectLockedFrameLayout.AspectRatioSource
|
|
import edu.tsinghua.hotmobi.HotMobiLogger
|
|
import edu.tsinghua.hotmobi.model.MediaDownloadEvent
|
|
import kotlinx.android.synthetic.main.layout_media_viewer_texture_video_view.*
|
|
import org.mariotaku.mediaviewer.library.CacheDownloadLoader
|
|
import org.mariotaku.mediaviewer.library.CacheDownloadMediaViewerFragment
|
|
import org.mariotaku.mediaviewer.library.subsampleimageview.SubsampleImageViewerFragment
|
|
import org.mariotaku.twidere.R
|
|
import org.mariotaku.twidere.TwidereConstants.EXTRA_ACCOUNT_KEY
|
|
import org.mariotaku.twidere.TwidereConstants.EXTRA_MEDIA
|
|
import org.mariotaku.twidere.activity.MediaViewerActivity
|
|
import org.mariotaku.twidere.activity.iface.IControlBarActivity
|
|
import org.mariotaku.twidere.model.ParcelableMedia
|
|
import org.mariotaku.twidere.model.UserKey
|
|
import org.mariotaku.twidere.util.media.MediaExtra
|
|
import java.util.*
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
class VideoPageFragment : CacheDownloadMediaViewerFragment(), MediaPlayer.OnPreparedListener,
|
|
MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, View.OnClickListener, IControlBarActivity.ControlBarOffsetListener {
|
|
|
|
private var playAudio: Boolean = false
|
|
private var mediaPlayer: MediaPlayer? = null
|
|
private var mediaPlayerError: Int = 0
|
|
private var videoProgressRunnable: VideoPlayProgressRunnable? = null
|
|
private var mediaDownloadEvent: MediaDownloadEvent? = null
|
|
|
|
private val isLoopEnabled: Boolean get() = arguments.getBoolean(EXTRA_LOOP, false)
|
|
|
|
private val media: ParcelableMedia? get() = arguments.getParcelable<ParcelableMedia>(EXTRA_MEDIA)
|
|
|
|
private val accountKey: UserKey get() = arguments.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)
|
|
|
|
private var aspectRatioSource = object : AspectRatioSource {
|
|
override fun getHeight(): Int {
|
|
val height = media?.height ?: 0
|
|
if (height <= 0) return view!!.measuredHeight
|
|
return height
|
|
}
|
|
|
|
override fun getWidth(): Int {
|
|
val width = media?.width ?: 0
|
|
if (width <= 0) return view!!.measuredWidth
|
|
return width
|
|
}
|
|
|
|
}
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
super.onActivityCreated(savedInstanceState)
|
|
setHasOptionsMenu(true)
|
|
|
|
var handler: Handler? = videoViewProgress.handler
|
|
if (handler == null) {
|
|
handler = Handler(activity.mainLooper)
|
|
}
|
|
|
|
val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
|
|
// Play audio by default if ringer mode on
|
|
playAudio = am.ringerMode == AudioManager.RINGER_MODE_NORMAL
|
|
|
|
videoProgressRunnable = VideoPlayProgressRunnable(handler, videoViewProgress,
|
|
durationLabel, positionLabel, videoView)
|
|
|
|
|
|
videoViewOverlay.setOnClickListener(this)
|
|
videoView.setOnPreparedListener(this)
|
|
videoView.setOnErrorListener(this)
|
|
videoView.setOnCompletionListener(this)
|
|
|
|
playPauseButton.setOnClickListener(this)
|
|
volumeButton.setOnClickListener(this)
|
|
videoControl.visibility = View.GONE
|
|
videoContainer.setAspectRatioSource(aspectRatioSource)
|
|
videoViewProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
private var paused: Boolean = false
|
|
|
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
if (!fromUser) return
|
|
val mp = mediaPlayer ?: return
|
|
val duration = mp.duration
|
|
if (duration <= 0) return
|
|
mp.seekTo(Math.round(duration * (progress.toFloat() / seekBar.max)))
|
|
}
|
|
|
|
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
|
paused = pauseVideo()
|
|
}
|
|
|
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
|
if (paused) {
|
|
resumeVideo()
|
|
}
|
|
}
|
|
|
|
})
|
|
startLoading(false)
|
|
setMediaViewVisible(false)
|
|
updateVolume()
|
|
}
|
|
|
|
|
|
override fun onAttach(context: Context?) {
|
|
super.onAttach(context)
|
|
if (context is IControlBarActivity) {
|
|
context.registerControlBarOffsetListener(this)
|
|
}
|
|
}
|
|
|
|
override fun onDetach() {
|
|
val activity = activity
|
|
if (activity is IControlBarActivity) {
|
|
activity.unregisterControlBarOffsetListener(this)
|
|
}
|
|
super.onDetach()
|
|
}
|
|
|
|
override fun getDownloadExtra(): Any? {
|
|
val extra = MediaExtra()
|
|
extra.isUseThumbor = false
|
|
val fallbackUrlAndType = getBestVideoUrlAndType(media, FALLBACK_VIDEO_TYPES)
|
|
if (fallbackUrlAndType != null) {
|
|
extra.fallbackUrl = fallbackUrlAndType.first
|
|
}
|
|
return extra
|
|
}
|
|
|
|
override fun isAbleToLoad(): Boolean {
|
|
return downloadUri != null
|
|
}
|
|
|
|
|
|
override fun getDownloadUri(): Uri? {
|
|
val bestVideoUrlAndType = getBestVideoUrlAndType(media, SUPPORTED_VIDEO_TYPES)
|
|
if (bestVideoUrlAndType != null && bestVideoUrlAndType.first != null) {
|
|
return Uri.parse(bestVideoUrlAndType.first)
|
|
}
|
|
return arguments.getParcelable<Uri>(SubsampleImageViewerFragment.EXTRA_MEDIA_URI)
|
|
}
|
|
|
|
override fun displayMedia(result: CacheDownloadLoader.Result) {
|
|
videoView.setVideoURI(result.cacheUri)
|
|
videoControl.visibility = View.GONE
|
|
setMediaViewVisible(true)
|
|
activity.supportInvalidateOptionsMenu()
|
|
}
|
|
|
|
override fun recycleMedia() {
|
|
|
|
}
|
|
|
|
override fun onCompletion(mp: MediaPlayer) {
|
|
updatePlayerState()
|
|
}
|
|
|
|
override fun onControlBarOffsetChanged(activity: IControlBarActivity, offset: Float) {
|
|
videoControl.translationY = (1 - offset) * videoControl.height
|
|
}
|
|
|
|
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
|
mediaPlayer = null
|
|
videoViewProgress.removeCallbacks(videoProgressRunnable)
|
|
videoViewProgress.visibility = View.GONE
|
|
videoControl.visibility = View.GONE
|
|
mediaPlayerError = what
|
|
return true
|
|
}
|
|
|
|
override fun onPrepared(mp: MediaPlayer) {
|
|
if (userVisibleHint) {
|
|
mediaPlayer = mp
|
|
mediaPlayerError = 0
|
|
mp.setScreenOnWhilePlaying(true)
|
|
updateVolume()
|
|
mp.isLooping = isLoopEnabled
|
|
mp.start()
|
|
videoViewProgress.visibility = View.VISIBLE
|
|
videoViewProgress.post(videoProgressRunnable)
|
|
updatePlayerState()
|
|
videoControl.visibility = View.VISIBLE
|
|
}
|
|
}
|
|
|
|
private fun updateVolume() {
|
|
|
|
volumeButton.setImageResource(if (playAudio) R.drawable.ic_action_speaker_max else R.drawable.ic_action_speaker_muted)
|
|
val mp = mediaPlayer ?: return
|
|
try {
|
|
if (playAudio) {
|
|
mp.setVolume(1f, 1f)
|
|
} else {
|
|
mp.setVolume(0f, 0f)
|
|
}
|
|
} catch (e: IllegalStateException) {
|
|
// Ignore
|
|
}
|
|
|
|
}
|
|
|
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
|
super.setUserVisibleHint(isVisibleToUser)
|
|
if (activity == null) return
|
|
if (isVisibleToUser) {
|
|
activity.supportInvalidateOptionsMenu()
|
|
} else if (videoView.isPlaying) {
|
|
videoView.pause()
|
|
updatePlayerState()
|
|
}
|
|
}
|
|
|
|
override fun onClick(v: View) {
|
|
when (v.id) {
|
|
R.id.volumeButton -> {
|
|
playAudio = !playAudio
|
|
updateVolume()
|
|
}
|
|
R.id.playPauseButton -> {
|
|
val mp = mediaPlayer ?: return
|
|
if (mp.isPlaying) {
|
|
mp.pause()
|
|
} else {
|
|
mp.start()
|
|
}
|
|
updatePlayerState()
|
|
}
|
|
R.id.videoViewOverlay -> {
|
|
val activity = activity as MediaViewerActivity
|
|
activity.setBarVisibility(!activity.isBarShowing)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCreateMediaView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
return inflater.inflate(R.layout.layout_media_viewer_texture_video_view, container, false)
|
|
}
|
|
|
|
|
|
override fun onDownloadRequested(nonce: Long) {
|
|
super.onDownloadRequested(nonce)
|
|
val context = context
|
|
if (context != null) {
|
|
mediaDownloadEvent = MediaDownloadEvent.create(context, media, nonce)
|
|
} else {
|
|
mediaDownloadEvent = null
|
|
}
|
|
}
|
|
|
|
override fun onDownloadStart(total: Long, nonce: Long) {
|
|
super.onDownloadStart(total, nonce)
|
|
mediaDownloadEvent?.let {
|
|
if (it.nonce == nonce) {
|
|
it.setOpenedTime(System.currentTimeMillis())
|
|
it.setSize(total)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onDownloadFinished(nonce: Long) {
|
|
super.onDownloadFinished(nonce)
|
|
if (mediaDownloadEvent != null && mediaDownloadEvent!!.nonce == nonce) {
|
|
mediaDownloadEvent!!.markEnd()
|
|
HotMobiLogger.getInstance(context).log(accountKey, mediaDownloadEvent!!)
|
|
mediaDownloadEvent = null
|
|
}
|
|
}
|
|
|
|
|
|
@SuppressLint("SwitchIntDef")
|
|
private fun getBestVideoUrlAndType(media: ParcelableMedia?, supportedTypes: Array<String>): Pair<String, String>? {
|
|
if (media == null) return null
|
|
when (media.type) {
|
|
ParcelableMedia.Type.VIDEO, ParcelableMedia.Type.ANIMATED_GIF -> {
|
|
if (media.video_info == null) {
|
|
return Pair.create<String, String>(media.media_url, null)
|
|
}
|
|
val firstMatch = media.video_info.variants.first { variant ->
|
|
supportedTypes.any { it.equals(variant.content_type, ignoreCase = true) }
|
|
} ?: return null
|
|
return Pair.create(firstMatch.url, firstMatch.content_type)
|
|
}
|
|
ParcelableMedia.Type.CARD_ANIMATED_GIF -> {
|
|
return Pair.create<String, String>(media.media_url, "video/mp4")
|
|
}
|
|
else -> {
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun updatePlayerState() {
|
|
val mp = mediaPlayer
|
|
if (mp != null) {
|
|
val playing = mp.isPlaying
|
|
playPauseButton.contentDescription = getString(if (playing) R.string.pause else R.string.play)
|
|
playPauseButton.setImageResource(if (playing) R.drawable.ic_action_pause else R.drawable.ic_action_play_arrow)
|
|
} else {
|
|
playPauseButton.contentDescription = getString(R.string.play)
|
|
playPauseButton.setImageResource(R.drawable.ic_action_play_arrow)
|
|
}
|
|
}
|
|
|
|
private fun pauseVideo(): Boolean {
|
|
val mp = mediaPlayer ?: return false
|
|
var result = false
|
|
if (mp.isPlaying) {
|
|
mp.pause()
|
|
result = true
|
|
}
|
|
updatePlayerState()
|
|
return result
|
|
}
|
|
|
|
private fun resumeVideo(): Boolean {
|
|
val mp = mediaPlayer ?: return false
|
|
var result = false
|
|
if (!mp.isPlaying) {
|
|
mp.start()
|
|
result = true
|
|
}
|
|
updatePlayerState()
|
|
return result
|
|
}
|
|
|
|
private class VideoPlayProgressRunnable internal constructor(
|
|
private val handler: Handler,
|
|
private val progressBar: ProgressBar,
|
|
private val durationLabel: TextView,
|
|
private val positionLabel: TextView,
|
|
private val mediaPlayerControl: MediaController.MediaPlayerControl
|
|
) : Runnable {
|
|
|
|
init {
|
|
progressBar.max = 1000
|
|
}
|
|
|
|
override fun run() {
|
|
val duration = mediaPlayerControl.duration
|
|
val position = mediaPlayerControl.currentPosition
|
|
if (duration <= 0 || position < 0) return
|
|
progressBar.progress = Math.round(1000 * position / duration.toFloat())
|
|
val durationSecs = TimeUnit.SECONDS.convert(duration.toLong(), TimeUnit.MILLISECONDS)
|
|
val positionSecs = TimeUnit.SECONDS.convert(position.toLong(), TimeUnit.MILLISECONDS)
|
|
durationLabel.text = String.format(Locale.ROOT, "%02d:%02d", durationSecs / 60, durationSecs % 60)
|
|
positionLabel.text = String.format(Locale.ROOT, "%02d:%02d", positionSecs / 60, positionSecs % 60)
|
|
handler.postDelayed(this, 16)
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
|
|
const val EXTRA_LOOP = "loop"
|
|
private val SUPPORTED_VIDEO_TYPES: Array<String>
|
|
private val FALLBACK_VIDEO_TYPES: Array<String> = arrayOf("video/mp4")
|
|
|
|
init {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
|
SUPPORTED_VIDEO_TYPES = arrayOf("video/mp4")
|
|
} else {
|
|
SUPPORTED_VIDEO_TYPES = arrayOf("video/webm", "video/mp4")
|
|
}
|
|
}
|
|
}
|
|
} |