本篇博客在上一篇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是否为空:
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。