详解Android开发之MP4文件转GIF文件

一 基本实现原理

在介绍具体实现过程之前,先简单说下基本原理和实现步骤,在解决相对比较复杂的问题,我习惯先理清主要原理步骤,不要一开始就被繁琐细节绊住,待具体实现时再逐个攻破。下面是主要步骤:

1、视频文件的读取:包括录制和本地文件读取

2、将需要转换的视频部分解析为 Bitmap 序列

3、将解析好的 Bitmap 序列编码生成 GIF 文件

二 视频文件的读取

视频文件的读取比较简单,没什么特别需要说的地方,这里简单贴出视频读取的核心部分代码,详细实现可以Google一下就行了。

private View.OnClickListener clickListener = new View.OnClickListener() {
 @Override
 public void onClick(View v) {
 Intent intent = new Intent();
 intent.setType("video/*");
 intent.setAction(Intent.ACTION_GET_CONTENT);
 startActivityForResult(Intent.createChooser(intent, "Select Video"), SELECT_VIDEO);
 }
};

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 if (requestCode == REQUEST_SELECT_VIDEO) {
 if (resultCode == RESULT_OK) {
  Uri videoUri = data.getData();
  filePath = getRealFilePath(videoUri);
 }
 }
}

三 视频文件的解析

视频文件读取成功后,接下来要做的就是解析视频文件,选取需要转换的视频片段,提取Bitmap序列。下面来看下具体实现,提取 Bitmap 序列就是根据给定的起始时间和结束时间以及帧率从视频文件中获取相应的 Bitmap,本文主要是利用 MediaMetadataRetriever 提供的 API 来实现的,在看代码前可以先看下 MediaMetadataRetriever 的 API 文档,该类的核心功能就是获取视频的帧和元数据,下面是核心实现代码:

public List<Bitmap> createBitmaps(String path) {
 MediaMetadataRetriever mmr = new MediaMetadataRetriever();
 mmr.setDataSource(path);
 double inc = 1000 * 1000 / fps;

 for (double i = begin; i < end; i += inc) {
 Bitmap frame = mmr.getFrameAtTime((long) i, MediaMetadataRetriever.OPTION_CLOSEST);
 if (frame != null) {
  bitmaps.add(scale(frame));
 }
 }

 return bitmaps;
}

private Bitmap scale(Bitmap bitmap) {
 return Bitmap.createScaledBitmap(bitmap,
 width > 0 ? width : bitmap.getWidth(),
 height > 0 ? height : bitmap.getHeight(),
 true);
}

四 生成 GIF 文件

拿到要生成 GIF 的 Bitmap 序列,接下来需要做的就是将 Bitmap 序列中的数据按照 GIF 的文件格式编码,生成最终的 GIF 文件。目标很明确,接下来就看具体实现过程了。

1. GIF 格式简介

生成 GIF 文件之前有必要介绍下 GIF 的存储格式,GIF 格式的相关文章比较多,这里也没必要太详细的介绍,只是简单说下后面程序中会用到的方面。

GIF 图象是基于颜色列表的(存储的数据是该点的颜色对应于颜色列表的索引值),最多只支持 8 位(256 色)。GIF 文件内部分成许多存储块,用来存储多幅图象或者是决定图象表现行为的控制块,用以实现动画和交互式应用。GIF 文件还通过 LZW 压缩算法压缩图象数据来减少图象尺寸。

GIF 文件内部是按块划分的,包括控制块和数据块两种。控制块是控制数据块行为的,根据不同的控制块包含一些不同的控制参数;数据块只包含一些 8-bit 的字符流,由它前面的控制块来决定它的功能,每个数据块 0 到 255 个字节,数据块的第一个字节指出这个数据块大小(字节数),计算数据块的大小时不包括这个字节,所以一个空的数据块有一个字节,那就是数据块的大小0x00。

2. GIF 文件写入

刚开始接触 GIF 文件会觉得比较复杂,存储格式、编码格式等都比 Bitmap 要复杂的多,但其实可以把问题简单化理解,生成 GIF 和生成 Bitmap 原理类似,就是按照规定的格式写文件就行了,不用太纠结内部细节,否则就会陷入繁琐的细节(俗称钻牛角尖)而忽略了最终目的只是为了生成 GIF 文件。下面就来看下有哪些文件部分需要写入的:

提取 Bitmap 的像素值

首先需要将上面得到的 Bitmap 的像素值提取出来,方便后面把像素值写入到 GIF 文件中,在提取像素值的同时,生成 GIF 文件所需要的颜色表,生成颜色表过程比较复杂,这里就不贴出源码,感兴趣的可以Google一下颜色量化算法,不感兴趣的直接用现成的就好,下面是提取像素值的具体实现:

protected void getImagePixels() {
 int w = image.getWidth();
 int h = image.getHeight();
 pixels = new byte[w*h*3];
 for (int i = 0; i < h; i++) {
 int stride = w * 3 * i;
 for (int j = 0; j < w; j++) {
  int p = image.getPixel(j, i);
  int step = j * 3;
  int offset = stride + step;
  // blue
  pixels[offset+0] = (byte) ((p & 0x0000FF) >> 0);
  // green
  pixels[offset+1] = (byte) ((p & 0x00FF00) >> 8);
  // red
  pixels[offset+2] = (byte) ((p & 0xFF0000) >> 16);
 }
 }
}

GIF 文件头(Header)

文件头部分总共 6 个字节,包括:GIF 署名和版本号,GIF 署名由 3 个字符"GIF"组成,共 3 个字节,版本号也是由 3 个字节组成,可以为"87a"或"89a"(分别为 1987 年和 1989 年版本),实现代码如下:

// 写入文件头
protected void writeHeader() throws IOException {
 writeString("GIF89a");
}

protected void writeString(String s) throws IOException {
 for (int i = 0; i < s.length(); i++) {
 out.write((byte) s.charAt(i));
 }
}

逻辑屏幕标识符(Logical Screen Descriptor)

文件头的后面是逻辑屏幕标识符(Logical Screen Descriptor),这一部分由 7 个字节组成,定义了 GIF 图象的大小、颜色深度、背景色以及有无全局颜色列表和颜色列表的索引数。实现代码如下:

// 写入逻辑屏幕标识符
protected void writeLSD() throws IOException {
 writeShort(width); // 写入图像宽度
 writeShort(height); // 写入图像高度

 out.write((0x80 | // 全局颜色列表标志置 1
    0x70 | // 确定图象的颜色深度(7+1=8)
    0x00 | // 全局颜色列表分类排列置为 0
    0x07)); // 颜色列表的索引数(2的7+1次方)

 out.write(0); // 背景颜色(在全局颜色列表中的索引)
 out.write(0); // 像素宽高比默认 1:1
}

protected void writeShort(int value) throws IOException {
 out.write(value & 0xff);
 out.write((value >> 8) & 0xff);
}

逻辑屏幕标识符部分结构稍微复杂些,如果不知道每一位代表什么意思可以参考:GIF图形文件格式文档 中的逻辑屏幕标识符部分。

全局颜色列表(Global Color Table)

全局颜色列表必须紧跟在逻辑屏幕标识符后面,每个颜色列表索引条目由三个字节组成,按R、G、B的顺序排列,具体生成颜色表的实现可以看源码部分,由于生成过程比较复杂,这里就不贴颜色表生成的代码了,下面是写入颜色表的代码:

// 写入颜色表
protected void writePalette() throws IOException {
 out.write(colorTab, 0, colorTab.length);
 int n = (3 * 256) - colorTab.length;
 for (int i = 0; i < n; i++) {
 out.write(0);
 }
}

图形控制扩展(Graphic Control Extension)

这一部分是可选的,89a 版本才支持,可以放在一个图象块(包括图象标识符、局部颜色列表和图象数据)或文本扩展块的前面,用来控制跟在它后面的第一个图象(或文本)的渲染( Render )形式,下面实现代码:

protected void writeGraphicCtrlExt() throws IOException {
 out.write(0x21); // 扩展块标识,固定值 0x21
 out.write(0xf9); // 图形控制扩展标签,固定值 0xf9
 out.write(4); // 块大小,固定值 4
 out.write(0 | // 1:3 保留位
   0 | // 4:6 不使用处置方法
   0 | // 7 用户输入标志置 0
   0); // 8 透明色标志置 0

 writeShort(delay); // 延迟时间
 out.write(0);  // 透明色索引值
 out.write(0);  // 块终结器,固定值 0
}

图象标识符(Image Descriptor)

一个 GIF 文件内可以包含多幅图象,一幅图象结束之后紧接着下是一幅图象的标识符,图象标识符以 0x2C(',')字符开始,定义紧接着它的图象的性质,包括图象相对于逻辑屏幕边界的偏移量、图象大小以及有无局部颜色列表和颜色列表大小,由10个字节组成,下面是实现代码:

protected void writeImageDesc() throws IOException {
 out.write(0x2c); // 图象标识符开始,固定值为 0x2c
 writeShort(0);  // x 方向偏移
 writeShort(0);  // y 方向偏移
 writeShort(width); // 图像宽度
 writeShort(height); // 图像高度
 out.write((
  0x80 |  // 局部颜色列表标志置 1
  0x00 |
  0x00 |
  0x07));  // 局部颜色列表的索引数(2的7+1次方)
}

