ffmpeg为mkv封装格式的音视频文件添加内挂字幕

Posted tusong86

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ffmpeg为mkv封装格式的音视频文件添加内挂字幕相关的知识,希望对你有一定的参考价值。

现在好莱坞的电影,都是全球看,一个地区的人看电影时,电影屏幕上应该展示对应的本地区语言字幕。故电影画面在不同的地区,需要配置不同的语言字幕。故视频画面里面的字幕应该可以拆出来,不能像老版三国演义,每到经典处,展示出文字,如下所示:

这种文字是直接嵌入到视频画面,无法拆出来,这种字幕也叫内嵌字幕。

本文要讲的是内挂字幕,字幕在视频文件里面,但是是独立的通道,可以独立拆出来。当然,还有一种外挂字幕,是在视频文件外面,播放器播放时,可以选择本地的字幕文件。

就封装格式而言,目前mkv对字幕支持的最好,读者可以先准备下字幕文件,字幕文件,读者可以网上下载现有的,也可以自己制作,本文准备的字幕文件ts.ass的内容如下:

[Script Info]
Title: Untitled
ScriptType: v4.00+
PlayResX:1280
PlayResY:720
WrapStyle: 0
ScaledBorderAndShadow: yes

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 2,0:00:00.22,0:00:31.93,Default,,0,0,0,,就算身处 流逝的时光里
Dialogue: 0,0:00:32.02,0:00:36.18,Default,,0,0,0,,也只有倦怠 在原地打转不停
Dialogue: 0,0:00:36.19,0:00:38.85,Default,,0,0,0,,从我身边 渐行渐远的心
Dialogue: 0,0:00:39.03,0:00:43.15,Default,,0,0,0,,再也模糊不清 你明白吗
Dialogue: 0,0:00:43.20,0:00:45.67,Default,,0,0,0,,我的身体 已经动弹不得
Dialogue: 0,0:00:45.79,0:00:50.16,Default,,0,0,0,,在时间的狭缝里 随波逐流
Dialogue: 0,0:00:50.17,0:00:53.26,Default,,0,0,0,,周围的一切 都与我无关
Dialogue: 0,0:00:53.39,0:00:57.07,Default,,0,0,0,,我就是我 仅·此·而·已
Dialogue: 0,0:00:57.18,0:01:00.04,Default,,0,0,0,,我在做梦吗?什么都没在看
Dialogue: 0,0:01:00.18,0:01:03.56,Default,,0,0,0,,出口也是枉然 自怜自艾的废话
Dialogue: 0,0:01:03.66,0:01:07.08,Default,,0,0,0,,悲伤什么的 只会徒增疲倦啊
Dialogue: 0,0:01:07.21,0:01:10.52,Default,,0,0,0,,干脆就这样 在麻木中度日吧
Dialogue: 0,0:01:10.62,0:01:13.95,Default,,0,0,0,,就算被灌以 喧嚣的闲言碎语
Dialogue: 0,0:01:14.09,0:01:17.45,Default,,0,0,0,,我的心也已经 不再起一丝涟漪
Dialogue: 0,0:01:17.56,0:01:21.03,Default,,0,0,0,,如果我能够 驱使自己的话
Dialogue: 0,0:01:21.13,0:01:24.39,Default,,0,0,0,,就让这一切 被黑暗所吞没吧
Dialogue: 0,0:01:24.50,0:01:28.06,Default,,0,0,0,,这样的我 还有未来可言吗
Dialogue: 0,0:01:28.19,0:01:31.46,Default,,0,0,0,,这种世界 允许我的存在吗
Dialogue: 0,0:01:31.56,0:01:34.85,Default,,0,0,0,,此刻感到窒息吗?此刻觉得悲伤吗
Dialogue: 0,0:01:34.98,0:01:38.45,Default,,0,0,0,,就连自己的事 也根本搞不懂啊
Dialogue: 0,0:01:38.55,0:01:41.94,Default,,0,0,0,,就算走下去 也只是徒增疲倦
Dialogue: 0,0:01:42.06,0:01:45.32,Default,,0,0,0,,对他人的一切 完全无法理解
Dialogue: 0,0:01:45.42,0:01:48.64,Default,,0,0,0,,这样的我 如果还能改变
Dialogue: 0,0:01:48.78,0:01:52.14,Default,,0,0,0,,还能改变的话 可以化为空白吗
Dialogue: 0,0:02:06.79,0:02:09.33,Default,,0,0,0,,就算身处 流逝的时光里
Dialogue: 0,0:02:09.48,0:02:13.64,Default,,0,0,0,,也只有倦怠 在原地打转不停
Dialogue: 0,0:02:13.77,0:02:16.24,Default,,0,0,0,,从我身边 渐行渐远的心
Dialogue: 0,0:02:16.37,0:02:20.54,Default,,0,0,0,,再也模糊不清 你明白吗
Dialogue: 0,0:02:20.67,0:02:23.26,Default,,0,0,0,,我的身体 已经动弹不得
Dialogue: 0,0:02:23.36,0:02:27.67,Default,,0,0,0,,在时间的狭缝里 随波逐流
Dialogue: 0,0:02:27.68,0:02:31.19,Default,,0,0,0,,周围的一切 都与我无关
Dialogue: 0,0:02:31.28,0:02:34.31,Default,,0,0,0,,我就是我 仅·此·而·已
Dialogue: 0,0:02:34.49,0:02:37.50,Default,,0,0,0,,我在做梦吗?什么都没在看
Dialogue: 0,0:02:37.62,0:02:40.93,Default,,0,0,0,,出口也是枉然 自怜自艾的废话
Dialogue: 0,0:02:41.03,0:02:44.30,Default,,0,0,0,,悲伤什么的 只会徒增疲倦啊
Dialogue: 0,0:02:44.43,0:02:47.91,Default,,0,0,0,,干脆就这样 在麻木中度日吧
Dialogue: 0,0:02:47.99,0:02:51.61,Default,,0,0,0,,就算被灌以 喧嚣的闲言碎语
Dialogue: 0,0:02:51.72,0:02:54.73,Default,,0,0,0,,我的心也已经 不再起一丝涟漪
Dialogue: 0,0:02:54.82,0:02:58.30,Default,,0,0,0,,如果我能够 驱使自己的话
Dialogue: 0,0:02:58.39,0:03:02.04,Default,,0,0,0,,就让这一切 被黑暗所吞没吧
Dialogue: 0,0:03:02.05,0:03:05.39,Default,,0,0,0,,如果任我驱使 驱使自己的话
Dialogue: 0,0:03:05.47,0:03:08.92,Default,,0,0,0,,一切都会毁灭 一切都会毁灭啊
Dialogue: 0,0:03:09.03,0:03:12.36,Default,,0,0,0,,被悲伤笼罩 被悲伤笼罩的话
Dialogue: 0,0:03:12.47,0:03:15.74,Default,,0,0,0,,我的心还能够 化为空白吗
Dialogue: 0,0:03:15.85,0:03:19.25,Default,,0,0,0,,不论你的存在 还是我的存在
Dialogue: 0,0:03:19.35,0:03:22.67,Default,,0,0,0,,这一切的真实 我都一无所知
Dialogue: 0,0:03:22.79,0:03:26.18,Default,,0,0,0,,如果在此睁开 这沉重的双眼
Dialogue: 0,0:03:26.36,0:03:29.88,Default,,0,0,0,,一切都会毁灭 被黑暗所吞没


