本篇文章开始,我和大家一起来讨论这个经久不衰的音视频开发的难点 —— 音视频同步。囊括内容比较多,大到代码组织,小至C语法糖,尽力做到每一个像我一样的菜鸡都能掌握解决方法。
正式开始之前,我又想起了之前利用OpenGLES+MediaCodec的水印录制系列文章,当时没有处理音频,合成出来的mp4只有图像。其实随后我已经添加上去音频处理的代码,就是目录当中的视频编码工作类的核心CameraRecordEncoderCore2,版本2是在CameraRecordEncoderCore的基础上增加AudioRecord处理音频。使用方法也很简单,只要在编码工作类CameraRecordEncoder当中把CameraRecordEncoderCore的引用改为CameraRecordEncoderCore2,并且启动音频录制audioRecord,喂养音频数据drainAudioEncoder就可以了。有疑问的同学,详细细节可以私信联系。
为何我会提起这个呢,其实是因为 FFmpeg音视频同步解决方案的设计思路,是借鉴上方CameraRecordEncoderCore2的工作模式。所以我建议同学们如果有时间,还是去看看CameraRecordEncoderCore2。
之前的文章,都是一些FFmpeg的教学例子,全都是运行在主线程,并不专业。来到这里我们就要开始向专业靠近了,所以我们第一步就是要改造以前的代码,结合POSIX线程进行音视频的解码流程。
线程解码要怎么做了?思维敏捷的同学可能第一反正就是,一个线程读取数据包(av_read_frame)并发送到对应解码器(avcodec_send_packet);另一个线程就不断的从解码上接收AVFrame(avcodec_receive_frame)并做对应的处理。balabala的就把线程解码改造好了,堪称完美的代码逻辑大致如下:
// run on thread
void read_avpakcet()
{
while (av_read_frame(pFormatContext, pkt) >= 0)
{
if (pkt->stream_index == video_stream_index)
{
avcodec_send_packet(videoCodecCtx, packet);
}
if (pkt->stream_index == audio_stream_index)
{
avcodec_send_packet(audioCodecCtx, packet);
}
}
}
// run on the other thread
void handle_video_frame()
{
while(1)
{
int ret = avcodec_receive_frame(videoCodecCtx, yuv_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
LOGD("avcodec_receive_frame:%d\n", ret);
break;
}else if (ret < 0) {
LOGW("avcodec_receive_frame:%d\n", AVERROR(ret));
goto end; //end处进行资源释放等善后处理
}
if (ret >= 0) {
// handle avframe ...
}
}
}
运行发现黑屏!并且avcodec_receive_frame的返回值一直是 -541478725 ???通过FFMPEG的错误速查,发现是AVERROR_EOF?!这里深层的原因需要对avcodec_send_packet 和 avcodec_receive_frame进行源码分析,暂且不在本篇文章的内容范围,有兴趣的同学可以参考这篇文章,看看自己是否能参悟透彻,不明白的可以联系我一起讨论讨论。
现在我们暂且认为AVCodecContext是一个非线程安全的对象吧。 既然不能这样子做,那该怎么改造?既然AVCodecContext非线程安全,那么肯定的是 avcodec_send_packet 和 avcodec_receive_frame要在同一线程下工作。那么我只能在解封装格式上下文(AVFormatContext)之后,就要开始分开两路工作线程,并且把对应的数据包(AVPacket)分别缓存。建议流程如图所示:
在解决问题之前,还是先让我把重构的代码呈现出来,这些整理规范后的代码,或多或少已经可以用到实际的开发当中了,希望能帮助到大家。
public class SyncPlayer {
private Context context;
private String media_input_str;
private Surface surface;
public SyncPlayer(Context context) {
this.context = context;
nativeInit();
}
public void setMediaSource(String media_input_str){
this.media_input_str = media_input_str;
}
public void setRender(Surface surface){
this.surface = surface;
}
public void prepare() {
nativePrepare(media_input_str, surface);
}
public void play() {
nativePlay();
}
public void release() {
nativeRelease();
media_input_str = null;
surface = null;
}
private native void nativeInit();
private native void nativePrepare(String media_input_str, Surface surface);
private native int nativePlay();
private native void nativeRelease();
static
{
try {
System.loadLibrary("yuv");
System.loadLibrary("avutil");
System.loadLibrary("swscale");
System.loadLibrary("swresample");
System.loadLibrary("avcodec");
System.loadLibrary("avformat");
System.loadLibrary("postproc");
System.loadLibrary("avfilter");
System.loadLibrary("avdevice");
System.loadLibrary("sync-player");
} catch (Exception e) {
e.printStackTrace();
}
}
public AudioTrack createAudioTrack(int sampleRateInHz, int nb_channels){
//音频码流编码格式
int encodingFormat = AudioFormat.ENCODING_PCM_16BIT;
//声道布局
int channelConfig;
if(nb_channels == 1){
channelConfig = android.media.AudioFormat.CHANNEL_OUT_MONO;
} else {
channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO;
}
AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, encodingFormat);
int sessionId = mAudioManager.generateAudioSessionId();
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFormat audioFormat = new AudioFormat.Builder().setSampleRate(sampleRateInHz)
.setEncoding(encodingFormat)
.setChannelMask(channelConfig)
.build();
AudioTrack mAudioTrack = new AudioTrack(audioAttributes, audioFormat, bufferSize + 2048, AudioTrack.MODE_STREAM, sessionId);
return mAudioTrack;
}
}
首先是Java层入口类SyncPlayer,和以前的学习例子有点区别,准备四个native方法,分别是nativeInit、nativePrepare、nativePlay和nativeRelease,而且全部声明为private私有的,不对外提供调用。
然后构造函数传入Context,以便createAudioTrack方法中使用新API创建AudioTrack对象,顺带nativeInit。setMediaSource / setRender两个方法只是为nativePrepare作准备,准备之后就可以nativePlay播放了。之后要记住回收资源nativeRelease。
下一步就实现nativeInit / nativeRelease / nativePrepare 三个方法,nativePlay的部分代码。这四个方法其实就是对以前的例子进行一些规范的封装,要注意一点就是对应资源的回收,NDK开发一定一定要做好内存资源的回收,要不然就会造成内存泄漏。
我们新建sync_player.c文件,从 nativeInit 开始。
typedef struct _SyncPlayer {
// SyncPlayer的java对象,需要建立引用 NewGlobalRef,记得DeleteGlobalRef
jobject jinstance;
... ...
} SyncPlayer;
SyncPlayer* mSyncPlayer; // 全局变量,可以理解为c层的SyncPlayer对象
JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativeInit(JNIEnv *env, jobject instance)
{
av_log_set_callback(ffmpeg_custom_log);
av_register_all();
avcodec_register_all();
avformat_network_init();
// malloc与calloc区别
// 1.malloc是以字节为单位,calloc是以item为单位。
// 2.malloc需要memset初始化为0,calloc默认初始化为0
mSyncPlayer = (SyncPlayer*)calloc(1, sizeof(SyncPlayer));
// SyncPlayer的java对象,需要建立引用 NewGlobalRef,记得DeleteGlobalRef
mSyncPlayer->jinstance = (*env)->NewGlobalRef(env, instance);
}
JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativeRelease(JNIEnv *env, jobject instance)
{
// 释放 SyncPlayer
(*env)->DeleteGlobalRef(env, mSyncPlayer->jinstance);
free(mSyncPlayer);
mSyncPlayer = NULL; //防止野指针
}
这里的SyncPlayer是一个结构体,因为不是写cpp工程,所以我们用struct结构体来代替class类对象。我们暂且不清楚结构体需要什么变量,先保存一个SyncPlayer的全局引用吧。 此时我们就可以立刻在nativeRelease当中删除这个全局引用了,释放结构体内存。
JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativePrepare(JNIEnv *env, jobject instance,
jstring media_input_jstr, jobject jSurface)
{
if(mSyncPlayer == NULL) {
LOGE("%s","请调用函数:nativeInit");
return;
}
const char *media_input_cstr = (*env)->GetStringUTFChars(env, media_input_jstr, 0);
AVFormatContext *pFormatContext = avformat_alloc_context();
// 打开输入视频文件
if(avformat_open_input(&pFormatContext, media_input_cstr, NULL, NULL) != 0){
LOGE("%s","打开输入视频文件失败");
return;
}
// 获取视频信息
if(avformat_find_stream_info(pFormatContext,NULL) < 0){
LOGE("%s","获取视频信息失败");
return;
}
int video_stream_idx = -1;
int audio_stream_idx = -1;
for(int i=0; inb_streams; i++)
{
enum AVMediaType meida_type = pFormatContext->streams[i]->codecpar->codec_type;
switch(meida_type)
{
case AVMEDIA_TYPE_VIDEO:
video_stream_idx = i;
break;
case AVMEDIA_TYPE_AUDIO:
audio_stream_idx = i;
break;
default:
continue;
}
}
LOGD("VIDEO的索引位置:%d", video_stream_idx);
LOGD("AUDIO的索引位置:%d", audio_stream_idx);
mSyncPlayer->input_format_ctx = pFormatContext;
mSyncPlayer->num_streams = pFormatContext->nb_streams;
mSyncPlayer->audio_stream_index = audio_stream_idx;
mSyncPlayer->video_stream_index = video_stream_idx;
// 开辟nb_streams个空间,每个都是指针 (AVCodecContext* )
mSyncPlayer->input_codec_ctx = calloc(pFormatContext->nb_streams, sizeof(AVCodecContext* ) );
// 根据索引初始化对应的AVCodecContext,并放入mSyncPlayer.input_codec_ctx数组 对应的位置
int ret ;
ret = alloc_codec_context(mSyncPlayer, video_stream_idx);
if(ret < 0) return;
ret = alloc_codec_context(mSyncPlayer, audio_stream_idx);
if(ret < 0) return;
// 初始化视频渲染相关 ANativeWindow是NDK对象,不需要NewGlobalRef
mSyncPlayer->native_window = ANativeWindow_fromSurface(env, jSurface);
// 初始化音频播放相关 audio_track是java对象,需要NewGlobalRef,记得DeleteGlobalRef
ret = initAudioTrack(mSyncPlayer, env);
if(ret < 0) return;
(*env)->ReleaseStringUTFChars(env, media_input_jstr, media_input_cstr);
}
紧接着我们就可以开始准备工作,进入nativePrepare方法,打开资源文件上下文,检索音视频索引这些都是模板代码了。按着流程走下来,我们就是根据索引值打开对应的解码器,获取解码上下文指针(AVCodecContext*),这样的解码上下文指针起码有两个或者以上,所以我们在SyncPlayer结构体创建一个AVCodecContext* 的数组,即一个AVCodecContext的二级指针。并根据AVFormatContext->nb_streams的流通道数,sizeof(AVCodecContext*)解码上下文指针为单元,创建内存空间。这是因为往后的(字幕)扩展预留的。
// 根据 stream_idx流索引,初始化对应的AVCodecContext,并保存到SyncPlayer
int alloc_codec_context(SyncPlayer *player,int stream_idx)
{
AVFormatContext *pFormatContext = player->input_format_ctx;
AVCodec *pCodec = avcodec_find_decoder(pFormatContext->streams[stream_idx]->codecpar->codec_id);
if(pCodec == NULL){
LOGE("无法获取 %d 的解码器",stream_idx);
return -1;
}
AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
if(pCodecContext == NULL) {
LOGE("创建 %d 解码器对应的上下文失败.", stream_idx);
return -2;
}
int ret = avcodec_parameters_to_context(pCodecContext, pFormatContext->streams[stream_idx]->codecpar);
if(ret < 0) {
LOGE("avcodec_parameters_to_context:%d\n", AVERROR(ret));
return -3;
}
if(avcodec_open2(pCodecContext, pCodec, NULL) < 0){
LOGE("%s","解码器无法打开");
return -4;
}
player->input_codec_ctx[stream_idx] = pCodecContext;
return 0;
}
// 初始化音频相关的变量
int initAudioTrack(SyncPlayer* player, JNIEnv* env)
{
AVCodecContext *audio_codec_ctx = player->input_codec_ctx[player->audio_stream_index];
//重采样设置参数-------------start
enum AVSampleFormat in_sample_fmt = audio_codec_ctx->sample_fmt;
enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
int in_sample_rate = audio_codec_ctx->sample_rate;
int out_sample_rate = in_sample_rate;
uint64_t in_ch_layout = audio_codec_ctx->channel_layout;
uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;
//16bit 44100 PCM 统一音频采样格式与采样率
SwrContext *swr_ctx = swr_alloc();
swr_alloc_set_opts(swr_ctx,
out_ch_layout,out_sample_fmt,out_sample_rate,
in_ch_layout,in_sample_fmt,in_sample_rate,
0, NULL);
int ret = swr_init(swr_ctx);
if(ret < 0) {
LOGE("swr_init:%d\n", AVERROR(ret));
return -1;
}
int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);
//重采样设置参数-------------end
// 保存设置
player->in_sample_fmt = in_sample_fmt;
player->out_sample_fmt = out_sample_fmt;
player->in_sample_rate = in_sample_rate;
player->out_sample_rate = out_sample_rate;
player->out_channel_nb = out_channel_nb;
player->swr_ctx = swr_ctx;
//JNI AudioTrack-------------start
jobject jthiz = player->jinstance;
jclass player_class = (*env)->GetObjectClass(env, jthiz);
//AudioTrack对象
jmethodID create_audio_track_mid = (*env)->GetMethodID(env,player_class,"createAudioTrack","(II)Landroid/media/AudioTrack;");
jobject audio_track = (*env)->CallObjectMethod(env,jthiz,create_audio_track_mid,player->out_sample_rate,player->out_channel_nb);
//调用AudioTrack.play方法
jclass audio_track_class = (*env)->GetObjectClass(env,audio_track);
jmethodID audio_track_play_mid = (*env)->GetMethodID(env,audio_track_class,"play","()V");
player->audio_track_play_mid = audio_track_play_mid;
//AudioTrack.write
jmethodID audio_track_write_mid = (*env)->GetMethodID(env,audio_track_class,"write","([BII)I");
player->audio_track_write_mid = audio_track_write_mid;
//JNI AudioTrack-------------end
player->audio_track = (*env)->NewGlobalRef(env,audio_track);
return 0;
}
随后我们进入自定义函数alloc_codec_context,根据 stream_idx流索引,初始化对应的AVCodecContext,并保存到SyncPlayer。紧接着就是初始化初始化音视频相关的变量属性,都是模板代码了,不了解的同学可以查看之前的知识文章。
经过nativePrepare方法,我们已经成功获取音视频解码上下文,音频播放对象AudioTrack,视频渲染对象ANativeWindow,都分别保存到SyncPlayer结构体当中,持有其指针对象。此时的SyncPlayer应该有如下属性:
typedef struct _SyncPlayer {
// 数据源 格式上下文
AVFormatContext *input_format_ctx;
// 流的总个数
int num_streams;
// 频视频流索引位置
int video_stream_index;
// 音视频流索引位置
int audio_stream_index;
// AVCodecContext 二级指针 动态数组
// 长度为streams_num
AVCodecContext * * input_codec_ctx;
// SyncPlayer的java对象,需要建立引用 NewGlobalRef,记得DeleteGlobalRef
jobject jinstance;
// 视频渲染相关
ANativeWindow* native_window;
SwrContext *swr_ctx;
// 音频播放相关
enum AVSampleFormat in_sample_fmt; //输入的采样格式
enum AVSampleFormat out_sample_fmt; //输出采样格式16bit PCM
int in_sample_rate; //输入采样率
int out_sample_rate; //输出采样率
int out_channel_nb; //输出的声道个数
// 音频播放对象 java对象,需要 NewGlobalRef,记得DeleteGlobalRef
jobject* audio_track;
jmethodID audio_track_play_mid;
jmethodID audio_track_write_mid;
// ... ...
} SyncPlayer;
nativeRelease方法也不能放松,各种内存空间的回收要时刻牢记:
JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativeRelease(JNIEnv *env, jobject instance)
{
if(mSyncPlayer == NULL)
return;
if(mSyncPlayer->input_format_ctx == NULL){
return;
}
// 释放音频相关
(*env)->DeleteGlobalRef(env, mSyncPlayer->audio_track);
swr_free(&(mSyncPlayer->swr_ctx));
// 释放解码器
for(int i=0; inum_streams; i++) {
// 有可能出现为空,因为只保存了音视频的AVCodecContext,没有处理字幕流的
// 但是空间还是按照num_streams的个数创建了
AVCodecContext * pCodecContext = mSyncPlayer->input_codec_ctx[i];
if(pCodecContext != NULL)
{
avcodec_close(pCodecContext);
avcodec_free_context(&pCodecContext);
pCodecContext = NULL; //防止野指针
}
}
free(mSyncPlayer->input_codec_ctx);
// 释放文件格式上下文
avformat_close_input(&(mSyncPlayer->input_format_ctx));
avformat_free_context(mSyncPlayer->input_format_ctx);
mSyncPlayer->input_format_ctx = NULL;
// 释放 SyncPlayer
(*env)->DeleteGlobalRef(env, mSyncPlayer->jinstance);
free(mSyncPlayer);
mSyncPlayer = NULL;
}
由于篇幅关系,文章先到这里结束。四个native方法,剩下最关键的nativePlay还没实现,可以思考nativePlay方法要怎么实现。无非就一个线程采集AVPacket保存到一个缓冲区,一个线程获取视频AVPakcet并渲染到ANativeWindow,另外一线程获取音频AVPacket并进行播放。 那么采集的AVPacket怎么合适的存放到一个缓冲区?怎么高效的从av_read_frame的avpacket 保存 到缓冲区?这些问题留待下一文章解答。