WebRTC技术简介及应用场景

news2025/4/8 4:48:08

写在前面

本文是参考稀土掘金的文章,整理得出,版权归原作者所有!参考链接请点击跳转

WebRTC(Web Real-Time Communication) 是一项开源技术,允许浏览器和移动应用直接进行实时音视频通信数据传输,无需安装插件或第三方软件。它由 Google 发起,现已成为 W3C 和 IETF 的标准。

核心特点:

  1. 点对点(P2P)连接

    • 设备间直接通信,降低延迟,提升效率。

    • 但需通过 ICE/STUN/TURN 服务器解决 NAT 穿越问题。

  2. 无需插件

    • 原生支持主流浏览器(Chrome、Firefox、Safari 等)。

  3. 关键组件

    • MediaStream(getUserMedia):获取摄像头/麦克风数据。

    • RTCPeerConnection:建立音视频传输连接。

    • RTCDataChannel:支持任意数据(如文件、游戏指令)传输。

  4. 安全加密

    • 强制使用 SRTP(音视频加密)和 DTLS(数据加密)。

  5. 适应网络变化

    • 自动调整码率、抗丢包,适应不同网络条件。

常见应用场景:

  • 视频会议(如 Google Meet、Zoom 的网页版)

  • 在线教育、远程医疗

  • 文件共享、屏幕共享

  • 物联网设备控制

摄像头和麦克风属于用户的隐私设备,WebRTC既然成为了浏览器中音视频即时通信的W3C标准,因此必然会提供API,让有一定代码开发能力的人去调用;

注意敲黑板: 使用这些API是有前提条件的哦,首先在安全源访问,调用API才没有任何阻碍的。那什么是安全源呢?看下面思维导图(更详细的看:chrome官方文档),且记住这句话:安全源 是至少匹配以下( Scheme 、 Host 、 Port )模式之一的源

举个简单的例子:你本地开发用HTTP请求地址获取摄像头API没有问题,但是你的同事用他的电脑访问你电脑IP对应的项目地址时,摄像头调用失败,为什么呢?

因为在他的浏览器中,你的项目访问地址非HTTPS,在非HTTPS的情况下,如果IP不是localhost127.0.0.1,都不属于安全源

当然事非绝对,在特定情况下必须使用非HTTPS访问也是可以的,Chrome提供了对应的取消限制但是不太建议用(安全为上),因此我在这里就不再多余阐述。

所以经常有人问,为什么我的代码在自己浏览器中可以获取到摄像头,但是在区域网下别的电脑的浏览器中获取不到?同样的浏览器、同样的操作系统,为什么获取不到呢?原因就是上面的安全源限制。

getUserMedia  ★ 重要

以前的版本中我们经常使用 navigator.getUserMedia 来获取计算机的摄像头或者麦克风,但是现在这个接口废弃,变更为 navigator.mediaDevices.getUserMedia,因此后面我们均使用新的API来完成代码编写。

getUserMedia可以干什么?  ★ 重要

意如其名,那就是获取用户层面的媒体,当你的计算机通过 USB 或者其他网络形式接入了 N 多个摄像头或虚拟设备时,都是可以通过这个 API 获取到的。 当然不仅仅是视频设备,还包括音频设备和虚拟音频设备。 获取媒体设备是最简单的操作,它还可以控制获取到媒体的分辨率,以及其他的以一些可选项。

PS:在很多云会议中,我们开会只能选择一个摄像头,这并不是只能使用一个摄像头,而是厂商针对“大多数场景中只会用到一个摄像头”而设计的;但在有些业务中,我们可能需要自己设备上的N 个摄像头(带USB摄像头)同时使用,那么如何办到呢(这个场景其实蛮多的,后面留个课后题)。因此熟知这个 API 对于解决基本的会议和其他复杂场景问题很有用。

如何使用 getUserMedia  ★ 重要

