Android Создание видео из скрэпинга экрана: почему выходное изображение выигрышно? - PullRequest
0 голосов
/ 05 февраля 2019

Обновление № 6 Обнаружено, что я неправильно обращался к значениям RGB.Я предположил, что я получаю доступ к данным из Int [], но вместо этого получаю доступ к байтовой информации из Byte [].Изменен доступ к Int [] и получено следующее изображение:

enter image description here


Обновление № 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 Для справки приведено исходное изображение для сравнения.

enter image description here


Update # 3 Это выходное изображение после чередования всех данных U / V в один массив и передачи его объекту Image в inputImagePlanes[1]; inputImagePlanes[2]; не используется;enter image description here

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


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

enter image description here


Обновление # 1 Как следует из комментария, здесь приведены шаги строки и пикселя Iполучить от вызовов 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 Кодирование данных! IВ настоящее время есть два разных способа сделать это, и у каждого свой вывод.Один использует 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) enter image description here

Кодирование с использованием 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) enter image description here

Шаг 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 Ответ

0 голосов
/ 05 февраля 2019

Если вы 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.

...