最近两年小视频越来越火热,音视频开发一直是迫在眉睫的事情,现在各个公司都想来站在风口上分一杯羹,跟上时代的潮流,说归说,可如何才能实现呢?

提出需求-->需求分析-->发现问题-->实现思路-->多次尝试实现->解决问题!

进行音视频开发需要注意以下几点:

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来展示预览,创建本地文件来缓存拍摄到的视频,然后调用摄像头,打开摄像头,调用录制视频,然后放到已经创建好的文件目录之下,不同机型使用同一路径可能找不到,这个还需要单独处理;

使用摄像头录制视频,需要特别的调用顺序。要想做好准备工作,必须遵从特定的执行顺序。

  1. 打开摄像头 - 使用Camera.open()获取Camera实例
  2. 连接预览视图 - 使用Camera.setPreviewDisplay()连接SurfaceView
  3. 开始预览 - 使用Camera.startPreview() 方法显示即时录像图片
  4. 开始录制视频 - 要完成视频录制,必须按顺序完成下面的不步骤:
  • 解锁摄像头 - 调用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()
  1. 停止录制 - 依次调用下面的方法以完成录制
  • 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()方法调用失败时,才需要调用这个方法。
  1. Stop the Preview - 当Activity完成使用完摄像头后,调用Camera.stopPreview()停止预览;
  2. 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();
        }
    }*/
}