webRtcPlayer.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // Copyright Epic Games, Inc. All Rights Reserved.
  2. // universal module definition - read https://www.davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/
  3. (function (root, factory) {
  4. if (typeof define === 'function' && define.amd) {
  5. // AMD. Register as an anonymous module.
  6. define(["./adapter"], factory);
  7. } else if (typeof exports === 'object') {
  8. // Node. Does not work with strict CommonJS, but
  9. // only CommonJS-like environments that support module.exports,
  10. // like Node.
  11. module.exports = factory(require("./adapter"));
  12. } else {
  13. // Browser globals (root is window)
  14. // root.webRtcPlayer = factory(root.adapter);
  15. window.webRtcPlayer = factory(window.adapter);
  16. }
  17. }(this, function (adapter) {
  18. function webRtcPlayer(parOptions) {
  19. parOptions = parOptions || {};
  20. var self = this;
  21. //**********************
  22. //Config setup
  23. //**********************;
  24. this.cfg = parOptions.peerConnectionOptions || {};
  25. this.cfg.sdpSemantics = 'unified-plan';
  26. //If this is true in Chrome 89+ SDP is sent that is incompatible with UE WebRTC and breaks.
  27. this.cfg.offerExtmapAllowMixed = false;
  28. this.pcClient = null;
  29. this.dcClient = null;
  30. this.tnClient = null;
  31. this.sdpConstraints = {
  32. offerToReceiveAudio: 1,
  33. offerToReceiveVideo: 1
  34. };
  35. // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values
  36. this.dataChannelOptions = {ordered: true};
  37. //**********************
  38. //Functions
  39. //**********************
  40. //Create Video element and expose that as a parameter
  41. function createWebRtcVideo () {
  42. var video = document.createElement('video');
  43. video.id = "streamingVideo";
  44. video.playsInline = true;
  45. video.muted = 'muted'
  46. video.addEventListener('loadedmetadata', function(e){
  47. if(self.onVideoInitialised){
  48. self.onVideoInitialised();
  49. }
  50. }, true);
  51. return video;
  52. }
  53. this.video = createWebRtcVideo();
  54. function onsignalingstatechange (state) {
  55. //console.info('signaling state change:', state)
  56. };
  57. function oniceconnectionstatechange (state) {
  58. //console.info('ice connection state change:', state)
  59. };
  60. function onicegatheringstatechange (state) {
  61. //console.info('ice gathering state change:', state)
  62. };
  63. function handleOnTrack (e) {
  64. //console.log('handleOnTrack', e.streams);
  65. if (self.video.srcObject !== e.streams[0]) {
  66. //console.log('setting video stream from ontrack');
  67. self.video.srcObject = e.streams[0];
  68. }
  69. };
  70. function setupDataChannel (pc, label, options) {
  71. try {
  72. var datachannel = pc.createDataChannel(label, options)
  73. //console.log(`Created datachannel (${label})`)
  74. datachannel.onopen = function (e) {
  75. //console.log(`data channel (${label}) connect`)
  76. if(self.onDataChannelConnected){
  77. self.onDataChannelConnected();
  78. }
  79. }
  80. datachannel.onclose = function (e) {
  81. //console.log(`data channel (${label}) closed`)
  82. }
  83. datachannel.onmessage = function (e) {
  84. // //console.log(`Got message (${label})`, e.data)
  85. if (self.onDataChannelMessage)
  86. self.onDataChannelMessage(e.data);
  87. }
  88. return datachannel;
  89. } catch (e) {
  90. console.warn('No data channel', e);
  91. return null;
  92. }
  93. }
  94. function onicecandidate (e) {
  95. //console.log('ICE candidate', e)
  96. if (e.candidate && e.candidate.candidate) {
  97. self.onWebRtcCandidate(e.candidate);
  98. }
  99. };
  100. function handleCreateOffer (pc) {
  101. pc.createOffer(self.sdpConstraints).then(function (offer) {
  102. offer.sdp = offer.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1;maxaveragebitrate=128000");
  103. pc.setLocalDescription(offer);
  104. if (self.onWebRtcOffer) {
  105. // (andriy): increase start bitrate from 300 kbps to 20 mbps and max bitrate from 2.5 mbps to 100 mbps
  106. // (100 mbps means we don't restrict encoder at all)
  107. // after we `setLocalDescription` because other browsers are not c happy to see google-specific config
  108. offer.sdp = offer.sdp.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, "$1;x-google-start-bitrate=10000;x-google-max-bitrate=20000\r\n");
  109. self.onWebRtcOffer(offer);
  110. }
  111. },
  112. function () { console.warn("Couldn't create offer") });
  113. }
  114. function setupPeerConnection (pc) {
  115. if (pc.SetBitrate)
  116. //console.log("Hurray! there's RTCPeerConnection.SetBitrate function");
  117. //Setup peerConnection events
  118. pc.onsignalingstatechange = onsignalingstatechange;
  119. pc.oniceconnectionstatechange = oniceconnectionstatechange;
  120. pc.onicegatheringstatechange = onicegatheringstatechange;
  121. pc.ontrack = handleOnTrack;
  122. pc.onicecandidate = onicecandidate;
  123. };
  124. function generateAggregatedStatsFunction (){
  125. if(!self.aggregatedStats)
  126. self.aggregatedStats = {};
  127. return function(stats){
  128. ////console.log('Printing Stats');
  129. let newStat = {};
  130. // //console.log('----------------------------- Stats start -----------------------------');
  131. stats.forEach(stat => {
  132. // //console.log(JSON.stringify(stat, undefined, 4));
  133. if (stat.type == 'inbound-rtp'
  134. && !stat.isRemote
  135. && (stat.mediaType == 'video' || stat.id.toLowerCase().includes('video'))) {
  136. newStat.timestamp = stat.timestamp;
  137. newStat.bytesReceived = stat.bytesReceived;
  138. newStat.framesDecoded = stat.framesDecoded;
  139. newStat.packetsLost = stat.packetsLost;
  140. newStat.bytesReceivedStart = self.aggregatedStats && self.aggregatedStats.bytesReceivedStart ? self.aggregatedStats.bytesReceivedStart : stat.bytesReceived;
  141. newStat.framesDecodedStart = self.aggregatedStats && self.aggregatedStats.framesDecodedStart ? self.aggregatedStats.framesDecodedStart : stat.framesDecoded;
  142. newStat.timestampStart = self.aggregatedStats && self.aggregatedStats.timestampStart ? self.aggregatedStats.timestampStart : stat.timestamp;
  143. if(self.aggregatedStats && self.aggregatedStats.timestamp){
  144. if(self.aggregatedStats.bytesReceived){
  145. // bitrate = bits received since last time / number of ms since last time
  146. //This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
  147. newStat.bitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceived) / (newStat.timestamp - self.aggregatedStats.timestamp);
  148. newStat.bitrate = Math.floor(newStat.bitrate);
  149. newStat.lowBitrate = self.aggregatedStats.lowBitrate && self.aggregatedStats.lowBitrate < newStat.bitrate ? self.aggregatedStats.lowBitrate : newStat.bitrate
  150. newStat.highBitrate = self.aggregatedStats.highBitrate && self.aggregatedStats.highBitrate > newStat.bitrate ? self.aggregatedStats.highBitrate : newStat.bitrate
  151. }
  152. if(self.aggregatedStats.bytesReceivedStart){
  153. newStat.avgBitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceivedStart) / (newStat.timestamp - self.aggregatedStats.timestampStart);
  154. newStat.avgBitrate = Math.floor(newStat.avgBitrate);
  155. }
  156. if(self.aggregatedStats.framesDecoded){
  157. // framerate = frames decoded since last time / number of seconds since last time
  158. newStat.framerate = (newStat.framesDecoded - self.aggregatedStats.framesDecoded) / ((newStat.timestamp - self.aggregatedStats.timestamp) / 1000);
  159. newStat.framerate = Math.floor(newStat.framerate);
  160. newStat.lowFramerate = self.aggregatedStats.lowFramerate && self.aggregatedStats.lowFramerate < newStat.framerate ? self.aggregatedStats.lowFramerate : newStat.framerate
  161. newStat.highFramerate = self.aggregatedStats.highFramerate && self.aggregatedStats.highFramerate > newStat.framerate ? self.aggregatedStats.highFramerate : newStat.framerate
  162. }
  163. if(self.aggregatedStats.framesDecodedStart){
  164. newStat.avgframerate = (newStat.framesDecoded - self.aggregatedStats.framesDecodedStart) / ((newStat.timestamp - self.aggregatedStats.timestampStart) / 1000);
  165. newStat.avgframerate = Math.floor(newStat.avgframerate);
  166. }
  167. }
  168. }
  169. //Read video track stats
  170. if(stat.type == 'track' && (stat.trackIdentifier == 'video_label' || stat.kind == 'video')) {
  171. newStat.framesDropped = stat.framesDropped;
  172. newStat.framesReceived = stat.framesReceived;
  173. newStat.framesDroppedPercentage = stat.framesDropped / stat.framesReceived * 100;
  174. newStat.frameHeight = stat.frameHeight;
  175. newStat.frameWidth = stat.frameWidth;
  176. newStat.frameHeightStart = self.aggregatedStats && self.aggregatedStats.frameHeightStart ? self.aggregatedStats.frameHeightStart : stat.frameHeight;
  177. newStat.frameWidthStart = self.aggregatedStats && self.aggregatedStats.frameWidthStart ? self.aggregatedStats.frameWidthStart : stat.frameWidth;
  178. }
  179. if(stat.type =='candidate-pair' && stat.hasOwnProperty('currentRoundTripTime') && stat.currentRoundTripTime != 0){
  180. newStat.currentRoundTripTime = stat.currentRoundTripTime;
  181. }
  182. });
  183. ////console.log(JSON.stringify(newStat));
  184. self.aggregatedStats = newStat;
  185. if(self.onAggregatedStats)
  186. self.onAggregatedStats(newStat)
  187. }
  188. };
  189. //**********************
  190. //Public functions
  191. //**********************
  192. //This is called when revceiving new ice candidates individually instead of part of the offer
  193. //This is currently not used but would be called externally from this class
  194. this.handleCandidateFromServer = function(iceCandidate) {
  195. //console.log("ICE candidate: ", iceCandidate);
  196. let candidate = new RTCIceCandidate(iceCandidate);
  197. self.pcClient.addIceCandidate(candidate).then(_=>{
  198. //console.log('ICE candidate successfully added');
  199. });
  200. };
  201. //Called externaly to create an offer for the server
  202. this.createOffer = function() {
  203. if(self.pcClient){
  204. //console.log("Closing existing PeerConnection")
  205. self.pcClient.close();
  206. self.pcClient = null;
  207. }
  208. self.pcClient = new RTCPeerConnection(self.cfg);
  209. setupPeerConnection(self.pcClient);
  210. self.dcClient = setupDataChannel(self.pcClient, 'cirrus', self.dataChannelOptions);
  211. handleCreateOffer(self.pcClient);
  212. };
  213. //Called externaly when an answer is received from the server
  214. this.receiveAnswer = function(answer) {
  215. //console.log(`Received answer:\n${answer}`);
  216. var answerDesc = new RTCSessionDescription(answer);
  217. self.pcClient.setRemoteDescription(answerDesc);
  218. };
  219. this.close = function(){
  220. if(self.pcClient){
  221. //console.log("Closing existing peerClient")
  222. self.pcClient.close();
  223. self.pcClient = null;
  224. }
  225. if(self.aggregateStatsIntervalId)
  226. clearInterval(self.aggregateStatsIntervalId);
  227. }
  228. //Sends data across the datachannel
  229. this.send = function(data){
  230. if(self.dcClient && self.dcClient.readyState == 'open'){
  231. ////console.log('Sending data on dataconnection', self.dcClient)
  232. self.dcClient.send(data);
  233. }
  234. };
  235. this.getStats = function(onStats){
  236. if(self.pcClient && onStats){
  237. self.pcClient.getStats(null).then((stats) => {
  238. onStats(stats);
  239. });
  240. }
  241. }
  242. this.aggregateStats = function(checkInterval){
  243. let calcAggregatedStats = generateAggregatedStatsFunction();
  244. let printAggregatedStats = () => { self.getStats(calcAggregatedStats); }
  245. self.aggregateStatsIntervalId = setInterval(printAggregatedStats, checkInterval);
  246. }
  247. };
  248. return webRtcPlayer;
  249. }));
  250. export default webRtcPlayer