從零開始仿寫一個抖音App——音視頻開篇

本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。微信號:a1018998632,交流qq羣:859640274html

GitHub地址

你們好,距離上次本專題發文已經有五個星期了,中間發了兩篇非本專題的文章,可能不少人都覺得我要棄坑了。可是並非這回事,主要是工做有點忙,並且我在音視頻方面其實也有許多東西須要學習和整理。那麼從本篇文章開始咱們就要進入音視頻領域進行研究學習了,Android 領域的文章會在中間整合音視頻代碼的時候進行穿插講解。其實 Android 裏面要講的東西仍是挺多的,奈什麼時候間不等人。廢話很少說,咱們進入文章。本文預計閱讀時間二十分鐘。前端

本文分爲如下章節,讀者能夠按需閱讀java

  • 1.聊一聊——主要是公佈一些事情,沒興趣的同窗能夠跳過。
  • 2.音視頻前置知識——列一列學習音視頻技術以前須要知道的東西。
  • 3.cmake 入門——瞭解一個 c/c++ 工程的組織與編譯。
  • 4.ffmpeg 入門——入門 ffmepg,講解一個官方 demo。

1、聊一聊

  • 1.有些朋友可能會發現本專題下掉了兩篇文章,這個問題在這裏說明一下。下掉文章是由於個人我的緣由,這兩篇文章我會在適當的時間修改好以後再從新發布。若是實在有同窗有須要的話能夠私聊我,我發給你。
  • 2.簡書的朋友可能會發現,個人我的界面上多了簡書版權這個標籤,這個標籤表示我已經與簡書籤約,在積累了必定的存稿以後會與簡書合做出版一本關於 Android+音視頻技術+深度學習+短視頻 方面的書籍。書的話預計會在半年以後開始寫,如今先預熱一下,哈哈:)。
  • 3.關注了個人 Github 的同窗會看見我將 MyTikTok 這個項目移動到了 TheGodsThemselves 這個組織內,之後關於本專題的正式項目也會都發布到這裏面去,或許再過不久就會有第一個同窗加入到本組織中來。另外這裏從新說明一下本項目的理想目標:完整的復刻大廠的項目流程,寫出一個短視頻 App(暫定模仿抖音),在項目中會用到各個端(包括不限於 Android、IOS、後臺、前端、算法、音視頻)的有意思的技術,讓參與和關注本項目的同窗可以學習到本身感興趣的技術棧(大廠的真實開發經驗)。

2、音視頻前置知識

其實我在 個人技術成長之路 中已經大概講解了學習音視頻技術須要學習哪些東西,在這一節我會講些具體的東西,固然也只是一個粗淺的入門,更加深刻的知識仍是須要讀者本身去積累。android

1.多媒體概念

  • 1.視頻格式:usb 攝像頭的輸出格式有RGB2四、YUV二、YV2這些都是沒編碼的原始數據,MJPEG 這種是通過編碼的數據
  • 2.音頻格式:不少
  • 3.容器和協議:容器指的是一種音視頻文件格式好比.avi,協議指的是存放在音視頻文件中的數據的編解碼方式,一個容器能夠裝有各類不一樣的編解碼方式的數據,每種編解碼方式都須要不一樣的編解碼器。MPEG、H.26X等等編碼方式比較常見。 AVI、MPG、MP4等等容器比較常見。
    • 1.容器文件格式:一個容器文件經常由三部分組成,文件頭、索引、多媒體數據。
      • 1.文件頭經常說明了多媒體數據的分辨率、幀率、音頻採樣率等等規範信息
      • 2.索引部分用於記錄多媒體數據在文件中的位置,由於多媒體數據不必定是連續的,同時還可能有音視頻同步索引等等。播放視頻的時候經常會把索引所有讀入內存。
      • 3.多媒體數據部分就是儲存壓縮過的視頻、音頻、文本數據等等。
    • 2.協議:視頻壓縮協議用 h.26x、mpeg-x 等等。h.265是最新的壓縮協議。音頻壓縮協議有 g.7xx 等等
  • 4.經常使用概念:
    • 1.硬解碼:不讓 cpu 參與解碼,而是使用專門的設備進行解碼,這種設備通常集成在 gpu 中。硬解碼的好處是速度比 cpu 快得多、節省了 cpu 資源。壞處是起步晚軟件支持少、沒法兼容各類不一樣的編解碼方式和文件格式、沒有像軟解那種畫質加強的好處、gpu 硬解碼比較難。
    • 2.ibp 幀:gop 是 一組畫面幀,一個視頻文件由 n 個 gop 組成。一個 gop 裏面分爲 i、b、p 三種幀。
      • 1.i 是全量幀,至關於一張圖片被壓縮後的數據,能夠本身恢復出一個顯示幀,壓縮率在7倍左右
      • 2.p 是向前預測幀,他須要依賴 i 幀來解碼,他使用運動補償的方式來傳送與前面的 i 或 p 幀的偏差,而後重建出一個顯示幀,i 和 p 均可以做爲 p 幀的前置幀。由於 p 幀能夠做爲後置幀的參考,因此其可能形成解碼錯誤的擴散。壓縮率在20倍左。
      • 3.b 是雙向預測幀,他須要依賴前面的 i 或 p 幀和後面的 p 幀來解碼,壓縮率爲50倍左右,由於壓縮率高因此解碼麻煩。須要預先知道後置幀,因此要預讀預解碼。
      • 4.mpeg4中每一幀開頭是 00 00 01 b6,而在這後面的兩個 bit 就表示的當前幀屬於那種幀,00爲 i,01爲 p,10爲 b。

