Android音视频 MediaCodec框架-创建流程(3)

embedded/2024/10/25 4:13:33/

Android音视频 MediaCodec框架-创建流程

简述

之前我们介绍并且演示了MediaCodec的接口使用方法,我们这一节来看一下MediaCodec进行编解码的创建流程。
java层的MediaCodec只是提供接口,实际的逻辑是通过jni层实现的,java层的MediaCodec通过jni创建管理一个C++层JMediaCodec,而JMediaCodec会创建C++层MediaCodec,C++层的MediaCodec会根据配置创建CCodec和OMX,新版本的Android一般使用CCodec,CCodec层会通过Codec2Client访问hal层的service,一般会有多个hal层的service,每个hal层的service又支持多个Component,不同Component会支持不同的编解码,且有不同是的实现方式,比如软解码和硬解码。

创建MediacCodec

在这里插入图片描述

1.1 MediaCodec.createDecoderByType
调用createDecoderByType创建MediaCodec,调用MediaCodec的构造函数,我们type传入的是H264,type就是name。

public static MediaCodec createDecoderByType(@NonNull String type)throws IOException {return new MediaCodec(type, true /* nameIsType */, false /* encoder */);
}

1.2 MediaCodec构造函数
构造函数调用一个重载构造函数,主要是构造了一个EventHandler,然后调用native_setup初始化native的MediaCodec。

private MediaCodec(@NonNull String name, boolean nameIsType, boolean encoder) {this(name, nameIsType, encoder, -1 /* pid */, -1 /* uid */);
}private MediaCodec(@NonNull String name, boolean nameIsType, boolean encoder, int pid,int uid) {Looper looper;if ((looper = Looper.myLooper()) != null) {mEventHandler = new EventHandler(this, looper);} else if ((looper = Looper.getMainLooper()) != null) {mEventHandler = new EventHandler(this, looper);} else {mEventHandler = null;}mCallbackHandler = mEventHandler;mOnFirstTunnelFrameReadyHandler = mEventHandler;mOnFrameRenderedHandler = mEventHandler;mBufferLock = new Object();// 保存type作为名字mNameAtCreation = nameIsType ? null : name;// 调用native函数初始化MediaCodecnative_setup(name, nameIsType, encoder, pid, uid);
}

1.3 native_setup
通过jni调用native_setup。
构造了一个JMediaCodec,每个java层的MediaCodec在native侧会对应一个JMediaCodec,后续还会创建一个native层的MediaCodec。
注册了消息监听。
setMediaCodec回调java层记录JMediaCodec的地址。

static void android_media_MediaCodec_native_setup(JNIEnv *env, jobject thiz,jstring name, jboolean nameIsType, jboolean encoder, int pid, int uid) {// ... 非空判断// 构造JMediaCodec,详见1.4sp<JMediaCodec> codec = new JMediaCodec(env, thiz, tmp, nameIsType, encoder, pid, uid);const status_t err = codec->initCheck();// ... 返回值错误的处理,权限异常/内存不足/编解码器类型不存在// 注册消息监听codec->registerSelf();// 回调java侧记录JMediaCodecsetMediaCodec(env, thiz, codec);
}

1.4 JMediaCodec构造函数
这里会构造一个ALooper,这个的功能类似于Looper。
通过MediaCodec::CreateByType创建一个Native层的MediaCodec。

JMediaCodec::JMediaCodec(JNIEnv *env, jobject thiz,const char *name, bool nameIsType, bool encoder, int pid, int uid): mClass(NULL),mObject(NULL) {// ... 记录Java层MediaCodec信息// 类似于Looper的功能,post消息,然后由对应的方法处理消息mLooper = new ALooper;mLooper->setName("MediaCodec_looper");mLooper->start(false,      // runOnCallingThreadtrue,       // canCallJavaANDROID_PRIORITY_VIDEO);// 我们传的nameIsType为true,name就是编码类型(比如H264)if (nameIsType) {// 详见1.5mCodec = MediaCodec::CreateByType(mLooper, name, encoder, &mInitStatus, pid, uid);if (mCodec == nullptr || mCodec->getName(&mNameAtCreation) != OK) {mNameAtCreation = "(null)";}} else {mCodec = MediaCodec::CreateByComponentName(mLooper, name, &mInitStatus, pid, uid);mNameAtCreation = name;}CHECK((mCodec != NULL) != (mInitStatus != OK));
}

