最近两年小视频越来越火热,音视频开发一直是迫在眉睫的事情,现在各个公司都想来站在风口上分一杯羹,跟上时代的潮流,说归说,可如何才能实现呢?
提出需求-->需求分析-->发现问题-->实现思路-->多次尝试实现->解决问题!
进行音视频开发需要注意以下几点:
1、Android 版本,需要知晓你进行的音视频开发是需要满足那些群体?使用的手机android版本是多少,毕竟我们华国是百家争鸣,百花齐放,到底如何统一?这还是一个长久的过程……大家都知道,android6.0是一个分界线,划分为危险权限,如果是危险权限需要动态申请,不然会报错,要么崩溃,要么no such a file等等;
2、权限管理,首先需要在清单文件中添加相对应的权限申请,如果是android6.0以上,则需要动态申请,第一次安装项目时申请,然后在需要调用的地方继续判断是否有此权限,还需要添加一个摄像头是否有的判断,手机摄像头坏了,或者没有那还录制个鬼?具体需求则需要自己处理;
3、使用SurfaceView展示预览,啥是SrufaceView?
①、 SurfaceView的定义
参考SDK文档和网络资料:SurfaceView是View的子类,它内嵌了一个专门用于绘制的Surface,你可以控制这个Surface的格式和尺寸,Surfaceview控制这个Surface的绘制位置。surface是纵深排序(Z-ordered)的,说明它总在自己所在窗口的后面。SurfaceView提供了一个可见区域,只有在这个可见区域内的surface内容才可见。surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面有透明控件,那么每次surface变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。
SurfaceView默认使用双缓冲技术的,它支持在子线程中绘制图像,这样就不会阻塞主线程了,所以它更适合于游戏的开发。
②、 SurfaceView的使用
首先继承SurfaceView,并实现SurfaceHolder.Callback接口,实现它的三个方法:surfaceCreated,surfaceChanged,surfaceDestroyed。
surfaceCreated(SurfaceHolder holder):surface创建的时候调用,一般在该方法中启动绘图的线程。
surfaceChanged(SurfaceHolder holder, int format, int width,int height):surface尺寸发生改变的时候调用,如横竖屏切换。
surfaceDestroyed(SurfaceHolder holder) :surface被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。
还需要获得SurfaceHolder,并添加回调函数,这样这三个方法才会执行。
了解完之后就要开始着手实施了,使用SurfaceView来展示预览,创建本地文件来缓存拍摄到的视频,然后调用摄像头,打开摄像头,调用录制视频,然后放到已经创建好的文件目录之下,不同机型使用同一路径可能找不到,这个还需要单独处理;
使用摄像头录制视频,需要特别的调用顺序。要想做好准备工作,必须遵从特定的执行顺序。
- 打开摄像头 - 使用
Camera.open()
获取Camera实例 - 连接预览视图 - 使用
Camera.setPreviewDisplay()
连接SurfaceView - 开始预览 - 使用
Camera.startPreview()
方法显示即时录像图片 - 开始录制视频 - 要完成视频录制,必须按顺序完成下面的不步骤:
- 解锁摄像头 - 调用
Camera.unlock()
解锁摄像头以供MediaRecorder使用 - 配置MediaRecorder - 依次调用以下MediaRecorder的方法
-
setCamera()
- 设置视频录制所用到的摄像头 -
setAudioSource
- 设置音频源,使用MediaRecorder.AudioSource.CAMCORDER
-
setVideoSource()
- 设置视频源,使用MediaRecorder.VideoSource.CAMERA
- 设置视频的输出格式和编码方式。Android 2.2 (API 8)及以上版本,用
MediaRecorder.setProfile
方法,用CamcorderProfile.get()
方法获取 profile 实例 -
setOutputFile()
- 设置输出文件 -
setPreviewDisplay()
注意:MediaRecorder的这些方法必须依次调用,否则应用会出错,录制失败
- 准备MediaRecorder - 调用
MediaRecorder.prepare()
方法 - 开始录制 - 调用
MediaRecorder.start()
- 停止录制 - 依次调用下面的方法以完成录制
- Stop MediaRecorder - 调用
MediaRecorder.stop()
- Reset MediaRecorder - 非必须的,调用
MediaRecorder.reset()
方法移除Recorder的配置设置 - Release MediaRecorder - 调用
MediaRecorder.release()
方法 - Lock the Camera - 调用
Camera.lock()
锁定摄像头,随后其他的MediaRecorder才能使用它。从Android 4.0(API 14)开始,只有在MediaRecorder.prepare()
方法调用失败时,才需要调用这个方法。
- Stop the Preview - 当Activity完成使用完摄像头后,调用
Camera.stopPreview()
停止预览; - Release Camera - 调用
Camera.release()
释放摄像头,其他应用才能使用。
布局文件:
<?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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
</FrameLayout>
<xxx.view.LineProgressView
android:id="@+id/lineProgressView"
android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_marginTop="@dimen/basic_activity_margin"
android:layout_marginLeft="@dimen/basic_activity_margin2"
android:layout_marginRight="@dimen/basic_activity_margin2"/>
</RelativeLayout>
实现
public class RecordedActivity extends BaseActivity {
public static final String INTENT_PATH = "intent_path";
public static final String INTENT_DATA_TYPE = "result_data_type";
public static final int RESULT_TYPE_VIDEO = 1;
public static final int RESULT_TYPE_PHOTO = 2;
public static final int REQUEST_CODE_KEY = 100;
/**
* 最大录制时间
*/
public static final float MAX_VIDEO_TIME = 5f * 1000;
private SurfaceView surfaceView;
private LineProgressView lineProgressView;
private TextView editorTextView;
/**
* 分段视频地址
*/
private ArrayList<String> segmentList = new ArrayList<>();
/**
* 分段音频地址
*/
private ArrayList<String> aacList = new ArrayList<>();
/**
* 分段录制时间
*/
private ArrayList<Long> timeList = new ArrayList<>();
private CameraHelp mCameraHelp = new CameraHelp();
private SurfaceHolder mSurfaceHolder;
private MyVideoEditor mVideoEditor = new MyVideoEditor();
private RecordUtil recordUtil;
/**
* 总编译次数
*/
private int executeCount;
/**
* 编译进度
*/
private float executeProgress;
private String audioPath;
private RecordUtil.OnPreviewFrameListener mOnPreviewFrameListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_recorded);
LanSoEditor.initSDK(this, null);
LanSongFileUtil.setFileDir("/sdcard/sel d'alkinylocomplexe/" + System.currentTimeMillis() + "/");
LibyuvUtil.loadLibrary();
initUI();
initMediaRecorder();
}
/**
* 初始化UI
*/
private void initUI() {
surfaceView = findViewById(R.id.surfaceView);
lineProgressView = findViewById(R.id.lineProgressView);
surfaceView.post(new Runnable() {
@Override
public void run() {
int width = surfaceView.getWidth();
int height = surfaceView.getHeight();
float viewRatio = width * 1f / height;
float videoRatio = 9f / 16f;
ViewGroup.LayoutParams layoutParams = surfaceView.getLayoutParams();
if (viewRatio > videoRatio) {
layoutParams.height = (int) (width / viewRatio);
} else {
layoutParams.width = (int) (height * viewRatio);
}
surfaceView.setLayoutParams(layoutParams);
}
});
}
private void initMediaRecorder() {
mCameraHelp.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (mOnPreviewFrameListener != null) {
mOnPreviewFrameListener.onPreviewFrame(data);
}
}
});
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceHolder = holder;
mCameraHelp.openCamera(mContext, Camera.CameraInfo.CAMERA_FACING_FRONT, mSurfaceHolder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mCameraHelp.release();
}
});
surfaceView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCameraHelp.callFocusMode();
}
});
mVideoEditor.setOnProgessListener(new onVideoEditorProgressListener() {
@Override
public void onProgress(VideoEditor v, int percent) {
if (percent == 100) {
executeProgress++;
}
int pro = (int) (executeProgress / executeCount * 100);
editorTextView.setText("视频编辑中" + pro + "%");
}
});
}
/**
* 合成视频
* TODO 根据自己需求是否需要合成音视频
*/
public void finishVideo() {
RxJavaUtil.run(new RxJavaUtil.OnRxAndroidListener<String>() {
@Override
public String doInBackground() throws Exception {
//h264转ts
ArrayList<String> tsList = new ArrayList<>();
for (int x = 0; x < segmentList.size(); x++) {
String tsPath = LanSongFileUtil.DEFAULT_DIR + System.currentTimeMillis() + ".ts";
mVideoEditor.h264ToTs(segmentList.get(x), tsPath);
tsList.add(tsPath);
}
String s = syntPcm();
//合成音频
String aacPath = mVideoEditor.executePcmEncodeAac(s, RecordUtil.sampleRateInHz, RecordUtil.channelCount);
//合成视频
String mp4Path = mVideoEditor.executeConvertTsToMp4(tsList.toArray(new String[]{}));
//音视频混合
mp4Path = mVideoEditor.executeVideoMergeAudio(mp4Path, aacPath);
return mp4Path;
}
@Override
public void onFinish(String result) {
closeProgressDialog();
// Intent intent = new Intent(mContext, EditVideoActivity.class);
// intent.putExtra(INTENT_PATH, result);
// startActivityForResult(intent, REQUEST_CODE_KEY);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
closeProgressDialog();
Toast.makeText(getApplicationContext(), "视频编辑失败", Toast.LENGTH_SHORT).show();
}
});
}
/**
* 获取pcm地址
*
* @return 地址
* @throws Exception
*/
private String syntPcm() throws Exception {
String pcmPath = LanSongFileUtil.DEFAULT_DIR + System.currentTimeMillis() + ".pcm";
File file = new File(pcmPath);
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
for (int x = 0; x < aacList.size(); x++) {
FileInputStream in = new FileInputStream(aacList.get(x));
byte[] buf = new byte[4096];
int len = 0;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
out.flush();
}
in.close();
}
out.close();
return pcmPath;
}
private long videoDuration;
private long recordTime;
private String videoPath;
/**
* 开始录制
*/
private void startRecord() {
RxJavaUtil.run(new RxJavaUtil.OnRxAndroidListener<Boolean>() {
@Override
public Boolean doInBackground() throws Throwable {
videoPath = LanSongFileUtil.DEFAULT_DIR + System.currentTimeMillis() + ".h264";
audioPath = LanSongFileUtil.DEFAULT_DIR + System.currentTimeMillis() + ".pcm";
final boolean isFrontCamera = mCameraHelp.getCameraId() == Camera.CameraInfo.CAMERA_FACING_FRONT;
final int rotation;
if (isFrontCamera) {
rotation = 270;
} else {
rotation = 90;
}
recordUtil = new RecordUtil(videoPath, audioPath, mCameraHelp.getWidth(), mCameraHelp.getHeight(), rotation, isFrontCamera);
return true;
}
@Override
public void onFinish(Boolean result) {
if (lineProgressView.getProgress() < 1.0) {
mOnPreviewFrameListener = recordUtil.start();
videoDuration = 0;
lineProgressView.setSplit();
recordTime = System.currentTimeMillis();
runLoopPro();
} else {//通过进度条来判断是否完成录制
recordUtil.release();
recordUtil = null;
}
}
@Override
public void onError(Throwable e) {
}
});
}
/**
* 录制完成操作
*/
private void runLoopPro() {
RxJavaUtil.loop(20, new RxJavaUtil.OnRxLoopListener() {
@Override
public Boolean takeWhile() {
return recordUtil != null && recordUtil.isRecording();
}
@Override
public void onExecute() {
long currentTime = System.currentTimeMillis();
videoDuration += currentTime - recordTime;
recordTime = currentTime;
long countTime = videoDuration;
for (long time : timeList) {
countTime += time;
}
if (countTime <= MAX_VIDEO_TIME) {
lineProgressView.setProgress(countTime / MAX_VIDEO_TIME);
} else {
upEvent();
}
}
@Override
public void onFinish() {
segmentList.add(videoPath);
aacList.add(audioPath);
timeList.add(videoDuration);
//录制完成直接编辑
editorTextView = showProgressDialog();
// executeCount = segmentList.size() + 4;
//TODO 根据自己操作来实现是否合成音视频
finishVideo();
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
lineProgressView.removeSplit();
}
});
}
private void upEvent() {
if (recordUtil != null) {
recordUtil.stop();
recordUtil = null;
}
}
/**
* 清除录制信息
*/
private void cleanRecord() {
lineProgressView.cleanSplit();
segmentList.clear();
aacList.clear();
timeList.clear();
executeCount = 0;
executeProgress = 0;
}
@Override
protected void onDestroy() {
super.onDestroy();
cleanRecord();
if (mCameraHelp != null) {
mCameraHelp.release();
}
if (recordUtil != null) {
recordUtil.stop();
}
}
/**
* 把视频路径回传
* @param requestCode 请求code
* @param resultCode 结果code
* @param data 数据
*/
/* @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && data != null) {
if (requestCode == REQUEST_CODE_KEY) {
Intent intent = new Intent();
intent.putExtra(INTENT_PATH, data.getStringExtra(INTENT_PATH));
intent.putExtra(INTENT_DATA_TYPE, RESULT_TYPE_VIDEO);
setResult(RESULT_OK, intent);
finish();
}
} else {
cleanRecord();
}
}*/
}