博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
android视频播放截图并制作成gif图片
阅读量:3976 次
发布时间:2019-05-24

本文共 10938 字,大约阅读时间需要 36 分钟。

    导言:

    根据文章标题,按三步走,一、视频播放;二、连续截图;三、转换成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的初始化并开始播放:

    @Override    public 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() {             @Override             public void onPrepared(MediaPlayer mediaPlayer) {                 mediaPlayer.start();             }         });    }

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

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

    二、截图

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

    三、转换成gif

    这里用到了一个第三方开源项目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
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() { @Override public 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() { @Override public void run() { Toast.makeText(TextureVideoActivity.this, "gif初始化成功,准备合并", Toast.LENGTH_SHORT).show(); } }); SequenceInputStream sequenceInputStream = null; FileOutputStream fos = null; try { Vector
streams = new Vector
(); 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() { @Override public 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左右也就够了。

上效果图:

20180318184707805uploading.4e448015.gif转存失败

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

angry.gifuploading.4e448015.gif转存失败angry.gifuploading.4e448015.gif转存失败angry.gifuploading.4e448015.gif转存失败发火直接由本地传的图片居然不动,有知道怎么传的请告诉我!!!

最后,上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可以获取到视频源的宽高和旋转角度。手机拍摄的视频,不论是横着拍的还是竖着拍的,视频源的宽高都是默认横屏拍的宽高,所以必须要用到旋转角度进行判断。代码已更新到上。

 
 
 

 

    

 

 

 

 

你可能感兴趣的文章
jboss java.lang.NoClassDefFoundError: Could not initialize class com.documentum.fc.client.DfClient
查看>>
芯片常见封装
查看>>
什么是oc门
查看>>
上拉电阻&nbsp;下拉电阻的汇总
查看>>
NTC热敏电阻的基本特性
查看>>
数字地和模拟地处理的基本原则
查看>>
集电极开路,漏极开路,推挽,上拉电…
查看>>
长尾式差分放大电路2
查看>>
十种精密整流电路
查看>>
红外线遥控原理
查看>>
放大电路的主要性能指标?
查看>>
稳压、调压、监控、DC/DC电路大全
查看>>
放大电路的主要性能指标?
查看>>
运放电压和电流负反馈的讨论
查看>>
运放自激问题
查看>>
运放电压和电流负反馈的讨论
查看>>
终于&nbsp;整明白了中断的工作原…
查看>>
终于&nbsp;整明白了中断的工作原…
查看>>
终于&nbsp;整明白了中断的工作原…
查看>>
终于&nbsp;整明白了中断的工作原…
查看>>