Android通过屏幕抓取创建视频:为什么输出图像会闪烁?

时间:2019-02-05 13:14:00

标签: android mediacodec color-space mediamuxer

更新#6 ,发现我未正确访问RGB值。我以为我是从Int []访问数据,而是从Byte []访问字节信息。更改为从Int []访问并获得以下图像:

enter image description here


更新#5 添加用于获取RGBA ByteBuffer供参考的代码

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="zebralist">
<li class="resetZebra">Example</li>
<li>Example</li>
<li>Example</li>
<li>Example</li>
<li class="resetZebra">Example</li>
<li>Example</li>
<li>Example</li>
<li class="resetZebra">Example</li>
<li>Example</li>
<li>Example</li>
</ul>

更新#4 作为参考,这是要与之进行比较的原始图像。

enter image description here


更新#3 :这是将所有U / V数据交织到单个数组中并将其传递到 private void screenScrape() { Log.d(TAG, "In screenScrape"); //read pixels from frame buffer into PBO (GL_PIXEL_PACK_BUFFER) mSurface.queueEvent(new Runnable() { @Override public void run() { Log.d(TAG, "In Screen Scrape 1"); //generate and bind buffer ID GLES30.glGenBuffers(1, pboIds); checkGlError("Gen Buffers"); GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pboIds.get(0)); checkGlError("Bind Buffers"); //creates and initializes data store for PBO. Any pre-existing data store is deleted GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, (mWidth * mHeight * 4), null, GLES30.GL_STATIC_READ); checkGlError("Buffer Data"); //glReadPixelsPBO(0,0,w,h,GLES30.GL_RGB,GLES30.GL_UNSIGNED_SHORT_5_6_5,0); glReadPixelsPBO(0, 0, mWidth, mHeight, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, 0); checkGlError("Read Pixels"); //GLES30.glReadPixels(0,0,w,h,GLES30.GL_RGBA,GLES30.GL_UNSIGNED_BYTE,intBuffer); } }); //map PBO data into client address space mSurface.queueEvent(new Runnable() { @Override public void run() { Log.d(TAG, "In Screen Scrape 2"); //read pixels from PBO into a byte buffer for processing. Unmap buffer for use in next pass mapBuffer = ((ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 4 * mWidth * mHeight, GLES30.GL_MAP_READ_BIT)).order(ByteOrder.nativeOrder()); checkGlError("Map Buffer"); GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER); checkGlError("Unmap Buffer"); isByteBufferEmpty(mapBuffer, "MAP BUFFER"); convertColorSpaceByteArray(mapBuffer); mapBuffer.clear(); } }); } inputImagePlanes[1];处的Image对象之后的输出图像; enter image description here

下一张图像是相同的交错UV数据,但是我们将其加载到inputImagePlanes[2];中而不是inputImagePlanes[2];enter image description here


更新#2 这是在U / V缓冲区的“真实”数据的每个字节之间填充零后的输出图像。 inputImagePlanes[1];

enter image description here


更新#1 如评论所建议,这是我通过调用uArray[uvByteIndex] = (byte) 0;getPixelStride

获得的行数和像素步幅
getRowStride

我的应用程序的目标是从屏幕上读取像素,对其进行压缩,然后通过WiFi发送该h264流以用作接收器。

当前,我正在使用MediaMuxer类将原始h264流转换为MP4,然后将其保存到文件中。 但是最终结果视频搞砸了,我不知道为什么。让我们逐步进行一些处理,看看是否可以找到任何可以跳出的内容。

步骤1 设置编码器。 我目前每2秒拍摄一次屏幕图像,并使用“视频/视频”用于MIME_TYPE

Y Plane Pixel Stride = 1, Row Stride = 960
U Plane Pixel Stride = 2, Row Stride = 960
V Plane Pixel Stride = 2, Row Stride = 960

步骤2 从屏幕上读取像素。 这是使用openGL ES完成的,并且像素以RGBA格式读取。 (我已经确认这部分工作正常)

第3步 将RGBA像素转换为YUV420(IYUV)格式。 可使用以下方法完成。请注意,该方法的末尾有2种编码方法。

        //create codec for compression
        try {
            mCodec = MediaCodec.createEncoderByType(MIME_TYPE);
        } catch (IOException e) {
            Log.d(TAG, "FAILED: Initializing Media Codec");
        }

        //set up format for codec
        MediaFormat mFormat = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);

        mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
        mFormat.setInteger(MediaFormat.KEY_BIT_RATE, 16000000);
        mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 1/2);
        mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);

