音视频开发项目:H.265播放器:视频解码篇

news2024/11/25 3:46:46

视频演示

如下将演示新版播放器播放 1分钟1080p/25fps/H.265 MP4视频,具体视频参数如下:

粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

  1. 预加载1000000帧(即整个视频),完全解码不播放的内存占用、CPU占用、解码间隔时间

因为整个解码过程没有进行播放,所以解码间隔=单帧解码耗时。

从上面视频能看出来,一个几十M的文件完全解码能达到4.6G的内存占用,CPU占用高达300以上(4核)。当然,这是完全不做限制,火力全开解码。但也能得出结论:无干扰情况下平均解码一帧1080p仅需要13ms(基于mbp2015版)。

旧版直播播放器解码720p需要26ms(基于mbp2015版),而新版播放器播1080p目前的13ms还不是极限,后续将继续探索优化空间。

  1. 预加载10帧并解码,后续边播边解的相关数据

演示1太过极端不符合日常使用的场景,但因为极限情况平均解码只需要13ms,而视频帧率是25(即间隔40ms),所以可以隔一段时间喂几帧到解码器,这样平衡了播放和解码的速率之后,CPU占用降到120左右、内存占用降低到了300M。同时还能流畅播放。不过播放策略有很多种,各位有更好的方案也欢迎和我交流。

架构设计

整体架构设计

上图所示为新播放器基本骨架,包含了主要模块。模块间互相独立,各自接收通用协议的参数。比如Loader传递给Demuxer的数据为ArrayBuffer,经Demuxer统一解封装成Packet格式Buffer数据(Annex-B)喂给Renderer。上图用MP4举例(HVCC为H.265码流格式之一),替换成flv、ts格式也是遵循这个流程。Renderer负责decoder调度,音画同步、音视频播放等,可以说是播放器最核心的模块。UI View则主要用来绘制播放器控件UI,如进度条等。本文不打算详细介绍每个功能,仅对decoder做细节解构,其它有关联的模块仅简单说明和实现。

DEMO架构

因为没有Demuxer,所以直接用Loader读取Annex-B码流。

  1. 通过Loader读取到Annex-B码流的Uint8Array数据
  2. 通过postMessge将数据发送给Worker线程的WASM包解码
  3. WASM通过回调函数传回YUV数据给Worker再通过postMessage传给主线程Canvas

实操步骤

如何将 FFmpeg 编译成 WASM 包

接下来就进入正题了,第一步,先编译FFmpeg做精简,为啥呢?因为FFmpeg不光是个C库,还是非常庞大的C库。我们要在Web上使用它就需要移除一些无用的模块,好在FFmpeg提供了相应配置的能力,使用根目录configure文件按如下步骤操作即可。

1. 准备

  • 编译前我们需要去emscripten官网[7]下载最新版emsdk

emsdk就是用来把FFmpeg编译成wasm包的工具

  • 官网FFmpeg[8] 下载源码版的FFmpeg(本文基于4.1)

2. 编译FFmpeg静态库

创建 make_decoder.sh

echo "Beginning Build:"
rm -r ./ffmpeg-lite
mkdir -p ./ffmpeg-lite # dist目录
cd ../ffmpeg  # src目录,ffmpeg源码
make clean
emconfigure ./configure --cc="emcc" --cxx="em++" --ar="emar" --ranlib="emranlib" --prefix=$(pwd)/../ffmpeg-wasm/ffmpeg-lite --enable-cross-compile --target-os=none --arch=x86_32 --cpu=generic \
    --enable-gpl --enable-version3 \
    --disable-swresample --disable-postproc --disable-logging --disable-everything \
    --disable-programs --disable-asm --disable-doc --disable-network --disable-debug \
    --disable-iconv --disable-sdl2 \ # 三方库
    --disable-avdevice \  # 设备
    --disable-avformat \ # 格式
    --disable-avfilter \  # 滤镜
    --disable-decoders \  # 解码器
    --disable-encoders \  # 编码器
    --disable-hwaccels \ # 硬件加速
    --disable-demuxers \ # 解封装
    --disable-muxers \  # 封装
    --disable-parsers \ # 解析器
    --disable-protocols \  # 协议
    --disable-bsfs \  # bit stream filter,码流转换
    --disable-indevs \  # 输入设备
    --disable-outdevs \ #输出设备
    --disable-filters \ # 滤镜
    --enable-decoder=hevc \ 
    --enable-parser=hevc
