Przeglądaj źródła

feat: srs初试

shuisheng 2 lat temu
rodzic
commit
2d25e27439

+ 1 - 1
.eslintrc.js

@@ -160,7 +160,7 @@ module.exports = {
     // @typescript-eslint插件
     // @typescript-eslint插件
     '@typescript-eslint/restrict-template-expressions': 2, // 强制模板文字表达式为string类型。即const a = {};console.log(`${a}`);会报错
     '@typescript-eslint/restrict-template-expressions': 2, // 强制模板文字表达式为string类型。即const a = {};console.log(`${a}`);会报错
     '@typescript-eslint/no-unused-vars': 2,
     '@typescript-eslint/no-unused-vars': 2,
-    '@typescript-eslint/no-floating-promises': 1, // 要求适当处理类似 Promise 的语句。即将await或者return Promise,或者对promise进行.then或者.catch
+    '@typescript-eslint/no-floating-promises': 0, // 要求适当处理类似 Promise 的语句。即将await或者return Promise,或者对promise进行.then或者.catch
     '@typescript-eslint/no-explicit-any': 0, // 不允许定义any类型。即let a: any;会报错
     '@typescript-eslint/no-explicit-any': 0, // 不允许定义any类型。即let a: any;会报错
     '@typescript-eslint/no-non-null-assertion': 0, // 禁止使用非空断言(后缀运算符!)。即const el = document.querySelector('.app');console.log(el!.tagName);会报错
     '@typescript-eslint/no-non-null-assertion': 0, // 禁止使用非空断言(后缀运算符!)。即const el = document.querySelector('.app');console.log(el!.tagName);会报错
     '@typescript-eslint/ban-ts-comment': 0, // 禁止使用@ts-<directive>注释
     '@typescript-eslint/ban-ts-comment': 0, // 禁止使用@ts-<directive>注释

+ 14 - 1
script/config/webpack.dev.ts

@@ -3,8 +3,8 @@ import portfinder from 'portfinder';
 import { Configuration } from 'webpack';
 import { Configuration } from 'webpack';
 import WebpackBar from 'webpackbar';
 import WebpackBar from 'webpackbar';
 
 
+import { outputStaticUrl, webpackBarEnable } from '../constant';
 import TerminalPrintPlugin from '../TerminalPrintPlugin';
 import TerminalPrintPlugin from '../TerminalPrintPlugin';
-import { webpackBarEnable, outputStaticUrl } from '../constant';
 import { chalkINFO } from '../utils/chalkTip';
 import { chalkINFO } from '../utils/chalkTip';
 import { resolveApp } from '../utils/path';
 import { resolveApp } from '../utils/path';
 
 
@@ -71,6 +71,19 @@ export default new Promise((resolve) => {
             directory: resolveApp('./public/'),
             directory: resolveApp('./public/'),
           },
           },
           proxy: {
           proxy: {
+            '/srs/': {
+              target: 'http://localhost:1985',
+              secure: false, // 默认情况下(secure: true),不接受在HTTPS上运行的带有无效证书的后端服务器。设置secure: false后,后端服务器的HTTPS有无效证书也可运行
+              /**
+               * changeOrigin,是否修改请求地址的源
+               * 默认changeOrigin: false,即发请求即使用devServer的localhost:port发起的,如果后端服务器有校验源,就会有问题
+               * 设置changeOrigin: true,就会修改发起请求的源,将原本的localhost:port修改为target,这样就可以通过后端服务器对源的校验
+               */
+              changeOrigin: true,
+              pathRewrite: {
+                '^/srs/': '', // 效果:/srs/link/list ==> http://localhost:3300/link/list
+              },
+            },
             '/api': {
             '/api': {
               target: 'http://localhost:3300',
               target: 'http://localhost:3300',
               secure: false, // 默认情况下(secure: true),不接受在HTTPS上运行的带有无效证书的后端服务器。设置secure: false后,后端服务器的HTTPS有无效证书也可运行
               secure: false, // 默认情况下(secure: true),不接受在HTTPS上运行的带有无效证书的后端服务器。设置secure: false后,后端服务器的HTTPS有无效证书也可运行

+ 11 - 0
src/api/live.ts

@@ -0,0 +1,11 @@
+import request from '@/utils/request';
+
+export function fetchLiveList() {
+  return request.instance({
+    url:
+      process.env.NODE_ENV === 'development'
+        ? 'http://localhost:4300/live/list'
+        : 'https://live.hsslive.cn/api/live/list',
+    method: 'get',
+  });
+}

+ 28 - 0
src/api/srs.ts

@@ -0,0 +1,28 @@
+import request from '@/utils/request';
+
+export function fetchRtcV1Publish(data: {
+  api: string;
+  clientip: string | null;
+  sdp: string;
+  streamurl: string;
+  tid: string;
+}) {
+  return request.instance({
+    url: `/srs/rtc/v1/publish/`,
+    method: 'post',
+    data,
+  });
+}
+export function fetchRtcV1Play(data: {
+  api: string;
+  clientip: string | null;
+  sdp: string;
+  streamurl: string;
+  tid: string;
+}) {
+  return request.instance({
+    url: '/srs/rtc/v1/play/',
+    method: 'post',
+    data,
+  });
+}

+ 0 - 4
src/assets/js/aa.js

@@ -1,4 +0,0 @@
-console.log(1111);
-function aaaa() {
-  console.log('aaaa');
-}

+ 0 - 400
src/assets/js/srs.player.js

@@ -1,400 +0,0 @@
-/**
-* the SrsPlayer object.
-* @param container the html container id.
-* @param width a float value specifies the width of player.
-* @param height a float value specifies the height of player.
-* @param private_object [optional] an object that used as private object, 
-*       for example, the logic chat object which owner this player.
-* Usage:
-        <script type="text/javascript" src="js/swfobject.js"></script>
-        <script type="text/javascript" src="js/srs.player.js"></script>
-        <div id="player"></div>
-        var p = new SrsPlayer("player", 640, 480);
-        p.set_srs_player_url("srs_player.swf?v=1.0.0");
-        p.on_player_ready = function() {
-            p.set_bt(0.8);
-            p.set_mbt(1.2);
-            p.play("rtmp://ossrs.net/live/livestream");
-        };
-        p.on_player_metadata = function(metadata) {
-            console.log(metadata);
-            console.log(p.dump_log());
-        };
-        p.start();
-*/
-function SrsPlayer(container, width, height, private_object) {
-  if (!SrsPlayer.__id) {
-    SrsPlayer.__id = 100;
-  }
-  if (!SrsPlayer.__players) {
-    SrsPlayer.__players = [];
-  }
-
-  SrsPlayer.__players.push(this);
-
-  this.private_object = private_object;
-  this.container = container;
-  this.width = width;
-  this.height = height;
-  this.id = SrsPlayer.__id++;
-  this.stream_url = null;
-  this.buffer_time = 0.3; // default to 0.3
-  this.max_buffer_time = this.buffer_time * 3; // default to 3 x bufferTime.
-  this.volume = 1.0; // default to 100%
-  this.callbackObj = null;
-  this.srs_player_url =
-    'srs_player/release/srs_player.swf?_version=' + srs_get_version_code();
-
-  // callback set the following values.
-  this.meatadata = {}; // for on_player_metadata
-  this.time = 0; // current stream time.
-  this.buffer_length = 0; // current stream buffer length.
-  this.kbps = 0; // current stream bitrate(video+audio) in kbps.
-  this.fps = 0; // current stream video fps.
-  this.rtime = 0; // flash relative time in ms.
-
-  this.__fluency = {
-    total_empty_count: 0,
-    total_empty_time: 0,
-    current_empty_time: 0,
-  };
-  this.__fluency.on_stream_empty = function (time) {
-    this.total_empty_count++;
-    this.current_empty_time = time;
-  };
-  this.__fluency.on_stream_full = function (time) {
-    if (this.current_empty_time > 0) {
-      this.total_empty_time += time - this.current_empty_time;
-      this.current_empty_time = 0;
-    }
-  };
-  this.__fluency.calc = function (time) {
-    var den = this.total_empty_count * 4 + this.total_empty_time * 2 + time;
-    if (den > 0) {
-      return (time * 100) / den;
-    }
-    return 0;
-  };
-}
-/**
- * user can set some callback, then start the player.
- * @param url the default url.
- * callbacks:
- *      on_player_ready():int, when srs player ready, user can play().
- *      on_player_metadata(metadata:Object):int, when srs player get metadata.
- * methods:
- *      set_bt(t:Number):void, set the buffer time in seconds.
- *      set_mbt(t:Number):void, set the max buffer time in seconds.
- *      dump_log():String, get all logs of player.
- */
-SrsPlayer.prototype.start = function (url) {
-  if (url) {
-    this.stream_url = url;
-  }
-
-  // embed the flash.
-  var flashvars = {};
-  flashvars.id = this.id;
-  flashvars.on_player_ready = '__srs_on_player_ready';
-  flashvars.on_player_metadata = '__srs_on_player_metadata';
-  flashvars.on_player_timer = '__srs_on_player_timer';
-  flashvars.on_player_empty = '__srs_on_player_empty';
-  flashvars.on_player_full = '__srs_on_player_full';
-  flashvars.on_player_status = '__srs_on_player_status';
-
-  var params = {};
-  params.wmode = 'opaque';
-  params.allowFullScreen = 'true';
-  params.allowScriptAccess = 'always';
-
-  var attributes = {};
-
-  var self = this;
-
-  swfobject.embedSWF(
-    this.srs_player_url,
-    this.container,
-    this.width,
-    this.height,
-    '11.1.0',
-    'js/AdobeFlashPlayerInstall.swf',
-    flashvars,
-    params,
-    attributes,
-    function (callbackObj) {
-      self.callbackObj = callbackObj;
-      if (!callbackObj.success) {
-        console.error('Initialize player failed:');
-        console.error(callbackObj);
-      }
-    }
-  );
-
-  return this;
-};
-/**
- * play the stream.
- * @param stream_url the url of stream, rtmp or http.
- * @param volume the volume, 0 is mute, 1 is 100%, 2 is 200%.
- */
-SrsPlayer.prototype.play = function (url, volume) {
-  this.stop();
-  SrsPlayer.__players.push(this);
-
-  if (url) {
-    this.stream_url = url;
-  }
-
-  // volume maybe 0, so never use if(volume) to check its value.
-  if (volume != null && volume != undefined) {
-    this.volume = volume;
-  }
-
-  this.callbackObj.ref.__play(
-    this.stream_url,
-    this.width,
-    this.height,
-    this.buffer_time,
-    this.max_buffer_time,
-    this.volume
-  );
-};
-/**
- * stop play stream.
- */
-SrsPlayer.prototype.stop = function () {
-  this.callbackObj.ref.__stop();
-};
-/**
- * pause the play.
- */
-SrsPlayer.prototype.pause = function () {
-  this.callbackObj.ref.__pause();
-};
-/**
- * resume the play.
- */
-SrsPlayer.prototype.resume = function () {
-  this.callbackObj.ref.__resume();
-};
-/**
- * get the stream fluency, where 100 is 100%.
- */
-SrsPlayer.prototype.fluency = function () {
-  return this.__fluency.calc(this.rtime);
-};
-/**
- * get the stream empty count.
- */
-SrsPlayer.prototype.empty_count = function () {
-  return this.__fluency.total_empty_count;
-};
-/**
- * get all log data.
- */
-SrsPlayer.prototype.dump_log = function () {
-  return this.callbackObj.ref.__dump_log();
-};
-/**
- * to set the DAR, for example, DAR=16:9 where num=16,den=9.
- * @param num, for example, 16.
- *       use metadata width if 0.
- *       use user specified width if -1.
- * @param den, for example, 9.
- *       use metadata height if 0.
- *       use user specified height if -1.
- */
-SrsPlayer.prototype.set_dar = function (num, den) {
-  this.callbackObj.ref.__set_dar(num, den);
-};
-/**
- * set the fullscreen size data.
- * @refer the refer fullscreen mode. it can be:
- *       video: use video orignal size.
- *       screen: use screen size to rescale video.
- * @param percent, the rescale percent, where
- *       100 means 100%.
- */
-SrsPlayer.prototype.set_fs = function (refer, percent) {
-  this.callbackObj.ref.__set_fs(refer, percent);
-};
-/**
- * set the stream buffer time in seconds.
- * @buffer_time the buffer time in seconds.
- */
-SrsPlayer.prototype.set_bt = function (buffer_time) {
-  if (this.buffer_time == buffer_time) {
-    return;
-  }
-
-  this.buffer_time = buffer_time;
-  this.callbackObj.ref.__set_bt(buffer_time);
-
-  // reset the max buffer time to 3 x buffer_time.
-  this.set_mbt(buffer_time * 3);
-};
-/**
- * set the stream max buffer time in seconds.
- * @param max_buffer_time the max buffer time in seconds.
- * @remark this is the key feature for realtime communication by flash.
- */
-SrsPlayer.prototype.set_mbt = function (max_buffer_time) {
-  // we must atleast set the max buffer time to 0.6s.
-  max_buffer_time = Math.max(0.6, max_buffer_time);
-  // max buffer time always greater than buffer time.
-  max_buffer_time = Math.max(this.buffer_time, max_buffer_time);
-
-  if (parseInt(this.max_buffer_time * 10) == parseInt(max_buffer_time * 10)) {
-    return;
-  }
-
-  this.max_buffer_time = max_buffer_time;
-  this.callbackObj.ref.__set_mbt(max_buffer_time);
-};
-/**
- * set the srs_player.swf url
- * @param url, srs_player.swf's url.
- * @param params, object.
- */
-SrsPlayer.prototype.set_srs_player_url = function (url, params) {
-  var query_array = [],
-    query_string = '',
-    p;
-  params = params || {};
-  params._version = srs_get_version_code();
-  for (p in params) {
-    if (params.hasOwnProperty(p)) {
-      query_array.push(p + '=' + encodeURIComponent(params[p]));
-    }
-  }
-  query_string = query_array.join('&');
-  this.srs_player_url = url + '?' + query_string;
-};
-/**
- * the callback when player is ready.
- */
-SrsPlayer.prototype.on_player_ready = function () {};
-/**
- * the callback when player got metadata.
- * @param metadata the metadata which player got.
- */
-SrsPlayer.prototype.on_player_metadata = function (metadata) {
-  // ignore.
-};
-/**
- * the callback when player timer event.
- * @param time current stream time.
- * @param buffer_length current buffer length.
- * @param kbps current video plus audio bitrate in kbps.
- * @param fps current video fps.
- * @param rtime current relative time by flash.util.getTimer().
- */
-SrsPlayer.prototype.on_player_timer = function (
-  time,
-  buffer_length,
-  kbps,
-  fps,
-  rtime
-) {
-  // ignore.
-};
-/**
- * the callback when player got NetStream.Buffer.Empty
- * @param time current relative time by flash.util.getTimer().
- */
-SrsPlayer.prototype.on_player_empty = function (time) {
-  // ignore.
-};
-/**
- * the callback when player got NetStream.Buffer.Full
- * @param time current relative time by flash.util.getTimer().
- */
-SrsPlayer.prototype.on_player_full = function (time) {
-  // ignore.
-};
-/**
- * the callback when player status change.
- * @param code the status code, "init", "connected", "play", "closed", "rejected", "failed".
- *      init => connected/rejected/failed
- *      connected => play/rejected => closed
- * @param desc the description for the status.
- */
-SrsPlayer.prototype.on_player_status = function (code, desc) {
-  // ignore.
-};
-
-/**
- * helpers.
- */
-function __srs_find_player(id) {
-  for (var i = 0; i < SrsPlayer.__players.length; i++) {
-    var player = SrsPlayer.__players[i];
-
-    if (player.id != id) {
-      continue;
-    }
-
-    return player;
-  }
-
-  throw new Error('player not found. id=' + id);
-}
-function __srs_on_player_ready(id) {
-  var player = __srs_find_player(id);
-  player.on_player_ready();
-}
-function __srs_on_player_metadata(id, metadata) {
-  var player = __srs_find_player(id);
-
-  // user may override the on_player_metadata,
-  // so set the data before invoke it.
-  player.metadata = metadata;
-
-  player.on_player_metadata(metadata);
-}
-function __srs_on_player_timer(id, time, buffer_length, kbps, fps, rtime) {
-  var player = __srs_find_player(id);
-
-  buffer_length = Math.max(0, buffer_length);
-  buffer_length = Math.min(player.buffer_time, buffer_length);
-
-  time = Math.max(0, time);
-
-  // user may override the on_player_timer,
-  // so set the data before invoke it.
-  player.time = time;
-  player.buffer_length = buffer_length;
-  player.kbps = kbps;
-  player.fps = fps;
-  player.rtime = rtime;
-
-  player.on_player_timer(time, buffer_length, kbps, fps, rtime);
-}
-function __srs_on_player_empty(id, time) {
-  var player = __srs_find_player(id);
-  player.__fluency.on_stream_empty(time);
-  player.on_player_empty(time);
-}
-function __srs_on_player_full(id, time) {
-  var player = __srs_find_player(id);
-  player.__fluency.on_stream_full(time);
-  player.on_player_full(time);
-}
-function __srs_on_player_status(id, code, desc) {
-  var player = __srs_find_player(id);
-  player.on_player_status(code, desc);
-
-  if (code != 'closed') {
-    return;
-  }
-  for (var i = 0; i < SrsPlayer.__players.length; i++) {
-    var player = SrsPlayer.__players[i];
-
-    if (player.id != this.id) {
-      continue;
-    }
-
-    SrsPlayer.__players.splice(i, 1);
-    break;
-  }
-}

+ 0 - 183
src/assets/js/srs.publisher.js

@@ -1,183 +0,0 @@
-/**
- * the SrsPublisher object.
- * @param container the html container id.
- * @param width a float value specifies the width of publisher.
- * @param height a float value specifies the height of publisher.
- * @param private_object [optional] an object that used as private object,
- *       for example, the logic chat object which owner this publisher.
- */
-function SrsPublisher(container, width, height, private_object) {
-  if (!SrsPublisher.__id) {
-    SrsPublisher.__id = 100;
-  }
-  if (!SrsPublisher.__publishers) {
-    SrsPublisher.__publishers = [];
-  }
-
-  SrsPublisher.__publishers.push(this);
-
-  this.private_object = private_object;
-  this.container = container;
-  this.width = width;
-  this.height = height;
-  this.id = SrsPublisher.__id++;
-  this.callbackObj = null;
-
-  // set the values when publish.
-  this.url = null;
-  this.vcodec = {};
-  this.acodec = {};
-
-  // callback set the following values.
-  this.cameras = [];
-  this.microphones = [];
-  this.code = 0;
-
-  // error code defines.
-  this.errors = {
-    100: '无法获取指定的摄像头。', //error_camera_get
-    101: '无法获取指定的麦克风。', //error_microphone_get
-    102: '摄像头为禁用状态,推流时请允许flash访问摄像头。', //error_camera_muted
-    103: '服务器关闭了连接。', //error_connection_closed
-    104: '服务器连接失败。', //error_connection_failed
-    199: '未知错误。',
-  };
-}
-/**
- * user can set some callback, then start the publisher.
- * callbacks:
- *      on_publisher_ready(cameras, microphones):int, when srs publisher ready, user can publish.
- *      on_publisher_error(code, desc):int, when srs publisher error, callback this method.
- *      on_publisher_warn(code, desc):int, when srs publisher warn, callback this method.
- */
-SrsPublisher.prototype.start = function () {
-  // embed the flash.
-  var flashvars = {};
-  flashvars.id = this.id;
-  flashvars.width = this.width;
-  flashvars.height = this.height;
-  flashvars.on_publisher_ready = '__srs_on_publisher_ready';
-  flashvars.on_publisher_error = '__srs_on_publisher_error';
-  flashvars.on_publisher_warn = '__srs_on_publisher_warn';
-
-  var params = {};
-  params.wmode = 'opaque';
-  params.allowFullScreen = 'true';
-  params.allowScriptAccess = 'always';
-
-  var attributes = {};
-
-  var self = this;
-
-  swfobject.embedSWF(
-    'srs_publisher/release/srs_publisher.swf?_version=' +
-      srs_get_version_code(),
-    this.container,
-    this.width,
-    this.height,
-    '11.1.0',
-    'js/AdobeFlashPlayerInstall.swf',
-    flashvars,
-    params,
-    attributes,
-    function (callbackObj) {
-      self.callbackObj = callbackObj;
-    }
-  );
-
-  return this;
-};
-/**
- * publish stream to server.
- * @param url a string indicates the rtmp url to publish.
- * @param vcodec an object contains the video codec info.
- * @param acodec an object contains the audio codec info.
- */
-SrsPublisher.prototype.publish = function (url, vcodec, acodec) {
-  this.stop();
-  SrsPublisher.__publishers.push(this);
-
-  if (url) {
-    this.url = url;
-  }
-  if (vcodec) {
-    this.vcodec = vcodec;
-  }
-  if (acodec) {
-    this.acodec = acodec;
-  }
-
-  this.callbackObj.ref.__publish(
-    this.url,
-    this.width,
-    this.height,
-    this.vcodec,
-    this.acodec
-  );
-};
-SrsPublisher.prototype.stop = function () {
-  for (var i = 0; i < SrsPublisher.__publishers.length; i++) {
-    var player = SrsPublisher.__publishers[i];
-
-    if (player.id != this.id) {
-      continue;
-    }
-
-    SrsPublisher.__publishers.splice(i, 1);
-    break;
-  }
-
-  this.callbackObj.ref.__stop();
-};
-/**
- * when publisher ready.
- * @param cameras a string array contains the names of cameras.
- * @param microphones a string array contains the names of microphones.
- */
-SrsPublisher.prototype.on_publisher_ready = function (cameras, microphones) {};
-/**
- * when publisher error.
- * @code the error code.
- * @desc the error desc message.
- */
-SrsPublisher.prototype.on_publisher_error = function (code, desc) {
-  throw new Error('publisher error. code=' + code + ', desc=' + desc);
-};
-SrsPublisher.prototype.on_publisher_warn = function (code, desc) {
-  throw new Error('publisher warn. code=' + code + ', desc=' + desc);
-};
-function __srs_find_publisher(id) {
-  for (var i = 0; i < SrsPublisher.__publishers.length; i++) {
-    var publisher = SrsPublisher.__publishers[i];
-
-    if (publisher.id != id) {
-      continue;
-    }
-
-    return publisher;
-  }
-
-  throw new Error('publisher not found. id=' + id);
-}
-function __srs_on_publisher_ready(id, cameras, microphones) {
-  var publisher = __srs_find_publisher(id);
-
-  publisher.cameras = cameras;
-  publisher.microphones = microphones;
-
-  publisher.on_publisher_ready(cameras, microphones);
-}
-function __srs_on_publisher_error(id, code) {
-  var publisher = __srs_find_publisher(id);
-
-  publisher.code = code;
-
-  publisher.on_publisher_error(code, publisher.errors['' + code]);
-}
-function __srs_on_publisher_warn(id, code) {
-  var publisher = __srs_find_publisher(id);
-
-  publisher.code = code;
-
-  publisher.on_publisher_warn(code, publisher.errors['' + code]);
-}

+ 0 - 743
src/assets/js/srs.sdk.js

@@ -1,743 +0,0 @@
-//
-// Copyright (c) 2013-2021 Winlin
-//
-// SPDX-License-Identifier: MIT
-//
-
-'use strict';
-
-function SrsError(name, message) {
-  this.name = name;
-  this.message = message;
-  this.stack = new Error().stack;
-}
-SrsError.prototype = Object.create(Error.prototype);
-SrsError.prototype.constructor = SrsError;
-
-// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
-// Async-awat-prmise based SRS RTC Publisher.
-function SrsRtcPublisherAsync() {
-  var self = {};
-
-  // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
-  self.constraints = {
-    audio: true,
-    video: {
-      width: { ideal: 320, max: 576 },
-    },
-  };
-
-  // @see https://github.com/rtcdn/rtcdn-draft
-  // @url The WebRTC url to play with, for example:
-  //      webrtc://r.ossrs.net/live/livestream
-  // or specifies the API port:
-  //      webrtc://r.ossrs.net:11985/live/livestream
-  // or autostart the publish:
-  //      webrtc://r.ossrs.net/live/livestream?autostart=true
-  // or change the app from live to myapp:
-  //      webrtc://r.ossrs.net:11985/myapp/livestream
-  // or change the stream from livestream to mystream:
-  //      webrtc://r.ossrs.net:11985/live/mystream
-  // or set the api server to myapi.domain.com:
-  //      webrtc://myapi.domain.com/live/livestream
-  // or set the candidate(eip) of answer:
-  //      webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
-  // or force to access https API:
-  //      webrtc://r.ossrs.net/live/livestream?schema=https
-  // or use plaintext, without SRTP:
-  //      webrtc://r.ossrs.net/live/livestream?encrypt=false
-  // or any other information, will pass-by in the query:
-  //      webrtc://r.ossrs.net/live/livestream?vhost=xxx
-  //      webrtc://r.ossrs.net/live/livestream?token=xxx
-  self.publish = async function (url) {
-    var conf = self.__internal.prepareUrl(url);
-    self.pc.addTransceiver('audio', { direction: 'sendonly' });
-    self.pc.addTransceiver('video', { direction: 'sendonly' });
-    //self.pc.addTransceiver("video", {direction: "sendonly"});
-    //self.pc.addTransceiver("audio", {direction: "sendonly"});
-
-    if (
-      !navigator.mediaDevices &&
-      window.location.protocol === 'http:' &&
-      window.location.hostname !== 'localhost'
-    ) {
-      throw new SrsError(
-        'HttpsRequiredError',
-        `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`
-      );
-    }
-    var stream = await navigator.mediaDevices.getUserMedia(self.constraints);
-
-    // @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
-    stream.getTracks().forEach(function (track) {
-      self.pc.addTrack(track);
-
-      // Notify about local track when stream is ok.
-      self.ontrack && self.ontrack({ track: track });
-    });
-
-    var offer = await self.pc.createOffer();
-    await self.pc.setLocalDescription(offer);
-    var session = await new Promise(function (resolve, reject) {
-      // @see https://github.com/rtcdn/rtcdn-draft
-      var data = {
-        api: conf.apiUrl,
-        tid: conf.tid,
-        streamurl: conf.streamUrl,
-        clientip: null,
-        sdp: offer.sdp,
-      };
-      console.log('Generated offer: ', data);
-
-      const xhr = new XMLHttpRequest();
-      xhr.onload = function () {
-        if (xhr.readyState !== xhr.DONE) return;
-        if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
-        const data = JSON.parse(xhr.responseText);
-        console.log('Got answer: ', data);
-        return data.code ? reject(xhr) : resolve(data);
-      };
-      xhr.open('POST', conf.apiUrl, true);
-      xhr.setRequestHeader('Content-type', 'application/json');
-      xhr.send(JSON.stringify(data));
-    });
-    await self.pc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: session.sdp })
-    );
-    session.simulator =
-      conf.schema +
-      '//' +
-      conf.urlObject.server +
-      ':' +
-      conf.port +
-      '/rtc/v1/nack/';
-
-    return session;
-  };
-
-  // Close the publisher.
-  self.close = function () {
-    self.pc && self.pc.close();
-    self.pc = null;
-  };
-
-  // The callback when got local stream.
-  // @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
-  self.ontrack = function (event) {
-    // Add track to stream of SDK.
-    self.stream.addTrack(event.track);
-  };
-
-  // Internal APIs.
-  self.__internal = {
-    defaultPath: '/rtc/v1/publish/',
-    prepareUrl: function (webrtcUrl) {
-      var urlObject = self.__internal.parse(webrtcUrl);
-
-      // If user specifies the schema, use it as API schema.
-      var schema = urlObject.user_query.schema;
-      schema = schema ? schema + ':' : window.location.protocol;
-
-      var port = urlObject.port || 1985;
-      if (schema === 'https:') {
-        port = urlObject.port || 443;
-      }
-
-      // @see https://github.com/rtcdn/rtcdn-draft
-      var api = urlObject.user_query.play || self.__internal.defaultPath;
-      if (api.lastIndexOf('/') !== api.length - 1) {
-        api += '/';
-      }
-
-      var apiUrl = schema + '//' + urlObject.server + ':' + port + api;
-      for (var key in urlObject.user_query) {
-        if (key !== 'api' && key !== 'play') {
-          apiUrl += '&' + key + '=' + urlObject.user_query[key];
-        }
-      }
-      // Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
-      apiUrl = apiUrl.replace(api + '&', api + '?');
-
-      var streamUrl = urlObject.url;
-
-      return {
-        apiUrl: apiUrl,
-        streamUrl: streamUrl,
-        schema: schema,
-        urlObject: urlObject,
-        port: port,
-        tid: Number(parseInt(new Date().getTime() * Math.random() * 100))
-          .toString(16)
-          .slice(0, 7),
-      };
-    },
-    parse: function (url) {
-      // @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
-      var a = document.createElement('a');
-      a.href = url
-        .replace('rtmp://', 'http://')
-        .replace('webrtc://', 'http://')
-        .replace('rtc://', 'http://');
-
-      var vhost = a.hostname;
-      var app = a.pathname.substring(1, a.pathname.lastIndexOf('/'));
-      var stream = a.pathname.slice(a.pathname.lastIndexOf('/') + 1);
-
-      // parse the vhost in the params of app, that srs supports.
-      app = app.replace('...vhost...', '?vhost=');
-      if (app.indexOf('?') >= 0) {
-        var params = app.slice(app.indexOf('?'));
-        app = app.slice(0, app.indexOf('?'));
-
-        if (params.indexOf('vhost=') > 0) {
-          vhost = params.slice(params.indexOf('vhost=') + 'vhost='.length);
-          if (vhost.indexOf('&') > 0) {
-            vhost = vhost.slice(0, vhost.indexOf('&'));
-          }
-        }
-      }
-
-      // when vhost equals to server, and server is ip,
-      // the vhost is __defaultVhost__
-      if (a.hostname === vhost) {
-        var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
-        if (re.test(a.hostname)) {
-          vhost = '__defaultVhost__';
-        }
-      }
-
-      // parse the schema
-      var schema = 'rtmp';
-      if (url.indexOf('://') > 0) {
-        schema = url.slice(0, url.indexOf('://'));
-      }
-
-      var port = a.port;
-      if (!port) {
-        // Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
-        if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) {
-          port = url.indexOf(`webrtc://${a.host}:80`) === 0 ? 80 : 443;
-        }
-
-        // Guess by schema.
-        if (schema === 'http') {
-          port = 80;
-        } else if (schema === 'https') {
-          port = 443;
-        } else if (schema === 'rtmp') {
-          port = 1935;
-        }
-      }
-
-      var ret = {
-        url: url,
-        schema: schema,
-        server: a.hostname,
-        port: port,
-        vhost: vhost,
-        app: app,
-        stream: stream,
-      };
-      self.__internal.fill_query(a.search, ret);
-
-      // For webrtc API, we use 443 if page is https, or schema specified it.
-      if (!ret.port) {
-        if (schema === 'webrtc' || schema === 'rtc') {
-          if (ret.user_query.schema === 'https') {
-            ret.port = 443;
-          } else if (window.location.href.indexOf('https://') === 0) {
-            ret.port = 443;
-          } else {
-            // For WebRTC, SRS use 1985 as default API port.
-            ret.port = 1985;
-          }
-        }
-      }
-
-      return ret;
-    },
-    fill_query: function (query_string, obj) {
-      // pure user query object.
-      obj.user_query = {};
-
-      if (query_string.length === 0) {
-        return;
-      }
-
-      // split again for angularjs.
-      if (query_string.indexOf('?') >= 0) {
-        query_string = query_string.split('?')[1];
-      }
-
-      var queries = query_string.split('&');
-      for (var i = 0; i < queries.length; i++) {
-        var elem = queries[i];
-
-        var query = elem.split('=');
-        obj[query[0]] = query[1];
-        obj.user_query[query[0]] = query[1];
-      }
-
-      // alias domain for vhost.
-      if (obj.domain) {
-        obj.vhost = obj.domain;
-      }
-    },
-  };
-
-  self.pc = new RTCPeerConnection(null);
-
-  // To keep api consistent between player and publisher.
-  // @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
-  // @see https://webrtc.org/getting-started/media-devices
-  self.stream = new MediaStream();
-
-  return self;
-}
-
-// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
-// Async-await-promise based SRS RTC Player.
-function SrsRtcPlayerAsync() {
-  var self = {};
-
-  // @see https://github.com/rtcdn/rtcdn-draft
-  // @url The WebRTC url to play with, for example:
-  //      webrtc://r.ossrs.net/live/livestream
-  // or specifies the API port:
-  //      webrtc://r.ossrs.net:11985/live/livestream
-  //      webrtc://r.ossrs.net:80/live/livestream
-  // or autostart the play:
-  //      webrtc://r.ossrs.net/live/livestream?autostart=true
-  // or change the app from live to myapp:
-  //      webrtc://r.ossrs.net:11985/myapp/livestream
-  // or change the stream from livestream to mystream:
-  //      webrtc://r.ossrs.net:11985/live/mystream
-  // or set the api server to myapi.domain.com:
-  //      webrtc://myapi.domain.com/live/livestream
-  // or set the candidate(eip) of answer:
-  //      webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
-  // or force to access https API:
-  //      webrtc://r.ossrs.net/live/livestream?schema=https
-  // or use plaintext, without SRTP:
-  //      webrtc://r.ossrs.net/live/livestream?encrypt=false
-  // or any other information, will pass-by in the query:
-  //      webrtc://r.ossrs.net/live/livestream?vhost=xxx
-  //      webrtc://r.ossrs.net/live/livestream?token=xxx
-  self.play = async function (url) {
-    var conf = self.__internal.prepareUrl(url);
-    self.pc.addTransceiver('audio', { direction: 'recvonly' });
-    self.pc.addTransceiver('video', { direction: 'recvonly' });
-    //self.pc.addTransceiver("video", {direction: "recvonly"});
-    //self.pc.addTransceiver("audio", {direction: "recvonly"});
-
-    var offer = await self.pc.createOffer();
-    await self.pc.setLocalDescription(offer);
-    var session = await new Promise(function (resolve, reject) {
-      // @see https://github.com/rtcdn/rtcdn-draft
-      var data = {
-        api: conf.apiUrl,
-        tid: conf.tid,
-        streamurl: conf.streamUrl,
-        clientip: null,
-        sdp: offer.sdp,
-      };
-      console.log('Generated offer: ', data);
-
-      const xhr = new XMLHttpRequest();
-      xhr.onload = function () {
-        if (xhr.readyState !== xhr.DONE) return;
-        if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
-        const data = JSON.parse(xhr.responseText);
-        console.log('Got answer: ', data);
-        return data.code ? reject(xhr) : resolve(data);
-      };
-      xhr.open('POST', conf.apiUrl, true);
-      xhr.setRequestHeader('Content-type', 'application/json');
-      xhr.send(JSON.stringify(data));
-    });
-    await self.pc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: session.sdp })
-    );
-    session.simulator =
-      conf.schema +
-      '//' +
-      conf.urlObject.server +
-      ':' +
-      conf.port +
-      '/rtc/v1/nack/';
-
-    return session;
-  };
-
-  // Close the player.
-  self.close = function () {
-    self.pc && self.pc.close();
-    self.pc = null;
-  };
-
-  // The callback when got remote track.
-  // Note that the onaddstream is deprecated, @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onaddstream
-  self.ontrack = function (event) {
-    // https://webrtc.org/getting-started/remote-streams
-    self.stream.addTrack(event.track);
-  };
-
-  // Internal APIs.
-  self.__internal = {
-    defaultPath: '/rtc/v1/play/',
-    prepareUrl: function (webrtcUrl) {
-      var urlObject = self.__internal.parse(webrtcUrl);
-
-      // If user specifies the schema, use it as API schema.
-      var schema = urlObject.user_query.schema;
-      schema = schema ? schema + ':' : window.location.protocol;
-
-      var port = urlObject.port || 1985;
-      if (schema === 'https:') {
-        port = urlObject.port || 443;
-      }
-
-      // @see https://github.com/rtcdn/rtcdn-draft
-      var api = urlObject.user_query.play || self.__internal.defaultPath;
-      if (api.lastIndexOf('/') !== api.length - 1) {
-        api += '/';
-      }
-
-      var apiUrl = schema + '//' + urlObject.server + ':' + port + api;
-      for (var key in urlObject.user_query) {
-        if (key !== 'api' && key !== 'play') {
-          apiUrl += '&' + key + '=' + urlObject.user_query[key];
-        }
-      }
-      // Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
-      apiUrl = apiUrl.replace(api + '&', api + '?');
-
-      var streamUrl = urlObject.url;
-
-      return {
-        apiUrl: apiUrl,
-        streamUrl: streamUrl,
-        schema: schema,
-        urlObject: urlObject,
-        port: port,
-        tid: Number(parseInt(new Date().getTime() * Math.random() * 100))
-          .toString(16)
-          .slice(0, 7),
-      };
-    },
-    parse: function (url) {
-      // @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
-      var a = document.createElement('a');
-      a.href = url
-        .replace('rtmp://', 'http://')
-        .replace('webrtc://', 'http://')
-        .replace('rtc://', 'http://');
-
-      var vhost = a.hostname;
-      var app = a.pathname.substring(1, a.pathname.lastIndexOf('/'));
-      var stream = a.pathname.slice(a.pathname.lastIndexOf('/') + 1);
-
-      // parse the vhost in the params of app, that srs supports.
-      app = app.replace('...vhost...', '?vhost=');
-      if (app.indexOf('?') >= 0) {
-        var params = app.slice(app.indexOf('?'));
-        app = app.slice(0, app.indexOf('?'));
-
-        if (params.indexOf('vhost=') > 0) {
-          vhost = params.slice(params.indexOf('vhost=') + 'vhost='.length);
-          if (vhost.indexOf('&') > 0) {
-            vhost = vhost.slice(0, vhost.indexOf('&'));
-          }
-        }
-      }
-
-      // when vhost equals to server, and server is ip,
-      // the vhost is __defaultVhost__
-      if (a.hostname === vhost) {
-        var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
-        if (re.test(a.hostname)) {
-          vhost = '__defaultVhost__';
-        }
-      }
-
-      // parse the schema
-      var schema = 'rtmp';
-      if (url.indexOf('://') > 0) {
-        schema = url.slice(0, url.indexOf('://'));
-      }
-
-      var port = a.port;
-      if (!port) {
-        // Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
-        if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) {
-          port = url.indexOf(`webrtc://${a.host}:80`) === 0 ? 80 : 443;
-        }
-
-        // Guess by schema.
-        if (schema === 'http') {
-          port = 80;
-        } else if (schema === 'https') {
-          port = 443;
-        } else if (schema === 'rtmp') {
-          port = 1935;
-        }
-      }
-
-      var ret = {
-        url: url,
-        schema: schema,
-        server: a.hostname,
-        port: port,
-        vhost: vhost,
-        app: app,
-        stream: stream,
-      };
-      self.__internal.fill_query(a.search, ret);
-
-      // For webrtc API, we use 443 if page is https, or schema specified it.
-      if (!ret.port) {
-        if (schema === 'webrtc' || schema === 'rtc') {
-          if (ret.user_query.schema === 'https') {
-            ret.port = 443;
-          } else if (window.location.href.indexOf('https://') === 0) {
-            ret.port = 443;
-          } else {
-            // For WebRTC, SRS use 1985 as default API port.
-            ret.port = 1985;
-          }
-        }
-      }
-
-      return ret;
-    },
-    fill_query: function (query_string, obj) {
-      // pure user query object.
-      obj.user_query = {};
-
-      if (query_string.length === 0) {
-        return;
-      }
-
-      // split again for angularjs.
-      if (query_string.indexOf('?') >= 0) {
-        query_string = query_string.split('?')[1];
-      }
-
-      var queries = query_string.split('&');
-      for (var i = 0; i < queries.length; i++) {
-        var elem = queries[i];
-
-        var query = elem.split('=');
-        obj[query[0]] = query[1];
-        obj.user_query[query[0]] = query[1];
-      }
-
-      // alias domain for vhost.
-      if (obj.domain) {
-        obj.vhost = obj.domain;
-      }
-    },
-  };
-
-  self.pc = new RTCPeerConnection(null);
-
-  // Create a stream to add track to the stream, @see https://webrtc.org/getting-started/remote-streams
-  self.stream = new MediaStream();
-
-  // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
-  self.pc.ontrack = function (event) {
-    if (self.ontrack) {
-      self.ontrack(event);
-    }
-  };
-
-  return self;
-}
-
-// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
-// Async-awat-prmise based SRS RTC Publisher by WHIP.
-function SrsRtcWhipWhepAsync() {
-  var self = {};
-
-  // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
-  self.constraints = {
-    audio: true,
-    video: {
-      width: { ideal: 320, max: 576 },
-    },
-  };
-
-  // See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
-  // @url The WebRTC url to publish with, for example:
-  //      http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream
-  self.publish = async function (url) {
-    if (url.indexOf('/whip/') === -1)
-      throw new Error(`invalid WHIP url ${url}`);
-
-    self.pc.addTransceiver('audio', { direction: 'sendonly' });
-    self.pc.addTransceiver('video', { direction: 'sendonly' });
-
-    if (
-      !navigator.mediaDevices &&
-      window.location.protocol === 'http:' &&
-      window.location.hostname !== 'localhost'
-    ) {
-      throw new SrsError(
-        'HttpsRequiredError',
-        `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`
-      );
-    }
-    var stream = await navigator.mediaDevices.getUserMedia(self.constraints);
-
-    // @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
-    stream.getTracks().forEach(function (track) {
-      self.pc.addTrack(track);
-
-      // Notify about local track when stream is ok.
-      self.ontrack && self.ontrack({ track: track });
-    });
-
-    var offer = await self.pc.createOffer();
-    await self.pc.setLocalDescription(offer);
-    const answer = await new Promise(function (resolve, reject) {
-      console.log('Generated offer: ', offer);
-
-      const xhr = new XMLHttpRequest();
-      xhr.onload = function () {
-        if (xhr.readyState !== xhr.DONE) return;
-        if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
-        const data = xhr.responseText;
-        console.log('Got answer: ', data);
-        return data.code ? reject(xhr) : resolve(data);
-      };
-      xhr.open('POST', url, true);
-      xhr.setRequestHeader('Content-type', 'application/sdp');
-      xhr.send(offer.sdp);
-    });
-    await self.pc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: answer })
-    );
-
-    return self.__internal.parseId(url, offer.sdp, answer);
-  };
-
-  // See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
-  // @url The WebRTC url to play with, for example:
-  //      http://localhost:1985/rtc/v1/whip-play/?app=live&stream=livestream
-  self.play = async function (url) {
-    if (url.indexOf('/whip-play/') === -1 && url.indexOf('/whep/') === -1)
-      throw new Error(`invalid WHEP url ${url}`);
-
-    self.pc.addTransceiver('audio', { direction: 'recvonly' });
-    self.pc.addTransceiver('video', { direction: 'recvonly' });
-
-    var offer = await self.pc.createOffer();
-    await self.pc.setLocalDescription(offer);
-    const answer = await new Promise(function (resolve, reject) {
-      console.log('Generated offer: ', offer);
-
-      const xhr = new XMLHttpRequest();
-      xhr.onload = function () {
-        if (xhr.readyState !== xhr.DONE) return;
-        if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
-        const data = xhr.responseText;
-        console.log('Got answer: ', data);
-        return data.code ? reject(xhr) : resolve(data);
-      };
-      xhr.open('POST', url, true);
-      xhr.setRequestHeader('Content-type', 'application/sdp');
-      xhr.send(offer.sdp);
-    });
-    await self.pc.setRemoteDescription(
-      new RTCSessionDescription({ type: 'answer', sdp: answer })
-    );
-
-    return self.__internal.parseId(url, offer.sdp, answer);
-  };
-
-  // Close the publisher.
-  self.close = function () {
-    self.pc && self.pc.close();
-    self.pc = null;
-  };
-
-  // The callback when got local stream.
-  // @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
-  self.ontrack = function (event) {
-    // Add track to stream of SDK.
-    self.stream.addTrack(event.track);
-  };
-
-  self.pc = new RTCPeerConnection(null);
-
-  // To keep api consistent between player and publisher.
-  // @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
-  // @see https://webrtc.org/getting-started/media-devices
-  self.stream = new MediaStream();
-
-  // Internal APIs.
-  self.__internal = {
-    parseId: (url, offer, answer) => {
-      let sessionid = offer.substr(
-        offer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length
-      );
-      sessionid = sessionid.substr(0, sessionid.indexOf('\n') - 1) + ':';
-      sessionid += answer.substr(
-        answer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length
-      );
-      sessionid = sessionid.substr(0, sessionid.indexOf('\n'));
-
-      const a = document.createElement('a');
-      a.href = url;
-      return {
-        sessionid: sessionid, // Should be ice-ufrag of answer:offer.
-        simulator: a.protocol + '//' + a.host + '/rtc/v1/nack/',
-      };
-    },
-  };
-
-  // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
-  self.pc.ontrack = function (event) {
-    if (self.ontrack) {
-      self.ontrack(event);
-    }
-  };
-
-  return self;
-}
-
-// Format the codec of RTCRtpSender, kind(audio/video) is optional filter.
-// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs#getting_the_supported_codecs
-function SrsRtcFormatSenders(senders, kind) {
-  var codecs = [];
-  senders.forEach(function (sender) {
-    var params = sender.getParameters();
-    params &&
-      params.codecs &&
-      params.codecs.forEach(function (c) {
-        if (kind && sender.track.kind !== kind) {
-          return;
-        }
-
-        if (
-          c.mimeType.indexOf('/red') > 0 ||
-          c.mimeType.indexOf('/rtx') > 0 ||
-          c.mimeType.indexOf('/fec') > 0
-        ) {
-          return;
-        }
-
-        var s = '';
-
-        s += c.mimeType.replace('audio/', '').replace('video/', '');
-        s += ', ' + c.clockRate + 'HZ';
-        if (sender.track.kind === 'audio') {
-          s += ', channels: ' + c.channels;
-        }
-        s += ', pt: ' + c.payloadType;
-
-        codecs.push(s);
-      });
-  });
-  return codecs.join(', ');
-}

