├── .gitignore ├── .idea ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── felix │ │ └── glcamera │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── felix │ │ │ └── glcamera │ │ │ ├── CameraRecordActivity.java │ │ │ ├── CameraRecorderHelper.java │ │ │ ├── FilterType.java │ │ │ ├── IMediaRecorder.java │ │ │ ├── MediaRecorder.java │ │ │ ├── PermissionHelper.java │ │ │ ├── TextureMovieEncoder.java │ │ │ ├── VideoEncoderCore.java │ │ │ ├── gles │ │ │ ├── Drawable2d.java │ │ │ ├── EglCore.java │ │ │ ├── EglSurfaceBase.java │ │ │ ├── FullFrameRect.java │ │ │ ├── GlUtil.java │ │ │ ├── Texture2dProgram.java │ │ │ └── WindowSurface.java │ │ │ ├── util │ │ │ └── CameraUtils.java │ │ │ └── widgets │ │ │ ├── AutoFitGLSurfaceView.java │ │ │ └── ProgressView.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ ├── record_back.png │ │ ├── record_camera_flash_led_off_disable.png │ │ ├── record_camera_flash_led_off_normal.png │ │ ├── record_camera_flash_led_off_pressed.png │ │ ├── record_camera_flash_led_on_disable.png │ │ ├── record_camera_flash_led_on_normal.png │ │ ├── record_camera_flash_led_on_pressed.png │ │ ├── record_camera_switch_disable.png │ │ ├── record_camera_switch_normal.png │ │ ├── record_camera_switch_pressed.png │ │ ├── record_cancel_normal.png │ │ ├── record_cancel_press.png │ │ └── record_ok.png │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── record_camera_flash_led_selector.xml │ │ ├── record_camera_switch_selector.xml │ │ ├── record_cancel_seletor.xml │ │ ├── record_shape_oval_gray.xml │ │ └── record_shape_oval_white.xml │ │ ├── layout │ │ └── activity_camera_record.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── felix │ └── glcamera │ └── ExampleUnitTest.java ├── build.gradle ├── doc ├── preview.gif └── record.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | .idea/codeStyles/Project.xml 11 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GLCamera 2 | 3 | ### 多种滤镜 后期仍会扩展 4 | 5 | ![img](https://github.com/ITcrazywgy/GLCamera/blob/master/doc/preview.gif) 6 | 7 | ### 支持相机录制 8 | 9 | ![img](https://github.com/ITcrazywgy/GLCamera/blob/master/doc/record.gif) 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | defaultConfig { 6 | applicationId "com.felix.glcamera" 7 | minSdkVersion 21 8 | targetSdkVersion 22 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:26.1.0' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.0' 25 | implementation 'com.android.support:design:26.1.0' 26 | testImplementation 'junit:junit:4.12' 27 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 28 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/felix/glcamera/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.felix.glcamera", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/CameraRecordActivity.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.app.Activity; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.os.Environment; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.view.Display; 11 | import android.view.Surface; 12 | import android.view.View; 13 | import android.view.Window; 14 | import android.view.WindowManager; 15 | import android.view.animation.AccelerateInterpolator; 16 | import android.widget.AdapterView; 17 | import android.widget.ArrayAdapter; 18 | import android.widget.CheckBox; 19 | import android.widget.ImageButton; 20 | import android.widget.Spinner; 21 | import android.widget.TextView; 22 | import android.widget.Toast; 23 | 24 | import com.felix.glcamera.util.CameraUtils; 25 | import com.felix.glcamera.widgets.AutoFitGLSurfaceView; 26 | import com.felix.glcamera.widgets.ProgressView; 27 | 28 | import java.io.File; 29 | 30 | public class CameraRecordActivity extends Activity implements View.OnClickListener, CameraRecorderHelper.OnPreviewListener, CameraRecorderHelper.OnErrorListener { 31 | 32 | 33 | private CheckBox mCameraSwitch; 34 | private CheckBox mRecordLed; 35 | private ImageButton mRecordCancel; 36 | private ImageButton mRecordConfirm; 37 | private TextView mRecordGestureHint; 38 | private AutoFitGLSurfaceView mGLView; 39 | private ProgressView mProgressView; 40 | private Spinner mSpinnerFilter; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setFullScreen(getWindow()); 46 | setContentView(R.layout.activity_camera_record); 47 | initViews(); 48 | } 49 | 50 | 51 | private void initViews() { 52 | mRecordGestureHint = findViewById(R.id.record_gesture_hint); 53 | mGLView = findViewById(R.id.record_preview); 54 | mCameraSwitch = findViewById(R.id.record_camera_switcher); 55 | mProgressView = findViewById(R.id.record_progress); 56 | mRecordLed = findViewById(R.id.record_camera_led); 57 | mRecordCancel = findViewById(R.id.record_cancel); 58 | mRecordConfirm = findViewById(R.id.record_ok); 59 | mRecordCancel.setOnClickListener(this); 60 | mRecordConfirm.setOnClickListener(this); 61 | 62 | findViewById(R.id.title_back).setOnClickListener(this); 63 | 64 | //是否支持前置摄像头 65 | if (CameraUtils.isSupportFront()) { 66 | mCameraSwitch.setOnClickListener(this); 67 | } else { 68 | mCameraSwitch.setEnabled(false); 69 | } 70 | //是否支持闪光灯 71 | if (CameraUtils.isSupportLedFlash(getPackageManager())) { 72 | mRecordLed.setOnClickListener(this); 73 | } else { 74 | mRecordLed.setEnabled(false); 75 | } 76 | 77 | mRecordGestureHint.postDelayed(new Runnable() { 78 | @Override 79 | public void run() { 80 | mRecordGestureHint.setVisibility(View.INVISIBLE); 81 | } 82 | }, 5000); 83 | 84 | mSpinnerFilter = findViewById(R.id.record_camera_filter); 85 | initSpinner(); 86 | } 87 | 88 | 89 | private void initSpinner() { 90 | ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.cameraFilterNames, android.R.layout.simple_spinner_item); 91 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 92 | mSpinnerFilter.setAdapter(adapter); 93 | mSpinnerFilter.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 94 | @Override 95 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 96 | Spinner spinner = (Spinner) parent; 97 | final int filterNum = spinner.getSelectedItemPosition(); 98 | if (mRecorderHelper != null) { 99 | switch (filterNum) { 100 | case 0: 101 | mRecorderHelper.changeFilterMode(FilterType.FILTER_NORMAL); 102 | break; 103 | case 1: 104 | mRecorderHelper.changeFilterMode(FilterType.FILTER_BLACK_WHITE); 105 | break; 106 | case 2: 107 | mRecorderHelper.changeFilterMode(FilterType.FILTER_BLUR); 108 | break; 109 | case 3: 110 | mRecorderHelper.changeFilterMode(FilterType.FILTER_SHARPEN); 111 | break; 112 | case 4: 113 | mRecorderHelper.changeFilterMode(FilterType.FILTER_EDGE_DETECT); 114 | break; 115 | case 5: 116 | mRecorderHelper.changeFilterMode(FilterType.FILTER_EMBOSS); 117 | break; 118 | } 119 | } 120 | } 121 | 122 | @Override 123 | public void onNothingSelected(AdapterView parent) { 124 | 125 | } 126 | }); 127 | } 128 | 129 | 130 | @Override 131 | public void onWindowFocusChanged(boolean hasFocus) { 132 | super.onWindowFocusChanged(hasFocus); 133 | if (!isFinishing()) { 134 | setFullScreen(getWindow()); 135 | } 136 | } 137 | 138 | protected void setFullScreen(Window window) { 139 | //隐藏虚拟按键,并且全屏 140 | if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19) { 141 | window.getDecorView().setSystemUiVisibility(View.GONE); 142 | } else if (Build.VERSION.SDK_INT >= 19) { 143 | window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN); 144 | } 145 | } 146 | 147 | private CameraRecorderHelper mRecorderHelper; 148 | 149 | @Override 150 | protected void onResume() { 151 | super.onResume(); 152 | setFullScreen(getWindow()); 153 | if (mRecorderHelper == null) { 154 | initMediaRecorderHelper(); 155 | } else { 156 | if (mRecorderHelper.isVideoExists()) { 157 | mRecorderHelper.startPlay(); 158 | } else { 159 | mRecordLed.setChecked(false); 160 | mRecorderHelper.startPreview(); 161 | } 162 | } 163 | mGLView.onResume(); 164 | } 165 | 166 | @Override 167 | protected void onPause() { 168 | super.onPause(); 169 | mRecorderHelper.stopPreview(); 170 | mRecorderHelper.stopRecord(); 171 | mRecorderHelper.stopPlay(); 172 | mRecorderHelper.release(); 173 | mGLView.onPause(); 174 | } 175 | 176 | private static final int RECORD_TIME_MAX = 10 * 1000; 177 | public final static int RECORD_TIME_MIN = 1000; 178 | private int mMaxDuration = RECORD_TIME_MAX; 179 | 180 | private void initMediaRecorderHelper() { 181 | mRecorderHelper = new CameraRecorderHelper(); 182 | mRecorderHelper.setOnErrorListener(this); 183 | mRecorderHelper.setOnPreparedListener(this); 184 | mRecorderHelper.setPreviewDisplay(mGLView); 185 | mRecorderHelper.startPreview(); 186 | mProgressView.setMaxDuration(mMaxDuration); 187 | 188 | mProgressView.setOnProgressListener(new ProgressView.OnProgressListener() { 189 | @Override 190 | public void onProgressStart() { 191 | startRecord(); 192 | } 193 | 194 | @Override 195 | public void onProgressCancel() { 196 | mRecorderHelper.stopRecord(); 197 | mRecorderHelper.deleteVideoObject(); 198 | } 199 | 200 | @Override 201 | public void onProgressEnd(float progress, long duration) { 202 | mRecorderHelper.stopRecord(); 203 | mRecorderHelper.stopPreview(); 204 | checkStatus(); 205 | } 206 | }); 207 | } 208 | 209 | private void checkStatus() { 210 | if (!isFinishing()) { 211 | long duration = mRecorderHelper.getVideoDuration(); 212 | if (duration < RECORD_TIME_MIN) { 213 | Toast.makeText(this, "录制时间太短,请重新录制", Toast.LENGTH_SHORT).show(); 214 | mRecorderHelper.deleteVideoObject(); 215 | mProgressView.reset(); 216 | mRecorderHelper.startPreview(); 217 | } else { 218 | //显示提交按钮,同时播放录制视频 219 | mProgressView.reset(); 220 | mRecorderHelper.startPlay(); 221 | showNextStep(); 222 | } 223 | } 224 | } 225 | 226 | private void showNextStep() { 227 | mProgressView.setVisibility(View.INVISIBLE); 228 | mRecordConfirm.setVisibility(View.VISIBLE); 229 | mRecordCancel.setVisibility(View.VISIBLE); 230 | mRecordConfirm.setTranslationX(mProgressView.getX() - mRecordConfirm.getX()); 231 | mRecordConfirm 232 | .animate() 233 | .translationX(0) 234 | .setListener(new AnimatorListenerAdapter() { 235 | @Override 236 | public void onAnimationEnd(Animator animation) { 237 | mRecordConfirm.setVisibility(View.VISIBLE); 238 | } 239 | }) 240 | .setInterpolator(new AccelerateInterpolator()) 241 | .start(); 242 | mRecordCancel.setTranslationX(mProgressView.getX() - mRecordCancel.getX()); 243 | mRecordCancel 244 | .animate() 245 | .translationX(0) 246 | .setListener(new AnimatorListenerAdapter() { 247 | @Override 248 | public void onAnimationEnd(Animator animation) { 249 | mRecordCancel.setVisibility(View.VISIBLE); 250 | } 251 | }) 252 | .setInterpolator(new AccelerateInterpolator()) 253 | .start(); 254 | } 255 | 256 | private void hideNextStep() { 257 | mProgressView.setVisibility(View.VISIBLE); 258 | mRecordCancel.setVisibility(View.INVISIBLE); 259 | mRecordConfirm.setVisibility(View.INVISIBLE); 260 | } 261 | 262 | private void startRecord() { 263 | String key = String.valueOf(System.currentTimeMillis()); 264 | mRecorderHelper.setOutputDirectory(new File(Environment.getExternalStorageDirectory(), "AV/" + key).getAbsolutePath(), key); 265 | mRecorderHelper.startRecord(); 266 | mCameraSwitch.setVisibility(View.GONE); 267 | mRecordLed.setVisibility(View.GONE); 268 | mSpinnerFilter.setVisibility(View.GONE); 269 | mRecordGestureHint.setVisibility(View.INVISIBLE); 270 | } 271 | 272 | 273 | @Override 274 | protected void onDestroy() { 275 | super.onDestroy(); 276 | } 277 | 278 | 279 | @Override 280 | public void onClick(View v) { 281 | switch (v.getId()) { 282 | case R.id.title_back: 283 | onBackPressed(); 284 | break; 285 | case R.id.record_camera_switcher:// 前后摄像头切换 286 | if (mRecordLed.isChecked()) { 287 | mRecorderHelper.toggleFlashMode(); 288 | mRecordLed.setChecked(false); 289 | } 290 | mRecorderHelper.switchCamera(); 291 | if (mRecorderHelper.isFrontCamera()) { 292 | mRecordLed.setEnabled(false); 293 | } else { 294 | mRecordLed.setEnabled(true); 295 | } 296 | break; 297 | case R.id.record_camera_led://闪光灯 298 | if (!mRecorderHelper.isFrontCamera()) { 299 | mRecorderHelper.toggleFlashMode(); 300 | } 301 | break; 302 | case R.id.record_ok: 303 | //sendVideo(); 304 | break; 305 | case R.id.record_cancel: 306 | mRecorderHelper.deleteVideoObject(); 307 | mRecorderHelper.stopPlay(); 308 | mRecorderHelper.startPreview(); 309 | mProgressView.reset(); 310 | initSpinner(); 311 | hideNextStep(); 312 | break; 313 | } 314 | } 315 | 316 | @Override 317 | public void onPreviewStarted(int previewWidth, int previewHeight) { 318 | this.mCameraSwitch.setVisibility(View.VISIBLE); 319 | this.mRecordLed.setVisibility(View.VISIBLE); 320 | this.mSpinnerFilter.setVisibility(View.VISIBLE); 321 | 322 | Display display = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay(); 323 | int rotation = display.getRotation(); 324 | if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { 325 | mGLView.setAspectRatio(previewHeight, previewWidth); 326 | } else { 327 | mGLView.setAspectRatio(previewWidth, previewHeight); 328 | } 329 | } 330 | 331 | 332 | @Override 333 | public void onError(int what, String msg) { 334 | Toast.makeText(this, "录制视频失败", Toast.LENGTH_SHORT).show(); 335 | mRecorderHelper.deleteVideoObject(); 336 | mProgressView.reset(); 337 | finish(); 338 | } 339 | 340 | 341 | } 342 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/CameraRecorderHelper.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.SurfaceTexture; 5 | import android.hardware.Camera; 6 | import android.hardware.Camera.CameraInfo; 7 | import android.hardware.Camera.Parameters; 8 | import android.hardware.Camera.Size; 9 | import android.media.AudioManager; 10 | import android.media.MediaMetadataRetriever; 11 | import android.media.MediaPlayer; 12 | import android.media.ThumbnailUtils; 13 | import android.opengl.GLSurfaceView; 14 | import android.os.Build; 15 | import android.os.Handler; 16 | import android.os.Looper; 17 | import android.os.Message; 18 | import android.text.TextUtils; 19 | import android.util.Log; 20 | import android.view.Surface; 21 | 22 | import com.felix.glcamera.gles.FullFrameRect; 23 | import com.felix.glcamera.gles.Texture2dProgram; 24 | import com.felix.glcamera.util.CameraUtils; 25 | 26 | import java.io.File; 27 | import java.io.FileOutputStream; 28 | import java.io.IOException; 29 | import java.io.Serializable; 30 | import java.util.Collections; 31 | import java.util.List; 32 | 33 | import javax.microedition.khronos.egl.EGLConfig; 34 | import javax.microedition.khronos.opengles.GL10; 35 | 36 | import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO; 37 | import static android.hardware.Camera.Parameters.WHITE_BALANCE_AUTO; 38 | import static com.felix.glcamera.FilterType.FILTER_NONE; 39 | import static com.felix.glcamera.FilterType.FILTER_NORMAL; 40 | 41 | 42 | @SuppressWarnings("ALL") 43 | public class CameraRecorderHelper { 44 | 45 | private static final int ERROR_CAMERA_OPEN = 1; 46 | private static final int ERROR_CAMERA_EXISTED = 2; 47 | private static final int ERROR_CAMERA_PREVIEW = 3; 48 | private static final int ERROR_FILE_CREATE = 4; 49 | private static final int MAX_FRAME_RATE = 30; 50 | 51 | private Camera mCamera; 52 | private Parameters mParameters; 53 | private VideoObject mVideoObject; 54 | private OnErrorListener mOnErrorListener; 55 | private OnPreviewListener mOnPreparedListener; 56 | private int mFrameRate = 15; 57 | private int mCameraId = CameraInfo.CAMERA_FACING_BACK; 58 | private boolean mStartPreview; 59 | private boolean mStartPlay; 60 | 61 | private MediaRecorder mMediaRecorder; 62 | 63 | private GLSurfaceView mGLSurfaceView; 64 | 65 | private CameraSurfaceRenderer mRenderer; 66 | 67 | public void setPreviewDisplay(GLSurfaceView glSurfaceView) { 68 | this.mGLSurfaceView = glSurfaceView; 69 | mGLSurfaceView.setEGLContextClientVersion(2); 70 | mRenderer = new CameraSurfaceRenderer(); 71 | mGLSurfaceView.setRenderer(mRenderer); 72 | mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 73 | } 74 | 75 | 76 | public void setOutputDirectory(String outputDirectory, String key) { 77 | if (!TextUtils.isEmpty(outputDirectory)) { 78 | File outputDir = new File(outputDirectory); 79 | deleteFileIfExists(outputDir); 80 | boolean result = outputDir.mkdirs(); 81 | if (result) { 82 | this.mVideoObject = new VideoObject(outputDirectory, key); 83 | } else { 84 | mOnErrorListener.onError(ERROR_FILE_CREATE, "unable to create file"); 85 | } 86 | } 87 | } 88 | 89 | private volatile boolean isRecording = false; 90 | private volatile boolean isPlaying = false; 91 | 92 | public void startRecord() { 93 | if (!isRecording) { 94 | mMediaRecorder = new MediaRecorder(mGLSurfaceView); 95 | mMediaRecorder.setOutputFile(this.getVideoPath()); 96 | mMediaRecorder.setVideoEncodingBitRate(2 * 1024 * 1024); 97 | mMediaRecorder.setVideoSize(mPreviewHeight, mPreviewWidth); 98 | mMediaRecorder.setFilterType(mRenderer.mCurrentFilter); 99 | mMediaRecorder.start(); 100 | isRecording = true; 101 | } 102 | } 103 | 104 | 105 | public void stopRecord() { 106 | if (isRecording) { 107 | stopPreview(); 108 | if (mMediaRecorder != null) { 109 | mMediaRecorder.stop(); 110 | mMediaRecorder = null; 111 | } 112 | isRecording = false; 113 | } 114 | } 115 | 116 | 117 | void setOnPreparedListener(OnPreviewListener preparedListener) { 118 | this.mOnPreparedListener = preparedListener; 119 | } 120 | 121 | void setOnErrorListener(OnErrorListener var1) { 122 | this.mOnErrorListener = var1; 123 | } 124 | 125 | boolean isFrontCamera() { 126 | return this.mCameraId == 1; 127 | } 128 | 129 | private void switchCamera(int cameraId) { 130 | switch (cameraId) { 131 | case CameraInfo.CAMERA_FACING_BACK: 132 | case CameraInfo.CAMERA_FACING_FRONT: 133 | this.mCameraId = cameraId; 134 | this.stopPreview(); 135 | this.startPreview(); 136 | default: 137 | } 138 | } 139 | 140 | void switchCamera() { 141 | if (this.mCameraId == CameraInfo.CAMERA_FACING_BACK) { 142 | this.switchCamera(CameraInfo.CAMERA_FACING_FRONT); 143 | } else { 144 | this.switchCamera(CameraInfo.CAMERA_FACING_BACK); 145 | } 146 | } 147 | 148 | 149 | void toggleFlashMode() { 150 | if (this.mParameters != null) { 151 | try { 152 | String flashMode; 153 | if (!TextUtils.isEmpty(flashMode = this.mParameters.getFlashMode()) && !Parameters.FLASH_MODE_OFF.equals(flashMode)) { 154 | this.setFlashMode(Parameters.FLASH_MODE_OFF); 155 | } else { 156 | this.setFlashMode(Parameters.FLASH_MODE_TORCH); 157 | } 158 | } catch (Exception e) { 159 | e.printStackTrace(); 160 | } 161 | } 162 | } 163 | 164 | private void setFlashMode(String flashMode) { 165 | if (this.mParameters != null && this.mCamera != null) { 166 | try { 167 | if (Parameters.FLASH_MODE_TORCH.equals(flashMode) || Parameters.FLASH_MODE_OFF.equals(flashMode)) { 168 | this.mParameters.setFlashMode(flashMode); 169 | this.mCamera.setParameters(this.mParameters); 170 | } 171 | } catch (Exception e) { 172 | e.printStackTrace(); 173 | } 174 | } 175 | } 176 | 177 | 178 | void deleteVideoObject() { 179 | if (this.mVideoObject != null && !TextUtils.isEmpty(this.mVideoObject.getOutputDirectory())) { 180 | deleteFileIfExists(new File(this.mVideoObject.getOutputDirectory())); 181 | } 182 | this.mVideoObject = null; 183 | } 184 | 185 | private static final String TAG = "MediaRecorderHelper"; 186 | 187 | private void deleteFileIfExists(File file) { 188 | if (file == null || !file.exists()) return; 189 | if (file.isDirectory()) { 190 | File[] files = file.listFiles(); 191 | if (files != null) { 192 | for (File subFile : files) { 193 | if (subFile.isDirectory()) { 194 | deleteFileIfExists(subFile); 195 | } 196 | boolean delete = subFile.delete(); 197 | if (!delete) { 198 | Log.e(TAG, "error occurred when deleting file"); 199 | } 200 | } 201 | } 202 | } 203 | boolean delete = file.delete(); 204 | if (!delete) { 205 | Log.e(TAG, "error occurred when deleting file"); 206 | } 207 | } 208 | 209 | private boolean isSupported(List list, String target) { 210 | return list != null && list.contains(target); 211 | } 212 | 213 | private int mPreviewWidth = -1; 214 | private int mPreviewHeight = -1; 215 | 216 | private void prepareCameraParameters() { 217 | if (this.mParameters != null) { 218 | //设置预览帧率 219 | List supportedFrameRates = this.mParameters.getSupportedPreviewFrameRates(); 220 | if (supportedFrameRates != null) { 221 | if (supportedFrameRates.contains(MAX_FRAME_RATE)) { 222 | this.mFrameRate = MAX_FRAME_RATE; 223 | } else { 224 | Collections.sort(supportedFrameRates); 225 | for (int i = supportedFrameRates.size() - 1; i >= 0; --i) { 226 | if (supportedFrameRates.get(i) <= MAX_FRAME_RATE) { 227 | this.mFrameRate = supportedFrameRates.get(i); 228 | break; 229 | } 230 | } 231 | if (this.mFrameRate == -1) { 232 | this.mFrameRate = supportedFrameRates.get(0); 233 | } 234 | } 235 | } 236 | this.mParameters.setPreviewFrameRate(this.mFrameRate); 237 | //设置预览尺寸 238 | List supportedPreviewSizes = this.mParameters.getSupportedPreviewSizes(); 239 | int preferPreviewWidth = mGLSurfaceView.getHeight(); 240 | int preferPreviewHeight = mGLSurfaceView.getWidth(); 241 | Size previewSize = CameraUtils.chooseOptimalSize(supportedPreviewSizes, preferPreviewWidth, preferPreviewHeight); 242 | this.mPreviewWidth = previewSize.width; 243 | this.mPreviewHeight = previewSize.height; 244 | this.mParameters.setPreviewSize(this.mPreviewWidth, this.mPreviewHeight); 245 | if (isSupported(this.mParameters.getSupportedFocusModes(), FOCUS_MODE_CONTINUOUS_VIDEO)) { 246 | this.mParameters.setFocusMode(FOCUS_MODE_CONTINUOUS_VIDEO); 247 | } 248 | if (isSupported(this.mParameters.getSupportedWhiteBalance(), WHITE_BALANCE_AUTO)) { 249 | this.mParameters.setWhiteBalance(WHITE_BALANCE_AUTO); 250 | } 251 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { 252 | if (this.mParameters.isVideoStabilizationSupported()) { 253 | this.mParameters.setVideoStabilization(true); 254 | } 255 | } 256 | if (!CameraUtils.isDevice("GT-N7100", "GT-I9308", "GT-I9300")) { 257 | this.mParameters.set("cam_mode", 1); 258 | this.mParameters.set("cam-mode", 1); 259 | } 260 | if (!CameraUtils.isDevice("GT-I9100")) 261 | this.mParameters.setRecordingHint(true); 262 | } 263 | } 264 | 265 | private void retrieveMetaData() { 266 | if (mVideoObject != null && !TextUtils.isEmpty(mVideoObject.getVideoPath()) && new File(mVideoObject.getVideoPath()).exists()) { 267 | MediaMetadataRetriever retr = new MediaMetadataRetriever(); 268 | retr.setDataSource(mVideoObject.getVideoPath()); 269 | String sWidth = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); 270 | String sHeight = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); 271 | String sDuration = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); 272 | mVideoObject.setVideoWidth((int) parseFloat(sWidth)); 273 | mVideoObject.setVideoHeight((int) parseFloat(sHeight)); 274 | mVideoObject.setVideoDuration((int) parseFloat(sDuration)); 275 | } 276 | } 277 | 278 | private float parseFloat(String sValue) { 279 | try { 280 | return (int) Float.parseFloat(sValue); 281 | } catch (Exception e) { 282 | return 0; 283 | } 284 | } 285 | 286 | 287 | public boolean isVideoExists() { 288 | return mVideoObject != null && new File(mVideoObject.getVideoPath()).exists(); 289 | } 290 | 291 | public int getVideoDuration() { 292 | if (mVideoObject != null) { 293 | int duration = mVideoObject.getVideoDuration(); 294 | if (duration != 0) { 295 | return duration; 296 | } else { 297 | retrieveMetaData(); 298 | return mVideoObject.getVideoDuration(); 299 | } 300 | } 301 | return 0; 302 | } 303 | 304 | public int getVideoWidth() { 305 | if (mVideoObject != null) { 306 | int videoWidth = mVideoObject.getVideoWidth(); 307 | if (videoWidth != 0) { 308 | return videoWidth; 309 | } else { 310 | retrieveMetaData(); 311 | return mVideoObject.getVideoWidth(); 312 | } 313 | } 314 | return 0; 315 | } 316 | 317 | public int getVideoHeight() { 318 | if (mVideoObject != null) { 319 | int videoHeight = mVideoObject.getVideoHeight(); 320 | if (videoHeight != 0) { 321 | return videoHeight; 322 | } else { 323 | retrieveMetaData(); 324 | return mVideoObject.getVideoHeight(); 325 | } 326 | } 327 | return 0; 328 | } 329 | 330 | 331 | public String getVideoThumbnail() { 332 | if (mVideoObject != null) { 333 | if (TextUtils.isEmpty(mVideoObject.getVideoThumbPath()) || !new File(mVideoObject.getVideoThumbPath()).exists()) { 334 | if (new File(mVideoObject.getVideoPath()).exists() && createVideoThumbnail(mVideoObject.getVideoPath(), mVideoObject.getVideoThumbPath())) { 335 | return mVideoObject.getVideoThumbPath(); 336 | } 337 | } 338 | } 339 | return null; 340 | } 341 | 342 | public String getVideoPath() { 343 | if (mVideoObject != null) { 344 | return mVideoObject.getVideoPath(); 345 | } 346 | return null; 347 | } 348 | 349 | private boolean createVideoThumbnail(String videoPath, String videoThumbPath) { 350 | if (TextUtils.isEmpty(videoThumbPath)) return false; 351 | File file = new File(videoThumbPath); 352 | if (file.exists()) { 353 | return true; 354 | } 355 | Bitmap bitmap = null; 356 | FileOutputStream fos = null; 357 | try { 358 | if (!file.getParentFile().exists()) { 359 | boolean mkdirs = file.getParentFile().mkdirs(); 360 | if (!mkdirs) return false; 361 | } 362 | bitmap = ThumbnailUtils.createVideoThumbnail(videoPath, 3); 363 | if (bitmap == null) return false; 364 | fos = new FileOutputStream(file); 365 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); 366 | return true; 367 | } catch (Exception e) { 368 | e.printStackTrace(); 369 | } finally { 370 | if (fos != null) { 371 | try { 372 | fos.close(); 373 | } catch (IOException e) { 374 | e.printStackTrace(); 375 | } 376 | } 377 | if (bitmap != null) { 378 | bitmap.recycle(); 379 | } 380 | } 381 | return true; 382 | } 383 | 384 | private MediaPlayer mMediaPlayer; 385 | 386 | public void startPlay() { 387 | if (!isPlaying) { 388 | try { 389 | if (mMediaPlayer == null) { 390 | mMediaPlayer = new MediaPlayer(); 391 | mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 392 | @Override 393 | public void onPrepared(MediaPlayer mp) { 394 | if (!mMediaPlayer.isPlaying()) { 395 | mMediaPlayer.start(); 396 | } 397 | } 398 | }); 399 | mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 400 | if (mSurfaceTexture != null) { 401 | playWithSurfaceTexture(mSurfaceTexture); 402 | } 403 | this.mStartPlay = true; 404 | } 405 | } catch (Exception e) { 406 | e.printStackTrace(); 407 | } 408 | isPlaying = true; 409 | } 410 | } 411 | 412 | public void stopPlay() { 413 | this.mStartPlay = false; 414 | this.isPlaying = false; 415 | if (mMediaPlayer != null) { 416 | mMediaPlayer.stop(); 417 | mMediaPlayer.release(); 418 | mMediaPlayer = null; 419 | } 420 | } 421 | 422 | public void startPreview() { 423 | if (this.mCamera != null) { 424 | if (this.mOnErrorListener != null) { 425 | this.mOnErrorListener.onError(ERROR_CAMERA_EXISTED, "camera already initialized"); 426 | } 427 | return; 428 | } 429 | if (this.mCameraId == CameraInfo.CAMERA_FACING_BACK) { 430 | this.mCamera = Camera.open(CameraInfo.CAMERA_FACING_BACK); 431 | } else { 432 | this.mCamera = Camera.open(CameraInfo.CAMERA_FACING_FRONT); 433 | } 434 | if (mCamera == null) { 435 | if (this.mOnErrorListener != null) { 436 | this.mOnErrorListener.onError(ERROR_CAMERA_OPEN, "Unable to open camera"); 437 | } 438 | return; 439 | } 440 | this.mCamera.setDisplayOrientation(90); 441 | if (this.mSurfaceTexture != null) { 442 | previewWithSurfaceTexture(this.mSurfaceTexture); 443 | } 444 | this.mStartPreview = true; 445 | } 446 | 447 | private SurfaceTexture mSurfaceTexture; 448 | 449 | private void handleSurfaceTexturePrepared(SurfaceTexture st) { 450 | if (mStartPreview) { 451 | this.mSurfaceTexture = st; 452 | previewWithSurfaceTexture(st); 453 | } else if (mStartPlay) { 454 | this.mSurfaceTexture = st; 455 | playWithSurfaceTexture(st); 456 | } 457 | } 458 | 459 | private void playWithSurfaceTexture(SurfaceTexture st) { 460 | try { 461 | Surface surface = new Surface(mSurfaceTexture); 462 | mMediaPlayer.setSurface(surface); 463 | st.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { 464 | @Override 465 | public void onFrameAvailable(SurfaceTexture surfaceTexture) { 466 | mGLSurfaceView.requestRender(); 467 | } 468 | }); 469 | final int videoWidth = mVideoObject.getVideoWidth(); 470 | final int videoHeight = mVideoObject.getVideoHeight(); 471 | this.mGLSurfaceView.queueEvent(new Runnable() { 472 | @Override 473 | public void run() { 474 | mRenderer.setCameraPreviewSize(videoWidth, videoHeight); 475 | } 476 | }); 477 | mMediaPlayer.setDataSource(mVideoObject.getVideoPath()); 478 | mMediaPlayer.setLooping(true); 479 | mMediaPlayer.prepareAsync(); 480 | } catch (IOException e) { 481 | e.printStackTrace(); 482 | } 483 | } 484 | 485 | private void previewWithSurfaceTexture(SurfaceTexture st) { 486 | try { 487 | this.mParameters = this.mCamera.getParameters(); 488 | this.prepareCameraParameters(); 489 | this.mCamera.setParameters(this.mParameters); 490 | this.mGLSurfaceView.queueEvent(new Runnable() { 491 | @Override 492 | public void run() { 493 | mRenderer.setCameraPreviewSize(mPreviewWidth, mPreviewHeight); 494 | } 495 | }); 496 | st.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { 497 | @Override 498 | public void onFrameAvailable(SurfaceTexture surfaceTexture) { 499 | mGLSurfaceView.requestRender(); 500 | } 501 | }); 502 | this.mCamera.setPreviewTexture(st); 503 | this.mCamera.startPreview(); 504 | 505 | if (this.mOnPreparedListener != null) { 506 | this.mOnPreparedListener.onPreviewStarted(this.mPreviewWidth, this.mPreviewHeight); 507 | } 508 | 509 | } catch (IOException ioe) { 510 | if (this.mOnErrorListener != null) { 511 | this.mOnErrorListener.onError(ERROR_CAMERA_PREVIEW, "Unable to preview"); 512 | } 513 | } 514 | } 515 | 516 | 517 | public void stopPreview() { 518 | this.mStartPreview = false; 519 | if (this.mCamera != null) { 520 | try { 521 | this.mCamera.stopPreview(); 522 | this.mCamera.setPreviewCallback(null); 523 | this.mCamera.release(); 524 | } catch (Exception e) { 525 | e.printStackTrace(); 526 | } 527 | this.mCamera = null; 528 | } 529 | } 530 | 531 | public void release() { 532 | mSurfaceTexture = null; 533 | this.mGLSurfaceView.queueEvent(new Runnable() { 534 | @Override 535 | public void run() { 536 | mRenderer.release(); 537 | } 538 | }); 539 | } 540 | 541 | public void changeFilterMode(final FilterType filterType) { 542 | mGLSurfaceView.queueEvent(new Runnable() { 543 | @Override 544 | public void run() { 545 | mRenderer.changeFilterMode(filterType); 546 | } 547 | }); 548 | } 549 | 550 | public interface OnErrorListener { 551 | void onError(int what, String msg); 552 | } 553 | 554 | public interface OnPreviewListener { 555 | void onPreviewStarted(int previewWidth, int previewHeight); 556 | } 557 | 558 | private static class VideoObject implements Serializable { 559 | private static final long serialVersionUID = -3584369940642260675L; 560 | private String outputDirectory; 561 | private String outputVideoThumbPath; 562 | private String outputVideoPath; 563 | 564 | private int videoWidth; 565 | private int videoHeight; 566 | private int videoDuration; 567 | 568 | 569 | VideoObject(String outputDirectory, String key) { 570 | this.outputDirectory = outputDirectory; 571 | this.outputVideoPath = this.outputDirectory + File.separator + key + ".mp4"; 572 | this.outputVideoThumbPath = this.outputDirectory + File.separator + key + "_thumb.jpg"; 573 | } 574 | 575 | 576 | String getVideoThumbPath() { 577 | return outputVideoThumbPath; 578 | } 579 | 580 | String getOutputDirectory() { 581 | return outputDirectory; 582 | } 583 | 584 | String getVideoPath() { 585 | return outputVideoPath; 586 | } 587 | 588 | 589 | int getVideoWidth() { 590 | return videoWidth; 591 | } 592 | 593 | void setVideoWidth(int videoWidth) { 594 | this.videoWidth = videoWidth; 595 | } 596 | 597 | int getVideoHeight() { 598 | return videoHeight; 599 | } 600 | 601 | void setVideoHeight(int videoHeight) { 602 | this.videoHeight = videoHeight; 603 | } 604 | 605 | int getVideoDuration() { 606 | return videoDuration; 607 | } 608 | 609 | void setVideoDuration(int videoDuration) { 610 | this.videoDuration = videoDuration; 611 | } 612 | 613 | } 614 | 615 | 616 | private class CameraSurfaceRenderer implements GLSurfaceView.Renderer { 617 | 618 | private static final int RECORDING_OFF = 0; 619 | private static final int RECORDING_ON = 1; 620 | private static final int RECORDING_RESUMED = 2; 621 | private FullFrameRect mFullScreen; 622 | private final float[] mTexMatrix = new float[16]; 623 | private int mTextureId; 624 | private SurfaceTexture mSurfaceTexture; 625 | private boolean mRecordingEnabled; 626 | private int mRecordingStatus; 627 | private boolean mIncomingSizeUpdated; 628 | private int mIncomingWidth; 629 | private int mIncomingHeight; 630 | private FilterType mCurrentFilter; 631 | private FilterType mNewFilter; 632 | private SurfaceHandler mSurfaceHandler; 633 | 634 | CameraSurfaceRenderer() { 635 | mSurfaceHandler = new SurfaceHandler(Looper.getMainLooper()); 636 | mTextureId = -1; 637 | mRecordingStatus = -1; 638 | mRecordingEnabled = false; 639 | 640 | mIncomingSizeUpdated = false; 641 | mIncomingWidth = mIncomingHeight = -1; 642 | 643 | // We could preserve the old filter mode, but currently not bothering. 644 | mCurrentFilter = FILTER_NONE; 645 | mNewFilter = FilterType.FILTER_NORMAL; 646 | } 647 | 648 | private void changeFilterMode(FilterType filterType) { 649 | mNewFilter = filterType; 650 | } 651 | 652 | private FilterType getFilterType() { 653 | return mCurrentFilter; 654 | } 655 | 656 | public void release() { 657 | if (mSurfaceTexture != null) { 658 | mSurfaceTexture.release(); 659 | mSurfaceTexture = null; 660 | } 661 | if (mFullScreen != null) { 662 | mFullScreen.release(false); // assume the GLSurfaceView EGL context is about 663 | mFullScreen = null; // to be destroyed 664 | } 665 | mIncomingWidth = mIncomingHeight = -1; 666 | } 667 | 668 | public void changeRecordingState(boolean isRecording) { 669 | mRecordingEnabled = isRecording; 670 | } 671 | 672 | private void updateFilter() { 673 | FilterType.FilterInfo filterInfo = FilterType.getFilterInfo(mNewFilter); 674 | Texture2dProgram.ProgramType programType = filterInfo.programType; 675 | float[] kernel = filterInfo.kernel; 676 | float colorAdj = filterInfo.colorAdj; 677 | if (programType != mFullScreen.getProgram().getProgramType()) { 678 | mFullScreen.changeProgram(new Texture2dProgram(programType)); 679 | mIncomingSizeUpdated = true; 680 | } 681 | if (kernel != null) { 682 | mFullScreen.getProgram().setKernel(kernel, colorAdj); 683 | } 684 | mCurrentFilter = mNewFilter; 685 | } 686 | 687 | void setCameraPreviewSize(int width, int height) { 688 | mIncomingWidth = width; 689 | mIncomingHeight = height; 690 | mIncomingSizeUpdated = true; 691 | } 692 | 693 | private boolean isPrepared = false; 694 | 695 | @Override 696 | public void onSurfaceCreated(GL10 gl, EGLConfig config) { 697 | this.mRecordingStatus = RECORDING_OFF; 698 | this.mFullScreen = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); 699 | this.mTextureId = this.mFullScreen.createTextureObject(); 700 | this.mSurfaceTexture = new SurfaceTexture(mTextureId); 701 | this.isPrepared = true; 702 | this.mSurfaceHandler.sendMessage(mSurfaceHandler.obtainMessage(SurfaceHandler.MSG_SET_SURFACE_TEXTURE, this.mSurfaceTexture)); 703 | } 704 | 705 | 706 | @Override 707 | public void onSurfaceChanged(GL10 gl, int width, int height) { 708 | 709 | } 710 | 711 | 712 | @Override 713 | public void onDrawFrame(GL10 gl) { 714 | mSurfaceTexture.updateTexImage(); 715 | 716 | if (mIncomingWidth <= 0 || mIncomingHeight <= 0) { 717 | return; 718 | } 719 | 720 | if (isPlaying) { 721 | mNewFilter = FILTER_NORMAL; 722 | } 723 | 724 | if (mCurrentFilter != mNewFilter) { 725 | updateFilter(); 726 | } 727 | 728 | if (mIncomingSizeUpdated) { 729 | mFullScreen.getProgram().setTexSize(mIncomingWidth, mIncomingHeight); 730 | mIncomingSizeUpdated = false; 731 | } 732 | 733 | mSurfaceTexture.getTransformMatrix(mTexMatrix); 734 | mFullScreen.drawFrame(mTextureId, mTexMatrix); 735 | 736 | if (isRecording) { 737 | if (mMediaRecorder != null) { 738 | mMediaRecorder.onFrameAvailable(mTextureId, mSurfaceTexture); 739 | } 740 | } 741 | } 742 | } 743 | 744 | 745 | private class SurfaceHandler extends Handler { 746 | private static final int MSG_SET_SURFACE_TEXTURE = 0; 747 | 748 | SurfaceHandler(Looper mainLooper) { 749 | super(mainLooper); 750 | } 751 | 752 | @Override 753 | public void handleMessage(Message inputMessage) { 754 | int what = inputMessage.what; 755 | switch (what) { 756 | case MSG_SET_SURFACE_TEXTURE: 757 | handleSurfaceTexturePrepared((SurfaceTexture) inputMessage.obj); 758 | break; 759 | default: 760 | throw new RuntimeException("unknown msg " + what); 761 | } 762 | } 763 | } 764 | } 765 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/FilterType.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera; 2 | 3 | import com.felix.glcamera.gles.Texture2dProgram; 4 | 5 | /** 6 | * Created by Felix on 2018/6/11 0011. 7 | */ 8 | public enum FilterType { 9 | FILTER_NONE, FILTER_NORMAL, FILTER_BLACK_WHITE, FILTER_BLUR, FILTER_SHARPEN, FILTER_EDGE_DETECT, FILTER_EMBOSS; 10 | 11 | public static class FilterInfo { 12 | public Texture2dProgram.ProgramType programType; 13 | public float[] kernel; 14 | public float colorAdj; 15 | 16 | public FilterInfo(Texture2dProgram.ProgramType programType, float[] kernel, float colorAdj) { 17 | this.programType = programType; 18 | this.kernel = kernel; 19 | this.colorAdj = colorAdj; 20 | } 21 | } 22 | 23 | public static FilterInfo getFilterInfo(FilterType filterType) { 24 | Texture2dProgram.ProgramType programType; 25 | float[] kernel = null; 26 | float colorAdj = 0.0f; 27 | switch (filterType) { 28 | case FILTER_NORMAL: 29 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT; 30 | break; 31 | case FILTER_BLACK_WHITE: 32 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BW; 33 | break; 34 | case FILTER_BLUR: 35 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 36 | kernel = new float[]{ 37 | 1f / 16f, 2f / 16f, 1f / 16f, 38 | 2f / 16f, 4f / 16f, 2f / 16f, 39 | 1f / 16f, 2f / 16f, 1f / 16f}; 40 | break; 41 | case FILTER_SHARPEN: 42 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 43 | kernel = new float[]{ 44 | 0f, -1f, 0f, 45 | -1f, 5f, -1f, 46 | 0f, -1f, 0f}; 47 | break; 48 | case FILTER_EDGE_DETECT: 49 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 50 | kernel = new float[]{ 51 | -1f, -1f, -1f, 52 | -1f, 8f, -1f, 53 | -1f, -1f, -1f}; 54 | break; 55 | case FILTER_EMBOSS: 56 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 57 | kernel = new float[]{ 58 | 2f, 0f, 0f, 59 | 0f, -1f, 0f, 60 | 0f, 0f, -1f}; 61 | colorAdj = 0.5f; 62 | break; 63 | case FILTER_NONE: 64 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT; 65 | break; 66 | default: 67 | throw new RuntimeException("Unknown filter mode "); 68 | } 69 | return new FilterInfo(programType, kernel, colorAdj); 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/IMediaRecorder.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera; 2 | 3 | /** 4 | * Created by Felix on 2018/6/9 23:59 5 | */ 6 | 7 | public interface IMediaRecorder { 8 | void setOutputFile(String outputFile); 9 | 10 | void setVideoSize(int width, int height); 11 | 12 | void start(); 13 | 14 | 15 | void stop(); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/MediaRecorder.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera; 2 | 3 | import android.graphics.SurfaceTexture; 4 | import android.opengl.EGL14; 5 | import android.opengl.EGLContext; 6 | import android.opengl.GLSurfaceView; 7 | 8 | import com.felix.glcamera.TextureMovieEncoder.EncoderConfig; 9 | 10 | import java.io.File; 11 | 12 | /** 13 | * Created by Felix on 2018/6/10 00:01 14 | */ 15 | 16 | public class MediaRecorder implements IMediaRecorder { 17 | private TextureMovieEncoder mVideoEncoder; 18 | private GLSurfaceView mGLSurfaceView; 19 | private String mOutputFile; 20 | private int mBitRate; 21 | private int mVideoWidth; 22 | private int mVideoHeight; 23 | private FilterType mFilterType; 24 | 25 | public MediaRecorder(GLSurfaceView gLSurfaceView) { 26 | this.mGLSurfaceView = gLSurfaceView; 27 | mVideoEncoder = new TextureMovieEncoder(); 28 | } 29 | 30 | @Override 31 | public void setOutputFile(String outputFile) { 32 | this.mOutputFile = outputFile; 33 | } 34 | 35 | public void setVideoEncodingBitRate(int bitRate) { 36 | this.mBitRate = bitRate; 37 | } 38 | 39 | @Override 40 | public void setVideoSize(int width, int height) { 41 | this.mVideoWidth = width; 42 | this.mVideoHeight = height; 43 | } 44 | 45 | public void setFilterType(FilterType filterType) { 46 | this.mFilterType = filterType; 47 | } 48 | 49 | public FilterType getFilterType() { 50 | return mFilterType; 51 | } 52 | 53 | @Override 54 | public void start() { 55 | mGLSurfaceView.queueEvent(new Runnable() { 56 | @Override 57 | public void run() { 58 | EGLContext eglContext = EGL14.eglGetCurrentContext(); 59 | EncoderConfig encoderConfig = new EncoderConfig(new File(mOutputFile), mVideoWidth, mVideoHeight, mBitRate, eglContext, getFilterType()); 60 | mVideoEncoder.startRecording(encoderConfig); 61 | } 62 | }); 63 | } 64 | 65 | @Override 66 | public void stop() { 67 | mVideoEncoder.stopRecording(); 68 | mVideoEncoder.waitForStop(); 69 | } 70 | 71 | 72 | void onFrameAvailable(int mTextureId, SurfaceTexture mSurfaceTexture) { 73 | mVideoEncoder.setTextureId(mTextureId); 74 | mVideoEncoder.frameAvailable(mSurfaceTexture); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/PermissionHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera; 18 | 19 | 20 | import android.Manifest; 21 | import android.app.Activity; 22 | import android.content.Intent; 23 | import android.content.pm.PackageManager; 24 | import android.net.Uri; 25 | import android.provider.Settings; 26 | import android.support.v4.app.ActivityCompat; 27 | import android.support.v4.content.ContextCompat; 28 | import android.widget.Toast; 29 | 30 | /** 31 | * Helper class for handling dangerous permissions for Android API level >= 23 which 32 | * requires user consent at runtime to access the camera. 33 | */ 34 | class PermissionHelper { 35 | public static final int RC_PERMISSION_REQUEST = 9222; 36 | public static boolean hasCameraPermission(Activity activity) { 37 | return ContextCompat.checkSelfPermission(activity, 38 | Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED; 39 | } 40 | public static boolean hasWriteStoragePermission(Activity activity) { 41 | return ContextCompat.checkSelfPermission(activity, 42 | Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; 43 | } 44 | public static void requestCameraPermission(Activity activity, boolean requestWritePermission) { 45 | 46 | boolean showRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, 47 | Manifest.permission.CAMERA) || (requestWritePermission && 48 | ActivityCompat.shouldShowRequestPermissionRationale(activity, 49 | Manifest.permission.WRITE_EXTERNAL_STORAGE)); 50 | if (showRationale) { 51 | Toast.makeText(activity, 52 | "Camera permission is needed to run this application", Toast.LENGTH_LONG).show(); 53 | } else { 54 | 55 | // No explanation needed, we can request the permission. 56 | 57 | String permissions[] = requestWritePermission ? new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}: new String[]{Manifest.permission.CAMERA}; 58 | ActivityCompat.requestPermissions(activity,permissions,RC_PERMISSION_REQUEST); 59 | } 60 | } 61 | 62 | public static void requestWriteStoragePermission(Activity activity) { 63 | boolean showRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, 64 | Manifest.permission.WRITE_EXTERNAL_STORAGE); 65 | if (showRationale) { 66 | Toast.makeText(activity, 67 | "Writing to external storage permission is needed to run this application", 68 | Toast.LENGTH_LONG).show(); 69 | } else { 70 | 71 | // No explanation needed, we can request the permission. 72 | 73 | String permissions[] = new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE}; 74 | 75 | ActivityCompat.requestPermissions(activity,permissions,RC_PERMISSION_REQUEST); 76 | } 77 | } 78 | 79 | /** Launch Application Setting to grant permission. */ 80 | public static void launchPermissionSettings(Activity activity) { 81 | Intent intent = new Intent(); 82 | intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 83 | intent.setData(Uri.fromParts("package", activity.getPackageName(), null)); 84 | activity.startActivity(intent); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/TextureMovieEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera; 18 | 19 | import android.graphics.SurfaceTexture; 20 | import android.opengl.EGLContext; 21 | import android.opengl.GLES20; 22 | import android.os.Handler; 23 | import android.os.Looper; 24 | import android.os.Message; 25 | import android.util.Log; 26 | 27 | 28 | import com.felix.glcamera.gles.EglCore; 29 | import com.felix.glcamera.gles.FullFrameRect; 30 | import com.felix.glcamera.gles.Texture2dProgram; 31 | import com.felix.glcamera.gles.WindowSurface; 32 | 33 | import java.io.File; 34 | import java.io.IOException; 35 | import java.lang.ref.WeakReference; 36 | 37 | /** 38 | * Encode a movie from frames rendered from an external texture image. 39 | *

