前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始仿写一个抖音App——音视频开篇GitHub地址

从零开始仿写一个抖音App——音视频开篇GitHub地址

作者头像
何时夕
发布2018-12-12 17:47:05
2.2K0
发布2018-12-12 17:47:05
举报
文章被收录于专栏:求索之路

GitHub地址

大家好,距离上次本专题发文已经有五个星期了,中间发了两篇非本专题的文章,可能很多人都以为我要弃坑了。但是并不是这回事,主要是工作有点忙,而且我在音视频方面其实也有许多东西需要学习和整理。那么从本篇文章开始我们就要进入音视频领域进行研究学习了,Android 领域的文章会在中间整合音视频代码的时候进行穿插讲解。其实 Android 里面要讲的东西还是挺多的,奈何时间不等人。废话不多说,我们进入文章。本文预计阅读时间二十分钟。

本文分为以下章节,读者可以按需阅读

  • 1.聊一聊——主要是公布一些事情,没兴趣的同学可以跳过。
  • 2.音视频前置知识——列一列学习音视频技术之前需要知道的东西。
  • 3.cmake 入门——了解一个 c/c++ 工程的组织与编译。
  • 4.ffmpeg 入门——入门 ffmepg,讲解一个官方 demo。

一、聊一聊

  • 1.有些朋友可能会发现本专题下掉了两篇文章,这个问题在这里说明一下。下掉文章是因为我的个人原因,这两篇文章我会在适当的时间修改好之后再重新发布。如果实在有同学有需要的话可以私聊我,我发给你。
  • 2.简书的朋友可能会发现,我的个人界面上多了简书版权这个标签,这个标签表示我已经与简书签约,在积累了一定的存稿之后会与简书合作出版一本关于 Android+音视频技术+深度学习+短视频 方面的书籍。书的话预计会在半年之后开始写,现在先预热一下,哈哈:)。
  • 3.关注了我的 Github 的同学会看见我将 MyTikTok 这个项目移动到了 TheGodsThemselves 这个组织内,以后关于本专题的正式项目也会都发布到这里面去,或许再过不久就会有第一个同学加入到本组织中来。另外这里重新说明一下本项目的理想目标:完整的复刻大厂的项目流程,写出一个短视频 App(暂定模仿抖音),在项目中会用到各个端(包括不限于 Android、IOS、后台、前端、算法、音视频)的有意思的技术,让参与和关注本项目的同学能够学习到自己感兴趣的技术栈(大厂的真实开发经验)。

二、音视频前置知识

其实我在 我的技术成长之路 中已经大概讲解了学习音视频技术需要学习哪些东西,在这一节我会讲些具体的东西,当然也只是一个粗浅的入门,更加深入的知识还是需要读者自己去积累。

1.多媒体概念

  • 1.视频格式:usb 摄像头的输出格式有RGB24、YUV2、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 暴露出来的函数对视频进行处理。

三、Cmake入门

Cmake 是组织 C/Cpp 项目的一个工具,类似我们在 android 中使用的 gradle。我们要写一个大一点的工具,Cmake 这种项目管理工具是必不可少的。这一节就来入门一下 Cmake,注意下面的教程是 官方教程 的翻译。

这是本章节对应的项目:cmake_learning项目

1.编译器准备

我因为主力机是 Mac,所以使用的 IDE 是 CLion,CLion 也是 JetBrain 全家桶的成员之一。使用了 Android Studio 或者 IDEA 的同学可以很方便的切换到这个 IDE 上。此外 CLion 还是一个跨平台的 IDE,也就是说在 Windows Linux 上面也可以使用它。当然 Visual Studio 永远是最强的 IDE(手动狗头)。需要注意的是 CLion 是需要花钱买激活码的,似乎没有免费版开始能免费试用一个月左右的时间,所以激活码的获取途径大家就各显神通吧。

2.Cmake

(1).最基本的Cmake程序
  • 1.我们进入项目中 one/a 的目录发现下面有两个文件:CMakeLists.txt 和 tutorial.cpp 里面的代码如下:
代码语言:txt
复制
- 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", argv0); return 1; } double inputValue = atof(argv1); 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 里面的代码如下:
代码语言:txt
复制
- 1.在 Tutorial\_A 这个项目中声明了两个参数,然后在TutorialConfig.h.in 文件引用了这两个参数,cmake 会根据这个文件生成一个名为 TutorialConfig.h 的文件。
- 2.我们在 tutorial.cpp 中使用了 TutorialConfig.h,也就使用了 cmake 文件中定义的参数。这和我们在开发 android 的时候在 gradle 文件中定义参数最后在 java 代码中使用非常类似。
- 3.我们接下来在 build 文件中依次运行 **cmake ..** 、 **make**、**./Tutorial\_A**。会发现输出了我们使用的参数。
代码语言:javascript
复制
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)
代码语言:javascript
复制
// 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;
}
代码语言:javascript
复制
// 这个是配置文件,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.定义了一个计算平方根的函数,然后使用头文件暴露在外面
代码语言:javascript
复制
cmake_minimum_required (VERSION 2.6)
# 声明了一个 library 名为 MathFunctions,他包含一个可执行文件 mysqrt.cpp
add_library(MathFunctions mysqrt.cpp)
代码语言:javascript
复制
#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;
}
代码语言:javascript
复制
//
// 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 一遍,会发现最后调用的是系统的函数。
代码语言:javascript
复制
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})
代码语言:javascript
复制
#cmakedefine USE_MYMATH
代码语言:javascript
复制
#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 电脑的路径。
代码语言:javascript
复制
# 安装这个库,将库和头文件分别添加到 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)
代码语言:javascript
复制
# 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 这个文件
代码语言:javascript
复制
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)
代码语言:javascript
复制
//
// 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;
}
代码语言:javascript
复制
#cmakedefine USE_MYMATH
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP
(5).CMake语法
  • 1.<command>必填、command可填、a|b 均可
  • 2.cmake 可以三种形式组织文件:
    • 1.CMakeList.txt:文件夹形式
    • 2.<script>.cmake:脚本形式
    • 3.<module>.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(<mode> "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(<name> [a1 [a2 a3 ...) ... endmacro(<name>),可在内部使用 ${a1} 来引用变量,
    • 1.ARGV#,#是下标,可用于引用变量
    • 2.ARGV,表示所有传入变量
    • 3.ARGN,传入了需要参数以外的参数
    • 4.ARGC,传入的参数总个数
    • 5.macro 是字符替换,类似 c 语言中的预处理,所以在 if 中使用的时候需要 ${} 来获取参数
  • 2.function(<name> [a1 [a2 a3 ...) ... endmacro(<name>),与 macro 类似,但是不是字符替换, 是实实在在的调用函数。

四、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.首先根据我们在第二章节描述的多媒体概念我们可以知道:视频的数据有许多的封装格式,比如 MP4、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 的源代码,然后模仿着公司的代码写一些企业级的可用代码。
代码语言:javascript
复制
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 里面声明。

五、尾巴

音视频开篇总算写完了,有个“伟人”说得好:你知道的越多,你不知道的就越多——何时夕。我最近也感觉到了自己的许多不足之处,每天早晨骑车上班的时候都会反思一下前一天做的不好的地方。吾日三省吾身,这句话不管在什么年代都不过时啊,共勉!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.11.12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • GitHub地址
    • 一、聊一聊
      • 二、音视频前置知识
        • 1.多媒体概念
        • 2.FFmpeg基本概念
      • 三、Cmake入门
        • 1.编译器准备
        • 2.Cmake
    • 一个 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
      • 四、FFmpeg官方demo讲解
        • 1.项目结构
        • 2.FFmpeg读取视频文件信息
        • 3.声明
      • 五、尾巴
        相关产品与服务
        语音识别
        腾讯云语音识别(Automatic Speech Recognition,ASR)是将语音转化成文字的PaaS产品,为企业提供精准而极具性价比的识别服务。被微信、王者荣耀、腾讯视频等大量业务使用,适用于录音质检、会议实时转写、语音输入法等多个场景。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档