├── .gitignore ├── app ├── .gitignore ├── android.keystore ├── build.gradle ├── gradle.properties ├── local.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── com │ │ └── mcxiaoke │ │ │ └── media │ │ │ ├── CameraConfig.java │ │ │ ├── CameraUtils.java │ │ │ ├── Const.java │ │ │ ├── Main.java │ │ │ ├── MediaCodecActivity.java │ │ │ ├── MediaRecorderActivity.java │ │ │ ├── VRecorder.java │ │ │ ├── av │ │ │ ├── AVRecorder.java │ │ │ ├── AndroidEncoder.java │ │ │ ├── AndroidMuxer.java │ │ │ ├── AudioEncoderConfig.java │ │ │ ├── AudioEncoderCore.java │ │ │ ├── CameraEncoder.java │ │ │ ├── CameraSurfaceRenderer.java │ │ │ ├── Drawable2d.java │ │ │ ├── EglCore.java │ │ │ ├── EglStateSaver.java │ │ │ ├── EglSurfaceBase.java │ │ │ ├── Filters.java │ │ │ ├── FullFrameRect.java │ │ │ ├── GlUtil.java │ │ │ ├── MicrophoneEncoder.java │ │ │ ├── Muxer.java │ │ │ ├── SessionConfig.java │ │ │ ├── SizeableFrameRect.java │ │ │ ├── Texture2dProgram.java │ │ │ ├── VideoEncoderConfig.java │ │ │ ├── VideoEncoderCore.java │ │ │ └── WindowSurface.java │ │ │ ├── lib │ │ │ ├── CaptureConfig.java │ │ │ ├── CaptureFragment.java │ │ │ └── CaptureHelper.java │ │ │ ├── ocodec │ │ │ ├── CodecUtils.java │ │ │ ├── Config.java │ │ │ ├── encoder │ │ │ │ ├── AudioEncoder.java │ │ │ │ ├── Encoder.java │ │ │ │ ├── Muxer.java │ │ │ │ ├── Recorder.java │ │ │ │ └── VideoEncoder.java │ │ │ └── glutils │ │ │ │ ├── EGLBase.java │ │ │ │ ├── GLDrawer2D.java │ │ │ │ └── RenderHandler.java │ │ │ ├── recoder │ │ │ └── CameraProxy.java │ │ │ ├── support │ │ │ ├── AvcEncoder.java │ │ │ ├── CameraPreview.java │ │ │ ├── CameraRecordingStream.java │ │ │ ├── CameraToMpegTest.java │ │ │ └── VideoEncoderCore.java │ │ │ └── widget │ │ │ ├── AspectRatio.java │ │ │ ├── Camera1.java │ │ │ ├── Camera2.java │ │ │ ├── Camera2Api23.java │ │ │ ├── CameraView.java │ │ │ ├── CameraViewImpl.java │ │ │ ├── Constants.java │ │ │ ├── DisplayOrientationDetector.java │ │ │ ├── PreviewImpl.java │ │ │ ├── Size.java │ │ │ ├── SizeMap.java │ │ │ ├── SurfaceViewPreview.java │ │ │ └── TextureViewPreview.java │ └── io │ │ └── kickflip │ │ └── sdk │ │ ├── FileUtils.java │ │ ├── Share.java │ │ ├── Util.java │ │ ├── av │ │ ├── AVRecorder.java │ │ ├── AndroidEncoder.java │ │ ├── AndroidMuxer.java │ │ ├── AudioEncoderConfig.java │ │ ├── AudioEncoderCore.java │ │ ├── CameraEncoder.java │ │ ├── CameraSurfaceRenderer.java │ │ ├── Drawable2d.java │ │ ├── EglCore.java │ │ ├── EglStateSaver.java │ │ ├── EglSurfaceBase.java │ │ ├── Filters.java │ │ ├── FullFrameRect.java │ │ ├── GlUtil.java │ │ ├── MicrophoneEncoder.java │ │ ├── Muxer.java │ │ ├── SessionConfig.java │ │ ├── SizeableFrameRect.java │ │ ├── Texture2dProgram.java │ │ ├── VideoEncoderConfig.java │ │ ├── VideoEncoderCore.java │ │ └── WindowSurface.java │ │ ├── exception │ │ └── KickflipException.java │ │ ├── location │ │ ├── DeviceLocation.java │ │ └── package-info.java │ │ └── view │ │ ├── GLCameraEncoderView.java │ │ ├── GLCameraView.java │ │ └── package-info.java │ └── res │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ ├── main.xml │ ├── media_recorder.xml │ ├── surface_view.xml │ └── texture_view.xml │ └── values │ ├── attrs.xml │ ├── public.xml │ └── styles.xml ├── build.gradle ├── deploy-local.sh ├── deploy-remote.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | build/ 4 | apks/ 5 | repo/ 6 | dist/ 7 | tmp/ 8 | *.iml 9 | *.apk 10 | *.pyc 11 | *.d 12 | *.o 13 | *.class 14 | .DS_Store 15 | a.out 16 | .classpath 17 | .project 18 | .settings/ 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | packer.properties 3 | -------------------------------------------------------------------------------- /app/android.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/MediaCodec/76fce088a9a3d19c5dda4427dcc870cec15fb783/app/android.keystore -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | maven { url '/tmp/repo/' } 3 | jcenter() 4 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 5 | } 6 | 7 | apply plugin: 'com.android.application' 8 | 9 | dependencies { 10 | compile 'com.jakewharton:butterknife:8.6.0' 11 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0' 12 | } 13 | 14 | android { 15 | 16 | compileOptions { 17 | sourceCompatibility JavaVersion.VERSION_1_7 18 | targetCompatibility JavaVersion.VERSION_1_7 19 | encoding "UTF-8" 20 | } 21 | 22 | compileSdkVersion project.compileSdkVersion 23 | buildToolsVersion project.buildToolsVersion 24 | 25 | defaultConfig { 26 | versionName project.VERSION_NAME 27 | versionCode Integer.parseInt(project.VERSION_CODE) 28 | minSdkVersion project.minSdkVersion 29 | targetSdkVersion project.targetSdkVersion 30 | } 31 | 32 | signingConfigs { 33 | release { 34 | storeFile file("android.keystore") 35 | storePassword "android" 36 | keyAlias "android" 37 | keyPassword "android" 38 | v2SigningEnabled true 39 | } 40 | 41 | } 42 | 43 | buildTypes { 44 | release { 45 | signingConfig signingConfigs.release 46 | minifyEnabled false 47 | } 48 | 49 | debug { 50 | signingConfig signingConfigs.release 51 | } 52 | 53 | } 54 | 55 | lintOptions { 56 | abortOnError false 57 | htmlReport true 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/local.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | #Thu Dec 11 11:24:17 CST 2014 11 | sdk.dir=/Users/mcxiaoke/develop/android-sdk-macosx 12 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/CameraConfig.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media; 2 | 3 | /** 4 | * User: mcxiaoke 5 | * Date: 2017/7/4 6 | * Time: 14:29 7 | */ 8 | 9 | public class CameraConfig { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/CameraUtils.java: -------------------------------------------------------------------------------- 1 | /*** 2 | Copyright (c) 2013 CommonsWare, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | not use this file except in compliance with the License. You may obtain 6 | a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package com.mcxiaoke.media; 16 | 17 | import android.hardware.Camera; 18 | import android.hardware.Camera.Size; 19 | import android.util.Log; 20 | 21 | import java.util.Collections; 22 | import java.util.Comparator; 23 | import java.util.List; 24 | 25 | public class CameraUtils { 26 | 27 | private static final double ASPECT_TOLERANCE = 0.1; 28 | 29 | public static Size getOptimalVideoSize(int width, 30 | int height, 31 | Camera.Parameters parameters) { 32 | return getOptimalPreviewSize(width, height, 33 | parameters.getSupportedVideoSizes()); 34 | } 35 | 36 | public static Size getOptimalPreviewSize(Camera.Size videoSize, Camera.Parameters parameters) { 37 | return getBestAspectPreviewSize(videoSize.width * 2, videoSize.height * 2, parameters); 38 | } 39 | 40 | private static Size getOptimalPreviewSize(int width, 41 | int height, 42 | List sizes) { 43 | double targetRatio = (double) width / height; 44 | Size optimalSize = null; 45 | int minDiff = Integer.MAX_VALUE; 46 | int targetHeight = height; 47 | // if (displayOrientation == 90 || displayOrientation == 270) { 48 | // targetRatio = (double) height / width; 49 | // } 50 | Log.v(Const.TAG, "checkSize: targetRatio=" + targetRatio + " targetHeight=" + targetHeight); 51 | // Try to find an size match aspect ratio and size 52 | 53 | for (Size size : sizes) { 54 | double ratio = (double) size.width / size.height; 55 | if (Math.abs(ratio - targetRatio) <= ASPECT_TOLERANCE) { 56 | Log.v(Const.TAG, "checkSize: matched w=" + size.width + " h=" + size.height); 57 | if (Math.abs(size.height - targetHeight) < minDiff) { 58 | optimalSize = size; 59 | minDiff = Math.abs(size.height - targetHeight); 60 | } 61 | } 62 | } 63 | 64 | // Cannot find the one match the aspect ratio, ignore 65 | // the requirement 66 | 67 | if (optimalSize == null) { 68 | minDiff = Integer.MAX_VALUE; 69 | 70 | for (Size size : sizes) { 71 | if (Math.abs(size.height - targetHeight) < minDiff) { 72 | Log.v(Const.TAG, "checkSize2: matched w=" + size.width + " h=" + size.height); 73 | optimalSize = size; 74 | minDiff = Math.abs(size.height - targetHeight); 75 | } 76 | } 77 | } 78 | 79 | return (optimalSize); 80 | } 81 | 82 | public static Size getBestAspectPreviewSize(int width, 83 | int height, 84 | Camera.Parameters parameters) { 85 | return (getBestAspectPreviewSize(width, height, parameters, 0.01d)); 86 | } 87 | 88 | public static Size getBestAspectPreviewSize(int width, 89 | int height, 90 | Camera.Parameters parameters, 91 | double closeEnough) { 92 | double targetRatio = (double) width / height; 93 | Size optimalSize = null; 94 | double minDiff = Double.MAX_VALUE; 95 | 96 | List sizes = parameters.getSupportedPreviewSizes(); 97 | 98 | Collections.sort(sizes, 99 | Collections.reverseOrder(new SizeComparator())); 100 | 101 | for (Size size : sizes) { 102 | double ratio = (double) size.width / size.height; 103 | 104 | Log.v(Const.TAG, "check best ratio w=" + size.width + " h=" + size.height); 105 | 106 | if (Math.abs(ratio - targetRatio) < minDiff) { 107 | optimalSize = size; 108 | minDiff = Math.abs(ratio - targetRatio); 109 | } 110 | 111 | if (minDiff < closeEnough) { 112 | break; 113 | } 114 | } 115 | 116 | return (optimalSize); 117 | } 118 | 119 | private static class SizeComparator implements 120 | Comparator { 121 | @Override 122 | public int compare(Size lhs, Size rhs) { 123 | int left = lhs.width * lhs.height; 124 | int right = rhs.width * rhs.height; 125 | 126 | if (left < right) { 127 | return (-1); 128 | } else if (left > right) { 129 | return (1); 130 | } 131 | 132 | return (0); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/Const.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media; 2 | 3 | import android.os.Environment; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * User: mcxiaoke 9 | * Date: 2017/6/29 10 | * Time: 14:15 11 | */ 12 | 13 | public class Const { 14 | public static final String TAG = "MediaCodec"; 15 | 16 | // parameters for the encoder 17 | private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding 18 | private static final int FRAME_RATE = 24; // 24fps 19 | private static final int I_FRAME_INTERVAL = 10; // 10 seconds between I-frames 20 | private static final File OUTPUT_DIR = new File(Environment.getExternalStorageDirectory(), "test"); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/Main.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media; 2 | 3 | import android.media.CamcorderProfile; 4 | 5 | /** 6 | * User: mcxiaoke 7 | * Date: 2017/7/4 8 | * Time: 14:00 9 | */ 10 | 11 | public class Main { 12 | 13 | public static void test(){ 14 | CamcorderProfile p; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/MediaCodecActivity.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media; 2 | 3 | import android.app.Activity; 4 | import android.graphics.SurfaceTexture; 5 | import android.hardware.Camera; 6 | import android.os.Bundle; 7 | import android.view.TextureView; 8 | import android.view.TextureView.SurfaceTextureListener; 9 | 10 | import java.io.IOException; 11 | 12 | public class MediaCodecActivity extends Activity implements SurfaceTextureListener { 13 | private Camera mCamera; 14 | private TextureView mTextureView; 15 | 16 | @Override 17 | protected void onCreate(final Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.main); 20 | mTextureView = (TextureView) findViewById(R.id.texture); 21 | mTextureView.setSurfaceTextureListener(this); 22 | } 23 | 24 | @Override 25 | public void onSurfaceTextureAvailable(final SurfaceTexture surface, 26 | final int width, final int height) { 27 | mCamera = Camera.open(); 28 | try { 29 | mCamera.setPreviewTexture(surface); 30 | mCamera.startPreview(); 31 | } catch (IOException ioe) { 32 | // Something bad happened 33 | } 34 | } 35 | 36 | @Override 37 | public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, 38 | final int width, final int height) { 39 | // Ignored, Camera does all the work for us 40 | } 41 | 42 | @Override 43 | public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) { 44 | mCamera.stopPreview(); 45 | mCamera.release(); 46 | return true; 47 | } 48 | 49 | @Override 50 | public void onSurfaceTextureUpdated(final SurfaceTexture surface) { 51 | // Invoked every time there's a new Camera preview frame 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/VRecorder.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager; 5 | import android.media.MediaCodecInfo; 6 | import android.media.MediaCodecInfo.CodecCapabilities; 7 | import android.media.MediaCodecList; 8 | import android.util.Log; 9 | import com.mcxiaoke.media.ocodec.Config; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | /** 16 | * User: mcxiaoke 17 | * Date: 2017/7/7 18 | * Time: 16:43 19 | */ 20 | 21 | public class VRecorder { 22 | public static final String TAG = "VRecorder"; 23 | public static final boolean DEBUG = true; 24 | 25 | public static final String CODEC_VIDEO_H264 = Config.CODEC_VIDEO_H264; 26 | public static final String CODEC_AUDIO_AAC = Config.CODEC_AUDIO_AAC; 27 | public static final String[] H264_ACC = new String[]{CODEC_VIDEO_H264, CODEC_AUDIO_AAC}; 28 | 29 | private static List sSupportedEncoders; 30 | // private static List sSupportedTypes; 31 | private static boolean sSupported; 32 | 33 | static { 34 | int total = MediaCodecList.getCodecCount(); 35 | List codecs = new ArrayList<>(); 36 | List types = new ArrayList<>(); 37 | for (int i = 0; i < total; i++) { 38 | MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); 39 | if (!codecInfo.isEncoder()) { 40 | continue; 41 | } 42 | codecs.add(codecInfo); 43 | String[] supportedTypes = codecInfo.getSupportedTypes(); 44 | if (supportedTypes.length > 0) { 45 | types.addAll(Arrays.asList(supportedTypes)); 46 | } 47 | } 48 | sSupportedEncoders = codecs; 49 | // sSupportedTypes = types; 50 | sSupported = types.containsAll(Arrays.asList(H264_ACC)); 51 | } 52 | 53 | public static void showSupportedTypes() { 54 | int total = MediaCodecList.getCodecCount(); 55 | for (int i = 0; i < total; i++) { 56 | MediaCodecInfo codec = MediaCodecList.getCodecInfoAt(i); 57 | if (!codec.isEncoder()) { 58 | continue; 59 | } 60 | String[] types = codec.getSupportedTypes(); 61 | Log.v(TAG, Arrays.toString(types) 62 | + " name" + codec.getName()); 63 | for (String type : types) { 64 | CodecCapabilities caps = codec.getCapabilitiesForType(type); 65 | Log.v(TAG, caps.getMimeType() + " colorFormats:" + Arrays.toString(caps.colorFormats)); 66 | } 67 | } 68 | } 69 | 70 | public static MediaCodecInfo selectCodec(String mimeType) { 71 | for (MediaCodecInfo codec : sSupportedEncoders) { 72 | String[] types = codec.getSupportedTypes(); 73 | for (final String type : types) { 74 | if (type.equalsIgnoreCase(mimeType)) { 75 | return codec; 76 | } 77 | } 78 | } 79 | return null; 80 | } 81 | 82 | public static boolean hasCamera(Context ctx) { 83 | return ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); 84 | } 85 | 86 | public static boolean isSupported(Context context) { 87 | return sSupported && hasCamera(context); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/AVRecorder.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | import io.kickflip.sdk.view.GLCameraView; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * Records an Audio / Video stream to disk. 9 | * 10 | * Example usage: 11 | *
    12 | *
  • AVRecorder recorder = new AVRecorder(mSessionConfig);
  • 13 | *
  • recorder.setPreviewDisplay(mPreviewDisplay);
  • 14 | *
  • recorder.startRecording();
  • 15 | *
  • recorder.stopRecording();
  • 16 | *
  • (Optional) recorder.reset(mNewSessionConfig);
  • 17 | *
  • (Optional) recorder.startRecording();
  • 18 | *
  • (Optional) recorder.stopRecording();
  • 19 | *
  • recorder.release();
  • 20 | *
21 | * 22 | * @hide 23 | */ 24 | public class AVRecorder { 25 | 26 | protected CameraEncoder mCamEncoder; 27 | protected MicrophoneEncoder mMicEncoder; 28 | private SessionConfig mConfig; 29 | private boolean mIsRecording; 30 | 31 | public AVRecorder(SessionConfig config) throws IOException { 32 | init(config); 33 | } 34 | 35 | private void init(SessionConfig config) throws IOException { 36 | mCamEncoder = new CameraEncoder(config); 37 | mMicEncoder = new MicrophoneEncoder(config); 38 | mConfig = config; 39 | mIsRecording = false; 40 | } 41 | 42 | public void setPreviewDisplay(GLCameraView display) { 43 | mCamEncoder.setPreviewDisplay(display); 44 | } 45 | 46 | public void applyFilter(int filter) { 47 | mCamEncoder.applyFilter(filter); 48 | } 49 | 50 | public void requestOtherCamera() { 51 | mCamEncoder.requestOtherCamera(); 52 | } 53 | 54 | public void requestCamera(int camera) { 55 | mCamEncoder.requestCamera(camera); 56 | } 57 | 58 | public void toggleFlash() { 59 | mCamEncoder.toggleFlashMode(); 60 | } 61 | 62 | public void adjustVideoBitrate(int targetBitRate) { 63 | mCamEncoder.adjustBitrate(targetBitRate); 64 | } 65 | 66 | /** 67 | * Signal that the recorder should treat 68 | * incoming video frames as Vertical Video, rotating 69 | * and cropping them for proper display. 70 | * 71 | * This method only has effect if {@link SessionConfig#setConvertVerticalVideo(boolean)} 72 | * has been set true for the current recording session. 73 | */ 74 | public void signalVerticalVideo(FullFrameRect.SCREEN_ROTATION orientation) { 75 | mCamEncoder.signalVerticalVideo(orientation); 76 | } 77 | 78 | public void startRecording() { 79 | mIsRecording = true; 80 | mMicEncoder.startRecording(); 81 | mCamEncoder.startRecording(); 82 | } 83 | 84 | public boolean isRecording() { 85 | return mIsRecording; 86 | } 87 | 88 | public void stopRecording() { 89 | mIsRecording = false; 90 | mMicEncoder.stopRecording(); 91 | mCamEncoder.stopRecording(); 92 | } 93 | 94 | /** 95 | * Prepare for a subsequent recording. Must be called after {@link #stopRecording()} 96 | * and before {@link #release()} 97 | * 98 | * @param config 99 | */ 100 | public void reset(SessionConfig config) throws IOException { 101 | mCamEncoder.reset(config); 102 | mMicEncoder.reset(config); 103 | mConfig = config; 104 | mIsRecording = false; 105 | } 106 | 107 | /** 108 | * Release resources. Must be called after {@link #stopRecording()} After this call 109 | * this instance may no longer be used. 110 | */ 111 | public void release() { 112 | mCamEncoder.release(); 113 | // MicrophoneEncoder releases all it's resources when stopRecording is called 114 | // because it doesn't have any meaningful state 115 | // between recordings. It might someday if we decide to present 116 | // persistent audio volume meters etc. 117 | // Until then, we don't need to write MicrophoneEncoder.release() 118 | } 119 | 120 | public void onHostActivityPaused() { 121 | mCamEncoder.onHostActivityPaused(); 122 | } 123 | 124 | public void onHostActivityResumed() { 125 | mCamEncoder.onHostActivityResumed(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/AndroidMuxer.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaFormat; 5 | import android.media.MediaMuxer; 6 | import android.util.Log; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | 11 | /** 12 | * @hide 13 | */ 14 | public class AndroidMuxer extends Muxer { 15 | private static final String TAG = "AndroidMuxer"; 16 | private static final boolean VERBOSE = false; 17 | 18 | private MediaMuxer mMuxer; 19 | private boolean mStarted; 20 | 21 | private AndroidMuxer(String outputFile, FORMAT format) { 22 | super(outputFile, format); 23 | try { 24 | switch (format) { 25 | case MPEG4: 26 | mMuxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); 27 | break; 28 | default: 29 | throw new IllegalArgumentException("Unrecognized format!"); 30 | } 31 | } catch (IOException e) { 32 | throw new RuntimeException("MediaMuxer creation failed", e); 33 | } 34 | mStarted = false; 35 | } 36 | 37 | public static AndroidMuxer create(String outputFile, FORMAT format) { 38 | return new AndroidMuxer(outputFile, format); 39 | } 40 | 41 | @Override 42 | public int addTrack(MediaFormat trackFormat) { 43 | super.addTrack(trackFormat); 44 | if (mStarted) 45 | throw new RuntimeException("format changed twice"); 46 | int track = mMuxer.addTrack(trackFormat); 47 | 48 | if (allTracksAdded()) { 49 | start(); 50 | } 51 | return track; 52 | } 53 | 54 | @Override 55 | public void release() { 56 | super.release(); 57 | mMuxer.release(); 58 | } 59 | 60 | @Override 61 | public boolean isStarted() { 62 | return mStarted; 63 | } 64 | 65 | @Override 66 | public void writeSampleData(MediaCodec encoder, int trackIndex, int bufferIndex, ByteBuffer encodedData, MediaCodec.BufferInfo bufferInfo) { 67 | super.writeSampleData(encoder, trackIndex, bufferIndex, encodedData, bufferInfo); 68 | if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { 69 | // MediaMuxer gets the codec config info via the addTrack command 70 | if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG"); 71 | encoder.releaseOutputBuffer(bufferIndex, false); 72 | return; 73 | } 74 | 75 | if (bufferInfo.size == 0) { 76 | if (VERBOSE) Log.d(TAG, "ignoring zero size buffer"); 77 | encoder.releaseOutputBuffer(bufferIndex, false); 78 | return; 79 | } 80 | 81 | if (!mStarted) { 82 | Log.e(TAG, "writeSampleData called before muxer started. Ignoring packet. Track index: " + trackIndex + " tracks added: " + mNumTracks); 83 | encoder.releaseOutputBuffer(bufferIndex, false); 84 | return; 85 | } 86 | 87 | bufferInfo.presentationTimeUs = getNextRelativePts(bufferInfo.presentationTimeUs, trackIndex); 88 | 89 | mMuxer.writeSampleData(trackIndex, encodedData, bufferInfo); 90 | 91 | encoder.releaseOutputBuffer(bufferIndex, false); 92 | 93 | if (allTracksFinished()) { 94 | stop(); 95 | } 96 | } 97 | 98 | @Override 99 | public void forceStop() { 100 | stop(); 101 | } 102 | 103 | protected void start() { 104 | mMuxer.start(); 105 | mStarted = true; 106 | } 107 | 108 | protected void stop() { 109 | mMuxer.stop(); 110 | mStarted = false; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/AudioEncoderConfig.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | /** 4 | * @hide 5 | */ 6 | public class AudioEncoderConfig { 7 | protected final int mNumChannels; 8 | protected final int mSampleRate; 9 | protected final int mBitrate; 10 | 11 | public AudioEncoderConfig(int channels, int sampleRate, int bitRate) { 12 | mNumChannels = channels; 13 | mBitrate = bitRate; 14 | mSampleRate = sampleRate; 15 | } 16 | 17 | public int getNumChannels() { 18 | return mNumChannels; 19 | } 20 | 21 | public int getSampleRate() { 22 | return mSampleRate; 23 | } 24 | 25 | public int getBitrate() { 26 | return mBitrate; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "AudioEncoderConfig: " + mNumChannels + " channels totaling " + mBitrate + " bps @" + mSampleRate + " Hz"; 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/AudioEncoderCore.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.MediaCodec; 5 | import android.media.MediaCodecInfo; 6 | import android.media.MediaFormat; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * @hide 12 | */ 13 | public class AudioEncoderCore extends AndroidEncoder { 14 | 15 | protected static final String MIME_TYPE = "audio/mp4a-latm"; // AAC Low Overhead Audio Transport Multiplex 16 | private static final String TAG = "AudioEncoderCore"; 17 | private static final boolean VERBOSE = false; 18 | // Configurable options 19 | protected int mChannelConfig; 20 | protected int mSampleRate; 21 | 22 | /** 23 | * Configures encoder and muxer state, and prepares the input Surface. 24 | */ 25 | public AudioEncoderCore(int numChannels, int bitRate, int sampleRate, Muxer muxer) throws IOException { 26 | switch (numChannels) { 27 | case 1: 28 | mChannelConfig = AudioFormat.CHANNEL_IN_MONO; 29 | break; 30 | case 2: 31 | mChannelConfig = AudioFormat.CHANNEL_IN_STEREO; 32 | break; 33 | default: 34 | throw new IllegalArgumentException("Invalid channel count. Must be 1 or 2"); 35 | } 36 | mSampleRate = sampleRate; 37 | mMuxer = muxer; 38 | mBufferInfo = new MediaCodec.BufferInfo(); 39 | 40 | MediaFormat format = MediaFormat.createAudioFormat(MIME_TYPE, mSampleRate, mChannelConfig); 41 | 42 | // Set some properties. Failing to specify some of these can cause the MediaCodec 43 | // configure() call to throw an unhelpful exception. 44 | format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); 45 | format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRate); 46 | format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, numChannels); 47 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); 48 | format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384); 49 | 50 | // Create a MediaCodec encoder, and configure it with our format. Get a Surface 51 | // we can use for input and wrap it with a class that handles the EGL work. 52 | mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); 53 | mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 54 | mEncoder.start(); 55 | 56 | mTrackIndex = -1; 57 | } 58 | 59 | /** 60 | * Depending on this method ties AudioEncoderCore 61 | * to a MediaCodec-based implementation. 62 | *

63 | * However, when reading AudioRecord samples directly 64 | * to MediaCode's input ByteBuffer we can avoid a memory copy 65 | * TODO: Measure performance gain and remove if negligible 66 | * 67 | * @return 68 | */ 69 | public MediaCodec getMediaCodec() { 70 | return mEncoder; 71 | } 72 | 73 | @Override 74 | protected boolean isSurfaceInputEncoder() { 75 | return false; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/CameraSurfaceRenderer.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | import android.opengl.GLSurfaceView; 4 | import android.util.Log; 5 | import android.view.MotionEvent; 6 | 7 | import javax.microedition.khronos.egl.EGLConfig; 8 | import javax.microedition.khronos.opengles.GL10; 9 | 10 | /** 11 | * @hide 12 | */ 13 | class CameraSurfaceRenderer implements GLSurfaceView.Renderer { 14 | private static final String TAG = "CameraSurfaceRenderer"; 15 | private static final boolean VERBOSE = false; 16 | private final float[] mSTMatrix = new float[16]; 17 | boolean showBox = false; 18 | private CameraEncoder mCameraEncoder; 19 | private FullFrameRect mFullScreenCamera; 20 | private FullFrameRect mFullScreenOverlay; // For texture overlay 21 | private int mOverlayTextureId; 22 | private int mCameraTextureId; 23 | private boolean mRecordingEnabled; 24 | private int mFrameCount; 25 | // Keep track of selected filters + relevant state 26 | private boolean mIncomingSizeUpdated; 27 | private int mIncomingWidth; 28 | private int mIncomingHeight; 29 | private int mCurrentFilter; 30 | private int mNewFilter; 31 | 32 | 33 | /** 34 | * Constructs CameraSurfaceRenderer. 35 | *

36 | * 37 | * @param recorder video encoder object 38 | */ 39 | public CameraSurfaceRenderer(CameraEncoder recorder) { 40 | mCameraEncoder = recorder; 41 | 42 | mCameraTextureId = -1; 43 | mFrameCount = -1; 44 | 45 | SessionConfig config = recorder.getConfig(); 46 | mIncomingWidth = config.getVideoWidth(); 47 | mIncomingHeight = config.getVideoHeight(); 48 | mIncomingSizeUpdated = true; // Force texture size update on next onDrawFrame 49 | 50 | mCurrentFilter = -1; 51 | mNewFilter = Filters.FILTER_NONE; 52 | 53 | mRecordingEnabled = false; 54 | } 55 | 56 | 57 | /** 58 | * Notifies the renderer that we want to stop or start recording. 59 | */ 60 | public void changeRecordingState(boolean isRecording) { 61 | Log.d(TAG, "changeRecordingState: was " + mRecordingEnabled + " now " + isRecording); 62 | mRecordingEnabled = isRecording; 63 | } 64 | 65 | @Override 66 | public void onSurfaceCreated(GL10 unused, EGLConfig config) { 67 | Log.d(TAG, "onSurfaceCreated"); 68 | // Set up the texture blitter that will be used for on-screen display. This 69 | // is *not* applied to the recording, because that uses a separate shader. 70 | mFullScreenCamera = new FullFrameRect( 71 | new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); 72 | // For texture overlay: 73 | //GLES20.glEnable(GLES20.GL_BLEND); 74 | //GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); 75 | mFullScreenOverlay = new FullFrameRect( 76 | new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_2D)); 77 | //mOverlayTextureId = GlUtil.createTextureWithTextContent("hello!"); 78 | //mOverlayTextureId = GlUtil.createTextureFromImage(mCameraView.getContext(), R.drawable.red_dot); 79 | mCameraTextureId = mFullScreenCamera.createTextureObject(); 80 | 81 | mCameraEncoder.onSurfaceCreated(mCameraTextureId); 82 | mFrameCount = 0; 83 | } 84 | 85 | @Override 86 | public void onSurfaceChanged(GL10 unused, int width, int height) { 87 | Log.d(TAG, "onSurfaceChanged " + width + "x" + height); 88 | } 89 | 90 | @Override 91 | public void onDrawFrame(GL10 unused) { 92 | if (VERBOSE) { 93 | if (mFrameCount % 30 == 0) { 94 | Log.d(TAG, "onDrawFrame tex=" + mCameraTextureId); 95 | mCameraEncoder.logSavedEglState(); 96 | } 97 | } 98 | 99 | if (mCurrentFilter != mNewFilter) { 100 | Filters.updateFilter(mFullScreenCamera, mNewFilter); 101 | mCurrentFilter = mNewFilter; 102 | mIncomingSizeUpdated = true; 103 | } 104 | 105 | if (mIncomingSizeUpdated) { 106 | mFullScreenCamera.getProgram().setTexSize(mIncomingWidth, mIncomingHeight); 107 | mFullScreenOverlay.getProgram().setTexSize(mIncomingWidth, mIncomingHeight); 108 | mIncomingSizeUpdated = false; 109 | Log.i(TAG, "setTexSize on display Texture"); 110 | } 111 | 112 | // Draw the video frame. 113 | if (mCameraEncoder.isSurfaceTextureReadyForDisplay()) { 114 | mCameraEncoder.getSurfaceTextureForDisplay().updateTexImage(); 115 | mCameraEncoder.getSurfaceTextureForDisplay().getTransformMatrix(mSTMatrix); 116 | //Drawing texture overlay: 117 | mFullScreenOverlay.drawFrame(mOverlayTextureId, mSTMatrix); 118 | mFullScreenCamera.drawFrame(mCameraTextureId, mSTMatrix); 119 | } 120 | mFrameCount++; 121 | } 122 | 123 | public void signalVertialVideo(FullFrameRect.SCREEN_ROTATION isVertical) { 124 | if (mFullScreenCamera != null) mFullScreenCamera.adjustForVerticalVideo(isVertical, false); 125 | } 126 | 127 | /** 128 | * Changes the filter that we're applying to the camera preview. 129 | */ 130 | public void changeFilterMode(int filter) { 131 | mNewFilter = filter; 132 | } 133 | 134 | public void handleTouchEvent(MotionEvent ev) { 135 | mFullScreenCamera.handleTouchEvent(ev); 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/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.mcxiaoke.media.av; 18 | 19 | import java.nio.FloatBuffer; 20 | 21 | /** 22 | * Base class for stuff we like to draw. 23 | */ 24 | public class Drawable2d { 25 | /** 26 | * Simple triangle (roughly equilateral, 1.0 per side). 27 | */ 28 | private static final float TRIANGLE_COORDS[] = { 29 | 0.0f, 0.622008459f, // top 30 | -0.5f, -0.311004243f, // bottom left 31 | 0.5f, -0.311004243f // bottom right 32 | }; 33 | private static final FloatBuffer TRIANGLE_BUF = GlUtil.createFloatBuffer(TRIANGLE_COORDS); 34 | 35 | /** 36 | * Simple square, specified as a triangle strip. The square is centered on (0,0) and has 37 | * a size of 1x1. 38 | *

39 | * Triangles are 0-1-2 and 2-1-3 (counter-clockwise winding). 40 | */ 41 | private static final float RECTANGLE_COORDS[] = { 42 | -0.5f, -0.5f, // 0 bottom left 43 | 0.5f, -0.5f, // 1 bottom right 44 | -0.5f, 0.5f, // 2 top left 45 | 0.5f, 0.5f, // 3 top right 46 | }; 47 | private static final FloatBuffer RECTANGLE_BUF = GlUtil.createFloatBuffer(RECTANGLE_COORDS); 48 | 49 | /** 50 | * A "full" square, extending from -1 to +1 in both dimensions. When the model/view/projection 51 | * matrix is identity, this will exactly cover the viewport. 52 | *

53 | * This has texture coordinates as well. 54 | */ 55 | private static final float FULL_RECTANGLE_COORDS[] = { 56 | -1.0f, -1.0f, // 0 bottom left 57 | 1.0f, -1.0f, // 1 bottom right 58 | -1.0f, 1.0f, // 2 top left 59 | 1.0f, 1.0f, // 3 top right 60 | }; 61 | private static final FloatBuffer FULL_RECTANGLE_BUF = 62 | GlUtil.createFloatBuffer(FULL_RECTANGLE_COORDS); 63 | 64 | private static final int SIZEOF_FLOAT = 4; 65 | 66 | private FloatBuffer mVertexArray; 67 | private int mVertexCount; 68 | private int mCoordsPerVertex; 69 | private int mVertexStride; 70 | private Prefab mPrefab; 71 | 72 | /** 73 | * Prepares a drawable from a "pre-fabricated" shape definition. 74 | *

75 | * Does no EGL/GL operations, so this can be done at any time. 76 | */ 77 | public Drawable2d(Prefab shape) { 78 | switch (shape) { 79 | case TRIANGLE: 80 | mVertexArray = TRIANGLE_BUF; 81 | mCoordsPerVertex = 2; 82 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 83 | mVertexCount = TRIANGLE_COORDS.length / mCoordsPerVertex; 84 | break; 85 | case RECTANGLE: 86 | mVertexArray = RECTANGLE_BUF; 87 | mCoordsPerVertex = 2; 88 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 89 | mVertexCount = RECTANGLE_COORDS.length / mCoordsPerVertex; 90 | break; 91 | case FULL_RECTANGLE: 92 | mVertexArray = FULL_RECTANGLE_BUF; 93 | mCoordsPerVertex = 2; 94 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 95 | mVertexCount = FULL_RECTANGLE_COORDS.length / mCoordsPerVertex; 96 | break; 97 | default: 98 | throw new RuntimeException("Unknown shape " + shape); 99 | } 100 | mPrefab = shape; 101 | } 102 | 103 | /** 104 | * Returns the array of vertices. 105 | *

106 | * To avoid allocations, this returns internal state. The caller must not modify it. 107 | */ 108 | public FloatBuffer getVertexArray() { 109 | return mVertexArray; 110 | } 111 | 112 | /** 113 | * Returns the number of vertices stored in the vertex array. 114 | */ 115 | public int getVertexCount() { 116 | return mVertexCount; 117 | } 118 | 119 | /** 120 | * Returns the width, in bytes, of the data for each vertex. 121 | */ 122 | public int getVertexStride() { 123 | return mVertexStride; 124 | } 125 | 126 | /** 127 | * Returns the number of position coordinates per vertex. This will be 2 or 3. 128 | */ 129 | public int getCoordsPerVertex() { 130 | return mCoordsPerVertex; 131 | } 132 | 133 | @Override 134 | public String toString() { 135 | if (mPrefab != null) { 136 | return "[Drawable2d: " + mPrefab + "]"; 137 | } else { 138 | return "[Drawable2d: ...]"; 139 | } 140 | } 141 | 142 | /** 143 | * Enum values for constructor. 144 | */ 145 | public enum Prefab { 146 | TRIANGLE, RECTANGLE, FULL_RECTANGLE 147 | } 148 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/EglStateSaver.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | import android.opengl.EGL14; 4 | import android.opengl.EGLContext; 5 | import android.opengl.EGLDisplay; 6 | import android.opengl.EGLSurface; 7 | import android.util.Log; 8 | 9 | 10 | /** 11 | * @hide 12 | */ 13 | public class EglStateSaver { 14 | private static final String TAG = "EglStateSaver"; 15 | private static final boolean DEBUG = true; 16 | 17 | private EGLContext mSavedContext = EGL14.EGL_NO_CONTEXT; 18 | private EGLSurface mSavedReadSurface = EGL14.EGL_NO_SURFACE; 19 | private EGLSurface mSavedDrawSurface = EGL14.EGL_NO_SURFACE; 20 | private EGLDisplay mSavedDisplay = EGL14.EGL_NO_DISPLAY; 21 | 22 | public void saveEGLState() { 23 | mSavedContext = EGL14.eglGetCurrentContext(); 24 | if (DEBUG && mSavedContext.equals(EGL14.EGL_NO_CONTEXT)) Log.e(TAG, "Saved EGL_NO_CONTEXT"); 25 | mSavedReadSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_READ); 26 | if (DEBUG && mSavedReadSurface.equals(EGL14.EGL_NO_CONTEXT)) Log.e(TAG, "Saved EGL_NO_SURFACE"); 27 | mSavedDrawSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); 28 | if (DEBUG && mSavedDrawSurface.equals(EGL14.EGL_NO_CONTEXT)) Log.e(TAG, "Saved EGL_NO_SURFACE"); 29 | mSavedDisplay = EGL14.eglGetCurrentDisplay(); 30 | if (DEBUG && mSavedDisplay.equals(EGL14.EGL_NO_DISPLAY)) Log.e(TAG, "Saved EGL_NO_DISPLAY"); 31 | 32 | } 33 | 34 | public EGLContext getSavedEGLContext() { 35 | return mSavedContext; 36 | } 37 | 38 | public void makeSavedStateCurrent() { 39 | EGL14.eglMakeCurrent(mSavedDisplay, mSavedReadSurface, mSavedDrawSurface, mSavedContext); 40 | } 41 | 42 | public void makeNothingCurrent() { 43 | EGL14.eglMakeCurrent(mSavedDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); 44 | } 45 | 46 | public void logState() { 47 | if (!mSavedContext.equals(EGL14.eglGetCurrentContext())) 48 | Log.i(TAG, "Saved context DOES NOT equal current."); 49 | else 50 | Log.i(TAG, "Saved context DOES equal current."); 51 | 52 | if (!mSavedReadSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_READ))) { 53 | if (mSavedReadSurface.equals(EGL14.EGL_NO_SURFACE)) 54 | Log.i(TAG, "Saved read surface is EGL_NO_SURFACE"); 55 | else 56 | Log.i(TAG, "Saved read surface DOES NOT equal current."); 57 | } else 58 | Log.i(TAG, "Saved read surface DOES equal current."); 59 | 60 | if (!mSavedDrawSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW))) { 61 | if (mSavedDrawSurface.equals(EGL14.EGL_NO_SURFACE)) 62 | Log.i(TAG, "Saved draw surface is EGL_NO_SURFACE"); 63 | else 64 | Log.i(TAG, "Saved draw surface DOES NOT equal current."); 65 | } else 66 | Log.i(TAG, "Saved draw surface DOES equal current."); 67 | 68 | if (!mSavedDisplay.equals(EGL14.eglGetCurrentDisplay())) 69 | Log.i(TAG, "Saved display DOES NOT equal current."); 70 | else 71 | Log.i(TAG, "Saved display DOES equal current."); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/Filters.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | import android.util.Log; 4 | import com.mcxiaoke.media.ocodec.CodecUtils; 5 | 6 | /** 7 | * This class matches descriptive final int 8 | * variables to Texture2dProgram.ProgramType 9 | * 10 | * @hide 11 | */ 12 | public class Filters { 13 | // Camera filters; must match up with camera_filter_names in strings.xml 14 | static final int FILTER_NONE = 0; 15 | static final int FILTER_BLACK_WHITE = 1; 16 | static final int FILTER_NIGHT = 2; 17 | static final int FILTER_CHROMA_KEY = 3; 18 | static final int FILTER_BLUR = 4; 19 | static final int FILTER_SHARPEN = 5; 20 | static final int FILTER_EDGE_DETECT = 6; 21 | static final int FILTER_EMBOSS = 7; 22 | static final int FILTER_SQUEEZE = 8; 23 | static final int FILTER_TWIRL = 9; 24 | static final int FILTER_TUNNEL = 10; 25 | static final int FILTER_BULGE = 11; 26 | static final int FILTER_DENT = 12; 27 | static final int FILTER_FISHEYE = 13; 28 | static final int FILTER_STRETCH = 14; 29 | static final int FILTER_MIRROR = 15; 30 | private static final String TAG = "Filters"; 31 | private static final boolean VERBOSE = false; 32 | 33 | /** 34 | * Ensure a filter int code is valid. Update this function as 35 | * more filters are defined 36 | * 37 | * @param filter 38 | */ 39 | public static void checkFilterArgument(int filter) { 40 | CodecUtils.checkArgument(filter >= 0 && filter <= 15); 41 | } 42 | 43 | /** 44 | * Updates the filter on the provided FullFrameRect 45 | * 46 | * @return the int code of the new filter 47 | */ 48 | public static void updateFilter(FullFrameRect rect, int newFilter) { 49 | Texture2dProgram.ProgramType programType; 50 | float[] kernel = null; 51 | float colorAdj = 0.0f; 52 | 53 | if (VERBOSE) Log.d(TAG, "Updating filter to " + newFilter); 54 | switch (newFilter) { 55 | case FILTER_NONE: 56 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT; 57 | break; 58 | case FILTER_BLACK_WHITE: 59 | // (In a previous version the TEXTURE_EXT_BW variant was enabled by a flag called 60 | // ROSE_COLORED_GLASSES, because the shader set the red channel to the B&W color 61 | // and green/blue to zero.) 62 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BW; 63 | break; 64 | case FILTER_NIGHT: 65 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_NIGHT; 66 | break; 67 | case FILTER_CHROMA_KEY: 68 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_CHROMA_KEY; 69 | break; 70 | case FILTER_SQUEEZE: 71 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_SQUEEZE; 72 | break; 73 | case FILTER_TWIRL: 74 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_TWIRL; 75 | break; 76 | case FILTER_TUNNEL: 77 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_TUNNEL; 78 | break; 79 | case FILTER_BULGE: 80 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BULGE; 81 | break; 82 | case FILTER_DENT: 83 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_DENT; 84 | break; 85 | case FILTER_FISHEYE: 86 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FISHEYE; 87 | break; 88 | case FILTER_STRETCH: 89 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_STRETCH; 90 | break; 91 | case FILTER_MIRROR: 92 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_MIRROR; 93 | break; 94 | case FILTER_BLUR: 95 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 96 | kernel = new float[]{ 97 | 1f / 16f, 2f / 16f, 1f / 16f, 98 | 2f / 16f, 4f / 16f, 2f / 16f, 99 | 1f / 16f, 2f / 16f, 1f / 16f}; 100 | break; 101 | case FILTER_SHARPEN: 102 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 103 | kernel = new float[]{ 104 | 0f, -1f, 0f, 105 | -1f, 5f, -1f, 106 | 0f, -1f, 0f}; 107 | break; 108 | case FILTER_EDGE_DETECT: 109 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 110 | kernel = new float[]{ 111 | -1f, -1f, -1f, 112 | -1f, 8f, -1f, 113 | -1f, -1f, -1f}; 114 | break; 115 | case FILTER_EMBOSS: 116 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 117 | kernel = new float[]{ 118 | 2f, 0f, 0f, 119 | 0f, -1f, 0f, 120 | 0f, 0f, -1f}; 121 | colorAdj = 0.5f; 122 | break; 123 | default: 124 | throw new RuntimeException("Unknown filter mode " + newFilter); 125 | } 126 | 127 | // Do we need a whole new program? (We want to avoid doing this if we don't have 128 | // too -- compiling a program could be expensive.) 129 | if (programType != rect.getProgram().getProgramType()) { 130 | rect.changeProgram(new Texture2dProgram(programType)); 131 | } 132 | 133 | // Update the filter kernel (if any). 134 | if (kernel != null) { 135 | rect.getProgram().setKernel(kernel, colorAdj); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/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.mcxiaoke.media.av; 18 | 19 | import android.opengl.Matrix; 20 | import android.view.MotionEvent; 21 | 22 | import java.nio.FloatBuffer; 23 | 24 | /** 25 | * This class essentially represents a viewport-sized sprite that will be rendered with 26 | * a texture, usually from an external source like the camera or video decoder. 27 | * 28 | * @hide 29 | */ 30 | public class FullFrameRect { 31 | private static final int SIZEOF_FLOAT = 4; 32 | private static final float TEX_COORDS[] = { 33 | 0.0f, 0.0f, // 0 bottom left 34 | 1.0f, 0.0f, // 1 bottom right 35 | 0.0f, 1.0f, // 2 top left 36 | 1.0f, 1.0f // 3 top right 37 | }; 38 | private static final FloatBuffer TEX_COORDS_BUF = GlUtil.createFloatBuffer(TEX_COORDS); 39 | private static final int TEX_COORDS_STRIDE = 2 * SIZEOF_FLOAT; 40 | private final Drawable2d mRectDrawable = new Drawable2d(Drawable2d.Prefab.FULL_RECTANGLE); 41 | private final Object mDrawLock = new Object(); 42 | private Texture2dProgram mProgram; 43 | private float[] IDENTITY_MATRIX = new float[16]; 44 | private boolean mCorrectVerticalVideo = false; 45 | private boolean mScaleToFit; 46 | private SCREEN_ROTATION requestedOrientation = SCREEN_ROTATION.LANDSCAPE; 47 | /** 48 | * Prepares the object. 49 | * 50 | * @param program The program to use. FullFrameRect takes ownership, and will release 51 | * the program when no longer needed. 52 | */ 53 | public FullFrameRect(Texture2dProgram program) { 54 | mProgram = program; 55 | Matrix.setIdentityM(IDENTITY_MATRIX, 0); 56 | } 57 | 58 | /** 59 | * Adjust the MVP Matrix to rotate and crop the texture 60 | * to make vertical video appear upright 61 | */ 62 | public void adjustForVerticalVideo(SCREEN_ROTATION orientation, boolean scaleToFit) { 63 | synchronized (mDrawLock) { 64 | mCorrectVerticalVideo = true; 65 | mScaleToFit = scaleToFit; 66 | requestedOrientation = orientation; 67 | Matrix.setIdentityM(IDENTITY_MATRIX, 0); 68 | switch (orientation) { 69 | case VERTICAL: 70 | if (scaleToFit) { 71 | Matrix.rotateM(IDENTITY_MATRIX, 0, -90, 0f, 0f, 1f); 72 | Matrix.scaleM(IDENTITY_MATRIX, 0, 3.16f, 1.0f, 1f); 73 | } else { 74 | Matrix.scaleM(IDENTITY_MATRIX, 0, 0.316f, 1f, 1f); 75 | } 76 | break; 77 | case UPSIDEDOWN_LANDSCAPE: 78 | if (scaleToFit) { 79 | Matrix.rotateM(IDENTITY_MATRIX, 0, -180, 0f, 0f, 1f); 80 | } 81 | break; 82 | case UPSIDEDOWN_VERTICAL: 83 | if (scaleToFit) { 84 | Matrix.rotateM(IDENTITY_MATRIX, 0, 90, 0f, 0f, 1f); 85 | Matrix.scaleM(IDENTITY_MATRIX, 0, 3.16f, 1.0f, 1f); 86 | } else { 87 | Matrix.scaleM(IDENTITY_MATRIX, 0, 0.316f, 1f, 1f); 88 | } 89 | break; 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Releases resources. 96 | */ 97 | public void release() { 98 | if (mProgram != null) { 99 | mProgram.release(); 100 | mProgram = null; 101 | } 102 | } 103 | 104 | /** 105 | * Returns the program currently in use. 106 | */ 107 | public Texture2dProgram getProgram() { 108 | return mProgram; 109 | } 110 | 111 | /** 112 | * Changes the program. The previous program will be released. 113 | */ 114 | public void changeProgram(Texture2dProgram program) { 115 | mProgram.release(); 116 | mProgram = program; 117 | } 118 | 119 | /** 120 | * Creates a texture object suitable for use with drawFrame(). 121 | */ 122 | public int createTextureObject() { 123 | return mProgram.createTextureObject(); 124 | } 125 | 126 | /** 127 | * Draws a viewport-filling rect, texturing it with the specified texture object. 128 | */ 129 | public void drawFrame(int textureId, float[] texMatrix) { 130 | // Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport. 131 | synchronized (mDrawLock) { 132 | if (mCorrectVerticalVideo && !mScaleToFit && (requestedOrientation == SCREEN_ROTATION.VERTICAL || requestedOrientation == SCREEN_ROTATION.UPSIDEDOWN_VERTICAL)) { 133 | Matrix.scaleM(texMatrix, 0, 0.316f, 1.0f, 1f); 134 | } 135 | mProgram.draw(IDENTITY_MATRIX, mRectDrawable.getVertexArray(), 0, 136 | mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(), 137 | mRectDrawable.getVertexStride(), 138 | texMatrix, TEX_COORDS_BUF, textureId, TEX_COORDS_STRIDE); 139 | } 140 | } 141 | 142 | /** 143 | * Pass touch event down to the 144 | * texture's shader program 145 | * 146 | * @param ev 147 | */ 148 | public void handleTouchEvent(MotionEvent ev) { 149 | mProgram.handleTouchEvent(ev); 150 | } 151 | 152 | public static enum SCREEN_ROTATION {LANDSCAPE, VERTICAL, UPSIDEDOWN_LANDSCAPE, UPSIDEDOWN_VERTICAL} 153 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/Muxer.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaFormat; 5 | import android.os.Build; 6 | import android.util.Log; 7 | import io.kickflip.sdk.Util; 8 | 9 | import java.nio.ByteBuffer; 10 | 11 | /** 12 | * Base Muxer class for interaction with MediaCodec based 13 | * encoders 14 | * 15 | * @hide 16 | */ 17 | public abstract class Muxer { 18 | private static final String TAG = "Muxer"; 19 | private final int mExpectedNumTracks = 2; // TODO: Make this configurable? 20 | protected FORMAT mFormat; 21 | protected String mOutputPath; 22 | protected int mNumTracks; 23 | protected int mNumTracksFinished; 24 | protected long mFirstPts; 25 | protected long mLastPts[]; 26 | protected Muxer(String outputPath, FORMAT format) { 27 | Log.i(TAG, "Created muxer for output: " + outputPath); 28 | mOutputPath = Util.checkNotNull(outputPath); 29 | mFormat = format; 30 | mNumTracks = 0; 31 | mNumTracksFinished = 0; 32 | mFirstPts = 0; 33 | mLastPts = new long[mExpectedNumTracks]; 34 | for (int i = 0; i < mLastPts.length; i++) { 35 | mLastPts[i] = 0; 36 | } 37 | } 38 | 39 | /** 40 | * Returns the absolute output path. 41 | * 42 | * e.g /sdcard/app/uuid/index.m3u8 43 | * 44 | * @return 45 | */ 46 | public String getOutputPath() { 47 | return mOutputPath; 48 | } 49 | 50 | /** 51 | * Adds the specified track and returns the track index 52 | * 53 | * @param trackFormat MediaFormat of the track to add. Gotten from MediaCodec#dequeueOutputBuffer 54 | * when returned status is INFO_OUTPUT_FORMAT_CHANGED 55 | * @return index of track in output file 56 | */ 57 | public int addTrack(MediaFormat trackFormat) { 58 | mNumTracks++; 59 | return mNumTracks - 1; 60 | } 61 | 62 | /** 63 | * Called by the hosting Encoder 64 | * to notify the Muxer that it should no 65 | * longer assume the Encoder resources are available. 66 | */ 67 | public void onEncoderReleased(int trackIndex) { 68 | } 69 | 70 | public void release() { 71 | // finished event 72 | } 73 | 74 | public boolean isStarted() { 75 | return false; 76 | } 77 | 78 | /** 79 | * Write the MediaCodec output buffer. This method must 80 | * be overridden by subclasses to release encodedData, transferring 81 | * ownership back to encoder, by calling encoder.releaseOutputBuffer(bufferIndex, false); 82 | * 83 | * @param trackIndex 84 | * @param encodedData 85 | * @param bufferInfo 86 | */ 87 | public void writeSampleData(MediaCodec encoder, int trackIndex, int bufferIndex, ByteBuffer encodedData, MediaCodec.BufferInfo bufferInfo) { 88 | if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 89 | signalEndOfTrack(); 90 | } 91 | } 92 | 93 | public abstract void forceStop(); 94 | 95 | protected boolean allTracksFinished() { 96 | return (mNumTracks == mNumTracksFinished); 97 | } 98 | 99 | protected boolean allTracksAdded() { 100 | return (mNumTracks == mExpectedNumTracks); 101 | } 102 | 103 | /** 104 | * Muxer will call this itself if it detects BUFFER_FLAG_END_OF_STREAM 105 | * in writeSampleData. 106 | */ 107 | protected void signalEndOfTrack() { 108 | mNumTracksFinished++; 109 | } 110 | 111 | /** 112 | * Does this Muxer's format require AAC ADTS headers? 113 | * see http://wiki.multimedia.cx/index.php?title=ADTS 114 | * 115 | * @return 116 | */ 117 | protected boolean formatRequiresADTS() { 118 | switch (mFormat) { 119 | case HLS: 120 | return true; 121 | default: 122 | return false; 123 | } 124 | } 125 | 126 | /** 127 | * Does this Muxer's format require 128 | * copying and buffering encoder output buffers. 129 | * Generally speaking, is the output a Socket or File? 130 | * 131 | * @return 132 | */ 133 | protected boolean formatRequiresBuffering() { 134 | if (Build.VERSION.SDK_INT >= 21) return true; 135 | 136 | switch (mFormat) { 137 | case HLS: 138 | return false; 139 | default: 140 | return false; 141 | } 142 | } 143 | 144 | /** 145 | * Return a relative pts given an absolute pts and trackIndex. 146 | * 147 | * This method advances the state of the Muxer, and must only 148 | * be called once per call to {@link #writeSampleData(MediaCodec, int, int, ByteBuffer, MediaCodec.BufferInfo)}. 149 | */ 150 | protected long getNextRelativePts(long absPts, int trackIndex) { 151 | if (mFirstPts == 0) { 152 | mFirstPts = absPts; 153 | return 0; 154 | } 155 | return getSafePts(absPts - mFirstPts, trackIndex); 156 | } 157 | 158 | /** 159 | * Sometimes packets with non-increasing pts are dequeued from the MediaCodec output buffer. 160 | * This method ensures that a crash won't occur due to non monotonically increasing packet timestamp. 161 | */ 162 | private long getSafePts(long pts, int trackIndex) { 163 | if (mLastPts[trackIndex] >= pts) { 164 | // Enforce a non-zero minimum spacing 165 | // between pts 166 | mLastPts[trackIndex] += 9643; 167 | return mLastPts[trackIndex]; 168 | } 169 | mLastPts[trackIndex] = pts; 170 | return pts; 171 | } 172 | 173 | public static enum FORMAT {MPEG4, HLS} 174 | } 175 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/SizeableFrameRect.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.mcxiaoke.media.av; 18 | 19 | import android.opengl.Matrix; 20 | import android.view.MotionEvent; 21 | 22 | import java.nio.FloatBuffer; 23 | 24 | /** 25 | * This class essentially represents a sizezble sprite that will be rendered with 26 | * a texture, usually from an external source like the camera or video decoder. 27 | * 28 | * Placeholder. Not yet implemented. 29 | * 30 | * @hide 31 | */ 32 | public class SizeableFrameRect { 33 | private static final int SIZEOF_FLOAT = 4; 34 | private static final float[] IDENTITY_MATRIX = new float[16]; 35 | private static final int TEX_COORDS_STRIDE = 2 * SIZEOF_FLOAT; 36 | private static float TEX_COORDS[] = { 37 | 0.0f, 0.0f, // 0 bottom left 38 | 1.0f, 0.0f, // 1 bottom right 39 | 0.0f, 1.0f, // 2 top left 40 | 1.0f, 1.0f // 3 top right 41 | }; 42 | private static final FloatBuffer TEX_COORDS_BUF = GlUtil.createFloatBuffer(TEX_COORDS); 43 | private final Drawable2d mRectDrawable = new Drawable2d(Drawable2d.Prefab.RECTANGLE); 44 | private Texture2dProgram mProgram; 45 | 46 | 47 | /** 48 | * Prepares the object. 49 | * 50 | * @param program The program to use. FullFrameRect takes ownership, and will release 51 | * the program when no longer needed. 52 | */ 53 | public SizeableFrameRect(Texture2dProgram program, float[] texCoords) { 54 | mProgram = program; 55 | TEX_COORDS = texCoords; 56 | 57 | Matrix.setIdentityM(IDENTITY_MATRIX, 0); 58 | } 59 | 60 | /** 61 | * Releases resources. 62 | */ 63 | public void release() { 64 | if (mProgram != null) { 65 | mProgram.release(); 66 | mProgram = null; 67 | } 68 | } 69 | 70 | /** 71 | * Returns the program currently in use. 72 | */ 73 | public Texture2dProgram getProgram() { 74 | return mProgram; 75 | } 76 | 77 | /** 78 | * Changes the program. The previous program will be released. 79 | */ 80 | public void changeProgram(Texture2dProgram program) { 81 | mProgram.release(); 82 | mProgram = program; 83 | } 84 | 85 | /** 86 | * Creates a texture object suitable for use with drawFrame(). 87 | */ 88 | public int createTextureObject() { 89 | return mProgram.createTextureObject(); 90 | } 91 | 92 | /** 93 | * Draws a rectangle in an area defined by TEX_COORDS 94 | */ 95 | public void drawFrame(int textureId, float[] texMatrix) { 96 | // Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport. 97 | mProgram.draw(IDENTITY_MATRIX, mRectDrawable.getVertexArray(), 0, 98 | mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(), 99 | mRectDrawable.getVertexStride(), 100 | texMatrix, TEX_COORDS_BUF, textureId, TEX_COORDS_STRIDE); 101 | } 102 | 103 | /** 104 | * Pass touch event down to the 105 | * texture's shader program 106 | * 107 | * @param ev 108 | */ 109 | public void handleTouchEvent(MotionEvent ev) { 110 | mProgram.handleTouchEvent(ev); 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/VideoEncoderConfig.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.av; 2 | 3 | /** 4 | * @hide 5 | */ 6 | public class VideoEncoderConfig { 7 | protected final int mWidth; 8 | protected final int mHeight; 9 | protected final int mBitRate; 10 | 11 | public VideoEncoderConfig(int width, int height, int bitRate) { 12 | mWidth = width; 13 | mHeight = height; 14 | mBitRate = bitRate; 15 | } 16 | 17 | public int getWidth() { 18 | return mWidth; 19 | } 20 | 21 | public int getHeight() { 22 | return mHeight; 23 | } 24 | 25 | public int getBitRate() { 26 | return mBitRate; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "VideoEncoderConfig: " + mWidth + "x" + mHeight + " @" + mBitRate + " bps"; 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/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.mcxiaoke.media.av; 18 | 19 | import android.media.MediaCodec; 20 | import android.media.MediaCodecInfo; 21 | import android.media.MediaFormat; 22 | import android.util.Log; 23 | import android.view.Surface; 24 | 25 | import java.io.IOException; 26 | 27 | /** 28 | * This class wraps up the core components used for surface-input video encoding. 29 | *

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

34 | * This class is not thread-safe, with one exception: it is valid to use the input surface 35 | * on one thread, and drain the output on a different thread. 36 | */ 37 | public class VideoEncoderCore extends AndroidEncoder { 38 | private static final String TAG = "VideoEncoderCore"; 39 | private static final boolean VERBOSE = false; 40 | 41 | // TODO: these ought to be configurable as well 42 | private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding 43 | private static final int FRAME_RATE = 30; // 30fps 44 | private static final int IFRAME_INTERVAL = 3; // 5 seconds between I-frames 45 | 46 | private Surface mInputSurface; 47 | 48 | 49 | /** 50 | * Configures encoder and muxer state, and prepares the input Surface. 51 | */ 52 | public VideoEncoderCore(int width, int height, int bitRate, Muxer muxer) throws IOException { 53 | mMuxer = muxer; 54 | mBufferInfo = new MediaCodec.BufferInfo(); 55 | 56 | MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height); 57 | 58 | // Set some properties. Failing to specify some of these can cause the MediaCodec 59 | // configure() call to throw an unhelpful exception. 60 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 61 | MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 62 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); 63 | format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); 64 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); 65 | if (VERBOSE) Log.d(TAG, "format: " + format); 66 | 67 | // Create a MediaCodec encoder, and configure it with our format. Get a Surface 68 | // we can use for input and wrap it with a class that handles the EGL work. 69 | mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); 70 | mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 71 | mInputSurface = mEncoder.createInputSurface(); 72 | mEncoder.start(); 73 | 74 | mTrackIndex = -1; 75 | } 76 | 77 | /** 78 | * Returns the encoder's input surface. 79 | */ 80 | public Surface getInputSurface() { 81 | return mInputSurface; 82 | } 83 | 84 | @Override 85 | protected boolean isSurfaceInputEncoder() { 86 | return true; 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/av/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.mcxiaoke.media.av; 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 | * This object owns the Surface; releasing this object will release the Surface as well. 27 | * 28 | * @hide 29 | */ 30 | public class WindowSurface extends EglSurfaceBase { 31 | private Surface mSurface; 32 | 33 | /** 34 | * Associates an EGL surface with the native window surface. The Surface will be 35 | * owned by WindowSurface, and released when release() is called. 36 | */ 37 | public WindowSurface(EglCore eglCore, Surface surface) { 38 | super(eglCore); 39 | createWindowSurface(surface); 40 | mSurface = surface; 41 | } 42 | 43 | /** 44 | * Associates an EGL surface with the SurfaceTexture. 45 | */ 46 | public WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) { 47 | super(eglCore); 48 | createWindowSurface(surfaceTexture); 49 | } 50 | 51 | /** 52 | * Releases any resources associated with the Surface and the EGL surface. 53 | */ 54 | public void release() { 55 | releaseEglSurface(); 56 | if (mSurface != null) { 57 | mSurface.release(); 58 | mSurface = null; 59 | } 60 | } 61 | 62 | /** 63 | * Recreate the EGLSurface, using the new EglBase. The caller should have already 64 | * freed the old EGLSurface with releaseEglSurface(). 65 | *

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

71 | * If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a 72 | * context somewhere, the create call will fail with complaints from the Surface 73 | * about already being connected. 74 | */ 75 | public void recreate(EglCore newEglCore) { 76 | if (mSurface == null) { 77 | throw new RuntimeException("not yet implemented for SurfaceTexture"); 78 | } 79 | mEglCore = newEglCore; // switch to new context 80 | createWindowSurface(mSurface); // create new surface 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/lib/CaptureConfig.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.lib; 2 | 3 | import android.media.MediaRecorder.AudioEncoder; 4 | import android.media.MediaRecorder.AudioSource; 5 | import android.media.MediaRecorder.OutputFormat; 6 | import android.media.MediaRecorder.VideoEncoder; 7 | import android.media.MediaRecorder.VideoSource; 8 | 9 | import java.io.File; 10 | 11 | /** 12 | * User: mcxiaoke 13 | * Date: 2017/7/3 14 | * Time: 16:17 15 | */ 16 | 17 | public class CaptureConfig { 18 | public static final int FORMAT_MP4 = OutputFormat.MPEG_4; 19 | public static final int ENC_H264 = VideoEncoder.H264; 20 | public static final int ENC_AAC = AudioEncoder.AAC; 21 | 22 | public static final int VIDEO_IN = VideoSource.CAMERA; 23 | public static final int AUDIO_IN = AudioSource.CAMCORDER; 24 | 25 | public static final int V480P_W = 640; 26 | public static final int V480P_H = 480; 27 | 28 | // 480p 640*480 29 | public int videoWidth = V480P_W; 30 | public int videoHeight = V480P_H; 31 | public int videoFrameRate = 24; 32 | public int videoBitRate = videoWidth * videoHeight * 3; 33 | 34 | public int audioSamplingRate = 44100; 35 | public int audioChannels = 2; 36 | public int audioBitRate = 64 * 1024; 37 | 38 | public File outputFile; 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/lib/CaptureFragment.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.lib; 2 | 3 | /** 4 | * User: mcxiaoke 5 | * Date: 2017/7/3 6 | * Time: 16:17 7 | */ 8 | 9 | public class CaptureFragment { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/lib/CaptureHelper.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.lib; 2 | 3 | /** 4 | * User: mcxiaoke 5 | * Date: 2017/7/3 6 | * Time: 16:17 7 | */ 8 | 9 | public class CaptureHelper { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/ocodec/CodecUtils.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.ocodec; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.hardware.Camera; 7 | import android.hardware.Camera.Size; 8 | import android.util.Log; 9 | import android.view.Surface; 10 | import com.mcxiaoke.media.Const; 11 | 12 | import java.util.Arrays; 13 | 14 | /** 15 | * User: mcxiaoke 16 | * Date: 2017/6/29 17 | * Time: 13:35 18 | */ 19 | 20 | public class CodecUtils { 21 | 22 | public static int getDisplayOrientation(Activity activity, 23 | int cameraId) { 24 | android.hardware.Camera.CameraInfo info = 25 | new android.hardware.Camera.CameraInfo(); 26 | android.hardware.Camera.getCameraInfo(cameraId, info); 27 | int orientation = activity.getResources().getConfiguration().orientation; 28 | int rotation = activity.getWindowManager().getDefaultDisplay() 29 | .getRotation(); 30 | Log.v(Const.TAG, "getDisplayOrientation() orientation=" + orientation); 31 | Log.v(Const.TAG, "getDisplayOrientation() rotation=" + rotation); 32 | int degrees = 0; 33 | switch (rotation) { 34 | case Surface.ROTATION_0: 35 | degrees = 0; 36 | break; 37 | case Surface.ROTATION_90: 38 | degrees = 90; 39 | break; 40 | case Surface.ROTATION_180: 41 | degrees = 180; 42 | break; 43 | case Surface.ROTATION_270: 44 | degrees = 270; 45 | break; 46 | } 47 | 48 | int result; 49 | if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 50 | result = (info.orientation + degrees) % 360; 51 | result = (360 - result) % 360; // compensate the mirror 52 | } else { // back-facing 53 | result = (info.orientation - degrees + 360) % 360; 54 | } 55 | Log.d(Const.TAG, "getDisplayOrientation() = " + result); 56 | return result; 57 | } 58 | 59 | public static int getOrientation(Context context) { 60 | final int orientation = context.getResources().getConfiguration().orientation; 61 | if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 62 | return 180; 63 | } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { 64 | return 90; 65 | } 66 | return 0; 67 | } 68 | 69 | public static void fixOrientation(Context context, Camera camera) { 70 | final int orientation = context.getResources().getConfiguration().orientation; 71 | if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 72 | camera.setDisplayOrientation(180); 73 | } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { 74 | camera.setDisplayOrientation(90); 75 | } 76 | } 77 | 78 | public static void showInfo(Context context, Camera camera) { 79 | int don = context.getResources().getConfiguration().orientation; 80 | Log.v(Const.TAG, "Device Orientation = " + don); 81 | for (int i = 0; i < Camera.getNumberOfCameras(); i++) { 82 | Camera.CameraInfo info = new Camera.CameraInfo(); 83 | Camera.getCameraInfo(i, info); 84 | if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { 85 | Log.v(Const.TAG, i + "Camera Orientation=" + info.orientation); 86 | } 87 | } 88 | Camera.Parameters ps = camera.getParameters(); 89 | for (Size s : ps.getSupportedPictureSizes()) { 90 | // Log.v(Const.TAG, "PictureSize: " + s.width + "x" + s.height); 91 | } 92 | Log.v(Const.TAG, "PictureFormats=" + ps.getSupportedPictureFormats()); 93 | Log.v(Const.TAG, "PreviewFormats=" + ps.getSupportedPreviewFormats()); 94 | for (int[] fs : ps.getSupportedPreviewFpsRange()) { 95 | Log.v(Const.TAG, "PreviewFpsRange: " + Arrays.toString(fs)); 96 | } 97 | for (Size s : ps.getSupportedPreviewSizes()) { 98 | Log.v(Const.TAG, "PreviewSize: " + s.width + "x" + s.height); 99 | } 100 | for (Size s : ps.getSupportedVideoSizes()) { 101 | Log.v(Const.TAG, "VideoSize: " + s.width + "x" + s.height); 102 | } 103 | } 104 | 105 | public static void checkArgument(final boolean b) { 106 | // TODO: 2017/7/3 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/ocodec/Config.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.ocodec; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.MediaRecorder.AudioEncoder; 5 | import android.media.MediaRecorder.AudioSource; 6 | import android.media.MediaRecorder.OutputFormat; 7 | import android.media.MediaRecorder.VideoEncoder; 8 | import android.media.MediaRecorder.VideoSource; 9 | 10 | import java.io.File; 11 | 12 | /** 13 | * User: mcxiaoke 14 | * Date: 2017/7/3 15 | * Time: 16:17 16 | */ 17 | 18 | public class Config { 19 | public static final String CODEC_VIDEO_H264 = "video/avc"; 20 | public static final String CODEC_AUDIO_AAC = "audio/aac"; 21 | public static final int IFRAME_INTERVAL = 5; 22 | public static final String FILE_MP4 = ".mp4"; 23 | public static final int FORMAT_MP4 = OutputFormat.MPEG_4; 24 | public static final int ENC_H264 = VideoEncoder.H264; 25 | public static final int ENC_AAC = AudioEncoder.AAC; 26 | 27 | public static final int VIDEO_IN = VideoSource.CAMERA; 28 | public static final int AUDIO_IN = AudioSource.CAMCORDER; 29 | 30 | public static final int V480P_W = 720; 31 | public static final int V480P_H = 480; 32 | 33 | public int outputFormat = FORMAT_MP4; 34 | public File outputFile; 35 | 36 | public int videoEncoder = ENC_H264; 37 | public int audioEncoder = ENC_AAC; 38 | public int videoSource = VIDEO_IN; 39 | public int audioSource = AUDIO_IN; 40 | public String videoType = CODEC_VIDEO_H264; 41 | public String audioType = CODEC_AUDIO_AAC; 42 | // 480p 480*720 43 | public int videoWidth = V480P_W; 44 | public int videoHeight = V480P_H; 45 | public int videoFrameRate = 25; 46 | public int videoBitRate = videoWidth * videoHeight * 3; 47 | 48 | public int audioSamplingRate = 44100; 49 | public int audioChannels = 2; 50 | public int audioBitRate = 64 * 1024; 51 | public int audioChannelMask = AudioFormat.CHANNEL_IN_STEREO; 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/ocodec/encoder/Recorder.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.ocodec.encoder; 2 | 3 | /** 4 | * User: mcxiaoke 5 | * Date: 2017/7/7 6 | * Time: 16:36 7 | */ 8 | 9 | public class Recorder { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/ocodec/encoder/VideoEncoder.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.ocodec.encoder; 2 | /* 3 | * AudioVideoRecordingSample 4 | * Sample project to cature audio and video from internal mic/camera and save as MPEG4 file. 5 | * 6 | * Copyright (c) 2014-2015 saki t_saki@serenegiant.com 7 | * 8 | * File name: MediaVideoEncoder.java 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. 21 | * 22 | * All files in the folder are under this Apache License, Version 2.0. 23 | */ 24 | 25 | import android.media.MediaCodec; 26 | import android.media.MediaCodecInfo; 27 | import android.media.MediaFormat; 28 | import android.opengl.EGLContext; 29 | import android.util.Log; 30 | import android.view.Surface; 31 | import com.mcxiaoke.media.VRecorder; 32 | import com.mcxiaoke.media.ocodec.Config; 33 | import com.mcxiaoke.media.ocodec.glutils.RenderHandler; 34 | 35 | import java.io.IOException; 36 | 37 | public class VideoEncoder extends Encoder { 38 | private static final String TAG = "MediaVideoEncoder"; 39 | private static final float BPP = 0.15f; 40 | 41 | private final int mWidth; 42 | private final int mHeight; 43 | private RenderHandler mRenderHandler; 44 | private Surface mSurface; 45 | 46 | public VideoEncoder(final Config config, final Muxer muxer, final MediaEncoderListener listener) { 47 | super(config, muxer, listener); 48 | if (DEBUG) Log.i(TAG, "MediaVideoEncoder: "); 49 | mWidth = config.videoWidth; 50 | mHeight = config.videoHeight; 51 | mRenderHandler = RenderHandler.createHandler(TAG); 52 | } 53 | 54 | public boolean frameAvailableSoon(final float[] tex_matrix) { 55 | boolean result; 56 | if (result = super.frameAvailableSoon()) 57 | mRenderHandler.draw(tex_matrix); 58 | return result; 59 | } 60 | 61 | public boolean frameAvailableSoon(final float[] tex_matrix, final float[] mvp_matrix) { 62 | boolean result; 63 | if (result = super.frameAvailableSoon()) 64 | mRenderHandler.draw(tex_matrix, mvp_matrix); 65 | return result; 66 | } 67 | 68 | @Override 69 | public boolean frameAvailableSoon() { 70 | boolean result; 71 | if (result = super.frameAvailableSoon()) 72 | mRenderHandler.draw(null); 73 | return result; 74 | } 75 | 76 | @Override 77 | protected void prepare() throws IOException { 78 | if (DEBUG) Log.i(TAG, "prepare: "); 79 | mTrackIndex = -1; 80 | mMuxerStarted = mIsEOS = false; 81 | 82 | final MediaCodecInfo videoCodecInfo = VRecorder.selectCodec(config.videoType); 83 | if (videoCodecInfo == null) { 84 | Log.e(TAG, "Unable to find an appropriate codec for " + config.videoType); 85 | return; 86 | } 87 | if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName()); 88 | 89 | final MediaFormat format = MediaFormat.createVideoFormat(config.videoType, mWidth, mHeight); 90 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // API >= 18 91 | format.setInteger(MediaFormat.KEY_BIT_RATE, config.videoBitRate); 92 | format.setInteger(MediaFormat.KEY_FRAME_RATE, config.videoFrameRate); 93 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, Config.IFRAME_INTERVAL); 94 | if (DEBUG) Log.i(TAG, "format: " + format); 95 | 96 | mMediaCodec = MediaCodec.createEncoderByType(config.videoType); 97 | mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 98 | // get Surface for encoder input 99 | // this method only can call between #configure and #start 100 | mSurface = mMediaCodec.createInputSurface(); // API >= 18 101 | mMediaCodec.start(); 102 | if (DEBUG) Log.i(TAG, "prepare finishing"); 103 | if (mListener != null) { 104 | try { 105 | mListener.onPrepared(this); 106 | } catch (final Exception e) { 107 | Log.e(TAG, "prepare:", e); 108 | } 109 | } 110 | } 111 | 112 | @Override 113 | protected void release() { 114 | if (DEBUG) Log.i(TAG, "release:"); 115 | if (mSurface != null) { 116 | mSurface.release(); 117 | mSurface = null; 118 | } 119 | if (mRenderHandler != null) { 120 | mRenderHandler.release(); 121 | mRenderHandler = null; 122 | } 123 | super.release(); 124 | } 125 | 126 | @Override 127 | protected void signalEndOfInputStream() { 128 | if (DEBUG) Log.d(TAG, "sending EOS to encoder"); 129 | mMediaCodec.signalEndOfInputStream(); // API >= 18 130 | mIsEOS = true; 131 | } 132 | 133 | public void setEglContext(final EGLContext shared_context, final int tex_id) { 134 | mRenderHandler.setEglContext(shared_context, tex_id, mSurface, true); 135 | } 136 | 137 | private int calcBitRate() { 138 | final int bitrate = (int) (BPP * config.videoFrameRate * mWidth * mHeight); 139 | Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f)); 140 | return bitrate; 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/recoder/CameraProxy.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.recoder; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Point; 5 | import android.hardware.Camera; 6 | import android.hardware.Camera.CameraInfo; 7 | import android.hardware.Camera.Parameters; 8 | import android.util.Log; 9 | import com.mcxiaoke.media.CameraUtils; 10 | import com.mcxiaoke.media.Const; 11 | import com.mcxiaoke.media.ocodec.CodecUtils; 12 | 13 | /** 14 | * User: mcxiaoke 15 | * Date: 2017/7/5 16 | * Time: 16:33 17 | */ 18 | 19 | public class CameraProxy { 20 | public static final String TAG = Const.TAG; 21 | 22 | private Activity activity; 23 | 24 | private int orientation; 25 | private Camera.Size videoSize; 26 | private Camera.Size previewSize; 27 | 28 | public CameraProxy(Activity activity) { 29 | this.activity = activity; 30 | } 31 | 32 | 33 | public Camera openCamera() { 34 | Camera c = null; 35 | try { 36 | c = Camera.open(CameraInfo.CAMERA_FACING_BACK); // attempt to get a Camera instance 37 | // get Camera parameters 38 | Camera.Parameters params = c.getParameters(); 39 | orientation = CodecUtils.getDisplayOrientation(activity, CameraInfo.CAMERA_FACING_BACK); 40 | Point p = new Point(); 41 | activity.getWindowManager().getDefaultDisplay().getSize(p); 42 | Log.d(TAG, "() orientation=" + orientation); 43 | Log.d(TAG, "() screen size=" + p); 44 | videoSize = CameraUtils.getOptimalVideoSize(720, 480, params); 45 | previewSize = CameraUtils.getOptimalPreviewSize(videoSize, params); 46 | Log.d(TAG, "() video size=" + videoSize.width + "x" + videoSize.height); 47 | Log.d(TAG, "() preview size=" + previewSize.width + "x" + previewSize.height); 48 | params.setPreviewSize(previewSize.width, previewSize.height); 49 | params.setRotation(orientation); 50 | params.setRecordingHint(true); 51 | params.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); 52 | params.setFlashMode(Parameters.FLASH_MODE_OFF); 53 | params.setSceneMode(Parameters.SCENE_MODE_AUTO); 54 | c.setParameters(params); 55 | c.setDisplayOrientation(orientation); 56 | 57 | CodecUtils.showInfo(activity, c); 58 | // Log.v(TAG, "Camera Parameters: " + params.flatten()); 59 | } catch (Exception e) { 60 | // Camera is not available (in use or does not exist) 61 | e.printStackTrace(); 62 | } 63 | return c; // returns null if camera is unavailable 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/support/CameraPreview.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.media.support; 2 | 3 | /** 4 | * User: mcxiaoke 5 | * Date: 2017/6/29 6 | * Time: 15:30 7 | */ 8 | 9 | import android.content.Context; 10 | import android.hardware.Camera; 11 | import android.util.Log; 12 | import android.view.SurfaceHolder; 13 | import android.view.SurfaceView; 14 | import com.mcxiaoke.media.Const; 15 | 16 | import java.io.IOException; 17 | 18 | /** 19 | * A basic Camera preview class 20 | */ 21 | public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { 22 | public static final String TAG = Const.TAG; 23 | 24 | private SurfaceHolder mHolder; 25 | private Camera mCamera; 26 | 27 | public CameraPreview(Context context, Camera camera) { 28 | super(context); 29 | mCamera = camera; 30 | 31 | // Install a SurfaceHolder.Callback so we get notified when the 32 | // underlying surface is created and destroyed. 33 | mHolder = getHolder(); 34 | mHolder.addCallback(this); 35 | } 36 | 37 | public void surfaceCreated(SurfaceHolder holder) { 38 | // The Surface has been created, now tell the camera where to draw the preview. 39 | try { 40 | mCamera.setPreviewDisplay(holder); 41 | mCamera.startPreview(); 42 | } catch (IOException e) { 43 | e.printStackTrace(); 44 | Log.d(TAG, "Error setting camera preview: " + e.getMessage()); 45 | } 46 | } 47 | 48 | public void surfaceDestroyed(SurfaceHolder holder) { 49 | // empty. Take care of releasing the Camera preview in your activity. 50 | } 51 | 52 | public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { 53 | // If your preview can change or rotate, take care of those events here. 54 | // Make sure to stop the preview before resizing or reformatting it. 55 | 56 | if (mHolder.getSurface() == null) { 57 | // preview surface does not exist 58 | return; 59 | } 60 | 61 | // stop preview before making changes 62 | try { 63 | mCamera.stopPreview(); 64 | } catch (Exception e) { 65 | e.printStackTrace(); 66 | // ignore: tried to stop a non-existent preview 67 | } 68 | 69 | // set preview size and make any resize, rotate or 70 | // reformatting changes here 71 | 72 | // start preview with new settings 73 | try { 74 | mCamera.setPreviewDisplay(mHolder); 75 | mCamera.startPreview(); 76 | 77 | } catch (Exception e) { 78 | e.printStackTrace(); 79 | Log.d(TAG, "Error starting camera preview: " + e.getMessage()); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/AspectRatio.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.os.Parcel; 20 | import android.os.Parcelable; 21 | import android.support.annotation.NonNull; 22 | import android.support.v4.util.SparseArrayCompat; 23 | 24 | /** 25 | * Immutable class for describing proportional relationship between width and height. 26 | */ 27 | public class AspectRatio implements Comparable, Parcelable { 28 | 29 | private final static SparseArrayCompat> sCache 30 | = new SparseArrayCompat<>(16); 31 | 32 | private final int mX; 33 | private final int mY; 34 | 35 | /** 36 | * Returns an instance of {@link AspectRatio} specified by {@code x} and {@code y} values. 37 | * The values {@code x} and {@code} will be reduced by their greatest common divider. 38 | * 39 | * @param x The width 40 | * @param y The height 41 | * @return An instance of {@link AspectRatio} 42 | */ 43 | public static AspectRatio of(int x, int y) { 44 | int gcd = gcd(x, y); 45 | x /= gcd; 46 | y /= gcd; 47 | SparseArrayCompat arrayX = sCache.get(x); 48 | if (arrayX == null) { 49 | AspectRatio ratio = new AspectRatio(x, y); 50 | arrayX = new SparseArrayCompat<>(); 51 | arrayX.put(y, ratio); 52 | sCache.put(x, arrayX); 53 | return ratio; 54 | } else { 55 | AspectRatio ratio = arrayX.get(y); 56 | if (ratio == null) { 57 | ratio = new AspectRatio(x, y); 58 | arrayX.put(y, ratio); 59 | } 60 | return ratio; 61 | } 62 | } 63 | 64 | /** 65 | * Parse an {@link AspectRatio} from a {@link String} formatted like "4:3". 66 | * 67 | * @param s The string representation of the aspect ratio 68 | * @return The aspect ratio 69 | * @throws IllegalArgumentException when the format is incorrect. 70 | */ 71 | public static AspectRatio parse(String s) { 72 | int position = s.indexOf(':'); 73 | if (position == -1) { 74 | throw new IllegalArgumentException("Malformed aspect ratio: " + s); 75 | } 76 | try { 77 | int x = Integer.parseInt(s.substring(0, position)); 78 | int y = Integer.parseInt(s.substring(position + 1)); 79 | return AspectRatio.of(x, y); 80 | } catch (NumberFormatException e) { 81 | throw new IllegalArgumentException("Malformed aspect ratio: " + s, e); 82 | } 83 | } 84 | 85 | private AspectRatio(int x, int y) { 86 | mX = x; 87 | mY = y; 88 | } 89 | 90 | public int getX() { 91 | return mX; 92 | } 93 | 94 | public int getY() { 95 | return mY; 96 | } 97 | 98 | public boolean matches(Size size) { 99 | int gcd = gcd(size.getWidth(), size.getHeight()); 100 | int x = size.getWidth() / gcd; 101 | int y = size.getHeight() / gcd; 102 | return mX == x && mY == y; 103 | } 104 | 105 | @Override 106 | public boolean equals(Object o) { 107 | if (o == null) { 108 | return false; 109 | } 110 | if (this == o) { 111 | return true; 112 | } 113 | if (o instanceof AspectRatio) { 114 | AspectRatio ratio = (AspectRatio) o; 115 | return mX == ratio.mX && mY == ratio.mY; 116 | } 117 | return false; 118 | } 119 | 120 | @Override 121 | public String toString() { 122 | return mX + ":" + mY; 123 | } 124 | 125 | public float toFloat() { 126 | return (float) mX / mY; 127 | } 128 | 129 | @Override 130 | public int hashCode() { 131 | // assuming most sizes are <2^16, doing a rotate will give us perfect hashing 132 | return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2))); 133 | } 134 | 135 | @Override 136 | public int compareTo(@NonNull AspectRatio another) { 137 | if (equals(another)) { 138 | return 0; 139 | } else if (toFloat() - another.toFloat() > 0) { 140 | return 1; 141 | } 142 | return -1; 143 | } 144 | 145 | /** 146 | * @return The inverse of this {@link AspectRatio}. 147 | */ 148 | public AspectRatio inverse() { 149 | //noinspection SuspiciousNameCombination 150 | return AspectRatio.of(mY, mX); 151 | } 152 | 153 | private static int gcd(int a, int b) { 154 | while (b != 0) { 155 | int c = b; 156 | b = a % b; 157 | a = c; 158 | } 159 | return a; 160 | } 161 | 162 | @Override 163 | public int describeContents() { 164 | return 0; 165 | } 166 | 167 | @Override 168 | public void writeToParcel(Parcel dest, int flags) { 169 | dest.writeInt(mX); 170 | dest.writeInt(mY); 171 | } 172 | 173 | public static final Creator CREATOR 174 | = new Creator() { 175 | 176 | @Override 177 | public AspectRatio createFromParcel(Parcel source) { 178 | int x = source.readInt(); 179 | int y = source.readInt(); 180 | return AspectRatio.of(x, y); 181 | } 182 | 183 | @Override 184 | public AspectRatio[] newArray(int size) { 185 | return new AspectRatio[size]; 186 | } 187 | }; 188 | 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/Camera2Api23.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.Context; 21 | import android.graphics.ImageFormat; 22 | import android.hardware.camera2.params.StreamConfigurationMap; 23 | 24 | 25 | @TargetApi(23) 26 | class Camera2Api23 extends Camera2 { 27 | 28 | Camera2Api23(Callback callback, PreviewImpl preview, Context context) { 29 | super(callback, preview, context); 30 | } 31 | 32 | @Override 33 | protected void collectPictureSizes(SizeMap sizes, StreamConfigurationMap map) { 34 | // Try to get hi-res output sizes 35 | android.util.Size[] outputSizes = map.getHighResolutionOutputSizes(ImageFormat.JPEG); 36 | if (outputSizes != null) { 37 | for (android.util.Size size : map.getHighResolutionOutputSizes(ImageFormat.JPEG)) { 38 | sizes.add(new Size(size.getWidth(), size.getHeight())); 39 | } 40 | } 41 | if (sizes.isEmpty()) { 42 | super.collectPictureSizes(sizes, map); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/CameraViewImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.view.View; 20 | 21 | import java.util.Set; 22 | 23 | abstract class CameraViewImpl { 24 | 25 | protected final Callback mCallback; 26 | 27 | protected final PreviewImpl mPreview; 28 | 29 | CameraViewImpl(Callback callback, PreviewImpl preview) { 30 | mCallback = callback; 31 | mPreview = preview; 32 | } 33 | 34 | View getView() { 35 | return mPreview.getView(); 36 | } 37 | 38 | /** 39 | * @return {@code true} if the implementation was able to start the camera session. 40 | */ 41 | abstract boolean start(); 42 | 43 | abstract void stop(); 44 | 45 | abstract boolean isCameraOpened(); 46 | 47 | abstract void setFacing(int facing); 48 | 49 | abstract int getFacing(); 50 | 51 | abstract Set getSupportedAspectRatios(); 52 | 53 | /** 54 | * @return {@code true} if the aspect ratio was changed. 55 | */ 56 | abstract boolean setAspectRatio(AspectRatio ratio); 57 | 58 | abstract AspectRatio getAspectRatio(); 59 | 60 | abstract void setAutoFocus(boolean autoFocus); 61 | 62 | abstract boolean getAutoFocus(); 63 | 64 | abstract void setFlash(int flash); 65 | 66 | abstract int getFlash(); 67 | 68 | abstract void takePicture(); 69 | 70 | abstract void setDisplayOrientation(int displayOrientation); 71 | 72 | interface Callback { 73 | 74 | void onCameraOpened(); 75 | 76 | void onCameraClosed(); 77 | 78 | void onPictureTaken(byte[] data); 79 | 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | 20 | interface Constants { 21 | 22 | AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(4, 3); 23 | 24 | int FACING_BACK = 0; 25 | int FACING_FRONT = 1; 26 | 27 | int FLASH_OFF = 0; 28 | int FLASH_ON = 1; 29 | int FLASH_TORCH = 2; 30 | int FLASH_AUTO = 3; 31 | int FLASH_RED_EYE = 4; 32 | 33 | int LANDSCAPE_90 = 90; 34 | int LANDSCAPE_270 = 270; 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/DisplayOrientationDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.content.Context; 20 | import android.util.SparseIntArray; 21 | import android.view.Display; 22 | import android.view.OrientationEventListener; 23 | import android.view.Surface; 24 | 25 | 26 | /** 27 | * Monitors the value returned from {@link Display#getRotation()}. 28 | */ 29 | abstract class DisplayOrientationDetector { 30 | 31 | private final OrientationEventListener mOrientationEventListener; 32 | 33 | /** Mapping from Surface.Rotation_n to degrees. */ 34 | static final SparseIntArray DISPLAY_ORIENTATIONS = new SparseIntArray(); 35 | 36 | static { 37 | DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0); 38 | DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90); 39 | DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180); 40 | DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270); 41 | } 42 | 43 | Display mDisplay; 44 | 45 | private int mLastKnownDisplayOrientation = 0; 46 | 47 | public DisplayOrientationDetector(Context context) { 48 | mOrientationEventListener = new OrientationEventListener(context) { 49 | 50 | /** This is either Surface.Rotation_0, _90, _180, _270, or -1 (invalid). */ 51 | private int mLastKnownRotation = -1; 52 | 53 | @Override 54 | public void onOrientationChanged(int orientation) { 55 | if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN || 56 | mDisplay == null) { 57 | return; 58 | } 59 | final int rotation = mDisplay.getRotation(); 60 | if (mLastKnownRotation != rotation) { 61 | mLastKnownRotation = rotation; 62 | dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(rotation)); 63 | } 64 | } 65 | }; 66 | } 67 | 68 | public void enable(Display display) { 69 | mDisplay = display; 70 | mOrientationEventListener.enable(); 71 | // Immediately dispatch the first callback 72 | dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(display.getRotation())); 73 | } 74 | 75 | public void disable() { 76 | mOrientationEventListener.disable(); 77 | mDisplay = null; 78 | } 79 | 80 | public int getLastKnownDisplayOrientation() { 81 | return mLastKnownDisplayOrientation; 82 | } 83 | 84 | void dispatchOnDisplayOrientationChanged(int displayOrientation) { 85 | mLastKnownDisplayOrientation = displayOrientation; 86 | onDisplayOrientationChanged(displayOrientation); 87 | } 88 | 89 | /** 90 | * Called when display orientation is changed. 91 | * 92 | * @param displayOrientation One of 0, 90, 180, and 270. 93 | */ 94 | public abstract void onDisplayOrientationChanged(int displayOrientation); 95 | 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/PreviewImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.view.Surface; 20 | import android.view.SurfaceHolder; 21 | import android.view.View; 22 | 23 | 24 | /** 25 | * Encapsulates all the operations related to camera preview in a backward-compatible manner. 26 | */ 27 | abstract class PreviewImpl { 28 | 29 | interface Callback { 30 | void onSurfaceChanged(); 31 | } 32 | 33 | private Callback mCallback; 34 | 35 | private int mWidth; 36 | 37 | private int mHeight; 38 | 39 | void setCallback(Callback callback) { 40 | mCallback = callback; 41 | } 42 | 43 | abstract Surface getSurface(); 44 | 45 | abstract View getView(); 46 | 47 | abstract Class getOutputClass(); 48 | 49 | abstract void setDisplayOrientation(int displayOrientation); 50 | 51 | abstract boolean isReady(); 52 | 53 | protected void dispatchSurfaceChanged() { 54 | mCallback.onSurfaceChanged(); 55 | } 56 | 57 | SurfaceHolder getSurfaceHolder() { 58 | return null; 59 | } 60 | 61 | Object getSurfaceTexture() { 62 | return null; 63 | } 64 | 65 | void setBufferSize(int width, int height) { 66 | } 67 | 68 | void setSize(int width, int height) { 69 | mWidth = width; 70 | mHeight = height; 71 | } 72 | 73 | int getWidth() { 74 | return mWidth; 75 | } 76 | 77 | int getHeight() { 78 | return mHeight; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/Size.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.support.annotation.NonNull; 20 | 21 | /** 22 | * Immutable class for describing width and height dimensions in pixels. 23 | */ 24 | public class Size implements Comparable { 25 | 26 | private final int mWidth; 27 | private final int mHeight; 28 | 29 | /** 30 | * Create a new immutable Size instance. 31 | * 32 | * @param width The width of the size, in pixels 33 | * @param height The height of the size, in pixels 34 | */ 35 | public Size(int width, int height) { 36 | mWidth = width; 37 | mHeight = height; 38 | } 39 | 40 | public int getWidth() { 41 | return mWidth; 42 | } 43 | 44 | public int getHeight() { 45 | return mHeight; 46 | } 47 | 48 | @Override 49 | public boolean equals(Object o) { 50 | if (o == null) { 51 | return false; 52 | } 53 | if (this == o) { 54 | return true; 55 | } 56 | if (o instanceof Size) { 57 | Size size = (Size) o; 58 | return mWidth == size.mWidth && mHeight == size.mHeight; 59 | } 60 | return false; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return mWidth + "x" + mHeight; 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | // assuming most sizes are <2^16, doing a rotate will give us perfect hashing 71 | return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); 72 | } 73 | 74 | @Override 75 | public int compareTo(@NonNull Size another) { 76 | return mWidth * mHeight - another.mWidth * another.mHeight; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/SizeMap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.support.v4.util.ArrayMap; 20 | 21 | import java.util.Set; 22 | import java.util.SortedSet; 23 | import java.util.TreeSet; 24 | 25 | /** 26 | * A collection class that automatically groups {@link Size}s by their {@link AspectRatio}s. 27 | */ 28 | class SizeMap { 29 | 30 | private final ArrayMap> mRatios = new ArrayMap<>(); 31 | 32 | /** 33 | * Add a new {@link Size} to this collection. 34 | * 35 | * @param size The size to add. 36 | * @return {@code true} if it is added, {@code false} if it already exists and is not added. 37 | */ 38 | public boolean add(Size size) { 39 | for (AspectRatio ratio : mRatios.keySet()) { 40 | if (ratio.matches(size)) { 41 | final SortedSet sizes = mRatios.get(ratio); 42 | if (sizes.contains(size)) { 43 | return false; 44 | } else { 45 | sizes.add(size); 46 | return true; 47 | } 48 | } 49 | } 50 | // None of the existing ratio matches the provided size; add a new key 51 | SortedSet sizes = new TreeSet<>(); 52 | sizes.add(size); 53 | mRatios.put(AspectRatio.of(size.getWidth(), size.getHeight()), sizes); 54 | return true; 55 | } 56 | 57 | /** 58 | * Removes the specified aspect ratio and all sizes associated with it. 59 | * 60 | * @param ratio The aspect ratio to be removed. 61 | */ 62 | public void remove(AspectRatio ratio) { 63 | mRatios.remove(ratio); 64 | } 65 | 66 | Set ratios() { 67 | return mRatios.keySet(); 68 | } 69 | 70 | SortedSet sizes(AspectRatio ratio) { 71 | return mRatios.get(ratio); 72 | } 73 | 74 | void clear() { 75 | mRatios.clear(); 76 | } 77 | 78 | boolean isEmpty() { 79 | return mRatios.isEmpty(); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/SurfaceViewPreview.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.content.Context; 20 | import android.support.v4.view.ViewCompat; 21 | import android.view.Surface; 22 | import android.view.SurfaceHolder; 23 | import android.view.SurfaceView; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import com.mcxiaoke.media.R; 27 | 28 | class SurfaceViewPreview extends PreviewImpl { 29 | 30 | final SurfaceView mSurfaceView; 31 | 32 | SurfaceViewPreview(Context context, ViewGroup parent) { 33 | final View view = View.inflate(context, R.layout.surface_view, parent); 34 | mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view); 35 | final SurfaceHolder holder = mSurfaceView.getHolder(); 36 | //noinspection deprecation 37 | holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 38 | holder.addCallback(new SurfaceHolder.Callback() { 39 | @Override 40 | public void surfaceCreated(SurfaceHolder h) { 41 | } 42 | 43 | @Override 44 | public void surfaceChanged(SurfaceHolder h, int format, int width, int height) { 45 | setSize(width, height); 46 | if (!ViewCompat.isInLayout(mSurfaceView)) { 47 | dispatchSurfaceChanged(); 48 | } 49 | } 50 | 51 | @Override 52 | public void surfaceDestroyed(SurfaceHolder h) { 53 | setSize(0, 0); 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | Surface getSurface() { 60 | return getSurfaceHolder().getSurface(); 61 | } 62 | 63 | @Override 64 | SurfaceHolder getSurfaceHolder() { 65 | return mSurfaceView.getHolder(); 66 | } 67 | 68 | @Override 69 | View getView() { 70 | return mSurfaceView; 71 | } 72 | 73 | @Override 74 | Class getOutputClass() { 75 | return SurfaceHolder.class; 76 | } 77 | 78 | @Override 79 | void setDisplayOrientation(int displayOrientation) { 80 | } 81 | 82 | @Override 83 | boolean isReady() { 84 | return getWidth() != 0 && getHeight() != 0; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcxiaoke/media/widget/TextureViewPreview.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.mcxiaoke.media.widget; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.Context; 21 | import android.graphics.Matrix; 22 | import android.graphics.SurfaceTexture; 23 | import android.view.Surface; 24 | import android.view.TextureView; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | import com.mcxiaoke.media.R; 28 | 29 | @TargetApi(14) 30 | class TextureViewPreview extends PreviewImpl { 31 | 32 | private final TextureView mTextureView; 33 | 34 | private int mDisplayOrientation; 35 | 36 | TextureViewPreview(Context context, ViewGroup parent) { 37 | final View view = View.inflate(context, R.layout.texture_view, parent); 38 | mTextureView = (TextureView) view.findViewById(R.id.texture_view); 39 | mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { 40 | 41 | @Override 42 | public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 43 | setSize(width, height); 44 | configureTransform(); 45 | dispatchSurfaceChanged(); 46 | } 47 | 48 | @Override 49 | public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 50 | setSize(width, height); 51 | configureTransform(); 52 | dispatchSurfaceChanged(); 53 | } 54 | 55 | @Override 56 | public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 57 | setSize(0, 0); 58 | return true; 59 | } 60 | 61 | @Override 62 | public void onSurfaceTextureUpdated(SurfaceTexture surface) { 63 | } 64 | }); 65 | } 66 | 67 | // This method is called only from Camera2. 68 | @TargetApi(15) 69 | @Override 70 | void setBufferSize(int width, int height) { 71 | mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height); 72 | } 73 | 74 | @Override 75 | Surface getSurface() { 76 | return new Surface(mTextureView.getSurfaceTexture()); 77 | } 78 | 79 | @Override 80 | SurfaceTexture getSurfaceTexture() { 81 | return mTextureView.getSurfaceTexture(); 82 | } 83 | 84 | @Override 85 | View getView() { 86 | return mTextureView; 87 | } 88 | 89 | @Override 90 | Class getOutputClass() { 91 | return SurfaceTexture.class; 92 | } 93 | 94 | @Override 95 | void setDisplayOrientation(int displayOrientation) { 96 | mDisplayOrientation = displayOrientation; 97 | configureTransform(); 98 | } 99 | 100 | @Override 101 | boolean isReady() { 102 | return mTextureView.getSurfaceTexture() != null; 103 | } 104 | 105 | /** 106 | * Configures the transform matrix for TextureView based on {@link #mDisplayOrientation} and 107 | * the surface size. 108 | */ 109 | void configureTransform() { 110 | Matrix matrix = new Matrix(); 111 | if (mDisplayOrientation % 180 == 90) { 112 | final int width = getWidth(); 113 | final int height = getHeight(); 114 | // Rotate the camera preview when the screen is landscape. 115 | matrix.setPolyToPoly( 116 | new float[]{ 117 | 0.f, 0.f, // top left 118 | width, 0.f, // top right 119 | 0.f, height, // bottom left 120 | width, height, // bottom right 121 | }, 0, 122 | mDisplayOrientation == 90 ? 123 | // Clockwise 124 | new float[]{ 125 | 0.f, height, // top left 126 | 0.f, 0.f, // top right 127 | width, height, // bottom left 128 | width, 0.f, // bottom right 129 | } : // mDisplayOrientation == 270 130 | // Counter-clockwise 131 | new float[]{ 132 | width, 0.f, // top left 133 | width, height, // top right 134 | 0.f, 0.f, // bottom left 135 | 0.f, height, // bottom right 136 | }, 0, 137 | 4); 138 | } else if (mDisplayOrientation == 180) { 139 | matrix.postRotate(180, getWidth() / 2, getHeight() / 2); 140 | } 141 | mTextureView.setTransform(matrix); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/Share.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | /** 7 | * @hide 8 | */ 9 | public class Share { 10 | 11 | public static Intent createShareChooserIntentWithTitleAndUrl(Context c, String title, String url) { 12 | return Intent.createChooser(createShareIntentWithUrl(c, url), title); 13 | } 14 | 15 | public static Intent createShareIntentWithUrl(Context c, String url) { 16 | Intent shareIntent = new Intent(Intent.ACTION_SEND); 17 | shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 18 | shareIntent.setType("text/plain"); 19 | shareIntent.putExtra(Intent.EXTRA_SUBJECT, ""); 20 | shareIntent.putExtra(Intent.EXTRA_TEXT, url); 21 | return shareIntent; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/Util.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.text.format.DateUtils; 6 | import io.kickflip.sdk.av.SessionConfig; 7 | 8 | import java.io.File; 9 | import java.text.ParseException; 10 | import java.text.SimpleDateFormat; 11 | import java.util.Date; 12 | import java.util.HashMap; 13 | import java.util.Locale; 14 | import java.util.TimeZone; 15 | 16 | /** 17 | * Created by David Brodsky on 3/20/14. 18 | */ 19 | public class Util { 20 | 21 | 22 | //"04/03/2014 23:41:37", 23 | private static SimpleDateFormat mMachineSdf = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss", Locale.US); 24 | private static SimpleDateFormat mHumanSdf = new SimpleDateFormat("MM/dd/yyyy hh:mm a", Locale.US); 25 | ; 26 | 27 | static { 28 | mMachineSdf.setTimeZone(TimeZone.getTimeZone("UTC")); 29 | } 30 | 31 | public static T checkNotNull(T o) { 32 | return o; 33 | } 34 | 35 | public static boolean isKitKat() { 36 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 37 | } 38 | 39 | public static String getHumanDateString() { 40 | return mHumanSdf.format(new Date()); 41 | } 42 | 43 | public static String getHumanRelativeDateStringFromString(String machineDateStr) { 44 | String result = null; 45 | try { 46 | result = DateUtils.getRelativeTimeSpanString(mMachineSdf.parse(machineDateStr).getTime()).toString(); 47 | result = result.replace("in 0 minutes", "just now"); 48 | } catch (ParseException e) { 49 | e.printStackTrace(); 50 | } 51 | return result; 52 | } 53 | 54 | public static SessionConfig create720pHLSSessionConfig(Context context) { 55 | HashMap extraData = new HashMap<>(); 56 | extraData.put("key", "value"); 57 | 58 | String outputPath = new File(context.getFilesDir(), "index.m3u8").getAbsolutePath(); 59 | // Note SessionConfig actually places each recording in a unique folder 60 | // e.g: if you pass /sdcard/test/index.m3u8, your recording will be in 61 | // /sdcard/test//index.m3u8 62 | // You can query the actual output file location with SessionConfig#getOutputPath() or the containing directory 63 | // with SessionConfig#getOutputDirectory() after construction. 64 | SessionConfig config = new SessionConfig.Builder(outputPath) 65 | .withTitle(Util.getHumanDateString()) 66 | .withDescription("A live stream!") 67 | .withAdaptiveStreaming(true) 68 | .withVideoResolution(1280, 720) 69 | .withVideoBitrate(2 * 1000 * 1000) 70 | .withAudioBitrate(192 * 1000) 71 | .withExtraInfo(extraData) 72 | .withPrivateVisibility(false) 73 | .withLocation(true) 74 | .build(); 75 | return config; 76 | } 77 | 78 | /** 79 | * Create a {@link SessionConfig} 80 | * corresponding to a 420p video stream 81 | * 82 | * @param context the host application Context. Used to access Internal Storage 83 | * @return the resulting SessionConfig 84 | */ 85 | public static SessionConfig create420pSessionConfig(Context context, String targer) { 86 | String outputPath = new File(context.getFilesDir(), "index.m3u8").getAbsolutePath(); 87 | SessionConfig config = new SessionConfig.Builder(outputPath) 88 | .withTitle(Util.getHumanDateString()) 89 | .withVideoBitrate(1 * 1000 * 1000) 90 | .withPrivateVisibility(false) 91 | .withLocation(true) 92 | .withVideoResolution(720, 480) 93 | .build(); 94 | return config; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/AVRecorder.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | import io.kickflip.sdk.view.GLCameraView; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * Records an Audio / Video stream to disk. 9 | * 10 | * Example usage: 11 | *

    12 | *
  • AVRecorder recorder = new AVRecorder(mSessionConfig);
  • 13 | *
  • recorder.setPreviewDisplay(mPreviewDisplay);
  • 14 | *
  • recorder.startRecording();
  • 15 | *
  • recorder.stopRecording();
  • 16 | *
  • (Optional) recorder.reset(mNewSessionConfig);
  • 17 | *
  • (Optional) recorder.startRecording();
  • 18 | *
  • (Optional) recorder.stopRecording();
  • 19 | *
  • recorder.release();
  • 20 | *
21 | * 22 | * @hide 23 | */ 24 | public class AVRecorder { 25 | 26 | protected CameraEncoder mCamEncoder; 27 | protected MicrophoneEncoder mMicEncoder; 28 | private SessionConfig mConfig; 29 | private boolean mIsRecording; 30 | 31 | public AVRecorder(SessionConfig config) throws IOException { 32 | init(config); 33 | } 34 | 35 | private void init(SessionConfig config) throws IOException { 36 | mCamEncoder = new CameraEncoder(config); 37 | mMicEncoder = new MicrophoneEncoder(config); 38 | mConfig = config; 39 | mIsRecording = false; 40 | } 41 | 42 | public void setPreviewDisplay(GLCameraView display) { 43 | mCamEncoder.setPreviewDisplay(display); 44 | } 45 | 46 | public void applyFilter(int filter) { 47 | mCamEncoder.applyFilter(filter); 48 | } 49 | 50 | public void requestOtherCamera() { 51 | mCamEncoder.requestOtherCamera(); 52 | } 53 | 54 | public void requestCamera(int camera) { 55 | mCamEncoder.requestCamera(camera); 56 | } 57 | 58 | public void toggleFlash() { 59 | mCamEncoder.toggleFlashMode(); 60 | } 61 | 62 | public void adjustVideoBitrate(int targetBitRate) { 63 | mCamEncoder.adjustBitrate(targetBitRate); 64 | } 65 | 66 | /** 67 | * Signal that the recorder should treat 68 | * incoming video frames as Vertical Video, rotating 69 | * and cropping them for proper display. 70 | * 71 | * This method only has effect if {@link io.kickflip.sdk.av.SessionConfig#setConvertVerticalVideo(boolean)} 72 | * has been set true for the current recording session. 73 | */ 74 | public void signalVerticalVideo(FullFrameRect.SCREEN_ROTATION orientation) { 75 | mCamEncoder.signalVerticalVideo(orientation); 76 | } 77 | 78 | public void startRecording() { 79 | mIsRecording = true; 80 | mMicEncoder.startRecording(); 81 | mCamEncoder.startRecording(); 82 | } 83 | 84 | public boolean isRecording() { 85 | return mIsRecording; 86 | } 87 | 88 | public void stopRecording() { 89 | mIsRecording = false; 90 | mMicEncoder.stopRecording(); 91 | mCamEncoder.stopRecording(); 92 | } 93 | 94 | /** 95 | * Prepare for a subsequent recording. Must be called after {@link #stopRecording()} 96 | * and before {@link #release()} 97 | * 98 | * @param config 99 | */ 100 | public void reset(SessionConfig config) throws IOException { 101 | mCamEncoder.reset(config); 102 | mMicEncoder.reset(config); 103 | mConfig = config; 104 | mIsRecording = false; 105 | } 106 | 107 | /** 108 | * Release resources. Must be called after {@link #stopRecording()} After this call 109 | * this instance may no longer be used. 110 | */ 111 | public void release() { 112 | mCamEncoder.release(); 113 | // MicrophoneEncoder releases all it's resources when stopRecording is called 114 | // because it doesn't have any meaningful state 115 | // between recordings. It might someday if we decide to present 116 | // persistent audio volume meters etc. 117 | // Until then, we don't need to write MicrophoneEncoder.release() 118 | } 119 | 120 | public void onHostActivityPaused() { 121 | mCamEncoder.onHostActivityPaused(); 122 | } 123 | 124 | public void onHostActivityResumed() { 125 | mCamEncoder.onHostActivityResumed(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/AndroidMuxer.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaFormat; 5 | import android.media.MediaMuxer; 6 | import android.util.Log; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | 11 | /** 12 | * @hide 13 | */ 14 | public class AndroidMuxer extends Muxer { 15 | private static final String TAG = "AndroidMuxer"; 16 | private static final boolean VERBOSE = false; 17 | 18 | private MediaMuxer mMuxer; 19 | private boolean mStarted; 20 | 21 | private AndroidMuxer(String outputFile, FORMAT format) { 22 | super(outputFile, format); 23 | try { 24 | switch (format) { 25 | case MPEG4: 26 | mMuxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); 27 | break; 28 | default: 29 | throw new IllegalArgumentException("Unrecognized format!"); 30 | } 31 | } catch (IOException e) { 32 | throw new RuntimeException("MediaMuxer creation failed", e); 33 | } 34 | mStarted = false; 35 | } 36 | 37 | public static AndroidMuxer create(String outputFile, FORMAT format) { 38 | return new AndroidMuxer(outputFile, format); 39 | } 40 | 41 | @Override 42 | public int addTrack(MediaFormat trackFormat) { 43 | super.addTrack(trackFormat); 44 | if (mStarted) 45 | throw new RuntimeException("format changed twice"); 46 | int track = mMuxer.addTrack(trackFormat); 47 | 48 | if (allTracksAdded()) { 49 | start(); 50 | } 51 | return track; 52 | } 53 | 54 | @Override 55 | public void release() { 56 | super.release(); 57 | mMuxer.release(); 58 | } 59 | 60 | @Override 61 | public boolean isStarted() { 62 | return mStarted; 63 | } 64 | 65 | @Override 66 | public void writeSampleData(MediaCodec encoder, int trackIndex, int bufferIndex, ByteBuffer encodedData, MediaCodec.BufferInfo bufferInfo) { 67 | super.writeSampleData(encoder, trackIndex, bufferIndex, encodedData, bufferInfo); 68 | if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { 69 | // MediaMuxer gets the codec config info via the addTrack command 70 | if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG"); 71 | encoder.releaseOutputBuffer(bufferIndex, false); 72 | return; 73 | } 74 | 75 | if (bufferInfo.size == 0) { 76 | if (VERBOSE) Log.d(TAG, "ignoring zero size buffer"); 77 | encoder.releaseOutputBuffer(bufferIndex, false); 78 | return; 79 | } 80 | 81 | if (!mStarted) { 82 | Log.e(TAG, "writeSampleData called before muxer started. Ignoring packet. Track index: " + trackIndex + " tracks added: " + mNumTracks); 83 | encoder.releaseOutputBuffer(bufferIndex, false); 84 | return; 85 | } 86 | 87 | bufferInfo.presentationTimeUs = getNextRelativePts(bufferInfo.presentationTimeUs, trackIndex); 88 | 89 | mMuxer.writeSampleData(trackIndex, encodedData, bufferInfo); 90 | 91 | encoder.releaseOutputBuffer(bufferIndex, false); 92 | 93 | if (allTracksFinished()) { 94 | stop(); 95 | } 96 | } 97 | 98 | @Override 99 | public void forceStop() { 100 | stop(); 101 | } 102 | 103 | protected void start() { 104 | mMuxer.start(); 105 | mStarted = true; 106 | } 107 | 108 | protected void stop() { 109 | mMuxer.stop(); 110 | mStarted = false; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/AudioEncoderConfig.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | /** 4 | * @hide 5 | */ 6 | public class AudioEncoderConfig { 7 | protected final int mNumChannels; 8 | protected final int mSampleRate; 9 | protected final int mBitrate; 10 | 11 | public AudioEncoderConfig(int channels, int sampleRate, int bitRate) { 12 | mNumChannels = channels; 13 | mBitrate = bitRate; 14 | mSampleRate = sampleRate; 15 | } 16 | 17 | public int getNumChannels() { 18 | return mNumChannels; 19 | } 20 | 21 | public int getSampleRate() { 22 | return mSampleRate; 23 | } 24 | 25 | public int getBitrate() { 26 | return mBitrate; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "AudioEncoderConfig: " + mNumChannels + " channels totaling " + mBitrate + " bps @" + mSampleRate + " Hz"; 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/AudioEncoderCore.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.MediaCodec; 5 | import android.media.MediaCodecInfo; 6 | import android.media.MediaFormat; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * @hide 12 | */ 13 | public class AudioEncoderCore extends AndroidEncoder { 14 | 15 | protected static final String MIME_TYPE = "audio/mp4a-latm"; // AAC Low Overhead Audio Transport Multiplex 16 | private static final String TAG = "AudioEncoderCore"; 17 | private static final boolean VERBOSE = false; 18 | // Configurable options 19 | protected int mChannelConfig; 20 | protected int mSampleRate; 21 | 22 | /** 23 | * Configures encoder and muxer state, and prepares the input Surface. 24 | */ 25 | public AudioEncoderCore(int numChannels, int bitRate, int sampleRate, Muxer muxer) throws IOException { 26 | switch (numChannels) { 27 | case 1: 28 | mChannelConfig = AudioFormat.CHANNEL_IN_MONO; 29 | break; 30 | case 2: 31 | mChannelConfig = AudioFormat.CHANNEL_IN_STEREO; 32 | break; 33 | default: 34 | throw new IllegalArgumentException("Invalid channel count. Must be 1 or 2"); 35 | } 36 | mSampleRate = sampleRate; 37 | mMuxer = muxer; 38 | mBufferInfo = new MediaCodec.BufferInfo(); 39 | 40 | MediaFormat format = MediaFormat.createAudioFormat(MIME_TYPE, mSampleRate, mChannelConfig); 41 | 42 | // Set some properties. Failing to specify some of these can cause the MediaCodec 43 | // configure() call to throw an unhelpful exception. 44 | format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); 45 | format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRate); 46 | format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, numChannels); 47 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); 48 | format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384); 49 | 50 | // Create a MediaCodec encoder, and configure it with our format. Get a Surface 51 | // we can use for input and wrap it with a class that handles the EGL work. 52 | mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); 53 | mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 54 | mEncoder.start(); 55 | 56 | mTrackIndex = -1; 57 | } 58 | 59 | /** 60 | * Depending on this method ties AudioEncoderCore 61 | * to a MediaCodec-based implementation. 62 | *

63 | * However, when reading AudioRecord samples directly 64 | * to MediaCode's input ByteBuffer we can avoid a memory copy 65 | * TODO: Measure performance gain and remove if negligible 66 | * 67 | * @return 68 | */ 69 | public MediaCodec getMediaCodec() { 70 | return mEncoder; 71 | } 72 | 73 | @Override 74 | protected boolean isSurfaceInputEncoder() { 75 | return false; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/CameraSurfaceRenderer.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | import android.opengl.GLSurfaceView; 4 | import android.util.Log; 5 | import android.view.MotionEvent; 6 | 7 | import javax.microedition.khronos.egl.EGLConfig; 8 | import javax.microedition.khronos.opengles.GL10; 9 | 10 | /** 11 | * @hide 12 | */ 13 | class CameraSurfaceRenderer implements GLSurfaceView.Renderer { 14 | private static final String TAG = "CameraSurfaceRenderer"; 15 | private static final boolean VERBOSE = false; 16 | private final float[] mSTMatrix = new float[16]; 17 | boolean showBox = false; 18 | private CameraEncoder mCameraEncoder; 19 | private FullFrameRect mFullScreenCamera; 20 | private FullFrameRect mFullScreenOverlay; // For texture overlay 21 | private int mOverlayTextureId; 22 | private int mCameraTextureId; 23 | private boolean mRecordingEnabled; 24 | private int mFrameCount; 25 | // Keep track of selected filters + relevant state 26 | private boolean mIncomingSizeUpdated; 27 | private int mIncomingWidth; 28 | private int mIncomingHeight; 29 | private int mCurrentFilter; 30 | private int mNewFilter; 31 | 32 | 33 | /** 34 | * Constructs CameraSurfaceRenderer. 35 | *

36 | * 37 | * @param recorder video encoder object 38 | */ 39 | public CameraSurfaceRenderer(CameraEncoder recorder) { 40 | mCameraEncoder = recorder; 41 | 42 | mCameraTextureId = -1; 43 | mFrameCount = -1; 44 | 45 | SessionConfig config = recorder.getConfig(); 46 | mIncomingWidth = config.getVideoWidth(); 47 | mIncomingHeight = config.getVideoHeight(); 48 | mIncomingSizeUpdated = true; // Force texture size update on next onDrawFrame 49 | 50 | mCurrentFilter = -1; 51 | mNewFilter = Filters.FILTER_NONE; 52 | 53 | mRecordingEnabled = false; 54 | } 55 | 56 | 57 | /** 58 | * Notifies the renderer that we want to stop or start recording. 59 | */ 60 | public void changeRecordingState(boolean isRecording) { 61 | Log.d(TAG, "changeRecordingState: was " + mRecordingEnabled + " now " + isRecording); 62 | mRecordingEnabled = isRecording; 63 | } 64 | 65 | @Override 66 | public void onSurfaceCreated(GL10 unused, EGLConfig config) { 67 | Log.d(TAG, "onSurfaceCreated"); 68 | // Set up the texture blitter that will be used for on-screen display. This 69 | // is *not* applied to the recording, because that uses a separate shader. 70 | mFullScreenCamera = new FullFrameRect( 71 | new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); 72 | // For texture overlay: 73 | //GLES20.glEnable(GLES20.GL_BLEND); 74 | //GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); 75 | mFullScreenOverlay = new FullFrameRect( 76 | new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_2D)); 77 | //mOverlayTextureId = GlUtil.createTextureWithTextContent("hello!"); 78 | //mOverlayTextureId = GlUtil.createTextureFromImage(mCameraView.getContext(), R.drawable.red_dot); 79 | mCameraTextureId = mFullScreenCamera.createTextureObject(); 80 | 81 | mCameraEncoder.onSurfaceCreated(mCameraTextureId); 82 | mFrameCount = 0; 83 | } 84 | 85 | @Override 86 | public void onSurfaceChanged(GL10 unused, int width, int height) { 87 | Log.d(TAG, "onSurfaceChanged " + width + "x" + height); 88 | } 89 | 90 | @Override 91 | public void onDrawFrame(GL10 unused) { 92 | if (VERBOSE) { 93 | if (mFrameCount % 30 == 0) { 94 | Log.d(TAG, "onDrawFrame tex=" + mCameraTextureId); 95 | mCameraEncoder.logSavedEglState(); 96 | } 97 | } 98 | 99 | if (mCurrentFilter != mNewFilter) { 100 | Filters.updateFilter(mFullScreenCamera, mNewFilter); 101 | mCurrentFilter = mNewFilter; 102 | mIncomingSizeUpdated = true; 103 | } 104 | 105 | if (mIncomingSizeUpdated) { 106 | mFullScreenCamera.getProgram().setTexSize(mIncomingWidth, mIncomingHeight); 107 | mFullScreenOverlay.getProgram().setTexSize(mIncomingWidth, mIncomingHeight); 108 | mIncomingSizeUpdated = false; 109 | Log.i(TAG, "setTexSize on display Texture"); 110 | } 111 | 112 | // Draw the video frame. 113 | if (mCameraEncoder.isSurfaceTextureReadyForDisplay()) { 114 | mCameraEncoder.getSurfaceTextureForDisplay().updateTexImage(); 115 | mCameraEncoder.getSurfaceTextureForDisplay().getTransformMatrix(mSTMatrix); 116 | //Drawing texture overlay: 117 | mFullScreenOverlay.drawFrame(mOverlayTextureId, mSTMatrix); 118 | mFullScreenCamera.drawFrame(mCameraTextureId, mSTMatrix); 119 | } 120 | mFrameCount++; 121 | } 122 | 123 | public void signalVertialVideo(FullFrameRect.SCREEN_ROTATION isVertical) { 124 | if (mFullScreenCamera != null) mFullScreenCamera.adjustForVerticalVideo(isVertical, false); 125 | } 126 | 127 | /** 128 | * Changes the filter that we're applying to the camera preview. 129 | */ 130 | public void changeFilterMode(int filter) { 131 | mNewFilter = filter; 132 | } 133 | 134 | public void handleTouchEvent(MotionEvent ev) { 135 | mFullScreenCamera.handleTouchEvent(ev); 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/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 io.kickflip.sdk.av; 18 | 19 | import java.nio.FloatBuffer; 20 | 21 | /** 22 | * Base class for stuff we like to draw. 23 | */ 24 | public class Drawable2d { 25 | /** 26 | * Simple triangle (roughly equilateral, 1.0 per side). 27 | */ 28 | private static final float TRIANGLE_COORDS[] = { 29 | 0.0f, 0.622008459f, // top 30 | -0.5f, -0.311004243f, // bottom left 31 | 0.5f, -0.311004243f // bottom right 32 | }; 33 | private static final FloatBuffer TRIANGLE_BUF = GlUtil.createFloatBuffer(TRIANGLE_COORDS); 34 | 35 | /** 36 | * Simple square, specified as a triangle strip. The square is centered on (0,0) and has 37 | * a size of 1x1. 38 | *

39 | * Triangles are 0-1-2 and 2-1-3 (counter-clockwise winding). 40 | */ 41 | private static final float RECTANGLE_COORDS[] = { 42 | -0.5f, -0.5f, // 0 bottom left 43 | 0.5f, -0.5f, // 1 bottom right 44 | -0.5f, 0.5f, // 2 top left 45 | 0.5f, 0.5f, // 3 top right 46 | }; 47 | private static final FloatBuffer RECTANGLE_BUF = GlUtil.createFloatBuffer(RECTANGLE_COORDS); 48 | 49 | /** 50 | * A "full" square, extending from -1 to +1 in both dimensions. When the model/view/projection 51 | * matrix is identity, this will exactly cover the viewport. 52 | *

53 | * This has texture coordinates as well. 54 | */ 55 | private static final float FULL_RECTANGLE_COORDS[] = { 56 | -1.0f, -1.0f, // 0 bottom left 57 | 1.0f, -1.0f, // 1 bottom right 58 | -1.0f, 1.0f, // 2 top left 59 | 1.0f, 1.0f, // 3 top right 60 | }; 61 | private static final FloatBuffer FULL_RECTANGLE_BUF = 62 | GlUtil.createFloatBuffer(FULL_RECTANGLE_COORDS); 63 | 64 | private static final int SIZEOF_FLOAT = 4; 65 | 66 | private FloatBuffer mVertexArray; 67 | private int mVertexCount; 68 | private int mCoordsPerVertex; 69 | private int mVertexStride; 70 | private Prefab mPrefab; 71 | 72 | /** 73 | * Prepares a drawable from a "pre-fabricated" shape definition. 74 | *

75 | * Does no EGL/GL operations, so this can be done at any time. 76 | */ 77 | public Drawable2d(Prefab shape) { 78 | switch (shape) { 79 | case TRIANGLE: 80 | mVertexArray = TRIANGLE_BUF; 81 | mCoordsPerVertex = 2; 82 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 83 | mVertexCount = TRIANGLE_COORDS.length / mCoordsPerVertex; 84 | break; 85 | case RECTANGLE: 86 | mVertexArray = RECTANGLE_BUF; 87 | mCoordsPerVertex = 2; 88 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 89 | mVertexCount = RECTANGLE_COORDS.length / mCoordsPerVertex; 90 | break; 91 | case FULL_RECTANGLE: 92 | mVertexArray = FULL_RECTANGLE_BUF; 93 | mCoordsPerVertex = 2; 94 | mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; 95 | mVertexCount = FULL_RECTANGLE_COORDS.length / mCoordsPerVertex; 96 | break; 97 | default: 98 | throw new RuntimeException("Unknown shape " + shape); 99 | } 100 | mPrefab = shape; 101 | } 102 | 103 | /** 104 | * Returns the array of vertices. 105 | *

106 | * To avoid allocations, this returns internal state. The caller must not modify it. 107 | */ 108 | public FloatBuffer getVertexArray() { 109 | return mVertexArray; 110 | } 111 | 112 | /** 113 | * Returns the number of vertices stored in the vertex array. 114 | */ 115 | public int getVertexCount() { 116 | return mVertexCount; 117 | } 118 | 119 | /** 120 | * Returns the width, in bytes, of the data for each vertex. 121 | */ 122 | public int getVertexStride() { 123 | return mVertexStride; 124 | } 125 | 126 | /** 127 | * Returns the number of position coordinates per vertex. This will be 2 or 3. 128 | */ 129 | public int getCoordsPerVertex() { 130 | return mCoordsPerVertex; 131 | } 132 | 133 | @Override 134 | public String toString() { 135 | if (mPrefab != null) { 136 | return "[Drawable2d: " + mPrefab + "]"; 137 | } else { 138 | return "[Drawable2d: ...]"; 139 | } 140 | } 141 | 142 | /** 143 | * Enum values for constructor. 144 | */ 145 | public enum Prefab { 146 | TRIANGLE, RECTANGLE, FULL_RECTANGLE 147 | } 148 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/EglStateSaver.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | import android.opengl.EGL14; 4 | import android.opengl.EGLContext; 5 | import android.opengl.EGLDisplay; 6 | import android.opengl.EGLSurface; 7 | import android.util.Log; 8 | 9 | 10 | /** 11 | * @hide 12 | */ 13 | public class EglStateSaver { 14 | private static final String TAG = "EglStateSaver"; 15 | private static final boolean DEBUG = true; 16 | 17 | private EGLContext mSavedContext = EGL14.EGL_NO_CONTEXT; 18 | private EGLSurface mSavedReadSurface = EGL14.EGL_NO_SURFACE; 19 | private EGLSurface mSavedDrawSurface = EGL14.EGL_NO_SURFACE; 20 | private EGLDisplay mSavedDisplay = EGL14.EGL_NO_DISPLAY; 21 | 22 | public void saveEGLState() { 23 | mSavedContext = EGL14.eglGetCurrentContext(); 24 | if (DEBUG && mSavedContext.equals(EGL14.EGL_NO_CONTEXT)) Log.e(TAG, "Saved EGL_NO_CONTEXT"); 25 | mSavedReadSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_READ); 26 | if (DEBUG && mSavedReadSurface.equals(EGL14.EGL_NO_CONTEXT)) Log.e(TAG, "Saved EGL_NO_SURFACE"); 27 | mSavedDrawSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); 28 | if (DEBUG && mSavedDrawSurface.equals(EGL14.EGL_NO_CONTEXT)) Log.e(TAG, "Saved EGL_NO_SURFACE"); 29 | mSavedDisplay = EGL14.eglGetCurrentDisplay(); 30 | if (DEBUG && mSavedDisplay.equals(EGL14.EGL_NO_DISPLAY)) Log.e(TAG, "Saved EGL_NO_DISPLAY"); 31 | 32 | } 33 | 34 | public EGLContext getSavedEGLContext() { 35 | return mSavedContext; 36 | } 37 | 38 | public void makeSavedStateCurrent() { 39 | EGL14.eglMakeCurrent(mSavedDisplay, mSavedReadSurface, mSavedDrawSurface, mSavedContext); 40 | } 41 | 42 | public void makeNothingCurrent() { 43 | EGL14.eglMakeCurrent(mSavedDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); 44 | } 45 | 46 | public void logState() { 47 | if (!mSavedContext.equals(EGL14.eglGetCurrentContext())) 48 | Log.i(TAG, "Saved context DOES NOT equal current."); 49 | else 50 | Log.i(TAG, "Saved context DOES equal current."); 51 | 52 | if (!mSavedReadSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_READ))) { 53 | if (mSavedReadSurface.equals(EGL14.EGL_NO_SURFACE)) 54 | Log.i(TAG, "Saved read surface is EGL_NO_SURFACE"); 55 | else 56 | Log.i(TAG, "Saved read surface DOES NOT equal current."); 57 | } else 58 | Log.i(TAG, "Saved read surface DOES equal current."); 59 | 60 | if (!mSavedDrawSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW))) { 61 | if (mSavedDrawSurface.equals(EGL14.EGL_NO_SURFACE)) 62 | Log.i(TAG, "Saved draw surface is EGL_NO_SURFACE"); 63 | else 64 | Log.i(TAG, "Saved draw surface DOES NOT equal current."); 65 | } else 66 | Log.i(TAG, "Saved draw surface DOES equal current."); 67 | 68 | if (!mSavedDisplay.equals(EGL14.eglGetCurrentDisplay())) 69 | Log.i(TAG, "Saved display DOES NOT equal current."); 70 | else 71 | Log.i(TAG, "Saved display DOES equal current."); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/EglSurfaceBase.java: -------------------------------------------------------------------------------- 1 | /* * Copyright 2013 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.kickflip.sdk.av; import android.graphics.Bitmap; import android.graphics.Matrix; import android.opengl.EGL14; import android.opengl.EGLSurface; import android.opengl.GLES20; import android.util.Log; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Common base class for EGL surfaces. *

* There can be multiple surfaces associated with a single context. * * @hide */ public class EglSurfaceBase { protected static final String TAG = "EglSurfaceBase"; // EglBase object we're associated with. It may be associated with multiple surfaces. protected EglCore mEglCore; private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE; private int mWidth = -1; private int mHeight = -1; protected EglSurfaceBase(EglCore eglBase) { mEglCore = eglBase; } /** * Creates a window surface. *

* * @param surface May be a Surface or SurfaceTexture. */ public void createWindowSurface(Object surface) { if (mEGLSurface != EGL14.EGL_NO_SURFACE) { throw new IllegalStateException("surface already created"); } mEGLSurface = mEglCore.createWindowSurface(surface); mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); } /** * Creates an off-screen surface. */ public void createOffscreenSurface(int width, int height) { if (mEGLSurface != EGL14.EGL_NO_SURFACE) { throw new IllegalStateException("surface already created"); } mEGLSurface = mEglCore.createOffscreenSurface(width, height); mWidth = width; mHeight = height; } /** * Returns the surface's width, in pixels. */ public int getWidth() { return mWidth; } /** * Returns the surface's height, in pixels. */ public int getHeight() { return mHeight; } /** * Release the EGL surface. */ public void releaseEglSurface() { mEglCore.releaseSurface(mEGLSurface); mEGLSurface = EGL14.EGL_NO_SURFACE; mWidth = mHeight = -1; } /** * Makes our EGL context and surface current. */ public void makeCurrent() { mEglCore.makeCurrent(mEGLSurface); } /** * Makes our EGL context and surface current for drawing, using the supplied surface * for reading. */ public void makeCurrentReadFrom(EglSurfaceBase readSurface) { mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface); } /** * Calls eglSwapBuffers. Use this to "publish" the current frame. * * @return false on failure */ public boolean swapBuffers() { boolean result = mEglCore.swapBuffers(mEGLSurface); if (!result) { Log.d(TAG, "WARNING: swapBuffers() failed"); } return result; } /** * Sends the presentation time stamp to EGL. * * @param nsecs Timestamp, in nanoseconds. */ public void setPresentationTime(long nsecs) { mEglCore.setPresentationTime(mEGLSurface, nsecs); } /** * Saves the EGL surface to a file. *

* Expects that this object's EGL surface is current. */ public void saveFrame(File file, final int scaleFactor) throws IOException { if (!mEglCore.isCurrent(mEGLSurface)) { throw new RuntimeException("Expected EGL context/surface is not current"); } // glReadPixels gives us a ByteBuffer filled with what is essentially big-endian RGBA // data (i.e. a byte of red, followed by a byte of green...). We need an int[] filled // with little-endian ARGB data to feed to Bitmap. // // If we implement this as a series of buf.get() calls, we can spend 2.5 seconds just // copying data around for a 720p frame. It's better to do a bulk get() and then // rearrange the data in memory. (For comparison, the PNG compress takes about 500ms // for a trivial frame.) // // So... we set the ByteBuffer to little-endian, which should turn the bulk IntBuffer // get() into a straight memcpy on most Android devices. Our ints will hold ABGR data. // Swapping B and R gives us ARGB. // // Making this even more interesting is the upside-down nature of GL, which means // our output will look upside-down relative to what appears on screen if the // typical GL conventions are used. final long startTime = System.currentTimeMillis(); final String filename = file.toString(); final ByteBuffer buf = ByteBuffer.allocateDirect(mWidth * mHeight * 4); buf.order(ByteOrder.LITTLE_ENDIAN); GLES20.glReadPixels(0, 0, mWidth, mHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); buf.rewind(); new Thread(new Runnable() { @Override public void run() { BufferedOutputStream bos = null; try { bos = new BufferedOutputStream(new FileOutputStream(filename)); Bitmap fullBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); fullBitmap.copyPixelsFromBuffer(buf); Matrix m = new Matrix(); m.preScale(1, -1); if (scaleFactor != 1) { Bitmap scaledBitmap = Bitmap.createScaledBitmap(fullBitmap, mWidth / scaleFactor, mHeight / scaleFactor, true); Bitmap flippedScaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), m, true); flippedScaledBitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos); scaledBitmap.recycle(); flippedScaledBitmap.recycle(); } else { Bitmap flippedBitmap = Bitmap.createBitmap(fullBitmap, 0, 0, mWidth, mHeight, m, true); flippedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos); } fullBitmap.recycle(); Log.d(TAG, "Saved " + mWidth / scaleFactor + "x" + mHeight / scaleFactor + " frame as '" + filename + "' in " + (System.currentTimeMillis() - startTime) + " ms"); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (bos != null) try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } } }).start(); } } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/Filters.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | import android.util.Log; 4 | import com.mcxiaoke.media.ocodec.CodecUtils; 5 | 6 | /** 7 | * This class matches descriptive final int 8 | * variables to Texture2dProgram.ProgramType 9 | * 10 | * @hide 11 | */ 12 | public class Filters { 13 | // Camera filters; must match up with camera_filter_names in strings.xml 14 | static final int FILTER_NONE = 0; 15 | static final int FILTER_BLACK_WHITE = 1; 16 | static final int FILTER_NIGHT = 2; 17 | static final int FILTER_CHROMA_KEY = 3; 18 | static final int FILTER_BLUR = 4; 19 | static final int FILTER_SHARPEN = 5; 20 | static final int FILTER_EDGE_DETECT = 6; 21 | static final int FILTER_EMBOSS = 7; 22 | static final int FILTER_SQUEEZE = 8; 23 | static final int FILTER_TWIRL = 9; 24 | static final int FILTER_TUNNEL = 10; 25 | static final int FILTER_BULGE = 11; 26 | static final int FILTER_DENT = 12; 27 | static final int FILTER_FISHEYE = 13; 28 | static final int FILTER_STRETCH = 14; 29 | static final int FILTER_MIRROR = 15; 30 | private static final String TAG = "Filters"; 31 | private static final boolean VERBOSE = false; 32 | 33 | /** 34 | * Ensure a filter int code is valid. Update this function as 35 | * more filters are defined 36 | * 37 | * @param filter 38 | */ 39 | public static void checkFilterArgument(int filter) { 40 | CodecUtils.checkArgument(filter >= 0 && filter <= 15); 41 | } 42 | 43 | /** 44 | * Updates the filter on the provided FullFrameRect 45 | * 46 | * @return the int code of the new filter 47 | */ 48 | public static void updateFilter(FullFrameRect rect, int newFilter) { 49 | Texture2dProgram.ProgramType programType; 50 | float[] kernel = null; 51 | float colorAdj = 0.0f; 52 | 53 | if (VERBOSE) Log.d(TAG, "Updating filter to " + newFilter); 54 | switch (newFilter) { 55 | case FILTER_NONE: 56 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT; 57 | break; 58 | case FILTER_BLACK_WHITE: 59 | // (In a previous version the TEXTURE_EXT_BW variant was enabled by a flag called 60 | // ROSE_COLORED_GLASSES, because the shader set the red channel to the B&W color 61 | // and green/blue to zero.) 62 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BW; 63 | break; 64 | case FILTER_NIGHT: 65 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_NIGHT; 66 | break; 67 | case FILTER_CHROMA_KEY: 68 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_CHROMA_KEY; 69 | break; 70 | case FILTER_SQUEEZE: 71 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_SQUEEZE; 72 | break; 73 | case FILTER_TWIRL: 74 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_TWIRL; 75 | break; 76 | case FILTER_TUNNEL: 77 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_TUNNEL; 78 | break; 79 | case FILTER_BULGE: 80 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BULGE; 81 | break; 82 | case FILTER_DENT: 83 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_DENT; 84 | break; 85 | case FILTER_FISHEYE: 86 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FISHEYE; 87 | break; 88 | case FILTER_STRETCH: 89 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_STRETCH; 90 | break; 91 | case FILTER_MIRROR: 92 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_MIRROR; 93 | break; 94 | case FILTER_BLUR: 95 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 96 | kernel = new float[]{ 97 | 1f / 16f, 2f / 16f, 1f / 16f, 98 | 2f / 16f, 4f / 16f, 2f / 16f, 99 | 1f / 16f, 2f / 16f, 1f / 16f}; 100 | break; 101 | case FILTER_SHARPEN: 102 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 103 | kernel = new float[]{ 104 | 0f, -1f, 0f, 105 | -1f, 5f, -1f, 106 | 0f, -1f, 0f}; 107 | break; 108 | case FILTER_EDGE_DETECT: 109 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 110 | kernel = new float[]{ 111 | -1f, -1f, -1f, 112 | -1f, 8f, -1f, 113 | -1f, -1f, -1f}; 114 | break; 115 | case FILTER_EMBOSS: 116 | programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT; 117 | kernel = new float[]{ 118 | 2f, 0f, 0f, 119 | 0f, -1f, 0f, 120 | 0f, 0f, -1f}; 121 | colorAdj = 0.5f; 122 | break; 123 | default: 124 | throw new RuntimeException("Unknown filter mode " + newFilter); 125 | } 126 | 127 | // Do we need a whole new program? (We want to avoid doing this if we don't have 128 | // too -- compiling a program could be expensive.) 129 | if (programType != rect.getProgram().getProgramType()) { 130 | rect.changeProgram(new Texture2dProgram(programType)); 131 | } 132 | 133 | // Update the filter kernel (if any). 134 | if (kernel != null) { 135 | rect.getProgram().setKernel(kernel, colorAdj); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/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 io.kickflip.sdk.av; 18 | 19 | import android.opengl.Matrix; 20 | import android.view.MotionEvent; 21 | 22 | import java.nio.FloatBuffer; 23 | 24 | /** 25 | * This class essentially represents a viewport-sized sprite that will be rendered with 26 | * a texture, usually from an external source like the camera or video decoder. 27 | * 28 | * @hide 29 | */ 30 | public class FullFrameRect { 31 | private static final int SIZEOF_FLOAT = 4; 32 | private static final float TEX_COORDS[] = { 33 | 0.0f, 0.0f, // 0 bottom left 34 | 1.0f, 0.0f, // 1 bottom right 35 | 0.0f, 1.0f, // 2 top left 36 | 1.0f, 1.0f // 3 top right 37 | }; 38 | private static final FloatBuffer TEX_COORDS_BUF = GlUtil.createFloatBuffer(TEX_COORDS); 39 | private static final int TEX_COORDS_STRIDE = 2 * SIZEOF_FLOAT; 40 | private final Drawable2d mRectDrawable = new Drawable2d(Drawable2d.Prefab.FULL_RECTANGLE); 41 | private final Object mDrawLock = new Object(); 42 | private Texture2dProgram mProgram; 43 | private float[] IDENTITY_MATRIX = new float[16]; 44 | private boolean mCorrectVerticalVideo = false; 45 | private boolean mScaleToFit; 46 | private SCREEN_ROTATION requestedOrientation = SCREEN_ROTATION.LANDSCAPE; 47 | /** 48 | * Prepares the object. 49 | * 50 | * @param program The program to use. FullFrameRect takes ownership, and will release 51 | * the program when no longer needed. 52 | */ 53 | public FullFrameRect(Texture2dProgram program) { 54 | mProgram = program; 55 | Matrix.setIdentityM(IDENTITY_MATRIX, 0); 56 | } 57 | 58 | /** 59 | * Adjust the MVP Matrix to rotate and crop the texture 60 | * to make vertical video appear upright 61 | */ 62 | public void adjustForVerticalVideo(SCREEN_ROTATION orientation, boolean scaleToFit) { 63 | synchronized (mDrawLock) { 64 | mCorrectVerticalVideo = true; 65 | mScaleToFit = scaleToFit; 66 | requestedOrientation = orientation; 67 | Matrix.setIdentityM(IDENTITY_MATRIX, 0); 68 | switch (orientation) { 69 | case VERTICAL: 70 | if (scaleToFit) { 71 | Matrix.rotateM(IDENTITY_MATRIX, 0, -90, 0f, 0f, 1f); 72 | Matrix.scaleM(IDENTITY_MATRIX, 0, 3.16f, 1.0f, 1f); 73 | } else { 74 | Matrix.scaleM(IDENTITY_MATRIX, 0, 0.316f, 1f, 1f); 75 | } 76 | break; 77 | case UPSIDEDOWN_LANDSCAPE: 78 | if (scaleToFit) { 79 | Matrix.rotateM(IDENTITY_MATRIX, 0, -180, 0f, 0f, 1f); 80 | } 81 | break; 82 | case UPSIDEDOWN_VERTICAL: 83 | if (scaleToFit) { 84 | Matrix.rotateM(IDENTITY_MATRIX, 0, 90, 0f, 0f, 1f); 85 | Matrix.scaleM(IDENTITY_MATRIX, 0, 3.16f, 1.0f, 1f); 86 | } else { 87 | Matrix.scaleM(IDENTITY_MATRIX, 0, 0.316f, 1f, 1f); 88 | } 89 | break; 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Releases resources. 96 | */ 97 | public void release() { 98 | if (mProgram != null) { 99 | mProgram.release(); 100 | mProgram = null; 101 | } 102 | } 103 | 104 | /** 105 | * Returns the program currently in use. 106 | */ 107 | public Texture2dProgram getProgram() { 108 | return mProgram; 109 | } 110 | 111 | /** 112 | * Changes the program. The previous program will be released. 113 | */ 114 | public void changeProgram(Texture2dProgram program) { 115 | mProgram.release(); 116 | mProgram = program; 117 | } 118 | 119 | /** 120 | * Creates a texture object suitable for use with drawFrame(). 121 | */ 122 | public int createTextureObject() { 123 | return mProgram.createTextureObject(); 124 | } 125 | 126 | /** 127 | * Draws a viewport-filling rect, texturing it with the specified texture object. 128 | */ 129 | public void drawFrame(int textureId, float[] texMatrix) { 130 | // Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport. 131 | synchronized (mDrawLock) { 132 | if (mCorrectVerticalVideo && !mScaleToFit && (requestedOrientation == SCREEN_ROTATION.VERTICAL || requestedOrientation == SCREEN_ROTATION.UPSIDEDOWN_VERTICAL)) { 133 | Matrix.scaleM(texMatrix, 0, 0.316f, 1.0f, 1f); 134 | } 135 | mProgram.draw(IDENTITY_MATRIX, mRectDrawable.getVertexArray(), 0, 136 | mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(), 137 | mRectDrawable.getVertexStride(), 138 | texMatrix, TEX_COORDS_BUF, textureId, TEX_COORDS_STRIDE); 139 | } 140 | } 141 | 142 | /** 143 | * Pass touch event down to the 144 | * texture's shader program 145 | * 146 | * @param ev 147 | */ 148 | public void handleTouchEvent(MotionEvent ev) { 149 | mProgram.handleTouchEvent(ev); 150 | } 151 | 152 | public static enum SCREEN_ROTATION {LANDSCAPE, VERTICAL, UPSIDEDOWN_LANDSCAPE, UPSIDEDOWN_VERTICAL} 153 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/Muxer.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaFormat; 5 | import android.os.Build; 6 | import android.util.Log; 7 | import io.kickflip.sdk.Util; 8 | 9 | import java.nio.ByteBuffer; 10 | 11 | /** 12 | * Base Muxer class for interaction with MediaCodec based 13 | * encoders 14 | * 15 | * @hide 16 | */ 17 | public abstract class Muxer { 18 | private static final String TAG = "Muxer"; 19 | private final int mExpectedNumTracks = 2; // TODO: Make this configurable? 20 | protected FORMAT mFormat; 21 | protected String mOutputPath; 22 | protected int mNumTracks; 23 | protected int mNumTracksFinished; 24 | protected long mFirstPts; 25 | protected long mLastPts[]; 26 | protected Muxer(String outputPath, FORMAT format) { 27 | Log.i(TAG, "Created muxer for output: " + outputPath); 28 | mOutputPath = Util.checkNotNull(outputPath); 29 | mFormat = format; 30 | mNumTracks = 0; 31 | mNumTracksFinished = 0; 32 | mFirstPts = 0; 33 | mLastPts = new long[mExpectedNumTracks]; 34 | for (int i = 0; i < mLastPts.length; i++) { 35 | mLastPts[i] = 0; 36 | } 37 | } 38 | 39 | /** 40 | * Returns the absolute output path. 41 | * 42 | * e.g /sdcard/app/uuid/index.m3u8 43 | * 44 | * @return 45 | */ 46 | public String getOutputPath() { 47 | return mOutputPath; 48 | } 49 | 50 | /** 51 | * Adds the specified track and returns the track index 52 | * 53 | * @param trackFormat MediaFormat of the track to add. Gotten from MediaCodec#dequeueOutputBuffer 54 | * when returned status is INFO_OUTPUT_FORMAT_CHANGED 55 | * @return index of track in output file 56 | */ 57 | public int addTrack(MediaFormat trackFormat) { 58 | mNumTracks++; 59 | return mNumTracks - 1; 60 | } 61 | 62 | /** 63 | * Called by the hosting Encoder 64 | * to notify the Muxer that it should no 65 | * longer assume the Encoder resources are available. 66 | */ 67 | public void onEncoderReleased(int trackIndex) { 68 | } 69 | 70 | public void release() { 71 | // finished event 72 | } 73 | 74 | public boolean isStarted() { 75 | return false; 76 | } 77 | 78 | /** 79 | * Write the MediaCodec output buffer. This method must 80 | * be overridden by subclasses to release encodedData, transferring 81 | * ownership back to encoder, by calling encoder.releaseOutputBuffer(bufferIndex, false); 82 | * 83 | * @param trackIndex 84 | * @param encodedData 85 | * @param bufferInfo 86 | */ 87 | public void writeSampleData(MediaCodec encoder, int trackIndex, int bufferIndex, ByteBuffer encodedData, MediaCodec.BufferInfo bufferInfo) { 88 | if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 89 | signalEndOfTrack(); 90 | } 91 | } 92 | 93 | public abstract void forceStop(); 94 | 95 | protected boolean allTracksFinished() { 96 | return (mNumTracks == mNumTracksFinished); 97 | } 98 | 99 | protected boolean allTracksAdded() { 100 | return (mNumTracks == mExpectedNumTracks); 101 | } 102 | 103 | /** 104 | * Muxer will call this itself if it detects BUFFER_FLAG_END_OF_STREAM 105 | * in writeSampleData. 106 | */ 107 | protected void signalEndOfTrack() { 108 | mNumTracksFinished++; 109 | } 110 | 111 | /** 112 | * Does this Muxer's format require AAC ADTS headers? 113 | * see http://wiki.multimedia.cx/index.php?title=ADTS 114 | * 115 | * @return 116 | */ 117 | protected boolean formatRequiresADTS() { 118 | switch (mFormat) { 119 | case HLS: 120 | return true; 121 | default: 122 | return false; 123 | } 124 | } 125 | 126 | /** 127 | * Does this Muxer's format require 128 | * copying and buffering encoder output buffers. 129 | * Generally speaking, is the output a Socket or File? 130 | * 131 | * @return 132 | */ 133 | protected boolean formatRequiresBuffering() { 134 | if (Build.VERSION.SDK_INT >= 21) return true; 135 | 136 | switch (mFormat) { 137 | case HLS: 138 | return false; 139 | default: 140 | return false; 141 | } 142 | } 143 | 144 | /** 145 | * Return a relative pts given an absolute pts and trackIndex. 146 | * 147 | * This method advances the state of the Muxer, and must only 148 | * be called once per call to {@link #writeSampleData(MediaCodec, int, int, ByteBuffer, MediaCodec.BufferInfo)}. 149 | */ 150 | protected long getNextRelativePts(long absPts, int trackIndex) { 151 | if (mFirstPts == 0) { 152 | mFirstPts = absPts; 153 | return 0; 154 | } 155 | return getSafePts(absPts - mFirstPts, trackIndex); 156 | } 157 | 158 | /** 159 | * Sometimes packets with non-increasing pts are dequeued from the MediaCodec output buffer. 160 | * This method ensures that a crash won't occur due to non monotonically increasing packet timestamp. 161 | */ 162 | private long getSafePts(long pts, int trackIndex) { 163 | if (mLastPts[trackIndex] >= pts) { 164 | // Enforce a non-zero minimum spacing 165 | // between pts 166 | mLastPts[trackIndex] += 9643; 167 | return mLastPts[trackIndex]; 168 | } 169 | mLastPts[trackIndex] = pts; 170 | return pts; 171 | } 172 | 173 | public static enum FORMAT {MPEG4, HLS} 174 | } 175 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/SizeableFrameRect.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 io.kickflip.sdk.av; 18 | 19 | import android.opengl.Matrix; 20 | import android.view.MotionEvent; 21 | 22 | import java.nio.FloatBuffer; 23 | 24 | /** 25 | * This class essentially represents a sizezble sprite that will be rendered with 26 | * a texture, usually from an external source like the camera or video decoder. 27 | * 28 | * Placeholder. Not yet implemented. 29 | * 30 | * @hide 31 | */ 32 | public class SizeableFrameRect { 33 | private static final int SIZEOF_FLOAT = 4; 34 | private static final float[] IDENTITY_MATRIX = new float[16]; 35 | private static final int TEX_COORDS_STRIDE = 2 * SIZEOF_FLOAT; 36 | private static float TEX_COORDS[] = { 37 | 0.0f, 0.0f, // 0 bottom left 38 | 1.0f, 0.0f, // 1 bottom right 39 | 0.0f, 1.0f, // 2 top left 40 | 1.0f, 1.0f // 3 top right 41 | }; 42 | private static final FloatBuffer TEX_COORDS_BUF = GlUtil.createFloatBuffer(TEX_COORDS); 43 | private final Drawable2d mRectDrawable = new Drawable2d(Drawable2d.Prefab.RECTANGLE); 44 | private Texture2dProgram mProgram; 45 | 46 | 47 | /** 48 | * Prepares the object. 49 | * 50 | * @param program The program to use. FullFrameRect takes ownership, and will release 51 | * the program when no longer needed. 52 | */ 53 | public SizeableFrameRect(Texture2dProgram program, float[] texCoords) { 54 | mProgram = program; 55 | TEX_COORDS = texCoords; 56 | 57 | Matrix.setIdentityM(IDENTITY_MATRIX, 0); 58 | } 59 | 60 | /** 61 | * Releases resources. 62 | */ 63 | public void release() { 64 | if (mProgram != null) { 65 | mProgram.release(); 66 | mProgram = null; 67 | } 68 | } 69 | 70 | /** 71 | * Returns the program currently in use. 72 | */ 73 | public Texture2dProgram getProgram() { 74 | return mProgram; 75 | } 76 | 77 | /** 78 | * Changes the program. The previous program will be released. 79 | */ 80 | public void changeProgram(Texture2dProgram program) { 81 | mProgram.release(); 82 | mProgram = program; 83 | } 84 | 85 | /** 86 | * Creates a texture object suitable for use with drawFrame(). 87 | */ 88 | public int createTextureObject() { 89 | return mProgram.createTextureObject(); 90 | } 91 | 92 | /** 93 | * Draws a rectangle in an area defined by TEX_COORDS 94 | */ 95 | public void drawFrame(int textureId, float[] texMatrix) { 96 | // Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport. 97 | mProgram.draw(IDENTITY_MATRIX, mRectDrawable.getVertexArray(), 0, 98 | mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(), 99 | mRectDrawable.getVertexStride(), 100 | texMatrix, TEX_COORDS_BUF, textureId, TEX_COORDS_STRIDE); 101 | } 102 | 103 | /** 104 | * Pass touch event down to the 105 | * texture's shader program 106 | * 107 | * @param ev 108 | */ 109 | public void handleTouchEvent(MotionEvent ev) { 110 | mProgram.handleTouchEvent(ev); 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/VideoEncoderConfig.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.av; 2 | 3 | /** 4 | * @hide 5 | */ 6 | public class VideoEncoderConfig { 7 | protected final int mWidth; 8 | protected final int mHeight; 9 | protected final int mBitRate; 10 | 11 | public VideoEncoderConfig(int width, int height, int bitRate) { 12 | mWidth = width; 13 | mHeight = height; 14 | mBitRate = bitRate; 15 | } 16 | 17 | public int getWidth() { 18 | return mWidth; 19 | } 20 | 21 | public int getHeight() { 22 | return mHeight; 23 | } 24 | 25 | public int getBitRate() { 26 | return mBitRate; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "VideoEncoderConfig: " + mWidth + "x" + mHeight + " @" + mBitRate + " bps"; 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/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 io.kickflip.sdk.av; 18 | 19 | import android.media.MediaCodec; 20 | import android.media.MediaCodecInfo; 21 | import android.media.MediaFormat; 22 | import android.util.Log; 23 | import android.view.Surface; 24 | 25 | import java.io.IOException; 26 | 27 | /** 28 | * This class wraps up the core components used for surface-input video encoding. 29 | *

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

34 | * This class is not thread-safe, with one exception: it is valid to use the input surface 35 | * on one thread, and drain the output on a different thread. 36 | */ 37 | public class VideoEncoderCore extends AndroidEncoder { 38 | private static final String TAG = "VideoEncoderCore"; 39 | private static final boolean VERBOSE = false; 40 | 41 | // TODO: these ought to be configurable as well 42 | private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding 43 | private static final int FRAME_RATE = 30; // 30fps 44 | private static final int IFRAME_INTERVAL = 3; // 5 seconds between I-frames 45 | 46 | private Surface mInputSurface; 47 | 48 | 49 | /** 50 | * Configures encoder and muxer state, and prepares the input Surface. 51 | */ 52 | public VideoEncoderCore(int width, int height, int bitRate, Muxer muxer) throws IOException { 53 | mMuxer = muxer; 54 | mBufferInfo = new MediaCodec.BufferInfo(); 55 | 56 | MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height); 57 | 58 | // Set some properties. Failing to specify some of these can cause the MediaCodec 59 | // configure() call to throw an unhelpful exception. 60 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 61 | MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 62 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); 63 | format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); 64 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); 65 | if (VERBOSE) Log.d(TAG, "format: " + format); 66 | 67 | // Create a MediaCodec encoder, and configure it with our format. Get a Surface 68 | // we can use for input and wrap it with a class that handles the EGL work. 69 | mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); 70 | mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 71 | mInputSurface = mEncoder.createInputSurface(); 72 | mEncoder.start(); 73 | 74 | mTrackIndex = -1; 75 | } 76 | 77 | /** 78 | * Returns the encoder's input surface. 79 | */ 80 | public Surface getInputSurface() { 81 | return mInputSurface; 82 | } 83 | 84 | @Override 85 | protected boolean isSurfaceInputEncoder() { 86 | return true; 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/av/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 io.kickflip.sdk.av; 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 | * This object owns the Surface; releasing this object will release the Surface as well. 27 | * 28 | * @hide 29 | */ 30 | public class WindowSurface extends EglSurfaceBase { 31 | private Surface mSurface; 32 | 33 | /** 34 | * Associates an EGL surface with the native window surface. The Surface will be 35 | * owned by WindowSurface, and released when release() is called. 36 | */ 37 | public WindowSurface(EglCore eglCore, Surface surface) { 38 | super(eglCore); 39 | createWindowSurface(surface); 40 | mSurface = surface; 41 | } 42 | 43 | /** 44 | * Associates an EGL surface with the SurfaceTexture. 45 | */ 46 | public WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) { 47 | super(eglCore); 48 | createWindowSurface(surfaceTexture); 49 | } 50 | 51 | /** 52 | * Releases any resources associated with the Surface and the EGL surface. 53 | */ 54 | public void release() { 55 | releaseEglSurface(); 56 | if (mSurface != null) { 57 | mSurface.release(); 58 | mSurface = null; 59 | } 60 | } 61 | 62 | /** 63 | * Recreate the EGLSurface, using the new EglBase. The caller should have already 64 | * freed the old EGLSurface with releaseEglSurface(). 65 | *

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

71 | * If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a 72 | * context somewhere, the create call will fail with complaints from the Surface 73 | * about already being connected. 74 | */ 75 | public void recreate(EglCore newEglCore) { 76 | if (mSurface == null) { 77 | throw new RuntimeException("not yet implemented for SurfaceTexture"); 78 | } 79 | mEglCore = newEglCore; // switch to new context 80 | createWindowSurface(mSurface); // create new surface 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/exception/KickflipException.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.exception; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Kickflip Exception 7 | */ 8 | public class KickflipException extends IOException { 9 | private String mMessage; 10 | private int mCode; 11 | 12 | public KickflipException() { 13 | mMessage = "An unknown error occurred"; 14 | mCode = 0; 15 | } 16 | 17 | public KickflipException(String message, int code) { 18 | mMessage = message; 19 | mCode = code; 20 | } 21 | 22 | public String getMessage() { 23 | return mMessage; 24 | } 25 | 26 | public int getCode() { 27 | return mCode; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/location/DeviceLocation.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.location; 2 | 3 | import android.content.Context; 4 | import android.location.Location; 5 | import android.location.LocationListener; 6 | import android.location.LocationManager; 7 | import android.os.Bundle; 8 | import android.util.Log; 9 | 10 | import java.util.Timer; 11 | import java.util.TimerTask; 12 | 13 | // Thanks, Fedor! 14 | // TODO: Modify getLocation to take callback 15 | public class DeviceLocation { 16 | private static final String TAG = "DeviceLocation"; 17 | private static final float ACCURATE_LOCATION_THRESHOLD_METERS = 100; 18 | Timer timer1; 19 | LocationManager lm; 20 | LocationResult locationResult; 21 | boolean gps_enabled = false; 22 | boolean network_enabled = false; 23 | private Location bestLocation; 24 | private boolean waitForGpsFix; 25 | LocationListener locationListenerNetwork = new LocationListener() { 26 | public void onLocationChanged(Location location) { 27 | Log.i(TAG, "got network loc accurate to " + String.valueOf(location.getAccuracy()) + "m"); 28 | if (bestLocation == null || bestLocation.getAccuracy() > location.getAccuracy()) 29 | bestLocation = location; 30 | 31 | if (!waitForGpsFix || bestLocation.getAccuracy() < ACCURATE_LOCATION_THRESHOLD_METERS) { 32 | timer1.cancel(); 33 | locationResult.gotLocation(bestLocation); 34 | lm.removeUpdates(this); 35 | lm.removeUpdates(locationListenerGps); 36 | } 37 | 38 | } 39 | 40 | public void onProviderDisabled(String provider) { 41 | } 42 | 43 | public void onProviderEnabled(String provider) { 44 | } 45 | 46 | public void onStatusChanged(String provider, int status, Bundle extras) { 47 | } 48 | }; 49 | LocationListener locationListenerGps = new LocationListener() { 50 | public void onLocationChanged(Location location) { 51 | Log.i(TAG, "got GPS loc accurate to " + String.valueOf(location.getAccuracy()) + "m"); 52 | if (bestLocation == null || bestLocation.getAccuracy() > location.getAccuracy()) 53 | bestLocation = location; 54 | 55 | if (!waitForGpsFix || bestLocation.getAccuracy() < ACCURATE_LOCATION_THRESHOLD_METERS) { 56 | timer1.cancel(); 57 | locationResult.gotLocation(bestLocation); 58 | lm.removeUpdates(this); 59 | lm.removeUpdates(locationListenerNetwork); 60 | } 61 | 62 | } 63 | 64 | public void onProviderDisabled(String provider) { 65 | } 66 | 67 | public void onProviderEnabled(String provider) { 68 | } 69 | 70 | public void onStatusChanged(String provider, int status, Bundle extras) { 71 | } 72 | }; 73 | 74 | public static void getLocation(Context context, boolean waitForGpsFix, final LocationResult cb) { 75 | DeviceLocation deviceLocation = new DeviceLocation(); 76 | deviceLocation.getLocation(context, cb, waitForGpsFix); 77 | } 78 | 79 | /** 80 | * Get the last known location. 81 | * If one is not available, fetch 82 | * a fresh location 83 | * 84 | * @param context 85 | * @param waitForGpsFix 86 | * @param cb 87 | */ 88 | public static void getLastKnownLocation(Context context, boolean waitForGpsFix, final LocationResult cb) { 89 | DeviceLocation deviceLocation = new DeviceLocation(); 90 | 91 | LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); 92 | Location last_loc; 93 | last_loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); 94 | if (last_loc == null) 95 | last_loc = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); 96 | 97 | if (last_loc != null && cb != null) { 98 | cb.gotLocation(last_loc); 99 | } else { 100 | deviceLocation.getLocation(context, cb, waitForGpsFix); 101 | } 102 | } 103 | 104 | public boolean getLocation(Context context, LocationResult result, boolean waitForGpsFix) { 105 | this.waitForGpsFix = waitForGpsFix; 106 | 107 | //I use LocationResult callback class to pass location value from MyLocation to user code. 108 | locationResult = result; 109 | if (lm == null) 110 | lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); 111 | 112 | //exceptions will be thrown if provider is not permitted. 113 | try { 114 | gps_enabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER); 115 | } catch (Exception ex) { 116 | } 117 | try { 118 | network_enabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER); 119 | } catch (Exception ex) { 120 | } 121 | 122 | //don't start listeners if no provider is enabled 123 | if (!gps_enabled && !network_enabled) 124 | return false; 125 | 126 | if (gps_enabled) 127 | lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps); 128 | if (network_enabled) 129 | lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork); 130 | timer1 = new Timer(); 131 | timer1.schedule(new GetBestLocation(), 20000); 132 | return true; 133 | } 134 | 135 | public static abstract class LocationResult { 136 | public abstract void gotLocation(Location location); 137 | } 138 | 139 | class GetBestLocation extends TimerTask { 140 | @Override 141 | public void run() { 142 | Log.i(TAG, "Timer expired before adequate location acquired"); 143 | lm.removeUpdates(locationListenerGps); 144 | lm.removeUpdates(locationListenerNetwork); 145 | 146 | Location net_loc = null, gps_loc = null; 147 | if (gps_enabled) 148 | gps_loc = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); 149 | if (network_enabled) 150 | net_loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); 151 | 152 | //if there are both values use the latest one 153 | if (gps_loc != null && net_loc != null) { 154 | if (gps_loc.getTime() > net_loc.getTime()) 155 | locationResult.gotLocation(gps_loc); 156 | else 157 | locationResult.gotLocation(net_loc); 158 | return; 159 | } 160 | 161 | if (gps_loc != null) { 162 | locationResult.gotLocation(gps_loc); 163 | return; 164 | } 165 | if (net_loc != null) { 166 | locationResult.gotLocation(net_loc); 167 | return; 168 | } 169 | locationResult.gotLocation(null); 170 | } 171 | } 172 | 173 | 174 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/location/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @hide 3 | **/ 4 | package io.kickflip.sdk.location; -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/view/GLCameraEncoderView.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | import io.kickflip.sdk.av.CameraEncoder; 7 | 8 | /** 9 | * Special GLSurfaceView for use with CameraEncoder 10 | * The tight coupling here allows richer touch interaction 11 | */ 12 | public class GLCameraEncoderView extends GLCameraView { 13 | private static final String TAG = "GLCameraEncoderView"; 14 | 15 | protected CameraEncoder mCameraEncoder; 16 | 17 | public GLCameraEncoderView(Context context) { 18 | super(context); 19 | } 20 | 21 | public GLCameraEncoderView(Context context, AttributeSet attrs) { 22 | super(context, attrs); 23 | } 24 | 25 | public void setCameraEncoder(CameraEncoder encoder) { 26 | mCameraEncoder = encoder; 27 | setCamera(mCameraEncoder.getCamera()); 28 | } 29 | 30 | @Override 31 | public boolean onTouchEvent(MotionEvent ev) { 32 | if (mScaleGestureDetector != null) { 33 | mScaleGestureDetector.onTouchEvent(ev); 34 | } 35 | if (mCameraEncoder != null && ev.getPointerCount() == 1 && (ev.getAction() == MotionEvent.ACTION_MOVE)) { 36 | mCameraEncoder.handleCameraPreviewTouchEvent(ev); 37 | } else if (mCameraEncoder != null && ev.getPointerCount() == 1 && (ev.getAction() == MotionEvent.ACTION_DOWN)) { 38 | mCameraEncoder.handleCameraPreviewTouchEvent(ev); 39 | } 40 | return true; 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/view/GLCameraView.java: -------------------------------------------------------------------------------- 1 | package io.kickflip.sdk.view; 2 | 3 | import android.content.Context; 4 | import android.hardware.Camera; 5 | import android.opengl.GLSurfaceView; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | import android.view.ScaleGestureDetector; 9 | 10 | /** 11 | * Created by davidbrodsky on 1/30/14. 12 | */ 13 | public class GLCameraView extends GLSurfaceView { 14 | private static final String TAG = "GLCameraView"; 15 | 16 | protected ScaleGestureDetector mScaleGestureDetector; 17 | private Camera mCamera; 18 | private int mMaxZoom; 19 | private ScaleGestureDetector.SimpleOnScaleGestureListener mScaleListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() { 20 | 21 | int mZoomWhenScaleBegan = 0; 22 | int mCurrentZoom = 0; 23 | 24 | @Override 25 | public boolean onScale(ScaleGestureDetector detector) { 26 | if (mCamera != null) { 27 | Camera.Parameters params = mCamera.getParameters(); 28 | mCurrentZoom = (int) (mZoomWhenScaleBegan + (mMaxZoom * (detector.getScaleFactor() - 1))); 29 | mCurrentZoom = Math.min(mCurrentZoom, mMaxZoom); 30 | mCurrentZoom = Math.max(0, mCurrentZoom); 31 | params.setZoom(mCurrentZoom); 32 | mCamera.setParameters(params); 33 | } 34 | 35 | return false; 36 | } 37 | 38 | @Override 39 | public boolean onScaleBegin(ScaleGestureDetector detector) { 40 | mZoomWhenScaleBegan = mCamera.getParameters().getZoom(); 41 | return true; 42 | } 43 | 44 | @Override 45 | public void onScaleEnd(ScaleGestureDetector detector) { 46 | } 47 | }; 48 | 49 | public GLCameraView(Context context) { 50 | super(context); 51 | init(context); 52 | } 53 | 54 | public GLCameraView(Context context, AttributeSet attrs) { 55 | super(context, attrs); 56 | init(context); 57 | } 58 | 59 | private void init(Context context) { 60 | mMaxZoom = 0; 61 | 62 | } 63 | 64 | public void setCamera(Camera camera) { 65 | mCamera = camera; 66 | Camera.Parameters camParams = mCamera.getParameters(); 67 | if (camParams.isZoomSupported()) { 68 | mMaxZoom = camParams.getMaxZoom(); 69 | mScaleGestureDetector = new ScaleGestureDetector(getContext(), mScaleListener); 70 | } 71 | } 72 | 73 | public void releaseCamera() { 74 | mCamera = null; 75 | mScaleGestureDetector = null; 76 | } 77 | 78 | @Override 79 | public boolean onTouchEvent(MotionEvent ev) { 80 | if (mScaleGestureDetector != null) { 81 | if (!mScaleGestureDetector.onTouchEvent(ev)) { 82 | // No scale gesture detected 83 | 84 | } 85 | } 86 | return true; 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/io/kickflip/sdk/view/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @hide 3 | **/ 4 | package io.kickflip.sdk.view; -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/MediaCodec/76fce088a9a3d19c5dda4427dcc870cec15fb783/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/media_recorder.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 |