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.