1.5 MediaCodec::CreateByType
调用一个重载CreateByType。
CreateByType会通过findMatchingCodecs根据名字查找对应的Codec的名字,返回的列表存储在了matchingCodecs。
遍历matchingCodecs,创建MediaCodec,然后初始化MediaCodec,初始化成功后就返回。

sp<MediaCodec> MediaCodec::CreateByType(const sp<ALooper> &looper, const AString &mime, bool encoder, status_t *err, pid_t pid,uid_t uid) {sp<AMessage> format;return CreateByType(looper, mime, encoder, err, pid, uid, format);
}sp<MediaCodec> MediaCodec::CreateByType(const sp<ALooper> &looper, const AString &mime, bool encoder, status_t *err, pid_t pid,uid_t uid, sp<AMessage> format) {Vector<AString> matchingCodecs;// 根据mime编码名字,查询支持的编码器,值存在matchingCodecs。  // 详见1.5.1MediaCodecList::findMatchingCodecs(mime.c_str(),encoder,0,format,&matchingCodecs);if (err != NULL) {*err = NAME_NOT_FOUND;}for (size_t i = 0; i < matchingCodecs.size(); ++i) {// 构建MediaCodec,根据componentName初始化Codec,初始化成功就返回。  sp<MediaCodec> codec = new MediaCodec(looper, pid, uid);AString componentName = matchingCodecs[i];// 初始化MediaCodec,详见1.6status_t ret = codec->init(componentName);if (err != NULL) {*err = ret;}if (ret == OK) {return codec;}// ...}return NULL;
}

1.5.1 MediaCodecList::findMatchingCodecs

void MediaCodecList::findMatchingCodecs(const char *mime, bool encoder, uint32_t flags, const sp<AMessage> &format,Vector<AString> *matches) {matches->clear();// 通过MediaPlayerService构建获取MediaCodecList,详见1.5.2const sp<IMediaCodecList> list = getInstance();if (list == nullptr) {return;}size_t index = 0;for (;;) {// 从MediaCodecList的mCodecInfos查询对应索引,mCodecInfos记录了所有支持的Codec信息。  // 我们主要来看一下mCodecInfos是哪里初始化的,详见1.5.2。  ssize_t matchIndex =list->findCodecByType(mime, encoder, index);// ...const sp<MediaCodecInfo> info = list->getCodecInfo(matchIndex);CHECK(info != nullptr);AString componentName = info->getCodecName();// ...// 将获取的组件名放入matchesmatches->push(componentName);ALOGV("matching '%s'", componentName.c_str());}if (flags & kPreferSoftwareCodecs ||property_get_bool("debug.stagefright.swcodec", false)) {matches->sort(compareSoftwareCodecsFirst);}// ...如果什么都没找到,从format信息里获取profile,调用findMatchingCodecs重试一下
}

1.5.2 MediaCodecList::getInstance
通过getLocalInstance构造获取MediaCodecList。

