导言:

根据文章标题,按三步走,一、视频播放;二、连续截图;三、转换成gif。视频播放很自然想到用MediaPlayer或者VideoView,但我在这里踩了几个坑,写在这里也希望别人少走点弯路。首先,是MediaPlayer+SurfaceView的坑,如果只是想实现视频播放,那么用这种方式确实不错,但是并不能实现截图,SurfaceView一般通过getHolder().lockCanvas()可以获取到Canvas,那么通过这个Canvas不就可以获取到它的bitmap了吗?错了!那只是针对普通的静态画面而言,像视频播放这样的动态画面来说,一开始播放,是不允许调用这个接口的,否则会出现SurfaceHolder: Exception locking surface和java.lang.IllegalArgumentException的错误。那么用下面这种方式呢:

View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
bitmap = view.getDrawingCache();

依然不行,SurfaceView部分截取出来的是黑屏,原因很多文章讲过我就不重复了。那么用VideoView呢?事实上,VideoView也是继承自SurfaceView,所以一样会截屏失败。有人会说用MediaMetadataRetriever就可以很方便截屏了啊,管它是VideoView还是SurfaceView都能截。是的,MediaMetadataRetriever跟VideoView或者SurfaceView一点关系都没有,它只需获取到视频文件根本不需要视频播放出来就能通过getFrameAtTime(long timeUs)这个接口获取指定时间的视频。但是,我还想说,但是,MediaMetadataRetriever获取的是指定位置附近的关键帧,而视频文件的关键帧,就我所测试,2-5秒才有一个关键帧,所以如果通过getFrameAtTime接口获取2-5秒内的几十张bitmap,你会发现每张都是一样的,真是令人崩溃,根本无法满足制作gif需要的帧率。

那么用什么方式播放才能连续获取到正确的截图呢?答案是MediaPlayer+TextureView的方式。

一、视频播放

