summaryrefslogtreecommitdiffstats
path: root/cmds
diff options
context:
space:
mode:
authorAndy McFadden <fadden@android.com>2013-07-22 12:23:07 -0700
committerAndy McFadden <fadden@android.com>2013-07-23 15:35:46 -0700
commit1f5a90bc795475896044fcb1f74816c102851f06 (patch)
tree6a446bd7e9aca02592c0f4420c46854704d1cf35 /cmds
parent288abb6fe160dff109dedbf89b227fc2b10a2ee8 (diff)
downloadframeworks_av-1f5a90bc795475896044fcb1f74816c102851f06.zip
frameworks_av-1f5a90bc795475896044fcb1f74816c102851f06.tar.gz
frameworks_av-1f5a90bc795475896044fcb1f74816c102851f06.tar.bz2
Prototype screen recording command
This records the screen contents as a movie. It works by feeding the output of a virtual display to the video/avc encoder. Recording continues until Ctrl-C is hit. Video only, no sound. Does not track screen rotations. Change-Id: I91d5c4e781792c740699b7a83590e846295b3617
Diffstat (limited to 'cmds')
-rw-r--r--cmds/screenrecord/Android.mk38
-rw-r--r--cmds/screenrecord/screenrecord.cpp568
2 files changed, 606 insertions, 0 deletions
diff --git a/cmds/screenrecord/Android.mk b/cmds/screenrecord/Android.mk
new file mode 100644
index 0000000..b4a5947
--- /dev/null
+++ b/cmds/screenrecord/Android.mk
@@ -0,0 +1,38 @@
+# Copyright 2013 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+ screenrecord.cpp \
+
+LOCAL_SHARED_LIBRARIES := \
+ libstagefright libmedia libutils libbinder libstagefright_foundation \
+ libjpeg libgui libcutils liblog
+
+LOCAL_C_INCLUDES := \
+ frameworks/av/media/libstagefright \
+ frameworks/av/media/libstagefright/include \
+ $(TOP)/frameworks/native/include/media/openmax \
+ external/jpeg
+
+LOCAL_CFLAGS += -Wno-multichar
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE:= screenrecord
+
+include $(BUILD_EXECUTABLE)
diff --git a/cmds/screenrecord/screenrecord.cpp b/cmds/screenrecord/screenrecord.cpp
new file mode 100644
index 0000000..3e79ee0
--- /dev/null
+++ b/cmds/screenrecord/screenrecord.cpp
@@ -0,0 +1,568 @@
+/*
+ * Copyright 2013 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 <utils/Log.h>
+
+#include <binder/IPCThreadState.h>
+#include <utils/Errors.h>
+#include <utils/Thread.h>
+
+#include <gui/Surface.h>
+#include <gui/SurfaceComposerClient.h>
+#include <gui/ISurfaceComposer.h>
+#include <ui/DisplayInfo.h>
+#include <media/openmax/OMX_IVCommon.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/MediaCodec.h>
+#include <media/stagefright/MediaErrors.h>
+#include <media/stagefright/MediaMuxer.h>
+#include <media/ICrypto.h>
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+
+using namespace android;
+
+// Command-line parameters.
+static bool gVerbose = false; // chatty on stdout
+static bool gRotate = false; // rotate 90 degrees
+static uint32_t gVideoWidth = 1280; // 720p
+static uint32_t gVideoHeight = 720;
+static uint32_t gBitRate = 4000000; // 4Mbps
+
+// Set by signal handler to stop recording.
+static bool gStopRequested;
+
+// Previous signal handler state, restored after first hit.
+static struct sigaction gOrigSigactionINT;
+static struct sigaction gOrigSigactionHUP;
+
+static const uint32_t kMinBitRate = 100000; // 0.1Mbps
+static const uint32_t kMaxBitRate = 100 * 1000000; // 100Mbps
+
+/*
+ * Catch keyboard interrupt signals. On receipt, the "stop requested"
+ * flag is raised, and the original handler is restored (so that, if
+ * we get stuck finishing, a second Ctrl-C will kill the process).
+ */
+static void signalCatcher(int signum)
+{
+ gStopRequested = true;
+ switch (signum) {
+ case SIGINT:
+ sigaction(SIGINT, &gOrigSigactionINT, NULL);
+ break;
+ case SIGHUP:
+ sigaction(SIGHUP, &gOrigSigactionHUP, NULL);
+ break;
+ default:
+ abort();
+ break;
+ }
+}
+
+/*
+ * Configures signal handlers. The previous handlers are saved.
+ *
+ * If the command is run from an interactive adb shell, we get SIGINT
+ * when Ctrl-C is hit. If we're run from the host, the local adb process
+ * gets the signal, and we get a SIGHUP when the terminal disconnects.
+ */
+static status_t configureSignals()
+{
+ struct sigaction act;
+ memset(&act, 0, sizeof(act));
+ act.sa_handler = signalCatcher;
+ if (sigaction(SIGINT, &act, &gOrigSigactionINT) != 0) {
+ status_t err = -errno;
+ fprintf(stderr, "Unable to configure SIGINT handler: %s\n",
+ strerror(errno));
+ return err;
+ }
+ if (sigaction(SIGHUP, &act, &gOrigSigactionHUP) != 0) {
+ status_t err = -errno;
+ fprintf(stderr, "Unable to configure SIGHUP handler: %s\n",
+ strerror(errno));
+ return err;
+ }
+ return NO_ERROR;
+}
+
+/*
+ * Configures and starts the MediaCodec encoder. Obtains an input surface
+ * from the codec.
+ */
+static status_t prepareEncoder(float displayFps, sp<MediaCodec>* pCodec,
+ sp<IGraphicBufferProducer>* pBufferProducer) {
+ status_t err;
+
+ sp<AMessage> format = new AMessage;
+ format->setInt32("width", gVideoWidth);
+ format->setInt32("height", gVideoHeight);
+ format->setString("mime", "video/avc");
+ format->setInt32("color-format", OMX_COLOR_FormatAndroidOpaque);
+ format->setInt32("bitrate", gBitRate);
+ format->setFloat("frame-rate", displayFps);
+ format->setInt32("i-frame-interval", 10);
+
+ /// MediaCodec
+ sp<ALooper> looper = new ALooper;
+ looper->setName("screenrecord_looper");
+ looper->start();
+ ALOGV("Creating codec");
+ sp<MediaCodec> codec = MediaCodec::CreateByType(looper, "video/avc", true);
+ 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);
+ return err;
+ }
+
+ ALOGV("Creating buffer producer");
+ sp<IGraphicBufferProducer> bufferProducer;
+ err = codec->createInputSurface(&bufferProducer);
+ if (err != NO_ERROR) {
+ fprintf(stderr,
+ "ERROR: unable to create encoder input surface (err=%d)\n", err);
+ return err;
+ }
+
+ ALOGV("Starting codec");
+ err = codec->start();
+ if (err != NO_ERROR) {
+ fprintf(stderr, "ERROR: unable to start codec (err=%d)\n", err);
+ return err;
+ }
+
+ *pCodec = codec;
+ *pBufferProducer = bufferProducer;
+ return 0;
+}
+
+/*
+ * Configures the virtual display. When this completes, virtual display
+ * frames will start being sent to the encoder's surface.
+ */
+static status_t prepareVirtualDisplay(const DisplayInfo& mainDpyInfo,
+ const sp<IGraphicBufferProducer>& bufferProducer,
+ sp<IBinder>* pDisplayHandle) {
+ status_t err;
+
+ // Set the region of the layer stack we're interested in, which in our
+ // case is "all of it". If the app is rotated (so that the width of the
+ // app is based on the height of the display), reverse width/height.
+ bool deviceRotated = mainDpyInfo.orientation != DISPLAY_ORIENTATION_0 &&
+ mainDpyInfo.orientation != DISPLAY_ORIENTATION_180;
+ uint32_t sourceWidth, sourceHeight;
+ if (!deviceRotated) {
+ sourceWidth = mainDpyInfo.w;
+ sourceHeight = mainDpyInfo.h;
+ } else {
+ ALOGV("using rotated width/height");
+ sourceHeight = mainDpyInfo.w;
+ sourceWidth = mainDpyInfo.h;
+ }
+ Rect layerStackRect(sourceWidth, sourceHeight);
+
+ // We need to preserve the aspect ratio of the display.
+ float displayAspect = (float) sourceHeight / (float) sourceWidth;
+
+
+ // Set the way we map the output onto the display surface (which will
+ // be e.g. 1280x720 for a 720p video). The rect is interpreted
+ // post-rotation, so if the display is rotated 90 degrees we need to
+ // "pre-rotate" it by flipping width/height, so that the orientation
+ // adjustment changes it back.
+ //
+ // We might want to encode a portrait display as landscape to use more
+ // of the screen real estate. (If players respect a 90-degree rotation
+ // hint, we can essentially get a 720x1280 video instead of 1280x720.)
+ // In that case, we swap the configured video width/height and then
+ // supply a rotation value to the display projection.
+ uint32_t videoWidth, videoHeight;
+ uint32_t outWidth, outHeight;
+ if (!gRotate) {
+ videoWidth = gVideoWidth;
+ videoHeight = gVideoHeight;
+ } else {
+ videoWidth = gVideoHeight;
+ videoHeight = gVideoWidth;
+ }
+ if (videoHeight > (uint32_t)(videoWidth * displayAspect)) {
+ // limited by narrow width; reduce height
+ outWidth = videoWidth;
+ outHeight = (uint32_t)(videoWidth * displayAspect);
+ } else {
+ // limited by short height; restrict width
+ outHeight = videoHeight;
+ outWidth = (uint32_t)(videoHeight / displayAspect);
+ }
+ uint32_t offX, offY;
+ offX = (videoWidth - outWidth) / 2;
+ offY = (videoHeight - outHeight) / 2;
+ Rect displayRect(offX, offY, offX + outWidth, offY + outHeight);
+
+ if (gVerbose) {
+ if (gRotate) {
+ printf("Rotated content area is %ux%u at offset x=%d y=%d\n",
+ outHeight, outWidth, offY, offX);
+ } else {
+ printf("Content area is %ux%u at offset x=%d y=%d\n",
+ outWidth, outHeight, offX, offY);
+ }
+ }
+
+
+ sp<IBinder> dpy = SurfaceComposerClient::createDisplay(
+ String8("ScreenRecorder"), false /* secure */);
+
+ SurfaceComposerClient::openGlobalTransaction();
+ SurfaceComposerClient::setDisplaySurface(dpy, bufferProducer);
+ SurfaceComposerClient::setDisplayProjection(dpy,
+ gRotate ? DISPLAY_ORIENTATION_90 : DISPLAY_ORIENTATION_0,
+ layerStackRect, displayRect);
+ SurfaceComposerClient::setDisplayLayerStack(dpy, 0); // default stack
+ SurfaceComposerClient::closeGlobalTransaction();
+
+ *pDisplayHandle = dpy;
+
+ return NO_ERROR;
+}
+
+/*
+ * Runs the MediaCodec encoder, sending the output to the MediaMuxer. The
+ * input frames are coming from the virtual display as fast as SurfaceFlinger
+ * wants to send them.
+ *
+ * The muxer must *not* have been started before calling.
+ */
+static status_t runEncoder(const sp<MediaCodec>& encoder,
+ const sp<MediaMuxer>& muxer) {
+ static int kTimeout = 250000; // be responsive on signal
+ status_t err;
+ ssize_t trackIdx = -1;
+ uint32_t debugNumFrames = 0;
+ time_t debugStartWhen = time(NULL);
+
+ Vector<sp<ABuffer> > buffers;
+ err = encoder->getOutputBuffers(&buffers);
+ if (err != NO_ERROR) {
+ fprintf(stderr, "Unable to get output buffers (err=%d)\n", err);
+ return err;
+ }
+
+ // This is set by the signal handler.
+ gStopRequested = false;
+
+ // Run until we're signaled.
+ while (!gStopRequested) {
+ size_t bufIndex, offset, size;
+ int64_t ptsUsec;
+ uint32_t flags;
+ ALOGV("Calling dequeueOutputBuffer");
+ err = encoder->dequeueOutputBuffer(&bufIndex, &offset, &size, &ptsUsec,
+ &flags, kTimeout);
+ ALOGV("dequeueOutputBuffer returned %d", err);
+ switch (err) {
+ case NO_ERROR:
+ // got a buffer
+ if ((flags & MediaCodec::BUFFER_FLAG_CODECCONFIG) != 0) {
+ // ignore this -- we passed the CSD into MediaMuxer when
+ // we got the format change notification
+ ALOGV("Got codec config buffer (%u bytes); ignoring", size);
+ size = 0;
+ }
+ if (size != 0) {
+ ALOGV("Got data in buffer %d, size=%d, pts=%lld",
+ bufIndex, size, ptsUsec);
+ CHECK(trackIdx != -1);
+
+ // The MediaMuxer docs are unclear, but it appears that we
+ // need to pass either the full set of BufferInfo flags, or
+ // (flags & BUFFER_FLAG_SYNCFRAME).
+ err = muxer->writeSampleData(buffers[bufIndex], trackIdx,
+ ptsUsec, flags);
+ if (err != NO_ERROR) {
+ fprintf(stderr, "Failed writing data to muxer (err=%d)\n",
+ err);
+ return err;
+ }
+ debugNumFrames++;
+ }
+ err = encoder->releaseOutputBuffer(bufIndex);
+ if (err != NO_ERROR) {
+ fprintf(stderr, "Unable to release output buffer (err=%d)\n",
+ err);
+ return err;
+ }
+ if ((flags & MediaCodec::BUFFER_FLAG_EOS) != 0) {
+ // Not expecting EOS from SurfaceFlinger. Go with it.
+ ALOGD("Received end-of-stream");
+ gStopRequested = false;
+ }
+ break;
+ case -EAGAIN: // INFO_TRY_AGAIN_LATER
+ // not expected with infinite timeout
+ ALOGV("Got -EAGAIN, looping");
+ break;
+ case INFO_FORMAT_CHANGED: // INFO_OUTPUT_FORMAT_CHANGED
+ {
+ // format includes CSD, which we must provide to muxer
+ ALOGV("Encoder format changed");
+ sp<AMessage> newFormat;
+ encoder->getOutputFormat(&newFormat);
+ trackIdx = muxer->addTrack(newFormat);
+ ALOGV("Starting muxer");
+ err = muxer->start();
+ if (err != NO_ERROR) {
+ fprintf(stderr, "Unable to start muxer (err=%d)\n", err);
+ return err;
+ }
+ }
+ break;
+ case INFO_OUTPUT_BUFFERS_CHANGED: // INFO_OUTPUT_BUFFERS_CHANGED
+ // not expected for an encoder; handle it anyway
+ ALOGV("Encoder buffers changed");
+ err = encoder->getOutputBuffers(&buffers);
+ if (err != NO_ERROR) {
+ fprintf(stderr,
+ "Unable to get new output buffers (err=%d)\n", err);
+ }
+ break;
+ default:
+ ALOGW("Got weird result %d from dequeueOutputBuffer", err);
+ return err;
+ }
+ }
+
+ ALOGV("Encoder stopping (req=%d)", gStopRequested);
+ if (gVerbose) {
+ printf("Encoder stopping; recorded %u frames in %ld seconds\n",
+ debugNumFrames, time(NULL) - debugStartWhen);
+ }
+ return NO_ERROR;
+}
+
+/*
+ * Main "do work" method.
+ *
+ * Configures codec, muxer, and virtual display, then starts moving bits
+ * around.
+ */
+static status_t recordScreen(const char* fileName) {
+ status_t err;
+
+ if (gVerbose) {
+ printf("Recording %dx%d video at %.2fMbps\n",
+ gVideoWidth, gVideoHeight, gBitRate / 1000000.0);
+ }
+
+ // Configure signal handler.
+ err = configureSignals();
+ if (err != NO_ERROR) return err;
+
+ // Start Binder thread pool. MediaCodec needs to be able to receive
+ // messages from mediaserver.
+ sp<ProcessState> self = ProcessState::self();
+ self->startThreadPool();
+
+ // Get main display parameters.
+ sp<IBinder> mainDpy = SurfaceComposerClient::getBuiltInDisplay(
+ ISurfaceComposer::eDisplayIdMain);
+ DisplayInfo mainDpyInfo;
+ err = SurfaceComposerClient::getDisplayInfo(mainDpy, &mainDpyInfo);
+ if (err != NO_ERROR) {
+ fprintf(stderr, "ERROR: unable to get display characteristics\n");
+ return err;
+ }
+ if (gVerbose) {
+ printf("Main display is %dx%d @%.2ffps (orientation=%u)\n",
+ mainDpyInfo.w, mainDpyInfo.h, mainDpyInfo.fps,
+ mainDpyInfo.orientation);
+ }
+
+ // Configure and start the encoder.
+ sp<MediaCodec> encoder;
+ sp<IGraphicBufferProducer> bufferProducer;
+ err = prepareEncoder(mainDpyInfo.fps, &encoder, &bufferProducer);
+ if (err != NO_ERROR) return err;
+
+ // Configure virtual display.
+ sp<IBinder> dpy;
+ err = prepareVirtualDisplay(mainDpyInfo, bufferProducer, &dpy);
+ if (err != NO_ERROR) return err;
+
+ // Configure, but do not start, muxer.
+ sp<MediaMuxer> muxer = new MediaMuxer(fileName,
+ MediaMuxer::OUTPUT_FORMAT_MPEG_4);
+ if (gRotate) {
+ muxer->setOrientationHint(90);
+ }
+
+ // Main encoder loop.
+ err = runEncoder(encoder, muxer);
+ if (err != NO_ERROR) return err;
+
+ if (gVerbose) {
+ printf("Stopping encoder and muxer\n");
+ }
+
+ // Shut everything down.
+ //
+ // The virtual display will continue to produce frames until "dpy"
+ // goes out of scope (and something causes the Binder traffic to transmit;
+ // can be forced with IPCThreadState::self()->flushCommands()). This
+ // could cause SurfaceFlinger to get stuck trying to feed us, so we want
+ // to set a NULL Surface to make the virtual display "dormant".
+ bufferProducer = NULL;
+ SurfaceComposerClient::openGlobalTransaction();
+ SurfaceComposerClient::setDisplaySurface(dpy, bufferProducer);
+ SurfaceComposerClient::closeGlobalTransaction();
+
+ encoder->stop();
+ muxer->stop();
+ encoder->release();
+
+ return 0;
+}
+
+/*
+ * Parses a string of the form "1280x720".
+ *
+ * Returns true on success.
+ */
+static bool parseWidthHeight(const char* widthHeight, uint32_t* pWidth,
+ uint32_t* pHeight) {
+ long width, height;
+ char* end;
+
+ // Must specify base 10, or "0x0" gets parsed differently.
+ width = strtol(widthHeight, &end, 10);
+ if (end == widthHeight || *end != 'x' || *(end+1) == '\0') {
+ // invalid chars in width, or missing 'x', or missing height
+ return false;
+ }
+ height = strtol(end + 1, &end, 10);
+ if (*end != '\0') {
+ // invalid chars in height
+ return false;
+ }
+
+ *pWidth = width;
+ *pHeight = height;
+ return true;
+}
+
+/*
+ * Dumps usage on stderr.
+ */
+static void usage() {
+ fprintf(stderr,
+ "Usage: screenrecord [options] <filename>\n"
+ "\n"
+ "Options:\n"
+ "--size WIDTHxHEIGHT\n"
+ " Set the video size, e.g. \"1280x720\". For best results, use\n"
+ " a size supported by the AVC encoder.\n"
+ "--bit-rate RATE\n"
+ " Set the video bit rate, in megabits per second. Default 4Mbps.\n"
+ "--rotate\n"
+ " Rotate the output 90 degrees. Useful for filling the frame\n"
+ " when in portrait mode.\n"
+ "--verbose\n"
+ " Display interesting information on stdout.\n"
+ "--help\n"
+ " Show this message.\n"
+ "\n"
+ "Recording continues until Ctrl-C is hit.\n"
+ "\n"
+ );
+}
+
+/*
+ * Parses args and kicks things off.
+ */
+int main(int argc, char* const argv[]) {
+ static const struct option longOptions[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "size", required_argument, NULL, 's' },
+ { "bit-rate", required_argument, NULL, 'b' },
+ { "rotate", no_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0 }
+ };
+
+ while (true) {
+ int optionIndex = 0;
+ int ic = getopt_long(argc, argv, "", longOptions, &optionIndex);
+ if (ic == -1) {
+ break;
+ }
+
+ switch (ic) {
+ case 'h':
+ usage();
+ return 0;
+ case 'v':
+ gVerbose = true;
+ break;
+ case 's':
+ if (!parseWidthHeight(optarg, &gVideoWidth, &gVideoHeight)) {
+ fprintf(stderr, "Invalid size '%s', must be width x height\n",
+ optarg);
+ return 2;
+ }
+ if (gVideoWidth == 0 || gVideoHeight == 0) {
+ fprintf(stderr,
+ "Invalid size %ux%u, width and height may not be zero\n",
+ gVideoWidth, gVideoHeight);
+ return 2;
+ }
+ break;
+ case 'b':
+ gBitRate = atoi(optarg);
+ if (gBitRate < kMinBitRate || gBitRate > kMaxBitRate) {
+ fprintf(stderr,
+ "Bit rate %dbps outside acceptable range [%d,%d]\n",
+ gBitRate, kMinBitRate, kMaxBitRate);
+ return 2;
+ }
+ break;
+ case 'r':
+ gRotate = true;
+ break;
+ default:
+ if (ic != '?') {
+ fprintf(stderr, "getopt_long returned unexpected value 0x%x\n", ic);
+ }
+ return 2;
+ }
+ }
+
+ if (optind != argc - 1) {
+ fprintf(stderr, "Must specify output file (see --help).\n");
+ return 2;
+ }
+
+ status_t err = recordScreen(argv[optind]);
+ ALOGD(err == NO_ERROR ? "success" : "failed");
+ return (int) err;
+}