GVKun编程网logo

ffmpeg 打包TS介绍(ffmpeg下载ts文件)

27

在本文中,我们将为您详细介绍ffmpeg打包TS介绍的相关知识,并且为您解答关于ffmpeg下载ts文件的疑问,此外,我们还会提供一些关于Android中集成ffmpeg(一):编译ffmpeg、An

在本文中,我们将为您详细介绍ffmpeg 打包TS介绍的相关知识,并且为您解答关于ffmpeg下载ts文件的疑问,此外,我们还会提供一些关于Android 中集成 ffmpeg (一):编译 ffmpeg、Android使用FFmpeg(一)--编译ffmpeg、Android使用FFmpeg(三)--ffmpeg实现视频播放、Android使用FFmpeg(六)--ffmpeg实现音视频同步播放的有用信息。

本文目录一览:

ffmpeg 打包TS介绍(ffmpeg下载ts文件)

ffmpeg 打包TS介绍(ffmpeg下载ts文件)

FFmpeg代码里面有ts打包和解包的代码,这里简单介绍下怎么使用吧。


先来看下FFmpeg目录结构:
libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构
和读取音视频帧等功能;
libavcodec:用于各种类型声音/图像编解码;
libavutil:包含一些公共的工具函数;
libswscale:用于视频场景比例缩放、色彩映射转换;
libpostproc:用于后期效果处理;
ffmpeg:该项目提供的一个工具,可用于格式转换、解码或电视卡即时编码等;
ffsever:一个 HTTP 多媒体即时广播串流服务器;
ffplay:是一个简单的播放器,使用ffmpeg 库解析和解码,通过SDL显示;


libavformat目录下 mpegtsenc.c,mpegts.c 分别是ts打包和解包的代码:
下面介绍下mpegtsenc.c一些重要函数(原理请看 iso 13818-1):

1)mpegts_write_pat(AVFormatContext *s);
      mpegts_write_pmt(AVFormatContext *s, MpegTSService *service)
      mpegts_write_sdt(AVFormatContext *s)//节目描述表

     pat,pmt这两个表是ts打包最重要的表,这两个表说白了就是多路复用的一个索引,解码器需要更具PAT知道有哪些节目(可以理解为电视节目),根据PMT知道每个节目里面有哪些es流(每个电视节目都有音频和视频),这两个函数一般是不需要改动的;

    pat,pmt的信息并不是只是开始打包的时候出现,看mpegts_write_pes代码会发现着两个表是根据retransmit_si_info计算出来的。

    看下mpegts_write_pmt部分代码:

//nb_streams 对应es流,如果一路音频一路视频,nb_streams=2
    for(i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];
        MpegTSWriteStream *ts_st = st->priv_data;
        switch(st->codec->codec_id) {
        case CODEC_ID_MPEG1VIDEO:
        case CODEC_ID_MPEG2VIDEO:
            stream_type = STREAM_TYPE_VIDEO_MPEG2;
            break;
        case CODEC_ID_MPEG4:
            stream_type = STREAM_TYPE_VIDEO_MPEG4;
            break;
        case CODEC_ID_H264:
            stream_type = STREAM_TYPE_VIDEO_H264;
            break;
        case CODEC_ID_MP2:
        case CODEC_ID_MP3:
            stream_type = STREAM_TYPE_AUDIO_MPEG1;
            break;
        case CODEC_ID_AAC:
            stream_type = STREAM_TYPE_AUDIO_AAC;
            break;
        case CODEC_ID_AC3:
            stream_type = STREAM_TYPE_AUDIO_AC3;
            break;
        default:
            stream_type = STREAM_TYPE_PRIVATE_DATA;
            break;
        }
        *q++ = stream_type;
        put16(&q, 0xe000 | ts_st->pid);
复制代码

从上面看出ts打包支持MPEG1,MPEG2,MPEG4,h264视频以及PCM,mp2,mp3,AAC,AC3音频,音频方面标准不支持G711等G开头的音频,当然如果自己开发客户端的话是可以自定义的。


2)mpegts_write_header(AVFormatContext *s)

初始化AVFormatContext参数,在正式封装开始加入PAT,PMT,SDT一些信息。代码中有基本的注释;

3)mpegts_write_pes(AVFormatContext *s, AVStream *st,
                             const uint8_t *payload, int payload_size,
                             int64_t pts, int64_t dts)
