《Android音視頻系列-5》音視頻采集,生成mp4

最近晚上和周末基本都在排隊(duì)練車伏伐,累成狗,好久沒寫文章了~

抽空整理了一下音視頻采集的方式晕拆,最終生成mp4藐翎。

一、音頻采集,得到PCM數(shù)據(jù)

音頻采集比較簡單实幕,通過 AudioRecord 錄音吝镣,然后在子線程不斷去讀PCM數(shù)據(jù)

記得聲明錄音權(quán)限 <uses-permission android:name="android.permission.RECORD_AUDIO" />

開始錄音

    //默認(rèn)參數(shù)
    private static final int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    private static final int SAMPLE_RATE = 44100;
    private static final int CHANNEL_CONFIGS = AudioFormat.CHANNEL_IN_STEREO;
    private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    private int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);

    private AudioRecord audioRecord;

    public void start() {
        start(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);
    }

    public void start(int audioSource, int sampleRate, int channels, int audioFormat) {
        if (isStartRecord) {
            Log.d(TAG, "音頻錄制已經(jīng)開啟");
            return;
        }

        bufferSize = AudioRecord.getMinBufferSize(sampleRate, channels, audioFormat);

        if (bufferSize == AudioRecord.ERROR_BAD_VALUE) {
            Log.d(TAG, "無效參數(shù)");
            return;
        }

        audioRecord = new AudioRecord(audioSource, sampleRate, channels, audioFormat, bufferSize);
        audioRecord.startRecording();
        isStopRecord = false;

        threadCapture = new Thread(new CaptureRunnable());
        threadCapture.start();
    }

主要是創(chuàng)建AudioRecord的一些參數(shù),然后調(diào)用audioRecord.startRecording();開始錄制昆庇,然后啟動(dòng)一個(gè)子線程末贾,去讀取錄制的PCM格式的數(shù)據(jù)

讀取PCM

 /**
 * 子線程讀取采集到的PCM數(shù)據(jù)
 */
private class CaptureRunnable implements Runnable {

    @Override
    public void run() {
        while (!isStopRecord) {
            byte[] buffer = new byte[bufferSize];
            int readRecord = audioRecord.read(buffer, 0, bufferSize);
            if (readRecord &gt; 0) {
                if (captureListener != null)
                    captureListener.onCaptureListener(buffer,readRecord);
                Log.d(TAG, "音頻采集數(shù)據(jù)源 -- ".concat(String.valueOf(readRecord)).concat(" -- bytes"));
            } else {
                Log.d(TAG, "錄音采集異常");
            }
            //延遲寫入 SystemClock  --  Android專用
            SystemClock.sleep(10);
        }
    }
}

讀取PCM 比較簡單,就是通過audioRecord.read(buffer, 0, bufferSize)整吆,最終PCM格式的數(shù)據(jù)會(huì)讀到這個(gè)buffer里

拿到錄制的每一幀PCM數(shù)據(jù)之后拱撵,可以用AudioTrack播放辉川,這里就不播放了,回調(diào)出去拴测,后面合成mp4要用到乓旗。

上面兩個(gè)步驟,可以封裝一個(gè)錄音的管理類集索。
【傳送門】(待補(bǔ)充)

現(xiàn)在獲取的音頻是PCM格式屿愚,我們要將它編碼成aac,然后跟視頻數(shù)據(jù)合成mp4务荆,這里要用到 MediaCodecMediaMuxer

MediaCodec 使用

MediaCodec 是一個(gè)音視頻編解碼器妆距,本篇主要用于:

  1. 將PCM格式的音頻數(shù)據(jù)編碼成aac格式,
  2. 將NV21格式的相機(jī)預(yù)覽數(shù)據(jù)編碼成avc格式函匕。

API 簡介

