webRTC(十三):webrtc 统计信息

  • 设置布局
<div class="bitrate">
				<div class="graph-container" id="bitrateGraph">
					<div>Bitrate</div>
					<canvas id="bitrateCanvas"></canvas>
				</div>
				<div class="graph-container" id="packetGraph">
					<div>Packets sent per second</div>
					<canvas id="packetCanvas"></canvas>
				</div>
			</div>
  • 引入第三方graph.js
<script src="js/third_party/graph.js"></script>
源码后附
  • 统计信息
//统计信息
window.setInterval(()=>{
	if (!pc) {
	    return;
	}
	const sender = pc.getSenders()[0];
	if (!sender) {
	   return;
	}
	sender.getStats()
		  .then(res=>{
				res.forEach(report =>{
					let bytes;
    				let packets;
    				if (report.type === 'outbound-rtp') {
    					if (report.isRemote) {
				          return;
				        }
				        const now = report.timestamp;
				        bytes = report.bytesSent;
				        packets = report.packetsSent;
				        if (lastResult && lastResult.has(report.id)) {
				        	 const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /(now - lastResult.get(report.id).timestamp);	
				        	 
				        	  // append to chart
					          bitrateSeries.addPoint(now, bitrate);
					          bitrateGraph.setDataSeries([bitrateSeries]);
					          bitrateGraph.updateEndDate();
					
					          // calculate number of packets and app end to chart
					          packetSeries.addPoint(now, packets - lastResult.get(report.id).packetsSent);
					          packetGraph.setDataSeries([packetSeries]);
					          packetGraph.updateEndDate();
				        }
    				}
				});
				lastResult = res;
		  }).catch(err=>{
			console.error(err);
		  });
		  
},1000);
  • 效果

在这里插入图片描述
在这里插入图片描述

源码

  • html
<html>
	<head>
		<title>WebRTC PeerConnection</title>
		<style>
			.preview,
			.bitrate{
				display: flex;
			}
			.remote{
				margin-left: 20px;
			}
		
		</style>
	</head>
	<body>
		<div>
			<div>
				<button id="connserver">Connect Sig Server</button>
				<button id="leave" disabled>Leave</button>
			</div>
			<div>
				<label>BandWidth:</label>
				<select id="bandwidth" disabled>
					<option value="unlimited" selected>unlimited</option>
					<option value="2000">2000</option>
					<option value="1000">1000</option>
					<option value="500">500</option>
					<option value="250">250</option>
					<option value="125">125</option>
				</select>
				kps
			</div>
			<div class="preview">
				<div>
					<h2>Local:</h2>
					<video id="localvideo" autoplay playsinline></video>
				</div>
				<div class="remote">
					<h2>Remote:</h2>
					<video id="remotevideo" autoplay playsinline></video>
				</div>
			</div>
			<div class="bitrate">
				<div class="graph-container" id="bitrateGraph">
					<div>Bitrate</div>
					<canvas id="bitrateCanvas"></canvas>
				</div>
				<div class="graph-container" id="packetGraph">
					<div>Packets sent per second</div>
					<canvas id="packetCanvas"></canvas>
				</div>
			</div>
			
		</div>
		<script src="js/third_party/graph.js"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
		<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
		<script src="./js/main_bw.js"></script>
	</body>
</html>
  • css

button {
  margin: 10px 20px 25px 0;
  vertical-align: top;
  width: 134px;
}

table {
  margin: 200px (50% - 100) 0 0; 
}

textarea {
  color: #444;
  font-size: 0.9em;
  font-weight: 300;
  height: 20.0em;
  padding: 5px;
  width: calc(100% - 10px);
}

div#getUserMedia {
  padding: 0 0 8px 0;
}

div.input {
  display: inline-block;
  margin: 0 4px 0 0;
  vertical-align: top;
  width: 310px;
}

div.input > div {
  margin: 0 0 20px 0;
  vertical-align: top;
}

div.output {
  background-color: #eee;
  display: inline-block;
  font-family: 'Inconsolata', 'Courier New', monospace;
  font-size: 0.9em;
  padding: 10px 10px 10px 25px;
  position: relative;
  top: 10px;
  white-space: pre;
  width: 270px;
}

div.label {
	display: inline-block;
	font-weight: 400;
	width: 120px;
}

