util.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import * as BinaryPack from "js-binarypack";
  2. import { RTCPeerConnection } from "./adapter";
  3. const DEFAULT_CONFIG = {
  4. iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
  5. };
  6. /*
  7. Prints log messages depending on the debug level passed in. Defaults to 0.
  8. 0 Prints no logs.
  9. 1 Prints only errors.
  10. 2 Prints errors and warnings.
  11. 3 Prints all logs.
  12. */
  13. const enum DebugLevel {
  14. Disabled,
  15. Errors,
  16. Warnings,
  17. All
  18. }
  19. export class util {
  20. static noop(): void {}
  21. static readonly CLOUD_HOST = "0.peerjs.com";
  22. static readonly CLOUD_PORT = 443;
  23. // Browsers that need chunking:
  24. static readonly chunkedBrowsers = { Chrome: 1 };
  25. static readonly chunkedMTU = 16300; // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
  26. // Logging logic
  27. static readonly debug = false;
  28. static logLevel = DebugLevel.Disabled;
  29. static setLogLevel(level: string): void {
  30. const debugLevel = parseInt(level, 10);
  31. if (!isNaN(parseInt(level, 10))) {
  32. util.logLevel = debugLevel;
  33. } else {
  34. // If they are using truthy/falsy values for debug
  35. util.logLevel = level ? DebugLevel.All : DebugLevel.Disabled;
  36. }
  37. util.log = util.warn = util.error = util.noop;
  38. if (util.logLevel > DebugLevel.Disabled) {
  39. util.error = util._printWith("ERROR");
  40. }
  41. if (util.logLevel > DebugLevel.Errors) {
  42. util.warn = util._printWith("WARNING");
  43. }
  44. if (util.logLevel > DebugLevel.Warnings) {
  45. util.log = util._print;
  46. }
  47. }
  48. static setLogFunction(fn): void {
  49. if (fn.constructor !== Function) {
  50. util.warn(
  51. "The log function you passed in is not a function. Defaulting to regular logs."
  52. );
  53. } else {
  54. util._print = fn;
  55. }
  56. }
  57. private static _printWith(prefix) {
  58. return function() {
  59. const copy = Array.prototype.slice.call(arguments);
  60. copy.unshift(prefix);
  61. util._print.apply(util, copy);
  62. };
  63. }
  64. private static _print(...rest): void {
  65. let err = false;
  66. const copy = [...rest];
  67. copy.unshift("PeerJS: ");
  68. for (let i in copy) {
  69. if (copy[i] instanceof Error) {
  70. copy[i] = "(" + copy[i].name + ") " + copy[i].message;
  71. err = true;
  72. }
  73. }
  74. err ? console.error.apply(console, copy) : console.log.apply(console, copy);
  75. }
  76. // Returns browser-agnostic default config
  77. static readonly defaultConfig = DEFAULT_CONFIG;
  78. // Returns the current browser.
  79. static readonly browser: string = (function(global) {
  80. // @ts-ignore
  81. if (global.mozRTCPeerConnection) {
  82. return "Firefox";
  83. }
  84. // @ts-ignore
  85. if (global.webkitRTCPeerConnection) {
  86. return "Chrome";
  87. }
  88. if (global.RTCPeerConnection) {
  89. return "Supported";
  90. }
  91. return "Unsupported";
  92. })(window);
  93. // Lists which features are supported
  94. static readonly supports = (function() {
  95. if (typeof RTCPeerConnection === "undefined") {
  96. return {};
  97. }
  98. let data = true;
  99. let audioVideo = true;
  100. let binaryBlob = false;
  101. let sctp = false;
  102. // @ts-ignore
  103. const onnegotiationneeded = !!window.webkitRTCPeerConnection;
  104. let pc, dc;
  105. try {
  106. pc = new RTCPeerConnection(DEFAULT_CONFIG, {
  107. optional: [{ RtpDataChannels: true }]
  108. });
  109. } catch (e) {
  110. data = false;
  111. audioVideo = false;
  112. }
  113. if (data) {
  114. try {
  115. dc = pc.createDataChannel("_PEERJSTEST");
  116. } catch (e) {
  117. data = false;
  118. }
  119. }
  120. if (data) {
  121. // Binary test
  122. try {
  123. dc.binaryType = "blob";
  124. binaryBlob = true;
  125. } catch (e) {}
  126. // Reliable test.
  127. // Unfortunately Chrome is a bit unreliable about whether or not they
  128. // support reliable.
  129. const reliablePC = new RTCPeerConnection(DEFAULT_CONFIG, {});
  130. try {
  131. const reliableDC = reliablePC.createDataChannel(
  132. "_PEERJSRELIABLETEST",
  133. {}
  134. );
  135. sctp = reliableDC.reliable;
  136. } catch (e) {}
  137. reliablePC.close();
  138. }
  139. // FIXME: not really the best check...
  140. if (audioVideo) {
  141. audioVideo = !!pc.addStream;
  142. }
  143. // FIXME: this is not great because in theory it doesn't work for
  144. // av-only browsers (?).
  145. /*
  146. if (!onnegotiationneeded && data) {
  147. // sync default check.
  148. var negotiationPC = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
  149. negotiationPC.onnegotiationneeded = function() {
  150. onnegotiationneeded = true;
  151. // async check.
  152. if (util && util.supports) {
  153. util.supports.onnegotiationneeded = true;
  154. }
  155. };
  156. negotiationPC.createDataChannel('_PEERJSNEGOTIATIONTEST');
  157. setTimeout(function() {
  158. negotiationPC.close();
  159. }, 1000);
  160. }
  161. */
  162. if (pc) {
  163. pc.close();
  164. }
  165. return {
  166. audioVideo: audioVideo,
  167. data: data,
  168. binaryBlob: binaryBlob,
  169. binary: sctp, // deprecated; sctp implies binary support.
  170. reliable: sctp, // deprecated; sctp implies reliable data.
  171. sctp: sctp,
  172. onnegotiationneeded: onnegotiationneeded
  173. };
  174. })();
  175. // Ensure alphanumeric ids
  176. static validateId(id: string): boolean {
  177. // Allow empty ids
  178. return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.test(id);
  179. }
  180. static inherits(ctor, superCtor): void {
  181. ctor.super_ = superCtor;
  182. ctor.prototype = Object.create(superCtor.prototype, {
  183. constructor: {
  184. value: ctor,
  185. enumerable: false,
  186. writable: true,
  187. configurable: true
  188. }
  189. });
  190. }
  191. static extend(dest, source): any {
  192. for (let key in source) {
  193. if (source.hasOwnProperty(key)) {
  194. dest[key] = source[key];
  195. }
  196. }
  197. return dest;
  198. }
  199. static pack = BinaryPack.pack;
  200. static unpack = BinaryPack.unpack;
  201. static log(...rest): void {
  202. if (!util.debug) return;
  203. let err = false;
  204. const copy = [...rest];
  205. copy.unshift("PeerJS: ");
  206. for (let i in copy) {
  207. if (copy[i] instanceof Error) {
  208. copy[i] = "(" + copy[i].name + ") " + copy[i].message;
  209. err = true;
  210. }
  211. }
  212. err ? console.error.apply(console, copy) : console.log.apply(console, copy);
  213. }
  214. static warn(...rest): void {}
  215. static error(...rest): void {}
  216. static setZeroTimeout = (global => {
  217. const timeouts = [];
  218. const messageName = "zero-timeout-message";
  219. // Like setTimeout, but only takes a function argument. There's
  220. // no time argument (always zero) and no arguments (you have to
  221. // use a closure).
  222. function setZeroTimeoutPostMessage(fn) {
  223. timeouts.push(fn);
  224. global.postMessage(messageName, "*");
  225. }
  226. function handleMessage(event) {
  227. if (event.source === global && event.data === messageName) {
  228. if (event.stopPropagation) {
  229. event.stopPropagation();
  230. }
  231. if (timeouts.length) {
  232. timeouts.shift()();
  233. }
  234. }
  235. }
  236. if (global.addEventListener) {
  237. global.addEventListener("message", handleMessage, true);
  238. }
  239. // @ts-ignore
  240. else if (global.attachEvent) {
  241. // @ts-ignore
  242. global.attachEvent("onmessage", handleMessage);
  243. }
  244. return setZeroTimeoutPostMessage;
  245. })(window);
  246. // Binary stuff
  247. private static _dataCount = 1;
  248. // chunks a blob.
  249. static chunk(bl: Blob): any[] {
  250. const chunks = [];
  251. const size = bl.size;
  252. const total = Math.ceil(size / util.chunkedMTU);
  253. let index;
  254. let start = (index = 0);
  255. while (start < size) {
  256. const end = Math.min(size, start + util.chunkedMTU);
  257. const b = bl.slice(start, end);
  258. const chunk = {
  259. __peerData: this._dataCount,
  260. n: index,
  261. data: b,
  262. total: total
  263. };
  264. chunks.push(chunk);
  265. start = end;
  266. index++;
  267. }
  268. this._dataCount++;
  269. return chunks;
  270. }
  271. static blobToArrayBuffer(blob: Blob, cb: (arg: any) => void): void {
  272. const fr = new FileReader();
  273. fr.onload = function(evt) {
  274. // @ts-ignore
  275. cb(evt.target.result);
  276. };
  277. fr.readAsArrayBuffer(blob);
  278. }
  279. static blobToBinaryString(blob: Blob, cb: (arg: any) => void): void {
  280. const fr = new FileReader();
  281. fr.onload = function(evt) {
  282. // @ts-ignore
  283. cb(evt.target.result);
  284. };
  285. fr.readAsBinaryString(blob);
  286. }
  287. static binaryStringToArrayBuffer(binary): ArrayBuffer | SharedArrayBuffer {
  288. let byteArray = new Uint8Array(binary.length);
  289. for (let i = 0; i < binary.length; i++) {
  290. byteArray[i] = binary.charCodeAt(i) & 0xff;
  291. }
  292. return byteArray.buffer;
  293. }
  294. static randomToken(): string {
  295. return Math.random()
  296. .toString(36)
  297. .substr(2);
  298. }
  299. static isSecure(): boolean {
  300. return location.protocol === "https:";
  301. }
  302. }