409 lines
15 KiB
Kotlin
409 lines
15 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package org.mariotaku.twidere.fragment.media
|
|
|
|
import android.accounts.AccountManager
|
|
import android.annotation.TargetApi
|
|
import android.content.Context
|
|
import android.graphics.Rect
|
|
import android.media.AudioManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.view.LayoutInflater
|
|
import android.view.MotionEvent
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import com.google.android.exoplayer2.*
|
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory
|
|
import com.google.android.exoplayer2.source.ExtractorMediaSource
|
|
import com.google.android.exoplayer2.source.LoopingMediaSource
|
|
import com.google.android.exoplayer2.source.TrackGroupArray
|
|
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection
|
|
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.DefaultBandwidthMeter
|
|
import com.google.android.exoplayer2.upstream.HttpDataSource
|
|
import kotlinx.android.synthetic.main.layout_media_viewer_exo_player_view.*
|
|
import kotlinx.android.synthetic.main.layout_media_viewer_video_overlay.*
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import okhttp3.Response
|
|
import org.mariotaku.ktextension.contains
|
|
import org.mariotaku.mediaviewer.library.MediaViewerFragment
|
|
import org.mariotaku.mediaviewer.library.subsampleimageview.SubsampleImageViewerFragment
|
|
import org.mariotaku.twidere.R
|
|
import org.mariotaku.twidere.activity.MediaViewerActivity
|
|
import org.mariotaku.twidere.annotation.CacheFileType
|
|
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_POSITION
|
|
import org.mariotaku.twidere.extension.model.authorizationHeader
|
|
import org.mariotaku.twidere.fragment.iface.IBaseFragment
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.EXTRA_PAUSED_BY_USER
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.EXTRA_PLAY_AUDIO
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.SUPPORTED_VIDEO_TYPES
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.accountKey
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.isControlDisabled
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.isLoopEnabled
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.isMutedByDefault
|
|
import org.mariotaku.twidere.fragment.media.VideoPageFragment.Companion.media
|
|
import org.mariotaku.twidere.model.AccountDetails
|
|
import org.mariotaku.twidere.model.ParcelableMedia
|
|
import org.mariotaku.twidere.model.util.AccountUtils
|
|
import org.mariotaku.twidere.provider.CacheProvider
|
|
import org.mariotaku.twidere.task.SaveFileTask
|
|
import org.mariotaku.twidere.util.dagger.GeneralComponent
|
|
import org.mariotaku.twidere.util.media.TwidereMediaDownloader
|
|
import java.io.InputStream
|
|
import javax.inject.Inject
|
|
|
|
|
|
/**
|
|
* Successor of `VideoPageFragment`, backed by `ExoPlayer`
|
|
* Created by mariotaku on 2017/2/28.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
|
class ExoPlayerPageFragment : MediaViewerFragment(), IBaseFragment<ExoPlayerPageFragment> {
|
|
|
|
@Inject
|
|
internal lateinit var dataSourceFactory: DataSource.Factory
|
|
|
|
@Inject
|
|
internal lateinit var extractorsFactory: ExtractorsFactory
|
|
|
|
@Inject
|
|
internal lateinit var okHttpClient: OkHttpClient
|
|
|
|
private lateinit var mainHandler: Handler
|
|
|
|
private var playAudio: Boolean = false
|
|
private var pausedByUser: Boolean = false
|
|
private var playbackCompleted: Boolean = false
|
|
private var positionBackup: Long = -1L
|
|
private var playerHasError: Boolean = false
|
|
|
|
private val account by lazy {
|
|
AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true)
|
|
}
|
|
|
|
private val playerListener = object : ExoPlayer.EventListener {
|
|
override fun onLoadingChanged(isLoading: Boolean) {
|
|
|
|
}
|
|
|
|
override fun onPlayerError(error: ExoPlaybackException) {
|
|
playerHasError = true
|
|
hideProgress()
|
|
}
|
|
|
|
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
|
when (playbackState) {
|
|
ExoPlayer.STATE_BUFFERING -> {
|
|
playerView.keepScreenOn = true
|
|
showProgress(true, 0f)
|
|
}
|
|
ExoPlayer.STATE_ENDED -> {
|
|
playbackCompleted = true
|
|
positionBackup = -1L
|
|
playerView.keepScreenOn = false
|
|
|
|
// Reset position
|
|
playerView.player?.let { player ->
|
|
player.seekTo(0)
|
|
player.playWhenReady = false
|
|
}
|
|
|
|
hideProgress()
|
|
val activity = activity as? MediaViewerActivity
|
|
activity?.setBarVisibility(true)
|
|
}
|
|
ExoPlayer.STATE_READY -> {
|
|
playbackCompleted = playWhenReady
|
|
playerHasError = false
|
|
playerView.keepScreenOn = playWhenReady
|
|
hideProgress()
|
|
}
|
|
ExoPlayer.STATE_IDLE -> {
|
|
playerView.keepScreenOn = false
|
|
hideProgress()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onPositionDiscontinuity() {
|
|
}
|
|
|
|
override fun onTimelineChanged(timeline: Timeline, manifest: Any?) {
|
|
}
|
|
|
|
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
|
|
}
|
|
|
|
}
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
super.onActivityCreated(savedInstanceState)
|
|
mainHandler = Handler()
|
|
|
|
|
|
if (savedInstanceState != null) {
|
|
positionBackup = savedInstanceState.getLong(EXTRA_POSITION)
|
|
pausedByUser = savedInstanceState.getBoolean(EXTRA_PAUSED_BY_USER)
|
|
playAudio = savedInstanceState.getBoolean(EXTRA_PLAY_AUDIO)
|
|
} else {
|
|
val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
// Play audio by default if ringer mode on
|
|
playAudio = !isMutedByDefault && am.ringerMode == AudioManager.RINGER_MODE_NORMAL
|
|
}
|
|
|
|
volumeButton.setOnClickListener {
|
|
this.playAudio = !this.playAudio
|
|
updateVolume()
|
|
}
|
|
playerView.useController = !isControlDisabled
|
|
playerView.controllerShowTimeoutMs = 0
|
|
playerView.setOnSystemUiVisibilityChangeListener {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return@setOnSystemUiVisibilityChangeListener
|
|
val visible = MediaViewerActivity.FLAG_SYSTEM_UI_HIDE_BARS !in
|
|
activity.window.decorView.systemUiVisibility
|
|
if (visible) {
|
|
playerView.showController()
|
|
} else {
|
|
playerView.hideController()
|
|
}
|
|
}
|
|
playerView.setOnTouchListener { _, event ->
|
|
if (event.action != MotionEvent.ACTION_DOWN) return@setOnTouchListener false
|
|
val activity = activity as? MediaViewerActivity ?: return@setOnTouchListener false
|
|
val visible = !activity.isBarShowing
|
|
activity.setBarVisibility(visible)
|
|
if (visible) {
|
|
playerView.showController()
|
|
} else {
|
|
playerView.hideController()
|
|
}
|
|
return@setOnTouchListener true
|
|
}
|
|
updateVolume()
|
|
}
|
|
|
|
override fun onAttach(context: Context) {
|
|
super.onAttach(context)
|
|
GeneralComponent.get(context).inject(this)
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
|
initializePlayer()
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
|
initializePlayer()
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
|
releasePlayer()
|
|
}
|
|
}
|
|
|
|
override fun onStop() {
|
|
super.onStop()
|
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
|
releasePlayer()
|
|
}
|
|
}
|
|
|
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
|
super.setUserVisibleHint(isVisibleToUser)
|
|
if (activity != null && !isDetached) {
|
|
if (isVisibleToUser) {
|
|
initializePlayer()
|
|
} else {
|
|
releasePlayer()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCreateMediaView(inflater: LayoutInflater, parent: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
return inflater.inflate(R.layout.layout_media_viewer_exo_player_view, parent, false)
|
|
}
|
|
|
|
override fun onApplySystemWindowInsets(insets: Rect) {
|
|
val lp = videoControl.layoutParams
|
|
if (lp is ViewGroup.MarginLayoutParams) {
|
|
lp.bottomMargin = insets.bottom
|
|
lp.leftMargin = insets.left
|
|
lp.rightMargin = insets.right
|
|
}
|
|
}
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
super.onSaveInstanceState(outState)
|
|
outState.putLong(EXTRA_POSITION, positionBackup)
|
|
outState.putBoolean(EXTRA_PAUSED_BY_USER, pausedByUser)
|
|
outState.putBoolean(EXTRA_PLAY_AUDIO, playAudio)
|
|
}
|
|
|
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
|
super.onViewStateRestored(savedInstanceState)
|
|
requestApplyInsets()
|
|
}
|
|
|
|
override fun executeAfterFragmentResumed(useHandler: Boolean, action: (ExoPlayerPageFragment) -> Unit) = TODO()
|
|
|
|
override fun isMediaLoaded(): Boolean {
|
|
return !playerHasError
|
|
}
|
|
|
|
override fun isMediaLoading(): Boolean {
|
|
return false
|
|
}
|
|
|
|
private fun releasePlayer() {
|
|
val player = playerView.player ?: return
|
|
positionBackup = player.currentPosition
|
|
pausedByUser = !player.playWhenReady
|
|
player.removeListener(playerListener)
|
|
player.release()
|
|
playerView.player = null
|
|
}
|
|
|
|
private fun initializePlayer() {
|
|
if (playerView.player != null) return
|
|
playerView.player = run {
|
|
val bandwidthMeter = DefaultBandwidthMeter()
|
|
val videoTrackSelectionFactory = AdaptiveVideoTrackSelection.Factory(bandwidthMeter)
|
|
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
|
|
val player = ExoPlayerFactory.newSimpleInstance(context, trackSelector, DefaultLoadControl())
|
|
if (positionBackup >= 0) {
|
|
player.seekTo(positionBackup)
|
|
}
|
|
player.playWhenReady = !pausedByUser
|
|
playerHasError = false
|
|
player.addListener(playerListener)
|
|
return@run player
|
|
}
|
|
|
|
val uri = media?.getDownloadUri() ?: return
|
|
val factory = AuthDelegatingDataSourceFactory(uri, account, dataSourceFactory)
|
|
val uriSource = ExtractorMediaSource(uri, factory, extractorsFactory, null, null)
|
|
if (isLoopEnabled) {
|
|
playerView.player.prepare(LoopingMediaSource(uriSource))
|
|
} else {
|
|
playerView.player.prepare(uriSource)
|
|
}
|
|
updateVolume()
|
|
}
|
|
|
|
private fun updateVolume() {
|
|
volumeButton.setImageResource(if (playAudio) R.drawable.ic_action_speaker_max else R.drawable.ic_action_speaker_muted)
|
|
val player = playerView.player ?: return
|
|
if (playAudio) {
|
|
player.volume = 1f
|
|
} else {
|
|
player.volume = 0f
|
|
}
|
|
}
|
|
|
|
private fun ParcelableMedia.getDownloadUri(): Uri? {
|
|
val bestVideoUrlAndType = VideoPageFragment.getBestVideoUrlAndType(this, SUPPORTED_VIDEO_TYPES)
|
|
if (bestVideoUrlAndType != null && bestVideoUrlAndType.first != null) {
|
|
return Uri.parse(bestVideoUrlAndType.first)
|
|
}
|
|
return arguments.getParcelable<Uri>(SubsampleImageViewerFragment.EXTRA_MEDIA_URI)
|
|
}
|
|
|
|
|
|
fun getRequestFileInfo(): RequestFileInfo? {
|
|
val uri = media?.getDownloadUri() ?: return null
|
|
return RequestFileInfo(uri, account, okHttpClient)
|
|
}
|
|
|
|
class AuthDelegatingDataSourceFactory(
|
|
val uri: Uri,
|
|
val account: AccountDetails?,
|
|
val delegate: DataSource.Factory
|
|
) : DataSource.Factory {
|
|
|
|
override fun createDataSource(): DataSource {
|
|
val source = delegate.createDataSource()
|
|
if (source is HttpDataSource) {
|
|
setAuthorizationHeader(source)
|
|
}
|
|
return source
|
|
}
|
|
|
|
private fun setAuthorizationHeader(dataSource: HttpDataSource) {
|
|
val credentials = account?.credentials
|
|
if (credentials != null && TwidereMediaDownloader.isAuthRequired(credentials, uri)) {
|
|
dataSource.setRequestProperty("Authorization", credentials.authorizationHeader(uri))
|
|
}
|
|
}
|
|
}
|
|
|
|
class RequestFileInfo(
|
|
val uri: Uri,
|
|
val account: AccountDetails?,
|
|
val okHttpClient: OkHttpClient
|
|
) : SaveFileTask.FileInfo, CacheProvider.CacheFileTypeSupport {
|
|
|
|
private var response: Response? = null
|
|
|
|
override val cacheFileType: String? = CacheFileType.VIDEO
|
|
|
|
override val fileName: String? = uri.lastPathSegment
|
|
|
|
override val mimeType: String?
|
|
get() = request().body()?.contentType()?.toString()
|
|
|
|
override val specialCharacter: Char = '_'
|
|
|
|
override fun inputStream(): InputStream {
|
|
return request().body()!!.byteStream()
|
|
}
|
|
|
|
override fun close() {
|
|
response?.close()
|
|
}
|
|
|
|
private fun request(): Response {
|
|
if (response != null) return response!!
|
|
val builder = Request.Builder()
|
|
builder.url(uri.toString())
|
|
val credentials = account?.credentials
|
|
if (credentials != null && TwidereMediaDownloader.isAuthRequired(credentials, uri)) {
|
|
builder.addHeader("Authorization", credentials.authorizationHeader(uri))
|
|
}
|
|
response = okHttpClient.newCall(builder.build()).execute()
|
|
return response!!
|
|
}
|
|
|
|
}
|
|
|
|
}
|