From 21bde57f0099fed5cca78d9357571dc015a63227 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Tue, 25 Feb 2014 18:29:06 -0800 Subject: Add frame dump output This adds the ability to dump frames as RGB data with a minimal frame header. Only recommended for devices with small displays. Enable with "--output-format=frames". The "--raw" option is now selected with "--output-format=h264". Change-Id: I18d3d4a87cd056d7acf0658985a90dc5895dbfb6 --- cmds/screenrecord/Android.mk | 1 + cmds/screenrecord/EglWindow.cpp | 68 +++++++++--- cmds/screenrecord/EglWindow.h | 5 +- cmds/screenrecord/FrameOutput.cpp | 208 +++++++++++++++++++++++++++++++++++++ cmds/screenrecord/FrameOutput.h | 101 ++++++++++++++++++ cmds/screenrecord/Program.cpp | 12 ++- cmds/screenrecord/Program.h | 8 +- cmds/screenrecord/screenrecord.cpp | 196 ++++++++++++++++++++++------------ 8 files changed, 512 insertions(+), 87 deletions(-) create mode 100644 cmds/screenrecord/FrameOutput.cpp create mode 100644 cmds/screenrecord/FrameOutput.h (limited to 'cmds') diff --git a/cmds/screenrecord/Android.mk b/cmds/screenrecord/Android.mk index d77fdb6..6747e60 100644 --- a/cmds/screenrecord/Android.mk +++ b/cmds/screenrecord/Android.mk @@ -19,6 +19,7 @@ include $(CLEAR_VARS) LOCAL_SRC_FILES := \ screenrecord.cpp \ EglWindow.cpp \ + FrameOutput.cpp \ TextRenderer.cpp \ Overlay.cpp \ Program.cpp diff --git a/cmds/screenrecord/EglWindow.cpp b/cmds/screenrecord/EglWindow.cpp index aa0517f..c16f2ad 100644 --- a/cmds/screenrecord/EglWindow.cpp +++ b/cmds/screenrecord/EglWindow.cpp @@ -35,11 +35,16 @@ using namespace android; status_t EglWindow::createWindow(const sp& surface) { - status_t err = eglSetupContext(); + if (mEglSurface != EGL_NO_SURFACE) { + ALOGE("surface already created"); + return UNKNOWN_ERROR; + } + status_t err = eglSetupContext(false); if (err != NO_ERROR) { return err; } + // Cache the current dimensions. We're not expecting these to change. surface->query(NATIVE_WINDOW_WIDTH, &mWidth); surface->query(NATIVE_WINDOW_HEIGHT, &mHeight); @@ -56,6 +61,34 @@ status_t EglWindow::createWindow(const sp& surface) { return NO_ERROR; } +status_t EglWindow::createPbuffer(int width, int height) { + if (mEglSurface != EGL_NO_SURFACE) { + ALOGE("surface already created"); + return UNKNOWN_ERROR; + } + status_t err = eglSetupContext(true); + if (err != NO_ERROR) { + return err; + } + + mWidth = width; + mHeight = height; + + EGLint pbufferAttribs[] = { + EGL_WIDTH, width, + EGL_HEIGHT, height, + EGL_NONE + }; + mEglSurface = eglCreatePbufferSurface(mEglDisplay, mEglConfig, pbufferAttribs); + if (mEglSurface == EGL_NO_SURFACE) { + ALOGE("eglCreatePbufferSurface error: %#x", eglGetError()); + eglRelease(); + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + status_t EglWindow::makeCurrent() const { if (!eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { ALOGE("eglMakeCurrent failed: %#x", eglGetError()); @@ -64,7 +97,7 @@ status_t EglWindow::makeCurrent() const { return NO_ERROR; } -status_t EglWindow::eglSetupContext() { +status_t EglWindow::eglSetupContext(bool forPbuffer) { EGLBoolean result; mEglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); @@ -82,17 +115,28 @@ status_t EglWindow::eglSetupContext() { ALOGV("Initialized EGL v%d.%d", majorVersion, minorVersion); EGLint numConfigs = 0; - EGLint configAttribs[] = { - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_RECORDABLE_ANDROID, 1, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_NONE + EGLint windowConfigAttribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RECORDABLE_ANDROID, 1, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + // no alpha + EGL_NONE + }; + EGLint pbufferConfigAttribs[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE }; - result = eglChooseConfig(mEglDisplay, configAttribs, &mEglConfig, 1, - &numConfigs); + result = eglChooseConfig(mEglDisplay, + forPbuffer ? pbufferConfigAttribs : windowConfigAttribs, + &mEglConfig, 1, &numConfigs); if (result != EGL_TRUE) { ALOGE("eglChooseConfig error: %#x", eglGetError()); return UNKNOWN_ERROR; diff --git a/cmds/screenrecord/EglWindow.h b/cmds/screenrecord/EglWindow.h index 02a2efc..69d0c31 100644 --- a/cmds/screenrecord/EglWindow.h +++ b/cmds/screenrecord/EglWindow.h @@ -44,6 +44,9 @@ public: // Creates an EGL window for the supplied surface. status_t createWindow(const sp& surface); + // Creates an EGL pbuffer surface. + status_t createPbuffer(int width, int height); + // Return width and height values (obtained from IGBP). int getWidth() const { return mWidth; } int getHeight() const { return mHeight; } @@ -65,7 +68,7 @@ private: EglWindow& operator=(const EglWindow&); // Init display, create config and context. - status_t eglSetupContext(); + status_t eglSetupContext(bool forPbuffer); void eglRelease(); // Basic EGL goodies. diff --git a/cmds/screenrecord/FrameOutput.cpp b/cmds/screenrecord/FrameOutput.cpp new file mode 100644 index 0000000..b5cf2f9 --- /dev/null +++ b/cmds/screenrecord/FrameOutput.cpp @@ -0,0 +1,208 @@ +/* + * Copyright 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "ScreenRecord" +//#define LOG_NDEBUG 0 +#include + +#include +#include + +#include "FrameOutput.h" + +using namespace android; + +static const bool kShowTiming = false; // set to "true" for debugging +static const int kGlBytesPerPixel = 4; // GL_RGBA +static const int kOutBytesPerPixel = 3; // RGB only + +inline void FrameOutput::setValueLE(uint8_t* buf, uint32_t value) { + // Since we're running on an Android device, we're (almost) guaranteed + // to be little-endian, and (almost) guaranteed that unaligned 32-bit + // writes will work without any performance penalty... but do it + // byte-by-byte anyway. + buf[0] = (uint8_t) value; + buf[1] = (uint8_t) (value >> 8); + buf[2] = (uint8_t) (value >> 16); + buf[3] = (uint8_t) (value >> 24); +} + +status_t FrameOutput::createInputSurface(int width, int height, + sp* pBufferProducer) { + status_t err; + + err = mEglWindow.createPbuffer(width, height); + if (err != NO_ERROR) { + return err; + } + mEglWindow.makeCurrent(); + + glViewport(0, 0, width, height); + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + + // Shader for rendering the external texture. + err = mExtTexProgram.setup(Program::PROGRAM_EXTERNAL_TEXTURE); + if (err != NO_ERROR) { + return err; + } + + // Input side (buffers from virtual display). + glGenTextures(1, &mExtTextureName); + if (mExtTextureName == 0) { + ALOGE("glGenTextures failed: %#x", glGetError()); + return UNKNOWN_ERROR; + } + + mBufferQueue = new BufferQueue(/*new GraphicBufferAlloc()*/); + mGlConsumer = new GLConsumer(mBufferQueue, mExtTextureName, + GL_TEXTURE_EXTERNAL_OES); + mGlConsumer->setName(String8("virtual display")); + mGlConsumer->setDefaultBufferSize(width, height); + mGlConsumer->setDefaultMaxBufferCount(5); + mGlConsumer->setConsumerUsageBits(GRALLOC_USAGE_HW_TEXTURE); + + mGlConsumer->setFrameAvailableListener(this); + + mPixelBuf = new uint8_t[width * height * kGlBytesPerPixel]; + + *pBufferProducer = mBufferQueue; + + ALOGD("FrameOutput::createInputSurface OK"); + return NO_ERROR; +} + +status_t FrameOutput::copyFrame(FILE* fp, long timeoutUsec) { + Mutex::Autolock _l(mMutex); + ALOGV("copyFrame %ld\n", timeoutUsec); + + if (!mFrameAvailable) { + nsecs_t timeoutNsec = (nsecs_t)timeoutUsec * 1000; + int cc = mEventCond.waitRelative(mMutex, timeoutNsec); + if (cc == -ETIMEDOUT) { + ALOGV("cond wait timed out"); + return ETIMEDOUT; + } else if (cc != 0) { + ALOGW("cond wait returned error %d", cc); + return cc; + } + } + if (!mFrameAvailable) { + // This happens when Ctrl-C is hit. Apparently POSIX says that the + // pthread wait call doesn't return EINTR, treating this instead as + // an instance of a "spurious wakeup". We didn't get a frame, so + // we just treat it as a timeout. + return ETIMEDOUT; + } + + // A frame is available. Clear the flag for the next round. + mFrameAvailable = false; + + float texMatrix[16]; + mGlConsumer->updateTexImage(); + mGlConsumer->getTransformMatrix(texMatrix); + + // The data is in an external texture, so we need to render it to the + // pbuffer to get access to RGB pixel data. We also want to flip it + // upside-down for easy conversion to a bitmap. + int width = mEglWindow.getWidth(); + int height = mEglWindow.getHeight(); + status_t err = mExtTexProgram.blit(mExtTextureName, texMatrix, 0, 0, + width, height, true); + if (err != NO_ERROR) { + return err; + } + + // GLES only guarantees that glReadPixels() will work with GL_RGBA, so we + // need to get 4 bytes/pixel and reduce it. Depending on the size of the + // screen and the device capabilities, this can take a while. + int64_t startWhenNsec, pixWhenNsec, endWhenNsec; + if (kShowTiming) { + startWhenNsec = systemTime(CLOCK_MONOTONIC); + } + GLenum glErr; + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, mPixelBuf); + if ((glErr = glGetError()) != GL_NO_ERROR) { + ALOGE("glReadPixels failed: %#x", glErr); + return UNKNOWN_ERROR; + } + if (kShowTiming) { + pixWhenNsec = systemTime(CLOCK_MONOTONIC); + } + reduceRgbaToRgb(mPixelBuf, width * height); + if (kShowTiming) { + endWhenNsec = systemTime(CLOCK_MONOTONIC); + ALOGD("got pixels (get=%.3f ms, reduce=%.3fms)", + (pixWhenNsec - startWhenNsec) / 1000000.0, + (endWhenNsec - pixWhenNsec) / 1000000.0); + } + + // Fill out the header. + size_t headerLen = sizeof(uint32_t) * 5; + size_t rgbDataLen = width * height * kOutBytesPerPixel; + size_t packetLen = headerLen - sizeof(uint32_t) + rgbDataLen; + uint8_t header[headerLen]; + setValueLE(&header[0], packetLen); + setValueLE(&header[4], width); + setValueLE(&header[8], height); + setValueLE(&header[12], width * kOutBytesPerPixel); + setValueLE(&header[16], HAL_PIXEL_FORMAT_RGB_888); + + // Currently using buffered I/O rather than writev(). Not expecting it + // to make much of a difference, but it might be worth a test for larger + // frame sizes. + if (kShowTiming) { + startWhenNsec = systemTime(CLOCK_MONOTONIC); + } + fwrite(header, 1, headerLen, fp); + fwrite(mPixelBuf, 1, rgbDataLen, fp); + fflush(fp); + if (kShowTiming) { + endWhenNsec = systemTime(CLOCK_MONOTONIC); + ALOGD("wrote pixels (%.3f ms)", + (endWhenNsec - startWhenNsec) / 1000000.0); + } + + if (ferror(fp)) { + // errno may not be useful; log it anyway + ALOGE("write failed (errno=%d)", errno); + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + +void FrameOutput::reduceRgbaToRgb(uint8_t* buf, unsigned int pixelCount) { + // Convert RGBA to RGB. + // + // Unaligned 32-bit accesses are allowed on ARM, so we could do this + // with 32-bit copies advancing at different rates (taking care at the + // end to not go one byte over). + const uint8_t* readPtr = buf; + for (unsigned int i = 0; i < pixelCount; i++) { + *buf++ = *readPtr++; + *buf++ = *readPtr++; + *buf++ = *readPtr++; + readPtr++; + } +} + +// Callback; executes on arbitrary thread. +void FrameOutput::onFrameAvailable() { + Mutex::Autolock _l(mMutex); + mFrameAvailable = true; + mEventCond.signal(); +} diff --git a/cmds/screenrecord/FrameOutput.h b/cmds/screenrecord/FrameOutput.h new file mode 100644 index 0000000..b8e9e68 --- /dev/null +++ b/cmds/screenrecord/FrameOutput.h @@ -0,0 +1,101 @@ +/* + * Copyright 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCREENRECORD_FRAMEOUTPUT_H +#define SCREENRECORD_FRAMEOUTPUT_H + +#include "Program.h" +#include "EglWindow.h" + +#include +#include + +namespace android { + +/* + * Support for "frames" output format. + */ +class FrameOutput : public GLConsumer::FrameAvailableListener { +public: + FrameOutput() : mFrameAvailable(false), + mExtTextureName(0), + mPixelBuf(NULL) + {} + virtual ~FrameOutput() { + delete[] mPixelBuf; + } + + // Create an "input surface", similar in purpose to a MediaCodec input + // surface, that the virtual display can send buffers to. Also configures + // EGL with a pbuffer surface on the current thread. + status_t createInputSurface(int width, int height, + sp* pBufferProducer); + + // Copy one from input to output. If no frame is available, this will wait up to the + // specified number of microseconds. + // + // Returns ETIMEDOUT if the timeout expired before we found a frame. + status_t copyFrame(FILE* fp, long timeoutUsec); + + // Prepare to copy frames. Makes the EGL context used by this object current. + void prepareToCopy() { + mEglWindow.makeCurrent(); + } + +private: + FrameOutput(const FrameOutput&); + FrameOutput& operator=(const FrameOutput&); + + // (overrides GLConsumer::FrameAvailableListener method) + virtual void onFrameAvailable(); + + // Reduces RGBA to RGB, in place. + static void reduceRgbaToRgb(uint8_t* buf, unsigned int pixelCount); + + // Put a 32-bit value into a buffer, in little-endian byte order. + static void setValueLE(uint8_t* buf, uint32_t value); + + // Used to wait for the FrameAvailableListener callback. + Mutex mMutex; + Condition mEventCond; + + // Set by the FrameAvailableListener callback. + bool mFrameAvailable; + + // Our queue. The producer side is passed to the virtual display, the + // consumer side feeds into our GLConsumer. + sp mBufferQueue; + + // This receives frames from the virtual display and makes them available + // as an external texture. + sp mGlConsumer; + + // EGL display / context / surface. + EglWindow mEglWindow; + + // GL rendering support. + Program mExtTexProgram; + + // External texture, updated by GLConsumer. + GLuint mExtTextureName; + + // Pixel data buffer. + uint8_t* mPixelBuf; +}; + +}; // namespace android + +#endif /*SCREENRECORD_FRAMEOUTPUT_H*/ diff --git a/cmds/screenrecord/Program.cpp b/cmds/screenrecord/Program.cpp index a198204..73cae6e 100644 --- a/cmds/screenrecord/Program.cpp +++ b/cmds/screenrecord/Program.cpp @@ -201,7 +201,7 @@ status_t Program::linkShaderProgram(GLuint vs, GLuint fs, GLuint* outPgm) { status_t Program::blit(GLuint texName, const float* texMatrix, - int32_t x, int32_t y, int32_t w, int32_t h) const { + int32_t x, int32_t y, int32_t w, int32_t h, bool invert) const { ALOGV("Program::blit %d xy=%d,%d wh=%d,%d", texName, x, y, w, h); const float pos[] = { @@ -218,7 +218,7 @@ status_t Program::blit(GLuint texName, const float* texMatrix, }; status_t err; - err = beforeDraw(texName, texMatrix, pos, uv); + err = beforeDraw(texName, texMatrix, pos, uv, invert); if (err == NO_ERROR) { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); err = afterDraw(); @@ -232,7 +232,7 @@ status_t Program::drawTriangles(GLuint texName, const float* texMatrix, status_t err; - err = beforeDraw(texName, texMatrix, vertices, texes); + err = beforeDraw(texName, texMatrix, vertices, texes, false); if (err == NO_ERROR) { glDrawArrays(GL_TRIANGLES, 0, count); err = afterDraw(); @@ -241,7 +241,7 @@ status_t Program::drawTriangles(GLuint texName, const float* texMatrix, } status_t Program::beforeDraw(GLuint texName, const float* texMatrix, - const float* vertices, const float* texes) const { + const float* vertices, const float* texes, bool invert) const { // Create an orthographic projection matrix based on viewport size. GLint vp[4]; glGetIntegerv(GL_VIEWPORT, vp); @@ -251,6 +251,10 @@ status_t Program::beforeDraw(GLuint texName, const float* texMatrix, 0.0f, 0.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, }; + if (invert) { + screenToNdc[5] = -screenToNdc[5]; + screenToNdc[13] = -screenToNdc[13]; + } glUseProgram(mProgram); diff --git a/cmds/screenrecord/Program.h b/cmds/screenrecord/Program.h index e47bc0d..558be8d 100644 --- a/cmds/screenrecord/Program.h +++ b/cmds/screenrecord/Program.h @@ -51,9 +51,11 @@ public: // Release the program and associated resources. void release(); - // Blit the specified texture to { x, y, x+w, y+h }. + // Blit the specified texture to { x, y, x+w, y+h }. Inverts the + // content if "invert" is set. status_t blit(GLuint texName, const float* texMatrix, - int32_t x, int32_t y, int32_t w, int32_t h) const; + int32_t x, int32_t y, int32_t w, int32_t h, + bool invert = false) const; // Draw a number of triangles. status_t drawTriangles(GLuint texName, const float* texMatrix, @@ -67,7 +69,7 @@ private: // Common code for draw functions. status_t beforeDraw(GLuint texName, const float* texMatrix, - const float* vertices, const float* texes) const; + const float* vertices, const float* texes, bool invert) const; status_t afterDraw() const; // GLES 2 shader utilities. diff --git a/cmds/screenrecord/screenrecord.cpp b/cmds/screenrecord/screenrecord.cpp index b6f150c..a17fc51 100644 --- a/cmds/screenrecord/screenrecord.cpp +++ b/cmds/screenrecord/screenrecord.cpp @@ -50,6 +50,7 @@ #include "screenrecord.h" #include "Overlay.h" +#include "FrameOutput.h" using namespace android; @@ -58,11 +59,14 @@ static const uint32_t kMaxBitRate = 200 * 1000000; // 200Mbps static const uint32_t kMaxTimeLimitSec = 180; // 3 minutes static const uint32_t kFallbackWidth = 1280; // 720p static const uint32_t kFallbackHeight = 720; +static const char* kMimeTypeAvc = "video/avc"; // Command-line parameters. static bool gVerbose = false; // chatty on stdout static bool gRotate = false; // rotate 90 degrees -static bool gRawOutput = false; // generate raw H.264 byte stream output +static enum { + FORMAT_MP4, FORMAT_H264, FORMAT_FRAMES +} gOutputFormat = FORMAT_MP4; // data format for output static bool gSizeSpecified = false; // was size explicitly requested? static bool gWantInfoScreen = false; // do we want initial info screen? static bool gWantFrameTime = false; // do we want times on each frame? @@ -142,14 +146,14 @@ static status_t prepareEncoder(float displayFps, sp* pCodec, status_t err; if (gVerbose) { - printf("Configuring recorder for %dx%d video at %.2fMbps\n", - gVideoWidth, gVideoHeight, gBitRate / 1000000.0); + printf("Configuring recorder for %dx%d %s at %.2fMbps\n", + gVideoWidth, gVideoHeight, kMimeTypeAvc, gBitRate / 1000000.0); } sp format = new AMessage; format->setInt32("width", gVideoWidth); format->setInt32("height", gVideoHeight); - format->setString("mime", "video/avc"); + format->setString("mime", kMimeTypeAvc); format->setInt32("color-format", OMX_COLOR_FormatAndroidOpaque); format->setInt32("bitrate", gBitRate); format->setFloat("frame-rate", displayFps); @@ -159,16 +163,18 @@ static status_t prepareEncoder(float displayFps, sp* pCodec, looper->setName("screenrecord_looper"); looper->start(); ALOGV("Creating codec"); - sp codec = MediaCodec::CreateByType(looper, "video/avc", true); + sp codec = MediaCodec::CreateByType(looper, kMimeTypeAvc, true); if (codec == NULL) { - fprintf(stderr, "ERROR: unable to create video/avc codec instance\n"); + fprintf(stderr, "ERROR: unable to create %s codec instance\n", + kMimeTypeAvc); return UNKNOWN_ERROR; } err = codec->configure(format, NULL, NULL, MediaCodec::CONFIGURE_FLAG_ENCODE); if (err != NO_ERROR) { - fprintf(stderr, "ERROR: unable to configure codec (err=%d)\n", err); + fprintf(stderr, "ERROR: unable to configure %s codec at %dx%d (err=%d)\n", + kMimeTypeAvc, gVideoWidth, gVideoHeight, err); codec->release(); return err; } @@ -513,7 +519,7 @@ static FILE* prepareRawOutput(const char* fileName) { } /* - * Main "do work" method. + * Main "do work" start point. * * Configures codec, muxer, and virtual display, then starts moving bits * around. @@ -555,30 +561,40 @@ static status_t recordScreen(const char* fileName) { // Configure and start the encoder. sp encoder; + sp frameOutput; sp encoderInputSurface; - err = prepareEncoder(mainDpyInfo.fps, &encoder, &encoderInputSurface); - - if (err != NO_ERROR && !gSizeSpecified) { - // fallback is defined for landscape; swap if we're in portrait - bool needSwap = gVideoWidth < gVideoHeight; - uint32_t newWidth = needSwap ? kFallbackHeight : kFallbackWidth; - uint32_t newHeight = needSwap ? kFallbackWidth : kFallbackHeight; - if (gVideoWidth != newWidth && gVideoHeight != newHeight) { - ALOGV("Retrying with 720p"); - fprintf(stderr, "WARNING: failed at %dx%d, retrying at %dx%d\n", - gVideoWidth, gVideoHeight, newWidth, newHeight); - gVideoWidth = newWidth; - gVideoHeight = newHeight; - err = prepareEncoder(mainDpyInfo.fps, &encoder, - &encoderInputSurface); + if (gOutputFormat != FORMAT_FRAMES) { + err = prepareEncoder(mainDpyInfo.fps, &encoder, &encoderInputSurface); + + if (err != NO_ERROR && !gSizeSpecified) { + // fallback is defined for landscape; swap if we're in portrait + bool needSwap = gVideoWidth < gVideoHeight; + uint32_t newWidth = needSwap ? kFallbackHeight : kFallbackWidth; + uint32_t newHeight = needSwap ? kFallbackWidth : kFallbackHeight; + if (gVideoWidth != newWidth && gVideoHeight != newHeight) { + ALOGV("Retrying with 720p"); + fprintf(stderr, "WARNING: failed at %dx%d, retrying at %dx%d\n", + gVideoWidth, gVideoHeight, newWidth, newHeight); + gVideoWidth = newWidth; + gVideoHeight = newHeight; + err = prepareEncoder(mainDpyInfo.fps, &encoder, + &encoderInputSurface); + } } - } - if (err != NO_ERROR) return err; - - // From here on, we must explicitly release() the encoder before it goes - // out of scope, or we will get an assertion failure from stagefright - // later on in a different thread. + if (err != NO_ERROR) return err; + // From here on, we must explicitly release() the encoder before it goes + // out of scope, or we will get an assertion failure from stagefright + // later on in a different thread. + } else { + // We're not using an encoder at all. The "encoder input surface" we hand to + // SurfaceFlinger will just feed directly to us. + frameOutput = new FrameOutput(); + err = frameOutput->createInputSurface(gVideoWidth, gVideoHeight, &encoderInputSurface); + if (err != NO_ERROR) { + return err; + } + } // Draw the "info" page by rendering a frame with GLES and sending // it directly to the encoder. @@ -595,7 +611,7 @@ static status_t recordScreen(const char* fileName) { overlay = new Overlay(); err = overlay->start(encoderInputSurface, &bufferProducer); if (err != NO_ERROR) { - encoder->release(); + if (encoder != NULL) encoder->release(); return err; } if (gVerbose) { @@ -610,46 +626,83 @@ static status_t recordScreen(const char* fileName) { sp dpy; err = prepareVirtualDisplay(mainDpyInfo, bufferProducer, &dpy); if (err != NO_ERROR) { - encoder->release(); + if (encoder != NULL) encoder->release(); return err; } sp muxer = NULL; FILE* rawFp = NULL; - if (gRawOutput) { - rawFp = prepareRawOutput(fileName); - if (rawFp == NULL) { - encoder->release(); - return -1; + switch (gOutputFormat) { + case FORMAT_MP4: { + // Configure muxer. We have to wait for the CSD blob from the encoder + // before we can start it. + muxer = new MediaMuxer(fileName, MediaMuxer::OUTPUT_FORMAT_MPEG_4); + if (gRotate) { + muxer->setOrientationHint(90); // TODO: does this do anything? + } + break; + } + case FORMAT_H264: + case FORMAT_FRAMES: { + rawFp = prepareRawOutput(fileName); + if (rawFp == NULL) { + if (encoder != NULL) encoder->release(); + return -1; + } + break; + } + default: + fprintf(stderr, "ERROR: unknown format %d\n", gOutputFormat); + abort(); + } + + if (gOutputFormat == FORMAT_FRAMES) { + // TODO: if we want to make this a proper feature, we should output + // an outer header with version info. Right now we never change + // the frame size or format, so we could conceivably just send + // the current frame header once and then follow it with an + // unbroken stream of data. + + // Make the EGL context current again. This gets unhooked if we're + // using "--bugreport" mode. + // TODO: figure out if we can eliminate this + frameOutput->prepareToCopy(); + + while (!gStopRequested) { + // Poll for frames, the same way we do for MediaCodec. We do + // all of the work on the main thread. + // + // Ideally we'd sleep indefinitely and wake when the + // stop was requested, but this will do for now. (It almost + // works because wait() wakes when a signal hits, but we + // need to handle the edge cases.) + err = frameOutput->copyFrame(rawFp, 250000); + if (err == ETIMEDOUT) { + err = NO_ERROR; + } else if (err != NO_ERROR) { + ALOGE("Got error %d from copyFrame()", err); + break; + } } } else { - // Configure muxer. We have to wait for the CSD blob from the encoder - // before we can start it. - muxer = new MediaMuxer(fileName, MediaMuxer::OUTPUT_FORMAT_MPEG_4); - if (gRotate) { - muxer->setOrientationHint(90); // TODO: does this do anything? + // Main encoder loop. + err = runEncoder(encoder, muxer, rawFp, mainDpy, dpy, + mainDpyInfo.orientation); + if (err != NO_ERROR) { + fprintf(stderr, "Encoder failed (err=%d)\n", err); + // fall through to cleanup } - } - - // Main encoder loop. - err = runEncoder(encoder, muxer, rawFp, mainDpy, dpy, - mainDpyInfo.orientation); - if (err != NO_ERROR) { - fprintf(stderr, "Encoder failed (err=%d)\n", err); - // fall through to cleanup - } - if (gVerbose) { - printf("Stopping encoder and muxer\n"); + if (gVerbose) { + printf("Stopping encoder and muxer\n"); + } } // Shut everything down, starting with the producer side. encoderInputSurface = NULL; SurfaceComposerClient::destroyDisplay(dpy); - if (overlay != NULL) { - overlay->stop(); - } - encoder->stop(); + if (overlay != NULL) overlay->stop(); + if (encoder != NULL) encoder->stop(); if (muxer != NULL) { // If we don't stop muxer explicitly, i.e. let the destructor run, // it may hang (b/11050628). @@ -657,7 +710,7 @@ static status_t recordScreen(const char* fileName) { } else if (rawFp != stdout) { fclose(rawFp); } - encoder->release(); + if (encoder != NULL) encoder->release(); return err; } @@ -819,11 +872,12 @@ int main(int argc, char* const argv[]) { { "size", required_argument, NULL, 's' }, { "bit-rate", required_argument, NULL, 'b' }, { "time-limit", required_argument, NULL, 't' }, + { "bugreport", no_argument, NULL, 'u' }, + // "unofficial" options { "show-device-info", no_argument, NULL, 'i' }, { "show-frame-time", no_argument, NULL, 'f' }, - { "bugreport", no_argument, NULL, 'u' }, { "rotate", no_argument, NULL, 'r' }, - { "raw", no_argument, NULL, 'w' }, + { "output-format", required_argument, NULL, 'o' }, { NULL, 0, NULL, 0 } }; @@ -875,23 +929,31 @@ int main(int argc, char* const argv[]) { return 2; } break; - case 'i': + case 'u': gWantInfoScreen = true; - break; - case 'f': gWantFrameTime = true; break; - case 'u': + case 'i': gWantInfoScreen = true; + break; + case 'f': gWantFrameTime = true; break; case 'r': // experimental feature gRotate = true; break; - case 'w': - // experimental feature - gRawOutput = true; + case 'o': + if (strcmp(optarg, "mp4") == 0) { + gOutputFormat = FORMAT_MP4; + } else if (strcmp(optarg, "h264") == 0) { + gOutputFormat = FORMAT_H264; + } else if (strcmp(optarg, "frames") == 0) { + gOutputFormat = FORMAT_FRAMES; + } else { + fprintf(stderr, "Unknown format '%s'\n", optarg); + return 2; + } break; default: if (ic != '?') { @@ -907,7 +969,7 @@ int main(int argc, char* const argv[]) { } const char* fileName = argv[optind]; - if (!gRawOutput) { + if (gOutputFormat == FORMAT_MP4) { // MediaMuxer tries to create the file in the constructor, but we don't // learn about the failure until muxer.start(), which returns a generic // error code without logging anything. We attempt to create the file -- cgit v1.1