Merge remote-tracking branch 'origin/master'
# Conflicts: # plugin.xml
56
README.md
Normal 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,但在 Android10(SdkVersion 29)中会出现 file.mkdir() 无法创建的问题,因此要么指定要么小于 29 的 sdk,要么添加过渡期解决方案。需要注意到 Android11(SdkVersion 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)
|
213
plugin.xml
@ -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>
|
||||
|
72
src/android/AudioRecorder.java
Normal 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;
|
||||
}
|
||||
}
|
132
src/android/BaseMediaBitrateConfig.java
Normal 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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
220
src/android/DeviceUtils.java
Normal 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();
|
||||
}
|
||||
}
|
49
src/android/FFMpegUtils.java
Normal 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 ;
|
||||
}
|
||||
|
||||
}
|
149
src/android/FFmpegBridge.java
Normal 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
@ -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;
|
||||
}
|
||||
}
|
38
src/android/IMediaRecorder.java
Normal 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);
|
||||
}
|
56
src/android/JianXiCamera.java
Normal 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
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
563
src/android/MediaObject.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
955
src/android/MediaRecorderBase.java
Normal 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;
|
||||
}
|
||||
}
|
144
src/android/MediaRecorderNative.java
Normal 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);
|
||||
}
|
||||
}
|
58
src/android/MediaThemeObject.java
Normal 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;
|
||||
}
|
||||
}
|
242
src/android/ProgressView.java
Normal 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;
|
||||
}
|
||||
}
|
318
src/android/StringUtils.java
Normal 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;
|
||||
}
|
||||
}
|
BIN
src/android/libs/arm64-v8a/libavcodec.so
Executable file
BIN
src/android/libs/arm64-v8a/libavfilter.so
Executable file
BIN
src/android/libs/arm64-v8a/libavformat.so
Executable file
BIN
src/android/libs/arm64-v8a/libavutil.so
Executable file
BIN
src/android/libs/arm64-v8a/libfdk-aac.so
Executable file
BIN
src/android/libs/arm64-v8a/libjx_ffmpeg_jni.so
Executable file
BIN
src/android/libs/arm64-v8a/libswresample.so
Executable file
BIN
src/android/libs/arm64-v8a/libswscale.so
Executable file
BIN
src/android/libs/armeabi-v7a/libavcodec.so
Executable file
BIN
src/android/libs/armeabi-v7a/libavfilter.so
Executable file
BIN
src/android/libs/armeabi-v7a/libavformat.so
Executable file
BIN
src/android/libs/armeabi-v7a/libavutil.so
Executable file
BIN
src/android/libs/armeabi-v7a/libfdk-aac.so
Executable file
BIN
src/android/libs/armeabi-v7a/libjx_ffmpeg_jni.so
Executable file
BIN
src/android/libs/armeabi-v7a/libswresample.so
Executable file
BIN
src/android/libs/armeabi-v7a/libswscale.so
Executable file
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.1 KiB |
BIN
src/android/res/drawable-xxhdpi/record_camera_switch_disable.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/android/res/drawable-xxhdpi/record_camera_switch_normal.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src/android/res/drawable-xxhdpi/record_camera_switch_pressed.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/android/res/drawable-xxhdpi/record_cancel_normal.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/android/res/drawable-xxhdpi/record_cancel_press.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/android/res/drawable-xxhdpi/record_delete_check_normal.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
src/android/res/drawable-xxhdpi/record_delete_check_press.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
src/android/res/drawable-xxhdpi/record_delete_normal.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/android/res/drawable-xxhdpi/record_delete_press.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/android/res/drawable-xxhdpi/record_next_normal.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/android/res/drawable-xxhdpi/record_next_press.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
@ -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>
|
@ -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>
|
9
src/android/res/drawable/record_delete_selector.xml
Normal 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>
|
7
src/android/res/drawable/record_next_seletor.xml
Normal 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>
|
6
src/android/res/drawable/small_video_shoot.xml
Normal 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>
|
111
src/android/res/layout/activity_media_recorder.xml
Normal 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>
|
17
src/android/res/values/colors.xml
Normal 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>
|
53
src/android/res/values/strings.xml
Normal 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>
|