Merge remote-tracking branch 'origin/master'

# Conflicts:
#	plugin.xml
This commit is contained in:
zher52 2021-10-11 16:34:37 +08:00
commit 25a7be1eef
60 changed files with 4110 additions and 139 deletions

56
README.md Normal file
View File

@ -0,0 +1,56 @@
# 短视频拍摄+压缩插件
## 来源
Android 代码根据 https://github.com/mabeijianxi/small-video-record 修改。
## 安装方法
`cordova plugin add git+http://m.shuto.cn:8680/center/capture-cordova-plugin.git`
## 调用方法
```javascript
/**
* 拍摄
* @Param options 拍摄参数
* @Param 成功callback
* @Param 失败callback
*
*/
capture.capture(options, success, error);
```
```javascript
/**
* 参数说明
*/
options = {
needFull: true, // 是否全屏预览,一般性能好些的机器都可开启
width: 640, // 视频宽度(相当于手机竖拍时的高度)
height: 480, // 视频高度
maxTime: 15000, // 可录制最大长度,单位:毫秒
minTime: 3000, // 录制最小长度,不到这个长度的会被忽略?,单位:毫秒
maxFramerate: 24, // 最大帧率,这个参数似乎影响不是很大
bitrate: 580000, // 比特率,值越大,文件越大,理论上越清晰
thumbnailsTime: 1 // 用作缩略图的图片的截取时间默认第1帧
}
```
## 说明
1. Android 需要设置最低 sdk 版本为24
```xml
<preference name="android-minSdkVersion" value="24" />
```
2. Android 中因使用到了外部存储 DCMI但在 Android10SdkVersion 29中会出现 file.mkdir() 无法创建的问题,因此要么指定要么小于 29 的 sdk要么添加过渡期解决方案。需要注意到 Android11SdkVersion 30开始这个过滤方案将失效需要按照 Scoped Storage 的规范使用外部存储。
- 2.1 指定 sdk 版本
```xml
<preference name="android-targetSdkVersion" value="25" />
```
- 2.2 使用过渡方案
```xml
<edit-config file="app/src/main/AndroidManifest.xml" mode="merge" target="/manifest/application" xmlns:android="http://schemas.android.com/apk/res/android">
<application android:requestLegacyExternalStorage="true" />
</edit-config>
```
[示例 app](http://m.shuto.cn:8680/center/captureDemo.git)

View File

@ -1,77 +1,140 @@
<?xml version='1.0' encoding='utf-8'?>
<plugin id="capture-cordova-plugin" version="1.0.0" xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<name>CapturePlugin</name>
<js-module name="capture" src="www/capture.js">
<clobbers target="capture"/>
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="capture-cordova-plugin">
<param name="android-package" value="cn.shuto.plugin.capture.CaptureCordovaPlugin"/>
</feature>
</config-file>
<config-file parent="/*" target="AndroidManifest.xml">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</config-file>
<config-file parent="application" target="AndroidManifest.xml">
<activity android:clearTaskOnLaunch="true" android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="false" android:name="com.mabeijianxi.smallvideorecord2.MediaRecorderActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:windowSoftInputMode="stateAlwaysHidden"/>
<provider android:authorities="${applicationId}.cordova.plugin.camera.provider" android:exported="false"
android:grantUriPermissions="true" android:name="org.apache.cordova.camera.FileProvider">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/camera_provider_paths"/>
</provider>
</config-file>
<config-file parent="/manifest" target="AndroidManifest.xml">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.FLASHLIGHT"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
</config-file>
<source-file src="src/android/CaptureCordovaPlugin.java" target-dir="src/cn/shuto/plugin/capture"/>
<framework custom="true" src="src/android/mobile-ffmpeg-x2.gradle" type="gradleReference"/>
</platform>
<platform name="ios">
<config-file parent="/*" target="config.xml">
<feature name="CapturePlugin">
<param name="ios-package" value="CapturePlugin"/>
</feature>
</config-file>
<source-file src="src/ios/CapturePlugin.m"/>
<header-file src="src/ios/SGRecord/SGMotionManager.h"/>
<source-file src="src/ios/SGRecord/SGMotionManager.m"/>
<header-file src="src/ios/SGRecord/SGRecordEncoder.h"/>
<source-file src="src/ios/SGRecord/SGRecordEncoder.m"/>
<header-file src="src/ios/SGRecord/SGRecordManager.h"/>
<source-file src="src/ios/SGRecord/SGRecordManager.m"/>
<header-file src="src/ios/SGRecord/SGRecordOptions.h"/>
<source-file src="src/ios/SGRecord/SGRecordOptions.m"/>
<header-file src="src/ios/SGRecord/SGRecordProgressView.h"/>
<source-file src="src/ios/SGRecord/SGRecordProgressView.m"/>
<header-file src="src/ios/SGRecord/SGRecordSuccessPreview.h"/>
<source-file src="src/ios/SGRecord/SGRecordSuccessPreview.m"/>
<header-file src="src/ios/SGRecord/SGRecordViewController.h"/>
<source-file src="src/ios/SGRecord/SGRecordViewController.m"/>
<header-file src="src/ios/SGRecord/UIButton+Convenience.h"/>
<source-file src="src/ios/SGRecord/UIButton+Convenience.m"/>
<resource-file src="src/ios/Assets.xcassets"/>
<framework src="AVFoundation.framework"/>
<framework src="AVKit.framework"/>
<framework src="CoreMotion.framework"/>
<framework src="MobileCoreServices.framework"/>
<preference name="CAMERA_USAGE_DESCRIPTION" default="This app requires access to your camera to take pictures" />
<config-file target="*-Info.plist" parent="NSCameraUsageDescription">
<string>$CAMERA_USAGE_DESCRIPTION</string>
</config-file>
<preference name="MICROPHONE_USAGE_DESCRIPTION" default="This app requires access to your microphone to take pictures" />
<config-file target="*-Info.plist" parent="NSMicrophoneUsageDescription">
<string>$MICROPHONE_USAGE_DESCRIPTION</string>
</config-file>
<preference name="PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION" default="This app requires access to your photo library to save your pictures" />
<config-file target="*-Info.plist" parent="NSPhotoLibraryAddUsageDescription">
<string>$PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION</string>
</config-file>
</platform>
<plugin id="capture-cordova-plugin" version="1.0.0"
xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<name>CapturePlugin</name>
<js-module name="capture" src="www/capture.js">
<clobbers target="capture" />
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="CapturePlugin">
<param name="android-package" value="cn.shuto.plugin.capture.CaptureCordovaPlugin" />
<param name="onload" value="true" />
</feature>
</config-file>
<config-file target="AndroidManifest.xml" parent="/manifest">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.FLASHLIGHT"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" />
</config-file>
<config-file target="AndroidManifest.xml" parent="application">
<activity android:name="com.mabeijianxi.smallvideorecord2.MediaRecorderActivity" android:clearTaskOnLaunch="true" android:configChanges="orientation|keyboardHidden|screenSize" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:windowSoftInputMode="stateAlwaysHidden" android:exported="false"/>
</config-file>
<source-file src="src/android/CaptureCordovaPlugin.java" target-dir="src/cn/shuto/plugin/capture" />
<source-file src="src/android/MediaRecorderConfig.java" target-dir="com/mabeijianxi/smallvideorecord2/model" />
<source-file src="src/android/MediaObject.java" target-dir="com/mabeijianxi/smallvideorecord2/model" />
<source-file src="src/android/BaseMediaBitrateConfig.java" target-dir="com/mabeijianxi/smallvideorecord2/model" />
<source-file src="src/android/MediaThemeObject.java" target-dir="com/mabeijianxi/smallvideorecord2/model" />
<source-file src="src/android/MediaRecorderActivity.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/MediaRecorderBase.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/MediaRecorderNative.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/IMediaRecorder.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/ProgressView.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/AudioRecorder.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/JianXiCamera.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/FileUtils.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/StringUtils.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/Log.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/FFMpegUtils.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/DeviceUtils.java" target-dir="com/mabeijianxi/smallvideorecord2" />
<source-file src="src/android/FFmpegBridge.java" target-dir="com/mabeijianxi/smallvideorecord2/jniinterface" />
<resource-file src="src/android/res/layout/activity_media_recorder.xml" target="res/layout/activity_media_recorder.xml" />
<resource-file src="src/android/res/values/colors.xml" target="/res/values/colors.xml" />
<resource-file src="src/android/res/values/strings.xml" target="/res/values/capture-strings.xml" />
<resource-file src="src/android/res/drawable/record_camera_flash_led_selector.xml" target="/res/drawable/record_camera_flash_led_selector.xml" />
<resource-file src="src/android/res/drawable/record_camera_switch_selector.xml" target="/res/drawable/record_camera_switch_selector.xml" />
<resource-file src="src/android/res/drawable/record_delete_selector.xml" target="/res/drawable/record_delete_selector.xml" />
<resource-file src="src/android/res/drawable/record_next_seletor.xml" target="/res/drawable/record_next_seletor.xml" />
<resource-file src="src/android/res/drawable/small_video_shoot.xml" target="/res/drawable/small_video_shoot.xml" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_flash_led_off_disable.png" target="/res/drawable-xxhdpi/record_camera_flash_led_off_disable.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_flash_led_off_normal.png" target="/res/drawable-xxhdpi/record_camera_flash_led_off_normal.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_flash_led_off_pressed.png" target="/res/drawable-xxhdpi/record_camera_flash_led_off_pressed.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_flash_led_on_disable.png" target="/res/drawable-xxhdpi/record_camera_flash_led_on_disable.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_flash_led_on_normal.png" target="/res/drawable-xxhdpi/record_camera_flash_led_on_normal.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_flash_led_on_pressed.png" target="/res/drawable-xxhdpi/record_camera_flash_led_on_pressed.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_switch_disable.png" target="/res/drawable-xxhdpi/record_camera_switch_disable.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_switch_normal.png" target="/res/drawable-xxhdpi/record_camera_switch_normal.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_camera_switch_pressed.png" target="/res/drawable-xxhdpi/record_camera_switch_pressed.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_cancel_normal.png" target="/res/drawable-xxhdpi/record_cancel_normal.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_cancel_press.png" target="/res/drawable-xxhdpi/record_cancel_press.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_delete_check_normal.png" target="/res/drawable-xxhdpi/record_delete_check_normal.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_delete_check_press.png" target="/res/drawable-xxhdpi/record_delete_check_press.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_delete_normal.png" target="/res/drawable-xxhdpi/record_delete_normal.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_delete_press.png" target="/res/drawable-xxhdpi/record_delete_press.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_next_normal.png" target="/res/drawable-xxhdpi/record_next_normal.png" />
<resource-file src="src/android/res/drawable-xxhdpi/record_next_press.png" target="/res/drawable-xxhdpi/record_next_press.png" />
<resource-file src="src/android/libs/armeabi-v7a/libavcodec.so" target="jniLibs/armeabi-v7a/libavcodec.so" />
<resource-file src="src/android/libs/armeabi-v7a/libavfilter.so" target="jniLibs/armeabi-v7a/libavfilter.so" />
<resource-file src="src/android/libs/armeabi-v7a/libavformat.so" target="jniLibs/armeabi-v7a/libavformat.so" />
<resource-file src="src/android/libs/armeabi-v7a/libavutil.so" target="jniLibs/armeabi-v7a/libavutil.so" />
<resource-file src="src/android/libs/armeabi-v7a/libfdk-aac.so" target="jniLibs/armeabi-v7a/libfdk-aac.so" />
<resource-file src="src/android/libs/armeabi-v7a/libjx_ffmpeg_jni.so" target="jniLibs/armeabi-v7a/libjx_ffmpeg_jni.so" />
<resource-file src="src/android/libs/armeabi-v7a/libswresample.so" target="jniLibs/armeabi-v7a/libswresample.so" />
<resource-file src="src/android/libs/armeabi-v7a/libswscale.so" target="jniLibs/armeabi-v7a/libswscale.so" />
<resource-file src="src/android/libs/arm64-v8a/libavcodec.so" target="jniLibs/arm64-v8a/libavcodec.so" />
<resource-file src="src/android/libs/arm64-v8a/libavfilter.so" target="jniLibs/arm64-v8a/libavfilter.so" />
<resource-file src="src/android/libs/arm64-v8a/libavformat.so" target="jniLibs/arm64-v8a/libavformat.so" />
<resource-file src="src/android/libs/arm64-v8a/libavutil.so" target="jniLibs/arm64-v8a/libavutil.so" />
<resource-file src="src/android/libs/arm64-v8a/libfdk-aac.so" target="jniLibs/arm64-v8a/libfdk-aac.so" />
<resource-file src="src/android/libs/arm64-v8a/libjx_ffmpeg_jni.so" target="jniLibs/arm64-v8a/libjx_ffmpeg_jni.so" />
<resource-file src="src/android/libs/arm64-v8a/libswresample.so" target="jniLibs/arm64-v8a/libswresample.so" />
<resource-file src="src/android/libs/arm64-v8a/libswscale.so" target="jniLibs/arm64-v8a/libswscale.so" />
<framework custom="true" src="src/android/mobile-ffmpeg-x2.gradle" type="gradleReference" />
</platform>
<platform name="ios">
<config-file parent="/*" target="config.xml">
<feature name="CapturePlugin">
<param name="ios-package" value="CapturePlugin"/>
</feature>
</config-file>
<source-file src="src/ios/CapturePlugin.m"/>
<header-file src="src/ios/SGRecord/SGMotionManager.h"/>
<source-file src="src/ios/SGRecord/SGMotionManager.m"/>
<header-file src="src/ios/SGRecord/SGRecordEncoder.h"/>
<source-file src="src/ios/SGRecord/SGRecordEncoder.m"/>
<header-file src="src/ios/SGRecord/SGRecordManager.h"/>
<source-file src="src/ios/SGRecord/SGRecordManager.m"/>
<header-file src="src/ios/SGRecord/SGRecordOptions.h"/>
<source-file src="src/ios/SGRecord/SGRecordOptions.m"/>
<header-file src="src/ios/SGRecord/SGRecordProgressView.h"/>
<source-file src="src/ios/SGRecord/SGRecordProgressView.m"/>
<header-file src="src/ios/SGRecord/SGRecordSuccessPreview.h"/>
<source-file src="src/ios/SGRecord/SGRecordSuccessPreview.m"/>
<header-file src="src/ios/SGRecord/SGRecordViewController.h"/>
<source-file src="src/ios/SGRecord/SGRecordViewController.m"/>
<header-file src="src/ios/SGRecord/UIButton+Convenience.h"/>
<source-file src="src/ios/SGRecord/UIButton+Convenience.m"/>
<resource-file src="src/ios/Assets.xcassets"/>
<framework src="AVFoundation.framework"/>
<framework src="AVKit.framework"/>
<framework src="CoreMotion.framework"/>
<framework src="MobileCoreServices.framework"/>
<preference name="CAMERA_USAGE_DESCRIPTION" default="This app requires access to your camera to take pictures" />
<config-file target="*-Info.plist" parent="NSCameraUsageDescription">
<string>$CAMERA_USAGE_DESCRIPTION</string>
</config-file>
<preference name="MICROPHONE_USAGE_DESCRIPTION" default="This app requires access to your microphone to take pictures" />
<config-file target="*-Info.plist" parent="NSMicrophoneUsageDescription">
<string>$MICROPHONE_USAGE_DESCRIPTION</string>
</config-file>
<preference name="PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION" default="This app requires access to your photo library to save your pictures" />
<config-file target="*-Info.plist" parent="NSPhotoLibraryAddUsageDescription">
<string>$PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION</string>
</config-file>
</platform>
</plugin>

View File

@ -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;
}
}