这个函数就是TS打包的主函数了,这个函数主要功能就是把一帧数据拆分成188字节(感觉效率低了点),并加入PTS,DTS同步信息,这个函数封装的对象是一帧视频或者音频数据,payload,payload_size分别是数据和大小。
上面提到的PAT,PMT表在每个188字节都会检查一次,

    //一帧数据拆成188字节
    while (payload_size > 0) {

        retransmit_si_info(s);
复制代码

retransmit_si_info 函数如下,可以看出条件(++ts->pat_packet_count == ts->pat_packet_freq)成立,就会加入PAT,PMT信息,而ts->pat_packet_freq这个值是根据码流大小计算出来。

    MpegTSWrite *ts = s->priv_data;
    int i;
//printf("sdt f:%d pat:%d nb:%d\n",ts->sdt_packet_freq,ts->pat_packet_freq,ts->nb_services);
    if (++ts->sdt_packet_count == ts->sdt_packet_freq) {
        ts->sdt_packet_count = 0;
        mpegts_write_sdt(s);
    }
    if (++ts->pat_packet_count == ts->pat_packet_freq) {
        ts->pat_packet_count = 0;
        mpegts_write_pat(s);
        for(i = 0; i < ts->nb_services; i++) {
            mpegts_write_pmt(s, ts->services[i]);
        }
    }
复制代码

PTS,DTS就是音视频同步时间戳,时间戳其实就是一次采样的颗粒(简单理解就是数据),以视频来举例,视频同步时钟90K hz(27M/300),如果帧率是25fps的话,一帧数据采样时间40ms,那么时间戳就是90K x 40ms = 3600(估算值)。

4)mpegts_write_packet(AVFormatContext *s,  AVPacket *pkt)

这个函数功能比较简单,就是把一帧数据拆分成几个块来封装成pes,因为pes头信息的长度只有两个字节长度(当时可能面向标清),高清的I帧数据肯定一次包不完的。

Android 中集成 ffmpeg (一):编译 ffmpeg

Android 中集成 ffmpeg (一):编译 ffmpeg

  • 方案选择

Android 中集成 ffmpeg 的 codec 功能无非两种方式:

    1. JNI 直接调用,主要用于 App 开发(无权限修改系统底层),如 EXOPlayer,JPlayer 等。
    2. 集成 ffmpeg 到 OMX,即封装 ffmpeg 为 OMX 的 plugin,然后实现 component 接口。

考虑到性能问题,我选取的方案二即集成 ffmpeg 到 OMX。首先第一个问题就是 ffmpeg 的编译。也有两种方式:

    1. 集成 ffmpeg 源码到整个 Android 系统工程,独立写 bp 或 mk 进行编译。(由于 ffmpeg 配置复杂,参考网上的一些现成 mk 进行修改发现工作量比较巨大)
    2. 独立 NDK 编译 ffmpeg 成动态库或静态库,然后通过 prebuild 的方式集成。(由于 OMX 的 plugin 是直接动态加载动态库,无编译上的依赖,此种方法编译跟普通的 ffmpeg 编译类似,故推荐该方法)

 

  • FFmpeg 配置

32bit 编译配置:

 1 #!/bin/bash
 2 
 3 chmod 777 ./configure
 4 #chmod 777 ./version.sh
 5 NDK=/your_ndk_path/android-ndk-r16b
 6 SYSROOT=$NDK/platforms/android-27/arch-arm
 7 TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
 8 CROSS=$TOOLCHAIN/bin/arm-linux-androideabi-
 9 
10 ISYSROOT=$NDK/sysroot
11 ASM=$ISYSROOT/usr/include/arm-linux-androideabi
12 CC=$TOOLCHAIN/bin/arm-linux-androideabi-gcc
13 NM=$TOOLCHAIN/bin/arm-linux-androideabi-nm
14 
15 CPU=arm
16 PREFIX=./android/$CPU
17 ADDI_CFLAGS="-marm"
18 
19 function build_one
20 {
21 ./configure \
22 --prefix=$PREFIX \
23 --target-os=linux \
24 --arch=$CPU \
25 --enable-cross-compile \
26 --incdir=$NDK/sysroot/usr/include \
27 --sysroot=$SYSROOT \
28 --cross-prefix=$CROSS \
29 --cc=$CC \
30 --nm=$NM \
31 --extra-cflags="-I$ASM -isysroot $ISYSROOT -Os -fpic $ADDI_CFLAGS" \
32 --enable-shared \
33 --enable-static \
34 --disable-asm \
35 --enable-pic \
36 --disable-doc \
37 --disable-ffmpeg \
38 --disable-ffplay \
39 --disable-ffprobe \
40 --disable-ffserver \
41 --disable-avdevice \
42 --disable-doc \
43 --disable-symver \
44 --disable-debug \
45 --disable-encoders \
46 --disable-muxers \
47 --enable-avresample \
48 --disable-pthreads \
49 --enable-swresample
50 make
51 make install
52 }
53 
54 rm -rf $PREFIX
55 make distclean
56 build_one
View Code

 

说明:

 1. --disable-asm 选项是因为在 ffmpeg-2.0 的版本编译出版本运行时出现错误:libavcodec.so: has text relocation。 但是在用 ffmpeg-3.3 版本时没有出现。也有文章说跟 NDK 版本也有关系。

可以通过命令检查:readelf -a aarch64/lib/libavcodec.so |grep TEXTREL

 2. 在高版本的 ffmpeg(如 3.3.8)中,可以直接指定 --target-os=android ,否则就需要修改 configure 文件让输出的动态库不带版本号,修改 SLIBNAME_WITH_VERSION 等选项为:

