一直想写一个完整可用的播放器,趁着五一休假几天终于有时间手搓一个mp4播放器,也算完成了自己的一个心愿。

出于简单考虑,这个播放器尽量简化流程,省略细节,也忽略了一些异常处理,目的是让我们快速了解掌握一个mp4播放器的主要流程和技术框架,适合学习使用。

这个播放器是在windows下实现,主要用到以下技术:

1、使用ffmpeg解封装mp4文件,解码视频帧和音频帧。

2、使用windows自带vfw库渲染视频。

3、使用SDL库渲染音频(本来想使用windows原生的接口来渲染,研究了一波没明白怎么做,还是妥协了,先用SDL)

基本流程如下:

打开mp4文件(avformat_open_input)-> 分别创建音频和视频的CodecContext -> 创建音视频解码器 -> 创建解封装解码线程(DemuxerThread)和渲染线程(PlayerThread)

DemuxerThread线程的工作:

从mp4文件中读取一个媒体包(av_read_frame)-> 判断媒体包的类型(音频还是视频)-> 送解码(avcodec_send_packet)-> 获取解码后的帧(avcodec_receive_frame)-> 将解码帧放入缓冲队列

PlayerThread线程的工作:

分别从音频和视频缓冲队列中读取一帧 -> 判断该帧是否到了渲染时机(播放控制逻辑)-> 如果到了渲染时机则渲染该帧 -> 从队列中删除该帧

音频渲染流程:

初始化SDL(程序初始化时执行,不在渲染线程中)-> 首帧渲染时打开SDL音频(SDL_OpenAudio) -> 重采样 -> 将重采样后的音频帧放入pending队列中(音频是通过系统拉帧,不能主动塞帧)

系统拉帧(调用callback函数)-> 从pending队列中获取一帧 -> 将音频数据拷贝到系统缓冲区 -> 从pending队列中删除该帧 -> 完成音频渲染

视频渲染流程:

初始化vfw库(程序初始化时执行,不在渲染线程中)-> 颜色空间转换(yuv转rgb)-> 按画布尺寸和视频帧尺寸计算出目标渲染尺寸(等比例缩放)-> 渲染到画布 -> 完成视频渲染

代码分析

1、ffmpeg初始化和探测文件相关前期逻辑

void Cmp4_playerDlg::OnBnClickedPlay()
{char filename[MAX_PATH] = { 0 };strncpy_s(filename, m_strPlayUrl.GetString(), MAX_PATH - 1);AVInputFormat *inFmt = av_find_input_format("mp4");m_fmtCtx = avformat_alloc_context();AVDictionary *format_opts = NULL;int ret = avformat_open_input(&m_fmtCtx, filename, inFmt, &format_opts);if (ret < 0){MessageBox("avformat_open_input failed");return;}m_vCodecCtx = avcodec_alloc_context3(NULL);m_aCodecCtx = avcodec_alloc_context3(NULL);if (!m_vCodecCtx || !m_aCodecCtx){MessageBox("avcodec_alloc_context3 failed");return;}// 为了简化逻辑,需要同时有音视频,否则接下来的逻辑需要做更多的异常判断m_vstream_index = av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);m_astream_index = av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);if (m_vstream_index < 0 || m_astream_index < 0){MessageBox("Cannot find audio or video index");return;}ret = avcodec_parameters_to_context(m_vCodecCtx, m_fmtCtx->streams[m_vstream_index]->codecpar);if (ret < 0){MessageBox("[video] avcodec_parameters_to_context failed");return;}ret = avcodec_parameters_to_context(m_aCodecCtx, m_fmtCtx->streams[m_astream_index]->codecpar);if (ret < 0){MessageBox("[audio] avcodec_parameters_to_context failed");return;}av_codec_set_pkt_timebase(m_vCodecCtx, m_fmtCtx->streams[m_vstream_index]->time_base);av_codec_set_pkt_timebase(m_aCodecCtx, m_fmtCtx->streams[m_astream_index]->time_base);AVCodec *vCodec = avcodec_find_decoder(m_vCodecCtx->codec_id);AVCodec *aCodec = avcodec_find_decoder(m_aCodecCtx->codec_id);if (vCodec != NULL){TRACE("video codec: %s\n", vCodec->name);m_vCodecCtx->codec_id = vCodec->id;}if (aCodec != NULL){TRACE("audio codec: %s\n", aCodec->name);m_aCodecCtx->codec_id = aCodec->id;}if ((ret = avcodec_open2(m_vCodecCtx, vCodec, NULL)) < 0){MessageBox("[video] avcodec_open2 failed");return;}if ((ret = avcodec_open2(m_aCodecCtx, aCodec, NULL)) < 0){MessageBox("[audio] avcodec_open2 failed");return;}m_hThreadEvent[0] = CreateEvent(NULL, TRUE, FALSE, NULL);m_hThreadEvent[1] = CreateEvent(NULL, TRUE, FALSE, NULL);m_bStopThread = false;m_bDemuxing = true;m_bPlaying = true;// 创建解封装和解码线程unsigned int demuxerThreadAddr;m_dwDemuxerThread = _beginthreadex(NULL,            // Security0,               // Stack sizeDemuxerProc,       // Function addressthis,            // Argument0,               // Init flag&demuxerThreadAddr);    // Thread addressif (!m_dwDemuxerThread){MessageBox("Could not create demuxer thread");return;}// 创建播放线程unsigned int playThreadAddr;m_dwPlayThread = _beginthreadex(NULL,            // Security0,               // Stack sizePlayProc,      // Function addressthis,            // Argument0,               // Init flag&playThreadAddr);   // Thread addressif (!m_dwPlayThread){MessageBox("Could not create play thread");}GetDlgItem(IDC_PLAY)->EnableWindow(FALSE);GetDlgItem(IDC_STOP)->EnableWindow(TRUE);
}

2、解封装&解码线程

void Cmp4_playerDlg::DemuxerWorker()
{int ret;bool isBufferFull;const int maxDecodedListSize = 100; // 解码队列中的音视频帧最大个数AVPacket pkt1, *pkt = &pkt1;while (av_read_frame(m_fmtCtx, pkt) >= 0){if (m_bStopThread){break;}isBufferFull = false;// 时间基转换AVStream *pStream = m_fmtCtx->streams[pkt->stream_index];pkt->pts = av_rescale_q(pkt->pts, pStream->time_base, AvTimeBaseQ()) / 1000;pkt->dts = av_rescale_q(pkt->dts, pStream->time_base, AvTimeBaseQ()) / 1000;// 视频if (pkt->stream_index == m_vstream_index){if (avcodec_send_packet(m_vCodecCtx, pkt) == AVERROR(EAGAIN)){TRACE("[video] avcodec_send_packet failed\n");}AVFrame *frame = av_frame_alloc();ret = avcodec_receive_frame(m_vCodecCtx, frame);if (ret >= 0){TRACE("[video] receive one video frame\n");std::lock_guard<std::mutex> lock(m_dListMtx);m_vdFrameList.push_back(frame);if (m_vdFrameList.size() > maxDecodedListSize && m_adFrameList.size() > maxDecodedListSize){isBufferFull = true;}}else{av_frame_free(&frame);}}else if (pkt->stream_index == m_astream_index){if (avcodec_send_packet(m_aCodecCtx, pkt) == AVERROR(EAGAIN)){TRACE("[audio] avcodec_send_packet failed\n");}AVFrame *frame = av_frame_alloc();ret = avcodec_receive_frame(m_aCodecCtx, frame);if (ret >= 0){TRACE("[audio] receive one audio frame\n");std::lock_guard<std::mutex> lock(m_dListMtx);m_adFrameList.push_back(frame);if (m_adFrameList.size() > maxDecodedListSize && m_vdFrameList.size() > maxDecodedListSize){isBufferFull = true;}}else{av_frame_free(&frame);}}else{// subtitle ?}if (isBufferFull){Sleep(100);}}SetEvent(m_hThreadEvent[0]);m_bDemuxing = false;
}

3、播放线程

void Cmp4_playerDlg::PlayerWorker()
{m_firstFramePts = 0;m_firstFrameTick = 0;while (true){if (m_bStopThread){break;}// 获取队列中的第一帧,如果该帧到了播放时机则进行渲染,然后删除队列中的帧// 如果未到播放时机则等下次peekAVFrame* aframe = peekOneAudioFrame();AVFrame* vframe = peekOneVideoFrame();if (renderOneAudioFrame(aframe)){popOneAudioFrame();}if (renderOneVideoFrame(vframe)){popOneVideoFrame();}Sleep(10);}SetEvent(m_hThreadEvent[1]);m_bPlaying = false;
}

播放控制逻辑

// 播放逻辑控制,按音视频中的pts来播放
bool Cmp4_playerDlg::isTimeToRender(int64_t pts)
{if (0 == m_firstFramePts){return true; // 首帧直接播放}int64_t ptsDelta = pts - m_firstFramePts;int64_t tickDelta = getTickCount() - m_firstFrameTick;if (tickDelta >= ptsDelta){return true;}return false;
}

4、音频播放

void Cmp4_playerDlg::playAudio(AVFrame *frame)
{if (m_firstPlayAudio){openSdlAudio(frame->sample_rate, frame->channels, frame->nb_samples);m_firstPlayAudio = false;}// 重采样if (m_audioSwrCtx == NULL){m_audioSwrCtx = swr_alloc_set_opts(m_audioSwrCtx,frame->channel_layout,AV_SAMPLE_FMT_S16,frame->sample_rate,frame->channel_layout,(AVSampleFormat)frame->format,frame->sample_rate,0,NULL);swr_init(m_audioSwrCtx);}int dataLen = frame->channels * frame->nb_samples * 2;ARFrame* rframe = new ARFrame(dataLen);swr_convert(m_audioSwrCtx, &rframe->m_data, frame->nb_samples, (const uint8_t **)frame->data, frame->nb_samples);{std::lock_guard<std::mutex> lock(m_apListMtx);m_aPendingList.push_back(rframe);// TRACE("push one audio frame to render list\n");}
}

系统拉音频帧,塞帧到系统缓冲

void Cmp4_playerDlg::innerFillAudio(Uint8* stream, int len)
{SDL_memset(stream, 0, len);{std::lock_guard<std::mutex> lock(m_apListMtx);if (m_aPendingList.empty()){// TRACE("audio pull empty\n");return;}ARFrame* rframe = m_aPendingList.front();m_aPendingList.pop_front();int copySize = min(len, (int)rframe->m_length);SDL_MixAudioFormat(stream, rframe->m_data, AUDIO_S16SYS, copySize, 100);delete rframe;// TRACE("fill one audio frame, len %d\n", copySize);}
}

5、视频播放

void Cmp4_playerDlg::playVideo(AVFrame *frame)
{if (m_picBytes == 0){m_picBytes = avpicture_get_size(AV_PIX_FMT_BGR24, m_vCodecCtx->width, m_vCodecCtx->height);m_picBuf = new uint8_t[m_picBytes];m_frameRGB = av_frame_alloc();avpicture_fill((AVPicture *)m_frameRGB, m_picBuf, AV_PIX_FMT_BGR24,m_vCodecCtx->width, m_vCodecCtx->height);}if (!m_imgCtx){m_imgCtx = sws_getContext(m_vCodecCtx->width, m_vCodecCtx->height,m_vCodecCtx->pix_fmt, m_vCodecCtx->width,m_vCodecCtx->height, AV_PIX_FMT_BGR24,SWS_BICUBIC, NULL, NULL, NULL);}frame->data[0] += frame->linesize[0] * (m_vCodecCtx->height - 1);frame->linesize[0] *= -1;frame->data[1] += frame->linesize[1] * (m_vCodecCtx->height / 2 - 1);frame->linesize[1] *= -1;frame->data[2] += frame->linesize[2] * (m_vCodecCtx->height / 2 - 1);frame->linesize[2] *= -1;sws_scale(m_imgCtx, (const uint8_t* const*)frame->data, frame->linesize,0, m_vCodecCtx->height, m_frameRGB->data, m_frameRGB->linesize);displayPicture(m_frameRGB->data[0], m_vCodecCtx->width, m_vCodecCtx->height);
}void Cmp4_playerDlg::displayPicture(uint8_t* data, int width, int height)
{CWnd* PlayWnd = GetDlgItem(IDC_VIDEO_CANVAS);HDC hdc = PlayWnd->GetDC()->GetSafeHdc();updateDisplayRect(width, height);init_bm_head(width, height);DrawDibDraw(m_DrawDib,hdc,m_dspRc.left,m_dspRc.top,m_dspRc.Width(),         // 按比例缩放尺寸m_dspRc.Height(),&m_bm_info.bmiHeader,(void*)data,0,0,width,height,0);
}

计算渲染尺寸

// 根据画布尺寸和视频的分辨率,计算出实际渲染尺寸(按原视频比例缩放)
void Cmp4_playerDlg::updateDisplayRect(int frame_width, int frame_height)
{CRect canvasRc;GetDlgItem(IDC_VIDEO_CANVAS)->GetClientRect(&canvasRc);if (m_lastFrameWidth == frame_width && m_lastFrameHeight == frame_height&& canvasRc.Width() == m_canvasWidth && canvasRc.Height() == m_canvasHeight){return;}m_lastFrameWidth = frame_width;m_lastFrameHeight = frame_height;m_canvasWidth = canvasRc.Width();m_canvasHeight = canvasRc.Height();double screen_ratio = (double)m_canvasWidth / m_canvasHeight;double pixel_ratio = (double)frame_width / frame_height;int dstX, dstY;int dstWidth, dstHeight;if (screen_ratio > pixel_ratio){dstHeight = m_canvasHeight;dstWidth = (int)(frame_width * ((double)dstHeight / frame_height));dstY = canvasRc.top;dstX = canvasRc.left + (m_canvasWidth - dstWidth) / 2;}else{dstWidth = m_canvasWidth;dstHeight = (int)(frame_height * ((double)dstWidth / frame_width));dstX = canvasRc.left;dstY = canvasRc.top + (m_canvasHeight - dstHeight) / 2;}m_dspRc.SetRect(dstX, dstY, dstX + dstWidth, dstY + dstHeight);
}

完整代码

media/player at main · ChriFang/media · GitHub

一个简单的mp4播放器相关推荐

  1. 如何使用aframe.js构建一个简单的VR播放器

    在当今这个信息化的时代,虚拟现实(VR)已经开始逐渐成为一种新的生活方式.作为一名前端开发工程师,在学习和探索VR技术方面,aframe.js是一个非常有趣和有用的工具.在本文中,我将介绍如何使用af ...

  2. GStreamer 编写一个简单的MP3播放器

    本文介绍如何使用GStreamer 编写一个简单的MP3播放器. 1,需要使用mad解码插件,因此需要先安装gstreamer0.10-plugins-ugly 2,编写mp3播放器 下面来看看如何利 ...

  3. 自制一个简单的音乐播放器

    这两天刚学完了contentprovider和service组件,就综合下所学的,自制了一个简单的音乐播放器. 代码如下: 主activity代码 public class MainActivity ...

  4. 用Qt写一个简单的音乐播放器(三):增加界面(播放跳转与音量控制)

    一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...

  5. 用Qt写一个简单的音乐播放器(六):显示歌词(正则表达式)

    一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...

  6. 用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐

    一.前言 QMediaplayer可以用于解析音频文件和视频文件,继承自QMediaObject,涉及到的对象为QMediaContent.QMediaObject可以提供关于媒体内容的接入,通过UR ...

  7. 用Qt写一个简单的音乐播放器(五):歌曲播放时间显示

    一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...

  8. Android开发做一个简单的音乐播放器

    Android开发如何做一个简单的音乐播放器,首先我们先要知道用到的知识点有哪些. 1.MediaPlayer:可以播放本地资源.sd卡内存资源以及网络uri资源,在这里我们播放sd卡上的音乐资源. ...

  9. 用Qt写一个简单的音乐播放器(七):界面美化(QSS样式表)

    一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...

最新文章

  1. 力扣(LeetCode)刷题,简单+中等题(第29期)
  2. 【Linux入门到精通系列讲解】内存管理malloc和free函数
  3. 学python需要什么文化基础-中国大学MOOC的APP2020Python编程基础答案
  4. docker入门与实践之【04-使用dockerfile定制镜像】
  5. 修改了WINCE自带的驱动程序后如何编译
  6. python if try except_python try except
  7. efinance获取基金、股票、债券、期货K线数据
  8. arm11搭建Linux平台,armlinux软硬件平台搭建.doc
  9. python使用queue和线程池
  10. 史上最全的springboot导出pdf文件
  11. vue3.0项目引入高德地图
  12. 特征工程之数据预处理与可视化
  13. 从 Next.js 看企业级框架的 SSR 支持
  14. 解读:政务信息资源整合共享难点分析及对策研究
  15. Harmonic Number LightOJ - 1234(暴力分段打表 / 欧拉爷爷的O(1))
  16. echarts 中国地图标注所在点
  17. OkGo第三方框架的上传与下载+Glide图片加载器
  18. 2021年低压电工最新解析及低压电工考试技巧
  19. 代码随想录训练营day42
  20. 泛海微FS68001 SOP8封装 无线充单片机IC领夹式麦克风加充电方案芯片

热门文章

  1. 基于GPRS远程开关和OneNET平台实现共享净水机控制
  2. 在局域网内开发 虚拟专用网络
  3. Go语言笔记—Go基础语法(2)
  4. 报错h is not defind
  5. Couldn't deal with it,bug!!!
  6. css特效实现表情包
  7. Java实现 蓝桥杯VIP 算法提高 打水问题
  8. 如何选择漏电保护器规格型号_家用漏电开关的型号和规格该怎么选
  9. 新手学习原画难吗?该从哪里开始学?
  10. flutter 发送验证码