有简单的用法,有复杂的用法。一般简易场景下,大多数 API 用默认参数就可以实现对应功能,getUserMedia也一样,直接调用不使用任何参数,则获取的就是 PC 的默认摄像头和麦克风。

但是,当我们遇到复杂一点的应用场景,比如你的电脑上自带麦克风,同时你连接了蓝牙耳机和有线耳机,那么在视频通话过程中,你如何主动选择使用哪个呢?也就是说, 在用摄像头或者麦克风之前,我们先要解决如何从 N 个摄像头或者麦克风中选择我们想要的。

要解决这个问题,我们必须先有个大体的思路(当然这个思路并不是凭空想象出来的,而是在一定的技术储备下才有的。如果你开始前没有任何思路也没关系,可以参考他人的经验),如下:

  1. 获取当前设备所有的摄像头和麦克风信息;

  1. 从所有的设备信息中遍历筛选出我们想要使用的设备;

  1. 将我们想要使用的设备以某种参数的形式传递给浏览器 API

  1. 浏览器API去执行获取的任务。

上面提到的设备以某种参数的形式传递给 API,那么这个设备必然是以参数存在的,因此这里有几个概念需要提前知道,如下:

设备分成了图中的三个大类型,每个类型都有固定的字段,比如 ID、kind、label ,而其中用于区分它们的就是kind字段中的固定值最核心的字段就是 ID,后面我们经常用的就是这个 ID。

那么,在前端如何使用 JavaScript获取到这些信息?

大家先看下面这段代码,大体上过一遍,并留意 initInnerLocalDevice函数内部执行顺序。

function handleError(error) {
    alert("摄像头无法正常使用,请检查是否占用或缺失")
    console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);
}
/**
 * @author suke
 * device list init 
 */
function initInnerLocalDevice(){
        const that  = this
        var localDevice = {
            audioIn:[],
            videoIn: [],
            audioOut: []

        }
        let constraints = {video:true, audio: true}
        if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
            console.log("浏览器不支持获取媒体设备");
            return;
        }
        navigator.mediaDevices.getUserMedia(constraints)
            .then(function(stream) {
                stream.getTracks().forEach(trick => {
                    trick.stop()
                })
                // List cameras and microphones.
                navigator.mediaDevices.enumerateDevices()
                    .then(function(devices) {
                        devices.forEach(function(device) {
                            let obj = {id:device.deviceId, kind:device.kind, label:device.label}
                            if(device.kind === 'audioinput'){
                                if(localDevice.audioIn.filter(e=>e.id === device.deviceId).length === 0){
                                    localDevice.audioIn.push(obj)
                                }
                            }if(device.kind === 'audiooutput'){
                                if(localDevice.audioOut.filter(e=>e.id === device.deviceId).length === 0){
                                    localDevice.audioOut.push(obj)
                                }
                            }else if(device.kind === 'videoinput' ){
                                if(localDevice.videoIn.filter(e=>e.id === device.deviceId).length === 0){
                                    localDevice.videoIn.push(obj)
                                }
                            }
                        });
                    })
                    .catch(handleError);

            })
            .catch(handleError);
    }

这个代码片段的主要作用就是获取用户设备上所有的摄像头和麦克风信息,起关键作用的是enumerateDevices函数,但是在调用这个关键函数之前,getUserMedia函数出现在了这里,它的出现是用户在访问服务时直接调用用户摄像头,此时如果用户授权且同意使用设备摄像头、麦克风,那么enumerateDevices函数就能获取设备信息了,在这里getUserMedia函数可以理解为获取摄像头或者麦克风权限集合的探路函数

看下图,我将我电脑上使用enumerateDevices函数加载到的信息,根据前面提到的字段kind,将其分三类并打印到控制台。

千万不要小看现在获取到的这些信息哦,在后面视频通话或会议过程中,我们需要抉择摄像头用前置还是后置,麦克风是用蓝牙还是有线,都是离不开这些信息的。

