一个简单的mp4播放器
一直想写一个完整可用的播放器,趁着五一休假几天终于有时间手搓一个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播放器相关推荐
- 如何使用aframe.js构建一个简单的VR播放器
在当今这个信息化的时代,虚拟现实(VR)已经开始逐渐成为一种新的生活方式.作为一名前端开发工程师,在学习和探索VR技术方面,aframe.js是一个非常有趣和有用的工具.在本文中,我将介绍如何使用af ...
- GStreamer 编写一个简单的MP3播放器
本文介绍如何使用GStreamer 编写一个简单的MP3播放器. 1,需要使用mad解码插件,因此需要先安装gstreamer0.10-plugins-ugly 2,编写mp3播放器 下面来看看如何利 ...
- 自制一个简单的音乐播放器
这两天刚学完了contentprovider和service组件,就综合下所学的,自制了一个简单的音乐播放器. 代码如下: 主activity代码 public class MainActivity ...
- 用Qt写一个简单的音乐播放器(三):增加界面(播放跳转与音量控制)
一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...
- 用Qt写一个简单的音乐播放器(六):显示歌词(正则表达式)
一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...
- 用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐
一.前言 QMediaplayer可以用于解析音频文件和视频文件,继承自QMediaObject,涉及到的对象为QMediaContent.QMediaObject可以提供关于媒体内容的接入,通过UR ...
- 用Qt写一个简单的音乐播放器(五):歌曲播放时间显示
一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...
- Android开发做一个简单的音乐播放器
Android开发如何做一个简单的音乐播放器,首先我们先要知道用到的知识点有哪些. 1.MediaPlayer:可以播放本地资源.sd卡内存资源以及网络uri资源,在这里我们播放sd卡上的音乐资源. ...
- 用Qt写一个简单的音乐播放器(七):界面美化(QSS样式表)
一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...
最新文章
- 力扣(LeetCode)刷题,简单+中等题(第29期)
- 【Linux入门到精通系列讲解】内存管理malloc和free函数
- 学python需要什么文化基础-中国大学MOOC的APP2020Python编程基础答案
- docker入门与实践之【04-使用dockerfile定制镜像】
- 修改了WINCE自带的驱动程序后如何编译
- python if try except_python try except
- efinance获取基金、股票、债券、期货K线数据
- arm11搭建Linux平台,armlinux软硬件平台搭建.doc
- python使用queue和线程池
- 史上最全的springboot导出pdf文件
- vue3.0项目引入高德地图
- 特征工程之数据预处理与可视化
- 从 Next.js 看企业级框架的 SSR 支持
- 解读:政务信息资源整合共享难点分析及对策研究
- Harmonic Number LightOJ - 1234(暴力分段打表 / 欧拉爷爷的O(1))
- echarts 中国地图标注所在点
- OkGo第三方框架的上传与下载+Glide图片加载器
- 2021年低压电工最新解析及低压电工考试技巧
- 代码随想录训练营day42
- 泛海微FS68001 SOP8封装 无线充单片机IC领夹式麦克风加充电方案芯片