From bb08f3dcb6d9386453371043b338d639c0f3620e Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 21 Jan 2021 15:08:19 +0900 Subject: [PATCH] video: sized thumbnails match content resolver ones --- .../aves/decoder/VideoThumbnailGlideModule.kt | 28 +++++++++++++++++-- lib/widgets/collection/thumbnail/raster.dart | 4 +-- lib/widgets/viewer/visual/video.dart | 4 ++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt index 7a39b7b0a..278e55cc6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -1,7 +1,9 @@ package deckers.thibault.aves.decoder import android.content.Context +import android.media.MediaMetadataRetriever import android.net.Uri +import android.os.Build import com.bumptech.glide.Glide import com.bumptech.glide.Priority import com.bumptech.glide.Registry @@ -48,9 +50,29 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe val retriever = openMetadataRetriever(model.context, model.uri) if (retriever != null) { try { - val picture = retriever.embeddedPicture ?: retriever.frameAtTime?.getBytes(canHaveAlpha = false, recycle = false) - if (picture != null) { - callback.onDataReady(ByteArrayInputStream(picture)) + var bytes = retriever.embeddedPicture + if (bytes == null) { + // try to match the thumbnails returned by the content resolver / Media Store + // the following strategies are from empirical evidence from a few test devices: + // - API 29: sync frame closest to the middle + // - API 26/27: default representative frame at any time position + var timeMillis: Long? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() + if (durationMillis != null) { + timeMillis = durationMillis / 2 + } + } + val frame = if (timeMillis != null) { + retriever.getFrameAtTime(timeMillis * 1000) + } else { + retriever.frameAtTime + } + bytes = frame?.getBytes(canHaveAlpha = false, recycle = false) + } + + if (bytes != null) { + callback.onDataReady(ByteArrayInputStream(bytes)) } else { callback.onLoadFailed(Exception("failed to get embedded picture or any frame")) } diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index d0cfe0d24..dcfb8a786 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -70,9 +70,7 @@ class _RasterImageThumbnailState extends State { if (!entry.canDecode) return; _fastThumbnailProvider = entry.getThumbnail(); - if (!entry.isVideo) { - _sizedThumbnailProvider = entry.getThumbnail(extent: extent); - } + _sizedThumbnailProvider = entry.getThumbnail(extent: extent); } void _pauseProvider() { diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 4cb1066a8..aac61c5e4 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -3,6 +3,8 @@ import 'dart:ui'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; @@ -98,7 +100,7 @@ class _VideoViewState extends State { backgroundColor: Colors.transparent, ) : Image( - image: entry.getBestThumbnail(entry.displaySize.longestSide), + image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)), fit: BoxFit.contain, ); });