ffmpeg综合项目:mp4播放器(项目代码已上传到码云)

0、系列文章:

ffmpeg音视频编码入门:视频解码

ffmpeg音视频编码入门:音频解码(acc/mp3 转 pcm)

ffmpeg —— SDL2播放yuv文件(使用事件驱动和多线程,支持按键暂停/退出)

ffmpeg —— SDL2播放pcm音频

ffmpeg 视频文件解封装,提取mp4中的h264码流和aac码流


1、mp4播放器主体框架:
image-20210518125641580

项目思路:

  1. 创建两个环形队列,用来存放处理后的h264码流数据和AAC码流;
  2. 创建一个解封装线程,不断demuxer并进行过滤/添加adts头处理码流,将数据入队;
  3. 创建音频和视频解码播放线程,从队列中取码流数据,然后解码播放。

2、Gitee项目代码:mp4_player

image-20210518230035554


2.1 文件介绍(相关结构体)
  1. packet_queue.h 提供环形缓冲区的数据管理机制(支持多线程),不涉及数据块的创建和销毁
  2. stream.h 提供解封装线程,包含StreamState,MediaState结构的定义和初始化
  3. audio_video_thread.h 提供音频和视频码流的解码播放线程,包含AudioState,VideoState结构的定义和初始化
  4. play_mp4.c 提供主线程,播放控制线程,定时刷新事件线程

2.2 结构层次

MediaState描述了媒体文件的信息,是最顶层的结构;

struct MediaState {
    AVFormatContext *fmt_ctx;           // 输入流的格式上下文
    struct VideoState *video_state;     // 视频结构 
    struct AudioState *audio_state;     // 音频结构

    SDL_Thread *demuxer_tid;            // 解封装线程
    SDL_Thread *playback_control_tid;   // 播放控制线程
    SDL_Event event;                    // 按键事件,播放/暂停/退出
};

StreamState描述了解码器和码流管理,是最底层的结构;

struct StreamState {
    int stream_index;           // 码流下标
    AVCodecContext *cod_ctx;    // 解码器上下文
    AVCodec *cod;               // 解码器
    float fps;                  // 视频播放延时为1/fps
    struct myqueue *que;        // 可播放的码流队列
    
    // 优化队列的管理:解封装完成,队列空时不需要等待数据
    int writer_count;         // 写者数量,对应解封装线程
    int reader_count;         // 读者数量,对应取数据解码线程
}; 

VideoState和AudioState描述了音频和视频码流解码播放的相关信息。

struct AudioState {
	// 解码相关
    struct StreamState *stream_state;
    int pcmbuf_size;
    uint8_t **pcmbuffer;
    struct SwrContext *swr_ctx;
    // 播放相关
    SDL_AudioSpec spec;
    unsigned int audio_len;
    unsigned char *audio_chunk;
    unsigned char *audio_pos;
    SDL_Thread *audio_tid;
};

struct VideoState {
    struct StreamState *stream_state;
    AVBSFContext *bsf_ctx;
    
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_Texture *texture;
    SDL_Rect rect;
    SDL_Thread *video_tid;
};