SLIBNAME=''$(SLIBPREF)$(FULLNAME)$(SLIBSUF)''
SLIBNAME_WITH_VERSION=''$(SLIBNAME).$(LIBVERSION)''
SLIBNAME_WITH_MAJOR=''$(SLIBNAME).$(LIBMAJOR)''
LIB_INSTALL_EXTRA_CMD=''$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"''
SLIB_INSTALL_NAME=''$(SLIBNAME_WITH_VERSION)''
SLIB_INSTALL_LINKS=''$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)''

   3. ffmpeg 增加动态库导出函数,只需要修改源码所在目录的.v 文件,如在 libavformat/libavformat.v 中增加导出 ffurl_register_protocol 方法供外部代码链接。

 

64bit 编译配置:

 1 #!/bin/bash
 2 
 3 chmod 777 ./configure
 4 #chmod 777 ./version.sh
 5 NDK=/disk2/wuxingde/android-ndk-r16b
 6 SYSROOT=$NDK/platforms/android-27/arch-arm64
 7 TOOLCHAIN=$NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64
 8 
 9 ISYSROOT=$NDK/sysroot
10 ASM=$ISYSROOT/usr/include/aarch64-linux-android
11 CC=$TOOLCHAIN/bin/aarch64-linux-android-gcc
12 NM=$TOOLCHAIN/bin/aarch64-linux-android-nm
13 
14 CPU=aarch64
15 PREFIX=$(pwd)/android/$CPU
16 #ADDI_CFLAGS="-marm"
17 
18 function build_one
19 {
20 ./configure \
21 --prefix=$PREFIX \
22 --target-os=linux \
23 --arch=$CPU \
24 --enable-cross-compile \
25 --incdir=$NDK/sysroot/usr/include \
26 --sysroot=$SYSROOT \
27 --cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
28 --cc=$CC \
29 --nm=$NM \
30 --extra-cflags="-I$ASM -isysroot $ISYSROOT -Os -fpic" \
31 --enable-shared \
32 --enable-static \
33 --enable-asm \
34 --enable-pic \
35 --disable-doc \
36 --disable-ffmpeg \
37 --disable-ffplay \
38 --disable-ffprobe \
39 --disable-ffserver \
40 --disable-avdevice \
41 --disable-doc \
42 --disable-symver \
43 --disable-debug \
44 --disable-encoders \
45 --disable-muxers \
46 --enable-avresample \
47 --disable-pthreads \
48 --enable-swresample
49 make
50 make install
51 }
52 
53 rm -rf $PREFIX
54 make distclean
55 build_one
View Code

 

  • PREBUID

如果只用 64bit 版本可以直接使用下面 Android.mk 一次性编译:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PREBUILT_LIBS := \
        lib/libavcodec.so \
        lib/libavformat.so \
        lib/libavutil.so \
        lib/libavfilter.so \
        lib/libswresample.so \
        lib/libavresample.so \
        lib/libswscale.so

include $(BUILD_MULTI_PREBUILT)
View Code

如果同时需要 32bit 和 64bit 版本需要使用下面 Android.mk 分别编译各个库:

 1 LOCAL_PATH := $(call my-dir)
 2 include $(CLEAR_VARS)
 3 
 4 LOCAL_SRC_FILES_32 := \
 5         arm/lib/libavcodec.so
 6 LOCAL_SRC_FILES_64 := \
 7         aarch64/lib/libavcodec.so
 8 LOCAL_MODULE := libavcodec
 9 LOCAL_MODULE_SUFFIX := .so
10 LOCAL_MODULE_CLASS := SHARED_LIBRARIES
11 #LOCAL_MODULE_TARGET_ARCH := arm
12 LOCAL_MULTILIB := both
13 include $(BUILD_PREBUILT)
14 
15 include $(CLEAR_VARS)
16 LOCAL_SRC_FILES_32 := \
17         arm/lib/libavformat.so
18 LOCAL_SRC_FILES_64 := \
19         aarch64/lib/libavformat.so
20 LOCAL_MODULE := libavformat
21 LOCAL_MODULE_SUFFIX := .so
22 LOCAL_MODULE_CLASS := SHARED_LIBRARIES
23 #LOCAL_MODULE_TARGET_ARCH := arm
24 LOCAL_MULTILIB := both
25 include $(BUILD_PREBUILT)
26 
27 include $(CLEAR_VARS)
28 LOCAL_SRC_FILES_32 := \
29         arm/lib/libavutil.so
30 LOCAL_SRC_FILES_64 := \
31         aarch64/lib/libavutil.so
32 LOCAL_MODULE := libavutil
33 LOCAL_MODULE_SUFFIX := .so
34 LOCAL_MODULE_CLASS := SHARED_LIBRARIES
35 #LOCAL_MODULE_TARGET_ARCH := arm
36 LOCAL_MULTILIB := both
37 include $(BUILD_PREBUILT)
38 
39 include $(CLEAR_VARS)
40 LOCAL_SRC_FILES_32 := \
41         arm/lib/libavresample.so
42 LOCAL_SRC_FILES_64 := \
43         aarch64/lib/libavresample.so
44 LOCAL_MODULE := libavresample
45 LOCAL_MODULE_SUFFIX := .so
46 LOCAL_MODULE_CLASS := SHARED_LIBRARIES
47 #LOCAL_MODULE_TARGET_ARCH := arm
48 LOCAL_MULTILIB := both
49 include $(BUILD_PREBUILT)
50 
51 include $(CLEAR_VARS)
52 LOCAL_SRC_FILES_32 := \
53         arm/lib/libavfilter.so
54 LOCAL_SRC_FILES_64 := \
55         aarch64/lib/libavfilter.so
56 LOCAL_MODULE := libavfilter
57 LOCAL_MODULE_SUFFIX := .so
58 LOCAL_MODULE_CLASS := SHARED_LIBRARIES
59 #LOCAL_MODULE_TARGET_ARCH := arm
60 LOCAL_MULTILIB := both
61 include $(BUILD_PREBUILT)
62 
63 include $(CLEAR_VARS)
64 LOCAL_SRC_FILES_32 := \
65         arm/lib/libswresample.so
66 LOCAL_SRC_FILES_64 := \
67         aarch64/lib/libswresample.so
68 LOCAL_MODULE := libswresample
69 LOCAL_MODULE_SUFFIX := .so
70 LOCAL_MODULE_CLASS := SHARED_LIBRARIES
71 #LOCAL_MODULE_TARGET_ARCH := arm
72 LOCAL_MULTILIB := both
73 include $(BUILD_PREBUILT)
74 
75 include $(CLEAR_VARS)
76 LOCAL_SRC_FILES_32 := \
77         arm/lib/libswscale.so
78 LOCAL_SRC_FILES_64 := \
79         aarch64/lib/libswscale.so
80 LOCAL_MODULE := libswscale
81 LOCAL_MODULE_SUFFIX := .so
82 LOCAL_MODULE_CLASS := SHARED_LIBRARIES
83 #LOCAL_MODULE_TARGET_ARCH := arm
84 LOCAL_MULTILIB := both
85 include $(BUILD_PREBUILT)
View Code

 

