diff --git a/plugin.xml b/plugin.xml index c73a790..73944fe 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,75 +1,138 @@ - - CapturePlugin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $CAMERA_USAGE_DESCRIPTION - - - - $MICROPHONE_USAGE_DESCRIPTION - - - - $PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION - - + + + CapturePlugin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $CAMERA_USAGE_DESCRIPTION + + + + $MICROPHONE_USAGE_DESCRIPTION + + + + $PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION + + diff --git a/src/android/AudioRecorder.java b/src/android/AudioRecorder.java new file mode 100644 index 0000000..0090031 --- /dev/null +++ b/src/android/AudioRecorder.java @@ -0,0 +1,72 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.media.AudioFormat; +import android.media.AudioRecord; + +/** + * 音频录制 + * + */ +public class AudioRecorder extends Thread { + + private AudioRecord mAudioRecord = null; + /** 采样率 */ + private int mSampleRate = 44100; + private IMediaRecorder mMediaRecorder; + + public AudioRecorder(IMediaRecorder mediaRecorder) { + this.mMediaRecorder = mediaRecorder; + } + + /** 设置采样率 */ + public void setSampleRate(int sampleRate) { + this.mSampleRate = sampleRate; + } + + @Override + public void run() { + if (mSampleRate != 8000 && mSampleRate != 16000 && mSampleRate != 22050 && mSampleRate != 44100) { + mMediaRecorder.onAudioError(MediaRecorderBase.AUDIO_RECORD_ERROR_SAMPLERATE_NOT_SUPPORT, "sampleRate not support."); + return; + } + + final int mMinBufferSize = AudioRecord.getMinBufferSize(mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); + + if (AudioRecord.ERROR_BAD_VALUE == mMinBufferSize) { + mMediaRecorder.onAudioError(MediaRecorderBase.AUDIO_RECORD_ERROR_GET_MIN_BUFFER_SIZE_NOT_SUPPORT, "parameters are not supported by the hardware."); + return; + } + + mAudioRecord = new AudioRecord(android.media.MediaRecorder.AudioSource.MIC, mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, mMinBufferSize); + if (null == mAudioRecord) { + mMediaRecorder.onAudioError(MediaRecorderBase.AUDIO_RECORD_ERROR_CREATE_FAILED, "new AudioRecord failed."); + return; + } + try { + mAudioRecord.startRecording(); + } catch (IllegalStateException e) { + mMediaRecorder.onAudioError(MediaRecorderBase.AUDIO_RECORD_ERROR_UNKNOWN, "startRecording failed."); + return; + } + + byte[] sampleBuffer = new byte[2048]; + + try { + while (!Thread.currentThread().isInterrupted()) { + + int result = mAudioRecord.read(sampleBuffer, 0, 2048); + if (result > 0) { + mMediaRecorder.receiveAudioData(sampleBuffer, result); + } + } + } catch (Exception e) { + String message = ""; + if (e != null) + message = e.getMessage(); + mMediaRecorder.onAudioError(MediaRecorderBase.AUDIO_RECORD_ERROR_UNKNOWN, message); + } + + mAudioRecord.release(); + mAudioRecord = null; + } +} diff --git a/src/android/BaseMediaBitrateConfig.java b/src/android/BaseMediaBitrateConfig.java new file mode 100644 index 0000000..a70afbc --- /dev/null +++ b/src/android/BaseMediaBitrateConfig.java @@ -0,0 +1,132 @@ +package com.mabeijianxi.smallvideorecord2.model; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Created by jianxi on 2017/3/16. + * https://github.com/mabeijianxi + * mabeijianxi@gmail.com + */ + +public class BaseMediaBitrateConfig implements Parcelable{ + /** + * 码率模式{@link MODE} + */ + protected int mode=-1; + /** + * 固定码率值 + */ + protected int bitrate=-1; + /** + * 最大码率值 + */ + protected int maxBitrate=-1; + + protected int bufSize=-1; + /** + * 码率等级0~51,越大 + */ + protected int crfSize=-1; + /** + * {@link Velocity} 转码速度控制 + */ + protected String velocity; + + protected BaseMediaBitrateConfig(Parcel in) { + mode = in.readInt(); + bitrate = in.readInt(); + maxBitrate = in.readInt(); + bufSize = in.readInt(); + crfSize = in.readInt(); + velocity = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mode); + dest.writeInt(bitrate); + dest.writeInt(maxBitrate); + dest.writeInt(bufSize); + dest.writeInt(crfSize); + dest.writeString(velocity); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public BaseMediaBitrateConfig createFromParcel(Parcel in) { + return new BaseMediaBitrateConfig(in); + } + + @Override + public BaseMediaBitrateConfig[] newArray(int size) { + return new BaseMediaBitrateConfig[size]; + } + }; + + public int getBitrate() { + return bitrate; + } + + public int getMaxBitrate() { + return maxBitrate; + } + + public int getMode() { + return mode; + } + + public int getBufSize() { + return bufSize; + } + + public int getCrfSize() { + return crfSize; + } + public String getVelocity() { + return velocity; + } + + /** + * + * @param velocity 转码速度控制,速度越快体积将变大,质量也稍差一点点 {@link Velocity} + * @return + */ + public BaseMediaBitrateConfig setVelocity(String velocity) { + this.velocity=velocity; + return this; + } + + public static class MODE { + /** + * 默认模式 + */ + public final static int AUTO_VBR = 3; + /** + * 这个模式下可设置额定码率 + */ + public final static int VBR = 1; + /** + * 固定码率 + */ + public final static int CBR = 2; + } + + public static class Velocity { + public final static String ULTRAFAST="ultrafast"; + public final static String SUPERFAST="superfast"; + public final static String VERYFAST="veryfast"; + public final static String FASTER="faster"; + public final static String FAST="fast"; + public final static String MEDIUM="medium"; + public final static String SLOW="slow"; + public final static String SLOWER="slower"; + public final static String VERYSLOW="veryslow"; + public final static String PLACEBO="placebo"; + } +} diff --git a/src/android/CaptureCordovaPlugin.java b/src/android/CaptureCordovaPlugin.java index 58bc4af..abfc3b2 100644 --- a/src/android/CaptureCordovaPlugin.java +++ b/src/android/CaptureCordovaPlugin.java @@ -1,31 +1,198 @@ package cn.shuto.plugin.capture; +import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import com.mabeijianxi.smallvideorecord2.DeviceUtils; +import com.mabeijianxi.smallvideorecord2.JianXiCamera; +import com.mabeijianxi.smallvideorecord2.MediaRecorderActivity; +import com.mabeijianxi.smallvideorecord2.model.MediaRecorderConfig; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.util.Log; +import android.content.Intent; +import android.os.Bundle; +import org.apache.cordova.PermissionHelper; + +import java.io.File; + /** * This class echoes a string called from JavaScript. */ public class CaptureCordovaPlugin extends CordovaPlugin { public static final int REQUEST_CODE = 0x777578; + private final int PERMISSION_REQUEST_CODE = 0x001; + private static String LOG_TAG = "CAPTURE_PLUGIN"; private CallbackContext callbackContext; + private JSONObject param; + private String [] permissions = { + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + + @Override + protected void pluginInitialize() { + // 设置拍摄视频缓存路径 + File dcim = Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + if (DeviceUtils.isZte()) { + if (dcim.exists()) { + JianXiCamera.setVideoCachePath(dcim + "/capture/"); + } else { + JianXiCamera.setVideoCachePath(dcim.getPath().replace("/sdcard/", + "/sdcard-ext/") + + "/capture/"); + } + } else { + JianXiCamera.setVideoCachePath(dcim + "/capture/"); + } + // 初始化拍摄 + JianXiCamera.initialize(false, null); + } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { this.callbackContext = callbackContext; - JSONObject param = args.optJSONObject(0); + this.param = args.optJSONObject(0); if (action.equals("capture")) { - this.capture(param, callbackContext); + if(!hasPermisssion()) { + requestPermissions(PERMISSION_REQUEST_CODE); + } else { + this.capture(); + } return true; } return false; } - private void capture(JSONObject param, CallbackContext callbackContext) { + private void capture() { + JSONObject obj = this.param; + if (obj == null) { + obj = new JSONObject(); + } + boolean fullScreen = obj.optBoolean("needFull", true); + MediaRecorderConfig config = new MediaRecorderConfig.Buidler() + .fullScreen(fullScreen) + .smallVideoWidth(fullScreen?0:obj.optInt("width", 640)) + .smallVideoHeight(obj.optInt("height", 480)) + .recordTimeMax(obj.optInt("maxTime", 15000)) + .recordTimeMin(obj.optInt("minTime", 3000)) + .maxFrameRate(obj.optInt("maxFramerate", 24)) + .videoBitrate(obj.optInt("bitrate", 580000)) + .captureThumbnailsTime(obj.optInt("thumbnailsTime", 1)) + .build(); + Intent intentCapture = new Intent(this.cordova.getActivity().getBaseContext(), MediaRecorderActivity.class); + // intentCapture.putExtra(OVER_ACTIVITY_NAME, overGOActivityName); + intentCapture.putExtra(MediaRecorderActivity.MEDIA_RECORDER_CONFIG_KEY, config); + intentCapture.setPackage(this.cordova.getActivity().getApplicationContext().getPackageName()); + this.cordova.startActivityForResult(this, intentCapture, REQUEST_CODE); + } + + /** + * Called when the barcode scanner intent completes. + * + * @param requestCode The request code originally supplied to startActivityForResult(), + * allowing you to identify who this result came from. + * @param resultCode The integer result code returned by the child activity through its setResult(). + * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == REQUEST_CODE && this.callbackContext != null && intent != null) { + Bundle bundle = intent.getExtras(); + JSONObject obj = new JSONObject(); + super.onActivityResult(requestCode, resultCode, intent); + if(resultCode == Activity.RESULT_OK){ + try { + obj.put("directory", bundle.getString(MediaRecorderActivity.OUTPUT_DIRECTORY)); + obj.put("video", bundle.getString(MediaRecorderActivity.VIDEO_URI)); + obj.put("thumbnail", bundle.getString(MediaRecorderActivity.VIDEO_SCREENSHOT)); + obj.put("cancelled", false); + } catch (JSONException e) { + Log.d(LOG_TAG, "This should never happen"); + } + callbackContext.success(obj); + }else if (resultCode == Activity.RESULT_CANCELED){ + try { + obj.put("directory", ""); + obj.put("video", ""); + obj.put("thumbnail", ""); + obj.put("cancelled", true); + } catch (JSONException e) { + Log.d(LOG_TAG, "This should never happen"); + } + callbackContext.error(obj); + } + else { + this.callbackContext.error("Unexpected error"); + } + } + } + + /** + * check application's permissions + */ + public boolean hasPermisssion() { + for(String p : permissions) { + if(!PermissionHelper.hasPermission(this, p)) { + return false; + } + } + return true; + } + + /** + * We override this so that we can access the permissions variable, which no longer exists in + * the parent class, since we can't initialize it reliably in the constructor! + * + * @param requestCode The code to get request action + */ + public void requestPermissions(int requestCode) { + PermissionHelper.requestPermissions(this, requestCode, permissions); + } + + /** + * processes the result of permission request + * + * @param requestCode The code to get request action + * @param permissions The collection of permissions + * @param grantResults The result of grant + */ + public void onRequestPermissionResult(int requestCode, String[] permissions, + int[] grantResults) throws JSONException { + PluginResult result; + for (int r : grantResults) { + if (r == PackageManager.PERMISSION_DENIED) { + Log.d(LOG_TAG, "Permission Denied!"); + result = new PluginResult(PluginResult.Status.ILLEGAL_ACCESS_EXCEPTION); + this.callbackContext.sendPluginResult(result); + return; + } + } + + if (requestCode == PERMISSION_REQUEST_CODE) { + this.capture(); + } + } + + /** + * This plugin launches an external Activity when the camera is opened, so we + * need to implement the save/restore API in case the Activity gets killed + * by the OS while it's in the background. + */ + public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) { + this.callbackContext = callbackContext; } } diff --git a/src/android/DeviceUtils.java b/src/android/DeviceUtils.java new file mode 100644 index 0000000..c7d0d63 --- /dev/null +++ b/src/android/DeviceUtils.java @@ -0,0 +1,220 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.content.Context; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.Build; +import android.util.TypedValue; +import android.view.Display; +import android.view.WindowManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +/** + * 系统版本信息类 + * + */ +public class DeviceUtils { + + /** >=2.2 */ + public static boolean hasFroyo() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; + } + + /** >=2.3 */ + public static boolean hasGingerbread() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; + } + + /** >=3.0 LEVEL:11 */ + public static boolean hasHoneycomb() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + } + + /** >=3.1 */ + public static boolean hasHoneycombMR1() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1; + } + + /** >=4.0 14 */ + public static boolean hasICS() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; + } + + /** + * >= 4.1 16 + * + * @return + */ + public static boolean hasJellyBean() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } + + /** >= 4.2 17 */ + public static boolean hasJellyBeanMr1() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + } + + /** >= 4.3 18 */ + public static boolean hasJellyBeanMr2() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; + } + + /** >=4.4 19 */ + public static boolean hasKitkat() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + } + + public static int getSDKVersionInt() { + return Build.VERSION.SDK_INT; + } + + @SuppressWarnings("deprecation") + public static String getSDKVersion() { + return Build.VERSION.SDK; + } + + /** + * 获得设备的固件版本号 + */ + public static String getReleaseVersion() { + return StringUtils.makeSafe(Build.VERSION.RELEASE); + } + + /** 检测是否是中兴机器 */ + public static boolean isZte() { + return getDeviceModel().toLowerCase().indexOf("zte") != -1; + } + + /** 判断是否是三星的手机 */ + public static boolean isSamsung() { + return getManufacturer().toLowerCase().indexOf("samsung") != -1; + } + + /** 检测是否HTC手机 */ + public static boolean isHTC() { + return getManufacturer().toLowerCase().indexOf("htc") != -1; + } + + /** + * 检测当前设备是否是特定的设备 + * + * @param devices + * @return + */ + public static boolean isDevice(String... devices) { + String model = DeviceUtils.getDeviceModel(); + if (devices != null && model != null) { + for (String device : devices) { + if (model.indexOf(device) != -1) { + return true; + } + } + } + return false; + } + + /** + * 获得设备型号 + * + * @return + */ + public static String getDeviceModel() { + return StringUtils.trim(Build.MODEL); + } + + /** 获取厂商信息 */ + public static String getManufacturer() { + return StringUtils.trim(Build.MANUFACTURER); + } + + /** + * 判断是否是平板电脑 + * + * @param context + * @return + */ + public static boolean isTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } + + /** + * 检测是否是平板电脑 + * + * @param context + * @return + */ + public static boolean isHoneycombTablet(Context context) { + return hasHoneycomb() && isTablet(context); + } + + public static int dipToPX(final Context ctx, float dip) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, ctx.getResources().getDisplayMetrics()); + } + + /** + * 获取CPU的信息 + * + * @return + */ + public static String getCpuInfo() { + String cpuInfo = ""; + try { + if (new File("/proc/cpuinfo").exists()) { + FileReader fr = new FileReader("/proc/cpuinfo"); + BufferedReader localBufferedReader = new BufferedReader(fr, 8192); + cpuInfo = localBufferedReader.readLine(); + localBufferedReader.close(); + + if (cpuInfo != null) { + cpuInfo = cpuInfo.split(":")[1].trim().split(" ")[0]; + } + } + } catch (IOException e) { + } catch (Exception e) { + } + return cpuInfo; + } + + /** 判断是否支持闪光灯 */ + public static boolean isSupportCameraLedFlash(PackageManager pm) { + if (pm != null) { + FeatureInfo[] features = pm.getSystemAvailableFeatures(); + if (features != null) { + for (FeatureInfo f : features) { + if (f != null && PackageManager.FEATURE_CAMERA_FLASH.equals(f.name)) //判断设备是否支持闪光灯 + return true; + } + } + } + return false; + } + + /** 检测设备是否支持相机 */ + public static boolean isSupportCameraHardware(Context context) { + if (context != null && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) { + // this device has a camera + return true; + } else { + // no camera on this device + return false; + } + } + + /** 获取屏幕宽度 */ + @SuppressWarnings("deprecation") + public static int getScreenWidth(Context context) { + Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + return display.getWidth(); + } + + @SuppressWarnings("deprecation") + public static int getScreenHeight(Context context) { + Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + return display.getHeight(); + } +} diff --git a/src/android/FFMpegUtils.java b/src/android/FFMpegUtils.java new file mode 100644 index 0000000..247bc97 --- /dev/null +++ b/src/android/FFMpegUtils.java @@ -0,0 +1,49 @@ +package com.mabeijianxi.smallvideorecord2; + + +import com.mabeijianxi.smallvideorecord2.jniinterface.FFmpegBridge; + +/** + * ffmpeg工具类 + * + */ +public class FFMpegUtils { + + + public static boolean captureThumbnails(String videoPath, String outputPath, String ss) { + //ffmpeg -i /storage/emulated/0/DCIM/04.04.mp4 -s 84x84 -vframes 1 /storage/emulated/0/DCIM/Camera/miaopai/1388843007381.jpg + //ffmpeg -i eis-sample.mpg -s 40x40 -r 1/5 -vframes 10 %d.jpg +// FileUtils.deleteFile(outputPath); +// String cmd = String.format("ffmpeg -i %s %s -vframes 1 %s ", "/storage/emulated/0/DCIM/mabeijianxi/1496549287250/1496549287250.mp4", ss, "/storage/emulated/0/DCIM/mabeijianxi/1496549287250/1496549287250.jpg"); +// FFmpegBridge.jxFFmpegCMDRun(cmd); + return FFmpegBridge.jxFFmpegCMDRun(getCaptureThumbnailsCMD(videoPath,outputPath,ss))==0; + } +public static String getCaptureThumbnailsCMD(String videoPath, String outputPath, String ss){ + if (ss == null) + ss = ""; + else + ss = " -ss " + ss; + return String.format("ffmpeg -i %s %s -vframes 1 %s ", videoPath, ss, outputPath); +} + /** + * 视频截图 + * + * @param videoPath 视频路径 + * @param outputPath 截图输出路径 + * @param wh 截图画面尺寸,例如84x84 + * @param ss 截图起始时间 + * @return + */ + public static boolean captureThumbnails(String videoPath, String outputPath, String wh, String ss) { + //ffmpeg -i /storage/emulated/0/DCIM/04.04.mp4 -s 84x84 -vframes 1 /storage/emulated/0/DCIM/Camera/miaopai/1388843007381.jpg + //ffmpeg -i eis-sample.mpg -s 40x40 -r 1/5 -vframes 10 %d.jpg + FileUtils.deleteFile(outputPath); + if (ss == null) + ss = ""; + else + ss = " -ss " + ss; + String cmd = String.format("ffmpeg -d stdout -loglevel verbose -i \"%s\"%s -s %s -vframes 1 \"%s\"", videoPath, ss, wh, outputPath); + return FFmpegBridge.jxFFmpegCMDRun(cmd)==0 ; + } + +} diff --git a/src/android/FFmpegBridge.java b/src/android/FFmpegBridge.java new file mode 100644 index 0000000..22b42f6 --- /dev/null +++ b/src/android/FFmpegBridge.java @@ -0,0 +1,149 @@ +package com.mabeijianxi.smallvideorecord2.jniinterface; + +import java.util.ArrayList; + +/** + * Created by jianxi on 2017/5/12. + * https://github.com/mabeijianxi + * mabeijianxi@gmail.com + */ + +public class FFmpegBridge { + private static ArrayList listeners=new ArrayList(); + static { + System.loadLibrary("avutil"); + System.loadLibrary("fdk-aac"); + System.loadLibrary("avcodec"); + System.loadLibrary("avformat"); + System.loadLibrary("swscale"); + System.loadLibrary("swresample"); + System.loadLibrary("avfilter"); + System.loadLibrary("jx_ffmpeg_jni"); + } + + /** + * 结束录制并且转码保存完成 + */ + public static final int ALL_RECORD_END =1; + + + public final static int ROTATE_0_CROP_LF=0; + /** + * 旋转90度剪裁左上 + */ + public final static int ROTATE_90_CROP_LT =1; + /** + * 暂时没处理 + */ + public final static int ROTATE_180=2; + /** + * 旋转270(-90)裁剪左上,左右镜像 + */ + public final static int ROTATE_270_CROP_LT_MIRROR_LR=3; + + + /** + * + * @return 返回ffmpeg的编译信息 + */ + public static native String getFFmpegConfig(); + + /** + * 命令形式运行ffmpeg + * @param cmd + * @return 返回0表示成功 + */ + private static native int jxCMDRun(String cmd[]); + + /** + * 编码一帧视频,暂时只能编码yv12视频 + * @param data + * @return + */ + public static native int encodeFrame2H264(byte[] data); + + + /** + * 编码一帧音频,暂时只能编码pcm音频 + * @param data + * @return + */ + public static native int encodeFrame2AAC(byte[] data); + + /** + * 录制结束 + * @return + */ + public static native int recordEnd(); + + /** + * 初始化 + * @param debug + * @param logUrl + */ + public static native void initJXFFmpeg(boolean debug,String logUrl); + + + public static native void nativeRelease(); + + /** + * + * @param mediaBasePath 视频存放目录 + * @param mediaName 视频名称 + * @param filter 旋转镜像剪切处理 + * @param in_width 输入视频宽度 + * @param in_height 输入视频高度 + * @param out_height 输出视频高度 + * @param out_width 输出视频宽度 + * @param frameRate 视频帧率 + * @param bit_rate 视频比特率 + * @return + */ + public static native int prepareJXFFmpegEncoder(String mediaBasePath, String mediaName, int filter,int in_width, int in_height, int out_width, int out_height, int frameRate, long bit_rate); + + + /** + * 命令形式执行 + * @param cmd + */ + public static int jxFFmpegCMDRun(String cmd){ + String regulation="[ \\t]+"; + final String[] split = cmd.split(regulation); + + return jxCMDRun(split); + } + + /** + * 底层回调 + * @param state + * @param what + */ + public static synchronized void notifyState(int state,float what){ + for(FFmpegStateListener listener: listeners){ + if(listener!=null){ + if(state== ALL_RECORD_END){ + listener.allRecordEnd(); + } + } + } + } + + /** + *注册录制回调 + * @param listener + */ + public static void registFFmpegStateListener(FFmpegStateListener listener){ + + if(!listeners.contains(listener)){ + listeners.add(listener); + } + } + public static void unRegistFFmpegStateListener(FFmpegStateListener listener){ + if(listeners.contains(listener)){ + listeners.remove(listener); + } + } + public interface FFmpegStateListener { + void allRecordEnd(); + } +} diff --git a/src/android/FileUtils.java b/src/android/FileUtils.java new file mode 100644 index 0000000..3f2cb7e --- /dev/null +++ b/src/android/FileUtils.java @@ -0,0 +1,374 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.os.Environment; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.net.FileNameMap; +import java.net.URLConnection; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class FileUtils { + + /** + * 拼接路径 + * concatPath("/mnt/sdcard", "/DCIM/Camera") => /mnt/sdcard/DCIM/Camera + * concatPath("/mnt/sdcard", "DCIM/Camera") => /mnt/sdcard/DCIM/Camera + * concatPath("/mnt/sdcard/", "/DCIM/Camera") => /mnt/sdcard/DCIM/Camera + */ + public static String concatPath(String... paths) { + StringBuilder result = new StringBuilder(); + if (paths != null) { + for (String path : paths) { + if (path != null && path.length() > 0) { + int len = result.length(); + boolean suffixSeparator = len > 0 && result.charAt(len - 1) == File.separatorChar;//后缀是否是'/' + boolean prefixSeparator = path.charAt(0) == File.separatorChar;//前缀是否是'/' + if (suffixSeparator && prefixSeparator) { + result.append(path.substring(1)); + } else if (!suffixSeparator && !prefixSeparator) {//补前缀 + result.append(File.separatorChar); + result.append(path); + } else { + result.append(path); + } + } + } + } + return result.toString(); + } + + /** + * 计算文件的md5值 + */ + public static String calculateMD5(File updateFile) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + Log.e("FileUtils", "Exception while getting digest", e); + return null; + } + + InputStream is; + try { + is = new FileInputStream(updateFile); + } catch (FileNotFoundException e) { + Log.e("FileUtils", "Exception while getting FileInputStream", e); + return null; + } + + //DigestInputStream + + byte[] buffer = new byte[8192]; + int read; + try { + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + byte[] md5sum = digest.digest(); + BigInteger bigInt = new BigInteger(1, md5sum); + String output = bigInt.toString(16); + // Fill to 32 chars + output = String.format("%32s", output).replace(' ', '0'); + return output; + } catch (IOException e) { + throw new RuntimeException("Unable to process file for MD5", e); + } finally { + try { + is.close(); + } catch (IOException e) { + Log.e("FileUtils", "Exception on closing MD5 input stream", e); + } + } + } + + /** + * 计算文件的md5值 + */ + public static String calculateMD5(File updateFile, int offset, int partSize) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + Log.e("FileUtils", "Exception while getting digest", e); + return null; + } + + InputStream is; + try { + is = new FileInputStream(updateFile); + } catch (FileNotFoundException e) { + Log.e("FileUtils", "Exception while getting FileInputStream", e); + return null; + } + + //DigestInputStream + final int buffSize = 8192;//单块大小 + byte[] buffer = new byte[buffSize]; + int read; + try { + if (offset > 0) { + is.skip(offset); + } + int byteCount = Math.min(buffSize, partSize), byteLen = 0; + while ((read = is.read(buffer, 0, byteCount)) > 0 && byteLen < partSize) { + digest.update(buffer, 0, read); + byteLen += read; + //检测最后一块,避免多读数据 + if (byteLen + buffSize > partSize) { + byteCount = partSize - byteLen; + } + } + byte[] md5sum = digest.digest(); + BigInteger bigInt = new BigInteger(1, md5sum); + String output = bigInt.toString(16); + // Fill to 32 chars + output = String.format("%32s", output).replace(' ', '0'); + return output; + } catch (IOException e) { + throw new RuntimeException("Unable to process file for MD5", e); + } finally { + try { + is.close(); + } catch (IOException e) { + Log.e("FileUtils", "Exception on closing MD5 input stream", e); + } + } + } + + /** + * 检测文件是否可用 + */ + public static boolean checkFile(File f) { + if (f != null && f.exists() && f.canRead() && (f.isDirectory() || (f.isFile() && f.length() > 0))) { + return true; + } + return false; + } + + /** + * 检测文件是否可用 + */ + public static boolean checkFile(String path) { + if (StringUtils.isNotEmpty(path)) { + return checkFile(new File(path)); + } + return false; + } + + /** + * 获取sdcard路径 + */ + public static String getExternalStorageDirectory() { + String path = Environment.getExternalStorageDirectory().getPath(); + if (DeviceUtils.isZte()) { + // if (!Environment.getExternalStoragePublicDirectory( + // Environment.DIRECTORY_DCIM).exists()) { + path = path.replace("/sdcard", "/sdcard-ext"); + // } + } + return path; + } + + public static long getFileSize(String fn) { + File f = null; + long size = 0; + + try { + f = new File(fn); + size = f.length(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + f = null; + } + return size < 0 ? null : size; + } + + public static long getFileSize(File fn) { + return fn == null ? 0 : fn.length(); + } + + public static String getFileType(String fn, String defaultType) { + FileNameMap fNameMap = URLConnection.getFileNameMap(); + String type = fNameMap.getContentTypeFor(fn); + return type == null ? defaultType : type; + } + + public static String getFileType(String fn) { + return getFileType(fn, "application/octet-stream"); + } + + public static String getFileExtension(String filename) { + String extension = ""; + if (filename != null) { + int dotPos = filename.lastIndexOf("."); + if (dotPos >= 0 && dotPos < filename.length() - 1) { + extension = filename.substring(dotPos + 1); + } + } + return extension.toLowerCase(); + } + + public static boolean deleteFile(File f) { + if (f != null && f.exists() && !f.isDirectory()) { + return f.delete(); + } + return false; + } + + public static void deleteDir(File f) { + if (f != null && f.exists() && f.isDirectory()) { + for (File file : f.listFiles()) { + if (file.isDirectory()) + deleteDir(file); + file.delete(); + } + f.delete(); + } + } + + public static void deleteCacheFile(String f) { + if (f != null && f.length() > 0) { + File files = new File(f); + if (files.exists() && files.isDirectory()) { + for (File file : files.listFiles()) { + if (!file.isDirectory() && (file.getName().contains(".ts") || file.getName().contains("temp"))) { + file.delete(); + } + + } + } + } + } + public static void deleteCacheFile2TS(String f) { + if (f != null && f.length() > 0) { + File files = new File(f); + if (files.exists() && files.isDirectory()) { + for (File file : files.listFiles()) { + if (!file.isDirectory() && (file.getName().contains(".ts"))) { + file.delete(); + } + + } + } + } + } + public static void deleteDir(String f) { + if (f != null && f.length() > 0) { + deleteDir(new File(f)); + } + } + + public static boolean deleteFile(String f) { + if (f != null && f.length() > 0) { + return deleteFile(new File(f)); + } + return false; + } + + /** + * read file + * + * @param file + * @param charsetName The name of a supported {@link java.nio.charset.Charset + * charset} + * @return if file not exist, return null, else return content of file + * @throws RuntimeException if an error occurs while operator BufferedReader + */ + public static String readFile(File file, String charsetName) { + StringBuilder fileContent = new StringBuilder(""); + if (file == null || !file.isFile()) { + return fileContent.toString(); + } + + BufferedReader reader = null; + try { + InputStreamReader is = new InputStreamReader(new FileInputStream(file), charsetName); + reader = new BufferedReader(is); + String line = null; + while ((line = reader.readLine()) != null) { + if (!fileContent.toString().equals("")) { + fileContent.append("\r\n"); + } + fileContent.append(line); + } + reader.close(); + } catch (IOException e) { + throw new RuntimeException("IOException occurred. ", e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + throw new RuntimeException("IOException occurred. ", e); + } + } + } + return fileContent.toString(); + } + + public static String readFile(String filePath, String charsetName) { + return readFile(new File(filePath), charsetName); + } + + public static String readFile(File file) { + return readFile(file, "utf-8"); + } + + /** + * 文件拷贝 + * + * @param from + * @param to + * @return + */ + public static boolean fileCopy(String from, String to) { + boolean result = false; + + int size = 1 * 1024; + + FileInputStream in = null; + FileOutputStream out = null; + try { + in = new FileInputStream(from); + out = new FileOutputStream(to); + byte[] buffer = new byte[size]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + result = true; + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (in != null) { + in.close(); + } + } catch (IOException e) { + } + try { + if (out != null) { + out.close(); + } + } catch (IOException e) { + } + } + return result; + } +} diff --git a/src/android/IMediaRecorder.java b/src/android/IMediaRecorder.java new file mode 100644 index 0000000..9fa2355 --- /dev/null +++ b/src/android/IMediaRecorder.java @@ -0,0 +1,38 @@ +package com.mabeijianxi.smallvideorecord2; + + +import com.mabeijianxi.smallvideorecord2.model.MediaObject; + +/** + * 视频录制接口 + * + */ +public interface IMediaRecorder { + + /** + * 开始录制 + * + * @return 录制失败返回null + */ + public MediaObject.MediaPart startRecord(); + + /** + * 停止录制 + */ + public void stopRecord(); + + /** + * 音频错误 + * + * @param what 错误类型 + * @param message + */ + public void onAudioError(int what, String message); + /** + * 接收音频数据 + * + * @param sampleBuffer 音频数据 + * @param len + */ + public void receiveAudioData(byte[] sampleBuffer, int len); +} diff --git a/src/android/JianXiCamera.java b/src/android/JianXiCamera.java new file mode 100644 index 0000000..ec64346 --- /dev/null +++ b/src/android/JianXiCamera.java @@ -0,0 +1,56 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.text.TextUtils; + +import com.mabeijianxi.smallvideorecord2.jniinterface.FFmpegBridge; + +import java.io.File; + +/** + * Created by jianxi on 2017/6/5. + * https://github.com/mabeijianxi + * mabeijianxi@gmail.com + */ + +public class JianXiCamera { + /** 视频缓存路径 */ + private static String mVideoCachePath; + + /** 执行FFMPEG命令保存路径 */ + public final static String FFMPEG_LOG_FILENAME_TEMP = "jx_ffmpeg.log"; + + /** + * + * @param debug debug模式 + * @param logPath 命令日志存储地址 + */ + public static void initialize(boolean debug,String logPath) { + + if(debug&&TextUtils.isEmpty(logPath)){ + logPath=mVideoCachePath+"/"+FFMPEG_LOG_FILENAME_TEMP; + }else if(!debug){ + logPath=null; + } + FFmpegBridge.initJXFFmpeg(debug,logPath); + + } + + + + /** 获取视频缓存文件夹 */ + public static String getVideoCachePath() { + return mVideoCachePath; + } + + /** 设置视频缓存路径 */ + public static void setVideoCachePath(String path) { +// File file = new File(path); +// if (!file.exists()) { +// boolean created = file.mkdirs(); +// System.out.println("created: " + created); +// } + + mVideoCachePath = path; + + } +} diff --git a/src/android/Log.java b/src/android/Log.java new file mode 100644 index 0000000..74a3fcb --- /dev/null +++ b/src/android/Log.java @@ -0,0 +1,113 @@ +package com.mabeijianxi.smallvideorecord2; + +public class Log { + + private static boolean gIsLog = true; + private static final String TAG = "CAPTURE_PLUGIN"; + + public static void setLog(boolean isLog) { + Log.gIsLog = isLog; + } + + public static boolean getIsLog() { + return gIsLog; + } + + public static void d(String tag, String msg) { + if (gIsLog) { + android.util.Log.d(tag, msg); + } + } + + public static void d(String msg) { + if (gIsLog) { + android.util.Log.d(TAG, msg); + } + + } + + /** + * Send a {@link #DEBUG} log message and log the exception. + * + * @param tag + * Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg + * The message you would like logged. + * @param tr + * An exception to log + */ + public static void d(String tag, String msg, Throwable tr) { + if (gIsLog) { + android.util.Log.d(tag, msg, tr); + } + } + + public static void i(String tag, String msg) { + if (gIsLog) { + android.util.Log.i(tag, msg); + } + } + + /** + * Send a {@link #INFO} log message and log the exception. + * + * @param tag + * Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg + * The message you would like logged. + * @param tr + * An exception to log + */ + public static void i(String tag, String msg, Throwable tr) { + if (gIsLog) { + android.util.Log.i(tag, msg, tr); + } + + } + + /** + * Send an {@link #ERROR} log message. + * + * @param tag + * Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg + * The message you would like logged. + */ + public static void e(String tag, String msg) { + if (gIsLog) { + android.util.Log.e(tag, msg); + } + } + + public static void e(String msg) { + if (gIsLog) { + android.util.Log.e(TAG, msg); + } + } + + /** + * Send a {@link #ERROR} log message and log the exception. + * + * @param tag + * Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg + * The message you would like logged. + * @param tr + * An exception to log + */ + public static void e(String tag, String msg, Throwable tr) { + if (gIsLog) { + android.util.Log.e(tag, msg, tr); + } + } + + public static void e(String msg, Throwable tr) { + if (gIsLog) { + android.util.Log.e(TAG, msg, tr); + } + } +} diff --git a/src/android/MediaObject.java b/src/android/MediaObject.java new file mode 100644 index 0000000..aff3477 --- /dev/null +++ b/src/android/MediaObject.java @@ -0,0 +1,563 @@ +package com.mabeijianxi.smallvideorecord2.model; + +import com.mabeijianxi.smallvideorecord2.FileUtils; +import com.mabeijianxi.smallvideorecord2.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.util.LinkedList; + + +@SuppressWarnings("serial") +public class MediaObject implements Serializable { + + /** + * 拍摄 + */ + public final static int MEDIA_PART_TYPE_RECORD = 0; + /** + * 导入视频 + */ + public final static int MEDIA_PART_TYPE_IMPORT_VIDEO = 1; + /** + * 导入图片 + */ + public final static int MEDIA_PART_TYPE_IMPORT_IMAGE = 2; + /** + * 使用系统拍摄mp4 + */ + public final static int MEDIA_PART_TYPE_RECORD_MP4 = 3; + /** + * 默认最大时长 + */ + public final static int DEFAULT_MAX_DURATION = 10 * 1000; + /** + * 默认码率 + */ + public final static int DEFAULT_VIDEO_BITRATE = 800; + + /** + * 视频最大时长,默认10秒 + */ + private int mMaxDuration; + /** + * 视频目录 + */ + private String mOutputDirectory; + /** + * 对象文件 + */ + private String mOutputObjectPath; + /** + * 视频码率 + */ + private int mVideoBitrate; + /** + * 最终视频输出路径 + */ + private String mOutputVideoPath; + /** + * 最终视频截图输出路径 + */ + private String mOutputVideoThumbPath; + /** + * 文件夹、文件名 + */ + private String mKey; + /** + * 当前分块 + */ + private volatile transient MediaPart mCurrentPart; + /** + * 获取所有分块 + */ + private LinkedList mMediaList = new LinkedList(); + /** + * 主题 + */ + public MediaThemeObject mThemeObject; + private String outputTempVideoPath; + + public MediaObject(String key, String path) { + this(key, path, DEFAULT_VIDEO_BITRATE); + } + + public MediaObject(String key, String path, int videoBitrate) { + this.mKey = key; + this.mOutputDirectory = path; + this.mVideoBitrate = videoBitrate; + this.mOutputObjectPath = mOutputDirectory + File.separator + mKey + ".obj"; + this.mOutputVideoPath = mOutputDirectory + ".mp4"; + this.mOutputVideoThumbPath = mOutputDirectory + File.separator + mKey + ".jpg"; + this.mMaxDuration = DEFAULT_MAX_DURATION; + this.outputTempVideoPath = mOutputDirectory + File.separator + mKey + "_temp.mp4"; + } + + + public String getBaseName(){ + return mKey; + } + /** + * 获取视频码率 + */ + public int getVideoBitrate() { + return mVideoBitrate; + } + + /** + * 获取视频最大长度 + */ + public int getMaxDuration() { + return mMaxDuration; + } + + /** + * 设置最大时长,必须大于1秒 + */ + public void setMaxDuration(int duration) { + if (duration >= 1000) { + mMaxDuration = duration; + } + } + + /** + * 获取视频临时文件夹 + */ + public String getOutputDirectory() { + return mOutputDirectory; + } + + /** + * 获取视频临时输出播放 + */ + public String getOutputTempVideoPath() { + return outputTempVideoPath; + } + + public void setOutputTempVideoPath(String path) { + this.outputTempVideoPath = path; + } + + public String getOutputTempTranscodingVideoPath() { + return mOutputDirectory + + File.separator + mKey + ".mp4"; + } + + /** + * 清空主题 + */ + public void cleanTheme() { + mThemeObject = null; + if (mMediaList != null) { + for (MediaPart part : mMediaList) { + part.cutStartTime = 0; + part.cutEndTime = part.duration; + } + } + } + + /** + * 获取视频信息春促路径 + */ + public String getObjectFilePath() { + if (StringUtils.isEmpty(mOutputObjectPath)) { + File f = new File(mOutputVideoPath); + String obj = mOutputDirectory + File.separator + f.getName() + ".obj"; + mOutputObjectPath = obj; + } + return mOutputObjectPath; + } + + /** + * 获取视频最终输出地址 + */ + public String getOutputVideoPath() { + return mOutputVideoPath; + } + + /** + * 获取视频截图最终输出地址 + */ + public String getOutputVideoThumbPath() { + return mOutputVideoThumbPath; + } + + /** + * 获取录制的总时长 + */ + public int getDuration() { + int duration = 0; + if (mMediaList != null) { + for (MediaPart part : mMediaList) { + duration += part.getDuration(); + } + } + return duration; + } + + /** + * 获取剪切后的总时长 + */ + public int getCutDuration() { + int duration = 0; + if (mMediaList != null) { + for (MediaPart part : mMediaList) { + int cut = (part.cutEndTime - part.cutStartTime); + if (part.speed != 10) { + cut = (int) (cut * (10F / part.speed)); + } + duration += cut; + } + } + return duration; + } + + /** + * 删除分块 + */ + public void removePart(MediaPart part, boolean deleteFile) { + if (mMediaList != null) + mMediaList.remove(part); + + if (part != null) { + part.stop(); + // 删除文件 + if (deleteFile) { + part.delete(); + } + mMediaList.remove(part); + if (mCurrentPart != null && part.equals(mCurrentPart)) { + mCurrentPart = null; + } + } + } + + /** + * 生成分块信息,主要用于拍摄 + * + * @param cameraId 记录摄像头是前置还是后置 + * @return + */ + public MediaPart buildMediaPart(int cameraId) { + mCurrentPart = new MediaPart(); + mCurrentPart.position = getDuration(); + mCurrentPart.index = mMediaList.size(); + mCurrentPart.mediaPath = mOutputDirectory + File.separator + mCurrentPart.index + ".v"; + mCurrentPart.audioPath = mOutputDirectory + File.separator + mCurrentPart.index + ".a"; + mCurrentPart.thumbPath = mOutputDirectory + File.separator + mCurrentPart.index + ".jpg"; + mCurrentPart.cameraId = cameraId; + mCurrentPart.prepare(); + mCurrentPart.recording = true; + mCurrentPart.startTime = System.currentTimeMillis(); + mCurrentPart.type = MEDIA_PART_TYPE_IMPORT_VIDEO; + mMediaList.add(mCurrentPart); + return mCurrentPart; + } + + public MediaPart buildMediaPart(int cameraId, String videoSuffix) { + mCurrentPart = new MediaPart(); + mCurrentPart.position = getDuration(); + mCurrentPart.index = mMediaList.size(); + mCurrentPart.mediaPath = mOutputDirectory + File.separator + mCurrentPart.index + videoSuffix; + mCurrentPart.audioPath = mOutputDirectory + File.separator + mCurrentPart.index + ".a"; + mCurrentPart.thumbPath = mOutputDirectory + File.separator + mCurrentPart.index + ".jpg"; + mCurrentPart.recording = true; + mCurrentPart.cameraId = cameraId; + mCurrentPart.startTime = System.currentTimeMillis(); + mCurrentPart.type = MEDIA_PART_TYPE_IMPORT_VIDEO; + mMediaList.add(mCurrentPart); + return mCurrentPart; + } + + /** + * 生成分块信息,主要用于视频导入 + * + * @param path + * @param duration + * @param type + * @return + */ + public MediaPart buildMediaPart(String path, int duration, int type) { + mCurrentPart = new MediaPart(); + mCurrentPart.position = getDuration(); + mCurrentPart.index = mMediaList.size(); + mCurrentPart.mediaPath = mOutputDirectory + File.separator + mCurrentPart.index + ".v"; + mCurrentPart.audioPath = mOutputDirectory + File.separator + mCurrentPart.index + ".a"; + mCurrentPart.thumbPath = mOutputDirectory + File.separator + mCurrentPart.index + ".jpg"; + mCurrentPart.duration = duration; + mCurrentPart.startTime = 0; + mCurrentPart.endTime = duration; + mCurrentPart.cutStartTime = 0; + mCurrentPart.cutEndTime = duration; + mCurrentPart.tempPath = path; + mCurrentPart.type = type; + mMediaList.add(mCurrentPart); + return mCurrentPart; + } + + public String getConcatYUV() { + StringBuilder yuv = new StringBuilder(); + if (mMediaList != null && mMediaList.size() > 0) { + if (mMediaList.size() == 1) { + if (StringUtils.isEmpty(mMediaList.get(0).tempMediaPath)) + yuv.append(mMediaList.get(0).mediaPath); + else + yuv.append(mMediaList.get(0).tempMediaPath); + } else { + yuv.append("concat:"); + for (int i = 0, j = mMediaList.size(); i < j; i++) { + MediaPart part = mMediaList.get(i); + if (StringUtils.isEmpty(part.tempMediaPath)) + yuv.append(part.mediaPath); + else + yuv.append(part.tempMediaPath); + if (i + 1 < j) { + yuv.append("|"); + } + } + } + } + return yuv.toString(); + } + + public String getConcatPCM() { + StringBuilder yuv = new StringBuilder(); + if (mMediaList != null && mMediaList.size() > 0) { + if (mMediaList.size() == 1) { + if (StringUtils.isEmpty(mMediaList.get(0).tempAudioPath)) + yuv.append(mMediaList.get(0).audioPath); + else + yuv.append(mMediaList.get(0).tempAudioPath); + } else { + yuv.append("concat:"); + for (int i = 0, j = mMediaList.size(); i < j; i++) { + MediaPart part = mMediaList.get(i); + if (StringUtils.isEmpty(part.tempAudioPath)) + yuv.append(part.audioPath); + else + yuv.append(part.tempAudioPath); + if (i + 1 < j) { + yuv.append("|"); + } + } + } + } + return yuv.toString(); + } + + /** + * 获取当前分块 + */ + public MediaPart getCurrentPart() { + if (mCurrentPart != null) + return mCurrentPart; + if (mMediaList != null && mMediaList.size() > 0) + mCurrentPart = mMediaList.get(mMediaList.size() - 1); + return mCurrentPart; + } + + public int getCurrentIndex() { + MediaPart part = getCurrentPart(); + if (part != null) + return part.index; + return 0; + } + + public MediaPart getPart(int index) { + if (mCurrentPart != null && index < mMediaList.size()) + return mMediaList.get(index); + return null; + } + + /** + * 取消拍摄 + */ + public void delete() { + if (mMediaList != null) { + for (MediaPart part : mMediaList) { + part.stop(); + } + } + FileUtils.deleteDir(mOutputDirectory); + } + + public LinkedList getMedaParts() { + return mMediaList; + } + + /** + * 预处理数据对象 + */ + public static void preparedMediaObject(MediaObject mMediaObject) { + if (mMediaObject != null && mMediaObject.mMediaList != null) { + int duration = 0; + for (MediaPart part : mMediaObject.mMediaList) { + part.startTime = duration; + part.endTime = part.startTime + part.duration; + duration += part.duration; + } + } + } + + @Override + public String toString() { + StringBuffer result = new StringBuffer(); + if (mMediaList != null) { + result.append("[" + mMediaList.size() + "]"); + for (MediaPart part : mMediaList) { + result.append(part.mediaPath + ":" + part.duration + "\n"); + } + } + return result.toString(); + } + + public static class MediaPart implements Serializable { + + /** + * 索引 + */ + public int index; + /** + * 视频路径 + */ + public String mediaPath; + /** + * 音频路径 + */ + public String audioPath; + /** + * 临时视频路径 + */ + public String tempMediaPath; + /** + * 临时音频路径 + */ + public String tempAudioPath; + /** + * 截图路径 + */ + public String thumbPath; + /** + * 存放导入的视频和图片 + */ + public String tempPath; + /** + * 类型 + */ + public int type = MEDIA_PART_TYPE_RECORD; + /** + * 剪切视频(开始时间) + */ + public int cutStartTime; + /** + * 剪切视频(结束时间) + */ + public int cutEndTime; + /** + * 分段长度 + */ + public int duration; + /** + * 总时长中的具体位置 + */ + public int position; + /** + * 0.2倍速-3倍速(取值2~30) + */ + public int speed = 10; + /** + * 摄像头 + */ + public int cameraId; + /** + * 视频尺寸 + */ + public int yuvWidth; + /** + * 视频高度 + */ + public int yuvHeight; + public transient boolean remove; + public transient long startTime; + public transient long endTime; + public transient FileOutputStream mCurrentOutputVideo; + public transient FileOutputStream mCurrentOutputAudio; + public transient volatile boolean recording; + + public MediaPart() { + + } + + public void delete() { + FileUtils.deleteFile(mediaPath); + FileUtils.deleteFile(audioPath); + FileUtils.deleteFile(thumbPath); + FileUtils.deleteFile(tempMediaPath); + FileUtils.deleteFile(tempAudioPath); + } + + /** + * 写入音频数据 + */ + public void writeAudioData(byte[] buffer) throws IOException { + if (mCurrentOutputAudio != null) + mCurrentOutputAudio.write(buffer); + } + + /** + * 写入视频数据 + */ + public void writeVideoData(byte[] buffer) throws IOException { + if (mCurrentOutputVideo != null) + mCurrentOutputVideo.write(buffer); + } + + public void prepare() { + try { + mCurrentOutputVideo = new FileOutputStream(mediaPath); + } catch (IOException e) { + e.printStackTrace(); + } + prepareAudio(); + } + + public void prepareAudio() { + try { + mCurrentOutputAudio = new FileOutputStream(audioPath); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public int getDuration() { + return duration > 0 ? duration : (int) (System.currentTimeMillis() - startTime); + } + + public void stop() { + if (mCurrentOutputVideo != null) { + try { + mCurrentOutputVideo.flush(); + mCurrentOutputVideo.close(); + } catch (IOException e) { + e.printStackTrace(); + } + mCurrentOutputVideo = null; + } + + if (mCurrentOutputAudio != null) { + try { + mCurrentOutputAudio.flush(); + mCurrentOutputAudio.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + mCurrentOutputAudio = null; + } + } + + } + +} diff --git a/src/android/MediaRecorderActivity.java b/src/android/MediaRecorderActivity.java index 781aea4..6858686 100644 --- a/src/android/MediaRecorderActivity.java +++ b/src/android/MediaRecorderActivity.java @@ -27,9 +27,6 @@ import com.mabeijianxi.smallvideorecord2.model.MediaRecorderConfig; import java.io.File; -import static com.mabeijianxi.smallvideorecord2.R.id.bottom_layout; - - /** * 视频录制 */ @@ -114,18 +111,6 @@ public class MediaRecorderActivity extends Activity implements * 视屏截图地址 */ public final static String VIDEO_SCREENSHOT = "video_screenshot"; - /** - * 录制完成后需要跳转的activity - */ - public final static String OVER_ACTIVITY_NAME = "over_activity_name"; - /** - * 最大录制时间的key - */ - public final static String MEDIA_RECORDER_MAX_TIME_KEY = "media_recorder_max_time_key"; - /** - * 最小录制时间的key - */ - public final static String MEDIA_RECORDER_MIN_TIME_KEY = "media_recorder_min_time_key"; /** * 录制配置key */ @@ -136,14 +121,6 @@ public class MediaRecorderActivity extends Activity implements private boolean NEED_FULL_SCREEN = false; private RelativeLayout title_layout; - /** - * @param context - * @param overGOActivityName 录制结束后需要跳转的Activity全类名 - */ - public static void goSmallVideoRecorder(Activity context, String overGOActivityName, MediaRecorderConfig mediaRecorderConfig) { - context.startActivity(new Intent(context, MediaRecorderActivity.class).putExtra(OVER_ACTIVITY_NAME, overGOActivityName).putExtra(MEDIA_RECORDER_CONFIG_KEY, mediaRecorderConfig)); - } - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -171,28 +148,32 @@ public class MediaRecorderActivity extends Activity implements GO_HOME = mediaRecorderConfig.isGO_HOME(); } + private int getId(String idName,String type){ + return getResources().getIdentifier(idName, type, getPackageName()); + } + /** * 加载视图 */ private void loadViews() { - setContentView(R.layout.activity_media_recorder); + setContentView(getId("activity_media_recorder","layout")); // ~~~ 绑定控件 - mSurfaceView = (SurfaceView) findViewById(R.id.record_preview); - title_layout = (RelativeLayout) findViewById(R.id.title_layout); - mCameraSwitch = (CheckBox) findViewById(R.id.record_camera_switcher); - mTitleNext = (ImageView) findViewById(R.id.title_next); - mProgressView = (ProgressView) findViewById(R.id.record_progress); - mRecordDelete = (CheckedTextView) findViewById(R.id.record_delete); - mRecordController = (TextView) findViewById(R.id.record_controller); - mBottomLayout = (RelativeLayout) findViewById(bottom_layout); - mRecordLed = (CheckBox) findViewById(R.id.record_camera_led); + mSurfaceView = (SurfaceView) findViewById(getId("record_preview","id")); + title_layout = (RelativeLayout) findViewById(getId("title_layout","id")); + mCameraSwitch = (CheckBox) findViewById(getId("record_camera_switcher","id")); + mTitleNext = (ImageView) findViewById(getId("title_next","id")); + mProgressView = (ProgressView) findViewById(getId("record_progress","id")); + mRecordDelete = (CheckedTextView) findViewById(getId("record_delete","id")); + mRecordController = (TextView) findViewById(getId("record_controller","id")); + mBottomLayout = (RelativeLayout) findViewById(getId("bottom_layout","id")); + mRecordLed = (CheckBox) findViewById(getId("record_camera_led","id")); // ~~~ 绑定事件 /*if (DeviceUtils.hasICS()) mSurfaceView.setOnTouchListener(mOnSurfaveViewTouchListener);*/ mTitleNext.setOnClickListener(this); - findViewById(R.id.title_back).setOnClickListener(this); + findViewById(getId("title_back","id")).setOnClickListener(this); // mRecordDelete.setOnClickListener(this); mRecordController.setOnTouchListener(mOnVideoControllerTouchListener); @@ -222,12 +203,12 @@ public class MediaRecorderActivity extends Activity implements private void initSurfaceView() { if (NEED_FULL_SCREEN) { mBottomLayout.setBackgroundColor(0); - title_layout.setBackgroundColor(getResources().getColor(R.color.full_title_color)); + title_layout.setBackgroundColor(getResources().getColor(getId("full_title_color","color"))); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mSurfaceView .getLayoutParams(); lp.setMargins(0,0,0,0); mSurfaceView.setLayoutParams(lp); - mProgressView.setBackgroundColor(getResources().getColor(R.color.full_progress_color)); + mProgressView.setBackgroundColor(getResources().getColor(getId("full_progress_color","color"))); } else { final int w = DeviceUtils.getScreenWidth(this); ((RelativeLayout.LayoutParams) mBottomLayout.getLayoutParams()).topMargin = (int) (w / (MediaRecorderBase.SMALL_VIDEO_HEIGHT / (MediaRecorderBase.SMALL_VIDEO_WIDTH * 1.0f))); @@ -254,12 +235,16 @@ public class MediaRecorderActivity extends Activity implements File f = new File(JianXiCamera.getVideoCachePath()); if (!FileUtils.checkFile(f)) { - f.mkdirs(); + boolean result = f.mkdirs(); + Log.d("can" + (result ? " " : " *not* ") +"create " + JianXiCamera.getVideoCachePath()); } String key = String.valueOf(System.currentTimeMillis()); mMediaObject = mMediaRecorder.setOutputDirectory(key, JianXiCamera.getVideoCachePath() + key); mMediaRecorder.setSurfaceHolder(mSurfaceView.getHolder()); + int screenWidth = DeviceUtils.getScreenWidth(this); + int screenHegith = DeviceUtils.getScreenHeight(this); + mMediaRecorder.setScreen(screenWidth, screenHegith); mMediaRecorder.prepare(); } @@ -395,10 +380,10 @@ public class MediaRecorderActivity extends Activity implements if (mMediaObject != null && mMediaObject.getDuration() > 1) { // 未转码 new AlertDialog.Builder(this) - .setTitle(R.string.hint) - .setMessage(R.string.record_camera_exit_dialog_message) + .setTitle(getString(getId("hint","string"))) + .setMessage(getString(getId("record_camera_exit_dialog_message","string"))) .setNegativeButton( - R.string.record_camera_cancel_dialog_yes, + getString(getId("record_camera_cancel_dialog_yes","string")), new DialogInterface.OnClickListener() { @Override @@ -409,7 +394,7 @@ public class MediaRecorderActivity extends Activity implements } }) - .setPositiveButton(R.string.record_camera_cancel_dialog_no, + .setPositiveButton(getString(getId("record_camera_cancel_dialog_no","string")), null).setCancelable(false).show(); return; } @@ -450,7 +435,7 @@ public class MediaRecorderActivity extends Activity implements } // 处理开启回删后其他点击操作 - if (id != R.id.record_delete) { + if (id != getId("record_delete","id")) { if (mMediaObject != null) { MediaObject.MediaPart part = mMediaObject.getCurrentPart(); if (part != null) { @@ -464,9 +449,9 @@ public class MediaRecorderActivity extends Activity implements } } - if (id == R.id.title_back) { + if (id == getId("title_back","id")) { onBackPressed(); - } else if (id == R.id.record_camera_switcher) {// 前后摄像头切换 + } else if (id == getId("record_camera_switcher","id")) {// 前后摄像头切换 if (mRecordLed.isChecked()) { if (mMediaRecorder != null) { mMediaRecorder.toggleFlashMode(); @@ -483,7 +468,7 @@ public class MediaRecorderActivity extends Activity implements } else { mRecordLed.setEnabled(true); } - } else if (id == R.id.record_camera_led) {// 闪光灯 + } else if (id == getId("record_camera_led","id")) {// 闪光灯 // 开启前置摄像头以后不支持开启闪光灯 if (mMediaRecorder != null) { if (mMediaRecorder.isFrontCamera()) { @@ -494,12 +479,12 @@ public class MediaRecorderActivity extends Activity implements if (mMediaRecorder != null) { mMediaRecorder.toggleFlashMode(); } - } else if (id == R.id.title_next) {// 停止录制 + } else if (id == getId("title_next","id")) {// 停止录制 stopRecord(); /*finish(); overridePendingTransition(R.anim.push_bottom_in, R.anim.push_bottom_out);*/ - } else if (id == R.id.record_delete) { + } else if (id == getId("record_delete","id")) { // 取消回删 if (mMediaObject != null) { MediaObject.MediaPart part = mMediaObject.getCurrentPart(); @@ -594,7 +579,7 @@ public class MediaRecorderActivity extends Activity implements @Override public void onEncodeStart() { - showProgress("", getString(R.string.record_camera_progress_message)); + showProgress("", getString(getId("record_camera_progress_message","string"))); } @Override @@ -607,18 +592,13 @@ public class MediaRecorderActivity extends Activity implements @Override public void onEncodeComplete() { hideProgress(); - Intent intent = null; - try { - intent = new Intent(this, Class.forName(getIntent().getStringExtra(OVER_ACTIVITY_NAME))); - intent.putExtra(MediaRecorderActivity.OUTPUT_DIRECTORY, mMediaObject.getOutputDirectory()); - intent.putExtra(MediaRecorderActivity.VIDEO_URI, mMediaObject.getOutputTempTranscodingVideoPath()); - intent.putExtra(MediaRecorderActivity.VIDEO_SCREENSHOT, mMediaObject.getOutputVideoThumbPath()); - intent.putExtra("go_home", GO_HOME); - startActivity(intent); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("需要传入录制完成后跳转的Activity的全类名"); - } - + Bundle bundle = new Bundle(); + bundle.putString(MediaRecorderActivity.OUTPUT_DIRECTORY, mMediaObject.getOutputDirectory()); + bundle.putString(MediaRecorderActivity.VIDEO_URI, mMediaObject.getOutputTempTranscodingVideoPath()); + bundle.putString(MediaRecorderActivity.VIDEO_SCREENSHOT, mMediaObject.getOutputVideoThumbPath()); + Intent resultIntent = new Intent(); + resultIntent.putExtras(bundle); + this.setResult(RESULT_OK, resultIntent); finish(); } @@ -628,7 +608,7 @@ public class MediaRecorderActivity extends Activity implements @Override public void onEncodeError() { hideProgress(); - Toast.makeText(this, R.string.record_video_transcoding_faild, + Toast.makeText(this, getString(getId("record_video_transcoding_faild","string")), Toast.LENGTH_SHORT).show(); finish(); } diff --git a/src/android/MediaRecorderBase.java b/src/android/MediaRecorderBase.java new file mode 100644 index 0000000..4d2dad0 --- /dev/null +++ b/src/android/MediaRecorderBase.java @@ -0,0 +1,955 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.ImageFormat; +import android.hardware.Camera; +import android.hardware.Camera.Area; +import android.hardware.Camera.AutoFocusCallback; +import android.hardware.Camera.PreviewCallback; +import android.hardware.Camera.Size; +import android.os.Build; +import android.os.CountDownTimer; +import android.text.TextUtils; +import android.view.SurfaceHolder; +import android.view.SurfaceHolder.Callback; + +import com.mabeijianxi.smallvideorecord2.jniinterface.FFmpegBridge; +import com.mabeijianxi.smallvideorecord2.model.BaseMediaBitrateConfig; +import com.mabeijianxi.smallvideorecord2.model.MediaObject; +import com.mabeijianxi.smallvideorecord2.Log; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + + +/** + * 视频录制抽象类 + */ +public abstract class MediaRecorderBase implements Callback, PreviewCallback, IMediaRecorder { + public static boolean NEED_FULL_SCREEN = false; + /** + * 小视频高度 + */ + public static int SMALL_VIDEO_HEIGHT = 480; + /** + * 小视频宽度 + */ + public static int SMALL_VIDEO_WIDTH = 360; + + + /** + * 未知错误 + */ + public static final int MEDIA_ERROR_UNKNOWN = 1; + /** + * 预览画布设置错误 + */ + public static final int MEDIA_ERROR_CAMERA_SET_PREVIEW_DISPLAY = 101; + /** + * 预览错误 + */ + public static final int MEDIA_ERROR_CAMERA_PREVIEW = 102; + /** + * 自动对焦错误 + */ + public static final int MEDIA_ERROR_CAMERA_AUTO_FOCUS = 103; + + public static final int AUDIO_RECORD_ERROR_UNKNOWN = 0; + /** + * 采样率设置不支持 + */ + public static final int AUDIO_RECORD_ERROR_SAMPLERATE_NOT_SUPPORT = 1; + /** + * 最小缓存获取失败 + */ + public static final int AUDIO_RECORD_ERROR_GET_MIN_BUFFER_SIZE_NOT_SUPPORT = 2; + /** + * 创建AudioRecord失败 + */ + public static final int AUDIO_RECORD_ERROR_CREATE_FAILED = 3; + + /** + * 视频码率 1M + */ + public static final int VIDEO_BITRATE_NORMAL = 1024; + /** + * 视频码率 1.5M(默认) + */ + public static final int VIDEO_BITRATE_MEDIUM = 1536; + /** + * 视频码率 2M + */ + public static final int VIDEO_BITRATE_HIGH = 2048; + + /** + * 开始转码 + */ + protected static final int MESSAGE_ENCODE_START = 0; + /** + * 转码进度 + */ + protected static final int MESSAGE_ENCODE_PROGRESS = 1; + /** + * 转码完成 + */ + protected static final int MESSAGE_ENCODE_COMPLETE = 2; + /** + * 转码失败 + */ + protected static final int MESSAGE_ENCODE_ERROR = 3; + + /** + * 最大帧率 + */ + protected static int MAX_FRAME_RATE = 20; + /** + * 最小帧率 + */ + protected static int MIN_FRAME_RATE = 8; + + protected static int CAPTURE_THUMBNAILS_TIME = 1; + + + protected BaseMediaBitrateConfig compressConfig; + /** + * 摄像头对象 + */ + protected Camera camera; + /** + * 摄像头参数 + */ + protected Camera.Parameters mParameters = null; + /** + * 摄像头支持的预览尺寸集合 + */ + protected List mSupportedPreviewSizes; + /** + * 画布 + */ + protected SurfaceHolder mSurfaceHolder; + + /** + * 声音录制 + */ + protected AudioRecorder mAudioRecorder; + /** + * 拍摄存储对象 + */ + protected MediaObject mMediaObject; + + /** + * 转码监听器 + */ + protected OnEncodeListener mOnEncodeListener; + /** + * 录制错误监听 + */ + protected OnErrorListener mOnErrorListener; + /** + * 录制已经准备就绪的监听 + */ + protected OnPreparedListener mOnPreparedListener; + + /** + * 帧率 + */ + protected int mFrameRate = MAX_FRAME_RATE; + /** + * 摄像头类型(前置/后置),默认后置 + */ + protected int mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK; + /** + * 视频码率 + */ + protected static int mVideoBitrate; + + public static int mSupportedPreviewWidth = 0; + /** + * 状态标记 + */ + protected boolean mPrepared, mStartPreview, mSurfaceCreated; + /** + * 是否正在录制 + */ + protected volatile boolean mRecording; + /** + * PreviewFrame调用次数,测试用 + */ + protected volatile long mPreviewFrameCallCount = 0; + + private String mFrameRateCmd=""; + + private int screenWidth; + private int screenHeight; + + public MediaRecorderBase() { + + } + + public void setScreen(int screenWidth, int screenHeight) { + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + } + + /** + * 设置预览输出SurfaceHolder + * + * @param sh + */ + @SuppressWarnings("deprecation") + public void setSurfaceHolder(SurfaceHolder sh) { + if (sh != null) { + sh.addCallback(this); + if (!DeviceUtils.hasHoneycomb()) { + sh.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + } + } + public void setRecordState(boolean state){ + this.mRecording=state; + } + public boolean getRecordState(){ + return mRecording; + } + /** + * 设置转码监听 + */ + public void setOnEncodeListener(OnEncodeListener l) { + this.mOnEncodeListener = l; + } + + /** + * 设置预处理监听 + */ + public void setOnPreparedListener(OnPreparedListener l) { + mOnPreparedListener = l; + } + + /** + * 设置错误监听 + */ + public void setOnErrorListener(OnErrorListener l) { + mOnErrorListener = l; + } + + /** + * 是否前置摄像头 + */ + public boolean isFrontCamera() { + return mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT; + } + + /** + * 是否支持前置摄像头 + */ + @SuppressLint("NewApi") + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + public static boolean isSupportFrontCamera() { + if (!DeviceUtils.hasGingerbread()) { + return false; + } + int numberOfCameras = Camera.getNumberOfCameras(); + if (2 == numberOfCameras) { + return true; + } + return false; + } + + /** + * 切换前置/后置摄像头 + */ + public void switchCamera(int cameraFacingFront) { + switch (cameraFacingFront) { + case Camera.CameraInfo.CAMERA_FACING_FRONT: + case Camera.CameraInfo.CAMERA_FACING_BACK: + mCameraId = cameraFacingFront; + stopPreview(); + startPreview(); + break; + } + } + + /** + * 切换前置/后置摄像头 + */ + public void switchCamera() { + if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { + switchCamera(Camera.CameraInfo.CAMERA_FACING_FRONT); + } else { + switchCamera(Camera.CameraInfo.CAMERA_FACING_BACK); + } + } + + /** + * 自动对焦 + * + * @param cb + * @return + */ + public boolean autoFocus(AutoFocusCallback cb) { + if (camera != null) { + try { + camera.cancelAutoFocus(); + + if (mParameters != null) { + String mode = getAutoFocusMode(); + if (StringUtils.isNotEmpty(mode)) { + mParameters.setFocusMode(mode); + camera.setParameters(mParameters); + } + } + camera.autoFocus(cb); + return true; + } catch (Exception e) { + if (mOnErrorListener != null) { + mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_AUTO_FOCUS, 0); + } + if (e != null) { + Log.e("autoFocus", e); + } + } + } + return false; + } + + /** + * 连续自动对焦 + */ + private String getAutoFocusMode() { + if (mParameters != null) { + //持续对焦是指当场景发生变化时,相机会主动去调节焦距来达到被拍摄的物体始终是清晰的状态。 + List focusModes = mParameters.getSupportedFocusModes(); + if ((Build.MODEL.startsWith("GT-I950") || Build.MODEL.endsWith("SCH-I959") || Build.MODEL.endsWith("MEIZU MX3")) && isSupported(focusModes, "continuous-picture")) { + return "continuous-picture"; + } else if (isSupported(focusModes, "continuous-video")) { + return "continuous-video"; + } else if (isSupported(focusModes, "auto")) { + return "auto"; + } + } + return null; + } + + /** + * 手动对焦 + * + * @param focusAreas 对焦区域 + * @return + */ + @SuppressLint("NewApi") + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public boolean manualFocus(AutoFocusCallback cb, List focusAreas) { + if (camera != null && focusAreas != null && mParameters != null && DeviceUtils.hasICS()) { + try { + camera.cancelAutoFocus(); + // getMaxNumFocusAreas检测设备是否支持 + if (mParameters.getMaxNumFocusAreas() > 0) { + // mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO);// + // Macro(close-up) focus mode + mParameters.setFocusAreas(focusAreas); + } + + if (mParameters.getMaxNumMeteringAreas() > 0) + mParameters.setMeteringAreas(focusAreas); + + mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO); + camera.setParameters(mParameters); + camera.autoFocus(cb); + return true; + } catch (Exception e) { + if (mOnErrorListener != null) { + mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_AUTO_FOCUS, 0); + } + if (e != null) + Log.e("autoFocus", e); + } + } + return false; + } + + /** + * 切换闪关灯,默认关闭 + */ + public boolean toggleFlashMode() { + if (mParameters != null) { + try { + final String mode = mParameters.getFlashMode(); + if (TextUtils.isEmpty(mode) || Camera.Parameters.FLASH_MODE_OFF.equals(mode)) + setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); + else + setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + return true; + } catch (Exception e) { + Log.e("toggleFlashMode", e); + } + } + return false; + } + + /** + * 设置闪光灯 + * + * @param value + */ + private boolean setFlashMode(String value) { + if (mParameters != null && camera != null) { + try { + if (Camera.Parameters.FLASH_MODE_TORCH.equals(value) || Camera.Parameters.FLASH_MODE_OFF.equals(value)) { + mParameters.setFlashMode(value); + camera.setParameters(mParameters); + } + return true; + } catch (Exception e) { + Log.e("setFlashMode", e); + } + } + return false; + } + + /** + * 设置码率 + */ + public void setVideoBitRate(int bitRate) { + if (bitRate > 0) + mVideoBitrate = bitRate; + } + + /** + * 开始预览 + */ + public void prepare() { + mPrepared = true; + if (mSurfaceCreated) + startPreview(); + } + + /** + * 设置视频临时存储文件夹 + * + * @param key 视频输出的名称,同目录下唯一,一般取系统当前时间 + * @param path 文件夹路径 + * @return 录制信息对象 + */ + public MediaObject setOutputDirectory(String key, String path) { + if (StringUtils.isNotEmpty(path)) { + File f = new File(path); + if (f != null) { + if (f.exists()) { + //已经存在,删除 + if (f.isDirectory()) + FileUtils.deleteDir(f); + else + FileUtils.deleteFile(f); + } + + if (f.mkdirs()) { + mMediaObject = new MediaObject(key, path, mVideoBitrate); + } + } + } + return mMediaObject; + } + + /** + * 设置视频信息 + */ + public void setMediaObject(MediaObject mediaObject) { + this.mMediaObject = mediaObject; + } + + public void stopRecord() { + mRecording = false; + setStopDate(); + + + } + + public void setStopDate() { + // 判断数据是否处理完,处理完了关闭输出流 + if (mMediaObject != null) { + MediaObject.MediaPart part = mMediaObject.getCurrentPart(); + if (part != null && part.recording) { + part.recording = false; + part.endTime = System.currentTimeMillis(); + part.duration = (int) (part.endTime - part.startTime); + part.cutStartTime = 0; + part.cutEndTime = part.duration; + // 检测视频大小是否大于0,否则丢弃(注意有音频没视频的情况下音频也会丢弃) + // File videoFile = new File(part.mediaPath); + // if (videoFile != null && videoFile.length() < 1) { + // mMediaObject.removePart(part, true); + // } + } + } + } + + /** + * 停止所有块的写入 + */ + private void stopAllRecord() { + mRecording = false; + if (mMediaObject != null && mMediaObject.getMedaParts() != null) { + for (MediaObject.MediaPart part : mMediaObject.getMedaParts()) { + if (part != null && part.recording) { + part.recording = false; + part.endTime = System.currentTimeMillis(); + part.duration = (int) (part.endTime - part.startTime); + part.cutStartTime = 0; + part.cutEndTime = part.duration; + // 检测视频大小是否大于0,否则丢弃(注意有音频没视频的情况下音频也会丢弃) + File videoFile = new File(part.mediaPath); + if (videoFile != null && videoFile.length() < 1) { + mMediaObject.removePart(part, true); + } + } + } + } + } + + /** + * 检测是否支持指定特性 + */ + private boolean isSupported(List list, String key) { + return list != null && list.contains(key); + } + + /** + * 预处理一些拍摄参数 + * 注意:自动对焦参数cam_mode和cam-mode可能有些设备不支持,导致视频画面变形,需要判断一下,已知有"GT-N7100", "GT-I9308"会存在这个问题 + */ + @SuppressWarnings("deprecation") + protected void prepareCameraParaments() { + if (mParameters == null) + return; + List rates = mParameters.getSupportedPreviewFrameRates(); + if (rates != null) { + if (rates.contains(MAX_FRAME_RATE)) { + mFrameRate = MAX_FRAME_RATE; + } else { + boolean findFrame = false; + Collections.sort(rates); + for (int i = rates.size() - 1; i >= 0; i--) { + if (rates.get(i) <= MAX_FRAME_RATE) { + mFrameRate = rates.get(i); + findFrame = true; + break; + } + } + if (!findFrame) { + mFrameRate = rates.get(0); + } + } + } + + mParameters.setPreviewFrameRate(mFrameRate); + // mParameters.setPreviewFpsRange(15 * 1000, 20 * 1000); +// TODO 设置浏览尺寸 + boolean findWidth = false; + float ratio = screenHeight / screenWidth; + for (int i = mSupportedPreviewSizes.size() - 1; i >= 0; i--) { + Size size = mSupportedPreviewSizes.get(i); + Log.d("width: " + size.width + ", height: " + size.height + ", ratio: " + ((float)size.width / size.height) + ", target: " + ratio); + if (size.height >= SMALL_VIDEO_HEIGHT && ((float)size.width / size.height) == ratio) { + mSupportedPreviewWidth = size.width; + checkFullWidth(mSupportedPreviewWidth,SMALL_VIDEO_WIDTH); + if (NEED_FULL_SCREEN) { + SMALL_VIDEO_HEIGHT = size.height; + } + findWidth = true; + break; + } + } + if (!findWidth) { + Log.e(getClass().getSimpleName(), "传入高度不支持或未找到对应宽度,请按照要求重新设置,否则会出现一些严重问题"); + mSupportedPreviewWidth = 640; + checkFullWidth(640,360); + SMALL_VIDEO_HEIGHT = 480; + } + mParameters.setPreviewSize(mSupportedPreviewWidth, SMALL_VIDEO_HEIGHT); + + // 设置输出视频流尺寸,采样率 + mParameters.setPreviewFormat(ImageFormat.YV12); + + //设置自动连续对焦 + String mode = getAutoFocusMode(); + if (StringUtils.isNotEmpty(mode)) { + mParameters.setFocusMode(mode); + } + + //设置人像模式,用来拍摄人物相片,如证件照。数码相机会把光圈调到最大,做出浅景深的效果。而有些相机还会使用能够表现更强肤色效果的色调、对比度或柔化效果进行拍摄,以突出人像主体。 + // if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT && isSupported(mParameters.getSupportedSceneModes(), Camera.Parameters.SCENE_MODE_PORTRAIT)) + // mParameters.setSceneMode(Camera.Parameters.SCENE_MODE_PORTRAIT); + + if (isSupported(mParameters.getSupportedWhiteBalance(), "auto")) + mParameters.setWhiteBalance("auto"); + + //是否支持视频防抖 + if ("true".equals(mParameters.get("video-stabilization-supported"))) + mParameters.set("video-stabilization", "true"); + + // mParameters.set("recording-hint", "false"); + // + // mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + if (!DeviceUtils.isDevice("GT-N7100", "GT-I9308", "GT-I9300")) { + mParameters.set("cam_mode", 1); + mParameters.set("cam-mode", 1); + } + } + + private void checkFullWidth(int trueValue, int falseValue) { + if(NEED_FULL_SCREEN){ + SMALL_VIDEO_WIDTH=trueValue; + }else { + SMALL_VIDEO_WIDTH = falseValue; + } + } + + /** + * 开始预览 + */ + public void startPreview() { + if (mStartPreview || mSurfaceHolder == null || !mPrepared) + return; + else + mStartPreview = true; + + try { + + if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) + camera = Camera.open(); + else + camera = Camera.open(mCameraId); + camera.setDisplayOrientation(90); + try { + camera.setPreviewDisplay(mSurfaceHolder); + } catch (IOException e) { + if (mOnErrorListener != null) { + mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_SET_PREVIEW_DISPLAY, 0); + } + Log.e("setPreviewDisplay fail " + e.getMessage()); + } + + //设置摄像头参数 + mParameters = camera.getParameters(); + mSupportedPreviewSizes = mParameters.getSupportedPreviewSizes();// 获取支持的尺寸 + prepareCameraParaments(); + camera.setParameters(mParameters); + setPreviewCallback(); + camera.startPreview(); + + onStartPreviewSuccess(); + if (mOnPreparedListener != null) + mOnPreparedListener.onPrepared(); + } catch (Exception e) { + e.printStackTrace(); + if (mOnErrorListener != null) { + mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_PREVIEW, 0); + } + Log.e("startPreview fail :" + e.getMessage()); + } + } + + + /** + * 预览调用成功,子类可以做一些操作 + */ + protected void onStartPreviewSuccess() { + + } + + /** + * 设置回调 + */ + protected void setPreviewCallback() { + Size size = mParameters.getPreviewSize(); + if (size != null) { + int buffSize = size.width * size.height * 3/2; + try { + camera.addCallbackBuffer(new byte[buffSize]); + camera.addCallbackBuffer(new byte[buffSize]); + camera.addCallbackBuffer(new byte[buffSize]); + camera.setPreviewCallbackWithBuffer(this); + } catch (OutOfMemoryError e) { + Log.e("startPreview...setPreviewCallback...", e); + } + Log.d("startPreview...setPreviewCallbackWithBuffer...width:" + size.width + " height:" + size.height); + } else { + camera.setPreviewCallback(this); + } + } + + /** + * 停止预览 + */ + public void stopPreview() { + if (camera != null) { + try { + camera.stopPreview(); + camera.setPreviewCallback(null); + // camera.lock(); + camera.release(); + } catch (Exception e) { + Log.e("stopPreview..."); + } + camera = null; + } + mStartPreview = false; + } + + /** + * 释放资源 + */ + public void release() { + + FFmpegBridge.nativeRelease(); + stopAllRecord(); + // 停止视频预览 + stopPreview(); + // 停止音频录制 + if (mAudioRecorder != null) { + mAudioRecorder.interrupt(); + mAudioRecorder = null; + } + + mSurfaceHolder = null; + mPrepared = false; + mSurfaceCreated = false; + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + this.mSurfaceHolder = holder; + this.mSurfaceCreated = true; + if (mPrepared && !mStartPreview) + startPreview(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + this.mSurfaceHolder = holder; + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurfaceHolder = null; + mSurfaceCreated = false; + } + + @Override + public void onAudioError(int what, String message) { + if (mOnErrorListener != null) + mOnErrorListener.onAudioError(what, message); + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + camera.addCallbackBuffer(data); + } + + /** + * 测试PreviewFrame回调次数,时间1分钟 + */ + public void testPreviewFrameCallCount() { + new CountDownTimer(1 * 60 * 1000, 1000) { + + @Override + public void onTick(long millisUntilFinished) { + Log.e("[Vitamio Recorder]", "testFrameRate..." + mPreviewFrameCallCount); + mPreviewFrameCallCount = 0; + } + + @Override + public void onFinish() { + + } + + }.start(); + } + + /** + * 接收音频数据 + */ + @Override + public void receiveAudioData(byte[] sampleBuffer, int len) { + + } + + protected String getScaleWH(){ + + return ""; + } + + + /** + * 预处理监听 + */ + public interface OnPreparedListener { + /** + * 预处理完毕,可以开始录制了 + */ + void onPrepared(); + } + + /** + * 错误监听 + */ + public interface OnErrorListener { + /** + * 视频录制错误 + * + * @param what + * @param extra + */ + void onVideoError(int what, int extra); + + /** + * 音频录制错误 + * + * @param what + * @param message + */ + void onAudioError(int what, String message); + } + + /** + * 转码接口 + */ + public interface OnEncodeListener { + /** + * 开始转码 + */ + void onEncodeStart(); + + /** + * 转码进度 + */ + void onEncodeProgress(int progress); + + /** + * 转码完成 + */ + void onEncodeComplete(); + + /** + * 转码失败 + */ + void onEncodeError(); + } + + + protected Boolean doCompress(boolean mergeFlag) { + if (compressConfig != null) { + String vbr = " -vbr 4 "; + if (compressConfig != null && compressConfig.getMode() == BaseMediaBitrateConfig.MODE.CBR) { + vbr = ""; + } + String scaleWH = getScaleWH(); + if(!TextUtils.isEmpty(scaleWH)){ + scaleWH="-s "+scaleWH; + }else { + scaleWH=""; + } + String cmd_transcoding = String.format("ffmpeg -threads 16 -i %s -c:v libx264 %s %s %s -c:a libfdk_aac %s %s %s %s", + mMediaObject.getOutputTempVideoPath(), + getBitrateModeCommand(compressConfig, "", false), + getBitrateCrfSize(compressConfig, "-crf 28", false), + getBitrateVelocity(compressConfig, "-preset:v ultrafast", false), + vbr, + getFrameRateCmd(), + scaleWH, + mMediaObject.getOutputTempTranscodingVideoPath() + ); + boolean transcodingFlag = FFmpegBridge.jxFFmpegCMDRun( cmd_transcoding) == 0; + + boolean captureFlag = FFMpegUtils.captureThumbnails(mMediaObject.getOutputTempTranscodingVideoPath(), mMediaObject.getOutputVideoThumbPath(), String.valueOf(CAPTURE_THUMBNAILS_TIME)); + + FileUtils.deleteCacheFile(mMediaObject.getOutputDirectory()); + boolean result = mergeFlag && captureFlag && transcodingFlag; + + return result; + } else { + boolean captureFlag = FFMpegUtils.captureThumbnails(mMediaObject.getOutputTempVideoPath(), mMediaObject.getOutputVideoThumbPath(), String.valueOf(CAPTURE_THUMBNAILS_TIME)); + + FileUtils.deleteCacheFile2TS(mMediaObject.getOutputDirectory()); + boolean result = captureFlag && mergeFlag; + + return result; + + } + + } + + protected String getFrameRateCmd() { + return mFrameRateCmd; + } + + protected void setTranscodingFrameRate(int rate){ + this.mFrameRateCmd=String.format(" -r %d",rate); + } + + + + protected String getBitrateModeCommand(BaseMediaBitrateConfig config, String defualtCmd, boolean needSymbol) { + String add = ""; + if (TextUtils.isEmpty(defualtCmd)) { + defualtCmd = ""; + } + if (config != null) { + if (config.getMode() == BaseMediaBitrateConfig.MODE.VBR) { + if (needSymbol) { + add = String.format(" -x264opts \"bitrate=%d:vbv-maxrate=%d\" ", config.getBitrate(), config.getMaxBitrate()); + } else { + add = String.format(" -x264opts bitrate=%d:vbv-maxrate=%d ", config.getBitrate(), config.getMaxBitrate()); + } + return add; + } else if (config.getMode() == BaseMediaBitrateConfig.MODE.CBR) { + if (needSymbol) { + add = String.format(" -x264opts \"bitrate=%d:vbv-bufsize=%d:nal_hrd=cbr\" ", config.getBitrate(), config.getBufSize()); + } else { + add = String.format(" -x264opts bitrate=%d:vbv-bufsize=%d:nal_hrd=cbr ", config.getBitrate(), config.getBufSize()); + + } + return add; + + } + } + return defualtCmd; + } + + protected String getBitrateCrfSize(BaseMediaBitrateConfig config, String defualtCmd, boolean nendSymbol) { + if (TextUtils.isEmpty(defualtCmd)) { + defualtCmd = ""; + } + String add = ""; + if (config != null && config.getMode() == BaseMediaBitrateConfig.MODE.AUTO_VBR && config.getCrfSize() > 0) { + if (nendSymbol) { + add = String.format("-crf \"%d\" ", config.getCrfSize()); + } else { + add = String.format("-crf %d ", config.getCrfSize()); + } + } else { + return defualtCmd; + } + return add; + } + + protected String getBitrateVelocity(BaseMediaBitrateConfig config, String defualtCmd, boolean nendSymbol) { + if (TextUtils.isEmpty(defualtCmd)) { + defualtCmd = ""; + } + String add = ""; + if (config != null && !TextUtils.isEmpty(config.getVelocity())) { + if (nendSymbol) { + add = String.format("-preset \"%s\" ", config.getVelocity()); + } else { + add = String.format("-preset %s ", config.getVelocity()); + } + } else { + return defualtCmd; + } + return add; + } +} diff --git a/src/android/MediaRecorderNative.java b/src/android/MediaRecorderNative.java new file mode 100644 index 0000000..55c05c2 --- /dev/null +++ b/src/android/MediaRecorderNative.java @@ -0,0 +1,144 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.hardware.Camera; +import android.media.MediaRecorder; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.mabeijianxi.smallvideorecord2.jniinterface.FFmpegBridge; +import com.mabeijianxi.smallvideorecord2.model.MediaObject; + + +/** + * 视频录制:边录制边底层处理视频(旋转和裁剪) + */ +public class MediaRecorderNative extends MediaRecorderBase implements MediaRecorder.OnErrorListener, FFmpegBridge.FFmpegStateListener { + + public MediaRecorderNative() { + FFmpegBridge.registFFmpegStateListener(this); + } + + /** + * 视频后缀 + */ + private static final String VIDEO_SUFFIX = ".ts"; + + /** + * 开始录制 + */ + @Override + public MediaObject.MediaPart startRecord() { + int vCustomFormat; + if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { + vCustomFormat=FFmpegBridge.ROTATE_90_CROP_LT; + } else { + vCustomFormat=FFmpegBridge.ROTATE_270_CROP_LT_MIRROR_LR; + } + + FFmpegBridge.prepareJXFFmpegEncoder( mMediaObject.getOutputDirectory(), mMediaObject.getBaseName(),vCustomFormat, mSupportedPreviewWidth, SMALL_VIDEO_HEIGHT, SMALL_VIDEO_WIDTH, SMALL_VIDEO_HEIGHT, mFrameRate, mVideoBitrate); + + MediaObject.MediaPart result = null; + + if (mMediaObject != null) { + + result = mMediaObject.buildMediaPart(mCameraId, VIDEO_SUFFIX); + String cmd = String.format("filename = \"%s\"; ", result.mediaPath); + //如果需要定制非480x480的视频,可以启用以下代码,其他vf参数参考ffmpeg的文档: + + if (mAudioRecorder == null && result != null) { + mAudioRecorder = new AudioRecorder(this); + mAudioRecorder.start(); + } + mRecording = true; + + } + return result; + } + + /** + * 停止录制 + */ + @Override + public void stopRecord() { + + super.stopRecord(); + if (mOnEncodeListener != null) { + mOnEncodeListener.onEncodeStart(); + } + FFmpegBridge.recordEnd(); + } + + /** + * 数据回调 + */ + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + if (mRecording) { + FFmpegBridge.encodeFrame2H264(data); + mPreviewFrameCallCount++; + } + super.onPreviewFrame(data, camera); + } + + /** + * 预览成功,设置视频输入输出参数 + */ + @Override + protected void onStartPreviewSuccess() { +// if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { +// UtilityAdapter.RenderInputSettings(mSupportedPreviewWidth, SMALL_VIDEO_WIDTH, 0, UtilityAdapter.FLIPTYPE_NORMAL); +// } else { +// UtilityAdapter.RenderInputSettings(mSupportedPreviewWidth, SMALL_VIDEO_WIDTH, 180, UtilityAdapter.FLIPTYPE_HORIZONTAL); +// } +// UtilityAdapter.RenderOutputSettings(SMALL_VIDEO_WIDTH, SMALL_VIDEO_HEIGHT, mFrameRate, UtilityAdapter.OUTPUTFORMAT_YUV | UtilityAdapter.OUTPUTFORMAT_MASK_MP4/*| UtilityAdapter.OUTPUTFORMAT_MASK_HARDWARE_ACC*/); + } + + @Override + public void onError(MediaRecorder mr, int what, int extra) { + try { + if (mr != null) + mr.reset(); + } catch (IllegalStateException e) { + Log.w("jianxi", "stopRecord", e); + } catch (Exception e) { + Log.w("jianxi", "stopRecord", e); + } + if (mOnErrorListener != null) + mOnErrorListener.onVideoError(what, extra); + } + + /** + * 接收音频数据,传递到底层 + */ + @Override + public void receiveAudioData(byte[] sampleBuffer, int len) { + if (mRecording && len > 0) { + FFmpegBridge.encodeFrame2AAC(sampleBuffer); + } + } + + @Override + public void allRecordEnd() { + + final boolean captureFlag = FFMpegUtils.captureThumbnails(mMediaObject.getOutputTempTranscodingVideoPath(), mMediaObject.getOutputVideoThumbPath(), String.valueOf(CAPTURE_THUMBNAILS_TIME)); + + if(mOnEncodeListener!=null){ + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + if(captureFlag){ + mOnEncodeListener.onEncodeComplete(); + }else { + mOnEncodeListener.onEncodeError(); + } + } + },0); + + } + + } + public void activityStop(){ + FFmpegBridge.unRegistFFmpegStateListener(this); + } +} diff --git a/src/android/MediaThemeObject.java b/src/android/MediaThemeObject.java new file mode 100644 index 0000000..752eb5c --- /dev/null +++ b/src/android/MediaThemeObject.java @@ -0,0 +1,58 @@ +package com.mabeijianxi.smallvideorecord2.model; + +public class MediaThemeObject { + + /** MV主题 */ + public String mMVThemeName; + + /** 音乐 */ + public String mMusicThemeName; + + /** 水印 */ + public String mWatermarkThemeName; + + /** 滤镜 */ + public String mFilterThemeName; + + // ~~~ 变声 + /** 音频文件 */ + public String mSoundText; + /** 音频文件编号 */ + public String mSoundTextId; + /** 变声主题名称 */ + public String mSoundThemeName; + + // ~~~ 变速 + /** 变声主题名称 */ + public String mSpeedThemeName; + + // ~~~ 静音 + /** 主题静音 */ + public boolean mThemeMute; + /** 原声静音 */ + public boolean mOrgiMute; + + public MediaThemeObject() { + + } + + /** 检测是否是空主题,没有设置任何参数 */ + public boolean isEmpty() { + //非空主题 + if (!"Empty".equals(mMVThemeName)) { + return false; + } + //没有静音、没有音乐、没有水印、没有滤镜、没有变声、没有变速 + return !mOrgiMute && isEmpty(mMusicThemeName, mWatermarkThemeName, mFilterThemeName, mSoundThemeName, mSpeedThemeName); + } + + private boolean isEmpty(String... themes) { + for (String theme : themes) { + //非空 + if (!"Empty".equals(theme)) { + return false; + } + } + return true; + } +} diff --git a/src/android/ProgressView.java b/src/android/ProgressView.java new file mode 100644 index 0000000..f908438 --- /dev/null +++ b/src/android/ProgressView.java @@ -0,0 +1,242 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.View; +import android.content.res.Resources; + +import com.mabeijianxi.smallvideorecord2.model.MediaObject; + +import java.util.Iterator; + + +public class ProgressView extends View { + + /** 进度条 */ + private Paint mProgressPaint; + /** 闪 */ + private Paint mActivePaint; + /** 暂停/中断色块 */ + private Paint mPausePaint; + /** 回删 */ + private Paint mRemovePaint; + /** 三秒 */ + private Paint mThreePaint; + /** 超时 */ + private Paint mOverflowPaint; + private boolean mStop, mProgressChanged; + private boolean mActiveState; + private MediaObject mMediaObject; + /** 最长时长 */ + private int mMaxDuration, mVLineWidth; + private int mRecordTimeMin=1500; + + public ProgressView(Context paramContext) { + super(paramContext); + init(); + } + + public ProgressView(Context paramContext, AttributeSet paramAttributeSet) { + super(paramContext, paramAttributeSet); + init(); + } + + public ProgressView(Context paramContext, AttributeSet paramAttributeSet, + int paramInt) { + super(paramContext, paramAttributeSet, paramInt); + init(); + } + + private int getColor(String idName){ + Resources resources = getResources(); + String packageName = getContext().getPackageName(); + int id = resources.getIdentifier(idName, "color", packageName); + return resources.getColor(id); + } + + private void init() { + mProgressPaint = new Paint(); + mActivePaint = new Paint(); + mPausePaint = new Paint(); + mRemovePaint = new Paint(); + mThreePaint = new Paint(); + mOverflowPaint = new Paint(); + + mVLineWidth = DeviceUtils.dipToPX(getContext(), 1); + + setBackgroundColor(getColor("camera_bg")); + mProgressPaint.setColor(0xFF45C01A); + mProgressPaint.setStyle(Paint.Style.FILL); + + mActivePaint.setColor(getResources().getColor(android.R.color.white)); + mActivePaint.setStyle(Paint.Style.FILL); + + mPausePaint.setColor(getColor("camera_progress_split")); + mPausePaint.setStyle(Paint.Style.FILL); + + mRemovePaint.setColor(getColor("camera_progress_delete")); + mRemovePaint.setStyle(Paint.Style.FILL); + + mThreePaint.setColor(getColor("camera_progress_three")); + mThreePaint.setStyle(Paint.Style.FILL); + + mOverflowPaint.setColor(getColor("camera_progress_overflow")); + mOverflowPaint.setStyle(Paint.Style.FILL); + } + + /** 闪动 */ + private final static int HANDLER_INVALIDATE_ACTIVE = 0; + /** 录制中 */ + private final static int HANDLER_INVALIDATE_RECORDING = 1; + + private Handler mHandler = new Handler() { + @Override + public void dispatchMessage(Message msg) { + switch (msg.what) { + case HANDLER_INVALIDATE_ACTIVE: + invalidate(); + mActiveState = !mActiveState; + if (!mStop) + sendEmptyMessageDelayed(0, 300); + break; + case HANDLER_INVALIDATE_RECORDING: + invalidate(); + if (mProgressChanged) + sendEmptyMessageDelayed(0, 50); + break; + } + super.dispatchMessage(msg); + } + }; + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final int width = getMeasuredWidth(), height = getMeasuredHeight(); + int left = 0, right = 0, duration = 0; + if (mMediaObject != null && mMediaObject.getMedaParts() != null) { + + left = right = 0; + Iterator iterator = mMediaObject + .getMedaParts().iterator(); + boolean hasNext = iterator.hasNext(); + + // final int duration = vp.getDuration(); + int maxDuration = mMaxDuration; + boolean hasOutDuration = false; + int currentDuration = mMediaObject.getDuration(); + hasOutDuration = currentDuration > mMaxDuration; + if (hasOutDuration) + maxDuration = currentDuration; + + while (hasNext) { + MediaObject.MediaPart vp = iterator.next(); + final int partDuration = vp.getDuration(); + // Logger.e("[ProgressView]partDuration" + partDuration + + // " maxDuration:" + maxDuration); + left = right; + right = left + + (int) (partDuration * 1.0F / maxDuration * width); + + if (vp.remove) { + // 回删 + canvas.drawRect(left, 0.0F, right, height, mRemovePaint); + } else { + // 画进度 + if (hasOutDuration) { + // 超时拍摄 + // 前段 + right = left + + (int) ((mMaxDuration - duration) * 1.0F + / maxDuration * width); + canvas.drawRect(left, 0.0F, right, height, + mProgressPaint); + + // 超出的段 + left = right; + right = left + + (int) ((partDuration - (mMaxDuration - duration)) + * 1.0F / maxDuration * width); + canvas.drawRect(left, 0.0F, right, height, + mOverflowPaint); + } else { + canvas.drawRect(left, 0.0F, right, height, + mProgressPaint); + } + } + + hasNext = iterator.hasNext(); + if (hasNext) { + // left = right - mVLineWidth; + canvas.drawRect(right - mVLineWidth, 0.0F, right, height, + mPausePaint); + } + + duration += partDuration; + // progress = vp.progress; + } + } + + // 画三秒 + if (duration < mRecordTimeMin) { + left = (int) ((mRecordTimeMin*1.0f )/ mMaxDuration * width); + canvas.drawRect(left, 0.0F, left + mVLineWidth, height, mThreePaint); + } + + // 删 + // + // 闪 + if (mActiveState) { + if (right + 8 >= width) + right = width - 8; + canvas.drawRect(right, 0.0F, right + 8, getMeasuredHeight(), + mActivePaint); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mStop = false; + mHandler.sendEmptyMessage(HANDLER_INVALIDATE_ACTIVE); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mStop = true; + mHandler.removeMessages(HANDLER_INVALIDATE_ACTIVE); + } + + // public void addProgress(MediaPart part) { + // if (part != null) { + // part.index = mVideoParts.size(); + // mVideoParts.add(part); + // } + // } + + public void setData(MediaObject mMediaObject) { + this.mMediaObject = mMediaObject; + } + + public void setMaxDuration(int duration) { + this.mMaxDuration = duration; + } + + public void start() { + mProgressChanged = true; + } + + public void stop() { + mProgressChanged = false; + } + + public void setMinTime(int recordTimeMin) { + this.mRecordTimeMin=recordTimeMin; + } +} diff --git a/src/android/StringUtils.java b/src/android/StringUtils.java new file mode 100644 index 0000000..aca8ab5 --- /dev/null +++ b/src/android/StringUtils.java @@ -0,0 +1,318 @@ +package com.mabeijianxi.smallvideorecord2; + +import android.text.TextPaint; +import android.text.TextUtils; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Iterator; +import java.util.TimeZone; + +/** + * 字符串工具类 + * + */ +public class StringUtils { + + public static final String EMPTY = ""; + + private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd"; + private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd hh:mm:ss"; + /** 用于生成文件 */ + private static final String DEFAULT_FILE_PATTERN = "yyyy-MM-dd-HH-mm-ss"; + private static final double KB = 1024.0; + private static final double MB = 1048576.0; + private static final double GB = 1073741824.0; + public static final SimpleDateFormat DATE_FORMAT_PART = new SimpleDateFormat( + "HH:mm"); + + public static String currentTimeString() { + return DATE_FORMAT_PART.format(Calendar.getInstance().getTime()); + } + + public static char chatAt(String pinyin, int index) { + if (pinyin != null && pinyin.length() > 0) + return pinyin.charAt(index); + return ' '; + } + + /** 获取字符串宽度 */ + public static float GetTextWidth(String Sentence, float Size) { + if (isEmpty(Sentence)) + return 0; + TextPaint FontPaint = new TextPaint(); + FontPaint.setTextSize(Size); + return FontPaint.measureText(Sentence.trim()) + (int) (Size * 0.1); // 留点余地 + } + + /** + * 格式化日期字符串 + * + * @param date + * @param pattern + * @return + */ + public static String formatDate(Date date, String pattern) { + SimpleDateFormat format = new SimpleDateFormat(pattern); + return format.format(date); + } + + public static String formatDate(long date, String pattern) { + SimpleDateFormat format = new SimpleDateFormat(pattern); + return format.format(new Date(date)); + } + + /** + * 格式化日期字符串 + * + * @param date + * @return 例如2011-3-24 + */ + public static String formatDate(Date date) { + return formatDate(date, DEFAULT_DATE_PATTERN); + } + + public static String formatDate(long date) { + return formatDate(new Date(date), DEFAULT_DATE_PATTERN); + } + + /** + * 获取当前时间 格式为yyyy-MM-dd 例如2011-07-08 + * + * @return + */ + public static String getDate() { + return formatDate(new Date(), DEFAULT_DATE_PATTERN); + } + + /** 生成一个文件名,不含后缀 */ + public static String createFileName() { + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat format = new SimpleDateFormat(DEFAULT_FILE_PATTERN); + return format.format(date); + } + + /** + * 获取当前时间 + * + * @return + */ + public static String getDateTime() { + return formatDate(new Date(), DEFAULT_DATETIME_PATTERN); + } + + /** + * 格式化日期时间字符串 + * + * @param date + * @return 例如2011-11-30 16:06:54 + */ + public static String formatDateTime(Date date) { + return formatDate(date, DEFAULT_DATETIME_PATTERN); + } + + public static String formatDateTime(long date) { + return formatDate(new Date(date), DEFAULT_DATETIME_PATTERN); + } + + /** + * 格林威时间转换 + * + * @param gmt + * @return + */ + public static String formatGMTDate(String gmt) { + TimeZone timeZoneLondon = TimeZone.getTimeZone(gmt); + return formatDate(Calendar.getInstance(timeZoneLondon) + .getTimeInMillis()); + } + + /** + * 拼接数组 + * + * @param array + * @param separator + * @return + */ + public static String join(final ArrayList array, + final String separator) { + StringBuffer result = new StringBuffer(); + if (array != null && array.size() > 0) { + for (String str : array) { + result.append(str); + result.append(separator); + } + result.delete(result.length() - 1, result.length()); + } + return result.toString(); + } + + public static String join(final Iterator iter, + final String separator) { + StringBuffer result = new StringBuffer(); + if (iter != null) { + while (iter.hasNext()) { + String key = iter.next(); + result.append(key); + result.append(separator); + } + if (result.length() > 0) + result.delete(result.length() - 1, result.length()); + } + return result.toString(); + } + + /** + * 判断字符串是否为空 + * + * @param str + * @return + */ + public static boolean isEmpty(String str) { + return str == null || str.length() == 0 || str.equalsIgnoreCase("null"); + } + + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * + * @param str + * @return + */ + public static String trim(String str) { + return str == null ? EMPTY : str.trim(); + } + + /** + * 转换时间显示 + * + * @param time + * 毫秒 + * @return + */ + public static String generateTime(long time) { + int totalSeconds = (int) (time / 1000); + int seconds = totalSeconds % 60; + int minutes = (totalSeconds / 60) % 60; + int hours = totalSeconds / 3600; + + return hours > 0 ? String.format("%02d:%02d:%02d", hours, minutes, + seconds) : String.format("%02d:%02d", minutes, seconds); + } + + public static boolean isBlank(String s) { + return TextUtils.isEmpty(s); + } + + /** 根据秒速获取时间格式 */ + public static String gennerTime(int totalSeconds) { + int seconds = totalSeconds % 60; + int minutes = (totalSeconds / 60) % 60; + return String.format("%02d:%02d", minutes, seconds); + } + + /** + * 转换文件大小 + * + * @param size + * @return + */ + public static String generateFileSize(long size) { + String fileSize; + if (size < KB) + fileSize = size + "B"; + else if (size < MB) + fileSize = String.format("%.1f", size / KB) + "KB"; + else if (size < GB) + fileSize = String.format("%.1f", size / MB) + "MB"; + else + fileSize = String.format("%.1f", size / GB) + "GB"; + + return fileSize; + } + + /** 查找字符串,找到返回,没找到返回空 */ + public static String findString(String search, String start, String end) { + int start_len = start.length(); + int start_pos = StringUtils.isEmpty(start) ? 0 : search.indexOf(start); + if (start_pos > -1) { + int end_pos = StringUtils.isEmpty(end) ? -1 : search.indexOf(end, + start_pos + start_len); + if (end_pos > -1) + return search.substring(start_pos + start.length(), end_pos); + } + return ""; + } + + /** + * 截取字符串 + * + * @param search + * 待搜索的字符串 + * @param start + * 起始字符串 例如: + * @param end + * 结束字符串 例如: + * @param defaultValue + * @return + */ + public static String substring(String search, String start, String end, + String defaultValue) { + int start_len = start.length(); + int start_pos = StringUtils.isEmpty(start) ? 0 : search.indexOf(start); + if (start_pos > -1) { + int end_pos = StringUtils.isEmpty(end) ? -1 : search.indexOf(end, + start_pos + start_len); + if (end_pos > -1) + return search.substring(start_pos + start.length(), end_pos); + else + return search.substring(start_pos + start.length()); + } + return defaultValue; + } + + /** + * 截取字符串 + * + * @param search + * 待搜索的字符串 + * @param start + * 起始字符串 例如: + * @param end + * 结束字符串 例如: + * @return + */ + public static String substring(String search, String start, String end) { + return substring(search, start, end, ""); + } + + /** + * 拼接字符串 + * + * @param strs + * @return + */ + public static String concat(String... strs) { + StringBuffer result = new StringBuffer(); + if (strs != null) { + for (String str : strs) { + if (str != null) + result.append(str); + } + } + return result.toString(); + } + + /** + * Helper function for making null strings safe for comparisons, etc. + * + * @return (s == null) ? "" : s; + */ + public static String makeSafe(String s) { + return (s == null) ? "" : s; + } +} diff --git a/src/android/libs/arm64-v8a/libavcodec.so b/src/android/libs/arm64-v8a/libavcodec.so new file mode 100755 index 0000000..8ce3917 Binary files /dev/null and b/src/android/libs/arm64-v8a/libavcodec.so differ diff --git a/src/android/libs/arm64-v8a/libavfilter.so b/src/android/libs/arm64-v8a/libavfilter.so new file mode 100755 index 0000000..b80e445 Binary files /dev/null and b/src/android/libs/arm64-v8a/libavfilter.so differ diff --git a/src/android/libs/arm64-v8a/libavformat.so b/src/android/libs/arm64-v8a/libavformat.so new file mode 100755 index 0000000..4194dc0 Binary files /dev/null and b/src/android/libs/arm64-v8a/libavformat.so differ diff --git a/src/android/libs/arm64-v8a/libavutil.so b/src/android/libs/arm64-v8a/libavutil.so new file mode 100755 index 0000000..adb0039 Binary files /dev/null and b/src/android/libs/arm64-v8a/libavutil.so differ diff --git a/src/android/libs/arm64-v8a/libfdk-aac.so b/src/android/libs/arm64-v8a/libfdk-aac.so new file mode 100755 index 0000000..b381003 Binary files /dev/null and b/src/android/libs/arm64-v8a/libfdk-aac.so differ diff --git a/src/android/libs/arm64-v8a/libjx_ffmpeg_jni.so b/src/android/libs/arm64-v8a/libjx_ffmpeg_jni.so new file mode 100755 index 0000000..28a8401 Binary files /dev/null and b/src/android/libs/arm64-v8a/libjx_ffmpeg_jni.so differ diff --git a/src/android/libs/arm64-v8a/libswresample.so b/src/android/libs/arm64-v8a/libswresample.so new file mode 100755 index 0000000..5844176 Binary files /dev/null and b/src/android/libs/arm64-v8a/libswresample.so differ diff --git a/src/android/libs/arm64-v8a/libswscale.so b/src/android/libs/arm64-v8a/libswscale.so new file mode 100755 index 0000000..a6bc751 Binary files /dev/null and b/src/android/libs/arm64-v8a/libswscale.so differ diff --git a/src/android/libs/armeabi-v7a/libavcodec.so b/src/android/libs/armeabi-v7a/libavcodec.so new file mode 100755 index 0000000..171a3b2 Binary files /dev/null and b/src/android/libs/armeabi-v7a/libavcodec.so differ diff --git a/src/android/libs/armeabi-v7a/libavfilter.so b/src/android/libs/armeabi-v7a/libavfilter.so new file mode 100755 index 0000000..acd5d92 Binary files /dev/null and b/src/android/libs/armeabi-v7a/libavfilter.so differ diff --git a/src/android/libs/armeabi-v7a/libavformat.so b/src/android/libs/armeabi-v7a/libavformat.so new file mode 100755 index 0000000..96b6523 Binary files /dev/null and b/src/android/libs/armeabi-v7a/libavformat.so differ diff --git a/src/android/libs/armeabi-v7a/libavutil.so b/src/android/libs/armeabi-v7a/libavutil.so new file mode 100755 index 0000000..375b6fc Binary files /dev/null and b/src/android/libs/armeabi-v7a/libavutil.so differ diff --git a/src/android/libs/armeabi-v7a/libfdk-aac.so b/src/android/libs/armeabi-v7a/libfdk-aac.so new file mode 100755 index 0000000..9a1b5df Binary files /dev/null and b/src/android/libs/armeabi-v7a/libfdk-aac.so differ diff --git a/src/android/libs/armeabi-v7a/libjx_ffmpeg_jni.so b/src/android/libs/armeabi-v7a/libjx_ffmpeg_jni.so new file mode 100755 index 0000000..fa1b456 Binary files /dev/null and b/src/android/libs/armeabi-v7a/libjx_ffmpeg_jni.so differ diff --git a/src/android/libs/armeabi-v7a/libswresample.so b/src/android/libs/armeabi-v7a/libswresample.so new file mode 100755 index 0000000..047e941 Binary files /dev/null and b/src/android/libs/armeabi-v7a/libswresample.so differ diff --git a/src/android/libs/armeabi-v7a/libswscale.so b/src/android/libs/armeabi-v7a/libswscale.so new file mode 100755 index 0000000..dc25ce1 Binary files /dev/null and b/src/android/libs/armeabi-v7a/libswscale.so differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_disable.png b/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_disable.png new file mode 100644 index 0000000..892d983 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_disable.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_normal.png b/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_normal.png new file mode 100644 index 0000000..197817e Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_normal.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_pressed.png b/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_pressed.png new file mode 100644 index 0000000..dd959f4 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_flash_led_off_pressed.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_disable.png b/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_disable.png new file mode 100644 index 0000000..5f44b39 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_disable.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_normal.png b/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_normal.png new file mode 100644 index 0000000..dac46bf Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_normal.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_pressed.png b/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_pressed.png new file mode 100644 index 0000000..fd1b76b Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_flash_led_on_pressed.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_switch_disable.png b/src/android/res/drawable-xxhdpi/record_camera_switch_disable.png new file mode 100644 index 0000000..8a3a561 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_switch_disable.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_switch_normal.png b/src/android/res/drawable-xxhdpi/record_camera_switch_normal.png new file mode 100644 index 0000000..bc066d1 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_switch_normal.png differ diff --git a/src/android/res/drawable-xxhdpi/record_camera_switch_pressed.png b/src/android/res/drawable-xxhdpi/record_camera_switch_pressed.png new file mode 100644 index 0000000..b06677e Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_camera_switch_pressed.png differ diff --git a/src/android/res/drawable-xxhdpi/record_cancel_normal.png b/src/android/res/drawable-xxhdpi/record_cancel_normal.png new file mode 100644 index 0000000..425ed74 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_cancel_normal.png differ diff --git a/src/android/res/drawable-xxhdpi/record_cancel_press.png b/src/android/res/drawable-xxhdpi/record_cancel_press.png new file mode 100644 index 0000000..54d7433 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_cancel_press.png differ diff --git a/src/android/res/drawable-xxhdpi/record_delete_check_normal.png b/src/android/res/drawable-xxhdpi/record_delete_check_normal.png new file mode 100644 index 0000000..a5a3ee8 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_delete_check_normal.png differ diff --git a/src/android/res/drawable-xxhdpi/record_delete_check_press.png b/src/android/res/drawable-xxhdpi/record_delete_check_press.png new file mode 100644 index 0000000..bbab4e8 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_delete_check_press.png differ diff --git a/src/android/res/drawable-xxhdpi/record_delete_normal.png b/src/android/res/drawable-xxhdpi/record_delete_normal.png new file mode 100644 index 0000000..8eb6b6b Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_delete_normal.png differ diff --git a/src/android/res/drawable-xxhdpi/record_delete_press.png b/src/android/res/drawable-xxhdpi/record_delete_press.png new file mode 100644 index 0000000..62eecb4 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_delete_press.png differ diff --git a/src/android/res/drawable-xxhdpi/record_next_normal.png b/src/android/res/drawable-xxhdpi/record_next_normal.png new file mode 100644 index 0000000..79a880b Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_next_normal.png differ diff --git a/src/android/res/drawable-xxhdpi/record_next_press.png b/src/android/res/drawable-xxhdpi/record_next_press.png new file mode 100644 index 0000000..e6c4026 Binary files /dev/null and b/src/android/res/drawable-xxhdpi/record_next_press.png differ diff --git a/src/android/res/drawable/record_camera_flash_led_selector.xml b/src/android/res/drawable/record_camera_flash_led_selector.xml new file mode 100644 index 0000000..0b98666 --- /dev/null +++ b/src/android/res/drawable/record_camera_flash_led_selector.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/res/drawable/record_camera_switch_selector.xml b/src/android/res/drawable/record_camera_switch_selector.xml new file mode 100644 index 0000000..96da727 --- /dev/null +++ b/src/android/res/drawable/record_camera_switch_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/android/res/drawable/record_delete_selector.xml b/src/android/res/drawable/record_delete_selector.xml new file mode 100644 index 0000000..e6db16b --- /dev/null +++ b/src/android/res/drawable/record_delete_selector.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/android/res/drawable/record_next_seletor.xml b/src/android/res/drawable/record_next_seletor.xml new file mode 100644 index 0000000..6bf851a --- /dev/null +++ b/src/android/res/drawable/record_next_seletor.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/android/res/drawable/small_video_shoot.xml b/src/android/res/drawable/small_video_shoot.xml new file mode 100644 index 0000000..6edd287 --- /dev/null +++ b/src/android/res/drawable/small_video_shoot.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/android/res/layout/activity_media_recorder.xml b/src/android/res/layout/activity_media_recorder.xml new file mode 100644 index 0000000..8c2add7 --- /dev/null +++ b/src/android/res/layout/activity_media_recorder.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/res/values/colors.xml b/src/android/res/values/colors.xml new file mode 100644 index 0000000..05a6fd2 --- /dev/null +++ b/src/android/res/values/colors.xml @@ -0,0 +1,17 @@ + + + #3F51B5 + #303F9F + #FF4081 + #222222 + #50000000 + #24ff00 + #2c2c2c + #005084 + #f68b2b + #1b5d89 + #41000000 + #50000000 + #00000000 + + diff --git a/src/android/res/values/strings.xml b/src/android/res/values/strings.xml new file mode 100644 index 0000000..0f4f8da --- /dev/null +++ b/src/android/res/values/strings.xml @@ -0,0 +1,53 @@ + + + 提示 + + + 返回 + 取消 + 确定 + + %1$s 作品 + 拍摄 + 返回 + 下一步 + 回删 + 延迟 + 滤镜 + 初始化视频存储路径失败 + 准备中… + 手机满了!至少需要200M存储空间才能继续拍摄! + 无法打开录音设备! + 视频信息保存失败! + 是否放弃这段视频? + 导入 + 照片 + 从本地照片选择 + 导入照片失败 + 视频 + 截\t取 + 从本地视频选择 + 导入视频失败 + 焦点 + 闪光 + 幽灵 + 编辑 + 拍摄 + 下一步 + 确定 + 取消 + 视频转码失败 + 视频保存在:%s + 拍摄信息读取失败!请检查SD卡,稍后重试! + 主题 + 预览 + 原始 + 主题加载失败 + 正在转码… + 转码中\t%d%% + + 主题 + 滤镜 + 视频生成中… + +