一个简单的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,			// 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);
}

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;
		}

		// 获取队列中的第一帧,如果该帧到了播放时机则进行渲染,然后删除队列中的帧
		// 如果未到播放时机则等下次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;
}

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值