mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-21 07:40:10 +01:00
Use trimmed video start for thumbnail generation.
This commit is contained in:
committed by
Michelle Tang
parent
5e4865be73
commit
d1e2fc0423
@@ -36,6 +36,7 @@ import com.bumptech.glide.request.Request;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import org.signal.core.models.media.TransformProperties;
|
||||
import org.signal.core.util.concurrent.ListenableFuture;
|
||||
import org.signal.core.util.concurrent.SettableFuture;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -608,7 +609,14 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
private RequestBuilder<Drawable> buildThumbnailRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
|
||||
RequestBuilder<Drawable> requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getDisplayUri())))
|
||||
long videoTrimStartTimeUs = 0;
|
||||
TransformProperties transformProperties = slide.asAttachment().transformProperties;
|
||||
|
||||
if (transformProperties != null && !transformProperties.shouldSkipTransform()) {
|
||||
videoTrimStartTimeUs = transformProperties.videoTrimStartTimeUs;
|
||||
}
|
||||
|
||||
RequestBuilder<Drawable> requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getDisplayUri()), videoTrimStartTimeUs))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.transition(withCrossFade()));
|
||||
|
||||
+8
-1
@@ -158,9 +158,16 @@ class V2ConversationItemThumbnail @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
if (thumbnailUri != null) {
|
||||
val transformProperties = thumbnailAttachment.transformProperties
|
||||
val videoTrimStartTimeUs = if (transformProperties != null && !transformProperties.skipTransform) {
|
||||
transformProperties.videoTrimStartTimeUs
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
|
||||
conversationContext
|
||||
.requestManager
|
||||
.load(DecryptableUri(thumbnailUri))
|
||||
.load(DecryptableUri(thumbnailUri, videoTrimStartTimeUs))
|
||||
.centerInside()
|
||||
.dontAnimate()
|
||||
.override(thumbnailSize.width, thumbnailSize.height)
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import org.signal.glide.common.io.InputStreamFactory
|
||||
import org.thoughtcrime.securesms.glide.DecryptableStreamFactory
|
||||
|
||||
object SignalGlideDependenciesProvider : SignalGlideDependencies.Provider {
|
||||
override fun getUriInputStreamFactory(uri: Uri): InputStreamFactory {
|
||||
return DecryptableStreamFactory(uri)
|
||||
override fun getUriInputStreamFactory(uri: Uri, thumbnailTimeUs: Long): InputStreamFactory {
|
||||
return DecryptableStreamFactory(uri, thumbnailTimeUs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import java.io.InputStream
|
||||
* A factory that creates a new [InputStream] for the given [Uri] each time [create] is called.
|
||||
*/
|
||||
class DecryptableStreamFactory(
|
||||
private val uri: Uri
|
||||
private val uri: Uri,
|
||||
private val thumbnailTimeUs: Long = 0
|
||||
) : InputStreamFactory {
|
||||
companion object {
|
||||
private val TAG = Log.tag(DecryptableStreamFactory::class)
|
||||
@@ -23,7 +24,7 @@ class DecryptableStreamFactory(
|
||||
|
||||
override fun create(): InputStream {
|
||||
return try {
|
||||
DecryptableStreamLocalUriFetcher(AppDependencies.application, uri).loadResource(uri, AppDependencies.application.contentResolver)
|
||||
DecryptableStreamLocalUriFetcher(AppDependencies.application, uri, thumbnailTimeUs).loadResource(uri, AppDependencies.application.contentResolver)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error creating input stream for URI.", e)
|
||||
throw e
|
||||
|
||||
+6
-3
@@ -35,16 +35,19 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
private static final long TOTAL_PIXEL_SIZE_LIMIT = 200_000_000L; // 200 megapixels
|
||||
|
||||
private final Context context;
|
||||
private final long thumbnailTimeUs;
|
||||
|
||||
DecryptableStreamLocalUriFetcher(Context context, Uri uri) {
|
||||
DecryptableStreamLocalUriFetcher(Context context, Uri uri, long thumbnailTimeUs) {
|
||||
super(context.getContentResolver(), uri);
|
||||
this.context = context;
|
||||
this.context = context;
|
||||
this.thumbnailTimeUs = thumbnailTimeUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException {
|
||||
if (MediaUtil.hasVideoThumbnail(context, uri)) {
|
||||
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000);
|
||||
long timeUs = thumbnailTimeUs > 0 ? thumbnailTimeUs : 1000;
|
||||
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, timeUs);
|
||||
|
||||
if (thumbnail != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
+5
-1
@@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionState
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity
|
||||
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -339,7 +340,10 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
pagerAdapter.submitMedia(state.selectedMedia)
|
||||
|
||||
selectionAdapter.submitList(
|
||||
state.selectedMedia.map { MediaReviewSelectedItem.Model(it, state.focusedMedia == it) } + MediaReviewAddItem.Model
|
||||
state.selectedMedia.map {
|
||||
val trimStartTimeUs = (state.editorStateMap[it.uri] as? VideoTrimData)?.startTimeUs ?: 0L
|
||||
MediaReviewSelectedItem.Model(it, state.focusedMedia == it, trimStartTimeUs)
|
||||
} + MediaReviewAddItem.Model
|
||||
)
|
||||
|
||||
presentSendButton(readyToSend, state.sendType, state.recipient)
|
||||
|
||||
+3
-3
@@ -20,13 +20,13 @@ object MediaReviewSelectedItem {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onSelectedMediaClicked) }, R.layout.v2_media_review_selected_item))
|
||||
}
|
||||
|
||||
class Model(val media: Media, val isSelected: Boolean) : MappingModel<Model> {
|
||||
class Model(val media: Media, val isSelected: Boolean, val videoTrimStartTimeUs: Long = 0) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return media == newItem.media
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return media == newItem.media && isSelected == newItem.isSelected
|
||||
return media == newItem.media && isSelected == newItem.isSelected && videoTrimStartTimeUs == newItem.videoTrimStartTimeUs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ object MediaReviewSelectedItem {
|
||||
|
||||
override fun bind(model: Model) {
|
||||
Glide.with(imageView)
|
||||
.load(DecryptableUri(model.media.uri))
|
||||
.load(DecryptableUri(model.media.uri, model.videoTrimStartTimeUs))
|
||||
.centerCrop()
|
||||
.into(imageView)
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ object SignalGlideDependencies {
|
||||
val application: Application
|
||||
get() = _application
|
||||
|
||||
fun getUriInputStreamFactory(uri: Uri): InputStreamFactory = _provider.getUriInputStreamFactory(uri)
|
||||
fun getUriInputStreamFactory(uri: Uri, thumbnailTimeUs: Long = 0): InputStreamFactory = _provider.getUriInputStreamFactory(uri, thumbnailTimeUs)
|
||||
|
||||
interface Provider {
|
||||
/**
|
||||
* A factory which can create an [java.io.InputStream] from a given [Uri]
|
||||
* A factory which can create an [java.io.InputStream] from a given [Uri]. For videos, [thumbnailTimeUs] specifies the frame time to extract.
|
||||
*/
|
||||
fun getUriInputStreamFactory(uri: Uri): InputStreamFactory
|
||||
fun getUriInputStreamFactory(uri: Uri, thumbnailTimeUs: Long = 0): InputStreamFactory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import java.io.InputStream
|
||||
interface InputStreamFactory {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun build(uri: Uri): InputStreamFactory = SignalGlideDependencies.getUriInputStreamFactory(uri)
|
||||
@JvmOverloads
|
||||
fun build(uri: Uri, thumbnailTimeUs: Long = 0): InputStreamFactory = SignalGlideDependencies.getUriInputStreamFactory(uri, thumbnailTimeUs)
|
||||
|
||||
@JvmStatic
|
||||
fun build(file: File): InputStreamFactory = FileInputStreamFactory(file)
|
||||
|
||||
@@ -9,8 +9,9 @@ import android.net.Uri
|
||||
import com.bumptech.glide.load.Key
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class DecryptableUri(val uri: Uri) : Key {
|
||||
data class DecryptableUri @JvmOverloads constructor(val uri: Uri, val thumbnailTimeUs: Long = 0) : Key {
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(uri.toString().toByteArray())
|
||||
messageDigest.update(thumbnailTimeUs.toString().toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ class DecryptableUriStreamFetcher(
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStreamFactory>) {
|
||||
try {
|
||||
callback.onDataReady(InputStreamFactory.build(decryptableUri.uri))
|
||||
callback.onDataReady(InputStreamFactory.build(decryptableUri.uri, decryptableUri.thumbnailTimeUs))
|
||||
} catch (e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user