Kotlin拿Android本地视频缩略图

Java获取视频缩略图(关键帧)。两种方法总结,汇总,踩坑。 最近因公司业务要做视频视频缩略图功能 首先说一下服务器环境,减少弯路。 服务器是ubuntu下打的docker镜像。 ok,继续。 总结两种方法: 第一种:通过Javacv生成。 第二种:服务器安装ffmpeg生成。 一:通过Java代码生成。 首先引入jar包: <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId&g 阅读详情

        本文主要讨论如下三个问题:

  1. 如何拿到本地视频?
  2. 怎么拿视频缩略图?
  3. 缩略图如何压缩?

1 如何拿到本地视频?

1.1 定义数据结构

        先定义媒体信息数据结构MediaInfo,以及视频信息数据结构VideoInfo。

open class MediaInfo(
    var size: Long = 0L, // 大小 单位B
    var width: Float = 0f, // 宽
    var height: Float = 0f, // 高
    var localPath: String = "", // 系统绝对路径
    var localPathUri: String = "", // 媒体文件Uri
    var fileName: String = "", // 文件名
    var mimeType: String = "", // 媒体类型
    var mediaId: String = "", // 媒体ID
    var lastModified: Long = 0L, // 最后更改时间

    /*----业务字段----*/
    var seq: Int = 0, // 事件序列号
    /*----业务字段----*/
)
/**
 * 码率(比特率),单位为 bps,比特率越高,传送的数据速度越快。在压缩视频时指定码率,则可确定压缩后的视频大小。
 * 视频大小(byte) = (duration(ms) / 1000) * (biteRate(bit/s) / 8)
 */
data class VideoInfo(
    var firstFrame: Bitmap? = null, // 视频第一帧图
    var duration: Long = 0L, // 视频长度 ms
    var biteRate: Long = 0L, // 视频码率 bps

    /* --------not necessary, maybe not value---- */
    var addTime: Long = 0L, // 视频添加时间
    var videoRotation: Int = 0, // 视频方向
    /* --------not necessary, maybe not value---- */

) : MediaInfo()

1.2 ContentResolver查询系统中视频

        这里直接去Android媒体库找到所有视频即可,代码使用了Kotlin协程,参考如下。

import android.provider.MediaStore.Video.Media

    /**
     * 获取系统所有视频文件
     */
    suspend fun getSystemVideos(contentResolver: ContentResolver): MutableList<MediaInfo> =
        withContext(Dispatchers.IO) {
            val videoList: MutableList<MediaInfo> = mutableListOf()
            var cursor: Cursor? = null
            try {
                val queryArray = arrayOf(
                    Media._ID,
                    Media.SIZE, // 视频大小
                    Media.WIDTH, // 视频宽
                    Media.HEIGHT, // 视频高
                    Media.DATA, // 视频绝对路径
                    Media.DISPLAY_NAME, // 视频文件名
                    Media.MIME_TYPE, // 媒体类型
                    Media.DURATION, // 视频长度
                    Media.DATE_ADDED, // 视频添加时间
                    Media.DATE_MODIFIED, // 视频最后更改时间
                ).toMutableList()
                if (SDK_INT >= Build.VERSION_CODES.R) queryArray.add(Media.BITRATE)// 视频码率
                cursor = contentResolver.query(
                    Media.EXTERNAL_CONTENT_URI, queryArray.toTypedArray(),
                    null, null, Media.DATE_ADDED + " DESC", null
                )
                cursor?.moveToFirst()
                if (cursor == null || cursor.isAfterLast) return@withContext videoList
                while (!cursor.isAfterLast) {
                    getVideoInfo(cursor).run { if (duration > 0 && size > 0) videoList.add(this) }
                    cursor.moveToNext()
                }
            } catch (e: Exception) {
            } finally {
                cursor?.close()
            }
            videoList
        }

注:getVideoInfo获取视频文件信息可参考附件1。

2 怎么拿视频缩略图?

2.1 MediaMetadataRetriever

        可以通过视频系统路径,直接使用getFrameAtTime方法拿到第一帧作为缩略图。

val retriever = MediaMetadataRetriever()
retriever.setDataSource("filePath")
val bitmap: Bitmap? = retriever.frameAtTime

        也可以使用getScaledFrameAtTime,指定缩略图Bitmap尺寸512*512。

retriever.getScaledFrameAtTime(-1, OPTION_CLOSEST_SYNC,512,512)

2.2 ThumbnailUtils

  • 第二个参数kind可取如下值:MICRO_KIND(3)不清晰;FULL_SCREEN_KIND(2)清晰;MINI_KIND(1)较清晰
  • MICRO_KIND:96*96的缩略图
  • MINI_KIND:512*384的缩略图
  • FULL_SCREEN_KIND:完整大小的图片