+ 50 - 17
src/layout/head/index.vue

@@ -9,10 +9,13 @@
       </div>
       </div>
       <div class="nav">
       <div class="nav">
         <div
         <div
-          v-for="(item, index) in list"
+          v-for="(item, index) in pushList"
           :key="index"
           :key="index"
-          class="item"
-          @click="item.routeName && router.push({ name: item.routeName })"
+          :class="{
+            item: 1,
+            active: router.currentRoute.value.name === item.routerName,
+          }"
+          @click="goPushPage(item.routerName)"
         >
         >
           {{ item.title }}
           {{ item.title }}
         </div>
         </div>
@@ -36,7 +39,7 @@
       <div
       <div
         v-if="route.path === '/'"
         v-if="route.path === '/'"
         class="start"
         class="start"
-        @click="goPushPage"
+        @click="goPushPage(routerName.webrtcPush)"
       >
       >
         我要开播
         我要开播
       </div>
       </div>
@@ -49,22 +52,20 @@ import { openToTarget } from 'billd-utils';
 import { ref } from 'vue';
 import { ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { useRoute, useRouter } from 'vue-router';
 
 
+import { routerName } from '@/router';
+
 const router = useRouter();
 const router = useRouter();
 const route = useRoute();
 const route = useRoute();
 
 
-const list = ref([
-  { ico: '', title: '一对一直播' },
-  { title: '一对多直播' },
-  { title: '直播拉流' },
-  { title: 'mesh模型' },
-  { title: 'sfu模型' },
-  { title: 'test1', routeName: 'test1' },
-  { title: 'bilibiliPush', routeName: 'bilibiliPush' },
-  { title: 'srsDemoOne', routeName: 'srsDemoOne' },
+const pushList = ref([
+  // { title: 'Webrtc Pull', routerName: 'webrtcPull' },
+  // { title: 'SRS WebRTC Pull', routerName: 'srsWebRtcPull' },
+  { title: 'Webrtc Push', routerName: routerName.webrtcPush },
+  { title: 'SRS WebRTC Push', routerName: routerName.srsWebRtcPush },
 ]);
 ]);
 
 
-function goPushPage() {
-  const url = router.resolve('/push');
+function goPushPage(routerName: string) {
+  const url = router.resolve({ name: routerName });
   openToTarget(url.href);
   openToTarget(url.href);
 }
 }
 </script>
 </script>
@@ -94,7 +95,38 @@ function goPushPage() {
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
       .item {
       .item {
-        margin-right: 20px;
+        padding: 0 10px;
+        cursor: pointer;
+        position: relative;
+        &.active {
+          &::after {
+            content: '';
+            position: absolute;
+            bottom: -6px;
+            left: 50%;
+            transform: translateX(-50%);
+            width: 50%;
+            height: 2px;
+            background-color: red;
+            transition: all 0.1s ease;
+          }
+        }
+        &::after {
+          content: '';
+          position: absolute;
+          bottom: -6px;
+          left: 50%;
+          transform: translateX(-50%);
+          width: 0px;
+          height: 2px;
+          background-color: red;
+          transition: all 0.1s ease;
+        }
+        &:hover {
+          &::after {
+            width: 50%;
+          }
+        }
       }
       }
     }
     }
   }
   }
