Android 视频中抓取图像 - MediaMetadataRetriever

最近在side project中遇到了需要从视频中抓取多张图片的需求。安卓已经提供了从视频获取预览图片的ThumbnailUtils, 但此类不能根据timestamp获取bitmap。

以下记录自己找出的解决方案。

需求

在本地视频根据时间戳(timestamp)抓取bitmap图像。

解决方案

配合使用这些flag可以达到不同的程度的时间精确度

  • OPTION_PREVIOUS_SYNC: 前一个i-frame
  • OPTION_NEXT_SYNC: 后一个 i-frame
  • OPTION_CLOSEST_SYNC: 最近的i-frame,不管前后
  • OPTION_CLOSEST: 最近的frame,不一定是i-frame。上面三个flag只需要decode一张frame即可(而且是i-frame)。但这个flag需要decode多张frame才能接近输入的timestamp,因此速度会慢些。

值得一提的是此类会用binder IBC给system media service发送请求, 因此decoder是在系统服务中进行的,并非在我自己的app进程中。
方法调用时logcat可看到如下log

  918 13498 I OMXMaster: makeComponentInstance(OMX.google.h264.decoder) in omx@1.0-service process
  918 28062 I OMXMaster: makeComponentInstance(OMX.google.h264.decoder) in omx@1.0-service process
  918  1854 I OMXMaster: makeComponentInstance(OMX.google.h264.decoder) in omx@1.0-service process
  918 13498 I OMXMaster: makeComponentInstance(OMX.google.h264.decoder) in omx@1.0-service process

具体实现可参考:http://androidxref.com/9.0.0_r3/xref/frameworks/av/media/libmedia/mediametadataretriever.cpp#154

需要注意的坑

