在Web中使用jsmpeg.js低时延播放RTSP视频流(海康、大华)方案 - vue-jsmpeg-player
前言
- web播放实时视频流的几种方案对比,详见此处
- 本组件发布在git开源gitee、github,欢迎各位一起优化和fork,拉取后可直接运行demo,同时发布到了npm中,可直接安装使用vue-jsmpeg-player
- jsmpeg相关地址:gitee、github、 官网地址
- jsmpeg为MIT开源协议,不用考虑版权问题
- 跨平台windows、linux都可用,只跟浏览器有关系,对浏览器可能有点小要求(版本越新越好),IE浏览器实测也是可以用的
- 本文仅在局域网验证,如果是公网或跨网项目,需要自行解决摄像机拉流问题(可参考GB28181协议,设备主动注册服务器)
- 仅支持mpeg1格式视频、mp2格式音频!!!仅支持mpeg1格式视频、mp2格式音频!!!仅支持mpeg1格式视频、mp2格式音频!!!不要随便拿个websocket流去给jsmpeg使用,播放不了的!!!也无法直接播放rtmp流!!!
1 安装与介绍
1.1 安装
- npm 安装(这样无法二次开发)
npm install jsmpeg -s
- 二次开发方案(本文章介绍)
把jsmpeg.min.js拖入项目目录下,建议将此js文件拆分为多个模块,如下图,当然你能在jsmpeg.min.js单文件上二次开发也是可以的
注:不能直接使用src文件夹下的代码,那是没经过编译的,jsmpeg.js有一部分使用的c语言,若要直接使用请自行学习如何编译jsmpeg,本文暂不介绍(详见build.sh)
1.2 介绍
- jsmpeg比较重要的两个技术点:webgl、wasm,只需大致了解即可,若不对这两个模块进行二次开发,则无需深究
- jsmpeg.js采用软解码方式,仅支持mpeg1格式视频、mp2格式音频!!!,将视频流解码成图片并渲染到canvas上,并且可在源码基础上二次开发
1.2.1 jsmpeg方案架构
单链路即:rtsp流=>ffmpeg转码(mpeg1+mp2)=>http server接收=>websocket server转发=>websocket client
1.2.2 jsmpeg的c语言部分
- wasm技术简单来说就是:可以在Web 中运行除了JavaScript以外的语言编写的代码(例如C,C ++,Rust或其他)
- jsmpeg采用了这项技术来实现其解码器(mpeg1+mp2)
- 不过我猜测作者为防止在一些低版本浏览器上wasm技术无法使用,又用js写了一套解码器。
首先贴上仓库中c语言的代码部分,如下图:
在根目录下有build.sh,用于编译出jsmpeg.min.js
在输出文件中,可以找到一段很长的字符串变量(WASM_BINARY_INLINED),这就是经过编译后的二进制c语言模块(经过base64加密压缩的)
这段内容在Player构造函数中被使用到,如下图:
至此,大致介绍完毕,相信看官们已经基本清楚jsmpeg的原理了
2 播放与使用
在前面的介绍中已经说到,本方案需要使用到一个websocket server来中转,jsmpeg作者已经为我们提供了一个简易websocket server端,即websocket-relay.js。
为方便测试,首先需要把jsmpeg的仓库拉取到本地,因为需要用到其中jsmpeg.min.js、websocket-relay.js文件
2.1 使用介绍
2.1.1 引入与使用
- 引入
如果是npm安装方式:
import JSMpeg from 'jsmpeg'
如果是使用源码:
import JSMpeg from 'xx/jsmpeg.min.js'
- 使用
创建实例方式:
let player = new JSMpeg.Player('ws://xxxx',{ ... })
自动识别方式:
jsmpeg.js会自动识别docment中所有class包含jsmpeg的元素,并获取data-url属性后把该元素当做播放器容器,html如下
<div class="jsmpeg" data-url="ws://xxxx"></div>
2.1.2 参数与事件
点此官方介绍
- options参数:
名称 | 类型 | 说明 |
---|---|---|
canvas | HTMLCanvasElement | 用于视频渲染的HTML Canvas元素。如果没有给出,渲染器将创建自己的Canvas元素。 |
loop | boolean | 是否循环播放视频(仅静态文件),默认=true |
autoplay | boolean | 是否立即开始播放(仅限静态文件),默认=false |
audio | boolean | 是否解码音频,默认=true |
video | boolean | 是否解码视频,默认=true |
poster | string | 预览图像的URL,用来在视频播放之前作为海报显示。 |
pauseWhenHidden | boolean | 当页面处于非活动状态时是否暂停播放,默认=true(请注意,浏览器通常会在非活动选项卡中限制 JS) |
disableGl | boolean | 是否禁用WebGL,始终使用Canvas2D渲染器,默认=false |
disableWebAssembly | boolean | 是否禁用WebAssembly并始终使用JavaScript解码器,默认=false(不建议设置为true) |
preserveDrawingBuffer | boolean | WebGL上下文是否创建必要的“截图” |
progressive | boolean | 是否以块的形式加载数据(仅静态文件)。当启用时,回放可以在完整加载源之前开始 |
throttled | boolean | 当不需要回放时是否推迟加载块。默认=progressive |
chunkSize | number | 使用时,以字节为单位加载的块大小。默认(1 mb)1024*1024 |
decodeFirstFrame | boolean | 是否解码并显示视频的第一帧,一般用于设置画布大小以及使用初始帧作为"poster"图像。当使用自动播放或流媒体资源时,此参数不受影响。默认true |
maxAudioLag | number | 流媒体时,以秒为单位的最大排队音频长度。(可以理解为能接受的最大音画不同步时间) |
videoBufferSize | number | 流媒体时,视频解码缓冲区的字节大小。默认的512 * 1024 (512 kb)。对于非常高的比特率,您可能需要增加此值。 |
audioBufferSize | number | 流媒体时,音频解码缓冲区的字节大小。默认的128 * 1024 (128 kb)。对于非常高的比特率,您可能需要增加此值。 |
- 事件:
名称 | 参数 | 说明 |
---|---|---|
onVideoDecode | decoder, time | 在每个解码和渲染的视频之后调用的回调 |
onAudioDecode | decoder, time | 在每个解码音频帧后调用的回调 |
onPlay | player | 当播放开始时调用的回调函数 |
onPause | player | 当播放暂停时调用的回调函数(例如当.pause()被调用或源结束时) |
onEnded | player | 当播放到达源的末尾时调用(只在loop=false调用) |
onStalled | player | 当没有足够的数据播放时调用的回调 |
onSourceEstablished | source | 当source第一次接收到数据时调用的回调 |
onSourceCompleted | source | 当源接收到所有数据时调用的回调 |
- 方法与属性:
名称 | 参数 | 说明 |
---|---|---|
play | none | 开始播放 |
pause | none | 暂停播放 |
stop | none | 停止播放,并跳到视频开头 |
nextFrame | none | 一个视频帧的高级回放。这并不解码音频。当没有足够的数据时,返回成功 |
destroy | none | 停止回放,断开源并清理WebGL和WebAudio状态。player不能被再使用。 |
volume | number | 获取或设置音频音量(0-1) |
currentTime | number | 获取或设置当前播放位置,以秒为单位 |
paused | boolean | 只读,无论回放是否暂停 |
2.2 简易播放测试
(1)确保你有可用的rtsp流,我是有摄像机可以测试的,如果没有摄像机,可以使用ffmpeg推视频文件流,或者去网上找找免费的电视台rtsp流,也可以用一些软件推你电脑的桌面流(不过一般的软件都只能推rtmp流)
- 提供一个推桌面流的方案:EasyDarwin(rtsp服务端)+ EasyScreenLive(捕获屏幕并推rtsp流)
- Windows上通过VLC播放器搭建rtsp流媒体测试地址操作步骤
- 通过ffmpeg直接拉取本地桌面视频流[new]
(2)运行websocket server,打开cmd,cd到websocket-relay.js所在目录(注:此步骤需要安装node.js环境,请百度)- 以下步骤推荐大家在VSCode中操作
- 运行命令:node ./websocket-relay.js test 8890 8891 (第一个参数为接流url子目录,第二个参数为接流端口,第三个参数为推流端口),作者在源码中的介绍如下:
- 若成功运行后,见下图:
(注:如果报错ws模块找不到,就运行下npm install ws)
(3)启用ffmpeg,拉取指定流转码后推给http server
- 转码海康摄像机流命令代码:
ffmpeg -rtsp_transport tcp -i rtsp://[用户名]:[密码]@[ip]:554/h264/ch1/main/av_stream -q 0 -f mpegts -codec:v mpeg1video -s 1280x720 -b:v 1500k -codec:a mp2 -ar 44100 -ac 1 -b:a 128k http://127.0.0.1:8890/test
- 其他流:
ffmpeg -rtsp_transport tcp -i rtsp流地址 -q 0 -f mpegts -codec:v mpeg1video -s 1280x720 -b:v 1500k -codec:a mp2 -ar 44100 -ac 1 -b:a 128k http://127.0.0.1:8890/test
- 本地桌面流:
ffmpeg -f gdigrab -i desktop -q 0 -f mpegts -codec:v mpeg1video -s 1280x720 -b:v 1500k -codec:a mp2 -ar 44100 -ac 1 -b:a 128k http://127.0.0.1:8890/jsmpeg
运行后,ffmpeg控制台如下图:
http server控制台中可以看到一个客户端连入,此后http server会把接受到的流数据转发给所有连入ws://127.0.0.1:8891的客户端:
(4)web中使用jsmpeg.js接收流并播放
- 为了方便测试,我这写了个简易html,可直接测试,不用搭建vue、react框架测试了;
- 在存放jsmpeg.min.js目录下新建一个html文件,拷贝下面的代码到放入其中,保存后运行即可;
<!DOCTYPE html>
<html><head><title>JSMpeg TEST</title><meta name="viewport" content="width=device-width"></head><body><div class="content"><h1>JSMpeg 测试 (为了播放声音,请先点击页面一次)</div><canvas id="jsmpeg-canvas"></canvas ></div><script type="text/javascript" src="jsmpeg.min.js"></script><script type="text/javascript">document.addEventListener('click', () => {let player = new JSMpeg.Player("ws://127.0.0.1:8891",{canvas: document.getElementById('jsmpeg-canvas'),// 要在用户点击过页面后,才可以播放声音// audio: false,})}, { once: true })</script></body>
</html>
目录结构:
双击运行到浏览器中,如下图:
2.3 延迟测试
- 测试1
- 测试2
- 测试3
- 录制了个gif,更加直观
- 延迟确实保持1s左右吧,整个延迟组合大概是:ffmpeg转码耗时+http server接流耗时+ws server转发耗时+client解码耗时,因为是本机测试,网络的延迟可忽略不计
3 优化与扩展
将jsmpeg使用到项目中的话,原生功能可能还是稍微不够的,需要对其二次开发
3.1 拆分模块
为了方便二次开发,建议各位将jsmpeg.min.js拆分为多个独立模块,在vscode中打开此文件,序列化之后,可以看到都已经分类好了,接下来将各个模块独立为js文件;
拆分后如下图:
各模块说明:
- modules
- audio-output :音频输出相关
- webaudio: 缓冲器模块
- decoder:解码相关
- decoder:解码器基类
- mp2-wasm:mp2音频解码模块(wasm技术,性能更好)
- mp2:mp2音频解码模块(原生js解码,性能较差)
- mpeg1-wasm:mp2音频解码模块(wasm技术,性能更好)
- mpeg1:mp2音频解码模块(原生js解码,性能较差)
- renderer:渲染器相关
- canvas2d:canvas2d渲染模块
- webgl:webgl渲染模块(性能更好)
- source:视频流接收相关
- ajax-progressive:使用ajax方案的稳定版本?没有深入了解
- ajax:使用XMLHttpRequest获取视频流
- fetch:使用原生的fetch方法获取视频流
- websocket:websocket客户端模块,用于接收websocet server转发的视频流(常用)
- buffer: 数据缓冲器模块
- jsmpeg:jsmpeg导出模块
- player: 播放器模块
- audio-output :音频输出相关
- utils:工具方法相关
拆分后的源码见git
3.2 功能扩展
- 画面旋转
/*** 旋转画布* @param {number} angle 角度* @param {boolean} append 是否为追加角度* @returns*/rotate(angle, append = false) {if (!this.canvas || typeof angle !== 'number') returnconst canvas = this.canvasangle = append ? this.store.canvasAngle + angle : angleangle = angle >= 360 ? angle - 360 : angle <= -360 ? angle + 360 : angleif ((Math.abs(angle) / 90) % 2 === 1) {// 如果是90整数倍,表示为垂直状态const containerBound = this.contianer.getBoundingClientRect(),canvasBound = canvas.getBoundingClientRect()if (canvas.width > canvas.height) {// 宽>高,取容器高度作为canvas最大宽度canvas.style.width = containerBound.height + 'px'} else {// 宽<=高,取容器宽度作为canvas最大高度canvas.style.height = containerBound.width + 'px'}} else {canvas.style.width = nullcanvas.style.height = null}canvas.style.transform = `rotate(${angle}deg)`this.store.canvasAngle = angle}
- 截图
这里的saveToLocal与savaAs方法相似
/*** 截图* @param {string} name*/snapshot(name = 'JSMPeg') {if (this.canvas) {const mime = 'image/png',url = this.canvas.toDataURL(mime)saveToLocal(url.replace(mime, 'image/octet-stream'), `${name}_截图_${new Date().toLocaleTimeString()}.png`, mime)}}// saveToLocal方法如下
/**** @param {object} param* @param {string|object|Array} param.data 数据,传入后url参数将被忽略* @param {string} param.url 文件下载地址* @param {string} param.name 文件名称* @param {string} param.mimeType 文件mime类型* @returns*/
export function saveToLocal(blob, name = 'JSMpeg_' + Date.now(), mimeType = '') {if (!blob) returnconst a = document.createElement('a')a.style.display = 'none'a.download = nameif (typeof blob === 'string') {a.href = blob} else {blob =blob instanceof Blob? blob: new Blob(blob instanceof Array ? blob : [blob], {type: mimeType})a.href = URL.createObjectURL(blob)}setTimeout(() => {a.click()}, 0)setTimeout(() => {a.remove()}, 1)if (blob instanceof Blob) {setTimeout(() => {URL.revokeObjectURL(blob)}, 1000)}
}
- 录制
这里需要注意的是MediaRecorder是新规范,有些浏览器不支持
recorder = {/** 录制持续时间 */duration: 0,timer: null,/** @type {'canvas'|'ws'} */mode: '',running: false,saveName: '',/*** @type {MediaRecorder}* 媒体录制器*/mediaRecorder: null,/*** @type {MediaStream}* 视频流*/stream: null,/*** @type {ArrayBuffer[]}* 视频数据块*/chunks: null,startTiming() {this.duration = 0this.timer = setInterval(() => {this.duration += 1}, 1000)},pauseTiming() {clearInterval(this.timer)this.timer = null},continueTiming() {this.timer = setInterval(() => {this.duration += 1}, 1000)},stopTiming() {this.pauseTiming()this.duration = 0},clear() {this.running = falsethis.mediaRecorder = nullthis.stream = nullthis.chunks = null}}/*** 视频录制* @param {string} name* @param {'ws'|'canvas'} mode*/recording(name = 'JSMpeg', mode = 'ws') {try {if (!this.isPlaying) {return}if (this.isRecording || this.recorder.stream) {// 停止录制this.recorder.stopTiming()// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_APIif (this.recorder.mode === 'canvas' && this.recorder.mediaRecorder && this.recorder.stream instanceof MediaStream) {this.recorder.mediaRecorder.stop()saveToLocal(this.recorder.chunks, `${this.recorder.saveName}.webm`, 'video/webm;codecs=vp9')} else if (this.recorder.mode === 'ws' && this.recorder.chunks instanceof Array) {saveToLocal(this.recorder.chunks, `${this.recorder.saveName}.ts`, 'video/MP2T')this.source.recorder = undefined}this.recorder.clear()this.recorder.running = false} else {// 开始录制if (mode === 'canvas') {// 此方法兼容性较差,captureStream、MediaRecorder好像都是新规范,有些浏览器不支持let chunks = []/** @type {MediaStream} */let stream = this.canvas?.captureStream(25)let mediaRecorder = new MediaRecorder(stream, {mimeType: 'video/webm;codecs=vp9'})mediaRecorder.ondataavailable = (e) => {chunks.push(e.data)}mediaRecorder.start()this.recorder.mediaRecorder = mediaRecorderthis.recorder.chunks = chunksthis.recorder.stream = streamthis.recorder.mode = 'canvas'this.recorder.running = true// this.recording.recorder = new MediaRecorder(this.recording.stream)} else if (mode === 'ws') {// 服务端转发过来的流就是ffmpeg已转码的ts视频流数据,所以在websocket收到数据的时候,存放到数组中即可实现录制this.recorder.chunks = []this.recorder.chunks.write = function(data) {this.push(data)}this.source.recorder = this.recorder.chunksthis.recorder.mode = 'ws'this.recorder.running = true} else returnthis.recorder.saveName = `${name}_录制_${new Date().toLocaleTimeString()}`this.recorder.startTiming()}} catch (error) {console.error(error)}}
3.3 封装为vue组件
为方便项目使用,需要将jsmpeg封装为vue组件,源码就不贴在文章中了,见此处,组件介绍如下:
- 属性:
名称 | 类型 | 说明 |
---|---|---|
url | string | 视频流地址 |
title | string | 播放器标题 |
options | object | jsmpeg原生选项,直接透传 |
closeable | boolean | 是否可关闭(单击关闭按钮,仅抛出事件) |
in-background | boolean | 是否处于后台,如el-tabs的切换,路由的切换等 |
show-duration | boolean | 是否现实持续播放时间 |
default-mute | boolean | 默认静音 |
with-toolbar | boolean | 是否需要工具栏 |
- 事件:
名称 | 参数 | 说明 |
---|---|---|
包含所有jsmpeg原生事件 | 看2.1.2小节或具体看jsmpeg官方文档 | |
onSourceConnected | 无 | 当websocket连接上服务端时触发 |
onSourceStreamInterrupt | 无 | 当websocket超过一定时间没有收到流时触发 |
onSourceStreamContinue | 无 | 当onSourceStreamInterrupt触发后websocket第一次接收到流时触发 |
onSourceClosed | 无 | 当websocket关闭后触发 |
onResolutionDecode | width,height | 当获取到视频分辨率后触发 |
- 演示:
无信号时
正常播放:
旋转:
接流中断:
截图测试:
录制测试:
4 多路推流
由于后端不是我做,提供后端java思路仅供参考
我们知道websocket和http一样是支持子目录形式的url的,那么这个子目录我们可以用摄像机的主键来定义
这里建议将http server和websocket server封装到一个服务类中,这样在http server收到ffmpeg推过来的流之后可以直接调用websocet server转发给client
- 创建一个类,包含http server和websocket server ,websocket server 监听8081端口,http server 监听8080端口
- ws server将连接进来的客户端缓存到字典中,比如一个客户端连接地址为ws://127.0.0.1:8081/camera1,那么就知道这个客户端想获取camera1的视频流
- 假设一个摄像机在数据库中主键为camera1,开启一条单链路流,ffmpeg拉取rtmp流转码推给http://127.0.0.1:8080/camera1,http server的接收事件中使用websocet server转发给camera1的客户端(即ws://127.0.0.1:8081/camera1)
PS: 使用ws多路传输时,后台需要优化转发处理,否则可能出现多链路混流现象,导致斑马纹或视频流异常
后语
- 由于项目对实时性要求不是那么的高,所以本方案1s以内延时完全满足项目需求
- 由于是客户端解码,所以性能不会太好,在目前的i5 10代+8g这种配置机器上单个页面六路同屏应该是没问题的,再多就不行了,对性能有追求的只能用webrtc了,具体性能对比可见jsmpeg官网性能对比
- 本组件发布在github开源jsmpeg-player,欢迎各位一起优化和fork
- 仅支持mpeg1格式视频、mp2格式音频!!!仅支持mpeg1格式视频、mp2格式音频!!!仅支持mpeg1格式视频、mp2格式音频!!!不要随便拿个websocket流去给jsmpeg使用,播放不了的!!!也无法直接播放rtmp流!!!
- 感谢ffmpeg、jsmpeg作者
更新记录
2022-03-09: 推荐使用开源库ZLMediaKit
2022-07-27: 封装为vue组件并发布到了npm上,可直接安装使用,详见仓库,npm
来杯奶茶
如果觉得这篇文章对你有帮助的话,请我喝杯奶茶吧~ (CSDN不让贴收款码,无语)
在Web中使用jsmpeg.js低时延播放RTSP视频流(海康、大华)方案 - vue-jsmpeg-player相关推荐
- 海康大华等安防摄像机采用通用RTSP协议流转RTMP推送至Web无插件播放展示的流程方法
行业现状 中国互联网化的进程已经越来越快了,各个行业都在进行着互联网化的改造,流媒体.音视频,作为跑在互联网上最大量级的数据类型,其从编码方式到传输协议到终端兼容都成为各家标准抢占的高点,RTMP.H ...
- 海康大华天地伟业网络摄像头chrome浏览器web二次开发
海康大华天地伟业网络摄像头chrome浏览器二次开发 海康大华天地伟业网络摄像头chrome浏览器web二次开发 由于工作的原因需要开发海康和大华,还有天地伟业的摄像头,而且必须是本地部署开发,每个厂 ...
- RTSP安防网络摄像头/海康大华硬盘录像机/NVR网页无插件低延时播放流媒体服务器EasyNVR页面显示网络请求失败问题
进入移动互联网时代以来,企业微信公众号已成为除官网以外非常重要的宣传渠道,当3.2亿直播用户与9亿微信用户的势能累加,在微信上开启直播已成为越来越多企业的必然选择. 青犀团队研发的EasyNVR核心在 ...
- 安防RTSP协议摄像头实现WEB端无插件直播流媒体服务EasyNVR实现海康大华宇视摄像头网页播放的方法
背景分析:微信直播的兴起 进入移动互联网时代以来,企业微信公众号已成为除官网以外非常重要的宣传渠道,当3.2亿直播用户与9亿微信用户的势能累加,在微信上开启直播已成为越来越多企业的必然选择. Easy ...
- 安防互联网摄像头海康大华硬盘录像机视频流媒体服务器EasyNVR在layer弹出层中使用video标签无法最大化全屏播放问题解决
诞生背景 众所周知,EasyNVR可以将局域网/广域网上的海康/大华等网络摄像头由rtsp转换为rtmp.rtsp.hls.flv协议转换,并提供推流服务,可以将拉到的网络摄像头直接转发到流媒体服务器 ...
- 在谷歌Chrome上用VLC低延迟(300毫秒)播放播放海康大华RTSP
一.历史背景 在遍地都是摄像头的今天,往往需要在各种B/S信息化系统中集成视频播放功能,海康.大华.华为等大厂摄像头遵循监控行业标准,一般只支持RTSP传输协议,而Chrome.Firefox.Edg ...
- 海康大华宇视等等安防监控摄像头转成WebRTC流实现Web浏览器超低延迟无插件直播新方案
flash禁用后RTMP视频流的替代方案webrtc直播流 1.问题场景 2.WEBRTC延时对比 3.LiveNVR支持WEBRTC输出 4.RTSP/HLS/FLV/RTMP拉流Onvif流媒体服 ...
- 海康大华宇视等等安防监控摄像头转成WebRTC流实现Web浏览器超低延迟无插件直播新方案...
@ 目录 1.问题场景 2.WEBRTC延时对比 3.LiveNVR支持WEBRTC输出 4.RTSP/HLS/FLV/RTMP拉流Onvif流媒体服务 5.GB28181接入服务 1.问题场景 需要 ...
- 海康大华等网络摄像机监控视频RTSP/RTMP推流网页播放/直播无需插件低延迟解决方案研究
市面上常见监控视频推流方案简介 当前如果想要将监控视频在浏览器中播放,有几种常见的办法如下: 1.获取摄像头RTSP流,使用FFmpeg或者程序如JavaCV或者其他方式,将其推流成RTMP,通过服务 ...
- 海康大华等摄像头RTSP低延迟(1秒以内)网页无插件播放解决方案
简介 监控摄像头网页无插件播放解决方案虽然很多,但是或多或少会有一定的延迟,其中网上使用最多的是RTSP转RTMP推流的方式,这种延迟一般十秒左右,优化的好的话能达到五秒,但是依旧算不上实时.本文提供 ...
最新文章
- 输入一个链表,反转链表后,输出新链表的表头(ACM格式)(美团面试题)
- SpringMVC背景介绍及常见MVC框架比较
- lnmp1.4上thinkphp5.0出现404的解决办法
- ubuntu下载百度网盘文件油猴+aria2
- python_异常处理
- unity3d曲线text文本
- 【struts2】预定义拦截器
- 设计模式---适配器设计模式
- python论文参考文献5篇_“送”5篇SCI论文,意味着什么?
- DMS经销商管理系统解决方案
- 街道设计导则与城市道路系统的优化提升:从通行能力到空间品质的转变
- android wps翻译功能吗,Android版WPS Office将支持金山词霸即时翻译
- 交换机、路由器、防火墙综述
- 微信小程序 一键保存视频到手机相册功能(视频来源为链接)
- Java jdk keytool 实现SSL双向认证的方法(客户端与服务器)
- 详解css设置默认字体
- 【计算理论】图灵机 ( 图灵机示例 )
- 指针数组与数组指针详解
- [picoCTF]Scavenger Hunt write up
- MySQL数据库操作补坑(一)数据类型
热门文章
- HbuilderX如何创建一个新的Vue工程
- 腾讯云cdn怎样接入域名
- Java 汉字获取拼音或首字母工具类
- Jetty 安全漏洞分析
- 老一辈学计算机的在那,真实的南京大学计算机系
- word流程图怎么使箭头对齐_怎样在word中绘制流程图方框间距相同,排列整齐,箭头笔直...
- html中如何写平方根等,excel平方根
- GAN评价指标代码(FID、LPIPS、MS-SSIM)
- 计算机“存储容量”、“速率”(换算详解)
- 解决笔记本电脑有线耳机插入无反应的情况