@@ -118,7 +150,8 @@ function goPushPage() {
   .right {
   .right {
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
-    padding-right: 20px;
+    margin-right: 20px;
+
     .github {
     .github {
       margin-right: 20px;
       margin-right: 20px;
     }
     }

+ 0 - 2
src/main.ts

@@ -1,5 +1,3 @@
-import '@/assets/js/aa';
-
 import './main.scss';
 import './main.scss';
 import './showBilldVersion';
 import './showBilldVersion';
 // import 'windi.css'; // windicss-webpack-plugin会解析windi.css这个MODULE_ID
 // import 'windi.css'; // windicss-webpack-plugin会解析windi.css这个MODULE_ID

+ 504 - 0
src/network/srsWebRtc.ts

@@ -0,0 +1,504 @@
+import browserTool from 'browser-tool';
+
+import { useNetworkStore } from '@/store/network';
+
+function prettierInfo(
+  str: string,
+  data: {
+    browser: string;
+  },
+  type?: 'log' | 'warn' | 'error',
+  ...args
+) {
+  console[type || 'log'](
+    `${new Date().toLocaleString()},${data.browser}浏览器,${str}`,
+    ...args
+  );
+}
+
+export class SRSWebRTCClass {
+  roomId = '-1';
+
+  peerConnection: RTCPeerConnection | null = null;
+  dataChannel: RTCDataChannel | null = null;
+
+  candidateFlag = false;
+
+  sender?: RTCRtpTransceiver;
+
+  getStatsSetIntervalDelay = 1000;
+  getStatsSetIntervalTimer;
+
+  // getStatsSetIntervalDelay是1秒的话,forceINumsMax是3,就代表一直卡了3秒。
+  forceINums = 0; // 发送forceI次数
+  forceINumsMax = 3; // 最多发送几次forceI
+
+  preFramesDecoded = -1; // 上一帧
+
+  browser: {
+    device: string;
+    language: string;
+    engine: string;
+    browser: string;
+    system: string;
+    systemVersion: string;
+    platform: string;
+    isWebview: boolean;
+    isBot: boolean;
+    version: string;
+  };
+
+  rtcStatus = {
+    joined: false, // true代表成功,false代表失败
+    icecandidate: false, // true代表成功,false代表失败
+    createOffer: false, // true代表成功,false代表失败
+    setLocalDescription: false, // true代表成功,false代表失败
+    answer: false, // true代表成功,false代表失败
+    setRemoteDescription: false, // true代表成功,false代表失败
+    addStream: false, // true代表成功,false代表失败
+    loadstart: false, // true代表成功,false代表失败
+    loadedmetadata: false, // true代表成功,false代表失败
+  };
+
+  localDescription: any;
+
+  constructor({ roomId }: { roomId: string }) {
+    this.roomId = roomId;
+    this.browser = browserTool();
+    this.createPeerConnection();
+    this.update();
+  }
+
+  addTrack = ({ track, stream, direction }) => {
+    console.warn('addTrackaddTrack', track, stream);
+    this.sender = this.peerConnection?.addTransceiver(track, {
+      streams: [stream],
+      direction,
+    });
+    // this.peerConnection?.addTrack(track, stream);
+  };
+
+  addStream = (stream) => {
+    console.warn('addStreamaddStream', stream);
+    if (!this.peerConnection) return;
+    this.rtcStatus.addStream = true;
+    this.update();
+    document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject = stream;
+    prettierInfo('addStream成功', { browser: this.browser.browser }, 'warn');
+  };
+
+  initStreamEvent = () => {
+    console.warn(`${this.roomId},开始监听pc的addstream`);
+    this.peerConnection?.addEventListener('addstream', (event: any) => {
+      console.warn(`${this.roomId},pc收到addstream事件`, event, event.stream);
+      this.addStream(event.stream);
+    });
+
+    console.warn(`${this.roomId},开始监听pc的ontrack`);
+    this.peerConnection?.addEventListener('ontrack', (event: any) => {
+      console.warn(`${this.roomId},pc收到ontrack事件`, event);
+      this.addStream(event.streams[0]);
+    });
+
+    console.warn(`${this.roomId},开始监听pc的addtrack`);
+    this.peerConnection?.addEventListener('addtrack', (event: any) => {
+      console.warn(`${this.roomId},pc收到addtrack事件`, event);
+    });
+
+    console.warn(`${this.roomId},开始监听pc的track`);
+    this.peerConnection?.addEventListener('track', (event: any) => {
+      console.warn(`${this.roomId},pc收到track事件`, event);
+      this.addStream(event.streams[0]);
+      // document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
+      //   event.streams[0];
+    });
+  };
+
+  // 创建offer
+  createOffer = async () => {
+    if (!this.peerConnection) return;
+    prettierInfo('createOffer开始', { browser: this.browser.browser }, 'warn');
+    try {
+      const description = await this.peerConnection.createOffer();
+      this.localDescription = description;
+      this.rtcStatus.createOffer = true;
+      this.update();
+      prettierInfo(
+        'createOffer成功',
+        { browser: this.browser.browser },
+        'warn'
+      );
+      console.log('createOffer', description);
+      return description;
+    } catch (error) {
+      prettierInfo(
+        'createOffer失败',
+        { browser: this.browser.browser },
+        'error'
+      );
+      console.log(error);
+    }
+  };
+
+  // 设置本地描述
+  setLocalDescription = async (description) => {
+    if (!this.peerConnection) return;
+    prettierInfo(
+      'setLocalDescription开始',
+      { browser: this.browser.browser },
+      'warn'
+    );
+    try {
+      await this.peerConnection.setLocalDescription(description);
+      this.rtcStatus.setLocalDescription = true;
+      this.update();
+      prettierInfo(
+        'setLocalDescription成功',
+        { browser: this.browser.browser },
+        'warn'
+      );
+      console.log(description);
+    } catch (error) {
+      prettierInfo(
+        'setLocalDescription失败',
+        { browser: this.browser.browser },
+        'error'
+      );
+      console.log('setLocalDescription', description);
+      console.log(error);
+    }
+  };
+
+  // 设置远端描述
+  setRemoteDescription = async (description) => {
+    if (!this.peerConnection) return;
+    prettierInfo(
+      `setRemoteDescription开始`,
+      { browser: this.browser.browser },
+      'warn'
+    );
+    try {
+      await this.peerConnection.setRemoteDescription(
+        new RTCSessionDescription(description)
+      );
+      this.rtcStatus.setRemoteDescription = true;
+      this.update();
+      prettierInfo(
+        'setRemoteDescription成功',
+        { browser: this.browser.browser },
+        'warn'
+      );
+      console.log(description);
+    } catch (error) {
+      prettierInfo(
+        'setRemoteDescription失败',
+        { browser: this.browser.browser },
+        'error'
+      );
+      console.log('setRemoteDescription', description);
+      console.log(error);
+    }
+  };
+
+  // 创建连接
+  startConnect = () => {
+    if (!this.peerConnection) return;
+    console.warn(`${this.roomId},开始监听pc的icecandidate`);
+    this.peerConnection.addEventListener('icecandidate', (event) => {
+      prettierInfo(
+        'pc收到icecandidate',
+        { browser: this.browser.browser },
+        'warn'
+      );
+    });
+
+    this.initStreamEvent();
+
+    // iceconnectionstatechange
+    this.peerConnection.addEventListener(
+      'iceconnectionstatechange',
+      (event: any) => {
+        // https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/connectionState
+        const iceConnectionState = event.currentTarget.iceConnectionState;
+        console.log(
+          'pc收到iceconnectionstatechange',
+          // eslint-disable-next-line
+          `iceConnectionState:${iceConnectionState}`,
+          event
+        );
+        if (iceConnectionState === 'connected') {
+          // ICE 代理至少对每个候选发现了一个可用的连接,此时仍然会继续测试远程候选以便发现更优的连接。同时可能在继续收集候选。
+          console.warn('iceConnectionState:connected', event);
+        }
+        if (iceConnectionState === 'completed') {
+          // ICE 代理已经发现了可用的连接,不再测试远程候选。
+          console.warn('iceConnectionState:completed', event);
+        }
+        if (iceConnectionState === 'failed') {
+          // ICE 候选测试了所有远程候选没有发现匹配的候选。也可能有些候选中发现了一些可用连接。
+          console.error('iceConnectionState:failed', event);
+        }
+        if (iceConnectionState === 'disconnected') {
+          // 测试不再活跃,这可能是一个暂时的状态,可以自我恢复。
+          console.error('iceConnectionState:disconnected', event);
+        }
+        if (iceConnectionState === 'closed') {
+          // ICE 代理关闭,不再应答任何请求。
+          console.error('iceConnectionState:closed', event);
+        }
+      }
+    );
+
+    // connectionstatechange
+    this.peerConnection.addEventListener(
+      'connectionstatechange',
+      (event: any) => {
+        const connectionState = event.currentTarget.connectionState;
+        console.log(
+          'pc收到connectionstatechange',
+          // eslint-disable-next-line
+          `connectionState:${connectionState}`,
+          event
+        );
+        if (connectionState === 'connected') {
+          // 表示每一个 ICE 连接要么正在使用(connected 或 completed 状态),要么已被关闭(closed 状态);并且,至少有一个连接处于 connected 或 completed 状态。
+          console.warn('connectionState:connected');
+        }
+        if (connectionState === 'disconnected') {
+          // 表示至少有一个 ICE 连接处于 disconnected 状态,并且没有连接处于 failed、connecting 或 checking 状态。
+          console.error('connectionState:disconnected');
+        }
+        if (connectionState === 'closed') {
+          // 表示 RTCPeerConnection 已关闭。
+          console.error('connectionState:closed');
+        }
+        if (connectionState === 'failed') {
+          // 表示至少有一个 ICE 连接处于 failed 的状态。
+          console.error('connectionState:failed');
+        }
+      }
+    );
+  };
+
+  // 创建对等连接
+  createPeerConnection = () => {
+    if (!window.RTCPeerConnection) {
+      console.error('当前环境不支持RTCPeerConnection!');
+      alert('当前环境不支持RTCPeerConnection!');
+      return;
+    }
+    if (!this.peerConnection) {
+      this.peerConnection = new RTCPeerConnection();
+      this.startConnect();
+      this.update();
+    }
+  };
+
+  handleWebRtcError = () => {
+    this.getStatsSetIntervalTimer = setInterval(() => {
+      this.peerConnection
+        ?.getStats()
+        .then((res) => {
+          let isBlack = false;
+          let currFramesDecoded = -1;
+          res.forEach((report: RTCInboundRtpStreamStats) => {
+            // 不能结构report的值,不然如果卡主之后,report的值就一直都是
+            // const { type, kind, framesDecoded } = report;
+            const data = {
+              type: report.type,
+              kind: report.kind,
+              framesDecoded: report.framesDecoded,
+              decoderImplementation: report.decoderImplementation,
+              isChrome: false,
+              isSafari: false,
+              other: '',
+            };
+            if (this.browser.browser === 'safari') {
+              data.isSafari = true;
+            } else if (this.browser.browser === 'chrome') {
+              data.isChrome = true;
+            } else {
+              data.other = this.browser.browser;
+            }
+            const isStopFlag = this.getCurrentFramesDecoded(data);
+            const isBlackFlag = this.isBlackScreen(data);
+            if (isStopFlag !== false) {
+              currFramesDecoded = isStopFlag;
+            }
+            if (isBlackFlag !== false) {
+              isBlack = isBlackFlag;
+            }
+          });
+          /** 处理视频流卡主 */
+          const handleStreamStop = () => {
+            // console.error(
+            //   `上一帧:${this.preFramesDecoded},当前帧:${currFramesDecoded},forceINums:${this.forceINums}`
+            // );
+            if (this.preFramesDecoded === currFramesDecoded) {
+              if (this.forceINums >= this.forceINumsMax) {
+                prettierInfo(
+                  '超过forceI次数,提示更换网络',
+                  { browser: this.browser.browser },
+                  'warn'
+                );
+                this.forceINums = 0;
+              } else {
+                this.forceINums += 1;
+                prettierInfo(
+                  `当前视频流卡主了,主动刷新云手机(${this.forceINums}/${this.forceINumsMax})`,
+                  { browser: this.browser.browser },
+                  'warn'
+                );
+              }
+            } else {
+              this.forceINums = 0;
+              // console.warn('视频流没有卡主');
+            }
+            this.preFramesDecoded = currFramesDecoded;
+          };
+          /** 处理黑屏 */
+          const handleBlackScreen = () => {
+            if (isBlack) {
+              prettierInfo(
+                '黑屏了',
+                { browser: this.browser.browser },
+                'error'
+              );
+            }
+            // else {
+            //   console.warn('没有黑屏');
+            // }
+          };
+          /** 处理rtcStatus */
+          const handleRtcStatus = () => {
+            const res = this.rtcStatusIsOk();
+            const length = Object.keys(res).length;
+            if (length) {
+              prettierInfo(
+                `rtcStatus错误:${Object.keys(res).join()}`,
+                { browser: this.browser.browser },
+                'error'
+              );
+            }
+            //  else {
+            //   console.warn('rtcStatus正常');
+            // }
+          };
+          handleStreamStop();
+          handleBlackScreen();
+          handleRtcStatus();
+          // if (!networkStore.errorCode.length) {
+          //   networkStore.setShowErrorModal(false);
+          // }
+        })
+        .catch((err) => {
+          console.error(new Date().toLocaleString(), 'getStatsErr', err);
+          // networkStore.setErrorCode(frontendErrorCode.getStatsErr);
+          // networkStore.setShowErrorModal(true);
+        });
+    }, this.getStatsSetIntervalDelay);
+  };
+
+  /** rtcStatus是否都是true了 */
+  rtcStatusIsOk = () => {
+    const res = {};
+    const status = this.rtcStatus;
+    Object.keys(status).forEach((key) => {
+      if (!status[key]) {
+        res[key] = false;
+      }
+    });
+    return res;
+  };
+
+  /** 当前是否黑屏,true代表黑屏了,false代表没有黑屏 */
+  isBlackScreen = ({
+    type,
+    kind,
+    framesDecoded,
+    decoderImplementation,
+    isSafari = false,
+    isChrome = false,
+  }) => {
+    // https://blog.csdn.net/weixin_44523653/article/details/127414387
+    // console.warn(
+    //   // eslint-disable-next-line
+    //   `type:${type},kind:${kind},framesDecoded:${framesDecoded},decoderImplementation:${decoderImplementation}`
+    // );
+    if (isSafari) {
+      if (
+        type === 'inbound-rtp' &&
+        kind === 'video' &&
+        // framesDecoded等于0代表黑屏
+        framesDecoded === 0
+      ) {
+        return true;
+      }
+    } else if (isChrome) {
+      if (
+        (type === 'track' || type === 'inbound-rtp') &&
+        kind === 'video' &&
+        // framesDecoded等于0代表黑屏
+        framesDecoded === 0
+      ) {
+        return true;
+      }
+    } else {
+      if (
+        (type === 'track' || type === 'inbound-rtp') &&
+        kind === 'video' &&
+        // framesDecoded等于0代表黑屏
+        framesDecoded === 0
+      ) {
+        // 安卓的qq浏览器适用这个黑屏判断
+        return true;
+      }
+    }
+
+    return false;
+  };
+
+  /** 获取当前帧 */
+  getCurrentFramesDecoded = ({
+    type,
+    kind,
+    framesDecoded = this.preFramesDecoded,
+    isSafari = false,
+    isChrome = false,
+  }) => {
+    if (isSafari) {
+      if (type === 'inbound-rtp' && kind === 'video') {
+        return framesDecoded;
+      }
+    } else if (isChrome) {
+      if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
+        return framesDecoded;
+      }
+    } else {
+      if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
+        // 安卓的qq浏览器适用这个卡屏判断
+        return framesDecoded;
+      }
+    }
+    return false;
+  };
+
+  // 手动关闭webrtc连接
+  close = () => {
+    console.warn(`${new Date().toLocaleString()},手动关闭webrtc连接`);
+    if (this.sender?.sender) {
+      this.peerConnection?.removeTrack(this.sender?.sender);
+    }
+    this.peerConnection?.close();
+    this.dataChannel?.close();
+    this.peerConnection = null;
+    this.dataChannel = null;
+    this.update();
+  };
+
+  // 更新store
+  update = () => {
+    const networkStore = useNetworkStore();
+    networkStore.updateRtcMap(this.roomId, this);
+  };
+}

+ 16 - 12
src/network/webRtc.ts

@@ -118,6 +118,7 @@ export class WebRTCClass {
     console.warn('addTrackaddTrack', track, stream);
     console.warn('addTrackaddTrack', track, stream);
     this.sender = this.peerConnection?.addTransceiver(track, {
     this.sender = this.peerConnection?.addTransceiver(track, {
       streams: [stream],
       streams: [stream],
+      direction: 'sendonly',
     });
     });
     // this.peerConnection?.addTrack(track, stream);
     // this.peerConnection?.addTrack(track, stream);
   };
   };
