Skip to content

Commit

Permalink
Merge branch 'streaming'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff committed Aug 24, 2021
2 parents a823d91 + 5b06c8f commit 47786e7
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 110 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ dependencies {

implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.0'
implementation 'com.google.android.exoplayer:extension-okhttp:2.15.0'

// Chris Banes PhotoView
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ class CameraRollFragment : Fragment() {

override fun onStart() {
super.onStart()
mediaPagerAdapter.initializePlayer(requireContext())
mediaPagerAdapter.initializePlayer(requireContext(), null)
mediaPagerAdapter.setAutoStart(true)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,35 @@ import android.view.*
import android.widget.ImageButton
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import com.github.chrisbanes.photoview.PhotoView
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import kotlinx.parcelize.Parcelize
import okhttp3.OkHttpClient
import site.leos.apps.lespas.R
import java.io.File
import java.time.LocalDateTime
import java.util.*

abstract class MediaSliderAdapter<T>(diffCallback: ItemCallback<T>, private val clickListener: (Boolean?) -> Unit, private val imageLoader: (T, ImageView, String) -> Unit, private val cancelLoader: (View) -> Unit
): ListAdapter<T, RecyclerView.ViewHolder>(diffCallback) {
lateinit var exoPlayer: SimpleExoPlayer
private lateinit var exoPlayer: SimpleExoPlayer
private var currentVolume = 0f
private var oldVideoViewHolder: VideoViewHolder? = null
private var savedPlayerState = PlayerState()
private var autoStart = false
private var cache: SimpleCache? = null

abstract fun getVideoItem(position: Int): VideoItem
abstract fun getItemTransitionName(position: Int): String
Expand Down Expand Up @@ -120,11 +128,13 @@ abstract class MediaSliderAdapter<T>(diffCallback: ItemCallback<T>, private val
with(videoView) {
hideController()
controllerShowTimeoutMs = 3000
//setShutterBackgroundColor(0)
setOnClickListener { clickListener(!videoView.isControllerVisible) }
}

videoMimeType = video.mimeType

/*
itemView.findViewById<ConstraintLayout>(R.id.videoview_container).let {
// Fix view aspect ratio
if (video.height != 0) with(ConstraintSet()) {
Expand All @@ -135,6 +145,8 @@ abstract class MediaSliderAdapter<T>(diffCallback: ItemCallback<T>, private val
it.setOnClickListener { clickListener(!videoView.isControllerVisible) }
}
*/
itemView.findViewById<ConstraintLayout>(R.id.videoview_container).setOnClickListener { clickListener(!videoView.isControllerVisible) }

thumbnailView = itemView.findViewById<ImageView>(R.id.media).apply {
// Even thought we don't load animated image with ImageLoader, we still need to call it here so that postponed enter transition can be started
Expand All @@ -145,6 +157,7 @@ abstract class MediaSliderAdapter<T>(diffCallback: ItemCallback<T>, private val
muteButton.setOnClickListener { toggleMute() }
}

fun hideThumbnailView() { thumbnailView.visibility = View.INVISIBLE }
fun hideControllers() { videoView.hideController() }
fun setStopPosition(position: Long) {
//Log.e(">>>","set stop position $position")
Expand Down Expand Up @@ -226,9 +239,16 @@ abstract class MediaSliderAdapter<T>(diffCallback: ItemCallback<T>, private val
}
}

fun initializePlayer(ctx: Context) {
fun initializePlayer(ctx: Context, callFactory: OkHttpClient?) {
//private var exoPlayer = SimpleExoPlayer.Builder(ctx, { _, _, _, _, _ -> arrayOf(MediaCodecVideoRenderer(ctx, MediaCodecSelector.DEFAULT)) }) { arrayOf(Mp4Extractor()) }.build()
exoPlayer = SimpleExoPlayer.Builder(ctx).build()
val builder = SimpleExoPlayer.Builder(ctx)
//callFactory?.let { builder.setMediaSourceFactory(DefaultMediaSourceFactory(DefaultDataSourceFactory(ctx, OkHttpDataSource.Factory(callFactory)))) }
callFactory?.let {
cache = SimpleCache(File(ctx.cacheDir, "media"), LeastRecentlyUsedCacheEvictor(100L * 1024L * 1024L))
builder.setMediaSourceFactory(DefaultMediaSourceFactory(CacheDataSource.Factory().setCache(cache!!).setUpstreamDataSourceFactory(DefaultDataSourceFactory(ctx, OkHttpDataSource.Factory(callFactory)))))
}
exoPlayer = builder.build()

currentVolume = exoPlayer.volume
exoPlayer.addListener(object: Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
Expand All @@ -247,6 +267,7 @@ abstract class MediaSliderAdapter<T>(diffCallback: ItemCallback<T>, private val
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
oldVideoViewHolder?.hideThumbnailView()
oldVideoViewHolder?.hideControllers()
clickListener(false)
}
Expand All @@ -262,7 +283,10 @@ abstract class MediaSliderAdapter<T>(diffCallback: ItemCallback<T>, private val
}
}

fun cleanUp() { exoPlayer.release() }
fun cleanUp() {
cache?.release()
exoPlayer.release()
}
fun setAutoStart(state: Boolean) { autoStart = state }

fun setPlayerState(state: PlayerState) { savedPlayerState = state }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package site.leos.apps.lespas.helper

import android.view.View
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Transition
import androidx.viewpager2.widget.ViewPager2
import com.google.android.exoplayer2.ui.PlayerView
import site.leos.apps.lespas.R

class MediaSliderTransitionListener(private val slider: ViewPager2): Transition.TransitionListener {
var isVideo = false
override fun onTransitionStart(transition: Transition) {
slider.getChildAt(0)?.apply {
findViewById<ImageView>(R.id.media)?.let { mediaView->
// media imageview in exoplayer item view is always in invisible state
if (mediaView.visibility != View.VISIBLE) {
isVideo = true
findViewById<PlayerView>(R.id.player_view)?.visibility = View.INVISIBLE
}
else {
mediaView.visibility = View.INVISIBLE
(slider.adapter as MediaSliderAdapter<*>).setAutoStart(true)
}
}
}
}
override fun onTransitionEnd(transition: Transition) {
(slider.getChildAt(0) as RecyclerView).apply {
findViewById<ImageView>(R.id.media)?.visibility = View.VISIBLE
if (isVideo) (findViewHolderForAdapterPosition(slider.currentItem) as MediaSliderAdapter<*>.VideoViewHolder).startOver()
}
}
override fun onTransitionCancel(transition: Transition) {
(slider.getChildAt(0) as RecyclerView).apply {
findViewById<ImageView>(R.id.media)?.visibility = View.VISIBLE
if (isVideo) (findViewHolderForAdapterPosition(slider.currentItem) as MediaSliderAdapter<*>.VideoViewHolder).startOver()
}
}
override fun onTransitionPause(transition: Transition) {}
override fun onTransitionResume(transition: Transition) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class OkHttpWebDav(private val userId: String, password: String, serverAddress:
cachedHttpClient = builder.cache(Cache(File(cacheFolder), DISK_CACHE_SIZE)).addNetworkInterceptor { chain -> chain.proceed(chain.request()).newBuilder().removeHeader("Pragma").header("Cache-Control", "public, max-age=${MAX_AGE}").build() }.build()

// Make cache folder for video download
File(cacheFolder, VIDEO_CACHE_FOLDER).mkdirs()
//File(cacheFolder, VIDEO_CACHE_FOLDER).mkdirs()
}

fun copy(source: String, dest: String) { copyOrMove(true, source, dest) }
Expand Down Expand Up @@ -87,6 +87,8 @@ class OkHttpWebDav(private val userId: String, password: String, serverAddress:
}
}

fun getCallFactory() = httpClient

fun getStream(source: String, useCache: Boolean, cacheControl: CacheControl?): InputStream = getStreamBool(source, useCache, cacheControl).first
fun getStreamBool(source: String, useCache: Boolean, cacheControl: CacheControl?): Pair<InputStream, Boolean> {
val reqBuilder = Request.Builder().url(source)
Expand Down Expand Up @@ -316,7 +318,7 @@ class OkHttpWebDav(private val userId: String, password: String, serverAddress:
companion object {
private const val DISK_CACHE_SIZE = 300L * 1024L * 1024L // 300MB
private const val MAX_AGE = "864000" // 10 days
const val VIDEO_CACHE_FOLDER = "videos"
//const val VIDEO_CACHE_FOLDER = "videos"

private const val CHUNK_SIZE = 50L * 1024L * 1024L // Default chunk size is 50MB

Expand Down
60 changes: 12 additions & 48 deletions app/src/main/java/site/leos/apps/lespas/photo/PhotoSlideFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,13 @@ import androidx.lifecycle.ViewModel
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Transition
import androidx.viewpager2.widget.ViewPager2
import androidx.work.*
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.material.transition.MaterialContainerTransform
import site.leos.apps.lespas.R
import site.leos.apps.lespas.album.Album
import site.leos.apps.lespas.album.AlbumViewModel
import site.leos.apps.lespas.helper.ImageLoaderViewModel
import site.leos.apps.lespas.helper.MediaSliderAdapter
import site.leos.apps.lespas.helper.SnapseedResultWorker
import site.leos.apps.lespas.helper.Tools
import site.leos.apps.lespas.helper.*
import site.leos.apps.lespas.sync.ActionViewModel
import java.io.File

Expand Down Expand Up @@ -72,47 +67,6 @@ class PhotoSlideFragment : Fragment() {
{ view -> imageLoaderModel.cancelLoading(view as ImageView) }
).apply { stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY }

sharedElementEnterTransition = MaterialContainerTransform().apply {
duration = resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
scrimColor = Color.TRANSPARENT
fadeMode = MaterialContainerTransform.FADE_MODE_CROSS
}.also {
// Prevent ViewPager from showing content before transition finished, without this, Android 11 will show it right at the beginning
// Also we can transit to video thumbnail before player start playing
it.addListener(object : Transition.TransitionListener {
var isVideo = false
override fun onTransitionStart(transition: Transition) {
slider.getChildAt(0)?.apply {
findViewById<ImageView>(R.id.media)?.let { mediaView->
// media imageview in exoplayer item view is always in invisible state
if (mediaView.visibility != View.VISIBLE) {
isVideo = true
findViewById<PlayerView>(R.id.player_view)?.visibility = View.INVISIBLE
}
else {
mediaView.visibility = View.INVISIBLE
pAdapter.setAutoStart(true)
}
}
}
}
override fun onTransitionEnd(transition: Transition) {
(slider.getChildAt(0) as RecyclerView).apply {
if (isVideo) (findViewHolderForAdapterPosition(slider.currentItem) as MediaSliderAdapter<*>.VideoViewHolder).startOver()
else findViewById<ImageView>(R.id.media)?.visibility = View.VISIBLE
}
}
override fun onTransitionCancel(transition: Transition) {
(slider.getChildAt(0) as RecyclerView).apply {
if (isVideo) (findViewHolderForAdapterPosition(slider.currentItem) as MediaSliderAdapter<*>.VideoViewHolder).startOver()
else findViewById<ImageView>(R.id.media)?.visibility = View.VISIBLE
}
}
override fun onTransitionPause(transition: Transition) {}
override fun onTransitionResume(transition: Transition) {}
})
}

// Adjusting the shared element mapping
setEnterSharedElementCallback(object : SharedElementCallback() {
override fun onMapSharedElements(names: MutableList<String>?, sharedElements: MutableMap<String, View>?) {
Expand Down Expand Up @@ -222,6 +176,16 @@ class PhotoSlideFragment : Fragment() {
})
}

sharedElementEnterTransition = MaterialContainerTransform().apply {
duration = resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
scrimColor = Color.TRANSPARENT
fadeMode = MaterialContainerTransform.FADE_MODE_CROSS
}.also {
// Prevent ViewPager from showing content before transition finished, without this, Android 11 will show it right at the beginning
// Also we can transit to video thumbnail before player start playing
it.addListener(MediaSliderTransitionListener(slider))
}

albumModel.getAllPhotoInAlbum(album.id).observe(viewLifecycleOwner, { photos->
pAdapter.setPhotos(photos, album.sortOrder)
currentPhotoModel.getCurrentPhotoId()?.let {
Expand Down Expand Up @@ -294,7 +258,7 @@ class PhotoSlideFragment : Fragment() {

override fun onStart() {
super.onStart()
pAdapter.initializePlayer(requireContext())
pAdapter.initializePlayer(requireContext(), null)
}

override fun onResume() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import okhttp3.FormBody
import okio.IOException
import okio.buffer
import okio.sink
import okio.source
import org.json.JSONException
import org.json.JSONObject
import site.leos.apps.lespas.R
Expand Down Expand Up @@ -66,6 +65,7 @@ class NCShareViewModel(application: Application): AndroidViewModel(application)

private val baseUrl: String
private val userName: String
private val token: String
private val resourceRoot: String
private val lespasBase = application.getString(R.string.lespas_base_folder_name)
private val localCacheFolder = "${application.cacheDir}${lespasBase}"
Expand All @@ -88,6 +88,7 @@ class NCShareViewModel(application: Application): AndroidViewModel(application)
AccountManager.get(application).run {
val account = getAccountsByType(application.getString(R.string.account_type_nc))[0]
userName = getUserData(account, application.getString(R.string.nc_userdata_username))
token = getUserData(account, application.getString(R.string.nc_userdata_secret))
baseUrl = getUserData(account, application.getString(R.string.nc_userdata_server))
resourceRoot = "$baseUrl${application.getString(R.string.dav_files_endpoint)}$userName"
webDav = OkHttpWebDav(userName, peekAuthToken(account, baseUrl), baseUrl, getUserData(account, application.getString(R.string.nc_userdata_selfsigned)).toBoolean(), "${application.cacheDir}/${application.getString(R.string.lespas_base_folder_name)}", "LesPas_${application.getString(R.string.lespas_version)}")
Expand All @@ -99,6 +100,10 @@ class NCShareViewModel(application: Application): AndroidViewModel(application)
}
}

fun getCallFactory() = webDav.getCallFactory()

fun getResourceRoot(): String = resourceRoot

val themeColor: Flow<Int> = flow {
var color = 0

Expand Down Expand Up @@ -537,7 +542,8 @@ class NCShareViewModel(application: Application): AndroidViewModel(application)
}

private fun getRemoteVideoThumbnail(inputStream: InputStream, photo: RemotePhoto): Bitmap? {
var bitmap: Bitmap?
var bitmap: Bitmap? = null
/*
// Download video file if necessary
val fileName = "${OkHttpWebDav.VIDEO_CACHE_FOLDER}/${photo.path.substringAfterLast('/')}"
val videoFile = File(localCacheFolder, fileName)
Expand All @@ -553,6 +559,28 @@ class NCShareViewModel(application: Application): AndroidViewModel(application)
bitmap = getFrameAtTime(0L) ?: videoThumbnail
release()
}
*/
val thumbnail = File(localCacheFolder, "${photo.fileId}.thumbnail")

// Load from local cache
if (thumbnail.exists()) bitmap = BitmapFactory.decodeStream(thumbnail.inputStream())

// Download from server
bitmap ?: run {
MediaMetadataRetriever().apply {
inputStream.close()
setDataSource("$resourceRoot${Uri.encode(photo.path, "/")}", HashMap<String, String>().apply { this["Authorization"] = "Basic $token" })
bitmap = getFrameAtTime(0L) ?: videoThumbnail
release()
}

// Cache thumbnail in local
bitmap?.let {
viewModelScope.launch(Dispatchers.IO) {
it.compress(Bitmap.CompressFormat.JPEG, 90, thumbnail.outputStream())
}
}
}

return bitmap
}
Expand Down Expand Up @@ -610,7 +638,6 @@ class NCShareViewModel(application: Application): AndroidViewModel(application)
e.printStackTrace()
it.close()
webDav.getStream("$resourceRoot${photo.path}", true,null).use { vResp->
// TODO could take a long time to download the video file
bitmap = getRemoteVideoThumbnail(vResp, photo)
}
}
Expand Down Expand Up @@ -732,7 +759,7 @@ class NCShareViewModel(application: Application): AndroidViewModel(application)
}

override fun onCleared() {
File(localCacheFolder, OkHttpWebDav.VIDEO_CACHE_FOLDER).deleteRecursively()
//File(localCacheFolder, OkHttpWebDav.VIDEO_CACHE_FOLDER).deleteRecursively()
decoderJobMap.forEach { if (it.value.isActive) it.value.cancel() }
downloadDispatcher.close()
super.onCleared()
Expand Down
Loading

0 comments on commit 47786e7

Please sign in to comment.