├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── polarxiong │ │ └── videotoimages │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── polarxiong │ │ │ └── videotoimages │ │ │ ├── MainActivity.java │ │ │ ├── OutputImageFormat.java │ │ │ └── VideoToFrames.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── polarxiong │ └── videotoimages │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | VideoToImages -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoToImages 2 | 本Android APP是博客文章[Android: MediaCodec视频文件硬件解码,高效率得到YUV格式帧,快速保存JPEG图片(不使用OpenGL)(附Demo)][blog]的Demo APP。 3 | 4 | 指定输入视频文件和输出文件夹名,此APP可将视频帧保存为I420、NV21或JPEG格式。 5 | 6 | [blog]:http://www.polarxiong.com/archives/Android-MediaCodec%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6%E7%A1%AC%E4%BB%B6%E8%A7%A3%E7%A0%81-%E9%AB%98%E6%95%88%E7%8E%87%E5%BE%97%E5%88%B0YUV%E6%A0%BC%E5%BC%8F%E5%B8%A7-%E5%BF%AB%E9%80%9F%E4%BF%9D%E5%AD%98JPEG%E5%9B%BE%E7%89%87-%E4%B8%8D%E4%BD%BF%E7%94%A8OpenGL.html 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.polarxiong.videotoimages" 9 | minSdkVersion 21 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.4.0' 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/zhantong/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/polarxiong/videotoimages/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.polarxiong.videotoimages; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/polarxiong/videotoimages/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.polarxiong.videotoimages; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.database.Cursor; 9 | import android.net.Uri; 10 | import android.os.Bundle; 11 | import android.os.Environment; 12 | import android.os.Handler; 13 | import android.os.Message; 14 | import android.provider.MediaStore; 15 | import android.support.annotation.NonNull; 16 | import android.support.v4.app.ActivityCompat; 17 | import android.support.v4.content.ContextCompat; 18 | import android.view.View; 19 | import android.widget.AdapterView; 20 | import android.widget.ArrayAdapter; 21 | import android.widget.Button; 22 | import android.widget.EditText; 23 | import android.widget.Spinner; 24 | import android.widget.TextView; 25 | import android.widget.Toast; 26 | 27 | public class MainActivity extends Activity implements VideoToFrames.Callback { 28 | private static final int REQUEST_CODE_GET_FILE_PATH = 1; 29 | private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 1; 30 | private OutputImageFormat outputImageFormat; 31 | private MainActivity self = this; 32 | private String outputDir; 33 | 34 | final Handler handler = new Handler() { 35 | public void handleMessage(Message msg) { 36 | String str = (String) msg.obj; 37 | updateInfo(str); 38 | } 39 | }; 40 | 41 | @Override 42 | protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_main); 45 | 46 | initImageFormatSpinner(); 47 | 48 | final Button buttonFilePathInput = (Button) findViewById(R.id.button_file_path_input); 49 | buttonFilePathInput.setOnClickListener(new View.OnClickListener() { 50 | @Override 51 | public void onClick(View v) { 52 | if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 53 | ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_WRITE_EXTERNAL_STORAGE); 54 | } else { 55 | getFilePath(REQUEST_CODE_GET_FILE_PATH); 56 | } 57 | } 58 | }); 59 | 60 | final Button buttonStart = (Button) findViewById(R.id.button_start); 61 | buttonStart.setOnClickListener(new View.OnClickListener() { 62 | @Override 63 | public void onClick(View v) { 64 | EditText editTextOutputFolder = (EditText) findViewById(R.id.folder_created); 65 | outputDir = Environment.getExternalStorageDirectory() + "/" + editTextOutputFolder.getText().toString(); 66 | EditText editTextInputFilePath = (EditText) findViewById(R.id.file_path_input); 67 | String inputFilePath = editTextInputFilePath.getText().toString(); 68 | VideoToFrames videoToFrames = new VideoToFrames(); 69 | videoToFrames.setCallback(self); 70 | try { 71 | videoToFrames.setSaveFrames(outputDir, outputImageFormat); 72 | updateInfo("运行中..."); 73 | videoToFrames.decode(inputFilePath); 74 | } catch (Throwable t) { 75 | t.printStackTrace(); 76 | } 77 | } 78 | }); 79 | } 80 | 81 | @Override 82 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 83 | switch (requestCode) { 84 | case PERMISSION_WRITE_EXTERNAL_STORAGE: 85 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 86 | getFilePath(REQUEST_CODE_GET_FILE_PATH); 87 | } else { 88 | Toast.makeText(this, "需要开启文件读写权限", Toast.LENGTH_SHORT).show(); 89 | } 90 | return; 91 | default: 92 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 93 | } 94 | } 95 | 96 | private void initImageFormatSpinner() { 97 | Spinner barcodeFormatSpinner = (Spinner) findViewById(R.id.image_format); 98 | ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, OutputImageFormat.values()); 99 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 100 | barcodeFormatSpinner.setAdapter(adapter); 101 | barcodeFormatSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 102 | @Override 103 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 104 | outputImageFormat = OutputImageFormat.values()[position]; 105 | } 106 | 107 | @Override 108 | public void onNothingSelected(AdapterView parent) { 109 | 110 | } 111 | }); 112 | } 113 | 114 | private void getFilePath(int requestCode) { 115 | Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 116 | intent.setType("*/*"); 117 | intent.addCategory(Intent.CATEGORY_OPENABLE); 118 | if (intent.resolveActivity(getPackageManager()) != null) { 119 | startActivityForResult(Intent.createChooser(intent, "选择视频文件"), requestCode); 120 | } else { 121 | new AlertDialog.Builder(this).setTitle("未找到文件管理器") 122 | .setMessage("请安装文件管理器以选择文件") 123 | .setPositiveButton("确定", null) 124 | .show(); 125 | } 126 | } 127 | 128 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 129 | int id = 0; 130 | switch (requestCode) { 131 | case REQUEST_CODE_GET_FILE_PATH: 132 | id = R.id.file_path_input; 133 | break; 134 | } 135 | if (resultCode == Activity.RESULT_OK) { 136 | EditText editText = (EditText) findViewById(id); 137 | String curFileName = getRealPathFromURI(data.getData()); 138 | editText.setText(curFileName); 139 | } 140 | } 141 | 142 | private void updateInfo(String info) { 143 | TextView textView = (TextView) findViewById(R.id.info); 144 | textView.setText(info); 145 | } 146 | 147 | public void onDecodeFrame(int index) { 148 | Message msg = handler.obtainMessage(); 149 | msg.obj = "运行中...第" + index + "帧"; 150 | handler.sendMessage(msg); 151 | } 152 | 153 | public void onFinishDecode() { 154 | Message msg = handler.obtainMessage(); 155 | msg.obj = "完成!所有图片已存储到" + outputDir; 156 | handler.sendMessage(msg); 157 | } 158 | 159 | private String getRealPathFromURI(Uri contentURI) { 160 | String result; 161 | String[] proj = {MediaStore.Images.Media.DATA}; 162 | Cursor cursor = getContentResolver().query(contentURI, proj, null, null, null); 163 | if (cursor == null) { // Source is Dropbox or other similar local file path 164 | result = contentURI.getPath(); 165 | } else { 166 | cursor.moveToFirst(); 167 | int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); 168 | result = cursor.getString(idx); 169 | cursor.close(); 170 | } 171 | return result; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /app/src/main/java/com/polarxiong/videotoimages/OutputImageFormat.java: -------------------------------------------------------------------------------- 1 | package com.polarxiong.videotoimages; 2 | 3 | /** 4 | * Created by zhantong on 16/9/8. 5 | */ 6 | public enum OutputImageFormat { 7 | I420("I420"), 8 | NV21("NV21"), 9 | JPEG("JPEG"); 10 | private String friendlyName; 11 | 12 | private OutputImageFormat(String friendlyName) { 13 | this.friendlyName = friendlyName; 14 | } 15 | 16 | public String toString() { 17 | return friendlyName; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/polarxiong/videotoimages/VideoToFrames.java: -------------------------------------------------------------------------------- 1 | package com.polarxiong.videotoimages; 2 | 3 | import android.graphics.ImageFormat; 4 | import android.graphics.Rect; 5 | import android.graphics.YuvImage; 6 | import android.media.Image; 7 | import android.media.MediaCodec; 8 | import android.media.MediaCodecInfo; 9 | import android.media.MediaExtractor; 10 | import android.media.MediaFormat; 11 | import android.util.Log; 12 | 13 | import java.io.File; 14 | import java.io.FileOutputStream; 15 | import java.io.IOException; 16 | import java.nio.ByteBuffer; 17 | import java.util.concurrent.LinkedBlockingQueue; 18 | 19 | /** 20 | * Created by zhantong on 16/5/12. 21 | */ 22 | public class VideoToFrames implements Runnable { 23 | private static final String TAG = "VideoToFrames"; 24 | private static final boolean VERBOSE = false; 25 | private static final long DEFAULT_TIMEOUT_US = 10000; 26 | 27 | private static final int COLOR_FormatI420 = 1; 28 | private static final int COLOR_FormatNV21 = 2; 29 | 30 | 31 | private final int decodeColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible; 32 | 33 | private LinkedBlockingQueue mQueue; 34 | private OutputImageFormat outputImageFormat; 35 | private String OUTPUT_DIR; 36 | private boolean stopDecode = false; 37 | 38 | private String videoFilePath; 39 | private Throwable throwable; 40 | private Thread childThread; 41 | 42 | private Callback callback; 43 | 44 | public interface Callback { 45 | void onFinishDecode(); 46 | 47 | void onDecodeFrame(int index); 48 | } 49 | 50 | public void setCallback(Callback callback) { 51 | this.callback = callback; 52 | } 53 | 54 | public void setEnqueue(LinkedBlockingQueue queue) { 55 | mQueue = queue; 56 | } 57 | 58 | public void setSaveFrames(String dir, OutputImageFormat imageFormat) throws IOException { 59 | outputImageFormat = imageFormat; 60 | File theDir = new File(dir); 61 | if (!theDir.exists()) { 62 | theDir.mkdirs(); 63 | } else if (!theDir.isDirectory()) { 64 | throw new IOException("Not a directory"); 65 | } 66 | OUTPUT_DIR = theDir.getAbsolutePath() + "/"; 67 | } 68 | 69 | public void stopDecode() { 70 | stopDecode = true; 71 | } 72 | 73 | public void decode(String videoFilePath) throws Throwable { 74 | this.videoFilePath = videoFilePath; 75 | if (childThread == null) { 76 | childThread = new Thread(this, "decode"); 77 | childThread.start(); 78 | if (throwable != null) { 79 | throw throwable; 80 | } 81 | } 82 | } 83 | 84 | public void run() { 85 | try { 86 | videoDecode(videoFilePath); 87 | } catch (Throwable t) { 88 | throwable = t; 89 | } 90 | } 91 | 92 | public void videoDecode(String videoFilePath) throws IOException { 93 | MediaExtractor extractor = null; 94 | MediaCodec decoder = null; 95 | try { 96 | File videoFile = new File(videoFilePath); 97 | extractor = new MediaExtractor(); 98 | extractor.setDataSource(videoFile.toString()); 99 | int trackIndex = selectTrack(extractor); 100 | if (trackIndex < 0) { 101 | throw new RuntimeException("No video track found in " + videoFilePath); 102 | } 103 | extractor.selectTrack(trackIndex); 104 | MediaFormat mediaFormat = extractor.getTrackFormat(trackIndex); 105 | String mime = mediaFormat.getString(MediaFormat.KEY_MIME); 106 | decoder = MediaCodec.createDecoderByType(mime); 107 | showSupportedColorFormat(decoder.getCodecInfo().getCapabilitiesForType(mime)); 108 | if (isColorFormatSupported(decodeColorFormat, decoder.getCodecInfo().getCapabilitiesForType(mime))) { 109 | mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, decodeColorFormat); 110 | Log.i(TAG, "set decode color format to type " + decodeColorFormat); 111 | } else { 112 | Log.i(TAG, "unable to set decode color format, color format type " + decodeColorFormat + " not supported"); 113 | } 114 | decodeFramesToImage(decoder, extractor, mediaFormat); 115 | decoder.stop(); 116 | } finally { 117 | if (decoder != null) { 118 | decoder.stop(); 119 | decoder.release(); 120 | decoder = null; 121 | } 122 | if (extractor != null) { 123 | extractor.release(); 124 | extractor = null; 125 | } 126 | } 127 | } 128 | 129 | private void showSupportedColorFormat(MediaCodecInfo.CodecCapabilities caps) { 130 | System.out.print("supported color format: "); 131 | for (int c : caps.colorFormats) { 132 | System.out.print(c + "\t"); 133 | } 134 | System.out.println(); 135 | } 136 | 137 | private boolean isColorFormatSupported(int colorFormat, MediaCodecInfo.CodecCapabilities caps) { 138 | for (int c : caps.colorFormats) { 139 | if (c == colorFormat) { 140 | return true; 141 | } 142 | } 143 | return false; 144 | } 145 | 146 | private void decodeFramesToImage(MediaCodec decoder, MediaExtractor extractor, MediaFormat mediaFormat) { 147 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 148 | boolean sawInputEOS = false; 149 | boolean sawOutputEOS = false; 150 | decoder.configure(mediaFormat, null, null, 0); 151 | decoder.start(); 152 | final int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH); 153 | final int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT); 154 | int outputFrameCount = 0; 155 | while (!sawOutputEOS && !stopDecode) { 156 | if (!sawInputEOS) { 157 | int inputBufferId = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US); 158 | if (inputBufferId >= 0) { 159 | ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufferId); 160 | int sampleSize = extractor.readSampleData(inputBuffer, 0); 161 | if (sampleSize < 0) { 162 | decoder.queueInputBuffer(inputBufferId, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM); 163 | sawInputEOS = true; 164 | } else { 165 | long presentationTimeUs = extractor.getSampleTime(); 166 | decoder.queueInputBuffer(inputBufferId, 0, sampleSize, presentationTimeUs, 0); 167 | extractor.advance(); 168 | } 169 | } 170 | } 171 | int outputBufferId = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US); 172 | if (outputBufferId >= 0) { 173 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 174 | sawOutputEOS = true; 175 | } 176 | boolean doRender = (info.size != 0); 177 | if (doRender) { 178 | outputFrameCount++; 179 | if (callback != null) { 180 | callback.onDecodeFrame(outputFrameCount); 181 | } 182 | Image image = decoder.getOutputImage(outputBufferId); 183 | //System.out.println("image format: " + image.getFormat()); 184 | 185 | ByteBuffer buffer = image.getPlanes()[0].getBuffer(); 186 | byte[] arr = new byte[buffer.remaining()]; 187 | buffer.get(arr); 188 | if (mQueue != null) { 189 | try { 190 | mQueue.put(arr); 191 | } catch (InterruptedException e) { 192 | e.printStackTrace(); 193 | } 194 | } 195 | 196 | if (outputImageFormat != null) { 197 | String fileName; 198 | switch (outputImageFormat) { 199 | case I420: 200 | fileName = OUTPUT_DIR + String.format("frame_%05d_I420_%dx%d.yuv", outputFrameCount, width, height); 201 | dumpFile(fileName, getDataFromImage(image, COLOR_FormatI420)); 202 | break; 203 | case NV21: 204 | fileName = OUTPUT_DIR + String.format("frame_%05d_NV21_%dx%d.yuv", outputFrameCount, width, height); 205 | dumpFile(fileName, getDataFromImage(image, COLOR_FormatNV21)); 206 | break; 207 | case JPEG: 208 | fileName = OUTPUT_DIR + String.format("frame_%05d.jpg", outputFrameCount); 209 | compressToJpeg(fileName, image); 210 | break; 211 | } 212 | } 213 | image.close(); 214 | decoder.releaseOutputBuffer(outputBufferId, true); 215 | } 216 | } 217 | } 218 | if (callback != null) { 219 | callback.onFinishDecode(); 220 | } 221 | } 222 | 223 | private static int selectTrack(MediaExtractor extractor) { 224 | int numTracks = extractor.getTrackCount(); 225 | for (int i = 0; i < numTracks; i++) { 226 | MediaFormat format = extractor.getTrackFormat(i); 227 | String mime = format.getString(MediaFormat.KEY_MIME); 228 | if (mime.startsWith("video/")) { 229 | if (VERBOSE) { 230 | Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format); 231 | } 232 | return i; 233 | } 234 | } 235 | return -1; 236 | } 237 | 238 | private static boolean isImageFormatSupported(Image image) { 239 | int format = image.getFormat(); 240 | switch (format) { 241 | case ImageFormat.YUV_420_888: 242 | case ImageFormat.NV21: 243 | case ImageFormat.YV12: 244 | return true; 245 | } 246 | return false; 247 | } 248 | 249 | private static byte[] getDataFromImage(Image image, int colorFormat) { 250 | if (colorFormat != COLOR_FormatI420 && colorFormat != COLOR_FormatNV21) { 251 | throw new IllegalArgumentException("only support COLOR_FormatI420 " + "and COLOR_FormatNV21"); 252 | } 253 | if (!isImageFormatSupported(image)) { 254 | throw new RuntimeException("can't convert Image to byte array, format " + image.getFormat()); 255 | } 256 | Rect crop = image.getCropRect(); 257 | int format = image.getFormat(); 258 | int width = crop.width(); 259 | int height = crop.height(); 260 | Image.Plane[] planes = image.getPlanes(); 261 | byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8]; 262 | byte[] rowData = new byte[planes[0].getRowStride()]; 263 | if (VERBOSE) Log.v(TAG, "get data from " + planes.length + " planes"); 264 | int channelOffset = 0; 265 | int outputStride = 1; 266 | for (int i = 0; i < planes.length; i++) { 267 | switch (i) { 268 | case 0: 269 | channelOffset = 0; 270 | outputStride = 1; 271 | break; 272 | case 1: 273 | if (colorFormat == COLOR_FormatI420) { 274 | channelOffset = width * height; 275 | outputStride = 1; 276 | } else if (colorFormat == COLOR_FormatNV21) { 277 | channelOffset = width * height + 1; 278 | outputStride = 2; 279 | } 280 | break; 281 | case 2: 282 | if (colorFormat == COLOR_FormatI420) { 283 | channelOffset = (int) (width * height * 1.25); 284 | outputStride = 1; 285 | } else if (colorFormat == COLOR_FormatNV21) { 286 | channelOffset = width * height; 287 | outputStride = 2; 288 | } 289 | break; 290 | } 291 | ByteBuffer buffer = planes[i].getBuffer(); 292 | int rowStride = planes[i].getRowStride(); 293 | int pixelStride = planes[i].getPixelStride(); 294 | if (VERBOSE) { 295 | Log.v(TAG, "pixelStride " + pixelStride); 296 | Log.v(TAG, "rowStride " + rowStride); 297 | Log.v(TAG, "width " + width); 298 | Log.v(TAG, "height " + height); 299 | Log.v(TAG, "buffer size " + buffer.remaining()); 300 | } 301 | int shift = (i == 0) ? 0 : 1; 302 | int w = width >> shift; 303 | int h = height >> shift; 304 | buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift)); 305 | for (int row = 0; row < h; row++) { 306 | int length; 307 | if (pixelStride == 1 && outputStride == 1) { 308 | length = w; 309 | buffer.get(data, channelOffset, length); 310 | channelOffset += length; 311 | } else { 312 | length = (w - 1) * pixelStride + 1; 313 | buffer.get(rowData, 0, length); 314 | for (int col = 0; col < w; col++) { 315 | data[channelOffset] = rowData[col * pixelStride]; 316 | channelOffset += outputStride; 317 | } 318 | } 319 | if (row < h - 1) { 320 | buffer.position(buffer.position() + rowStride - length); 321 | } 322 | } 323 | if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i); 324 | } 325 | return data; 326 | } 327 | 328 | private static void dumpFile(String fileName, byte[] data) { 329 | FileOutputStream outStream; 330 | try { 331 | outStream = new FileOutputStream(fileName); 332 | } catch (IOException ioe) { 333 | throw new RuntimeException("Unable to create output file " + fileName, ioe); 334 | } 335 | try { 336 | outStream.write(data); 337 | outStream.close(); 338 | } catch (IOException ioe) { 339 | throw new RuntimeException("failed writing data to file " + fileName, ioe); 340 | } 341 | } 342 | 343 | private void compressToJpeg(String fileName, Image image) { 344 | FileOutputStream outStream; 345 | try { 346 | outStream = new FileOutputStream(fileName); 347 | } catch (IOException ioe) { 348 | throw new RuntimeException("Unable to create output file " + fileName, ioe); 349 | } 350 | Rect rect = image.getCropRect(); 351 | YuvImage yuvImage = new YuvImage(getDataFromImage(image, COLOR_FormatNV21), ImageFormat.NV21, rect.width(), rect.height(), null); 352 | yuvImage.compressToJpeg(rect, 100, outStream); 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 17 | 23 |