2.FFmpeg基本概念

  • 1.模塊組成:
    • 1.libavformat:解析各類格式的音視頻文件、獲取解碼信息的讀取音視頻幀、爲 libavcode 提供獨立的音頻視頻的流
    • 2.libavcodec: 適用於各類編解碼協議的編解碼器
    • 3.libavdevice:硬件採集、加速、顯示視頻。
    • 4.libavfilter:進行視頻的轉換,好比剪裁、伸縮、寬高比等等
    • 5.libavutil:工具庫
    • 6.libavresample:。。
    • 7.libswscale:比例縮放、色彩映射轉換、圖像色彩空間轉換
    • 8.libpostproc:音視頻後期效果處理
    • 9.ffmpeg:一個暴露到外部的工具
    • 10.ffplay:簡單的播放器,使用 ffmpeg 庫進行解析和解碼
  • 2.總的來講 FFmpeg 是一個 c 語言寫的程序庫。它由上面這些模塊組成。它並非一個播放器,他是播放器的核心組件。好比我須要在 windows 上面寫一個播放器,咱們有一個 MP4 文件了,那麼這個播放器由下面這些步驟來播放這個視頻:FFmpeg 解析文件格式——>FFmpeg 讀取文件數據——>FFmpeg 解碼文件數據將數據還原成圖片幀——> Windows Api 顯示圖片幀。而我若是又須要在 Android 上寫一個播放器,前面的三個步驟並不用變化,只須要將最後一個步驟替換成 Opengl es + TextureSurfaceView 來實現圖片幀的顯示便可。由此咱們能夠發現,FFmpeg 是具備跨平臺性的,視頻播放的核心邏輯只要用了 FFmpeg 那麼在各個平臺中就不須要大的變化了,須要變化的就只是各個平臺顯示圖片幀的邏輯。
  • 3.FFmpeg 中有個 ffmepg 模塊,當你的電腦上安裝了 FFmpeg,那麼你就能夠經過命令行來調用 ffmpeg 暴露出來的函數對視頻進行處理。

3、Cmake入門

Cmake 是組織 C/Cpp 項目的一個工具,相似咱們在 android 中使用的 gradle。咱們要寫一個大一點的工具,Cmake 這種項目管理工具是必不可少的。這一節就來入門一下 Cmake,注意下面的教程是 官方教程 的翻譯。ios

這是本章節對應的項目:cmake_learning項目c++

1.編譯器準備

我由於主力機是 Mac,因此使用的 IDE 是 CLion,CLion 也是 JetBrain 全家桶的成員之一。使用了 Android Studio 或者 IDEA 的同窗能夠很方便的切換到這個 IDE 上。此外 CLion 仍是一個跨平臺的 IDE,也就是說在 Windows Linux 上面也可使用它。固然 Visual Studio 永遠是最強的 IDE(手動狗頭)。須要注意的是 CLion 是須要花錢買激活碼的,彷佛沒有免費版開始能免費試用一個月左右的時間,因此激活碼的獲取途徑你們就各顯神通吧。git

2.Cmake

(1).最基本的Cmake程序

  • 1.咱們進入項目中 one/a 的目錄發現下面有兩個文件:CMakeLists.txt 和 tutorial.cpp 裏面的代碼以下:程序員

    • 1.咱們寫了一個計算平方根的 cpp 代碼,而後放入了 Tutorial 這個 project 中。
    • 2.咱們在 a 中建立一個 build 的目錄,而後在命令行中進入這個目錄中,最後運行 cmake .. 這個命令,咱們會發現 build 下面生成了幾個文件,這些文件就是進行 make 須要的文件。
    • 3.咱們最後在 build 文件夾下運行 make 命令,這個時候會生成一個 Tutorial 的可執行文件,這就是 Tutorial 項目最終的產物了,咱們能夠輸入 ./Tutorial 3 來對3進行平方根的計算。
    # 一個 cmake 組織的項目最少有下面這三行代碼
    cmake_minimum_required (VERSION 2.6) # 表示cmake的最小版本
    project (Tutorial)# 新建一個project,這個project的名字叫Tutorial
    add_executable(Tutorial tutorial.cpp) # 爲 Tutorial 這個 project 添加一個可執行的文件tutorial.cpp
    # 1.cmake的語法支持大小、小寫和大小寫混合例如上面的 project 能夠寫成 PROJECT
    複製代碼
    //
    // Created by 什麼時候夕 on 2018/10/20.
    // 
    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    int main (int argc, char *argv[]) {
        if (argc < 2) {
            fprintf(stdout, "usage: %s number\n", argv[0]);
            return 1;
        }
        double inputValue = atof(argv[1]);
        double outputValue = sqrt(inputValue);
        fprintf(stdout, "The square root of %g is %g\n", inputValue, outputValue);
        return 0;
    }
    複製代碼
  • 2.咱們進入項目中 one/b 的目錄發現下面有三個文件:CMakeLists.txt、tutorial.cpp、TutorialConfig.h.in 裏面的代碼以下:github

    • 1.在 Tutorial_A 這個項目中聲明瞭兩個參數,而後在TutorialConfig.h.in 文件引用了這兩個參數,cmake 會根據這個文件生成一個名爲 TutorialConfig.h 的文件。
    • 2.咱們在 tutorial.cpp 中使用了 TutorialConfig.h,也就使用了 cmake 文件中定義的參數。這和咱們在開發 android 的時候在 gradle 文件中定義參數最後在 java 代碼中使用很是相似。
    • 3.咱們接下來在 build 文件中依次運行 cmake ..make./Tutorial_A。會發現輸出了咱們使用的參數。