ThumbnailUtils.createVideoThumbnail("filePath",MediaStore.Video.Thumbnails.FULL_SCREEN_KIND)

        ThumbnailUtils.createVideoThumbnail其实也是使用的MediaMetadataRetriever,如下源码截图:

2.3 Glide

Glide.with(mContext)
    .load(Uri.fromFile(File("filePath")))
    .into(binding.icon)

        项目中一般会使用图片加载框架如Glide,它内部也是支持加载视频作为图片的,亦是使用的MediaMetadataRetriever。 

2.4 Android媒体库

        直接从Android媒体库中查询缩略图,但是调试时发现未找到(视频为手机本地录制mp4格式) ,下面是查询视频缩略图的方法:

    /**
     * 获取视频缩略图:从媒体库中查询
     */
    @Deprecated("暂时不可用", replaceWith = ReplaceWith("getVideoThumbnail"))
    suspend fun getVideoThumbnailDefault(
        contentResolver: ContentResolver,
        cursor: Cursor
    ): Bitmap? = withContext(Dispatchers.IO) {
//        // 方式一:能拿到路径"content://media/external/images/media/126",但没有具体图片
//        BitmapFactory.decodeFile(
//            ContentUris.withAppendedId(
//                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
//                cursor.getLong(cursor.getColumnIndex(Media._ID))
//            ).toString()
//        )
//
//        // 方式二:拿到的是video视频路径"/storage/emulated/0/DCIM/Camera/20221122_200648.mp4"
//        BitmapFactory.decodeFile(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)))

        // 方式三:开发调试时发现,thumbCursor.moveToFirst()为false,也就是说cursor为空,视频缩略图路径未找到
        contentResolver.query(
            Thumbnails.EXTERNAL_CONTENT_URI,
            arrayOf(Thumbnails.DATA, Thumbnails.VIDEO_ID),
            Thumbnails.VIDEO_ID + "=" + cursor.getInt(cursor.getColumnIndex(Media._ID)),
            null,
            null
        )?.let { thumbCursor ->
            if (thumbCursor.moveToFirst()) {
                // 获取视频缩略图路径,并转为bitmap
                // MediaStore.Video.Thumbnails.DATA: 视频缩略图的文件路径
                BitmapFactory.decodeFile(thumbCursor.getString(thumbCursor.getColumnIndex(Thumbnails.DATA)))
            } else null
        }
    }

2.5 Android 10兼容

         Android 10对文件访问更加严格,不能直接操作视频绝对路径,如要操作则须加上在AndroidManifest.xml的application标签下声明requestLegacyExternalStorage=true,才可以访问非沙盒路径下的数据。

        但Google Play发布的应用,不允许带有requestLegacyExternalStorage,所以对于Android 10不能采用操作视频的绝对路径,改为操作视频的Uri即可。

       1) 针对Android 10可以采用更高版本API获取视频第一帧,如下:

    /**
     * 获取视频缩略图:兼容Android 10
     */
    suspend fun getVideoThumbnail(context: Context, uriString: String): Bitmap? =
        withContext(Dispatchers.IO) {
            var bitmap: Bitmap? = null
            if (SDK_INT >= Build.VERSION_CODES.Q) {
                try {
                    bitmap = context.contentResolver.loadThumbnail(
                        Uri.parse(uriString), Size(
                            THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt(),
                            THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt()
                        ), null
                    )
                } catch (e: Exception) {
                }
            } else getVideoThumbnail(uriString)
            bitmap
        }

        2)针对Android 10,也可以使用视频Uri去获取第一帧,如下:

    suspend fun getVideoThumbnail(context: Context, uriString: String): Bitmap? =
        withContext(Dispatchers.IO) {
            var bitmap: Bitmap? = null
            val retriever = MediaMetadataRetriever()
            try {
                retriever.setDataSource(context, Uri.parse(uriString))
                // OPTION_CLOSEST_SYNC:在给定的时间,检索最近一个同步与数据源相关联的的帧(关键帧)
                // OPTION_CLOSEST:表示获取离该时间戳最近帧(I帧或P帧)
                bitmap = if (SDK_INT >= Build.VERSION_CODES.O_MR1) {
                    retriever.getScaledFrameAtTime(
                        VIDEO_FIRST_FRAME_TIME_US, OPTION_CLOSEST_SYNC,
                        THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt(),
                        THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt()
                    )
                } else {
                    retriever.getFrameAtTime(VIDEO_FIRST_FRAME_TIME_US)
                        ?.let { compressVideoThumbnail(it) }
                }
            } catch (e: Exception) {
            } finally {
                try {
                    retriever.release()
                } catch (e: Exception) {
                }
            }
            bitmap
        }

        3)但,Android 10,不能使用视频绝对路径去获取第一帧,会存在权限不足问题,如下:

    /**
     * 获取视频缩略图:从路径中拿取第一帧,Android 10不可用
     */
    suspend fun getVideoThumbnail(filePath: String): Bitmap? = withContext(Dispatchers.IO) {
        var bitmap: Bitmap? = null
        val retriever = MediaMetadataRetriever()
        try {
            retriever.setDataSource(filePath)
            // OPTION_CLOSEST_SYNC:在给定的时间,检索最近一个同步与数据源相关联的的帧(关键帧)
            // OPTION_CLOSEST:表示获取离该时间戳最近帧(I帧或P帧)
            bitmap = if (SDK_INT >= Build.VERSION_CODES.O_MR1) {
                retriever.getScaledFrameAtTime(
                    VIDEO_FIRST_FRAME_TIME_US, OPTION_CLOSEST_SYNC,
                    THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt(),
                    THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt()
                )
            } else {
                retriever.getFrameAtTime(VIDEO_FIRST_FRAME_TIME_US)
                    ?.let { compressVideoThumbnail(it) }
            }
        } catch (e: Exception) {
        } finally {
            try {
                retriever.release()
            } catch (e: Exception) {
            }
        }
        bitmap
    }

         综上,使用第二种方式是一种比较统一的方式,不存在版本兼容问题。