make
make install

因为wasm支持的能力还是比较有限,一些FFmpeg用来优化性能的模块都需要禁用(比如硬件加速、汇编等)。本文也仅介绍解码。所以播放涉及的功能只用到了hevc-decoder(hevc=h265),其它的通通禁掉。

执行make_decoder.sh在ffmpeg-lite文件夹内生成简化后的FFmpeg静态库和对应的.h声明文件。

3. 编写入口文件

编译完依赖库不代表就直接能用了,还需要自己动手写入口文件的代码去调用FFmpeg的接口,这一步就需要你稍微懂一点点c语言了。我们起个名字叫decoder.c

初始化解码器

首先我们调用init_decoder初始化解码器,依次初始化codec、dec_ctx、parser、frame、pkt。frame和pkt作为全局变量用来给后面交换数据使用。init_decoder接收一个JS回调函数作为入参。后面通过这个回调函数给JS worker线程回传数据。回调函数声明定义了三个入参,依次是数据开始地址、长度、以及pts。本文暂不涉及pts,不传也可以。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
typedef void(*OnBuffer)(unsigned char* data_y, int size, int pts);
AVCodec *codec = NULL;
AVCodecContext *dec_ctx = NULL;
AVCodecParserContext *parser_ctx = NULL;
AVPacket *pkt = NULL;
AVFrame *frame = NULL;
OnBuffer decoder_callback = NULL;
void init_decoder(OnBuffer callback) {
 // 找到hevc解码器
    codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
   // 初始化对应的解析器
    parser_ctx = av_parser_init(codec->id);
   // 初始化上下文
    dec_ctx = avcodec_alloc_context3(codec);
    // 打开decoder
    avcodec_open2(dec_ctx, codec, NULL);
 // 分配一个frame内存,并指明yuv 420p格式
    frame = av_frame_alloc();
    frame->format = AV_PIX_FMT_YUV420P;
   // 分配一个pkt内存
    pkt = av_packet_alloc();
 // 暂存回调
    decoder_callback = callback;
}

uint8转AVPacket

这一步就是接收JS的视频数据给到av_parser_parse2方法,av_parser_parse2接收任意长度的buffer数据,并从buffer中解析出avpacket结构直到没有数据为止。avpacket存放了压缩的媒体数据,如果是视频类型,则通常表示一帧,音频数据表示N帧。下面节选了一段FFmpeg源码注释

This structure stores compressed data. It is typically exported by demuxers and then passed as input to decoders, or received as output from encoders and then passed to muxers. For video, it should typically contain one compressed frame. For audio it may contain several compressed frames. Encoders are allowed to output empty packets, with no compressed data, containing only side data (e.g. to update some stream parameters at the end of encoding).

void decode_buffer(uint8_t* buffer, size_t data_size) { // 入参是js传入的uint8array数据以及数据长度
 while (data_size > 0) {
     // 从buffer中解析出packet
  int size = av_parser_parse2(parser_ctx, dec_ctx, &pkt->data, &pkt->size,
   buffer, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
  if (size < 0) {
   break;
  }
  buffer += size;
  data_size -= size;
  if (pkt->size) {
     // 解码packet
   decode_packet(dec_ctx, frame, pkt);
  }
 }
}

解码AVPacket,接收AVFrame

拿到avpacket之后,需要调用avcodec_send_packet把数据扔给解码器解码,上面已经说到了音频数据一个packet可能包含了多个帧(即avframe),所以通过一个while循环调用avcodec_receive_frame从解码器中取出avframe数据。直到它返回AVERROR(EAGAIN)、AVERROR_EOF或错误。avframe包含的就是解码后的数据了。

AVERROR(EAGAIN)表示packet数据消费完了,需要新数据。而AVERROR_EOF则是当你输入的pkt->data为NULL时会触发。解码器一般会缓存几帧的数据,当你想拿到这些数据时就需要传递NULL的pkt给解码器。

avcodec_send_packet是4.x版本的新解口,3.x是avcodec_decode_video2和avcodec_decode_audio4。前者如上面所说,输入一次,输出多次。后者则是当pkt数据不足以产生frame的时候,需要在后续数据到来时合并数据并重新调用方法进行解码。

int decode_packet(AVCodecContext* ctx, AVFrame* frame, AVPacket* pkt)
{
    int ret = 0;
    // 发送packet到解码器
    ret = avcodec_send_packet(dec, pkt);
    if (ret < 0) {
        return ret;
    }
    // 从解码器接收frame
    while (ret >= 0) {
        ret = avcodec_receive_frame(dec, frame);
  if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
    break;
  } else if (ret < 0) {
    // handle error
    break;
  }
    // 输出yuv buffer数据
  output_yuv_buffer(frame);
    }
    return ret;
}

