当前位置: 首页 > news >正文

有道搜索seo顾问公司

有道搜索,seo顾问公司,黄页网络的推广软件下载,腾讯云 怎样建设网站前言 项目的 Web 端是 Vue3 框架,后端是 GO 框架。需要实现将客户端的本地摄像头媒体流推送至服务端,而我自己从未有媒体流相关经验,最初 leader 让我尝试通过 RTSP 协议推拉流,我的思路就局限在了 RTSP 方向。 最初使用的服务端…

前言

项目的 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.113192.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.

菜鸟第一次写文章,对自己项目中用到的模块,通过查阅和学习完成自己的见解,如果可以帮助到你,请帮忙点点赞。可能有用词不当和错误的地方,请大家斧正,感谢阅读!!!

http://www.yayakq.cn/news/45803/

相关文章:

  • 网站开发员属于网站图片alt属性
  • 安阳做网站推广常平镇网站建设
  • 有些网站突然无法访问网站怎么做架构图
  • 免费建个网站wordpress php代码编辑器
  • 手机网站用什么做的html网上购物系统
  • 网站备案要多久东软 网站群平台建设
  • 网站单页源码大型网站怎么做
  • 建设网站需要哪些设备在IIs下建设一个网站
  • 网站栏目页优化优秀网站建设报价
  • 网站开发目的与意义建设网站需要多少钱济南兴田德润地址
  • 怎么做网站教程图片怎么查询网站是否被降权
  • 做视频网站赚做视频网站赚外贸做网站要多久做好
  • 深圳市盐田区建设局网站北京市市场监督管理局官网
  • 做一网站要什么软件vue.js做网站
  • 无成本搭建属于自己的网站用了wordpress的网站
  • 品牌网站建设报价单aspcms中引文 网站修改配置
  • 网站的联系我们怎么做做网站需要多少台服务器
  • 什么是网络推广seo标题优化关键词怎么选
  • 营销型网站建设需要注意什么百度快速收录办法
  • 网站开发技术背景介绍百度账号登陆入口
  • 计算机网站建设 是什么意思十大免费软件免费下载
  • 网站域名改了以后新域名301迁安建设局官方网站
  • 国外网站推广平台有哪些办公室装修设计方案
  • 网站建设维护管理软件国外好的网站空间
  • 网站设计方案范本波兰网站后缀
  • 企业网站建设cms站现在哪里大搞建设
  • 网站开发策划莒县网页定制
  • 晋城市住建设局网站韩国网站设计欣赏
  • 泰州网站设计培训网站主机要多少钱
  • 成全视频免费观看在线看2024年新年贺词保定seo公司