2.3 主要的线程
int demuxer_thread(void *p) {
    struct MediaState *media = (struct MediaState *)p;
    struct VideoState *video = media->video_state;
    struct AudioState *audio = media->audio_state;

    // 读取一帧编码数据
    while (1) {
        HANDLE_EVENT(media->event.type);
        
        AVPacket *packet = av_packet_alloc();
        if (av_read_frame(media->fmt_ctx, packet) != 0) break;

        /*************** 处理码流 *******************/
        if (packet->stream_index == video->stream_state->stream_index) {
            // 过滤器处理视频码流
            if (av_bsf_send_packet(video->bsf_ctx, packet) != 0) {
                printf("failed to send packet to bitstream filter\n");
                break;
            }
            if (av_bsf_receive_packet(video->bsf_ctx, packet) != 0) {
                printf("failed to receive packet from bitstream filter\n");
                break;
            }
            // 将码流数据放到队列中管理
            if (enqueue(video->stream_state->que, packet, video->stream_state->reader_count) < 0) {
                break;  // 解码线程退出时才会出现返回-1的情况
            }
        } else if (packet->stream_index == audio->stream_state->stream_index) {
            // 添加ADTS头
            AVPacket *tmp = av_packet_alloc();
            tmp->size = packet->size + 7;;
            tmp->data = malloc(tmp->size);
            memcpy(tmp->data, adts_header_gen(tmp->size), 7);
            memcpy(tmp->data + 7, packet->data, packet->size);
            
            av_packet_free(&packet);
            if (enqueue(audio->stream_state->que, tmp, audio->stream_state->reader_count) < 0) {
                break;
            }
        } else {
            printf("unkown stream\n");
            av_packet_free(&packet);
            break; 
        }
        // 视频24fps,40ms播放一帧,音频44100/1024 = 42fps,22ms播放一帧
        
        // 当大于缓冲区一半时,延时使消耗大于产出,que->size减小
        if (video->stream_state->que->size > MAX_QUEUE/2)
            SDL_Delay(5*video->stream_state->que->size);
    }
    audio->stream_state->writer_count = 0;
    video->stream_state->writer_count = 0;
    printf("demuxer_thread end\n");
}
int playback_control_thread(void *p) {
    struct MediaState *media = (struct MediaState *)p;
	
    media->audio_state->audio_tid = SDL_CreateThread(audio_thread, NULL, media);
    media->video_state->video_tid = SDL_CreateThread(video_thread, NULL, media);
    
    SDL_Thread *refresh_tid = SDL_CreateThread(refresh_thread, NULL, NULL);
    while (1) {
        if (media->event.type == SDL_KEYDOWN) {
            // 空格键暂停,ESC键退出
            if (media->event.key.keysym.sym == SDLK_SPACE) {
                thread_stop = !thread_stop;
            } else if (media->event.key.keysym.sym == SDLK_ESCAPE) {
                thread_quit = 1;
            }
        } else if (media->event.type == SDL_QUIT || media->event.type == QUIT_EVENT) {
            break;
        }
 
        // 等待刷新,刷新间隔越小,实时性越好
        SDL_WaitEvent(&media->event);
    }
    SDL_WaitThread(refresh_tid, NULL);
    
    SDL_WaitThread(media->audio_state->audio_tid, NULL);
    SDL_WaitThread(media->video_state->video_tid, NULL);
    printf("playback_control_thread end\n");
}
int audio_thread(void *p) {
    struct MediaState *media = (struct MediaState *)p;
    struct AudioState *audio = media->audio_state;
    AVFrame *frame = av_frame_alloc();
    
    // 启动播放
    SDL_PauseAudio(0); 
    while (1) {
        HANDLE_EVENT(media->event.type);

        // 从码流队列中取数据, 解缓冲区无数据且封装已完成时退出
        AVPacket *packet =  dequeue(audio->stream_state->que, audio->stream_state->writer_count);
        if (packet == NULL) break;

        int ret = decodec_packet_to_frame(audio->stream_state->cod_ctx, packet, frame);
        if (ret == EAGAIN) continue;
        else if (ret == EINVAL) break;
        
        SDL_play_pcm(audio, frame);
        av_packet_free(&packet);  // 播放完一帧后清理
        av_frame_unref(frame);
    }
    av_frame_free(&frame);
    audio->stream_state->reader_count--;
    //queue_wakeup(audio->stream_state->que);
    printf("audio_thread end\n");
}

3、不足与优化:

问题:暂停操作正常,但解封装线程无法中途退出,导致主线程一直卡在SDL_WaitThread(media->demuxer_tid, NULL)。

猜测原因是队列操作的影响(使用了条件变量,而取数据解码的速度总是慢于解封装码流入队的速度,所以经常队列满,卡在pthread_wait_cond)。

虽然添加了读者写者的条件,但因为没有数据变化的广播,所以解封装线程被阻塞在入队函数中的pthread_cond_wait位置

int enqueue(struct myqueue *q, Elem_t t, int reader_count) {
    if (reader_count <= 0) return -1; // 读者不存在,存数据到队列没有意义 
    pthread_mutex_lock(&q->mut);

    while (q->size == MAX_QUEUE && reader_count > 0) {
        pthread_cond_wait(&q->cond, &q->mut);
    }
    q->buffer[q->tail] = t;
    q->tail = next_pos(q->tail);
    q->size++;
    pthread_cond_broadcast(&q->cond);

    pthread_mutex_unlock(&q->mut);
    return 0;
}

①可以考虑添加一个周期检测读者/写者是否都存在的线程,如果有一方缺失,则调用pthread_cond_broadcast,唤醒被阻塞的线程。(加一个管理线程/每个队列都拥有一个检测线程?未尝试)

②在解码线程退出时,更新读者数量,并用广播唤醒被阻塞的线程,退出时手动调用pthread_cond_broadcast并没有唤醒解封装线程

③经常卡在pthread_wait_cond,这是读写速度差异太大导致的。可以适当减慢一下解码的速度,从而避免队列满的情况。但只是固定延时的话,无法杜绝被阻塞的情况。(只要速度不均衡,最终还是会走向队列满,越往后越拥塞)。

根据缓冲区大小,及音视频播放帧率,动态调整延时。缓冲区中的码流数据越多,延时越久,从而使que->size保持在一个稳定范围,以避免线程被阻塞。最好是初期不延时,当数据过半时,使其快速稳定。(自动控制原理?2333)


问题2:只是用多线程,并发的解码播放音频和视频,没有提供自适应的同步机制

5.29 补充同步思路:
 在MP4播放项目中,当我们从码流队列中取出音频帧和视频帧时,并没有人为去控制两者的播放,只是用多线程同时解码播放。当遇到其它情况,如一方取数据出现延迟或解码失败时,可能会出现声音视频不同步,因此需要有一个自适应的音视频的同步处理机制,来避免意外情况,保证声音视频同步稳定的播放

音频是匀速的,视频是非线性的。所以我们可以以音频为标杆,跟踪音频当前播放的时间点,在video thread中将用这个值来计算和判断视频是播快了还是播慢了,动态调整播放一帧视频的时间,以达到音视频同步的目的。

问题3:可以尝试抽象统一音频和视频的码流方法。其它:。。。(解耦,封装与抽象)

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值