ubuntu GStreamer + QT多媒体播放器开发(三)

本篇博客在上一篇ubuntu GStreamer + QT多媒体播放器开发(二)的基础上主要修改新增以下几点:
(1)log 分为trace、info、debug、warn、error五个级别打印;
(2)mmplayer lib新增MMPlayerPause 接口。
(3)QT(UI)层更改为通过control thread 控制播放、暂停、停止等功能,通过command queue实现异步响应UI 点击事件;
(4)QT 窗口退出时增加资源释放的流程。
(5)优化QT project cmake file.
以下对新增的部分简单进行说明。

1 log分级打印

项目开发过程中,日志系统必不可少。对于c语言项目,平常的练习和简单的开发过程中可以直接用 printf 打印程一些调试信息,但是对于大一点的项目,必须分级打印,这样在开发阶段可以把所以log信息打出来,方便调试,而项目正式上线以后只保留一些关键log,提高程序运行效率,log分级打印要点实现过程如下:
(1)定义log打印级别枚举:

/*log level*/
typedef enum {
    
    
    LOG_LEVEL_OFF,
    LOG_LEVEL_ERROR,
    LOG_LEVEL_WARN,
    LOG_LEVEL_INFO,
    LOG_LEVEL_DEBUG,
    LOG_LEVEL_TRACE,
    LOG_LEVEL_ALL,
} LOG_LEVEL;

(2)宏定义不同等级的log打印函数,根据设置的log_level来判断是否打印该级别的log,打印log的宏函数可以通过printf或g_print(glib 提供的打印函数)实现。

#define LIB_NAME "mmplayer"

static LOG_LEVEL log_level;

#define LOG_ENTER()	\
do {
      
      							\
	if (log_level >= LOG_LEVEL_TRACE) {
      
      	\
		g_print("[%s][IN][%s][%d]\n", LIB_NAME, __FUNCTION__, __LINE__);	\
	}	\
} while(0)

#define LOG_OUT()	\
do {
      
      							\
	if (log_level >= LOG_LEVEL_TRACE) {
      
      	\
		g_print("[%s][OUT][%s][%d]\n", LIB_NAME, __FUNCTION__, __LINE__);	\
	}	\
} while(0)

#define LOG_DEBUG(format,...)	\
do {
      
      							\
	if (log_level >= LOG_LEVEL_DEBUG) {
      
      	\
		g_print("[%s][DEBUG][%s][%d]"format"", LIB_NAME, __FUNCTION__, __LINE__, ##__VA_ARGS__);	\
	}	\
} while(0)
 
#define LOG_INFO(format,...)	\
do {
      
      							\
	if (log_level >= LOG_LEVEL_INFO) {
      
      	\
		g_print("[%s][INFO][%s][%d]"format"", LIB_NAME, __FUNCTION__, __LINE__, ##__VA_ARGS__);	\
	}	\
} while(0)
 
#define LOG_WARN(format,...)	\
do {
      
      							\
	if (log_level >= LOG_LEVEL_WARN) {
      
      	\
		g_print("[%s][WARN][%s][%d]"format"", LIB_NAME, __FUNCTION__, __LINE__, ##__VA_ARGS__);	\
	}	\
} while(0)
 
#define LOG_ERROR(format,...)	\
do {
      
      							\
	if (log_level >= LOG_LEVEL_ERROR) {
      
      	\
		g_print("[%s][ERROR][%s][%d]"format"", LIB_NAME, __FUNCTION__, __LINE__, ##__VA_ARGS__);	\
	}	\
} while(0)

(3)初始化的时候设置log级别:

log_level = pstInitParam->logLevel;

印出的log 如下:

[userplayer][INFO][main][38]malloc MEM(0x558bd56563b0)
[mmplayer][OUT][MMPlayerInit][119]
[mmplayer][INFO][handle_element_added][334]elementName uridecodebin3-0. 
[mmplayer][INFO][handle_element_added][334]elementName urisourcebin0. 
[mmplayer][INFO][handle_element_added][334]elementName source. 
[mmplayer][INFO][handle_element_added][334]elementName typefindelement0. 
[userplayer][INFO][hanleCallBackEvent][114]recive event type (0)
[userplayer][INFO][hanleCallBackEvent][119]player init OK! 
[userplayer][INFO][main][63]recive palyer init OK msg
[userplayer][INFO][_palyer_control_thread][151]control thread enter

2 mmplayer lib新增MMPlayerPause 接口

MMPlayerPause实现暂停播放的功能,主要把pipeline状态设置为GST_STATE_PAUSED,内容比较简单。

int MMPlayerPause(HANDLE_ID hanldeId)
{
    
    
    GstStateChangeReturn stateRet;
    int ret = 0;

    LOG_ENTER();

    if (!mediaHandle || !mediaHandle->pipeline)
    {
    
    
        LOG_ERROR ("handle or pipeline is NULL.\n");
        ret = -1;
        goto end;
    }

    stateRet = gst_element_set_state (mediaHandle->pipeline, GST_STATE_PAUSED);
    if (stateRet == GST_STATE_CHANGE_FAILURE) 
    {
    
    
        LOG_ERROR ("Unable to set the pipeline to the pause state.\n");
        ret = -1;
        goto end;
    }

   LOG_OUT();
end:
    if (ret != 0)
    {
    
    
        g_main_loop_quit (mediaHandle->main_loop);
    }
    return ret;
}

3 UI层通过control thread 控制播放、暂停、停止等功能

设计思路:创建一个command queue,点击UI上的按钮时将对应type的cmd和一些data push到queue中,QT中创建一个control thread ,取出cmd queue中的cmd ,根据cmd type 调用mmplayer lib中接口实现播放控制,这样可以实现异步响应UI 操作,可以提升一些耗时比较长场景的用户体验。
cmd type 枚举定义:

typedef enum {
    
    
    CMD_PLAY,
    CMD_PAUSE,
    CMD_RESUME,
    CMD_STOP,
    CMD_SEEK,
    CMD_ALL,
} MM_PLAYER_CMD_TYPE;

cmd queue数据结构的定义:

#define MAX_QUEUE_ESIZE 4
typedef struct _ST_PLAYER_CMD
{
    
    
    MM_PLAYER_CMD_TYPE type;
    int seekTime;
    int volumeValue;
} ST_PLAYER_CMD;

typedef struct _ST_PLAYER_CMD_QUEUE
{
    
    
    ST_PLAYER_CMD *cmd[MAX_QUEUE_ESIZE];
    int front;
    int rear;
    int length;
} ST_PLAYER_CMD_QUEUE;

cmd queue中存放的是ST_PLAYER_CMD 结构体,该结构体存放的是cmd type和一下user data,如做seek时需要设置seek time等,后期需要其他数据再扩充。
接下来定义操作cmd queue的函数,主要涉及queue 初始化、是否为空、是否已满、push cmd 到queue、从queue 中pop一个cmd、 销毁queue,详细的原理可以参考queue数据结构。

bool cmdQueueInit(ST_PLAYER_CMD_QUEUE **cmdQueue);
bool cmdQueueIsEmpty(ST_PLAYER_CMD_QUEUE *cmdQueue);
bool cmdQueueIsFull(ST_PLAYER_CMD_QUEUE *cmdQueue);
bool cmdQueuePush(ST_PLAYER_CMD_QUEUE *cmdQueue);
ST_PLAYER_CMD *cmdQueuePop(ST_PLAYER_CMD_QUEUE *cmdQueue);
bool cmdQueueDeInit(ST_PLAYER_CMD_QUEUE *cmdQueue);
bool createCmdAndPush(ST_USER_HANDLE *userHandle, MM_PLAYER_CMD_TYPE type, void *data);

初始化queque,注意传入的参数是cmd queue指针的指针:

bool cmdQueueInit(ST_PLAYER_CMD_QUEUE **cmdQueue)
{
    
    
    bool ret = false;
    ST_PLAYER_CMD_QUEUE *retQueue = NULL;

    if (NULL == cmdQueue)
    {
    
    
        LOG_ERROR("cmd queue is NULL\n");
        goto end;
    }

    retQueue = (ST_PLAYER_CMD_QUEUE *)g_malloc(sizeof(ST_PLAYER_CMD_QUEUE));
    if (retQueue == NULL)
    {
    
    
        LOG_ERROR("malloc cmd queue error\n");
        goto end;
    }
    memset(retQueue->cmd, 0, sizeof(retQueue->cmd));

    retQueue->rear = retQueue->front = 0;
    retQueue->length = 0;
    *cmdQueue = retQueue;
    ret = true;
end:
    return ret;
}

判断queue是否为空:

扫描二维码关注公众号,回复: 14684307 查看本文章
bool cmdQueueIsEmpty(ST_PLAYER_CMD_QUEUE *cmdQueue)
{
    
    
    bool ret = false;

    if (NULL == cmdQueue)
    {
    
    
        LOG_ERROR("cmd queue is NULL\n");
        goto end;
    }

    ret = (cmdQueue->rear == cmdQueue->front ? true : false);

end:
    return ret;
}

判断queue是否已满:

bool cmdQueueIsFull(ST_PLAYER_CMD_QUEUE *cmdQueue)
{
    
    
    bool ret = false;

    if (NULL == cmdQueue)
    {
    
    
        LOG_ERROR("cmd queue is NULL\n");
        goto end;
    }

    ret = ((cmdQueue->rear + 1) % MAX_QUEUE_ESIZE == cmdQueue->front ? true : false);

end:
    return ret;
}

向queue中push 一个cmd:

bool cmdQueuePush(ST_PLAYER_CMD_QUEUE *cmdQueue, ST_PLAYER_CMD *cmd)
{
    
    
    bool ret = false;

    if (NULL == cmdQueue || NULL == cmd)
    {
    
    
        LOG_ERROR("cmd queue is NULL\n");
        goto end;
    }

    if (cmdQueueIsFull(cmdQueue))
    {
    
    
        LOG_ERROR("cmd queue is full, throw cmd (%d)\n", cmd->type);
        goto end;
    }

    cmdQueue->cmd[cmdQueue->rear] = cmd;
    cmdQueue->rear = (cmdQueue->rear + 1) % MAX_QUEUE_ESIZE;
    ret = true;
    LOG_INFO ("push cmd(%d) to queue success\n", cmd->type);

end:
    return ret;
}

从queue中取出一个cmd:

ST_PLAYER_CMD *cmdQueuePop(ST_PLAYER_CMD_QUEUE *cmdQueue)
{
    
    
    ST_PLAYER_CMD *ret = NULL;
    if (NULL == cmdQueue)
    {
    
    
        LOG_ERROR("cmd queue is NULL\n");
        goto end;
    }
    if (cmdQueue->rear == cmdQueue->front)
    {
    
    
        LOG_ERROR("cmd queue is empty\n");
        goto end;
    }

    ret = cmdQueue->cmd[cmdQueue->front];
    cmdQueue->cmd[cmdQueue->front] = NULL;
    cmdQueue->front = (cmdQueue->front + 1) % MAX_QUEUE_ESIZE;

end:
    return ret;
}

销毁queue:

bool cmdQueueDeInit(ST_PLAYER_CMD_QUEUE *cmdQueue)
{
    
    
    bool ret = false;
    int i = 0;

    if (NULL == cmdQueue)
    {
    
    
        LOG_ERROR("cmd queue is NULL\n");
        goto end;
    }

    LOG_INFO("start free cmd\n");

    for (i = 0; i < MAX_QUEUE_ESIZE; i++)
    {
    
    
        if (cmdQueue->cmd[i])
        {
    
    
            LOG_INFO ("free cmd(%p)\n", cmdQueue->cmd[i]);
            g_free(cmdQueue->cmd[i]);
            cmdQueue->cmd[i] = NULL;
        }
    }

    cmdQueue->rear = cmdQueue->front = 0;
    cmdQueue->length = 0;
    ret = true;
end:
    return ret;
}

UI 向cmd queue中写cmd时不直接调用cmdQueuePop函数,而是基于该函数封装一个createCmdAndPush函数,这样为了扩充后期复杂的cmd,如seek、倍速播放。

bool createCmdAndPush(ST_USER_HANDLE *userHandle, MM_PLAYER_CMD_TYPE type, void *data)
{
    
    
    bool ret = false;
    ST_PLAYER_CMD *cmd = NULL;

    if (NULL == userHandle || type > CMD_ALL)
    {
    
    
        LOG_ERROR("invalid param\n");
        goto end;
    }

    cmd = (ST_PLAYER_CMD *)g_malloc(sizeof(ST_PLAYER_CMD));
    if (cmd == NULL)
    {
    
    
        LOG_ERROR("malloc cmd error\n");
        goto end;
    }
    cmd->type = type;
    switch (type)
    {
    
    
        case CMD_PLAY:
        {
    
    
            break;
        }
        case CMD_PAUSE:
        {
    
    
            break;
        }
        case CMD_RESUME:
        {
    
    
            break;
        }
        case CMD_STOP:
        {
    
    
            break;
        }
        case CMD_SEEK:
        {
    
    
            break;
        }
        default:
        {
    
    
            break;
        }
    }

    g_mutex_lock(&userHandle->queueMutex);
    ret = cmdQueuePush(userHandle->cmdQueue, cmd);
    g_cond_signal(&userHandle->queueCond);
    g_mutex_unlock(&userHandle->queueMutex);

end:

    if (ret == false && cmd != NULL)
    {
    
    
        //need free cmd when error
        g_free(cmd);
        cmd = NULL;
        ret = false;
    }

    return ret;
}

需要注意的是createCmdAndPush函数中向queue中写cmd时必须使用mutex,再配合信号量使用。
UI按钮点击时要push cmd到queue中调用createCmdAndPush函数即可。

void PlayerWindow::onPlayClicked() {
    
    
    bool ret = false;
    MM_PLAYER_CMD_TYPE type = CMD_PLAY;

    if (NULL == pstHandle)
    {
    
    
        LOG_ERROR("pst is NULL\n");
        goto end;
    }
    ret = createCmdAndPush(pstHandle, type, NULL);
    if (!ret)
    {
    
    
        LOG_ERROR("start play error\n");
    }
 end:
    LOG_OUT();
}

以上过程实现cmd 的push,main函数中需要初始化queue以及cmd queue使用的mutex和cond,并创建control thread取出queue中的cmd:

    //init cmd queue
    g_mutex_init(&userHandle->queueMutex);
    g_cond_init(&userHandle->queueCond);
    cmdQueueInit(&userHandle->cmdQueue);

    userHandle->controlThread = g_thread_new("control_thread", _palyer_control_thread, userHandle);
    if (!userHandle->controlThread)
    {
    
    
        LOG_ERROR ("create control thread fail.\n");
    }

_palyer_control_thread的内容如下,取出cmd queue并调用mmplayer lib中的接口,这样一个异步操作就实现了,最后thread 退出时记得销毁cmd queue和queue使用的mutex 和cond。

void *_palyer_control_thread(void* Parameter)
{
    
    
    ST_USER_HANDLE *pstUserHandle = (ST_USER_HANDLE *)Parameter;
    ST_PLAYER_CMD *curCmd = NULL;

    if (NULL == pstUserHandle)
    {
    
    
        LOG_ERROR ("handle is NULL.\n");
    }

    LOG_INFO("control thread enter\n");
    while(pstUserHandle->handleStatus < ERROR_STATUS)
    {
    
    
        while (!cmdQueueIsEmpty(pstUserHandle->cmdQueue))
        {
    
    
            LOG_INFO("queue not empty\n");
            g_mutex_lock(&pstUserHandle->queueMutex);
            curCmd = cmdQueuePop(pstUserHandle->cmdQueue);
            g_mutex_unlock(&pstUserHandle->queueMutex);

            if (curCmd == NULL)
            {
    
    
                LOG_INFO("cmd is empty\n");
                break;
            }

            switch (curCmd->type)
            {
    
    
                case CMD_PLAY:
                {
    
    
                    MMPlayerPlay(pstUserHandle->handleId);
                    break;
                }
                case CMD_PAUSE:
                {
    
    
                    MMPlayerPause(pstUserHandle->handleId);
                    break;
                }
                case CMD_RESUME:
                {
    
    
                    break;
                }
                case CMD_STOP:
                {
    
    
                    MMPlayerStop(pstUserHandle->handleId);
                    break;
                }
                case CMD_SEEK:
                {
    
    
                    break;
                }
                default:
                    break;
            }

            //need free cmd here
            if (curCmd)
            {
    
    
                g_free(curCmd);
            }
        }

        if (cmdQueueIsEmpty(pstUserHandle->cmdQueue))
        {
    
    
            //wait new cmd push in cmd queue
            LOG_INFO("start wait new cmd...\n");
            g_mutex_lock(&pstUserHandle->queueMutex);
            g_cond_wait(&pstUserHandle->queueCond, &pstUserHandle->queueMutex);
            g_mutex_unlock(&pstUserHandle->queueMutex);
            LOG_INFO("wait new cmd done\n");
        }
    }

    LOG_INFO("quit control thread\n");
    cmdQueueDeInit(pstUserHandle->cmdQueue);
    //control thead stop free cmd queue
    if (pstUserHandle->cmdQueue)
    {
    
    
        g_free(pstUserHandle->cmdQueue);
        pstUserHandle->cmdQueue = NULL;
    }

    g_mutex_clear(&pstUserHandle->queueMutex);
    g_cond_clear(&pstUserHandle->queueCond);
    g_thread_unref(pstUserHandle->controlThread);
    pstUserHandle->controlThread = NULL;

    //emit quit signal
    g_mutex_lock(&userHandle->quitMutex);
    g_cond_signal(&userHandle->quitCond);
    g_mutex_unlock(&userHandle->quitMutex);

}

4 QT 窗口退出时增加资源释放的流程

程序中创建了一个player thread ,一个control thread以及一些handle 资源,点击QT 窗口退出时必须保证两个thread都退出,申请的资源都free调,否则会造成内存泄漏。资源释放流程设计如下
(1)关闭窗口时会触发closeEvent,在closeEvent 中push 一个stop cmd :

void PlayerWindow::closeEvent(QCloseEvent *event)
{
    
    
    bool ret = false;
    MM_PLAYER_CMD_TYPE type = CMD_STOP;

    if (NULL == pstHandle)
    {
    
    
        LOG_ERROR("pst is NULL\n");
        goto end;
    }
    ret = createCmdAndPush(pstHandle, type, NULL);
  ...
}

(2)control thread 中取出stop cmd后调用MMPlayerStop函数,MMPlayerStop函数调用后退出main loop,退出paly thread ,释放mmplayer lib中的相关资源,释放完毕后向UI层返回PLAYER_STOP_OK event

    mediaHandle->handleInfo.handleStatus = STOP_STATUS;
    if (mediaHandle->hanlecallBackFn)
    {
    
    
       (mediaHandle->hanlecallBackFn)(PLAYER_STOP_OK ,(void *)&mediaHandle->handleInfo);
    }

(3)UI收到PLAYER_STOP_OK event后将handleStatus 设置为ERROR_STATUS,并调用g_cond_signal,让control thread 退出。

        case PLAYER_STOP_OK:
        {
    
    
            LOG_INFO("pipeline deinit OK! \n");
            userHandle->handleStatus = ERROR_STATUS;
            g_mutex_lock(&userHandle->queueMutex);
            g_cond_signal(&userHandle->queueCond);
            g_mutex_unlock(&userHandle->queueMutex);
            break;
        }

(4)为了让closeEvent等待所有资源释放完毕,使用了条件变量,closeEvent中发送完stop cmd 后开始等待条件变量,control thread 退出后发送信号量,让closeEvent结束等待。
closeEvent:

    ret = createCmdAndPush(pstHandle, type, NULL);
    if (!ret)
    {
    
    
        LOG_ERROR("start play error\n");
    }
    g_mutex_lock(&pstHandle->quitMutex);
    g_cond_wait(&pstHandle->quitCond, &pstHandle->quitMutex);
    g_mutex_unlock(&pstHandle->quitMutex);

control thread:

    //emit quit signal
    g_mutex_lock(&userHandle->quitMutex);
    g_cond_signal(&userHandle->quitCond);
    g_mutex_unlock(&userHandle->quitMutex);

这样就可以安全释放所用申请的资料了。

5 优化QT project cmake file

项目编译的时候先进player_lib中执行make 编译mmplayer lib,编译后会自动把lib的so和头文件copy到QT project 目录。QT project 使用了cmake编译,cmake文件如下:

cmake_minimum_required(VERSION 3.5)
project(gst_player LANGUAGES CXX C)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_PROJECT_NAME gst_player)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(PkgConfig REQUIRED)
pkg_check_modules(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0)
pkg_check_modules(gstreamer-video REQUIRED IMPORTED_TARGET gstreamer-video-1.0)
pkg_check_modules(gobject REQUIRED IMPORTED_TARGET gobject-2.0)
pkg_check_modules(glib REQUIRED IMPORTED_TARGET glib-2.0)

# shell中编译打开以下注释,Qt5_DIR设置为QT安装路径
#set(Qt5_DIR "/home/zhy/Qt5.13.2/5.13.2/gcc_64/lib/cmake/Qt5")
#find_package (Qt5Widgets)
#find_package (Qt5Core)
#find_package (Qt5Gui)

# qt create中编译,shell下编译需注释
find_package(Qt5 REQUIRED COMPONENTS Widgets )

include_directories(${PROJECT_SOURCE_DIR}/common/)
include_directories(${PROJECT_SOURCE_DIR}/libs/)
link_directories(${PROJECT_SOURCE_DIR}/libs/)
find_library (libpath mmplayer ${PROJECT_SOURCE_DIR}/libs/)


if (${libpath} STREQUAL "libpath-NOTFOUND")
    message (STATUS "required mmplayer library but not found!")
else()
    message (STATUS "libpath library found in ${libpath}")
endif()

add_executable(gst_player
  main.cpp
  playerwindow.cpp
  playerwindow.h
  common/common.h
  common/common.cpp
  libs/mediaPlayer.h
)

target_link_libraries(gst_player PRIVATE Qt5::Widgets PkgConfig::gstreamer PkgConfig::gstreamer-video PkgConfig::gobject PkgConfig::glib mmplayer)

项目github地址:https://github.com/zhenghaiyang123/gst_player.git,本篇博客对应的tag为v0.3。

猜你喜欢

转载自blog.csdn.net/qq_38694388/article/details/125118665