View File

@ -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<BaseMediaBitrateConfig> CREATOR = new Creator<BaseMediaBitrateConfig>() {
@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";
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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 ;
}
}

View File

@ -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<FFmpegStateListener> 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();
}
}

374
src/android/FileUtils.java Normal file
View File

@ -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
* </code>charset<code>}
* @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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

113
src/android/Log.java Normal file
View File

@ -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);
}
}
}

View File

@ -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<MediaPart> mMediaList = new LinkedList<MediaPart>();
/**
* 主题
*/
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<MediaPart> 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;
}
}
}
}

View File

@ -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();
}

View File

@ -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<Size> 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<String> 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<Area> 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<String> 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<Integer> 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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<MediaObject.MediaPart> 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;
}
}

View File

@ -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<String> 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<String> 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
* 起始字符串 例如<title>
* @param end
* 结束字符串 例如</title>
* @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
* 起始字符串 例如<title>
* @param end
* 结束字符串 例如</title>
* @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;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/record_camera_flash_led_on_disable" android:state_checked="true" android:state_enabled="false"/>
<item android:drawable="@drawable/record_camera_flash_led_on_pressed" android:state_checked="true" android:state_pressed="true"/>
<item android:drawable="@drawable/record_camera_flash_led_on_normal" android:state_checked="true" android:state_pressed="false"/>
<item android:drawable="@drawable/record_camera_flash_led_off_disable" android:state_enabled="false"/>
<item android:drawable="@drawable/record_camera_flash_led_off_pressed" android:state_checked="false" android:state_pressed="true"/>
<item android:drawable="@drawable/record_camera_flash_led_off_normal"/>
</selector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/record_camera_switch_disable" android:state_enabled="false"/>
<item android:drawable="@drawable/record_camera_switch_pressed" android:state_pressed="true"/>
<item android:drawable="@drawable/record_camera_switch_normal"/>
</selector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/record_delete_check_normal" android:state_checked="true" android:state_pressed="false"/>
<item android:drawable="@drawable/record_delete_check_press" android:state_checked="true" android:state_pressed="true"/>
<item android:drawable="@drawable/record_delete_press" android:state_pressed="true"/>
<item android:drawable="@drawable/record_delete_normal"/>
</selector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/record_next_press" android:state_pressed="true"/>
<item android:drawable="@drawable/record_next_normal"/>
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"
>
<stroke android:color="@color/camera_progress_three" android:width="2dp"/>
</shape>

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<SurfaceView
android:id="@+id/record_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="49dp" />
</FrameLayout>
<RelativeLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="49dp"
android:background="@color/color_381902"
android:gravity="center_vertical">
<ImageView
android:id="@+id/title_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="10dip"
android:contentDescription="@string/imageview_content_description"
android:padding="10dip"
android:src="@drawable/record_cancel_normal" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="49dip"
android:layout_alignParentEnd="true"
android:gravity="right|center_vertical"
android:orientation="horizontal">
<CheckBox
android:id="@+id/record_camera_led"
android:layout_width="40dp"
android:layout_height="20dp"
android:background="@drawable/record_camera_flash_led_selector"
android:button="@null"
android:textColor="@android:color/white" />
<CheckBox
android:id="@+id/record_camera_switcher"
android:layout_width="40dp"
android:layout_height="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="@drawable/record_camera_switch_selector"
android:button="@null" />
<ImageView
android:id="@+id/title_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:contentDescription="@string/imageview_content_description"
android:padding="10dip"
android:src="@drawable/record_next_seletor"
android:visibility="gone" />
</LinearLayout>
</RelativeLayout>
<com.mabeijianxi.smallvideorecord2.ProgressView
android:id="@+id/record_progress"
android:layout_width="match_parent"
android:layout_height="5dp"
android:layout_below="@+id/title_layout" />
<!-- camera_bottom_bg -->
<RelativeLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/record_progress"
android:layout_marginTop="300dip"
android:background="@color/color_381902"
>
<CheckedTextView
android:id="@+id/record_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="18dp"
android:background="@drawable/record_delete_selector"
android:button="@null"
android:visibility="gone" />
<TextView
android:id="@+id/record_controller"
android:layout_width="108dp"
android:layout_height="108dp"
android:layout_centerInParent="true"
android:background="@drawable/small_video_shoot"
android:gravity="center"
android:text="按住拍"
android:textColor="@color/camera_progress_three"
android:textSize="16sp" />
</RelativeLayout>
</RelativeLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="color_381902">#222222</color>
<color name="transparent2">#50000000</color>
<color name="camera_progress_three">#24ff00</color>
<color name="camera_bg">#2c2c2c</color>
<color name="camera_progress_split">#005084</color>
<color name="camera_progress_delete">#f68b2b</color>
<color name="camera_progress_overflow">#1b5d89</color>
<color name="full_title_color">#41000000</color>
<color name="full_progress_color">#50000000</color>
<color name="transparent">#00000000</color>
</resources>