activity先实现SurfaceTextureListener接口,在onCreate的时候调用TextureView的setSurfaceTextureListener(TextureVideoActivity.this)即可,在TextureView初始化完成之后,会自动调用SurfaceTextureListener的接口方法onSurfaceTextureAvailable,在这里进行MediaPlayer的初始化并开始播放:

    @Overridepublic void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {//surface不能重复使用,每次必须重新new一个surface = new Surface(surfaceTexture);if (!TextUtils.isEmpty(mUrl)) {startPlay();}}private void startPlay() {if (mMediaPlayer == null) {mMediaPlayer = new MediaPlayer();}//mUrl是本地视频的路径地址mMediaPlayer.setDataSource(this, Uri.parse(mUrl));mMediaPlayer.setSurface(surface);mMediaPlayer.setLooping(false);mMediaPlayer.prepareAsync();mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mediaPlayer) {mediaPlayer.start();}});}

看,很简单,这样就可以开始进行播放了。需要注意的是,界面跳转或切换到后台再切回来,就会再次调用该接口,而原先的Surface不能再用,需要重新new一个。

注:后续为适配android10.0,targetSdkVersion升级到29,出现问题了,onSurfaceTextureAvailable不会执行导致画面没有了,解决方法在这里:TextureView有声音没画面&onSurfaceTextureAvailable没调用,不想看的也没关系,代码已更新。

二、截图

截图非常简单,只需要调用TextureView的getBitmap()方法就可以,连续快速地调用都没有问题。

三、转换成gif

这里用到了一个第三方开源项目GifBuilder(https://github.com/GLGJing/GIFBuilder),使用也很简单:

//另开线程并执行
GIFEncoder encoder = new GIFEncoder();
encoder.init(bitmaps.get(0));
encoder.setFrameRate(1000 / DURATION);
//filePath为本地gif存储路径
encoder.start(filePath);
for (int i = 1; i < bitmaps.size(); i++) {encoder.addFrame(bitmaps.get(i));
}
encoder.finish();

bitmaps是在定时循环DURATION下总共取得的bitmap列表,这样一个gif就制作完成了。但是这样执行的速度会非常慢,三四十张bitmap的转换就需要好几分钟,显然不行,于是我参照GifEncoder类再写了一个GifEncoderWithSingleFrame的类,将每张bitmap各自转换成一张临时的.partgif文件,待所有的bitmap都转换完之后再合并成一张gif图片,代码稍微长了些:

            List<String> fileParts = new ArrayList<>();ExecutorService service = Executors.newCachedThreadPool();final CountDownLatch countDownLatch = new CountDownLatch(bitmaps.size());for (int i = 0; i < bitmaps.size(); i++) {final int n = i;final String fileName = getExternalCacheDir() + File.separator + (n + 1) + ".partgif";fileParts.add(fileName);Runnable runnable = new Runnable() {@Overridepublic void run() {GIFEncoderWithSingleFrame encoder = new GIFEncoderWithSingleFrame();encoder.setFrameRate(1000 / frameRate / 1.5f);Log.e(TAG, "总共" + bitmaps.size() + "帧,正在添加第" + (n + 1) + "帧");if (n == 0) {encoder.addFirstFrame(fileName, bitmaps.get(n));} else if (n == bitmaps.size() - 1) {encoder.addLastFrame(fileName, bitmaps.get(n));} else {encoder.addFrame(fileName, bitmaps.get(n));}countDownLatch.countDown();}};service.execute(runnable);}try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}handler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(TextureVideoActivity.this, "gif初始化成功,准备合并", Toast.LENGTH_SHORT).show();}});SequenceInputStream sequenceInputStream = null;FileOutputStream fos = null;try {Vector<InputStream> streams = new Vector<InputStream>();for (String filePath : fileParts) {InputStream inputStream = new FileInputStream(filePath);streams.add(inputStream);}sequenceInputStream = new SequenceInputStream(streams.elements());File file = new File(getExternalCacheDir() + File.separator + System.currentTimeMillis() + ".gif");if (!file.exists()) {file.createNewFile();}fos = new FileOutputStream(file);byte[] buffer = new byte[1024];int len = 0;while ((len = sequenceInputStream.read(buffer)) != -1) {fos.write(buffer, 0, len);}fos.flush();fos.close();sequenceInputStream.close();handler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(TextureVideoActivity.this, "gif制作完成", Toast.LENGTH_SHORT).show();}});for (String filePath : fileParts) {File f = new File(filePath);if (f.exists()) {f.delete();}}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}if (sequenceInputStream != null) {try {sequenceInputStream.close();} catch (IOException e) {e.printStackTrace();}}}

稍微解释下,这里用了ExecutorService线程池和CountDownLatch线程控制工具类,保证所有线程执行完再执行countDownLatch.await()下面的代码。gif的第一帧和最后一帧分别需要加入文件头和结束符等,所以需要区别对待,分别调用了addFirstFrame和addLastFrame,其他帧调用addFrame即可。然后利用SequenceInpustream这个类将所有.partgif文件统一加入到输入流,最终再用FileOutputStream输出来就可以。经过这样修改之后,gif的转换时间从几分钟缩短到了几秒钟(像素高一点图片数量多一点可能也需要20S左右)。

细节注意:

从TextureView.getBimap()获取到的bitmap像素因不同手机而不同,如果不做处理直接加入bitmap列表很容易引起OOM,所以需要对bitmap先进行尺寸压缩:

                Bitmap bitmap = mTexureView.getBitmap();String path = getExternalCacheDir() + File.separator + String.valueOf(count + 1) + ".jpg";BitmapSizeUtils.compressSize(bitmap, path, 720, 80);Bitmap bmp = BitmapFactory.decodeFile(path);//压缩后再添加bitmaps.add(bmp);
    public static void compressSize(Bitmap bitmap, String toFile, int targetWidth, int quality) {try {int bitmapWidth = bitmap.getWidth();int bitmapHeight = bitmap.getHeight();int targetHeight = bitmapHeight * targetWidth / bitmapWidth;Bitmap resizeBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true);File myCaptureFile = new File(toFile);FileOutputStream out = new FileOutputStream(myCaptureFile);if (resizeBitmap.compress(Bitmap.CompressFormat.JPEG, quality, out)) {out.flush();out.close();}if (!bitmap.isRecycled()) {bitmap.recycle();}if (!resizeBitmap.isRecycled()) {resizeBitmap.recycle();}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException ex) {ex.printStackTrace();}}