div.graph-container {
  background-color: #ccc;
  float: left;
  margin: 0.5em;
  width: calc(50%-1em);
}

div#preview {
  border-bottom: 1px solid #eee;
  margin: 0 0 1em 0;
  padding: 0 0 0.5em 0;
}

div#preview > div {
  display: inline-block;
  vertical-align: top;
  width: calc(50% - 12px);
}

section#statistics div {
  display: inline-block;
  font-family: 'Inconsolata', 'Courier New', monospace;
  vertical-align: top;
  width: 308px;
}

section#statistics div#senderStats {
  margin: 0 20px 0 0;
}

section#constraints > div {
  margin: 0 0 20px 0;
}

h2 {
  margin: 0 0 1em 0;
}


section#constraints label {
  display: inline-block;
  width: 156px;
}

section {
  margin: 0 0 20px 0;
  padding: 0 0 15px 0;
}

video {
  background: #222;
  margin: 0 0 0 0;
  --width: 100%;
  width: var(--width);
  height: 225px;
}

@media screen and (max-width: 720px) {
  button {
    font-weight: 500;
    height: 56px;
    line-height: 1.3em;
    width: 90px;
  }

  div#getUserMedia {
    padding: 0 0 40px 0;
  }

  section#statistics div {
    width: calc(50% - 14px);
  }

}

  • graph.js
/*
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
// taken from chrome://webrtc-internals with jshint adaptions

'use strict';
/* exported TimelineDataSeries, TimelineGraphView */

// The maximum number of data points bufferred for each stats. Old data points
// will be shifted out when the buffer is full.
const MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000;

const TimelineDataSeries = (function() {
  /**
   * @constructor
   */
  function TimelineDataSeries() {
    // List of DataPoints in chronological order.
    this.dataPoints_ = [];

    // Default color.  Should always be overridden prior to display.
    this.color_ = 'red';
    // Whether or not the data series should be drawn.
    this.isVisible_ = true;

    this.cacheStartTime_ = null;
    this.cacheStepSize_ = 0;
    this.cacheValues_ = [];
  }

  TimelineDataSeries.prototype = {
    /**
     * @override
     */
    toJSON: function() {
      if (this.dataPoints_.length < 1) {
        return {};
      }

      let values = [];
      for (let i = 0; i < this.dataPoints_.length; ++i) {
        values.push(this.dataPoints_[i].value);
      }
      return {
        startTime: this.dataPoints_[0].time,
        endTime: this.dataPoints_[this.dataPoints_.length - 1].time,
        values: JSON.stringify(values),
      };
    },

    /**
     * Adds a DataPoint to |this| with the specified time and value.
     * DataPoints are assumed to be received in chronological order.
     */
    addPoint: function(timeTicks, value) {
      let time = new Date(timeTicks);
      this.dataPoints_.push(new DataPoint(time, value));

      if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE) {
        this.dataPoints_.shift();
      }
    },

    isVisible: function() {
      return this.isVisible_;
    },

    show: function(isVisible) {
      this.isVisible_ = isVisible;
    },

    getColor: function() {
      return this.color_;
    },

    setColor: function(color) {
      this.color_ = color;
    },

    getCount: function() {
      return this.dataPoints_.length;
    },
    /**
     * Returns a list containing the values of the data series at |count|
     * points, starting at |startTime|, and |stepSize| milliseconds apart.
     * Caches values, so showing/hiding individual data series is fast.
     */
    getValues: function(startTime, stepSize, count) {
      // Use cached values, if we can.
      if (this.cacheStartTime_ === startTime &&
        this.cacheStepSize_ === stepSize &&
        this.cacheValues_.length === count) {
        return this.cacheValues_;
      }

      // Do all the work.
      this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count);
      this.cacheStartTime_ = startTime;
      this.cacheStepSize_ = stepSize;

      return this.cacheValues_;
    },

    /**
     * Returns the cached |values| in the specified time period.
     */
    getValuesInternal_: function(startTime, stepSize, count) {
      let values = [];
      let nextPoint = 0;
      let currentValue = 0;
      let time = startTime;
      for (let i = 0; i < count; ++i) {
        while (nextPoint < this.dataPoints_.length &&
        this.dataPoints_[nextPoint].time < time) {
          currentValue = this.dataPoints_[nextPoint].value;
          ++nextPoint;
        }
        values[i] = currentValue;
        time += stepSize;
      }
      return values;
    }
  };

  /**
   * A single point in a data series.  Each point has a time, in the form of
   * milliseconds since the Unix epoch, and a numeric value.
   * @constructor
   */
  function DataPoint(time, value) {
    this.time = time;
    this.value = value;
  }

  return TimelineDataSeries;
})();

