音视频入门(学习ffmpeg tutorial之 iOS音频解码与播放)

隔了好久好久。。。。,其实ffmpeg是有专门的教程的,只是其中很多内容过期了,我这里也只是当作一个学习的过程,然后记录下来。音视频开发需要具备的知识点如下(从他人处copy过来的)

  • rtsp、sdp、tcp、udp、ip协议(rtsp的DESCRIBE、OPTION、SETUP、PLAY、PAUSE、TEARDOWN;tcp连接的三次握手/断开的四次握手)
  • socket
  • 多线程
  • opengl es
  • FFmpeg
  • YUV420(知道它的原理与格式)
  • 音视频同步(时间戳的处理)
  • C语言指针

言归正传:

音频播放的基本流程

基本流程跟上一节的视频播放其实差不多,但是这里由于采用SDL的音频播放机制,所以需要采用多线程的方式调整一下基本流程(之后也需要对视频播放进行改造),基本流程其实就是生产者消费者模式,一个线程不停的从某个队列中读数据,读不到就等着,另外一个线程不停的往某个队列中塞数据。
创建队列的代码:

typedef struct PacketQueue{
    AVPacketList *first_pkt, *last_pkt;
    int nb_packets;
    int size;
    SDL_mutex *mutex;
    SDL_cond *cond;
}PacketQueue;

void packet_queue_init(PacketQueue *q) {
    memset(q, 0, sizeof(PacketQueue));
    q->mutex = SDL_CreateMutex();
    q->cond = SDL_CreateCond();
}

int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
    AVPacketList *pktl;
    if (av_packet_ref(pkt, pkt) < 0){
        return -1;
    }

    pktl = (AVPacketList *)av_malloc(sizeof(AVPacketList));
    if (!pktl) {
        return -1;
    }

    pktl->pkt = *pkt;

    SDL_LockMutex(q->mutex);//生产者消费者模式互斥锁

    //指针操作,把最新生成的packet插入队尾,保证每次取的packet都是按照解码顺序的
    if (!q->last_pkt) {
        q->first_pkt = pktl;
    } else {
        q->last_pkt->next = pktl;
    }

    q->last_pkt = pktl;
    q->nb_packets ++ ;
    q->size += pktl->pkt.size;
    //条件锁释放有资源的信号
    SDL_CondSignal(q->cond);

    SDL_UnlockMutex(q->mutex);
    return 0;
}

static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
    AVPacketList *pktl;
    int ret;

    SDL_LockMutex(q->mutex);

    do{
        if (quit) {
            ret = -1;
            break;
        }
        pktl = q->first_pkt;
        if (pktl) {
            //指针操作,从队列中把队首元素取出来
            q->first_pkt = pktl->next;
            if (!q->first_pkt) {
                q->last_pkt = NULL;
            }

            q->nb_packets--;
            q->size -= pktl->pkt.size;
            *pkt = pktl->pkt;
            av_free(pktl);
            ret = 1;
            break;
        } else if (!block) {
            ret = 0;
            break;
        } else {
            //这个条件锁,能在条件不达要求的时候,把互斥锁释放
            SDL_CondWait(q->cond, q->mutex);
        }
    }while(true);
    SDL_UnlockMutex(q->mutex);
    return ret;
}
生产者线程流程
  • 注册需要支持的文件格式以及对应的编解码器
  • 打开文件
  • 从文件中提取数据流信息
  • 从数据流(视频数据流/音频数据流/字幕数据流)中找到对应的音频数据流
  • 从音频数据流中找到对应的解码器
  • 打开解码器
  • 将原始帧塞入队列中以供消费