压缩成宽度720像素,这样压缩出来的图片比较清晰,当然最终的gif图片也会比较大,30-40张bitmap转换成的gif大概有3-4M左右,如果想让gif小一点,宽度设置成400左右也就够了。

上效果图:

转存失败重新上传取消

由25张分辨率440*247的bitmap合并而成,大小1.36M

转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消直接由本地传的图片居然不动,有知道怎么传的请告诉我!!!

最后,上Github源码, 源码还包括surfaceview和videoview的播放方式的代码,想看gif生成代码的只需要看TextureVideoActivity这个界面就可以了。

PS:后续测试,在执行GIFEncoderWithSingleFrame类的提取像素值方法getImagePixels时,因为涉及到密集型数据运算,CPU会飙高到90%左右,而不同手机因为CPU型号不同,转换100张440*260像素的bitmap在运行到这个方法时,有些手机如小米运算速度仍然非常快,只需要几秒钟,有些手机如华为、三星速度就慢成狗了,达到两分钟以上,忍无可忍。在JAVA层面计算大量数据确实不是明智的选择,所以我又把这个方法移到了JNI去计算,效果非常显著,执行这个方法最多只需要两秒钟,源码已更新。

PSS:发现手机拍的视频播放到TextureViewActivity界面的时候宽高比不对,又优化了下。首先想到的是调用mediaPlayer.getVideoWidth()和mediaPlayer.getVideoHeight()来对TextureView重新设置宽高,但失败了,mediaPlayer一旦准备就绪后就没办法再修改TextureView的size,否则播放无图像。这时候又想到了MediaMetadataRetriever,不得不说这时候它还是很好用的:

    /*** dp转换px*/public int dip2px(Context context, float dipValue) {return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, context.getResources().getDisplayMetrics());}private float videoWidth;private float videoHeight;private int videoRotation;private void initVideoSize() {MediaMetadataRetriever mmr = new MediaMetadataRetriever();try {mmr.setDataSource(mUrl);String width = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);String height = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);String rotation = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);videoWidth = Float.valueOf(width);videoHeight = Float.valueOf(height);videoRotation = Integer.valueOf(rotation);int w1;if (videoRotation == 90) {w1 = (int) ((videoHeight / videoWidth) * dip2px(TextureVideoActivity.this, 250));} else {w1 = (int) (videoWidth / videoHeight * dip2px(TextureVideoActivity.this, 250));}LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mPreview.getLayoutParams();layoutParams.width = w1;layoutParams.height = mPreview.getHeight();mPreview.setLayoutParams(layoutParams);} catch (Exception ex) {Log.e(TAG, "MediaMetadataRetriever exception " + ex);} finally {mmr.release();}}

播放前先调用以上代码进行TextureView的宽高初始化,MediaMetadataRetriever可以获取到视频源的宽高和旋转角度。手机拍摄的视频,不论是横着拍的还是竖着拍的,视频源的宽高都是默认横屏拍的宽高,所以必须要用到旋转角度进行判断。代码已更新到github上。

 
 
 