读者可以清晰的看到,哪段时间至哪段时间,界面需要展示的文字,比如最后一个Dialogue显示在03:26.36到0:03:29.88这段时间,界面应该展示一切都会毁灭 被黑暗所吞没。

可以通过如下的ffmpeg命令降此字幕内挂到视频文件中

ffmpeg -i TAEYEON-Weekend.mkv -i ts.ass -c copy output.mkv

下面用ffmpeg代码的方式展示如何实现。
首先,需要说明的是,字幕跟音频,视频一样,有自己的通道,有自己的time_base,其读取方法也是av_read_frame。这点跟内嵌字幕不一样,在一个视频中,添加内嵌文字,可以通过滤镜drawtext实现,有解码,滤镜运算,编码过程,很费时,内挂不一样,没有这三个费时的计算,故往视频文件中添加内挂字幕很快。

其次,本人通过两个队列m_vecMediaPacket和m_vecAssPacket来存储读取的packet,然后在一个线程里面按照写入时间顺序分别写入m_vecMediaPacket和m_vecAssPacket的数据。

std::deque<AVPacket *> m_vecMediaPacket;
std::deque<AVPacket *> m_vecAssPacket;

此处,本人在av_read_frame,得到AVPacket后,没有直接调用av_interleaved_write_frame写文件,最主要的原因是av_interleaved_write_frame里面会对AVPacket的时间(相对各自的AVStream)进行排序,若视频文件比较大,则可能里面需要分配的空间也越来越大,最终由于内存不足导致崩溃。
故本人将读取的音视频packet和字幕packet分别存入队列,然后按照音视频播放同步的原理,调用av_write_frame依次写入m_vecMediaPacket和m_vecAssPacket里面的内容。也就是音视频的packet和字幕的packet,由自己编码判断谁先写(代码中av_compare_ts部分),而不是交由av_interleaved_write_frame处理。

再次,本人讲解下代码的大致结构:
1.用avformat_open_input分别打开媒体文件和字幕文件
2.avformat_alloc_output_context2构建输出文件context后,用avformat_new_stream分别往里面添加媒体流和字幕流,代码如下:

int iStreamNum = m_pFormatCtx_MediaFile->nb_streams;
for (int i = 0; i < iStreamNum; i++)

	AVCodec* pCodecEncode_Media = (AVCodec *)avcodec_find_encoder(m_pFormatCtx_MediaFile->streams[i]->codecpar->codec_id);
	AVStream *pMediaStream = avformat_new_stream(m_pFormatCtx_Out, pCodecEncode_Media);
	if (!pCodecEncode_Media)
	
		break;
	
	avcodec_parameters_copy(pMediaStream->codecpar, m_pFormatCtx_MediaFile->streams[i]->codecpar);
	pMediaStream->codecpar->codec_tag = 0;



	
	AVCodec* pCodecEncode_Ass = (AVCodec *)avcodec_find_encoder(m_pFormatCtx_AssFile->streams[0]->codecpar->codec_id);
	AVStream *pAssStream = avformat_new_stream(m_pFormatCtx_Out, pCodecEncode_Ass);
	if (!pAssStream)
	
		break;
	
	avcodec_parameters_copy(pAssStream->codecpar, m_pFormatCtx_AssFile->streams[0]->codecpar);
	pAssStream->codecpar->codec_tag = 0;

3.创建三个线程,如下所示:

m_hMediaFileReadThread = CreateThread(NULL, 0, MediaFileReadProc, this, 0, NULL);
m_hAssFileReadThread = CreateThread(NULL, 0, AssFileReadProc, this, 0, NULL);
m_hWriteThread = CreateThread(NULL, 0, WriteProc, this, 0, NULL);

前两个线程分别读取媒体流和字幕流,然后塞入队列,第三个线程读取两个队列中的数据,然后按照时间顺序写入packet。
这里说明下,对于字幕流而言,av_read_frame,本人调用到了48次,这48次,其实也是上面的ass文件中,Dialogue节点的数量。

