Bootstrap

zlmediakit移植与开发(arm64, Qt)

2023-5-26

关键字
rk3568,arm64,linux,zlmediakit交叉编译,zlmediakit实现rtsp服务

一、ZLMediaKit交叉编译

aarch64交叉编译

下载ZLMediaKit源码,并在程序主目录下的CMakeLists.txt中,追加交叉编译配置项。

    set(CMAKE_SYSTEM_NAME Linux)
    set(CMAKE_SYSTEM_PROCESSOR aarch64)
    set(target_arch aarch64-linux-gnu)
    
     #这可替换为需要的编译工具路径
    set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc)
    set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++)
    set(CMAKE_LIBRARY_ARCHITECTURE ${target_arch} CACHE STRING "" FORCE)
    set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
    set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
    set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
    set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
    
    #添加交叉编译时需要的目标平台头,库文件路径
    include_directories (/opt/sysroot/firefly-arm64-sysroot-18.04/usr/include/aarch64-linux-gnu 
	            /opt/sysroot/firefly-arm64-sysroot-18.04/usr/include 
	            ../../mbedtls-2.2.1/include)            
	link_directories (/opt/sysroot/firefly-arm64-sysroot-18.04/usr/lib/aarch64-linux-gnu
				/opt/sysroot/firefly-arm64-sysroot-18.04/lib
			  	/opt/sysroot/firefly-arm64-sysroot-18.04/usr/lib)

ZLMediaKit加入工程

交叉编译完成后,安装ZLMediaKit到临时目录下;将libmk_api.so库文件和mk_开头的文件添加到应用。

二、ZLMeidaKit功能梳理

  1. zlmediakit提供的api接口功能
    zlmediakit能很方便地创建rtspServer,但是需要自己设计网络事件响应,实现视频的推流。api接口可以很方便实现播放,停止,倍速等基础功能;同时要注意在无终端访问时需关闭媒体资源。

  2. 客户端请求播放地址规则
    1)产生的rtsp请求url必须是二级地址,要注意其字段含义:参看: 播放url规则
    2)blw-gy3-nvr产品中,采用字段格式:rtsp://[ipaddr]/live/video?FilePath=[fileName]
    [ipaddr]:填写nvr本地可用有效ip地址。这个字段,将由mds查询nvr视频时,nvr提供。如rtsp://192.168.111.1/live/video?FilePath=
    [fileName]:填写文件名称,不能填写绝对路径,nvr将根据文件名称搜索数据库得到对应的文件路径。如填写:Ch1_2023-04-03-10-49-53.ts
    3)完整url例子:rtsp://192.168.111.1/live/video?FilePath=Ch1_2023-04-03-10-49-53.ts

  3. events监听:
    rtsp服务主要注册两个事件:on_mk_media_not_foundon_mk_media_no_reader
    on_mk_media_not_found:主要负责对媒体资源对象的申请和配置。
    on_mk_media_no_reader:主要负责在没有播放客户端时,关闭现有的媒体资源对象。

  4. 媒体对象
    主要是使用mk_media, mk_h264_splitter这两个对象。zlmediakit api将申请的对象采用链表的形式进行管理。
    mk_media对象可以提供播放资源url。是操作核心对象。
    mk_h264_splitter对象则负责处理数据帧。也必不可少。

  5. 暂停,倍速,时间戳功能
    这些功能可以向媒体资源(mk_media)进行注册回调函数:
    mk_media_set_on_pause, mk_media_set_on_speed, mk_media_set_on_seek


三、程序设计

1. 服务器注册

方法:将zlmediakit的服务功能放到一个线程中提供。利用zlmediakit api函数进行注册和配置相关服务器功能,注册服务器响应事件。

成员准备

/* 成员数据准备 */
typedef struct {
    AVFormatContext *pFormatCtx;    /* AVFormatContext上下文 */
    AVPacket *packet;               /* AVPacket */
    int videoStream;                /* 视频流下标 */
    int fps;                        /* 每秒帧数 */
    int64_t cur_pts;                /* 当前播放位置 */
    int pause;                      /* 暂停控制位 */
    int speed;                      /* 速度控制位 */
}ffmpegObj;

typedef struct {
    mk_media media;                 /* 媒体对象 */
    mk_h264_splitter splitter;      /* 分帧对象 */
    char absFilePath[512];          /* 文件绝对路径 */
    pthread_t spliterThread;        /* 分帧处理线程.  目的:ffmpeg从文件中获取packet传给媒体对象进行处理 */
    pthread_mutex_t t_mutex;        /* 互斥锁 */
    bool splitFlag;                 /* 分帧线程运行标志 */
    char schema[10];                /* 服务类型 */
}zlMediaSource;