sp<IMediaCodecList> MediaCodecList::getInstance() {Mutex::Autolock _l(sRemoteInitMutex);if (sRemoteList == nullptr) {sMediaPlayer = defaultServiceManager()->getService(String16("media.player"));sp<IMediaPlayerService> service =interface_cast<IMediaPlayerService>(sMediaPlayer);if (service.get() != nullptr) {// 这里通过MediaPlayerService的getCodecList也是通过getLocalInstance获取sRemoteList  sRemoteList = service->getCodecList();if (sRemoteList != nullptr) {sBinderDeathObserver = new BinderDeathObserver();sMediaPlayer->linkToDeath(sBinderDeathObserver.get());}}if (sRemoteList == nullptr) {// 通过getLocalInstance构造MediaCodecList,详见1.5.3sRemoteList = getLocalInstance();}}return sRemoteList;
}

1.5.3 MediaCodecList::getLocalInstance
构造MediaCodecList,参数通过GetBuilders获取。

sp<IMediaCodecList> MediaCodecList::getLocalInstance() {Mutex::Autolock autoLock(sInitMutex);if (sCodecList == nullptr) {// 构造MediaCodecList,详见1.5.4MediaCodecList *codecList = new MediaCodecList(GetBuilders());if (codecList->initCheck() == OK) {sCodecList = codecList;// ... } else {// failure to initialize may be temporary. retry on next call.delete codecList;}}return sCodecList;
}

1.5.4 MediaCodecList构造函数
遍历所有builders,调用它的buildMediaCodecList,一般buildMediaCodecList会调用MediaCodecListWriter::findMediaCodecInfo
而findMediaCodecInfo则会将MediaCodecInfo信息存储到MediaCodecListWriter
最后会调用writer.writeCodecInfos(&mCodecInfos)将所有write里面的信息存储到mCodecInfos变量中
所以可以看出来mCodecInfos最终有哪些数据取决于构造函数入参GetBuilders(),我们来看下GetBuilders()的实现,详见1.5.5。

MediaCodecList::MediaCodecList(std::vector<MediaCodecListBuilderBase*> builders) {mGlobalSettings = new AMessage();mCodecInfos.clear();MediaCodecListWriter writer;for (MediaCodecListBuilderBase *builder : builders) {// ...// buildMediaCodecList会调用MediaCodecListWriter::findMediaCodecInfo// 而findMediaCodecInfo则会将MediaCodecInfo信息存储到MediaCodecListWriter// 最后会调用writer.writeCodecInfos(&mCodecInfos)将所有write里面的信息存储到mCodecInfos变量中auto currentCheck = builder->buildMediaCodecList(&writer);if (currentCheck != OK) {ALOGD("ignored failed builder");continue;} else {mInitCheck = currentCheck;}}writer.writeGlobalSettings(mGlobalSettings);// 将MediaCodecListWriter里的CodecInfos存储到mCodecInfos列表里。  writer.writeCodecInfos(&mCodecInfos);std::stable_sort(mCodecInfos.begin(),mCodecInfos.end(),[](const sp<MediaCodecInfo> &info1, const sp<MediaCodecInfo> &info2) {// null is lowestreturn info1 == nullptr|| (info2 != nullptr && info1->getRank() < info2->getRank());});// ...删除重复的
}

1.5.5 GetBuilders
构建OMX build和Codec2InfoBuilder。
每个builder会支持一系列的编码类型。Codec2是Android Q引入的,而之前使用的是OMX。
Codec2相比于OMX,状态更加简化。

std::vector<MediaCodecListBuilderBase *> GetBuilders() {std::vector<MediaCodecListBuilderBase *> builders;// 主要是Codec2InfoBuilder和OMX两种。  sp<PersistentSurface> surfaceTest = CCodec::CreateInputSurface();if (surfaceTest == nullptr) {ALOGD("Allowing all OMX codecs");builders.push_back(&sOmxInfoBuilder);} else {ALOGD("Allowing only non-surface-encoder OMX codecs");builders.push_back(&sOmxNoSurfaceEncoderInfoBuilder);}builders.push_back(GetCodec2InfoBuilder());return builders;
}

1.6 MediaCodec::init
除了初始化ResourceManager,通过mGetCodecInfo查找CodecInfo,然后根据CodecInfo和name,使用mGetCodecBase创建实际的Codec,比如CCodec活着ACodec。
后续发送kWhatInit给ALooper来进行初始化。

status_t MediaCodec::init(const AString &name) {// 初始化ResourceManager,service的key为media.resource_manager。status_t err = mResourceManagerProxy->init();if (err != OK) {// ...如果初始化异常,直接返回}mInitName = name;mCodecInfo.clear();bool secureCodec = false;const char *owner = "";if (!name.startsWith("android.filter.")) {// mGetCodecInfo是在MediaCodec构造函数时赋值的,是一个根据name查找CodecInfo的方法。err = mGetCodecInfo(name, &mCodecInfo);// ...查找失败直接返回secureCodec = name.endsWith(".secure");Vector<AString> mediaTypes;mCodecInfo->getSupportedMediaTypes(&mediaTypes);for (size_t i = 0; i < mediaTypes.size(); ++i) {if (mediaTypes[i].startsWith("video/")) {mDomain = DOMAIN_VIDEO;break;} else if (mediaTypes[i].startsWith("audio/")) {mDomain = DOMAIN_AUDIO;break;} else if (mediaTypes[i].startsWith("image/")) {mDomain = DOMAIN_IMAGE;break;}}owner = mCodecInfo->getOwnerName();}// 根据名称以及owner获取实际的Codec,详见1.6.1mCodec = mGetCodecBase(name, owner);// ...没有获取到Codec直接返回。if (mDomain == DOMAIN_VIDEO) {// 启动mCodecLooper消息接收处理。if (mCodecLooper == NULL) {status_t err = OK;mCodecLooper = new ALooper;mCodecLooper->setName("CodecLooper");err = mCodecLooper->start(false, false, ANDROID_PRIORITY_AUDIO);// ...启动失败直接返回}mCodecLooper->registerHandler(mCodec);} else {mLooper->registerHandler(mCodec);}mLooper->registerHandler(this);// 配置Codec的CallbackmCodec->setCallback(std::unique_ptr<CodecBase::CodecCallback>(new CodecCallback(new AMessage(kWhatCodecNotify, this))));mBufferChannel = mCodec->getBufferChannel();mBufferChannel->setCallback(std::unique_ptr<CodecBase::BufferCallback>(new BufferCallback(new AMessage(kWhatCodecNotify, this))));// 初始化kWhatInit消息,发送消息由ALooper处理。sp<AMessage> msg = new AMessage(kWhatInit, this);if (mCodecInfo) {msg->setObject("codecInfo", mCodecInfo);}msg->setString("name", name);if (mMetricsHandle != 0) {mediametrics_setCString(mMetricsHandle, kCodecCodec, name.c_str());mediametrics_setCString(mMetricsHandle, kCodecMode, toCodecMode(mDomain));}if (mDomain == DOMAIN_VIDEO) {mBatteryChecker = new BatteryChecker(new AMessage(kWhatCheckBatteryStats, this));}std::vector<MediaResourceParcel> resources;resources.push_back(MediaResource::CodecResource(secureCodec, toMediaResourceSubType(mDomain)));// If the ComponentName is not set yet, use the name passed by the user.if (mComponentName.empty()) {mResourceManagerProxy->setCodecName(name.c_str());}for (int i = 0; i <= kMaxRetry; ++i) {if (i > 0) {if (!mResourceManagerProxy->reclaimResource(resources)) {break;}}sp<AMessage> response;// 提交kwhatInit的msg,处理方法详见1.7err = PostAndAwaitResponse(msg, &response);if (!isResourceError(err)) {break;}}if (OK == err) {mResourceManagerProxy->notifyClientCreated();}return err;
}

1.6.1 MediaCodec::GetCodecBase
可以看出来,这里只会返回两种Codec,一种是ACodec,一种是CCodec。
CCodec配合Codec2使用,而ACodec配置OMX使用,我们来看新一些的架构CCOdec。

sp<CodecBase> MediaCodec::GetCodecBase(const AString &name, const char *owner) {if (owner) {if (strcmp(owner, "default") == 0) {return new ACodec;} else if (strncmp(owner, "codec2", 6) == 0) {return CreateCCodec();}}if (name.startsWithIgnoreCase("c2.")) {return CreateCCodec();} else if (name.startsWithIgnoreCase("omx.")) {return new ACodec;} else {return NULL;}
}

1.7 MediaCodec::onMessageReceived
这里是一个中间层,将init消息转发到CodecBase去。
我们这里来跟踪CCodec,调用CCodec到initiateAllocateComponent方法。

void MediaCodec::onMessageReceived(const sp<AMessage> &msg) {// ...case kWhatInit:{// ...检测当前状态是否是UNINITIALIZEDmReplyID = replyID;// 修改状态至INITIALIZINGsetState(INITIALIZING);sp<RefBase> codecInfo;(void)msg->findObject("codecInfo", &codecInfo);AString name;CHECK(msg->findString("name", &name));sp<AMessage> format = new AMessage;if (codecInfo) {format->setObject("codecInfo", codecInfo);}format->setString("componentName", name);// 将消息转发到Codec,这里我们来看CCodec,详见1.8mCodec->initiateAllocateComponent(format);break;}// ...
}

1.8 CCodec::initiateAllocateComponent
发出kWhatAllocate消息。

void CCodec::initiateAllocateComponent(const sp<AMessage> &msg) {// 更新CCodec状态至ALLOCATINGauto setAllocating = [this] {Mutexed<State>::Locked state(mState);if (state->get() != RELEASED) {return INVALID_OPERATION;}state->set(ALLOCATING);return OK;};if (tryAndReportOnError(setAllocating) != OK) {return;}// 发出kWhatAllocate消息。sp<RefBase> codecInfo;CHECK(msg->findObject("codecInfo", &codecInfo));sp<AMessage> allocMsg(new AMessage(kWhatAllocate, this));allocMsg->setObject("codecInfo", codecInfo);详见1.9allocMsg->post();
}

1.9 CCodec::onMessageReceived
调用allocate处理消息。

void CCodec::onMessageReceived(const sp<AMessage> &msg) {// ...case kWhatAllocate: {// C2ComponentStore::createComponent() should return within 100ms.setDeadline(now, 1500ms, "allocate");sp<RefBase> obj;CHECK(msg->findObject("codecInfo", &obj));// 详见1.10allocate((MediaCodecInfo *)obj.get());break;}// ...
}

1.10 CCodec::allocate
Codec2框架有一个Codec2Client,而Codec2Client会访问hal层,这里Codec2Client组件会有多个service,例如V4L2ComponentStore里面会对应V4L2驱动,提供硬编解码组件,而GoldfishComponentStore提供软编解码组件。
同时会更新CCodec的状态到ALLOCATING。
创建组件后,会通过config->initialize初始化编解码配置,例如编解码的宽高。

void CCodec::allocate(const sp<MediaCodecInfo> &codecInfo) {// ... 参数检测mClientListener.reset(new ClientListener(this));AString componentName = codecInfo->getCodecName();std::shared_ptr<Codec2Client> client;// Codec2Client会搜索ServiceManager所有句柄查找IComponentStore的服务// 这里ComponentStore实现有多个,例如V4L2ComponentStore里的组件一般是实现硬编解码的,而GoldfishComponentStore里面的组件是软件编解码的。// 我们主要看软件编解码构建组件过程client = Codec2Client::CreateFromService("default");// ...std::shared_ptr<Codec2Client::Component> comp;// 根据组件名创建组件,这里会调用到hal层来创建Component,详见1.11c2_status_t status = Codec2Client::CreateComponentByName(componentName.c_str(),mClientListener,&comp,&client);if (status != C2_OK) {// 创建失败直接返回}ALOGI("Created component [%s]", componentName.c_str());// 构造好的C2Component会存在mChannel中mChannel->setComponent(comp);auto setAllocated = [this, comp, client] {// ...更新state,并且记录创建的组件到statereturn OK;};if (tryAndReportOnError(setAllocated) != OK) {return;}Mutexed<std::unique_ptr<Config>>::Locked configLocked(mConfig);const std::unique_ptr<Config> &config = *configLocked;// 初始化CodecConfig参数,和编码相关的参数会存储在CodecConfigstatus_t err = config->initialize(mClient->getParamReflector(), comp);// ...config->queryConfiguration(comp);mCallback->onComponentAllocated(componentName.c_str());
}

1.11 Codec2Client::CreateComponentByName
遍历所有IComponentStore service,通过组件名称创建组件。

c2_status_t Codec2Client::CreateComponentByName(const char* componentName,const std::shared_ptr<Listener>& listener,std::shared_ptr<Component>* component,std::shared_ptr<Codec2Client>* owner,size_t numberOfAttempts) {std::string key{"create:"};key.append(componentName);// 遍历所有IComponentStorec2_status_t status = ForAllServices(key,numberOfAttempts,[owner, component, componentName, &listener](const std::shared_ptr<Codec2Client> &client)-> c2_status_t {// binder调用对端创建组件,详见1.12c2_status_t status = client->createComponent(componentName,listener,component);// ...return status;});// ...return status;
}

1.12 GoldfishComponentStore::createComponent
找到对应的ComponentModule,调用ComponentModule::createComponent

c2_status_t GoldfishComponentStore::createComponent(C2String name, std::shared_ptr<C2Component> *const component) {// This method SHALL return within 100ms.component->reset();std::shared_ptr<ComponentModule> module;c2_status_t res = findComponent(name, &module);if (res == C2_OK) {// 调用ComponentModule::createComponent,详见1.13res = module->createComponent(0, component);}return res;
}

1.13 GoldfishComponentStore::ComponentModule::createComponent
调用mComponentFactory的createComponent,这里的mComponentFactory是在ComponentModule的init的时候构建的,ComponentModule的init会传入一个lib库,然后动态加载lib库,搜索CreateCodec2Factory方法来进行Factory构造,所以如果想要实现一个编解码算法,只需要按照一个固定的写法,然后编译成一个lib库,在这里添加支持即可。

c2_status_t GoldfishComponentStore::ComponentModule::createComponent(c2_node_id_t id, std::shared_ptr<C2Component> *component,std::function<void(::C2Component *)> deleter) {component->reset();if (mInit != C2_OK) {return mInit;}std::shared_ptr<ComponentModule> module = shared_from_this();// 调用mComponentFactory的createComponent,我们这里看CCodec2的H264软编码,详见1.14c2_status_t res = mComponentFactory->createComponent(id, component, [module, deleter](C2Component *p) mutable {deleter(p);     // delete component firstmodule.reset(); // remove module ref (not technically needed)});ALOGI("created component");return res;
}

1.14 C2SoftAvcEncFactory::createComponent
C2SoftAvcEncFactory创建C2SoftAvcEnc,我们这里看软编码。

virtual c2_status_t createComponent(c2_node_id_t id,std::shared_ptr<C2Component>* const component,std::function<void(C2Component*)> deleter) override {// 构建C2SoftAvcEnc,C2SoftAvcEnc为实际编解码的操作。  *component = std::shared_ptr<C2Component>(new C2SoftAvcEnc(COMPONENT_NAME,id,std::make_shared<C2SoftAvcEnc::IntfImpl>(mHelper)),deleter);return C2_OK;
}

小结

我们本节介绍了MediaCodec接口创建流程,通过MediaCodec提供给应用的接口开始,jni调用构建JMediaCodec,JMediaCodec创建管理C++层的MediaCodec,而MediaCodec都创建管理CCodec2/OMX,旧版本的Android使用OMX框架,使用ACodec,而新版本Android一般是使用CCodec2,使用CCodec。CCodec会访问hal层创建编解码组件,一般会有hal层服务,每个服务也会支持一个或者多个组件,每个组件都实现了不同的编解码,以及软硬编码,我们下一节会介绍H264软编码的流程。


http://www.ppmy.cn/embedded/132235.html

相关文章

1024程序员节- AI智能时代,码出未来

在 1024 程序员节这个特殊的日子里&#xff0c;探讨了 AI 技术在不同领域的应用与发展。上海和深圳作为科技创新的前沿阵地&#xff0c;相关活动中的演讲内容更是聚焦了 AI 技术的核心要点&#xff0c;为我们展示了 AI 时代的新趋势和新机遇。 一、AI 技术的发展历程与背景 AI…

python支付宝支付和回调

创建支付订单 logging.basicConfig(levellogging.INFO,format%(asctime)s %(levelname)s %(message)s,filemodea,) logger logging.getLogger()if __name__ __main__:"""设置配置&#xff0c;包括支付宝网关地址、app_id、应用私钥、支付宝公钥等&#xff0c…

【优选算法篇】在分割中追寻秩序:二分查找的智慧轨迹

文章目录 C 二分查找详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;热身练习1.1 二分查找基本实现解题思路图解分析C代码实现易错点提示代码解读 1.2 在排序数组中查找元素的第一个和最后一个位置解题思路1.2.1 查找左边界算法步骤&#xff1a;图解分析C代码实现 1…

3.1.1ReactOS系统中搜索给定长度的空间地址区间函数的实现

系列文章目录 //搜索给定长度的空间地址区间 MmFindGap&#xff08;&#xff09;&#xff1b; PMADDRESS_SPACE AddressSpace,//该进程用户空间 ULONG_PTR Length,//寻找的空间间隔大小 ULONG_PTR Granularity,//粒度位&#xff0c;表明空间起点的对齐要求&#xff0c;注意是起…

时序数据库 TDengine 支持集成开源的物联网平台 ThingsBoard

Thingsboard 中“设备配置”和“设备”的关系是一对多的关系&#xff0c;通过设备配置为每个设备设置不同的配置&#xff0c;每个设备都会有一个与其关联的设备配置文件。等等&#xff0c;这不就是TDengine 中超级表的概念&#xff1a; 超级表是一种特殊的表结构&#xff0c;用…

针对 el-date picker pickerOptions 快捷选项的超级方法

提供快捷的配置&#xff0c;支持原子组合&#xff0c;高级用法支持用户自定义配置项 demo import { generateShortCuts } from ./date-shortcuts.js ... pickerOptions: {shortcuts: generateShortCuts({type: day}) } ...date-shortcuts 文件 import moment from moment // …

vue使用 jsplumb 生成流程图

1、安装jsPlumb&#xff1a; npm install jsplumb 2、 在使用的 .vue 文件中引入 import { jsPlumb } from "jsplumb"; 简单示例&#xff1a; 注意&#xff1a;注意看 id 为"item-3"和"item-9"那条数据的连线配置 其中有几个小图片&#x…

银河麒麟(debian)下安装postgresql、postgis

1、安装postgresql、postgis sudo apt update sudo apt install postgresql postgresql-contrib sudo apt install postgis postgresql-12-postgis-32、创建一个使用postgis的数据库 sudo -i -u postgres #postgres管理员用户createdb gisdb #创建新的gisdb数据库 psql -d gi…