欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

详解有关Android截图与录屏功能的学习

程序员文章站 2022-10-24 17:19:12
简单的截屏和录屏功能。 因为mediaprojection是5.0以上才出现的,所以今天所讲述功能实现,只在5.0以上的系统有效。 截屏: 步骤如下: 1:...

简单的截屏和录屏功能。

因为mediaprojection是5.0以上才出现的,所以今天所讲述功能实现,只在5.0以上的系统有效。

截屏:

步骤如下:

1:获取mediaprojectionmanager

2:通过mediaprojectionmanager.createscreencaptureintent()获取intent

3:通过startactivityforresult传入intent然后在onactivityresult中通过mediaprojectionmanager.getmediaprojection(resultcode,data)获取mediaprojection

4:创建imagereader,构建virtualdisplay

5:最后就是通过imagereader截图,就可以从imagereader里获得image对象。

6:将image对象转换成bitmap

实现:

步骤已经给出了,我们就按照步骤来实现代码吧。

首先mediaprojectionmanager是系统服务,我们通过getsystemservice(media_projection_service)获取它

复制代码 代码如下:

projectionmanager = (mediaprojectionmanager) getsystemservice(media_projection_service);

然后调用startactivityforresult传入projectionmanager.createscreencaptureintent()创建的intent

复制代码 代码如下:

startactivityforresult(projectionmanager.createscreencaptureintent(),screen_shot);

紧接着我们就可以在onactivityresult(int requestcode, int resultcode, intent data)中通过resultcode和data来获取mediaprojection

  @override
  protected void onactivityresult(int requestcode, int resultcode, intent data) {
    if(requestcode == screen_shot){
      if(resultcode == result_ok){
        //获取mediaprojection
        mediaprojection = projectionmanager.getmediaprojection(requestcode,data);
      }
    }
  }

然后就是创建imagereader和virtualdisplay

    imagereader = imagereader.newinstance(width, height, pixelformat.rgba_8888, 1);
    if(imagereader!=null){
      log.d(tag, "imagereader successful");
    }
    mediaprojection.createvirtualdisplay("screenshout",
        width,height,dpi,
        displaymanager.virtual_display_flag_auto_mirror,
        imagereader.getsurface(),null,null);

这里我们依次讲解一下。

首先是imagereader.newinstance方法:

复制代码 代码如下:

public static imagereader newinstance(int width, int height, int format, int maximages)

方法里接收四个参数。
前两个width,height是用来指定生成图像的宽和高。

第三个参数format是图像的格式,这个格式必须是imageformatpixelformat中的一个,这两个format里有很多格式,大家可以点进去看看,我们例子中使用的是pixelformat.rgba_8888格式(需要注意的是并不是所有的格式都被imagereader支持,比如说imageformat.nv21)。

第四个参数是maximages,这个参数指的是你想同时在imagereader里获取到的image对象的个数,这个参数我不是很懂,我不理解同时的意思。我的理解是imagereader是一个类似数组的东西,然后我们可以通过acquirelatestimage()或acquirenextimage()方法来得到里面的image对象(可能有误,仅供参考)。这个值应该设置的越小越好,但是得大于0,所以我们上面设置的是1。

然后我们看看mediaprojection.createvirtualdisplay方法:

createvirtualdisplay(@nonnull string name,
      int width, int height, int dpi, int flags, @nullable surface surface,
      @nullable virtualdisplay.callback callback, @nullable handler handler)

首先这个方法返回的是virtualdisplay。

前四个不用说了,分别是virtualdisplay的名字,宽,高和dpi。

第五个参数,大家可以点 displaymanager查看所有的flags,我没有具体的研究过,在本次要实现的例子里,除了virtual_display_flag_secure这个会报错,其他的flags效果都一样。

第六个参数,是一个surface。我这里表达一下我的理解,当virtualdisplay被创建出来时,也就是createvirtualdisplay调用后,你在真实屏幕上的每一帧都会输入到surface参数里。也就是说,如果你放个surfaceview,然后传入surfaceview的surface那么你在屏幕上的操作都会显示在surfaceview里(这里我们后面录屏会讲)。我们这里传入的是imagereader的surface。这其中的逻辑我的理解是这样的,真实屏幕的每一帧都都会传给imagereader,根据imagereader的maximages参数,比如说maximages是2,那么imagereader始终保持两帧图片,但这两帧图片是一直随着真实屏幕的操作而更新的(不知道大家有没有听懂)。

第七个参数,是一个回调函数,在virtualdisplay状态改变时调用。因为我们这里没有,所以传null。

第八个参数,这里我给出原文:“the handler on which the callback should be invoked, or null if the callback should be invoked on the calling thread's main looper.”因为我翻译不好。不过和普通的handler使用场景类似。

现在我们imagereader和virtualdisplay,接下来我们就可以通过imagereader的acquirelatestimage()或acquirenextimage()来得到image对象了。