getInputBuffers:獲取需要編碼數(shù)據(jù)的輸入流隊(duì)列娱据,返回的是一個(gè)ByteBuffer數(shù)組
queueInputBuffer:輸入流入隊(duì)列
dequeueInputBuffer:從輸入流隊(duì)列中取數(shù)據(jù)進(jìn)行編碼操作
getOutputBuffers:獲取編解碼之后的數(shù)據(jù)輸出流隊(duì)列,返回的是一個(gè)ByteBuffer數(shù)組
dequeueOutputBuffer:從輸出隊(duì)列中取出編碼操作之后的數(shù)據(jù)
releaseOutputBuffer:處理完成浦箱,釋放ByteBuffer數(shù)據(jù)

初始化音頻編解碼器

    private MediaCodec mAudioCodec;

    String audioType = MediaFormat.MIMETYPE_AUDIO_AAC;  //編碼成aac格式
    int sampleRate = 44100;
    int channels = 2;//單聲道 channelCount=1 , 雙聲道  channelCount=2

    private void initAudioCodec(String audioType, int sampleRate, int channels) {
    try {
        mAudioCodec = MediaCodec.createEncoderByType(audioType);
        MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channels);
        int BIT_RATE = 96000;
        audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
        audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
                MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        int MAX_INOUT_SIZE = 8192;
        audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_INOUT_SIZE);

        mAudioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

    } catch (IOException e) {
        Log.e(TAG, "initAudioCodec: 音頻類型無效");
    }
}

視頻編解碼器的初始化同理

    String videoType = MediaFormat.MIMETYPE_VIDEO_AVC;

    private void initVideoCodec(String videoType, int width, int height) {
    try {
        mVideoCodec = MediaCodec.createEncoderByType(videoType);
        MediaFormat videoFormat = MediaFormat.createVideoFormat(videoType, width, height);

        videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        //MediaFormat.KEY_FRAME_RATE -- 可通過Camera#Parameters#getSupportedPreviewFpsRange獲取
        videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        //mSurfaceWidth*mSurfaceHeight*N  N標(biāo)識碼率低吸耿、中、高酷窥,類似可設(shè)置成1咽安,3,5蓬推,碼率越高視頻越大妆棒,也越清晰
        videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 4);
        //每秒關(guān)鍵幀數(shù)
        videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

        if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.N) {
            videoFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
            videoFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31);
        }

        mVideoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //注意這里,獲取視頻解碼器的surface沸伏,之后要將opengl輸出到這個(gè)surface中
        mSurface = mVideoCodec.createInputSurface();
    } catch (IOException e) {
        Log.e(TAG, "initVideoCodec: 視頻類型無效");
    }
}

這里要注意的是糕珊,音頻數(shù)據(jù)從AudioRecord 直接讀出來,但是視頻數(shù)據(jù)處理有些不同毅糟,視頻數(shù)據(jù)不直接讀相機(jī)預(yù)覽红选,而是通過相機(jī)紋理id,利用OpenGL直接渲染到視頻編解碼器的surface上姆另,可以直接被編碼喇肋,效率比較高,所以這里獲取了MediaCodec的surface迹辐,mSurface = mVideoCodec.createInputSurface();蝶防,后面通過EGL創(chuàng)建NatieWindow的時(shí)候要用到,這里大概先了解一下就行明吩,關(guān)于GLSurfaceView原理间学,EGL的使用,后面再整理一篇文章吧。

兩個(gè)編解碼器創(chuàng)建好了低葫,接下來要用到混合器详羡, MediaMuxer

MediaMuxer 使用詳解

MediaMuxer 是一個(gè)音視頻混合器,我們錄制音頻和視頻數(shù)據(jù)氮采,經(jīng)過MediaCodec編碼殷绍,然后再將編碼后的音視頻數(shù)據(jù)混合在一起,最終生成mp4鹊漠。

MediaMuxer主要方法:

1.int addTrack(@NonNull MediaFormat format)

