Add blur hashes behind videos.

This commit is contained in:
Alex Hart 2022-11-15 10:30:23 -04:00
parent fb8e81cf50
commit 3e2ecdaaa9
7 changed files with 176 additions and 71 deletions

View file

@ -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.

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()

View file

@ -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()

View file

@ -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 {

View file

@ -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) {