编译部分就到此结束,这部分由于 Android NDK 版本(我用的是 android-ndk-r16b)和 ffmpeg 版本不同,网上很多教程都没法用,花费了很多时间。特别要注意 ffmpeg 中 --extra-cflags = 配置,不要随意加额外一些定义,可能会导致链接不上的问题。

 

Android使用FFmpeg(一)--编译ffmpeg

Android使用FFmpeg(一)--编译ffmpeg

关于

Android使用FFmpeg(一)--编译ffmpeg
Android使用FFmpeg(二)--Android Studio配置ffmpeg
Android使用FFmpeg(三)--ffmpeg实现视频播放
Android使用FFmpeg(四)--ffmpeg实现音频播放(使用AudioTrack进行播放)
Android使用FFmpeg(五)--ffmpeg实现音频播放(使用openSL ES进行播放)
Android使用FFmpeg(六)--ffmpeg实现音视频同步播放
Android使用FFmpeg(七)--ffmpeg实现暂停、快退快进播放

前言

ffmpeg简介
在现今这个阶段,越来越多的app会涉及到音视频,那么学会使用ffmpeg就很有必要了。在这个系类中将讲解如何把ffmpeg编译成动态库,以及使用ffmpeg实现音视频播放和音视频的采集。

编译环境

Linux/Ubuntu/centos都行
在windows环境下可以安装虚拟机或者购买一个云主机,作者在金山云购买的一个Ubuntu主机。

准备工作

下载配置ndk,下载ffmpeg并解压。

开始

配置ndk

如果你已经配置ok,请跳过这一步。
我们打开自己的虚拟机或者云服务器并且获取到root权限。创建文件夹ndk_build并进入到文件夹中,下载ndk,并解压

mkdir ndk_build//新建文件夹
cd ndk_build//进入到文件夹
wget +linux版本的下载链接地址//下载ndk
unzip + 压缩文件//解压

配置ndk

vim ~/.bashrc//进入环境变量配置
export NDKROOT=/home/ndk_build/android-ndk-r14b//配置你的安装路径
export PATH=$NDKROOT:$PATH//配置路径
:wq!//保存退出
source ~/.bashrc//执行环境变量
ndk-build//查看是否安装成功,只要不是显示ndk-build not found,则表示安装成功

下载编译ffmpeg

下载ffmpeg并解压

 

 

下载ffmpeg并解压.png

wget http://ffmpeg.org/releases/ffmpeg-2.6.9.tar.gz
tar -xzf  ffmpeg-2.6.9.tar.gz

修改configure文件并新建android_build.sh文件,编辑android_build.sh

#!/bin/bash
make clean
export NDK=/home/ndk_build/android-ndk-r14b
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
export CPU=arm
export PREFIX=$(pwd)/android/$CPU
export ADDI_CFLAGS="-marm"
./configure --target-os=linux \
--prefix=$PREFIX --arch=arm \
--disable-doc \
--enable-shared \
--disable-static \
--disable-yasm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install

其中NDK是你自己刚刚配置的ndk路径,SYSROOT为platforms中的路径,TOOLCHAIN为toolchains里面中的路径,依次配置好过后保存并退出。
修改configure,如果你不修改的话,编译出来过后的.so文件后面会有一串数字,无法使用,所以得修改他的命名规则。
将该文件中的如下四行:

