SampleVideoEncoder: To encode AVC/HEVC stream with B-frames enabled
It uses MediaRecorder APIs to record B-frames enabled video from camera2
input and MediaCodec APIs to encode reference test vector using input surface.
Test: adb shell am start -n "com.android.media.samplevideoencoder/com.\
android.media.samplevideoencoder.MainActivity"
Bug: 176060167
Change-Id: If0bae193c49a923d642a9bbc4aa0bb0b49670264
diff --git a/media/tests/SampleVideoEncoder/app/Android.bp b/media/tests/SampleVideoEncoder/app/Android.bp
new file mode 100644
index 0000000..35fe0d8
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/Android.bp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+android_app {
+ name: "SampleVideoEncoder",
+
+ manifest: "src/main/AndroidManifest.xml",
+
+ srcs: ["src/**/*.java"],
+
+ sdk_version: "current",
+ min_sdk_version: "24", // N
+
+ resource_dirs: [
+ "src/main/res",
+ ],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.appcompat_appcompat",
+ "androidx-constraintlayout_constraintlayout",
+ ],
+
+ javacflags: [
+ "-Xlint:deprecation",
+ "-Xlint:unchecked",
+ ],
+}
diff --git a/media/tests/SampleVideoEncoder/app/build.gradle b/media/tests/SampleVideoEncoder/app/build.gradle
new file mode 100644
index 0000000..cc54981
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.2"
+
+ defaultConfig {
+ applicationId "com.android.media.samplevideoencoder"
+ minSdkVersion 24
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ testImplementation 'junit:junit:4.13.1'
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+}
\ No newline at end of file
diff --git a/media/tests/SampleVideoEncoder/app/src/main/AndroidManifest.xml b/media/tests/SampleVideoEncoder/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ed668bb
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.media.samplevideoencoder">
+
+ <uses-permission android:name="android.permission.CAMERA"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+
+ <application
+ android:configChanges="orientation"
+ android:screenOrientation="portrait"
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <activity android:name="com.android.media.samplevideoencoder.MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/AutoFitTextureView.java b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/AutoFitTextureView.java
new file mode 100644
index 0000000..a3ea4c7
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/AutoFitTextureView.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.media.samplevideoencoder;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.TextureView;
+
+public class AutoFitTextureView extends TextureView {
+
+ public AutoFitTextureView(Context context) {
+ this(context, null);
+ }
+
+ public AutoFitTextureView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setAspectRatio(int width, int height) {
+ if (width < 0 || height < 0) {
+ throw new IllegalArgumentException("Size cannot be negative.");
+ }
+ requestLayout();
+ }
+}
diff --git a/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MainActivity.java b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MainActivity.java
new file mode 100644
index 0000000..33e81bb
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MainActivity.java
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.media.samplevideoencoder;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+
+import android.Manifest;
+import android.app.Activity;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.graphics.SurfaceTexture;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.Surface;
+import android.view.View;
+import android.view.TextureView;
+import android.widget.Button;
+import android.widget.CheckBox;
+
+import java.io.File;
+import java.io.IOException;
+
+import android.util.Log;
+import android.util.Size;
+import android.widget.RadioGroup;
+import android.widget.Toast;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+
+public class MainActivity extends AppCompatActivity
+ implements View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = "SampleVideoEncoder";
+ private static final String[] RECORD_PERMISSIONS =
+ {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
+ private static final int REQUEST_RECORD_PERMISSIONS = 1;
+ private final Semaphore mCameraOpenCloseLock = new Semaphore(1);
+ private static final int VIDEO_BITRATE = 8000000 /* 8 Mbps */;
+ private static final int VIDEO_FRAMERATE = 30;
+
+ private String mMime = MediaFormat.MIMETYPE_VIDEO_AVC;
+ private String mOutputVideoPath = null;
+
+ private final boolean mIsFrontCamera = true;
+ private boolean mIsCodecSoftware = false;
+ private boolean mIsMediaRecorder = true;
+ private boolean mIsRecording;
+
+ private AutoFitTextureView mTextureView;
+ private CameraDevice mCameraDevice;
+ private CameraCaptureSession mPreviewSession;
+ private CaptureRequest.Builder mPreviewBuilder;
+ private MediaRecorder mMediaRecorder;
+ private Size mVideoSize;
+ private Size mPreviewSize;
+
+ private Handler mBackgroundHandler;
+ private HandlerThread mBackgroundThread;
+
+ private Button mStartButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ final RadioGroup radioGroup_mime = findViewById(R.id.radio_group_mime);
+ radioGroup_mime.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ if (checkedId == R.id.avc) {
+ mMime = MediaFormat.MIMETYPE_VIDEO_AVC;
+ } else {
+ mMime = MediaFormat.MIMETYPE_VIDEO_HEVC;
+ }
+ }
+ });
+
+ final RadioGroup radioGroup_codec = findViewById(R.id.radio_group_codec);
+ radioGroup_codec.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ mIsCodecSoftware = checkedId == R.id.sw;
+ }
+ });
+
+ final CheckBox checkBox_mr = findViewById(R.id.checkBox_media_recorder);
+ final CheckBox checkBox_mc = findViewById(R.id.checkBox_media_codec);
+ mTextureView = findViewById(R.id.texture);
+ checkBox_mr.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ boolean checked = ((CheckBox) v).isChecked();
+ if (checked) {
+ checkBox_mc.setChecked(false);
+ mIsMediaRecorder = TRUE;
+ for (int i = 0; i < radioGroup_codec.getChildCount(); i++) {
+ radioGroup_codec.getChildAt(i).setEnabled(false);
+ }
+ }
+ }
+ });
+ checkBox_mc.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ boolean checked = ((CheckBox) v).isChecked();
+ if (checked) {
+ checkBox_mr.setChecked(false);
+ mIsMediaRecorder = FALSE;
+ for (int i = 0; i < radioGroup_codec.getChildCount(); i++) {
+ radioGroup_codec.getChildAt(i).setEnabled(true);
+ }
+ }
+ }
+ });
+ mStartButton = findViewById(R.id.start_button);
+ mStartButton.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.start_button) {
+ if (mIsMediaRecorder) {
+ if (mIsRecording) {
+ stopRecordingVideo();
+ } else {
+ mStartButton.setEnabled(false);
+ startRecordingVideo();
+ }
+ } else {
+ mStartButton.setEnabled(false);
+ mOutputVideoPath = getVideoPath(MainActivity.this);
+ MediaCodecSurfaceAsync codecAsyncTask = new MediaCodecSurfaceAsync(this);
+ codecAsyncTask.execute(
+ "Encoding reference test vector with MediaCodec APIs using surface");
+ }
+ }
+ }
+
+ private static class MediaCodecSurfaceAsync extends AsyncTask<String, String, Integer> {
+
+ private final WeakReference<MainActivity> activityReference;
+
+ MediaCodecSurfaceAsync(MainActivity context) {
+ activityReference = new WeakReference<>(context);
+ }
+
+ @Override
+ protected Integer doInBackground(String... strings) {
+ MainActivity mainActivity = activityReference.get();
+ int resId = R.raw.crowd_1920x1080_25fps_4000kbps_h265;
+ int encodingStatus = 1;
+ MediaCodecSurfaceEncoder codecSurfaceEncoder =
+ new MediaCodecSurfaceEncoder(mainActivity.getApplicationContext(), resId,
+ mainActivity.mMime, mainActivity.mIsCodecSoftware,
+ mainActivity.mOutputVideoPath);
+ try {
+ encodingStatus = codecSurfaceEncoder.startEncodingSurface();
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ return encodingStatus;
+ }
+
+ @Override
+ protected void onPostExecute(Integer encodingStatus) {
+ MainActivity mainActivity = activityReference.get();
+ mainActivity.mStartButton.setEnabled(true);
+ if (encodingStatus == 0) {
+ Toast.makeText(mainActivity.getApplicationContext(), "Encoding Completed",
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(mainActivity.getApplicationContext(),
+ "Error occurred while " + "encoding", Toast.LENGTH_SHORT).show();
+ }
+ mainActivity.mOutputVideoPath = null;
+ super.onPostExecute(encodingStatus);
+ }
+ }
+
+ private final TextureView.SurfaceTextureListener mSurfaceTextureListener =
+ new TextureView.SurfaceTextureListener() {
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
+ int height) {
+ openCamera(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
+ int height) {
+ configureTransform(width, height);
+ Log.v(TAG, "Keeping camera preview size fixed");
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ }
+ };
+
+
+ private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
+
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ mCameraDevice = cameraDevice;
+ startPreview();
+ mCameraOpenCloseLock.release();
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int error) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ Activity activity = MainActivity.this;
+ activity.finish();
+ }
+ };
+
+ private boolean shouldShowRequestPermissionRationale(String[] recordPermissions) {
+ for (String permission : recordPermissions) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void requestRecordPermissions() {
+ if (!shouldShowRequestPermissionRationale(RECORD_PERMISSIONS)) {
+ ActivityCompat.requestPermissions(this, RECORD_PERMISSIONS, REQUEST_RECORD_PERMISSIONS);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ if (requestCode == REQUEST_RECORD_PERMISSIONS) {
+ if (grantResults.length == RECORD_PERMISSIONS.length) {
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Permission is not granted");
+ break;
+ }
+ }
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ @SuppressWarnings("MissingPermission")
+ private void openCamera(int width, int height) {
+ if (!hasPermissionGranted(RECORD_PERMISSIONS)) {
+ Log.e(TAG, "Camera does not have permission to record video");
+ requestRecordPermissions();
+ return;
+ }
+ final Activity activity = MainActivity.this;
+ if (activity == null || activity.isFinishing()) {
+ Log.e(TAG, "Activity not found");
+ return;
+ }
+ CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
+ try {
+ Log.v(TAG, "Acquire Camera");
+ if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
+ throw new RuntimeException("Timed out waiting to lock camera opening");
+ }
+ Log.d(TAG, "Camera Acquired");
+
+ String cameraId = manager.getCameraIdList()[0];
+ if (mIsFrontCamera) {
+ cameraId = manager.getCameraIdList()[1];
+ }
+
+ CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
+ StreamConfigurationMap map =
+ characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class));
+ mPreviewSize =
+ chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height,
+ mVideoSize);
+ mTextureView.setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth());
+ configureTransform(width, height);
+ mMediaRecorder = new MediaRecorder();
+ manager.openCamera(cameraId, mStateCallback, null);
+ } catch (InterruptedException | CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void closeCamera() {
+ try {
+ mCameraOpenCloseLock.acquire();
+ closePreviewSession();
+ if (null != mCameraDevice) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ if (null != mMediaRecorder) {
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera closing.");
+ } finally {
+ mCameraOpenCloseLock.release();
+ }
+ }
+
+ private static Size chooseVideoSize(Size[] choices) {
+ for (Size size : choices) {
+ if (size.getWidth() == size.getHeight() * 16 / 9 && size.getWidth() <= 1920) {
+ return size;
+ }
+ }
+ Log.e(TAG, "Couldn't find any suitable video size");
+ return choices[choices.length - 1];
+ }
+
+ private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
+ List<Size> bigEnough = new ArrayList<>();
+ int w = aspectRatio.getWidth();
+ int h = aspectRatio.getHeight();
+ for (Size option : choices) {
+ if (option.getHeight() == option.getWidth() * h / w && option.getWidth() >= width &&
+ option.getHeight() >= height) {
+ bigEnough.add(option);
+ }
+ }
+
+ // Pick the smallest of those, assuming we found any
+ if (bigEnough.size() > 0) {
+ return Collections.min(bigEnough, new CompareSizesByArea());
+ } else {
+ Log.e(TAG, "Couldn't find any suitable preview size");
+ return choices[0];
+ }
+ }
+
+ private boolean hasPermissionGranted(String[] recordPermissions) {
+ for (String permission : recordPermissions) {
+ if (ActivityCompat.checkSelfPermission(MainActivity.this, permission) !=
+ PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ startBackgroundThread();
+ if (mTextureView.isAvailable()) {
+ openCamera(mTextureView.getWidth(), mTextureView.getHeight());
+ } else {
+ mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ closeCamera();
+ stopBackgroundThread();
+ super.onPause();
+ }
+
+ private void startBackgroundThread() {
+ mBackgroundThread = new HandlerThread("CameraBackground");
+ mBackgroundThread.start();
+ mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+ }
+
+ private void stopBackgroundThread() {
+ mBackgroundThread.quitSafely();
+ try {
+ mBackgroundThread.join();
+ mBackgroundThread = null;
+ mBackgroundHandler = null;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void startRecordingVideo() {
+ if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
+ Toast.makeText(MainActivity.this, "Cannot start recording.", Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Cannot start recording.");
+ return;
+ }
+ try {
+ closePreviewSession();
+ setUpMediaRecorder();
+ SurfaceTexture texture = mTextureView.getSurfaceTexture();
+ assert texture != null;
+ texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
+ mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
+ List<Surface> surfaces = new ArrayList<>();
+
+ // Set up Surface for the camera preview
+ Surface previewSurface = new Surface(texture);
+ surfaces.add(previewSurface);
+ mPreviewBuilder.addTarget(previewSurface);
+
+ // Set up Surface for the MediaRecorder
+ Surface recorderSurface = mMediaRecorder.getSurface();
+ surfaces.add(recorderSurface);
+ mPreviewBuilder.addTarget(recorderSurface);
+
+ //Start a capture session
+ mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ mPreviewSession = session;
+ updatePreview();
+ MainActivity.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mIsRecording = true;
+ mMediaRecorder.start();
+ mStartButton.setText(R.string.stop);
+ mStartButton.setEnabled(true);
+ }
+ });
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ Log.e(TAG, "Failed to configure. Cannot start Recording");
+ }
+ }, mBackgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void setUpMediaRecorder() {
+ final Activity activity = MainActivity.this;
+ if (activity == null) {
+ Toast.makeText(MainActivity.this, "Error occurred while setting up the MediaRecorder",
+ Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Error occurred while setting up the MediaRecorder");
+ return;
+ }
+ try {
+ mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
+ mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ if (mOutputVideoPath == null) {
+ mOutputVideoPath = getVideoPath(MainActivity.this);
+ }
+ mMediaRecorder.setOutputFile(mOutputVideoPath);
+ mMediaRecorder.setVideoEncodingBitRate(VIDEO_BITRATE);
+ mMediaRecorder.setVideoFrameRate(VIDEO_FRAMERATE);
+ mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
+ mMediaRecorder.setOrientationHint(270);
+ if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
+ mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.HEVC);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mMediaRecorder.setVideoEncodingProfileLevel(
+ MediaCodecInfo.CodecProfileLevel.HEVCProfileMain,
+ MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel4);
+ }
+ } else {
+ mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mMediaRecorder.setVideoEncodingProfileLevel(
+ MediaCodecInfo.CodecProfileLevel.AVCProfileMain,
+ MediaCodecInfo.CodecProfileLevel.AVCLevel4);
+ }
+ }
+ mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+ try {
+ mMediaRecorder.prepare();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private String getVideoPath(Activity activity) {
+ File dir = activity.getApplicationContext().getExternalFilesDir(null);
+ if (dir == null) {
+ Log.e(TAG, "Cannot get external directory path to save output video");
+ return null;
+ }
+ String videoPath = dir.getAbsolutePath() + "/Video-" + System.currentTimeMillis() + ".mp4";
+ Log.d(TAG, "Output video is saved at: " + videoPath);
+ return videoPath;
+ }
+
+ private void closePreviewSession() {
+ if (mPreviewSession != null) {
+ mPreviewSession.close();
+ mPreviewSession = null;
+ }
+ }
+
+ private void stopRecordingVideo() {
+ mIsRecording = false;
+ mStartButton.setText(R.string.start);
+ mMediaRecorder.stop();
+ mMediaRecorder.reset();
+ Toast.makeText(MainActivity.this, "Recording Finished", Toast.LENGTH_SHORT).show();
+ mOutputVideoPath = null;
+ startPreview();
+ }
+
+ private void startPreview() {
+ if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
+ return;
+ }
+ try {
+ closePreviewSession();
+ SurfaceTexture texture = mTextureView.getSurfaceTexture();
+ assert texture != null;
+ texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
+ mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+
+ Surface previewSurface = new Surface(texture);
+ mPreviewBuilder.addTarget(previewSurface);
+
+ mCameraDevice.createCaptureSession(Collections.singletonList(previewSurface),
+ new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ mPreviewSession = session;
+ updatePreview();
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ Toast.makeText(MainActivity.this,
+ "Configure Failed; Cannot start " + "preview",
+ Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Configure failed; Cannot start preview");
+ }
+ }, mBackgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void updatePreview() {
+ if (mCameraDevice == null) {
+ Toast.makeText(MainActivity.this, "Camera not found; Cannot update " + "preview",
+ Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Camera not found; Cannot update preview");
+ return;
+ }
+ try {
+ mPreviewBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
+ HandlerThread thread = new HandlerThread("Camera preview");
+ thread.start();
+ mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void configureTransform(int viewWidth, int viewHeight) {
+ Activity activity = MainActivity.this;
+ if (null == mTextureView || null == mPreviewSize || null == activity) {
+ return;
+ }
+ Matrix matrix = new Matrix();
+ RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
+ RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());
+ float centerX = viewRect.centerX();
+ float centerY = viewRect.centerY();
+ bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
+ matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
+ float scale = Math.max((float) viewHeight / mPreviewSize.getHeight(),
+ (float) viewWidth / mPreviewSize.getWidth());
+ matrix.postScale(scale, scale, centerX, centerY);
+ mTextureView.setTransform(matrix);
+ }
+
+ static class CompareSizesByArea implements Comparator<Size> {
+ @Override
+ public int compare(Size lhs, Size rhs) {
+ return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
+ (long) rhs.getWidth() * rhs.getHeight());
+ }
+ }
+}
diff --git a/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MediaCodecBase.java b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MediaCodecBase.java
new file mode 100644
index 0000000..88ce73b
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MediaCodecBase.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.media.samplevideoencoder;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+class CodecAsyncHandler extends MediaCodec.Callback {
+ private static final String TAG = CodecAsyncHandler.class.getSimpleName();
+ private final Lock mLock = new ReentrantLock();
+ private final Condition mCondition = mLock.newCondition();
+ private final LinkedList<Pair<Integer, MediaCodec.BufferInfo>> mCbInputQueue;
+ private final LinkedList<Pair<Integer, MediaCodec.BufferInfo>> mCbOutputQueue;
+ private volatile boolean mSignalledError;
+
+ CodecAsyncHandler() {
+ mCbInputQueue = new LinkedList<>();
+ mCbOutputQueue = new LinkedList<>();
+ mSignalledError = false;
+ }
+
+ void clearQueues() {
+ mLock.lock();
+ mCbInputQueue.clear();
+ mCbOutputQueue.clear();
+ mLock.unlock();
+ }
+
+ void resetContext() {
+ clearQueues();
+ mSignalledError = false;
+ }
+
+ @Override
+ public void onInputBufferAvailable(MediaCodec codec, int bufferIndex) {
+ mLock.lock();
+ mCbInputQueue.add(new Pair<>(bufferIndex, (MediaCodec.BufferInfo) null));
+ mCondition.signalAll();
+ mLock.unlock();
+ }
+
+ @Override
+ public void onOutputBufferAvailable(MediaCodec codec, int bufferIndex,
+ MediaCodec.BufferInfo info) {
+ mLock.lock();
+ mCbOutputQueue.add(new Pair<>(bufferIndex, info));
+ mCondition.signalAll();
+ mLock.unlock();
+ }
+
+ @Override
+ public void onError(MediaCodec codec, MediaCodec.CodecException e) {
+ mLock.lock();
+ mSignalledError = true;
+ mCondition.signalAll();
+ mLock.unlock();
+ Log.e(TAG, "Received media codec error : " + e.getMessage());
+ }
+
+ @Override
+ public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
+ Log.i(TAG, "Output format changed: " + format.toString());
+ }
+
+ void setCallBack(MediaCodec codec, boolean isCodecInAsyncMode) {
+ if (isCodecInAsyncMode) {
+ codec.setCallback(this);
+ }
+ }
+
+ Pair<Integer, MediaCodec.BufferInfo> getOutput() throws InterruptedException {
+ Pair<Integer, MediaCodec.BufferInfo> element = null;
+ mLock.lock();
+ while (!mSignalledError) {
+ if (mCbOutputQueue.isEmpty()) {
+ mCondition.await();
+ } else {
+ element = mCbOutputQueue.remove(0);
+ break;
+ }
+ }
+ mLock.unlock();
+ return element;
+ }
+
+ Pair<Integer, MediaCodec.BufferInfo> getWork() throws InterruptedException {
+ Pair<Integer, MediaCodec.BufferInfo> element = null;
+ mLock.lock();
+ while (!mSignalledError) {
+ if (mCbInputQueue.isEmpty() && mCbOutputQueue.isEmpty()) {
+ mCondition.await();
+ } else {
+ if (!mCbOutputQueue.isEmpty()) {
+ element = mCbOutputQueue.remove(0);
+ break;
+ }
+ if (!mCbInputQueue.isEmpty()) {
+ element = mCbInputQueue.remove(0);
+ break;
+ }
+ }
+ }
+ mLock.unlock();
+ return element;
+ }
+
+ boolean hasSeenError() {
+ return mSignalledError;
+ }
+}
+
+abstract public class MediaCodecBase {
+ static ArrayList<String> selectCodecs(String mime, ArrayList<MediaFormat> formats,
+ String[] features, boolean isEncoder,
+ boolean isSoftware) {
+
+ MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+ MediaCodecInfo[] codecInfos = codecList.getCodecInfos();
+ ArrayList<String> listOfCodecs = new ArrayList<>();
+ for (MediaCodecInfo codecInfo : codecInfos) {
+ if (isEncoder) {
+ if (!codecInfo.isEncoder()) continue;
+ } else {
+ if (codecInfo.isEncoder()) continue;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && codecInfo.isAlias()) continue;
+ String[] types = codecInfo.getSupportedTypes();
+ for (String type : types) {
+ if (type.equalsIgnoreCase(mime)) {
+ boolean isOk = true;
+ MediaCodecInfo.CodecCapabilities codecCapabilities =
+ codecInfo.getCapabilitiesForType(type);
+ if (formats != null) {
+ for (MediaFormat format : formats) {
+ if (!codecCapabilities.isFormatSupported(format)) {
+ isOk = false;
+ break;
+ }
+ }
+ }
+ if (features != null) {
+ for (String feature : features) {
+ if (!codecCapabilities.isFeatureSupported(feature)) {
+ isOk = false;
+ break;
+ }
+ }
+ }
+ if (isSoftware) {
+ if (codecInfo.getName().contains("software") ||
+ codecInfo.getName().contains("android") ||
+ codecInfo.getName().contains("google")) {
+ if (isOk) listOfCodecs.add(codecInfo.getName());
+ }
+ } else {
+ if (codecInfo.getName().contains("software") ||
+ codecInfo.getName().contains("android") ||
+ codecInfo.getName().contains("google")) {
+ continue;
+ } else {
+ if (isOk) listOfCodecs.add(codecInfo.getName());
+ }
+ }
+ }
+ }
+ }
+ return listOfCodecs;
+ }
+}
diff --git a/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MediaCodecSurfaceEncoder.java b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MediaCodecSurfaceEncoder.java
new file mode 100644
index 0000000..146a475
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/java/com/android/media/samplevideoencoder/MediaCodecSurfaceEncoder.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.media.samplevideoencoder;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Build;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+public class MediaCodecSurfaceEncoder {
+ private static final String TAG = MediaCodecSurfaceEncoder.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+ private static final int VIDEO_BITRATE = 8000000 /*8 Mbps*/;
+ private static final int VIDEO_FRAMERATE = 30;
+ private final Context mActivityContext;
+ private final int mResID;
+ private final int mMaxBFrames;
+ private final String mMime;
+ private final String mOutputPath;
+ private int mTrackID = -1;
+
+ private Surface mSurface;
+ private MediaExtractor mExtractor;
+ private MediaCodec mDecoder;
+ private MediaCodec mEncoder;
+ private MediaMuxer mMuxer;
+
+ private final boolean mIsCodecSoftware;
+ private boolean mSawDecInputEOS;
+ private boolean mSawDecOutputEOS;
+ private boolean mSawEncOutputEOS;
+ private int mDecOutputCount;
+ private int mEncOutputCount;
+
+ private final CodecAsyncHandler mAsyncHandleEncoder = new CodecAsyncHandler();
+ private final CodecAsyncHandler mAsyncHandleDecoder = new CodecAsyncHandler();
+
+ public MediaCodecSurfaceEncoder(Context context, int resId, String mime, boolean isSoftware,
+ String outputPath, int maxBFrames) {
+ mActivityContext = context;
+ mResID = resId;
+ mMime = mime;
+ mIsCodecSoftware = isSoftware;
+ mOutputPath = outputPath;
+ mMaxBFrames = maxBFrames;
+ }
+
+ public MediaCodecSurfaceEncoder(Context context, int resId, String mime, boolean isSoftware,
+ String outputPath) {
+ // Default value of MediaFormat.KEY_MAX_B_FRAMES is set to 1, if not passed as a parameter.
+ this(context, resId, mime, isSoftware, outputPath, 1);
+ }
+
+ public int startEncodingSurface() throws IOException, InterruptedException {
+ MediaFormat decoderFormat = setUpSource();
+ if (decoderFormat == null) {
+ return -1;
+ }
+
+ String decoderMime = decoderFormat.getString(MediaFormat.KEY_MIME);
+ ArrayList<String> listOfDeocders =
+ MediaCodecBase.selectCodecs(decoderMime, null, null, false, mIsCodecSoftware);
+ if (listOfDeocders.isEmpty()) {
+ Log.e(TAG, "No suitable decoder found for mime: " + decoderMime);
+ return -1;
+ }
+ mDecoder = MediaCodec.createByCodecName(listOfDeocders.get(0));
+
+ MediaFormat encoderFormat = setUpEncoderFormat(decoderFormat);
+ ArrayList<String> listOfEncoders =
+ MediaCodecBase.selectCodecs(mMime, null, null, true, mIsCodecSoftware);
+ if (listOfEncoders.isEmpty()) {
+ Log.e(TAG, "No suitable encoder found for mime: " + mMime);
+ return -1;
+ }
+
+ boolean muxOutput = true;
+ for (String encoder : listOfEncoders) {
+ mEncoder = MediaCodec.createByCodecName(encoder);
+ mExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
+ if (muxOutput) {
+ int muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
+ mMuxer = new MediaMuxer(mOutputPath, muxerFormat);
+ }
+ configureCodec(decoderFormat, encoderFormat);
+ mEncoder.start();
+ mDecoder.start();
+ doWork(Integer.MAX_VALUE);
+ queueEOS();
+ waitForAllEncoderOutputs();
+ if (muxOutput) {
+ if (mTrackID != -1) {
+ mMuxer.stop();
+ mTrackID = -1;
+ }
+ if (mMuxer != null) {
+ mMuxer.release();
+ mMuxer = null;
+ }
+ }
+ mDecoder.reset();
+ mEncoder.reset();
+ mSurface.release();
+ mSurface = null;
+ }
+
+ mEncoder.release();
+ mDecoder.release();
+ mExtractor.release();
+ return 0;
+ }
+
+ private MediaFormat setUpSource() throws IOException {
+ mExtractor = new MediaExtractor();
+ AssetFileDescriptor fd = mActivityContext.getResources().openRawResourceFd(mResID);
+ mExtractor.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
+ for (int trackID = 0; trackID < mExtractor.getTrackCount(); trackID++) {
+ MediaFormat format = mExtractor.getTrackFormat(trackID);
+ String mime = format.getString(MediaFormat.KEY_MIME);
+ if (mime.startsWith("video/")) {
+ mExtractor.selectTrack(trackID);
+ format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
+ MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
+ return format;
+ }
+ }
+ mExtractor.release();
+ return null;
+ }
+
+ private MediaFormat setUpEncoderFormat(MediaFormat decoderFormat) {
+ MediaFormat encoderFormat = new MediaFormat();
+ encoderFormat.setString(MediaFormat.KEY_MIME, mMime);
+ encoderFormat
+ .setInteger(MediaFormat.KEY_WIDTH, decoderFormat.getInteger(MediaFormat.KEY_WIDTH));
+ encoderFormat.setInteger(MediaFormat.KEY_HEIGHT,
+ decoderFormat.getInteger(MediaFormat.KEY_HEIGHT));
+ encoderFormat.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_FRAMERATE);
+ encoderFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_BITRATE);
+ encoderFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, 1.0f);
+ encoderFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
+ MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
+ if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
+ encoderFormat.setInteger(MediaFormat.KEY_PROFILE,
+ MediaCodecInfo.CodecProfileLevel.HEVCProfileMain);
+ encoderFormat.setInteger(MediaFormat.KEY_LEVEL,
+ MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel4);
+ } else {
+ encoderFormat.setInteger(MediaFormat.KEY_PROFILE,
+ MediaCodecInfo.CodecProfileLevel.AVCProfileMain);
+ encoderFormat
+ .setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel4);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ encoderFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, mMaxBFrames);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ encoderFormat.setInteger(MediaFormat.KEY_LATENCY, 1);
+ }
+ return encoderFormat;
+ }
+
+ private void resetContext() {
+ mAsyncHandleDecoder.resetContext();
+ mAsyncHandleEncoder.resetContext();
+ mSawDecInputEOS = false;
+ mSawDecOutputEOS = false;
+ mSawEncOutputEOS = false;
+ mDecOutputCount = 0;
+ mEncOutputCount = 0;
+ }
+
+ private void configureCodec(MediaFormat decFormat, MediaFormat encFormat) {
+ resetContext();
+ mAsyncHandleEncoder.setCallBack(mEncoder, true);
+ mEncoder.configure(encFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ mSurface = mEncoder.createInputSurface();
+ if (!mSurface.isValid()) {
+ Log.e(TAG, "Surface is not valid");
+ return;
+ }
+ mAsyncHandleDecoder.setCallBack(mDecoder, true);
+ mDecoder.configure(decFormat, mSurface, null, 0);
+ Log.d(TAG, "Codec configured");
+ if (DEBUG) {
+ Log.d(TAG, "Encoder Output format: " + mEncoder.getOutputFormat());
+ }
+ }
+
+ private void dequeueDecoderOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+ if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ mSawDecOutputEOS = true;
+ }
+ if (DEBUG) {
+ Log.d(TAG,
+ "output: id: " + bufferIndex + " flags: " + info.flags + " size: " + info.size +
+ " timestamp: " + info.presentationTimeUs);
+ }
+ if (info.size > 0 && (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
+ mDecOutputCount++;
+ }
+ mDecoder.releaseOutputBuffer(bufferIndex, mSurface != null);
+ }
+
+ private void enqueueDecoderInput(int bufferIndex) {
+ ByteBuffer inputBuffer = mDecoder.getInputBuffer(bufferIndex);
+ int size = mExtractor.readSampleData(inputBuffer, 0);
+ if (size < 0) {
+ enqueueDecoderEOS(bufferIndex);
+ } else {
+ long pts = mExtractor.getSampleTime();
+ int extractorFlags = mExtractor.getSampleFlags();
+ int codecFlags = 0;
+ if ((extractorFlags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
+ codecFlags |= MediaCodec.BUFFER_FLAG_KEY_FRAME;
+ }
+ if ((extractorFlags & MediaExtractor.SAMPLE_FLAG_PARTIAL_FRAME) != 0) {
+ codecFlags |= MediaCodec.BUFFER_FLAG_PARTIAL_FRAME;
+ }
+ if (!mExtractor.advance()) {
+ codecFlags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ mSawDecInputEOS = true;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "input: id: " + bufferIndex + " size: " + size + " pts: " + pts +
+ " flags: " + codecFlags);
+ }
+ mDecoder.queueInputBuffer(bufferIndex, 0, size, pts, codecFlags);
+ }
+ }
+
+ private void doWork(int frameLimit) throws InterruptedException {
+ int frameCount = 0;
+ while (!hasSeenError() && !mSawDecInputEOS && frameCount < frameLimit) {
+ Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandleDecoder.getWork();
+ if (element != null) {
+ int bufferID = element.first;
+ MediaCodec.BufferInfo info = element.second;
+ if (info != null) {
+ // <id, info> corresponds to output callback.
+ dequeueDecoderOutput(bufferID, info);
+ } else {
+ // <id, null> corresponds to input callback.
+ enqueueDecoderInput(bufferID);
+ frameCount++;
+ }
+ }
+ // check decoder EOS
+ if (mSawDecOutputEOS) mEncoder.signalEndOfInputStream();
+ // encoder output
+ if (mDecOutputCount - mEncOutputCount > mMaxBFrames) {
+ tryEncoderOutput();
+ }
+ }
+ }
+
+ private void queueEOS() throws InterruptedException {
+ while (!mAsyncHandleDecoder.hasSeenError() && !mSawDecInputEOS) {
+ Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandleDecoder.getWork();
+ if (element != null) {
+ int bufferID = element.first;
+ MediaCodec.BufferInfo info = element.second;
+ if (info != null) {
+ dequeueDecoderOutput(bufferID, info);
+ } else {
+ enqueueDecoderEOS(element.first);
+ }
+ }
+ }
+
+ while (!hasSeenError() && !mSawDecOutputEOS) {
+ Pair<Integer, MediaCodec.BufferInfo> decOp = mAsyncHandleDecoder.getOutput();
+ if (decOp != null) dequeueDecoderOutput(decOp.first, decOp.second);
+ if (mSawDecOutputEOS) mEncoder.signalEndOfInputStream();
+ if (mDecOutputCount - mEncOutputCount > mMaxBFrames) {
+ tryEncoderOutput();
+ }
+ }
+ }
+
+ private void tryEncoderOutput() throws InterruptedException {
+ if (!hasSeenError() && !mSawEncOutputEOS) {
+ Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandleEncoder.getOutput();
+ if (element != null) {
+ dequeueEncoderOutput(element.first, element.second);
+ }
+ }
+ }
+
+ private void waitForAllEncoderOutputs() throws InterruptedException {
+ while (!hasSeenError() && !mSawEncOutputEOS) {
+ tryEncoderOutput();
+ }
+ }
+
+ private void enqueueDecoderEOS(int bufferIndex) {
+ if (!mSawDecInputEOS) {
+ mDecoder.queueInputBuffer(bufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ mSawDecInputEOS = true;
+ Log.d(TAG, "Queued End of Stream");
+ }
+ }
+
+ private void dequeueEncoderOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+ if (DEBUG) {
+ Log.d(TAG, "encoder output: id: " + bufferIndex + " flags: " + info.flags + " size: " +
+ info.size + " timestamp: " + info.presentationTimeUs);
+ }
+ if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ mSawEncOutputEOS = true;
+ }
+ if (info.size > 0) {
+ ByteBuffer buf = mEncoder.getOutputBuffer(bufferIndex);
+ if (mMuxer != null) {
+ if (mTrackID == -1) {
+ mTrackID = mMuxer.addTrack(mEncoder.getOutputFormat());
+ mMuxer.start();
+ }
+ mMuxer.writeSampleData(mTrackID, buf, info);
+ }
+ if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
+ mEncOutputCount++;
+ }
+ }
+ mEncoder.releaseOutputBuffer(bufferIndex, false);
+ }
+
+ private boolean hasSeenError() {
+ return mAsyncHandleDecoder.hasSeenError() || mAsyncHandleEncoder.hasSeenError();
+ }
+}
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/media/tests/SampleVideoEncoder/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/drawable/ic_launcher_background.xml b/media/tests/SampleVideoEncoder/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/layout/activity_main.xml b/media/tests/SampleVideoEncoder/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..164e02a
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ tools:context="com.android.media.samplevideoencoder.MainActivity">
+
+ <com.android.media.samplevideoencoder.AutoFitTextureView
+ android:id="@+id/texture"
+ android:layout_width="wrap_content"
+ android:layout_height="300dp"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginBottom="16dp"
+ android:gravity="center"
+ app:layout_constraintBottom_toTopOf="@+id/checkBox_media_recorder"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <CheckBox
+ android:id="@+id/checkBox_media_recorder"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="90dp"
+ android:layout_marginTop="10dp"
+ android:fontFamily="sans-serif-medium"
+ android:text="@string/media_recorder"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ android:textStyle="normal"
+ android:checked="true"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/texture"/>
+
+ <CheckBox
+ android:id="@+id/checkBox_media_codec"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="90dp"
+ android:fontFamily="sans-serif-medium"
+ android:text="@string/media_codec"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ android:textStyle="normal"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/checkBox_media_recorder" />
+
+ <RadioGroup
+ android:id="@+id/radio_group_mime"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="40dp"
+ android:layout_marginTop="10dp"
+ android:orientation="vertical"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/frameLayout2"
+ app:layout_constraintTop_toBottomOf="@+id/checkBox_media_codec">
+
+ <RadioButton
+ android:id="@+id/avc"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:checked="true"
+ android:text="@string/avc" />
+
+ <RadioButton
+ android:id="@+id/hevc"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/hevc" />
+ </RadioGroup>
+
+ <RadioGroup
+ android:id="@+id/radio_group_codec"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginEnd="40dp"
+ android:orientation="vertical"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/checkBox_media_codec">
+
+ <RadioButton
+ android:id="@+id/hw"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:checked="true"
+ android:enabled="false"
+ android:text="@string/hardware" />
+
+ <RadioButton
+ android:id="@+id/sw"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:enabled="false"
+ android:text="@string/software" />
+ </RadioGroup>
+
+ <FrameLayout
+ android:id="@+id/frameLayout2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/radio_group_mime"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentBottom="true"
+ android:layout_marginTop="10dp"
+ android:background="@color/colorPrimary"
+ app:layout_constraintTop_toBottomOf="@+id/radio_group_mime"
+ tools:layout_editor_absoluteX="80dp">
+
+ <Button
+ android:id="@+id/start_button"
+ android:layout_width="108dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:text="@string/start_button"
+ tools:layout_editor_absoluteX="155dp"
+ tools:layout_editor_absoluteY="455dp" />
+
+ </FrameLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/media/tests/SampleVideoEncoder/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/media/tests/SampleVideoEncoder/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/raw/crowd_1920x1080_25fps_4000kbps_h265.mp4 b/media/tests/SampleVideoEncoder/app/src/main/res/raw/crowd_1920x1080_25fps_4000kbps_h265.mp4
new file mode 100644
index 0000000..6204008
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/raw/crowd_1920x1080_25fps_4000kbps_h265.mp4
Binary files differ
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/values/colors.xml b/media/tests/SampleVideoEncoder/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..4faecfa
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#6200EE</color>
+ <color name="colorPrimaryDark">#3700B3</color>
+ <color name="colorAccent">#03DAC5</color>
+</resources>
\ No newline at end of file
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/values/strings.xml b/media/tests/SampleVideoEncoder/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..f825a7f
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+<resources>
+ <string name="app_name">SampleVideoEncoder</string>
+ <string name="media_recorder">MediaRecorder</string>
+ <string name="media_codec">MediaCodec</string>
+ <string name="start_button">Start</string>
+ <string name="stop">Stop</string>
+ <string name="start">Start</string>
+ <string name="avc">AVC</string>
+ <string name="hevc">HEVC</string>
+ <string name="hardware">H/W</string>
+ <string name="software">S/W</string>
+
+</resources>
\ No newline at end of file
diff --git a/media/tests/SampleVideoEncoder/app/src/main/res/values/styles.xml b/media/tests/SampleVideoEncoder/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..fac9291
--- /dev/null
+++ b/media/tests/SampleVideoEncoder/app/src/main/res/values/styles.xml
@@ -0,0 +1,10 @@
+<resources>
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
\ No newline at end of file