const TimelineGraphView = (function() {
  // Maximum number of labels placed vertically along the sides of the graph.
  let MAX_VERTICAL_LABELS = 6;

  // Vertical spacing between labels and between the graph and labels.
  let LABEL_VERTICAL_SPACING = 4;
  // Horizontal spacing between vertically placed labels and the edges of the
  // graph.
  let LABEL_HORIZONTAL_SPACING = 3;
  // Horizintal spacing between two horitonally placed labels along the bottom
  // of the graph.
  // var LABEL_LABEL_HORIZONTAL_SPACING = 25;

  // Length of ticks, in pixels, next to y-axis labels.  The x-axis only has
  // one set of labels, so it can use lines instead.
  let Y_AXIS_TICK_LENGTH = 10;

  let GRID_COLOR = '#CCC';
  let TEXT_COLOR = '#000';
  let BACKGROUND_COLOR = '#FFF';

  let MAX_DECIMAL_PRECISION = 2;

  /**
   * @constructor
   */
  function TimelineGraphView(divId, canvasId) {
    this.scrollbar_ = {position_: 0, range_: 0};

    this.graphDiv_ = document.getElementById(divId);
    this.canvas_ = document.getElementById(canvasId);

    // Set the range and scale of the graph.  Times are in milliseconds since
    // the Unix epoch.

    // All measurements we have must be after this time.
    this.startTime_ = 0;
    // The current rightmost position of the graph is always at most this.
    this.endTime_ = 1;

    this.graph_ = null;

    // Horizontal scale factor, in terms of milliseconds per pixel.
    this.scale_ = 1000;

    // Initialize the scrollbar.
    this.updateScrollbarRange_(true);
  }

  TimelineGraphView.prototype = {
    setScale: function(scale) {
      this.scale_ = scale;
    },

    // Returns the total length of the graph, in pixels.
    getLength_: function() {
      let timeRange = this.endTime_ - this.startTime_;
      // Math.floor is used to ignore the last partial area, of length less
      // than this.scale_.
      return Math.floor(timeRange / this.scale_);
    },

    /**
     * Returns true if the graph is scrolled all the way to the right.
     */
    graphScrolledToRightEdge_: function() {
      return this.scrollbar_.position_ === this.scrollbar_.range_;
    },

    /**
     * Update the range of the scrollbar.  If |resetPosition| is true, also
     * sets the slider to point at the rightmost position and triggers a
     * repaint.
     */
    updateScrollbarRange_: function(resetPosition) {
      let scrollbarRange = this.getLength_() - this.canvas_.width;
      if (scrollbarRange < 0) {
        scrollbarRange = 0;
      }

      // If we've decreased the range to less than the current scroll position,
      // we need to move the scroll position.
      if (this.scrollbar_.position_ > scrollbarRange) {
        resetPosition = true;
      }

      this.scrollbar_.range_ = scrollbarRange;
      if (resetPosition) {
        this.scrollbar_.position_ = scrollbarRange;
        this.repaint();
      }
    },

    /**
     * Sets the date range displayed on the graph, switches to the default
     * scale factor, and moves the scrollbar all the way to the right.
     */
    setDateRange: function(startDate, endDate) {
      this.startTime_ = startDate.getTime();
      this.endTime_ = endDate.getTime();

      // Safety check.
      if (this.endTime_ <= this.startTime_) {
        this.startTime_ = this.endTime_ - 1;
      }

      this.updateScrollbarRange_(true);
    },

    /**
     * Updates the end time at the right of the graph to be the current time.
     * Specifically, updates the scrollbar's range, and if the scrollbar is
     * all the way to the right, keeps it all the way to the right.  Otherwise,
     * leaves the view as-is and doesn't redraw anything.
     */
    updateEndDate: function(optDate) {
      this.endTime_ = optDate || (new Date()).getTime();
      this.updateScrollbarRange_(this.graphScrolledToRightEdge_());
    },

    getStartDate: function() {
      return new Date(this.startTime_);
    },

    /**
     * Replaces the current TimelineDataSeries with |dataSeries|.
     */
    setDataSeries: function(dataSeries) {
      // Simply recreates the Graph.
      this.graph_ = new Graph();
      for (let i = 0; i < dataSeries.length; ++i) {
        this.graph_.addDataSeries(dataSeries[i]);
      }
      this.repaint();
    },

    /**
     * Adds |dataSeries| to the current graph.
     */
    addDataSeries: function(dataSeries) {
      if (!this.graph_) {
        this.graph_ = new Graph();
      }
      this.graph_.addDataSeries(dataSeries);
      this.repaint();
    },

    /**
     * Draws the graph on |canvas_|.
     */
    repaint: function() {
      this.repaintTimerRunning_ = false;

      let width = this.canvas_.width;
      let height = this.canvas_.height;
      let context = this.canvas_.getContext('2d');

      // Clear the canvas.
      context.fillStyle = BACKGROUND_COLOR;
      context.fillRect(0, 0, width, height);

      // Try to get font height in pixels.  Needed for layout.
      let fontHeightString = context.font.match(/([0-9]+)px/)[1];
      let fontHeight = parseInt(fontHeightString);

      // Safety check, to avoid drawing anything too ugly.
      if (fontHeightString.length === 0 || fontHeight <= 0 ||
        fontHeight * 4 > height || width < 50) {
        return;
      }

      // Save current transformation matrix so we can restore it later.
      context.save();

      // The center of an HTML canvas pixel is technically at (0.5, 0.5).  This
      // makes near straight lines look bad, due to anti-aliasing.  This
      // translation reduces the problem a little.
      context.translate(0.5, 0.5);

      // Figure out what time values to display.
      let position = this.scrollbar_.position_;
      // If the entire time range is being displayed, align the right edge of
      // the graph to the end of the time range.
      if (this.scrollbar_.range_ === 0) {
        position = this.getLength_() - this.canvas_.width;
      }
      let visibleStartTime = this.startTime_ + position * this.scale_;

      // Make space at the bottom of the graph for the time labels, and then
      // draw the labels.
      let textHeight = height;
      height -= fontHeight + LABEL_VERTICAL_SPACING;
      this.drawTimeLabels(context, width, height, textHeight, visibleStartTime);

      // Draw outline of the main graph area.
      context.strokeStyle = GRID_COLOR;
      context.strokeRect(0, 0, width - 1, height - 1);

      if (this.graph_) {
        // Layout graph and have them draw their tick marks.
        this.graph_.layout(
          width, height, fontHeight, visibleStartTime, this.scale_);
        this.graph_.drawTicks(context);

        // Draw the lines of all graphs, and then draw their labels.
        this.graph_.drawLines(context);
        this.graph_.drawLabels(context);
      }

      // Restore original transformation matrix.
      context.restore();
    },

    /**
     * Draw time labels below the graph.  Takes in start time as an argument
     * since it may not be |startTime_|, when we're displaying the entire
     * time range.
     */
    drawTimeLabels: function(context, width, height, textHeight, startTime) {
      // Draw the labels 1 minute apart.
      let timeStep = 1000 * 60;

      // Find the time for the first label.  This time is a perfect multiple of
      // timeStep because of how UTC times work.
      let time = Math.ceil(startTime / timeStep) * timeStep;

      context.textBaseline = 'bottom';
      context.textAlign = 'center';
      context.fillStyle = TEXT_COLOR;
      context.strokeStyle = GRID_COLOR;

      // Draw labels and vertical grid lines.
      while (true) {
        let x = Math.round((time - startTime) / this.scale_);
        if (x >= width) {
          break;
        }
        let text = (new Date(time)).toLocaleTimeString();
        context.fillText(text, x, textHeight);
        context.beginPath();
        context.lineTo(x, 0);
        context.lineTo(x, height);
        context.stroke();
        time += timeStep;
      }
    },

    getDataSeriesCount: function() {
      if (this.graph_) {
        return this.graph_.dataSeries_.length;
      }
      return 0;
    },

    hasDataSeries: function(dataSeries) {
      if (this.graph_) {
        return this.graph_.hasDataSeries(dataSeries);
      }
      return false;
    },

  };

  /**
   * A Graph is responsible for drawing all the TimelineDataSeries that have
   * the same data type.  Graphs are responsible for scaling the values, laying
   * out labels, and drawing both labels and lines for its data series.
   */
  const Graph = (function() {
    /**
     * @constructor
     */
    function Graph() {
      this.dataSeries_ = [];

      // Cached properties of the graph, set in layout.
      this.width_ = 0;
      this.height_ = 0;
      this.fontHeight_ = 0;
      this.startTime_ = 0;
      this.scale_ = 0;

      // The lowest/highest values adjusted by the vertical label step size
      // in the displayed range of the graph. Used for scaling and setting
      // labels.  Set in layoutLabels.
      this.min_ = 0;
      this.max_ = 0;

      // Cached text of equally spaced labels.  Set in layoutLabels.
      this.labels_ = [];
    }

    /**
     * A Label is the label at a particular position along the y-axis.
     * @constructor
     */
    /*
    function Label(height, text) {
      this.height = height;
      this.text = text;
    }
    */

    Graph.prototype = {
      addDataSeries: function(dataSeries) {
        this.dataSeries_.push(dataSeries);
      },

      hasDataSeries: function(dataSeries) {
        for (let i = 0; i < this.dataSeries_.length; ++i) {
          if (this.dataSeries_[i] === dataSeries) {
            return true;
          }
        }
        return false;
      },

      /**
       * Returns a list of all the values that should be displayed for a given
       * data series, using the current graph layout.
       */
      getValues: function(dataSeries) {
        if (!dataSeries.isVisible()) {
          return null;
        }
        return dataSeries.getValues(this.startTime_, this.scale_, this.width_);
      },

      /**
       * Updates the graph's layout.  In particular, both the max value and
       * label positions are updated.  Must be called before calling any of the
       * drawing functions.
       */
      layout: function(width, height, fontHeight, startTime, scale) {
        this.width_ = width;
        this.height_ = height;
        this.fontHeight_ = fontHeight;
        this.startTime_ = startTime;
        this.scale_ = scale;

        // Find largest value.
        let max = 0;
        let min = 0;
        for (let i = 0; i < this.dataSeries_.length; ++i) {
          let values = this.getValues(this.dataSeries_[i]);
          if (!values) {
            continue;
          }
          for (let j = 0; j < values.length; ++j) {
            if (values[j] > max) {
              max = values[j];
            } else if (values[j] < min) {
              min = values[j];
            }
          }
        }

        this.layoutLabels_(min, max);
      },

      /**
       * Lays out labels and sets |max_|/|min_|, taking the time units into
       * consideration.  |maxValue| is the actual maximum value, and
       * |max_| will be set to the value of the largest label, which
       * will be at least |maxValue|. Similar for |min_|.
       */
      layoutLabels_: function(minValue, maxValue) {
        if (maxValue - minValue < 1024) {
          this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION);
          return;
        }

        // Find appropriate units to use.
        let units = ['', 'k', 'M', 'G', 'T', 'P'];
        // Units to use for labels.  0 is '1', 1 is K, etc.
        // We start with 1, and work our way up.
        let unit = 1;
        minValue /= 1024;
        maxValue /= 1024;
        while (units[unit + 1] && maxValue - minValue >= 1024) {
          minValue /= 1024;
          maxValue /= 1024;
          ++unit;
        }

        // Calculate labels.
        this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION);

        // Append units to labels.
        for (let i = 0; i < this.labels_.length; ++i) {
          this.labels_[i] += ' ' + units[unit];
        }

        // Convert |min_|/|max_| back to unit '1'.
        this.min_ *= Math.pow(1024, unit);
        this.max_ *= Math.pow(1024, unit);
      },

      /**
       * Same as layoutLabels_, but ignores units.  |maxDecimalDigits| is the
       * maximum number of decimal digits allowed.  The minimum allowed
       * difference between two adjacent labels is 10^-|maxDecimalDigits|.
       */
      layoutLabelsBasic_: function(minValue, maxValue, maxDecimalDigits) {
        this.labels_ = [];
        let range = maxValue - minValue;
        // No labels if the range is 0.
        if (range === 0) {
          this.min_ = this.max_ = maxValue;
          return;
        }

        // The maximum number of equally spaced labels allowed.  |fontHeight_|
        // is doubled because the top two labels are both drawn in the same
        // gap.
        let minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING;

        // The + 1 is for the top label.
        let maxLabels = 1 + this.height_ / minLabelSpacing;
        if (maxLabels < 2) {
          maxLabels = 2;
        } else if (maxLabels > MAX_VERTICAL_LABELS) {
          maxLabels = MAX_VERTICAL_LABELS;
        }

        // Initial try for step size between conecutive labels.
        let stepSize = Math.pow(10, -maxDecimalDigits);
        // Number of digits to the right of the decimal of |stepSize|.
        // Used for formating label strings.
        let stepSizeDecimalDigits = maxDecimalDigits;

        // Pick a reasonable step size.
        while (true) {
          // If we use a step size of |stepSize| between labels, we'll need:
          //
          // Math.ceil(range / stepSize) + 1
          //
          // labels.  The + 1 is because we need labels at both at 0 and at
          // the top of the graph.

          // Check if we can use steps of size |stepSize|.
          if (Math.ceil(range / stepSize) + 1 <= maxLabels) {
            break;
          }
          // Check |stepSize| * 2.
          if (Math.ceil(range / (stepSize * 2)) + 1 <= maxLabels) {
            stepSize *= 2;
            break;
          }
          // Check |stepSize| * 5.
          if (Math.ceil(range / (stepSize * 5)) + 1 <= maxLabels) {
            stepSize *= 5;
            break;
          }
          stepSize *= 10;
          if (stepSizeDecimalDigits > 0) {
            --stepSizeDecimalDigits;
          }
        }

        // Set the min/max so it's an exact multiple of the chosen step size.
        this.max_ = Math.ceil(maxValue / stepSize) * stepSize;
        this.min_ = Math.floor(minValue / stepSize) * stepSize;

        // Create labels.
        for (let label = this.max_; label >= this.min_; label -= stepSize) {
          this.labels_.push(label.toFixed(stepSizeDecimalDigits));
        }
      },

      /**
       * Draws tick marks for each of the labels in |labels_|.
       */
      drawTicks: function(context) {
        let x1;
        let x2;
        x1 = this.width_ - 1;
        x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH;

        context.fillStyle = GRID_COLOR;
        context.beginPath();
        for (let i = 1; i < this.labels_.length - 1; ++i) {
          // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
          // lines.
          let y = Math.round(this.height_ * i / (this.labels_.length - 1));
          context.moveTo(x1, y);
          context.lineTo(x2, y);
        }
        context.stroke();
      },

      /**
       * Draws a graph line for each of the data series.
       */
      drawLines: function(context) {
        // Factor by which to scale all values to convert them to a number from
        // 0 to height - 1.
        let scale = 0;
        let bottom = this.height_ - 1;
        if (this.max_) {
          scale = bottom / (this.max_ - this.min_);
        }

        // Draw in reverse order, so earlier data series are drawn on top of
        // subsequent ones.
        for (let i = this.dataSeries_.length - 1; i >= 0; --i) {
          let values = this.getValues(this.dataSeries_[i]);
          if (!values) {
            continue;
          }
          context.strokeStyle = this.dataSeries_[i].getColor();
          context.beginPath();
          for (let x = 0; x < values.length; ++x) {
            // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
            // horizontal lines.
            context.lineTo(
              x, bottom - Math.round((values[x] - this.min_) * scale));
          }
          context.stroke();
        }
      },

      /**
       * Draw labels in |labels_|.
       */
      drawLabels: function(context) {
        if (this.labels_.length === 0) {
          return;
        }
        let x = this.width_ - LABEL_HORIZONTAL_SPACING;

        // Set up the context.
        context.fillStyle = TEXT_COLOR;
        context.textAlign = 'right';

        // Draw top label, which is the only one that appears below its tick
        // mark.
        context.textBaseline = 'top';
        context.fillText(this.labels_[0], x, 0);

        // Draw all the other labels.
        context.textBaseline = 'bottom';
        let step = (this.height_ - 1) / (this.labels_.length - 1);
        for (let i = 1; i < this.labels_.length; ++i) {
          context.fillText(this.labels_[i], x, step * i);
        }
      }
    };

    return Graph;
  })();

  return TimelineGraphView;
})();

  • js
