From 1f5a90bc795475896044fcb1f74816c102851f06 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Mon, 22 Jul 2013 12:23:07 -0700 Subject: 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 --- cmds/screenrecord/Android.mk | 38 +++ cmds/screenrecord/screenrecord.cpp | 568 +++++++++++++++++++++++++++++++++++++ 2 files changed, 606 insertions(+) create mode 100644 cmds/screenrecord/Android.mk create mode 100644 cmds/screenrecord/screenrecord.cpp (limited to 'cmds') 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 + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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* pCodec, + sp* pBufferProducer) { + status_t err; + + sp 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 looper = new ALooper; + looper->setName("screenrecord_looper"); + looper->start(); + ALOGV("Creating codec"); + sp 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 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& bufferProducer, + sp* 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 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& encoder, + const sp& 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 > 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 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 self = ProcessState::self(); + self->startThreadPool(); + + // Get main display parameters. + sp 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 encoder; + sp bufferProducer; + err = prepareEncoder(mainDpyInfo.fps, &encoder, &bufferProducer); + if (err != NO_ERROR) return err; + + // Configure virtual display. + sp dpy; + err = prepareVirtualDisplay(mainDpyInfo, bufferProducer, &dpy); + if (err != NO_ERROR) return err; + + // Configure, but do not start, muxer. + sp 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] \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; +} -- cgit v1.1