FFmpeg音视频播放器系列(第三篇:seek实现播放进度控制)


上一篇基本实现了音视频的播放同步,简单的按键控制暂停、恢复、退出操作,这一篇就打算实现播放的进度控制主要是实现快进、快退、重新播放等,但是不打算用SDL来实现GUI操作,主要是用按键操作实现、GUI的部分还是放到用QT实现吧,毕竟不是主要研究SDL的GUI的。

如何实现播放进度控制

想实现播放进度控制,就意味着需要随机的访问流媒体文件,那么就需要使用av_seek_frame或者avformat_seek_file函数。

av_seek_frame

  • 函数:int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags)
  • AVFormatContext *s:流媒体打开的上下文结构指针
  • int stream_index:流媒体索引,视频流或者是音频流,根据其中一个来检索,实现seek操作
  • int64_t timestamp:检索时间戳,若指定流媒体索引,则时间单位是流媒体对应的AVStream.time_base,若不指定流媒体索引,则时间单位是AV_TIME_BASE
  • int flags:指明seek操作的标志
    #define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
    #define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes
    #define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes
    #define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number
    当flag中有AVSEEK_FLAG_BYTE时,时间戳要改为byte字节计数
    当flag中有AVSEEK_FLAG_FRAME时,表示seek到离timestamp最近的关键帧,对于视频流来说就是I帧,音频流未知。

seek操作要点

  • seek操作基准:即seek操作当前时间点,因此在进行播放时需要实时记录音视频流的DTS或者PTS
  • seek时间单位:进行seek操作时,根据流索引,确定其时间单位time_base,
  • seek时间大小:即向前或者向后seek的时间大小,根据此时间大小和当前时间点,可以计算出向前或者向后进行seek操作时的目标时间
  • seek操作载体:即是按照音频还是视频进行seek操作,只需要按照其中一个流进行seek即可,不需要分别对音视频流进行seek操作
  • seek操作刷新:在进行seek操作之后,正常播放之前需要将编解码的内部缓冲区进行刷新,同时也要将自行控制的缓冲区内缓存的音视频清零

按视频流seek

首先定义视频流控制结构:

typedef struct __VideoCtrlStruct
{
   
   
	AVFormatContext	*pFormatCtx;
	AVStream 		*pStream;
	AVCodec			*pCodec;
	AVCodecContext	*pCodecCtx;
	SwsContext 		*pConvertCtx;
	AVFrame			*pVideoFrame, *pFrameYUV;
	unsigned char 	*pVideoOutBuffer;
	int 			VideoIndex;
	int 			VideoCnt;
	int 			RefreshTime;
	int screen_w,screen_h;
	SDL_Window *screen;
	SDL_Renderer* sdlRenderer;
	SDL_Texture* sdlTexture;
	SDL_Rect sdlRect;
	SDL_Thread *video_tid;

	sem_t frame_put;
	sem_t video_refresh;
	PacketArrayStruct Video;
}VideoCtrlStruct;

根据seek操作的要点,其中seek时间大小我们可以自行定义,这里暂定为3S

  • CurVideoDts:记录视频流当前的DTS
  • seek的时间单位:VideoCtrl.pStream->time_base就是视频流的时间单位,
  • 前进:int64_t DstVideoDts = CurVideoDts + (int64_t) ( 3 / av_q2d(VideoCtrl.pStream->time_base));
  • 后退:int64_t DstVideoDts = CurVideoDts - (int64_t) ( 3 / av_q2d(VideoCtrl.pStream->time_base));
  • 因为pStream->time_base结构表示一个分数数据,而视频流中的DTS与PTS时间都是以time_base为时间单位,因此需要将seek时间大小转换为以time_base为时间单位的大小
  • seek操作:ret = av_seek_frame(pFormatCtx, VideoIndex, DstVideoDts, AVSEEK_FLAG_FRAME);向后seek是需要在flag参数中加上AVSEEK_FLAG_BACKWARD
  • 缓冲区刷新:avcodec_flush_buffers(VideoCtrl.pCodecCtx);
  • 因为time_base分数结构中,num基本都为1,因此为了减少浮点数乘除法,可以直接乘以time_base的分母,见以下代码实例

按音频流seek

音频流控制结构

typedef struct __AudioCtrlStruct
{
   
   
	AVFormatContext	*pFormatCtx;
	AVStream 		*pStream;
	AVCodec			*pCodec;
	AVCodecContext	*pCodecCtx;
	SwrContext 		*pConvertCtx;

	Uint8  	*audio_chunk;
	Sint32  audio_len;
	Uint8  	*audio_pos;
	int 	AudioIndex;
	int 	AudioCnt;
	uint64_t AudioOutChannelLayout;
	int out_nb_samples;				//nb_samples: AAC-1024 MP3-1152
	AVSampleFormat out_sample_fmt;
	int out_sample_rate;
	int out_channels;
	int out_buffer_size;
	unsigned char* pAudioOutBuffer;

	sem_t frame_put;
	sem_t frame_get;

	PacketArrayStruct 	Audio;
}AudioCtrlStruct;

根据seek操作的要点,其中seek时间大小我们可以自行定义,这里暂定为3S

  • CurAudioDts:记录视频流当前的DTS
  • seek的时间单位:AudioCtrl.pStream->time_base就是视频流的时间单位,
  • 前进:int64_t DstAudioDts = CurAudioDts + (int64_t) ( 3 / av_q2d(AudioCtrl.pStream->time_base));
  • 后退:int64_t DstAudioDts = CurAudioDts - (int64_t) ( 3 / av_q2d(AudioCtrl.pStream->time_base));
  • 因为pStream->time_base结构表示一个分数数据,而视频流中的DTS与PTS时间都是以time_base为时间单位,因此需要将seek时间大小转换为以time_base为时间单位的大小
    • seek操作:ret = av_seek_frame(pFormatCtx, AudioIndex, DstAudioDts, AVSEEK_FLAG_FRAME);向后seek是需要在flag参数中加上AVSEEK_FLAG_BACKWARD
  • 缓冲区刷新:avcodec_flush_buffers(AudioCtrl.pCodecCtx);

代码实现

此代码用左右方向键表示前进与后退,并且在文件播放完毕时,重头开始播放,使用音频流进行seek操作

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define __STDC_CONSTANT_MACROS

#ifdef __cplusplus
extern "C"
{
   
   
#endif
#include <libavutil/time.h>
#include <libavutil/imgutils.h>
#include <libavutil/mathematics.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavdevice/avdevice.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>

#include <errno.h>

#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <semaphore.h>

#ifdef __cplusplus
};
#endif


#define MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio

#define PACKET_ARRAY_SIZE			(60)
typedef struct __PacketStruct
{
   
   
	AVPacket Packet;
	int64_t dts;
	int64_t pts;
	int state;
}PacketStruct;

typedef struct
{
   
   
	unsigned int rIndex;
	unsigned int wIndex;
	PacketStruct PacketArray[PACKET_ARRAY_SIZE];
}PacketArrayStruct;

typedef struct __AudioCtrlStruct
{
   
   
	AVFormatContext	*pFormatCtx;
	AVStream 		*pStream;
	AVCodec			*pCodec;
	AVCodecContext	*pCodecCtx;
	SwrContext 		*pConvertCtx;

	Uint8  	*audio_chunk;
	Sint32  audio_len;
	Uint8  	*audio_pos;
	int 	AudioIndex;
	int 	AudioCnt;
	uint64_t AudioOutChannelLayout;
	int out_nb_samples;				//nb_samples: AAC-1024 MP3-1152
	AVSampleFormat out_sample_fmt;
	int out_sample_rate;
	int out_channels;
	int out_buffer_size;
	unsigned char* pAudioOutBuffer;

	sem_t frame_put;
	sem_t frame_get;

	PacketArrayStruct 	Audio;
}AudioCtrlStruct;


typedef struct __VideoCtrlStruct
{
   
   
	AVFormatContext	*pFormatCtx;
	AVStream 		*pStream;
	AVCodec			*pCodec;
	AVCodecContext	*pCodecCtx;
	SwsContext 		*pConvertCtx;
	AVFrame			*pVideoFrame, *pFrameYUV;
	unsigned char 	*pVideoOutBuffer;
	int 			VideoIndex;
	int 			VideoCnt;
	int 			RefreshTime;
	int screen_w,screen_h;
	SDL_Window *screen;
	SDL_Renderer* sdlRenderer;
	SDL_Texture* sdlTexture;
	SDL_Rect sdlRect;
	SDL_Thread *video_tid;

	sem_t frame_put;
	sem_t video_refresh;
	PacketArrayStruct Video;
}VideoCtrlStruct;


//Refresh Event
#define SFM_REFRESH_VIDEO_EVENT  	(SDL_USEREVENT + 1)
#define SFM_REFRESH_AUDIO_EVENT  	(SDL_USEREVENT + 2)
#define SFM_BREAK_EVENT  			(SDL_USEREVENT + 3)

int thread_exit = 0;
int thread_pause = 0;
int audio_pause = 0;		//音频播放是否暂停,1表示暂停,0表示在播放
int video_pause = 0;		//视频播放是否暂停,1表示暂停,0表示在播放
SDL_Keycode CurKeyCode;		//记录按下的是前进还是后退键,即小键盘的左右方向键,右表示前进,左表示后退
int CurKeyProcess;			//按键seek操作是否已经被处理,0表示未处理,1表示已经处理
int64_t CurVideoDts;	//记录当前播放的视频流Packet的DTS
int64_t CurVideoPts;	//记录当前播放的视频流Packet的DTS

int64_t CurAudioDts;	//记录当前播放的音频流Packet的DTS
int64_t CurAudioPts;	//记录当前播放的音频流Packet的DTS

int64_t DstAudioDts;	//进行seek操作时,计算后的目标音频流的DTS
int64_t DstAudioPts;	//进行seek操作时,计算后的目标音频流的PTS
int64_t DstVideoDts;	//进行seek操作时,计算后的目标视频流的DTS
int64_t DstVideoPts;	//进行seek操作时,计算后的目标视频流的DTS

VideoCtrlStruct VideoCtrl;
AudioCtrlStruct AudioCtrl;
//video time_base.num:1, time_base.den:16, avg_frame_rate.num:8, avg_frame_rate.den:1
//audio time_base.num:1, time_base.den:48000, avg_frame_rate.num:0, avg_frame_rate.den:0
int IsPacketArrayFull(PacketArrayStruct* p)
{
   
   
	int i = 0;
	i = p->wIndex % PACKET_ARRAY_SIZE;
	if(p->PacketArray[i].state != 0) return 1;

	return 0;
}

int IsPacketArrayEmpty(PacketArrayStruct* p)
{
   
   
	int i = 0;
	i = p->rIndex % PACKET_ARRAY_SIZE;
	if(p->PacketArray[i].state == 0) return 1;

	return 0;
}

int PacketArrayClear(PacketArrayStruct* p)
{
   
   
	int i = 0;
	for(i = 0; i < PACKET_ARRAY_SIZE; i++)
	{
   
   
		if(p->PacketArray[i].state != 0)
		{
   
   
			av_packet_unref(&p->PacketArray[i].Packet);
			p->PacketArray[i].state = 0;
		}
	}
	p->rIndex = 0;
	p->wIndex = 0;

	return 0;
}

int SDL_event_thread(void *opaque)
{
   
   
	SDL_Event event;

	while(1)
	{
   
   
		SDL_WaitEvent(&event);
		if(event.type == SDL_KEYDOWN)
		{
   
   
			//Pause
			if(event.key.keysym.sym 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值