SLIBNAME_WITH_MAJOR=''$(SLIBNAME).$(LIBMAJOR)''
LIB_INSTALL_EXTRA_CMD=''$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"''
SLIB_INSTALL_NAME=''$(SLIBNAME_WITH_VERSION)''
SLIB_INSTALL_LINKS=''$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)''

替换为:

SLIBNAME_WITH_MAJOR=''$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)''
LIB_INSTALL_EXTRA_CMD=''$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"''
SLIB_INSTALL_NAME=''$(SLIBNAME_WITH_MAJOR)''
SLIB_INSTALL_LINKS=''$(SLIBNAME)''

编译.sh文件
···
./android_build.sh
···
当编译成功过后会在文件夹中出现android文件夹,


在lib文件夹中就有你需要的.so文件了


这样就差不多编译完成,如果你需要编译一份别人已经编译过的ffmpeg,那么请先执行./confifure --disable-yasm,然后再自己编译。
如果还没编译好但是急需的朋友可以直接下载使用。

so下载地址

Android使用FFmpeg(三)--ffmpeg实现视频播放

Android使用FFmpeg(三)--ffmpeg实现视频播放

前言

如果你已经准备好ffmpeg的开发环境,那么我们在这篇文章中实现对视频的一个播放,如果还没有准备好,请看前面的内容。

正文

Ok,上图就是使用ffmpeg实现了一个视频的播放的大概流程图,那么,我们将根据流程图来编写代码,这样子,代码的编写就会显得比较简单,比较好理解了。
1.注册各大组件,这一步很重要,如果不注册就无法使用后面的函数了。

av_register_all();

2.在解码之前我们得获取里面的内容吧,所以这一步就是打开地址并且获取里面的内容。其中avFormatContext是内容的一个上下文,inputPath为输入的地址。

AVFormatContext *avFormatContext = avformat_alloc_context();//获取上下文
avformat_open_input(&avFormatContext, inputPath, NULL, NULL)//解封装
avformat_find_stream_info(avFormatContext, NULL)

3.我们在上面已经获取了内容,但是在一个音视频中包括了音频流,视频流和字幕流,所以在所有的内容当中,我们应当找出相对应的视频流。

int video_index=-1;
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
        if (avFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            //如果是视频流,标记一哈
            video_index = i;
        }
    }

4.在第三步的时候已经找到了视频流,那么我们就对视频流进行解码、转换和绘制。
a.如果要进行解码,那么得有解码的装置并打开解码的装置。

   //获取解码的装置上下文
    AVCodecContext *avCodecContext = avFormatContext->streams[video_index]->codec;
    //获取解码的装置
    AVCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id);
    //打开解码的装置
    if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
        LOGE("打开失败")
        return;
    }

b.申请AVPacket和AVFrame,其中AVPacket的作用是:保存解码之前的数据和一些附加信息,如显示时间戳(pts)、解码时间戳(dts)、数据时长,所在媒体流的索引等;AVFrame的作用是:存放解码过后的数据。
具体可参考:http://blog.csdn.net/leixiaohua1020/article/details/11693997

 //申请AVPacket
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
    av_init_packet(packet);
    //申请AVFrame
    AVFrame *frame = av_frame_alloc();//分配一个AVFrame结构体,AVFrame结构体一般用于存储原始数据,指向解码后的原始帧
    AVFrame *rgb_frame = av_frame_alloc();//分配一个AVFrame结构体,指向存放转换成rgb后的帧

c.因为rgb_frame是一个缓存区域,所以需要设置。

 //缓存区
    uint8_t  *out_buffer= (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_RGBA,
                                                                         avCodecContext->width,avCodecContext->height));
    //与缓存区相关联,设置rgb_frame缓存区
    avpicture_fill((AVPicture *)rgb_frame,out_buffer,AV_PIX_FMT_RGBA,avCodecContext->width,avCodecContext->height);


d.因为是原生绘制,即是说需要ANativeWindow。

 //取到nativewindow
    ANativeWindow *nativeWindow=ANativeWindow_fromSurface(env,surface);
    if(nativeWindow==0){
        LOGE("nativewindow取到失败")
        return;
    }
    //视频缓冲区
    ANativeWindow_Buffer native_outBuffer;

e.一切准备妥当,那么我们开始解码。

while (av_read_frame(avFormatContext, packet) >= 0) {
        LOGE("解码 %d",packet->stream_index)
        LOGE("VINDEX %d",video_index)
        if(packet->stream_index==video_index){
            LOGE("解码 hhhhh")
            //如果是视频流
            //解码
            avcodec_decode_video2(avCodecContext, frame, &frameCount, packet)
        }
        av_free_packet(packet);
    }

f.以下均在循环里面进行,当解码一帧成功过后,我们转换成rgb格式并且绘制。

