前言
前面介紹了設(shè)備搜索、獲取設(shè)備能力信息,在此基礎(chǔ)上,本篇博客介紹如何播放音視頻(ONVIF協(xié)議 + FFmpeg庫(kù) + SDL 2.0庫(kù))。
編碼流程
1.調(diào)用 discoveryDevice()接口獲取設(shè)備服務(wù)地址。
2.使用設(shè)備服務(wù)地址調(diào)用 getDeviceCapabilities() 接口獲取媒體服務(wù)地址。
3.調(diào)用 getMediaProfiles()接口獲取媒體配置信息(主要是獲取Token)。
4.使用Token調(diào)用 getStreamUri()接口,獲取RTSP地址。
5.使用RTSP地址調(diào)用 openRtsp()接口,播放音視頻。
FFmpeg庫(kù)
由于解碼用到了FFmpeg庫(kù),所以簡(jiǎn)要介紹一下FFmpeg庫(kù)的用法。
- FFmpeg庫(kù)簡(jiǎn)介
FFmpeg是一套可以用來(lái)記錄、轉(zhuǎn)換數(shù)字音頻、視頻,并能將其轉(zhuǎn)化為流的開源計(jì)算機(jī)程序。采用LGPL或GPL許可證。它提供了錄制、轉(zhuǎn)換以及流化音視頻的完整解決方案。它包含了非常先進(jìn)的音頻/視頻編解碼庫(kù)libavcodec,為了保證高可移植性和編解碼質(zhì)量,libavcodec里很多code都是從頭開發(fā)的。
FFmpeg在Linux平臺(tái)下開發(fā),但它同樣也可以在其它操作系統(tǒng)環(huán)境中編譯運(yùn)行,包括Windows、Mac OS X等。這個(gè)項(xiàng)目最早由Fabrice Bellard發(fā)起,2004年至2015年間由Michael Niedermayer主要負(fù)責(zé)維護(hù)。許多FFmpeg的開發(fā)人員都來(lái)自MPlayer項(xiàng)目,而且當(dāng)前FFmpeg也是放在MPlayer項(xiàng)目組的服務(wù)器上。項(xiàng)目的名稱來(lái)自MPEG視頻編碼標(biāo)準(zhǔn),前面的"FF"代表"Fast Forward"。 - FFmpeg庫(kù)下載
Windows平臺(tái)下,官網(wǎng)有編譯好的動(dòng)態(tài)庫(kù)。 - 使用FFmpeg庫(kù)的視頻解碼流程
1.注冊(cè)支持的所有文件格式及其編解碼器av_register_all()。
2.打開文件avformat_open_input()。
3.從文件中提取流信息avformat_find_stream_info(),從多個(gè)數(shù)據(jù)流中找到類型為AVMEDIA_TYPE_VIDEO的視頻流。
4.查找與視頻流相對(duì)應(yīng)的解碼器avcodec_find_decoder()。
5.打開解碼器avcodec_open2()。
6.讀取碼流中的視頻一幀av_read_frame()。
7.解碼視頻一幀avcodec_send_packet()avcodec_receive_frame()。 - 注意事項(xiàng)
由于FFmpeg庫(kù)是用C語(yǔ)言實(shí)現(xiàn)的,要在C++中調(diào)用C函數(shù)需要extern 'C'聲明。如下所示:
extern "C"
{
#include "FFmpeg/include/libavcodec/avcodec.h"
#include "FFmpeg/include/libavformat/avformat.h"
#include "FFmpeg/include/libswscale/swscale.h"
#include "FFmpeg/include/libavutil/imgutils.h"
};
SDL 2.0庫(kù)
由于播放視頻用到了SDL庫(kù),所以簡(jiǎn)要介紹一下SDL庫(kù)的用法。
- SDL 2.0庫(kù)簡(jiǎn)介
SDL(Simple DirectMedia Layer)是一套開放源代碼的跨平臺(tái)多媒體開發(fā)庫(kù),使用C語(yǔ)言寫成。SDL提供了數(shù)種控制圖像、聲音、輸出入的函數(shù),讓開發(fā)者只要用相同或是相似的代碼就可以開發(fā)出跨多個(gè)平臺(tái)(Linux、Windows、Mac OS X等)的應(yīng)用軟件。目前SDL多用于開發(fā)游戲、模擬器、媒體播放器等多媒體應(yīng)用領(lǐng)域。 - SDL 2.0庫(kù)下載
Windows平臺(tái):從官網(wǎng)下載開發(fā)庫(kù) SDL2-devel-2.0.5-VC.zip (Visual C++ 32/64-bit),里面包含.h頭文件、.lib庫(kù)文件和.dll動(dòng)態(tài)鏈接庫(kù)文件。 - 使用SDL 2.0庫(kù)播放視頻流程
- 初始化:
1.初始化SDL 2.0庫(kù)SDL_Init()。
2.創(chuàng)建窗口SDL_CreateWindowFrom()或SDL_CreateWindow()。
3.基于窗口創(chuàng)建渲染器SDL_CreateRenderer()。
4.創(chuàng)建紋理SDL_CreateTexture()。 - 循環(huán)渲染數(shù)據(jù):
5.設(shè)置紋理的數(shù)據(jù)SDL_UpdateTexture()。
6.紋理復(fù)制給渲染器SDL_RenderCopy()。
7.顯示SDL_RenderPresent()。
- 初始化:
編碼
獲取媒體配置信息
/**
* @description: 獲取媒體配置信息(主/輔碼流配置信息)
*
* @brief getMediaProfiles
* @param[in] mediaXAddrs 媒體服務(wù)地址
* @param[in][out] deviceProVec 設(shè)備配置信息
* @return bool 返回true表示成功,其余查看soap錯(cuò)誤碼
*/
bool OnvifFunc::getMediaProfiles(std::string mediaXAddrs, std::vector<DEVICEPROFILE> &deviceProVec)
{
// 初始化soap
struct soap soap;
soap_set_mode(&soap, SOAP_C_UTFSTRING);
MediaBindingProxy media(&soap);
// 設(shè)置超時(shí)(超過(guò)指定時(shí)間沒有數(shù)據(jù)就退出)
media.soap->recv_timeout = SOAP_SOCK_TIMEOUT;
media.soap->send_timeout = SOAP_SOCK_TIMEOUT;
media.soap->connect_timeout = SOAP_SOCK_TIMEOUT;
setAuthInfo(media.soap, m_username, m_password);
_trt__GetProfiles trt__GetProfiles;
_trt__GetProfilesResponse trt__GetProfilesResponse;
int iRet = media.GetProfiles(mediaXAddrs.c_str(), NULL, &trt__GetProfiles, trt__GetProfilesResponse);
if (SOAP_OK == iRet)
{
for (std::vector<tt__Profile *>::const_iterator iter = trt__GetProfilesResponse.Profiles.begin();
iter != trt__GetProfilesResponse.Profiles.end(); ++iter)
{
DEVICEPROFILE devicePro;
tt__Profile *ttProfile = *iter;
// 配置文件Token
if (!ttProfile->token.empty())
{
devicePro.token = ttProfile->token;
}
// 視頻編碼器配置信息
if (NULL != ttProfile->VideoEncoderConfiguration)
{
// 視頻編碼Token
if (!ttProfile->VideoEncoderConfiguration->token.empty())
devicePro.venc.token = ttProfile->VideoEncoderConfiguration->token;
// 視頻編碼器分辨率
if (NULL != ttProfile->VideoEncoderConfiguration->Resolution)
{
devicePro.venc.Height = ttProfile->VideoEncoderConfiguration->Resolution->Height;
devicePro.venc.Width = ttProfile->VideoEncoderConfiguration->Resolution->Width;
}
}
deviceProVec.push_back(devicePro);
}
// 清理變量
media.destroy();
return true;
}
// 清理變量
media.destroy();
return false;
}
獲取設(shè)備碼流地址(RTSP)
/**
* @description: 獲取設(shè)備碼流地址(RTSP)
*
* @brief getStreamUri
* @param[in] mediaXAddrs 媒體服務(wù)地址
* @param[in] profileToken 唯一標(biāo)識(shí)設(shè)備配置文件的令牌字符串
* @param[in][out] uri 碼流地址
* @return bool 返回true表示成功,其余查看soap錯(cuò)誤碼
*/
bool OnvifFunc::getStreamUri(std::string mediaXAddrs, std::string profileToken, std::string &uri)
{
// 初始化soap
struct soap soap;
soap_set_mode(&soap, SOAP_C_UTFSTRING);
MediaBindingProxy media(&soap);
// 設(shè)置超時(shí)(超過(guò)指定時(shí)間沒有數(shù)據(jù)就退出)
media.soap->recv_timeout = SOAP_SOCK_TIMEOUT;
media.soap->send_timeout = SOAP_SOCK_TIMEOUT;
media.soap->connect_timeout = SOAP_SOCK_TIMEOUT;
_trt__GetStreamUri trt__GetStreamUri;
_trt__GetStreamUriResponse trt__GetStreamUriResponse;
tt__StreamSetup ttStreamSetup;
tt__Transport ttTransport;
ttStreamSetup.Stream = tt__StreamType__RTP_Unicast;
ttStreamSetup.Transport = &ttTransport;
ttStreamSetup.Transport->Protocol = tt__TransportProtocol__RTSP;
ttStreamSetup.Transport->Tunnel = NULL;
trt__GetStreamUri.StreamSetup = &ttStreamSetup;
trt__GetStreamUri.ProfileToken = profileToken;
setAuthInfo(media.soap, m_username, m_password);
int iRet = media.GetStreamUri(mediaXAddrs.c_str(), NULL, &trt__GetStreamUri, trt__GetStreamUriResponse);
if (SOAP_OK == iRet)
{
if (NULL != trt__GetStreamUriResponse.MediaUri)
{
if (!trt__GetStreamUriResponse.MediaUri->Uri.empty())
uri = trt__GetStreamUriResponse.MediaUri->Uri;
}
// 清理變量
media.destroy();
return true;
}
// 清理變量
media.destroy();
return false;
}
構(gòu)造帶有認(rèn)證信息的URI地址(有的IPC要求認(rèn)證)
/**
* @description: 構(gòu)造帶有認(rèn)證信息的URI地址
*
* @brief makeUriWithauth
* @param[in][out] uri 碼流地址
* @param[in] username 用戶名
* @param[in] password 密碼
* @return bool 返回true表示成功,其余查看soap錯(cuò)誤碼
*/
bool OnvifFunc::makeUriWithauth(std::string &uri, std::string username, std::string password)
{
assert(!uri.empty());
assert(!username.empty());
assert(!password.empty());
std::string str = username + ":" + password + "@";
size_t pos = uri.find("http://");
uri.insert(pos + 2, str);
return true;
}
播放RTSP流
/**
* @description: 播放RTSP流
*
* @brief openRtsp
* @param[in] rtsp 碼流地址(帶認(rèn)證)
* @param[in] hwnd 窗口句柄
* @param[in] width 窗口的寬度
* @param[in] height 窗口的高度
* @return bool 返回true表示成功,返回false表示失敗
*/
bool OnvifFunc::openRtsp(std::string rtsp, HWND hwnd, int width, int height)
{
AVFormatContext *pFormatCtx;
int i, videoindex = -1;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVFrame *pFrame, *pFrameYUV;
uint8_t *out_buffer;
AVPacket *packet;
int ret;
struct SwsContext *img_convert_ctx;
// SDL
int screen_w, screen_h;
SDL_Window *screen;
SDL_Renderer* sdlRenderer;
SDL_Texture* sdlTexture;
SDL_Rect sdlRect;
SDL_Thread *videoTid;
SDL_Event event;
av_register_all();
avformat_network_init();
pFormatCtx = avformat_alloc_context();
pCodecCtx = avcodec_alloc_context3(NULL);
if (avformat_open_input(&pFormatCtx, rtsp.c_str(), NULL, NULL) != 0)
return false;
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
return false;
for (i = 0; i < pFormatCtx->nb_streams; i++)
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoindex = i;
break;
}
if (-1 == videoindex) {
return false;
}
ret = avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoindex]->codecpar);
if (ret < 0)
return false;
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (NULL == pCodec)
return false;
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
return false;
pFrame = av_frame_alloc();
pFrameYUV = av_frame_alloc();
out_buffer = (uint8_t *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1));
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);
packet = (AVPacket *)av_malloc(sizeof(AVPacket));
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
width, height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))
return false;
screen_w = width;
screen_h = height;
screen = SDL_CreateWindowFrom(static_cast<void *>(hwnd));
if (!screen)
return false;
sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
//IYUV: Y + U + V (3 planes)
//YV12: Y + V + U (3 planes)
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, width, height);
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = screen_w;
sdlRect.h = screen_h;
videoTid = SDL_CreateThread(sfp_refresh_thread, NULL, NULL);
for (;;)
{
// 等待事件
SDL_WaitEvent(&event);
if (SFM_REFRESH_EVENT == event.type)
{
if (0 == av_read_frame(pFormatCtx, packet)) {
if (packet->stream_index == videoindex) {
ret = avcodec_send_packet(pCodecCtx, packet);
if (ret != 0)
return false;
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret != 0)
return false;
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
SDL_UpdateYUVTexture(sdlTexture, &sdlRect,
pFrameYUV->data[0], pFrameYUV->linesize[0],
pFrameYUV->data[1], pFrameYUV->linesize[1],
pFrameYUV->data[2], pFrameYUV->linesize[2]);
SDL_RenderClear(sdlRenderer);
SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
SDL_RenderPresent(sdlRenderer);
}
av_packet_unref(packet);
}
else
{
// 退出線程
g_global.m_threadExit = 1;
break;
}
}
else if (SFM_BREAK_EVENT == event.type)
break;
}
sws_freeContext(img_convert_ctx);
SDL_Quit();
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
avcodec_free_context(&pCodecCtx);
return true;
}
上述代碼均為核心代碼。