Javacv-利用Netty实现推流直播复用(flv)

news2024/9/30 7:20:40

前言

上一篇文章《JavaCV之rtmp推流(FLV和M3U8)》介绍了javacv的基本使用,今天来讲讲如何实现推流复用。
以监控摄像头的直播为例,通常分为三步:

  1. 从设备获取音视频流
  2. 利用javacv进行解码(例如flv或m3u8)
  3. 将视频解码后数据推送到前端页面播放

推流直播复用,是指假如该设备某一个channel已经在解码直播了,其他channel只需要直接拿该设备解码后的视频帧数据进行播放即可,而无需重复上面三步。实现一次解码,多客户端播放。

什么是channel?

在Netty中,每个Channel实例代表一个与远程对等方的通信链接。在网络编程中,一个Channel通常对应于一个网络连接,可以是客户端到服务器的连接,也可以是服务器接受的客户端连接。

上述大概的推流复用流程如下图所示:

image.png

代码实例

MediaServer

负责创建Netty服务器。关键的步骤包括创建EventLoopGroup、配置ServerBootstrap、指定服务器的Channel类型为NioServerSocketChannel、设置服务器的处理器等。

这个服务器的实际处理逻辑是在LiveHandler类中实现的,这是一个自定义的ChannelHandler,它继承自SimpleChannelInboundHandler。在实际应用中,可以根据业务需求实现自己的ChannelHandler来处理接收到的消息。
这里维护了一个deviceContext设备容器,存放各个设备的TransferToFlv实例。

@Slf4j
@Component
public class MediaServer implements CommandLineRunner {

    @Autowired
    private LiveHandler liveHandler;
    public static ConcurrentHashMap<String, TransferToFlv> deviceContext = new ConcurrentHashMap<>();
    public final static String  YOUR_VIDEO_PATH = "D:\灌篮高手.mp4";
    public final static int PORT = 8234;

    public void start() {
        InetSocketAddress socketAddress = new InetSocketAddress("0.0.0.0", PORT);
        //主线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //工作线程组
        EventLoopGroup workGroup = new NioEventLoopGroup(200);
        ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) {
                        CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin().allowNullOrigin().allowCredentials().build();

                        socketChannel.pipeline()
                                .addLast(new HttpResponseEncoder())
                                .addLast(new HttpRequestDecoder())
                                .addLast(new ChunkedWriteHandler())
                                .addLast(new HttpObjectAggregator(64 * 1024))
                                .addLast(new CorsHandler(corsConfig))
                                .addLast(liveHandler);
                    }
                })
                .localAddress(socketAddress)
                .option(ChannelOption.SO_BACKLOG, 128)
                //选择直接内存
                .option(ChannelOption.ALLOCATOR, PreferredDirectByteBufAllocator.DEFAULT)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.SO_RCVBUF, 128 * 1024)
                .childOption(ChannelOption.SO_SNDBUF, 1024 * 1024)
                .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(1024 * 1024 / 2, 1024 * 1024));
        //绑定端口,开始接收进来的连接
        try {
            ChannelFuture future = bootstrap.bind(socketAddress).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //关闭主线程组
            bossGroup.shutdownGracefully();
            //关闭工作线程组
            workGroup.shutdownGracefully();
        }
    }

    @Override
    public void run(String... args) {
        this.start();
    }
}

LiveHandler

继承于SimpleChannelInboundHandler,它是Netty中的一个特殊类型的Channel处理器,用于处理从通道中读取的数据,提供了一个简化的channelRead0方法,用于处理接收到的消息,而不必担心消息的释放。
这里实现的是判断请求地址是否为/live,并且获取地址中的deviceId,并将channel加入到设备的httpClients

@Service
@ChannelHandler.Sharable
public class LiveHandler extends SimpleChannelInboundHandler<Object> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {

        FullHttpRequest req = (FullHttpRequest) msg;
        QueryStringDecoder decoder = new QueryStringDecoder(req.uri());
        // 判断请求uri
        if (!"/live".equals(decoder.path())) {
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        QueryStringDecoder queryStringDecoder = new QueryStringDecoder(req.uri());
        List<String> parameters = queryStringDecoder.parameters().get("deviceId");
        if(parameters == null || parameters.isEmpty()){
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        String deviceId = parameters.get(0);
        sendFlvResHeader(ctx);
        Device device = new Device(deviceId, MediaServer.YOUR_VIDEO_PATH);
        playForHttp(device, ctx);
    }

    public void playForHttp(Device device, ChannelHandlerContext ctx) {
        try {
            TransferToFlv mediaConvert = new TransferToFlv();
            if (MediaServer.deviceContext.containsKey(device.getDeviceId())) {
                mediaConvert = MediaServer.deviceContext.get(device.getDeviceId());
                mediaConvert.getMediaChannel().addChannel(ctx, true);
                return;
            }
            mediaConvert.setCurrentDevice(device);
            MediaChannel mediaChannel = new MediaChannel(device);
            mediaConvert.setMediaChannel(mediaChannel);
            MediaServer.deviceContext.put(device.getDeviceId(), mediaConvert);
            //注册事件
            mediaChannel.getEventBus().register(mediaConvert);
            new Thread(mediaConvert).start();
            mediaConvert.getMediaChannel().addChannel(ctx, false);
        } catch (InterruptedException | FFmpegFrameRecorder.Exception e) {
            throw new RuntimeException(e);
        }

    }


    /**
     * 错误请求响应
     *
     * @param ctx
     * @param status
     */
    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
                Unpooled.copiedBuffer("请求地址有误: " + status + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");

        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 发送req header,告知浏览器是flv格式
     *
     * @param ctx
     */
    private void sendFlvResHeader(ChannelHandlerContext ctx) {
        HttpResponse rsp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);

        rsp.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
                .set(HttpHeaderNames.CONTENT_TYPE, "video/x-flv").set(HttpHeaderNames.ACCEPT_RANGES, "bytes")
                .set(HttpHeaderNames.PRAGMA, "no-cache").set(HttpHeaderNames.CACHE_CONTROL, "no-cache")
                .set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED).set(HttpHeaderNames.SERVER, "测试");
        ctx.writeAndFlush(rsp);
    }
}

MediaChannel

主要负责每个设备的channel添加、关闭,以及向channel发送数据。利用newScheduledThreadPool进行周期性检查channel的在线情况,如果全部channel下线,则使用事件总线eventBus通知关闭解码推流。

@Data
@AllArgsConstructor
public class MediaChannel {

    private Device currentDevice;

    public ConcurrentHashMap<String, ChannelHandlerContext> httpClients;

    private ScheduledFuture<?> checkFuture;

    private final ScheduledExecutorService scheduler;

    protected EventBus eventBus;

    public MediaChannel(Device currentDevice) {
        this.currentDevice = currentDevice;
        this.httpClients = new ConcurrentHashMap<>();
        this.scheduler = Executors.newScheduledThreadPool(1);
        this.eventBus = new EventBus();
    }

    public void addChannel(ChannelHandlerContext ctx, boolean needSendFlvHeader) throws InterruptedException, FFmpegFrameRecorder.Exception {
        if (ctx.channel().isWritable()) {
            ChannelFuture channelFuture = null;
            if (needSendFlvHeader) {
                //如果当前设备正在有channel播放,则先发送flvheader,再发送视频数据。
                byte[] flvHeader = MediaServer.deviceContext.get(currentDevice.getDeviceId()).getFlvHeader();
                channelFuture = ctx.writeAndFlush(Unpooled.copiedBuffer(flvHeader));
            } else {
                channelFuture = ctx.writeAndFlush(Unpooled.copiedBuffer(new ByteArrayOutputStream().toByteArray()));
            }
            channelFuture.addListener(future -> {
                if (future.isSuccess()) {
                    httpClients.put(ctx.channel().id().toString(), ctx);
                }
            });
            this.checkFuture = scheduler.scheduleAtFixedRate(this::checkChannel, 0, 10, TimeUnit.SECONDS);
            System.out.println(currentDevice.getDeviceId() + ":channel:" + ctx.channel().id() + "创建成功");
        }
        Thread.sleep(50);
    }

