Add blur hashes behind videos.

main
Alex Hart 2022-11-15 10:30:23 -04:00
rodzic fb8e81cf50
commit 3e2ecdaaa9
7 zmienionych plików z 176 dodań i 71 usunięć

Wyświetl plik

@ -29,15 +29,17 @@ class StoryCache(
val prefetchableAttachments: List<Attachment> = attachments val prefetchableAttachments: List<Attachment> = attachments
.asSequence() .asSequence()
.filter { it.uri != null && it.uri !in cache } .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 } .filter { it.transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE }
.toList() .toList()
val newMappings: Map<Uri, StoryCacheValue> = prefetchableAttachments.associateWith { attachment -> val newMappings: Map<Uri, StoryCacheValue> = prefetchableAttachments.associateWith { attachment ->
val imageTarget = glideRequests val imageTarget = if (MediaUtil.isImage(attachment)) {
.load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!)) glideRequests
.priority(Priority.HIGH) .load(DecryptableStreamUriLoader.DecryptableUri(attachment.uri!!))
.into(StoryCacheTarget(attachment.uri!!, storySize)) .priority(Priority.HIGH)
.into(StoryCacheTarget(attachment.uri!!, storySize))
} else null
val blurTarget = if (attachment.blurHash != null) { val blurTarget = if (attachment.blurHash != null) {
glideRequests glideRequests
@ -79,7 +81,7 @@ class StoryCache(
/** /**
* Represents the load targets for an image and blur. * 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. * A custom glide target for loading a drawable. Placeholder immediately clears, and we don't want to do that, so we use this instead.

Wyświetl plik

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

Wyświetl plik

@ -9,7 +9,6 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.stories.viewer.page.StoryCache import org.thoughtcrime.securesms.stories.viewer.page.StoryCache
@ -24,9 +23,19 @@ class StoryImageLoader(
private val storyCache: StoryCache, private val storyCache: StoryCache,
private val storySize: StoryDisplay.Size, private val storySize: StoryDisplay.Size,
private val postImage: ImageView, private val postImage: ImageView,
private val blurImage: ImageView, blurImage: ImageView,
private val callback: StoryPostFragment.Callback private val callback: StoryPostFragment.Callback
) { ) : StoryBlurLoader.Callback {
private val blurLoader = StoryBlurLoader(
fragment.viewLifecycleOwner.lifecycle,
imagePost.blurHash,
imagePost.imageUri,
storyCache,
storySize,
blurImage,
this
)
companion object { companion object {
private val TAG = Log.tag(StoryImageLoader::class.java) 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() { fun load() {
val cacheValue = storyCache.getFromCache(imagePost.imageUri) val cacheValue = storyCache.getFromCache(imagePost.imageUri)
if (cacheValue != null) { if (cacheValue != null) {
loadViaCache(cacheValue) loadViaCache(cacheValue)
} else { } else {
loadViaGlide(imagePost.blurHash, storySize) loadViaGlide(storySize)
} }
blurLoader.load()
} }
fun clear() { fun clear() {
GlideApp.with(postImage).clear(postImage) GlideApp.with(postImage).clear(postImage)
GlideApp.with(blurImage).clear(blurImage)
postImage.setImageDrawable(null) postImage.setImageDrawable(null)
blurImage.setImageDrawable(null)
blurLoader.clear()
} }
private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) { private fun loadViaCache(cacheValue: StoryCache.StoryCacheValue) {
Log.d(TAG, "Attachment in cache. Loading via cache...") Log.d(TAG, "Image 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()
}
val imageTarget = cacheValue.imageTarget val imageTarget = cacheValue.imageTarget!!
imageTarget.addListener(imageListener) 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) { private fun loadViaGlide(storySize: StoryDisplay.Size) {
Log.d(TAG, "Attachment not in cache. Loading via glide...") Log.d(TAG, "Image 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()
}
GlideApp.with(postImage) GlideApp.with(postImage)
.load(DecryptableStreamUriLoader.DecryptableUri(imagePost.imageUri)) .load(DecryptableStreamUriLoader.DecryptableUri(imagePost.imageUri))
.override(storySize.width, storySize.height) .override(storySize.width, storySize.height)
@ -138,6 +105,16 @@ class StoryImageLoader(
.into(postImage) .into(postImage)
} }
override fun onBlurLoaded() {
blurState = LoadState.READY
notifyListeners()
}
override fun onBlurFailed() {
blurState = LoadState.FAILED
notifyListeners()
}
private fun notifyListeners() { private fun notifyListeners() {
if (fragment.isDetached) { if (fragment.isDetached) {
Log.w(TAG, "Fragment is detached, dropping notify call.") 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 { private enum class LoadState {
INIT, INIT,
READY, READY,
FAILED FAILED
} }
private inner class OnDestroy(private val onDestroy: () -> Unit) : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
onDestroy()
}
}
} }

Wyświetl plik

@ -110,12 +110,23 @@ class StoryPostFragment : Fragment(R.layout.stories_post_fragment) {
presentNone() presentNone()
binding.video.visible = true 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( storyVideoLoader = StoryVideoLoader(
this, this,
state, state,
binding.video, binding.video,
requireCallback() requireCallback(),
storyBlurLoader
) )
storyVideoLoader?.load() storyVideoLoader?.load()

Wyświetl plik

@ -24,7 +24,8 @@ sealed class StoryPostState {
val videoUri: Uri, val videoUri: Uri,
val size: Long, val size: Long,
val clipStart: Duration, val clipStart: Duration,
val clipEnd: Duration val clipEnd: Duration,
val blurHash: BlurHash?
) : StoryPostState() ) : StoryPostState()
data class None(private val ts: Long = System.currentTimeMillis()) : StoryPostState() data class None(private val ts: Long = System.currentTimeMillis()) : StoryPostState()

Wyświetl plik

@ -44,7 +44,8 @@ class StoryPostViewModel(private val repository: StoryTextPostRepository) : View
videoUri = storyPostContent.uri, videoUri = storyPostContent.uri,
size = storyPostContent.attachment.size, size = storyPostContent.attachment.size,
clipStart = storyPostContent.attachment.transformProperties.videoTrimStartTimeUs.microseconds, clipStart = storyPostContent.attachment.transformProperties.videoTrimStartTimeUs.microseconds,
clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds clipEnd = storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds,
blurHash = storyPostContent.attachment.blurHash
) )
} }
} else { } else {

Wyświetl plik

@ -13,7 +13,8 @@ class StoryVideoLoader(
private val fragment: StoryPostFragment, private val fragment: StoryPostFragment,
private val videoPost: StoryPostState.VideoPost, private val videoPost: StoryPostState.VideoPost,
private val videoPlayer: VideoPlayer, private val videoPlayer: VideoPlayer,
private val callback: StoryPostFragment.Callback private val callback: StoryPostFragment.Callback,
private val blurLoader: StoryBlurLoader
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
companion object { 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.setVideoSource(VideoSlide(fragment.requireContext(), videoPost.videoUri, videoPost.size, false), false, TAG, videoPost.clipStart.inWholeMilliseconds, videoPost.clipEnd.inWholeMilliseconds)
videoPlayer.hideControls() videoPlayer.hideControls()
videoPlayer.setKeepContentOnPlayerReset(false) videoPlayer.setKeepContentOnPlayerReset(false)
blurLoader.load()
} }
fun clear() { fun clear() {
fragment.viewLifecycleOwner.lifecycle.removeObserver(this) fragment.viewLifecycleOwner.lifecycle.removeObserver(this)
videoPlayer.stop() videoPlayer.stop()
blurLoader.clear()
} }
override fun onResume(lifecycleOwner: LifecycleOwner) { override fun onResume(lifecycleOwner: LifecycleOwner) {