cmake_minimum_required (VERSION 2.6)
project (Tutorial_A)
# 咱們能夠在 cmake 的程序中添加鍵值對 set(KEY VALUE),下面就是一個鍵值對的設置方式。
# 若是想要在 cmake 文件中取出這個鍵值對則須要使用 ${KEY} 的方式
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

## 這裏能夠設置一個配置文件,咱們能夠在 TutorialConfig.h.in 中配置 set() 中設置的鍵值對
## PROJECT_SOURCE_DIR 表示的是源代碼的路徑
## PROJECT_BINARY_DIR 表示的是cmake build 的路徑
configure_file (
        "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
        "${PROJECT_BINARY_DIR}/TutorialConfig.h"
)

# 將 cmake 的 build 目錄添加到cmake 尋找 include 文件的目錄列表中,這樣一來 cmake 就能找到前面生成的 TutorialConfig.h 配置文件
include_directories("${PROJECT_BINARY_DIR}")

add_executable(Tutorial_A tutorial.cpp)
複製代碼
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// include 了cmake 生成配置文件
#include "TutorialConfig.h"

int main (int argc, char *argv[]) {
    if (argc < 2)
    {
        fprintf(stdout,"%s Version %d.%d\n",
                argv[0],
                // 使用了 cmake 生成的配置參數
                Tutorial_VERSION_MAJOR,
                Tutorial_VERSION_MINOR);
        fprintf(stdout,"Usage: %s number\n",argv[0]);
        return 1;
    }
    double inputValue = atof(argv[1]);
    double outputValue = sqrt(inputValue);
    fprintf(stdout,"The square root of %g is %g\n",
            inputValue, outputValue);
    return 0;
}
複製代碼
// 這個是配置文件,cmake 會根據他在 cmake 的 build 目錄生成一個 TutorialConfig.h 文件
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR
複製代碼

(2).添加庫的依賴

  • 1.咱們進入項目的 two/a/mylib 中會看見三個文件 CMakeLists.txt、mysqrt.cpp、MathFunctions.h 代碼以下:
    • 1.聲明瞭一個 library
    • 2.定義了一個計算平方根的函數,而後使用頭文件暴露在外面
cmake_minimum_required (VERSION 2.6)
# 聲明瞭一個 library 名爲 MathFunctions,他包含一個可執行文件 mysqrt.cpp
add_library(MathFunctions mysqrt.cpp)
複製代碼
#include "MathFunctions.h"
#include <stdio.h>

// a hack square root calculation using simple operations
double mysqrt(double x) {
    if (x <= 0) {
        return 0;
    }

    double result;
    double delta;
    result = x;

    // do ten iterations
    int i;
    for (i = 0; i < 10; ++i) {
        if (result <= 0) {
            result = 0.1;
        }
        delta = x - (result * result);
        result = result + 0.5 * delta / result;
        fprintf(stdout, "Computing sqrt of %g to be %g\n", x, result);
    }
    return result;
}
複製代碼
//
// Created by 什麼時候夕 on 2018/11/11.
//

#ifndef PROJECT_MATHFUNCTIONS_H
#define PROJECT_MATHFUNCTIONS_H
double mysqrt(double x);
#endif //PROJECT_MATHFUNCTIONS_H
複製代碼
  • 2.而後咱們再看看 two/a 這個目錄下面的文件,這些文件大部分是從 one/b 中拷貝來的,我就只貼有修改的部分 CMakeLists.txt、Configure.h.in、MathFunctions.h、tutorial.cpp:
    • 1.這裏主要作的工做是如今 cmake 文件中定義了一個 USE_MYMATH 的開關,當這個開關爲 ON 的時候就將咱們定義的 library 集成到 project 中,不然就不集成,只使用系統自帶的庫。這個東西在跨平臺的時候很是有用,好比 ios 和 android 中的 log 庫不一樣,那麼我就能夠定義一個開關來區別這兩個平臺。
    • 2.能夠注意到的是這裏也定義了一個 Configure.h.in 文件做爲配置文件,cmake 會根據這個文件來建立一個 Configure.h 文件,而後咱們就能夠在 Cpp 文件中使用咱們定義的開關了。
    • 3.咱們能夠在 two/a/build 中運行 cmake..、make、./Tutorial_Mylib 3 這幾個命令,會發現最終調用的是咱們本身的函數,若是將 USE_MYMATH 改爲 OFF 而後刪除 build 中的文件再從新 build 一遍,會發現最後調用的是系統的函數。
