Android 从屏幕抓取创建视频:为什么输出图像不稳定?

Android Creating Video from Screen Scraping: Why is output Image wonky?

更新 #6 发现我无法正确访问 RGB 值。我假设我是从 Int[] 访问数据,但实际上是从 Byte[] 访问字节信息。更改为从 Int[] 访问并得到以下图像:


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

 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();
        }
    });
}

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


更新 #3 这是将所有 U/V 数据交织成一个数组并将其传递给 inputImagePlanes[1]; 处的图像对象后的输出图像 inputImagePlanes[2];未使用;

下一个图像是相同的交错 UV 数据,但我们将其加载到 inputImagePlanes[2]; 而不是 inputImagePlanes[1];


更新 #2 这是在 'real' 数据的每个字节之间用零填充 U/V 缓冲区后的输出图像。 uArray[uvByteIndex] = (byte) 0;


更新 #1 正如评论所建议的,这是我通过调用 getPixelStridegetRowStride[=39= 获得的行和像素步幅]

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

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

目前我正在使用 MediaMuxer class 将原始 h264 流转换为 MP4,然后将其保存到文件中。 然而最终的结果视频是乱七八糟的,我不知道为什么。让我们来看看一些处理,看看我们是否能找到任何跳出来的东西。

步骤 1设置编码器。我目前正在拍摄一次屏幕图像每 2 秒,并使用 "video/avc" 作为 MIME_TYPE

        //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);

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

步骤3 将RGBA像素转换为YUV420(IYUV)格式。这是使用以下方法完成。请注意,我在此方法的末尾调用了 2 种编码方法。

 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);
}

第 4 步 编码数据! 我目前有两种不同的方法来做这个,并且每个都有不同的输出。一个使用从 MediaCodec.getInputBuffer(); 返回的 ByteBuffer,另一个使用从 MediaCodec.getInputImage();

返回的 Image

编码使用ByteBuffer

 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();
}

以及关联的输出图片(注:我截图的是MP4)

编码使用Image

 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();
}

以及关联的输出图片(注:我截图的是MP4)

步骤 5 将 H264 流转换为 MP4。 最后我从中获取输出缓冲区编解码器,并使用 MediaMuxer 将原始 h264 流转换为我可以播放并测试正确性的 MP4

 private void sendToWifi() {
    Log.d(TAG, "In sendToWifi");

    MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();

    //Check to see if encoder has output before proceeding
    boolean waitingForOutput = true;
    boolean outputHasChanged = false;
    int outputBufferIndex = 0;

    while (waitingForOutput) {
        //access the output buffer from the codec
        outputBufferIndex = mCodec.dequeueOutputBuffer(mBufferInfo, -1);

        if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            outputFormat = mCodec.getOutputFormat();
            outputHasChanged = true;
            Log.d(TAG, "OUTPUT FORMAT HAS CHANGED");
        }

        if (outputBufferIndex >= 0) {
            waitingForOutput = false;
        }
    }

    //this buffer now contains the compressed YUV data, ready to be sent over WiFi
    ByteBuffer outputBuffer = mCodec.getOutputBuffer(outputBufferIndex);

    //adjust output buffer position and limit.  As of API 19, this is not automatic
    if(mBufferInfo.size != 0) {
        outputBuffer.position(mBufferInfo.offset);
        outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
    }


    ////////////////////////////////FOR DEGBUG/////////////////////////////
    if (muxerNotStarted && outputHasChanged) {
        //set up track
        mTrackIndex = mMuxer.addTrack(outputFormat);

        mMuxer.start();
        muxerNotStarted = false;
    }

    if (!muxerNotStarted) {
        mMuxer.writeSampleData(mTrackIndex, outputBuffer, mBufferInfo);
    }
    ////////////////////////////END DEBUG//////////////////////////////////

    //release the buffer
    mCodec.releaseOutputBuffer(outputBufferIndex, false);
    muxerPasses++;
}

如果你做到了这一步,你就是一位绅士(或女士!)和一位学者!基本上我很困惑为什么我的图像不能正常显示。我对视频处理比较陌生,所以我确定我只是遗漏了一些东西。

如果您 API 19 岁以上,不妨坚持使用编码方法 #2,getImage()/encodeVideoFromImage(),因为后者更现代。

关注那个方法:一个问题是,你有一个意想不到的图像格式。使用 COLOR_FormatYUV420Flexible,您知道您将拥有 8 位 U 和 V 组件,但您不会事先知道它们去哪里。这就是您必须查询 Image.Plane 格式的原因。每个设备上可能不同。

在这种情况下,UV 格式被证明是交错的(在 Android 设备上很常见)。如果您使用 Java,并且您分别提供每个数组 (U/V),并请求 "stride"(每个样本之间的 "spacer" 字节),我相信一个array 最终会破坏另一个,因为它们实际上是 "direct" ByteBuffers,并且它们旨在从本机代码中使用,例如 。我解释的解决方案是将交错数组复制到第三 (V) 平面,而忽略 U 平面。在本机方面,这两个平面实际上 在内存中相互重叠(第一个和最后一个字节除外),因此填充一个会导致实现同时填充两个。

如果您改用第二个 (U) 平面,您会发现一切正常,但颜色看起来很有趣。那也是因为这两个平面的重叠排列;实际上,它的作用是将每个数组元素移动一个字节(将 U 放在 V 应该在的位置,反之亦然。)

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

一旦解决了颜色平面问题,就会留下所有有趣的重叠文本和垂直条纹。这些实际上是您对RGB数据的解释造成的,步幅错误。

而且,一旦这个问题得到解决,您就有了一张看起来不错的照片。它是垂直镜像的;我不知道根本原因,但我怀疑这是一个 OpenGL 问题。