参考:

https://blog.youkuaiyun.com/g984160547/article/details/115749804

android 10(Q)上传图片及视频,访问媒体文件适配_李小白的生活的博客-优快云博客_android 10 上传文件

2.6 小结

  • Android媒体库可以获取缩略图但不稳定;

  • 第三方图片库如Glide以及ThumbnailUtils都是采用MediaMetadataRetriever。

          所以根本上来说,目前有两种方式拿到视频缩略图,Android媒体库或MediaMetadataRetriever,一般来说采用MediaMetadataRetriever方式,参考代码如下:

    const val VIDEO_FIRST_FRAME_TIME_US = 1000L
    /**
     * 视频缩略图默认压缩尺寸
     */
    const val THUMBNAIL_DEFAULT_COMPRESS_VALUE = 1024f

        /**
     * 获取视频缩略图:通过绝对路径抓取第一帧
     * @param filePath 视频在系统绝对路径。如:/storage/emulated/0/Pictures/Screenshots/SVID_20221109_181203_1.mp4
     */
    suspend fun getVideoThumbnail(filePath: String): Bitmap? = withContext(Dispatchers.IO) {
        var bitmap: Bitmap? = null
        val retriever = MediaMetadataRetriever()
        try {
            retriever.setDataSource(filePath)
            // OPTION_CLOSEST_SYNC:在给定的时间,检索最近一个同步与数据源相关联的的帧(关键帧)
            // OPTION_CLOSEST:表示获取离该时间戳最近帧(I帧或P帧)
            bitmap = if (SDK_INT >= Build.VERSION_CODES.O_MR1) {
                retriever.getScaledFrameAtTime(
                    VIDEO_FIRST_FRAME_TIME_US, OPTION_CLOSEST_SYNC,
                    THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt(),
                    THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt()
                )
            } else {
                retriever.getFrameAtTime(VIDEO_FIRST_FRAME_TIME_US)
                    ?.let { compressVideoThumbnail(it) }
            }
        } catch (e: Exception) {
        } finally {
            try {
                retriever.release()
            } catch (e: Exception) {
            }
        }
        bitmap
    }

    /**
     * 获取视频缩略图:通过Uri抓取第一帧
     * @param videoUriString 视频在媒体库中Uri。如:content://media/external/video/media/11378
     */
    suspend fun getVideoThumbnail(context: Context, videoUriString: String): Bitmap? =
        withContext(Dispatchers.IO) {
            var bitmap: Bitmap? = null
            val retriever = MediaMetadataRetriever()
            try {
                retriever.setDataSource(context, Uri.parse(videoUriString))
                // OPTION_CLOSEST_SYNC:在给定的时间,检索最近一个同步与数据源相关联的的帧(关键帧)
                // OPTION_CLOSEST:表示获取离该时间戳最近帧(I帧或P帧)
                bitmap = if (SDK_INT >= Build.VERSION_CODES.O_MR1) {
                    retriever.getScaledFrameAtTime(
                        VIDEO_FIRST_FRAME_TIME_US, OPTION_CLOSEST_SYNC,
                        THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt(),
                        THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt()
                    )
                } else {
                    retriever.getFrameAtTime(VIDEO_FIRST_FRAME_TIME_US)
                        ?.let { compressVideoThumbnail(it) }
                }
            } catch (e: Exception) {
            } finally {
                try {
                    retriever.release()
                } catch (e: Exception) {
                }
            }
            bitmap
        }

        为避免应用OOM,需要对缩略图大小进行压缩compressVideoThumbnail,下面看看Bitmap压缩方式。

