前语

周六准时发了工资。好好歇息了两天,出去消费了一波美滋滋

顺带有粉丝找我问有没有openGL ES比较深化的学习内容,和Recyclerview的.抽空弄一波。

前几天零零散散的更新了一些音视频的片段,今日就分为初中级三个方面来全面的剖析下,新更新的内容后续在说。

新增:Flutter番外篇:Flutter面试-项目实战-电子书;openGL ES深化版+Recyclerview

重视大众号:Android苦做舟
解锁 《Android十一大板块文档》
音视频大合集,从初中高到面试包罗万象;让学习更靠近未来实战。已形成PDF版

十一个模块内容如下

1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试包罗万象
3.Android车载运用大合集,从零开端一同学
4.功用优化大合集,离别优化烦恼
5.Framework大合集,从里到外剖析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对作业需求
10.Android根底篇大合集,根基安定高楼平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书

收拾不易,重视一下吧。开端进入正题,ღ( ・ᴗ・` )

一丶经过三种办法制作图片

在 Android 音视频开发学习思路里边,咱们写到了,想要逐步入门音视频开发,就需求一步步的去学习收拾,并积累。本文是音视频开发积累的第一篇。 对应的要学习的内容是:在 Android 平台制作一张图片,运用至少 3 种不同的 API,ImageView,SurfaceView,自定义 View。

ImageView 制作图片

这个想必做过Android开发的都知道怎样去制作了。很简略:

Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + File.separator + "11.jpg");
imageView.setImageBitmap(bitmap);

很轻松,在界面上看到了咱们制作的图片。

SurfaceView 制作图片

这个比 ImageView 制作图片略微杂乱一点点:

SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        if (surfaceHolder == null) {
            return;
        }
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + File.separator + "11.jpg");  // 获取bitmap
        Canvas canvas = surfaceHolder.lockCanvas();  // 先锁定当时surfaceView的画布
        canvas.drawBitmap(bitmap, 0, 0, paint); //执行制作操作
        surfaceHolder.unlockCanvasAndPost(canvas); // 解除锁定并显现在界面上
    }
    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
    }
});

自定义 View 制作图片

这个有制作自定义View经历的能够很轻松的完结,本人也简略收拾过 Android 自定义 View 制作这一块的常识:

public class CustomView extends View {
    Paint paint = new Paint();
    Bitmap bitmap;
    public CustomView(Context context) {
        super(context);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + File.separator + "11.jpg");  // 获取bitmap
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 不建议在onDraw做任何分配内存的操作
        if (bitmap != null) {
            canvas.drawBitmap(bitmap, 0, 0, paint);
        }
    }
}

注:别忘记了权限,不然是不会展现成功的。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

这三种办法都成功了展现出来了,咱们能够持续学习并收拾后边的常识了

二丶运用 AudioRecord 搜集音频PCM并保存到文件

AudioRecord API详解

AudioRecord是Android体系供给的用于完结录音的功用类。

要想了解这个类的详细的阐明和用法,咱们能够去看一下官方的文档:

AndioRecord类的首要功用是让各种JAVA运用能够办理音频资源,以便它们经过此类能够录制声响相关的硬件所搜集的声响。

此功用的完结便是经过”pulling”(读取)AudioRecord方针的声响数据来完结的。在录音进程中,运用所需求做的便是经过后边三个类办法中的一个去及时地获取AudioRecord方针的录音数据. AudioRecord类供给的三个获取声响数据的办法分别是read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int). 不管挑选运用那一个办法都有必要事先设定便运用户的声响数据的存储格局。 

开端录音的时分,AudioRecord需求初始化一个相关联的声响buffer, 这个buffer首要是用来保存新的声响数据。这个buffer的巨细,咱们能够在方针结构期间去指定。它标明一个AudioRecord方针还没有被读取(同步)声响数据前能录多长的音(即一次能够录制的声响容量)。声响数据从音频硬件中被读出,数据巨细不超越整个录音数据的巨细(能够分多次读出),即每次读取初始化buffer容量的数据。

完结Android录音的流程为

  • 结构一个AudioRecord方针,其间需求的最小录音缓存buffer巨细能够经过getMinBufferSize办法得到。假如buffer容量过小,将导致方针结构的失利。
  • 初始化一个buffer,该buffer大于等于AudioRecord方针用于写声响数据的buffer巨细。
  • 开端录音
  • 创立一个数据流,一边从AudioRecord中读取声响数据到初始化的buffer,一边将buffer中数据导入数据流。
  • 关闭数据流
  • 中止录音

运用 AudioRecord 完结录音,并生成wav

创立一个AudioRecord方针

首先要声明一些全局的变量参数:

private AudioRecord audioRecord = null;  // 声明 AudioRecord 方针
private int recordBufSize = 0; // 声明recoordBufffer的巨细字段

获取buffer的巨细并创立AudioRecord:

public void createAudioRecord() {
  recordBufSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, EncodingBitRate);  //audioRecord能承受的最小的buffer巨细
   audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, EncodingBitRate, recordBufSize);
}

初始化一个buffer

byte data[] = new byte[recordBufSize];

开端录音

audioRecord.startRecording();
isRecording = true;

创立一个数据流,一边从AudioRecord中读取声响数据到初始化的buffer,一边将buffer中数据导入数据流。

FileOutputStream os = null;
try {
    os = new FileOutputStream(filename);
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
if (null != os) {
    while (isRecording) {
        read = audioRecord.read(data, 0, recordBufSize);
      // 假如读取音频数据没有呈现过错,就将数据写入到文件
        if (AudioRecord.ERROR_INVALID_OPERATION != read) {
            try {
                os.write(data);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    try {
        os.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
} 

关闭数据流

修正标志位:isRecording 为false,上面的while循环就自动中止了,数据流也就中止流动了,Stream也就被关闭了。

isRecording = false;

中止录音

中止录音之后,留意要开释资源。

if (null != audioRecord) {
  audioRecord.stop();
   audioRecord.release();
  audioRecord = null;
   recordingThread = null;
}

注:权限需求:WRITE_EXTERNAL_STORAGE、RECORD_AUDIO

到现在根本的录音的流程就介绍完了。可是这时分,有人就提出问题来了:

1)、我依照流程,把音频数据都输出到文件里边了,中止录音后,翻开此文件,发现不能播映,到底是为什么呢?

答:依照流程走完了,数据是进去了,可是现在的文件里边的内容仅仅是最原始的音频数据,术语称为raw(中文解说是“原材料”或“未经处理的东西”),这时分,你让播映器去翻开,它既不知道保存的格局是什么,又不知道怎样进行解码操作。当然播映不了。

2)、那怎样才干在播映器中播映我录制的内容呢?

答: 在文件的数据最初加入WAVE HEAD数据即可,也便是文件头。只要加上文件头部的数据,播映器才干正确的知道里边的内容到底是什么,然后能够正常的解析并播映里边的内容。详细的头文件的描绘,在Play a WAV file on an AudioTrack里边能够进行了解。

添加WAVE文件头的代码如下:

public class PcmToWavUtil {
    /**
     * 缓存的音频巨细
     */
    private int mBufferSize;
    /**
     * 采样率
     */
    private int mSampleRate;
    /**
     * 声道数
     */
    private int mChannel;
    /**
     * @param sampleRate sample rate、采样率
     * @param channel channel、声道
     * @param encoding Audio data format、音频格局
     */
    PcmToWavUtil(int sampleRate, int channel, int encoding) {
        this.mSampleRate = sampleRate;
        this.mChannel = channel;
        this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding);
    }
    /**
     * pcm文件转wav文件
     *
     * @param inFilename 源文件路径
     * @param outFilename 方针文件路径
     */
    public void pcmToWav(String inFilename, String outFilename) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen;
        long totalDataLen;
        long longSampleRate = mSampleRate;
        int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
        long byteRate = 16 * mSampleRate * channels / 8;
        byte[] data = new byte[mBufferSize];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;
            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 加入wav文件头
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
        throws IOException {
        byte[] header = new byte[44];
        // RIFF/WAVE header
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        //WAVE
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        // 'fmt ' chunk
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        // 4 bytes: size of 'fmt ' chunk
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        // format = 1
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // block align
        header[32] = (byte) (2 * 16 / 8);
        header[33] = 0;
        // bits per sample
        header[34] = 16;
        header[35] = 0;
        //data
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }
}

附言

Android SDK 供给了两套音频搜集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个愈加上层一点的API,它能够直接把手机麦克风录入的音频数据进行编码紧缩(如AMR、MP3等)并存成文件,而后者则更挨近底层,能够愈加自由灵活地操控,能够得到原始的一帧帧PCM音频数据。假如想简略地做一个录音机,录制成音频文件,则引荐运用 MediaRecorder,而假如需求对音频做进一步的算法处理、或许选用第三方的编码库进行紧缩、以及网络传输等运用,则建议运用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。直播中实时搜集音频自然是要用AudioRecord了。

三丶运用 AudioTrack 播映PCM音频

AudioTrack 根本运用

AudioTrack 类能够完结Android平台上音频数据的输出使命。AudioTrack有两种数据加载方式(MODE_STREAM和MODE_STATIC),对应的是数据加载方式和音频流类型, 对应着两种完全不同的运用场景。

  • MODE_STREAM:在这种方式下,经过write一次次把音频数据写到AudioTrack中。这和平时经过write体系调用往文件中写数据相似,但这种作业办法每次都需求把数据从用户供给的Buffer中拷贝到AudioTrack内部的Buffer中,这在必定程度上会使引进延时。为解决这一问题,AudioTrack就引进了第二种方式。
  • MODE_STATIC:这种方式下,在play之前只需求把一切数据经过一次write调用传递到AudioTrack中的内部缓冲区,后续就不用再传递数据了。这种方式适用于像铃声这种内存占用量较小,延时要求较高的文件。但它也有一个缺点,便是一次write的数据不能太多,不然体系无法分配满意的内存来存储悉数数据。

MODE_STATIC方式

MODE_STATIC方式输出音频的办法如下(留意:假如选用STATIC方式,须先调用write写数据,然后再调用play。):

public class AudioTrackPlayerDemoActivity extends Activity implements
        OnClickListener {
    private static final String TAG = "AudioTrackPlayerDemoActivity";
    private Button button;
    private byte[] audioData;
    private AudioTrack audioTrack;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.setContentView(R.layout.main);
        this.button = (Button) super.findViewById(R.id.play);
        this.button.setOnClickListener(this);
        this.button.setEnabled(false);
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                try {
                    InputStream in = getResources().openRawResource(R.raw.ding);
                    try {
                        ByteArrayOutputStream out = new ByteArrayOutputStream(
                                264848);
                        for (int b; (b = in.read()) != -1;) {
                            out.write(b);
                        }
                        Log.d(TAG, "Got the data");
                        audioData = out.toByteArray();
                    } finally {
                        in.close();
                    }
                } catch (IOException e) {
                    Log.wtf(TAG, "Failed to read", e);
                }
                return null;
            }
            @Override
            protected void onPostExecute(Void v) {
                Log.d(TAG, "Creating track...");
                button.setEnabled(true);
                Log.d(TAG, "Enabled button");
            }
        }.execute();
    }
    public void onClick(View view) {
        this.button.setEnabled(false);
        this.releaseAudioTrack();
        this.audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
                AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT,
                audioData.length, AudioTrack.MODE_STATIC);
        Log.d(TAG, "Writing audio data...");
        this.audioTrack.write(audioData, 0, audioData.length);
        Log.d(TAG, "Starting playback");
        audioTrack.play();
        Log.d(TAG, "Playing");
        this.button.setEnabled(true);
    }
    private void releaseAudioTrack() {
        if (this.audioTrack != null) {
            Log.d(TAG, "Stopping");
            audioTrack.stop();
            Log.d(TAG, "Releasing");
            audioTrack.release();
            Log.d(TAG, "Nulling");
        }
    }
    public void onPause() {
        super.onPause();
        this.releaseAudioTrack();
    }
}

MODE_STREAM方式

MODE_STREAM 方式输出音频的办法如下:

byte[] tempBuffer = new byte[bufferSize];
int readCount = 0;
while (dis.available() > 0) {
    readCount = dis.read(tempBuffer);
    if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
        continue;
    }
    if (readCount != 0 && readCount != -1) {
        audioTrack.play();
        audioTrack.write(tempBuffer, 0, readCount);
    }
} 

AudioTrack 详解

音频流的类型

在AudioTrack结构函数中,会接触到AudioManager.STREAM_MUSIC这个参数。它的含义与Android体系对音频流的办理和分类有关。

Android将体系的声响分为好几种流类型,下面是几个常见的:

STREAM_ALARM:警告声

STREAM_MUSIC:音乐声,例如music等

STREAM_RING:铃声

STREAM_SYSTEM:体系声响,例如低电提示音,锁屏音等

STREAM_VOCIE_CALL:通话声

留意:上面这些类型的区分和音频数据自身并没有联系。例如MUSIC和RING类型都能够是某首MP3歌曲。别的,声响流类型的挑选没有固定的标准,例如,铃声预览中的铃声能够设置为MUSIC类型。音频流类型的区分和Audio体系对音频的办理战略有关。

Buffer分配和Frame的概念

在核算Buffer分配的巨细的时分,咱们常常用到的一个办法便是:getMinBufferSize。这个函数决议了运用层分配多大的数据Buffer。

AudioTrack.getMinBufferSize(8000,//每秒8K个采样点                              
        AudioFormat.CHANNEL_CONFIGURATION_STEREO,//双声道                  
        AudioFormat.ENCODING_PCM_16BIT);

从AudioTrack.getMinBufferSize开端追溯代码,能够发现在底层的代码中有一个很重要的概念:Frame(帧)。Frame是一个单位,用来描绘数据量的多少。1单位的Frame等于1个采样点的字节数声道数(比方PCM16,双声道的1个Frame等于22=4字节)。1个采样点只针对一个声道,而实践上或许会有一或多个声道。由于不能用一个独立的单位来表明悉数声道一次采样的数据量,也就引出了Frame的概念。Frame的巨细,便是一个采样点的字节数声道数。别的,在现在的声卡驱动程序中,其内部缓冲区也是选用Frame作为单位来分配和办理的。

下面是追溯到的native层的办法:

 // minBufCount表明缓冲区的最少个数,它以Frame作为单位
   uint32_t minBufCount = afLatency / ((1000 *afFrameCount)/afSamplingRate);
    if(minBufCount < 2) minBufCount = 2;//至少要两个缓冲
   //核算最小帧个数
   uint32_tminFrameCount =               
         (afFrameCount*sampleRateInHertz*minBufCount)/afSamplingRate;
  //下面依据最小的FrameCount核算最小的缓冲巨细   
   intminBuffSize = minFrameCount //核算办法完全符合咱们前面关于Frame的介绍
           * (audioFormat == javaAudioTrackFields.PCM16 ? 2 : 1)
           * nbChannels;
    returnminBuffSize;

getMinBufSize会综合考虑硬件的情况(诸如是否支撑采样率,硬件自身的推迟情况等)后,得出一个最小缓冲区的巨细。一般咱们分配的缓冲巨细会是它的整数倍。

AudioTrack结构进程

每一个音频流对应着一个AudioTrack类的一个实例,每个AudioTrack会在创立时注册到 AudioFlinger中,由AudioFlinger把一切的AudioTrack进行混合(Mixer),然后输送到AudioHardware中进行播映,现在Android一同最多能够创立32个音频流,也便是说,Mixer最多会一同处理32个AudioTrack的数据流。

音视频大合集,先从零开始万事开头难

AudioTrack 与 MediaPlayer 的比照

播映声响能够用MediaPlayer和AudioTrack,两者都供给了Java API供运用开发者运用。尽管都能够播映声响,但两者仍是有很大的差异的。

差异

其间最大的差异是MediaPlayer能够播映多种格局的声响文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创立对应的音频解码器。而AudioTrack只能播映现已解码的PCM流,假如比照支撑的文件格局的话则是AudioTrack只支撑wav格局的音频文件,由于wav格局的音频文件大部分都是PCM流。AudioTrack不创立解码器,所以只能播映不需求解码的wav文件。

联系

MediaPlayer在framework层仍是会创立AudioTrack,把解码后的PCM数撒播递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播映,所所以MediaPlayer包括了AudioTrack。

SoundPool

在接触Android音频播映API的时分,发现SoundPool也能够用于播映音频。下面是三者的运用场景:MediaPlayer 愈加合适在后台长时刻播映本地音乐文件或许在线的流式资源; SoundPool 则合适播映比较短的音频片段,比方游戏声响、按键声、铃声片段等等,它能够一同播映多个音频; 而 AudioTrack 则更挨近底层,供给了十分强壮的操控能力,支撑低推迟播映,合适流媒体和VoIP语音电话等场景。

四丶运用 Camera API 搜集视频数据

本文首要将的是:运用 Camera API 搜集视频数据并保存到文件,分别运用 SurfaceView、TextureView 来预览 Camera 数据,取到 NV21 的数据回调。

注: 需求权限

预览 Camera 数据

做过Android开发的人一般都知道,有两种办法能够做到这一点:SurfaceView、TextureView。

下面是运用SurfaceView预览数据的办法:

SurfaceView surfaceView;
Camera camera;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    surfaceView = (SurfaceView) findViewById(R.id.surface_view);
    surfaceView.getHolder().addCallback(this);
    // 翻开摄像头并将展现方向旋转90度
    camera = Camera.open();
    camera.setDisplayOrientation(90);
}
//------ Surface 预览 -------
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
    try {
        camera.setPreviewDisplay(surfaceHolder);
        camera.startPreview();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int w, int h) {
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
    camera.release();
}

下面是运用TextureView预览数据的办法:

    TextureView textureView;
    Camera camera;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textureView = (TextureView) findViewById(R.id.texture_view);
        textureView.setSurfaceTextureListener(this);// 翻开摄像头并将展现方向旋转90度
        camera = Camera.open();
        camera.setDisplayOrientation(90);
    }  //------ Texture 预览 -------
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
        try {
            camera.setPreviewTexture(surfaceTexture);
            camera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {
    }
    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        camera.release();
        return false;
    }
    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
    }

取到 NV21 的数据回调

Android 中Google支撑的 Camera Preview Callback的YUV常用格局有两种:一个是NV21,一个是YV12。Android一般默认运用YCbCr_420_SP的格局(NV21)。

咱们能够配置数据回调的格局:

Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);

经过setPreviewCallback办法监听预览的回调:

camera.setPreviewCallback(new Camera.PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] bytes, Camera camera) {
    }
});

这里边的Bytes的数据便是NV21格局的数据。

在后边的文章中,会对这些数据进行处理,来满意相关的需求场景。

一个音视频文件是由音频和视频组成的,咱们能够经过MediaExtractor、MediaMuxer把音频或视频给单独抽取出来,抽取出来的音频和视频能单独播映;

五丶运用 MediaExtractor 和 MediaMuxer API 解析和封装 mp4 文件

MediaExtractor API介绍

MediaExtractor的作用是把音频和视频的数据进行别离。

首要API介绍:

  • setDataSource(String path):即能够设置本地文件又能够设置网络文件
  • getTrackCount():得到源文件通道数
  • getTrackFormat(int index):获取指定(index)的通道格局
  • getSampleTime():回来当时的时刻戳
  • readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中;
  • advance():读取下一帧数据
  • release(): 读取完毕后开释资源

运用示例:

 MediaExtractor extractor = new MediaExtractor();
 extractor.setDataSource(...);
 int numTracks = extractor.getTrackCount();
 for (int i = 0; i < numTracks; ++i) {
   MediaFormat format = extractor.getTrackFormat(i);
   String mime = format.getString(MediaFormat.KEY_MIME);
   if (weAreInterestedInThisTrack) {
     extractor.selectTrack(i);
   }
 }
 ByteBuffer inputBuffer = ByteBuffer.allocate(...)
 while (extractor.readSampleData(inputBuffer, ...) >= 0) {
   int trackIndex = extractor.getSampleTrackIndex();
   long presentationTimeUs = extractor.getSampleTime();
   ...
   extractor.advance();
 }
 extractor.release();
 extractor = null;

MediaMuxer API介绍

MediaMuxer的作用是生成音频或视频文件;还能够把音频与视频混组成一个音视频文件。

相关API介绍:

  • MediaMuxer(String path, int format):path:输出文件的称号 format:输出文件的格局;当时只支撑MP4格局;
  • addTrack(MediaFormat format):添加通道;咱们更多的是运用MediaCodec.getOutpurForma()Extractor.getTrackFormat(int index)来获取MediaFormat;也能够自己创立;
  • start():开端组成文件
  • writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):把ByteBuffer中的数据写入到在结构器设置的文件中;
  • stop():中止组成文件
  • release():开释资源

运用示例:

MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
 // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
 // or MediaExtractor.getTrackFormat().
 MediaFormat audioFormat = new MediaFormat(...);
 MediaFormat videoFormat = new MediaFormat(...);
 int audioTrackIndex = muxer.addTrack(audioFormat);
 int videoTrackIndex = muxer.addTrack(videoFormat);
 ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
 boolean finished = false;
 BufferInfo bufferInfo = new BufferInfo();
 muxer.start();
 while(!finished) {
   // getInputBuffer() will fill the inputBuffer with one frame of encoded
   // sample from either MediaCodec or MediaExtractor, set isAudioSample to
   // true when the sample is audio data, set up all the fields of bufferInfo,
   // and return true if there are no more samples.
   finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
   if (!finished) {
     int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
     muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
   }
 };
 muxer.stop();
 muxer.release();

运用情境

从MP4文件中提取视频并生成新的视频文件

public class MainActivity extends AppCompatActivity {
    private static final String SDCARD_PATH = Environment.getExternalStorageDirectory().getPath();
    private MediaExtractor mMediaExtractor;
    private MediaMuxer mMediaMuxer;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 获取权限
        int checkWriteExternalPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
        int checkReadExternalPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);if (checkWriteExternalPermission != PackageManager.PERMISSION_GRANTED ||
                checkReadExternalPermission != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
        }
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    process();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    private boolean process() throws IOException {
        mMediaExtractor = new MediaExtractor();
        mMediaExtractor.setDataSource(SDCARD_PATH + "/ss.mp4");
        int mVideoTrackIndex = -1;
        int framerate = 0;
        for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) {
            MediaFormat format = mMediaExtractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (!mime.startsWith("video/")) {
                continue;
            }
            framerate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
            mMediaExtractor.selectTrack(i);
            mMediaMuxer = new MediaMuxer(SDCARD_PATH + "/ouput.mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            mVideoTrackIndex = mMediaMuxer.addTrack(format);
            mMediaMuxer.start();
        }
        if (mMediaMuxer == null) {
            return false;
        }
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        info.presentationTimeUs = 0;
        ByteBuffer buffer = ByteBuffer.allocate(500 * 1024);
        int sampleSize = 0;
        while ((sampleSize = mMediaExtractor.readSampleData(buffer, 0)) > 0) {
            info.offset = 0;
            info.size = sampleSize;
            info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
            info.presentationTimeUs += 1000 * 1000 / framerate;
            mMediaMuxer.writeSampleData(mVideoTrackIndex, buffer, info);
            mMediaExtractor.advance();
        }
        mMediaExtractor.release();
        mMediaMuxer.stop();
        mMediaMuxer.release();
        return true;
    }
}

六丶MediaCodec API 详解

在学习了Android 音视频的根本的相关常识,并收拾了相关的API之后,咱们应该对根本的音视频有必定的概括了。

下面开端接触一个Android音视频中适当重要的一个API: **MediaCodec。**经过这个API,咱们能够做很多Android音视频方面的作业,下面是咱们学习这个API的时分,首要的方向:

  • 学习 MediaCodec API,完结音频 AAC 硬编、硬解
  • 学习 MediaCodec API,完结视频 H.264 的硬编、硬解

MediaCodec 介绍

MediaCodec类能够用于运用一些根本的多媒体编解码器(音视频编解码组件),它是Android根本的多媒体支撑根底架构的一部分一般和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack 一同运用。

一个编解码器能够处理输入的数据来产生输出的数据,编解码器运用一组输入和输出缓冲器来异步处理数据。你能够创立一个空的输入缓冲区,填充数据后发送到编解码器进行处理。编解码器运用输入的数据进行转化,然后输出到一个空的输出缓冲区。最后你获取到输出缓冲区的数据,消耗掉里边的数据,开释回编解码器。假如后续还有数据需求持续处理,编解码器就会重复这些操作。输出流程如下:

音视频大合集,先从零开始万事开头难

编解码器支撑的数据类型:

编解码器能处理的数据类型为: **紧缩数据、原始音频数据和原始视频数据。**你能够经过ByteBuffers能够处理这三种数据,可是需求你供给一个Surface,用于对原始的视频数据进行展现,这样也能进步编解码的功用。Surface运用的是本地的视频缓冲区,这个缓冲区不映射或拷贝到ByteBuffers。这样的机制让编解码器的功率更高。一般在运用Surface的时分,无法访问原始的视频数据,可是你能够运用ImageReader访问解码后的原始视频帧。在运用ByteBuffer的方式下,您能够运用Image类和getInput/OutputImage(int)访问原始视频帧。

编解码器的生命周期:

  首要的生命周期为:Stopped、Executing、Released。

  • Stopped的状况下也分为三种子状况:Uninitialized、Configured、Error。
  • Executing的状况下也分为三种子状况:Flushed, Running、End-of-Stream。

下图是生命周期的阐明图:

音视频大合集,先从零开始万事开头难

如图能够看到:

  1. 当创立编解码器的时分处于未初始化状况。首先你需求调用configure(…)办法让它处于Configured状况,然后调用start()办法让其处于Executing状况。在Executing状况下,你就能够运用上面说到的缓冲区来处理数据。
  2. Executing的状况下也分为三种子状况:Flushed, Running、End-of-Stream。在start() 调用后,编解码器处于Flushed状况,这个状况下它保存着一切的缓冲区。一旦第一个输入buffer呈现了,编解码器就会自动运行到Running的状况。当带有end-of-stream标志的buffer进去后,编解码器会进入End-of-Stream状况,这种状况下编解码器不在承受输入buffer,可是仍然在产生输出的buffer。此刻你能够调用flush()办法,将编解码器重置于Flushed状况。
  3. 调用stop()将编解码器回来到未初始化状况,然后能够重新配置。 完结运用编解码器后,您有必要经过调用release()来开释它。
  4. 在极少数情况下,编解码器或许会遇到过错并转到过错状况。 这是运用来自排队操作的无效回来值或有时经过异常来传达的。 调用reset()使编解码器再次可用。 您能够从任何状况调用它来将编解码器移回未初始化状况。 不然,调用 release()动到终端开释状况。

MediaCodec API 阐明

MediaCodec能够处理详细的视频流,首要有这几个办法:

  • getInputBuffers:获取需求编码数据的输入流行列,回来的是一个ByteBuffer数组
  • queueInputBuffer:输入流入行列
  • dequeueInputBuffer:从输入流行列中取数据进行编码操作
  • getOutputBuffers:获取编解码之后的数据输出流行列,回来的是一个ByteBuffer数组
  • dequeueOutputBuffer:从输出行列中取出编码操作之后的数据
  • releaseOutputBuffer:处理完结,开释ByteBuffer数据

MediaCodec 流控

流控根本概念

流控便是流量操控。为什么要操控,由于条件有限! 触及到了 TCP 和视频编码:

对 TCP 来说便是操控单位时刻内发送数据包的数据量,对编码来说便是操控单位时刻内输出数据的数据量。

  • TCP 的约束条件是网络带宽,流控便是在防止形成或许加重网络拥塞的前提下,尽或许运用网络带宽。带宽够、网络好,咱们就加速速度发送数据包,呈现了推迟增大、丢包之后,就怠慢发包的速度(由于持续高速发包,或许会加重网络拥塞,反而发得更慢)。
  • 视频编码的约束条件最初是解码器的能力,码率太高就会无法解码,后来随着 codec 的发展,解码能力不再是瓶颈,约束条件变成了传输带宽/文件巨细,咱们希望在操控数据量的前提下,画面质量尽或许高。

一般编码器都能够设置一个方针码率,但编码器的实践输出码率不会完全符合设置,由于在编码进程中实践能够操控的并不是最终输出的码率,而是编码进程中的一个量化参数(Quantization Parameter,QP),它和码率并没有固定的联系,而是取决于图画内容。

不管是要发送的 TCP 数据包,仍是要编码的图画,都或许呈现“尖峰”,也便是短时刻内呈现较大的数据量。TCP 面对尖峰,能够挑选不为所动(尤其是网络现已拥塞的时分),这没有太大的问题,但假如视频编码也对尖峰不为所动,那图画质量就会大打折扣了。假如有几帧数据量特别大,但仍要把码率操控在原来的水平,那必然要损失更多的信息,因而图画失真就会更严重。

Android 硬编码流控

MediaCodec 流控相关的接口并不多,一是配置时设置方针码率和码率操控方式,二是动态调整方针码率(Android 19 版本以上)。

配置时指定方针码率和码率操控方式:

mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
mVideoCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

码率操控方式有三种:

  • CQ 表明完全不操控码率,尽最大或许保证图画质量;
  • CBR 表明编码器会尽量把输出码率操控为设定值,即咱们前面说到的“不为所动”;
  • VBR 表明编码器会依据图画内容的杂乱度(实践上是帧间变化量的巨细)来动态调整输出码率,图画杂乱则码率高,图画简略则码率低;

动态调整方针码率:

Bundle param = new Bundle();
param.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
mediaCodec.setParameters(param);

Android 流控战略挑选

  • 质量要求高、不在乎带宽、解码器支撑码率剧烈动摇的情况下,能够挑选 CQ 码率操控战略。
  • VBR 输出码率会在必定范围内动摇,关于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,假如无法承受这个问题,那 VBR 就不是好的挑选。
  • CBR 的优点是安稳可控,这样对实时性的保证有帮助。所以 WebRTC 开发中一般运用的是CBR。

七丶音视频录制流程总结

在前面咱们学习和运用了AudioRecord、AudioTrack、Camera、 MediaExtractor、MediaMuxer API、MediaCodec。 学习和运用了上述的API之后,信任对Android体系的音视频处理有必定的经历和心得了。本文及后边的几篇文章做的事情便是将这些常识串联起来,做一些略微杂乱的事情。

流程剖析

需求阐明

咱们需求做的事情便是:串联整个音视频录制流程,完结音视频的搜集、编码、封包成 mp4 输出。

完结办法

Android音视频搜集的办法:预览用SurfaceView,视频搜集用Camera类,音频搜集用AudioRecord。

数据处理思路

运用MediaCodec 类进行编码紧缩,视频紧缩为H.264,音频紧缩为aac,运用MediaMuxer 将音视频组成为MP4。

完结进程

搜集Camera数据,并转码为H264存储到文件

在搜集数据之前,对Camera设置一些参数,便利搜集后进行数据处理:

Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21);
parameters.setPreviewSize(1280, 720);

然后设置PreviewCallback:

camera.setPreviewCallback(this);

就能够获取到Camera的原始NV21数据:

onPreviewFrame(byte[] bytes, Camera camera)

在创立一个H264Encoder类,在里边进行编码操作,并将编码后的数据存储到文件:

new Thread(new Runnable() {
    @Override
    public void run() {
        isRuning = true;
        byte[] input = null;
        long pts = 0;
        long generateIndex = 0;
        while (isRuning) {
            if (yuv420Queue.size() > 0) {
                input = yuv420Queue.poll();
                byte[] yuv420sp = new byte[width * height * 3 / 2];
                // 有必要要转格局,不然录制的内容播映出来为绿屏
                NV21ToNV12(input, yuv420sp, width, height);
                input = yuv420sp;
            }
            if (input != null) {
                try {
                    ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
                    ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
                    int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
                    if (inputBufferIndex >= 0) {
                      pts = computePresentationTime(generateIndex);
                      ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                      inputBuffer.clear();
                      inputBuffer.put(input);
                      mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, System.currentTimeMillis(), 0);
                      generateIndex += 1;
                    }
                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                    int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                    while (outputBufferIndex >= 0) {
                        ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                        byte[] outData = new byte[bufferInfo.size];
                        outputBuffer.get(outData);
                        if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                            configbyte = new byte[bufferInfo.size];
                            configbyte = outData;
                        } else if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_SYNC_FRAME) {
                            byte[] keyframe = new byte[bufferInfo.size + configbyte.length];
                            System.arraycopy(configbyte, 0, keyframe, 0, configbyte.length);
                            System.arraycopy(outData, 0, keyframe, configbyte.length, outData.length);
                            outputStream.write(keyframe, 0, keyframe.length);
                        } else {
                            outputStream.write(outData, 0, outData.length);
                        }
                        mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                        outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                    }
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            } else {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        // 中止编解码器并开释资源
        try {
            mediaCodec.stop();
            mediaCodec.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 关闭数据流
        try {
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start(); 

当完毕编码的时分,需求将相关的资源开释掉:

// 中止编解码器并开释资源
try {
    mediaCodec.stop();
    mediaCodec.release();
} catch (Exception e) {
    e.printStackTrace();
}
// 关闭数据流
try {
    outputStream.flush();
    outputStream.close();
} catch (IOException e) {
    e.printStackTrace();
}

此刻,咱们做到了将视频内容搜集–>编码–>存储文件。但这个仅仅是Android 音视频开发(四):运用 Camera API 搜集视频数据的延伸,可是很有必要。由于在前面学习了怎样搜集音频,怎样运用MediaCodec去处理音视频,怎样运用MediaMuxer去混合音视频。

下面咱们在当时的的根底上持续完善,行将音视频搜集并混合为音视频。

音视频搜集+混合,存储到文件

八丶音视频同步简略介绍

音视频同步在音视频开发是有必定难度的。

本文首要描绘音视频同步原理,及常见的音视频同步方案,并以代码示例,展现怎样以音频的播映时长 为基准。

将视频同步到音频上以完结视音频的同步播映。内容如下:

1.音视频同步简略介绍
2.DTS和PTS简介 (I/P/B帧;时刻戳DTS、PTS)
3.常用同步战略
4.音视频同步简略示例代码

1.音视频同步简略介绍

关于一个播映器,一般来说,其根本构成均可区分为以下几部分:

数据接收(网络/本地)->解复用->音视频解码->音视频同步->音视频输出。

根本框架如下图所示:

音视频大合集,先从零开始万事开头难
为什么需求音视频同步? 媒体数据经过解复用流程后,音频/视频解码便是独立的,也是独立播映的。而在音频流和视频流中,其 播映速度都是有相关信息指定的:

视频:帧率,表明视频一秒显现的帧数。 音频:采样率,表明音频一秒播映的样本的个数。

音视频大合集,先从零开始万事开头难

从帧率及采样率,即可知道视频/音频播映速度

声卡和显卡均是以一帧数据来作为播映单位,假如单纯依靠帧率及采样率来进行播映,在理想条件下, 应该是同步的,不会呈现误差。

以一个44.1KHz的AAC音频流和24FPS的视频流为例:

  • 一个AAC音频frame每个声道包括1024个采样点,则一个frame的播映时长(duration)为:(1024/44100)1000ms = 23.22ms;
  • 一个视频frame播映时长(duration)为:1000ms/24 = 41.67ms。理想情况下,音视频完全同步

音视频播映进程如下图所示

音视频大合集,先从零开始万事开头难
但实践情况下,假如用上面那种简略的办法,渐渐的就会呈现音视频不同步的情况,要不是视频播映快 了,要么是音频播映快了。或许的原因如下:

一帧的播映时刻,难以精准操控。音视频解码及烘托的耗时不同,或许形成每一帧输出有一点细微差 距,长久累计,不同步便越来越显着。(例如受限于功用,42ms才干输出一帧)

音频输出是线性的,而视频输出或许对错线性,然后导致有误差。

媒体流自身音视频有距离。(特别是TS实时流,音视频能播映的第一个帧起点不同)

所以,解决音视频同步问题,引进了时刻戳: 首先挑选一个参阅时钟(要求参阅时钟上的时刻是线性递加的);编码时依据参阅时钟上的给每个音视频数据块都打上时刻戳;

播映时,依据音视频时刻戳及参阅时钟,来调整播映。

所以,视频和音频的同步实践上是一个动态的进程,同步是暂时的,不同步则是常态。以参阅时钟为标 准,放快了就减慢播映速度;播映快了就加速播映的速度。

接下来,咱们介绍媒体流中时刻戳的概念

2.DTS和PTS简介

I/P/B帧

在介绍DTS/PTS之前,咱们先了解I/P/B帧的概念。I/P/B帧自身和音视频同步联系不大,但了解其概念有 助于咱们了解DTS/PTS存在的含义。

视频本质上是由一帧帧画面组成,但实践运用进程中,每一帧画面会进行紧缩(编码)处理,已到达减 少空间占用的意图。

编码办法能够分为帧内编码和帧间编码。

内编码办法: 即只运用了单帧图画内的空间相关性,对冗余数据进行编码,到达紧缩作用,而没有运用时刻相关性, 不运用运动补偿。所以单靠自己,便能完好解码出一帧画面。

帧间编码

由于视频的特性,相邻的帧之间其实是很相似的,一般是运动矢量的变化。运用其时刻相关性,能够通 过参阅帧运动矢量的变化来猜测图画,并结合猜测图画与原始图画的差分,便能解码出原始图画。所 以,帧间编码需求依靠其他帧才干解码出一帧画面。 由于编码办法的不同,视频中的画面帧就分为了不同的类别,其间包括:I 帧、P 帧、B 帧。I 帧、P 帧、B 帧的差异在于:

  • I 帧(Intra coded frames):

I 帧图画选用帧I 帧运用帧内紧缩,不运用运动补偿,由于 I 帧不依靠其它帧,能够独立解码。I 帧图画的 紧缩倍数相对较低,周期性呈现在图画序列中的,呈现频率可由编码器挑选。

  • P 帧(Predicted frames):

P 帧选用帧间编码办法,即一同运用了空间和时刻上的相关性。P 帧图画只选用前向时刻猜测,能够提 高紧缩功率和图画质量。P 帧图画中能够包括帧内编码的部分,即 P 帧中的每一个宏块能够是前向预 测,也能够是帧内编码。

  • B 帧(Bi-directional predicted frames):

B 帧图画选用帧间编码办法,且选用双向时刻猜测,能够大大进步紧缩倍数。也便是其在时刻相关性 上,还依靠后边的视频帧,也正是由于 B 帧图画选用了后边的帧作为参阅,因而形成视频帧的传输次第 和显现次第是不同的。

也便是说,一个 I 帧能够不依靠其他帧就解码出一幅完好的图画,而 P 帧、B 帧不行。P 帧需求依靠视 频流中排在它前面的帧才干解码出图画。B 帧则需求依靠视频流中排在它前面或后边的I/P帧才干解码出 图画。

关于I帧和P帧,其解码次第和显现次第是相同的,但B帧不是,假如视频流中存在B帧,那么就会计划解 码和显现次第。

正由于解码和显现的这种非线性联系,所以需求DTS、PTS来标识帧的解码及显现时刻。

时刻戳DTS、PTS

DTS(Decoding Time Stamp):即解码时刻戳,这个时刻戳的含义在于告诉播映器该在什么时分解码 这一帧的数据。

PTS(Presentation Time Stamp):即显现时刻戳,这个时刻戳用来告诉播映器该在什么时分显现这一 帧的数据。

当视频流中没有 B 帧时,一般 DTS 和 PTS 的次第是共同的。但假如有 B 帧时,就回到了咱们前面说的 问题:解码次第和播映次第不共同了,即视频输出对错线性的。

比方一个视频中,帧的显现次第是:I B B P,由于B帧解码需求依靠P帧,因而这几帧在视频流中的次第 或许是:I P B B,这时分就体现出每帧都有 DTS 和 PTS 的作用了。DTS 告诉咱们该按什么次第解码这 几帧图画,PTS 告诉咱们该按什么次第显现这几帧图画。次第大约如下:

从流剖析东西看,流中P帧在B帧之前,但显现确实在B帧之后。

需求留意的是:尽管 DTS、PTS 是用于辅导播映端的行为,但它们是在编码的时分由编码器生成的。 以咱们最常见的TS为例: TS流中,PTS/DTS信息在打流阶段生成在PES层,首要是在PES头信息里。

标志第一位是PTS标识,第二位是DTS标识。

标志: 00,表明无PTS无DTS; 01,过错,不能只要DTS没有PTS; 10,有PTS; 11,有PTS和DTS。

PTS有33位,可是它不是直接的33位数据,而是占了5个字节,PTS分别在这5字节中取。

TS的I/P帧携带PTS/DTS信息,B帧PTS/DTS持平,进保留PTS;由于声响没有用到双向猜测,它的解码次第便是它的显现次第,故它只要PTS。

TS的编码器中有一个体系时钟STC(其频率是27MHz),此刻钟用来产生指示音视频的正确显现和解码

时刻戳。

PTS域在PES中为33bits,是对体系时钟的300分频的时钟的计数值。它被编码成为3个独立的字段: PTS[32…30][29…15][14…0]。

DTS域在PES中为33bits,是对体系时钟的300分频的时钟的计数值。它被编码成为3个独立的字段: DTS[32…30][29…15][14…0]。

因而,关于TS流,PTS/DTS时刻基均为1/90000秒(27MHz经过300分频)。

PTS关于TS流的含义不仅在于音视频同步,TS流自身不携带duration(可播映时长)信息,所以核算 duration也是依据PTS得到。

附上TS流解析PTS示例:

#define MAKE_WORD(h, l) (((h) << 8) | (l))
  //packet为PES
  int64_t get_pts(const uint8_t *packet) {
     const uint8_t *p = packet;
     if(packet == NULL) {
     return -1;
 }
if(!(p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01)) {
    //pes sync word
    return -1;
}
p += 3; //jump pes sync word
p += 4; //jump stream id(1) pes length(2) pes flag(1)
int pts_pts_flag = *p >> 6;
p += 2; //jump pes flag(1) pes header length(1)
if (pts_pts_flag & 0x02) {
    int64_t pts32_30, pts29_15, pts14_0, pts;
    pts32_30 = (*p) >> 1 & 0x07;
    p += 1;
    pts29_15 = (MAKE_WORD(p[0],p[1])) >> 1;
    p += 2;
    pts14_0 = (MAKE_WORD(p[0],p[1])) >> 1;
    p += 2;
    pts = (pts32_30 << 30) | (pts29_15 << 15) | pts14_0;
    return pts;
    }
return -1;
}

常用同步战略 前面现已说了,完结音视频同步,在播映时,需求选定一个参阅时钟,读取帧上的时刻戳,一同依据的 参阅时钟来动态调理播映。现在现已知道时刻戳便是PTS,那么参阅时钟的挑选一般来说有以下三种:

  • 将视频同步到音频上:便是以音频的播映速度为基准来同步视频。
  • 将音频同步到视频上:便是以视频的播映速度为基准来同步音频。
  • 将视频和音频同步外部的时钟上:挑选一个外部时钟为基准,视频和音频的播映速度都以该时钟为标 准。

当播映源比参阅时钟慢,则加速其播映速度,或许丢掉;快了,则推迟播映。

这三种是最根本的战略,考虑到人对声响的敏感度要强于视频,频频调理音频会带来较差的观感体验, 且音频的播映时钟为线性增加,所以一般会以音频时钟为参阅时钟,视频同步到音频上。

在实践运用基于这三种战略做一些优化调整,例如: 调整战略能够尽量选用渐进的办法,由于音视频同步是一个动态调理的进程,一次调整让音视频PTS完 全共同,没有必要,且或许导致播映异常较为显着。

调整战略仅仅对早到的或晚到的数据块进行推迟或加速处理,有时分是不够的。假如想要愈加自动而且 有效地调理播映功用,需求引进一个反应机制,也便是要将当时数据流速度太快或太慢的状况反应给 “源”,让源去怠慢或加速数据流的速度。

关于起播阶段,特别是TS实时流,由于视频解码需求依靠第一个I帧,而音频是能够实时输出,或许呈现 的情况是视频PTS超前音频PTS较多,这种情况下进行同步,必然形成较为显着的慢同步。处理这种情况 的较好办法是将较为剩余的音频数据丢掉,尽量减少起播阶段的音视频距离。

音视频同步简略示例代码

代码参阅ffplay完结办法,一同加入自己的修正。以audio为参阅时钟,video同步到音频的示例代码: 获取当时要显现的video PTS,减去上一帧视频PTS,则得出上一帧视频应该显现的时长delay; 当时video PTS与参阅时钟当时audio PTS比较,得出音视频距离diff;获取同步阈值sync_threshold,为一帧视频距离,范围为10ms-100ms; diff小于sync_threshold,则以为不需求同步;不然delay+diff值,则是正确纠正delay;

假如超越sync_threshold,且视频落后于音频,那么需求减小delay(FFMAX(0, delay + diff)),让当时 帧赶快显现。

假如视频落后超越1秒,且之前10次都快速输出视频帧,那么需求反应给音频源减慢,一同反应视频源 进行丢帧处理,让视频赶快追上音频。由于这很或许是视频解码跟不上了,再怎样调整delay也没用。

假如超越sync_threshold,且视频快于音频,那么需求加大delay,让当时帧推迟显现。

将delay*2渐渐调整距离,这是为了平缓调整距离,由于直接delay+diff,会让画面画面迟滞。

假如视频前一帧自身显现时刻很长,那么直接delay+diff一步调整到位,由于这种情况再渐渐调整也没太大含义。

考虑到烘托的耗时,还需进行调整。frame_timer为一帧显现的体系时刻,frame_timer+delay- curr_time,则得出正在需求推迟显现当时帧的时刻。

video->frameq.deQueue(&video->frame);
   //获取上一帧需求显现的时长delay
   double current_pts = *(double *)video->frame->opaque;
   double delay = current_pts - video->frame_last_pts;
      if (delay <= 0 || delay >= 1.0) {
     delay = video->frame_last_delay;
   }
}
  // 依据视频PTS和参阅时钟调整delay
  double ref_clock = audio->get_audio_clock();
  double diff = current_pts - ref_clock;// diff < 0 :video slow,diff > 0 :video fast
  //一帧视频时刻或10ms,10ms音视频距离无法察觉
  double sync_threshold = FFMAX(MIN_SYNC_THRESHOLD, FFMIN(MAX_SYNC_THRESHOLD,delay)) ;
  audio->audio_wait_video(current_pts,false);
  video->video_drop_frame(ref_clock,false);
  if (!isnan(diff) && fabs(diff) < NOSYNC_THRESHOLD) // 不同步 {
      if (diff <= -sync_threshold)//视频比音频慢,加速 {
          delay = FFMAX(0, delay + diff);
          static int last_delay_zero_counts = 0;
          if(video->frame_last_delay <= 0) {
             last_delay_zero_counts++;
          } else {
             last_delay_zero_counts = 0;
          }
          if(diff < -1.0 && last_delay_zero_counts >= 10) {
             printf("maybe video codec too slow, adjust video&audio\n"); 
             #ifndef DORP_PACK
             audio->audio_wait_video(current_pts,true);//距离较大,需求反应音频等待视频
             #endif
             video->video_drop_frame(ref_clock,true);//距离较大,需求视频丢帧追上
          }
      }
      //视频比音频快,减慢
      else if (diff >= sync_threshold && delay > SYNC_FRAMEDUP_THRESHOLD)
          delay = delay + diff;//音视频距离较大,且一帧的超越帧最常时刻,一步到位
      else if (diff >= sync_threshold)
          delay = 2 * delay;//音视频距离较小,加倍推迟,逐步缩小
  }
  video->frame_last_delay = delay;
  video->frame_last_pts = current_pts;
  double curr_time = static_cast<double>(av_gettime()) / 1000000.0;
  if(video->frame_timer == 0)  {
      video->frame_timer = curr_time;//show first frame ,set frame timer
  }
  double actual_delay = video->frame_timer + delay - curr_time;
  if (actual_delay <= MIN_REFRSH_S) {
      actual_delay = MIN_REFRSH_S;
  }
  usleep(static_cast<int>(actual_delay * 1000 * 1000));
  //printf("actual_delay[%lf] delay[%lf] diff[%lf]\n",actual_delay,delay,diff);
  // Display
  SDL_UpdateTexture(video->texture, &(video>rect), video->frame->data[0], video->frame->linesize[0]);
  SDL_RenderClear(video->renderer);
  SDL_RenderCopy(video->renderer, video->texture, &video->rect, &video->rect);
  SDL_RenderPresent(video->renderer);
  video->frame_timer = static_cast<double>(av_gettime()) / 1000000.0 ;
  av_frame_unref(video->frame);
  //update next frame
  schedule_refresh(media, 1);
  }

九丶视频变速

1 SoundTouch详解

是一个用C++编写的开源的音频处理库,能够改动音频文件或实时音频流的节拍(Tempo)、腔调(Pitch)、 回放率(Playback Rates),还支撑预算音轨的安稳节拍率(BPM rate)。ST的3个作用互相独立,也能够一 起运用。这些作用经过采样率转化、时刻拉伸结合完结。

  • Tempo节拍 :经过拉伸时刻,改动声响的播映速率而不影响腔调。
  • Playback Rate回放率 : 以不同的转率播映唱片(DJ打碟?),经过采样率转化完结。
  • Pitch腔调 :在保持节拍不变的前提下改动声响的腔调,结合采样率转化+时刻拉伸完结。如:增高腔调的处理进程是:将原音频拉伸时长,再经过采样率转化,一同减少时长与增高腔调变为原时长。

2 处理方针

ST处理的方针是PCM(Pulse Code Modulation,脉冲编码调制),.wav文件中首要是这种格局,因而ST的示例都是处理wav音频。mp3等格局经过了紧缩,需转化为PCM后再用ST处理。

3.首要特性

  • 易于完结:ST为一切支撑gcc编译器或许visual Studio的处理器或操作体系进行了编译,支撑Windows、Mac OS、Linux、Android、Apple iOS等。
  • 完全开源:ST库与示例工程完全开源可下载
  • 简单运用:编程接口运用单一的C++类
  • 支撑16位整型或32位浮点型的单声道、立体声、多通道的音频格局
  • 可完结实时音频流处理:
    • 输入/输出推迟约为100ms
    • 实时处理44.1kHz/16bit的立体声,需求133Mhz英特尔奔腾处理器或更好

4.相关

官网供给了ST的可执行程序、C++源码、阐明文档、不同操作体系的示例工程,几个重要链接:

  • SoundTouch官网
  • ST处理作用预览(SoundStretch是官网用ST库完结的处理WAV音频的东西)
  • 源码编译办法、算法以及参数阐明
  • 常见问题(如实时处理)

5 Android中怎样运用SoundTouch

Android中运用ST,需将ST的C++代码运用NDK编译为.so库,再经过JNI调用。参阅:SoundTouch in Android

6.下载源码

下载:soundtouch-1.9.2.zip ,包括ST的C++源码、Android-lib示例工程。

音视频大合集,先从零开始万事开头难
copy头文件 和 库文件 直接用AndroidStudio编译

7.调用接口与参数

示例工程中的SoundTouch.cpp是ST的调用接口,腔调、音速的变化是经过为ST设置新的参数,这些参 数需在正式开端处理前设置好。接口的调用示例能够参阅soundtouch-jni.cpp中的_processFile函数。

采样

  • setChannels(int) 设置声道,1 = mono单声道, 2 = stereo立体声
  • setSampleRate(uint) 设置采样率

速率

  • setRate(double) 指定播映速率,原始值为1.0,大快小慢 setTempo(double) 指定节拍,原始值为1.0,大快小慢
  • setRateChange(double) 、
  • setTempoChange(double) 在原速1.0根底上,按百分比做增量,取值(-50 .. +100 %)

腔调

  • setPitch(double) 指定腔调值,原始值为1.0
  • setPitchOctaves(double) 在原腔调根底上以八度音为单位进行调整,取值为[-1.00,+1.00]
  • setPitchSemiTones(int) 在原腔调根底上以半音为单位进行调整,取值为[-12,+12]

以上调音函数依据乐理进行单位换算,最后进入相同的处理流程calcEffectiveRateAndTempo()。三个函 数对参数没有上下界限约束,仅仅参数过大失真越大。SemiTone指半音,一般说的“降1个key”便是降低 1个半音。所以我以为运用SemiTone为单位即可满意需求,而且简单了解。

处理

  • putSamples(const SAMPLETYPE *samples, uint nSamples) 输入采样数据
  • receiveSamples(SAMPLETYPE *output, uint maxSamples) 输出处理后的数据,需求循环执 行
  • flush() 冲出处理管道中的最后一组“残留”的数据,应在最后执行

8.SoundTouch实时处理音频流

ST对音频的处理是输入函数putSamples()与输出函数receiveSamples()。实时处理音频流的思路便是, 循环读取音频数据段,放入ST进行输出,输出处理后的数据段用于播映。

由于业务要求运用Android的AudioEffect机制完结变调处理,得空后再尝试以JNI方式直接处理音频数据 的工程。

新增:Flutter番外篇:Flutter面试-项目实战-电子书;openGL ES深化版+Recyclerview

重视大众号:Android苦做舟
解锁 《Android十一大板块文档》
音视频大合集,从初中高到面试包罗万象;让学习更靠近未来实战。已形成PDF版

十一个模块内容如下

1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试包罗万象
3.Android车载运用大合集,从零开端一同学
4.功用优化大合集,离别优化烦恼
5.Framework大合集,从里到外剖析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对作业需求
10.Android根底篇大合集,根基安定高楼平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书

收拾不易,重视一下吧。ღ( ・ᴗ・` )