ONVIF之播放音视频

前言

前面介绍了设备搜索、获取设备能力信息,在此基础上,本篇博客介绍如何播放音视频(ONVIF协议 + FFmpeg库 + SDL 2.0库)。

编码流程

1.调用 discoveryDevice()接口获取设备服务地址。
2.使用设备服务地址调用 getDeviceCapabilities() 接口获取媒体服务地址。
3.调用 getMediaProfiles()接口获取媒体配置信息(主要是获取Token)。
4.使用Token调用 getStreamUri()接口,获取RTSP地址。
5.使用RTSP地址调用 openRtsp()接口,播放音视频。

FFmpeg库

由于解码用到了FFmpeg库,所以简要介绍一下FFmpeg库的用法。

  • FFmpeg库简介
    FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。
    FFmpeg在Linux平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括Windows、Mac OS X等。这个项目最早由Fabrice Bellard发起,2004年至2015年间由Michael Niedermayer主要负责维护。许多FFmpeg的开发人员都来自MPlayer项目,而且当前FFmpeg也是放在MPlayer项目组的服务器上。项目的名称来自MPEG视频编码标准,前面的"FF"代表"Fast Forward"。
  • FFmpeg库下载
    Windows平台下,官网有编译好的动态库。
    • 官网下载Dev版本,里面包含了.h头文件.lib库文件
    • 官网下载Shared版本,里面包含了.dll动态链接库文件
  • 使用FFmpeg库的视频解码流程
    1.注册支持的所有文件格式及其编解码器 av_register_all()
    2.打开文件 avformat_open_input()
    3.从文件中提取流信息 avformat_find_stream_info(),从多个数据流中找到类型为 AVMEDIA_TYPE_VIDEO 的视频流。
    4.查找与视频流相对应的解码器 avcodec_find_decoder()
    5.打开解码器 avcodec_open2()
    6.读取码流中的视频一帧 av_read_frame()
    7.解码视频一帧 avcodec_send_packet() avcodec_receive_frame()
  • 注意事项
    由于FFmpeg库是用C语言实现的,要在C++中调用C函数需要 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库

由于播放视频用到了SDL库,所以简要介绍一下SDL库的用法。

  • SDL 2.0库简介
    SDL(Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发库,使用C语言写成。SDL提供了数种控制图像、声音、输出入的函数,让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。目前SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。
  • SDL 2.0库下载
    Windows平台:从官网下载开发库 SDL2-devel-2.0.5-VC.zip (Visual C++ 32/64-bit),里面包含.h头文件.lib库文件.dll动态链接库文件
  • 使用SDL 2.0库播放视频流程
    • 初始化:
      1.初始化SDL 2.0库 SDL_Init()
      2.创建窗口 SDL_CreateWindowFrom()SDL_CreateWindow()
      3.基于窗口创建渲染器 SDL_CreateRenderer()
      4.创建纹理SDL_CreateTexture()
    • 循环渲染数据:
      5.设置纹理的数据 SDL_UpdateTexture()
      6.纹理复制给渲染器 SDL_RenderCopy()
      7.显示 SDL_RenderPresent()

编码

获取媒体配置信息

/**
* @description: 获取媒体配置信息(主/辅码流配置信息)
*
* @brief getMediaProfiles
* @param[in] mediaXAddrs                媒体服务地址
* @param[in][out] deviceProVec          设备配置信息
* @return bool          返回true表示成功,其余查看soap错误码
*/
bool OnvifFunc::getMediaProfiles(std::string mediaXAddrs, std::vector<DEVICEPROFILE> &deviceProVec)
{
    // 初始化soap
    struct soap soap;
    soap_set_mode(&soap, SOAP_C_UTFSTRING);
    MediaBindingProxy media(&soap);
    // 设置超时(超过指定时间没有数据就退出)
    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;
}

获取设备码流地址(RTSP)

/**
* @description: 获取设备码流地址(RTSP)
*
* @brief getStreamUri
* @param[in] mediaXAddrs            媒体服务地址
* @param[in] profileToken           唯一标识设备配置文件的令牌字符串
* @param[in][out] uri               码流地址
* @return bool          返回true表示成功,其余查看soap错误码
*/
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);
    // 设置超时(超过指定时间没有数据就退出)
    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;
}

构造带有认证信息的URI地址(有的IPC要求认证)

/**
* @description: 构造带有认证信息的URI地址
*
* @brief makeUriWithauth
* @param[in][out] uri           码流地址
* @param[in] username           用户名
* @param[in] password           密码
* @return bool          返回true表示成功,其余查看soap错误码
*/
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("//");
    uri.insert(pos + 2, str);
    
    return true;
}

播放RTSP流

/**
* @description: 播放RTSP流
*
* @brief openRtsp
* @param[in] rtsp               码流地址(带认证)
* @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;
}

上述代码均为核心代码。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容