FFmpeg系列(二)—— 音视频裸流转换:mp3转pcm、h264转YUV

1、总流程

  • 创建解析器、解码器、AVPacket和AVFrame
  • 打开文件,将mp3数据读入缓冲区
  • 解析mp3数据(在 main 函数中完成)
  • 解码,并将解码后的pcm数据写入文件(在 my_audio_decode 函数中完成

2、解析流程

mp3文件可能比较大,一次性读取会浪费比较多的内存,采用边读边解析办法。
如下图所示,红色表示buf缓冲区,首先从mp3文件里读取数据填满buf,然后开始解析,buf_index负责标记解析结束位置,buf_size负责记录buf里还剩多少数据未解析,如果buf_size小于4096,则将剩余未解析的数据移动到buf起始位置,然后再次从文件里读数据填满buf,同时buf_index也移动到buf起始位置,以此循环直到文件读取结束。
在这里插入图片描述

  • 代码实现如下
    buf_size = fread(buf, 1, AUDIO_BUF_SIZE, fl_in); // 读mp3文件
    while (buf_size > 0)
    {
        ret = av_parser_parse2(parser, ctx, &pkt->data, &pkt->size, buf_index, buf_size,
                               AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if (ret < 0) { exit(1); }  // parser失败,直接退出程序
        buf_index += ret;          // 跳过已经解析的数据
        buf_size  -= ret;          // 缓冲区还未解析的mp3数据大小

        if (pkt->size)
            my_audio_decode(ctx, pkt, frame, fl_out); // 解码并将解码后的pcm数据写入文件

        if (buf_size < 4096)  // 缓冲区未解析的音频数据小于4096,再从mp3文件中读取一点数据放入缓冲区
        {
            memmove(buf, buf_index, buf_size); // 把还未解析的数据移动到buf的起始位置
            buf_index = buf;                   // 当前解析结束位置也移动到buf起始位置
            len = fread(buf_index + buf_size, 1, AUDIO_BUF_SIZE - buf_size, fl_in);
            if (len > 0) buf_size += len;
        }
    }

3、解码流程

解码流程相对简单,这里单独用一个函数封装。解析完后直接从 AVPacket 里面将数据解码得到解码数据即可,然后写入文件。音频文件和视频文件的写法不同,注意做区分,代码实现如下:

// 解码
static void my_decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, FILE *fl_out, AVCodecID codec_id)
{
    int size;
    int ret = avcodec_send_packet(ctx, pkt);  // 发送packet到解码线程,不占用CPU资源
    if (ret < 0) { return; }                  // 需要注意 AVERROR(EAGAIN) 错误处理

    while (ret >= 0)
    {
        ret = avcodec_receive_frame(ctx, frame);         //从解码线程中获取解码接口,不占用CPU资源
        if (ret != 0) break;

        size = av_get_bytes_per_sample(ctx->sample_fmt); // 获取每个样本的字节数
        if (size < 0) { exit(1); }                       // 异常!大小计算失败

        // 将解码后的yuv或pcm数据写入文件
        if (codec_id == AV_CODEC_ID_H264) // h264视频
        {
            print_video_format(frame); // 打印视频基本参数
            for(int j=0; j<frame->height; j++)   // Y
                fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, fl_out);
            for(int j=0; j<frame->height/2; j++) // U
                fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, fl_out);
            for(int j=0; j<frame->height/2; j++) // V
                fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, fl_out);
        }
        else // mp3或aac音频
        {
            print_audio_format(frame); // 打印音频基本参数
            for (int i = 0; i < frame->nb_samples; i++)  // 采样率
            {
                for (int channel = 0; channel < ctx->channels; ++channel)   // 声道数
                {
                    fwrite(frame->data[channel] + size*i, 1, size, fl_out); // 将pcm数据写入文件
                }
            }
        }
    }
}

4、完整代码

