ffmpeg & c语言 & udp & 音频流传输播放
最近由于更新的嵌入式项目催的紧,竟被安排用C语言开发音频推流播放音频——靠谱!!!(非C人员,勿喷,拜谢!)
由于是非专业人士,直接贴代码:恭请批览!
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/avstring.h>
#include <libswresample/swresample.h>
#include <alsa/asoundlib.h>
#include <stdio.h>
#include <stdbool.h> //用于布尔类型 bool
int main(int argc, char *argv[]){
const char *input_url = "udp://127.0.0.1:1030"; // 替换为实际的 UDP 地址和端口
AVFormatContext *format_ctx = NULL; // 声明了一个指向 AVFormatContext 结构体的指针 format_ctx,并将其初始化为 NULL。
AVCodecContext *codec_ctx = NULL; // 声明了一个指向 AVCodecContext 结构体的指针 codec_ctx,并将其初始化为 NULL。
AVCodec *codec = NULL; // 声明了一个指向 AVCodec 结构体的指针 codec,并将其初始化为 NULL。
AVPacket packet; // 声明了一个 AVPacket 结构体变量 packet。
AVFrame *frame = NULL; // 声明了一个指向 AVFrame 结构体的指针 frame,并将其初始化为 NULL。
int audio_stream_index = -1; // 声明了一个整数变量 audio_stream_index,并将其初始化为 -1。
int ret; // 声明了一个整数变量 ret。
printf("完成声明\n");
// 初始化 FFmpeg 库,新版本不用再执行这一条
// av_register_all();
avformat_network_init(); // 函数来初始化网络功能,这对于处理网络流(如 UDP 流)
printf("FFmpeg 网络初始化成功\n");
//====================
// 设置输入格式
//===
avformat_free_context(format_ctx); // 释放之前分配的 AVFormatContext
format_ctx = avformat_alloc_context();
if (!format_ctx)
{
fprintf(stderr, "无法分配 AVFormatContext\n");
return -1;
}
// 设置缓冲区大小
AVDictionary *options = NULL;
av_dict_set(&options, "buffer_size", "65536", 0); // 设置缓冲区大小为 64KB
//================打开流,使的格式 ---star =========================
// 尝试自动检测格式打开输入流
ret = avformat_open_input(&format_ctx, input_url, NULL, &options);
av_dict_free(&options); // 释放字典
if (ret < 0){ // 如果没有成功打开流,则尝试使用 G.722 格式打开输入流
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf));
fprintf(stderr, "无法自动检测格式打开输入流: %s\n", av_err2str(ret));
// 尝试使用 G.722 格式打开输入流
AVInputFormat *ifmt = av_find_input_format("g722"); // 查找 G.722 格式 (G.722音频编码格式)
if (ifmt){
avformat_free_context(format_ctx);
format_ctx = avformat_alloc_context();
if (!format_ctx){
fprintf(stderr, "无法分配 AVFormatContext\n");
return -1;
}
ret = avformat_open_input(&format_ctx, input_url, ifmt, NULL);
if (ret < 0){
fprintf(stderr, "无法使用 G.722 格式打开输入流: %s\n", av_err2str(ret));
avformat_free_context(format_ctx);
return -1;
}else{
printf("成功使用 G.722 格式打开输入流\n");
}
}
else{
fprintf(stderr, "找不到 G.722 格式\n");
return -1;
}
}else{
printf("成功使用自动检测格式打开输入流\n");
}
//================打开流,使的格式 ---end ========================
// 查找流信息
ret = avformat_find_stream_info(format_ctx, NULL);
if (ret < 0)
{
fprintf(stderr, "无法查找流信息: %s\n", av_err2str(ret));
avformat_close_input(&format_ctx);
return -1;
}
// 找到音频流,
for (int i = 0; i < format_ctx->nb_streams; i++)
{ // format_ctx
// format_ctx->nb_streams 是流的数量,format_ctx->streams[i]->codecpar->codec_type 是流的类型。如果找到音频流(codec_type 为 AVMEDIA_TYPE_AUDIO)
if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
audio_stream_index = i; // 将其索引存储在 audio_stream_index 中
break; // 跳循环出
}
}
// 码检查是否找到了音频流
// audio_stream_index 仍然是 -1,表示没有找到音频流,程序会打印错误信息,关闭输入文件,并返回 -1,表示程序执行失败
if (audio_stream_index == -1)
{
fprintf(stderr, "找不到音频流\n");
avformat_close_input(&format_ctx);
return -1;
}
// 打印 codec_id
// printf("音频流的 codec_id: %d\n", format_ctx->streams[audio_stream_index]->codecpar->codec_id);
// printf("进入2-4!\n");
// 查找音频流对应的解码器。avcodec_find_decoder 函数根据音频流的 codec_id 查找解码器。
// 如果找不到解码器,程序会打印错误信息,关闭输入文件,并返回 -1
codec = avcodec_find_decoder(format_ctx->streams[audio_stream_index]->codecpar->codec_id); // 根据输入流的 codec_id 自动选择合适的解码器
if (!codec){
fprintf(stderr, "找不到编解码器\n");
avformat_close_input(&format_ctx);
return -1;
}
printf("进入2-5!\n");
printf("找到的解码器: %s\n", codec->name);
// 为解码器分配一个上下文。avcodec_alloc_context3 函数根据找到的解码器分配一个 AVCodecContext 结构体。
// 如果分配失败,程序会打印错误信息,关闭输入文件,并返回 -1。
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx){
fprintf(stderr, "无法分配编解码器上下文1\n");
avformat_close_input(&format_ctx);
return -1;
}
// 将音频流的编解码参数复制到解码器上下文中。avcodec_parameters_to_context 函数将 codecpar 中的参数复制到 codec_ctx 中。
// 如果复制失败,程序会打印错误信息,释放解码器上下文,关闭输入文件,并返回 -1。
if ((ret = avcodec_parameters_to_context(codec_ctx, format_ctx->streams[audio_stream_index]->codecpar)) < 0){
fprintf(stderr, "无法复制编解码器参数: %s\n", av_err2str(ret));
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
printf("解码器参数设置成功\n");
// 打开解码器。avcodec_open2 函数初始化解码器上下文并打开解码器。
// 如果打开失败,程序会打印错误信息,释放解码器上下文,关闭输入文件,并返回 -1。
if ((ret = avcodec_open2(codec_ctx, codec, NULL)) < 0){
fprintf(stderr, "无法打开编解码器c: %s\n", av_err2str(ret));
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 分配一个 AVFrame 结构体,用于存储解码后的帧数据。av_frame_alloc 函数分配内存并初始化 AVFrame 结构体。
// 如果分配失败,程序会打印错误信息,释放解码器上下文,关闭输入文件,并返回 -1。
frame = av_frame_alloc();
if (!frame){
fprintf(stderr, "无法分配帧\n");
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
//===== 初始化 ALSA---star ============
snd_pcm_t *pcm_handle; // 声明一个指向 snd_pcm_t 结构体的指针 pcm_handle,用于表示 ALSA 的 PCM 设备句柄
snd_pcm_hw_params_t *hw_params; // 声明一个指向 snd_pcm_hw_params_t 结构体的指针 hw_params,用于表示硬件参数
unsigned int sample_rate = 16000; // 设置为 16000
int channels = 1; // 设置为 1
snd_pcm_uframes_t frames = 1024; // 设置每个周期的帧数为 1024
if ((ret = snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0)) < 0){ // 打开默认的音频设备进行播放。如果打开失败,返回负值;pcm_handle:PCM 设备句柄;"default":使用默认的音频设备;SND_PCM_STREAM_PLAYBACK:设置为播放模式;0:默认模式
fprintf(stderr, "无法打开音频设备: %s\n", snd_strerror(ret)); // 打印错误信息
av_frame_free(&frame); // 释放解码后的帧
avcodec_free_context(&codec_ctx); // 释放解码器上下文
avformat_close_input(&format_ctx); // 关闭输入文件并释放格式上下文
return -1; // 返回 -1,表示程序执行失败
}
snd_pcm_hw_params_alloca(&hw_params); // 分配硬件参数结构体
snd_pcm_hw_params_any(pcm_handle, hw_params); // 初始化硬件参数结构体
snd_pcm_hw_params_set_access(pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); // 设置访问类型为交错模式(即多通道数据交错存储
snd_pcm_hw_params_set_format(pcm_handle, hw_params, SND_PCM_FORMAT_S16_LE); // 设置音频数据格式为 16 位小端(LE)
snd_pcm_hw_params_set_channels(pcm_handle, hw_params, channels); // 设置音频通道数
snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params, &sample_rate, NULL); // 设置采样率,sample_rate 可能会被调整为最接近的支持值
snd_pcm_hw_params_set_period_size_near(pcm_handle, hw_params, &frames, NULL); // 设置每个周期的帧数,frames 可能会被调整为最接近的支持值
if ((ret = snd_pcm_hw_params(pcm_handle, hw_params)) < 0){ // 设置硬件参数。如果返回值小于 0,表示设置失败
fprintf(stderr, "无法设置硬件参数: %s\n", snd_strerror(ret)); // 打印错误信息
snd_pcm_close(pcm_handle); // 关闭音频设备
av_frame_free(&frame); // 释放解码后的帧
avcodec_free_context(&codec_ctx); // 释放解码器上下文
avformat_close_input(&format_ctx); // 关闭输入文件并释放格式上下文
return -1; // 返回 -1,表示程序执行失败
}
//=========== 初始化 ALSA---end ===================
//======= 初始化音频重采样上下文-- star========
struct SwrContext *swr_ctx = swr_alloc(); // 分配重采样上下文, swr_alloc 函数用于分配并初始化一个 SwrContext 结构体,该结构体用于音频重采样。SwrContext 是 FFmpeg 中用于音频重采样的上下文结构体
if (!swr_ctx){ // 检查 swr_ctx 是否为 NULL
fprintf(stderr, "无法分配重采样上下文\n"); // 打印错误信息 “无法分配重采样上下文” 到标准错误输出(stderr)
snd_pcm_close(pcm_handle); // snd_pcm_close 函数用于关闭一个已经打开的 PCM 设备,并释放与该设备相关的所有资源。pcm_handle 是一个指向 snd_pcm_t 结构体的指针,表示要关闭的 PCM 设备句柄
av_frame_free(&frame); // av_frame_free 函数用于释放 AVFrame 结构体及其内部数据,并将指针置为 NULL。frame 是一个指向 AVFrame 结构体的指针
avcodec_free_context(&codec_ctx); // avcodec_free_context 函数用于释放 AVCodecContext 结构体及其内部数据,并将指针置为 NULL。codec_ctx 是一个指向 AVCodecContext 结构体的指针
avformat_close_input(&format_ctx); // avformat_close_input 函数用于关闭文件并释放 AVFormatContext 结构体及其内部数据。format_ctx 是一个指向 AVFormatContext 结构体的指针
return -1;
}
av_opt_set_int(swr_ctx, "in_channel_layout", codec_ctx->channel_layout, 0); // 设置输入音频的通道布局 ---swr_ctx:指向 SwrContext 结构体的指针;"in_channel_layout":选项名称,表示输入音频的通道布局;codec_ctx->channel_layout:输入音频的通道布局,取自解码器上下文 codec_ctx;0:标志参数,通常设置为 0 表示没有特殊标志。
av_opt_set_int(swr_ctx, "in_sample_rate", codec_ctx->sample_rate, 0); // 设置输入音频的采样率--- swr_ctx:指向 SwrContext 结构体的指针;"in_sample_rate":选项名称,表示输入音频的采样率;codec_ctx->sample_rate:输入音频的采样率,取自解码器上下文 codec_ctx;0:标志参数,通常设置为 0 表示没有特殊标志
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", codec_ctx->sample_fmt, 0); // 设置输入音频的采样格式---swr_ctx:指向 SwrContext 结构体的指针;"in_sample_fmt":选项名称,表示输入音频的采样格式;codec_ctx->sample_fmt:输入音频的采样格式,取自解码器上下文 codec_ctx;0:标志参数,通常设置为 0 表示没有特殊标志
av_opt_set_int(swr_ctx, "out_channel_layout", AV_CH_LAYOUT_MONO, 0); // 设置输出音频的通道布局---swr_ctx:指向 SwrContext 结构体的指针;"out_channel_layout":选项名称,表示输出音频的通道布局;AV_CH_LAYOUT_MONO:输出音频的通道布局,设置为单声道(MONO);0:标志参数,通常设置为 0 表示没有特殊标志
av_opt_set_int(swr_ctx, "out_sample_rate", sample_rate, 0); // 设置输出音频的采样率--- swr_ctx:指向 SwrContext 结构体的指针;"out_sample_rate":选项名称,表示输出音频的采样率;输出音频的采样率,通常是用户定义的值;0:标志参数,通常设置为 0 表示没有特殊标志;
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0); // 设置输出音频的采样格式---swr_ctx:指向 SwrContext 结构体的指针;"out_sample_fmt":选项名称,表示输出音频的采样格式;AV_SAMPLE_FMT_S16:输出音频的采样格式,设置为 16 位整数格式;0:标志参数,通常设置为 0 表示没有特殊标志
if ((ret = swr_init(swr_ctx)) < 0){
fprintf(stderr, "无法初始化重采样上下文: %s\n", av_err2str(ret));
//释放资源
swr_free(&swr_ctx);
snd_pcm_close(pcm_handle);
av_frame_free(&frame);
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
uint8_t *resampled_data = NULL;
int resampled_data_size = 0;
//======= 初始化音频重采样上下文-- end========
// 读取数据包,av_read_frame 函数从 format_ctx(格式上下文)中读取一个数据包,并将其存储在 packet 变量中
while (av_read_frame(format_ctx, &packet) >= 0){ // 成功
if (packet.stream_index == audio_stream_index){ // 检查读取的数据包是否属于音频流, packet.stream_index 是数据包所属的流索引,audio_stream_index 是音频流的索引
// printf("读取到音频数据包\n");
ret = avcodec_send_packet(codec_ctx, &packet); // 将数据包发送到解码器上下文 codec_ctx。avcodec_send_packet
if (ret < 0){ // 小于0表示发送失败
fprintf(stderr, "发送数据包时出错: %s\n", av_err2str(ret));
continue; // 继续处理下一个数据包
}
while (ret >= 0){ // 成功发送数据包后
// printf("成功发送数据包\n");
ret = avcodec_receive_frame(codec_ctx, frame); // 尝试从解码器中接收解码后的帧
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){ // avcodec_receive_frame 函数返回值为 AVERROR(EAGAIN) 表示需要更多数据包才能解码,AVERROR_EOF 表示解码结束
// printf("解码结束\n");
break;
}else if (ret < 0){
fprintf(stderr, "接收帧时出错: %s\n", av_err2str(ret));
break; // 继续处理下一个数据包
}
// 打印解码后的音频数据
// printf("解码后的音频数据大小: %d\n", frame->linesize[0]);
// print_audio_data(frame->data[0], frame->linesize[0]);//调用打印传输的数据方法前面定义
// ======重采样--star=======
// 计算重采样后的输出样本数,swr_get_delay(swr_ctx, codec_ctx->sample_rate):获取重采样器的延迟,以输入采样率为单位
int out_samples = av_rescale_rnd(swr_get_delay(swr_ctx, codec_ctx->sample_rate) + frame->nb_samples, sample_rate, codec_ctx->sample_rate, AV_ROUND_UP);
if (resampled_data_size < out_samples * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) * channels){ // 检查并分配足够的内存以存储重采样后的数据 -- out_samples * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) * channels:计算重采样后数据所需的字节数
av_free(resampled_data); // 释放之前分配的内存
resampled_data_size = out_samples * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) * channels; // 更新 resampled_data_size 为新的大小
resampled_data = av_malloc(resampled_data_size); // 分配新的内存
}
// 执行重采样 swr_ctx:重采样上下文; &resampled_data:指向输出缓冲区的指针;out_samples:输出缓冲区的大小,以样本为单位;(const uint8_t **)frame->data:输入数据;frame->nb_samples:输入样本数
int samples_converted = swr_convert(swr_ctx, &resampled_data, out_samples, (const uint8_t **)frame->data, frame->nb_samples);
if (samples_converted < 0){ // samples_converted 表示转换后的样本数。如果小于 0,表示出错
fprintf(stderr, "重采样时出错: %s\n", av_err2str(samples_converted));
continue; // 继续处理下一个数据包
}
// 算重采样后数据的大小;samples_converted:转换后的样本数;av_get_bytes_per_sample(AV_SAMPLE_FMT_S16):每个样本的字节数;channels:通道数;
// samples_converted * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) * channels:计算重采样后数据的总字节数。
int data_size = samples_converted * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) * channels;
if ((ret = snd_pcm_writei(pcm_handle, resampled_data, samples_converted)) < 0){ // 将重采样后的数据写入音频设备
fprintf(stderr, "写入音频数据时出错: %s\n", snd_strerror(ret)); // snd_pcm_prepare(pcm_handle) 函数重新准备音频设备,以便继续写入
snd_pcm_prepare(pcm_handle);
}
//=========重采样--end===========
}
}
av_packet_unref(&packet); // 在处理完数据包后,使用 av_packet_unref 函数释放数据包的内存
}
if (ret < 0 && ret != AVERROR_EOF){ // 读取帧时出错
fprintf(stderr, "读取帧时出错: %s\n", av_err2str(ret));
}
end: // 发生错误或处理完所有数据包后跳转到清理代码。前面的 goto end; 语句会跳转到这里
av_free(resampled_data);
swr_free(&swr_ctx);
av_frame_free(&frame); // 释放解码后的帧 frame。av_frame_free 函数会释放 AVFrame 结构体及其内部数据,并将指针置为 NULL。
avcodec_free_context(&codec_ctx); // 释放解码器上下文 codec_ctx。avcodec_free_context 函数会释放 AVCodecContext 结构体及其内部数据,并将指针置为 NULL
avformat_close_input(&format_ctx); // 闭输入文件并释放格式上下文 format_ctx。avformat_close_input 函数会关闭文件并释放 AVFormatContext 结构体及其内部数据。
avformat_network_deinit(); // 反初始化网络库,如果程序使用了网络流媒体功能(例如通过 HTTP 或 RTSP 协议读取媒体),需要调用 avformat_network_deinit 来清理网络库。
snd_pcm_close(pcm_handle); // 函数用于关闭一个已经打开的 PCM 设备,并释放与该设备相关的所有资源 -- pcm_handle:这是一个指向 snd_pcm_t 结构体的指针,表示要关闭的 PCM 设备句柄
return 0; // 表示程序正常结束,返回值为 0。在 C 语言中,return 0; 通常表示程序成功执行。
}
命令行推流
ffmpeg -re -i D:\music\六哲-朋友名义.mp3 -acodec mp3 -ar 16000 -ac 1 -f mp3 udp://192.168.8.2:1030
ffmpeg -re -i D:\music\六哲-朋友名义.wav -f s16le -acodec pcm_s16le -ar 16000 -ac 1 udp://192.168.8.2:1030
ffmpeg -re -i D:\music\六哲-朋友名义.722 -acodec g722 -ar 16000 -ac 1 -f g722 udp://192.168.8.2:1030