服务器初始化

void zlmedia_api::server()
{
    /* 环境配置 */
    mk_config config;
    config.thread_num = 1;
    config.log_level = 4;
    config.log_mask = LOG_CONSOLE;
    config.log_file_path = NULL;
    config.log_file_days = 7;
    config.ini_is_path = 1;
    config.ini = NULL;
    config.ssl_is_path = 0;
    config.ssl = NULL;
    config.ssl_pwd = NULL;

    mk_env_init(&config);

    /* RTSP服务器申请 */
    mk_rtsp_server_start(554, 0);

    /* server服务器事件响应 */
    mk_events events = {
            .on_mk_media_changed = NULL,
            .on_mk_media_publish = NULL,
            .on_mk_media_play = NULL,
            .on_mk_media_not_found = on_mk_media_not_found,
            .on_mk_media_no_reader = on_mk_media_no_reader,
            .on_mk_http_request = NULL,
            .on_mk_http_access = NULL,
            .on_mk_http_before_access = NULL,
            .on_mk_rtsp_get_realm = NULL,
            .on_mk_rtsp_auth = NULL,
            .on_mk_record_mp4 = NULL,
            .on_mk_shell_login = NULL,
            .on_mk_flow_report = NULL,
    };

    mk_events_listen(&events);

    /* ffmpeg协议注册 */
    avformat_network_init();

    /* 数据库连接 */
    this->initDb();

    /* 成员初始化 */
    this->Ctx.spliterThread = 0;
    this->Ctx.splitFlag = false;
    memset(this->Ctx.absFilePath, '0', sizeof(this->Ctx.absFilePath));
    memset(this->Ctx.schema, '0', sizeof(this->Ctx.schema));

    this->ffmpegObject.videoStream = -1;
    this->ffmpegObject.speed = 1;
    this->ffmpegObject.pause = 0;

    /* 服务器前缀 , 传递给mds的 */
    AppData::RtspURLPrefix = QString("rtsp://%1/live/video?FilePath=").arg(hostIP().toString());
    qDebug() << "zlmediakit url: " << AppData::RtspURLPrefix << "[filename]";

    /* 规定时间内无访问客户终端,释放媒体对象及资源 */
    mk_set_option("general.streamNoneReaderDelayMS", "1000");

}

2.事件监听

on_mk_media_not_found用于注册mk_media对象,并提供播放服务。

1.on_mk_media_not_found响应

/* on_mk_media_not_found响应 */
int API_CALL on_mk_media_not_found(const mk_media_info url_info,
                                    const mk_sock_info sender) {
   zlmedia_api::Instance()->mediaSourceCreate(url_info);
    return 0;
}

2.媒体资源申请

/* 申请/响应逻辑 */
void zlmedia_api::mediaSourceCreate(void *user_data)
{
    mk_media_info url_info = (mk_media_info)user_data;

    const char* schema = mk_media_info_get_schema(url_info);
    const char* app = mk_media_info_get_app(url_info);
    const char* prama = mk_media_info_get_params(url_info);

    /* 从url的FilePath参数获取文件名称 */
    QString strPrama(prama);
    if(!strPrama.startsWith("FilePath="))
        return;

    QStringList strListParam = strPrama.split('=');
    if(strListParam.size() != 2)
        return;

    QString filePath = strListParam.last();
    if(filePath.length() <= 0)
        return;

    /* 从数据库查找文件完整位置 */
    QString absFilePath = this->dbSearchRecord(filePath);
    if(absFilePath.isEmpty())
        return;

    const char *specFilePath = absFilePath.toLatin1().data();
    if(!QFile::exists(absFilePath)){
        qDebug() << "zlmediakit : file is not exist";
        goto fault_missing_file;
    }

    /* 申请媒体资源 */
    if(this->getStopStatus()) {
        if(this->mediaPublish(schema, app, specFilePath) < 0)
            goto fault_open_file;
    }
    else {

        if(strncmp(specFilePath, this->Ctx.absFilePath, strlen(specFilePath)) != 0 ||
           strncmp(schema, this->Ctx.schema, strlen(schema)) != 0) {
             this->stopSpliterThread();
             if(this->mediaPublish(schema, app, specFilePath) < 0)
                 goto fault_open_file;
        }
        
    } /* end: if(this->getStopStatus())*/

    return ;
    
fault_open_file:
fault_missing_file:
    return ;
}

/* 
1.mk_media, mk_h264_splitter资源创建,并指定播放url 。
2.打开文件并配置相关内容。
*/
int zlmedia_api::mediaPublish(const char* schema, const char* app, const char* specFilePath)
{
    strcpy(this->Ctx.schema, schema);
    strcpy(this->Ctx.absFilePath, specFilePath);
    
    /* 总时长 */
    int64_t duration = 0;

    /* openFile函数内部将打开this->Ctx.absFilePath存储的文件,
     * 获取AVFormatContext句柄,
     * 返回文件总时长
    */
    if(this->openFile(&duration) < 0) {
        qDebug() << "ffmpeg can't open file";
        return -1;
    }

    /* 总时长转换 */
    duration /= AV_TIME_BASE;

    /* mk_media 媒体资源创建,并且指定了播放url */
    this->Ctx.media = mk_media_create("__defaultVhost__", "live", "video", duration, 0, 0);
    codec_args v_args = {0};
    mk_track v_track = mk_track_create(MKCodecH264, &v_args);
    mk_media_init_track(this->Ctx.media, v_track);
    mk_media_init_complete(this->Ctx.media);

    /* 暂停,倍速,时间戳功能注册 */
    //mk_media_set_on_regist(this->Ctx.media, on_mk_media_source_regist_func, this);
    mk_media_set_on_pause(this->Ctx.media, mk_media_pause, this);
    mk_media_set_on_speed(this->Ctx.media, mk_media_speed, this);
    mk_media_set_on_seek(this->Ctx.media, mk_media_seek, this);
    mk_track_unref(v_track);

    /* 分帧器创建,必须的 */
    this->Ctx.splitter = mk_h264_splitter_create(on_h264_frame, this, 0);

    /* ffmpeg处理线程 */
    this->Ctx.splitFlag = true;
    this->initPthreadMutex();
    pthread_create(&this->Ctx.spliterThread, NULL, splitter_input_calls, this);

    return 0;
}

3.openFile函数获取ffmpeg相关资源

int zlmedia_api::openFile(int64_t *duration)
{
    int err_code;

    /* 重新申请上下文句柄 */
    if(this->ffmpegObject.pFormatCtx != NULL) {
        avformat_close_input(&this->ffmpegObject.pFormatCtx);
        this->ffmpegObject.pFormatCtx = NULL;
    }

    this->ffmpegObject.pFormatCtx = avformat_alloc_context();
    if (this->ffmpegObject.pFormatCtx == NULL) {
       qDebug() << "zlmedia: failed to alloc format context.";
       return -1;
    }

    /* 重新申请AVPackte */
    if(this->ffmpegObject.packet != NULL) {
        av_packet_free(&this->ffmpegObject.packet);
        this->ffmpegObject.packet = NULL;
    }

    this->ffmpegObject.packet = av_packet_alloc();
    if(this->ffmpegObject.packet == NULL){
        qDebug() << "zlmedia: failed to alloc packet.";
        avformat_close_input(&this->ffmpegObject.pFormatCtx);
        return -1;
    }

    /* 获取文件操作上下文句柄 */
    err_code = avformat_open_input(&this->ffmpegObject.pFormatCtx, this->Ctx.absFilePath, nullptr, nullptr);
    if(err_code < 0){
        qDebug() << "av_open_input_file failed:" << err_code;
        avformat_close_input(&this->ffmpegObject.pFormatCtx);
        av_free_packet(this->ffmpegObject.packet);
        return -1;
    }
    
    if(avformat_find_stream_info(this->ffmpegObject.pFormatCtx, NULL) < 0)
    {
        avformat_close_input(&this->ffmpegObject.pFormatCtx);
        avformat_free_context(this->ffmpegObject.pFormatCtx);
        av_free_packet(this->ffmpegObject.packet);
        return -1;
    }

    /* 获取文件视频流下标和帧率 */
    for(int i = 0; i < this->ffmpegObject.pFormatCtx->nb_streams; i++)
    {
       if(this->ffmpegObject.pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
       {
           this->ffmpegObject.videoStream = i;
           this->ffmpegObject.fps = av_q2d(this->ffmpegObject.pFormatCtx->streams[i]->r_frame_rate);
           break;
       }
    }

    if(this->ffmpegObject.videoStream < 0) {
        return -1;
    }

    /* 获取总时长 */
    *duration = this->ffmpegObject.pFormatCtx->duration;

    return 0;
}

4.分帧器处理回调函数

必须要将视频Packet获取及传递给媒体资源对象的这个过程放置在子线程中执行,否则会阻塞服务线程,导致无法继续开展事件处理。

//pthread_create(&this->Ctx.spliterThread, NULL, splitter_input_calls, this);
void* splitter_input_calls(void *data) {

    zlmedia_api *zlmedia = (zlmedia_api *)data;

    AVFormatContext *pFormatCtx =zlmedia->ffmpegObject.pFormatCtx;
    int i_videoIndex = zlmedia->ffmpegObject.videoStream;
    AVStream *stream = zlmedia->ffmpegObject.pFormatCtx->streams[i_videoIndex];
    AVPacket *packet = zlmedia->ffmpegObject.packet;
    int fps = zlmedia->ffmpegObject.fps;
    int *speed = &zlmedia->ffmpegObject.speed;

    mk_media ctx_media = zlmedia->Ctx.media;
    mk_h264_splitter splitter = zlmedia->Ctx.splitter;

    while(zlmedia->Ctx.splitFlag)
    {
        /* 暂停控制 */
        if(zlmedia->ffmpegObject.pause) {
            pthread_mutex_unlock(&zlmedia->Ctx.t_mutex);
            continue;
        }

        /* 有效数据控制 */
        if(av_read_frame(pFormatCtx, packet) < 0)
            break;

        /* 当前播放位置 */
        uint64_t dts = (packet->pts * av_q2d(stream->time_base)) *1000;
        /* 数据载入间隔 */
        int interval = (int)((1000.0 / ( fps * (*speed) )) * 1000.0);

        /* 数据传递给媒体资源对象和分帧对象处理 */
        if(packet->stream_index == i_videoIndex)
        {
            try {
                /* mk_h264_splitter_input_data
                 * 函数调用,将会把buf和size提交给h264_splitter回调函数on_h264_frame,
                 * 但是,实际提交的数据量和发送的数据量不同,导致最后会丢失部分视频。
                 * 因此直接在这里提交数据给媒体资源对象。
                 * 
                 * 同时该input函数会管理4MB缓存,有溢出风险,导致程序崩溃。
                 * 暂没找到,故不知道使用什么接口,在查看/释放该缓存。
                */
                //mk_h264_splitter_input_data(splitter, (char *)packet->data, packet->size);
                usleep(interval);
                mk_frame frame = mk_frame_create(MKCodecH264, dts, dts, (char *)packet->data, packet->size, NULL, NULL);
                mk_media_input_frame(ctx_media, frame);
                mk_frame_unref(frame);
            }
            catch(...) {
                qDebug() << QDateTime::currentDateTime()  << "Err ZlmeidaApi: mk_h264_splitter_input_data";
            }
        }

        /* 释放AVPacket引用 */
        av_packet_unref(packet);

    } /* end: while */

    /* 释放媒体资源和ffmpeg操作对象 */
    zlmedia->ffmpegObjFree();
    zlmedia->mediaRelease();

    pthread_exit((void *)0);
}

5.媒体资源释放

void zlmedia_api::ffmpegObjFree()
{
    if(this->ffmpegObject.pFormatCtx != NULL) {
        avformat_close_input(&this->ffmpegObject.pFormatCtx);
        this->ffmpegObject.pFormatCtx = NULL;
    }

    if(this->ffmpegObject.packet != NULL) {
        av_packet_free(&this->ffmpegObject.packet);
        this->ffmpegObject.packet = NULL;
    }

    this->ffmpegObject.videoStream = -1;
    this->ffmpegObject.fps = 0;
    this->ffmpegObject.cur_pts = 0;
    this->ffmpegObject.speed = 1;
    this->ffmpegObject.pause = 0;
}

void zlmedia_api::mediaRelease()
{
    if(this->Ctx.media != NULL) {
        mk_media_release(this->Ctx.media);
        this->Ctx.media = NULL;
    }

    if(this->Ctx.splitter != NULL) {
        mk_h264_splitter_release(this->Ctx.splitter);
        this->Ctx.splitter = NULL;
    }

    memset(this->Ctx.absFilePath, '0', sizeof(this->Ctx.absFilePath));
    memset(this->Ctx.schema, '0', sizeof(this->Ctx.schema));

    this->Ctx.splitFlag = false;
}

6.暂停,倍速,时间戳功能

//创建mk_media对象时设置回调函数
//mk_media_set_on_pause(this->Ctx.media, mk_media_pause, this);
//mk_media_set_on_speed(this->Ctx.media, mk_media_speed, this);
//mk_media_set_on_seek(this->Ctx.media, mk_media_seek, this);

static int mk_media_pause(void* user_data, int pause)
{
    zlmedia_api *zlmedia = (zlmedia_api *)user_data;
    zlmedia->ffmpegObject.pause = pause;

    return 0;
}

int mk_media_speed(void* user_data, float speed)
{
    if(speed > 32)
        return -1;

    zlmedia_api *zlmedia = (zlmedia_api *)user_data;
    zlmedia->ffmpegObject.speed = (int)speed;

    return 0;
}

;