Gerar miniaturas de mídia

As miniaturas de mídia oferecem aos usuários uma prévia visual rápida de imagens e vídeos, tornando a navegação mais rápida e deixando a interface do app mais visualmente atrativa e envolvente. Como as miniaturas são menores que as mídias de tamanho normal, elas ajudam a economizar memória, espaço de armazenamento e largura de banda, além de melhorar o desempenho da navegação de mídia.

Dependendo do tipo de arquivo e do acesso a arquivos que você tem em seu aplicativo e seus recursos de mídia, é possível criar miniaturas de várias formas.

Criar uma miniatura usando uma biblioteca de carregamento de imagens

As bibliotecas de carregamento de imagens fazem grande parte do trabalho pesado para você. podem lidar com armazenamento em cache, além da lógica para buscar a mídia de origem do servidor com base em um Uri. O código a seguir demonstra o uso da biblioteca de carregamento de imagens Coil (link em inglês) para imagens e vídeos, e funciona em um recurso local ou de rede.

// Use Coil to create and display a thumbnail of a video or image with a specific height
// ImageLoader has its own memory and storage cache, and this one is configured to also
// load frames from videos
val videoEnabledLoader = ImageLoader.Builder(context)
    .components {
        add(VideoFrameDecoder.Factory())
    }.build()
// Coil requests images that match the size of the AsyncImage composable, but this allows
// for precise control of the height
val request = ImageRequest.Builder(context)
    .data(mediaUri)
    .size(Int.MAX_VALUE, THUMBNAIL_HEIGHT)
    .build()
AsyncImage(
    model = request,
    imageLoader = videoEnabledLoader,
    modifier = Modifier
        .clip(RoundedCornerShape(20))    ,
    contentDescription = null
)

Se possível, crie miniaturas do lado do servidor. Consulte Como carregar imagens para saber como carregar imagens usando o Compose e Como carregar bitmaps grandes de maneira eficiente para saber como trabalhar com imagens grandes.

Criar uma miniatura de um arquivo de imagem local

A obtenção de imagens de miniatura envolve uma redução de escala eficiente, preservando a qualidade visual, evitando o uso excessivo de memória, lidando com vários formatos de imagem e fazendo uso correto dos dados Exif.

O método createImageThumbnail faz tudo isso, desde que você tenha acesso ao caminho do arquivo de imagem.

val bitmap = ThumbnailUtils.createImageThumbnail(File(file_path), Size(640, 480), null)

Se você tiver apenas o Uri, poderá usar o método loadThumbnail no ContentResolver a partir do Android 10, nível 29 da API.

val thumbnail: Bitmap =
        applicationContext.contentResolver.loadThumbnail(
        content-uri, Size(640, 480), null)

O ImageDecoder, disponível a partir do Android 9, nível 28 da API, tem algumas opções sólidas para reamostrar a imagem à medida que você a decodifica para evitar o uso de mais memória.

class DecodeResampler(val size: Size, val signal: CancellationSignal?) : OnHeaderDecodedListener {
    private val size: Size

   override fun onHeaderDecoded(decoder: ImageDecoder, info: ImageInfo, source:
       // sample down if needed.
        val widthSample = info.size.width / size.width
        val heightSample = info.size.height / size.height
        val sample = min(widthSample, heightSample)
        if (sample > 1) {
            decoder.setTargetSampleSize(sample)
        }
    }
}

val resampler = DecoderResampler(size, null)
val source = ImageDecoder.createSource(context.contentResolver, imageUri)
val bitmap = ImageDecoder.decodeBitmap(source, resampler);

É possível usar o BitmapFactory para criar miniaturas de apps destinados a versões anteriores do Android. BitmapFactory.Options tem uma configuração para decodificar apenas os limites de uma imagem para fins de reamostragem.

Primeiro, decodifique apenas os limites do bitmap no BitmapFactory.Options:

private fun decodeResizedBitmap(context: Context, uri: Uri, size: Size): Bitmap?{
    val boundsStream = context.contentResolver.openInputStream(uri)
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeStream(boundsStream, null, options)
    boundsStream?.close()

Usar width e height de BitmapFactory.Options para definir a amostra tamanho:

if ( options.outHeight != 0 ) {
        // we've got bounds
        val widthSample = options.outWidth / size.width
        val heightSample = options.outHeight / size.height
        val sample = min(widthSample, heightSample)
        if (sample > 1) {
            options.inSampleSize = sample
        }
    }

Decodifique o stream. O tamanho da imagem resultante é amostrado por potências de dois com base no inSampleSize.

    options.inJustDecodeBounds = false
    val decodeStream = context.contentResolver.openInputStream(uri)
    val bitmap =  BitmapFactory.decodeStream(decodeStream, null, options)
    decodeStream?.close()
    return bitmap
}

Criar uma miniatura a partir de um arquivo de vídeo local

A obtenção de imagens de miniaturas de vídeos envolve muitos dos mesmos desafios de obter miniaturas de imagens, mas os tamanhos dos arquivos podem ser muito maiores e frame de vídeo representativo nem sempre é tão simples quanto escolher o primeiro frame do vídeo.

O método createVideoThumbnail é uma boa escolha se você tiver acesso ao caminho do arquivo de vídeo.

val bitmap = ThumbnailUtils.createVideoThumbnail(File(file_path), Size(640, 480), null)

Se você só tiver acesso a um URI de conteúdo, poderá usar MediaMetadataRetriever

Primeiro, verifique se o vídeo tem uma miniatura incorporada e use-a se possível:

private suspend fun getVideoThumbnailFromMediaMetadataRetriever(context: Context, uri: Uri, size: Size): Bitmap? {
    val mediaMetadataRetriever = MediaMetadataRetriever()
    mediaMetadataRetriever.setDataSource(context, uri)
    val thumbnailBytes = mediaMetadataRetriever.embeddedPicture
    val resizer = Resizer(size, null)
    ImageDecoder.createSource(context.contentResolver, uri)
    // use a built-in thumbnail if the media file has it
    thumbnailBytes?.let {
        return ImageDecoder.decodeBitmap(ImageDecoder.createSource(it));
    }

Busque a largura e a altura do vídeo do MediaMetadataRetriever para calcule o fator de escalonamento:

val width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
            ?.toFloat() ?: size.width.toFloat()
    val height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
            ?.toFloat() ?: size.height.toFloat()
    val widthRatio = size.width.toFloat() / width
    val heightRatio = size.height.toFloat() / height
    val ratio = max(widthRatio, heightRatio)

No Android 9 e versões mais recentes (nível 28 da API), a MediaMetadataRetriever pode retornar frame:

if (ratio > 1) {
        val requestedWidth = width * ratio
        val requestedHeight = height * ratio
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            val frame = mediaMetadataRetriever.getScaledFrameAtTime(
                -1, OPTION_PREVIOUS_SYNC,
                requestedWidth.toInt(), requestedHeight.toInt())
            mediaMetadataRetriever.close()
            return frame
        }
    }

Caso contrário, retorne o primeiro frame sem escala:

    // consider scaling this after the fact
    val frame = mediaMetadataRetriever.frameAtTime
    mediaMetadataRetriever.close()
    return frame
}