如果只看文档就会以为用getFrameAtTime + OPTION_CLOSEST就能非常精准的返回在输入timestamp附近的视频帧,但在不同device上跑过代码才发现其行为其实很不统一。:(

高端机基本都能做到预期效果,返回的bitmap在输入timestamp附近。但低端机型直接无视OPTION_CLOSEST 返回附近I-frame的bitmap,因此有时得到的图像跟输入的timestamp相差甚远。例如输入的是3.3sec,返回的有可能是2.8sec的i-frame图像。

出于好奇,我在低端机上做了个小实验。扫描一个视频文件的video timestamp,再把他们逐一用来调用getFrameAtTime,看看返回的bitmap有没有重复。

很明显的看到高端机基本无重复的bitmap,低端机却有大把大把的重复。猜测应该是厂商为了性能而只做了附近i-frame的解码就返回了。有点坑。:(

如果需要在所有机器上返回精准timestamp的bitmap,恐怕只能自己操作decoder了。估计非常复杂。

此外 API28+ 新增加了MediaMetadataRetriever.getFramesAtIndex() 。 这应该是比pts更加稳定的抓取方式,毕竟index是连续的int。但不知低端机型会不会继续坑。


==高端
D/MediaMetadataRetrieverRunner: pts 33375    digest :ym71cu9iO1H94190FWVgeg==
D/MediaMetadataRetrieverRunner: pts 66625    digest :y9hG93MqABw3ILMta/noUg==
D/MediaMetadataRetrieverRunner: pts 100000   digest :0l2CkOYDNyxvZCfCeLuq9A==
D/MediaMetadataRetrieverRunner: pts 133375   digest :OHSVQQOxrO77mh9f7tTorQ==
D/MediaMetadataRetrieverRunner: pts 166625   digest :XZ8kDFxsdjgBkUzKl41dgA==
D/MediaMetadataRetrieverRunner: pts 200000   digest :0/13IPmw1gP9FLdpsJc/Wg==
D/MediaMetadataRetrieverRunner: pts 233375   digest :TeHChk9OWbb8nLbrFphlEg==
D/MediaMetadataRetrieverRunner: pts 266625   digest :GEzE3LtFzG88bT7WtMTbxA==
D/MediaMetadataRetrieverRunner: pts 300000   digest :QjdeoZYwYgNxV144kKLTNw==
D/MediaMetadataRetrieverRunner: pts 333375   digest :0DZCQer3BqMPfSYTJaafMg==
D/MediaMetadataRetrieverRunner: pts 366625   digest :mfjEap/awknngWIeyPAScg==
D/MediaMetadataRetrieverRunner: pts 400000   digest :Q1oSKau7n9boi0vDSFZJFA==
D/MediaMetadataRetrieverRunner: pts 433375   digest :kW0vjMra+boztvH+PXQrlw==
D/MediaMetadataRetrieverRunner: pts 466625   digest :a6C/BpHAcr4BxSDg7Gt4bQ==
D/MediaMetadataRetrieverRunner: pts 500000   digest :odAEN5ESKWrLmXwDu09arA==
D/MediaMetadataRetrieverRunner: pts 533375   digest :+zTTQsil/s6on0EVqFuH/Q==
D/MediaMetadataRetrieverRunner: pts 566625   digest :ZqsNHZd1r12UL7cFHdc9vw==
D/MediaMetadataRetrieverRunner: pts 600000   digest :CgUZWadeCe+S+e28C/4qkA==
D/MediaMetadataRetrieverRunner: pts 633375   digest :hUaJy2jWWa9RU4dR24Om7w==
D/MediaMetadataRetrieverRunner: pts 666625   digest :a5HlUYUJcz3xaQrn/hNsIg==
D/MediaMetadataRetrieverRunner: pts 700000   digest :YF70StAojixZxjk8epZL3w==
D/MediaMetadataRetrieverRunner: pts 733375   digest :rolF61sMxRMSP09ePtUGcQ==
D/MediaMetadataRetrieverRunner: pts 766625   digest :K+qZUFNP5EgzWPscmmnFew==
D/MediaMetadataRetrieverRunner: pts 800000   digest :d8iVGhHf3VpMn+vMnBdmng==
D/MediaMetadataRetrieverRunner: pts 833375   digest :cKREwaki8AJmOuSVa8Zvbw==
D/MediaMetadataRetrieverRunner: pts 866625   digest :b3QXkX+wTE1CuCe79JK7Ww==
D/MediaMetadataRetrieverRunner: pts 900000   digest :atzAyrZNcOf8Ghgf04lftw==
D/MediaMetadataRetrieverRunner: pts 933375   digest :hS9ukOCLMCobHplBeRNdOA==
D/MediaMetadataRetrieverRunner: pts 966625   digest :rZyr6vVt5ae+TiMMVTCRrg==




=====


==低端
D/MediaMetadataRetrieverRunner: pts 1601625      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1635000      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1668375      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1701750      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1735000      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1768375      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1801750      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1835125      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1868500      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1901875      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1935250      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 1968625      digest :Ba29dyKehU1o6EanhG0wvg==
D/MediaMetadataRetrieverRunner: pts 2002000      digest :dcMXdpTton6/YQHJA2zCjg==
D/MediaMetadataRetrieverRunner: pts 2035375      digest :830odrRgw9UyAhNKeBUWAA==
D/MediaMetadataRetrieverRunner: pts 2068750      digest :8WFabowQKX+I2q3XNC2HJg==
D/MediaMetadataRetrieverRunner: pts 2102125      digest :8WFabowQKX+I2q3XNC2HJg==
D/MediaMetadataRetrieverRunner: pts 2135500      digest :8WFabowQKX+I2q3XNC2HJg==
D/MediaMetadataRetrieverRunner: pts 2168875      digest :dcMXdpTton6/YQHJA2zCjg==
D/MediaMetadataRetrieverRunner: pts 2202250      digest :dcMXdpTton6/YQHJA2zCjg==
D/MediaMetadataRetrieverRunner: pts 2235500      digest :dcMXdpTton6/YQHJA2zCjg==
D/MediaMetadataRetrieverRunner: pts 2268875      digest :dcMXdpTton6/YQHJA2zCjg==
D/MediaMetadataRetrieverRunner: pts 2302250      digest :dcMXdpTton6/YQHJA2zCjg==
D/MediaMetadataRetrieverRunner: pts 2335625      digest :h5ZNxcxO6WrbYFQPsknjnw==
D/MediaMetadataRetrieverRunner: pts 2369000      digest :KXZQ788bP25Oqi7sHxWPLg==
D/MediaMetadataRetrieverRunner: pts 2402375      digest :KXZQ788bP25Oqi7sHxWPLg==
D/MediaMetadataRetrieverRunner: pts 2435750      digest :KXZQ788bP25Oqi7sHxWPLg==
D/MediaMetadataRetrieverRunner: pts 2469125      digest :KXZQ788bP25Oqi7sHxWPLg==
D/MediaMetadataRetrieverRunner: pts 2502500      digest :2h341j90Scn3kRtA4Fr+IA==
D/MediaMetadataRetrieverRunner: pts 2535875      digest :2h341j90Scn3kRtA4Fr+IA==
D/MediaMetadataRetrieverRunner: pts 2569250      digest :OOdPxTb4/NWbUrC8HDzpng==
D/MediaMetadataRetrieverRunner: pts 2602625      digest :OOdPxTb4/NWbUrC8HDzpng==
D/MediaMetadataRetrieverRunner: pts 2636000      digest :OOdPxTb4/NWbUrC8HDzpng==
D/MediaMetadataRetrieverRunner: pts 2669375      digest :OOdPxTb4/NWbUrC8HDzpng==
D/MediaMetadataRetrieverRunner: pts 2702750      digest :OOdPxTb4/NWbUrC8HDzpng==


相关代码

public class MediaMetadataRetrieverRunner {

  private String TAG = MediaMetadataRetrieverRunner.class.getSimpleName();

  void scanAndDigest(String path) throws Exception {
    List<Long> ptsUsList = VideoPtsScanner.scanPts(path);
    Log.d(TAG, "pts list in micro sec: " + ptsUsList);

    MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    retriever.setDataSource(path);

    for (long ptsUs : ptsUsList) {
      Bitmap bitmap = retriever.getFrameAtTime(ptsUs, MediaMetadataRetriever.OPTION_CLOSEST);

      if (bitmap == null) {
        Log.e(TAG, "got null in pts " + ptsUs);
        continue;
      }

      int size = bitmap.getAllocationByteCount();
      ByteBuffer buffer = ByteBuffer.allocateDirect(size);
      bitmap.copyPixelsToBuffer(buffer);
      buffer.flip();

      String digest = makeString(buffer);
      Log.d(TAG, "pts " + ptsUs + " \t digest :" + digest);
    }

    retriever.release();
  }

  private String makeString(ByteBuffer buffer) throws NoSuchAlgorithmException {
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    md5.update(buffer);
    byte[] digest = md5.digest();
    // covert into base64 string, since it's already provided in android
    return Base64.encodeToString(digest, Base64.DEFAULT);
  }

}

-----
public class VideoPtsScanner {

  public static List<Long> scanPts(String path) throws IOException {
    MediaExtractor mediaExtractor = new MediaExtractor();
    mediaExtractor.setDataSource(path);
    mediaExtractor.selectTrack(getVideoTrack(mediaExtractor));

    List<Long> ret = new ArrayList();
    long pts = -1;
    while ((pts = mediaExtractor.getSampleTime()) >= 0) {
      ret.add(pts);
      mediaExtractor.advance();
    }

    mediaExtractor.release();

    return ret;
  }

  private static int getVideoTrack(MediaExtractor extractor) {
    for (int i = 0; i < extractor.getTrackCount(); i++) {
      MediaFormat format = extractor.getTrackFormat(i);
      if (isVideo(format)) {
        return i;
      }
    }

    return -1;
  }

  private static boolean isVideo(MediaFormat format) {
    return format.getString(MediaFormat.KEY_MIME).toLowerCase().contains("video");
  }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容

  • 053期,大底014 015 017 024 025 027 034 036 041 042 043 045 04...
    想我的D友你就进阅读 372评论 0 1
  • 很高兴今天能够参加信与行成长舍榜样季的最后一堂课。今天回顾了十个最关键的种子。学习和巩固了之前学习的内容,也和大家...
    马思月阅读 579评论 0 7
  • 摄影器材:红米note 摄影者:歸壹 所感: 最美如你,遇见是一种美丽的意外。或惊喜,或赞叹,或回味。这一切的一切...
    夏小二儿阅读 273评论 0 4