在拿到所有的摄像头麦克风信息之后,我们需选出最终要参与视频通话的那个信息体,看上图中 VideoIn数组里面label:"eseSoft Vcam"  这个摄像头就是我想要参会的摄像头,那么我怎样指定让代码去选择这个摄像头呢?这里就涉及到了getUserMedia的约束参数constraints 。

判断摄像头是前置还是后置 ★ 常用
获取设备列表:使用 navigator.mediaDevices.enumerateDevices() 方法获取设备列表。
检查设备类型:在设备列表中,查找 kind 为 videoinput 的设备。
判断前置或后置:通常,前置摄像头的 label 包含“front”或“前置”等字样,
而后置摄像头的 label 可能包含“back”或“后置”等字样。
具体内容可能因设备和浏览器而异,因此需要根据实际情况进行判断。

判断音频设备是麦克风还是扬声器 ★ 常用
获取设备列表:同样使用 navigator.mediaDevices.enumerateDevices() 方法。
检查设备类型:查找 kind 为 audioinput(麦克风)和 audiooutput(扬声器)的设备。
判断设备类型:通常,audioinput 设备会被标识为麦克风,而 audiooutput 设备会被标识为扬声器。
耳机是归类为音频输出设备(扬声器)kind字段

媒体约束 constraints ★ 基础

在具体讲解约束参数 constraints 之前,大家先看下面这段示例代码。

let constraints = {video:true, audio: true} 

    function handleError(error) {
        console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);
    }

    /**
     * 获取设备 stream
     * @param constraints
     * @returns {Promise<MediaStream>}
     */
    async function getLocalUserMedia(constraints){
        return await navigator.mediaDevices.getUserMedia(constraints)
    }

    let stream = await this.getLocalUserMedia(constraints).catch(handleError);
console.log(stream)

上面的代码片段为JavaScript获取计算机摄像头和麦克风的媒体流(视频和音频流我们统称为媒体流)的一种方式,大多数情况下都是这么用的,如果电脑有摄像头、麦克风,这样获取没有任何问题,但就担心你用的时候,你的电脑上没有配摄像头或麦克风,或者有多个摄像头而你想指定其中某一个。 为了兼容更多情况,我们需要知道constraints这个参数的详细用法。

接下来我们看下这个参数在几种常见场景下的具体配置,以及为什么这样配置。

1.同时获取视频和音频输入

使用下面约束, 如果遇到计算机没有摄像头的话,你调用上述代码的过程中就会报错,因此我们在调用之前可以通过enumerateDevices返回结果主动判断有无视频输入源,没有的话,可以动态将这个参数中的 video设置为false

{ audio: true, video: true }

2.获取指定分辨率

在会议宽带足够且流媒体传输合理的情况下,无需考虑服务端压力,而需考虑客户端用户摄像头的分辨率范围,通常我们会设置一个分辨率区间。

下面展示的①约束是请求一个 1920×1080 分辨率的视频,但是还提到 min 参数,将 320×240 作为最小分辨率,因为并不是所有的网络摄像头都可以支持 1920×1080 。当请求包含一个 ideal(应用最理想的)值时,这个值有着更高的权重,意味着浏览器会先尝试找到最接近指定理想值的设定或者摄像头(如果设备拥有不止一个摄像头)。

但是,在多人会议简单架构场景中,在不改变会议稳定性的情况下,为了让更多的客户端加入,我们通常会把高分辨率主动降低到低分辨率,约束特定摄像头获取指定分辨率如下面②配置。


    --------------------①:1--------------------------
    {
        audio: true,
        video: {
            width: { min: 320, ideal: 1280, max: 1920 },
            height: { min: 240, ideal: 720, max: 1080 }
        }
    }
    --------------------②:2--------------------------
    {
    audio: true,
    video: { width: 720, height: 480}
}

3.指定视频轨道约束:获取移动设备的前置或者后置摄像头

facingMode属性。可接受的值有:user(前置摄像头)、environment(后置摄像头);需要注意的是,这个属性在移动端可用,当我们的会议项目通过 h5 在移动端打开时,我们可以动态设置这个属性从而达到切换前后摄像头的场景。