    /**
     * 检查是否存在channel
     */
    private void checkChannel() {
        if (httpClients.isEmpty()) {
            System.out.println("通知关闭推流");
            eventBus.post(this.currentDevice);
            this.checkFuture = null;
            scheduler.shutdown();
        }
    }

    /**
     * 关闭通道
     */
    public void closeChannel() {
        for (Map.Entry<String, ChannelHandlerContext> entry : httpClients.entrySet()) {
            entry.getValue().close();
        }
    }

    /**
     * 发送数据
     *
     * @param data
     */
    public void sendData(byte[] data) {
        for (Map.Entry<String, ChannelHandlerContext> entry : httpClients.entrySet()) {
            if (entry.getValue().channel().isWritable()) {
                entry.getValue().writeAndFlush(Unpooled.copiedBuffer(data));
            } else {
                httpClients.remove(entry.getKey());
                System.out.println(currentDevice.getDeviceId() + ":channel:" + entry.getKey() + "已被去除");
            }
        }
    }


}

TransferToFlv

流的解码、推送部分就是在这个类里面,使用的是javacv封装的ffmpeg库,将音视频流转换为flv格式。实际的参数可以根据业务调整。
这里增加了一个获取flv格式header数据方法,因为flv格式视频必须要包含flv header才能播放。复用推流数据的时候,先向前端发送flv格式header,再发送流数据。

@Slf4j
@Data
public class TransferToFlv implements Runnable {

    private volatile boolean running = false;

    private FFmpegFrameGrabber grabber;

    private FFmpegFrameRecorder recorder;

    public ByteArrayOutputStream bos = new ByteArrayOutputStream();

    private Device currentDevice;

    private MediaChannel mediaChannel;

    public ConcurrentHashMap<String, ChannelHandlerContext> httpClients = new ConcurrentHashMap<>();

    /**
     * 创建拉流器
     *
     * @return
     */
    protected void createGrabber(String url) throws FFmpegFrameGrabber.Exception {

        grabber = new FFmpegFrameGrabber(url);
        //拉流超时时间(10秒)
        grabber.setOption("stimeout", "10000000");
        grabber.setOption("threads", "1");
        grabber.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
        // 设置缓存大小,提高画质、减少卡顿花屏
        grabber.setOption("buffer_size", "1024000");
        // 读写超时,适用于所有协议的通用读写超时
        grabber.setOption("rw_timeout", "15000000");
        // 探测视频流信息,为空默认5000000微秒
        // grabber.setOption("probesize", "5000000");
        // 解析视频流信息,为空默认5000000微秒
        //grabber.setOption("analyzeduration", "5000000");
        grabber.start();
    }

    /**
     * 创建录制器
     *
     * @return
     */
    protected void createTransterOrRecodeRecorder() throws FFmpegFrameRecorder.Exception {
        recorder = new FFmpegFrameRecorder(bos, grabber.getImageWidth(), grabber.getImageHeight(),
                grabber.getAudioChannels());
        setRecorderParams(recorder);
        recorder.start();
    }

