音频、视频
采用 FFmpeg 将流媒体切片(编码),类似 m3u8 作为目录,切成多个视频 ts 文件
再根据不同清晰度编码多个版本,来应对用户再不同网络环境情况
直播
动态 m3u8 文件,不断编码写入目录,并上传内容,再由流媒体服务器分发
传输
通常视频文件较大,所以传输需要压缩,播放需要解码
压缩算法中,有一个概念【宏块】:抽象提取连续画面(帧)中未发生变化的区域
视频通话
两台不在同一内网下的主机 Host1,Host2 需要进行视频通话,由于 NAT 和 路由的存在,是不能直接通信的,需要 NAT 穿透



例子:https://github.com/c-yyy/webRTC-JS-Demo
部署线上视频通话服务
- 前端(具体请看示例代码 Vue 3) ```javascript /* socket 配置 / export const socket_config = { url: ‘ws://127.0.0.1:3479’, config: {} }
/* webRTC ICE 配置 / export const webRTC_configuration = { iceServers: [ { urls: ‘stun:stun.l.google.com:19302’ } ], iceTransportPolicy: ‘relay’, // 可选值 all or relay iceCandidatePoolSize: 0, optionalArgument: { optional: [{ DtlsSrtpKeyAgreement: true }, { googImprovedWifiBwe: true }, { googScreencastMinBitrate: 300 }, { googIPv6: true }, { googDscp: true }, { googCpuUnderuseThreshold: 55 }, { googCpuOveruseThreshold: 85 }, { googSuspendBelowMinBitrate: true }, { googCpuOveruseDetection: true }], mandatory: {} } }
/* webRTC offer 配置 / export const webRTC_offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }
/* webRTC 流媒体 配置 / export const webRTC_mediaStreamQuery = { video: { facingMode: “user”, width: { min: 600, ideal: 1280, max: 1920, }, height: { min: 400, ideal: 800, max: 1080 } }, audio: true }
```vue<template><div class="video-page"><video class="localVideo" autoplay ref="localVideo" muted /><video class="remoteVideo" autoplay ref="remoteVideo" /><audio src="https://qiniu.terminal.laway.cn/wait.mp3" loop ref="waitAudio" /><section class="phone-section"><PhoneIcon /><span>{{ callTime }}</span></section><section class="ping-fps" @click="showLog = !showLog"><span v-show="FPS !== undefined">fps {{ FPS }}</span><p v-show="showLog" style="width:30vw">{{ Logs }}</p></section></div><Calling v-if="callTime === '00:00:00'" /></template><script>import { ref, defineComponent, onMounted, onUnmounted } from 'vue'import PhoneIcon from '@/components/PhoneIcon.vue'import Calling from '@/components/Calling.vue'import { Dialog } from 'vant';import router from '@/router'import { webRTC_mediaStreamQuery, webRTC_configuration, socket_config } from '@/config/webRTC'export default defineComponent({components: {PhoneIcon,Calling},setup () {let localVideo = ref(null)let remoteVideo = ref(null)let waitAudio = ref(null)let showLog = ref(false)let Logs = ref('log')// let RTT = ref('')let FPS = ref('')let timer = nulllet callTime = ref('00:00:00')const isTest = !window.location.href.includes('type=test')// eslint-disable-next-line no-unused-varslet localMediaStream, remoteMediaStream = nullconst connection = new RTCPeerConnection(webRTC_configuration)// eslint-disable-next-line no-undefconst socket = io(socket_config.url, socket_config.config)console.log('RTCPeerConnection', connection)const closeVideo = () => {localMediaStream.getTracks().map(track => track.stop())localMediaStream = nullremoteMediaStream = nullclearInterval(timer)connection.close()socket.emit('leave', {from: 'test'})socket.close()}const formateTime = (time) => {if (time < 60) {return `00:00:${time < 10 ? '0' + time : time}`;} else if (time > 60 && time < 3600) {const min = parseInt(time / 60);const sed = time % 60;return `00:${min < 10 ? '0' + min : min}:${sed < 10 ? '0' + sed : sed}`;} else if (time > 3600) {const h = parseInt(time / 3600);const min = parseInt((time % 3600) / 60);const sed = (time % 3600) % 60;return `${h < 10 ? '0' + h : h}:${min < 10 ? '0' + min : min}:${sed < 10 ? '0' + sed : sed}`;}}const startTiming = () => {let time = 0;timer = setInterval(async () => {time += 1;callTime.value = formateTime(time);const stats = await connection.getStats(null)stats.forEach(report => {if (report.type === "inbound-rtp" && report.kind === "video") {Logs.value = report// if (report.totalRoundTripTime && report.responsesReceived) {// RTT.value = parseInt((report.totalRoundTripTime / report.responsesReceived) * 1000)// }FPS.value = report.framesPerSecond}})}, 1e3);}const getLocalVideo = async () => {try {document.querySelector('.localVideo').setAttribute('autoplay', '');document.querySelector('.localVideo').setAttribute('muted', '');document.querySelector('.localVideo').setAttribute('playsinline', '');document.querySelector('.remoteVideo').setAttribute('autoplay', '');document.querySelector('.remoteVideo').setAttribute('muted', '');document.querySelector('.remoteVideo').setAttribute('playsinline', '');localMediaStream = await navigator.mediaDevices.getUserMedia(webRTC_mediaStreamQuery)localVideo.value.srcObject = localMediaStreamconsole.log(localMediaStream)isTest && waitAudio.value.play()socket.emit('message', {type: 'call',from: 'test'})socket.on('leaved', () => {console.log('leaved')Dialog.alert({title: '通话结束',message: '对方已挂断~',}).then(() => {// on closerouter.replace('/')});})/** Track 方式 */localMediaStream.getTracks().forEach(track => connection.addTrack(track, localMediaStream))connection.ontrack = (e) => {console.log('ontrack', e)if (remoteVideo.value.srcObject !== e.streams[0]) {waitAudio.value.pause()remoteMediaStream = e.streams[0]remoteVideo.value.srcObject = e.streams[0]startTiming()}}/** Stream 方式 */// connection.addStream(localMediaStream)// connection.onaddstream = e => {// console.log('onaddstream', e)// if (remoteVideo.value.srcObject !== e.streams[0]) {// waitAudio.value.pause()// remoteMediaStream = e.streams[0]// remoteVideo.value.srcObject = e.streams[0]// startTiming()// }// }connection.onicecandidate = e => {console.log('onicecandidate', e)if (e.candidate) {socket.emit('message', {type: 'candidate',from: 'test',candidate: e.candidate})}}if (!isTest) {connection.createOffer({offerToReceiveAudio: true,offerToReceiveVideo: true}).then(offer => {console.log('offer', offer)connection.setLocalDescription(offer)socket.emit('message', {type: 'offer',from: 'test',offer})})}} catch (error) {Dialog.alert({title: '提示',message: '请打开授权~',}).then(() => {// on closerouter.replace('/')});console.error(error)}}const getRemoteVide = () => {socket.on('message', async data => {const { type } = dataswitch (type) {case 'offer':if (isTest && data.offer) {await connection.setRemoteDescription(new RTCSessionDescription(data.offer))connection.createAnswer().then(answer => {console.log('answer', answer)connection.setLocalDescription(answer)socket.emit('message', {type: 'answer',from: 'test',answer})})break;}returncase 'answer':if (!isTest && data.answer) {connection.setRemoteDescription(new RTCSessionDescription(data.answer))}break;case 'candidate':if (connection && data.candidate && data.from !== isTest) {connection.addIceCandidate(new RTCIceCandidate(data.candidate))}break;case 'close':closeVideo()}})}onMounted(() => {getLocalVideo()getRemoteVide()})onUnmounted(() => {closeVideo()})return {localVideo,remoteVideo,waitAudio,callTime,showLog,Logs,// RTT,FPS}},})</script><style scoped>.video-page {width: 100vw;height: 100vh;position: relative;}.localVideo,.remoteVideo {background: #303133;}.localVideo {position: absolute;top: 0;right: 0;width: 30%;height: 30vh;background: #fff;z-index: 3;}.remoteVideo {width: 100%;height: 100%;}.phone-section {position: absolute;top: 80%;left: 50%;transform: translate(-80%, -50%);z-index: 5;color: #fff;}.ping-fps {color: #fff;position: absolute;top: 10px;left: 10px;z-index: 3;}</style>
ICE 服务(穿透 NAT )
# docker 部署一个私有 ICE 服务# coTurn 开源地址:https://github.com/coturn/coturn# 参考:https://www.wyr.me/post/640docker run -d --network=host instrumentisto/coturn --realm=my.realm.org --external-ip='$(detect-external-ip)' --user=user123:pass123
本地视频流接入、发送流
- 发送 offer
- Socket监听远程视频流接入
- 另一端接收 offer 并发送 answer
- 实时显示延迟和 FPS
- 正常 Nginx 部署(最好支持 https)
location /webRTC-test {alias /home/ubuntu/site/yls/dist;index index.html;expires -1;try_files $uri $uri/ /dist/index.html;}
- 后端
- Socket 服务(支持跨域)
- 支持开启房间 room
- 丢服务器
pm2 start main.js --name webRTC-socket-test - Nginx proxy 反向代理
location /socket.io {add_header 'Access-Control-Allow-Headers' 'Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token' always;# 设置主机头和客户端真实地址,以便服务器获取客户端真实 IPproxy_redirect off;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_http_version 1.1;proxy_set_header X-NginX-Proxy true;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";# 禁用缓存proxy_buffering off;# 反向代理的地址proxy_pass http://127.0.0.1:3479;}