3 缩略图如何压缩?        

        宽高压缩、缩放法压缩可针对Bitmap操作,而采样率压缩和质量压缩针对于File、Resource操作,下面可主要看看宽高压缩、缩放法压缩。

3.1 宽高压缩

    /**
     * 视频缩略图默认压缩尺寸
     */
    const val THUMBNAIL_DEFAULT_COMPRESS_VALUE = 512f

    /**
     * 压缩视频缩略图
     * @param bitmap 视频缩略图
     */
    fun compressVideoThumbnail(bitmap: Bitmap): Bitmap? {
        val width: Int = bitmap.width
        val height: Int = bitmap.height
        val max: Int = Math.max(width, height)
        if (max > THUMBNAIL_DEFAULT_COMPRESS_VALUE) {
            val scale: Float = THUMBNAIL_DEFAULT_COMPRESS_VALUE / max
            val w = (scale * width).roundToInt()
            val h = (scale * height).roundToInt()
            return compressVideoThumbnail(bitmap, w, h)
        }
        return bitmap
    }

    /**
     * 压缩视频缩略图:宽高压缩
     * 注:如果用户期望的长度和宽度和原图长度宽度相差太多的话,图片会很不清晰。
     * @param bitmap 视频缩略图
     */
    fun compressVideoThumbnail(bitmap: Bitmap, width: Int, height: Int): Bitmap? {
        return Bitmap.createScaledBitmap(bitmap, width, height, true)
    }

3.2 缩放法压缩

    /**
     * 视频缩略图默认压缩比例
     */
    private const val THUMBNAIL_DEFAULT_SCALE_VALUE = 0.5f

    /**
     * 压缩视频缩略图:缩放法压缩
     * 注:长度和宽度没有变,内存缩小4倍(宽高各缩小一半)
     */
    fun compressVideoThumbnailMatrix(bitmap: Bitmap): Bitmap? {
        val matrix = Matrix()
        matrix.setScale(THUMBNAIL_DEFAULT_SCALE_VALUE, THUMBNAIL_DEFAULT_SCALE_VALUE)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
    }

        其实,宽高压缩调用createScaledBitmap方法,最终也是走的缩放法压缩Matrix.setScale,如下图源码,通过宽高计算出scale值,进而进行压缩:

3.3 采样率压缩

    /**
     * 压缩视频缩略图:采样率压缩
     * @param filePath 视频缩略图路径
     */
    fun compressVideoThumbnailSample(filePath: String, width: Int, height: Int): Bitmap? {
        return BitmapFactory.Options().run {
            // inJustDecodeBounds 设置为 true后,BitmapFactory.decodeFile 不生成Bitmap对象,而仅仅是读取该图片的尺寸和类型信息。
            inJustDecodeBounds = true
            BitmapFactory.decodeFile(filePath, this)
            inSampleSize = calculateInSampleSize(this, width, height)
            inJustDecodeBounds = false
            BitmapFactory.decodeFile(filePath, this)
        }
    }

        计算采样率,可有以下2种方式: 

    /**
     * 计算采样率:值为2的幂。例如, 一个分辨率为2048x1536的图片,如果设置 inSampleSize 为4,那么会产出一个大约512x384大小的Bitmap。
     * @param options
     * @param reqWidth 想要压缩到的宽度
     * @param reqHeight  想要压缩到的高度
     * @return
     */
    fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
        var inSampleSize = 1
        if (options.outHeight > reqHeight || options.outWidth > reqWidth) {
            val halfHeight: Int = options.outHeight / 2
            val halfWidth: Int = options.outWidth / 2
            while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                inSampleSize *= 2
            }
        }
        return inSampleSize
    }

    /**
     * 计算采样率:值为整数的采样率。
     */
    fun calculateInSampleSizeFixed(options: BitmapFactory.Options, width: Int, height: Int): Int {
        val hRatio = ceil(options.outHeight.div(height.toDouble())) // 大于1:图片高度>手机屏幕高度
        val wRatio = ceil(options.outWidth.div(width.toDouble())) // 大于1:图片宽度>手机屏幕宽度
        return if (hRatio > wRatio) hRatio.toInt() else wRatio.toInt()
    }

        其实,这里的采样率压缩称之为采用邻近点插值算法的邻近采样,即:如果采样率设置为2,意味着直接选择其中的一个像素作为生成像素,另一个像素直接抛弃。

        另外,宽高压缩或缩放法压缩称为双线性采样,且:它不像邻近点插值算法一样,直接粗暴的选择一个像素,而是参考了源像素相应位置周围 2x2 个点的值,根据相对位置取对应的权重,经过计算之后得到目标图像。

        双线性采样对比邻近采样的优势在于:

  • 它的系数可以是小数,而不一定是整数,在某些压缩限制下,效果尤为明显;
  • 处理文字比较多的图片在展示效果上的差别,双线性采样效果要更好。

