├── .gitignore ├── AndroidManifest.xml ├── LICENSE ├── README.md ├── doc └── flow.png ├── project.properties ├── res ├── drawable-hdpi │ └── ic_app.png ├── drawable-mdpi │ └── ic_app.png ├── layout │ └── source_activity.xml └── values │ └── strings.xml └── src └── com └── a30corner └── screenrecoder ├── ControlActivity.java └── ScreenRecoder.java /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Intellij project files 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Szu-Hsien Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ScreenRecoder 2 | ============= 3 | A screenrecorder in Android KitKat. 4 | 5 | 6 | 7 | This is a proof-of-concept project. 8 | 9 | I just try to use some new stuffs in KitKat to implement a screen record function in pure Java. 10 | 11 | Capturing screen out of your app need one protected permission.(CAPTURE_VIDEO_OUTPUT) 12 | It works well but you need to sign apk with platform key. 13 | 14 | How it works 15 | ============= 16 | 17 | ![flow](http://raw.github.com/misgod/ScreenRecoder/master/doc/flow.png) 18 | 19 | License 20 | ============= 21 | 22 | The MIT License (MIT) 23 | Copyright (c) 2014 Szu-Hsien Lee 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /doc/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misgod/ScreenRecoder/b9537b7a910933002fda97c3fc0d77992192d02f/doc/flow.png -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-19 15 | -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misgod/ScreenRecoder/b9537b7a910933002fda97c3fc0d77992192d02f/res/drawable-hdpi/ic_app.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misgod/ScreenRecoder/b9537b7a910933002fda97c3fc0d77992192d02f/res/drawable-mdpi/ic_app.png -------------------------------------------------------------------------------- /res/layout/source_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 24 | 32 | 33 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ScreenRecoder 6 | 7 | -------------------------------------------------------------------------------- /src/com/a30corner/screenrecoder/ControlActivity.java: -------------------------------------------------------------------------------- 1 | 2 | package com.a30corner.screenrecoder; 3 | 4 | import com.a30corner.screenrecoder.R; 5 | 6 | import android.app.Activity; 7 | import android.os.Bundle; 8 | import android.view.View; 9 | import android.widget.ToggleButton; 10 | 11 | public class ControlActivity extends Activity { 12 | private ScreenRecoder mDisplaySourceService; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | 18 | setContentView(R.layout.source_activity); 19 | 20 | mDisplaySourceService = new ScreenRecoder(this); 21 | 22 | } 23 | 24 | @Override 25 | protected void onDestroy() { 26 | super.onDestroy(); 27 | stopServices(); 28 | } 29 | 30 | private void startServices() { 31 | 32 | mDisplaySourceService.start("/sdcard/temp.mp4"); 33 | } 34 | 35 | private void stopServices() { 36 | if (mDisplaySourceService != null && mDisplaySourceService.isStarted()) { 37 | mDisplaySourceService.stop(); 38 | mDisplaySourceService = null; 39 | } 40 | } 41 | 42 | public void onToggleClicked(View view) { 43 | boolean on = ((ToggleButton) view).isChecked(); 44 | if (on) { 45 | startServices(); 46 | } else { 47 | stopServices(); 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/com/a30corner/screenrecoder/ScreenRecoder.java: -------------------------------------------------------------------------------- 1 | package com.a30corner.screenrecoder; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | import android.content.Context; 7 | import android.hardware.display.DisplayManager; 8 | import android.hardware.display.VirtualDisplay; 9 | import android.media.MediaCodec; 10 | import android.media.MediaCodec.BufferInfo; 11 | import android.media.MediaCodecInfo; 12 | import android.media.MediaFormat; 13 | import android.media.MediaMuxer; 14 | import android.media.MediaMuxer.OutputFormat; 15 | import android.util.Log; 16 | import android.view.Surface; 17 | 18 | public class ScreenRecoder { 19 | private static final String DISPLAY_NAME = "ScreenRecoder"; 20 | private final DisplayManager mDisplayManager; 21 | 22 | private int videoTrackIndex; 23 | 24 | private VirtualDisplayThread mVirtualDisplayThread; 25 | 26 | 27 | private volatile boolean started; 28 | 29 | public ScreenRecoder(Context context) { 30 | 31 | mDisplayManager = (DisplayManager) context 32 | .getSystemService(Context.DISPLAY_SERVICE); 33 | } 34 | 35 | public synchronized void start(String path) { 36 | if (!started) { 37 | mVirtualDisplayThread = new VirtualDisplayThread(720, 38 | 1280, 320,path); 39 | mVirtualDisplayThread.start(); 40 | started = true; 41 | } 42 | } 43 | 44 | public synchronized void stop() { 45 | if (started) { 46 | started = false; 47 | mVirtualDisplayThread.quit(); 48 | } 49 | 50 | } 51 | 52 | public boolean isStarted(){ 53 | return started; 54 | } 55 | 56 | 57 | private final class VirtualDisplayThread extends Thread { 58 | private static final int TIMEOUT_USEC = 1000000; 59 | private static final int BIT_RATE = 6000000; 60 | private static final int FRAME_RATE = 30; 61 | private static final int I_FRAME_INTERVAL = 10; 62 | 63 | private final int mWidth; 64 | private final int mHeight; 65 | private final int mDensityDpi; 66 | private MediaMuxer muxer; 67 | 68 | private volatile boolean mQuitting; 69 | 70 | private boolean mMuxerStarted; 71 | 72 | public VirtualDisplayThread(int width, int height, int densityDpi,String path) { 73 | mWidth = width; 74 | mHeight = height; 75 | mDensityDpi = densityDpi; 76 | 77 | try { 78 | muxer = new MediaMuxer(path, OutputFormat.MUXER_OUTPUT_MPEG_4); 79 | } catch (IOException e) { 80 | Log.e("sam", e.getMessage(), e); 81 | 82 | } 83 | 84 | } 85 | 86 | @Override 87 | public void run() { 88 | MediaFormat format = MediaFormat.createVideoFormat("video/avc", 89 | mWidth, mHeight); 90 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 91 | MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 92 | format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); 93 | format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); 94 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 95 | I_FRAME_INTERVAL); 96 | 97 | MediaCodec codec = MediaCodec.createEncoderByType("video/avc"); 98 | codec.configure(format, null, null, 99 | MediaCodec.CONFIGURE_FLAG_ENCODE); 100 | Surface surface = codec.createInputSurface(); 101 | codec.start(); 102 | 103 | VirtualDisplay virtualDisplay = mDisplayManager 104 | .createVirtualDisplay(DISPLAY_NAME, mWidth, mHeight, 105 | mDensityDpi, surface, 106 | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC); 107 | 108 | if (virtualDisplay != null) { 109 | stream(codec); 110 | virtualDisplay.release(); 111 | } 112 | 113 | codec.signalEndOfInputStream(); 114 | codec.stop(); 115 | } 116 | 117 | public void quit() { 118 | mQuitting = true; 119 | } 120 | 121 | private void stream(MediaCodec codec) { 122 | BufferInfo info = new BufferInfo(); 123 | ByteBuffer[] buffers = null; 124 | 125 | while (!mQuitting) { 126 | int index = codec.dequeueOutputBuffer(info, TIMEOUT_USEC); 127 | if (index >= 0) { 128 | if (buffers == null) { 129 | buffers = codec.getOutputBuffers(); 130 | } 131 | 132 | ByteBuffer buffer = buffers[index]; 133 | buffer.limit(info.offset + info.size); 134 | buffer.position(info.offset); 135 | 136 | muxer.writeSampleData(videoTrackIndex, buffer, info); 137 | 138 | codec.releaseOutputBuffer(index, false); 139 | } else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 140 | if (mMuxerStarted) { 141 | throw new RuntimeException("format changed twice"); 142 | } 143 | 144 | MediaFormat newFormat = codec.getOutputFormat(); 145 | 146 | // now that we have the Magic Goodies, start the muxer 147 | videoTrackIndex = muxer.addTrack(newFormat); 148 | muxer.start(); 149 | 150 | mMuxerStarted = true; 151 | 152 | buffers = null; 153 | } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 154 | buffers = null; 155 | } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { 156 | Log.e("sam", "Codec dequeue buffer timed out."); 157 | } 158 | } 159 | 160 | muxer.stop(); 161 | muxer.release(); 162 | } 163 | } 164 | } 165 | --------------------------------------------------------------------------------