@@ -318,9 +319,12 @@ export class WebRTCClass {
     try {
     try {
       const description = await this.peerConnection.createOffer({
       const description = await this.peerConnection.createOffer({
         iceRestart: true,
         iceRestart: true,
-        offerToReceiveAudio: true,
-        offerToReceiveVideo: true,
       });
       });
+      // const description = await this.peerConnection.createOffer({
+      //   iceRestart: true,
+      //   offerToReceiveAudio: true,
+      //   offerToReceiveVideo: true,
+      // });
       this.localDescription = description;
       this.localDescription = description;
       this.rtcStatus.createOffer = true;
       this.rtcStatus.createOffer = true;
       this.update();
       this.update();
@@ -535,16 +539,16 @@ export class WebRTCClass {
     }
     }
     if (!this.peerConnection) {
     if (!this.peerConnection) {
       this.peerConnection = new RTCPeerConnection({
       this.peerConnection = new RTCPeerConnection({
-        iceServers: [
-          // {
-          //   urls: 'stun:stun.l.google.com:19302',
-          // },
-          {
-            urls: 'turn:hsslive.cn:3478',
-            username: 'hss',
-            credential: '123456',
-          },
-        ],
+        // iceServers: [
+        //   // {
+        //   //   urls: 'stun:stun.l.google.com:19302',
+        //   // },
+        //   {
+        //     urls: 'turn:hsslive.cn:3478',
+        //     username: 'hss',
+        //     credential: '123456',
+        //   },
+        // ],
       });
       });
       // this.dataChannel =
       // this.dataChannel =
       //   this.peerConnection.createDataChannel('MessageChannel');
       //   this.peerConnection.createDataChannel('MessageChannel');

+ 40 - 17
src/router/index.ts

@@ -4,6 +4,17 @@ import Layout from '@/layout/index.vue';
 
 
 import type { RouteRecordRaw } from 'vue-router';
 import type { RouteRecordRaw } from 'vue-router';
 
 
+export const routerName = {
+  home: 'home',
+  notFound: 'notFound',
+  bilibiliPush: 'bilibiliPush',
+  test1: 'test1',
+  webrtcPush: 'webrtcPush',
+  webrtcPull: 'webrtcPull',
+  srsWebRtcPush: 'srsWebRtcPush',
+  srsWebRtcPull: 'srsWebRtcPull',
+};
+
 // 默认路由
 // 默认路由
 export const defaultRoutes: RouteRecordRaw[] = [
 export const defaultRoutes: RouteRecordRaw[] = [
   {
   {
@@ -11,40 +22,52 @@ export const defaultRoutes: RouteRecordRaw[] = [
     component: Layout,
     component: Layout,
     children: [
     children: [
       {
       {
-        name: 'home',
+        name: routerName.home,
         path: '/',
         path: '/',
         component: () => import('@/views/home/index.vue'),
         component: () => import('@/views/home/index.vue'),
       },
       },
       {
       {
-        name: 'push',
-        path: '/push',
-        component: () => import('@/views/push/index.vue'),
-      },
-      {
-        name: 'pull',
-        path: '/:roomId',
-        component: () => import('@/views/pull/index.vue'),
-      },
-      {
-        name: 'bilibiliPush',
+        name: routerName.bilibiliPush,
         path: '/bilibiliPush',
         path: '/bilibiliPush',
         component: () => import('@/views/bilibiliPush/index.vue'),
         component: () => import('@/views/bilibiliPush/index.vue'),
       },
       },
       {
       {
-        name: 'test1',
+        name: routerName.test1,
         path: '/test1',
         path: '/test1',
         component: () => import('@/views/test1/index.vue'),
         component: () => import('@/views/test1/index.vue'),
       },
       },
       {
       {
-        name: 'srsDemoOne',
-        path: '/srs-demo1',
-        component: () => import('@/views/srs-demo1/index.vue'),
+        name: routerName.webrtcPush,
+        path: '/webrtc-push',
+        component: () => import('@/views/webrtc-push/index.vue'),
+      },
+      {
+        name: routerName.webrtcPull,
+        path: '/webrtc-pull/:roomId',
+        component: () => import('@/views/webrtc-pull/index.vue'),
+      },
+      {
+        name: routerName.srsWebRtcPush,
+        path: '/srs-webrtc-push',
+        component: () => import('@/views/srs-webrtc-push/index.vue'),
+      },
+      {
+        name: routerName.srsWebRtcPull,
+        path: '/srs-webrtc-pull/:roomId',
+        component: () => import('@/views/srs-webrtc-pull/index.vue'),
       },
       },
     ],
     ],
   },
   },
 ];
 ];
 const router = createRouter({
 const router = createRouter({
-  routes: defaultRoutes,
+  routes: [
+    ...defaultRoutes,
+    {
+      path: '/:pathMatch(.*)*',
+      name: routerName.notFound,
+      component: () => import('@/views/notFound.vue'),
+    },
+  ],
   history: createWebHistory(),
   history: createWebHistory(),
 });
 });
 
 

+ 110 - 0
src/utils/request.ts

@@ -0,0 +1,110 @@
+import axios, { Axios, AxiosRequestConfig } from 'axios';
+
+export interface MyAxiosPromise<T = any>
+  extends Promise<{
+    code: number;
+    data: T;
+    msg: string;
+    message?: string;
+  }> {}
+
+interface MyAxiosInstance extends Axios {
+  // eslint-disable-next-line
+  (config: AxiosRequestConfig): MyAxiosPromise;
+  // eslint-disable-next-line
+  (url: string, config?: AxiosRequestConfig): MyAxiosPromise;
+}
+
+class MyAxios {
+  // axios 实例
+  instance: MyAxiosInstance;
+
+  constructor(config: AxiosRequestConfig) {
+    // @ts-ignore
+    this.instance = axios.create(config);
+
+    // 请求拦截器
+    this.instance.interceptors.request.use(
+      (cfg) => {
+        return cfg;
+      },
+      (error) => {
+        console.log(error);
+        return Promise.reject(error);
+      }
+    );
+
+    // 响应拦截器
+    this.instance.interceptors.response.use(
+      (response) => {
+        console.log('response.config.url', response.config.url);
+        console.log('response.data', response.data);
+        return response.data;
+      },
+      (error) => {
+        console.log('响应拦截到错误', error);
+        if (error.message.indexOf('timeout') !== -1) {
+          console.error(error.message);
+          return;
+        }
+        const statusCode = error.response.status as number;
+        const errorResponseData = error.response.data;
+        const whiteList = ['400', '401', '403', '404'];
+        if (error.response) {
+          if (!whiteList.includes(`${statusCode}`)) {
+            if (statusCode === 500) {
+              let msg = errorResponseData.message;
+              if (errorResponseData?.errorCode) {
+                msg = errorResponseData.error;
+              }
+              console.error(msg);
+              return Promise.reject(msg);
+            }
+            console.error(error.message);
+            return Promise.reject(error);
+          }
+          if (statusCode === 400) {
+            console.error(errorResponseData.message);
+            return Promise.reject(errorResponseData);
+          }
+          if (statusCode === 401) {
+            console.error(errorResponseData.message);
+            return Promise.reject(errorResponseData);
+          }
+          if (statusCode === 403) {
+            console.error(errorResponseData.message);
+            return Promise.reject(errorResponseData);
+          }
+          if (statusCode === 404) {
+            console.error(errorResponseData.message);
+            return Promise.reject(errorResponseData);
+          }
+        } else {
+          // 请求超时没有response
+          console.error(error.message);
+          return Promise.reject(error.message);
+        }
+      }
+    );
+  }
+
+  get<T = any>(
+    url: string,
+    config?: AxiosRequestConfig<any> | undefined
+  ): MyAxiosPromise<T> {
+    return this.instance.get(url, config);
+  }
+
+  post<T = any>(
+    url: string,
+    data?: {} | undefined,
+    config?: AxiosRequestConfig
+  ): MyAxiosPromise<T> {
+    return this.instance.post(url, data, config);
+  }
+}
+
+export default new MyAxios({
+  // baseURL:'/'
+  timeout: 1000 * 5,
+});

+ 60 - 19
src/views/home/index.vue

@@ -18,10 +18,18 @@
         <div
         <div
           v-for="(item, index) in liveRoomList"
           v-for="(item, index) in liveRoomList"
           :key="index"
           :key="index"
-          :class="{ item: 1, active: item.roomId === currentRoom.roomId }"
+          :class="{ item: 1, active: item.roomId === currentRoom?.roomId }"
           :style="{ backgroundImage: `url(${item.roomId})` }"
           :style="{ backgroundImage: `url(${item.roomId})` }"
           @click="currentRoom = item"
           @click="currentRoom = item"
         >
         >
+          <div
+            class="border"
+            :style="{ opacity: item.roomId === currentRoom?.roomId ? 1 : 0 }"
+          ></div>
+          <div
+            v-if="item.roomId === currentRoom?.roomId"
+            class="triangle"
+          ></div>
           <div class="txt">{{ item.roomName }}</div>
           <div class="txt">{{ item.roomName }}</div>
         </div>
         </div>
       </div>
       </div>
@@ -36,10 +44,12 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import axios from 'axios';
 import { onMounted, ref } from 'vue';
 import { onMounted, ref } from 'vue';
 import { useRouter } from 'vue-router';
 import { useRouter } from 'vue-router';
 
 
+import { fetchLiveList } from '@/api/live';
+import { routerName } from '@/router';
+
 const router = useRouter();
 const router = useRouter();
 const liveRoomList = ref<{ roomId: string; roomName: string }[]>([
 const liveRoomList = ref<{ roomId: string; roomName: string }[]>([
   // {
   // {
@@ -74,34 +84,36 @@ const liveRoomList = ref<{ roomId: string; roomName: string }[]>([
   // },
   // },
 ]);
 ]);
 
 
-const currentRoom = ref();
+const currentRoom = ref<{
+  roomId: string;
+  roomName: string;
+  srs?: { streamurl: string };
+}>();
 
 
 async function getLiveRoomList() {
 async function getLiveRoomList() {
   try {
   try {
-    const res = await axios.get(
-      process.env.NODE_ENV === 'development'
-        ? 'http://localhost:4300/live/list'
-        : 'https://live.hsslive.cn/api/live/list'
-    );
-    if (res.data.code === 200) {
-      liveRoomList.value = res.data.data.rows.map((item) => {
+    const res = await fetchLiveList();
+    if (res.code === 200) {
+      liveRoomList.value = res.data.rows.map((item) => {
         console.log(
         console.log(
-          item,
-          JSON.parse(item.data),
-          JSON.parse(item.data).data.roomName
+          JSON.parse(item.data).data.roomName,
+          JSON.parse(item.data).data
         );
         );
         return {
         return {
           roomId: item.roomId,
           roomId: item.roomId,
           roomName: JSON.parse(item.data).data.roomName,
           roomName: JSON.parse(item.data).data.roomName,
+          srs: JSON.parse(item.data).data.srs,
         };
         };
       });
       });
-      if (res.data.data.rows.length) {
+      if (res.data.count) {
         currentRoom.value = {
         currentRoom.value = {
-          roomId: res.data.data.rows[0].roomId,
-          roomName: JSON.parse(res.data.data.rows[0].data).data.roomName,
+          roomId: res.data.rows[0].roomId,
+          roomName: JSON.parse(res.data.rows[0].data).data.roomName,
+          srs: JSON.parse(res.data.rows[0].data).data.srs,
         };
         };
       }
       }
     }
     }
+    console.log(liveRoomList.value);
   } catch (error) {
   } catch (error) {
     console.log(error);
     console.log(error);
   }
   }
@@ -112,7 +124,17 @@ onMounted(() => {
 });
 });
 
 
 function joinRoom() {
 function joinRoom() {
-  router.push({ path: `/${currentRoom.value.roomId}` });
+  if (currentRoom.value?.srs) {
+    router.push({
+      name: routerName.srsWebRtcPull,
+      params: { roomId: currentRoom.value.roomId },
+    });
+  } else {
+    router.push({
+      name: routerName.webrtcPull,
+      params: { roomId: currentRoom.value?.roomId },
+    });
+  }
 }
 }
 </script>
 </script>
 
 
@@ -159,19 +181,21 @@ function joinRoom() {
     display: inline-block;
     display: inline-block;
     box-sizing: border-box;
     box-sizing: border-box;
     margin-left: 10px;
     margin-left: 10px;
-    padding: 10px;
+    padding: 12px;
     height: 610px;
     height: 610px;
     border-radius: 4px;
     border-radius: 4px;
     background-color: rgba($color: #000000, $alpha: 0.3);
     background-color: rgba($color: #000000, $alpha: 0.3);
     vertical-align: top;
     vertical-align: top;
 
 
+    overflow: scroll;
+
     .list {
     .list {
       .item {
       .item {
         position: relative;
         position: relative;
+        box-sizing: border-box;
         margin-bottom: 10px;
         margin-bottom: 10px;
         width: 200px;
         width: 200px;
         height: 110px;
         height: 110px;
-        border-radius: 4px;
         background-color: rgba($color: #000000, $alpha: 0.3);
         background-color: rgba($color: #000000, $alpha: 0.3);
         cursor: pointer;
         cursor: pointer;
 
 
@@ -180,6 +204,23 @@ function joinRoom() {
         &:last-child {
         &:last-child {
           margin-bottom: 0;
           margin-bottom: 0;
         }
         }
+        .border {
+          position: absolute;
+          top: 0;
+          right: 0;
+          bottom: 0;
+          left: 0;
+          border: 2px solid skyblue;
+        }
+        .triangle {
+          position: absolute;
+          top: 50%;
+          left: 0;
+          display: inline-block;
+          border: 5px solid transparent;
+          border-right-color: skyblue;
+          transform: translate(-100%, -50%);
+        }
         &.active {
         &.active {
           &::before {
           &::before {
             background-color: transparent;
             background-color: transparent;

+ 31 - 0
src/views/notFound.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="notFound-wrap">
+    404,
+    <span
+      class="click"
+      @click="router.push('/')"
+    >
+      点我
+    </span>
+    回首页
+  </div>
+</template>
+
+<script lang="ts" setup>
+import router from '@/router';
+</script>
+
+<style lang="scss" scoped>
+.notFound-wrap {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  font-size: 30px;
+  transform: translate(-50%, -50%);
+  .click {
+    color: skyblue;
+    text-decoration: underline;
+    cursor: pointer;
+  }
+}
+</style>

+ 690 - 0
src/views/srs-webrtc-pull/index.vue

@@ -0,0 +1,690 @@
+<template>
+  <div class="pull-wrap">
+    <template v-if="roomNoLive">当前房间没在直播~</template>
+    <template v-else>
+      <div class="left">
+        <div
+          ref="topRef"
+          class="head"
+        >
+          <div class="info">
+            <div class="avatar"></div>
+            <div class="detail">
+              <div class="top">房间号:{{ route.params.roomId }}</div>
+              <div class="bottom">
+                <span>你的socketId:{{ getSocketId() }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="video-wrap">
+          <video
+            id="localVideo"
+            ref="localVideoRef"
+            autoplay
+            webkit-playsinline="true"
+            playsinline
+            x-webkit-airplay="allow"
+            x5-video-player-type="h5"
+            x5-video-player-fullscreen="true"
+            x5-video-orientation="portraint"
+            muted
+            controls
+          ></video>
+        </div>
+        <div
+          ref="bottomRef"
+          class="gift"
+        >
+          <div
+            v-for="(item, index) in giftList"
+            :key="index"
+            class="item"
+          >
+            <div class="ico"></div>
+            <div class="name">{{ item.name }}</div>
+            <div class="price">{{ item.price }}</div>
+          </div>
+        </div>
+      </div>
+      <div class="right">
+        <div class="tab">
+          <span>在线用户</span>
+          <span> | </span>
+          <span>大航海</span>
+        </div>
+        <div class="user-list">
+          <div
+            v-for="(item, index) in liveUserList"
+            :key="index"
+            class="item"
+          >
+            <div class="info">
+              <div class="avatar"></div>
+              <div class="nickname">{{ item.socketId }}</div>
+            </div>
+            <div class="expr">{{ item.expr }}</div>
+          </div>
+        </div>
+        <div class="danmu-list">
+          <div
+            v-for="(item, index) in damuList"
+            :key="index"
+            class="item"
+          >
+            <span class="name">{{ item.socketId }}:</span>
+            <span class="msg">{{ item.msg }}</span>
+          </div>
+        </div>
+        <div class="send-msg">
+          <textarea
+            v-model="danmuStr"
+            class="ipt"
+          ></textarea>
+          <div
+            class="btn"
+            @click="sendDanmu"
+          >
+            发送
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { getRandomString } from 'billd-utils';
+import { onMounted, onUnmounted, ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { fetchRtcV1Play } from '@/api/srs';
+import { IAdminIn, liveTypeEnum } from '@/interface';
+import { SRSWebRTCClass } from '@/network/srsWebRtc';
+import { WebRTCClass } from '@/network/webRtc';
+import {
+  WebSocketClass,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+} from '@/network/webSocket';
+import { useNetworkStore } from '@/store/network';
+
+const networkStore = useNetworkStore();
+const route = useRoute();
+const danmuStr = ref('');
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const roomNoLive = ref(false);
+const isAddTrack = ref(false);
+const roomIdRef = ref<HTMLInputElement>();
+const joinRef = ref<HTMLButtonElement>();
+const leaveRef = ref<HTMLButtonElement>();
+const roomId = ref('');
+const websocketInstant = ref<WebSocketClass>();
+const isDone = ref(false);
+const localVideoRef = ref<HTMLVideoElement>();
+const localStream = ref();
+const currType = ref(liveTypeEnum.camera); // 1:摄像头,2:录屏
+const joined = ref(false);
+const offerSended = ref(new Set());
+
+const giftList = ref([
+  { name: '鲜花', ico: '', price: '免费' },
+  { name: '肥宅水', ico: '', price: '2元' },
+  { name: '小鸡腿', ico: '', price: '3元' },
+  { name: '大鸡腿', ico: '', price: '5元' },
+  { name: '一杯咖啡', ico: '', price: '10元' },
+]);
+
+const damuList = ref<
+  {
+    socketId: string;
+    msgType: number;
+    msg: string;
+  }[]
+>([]);
+
+const liveUserList = ref<
+  {
+    socketId: string;
+    avatar: string;
+    expr: number;
+  }[]
+>([]);
+
+function closeWs() {
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.close();
+}
+
+function sendDanmu() {
+  if (!danmuStr.value.length) {
+    alert('请输入弹幕内容!');
+  }
+  if (!websocketInstant.value) return;
+  websocketInstant.value.send({
+    msgType: WsMsgTypeEnum.message,
+    data: { msg: danmuStr.value },
+  });
+  damuList.value.push({
+    socketId: getSocketId(),
+    msgType: 1,
+    msg: danmuStr.value,
+  });
+  danmuStr.value = '';
+}
+
+onUnmounted(() => {
+  closeWs();
+});
+
+onMounted(() => {
+  if (topRef.value && bottomRef.value && localVideoRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      (topRef.value.getBoundingClientRect().top +
+        topRef.value.getBoundingClientRect().height);
+    localVideoRef.value.style.height = `${res}px`;
+  }
+  roomId.value = route.params.roomId as string;
+  console.warn('开始new WebSocketClass');
+  websocketInstant.value = new WebSocketClass({
+    roomId: roomId.value,
+    url:
+      process.env.NODE_ENV === 'development'
+        ? 'ws://localhost:4300'
+        : 'wss://live.hsslive.cn',
+    isAdmin: false,
+  });
+  websocketInstant.value.update();
+  initReceive();
+  sendJoin();
+
+  localVideoRef.value?.addEventListener('loadstart', () => {
+    console.warn('视频流-loadstart');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadstart = true;
+    rtc.update();
+  });
+
+  localVideoRef.value?.addEventListener('loadedmetadata', () => {
+    console.warn('视频流-loadedmetadata');
+    const rtc = networkStore.getRtcMap(roomId.value);
+    if (!rtc) return;
+    rtc.rtcStatus.loadedmetadata = true;
+    rtc.update();
+    batchSendOffer();
+  });
+});
+
+function getSocketId() {
+  return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
+}
+
+function sendJoin() {
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
+}
+
+function batchSendOffer() {
+  liveUserList.value.forEach(async (item) => {
+    if (
+      !offerSended.value.has(item.socketId) &&
+      item.socketId !== getSocketId()
+    ) {
+      await startNewWebRtc(item.socketId);
+      await addTrack();
+      console.warn('new WebRTCClass完成');
+      console.log('执行sendOffer', {
+        sender: getSocketId(),
+        receiver: item.socketId,
+      });
+      sendOffer({ sender: getSocketId(), receiver: item.socketId });
+      offerSended.value.add(item.socketId);
+    }
+  });
+}
+
+function closeRtc() {
+  networkStore.rtcMap.forEach((rtc) => {
+    rtc.close();
+  });
+}
+
+async function handleSrsPlay() {
+  const rtc = new SRSWebRTCClass({
+    roomId: `${roomId.value}___${getSocketId()}`,
+  });
+  if (!rtc) return;
+  // rtc.addTrack({ track, stream: localStream.value, direction: 'recvonly' });
+  rtc.peerConnection?.addTransceiver('audio', { direction: 'recvonly' });
+  rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
+  try {
+    const offer = await rtc.createOffer();
+    if (!offer) return;
+    await rtc.setLocalDescription(offer);
+    const res: any = await fetchRtcV1Play({
+      api: 'http://localhost:1985/rtc/v1/play/',
+      clientip: null,
+      sdp: offer.sdp!,
+      streamurl: `webrtc://localhost/live/livestream/${roomId.value}`,
+      tid: getRandomString(10),
+    });
+    await rtc.setRemoteDescription(
+      new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
+    );
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+function initReceive() {
+  const instance = websocketInstant.value;
+  if (!instance?.socketIo) return;
+  // websocket连接成功
+  instance.socketIo.on(WsConnectStatusEnum.connect, () => {
+    console.log('【websocket】websocket连接成功', instance.socketIo?.id);
+    if (!instance) return;
+    instance.status = WsConnectStatusEnum.connect;
+    instance.update();
+  });
+
+  // websocket连接断开
+  instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
+    console.log('【websocket】websocket连接断开', instance);
+    if (!instance) return;
+    instance.status = WsConnectStatusEnum.disconnect;
+    instance.update();
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
+    console.log('【websocket】收到管理员正在直播', data);
+    handleSrsPlay();
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.roomNoLive, (data: IAdminIn) => {
+    console.log('【websocket】收到管理员不在直播', data);
+    roomNoLive.value = true;
+    closeRtc();
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
+    console.log('【websocket】当前所有在线用户', data);
+    if (!instance) return;
+    liveUserList.value = data.map((item) => ({
+      avatar: 'red',
+      socketId: item.id,
+      expr: 1,
+    }));
+  });
+
+  // 收到用户发送消息
+  instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
+    console.log('【websocket】收到用户发送消息', data);
+    if (!instance) return;
+    damuList.value.push({
+      socketId: data.socketId,
+      msgType: 1,
+      msg: data.data.msg,
+    });
+  });
+
+  // 用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
+    console.log('【websocket】用户加入房间完成', data);
+    joined.value = true;
+  });
+
+  // 其他用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+    console.log('【websocket】其他用户加入房间', data);
+    if (joined.value) {
+      batchSendOffer();
+    }
+  });
+
+  // 用户离开房间
+  instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
+    console.log('【websocket】用户离开房间', data);
+    if (!instance) return;
+    instance.socketIo?.emit(WsMsgTypeEnum.leave, {
+      roomId: instance.roomId,
+    });
+  });
+
+  // 用户离开房间完成
+  instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
+    console.log('【websocket】用户离开房间完成', data);
+    if (!instance) return;
+    const res = liveUserList.value.filter(
+      (item) => item.socketId !== data.socketId
+    );
+    liveUserList.value = res;
+    console.log('当前所有在线用户', JSON.stringify(res));
+  });
+}
+
+async function startMediaDevices() {
+  currType.value = liveTypeEnum.camera;
+  if (!localStream.value) {
+    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: true,
+      audio: true,
+    });
+    console.log('getUserMedia成功', event);
+    if (!localVideoRef.value) return;
+    localVideoRef.value.srcObject = event;
+    localStream.value = event;
+  }
+}
+function addTrack() {
+  if (!localStream.value) return;
+  liveUserList.value.forEach((item) => {
+    if (item.socketId !== getSocketId()) {
+      localStream.value.getTracks().forEach((track) => {
+        const rtc = networkStore.getRtcMap(
+          `${roomId.value}___${item.socketId}`
+        );
+        rtc?.addTrack(track, localStream.value);
+      });
+    }
+  });
+  isAddTrack.value = true;
+}
+
+async function startGetDisplayMedia() {
+  currType.value = liveTypeEnum.screen;
+  if (!localStream.value) {
+    // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
+    const event = await navigator.mediaDevices.getDisplayMedia({
+      video: true,
+      audio: true,
+    });
+    console.log('getDisplayMedia成功', event);
+    if (!localVideoRef.value) return;
+    localVideoRef.value.srcObject = event;
+    localStream.value = event;
+  }
+}
+
+async function sendOffer({
+  sender,
+  receiver,
+}: {
+  sender: string;
+  receiver: string;
+}) {
+  if (isDone.value) return;
+  if (!websocketInstant.value) return;
+  const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
+  if (!rtc) return;
+  const sdp = await rtc.createOffer();
+  await rtc.setLocalDescription(sdp);
+  websocketInstant.value.send({
+    msgType: WsMsgTypeEnum.offer,
+    data: { sdp, sender, receiver },
+  });
+}
+
+function startNewWebRtc(receiver: string) {
+  console.warn('开始new WebRTCClass', receiver);
+  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
+  rtc.rtcStatus.joined = true;
+  rtc.update();
+  return rtc;
+}
+
+function leave() {
+  if (joinRef.value && leaveRef.value && roomIdRef.value) {
+    roomIdRef.value.disabled = false;
+    joinRef.value.disabled = false;
+    leaveRef.value.disabled = true;
+  }
+  if (!websocketInstant.value) return;
+  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
+    roomId: websocketInstant.value.roomId,
+  });
+}
+</script>
+
+<style lang="scss" scoped>
+.pull-wrap {
+  margin: 20px auto 0;
+  min-width: $large-width;
+  height: 700px;
+  text-align: center;
+  .left {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    width: $large-left-width;
+    height: 100%;
+    border: 1px solid red;
+    border-radius: 10px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+    .head {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: pink;
+      .tag {
+        display: inline-block;
+        margin-right: 5px;
+        padding: 1px 4px;
+        border: 1px solid;
+        border-radius: 2px;
+        color: #9499a0;
+        font-size: 12px;
+      }
+
+      .info {
+        display: flex;
+        align-items: center;
+        text-align: initial;
+
+        .avatar {
+          margin-right: 20px;
+          width: 64px;
+          height: 64px;
+          border-radius: 50%;
+          background-color: yellow;
+        }
+        .detail {
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+          display: flex;
+          align-items: center;
+          .item {
+            display: flex;
+            align-items: center;
+            margin-right: 20px;
+            .ico {
+              display: inline-block;
+              margin-right: 4px;
+              width: 10px;
+              height: 10px;
+              border-radius: 50%;
+              background-color: skyblue;
+            }
+          }
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+    .video-wrap {
+      // height: 100px;
+      // height: 550px;
+      background-color: #18191c;
+      #localVideo {
+        max-width: 100%;
+        max-height: 100%;
+      }
+    }
+    .gift {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      height: 100px;
+      background-color: yellow;
+      .item {
+        margin-right: 10px;
+        text-align: center;
+
+        .ico {
+          width: 50px;
+          height: 50px;
+          background-color: skyblue;
+        }
+        .name {
+          color: #18191c;
+          font-size: 12px;
+        }
+        .price {
+          color: #9499a0;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+  .right {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    box-sizing: border-box;
+    margin-left: 10px;
+    min-width: 300px;
+    height: 100%;
+    border: 1px solid red;
+    border-radius: 10px;
+    background-color: white;
+    color: #9499a0;
+    .tab {
+      display: flex;
+      align-items: center;
+      justify-content: space-evenly;
+      padding: 5px 0;
+      font-size: 12px;
+    }
+    .user-list {
+      overflow-y: scroll;
+      padding: 0 15px;
+      height: 100px;
+      background-color: pink;
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin-bottom: 10px;
+        font-size: 12px;
+        .info {
+          display: flex;
+          align-items: center;
+          .avatar {
+            margin-right: 5px;
+            width: 25px;
+            height: 25px;
+            border-radius: 50%;
+            background-color: skyblue;
+          }
+          .nickname {
+            color: black;
+          }
+        }
+      }
+    }
+    .danmu-list {
+      overflow-y: scroll;
+      padding: 0 15px;
+      height: 350px;
+      text-align: initial;
+      .item {
+        margin-bottom: 10px;
+        font-size: 12px;
+        .name {
+          color: #9499a0;
+        }
+        .msg {
+          color: #61666d;
+        }
+      }
+    }
+    .send-msg {
+      position: absolute;
+      bottom: 15px;
+      box-sizing: border-box;
+      padding: 0 10px;
+      width: 100%;
+      .ipt {
+        display: block;
+        box-sizing: border-box;
+        margin: 0 auto;
+        padding: 10px;
+        width: 100%;
+        height: 60px;
+        outline: none;
+        border: 1px solid hsla(0, 0%, 60%, 0.2);
+        border-radius: 4px;
+        background-color: #f1f2f3;
+        font-size: 14px;
+      }
+      .btn {
+        box-sizing: border-box;
+        margin-top: 10px;
+        margin-left: auto;
+        padding: 5px;
+        width: 80px;
+        border-radius: 4px;
+        background-color: #23ade5;
+        color: white;
+        text-align: center;
+        font-size: 12px;
+      }
+    }
+  }
+}
+
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .pull-wrap {
+    .left {
+      width: $medium-left-width;
+    }
+    .right {
+      .list {
+        .item {
+          width: 150px;
+          height: 80px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 81 - 312
src/views/srs-demo1/index.vue → src/views/srs-webrtc-push/index.vue

@@ -19,7 +19,7 @@
           controls
           controls
         ></video>
         ></video>
         <div
         <div
-          v-if="currMediaTypeList.length <= 0"
+          v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
           class="add-wrap"
           class="add-wrap"
         >
         >
           <div
           <div
@@ -71,8 +71,7 @@
             </span>
             </span>
           </div>
           </div>
           <div class="bottom">
           <div class="bottom">
-            <button @click="startLive">开始直播</button>
-            <button @click="startSrsRtcLive">srs-rtc直播</button>
+            <button @click="startLive">srs-webrtc直播</button>
             <button @click="endLive">结束直播</button>
             <button @click="endLive">结束直播</button>
           </div>
           </div>
         </div>
         </div>
@@ -121,24 +120,13 @@
   </div>
   </div>
 </template>
 </template>
 
 
-<script lang="ts">
-import '@/assets/js/aa';
-import '@/assets/js/srs.player';
-import '@/assets/js/srs.publisher';
-import '@/assets/js/srs.sdk';
-
-export default {
-  name: 'Devices',
-};
-</script>
-
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { getRandomString } from 'billd-utils';
 import { getRandomString } from 'billd-utils';
-import MediaStreamRecorder from 'msr';
 import { onMounted, onUnmounted, ref } from 'vue';
 import { onMounted, onUnmounted, ref } from 'vue';
 
 
-import { IAdminIn, ICandidate, IOffer, liveTypeEnum } from '@/interface';
-import { WebRTCClass } from '@/network/webRtc';
+import { fetchRtcV1Publish } from '@/api/srs';
+import { IAdminIn, liveTypeEnum } from '@/interface';
+import { SRSWebRTCClass } from '@/network/srsWebRtc';
 import {
 import {
   WebSocketClass,
   WebSocketClass,
   WsConnectStatusEnum,
   WsConnectStatusEnum,
@@ -155,18 +143,33 @@ const joinRef = ref<HTMLButtonElement>();
 const leaveRef = ref<HTMLButtonElement>();
 const leaveRef = ref<HTMLButtonElement>();
 const defaultRoomId = getRandomString(15);
 const defaultRoomId = getRandomString(15);
 const roomId = ref<string>(defaultRoomId);
 const roomId = ref<string>(defaultRoomId);
+const streamurl = ref(`webrtc://localhost/live/livestream/${roomId.value}`);
 const danmuStr = ref('');
 const danmuStr = ref('');
 const roomName = ref('');
 const roomName = ref('');
 const roomNameRef = ref<HTMLInputElement>();
 const roomNameRef = ref<HTMLInputElement>();
 const websocketInstant = ref<WebSocketClass>();
 const websocketInstant = ref<WebSocketClass>();
-const isDone = ref(false);
 const localVideoRef = ref<HTMLVideoElement>();
 const localVideoRef = ref<HTMLVideoElement>();
 const localStream = ref();
 const localStream = ref();
-const currMediaTypeList = ref<liveTypeEnum[]>([]);
-const currMediaType = ref<liveTypeEnum>();
-const joined = ref(false);
-const isAdmin = ref(true);
-const offerSended = ref(new Set());
+const allMediaTypeList = {
+  [liveTypeEnum.camera]: {
+    type: liveTypeEnum.camera,
+    txt: '摄像头',
+  },
+  [liveTypeEnum.screen]: {
+    type: liveTypeEnum.screen,
+    txt: '窗口',
+  },
+};
+const currMediaTypeList = ref<
+  {
+    type: liveTypeEnum;
+    txt: string;
+  }[]
+>([]);
+const currMediaType = ref<{
+  type: liveTypeEnum;
+  txt: string;
+}>();
 const damuList = ref<
 const damuList = ref<
   {
   {
     socketId: string;
     socketId: string;
@@ -216,38 +219,14 @@ onUnmounted(() => {
   closeWs();
   closeWs();
   closeRtc();
   closeRtc();
 });
 });
-let flvPlayer;
-
-function startRtmp() {
-  try {
-    flvPlayer.play();
-  } catch (error) {
-    console.log(error);
-  }
-}
 
 
 onMounted(() => {
 onMounted(() => {
-  // if (flvJs.isSupported()) {
-  //   flvPlayer = flvJs.createPlayer({
-  //     type: 'flv',
-  //     url: 'http://localhost:8080/live/show.flv',
-  //     // url: 'http://42.193.157.44:9000/live/fddm_2.flv',
-  //     // url: 'https://www.hsslive.cn/stream/live/fddm_2.flv',
-  //     // url: 'https://www.hsslive.cn/bilibilistream/live-bvc/173676/live_381307133_59434826.flv?expires=1680798150&len=0&oi=1900220676&pt=web&qn=0&trid=10002eb4d1a458684cc9a93d888d7b580cac&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha15&sign=1724a76684e5e1f8d68346613a32f8c0&sk=3e2a893893799632504afbdef4a9e3d7&p2p_type=1&sl=1&free_type=0&mid=381307133&sche=ban&score=1&pp=rtmp&freeze=1&source=onetier&trace=10&site=12e2a543d23e1c612a7145eb5a5cfac4&order=1',
-  //     // url: 'https://xy117x149x235x203xy.mcdn.bilivideo.cn/live-bvc/173676/live_381307133_59434826.flv?expires=1680798150&len=0&oi=1900220676&pt=web&qn=0&trid=10002eb4d1a458684cc9a93d888d7b580cac&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha15&sign=1724a76684e5e1f8d68346613a32f8c0&sk=3e2a893893799632504afbdef4a9e3d7&p2p_type=1&sl=1&free_type=0&mid=381307133&sche=ban&score=1&pp=rtmp&freeze=1&source=onetier&trace=10&site=12e2a543d23e1c612a7145eb5a5cfac4&order=1',
-  //   });
-  //   // @ts-ignore
-  //   flvPlayer.attachMediaElement(
-  //     document.querySelector<HTMLVideoElement>('#blobVideo')
-  //   );
-  //   flvPlayer.load();
-  // }
   if (topRef.value && bottomRef.value && localVideoRef.value) {
   if (topRef.value && bottomRef.value && localVideoRef.value) {
     const res =
     const res =
       bottomRef.value.getBoundingClientRect().top -
       bottomRef.value.getBoundingClientRect().top -
       topRef.value.getBoundingClientRect().top;
       topRef.value.getBoundingClientRect().top;
     localVideoRef.value.style.height = `100px`;
     localVideoRef.value.style.height = `100px`;
-    // localVideoRef.value.style.height = `${res}px`;
+    localVideoRef.value.style.height = `${res}px`;
   }
   }
   localVideoRef.value?.addEventListener('loadstart', () => {
   localVideoRef.value?.addEventListener('loadstart', () => {
     console.warn('视频流-loadstart');
     console.warn('视频流-loadstart');
@@ -263,9 +242,6 @@ onMounted(() => {
     if (!rtc) return;
     if (!rtc) return;
     rtc.rtcStatus.loadedmetadata = true;
     rtc.rtcStatus.loadedmetadata = true;
     rtc.update();
     rtc.update();
-    if (isAdmin.value) {
-      batchSendOffer();
-    }
   });
   });
 });
 });
 
 
@@ -273,36 +249,6 @@ function getSocketId() {
   return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
   return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
 }
 }
 
 
-function sendJoin() {
-  const instance = networkStore.wsMap.get(roomId.value);
-  if (!instance) return;
-  instance.send({
-    msgType: WsMsgTypeEnum.join,
-    data: {
-      roomName: roomName.value,
-    },
-  });
-}
-
-function batchSendOffer() {
-  liveUserList.value.forEach(async (item) => {
-    if (
-      !offerSended.value.has(item.socketId) &&
-      item.socketId !== getSocketId()
-    ) {
-      await startNewWebRtc(item.socketId);
-      await addTrack();
-      console.warn('new WebRTCClass完成');
-      console.log('执行sendOffer', {
-        sender: getSocketId(),
-        receiver: item.socketId,
-      });
-      sendOffer({ sender: getSocketId(), receiver: item.socketId });
-      offerSended.value.add(item.socketId);
-    }
-  });
-}
-
 function initReceive() {
 function initReceive() {
   const instance = websocketInstant.value;
   const instance = websocketInstant.value;
   if (!instance?.socketIo) return;
   if (!instance?.socketIo) return;
@@ -312,6 +258,7 @@ function initReceive() {
     if (!instance) return;
     if (!instance) return;
     instance.status = WsConnectStatusEnum.connect;
     instance.status = WsConnectStatusEnum.connect;
     instance.update();
     instance.update();
+    sendJoin();
   });
   });
 
 
   // websocket连接断开
   // websocket连接断开
@@ -333,73 +280,6 @@ function initReceive() {
     if (!instance) return;
     if (!instance) return;
   });
   });
 
 
-  // 收到offer
-  instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
-    console.warn('【websocket】收到offer', data);
-    if (!instance) return;
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到offer,这个offer是发给我的');
-      const rtc = startNewWebRtc(data.data.sender);
-      await rtc.setRemoteDescription(data.data.sdp);
-      const sdp = await rtc.createAnswer();
-      await rtc.setLocalDescription(sdp);
-      websocketInstant.value?.send({
-        msgType: WsMsgTypeEnum.answer,
-        data: { sdp, sender: getSocketId(), receiver: data.data.sender },
-      });
-    } else {
-      console.log('收到offer,但是这个offer不是发给我的');
-    }
-  });
-
-  // 收到answer
-  instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
-    console.warn('【websocket】收到answer', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
-    console.log(rtc, '收到answer收到answer');
-    if (!rtc) return;
-    rtc.rtcStatus.answer = true;
-    rtc.update();
-    if (data.data.receiver === getSocketId()) {
-      console.log('收到answer,这个answer是发给我的');
-      await rtc.setRemoteDescription(data.data.sdp);
-    } else {
-      console.log('收到answer,但这个answer不是发给我的');
-    }
-  });
-
-  // 收到candidate
-  instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
-    console.warn('【websocket】收到candidate', data);
-    if (isDone.value) return;
-    if (!instance) return;
-    const rtc =
-      networkStore.getRtcMap(`${roomId.value}___${data.socketId}`) ||
-      networkStore.getRtcMap(roomId.value);
-    if (!rtc) return;
-    if (data.socketId !== getSocketId()) {
-      console.log('不是我发的candidate');
-      const candidate = new RTCIceCandidate({
-        sdpMid: data.data.sdpMid,
-        sdpMLineIndex: data.data.sdpMLineIndex,
-        candidate: data.data.candidate,
-      });
-      rtc.peerConnection
-        ?.addIceCandidate(candidate)
-        .then(() => {
-          console.log('candidate成功');
-          // rtc.handleStream();
-        })
-        .catch((err) => {
-          console.error('candidate失败', err);
-        });
-    } else {
-      console.log('是我发的candidate');
-    }
-  });
-
   // 收到用户发送消息
   // 收到用户发送消息
   instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
   instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
     console.log('【websocket】收到用户发送消息', data);
     console.log('【websocket】收到用户发送消息', data);
@@ -411,22 +291,15 @@ function initReceive() {
     });
     });
   });
   });
 
 
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
-    console.log('【websocket】用户加入房间', data);
-    if (!instance) return;
-  });
-
-  // 用户加入房间
+  // 用户加入房间完成
   instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
   instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
     console.log('【websocket】用户加入房间完成', data);
     console.log('【websocket】用户加入房间完成', data);
-    joined.value = true;
     liveUserList.value.push({
     liveUserList.value.push({
       avatar: 'red',
       avatar: 'red',
       socketId: `${getSocketId()}`,
       socketId: `${getSocketId()}`,
       expr: 1,
       expr: 1,
     });
     });
-    batchSendOffer();
+    handleSrsPush();
   });
   });
 
 
   // 其他用户加入房间
   // 其他用户加入房间
