FFmpeg+SDL+Qt 构建简单视频播放器

谁践踏了优雅 2023-10-13 11:18 141阅读 0赞

FFmpeg是一个音视频处理的开源库,提供了C接口用于音视频的编解码、封装、流处理。在本教程中主要利用FFmpeg对视频封装文件进行解封装,解码。

SDL是音视频播放和渲染的一个开源库,主要利用它进行视频渲染和音频播放。

Qt主要用于写播放器简单UI,以及播放暂停音视频选择按钮。

首先要了解音视频的一些基本知识,平常所说的MP4,mkv文件是一个音视频封装文件,里面一般包含音频视频两条流,每条流存储着编码信息以及展示时间基等信息。

播放器的整个流程如下图,首先从服务器拉流,或者从本地打开视频文件(用FFmpeg处理时接口都一样,只是提供的地址不一样)。打开之后进行解封装,每次读取一个packet,是音频则解码成音频帧,视频则解码成视频帧。再对音视频帧做一个转换,转换成可以播放和渲染的格式,进行播放。
ee14541e00de48d1ad73e7f862d1745e.png

程序的整个流程如下图。首先需要写好Qt UI,对FFmpeg进行初始化,对SDL进行初始化,然后打开输入源,同时打开相应的解码器,设置播放格式,根据输入源格式和播放格式创建转换器。再创建音视频播放线程,接着就可以开始读文件了,每次读到一个packet就根据stream_index判断是否为音视频,是音频则放入音频解码器解码成音频帧,再转换格式,送入音频缓存中,是视频则放入视频解码器并转换。再继续读下一packet.音视频播放则由不同的线程完成,所以还需要时间同步。一般而言以音频为准,视频根据音频的播放时间进行渲染。
dab55409252a41c4bb25316b9119a077.png

所以以下就分五个部分讲解整个流程,分别是初始化,解封装,解码,转换,播放。整个工程用Qt作为框架,需要创建qt app。

初始化

FFmpeg网络初始化,因为我们的程序是从rtmp服务器拉流,所以需要对网络做一个初始化。

  1. avformat_network_init();

SDL初始化,初始化音频和视频模块以及时间模块。

  1. if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
  2. TestNotNull(NULL, SDL_GetError());
  3. }

以及画好界面窗口和按钮之后将其组织起来并设置槽函数。

  1. ui.audio->setCheckState(Qt::Checked);
  2. ui.video->setCheckState(Qt::Checked);
  3. //水平布局,控制按钮
  4. QBoxLayout *ctlLayout = new QHBoxLayout;
  5. ctlLayout->addWidget(ui.pauseBtn, 5, Qt::AlignCenter);
  6. ctlLayout->addWidget(ui.video);
  7. ctlLayout->addWidget(ui.audio);
  8. //垂直布局:视频播放器、进度条、控制按钮布局
  9. QBoxLayout *mainLayout = new QVBoxLayout;
  10. mainLayout->addWidget(videoWidget);
  11. mainLayout->addLayout(ctlLayout);
  12. //设置布局
  13. mainWindowWidget->setLayout(mainLayout);
  14. //设置槽函数
  15. connect(ui.pauseBtn, SIGNAL(clicked()), this, SLOT(ChangePlay()));
  16. connect(ui.audio, SIGNAL(stateChanged(int)), this, SLOT(ChangeAudio()));
  17. connect(ui.video, SIGNAL(stateChanged(int)), this, SLOT(ChangeVideo()));

解封装

解封装之前需要打开输入,并找到相应的音视频流的index,用于以后判断从文件读取的packet属于哪个流。

  1. //打开输入
  2. if (avformat_open_input(&pFmtCtx, fileName.c_str(), NULL, NULL) < 0) {
  3. TestNotNull(NULL, "Failed to open input file: " + fileName);
  4. }
  5. //找编码信息
  6. if (avformat_find_stream_info(pFmtCtx, NULL) < 0) {
  7. TestNotNull(NULL, "Failed to find stream information.");
  8. }
  9. //设置音视频的 index
  10. for (int i = 0; i < pFmtCtx->nb_streams; i++) {
  11. if (pFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
  12. aIndex = i;
  13. }//for
  14. //设置视频索引
  15. for (int i = 0; i < pFmtCtx->nb_streams; i++) {
  16. if (pFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
  17. vIndex = i;
  18. }

打开音视频解码器,一般来讲是重新申请一个解码器CodecContext,但是官网给的很多例子也是直接指向stream里面的解码器并将其打开,所以我也这么做了。

  1. //打开音频解码器
  2. pAudioCdcCtx = pFmtCtx->streams[aIndex]->codec;
  3. AVCodec* pAudioCdc = avcodec_find_decoder(pAudioCdcCtx->codec_id);
  4. TestNotNull(pAudioCdc, "Failed to find audio decoder.");
  5. if (avcodec_open2(pAudioCdcCtx, pAudioCdc, NULL) < 0) {
  6. TestNotNull(NULL, "Failed to open audio codec.");
  7. }
  8. //打开视频解码器
  9. pVideoCdcCtx = pFmtCtx->streams[vIndex]->codec;
  10. AVCodec* pVideoCdc = avcodec_find_decoder(pVideoCdcCtx->codec_id);
  11. TestNotNull(pVideoCdc, "Failed to find decoder.");
  12. if (avcodec_open2(pVideoCdcCtx, pVideoCdc, NULL) < 0) {
  13. TestNotNull(NULL, "Failed to open codec.");
  14. }

然后调用av_read_frame()函数读取文件的一个packet.

  1. AVPacket pkt = { 0 };
  2. av_init_packet(&pkt);
  3. av_read_frame(pFmtCtx, &pkt);

转换

视频帧主要是转换一帧视频像素的编码格式以及长度和宽度。

  1. //设置
  2. pSwsCtx = sws_getContext(pIn->width,
  3. pIn->height,
  4. (AVPixelFormat)pOut->format,
  5. pOut->width,
  6. pOut->height,
  7. (AVPixelFormat)pOut->format,
  8. SWS_BILINEAR, NULL, NULL, NULL);
  9. ....
  10. //转换
  11. sws_scale(pSwsCtx, (uint8_t const* const*)pInFrm->data,
  12. pInFrm->linesize, 0, VIDEO_HEIGH, pFrmYUV->data, pFrmYUV->linesize);

音频帧主要转换音频的采集格式声道信息以及采样率。

  1. //转换设置
  2. pSwrCtx = swr_alloc_set_opts(NULL,
  3. pOutPara->channel_layout,
  4. (AVSampleFormat)pOutPara->format,
  5. pOutPara->sample_rate,
  6. pInPara->channel_layout,
  7. (AVSampleFormat)pInPara->format,
  8. pInPara->sample_rate,
  9. 0, NULL);
  10. .....
  11. //转换
  12. swr_convert(pSwrCtx, &(pBuff->dataPos), pBuff->dataLen,
  13. (const uint8_t**)pFrm->data, pFrm->nb_samples)

播放

音频播放比较复杂。首先需要设置一个SDL_AudioSpec结构体,里面会传入一个回调函数,在用SDL_OpenAudio()函数打开播放器时,当音频播放完数据时就会去调用这个回调函数,我的回调函数如下:

  1. void CallBackFunc(void * userdata, Uint8 * stream, int len)
  2. {
  3. SDL_memset(stream, 0, len);
  4. PlayBuffer* playData = (PlayBuffer*)userdata;
  5. if (playData->dataLen == 0)
  6. return;
  7. /* Mix as much data as possible */
  8. len = (len > playData->dataLen ? playData->dataLen : len);
  9. SDL_MixAudio(stream, playData->dataPos, len, SDL_MIX_MAXVOLUME);
  10. playData->dataPos += len;
  11. playData->dataLen -= len;
  12. }

此外我还设置了一个线程,一个不断从音频缓存队列读取一段数据,并改变userdata参数,使得userdata 的指针不断更新,(实际上这个线程的工作可以放到回调函数去做,让回调函数自己从缓存队列里面读取数据)。这个线程代码如下:

  1. void Player::PlayAudio()
  2. {
  3. while (true)
  4. {
  5. if (ctrl.GetPlayStatus() && ctrl.GetAudioStatus()) {
  6. SDL_PauseAudio(0);
  7. aPlay.WaitToPlay();
  8. }
  9. else {
  10. SDL_PauseAudio(1);
  11. }
  12. }
  13. }

视频播放相对来说逻辑比较好理解,首先设置窗口,不断地从缓存队列里面读取一帧,根据音频播放时间来决定是否播放。

设置窗口代码如下,需要说明的时创建窗口和渲染需要在同一个线程,否则渲染时会出现Reset() INVALIDCALL的错误。winid是qt窗口的id,这样SDL窗口就会嵌入到Qt窗口里面。

  1. int VideoPlay::SDLInit(int w, int h, void* winid)
  2. {
  3. //设置渲染长方形
  4. pScreen = SDL_CreateWindowFrom(winid);
  5. TestNotNull(pScreen, SDL_GetError());
  6. //创建窗口
  7. pRender = SDL_CreateRenderer(pScreen, -1,
  8. SDL_RENDERER_ACCELERATED);
  9. TestNotNull(pRender, SDL_GetError());
  10. //SDL_HINT_VIDEO_WINDOW_SHARE_PIXEL_FORMAT
  11. pTexture = SDL_CreateTexture(pRender,
  12. SDL_PIXELFORMAT_YV12,
  13. SDL_TEXTUREACCESS_STREAMING,
  14. rect.w, rect.h);
  15. TestNotNull(pTexture, SDL_GetError());
  16. return 0;
  17. }

视频播放线程函数:

  1. void Player::PlayVideo(void* p)
  2. {
  3. vPlay.SDLInit(0,0,p);
  4. while (true) {
  5. //获取音频播放时间
  6. int64_t ts = aPlay.GetCurrentTime();
  7. if (ctrl.GetPlayStatus() && ctrl.GetVideoStatus()) {
  8. //控制器允许播放
  9. vPlay.Render(ts);
  10. }
  11. else {
  12. //显示黑屏
  13. vPlay.BlackScreen();
  14. }
  15. }
  16. }

渲染函数:

  1. int VideoPlay::Render(int64_t ts) {
  2. //从队列中获取一帧
  3. if (pFrame == NULL) {
  4. pFrame = (AVFrame*)cache->ComsumeElem();
  5. }
  6. int64_t diff = pFrame->pts/VIDEO_TIME_BASE - ts/AUDIO_TIME_BASE;
  7. if (diff< -25 || diff > 1000){
  8. //晚了25毫秒以上,或者早于100毫秒丢弃
  9. printf("drop frame, diff = %d\n", diff);
  10. av_frame_free(&pFrame);
  11. pFrame = NULL;
  12. return 0;
  13. }
  14. if (diff > 20)
  15. {
  16. Uint32 delay = diff - 20;
  17. //printf("没到时间, sleep %d ms\n", delay);
  18. SDL_Delay(1);
  19. return 0;
  20. }
  21. //刷新渲染层
  22. int ret = SDL_UpdateYUVTexture(pTexture, &rect,
  23. pFrame->data[0], pFrame->linesize[0],
  24. pFrame->data[1], pFrame->linesize[1],
  25. pFrame->data[2], pFrame->linesize[2]);
  26. if (ret < 0) {
  27. TestNotNull(NULL, "texture is not valid.");
  28. }
  29. //清除、复制、播放
  30. //if (SDL_RenderClear(pRender) < 0) {
  31. // TestNotNull(NULL, SDL_GetError());
  32. //}
  33. if (SDL_RenderCopy(pRender, pTexture, &rect, NULL) < 0) {
  34. TestNotNull(NULL, SDL_GetError());
  35. }
  36. SDL_RenderPresent(pRender);
  37. printf("play video.\n");
  38. av_frame_free(&pFrame);
  39. pFrame = NULL;
  40. return 0;
  41. }

发表评论

表情:
评论列表 (有 0 条评论,141人围观)

还没有评论,来说两句吧...

相关阅读