    /**
     * 设置录制器参数
     *
     * @param fFmpegFrameRecorder
     */
    private void setRecorderParams(FFmpegFrameRecorder fFmpegFrameRecorder) {
        fFmpegFrameRecorder.setFormat("flv");
        // 转码
        fFmpegFrameRecorder.setInterleaved(false);
        fFmpegFrameRecorder.setVideoOption("tune", "zerolatency");
        fFmpegFrameRecorder.setVideoOption("preset", "ultrafast");
        fFmpegFrameRecorder.setVideoOption("crf", "23");
        fFmpegFrameRecorder.setVideoOption("threads", "1");
        fFmpegFrameRecorder.setFrameRate(25);// 设置帧率
        fFmpegFrameRecorder.setGopSize(25);// 设置gop,与帧率相同
        //recorder.setVideoBitrate(500 * 1000);// 码率500kb/s
        fFmpegFrameRecorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        fFmpegFrameRecorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
        fFmpegFrameRecorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
        fFmpegFrameRecorder.setOption("keyint_min", "25");  //gop最小间隔
        fFmpegFrameRecorder.setTrellis(1);
        fFmpegFrameRecorder.setMaxDelay(0);// 设置延迟
    }

    /**
     * 获取flv格式header数据
     *
     * @return
     * @throws FFmpegFrameRecorder.Exception
     */
    public byte[] getFlvHeader() throws FFmpegFrameRecorder.Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        FFmpegFrameRecorder fFmpegFrameRecorder = new FFmpegFrameRecorder(byteArrayOutputStream, grabber.getImageWidth(), grabber.getImageHeight(),
                grabber.getAudioChannels());
        setRecorderParams(fFmpegFrameRecorder);
        fFmpegFrameRecorder.start();
        return byteArrayOutputStream.toByteArray();
    }


    /**
     * 将视频源转换为flv
     */
    protected void transferToFlv() {
        //创建拉流器
        try {
            createGrabber(currentDevice.getRtmpUrl());
            //创建录制器
            createTransterOrRecodeRecorder();

            grabber.flush();
            running = true;
            // 时间戳计算
            long startTime = 0;
            long lastTime = System.currentTimeMillis();
            while (running) {
                // 转码
                Frame frame = grabber.grab();
                if (frame != null && frame.image != null) {
                    lastTime = System.currentTimeMillis();
                    recorder.setTimestamp((1000 * (System.currentTimeMillis() - startTime)));
                    recorder.record(frame);
                    if (bos.size() > 0) {
                        byte[] b = bos.toByteArray();
                        bos.reset();
                        sendFrameData(b);
                        continue;
                    }
                }
                //10秒内读不到视频帧,则关闭连接
                if ((System.currentTimeMillis() / 1000 - lastTime / 1000) > 10) {
                    System.out.println(currentDevice.getDeviceId() + ":10秒内读不到视频帧");
                    break;
                }
            }
        } catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                recorder.close();
                grabber.close();
                bos.close();
                closeMedia();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }
    }

    /**
     * 发送帧数据
     *
     * @param data
     */
    private void sendFrameData(byte[] data) {
        mediaChannel.sendData(data);
    }


    /**
     * 关闭流媒体
     */
    private void closeMedia() {
        running = false;
        MediaServer.deviceContext.remove(currentDevice.getDeviceId());
        mediaChannel.closeChannel();
    }

    /**
     * 通知关闭推流
     *
     * @param device
     */
    @Subscribe
    public void checkChannel(Device device) {
        if (device.getDeviceId().equals(currentDevice.getDeviceId())) {
            closeMedia();
            System.out.println("关闭推流完成");
        }
    }


    @Override
    public void run() {
        transferToFlv();
    }

}

演示

前端就简单用flv.js进行演示,首次进行设备1和设备2播放,都需要进行解码推流,当设备1建立一个新channel(第三个视频画面)进行播放时,只需拿前面的第一个channel数据即可,无需进行再次进行解码。

image.png
可以看出,第三个视频播放的时候,跟第一个视频画面进度是同步的。

结束

附上代码地址: https://gitee.com/zhouxiaoben/keep-learning.git
这次分享就到这,大家有什么好的优化建议可以放在评论区。

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

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

相关文章

(2021|CoRR,AugCLIP,优化)FuseDream:通过改进的 CLIP+GAN 空间优化实现免训练文本到图像生成