最后,是代码,文件结构如下:

其中FfmpegMkvTest.cpp内容如下:

#include <iostream>
#include "FfmpegAddAss.h"

int main()

	CFfmpegAddAss cFfmpegAddAss;
	std::string strMediaFile = "D:/learn/ffmpeg/FfmpegConvert/x64/Release/TAEYEON-Weekend.mkv";
	std::string strAssFile = "D:/learn/ffmpeg/FfmpegConvert/x64/Release/ts.ass";

	std::string strOutFile = "D:/learn/ffmpeg/FfmpegConvert/x64/Release/TAEYEON-Weekend_ass.mkv";
	cFfmpegAddAss.StartAddAss(strMediaFile, strAssFile, strOutFile);
	cFfmpegAddAss.WaitFinish();
	return 0;

其中WaitFinish函数在文件处理结束后,会返回。

FfmpegAddAss.h的内容如下:

#pragma once

#include <string>
#include <Windows.h>
#include <deque>

#define MAX_PACKET_NUM 200

#ifdef	__cplusplus
extern "C"

#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavdevice/avdevice.h"
#include "libavutil/audio_fifo.h"
#include "libavutil/avutil.h"
#include "libavutil/fifo.h"
#include "libavutil/frame.h"
#include "libavutil/imgutils.h"

#include "libavfilter/avfilter.h"
#include "libavfilter/buffersink.h"
#include "libavfilter/buffersrc.h"


#ifdef __cplusplus
;
#endif

class CFfmpegAddAss

public:
	CFfmpegAddAss();
	~CFfmpegAddAss();
public:
	int StartAddAss(std::string strMediaFile, std::string strAssFile, std::string strOutFile);
	void WaitFinish();
private:
	int OpenMediaFile(std::string strMediaFile);
	int OpenAssFile(std::string strAssFile);
	int OpenOutFile(std::string strOutFile);
private:
	static DWORD WINAPI MediaFileReadProc(LPVOID lpParam);
	void MediaFileRead();

	static DWORD WINAPI AssFileReadProc(LPVOID lpParam);
	void AssFileRead();

	static DWORD WINAPI WriteProc(LPVOID lpParam);
	void Write();
private:
	AVFormatContext *m_pFormatCtx_MediaFile = NULL;
	AVFormatContext *m_pFormatCtx_AssFile = NULL;
	AVFormatContext *m_pFormatCtx_Out = NULL;
	int m_iAssStreamIndex = -1;
	HANDLE m_hMediaFileReadThread = NULL;
	HANDLE m_hAssFileReadThread = NULL;
	HANDLE m_hWriteThread = NULL;
	std::deque<AVPacket *> m_vecMediaPacket;
	std::deque<AVPacket *> m_vecAssPacket;
	CRITICAL_SECTION m_csMediaSection;
	CRITICAL_SECTION m_csAssSection;
	bool m_bStart = false;
;


FfmpegAddAss.cpp内容如下:

#include "FfmpegAddAss.h"


#ifdef	__cplusplus
extern "C"

#endif

#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avdevice.lib")
#pragma comment(lib, "avfilter.lib")
#pragma comment(lib, "postproc.lib")
#pragma comment(lib, "swresample.lib")
#pragma comment(lib, "swscale.lib")


#ifdef __cplusplus
;
#endif

CFfmpegAddAss::CFfmpegAddAss()

	InitializeCriticalSection(&m_csMediaSection);
	InitializeCriticalSection(&m_csAssSection);


CFfmpegAddAss::~CFfmpegAddAss()

	DeleteCriticalSection(&m_csMediaSection);
	DeleteCriticalSection(&m_csAssSection);


int CFfmpegAddAss::StartAddAss(std::string strMediaFile, std::string strAssFile, std::string strOutFile)

	int ret = 0;
	do
	
		ret = OpenMediaFile(strMediaFile);
		if (ret < 0)
		
			break;
		
		ret = OpenAssFile(strAssFile);
		if (ret < 0)
		
			break;
		
		ret = OpenOutFile(strOutFile);
		if (ret < 0)
		
			break;
		
		m_bStart = true;
		m_hMediaFileReadThread = CreateThread(NULL, 0, MediaFileReadProc, this, 0, NULL);
		m_hAssFileReadThread = CreateThread(NULL, 0, AssFileReadProc, this, 0, NULL);
		m_hWriteThread = CreateThread(NULL, 0, WriteProc, this, 0, NULL);

	 while (0);
	
	return ret;


void CFfmpegAddAss::WaitFinish()

	DWORD dw = 0;
	for (int i = 0; i < 10000; i++)
	
		if (m_hMediaFileReadThread == NULL && m_hAssFileReadThread == NULL)
		
			break;
		
		if (m_hMediaFileReadThread != NULL)
		
			dw = WaitForSingleObject(m_hMediaFileReadThread, 1000);
			if (dw == WAIT_OBJECT_0)
			
				CloseHandle(m_hMediaFileReadThread);
				m_hMediaFileReadThread = NULL;
			
		
		
		if (m_hAssFileReadThread != NULL)
		
			dw = WaitForSingleObject(m_hAssFileReadThread, 1000);
			if (dw == WAIT_OBJECT_0)
			
				CloseHandle(m_hAssFileReadThread);
				m_hAssFileReadThread = NULL;
			
		
	

	while (m_vecMediaPacket.size() > 0 && m_vecAssPacket.size() > 0)
	
		Sleep(1000);
	
	Sleep(1000);
	m_bStart = false;

	WaitForSingleObject(m_hWriteThread, INFINITE);
	CloseHandle(m_hWriteThread);
	m_hWriteThread = NULL;


