当前位置: 首页 > article >正文

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

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函数初始化MediaCodec
    native_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.4
    sp<JMediaCodec> codec = new JMediaCodec(env, thiz, tmp, nameIsType, encoder, pid, uid);

    const status_t err = codec->initCheck();
    // ... 返回值错误的处理,权限异常/内存不足/编解码器类型不存在
    // 注册消息监听
    codec->registerSelf();
    // 回调java侧记录JMediaCodec
    setMediaCodec(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,      // runOnCallingThread
            true,       // canCallJava
            ANDROID_PRIORITY_VIDEO);

    // 我们传的nameIsType为true,name就是编码类型(比如H264)
    if (nameIsType) {
        // 详见1.5
        mCodec = 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.1
    MediaCodecList::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.6
        status_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.2
    const 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();
        // ...
        // 将获取的组件名放入matches
        matches->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.3
            sRemoteList = getLocalInstance();
        }
    }
    return sRemoteList;
}

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

sp<IMediaCodecList> MediaCodecList::getLocalInstance() {
    Mutex::Autolock autoLock(sInitMutex);

    if (sCodecList == nullptr) {
        // 构造MediaCodecList,详见1.5.4
        MediaCodecList *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 lowest
                return 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.1
    mCodec = 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的Callback
    mCodec->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.7
        err = 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:
        {
            // ...检测当前状态是否是UNINITIALIZED

            mReplyID = replyID;
            // 修改状态至INITIALIZING
            setState(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.8
            mCodec->initiateAllocateComponent(format);
            break;
        }
    // ...
}

1.8 CCodec::initiateAllocateComponent
发出kWhatAllocate消息。

void CCodec::initiateAllocateComponent(const sp<AMessage> &msg) {
    // 更新CCodec状态至ALLOCATING
    auto 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.9
    allocMsg->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.10
        allocate((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.11
    c2_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,并且记录创建的组件到state
        return OK;
    };
    if (tryAndReportOnError(setAllocated) != OK) {
        return;
    }

    Mutexed<std::unique_ptr<Config>>::Locked configLocked(mConfig);
    const std::unique_ptr<Config> &config = *configLocked;
    // 初始化CodecConfig参数,和编码相关的参数会存储在CodecConfig
    status_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);
    // 遍历所有IComponentStore
    c2_status_t status = ForAllServices(
            key,
            numberOfAttempts,
            [owner, component, componentName, &listener](
                    const std::shared_ptr<Codec2Client> &client)
                        -> c2_status_t {
                // binder调用对端创建组件,详见1.12
                c2_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.13
        res = 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.14
    c2_status_t res = mComponentFactory->createComponent(
        id, component, [module, deleter](C2Component *p) mutable {
            deleter(p);     // delete component first
            module.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.kler.cn/news/358362.html

相关文章:

  • resnetv1骨干
  • 阿里巴巴 | 推出升级版AI翻译工具:Marco MT 性能超越Google和ChatGPT
  • oracle创建用户与表空间,用户授权、以及导入dmp数据泵文件
  • Python----QT篇基础篇(一)
  • 图像中的融合
  • zotero文献管理学习
  • 柬埔寨旅游应该准备的高棉语翻译器《柬埔寨语翻译通》app语音翻译功能让你跟当地人无阻沟通交流,高棉语OCR识别技术分享
  • 桂林美景探索:SpringBoot旅游平台指南
  • 5.C++经典实例-判断输入的年份是否为闰年
  • go 中指针的执行效率比较
  • AI大模型开发架构设计(14)——基于LangChain大模型的案例架构实战
  • Windows环境apache控制台命令行启动、停止、重启httpd服务
  • 【Flutter】页面布局:线性布局(Row 和 Column)
  • mybatis针对枚举的处理的总结
  • 《Vue3 版本差异》Vue3.5+ 在组件或HTML元素绑定 ref 差异
  • (RAG 系列)重排序模型部署以及接入 fastgpt 平台
  • 【Flutter】页面布局:弹性布局(Flex)
  • Linux下的进程解析(level 2)
  • C++核心编程和桌面应用开发 第十二天(输入输出流 流对象 写文件 读文件)
  • 鸿蒙应用开发----西西购物商城(一)