一個(gè)視頻文件是包含一個(gè)或多個(gè)音視頻軌道的主到,而這個(gè)方法就是用于添加一個(gè)音頻頻或視頻軌道,并返回對應(yīng)的ID躯概。之后我們可以通過這個(gè)ID向相應(yīng)的軌道寫入數(shù)據(jù)登钥。用于新建音視頻軌道的MediaFormat是需要從MediaCodec.getOutputFormat()獲取的,而不是自己簡單構(gòu)造的MediaFormat娶靡。

2.start()

當(dāng)我們添加完所有音視頻軌道之后牧牢,需要調(diào)用這個(gè)方法告訴Muxer,我要開始寫入數(shù)據(jù)了姿锭。需要注意的是塔鳍,調(diào)用了這個(gè)方法之后,我們是無法再次addTrack了的呻此。

3.void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
@NonNull BufferInfo bufferInfo)

用于向Muxer寫入編碼后的音視頻數(shù)據(jù)轮纫。trackIndex是我們addTrack的時(shí)候返回的ID,byteBuf便是要寫入的數(shù)據(jù)焚鲜,而bufferInfo是跟這一幀byteBuf相關(guān)的信息掌唾,包括時(shí)間戳、數(shù)據(jù)長度和數(shù)據(jù)在ByteBuffer中的位移

4.void stop()

與start()相對應(yīng)忿磅,用于停止寫入數(shù)據(jù)糯彬,并生成文件。

5.void release()

釋放Muxer資源葱她。

MediaMuxer 實(shí)戰(zhàn)

我們先來構(gòu)造一個(gè) MediaMuxer 撩扒,需要兩個(gè)參數(shù),第一個(gè)是音視頻文件的保存路徑吨些,第二個(gè)是音視頻封裝文件的格式搓谆,可以選擇mp4或3gp,我們使用mp4就好

int mediaFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;

private void initMediaMuxer(String filePath, int mediaFormat) {
    try {
        mMediaMuxer = new MediaMuxer(filePath, mediaFormat);
    } catch (IOException e) {
        Log.e(TAG, "initMediaMuxer: 文件打開失敗,path=" + filePath);
    }
}

添加音頻軌道

添加音頻軌道锤灿,在音頻編碼線程 AudioCodecThread 處理

public void run() {
    super.run();
    mIsStop = false;
    audioCodec.start();
    while (true) {
        if (mMediaEncodeManager == null) {
            Log.e(TAG, "run: mediaEncodeManagerWeakReference == null");
            return;
        }
        if (mIsStop) {
            mMediaEncodeManager.audioStop();
            return;
        }

        //獲取一幀解碼完成的數(shù)據(jù)到bufferInfo挽拔,沒有數(shù)據(jù)就阻塞
        int outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
        //第一次會(huì)返回-2辆脸,在這時(shí)候添加音軌
        if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            mAudioTrackIndex = mediaMuxer.addTrack(audioCodec.getOutputFormat());
            mMediaEncodeManager.mAudioTrackReady  = true;
            Log.d(TAG, "run: 添加音軌 mAudioTrackIndex= " + mAudioTrackIndex);
            mMediaEncodeManager.startMediaMuxer();
        } else {
            while (outputBufferIndex != 0) {
                if (!mMediaEncodeManager.mEncodeStart) {
                    Log.d(TAG, "run: 混合器還沒開始但校,線程延遲");
                    SystemClock.sleep(10);
                    continue;
                }

                ByteBuffer outputBuffer = audioCodec.getOutputBuffers()[outputBufferIndex];
                outputBuffer.position(bufferInfo.offset);
                outputBuffer.limit(bufferInfo.offset + bufferInfo.size);

                if (mPresentationTimeUs == 0) {
                    mPresentationTimeUs = bufferInfo.presentationTimeUs;
                }
                bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
                mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);

                audioCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
            }
        }
    }
}

添加視頻軌道

添加視頻軌道,在視頻編碼線程 VideoCodecThread 處理

public void run() {
    mIsStop = false;
    videoCodec.start();
    while (true) {
        if (mMediaEncodeManager == null) {
            Log.e(TAG, "run: mMediaEncodeManager == null");
            return;
        }
        if (mIsStop) {
            mMediaEncodeManager.videoStop();
            return;
        }

        int outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
        //第一次返回 -2啡氢,在這個(gè)時(shí)候添加音軌
        if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            mVideoTrackIndex = mediaMuxer.addTrack(videoCodec.getOutputFormat());
            Log.d(TAG, "添加視頻軌道状囱,mVideoTrackIndex = " + mVideoTrackIndex);
            mMediaEncodeManager.mVideoTrackReady = true;
            mMediaEncodeManager.startMediaMuxer();
        } else {
            while (outputBufferIndex &gt;= 0) {
                if (!mMediaEncodeManager.mEncodeStart) {
                    Log.d(TAG, "run: 混合器還沒開始术裸,線程延遲");
                    SystemClock.sleep(10);
                    continue;
                }

                ByteBuffer outputBuffer = videoCodec.getOutputBuffers()[outputBufferIndex];
                outputBuffer.position(bufferInfo.offset);
                outputBuffer.limit(bufferInfo.offset + bufferInfo.size);

                if (mPresentationTimeUs == 0) {
                    mPresentationTimeUs = bufferInfo.presentationTimeUs;
                }
                bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
                mediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
                if (bufferInfo != null) {
                    mMediaEncodeManager.onRecordTimeCallBack((int) (bufferInfo.presentationTimeUs / 1000000));
                }
                videoCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
            }
        }
    }
}

添加音頻軌道和視頻軌道后,就可以啟動(dòng)混合器亭枷,然后不斷從編解碼器MediaCodec中讀取已經(jīng)編碼成功的數(shù)據(jù)袭艺,然后調(diào)用mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);將編碼后的音/視頻數(shù)據(jù)寫到混合器里,停止的時(shí)候要調(diào)用

mMediaMuxer.stop();
mMediaMuxer.release();

如果不出意外的話叨粘,會(huì)在指定目錄下生成mp4文件猾编。

然后在PCM回調(diào)那里,將PCM數(shù)據(jù)扔到 MediaCodec 里面去升敲,這樣AudioCodecThread 里面就能讀到已經(jīng)編碼的aac格式數(shù)據(jù)答倡。

    public void setPcmSource(byte[] pcmBuffer, int buffSize) {

    try {

        int buffIndex = mAudioCodec.dequeueInputBuffer(0);
        if (buffIndex &lt; 0) {
            return;
        }
        ByteBuffer byteBuffer;
        if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.LOLLIPOP) {
            byteBuffer = mAudioCodec.getInputBuffer(buffIndex);
        } else {
            byteBuffer = mAudioCodec.getInputBuffers()[buffIndex];
        }

        byteBuffer.clear();
        byteBuffer.put(pcmBuffer);
        //mPresentationTimeUs = 1000000L * (buffSize / 2) / mSampleRate
        //一幀音頻幀大小 int size = 采樣率 x 位寬 x 采樣時(shí)間 x 通道數(shù)
        // 1s時(shí)間戳計(jì)算公式  mPresentationTimeUs = 1000000L * (totalBytes / mSampleRate/ mAudioFormat / mChannelCount / 8 )
        //totalBytes : 傳入編碼器的總大小
        //1000 000L : 單位為 微秒,換算后 = 1s,
        //除以8     : pcm原始單位是bit, 1 byte = 8 bit, 1 short = 16 bit, 用 Byte[]驴党、Short[] 承載則需要進(jìn)行換算
        mPresentationTimeUs += (long) (1.0 * buffSize / (mSampleRate * mChannelCount * (mAudioFormat / 8)) * 1000000.0);
        Log.d(TAG, "pcm一幀時(shí)間戳 = " + mPresentationTimeUs / 1000000.0f);
        mAudioCodec.queueInputBuffer(buffIndex, 0, buffSize, mPresentationTimeUs, 0);
    } catch (IllegalStateException e) {
        //mAudioCodec 線程對象已釋放MediaCodec對象
        Log.d(TAG, "setPcmSource: " + "MediaCodec對象已釋放");
    }
}

視頻數(shù)據(jù)通過OpenGL渲染到視頻編解碼器的surface中瘪撇,只要打開相機(jī),視頻編碼器就能獲取到編碼后的視頻數(shù)據(jù)港庄,然后寫到混合器里倔既,跟音頻處理基本差不多。當(dāng)然鹏氧,這里涉及到自定義GLSurfaceView渤涌,參照GLSurfaceView中對EGL的處理,自己寫EglHelper度帮,這里不是本文重點(diǎn)歼捏,后面有時(shí)間再說下GLSurfaceView源碼。

接下來再簡單看一下如何通過Camera1采集視頻數(shù)據(jù)

視頻數(shù)據(jù)采集

相機(jī)功能封裝在 CameraManager中笨篷,使用的是Camera1瞳秽,需要注意的是設(shè)置預(yù)覽數(shù)據(jù)格式,還有一個(gè)是SurfaceTexture率翅,在外部創(chuàng)建(OpenGL創(chuàng)建紋理的時(shí)候)练俐,然后再啟動(dòng)相機(jī),把紋理傳過去冕臭,簡單貼下啟動(dòng)相機(jī)代碼

    private void startCamera(int cameraId) {
    try {
        camera = Camera.open(cameraId);
        camera.setPreviewTexture(surfaceTexture);

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

        //設(shè)置對焦模式腺晾,后置攝像頭開啟時(shí)打開,切換到前置時(shí)關(guān)閉(三星辜贵、華為不能設(shè)置前置對焦,魅族悯蝉、小米部分機(jī)型可行)
        if (cameraId == 0) {
            //小米、魅族手機(jī)存在對焦無效情況托慨,需要針對設(shè)備適配鼻由,想要無感知對焦完全適配最好是監(jiān)聽加速度傳感器
            camera.cancelAutoFocus();
            //這種設(shè)置方式存在屏幕閃爍一下問題,包括Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        }
        Camera.Size size = getCameraSize(parameters.getSupportedPreviewSizes(), screenWidth,
                screenHeight, 0.1f);
        parameters.setPreviewSize(size.width, size.height);
        //水平方向未旋轉(zhuǎn),所以寬就是豎直方向的高,對應(yīng)旋轉(zhuǎn)操作
        Log.d(TAG, "startCamera: 預(yù)覽寬:" + size.width + " -- " + "預(yù)覽高:" + size.height);
        previewWidth = size.width;
        previewHeight = size.height;

        size = getCameraSize(parameters.getSupportedPictureSizes(), screenWidth, screenHeight, 0.1f);
        parameters.setPictureSize(size.width, size.height);
        //水平方向未旋轉(zhuǎn)蕉世,所以寬就是豎直方向的高
        Log.d(TAG, "startCamera: 圖片寬:" + size.width + " -- " + "圖片高:" + size.height);

        camera.setParameters(parameters);
        camera.startPreview();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

相機(jī)啟動(dòng)之后預(yù)覽數(shù)據(jù)會(huì)輸出到 surfaceTexture蔼紧,這個(gè)surfaceTexture 關(guān)聯(lián)一個(gè)紋理id,就是通過OpenGL創(chuàng)建并綁定的紋理id

  /**
 * 創(chuàng)建攝像頭預(yù)覽擴(kuò)展紋理
 */
private void createCameraTexture() {
    int[] textureIds = new int[1];
    GLES20.glGenTextures(1, textureIds, 0);
    cameraTextureId = textureIds[0];
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId);
    //環(huán)繞(超出紋理坐標(biāo)范圍)  (s==x t==y GL_REPEAT 重復(fù))
    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
    //過濾(紋理像素映射到坐標(biāo)點(diǎn))  (縮小狠轻、放大:GL_LINEAR線性)
    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

    surfaceTexture = new SurfaceTexture(cameraTextureId);
    surfaceTexture.setOnFrameAvailableListener(this);

    if (onSurfaceListener != null) {
        //回調(diào)給CameraManager獲取surfaceTexture:通過camera.setPreviewTexture(surfaceTexture);
        onSurfaceListener.onSurfaceCreate(surfaceTexture, fboTextureId);
    }

    // 解綁擴(kuò)展紋理
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
}

總結(jié)一下相機(jī)數(shù)據(jù)采集過程

  1. OpenGL創(chuàng)建紋理奸例,綁定一個(gè)紋理id
  2. 啟動(dòng)相機(jī),傳入前面創(chuàng)建的紋理向楼,這樣相機(jī)預(yù)覽數(shù)據(jù)就會(huì)輸出到OpenGL綁定的紋理查吊。
  3. 紋理并不能直接拿來編碼,需要參考GLSurfaceView的顯示原理湖蜕,創(chuàng)建EGL菩貌,通過OpenGL不斷將紋理渲染到MediaCodec的surface上,然后在一個(gè)子線程不斷獲取MediaCodec 中編碼成功的數(shù)據(jù)重荠,后面就跟音頻處理一樣箭阶,添加到混合器里,最終合成mp4文件戈鲁。

對OpenGL不熟悉的話沒關(guān)系仇参,有時(shí)間的話可以去學(xué)一下,不需要太深婆殿,也可以在我的簡書主頁查看OpenGL的入門系列文章
http://www.reibang.com/u/282785a6b12f

這篇文章內(nèi)容屬于音視頻開發(fā)的基礎(chǔ)部分了诈乒,后面要整理相機(jī)推流,會(huì)涉及到音視頻采集婆芦,也就是本章內(nèi)容怕磨。

在后面章節(jié)完成之后會(huì)把源碼提交到github,

想讓自己變優(yōu)秀消约,就要少看頭條肠鲫,少刷抖音,堅(jiān)持學(xué)習(xí)或粮,寫文章导饲,不然的話可能如下圖:


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市氯材,隨后出現(xiàn)的幾起案子渣锦,更是在濱河造成了極大的恐慌,老刑警劉巖氢哮,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袋毙,死亡現(xiàn)場離奇詭異,居然都是意外死亡冗尤,警方通過查閱死者的電腦和手機(jī)听盖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門贱除,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人媳溺,你說我怎么就攤上這事“叮” “怎么了悬蔽?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捉兴。 經(jīng)常有香客問我蝎困,道長,這世上最難降的妖魔是什么倍啥? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任禾乘,我火速辦了婚禮,結(jié)果婚禮上虽缕,老公的妹妹穿的比我還像新娘始藕。我一直安慰自己,他們只是感情好氮趋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布伍派。 她就那樣靜靜地躺著,像睡著了一般剩胁。 火紅的嫁衣襯著肌膚如雪诉植。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天昵观,我揣著相機(jī)與錄音晾腔,去河邊找鬼。 笑死啊犬,一個(gè)胖子當(dāng)著我的面吹牛灼擂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播觉至,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缤至,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了康谆?” 一聲冷哼從身側(cè)響起领斥,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沃暗,沒想到半個(gè)月后月洛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡孽锥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年嚼黔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了细层。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡唬涧,死狀恐怖疫赎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情碎节,我是刑警寧澤捧搞,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站狮荔,受9級特大地震影響胎撇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜殖氏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一晚树、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雅采,春花似錦爵憎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至闰渔,卻和暖如春席函,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背冈涧。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工茂附, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人督弓。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓营曼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親愚隧。 傳聞我的和親對象是個(gè)殘疾皇子蒂阱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355