音频、视频

采用 FFmpeg 将流媒体切片(编码),类似 m3u8 作为目录,切成多个视频 ts 文件
image.png
再根据不同清晰度编码多个版本,来应对用户再不同网络环境情况
image.png

直播

动态 m3u8 文件,不断编码写入目录,并上传内容,再由流媒体服务器分发

传输

通常视频文件较大,所以传输需要压缩,播放需要解码
image.png

压缩算法中,有一个概念【宏块】:抽象提取连续画面(帧)中未发生变化的区域

视频通话

两台不在同一内网下的主机 Host1,Host2 需要进行视频通话,由于 NAT 和 路由的存在,是不能直接通信的,需要 NAT 穿透
image.png
image.png
image.png
image.png

例子:https://github.com/c-yyy/webRTC-JS-Demo

image.png

部署线上视频通话服务

  1. 前端(具体请看示例代码 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 }

  1. ```vue
  2. <template>
  3. <div class="video-page">
  4. <video class="localVideo" autoplay ref="localVideo" muted />
  5. <video class="remoteVideo" autoplay ref="remoteVideo" />
  6. <audio src="https://qiniu.terminal.laway.cn/wait.mp3" loop ref="waitAudio" />
  7. <section class="phone-section">
  8. <PhoneIcon />
  9. <span>{{ callTime }}</span>
  10. </section>
  11. <section class="ping-fps" @click="showLog = !showLog">
  12. <span v-show="FPS !== undefined">fps {{ FPS }}</span>
  13. <p v-show="showLog" style="width:30vw">{{ Logs }}</p>
  14. </section>
  15. </div>
  16. <Calling v-if="callTime === '00:00:00'" />
  17. </template>
  18. <script>
  19. import { ref, defineComponent, onMounted, onUnmounted } from 'vue'
  20. import PhoneIcon from '@/components/PhoneIcon.vue'
  21. import Calling from '@/components/Calling.vue'
  22. import { Dialog } from 'vant';
  23. import router from '@/router'
  24. import { webRTC_mediaStreamQuery, webRTC_configuration, socket_config } from '@/config/webRTC'
  25. export default defineComponent({
  26. components: {
  27. PhoneIcon,
  28. Calling
  29. },
  30. setup () {
  31. let localVideo = ref(null)
  32. let remoteVideo = ref(null)
  33. let waitAudio = ref(null)
  34. let showLog = ref(false)
  35. let Logs = ref('log')
  36. // let RTT = ref('')
  37. let FPS = ref('')
  38. let timer = null
  39. let callTime = ref('00:00:00')
  40. const isTest = !window.location.href.includes('type=test')
  41. // eslint-disable-next-line no-unused-vars
  42. let localMediaStream, remoteMediaStream = null
  43. const connection = new RTCPeerConnection(webRTC_configuration)
  44. // eslint-disable-next-line no-undef
  45. const socket = io(socket_config.url, socket_config.config)
  46. console.log('RTCPeerConnection', connection)
  47. const closeVideo = () => {
  48. localMediaStream.getTracks().map(track => track.stop())
  49. localMediaStream = null
  50. remoteMediaStream = null
  51. clearInterval(timer)
  52. connection.close()
  53. socket.emit('leave', {
  54. from: 'test'
  55. })
  56. socket.close()
  57. }
  58. const formateTime = (time) => {
  59. if (time < 60) {
  60. return `00:00:${time < 10 ? '0' + time : time}`;
  61. } else if (time > 60 && time < 3600) {
  62. const min = parseInt(time / 60);
  63. const sed = time % 60;
  64. return `00:${min < 10 ? '0' + min : min}:${sed < 10 ? '0' + sed : sed}`;
  65. } else if (time > 3600) {
  66. const h = parseInt(time / 3600);
  67. const min = parseInt((time % 3600) / 60);
  68. const sed = (time % 3600) % 60;
  69. return `${h < 10 ? '0' + h : h}:${min < 10 ? '0' + min : min}:${
  70. sed < 10 ? '0' + sed : sed
  71. }`;
  72. }
  73. }
  74. const startTiming = () => {
  75. let time = 0;
  76. timer = setInterval(async () => {
  77. time += 1;
  78. callTime.value = formateTime(time);
  79. const stats = await connection.getStats(null)
  80. stats.forEach(report => {
  81. if (report.type === "inbound-rtp" && report.kind === "video") {
  82. Logs.value = report
  83. // if (report.totalRoundTripTime && report.responsesReceived) {
  84. // RTT.value = parseInt((report.totalRoundTripTime / report.responsesReceived) * 1000)
  85. // }
  86. FPS.value = report.framesPerSecond
  87. }
  88. })
  89. }, 1e3);
  90. }
  91. const getLocalVideo = async () => {
  92. try {
  93. document.querySelector('.localVideo').setAttribute('autoplay', '');
  94. document.querySelector('.localVideo').setAttribute('muted', '');
  95. document.querySelector('.localVideo').setAttribute('playsinline', '');
  96. document.querySelector('.remoteVideo').setAttribute('autoplay', '');
  97. document.querySelector('.remoteVideo').setAttribute('muted', '');
  98. document.querySelector('.remoteVideo').setAttribute('playsinline', '');
  99. localMediaStream = await navigator.mediaDevices.getUserMedia(webRTC_mediaStreamQuery)
  100. localVideo.value.srcObject = localMediaStream
  101. console.log(localMediaStream)
  102. isTest && waitAudio.value.play()
  103. socket.emit('message', {
  104. type: 'call',
  105. from: 'test'
  106. })
  107. socket.on('leaved', () => {
  108. console.log('leaved')
  109. Dialog.alert({
  110. title: '通话结束',
  111. message: '对方已挂断~',
  112. }).then(() => {
  113. // on close
  114. router.replace('/')
  115. });
  116. })
  117. /** Track 方式 */
  118. localMediaStream.getTracks().forEach(track => connection.addTrack(track, localMediaStream))
  119. connection.ontrack = (e) => {
  120. console.log('ontrack', e)
  121. if (remoteVideo.value.srcObject !== e.streams[0]) {
  122. waitAudio.value.pause()
  123. remoteMediaStream = e.streams[0]
  124. remoteVideo.value.srcObject = e.streams[0]
  125. startTiming()
  126. }
  127. }
  128. /** Stream 方式 */
  129. // connection.addStream(localMediaStream)
  130. // connection.onaddstream = e => {
  131. // console.log('onaddstream', e)
  132. // if (remoteVideo.value.srcObject !== e.streams[0]) {
  133. // waitAudio.value.pause()
  134. // remoteMediaStream = e.streams[0]
  135. // remoteVideo.value.srcObject = e.streams[0]
  136. // startTiming()
  137. // }
  138. // }
  139. connection.onicecandidate = e => {
  140. console.log('onicecandidate', e)
  141. if (e.candidate) {
  142. socket.emit('message', {
  143. type: 'candidate',
  144. from: 'test',
  145. candidate: e.candidate
  146. })
  147. }
  148. }
  149. if (!isTest) {
  150. connection.createOffer({
  151. offerToReceiveAudio: true,
  152. offerToReceiveVideo: true
  153. }).then(offer => {
  154. console.log('offer', offer)
  155. connection.setLocalDescription(offer)
  156. socket.emit('message', {
  157. type: 'offer',
  158. from: 'test',
  159. offer
  160. })
  161. })
  162. }
  163. } catch (error) {
  164. Dialog.alert({
  165. title: '提示',
  166. message: '请打开授权~',
  167. }).then(() => {
  168. // on close
  169. router.replace('/')
  170. });
  171. console.error(error)
  172. }
  173. }
  174. const getRemoteVide = () => {
  175. socket.on('message', async data => {
  176. const { type } = data
  177. switch (type) {
  178. case 'offer':
  179. if (isTest && data.offer) {
  180. await connection.setRemoteDescription(new RTCSessionDescription(data.offer))
  181. connection.createAnswer().then(answer => {
  182. console.log('answer', answer)
  183. connection.setLocalDescription(answer)
  184. socket.emit('message', {
  185. type: 'answer',
  186. from: 'test',
  187. answer
  188. })
  189. })
  190. break;
  191. }
  192. return
  193. case 'answer':
  194. if (!isTest && data.answer) {
  195. connection.setRemoteDescription(new RTCSessionDescription(data.answer))
  196. }
  197. break;
  198. case 'candidate':
  199. if (connection && data.candidate && data.from !== isTest) {
  200. connection.addIceCandidate(new RTCIceCandidate(data.candidate))
  201. }
  202. break;
  203. case 'close':
  204. closeVideo()
  205. }
  206. })
  207. }
  208. onMounted(() => {
  209. getLocalVideo()
  210. getRemoteVide()
  211. })
  212. onUnmounted(() => {
  213. closeVideo()
  214. })
  215. return {
  216. localVideo,
  217. remoteVideo,
  218. waitAudio,
  219. callTime,
  220. showLog,
  221. Logs,
  222. // RTT,
  223. FPS
  224. }
  225. },
  226. })
  227. </script>
  228. <style scoped>
  229. .video-page {
  230. width: 100vw;
  231. height: 100vh;
  232. position: relative;
  233. }
  234. .localVideo,
  235. .remoteVideo {
  236. background: #303133;
  237. }
  238. .localVideo {
  239. position: absolute;
  240. top: 0;
  241. right: 0;
  242. width: 30%;
  243. height: 30vh;
  244. background: #fff;
  245. z-index: 3;
  246. }
  247. .remoteVideo {
  248. width: 100%;
  249. height: 100%;
  250. }
  251. .phone-section {
  252. position: absolute;
  253. top: 80%;
  254. left: 50%;
  255. transform: translate(-80%, -50%);
  256. z-index: 5;
  257. color: #fff;
  258. }
  259. .ping-fps {
  260. color: #fff;
  261. position: absolute;
  262. top: 10px;
  263. left: 10px;
  264. z-index: 3;
  265. }
  266. </style>
  1. ICE 服务(穿透 NAT )

    1. # docker 部署一个私有 ICE 服务
    2. # coTurn 开源地址:https://github.com/coturn/coturn
    3. # 参考:https://www.wyr.me/post/640
    4. docker run -d --network=host instrumentisto/coturn --realm=my.realm.org --external-ip='$(detect-external-ip)' --user=user123:pass123
  2. 本地视频流接入、发送流

  3. 发送 offer
  4. Socket监听远程视频流接入
  5. 另一端接收 offer 并发送 answer
  6. 实时显示延迟和 FPS
  7. 正常 Nginx 部署(最好支持 https)
    1. location /webRTC-test {
    2. alias /home/ubuntu/site/yls/dist;
    3. index index.html;
    4. expires -1;
    5. try_files $uri $uri/ /dist/index.html;
    6. }
  1. 后端
    1. Socket 服务(支持跨域)
    2. 支持开启房间 room
    3. 丢服务器 pm2 start main.js --name webRTC-socket-test
    4. Nginx proxy 反向代理
      1. location /socket.io {
      2. add_header 'Access-Control-Allow-Headers' 'Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agen
      3. t, X-Requested-With, Token, x-access-token' always;
      4. # 设置主机头和客户端真实地址,以便服务器获取客户端真实 IP
      5. proxy_redirect off;
      6. proxy_set_header Host $host;
      7. proxy_set_header X-Real-IP $remote_addr;
      8. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      9. proxy_http_version 1.1;
      10. proxy_set_header X-NginX-Proxy true;
      11. proxy_set_header Upgrade $http_upgrade;
      12. proxy_set_header Connection "upgrade";
      13. # 禁用缓存
      14. proxy_buffering off;
      15. # 反向代理的地址
      16. proxy_pass http://127.0.0.1:3479;
      17. }