android视频播放截图并制作成gif图片相关推荐

  1. android 绘制正方形图片,是Android的自定义View-绘制流程-正方形图片控件(SquareImageView)...

    前言 了解View的绘制三大流程后,接下来就要对这些知识做一个实践,首先来实现一个在Android中最为常见的控件--方形图片控件,即让图片在一个方形区域内显示,最常见的场景是在九宫格图片当中. 一般 ...

  2. 怎么把视频里的一小段制作成gif图片?教你视频片断做成GIF

    很多小伙伴都在问,那么多好玩的gif图片是怎么制作的, 怎么把视频转gif(https://www.gif.cn)呢?有一种视频在线转gif格式的工具,可以简单又快速的把视频转换成动态图,赶紧跟着小编 ...

  3. android 动态截图软件下载,PixaMotion app-PixaMotion图片动态化软件下载v9.9.9 安卓版-西西软件下载...

    PixaMotion图片动态化软件,PixaMotion可以让你的照片"动起来",PixaMotion拥有强大的图片处理技术,可以将照片上的各个细节变得更加生动,让水流能真正的流动 ...

  4. ps怎么将图片制作成ico图标? ps制作ico图标的教程

    ps怎么将图片制作成ico图标? ps制作ico图标的教程 发布时间:2018-03-08 09:19:23   作者:塔上的蜗牛   我要评论 ps怎么将图片制作成ico图标?ps中想要设计一款一款 ...

  5. android图片保存形式,Android应用开发之Android ScrollView截图和图片保存到相册的方式...

    本文将带你了解Android应用开发之Android ScrollView截图和图片保存到相册的方式,希望本文对大家学Android有所帮助. 1.1首先来看你一种截取屏幕,这种代码有缺陷,只能截取一 ...

  6. 如何将图片序列化_PS如何将图片制作成gif动态图 ps制作gif动态图教程

    想要制作gif动态图片,为何不试试万能的PS呢!使用PS可以帮助用户快速轻松的制作gif动图,操作简单又方便.那么如何利用PS快速将图片做成gif动态图,其实方法是很简单,制作这种gif动图其实就是把 ...

  7. php生成gif1009php生成gif,怎样将几张图片做成会动的GIF的动态图像?GIF动画制作软件,将图片制作成GIF动图...

    打开软件找到上方菜单栏的"help"然后点击"registration",打开注册页面,我们回到刚刚程序包的位置,找到一个名为"Keymaker.ex ...

  8. android分享图片到qq,Android实现截图分享qq,微信

    Android实现截图分享qq,微信 立即下载 金额: 3 元 支付方式: 友情提醒:源码购买后不支持退换货 立即支付 发布时间:2018-05-23 概述 android上封装工具类,一行实现截屏分 ...

  9. php多张图片切换效果,怎么把多张图片制作成gif动图 可设置图片切换效果及显示时间...

    小编在微信上跟朋友斗图的时候发现,有些表情包是用很多张图片不断切换制作成的,这种动图是怎么制作出来的呢?要是学会了,可以将自己或者朋友的照片制作成gif动图,想想就很有意思呀!那么在此小编给大家推荐一 ...

最新文章

  1. phonegap android,Phonegap 3不适用于Android Studio
  2. JavaScript对象克隆
  3. 计算机软件评测减增值税,软件产品即征即退政策依据之一
  4. 关于分页的一些经验。
  5. 计算机外观类型,知道你的笔记本电脑是什么类型的吗?五大类型派对号入座
  6. Vue会了吗?来认识一下React吧(上)
  7. 对象的初始化列表const变量的初始化
  8. linux上卸载kafka,kafka安装在linux上的安装
  9. 快手用计算机说唱的叫什么,HIPHOP人物:“我们呢说唱,会在快手上爆炸!”
  10. CSS基础——CSS 背景(background)【学习笔记】
  11. 工作7年开发小哥转行测试:只有努力向前奔跑,才能得到你要的~
  12. 华为摄像头搜索软件_华为Mate 40 Pro评测:硬件和软件表现都近乎完美
  13. C#之DotfuscatorCommunity
  14. shouldband绑定数据的办法
  15. 使用GifCam软件录制gif动图
  16. python分层抽样_抽样方法—分层抽样
  17. python tokenize_model_AttributeError:“module”对象没有属性“tokenize”
  18. 威联通Docker安装为知笔记方法
  19. 微信小程序上传图片到服务器(java后台以及使用springmvc)
  20. paas平台_paas平台排名

热门文章

  1. Access Token机制简单介绍
  2. C语言——数组定义及用法
  3. 小程序获取用户微信步数
  4. 图床云存储项目课程随堂笔记
  5. 图片批量上传并限制图片大小
  6. 开启mongodb数据库命令行_MongoDB服务启动命令及DB创建
  7. 杨超越的经历故事性太强了,现实版的娱乐圈爽文
  8. Testin云测平台操作步骤
  9. constrain用法_constrain是什么意思_constrain的用法
  10. python 打印三角形