热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:AndroidMultimedia框架总结(二十四)MediaMuxer实现手机屏幕录制成gif图

篇首语:本文由编程笔记#小编为大家整理,主要介绍了AndroidMultimedia框架总结(二十四)MediaMuxer实现手机屏幕录制成gif图相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Android Multimedia框架总结(二十四)MediaMuxer实现手机屏幕录制成gif图相关的知识,希望对你有一定的参考价值。



转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼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 createBitmaps(String path) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(path);
double inc = 1000 * 1000 / fps;
for (double i = begin; i 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 int stride = w * 3 * i;
for (int j = 0; 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 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 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 密码: 2wb3

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



这里写图片描述


如果你觉得好,随手点赞,也是对笔者的肯定,也可以分享此公众号给你更多的人,原创不易

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



推荐阅读
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了在Linux下安装Perl的步骤,并提供了一个简单的Perl程序示例。同时,还展示了运行该程序的结果。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文讲述了如何通过代码在Android中更改Recycler视图项的背景颜色。通过在onBindViewHolder方法中设置条件判断,可以实现根据条件改变背景颜色的效果。同时,还介绍了如何修改底部边框颜色以及提供了RecyclerView Fragment layout.xml和项目布局文件的示例代码。 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 在开发app时,使用了butterknife后,在androidStudio打包apk时可能会遇到报错。为了解决这个问题,可以通过打开proguard-rules.pro文件进行代码混淆来解决。本文介绍了具体的混淆代码和方法。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
author-avatar
Yunir_944
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有