3.4 质量压缩

        因为,质量压缩会让图片在磁盘中的大小(File文件的大小)减小,但是图片的像素数不会改变,加载压缩后的图片,占据的内存不会减少,所以我们这里内存中视频缩略图的压缩,就先不展开讨论此种方式。具体实现可参考此文 。

注:

  • 质量压缩依靠Bitmap.compress(CompressFormat format, int quality, OutputStream stream)实现;
  • 质量压缩可以减少文件大小,典型使用场景为:图片的上传。

3.5 小结  

        图片压缩方法根据实际情况去选择即可,比如微信图片压缩算法,大致就是按照以下步骤进行的:

  • 第一步进行采样率压缩;
  • 第二步进行宽高压缩(微信对原图和缩略图限制了最大长宽或者最小长宽);
  • 第三步就是对图片的质量进行压缩(一般75或者70);
  • 第四部就是采用webP的格式。

4 附件

【附1:获取视频文件信息】

    /**
     * 获取视频文件信息
     * 注:(1)暂未包括videoRotation;(2)biteRate通过文件大小和视频时长计算
     */
    suspend fun getVideoInfo(cursor: Cursor): VideoInfo = withContext(Dispatchers.IO) {
        val videoInfo = VideoInfo()
        videoInfo.mediaId = cursor.getString(cursor.getColumnIndex(Media._ID))
        videoInfo.size = cursor.getLong(cursor.getColumnIndex(Media.SIZE))
        videoInfo.width = cursor.getFloat(cursor.getColumnIndex(Media.WIDTH))
        videoInfo.height = cursor.getFloat(cursor.getColumnIndex(Media.HEIGHT))
        videoInfo.localPath = cursor.getString(cursor.getColumnIndex(Media.DATA))
        videoInfo.localPathUri = getVideoPathUri(videoInfo.mediaId).toString()
        videoInfo.fileName = cursor.getString(cursor.getColumnIndex(Media.DISPLAY_NAME))
        videoInfo.mimeType = cursor.getString(cursor.getColumnIndex(Media.MIME_TYPE))
//        不能在这获取第一帧,太耗时,改为使用的地方去获取第一帧。
//        videoInfo.firstFrame = getVideoThumbnail(cursor.getString(cursor.getColumnIndex(Media.DATA)))
        videoInfo.duration = cursor.getLong(cursor.getColumnIndex(Media.DURATION))
        videoInfo.biteRate =
            if (SDK_INT >= Build.VERSION_CODES.R) cursor.getLong(cursor.getColumnIndex(Media.BITRATE))
            else ((8 * videoInfo.size * 1024) / (videoInfo.duration / 1000f)).toLong()
        videoInfo.addTime = cursor.getLong(cursor.getColumnIndex(Media.DATE_ADDED))
        videoInfo.lastModified = cursor.getLong(cursor.getColumnIndex(Media.DATE_MODIFIED))
        videoInfo
    }

【附2:通过视频路径直接获取文件信息】

    /**
     * 获取视频文件信息
     * 注:(1)暂未包括lastModified、addTime;(2)mediaId以filePath代替
     *
     * @param path 视频文件的路径
     * @return VideoInfo 视频文件信息
     */
    suspend fun getVideoInfo(path: String?): VideoInfo = withContext(Dispatchers.IO) {
        val videoInfo = VideoInfo()
        if (!path.isNullOrEmpty()) {
            val media = MediaMetadataRetriever()
            try {
                media.setDataSource(path)
                videoInfo.size =
                    File(path).let { if (FileUtils.isFileExists(it)) it.length() else 0 }
                videoInfo.width = media.extractMetadata(METADATA_KEY_VIDEO_WIDTH)?.toFloat() ?: 0f
                videoInfo.height = media.extractMetadata(METADATA_KEY_VIDEO_HEIGHT)?.toFloat() ?: 0f
                videoInfo.localPath = path
                videoInfo.localPathUri =
                    getVideoPathUri(ApplicationUtils.getApplication(), path).toString()
                videoInfo.fileName = path.split(File.separator).let {
                    if (it.isNotEmpty()) it[it.size - 1] else ""
                }
                videoInfo.mimeType = media.extractMetadata(METADATA_KEY_MIMETYPE) ?: ""
                videoInfo.firstFrame =
                    media.getFrameAtTime(VIDEO_FIRST_FRAME_TIME_US)
                        ?.let { compressVideoThumbnail(it) }
                videoInfo.duration = media.extractMetadata(METADATA_KEY_DURATION)?.toLong() ?: 0
                videoInfo.biteRate = media.extractMetadata(METADATA_KEY_BITRATE)?.toLong() ?: 0
                videoInfo.videoRotation =
                    media.extractMetadata(METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0
                videoInfo.mediaId = path
            } catch (e: Exception) {
            } finally {
                media.release()
            }
        }
        videoInfo
    }

【附3:获取视频Uri】

    private const val URI_VIDEO_PRE = "content://media/external/video/media"

    /**
     * 通过视频资源ID,直接获取视频Uri
     * @param mediaId 视频资源ID
     */
    fun getVideoPathUri(mediaId: String): Uri =
        Uri.withAppendedPath(Uri.parse(URI_VIDEO_PRE), mediaId)

    /**
     * 通过视频资源本地路径,获取视频Uri
     * @param path 视频资源本地路径
     */
    fun getVideoPathUri(context: Context, path: String): Uri? {
        var uri: Uri? = null
        var cursor: Cursor? = null
        try {
            cursor = context.contentResolver.query(
                Media.EXTERNAL_CONTENT_URI, arrayOf(Media._ID),
                Media.DATA + "=? ", arrayOf(path), null
            )
            uri = if (cursor != null && cursor.moveToFirst()) {
                Uri.withAppendedPath(
                    Uri.parse(URI_VIDEO_PRE),
                    "" + cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID))
                )
            } else {
                if (File(path).exists()) {
                    val values = ContentValues()
                    values.put(Media.DATA, path)
                    context.contentResolver.insert(Media.EXTERNAL_CONTENT_URI, values)
                } else {
                    null
                }
            }
        } catch (e: Exception) {
        } finally {
            cursor?.close()
        }
        return uri
    }

