Video的thumbnail抽取算法

本文介绍了如何生成视频缩略图,重点关注了在`android/media/ThumbnailUtils.java`中的函数实现,以及在`MPEG4Extractor.cpp`和`SampleTable.cpp`中关于选取关键帧的算法。当存在movie fragments时,会选择1/4duration处的帧,否则选择前20个样本中最大值的样本作为缩略图。

How to generate video thumbnail:


注意生成thumbnail时,传入getFrameAtTime的时间是-1

frameworks/base/media/java/android/media/ThumbnailUtils.java

    158     public static Bitmap createVideoThumbnail(String filePath, int kind) {
    159         Bitmap bitmap = null;
    160         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    161         try {
    162             retriever.setDataSource(filePath);
    163             bitmap = retriever.getFrameAtTime(-1);
    164         } catch (IllegalArgumentException ex) {
    165             // Assume this is a corrupt video file
    166         } catch (RuntimeException ex) {
    167             // Assume this is a corrupt video file.
    168         } finally {
    169             try {
    170                 retriever.release();
    171             } catch (RuntimeException ex) {
    172                 // Ignore failures while cleaning up.
    173             }
    174         }

frameworks / base / media / java / android / media / MediaMetadataRetriever.java

    270     public Bitmap getFrameAtTime(long timeUs) {
    271         return getFrameAtTime(timeUs, OPTION_CLOSEST_SYNC);

sp<IMemory> MediaMetadataRetriever::getFrameAtTime(int64_t timeUs, int option)
{
    ALOGV("getFrameAtTime: time(%lld us) option(%d)", timeUs, option);
    Mutex::Autolock _l(mLock);
    if (mRetriever == 0) {
        ALOGE("retriever is not initialized");
        return NULL;
    }
    return mRetriever->getFrameAtTime(timeUs, option);
}


sp<IMemory> MetadataRetrieverClient::getFrameAtTime(int64_t timeUs, int option)
{
    ALOGV("getFrameAtTime: time(%lld us) option(%d)", timeUs, option);
    Mutex::Autolock lock(mLock);
    mThumbnail.clear();
    if (mRetriever == NULL) {
        ALOGE("retriever is not initialized");
        return NULL;
    }
    VideoFrame *frame = mRetriever->getFrameAtTime(timeUs, option);

media/libstagefright/StagefrightMetadataRetriever.cpp
VideoFrame *StagefrightMetadataRetriever::getFrameAtTime(
        int64_t timeUs, int option) {...
    if (mExtractor.get() == NULL) {
        ALOGV("no extractor.");
        return NULL;
    }

    sp<MetaData> fileMeta = mExtractor->getMetaData();
...
    size_t n = mExtractor->countTracks();
    size_t i;
    for (i = 0; i < n; ++i) {
        sp<MetaData> meta = mExtractor->getTrackMetaData(i);  // 这里将赋值给kKeyThumbnailTime

        const char *mime;
        CHECK(meta->findCString(kKeyMIMEType, &mime));

        if (!strncasecmp(mime, "video/", 6)) {
            break;
        }
    }
...
    if (fileMeta->findData(kKeyAlbumArt, &type, &data, &dataSize)
            && mAlbumArt == NULL) {
        mAlbumArt = new MediaAlbumArt;
        mAlbumArt->mSize = dataSize;
        mAlbumArt->mData = new uint8_t[dataSize];
        memcpy(mAlbumArt->mData, data, dataSize);
    }

    VideoFrame *frame =
        extractVideoFrameWithCodecFlags(                    //抽取thumbnail
                &mClient, trackMeta, source, OMXCodec::kSoftwareCodecsOnly,
                timeUs, option);
    ...}

下面函数具体抽取thumbnail:

    static VideoFrame *extractVideoFrameWithCodecFlags(

        OMXClient *client,
        const sp<MetaData> &trackMeta,
        const sp<MediaSource> &source,
        uint32_t flags,
        int64_t frameTimeUs,
        int seekMode) {...
    int64_t thumbNailTime;
    if (frameTimeUs < 0) { //如果传入的时间为负数
        if (!trackMeta->findInt64(kKeyThumbnailTime, &thumbNailTime)
                || thumbNailTime < 0) { //查看kKeyThumbnailTime是否存在
            thumbNailTime = 0; //如不存在取第0帧
        }
        options.setSeekTo(thumbNailTime, mode);
    } else {
        thumbNailTime = -1;
        options.setSeekTo(frameTimeUs, mode);
    }
    MediaBuffer *buffer = NULL;
    do {
        if (buffer != NULL) {
            buffer->release();
            buffer = NULL;
        }
        err = decoder->read(&buffer, &options);
        options.clearSeekTo();
    } while (err == INFO_FORMAT_CHANGED
             || (buffer != NULL && buffer->range_length() == 0));


之前在MPEG4Extractor中读取TrackMetadata时,已经对取哪一帧作为thumbnail进行了计算,并存入到kKeyThumbnailTime中。具体代码见下面的函数getTrackMetaData

注意仅当存在movie fragments ('moof' box)时,将把1/4duration处的帧作为thumbnail,否则将取前20个sample内具有最大值的sample作为thumbnail

media/libstagefright/MPEG4Extractor.cpp

sp<MetaData> MPEG4Extractor::getTrackMetaData(
        size_t index, uint32_t flags) {...
    if ((flags & kIncludeExtensiveMetaData)
            && !track->includes_expensive_metadata) {
        track->includes_expensive_metadata = true;    //置位

        const char *mime;
        CHECK(track->meta->findCString(kKeyMIMEType, &mime));
        if (!strncasecmp("video/", mime, 6)) {   //如果是video
            if (mMoofOffset > 0) { // if has moof box ,why ??
                int64_t duration;
                if (track->meta->findInt64(kKeyDuration, &duration)) {
                    // nothing fancy, just pick a frame near 1/4th of the duration
                    track->meta->setInt64(
                            kKeyThumbnailTime, duration / 4);
                }
            } else {
                uint32_t sampleIndex;
                uint32_t sampleTime;
                if (track->sampleTable->findThumbnailSample(&sampleIndex) == OK
                        && track->sampleTable->getMetaDataForSample(
                            sampleIndex, NULL /* offset */, NULL /* size */,
                            &sampleTime) == OK) {
                    track->meta->setInt64( //将选定sample处的时间作为thumbnail对应时间
                            kKeyThumbnailTime,
                            ((int64_t)sampleTime * 1000000) / track->timescale);
                }
            }
        }


具体sample的挑选算法:


media/libstagefright/SampleTable.cpp

status_t SampleTable::findThumbnailSample(uint32_t *sample_index) {
    Mutex::Autolock autoLock(mLock);
    uint32_t bestSampleIndex = 0;
    size_t maxSampleSize = 0;

    static const size_t kMaxNumSyncSamplesToScan = 20;  //取前20个sample

    // Consider the first kMaxNumSyncSamplesToScan sync samples and
    // pick the one with the largest (compressed) size as the thumbnail.

    size_t numSamplesToScan = mNumSyncSamples;
    if (numSamplesToScan > kMaxNumSyncSamplesToScan) {
        numSamplesToScan = kMaxNumSyncSamplesToScan;
    }
    for (size_t i = 0; i < numSamplesToScan; ++i) {
        uint32_t x = mSyncSamples[i];

        // Now x is a sample index.
        size_t sampleSize;
        status_t err = getSampleSize_l(x, &sampleSize);
        if (err != OK) {
            return err;
        }

        if (i == 0 || sampleSize > maxSampleSize) {
            bestSampleIndex = x;
            maxSampleSize = sampleSize;
        }
    }
    *sample_index = bestSampleIndex;

    return OK;
}



### 使用 BetterPlayer 在 Flutter 或 Android 中生成视频缩略图 BetterPlayer 是一款用于播放视频的强大插件,支持多种功能,包括自定义 UI 和控制选项。然而,在其官方文档中并未直接提及如何通过 BetterPlayer 自动生成视频缩略图的功能[^1]。 #### 方法一:利用 BetterPlayer 的 `placeholder` 属性 在 BetterPlayer 中可以设置一个占位符作为视频加载前的显示内容。可以通过提前生成一张静态图片并将其设为 placeholder 来实现类似的效果。以下是具体方法: ```dart import 'package:better_player/better_player.dart'; class VideoThumbnailExample extends StatefulWidget { @override _VideoThumbnailExampleState createState() => _VideoThumbnailExampleState(); } class _VideoThumbnailExampleState extends State<VideoThumbnailExample> { late final BetterPlayerController _betterPlayerController; @override void initState() { super.initState(); /// 创建 BetterPlayer 配置对象 BetterPlayerConfiguration betterPlayerConfiguration = const BetterPlayerConfiguration( aspectRatio: 16 / 9, placeholder: Center(child: Text('Loading...')), // 可替换为实际缩略图路径 ); /// 初始化控制器 _betterPlayerController = BetterPlayerController(betterPlayerConfiguration); _betterPlayerController.setupDataSource(BetterPlayerDataSource( BetterPlayerDataSourceType.network, "https://example.com/video.mp4", )); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Video Thumbnail Example")), body: AspectRatio( aspectRatio: 16 / 9, child: BetterPlayer(controller: _betterPlayerController), ), ); } } ``` 上述代码展示了如何使用 `placeholder` 参数来展示预览图像。如果需要动态生成缩略图,则需借助其他库完成此操作[^2]。 #### 方法二:集成 FFmpeg 提取帧 对于更复杂的场景(如提取特定时间点的画面),可考虑引入第三方工具如 FFmpeg 抽取指定时刻的关键帧,并保存为本地文件形式供应用调用。下面是一个简单的命令行例子说明如何从 MP4 文件获取单张 JPEG 图片: ```bash ffmpeg -i input_video.mp4 -ss 00:00:10 -vframes 1 output_thumb.jpg ``` 该指令会在第 10 秒的位置截取一幅画面存入名为 `output_thumb.jpg` 的新文件里[^3]。 随后可以在 Dart 应用程序内部封装此类逻辑或者预先处理好素材再分发给客户端设备上运行的应用读取这些资源。 --- ### 注意事项 - 如果项目需求频繁涉及实时抓拍当前播放进度对应的画幅作为封面等功能的话,可能还需要深入研究底层渲染机制以及硬件加速方面的知识才能达成目标效果。 - 对于移动端性能敏感型应用场景而言,务必权衡计算成本与用户体验之间的平衡关系。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值