FuseDream: Training-Free Text-to-Image Generation with Improved CLIPGAN Space Optimization 公众&#xff1a;EDPJ&#xff08;添加 VX&#xff1a;CV_EDPJ 或直接进 Q 交流群&#xff1a;922230617 获取资料&#xff09; 目录 0. 摘要 1. 简介 2. CLIPGAN 文本到图…

如何使用kali来进行一次ddos攻击

本文章用于记录自己的学习路线&#xff0c;不用于其他任何途径! ! ! 哈喽啊&#xff01;又是好久不见&#xff0c;本博主在之前发过一个ddos攻击的介绍。 emm…虽然那篇文章也提到了ddos攻击的方式&#xff0c;但太过于简陋&#xff0c;好像也没有什么用&#xff0c;so&#…

金蝶云星空权限项表结构

文章目录 金蝶云星空权限项表结构BOS平台【权限项】MSSQL脚本使用场景优点减少手工一个个创建的人工成本&#xff0c;还容易出错保留内码&#xff0c;可以在代码层级使用&#xff0c;方便 金蝶云星空权限项表结构 BOS平台【权限项】 MSSQL脚本 --权限项主表 SELECT * FROM db…

快速学习 webpack

目录 1. webpack基本概念 webpack能做什么&#xff1f; 2. webpack的使用步骤 2.1_webpack 更新打包 3. webpack的配置 3.1_打包流程图 3.2_案例-webpack隔行变色 3.3_插件-自动生成html文件 3.4_加载器 - 处理css文件问题 3.5_加载器 - 处理css文件 3.6_加载器 - 处…

大数据----基于sogou.500w.utf8数据的MapReduce编程

目录 一、前言二、准备数据三、编程实现3.1、统计出搜索过包含有“仙剑奇侠传”内容的UID及搜索关键字记录3.2、统计rank<3并且order>2的所有UID及数量3.3、上午7-9点之间&#xff0c;搜索过“赶集网”的用户UID3.4、通过Rank&#xff1a;点击排名 对数据进行排序 四、参…

jQuery: 整理4---创建元素和添加元素

1.创建元素&#xff1a;$("内容") const p "<p>这是一个p标签</p>" console.log(p)console.log($(p)) 2. 添加元素 2.1 前追加子元素 1. 指定元素.prepend(内容) -> 在指定元素的内部的最前面追加内容&#xff0c;内容可以是字符串、…

代码随想录算法训练营 | day60 单调栈 84.柱状图中最大的矩形

刷题 84.柱状图中最大的矩形 题目链接 | 文章讲解 | 视频讲解 题目&#xff1a;给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 1 < heights.len…

动态规划系列 | 最长上升子序列模型(上)

文章目录 最长上升子序列回顾题目描述问题分析程序代码复杂度分析 怪盗基德的滑翔翼题目描述输入格式输出格式 问题分析程序代码复杂度分析 登山题目描述输入格式输出格式 问题分析程序代码复杂度分析 合唱队形题目描述输入格式输出格式 问题分析程序代码复杂度分析 友好城市题…

基于Java SSM框架实现医院挂号上班打卡系统项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架实现医院挂号上班打卡系统演示 摘要 在网络发展的时代&#xff0c;国家对人们的健康越来越重视&#xff0c;医院的医疗设备更加先进&#xff0c;医生的医术、服务水平也不断在提高&#xff0c;给用户带来了很大的选择余地&#xff0c;而且人们越来越追求更个…

Linux与Bash 编程——Linux文件处理命令-L1

目录&#xff1a; linux系统与shell环境准备 Linux系统简介操作系统简史Linux的发行版&#xff1a;Linux与Windows比较&#xff1a;Linux安装安装包下载Linux的访问方式远程登录方式远程登录软件&#xff1a;mobaxterm的使用&#xff1a;使用电脑命令行连接&#xff1a;sshd的…

系列十四、SpringBoot + JVM参数配置实战调优