AVFrame转YUV uint8

拿到解码后的avframe数据后我们需要把它的传递给JS,但因为avframe的数据是个双层数组。而我们需要把它转换成uint8再传给JS线程。

YUV 图像有两种存储格式:

  • 紧缩格式(packed formats): Y、U、V 三通道像素值依次排列,即 Y0 U0 V0 Y1 U1 V1 …
  • 平面格式(planar formats): 先排列 Y 的所有像素值,再排列 U,最后排列 V YUV420p 中使用平面格式,水平 2:1 取样,垂直 2:1 采样,即每 4 个 Y 分量对应一个 U、V 分量

如上图所示,我们编写代码把avframe数据依次copy到yuv_buffer中,并使用decoder_callback传给JS线程

实际上你这一步怎么存都可以,但在渲染的时候你得依据存的顺序取出数据并按420p的方式渲染

void output_yuv_buffer(AVFrame *frame) {
 int width, height, frame_size;
 uint8_t *yuv_buffer = NULL;
 width = frame->width;
 height = frame->height;
   // 根据格式,获取buffer大小
 frame_size = av_image_get_buffer_size(frame->format, width, height, 1);
   // 分配内存
 yuv_buffer = (uint8_t *)av_mallocz(frame_size * sizeof(uint8_t));
   // 将frame数据按照yuv的格式依次填充到bufferr中。下面的步骤可以用工具函数av_image_copy_to_buffer代替。
 int i, j, k;
    // Y
 for(i = 0; i < height; i++) {
     memcpy(yuv_buffer + width*i,
             frame->data[0]+frame->linesize[0]*i,
             width);
 }
 for(j = 0; j < height / 2; j++) {
     memcpy(yuv_buffer + width * i + width / 2 * j,
             frame->data[1] + frame->linesize[1] * j,
             width / 2);
 }
 for(k =0; k < height / 2; k++) {
     memcpy(yuv_buffer + width * i + width / 2 * j + width / 2 * k,
             frame->data[2] + frame->linesize[2] * k,
             width / 2);
 }
  // 通过之前传入的回调函数发给js
 decoder_callback(yuv_buffer, frame_size, frame->pts);
 av_free(yuv_buffer);
}

以上就是入口文件的所有代码,我尽量用最简化的代码呈现。总共包含了init_decoder、decode_buffer、decode_packet、output_yuv_buffer。其它不关键的部分都省略了,比如(close_decoder、异常处理等)

注意:因为编译时没有包含demux、bsfs。所以decoder_buffer接收的buffer数据必须是annexb码流。

4. 编译WASM包

终于到了本小节的尾声,把入口文件+依赖库编译成wasm包。这一步比较简单,依然是创建一个build_decoder.sh,按下面的代码编写,然后执行即可。

export TOTAL_MEMORY=67108864
export EXPORTED_FUNCTIONS="[ \
    '_init_decoder', \
    '_decode_buffer'
]"
echo "Running Emscripten..."
# 入口文件+3个依赖库文件
emcc decoder.c ffmpeg-lite/lib/libavcodec.a ffmpeg-lite/lib/libavutil.a ffmpeg-lite/lib/libswscale.a \
    -O2 \
    -I "ffmpeg-lite/include" \
    -s WASM=1 \ 
    -s ASSERTIONS=1 \
    -s LLD_REPORT_UNDEFINED \
    -s NO_EXIT_RUNTIME=1 \
    -s DISABLE_EXCEPTION_CATCHING=1 \
    -s TOTAL_MEMORY=${TOTAL_MEMORY} \
    -s EXPORTED_FUNCTIONS="${EXPORTED_FUNCTIONS}" \
    -s EXTRA_EXPORTED_RUNTIME_METHODS="['addFunction', 'removeFunction']" \
 -s RESERVED_FUNCTION_POINTERS=14 \
 -s FORCE_FILESYSTEM=1 \
    -o ./wasm/libffmpeg.js