if (frameCount) {
                LOGE("转换并绘制")
                //说明有内容
                //绘制之前配置nativewindow
                ANativeWindow_setBuffersGeometry(nativeWindow,avCodecContext->width,avCodecContext->height,WINDOW_FORMAT_RGBA_8888);
                //上锁
                ANativeWindow_lock(nativeWindow, &native_outBuffer, NULL);
                //转换为rgb格式
                sws_scale(swsContext,(const uint8_t *const *)frame->data,frame->linesize,0,
                          frame->height,rgb_frame->data,
                          rgb_frame->linesize);
              //  rgb_frame是有画面数据
                uint8_t *dst= (uint8_t *) native_outBuffer.bits;
//            拿到一行有多少个字节 RGBA
                int destStride=native_outBuffer.stride*4;
            //像素数据的首地址
                uint8_t * src=  rgb_frame->data[0];
//            实际内存一行数量
                int srcStride = rgb_frame->linesize[0];
                //int i=0;
                for (int i = 0; i < avCodecContext->height; ++i) {
//                memcpy(void *dest, const void *src, size_t n)
                    //将rgb_frame中每一行的数据复制给nativewindow
                    memcpy(dst + i * destStride,  src + i * srcStride, srcStride);
                }
//解锁
                ANativeWindow_unlockAndPost(nativeWindow);
                usleep(1000 * 16);

            }

在上面的代码中,因为转换成rgb格式过后的内容是存在ffmpeg所指向的地址而不是ANativeWindow所指向的所在地址,所以要绘制的话我们需要将内容复制到ANativeWindow中。
5.完成过后得释放资源,不然就造成内存泄露了。

  ANativeWindow_release(nativeWindow);
    av_frame_free(&frame);
    av_frame_free(&rgb_frame);
    avcodec_close(avCodecContext);
    avformat_free_context(avFormatContext);
    env->ReleaseStringUTFChars(inputStr_, inputPath);

6.java层代码,因为是原生绘制,所以需要传入Surface,所以创建一个类继承SurfaceView。

class VideoView : SurfaceView {
    constructor(context: Context) : super(context) {}

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        init()

    }

    private fun init() {
        val holder = holder
        holder.setFormat(PixelFormat.RGBA_8888)
    }

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

    fun player(input: String) {
        Thread(Runnable {
            //        绘制功能 不需要交给SurfaveView        VideoView.this.getHolder().getSurface()
            render(input, this@VideoView.holder.surface)
        }).start()
    }

    external fun render(input: String, surface: Surface)

    companion object {
        init {
            System.loadLibrary("avcodec-56")
            System.loadLibrary("avdevice-56")
            System.loadLibrary("avfilter-5")
            System.loadLibrary("avformat-56")
            System.loadLibrary("avutil-54")
            System.loadLibrary("postproc-53")
            System.loadLibrary("swresample-1")
            System.loadLibrary("swscale-3")
            System.loadLibrary("native-lib")
        }
    }
}

小结

以上就是对视频的解封装,解码,转换,绘制的一个过程,过程清晰明了,按着这个步奏来就应该来说比较简单,另外,请在真机上测试,同时导入自己想测试的视频,什么格式都可以。

 

Android使用FFmpeg(六)--ffmpeg实现音视频同步播放

Android使用FFmpeg(六)--ffmpeg实现音视频同步播放

关于

Android使用FFmpeg(一)--编译ffmpeg
Android使用FFmpeg(二)--Android Studio配置ffmpeg
Android使用FFmpeg(三)--ffmpeg实现视频播放
Android使用FFmpeg(四)--ffmpeg实现音频播放(使用AudioTrack进行播放)
Android使用FFmpeg(五)--ffmpeg实现音频播放(使用openSL ES进行播放)
Android使用FFmpeg(六)--ffmpeg实现音视频同步播放
Android使用FFmpeg(七)--ffmpeg实现暂停、快退快进播放

准备工作

Android使用FFmpeg(三)--ffmpeg实现视频播放
Android使用FFmpeg(五)--ffmpeg实现音频播放(使用openSL ES进行播放)
同步原理介绍

正文

依旧依照流程图来逐步实现同步播放:

从流程图可以看出,实现同步播放需要三个线程,一个开启解码的装置得到packet线程,然后分别是播放音频和视频的线程。这篇简书是以音频播放为基准来进行播放,也就是音频一直不停的播放,视频根据音频播放来调整延迟时间。
1.开启play线程,在这个线程中,注册组件,得到音视频的解码的装置并将packet压入队列。这里和前面的音视频分开播放并没有多大差别,也就多了一个队列。

void *begin(void *args) {
    LOGE("开启解码线程")
    //1.注册组件
    av_register_all();
    avformat_network_init();
    //封装格式上下文
    AVFormatContext *pFormatCtx = avformat_alloc_context();

    //2.打开输入视频文件
    if (avformat_open_input(&pFormatCtx, inputPath, NULL, NULL) != 0) {
        LOGE("%s", "打开输入视频文件失败");
    }
    //3.获取视频信息
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGE("%s", "获取视频信息失败");
    }
    //找到视频流和音频流
    int i=0;
    for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
        //获取解码的装置
        AVCodecContext *avCodecContext = pFormatCtx->streams[i]->codec;
        AVCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id);

        //copy一个解码的装置,
        AVCodecContext *codecContext = avcodec_alloc_context3(avCodec);
        avcodec_copy_context(codecContext,avCodecContext);
        if(avcodec_open2(codecContext,avCodec,NULL)<0){
            LOGE("打开失败")
            continue;
        }
        //如果是视频流
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
            ffmpegVideo->index=i;
            ffmpegVideo->setAvCodecContext(codecContext);
            ffmpegVideo->time_base=pFormatCtx->streams[i]->time_base;
            if (window) {
                ANativeWindow_setBuffersGeometry(window, ffmpegVideo->codec->width, ffmpegVideo->codec->height,
                                                 WINDOW_FORMAT_RGBA_8888);
            }
        }//如果是音频流
        else if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO){
            ffmpegMusic->index=i;
            ffmpegMusic->setAvCodecContext(codecContext);
            ffmpegMusic->time_base=pFormatCtx->streams[i]->time_base;
        }
    }
