live555 rtsp服务器实战之doGetNextFrame

live555关于RTSP协议交互流程

live555的核心数据结构值之闭环双向链表

live555 rtsp服务器实战之createNewStreamSource

live555 rtsp服务器实战之doGetNextFrame

注意:该篇文章可能有些绕,最好跟着文章追踪下源码,不了解源码可能就是天书;

概要

        live555用于实际项目开发时,createNewStreamSource和doGetNextFrame是必须要实现的两个虚函数,一般会创建两个类来实现这两个函数:假如这两个类为H264LiveVideoServerMediaSubssion和H264FramedLiveSource;H264LiveVideoServerMediaSubssion为实时视频会话类,用于实现createNewStreamSource虚函数;

        H264FramedLiveSource为实时视频帧资源类,用户实现doGetNextFrame函数;

        那么这两个函数是什么时候被调用以及他们的作用是什么呢?本节将详细介绍;

声明:该文章基于H264视频源为基础分析,其他源类似;

        由于这两个类主要是自定义虚函数,所以存在一定的继承关系,首先需要明确这两个类的继承关系(本章只介绍H264FramedLiveSource):

        H264FramedLiveSource:FramedSource:MediaSource:Medium

doGetNextFrame调用流程

        doGetNextFrame函数声明于FrameSource类:

virtual void doGetNextFrame() = 0;

        该声明为纯虚函数,所以必须有子类对该函数进行实现,H264FramedLiveSource类就是用于实现该函数;doGetNextFrame函数只有一个功能:获取视频帧;

void H264FramedLiveSource::doGetNextFrame()
{uint8_t *frame = new uint8_t[1024*1024];int len = 0;//获取一帧视频帧get_frame(&frame, len);//将帧长度赋值给父类的成员变量fFrameSizefFrameSize = len;//将帧数据赋值给父类的成员变量fTomemcpy(fTo, frame, len);delete [] frame;// nextTask() = envir().taskScheduler().scheduleDelayedTask(0,(TaskFunc*)FramedSource::afterGetting, this);//表示延迟0秒后再执行 afterGetting 函数afterGetting(this);return;
}

        fFrameSize和fTo的两个父类变量在流传输时会用到;那么问题来了:doGetNextFrame函数在整个流程中在哪里被调用?什么时候调用?

        首先doGetNextFrame函数是在PLAY信令交互的时候被调用;调用流程为:

handleCmd_PLAY->startStream->startPlaying(OnDemandServerMediaSubsession)->startPlaying(MediaSink)->continuePlaying(虚函数,子类H264or5VideoRTPSink实现)->continuePlaying(虚函数,子类MultiFramedRTPSink实现)->buildAndSendPacket->packFrame(MultiFramedRTPSink)->getNextFrame->doGetNextFrame

        下面详解介绍下这个流程,看过我的另一篇文章:"live555 rtsp服务器实战之createNewStreamSource" 的都知道handleRequestBytes函数调用了handleCmd_SETUP;同样handleCmd_PLAY函数也是在这里被调用的;handleRequestBytes函数就是用来处理各种客户端信令交互信息的;handleRequestBytes函数如下:

void RTSPServer::RTSPClientConnection::handleRequestBytes(int newBytesRead)
{...if (urlIsRTSPS != fOurRTSPServer.fOurConnectionsUseTLS){
#ifdef DEBUGfprintf(stderr, "Calling handleCmd_redirect()\n");
#endifhandleCmd_redirect(urlSuffix);}else if (strcmp(cmdName, "OPTIONS") == 0){// If the "OPTIONS" command included a "Session:" id for a session that doesn't exist,// then treat this as an error:if (requestIncludedSessionId && clientSession == NULL){
#ifdef DEBUGfprintf(stderr, "Calling handleCmd_sessionNotFound() (case 1)\n");
#endifhandleCmd_sessionNotFound();}else{// Normal case:handleCmd_OPTIONS();}}else if (urlPreSuffix[0] == '\0' && urlSuffix[0] == '*' && urlSuffix[1] == '\0'){// The special "*" URL means: an operation on the entire server.  This works only for GET_PARAMETER and SET_PARAMETER:if (strcmp(cmdName, "GET_PARAMETER") == 0){handleCmd_GET_PARAMETER((char const *)fRequestBuffer);}else if (strcmp(cmdName, "SET_PARAMETER") == 0){handleCmd_SET_PARAMETER((char const *)fRequestBuffer);}else{handleCmd_notSupported();}}else if (strcmp(cmdName, "DESCRIBE") == 0){handleCmd_DESCRIBE(urlPreSuffix, urlSuffix, (char const *)fRequestBuffer);}else if (strcmp(cmdName, "SETUP") == 0){Boolean areAuthenticated = True;if (!requestIncludedSessionId){// No session id was present in the request.// So create a new "RTSPClientSession" object for this request.// But first, make sure that we're authenticated to perform this command:char urlTotalSuffix[2 * RTSP_PARAM_STRING_MAX];// enough space for urlPreSuffix/urlSuffix'\0'urlTotalSuffix[0] = '\0';if (urlPreSuffix[0] != '\0'){strcat(urlTotalSuffix, urlPreSuffix);strcat(urlTotalSuffix, "/");}strcat(urlTotalSuffix, urlSuffix);if (authenticationOK("SETUP", urlTotalSuffix, (char const *)fRequestBuffer)){clientSession = (RTSPServer::RTSPClientSession *)fOurRTSPServer.createNewClientSessionWithId();}else{areAuthenticated = False;}}if (clientSession != NULL){clientSession->handleCmd_SETUP(this, urlPreSuffix, urlSuffix, (char const *)fRequestBuffer);playAfterSetup = clientSession->fStreamAfterSETUP;}else if (areAuthenticated){
#ifdef DEBUGfprintf(stderr, "Calling handleCmd_sessionNotFound() (case 2)\n");
#endifhandleCmd_sessionNotFound();}}else if (strcmp(cmdName, "TEARDOWN") == 0 || strcmp(cmdName, "PLAY") == 0 || strcmp(cmdName, "PAUSE") == 0 || strcmp(cmdName, "GET_PARAMETER") == 0 || strcmp(cmdName, "SET_PARAMETER") == 0){if (clientSession != NULL){clientSession->handleCmd_withinSession(this, cmdName, urlPreSuffix, urlSuffix, (char const *)fRequestBuffer);}else{
#ifdef DEBUGfprintf(stderr, "Calling handleCmd_sessionNotFound() (case 3)\n");
#endifhandleCmd_sessionNotFound();}}else if (strcmp(cmdName, "REGISTER") == 0 || strcmp(cmdName, "DEREGISTER") == 0){// Because - unlike other commands - an implementation of this command needs// the entire URL, we re-parse the command to get it:char *url = strDupSize((char *)fRequestBuffer);if (sscanf((char *)fRequestBuffer, "%*s %s", url) == 1){// Check for special command-specific parameters in a "Transport:" header:Boolean reuseConnection, deliverViaTCP;char *proxyURLSuffix;parseTransportHeaderForREGISTER((const char *)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix);handleCmd_REGISTER(cmdName, url, urlSuffix, (char const *)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix);delete[] proxyURLSuffix;}else{handleCmd_bad();}delete[] url;}else{// The command is one that we don't handle:handleCmd_notSupported();}...
}

        handleCmd_withinSession函数内部就调用了handleCmd_PLAY函数;仅整理流程,调用途中的函数这里不做解释;直接跳到packFrame(MultiFramedRTPSink)函数:

void MultiFramedRTPSink::packFrame()
{// Get the next frame.// First, skip over the space we'll use for any frame-specific header:fCurFrameSpecificHeaderPosition = fOutBuf->curPacketSize();fCurFrameSpecificHeaderSize = frameSpecificHeaderSize();fOutBuf->skipBytes(fCurFrameSpecificHeaderSize);fTotalFrameSpecificHeaderSizes += fCurFrameSpecificHeaderSize;// See if we have an overflow frame that was too big for the last pktif (fOutBuf->haveOverflowData()){// Use this frame before reading a new one from the sourceunsigned frameSize = fOutBuf->overflowDataSize();struct timeval presentationTime = fOutBuf->overflowPresentationTime();unsigned durationInMicroseconds = fOutBuf->overflowDurationInMicroseconds();fOutBuf->useOverflowData();afterGettingFrame1(frameSize, 0, presentationTime, durationInMicroseconds);}else{// Normal case: we need to read a new frame from the sourceif (fSource == NULL)return;fSource->getNextFrame(fOutBuf->curPtr(), fOutBuf->totalBytesAvailable(),afterGettingFrame, this, ourHandleClosure, this);}
}

        这里调用了getNextFrame函数,来看下getNextFrame函数的实现:

void FramedSource::getNextFrame(unsigned char* to, unsigned maxSize,afterGettingFunc* afterGettingFunc,void* afterGettingClientData,onCloseFunc* onCloseFunc,void* onCloseClientData) {// Make sure we're not already being read:if (fIsCurrentlyAwaitingData) {envir() << "FramedSource[" << this << "]::getNextFrame(): attempting to read more than once at the same time!\n";envir().internalError();}fTo = to;fMaxSize = maxSize;fNumTruncatedBytes = 0; // by default; could be changed by doGetNextFrame()fDurationInMicroseconds = 0; // by default; could be changed by doGetNextFrame()fAfterGettingFunc = afterGettingFunc;fAfterGettingClientData = afterGettingClientData;fOnCloseFunc = onCloseFunc;fOnCloseClientData = onCloseClientData;fIsCurrentlyAwaitingData = True;doGetNextFrame();
}

        发现啦!发现doGetNextFrame函数啦!所以doGetNextFrame函数就是在getNextFrame内被调用的;但是问题来了:搜索可以发现在live555中doGetNextFrame函数很多;怎么就确定这里的doGetNextFrame和我们自定义的doGetNextFrame有关呢?

        问的好!那这的关键点就在于fSource->getNextFrame的fSource变量到底是什么类的对象;才能确定doGetNextFrame到底调用的是哪个类的函数;fSource属于MediaSink类的成员变量:

FramedSource* fSource;

        但是FrameSource子类很多;所以需要确定fSource到底指向的是哪个子类;

        既然fSource是MediaSink类的成员,那么fSource肯定是在MediaSink或其子类中被赋值;因此查找下这些类;首先明确下MediaSink的继承关系,从fSource调用的类开始查找:

//父类
MultiFramedRTPSink:RTPSink:MediaSink:Medium
//子类
H264VideoRTPSink:H264or5VideoRTPSink:VideoRTPSink:MultiFramedRTPSink

        因此在这些类中查找即可;根据调用流程可知startPlaying最先赋值,值是startPlaying的第一个参数;

Boolean MediaSink::startPlaying(MediaSource& source,afterPlayingFunc* afterFunc,void* afterClientData) {// Make sure we're not already being played:if (fSource != NULL) {envir().setResultMsg("This sink is already being played");return False;}// Make sure our source is compatible:if (!sourceIsCompatibleWithUs(source)) {envir().setResultMsg("MediaSink::startPlaying(): source is not compatible!");return False;}fSource = (FramedSource*)&source;fAfterFunc = afterFunc;fAfterClientData = afterClientData;return continuePlaying();
}

        而该函数是在StreamState类中的startPlaying函数中调用;

void StreamState ::startPlaying(Destinations *dests, unsigned clientSessionId,TaskFunc *rtcpRRHandler, void *rtcpRRHandlerClientData,ServerRequestAlternativeByteHandler *serverRequestAlternativeByteHandler,void *serverRequestAlternativeByteHandlerClientData)
{...if (!fAreCurrentlyPlaying && fMediaSource != NULL){if (fRTPSink != NULL){fRTPSink->startPlaying(*fMediaSource, afterPlayingStreamState, this);fAreCurrentlyPlaying = True;}else if (fUDPSink != NULL){fUDPSink->startPlaying(*fMediaSource, afterPlayingStreamState, this);fAreCurrentlyPlaying = True;}}
}

        startPlaying的第一个参数是fMediaSource;而fMediaSource是在StreamState类的构造函数中被赋值的:

StreamState::StreamState(OnDemandServerMediaSubsession &master,Port const &serverRTPPort, Port const &serverRTCPPort,RTPSink *rtpSink, BasicUDPSink *udpSink,unsigned totalBW, FramedSource *mediaSource,Groupsock *rtpGS, Groupsock *rtcpGS): fMaster(master), fAreCurrentlyPlaying(False), fReferenceCount(1),fServerRTPPort(serverRTPPort), fServerRTCPPort(serverRTCPPort),fRTPSink(rtpSink), fUDPSink(udpSink), fStreamDuration(master.duration()),fTotalBW(totalBW), fRTCPInstance(NULL) /* created later */,fMediaSource(mediaSource), fStartNPT(0.0), fRTPgs(rtpGS), fRTCPgs(rtcpGS)
{
}

        StreamState是在OnDemandServerMediaSubsession类的getStreamParameters函数中被调用:

void OnDemandServerMediaSubsession ::getStreamParameters(unsigned clientSessionId,struct sockaddr_storage const &clientAddress,Port const &clientRTPPort,Port const &clientRTCPPort,int tcpSocketNum,unsigned char rtpChannelId,unsigned char rtcpChannelId,TLSState *tlsState,struct sockaddr_storage &destinationAddress,u_int8_t & /*destinationTTL*/,Boolean &isMulticast,Port &serverRTPPort,Port &serverRTCPPort,void *&streamToken)
{if (addressIsNull(destinationAddress)){// normal case - use the client address as the destination address:destinationAddress = clientAddress;}isMulticast = False;if (fLastStreamToken != NULL && fReuseFirstSource){// Special case: Rather than creating a new 'StreamState',// we reuse the one that we've already created:serverRTPPort = ((StreamState *)fLastStreamToken)->serverRTPPort();serverRTCPPort = ((StreamState *)fLastStreamToken)->serverRTCPPort();++((StreamState *)fLastStreamToken)->referenceCount();streamToken = fLastStreamToken;}else{// Normal case: Create a new media source:unsigned streamBitrate;FramedSource *mediaSource = createNewStreamSource(clientSessionId, streamBitrate);...streamToken = fLastStreamToken = new StreamState(*this, serverRTPPort, serverRTCPPort, rtpSink, udpSink,streamBitrate, mediaSource,rtpGroupsock, rtcpGroupsock);}
}

        createNewStreamSource函数加上OnDemandServerMediaSubsession类是不是很熟悉?对的OnDemandServerMediaSubsession就是我们自定义的用于实现createNewStreamSource函数的类H264LiveVideoServerMediaSubssion的父类;因为createNewStreamSource在OnDemandServerMediaSubsession中是纯虚函数,因此该处调用的就是我们自定义的createNewStreamSource函数;

        因此在startPlaying函数中将fSource赋值为createNewStreamSource的返回值;我们再看一下createNewStreamSource函数吧:

FramedSource* H264LiveVideoServerMediaSubssion::createNewStreamSource(unsigned clientSessionId, unsigned& estBitrate)
{/* Remain to do : assign estBitrate */estBitrate = 1000; // kbps, estimate//创建视频源H264FramedLiveSource* liveSource = H264FramedLiveSource::createNew(envir(), Server_datasize, Server_databuf, Server_dosent);if (liveSource == NULL){return NULL;}// Create a framer for the Video Elementary Stream:return H264VideoStreamFramer::createNew(envir(), liveSource);
}

        则fSource就是H264VideoStreamFramer::createNew(envir(), liveSource); 因此getNextFrame函数中运行的doGetNextFrame就是H264VideoStreamFramer中的doGetNextFrame函数;

        等等!发现问题没有:这个并不是我们自定义的H264FramedLiveSource类中获取视频帧的函数doGetNextFrame;这是为什么呢?

        别急!返回值中H264VideoStreamFramer::createNew(envir(), liveSource); 将doGetNextFrame的类对象liveSource传递进了H264VideoStreamFramer类中;而liveSource最终赋值给了StreamParser类中成员变量fInputSource和FramedFilter类的成员变量fInputSource;更多细节参考我的另一篇文章;

        接着往下说:H264VideoStreamFramer中的doGetNextFrame函数会调用MPEGVideoStreamFramer中的doGetNextFrame函数;在这个函数中有一个ensureValidBytes1第一次调用了自定义的函数doGetNextFrame

void StreamParser::ensureValidBytes1(unsigned numBytesNeeded) {
.
.
.fInputSource->getNextFrame(&curBank()[fTotNumValidBytes],maxNumBytesToRead,afterGettingBytes, this,onInputClosure, this);throw NO_MORE_BUFFERED_INPUT;
}

这里仅仅是对流的解析;下面才是真正获取视频流:

void H264or5Fragmenter::afterGettingFrame(void* clientData, unsigned frameSize,unsigned numTruncatedBytes,struct timeval presentationTime,unsigned durationInMicroseconds) {H264or5Fragmenter* fragmenter = (H264or5Fragmenter*)clientData;fragmenter->afterGettingFrame1(frameSize, numTruncatedBytes, presentationTime,durationInMicroseconds);
}void H264or5Fragmenter::afterGettingFrame1(unsigned frameSize,unsigned numTruncatedBytes,struct timeval presentationTime,unsigned durationInMicroseconds) {fNumValidDataBytes += frameSize;fSaveNumTruncatedBytes = numTruncatedBytes;fPresentationTime = presentationTime;fDurationInMicroseconds = durationInMicroseconds;// Deliver data to the client:doGetNextFrame();
}void H264or5Fragmenter::doGetNextFrame() {if (fNumValidDataBytes == 1) {// We have no NAL unit data currently in the buffer.  Read a new one:fInputSource->getNextFrame(&fInputBuffer[1], fInputBufferSize - 1,afterGettingFrame, this,FramedSource::handleClosure, this);} else {...}
}

        afterGettingFrame函数才是真正调用doGetNextFrame的函数?那么afterGettingFrame是在哪里被调用的呢?

        看一下fSource->getNextFrame函数:

fInputSource->getNextFrame(fOutBuf->curPtr(), fOutBuf->totalBytesAvailable(),afterGettingFrame, this, ourHandleClosure, this);

        这里第三个参数就调用了afterGettingFrame函数,再看看getNextFrame函数实现;上面已经写过了 为了方便理解这里再写下:

void FramedSource::getNextFrame(unsigned char* to, unsigned maxSize,afterGettingFunc* afterGettingFunc,void* afterGettingClientData,onCloseFunc* onCloseFunc,void* onCloseClientData) {// Make sure we're not already being read:if (fIsCurrentlyAwaitingData) {envir() << "FramedSource[" << this << "]::getNextFrame(): attempting to read more than once at the same time!\n";envir().internalError();}fTo = to;fMaxSize = maxSize;fNumTruncatedBytes = 0; // by default; could be changed by doGetNextFrame()fDurationInMicroseconds = 0; // by default; could be changed by doGetNextFrame()fAfterGettingFunc = afterGettingFunc;fAfterGettingClientData = afterGettingClientData;fOnCloseFunc = onCloseFunc;fOnCloseClientData = onCloseClientData;fIsCurrentlyAwaitingData = True;doGetNextFrame();
}

        afterGettingFrame被赋值给了fAfterGettingClientData指针;那么fAfterGettingClientData指针什么时候被调用的?

        回望doGetNextFrame的实现,每次读完流都会调用:

afterGetting(this);
//函数实现
void FramedSource::afterGetting(FramedSource* source) {source->nextTask() = NULL;source->fIsCurrentlyAwaitingData = False;// indicates that we can be read again// Note that this needs to be done here, in case the "fAfterFunc"// called below tries to read another frame (which it usually will)if (source->fAfterGettingFunc != NULL) {(*(source->fAfterGettingFunc))(source->fAfterGettingClientData,source->fFrameSize, source->fNumTruncatedBytes,source->fPresentationTime,source->fDurationInMicroseconds);}
}

        afterGetting的函数实现中就调用了fAfterGettingFunc指针;这就使doGetNextFrame形成了闭环:

        

至此函数doGetNextFrame的执行流程已经全部解析完毕,后续还会继续更新关于doGetNextFrame函数获取的帧数据是怎么处理和发送的,H264VideoStreamFramer中的doGetNextFrame函数和MPEGVideoStreamFramer中的doGetNextFrame函数都是什么作用!期待的话关注我,了解最新动态!

该文章持续更新!如果有错误或者模糊的地方欢迎留言探讨!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/872838.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自动化产线 搭配数据采集监控平台 创新与突破

自动化产线在现在的各行各业中应用广泛&#xff0c;已经是现在的生产趋势&#xff0c;不同的自动化生产设备充斥在各行各业中&#xff0c;自动化的设备会产生很多的数据&#xff0c;这些数据如何更科学化的管理&#xff0c;更优质的利用&#xff0c;就需要数据采集监控平台来完…

【操作系统】定时器(Timer)的实现

这里写目录标题 定时器一、定时器是什么二、标准库中的定时器三、实现定时器 定时器 一、定时器是什么 定时器也是软件开发中的⼀个重要组件.类似于⼀个"闹钟".达到⼀个设定的时间之后,就执行某个指定 好的代码. 定时器是⼀种实际开发中⾮常常用的组件. ⽐如⽹络通…

卷积神经网络(一)-LeNet-5

前言 LeNet开启了卷积神经网络的第一枪&#xff0c;这一网络模型由Yann LeCun等人在1998年提出&#xff0c;被视为卷积神经网络的开山之作。 论文地址&#xff1a; http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf 如果打不开就看csdn&#xff1a; https://download.…

人工智能算法工程师(中级)课程15-常见的网络模型及设计原理与代码详解

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能算法工程师(中级)课程15-常见的网络模型及设计原理与代码详解。 本文给大家介绍常见的网络模型及其设计原理与代码实现&#xff0c;涵盖了LeNet、AlexNet、VggNet、GoogLeNet、InceptionNet、ResNet、Dense…

MATLAB科研数据可视化教程

原文链接&#xff1a;MATLAB科研数据可视化https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247609462&idx3&snf7043936fc5ee42b833c7c9f3bcd24ba&chksmfa826d91cdf5e4872eb275e5319b66ba6927ea0074fb2293fe1ca47d6aedf38ab91050be484c&token1551213…

SQL面试题练习 —— 统计最大连续登录天数区间

目录 1 题目2 建表语句3 题解 1 题目 2 建表语句 CREATE TABLE IF NOT EXISTS user_login_tb (uid INT,login_date DATE ); insert into user_login_tb(uid, login_date) values( 1, 2022-08-02),(1, 2022-08-03),(2, 2022-08-03),(2, 2022-08-04),(2, 2022-08-05),(2, 2022-08…

6个高效再利用的UI作品集设计模板

UI 作品集是指用户界面设计师的个人作品集。它展示了设计师的设计能力、技巧和风格&#xff0c;也是充分展示他们设计能力的证明。优秀的UI 作品集应具有简洁明了、美观大方、良好的互动体验和明确的目标。本文将从两个方面的介绍 Ui 作品集模板的全部内容&#xff1a;UI 作品集…

【C++11】(lambda)

C11中的lambda与线程。 目录 Lambda&#xff1a;仿函数的缺点&#xff1a;Lambda语法&#xff1a;Lambda使用示例&#xff1a;两数相加&#xff1a;两数交换&#xff1a;解决Goods排序问题&#xff1a; Lambda原理&#xff1a; Lambda&#xff1a; 假设我们有一个商品类&…

ClickHouse集成LDAP实现简单的用户认证

1.这里我的ldap安装的是docker版的 docker安装的化就yum就好了 sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl start docker 使用下面的命令验证sudo docker run hello-world docker pull osixia/openl…

微信小程序与本地MySQL数据库通信

微信小程序与本地MySQL数据库通信 因为本地MySQL服务器没有域名&#xff0c;也没有进行相应的请求操作封装&#xff0c;因此微信小程序没办法和数据库通信。 但是对于开发人员来说&#xff0c;没有数据库&#xff0c;那还能干撒&#xff1f;虽然我尝试过用json-server&#x…

好用的AI搜索引擎

1. 360AI 搜索 访问 360AI 搜索: https://www.huntagi.com/sites/1706642948656.html 360AI 搜索介绍&#xff1a; 360AI 搜索&#xff0c;新一代智能答案引擎&#xff0c;值得信赖的智能搜索伙伴&#xff0c;为复杂搜索提供专业支持&#xff0c;解锁更相关、更全面的答案。AI…

探索Facebook的最新更新:社交体验的新高度

Facebook作为全球领先的社交媒体平台&#xff0c;一直致力于不断创新和改进&#xff0c;以提供更优质的用户体验。近期&#xff0c;Facebook推出了一系列新的更新&#xff0c;旨在提升用户的社交互动体验和平台功能。本文将详细探讨这些最新更新&#xff0c;分析其对用户和社交…

如何用AI交互数字人一体机,打造政务服务新名片?

如今&#xff0c;将“高效办成一件事”作为优化政务服务、提升行政效能的重要抓手&#xff0c;各地方为了促进政务服务由传统模式向数字化、智能化方向转变&#xff0c;纷纷在政务服务场景融合了AI交互数字人&#xff0c;实现“无人化、智慧化”导办、帮办、代办等模式&#xf…

Apache AGE的MATCH子句

MATCH子句允许您在数据库中指定查询将搜索的模式。这是检索数据以在查询中使用的主要方法。 通常在MATCH子句之后会跟随一个WHERE子句&#xff0c;以添加用户定义的限制条件到匹配的模式中&#xff0c;以操纵返回的数据集。谓词是模式描述的一部分&#xff0c;不应被视为仅在匹…

3D问界—在MAYA中使用Python脚本进行批量轴居中

问题提出&#xff1a;MAYA中如何使用Python脚本 今天不是一篇纯理论&#xff0c;主要讲一下MAYA中如何使用Python脚本&#xff0c;并解决一个实际问题&#xff0c;文章会放上我自己的代码&#xff0c;若感兴趣欢迎尝试&#xff0c;当然&#xff0c;若有问题可以见文章末尾渠道&…

分布式IO系统2通道串口通信模块M602x

现场总线耦合器本身包含一个电源模块&#xff0c;它有 2 个串口通道&#xff0c;通过 Modbus RTU&#xff08;Master&#xff09;协议连接外部串行设备&#xff0c;实现耦合器与外部串行设备通信&#xff0c;现以连接设备的示例带大家了解我们钡铼的2 通道串口通信模块 M602x。…

【BUG】已解决:Uncaught SyntaxError: Unexpected token ‘<‘

已解决&#xff1a;Could not install packages due to an EnvironmentError: [Errno 13] Permission denied 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市开发者社区主理人 …

Neither the JAVA_HOME nor the JRE_HOME environment variable is defined问题解决

一、系统环境变量中添加tomcatjdk的环境变量声明 1、右击此电脑->属性->高级系统设置 可复制粘贴下面的变量名 CATALINA_HOME 点击path->编辑->新建 可将下面值粘入 %CATALINA_HOME%\bin 2、配置jdk的系统变量 系统变量->新建->如图 可将下面变量名粘入 J…

Flutter热更新技术探索

一&#xff0c;需求背景&#xff1a; APP 发布到市场后&#xff0c;难免会遇到严重的 BUG 阻碍用户使用&#xff0c;因此有在不发布新版本 APP 的情况下使用热更新技术立即修复 BUG 需求。原生 APP&#xff08;例如&#xff1a;Android & IOS&#xff09;的热更新需求已经…

LabVIEW 与 PLC 通讯方式

在工业自动化中&#xff0c;LabVIEW 与 PLC&#xff08;可编程逻辑控制器&#xff09;的通信至关重要&#xff0c;常见的通信方式包括 OPC、Modbus、EtherNet/IP、Profibus/Profinet 和 Serial&#xff08;RS232/RS485&#xff09;。这些通信协议各有特点和应用场景&#xff0c…