shuisheng 2 vuotta sitten
vanhempi
sitoutus
e48c68da01

+ 1 - 1
.eslintrc.js

@@ -118,7 +118,7 @@ module.exports = {
         // 根据导入路径以字母顺序排列每个组中的顺序
         alphabetize: {
           order: 'asc', // 使用asc按升序排序,使用desc按降序排序(默认值:ignore)。
-          caseInsensitive: false, // 使用true忽略大小写,而false考虑大小写(默认值:false)。
+          caseInsensitive: true, // 使用true忽略大小写,而false考虑大小写(默认值:false)。
           orderImportKind: 'asc', // 使用asc以升序对各种导入类型进行排序,例如以type或typeof为前缀的导入,具有相同的导入路径。使用desc按降序排序(默认值:忽略)
         },
       },

+ 1 - 1
package.json

@@ -33,7 +33,7 @@
   },
   "dependencies": {
     "axios": "^1.2.1",
-    "billd-deploy": "^1.0.15",
+    "billd-deploy": "^1.0.16",
     "billd-html-webpack-plugin": "^1.0.1",
     "billd-scss": "^0.0.6",
     "billd-utils": "^0.0.9",

+ 4 - 4
pnpm-lock.yaml

@@ -15,7 +15,7 @@ specifiers:
   '@vue/preload-webpack-plugin': ^2.0.0
   axios: ^1.2.1
   babel-loader: ^8.2.2
-  billd-deploy: ^1.0.15
+  billd-deploy: ^1.0.16
   billd-html-webpack-plugin: ^1.0.1
   billd-scss: ^0.0.6
   billd-utils: ^0.0.9
@@ -78,7 +78,7 @@ specifiers:
 
 dependencies:
   axios: 1.3.4
-  billd-deploy: 1.0.15_imakxv3mh5kp5z5uouwrjmnj5q
+  billd-deploy: 1.0.16_imakxv3mh5kp5z5uouwrjmnj5q
   billd-html-webpack-plugin: 1.0.1_imakxv3mh5kp5z5uouwrjmnj5q
   billd-scss: 0.0.6
   billd-utils: 0.0.9_typescript@4.9.5
@@ -3084,8 +3084,8 @@ packages:
     resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
     dev: true
 
-  /billd-deploy/1.0.15_imakxv3mh5kp5z5uouwrjmnj5q:
-    resolution: {integrity: sha512-QqD1Ge01Nn4WRs3BLQ+Bp8rccgnXt0f20DaUjLywHPxQ/ACCnUI3nFXbfrOeQ1W29PhRmto0tKt1IsKNzno63Q==}
+  /billd-deploy/1.0.16_imakxv3mh5kp5z5uouwrjmnj5q:
+    resolution: {integrity: sha512-T1ZejJOdNUuY6bMjGz6hZl2okoivEVuv1qOaRW45CInypUdWj8LDx21AITcykT0XR/32fJPXhO/eVWhfratQAQ==}
     requiresBuild: true
     dependencies:
       ali-oss: 6.17.1

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

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

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

@@ -0,0 +1,400 @@
+/**
+* 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;
+  }
+}

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

@@ -0,0 +1,183 @@
+/**
+ * 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]);
+}

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

@@ -0,0 +1,743 @@
+//
+// 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(', ');
+}

+ 4 - 3
src/layout/head/index.vue

@@ -12,7 +12,7 @@
           v-for="(item, index) in list"
           :key="index"
           class="item"
-          @click="item.route && router.push(item.route)"
+          @click="item.routeName && router.push({ name: item.routeName })"
         >
           {{ item.title }}
         </div>
@@ -58,8 +58,9 @@ const list = ref([
   { title: '直播拉流' },
   { title: 'mesh模型' },
   { title: 'sfu模型' },
-  { title: 'test1', route: '/test1' },
-  { title: 'bilibiliPush', route: '/bilibiliPush' },
+  { title: 'test1', routeName: 'test1' },
+  { title: 'bilibiliPush', routeName: 'bilibiliPush' },
+  { title: 'srsDemoOne', routeName: 'srsDemoOne' },
 ]);
 
 function goPushPage() {

+ 2 - 0
src/main.ts

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

+ 5 - 0
src/router/index.ts

@@ -35,6 +35,11 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/test1',
         component: () => import('@/views/test1/index.vue'),
       },
+      {
+        name: 'srsDemoOne',
+        path: '/srs-demo1',
+        component: () => import('@/views/srs-demo1/index.vue'),
+      },
     ],
   },
 ];

+ 921 - 0
src/views/srs-demo1/index.vue

@@ -0,0 +1,921 @@
+<template>
+  <div class="push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <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
+          v-if="currMediaTypeList.length <= 0"
+          class="add-wrap"
+        >
+          <div
+            class="item"
+            @click="startMediaDevices"
+          >
+            摄像头
+          </div>
+          <div
+            class="item"
+            @click="startGetDisplayMedia"
+          >
+            窗口
+          </div>
+        </div>
+      </div>
+      <div
+        ref="bottomRef"
+        class="control"
+      >
+        <div class="info">
+          <div class="avatar"></div>
+          <div class="detail">
+            <div class="top">
+              <input
+                ref="roomNameRef"
+                v-model="roomName"
+                type="text"
+                placeholder="输入房间名"
+              />
+              <button
+                class="btn"
+                @click="confirmRoomName"
+              >
+                确定
+              </button>
+              <!-- 房东的猫livehouse/音乐节 -->
+            </div>
+            <div class="bottom">
+              <span>socketId:{{ getSocketId() }}</span>
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>正在观看人数:{{ liveUserList.length }}</span>
+            </span>
+          </div>
+          <div class="bottom">
+            <button @click="startLive">开始直播</button>
+            <button @click="startSrsRtcLive">srs-rtc直播</button>
+            <button @click="endLive">结束直播</button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in currMediaTypeList"
+            :key="index"
+            class="item"
+          >
+            <span class="name">{{ item }}</span>
+          </div>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="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">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+          />
+          <div
+            class="btn"
+            @click="sendDanmu"
+          >
+            发送
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</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>
+import { getRandomString } from 'billd-utils';
+import MediaStreamRecorder from 'msr';
+import { onMounted, onUnmounted, ref } from 'vue';
+
+import { IAdminIn, ICandidate, IOffer, liveTypeEnum } from '@/interface';
+import { WebRTCClass } from '@/network/webRtc';
+import {
+  WebSocketClass,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+} from '@/network/webSocket';
+import { useNetworkStore } from '@/store/network';
+
+const networkStore = useNetworkStore();
+
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const roomIdRef = ref<HTMLInputElement>();
+const joinRef = ref<HTMLButtonElement>();
+const leaveRef = ref<HTMLButtonElement>();
+const defaultRoomId = getRandomString(15);
+const roomId = ref<string>(defaultRoomId);
+const danmuStr = ref('');
+const roomName = ref('');
+const roomNameRef = ref<HTMLInputElement>();
+const websocketInstant = ref<WebSocketClass>();
+const isDone = ref(false);
+const localVideoRef = ref<HTMLVideoElement>();
+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 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 closeRtc() {
+  networkStore.rtcMap.forEach((rtc) => {
+    rtc.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();
+  closeRtc();
+});
+let flvPlayer;
+
+function startRtmp() {
+  try {
+    flvPlayer.play();
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+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) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      topRef.value.getBoundingClientRect().top;
+    localVideoRef.value.style.height = `100px`;
+    // localVideoRef.value.style.height = `${res}px`;
+  }
+  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();
+    if (isAdmin.value) {
+      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: {
+      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() {
+  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);
+  });
+
+  // 当前所有在线用户
+  instance.socketIo.on(WsMsgTypeEnum.liveUser, () => {
+    console.log('【websocket】当前所有在线用户');
+    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) => {
+    console.log('【websocket】收到用户发送消息', data);
+    if (!instance) return;
+    damuList.value.push({
+      socketId: data.socketId,
+      msgType: 1,
+      msg: data.data.msg,
+    });
+  });
+
+  // 用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
+    console.log('【websocket】用户加入房间', data);
+    if (!instance) return;
+  });
+
+  // 用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
+    console.log('【websocket】用户加入房间完成', data);
+    joined.value = true;
+    liveUserList.value.push({
+      avatar: 'red',
+      socketId: `${getSocketId()}`,
+      expr: 1,
+    });
+    batchSendOffer();
+  });
+
+  // 其他用户加入房间
+  instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+    console.log('【websocket】其他用户加入房间', data);
+    liveUserList.value.push({
+      avatar: 'red',
+      socketId: data.socketId,
+      expr: 1,
+    });
+    console.log('当前所有在线用户', JSON.stringify(liveUserList.value));
+    console.log(isAdmin.value, joined.value);
+    if (isAdmin.value && 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
+    );
+    console.log('当前所有在线用户', JSON.stringify(res));
+    liveUserList.value = res;
+  });
+}
+
+function roomNameIsOk() {
+  if (!roomName.value.length) {
+    alert('请输入房间名!');
+    return false;
+  }
+  if (roomName.value.length < 3 || roomName.value.length > 10) {
+    alert('房间名要求3-10个字符!');
+    return false;
+  }
+  return true;
+}
+
+function confirmRoomName() {
+  if (!roomNameIsOk()) return;
+  if (!roomNameRef.value) return;
+  roomNameRef.value.disabled = true;
+}
+
+function endLive() {
+  console.log('endLive');
+  closeRtc();
+  currMediaTypeList.value = [];
+  localStream.value = null;
+  if (localVideoRef.value) {
+    localVideoRef.value.srcObject = null;
+  }
+  if (!websocketInstant.value) return;
+  websocketInstant.value.close();
+  // websocketInstant.value.send({
+  //   msgType: WsMsgTypeEnum.roomNoLive,
+  // });
+}
+
+function startSrsRtcLive() {
+  const sdk = new SrsRtcPublisherAsync();
+  console.log(sdk);
+}
+
+function startLive() {
+  websocketInstant.value = new WebSocketClass({
+    roomId: roomId.value,
+    url:
+      process.env.NODE_ENV === 'development'
+        ? 'ws://localhost:4300'
+        : 'wss://live.hsslive.cn',
+    isAdmin: isAdmin.value,
+  });
+  websocketInstant.value.update();
+  setTimeout(() => {
+    websocketInstant.value!.socketIo?.emit(WsMsgTypeEnum.message, {
+      data: { debug: 1 },
+    });
+  }, 1000);
+
+  // initReceive();
+  // sendJoin();
+}
+
+const blobArr = ref<Blob[]>([]);
+/** 摄像头 */
+async function startMediaDevices() {
+  if (!localStream.value) {
+    // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: true,
+      audio: false,
+    });
+    console.log('getUserMedia成功', event);
+    currMediaType.value = liveTypeEnum.camera;
+    currMediaTypeList.value.push(liveTypeEnum.camera);
+    if (!localVideoRef.value) return;
+    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;
+  }
+}
+/** 窗口 */
+async function startGetDisplayMedia() {
+  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);
+    currMediaType.value = liveTypeEnum.screen;
+    currMediaTypeList.value.push(liveTypeEnum.screen);
+    if (!localVideoRef.value) return;
+    localVideoRef.value.srcObject = 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() {
+  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>
+.push-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;
+
+    .video-wrap {
+      position: relative;
+      background-color: #18191c;
+      #localVideo {
+        max-width: 100%;
+        max-height: 100%;
+      }
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        width: 200px;
+        height: 50px;
+        background-color: #fff;
+        transform: translate(-50%, -50%);
+        .item {
+          width: 60px;
+          height: 30px;
+          border-radius: 4px;
+          background-color: rebeccapurple;
+          color: white;
+          font-size: 14px;
+          line-height: 30px;
+          cursor: pointer;
+        }
+      }
+    }
+    .control {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: pink;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 64px;
+          height: 64px;
+          border-radius: 50%;
+          background-color: yellow;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+            .btn {
+              margin-left: 10px;
+            }
+          }
+          .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;
+        }
+      }
+    }
+  }
+  .right {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: 240px;
+    height: 100%;
+    border-radius: 10px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      margin-bottom: 5%;
+      margin-bottom: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 4px;
+      background-color: pink;
+      .title {
+        padding: 10px;
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin-bottom: 10px;
+        font-size: 12px;
+      }
+    }
+    .danmu-card {
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      height: 400px;
+      border-radius: 4px;
+      background-color: pink;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        margin-bottom: 10px;
+        height: 300px;
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          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;
+          width: 80px;
+          height: 30px;
+          border-radius: 4px;
+          background-color: #23ade5;
+          color: white;
+          text-align: center;
+          font-size: 12px;
+          line-height: 30px;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+}
+// 屏幕宽度小于$large-width的时候
+@media screen and (max-width: $large-width) {
+  .push-wrap {
+    .left {
+      width: $medium-left-width;
+    }
+    .right {
+      .list {
+        .item {
+        }
+      }
+    }
+  }
+}
+</style>