@@ -438,10 +311,6 @@ function initReceive() {
       expr: 1,
       expr: 1,
     });
     });
     console.log('当前所有在线用户', JSON.stringify(liveUserList.value));
     console.log('当前所有在线用户', JSON.stringify(liveUserList.value));
-    console.log(isAdmin.value, joined.value);
-    if (isAdmin.value && joined.value) {
-      batchSendOffer();
-    }
   });
   });
 
 
   // 用户离开房间
   // 用户离开房间
@@ -493,91 +362,84 @@ function endLive() {
   }
   }
   if (!websocketInstant.value) return;
   if (!websocketInstant.value) return;
   websocketInstant.value.close();
   websocketInstant.value.close();
-  // websocketInstant.value.send({
-  //   msgType: WsMsgTypeEnum.roomNoLive,
-  // });
 }
 }
 
 
-function startSrsRtcLive() {
-  const sdk = new SrsRtcPublisherAsync();
-  console.log(sdk);
+function sendJoin() {
+  const instance = networkStore.wsMap.get(roomId.value);
+  if (!instance) return;
+  instance.send({
+    msgType: WsMsgTypeEnum.join,
+    data: {
+      roomName: roomName.value,
+      srs: {
+        streamurl: streamurl.value,
+      },
+    },
+  });
 }
 }
 
 
 function startLive() {
 function startLive() {
+  if (!roomNameIsOk()) return;
+  if (currMediaTypeList.value.length <= 0) {
+    alert('请选择一个素材!');
+    return;
+  }
   websocketInstant.value = new WebSocketClass({
   websocketInstant.value = new WebSocketClass({
     roomId: roomId.value,
     roomId: roomId.value,
     url:
     url:
       process.env.NODE_ENV === 'development'
       process.env.NODE_ENV === 'development'
         ? 'ws://localhost:4300'
         ? 'ws://localhost:4300'
         : 'wss://live.hsslive.cn',
         : 'wss://live.hsslive.cn',
-    isAdmin: isAdmin.value,
+    isAdmin: true,
   });
   });
   websocketInstant.value.update();
   websocketInstant.value.update();
-  setTimeout(() => {
-    websocketInstant.value!.socketIo?.emit(WsMsgTypeEnum.message, {
-      data: { debug: 1 },
-    });
-  }, 1000);
+  initReceive();
+}
 
 
-  // initReceive();
-  // sendJoin();
+async function handleSrsPush() {
+  const rtc = new SRSWebRTCClass({
+    roomId: `${roomId.value}___${getSocketId()}`,
+  });
+  if (!rtc) return;
+  localStream.value.getTracks().forEach((track) => {
+    rtc.addTrack({ track, stream: localStream.value, direction: 'sendonly' });
+  });
+  try {
+    const offer = await rtc.createOffer();
+    if (!offer) return;
+    await rtc.setLocalDescription(offer);
+    const res: any = await fetchRtcV1Publish({
+      api: 'http://localhost:1985/rtc/v1/publish/',
+      clientip: null,
+      sdp: offer.sdp!,
+      streamurl: streamurl.value,
+      tid: getRandomString(10),
+    });
+    await rtc.setRemoteDescription(
+      new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
+    );
+  } catch (error) {
+    console.log(error);
+  }
 }
 }
 
 