{ audio: true, video: { facingMode: "user" } }
{ audio: true, video: { facingMode: { exact: "environment" } } }

 4.定帧速率frameRate

帧速率(你可以理解为FPS)不仅对视频质量,还对带宽有着影响,所以在我们通话过程中,如果判定网络状况不好,那么可以限制帧速率。

我们都知道,视频是通过一定速率的连续多张图像形成的,比如每秒 24 张图片才会形成一个基础流畅的视频,因此帧速率对于实时通话的质量也有影响,你可以想象成和你的游戏的FPS一个道理。

const constraints = {
    audio: true,
    video: {
        width:1920,
        height:1080,
        frameRate: { ideal: 10, max: 15 }
    }
};

实际上,通过FPS我们可以引申出来一些场合,在特定场合选择特定的FPS搭配前面的分辨率配置,以提高我们会议系统的质量,比如:

  • 屏幕分享过程中,我们应当很重视高分辨率而不是帧速率,稍微卡点也没关系;
  • 在普通会议过程中,我们应当重视的是画面的流畅,即帧速率而不是高分辨率;
  • 在开会人数多但宽带又受限的情况下,我们重视的同样是会议的流程性,同样低分辨率更适合宽带受限的多人会议;
  • ……

5.使用特定的网络摄像头或者麦克风

重点哦,我们最前面enumerateDevices函数获取到的设备集合可以派上用场了。

/**
 * 获取指定媒体设备id对应的媒体流
 * @author suke
 * @param videoId
 * @param audioId
 * @returns {Promise<void>}
 */
async function getTargetIdStream(videoId,audioId){
    const constraints = {
        audio: {deviceId: audioId ? {exact: audioId} : undefined},
        video: {
            deviceId: videoId ? {exact: videoId} : undefined,
            width:1920,
            height:1080,
            frameRate: { ideal: 10, max: 15 }
        }
    };
    if (window.stream) {
        window.stream.getTracks().forEach(track => {
            track.stop();
        });
    }
    //被调用方法前面有,此处不再重复
    let stream = await this.getLocalUserMedia(constraints).catch(handleError);

}

getDisplayMedia ★ 重要

我们日常开会,多数需要通过会议 App 来分享自己的屏幕,或者仅分享桌面上固定的应用程序那么在浏览器中实现视频通话,能否实现分享屏幕呢?答案是肯定的, W3C的 Screen Capture 标准中有说明,就是使用getDisplayMedia

const constraints = {
  audio: true,
  video: true
};

const promise = navigator.mediaDevices.getDisplayMedia(constraints);

console.info('--promise--', promise);

navigator.mediaDevices.getDisplayMedia(constraints)
  .then((stream) => {
     /* use the stream */
    console.info('--use the stream--', stream);
  })
  .catch((err) => {
    /* handle the error */
    console.error('--handle the erro--', err);
  });

参数 Constraints

同上一个函数一样,同样需要配置constraints约束,当然这个也是可选的, 如果选择传参的话,那么参数设置如下:

getDisplayMedia({
  audio: true,
  video: true
})

但是这里的constraints配置和前面getUserMedia的约束配置是有差别的。又一个重点来了,在屏幕分享的约束中,video 是不能设置为false 的,但是可以设置指定的分辨率,如下:

getDisplayMedia({
  audio: true,
  video: {width:1920,height:1080}
})
  1. audiotrue

  2. audiofalse

 请留意上面两图的对比,当去掉音频后,第二张图少了个勾选系统音频的 radio 框

完整案例

/**
 * 获取屏幕分享的媒体流
 * @author suke
 * @returns {Promise<void>}
 */
async function getShareMedia(){
    const constraints = {
        video:{width:1920,height:1080},
        audio:false
    };
    if (window.stream) {
        window.stream.getTracks().forEach(track => {
            track.stop();
        });
    }
    return await navigator.mediaDevices.getDisplayMedia(constraints).catch(handleError);
}

