Add blur hashes behind videos.
This commit is contained in:
parent
fb8e81cf50
commit
3e2ecdaaa9
7 changed files with 176 additions and 71 deletions
|
@ -29,15 +29,17 @@ class StoryCache(
|
|||
val prefetchableAttachments: List<Attachment> = attachments
|
||||
.asSequence()
|
||||
.filter { it.uri != null && it.uri !in cache }
|
||||
.filter { MediaUtil.isImage(it) }
|
||||
.filter { MediaUtil.isImage(it) || it.blurHash != null }
|
||||
.filter { it.transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE }
|
||||
.toList()
|
||||
|
||||
val newMappings: Map<Uri, StoryCacheValue> = prefetchableAttachments.associateWith { attachment ->
|
||||
val imageTarget = glideRequests
|
||||
.load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!))
|
||||
.priority(Priority.HIGH)
|
||||
.into(StoryCacheTarget(attachment.uri!!, storySize))
|
||||
val imageTarget = if (MediaUtil.isImage(attachment)) {
|
||||
glideRequests
|
||||
.load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!))
|
||||
.priority(Priority.HIGH)
|
||||
.into(StoryCacheTarget(attachment.uri!!, storySize))
|
||||
} else null
|
||||
|
||||
val blurTarget = if (attachment.blurHash != null) {
|
||||
glideRequests
|
||||
|
@ -79,7 +81,7 @@ class StoryCache(
|
|||
/**
|
||||
* Represents the load targets for an image and blur.
|
||||
*/
|
||||
data class StoryCacheValue(val imageTarget: StoryCacheTarget, val blurTarget: StoryCacheTarget?)
|
||||
data class StoryCacheValue(val imageTarget: StoryCacheTarget?, val blurTarget: StoryCacheTarget?)
|
||||
|
||||
/**
|
||||
* A custom glide target for loading a drawable. Placeholder immediately clears, and we don't want to do that, so we use this instead.
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.post
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryCache
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay
|
||||
|
||||
/**
|
||||
* Responsible for managing the lifecycle around loading a BlurHash
|
||||
*/
|
||||
class StoryBlurLoader(
|
||||
private val lifecycle: Lifecycle,
|
||||
private val blurHash: BlurHash?,
|
||||
private val cacheKey: Uri,
|
||||
private val storyCache: StoryCache,
|
||||
private val storySize: StoryDisplay.Size,
|
||||
private val blurImage: ImageView,
|
||||
private val callback: Callback = NO_OP
|
||||
) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(StoryBlurLoader::class.java)
|
||||
|
||||
private val NO_OP = object : Callback {
|
||||
override fun onBlurLoaded() = Unit
|
||||
override fun onBlurFailed() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
private val blurListener = object : StoryCache.Listener {
|
||||
override fun onResourceReady(resource: Drawable) {
|
||||
blurImage.setImageDrawable(resource)
|
||||
callback.onBlurLoaded()
|
||||
}
|
||||
|
||||
override fun onLoadFailed() {
|
||||
callback.onBlurFailed()
|
||||
}
|
||||
}
|
||||
|
||||
fun load() {
|
||||
val cacheValue = storyCache.getFromCache(cacheKey)
|
||||
if (cacheValue != null) {
|
||||
loadViaCache(cacheValue)
|
||||
} else {
|
||||
loadViaGlide(blurHash, storySize)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
GlideApp.with(blurImage).clear(blurImage)
|
||||
|
||||
blurImage.setImageDrawable(null)
|
||||
}
|
||||
|
||||
private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) {
|
||||
Log.d(TAG, "Blur in cache. Loading via cache...")
|
||||
|
||||
val blurTarget = cacheValue.blurTarget
|
||||
if (blurTarget != null) {
|
||||
blurTarget.addListener(blurListener)
|
||||
lifecycle.addObserver(OnDestroy { blurTarget.removeListener(blurListener) })
|
||||
} else {
|
||||
callback.onBlurFailed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadViaGlide(blurHash: BlurHash?, storySize: StoryDisplay.Size) {
|
||||
if (blurHash != null) {
|
||||
GlideApp.with(blurImage)
|
||||
.load(blurHash)
|
||||
.override(storySize.width, storySize.height)
|
||||
.addListener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
callback.onBlurFailed()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||
callback.onBlurLoaded()
|
||||
return false
|
||||
}
|
||||
})
|
||||
.into(blurImage)
|
||||
} else {
|
||||
callback.onBlurFailed()
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onBlurLoaded()
|
||||
fun onBlurFailed()
|
||||
}
|
||||
|
||||
private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
onDestroy()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import com.bumptech.glide.load.engine.GlideException
|
|||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryCache
|
||||
|
@ -24,9 +23,19 @@ class StoryImageLoader(
|
|||
private val storyCache: StoryCache,
|
||||
private val storySize: StoryDisplay.Size,
|
||||
private val postImage: ImageView,
|
||||
private val blurImage: ImageView,
|
||||
blurImage: ImageView,
|
||||
private val callback: StoryPostFragment.Callback
|
||||
) {
|
||||
) : StoryBlurLoader.Callback {
|
||||
|
||||
private val blurLoader = StoryBlurLoader(
|
||||
fragment.viewLifecycleOwner.lifecycle,
|
||||
imagePost.blurHash,
|
||||
imagePost.imageUri,
|
||||
storyCache,
|
||||
storySize,
|
||||
blurImage,
|
||||
this
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StoryImageLoader::class.java)
|
||||
|
@ -48,77 +57,35 @@ class StoryImageLoader(
|
|||
}
|
||||
}
|
||||
|
||||
private val blurListener = object : StoryCache.Listener {
|
||||
override fun onResourceReady(resource: Drawable) {
|
||||
blurImage.setImageDrawable(resource)
|
||||
blurState = LoadState.READY
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
override fun onLoadFailed() {
|
||||
blurState = LoadState.FAILED
|
||||
notifyListeners()
|
||||
}
|
||||
}
|
||||
|
||||
fun load() {
|
||||
val cacheValue = storyCache.getFromCache(imagePost.imageUri)
|
||||
if (cacheValue != null) {
|
||||
loadViaCache(cacheValue)
|
||||
} else {
|
||||
loadViaGlide(imagePost.blurHash, storySize)
|
||||
loadViaGlide(storySize)
|
||||
}
|
||||
|
||||
blurLoader.load()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
GlideApp.with(postImage).clear(postImage)
|
||||
GlideApp.with(blurImage).clear(blurImage)
|
||||
|
||||
postImage.setImageDrawable(null)
|
||||
blurImage.setImageDrawable(null)
|
||||
|
||||
blurLoader.clear()
|
||||
}
|
||||
|
||||
private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) {
|
||||
Log.d(TAG, "Attachment in cache. Loading via cache...")
|
||||
val blurTarget = cacheValue.blurTarget
|
||||
if (blurTarget != null) {
|
||||
blurTarget.addListener(blurListener)
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(OnDestroy { blurTarget.removeListener(blurListener) })
|
||||
} else {
|
||||
blurState = LoadState.FAILED
|
||||
notifyListeners()
|
||||
}
|
||||
Log.d(TAG, "Image in cache. Loading via cache...")
|
||||
|
||||
val imageTarget = cacheValue.imageTarget
|
||||
val imageTarget = cacheValue.imageTarget!!
|
||||
imageTarget.addListener(imageListener)
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(OnDestroy { imageTarget.removeListener(blurListener) })
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(OnDestroy { imageTarget.removeListener(imageListener) })
|
||||
}
|
||||
|
||||
private fun loadViaGlide(blurHash: BlurHash?, storySize: StoryDisplay.Size) {
|
||||
Log.d(TAG, "Attachment not in cache. Loading via glide...")
|
||||
if (blurHash != null) {
|
||||
GlideApp.with(blurImage)
|
||||
.load(blurHash)
|
||||
.override(storySize.width, storySize.height)
|
||||
.addListener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
blurState = LoadState.FAILED
|
||||
notifyListeners()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||
blurState = LoadState.READY
|
||||
notifyListeners()
|
||||
return false
|
||||
}
|
||||
})
|
||||
.into(blurImage)
|
||||
} else {
|
||||
blurState = LoadState.FAILED
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
private fun loadViaGlide(storySize: StoryDisplay.Size) {
|
||||
Log.d(TAG, "Image not in cache. Loading via glide...")
|
||||
GlideApp.with(postImage)
|
||||
.load(DecryptableStreamUriLoader.DecryptableUri(imagePost.imageUri))
|
||||
.override(storySize.width, storySize.height)
|
||||
|
@ -138,6 +105,16 @@ class StoryImageLoader(
|
|||
.into(postImage)
|
||||
}
|
||||
|
||||
override fun onBlurLoaded() {
|
||||
blurState = LoadState.READY
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
override fun onBlurFailed() {
|
||||
blurState = LoadState.FAILED
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
private fun notifyListeners() {
|
||||
if (fragment.isDetached) {
|
||||
Log.w(TAG, "Fragment is detached, dropping notify call.")
|
||||
|
@ -153,15 +130,15 @@ class StoryImageLoader(
|
|||
}
|
||||
}
|
||||
|
||||
private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
private enum class LoadState {
|
||||
INIT,
|
||||
READY,
|
||||
FAILED
|
||||
}
|
||||
|
||||
private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
onDestroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,12 +110,23 @@ class StoryPostFragment : Fragment(R.layout.stories_post_fragment) {
|
|||
presentNone()
|
||||
|
||||
binding.video.visible = true
|
||||
binding.blur.visible = true
|
||||
|
||||
val storyBlurLoader = StoryBlurLoader(
|
||||
viewLifecycleOwner.lifecycle,
|
||||
state.blurHash,
|
||||
state.videoUri,
|
||||
pageViewModel.storyCache,
|
||||
StoryDisplay.getStorySize(resources),
|
||||
binding.blur
|
||||
)
|
||||
|
||||
storyVideoLoader = StoryVideoLoader(
|
||||
this,
|
||||
state,
|
||||
binding.video,
|
||||
requireCallback()
|
||||
requireCallback(),
|
||||
storyBlurLoader
|
||||
)
|
||||
|
||||
storyVideoLoader?.load()
|
||||
|
|
|
@ -24,7 +24,8 @@ sealed class StoryPostState {
|
|||
val videoUri: Uri,
|
||||
val size: Long,
|
||||
val clipStart: Duration,
|
||||
val clipEnd: Duration
|
||||
val clipEnd: Duration,
|
||||
val blurHash: BlurHash?
|
||||
) : StoryPostState()
|
||||
|
||||
data class None(private val ts: Long = System.currentTimeMillis()) : StoryPostState()
|
||||
|
|
|
@ -44,7 +44,8 @@ class StoryPostViewModel(private val repository: StoryTextPostRepository) : View
|
|||
videoUri = storyPostContent.uri,
|
||||
size = storyPostContent.attachment.size,
|
||||
clipStart = storyPostContent.attachment.transformProperties.videoTrimStartTimeUs.microseconds,
|
||||
clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds
|
||||
clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds,
|
||||
blurHash = storyPostContent.attachment.blurHash
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -13,7 +13,8 @@ class StoryVideoLoader(
|
|||
private val fragment: StoryPostFragment,
|
||||
private val videoPost: StoryPostState.VideoPost,
|
||||
private val videoPlayer: VideoPlayer,
|
||||
private val callback: StoryPostFragment.Callback
|
||||
private val callback: StoryPostFragment.Callback,
|
||||
private val blurLoader: StoryBlurLoader
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
companion object {
|
||||
|
@ -25,11 +26,13 @@ class StoryVideoLoader(
|
|||
videoPlayer.setVideoSource(VideoSlide(fragment.requireContext(), videoPost.videoUri, videoPost.size, false), false, TAG, videoPost.clipStart.inWholeMilliseconds, videoPost.clipEnd.inWholeMilliseconds)
|
||||
videoPlayer.hideControls()
|
||||
videoPlayer.setKeepContentOnPlayerReset(false)
|
||||
blurLoader.load()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
fragment.viewLifecycleOwner.lifecycle.removeObserver(this)
|
||||
videoPlayer.stop()
|
||||
blurLoader.clear()
|
||||
}
|
||||
|
||||
override fun onResume(lifecycleOwner: LifecycleOwner) {
|
||||
|
|
Loading…
Add table
Reference in a new issue