ffmpeg_c语言_udp_音频流传输播放

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值