Android codec2 视频框架之输出端的内存管理

文章目录

      • 前言
      • setSurface
      • start
      • 从哪个pool中申请buffer
      • 解码后框架的处理流程
      • renderOutbuffer 输出显示

前言

在这里插入图片描述

输出buffer整体的管理流程主要可以分为三个部分:

  1. MediaCodc 和 应用之间的交互 包括设置Surface、解码输出回调到MediaCodec。将输出buffer render或者releas到surface。
  2. MediaCodec到CCodecBufferChannel,主要是传递控制命令
  3. CCodecbufferChannel到componet buffer的封装 传递 控制等等。
  4. componet到bufferqueuepool buffer的申请

外部设置Surface进来,然后把输入buffer 输入,等待输出buffer 的回调,回调回来后 根据音视频同步的策略。在合适的时机renderOutput 送到MediaCodec。

需要了解的几个方面

  1. setSurface内部做了什么处理。
  2. 什么时候有输出的buffer可用?
  3. 输出buffer render到MediaCodec.内部做了什么处理。

setSurface

外部的setSurface调用到 MediaCodec的kWhatSetSurface

  1. MediaCodec::setSurface
    调用下面的connetToSurface 对surface进行连接
 nativeWindowConnect(surface.get(), "connectToSurface(reconnect)");

  1. CCodecBufferChannel::setSurface

根据bufferChanned的信息配置surface,比如配置deuque buffer 的超时时间、
dequeue最大的buffer数,当然这些值在后续可能还会改变,后续在解码器中解码出来的delay改变的话 回重新设置这个delay,
然后在handlework 重新设置最大的可dequeue的buffer 数。赋值mOutputSurface的相关变量。

        Mutexed<OutputSurface>::Locked output(mOutputSurface);
        output->surface = newSurface;
        output->generation = generation;
  1. 设置surface到 C2BufferQueueBlockPool 用于后续的解码buffer的申请

在ccodecbufferchannel的start中调用configureProducer设置外部surface的GraphicBufferProducer到
bufferQueueBlockpopl中。

          outputSurface = output->surface ?
                    output->surface->getIGraphicBufferProducer() : nullptr;


        if (outputSurface) {
            mComponent->setOutputSurface(
                    outputPoolId_,
                    outputSurface,
                    outputGeneration,
                    maxDequeueCount);
        }

Return<Status> Component::setOutputSurface(
        uint64_t blockPoolId,
        const sp<HGraphicBufferProducer2>& surface) {
    std::shared_ptr<C2BlockPool> pool;
    GetCodec2BlockPool(blockPoolId, mComponent, &pool);
    if (pool && pool->getAllocatorId() == C2PlatformAllocatorStore::BUFFERQUEUE) {
        if (bqPool) {
            bqPool->setRenderCallback(cb);
            bqPool->configureProducer(surface);
        }
    }
    return Status::OK;
}

void configureProducer(const sp<HGraphicBufferProducer> &producer,
                       native_handle_t *syncHandle,
                       uint64_t producerId,
                       uint32_t generation,
                       uint64_t usage,
                       bool bqInformation) {        
          if (producer) {
                mProducer = producer;
                mProducerId = producerId;
                mGeneration = bqInformation ? generation : 0;
         }

}

start

start 中跟输出buffer 相关的主要是两个方面

  1. 可以从surface最大能够dequeue出的buffer 数。由4个值组成 其中
    kSmoothnessFactor为4 kRenderingDepth为3。outputDelay由各个解码组件进行设置
    比如h264的默认设置为8, 同时会在解码过程handlework进行重新设置。

具体来说:

  • 在解码组件中解析到相关的reorder系数变化时 将系统放到输出的work中携带出去。
    在外部的ccodebufferchannel 中取出系统设置到surface中。
  • 实现动态的控制surface最大可以dequeue的buffer 数量。 外部通过dequeue申请的最大的buffer数是通过surface的setMaxDequeuedBufferCount
    设置到bufferqueueproducter 中,后续调用dequeue的时候会进行判断。
  • 比如解码器会重新设置为i4_reorder_depth。i4_reorder_depth 是什么? 怎么赋值的?(显示次序在某帧图像之后,解码次序在某帧图像之前的图像数量的最大值。因为编码器中的B帧不仅有前向参考,还有后向参考。 后向参考要求当前图像编码前,参考的后向图像已经编码完成,所以会导致图像的编码顺序和显示顺序不一样。)hevc 是存储在sps的sps_max_num_reorder_pics语言当中。