int CFfmpegAddAss::OpenMediaFile(std::string strMediaFile)

	int ret = -1;

	do
	
		if ((ret = avformat_open_input(&m_pFormatCtx_MediaFile, strMediaFile.c_str(), 0, 0)) < 0) 
			break;
		
		if ((ret = avformat_find_stream_info(m_pFormatCtx_MediaFile, 0)) < 0) 
			break;
		

		ret = 0;
	 while (0);


	return ret;


int CFfmpegAddAss::OpenAssFile(std::string strAssFile)

	int ret = -1;

	do
	
		if ((ret = avformat_open_input(&m_pFormatCtx_AssFile, strAssFile.c_str(), 0, 0)) < 0) 
			break;
		
		if ((ret = avformat_find_stream_info(m_pFormatCtx_AssFile, 0)) < 0) 
			break;
		

		ret = 0;
	 while (0);


	return ret;



int CFfmpegAddAss::OpenOutFile(std::string strOutFile)

	int iRet = -1;

	do
	
		avformat_alloc_output_context2(&m_pFormatCtx_Out, NULL, NULL, strOutFile.c_str());

		int iStreamNum = m_pFormatCtx_MediaFile->nb_streams;
		for (int i = 0; i < iStreamNum; i++)
		
			AVCodec* pCodecEncode_Media = (AVCodec *)avcodec_find_encoder(m_pFormatCtx_MediaFile->streams[i]->codecpar->codec_id);
			AVStream *pMediaStream = avformat_new_stream(m_pFormatCtx_Out, pCodecEncode_Media);
			if (!pCodecEncode_Media)
			
				break;
			
			avcodec_parameters_copy(pMediaStream->codecpar, m_pFormatCtx_MediaFile->streams[i]->codecpar);
			pMediaStream->codecpar->codec_tag = 0;
		

		
			
			AVCodec* pCodecEncode_Ass = (AVCodec *)avcodec_find_encoder(m_pFormatCtx_AssFile->streams[0]->codecpar->codec_id);
			AVStream *pAssStream = avformat_new_stream(m_pFormatCtx_Out, pCodecEncode_Ass);
			if (!pAssStream)
			
				break;
			
			avcodec_parameters_copy(pAssStream->codecpar, m_pFormatCtx_AssFile->streams[0]->codecpar);
			pAssStream->codecpar->codec_tag = 0;
		

		if (!(m_pFormatCtx_Out->oformat->flags & AVFMT_NOFILE))
		
			if (avio_open(&m_pFormatCtx_Out->pb, strOutFile.c_str(), AVIO_FLAG_WRITE) < 0)
			
				break;
			
		



		if (avformat_write_header(m_pFormatCtx_Out, NULL) < 0)
		
			break;
		
		m_iAssStreamIndex = iStreamNum;
		iRet = 0;
	 while (0);


	if (iRet != 0)
	
		if (m_pFormatCtx_Out != NULL)
		
			avformat_free_context(m_pFormatCtx_Out);
			m_pFormatCtx_Out = NULL;
		
	

	return iRet;


DWORD WINAPI CFfmpegAddAss::MediaFileReadProc(LPVOID lpParam)

	CFfmpegAddAss *pFfmpegAddAss = (CFfmpegAddAss *)lpParam;
	if (pFfmpegAddAss != NULL)
	
		pFfmpegAddAss->MediaFileRead();
	
	return 0;


void CFfmpegAddAss::MediaFileRead()

	AVPacket packet =  0 ;
	int ret = 

音视频处理之FFmpeg封装格式20180510

一、FFMPEG的封装格式转换器(无编解码)

1.封装格式转换

所谓的封装格式转换,就是在AVI,FLV,MKV,MP4这些格式之间转换(对应.avi,.flv,.mkv,.mp4文件)。

需要注意的是,本程序并不进行视音频的编码和解码工作。而是直接将视音频压缩码流从一种封装格式文件中获取出来然后打包成另外一种封装格式的文件。

本程序的工作原理如下图1所示:

 

由图可见,本程序并不进行视频和音频的编解码工作,因此本程序和普通的转码软件相比,有以下两个特点:

处理速度极快。视音频编解码算法十分复杂,占据了转码的绝大部分时间。因为不需要进行视音频的编码和解码,所以节约了大量的时间。

视音频质量无损。因为不需要进行视音频的编码和解码,所以不会有视音频的压缩损伤。

 

2.基于FFmpeg的Remuxer的流程图

下面附上基于FFmpeg的Remuxer的流程图。图2中使用浅红色标出了关键的数据结构,浅蓝色标出了输出视频数据的函数。

可见成个程序包含了对两个文件的处理:读取输入文件(位于左边)和写入输出文件(位于右边)。中间使用了一个avcodec_copy_context()拷贝输入的AVCodecContext到输出的AVCodecContext。

 

简单介绍一下流程中关键函数的意义:

输入文件操作:

avformat_open_input():打开输入文件,初始化输入视频码流的AVFormatContext。

av_read_frame():从输入文件中读取一个AVPacket。

 

输出文件操作:

avformat_alloc_output_context2():初始化输出视频码流的AVFormatContext。

avformat_new_stream():创建输出码流的AVStream。

avcodec_copy_context():拷贝输入视频码流的AVCodecContex的数值t到输出视频的AVCodecContext。

avio_open():打开输出文件。

