一直想写一个完整可用的播放器,趁着五一休假几天终于有时间手搓一个mp4播放器,也算完成了自己的一个心愿。
出于简单考虑,这个播放器尽量简化流程,省略细节,也忽略了一些异常处理,目的是让我们快速了解掌握一个mp4播放器的主要流程和技术框架,适合学习使用。
1、使用ffmpeg解封装mp4文件,解码视频帧和音频帧。
2、使用windows自带vfw库渲染视频。
3、使用SDL库渲染音频(本来想使用windows原生的接口来渲染,研究了一波没明白怎么做,还是妥协了,先用SDL)
打开mp4文件(avformat_open_input)-> 分别创建音频和视频的CodecContext -> 创建音视频解码器 -> 创建解封装解码线程(DemuxerThread)和渲染线程(PlayerThread)
从mp4文件中读取一个媒体包(av_read_frame)-> 判断媒体包的类型(音频还是视频)-> 送解码(avcodec_send_packet)-> 获取解码后的帧(avcodec_receive_frame)-> 将解码帧放入缓冲队列
分别从音频和视频缓冲队列中读取一帧 -> 判断该帧是否到了渲染时机(播放控制逻辑)-> 如果到了渲染时机则渲染该帧 -> 从队列中删除该帧
初始化SDL(程序初始化时执行,不在渲染线程中)-> 首帧渲染时打开SDL音频(SDL_OpenAudio) -> 重采样 -> 将重采样后的音频帧放入pending队列中(音频是通过系统拉帧,不能主动塞帧)
系统拉帧(调用callback函数)-> 从pending队列中获取一帧 -> 将音频数据拷贝到系统缓冲区 -> 从pending队列中删除该帧 -> 完成音频渲染
初始化vfw库(程序初始化时执行,不在渲染线程中)-> 颜色空间转换(yuv转rgb)-> 按画布尺寸和视频帧尺寸计算出目标渲染尺寸(等比例缩放)-> 渲染到画布 -> 完成视频渲染
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, // Security
0, // Stack size
DemuxerProc, // Function address
this, // Argument
0, // Init flag
&demuxerThreadAddr); // Thread address
if (!m_dwDemuxerThread)
{
MessageBox("Could not create demuxer thread");
return;
}
// 创建播放线程
unsigned int playThreadAddr;
m_dwPlayThread = _beginthreadex(
NULL, // Security
0, // Stack size
PlayProc, // Function address
this, // Argument
0, // Init flag
&playThreadAddr); // Thread address
if (!m_dwPlayThread)
{
MessageBox("Could not create play thread");
}
GetDlgItem(IDC_PLAY)->EnableWindow(FALSE);
GetDlgItem(IDC_STOP)->EnableWindow(TRUE);
}
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 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 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;
}
void Cmp4_playerDlg::PlayerWorker()
{
m_firstFramePts = 0;
m_firstFrameTick = 0;
while (true)
{
if (m_bStopThread)
{
break;
}
// 获取队列中的第一帧,如果该帧到了播放时机则进行渲染,然后删除队列中的帧
// 如果未到播放时机则等下次peek
AVFrame* 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;
}
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 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 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);
}
}
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