小提示

  • 在前面的案例代码中,我们在获取系统的音频或者视频的stream之前,一般会调用以下代码,目的是清除当前标签页中没有销毁的媒体流。   
       if (window.stream) {
               window.stream.getTracks().forEach(track => {
                   track.stop();
               });
           }
    

    如果不销毁,你可以看到在标签页旁边一直有个小红圈闪烁,鼠标按上去提示正在使用当前设备的摄像头,因此在后面的开发中保持好习惯:结束自己会议后或页面用完摄像头后,一般除了强制刷新,也可以调用上面代码清除正在使用的stream调用。

    好了,这节课我们我们掌握了两个最重要的 API,下节课我们开始搭建一个信令服务器,同时完成 P2P (单人对单人)的视频通话(跑代码的时候一定要记得前面提到的安全源哦)。

检测函数

githup上检测webRtc链接:Select audio and video sources

静默基础检测

function isSupportWebRtcFlag() {
    // 获取用户代理字符串,用于检测浏览器类型
    const userAgent = navigator.userAgent,
        isIphone = userAgent.indexOf('iPhone') > -1,
        isUcBrowser = userAgent.indexOf('UCBrowser') > -1,
        isIphoneUC = isIphone && isUcBrowser;
    let canIUseDataChannel = true,
        canIUseRTCPeer = true,
        canIUseGetUserMedia = false,
        canIUseRealTime = false;

    // 检测是否支持 getUserMedia(获取设备列表)
    if (navigator.mediaDevices
        && navigator.mediaDevices.getUserMedia
        || navigator.getUserMedia
        || navigator.mozGetUserMedia
        || navigator.mozGetUserMedia) {
        canIUseGetUserMedia = true;
    }

    // 检测是否支持 RTCPeerConnection (数据通道)
    canIUseRTCPeer = Boolean(window.RTCPeerConnection)
        || Boolean(window.webkitRTCPeerConnection)
        || Boolean(window.mozRTCPeerConnection)
        || Boolean(window.msRTCPeerConnection)
        || Boolean(window.oRTCPeerConnection);

    try {
        const o = new (window.RTCPeerConnection || window.msRTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection)(null);

        // eslint-disable-next-line no-restricted-syntax
        canIUseDataChannel = 'createDataChannel' in o;
    } catch (e) {
        console.error('尝试创建 RTCPeerConnection 对象,以检测是否支持数据通道错误,error:', e);
        canIUseDataChannel = false;
    }

    // 综合判断是否支持所有 WebRTC 功能
    canIUseRealTime = canIUseGetUserMedia && canIUseRTCPeer && canIUseDataChannel && !isIphoneUC;

    if (!canIUseGetUserMedia) {
        console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持getUserMedia');
    }
    if (!canIUseRTCPeer) {
        console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持RTCPeerConnection');
    }

    if (!canIUseDataChannel) {
        console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持createDataChannel');
    }

    if (canIUseRealTime) {
        console.info('webRtcUtils[isSupportWebRtcFlag] --> 支持炫彩api');
    } else {
        console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持炫彩api');
    }

    return {
        canIUseGetUserMedia,
        canIUseRTCPeer,
        canIUseDataChannel,
        canIUseRealTime
    };
}

isSupportWebRtcFlag();

静默黑名单检测