以下代码再Qt5.14.0中编译测试OK,运行目录需要放置一个believe.mp3文件,程序执行完成后会在运行目录下生成一个believe.pcm文件,使用以下命令即可测试转换是否成功:
音频:ffplay -ar 48000 -ac 2 -f f32le believe.pcm
视频:ffplay -pixel_format yuv420p -video_size 766x322 -framerate 25 out.yuv


#ifdef __cplusplus
extern "C"
{
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavutil/frame.h>
#include <libavutil/mem.h>
#include <libavcodec/avcodec.h>
#ifdef __cplusplus
}
#endif

// 缓冲区大小
#define BUF_SIZE    20480

// 解码
static void my_decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, FILE *fl_out, AVCodecID codec_id)
{
    int size;
    int ret = avcodec_send_packet(ctx, pkt);  // 发送packet到解码线程,不占用CPU资源
    if (ret < 0) { return; }                  // 需要注意 AVERROR(EAGAIN) 错误处理

    while (ret >= 0)
    {
        ret = avcodec_receive_frame(ctx, frame);         //从解码线程中获取解码接口,不占用CPU资源
        if (ret != 0) break;

        size = av_get_bytes_per_sample(ctx->sample_fmt); // 获取每个样本的字节数
        if (size < 0) { exit(1); }                       // 异常!大小计算失败

        // 将解码后的yuv或pcm数据写入文件
        if (codec_id == AV_CODEC_ID_H264) // h264视频
        {
            for(int j=0; j<frame->height; j++)   // Y
                fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, fl_out);
            for(int j=0; j<frame->height/2; j++) // U
                fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, fl_out);
            for(int j=0; j<frame->height/2; j++) // V
                fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, fl_out);
        }
        else // mp3或aac音频
        {
            for (int i = 0; i < frame->nb_samples; i++)  // 采样率
            {
                for (int channel = 0; channel < ctx->channels; ++channel)   // 声道数
                {
                    fwrite(frame->data[channel] + size*i, 1, size, fl_out); // 将pcm数据写入文件
                }
            }
        }
    }
}