'use strict'

var localVideo = document.querySelector('video#localvideo');
var remoteVideo = document.querySelector('video#remotevideo');

var btnConn = document.querySelector('button#connserver');
var btnLeave = document.querySelector('button#leave');

var optBw = document.querySelector('select#bandwidth')


var localStream = null;

var roomid = '444444';
var socket =null;

var state = 'init';

var pc = null;

var lastResult;
var packetGraph;
var packetSeries;
var bitrateGraph;
var bitrateSeries;

var pcConfig={
		'iceServers':[{
			'urls':'turn:121.41.76.43:3478',
			'credential':'123456',
			'username':'huang'
		}]
	}

function sendMessage(roomid,data){
	if(socket){
		socket.emit('message',roomid,data);
	}
}

function getAnswer(desc){
	pc.setLocalDescription(desc);
	optBw.disabled =false;
	sendMessage(roomid,desc);
}

function handleAnswerError(err){
	console.error('Failed to get Answer!',err);
}

function getOffer(desc){
	pc.setLocalDescription(desc);
	sendMessage(roomid,desc)
}
function handleOfferError(err){
	console.error('Failed to get Offer!',err);
}

//接收远端流通道
function call(){
	if(state === 'joined_conn'){
		if(pc){
			var options = {
				offerToReceiveAudio:1,
				offerToReceiveVideo:1
			}
			pc.createOffer(options)
			  .then(getOffer)
			  .catch(handleOfferError);
		}
	}	
}