echo "Finished Build"

EXPORTED_FUNCTIONS就是入口文件里需要对外暴露的方法了。记得前面加_

构建产物如下:

libffmpeg.js就是wasm包的JS入口文件

JS如何加载并调用WASM包方法

Worker部分

本环节到了我们的主场领域,编写JS代码(采用了TypeScript语法,应该不影响阅读吧)。由于WASM代码需要跑在worker线程。所以下面代码的环境变量只能在worker中访问

decoder.ts

export class Decoder extends EventEmitter<IEventMap> {
    M: any
    init(M: any) {
    // M = self.Module 即wasm环境变量
        this.M = M
    // 创建wasm的回调函数,viii表示有3个int参数
        const callback = this.M.addFunction(this._handleYUVData, 'viii')
  // 通过我们上面decoder.c文件的方法传入回调
        this.M._init_decoder(callback)
    }
    decode(packet: IPacket) {
        const { data } = packet
        const typedArray = data
        const bufferLength = typedArray.length
  // 申请内存区,并放入数据
        const bufferPtr = this.M._malloc(bufferLength)
        this.M.HEAPU8.set(typedArray, bufferPtr)
    // 解码buffer
        this.M._decode_buffer(bufferPtr, bufferLength)
    // 释放内存区
        this.M._free(bufferPtr)
    }
    private _handleYUVData = (start: number, size: number, pts: number) => {
    // 回调传回来的第一个参数是yuv_buffer的内存起始索引
        const u8s = this.M.HEAPU8.subarray(start, start + size)
        const output = new Uint8Array(u8s)
        this.emit('decoded-frame', {
            data: output,
            pts,
        })
    }
}

decoder-manager.ts

因为Worker线程加载wasm文件是异步的,需要在onRuntimeInitialized之后才能调用wasm方法,所以写了一个简单的manager管理decoder。

import { Decoder } from './decoder'
const global = self as any
export class DecoderManager {
    loaded = false
    decoder = new Decoder()
    cachePackets: IPacket[] = []
    load() {
     // 表明wasm文件的位置
        global.Module = {
            locateFile: (wasm: string) => './wasm/' + wasm,
        }
        global.importScripts('./wasm/libffmpeg.js')
       // 初始化之后,执行一次push,把缓存的packet送到decoder里
        global.Module.onRuntimeInitialized = () => {
            this.loaded = true
            this.decoder.init(global.Module)
            this.push([])
        }
        this.decoder.on('decoded-frame', this.handleYUVBuffer)
    }
    push(packets: IPacket[]) {
     // 没加载就缓存起来,加载了就先取缓存
        if (!this.loaded) {
            this.cachePackets = this.cachePackets.concat(packets)
        } else {
            if (this.cachePackets.length) {
                this.cachePackets.forEach((frame) => this.decoder.decode(frame))
                this.cachePackets = []
            }
            packets.forEach((frame) => this.decoder.decode(frame))
        }
    }
    handleYUVBuffer = (frame) => {
        global.postMessage({
            type: 'decoded-frame',
            data: frame,
        })
    }
}
const manager = new DecoderManager()
manager.load()
self.onmessage = function(event) {
    const data = event.data
    const type = data.type
    switch (type) {
        case 'decode':
            manager.push(data.data)
            break
    }
}

JS主线程部分

这一步为加载worker代码并进行通信。加载worker的流程很简单,使用webpack+worker-loader即可,然后用fetch递归读取数据并发送给worker线程,编码器接收到数据就会进行解码。

import Worker from 'worker-loader!../worker/decoder-manager'
const worker = new Worker()
const url = 'http://xx.com' // 码流地址
fetch(url)
.then((res) => {
    if (res.body) {
        const reader = res.body.getReader()
        const read = () => {
    // 递归读取buffer数据
            reader.read().then((json) => {
                if (!json.done) {
                    worker.postMessage({
                        type: 'decode',
                        data: [{
                            data: json
                        }],
                    })
                    read()
                }
            })
        }
        read()
    }
})

结语

按照上面的代码就可以实现一个简易的H.265解码器,如下是用JS仿照前文所列举的AVPacket和AVFrame结构打印出来的数据:

解码前:从JS主线程传递给WASM的数据