//开启播放
    ffmpegVideo->setFFmepegMusic(ffmpegMusic);
    ffmpegMusic->play();
    ffmpegVideo->play();
    isPlay=1;
    //读取packet,并压入队列中
    AVPacket *packet = (AVPacket *)av_mallocz(sizeof(AVPacket));
    int ret;
    while (isPlay){
        ret = av_read_frame(pFormatCtx,packet);
        if(ret ==0){
            if(ffmpegVideo&&ffmpegVideo->isPlay&&packet->stream_index==ffmpegVideo->index){
                //将视频packet压入队列
               ffmpegVideo->put(packet);
            } else if(ffmpegMusic&&ffmpegMusic->isPlay&&packet->stream_index==ffmpegMusic->index){
                ffmpegMusic->put(packet);
            }
            av_packet_unref(packet);
        } else if(ret ==AVERROR_EOF){
 while (isPlay) {
                if (vedio->queue.empty() && audio->queue.empty()) {
                    break;
                }
//                LOGI("等待播放完成");
                av_usleep(10000);
            }
        }
    }

    //读取完过后可能还没有播放完
    isPlay=0;
    if(ffmpegMusic && ffmpegMusic->isPlay){
        ffmpegMusic->stop();
    }
    if(ffmpegVideo && ffmpegVideo->isPlay){
        ffmpegVideo->stop();
    }
    //释放
    av_free_packet(packet);
    avformat_free_context(pFormatCtx);
}

2.因为音频播放就是一直播放,所以音频播放和单独的音频播放并没有多大差别,只是多了一个时间的记录。其中,因为pakcet是开始压入队列,然后再从队列中取出,当注意到线程安全,此处使用生产者消费者模式,也就是说当队列中有了pakcet过后才能从中取出,不然的话就等待。

//生产者
int FFmpegAudio::put(AVPacket *packet) {
    AVPacket *packet1 = (AVPacket *) av_mallocz(sizeof(AVPacket));
    if (av_packet_ref(packet1, packet)) {
//        克隆失败
        return 0;
    }
    pthread_mutex_lock(&mutex);
    queue.push(packet1);
    LOGE("压入一帧音频数据  队列%d ",queue.size());
//    给消费者解锁
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return 1;
}
//消费者
int FFmpegAudio::get(AVPacket *packet) {
    pthread_mutex_lock(&mutex);
    while (isPlay) {
        if (!queue.empty()) {
//            从队列取出一个packet   clone一个 给入参对象
            if (av_packet_ref(packet, queue.front())) {
                break;
            }
//            取成功了  弹出队列  销毁packet
            AVPacket *pkt = queue.front();
            queue.pop();
            LOGE("取出一 个音频帧%d",queue.size());
            av_free(pkt);
            break;
        } else {
//            如果队列里面没有数据的话  一直等待阻塞
            pthread_cond_wait(&cond, &mutex);
        }
    }
    pthread_mutex_unlock(&mutex);
    return 0;
}
//时间计算
//回调函数
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context){
    //得到pcm数据
    LOGE("回调pcm数据")
    FFmpegMusic *musicplay = (FFmpegMusic *) context;
    int datasize = getPcm(musicplay);
    if(datasize>0){
        //第一针所需要时间采样字节/采样率
        double time = datasize/(44100*2*2);
        //
        musicplay->clock=time+musicplay->clock;
        LOGE("当前一帧声音时间%f   播放时间%f",time,musicplay->clock);
        (*bq)->Enqueue(bq,musicplay->out_buffer,datasize);
        LOGE("播放 %d ",musicplay->queue.size());
    }
}
//时间矫正
        if (avPacket->pts != AV_NOPTS_VALUE) {
            agrs->clock = av_q2d(agrs->time_base) * avPacket->pts;
        }

3.视频播放线程,因为视频是根据播放的时间来进行一个加速或者延迟播放的,所以对时间的计算是很重要的。