// 第一步:开始服务
function connSignalServer(){
	//开启本地视频
	start();
	return true;
}

function conn(){
	//1 触发socke连接
	socket = io.connect();
	
	//2 加入房间后的回调
	socket.on('joined',(roomid,id)=>{
		
		state = 'joined';
		
		createPeerConnection();
		
		btnConn.disabled = true;
		btnLeave.disabled =false;
		
		console.log("reveive joined message:state=",state);	
	});
	socket.on('otherjoin',(roomid,id)=>{
		
		if (state === 'joined_unbind') {
			createPeerConnection();
		}
		state = 'joined_conn';
		
		//媒体协商
		call();
		console.log("reveive otherjoin message:state=",state);	
	});
	socket.on('full',(roomid,id)=>{
		console.log('receive full message ', roomid, id);

		closePeerConnection();
		closeLocalMedia();
		
		state = 'leaved';
		
		btnConn.disabled = false;
		btnLeave.disabled = true;
		console.log("reveive full message:state=",state);
		alert("the room is full!");
	});
	
	socket.on('leaved',(roomid,id)=>{
		
		state = 'leaved';
		socket.disconnect();
		btnConn.disabled = false;
		btnLeave.disabled = true;
		console.log("reveive leaved message:state=",state);
	});
	
	socket.on('bye',(roomid,id)=>{
		
		state = 'joined_unbind';
		closePeerConnection();
		console.log("reveive bye message:state=",state);	
	});
	socket.on('disconnect', (socket) => {
		console.log('receive disconnect message!', roomid);
		if(!(state === 'leaved')){
			closePeerConnection();
			closeLocalMedia();
		}
		state = 'leaved';
	
	});
	socket.on('message',(roomid,id,data)=>{
		
		//媒体协商
		if(data){
			if(data.type === 'offer'){
				pc.setRemoteDescription(new RTCSessionDescription(data));
				pc.createAnswer()
				  .then(getAnswer)
				  .catch(handleAnswerError);
			}else if(data.type === 'answer'){
				console.log("reveive client message=====>",data);
				optBw.disabled = false;
				pc.setRemoteDescription(new RTCSessionDescription(data));
			}else if(data.type === 'candidate'){
				var candidate = new RTCIceCandidate({
					sdpMLineIndex:data.label,
					candidate:data.candidate
				});
				pc.addIceCandidate(candidate);
				
			}else{
				console.error('the message is invalid!',data)
			}
		}
		
		console.log("reveive client message",roomid,id,data);	
	});
	
	socket.emit('join',roomid);
	return;
}

