mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

转载请把头部出处链接和尾部二维码一起转载。本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/53866405

前言:上篇中,介绍是用MediaMuxer与MediaExtractor进入音视频的裁剪。今天用MediaMuxer与AudioRecord与MediaCodec及Surface进行屏幕录制成gif。看下Agenda:

  • 效果图
  • 主体思路
  • 转gif两种方案

MediaMuxer是用于将音频和视频进行混合生成多媒体文件。

缺点是眼下仅仅能支持一个audio track和一个video track。并且仅支持mp4输出。

效果图1:操作步骤


这里写图片描写叙述

效果图2:注意效果图里的gif就是终于产生的录制屏幕后产生的


这里写图片描写叙述

主体思路:

逻辑:录屏不须要操作视频原始数据,因此使用InputSurface作为编码器的输入。

视频:MediaProjection通过createVirtualDisplay创建的VirtualDisplay传入的Surface是通过MediaCodec的createInputSurface方法返回的,【本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/53866405】表明编码器的输入事实上来自于录制到的屏幕数据,于是仅仅须要在MediaCodec的输出缓冲区中拿到编码后的ByteBuffer就可以。

音频:录制程序获得音频原始数据PCM,传给MediaCodec编码,然后从MediaCodec的输出缓冲区拿到编码后的ByteBuffer就可以。

终于通过合并模块将音视频混合。

视频:MediaProjection通过createVirtualDisplay创建的VirtualDisplay传入的Surface是通过ImageReader的getSurface方法返回的,表明录制的屏幕帧数据传递到ImageReader,于是通过ImageReader的相关API能够读取到录制的屏幕每一帧的数据

音频:因为录制的就是原始PCM编码的音频数据,因此录制到音频数据后直接调用AudioRecord就可以。

简单说就是重定向了屏幕录制的数据的方向,这个Surface提供的是什么,录制的视频数据就传到哪里。Surface提供的是本地某个SurfaceView控件。那么就会将屏幕内容显示到这个控件上。提供MediaCodec就是作为编码器的输入源终于获得编码后的数据。提供ImageReader就会作为ImageReader的数据源,终于获得了视频的原始数据流。

因为录制的是视频,得变成gif。有两种方案:

  • 提取视频文件->解析视频->提取 Bitmap 序列(使用 MediaMetadataRetriever 提取某一时刻的图片。然后把非常多某一时刻的图片串联起来编码成 gif。看来其也正是 gif 的原理。但实现出来的效果极差。无法准确提取到准确的图片,导致合成的 gif 图也无法连贯播放,播放起来也跳帧跳得非常厉害。能够用慘不忍睹来形容)
  • 利用FFmpeg直接转gif, 这个在我的《FFmpeg在Linux下安装编译过程》一文中。就是把编译出来库,进行演示转gif。当时是SuperIndicator的gif。

    对于把Android上,也是相同的原理。

    这样的方法岗岗的。

方案一思路

视频文件的解析

视频文件读取成功后,接下来要做的就是解析视频文件。选取须要转换的视频片段,提取 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 的 Bitmap 序列,接下来须要做的就是将 Bitmap 序列中的数据依照 GIF 的文件格式编码,生成终于的 GIF 文件。目标非常明白,接下来就看详细实现过程了。

GIF 格式简单介绍

生成 GIF 文件之前有必要介绍下 GIF 的存储格式。仅仅是简单说下后面程序中会用到的方面。


GIF 图象是基于颜色列表的(存储的数据是该点的颜色对应于颜色列表的索引值),最多仅仅支持 8 位(256 色)。GIF 文件内部分成很多存储块,用来存储多幅图象或者是决定图象表现行为的控制块,用以实现动画和交互式应用。GIF 文件还通过 LZW 压缩算法压缩图象数据来降低图象尺寸。
GIF 文件内部是按块划分的,包含控制块和数据块两种。控制块是控制数据块行为的,依据不同的控制块包含一些不同的控制參数;数据块仅仅包含一些 8-bit 的字符流,由它前面的控制块来决定它的功能,每一个数据块 0 到 255 个字节。数据块的第一个字节指出这个数据块大小(字节数),计算数据块的大小时不包含这个字节。所以一个空的数据块有一个字节,那就是数据块的大小0x00。

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的顺序排列,详细生成颜色表的实现能够看源代码部分,因为生成过程比較复杂,【本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/53866405】这里就不贴颜色表生成的代码了。以下是写入颜色表的代码:

// 写入颜色表
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 之前。对【本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/53866405】 Bitmap 进行对应的处理就可以

方案二思路:

编译so文件过程:

这里写图片描写叙述

编译最后产生so,会自己主动生成一个libs文件夹:

这里写图片描写叙述

点击进入libs文件夹,能够发现一个是arm平台的so文件夹,一个是x86平台的so文件夹:

这里写图片描写叙述

随便点击一个,进入。就是一些so:

这里写图片描写叙述

体验 apk

下载地址:链接: https://pan.baidu.com/s/1skR35nB password: 2wb3

第一时间获得博客更新提醒,以及很多其它android干货,源代码分析,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码。就可以关注。

这里写图片描写叙述

假设你认为好,随手点赞,也是对笔者的肯定,也能够分享此公众号给你很多其它的人。原创不易

资料參考:(http://www.jb51.net/article/91305.htm

posted on 2017-08-14 11:13  mthoutai  阅读(824)  评论(0编辑  收藏  举报