获取视频封面图 在 Kotlin(尤其是 Android 开发)中,获取本地视频和网络视频的封面图(缩略图)通常采用 `MediaMetadataRetriever` 配合文件路径或 URI。对于网络视频,需要先下载到本地临时文件再提取。 以下是完整的实现方案,包含必要的权限说明和异步处理建议。 阅读详情

相关推荐

Android BitmapFactory实现AOSP ContentResolver.loadThumbnail快速取小缩略图,Kotlin

本文深入分析了Android中快速加载缩略图的实现原理。关键点在于:1)通过openTypedAssetFileDescriptor传入目标尺寸,让MediaProvider直接返回合适的缩略图,避免全尺寸解码;2)采用二次采样策略,先获取图片尺寸计算采样率,再按需解码;3)处理方向信息时优先使用Provider提供的元数据;4)最终仍做防御性缩放确保尺寸合规。文中提供了基于BitmapFactory的Kotlin实现方案,吸收系统优化思路的同时,也指出其与系统实现(如ImageDecoder)的性能差异,

Zhang Phil 431

Android 获取视频缩略图

1.引用第三方库 implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever:1.0.14' 2.实现 /** * 获取本地视频缩略图 * * @param filePath * @return */ public static Bitmap getVideoThumbnail(String filePath) { Log.d(TAG, "getVideoT

安卓兼职framework和app工程师的博客 851

我的Android进阶之旅------>使用ThumbnailUtils类获取视频的缩略图

今天看了一段代码,是关于获取视频的缩略图的,让我认识了一个ThumbnailUtils类,代码如下。 Bitmap bitmap = ThumbnailUtils.createVideoThumbnail(path, ThumbnailUtils.OPTIONS_RECYCLE_INPUT); //用于获取视频的缩略图 BitmapDrawable background=new BitmapD...

字节卷动 3万+

Android中如何获取视频文件的缩略图

欢迎访问我的个人博客阅读:http://androidfuture.com/blog/?p=195 在android中获取视频文件的缩略图有三种方法: 1.从媒体库中查询 2. android 2.2以后使用ThumbnailUtils类获取 3.调用jni文件,实现MediaMetadataRetriever类 三种方法各有利弊 第一种方法,新视频增加后需要S

cs870101的专栏 1万+

Android 视频播放系统开发:Media3 ExoPlayer 实战链路

视频播放器开发的核心不是播放控件,而是解码内核、数据源、UI 控制与媒体库扫描的分层协作。Android 上 Media3 ExoPlayer 提供状态机、音频焦点、生命周期与可替换 DataSource,让 content:// URI、MediaStore 查询、边播边缓存、倍速和画中画等能力具备统一扩展点。对开发者而言,理解播放状态、进度同步、权限与缩略图加载,比单纯让画面出现更影响稳定性与体验。常见场景包括本地相册视频列表、在线 HLS/DASH、短视频、后台小窗续播与 adb 日志排错。Andro

weixin_26742753的博客 225

Android中如何获取视频文件的截图、缩略图

主要介绍了Android中如何获取视频文件的截图、缩略图的相关资料,需要的朋友可以参考下

Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin

本文介绍了一个基于Coil3的Android视频封面抽取实现方案。项目通过MediaStore获取设备视频列表,使用MediaMetadataRetriever提取视频首帧作为缩略图,并采用二级缓存(内存+磁盘)优化性能。核心功能包括:1)声明读写存储权限;2)RecyclerView网格布局展示;3)协程处理耗时操作;4)自定义ImageLoader配置缓存策略(最大2GB)。相比原生方案,该实现通过Coil的缓存机制提升了缩略图加载效率,同时处理了视频损坏等异常情况。相关技术细节可参考作者优快云博客中

Zhang Phil 1137

Scipy优化踩坑实录:trust-constr和SLSQP约束定义到底差在哪?

本文深入解析了Scipy优化模块中trust-constr和SLSQP两种方法在约束定义上的差异。通过对比面向对象的trust-constr和字典式定义的SLSQP,揭示了它们各自的优势与适用场景,帮助开发者在有约束优化问题中选择合适的算法,提升计算效率与准确性。

weixin_42573113的博客 271

Android实现Glide/Coil样式图/视频加载框架,Kotlin

本文介绍了一种Android实现Glide/Coil样式图片/视频加载框架的Kotlin解决方案。该方案采用二级缓存架构:内存缓存使用LruCache,磁盘缓存基于DiskLruCache实现,最大支持1GB存储空间。通过MD5加密URL生成缓存键,支持同步/异步加载模式,并利用协程处理IO操作。框架包含完整的缓存管理、异常处理和生命周期绑定功能,特别针对视频封面提取场景进行了优化。相比原生实现,该方案在性能测试中显著提升了缩略图加载速度,内存占用减少约30%。相关技术细节可参考作者优快云博客中对FFmp

Zhang Phil 1277

遥感图像小白--2--图像分割

遥感图像分割基础入门

Mary的博客 4237

ROS 入门教程:小乌龟仿真器和基础操作

小乌龟仿真器和ros基础操作

zlflll的博客 4126

Android loadThumbnail ThumbnailUtils.createVideoThumbnail MediaMetadataRetriever time cost, Kotlin

Android设置头像,手机拍照或从本地相册选取图片作为头像_android 头像拍照_zhangphil的博客-优快云博客。【代码】Android Paging 3,kotlin(1)在实际的开发中,虽然Glide解决了快速加载图片的问题,但还有一个问题悬而未决:比如用户的头像,往往用户的头像是从服务器端读出的一个普通矩形图片,但是现在的设计一般要求在APP端的用户头像显示成圆形头像,那么此时虽然Glide可以加载,但加载出来的是一个矩形,如果要Glide_android 毛玻璃圆角。

Zhang Phil 969

​基于stm32的智能空气加湿器设计(含源代码和Proteus仿真)

课程设计分享

LoongSTUDIO的博客 2929

android显示视频预览

android 11 kotlin显示视频预览 权限 读sd卡中的mp4文件的权限 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 代码 fun showVideo() { // 比如显示sdcard对应app的data中的kongfu.mp4 val video: String = getExternalFilesDir("")!!.getAbsol

zmlovelx(帅得不敢出门 程序员群31843264) 4440

AM243x多核MCU启动流程解析与OSPI Flash烧录实战

嵌入式系统开发中,引导加载器是芯片上电后执行的第一段软件,负责初始化硬件、加载操作系统或应用程序。其工作原理通常遵循多级引导链,从ROM固件到用户可定制的二级引导程序,逐级建立完整的软件运行环境。这一机制的技术价值在于实现可靠的系统启动、支持安全校验,并为多核异构架构提供协同基础。在应用场景上,引导加载器广泛应用于工业控制、汽车电子和物联网设备,确保设备能从非易失性存储器中自主启动。本文聚焦于TI AM243x这类高性能多核MCU,深入剖析其包含RBL和SBL在内的三级启动流程,并详细演示如何通过UART接

weixin_28703451的博客 583

AI初识—LLM、Ollama、Llama都是些个啥?

LLM指的是大语言模型(Large Language Model),是一种基于深度学习的模型,能够理解和生成自然语言。它通过大量文本数据进行训练,具备了理解上下文、生成连贯文本和回答问题的能力。著名的LLM包括OpenAI的GPT系列和Google的BERT。在AI技术快速发展的背景下,LLM、Ollama和Llama代表了现代自然语言处理的不同方面。LLM为我们提供了强大的语言理解能力,Ollama为模型的部署和管理提供了便利,而Llama则在效率和多样性上不断推进。

小蜗牛的珍贵百宝箱 4630

Android快速视频解码抽帧FFmpegMediaMetadataRetriever,Kotlin(2)

本文介绍了两种Android视频封面提取方案对比:1)原生MediaMetadataRetriever速度较慢;2)第三方FFmpegMediaMetadataRetriever(FFMMR)实现快速抽帧。详细说明了FFMMR的集成方法(添加依赖和权限),并提供了完整的Kotlin实现代码,包括视频列表读取、缓存管理、协程异步处理等核心功能。通过LruCache缓存缩略图提升性能,记录处理耗时和失败情况。相比前文介绍的原生方案,本文重点突出了FFMMR在解码效率和性能上的优势,为需要快速获取视频帧的场景提供