systemclock.sleep(1000);
image image = imagereader.acquirenextimage();

这里有个坑,就是你在获取image的时候,得先暂停1秒左右,不然就会获取失败(原因未知)。

现在我们有了image对象,但是image对象并不能直接作为ui资源被使用,我们可以将它转换成bitmap对象。

    int width = image.getwidth();
    int height = image.getheight();
    final image.plane[] planes = image.getplanes();
    final bytebuffer buffer = planes[0].getbuffer();
    int pixelstride = planes[0].getpixelstride();
    int rowstride = planes[0].getrowstride();
    int rowpadding = rowstride - pixelstride * width;
    bitmap = bitmap.createbitmap(width + rowpadding / pixelstride, height, bitmap.config.argb_8888);
    bitmap.copypixelsfrombuffer(buffer);
    image.close();

这里最主要的逻辑就是像素与字节的转换,我们需要将image对象的字节流写进bitmap里,但是bitmap接收的是像素格式的。
我们一行一行来看:

首先获取image对象的宽和高,注意width和height是像素格式的。

然后获取bytebuffer,里面存放的就是图片的字节流,是字节格式的。我是这么理解的,bytebuffer里面是一长串的字节序列,按照某种格式分成行列就变成了图片。

然后获取pixelstride,这指的是两个像素的距离(就是一个像素头部到相邻像素的头部),这是字节格式的。

rowstride是一行占用的距离(就是一行像素头部到相邻行像素的头部),这个大小和width有关,这里需要注意,因为内存对齐的原因,所以每行会有一些空余。这个值也是字节格式的。

紧接着我们需要创建一个bitmap用来接受image的buffer的输入,buffer是字节流,它会按照我们设置的format转换成像素,所以这里最重要的一个地方就是bitmap创建的大小,因为高度就是行数所以就是height,但是宽度因为上面说的内存对齐问题会有些空余,所以我们要先求出空余部分,然后加上width。

int rowpadding = rowstride - pixelstride * width;

这句话用整行的距离减去了一行里像素及空隙占用的距离,剩下的就是空余部分。但是这个是字节格式的。我们将它除以pixelstride,也就是一个像素及空隙占用的字节大小,就转换成了像素格式。
然后:

width + rowpadding / pixelstride

这个就是一行里像素的占用了,我们将它传给bitmap:

复制代码 代码如下:

bitmap = bitmap.createbitmap(width + rowpadding / pixelstride, height, bitmap.config.argb_8888);

创建出合适大小的bitmap,然后把image的buffer传给它,就成功的将image对象转换成了bitmap。

这里我可能讲的不清楚,我给大家画了张图:

详解有关Android截图与录屏功能的学习

上面的一小格一小格是一块块像素。

好了,现在我们已经获取到了bitmap了,我们可以把它放到imageview里显示一下,我写了一个例子,效果如下:

详解有关Android截图与录屏功能的学习

点击按钮,弹出一个对话框请求截屏,点击立即开始的话,截屏就会显示在下面的imageview里。

截屏就这样,我已经尽力了,╮(╯▽╰)╭

录屏:

步骤:

录屏的前三步和截屏是一样的,出现分歧点的地方在于virtualdisplay创建时传入的surface,还记得我们上面说的吗,说在创建virtualdisplay的时候,传入一个surfaceview的surface的话,那么你在真实屏幕上的操作,都会重现在surfaceview上。我们来试一下:

mediaprojection.createvirtualdisplay("screenshout",
        width,height,dpi,
        displaymanager.virtual_display_flag_auto_mirror,
        surfaceview.getholder().getsurface(),null,null);

我们在surface参数中传入一个surfaceview的surface

效果如下:

详解有关Android截图与录屏功能的学习

可以看到我们放了一个button,放了一个imageview,放了一个surfaceview。

点击button,然后点立即开始之后,真实屏幕就映射到了surfaceview里。