cmake_minimum_required (VERSION 2.6)
project (Tutorial_Mylib)

set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

configure_file (
        "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
        "${PROJECT_BINARY_DIR}/TutorialConfig.h"
)

# 添加一個是否使用咱們本身的庫的開關 USE_MYMATH,這個開關能夠在 cmake 中直接使用
option (USE_MYMATH
        "Use tutorial provided math implementation" ON)

# 定義一個文件來儲存 USE_MYMATH,以便在 cpp 文件中使用
configure_file("${PROJECT_SOURCE_DIR}/Configure.h.in"
        "${PROJECT_BINARY_DIR}/Configure.h")

include_directories("${PROJECT_BINARY_DIR}")

# 若是咱們把開關設置爲 ON,那麼就將 mylib 集成進編譯中,不然就不集成。
if (USE_MYMATH)
    include_directories ("${PROJECT_SOURCE_DIR}/mylib")
    add_subdirectory (mylib)
    set (EXTRA_LIBS MathFunctions)
endif (USE_MYMATH)

add_executable (Tutorial_Mylib tutorial.cpp)

# 將library 與 project 進行連接,使得 project 中能夠調用 library 中的函數
target_link_libraries (Tutorial_Mylib ${EXTRA_LIBS})
複製代碼
#cmakedefine USE_MYMATH
複製代碼
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
#include "Configure.h"
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif

int main (int argc, char *argv[]) {
    if (argc < 2)
    {
        fprintf(stdout,"%s Version %d.%d\n", argv[0],
                Tutorial_VERSION_MAJOR,
                Tutorial_VERSION_MINOR);
        fprintf(stdout,"Usage: %s number\n",argv[0]);
        return 1;
    }

    double inputValue = atof(argv[1]);

#ifdef USE_MYMATH
    // 若是開關開了,就使用我本身的庫 
    double outputValue = mysqrt(inputValue);
    fprintf(stdout,"use my math");
#else
    double outputValue = sqrt(inputValue);
    fprintf(stdout,"not use my math");
#endif

    fprintf(stdout,"The square root of %g is %g\n",
            inputValue, outputValue);
    return 0;
}
複製代碼

(2).添加庫的依賴

  • 1.咱們進入項目的 two/a/mylib 中會看見三個文件 CMakeLists.txt、mysqrt.cpp、MathFunctions.h 代碼以下:
    • 1.聲明瞭一個 library
    • 2.定義了一個計算平方根的函數,而後使用頭文件暴露在外面
cmake_minimum_required (VERSION 2.6)
# 聲明瞭一個 library 名爲 MathFunctions,他包含一個可執行文件 mysqrt.cpp
add_library(MathFunctions mysqrt.cpp)
複製代碼
#include "MathFunctions.h"
#include <stdio.h>

// a hack square root calculation using simple operations
double mysqrt(double x) {
    if (x <= 0) {
        return 0;
    }

    double result;
    double delta;
    result = x;

    // do ten iterations
    int i;
    for (i = 0; i < 10; ++i) {
        if (result <= 0) {
            result = 0.1;
        }
        delta = x - (result * result);
        result = result + 0.5 * delta / result;
        fprintf(stdout, "Computing sqrt of %g to be %g\n", x, result);
    }
    return result;
}
複製代碼
//
// Created by 什麼時候夕 on 2018/11/11.
//

#ifndef PROJECT_MATHFUNCTIONS_H
#define PROJECT_MATHFUNCTIONS_H
double mysqrt(double x);
#endif //PROJECT_MATHFUNCTIONS_H
複製代碼
  • 2.而後咱們再看看 two/a 這個目錄下面的文件,這些文件大部分是從 one/b 中拷貝來的,我就只貼有修改的部分 CMakeLists.txt、Configure.h.in、MathFunctions.h、tutorial.cpp:
    • 1.這裏主要作的工做是如今 cmake 文件中定義了一個 USE_MYMATH 的開關,當這個開關爲 ON 的時候就將咱們定義的 library 集成到 project 中,不然就不集成,只使用系統自帶的庫。這個東西在跨平臺的時候很是有用,好比 ios 和 android 中的 log 庫不一樣,那麼我就能夠定義一個開關來區別這兩個平臺。
    • 2.能夠注意到的是這裏也定義了一個 Configure.h.in 文件做爲配置文件,cmake 會根據這個文件來建立一個 Configure.h 文件,而後咱們就能夠在 Cpp 文件中使用咱們定義的開關了。
    • 3.咱們能夠在 two/a/build 中運行 cmake..、make、./Tutorial_Mylib 3 這幾個命令,會發現最終調用的是咱們本身的函數,若是將 USE_MYMATH 改爲 OFF 而後刪除 build 中的文件再從新 build 一遍,會發現最後調用的是系統的函數。