View File

@ -0,0 +1,53 @@
<resources>
<string name="hint">提示</string>
<string name="dialog_yes"></string>
<string name="dialog_no"></string>
<string name="action_back">返回</string>
<string name="action_cancel">取消</string>
<string name="action_ok">确定</string>
<string name="imageview_content_description"></string>
<string name="record_camera_author">%1$s 作品</string>
<string name="record_camera_title">拍摄</string>
<string name="record_camera_back">返回</string>
<string name="record_camera_next">下一步</string>
<string name="record_camera_back_delete">回删</string>
<string name="record_camera_delay">延迟</string>
<string name="record_camera_filter">滤镜</string>
<string name="record_camera_init_faild">初始化视频存储路径失败</string>
<string name="record_camera_progress_message">准备中…</string>
<string name="record_camera_check_available_faild">手机满了至少需要200M存储空间才能继续拍摄</string>
<string name="record_camera_open_audio_faild">无法打开录音设备!</string>
<string name="record_camera_save_faild">视频信息保存失败!</string>
<string name="record_camera_exit_dialog_message">是否放弃这段视频?</string>
<string name="record_camera_import">导入</string>
<string name="record_camera_import_image">照片</string>
<string name="record_camera_import_image_choose">从本地照片选择</string>
<string name="record_camera_import_image_faild">导入照片失败</string>
<string name="record_camera_import_video">视频</string>
<string name="record_camera_import_video_title">截\t取</string>
<string name="record_camera_import_video_choose">从本地视频选择</string>
<string name="record_camera_import_video_faild">导入视频失败</string>
<string name="record_camera_tools_focus">焦点</string>
<string name="record_camera_tools_led">闪光</string>
<string name="record_camera_ghost">幽灵</string>
<string name="record_camera_preview_title">编辑</string>
<string name="record_camera_preview_pre">拍摄</string>
<string name="record_camera_preview_next">下一步</string>
<string name="record_camera_cancel_dialog_yes">确定</string>
<string name="record_camera_cancel_dialog_no">取消</string>
<string name="record_video_transcoding_faild">视频转码失败</string>
<string name="record_video_transcoding_success">视频保存在:%s</string>
<string name="record_read_object_faild">拍摄信息读取失败请检查SD卡稍后重试</string>
<string name="record_preview_theme">主题</string>
<string name="record_preview_title">预览</string>
<string name="record_preview_theme_original">原始</string>
<string name="record_preview_theme_load_faild">主题加载失败</string>
<string name="record_preview_encoding">正在转码…</string>
<string name="record_preview_encoding_format">转码中\t%d%%</string>
<string name="record_preview_music_nothing"></string>
<string name="record_preview_tab_theme">主题</string>
<string name="record_preview_tab_filter">滤镜</string>
<string name="record_preview_building">视频生成中…</string>
</resources>