FFmpeg官方介绍文档:http://ffmpeg.org/doxygen/trunk/
FFmpeg的架构
FFmpeg包含以下几个库:
- libavutil:工具库,用来辅助进行简单的多媒体编程。包括安全的字符串函数,随机数生成器,一些数据结构,扩展的数学函数,基于编码与多媒体的功能。
- libavcodec:编解码相关的库,提供通用的编解码框架,并且包含了多种用于音频流,视频流,字幕流的编解码器,并且包含了一些比特流过滤器(filter)。
- libavformat:一个为音频流,视频流,字幕流提供通用多路复用(multiplexing/muxing)与反多路复用(demultiplexing/demuxing)框架的库。包含很多适合各种多媒体格式的复用器(muxer)与解复用器(demuxer)。
- libavdevice:一个通用框架库,用来从多媒体设备中抓取多媒体流,或将其渲染至多媒体设备,并且支持许多种设备,包括Video4Linux2, VfW, DShow, ALSA等。
- libavfilter:通用音视频过滤(filtering)库,包含多种过滤器,源与接收器。
- libswscale:此库提供高度优化的图像缩放,颜色空间与像素格式转换操作。
- libswresample:此库提供高度优化的音频重采样,缩放(rematrix)与采样格式转换等操作。
常用的数据结构
Ps.数据结构中包含的变量与数据结构列表不做赘述,再用到的时候在做介绍,简介见JavaDoc。
AVCodecContext
FFmpeg中主要的扩展数据结构,包含了编解码信息,音频/视频/字幕流信息等,并且提供了一系列函数来访问或修改这些数据。
AVFormatContext
FFmpeg中另外一个重要的数据结构,包含格式化的I/O上下文,主要用来进行I/O相关的操作,默认使用avformat_alloc_context()来初始化,用处与其字面意思一样,由于Java上的FFmpeg本质上还是JNI技术的一种实现,所以原理同C差不多,也是需要先开辟内存。
AVIOContext
包含字节流I/O上下文,用来管理输入输出数据,同样默认需要使用avio_alloc_context()来进行初始化。
AVFrame
这个数据结构包含了已解码的(raw)音频或视频数据。AVFrame必须使用av_frame_alloc()来初始化,需要注意的是这只会初始化AVFrame自身,具体的缓冲区数据需要使用其他方式来管理。在使用过后,AVFrame还需要使用av_frame_free()来释放。
音视频的编解码
对于多媒体编程来说,最为重要的就是多媒体数据的编解码,而编解码器(Codec)是音视频编解码的核心部分,主要涉及到了AVCodecContext这个数据结构。
容器的概念
容器(Container)封装了一个多媒体文件所需要的所有多媒体数据,包括音频,视频,字幕等,需要注意的是,容器的封装格式仅仅描述了多媒体文件以什么方式保存,外部表现为文件的后缀名,例如mp3,mp4,mkv等,并没有描述多媒体数据的具体编码方式,所以只看多媒体文件的后缀名,并不能确定其编码方式。
流与帧
流(Stream)是一种有序的数据载体,在FFmpeg中,可以表现为视频流,音频流,字幕流。流中的某一具体数据元素称为帧(Frame)。
FFmpeg的视频解码过程
一般来说,FFmpeg的视频解码分为以下几个步骤:
- 注册所有的容器格式以及其相对应的Codec
av_register_all()
- 打开文件,将其读入AVFormatContext中
avformat_open_input()
- 从文件中提取流信息
avformat_find_stream_info()
- 从多个数据流中找到需要的流
- 查询流所对应的解码器
avcodec_find_decoder()
- 打开解码器
avcodec_open2()
- 为解码帧分配内存
av_frame_alloc()
- 将数据从流读入至Packet
av_read_frame()
- 对视频帧进行解码
avcodec_decode_video2()
音视频Metadata的获取
Ps.以下是FFmpeg-JavaCPP的Metadata获取方式。
由于原FFmpeg是C编写的,而其JavaCPP实现是运用了JNI技术将其移植至Java,所以在写法上Metadata的获取还是与C有着很大的相似的。PHSS的Metadata读取函数如下:
- public Map<String, Object> readMetaData(Path path) throws Exception {
- av_register_all();
- Map<String, Object> metadataFullMap = new HashMap<>();
- Map<String, Object> metadataCurrentMap = new HashMap<>();
- AVFormatContext avFormatContext = avformat_alloc_context();
- AVDictionaryEntry entry = null;
- avformat_open_input(avFormatContext, path.toString(), null, null);
- avformat_find_stream_info(avFormatContext, ((PointerPointer) null));
- while ((entry = av_dict_get(avFormatContext.metadata(), "", entry, AV_DICT_IGNORE_SUFFIX)) != null) {
- metadataFullMap.put(entry.key().getString(), entry.value().getString());
- }
- for (String key : metadataList) {
- if (metadataFullMap.get(key) != null) {
- metadataCurrentMap.put(key, metadataFullMap.get(key));
- } else {
- metadataCurrentMap.put(key, "");
- }
- }
- metadataCurrentMap.put("duration", formatDuration(avFormatContext.duration()));
- metadataCurrentMap.put("bitrate", formatBitrate(avFormatContext.streams(0).codecpar().bit_rate()));
- metadataCurrentMap.put("sample_rate", avFormatContext.streams(0).codecpar().sample_rate());
- metadataCurrentMap.put("bit_depth", avFormatContext.streams(0).codecpar().bits_per_raw_sample());
- metadataCurrentMap.put("size", formatSize(path.toFile().length()));
- avformat_close_input(avFormatContext);
- return metadataCurrentMap;
- }
首先是通过av_register_all()函数来注册所有相关的组件,然后使用avformat_alloc_context()函数来新建一个AVFormatContext对象,此对象用来存储多媒体对象的基本构成信息。声明一个AVDictionaryEntry来储存音轨的元数据信息。
使用avformat_open_input()函数来将音轨写入avFormatContext的buffer中,然后使用av_dict_get()函数来将avFormatContext中结构化的metadata数据读入AVDictionaryEntry,然后便可以将AVDictionaryEntry中的元数据读出。
音频封面的获取
由于metadata只包含文字形式的数据,所以需要通过别的方法来获取音频的封面。由于处于FFmpeg的版本更替中,所以封面获取的相关类非常不稳定,因此PHSS暂时选择了FFmpeg-JavaCPP的上层实现,也就是JavaCV来实现音频封面的获取。
- public byte[] getArtwork(Path path) throws Exception {
- FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(path.toFile());
- Java2DFrameConverter converter = new Java2DFrameConverter();
- grabber.start();
- BufferedImage bufferedImage = converter.getBufferedImage(grabber.grabImage());
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ImageIO.write(bufferedImage, "jpg", outputStream);
- grabber.close();
- return outputStream.toByteArray();
- }
FFmpeg的Time base系统以及duration的计算
对于time base(时基)的概念,首先需要介绍关于音视频同步的简单知识。
参考文献:
https://zh.wikipedia.org/wiki/Java%E6%9C%AC%E5%9C%B0%E6%8E%A5%E5%8F%A3