1.文章介绍
在应用开发中,你是否面临着第三方库的FFMPEG不是最新版本,或者需要使用扩展自己的业务的FFMPEG版本等问题,一旦遇到这些问题,你会怎么处理?
本篇文章是把FFMPEG的libavcodec,libavformat,libavutil,libavfilter等移植到Android设备上,并且使用这些API完成音视频转码功能,作为FFMPEG进阶使用。
2.干货
1.准备编译工具
Android NDK编译环境:
关于NDK的下载我就不多说了,基本百度就能搞定。
android-ndk-r14b
为了方便配置NDK编译环境,可以添加如下脚本:
#! /bin/sh
set -e
############
# Configuring Android NDK #
############
Env_Dir=$(pwd)
echo "#########config android ndk############"
export ANDROID_NDK="$Env_Dir/android-ndk-r14b"
echo ""
echo "ANDROID_NDK:$ANDROID_NDK"
echo ""
2.git源码到linux编译环境下:
git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
3.配置Android编译脚本:
搞音视频开发,相信对avc和aac的编解码库再熟悉不过了,那么这篇文章该涉及到X264,libfdk-aac的交叉编译了。
参考了某某开源项目的编译脚本配置为合适自己的编译脚本,基于最新的FFMPEG源码,编译出可在Android终端上使用的FFMPEG库
#!/bin/bash
#Detect ANDROID_NDK
export ANDROID_NDK=/home3/yangwu/build_tools/android-ndk-r14b
NDK_TOOLCHAIN_VERSION=4.9
ANDROID_PLATFROM_VERSION=android-19
if [ -z "$ANDROID_NDK" ]; then
echo "You must define ANDROID_NDK before starting."
echo "You must point to your NDK directories.\n"
exit 1
fi
#Detect OS
OS=`uname`
HOST_ARCH=`uname -m`
export CCACHE=; type ccache >/dev/null 2>&1 && export CCACHE=ccache
if [ $OS == 'Linux' ]; then
export HOST_SYSTEM=linux-$HOST_ARCH
elif [ $OS == 'Darwin' ]; then
export HOST_SYSTEM=darwin-$HOST_ARCH
fi
platform="$1"
version_type="$2"
function arm_toolchain()
{
export CROSS_PREFIX=arm-linux-androideabi-
$ANDROID_NDK/build/tools/make-standalone-toolchain.sh --toolchain=${CROSS_PREFIX}${NDK_TOOLCHAIN_VERSION} --platform=${ANDROID_PLATFROM_VERSION} --install-dir=$TOOLCHAIN --arch=arm --force
#--system=$HOST_SYSTEM #ndk4.9 do not support --system
}
function x86_toolchain()
{
export CROSS_PREFIX=i686-linux-android-
$ANDROID_NDK/build/tools/make-standalone-toolchain.sh --toolchain=x86-${NDK_TOOLCHAIN_VERSION} --platform=${ANDROID_PLATFROM_VERSION} --install-dir=$TOOLCHAIN --arch=x86 --force
#--system=$HOST_SYSTEM #ndk4.9 do not support --system
}
function mips_toolchain()
{
export CROSS_PREFIX=mipsel-linux-android-
$ANDROID_NDK/build/tools/make-standalone-toolchain.sh --toolchain=${CROSS_PREFIX}${NDK_TOOLCHAIN_VERSION} --platform=${ANDROID_PLATFROM_VERSION} --install-dir=$TOOLCHAIN --arch=mips --force
#--system=$HOST_SYSTEM #ndk4.9 do not support --system
}
SOURCE=`pwd`
DEST=$SOURCE/build/android
TOOLCHAIN=$SOURCE/ffmpeg_toolchain
SYSROOT=$TOOLCHAIN/sysroot/
function download {
mkdir -p "$SOURCE/downloads"
if [[ ! -e "$SOURCE/downloads/$2" ]]; then
echo "Downloading $1"
curl -L "$1" -o "$SOURCE/downloads/$2"
fi
}
if [ "$platform" = "x86" ];then
echo "Build Android x86 ffmpeg\n"
x86_toolchain
TARGET="x86"
TARGET_HOST="x86-linux-android"
PLATFORM="arch-x86"
elif [ "$platform" = "mips" ];then
echo "Build Android mips ffmpeg\n"
mips_toolchain
TARGET="mips"
TARGET_HOST="mipsel-linux-android"
PLATFORM="arch-mips"
elif [ "$platform" = "armv7" ];then
echo "Build Android armv7 ffmpeg\n"
arm_toolchain
TARGET="armv7"
TARGET_HOST="arm-linux-android"
PLATFORM="arch-arm"
else
echo "Build Android arm ffmpeg\n"
arm_toolchain
TARGET="neon armv7 vfp armv6"
TARGET_HOST="arm-linux-android"
PLATFORM="arch-arm"
fi
export PATH=$TOOLCHAIN/bin:$PATH
export CC="$CCACHE ${CROSS_PREFIX}gcc"
export CXX=${CROSS_PREFIX}g++
export LD=${CROSS_PREFIX}ld
export AR=${CROSS_PREFIX}ar
export STRIP=${CROSS_PREFIX}strip
#set ffmpeg dep libs here
echo "Decompressing archives..."
OPENH264_VERSION=1.6.0
FDKACC_VERSION=0.1.6
OPENSSL_VERSION=openssl-1.0.2j
download https://github.com/cisco/openh264/archive/v$OPENH264_VERSION.tar.gz openh264-$OPENH264_VERSION.tar.gz
download ftp://ftp.videolan.org/pub/videolan/x264/snapshots/last_stable_x264.tar.bz2 last_stable_x264.tar.bz2
#download https://downloads.sourceforge.net/opencore-amr/fdk-aac-$FDKACC_VERSION.tar.gz
download https://www.openssl.org/source/$OPENSSL_VERSION.tar.gz $OPENSSL_VERSION.tar.gz
tar --totals -xzf $SOURCE/downloads/openh264-$OPENH264_VERSION.tar.gz -C $SOURCE/downloads/
tar --totals -xjf $SOURCE/downloads/last_stable_x264.tar.bz2 -C $SOURCE/downloads/
tar --totals -xzf $SOURCE/downloads/fdk-aac-$FDKACC_VERSION.tar.gz -C $SOURCE/downloads/
tar --totals -xzf $SOURCE/downloads/$OPENSSL_VERSION.tar.gz -C $SOURCE/downloads/
X264=`echo $SOURCE/downloads/x264-snapshot-*`
FDKACC=`echo $SOURCE/downloads/fdk-aac-*`
OpenSSL=`echo $SOURCE/downloads/openssl-*`
CFLAGS="-std=c99 -O3 -Wall -pipe -fpic -fasm -finline-limit=300 -ffast-math -fstrict-aliasing -Wno-psabi -Wa,--noexecstack -fdiagnostics-color=always -DANDROID -DNDEBUG"
LDFLAGS="-lm -lz -Wl,--no-undefined -Wl,-z,noexecstack"
case $CROSS_PREFIX in
arm-*)
CFLAGS="-mthumb $CFLAGS -D__ARM_ARCH_5__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5TE__"
;;
x86-*)
;;
mipsel-*)
CFLAGS="-std=c99 -O3 -Wall -pipe -fpic -fasm -ftree-vectorize -ffunction-sections -funwind-tables -fomit-frame-pointer -funswitch-loops -finline-limit=300 -finline-functions -fpredictive-commoning -fgcse-after-reload -fipa-cp-clone -Wno-psabi -Wa,--noexecstack -DANDROID -DNDEBUG"
;;
esac
if [ "$version_type" = "online" ]; then
FFMPEG_FLAGS_COMMON="--target-os=android --cross-prefix=$CROSS_PREFIX --enable-cross-compile --enable-version3 --enable-shared --disable-static --disable-symver --disable-programs --disable-doc --disable-avdevice --disable-encoders --disable-muxers --disable-devices --disable-everything --disable-protocols --disable-demuxers --disable-decoders --disable-bsfs --disable-debug --enable-optimizations --enable-filters --enable-parsers --disable-parser=hevc --enable-swscale --enable-network --enable-protocol=file --enable-protocol=http --enable-protocol=rtmp --enable-protocol=rtp --enable-protocol=mmst --enable-protocol=mmsh --enable-protocol=crypto --enable-protocol=hls --enable-demuxer=hls --enable-demuxer=mpegts --enable-demuxer=mpegtsraw --enable-demuxer=mpegvideo --enable-demuxer=concat --enable-demuxer=mov --enable-demuxer=flv --enable-demuxer=rtsp --enable-demuxer=mp3 --enable-demuxer=matroska --enable-decoder=mpeg4 --enable-decoder=mpegvideo --enable-decoder=mpeg1video --enable-decoder=mpeg2video --enable-decoder=h264 --enable-decoder=h263 --enable-decoder=flv --enable-decoder=vp8 --enable-decoder=wmv3 --enable-decoder=aac --enable-decoder=ac3 --enable-decoder=mp3 --enable-decoder=nellymoser --enable-muxer=mp4 --enable-asm --enable-pic"
else
FFMPEG_FLAGS_COMMON="--target-os=android --cross-prefix=$CROSS_PREFIX --enable-cross-compile --enable-version3 --enable-shared --disable-static --disable-symver --disable-programs --disable-doc --disable-avdevice --disable-encoders --enable-libx264 --enable-gpl --enable-libfdk_aac --enable-nonfree --enable-encoder=libx264 --enable-encoder=libfdk_aac --disable-muxers --enable-muxer=mp4 --enable-muxer=mpegts --disable-devices --disable-demuxer=sbg --disable-demuxer=dts --disable-parser=dca --disable-decoder=dca --disable-decoder=svq3 --enable-optimizations --disable-fast-unaligned --disable-postproc --enable-network --enable-asm --enable-openssl"
fi
for version in $TARGET; do
cd $SOURCE
FFMPEG_FLAGS="$FFMPEG_FLAGS_COMMON"
case $version in
neon)
FFMPEG_FLAGS="--arch=armv7-a --cpu=cortex-a8 --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv7-a -mfpu=neon -mfloat-abi=softfp -mvectorize-with-neon-quad"
EXTRA_LDFLAGS="-Wl,--fix-cortex-a8"
SSL_OBJS=""
;;
armv7)
FFMPEG_FLAGS="--arch=armv7-a --cpu=cortex-a8 --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=softfp"
EXTRA_LDFLAGS="-Wl,--fix-cortex-a8"
SSL_OBJS=""
;;
vfp)
FFMPEG_FLAGS="--arch=arm --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv6 -mfpu=vfp -mfloat-abi=softfp"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
armv6)
FFMPEG_FLAGS="--arch=arm --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv6 -msoft-float"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
x86)
FFMPEG_FLAGS="--arch=x86 --cpu=i686 --enable-runtime-cpudetect --enable-yasm --disable-amd3dnow --disable-amd3dnowext $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=atom -msse3 -ffast-math -mfpmath=sse"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
mips)
FFMPEG_FLAGS="--arch=mips --cpu=mips32r2 --enable-runtime-cpudetect --enable-yasm --disable-mipsfpu --disable-mipsdspr1 --disable-mipsdspr2 $FFMPEG_FLAGS"
EXTRA_CFLAGS="-fno-strict-aliasing -fmessage-length=0 -fno-inline-functions-called-once -frerun-cse-after-loop -frename-registers"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
*)
FFMPEG_FLAGS=""
EXTRA_CFLAGS=""
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
esac
PREFIX="$DEST/$version" && rm -rf $PREFIX && mkdir -p $PREFIX
FFMPEG_FLAGS="$FFMPEG_FLAGS --prefix=$PREFIX"
# build OpenSSL
cd $OpenSSL
./Configure --prefix=$PREFIX android-$TARGET $CFLAGS $EXTRA_CFLAGS no-shared
[ $PIPESTATUS == 0 ] || exit 1
make -j12 || exit 1
make install
# build X264
cd $X264
./configure --prefix=$PREFIX --enable-static --enable-pic --disable-cli --cross-prefix=$CROSS_PREFIX --sysroot=$SYSROOT --host=$TARGET_HOST --extra-cflags="$CFLAGS $EXTRA_CFLAGS" --extra-ldflags="$LDFLAGS $EXTRA_LDFLAGS"
[ $PIPESTATUS == 0 ] || exit 1
make -j12 || exit 1
make install
# build FDKACC
cd $FDKACC
./configure --prefix=$PREFIX --with-sysroot=$ANDROID_NDK/platforms/$ANDROID_PLATFROM_VERSION/$PLATFORM --host=$TARGET_HOST
[ $PIPESTATUS == 0 ] || exit 1
make -j12 || exit 1
make install
# build ffmpeg
cd $SOURCE
./configure $FFMPEG_FLAGS --extra-cflags="$CFLAGS $EXTRA_CFLAGS -I$DEST/$TARGET/include" --extra-ldflags="$LDFLAGS $EXTRA_LDFLAGS -L$DEST/$TARGET/lib" | tee $PREFIX/configuration.txt
cp config.* $PREFIX
[ $PIPESTATUS == 0 ] || exit 1
make clean
find . -path $TOOLCHAIN -prune -name "*.o" -type f -delete
make -j12 || exit 1
make examples
make install
echo "----------------------$version -----------------------------"
done
以上脚本参考了其他开源项目中的思路,并且额外添加了自己的业务需求。
注意:在FFMPEG的configure配置时,添加你业务上需要的encoder:
--enable-libx264 \
--enable-gpl
--enable-encoder=libx264 \
--enable-libfdk-aac \
--enable-nonfree
--enable-encoder=libfdk-aac \
编译时比较重要的配置,连接对应的x264和fdk_aac:
-I$DEST/$TARGET/include
-L$DEST/$TARGET/lib
可以看到demux,decode,filter,encode,mux成功输出:
关键问题:
- aac编码时由于编码器的采样率和输入数据采样率不一致,需要等缓存输入数据到编码器的size
- 转码后的数据如何通过封装成网络传输流的格式进行实时测试
/*
* Copyright (c) 20180510 yangwu
* 基本思路:解复用->视频+音频流->解码->YUV/PCM等->音/视频编码->重新生成的音视频流->复用->合成流
*/
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libavutil/audio_fifo.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
#include <libavutil/time.h>
#define ENCODE_TYPE_VIDEO AV_CODEC_ID_H264
#define ENCODE_TYPE_AUDIO AV_CODEC_ID_MP2
#define DECODE_THREADS 1
#define ENCODE_THREADS 1
static AVFormatContext *ifmt_ctx;
static AVFormatContext *ofmt_ctx;
typedef struct StreamContext {
AVCodecContext *dec_ctx;
AVCodecContext *enc_ctx;
} StreamContext;
static StreamContext *stream_ctx;
static int disable_audio = 0;
static int disable_video = 0;
static int open_input_file(const char *filename)
{
int ret;
unsigned int i;
ifmt_ctx = NULL;
if ((ret = avformat_open_input(&ifmt_ctx, filename, NULL, NULL)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
return ret;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
return ret;
}
stream_ctx = av_mallocz_array(ifmt_ctx->nb_streams, sizeof(*stream_ctx));
if (!stream_ctx){
return AVERROR(ENOMEM);
}
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
//add for single stream mode
if(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && disable_audio){
continue;
}else if(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && disable_video){
continue;
}
AVStream *stream = ifmt_ctx->streams[i];
AVCodec *dec = avcodec_find_decoder(stream->codecpar->codec_id);
AVCodecContext *codec_ctx;
if (!dec) {
av_log(NULL, AV_LOG_ERROR, "Failed to find decoder for stream #%u\n", i);
return AVERROR_DECODER_NOT_FOUND;
}
codec_ctx = avcodec_alloc_context3(dec);
if (!codec_ctx) {
av_log(NULL, AV_LOG_ERROR, "Failed to allocate the decoder context for stream #%u\n", i);
return AVERROR(ENOMEM);
}
ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to copy decoder parameters to input decoder context "
"for stream #%u\n", i);
return ret;
}
/* Reencode video & audio and remux subtitles etc. */
if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO || codec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO){
codec_ctx->framerate = av_guess_frame_rate(ifmt_ctx, stream, NULL);
}
codec_ctx->thread_count = DECODE_THREADS;
codec_ctx->thread_type = FF_THREAD_FRAME;//FF_THREAD_SLICE;//FF_THREAD_FRAME
/* Open decoder */
ret = avcodec_open2(codec_ctx, dec, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to open decoder for stream #%u\n", i);
return ret;
}
}
stream_ctx[i].dec_ctx = codec_ctx;
}
av_dump_format(ifmt_ctx, 0, filename, 0);
return 0;
}
static int open_output_file(const char *filename)
{
AVStream *out_stream;
AVStream *in_stream;
AVCodecContext *dec_ctx, *enc_ctx;
AVCodec *encoder;
int ret;
unsigned int i;
ofmt_ctx = NULL;
avformat_alloc_output_context2(&ofmt_ctx, NULL, "rtp_mpegts", NULL);
if (!ofmt_ctx) {
av_log(NULL, AV_LOG_ERROR, "Could not create output context\n");
return AVERROR_UNKNOWN;
}
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
//for mux just creat a stream here
out_stream = avformat_new_stream(ofmt_ctx, NULL);
if (!out_stream) {
av_log(NULL, AV_LOG_ERROR, "Failed allocating output stream\n");
return AVERROR_UNKNOWN;
}
//add for single stream mode
if(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && disable_audio){
continue;
}else if(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && disable_video){
continue;
}
in_stream = ifmt_ctx->streams[i];
dec_ctx = stream_ctx[i].dec_ctx;
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO || dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
if(dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO){
encoder = avcodec_find_encoder(ENCODE_TYPE_AUDIO);
}else if(dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO){
encoder = avcodec_find_encoder(ENCODE_TYPE_VIDEO);
}
if (!encoder) {
av_log(NULL, AV_LOG_FATAL, "Necessary %s encoder not found\n",(dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO)?"audio":"video");
return AVERROR_INVALIDDATA;
}
enc_ctx = avcodec_alloc_context3(encoder);
if (!enc_ctx) {
av_log(NULL, AV_LOG_FATAL, "Failed to allocate the encoder context\n");
return AVERROR(ENOMEM);
}
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
enc_ctx->height = dec_ctx->height;
enc_ctx->width = dec_ctx->width;
enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio;
/* take first format from list of supported formats */
if (encoder->pix_fmts){
enc_ctx->pix_fmt = encoder->pix_fmts[0];
}else{
enc_ctx->pix_fmt = dec_ctx->pix_fmt;
}
/* video time_base can be set to whatever is handy and supported by encoder */
enc_ctx->time_base = av_inv_q(dec_ctx->framerate);
//Constrained Baseline
enc_ctx->profile = FF_PROFILE_H264_CONSTRAINED_BASELINE;
} else if(dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO){
enc_ctx->sample_rate = dec_ctx->sample_rate;
enc_ctx->channel_layout = dec_ctx->channel_layout;
enc_ctx->channels = av_get_channel_layout_nb_channels(enc_ctx->channel_layout);
/* take first format from list of supported formats */
enc_ctx->sample_fmt = encoder->sample_fmts[0];
enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate};
}
enc_ctx->thread_count = ENCODE_THREADS;
enc_ctx->thread_type = FF_THREAD_FRAME;//FF_THREAD_SLICE;//FF_THREAD_FRAME
/* Third parameter can be used to pass settings to encoder */
ret = avcodec_open2(enc_ctx, encoder, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open video encoder for stream #%u\n", i);
return ret;
}
ret = avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder parameters to output stream #%u\n", i);
return ret;
}
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER){
enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
out_stream->time_base = enc_ctx->time_base;
stream_ctx[i].enc_ctx = enc_ctx;
} else if (dec_ctx->codec_type == AVMEDIA_TYPE_UNKNOWN) {
av_log(NULL, AV_LOG_FATAL, "Elementary stream #%d is of unknown type, cannot proceed\n", i);
return AVERROR_INVALIDDATA;
} else {
/* if this stream must be remuxed */
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Copying parameters for stream #%u failed\n", i);
return ret;
}
out_stream->time_base = in_stream->time_base;
}
}
av_dump_format(ofmt_ctx, 0, filename, 1);
if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&ofmt_ctx->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", filename);
return ret;
}
}
/* init muxer, write output file header */
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error occurred when opening output file\n");
return ret;
}
return 0;
}
static int encode_write_frame(AVFrame *filt_frame, unsigned int stream_index, int *got_frame) {
int ret;
int got_frame_local;
AVPacket enc_pkt;
//add for single stream mode
if(ifmt_ctx->streams[stream_index]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && disable_audio){
return 0;
}else if(ifmt_ctx->streams[stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && disable_video){
return 0;
}
int (*enc_func)(AVCodecContext *, AVPacket *, const AVFrame *, int *) =
(ifmt_ctx->streams[stream_index]->codecpar->codec_type ==
AVMEDIA_TYPE_VIDEO) ? avcodec_encode_video2 : avcodec_encode_audio2;
if (!got_frame){
got_frame = &got_frame_local;
}
//av_log(NULL, AV_LOG_INFO, "Encoding frame\n");
/* encode filtered frame */
enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);
ret = enc_func(stream_ctx[stream_index].enc_ctx, &enc_pkt,
filt_frame, got_frame);
if (ret < 0){
return ret;
}
if (!(*got_frame)){
return 0;
}
/* prepare packet for muxing */
enc_pkt.stream_index = stream_index;
av_packet_rescale_ts(&enc_pkt,
stream_ctx[stream_index].enc_ctx->time_base,
ofmt_ctx->streams[stream_index]->time_base);
//av_log(NULL, AV_LOG_DEBUG, "Muxing frame\n");
/* mux encoded frame */
ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
return ret;
}
static int flush_encoder(unsigned int stream_index)
{
int ret;
int got_frame;
if (!(stream_ctx[stream_index].enc_ctx->codec->capabilities &
AV_CODEC_CAP_DELAY)){
return 0;
}
while (1) {
av_log(NULL, AV_LOG_INFO, "Flushing stream #%u encoder\n", stream_index);
ret = encode_write_frame(NULL, stream_index, &got_frame);
if (ret < 0){
break;
}
if (!got_frame){
return 0;
}
//av_usleep(50);
}
return ret;
}
int main(int argc, char **argv)
{
int ret;
AVPacket packet = { .data = NULL, .size = 0 };
AVFrame *frame = NULL;
enum AVMediaType type;
unsigned int stream_index;
unsigned int i;
int got_frame;
int (*dec_func)(AVCodecContext *, AVFrame *, int *, const AVPacket *);
//for network
avformat_network_init();
if (argc < 3) {
av_log(NULL, AV_LOG_ERROR, "Usage: %s <input file> <output file> [-noa,nov]\n", argv[0]);
return 1;
}
if(argc == 3){
disable_audio = 0;
disable_video = 0;
}else if(argc == 4 && !strcmp(argv[3], "-noa")){
disable_audio = 1;
disable_video = 0;
}else if(argc == 4 && !strcmp(argv[3], "-nov")){
disable_audio = 0;
disable_video = 1;
}
if ((ret = open_input_file(argv[1])) < 0){
goto end;
}
if ((ret = open_output_file(argv[2])) < 0){
goto end;
}
/* read all packets */
while (1) {
if ((ret = av_read_frame(ifmt_ctx, &packet)) < 0){
break;
}
stream_index = packet.stream_index;
type = ifmt_ctx->streams[packet.stream_index]->codecpar->codec_type;
//av_log(NULL, AV_LOG_DEBUG, "Demuxer gave frame of stream_index %u\n",stream_index);
//add for single stream mode
if(type == AVMEDIA_TYPE_AUDIO && disable_audio){
}else if(type == AVMEDIA_TYPE_VIDEO && disable_video){
}else{
frame = av_frame_alloc();
if (!frame) {
ret = AVERROR(ENOMEM);
break;
}
av_packet_rescale_ts(&packet,ifmt_ctx->streams[stream_index]->time_base,stream_ctx[stream_index].dec_ctx->time_base);
dec_func = (type == AVMEDIA_TYPE_VIDEO) ? avcodec_decode_video2 : avcodec_decode_audio4;
ret = dec_func(stream_ctx[stream_index].dec_ctx,frame,&got_frame, &packet);
if (ret < 0) {
av_frame_free(&frame);
av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
break;
}
if (got_frame) {
frame->pts = frame->best_effort_timestamp;
//just do for iframe
if(frame->pict_type == AV_PICTURE_TYPE_I || type == AVMEDIA_TYPE_AUDIO){
ret = encode_write_frame(frame, stream_index,NULL);
av_frame_free(&frame);
if (ret < 0){
goto end;
}
}
} else {
av_frame_free(&frame);
}
}
av_packet_unref(&packet);
}
/* flush encoders */
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
/* flush encoder */
ret = flush_encoder(i);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Flushing encoder failed\n");
goto end;
}
}
av_write_trailer(ofmt_ctx);
end:
av_packet_unref(&packet);
av_frame_free(&frame);
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
//add for single stream mode
if(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && disable_audio){
continue;
}else if(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && disable_video){
continue;
}
avcodec_free_context(&stream_ctx[i].dec_ctx);
if (ofmt_ctx && ofmt_ctx->nb_streams > i && ofmt_ctx->streams[i] && stream_ctx[i].enc_ctx){
avcodec_free_context(&stream_ctx[i].enc_ctx);
}
}
av_free(stream_ctx);
avformat_close_input(&ifmt_ctx);
if (ofmt_ctx && !(ofmt_ctx->oformat->flags & AVFMT_NOFILE)){
avio_closep(&ofmt_ctx->pb);
}
avformat_free_context(ofmt_ctx);
if (ret < 0){
av_log(NULL, AV_LOG_ERROR, "Error occurred: %s\n", av_err2str(ret));
}
return ret ? 1 : 0;
}
使用如下指令 完成了在两台Android设备上(我使用的是机顶盒,一台完成转码,另一台播放转码封装后的音视频)
codec /mnt/sda/sda1/demo.mpg rtp://192.168.30.102:6666 &
针对之前说提到的两个关键问题:
- aac编码size问题
此阶段转换为mp2音频先验证功能问题,其实解决思路也就是等输入的size满足编码器的size后再传入给编码器 - 转码后数据封装成网络实时传输流
这里就得说说ffmpeg的框架设计了,我的理解是一种组件热插拔模式,先看看以下几种muxer:
--enable-muxer=mp4
--enable-muxer=mpegts
拿以上两种muxer来说,是针对文件的合成,比如mpegts:
/**libavformat/mpegtsenc.c */
AVOutputFormat ff_mpegts_muxer = {
.name = "mpegts",
.long_name = NULL_IF_CONFIG_SMALL("MPEG-TS (MPEG-2 Transport Stream)"),
.mime_type = "video/MP2T",
.extensions = "ts,m2t,m2ts,mts",
.priv_data_size = sizeof(MpegTSWrite),
.audio_codec = AV_CODEC_ID_MP2,
.video_codec = AV_CODEC_ID_MPEG2VIDEO,
.init = mpegts_init,
.write_packet = mpegts_write_packet,
.write_trailer = mpegts_write_end,
.deinit = mpegts_deinit,
.check_bitstream = mpegts_check_bitstream,
.flags = AVFMT_ALLOW_FLUSH | AVFMT_VARIABLE_FPS | AVFMT_NODIMENSIONS,
.priv_class = &mpegts_muxer_class,
};
如果在设置编码器参数时如下配置:
avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", NULL);
编码时就会使用mpegtsenc来合成文件。
--enable-muxer=rtp
--enable-muxer=rtp_mpegts
而这两种是针对网络实时流的合成处理,比如rtp_mpegts:
/**libavformat/rtpenc_mpegts.c */
AVOutputFormat ff_rtp_mpegts_muxer = {
.name = "rtp_mpegts",
.long_name = NULL_IF_CONFIG_SMALL("RTP/mpegts output format"),
.priv_data_size = sizeof(struct MuxChain),
.audio_codec = AV_CODEC_ID_AAC,
.video_codec = AV_CODEC_ID_MPEG4,
.write_header = rtp_mpegts_write_header,
.write_packet = rtp_mpegts_write_packet,
.write_trailer = rtp_mpegts_write_close,
};
当设置编码器参数时按如下配置:
avformat_alloc_output_context2(&ofmt_ctx, NULL, "rtp_mpegts", NULL);
编码时就会使用rtpenc_mpegts来合成音视频并且封装成rtp的ts流实时发送。
小结:
以上就是使用X264来编码视频,mp2编码音频,并封装成ts包,然后通过rtp转发的处理。
在实际测试时发现X264编码很耗CPU,于是萌生了使用openh264来编码视频的想法:
于是取消了x264的编译,使用openh264的编译(openh264的编译参考openh264目录下的README):
For Android Builds
------------------
To build for android platform, You need to install android sdk and ndk. You also need to export `**ANDROID_SDK**/tools` to PATH. On Linux, this can be done by
export PATH=**ANDROID_SDK**/tools:$PATH
The codec and demo can be built by
make OS=android NDKROOT=**ANDROID_NDK** TARGET=**ANDROID_TARGET**
Valid `**ANDROID_TARGET**` can be found in `**ANDROID_SDK**/platforms`, such as `android-12`.
You can also set `ARCH`, `NDKLEVEL` according to your device and NDK version.
`ARCH` specifies the architecture of android device. Currently `arm`, `arm64`, `x86` and `x86_64` are supported, the default is `arm`. (`mips` and `mips64` can also be used, but there's no specific optimization for those architectures.)
`NDKLEVEL` specifies android api level, the default is 12. Available possibilities can be found in `**ANDROID_NDK**/platforms`, such as `android-21` (strip away the `android-` prefix).
By default these commands build for the `armeabi-v7a` ABI. To build for the other android
ABIs, add `ARCH=arm64`, `ARCH=x86`, `ARCH=x86_64`, `ARCH=mips` or `ARCH=mips64`.
To build for the older `armeabi` ABI (which has armv5te as baseline), add `APP_ABI=armeabi` (`ARCH=arm` is implicit).
To build for 64-bit ABI, such as `arm64`, explicitly set `NDKLEVEL` to 21 or higher.
编译时又发现在FFMPEG的configure中,需要使用pkg-config校验openh264:
enabled libopenh264 && require_pkg_config libopenh264 openh264 wels/codec_api.h WelsGetCodecVersion
编译FFMPEG Android版本时由于使用的是交叉编译工具链,在NDK的arm-linux-androideabi下是没有arm-linux-androideabi-pkg-config的,但是可以替换成编译环境下的pkg-config,否则会提示:
ERROR: openh264 not found using pkg-config
可以修改FFMPEG的编译脚本如下:
#!/bin/bash
#Detect ANDROID_NDK
export ANDROID_NDK=/home3/yangwu/build_tools/android-ndk-r14b
NDK_TOOLCHAIN_VERSION=4.9
ANDROID_PLATFROM_VERSION=android-19
if [ -z "$ANDROID_NDK" ]; then
echo "You must define ANDROID_NDK before starting."
echo "You must point to your NDK directories.\n"
exit 1
fi
#Detect OS
OS=`uname`
HOST_ARCH=`uname -m`
export CCACHE=; type ccache >/dev/null 2>&1 && export CCACHE=ccache
if [ $OS == 'Linux' ]; then
export HOST_SYSTEM=linux-$HOST_ARCH
elif [ $OS == 'Darwin' ]; then
export HOST_SYSTEM=darwin-$HOST_ARCH
fi
platform="$1"
version_type="$2"
function arm_toolchain()
{
export CROSS_PREFIX=arm-linux-androideabi-
$ANDROID_NDK/build/tools/make-standalone-toolchain.sh --toolchain=${CROSS_PREFIX}${NDK_TOOLCHAIN_VERSION} --platform=${ANDROID_PLATFROM_VERSION} --install-dir=$TOOLCHAIN --arch=arm --force
#--system=$HOST_SYSTEM #ndk4.9 do not support --system
}
function x86_toolchain()
{
export CROSS_PREFIX=i686-linux-android-
$ANDROID_NDK/build/tools/make-standalone-toolchain.sh --toolchain=x86-${NDK_TOOLCHAIN_VERSION} --platform=${ANDROID_PLATFROM_VERSION} --install-dir=$TOOLCHAIN --arch=x86 --force
#--system=$HOST_SYSTEM #ndk4.9 do not support --system
}
function mips_toolchain()
{
export CROSS_PREFIX=mipsel-linux-android-
$ANDROID_NDK/build/tools/make-standalone-toolchain.sh --toolchain=${CROSS_PREFIX}${NDK_TOOLCHAIN_VERSION} --platform=${ANDROID_PLATFROM_VERSION} --install-dir=$TOOLCHAIN --arch=mips --force
#--system=$HOST_SYSTEM #ndk4.9 do not support --system
}
SOURCE=`pwd`
DEST=$SOURCE/build/android
TOOLCHAIN=$SOURCE/ffmpeg_toolchain
SYSROOT=$TOOLCHAIN/sysroot/
function download {
mkdir -p "$SOURCE/downloads"
if [[ ! -e "$SOURCE/downloads/$2" ]]; then
echo "Downloading $1"
curl -L "$1" -o "$SOURCE/downloads/$2"
fi
}
if [ "$platform" = "x86" ];then
echo "Build Android x86 ffmpeg\n"
x86_toolchain
TARGET="x86"
TARGET_HOST="x86-linux-android"
PLATFORM="arch-x86"
elif [ "$platform" = "mips" ];then
echo "Build Android mips ffmpeg\n"
mips_toolchain
TARGET="mips"
TARGET_HOST="mipsel-linux-android"
PLATFORM="arch-mips"
elif [ "$platform" = "armv7" ];then
echo "Build Android armv7 ffmpeg\n"
arm_toolchain
TARGET="armv7"
TARGET_HOST="arm-linux-android"
PLATFORM="arch-arm"
else
echo "Build Android arm ffmpeg\n"
arm_toolchain
TARGET="neon armv7 vfp armv6"
TARGET_HOST="arm-linux-android"
PLATFORM="arch-arm"
fi
export PATH=$TOOLCHAIN/bin:$PATH
export CC="$CCACHE ${CROSS_PREFIX}gcc"
export CXX=${CROSS_PREFIX}g++
export LD=${CROSS_PREFIX}ld
export AR=${CROSS_PREFIX}ar
export STRIP=${CROSS_PREFIX}strip
#set ffmpeg dep libs here
echo "Decompressing archives..."
OPENH264_VERSION=1.7.0
FDKACC_VERSION=0.1.6
OPENSSL_VERSION=openssl-1.0.2j
download https://github.com/cisco/openh264/archive/v$OPENH264_VERSION.tar.gz openh264-$OPENH264_VERSION.tar.gz
#download ftp://ftp.videolan.org/pub/videolan/x264/snapshots/last_stable_x264.tar.bz2 last_stable_x264.tar.bz2
#download https://downloads.sourceforge.net/opencore-amr/fdk-aac-$FDKACC_VERSION.tar.gz
download https://www.openssl.org/source/$OPENSSL_VERSION.tar.gz $OPENSSL_VERSION.tar.gz
tar --totals -xzf $SOURCE/downloads/openh264-$OPENH264_VERSION.tar.gz -C $SOURCE/downloads/
#tar --totals -xjf $SOURCE/downloads/last_stable_x264.tar.bz2 -C $SOURCE/downloads/
#tar --totals -xzf $SOURCE/downloads/fdk-aac-$FDKACC_VERSION.tar.gz -C $SOURCE/downloads/
tar --totals -xzf $SOURCE/downloads/$OPENSSL_VERSION.tar.gz -C $SOURCE/downloads/
OPENH264=`echo $SOURCE/downloads/openh264-*`
X264=`echo $SOURCE/downloads/x264-snapshot-*`
FDKACC=`echo $SOURCE/downloads/fdk-aac-*`
OpenSSL=`echo $SOURCE/downloads/openssl-*`
CFLAGS="-std=c99 -O3 -Wall -pipe -fpic -fasm -finline-limit=300 -ffast-math -fstrict-aliasing -Wno-psabi -Wa,--noexecstack -fdiagnostics-color=always -DANDROID -DNDEBUG"
LDFLAGS="-lm -lz -Wl,--no-undefined -Wl,-z,noexecstack"
case $CROSS_PREFIX in
arm-*)
CFLAGS="-mthumb $CFLAGS -D__ARM_ARCH_5__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5TE__"
;;
x86-*)
;;
mipsel-*)
CFLAGS="-std=c99 -O3 -Wall -pipe -fpic -fasm -ftree-vectorize -ffunction-sections -funwind-tables -fomit-frame-pointer -funswitch-loops -finline-limit=300 -finline-functions -fpredictive-commoning -fgcse-after-reload -fipa-cp-clone -Wno-psabi -Wa,--noexecstack -DANDROID -DNDEBUG"
;;
esac
if [ "$version_type" = "online" ]; then
FFMPEG_FLAGS_COMMON="--target-os=android --cross-prefix=$CROSS_PREFIX --enable-cross-compile --enable-version3 --enable-shared --disable-static --disable-symver --disable-programs --disable-doc --disable-avdevice --disable-encoders --disable-muxers --disable-devices --disable-everything --disable-protocols --disable-demuxers --disable-decoders --disable-bsfs --disable-debug --enable-optimizations --enable-filters --enable-parsers --disable-parser=hevc --enable-swscale --enable-network --enable-protocol=file --enable-protocol=http --enable-protocol=rtmp --enable-protocol=rtp --enable-protocol=mmst --enable-protocol=mmsh --enable-protocol=crypto --enable-protocol=hls --enable-demuxer=hls --enable-demuxer=mpegts --enable-demuxer=mpegtsraw --enable-demuxer=mpegvideo --enable-demuxer=concat --enable-demuxer=mov --enable-demuxer=flv --enable-demuxer=rtsp --enable-demuxer=mp3 --enable-demuxer=matroska --enable-decoder=mpeg4 --enable-decoder=mpegvideo --enable-decoder=mpeg1video --enable-decoder=mpeg2video --enable-decoder=h264 --enable-decoder=h263 --enable-decoder=flv --enable-decoder=vp8 --enable-decoder=wmv3 --enable-decoder=aac --enable-decoder=ac3 --enable-decoder=mp3 --enable-decoder=nellymoser --enable-muxer=mp4 --enable-asm --enable-pic"
else
FFMPEG_FLAGS_COMMON="--target-os=android --cross-prefix=$CROSS_PREFIX --enable-cross-compile --enable-version3 --enable-shared --disable-static --disable-symver --disable-programs --disable-doc --disable-avdevice --disable-encoders --enable-gpl --enable-nonfree --enable-libopenh264 --enable-encoder=libopenh264 --enable-encoder=aac --enable-encoder=mp2 --disable-muxers --enable-muxer=mp4 --enable-muxer=mpegts --enable-muxer=rtp --enable-muxer=rtp_mpegts --disable-devices --disable-demuxer=sbg --disable-demuxer=dts --disable-parser=dca --disable-decoder=dca --disable-decoder=svq3 --enable-optimizations --disable-fast-unaligned --disable-postproc --enable-network --enable-asm --enable-openssl --disable-debug --enable-pthreads"
fi
for version in $TARGET; do
cd $SOURCE
FFMPEG_FLAGS="$FFMPEG_FLAGS_COMMON"
case $version in
neon)
FFMPEG_FLAGS="--arch=armv7-a --cpu=cortex-a8 --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv7-a -mfpu=neon -mfloat-abi=softfp -mvectorize-with-neon-quad"
EXTRA_LDFLAGS="-Wl,--fix-cortex-a8"
SSL_OBJS=""
;;
armv7)
FFMPEG_FLAGS="--arch=armv7-a --cpu=cortex-a8 --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=softfp"
EXTRA_LDFLAGS="-Wl,--fix-cortex-a8"
SSL_OBJS=""
;;
vfp)
FFMPEG_FLAGS="--arch=arm --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv6 -mfpu=vfp -mfloat-abi=softfp"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
armv6)
FFMPEG_FLAGS="--arch=arm --disable-runtime-cpudetect $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=armv6 -msoft-float"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
x86)
FFMPEG_FLAGS="--arch=x86 --cpu=i686 --enable-runtime-cpudetect --enable-yasm --disable-amd3dnow --disable-amd3dnowext $FFMPEG_FLAGS"
EXTRA_CFLAGS="-march=atom -msse3 -ffast-math -mfpmath=sse"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
mips)
FFMPEG_FLAGS="--arch=mips --cpu=mips32r2 --enable-runtime-cpudetect --enable-yasm --disable-mipsfpu --disable-mipsdspr1 --disable-mipsdspr2 $FFMPEG_FLAGS"
EXTRA_CFLAGS="-fno-strict-aliasing -fmessage-length=0 -fno-inline-functions-called-once -frerun-cse-after-loop -frename-registers"
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
*)
FFMPEG_FLAGS=""
EXTRA_CFLAGS=""
EXTRA_LDFLAGS=""
SSL_OBJS=""
;;
esac
PREFIX="$DEST/$version"
rm -rf $PREFIX && mkdir -p $PREFIX
FFMPEG_FLAGS="$FFMPEG_FLAGS --prefix=$PREFIX"
#build OpenSSL
cd $OpenSSL
./Configure --prefix=$PREFIX android-$TARGET $CFLAGS $EXTRA_CFLAGS no-shared
[ $PIPESTATUS == 0 ] || exit 1
make -j12 || exit 1
make install
# build OPENH264
cd $OPENH264
make -j12 PREFIX=$PREFIX OS=android NDKROOT="$ANDROID_NDK" TARGET=$ANDROID_PLATFROM_VERSION libraries install-static
# build X264
# cd $X264
# ./configure --prefix=$PREFIX --enable-static --enable-pic --disable-cli --cross-prefix=$CROSS_PREFIX --sysroot=$SYSROOT --host=$TARGET_HOST --extra-cflags="$CFLAGS $EXTRA_CFLAGS" --extra-ldflags="$LDFLAGS $EXTRA_LDFLAGS"
# [ $PIPESTATUS == 0 ] || exit 1
# make -j12 || exit 1
# make install
# build FDKACC
# cd $FDKACC
# ./configure --prefix=$PREFIX --with-sysroot=$ANDROID_NDK/platforms/$ANDROID_PLATFROM_VERSION/$PLATFORM --host=$TARGET_HOST
# [ $PIPESTATUS == 0 ] || exit 1
# make -j12 || exit 1
# make install
# build ffmpeg
export PKG_CONFIG_PATH=$DEST/$TARGET/lib/pkgconfig:$PKG_CONFIG_PATH
cd $SOURCE
./configure $FFMPEG_FLAGS --extra-cflags="$CFLAGS $EXTRA_CFLAGS -I$DEST/$TARGET/include" --extra-ldflags="$LDFLAGS $EXTRA_LDFLAGS -L$DEST/$TARGET/lib" --pkg-config="pkg-config" | tee $PREFIX/configuration.txt
cp config.* $PREFIX
[ $PIPESTATUS == 0 ] || exit 1
make clean
find . -path $TOOLCHAIN -prune -name "*.o" -type f -delete
make -j12 || exit 1
make examples
make install
echo "----------------------$version -----------------------------"
done
把configure的pkg_config_default指向编译本机中的pkg_config:
#pkg_config_default="${cross_prefix}${pkg_config_default}"
pkg_config_default="/usr/bin/pkg_config"
x264转码效率:
openh264转码效率:
实测openh264的cpu占用确实由x264的90%降到了25%,可画质效果确实也是差了很多,实际情况还需要根据自身业务做调整。
x264的在使用多线程调优的情况下也可以从90%优化降到60%左右,但是效果还是不理想,其实就是修改多线程编解码DECODE_THREADS和ENCODE_THREADS的线程数,修改多线程工作方式为FF_THREAD_SLICE或FF_THREAD_FRAME。
3.结束语
本篇文章旨在帮助同学们自己完成FFMPEG源码移植的编译,并且在次基础上通过音视频转码来让大家熟悉FFMPEG API的使用,FFMPEG是个很优秀的框架,学习的路还很长,我会把自己的理解和成果尽可能的记录下来,便于大家一起学习。
文章更新的进度越来越慢了,因为有些还没有写完,好几篇文章在日记本中躺着。感谢各位读者的支持!