所以当创建virtualdisplay时,真实屏幕就映射到了surface,也就是我们可以再surface里拿到屏幕的一个输入。那我们要录屏的话,就只要把surface转换成我们需要的格式就行了,在本篇文章的例子中,我们会将surface对象转换成mp4格式。这就需要用到mediacodec类和mediamuxer类。mediacodec生成一个surface用来接收屏幕的输出并按照格式编码,然后传给mediamuxer用来封装成mp4格式的视频。

    //第一个参数是mime类型,我们传入video/avc
    //第二第三个参数是宽和高
    mediaformat format = mediaformat.createvideoformat("video/avc", width, height);
    //color_formatsurface这里表明数据将是一个graphicbuffer元数据
    format.setinteger(mediaformat.key_color_format,
        mediacodecinfo.codeccapabilities.color_formatsurface);
    //设置码率,码率越大视频越清晰,相对的占用内存也要更大
    format.setinteger(mediaformat.key_bit_rate, 6000000);
    //设置帧数
    format.setinteger(mediaformat.key_frame_rate, 30);
    //设置两个关键帧的间隔,这个值你设置成多少对我们这个例子都没啥影响
    //这个值做视频的朋友可能会懂,反正我不是很懂,大概就是你预览的时候,比如你设置为10,那么你10秒内的预览图都是同一张
    format.setinteger(mediaformat.key_i_frame_interval, 10);
    //创建一个mediacodec实例
    mediacodec = mediacodec.createencoderbytype("video/avc");
    //第一个参数将我们上面设置的format传进去
    //第二个参数是surface,如果我们需要读取mediacodec编码后的数据就要传,但我们这里不需要所以传null
    //第三个参数关于加解密的,我们不需要,传null
    //第四个参数是一个确定的标志位,也就是我们现在传的这个
    mediacodec.configure(format, null, null, mediacodec.configure_flag_encode);
    //获取mediacodec的surface,这个surface其实就是一个入口,屏幕作为输入源就会进入这个入口,然后交给mediacodec编码
    surface = mediacodec.createinputsurface();
    mediacodec.start();

上面讲了mediacodec的创建,我们也可以从中看到屏幕数据是怎么进入mediacodec的。具体的我已经注释了。

接下来我们创建一个mediamuxer对象:

//第一个参数是输出的地址
//第二个参数是输出的格式,我们设置的是mp4格式
mediamuxer = new mediamuxer(filepath, mediamuxer.outputformat.muxer_output_mpeg_4);

然后创建virtualdisplay,把mediacodec的surface传进去:

virtualdisplay = mediaprojection.createvirtualdisplay(tag + "-display",
              width, height, dpi, displaymanager.virtual_display_flag_public,
              surface, null, null);

最后就是视频的编码与转换mp4还有保存了:

  private void recordvirtualdisplay() {
    while (!mquit.get()) {
      //dequeueoutputbuffer方法你可以这么理解,它会出列一个输出buffer(你可以理解为一帧画面),返回值是这一帧画面的顺序位置(类似于数组的下标)
      //第二个参数是超时时间,如果超过这个时间了还没成功出列,那么就会跳过这一帧,去出列下一帧,并返回info_try_again_later标志位
      int index = mediacodec.dequeueoutputbuffer(bufferinfo, 10000);
      //当格式改变的时候吗,我们需要重新设置格式
      //在本例中,只第一次开始的时候会返回这个值
      if (index == mediacodec.info_output_format_changed) {
        resetoutputformat();

      } else if (index >= 0) {//这里说明dequeueoutputbuffer执行正常
        //这里执行我们转换成mp4的逻辑
        encodetovideotrack(index);
        mediacodec.releaseoutputbuffer(index, false);
      }
    }
  }
  //这里是将数据传给mediamuxer,将其转换成mp4
  private void encodetovideotrack(int index) {
    //通过index获取到bytebuffer(可以理解为一帧)
    bytebuffer encodeddata = mediacodec.getoutputbuffer(index);
    //当bufferinfo返回这个标志位时,就说明已经传完数据了,我们将bufferinfo.size设为0,准备将其回收
    if ((bufferinfo.flags & mediacodec.buffer_flag_codec_config) != 0) {
      bufferinfo.size = 0;
    }
    if (bufferinfo.size == 0) {
      encodeddata = null;
    } 
    if (encodeddata != null) {
      encodeddata.position(bufferinfo.offset);//设置我们该从哪个位置读取数据
      encodeddata.limit(bufferinfo.offset + bufferinfo.size);//设置我们该读多少数据
      //这里将数据写入
      //第一个参数是每一帧画面要放置的顺序
      //第二个是要写入的数据
      //第三个参数是bufferinfo,这个数据包含的是encodeddata的offset和size
      mediamuxer.writesampledata(videotrackindex, encodeddata, bufferinfo);

    }
  }

  //这个方法其实就是设置mediamuxer的format
  private void resetoutputformat() {
    //将mediacodec的format设置给mediamuxer
    mediaformat newformat = mediacodec.getoutputformat();
    //获取videotrackindex,这个值是每一帧画面要放置的顺序
    videotrackindex = mediamuxer.addtrack(newformat);
    mediamuxer.start();
    muxerstarted = true;
  }

好了,录屏到此结束了。

我们来看下实例演示:

详解有关Android截图与录屏功能的学习

总结:

这篇博客写的真是费时费力,果然水平未到就不该强行写文。

我不知道我是不是写清楚了,但还是希望大家看了之后能有一丝丝的收获,这就是对我最大的鼓励。

本篇博客的录屏代码参考自:screenrecorder

本篇博客的实例代码:

github项目地址:https://github.com/chentiansaber/screenrecordershoter

源码下载地址:screenrecordershoter_jb51.rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。