IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 安卓音视频开发(4)—— 视频录制、预览、解析与封装合成 -> 正文阅读

[移动开发]安卓音视频开发(4)—— 视频录制、预览、解析与封装合成

前言

这节学习视频开发的一些基础操作,具体包括使用MediaRecorder来录制视频,采集视频数据并保存为mp4文件。我学习的教程里使用的是Camera,通过回调来获取到NV21数据,这个获取的数据更加原始。

使用SurfaceView来预览,也可以使用TextureView来预览,但是我发现TextureView在手机上使用时存在卡顿的情况,可能是不支持硬件加速。

最开始使用MediaExtractor来解析视频时,我还以为可以直接将mp4文件输出单独的音频文件和视频文件,最后发现需要结合MediaMuxer将取出的数据封装才可以使用。

权限

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.CAMERA"/>

XML文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/btStart"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:text="开始录制"
            android:textAllCaps="false"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btStop"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:text="停止录制"
            android:textAllCaps="false"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btStart" />

        <Button
            android:id="@+id/btParsing"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:text="拆分视频"
            android:textAllCaps="false"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btStop" />


        <Button
            android:id="@+id/btSynthetic"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:text="合成视频"
            android:textAllCaps="false"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btParsing" />
        
        <SurfaceView
            android:id="@+id/surface"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="invisible" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

MediaRecorder录制流程

1.使用Camera.open()来打开相机。

2.初始化MediaRecorder,设置编码格式、封装格式、码率等。

3.添加surface预览。

4.调用prepare函数后调用start方法开始录制视频。

5.调用stop方法,并释放资源。

        binding.btStart.setOnClickListener(view -> {
            binding.surface.setVisibility(View.VISIBLE);
            camera = Camera.open();
            camera.setDisplayOrientation(90);
            camera.unlock();
            mediaRecorder = new MediaRecorder();
            mediaRecorder.setCamera(camera);
            // 设置音频采集方式
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            //设置视频的采集方式
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
            //设置文件的输出格式
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            //设置audio的编码格式
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            //设置video的编码格式
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
            //设置录制的视频帧率,注意文档的说明:
            mediaRecorder.setVideoFrameRate(60);
            //设置录制文件输出目录
            File file = new File(filePath);
            mediaRecorder.setOutputFile(file);
            //设置分辨率
            mediaRecorder.setVideoSize(1920, 1080);
            //设置录制方向
            mediaRecorder.setOrientationHint(90);
            //添加预览
            mediaRecorder.setPreviewDisplay(binding.surface.getHolder().getSurface());
            //准备
            try {
                mediaRecorder.prepare();
            } catch (IOException e) {
                e.printStackTrace();
            }
            //开始录制
            mediaRecorder.start();
        });

        binding.btStop.setOnClickListener(view -> {
            binding.surface.setVisibility(View.INVISIBLE);
            mediaRecorder.stop();
            mediaRecorder.release();
            camera.stopPreview();
            camera.release();
        });

MediaExtractor简介

MediaExtractor的主要作用时将音频和视频数据分离,主要api如下。

  • setDataSource(String path):设置文件地址,支持的类型挺多的。

  • getTrackCount():得到源文件通道数,包括音频和视频。

  • getTrackFormat(int i):获取指定的通道格式,包含其通道的很多配置信息。

  • readSampleData(ByteBuffer byteBuf, int offset):把制定通道的数据按偏移量读取到ByteBuffer中。

  • selectTrack(int i):选定特定的轨道,会影响 readSampleData(ByteBuffer, int), getSampleTrackIndex() and getSampleTime()的输出,这三个函数输出的是选定轨道的信息。(特别注意!!!)

  • advance():读取下一帧数据。

MediaMuxer简介

通过MediaExtractor得到数据,然后使用MediaMuxer可以将其单独封装成视频和音频文件,还能将音频和视频混合成一个音视频文件。其主要api如下:

  • MediaMuxer(String path, int format):初始化MediaMuxer,path为输出文件的名称,format为输出文件的格式。

  • addTrack(MediaFormat format):添加通道,通常使用Extractor.getTrackFormat(int index)来获取MediaFormat。

  • writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo

    bufferInfo):把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道。

视频解析与封装流程

