当前位置:网站首页>海思3559万能平台搭建:RTSP实时播放的支持
海思3559万能平台搭建:RTSP实时播放的支持
2022-07-06 20:23:00 【快跑bug来啦】
前言
想搭建功能完备的万能平台,rtsp的支持自然必不可少。不论是编码的h264和我们用来实时传输的rtsp,都是可以单拎出来作为研究方向的,所幸在功能支持上而言,暂时全部当做黑盒子调用,还是比较容易实现的。其实知识是学不完的,选取我们有需要的即可。
RTSP
实时流传输协议(Real Time Streaming Protocol,RTSP),RFC2326(中文版),是TCP/IP协议体系中的一个应用层协议,由哥伦比亚大学、网景和RealNetworks公司提交的IETF RFC标准。该协议定义了一对多应用程序如何有效地通过IP网络传送多媒体数据。
RTSP是 TCP/IP 协议体系中的一个应用层协议,该协议定义了一对多应用程序如何有效地通过 IP 网络传送多媒体数据。RTSP在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。HTTP与RTSP相比,HTTP传送HTML,而RTSP传送的是多媒体数据。
RTSP是基于文本的协议,采用ISO10646字符集,使用UTF-8编码方案。行以CRLF中断,包括消息类型、消息头、消息体和消息长。但接收者本身可将CR和LF解释成行终止符。基于文本的协议使其以自描述方式增加可选参数更容易,接口中采用SDP作为描述语言。
RTSP是应用级协议,控制实时数据的发送。RTSP提供了一个可扩展框架,使实时数据,如音频与视频的受控点播成为可能。数据源包括现场数据与存储在剪辑中数据。该协议目的在于控制多个数据发送连接,为选择发送通道,如UDP、组播UDP与TCP,提供途径,并为选择基于RTP上发送机制提供方法。
RTSP建立并控制一个或几个时间同步的连续流媒体。尽管连续媒体流与控制流交换是可能的,通常它本身并不发送连续流。换言之,RTSP充当多媒体服务器的网络远程控制。RTSP连接没有绑定到传输层连接,如TCP。在RTSP连接期间,RTSP用户可打开或关闭多个对服务器的可传输连接以发出RTSP请求。此外,可使用无连接传输协议,如UDP。RTSP流控制的流可能用到RTP,但RTSP操作并不依赖用于携带连续媒体的传输机制。
简单一扫百度百科,瞬间云里雾里,不考虑技术实现方式,大白话说,就是实现了将编码完成后的音视频文件进行了实时的播放而已,我们的sample已经自带了保存编码完成后的音视频文件功能,只需要调用rtsp库,完成功能的支持其实就可以了
移植过程
之前venc 的sample分析里已经讲到SAMPLE_COMM_VENC_StartGetStream是用来保存文件的函数,不幸的是,这个函数是在common目录下的,Makefile在编译时会将这一目录下的所有文件加进来,为了不必要的麻烦,除了添加注释之外,一般不要擅自更改这些地方的函数
另外,出于个人习惯,将原sample的中所有可能修改的都重新参考sample写一遍,一是可以加深印象,而来修改起来非常方便,不用估计会给其他的sample文件带来影响
编码文件保存
pthread_t gs_RtspVencPid;
static SAMPLE_VENC_GETSTREAM_PARA_S gs_stPara;
/* *描述 :调用RTSP静态库后,用于实时显示编码后图像 *参数 :VeChn[] 编码通道号 s32Cnt 通道数 *返回值:创建线程PLATFORM_VENC_GetVencStreamRtsp,传递结构体gs_stPara *注意 :无 */
HI_S32 PLATFORM_VENC_StartGetStreamRtsp(VENC_CHN VeChn[],HI_S32 s32Cnt)
{
HI_U32 i;
gs_stPara.bThreadStart = HI_TRUE;
gs_stPara.s32Cnt = s32Cnt;
for(i=0; i<s32Cnt; i++)
{
gs_stPara.VeChn[i] = VeChn[i];
}
return pthread_create(&gs_RtspVencPid, 0, PLATFORM_VENC_GetVencStreamRtsp, (HI_VOID*)&gs_stPara);
}
/****************************************************************************** *描述 :结束RTSP实时播放线程 *参数 :无 *返回值:成功返回0 *注意 :无 ******************************************************************************/
HI_S32 PLATFORM_VENC_StopGetStreamRtsp(void)
{
if (HI_TRUE == gs_stPara.bThreadStart)
{
gs_stPara.bThreadStart = HI_FALSE;
pthread_join(gs_RtspVencPid, 0);
}
return HI_SUCCESS;
}
/****************************************************************************** *描述 :保存文件 *参数 :pFd 文件描述符 pstStream 帧码流类型结构体。 *返回值:成功返回0 *注意 :无 ******************************************************************************/
HI_S32 PLATFORM_VENC_SaveStream(FILE* pFd, VENC_STREAM_S* pstStream)
{
HI_S32 i;
for (i = 0; i < pstStream->u32PackCount; i++)
{
fwrite(pstStream->pstPack[i].pu8Addr + pstStream->pstPack[i].u32Offset,
pstStream->pstPack[i].u32Len - pstStream->pstPack[i].u32Offset, 1, pFd);
fflush(pFd);
}
return HI_SUCCESS;
}
/****************************************************************************** *描述 :根据enPayload获取文件后缀 *参数 :enPayload 音视频净荷类型枚举 szFilePostfix 文件前缀 *返回值:成功返回0,失败返回-1 ******************************************************************************/
HI_S32 PLATFORM_VENC_GetFilePostfix(PAYLOAD_TYPE_E enPayload, char* szFilePostfix)
{
if (PT_H264 == enPayload)
{
strcpy(szFilePostfix, ".h264");
}
else if (PT_H265 == enPayload)
{
strcpy(szFilePostfix, ".h265");
}
else if (PT_JPEG == enPayload)
{
strcpy(szFilePostfix, ".jpg");
}
else if (PT_MJPEG == enPayload)
{
strcpy(szFilePostfix, ".mjp");
}
else if (PT_PRORES == enPayload)
{
strcpy(szFilePostfix, ".prores");
}
else
{
SAMPLE_PRT("payload type err!\n");
return HI_FAILURE;
}
return HI_SUCCESS;
}
/* *描述 :调用RTSP静态库后,用于实时显示编码后图像 *参数 :p 线程传递进来的结构体指针原型为 SAMPLE_VENC_GETSTREAM_PARA_S *返回值:NULL *注意 :无 */
HI_VOID* PLATFORM_VENC_GetVencStreamRtsp(HI_VOID* p)
{
HI_S32 i;
HI_S32 s32ChnTotal;
VENC_CHN_ATTR_S stVencChnAttr;
SAMPLE_VENC_GETSTREAM_PARA_S* pstPara;
HI_S32 maxfd = 0;
struct timeval TimeoutVal;
fd_set read_fds;
HI_U32 u32PictureCnt[VENC_MAX_CHN_NUM]={
0};
HI_S32 VencFd[VENC_MAX_CHN_NUM];
HI_CHAR aszFileName[VENC_MAX_CHN_NUM][64];
FILE* pFile[VENC_MAX_CHN_NUM];
char szFilePostfix[10];
VENC_CHN_STATUS_S stStat;
VENC_STREAM_S stStream;
HI_S32 s32Ret;
VENC_CHN VencChn;
PAYLOAD_TYPE_E enPayLoadType[VENC_MAX_CHN_NUM];
VENC_STREAM_BUF_INFO_S stStreamBufInfo[VENC_MAX_CHN_NUM];
prctl(PR_SET_NAME, "GetVencStream", 0,0,0);
pstPara = (SAMPLE_VENC_GETSTREAM_PARA_S*)p;
s32ChnTotal = pstPara->s32Cnt;
/****************************************** step 1: check & prepare save-file & venc-fd 检查并准备保存文件和venc fd ******************************************/
if (s32ChnTotal >= VENC_MAX_CHN_NUM)
{
SAMPLE_PRT("input count invaild\n");
return NULL;
}
for (i = 0; i < s32ChnTotal; i++)
{
/* decide the stream file name, and open file to save stream 确定视频流文件名,并打开文件以保存视频流*/
VencChn = pstPara->VeChn[i];
s32Ret = HI_MPI_VENC_GetChnAttr(VencChn, &stVencChnAttr);
if (s32Ret != HI_SUCCESS)
{
SAMPLE_PRT("HI_MPI_VENC_GetChnAttr chn[%d] failed with %#x!\n", \
VencChn, s32Ret);
return NULL;
}
enPayLoadType[i] = stVencChnAttr.stVencAttr.enType;
s32Ret = PLATFORM_VENC_GetFilePostfix(enPayLoadType[i], szFilePostfix);
if (s32Ret != HI_SUCCESS)
{
SAMPLE_PRT("PLATFORM_VENC_GetFilePostfix [%d] failed with %#x!\n", \
stVencChnAttr.stVencAttr.enType, s32Ret);
return NULL;
}
if(PT_JPEG != enPayLoadType[i])
{
snprintf(aszFileName[i],32, "./RTSP/RTSP_chn%d%s", i, szFilePostfix);
pFile[i] = fopen(aszFileName[i], "wb");
if (!pFile[i])
{
SAMPLE_PRT("open file[%s] failed!\n",
aszFileName[i]);
return NULL;
}
}
/* Set Venc Fd. */
VencFd[i] = HI_MPI_VENC_GetFd(i);
if (VencFd[i] < 0)
{
SAMPLE_PRT("HI_MPI_VENC_GetFd failed with %#x!\n",
VencFd[i]);
return NULL;
}
if (maxfd <= VencFd[i])
{
maxfd = VencFd[i];
}
s32Ret = HI_MPI_VENC_GetStreamBufInfo (i, &stStreamBufInfo[i]);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("HI_MPI_VENC_GetStreamBufInfo failed with %#x!\n", s32Ret);
return (void *)HI_FAILURE;
}
}
/****************************************** step 2: Start to get streams of each channel. 开始获取每个通道的视频流 ******************************************/
while (HI_TRUE == pstPara->bThreadStart)
{
FD_ZERO(&read_fds);
for (i = 0; i < s32ChnTotal; i++)
{
FD_SET(VencFd[i], &read_fds);
}
TimeoutVal.tv_sec = 2;
TimeoutVal.tv_usec = 0;
s32Ret = select(maxfd + 1, &read_fds, NULL, NULL, &TimeoutVal);
if (s32Ret < 0)
{
SAMPLE_PRT("select failed!\n");
break;
}
else if (s32Ret == 0)
{
SAMPLE_PRT("get venc stream time out, exit thread\n");
continue;
}
else
{
for (i = 0; i < s32ChnTotal; i++)
{
if (FD_ISSET(VencFd[i], &read_fds))
{
/******************************************************* step 2.1 : query how many packs in one-frame stream. 查询每个帧流中有多少包。 *******************************************************/
memset(&stStream, 0, sizeof(stStream));
s32Ret = HI_MPI_VENC_QueryStatus(i, &stStat);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("HI_MPI_VENC_QueryStatus chn[%d] failed with %#x!\n", i, s32Ret);
break;
}
/******************************************************* step 2.2 :suggest to check both u32CurPacks and u32LeftStreamFrames at the same time,for example: 建议同时检查u32CurPacks和u32LeftStreamFrames if(0 == stStat.u32CurPacks || 0 == stStat.u32LeftStreamFrames) { SAMPLE_PRT("NOTE: Current frame is NULL!\n"); continue; } *******************************************************/
if(0 == stStat.u32CurPacks)
{
SAMPLE_PRT("NOTE: Current frame is NULL!\n");
continue;
}
/******************************************************* step 2.3 : malloc corresponding number of pack nodes. malloc对应的包节点数。 *******************************************************/
stStream.pstPack = (VENC_PACK_S*)malloc(sizeof(VENC_PACK_S) * stStat.u32CurPacks);
if (NULL == stStream.pstPack)
{
SAMPLE_PRT("malloc stream pack failed!\n");
break;
}
/******************************************************* step 2.4 : call mpi to get one-frame stream 调用mpi获取一个帧流 *******************************************************/
stStream.u32PackCount = stStat.u32CurPacks;
s32Ret = HI_MPI_VENC_GetStream(i, &stStream, HI_TRUE);
if (HI_SUCCESS != s32Ret)
{
free(stStream.pstPack);
stStream.pstPack = NULL;
SAMPLE_PRT("HI_MPI_VENC_GetStream failed with %#x!\n", \
s32Ret);
break;
}
/******************************************************* step 2.5 : save frame to file 将框架保存到文件 *******************************************************/
if(PT_JPEG == enPayLoadType[i])
{
snprintf(aszFileName[i],32, "stream_chn%d_%d%s", i, u32PictureCnt[i],szFilePostfix);
pFile[i] = fopen(aszFileName[i], "wb");
if (!pFile[i])
{
SAMPLE_PRT("open file err!\n");
return NULL;
}
}
#ifndef __HuaweiLite__
s32Ret = PLATFORM_VENC_SaveStream(pFile[i], &stStream);
#else
s32Ret = SAMPLE_COMM_VENC_SaveStream_PhyAddr(pFile[i], &stStreamBufInfo[i], &stStream);
#endif
if (HI_SUCCESS != s32Ret)
{
free(stStream.pstPack);
stStream.pstPack = NULL;
SAMPLE_PRT("save stream failed!\n");
break;
}
/******************************************************* step 2.6 : release stream 释放视频流 *******************************************************/
s32Ret = HI_MPI_VENC_ReleaseStream(i, &stStream);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("HI_MPI_VENC_ReleaseStream failed!\n");
free(stStream.pstPack);
stStream.pstPack = NULL;
break;
}
/******************************************************* step 2.7 : free pack nodes 释放包节点 *******************************************************/
free(stStream.pstPack);
stStream.pstPack = NULL;
u32PictureCnt[i]++;
if(PT_JPEG == enPayLoadType[i])
{
fclose(pFile[i]);
}
}
}
}
}
/******************************************************* * step 3 : close save-file *******************************************************/
for (i = 0; i < s32ChnTotal; i++)
{
if(PT_JPEG != enPayLoadType[i])
{
fclose(pFile[i]);
}
}
return NULL;
}
MakeFile修改
rtsp库我们更改交叉编译环境(修改rtsp库的Makefile),重新编译生成静态库后,修改海思的Makefile,添加:
RTSP_DIR ?= $(PWD)/../../rtsp_lib
INC_FLAGS += -I$(RTSP_DIR)
SENSOR_LIBS += $(REL_LIB)/librtsp.a
路径完全看自己放的位置所决定,静态库暂时放在了默认位置,后续可以改动
RTSP移植
参考demo定义加载文件结构体,后续会升级!
#define MAX_SESSION_NUM 8 //rtsp最大接口数
#define DEMO_CFG_FILE "platform.ini"
/* 配置文件结构体 */
typedef struct demo_cfg_para
{
int session_count;
struct {
char path[64];
char video_file[64];
char audio_file[64];
} session_cfg[MAX_SESSION_NUM];
}demo_cfg;
static int flag_run = 1;
static void sig_proc(int signo)
{
flag_run = 0;
}
static int get_next_video_frame (FILE *fp, uint8_t **buff, int *size)
{
uint8_t szbuf[1024];
int szlen = 0;
int ret;
if (!(*buff)) {
*buff = (uint8_t*)malloc(2*1024*1024);
if (!(*buff))
return -1;
}
*size = 0;
while ((ret = fread(szbuf + szlen, 1, sizeof(szbuf) - szlen, fp)) > 0) {
int i = 3;
szlen += ret;
while (i < szlen - 3 && !(szbuf[i] == 0 && szbuf[i+1] == 0 && (szbuf[i+2] == 1 || (szbuf[i+2] == 0 && szbuf[i+3] == 1)))) i++;
memcpy(*buff + *size, szbuf, i);
*size += i;
memmove(szbuf, szbuf + i, szlen - i);
szlen -= i;
if (szlen > 3) {
//printf("szlen %d\n", szlen);
fseek(fp, -szlen, SEEK_CUR);
break;
}
}
if (ret > 0)
return *size;
return 0;
}
static int get_next_audio_frame (FILE *fp, uint8_t **buff, int *size)
{
int ret;
#define AUDIO_FRAME_SIZE 320
if (!(*buff)) {
*buff = (uint8_t*)malloc(AUDIO_FRAME_SIZE);
if (!(*buff))
return -1;
}
ret = fread(*buff, 1, AUDIO_FRAME_SIZE, fp);
if (ret > 0) {
*size = ret;
return ret;
}
return 0;
}
int load_cfg(demo_cfg *cfg, const char *cfg_file)
{
//cfgline: path=%s video=%s audio=%s
FILE *fp = fopen(cfg_file, "r");
char line[256];
int count = 0;
if (!fp) {
fprintf(stderr, "open %s failed\n", cfg_file);
return -1;
}
memset(cfg, 0, sizeof(*cfg));
while (fgets(line, sizeof(line) - 1, fp)) {
const char *p;
memset(&cfg->session_cfg[count], 0, sizeof(cfg->session_cfg[count]));
if (line[0] == '#')
continue;
p = strstr(line, "path=");
if (!p)
continue;
if (sscanf(p, "path=%s", cfg->session_cfg[count].path) != 1)
continue;
if ((p = strstr(line, "video="))) {
if (sscanf(p, "video=%s", cfg->session_cfg[count].video_file) != 1) {
fprintf(stderr, "parse video file failed %s\n", p);
}
}
if ((p = strstr(line, "audio="))) {
if (sscanf(p, "audio=%s", cfg->session_cfg[count].audio_file) != 1) {
fprintf(stderr, "parse audio file failed %s\n", p);
}
}
if (strlen(cfg->session_cfg[count].video_file) || strlen(cfg->session_cfg[count].audio_file)) {
count ++;
} else {
fprintf(stderr, "parse line %s failed\n", line);
}
}
cfg->session_count = count;
/* path=/live/chn0 video=BarbieGirl.h264 audio=BarbieGirl.alaw path=/live/chn1 video=BarbieGirl.h264 path=/live/chn2 audio=BarbieGirl.alaw */
printf("cfg->session_count:%d\n",cfg->session_count);//3
fclose(fp);
return count;
}
/* *描述 :用于rtsp实时播放的线程 *参数 :NULL *返回值:无 *注意 :加载文件demo.cfg path=/mnt/sample/venc/RTSP video=RTSP_chn1.h264 */
void *video_play_rtsp_task(void*arg)
{
const char *cfg_file = DEMO_CFG_FILE;
demo_cfg cfg;
FILE *fp[MAX_SESSION_NUM][2] = {
{
NULL}};
rtsp_demo_handle demo;
rtsp_session_handle session[MAX_SESSION_NUM] = {
NULL};
int session_count = 0;
uint8_t *vbuf = NULL;
uint8_t *abuf = NULL;
uint64_t ts = 0;
int vsize = 0, asize = 0;
int ret, ch;
ret = load_cfg(&cfg, cfg_file);
demo = rtsp_new_demo(8554);//rtsp sever socket
if (NULL == demo) {
SAMPLE_PRT("rtsp new demo failed!\n");
return 0;
}
session_count = 1;
for (ch = 0; ch < session_count; ch++)
{
if (strlen(cfg.session_cfg[ch].video_file)) {
fp[ch][0] = fopen(cfg.session_cfg[ch].video_file, "rb");//打开视频文件
if (!fp[ch][0]) {
fprintf(stderr, "open %s failed\n", cfg.session_cfg[ch].video_file);
}
}
//fp[ch][1] :音频文件的句柄
// if (strlen(cfg.session_cfg[ch].audio_file)) {
// fp[ch][1] = fopen(cfg.session_cfg[ch].audio_file, "rb");
// if (!fp[ch][1]) {
// fprintf(stderr, "open %s failed\n", cfg.session_cfg[ch].audio_file);
// }
// }
if (fp[ch][0] == NULL && fp[ch][1] == NULL)
continue;
session[ch] = rtsp_new_session(demo, cfg.session_cfg[ch].path);//对应rtsp session
if (NULL == session[ch]) {
printf("rtsp_new_session failed\n");
continue;
}
if (fp[ch][0]) {
//当前请求路径存视频数据源
rtsp_set_video(session[ch], RTSP_CODEC_ID_VIDEO_H264, NULL, 0);
rtsp_sync_video_ts(session[ch], rtsp_get_reltime(), rtsp_get_ntptime());
}
printf("==========> rtsp://192.168.119.200:8554%s for %s <===========\n", cfg.session_cfg[ch].path,
fp[ch][0] ? cfg.session_cfg[ch].video_file : "");
}
ts = rtsp_get_reltime();
signal(SIGINT, sig_proc);
while (flag_run) {
uint8_t type = 0;
for (ch = 0; ch < session_count; ch++) {
//3个源
if (fp[ch][0]) {
read_video_again:
ret = get_next_video_frame(fp[ch][0], &vbuf, &vsize);
if (ret < 0) {
fprintf(stderr, "get_next_video_frame failed\n");
flag_run = 0;
break;
}
if (ret == 0) {
fseek(fp[ch][0], 0, SEEK_SET);
if (fp[ch][1])
fseek(fp[ch][1], 0, SEEK_SET);
goto read_video_again;
}
if (session[ch])//1源session 存存
rtsp_tx_video(session[ch], vbuf, vsize, ts);//2rtsp_client_connect存在
type = 0;
if (vbuf[0] == 0 && vbuf[1] == 0 && vbuf[2] == 1) {
type = vbuf[3] & 0x1f;
}
if (vbuf[0] == 0 && vbuf[1] == 0 && vbuf[2] == 0 && vbuf[3] == 1) {
type = vbuf[4] & 0x1f;
}
if (type != 5 && type != 1)
goto read_video_again;
}
if (fp[ch][1]) {
ret = get_next_audio_frame(fp[ch][1], &abuf, &asize);
if (ret < 0) {
fprintf(stderr, "get_next_audio_frame failed\n");
break;
}
if (ret == 0) {
fseek(fp[ch][1], 0, SEEK_SET);
if (fp[ch][0])
fseek(fp[ch][0], 0, SEEK_SET);
continue;
}
if (session[ch])
rtsp_tx_audio(session[ch], abuf, asize, ts);
}
}
do {
ret = rtsp_do_event(demo);//
if (ret > 0)
continue;
if (ret < 0)
break;
usleep(20000);
} while (rtsp_get_reltime() - ts < 1000000 / 25);
if (ret < 0)
break;
ts += 1000000 / 25;
printf(".");fflush(stdout);//立即将printf的数据输出显示
}
free(vbuf);
free(abuf);
for (ch = 0; ch < session_count; ch++) {
if (fp[ch][0])
fclose(fp[ch][0]);
if (fp[ch][1])
fclose(fp[ch][1]);
if (session[ch])
rtsp_del_session(session[ch]);
}
rtsp_del_demo(demo);
printf("Exit.\n");
getchar();
return 0;
}
边栏推荐
- 2022.6.28
- Don't you know the relationship between JSP and servlet?
- 【无标题】
- How to replace the backbone of the model
- 图形化工具打包YOLOv5,生成可执行文件EXE
- RestClould ETL 社区版六月精选问答
- Set WiFi automatic connection for raspberry pie
- Tencent cloud native database tdsql-c was selected into the cloud native product catalog of the Academy of communications and communications
- Do you know the five most prominent advantages of E-bidding?
- Cocos2d-x box2d physical engine compilation settings
猜你喜欢
Make (convert) ICO Icon
Jericho is in non Bluetooth mode. Do not jump back to Bluetooth mode when connecting the mobile phone [chapter]
小程序能运行在自有App中,且实现直播和连麦?
存储过程与函数(MySQL)
19.(arcgis api for js篇)arcgis api for js线采集(SketchViewModel)
ubuntu20安裝redisjson記錄
变量、流程控制与游标(MySQL)
源代码保密的意义和措施
[cpk-ra6m4 development board environment construction based on RT thread studio]
22.(arcgis api for js篇)arcgis api for js圆采集(SketchViewModel)
随机推荐
22.(arcgis api for js篇)arcgis api for js圆采集(SketchViewModel)
Set static IP for raspberry pie
树莓派设置wifi自动连接
Set WiFi automatic connection for raspberry pie
Not All Points Are Equal Learning Highly Efficient Point-based Detectors for 3D LiDAR Point
Starting from 1.5, build a micro Service Framework -- log tracking traceid
Sorting operation partition, argpartition, sort, argsort in numpy
HDU 4337 King Arthur&#39;s Knights 它输出一个哈密顿电路
Jerry's FM mode mono or stereo selection setting [chapter]
Flutter3.0, the applet is not only run across mobile applications
Make (convert) ICO Icon
SQL中删除数据
How to customize the shortcut key for latex to stop running
线性表的查找
Enumeration general interface & enumeration usage specification
Domcontentloaded and window onload
Flutter3.0了,小程序不止于移动应用跨端运行
leetcode
Hazel engine learning (V)
netperf 而网络性能测量