cmake_minimum_required (VERSION 2.6)
project (Tutorial_Mylib)

set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

configure_file (
        "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
        "${PROJECT_BINARY_DIR}/TutorialConfig.h"
)

# 添加一個是否使用咱們本身的庫的開關 USE_MYMATH,這個開關能夠在 cmake 中直接使用
option (USE_MYMATH
        "Use tutorial provided math implementation" ON)

# 定義一個文件來儲存 USE_MYMATH,以便在 cpp 文件中使用
configure_file("${PROJECT_SOURCE_DIR}/Configure.h.in"
        "${PROJECT_BINARY_DIR}/Configure.h")

include_directories("${PROJECT_BINARY_DIR}")

# 若是咱們把開關設置爲 ON,那麼就將 mylib 集成進編譯中,不然就不集成。
if (USE_MYMATH)
    include_directories ("${PROJECT_SOURCE_DIR}/mylib")
    add_subdirectory (mylib)
    set (EXTRA_LIBS MathFunctions)
endif (USE_MYMATH)

add_executable (Tutorial_Mylib tutorial.cpp)

# 將library 與 project 進行連接,使得 project 中能夠調用 library 中的函數
target_link_libraries (Tutorial_Mylib ${EXTRA_LIBS})
複製代碼
#cmakedefine USE_MYMATH
複製代碼
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
#include "Configure.h"
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif

int main (int argc, char *argv[]) {
    if (argc < 2)
    {
        fprintf(stdout,"%s Version %d.%d\n", argv[0],
                Tutorial_VERSION_MAJOR,
                Tutorial_VERSION_MINOR);
        fprintf(stdout,"Usage: %s number\n",argv[0]);
        return 1;
    }

    double inputValue = atof(argv[1]);

#ifdef USE_MYMATH
    // 若是開關開了,就使用我本身的庫 
    double outputValue = mysqrt(inputValue);
    fprintf(stdout,"use my math");
#else
    double outputValue = sqrt(inputValue);
    fprintf(stdout,"not use my math");
#endif

    fprintf(stdout,"The square root of %g is %g\n",
            inputValue, outputValue);
    return 0;
}
複製代碼

(3).安裝庫與可執行文件

  • 1.咱們進入項目的 three/a 文件夾中,這裏面的文件都是從 two/a 中複製過來的,我只將增長的代碼列一下mylib/CMakeLists.txt、a/CMakeLists.txt:
    • 1.這裏就比較簡單了,就只是將咱們生成的庫與可執行文件安裝到電腦中去
    • 2.先依次運行cmake ..、make、make install,而後能夠運行 /usr/local/bin/Tutorial_Mylib_Install 3 來查看是否安裝成功,注意這裏的路徑是 Mac 電腦的路徑。
# 安裝這個庫,將庫和頭文件分別添加到 bin 和 include 文件夾中,最後移動到的地方以下
# /usr/local/bin/libMathFunctions_Install.a
# /usr/local/include/MathFunctions.h
install (TARGETS MathFunctions_Install DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)
複製代碼
# TARGETS包含六種形式:ARCHIVE, LIBRARY, RUNTIME, OBJECTS, FRAMEWORK, BUNDLE。注意Mathfunction_Install安裝的是LIBRARY,Tutorial_Mylib_Install 是RUNTIME類型。
# FILE 將給定的文件複製到指定目錄。若是沒有給定權限參數,則由該表單安裝的文件默認爲OWNER_WRITE、OWNER_READ、GROUP_READ和WORLD_READ。
# TARGETS和FILE可指定爲相對目錄和絕對目錄。
# DESTINATION在這裏是一個相對路徑,取默認值。在unix系統中指向 /usr/local 在windows上c:/Program Files/${PROJECT_NAME}。
# 也能夠經過設置CMAKE_INSTALL_PREFIX這個變量來設置安裝的路徑,那麼安裝位置不指向/usr/local,而指向你所指定的目錄。

# 安裝這個可執行文件,將可執行文件和頭文件分別添加到 bin 和 include 文件夾中,最後移動到的地方以下
# /usr/local/bin/Tutorial_Mylib_Install
# /usr/local/include/TutorialConfig.h
install (TARGETS Tutorial_Mylib_Install DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
         DESTINATION include)

複製代碼

(4).Cmake生成Cpp文件

  • 1.咱們進入 four/a 目錄中,這裏的代碼都是從 two/a 中拷貝過來的,因此我就只貼修改的部分,mylib/CMakeLists.txt、mylib/MakeTable.cpp、a/Configure.h.in:
    • 1.這裏的目的主要是經過 MakeTable 這個 project 生成一個 Table.h。最後給 mysqrt.cpp 在當前系統中沒有 log 和 exp 這兩個函數的時候使用。
    • 2.咱們運行了 cmake.. 以後會發現 build/mylib 目錄中生成了 Table.h 這個文件
project(MakeTable)

add_executable(MakeTable MakeTable.cpp)