int main(int argc, char *argv[]) {
    SDL_Event event;
    AVFormatContext* formatContext = avformat_alloc_context();//初始化AVFormatContext
    AVCodecContext* codeContext = NULL;
    int audioIndex = -1;
    av_register_all();//一劳永逸,第一步注册所有的文件格式以及编码器 deprecated
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);//初始化SDL

    NSString * path = [[NSBundle  mainBundle]pathForResource:@"test.mp3" ofType:@""];//获取文件路径

    if(0!=avformat_open_input(&formatContext, [path UTF8String], NULL, NULL)){
        return -1;//第二步打开文件,并存入formatContext中。
    }

    if(0!=avformat_find_stream_info(formatContext, NULL)){
        return -1;//第三步从formatContext对象中读取AVStream信息.
    }
    AVCodec* codec = NULL;
    AVPacket packet;

    for(int i = 0;i<formatContext->nb_streams;i++){
        AVStream * stream = formatContext->streams[i];
        if(stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){//第四步遍历formatContext中的所有数据流。从中选出音频流
            codec = avcodec_find_decoder(stream->codecpar->codec_id);//第五步,根据流的信息找到对应的解码器。
            codeContext = stream->codec;
            audioIndex = i;
            break;
        }
    }
    if(codeContext == NULL)
        return -1;

    SDL_AudioSpec   wanted_spec, spec;

    wanted_spec.freq = codeContext->sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.channels = codeContext->channels;
    wanted_spec.silence = 0;
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
    wanted_spec.callback = audio_callback;//音频硬件从此方法中获取相应的数据播放
    wanted_spec.userdata = codeContext;

    //使用SDL音频播放功能
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0)
    {
        fprintf(stderr, "SDL_OpenAudio: %s \n", SDL_GetError());
        return -1;
    }

    //第六步,打开解码器
    avcodec_open2(codeContext, codec, NULL);

    packet_queue_init(&audioq);
    SDL_PauseAudio(0);

    if(0!=avcodec_open2(codeContext, codec, NULL))//第六步,打开编解码器
        return -1;

    while (av_read_frame(formatContext, &packet) >= 0)
    {
        //读取压缩帧塞入队列中
        if (packet.stream_index == audioIndex)
            packet_queue_put(&audioq, &packet);
        else
            av_free_packet(&packet);

        SDL_PollEvent(&event);
        switch (event.type) {
            case SDL_QUIT:
                quit = 1;
                SDL_Quit();
                exit(0);
                break;

            default:
                break;
        }
    }
//
//  在控制台输出文件信息
    av_dump_format(formatContext, 0, [[[NSBundle mainBundle] pathForResource:@"test.mp3" ofType:@""] UTF8String], 0);

    return 0;
}
消费者线程流程
  • 音频硬件利用回调函数,获取数据,填满自己的缓冲区
  • 从队列中获取压缩帧
  • 获取解压缩帧
  • 解码数据然后放入SDL播放音频
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t * audio_buf, int buf_size) {
    static AVPacket thisPkt;
    static AVFrame frame;

    int data_size = 0;

    do{
    //需要先使用avcodec_send_packet发送了压缩帧才能从aCodecCtx中获取解压帧
        while (0 == avcodec_receive_frame(aCodecCtx, &frame)) {
            
            data_size = av_samples_get_buffer_size(NULL, aCodecCtx->channels, frame.nb_samples, aCodecCtx->sample_fmt, 1);
            memcpy(audio_buf, frame.data[0], data_size);
            

            if (data_size <= 0){
                continue;
            }

            printf("saving frame %3d\n", aCodecCtx->frame_number);

            return data_size;
        }

        if (thisPkt.data) {
            av_packet_unref(&thisPkt);
        }

        if (quit) {
            return -1;
        }

        if (packet_queue_get(&audioq, &thisPkt, 1) < 0) {
            return -1;
        } else {
            avcodec_send_packet(aCodecCtx, &thisPkt);
        }



    }while(true);
}

void audio_callback(void *userdata, uint8_t *stream, int len) {
    //外部传进来的userdata,stream可以理解为SDL音频硬件的缓冲区,len表示缓冲区大小
    AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
    int lenl ,audio_size;
    //audio_buf用于暂存解压帧数据的缓冲区,最后要把这个里面的数据copy到stream中,直到这个缓冲区被填满。
    static uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE * 3)/2];
    static unsigned int audio_buf_size = 0;
    static unsigned int audio_buf_index = 0;

    while (len > 0) {
        if (audio_buf_index >= audio_buf_size) {
            audio_size = audio_decode_frame(aCodecCtx, audio_buf, audio_buf_size);
            if (audio_size < 0 ) {
                audio_buf_size = 1024;
                memset(audio_buf, 0, audio_buf_size);
            } else {
                audio_buf_size = audio_size;
            }
            audio_buf_index = 0;
        }

        lenl = audio_buf_size - audio_buf_index;
        if (lenl > len) {
            lenl = len;
        }

        memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, lenl);
        len -= lenl;
        stream += lenl;
        audio_buf_index += lenl;
    }
}

以上代码还有一个缺点,就是基本是杂音。需要重采样(还是有杂音,具体原因我暂时也不知道),重采样的代码在下面的链接中:

FFmpeg音频播放


如果觉得我的文章对你有帮助,希望能点个赞,您的“点赞”将是我最大的写作动力,如果觉得有什么问题也可以直接指出。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值