Vue3+Node中使用webrtc推流至mediamtx
前言
项目的 Web 端是 Vue3 框架,后端是 GO 框架。需要实现将客户端的本地摄像头媒体流推送至服务端,而我自己从未有媒体流相关经验,最初 leader 让我尝试通过 RTSP 协议推拉流,我的思路就局限在了 RTSP 方向。
最初使用的服务端流媒体处理服务器是RTSPToWeb
GitHub - deepch/RTSPtoWeb:RTSP 流到 WebBrowser
RTSPtoWeb 可以将RTSP 流转换为可在 Web 浏览器中使用的格式,如 MSE(媒体源扩展)、WebRTC 或 HLS。
我打算在 Web 端将本地摄像头数据流以RTSP协议发送至服务端,通过RTSPtoWeb处理为Web可以使用的格式。客户端的推流软件我选择FFmpeg,我找到了可以在Vue中使用FFmpeg的方法:
FFmpeg——在Vue项目中使用FFmpeg(安装、配置、使用、SharedArrayBuffer、跨域隔离、避坑…)_vue ffmpeg-CSDN博客
在浏览器中我们是无法直接使用 FFmpeg 软件的,但好在有个东西叫FFmpeg.wasm,它可以让 FFmpeg 的功能在浏览器中使用。我们在 Vue 项目中使用 FFmpeg.wasm来代替手动输入命令行操作的 FFmpeg 软件。FFmpeg.wasm 是 FFmpeg 的纯 WebAssembly 接口,可以在浏览器内录制音频和视频,并进行转换和流式传输。但后面实际操作我发现,现在FFmpeg.wasm在0.12.0版本之后不再支持 NodeJS
FAQ | ffmpeg.wasm (ffmpegwasm.netlify.app)
但使用 FFmpeg.wasm 旧版本时我遇到好多报错。。。我第一次写前端能力属实不足,最后选择放弃了这条思路。。。有能力或者使用的不是 NodeJS 的小伙伴可以用 FFmpeg.wasm 在 Web 推流,很方便好用。
后面我又有一个歪点子,用 GO 编写从命令行端操作 FFmpeg 推拉流 API ,再打包为 exe 可执行文件,运行在客户端。但在小组开会后,这个方案被毙了。。。因为没有考虑客户需求,首先客户在 PC 端访问我们的 Web ,不仅需要下载 FFmpeg ,现在还得多下载一个 exe 文件;其次是考虑客户要在移动端使用。第一次实习,第一次做客户项目,考虑的没有很全面。
后面我发现为什么不直接用WebRTC呢?这可是专门用来解决Web媒体流的好东西!!!
于是我更改了方案,将mediamtx作为新的服务器,
GitHub - bluenviron/mediamtx: Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
mediamtx支持多种协议,可以解决很多需求,强推!!!
WebRTC简介
搞懂WebRTC ,看这一篇就够了-CSDN博客
WebRTC提供了基础的前端功能实现,仅仅通过JavaScript,Web端即可实现点对点的视频流、音频流或者其他数据的传输,所用到的知识点如下:
WHIP /WHEP 协议
WTN普及(一)WHIP/WHEP标准信令 - 知乎 (zhihu.com)
WebRTC(WebRTC-HTTP Ingestion Protocol)通过 WHIP 协议将音视频流从客户端传输到服务器。WHEP(WebRTC-HTTPEgressProtocol)允许基于浏览器的流媒体内容的低延迟观看。
WHIP /WHEP 不仅仅可以用作流媒体的传输。在未建立 WebRTC 之前,通讯双方需要商议彼此的媒体协议,也可能无法访问彼此的 IP,故我们需要信令服务器传递双方的 SDP 和 candidates 信息,而俩个协议在 WebRTC 之上增加了一个简单的信令层解决了这个问题,我们可以通过 WebStock 或者 http 向信令层发送信息。
SDP 协议
WebRTC通话原理(SDP、STUN、 TURN、 信令服务器)_webrtc stun服务器-CSDN博客
通信双方需要发送媒体流,而视频和音频都涉及到编码格式,故双方需要先协商统一编码格式,保证媒体流顺利发送。
SDP(Session Description Protocol)是一种用于描述多媒体会话的格式。它包含了会话的媒体类型、格式、传输协议和网络信息等。SDP 在 WebRTC 中用于协商音视频通话的各种参数,确保两个端点可以兼容并顺利进行通信。
NAT 穿透
NAT(Network Address Translation,网络地址交换)主要解决 IPv4 地址不够用和安全问题。通过多台主机共用一个公网 IP 地址来减缓 IPv4 地址不够用的问题。使用 NAT 后,主机隐藏在内网,这样黑客很难访问到内网主机,从而达到保护内网主机的目的。NAT 其实就是一种地址映射技术,它在内网地址与外网地址之间建立了映射关系。
通讯双方不在一个局域网内,则无法访问直接彼此的 IP,故需要 NAT 将双方的内网 IP 转换为 公网 IP,以便于双方可以互相访问。为实现穿透,我们需要用到 ICE(Interactive Connectivity Establishment,交互式连接创建)建立双方的网络连接。
ICE
WebRTC技术文档 – 5.ICE(笔记)_webrtc ice-CSDN博客
ICE 是一种基于 offer/answer 模式解决 NAT 穿越的协议集合。它结合STUN和TURN协议,使客户端无需考虑网络位置和NAT类型即可动态发现最优传输路径。
实现的具体过程为:收集网络信息 Candidate、交换 Candidate、按优先级尝试连接。Candidate指可连接的候选者。每个候选者是包含address(IP地址)、port(端口号)、protocol(传输协议)、CandidateType(Candidate类型)、ufrag(用户名)等内容的信息集。WebRTC将Candidate分为host、srflx、prflx和relay四类,优先级依次由高到低。
STUN / TRUN
WebRTC学习之路—TURN/STUN服务原理及搭建_webrtc 客户端建立连接 stun-CSDN博客
ICE 使用 STUN Binding Request 和 Response,来获取公网映射地址和进行连通性检查。客户端向 STUN 服务器发送请求,STUN 服务器返回其看到的客户端的公共地址和端口。这样,客户就可以告诉其他对等方(Peer)自己的公共地址,以便建立直接连接。
ICE 使用 TURN 协议作为 STUN 的辅助,在点对点穿越失败的情况下,借助于 TURN 服务的转发功能,来实现互通。客户端首先尝试使用 STUN 获取公共地址。如果双方无法通过公共地址直接连接,客户端可以将媒体发送到 TURN 服务器,由 TURN 服务器转发到对等方。这种方式虽然增加了延迟,但可以保证连接的建立。
WebRTC 流程图
WebRTC的建立如下图:
在下面代码中,Web端(client)与远端(mediamtx 服务器)通过HTTP 请求进行交互实现信令。
具体实现
安装运行mediamtx
mediamtx 我们只需要直接下载独立二进制文件运行即可。
下载地址:Releases · bluenviron/mediamtx (github.com)
windows 系统下载圈出来的即可,解压后里面有一个 exe 文件,打开即可
通过WebRTC发送媒体流的示例网址
注意:以下项目和mediamtx 都运行在一个 PC 上
mediamtx提供了一个发送媒体流的示例网址的源代码:
mediamtx/internal/servers/webrtc/publish_index.html at main · bluenviron/mediamtx · GitHub
URL:localhost:8889/1/publish
其中1代表的是路径,也是后面查询媒体流和保存媒体流的路径,示例页面如下:
我们看到video device为OBS,数据流的默认选项是OBS虚拟摄像头,当有外部设备接入,如USB摄像头,会默认选择为 USB 摄像头设备。video device 还可选择 screen ,即本地屏幕推流。
其他的选项依次是视频的编码、波特率、帧率、分辨率和音频的设备、编码、波特率、优化
我接入设备后,选项都是默认的 publish 画面如下:
mediamtx 的info信息为:
WebRTC 创建新的 session
对等连接(peer connection)成功建立;本地(Web)候选地址和远端(mediamtx)候选地址
[path 1] 代表录制的路径,这里会录制是因为我在mediamtx.yaml 文件中配置了录制,其他配置还要保存路径、格式、最大录制时间、录制片段时间和自动删除时间
正在录制音视频轨道,Opus 格式的音频轨道AV1 格式的视频轨道。
Vue3中实现WebRTC发送媒体流
根据示例网址的源代码,我们可以修改 WebRTC 代码格式如下:
HTML 元素:
<template><div><video ref="videoElement" autoplay playsinline></video></div>
</template>
导包和定义的参数:
import { ref } from 'vue';// 其中1为路径
// whip 用于身份验证
const webrtcUrl = http://localhost:8889/1/whip; // 1代表路径,可改为你自己的路径
const retryPause = 2000;
const videoElement = ref<HTMLVideoElement | null>(null);let pc: any = null;
let stream: any = null;
let restartTimeout: number | null = null;
let sessionUrl = '';
let offerData: OfferDescription;
let queuedCandidates: RTCIceCandidate[] = [];interface OfferDescription {iceUfrag: string; // 唯一标识 sdp 的短字符串icePwd: string; // sdp 对应密码medias: any[]; // 媒体描述,编码率等信息
}
主函数:
const onPublish = () => {postMessage('connecting');const videoId = videoForm.device;const audioId = audioForm.device;let videoOpts: { deviceId: string } | boolean = false;let audioOpts = {deviceId: '',autoGainControl: true, //自动增益控制echoCancellation: true, //启用回声消除noiseSuppression: true, //噪音抑制};if (videoId !== 'screen') {if (videoId !== 'none') {videoOpts = {deviceId: videoId,};}if (audioId !== 'none') {audioOpts.deviceId = audioId;const voice = audioForm.voice;if (!voice) {// 如果没有声音选择,则关闭声音audioOpts.autoGainControl = false;audioOpts.echoCancellation = false;audioOpts.noiseSuppression = false;}}navigator.mediaDevices.getUserMedia({video: videoOpts,audio: audioOpts,}).then((str) => {stream = str;if (videoElement.value) {//将得到的媒体流赋予videoElement,显示在 HTML 元素中videoElement.value.srcObject = stream; }requestICEServers();}).catch((err) => {onError(err.toString(), false);});} else {navigator.mediaDevices.getDisplayMedia({video: {width: { ideal: parseInt(videoForm.width) },height: { ideal: parseInt(videoForm.height) },frameRate: { ideal: parseInt(videoForm.framerate) },},audio: true,}).then((str) => {stream = str;if (videoElement.value) {videoElement.value.srcObject = stream;}requestICEServers();}).catch((err) => {onError(err.toString(), false);});}
};
Web 端获取STUN 服务器,收集本地网络信息(candidate),通过 ICE 服务器获取 Web 端的公网ip,并添加至 candidate
const requestICEServers = () => {//请求 STUN 服务器fetch(webrtcUrl.value, {method: 'OPTIONS',}).then((res) => {// 通过返回值中的头获取 STUN 服务器信息// STUN 服务器信息在yaml文件中设置// 我在mediamtx.yaml设置 STUN 为 url: stun:stun.l.google.com:19302pc = new RTCPeerConnection({iceServers: linkToIceServers(res.headers.get('Link')),});pc.onicecandidate = (evt: RTCPeerConnectionIceEvent) =>onLocalCandidate(evt);pc.oniceconnectionstatechange = () => onConnectionState();stream.getTracks().forEach((track: any) => {pc.addTrack(track, stream);});createOffer();}).catch((err) => {onError(err.toString(), true);});
};const linkToIceServers = (links: any): any => {if (links === null) return []; // 检查 `links` 是否为 nullreturn links.split(', ').map((link: any) => {const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if (!m) return null; // 如果没有匹配,返回 nullconst ret = {urls: [m[1]],} as {urls: any[];username?: string;credential?: string;credentialType?: string;};if (m[3] !== undefined) {ret.username = unquoteCredential(m[3]);ret.credential = unquoteCredential(m[4]);ret.credentialType = 'password';}return ret; // 始终返回 ret}).filter(Boolean); // 筛选掉 null 值
};// 带有引号的凭证字符串解析为 JSON 格式
const unquoteCredential = (v: string) => JSON.parse(`"${v}"`);// 监听并收集本地的网络信息 candidate
const onLocalCandidate = (evt: any) => {if (restartTimeout !== null) {return;}// 检测到新的 candidateif (evt.candidate !== null) {// 代表尚未建立连接if (sessionUrl === '') {// 将 candidate 加入队列queuedCandidates.push(evt.candidate);} else {sendLocalCandidates([evt.candidate]);}}
};// 发送 SDP 主要信息和网络信息 candidate 完成WebRTC 建立
const sendLocalCandidates = async (candidates: any) => {await fetch(sessionUrl, {method: 'PATCH',headers: {'Content-Type': 'application/trickle-ice-sdpfrag','If-Match': '*',},body: generateSdpFragment(offerData, candidates),}).then((res) => {if (res.status !== 204) {throw new Error(`bad status code ${res.status}`);}}).catch((err) => {onError(err.toString(), true);});
};// 使用 SDP 主要信息和网络信息 candidate生成片段
const generateSdpFragment = (od: any, candidates: any) => {const candidatesByMedia: any = {};for (const candidate of candidates) {const mid = candidate.sdpMLineIndex;if (candidatesByMedia[mid] === undefined) {candidatesByMedia[mid] = [];}candidatesByMedia[mid].push(candidate);}let frag ='a=ice-ufrag:' + od.iceUfrag + '
' + 'a=ice-pwd:' + od.icePwd + '
';let mid = 0;for (const media of od.medias) {if (candidatesByMedia[mid] !== undefined) {frag += 'm=' + media + '
' + 'a=mid:' + mid + '
';for (const candidate of candidatesByMedia[mid]) {frag += 'a=' + candidate.candidate + '
';}}mid++;}return frag;
};
Web 端和远端(mediamtx)交换 SDP
// 创建 SDP ,描述本端浏览器支持哪些能力
const createOffer = () => {pc.createOffer().then((offer: any) => {offerData = parseOffer(offer.sdp);if (pc) {// offer 设置为本地描述pc.setLocalDescription(offer).then(() => {sendOffer(offer.sdp);}).catch((err: any) => {onError(err.toString());});}}).catch((err: any) => {onError(err.toString());});
};// 解析 SDP ,得到 SDP 中的主要信息
const parseOffer = (offer: any) => {const ret: OfferDescription = {iceUfrag: '',icePwd: '',medias: [],};for (const line of offer.split('
')) {if (line.startsWith('m=')) {ret.medias.push(line.slice('m='.length));} else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {ret.iceUfrag = line.slice('a=ice-ufrag:'.length);} else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) {ret.icePwd = line.slice('a=ice-pwd:'.length);}}return ret;
};// 发送 SDP 到远端(mediamtx)
const sendOffer = async (offer: any) => {console.log('sendOffer', offer);offer = editOffer(offer);await fetch(webrtcUrl.value + `?video-device=${videoForm.device}`,{method: 'POST',headers: {'Content-Type': 'application/sdp',},body: offer,}).then((res) => {switch (res.status) {case 201:break;case 400:return res.json().then((e) => {throw new Error(e.error);});default:throw new Error(`bad status code ${res.status}`);}const locationHeader = res.headers.get('location');if (!locationHeader) {throw new Error('Location header is missing');}sessionUrl = new URL(locationHeader, 'http://localhost:8889').toString();return res.text().then((answer) => onRemoteAnswer(answer));}).catch((err) => {onError(err.toString(), true);});
};const editOffer = (sdp: any) => {console.log('editOffer', sdp);const sections = sdp.split('m=');console.log('sections', sections);for (let i = 0; i < sections.length; i++) {if (sections[i].startsWith('video')) {// 设置 SDP 中 vedio 的编码率 sections[i] = setCodec(sections[i], videoForm.codec);} else if (sections[i].startsWith('audio')) {// 设置 SDP 中 audio 的编码率和波特率sections[i] = setAudioBitrate(setCodec(sections[i], audioForm.codec),audioForm.bitrate,audioForm.voice);}}return sections.join('m=');
};// // 接受远端 SDP信息的 Answer
const onRemoteAnswer = (sdp: string) => {if (restartTimeout !== null) {return;}sdp = editAnswer(sdp);// 保存远端 SDP信息的 Answerpc.setRemoteDescription(new RTCSessionDescription({type: 'answer',sdp,})).then(() => {if (queuedCandidates.length !== 0) {sendLocalCandidates(queuedCandidates);queuedCandidates = [];}}).catch((err: any) => {onError(err.toString());});
};const editAnswer = (sdp: any) => {const sections = sdp.split('m=');for (let i = 0; i < sections.length; i++) {if (sections[i].startsWith('video')) {sections[i] = setVideoBitrate(sections[i], videoForm.bitrate);}}return sections.join('m=');
};
设置 vedio 和 audio 编码格式
// 设置 video 波特率
const setVideoBitrate = (section: any, bitrate: any) => {let lines = section.split('
');for (let i = 0; i < lines.length; i++) {if (lines[i].startsWith('c=')) {lines = [...lines.slice(0, i + 1),'b=TIAS:' + (parseInt(bitrate) * 1024).toString(),...lines.slice(i + 1),];break;}}return lines.join('
');
};//设置编码格式
const setCodec = (section: any, codec: any) => {const lines = section.split('
');const lines2 = [];const payloadFormats = [];for (const line of lines) {if (!line.startsWith('a=rtpmap:')) {lines2.push(line);} else {if (line.toLowerCase().includes(codec)) {payloadFormats.push(line.slice('a=rtpmap:'.length).split(' ')[0]);lines2.push(line);}}}const lines3 = [];let firstLine = true;for (const line of lines2) {if (firstLine) {firstLine = false;lines3.push(line.split(' ').slice(0, 3).concat(payloadFormats).join(' '));} else if (line.startsWith('a=fmtp:')) {if (payloadFormats.includes(line.slice('a=fmtp:'.length).split(' ')[0])) {lines3.push(line);}} else if (line.startsWith('a=rtcp-fb:')) {if (payloadFormats.includes(line.slice('a=rtcp-fb:'.length).split(' ')[0])) {lines3.push(line);}} else {lines3.push(line);}}return lines3.join('
');
};const setAudioBitrate = (section: string, bitrate: string, voice: any) => {let opusPayloadFormat = '';let lines = section.split('
');for (let i = 0; i < lines.length; i++) {if (lines[i].startsWith('a=rtpmap:') &&lines[i].toLowerCase().includes('opus/')) {opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0];break;}}if (opusPayloadFormat === '') {return section;}for (let i = 0; i < lines.length; i++) {if (lines[i].startsWith('a=fmtp:' + opusPayloadFormat + ' ')) {if (voice) {lines[i] ='a=fmtp:' +opusPayloadFormat +' minptime=10;useinbandfec=1;maxaveragebitrate=' +(parseInt(bitrate) * 1024).toString();} else {lines[i] ='a=fmtp:' +opusPayloadFormat +' maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate=' +(parseInt(bitrate) * 1024).toString();}}}return lines.join('
');
};
错误处理函数:
const onError = (err: string, retry?: boolean) => {if (!retry) {console.error('err:', err);} else {if (restartTimeout === null) {console.error(err + ', retrying in some seconds');if (pc !== null) {pc.close();pc = null;}restartTimeout = window.setTimeout(() => {restartTimeout = null;startTransmit();}, retryPause);if (sessionUrl) {fetch(sessionUrl, {method: 'DELETE',});}sessionUrl = '';// 清空 STUN 服务器候选队列queuedCandidates = [];}}
};
注意
关于 vedio 设置
const videoForm = {device: '', // 设备ID:none,screen(屏幕),空值默认为外部设备,若没有则为OBS虚拟设备codec: 'h264/90000', // 编解码器格式有bitrate: '10000', // 比特率framerate: '30', // 帧率width: '1920',height: '1080',
};
例如其中 codec 的设置为h264/90000,其中90000是时钟频率,用于时间戳的单位,它表示每秒钟可以产生90000个时间单位,用于确保视频流和音频流的同步。若设置为 h264 ,则会导致发送的 SDP 中缺少编码协议,导致 WebRTC 建立失败。
搜集到的网络信息candidates
host候选:
candidate:1799829579 1 udp 2122260223 10.102.24.113 51222 typ host generation 0 ufrag 1Phf network-id 1
10.102.24.113 是我电脑内WSL虚拟网络适配器的IP
a=candidate:66318701 1 udp 2122194687 192.168.64.1 51223 typ host generation 0 ufrag 1Phf network-id 2
192.168.64.1 电脑以太网适配器的地址
这些是主机候选,表示的是客户端本地网络中的IP地址(如10.102.24.113
和192.168.64.1
)。这些地址通常是私有IP地址,无法被公网直接访问。
添加STUN/TRUN
srflx候选:
a=candidate:2861133569 1 udp 1686052607 221.xx.xx.xxx 51222 typ srflx raddr 10.102.24.113 rport 51222 generation 0 ufrag 1Phf network-id 1
这个候选是通过STUN服务器获取的反射候选(srflx),显示外部的可路由地址(即公网IP),在这个例子中为221.xx.xx.xxx。这意味着 STUN 服务器成功返回了一个公网 IP 地址。
结果
当 mediamtx 反馈下面 info,即代表 WebRTC 连接和传输媒体流成功
这样媒体流就可以保存在 mediamtx 服务器上了。服务器上查询、转发媒体流等方法均可以在手册中获取。
GitHub - bluenviron/mediamtx: Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
菜鸟第一次写文章,对自己项目中用到的模块,通过查阅和学习完成自己的见解,如果可以帮助到你,请帮忙点点赞。可能有用词不当和错误的地方,请大家斧正,感谢阅读!!!
相关文章:

Vue3+Node中使用webrtc推流至mediamtx
前言 项目的 Web 端是 Vue3 框架,后端是 GO 框架。需要实现将客户端的本地摄像头媒体流推送至服务端,而我自己从未有媒体流相关经验,最初 leader 让我尝试通过 RTSP 协议推拉流,我的思路就局限在了 RTSP 方向。 最初使用的服务端…...

React 内置的Hook学习
useState:管理组件状态 useState 是一个用于在函数组件中添加状态的 Hook。它允许你在函数组件中声明一个状态变量,并提供一个更新该状态的方法,其中与组件生命周期的关系: 初始化:当组件首次渲染时,useS…...

Flutter Navigator2.0的原理和Web端实践
01 背景与动机 在Navigator 2.0推出之前,Flutter主要通过Navigator 1.0和其提供的 API(如push(), pop(), pushNamed()等)来管理页面路由。然而,Navigator 1.0存在一些局限性,如难以实现复杂的页面操作(如移…...

初次使用uniapp编译到微信小程序编辑器页面空白,真机预览有内容
uniapp微信小程序页面结构 首页页面代码 微信小程序模拟器 模拟器页面为空白时查了下,有几个说是“Hbuilder编译的时候应该编译出来一个app.js文件 但是却编译出了App.js”,但是我的小程序结构没问题,并且真机预览没有问题 真机调试 根据defi…...

【HF设计模式】03-装饰者模式
声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。 摘要 《Head First设计模式》第3章笔记:结合示例应用和代码,介绍装饰者模式,包括遇到的问题、遵循的 OO 原则、达到的效果。 …...
【人工智能-中级】模型部署与优化:从本地实验到云端与边缘部署
模型部署与优化:从本地实验到云端与边缘部署 在机器学习和深度学习模型训练完成后,如何高效、稳定地将模型部署到生产环境中,是实际应用中的关键环节。模型部署不仅涉及技术实现,还需要考虑性能优化、资源管理和安全性等多方面因素。本文将全面探讨模型部署与优化的相关内…...
Jenkins 编写Pipeline 简介及使用初识详解
一、Jenkins Pipeline简介 Jenkins Pipeline是Jenkins的一个重要功能,Jenkins 2.0 以上才会有,一系列 Jenkins 插件将整个持续集成用解释性代码 Jenkinsfile 来描述,它允许开发者以代码的方式定义整个持续集成和交付(CI/CD)流程,包括构建、测试、部署和监控等步骤。Jenk…...

uboot移植网络驱动过程,无法ping通mx6ull和ubuntu问题解决方案
开发板:mx6ull-ALPHA_V2.4 ubuntu版本:20.04 1.现在虚拟机设置中添加网路适配器用于开启桥接模式 2.在编辑中打开“虚拟网络编辑器” 我的电脑本身只有VMnet1和VMnet8,需要底下“添加网络”,增加这个VMnet0 ,并且进行…...

精准预测美国失业率和贫困率,谷歌人口动态基础模型PDFM已开源,可增强现有地理空间模型
疾病、经济危机、失业、灾害……人类世界长期以来被各种各样的问题「侵扰」,了解人口动态对于解决这类复杂的社会问题至关重要。 政府相关人员可以通过人口动态数据来模拟疾病的传播,预测房价和失业率,甚至预测经济危机。然而,在过…...
C#速成(文件读、写操作)
导包 using System.IO;1、写入文件(重要) StreamWriter sw new StreamWriter("C:\Users\29674\Desktop\volumn.txt");//创建一个TXT的文件 sw.WriteLine(textBox2.Text);//写入文件的内容 sw.Close();//关闭2、读取文件(不重要&…...

SQL server学习03-创建和管理数据表
目录 一,SQL server的数据类型 1,基本数据类型 2,自定义数据类型 二,使用T-SQL创建表 1,数据完整性的分类 2,约束的类型 3,创建表时创建约束 4,任务 5,由任务编写…...

【UE5 “RuntimeLoadFbx”插件】运行时加载FBX模型
前言 为了解决在Runtime时能够直接根据FBX模型路径直接加载FBX的问题,推荐一款名为“RuntimeLoadFBX”的插件。 用法 插件用法如下,只需要指定fbx的地址就可以在场景中生成Actor模型 通过指定输入参数“Cal Collision”来设置FBX模型的碰撞 还可以通过…...

【潜意识Java】深入理解 Java 面向对象编程(OOP)
目录 什么是面向对象编程(OOP)? 1. 封装(Encapsulation) Java 中的封装 2. 继承(Inheritance) Java 中的继承 3. 多态(Polymorphism) Java 中的多态 4. 抽象&…...
windows同时使用多个网卡
windows同时链接了有线网络,多个无线网卡,默认会使用有线网络,如果想要局域网内使用某个特定的网络,可以设置静态ip 1. 首先删除原来的静态网络(不冲突可以不删除),我这里usb无线网卡切换过usb插口,这里需要删除原来的. 使用 route print 查看接口列表及静态路由信息 route p…...

Spark执行计划解析后是如何触发执行的?
在前一篇Spark SQL 执行计划解析源码分析中,笔者分析了Spark SQL 执行计划的解析,很多文章甚至Spark相关的书籍在讲完执行计划解析之后就开始进入讲解Stage切分和调度Task执行,每个概念之间没有强烈的关联,因此这中间总感觉少了点…...
B4X编程语言:B4X控件方法汇总
1、AddNode、AddView方法 AddNode(Node As javafx.scence.Node,Left As Double,Top As Double,Width As Double,Height As Double) B4J控件 AddView(View As javafx.scence.Node,Left As Double,Top As Double,Width As Double,Height As Double) B4J的B4XView …...

基于XML配置Bean和基于XML自动装配
目录 基于XML配置Bean id分配规则 通过id获取bean 通过类型获取bean 通过C命名空间配置bean 使用C命名空间 通过P命名空间配置bean 通过util:list进行配置bean 指定id,直接ref引用过来 通过外部属性文件配置Bean Bean信息重用(继承)…...
全排列 dfs
给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列。 我们假设对于小写字母有 a<b<…<y<z ,而且给定的字符串中的字母已经按照从小到大的顺序排列。 输入格式 输入只有一行,是一个由不同的小写字母组成的字符串…...
linux内存相关命令的尝试
文章目录 前言freeMem 部分的解释Swap 部分的解释 vmstatProcs (进程)Memory (内存)Swap (交换)IO (磁盘 I/O)System (系统)CPU (处理器) pidstat标题行解释数据列解释 sar字段含义解释示例分析 总结 前言 菜就多练,昨天看了一篇有关剖析 RocksDB 内存超限问题的文…...

Vue2 基础
Vue 2 是 Vue.js 的第二个主要版本,于 2016 年发布。它是一个渐进式的 JavaScript 框架,以其简单、灵活、易用性高而广受欢迎。Vue 2 主要专注于构建用户界面(UI),并且非常适合用于构建单页应用(SPA&#x…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...

R语言速释制剂QBD解决方案之三
本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...

云原生安全实战:API网关Kong的鉴权与限流详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关(API Gateway) API网关是微服务架构中的核心组件,负责统一管理所有API的流量入口。它像一座…...

【C++进阶篇】智能指针
C内存管理终极指南:智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...