// YUV视频播放范例:ffplay -pixel_format yuv420p -video_size 766x322 -framerate 25 out.yuv
// pcm音频播放示例:ffplay -ar 48000 -ac 2 -f f32le believe.pcm
int main()
{
    const char           *outfilename    = "out.pcm";         // 要输出的yuv文件路径
    const char           *infilename     = "believe.mp3";     // h264文件路径,200kbps 766x322 10s
    FILE                 *fl_in          = NULL;
    FILE                 *fl_out         = NULL;
    const AVCodec        *codec          = NULL;              // 解码器
    AVCodecContext       *ctx            = NULL;              // 解码器上下文
    AVCodecParserContext *parser         = NULL;              // 解析器,解码前需要先解析
    AVPacket             *pkt            = av_packet_alloc(); // 接封装后的帧
    AVFrame              *frame          = av_frame_alloc();  // 解码后的帧
    uint8_t              buf[BUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];    // yuv帧数据缓冲区
    size_t               buf_size        = 0;                 // 缓冲区未解析的数据大小
    uint8_t              *buf_index      = buf;               // 当前解析位置,初始化为buf起始位置
    enum AVCodecID       codec_id        = AV_CODEC_ID_MP3;   // 解码器ID
    int                  len             = 0;                 // 从文件中读到的数据大小
    int                  ret             = 0;

    // ===== 根据文件名后缀判断是视频还是音频 =====
    if (strstr(infilename, "aac") != NULL){             // aac音频裸流
        codec_id = AV_CODEC_ID_AAC;
    } else if (strstr(infilename, "mp3") != NULL) {     // mp3音频裸流
        codec_id = AV_CODEC_ID_MP3;
    } else if (strstr(infilename, "h264") != NULL) {    // h264视频裸流
        codec_id = AV_CODEC_ID_H264;
    } else {
        return 1; // 不支持的格式
    }

    // ================== 1、初始化 ==================
    if (!frame || !pkt){ exit(1); }         // 帧空间分配失败

    codec = avcodec_find_decoder(codec_id); // 查找解码器
    if (!codec)     { exit(1); }

    parser = av_parser_init(codec->id);     // 获取裸流的解析器
    if (!parser)    { exit(1); }

    ctx = avcodec_alloc_context3(codec);    // 分配codec上下文
    if (!ctx)       { exit(1); }

    ret = avcodec_open2(ctx, codec, NULL);  // 将解码器和解码器上下文进行关联
    if (ret < 0)    { exit(1); }

    fl_in  = fopen(infilename, "rb");       // 打开输入文件
    fl_out = fopen(outfilename, "wb");      // 打开输出文件
    if (!fl_in || !fl_out) {
        av_free(ctx);
        exit(1);
    }

    // ================== 2、开始解码 ==================
    buf_size = fread(buf, 1, BUF_SIZE, fl_in); // 读mp3文件
    while (buf_size > 0)
    {
        ret = av_parser_parse2(parser, ctx, &pkt->data, &pkt->size, buf_index, buf_size,
                               AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if (ret < 0) { exit(1); }  // parser失败,直接退出程序
        buf_index += ret;          // 跳过已经解析的数据
        buf_size  -= ret;          // 缓冲区还未解析的mp3数据大小

        if (pkt->size)
            my_decode(ctx, pkt, frame, fl_out, codec_id); // 解码并将解码后的pcm数据写入文件

        if (buf_size < 4096)  // 缓冲区音频数据小于4096,再从mp3文件中读取一点数据放入缓冲区
        {
            memmove(buf, buf_index, buf_size); // 把还未解析的数据移动到buf的起始位置
            buf_index = buf;                   // 当前解析位置也移动到buf起始位置
            len = fread(buf_index + buf_size, 1, BUF_SIZE - buf_size, fl_in);
            if (len > 0) buf_size += len;
        }
    }

    // ================== 3、解码结束 ==================
    pkt->data = NULL; // 进入drain mode
    pkt->size = 0;
    my_decode(ctx, pkt, frame, fl_out, codec_id);

    fclose(fl_out);   // 关闭文件
    fclose(fl_in);

    // 释放空间
    avcodec_free_context(&ctx);
    av_parser_close(parser);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    printf("\n转换成功\n");
    return 0;
}

说明:代码中通过文件后缀名判断文件是音频还是视频,这种方式不太可取,后面再考虑优化一下。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Linux上使用FFmpeg推流RTMP流为h264/PC的步骤如下: 1. 首先,确保你已经安装了FFmpeg。如果没有安装,可以使用以下命令进行安装: ``` sudo apt-get install ffmpeg ``` 2. 使用以下命令将h264视频流和PCMU音频流推送到RTMP服务器: ``` ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast -tune zerolatency -b:v 2000k -maxrate 2000k -bufsize 2000k -pix_fmt yuv420p -g 50 -c:a pcm_mulaw -ar 8000 -f flv rtmp://server/live/stream ``` 解释一下上述命令的参数: - `-re`:以实时速度读取输入文件。 - `-i input.mp4`:指定输入文件的路径和名称。 - `-c:v libx264`:使用libx264编码器进行视频编码。 - `-preset veryfast`:设置视频编码速度为veryfast。 - `-tune zerolatency`:设置视频编码器为零延迟模式。 - `-b:v 2000k`:设置视频的比特率为2000k。 - `-maxrate 2000k`:设置视频的最大比特率为2000k。 - `-bufsize 2000k`:设置视频的缓冲区大小为2000k。 - `-pix_fmt yuv420p`:设置像素格式为yuv420p。 - `-g 50`:设置关键帧间隔为50帧。 - `-c:a pcm_mulaw`:使用PCM mu-law编码器进行音频编码。 - `-ar 8000`:设置音频的采样率为8000Hz。 - `-f flv`:指定输出格式为FLV。 - `rtmp://server/live/stream`:指定RTMP服务器的URL和流名称。 请根据你的实际情况修改输入文件的路径和名称,以及RTMP服务器的URL和流名称。 希望以上信息对你有帮助!如果你还有其他问题,请继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值