Android Создание видео из скрэпинга экрана: почему выходное изображение выигрышно?

Обновление № 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 в один массив и передачи его объекту Image в inputImagePlanes[1];inputImagePlanes[2]; не используется;

Следующее изображение - это те же чередующиеся УФ данные, но мы загружаем их в inputImagePlanes[2]; вместо inputImagePlanes[1];


Обновление № 2 Это выходное изображение после заполнения буферов U / V нулем между каждым байтом "реальных" данных. uArray[uvByteIndex] = (byte) 0;


Обновление № 1 Как следует из комментария, здесь приведены шаги в строках и пикселях, которые я получаю от вызова getPixelStride а также getRowStride

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

Цель моего приложения - считывать пиксели с экрана, сжимать их, а затем отправлять этот поток h264 через WiFi для воспроизведения в качестве приемника.

В настоящее время я использую класс MediaMuxer для преобразования необработанного потока 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 Закодируйте данные! В настоящее время у меня есть два разных способа сделать это, и у каждого свой вывод. Один использует ByteBuffer вернулся из MediaCodec.getInputBuffer(); другой использует Image вернулся из MediaCodec.getInputImage();

Кодирование с использованием 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++;
}

Если вы сделали это так далеко, вы джентльмен (или леди!) И ученый! По сути, я озадачен тем, почему мое изображение не выходит должным образом. Я относительно новичок в обработке видео, поэтому я уверен, что мне чего-то не хватает.

1 ответ

Решение

Если вы API 19+, можете также придерживаться метода кодирования № 2, getImage()/encodeVideoFromImage(), так как это более современно.

Сосредоточив внимание на этом методе: одна проблема заключалась в том, что у вас был неожиданный формат изображения. С COLOR_FormatYUV420Flexibleвы знаете, что у вас будут 8-битные компоненты U и V, но вы не будете заранее знать, куда они идут. Вот почему вы должны запросить Image.Plane форматы. Может быть по-разному на каждом устройстве.

В этом случае формат UV оказался чередующимся (очень распространенным на устройствах Android). Если вы используете Java и поставляете каждый массив (U/V) отдельно, с запрошенным "шагом" (байт "spacer" между каждым образцом), я полагаю, что один массив заканчивает слипанием другого, потому что это на самом деле "прямые" ByteBuffers, и они были предназначены для использования из нативного кода, как в этом ответе. Решение, которое я объяснил, состояло в том, чтобы скопировать перемеженный массив в третью (V) плоскость и игнорировать плоскость U. На нативной стороне эти две плоскости фактически перекрывают друг друга в памяти (за исключением первого и последнего байта), поэтому заполнение одной приводит к тому, что реализация заполняет обе.

Если вы используете вторую (U) плоскость вместо этого, вы найдете, что все работает, но цвета выглядят забавно. Это также из-за наложения этих двух плоскостей; то, что это делает, по сути, сдвигает каждый элемент массива на один байт (что помещает U в то место, где должно быть V, и наоборот.)

... Другими словами, это решение на самом деле немного взломать. Вероятно, единственный способ сделать это правильно и заставить его работать на всех устройствах, это использовать нативный код (как в ответе, который я привел выше).

Как только проблема с цветовой плоскостью устранена, остается смешной текст и вертикальные полоски. Это было на самом деле вызвано вашей интерпретацией данных RGB, которые имели неверный шаг.

И, как только это будет исправлено, вы получите прилично выглядящую картинку. Это было отражено вертикально; Я не знаю причину этого, но я подозреваю, что это проблема OpenGL.

Другие вопросы по тегам