解析和封装的关系可以说“焦不离孟,孟不离焦”,我们先将录制的视频解析封装成两个独立的文件,都可以播放的文件,然后在将两者混合成一个音视频文件。(当然这是一种比较学习的做法,通常是解析以后,直接将源数据封装成mp4文件)

1.初始化两个MediaExtractor,一个为videoExtractor,另一个为audioExtractor。

2.初始化MediaMuxer,设置其输出格式为:MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4。

3.使用videoExtractor获取视频文件的视频通道,使用audioExtractor获取音频文件的音频通道。

4.开始合成,设置Buffer。

5.通过mediaExtractor.readSampleData读取数据流,然后把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道。

6.封装合成完成,释放资源。

    @SuppressLint("WrongConstant")
    private void packageMp4(String videoPath, String audioPath){
         String outFile = Environment.getExternalStorageDirectory().getAbsolutePath()
                + "/Audio/v/2.mp4";

        MediaExtractor videoExtractor = new MediaExtractor();
        MediaExtractor audioExtractor = new MediaExtractor();

        try {
            MediaMuxer mediaMuxer = new MediaMuxer(outFile,MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            int videoTrack = -1;
            int audioTrack = -1;
            //添加视频
            videoExtractor.setDataSource(videoPath);
            for (int i=0;i<videoExtractor.getTrackCount();i++){
                String mineType = videoExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME);
                if (mineType.startsWith("video/")){
                    MediaFormat mediaFormat = videoExtractor.getTrackFormat(i);
                    videoExtractor.selectTrack(i);
                    videoTrack = mediaMuxer.addTrack(mediaFormat);
                }
            }

            //添加音频
            audioExtractor.setDataSource(audioPath);
            for (int i=0;i<videoExtractor.getTrackCount();i++){
                String mineType = audioExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME);
                if (mineType.startsWith("audio/")){
                    MediaFormat mediaFormat = audioExtractor.getTrackFormat(i);
                    audioTrack = mediaMuxer.addTrack(mediaFormat);
                    audioExtractor.selectTrack(i);
                }
            }

            mediaMuxer.start();
            ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);

            MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();
            //视频合成
            while (true) {
                int readSampleCount = videoExtractor.readSampleData(byteBuffer, 0);
                if (readSampleCount < 0) {
                    break;
                }
                videoBufferInfo.flags = videoExtractor.getSampleFlags();
                videoBufferInfo.offset = 0;
                videoBufferInfo.size = readSampleCount;
                videoBufferInfo.presentationTimeUs = videoExtractor.getSampleTime();
                //把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道
                mediaMuxer.writeSampleData(videoTrack,byteBuffer,videoBufferInfo);
                //读取下一帧
                videoExtractor.advance();
            }

            //视频合成
            MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();
            while (true) {
                int readSampleCount = audioExtractor.readSampleData(byteBuffer, 0);
                if (readSampleCount < 0) {
                    break;
                }
                audioBufferInfo.flags = audioExtractor.getSampleFlags();
                audioBufferInfo.offset = 0;
                audioBufferInfo.size = readSampleCount;
                audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
                //把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道
                mediaMuxer.writeSampleData(audioTrack,byteBuffer,audioBufferInfo);
                //读取下一帧
                audioExtractor.advance();
            }

            videoExtractor.release();
            audioExtractor.release();
            mediaMuxer.stop();
            mediaMuxer.release();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

注意

1.mediamuxer只支持aac格式的音频数据,不支持mp3,使用mp3时需要先使用ffmpeg截取和转换,当然这个我也不会。

2.使用这个mediaMuxer.writeSampleData方法时,一定要注意的一个参数trackIndex,这个参数在纯音频或纯视频时只有一个轨道。

结语

这期使用目前最用心的一期,从录制到预览到解析合成我走了很多弯路,还好有其他博客得以借鉴。其实我也偷了个懒,我的学习pdf里录制使用然后处理原始数据,但是它没有具体的代码可以参考,只提出了一个思路。

上班了,更新频率不会那么快。

本期博客参考:

灰色飘零博客园

知乎文章

需要源码的盆友也可以访问我的gitlub,源码

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-10-17 12:46:08  更:2022-10-17 12:47:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 22:22:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码