40 | * The object wraps an encoder running on a dedicated thread. The various control messages 41 | * may be sent from arbitrary threads (typically the app UI thread). The encoder thread 42 | * manages both sides of the encoder (feeding and draining); the only external input is 43 | * the GL texture. 44 | *

45 | * The design is complicated slightly by the need to create an EGL context that shares state 46 | * with a view that gets restarted if (say) the device orientation changes. When the view 47 | * in question is a GLSurfaceView, we don't have full control over the EGL context creation 48 | * on that side, so we have to bend a bit backwards here. 49 | *

50 | * To use: 51 | *

    52 | *
  • create TextureMovieEncoder object 53 | *
  • create an EncoderConfig 54 | *
  • call TextureMovieEncoder#startRecording() with the config 55 | *
  • call TextureMovieEncoder#setTextureId() with the texture object that receives frames 56 | *
  • for each frame, after latching it with SurfaceTexture#updateTexImage(), 57 | * call TextureMovieEncoder#frameAvailable(). 58 | *
59 | *

60 | * TODO: tweak the API (esp. textureId) so it's less awkward for simple use cases. 61 | */ 62 | public class TextureMovieEncoder implements Runnable { 63 | private static final String TAG = "TextureMovieEncoder"; 64 | private static final boolean VERBOSE = false; 65 | 66 | private static final int MSG_START_RECORDING = 0; 67 | private static final int MSG_STOP_RECORDING = 1; 68 | private static final int MSG_FRAME_AVAILABLE = 2; 69 | private static final int MSG_SET_TEXTURE_ID = 3; 70 | private static final int MSG_UPDATE_SHARED_CONTEXT = 4; 71 | private static final int MSG_QUIT = 5; 72 | 73 | // ----- accessed exclusively by encoder thread ----- 74 | private WindowSurface mInputWindowSurface; 75 | private EglCore mEglCore; 76 | private FullFrameRect mFullScreen; 77 | private int mTextureId; 78 | private int mFrameNum; 79 | private VideoEncoderCore mVideoEncoder; 80 | 81 | // ----- accessed by multiple threads ----- 82 | private volatile EncoderHandler mHandler; 83 | 84 | private final Object mReadyFence = new Object(); // guards ready/running 85 | private boolean mReady; 86 | private boolean mRunning; 87 | 88 | 89 | /** 90 | * Encoder configuration. 91 | *

