| /* | |
| * 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. | |
| */ | |
| #include <assert.h> | |
| #include <inttypes.h> | |
| #include <stdlib.h> | |
| #define LOG_TAG "ScreenRecord" | |
| //#define LOG_NDEBUG 0 | |
| #include <utils/Log.h> | |
| #include <gui/BufferQueue.h> | |
| #include <gui/GraphicBufferAlloc.h> | |
| #include <gui/Surface.h> | |
| #include <cutils/properties.h> | |
| #include <utils/misc.h> | |
| #include <GLES2/gl2.h> | |
| #include <GLES2/gl2ext.h> | |
| #include "screenrecord.h" | |
| #include "Overlay.h" | |
| #include "TextRenderer.h" | |
| using namespace android; | |
| // System properties to look up and display on the info screen. | |
| const char* Overlay::kPropertyNames[] = { | |
| "ro.build.description", | |
| // includes ro.build.id, ro.build.product, ro.build.tags, ro.build.type, | |
| // and ro.build.version.release | |
| "ro.product.manufacturer", | |
| "ro.product.model", | |
| "ro.board.platform", | |
| "ro.revision", | |
| "dalvik.vm.heapgrowthlimit", | |
| "dalvik.vm.heapsize", | |
| "persist.sys.dalvik.vm.lib.2", | |
| //"ro.product.cpu.abi", | |
| //"ro.bootloader", | |
| //"this-never-appears!", | |
| }; | |
| status_t Overlay::start(const sp<IGraphicBufferProducer>& outputSurface, | |
| sp<IGraphicBufferProducer>* pBufferProducer) { | |
| ALOGV("Overlay::start"); | |
| mOutputSurface = outputSurface; | |
| // Grab the current monotonic time and the current wall-clock time so we | |
| // can map one to the other. This allows the overlay counter to advance | |
| // by the exact delay between frames, but if the wall clock gets adjusted | |
| // we won't track it, which means we'll gradually go out of sync with the | |
| // times in logcat. | |
| mStartMonotonicNsecs = systemTime(CLOCK_MONOTONIC); | |
| mStartRealtimeNsecs = systemTime(CLOCK_REALTIME); | |
| Mutex::Autolock _l(mMutex); | |
| // Start the thread. Traffic begins immediately. | |
| run("overlay"); | |
| mState = INIT; | |
| while (mState == INIT) { | |
| mStartCond.wait(mMutex); | |
| } | |
| if (mThreadResult != NO_ERROR) { | |
| ALOGE("Failed to start overlay thread: err=%d", mThreadResult); | |
| return mThreadResult; | |
| } | |
| assert(mState == RUNNING); | |
| ALOGV("Overlay::start successful"); | |
| *pBufferProducer = mProducer; | |
| return NO_ERROR; | |
| } | |
| status_t Overlay::stop() { | |
| ALOGV("Overlay::stop"); | |
| Mutex::Autolock _l(mMutex); | |
| mState = STOPPING; | |
| mEventCond.signal(); | |
| return NO_ERROR; | |
| } | |
| bool Overlay::threadLoop() { | |
| Mutex::Autolock _l(mMutex); | |
| mThreadResult = setup_l(); | |
| if (mThreadResult != NO_ERROR) { | |
| ALOGW("Aborting overlay thread"); | |
| mState = STOPPED; | |
| release_l(); | |
| mStartCond.broadcast(); | |
| return false; | |
| } | |
| ALOGV("Overlay thread running"); | |
| mState = RUNNING; | |
| mStartCond.broadcast(); | |
| while (mState == RUNNING) { | |
| mEventCond.wait(mMutex); | |
| if (mFrameAvailable) { | |
| ALOGV("Awake, frame available"); | |
| processFrame_l(); | |
| mFrameAvailable = false; | |
| } else { | |
| ALOGV("Awake, frame not available"); | |
| } | |
| } | |
| ALOGV("Overlay thread stopping"); | |
| release_l(); | |
| mState = STOPPED; | |
| return false; // stop | |
| } | |
| status_t Overlay::setup_l() { | |
| status_t err; | |
| err = mEglWindow.createWindow(mOutputSurface); | |
| if (err != NO_ERROR) { | |
| return err; | |
| } | |
| mEglWindow.makeCurrent(); | |
| int width = mEglWindow.getWidth(); | |
| int height = mEglWindow.getHeight(); | |
| glViewport(0, 0, width, height); | |
| glDisable(GL_DEPTH_TEST); | |
| glDisable(GL_CULL_FACE); | |
| // Shaders for rendering from different types of textures. | |
| err = mTexProgram.setup(Program::PROGRAM_TEXTURE_2D); | |
| if (err != NO_ERROR) { | |
| return err; | |
| } | |
| err = mExtTexProgram.setup(Program::PROGRAM_EXTERNAL_TEXTURE); | |
| if (err != NO_ERROR) { | |
| return err; | |
| } | |
| err = mTextRenderer.loadIntoTexture(); | |
| if (err != NO_ERROR) { | |
| return err; | |
| } | |
| mTextRenderer.setScreenSize(width, height); | |
| // Input side (buffers from virtual display). | |
| glGenTextures(1, &mExtTextureName); | |
| if (mExtTextureName == 0) { | |
| ALOGE("glGenTextures failed: %#x", glGetError()); | |
| return UNKNOWN_ERROR; | |
| } | |
| sp<IGraphicBufferConsumer> consumer; | |
| BufferQueue::createBufferQueue(&mProducer, &consumer); | |
| mGlConsumer = new GLConsumer(consumer, mExtTextureName, | |
| GL_TEXTURE_EXTERNAL_OES, true, false); | |
| mGlConsumer->setName(String8("virtual display")); | |
| mGlConsumer->setDefaultBufferSize(width, height); | |
| mProducer->setMaxDequeuedBufferCount(4); | |
| mGlConsumer->setConsumerUsageBits(GRALLOC_USAGE_HW_TEXTURE); | |
| mGlConsumer->setFrameAvailableListener(this); | |
| return NO_ERROR; | |
| } | |
| void Overlay::release_l() { | |
| ALOGV("Overlay::release_l"); | |
| mOutputSurface.clear(); | |
| mGlConsumer.clear(); | |
| mProducer.clear(); | |
| mTexProgram.release(); | |
| mExtTexProgram.release(); | |
| mEglWindow.release(); | |
| } | |
| void Overlay::processFrame_l() { | |
| float texMatrix[16]; | |
| mGlConsumer->updateTexImage(); | |
| mGlConsumer->getTransformMatrix(texMatrix); | |
| nsecs_t monotonicNsec = mGlConsumer->getTimestamp(); | |
| nsecs_t frameNumber = mGlConsumer->getFrameNumber(); | |
| int64_t droppedFrames = 0; | |
| if (mLastFrameNumber > 0) { | |
| mTotalDroppedFrames += size_t(frameNumber - mLastFrameNumber) - 1; | |
| } | |
| mLastFrameNumber = frameNumber; | |
| mTextRenderer.setProportionalScale(35); | |
| if (false) { // DEBUG - full blue background | |
| glClearColor(0.0f, 0.0f, 1.0f, 1.0f); | |
| glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); | |
| } | |
| int width = mEglWindow.getWidth(); | |
| int height = mEglWindow.getHeight(); | |
| if (false) { // DEBUG - draw inset | |
| mExtTexProgram.blit(mExtTextureName, texMatrix, | |
| 100, 100, width-200, height-200); | |
| } else { | |
| mExtTexProgram.blit(mExtTextureName, texMatrix, | |
| 0, 0, width, height); | |
| } | |
| glEnable(GL_BLEND); | |
| glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); | |
| if (false) { // DEBUG - show entire font bitmap | |
| mTexProgram.blit(mTextRenderer.getTextureName(), Program::kIdentity, | |
| 100, 100, width-200, height-200); | |
| } | |
| char textBuf[64]; | |
| getTimeString_l(monotonicNsec, textBuf, sizeof(textBuf)); | |
| String8 timeStr(String8::format("%s f=%" PRId64 " (%zd)", | |
| textBuf, frameNumber, mTotalDroppedFrames)); | |
| mTextRenderer.drawString(mTexProgram, Program::kIdentity, 0, 0, timeStr); | |
| glDisable(GL_BLEND); | |
| if (false) { // DEBUG - add red rectangle in lower-left corner | |
| glEnable(GL_SCISSOR_TEST); | |
| glScissor(0, 0, 200, 200); | |
| glClearColor(1.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glDisable(GL_SCISSOR_TEST); | |
| } | |
| mEglWindow.presentationTime(monotonicNsec); | |
| mEglWindow.swapBuffers(); | |
| } | |
| void Overlay::getTimeString_l(nsecs_t monotonicNsec, char* buf, size_t bufLen) { | |
| //const char* format = "%m-%d %T"; // matches log output | |
| const char* format = "%T"; | |
| struct tm tm; | |
| // localtime/strftime is not the fastest way to do this, but a trivial | |
| // benchmark suggests that the cost is negligible. | |
| int64_t realTime = mStartRealtimeNsecs + | |
| (monotonicNsec - mStartMonotonicNsecs); | |
| time_t secs = (time_t) (realTime / 1000000000); | |
| localtime_r(&secs, &tm); | |
| strftime(buf, bufLen, format, &tm); | |
| int32_t msec = (int32_t) ((realTime % 1000000000) / 1000000); | |
| char tmpBuf[5]; | |
| snprintf(tmpBuf, sizeof(tmpBuf), ".%03d", msec); | |
| strlcat(buf, tmpBuf, bufLen); | |
| } | |
| // Callback; executes on arbitrary thread. | |
| void Overlay::onFrameAvailable(const BufferItem& /* item */) { | |
| ALOGV("Overlay::onFrameAvailable"); | |
| Mutex::Autolock _l(mMutex); | |
| mFrameAvailable = true; | |
| mEventCond.signal(); | |
| } | |
| /*static*/ status_t Overlay::drawInfoPage( | |
| const sp<IGraphicBufferProducer>& outputSurface) { | |
| status_t err; | |
| EglWindow window; | |
| err = window.createWindow(outputSurface); | |
| if (err != NO_ERROR) { | |
| return err; | |
| } | |
| window.makeCurrent(); | |
| int width = window.getWidth(); | |
| int height = window.getHeight(); | |
| glViewport(0, 0, width, height); | |
| glDisable(GL_DEPTH_TEST); | |
| glDisable(GL_CULL_FACE); | |
| // Shaders for rendering. | |
| Program texProgram; | |
| err = texProgram.setup(Program::PROGRAM_TEXTURE_2D); | |
| if (err != NO_ERROR) { | |
| return err; | |
| } | |
| TextRenderer textRenderer; | |
| err = textRenderer.loadIntoTexture(); | |
| if (err != NO_ERROR) { | |
| return err; | |
| } | |
| textRenderer.setScreenSize(width, height); | |
| doDrawInfoPage(window, texProgram, textRenderer); | |
| // Destroy the surface. This causes a disconnect. | |
| texProgram.release(); | |
| window.release(); | |
| return NO_ERROR; | |
| } | |
| /*static*/ void Overlay::doDrawInfoPage(const EglWindow& window, | |
| const Program& texProgram, TextRenderer& textRenderer) { | |
| const nsecs_t holdTime = 250000000LL; | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| int width = window.getWidth(); | |
| int height = window.getHeight(); | |
| // Draw a thin border around the screen. Some players, e.g. browser | |
| // plugins, make it hard to see where the edges are when the device | |
| // is using a black background, so this gives the viewer a frame of | |
| // reference. | |
| // | |
| // This is a clumsy way to do it, but we're only doing it for one frame, | |
| // and it's easier than actually drawing lines. | |
| const int lineWidth = 4; | |
| glEnable(GL_SCISSOR_TEST); | |
| glClearColor(0.5f, 0.5f, 0.5f, 1.0f); | |
| glScissor(0, 0, width, lineWidth); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glScissor(0, height - lineWidth, width, lineWidth); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glScissor(0, 0, lineWidth, height); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glScissor(width - lineWidth, 0, lineWidth, height); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glDisable(GL_SCISSOR_TEST); | |
| //glEnable(GL_BLEND); | |
| //glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); | |
| textRenderer.setProportionalScale(30); | |
| float xpos = 0; | |
| float ypos = 0; | |
| ypos = textRenderer.drawWrappedString(texProgram, xpos, ypos, | |
| String8::format("Android screenrecord v%d.%d", | |
| kVersionMajor, kVersionMinor)); | |
| // Show date/time | |
| time_t now = time(0); | |
| struct tm tm; | |
| localtime_r(&now, &tm); | |
| char timeBuf[64]; | |
| strftime(timeBuf, sizeof(timeBuf), "%a, %d %b %Y %T %z", &tm); | |
| String8 header("Started "); | |
| header += timeBuf; | |
| ypos = textRenderer.drawWrappedString(texProgram, xpos, ypos, header); | |
| ypos += 8 * textRenderer.getScale(); // slight padding | |
| // Show selected system property values | |
| for (int i = 0; i < NELEM(kPropertyNames); i++) { | |
| char valueBuf[PROPERTY_VALUE_MAX]; | |
| property_get(kPropertyNames[i], valueBuf, ""); | |
| if (valueBuf[0] == '\0') { | |
| continue; | |
| } | |
| String8 str(String8::format("%s: [%s]", kPropertyNames[i], valueBuf)); | |
| ypos = textRenderer.drawWrappedString(texProgram, xpos, ypos, str); | |
| } | |
| ypos += 8 * textRenderer.getScale(); // slight padding | |
| // Show GL info | |
| String8 glStr("OpenGL: "); | |
| glStr += (char*) glGetString(GL_VENDOR); | |
| glStr += " / "; | |
| glStr += (char*) glGetString(GL_RENDERER); | |
| glStr += ", "; | |
| glStr += (char*) glGetString(GL_VERSION); | |
| ypos = textRenderer.drawWrappedString(texProgram, xpos, ypos, glStr); | |
| //glDisable(GL_BLEND); | |
| // Set a presentation time slightly in the past. This will cause the | |
| // player to hold the frame on screen. | |
| window.presentationTime(systemTime(CLOCK_MONOTONIC) - holdTime); | |
| window.swapBuffers(); | |
| } |