hevc'解码为例
ps_dec_op->i4_reorder_depth =
ps_sps->ai1_sps_max_num_reorder_pics[ps_sps->i1_sps_max_sub_layers - 1];

mOutputDelay = ps_decode_op->i4_reorder_depth;
ALOGV("New Output delay %d ", mOutputDelay);
C2PortActualDelayTuning::output outputDelay(mOutputDelay);
std::vector<std::unique_ptr<C2SettingResult>> failures;
c2_status_t err =
    mIntf->config({&outputDelay}, C2_MAY_BLOCK, &failures);
if (err == OK) {
    work->worklets.front()->output.configUpdate.push_back(
        C2Param::Copy(outputDelay));
 }
 
 
bool CCodecBufferChannel::handleWork(
        std::unique_ptr<C2Work> work,
        const sp<AMessage> &outputFormat,
        const C2StreamInitDataInfo::output *initData) {
        
while (!worklet->output.configUpdate.empty()) {
        std::unique_ptr<C2Param> param;
        worklet->output.configUpdate.back().swap(param);
        worklet->output.configUpdate.pop_back();

                if (param->forOutput()) {
                    C2PortActualDelayTuning::output outputDelay;
                    if (outputDelay.updateFrom(*param)) {
                        ALOGE("[%s] onWorkDone: updating output delay %u",
                              mName, outputDelay.value);
                        (void)mPipelineWatcher.lock()->outputDelay(outputDelay.value);
                        newOutputDelay = outputDelay.value;
                        needMaxDequeueBufferCountUpdate = true;
                    }
                }
                break;

    if (needMaxDequeueBufferCountUpdate) {
        int maxDequeueCount = 0;
        {
            Mutexed<OutputSurface>::Locked output(mOutputSurface);
            maxDequeueCount = output->maxDequeueBuffers =
                    numOutputSlots + reorderDepth + kRenderingDepth;
            if (output->surface) {
                output->surface->setMaxDequeuedBufferCount(output->maxDequeueBuffers);
            }
        }
        if (maxDequeueCount > 0) {
            mComponent->setOutputSurfaceMaxDequeueCount(maxDequeueCount);
        }
    }





}

constexpr size_t kSmoothnessFactor = 4;
constexpr size_t kRenderingDepth = 3;

    C2PortActualDelayTuning::output outputDelay(0);
    c2_status_t err = mComponent->query(
            {
                &iStreamFormat,
                &oStreamFormat,
                &kind,
                &reorderDepth,
                &reorderKey,
                &inputDelay,
                &pipelineDelay,
                &outputDelay,
                &secureMode,
            },
            {},
            C2_DONT_BLOCK,
            nullptr);
        size_t numOutputSlots = outputDelayValue + kSmoothnessFactor

        sp<IGraphicBufferProducer> outputSurface;
        uint32_t outputGeneration;
        int maxDequeueCount = 0;
        {
            Mutexed<OutputSurface>::Locked output(mOutputSurface);
            maxDequeueCount = output->maxDequeueBuffers = numOutputSlots +
                    reorderDepth.value + kRenderingDepth;
            outputSurface = output->surface ?
                    output->surface->getIGraphicBufferProducer() : nullptr;
            if (outputSurface) {
                output->surface->setMaxDequeuedBufferCount(output->maxDequeueBuffers);
            }
            outputGeneration = output->generation;

constexpr uint32_t kDefaultOutputDelay = 8;

        addParameter(
                DefineParam(mActualOutputDelay, C2_PARAMKEY_OUTPUT_DELAY)
                .withDefault(new C2PortActualDelayTuning::output(kDefaultOutputDelay))
                .withFields({C2F(mActualOutputDelay, value).inRange(0, kMaxOutputDelay)})
                .withSetter(Setter<decltype(*mActualOutputDelay)>::StrictValueWithNoDeps)
                .build());



   if (ps_decode_op->i4_reorder_depth >= 0 && mOutputDelay != ps_decode_op->i4_reorder_depth) {
            mOutputDelay = ps_decode_op->i4_reorder_depth;
            ALOGV("New Output delay %d ", mOutputDelay);
            C2PortActualDelayTuning::output outputDelay(mOutputDelay);
            std::vector<std::unique_ptr<C2SettingResult>> failures;
            c2_status_t err =
                mIntf->config({&outputDelay}, C2_MAY_BLOCK, &failures);
            if (err == OK) {
                work->worklets.front()->output.configUpdate.push_back(
                    C2Param::Copy(outputDelay));
            } else {
                ALOGE("Cannot set output delay");
                mSignalledError = true;
                work->workletsProcessed = 1u;
                work->result = C2_CORRUPTED;
                return;
            }
        }


从哪个pool中申请buffer

在CCodecBufferChannel::start的时候决定,在下面代码中将pools的allocatedID转为
C2BufferQueueBlockPool。 在这之后调用mComponent->createBlockPool。Codec2Client::Component::createBlockPool调用c2store的c2_status_t createBlockPool()然后调用_createBlockPool,在之前设置了是BUFFERQUEUE,这边就保存了创建好的C2BufferQueueBlockPool。 在后面解码的流程中fetchGrallocBlock,使用的是这个类型的
C2BufferQueueBlockPool。

poolmask的默认值:

int GetCodec2PoolMask() {
    return property_get_int32(
            "debug.stagefright.c2-poolmask",
            1 << C2PlatformAllocatorStore::ION |
            1 << C2PlatformAllocatorStore::BUFFERQUEUE);
}

int poolMask = GetCodec2PoolMask();

申请的buffer的类型函数是bufferqueue

if (pools->outputAllocatorId == C2PlatformAllocatorStore::GRALLOC
&& err != C2_OK
&& ((poolMask >> C2PlatformAllocatorStore::BUFFERQUEUE) & 1)) {
pools->outputAllocatorId = C2PlatformAllocatorStore::BUFFERQUEUE;
}
}


bufferqueue的申请调用的是C2PlatformAllocatorStoreImpl的fetchAllocator

case C2PlatformAllocatorStore::BUFFERQUEUE:
res = allocatorStore->fetchAllocator(
C2PlatformAllocatorStore::BUFFERQUEUE, &allocator);
if (res == C2_OK) {
std::shared_ptr<C2BlockPool> ptr(
new C2BufferQueueBlockPool(allocator, poolId), deleter);
*pool = ptr;
mBlockPools[poolId] = ptr;
mComponents[poolId].insert(
mComponents[poolId].end(),
components.begin(), components.end());
}
break;

fetchAllocator返回gralloc的allocator。

std::shared_ptr<C2Allocator> C2PlatformAllocatorStoreImpl::fetchBufferQueueAllocator() {
    static std::mutex mutex;
    static std::weak_ptr<C2Allocator> grallocAllocator;
    std::lock_guard<std::mutex> lock(mutex);
    std::shared_ptr<C2Allocator> allocator = grallocAllocator.lock();
    if (allocator == nullptr) {
        allocator = std::make_shared<C2AllocatorGralloc>(
                C2PlatformAllocatorStore::BUFFERQUEUE, true);
        grallocAllocator = allocator;
    }
    return allocator;
}


### fetchGraphicBlock 流程

  • fetchGraphicBlock

fetch经过一系列判断和处理 最终调用mProducer的dequeueBuffer

    c2_status_t fetchGraphicBlock(
            uint32_t width,
            uint32_t height,
            uint32_t format,
            C2MemoryUsage usage,
            std::shared_ptr<C2GraphicBlock> *block /* nonnull */,
            C2Fence *fence) {

c2_status_t status = fetchFromIgbp_l(width, height, format, usage, block, fence);
             c2Status = dequeueBuffer(width, height, format, usage,
                              &slot, &bufferNeedsReallocation, &fence);                         
        if (fence) {
            static constexpr int kFenceWaitTimeMs = 10;
            status_t status = fence->wait(kFenceWaitTimeMs);
     }
               
    其中 dequeueBuffer
    
        Return<void> transResult = mProducer->dequeueBuffer(
                Input{
                    width,
                    height,
                    format,
                    androidUsage.asGrallocUsage()},
                [&status, slot, needsRealloc,
                 fence](HStatus hStatus,
                         int32_t hSlot,
                         Output const& hOutput) {
                    *slot = static_cast<int>(hSlot);
                    if (!h2b(hStatus, &status) ||
                            !h2b(hOutput.fence, fence)) {
                        status = ::android::BAD_VALUE;
                    } else {
                        *needsRealloc =
                                hOutput.bufferNeedsReallocation;
                    }
                });




  • dequebuffer中的fence 有什么作用

Fence是一种同步机制,用于GraphicBuffer的同步。用来处理跨硬件平台不同的情况(CPU和GPU),尤其是CPU、GPU和HWC之间的同步。另外,也可用于多个时间点之间的同步,当Graphics Buffer的生产者或消费者在对buffer处理完之后,通过fence发出信号,这样系统可以异步queue当前不需要但有可能接下来会使用读写的buffer。

简言之,在合适的时间发一种信号,将先到的buffer拦住,等后来的到达,两者步调一致再一起走。也就是dequeuebuffer 之后并不能直接用这块buffer,需要等待buffer的fence发送上来之后 才可以使用这块buffer。

  • 完整一个获取fetch buffer的流程

dequeueBuffer ---->(获取到slot或fence) fence->wait -----> mProducer->requestBuffer(通过slot 获取到buffer)

将从gralloc 获取到的buffer (native_handle_t)通过调用android::WrapNativeCodec2GrallocHandle转化为C2Handle
这个C2Handle 会生成C2AllocationGralloc,这个alloc最后会new 封装成C2GraphicBlock。这个block就是返回给外部解码申请的地方。

经过上面的这个流程 解码要的共享的buffer 就从gralloc这边申请出来了,然后这个buffer就可以给到后面的解码器使用了,如果是软解就map出虚拟地址,然后将软解后的数据拷贝到里面。但一般厂商不会用软解,正常的实现是这块buffer给到硬件,硬解数据直接写到这块buffer。

解码的buffer准备好之后,会把grallocblock的buffer 转换为c2buffer 然后会放到c2work中output buffers里面。

std::shared_ptr<C2Buffer> buffer
= createGraphicBuffer(std::move(entry->outblock),
C2Rect(mWidth, mHeight).at(left, top));

解码后框架的处理流程

解码后的哪些信息是携带在work里面的, 解码的buffer,

work->worklets.front()->output.flags = (C2FrameData::flags_t)0;
work->worklets.front()->output.buffers.clear();
work->worklets.front()->output.buffers.push_back(buffer);
work->worklets.front()->output.ordinal = work->input.ordinal;
work->workletsProcessed = 1u;

  • 从应用开始, 应用调用的是dequeueOutputBuffer返回的是index 时间戳等等信息,这个调用到mediacodec, mediacodec 从 mAvailPortBuffers 取出可用的buffer。
  • mAvailPortBuffers是通过解码那边 BufferCallback onOutputBufferAvailable来把解码buffer push 到mAvailPortBuffers。这个回调是simpleC2Componet 的finish的listener->onWorkDone_nb调用到CCodec的onWorkDone。
  • onWorkDone调用到mChannel->onWorkDone。 在mChannel的workDone 中 调用handleWork。
  • handlework 里面将解码器传递在work 中outputbuffer 转换为mediacodec的用的index 和 mediaCodecbuffer。同时返回到MediaCodec之前设置的callback。这个最后会返回应用设置callback的地方。
mCallback->onOutputBufferAvailable(index, outBuffer);

这个callback 是从何而来的。 在mediacodec的init的时候会新建一个codec 并将codec设置到codec2里面。mCodec->setCallback(
std::unique_ptrCodecBase::CodecCallback(
new CodecCallback(new AMessage(kWhatCodecNotify, this))));

  • 各个buffer 直接的转换

首先从解码这边出来的是C2GraphicBlock,会在codecbufferchannel中转为index 传递出去给mediacodec 转换过程是 内部有一个
mBuffers数组,在handlework先pushToStash到这里面。然后从这里面取出来。popFromStashAndRegister是这个里面去转换为mediacodec的buffer 和index 的。转换的MediaCodecBuffer, 就是把c2buffer的一个结构体赋值到Codec2Buffer中。c2Buffer->copy(buffer)。

renderOutbuffer 输出显示

  • render的时候传递的是index,同样也是mAvailPortBuffers 取出可用的buffer。
  • 这个buffer 通过status_t err = mBufferChannel->renderOutputBuffer(buffer, renderTimeNs)。将MediaCodecBuffer转换为C2buffer。
  • 从这个C2buffer 中取出C2ConstGraphicBlock, block 在转换为bqslot。 这个slot最后queue到surface那边。
getBufferQueueAssignment(block, &generation, &bqId, &bqSlot)
status = outputIgbp->queueBuffer(static_cast<int>(bqSlot),
input, output);

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用JMX监控ZooKeeper和Kafka

JVM 默认会通过 JMX 的方式暴露基础指标,很多中间件也会通过 JMX 的方式暴露业务指标,比如 Kafka、Zookeeper、ActiveMQ、Cassandra、Spark、Tomcat、Flink 等等。掌握了 JMX 监控方式,就掌握了一批程序的监控方式。本节介绍 JMX-Exporter 的使用,利用 JMX-Exporter 把 JMX…

win11,无法修改文件的只读属性,解决办法

在尝试更改文件或文件夹的权限时&#xff0c;您可能经常会遇到错误 - 无法枚举容器中的对象访问被拒绝。 虽然作为管理员&#xff0c;您可以更改访问权限&#xff0c;但有时即使是管理员也可能会遇到相同的错误消息。 这是一个常见错误&#xff0c;通常由不同论坛上的用户提出…

【云原生-Kurbernetes篇】HPA 与 Rancher管理工具

文章目录 一、Pod的自动伸缩1.1 HPA1.1.1 简介1.1.2 HPA的实现原理1.1.3 相关命令 1.2 VPA1.2.1 简介1.2.2 VPA的组件1.2.3 VPA工作原理 1.3 metrics-server简介 二、 HPA的部署与测试2.1 部署metrics-serverStep1 编写metrics-server的配置清单文件Step2 部署Step3 测试kubect…

Python数据结构基础教学,从零基础小白到实战大佬!

文章目录 前言 Python有那几种数据结构&#xff1f;1)列表&#xff08;list)1.1 什么是列表&#xff1f;1.2列表的增删改查 2&#xff09;字典&#xff08;Dictionary)2.1 什么是字典&#xff1f;2.2 字典的增删改查 3&#xff09;元组&#xff08;Tuple)4)集合&#xff08;Set…

STM32通用定时器产生PWM信号

STM32通用定时器产生PWM信号 PWM信号stm32定时器PWM生成模式PWM配置基本步骤PWM周期计算CubeMX配置代码展现 本期内容我将展示使用STM32通用定时器产生PWM信号&#xff0c;这里以定时器3通道3为例 PWM信号 如果还不懂的话&#xff0c;可以看看 &#xff1a; “蓝桥杯单片机学习…

SSM框架(一):Spring 容器

文章目录 一、Spring Framework系统框架二、IoC控制反转 与 DI依赖注入 简单入门三、Bean3.1 Bean的配置3.2 实例化Bean的四种方式3.3 Bean的生命周期 四、依赖注入4.1 setter注入4.2 构造器注入4.3 注入方式选择4.4 依赖自动装配4.5 集合注入4.6 案例&#xff1a;配置数据库4.…

Android加固为何重要?很多人不学

为什么要加固&#xff1f; APP加固是对APP代码逻辑的一种保护。原理是将应用文件进行某种形式的转换&#xff0c;包括不限于隐藏&#xff0c;混淆&#xff0c;加密等操作&#xff0c;进一步保护软件的利益不受损坏。总结主要有以下三方面预期效果&#xff1a; 1.防篡改&#x…

JSP在线商城系统eclipse定制开发mysql数据库BS模式java编程B2C

一、源码特点 java 在线商城系统是一套完善的web设计系统 &#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&#xff0c;使用…

Redis事务+秒杀案例

Redis事务是一个单独的隔离操作&#xff0c;是指将多条命令放在一个命令队列当中&#xff0c;按顺序执行&#xff0c;保证多个命令在同一个事务中执行而不受其他客户端的影响。 通俗来说就是&#xff1a;串联多个命令防止别的命令插队。 1.Multi、Exec、discard 在输入Multi命…

基于非链式(数组)结点结构的二叉树的层序、先序、中序、后序输入创建以及层序、先序、中序、后序输出

这个系列来记录学习一下如何用数组完成二叉树的4种顺序的创建&#xff0c;以及其4种顺序的遍历。 我们知道&#xff0c;对于一棵二叉树而言它有4种遍历的顺序&#xff0c;那自然就导致其输入结点时&#xff0c;也分这四种顺序。 分别是—— 层序&#xff1a; …

STM32定时器输入捕获测量高电平时间

STM32定时器输入捕获测量高电平时间 输入捕获测量高电平时间CuebMX配置代码部分 本篇内容要求读者对STM32通用定时器有一点理解&#xff0c;如有不解&#xff0c;请看 夜深人静学32系列15——通用定时器 输入捕获 输入捕获是STM32通用定时器的一种功能&#xff0c;可以捕获特定…

Selenium自动化测试详解

最近也有很多人私下问我&#xff0c;selenium学习难吗&#xff0c;基础入门的学习内容很多是3以前的版本资料&#xff0c;对于有基础的人来说&#xff0c;3到4的差别虽然有&#xff0c;但是不足以影响自己&#xff0c;但是对于没有学过的人来说&#xff0c;通过资料再到自己写的…

微信小程序记住密码,让登录解放双手

密码是用户最重要的数据&#xff0c;也是系统最需要保护的数据&#xff0c;我们在登录的时候需要用账号密码请求登录接口&#xff0c;如果用户勾选记住密码&#xff0c;那么下一次登录时&#xff0c;我们需要将账号密码回填到输入框&#xff0c;用户可以直接登录系统。我们分别…

从零开始的c语言日记day35——数据在内存中的储存

数据类型介绍 之前已经学了了一些基本的内置类型&#xff0c;以及空间大小。 类型的意义&#xff1a; 使用这个类型开辟内存空间的大小&#xff08;大小决定了使用范围&#xff09;。如何看待内存空间的视角 类型的基本归类 整形&#xff1a; 字符的本质是ASCLL码值&#x…

Java中的抽象类和接口

目录 1. 抽象类 1.1 抽象类概念 1.2 抽象类语法 1.3 抽象类需要注意的点 1.4 抽象类的作用 2. 接口 2.1 接口的概念 2.2 语法规则 2.3 接口使用 2.4 接口特性 2.5 实现多个接口 2.6 接口间的继承 2.7 接口使用实例 2.8 Clonable接口,浅拷贝和深拷贝 2.9 抽…

使用pytorch利用神经网络原理进行图片的训练(持续学习中....)

1.做这件事的目的 语言只是工具,使用python训练图片数据,最终会得到.pth的训练文件,java有使用这个文件进行图片识别的工具,顺便整合,我觉得Neo4J正确率太低了,草莓都能识别成为苹果,而且速度慢,不能持续识别视频帧 2.什么是神经网络?(其实就是数学的排列组合最终得到统计结果…

算法分析与设计课后练习23

求下面的0-1背包问题 &#xff08;1&#xff09;N5,M12,(p1,p2,…,p5)(10,15,6,8,4),(w1,w2,…,w5)(4,6,3,4,2) &#xff08;2&#xff09;N5,M15,(p1,p2,…,p5)(w1,w2,…,w5)(4,4,5,8,9)

深入理解JSON及其在Java中的应用

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a;每天一个知识点 ✨特色专栏&#xff1a…

日常办公:批处理编写Word邮件合并获取图片全路径

大家在使用Word邮件合并这个功能&#xff0c;比如制作席卡、贺卡、准考证、员工档案、成绩单、邀请函、名片等等&#xff0c;那就需要对图片路径进行转换处理&#xff0c;此脚本就是直接将图片的路径提取出来&#xff0c;并把内容放到txt格式的文本文档里&#xff0c;打开Excel…

netty整合websocket(完美教程)

websocket的介绍&#xff1a; WebSocket是一种在网络通信中的协议&#xff0c;它是独立于HTTP协议的。该协议基于TCP/IP协议&#xff0c;可以提供双向通讯并保有状态。这意味着客户端和服务器可以进行实时响应&#xff0c;并且这种响应是双向的。WebSocket协议端口通常是80&am…