# 1.輸出 Table 文件
# 2.將 Table 文件做爲參數傳入 MakeTable 項目中,並運行它
# 3.Table 的生成是依賴於 MakeTable 這個 project 的
# CMAKE_CURRENT_BINARY_DIR 表示某個 cmake 文件build以後的文件夾,好比這裏就是指 build/mylib
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
        COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
        DEPENDS MakeTable)

include_directories(${CMAKE_CURRENT_BINARY_DIR})
# 將生成的表一塊兒編譯到 MathFunctions_Table 中去
add_library(MathFunctions_Table mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
複製代碼
//
// Created by 什麼時候夕 on 2018/10/20.
//
#include <stdio.h>
#include <stdlib.h>
#include "math.h"

int main (int argc, char *argv[]) {
    double result;
    if (argc < 2) {
        return 1;
    }
    FILE *fout = fopen(argv[1], "w");
    if (!fout) {
        return 1;
    }
    fprintf(fout, "double sqrtTable[] = {\n");
    for (int j = 0; j < 10; ++j) {
        result = sqrt(static_cast<double>(j));
        fprintf(fout, "%g,\n", result);
    }
    fprintf(fout, "0};\n");
    fclose(fout);
    return 0;
}
複製代碼
#cmakedefine USE_MYMATH
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP
複製代碼

(5).CMake語法

  • 1.必填、[command]可填、a|b 都可
  • 2.cmake 能夠三種形式組織文件
  • 3.文件夾形式:相似 gradle 根目錄下須要有一個 CMakeList.txt 的文件做爲入口,若是其餘目錄下面還 須要有新的子文件夾要編譯,子文件夾下也須要有 CMakeList.txt。並且須要在根目錄 CMakeList.txt 下 用 add_subdirectory() 來註明。此外,每一個CMakeList.txt 在被處理的時候都是以 cmake 命令調用 的文件夾做爲當前工做目錄和輸出目錄。
  • 4.定義和取消變量用的是 set() 和 unset(),被定義的變量始終是字符串類型,變量名區分大小寫。 變量名用{}進行獲取,也可在變量中進行嵌套{${}}
  • 5.add_excutable() 和 add_library() 分別用於生成可執行文件與庫。構建 android so 庫的時候 可以使用 add_library()。target_link_libraries() 用於連接n個互相之間有依賴關係的庫
  • 6.message([] "message to display") 這個方法用於輸出日誌

(6).CMake流程語句

  • 1.if:用法相似c語言,在使用參數的時候不須要用${}來取值
  • 2.foreach:foreach(loop_var 1 2 3) ... endforeach(loop_var) 或者 foreach(loop_var RANGE 4) ... endforeach(loop_var) 或者 foreach(loop_var RANGE 0 3 1) ... endforeach(loop_var) 從 0 到 3,1是步伐
  • 3.while:while(condition) ... endwhile(condition)
  • 4.foreach 和 while 可用 break 和 continue,在循環中使用${}進行取值
  • 5.可用 option 和 if 進行配合。option(<option_var> "description" [initial_var])

(7).宏與方法

  • 1.macro( [a1 [a2 [a3 ...]) ... endmacro(),可在內部使用 ${a1} 來引用變量,
    • 1.ARGV#,#是下標,可用於引用變量
    • 2.ARGV,表示全部傳入變量
    • 3.ARGN,傳入了須要參數之外的參數
    • 4.ARGC,傳入的參數總個數
    • 5.macro 是字符替換,相似 c 語言中的預處理,因此在 if 中使用的時候須要 ${} 來獲取參數
  • 2.function( [a1 [a2 [a3 ...]) ... endmacro(),與 macro 相似,可是不是字符替換, 是實實在在的調用函數。

4、FFmpeg官方demo講解

先上一個項目:FFmpeg-learing,之後關於 FFmpeg 的 demo 都會添加到這個項目中去,你們看博客的時候仍是須要結合這個項目一塊兒看。算法

1.項目結構

  • 1.首先先了解一下這個項目的結構吧,如圖1:
    • 1.圖1的 java 目錄下面我想你們應該都清楚,放的是開發 android 的 java 文件
    • 2.而後咱們看 jni/ffmpeg 這個目錄中有三個文件夾:
      • 1.armeabi:這裏放的是 so 文件,這裏的 so 文件是我從 ffmpeg 的源碼中編譯過來的。每個 so 文件都對應着咱們在第二章中講解的一個 FFmpeg 的模塊代碼。
      • 2.include:這裏放的是 FFmepg 各個模塊暴露出來的 .h 文件,也就是說咱們須要經過 .h 文件中的函數定義來調起各個 so 文件中的函數實現。
      • 3.my:這裏放的是我寫的代碼。

圖1:項目結構 水印.png

  • 2.再來看看項目中的 Cmake 文件,由於 android studio 目前支持 Cmake 文件來管理 android 中的 Cpp代碼。
    • 1.若是第三章你認真看過了的話,那麼這裏應該也很好理解。這裏主要新增了兩個咱們以前沒有講到的 cmake 命令:
      • 1.find_library:這個命令主要是用來尋找本地存在的庫的路徑的,在這裏我去尋找 log 這個庫在本地的路徑而後將其賦值給 log-lib 這個參數。使用 message 輸出一個 ${log-lib} 咱們能夠發現其就是 android ndk 目錄下面的 liblog.so 文件,其主要用於 android 的日誌輸出。除此以外,你可使用這個命令去尋找你在本地擁有的各類so文件。
      • 2.set_target_properties:這個命令主要是將各類 so 文件的路徑轉化成簡單的值。 例如:set_target_properties( postproc-54 PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi/libpostproc-54.so) 這裏就是將 so 文件的地址轉化成 postproc-54 這個簡短的名字以便後面使用。
      • 3.剩下來的代碼我就簡單說了:主要就是將一個個 so 文件聲明成一個個 library,最後使用target_link_libraries 命令將我寫的代碼與各個 so 文件的 library 再連接起來,這樣最終就能將全部的 Cpp 代碼打包到 android app 中去。

圖2:cmake文件1 水印.png

圖3:cmake文件2 水印.png

2.FFmpeg讀取視頻文件信息

**咱們先來看第一個官方文檔中的 Demo:從視頻文件中讀取視頻信息。 **

  • 1.首先根據咱們在第二章節描述的多媒體概念咱們能夠知道:視頻的數據有許多的封裝格式,好比 MP四、avi、flv 等等。在 FFmpeg 中用於處理這些視頻格式的 struct (由於 FFmpeg 使用 c 寫的因此,內部尚未類的概念。)就是 AVFormatContext。你們能夠進入這個 struct 能夠看見其定義其實和 java 中的 class 相似。有成員變量,有函數指針(用於代替成員函數)。
  • 2.有了解析視頻數據封裝格式的 struct,咱們還須要一個能從文件中讀取數據的東西。在 FFmpeg 中這個東西就是 AVIOContext,這個東西是 AVFormatContext 的成員變量,用於從不斷的從文件中讀取數據,而後將數據送給 AVFormatContext 解析。
  • 3.科普了兩個 struct 咱們就能夠講解 demo 了。入口是下面代碼中的 av_io_reading 方法,這個方法的入參是 argc 表示 argv 數組的數量,argv 中有兩個參數 分別表示輸入文件與輸出文件。注意:接下來我在文章中講解的 FFmpeg 的方法,已經下載過項目的同窗能夠直接去方法定義的地方查看,我講過的方法的文檔我都翻譯成便於理解的中文了。
    • 1.首先在前面定義了一堆變量
      • 1.好比咱們前面說的兩個 struct。
      • 2.而後是定義了兩個 unit8_t 的指針,其實 unit8_t 就是 unsigned char 你們能夠進入看看它的定義。而熟悉 c 的同窗應該知道,unsigned char 指針其實通常指向的就是一塊內存相似於 java 中的 byte 數組。
      • 3.而後定義了兩個 size_t 分別表示2中定義的兩個 unit8_t 指針指向的內存大小。
      • 4.而後定義了兩個 char 指針,分別表示輸入輸出文件。
      • 5.最後定義了一個 ret 表示本方法的返回值,和一個 buffer_data 類型的 struct ,這個是咱們本身定義的,封裝了 unit8_t 指針與 size_t,這樣方便一點。
    • 2.接下來咱們直接到 av_file_map 這個方法,這個方法簡單來講就是將:input_filename 這個文件中的數據使用 mmap() 映射到內存中,而後用 buffer 指針指向這塊內存,而後將這塊內存的大小交給 buffer_size 指針
    • 3.跳過中間的一些代碼咱們來到 avformat_alloc_context 這個方法,這個方法很簡單:**就是初始化一個AVFormatContext **
    • 4.而後再到 av_malloc 方法,咱們用這個方法讓 avio_ctx_buffer 指針指向了一個 4kb 的內存區域,這塊內存用於後面不斷的從 buffer 中以 4kb 的量讀取數據。關鍵字是內存對齊參考資料
    • 5.咱們接下來到了 avio_alloc_context 方法,這個方法是用於初始化一個 AVIOContext。這裏咱們傳入了幾個參數我來解釋一下:
      • 1.首先是 avio_ctx_buffer 和其對應的 size。咱們在4中說了,以後從 buffer 中讀取數據都是用這個內存塊讀取,而 AVIOContext 就是調用這個讀取的對象。
      • 2.而後傳入了一個 buffer_data 類型的地址和一個函數的地址 read_packet。其實這裏很相似咱們在 java 中使用的回調。**AVIOContext 不會負責真正的從 buffer 中取數據到 buffer_data 的過程。他只須要在適當的時候調用 read_packet,其中填充 buffer_data 的邏輯由咱們來實現。**若是你手上有代碼,去查看定義會發現,下一個 NULL 的參數是一個用於將 buffer_data 寫入到某個地方的函數。
    • 6.接下來就到了 avformat_open_input 這個方法,這個方法用起來也簡單就是將咱們前面構建的 AVFormatContext 讓其將咱們在前面定義的AVIOContext 以流的方式來讀取。這裏會先讀取文件的 header 也就是咱們在第二章中提到的 文件頭,這裏面有着視頻文件的各類信息。
    • 7.最後兩個方法 avformat_find_stream_info 和 av_dump_format 就比較簡單了,一個是解析6中流的信息,一個是將視頻封裝文件的信息輸出到文件中。
    • 8.後面的工做就是釋放前面申請的各類內存空間了,c 不像 java 有垃圾回收機制,咱們前面說的不少建立struct 的方法都有對應的釋放內存的方法,我在項目中的方法定義處都一一翻譯了。
    • 9.講到這裏我想不少同窗可能會一臉懵逼,這也是正常的,畢竟只是調用一個個方法而不知道內部是咋實現的,心中確定會很是的虛。並且一些數據結構也不知道有啥用,內部實現是啥。不過別擔憂,這只是音視頻的開篇,事情總得一步步來。後續我也會帶你們深刻 FFmpeg 的源代碼,而後模仿着公司的代碼寫一些企業級的可用代碼。
struct buffer_data {
    uint8_t *ptr;
    size_t size; ///< size left in the buffer
};
static int read_packet(void *opaque, uint8_t *buf, int buf_size)
{
    struct buffer_data *bd = (struct buffer_data *)opaque;
    buf_size = FFMIN(buf_size, bd->size);

    if (!buf_size)
        return AVERROR_EOF;
    printf("ptr:%p size:%zu\n", bd->ptr, bd->size);

    /* copy internal buffer data to buf */
    memcpy(buf, bd->ptr, buf_size);
    bd->ptr  += buf_size;
    bd->size -= buf_size;

    return buf_size;
}

int av_io_reading(int argc, char *argv[])
{
    syslog_init();
    AVFormatContext *fmt_ctx = NULL;
    AVIOContext *avio_ctx = NULL;
    uint8_t *buffer = NULL, *avio_ctx_buffer = NULL;
    size_t buffer_size, avio_ctx_buffer_size = 4096;
    char *input_filename = NULL;
    char *output_filename = NULL;
    int ret = 0;
    struct buffer_data bd = { 0 };

    if (argc != 2) {
        fprintf(stderr, "usage: %s input_file\n"
                "API example program to show how to read from a custom buffer "
                "accessed through AVIOContext.\n", argv[0]);
        return 1;
    }
    input_filename = argv[0];
    output_filename = argv[1];

    // 將 input_filename 指向的文件數據讀取出來,而後用 buffer 指針指向他,buffer_size 中存有 buffer 內存的大小
    ret = av_file_map(input_filename, &buffer, &buffer_size, 0, NULL);
    if (ret < 0)
        goto end;

    bd.ptr  = buffer;
    bd.size = buffer_size;

    if (!(fmt_ctx = avformat_alloc_context())) {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    // 申請四個字節大小的緩衝區,在後面做爲內存對齊的標準使用
    avio_ctx_buffer = (uint8_t *) av_malloc(avio_ctx_buffer_size);
    if (!avio_ctx_buffer) {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    avio_ctx = avio_alloc_context(avio_ctx_buffer, avio_ctx_buffer_size,
                                  0, &bd, &read_packet, NULL, NULL);
    if (!avio_ctx) {
        ret = AVERROR(ENOMEM);
        goto end;
    }
    fmt_ctx->pb = avio_ctx;

    ret = avformat_open_input(&fmt_ctx, NULL, NULL, NULL);
    if (ret < 0) {
        fprintf(stderr, "Could not open input\n");
        goto end;
    }

    ret = avformat_find_stream_info(fmt_ctx, NULL);
    if (ret < 0) {
        fprintf(stderr, "Could not find stream information\n");
        goto end;
    }

    av_dump_format(fmt_ctx, 0, output_filename , 0);

    end:
    avformat_close_input(&fmt_ctx);
    /* note: the internal buffer could have changed, and be != avio_ctx_buffer */
    if (avio_ctx) {
        av_freep(&avio_ctx->buffer);
        av_freep(&avio_ctx);
    }
    av_file_unmap(buffer, buffer_size);

    char buf2[500] = {0};
    av_strerror(ret, buf2, 1024);
    if (ret < 0) {
        fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
        return 1;
    }

    return 0;
}
複製代碼

3.聲明

原本講兩個官方 Demo 的,可是篇幅有限就到此爲止吧。我在項目中其實已經集成了編碼視頻解碼視頻的 demo。各個方法的定義處也有中文解釋,有興趣的同窗能夠自行查看。還要說的一件事情是,由於時間有限,其實項目裏的不少東西是不能保證運行成功的,這個問題我後面若是都測試經過了會在 commit 裏面聲明。

5、尾巴

音視頻開篇總算寫完了,有個「偉人」說得好:你知道的越多,你不知道的就越多——什麼時候夕。我最近也感受到了本身的許多不足之處,天天早晨騎車上班的時候都會反思一下前一天作的很差的地方。吾日三省吾身,這句話無論在什麼年代都不過期啊,共勉!!!

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、互聯網、程序員、計算機編程。下面是個人微信公衆號:世界上有意思的事,乾貨多多等你來看。

世界上有意思的事
相關文章
相關標籤/搜索