Zhang Phil 1155

【从零开始学习 UVM】1.1、UVM 概述 —— Preface

【从零开始学习 UVM】1.1、UVM 概述 —— Preface

努力,加油每一天鸭~ 826

Android获取SD卡上视频缩略图:TaoToken统一Key通道下的MediaMetadataRetriever实战

Android 10+分区存储下读取SD卡视频缩略图,用MediaStore查询Uri替代DATA字段,配合MediaMetadataRetriever取帧即可落地。本文给出权限声明、FileProvider配置与Kotlin取帧代码,并说明如何用TaoToken统一Key通道管理大模型凭证,官网见 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 。

weixin_42601608的博客 6

T527 uart串口配置导致系统进不去

T527 s_uart串口配置导致系统进不去。

Linux驱动开发工程师,熟悉Linux/Android底层驱动、TP、显示、IIC、网络等通用外设调试适配,Linux上层应用开发。常用平台瑞芯微、全志、恩智浦等。 676

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin

遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。2、现在分别使用缩略图内存缓存和正图内存缓存,感觉应该可以合并,只使用一套内存缓存。

Zhang Phil 870

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

JZVideo:高度自定义的安卓视频框架

JZVideo 是一个高度自定义的安卓视频框架,主要使用 Java 和 Kotlin 编程语言开发。该项目旨在为安卓开发者提供一个灵活且功能强大的视频播放解决方案,支持多种视频播放器内核,如 MediaPlayer、ExoPlayer、IJKPlayer 和 FFmpeg。 #### 2、项目的核心功能 JZVideo 的核心功能包括: - **高度自定义**:开发者可以根据需求自定义视频播...

gitblog_01279的博客 545

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)

本文介绍了基于Coil3的Android视频封面抽取优化方案,重点改进了磁盘缓存性能。通过将封面帧从PNG无损格式改为JPEG有损压缩(质量80),使缓存文件大小缩减至原先的1/10,显著提升了读写速度。核心实现包括:1)使用FFmpegMediaMetadataRetriever快速抽取视频首帧;2)自定义Bitmap压缩参数(Bitmap.CompressFormat.JPEG);3)采用缓冲流(BufferedOutputStream)处理磁盘缓存写入。该方案在保持图像质量的同时优化了I/O性能,适用

Zhang Phil 1332

kotlin 隐藏 组件_基于Kotlin的Android复合组件简介-第2部分

kotlin 隐藏 组件 系列路线图 (Series Roadmap) Part 1 第1部分 Part 2 (you’re here) 第2部分(您在这里) Part 3 第三部分 Hello there, and welcome back. This is the second part in a three-part series tutorial on using Compound Com...

weixin_26739079的博客 614

Android Coil 3 data加载图的Bitmap或ByteArray数据类型,Kotlin

摘要:三篇技术文章主要探讨了Android开发中Coil3库的图像处理功能。第一篇展示了如何使用Coil3加载Bitmap和ByteArray数据到ImageView中;第二篇介绍了Bitmap与byte[]数组之间的转换方法;第三篇讨论了用Coil3批量拼接Bitmap生成宽图的实现,同时指出磁盘缓存未生效的问题。这些文章提供了Kotlin代码示例,涉及图像加载、转换和拼接等实用技术。

Zhang Phil 1253

实战准备:为短视频安卓应用开发,用快马ai定制专属的android studio环境配置方案

因此,我们的环境配置需要围绕几个关键点展开:强大的视频播放库、高效的图片加载与缓存、性能监控工具,以及一个能模拟真实设备进行编解码测试的虚拟环境。基于此,我向快马平台描述了需求:为一个基于Kotlin的短视频应用项目,生成Android Studio的专项配置方案,重点包括必要的插件、Gradle依赖、AVD配置和性能分析工具集成。它的好处是,你不需要自己从头去拼凑这些琐碎但又关键的配置信息,只需用自然语言描述你的项目类型和技术倾向,AI就能整合出一份可执行的指南。整个方案获取和落地的过程,我是在。

YellowSun24的博客 174
上一篇: Android-DialogX使用与分享
下一篇: 我的MEM管理类研究生,在路上
Swuagg
Swuagg 新星创作者: 移动开发技术领域 新星创作者: 移动开发技术领域 领域专家: 操作系统技术领域 领域专家: 操作系统技术领域
博客等级 码龄10年 2355粉丝 153原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值