// 扑捉本地视频
function getMediaStream(stream){
	
	localStream =stream;
	//2 ===============显示本地视频===============
	localVideo.srcObject = localStream;
	
	//这个函数的调用时机特别重要 一定要在getMediaStream之后再调用,否则会出现绑定失败的情况
	conn();
	
	//信息统计
	bitrateSeries = new TimelineDataSeries();
	bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
	bitrateGraph.updateEndDate();

	packetSeries = new TimelineDataSeries();
	packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
	packetGraph.updateEndDate();
}

function handleError(err){
	if(err){
		console.error("getUserMedia  error:",err);	
	}
}

// 第二步:采集本地视频
function start(){
	
	
	if (!navigator.mediaDevices||
			!navigator.mediaDevices.getUserMedia) {			
		  console.log("getUserMedia is not supported!")
		  return;
	} else {
		
		//1 ===============配置音视频参数===============
		var constraints={
			video:true,
			audio: false
		}
		
		navigator.mediaDevices.getUserMedia(constraints)
							  .then(getMediaStream)
							  .catch(handleError)
	}
}

//关闭流通道
function closeLocalMedia(){
	if (localStream&&localStream.getTracks()) {
		localStream.getTracks().forEach((track)=>{
			track.stop();	
		});
	}
	localStream = null;
}