function isSupportWebRtcSilently() {
    const ua = navigator.userAgent;
    const isMobile = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i).test(ua);

    // 1. 检测关键 API 是否存在
    const hasGetUserMedia = Boolean(navigator.mediaDevices?.getUserMedia
        || navigator.getUserMedia
        || navigator.webkitGetUserMedia
        || navigator.mozGetUserMedia);

    const hasRTCPeerConnection = Boolean(window.RTCPeerConnection
        || window.webkitRTCPeerConnection
        || window.mozRTCPeerConnection);

    // 2. 检测 DataChannel 支持
    let hasDataChannel = false;

    if (hasRTCPeerConnection) {
        try {
            const pc = new (window.RTCPeerConnection || window.webkitRTCPeerConnection)({iceServers: []});

            // eslint-disable-next-line no-restricted-syntax
            hasDataChannel = 'createDataChannel' in pc;
            pc.close();
        } catch (e) {
            console.error('检测 DataChannel 支持', e);
            hasDataChannel = false;
        }
    }

    // 3. 排除已知有问题的浏览器或场景
    const isBlockedBrowser

        // 排除 UC 浏览器、QQ 浏览器、MIUI 浏览器等
        = (/UCBrowser|QQBrowser|MiuiBrowser|Quark|baiduboxapp/i).test(ua)

        // iOS 第三方浏览器(如 Firefox Focus)可能限制 WebRTC
        || isMobile && (/Firefox/i).test(ua) && !(/FxiOS/).test(ua);

    // 4. 综合判断
    const isSupported
        = hasGetUserMedia
        && hasRTCPeerConnection
        && hasDataChannel
        && !isBlockedBrowser;
		
	const result = {
        isSupported,
        details: {
            hasGetUserMedia,
            hasRTCPeerConnection,
            hasDataChannel,
            isBlockedBrowser
        }
    };	
		console.info('--result--', result);

    return result;
}
isSupportWebRtcSilently();

精准检测 (需用户授权)

async function preciseWebRTCSupportCheck() {
  const result = {
    supportsWebRTC: false,
    details: {
      hasRTCPeerConnection: false,
      hasDataChannel: false,
      hasGetUserMedia: false,
      hasIceSupport: false,
      hasCodecSupport: { video: [], audio: [] },
      errors: []
    }
  };

  try {
    // 1. 检测 RTCPeerConnection 和 DataChannel
    const RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
    if (!RTCPeerConnection) {
      result.details.errors.push('RTCPeerConnection API missing');
      return result;
    }
    result.details.hasRTCPeerConnection = true;

    const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
    result.details.hasDataChannel = 'createDataChannel' in pc;

    // 2. 检测 ICE 支持(网络穿透)
    let hasIce = false;
    pc.onicecandidate = (e) => {
      if (e.candidate && e.candidate.candidate) {
        hasIce = true;
        result.details.hasIceSupport = true;
      }
    };

    // 3. 检测编解码器支持(H.264/VP8/Opus)
    const sender = pc.addTransceiver('video');
    const capabilities = sender.sender.getCapabilities();
    result.details.hasCodecSupport.video = capabilities.codecs.filter(c => c.mimeType.includes('video'));
    result.details.hasCodecSupport.audio = capabilities.codecs.filter(c => c.mimeType.includes('audio'));

    // 4. 实际创建 Offer 以触发 ICE 收集
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);

    // 等待 ICE 收集完成(最多 2 秒)
    await new Promise(resolve => setTimeout(resolve, 2000));
    pc.close();

    // 5. 检测 getUserMedia(需用户授权)
    if (navigator.mediaDevices?.getUserMedia) {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
        stream.getTracks().forEach(track => track.stop());
        result.details.hasGetUserMedia = true;
      } catch (e) {
        result.details.errors.push(`getUserMedia failed: ${e.name}`);
      }
    }

    // 综合判定
    result.supportsWebRTC = (
      result.details.hasRTCPeerConnection &&
      result.details.hasDataChannel &&
      result.details.hasIceSupport &&
      result.details.hasGetUserMedia &&
      result.details.hasCodecSupport.video.length > 0
    );

  } catch (e) {
    result.details.errors.push(`Critical error: ${e.message}`);
  }

  return result;
}

 

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

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

相关文章

介绍几种创意登录页(含完整源码)