avformat_write_header():写文件头(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

av_interleaved_write_frame():将AVPacket(存储视频压缩码流数据)写入文件。

av_write_trailer():写文件尾(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

 

二、FFmpegRemuxer代码

基于FFmpeg的封装格式转换器,取了个名字称为FFmpegRemuxer

主要是FFmpegRemuxer.cpp文件,代码如下(基本上每一行都有注释):

  1 /*****************************************************************************
  2 * Copyright (C) 2017-2020 Hanson Yu  All rights reserved.
  3 ------------------------------------------------------------------------------
  4 * File Module       :     FFmpegRemuxer.cpp
  5 * Description       :     FFmpegRemuxer Demo
  6 
  7 输出结果:
  8 Input #0, flv, from \'cuc_ieschool1.flv\':
  9   Metadata:
 10     metadatacreator : iku
 11     hasKeyframes    : true
 12     hasVideo        : true
 13     hasAudio        : true
 14     hasMetadata     : true
 15     canSeekToEnd    : false
 16     datasize        : 932906
 17     videosize       : 787866
 18     audiosize       : 140052
 19     lasttimestamp   : 34
 20     lastkeyframetimestamp: 30
 21     lastkeyframelocation: 886498
 22     encoder         : Lavf55.19.104
 23   Duration: 00:00:34.20, start: 0.042000, bitrate: 394 kb/s
 24     Stream #0:0: Video: h264 (High), yuv420p, 512x288 [SAR 1:1 DAR 16:9], 15.17 fps, 15 tbr, 1k tbn, 30 tbc
 25     Stream #0:1: Audio: mp3, 44100 Hz, stereo, s16p, 128 kb/s
 26 Output #0, mp4, to \'cuc_ieschool1.mp4\':
 27     Stream #0:0: Video: h264, yuv420p, 512x288 [SAR 1:1 DAR 16:9], q=2-31, 90k tbn, 30 tbc
 28     Stream #0:1: Audio: mp3, 44100 Hz, stereo, s16p, 128 kb/s
 29 Write        0 frames to output file
 30 Write        1 frames to output file
 31 Write        2 frames to output file
 32 Write        3 frames to output file
 33 .
 34 .
 35 .
 36 
 37 * Created           :     2017.09.21.
 38 * Author            :     Yu Weifeng
 39 * Function List     :     
 40 * Last Modified     :     
 41 * History           :     
 42 * Modify Date      Version         Author           Modification
 43 * -----------------------------------------------
 44 * 2017/09/21      V1.0.0         Yu Weifeng       Created
 45 ******************************************************************************/
 46 #include <stdio.h>
 47 
 48 
 49 /*
 50 __STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS are a workaround to allow C++ programs to use stdint.h
 51 macros specified in the C99 standard that aren\'t in the C++ standard. The macros, such as UINT8_MAX, INT64_MIN,
 52 and INT32_C() may be defined already in C++ applications in other ways. To allow the user to decide
 53 if they want the macros defined as C99 does, many implementations require that __STDC_LIMIT_MACROS
 54 and __STDC_CONSTANT_MACROS be defined before stdint.h is included.
 55 
 56 This isn\'t part of the C++ standard, but it has been adopted by more than one implementation.
 57 */
 58 #define __STDC_CONSTANT_MACROS
 59 
 60 
 61 #ifdef _WIN32//Windows
 62 extern "C"
 63 {
 64     #include "libavformat/avformat.h"
 65 };
 66 #else//Linux...
 67     #ifdef __cplusplus
 68     extern "C"
 69     {
 70     #endif
 71         #include <libavformat/avformat.h>
 72     #ifdef __cplusplus
 73     };
 74     #endif
 75 #endif
 76 
 77 /*****************************************************************************
 78 -Fuction        : main
 79 -Description    : main
 80 -Input          : 
 81 -Output         : 
 82 -Return         : 
 83 * Modify Date      Version         Author           Modification
 84 * -----------------------------------------------
 85 * 2017/09/21      V1.0.0         Yu Weifeng       Created
 86 ******************************************************************************/
 87 int main(int argc, char* argv[])
 88 {
 89     AVOutputFormat * ptOutputFormat = NULL;//The output container format.Muxing only, must be set by the caller before avformat_write_header().
 90     AVFormatContext * ptInFormatContext = NULL;//输入文件的封装格式上下文,内部包含所有的视频信息
 91     AVFormatContext * ptOutFormatContext = NULL;//输出文件的封装格式上下文,内部包含所有的视频信息
 92     AVPacket tOutPacket ={0};//存储一帧压缩编码数据给输出文件
 93     const char * strInFileName=NULL, * strOutFileName = NULL;//输入文件名和输出文件名
 94     int iRet, i;
 95     int iFrameCount = 0;//输出的帧个数
 96     AVStream * ptInStream=NULL,* ptOutStream=NULL;//输入音视频流和输出音视频流
 97     
 98     if(argc!=3)//argc包括argv[0]也就是程序名称
 99     {
100         printf("Usage:%s InputFileURL OutputFileURL\\r\\n",argv[0]);
101         printf("For example:\\r\\n");
102         printf("%s InputFile.flv OutputFile.mp4\\r\\n",argv[0]);
103         return -1;
104     }
105     strInFileName = argv[1];//Input file URL
106     strOutFileName = argv[2];//Output file URL
107 
108     av_register_all();//注册FFmpeg所有组件    
109     
110     /*------------Input------------*/
111     if ((iRet = avformat_open_input(&ptInFormatContext, strInFileName, 0, 0)) < 0) 
112     {//打开输入视频文件
113         printf("Could not open input file\\r\\n");
114     }
115     else
116     {
117         if ((iRet = avformat_find_stream_info(ptInFormatContext, 0)) < 0) 
118         {//获取视频文件信息
119             printf("Failed to find input stream information\\r\\n");
120         }
121         else
122         {
123             av_dump_format(ptInFormatContext, 0, strInFileName, 0);//手工调试的函数,内部是log,输出相关的格式信息到log里面
124             
125             /*------------Output------------*/
126             
127             /*初始化一个用于输出的AVFormatContext结构体
128              *ctx:函数调用成功之后创建的AVFormatContext结构体。
129              *oformat:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,
130               可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。
131               PS:使用该参数需要自己手动获取AVOutputFormat,相对于使用后两个参数来说要麻烦一些。
132              *format_name:指定输出格式的名称。根据格式名称,FFmpeg会推测输出格式。输出格式可以是“flv”,“mkv”等等。
133              *filename:指定输出文件的名称。根据文件名称,FFmpeg会推测输出格式。文件名称可以是“xx.flv”,“yy.mkv”等等。
134              函数执行成功的话,其返回值大于等于0
135              */
136             avformat_alloc_output_context2(&ptOutFormatContext, NULL, NULL, strOutFileName);
137             if (!ptOutFormatContext) 
138             {
139                 printf("Could not create output context\\r\\n");
140                 iRet = AVERROR_UNKNOWN;
141             }
142             else
143             {
144                 ptOutputFormat = ptOutFormatContext->oformat;
145                 for (i = 0; i < ptInFormatContext->nb_streams; i++) 
146                 {
147                     //Create output AVStream according to input AVStream
148                     ptInStream = ptInFormatContext->streams[i];
149                     ptOutStream = avformat_new_stream(ptOutFormatContext, ptInStream->codec->codec);//给ptOutFormatContext中的流数组streams中的
150                     if (!ptOutStream) //一条流(数组中的元素)分配空间,也正是由于这里分配了空间,后续操作直接拷贝编码数据(pkt)就可以了。
151                     {
152                         printf("Failed allocating output stream\\r\\\\n");
153                         iRet = AVERROR_UNKNOWN;
154                         break;
155                     }
156                     else
157                     {
158                         if (avcodec_copy_context(ptOutStream->codec, ptInStream->codec) < 0) //Copy the settings of AVCodecContext
159                         {
160                             printf("Failed to copy context from input to output stream codec context\\r\\n");
161                             iRet = AVERROR_UNKNOWN;
162                             break;
163                         }
164                         else
165                         {
166                             ptOutStream->codec->codec_tag = 0;
167                             if (ptOutFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
168                                 ptOutStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
169                                                         
170                         }
171                     }
172                 }
173                 if(AVERROR_UNKNOWN == iRet)
174                 {
175                 }
176                 else
177                 {
178                     av_dump_format(ptOutFormatContext, 0, strOutFileName, 1);//Output information------------------
179                     //Open output file
180                     if (!(ptOutputFormat->flags & AVFMT_NOFILE))
181                     {   /*打开FFmpeg的输入输出文件,使后续读写操作可以执行
182                          *s:函数调用成功之后创建的AVIOContext结构体。
183                          *url:输入输出协议的地址(文件也是一种“广义”的协议,对于文件来说就是文件的路径)。
184                          *flags:打开地址的方式。可以选择只读,只写,或者读写。取值如下。
185                                  AVIO_FLAG_READ:只读。AVIO_FLAG_WRITE:只写。AVIO_FLAG_READ_WRITE:读写。*/
186                         iRet = avio_open(&ptOutFormatContext->pb, strOutFileName, AVIO_FLAG_WRITE);
187                         if (iRet < 0) 
188                         {
189                             printf("Could not open output file %s\\r\\n", strOutFileName);
190                         }
191                         else
192                         {
193                             //Write file header
194                             if (avformat_write_header(ptOutFormatContext, NULL) < 0) //avformat_write_header()中最关键的地方就是调用了AVOutputFormat的write_header()
195                             {//不同的AVOutputFormat有不同的write_header()的实现方法
196                                 printf("Error occurred when opening output file\\r\\n");
197                             }
198                             else
199                             {
200                                 while (1) 
201                                 {
202                                     //Get an AVPacket
203                                     iRet = av_read_frame(ptInFormatContext, &tOutPacket);//从输入文件读取一帧压缩数据
204                                     if (iRet < 0)
205                                         break;
206                                         
207                                     ptInStream = ptInFormatContext->streams[tOutPacket.stream_index];
208                                     ptOutStream = ptOutFormatContext->streams[tOutPacket.stream_index];
209                                     //Convert PTS/DTS
210                                     tOutPacket.pts = av_rescale_q_rnd(tOutPacket.pts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
211                                     tOutPacket.dts = av_rescale_q_rnd(tOutPacket.dts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
212                                     tOutPacket.duration = av_rescale_q(tOutPacket.duration, ptInStream->time_base, ptOutStream->time_base);
213                                     tOutPacket.pos = -1;
214                                     //Write
215                                     /*av_interleaved_write_frame包括interleave_packet()以及write_packet(),将还未输出的AVPacket输出出来
216                                      *write_packet()函数最关键的地方就是调用了AVOutputFormat中写入数据的方法。write_packet()实际上是一个函数指针,
217                                      指向特定的AVOutputFormat中的实现函数*/
218                                     if (av_interleaved_write_frame(ptOutFormatContext, &tOutPacket) < 0) 
219                                     {
220                                         printf("Error muxing packet\\r\\n");
221                                         break;
222                                     }
223                                     printf("Write %8d frames to output file\\r\\n", iFrameCount);
224                                     av_free_packet(&tOutPacket);//释放空间
225                                     iFrameCount++;
226                                 }
227                                 //Write file trailer//av_write_trailer()中最关键的地方就是调用了AVOutputFormat的write_trailer()
228                                 av_write_trailer(ptOutFormatContext);//不同的AVOutputFormat有不同的write_trailer()的实现方法
229                             }
230                             if (ptOutFormatContext && !(ptOutputFormat->flags & AVFMT_NOFILE))
231                                 avio_close(ptOutFormatContext->pb);//该函数用于关闭一个AVFormatContext->pb,一般情况下是和avio_open()成对使用的。
232                         }
233                     }
234                 }
235                 avformat_free_context(ptOutFormatContext);//释放空间
236             }
237         }
238         avformat_close_input(&ptInFormatContext);//该函数用于关闭一个AVFormatContext,一般情况下是和avformat_open_input()成对使用的。
239     }
240     return 0;
241 }
FFmpegRemuxer.cpp

具体代码见github:

https://github.com/fengweiyu/FFmpegFormat/FFmpegRemuxer

 

三、FFmpeg的封装格式处理:视音频复用器(muxer)

1.封装格式处理

视音频复用器(Muxer)即是将视频压缩数据(例如H.264)和音频压缩数据(例如AAC)合并到一个封装格式数据(例如MKV)中去。

如图3所示。在这个过程中并不涉及到编码和解码。

 

2.基于FFmpeg的muxer的流程图

程序的流程如下图4所示。从流程图中可以看出,一共初始化了3个AVFormatContext,其中2个用于输入,1个用于输出。3个AVFormatContext初始化之后,通过avcodec_copy_context()函数可以将输入视频/音频的参数拷贝至输出视频/音频的AVCodecContext结构体。

然后分别调用视频输入流和音频输入流的av_read_frame(),从视频输入流中取出视频的AVPacket,音频输入流中取出音频的AVPacket,分别将取出的AVPacket写入到输出文件中即可。

其间用到了一个不太常见的函数av_compare_ts(),是比较时间戳用的。通过该函数可以决定该写入视频还是音频。

 

本文介绍的视音频复用器,输入的视频不一定是H.264裸流文件,音频也不一定是纯音频文件。可以选择两个封装过的视音频文件作为输入。程序会从视频输入文件中“挑”出视频流,音频输入文件中“挑”出音频流,再将“挑选”出来的视音频流复用起来。

PS1:对于某些封装格式(例如MP4/FLV/MKV等)中的H.264,需要用到名称为“h264_mp4toannexb”的bitstream filter。

PS2:对于某些封装格式(例如MP4/FLV/MKV等)中的AAC,需要用到名称为“aac_adtstoasc”的bitstream filter。

 

简单介绍一下流程中各个重要函数的意义:

avformat_open_input():打开输入文件。

avcodec_copy_context():赋值AVCodecContext的参数。

avformat_alloc_output_context2():初始化输出文件。

avio_open():打开输出文件。

avformat_write_header():写入文件头。

av_compare_ts():比较时间戳,决定写入视频还是写入音频。这个函数相对要少见一些。

av_read_frame():从输入文件读取一个AVPacket。

av_interleaved_write_frame():写入一个AVPacket到输出文件。

av_write_trailer():写入文件尾。

 

3.优化为可以从内存中读取音视频数据

打开文件的函数是avformat_open_input(),直接将文件路径或者流媒体URL的字符串传递给该函数就可以了。

但其是否支持从内存中读取数据呢?

 

分析ffmpeg的源代码,发现其竟然是可以从内存中读取数据的,代码很简单,如下所示:

ptInFormatContext = avformat_alloc_context(); 

pbIoBuf = (unsigned char *)av_malloc(IO_BUFFER_SIZE); 

ptAVIO = avio_alloc_context(pbIoBuf, IO_BUFFER_SIZE, 0, NULL, FillIoBuffer, NULL, NULL);

ptInFormatContext->pb = ptAVIO;

 

ptInputFormat = av_find_input_format("h264");//得到ptInputFormat以便后面打开使用

if ((iRet = avformat_open_input(&ptInFormatContext, "", ptInputFormat, NULL)) < 0)

{

    printf("Could not open input file\\r\\n");

}

else

{

}

关键要在avformat_open_input()之前初始化一个AVIOContext,而且将原本的AVFormatContext的指针pb(AVIOContext类型)指向这个自行初始化AVIOContext。

当自行指定了AVIOContext之后,avformat_open_input()里面的URL参数就不起作用了。示例代码开辟了一块空间iobuffer作为AVIOContext的缓存。

FillIoBuffer则是将数据读取至iobuffer的回调函数。FillIoBuffer()形式(参数,返回值)是固定的,是一个回调函数,如下所示(只是个例子,具体怎么读取数据可以自行设计)。

示例中回调函数将文件中的内容通过fread()读入内存。

int FillIoBuffer(void *opaque, unsigned char *o_pbBuf, int i_iMaxSize) 

{

    int iRet=-1;

    if (!feof(g_fileH264))

    { 

        iRet = fread(o_pbBuf, 1, i_iMaxSize, g_fileH264); 

    } 

    else

    { 

    } 

    return iRet; 

整体结构大致如下:

FILE *fp_open; 

 

int fill_iobuffer(void *opaque, uint8_t *buf, int buf_size){ 

... 

 

int main(){ 

    ... 

    fp_open=fopen("test.h264","rb+"); 

    AVFormatContext *ic = NULL; 

    ic = avformat_alloc_context(); 

    unsigned char * iobuffer=(unsigned char *)av_malloc(32768); 

    AVIOContext *avio =avio_alloc_context(iobuffer, 32768,0,NULL,fill_iobuffer,NULL,NULL); 

    ic->pb=avio; 

    err = avformat_open_input(&ic, "nothing", NULL, NULL); 

    ...//解码 

 

4.将音视频数据输出到内存

同时再说明一下,和从内存中读取数据类似,ffmpeg也可以将处理后的数据输出到内存。

 

回调函数如下示例,可以将输出到内存的数据写入到文件中。

//写文件的回调函数 

int write_buffer(void *opaque, uint8_t *buf, int buf_size){ 

    if(!feof(fp_write)){ 

        int true_size=fwrite(buf,1,buf_size,fp_write); 

        return true_size; 

    }else{ 

        return -1; 

    } 

}

主函数如下所示:

FILE *fp_write; 

 

int write_buffer(void *opaque, uint8_t *buf, int buf_size){ 

... 

 

main(){ 

    ... 

    fp_write=fopen("src01.h264","wb+"); //输出文件 

    ... 

    AVFormatContext* ofmt_ctx=NULL; 

    avformat_alloc_output_context2(&ofmt_ctx, NULL, "h264", NULL); 

    unsigned char* outbuffer=(unsigned char*)av_malloc(32768); 

 

    AVIOContext *avio_out =avio_alloc_context(outbuffer, 32768,0,NULL,NULL,write_buffer,NULL);   

 

    ofmt_ctx->pb=avio_out;  

    ofmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO; 

    ... 

从上述可以很明显的看到,知道把写回调函数放到avio_alloc_context函数对应的位置就可以了。

 

四、FFmpegMuxer代码

基于FFmpeg的视音频复用器,取了个名字称为FFmpegMuxer

主要是FFmpegMuxer.cpp文件,代码如下(基本上每一行都有注释):

  1 /*****************************************************************************
  2 * Copyright (C) 2017-2020 Hanson Yu  All rights reserved.
  3 ------------------------------------------------------------------------------
  4 * File Module       :     FFmpegMuxer.cpp
  5 * Description       :     FFmpegMuxer Demo
  6 
  7 *先将H.264文件读入内存, 
  8 *再输出封装格式文件。
  9 
 10 输出结果:
 11 book@book-desktop:/work/project/FFmpegMuxer$ make clean;make
 12 rm FFmpegMuxer
 13 g++ FFmpegMuxer.cpp -I ./include -rdynamic ./lib/libavformat.so.57 ./lib/libavcodec.so.57 ./lib/libavutil.so.55 ./lib/libswresample.so.2 -o FFmpegMuxer 
 14 book@book-desktop:/work/project/FFmpegMuxer$ export LD_LIBRARY_PATH=./lib
 15 book@book-desktop:/work/project/FFmpegMuxer$ ./FFmpegMuxer sintel.h264 sintel.mp4 
 16 Input #0, h264, from \'sintel.h264\':
 17   Duration: N/A, bitrate: N/A
 18     Stream #0:0: Video: h264 (High), yuv420p(progressive), 640x360, 25 fps, 25 tbr, 1200k tbn, 50 tbc
 19 Output #0, mp4, to \'sintel.mp4\':
 20     Stream #0:0: Unknown: none
 21 [mp4 @ 0x9352d80] Using AVStream.codec.time_base as a timebase hint to the muxer is deprecated. Set AVStream.time_base instead.
 22 [mp4 @ 0x9352d80] Using AVStream.codec to pass codec parameters to muxers is deprecated, use AVStream.codecpar instead.
 23 Write iFrameIndex:1,stream_index:0,num:25,den:1
 24 Write iFrameIndex:2,stream_index:0,num:25,den:1
 25 Write iFrameIndex:3,stream_index:0,num:25,den:1
 26 .
 27 .
 28 .
 29 
 30 * Created           :     2017.09.21.
 31 * Author            :     Yu Weifeng
 32 * Function List     :     
 33 * Last Modified     :     
 34 * History           :     
 35 * Modify Date      Version         Author           Modification
 36 * -----------------------------------------------
 37 * 2017/09/21      V1.0.0         Yu Weifeng       Created
 38 ******************************************************************************/
 39 #include <stdio.h>
 40 
 41 
 42 /*
 43 __STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS are a workaround to allow C++ programs to use stdint.h
 44 macros specified in the C99 standard that aren\'t in the C++ standard. The macros, such as UINT8_MAX, INT64_MIN,
 45 and INT32_C() may be defined already in C++ applications in other ways. To allow the user to decide
 46 if they want the macros defined as C99 does, many implementations require that __STDC_LIMIT_MACROS
 47 and __STDC_CONSTANT_MACROS be defined before stdint.h is included.
 48 
 49 This isn\'t part of the C++ standard, but it has been adopted by more than one implementation.
 50 */
 51 #define __STDC_CONSTANT_MACROS
 52 
 53 
 54 #ifdef _WIN32//Windows
 55 extern "C"
 56 {
 57     #include "libavformat/avformat.h"
 58 };
 59 #else//Linux...
 60     #ifdef __cplusplus
 61     extern "C"
 62     {
 63     #endif
 64         #include <libavformat/avformat.h>
 65     #ifdef __cplusplus
 66     };
 67     #endif
 68 #endif
 69 
 70 #define IO_BUFFER_SIZE 32768  //缓存32k
 71   
 72 static FILE * g_fileH264=NULL;
 73 
 74  
 75 /*****************************************************************************
 76 -Fuction        : FillIoBuffer
 77 

以上是关于ffmpeg为mkv封装格式的音视频文件添加内挂字幕的主要内容,如果未能解决你的问题,请参考以下文章

音视频处理之FFmpeg封装格式20180510

FFMpeg SDK使用5调用FFmpeg SDK解析封装格式的视频为音频流和视频流

FFMpeg SDK使用4调用FFmpeg SDK解析封装格式的视频为音频流和视频流

FFMPEG音视频解码

FFMPEG+SDL实现视频播放器

ffmpeg 转换不损失质量