void *videoPlay(void *args){
    FFmpegVideo *ffmpegVideo = (FFmpegVideo *) args;
    //申请AVFrame
    AVFrame *frame = av_frame_alloc();//分配一个AVFrame结构体,AVFrame结构体一般用于存储原始数据,指向解码后的原始帧
    AVFrame *rgb_frame = av_frame_alloc();//分配一个AVFrame结构体,指向存放转换成rgb后的帧
    AVPacket *packet = (AVPacket *) av_mallocz(sizeof(AVPacket));
    //输出文件
    //FILE *fp = fopen(outputPath,"wb");


    //缓存区
    uint8_t  *out_buffer= (uint8_t *)av_mallocz(avpicture_get_size(AV_PIX_FMT_RGBA,
                                                                  ffmpegVideo->codec->width,ffmpegVideo->codec->height));
    //与缓存区相关联,设置rgb_frame缓存区
    avpicture_fill((AVPicture *)rgb_frame,out_buffer,AV_PIX_FMT_RGBA,ffmpegVideo->codec->width,ffmpegVideo->codec->height);


    LOGE("转换成rgba格式")
    ffmpegVideo->swsContext = sws_getContext(ffmpegVideo->codec->width,ffmpegVideo->codec->height,ffmpegVideo->codec->pix_fmt,
                                            ffmpegVideo->codec->width,ffmpegVideo->codec->height,AV_PIX_FMT_RGBA,
                                            SWS_BICUBIC,NULL,NULL,NULL);



    LOGE("LC XXXXX  %f",ffmpegVideo->codec);

    double  last_play  //上一帧的播放时间
    ,play             //当前帧的播放时间
    ,last_delay    // 上一次播放视频的两帧视频间隔时间
    ,delay         //两帧视频间隔时间
    ,audio_clock //音频轨道 实际播放时间
    ,diff   //音频帧与视频帧相差时间
    ,sync_threshold
    ,start_time  //从第一帧开始的绝对时间
    ,pts
    ,actual_delay//真正需要延迟时间
    ;

    //两帧间隔合理间隔时间
    start_time = av_gettime() / 1000000.0;
    int frameCount;
    int h =0;
    LOGE("解码 ")
    while (ffmpegVideo->isPlay) {
        ffmpegVideo->get(packet);
        LOGE("解码 %d",packet->stream_index)
        avcodec_decode_video2(ffmpegVideo->codec, frame, &frameCount, packet);
        if(!frameCount){
            continue;
        }
        //转换为rgb格式
        sws_scale(ffmpegVideo->swsContext,(const uint8_t *const *)frame->data,frame->linesize,0,
                  frame->height,rgb_frame->data,
                  rgb_frame->linesize);
        LOGE("frame 宽%d,高%d",frame->width,frame->height);
        LOGE("rgb格式 宽%d,高%d",rgb_frame->width,rgb_frame->height);

        if((pts=av_frame_get_best_effort_timestamp(frame))==AV_NOPTS_VALUE){
            pts=0;
        }

        play = pts*av_q2d(ffmpegVideo->time_base);
        //纠正时间
        play = ffmpegVideo->synchronize(frame,play);
        delay = play - last_play;
        if (delay <= 0 || delay > 1) {
            delay = last_delay;
        }
        audio_clock = ffmpegVideo->ffmpegMusic->clock;
        last_delay = delay;
        last_play = play;
//音频与视频的时间差
        diff = ffmpegVideo->clock - audio_clock;
//        在合理范围外  才会延迟  加快
        sync_threshold = (delay > 0.01 ? 0.01 : delay);

        if (fabs(diff) < 10) {
            if (diff <= -sync_threshold) {
                delay = 0;
            } else if (diff >=sync_threshold) {
                delay = 2 * delay;
            }
        }
        start_time += delay;
        actual_delay=start_time-av_gettime()/1000000.0;
        if (actual_delay < 0.01) {
            actual_delay = 0.01;
        }
        av_usleep(actual_delay*1000000.0+6000);
        LOGE("播放视频")
        video_call(rgb_frame);
//        av_packet_unref(packet);
//        av_frame_unref(rgb_frame);
//        av_frame_unref(frame);
    }
    LOGE("free packet");
    av_free(packet);
    LOGE("free packet ok");
    LOGE("free packet");
    av_frame_free(&frame);
    av_frame_free(&rgb_frame);
    sws_freeContext(ffmpegVideo->swsContext);
    size_t size = ffmpegVideo->queue.size();
    for (int i = 0; i < size; ++i) {
        AVPacket *pkt = ffmpegVideo->queue.front();
        av_free(pkt);
        ffmpegVideo->queue.pop();
    }
    LOGE("VIDEO EXIT");
    pthread_exit(0);
    }

从av_usleep(actual_delay*1000000.0+6000);这段代码中可以看出,我们真正需要的就是一个真正需要延迟时间,所以相比于单独播放视频,就是计算出真正需要延迟的时间。

小结

因为有单独的音视频播放作为基础,所以实现同步播放也不是很难,搞清楚以下几点就行:
1.同步播放并没有完美的同步播放,保持在一个合理的范围即可;
2.因为人对声音比较敏感,所以同步播放以音频播放为基础,视频播放以追赶或者延迟形式进行播放;
3.在音频播放时计算出从第一帧开始的播放时间并矫正,用于视频播放的延迟时间的计算;
4.计算出视频播放真正需要延迟的时间,视频播放。

 

今天关于ffmpeg 打包TS介绍ffmpeg下载ts文件的讲解已经结束,谢谢您的阅读,如果想了解更多关于Android 中集成 ffmpeg (一):编译 ffmpeg、Android使用FFmpeg(一)--编译ffmpeg、Android使用FFmpeg(三)--ffmpeg实现视频播放、Android使用FFmpeg(六)--ffmpeg实现音视频同步播放的相关知识,请在本站搜索。

本文标签: