Android AVDemo(8):視頻編碼凡傅,H.264 和 H.265 都支持丨音視頻工程示例

vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時(shí)獲得最新的音視頻技術(shù)文章乖酬。

塞尚《櫻桃和桃子》

iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā)于未,最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后弱贼,再借助 iOS/Android 平臺的音視頻能力上手去實(shí)踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染過程蒸苇,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(shù)據(jù)。

音視頻工程示例這個(gè)欄目吮旅,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實(shí)現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)溪烤。

這里是 Android 第八篇:Android 視頻編碼 Demo。這個(gè) Demo 里包含以下內(nèi)容:

  • 1)實(shí)現(xiàn)一個(gè)視頻采集模塊庇勃;
  • 2)實(shí)現(xiàn)兩個(gè)視頻編碼模塊 ByteBuffer檬嘀、Surface,支持 H.264/H.265责嚷;
  • 3)串聯(lián)視頻采集和編碼模塊鸳兽,將采集到的視頻數(shù)據(jù)輸入給編碼模塊進(jìn)行編碼,并存儲為文件罕拂;
  • 4)詳盡的代碼注釋贸铜,幫你理解代碼邏輯和原理。

在本文中聂受,我們將詳解一下 Demo 的具體實(shí)現(xiàn)和源碼蒿秦。讀完本文內(nèi)容相信就能幫你掌握相關(guān)知識。

不過蛋济,如果你的需求是:1)直接獲得全部工程源碼棍鳖;2)想進(jìn)一步咨詢音視頻技術(shù)問題;3)咨詢音視頻職業(yè)發(fā)展問題碗旅《纱Γ可以根據(jù)自己的需要考慮是否加入『關(guān)鍵幀的音視頻開發(fā)圈』。

想要了解視頻編碼祟辟,可以看看這幾篇:

1医瘫、視頻采集模塊

在這個(gè) Demo 中,視頻采集模塊 KFVideoCapture 的實(shí)現(xiàn)與《Android 視頻采集 Demo》中一樣旧困,這里就不再重復(fù)介紹了醇份,其接口如下:

KFIVideoCapture.java

public interface KFIVideoCapture {
    ///< 視頻采集初始化。
    public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
    ///< 釋放采集實(shí)例吼具。
    public void release();

    ///< 開始采集僚纷。
    public void startRunning();
    ///< 關(guān)閉采集。
    public void stopRunning();
    ///< 是否正在采集拗盒。
    public boolean isRunning();
    ///< 獲取 OpenGL 上下文怖竭。
    public EGLContext getEGLContext();
    ///< 切換攝像頭。
    public void switchCamera();
}

2陡蝇、視頻 ByteBuffer 編碼模塊

在實(shí)現(xiàn)視頻編碼模塊之前痊臭,我們先實(shí)現(xiàn)一個(gè)視頻編碼配置類 KFVideoEncoderConfig

KFVideoEncoderConfig.java


public class KFVideoEncoderConfig {
    public Size size = new Size(720,1280);
    public int bitrate = 4 * 1024 * 1024;
    public int fps = 30;
    public int gop = 30 * 4;
    public boolean isHEVC = false;
    public int profile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline;
    public int profileLevel = MediaCodecInfo.CodecProfileLevel.AVCLevel1;

    public KFVideoEncoderConfig() {

    }
}

這里可以配置各種編碼參數(shù)哮肚,但不同機(jī)型支持能力不同,通過此配置生成編碼格式描述 MediaFormat广匙。

接下來绽左,我們來實(shí)現(xiàn)一個(gè)視頻編碼模塊 KFByteBufferCodec,編碼模塊 KFByteBufferCodec 的實(shí)現(xiàn)與 《Android 音頻編碼 Demo》 中一樣艇潭,這里就不再重復(fù)介紹了拼窥,其接口如下

KFMediaCodecInterface.java


public interface KFMediaCodecInterface {
    public static final int KFMediaCodecInterfaceErrorCreate = -2000;
    public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
    public static final int KFMediaCodecInterfaceErrorStart = -2002;
    public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
    public static final int KFMediaCodecInterfaceErrorParams = -2004;

    public static int KFMediaCodeProcessParams = -1;
    public static int KFMediaCodeProcessAgainLater = -2;
    public static int KFMediaCodeProcessSuccess = 0;

    ///< 初始化 Codec,第一個(gè)參數(shù)需告知使用編碼還是解碼蹋凝。
    public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
    ///< 釋放 Codec鲁纠。
    public void release();

    ///< 獲取輸出格式描述。
    public MediaFormat getOutputMediaFormat();
    ///< 獲取輸入格式描述鳍寂。
    public MediaFormat getInputMediaFormat();
    ///< 處理每一幀數(shù)據(jù)改含,編碼前與編碼后都可以,支持編解碼 2 種模式迄汛。
    public int processFrame(KFFrame frame);
    ///< 清空 Codec 緩沖區(qū)捍壤。
    public void flush();
}

上面是 KFByteBufferCodec 接口的設(shè)計(jì),與音頻編碼對比區(qū)別如下:

  • 1)音頻編碼使用了繼承類 KFAudioByteBufferEncoder鞍爱,視頻編碼則直接使用類 KFByteBufferCodec鹃觉。
    • 音頻編碼使用了繼承類 KFByteBufferCodec,目的是切割合適大小的數(shù)據(jù) 2048 送入編碼器,因?yàn)?AAC 數(shù)據(jù)編碼每幀大小為 1024 * 2(位深 16 Bit)睹逃。
    • 視頻編碼使用了類 KFByteBufferCodec盗扇。
  • 2)外層使用構(gòu)造方法時(shí)配置參數(shù)修改:
    • setup 接口 mInputMediaFormat 需要設(shè)置視頻編碼的格式描述。

更具體細(xì)節(jié)見上述代碼及其注釋沉填。

3疗隶、視頻 Surface 編碼模塊

接下來,我們來實(shí)現(xiàn)一個(gè)視頻編碼模塊 KFVideoSurfaceEncoder翼闹,在這里輸入采集后的數(shù)據(jù)斑鼻,輸出編碼后的數(shù)據(jù),同樣也需要實(shí)現(xiàn)接口 KFMediaCodecInterface猎荠,參考模塊 KFByteBufferCodec坚弱。

KFVideoSurfaceEncoder.java

public class KFVideoSurfaceEncoder implements KFMediaCodecInterface {
    private static final String TAG = "KFVideoSurfaceEncoder";
    private KFMediaCodecListener mListener = null; ///< 回調(diào)。
    private KFGLContext mEGLContext = null; ///< GL 上下文法牲。
    private KFGLFilter mFilter = null; ///< 渲染到 Surface 特效史汗。
    private MediaCodec mEncoder = null; ///< 編碼器。
    private Surface mSurface = null; ///< 渲染 Surface 緩存拒垃。

    private HandlerThread mEncoderThread = null; ///< 編碼線程。
    private Handler mEncoderHandler = null;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主線程瓷蛙。
    private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
    private long mLastInputPts = 0;
    private MediaFormat mOutputFormat = null; ///< 輸出格式描述悼瓮。
    private MediaFormat mInputFormat = null; ///< 輸入格式描述戈毒。

    public KFVideoSurfaceEncoder() {

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext) {
        mInputFormat = mediaFormat;
        mListener = listener;

        mEncoderThread = new HandlerThread("KFSurfaceEncoderThread");
        mEncoderThread.start();
        mEncoderHandler = new Handler((mEncoderThread.getLooper()));

        mEncoderHandler.post(()->{
            if (mInputFormat == null) {
                _callBackError(KFMediaCodecInterfaceErrorParams,"mInputFormat == null");
                return;
            }

            ///< 初始化編碼器。
            boolean setupSuccess = _setupEnocder();
            if (setupSuccess) {
                mEGLContext = new KFGLContext(eglShareContext,mSurface);
                mEGLContext.bind();
                ///< 初始化特效横堡,用于紋理渲染到編碼器 Surface 上埋市。
                _setupFilter();
                mEGLContext.unbind();
            }
        });
    }

    @Override
    public MediaFormat getOutputMediaFormat() {
        return mOutputFormat;
    }

    @Override
    public MediaFormat getInputMediaFormat() {
        return mInputFormat;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void release() {
        mEncoderHandler.post(()->{
            ///< 釋放編碼器。
            if (mEncoder != null) {
                try {
                    mEncoder.stop();
                    mEncoder.release();
                } catch (Exception e) {
                    Log.e(TAG, "release: " + e.toString());
                }
                mEncoder = null;
            }

            ///< 釋放 GL 特效上下文命贴。
            if (mEGLContext != null) {
                mEGLContext.bind();
                if (mFilter != null) {
                    mFilter.release();
                    mFilter = null;
                }
                mEGLContext.unbind();

                mEGLContext.release();
                mEGLContext = null;
            }

            ///< 釋放 Surface 緩存道宅。
            if (mSurface != null) {
                mSurface.release();
                mSurface = null;
            }

            mEncoderThread.quit();
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public int processFrame(KFFrame inputFrame) {
        if (inputFrame == null || mEncoderHandler == null) {
            return KFMediaCodeProcessParams;
        }
        KFTextureFrame frame = (KFTextureFrame)inputFrame;

        mEncoderHandler.post(()-> {
            if (mEncoder != null && mEGLContext != null) {
                if (frame.isEnd) {
                    ///< 最后一幀標(biāo)記。
                    mEncoder.signalEndOfInputStream();
                } else {
                    ///< 最近一幀時(shí)間戳胸蛛。
                    mLastInputPts = frame.usTime();
                    mEGLContext.bind();
                    ///< 渲染紋理到編碼器 Surface 設(shè)置視口污茵。
                    GLES20.glViewport(0, 0, frame.textureSize.getWidth(), frame.textureSize.getHeight());
                    mFilter.render(frame);
                    ///< 設(shè)置時(shí)間戳。
                    mEGLContext.setPresentationTime(frame.usTime() * 1000);
                    mEGLContext.swapBuffers();
                    mEGLContext.unbind();

                    ///< 獲取編碼后的數(shù)據(jù)葬项,盡量拿出最多的數(shù)據(jù)出來泞当,回調(diào)給外層。
                    long outputDts = -1;
                    while (outputDts < mLastInputPts){
                        int bufferIndex = 0;
                        try {
                            bufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 10 * 1000);
                        } catch (Exception e) {
                            Log.e(TAG, "Unexpected MediaCodec exception in dequeueOutputBufferIndex, " + e);
                            _callBackError(KFMediaCodecInterfaceErrorDequeueOutputBuffer,e.getMessage());
                            return;
                        }

                        if (bufferIndex >= 0) {
                            ByteBuffer byteBuffer = mEncoder.getOutputBuffer(bufferIndex);
                            if (byteBuffer != null) {
                                outputDts = mBufferInfo.presentationTimeUs;
                                if (mListener != null) {
                                    KFBufferFrame encodeFrame = new KFBufferFrame();
                                    encodeFrame.buffer = byteBuffer;
                                    encodeFrame.bufferInfo = mBufferInfo;
                                    mListener.dataOnAvailable(encodeFrame);
                                }
                            } else {
                                break;
                            }

                            try {
                                mEncoder.releaseOutputBuffer(bufferIndex, false);
                            } catch (Exception e) {
                                Log.e(TAG, e.toString());
                                return;
                            }
                        } else {
                            if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                                mOutputFormat = mEncoder.getOutputFormat();
                            }
                            break;
                        }
                    }
                }
            }
        });

        return KFMediaCodeProcessSuccess;
    }

    @Override
    public void flush() {
        mEncoderHandler.post(()-> {
            ///< 刷新緩沖區(qū)民珍。
            if (mEncoder != null) {
                try {
                    mEncoder.flush();
                } catch (Exception e) {
                    Log.e(TAG, "flush error!" + e);
                }
            }
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private boolean _setupEnocder() {
        ///< 初始化編碼器襟士。
        try {
            String mimeType = mInputFormat.getString(MediaFormat.KEY_MIME);
            mEncoder = MediaCodec.createEncoderByType(mimeType);
            mEncoder.configure(mInputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        } catch (IOException e) {
            Log.e(TAG, "createEncoderByType" + e);
            _callBackError(KFMediaCodecInterfaceErrorCreate,e.getMessage());
            return false;
        }

        ///< 創(chuàng)建 Surface。
        mSurface = mEncoder.createInputSurface();

        ///< 開啟編碼器嚷量。
        try {
            mEncoder.start();
        } catch (Exception e) {
            Log.e(TAG, "start" +  e );
            _callBackError(KFMediaCodecInterfaceErrorStart,e.getMessage());
            return false;
        }

        return true;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _setupFilter() {
        ///< 創(chuàng)建渲染模塊陋桂,渲染到編碼器 Surface。
        if (mFilter == null) {
            mFilter = new KFGLFilter(true, KFGLBase.defaultVertexShader,KFGLBase.defaultFragmentShader);
        }
    }

    private void _callBackError(int error, String errorMsg){
        ///< 出錯(cuò)回調(diào)蝶溶。
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.onError(error,TAG + errorMsg);
            });
        }
    }
}

上面是 KFVideoSurfaceEncoder 的實(shí)現(xiàn)章喉,與視頻編碼 KFByteBufferCodec 對比區(qū)別如下:

  • 1)數(shù)據(jù)源輸入不同。
    • KFByteBufferCodec 輸入為 YUV 數(shù)據(jù) KFBufferFrame身坐。
    • KFVideoSurfaceEncoder 輸入為紋理數(shù)據(jù) KFTextureFrame秸脱。
  • 2)編碼流水線不同。
    • KFByteBufferCodec 輸入 YUV 數(shù)據(jù)進(jìn)行編碼部蛇。
    • KFVideoSurfaceEncoder 輸入為紋理數(shù)據(jù)摊唇,執(zhí)行 OpenGL 渲染,將紋理渲染到編碼器緩存 mSurface涯鲁。使用 mFilter.render 進(jìn)行渲染巷查,同時(shí)設(shè)置時(shí)間戳 setPresentationTime,交換前后臺緩沖區(qū) swapBuffers 抹腿,將紋理數(shù)據(jù)刷新到了 mSurface岛请。最后取出編碼后數(shù)據(jù),需要注意 releaseOutputBuffer 方法第 2 個(gè)參數(shù) render 設(shè)置為 true警绩。
  • 3)使用場景不同崇败。
    • KFVideoSurfaceEncoder適用于輸入數(shù)據(jù)為紋理的情況,例如采集后添加特效。
    • KFByteBufferCodec 適用于非紋理數(shù)據(jù)后室,例如游戲直播缩膝、錄屏直播、圖片轉(zhuǎn)視頻等輸入數(shù)據(jù)為 ByteBuffer岸霹,此時(shí)沒必要再做數(shù)據(jù)轉(zhuǎn)換疾层。

更具體細(xì)節(jié)見上述代碼及其注釋。

4贡避、采集視頻數(shù)據(jù)進(jìn)行 H.264/H.265 編碼和存儲

我們在一個(gè) MainActivity 中來實(shí)現(xiàn)視頻采集及編碼邏輯痛黎,因?yàn)?Android 編碼的默認(rèn)輸出 AnnexB 碼流格式,所以這里不需要轉(zhuǎn)換刮吧。

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private KFIVideoCapture mCapture; ///< 采集器湖饱。
    private KFVideoCaptureConfig mCaptureConfig; ///< 采集配置。
    private KFRenderView mRenderView; ///< 渲染視圖皇筛。
    private KFGLContext mGLContext; ///< OpenGL 上下文琉历。

    private KFVideoEncoderConfig mEncoderConfig; ///< 編碼配置。
    private KFMediaCodecInterface mEncoder; ///< 編碼水醋。
    private FileOutputStream mStream = null;

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) this,
                    new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1);
        }

        ///< 創(chuàng)建 GL 上下文旗笔。
        mGLContext = new KFGLContext(null);
        ///< 創(chuàng)建渲染視圖。
        mRenderView = new KFRenderView(this,mGLContext.getContext());

        WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE);
        Rect outRect = new Rect();
        windowManager.getDefaultDisplay().getRectSize(outRect);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height());
        addContentView(mRenderView,params);

        FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
        startParams.gravity = Gravity.CENTER_HORIZONTAL;
        Button startButton = new Button(this);
        startButton.setTextColor(Color.BLUE);
        startButton.setText("開始");
        startButton.setVisibility(View.VISIBLE);
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ///< 創(chuàng)建編碼器拄踪。
                if (mEncoder == null) {
                    mEncoder = new KFVideoSurfaceEncoder();
                    MediaFormat mediaFormat = KFAVTools.createVideoFormat(mEncoderConfig.isHEVC,mEncoderConfig.size, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface,mEncoderConfig.bitrate,mEncoderConfig.fps,mEncoderConfig.gop / mEncoderConfig.fps,mEncoderConfig.profile,mEncoderConfig.profileLevel);
                    mEncoder.setup(true,mediaFormat,mVideoEncoderListener,mGLContext.getContext());
                    ((Button)view).setText("停止");
                } else {
                    mEncoder.release();
                    mEncoder = null;
                    ((Button)view).setText("開始");
                }
            }
        });
        addContentView(startButton, startParams);

        ///< 創(chuàng)建采集器蝇恶。
        mCaptureConfig = new KFVideoCaptureConfig();
        mCaptureConfig.cameraFacing = LENS_FACING_FRONT;
        mCaptureConfig.resolution = new Size(720,1280);
        mCaptureConfig.fps = 30;
        boolean useCamera2 = false;
        if (useCamera2) {
            mCapture = new KFVideoCaptureV2();
        } else {
            mCapture = new KFVideoCaptureV1();
        }
        mCapture.setup(this,mCaptureConfig,mVideoCaptureListener,mGLContext.getContext());
        mCapture.startRunning();

        mEncoderConfig = new KFVideoEncoderConfig();
    }

    private KFVideoCaptureListener mVideoCaptureListener = new KFVideoCaptureListener() {
        @Override
        public void cameraOnOpened(){}

        @Override
        public void cameraOnClosed() {
        }

        @Override
        public void cameraOnError(int error,String errorMsg) {

        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onFrameAvailable(KFFrame frame) {
            ///< 采集數(shù)據(jù)回調(diào),進(jìn)入編碼器惶桐。
            mRenderView.render((KFTextureFrame) frame);
            if (mEncoder != null) {
                mEncoder.processFrame(frame);
            }
        }
    };

    private KFMediaCodecListener mVideoEncoderListener = new KFMediaCodecListener() {
        @Override
        public void onError(int error, String errorMsg) {

        }

        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public void dataOnAvailable(KFFrame frame) {
            ///< 編碼數(shù)據(jù)回調(diào)寫入本地文件撮弧。
            if (mStream == null) {
                try {
                    mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.h264");
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
            }
            KFBufferFrame bufferFrame = (KFBufferFrame)frame;

            try {
                byte[] dst = new byte[bufferFrame.bufferInfo.size];
                bufferFrame.buffer.get(dst);
                mStream.write(dst);
            }  catch (IOException e) {
                e.printStackTrace();
            }
        }
    };
}

上面是 MainActivity 的實(shí)現(xiàn),主要分為以下幾個(gè)部分:

  • 1)創(chuàng)建 OpenGL 上下文姚糊。

    • 創(chuàng)建上下文 mGLContext贿衍,這樣好處是采集與預(yù)覽可以共享,提高擴(kuò)展性救恨。
  • 2)創(chuàng)建采集實(shí)例贸辈。

    • 這里需要注意的是,我們通過開關(guān) useCamera2 選擇 CameraCamera2肠槽。
    • 參數(shù)配置 mCaptureConfig擎淤,可自定義攝像頭方向、幀率秸仙、分辨率嘴拢。
  • \

    1. 采集數(shù)據(jù)回調(diào) onFrameAvailable,將數(shù)據(jù)輸入給渲染模塊與編碼模塊寂纪。
  • 4)編碼數(shù)據(jù)回調(diào) KFMediaCodecListenerdataOnAvailable 中席吴,將編碼數(shù)據(jù)存儲為 H.264/H.265 文件。

5、用工具播放 H.264/H.265 文件

完成視頻采集和編碼后抢腐,可以將 sdcard 文件夾下面的 test.h264test.h265 文件拷貝到電腦上姑曙,使用 ffplay 播放來驗(yàn)證一下效果是否符合預(yù)期:

$ ffplay -I test.h264
$ ffplay -I test.h265

關(guān)于播放 H.264/H.265 文件的工具襟交,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具《可視化音視頻分析工具》第 2.1 節(jié) StreamEye迈倍。

- 完 -

推薦閱讀

《Android AVDemo(7):視頻采集》

《Android AVDemo(6):音頻渲染》

《Android AVDemo(5):音頻解碼》

《Android AVDemo(4):音頻解封裝》

《Android AVDemo(3):音頻封裝》

《Android AVDemo(2):音頻編碼》

《Android AVDemo(1):音頻采集》

《iOS AVDemo(7):視頻采集》

《iOS 音頻處理框架及重點(diǎn) API 合集》

《iOS AVDemo(6):音頻渲染》

《iOS AVDemo(5):音頻解碼》

《iOS AVDemo(4):音頻解封裝》

《iOS AVDemo(3):音頻封裝》

《iOS AVDemo(2):音頻編碼》

《iOS AVDemo(1):音頻采集》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市捣域,隨后出現(xiàn)的幾起案子啼染,更是在濱河造成了極大的恐慌,老刑警劉巖焕梅,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迹鹅,死亡現(xiàn)場離奇詭異,居然都是意外死亡硕舆,警方通過查閱死者的電腦和手機(jī)盗似,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門火诸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弟蚀,你說我怎么就攤上這事⌒锸В” “怎么了义钉?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長规肴。 經(jīng)常有香客問我捶闸,道長,這世上最難降的妖魔是什么拖刃? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任删壮,我火速辦了婚禮,結(jié)果婚禮上兑牡,老公的妹妹穿的比我還像新娘央碟。我一直安慰自己,他們只是感情好发绢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布硬耍。 她就那樣靜靜地躺著,像睡著了一般边酒。 火紅的嫁衣襯著肌膚如雪经柴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天墩朦,我揣著相機(jī)與錄音坯认,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牛哺,可吹牛的內(nèi)容都是我干的陋气。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼引润,長吁一口氣:“原來是場噩夢啊……” “哼巩趁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淳附,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤议慰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后奴曙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體别凹,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年洽糟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炉菲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坤溃,死狀恐怖拍霜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浇雹,我是刑警寧澤沉御,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站昭灵,受9級特大地震影響吠裆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烂完,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一试疙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧抠蚣,春花似錦祝旷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至柄冲,卻和暖如春吻谋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背现横。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工漓拾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阁最,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓骇两,卻偏偏與公主長得像速种,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子低千,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容