解码后:从WASM传递给JS主线程的数据

上图对比可以看出解码后的数据量有多么恐怖,所以就像在开始的视频里所演示的,解码完成后的内存管理十分重要。

以上就是H.265视频解码篇的全部内容了。音频解码同样可以复用上面的链路去解码,也可以使用浏览器自带的decodeAudioData。音频播放则是使用AudioContext。目前主流的音频编码格式浏览器都支持。最后希望上面的经验分享能够帮大家少踩点坑。另外除了播放H.265以外,FFmpeg也可以做很多视频处理的工作。大家可以思维发散畅想可能的应用场景。

粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

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

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

相关文章

SpringBoot整合rabbitmq-扇形交换机队列(三)

说明&#xff1a;本文章主要是Fanout 扇形交换机的使用&#xff0c;它路由键的概念&#xff0c;绑定了页无用&#xff0c;这个交换机在接收到消息后&#xff0c;会直接转发到绑定到它上面的所有队列。 大白话&#xff1a;广播模式&#xff0c;交换机会把消息发给绑定它的所有队…

编译链接实战(25)ThreadSanitizer检测线程安全

ThreadSanitizer&#xff08;又称为TSan&#xff09;是一个用于C/C的数据竞争检测器。在并发系统中&#xff0c;数据竞争是最常见且最难调试的错误类型之一。当两个线程并发访问同一个变量&#xff0c;并且至少有一个访问是写操作时&#xff0c;就会发生数据竞争。C11标准正式将…

STM32 | J-link安装过程

J-Link是一种由SEGGER公司开发的调试器和仿真器,用于嵌入式系统开发。它可以帮助开发人员在开发过程中进行调试和仿真,提供了快速、稳定的连接,并支持多种不同类型的微处理器和微控制器。 要获取J-Link软件,请访问SEGGER公司的官方网站(www.segger.com)并进入他们的下载…

LLC谐振变换器变频移相混合控制MATLAB仿真

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 基本控制原理 为了实现变换器较小的电压增益&#xff0c;同时又有较 高的效率&#xff0c;文中在变频控制的基础上加入移相控制&#xff0c; 在这种控制策略下&#xff0c;变换器通过调节一次侧开关管 的开关…

现代信号处理学习笔记(二)参数估计理论

参数估计理论为我们提供了一套系统性的工具和方法&#xff0c;使我们能够从样本数据中推断总体参数&#xff0c;并评估估计的准确性和可靠性。这些概念在统计学和数据分析中起着关键的作用。 目录 前言 一、估计子的性能 1、无偏估计与渐近无偏估计 2、估计子的有效性 两个…

【C++】用命名空间避免命名冲突

&#x1f338;博主主页&#xff1a;釉色清风&#x1f338;文章专栏&#xff1a;C&#x1f338;今日语录&#xff1a;如果神明还不帮你&#xff0c;说明他相信你。 &#x1fab7;文章简介&#xff1a;这篇文章是结合谭浩强老师的书以及自己的理解&#xff0c;同时加入了一些例子…

【前端素材】推荐优质后台管理系统网页Stisla平台模板(附源码)

一、需求分析 1、系统定义 后台管理系统是一种用于管理和控制网站、应用程序或系统的管理界面。它通常被设计用来让网站或应用程序的管理员或运营人员管理内容、用户、数据以及其他相关功能。后台管理系统是一种用于管理网站、应用程序或系统的工具&#xff0c;通常由管理员使…

matlab实现层次聚类与k-均值聚类算法

1. 原理 1.层次聚类&#xff1a;通过计算两类数据点间的相似性&#xff0c;对所有数据点中最为相似的两个数据点进行组合&#xff0c;并反复迭代这一过程并生成聚类树 2.k-means聚类&#xff1a;在数据集中根据一定策略选择K个点作为每个簇的初始中心&#xff0c;然后将数据划…

【InternLM 实战营笔记】基于 InternLM 和 LangChain 搭建你的知识库

准备环境 bash /root/share/install_conda_env_internlm_base.sh InternLM升级PIP # 升级pip python -m pip install --upgrade pippip install modelscope1.9.5 pip install transformers4.35.2 pip install streamlit1.24.0 pip install sentencepiece0.1.99 pip install a…

银行卡二三四要素验证API接口:实现高准确性与高稳定性的验证服务

