音视频:12.FFmpeg-音乐播放器1

具体代码请看:NDKPractice项目的ffmpeg83

先来看下流程图:

1.获取音频 Meta 信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
extern "C" JNIEXPORT void JNICALL
Java_com_east_ffmpeg83_media_JaminPlayer_nPlay(JNIEnv *env, jobject instance,
jstring url_) {
const char *url = env->GetStringUTFChars(url_,0);
// 1.初始化所有组件,只有调用了该函数,才能使用复用器和编解码器(源码)
av_register_all();
// 2.初始化网络
avformat_network_init();

AVFormatContext *pFormatContext = NULL;
int formatOpenInputRes = -1;
int formatFindStreamInfoRes = -1;
int audioStreamIndex = -1;
AVStream *audio_stream;

// 3.打开输入
formatOpenInputRes = avformat_open_input(&pFormatContext,url,NULL,NULL);
if(formatOpenInputRes !=0 ){
// 第一件事,需要回调给 Java层
// 第二件 事,需要释放资源
LOGE("format open input error:%s",av_err2str(formatOpenInputRes));
// 一般不推荐这么写,但是的确很方便。
goto __av_resources_destroy;
}

// 4.找出输入流的信息
formatFindStreamInfoRes = avformat_find_stream_info(pFormatContext,NULL);
if(formatFindStreamInfoRes < 0){
LOGE("format find stream info error: %s",av_err2str(formatFindStreamInfoRes));
goto __av_resources_destroy;
}

// 5.查找音频流的 index
audioStreamIndex = av_find_best_stream(pFormatContext,AVMediaType::AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
if(audioStreamIndex < 0){
LOGE("format audio stream error");
goto __av_resources_destroy;
}
// 获取采样率和通道
audio_stream = pFormatContext->streams[audioStreamIndex];
LOGE("采样率:%d, 通道数: %d", audio_stream->codecpar->sample_rate, audio_stream->codecpar->channels);

__av_resources_destroy:
if(pFormatContext != NULL){
avformat_close_input(&pFormatContext);
avformat_free_context(pFormatContext);
pFormatContext = NULL;
}
avformat_network_deinit();

env->ReleaseStringUTFChars(url_,url);
}

2.解码音频数据

关于解码函数 avcodec_decode_audio4 已经过时了,取而代之的是 avcodec_send_packet 和 avcodec_receive_frame 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 6.查找解码
pCodecParameters = pFormatContext->streams[audioStreamIndex]->codecpar;
pCodec = avcodec_find_decoder(pCodecParameters->codec_id);
if(pCodec == NULL){
LOGE("codec find audio decoder error");
goto __av_resources_destroy;
}

// 7.创建一个解码器的上下文
pCodecContext = avcodec_alloc_context3(pCodec);
if(pCodecContext == NULL){
LOGE("codec alloc context error");
goto __av_resources_destroy;
}
// 8.根据参数值填充Codec上下文参数
codecParametersToContextRes = avcodec_parameters_to_context(pCodecContext,pCodecParameters);
if (codecParametersToContextRes < 0) {
LOGE("codec parameters to context error: %s", av_err2str(codecParametersToContextRes));
goto __av_resources_destroy;
}
// 9.打开解码器
codecOpenRes = avcodec_open2(pCodecContext,pCodec,NULL);
if (codecOpenRes != 0) {
LOGE("codec audio open error: %s", av_err2str(codecOpenRes));
goto __av_resources_destroy;
}

pPacket = av_packet_alloc();
pFrame = av_frame_alloc();
// 循环从上下文中读取帧到包中
while(av_read_frame(pFormatContext,pPacket) >= 0){
if(pPacket->stream_index == audioStreamIndex){
// Packet 包,压缩的数据,解码成 pcm 数据
int codecSendPacketRes = avcodec_send_packet(pCodecContext,pPacket);
if(codecSendPacketRes == 0){
int codecReceiveFrameRes = avcodec_receive_frame(pCodecContext,pFrame);
if(codecReceiveFrameRes == 0){
// AVPacket -> AVFrame
index++;
LOGE("解码第 %d 帧",index);
}
}
}
// 解引用
av_packet_unref(pPacket);
av_frame_unref(pFrame);
}

// 1.解引用数据 data, 2.销魂 pPacket 结构体内存, 3.pPacket = NULL;
av_packet_free(&pPacket);
av_frame_free(&pFrame);

__av_resources_destroy:
if(pCodecContext != NULL){
avcodec_close(pCodecContext);
avcodec_free_context(&pCodecContext);
pCodecContext = NULL;
}

if(pFormatContext != NULL){
avformat_close_input(&pFormatContext);
avformat_free_context(pFormatContext);
pFormatContext = NULL;
}
avformat_network_deinit();

env->ReleaseStringUTFChars(url_,url);

3.播放音频

播放 pcm 数据目前比较流行的有两种方式,一种是通过 Android 的 AudioTrack 来播放,另一种是采用跨平台的 OpenSLES(直接用的底层操作驱动播放) 来播放,
个人比较倾向于用更加高效的 OpenSLES 来播放音频,大家可以先看看 Google 官方的 native-audio 事例,这里我们先采用 AudioTrack 来播放

AudioTrack 来播:

  1. 创建 AudioTack 对象
  2. 启动循环,设置为播放状态 (play)
  3. 把数据推到指定数组中 (write)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
jobject createAudioTrack(JNIEnv *env){
/*AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes, int mode)*/
jclass jAudioTrackClass = env->FindClass("android/media/AudioTrack");
jmethodID jAudioTackCMid = env->GetMethodID(jAudioTrackClass,"<init>","(IIIIII)V");

int streamType = 3;
int sampleRateInHz = AUDIO_SAMPLE_RATE;
int channelConfig = (0x4 | 0x8);
int audioFormat = 2;
int mode = 1;

// int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
jmethodID getMinBufferSizeMID = env->GetStaticMethodID(jAudioTrackClass,"getMinBufferSize","(III)I");
int bufferSizeInBytes = env->CallStaticIntMethod(jAudioTrackClass,getMinBufferSizeMID,sampleRateInHz,
channelConfig,audioFormat);
LOGE("bufferSizeInBytes = %d",bufferSizeInBytes);

jobject jAudioTrackObj = env->NewObject(jAudioTrackClass,jAudioTackCMid,streamType,sampleRateInHz,channelConfig,
audioFormat,bufferSizeInBytes,mode);

// play
jmethodID playMid = env->GetMethodID(jAudioTrackClass,"play","()V");
env->CallVoidMethod(jAudioTrackObj,playMid);

return jAudioTrackObj;
}

{
// 10.使用AudioTrack播放
jAudioTrackClass = env->FindClass("android/media/AudioTrack");
// public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)
jWriteMid = env->GetMethodID(jAudioTrackClass,"write","([BII)I");
jAudioTrackObj = createAudioTrack(env);

pPacket = av_packet_alloc();
pFrame = av_frame_alloc();
// 循环从上下文中读取帧到包中
while(av_read_frame(pFormatContext,pPacket) >= 0){
if(pPacket->stream_index == audioStreamIndex){
// Packet 包,压缩的数据,解码成 pcm 数据
int codecSendPacketRes = avcodec_send_packet(pCodecContext,pPacket);
if(codecSendPacketRes == 0){
int codecReceiveFrameRes = avcodec_receive_frame(pCodecContext,pFrame);
if(codecReceiveFrameRes == 0){
// AVPacket -> AVFrame
index++;
LOGE("解码第 %d 帧",index);

// write 写到缓冲区 pFrame.data -> javabyte
// size 是多大,装 pcm 的数据
// 1s 44100 点,2通道, 2字节 44100*2*2
// 1帧不是一秒,pFrame->nb_samples点
int dataSize = av_samples_get_buffer_size(NULL,pFrame->channels,
pFrame->nb_samples,pCodecContext->sample_fmt,0);
jbyteArray jPcmByteArray = env->NewByteArray(dataSize);
// native 创建 c 数组
jbyte *jPcmData= env->GetByteArrayElements(jPcmByteArray,NULL);
memcpy(jPcmData,pFrame->data,dataSize);
// 0 把 c 的数组的数据同步到 jbyteArray,然后释放native数组
env->ReleaseByteArrayElements(jPcmByteArray,jPcmData,0);
env->CallIntMethod(jAudioTrackObj,jWriteMid,jPcmByteArray,0,dataSize);
// 解除 jPcmDataArray 的持有,让 javaGC 回收
env->DeleteLocalRef(jPcmByteArray);
}
}
}
// 解引用
av_packet_unref(pPacket);
av_frame_unref(pFrame);
}

// 1.解引用数据 data, 2.销魂 pPacket 结构体内存, 3.pPacket = NULL;
av_packet_free(&pPacket);
av_frame_free(&pFrame);
env->DeleteLocalRef(jAudioTrackObj);

__av_resources_destroy:
if(pCodecContext != NULL){
avcodec_close(pCodecContext);
avcodec_free_context(&pCodecContext);
pCodecContext = NULL;
}

if(pFormatContext != NULL){
avformat_close_input(&pFormatContext);
avformat_free_context(pFormatContext);
pFormatContext = NULL;
}
avformat_network_deinit();

env->ReleaseStringUTFChars(url_,url);
}

这块播放还是有杂音,而且内存持续上涨,下节我们通过多线程播放来解决

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%