function leave(){
	if(socket){
		socket.emit('leave',roomid);
	}
	
	//释放资源
	closePeerConnection();
	closeLocalMedia();
		
	btnConn.disabled = false;
	btnLeave.disabled = true;
}

//创建本地流媒体链接
function createPeerConnection(){
	console.log('create RTCPeerConnection!');
	if(!pc){
		pc = new RTCPeerConnection(pcConfig);
		pc.onicecandidate = (e) =>{
			if(e.candidate){
				sendMessage(roomid,{
					type:'candidate',
					label:e.candidate.sdpMLineIndex,
					id:e.candidate.sdpMid,
					candidate:e.candidate.candidate
				});
			}
		}
		pc.ontrack = (e)=>{
			remoteVideo.srcObject = e.streams[0];
		}
	}
	if(pc === null || pc === undefined){
			console.error('pc is null or undefined!');
			return;
	}

	if(localStream === null || localStream === undefined){
		console.error('localStream is null or undefined!');
		return;
	}

	if(localStream){
		localStream.getTracks().forEach((track)=>{
			pc.addTrack(track,localStream);
		})
	}
}

//关闭本地媒体流链接
function closePeerConnection(){
	console.log('close RTCPeerConnection!');
	if(pc){
		pc.close();
		pc = null;
	}
}

//控制传输速率
function chang_bw(){
	optBw.disabled =true;
	var bw = optBw.options[optBw.selectedIndex].value;
	
	var vsender = null;
	var senders = pc.getSenders();
	senders.forEach(sender=>{
		if(sender&&sender.track && sender.track.kind === 'video'){
			vsender = sender;
		}	
	});
	var parameters= vsender.getParameters();
	if(!parameters.encodings){
		return;
	}
	if(bw === 'unlimited'){
		return;
	}
	parameters.encodings[0].maxBitrate = bw * 1000;
	vsender.setParameters(parameters)
		   .then(()=>{
		   		optBw.disabled =false;
		   	 console.log('Successed to set parameters!');
		   })
		   .catch(err=>{
		   		 console.error(err);
		   })
	
}


//统计信息
window.setInterval(()=>{
	if (!pc) {
	    return;
	}
	const sender = pc.getSenders()[0];
	if (!sender) {
	   return;
	}
	sender.getStats()
		  .then(res=>{
				res.forEach(report =>{
					let bytes;
    				let packets;
    				if (report.type === 'outbound-rtp') {
    					if (report.isRemote) {
				          return;
				        }
				        const now = report.timestamp;
				        bytes = report.bytesSent;
				        packets = report.packetsSent;
				        if (lastResult && lastResult.has(report.id)) {
				        	 const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /(now - lastResult.get(report.id).timestamp);	
				        	 
				        	  // append to chart
					          bitrateSeries.addPoint(now, bitrate);
					          bitrateGraph.setDataSeries([bitrateSeries]);
					          bitrateGraph.updateEndDate();
					
					          // calculate number of packets and app end to chart
					          packetSeries.addPoint(now, packets - lastResult.get(report.id).packetsSent);
					          packetGraph.setDataSeries([packetSeries]);
					          packetGraph.updateEndDate();
				        }
    				}
				});
				lastResult = res;
		  }).catch(err=>{
			console.error(err);
		  });
		  
},1000);
btnConn.onclick = connSignalServer;

btnLeave.onclick = leave;


optBw.onchange=chang_bw;




测试地址:https://www.huangxiaoguo.club/getstats/index.html

发布了316 篇原创文章 · 获赞 660 · 访问量 122万+

猜你喜欢

转载自blog.csdn.net/huangxiaoguo1/article/details/104230067
今日推荐