在《基于生产者-消费者模型的视频播放器 - 续2》中还存在两个问题:
1. 视频播放结束后,窗口不能自动退出,且无法响应鼠标、键盘事件;
2. 有时按 空格 暂停不生效,或延时生效。
主要原因是在Consumer Thread中,有两个阻塞等待的地方:一个是SDL_WaitEvent(&event),等待按键或REFRESH event;另外一个是dequeue(&frame_q),从队列中获取视频帧。如果队列为空,则进入阻塞等待。如果在这个地方阻塞了,此时按键事件就无法响应了。
因此,对程序做了小改进,去掉了dequeue()里面的阻塞等待。这个地方确实用得不对,因为在同一个循环里面,已经有了一处阻塞等待的地方。另外,加了一个快进功能。每按一次方向右键,快进30秒(通过快速播放30*25帧画面实现)。
MyPlayerV2.c:
/*
* A simple player with FFMPEG4.0 and SDL2.0.8.
* Created by LiuWei@20180530
* 1. Only support video decoder, not support audio and subtitle.
* 2. Based on Producer-Consumer thread model.
*
* 20180531 Modifications:
* 1. Add key event process
* 2. Producer thread produce frames every 40ms, using the same frequence with Consumer thread.
*
* 20180601 Modifications:
* 1. Fix bug which Player Window can't quit when end of video.
* 2. Add fast forward function.
*/
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <sys/stat.h>
#include <pthread.h>
#include <SDL2/SDL.h>
#include "video_frame_queue.h"
#define FRAMES_NUM_PER_SEC 25 /* how many frames each second */
#define FRAMES_SKIP_ON_FAST_FORWARD (30*FRAMES_NUM_PER_SEC) /* 10 seconds per key event(Right Key) */
#define FRAME_REFRESH_EVENT (SDL_USEREVENT+1)
#define FRAME_REFRESH_TIMER 40 /* 40ms */
static AVFormatContext *fmt_ctx = NULL;
static AVStream *video_stream = NULL;
static AVCodec *dec = NULL;
static AVCodecContext *video_dec_ctx = NULL;
static int video_stream_index = -1;
typedef struct {
int end_of_video;
int play_stop;
int play_pause;
int fastforward_skip_frames;
int frame_refresh_timer;
} VideoState;
/* The queue which stores uncompressed frame data */
avframe_q frame_q;
/* Video play state */
VideoState av_state = {0, 0, 0, 0, FRAME_REFRESH_TIMER};
/* Send a refresh signal to Consumer Thread every VideoState.frame_refresh_timer(it's 40ms by default). */
static int frame_refresh_thread(void *arg)
{
while(!av_state.end_of_video && !av_state.play_stop) {
if(!av_state.play_pause) {
SDL_Event event;
event.type = FRAME_REFRESH_EVENT;
SDL_PushEvent(&event);
}
SDL_Delay(av_state.frame_refresh_timer);
}
return 0;
}
/* Output uncompressed frame, then push into queue */
static void *producer_thread(void *arg)
{
int ret = 0;
AVPacket pkt;
AVFrame *video_frame = av_frame_alloc();
if(!video_frame) {
printf("producer_thread(): Could not allocate frame\n");
return;
}
/* Initialize packet, send data to NULL, let the demuxer fill it */
av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;
/* Read frames: The packet must be freed with av_packet_unref(). */
while(!av_state.play_stop && av_read_frame(fmt_ctx, &pkt) >= 0) {
if(pkt.stream_index != video_stream_index) {
av_packet_unref(&pkt);
continue;
}
ret = avcodec_send_packet(video_dec_ctx, &pkt);
if(ret < 0) {
av_packet_unref(&pkt);
continue;
}
do {
ret = avcodec_receive_frame(video_dec_ctx, video_frame);
if(ret < 0)
break;
else if(ret == 0) { /* Got a frame successfully */
/* It will be blocked if queue is full. */
enqueue(&frame_q, video_frame);
if(av_state.fastforward_skip_frames) /* Delay timer will be cancelled */
av_state.fastforward_skip_frames--;
else /* Back to default value: 40ms */
av_state.frame_refresh_timer = FRAME_REFRESH_TIMER;
} else if(ret == AVERROR_EOF) {
avcodec_flush_buffers(video_dec_ctx);
break;
}
} while(ret != AVERROR(EAGAIN));
av_packet_unref(&pkt);
/* Use the same frequence with Consumer thread. */
SDL_Delay(av_state.frame_refresh_timer);
}
av_frame_free(&video_frame);
av_state.end_of_video = 1;
av_state.play_stop = 1;
}
/* Create Player window, get frame from queue every 40ms. */
static void *consumer_thread(void *arg)
{
int screen_w, screen_h = 0;
SDL_Window *screen;
SDL_Renderer *sdlRenderer;
SDL_Texture *sdlTexture;
SDL_Rect sdlRect;
SDL_Event event;
frame_t *frame; /* frame from queue */
/* Wait 40ms to make sure producer thread run first. */
SDL_Delay(40);
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);
screen_w = video_dec_ctx->width;
screen_h = video_dec_ctx->height;
screen = SDL_CreateWindow("MyPlayerV2 based on Procuder-Consumer Model",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screen_w, screen_h, SDL_WINDOW_OPENGL);
sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
video_dec_ctx->width, video_dec_ctx->height);
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = screen_w;
sdlRect.h = screen_h;
while(!av_state.end_of_video && !av_state.play_stop) {
if(SDL_WaitEvent(&event) != 1)
continue;
switch(event.type) {
case SDL_KEYDOWN:
if(event.key.keysym.sym == SDLK_ESCAPE)
av_state.play_stop = 1;
else if(event.key.keysym.sym == SDLK_SPACE)
av_state.play_pause = !av_state.play_pause;
else if(event.key.keysym.sym == SDLK_RIGHT) /* Fast Forward */
av_state.fastforward_skip_frames = FRAMES_SKIP_ON_FAST_FORWARD;
av_state.frame_refresh_timer = 0;
break;
case SDL_QUIT: /* Window is closed */
av_state.play_stop = 1;
break;
case FRAME_REFRESH_EVENT: /* refresh window with a new frame every 40ms */
/* It will be blocked if queue is empty. */
frame = dequeue(&frame_q);
if( frame != NULL ) {
SDL_UpdateYUVTexture(sdlTexture, &sdlRect,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
SDL_RenderClear(sdlRenderer);
SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
SDL_RenderPresent(sdlRenderer);
}
break;
}
}
SDL_DestroyRenderer(sdlRenderer);
SDL_Quit();
exit(0); /* The whole program including all threads will quit together. */
}
int main(int argc, char *argv[])
{
int ret = 0;
char *video_file_path = NULL;
pthread_t producer_tid;
pthread_t consumer_tid;
if(argc != 2) {
printf("e.g. ./MyPlayerV2 ~/Videos/xx.mp4 \n");
return -1;
}
video_file_path = argv[1];
/* Open an input stream and read the reader. The codecs are not opened.
* The stream must be closed with avformat_close_input().*/
if(avformat_open_input(&fmt_ctx, video_file_path, NULL, NULL) < 0) {
printf("Could not open %s\n", video_file_path);
return -1;
}
if(avformat_find_stream_info(fmt_ctx, NULL) < 0) {
printf("Could not find stream information\n");
goto END;
}
/* Open codec for video stream */
ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if(ret < 0) {
printf("Could not find video stream in input file %s\n", video_file_path);
goto END;
} else {
video_stream_index = ret;
}
video_stream = fmt_ctx->streams[video_stream_index];
dec = avcodec_find_decoder(video_stream->codecpar->codec_id);
if(!dec) {
printf("Failed to find %s codec\n", av_get_media_type_string(AVMEDIA_TYPE_VIDEO));
goto END;
}
/* Allocate a codec context for the decoder.
* The resulting struct should be freed with avcodec_free_context().*/
video_dec_ctx = avcodec_alloc_context3(dec);
if(!video_dec_ctx) {
printf("Failed to allocate the %s codec context\n",
av_get_media_type_string(AVMEDIA_TYPE_VIDEO));
goto END;
}
/* Copy codec parameters from input stream to output codec context */
ret = avcodec_parameters_to_context(video_dec_ctx, video_stream->codecpar);
if(ret < 0) {
printf("Failed to copy %s codec parameters to decoder context\n",
av_get_media_type_string(AVMEDIA_TYPE_VIDEO));
goto END;
}
/* Init the decoders */
ret = avcodec_open2(video_dec_ctx, dec, NULL);
if(ret < 0) {
printf("Failed to open %s codec\n", av_get_media_type_string(AVMEDIA_TYPE_VIDEO));
goto END;
}
/* Dump information */
printf("----------------------File Info---------------------");
av_dump_format(fmt_ctx, 0, video_file_path, 0);
printf("----------------------------------------------------\n");
/* Create a queue which could cache 10 frames at most. */
if(queue_init(video_dec_ctx, &frame_q, 10, &av_state.end_of_video) == RET_FALSE)
goto END;
/* Create threads. */
pthread_create(&producer_tid, NULL, producer_thread, NULL); /* Decoder */
pthread_create(&consumer_tid, NULL, consumer_thread, NULL); /* Displayer */
SDL_CreateThread(frame_refresh_thread, NULL, NULL); /* Refresh timer for Displayer */
pthread_join(producer_tid, NULL);
pthread_join(consumer_tid, NULL);
END:
queue_destroy(&frame_q);
if(video_dec_ctx)
avcodec_free_context(&video_dec_ctx);
if(fmt_ctx)
avformat_close_input(&fmt_ctx);
return 0;
}
video_frame_queue.h:
/*
* A simple player with FFMPEG4.0 and SDL2.0.8.
* Created by LiuWei@20180530
* 1. Only support video decoder, not support audio and subtitle.
* 2. Based on Producer-Consumer thread model.
*
* 20180531 Modifications:
* 1. Add key event process
* 2. Producer thread produce frames every 40ms, using the same frequence with Consumer thread.
*
* 20180601 Modifications:
* 1. Fix bug which Player Window can't quit when end of video.
* 2. Add fast forward function.
*/
#ifndef VIDEO_FRAME_QUEUE_H
#define VIDEO_FRAME_QUEUE_H
#define RET_TRUE 1
#define RET_FALSE 0
#define RET_ERROR -1
typedef struct {
uint8_t *data[4];
int linesize[4];
} frame_t;
typedef struct {
pthread_mutex_t lock;
pthread_cond_t cond;
int size;
int ele_num;
int enq_pos;
int deq_pos;
int *end_of_video; /* Stop dequeue waiting when end_of_video=1 */
frame_t **frames;
AVCodecContext *dec_ctx;
} avframe_q;
static int queue_init(AVCodecContext *video_dec_ctx, avframe_q *queue, int size, int *end)
{
int i = 0;
if(!queue || size <= 0 || !video_dec_ctx) {
printf("queue_init failed with size=%d\n", size);
return RET_FALSE;
}
pthread_mutex_init(&queue->lock, NULL);
pthread_cond_init(&queue->cond, NULL);
pthread_mutex_lock(&queue->lock);
queue->frames = malloc(size * sizeof(frame_t *));
if(queue->frames == NULL) {
printf("queue_init: failed to malloc %d bytes for queue->frames\n", size * sizeof(frame_t *));
pthread_mutex_unlock(&queue->lock);
return RET_FALSE;
}
memset(queue->frames, 0, size * sizeof(frame_t *));
for(i = 0; i < size; i++) {
queue->frames[i] = malloc(sizeof(frame_t));
if(queue->frames == NULL) {
printf("queue_init: failed to malloc %d bytes for queue->frames[%d]\n", sizeof(frame_t), i);
pthread_mutex_unlock(&queue->lock);
return RET_FALSE;
}
memset(queue->frames[i], 0, sizeof(frame_t));
av_image_alloc(queue->frames[i]->data, queue->frames[i]->linesize,
video_dec_ctx->width, video_dec_ctx->height, video_dec_ctx->pix_fmt, 1);
}
queue->size = size;
queue->ele_num = 0;
queue->enq_pos = 0;
queue->deq_pos = 0;
queue->dec_ctx = video_dec_ctx;
queue->end_of_video = end;
pthread_mutex_unlock(&queue->lock);
return RET_TRUE;
}
static void queue_destroy(avframe_q *queue)
{
int i = 0;
if(!queue || !queue->size || !queue->frames)
return;
pthread_mutex_lock(&queue->lock);
for(i = 0; i < queue->size; i++) {
av_freep(&(queue->frames[i]->data[0]));
free(queue->frames[i]);
queue->frames[i] = NULL;
}
free(queue->frames);
queue->frames = NULL;
pthread_mutex_unlock(&queue->lock);
pthread_cond_destroy(&queue->cond);
pthread_mutex_destroy(&queue->lock);
}
static int is_queue_empty(avframe_q *queue)
{
/* Not initialized. It should never happen. */
if(!queue || !queue->size || !queue->frames)
return RET_ERROR;
if(queue->ele_num == 0)
return RET_TRUE;
return RET_FALSE;
}
static int is_queue_full(avframe_q *queue)
{
if(!queue || !queue->size || !queue->frames)
return RET_ERROR;
if(queue->ele_num == queue->size)
return RET_TRUE;
return RET_FALSE;
}
static frame_t * dequeue(avframe_q *queue)
{
int i = 0;
frame_t *frame;
pthread_mutex_lock(&queue->lock);
/* return NULL if end of video or queue is empty. */
if(*(queue->end_of_video) == 1 || is_queue_empty(queue) != RET_FALSE) {
pthread_mutex_unlock(&queue->lock);
return NULL;
}
frame = queue->frames[queue->deq_pos];
queue->ele_num -= 1;
queue->deq_pos += 1;
if(queue->deq_pos == queue->size)
queue->deq_pos = 0;
/* Producer thread is blocked when queue is full.
* A signal is sent to Producer when an element is consumed in a full queue. */
if(queue->ele_num == queue->size - 1) {
pthread_mutex_unlock(&queue->lock);
pthread_cond_signal(&queue->cond);
} else {
pthread_mutex_unlock(&queue->lock);
}
return frame;
}
static int enqueue(avframe_q *queue, AVFrame *av_frame)
{
int i = 0;
frame_t *q_frame;
pthread_mutex_lock(&queue->lock);
if(is_queue_full(queue) == RET_ERROR || !av_frame) {
pthread_mutex_unlock(&queue->lock);
return RET_ERROR;
}
while(is_queue_full(queue) == RET_TRUE){
//printf("queue is full, waiting for consumer...\n");
pthread_cond_wait(&queue->cond, &queue->lock);
}
q_frame = queue->frames[queue->enq_pos];
av_image_copy(q_frame->data, q_frame->linesize,
av_frame->data, av_frame->linesize,
queue->dec_ctx->pix_fmt, queue->dec_ctx->width, queue->dec_ctx->height);
queue->ele_num += 1;
queue->enq_pos += 1;
if(queue->enq_pos == queue->size)
queue->enq_pos = 0;
pthread_mutex_unlock(&queue->lock);
return RET_TRUE;
}
#endif
Makefile: 这个文件没有变化。
CC=gcc
CCFLAGS=-I/usr/local/include -O2
LDFLAGS=-L/usr/local/lib -lavformat -lavfilter -lavcodec -lswresample -lavdevice -lavutil -lswscale -lpostproc -lpthread -lm -lx264 -lx265 -lz -lSDL2
TARGET=myplayerv2
OBJS=MyPlayerV2.o
RM=rm -f
STRIP=strip
$(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(LDFLAGS)
# $(STRIP) $(TARGET)
$(OBJS):%.o:%.c
$(CC) -c -g $(CCFLAGS) $< -o $@
clean:
$(RM) $(TARGET) $(OBJS) *~