-const blobArr = ref<Blob[]>([]);
 /** 摄像头 */
 /** 摄像头 */
 async function startMediaDevices() {
 async function startMediaDevices() {
   if (!localStream.value) {
   if (!localStream.value) {
     // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
     // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
     const event = await navigator.mediaDevices.getUserMedia({
     const event = await navigator.mediaDevices.getUserMedia({
       video: true,
       video: true,
-      audio: false,
+      audio: true,
     });
     });
     console.log('getUserMedia成功', event);
     console.log('getUserMedia成功', event);
-    currMediaType.value = liveTypeEnum.camera;
-    currMediaTypeList.value.push(liveTypeEnum.camera);
+    currMediaType.value = allMediaTypeList[liveTypeEnum.camera];
+    currMediaTypeList.value.push(allMediaTypeList[liveTypeEnum.camera]);
     if (!localVideoRef.value) return;
     if (!localVideoRef.value) return;
     localVideoRef.value.srcObject = event;
     localVideoRef.value.srcObject = event;
-    // const rec = new MediaRecorder(event, { mimeType: 'image/png' });
-    // const rec = new MediaRecorder(event);
-    const rec = new MediaRecorder(event, {
-      // mimeType: 'video/webm;codecs=avc1.64001f,opus',
-      mimeType: 'video/webm',
-    });
-    rec.addEventListener('dataavailable', (e) => {
-      console.log(new Date().toLocaleString(), 'dataavailable');
-      if (e.data.size > 0) {
-        blobArr.value.push(e.data);
-      }
-      console.log(e.data.stream());
-      // document.querySelector<HTMLVideoElement>('#blobVideo')!.srcObject =
-      //   e.data.stream();
-      // const recordedBlob = new Blob([e.data], { type: 'video/webm' });
-      const recordedBlob = new Blob([e.data]);
-      console.log(recordedBlob);
-      // const url = window.URL.createObjectURL(recordedBlob);
-      // const a = document.createElement('a');
-      // a.style.display = 'none';
-      // a.href = url;
-      // a.download = 'test.webm';
-      // document.body.appendChild(a);
-      // a.click();
-      // setTimeout(() => {
-      //   document.body.removeChild(a);
-      //   window.URL.revokeObjectURL(url);
-      // }, 100);
-      if (!websocketInstant.value) return;
-      // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-      // websocketInstant.value.socketIo?.emit('aaa', { aa: recordedBlob });
-      websocketInstant.value.send({
-        msgType: WsMsgTypeEnum.sendBlob,
-        data: { blob: recordedBlob, timestamp: new Date().getTime() },
-      });
-    });
-    rec.start(500);
-
     localStream.value = event;
     localStream.value = event;
   }
   }
 }
 }
+
 /** 窗口 */
 /** 窗口 */
 async function startGetDisplayMedia() {
 async function startGetDisplayMedia() {
   if (!localStream.value) {
   if (!localStream.value) {
@@ -587,106 +449,13 @@ async function startGetDisplayMedia() {
       audio: true,
       audio: true,
     });
     });
     console.log('getDisplayMedia成功', event);
     console.log('getDisplayMedia成功', event);
-    currMediaType.value = liveTypeEnum.screen;
-    currMediaTypeList.value.push(liveTypeEnum.screen);
+    currMediaType.value = allMediaTypeList[liveTypeEnum.screen];
+    currMediaTypeList.value.push(allMediaTypeList[liveTypeEnum.screen]);
     if (!localVideoRef.value) return;
     if (!localVideoRef.value) return;
     localVideoRef.value.srcObject = event;
     localVideoRef.value.srcObject = event;
     localStream.value = event;
     localStream.value = event;
-    const rec = new MediaStreamRecorder(event);
-    // const rec = new MediaRecorder(event, {
-    //   mimeType: 'video/webm',
-    // });
-    rec.ondataavailable = (blob) => {
-      if (!websocketInstant.value) return;
-      // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-      // websocketInstant.value.socketIo?.emit('aaa', { aa: recordedBlob });
-      websocketInstant.value.send({
-        msgType: WsMsgTypeEnum.sendBlob,
-        data: { blob, timestamp: new Date().getTime() },
-      });
-    };
-    rec.start(500);
-
-    // rec.addEventListener('dataavailable', (e) => {
-    //   console.log(new Date().toLocaleString(), 'dataavailable');
-    //   if (e.data.size <= 0) {
-    //     return;
-    //   }
-    //   blobArr.value.push(e.data);
-
-    //   console.log(e.data.stream());
-    //   // document.querySelector<HTMLVideoElement>('#blobVideo')!.srcObject =
-    //   //   e.data.stream();
-    //   // const recordedBlob = new Blob([e.data], { type: 'video/webm' });
-    //   // const res1 = blobArr.value.pop();
-    //   // const res2 = res1 ? [res1] : [];
-    //   // const recordedBlob = new Blob(res2, { type: 'video/webm' });
-    //   const recordedBlob = new Blob(blobArr.value, { type: 'video/webm' });
-    //   // const recordedBlob = new Blob([e.data]);
-    //   console.log(recordedBlob, blobArr.value.length, 222);
-    //   // const url = window.URL.createObjectURL(recordedBlob);
-    //   // const a = document.createElement('a');
-    //   // a.style.display = 'none';
-    //   // a.href = url;
-    //   // a.download = 'test.webm';
-    //   // document.body.appendChild(a);
-    //   // a.click();
-    //   // setTimeout(() => {
-    //   //   document.body.removeChild(a);
-    //   //   window.URL.revokeObjectURL(url);
-    //   // }, 100);
-    //   if (!websocketInstant.value) return;
-    //   // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-    //   // websocketInstant.value.socketIo?.emit('aaa', { aa: recordedBlob });
-    //   websocketInstant.value.send({
-    //     msgType: WsMsgTypeEnum.sendBlob,
-    //     data: { blob: recordedBlob, timestamp: new Date().getTime() },
-    //   });
-    //   // blobArr.value = [];
-    // });
-    // rec.start(500);
   }
   }
 }
 }