图象数据(Image Data)

GIF 图象数据使用了 LZW 压缩算法,大大减小了图象数据的大小,具体的 LZW 压缩算法可以Google一下,程序实现部分可以参考文章底部的源码链接。下面是图像数据的写入实现:

protected void writePixels() throws IOException {
 LZWEncoder encoder = new LZWEncoder(
  width, height, indexedPixels, colorDepth);
 encoder.encode(out);
}

文件终结器(Trailer)

这一部分只有一个字节,标识一个GIF文件结束,固定值为 0x3B,实现代码:

public void finish() throws IOException {
 out.write(0x3b);
 out.flush();
 out.close();
}

总结

到目前为止,将 MP4 文件转换为 GIF 文件的实现过程基本完成,如果需要对 GIF 文件进行裁剪、添加水印等处理的话,可以在 Bitmap 序列写入 GIF 之前,对 Bitmap 进行相应的处理即可,如果有什么问题欢迎交流学习。希望本文的内容对大家的学习工作能有所帮助。

(0)

相关推荐

  • Android实现使用流媒体播放远程mp3文件的方法

    本文实例讲述了Android实现使用流媒体播放远程mp3文件的方法.分享给大家供大家参考,具体如下: package com.shadow.util; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.

  • Android清除工程中无用资源文件的两种方法

    一.调用Android lint命令查找出没有用到的资源,并生成一个清单列表: 命令:lint –check "UnusedResources" [project_path] > result.txt 执行完之后会生成一个清单文件,内容如下: 二.使用代码自动删除无用的文件: public class DelAction { public static void main(String[] args) throws IOException { String projectPath

  • Android 通过httppost上传文本文件到服务器的实例代码

    废话不多说了,直接给大家贴关键代码了. /** * 往服务器上上传文本 比如log日志 * @param urlstr 请求的url * @param uploadFile log日志的路径 * /mnt/shell/emulated/0/LOG/LOG.log * @param newName log日志的名字 LOG.log * @return */ public static void httpPost(Activity activity,String urlstr,String uplo

  • Android遍历所有文件夹和子目录搜索文件

    本文实例为大家分享了android遍历所有文件夹和子目录来搜索文件,供大家参考,具体内容如下 java代码: import java.io.File; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget

  • Android实现打开各种文件的intent方法小结

    本文实例讲述了Android实现打开各种文件的intent方法.分享给大家供大家参考,具体如下: import android.app.Activity; import Android.content.Intent; import android.net.Uri; import android.net.Uri.Builder; import Java.io.File; import android.content.Intent; //自定义android Intent类, //可用于获取打开以下

  • Android 文件选择器详解及实例代码

    本文给大家讲解下Android文件选择器的使用.实际上就是获取用户在SD卡中选择的文件或文件夹的路径,这很像C#中的OpenFileDialog控件. 此实例的实现过程很简单,这样可以让大家快速的熟悉Android文件选择器,提高开发效率. 网上曾经见到过一个关于文件选择器的实例,很多人都看过,本实例是根据它修改而成的,但更容易理解,效率也更高,另外,本实例有自己的特点:   1.监听了用户按下Back键的事件,使其返回上一层目录.        2.针对不同的文件类型(文件vs文件夹 , 目标

  • Android如何遍历特定目录下所有文件

    第一个案例为大家分享了Android遍历特定目录下所有文件,包含子目录的,并删除最新创建的. private boolean deleteLastFromFloder(String path) { boolean success = false; try { ArrayList<File> images = new ArrayList<File>(); getFiles(images, path); File latestSavedImage = images.get(0); if

  • 详解Android开发之MP4文件转GIF文件

    一 基本实现原理 在介绍具体实现过程之前,先简单说下基本原理和实现步骤,在解决相对比较复杂的问题,我习惯先理清主要原理步骤,不要一开始就被繁琐细节绊住,待具体实现时再逐个攻破.下面是主要步骤: 1.视频文件的读取:包括录制和本地文件读取 2.将需要转换的视频部分解析为 Bitmap 序列 3.将解析好的 Bitmap 序列编码生成 GIF 文件 二 视频文件的读取 视频文件的读取比较简单,没什么特别需要说的地方,这里简单贴出视频读取的核心部分代码,详细实现可以Google一下就行了. priva

  • 详解 Corba开发之Java实现Service与Client

    详解 Corba开发之Java实现Service与Client 1      概述 CORBA(Common Object Request Broker Architecture,公共对象请求代理体系结构)是由OMG组织制订的一种标准的面向对象应用程 序体系规范.或者说 CORBA体系结构是OMG为解决分布式处理环境(DCE)中,硬件和软件系统的互连而提出的一种解决方案. OMG:Object Management Group,对象管理组织.是一个国际化的.开放成员的.非盈利性的计算机行业标准协

  • 实例详解IOS开发之UIWebView

    iOS开发之UIWebView 是本文要介绍的内容,UIWebView是iOS sdk中一个最常用的控件.是内置的浏览器控件,我们可以用它来浏览网页.打开文档等等.这篇文章我将使用这个控件,做一个简易的浏览器.如下图: 我们创建一个Window-based Application程序命名为:UIWebViewDemo UIWebView的loadRequest可以用来加载一个url地址,它需要一个NSURLRequest参数.我们定义一个方法用来加载url.在UIWebViewDemoViewC

  • 详解微信开发之access_token之坑

    首先不得不提到access_token的分类,一是普通access_token,二是网页授权access_token.其中前者是用于调用微信提供的各种借口,作为开发者的调用凭证,一般有效期为7200S,获取次数受限:另一种是第三方网页若需要使用用户微信账户登录,需要获取该access_token从而来获取用户微信账户信息.这个一定得区分开. 另外获取用户微信账户信息也有两种情况,一是普通的获取用户信息,它只需要调用微信用户信息接口即可获取,因而使用到的是第一种普通access_token,另一种

  • 详解微信开发之Author网页授权

    微信开发中,经常有这样的需求:获得用户头像.绑定微信号给用户发信息.. 那么实现这些的前提就是授权! 1.配置安全回调域名: 在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的"开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息"的配置选项中,修改授权回调域名,值得注意的是这里就是直接写全域名,如: www.liliangel.cn.然而我们开发h5中一般用的是二级域名,如:h5.liliangel.cn 也同样在安全回调域名中. 2.用户级授权

  • 详解iOS开发之NSURLProtocol的那些坑

    NSURLProtocol NSURLProtocol能够让你去重新定义苹果的URL加载系统 (URL Loading System)的行为,URL Loading System里有许多类用于处理URL请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSU

  • Android开发之Android.mk模板的实例详解

    Android开发之Android.mk模板的实例详解 关于Android NDK开发的文章已经比较多了,我的博客中也分享了很多NDK开发相关经验和技巧,今天简单写了一个 Android.mk 的示例模板,供初学者参考. 本模板主要给大家示例 Android NDK 开发中的如下几个问题: 1. 如何自动添加需要编译的源文件列表   2. 如何添加第三方静态库.动态库的依赖   3. 如何构造一个完整的NDK工程框架 假设我们的项目依赖 libmath.a, libjson.a, libffmp

  • Android开发之AppWidget详解

    Android通知系统是它的一大特色,而其中,AppWidget是其中一个亮点.在开发应用的中,很多时候可以为其添加一个AppWidget显示在桌面中,及时方便的与用户进行 交互.这里就简单的熟悉一下开发一个AppWidget的流程吧. 想要在应用中创建一个AppWidget,至少需要以下几样东西: 需要创建一个AppWidgetProviderInfo,来描述AppWidget的元数据. 需要实现一个自己的AppWidgetProvider对AppWidget进行更新等操作. 需要布局文件来描

  • Android开发之Kotlin委托的原理与使用详解

    目录 前言 一.接口/类委托 二.属性委托 三.延迟委托 四.观察者委托 五.Map委托 总结 前言 在设计模式中,委托模式(Delegate Pattern)与代理模式都是我们常用的设计模式(Proxy Pattern),两者非常的相似,又有细小的区分. 委托模式中,委托对象和被委托对象都是同一类型的对象,委托对象将任务委托给被委托对象来完成.委托模式可以用于实现事件监听器.回调函数等功能. 代理模式中,代理对象与被代理对象是两种不同的对象,代理对象代表被代理对象的功能,代理对象可以控制客户对

  • Android开发之ContentProvider的使用详解

    前言         Content Provider为存储数据和获取数据提供了统一的接口,它可以完成在不同应用程序下的数据共享,而在上一篇文章Android开发之SQLite的使用方法讲到的SQLite只能在同一个程序中共享数据.另外android为一些常见的数据,比如说音频,视频,图片,通讯录等提供了Content Provider,这样我们就可以很方便的对这些类型的数据操作了.使用ContentProvider的好处是开发人员不需要考虑数据内部是怎么存储的,比如说如果我们想利用Conten

随机推荐