今天为大家收集了几种不同风格的登录页&#xff0c;搭配动态渐变背景&#xff0c;效果绝对惊艳&#xff01; CSS3实现动态渐变玻璃拟态登录页 一、开篇语 纯CSS实现当下最火的玻璃拟态(Morphism)风格登录页&#xff0c;搭配动态渐变背景&#xff0c;效果绝对惊艳&#xff01; …

Uni-app入门到精通:uni-app的基础组件

1、view view是容器组件&#xff0c;类似于HTML中的<div></div>标签&#xff0c;用于包裹各种元素内容&#xff0c;是页面布局常用的组件。view组件的属性如下 属性类型默认值说明hover-classStringnone指定按下去的样式类。当hover-class"none"时&…

大文件上传源码,支持单个大文件与多个大文件

大文件上传源码&#xff0c;支持单个大文件与多个大文件 Ⅰ 思路Ⅱ 具体代码前端--单个大文件前端--多个大文件前端接口后端 Ⅰ 思路 具体思路请参考我之前的文章&#xff0c;这里分享的是上传流程与源码 https://blog.csdn.net/sugerfle/article/details/130829022 Ⅱ 具体代码…

C语言--插入排序

插入排序&#xff1a;简单而高效的排序算法 在计算机科学中&#xff0c;排序是一种常见的操作&#xff0c;用于将一组数据按照特定的顺序排列。插入排序&#xff08;Insertion Sort&#xff09;是一种简单直观的排序算法&#xff0c;它的工作原理类似于我们整理扑克牌的过程。…

L2-024 部落 #GPLT,并查集 C++

文章目录 题目解读输入格式输出格式 思路Ac Code参考 题目解读 我们认为朋友的朋友都算在一个部落里&#xff0c;于是要请你统计一下&#xff0c;在一个给定社区中&#xff0c;到底有多少个互不相交的部落&#xff1f;并且检查任意两个人是否属于同一个部落。 输入格式 第一…

在线记事本——支持Markdown

项目地址 https://github.com/Anyuersuper/CloudNotebook 百度网盘 通过网盘分享的文件&#xff1a;CloudNotebook-master.zip 链接: https://pan.baidu.com/s/1_Y--aBzNkKiFRIMHYmwPdA?pwdyuer 提取码: yuer &#x1f4dd; 云笔记 (Cloud Notebook) 云笔记是一个简洁、安全…

Day2:前端项目uniapp壁纸实战

先来做一个轮番图。 效果如下&#xff1a; common-style.css view,swiper,swiper-item{box-sizing: border-box; } index.vue <template><view class"homeLayout"><view class"banner"><swiper circular indicator-dots autoplay…

LeetCode Hot100 刷题笔记(3)—— 链表

目录 前言 1. 相交链表 2. 反转链表 3. 回文链表 4. 环形链表 5. 环形链表 II 6. 合并两个有序链表 7. 两数相加 8. 删除链表的倒数第 N 个结点 9. 两两交换链表中的节点 10. K 个一组翻转链表 11. 随机链表的复制 12. 排序链表 13. 合并 K 个升序链表 14. LRU 缓存 前言 一、…

状态机思想编程

1. LED流水灯的FPGA代码 在这个任务中&#xff0c;首先我们会使用状态机的思想来设计一个LED流水灯的控制逻辑。LED流水灯一般需要依次点亮不同的LED&#xff0c;并且循环播放。我们将其分为几个状态&#xff0c;每个状态控制一个或一组LED灯。 状态机设计 假设我们有8个LED…

第二十八章:Python可视化图表扩展-和弦图、旭日图、六边形箱图、桑基图和主题流图

一、引言 在数据可视化领域&#xff0c;除了常见的折线图、柱状图和散点图&#xff0c;还有一些高级图表类型可以帮助我们更直观地展示复杂数据关系。本文将介绍五种扩展图表&#xff1a;和弦图、旭日图、六边形箱图、桑基图和主题流图。这些图表在展示数据关系、层次结构和流量…