-function addTrack() {
-  if (!localStream.value) return;
-  liveUserList.value.forEach((item) => {
-    if (item.socketId !== getSocketId()) {
-      localStream.value.getTracks().forEach((track) => {
-        const rtc = networkStore.getRtcMap(
-          `${roomId.value}___${item.socketId}`
-        );
-        rtc?.addTrack(track, localStream.value);
-      });
-    }
-  });
-}
-
-async function sendOffer({
-  sender,
-  receiver,
-}: {
-  sender: string;
-  receiver: string;
-}) {
-  if (isDone.value) return;
-  if (!websocketInstant.value) return;
-  const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-  if (!rtc) return;
-  const sdp = await rtc.createOffer();
-  await rtc.setLocalDescription(sdp);
-  websocketInstant.value.send({
-    msgType: WsMsgTypeEnum.offer,
-    data: { sdp, sender, receiver },
-  });
-}
-
-function startNewWebRtc(receiver: string) {
-  console.warn('开始new WebRTCClass', receiver);
-  const rtc = new WebRTCClass({ roomId: `${roomId.value}___${receiver}` });
-  rtc.rtcStatus.joined = true;
-  rtc.update();
-  return rtc;
-}
 
 
 function leave() {
 function leave() {
   if (joinRef.value && leaveRef.value && roomIdRef.value) {
   if (joinRef.value && leaveRef.value && roomIdRef.value) {

+ 2 - 53
src/views/pull/index.vue → src/views/webrtc-pull/index.vue

@@ -97,19 +97,17 @@
 import { onMounted, onUnmounted, ref } from 'vue';
 import { onMounted, onUnmounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { useRoute } from 'vue-router';
 
 
-import { IAdminIn, ICandidate, IOffer, liveTypeEnum } from '@/interface';
+import { IAdminIn, ICandidate, IOffer } from '@/interface';
 import { WebRTCClass } from '@/network/webRtc';
 import { WebRTCClass } from '@/network/webRtc';
 import {
 import {
   WebSocketClass,
   WebSocketClass,
   WsConnectStatusEnum,
   WsConnectStatusEnum,
   WsMsgTypeEnum,
   WsMsgTypeEnum,
 } from '@/network/webSocket';
 } from '@/network/webSocket';
-import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useNetworkStore } from '@/store/network';
 
 
 const networkStore = useNetworkStore();
 const networkStore = useNetworkStore();
 const route = useRoute();
 const route = useRoute();
-const appStore = useAppStore();
 const danmuStr = ref('');
 const danmuStr = ref('');
 const topRef = ref<HTMLDivElement>();
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
@@ -123,7 +121,6 @@ const websocketInstant = ref<WebSocketClass>();
 const isDone = ref(false);
 const isDone = ref(false);
 const localVideoRef = ref<HTMLVideoElement>();
 const localVideoRef = ref<HTMLVideoElement>();
 const localStream = ref();
 const localStream = ref();
-const currType = ref(liveTypeEnum.camera); // 1:摄像头,2:录屏
 const joined = ref(false);
 const joined = ref(false);
 const offerSended = ref(new Set());
 const offerSended = ref(new Set());
 
 
@@ -352,7 +349,6 @@ function initReceive() {
         ?.addIceCandidate(candidate)
         ?.addIceCandidate(candidate)
         .then(() => {
         .then(() => {
           console.log('candidate成功');
           console.log('candidate成功');
-          // rtc.handleStream();
         })
         })
         .catch((err) => {
         .catch((err) => {
           console.error('candidate失败', err);
           console.error('candidate失败', err);
@@ -373,13 +369,7 @@ function initReceive() {
     });
     });
   });
   });
 
 
-  // 用户加入房间
-  instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
-    console.log('【websocket】用户加入房间', data);
-    if (!instance) return;
-  });
-
-  // 用户加入房间
+  // 用户加入房间完成
   instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
   instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
     console.log('【websocket】用户加入房间完成', data);
     console.log('【websocket】用户加入房间完成', data);
     joined.value = true;
     joined.value = true;
@@ -414,20 +404,6 @@ function initReceive() {
   });
   });
 }
 }
 
 
-async function startMediaDevices() {
-  currType.value = liveTypeEnum.camera;
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getUserMedia成功', event);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
 function addTrack() {
 function addTrack() {
   if (!localStream.value) return;
   if (!localStream.value) return;
   liveUserList.value.forEach((item) => {
   liveUserList.value.forEach((item) => {
@@ -443,21 +419,6 @@ function addTrack() {
   isAddTrack.value = true;
   isAddTrack.value = true;
 }
 }
 
 
-async function startGetDisplayMedia() {
-  currType.value = liveTypeEnum.screen;
-  if (!localStream.value) {
-    // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
-    const event = await navigator.mediaDevices.getDisplayMedia({
-      video: true,
-      audio: true,
-    });
-    console.log('getDisplayMedia成功', event);
-    if (!localVideoRef.value) return;
-    localVideoRef.value.srcObject = event;
-    localStream.value = event;
-  }
-}
-
 async function sendOffer({
 async function sendOffer({
   sender,
   sender,
   receiver,
   receiver,
@@ -484,18 +445,6 @@ function startNewWebRtc(receiver: string) {
   rtc.update();
   rtc.update();
   return rtc;
   return rtc;
 }
 }
-
-function leave() {
-  if (joinRef.value && leaveRef.value && roomIdRef.value) {
-    roomIdRef.value.disabled = false;
-    joinRef.value.disabled = false;
-    leaveRef.value.disabled = true;
-  }
-  if (!websocketInstant.value) return;
-  websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
-    roomId: websocketInstant.value.roomId,
-  });
-}
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>

+ 1 - 37
src/views/push/index.vue → src/views/webrtc-push/index.vue

@@ -4,10 +4,6 @@
       ref="topRef"
       ref="topRef"
       class="left"
       class="left"
     >
     >
-      <video
-        id="blobVideo"
-        style="width: 300px; background-color: red"
-      ></video>
       <div class="video-wrap">
       <div class="video-wrap">
         <video
         <video
           id="localVideo"
           id="localVideo"
@@ -482,7 +478,6 @@ function startLive() {
   sendJoin();
   sendJoin();
 }
 }
 
 
-const blobArr = ref<Blob[]>([]);
 /** 摄像头 */
 /** 摄像头 */
 async function startMediaDevices() {
 async function startMediaDevices() {
   if (!localStream.value) {
   if (!localStream.value) {
@@ -496,38 +491,6 @@ async function startMediaDevices() {
     currMediaTypeList.value.push(liveTypeEnum.camera);
     currMediaTypeList.value.push(liveTypeEnum.camera);
     if (!localVideoRef.value) return;
     if (!localVideoRef.value) return;
     localVideoRef.value.srcObject = event;
     localVideoRef.value.srcObject = event;
-    const rec = new MediaRecorder(event, { mimeType: 'video/webm' });
-    console.log('rec', rec);
-    rec.addEventListener('dataavailable', (e) => {
-      console.log(new Date().toLocaleString(), 'dataavailable');
-      if (e.data.size > 0) {
-        blobArr.value.push(e.data);
-      }
-      console.log(e.data.stream());
-      // document.querySelector<HTMLVideoElement>('#blobVideo')!.srcObject =
-      //   e.data.stream();
-      const recordedBlob = new Blob(blobArr.value, { type: 'video/webm' });
-      console.log(recordedBlob);
-      // const url = window.URL.createObjectURL(recordedBlob);
-      // const a = document.createElement('a');
-      // a.style.display = 'none';
-      // a.href = url;
-      // a.download = 'test.webm';
-      // document.body.appendChild(a);
-      // a.click();
-      // setTimeout(() => {
-      //   document.body.removeChild(a);
-      //   window.URL.revokeObjectURL(url);
-      // }, 100);
-      if (!websocketInstant.value) return;
-      // websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.sendBlob, e.data);
-      websocketInstant.value.send({
-        msgType: WsMsgTypeEnum.sendBlob,
-        data: { blob: recordedBlob, timestamp: new Date().getTime() },
-      });
-    });
-    rec.start(1000);
-
     localStream.value = event;
     localStream.value = event;
   }
   }
 }
 }
@@ -547,6 +510,7 @@ async function startGetDisplayMedia() {
     localStream.value = event;
     localStream.value = event;
   }
   }
 }
 }
+
 function addTrack() {
 function addTrack() {
   if (!localStream.value) return;
   if (!localStream.value) return;
   liveUserList.value.forEach((item) => {
   liveUserList.value.forEach((item) => {