在进行互联网金融、电商等业务时&#xff0c;我们常常需要验证用户的银行卡信息&#xff0c;以确保交易安全和顺利进行。而银行卡二三四要素验证API接口&#xff0c;就成为了一种高效、准确的解决方案。本文将以挖数据平台为例&#xff0c;介绍该接口的使用方法和优势。 接口简…

【自动驾驶技术系列丛书学习】1.《自动驾驶技术概论》学习笔记

《自动驾驶技术概论》学习笔记 致谢&#xff1a;作者&#xff1a;王建、徐国艳、陈竞凯、冯宗宝 本书主要介绍汽车构造和无人驾驶汽车的基本概念&#xff0c;从基础开始&#xff0c;由浅入深地了解无人驾驶的历史由来、国内外自动驾驶产业现状及技术发展、自动驾驶汽车的技术架…

jupyter调用envs环境——jupyter内核配置虚拟环境

1.jupyter无法使用envs环境 pycharm的终端打开jupyter notebook&#xff1a; 在kernel下找不到上面的Pytorch_GPU环境&#xff1a; 2.解决方法 在对应的envs环境中安装ipykernel&#xff1a; 将该环境写入jupyter&#xff1a; python -m ipykernel install --user --name Py…

lv20 QT进程线程编程

知识点&#xff1a;启动进程 &#xff0c;线程 &#xff0c;线程同步互斥 1 启动进程 应用场景&#xff1a;通常在qt中打开另一个程序 process模板 QString program “/bin/ls"; QStringList arguments; arguments << "-l" << “-a";QPro…

LeetCode 热题100 刷题笔记

一&#xff1a;哈希表 一般哈希表都是用来快速判断一个元素是否出现集合里。 直白来讲其实数组就是一张哈希表&#xff0c;哈希表中关键码就是数组的索引下标&#xff0c;然后通过下标直接访问数组中的元素。 1.两数之和 题目链接&#xff1a;. - 力扣&#xff08;LeetCode…

C++ //练习 10.15 编写一个lambda,捕获它所在函数的int,并接受一个int参数。lambda应该返回捕获的int和int参数的和。

C Primer&#xff08;第5版&#xff09; 练习 10.15 练习 10.15 编写一个lambda&#xff0c;捕获它所在函数的int&#xff0c;并接受一个int参数。lambda应该返回捕获的int和int参数的和。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;v…

TDengine 在 DISTRIBUTECH 分享输配电数据管理实践

2 月 27-29 日&#xff0c;2024 美国国际输配电电网及公共事业展&#xff08;DISTRIBUTECH International 2024&#xff09;在美国-佛罗里达州-奥兰多国家会展中心举办。作为全球领先的年度输配电行业盛会&#xff0c;也是美洲地区首屈一指的专业展览会&#xff0c;该展会的举办…

uniapp_微信小程序日历

一、需求要求这样 二、代码实现 <view class"calender" click"showriliall"><text class"lineText">探视日期&#xff1a;</text><text class"middleText">{{timerili}}</text><image src"/s…

unsubscribe:Angular 项目中常见场景以及是否需要 unsubscribe

本文由庄汇晔同学编写~ 在 Angular 项目中&#xff0c;经常会使用到 observable subscribe&#xff0c;但是 subscribe 读取了数据之后&#xff0c;真的就是万事大吉了吗&#xff1f;这个问题的答案或许是&#xff0c;或许不是。有些 observable 需要 unsubscribe&#xff0c;…

递归回溯剪枝-括号生成

LCR 085. 括号生成 - 力扣&#xff08;LeetCode&#xff09; 一. 根据题意&#xff0c;分析出符合要求的括号组合需要满足以下两个条件&#xff1a; 1. 左括号数或者右括号数都不能超过 n&#xff1b; 2. 从最左侧开始的每一个子集&#xff0c;不可以出现右括号数大于左括号数&…

GEE必须会教程—蒸散发数据时间序列分析与下载

今天带来的有关蒸散发数据的下载代码&#xff0c;蒸散发数据在气象气候&#xff0c;农业干旱监测等领域应用广泛&#xff0c;那么在GEE上如何方便快捷获取蒸散发数据呢&#xff1f;今天跟着小编分享代码&#xff0c;快来学习吧&#xff01;&#xff01; A.定义研究区域 //定义…