一、SpringBoot JVM参数配置实战调优 1.1、概述 前面的系列文章大篇幅的讲述了JVM的内存结构以及各种参数&#xff0c;今天就使用SpringBoot项目实战演示一下&#xff0c;如何进行JVM参数调优&#xff0c;如果没有阅读过前面系列文章的朋友&#xff0c;建议先阅读后再看本篇文…

python库win32gui,windows的API管理及自动化

使用了python实现了打开windows的鼠标属性页面并更改鼠标的主键的功能&#xff0c;今天主要是针对使用的库进行一个讲解&#xff0c;也即是win32gui库的详细讲解。 对于windows的打开的窗口中&#xff0c;可以通过窗口的类型和名字来进行窗口的具体查找&#xff0c;使用的win3…

Topaz Video AI 视频修复工具(内附安装压缩包win+Mac)

目录 一、Topaz Video AI 简介 二、Topaz Video AI 安装下载 三、Topaz Video AI 使用 最近玩上了pika1.0和runway的图片转视频&#xff0c;发现生成出来的视频都是有点糊的&#xff0c;然后就找到这款AI修复视频工具 Topaz Video AI。 一、Topaz Video AI 简介 Topaz Video…

外贸多语言电商系统的运作流程

外贸多语言电商系统的运作流程通常包括以下几个步骤&#xff1a; 1. 网站搭建和设计&#xff1a;首先需要搭建一个多语言电商网站&#xff0c;可以选择现有的电商平台或自行开发。网站设计应考虑不同语言和文化背景的用户需求&#xff0c;包括界面布局、导航结构、语言切换等。…

在线简历制作!这3个简历模板网站超好用

马上就要到一年一度的金九银十&#xff0c;找工作的季节啦。如何制作一份优质的简历&#xff0c;是每位找工作人都想探询的问题&#xff0c;如何才能让自己的简历脱颖而出&#xff0c;选择一个优质的简历制作网站十分重要&#xff0c;下面就来推荐3款超好用的在线简历制作网站&…

4.7 【共享源】流的生产者(二)

七,模式 流的模式决定了Screen如何使前台缓冲区可用。生产者通过调用screen_set_stream_property_iv()并设置SCREEN_PROPERTY_MODE属性来设置模式。有效模式如下: 7.1 SCREEN_STREAM_MODE_DEFAULT 如果生产者应用程序没有在流上明确设置 SCREEN_PROPERTY_MODE 属性,则 Sc…

贝叶斯球快速检验条件独立

贝叶斯球 定义几个术语&#xff0c;描述贝叶斯球在一个结点上的动作&#xff1a; 通过&#xff08;pass through&#xff09;&#xff1a;从当前结点的父结点方向过来的球&#xff0c;可以访问当前结点的任意子结点&#xff08;父->子&#xff09;。从当前节点的子结点方向…

微前端——无界wujie

B站课程视频 课程视频 课程课件笔记&#xff1a; 1.微前端 2.无界 现有的微前端框架&#xff1a;iframe、qiankun、Micro-app&#xff08;京东&#xff09;、EMP&#xff08;百度&#xff09;、无届 前置 初始化 新建一个文件夹 1.通过npm i typescript -g安装ts 2.然后可…

【教学类-42-02】20231224 X-Y 之间加法题判断题2.0(按2:8比例抽取正确题和错误题)

作品展示&#xff1a; 0-5&#xff1a; 21题&#xff0c;正确21题&#xff0c;错误21题42题 。小于44格子&#xff0c;都写上&#xff0c;哪怕输入2:8&#xff0c;实际也是5:5 0-10 66题&#xff0c;正确66题&#xff0c;错误66题132题 大于44格子&#xff0c;正确66题抽取44*…

python pip安装依赖的常用软件源

目录 引言 一、什么是镜像源&#xff1f;​​​​​​​ 二、清华源 三、阿里源 四、中科大源 五、豆瓣源 六、更多资源 引言 在软件开发和使用过程中&#xff0c;我们经常需要下载和更新各种软件包和库文件。然而&#xff0c;由于网络环境的限制或者服务器的负载&#…