基于vue框架的重庆美食网站的设计与实现kt945(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,美食分类,美食菜品 开题报告内容 基于Vue框架的重庆美食网站的设计与实现开题报告 一、选题背景与意义 &#xff08;一&#xff09;选题背景 重庆&#xff0c;作为中国西南地区的璀璨明珠&#xff0c;以其独特的地理位置和丰富…

Metal学习笔记十三:阴影

在本章中&#xff0c;您将了解阴影。阴影表示表面上没有光。当另一个表面或对象使对象与光线相遮挡时&#xff0c;您会看到对象上的阴影。在项目中添加阴影可使您的场景看起来更逼真&#xff0c;并提供深度感。 阴影贴图 阴影贴图是包含场景阴影信息的纹理。当光线照射到物体…

时间梯度匹配损失 TGMLoss

目录 时间梯度匹配损失(Temporal Gradient Matching Loss, TGM Loss) 完整示例,该损失函数常用于视频预测、运动平滑等任务,通过约束预测序列的时间梯度与真实序列一致来提升时序连续性 训练测试demo代码: 时间梯度匹配损失(Temporal Gradient Matching Loss, TGM Los…

iPhone XR:一代神机,止步于此

什么样的 iPhone &#xff0c;才配称为一代神机&#xff1f; 我曾经用过iPhone 4S、iPhone 6S Plus、iPhone 8 Plus&#xff0c;iPhone SE2、iPhone XR、iPhone 13、iPhone 14 Plus、iPhone 15/Pro。 不管硬件再怎么卷&#xff0c;不管囊中是否羞涩&#xff0c;主力机基本没考…

第十四届蓝桥杯省赛真题解析(含C++详细源码)

第十四届蓝桥杯省赛 整数删除满分思路及代码solution1 &#xff08;40% 双指针暴力枚举&#xff09;solution 2&#xff08;优先队列模拟链表 AC&#xff09; 冶炼金属满分代码及思路 子串简写满分思路及代码solution 1&#xff08;60% 双指针&#xff09;solution 2&#xff0…

OpenAI即将开源!DeepSeek“逼宫”下,AI争夺战将走向何方?

OpenAI 终于要 Open 了。 北京时间 4 月 1 日凌晨&#xff0c;OpenAI 正式宣布&#xff1a;将在未来几个月内开源一款具备推理能力的语言模型&#xff0c;并开放训练权重参数。这是自 2019 年 GPT-2 部分开源以来&#xff0c;OpenAI 首次向公众开放核心模型技术。 【图片来源于…

mysql 8.0.27-docker

安装 可以略过本步 https://dev.mysql.com/downloads/https://dev.mysql.com/downloads/ 镜像查询与安装 先查询&#xff1a; docker search mysql 明显会报错 Error response from daemon: Get "https://index.docker.io/v1/search?qmysql&n25": dial tcp…

使用NVM下载Node.js管理多版本

提示&#xff1a;我解决这个bug跟别人思路可能不太一样&#xff0c;因为我是之前好用&#xff0c;换个项目就不好使了&#xff0c;倦了 文章目录 前言项目场景一项目场景二解决方案&#xff1a;下载 nvm安装 nvm重新下载所需Node 版本nvm常用命令 项目结构说明 前言 提示&…

Linux——文件(2)文件系统

我们知道&#xff0c;文件在没有被打开时是放在磁盘中的&#xff0c;通常我们未打开的文件数量要远远大于打开的文件数量&#xff0c;而且我们要想打开一个文件&#xff0c;首先需要知道文件在磁盘的位置才能打开&#xff0c;但问题是&#xff0c;面对磁盘中成百上千个文件&…

蓝桥杯 web 水果拼盘 (css3)

做题步骤&#xff1a; 看结构&#xff1a;html 、css 、f12 分析: f12 查看元素&#xff0c;你会发现水果的高度刚好和拼盘的高度一样&#xff0c;每一种水果的盘子刚好把页面填满了&#xff0c;所以咱们就只要让元素竖着排列&#xff0c;加上是竖着&#xff0c;排不下的换行…