第4步 编码数据! 目前,我有两种不同的方法,每种方法都有不同的输出。一个使用 private void convertColorSpaceByteArray(ByteBuffer rgbBuffer) { long startTime = System.currentTimeMillis(); Log.d(TAG, "In convertColorspace"); final int frameSize = mWidth * mHeight; final int chromaSize = frameSize / 4; byte[] rgbByteArray = new byte[rgbBuffer.remaining()]; rgbBuffer.get(rgbByteArray); byte[] yuvByteArray = new byte[inputBufferSize]; Log.d(TAG, "Input Buffer size = " + inputBufferSize); byte[] yArray = new byte[frameSize]; byte[] uArray = new byte[(frameSize / 4)]; byte[] vArray = new byte[(frameSize / 4)]; isByteBufferEmpty(rgbBuffer, "RGB BUFFER"); int yIndex = 0; int uIndex = frameSize; int vIndex = frameSize + chromaSize; int yByteIndex = 0; int uvByteIndex = 0; int R, G, B, Y, U, V; int index = 0; //this loop controls the rows for (int i = 0; i < mHeight; i++) { //this loop controls the columns for (int j = 0; j < mWidth; j++) { R = (rgbByteArray[index] & 0xff0000) >> 16; G = (rgbByteArray[index] & 0xff00) >> 8; B = (rgbByteArray[index] & 0xff); Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16; U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128; V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128; //clamp and load in the Y data yuvByteArray[yIndex++] = (byte) ((Y < 16) ? 16 : ((Y > 235) ? 235 : Y)); yArray[yByteIndex] = (byte) ((Y < 16) ? 16 : ((Y > 235) ? 235 : Y)); yByteIndex++; if (i % 2 == 0 && index % 2 == 0) { //clamp and load in the U & V data yuvByteArray[uIndex++] = (byte) ((U < 16) ? 16 : ((U > 239) ? 239 : U)); yuvByteArray[vIndex++] = (byte) ((V < 16) ? 16 : ((V > 239) ? 239 : V)); uArray[uvByteIndex] = (byte) ((U < 16) ? 16 : ((U > 239) ? 239 : U)); vArray[uvByteIndex] = (byte) ((V < 16) ? 16 : ((V > 239) ? 239 : V)); uvByteIndex++; } index++; } } encodeVideoFromImage(yArray, uArray, vArray); encodeVideoFromBuffer(yuvByteArray); } 返回的ByteBuffer,另一个使用MediaCodec.getInputBuffer();返回的Image

使用MediaCodec.getInputImage();

进行编码
ByteBuffer

以及相关的输出图像(注意:我拍摄了MP4的屏幕截图) enter image description here

使用 private void encodeVideoFromBuffer(byte[] yuvData) { Log.d(TAG, "In encodeVideo"); int inputSize = 0; //create index for input buffer inputBufferIndex = mCodec.dequeueInputBuffer(0); //create the input buffer for submission to encoder ByteBuffer inputBuffer = mCodec.getInputBuffer(inputBufferIndex); //clear, then copy yuv buffer into the input buffer inputBuffer.clear(); inputBuffer.put(yuvData); //flip buffer before reading data out of it inputBuffer.flip(); mCodec.queueInputBuffer(inputBufferIndex, 0, inputBuffer.remaining(), presentationTime, 0); presentationTime += MICROSECONDS_BETWEEN_FRAMES; sendToWifi(); }

进行编码
Image

以及相关的输出图像(注意:我拍摄了MP4的屏幕截图) enter image description here

步骤5 将H264流转换为MP4。 最后,我从编解码器中获取输出缓冲区,并使用 private void encodeVideoFromImage(byte[] yToEncode, byte[] uToEncode, byte[]vToEncode) { Log.d(TAG, "In encodeVideo"); int inputSize = 0; //create index for input buffer inputBufferIndex = mCodec.dequeueInputBuffer(0); //create the input buffer for submission to encoder Image inputImage = mCodec.getInputImage(inputBufferIndex); Image.Plane[] inputImagePlanes = inputImage.getPlanes(); ByteBuffer yPlaneBuffer = inputImagePlanes[0].getBuffer(); ByteBuffer uPlaneBuffer = inputImagePlanes[1].getBuffer(); ByteBuffer vPlaneBuffer = inputImagePlanes[2].getBuffer(); yPlaneBuffer.put(yToEncode); uPlaneBuffer.put(uToEncode); vPlaneBuffer.put(vToEncode); yPlaneBuffer.flip(); uPlaneBuffer.flip(); vPlaneBuffer.flip(); mCodec.queueInputBuffer(inputBufferIndex, 0, inputBufferSize, presentationTime, 0); presentationTime += MICROSECONDS_BETWEEN_FRAMES; sendToWifi(); } 将原始的h264流转换为我可以播放并测试正确性的MP4

MediaMuxer

如果到目前为止,您是绅士(或女士!)和学者!基本上,我为为什么我的图像不能正确显示而感到困惑。我是视频处理的新手,所以我确定我只是缺少一些东西。

1 个答案:

答案 0 :(得分:2)

如果您使用的是API 19+,则最好还是使用#2编码方法getImage()/encodeVideoFromImage(),因为它更现代。

关注该方法:一个问题是,您的图像格式异常。使用COLOR_FormatYUV420Flexible,您将知道将具有8位U和V分量,但是您不会事先知道它们的去向。这就是为什么您必须查询Image.Plane格式。每个设备上可能都不同。

在这种情况下,UV格式被交错了(在Android设备上非常常见)。如果您使用的是Java,并且分别提供每个数组(U / V),并请求了“跨度”(每个样本之间的“空格”字节),我相信一个数组最终会破坏另一个数组,因为这些实际上是“直接”的ByteBuffer,它们打算从本机代码中使用,例如this answer。我解释的解决方案是将交错的数组复制到第三(V)平面,而忽略U平面。在本机方面,这两个平面实际上在内存中彼此重叠(除了第一个和最后一个字节),因此填充一个会导致实现同时填充这两个平面。

如果改用第二个(U)平面,则会发现一切正常,但颜色看起来很有趣。这也是由于这两个平面的重叠布置。实际上,有效的做法是将每个数组元素移动一个字节(这会使U出现在V所在的位置,反之亦然。)

...换句话说,此解决方案实际上有点破烂。正确执行此操作并使它在所有设备上都能正常工作的唯一方法可能是使用本机代码(如我上面链接的答案所示)。

颜色平面问题一旦解决,将留下所有有趣的重叠文本和垂直条纹。这些实际上是由于您对RGB数据的解释而导致的,跨度有误。

而且,一旦修复该问题,您将获得一个看起来不错的图片。它是垂直镜像的;我不知道其根本原因,但我怀疑这是OpenGL问题。