diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt index d7263930d..97d3a2133 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt @@ -70,6 +70,7 @@ fun VideoView( alwaysShowVideo: Boolean = false, thumbhash: String? = null, isLiveStream: Boolean = false, + hash: String? = null, ) { val borderModifier = if (roundedCorner) { @@ -80,7 +81,7 @@ fun VideoView( Modifier } - VideoView(videoUri, mimeType, title, thumb, borderModifier, contentScale, waveform, artworkUri, authorName, dimensions, blurhash, nostrUriCallback, onDialog, alwaysShowVideo, accountViewModel = accountViewModel, thumbhash = thumbhash, isLiveStream = isLiveStream) + VideoView(videoUri, mimeType, title, thumb, borderModifier, contentScale, waveform, artworkUri, authorName, dimensions, blurhash, nostrUriCallback, onDialog, alwaysShowVideo, accountViewModel = accountViewModel, thumbhash = thumbhash, isLiveStream = isLiveStream, hash = hash) } @Composable @@ -103,6 +104,7 @@ fun VideoView( isLiveStream: Boolean = false, accountViewModel: AccountViewModel, thumbhash: String? = null, + hash: String? = null, ) { val initialAutoStart = if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback() // Reset the manual-show toggle when the video URI changes so a recycled feed slot @@ -160,6 +162,10 @@ fun VideoView( isLiveStream = isLiveStream, onZoom = onDialog, hasBlurhash = false, + blurhash = blurhash, + dim = dimensions, + hash = hash, + thumbhash = thumbhash, accountViewModel = accountViewModel, showControls = showControls, ) @@ -207,6 +213,10 @@ fun VideoView( isLiveStream = isLiveStream, onZoom = onDialog, hasBlurhash = true, + blurhash = blurhash, + dim = dimensions, + hash = hash, + thumbhash = thumbhash, accountViewModel = accountViewModel, showControls = showControls, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index 49e5651a4..46cccd28c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.VideoQual import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag val DEFAULT_MUTED_SETTING = mutableStateOf(true) @@ -54,6 +55,10 @@ fun VideoViewInner( onZoom: (() -> Unit)? = null, hasBlurhash: Boolean = false, isFullscreen: Boolean = false, + blurhash: String? = null, + dim: DimensionTag? = null, + hash: String? = null, + thumbhash: String? = null, accountViewModel: AccountViewModel, ) { // keeps a copy of the value to avoid recompositions here when the DEFAULT value changes @@ -76,6 +81,10 @@ fun VideoViewInner( keepPlaying = true, waveformData = waveform, isLiveStream = isLiveStream, + blurhash = blurhash, + dim = dim, + hash = hash, + thumbhash = thumbhash, ) { mediaItem -> GetVideoController( mediaItem = mediaItem, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt index c694c688c..a565b2376 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt @@ -303,9 +303,9 @@ fun RenderTopButtons( popupExpanded = shareDialogVisible, videoUri = mediaData.videoUri, postNostrUri = mediaData.callbackUri, - blurhash = null, - dim = null, - hash = null, + blurhash = mediaData.blurhash, + dim = mediaData.dim, + hash = mediaData.hash, mimeType = mediaData.mimeType, onDismiss = { shareDialogVisible.value = false }, content = @@ -316,6 +316,10 @@ fun RenderTopButtons( authorName = mediaData.authorName, description = mediaData.title, uri = mediaData.callbackUri, + blurhash = mediaData.blurhash, + dim = mediaData.dim, + hash = mediaData.hash, + thumbhash = mediaData.thumbhash, ), accountViewModel = accountViewModel, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt index 11e6c1fde..94ad639af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.service.playback.composable.WaveformData +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag val mediaItemCache = MediaItemCache() @@ -41,6 +42,10 @@ fun GetMediaItem( keepPlaying: Boolean = false, waveformData: WaveformData? = null, isLiveStream: Boolean = false, + blurhash: String? = null, + dim: DimensionTag? = null, + hash: String? = null, + thumbhash: String? = null, inner: @Composable (LoadedMediaItem) -> Unit, ) { val data = @@ -57,6 +62,10 @@ fun GetMediaItem( keepPlaying = keepPlaying, waveformData = waveformData, isLiveStream = isLiveStream, + blurhash = blurhash, + dim = dim, + hash = hash, + thumbhash = thumbhash, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt index 44e6441ab..7c771951c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.playback.composable.mediaitem import androidx.compose.runtime.Immutable import androidx.media3.common.MediaItem import com.vitorpamplona.amethyst.service.playback.composable.WaveformData +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag @Immutable data class MediaItemData( @@ -40,6 +41,13 @@ data class MediaItemData( // (e.g. multi-rendition NIP-71 videos) must leave this false so that // ExoPlayer's SimpleCache can cache their immutable segments. val isLiveStream: Boolean = false, + // Source-imeta visual fields — carried through so the player's "Add to Gallery" / + // "Share" actions can publish a richer ProfileGalleryEntryEvent. Optional; null when + // the surface that constructed this MediaItemData didn't have them. + val blurhash: String? = null, + val dim: DimensionTag? = null, + val hash: String? = null, + val thumbhash: String? = null, ) @Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt index 5d8aed97c..c85e13ade 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt @@ -48,6 +48,8 @@ data class PreviewHashes( } object PreviewMetadataCalculator { + private const val LOG_TAG = "PreviewMetadataCalc" + private fun isImage(mimeType: String?) = mimeType?.startsWith("image/", ignoreCase = true) == true private fun isVideo(mimeType: String?) = mimeType?.startsWith("video/", ignoreCase = true) == true @@ -117,7 +119,7 @@ object PreviewMetadataCalculator { } } } catch (e: Exception) { - Log.w("PreviewMetadataCalc", "Failed to compute metadata from uri", e) + Log.w(LOG_TAG, "Failed to compute metadata from uri", e) null } } @@ -133,8 +135,14 @@ object PreviewMetadataCalculator { private fun processBitmap(bitmap: Bitmap?): PreviewHashes = if (bitmap != null) { try { - val blurhash = runCatching { BlurhashWrapper(bitmap.toBlurhash()) }.getOrNull() - val thumbhash = runCatching { ThumbhashWrapper(bitmap.toThumbhash()) }.getOrNull() + val blurhash = + runCatching { BlurhashWrapper(bitmap.toBlurhash()) } + .onFailure { Log.w(LOG_TAG, "blurhash generation failed", it) } + .getOrNull() + val thumbhash = + runCatching { ThumbhashWrapper(bitmap.toThumbhash()) } + .onFailure { Log.w(LOG_TAG, "thumbhash generation failed", it) } + .getOrNull() PreviewHashes( blurhash = blurhash, thumbhash = thumbhash, @@ -153,6 +161,9 @@ object PreviewMetadataCalculator { ): PreviewHashes { val dim = retriever.prepareDimFromVideo() ?: dimPrecomputed val thumb = retriever.getThumbnail() + if (thumb == null) { + Log.w(LOG_TAG) { "video frame extraction returned null; no blurhash/thumbhash will be generated" } + } val hashes = processBitmap(thumb) val finalDim = if (dim?.hasSize() == true) dim else hashes.dim return hashes.copy(dim = finalDim) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 64889c829..21aaca8d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -226,6 +226,7 @@ fun ZoomableContentView( accountViewModel = accountViewModel, thumbhash = content.thumbhash, isLiveStream = content.isLiveStream, + hash = content.hash, ) } } @@ -961,7 +962,10 @@ fun ShareMediaAction( // still — the .m3u8 URL itself is a text manifest. Only // MediaUrlVideo carries an artworkUri. val posterUrl = (content as? MediaUrlVideo)?.artworkUri - accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType, image = posterUrl) + val thumbhashFromContent = + (content as? MediaUrlVideo)?.thumbhash + ?: (content as? MediaUrlImage)?.thumbhash + accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType, thumbhash = thumbhashFromContent, image = posterUrl) accountViewModel.toastManager.toast(R.string.media_added, R.string.media_added_to_profile_gallery) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index af02c9f5d..a45ab3186 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -896,8 +896,9 @@ class AccountViewModel( dim: DimensionTag?, hash: String?, mimeType: String?, + thumbhash: String? = null, image: String? = null, - ) = launchSigner { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType, image = image) } + ) = launchSigner { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType, thumbhash = thumbhash, image = image) } fun removeFromMediaGallery(note: Note) = launchSigner { account.removeFromGallery(note) }