92 | * Object is immutable, which means we can safely pass it between threads without 93 | * explicit synchronization (and don't need to worry about it getting tweaked out from 94 | * under us). 95 | *

96 | * TODO: make frame rate and iframe interval configurable? Maybe use builder pattern 97 | * with reasonable defaults for those and bit rate. 98 | */ 99 | public static class EncoderConfig { 100 | final File mOutputFile; 101 | final int mWidth; 102 | final int mHeight; 103 | final int mBitRate; 104 | final EGLContext mEglContext; 105 | final FilterType mFilterType; 106 | 107 | public EncoderConfig(File outputFile, int width, int height, int bitRate, EGLContext sharedEglContext, FilterType filterType) { 108 | mOutputFile = outputFile; 109 | mWidth = width; 110 | mHeight = height; 111 | mBitRate = bitRate; 112 | mEglContext = sharedEglContext; 113 | mFilterType = filterType; 114 | } 115 | 116 | @Override 117 | public String toString() { 118 | return "EncoderConfig: " + mWidth + "x" + mHeight + " @" + mBitRate + " to '" + mOutputFile.toString() + "' ctxt=" + mEglContext; 119 | } 120 | } 121 | 122 | 123 | public void startRecording(EncoderConfig config) { 124 | Log.d(TAG, "Encoder: startRecording()"); 125 | synchronized (mReadyFence) { 126 | if (mRunning) { 127 | Log.w(TAG, "Encoder thread already running"); 128 | return; 129 | } 130 | mRunning = true; 131 | new Thread(this, "TextureMovieEncoder").start(); 132 | while (!mReady) { 133 | try { 134 | mReadyFence.wait(); 135 | } catch (InterruptedException ie) { 136 | // ignore 137 | } 138 | } 139 | } 140 | 141 | mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config)); 142 | } 143 | 144 | 145 | public void stopRecording() { 146 | mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_RECORDING)); 147 | mHandler.sendMessage(mHandler.obtainMessage(MSG_QUIT)); 148 | } 149 | 150 | public void waitForStop() { 151 | synchronized (mReadyFence) { 152 | if (!mRunning) { 153 | return; 154 | } 155 | while (mRunning) { 156 | try { 157 | mReadyFence.wait(); 158 | } catch (InterruptedException ie) { 159 | // ignore 160 | } 161 | } 162 | } 163 | } 164 | 165 | public boolean isRecording() { 166 | synchronized (mReadyFence) { 167 | return mRunning; 168 | } 169 | } 170 | 171 | public void updateSharedContext(EGLContext sharedContext) { 172 | mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SHARED_CONTEXT, sharedContext)); 173 | } 174 | 175 | public void frameAvailable(SurfaceTexture st) { 176 | synchronized (mReadyFence) { 177 | if (!mReady) { 178 | return; 179 | } 180 | } 181 | float[] transform = new float[16]; // TODO - avoid alloc every frame 182 | st.getTransformMatrix(transform); 183 | long timestamp = st.getTimestamp(); 184 | if (timestamp == 0) { 185 | return; 186 | } 187 | mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE, (int) (timestamp >> 32), (int) timestamp, transform)); 188 | } 189 | 190 | 191 | public void setTextureId(int id) { 192 | synchronized (mReadyFence) { 193 | if (!mReady) { 194 | return; 195 | } 196 | } 197 | mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_TEXTURE_ID, id, 0, null)); 198 | } 199 | 200 | /** 201 | * Encoder thread entry point. Establishes Looper/Handler and waits for messages. 202 | *

203 | * 204 | * @see Thread#run() 205 | */ 206 | @Override 207 | public void run() { 208 | // Establish a Looper for this thread, and define a Handler for it. 209 | Looper.prepare(); 210 | synchronized (mReadyFence) { 211 | mHandler = new EncoderHandler(this); 212 | mReady = true; 213 | mReadyFence.notify(); 214 | } 215 | Looper.loop(); 216 | 217 | Log.d(TAG, "Encoder thread exiting"); 218 | synchronized (mReadyFence) { 219 | mReady = mRunning = false; 220 | mHandler = null; 221 | mReadyFence.notify(); 222 | } 223 | } 224 | 225 | 226 | /** 227 | * Handles encoder state change requests. The handler is created on the encoder thread. 228 | */ 229 | private static class EncoderHandler extends Handler { 230 | private WeakReference mWeakEncoder; 231 | 232 | public EncoderHandler(TextureMovieEncoder encoder) { 233 | mWeakEncoder = new WeakReference(encoder); 234 | } 235 | 236 | @Override // runs on encoder thread 237 | public void handleMessage(Message inputMessage) { 238 | int what = inputMessage.what; 239 | Object obj = inputMessage.obj; 240 | 241 | TextureMovieEncoder encoder = mWeakEncoder.get(); 242 | if (encoder == null) { 243 | Log.w(TAG, "EncoderHandler.handleMessage: encoder is null"); 244 | return; 245 | } 246 | 247 | switch (what) { 248 | case MSG_START_RECORDING: 249 | encoder.handleStartRecording((EncoderConfig) obj); 250 | break; 251 | case MSG_STOP_RECORDING: 252 | encoder.handleStopRecording(); 253 | break; 254 | case MSG_FRAME_AVAILABLE: 255 | long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL); 256 | encoder.handleFrameAvailable((float[]) obj, timestamp); 257 | break; 258 | case MSG_SET_TEXTURE_ID: 259 | encoder.handleSetTexture(inputMessage.arg1); 260 | break; 261 | case MSG_UPDATE_SHARED_CONTEXT: 262 | encoder.handleUpdateSharedContext((EGLContext) inputMessage.obj); 263 | break; 264 | case MSG_QUIT: 265 | Looper.myLooper().quit(); 266 | break; 267 | default: 268 | throw new RuntimeException("Unhandled msg what=" + what); 269 | } 270 | } 271 | } 272 | 273 | /** 274 | * Starts recording. 275 | */ 276 | private void handleStartRecording(EncoderConfig config) { 277 | Log.d(TAG, "handleStartRecording " + config); 278 | mFrameNum = 0; 279 | prepareEncoder(config.mEglContext, config.mWidth, config.mHeight, config.mBitRate, config.mOutputFile, config.mFilterType); 280 | } 281 | 282 | 283 | private void handleFrameAvailable(float[] transform, long timestampNanos) { 284 | if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform); 285 | mFullScreen.drawFrame(mTextureId, transform); 286 | mInputWindowSurface.setPresentationTime(timestampNanos); 287 | mInputWindowSurface.swapBuffers(); 288 | mVideoEncoder.drainEncoder(false); 289 | } 290 | 291 | /** 292 | * Handles a request to stop encoding. 293 | */ 294 | private void handleStopRecording() { 295 | Log.d(TAG, "handleStopRecording"); 296 | mVideoEncoder.drainEncoder(true); 297 | releaseEncoder(); 298 | } 299 | 300 | /** 301 | * Sets the texture name that SurfaceTexture will use when frames are received. 302 | */ 303 | private void handleSetTexture(int id) { 304 | //Log.d(TAG, "handleSetTexture " + id); 305 | mTextureId = id; 306 | } 307 | 308 | /** 309 | * Tears down the EGL surface and context we've been using to feed the MediaCodec input 310 | * surface, and replaces it with a new one that shares with the new context. 311 | *

312 | * This is useful if the old context we were sharing with went away (maybe a GLSurfaceView 313 | * that got torn down) and we need to hook up with the new one. 314 | */ 315 | private void handleUpdateSharedContext(EGLContext newSharedContext) { 316 | Log.d(TAG, "handleUpdatedSharedContext " + newSharedContext); 317 | 318 | // Release the EGLSurface and EGLContext. 319 | mInputWindowSurface.releaseEglSurface(); 320 | mFullScreen.release(false); 321 | mEglCore.release(); 322 | 323 | // Create a new EGLContext and recreate the window surface. 324 | mEglCore = new EglCore(newSharedContext, EglCore.FLAG_RECORDABLE); 325 | mInputWindowSurface.recreate(mEglCore); 326 | mInputWindowSurface.makeCurrent(); 327 | 328 | // Create new programs and such for the new context. 329 | mFullScreen = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); 330 | } 331 | 332 | private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate, File outputFile, FilterType filterType) { 333 | try { 334 | mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile); 335 | } catch (IOException ioe) { 336 | throw new RuntimeException(ioe); 337 | } 338 | mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE); 339 | mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true); 340 | mInputWindowSurface.makeCurrent(); 341 | mFullScreen = new FullFrameRect(createTextureProgram(filterType)); 342 | } 343 | 344 | private Texture2dProgram createTextureProgram(FilterType filterType) { 345 | FilterType.FilterInfo filterInfo = FilterType.getFilterInfo(filterType); 346 | Texture2dProgram.ProgramType programType = filterInfo.programType; 347 | float[] kernel = filterInfo.kernel; 348 | float colorAdj = filterInfo.colorAdj; 349 | Texture2dProgram texture2dProgram = new Texture2dProgram(programType); 350 | if (kernel != null) { 351 | texture2dProgram.setKernel(kernel, colorAdj); 352 | } 353 | return texture2dProgram; 354 | } 355 | 356 | private void releaseEncoder() { 357 | mVideoEncoder.release(); 358 | if (mInputWindowSurface != null) { 359 | mInputWindowSurface.release(); 360 | mInputWindowSurface = null; 361 | } 362 | if (mFullScreen != null) { 363 | mFullScreen.release(false); 364 | mFullScreen = null; 365 | } 366 | if (mEglCore != null) { 367 | mEglCore.release(); 368 | mEglCore = null; 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/VideoEncoderCore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera; 18 | 19 | import android.media.MediaCodec; 20 | import android.media.MediaCodecInfo; 21 | import android.media.MediaFormat; 22 | import android.media.MediaMuxer; 23 | import android.util.Log; 24 | import android.view.Surface; 25 | 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.nio.ByteBuffer; 29 | 30 | /** 31 | * This class wraps up the core components used for surface-input video encoding. 32 | *

33 | * Once created, frames are fed to the input surface. Remember to provide the presentation 34 | * time stamp, and always call drainEncoder() before swapBuffers() to ensure that the 35 | * producer side doesn't get backed up. 36 | *

37 | * This class is not thread-safe, with one exception: it is valid to use the input surface 38 | * on one thread, and drain the output on a different thread. 39 | */ 40 | public class VideoEncoderCore { 41 | private static final String TAG = "VideoEncoderCore"; 42 | private static final boolean VERBOSE = false; 43 | 44 | // TODO: these ought to be configurable as well 45 | private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding 46 | private static final int FRAME_RATE = 30; // 30fps 47 | private static final int IFRAME_INTERVAL = 5; // 5 seconds between I-frames 48 | 49 | private Surface mInputSurface; 50 | private MediaMuxer mMuxer; 51 | private MediaCodec mEncoder; 52 | private MediaCodec.BufferInfo mBufferInfo; 53 | private int mTrackIndex; 54 | private boolean mMuxerStarted; 55 | 56 | 57 | /** 58 | * Configures encoder and muxer state, and prepares the input Surface. 59 | */ 60 | public VideoEncoderCore(int width, int height, int bitRate, File outputFile) 61 | throws IOException { 62 | mBufferInfo = new MediaCodec.BufferInfo(); 63 | 64 | MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height); 65 | 66 | // Set some properties. Failing to specify some of these can cause the MediaCodec 67 | // configure() call to throw an unhelpful exception. 68 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 69 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); 70 | format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); 71 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); 72 | if (VERBOSE) Log.d(TAG, "format: " + format); 73 | 74 | // Create a MediaCodec encoder, and configure it with our format. Get a Surface 75 | // we can use for input and wrap it with a class that handles the EGL work. 76 | mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); 77 | mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 78 | mInputSurface = mEncoder.createInputSurface(); 79 | mEncoder.start(); 80 | 81 | // Create a MediaMuxer. We can't add the video track and start() the muxer here, 82 | // because our MediaFormat doesn't have the Magic Goodies. These can only be 83 | // obtained from the encoder after it has started processing data. 84 | // 85 | // We're not actually interested in multiplexing audio. We just want to convert 86 | // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file. 87 | mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); 88 | 89 | mTrackIndex = -1; 90 | mMuxerStarted = false; 91 | } 92 | 93 | /** 94 | * Returns the encoder's input surface. 95 | */ 96 | public Surface getInputSurface() { 97 | return mInputSurface; 98 | } 99 | 100 | /** 101 | * Releases encoder resources. 102 | */ 103 | public void release() { 104 | if (VERBOSE) Log.d(TAG, "releasing encoder objects"); 105 | if (mEncoder != null) { 106 | mEncoder.stop(); 107 | mEncoder.release(); 108 | mEncoder = null; 109 | } 110 | if (mMuxer != null) { 111 | // TODO: stop() throws an exception if you haven't fed it any data. Keep track 112 | // of frames submitted, and don't call stop() if we haven't written anything. 113 | mMuxer.stop(); 114 | mMuxer.release(); 115 | mMuxer = null; 116 | } 117 | } 118 | 119 | /** 120 | * Extracts all pending data from the encoder and forwards it to the muxer. 121 | *

122 | * If endOfStream is not set, this returns when there is no more data to drain. If it 123 | * is set, we send EOS to the encoder, and then iterate until we see EOS on the output. 124 | * Calling this with endOfStream set should be done once, right before stopping the muxer. 125 | *

126 | * We're just using the muxer to get a .mp4 file (instead of a raw H.264 stream). We're 127 | * not recording audio. 128 | */ 129 | public void drainEncoder(boolean endOfStream) { 130 | final int TIMEOUT_USEC = 10000; 131 | if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")"); 132 | 133 | if (endOfStream) { 134 | if (VERBOSE) Log.d(TAG, "sending EOS to encoder"); 135 | mEncoder.signalEndOfInputStream(); 136 | } 137 | 138 | ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers(); 139 | while (true) { 140 | int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); 141 | if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { 142 | // no output available yet 143 | if (!endOfStream) { 144 | break; // out of while 145 | } else { 146 | if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS"); 147 | } 148 | } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 149 | // not expected for an encoder 150 | encoderOutputBuffers = mEncoder.getOutputBuffers(); 151 | } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 152 | // should happen before receiving buffers, and should only happen once 153 | if (mMuxerStarted) { 154 | throw new RuntimeException("format changed twice"); 155 | } 156 | MediaFormat newFormat = mEncoder.getOutputFormat(); 157 | Log.d(TAG, "encoder output format changed: " + newFormat); 158 | 159 | // now that we have the Magic Goodies, start the muxer 160 | mTrackIndex = mMuxer.addTrack(newFormat); 161 | mMuxer.start(); 162 | mMuxerStarted = true; 163 | } else if (encoderStatus < 0) { 164 | Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + 165 | encoderStatus); 166 | // let's ignore it 167 | } else { 168 | ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; 169 | if (encodedData == null) { 170 | throw new RuntimeException("encoderOutputBuffer " + encoderStatus + 171 | " was null"); 172 | } 173 | 174 | if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { 175 | // The codec config data was pulled out and fed to the muxer when we got 176 | // the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it. 177 | if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG"); 178 | mBufferInfo.size = 0; 179 | } 180 | 181 | if (mBufferInfo.size != 0) { 182 | if (!mMuxerStarted) { 183 | throw new RuntimeException("muxer hasn't started"); 184 | } 185 | 186 | // adjust the ByteBuffer values to match BufferInfo (not needed?) 187 | encodedData.position(mBufferInfo.offset); 188 | encodedData.limit(mBufferInfo.offset + mBufferInfo.size); 189 | 190 | mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); 191 | if (VERBOSE) { 192 | Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" + 193 | mBufferInfo.presentationTimeUs); 194 | } 195 | } 196 | 197 | mEncoder.releaseOutputBuffer(encoderStatus, false); 198 | 199 | if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 200 | if (!endOfStream) { 201 | Log.w(TAG, "reached end of stream unexpectedly"); 202 | } else { 203 | if (VERBOSE) Log.d(TAG, "end of stream reached"); 204 | } 205 | break; // out of while 206 | } 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/gles/Drawable2d.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.gles; 18 | 19 | import java.nio.FloatBuffer; 20 | 21 | /** 22 | * Base class for stuff we like to draw. 23 | */ 24 | public class Drawable2d { 25 | private static final int SIZEOF_FLOAT = 4; 26 | 27 | /** 28 | * Simple equilateral triangle (1.0 per side). Centered on (0,0). 29 | */ 30 | private static final float TRIANGLE_COORDS[] = { 31 | 0.0f, 0.577350269f, // 0 top 32 | -0.5f, -0.288675135f, // 1 bottom left 33 | 0.5f, -0.288675135f // 2 bottom right 34 | }; 35 | private static final float TRIANGLE_TEX_COORDS[] = { 36 | 0.5f, 0.0f, // 0 top center 37 | 0.0f, 1.0f, // 1 bottom left 38 | 1.0f, 1.0f, // 2 bottom right 39 | }; 40 | private static final FloatBuffer TRIANGLE_BUF = 41 | GlUtil.createFloatBuffer(TRIANGLE_COORDS); 42 | private static final FloatBuffer TRIANGLE_TEX_BUF = 43 | GlUtil.createFloatBuffer(TRIANGLE_TEX_COORDS); 44 | 45 | /** 46 | * Simple square, specified as a triangle strip. The square is centered on (0,0) and has 47 | * a size of 1x1. 48 | *

49 | * Triangles are 0-1-2 and 2-1-3 (counter-clockwise winding). 50 | */ 51 | private static final float RECTANGLE_COORDS[] = { 52 | -0.5f, -0.5f, // 0 bottom left 53 | 0.5f, -0.5f, // 1 bottom right 54 | -0.5f, 0.5f, // 2 top left 55 | 0.5f, 0.5f, // 3 top right 56 | }; 57 | private static final float RECTANGLE_TEX_COORDS[] = { 58 | 0.0f, 1.0f, // 0 bottom left 59 | 1.0f, 1.0f, // 1 bottom right 60 | 0.0f, 0.0f, // 2 top left 61 | 1.0f, 0.0f // 3 top right 62 | }; 63 | private static final FloatBuffer RECTANGLE_BUF = 64 | GlUtil.createFloatBuffer(RECTANGLE_COORDS); 65 | private static final FloatBuffer RECTANGLE_TEX_BUF = 66 | GlUtil.createFloatBuffer(RECTANGLE_TEX_COORDS); 67 | 68 | /** 69 | * A "full" square, extending from -1 to +1 in both dimensions. When the model/view/projection 70 | * matrix is identity, this will exactly cover the viewport. 71 | *

72 | * The texture coordinates are Y-inverted relative to RECTANGLE. (This seems to work out 73 | * right with external textures from SurfaceTexture.) 74 | */ 75 | private static final float FULL_RECTANGLE_COORDS[] = { 76 | -1.0f, -1.0f, // 0 bottom left 77 | 1.0f, -1.0f, // 1 bottom right 78 | -1.0f, 1.0f, // 2 top left 79 | 1.0f, 1.0f, // 3 top right 80 | }; 81 | private static final float FULL_RECTANGLE_TEX_COORDS[] = { 82 | 0.0f, 0.0f, // 0 bottom left 83 | 1.0f, 0.0f, // 1 bottom right 84 | 0.0f, 1.0f, // 2 top left 85 | 1.0f, 1.0f // 3 top right 86 | }; 87 | private static final FloatBuffer FULL_RECTANGLE_BUF = 88 | GlUtil.createFloatBuffer(FULL_RECTANGLE_COORDS); 89 | private static final FloatBuffer FULL_RECTANGLE_TEX_BUF = 90 | GlUtil.createFloatBuffer(FULL_RECTANGLE_TEX_COORDS); 91 | 92 | 93 | private FloatBuffer mVertexArray; 94 | private FloatBuffer mTexCoordArray; 95 | private int mVertexCount; 96 | private int mCoordsPerVertex; 97 | private int mVertexStride; 98 | private int mTexCoordStride; 99 | private Prefab mPrefab; 100 | 101 | /** 102 | * Enum values for constructor. 103 | */ 104 | public enum Prefab { 105 | TRIANGLE, RECTANGLE, FULL_RECTANGLE 106 | } 107 | 108 | /** 109 | * Prepares a drawable from a "pre-fabricated" shape definition. 110 | *

111 | * Does no EGL/GL operations, so this can be done at any time. 112 | */ 113 | public Drawable2d(Prefab shape) { 114 | switch (shape) { 115 | case TRIANGLE: 116 | mVertexArray = TRIANGLE_BUF; 117 | mTexCoordArray = TRIANGLE_TEX_BUF; 118 | mCoordsPerVertex = 2; 119 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 120 | mVertexCount = TRIANGLE_COORDS.length / mCoordsPerVertex; 121 | break; 122 | case RECTANGLE: 123 | mVertexArray = RECTANGLE_BUF; 124 | mTexCoordArray = RECTANGLE_TEX_BUF; 125 | mCoordsPerVertex = 2; 126 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 127 | mVertexCount = RECTANGLE_COORDS.length / mCoordsPerVertex; 128 | break; 129 | case FULL_RECTANGLE: 130 | mVertexArray = FULL_RECTANGLE_BUF; 131 | mTexCoordArray = FULL_RECTANGLE_TEX_BUF; 132 | mCoordsPerVertex = 2; 133 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 134 | mVertexCount = FULL_RECTANGLE_COORDS.length / mCoordsPerVertex; 135 | break; 136 | default: 137 | throw new RuntimeException("Unknown shape " + shape); 138 | } 139 | mTexCoordStride = 2 * SIZEOF_FLOAT; 140 | mPrefab = shape; 141 | } 142 | 143 | /** 144 | * Returns the array of vertices. 145 | *

146 | * To avoid allocations, this returns internal state. The caller must not modify it. 147 | */ 148 | public FloatBuffer getVertexArray() { 149 | return mVertexArray; 150 | } 151 | 152 | /** 153 | * Returns the array of texture coordinates. 154 | *

155 | * To avoid allocations, this returns internal state. The caller must not modify it. 156 | */ 157 | public FloatBuffer getTexCoordArray() { 158 | return mTexCoordArray; 159 | } 160 | 161 | /** 162 | * Returns the number of vertices stored in the vertex array. 163 | */ 164 | public int getVertexCount() { 165 | return mVertexCount; 166 | } 167 | 168 | /** 169 | * Returns the width, in bytes, of the data for each vertex. 170 | */ 171 | public int getVertexStride() { 172 | return mVertexStride; 173 | } 174 | 175 | /** 176 | * Returns the width, in bytes, of the data for each texture coordinate. 177 | */ 178 | public int getTexCoordStride() { 179 | return mTexCoordStride; 180 | } 181 | 182 | /** 183 | * Returns the number of position coordinates per vertex. This will be 2 or 3. 184 | */ 185 | public int getCoordsPerVertex() { 186 | return mCoordsPerVertex; 187 | } 188 | 189 | @Override 190 | public String toString() { 191 | if (mPrefab != null) { 192 | return "[Drawable2d: " + mPrefab + "]"; 193 | } else { 194 | return "[Drawable2d: ...]"; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/gles/EglCore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.gles; 18 | 19 | import android.graphics.SurfaceTexture; 20 | import android.opengl.EGL14; 21 | import android.opengl.EGLConfig; 22 | import android.opengl.EGLContext; 23 | import android.opengl.EGLDisplay; 24 | import android.opengl.EGLExt; 25 | import android.opengl.EGLSurface; 26 | import android.util.Log; 27 | import android.view.Surface; 28 | 29 | /** 30 | * Core EGL state (display, context, config). 31 | *

32 | * The EGLContext must only be attached to one thread at a time. This class is not thread-safe. 33 | */ 34 | public final class EglCore { 35 | private static final String TAG = "EglCore"; 36 | 37 | /** 38 | * Constructor flag: surface must be recordable. This discourages EGL from using a 39 | * pixel format that cannot be converted efficiently to something usable by the video 40 | * encoder. 41 | */ 42 | public static final int FLAG_RECORDABLE = 0x01; 43 | 44 | /** 45 | * Constructor flag: ask for GLES3, fall back to GLES2 if not available. Without this 46 | * flag, GLES2 is used. 47 | */ 48 | public static final int FLAG_TRY_GLES3 = 0x02; 49 | 50 | // Android-specific extension. 51 | private static final int EGL_RECORDABLE_ANDROID = 0x3142; 52 | 53 | private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY; 54 | private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT; 55 | private EGLConfig mEGLConfig = null; 56 | private int mGlVersion = -1; 57 | 58 | 59 | /** 60 | * Prepares EGL display and context. 61 | *

62 | * Equivalent to EglCore(null, 0). 63 | */ 64 | public EglCore() { 65 | this(null, 0); 66 | } 67 | 68 | /** 69 | * Prepares EGL display and context. 70 | *

71 | * @param sharedContext The context to share, or null if sharing is not desired. 72 | * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE. 73 | */ 74 | public EglCore(EGLContext sharedContext, int flags) { 75 | if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { 76 | throw new RuntimeException("EGL already set up"); 77 | } 78 | 79 | if (sharedContext == null) { 80 | sharedContext = EGL14.EGL_NO_CONTEXT; 81 | } 82 | 83 | mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); 84 | if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { 85 | throw new RuntimeException("unable to get EGL14 display"); 86 | } 87 | int[] version = new int[2]; 88 | if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { 89 | mEGLDisplay = null; 90 | throw new RuntimeException("unable to initialize EGL14"); 91 | } 92 | 93 | // Try to get a GLES3 context, if requested. 94 | if ((flags & FLAG_TRY_GLES3) != 0) { 95 | //Log.d(TAG, "Trying GLES 3"); 96 | EGLConfig config = getConfig(flags, 3); 97 | if (config != null) { 98 | int[] attrib3_list = { 99 | EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, 100 | EGL14.EGL_NONE 101 | }; 102 | EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, 103 | attrib3_list, 0); 104 | 105 | if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) { 106 | //Log.d(TAG, "Got GLES 3 config"); 107 | mEGLConfig = config; 108 | mEGLContext = context; 109 | mGlVersion = 3; 110 | } 111 | } 112 | } 113 | if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed 114 | //Log.d(TAG, "Trying GLES 2"); 115 | EGLConfig config = getConfig(flags, 2); 116 | if (config == null) { 117 | throw new RuntimeException("Unable to find a suitable EGLConfig"); 118 | } 119 | int[] attrib2_list = { 120 | EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, 121 | EGL14.EGL_NONE 122 | }; 123 | EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, 124 | attrib2_list, 0); 125 | checkEglError("eglCreateContext"); 126 | mEGLConfig = config; 127 | mEGLContext = context; 128 | mGlVersion = 2; 129 | } 130 | 131 | // Confirm with query. 132 | int[] values = new int[1]; 133 | EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, 134 | values, 0); 135 | Log.d(TAG, "EGLContext created, client version " + values[0]); 136 | } 137 | 138 | /** 139 | * Finds a suitable EGLConfig. 140 | * 141 | * @param flags Bit flags from constructor. 142 | * @param version Must be 2 or 3. 143 | */ 144 | private EGLConfig getConfig(int flags, int version) { 145 | int renderableType = EGL14.EGL_OPENGL_ES2_BIT; 146 | if (version >= 3) { 147 | renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR; 148 | } 149 | 150 | // The actual surface is generally RGBA or RGBX, so situationally omitting alpha 151 | // doesn't really help. It can also lead to a huge performance hit on glReadPixels() 152 | // when reading into a GL_RGBA buffer. 153 | int[] attribList = { 154 | EGL14.EGL_RED_SIZE, 8, 155 | EGL14.EGL_GREEN_SIZE, 8, 156 | EGL14.EGL_BLUE_SIZE, 8, 157 | EGL14.EGL_ALPHA_SIZE, 8, 158 | //EGL14.EGL_DEPTH_SIZE, 16, 159 | //EGL14.EGL_STENCIL_SIZE, 8, 160 | EGL14.EGL_RENDERABLE_TYPE, renderableType, 161 | EGL14.EGL_NONE, 0, // placeholder for recordable [@-3] 162 | EGL14.EGL_NONE 163 | }; 164 | if ((flags & FLAG_RECORDABLE) != 0) { 165 | attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID; 166 | attribList[attribList.length - 2] = 1; 167 | } 168 | EGLConfig[] configs = new EGLConfig[1]; 169 | int[] numConfigs = new int[1]; 170 | if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, 171 | numConfigs, 0)) { 172 | Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig"); 173 | return null; 174 | } 175 | return configs[0]; 176 | } 177 | 178 | /** 179 | * Discards all resources held by this class, notably the EGL context. This must be 180 | * called from the thread where the context was created. 181 | *

182 | * On completion, no context will be current. 183 | */ 184 | public void release() { 185 | if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { 186 | // Android is unusual in that it uses a reference-counted EGLDisplay. So for 187 | // every eglInitialize() we need an eglTerminate(). 188 | EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, 189 | EGL14.EGL_NO_CONTEXT); 190 | EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); 191 | EGL14.eglReleaseThread(); 192 | EGL14.eglTerminate(mEGLDisplay); 193 | } 194 | 195 | mEGLDisplay = EGL14.EGL_NO_DISPLAY; 196 | mEGLContext = EGL14.EGL_NO_CONTEXT; 197 | mEGLConfig = null; 198 | } 199 | 200 | @Override 201 | protected void finalize() throws Throwable { 202 | try { 203 | if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { 204 | // We're limited here -- finalizers don't run on the thread that holds 205 | // the EGL state, so if a surface or context is still current on another 206 | // thread we can't fully release it here. Exceptions thrown from here 207 | // are quietly discarded. Complain in the log file. 208 | Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked"); 209 | release(); 210 | } 211 | } finally { 212 | super.finalize(); 213 | } 214 | } 215 | 216 | /** 217 | * Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's 218 | * still current in a context. 219 | */ 220 | public void releaseSurface(EGLSurface eglSurface) { 221 | EGL14.eglDestroySurface(mEGLDisplay, eglSurface); 222 | } 223 | 224 | /** 225 | * Creates an EGL surface associated with a Surface. 226 | *

227 | * If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute. 228 | */ 229 | public EGLSurface createWindowSurface(Object surface) { 230 | if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) { 231 | throw new RuntimeException("invalid surface: " + surface); 232 | } 233 | 234 | // Create a window surface, and attach it to the Surface we received. 235 | int[] surfaceAttribs = { 236 | EGL14.EGL_NONE 237 | }; 238 | EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface, 239 | surfaceAttribs, 0); 240 | checkEglError("eglCreateWindowSurface"); 241 | if (eglSurface == null) { 242 | throw new RuntimeException("surface was null"); 243 | } 244 | return eglSurface; 245 | } 246 | 247 | /** 248 | * Creates an EGL surface associated with an offscreen buffer. 249 | */ 250 | public EGLSurface createOffscreenSurface(int width, int height) { 251 | int[] surfaceAttribs = { 252 | EGL14.EGL_WIDTH, width, 253 | EGL14.EGL_HEIGHT, height, 254 | EGL14.EGL_NONE 255 | }; 256 | EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, 257 | surfaceAttribs, 0); 258 | checkEglError("eglCreatePbufferSurface"); 259 | if (eglSurface == null) { 260 | throw new RuntimeException("surface was null"); 261 | } 262 | return eglSurface; 263 | } 264 | 265 | /** 266 | * Makes our EGL context current, using the supplied surface for both "draw" and "read". 267 | */ 268 | public void makeCurrent(EGLSurface eglSurface) { 269 | if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { 270 | // called makeCurrent() before create? 271 | Log.d(TAG, "NOTE: makeCurrent w/o display"); 272 | } 273 | if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) { 274 | throw new RuntimeException("eglMakeCurrent failed"); 275 | } 276 | } 277 | 278 | /** 279 | * Makes our EGL context current, using the supplied "draw" and "read" surfaces. 280 | */ 281 | public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) { 282 | if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { 283 | // called makeCurrent() before create? 284 | Log.d(TAG, "NOTE: makeCurrent w/o display"); 285 | } 286 | if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) { 287 | throw new RuntimeException("eglMakeCurrent(draw,read) failed"); 288 | } 289 | } 290 | 291 | /** 292 | * Makes no context current. 293 | */ 294 | public void makeNothingCurrent() { 295 | if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, 296 | EGL14.EGL_NO_CONTEXT)) { 297 | throw new RuntimeException("eglMakeCurrent failed"); 298 | } 299 | } 300 | 301 | /** 302 | * Calls eglSwapBuffers. Use this to "publish" the current frame. 303 | * 304 | * @return false on failure 305 | */ 306 | public boolean swapBuffers(EGLSurface eglSurface) { 307 | return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface); 308 | } 309 | 310 | /** 311 | * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. 312 | */ 313 | public void setPresentationTime(EGLSurface eglSurface, long nsecs) { 314 | EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs); 315 | } 316 | 317 | /** 318 | * Returns true if our context and the specified surface are current. 319 | */ 320 | public boolean isCurrent(EGLSurface eglSurface) { 321 | return mEGLContext.equals(EGL14.eglGetCurrentContext()) && 322 | eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)); 323 | } 324 | 325 | /** 326 | * Performs a simple surface query. 327 | */ 328 | public int querySurface(EGLSurface eglSurface, int what) { 329 | int[] value = new int[1]; 330 | EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0); 331 | return value[0]; 332 | } 333 | 334 | /** 335 | * Queries a string value. 336 | */ 337 | public String queryString(int what) { 338 | return EGL14.eglQueryString(mEGLDisplay, what); 339 | } 340 | 341 | /** 342 | * Returns the GLES version this context is configured for (currently 2 or 3). 343 | */ 344 | public int getGlVersion() { 345 | return mGlVersion; 346 | } 347 | 348 | /** 349 | * Writes the current display, context, and surface to the log. 350 | */ 351 | public static void logCurrent(String msg) { 352 | EGLDisplay display; 353 | EGLContext context; 354 | EGLSurface surface; 355 | 356 | display = EGL14.eglGetCurrentDisplay(); 357 | context = EGL14.eglGetCurrentContext(); 358 | surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); 359 | Log.i(TAG, "Current EGL (" + msg + "): display=" + display + ", context=" + context + 360 | ", surface=" + surface); 361 | } 362 | 363 | /** 364 | * Checks for EGL errors. Throws an exception if an error has been raised. 365 | */ 366 | private void checkEglError(String msg) { 367 | int error; 368 | if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { 369 | throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/gles/EglSurfaceBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.gles; 18 | 19 | import android.graphics.Bitmap; 20 | import android.opengl.EGL14; 21 | import android.opengl.EGLSurface; 22 | import android.opengl.GLES20; 23 | import android.util.Log; 24 | 25 | import java.io.BufferedOutputStream; 26 | import java.io.File; 27 | import java.io.FileOutputStream; 28 | import java.io.IOException; 29 | import java.nio.ByteBuffer; 30 | import java.nio.ByteOrder; 31 | 32 | /** 33 | * Common base class for EGL surfaces. 34 | *

35 | * There can be multiple surfaces associated with a single context. 36 | */ 37 | public class EglSurfaceBase { 38 | private static final String TAG = "EglSurfaceBase"; 39 | 40 | // EglCore object we're associated with. It may be associated with multiple surfaces. 41 | protected EglCore mEglCore; 42 | 43 | private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE; 44 | private int mWidth = -1; 45 | private int mHeight = -1; 46 | 47 | protected EglSurfaceBase(EglCore eglCore) { 48 | mEglCore = eglCore; 49 | } 50 | 51 | /** 52 | * Creates a window surface. 53 | *

54 | * @param surface May be a Surface or SurfaceTexture. 55 | */ 56 | public void createWindowSurface(Object surface) { 57 | if (mEGLSurface != EGL14.EGL_NO_SURFACE) { 58 | throw new IllegalStateException("surface already created"); 59 | } 60 | mEGLSurface = mEglCore.createWindowSurface(surface); 61 | 62 | // Don't cache width/height here, because the size of the underlying surface can change 63 | // out from under us (see e.g. HardwareScalerActivity). 64 | //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); 65 | //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); 66 | } 67 | 68 | /** 69 | * Creates an off-screen surface. 70 | */ 71 | public void createOffscreenSurface(int width, int height) { 72 | if (mEGLSurface != EGL14.EGL_NO_SURFACE) { 73 | throw new IllegalStateException("surface already created"); 74 | } 75 | mEGLSurface = mEglCore.createOffscreenSurface(width, height); 76 | mWidth = width; 77 | mHeight = height; 78 | } 79 | 80 | /** 81 | * Returns the surface's width, in pixels. 82 | *

83 | * If this is called on a window surface, and the underlying surface is in the process 84 | * of changing size, we may not see the new size right away (e.g. in the "surfaceChanged" 85 | * callback). The size should match after the next buffer swap. 86 | */ 87 | public int getWidth() { 88 | if (mWidth < 0) { 89 | return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); 90 | } else { 91 | return mWidth; 92 | } 93 | } 94 | 95 | /** 96 | * Returns the surface's height, in pixels. 97 | */ 98 | public int getHeight() { 99 | if (mHeight < 0) { 100 | return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); 101 | } else { 102 | return mHeight; 103 | } 104 | } 105 | 106 | /** 107 | * Release the EGL surface. 108 | */ 109 | public void releaseEglSurface() { 110 | mEglCore.releaseSurface(mEGLSurface); 111 | mEGLSurface = EGL14.EGL_NO_SURFACE; 112 | mWidth = mHeight = -1; 113 | } 114 | 115 | /** 116 | * Makes our EGL context and surface current. 117 | */ 118 | public void makeCurrent() { 119 | mEglCore.makeCurrent(mEGLSurface); 120 | } 121 | 122 | /** 123 | * Makes our EGL context and surface current for drawing, using the supplied surface 124 | * for reading. 125 | */ 126 | public void makeCurrentReadFrom(EglSurfaceBase readSurface) { 127 | mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface); 128 | } 129 | 130 | /** 131 | * Calls eglSwapBuffers. Use this to "publish" the current frame. 132 | * 133 | * @return false on failure 134 | */ 135 | public boolean swapBuffers() { 136 | boolean result = mEglCore.swapBuffers(mEGLSurface); 137 | if (!result) { 138 | Log.d(TAG, "WARNING: swapBuffers() failed"); 139 | } 140 | return result; 141 | } 142 | 143 | /** 144 | * Sends the presentation time stamp to EGL. 145 | * 146 | * @param nsecs Timestamp, in nanoseconds. 147 | */ 148 | public void setPresentationTime(long nsecs) { 149 | mEglCore.setPresentationTime(mEGLSurface, nsecs); 150 | } 151 | 152 | /** 153 | * Saves the EGL surface to a file. 154 | *

155 | * Expects that this object's EGL surface is current. 156 | */ 157 | public void saveFrame(File file) throws IOException { 158 | if (!mEglCore.isCurrent(mEGLSurface)) { 159 | throw new RuntimeException("Expected EGL context/surface is not current"); 160 | } 161 | 162 | // glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA 163 | // data (i.e. a byte of red, followed by a byte of green...). While the Bitmap 164 | // constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the 165 | // Bitmap "copy pixels" method wants the same format GL provides. 166 | // 167 | // Ideally we'd have some way to re-use the ByteBuffer, especially if we're calling 168 | // here often. 169 | // 170 | // Making this even more interesting is the upside-down nature of GL, which means 171 | // our output will look upside down relative to what appears on screen if the 172 | // typical GL conventions are used. 173 | 174 | String filename = file.toString(); 175 | 176 | int width = getWidth(); 177 | int height = getHeight(); 178 | ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4); 179 | buf.order(ByteOrder.LITTLE_ENDIAN); 180 | GLES20.glReadPixels(0, 0, width, height, 181 | GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); 182 | GlUtil.checkGlError("glReadPixels"); 183 | buf.rewind(); 184 | 185 | BufferedOutputStream bos = null; 186 | try { 187 | bos = new BufferedOutputStream(new FileOutputStream(filename)); 188 | Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 189 | bmp.copyPixelsFromBuffer(buf); 190 | bmp.compress(Bitmap.CompressFormat.PNG, 90, bos); 191 | bmp.recycle(); 192 | } finally { 193 | if (bos != null) bos.close(); 194 | } 195 | Log.d(TAG, "Saved " + width + "x" + height + " frame as '" + filename + "'"); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/gles/FullFrameRect.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.gles; 18 | 19 | /** 20 | * This class essentially represents a viewport-sized sprite that will be rendered with 21 | * a texture, usually from an external source like the camera or video decoder. 22 | */ 23 | public class FullFrameRect { 24 | private final Drawable2d mRectDrawable = new Drawable2d(Drawable2d.Prefab.FULL_RECTANGLE); 25 | private Texture2dProgram mProgram; 26 | 27 | /** 28 | * Prepares the object. 29 | * 30 | * @param program The program to use. FullFrameRect takes ownership, and will release 31 | * the program when no longer needed. 32 | */ 33 | public FullFrameRect(Texture2dProgram program) { 34 | mProgram = program; 35 | } 36 | 37 | /** 38 | * Releases resources. 39 | *

40 | * This must be called with the appropriate EGL context current (i.e. the one that was 41 | * current when the constructor was called). If we're about to destroy the EGL context, 42 | * there's no value in having the caller make it current just to do this cleanup, so you 43 | * can pass a flag that will tell this function to skip any EGL-context-specific cleanup. 44 | */ 45 | public void release(boolean doEglCleanup) { 46 | if (mProgram != null) { 47 | if (doEglCleanup) { 48 | mProgram.release(); 49 | } 50 | mProgram = null; 51 | } 52 | } 53 | 54 | /** 55 | * Returns the program currently in use. 56 | */ 57 | public Texture2dProgram getProgram() { 58 | return mProgram; 59 | } 60 | 61 | /** 62 | * Changes the program. The previous program will be released. 63 | *

64 | * The appropriate EGL context must be current. 65 | */ 66 | public void changeProgram(Texture2dProgram program) { 67 | mProgram.release(); 68 | mProgram = program; 69 | } 70 | 71 | /** 72 | * Creates a texture object suitable for use with drawFrame(). 73 | */ 74 | public int createTextureObject() { 75 | return mProgram.createTextureObject(); 76 | } 77 | 78 | /** 79 | * Draws a viewport-filling rect, texturing it with the specified texture object. 80 | */ 81 | public void drawFrame(int textureId, float[] texMatrix) { 82 | // Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport. 83 | mProgram.draw(GlUtil.IDENTITY_MATRIX, 84 | mRectDrawable.getVertexArray(), 85 | 0, 86 | mRectDrawable.getVertexCount(), 87 | mRectDrawable.getCoordsPerVertex(), 88 | mRectDrawable.getVertexStride(), 89 | texMatrix, 90 | mRectDrawable.getTexCoordArray(), 91 | textureId, 92 | mRectDrawable.getTexCoordStride()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/gles/GlUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.gles; 18 | 19 | import android.opengl.GLES20; 20 | import android.opengl.GLES30; 21 | import android.opengl.Matrix; 22 | import android.util.Log; 23 | 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | import java.nio.FloatBuffer; 27 | 28 | /** 29 | * Some OpenGL utility functions. 30 | */ 31 | public class GlUtil { 32 | private static final String TAG = "GlUtil"; 33 | 34 | /** Identity matrix for general use. Don't modify or life will get weird. */ 35 | public static final float[] IDENTITY_MATRIX; 36 | static { 37 | IDENTITY_MATRIX = new float[16]; 38 | Matrix.setIdentityM(IDENTITY_MATRIX, 0); 39 | } 40 | 41 | private static final int SIZEOF_FLOAT = 4; 42 | 43 | 44 | private GlUtil() {} // do not instantiate 45 | 46 | /** 47 | * Creates a new program from the supplied vertex and fragment shaders. 48 | * 49 | * @return A handle to the program, or 0 on failure. 50 | */ 51 | public static int createProgram(String vertexSource, String fragmentSource) { 52 | int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); 53 | if (vertexShader == 0) { 54 | return 0; 55 | } 56 | int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); 57 | if (pixelShader == 0) { 58 | return 0; 59 | } 60 | 61 | int program = GLES20.glCreateProgram(); 62 | checkGlError("glCreateProgram"); 63 | if (program == 0) { 64 | Log.e(TAG, "Could not create program"); 65 | } 66 | GLES20.glAttachShader(program, vertexShader); 67 | checkGlError("glAttachShader"); 68 | GLES20.glAttachShader(program, pixelShader); 69 | checkGlError("glAttachShader"); 70 | GLES20.glLinkProgram(program); 71 | int[] linkStatus = new int[1]; 72 | GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); 73 | if (linkStatus[0] != GLES20.GL_TRUE) { 74 | Log.e(TAG, "Could not link program: "); 75 | Log.e(TAG, GLES20.glGetProgramInfoLog(program)); 76 | GLES20.glDeleteProgram(program); 77 | program = 0; 78 | } 79 | return program; 80 | } 81 | 82 | /** 83 | * Compiles the provided shader source. 84 | * 85 | * @return A handle to the shader, or 0 on failure. 86 | */ 87 | public static int loadShader(int shaderType, String source) { 88 | int shader = GLES20.glCreateShader(shaderType); 89 | checkGlError("glCreateShader type=" + shaderType); 90 | GLES20.glShaderSource(shader, source); 91 | GLES20.glCompileShader(shader); 92 | int[] compiled = new int[1]; 93 | GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); 94 | if (compiled[0] == 0) { 95 | Log.e(TAG, "Could not compile shader " + shaderType + ":"); 96 | Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)); 97 | GLES20.glDeleteShader(shader); 98 | shader = 0; 99 | } 100 | return shader; 101 | } 102 | 103 | /** 104 | * Checks to see if a GLES error has been raised. 105 | */ 106 | public static void checkGlError(String op) { 107 | int error = GLES20.glGetError(); 108 | if (error != GLES20.GL_NO_ERROR) { 109 | String msg = op + ": glError 0x" + Integer.toHexString(error); 110 | Log.e(TAG, msg); 111 | throw new RuntimeException(msg); 112 | } 113 | } 114 | 115 | /** 116 | * Checks to see if the location we obtained is valid. GLES returns -1 if a label 117 | * could not be found, but does not set the GL error. 118 | *

119 | * Throws a RuntimeException if the location is invalid. 120 | */ 121 | public static void checkLocation(int location, String label) { 122 | if (location < 0) { 123 | throw new RuntimeException("Unable to locate '" + label + "' in program"); 124 | } 125 | } 126 | 127 | /** 128 | * Creates a texture from raw data. 129 | * 130 | * @param data Image data, in a "direct" ByteBuffer. 131 | * @param width Texture width, in pixels (not bytes). 132 | * @param height Texture height, in pixels. 133 | * @param format Image data format (use constant appropriate for glTexImage2D(), e.g. GL_RGBA). 134 | * @return Handle to texture. 135 | */ 136 | public static int createImageTexture(ByteBuffer data, int width, int height, int format) { 137 | int[] textureHandles = new int[1]; 138 | int textureHandle; 139 | 140 | GLES20.glGenTextures(1, textureHandles, 0); 141 | textureHandle = textureHandles[0]; 142 | GlUtil.checkGlError("glGenTextures"); 143 | 144 | // Bind the texture handle to the 2D texture target. 145 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle); 146 | 147 | // Configure min/mag filtering, i.e. what scaling method do we use if what we're rendering 148 | // is smaller or larger than the source image. 149 | GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, 150 | GLES20.GL_LINEAR); 151 | GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, 152 | GLES20.GL_LINEAR); 153 | GlUtil.checkGlError("loadImageTexture"); 154 | 155 | // Load the data from the buffer into the texture handle. 156 | GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, /*level*/ 0, format, 157 | width, height, /*border*/ 0, format, GLES20.GL_UNSIGNED_BYTE, data); 158 | GlUtil.checkGlError("loadImageTexture"); 159 | 160 | return textureHandle; 161 | } 162 | 163 | /** 164 | * Allocates a direct float buffer, and populates it with the float array data. 165 | */ 166 | public static FloatBuffer createFloatBuffer(float[] coords) { 167 | // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it. 168 | ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT); 169 | bb.order(ByteOrder.nativeOrder()); 170 | FloatBuffer fb = bb.asFloatBuffer(); 171 | fb.put(coords); 172 | fb.position(0); 173 | return fb; 174 | } 175 | 176 | /** 177 | * Writes GL version info to the log. 178 | */ 179 | public static void logVersionInfo() { 180 | Log.i(TAG, "vendor : " + GLES20.glGetString(GLES20.GL_VENDOR)); 181 | Log.i(TAG, "renderer: " + GLES20.glGetString(GLES20.GL_RENDERER)); 182 | Log.i(TAG, "version : " + GLES20.glGetString(GLES20.GL_VERSION)); 183 | 184 | if (false) { 185 | int[] values = new int[1]; 186 | GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, values, 0); 187 | int majorVersion = values[0]; 188 | GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, values, 0); 189 | int minorVersion = values[0]; 190 | if (GLES30.glGetError() == GLES30.GL_NO_ERROR) { 191 | Log.i(TAG, "iversion: " + majorVersion + "." + minorVersion); 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/gles/Texture2dProgram.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.gles; 18 | 19 | import android.opengl.GLES11Ext; 20 | import android.opengl.GLES20; 21 | import android.util.Log; 22 | 23 | import java.nio.FloatBuffer; 24 | 25 | /** 26 | * GL program and supporting functions for textured 2D shapes. 27 | */ 28 | public class Texture2dProgram { 29 | private static final String TAG = "Texture2dProgram"; 30 | 31 | public enum ProgramType { 32 | TEXTURE_2D, TEXTURE_EXT, TEXTURE_EXT_BW, TEXTURE_EXT_FILT 33 | } 34 | 35 | // Simple vertex shader, used for all programs. 36 | private static final String VERTEX_SHADER = 37 | "uniform mat4 uMVPMatrix;\n" + 38 | "uniform mat4 uTexMatrix;\n" + 39 | "attribute vec4 aPosition;\n" + 40 | "attribute vec4 aTextureCoord;\n" + 41 | "varying vec2 vTextureCoord;\n" + 42 | "void main() {\n" + 43 | " gl_Position = uMVPMatrix * aPosition;\n" + 44 | " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" + 45 | "}\n"; 46 | 47 | // Simple fragment shader for use with "normal" 2D textures. 48 | private static final String FRAGMENT_SHADER_2D = 49 | "precision mediump float;\n" + 50 | "varying vec2 vTextureCoord;\n" + 51 | "uniform sampler2D sTexture;\n" + 52 | "void main() {\n" + 53 | " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + 54 | "}\n"; 55 | 56 | // Simple fragment shader for use with external 2D textures (e.g. what we get from 57 | // SurfaceTexture). 58 | private static final String FRAGMENT_SHADER_EXT = 59 | "#extension GL_OES_EGL_image_external : require\n" + 60 | "precision mediump float;\n" + 61 | "varying vec2 vTextureCoord;\n" + 62 | "uniform samplerExternalOES sTexture;\n" + 63 | "void main() {\n" + 64 | " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + 65 | "}\n"; 66 | 67 | // Fragment shader that converts color to black & white with a simple transformation. 68 | private static final String FRAGMENT_SHADER_EXT_BW = 69 | "#extension GL_OES_EGL_image_external : require\n" + 70 | "precision mediump float;\n" + 71 | "varying vec2 vTextureCoord;\n" + 72 | "uniform samplerExternalOES sTexture;\n" + 73 | "void main() {\n" + 74 | " vec4 tc = texture2D(sTexture, vTextureCoord);\n" + 75 | " float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;\n" + 76 | " gl_FragColor = vec4(color, color, color, 1.0);\n" + 77 | "}\n"; 78 | 79 | // Fragment shader with a convolution filter. The upper-left half will be drawn normally, 80 | // the lower-right half will have the filter applied, and a thin red line will be drawn 81 | // at the border. 82 | // 83 | // This is not optimized for performance. Some things that might make this faster: 84 | // - Remove the conditionals. They're used to present a half & half view with a red 85 | // stripe across the middle, but that's only useful for a demo. 86 | // - Unroll the loop. Ideally the compiler does this for you when it's beneficial. 87 | // - Bake the filter kernel into the shader, instead of passing it through a uniform 88 | // array. That, combined with loop unrolling, should reduce memory accesses. 89 | public static final int KERNEL_SIZE = 9; 90 | private static final String FRAGMENT_SHADER_EXT_FILT = 91 | "#extension GL_OES_EGL_image_external : require\n" + 92 | "#define KERNEL_SIZE " + KERNEL_SIZE + "\n" + 93 | "precision highp float;\n" + 94 | "varying vec2 vTextureCoord;\n" + 95 | "uniform samplerExternalOES sTexture;\n" + 96 | "uniform float uKernel[KERNEL_SIZE];\n" + 97 | "uniform vec2 uTexOffset[KERNEL_SIZE];\n" + 98 | "uniform float uColorAdjust;\n" + 99 | "void main() {\n" + 100 | " int i = 0;\n" + 101 | " vec4 sum = vec4(0.0);\n" + 102 | " for (i = 0; i < KERNEL_SIZE; i++) {\n" + 103 | " vec4 texc = texture2D(sTexture, vTextureCoord + uTexOffset[i]);\n" + 104 | " sum += texc * uKernel[i];\n" + 105 | " }\n" + 106 | " sum += uColorAdjust;\n" + 107 | " gl_FragColor = sum;\n" + 108 | "}\n"; 109 | 110 | private ProgramType mProgramType; 111 | 112 | // Handles to the GL program and various components of it. 113 | private int mProgramHandle; 114 | private int muMVPMatrixLoc; 115 | private int muTexMatrixLoc; 116 | private int muKernelLoc; 117 | private int muTexOffsetLoc; 118 | private int muColorAdjustLoc; 119 | private int maPositionLoc; 120 | private int maTextureCoordLoc; 121 | 122 | private int mTextureTarget; 123 | 124 | private float[] mKernel = new float[KERNEL_SIZE]; 125 | private float[] mTexOffset; 126 | private float mColorAdjust; 127 | 128 | 129 | /** 130 | * Prepares the program in the current EGL context. 131 | */ 132 | public Texture2dProgram(ProgramType programType) { 133 | mProgramType = programType; 134 | switch (programType) { 135 | case TEXTURE_2D: 136 | mTextureTarget = GLES20.GL_TEXTURE_2D; 137 | mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_2D); 138 | break; 139 | case TEXTURE_EXT: 140 | mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; 141 | mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT); 142 | break; 143 | case TEXTURE_EXT_BW: 144 | mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; 145 | mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT_BW); 146 | break; 147 | case TEXTURE_EXT_FILT: 148 | mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; 149 | mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT_FILT); 150 | break; 151 | default: 152 | throw new RuntimeException("Unhandled type " + programType); 153 | } 154 | if (mProgramHandle == 0) { 155 | throw new RuntimeException("Unable to create program"); 156 | } 157 | Log.d(TAG, "Created program " + mProgramHandle + " (" + programType + ")"); 158 | 159 | // get locations of attributes and uniforms 160 | 161 | maPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "aPosition"); 162 | GlUtil.checkLocation(maPositionLoc, "aPosition"); 163 | maTextureCoordLoc = GLES20.glGetAttribLocation(mProgramHandle, "aTextureCoord"); 164 | GlUtil.checkLocation(maTextureCoordLoc, "aTextureCoord"); 165 | muMVPMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uMVPMatrix"); 166 | GlUtil.checkLocation(muMVPMatrixLoc, "uMVPMatrix"); 167 | muTexMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexMatrix"); 168 | GlUtil.checkLocation(muTexMatrixLoc, "uTexMatrix"); 169 | muKernelLoc = GLES20.glGetUniformLocation(mProgramHandle, "uKernel"); 170 | if (muKernelLoc < 0) { 171 | // no kernel in this one 172 | muKernelLoc = -1; 173 | muTexOffsetLoc = -1; 174 | muColorAdjustLoc = -1; 175 | } else { 176 | // has kernel, must also have tex offset and color adj 177 | muTexOffsetLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexOffset"); 178 | GlUtil.checkLocation(muTexOffsetLoc, "uTexOffset"); 179 | muColorAdjustLoc = GLES20.glGetUniformLocation(mProgramHandle, "uColorAdjust"); 180 | GlUtil.checkLocation(muColorAdjustLoc, "uColorAdjust"); 181 | 182 | // initialize default values 183 | setKernel(new float[] {0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f}, 0f); 184 | setTexSize(256, 256); 185 | } 186 | } 187 | 188 | /** 189 | * Releases the program. 190 | *

191 | * The appropriate EGL context must be current (i.e. the one that was used to create 192 | * the program). 193 | */ 194 | public void release() { 195 | Log.d(TAG, "deleting program " + mProgramHandle); 196 | GLES20.glDeleteProgram(mProgramHandle); 197 | mProgramHandle = -1; 198 | } 199 | 200 | /** 201 | * Returns the program type. 202 | */ 203 | public ProgramType getProgramType() { 204 | return mProgramType; 205 | } 206 | 207 | /** 208 | * Creates a texture object suitable for use with this program. 209 | *

210 | * On exit, the texture will be bound. 211 | */ 212 | public int createTextureObject() { 213 | int[] textures = new int[1]; 214 | GLES20.glGenTextures(1, textures, 0); 215 | GlUtil.checkGlError("glGenTextures"); 216 | 217 | int texId = textures[0]; 218 | GLES20.glBindTexture(mTextureTarget, texId); 219 | GlUtil.checkGlError("glBindTexture " + texId); 220 | 221 | GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, 222 | GLES20.GL_NEAREST); 223 | GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, 224 | GLES20.GL_LINEAR); 225 | GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, 226 | GLES20.GL_CLAMP_TO_EDGE); 227 | GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, 228 | GLES20.GL_CLAMP_TO_EDGE); 229 | GlUtil.checkGlError("glTexParameter"); 230 | 231 | return texId; 232 | } 233 | 234 | /** 235 | * Configures the convolution filter values. 236 | * 237 | * @param values Normalized filter values; must be KERNEL_SIZE elements. 238 | */ 239 | public void setKernel(float[] values, float colorAdj) { 240 | if (values.length != KERNEL_SIZE) { 241 | throw new IllegalArgumentException("Kernel size is " + values.length + 242 | " vs. " + KERNEL_SIZE); 243 | } 244 | System.arraycopy(values, 0, mKernel, 0, KERNEL_SIZE); 245 | mColorAdjust = colorAdj; 246 | //Log.d(TAG, "filt kernel: " + Arrays.toString(mKernel) + ", adj=" + colorAdj); 247 | } 248 | 249 | /** 250 | * Sets the size of the texture. This is used to find adjacent texels when filtering. 251 | */ 252 | public void setTexSize(int width, int height) { 253 | float rw = 1.0f / width; 254 | float rh = 1.0f / height; 255 | 256 | // Don't need to create a new array here, but it's syntactically convenient. 257 | mTexOffset = new float[] { 258 | -rw, -rh, 0f, -rh, rw, -rh, 259 | -rw, 0f, 0f, 0f, rw, 0f, 260 | -rw, rh, 0f, rh, rw, rh 261 | }; 262 | //Log.d(TAG, "filt size: " + width + "x" + height + ": " + Arrays.toString(mTexOffset)); 263 | } 264 | 265 | /** 266 | * Issues the draw call. Does the full setup on every call. 267 | * 268 | * @param mvpMatrix The 4x4 projection matrix. 269 | * @param vertexBuffer Buffer with vertex position data. 270 | * @param firstVertex Index of first vertex to use in vertexBuffer. 271 | * @param vertexCount Number of vertices in vertexBuffer. 272 | * @param coordsPerVertex The number of coordinates per vertex (e.g. x,y is 2). 273 | * @param vertexStride Width, in bytes, of the position data for each vertex (often 274 | * vertexCount * sizeof(float)). 275 | * @param texMatrix A 4x4 transformation matrix for texture coords. (Primarily intended 276 | * for use with SurfaceTexture.) 277 | * @param texBuffer Buffer with vertex texture data. 278 | * @param texStride Width, in bytes, of the texture data for each vertex. 279 | */ 280 | public void draw(float[] mvpMatrix, FloatBuffer vertexBuffer, int firstVertex, 281 | int vertexCount, int coordsPerVertex, int vertexStride, 282 | float[] texMatrix, FloatBuffer texBuffer, int textureId, int texStride) { 283 | GlUtil.checkGlError("draw start"); 284 | 285 | // Select the program. 286 | GLES20.glUseProgram(mProgramHandle); 287 | GlUtil.checkGlError("glUseProgram"); 288 | 289 | // Set the texture. 290 | GLES20.glActiveTexture(GLES20.GL_TEXTURE0); 291 | GLES20.glBindTexture(mTextureTarget, textureId); 292 | 293 | // Copy the model / view / projection matrix over. 294 | GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mvpMatrix, 0); 295 | GlUtil.checkGlError("glUniformMatrix4fv"); 296 | 297 | // Copy the texture transformation matrix over. 298 | GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, texMatrix, 0); 299 | GlUtil.checkGlError("glUniformMatrix4fv"); 300 | 301 | // Enable the "aPosition" vertex attribute. 302 | GLES20.glEnableVertexAttribArray(maPositionLoc); 303 | GlUtil.checkGlError("glEnableVertexAttribArray"); 304 | 305 | // Connect vertexBuffer to "aPosition". 306 | GLES20.glVertexAttribPointer(maPositionLoc, coordsPerVertex, 307 | GLES20.GL_FLOAT, false, vertexStride, vertexBuffer); 308 | GlUtil.checkGlError("glVertexAttribPointer"); 309 | 310 | // Enable the "aTextureCoord" vertex attribute. 311 | GLES20.glEnableVertexAttribArray(maTextureCoordLoc); 312 | GlUtil.checkGlError("glEnableVertexAttribArray"); 313 | 314 | // Connect texBuffer to "aTextureCoord". 315 | GLES20.glVertexAttribPointer(maTextureCoordLoc, 2, 316 | GLES20.GL_FLOAT, false, texStride, texBuffer); 317 | GlUtil.checkGlError("glVertexAttribPointer"); 318 | 319 | // Populate the convolution kernel, if present. 320 | if (muKernelLoc >= 0) { 321 | GLES20.glUniform1fv(muKernelLoc, KERNEL_SIZE, mKernel, 0); 322 | GLES20.glUniform2fv(muTexOffsetLoc, KERNEL_SIZE, mTexOffset, 0); 323 | GLES20.glUniform1f(muColorAdjustLoc, mColorAdjust); 324 | } 325 | 326 | // Draw the rect. 327 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, firstVertex, vertexCount); 328 | GlUtil.checkGlError("glDrawArrays"); 329 | 330 | // Done -- disable vertex array, texture, and program. 331 | GLES20.glDisableVertexAttribArray(maPositionLoc); 332 | GLES20.glDisableVertexAttribArray(maTextureCoordLoc); 333 | GLES20.glBindTexture(mTextureTarget, 0); 334 | GLES20.glUseProgram(0); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/gles/WindowSurface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.gles; 18 | 19 | import android.graphics.SurfaceTexture; 20 | import android.view.Surface; 21 | 22 | /** 23 | * Recordable EGL window surface. 24 | *

25 | * It's good practice to explicitly release() the surface, preferably from a "finally" block. 26 | */ 27 | public class WindowSurface extends EglSurfaceBase { 28 | private Surface mSurface; 29 | private boolean mReleaseSurface; 30 | 31 | /** 32 | * Associates an EGL surface with the native window surface. 33 | *

34 | * Set releaseSurface to true if you want the Surface to be released when release() is 35 | * called. This is convenient, but can interfere with framework classes that expect to 36 | * manage the Surface themselves (e.g. if you release a SurfaceView's Surface, the 37 | * surfaceDestroyed() callback won't fire). 38 | */ 39 | public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) { 40 | super(eglCore); 41 | createWindowSurface(surface); 42 | mSurface = surface; 43 | mReleaseSurface = releaseSurface; 44 | } 45 | 46 | /** 47 | * Associates an EGL surface with the SurfaceTexture. 48 | */ 49 | public WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) { 50 | super(eglCore); 51 | createWindowSurface(surfaceTexture); 52 | } 53 | 54 | /** 55 | * Releases any resources associated with the EGL surface (and, if configured to do so, 56 | * with the Surface as well). 57 | *

58 | * Does not require that the surface's EGL context be current. 59 | */ 60 | public void release() { 61 | releaseEglSurface(); 62 | if (mSurface != null) { 63 | if (mReleaseSurface) { 64 | mSurface.release(); 65 | } 66 | mSurface = null; 67 | } 68 | } 69 | 70 | /** 71 | * Recreate the EGLSurface, using the new EglBase. The caller should have already 72 | * freed the old EGLSurface with releaseEglSurface(). 73 | *

74 | * This is useful when we want to update the EGLSurface associated with a Surface. 75 | * For example, if we want to share with a different EGLContext, which can only 76 | * be done by tearing down and recreating the context. (That's handled by the caller; 77 | * this just creates a new EGLSurface for the Surface we were handed earlier.) 78 | *

79 | * If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a 80 | * context somewhere, the create call will fail with complaints from the Surface 81 | * about already being connected. 82 | */ 83 | public void recreate(EglCore newEglCore) { 84 | if (mSurface == null) { 85 | throw new RuntimeException("not yet implemented for SurfaceTexture"); 86 | } 87 | mEglCore = newEglCore; // switch to new context 88 | createWindowSurface(mSurface); // create new surface 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/util/CameraUtils.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera.util; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.pm.FeatureInfo; 5 | import android.content.pm.PackageManager; 6 | import android.hardware.Camera; 7 | import android.os.Build; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.Comparator; 12 | import java.util.List; 13 | 14 | import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT; 15 | 16 | 17 | public class CameraUtils { 18 | private static final String TAG = "CameraUtils"; 19 | 20 | public static Camera.Size chooseOptimalSize(List choices, int preferPreviewWidth, int preferPreviewHeight) { 21 | List bigEnough = new ArrayList<>(); 22 | List notBigEnough = new ArrayList<>(); 23 | for (Camera.Size option : choices) { 24 | if (option.width >= preferPreviewWidth && option.height >= preferPreviewHeight) { 25 | bigEnough.add(option); 26 | } else { 27 | notBigEnough.add(option); 28 | } 29 | } 30 | if (bigEnough.size() > 0) { 31 | return Collections.min(bigEnough, new CompareSizesByArea()); 32 | } else if (notBigEnough.size() > 0) { 33 | return Collections.max(notBigEnough, new CompareSizesByArea()); 34 | } else { 35 | return choices.get(0); 36 | } 37 | } 38 | 39 | private static class CompareSizesByArea implements Comparator { 40 | @Override 41 | public int compare(Camera.Size lhs, Camera.Size rhs) { 42 | return Long.signum((long) lhs.width * lhs.height - (long) rhs.width * rhs.height); 43 | } 44 | } 45 | 46 | private static boolean checkCameraFacing(final int facing) { 47 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { 48 | return false; 49 | } 50 | final int cameraCount = Camera.getNumberOfCameras(); 51 | Camera.CameraInfo info = new Camera.CameraInfo(); 52 | for (int i = 0; i < cameraCount; i++) { 53 | Camera.getCameraInfo(i, info); 54 | if (facing == info.facing) { 55 | return true; 56 | } 57 | } 58 | return false; 59 | } 60 | 61 | @SuppressLint("ObsoleteSdkInt") 62 | public static boolean isSupportFront() { 63 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) return false; 64 | final int cameraCount = Camera.getNumberOfCameras(); 65 | Camera.CameraInfo info = new Camera.CameraInfo(); 66 | for (int i = 0; i < cameraCount; i++) { 67 | Camera.getCameraInfo(i, info); 68 | if (info.facing == CAMERA_FACING_FRONT) { 69 | return true; 70 | } 71 | } 72 | return false; 73 | } 74 | 75 | 76 | public static boolean isSupportLedFlash(PackageManager packageManager) { 77 | FeatureInfo[] availableFeatures = packageManager.getSystemAvailableFeatures(); 78 | if (availableFeatures != null) { 79 | for (FeatureInfo featureInfo : availableFeatures) { 80 | if (featureInfo != null && PackageManager.FEATURE_CAMERA_FLASH.equals(featureInfo.name)) { 81 | return true; 82 | } 83 | } 84 | } 85 | return false; 86 | } 87 | 88 | public static boolean isDevice(String... devices) { 89 | String model = Build.MODEL; 90 | if (devices != null && model != null) { 91 | for (String device : devices) { 92 | if (model.trim().contains(device)) { 93 | return true; 94 | } 95 | } 96 | } 97 | return false; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/widgets/AutoFitGLSurfaceView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.felix.glcamera.widgets; 18 | 19 | import android.content.Context; 20 | import android.opengl.GLSurfaceView; 21 | import android.util.AttributeSet; 22 | import android.view.TextureView; 23 | 24 | /** 25 | * A {@link TextureView} that can be adjusted to a specified aspect ratio. 26 | */ 27 | public class AutoFitGLSurfaceView extends GLSurfaceView { 28 | 29 | private int mRatioWidth = 0; 30 | private int mRatioHeight = 0; 31 | 32 | public AutoFitGLSurfaceView(Context context) { 33 | super(context); 34 | } 35 | 36 | public AutoFitGLSurfaceView(Context context, AttributeSet attrs) { 37 | super(context, attrs); 38 | } 39 | 40 | 41 | /** 42 | * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio 43 | * calculated from the parameters. Note that the actual sizes of parameters don't matter, that 44 | * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result. 45 | * 46 | * @param width Relative horizontal size 47 | * @param height Relative vertical size 48 | */ 49 | public void setAspectRatio(int width, int height) { 50 | if (width < 0 || height < 0) { 51 | throw new IllegalArgumentException("Size cannot be negative."); 52 | } 53 | mRatioWidth = width; 54 | mRatioHeight = height; 55 | requestLayout(); 56 | } 57 | 58 | @Override 59 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 60 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 61 | int width = MeasureSpec.getSize(widthMeasureSpec); 62 | int height = MeasureSpec.getSize(heightMeasureSpec); 63 | if (0 == mRatioWidth || 0 == mRatioHeight) { 64 | setMeasuredDimension(width, height); 65 | } else { 66 | if (width < height * mRatioWidth / mRatioHeight) { 67 | setMeasuredDimension(width, width * mRatioHeight / mRatioWidth); 68 | } else { 69 | setMeasuredDimension(height * mRatioWidth / mRatioHeight, height); 70 | } 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/felix/glcamera/widgets/ProgressView.java: -------------------------------------------------------------------------------- 1 | package com.felix.glcamera.widgets; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.RectF; 9 | import android.support.annotation.Nullable; 10 | import android.util.AttributeSet; 11 | import android.util.Log; 12 | import android.view.GestureDetector; 13 | import android.view.MotionEvent; 14 | import android.view.View; 15 | import android.view.animation.AccelerateInterpolator; 16 | 17 | public class ProgressView extends View { 18 | 19 | private Paint mArcPaint; 20 | private Paint mInnerCirclePaint; 21 | private Paint mOuterCirclePaint; 22 | 23 | private float mCurrentScaleFactor = SCALE_FACTOR_INIT; 24 | private static final float SCALE_FACTOR_MAX = 1.5f; 25 | private static final float SCALE_FACTOR_INIT = 1.0f; 26 | private static final int MAX_SCALE_DURATION = 150; 27 | private GestureDetector mGestureDetector; 28 | private long mCurrentDuration; 29 | 30 | 31 | public ProgressView(Context context) { 32 | super(context); 33 | init(); 34 | } 35 | 36 | public ProgressView(Context context, @Nullable AttributeSet attrs) { 37 | super(context, attrs); 38 | init(); 39 | } 40 | 41 | public ProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 42 | super(context, attrs, defStyleAttr); 43 | init(); 44 | } 45 | 46 | 47 | private void init() { 48 | mArcPaint = new Paint(); 49 | mArcPaint.setAntiAlias(true); 50 | mArcPaint.setStyle(Paint.Style.STROKE); 51 | mArcPaint.setStrokeCap(Paint.Cap.ROUND); 52 | mArcPaint.setColor(Color.parseColor("#FF24ff00")); 53 | 54 | mOuterCirclePaint = new Paint(); 55 | mOuterCirclePaint.setAntiAlias(true); 56 | mOuterCirclePaint.setStyle(Paint.Style.FILL); 57 | mOuterCirclePaint.setColor(Color.parseColor("#FFB3A8A4")); 58 | 59 | mInnerCirclePaint = new Paint(); 60 | mInnerCirclePaint.setAntiAlias(true); 61 | mInnerCirclePaint.setStyle(Paint.Style.FILL); 62 | mInnerCirclePaint.setColor(getResources().getColor(android.R.color.white)); 63 | 64 | mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { 65 | @Override 66 | public boolean onDown(MotionEvent e) { 67 | return true; 68 | } 69 | 70 | @Override 71 | public void onLongPress(MotionEvent e) { 72 | super.onLongPress(e); 73 | startProgress(); 74 | } 75 | }); 76 | } 77 | 78 | 79 | @Override 80 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 81 | int width = MeasureSpec.getSize(widthMeasureSpec); 82 | int height = MeasureSpec.getSize(heightMeasureSpec); 83 | setMeasuredDimension((int) (width * mCurrentScaleFactor), (int) (height * mCurrentScaleFactor)); 84 | } 85 | 86 | private ValueAnimator mScaleAnimator; 87 | 88 | private void animateScale(float end) { 89 | if (mScaleAnimator != null) { 90 | mScaleAnimator.cancel(); 91 | } 92 | int duration = (int) ((Math.abs(end - mCurrentScaleFactor) / Math.abs(SCALE_FACTOR_MAX - SCALE_FACTOR_INIT)) * MAX_SCALE_DURATION); 93 | if (duration != 0) { 94 | mScaleAnimator = new ValueAnimator(); 95 | mScaleAnimator.setInterpolator(new AccelerateInterpolator()); 96 | mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 97 | @Override 98 | public void onAnimationUpdate(ValueAnimator animation) { 99 | Object scale = animation.getAnimatedValue(); 100 | if (scale instanceof Float) { 101 | mCurrentScaleFactor = (float) scale; 102 | requestLayout(); 103 | } 104 | } 105 | }); 106 | mScaleAnimator.setDuration(duration); 107 | mScaleAnimator.setFloatValues(mCurrentScaleFactor, end); 108 | mScaleAnimator.start(); 109 | } 110 | } 111 | 112 | 113 | private boolean isLongPressStatus = false; 114 | private long mStartTime; 115 | 116 | 117 | @Override 118 | public boolean onTouchEvent(MotionEvent event) { 119 | mGestureDetector.onTouchEvent(event); 120 | int action = event.getAction(); 121 | switch (action) { 122 | case MotionEvent.ACTION_CANCEL: 123 | cancelProgress(); 124 | break; 125 | case MotionEvent.ACTION_UP: 126 | stopProgress(); 127 | break; 128 | } 129 | return true; 130 | } 131 | 132 | private static final int MAX_DURATION_DEFAULT = 10 * 1000; 133 | private int mMaxDuration = MAX_DURATION_DEFAULT; 134 | 135 | public void setMaxDuration(int maxDuration) { 136 | this.mMaxDuration = maxDuration; 137 | } 138 | 139 | private OnProgressListener mOnProgressListener; 140 | 141 | public void setOnProgressListener(OnProgressListener listener) { 142 | this.mOnProgressListener = listener; 143 | } 144 | 145 | public interface OnProgressListener { 146 | void onProgressStart(); 147 | 148 | void onProgressCancel(); 149 | 150 | void onProgressEnd(float progress, long duration); 151 | } 152 | 153 | private RectF mArcRecF; 154 | 155 | private int dp2px(float dip) { 156 | float scale = getResources().getDisplayMetrics().density; 157 | return (int) (dip * scale + 0.5f); 158 | } 159 | 160 | @Override 161 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 162 | super.onSizeChanged(w, h, oldw, oldh); 163 | int strokeWidth = dp2px(8f); 164 | mArcPaint.setStrokeWidth(strokeWidth); 165 | int halfStroke = strokeWidth / 2; 166 | RectF viewRect = new RectF(halfStroke, halfStroke, w - halfStroke, h - halfStroke); 167 | float centerX = viewRect.centerX(); 168 | float centerY = viewRect.centerY(); 169 | int size = Math.min(w, h); 170 | mArcRecF = new RectF(halfStroke, halfStroke, size - halfStroke, size - halfStroke); 171 | mArcRecF.offset(centerX - mArcRecF.centerX(), centerY - mArcRecF.centerY()); 172 | } 173 | 174 | private void startProgress() { 175 | if (!isLongPressStatus) { 176 | isLongPressStatus = true; 177 | removeCallbacks(mStopRunnable); 178 | removeCallbacks(mCancelRunnable); 179 | removeCallbacks(mStartRunnable); 180 | post(mStartRunnable); 181 | } 182 | } 183 | 184 | public void reset() { 185 | isLongPressStatus = false; 186 | mStartTime = 0; 187 | mCurrentDuration = 0; 188 | mCurrentScaleFactor = SCALE_FACTOR_INIT; 189 | removeCallbacks(mStopRunnable); 190 | removeCallbacks(mCancelRunnable); 191 | removeCallbacks(mStartRunnable); 192 | if (mScaleAnimator != null) { 193 | mScaleAnimator.cancel(); 194 | } 195 | requestLayout(); 196 | } 197 | 198 | private void cancelProgress() { 199 | if (isLongPressStatus) { 200 | isLongPressStatus = false; 201 | removeCallbacks(mStopRunnable); 202 | removeCallbacks(mCancelRunnable); 203 | removeCallbacks(mStartRunnable); 204 | post(mCancelRunnable); 205 | } 206 | } 207 | 208 | private void stopProgress() { 209 | if (isLongPressStatus) { 210 | isLongPressStatus = false; 211 | removeCallbacks(mStopRunnable); 212 | removeCallbacks(mCancelRunnable); 213 | removeCallbacks(mStartRunnable); 214 | post(mStopRunnable); 215 | } 216 | } 217 | 218 | private static final String TAG = "ProgressView"; 219 | 220 | @Override 221 | protected void onDraw(Canvas canvas) { 222 | super.onDraw(canvas); 223 | int width = getWidth(); 224 | int height = getHeight(); 225 | int r = Math.min(width, height) / 2; 226 | int cx = width / 2; 227 | int cy = height / 2; 228 | //大圆 229 | canvas.drawCircle(cx, cy, r, mOuterCirclePaint); 230 | //小圆 231 | canvas.drawCircle(cx, cy, r * 0.7f * (2 - mCurrentScaleFactor), mInnerCirclePaint); 232 | if (isLongPressStatus) { 233 | mCurrentDuration = System.currentTimeMillis() - mStartTime; 234 | if (mCurrentDuration >= mMaxDuration) { 235 | mCurrentDuration = mMaxDuration; 236 | stopProgress(); 237 | } 238 | } 239 | canvas.drawArc(mArcRecF, -90, (float) mCurrentDuration / (float) mMaxDuration * 360, false, mArcPaint); 240 | if (isLongPressStatus) { 241 | invalidate(); 242 | } 243 | } 244 | 245 | 246 | private Runnable mStartRunnable = new Runnable() { 247 | @Override 248 | public void run() { 249 | if (mOnProgressListener != null) { 250 | mOnProgressListener.onProgressStart(); 251 | } 252 | mStartTime = System.currentTimeMillis(); 253 | animateScale(SCALE_FACTOR_MAX); 254 | Log.e(TAG, "StartRunnable:" + mCurrentScaleFactor); 255 | } 256 | }; 257 | 258 | private Runnable mCancelRunnable = new Runnable() { 259 | @Override 260 | public void run() { 261 | mStartTime = 0; 262 | mCurrentDuration = 0; 263 | if (mOnProgressListener != null) { 264 | mOnProgressListener.onProgressCancel(); 265 | } 266 | animateScale(SCALE_FACTOR_INIT); 267 | Log.e(TAG, "CancelRunnable:" + mCurrentScaleFactor); 268 | } 269 | }; 270 | 271 | 272 | private Runnable mStopRunnable = new Runnable() { 273 | @Override 274 | public void run() { 275 | mCurrentDuration = Math.min(System.currentTimeMillis() - mStartTime, mMaxDuration); 276 | if (mOnProgressListener != null) { 277 | mOnProgressListener.onProgressEnd((float) mCurrentDuration / (float) mMaxDuration, mCurrentDuration); 278 | } 279 | animateScale( SCALE_FACTOR_INIT); 280 | } 281 | }; 282 | 283 | } 284 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_flash_led_off_disable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_flash_led_off_disable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_flash_led_off_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_flash_led_off_normal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_flash_led_off_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_flash_led_off_pressed.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_flash_led_on_disable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_flash_led_on_disable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_flash_led_on_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_flash_led_on_normal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_flash_led_on_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_flash_led_on_pressed.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_switch_disable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_switch_disable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_switch_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_switch_normal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_camera_switch_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_camera_switch_pressed.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_cancel_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_cancel_normal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_cancel_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_cancel_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/record_ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/drawable-xhdpi/record_ok.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/record_camera_flash_led_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/record_camera_switch_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/record_cancel_seletor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/record_shape_oval_gray.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/record_shape_oval_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_camera_record.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 23 | 24 | 30 | 31 | 35 | 36 | 43 | 44 | 51 | 52 | 59 | 60 | 61 | 62 | 63 | 69 | 70 | 74 | 75 | 83 | 84 | 88 | 89 | 94 | 95 | 99 | 100 | 108 | 109 | 113 | 114 | 115 | 124 | 125 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITcrazywgy/GLCamera/0a3b7d81d4fe936b5aef8afecfe6e7b4aed9f892/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | GLCamera 21 | 22 | 23 | Normal 24 | Black & white 25 | Filter: blur 26 | Filter: sharpen 27 | Filter: edge detect 28 | Filter: emboss 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |