做一个中文的开源播放器,用于视频的开发的教学。欢迎大家参与、交流。
如果你需要商业开发,请联系QQ:16614119
代码位置:https://gitee.com/kl222/player
1.1 在mingwvs中编译ffmpeg
Ffmpeg用mingw进行编译,并安装到/usr/local下。
$ ./configure--enable-libx264 --enable-gpl --extra-cflags=-I/usr/local/include--disable-iconv --extra-ldflags=-L/usr/local/lib --disable-stripping--enable-debug --disable-optimizations --enable-shared
$ make install
从/Mingw/include中复制头文件stdint.h、inttypes.h到/usr/local/include下。删掉这两个头文件中的#include<_mingw.h>
这两个头文件是C99标准中的类型定义。Vc不完全支持C99,所以没有此两文件。
C:\MingW\lib\gcc\mingw32\4.8.1:libgcc位置
C:\MingW\msys\1.0\lib
C:\MingW\msys\1.0\local\lib:ffmpeg静态库位置
C:\MingW\msys\1.0\local\bin:ffmpeg动态库位置
Administrator@k-PC /usr/local/bin
$ ls *.lib
avcodec.lib avfilter.lib avutil.lib swresample.lib
avdevice.lib avformat.lib postproc.lib swscale.lib
C:\Mingw\lib:mingw库位置
/*静态库
#pragma comment( lib, "libavutil.a")
#pragma comment( lib, "libavcodec.a")
#pragma comment( lib, "libavformat.a")
#pragma comment( lib, "libavdevice.a")
#pragma comment( lib, "libpostproc.a")
#pragma comment( lib, "libswresample.a")
#pragma comment( lib, "libswscale.a")
//*/
//*动态库
#pragma comment( lib, "avformat.lib")
#pragma comment( lib, "avutil.lib")
#pragma comment( lib, "avcodec.lib")
#pragma comment( lib, "avdevice.lib")
#pragma comment( lib, "postproc.lib")
#pragma comment( lib, "swresample.lib")
#pragma comment( lib, "swscale.lib")
//*/
#pragma comment(lib,"libx264.a")
#pragma comment( lib, "libavicap32.a")
#pragma comment( lib, "libws2_32.a")
#pragma comment( lib, "libbz2.a")
#pragma comment( lib, "libz.a")
#pragma comment( lib, "libpsapi.a")
#pragma comment( lib, "libadvapi32.a")
#pragma comment( lib, "libshell32.a")
#pragma comment( lib, "libgcc.a")
#pragma comment( lib, "libmingwex.a")
把C:\MingW\msys\1.0\local\bin中的ffmpeg动态库复制到生成的执行文件相同的目录下。
Administrator@k-PC/usr/local/bin
$ ls *.dll
avcodec-55.dll* avfilter-4.dll* avutil-52.dll* swresample-0.dll*
avdevice-55.dll* avformat-55.dll* postproc-52.dll* swscale-2.dll*
1.5 设置ffmpeg库的日志
//设置日志的回调函数
void Log(void*, int,constchar*fmt,va_listvl)
{
TRACE(fmt,vl);//打印到输出窗口
}
//在程序初始化时设置ffmpeg日志的回调函数
av_log_set_callback(Log);
初始化中包括:打开视频文件、发现视频、音频信息、设置音视频编解码器、初始化显示窗口、初始化音频输出设备、并启动解码线程。
voidCplayerDoc::OnPlay()
{
USES_CONVERSION;
intnRet = 0;
OnStop();
Free();
if(m_szFileName.IsEmpty())
{
MessageBox(NULL,_T("请打开播放文件"),_T("错误"),MB_OK);
return;
}
if(m_FC)
return;
//打开媒体文件,并设置流,及相应的编译类型
nRet=avformat_open_input(&m_FC,T2A(m_szFileName),NULL,NULL);
if(nRet)
{
TRACE(_T("打开播放文件错误[%d]:%s\n"),nRet,m_szFileName);
return;
}
//发现流信息,主要是那些没有文件头的流类型
nRet=avformat_find_stream_info(m_FC,NULL);
if(nRet)
{
return;
}
//输出流信息
//av_dump_format(m_FC, -1, T2A(m_szFileName), 0);
for(inti = 0; i <m_FC->nb_streams;i++)
{
//查找视频流索引,以后根据这个索引确定视频流
if(AVMEDIA_TYPE_VIDEO == m_FC->streams[i]->codec->codec_type)
{
m_nVideoStream =i;
}
//查找音频流索引,以后根据这个索引确定音频流
if(AVMEDIA_TYPE_AUDIO == m_FC->streams[i]->codec->codec_type)
{
m_nAudioStream =i;
}
}
//==============================打开视频编解码器==========================
if(-1!=m_nVideoStream)
{
//得到视频编解码器上下文件,这个在前面的过程建立
m_vCodecContext =m_FC->streams[m_nVideoStream]->codec;
if(NULL == m_vCodecContext)
{
TRACE(_T("NULL == m_vCodecContext\n"));
return;
}
//根据id查找编解码器
m_vCodec=avcodec_find_decoder(m_vCodecContext->codec_id);
if(NULL == m_vCodec)
{
TRACE(_T("don't find video [%d]decoder\n",m_vCodecContext->codec_id));
return;
}
//打开视频编解码器
nRet=avcodec_open2(m_vCodecContext,m_vCodec,NULL);
if(nRet)
{
TRACE(_T("open video codec false\n"));
return;
}
}
//==============================打开视频编解码器==========================
//==============================打开音频编解码器==========================
if(-1!=m_nAudioStream)
{
m_aCodecContext =m_FC->streams[m_nAudioStream]->codec;
if(NULL == m_aCodecContext)
{
TRACE(_T("NULL == m_aCodecContext\n"));
return;
}
m_aCodec=avcodec_find_decoder(m_FC->streams[m_nAudioStream]->codec->codec_id);
if(NULL == m_aCodec)
{
TRACE(_T("find audio codec [%d]fail\n"),m_FC->streams[m_nAudioStream]->codec->codec_id);
return;
}
nRet=avcodec_open2(m_aCodecContext,m_aCodec,NULL);
if(nRet)
{
TRACE(_T("open audio codec false\n"));
return;
}
}
//==============================打开音频编解码器==========================
//初始化音频输出设备
m_WavePlay.Init(m_aCodecContext->sample_rate,m_aCodecContext->sample_fmt,m_aCodecContext->channels);
//调整窗口到屏幕中间,窗口大小为视频大小
CMainFrame*pView = (CMainFrame*)(AfxGetMainWnd()); //得到主窗口指针
//把窗口大小设置为视频大小
CRectrect(0, 0,
m_vCodecContext->width,m_vCodecContext->height);//视频大小
pView->ClientToScreen(&rect); //把客户区坐标转化为屏幕坐标
//把窗口坐标调整到屏幕中心
intnScreenWidth =GetSystemMetrics(SM_CXSCREEN); //屏幕宽度
intnScreenHeight =GetSystemMetrics(SM_CYSCREEN); //屏幕高度
intnLeft = (nScreenWidth - rect.Width()) / 2;
intnTop = (nScreenHeight - rect.Height()) / 2;
CRectrectWindows(nLeft,nTop,nLeft +rect.Width(),nTop +rect.Height());
//设置窗口大小
pView->SetWindowPos(NULL,rectWindows.left,rectWindows.top,
rectWindows.Width(),rectWindows.Height(),NULL);
m_bExit=FALSE;
DWORDid;
//启动解码线程
CreateThread(NULL, 0, ThreadRead,this, 0, &id);
}
1.7 从流中读入一个包(AVPact)过程
int CplayerDoc::OnReadFreame()
{
intnRet = 0;
AVPacketpkt;
intframeFinished = 0;
//初始化包结构
av_init_packet(&pkt);
while(!m_bExit && 0 ==av_read_frame(m_FC, &pkt))//从流中读入一个包
{
//根据流索引判断流的类型,并进行相应的解码处理
if(pkt.stream_index==m_nVideoStream)//是视频流
{
//对视频流数据进行解码
nRet = OnProcessVideo(&pkt);
}
elseif(pkt.stream_index ==m_nAudioStream)//是音频流
{
//对音频流数据进行解码
nRet = OnProcessAudio(&pkt);
}
//释放包的资源
av_free_packet(&pkt);
}
returnnRet;
}
视频解码是把解复合的包(AVPacket)由解码器解出图像帧(AVFrame)。AVFrame::format指示图像的格式。AVFrame::data包含图像数据。它是一个多维数组。例如:RGB位图数据包含在AVFrame::data[0],RGB位图数据长度包含在AVFrame::linesize[0]。YVU位图包每个分量数据占用AVFrame::data[3]每个数组,每个分量数据的长度分别包含在AVFrame::linesize[3]。
int CplayerDoc::OnProcessVideo(AVPacket *pPkt)
{
intnRet = 0;
intframeFinished = 0;
AVFrame*pFrame =av_frame_alloc();//分配一帧数据结构
//解码
nRet= avcodec_decode_video2(m_vCodecContext, pFrame,&frameFinished, pPkt);
if(frameFinished)//不为0,则解码成功,被解码的帧已放入到pFrame中.为0,解码不成功,因为数据不完全,编解码器会缓存已处理过的数据,下次调用时会继续解码直到成功
{
TRACE(_T("format:%d\n"),pFrame->format);//此帧的格式 enum AVPixelFormat
if(pFrame->key_frame)//是否是关键帧
{
TRACE(_T("key frame.pts:%lld,pkt_pts%lld,pkt_dts:%lld\n"),
pFrame->pts,pFrame->pkt_pts,pFrame->pkt_dts);
}
else
TRACE(_T("no keyframe.pts:%lld,pkt_pts%lld,pkt_dts:%lld\n"),
pFrame->pts,pFrame->pkt_pts,pFrame->pkt_dts);
AVPicture*pPic =newAVPicture;//在CplayerView::Display(CDC*pDc, int nWidth, int nHeight, AVPicture* pPic)中释放
//把帧格式转化为RGB24格式
AVFrameToRGB24CBitmap(pFrame,m_vCodecContext->width,m_vCodecContext->height,pPic);
//把RGB24位图放到播放缓存,传给视图使用
m_mutex.Lock();
m_Bitmap.push_back(pPic);
m_mutex.Unlock();
//向视频通知有新帧产生,做好定时后,这部分可以不用
CMainFrame*pMain=(CMainFrame*)AfxGetApp()->m_pMainWnd;
CplayerView*pView=(CplayerView*)pMain->GetActiveView();
::PostMessage(pView->GetSafeHwnd(),WM_USER_THREADEND,m_vCodecContext->width,m_vCodecContext->height);
}
//释放帧的资源
av_frame_free(&pFrame);
returnnRet;
}
不同的操作系统的绘图函数只支持指定的位图格式。例如:GDI绘图只支持RGB的位图,所以需要进行位图格式转换。这里我们转换成RGB24位图。在实际生产代码中,还需要检查设备支持的位图格式。由于GDI支持RGB24位图,其设备兼容格式由GDI库进行处理,所以,我们这里转换成RGB24。
intCplayerDoc::AVFrameToRGB24CBitmap(AVFrame*frame,AVPicture*pPic)
{
intnRet= 0;
structSwsContext*pSwsCtx =NULL;
if(NULL == frame)
{
return-1;
}
if(PIX_FMT_BGR24 == frame->format)
{
memcpy(pPic->data[0],frame->data[0],frame->linesize[0]);
pPic->linesize[0]=frame->linesize[0];
return0;
}
//为图片分配置空间
nRet=avpicture_alloc(pPic,PIX_FMT_BGR24,frame->width,frame->height);
if(nRet)
return-2;
//得到窗口客户区域大小
CRectrect(0, 0,frame->width,frame->height);
CMainFrame*pMain = (CMainFrame *)AfxGetApp()->m_pMainWnd;//得到主窗口指针
if(pMain)
{
//得到当前活动视图指针
CplayerView*pView = (CplayerView*)pMain->GetActiveView();
if(pView->GetSafeHwnd())
{
pView->GetClientRect(&rect);
TRACE(_T("client:w:%d;h:%d\n"),rect.Width(),rect.Height());
}
}
//设置图像转换上下文
pSwsCtx=sws_getCachedContext (NULL,
frame->width, //源宽度
frame->height, //源高度
(AVPixelFormat)frame->format,//源格式
rect.Width(), //目标宽度
rect.Height(), //目标高度
PIX_FMT_BGR24, //目的格式
SWS_FAST_BILINEAR, //转换算法
NULL,NULL,NULL);
if(NULL == pSwsCtx)
{
TRACE(_T("sws_getContext false\n"));
avpicture_free(pPic);
return-3;
}
//进行图片转换
sws_scale(pSwsCtx,
frame->data,frame->linesize,
0, frame->height,
pPic->data,pPic->linesize);
sws_freeContext(pSwsCtx);
returnnRet;
}
1.10 显示一视频帧
int CplayerView::Display(CDC*pDc,intnWidth,intnHeight,AVPicture*pPic)
{
intnRet = 0;
BITMAPINFObmi = {0};
bmi.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);//BITMAPINFO结构大小
bmi.bmiHeader.biBitCount= 24;//24位
bmi.bmiHeader.biCompression=BI_RGB;//未压缩的RGB
bmi.bmiHeader.biHeight= -nHeight;//图片宽,原始图形是倒的,所以这里用负号进行翻转
bmi.bmiHeader.biWidth=nWidth;//图片高
bmi.bmiHeader.biPlanes= 1;//调色板,因为是彩色的,所以设置为
//bmi.bmiHeader.biSizeImage= pPic->linesize[0];//图片的大小可以不用
CRectrect;
GetClientRect(rect);
//把DIB位图画到设备上显示出来
StretchDIBits(pDc->GetSafeHdc(),//设备上下文句柄
0,0, rect.Width(), rect.Height(), //设备dc的大小
0, 0, nWidth, nHeight,//图片的大小
pPic->data[0],//RGB24位图数据
&bmi, //BITMAPINFO结构指针,用于描述位图
DIB_RGB_COLORS,//RGB格式
SRCCOPY);//操作运算,复制
//释放在CplayerDoc::OnProcessVideo()中分配的内存
avpicture_free(pPic);
deletepPic;
returnnRet;
}
//调整窗口到屏幕中间,窗口大小为视频大小
CMainFrame*pView = (CMainFrame*)(AfxGetMainWnd()); //得到主窗口指针
//把窗口大小设置为视频大小
CRectrect(0, 0,
m_vCodecContext->width,m_vCodecContext->height);//视频大小
pView->ClientToScreen(&rect); //把客户区坐标转化为屏幕坐标
//把窗口坐标调整到屏幕中心
intnScreenWidth =GetSystemMetrics(SM_CXSCREEN); //屏幕宽度
intnScreenHeight =GetSystemMetrics(SM_CYSCREEN); //屏幕高度
intnLeft = (nScreenWidth - rect.Width()) / 2;
intnTop = (nScreenHeight - rect.Height()) / 2;
CRectrectWindows(nLeft,nTop,nLeft +rect.Width(),nTop +rect.Height());
//设置窗口大小
pView->SetWindowPos(NULL,rectWindows.left,rectWindows.top,
rectWindows.Width(),rectWindows.Height(),NULL);
1.12 解码音频帧
intCplayerDoc::OnProcessAudio(AVPacket *pPkt)
{
intnRet= 0;
intframeFinished= 0;
AVFrame *pFrame = av_frame_alloc();//分配一帧数据结构
//解码
nRet=avcodec_decode_audio4(m_aCodecContext,pFrame,&frameFinished,pPkt);
if(frameFinished)//不为0,则解码成功,被解码的帧已放入到pFrame中.为0,解码不成功,因为数据不完全,编解码器会缓存已处理过的数据,下次调用时会继续解码直到成功
{
TRACE(_T("format:%d\n"),pFrame->format);//此帧音频的采样格式 enum AVSampleFormat
if(pFrame->key_frame)//是否是关键帧
{
TRACE(_T("key frame.pts:%lld,pkt_pts%lld,pkt_dts:%lld\n"),
pFrame->pts,pFrame->pkt_pts,pFrame->pkt_dts);
}
else
TRACE(_T("no key frame.pts:%lld,pkt_pts%lld,pkt_dts:%lld\n"),
pFrame->pts,pFrame->pkt_pts,pFrame->pkt_dts);
nRet=m_WavePlay.AddFrame(pFrame);
}
TRACE(_T("OnProcessAudio\n"));
//释放帧的资源
av_frame_free(&pFrame);
returnnRet;
}
1.13 声音播放waveOut API说明
//声明:向波形输出设备发送一条消息
waveOutMessage(
hWaveOut: HWAVEOUT; {设备句柄}
uMessage: UINT; {消息}
dw1: DWORD {消息参数}
dw2: DWORD {消息参数}
): Longint; {将由设备给返回值}
//声明:打开waveout设备
waveOutOpen(
lphWaveOut: PHWaveOut; {用于返回设备句柄的指针;如果dwFlags=WAVE_FORMAT_QUERY, 这里应是 nil}
uDeviceID: UINT; {设备ID;可以指定为:WAVE_MAPPER, 这样函数会根据给定的波形格式选择合适的设备}
lpFormat: PWaveFormatEx; {TWaveFormat结构的指针; TWaveFormat包含要申请的波形格式}
dwCallback: DWORD {回调函数地址或窗口句柄;若不使用回调机制, 设为 nil}
dwInstance: DWORD {给回调函数的实例数据;不用于窗口}
dwFlags: DWORD {打开选项}
): MMRESULT; {成功返回 0;可能的错误值见下:}
MMSYSERR_BADDEVICEID = 2; {设备ID超界}
MMSYSERR_ALLOCATED = 4; {指定的资源已被分配}
MMSYSERR_NODRIVER = 6; {没有安装驱动程序}
MMSYSERR_NOMEM = 7; {不能分配或锁定内存}
WAVERR_BADFORMAT = 32;{设备不支持请求的波形格式}
//TWaveFormatEx 结构:
TWaveFormatEx = packedrecord
wFormatTag: Word; {指定格式类型;默认WAVE_FORMAT_PCM = 1;}
nChannels: Word; {指出波形数据的通道数;单声道为 1, 立体声为 2}
nSamplesPerSec: DWORD; {指定样本速率(每秒的样本数)}
nAvgBytesPerSec: DWORD; {指定数据传输的平均速率(每秒的字节数)}
nBlockAlign: Word; {指定块对齐(单位字节), 块对齐是数据的最小单位}
wBitsPerSample: Word; {采样大小(字节)}
cbSize: Word; {附加信息大小; PCM格式没这个字段}
end;
{16 位立体声 PCM的块对齐是 4 字节(每个样本2字节, 2个通道)}
//打开选项 dwFlags的可选值:
WAVE_FORMAT_QUERY = $0001; {只是判断设备是否支持给定的格式, 并不打开}
WAVE_ALLOWSYNC = $0002; {当是同步设备时必须指定}
CALLBACK_WINDOW = $00010000;{当 dwCallback是窗口句柄时指定}
CALLBACK_FUNCTION = $00030000;{当 dwCallback是函数指针时指定}
//如果选择窗口接受回调信息,可能会发送到窗口的消息有:
MM_WOM_OPEN = $3BB;
MM_WOM_CLOSE = $3BC;
MM_WOM_DONE = $3BD;
//如果选择函数接受回调信息,可能会发送给函数的消息有:
WOM_OPEN = MM_WOM_OPEN;
WOM_CLOSE = MM_WOM_CLOSE;
WOM_DONE = MM_WOM_DONE;
//声明:准备一个波形数据块用于播放。提示:必须调用 GlobalAlloc 给TWaveHdr 和其中的 lpData 指向的缓冲区分配内存(使用 GMEM_MOVEABLE、GMEM_SHARE), 并用 GlobalLock 锁定.
waveOutPrepareHeader(
hWaveOut: HWAVEOUT; {设备句柄}
lpWaveOutHdr: PWaveHdr; {TWaveHdr结构的指针}
uSize: UINT {TWaveHdr结构大小}
): MMRESULT; {成功返回 0;可能的错误值见下:}
MMSYSERR_INVALHANDLE = 5; {设备句柄无效}
MMSYSERR_NOMEM = 7; {不能分配或锁定内存}
MMSYSERR_HANDLEBUSY = 12;{其他线程正在使用该设备}
//TWaveHdr 是 wavehdr_tag结构的重定义
wavehdr_tag = record
lpData: PChar; {指向波形数据缓冲区}
dwBufferLength: DWORD; {波形数据缓冲区的长度}
dwBytesRecorded: DWORD; {若首部用于输入,指出缓冲区中的数据量}
dwUser: DWORD; {指定用户的32位数据}
dwFlags: DWORD; {缓冲区标志}
dwLoops: DWORD; {循环播放次数,仅用于输出缓冲区}
lpNext: PWaveHdr; {保留}
reserved: DWORD; {保留}
end;
//TWaveHdr 中的 dwFlags的可选值:
WHDR_DONE = $00000001;{设备已使用完缓冲区,并返回给程序}
WHDR_PREPARED = $00000002;{waveInPrepareHeader或 waveOutPrepareHeader已将缓冲区准备好}
WHDR_BEGINLOOP = $00000004;{缓冲区是循环中的第一个缓冲区,仅用于输出}
WHDR_ENDLOOP = $00000008; {缓冲区是循环中的最后一个缓冲区, 仅用于输出}
WHDR_INQUEUE = $00000010; {reserved for driver }
//声明:清除由waveOutPrepareHeader完成的准备
//提示:
//设备使用完数据块后, 须调用此函数;
//释放(GlobalFree)缓冲区前,须调用此函数;
//取消一个尚未准备的缓冲区将无效,但函数返回 0
waveOutUnprepareHeader(
hWaveOut: HWAVEOUT; {设备句柄}
lpWaveOutHdr: PWaveHdr; {TWaveHdr 结构的指针}
uSize: UINT {TWaveHdr 结构大小}
): MMRESULT; {成功返回 0; 可能的错误值见下:}
MMSYSERR_INVALHANDLE = 5; {设备句柄无效}
MMSYSERR_HANDLEBUSY = 12; {设备已被另一线程使用}
WAVERR_STILLPLAYING = 33; {缓冲区还在队列中}
//TWaveHdr 是 wavehdr_tag 结构的重定义
wavehdr_tag = record
lpData: PChar; {指向波形数据缓冲区}
dwBufferLength: DWORD; {波形数据缓冲区的长度}
dwBytesRecorded: DWORD; {若首部用于输入, 指出缓冲区中的数据量}
dwUser: DWORD; {指定用户的32位数据}
dwFlags: DWORD; {缓冲区标志}
dwLoops: DWORD; {循环播放次数, 仅用于输出缓冲区}
lpNext: PWaveHdr; {保留}
reserved: DWORD; {保留}
end;
//TWaveHdr 中的 dwFlags 的可选值:
WHDR_DONE = $00000001; {设备已使用完缓冲区, 并返回给程序}
WHDR_PREPARED = $00000002; {waveInPrepareHeader 或 waveOutPrepareHeader 已将缓冲区准备好}
WHDR_BEGINLOOP = $00000004; {缓冲区是循环中的第一个缓冲区, 仅用于输出}
WHDR_ENDLOOP = $00000008; {缓冲区是循环中的最后一个缓冲区, 仅用于输出}
WHDR_INQUEUE = $00000010; { reserved for driver }
//声明: 向输出设备发送一个数据块
//提示: 把数据缓冲区传给waveOutWrite 之前, 必须使用waveOutPrepareHeader准备该缓冲区;
//若未调用 waveOutPause函数暂停设备, 则第一次把数据块发送给设备时即开始播放.
waveOutWrite(
hWaveOut: HWAVEOUT; {设备句柄}
lpWaveOutHdr: PWaveHdr; {TWaveHdr结构的指针}
uSize: UINT {TWaveHdr结构大小}
): MMRESULT; {成功返回 0;可能的错误值见下:}
MMSYSERR_INVALHANDLE = 5; {设备句柄无效}
MMSYSERR_HANDLEBUSY = 12;{设备已被另一线程使用}
WAVERR_UNPREPARED = 34;{未准备数据块}
//TWaveHdr 是 wavehdr_tag结构的重定义
wavehdr_tag = record
lpData: PChar; {指向波形数据缓冲区}
dwBufferLength: DWORD; {波形数据缓冲区的长度}
dwBytesRecorded: DWORD; {若首部用于输入,指出缓冲区中的数据量}
dwUser: DWORD; {指定用户的32位数据}
dwFlags: DWORD; {缓冲区标志}
dwLoops: DWORD; {循环播放次数,仅用于输出缓冲区}
lpNext: PWaveHdr; {保留}
reserved: DWORD; {保留}
end;
//TWaveHdr 中的 dwFlags的可选值:
WHDR_DONE = $00000001;{设备已使用完缓冲区,并返回给程序}
WHDR_PREPARED = $00000002;{waveInPrepareHeader或 waveOutPrepareHeader已将缓冲区准备好}
WHDR_BEGINLOOP = $00000004;{缓冲区是循环中的第一个缓冲区,仅用于输出}
WHDR_ENDLOOP = $00000008; {缓冲区是循环中的最后一个缓冲区, 仅用于输出}
WHDR_INQUEUE = $00000010; {reserved for driver }
1.14 初始化音频设备
intCWavePlay::Init(DWORDnSampleRate,enumAVSampleFormatnSampleFormat,WORDnChannels)
{
WAVEFORMATEXwfx ={0}; /*look this up in your documentation */
MMRESULTresult =MMSYSERR_NOERROR; /*for waveOut return values */
/*
* first weneed to set up the WAVEFORMATEX structure.
* thestructure describes the format of the audio.
*/
wfx.nSamplesPerSec=nSampleRate;/*采样率 */
wfx.wBitsPerSample=av_get_bytes_per_sample(nSampleFormat);//采样大小,ffmpeg叫采样格式,enum AVSampleFormat常量,因为windows waveout api只支持8或16
wfx.nChannels=nChannels;/*通道数*/
/*
*WAVEFORMATEX also has other fields which need filling.
* as longas the three fields above are filled this should
* work forany PCM (pulse code modulation) format.
*/
wfx.cbSize= 0; /* size of _extra_ info */
wfx.wFormatTag=WAVE_FORMAT_PCM;
wfx.nBlockAlign= (wfx.wBitsPerSample*wfx.nChannels)>> 3;
wfx.nAvgBytesPerSec=wfx.nBlockAlign*wfx.nSamplesPerSec;
//检查格式是否被支持
result=waveOutOpen(
NULL, //ptr can be NULL for query
WAVE_MAPPER, //the device identifier
&wfx, //defines requested format
NULL, //no callback
NULL, // no instance data
WAVE_FORMAT_QUERY); // query only, do not open device
if(WAVERR_BADFORMAT == result)
{
//设置默认格式
m_bConvertFormat=TRUE;//需要转换格式
wfx.nSamplesPerSec= 44100; /* 采样率*/
wfx.wBitsPerSample= 16;//采样大小,ffmpeg叫采样格式,enum AVSampleFormat常量,因为windows waveout api只支持8或16
wfx.nChannels= 2; /*通道数*/
wfx.cbSize= 0; /* size of _extra_ info */
wfx.wFormatTag=WAVE_FORMAT_PCM;
wfx.nBlockAlign= (wfx.wBitsPerSample*wfx.nChannels)>> 3;
wfx.nAvgBytesPerSec=wfx.nBlockAlign*wfx.nSamplesPerSec;
}
StartThread();
//设置线程处理响应事件
result=waveOutOpen(&m_hWaveOut,WAVE_MAPPER, &wfx,
(DWORD_PTR)m_ThreadID, (DWORD_PTR)this,CALLBACK_THREAD);
/*设置回调函数,响应事件
* try toopen the default wave device. WAVE_MAPPER is
* aconstant defined in mmsystem.h, it always points to the
* defaultwave device on the system (some people have 2 or
* moresound cards).
*/
//result= waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &wfx,
// (DWORD_PTR)CallBackwaveOut, (DWORD_PTR)this,CALLBACK_FUNCTION);
if(result!=MMSYSERR_NOERROR)
{
TRACE(_T("unable to open WAVE_MAPPER device:%d\n"),result);
returnresult;
}
returnresult;
}
1.15 声音格式转换
//音频数据转换成默认的格式
intCWavePlay::AVFrameToAudio(AVFrame*frame,char**pBuf,int&len)
{
intnRet= 0;
structSwrContext *pSC =NULL;
//准备格式转换
pSC=swr_alloc_set_opts(NULL,
AV_CH_LAYOUT_STEREO, //输出通道布局
AVSampleFormat::AV_SAMPLE_FMT_S16,//输出格式
44100, //输出采样率
AV_CH_LAYOUT_STEREO, //输入通道布局
(AVSampleFormat)frame->format, //输入格式
frame->sample_rate, //输入采样率
NULL,NULL);
if(NULL == pSC)
return-1;
nRet=swr_init(pSC);
if(nRet)
{
TRACE(_T("swr init fail\n"));
returnnRet;
}
//转换音频
nRet=swr_convert(pSC,
(uint8_t**)pBuf,//输出BUFFER
len/frame->channels/av_get_bytes_per_sample(AV_SAMPLE_FMT_S16),//buffer长度能容纳每个通道多少个采样,输出缓冲器中每个通道的采样数
(constuint8_t**)frame->data,
frame->nb_samples//每个通道的采样数
);
if(nRet> 0)
{
len=nRet *av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) *frame->channels;
nRet= 0;
}
swr_free(&pSC);
returnnRet;
}
1.16 把音频数据写到音频设备
intCWavePlay::AddFrame(AVFrame *pFrame)
{
intnRet= 0;
if(NULL == pFrame)
return-1;
//检查是否超出缓存
if(m_nBufferNumber>= 5)
{
return-100;
}
intnLen=pFrame->linesize[0]*pFrame->channels<< 1;
char*pBuf=newchar[nLen];
if(m_bConvertFormat)
{
//转换音频格式
AVFrameToAudio(pFrame, &pBuf,nLen);
}
else
{
nLen=pFrame->linesize[0]*pFrame->channels;
memcpy(pBuf,pFrame->data,nLen);
}
writeAudioBlock(m_hWaveOut,pBuf,nLen);
InterlockedIncrement(&m_nBufferNumber);
returnnRet;
}
voidCWavePlay::writeAudioBlock(HWAVEOUThWaveOut,char*block,DWORDsize)
{
WAVEHDR*pHeader =newWAVEHDR;
MMRESULTresult;
//用音频数据初始化 WAVEHDR结构
memset(pHeader, 0, sizeof(WAVEHDR));
pHeader->dwBufferLength=size;
pHeader->lpData= (LPSTR)block;
//pHeader->dwFlags= WHDR_DONE;
//为播放准备数据块
result=waveOutPrepareHeader(hWaveOut,pHeader,sizeof(WAVEHDR));
if(result!=MMSYSERR_NOERROR )
{
TRACE(_T("waveOutPrepareHeader fail:%d\n", result));
return;
}
//写入数据块到设备.一般情况下waveOutWrite立即返回。除了设备使用同步的情况。
result=waveOutWrite(hWaveOut,pHeader,sizeof(WAVEHDR));
if(result!=MMSYSERR_NOERROR )
{
TRACE(_T("waveOutPrepareHeader fail:%d\n", result));
return;
}
}