janus.js 169 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645
  1. /*
  2. The MIT License (MIT)
  3. Copyright (c) 2016 Meetecho
  4. Permission is hereby granted, free of charge, to any person obtaining
  5. a copy of this software and associated documentation files (the "Software"),
  6. to deal in the Software without restriction, including without limitation
  7. the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. and/or sell copies of the Software, and to permit persons to whom the
  9. Software is furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included
  11. in all copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  13. OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  15. THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
  16. OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
  17. ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  18. OTHER DEALINGS IN THE SOFTWARE.
  19. */
  20. // List of sessions
  21. Janus.sessions = {};
  22. Janus.isExtensionEnabled = function () {
  23. if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
  24. // No need for the extension, getDisplayMedia is supported
  25. return true;
  26. }
  27. if (window.navigator.userAgent.match('Chrome')) {
  28. var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10);
  29. var maxver = 33;
  30. if (window.navigator.userAgent.match('Linux'))
  31. maxver = 35; // "known" crash in chrome 34 and 35 on linux
  32. if (chromever >= 26 && chromever <= maxver) {
  33. // Older versions of Chrome don't support this extension-based approach, so lie
  34. return true;
  35. }
  36. return Janus.extension.isInstalled();
  37. } else {
  38. // Firefox of others, no need for the extension (but this doesn't mean it will work)
  39. return true;
  40. }
  41. };
  42. var defaultExtension = {
  43. // Screensharing Chrome Extension ID
  44. extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj',
  45. isInstalled: function () {
  46. return document.querySelector('#janus-extension-installed') !== null;
  47. },
  48. getScreen: function (callback) {
  49. var pending = window.setTimeout(function () {
  50. var error = new Error('NavigatorUserMediaError');
  51. error.name = 'The required Chrome extension is not installed: click <a href="#">here</a> to install it. (NOTE: this will need you to refresh the page)';
  52. return callback(error);
  53. }, 1000);
  54. this.cache[pending] = callback;
  55. window.postMessage({type: 'janusGetScreen', id: pending}, '*');
  56. },
  57. init: function () {
  58. var cache = {};
  59. this.cache = cache;
  60. // Wait for events from the Chrome Extension
  61. window.addEventListener('message', function (event) {
  62. if (event.origin != window.location.origin)
  63. return;
  64. if (event.data.type == 'janusGotScreen' && cache[event.data.id]) {
  65. var callback = cache[event.data.id];
  66. delete cache[event.data.id];
  67. if (event.data.sourceId === '') {
  68. // user canceled
  69. var error = new Error('NavigatorUserMediaError');
  70. error.name = 'You cancelled the request for permission, giving up...';
  71. callback(error);
  72. } else {
  73. callback(null, event.data.sourceId);
  74. }
  75. } else if (event.data.type == 'janusGetScreenPending') {
  76. console.log('clearing ', event.data.id);
  77. window.clearTimeout(event.data.id);
  78. }
  79. });
  80. }
  81. };
  82. Janus.useDefaultDependencies = function (deps) {
  83. var f = (deps && deps.fetch) || fetch;
  84. var p = (deps && deps.Promise) || Promise;
  85. var socketCls = (deps && deps.WebSocket) || WebSocket;
  86. return {
  87. newWebSocket: function (server, proto) {
  88. return new socketCls(server, proto);
  89. },
  90. extension: (deps && deps.extension) || defaultExtension,
  91. isArray: function (arr) {
  92. return Array.isArray(arr);
  93. },
  94. webRTCAdapter: (deps && deps.adapter) || adapter,
  95. httpAPICall: function (url, options) {
  96. var fetchOptions = {
  97. method: options.verb,
  98. headers: {
  99. 'Accept': 'application/json, text/plain, */*'
  100. },
  101. cache: 'no-cache'
  102. };
  103. if (options.verb === "POST") {
  104. fetchOptions.headers['Content-Type'] = 'application/json';
  105. }
  106. if (options.withCredentials !== undefined) {
  107. fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit');
  108. }
  109. if (options.body !== undefined) {
  110. fetchOptions.body = JSON.stringify(options.body);
  111. }
  112. var fetching = f(url, fetchOptions).catch(function (error) {
  113. return p.reject({message: 'Probably a network error, is the server down?', error: error});
  114. });
  115. /*
  116. * fetch() does not natively support timeouts.
  117. * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first.
  118. */
  119. if (options.timeout !== undefined) {
  120. var timeout = new p(function (resolve, reject) {
  121. var timerId = setTimeout(function () {
  122. clearTimeout(timerId);
  123. return reject({message: 'Request timed out', timeout: options.timeout});
  124. }, options.timeout);
  125. });
  126. fetching = p.race([fetching, timeout]);
  127. }
  128. fetching.then(function (response) {
  129. if (response.ok) {
  130. if (typeof (options.success) === typeof (Janus.noop)) {
  131. return response.json().then(function (parsed) {
  132. options.success(parsed);
  133. }).catch(function (error) {
  134. return p.reject({
  135. message: 'Failed to parse response body',
  136. error: error,
  137. response: response
  138. });
  139. });
  140. }
  141. } else {
  142. return p.reject({message: 'API call failed', response: response});
  143. }
  144. }).catch(function (error) {
  145. if (typeof (options.error) === typeof (Janus.noop)) {
  146. options.error(error.message || '<< internal error >>', error);
  147. }
  148. });
  149. return fetching;
  150. }
  151. }
  152. };
  153. Janus.useOldDependencies = function (deps) {
  154. var jq = (deps && deps.jQuery) || jQuery;
  155. var socketCls = (deps && deps.WebSocket) || WebSocket;
  156. return {
  157. newWebSocket: function (server, proto) {
  158. return new socketCls(server, proto);
  159. },
  160. isArray: function (arr) {
  161. return jq.isArray(arr);
  162. },
  163. extension: (deps && deps.extension) || defaultExtension,
  164. webRTCAdapter: (deps && deps.adapter) || adapter,
  165. httpAPICall: function (url, options) {
  166. var payload = options.body !== undefined ? {
  167. contentType: 'application/json',
  168. data: JSON.stringify(options.body)
  169. } : {};
  170. var credentials = options.withCredentials !== undefined ? {xhrFields: {withCredentials: options.withCredentials}} : {};
  171. return jq.ajax(jq.extend(payload, credentials, {
  172. url: url,
  173. type: options.verb,
  174. cache: false,
  175. dataType: 'json',
  176. async: options.async,
  177. timeout: options.timeout,
  178. success: function (result) {
  179. if (typeof (options.success) === typeof (Janus.noop)) {
  180. options.success(result);
  181. }
  182. },
  183. error: function (xhr, status, err) {
  184. if (typeof (options.error) === typeof (Janus.noop)) {
  185. options.error(status, err);
  186. }
  187. }
  188. }));
  189. },
  190. };
  191. };
  192. Janus.noop = function () {
  193. };
  194. Janus.dataChanDefaultLabel = "JanusDataChannel";
  195. // Note: in the future we may want to change this, e.g., as was
  196. // attempted in https://github.com/meetecho/janus-gateway/issues/1670
  197. Janus.endOfCandidates = null;
  198. // Initialization
  199. Janus.init = function (options) {
  200. options = options || {};
  201. options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop;
  202. if (Janus.initDone === true) {
  203. // Already initialized
  204. options.callback();
  205. } else {
  206. if (typeof console == "undefined" || typeof console.log == "undefined")
  207. console = {
  208. log: function () {
  209. }
  210. };
  211. // Console logging (all debugging disabled by default)
  212. Janus.trace = Janus.noop;
  213. Janus.debug = Janus.noop;
  214. Janus.vdebug = Janus.noop;
  215. Janus.log = Janus.noop;
  216. Janus.warn = Janus.noop;
  217. Janus.error = Janus.noop;
  218. if (options.debug === true || options.debug === "all") {
  219. // Enable all debugging levels
  220. Janus.trace = console.trace.bind(console);
  221. Janus.debug = console.debug.bind(console);
  222. Janus.vdebug = console.debug.bind(console);
  223. Janus.log = console.log.bind(console);
  224. Janus.warn = console.warn.bind(console);
  225. Janus.error = console.error.bind(console);
  226. } else if (Array.isArray(options.debug)) {
  227. for (var i in options.debug) {
  228. var d = options.debug[i];
  229. switch (d) {
  230. case "trace":
  231. Janus.trace = console.trace.bind(console);
  232. break;
  233. case "debug":
  234. Janus.debug = console.debug.bind(console);
  235. break;
  236. case "vdebug":
  237. Janus.vdebug = console.debug.bind(console);
  238. break;
  239. case "log":
  240. Janus.log = console.log.bind(console);
  241. break;
  242. case "warn":
  243. Janus.warn = console.warn.bind(console);
  244. break;
  245. case "error":
  246. Janus.error = console.error.bind(console);
  247. break;
  248. default:
  249. console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')");
  250. break;
  251. }
  252. }
  253. }
  254. Janus.log("Initializing library");
  255. var usedDependencies = options.dependencies || Janus.useDefaultDependencies();
  256. Janus.isArray = usedDependencies.isArray;
  257. Janus.webRTCAdapter = usedDependencies.webRTCAdapter;
  258. Janus.httpAPICall = usedDependencies.httpAPICall;
  259. Janus.newWebSocket = usedDependencies.newWebSocket;
  260. Janus.extension = usedDependencies.extension;
  261. Janus.extension.init();
  262. // Helper method to enumerate devices
  263. Janus.listDevices = function (callback, config) {
  264. callback = (typeof callback == "function") ? callback : Janus.noop;
  265. if (config == null) config = {audio: true, video: true};
  266. if (Janus.isGetUserMediaAvailable()) {
  267. navigator.mediaDevices.getUserMedia(config)
  268. .then(function (stream) {
  269. navigator.mediaDevices.enumerateDevices().then(function (devices) {
  270. Janus.debug(devices);
  271. callback(devices);
  272. // Get rid of the now useless stream
  273. try {
  274. var tracks = stream.getTracks();
  275. for (var i in tracks) {
  276. var mst = tracks[i];
  277. if (mst !== null && mst !== undefined)
  278. mst.stop();
  279. }
  280. } catch (e) {
  281. }
  282. });
  283. })
  284. .catch(function (err) {
  285. Janus.error(err);
  286. callback([]);
  287. });
  288. } else {
  289. Janus.warn("navigator.mediaDevices unavailable");
  290. callback([]);
  291. }
  292. }
  293. // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js)
  294. Janus.attachMediaStream = function (element, stream) {
  295. if (Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
  296. var chromever = Janus.webRTCAdapter.browserDetails.version;
  297. if (chromever >= 52) {
  298. element.srcObject = stream;
  299. } else if (typeof element.src !== 'undefined') {
  300. element.src = URL.createObjectURL(stream);
  301. } else {
  302. Janus.error("Error attaching stream to element");
  303. }
  304. } else {
  305. element.srcObject = stream;
  306. }
  307. };
  308. Janus.reattachMediaStream = function (to, from) {
  309. if (Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
  310. var chromever = Janus.webRTCAdapter.browserDetails.version;
  311. if (chromever >= 52) {
  312. to.srcObject = from.srcObject;
  313. } else if (typeof to.src !== 'undefined') {
  314. to.src = from.src;
  315. } else {
  316. Janus.error("Error reattaching stream to element");
  317. }
  318. } else {
  319. to.srcObject = from.srcObject;
  320. }
  321. };
  322. // Detect tab close: make sure we don't loose existing onbeforeunload handlers
  323. // (note: for iOS we need to subscribe to a different event, 'pagehide', see
  324. // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe)
  325. var iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;
  326. var eventName = iOS ? 'pagehide' : 'beforeunload';
  327. var oldOBF = window["on" + eventName];
  328. window.addEventListener(eventName, function (event) {
  329. Janus.log("Closing window");
  330. for (var s in Janus.sessions) {
  331. if (Janus.sessions[s] !== null && Janus.sessions[s] !== undefined &&
  332. Janus.sessions[s].destroyOnUnload) {
  333. Janus.log("Destroying session " + s);
  334. Janus.sessions[s].destroy({asyncRequest: false, notifyDestroyed: false});
  335. }
  336. }
  337. if (oldOBF && typeof oldOBF == "function")
  338. oldOBF();
  339. });
  340. // If this is a Safari Technology Preview, check if VP8 is supported
  341. Janus.safariVp8 = false;
  342. if (Janus.webRTCAdapter.browserDetails.browser === 'safari' &&
  343. Janus.webRTCAdapter.browserDetails.version >= 605) {
  344. // Let's see if RTCRtpSender.getCapabilities() is there
  345. if (RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") &&
  346. RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) {
  347. for (var i in RTCRtpSender.getCapabilities("video").codecs) {
  348. var codec = RTCRtpSender.getCapabilities("video").codecs[i];
  349. if (codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") {
  350. Janus.safariVp8 = true;
  351. break;
  352. }
  353. }
  354. if (Janus.safariVp8) {
  355. Janus.log("This version of Safari supports VP8");
  356. } else {
  357. Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " +
  358. "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu");
  359. }
  360. } else {
  361. // We do it in a very ugly way, as there's no alternative...
  362. // We create a PeerConnection to see if VP8 is in an offer
  363. var testpc = new RTCPeerConnection({}, {});
  364. testpc.createOffer({offerToReceiveVideo: true}).then(function (offer) {
  365. Janus.safariVp8 = offer.sdp.indexOf("VP8") !== -1;
  366. if (Janus.safariVp8) {
  367. Janus.log("This version of Safari supports VP8");
  368. } else {
  369. Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " +
  370. "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu");
  371. }
  372. testpc.close();
  373. testpc = null;
  374. });
  375. }
  376. }
  377. // Check if this browser supports Unified Plan and transceivers
  378. // Based on https://codepen.io/anon/pen/ZqLwWV?editors=0010
  379. Janus.unifiedPlan = false;
  380. if (Janus.webRTCAdapter.browserDetails.browser === 'firefox' &&
  381. Janus.webRTCAdapter.browserDetails.version >= 59) {
  382. // Firefox definitely does, starting from version 59
  383. Janus.unifiedPlan = true;
  384. } else if (Janus.webRTCAdapter.browserDetails.browser === 'chrome' &&
  385. Janus.webRTCAdapter.browserDetails.version < 72) {
  386. // Chrome does, but it's only usable from version 72 on
  387. Janus.unifiedPlan = false;
  388. } else if (!('currentDirection' in RTCRtpTransceiver.prototype)) {
  389. // Safari supports addTransceiver() but not Unified Plan when
  390. // currentDirection is not defined (see codepen above)
  391. Janus.unifiedPlan = false;
  392. } else {
  393. // Check if addTransceiver() throws an exception
  394. const tempPc = new RTCPeerConnection();
  395. try {
  396. tempPc.addTransceiver('audio');
  397. Janus.unifiedPlan = true;
  398. } catch (e) {
  399. }
  400. tempPc.close();
  401. }
  402. Janus.initDone = true;
  403. options.callback();
  404. }
  405. };
  406. // Helper method to check whether WebRTC is supported by this browser
  407. Janus.isWebrtcSupported = function () {
  408. return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null;
  409. };
  410. // Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP)
  411. Janus.isGetUserMediaAvailable = function () {
  412. return navigator.mediaDevices !== undefined && navigator.mediaDevices !== null &&
  413. navigator.mediaDevices.getUserMedia !== undefined && navigator.mediaDevices.getUserMedia !== null;
  414. };
  415. // Helper method to create random identifiers (e.g., transaction)
  416. Janus.randomString = function (len) {
  417. var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  418. var randomString = '';
  419. for (var i = 0; i < len; i++) {
  420. var randomPoz = Math.floor(Math.random() * charSet.length);
  421. randomString += charSet.substring(randomPoz, randomPoz + 1);
  422. }
  423. return randomString;
  424. }
  425. function Janus(gatewayCallbacks) {
  426. if (Janus.initDone === undefined) {
  427. gatewayCallbacks.error("Library not initialized");
  428. return {};
  429. }
  430. if (!Janus.isWebrtcSupported()) {
  431. gatewayCallbacks.error("WebRTC not supported by this browser");
  432. return {};
  433. }
  434. Janus.log("Library initialized: " + Janus.initDone);
  435. gatewayCallbacks = gatewayCallbacks || {};
  436. gatewayCallbacks.success = (typeof gatewayCallbacks.success == "function") ? gatewayCallbacks.success : Janus.noop;
  437. gatewayCallbacks.error = (typeof gatewayCallbacks.error == "function") ? gatewayCallbacks.error : Janus.noop;
  438. gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == "function") ? gatewayCallbacks.destroyed : Janus.noop;
  439. if (gatewayCallbacks.server === null || gatewayCallbacks.server === undefined) {
  440. gatewayCallbacks.error("Invalid server url");
  441. return {};
  442. }
  443. var websockets = false;
  444. var ws = null;
  445. var wsHandlers = {};
  446. var wsKeepaliveTimeoutId = null;
  447. var servers = null, serversIndex = 0;
  448. var server = gatewayCallbacks.server;
  449. if (Janus.isArray(server)) {
  450. Janus.log("Multiple servers provided (" + server.length + "), will use the first that works");
  451. server = null;
  452. servers = gatewayCallbacks.server;
  453. Janus.debug(servers);
  454. } else {
  455. if (server.indexOf("ws") === 0) {
  456. websockets = true;
  457. Janus.log("Using WebSockets to contact Janus: " + server);
  458. } else {
  459. websockets = false;
  460. Janus.log("Using REST API to contact Janus: " + server);
  461. }
  462. }
  463. var iceServers = gatewayCallbacks.iceServers;
  464. if (iceServers === undefined || iceServers === null)
  465. iceServers = [{urls: "stun:stun.l.google.com:19302"}];
  466. var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy;
  467. var bundlePolicy = gatewayCallbacks.bundlePolicy;
  468. // Whether IPv6 candidates should be gathered
  469. var ipv6Support = gatewayCallbacks.ipv6;
  470. if (ipv6Support === undefined || ipv6Support === null)
  471. ipv6Support = false;
  472. // Whether we should enable the withCredentials flag for XHR requests
  473. var withCredentials = false;
  474. if (gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null)
  475. withCredentials = gatewayCallbacks.withCredentials === true;
  476. // Optional max events
  477. var maxev = 10;
  478. if (gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null)
  479. maxev = gatewayCallbacks.max_poll_events;
  480. if (maxev < 1)
  481. maxev = 1;
  482. // Token to use (only if the token based authentication mechanism is enabled)
  483. var token = null;
  484. if (gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null)
  485. token = gatewayCallbacks.token;
  486. // API secret to use (only if the shared API secret is enabled)
  487. var apisecret = null;
  488. if (gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null)
  489. apisecret = gatewayCallbacks.apisecret;
  490. // Whether we should destroy this session when onbeforeunload is called
  491. this.destroyOnUnload = true;
  492. if (gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null)
  493. this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true);
  494. // Some timeout-related values
  495. var keepAlivePeriod = 25000;
  496. if (gatewayCallbacks.keepAlivePeriod !== undefined && gatewayCallbacks.keepAlivePeriod !== null)
  497. keepAlivePeriod = gatewayCallbacks.keepAlivePeriod;
  498. if (isNaN(keepAlivePeriod))
  499. keepAlivePeriod = 25000;
  500. var longPollTimeout = 60000;
  501. if (gatewayCallbacks.longPollTimeout !== undefined && gatewayCallbacks.longPollTimeout !== null)
  502. longPollTimeout = gatewayCallbacks.longPollTimeout;
  503. if (isNaN(longPollTimeout))
  504. longPollTimeout = 60000;
  505. // overrides for default maxBitrate values for simulcasting
  506. function getMaxBitrates(simulcastMaxBitrates) {
  507. var maxBitrates = {
  508. high: 900000,
  509. medium: 300000,
  510. low: 100000,
  511. };
  512. if (simulcastMaxBitrates !== undefined && simulcastMaxBitrates !== null) {
  513. if (simulcastMaxBitrates.high)
  514. maxBitrates.high = simulcastMaxBitrates.high;
  515. if (simulcastMaxBitrates.medium)
  516. maxBitrates.medium = simulcastMaxBitrates.medium;
  517. if (simulcastMaxBitrates.low)
  518. maxBitrates.low = simulcastMaxBitrates.low;
  519. }
  520. return maxBitrates;
  521. }
  522. var connected = false;
  523. var sessionId = null;
  524. var pluginHandles = {};
  525. var that = this;
  526. var retries = 0;
  527. var transactions = {};
  528. createSession(gatewayCallbacks);
  529. // Public methods
  530. this.getServer = function () {
  531. return server;
  532. };
  533. this.isConnected = function () {
  534. return connected;
  535. };
  536. this.reconnect = function (callbacks) {
  537. callbacks = callbacks || {};
  538. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  539. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  540. callbacks["reconnect"] = true;
  541. createSession(callbacks);
  542. };
  543. this.getSessionId = function () {
  544. return sessionId;
  545. };
  546. this.destroy = function (callbacks) {
  547. destroySession(callbacks);
  548. };
  549. this.attach = function (callbacks) {
  550. createHandle(callbacks);
  551. };
  552. function eventHandler() {
  553. if (sessionId == null)
  554. return;
  555. Janus.debug('Long poll...');
  556. if (!connected) {
  557. Janus.warn("Is the server down? (connected=false)");
  558. return;
  559. }
  560. var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime();
  561. if (maxev !== undefined && maxev !== null)
  562. longpoll = longpoll + "&maxev=" + maxev;
  563. if (token !== null && token !== undefined)
  564. longpoll = longpoll + "&token=" + encodeURIComponent(token);
  565. if (apisecret !== null && apisecret !== undefined)
  566. longpoll = longpoll + "&apisecret=" + encodeURIComponent(apisecret);
  567. Janus.httpAPICall(longpoll, {
  568. verb: 'GET',
  569. withCredentials: withCredentials,
  570. success: handleEvent,
  571. timeout: longPollTimeout,
  572. error: function (textStatus, errorThrown) {
  573. Janus.error(textStatus + ":", errorThrown);
  574. retries++;
  575. if (retries > 3) {
  576. // Did we just lose the server? :-(
  577. connected = false;
  578. gatewayCallbacks.error("Lost connection to the server (is it down?)");
  579. return;
  580. }
  581. eventHandler();
  582. }
  583. });
  584. }
  585. // Private event handler: this will trigger plugin callbacks, if set
  586. function handleEvent(json, skipTimeout) {
  587. retries = 0;
  588. if (!websockets && sessionId !== undefined && sessionId !== null && skipTimeout !== true)
  589. eventHandler();
  590. if (!websockets && Janus.isArray(json)) {
  591. // We got an array: it means we passed a maxev > 1, iterate on all objects
  592. for (var i = 0; i < json.length; i++) {
  593. handleEvent(json[i], true);
  594. }
  595. return;
  596. }
  597. if (json["rtcgw"] === "keepalive") {
  598. // Nothing happened
  599. Janus.vdebug("Got a keepalive on session " + sessionId);
  600. return;
  601. } else if (json["rtcgw"] === "ack") {
  602. // Just an ack, we can probably ignore
  603. Janus.debug("Got an ack on session " + sessionId);
  604. Janus.debug(json);
  605. var transaction = json["transaction"];
  606. if (transaction !== null && transaction !== undefined) {
  607. var reportSuccess = transactions[transaction];
  608. if (reportSuccess !== null && reportSuccess !== undefined) {
  609. reportSuccess(json);
  610. }
  611. delete transactions[transaction];
  612. }
  613. return;
  614. } else if (json["rtcgw"] === "success") {
  615. // Success!
  616. Janus.debug("Got a success on session " + sessionId);
  617. Janus.debug(json);
  618. var transaction = json["transaction"];
  619. if (transaction !== null && transaction !== undefined) {
  620. var reportSuccess = transactions[transaction];
  621. if (reportSuccess !== null && reportSuccess !== undefined) {
  622. reportSuccess(json);
  623. }
  624. delete transactions[transaction];
  625. }
  626. return;
  627. } else if (json["rtcgw"] === "trickle") {
  628. // We got a trickle candidate from Janus
  629. var sender = json["sender"];
  630. if (sender === undefined || sender === null) {
  631. Janus.warn("Missing sender...");
  632. return;
  633. }
  634. var pluginHandle = pluginHandles[sender];
  635. if (pluginHandle === undefined || pluginHandle === null) {
  636. Janus.debug("This handle is not attached to this session");
  637. return;
  638. }
  639. var candidate = json["candidate"];
  640. Janus.debug("Got a trickled candidate on session " + sessionId);
  641. Janus.debug(candidate);
  642. var config = pluginHandle.webrtcStuff;
  643. if (config.pc && config.remoteSdp) {
  644. // Add candidate right now
  645. Janus.debug("Adding remote candidate:", candidate);
  646. if (!candidate || candidate.completed === true) {
  647. // end-of-candidates
  648. config.pc.addIceCandidate(Janus.endOfCandidates);
  649. } else {
  650. // New candidate
  651. config.pc.addIceCandidate(candidate);
  652. }
  653. } else {
  654. // We didn't do setRemoteDescription (trickle got here before the offer?)
  655. Janus.debug("We didn't do setRemoteDescription (trickle got here before the offer?), caching candidate");
  656. if (!config.candidates)
  657. config.candidates = [];
  658. config.candidates.push(candidate);
  659. Janus.debug(config.candidates);
  660. }
  661. } else if (json["rtcgw"] === "webrtcup") {
  662. // The PeerConnection with the server is up! Notify this
  663. Janus.debug("Got a webrtcup event on session " + sessionId);
  664. Janus.debug(json);
  665. var sender = json["sender"];
  666. if (sender === undefined || sender === null) {
  667. Janus.warn("Missing sender...");
  668. return;
  669. }
  670. var pluginHandle = pluginHandles[sender];
  671. if (pluginHandle === undefined || pluginHandle === null) {
  672. Janus.debug("This handle is not attached to this session");
  673. return;
  674. }
  675. pluginHandle.webrtcState(true);
  676. return;
  677. } else if (json["rtcgw"] === "hangup") {
  678. // A plugin asked the core to hangup a PeerConnection on one of our handles
  679. Janus.debug("Got a hangup event on session " + sessionId);
  680. Janus.debug(json);
  681. var sender = json["sender"];
  682. if (sender === undefined || sender === null) {
  683. Janus.warn("Missing sender...");
  684. return;
  685. }
  686. var pluginHandle = pluginHandles[sender];
  687. if (pluginHandle === undefined || pluginHandle === null) {
  688. Janus.debug("This handle is not attached to this session");
  689. return;
  690. }
  691. pluginHandle.webrtcState(false, json["reason"]);
  692. pluginHandle.hangup();
  693. } else if (json["rtcgw"] === "detached") {
  694. // A plugin asked the core to detach one of our handles
  695. Janus.debug("Got a detached event on session " + sessionId);
  696. Janus.debug(json);
  697. var sender = json["sender"];
  698. if (sender === undefined || sender === null) {
  699. Janus.warn("Missing sender...");
  700. return;
  701. }
  702. var pluginHandle = pluginHandles[sender];
  703. if (pluginHandle === undefined || pluginHandle === null) {
  704. // Don't warn here because destroyHandle causes this situation.
  705. return;
  706. }
  707. pluginHandle.detached = true;
  708. pluginHandle.ondetached();
  709. pluginHandle.detach();
  710. } else if (json["rtcgw"] === "media") {
  711. // Media started/stopped flowing
  712. Janus.debug("Got a media event on session " + sessionId);
  713. Janus.debug(json);
  714. var sender = json["sender"];
  715. if (sender === undefined || sender === null) {
  716. Janus.warn("Missing sender...");
  717. return;
  718. }
  719. var pluginHandle = pluginHandles[sender];
  720. if (pluginHandle === undefined || pluginHandle === null) {
  721. Janus.debug("This handle is not attached to this session");
  722. return;
  723. }
  724. pluginHandle.mediaState(json["type"], json["receiving"]);
  725. } else if (json["rtcgw"] === "slowlink") {
  726. Janus.debug("Got a slowlink event on session " + sessionId);
  727. Janus.debug(json);
  728. // Trouble uplink or downlink
  729. var sender = json["sender"];
  730. if (sender === undefined || sender === null) {
  731. Janus.warn("Missing sender...");
  732. return;
  733. }
  734. var pluginHandle = pluginHandles[sender];
  735. if (pluginHandle === undefined || pluginHandle === null) {
  736. Janus.debug("This handle is not attached to this session");
  737. return;
  738. }
  739. pluginHandle.slowLink(json["uplink"], json["lost"]);
  740. } else if (json["rtcgw"] === "error") {
  741. // Oops, something wrong happened
  742. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  743. Janus.debug(json);
  744. var transaction = json["transaction"];
  745. if (transaction !== null && transaction !== undefined) {
  746. var reportSuccess = transactions[transaction];
  747. if (reportSuccess !== null && reportSuccess !== undefined) {
  748. reportSuccess(json);
  749. }
  750. delete transactions[transaction];
  751. }
  752. return;
  753. } else if (json["rtcgw"] === "event") {
  754. Janus.debug("Got a plugin event on session " + sessionId);
  755. Janus.debug(json);
  756. var sender = json["sender"];
  757. if (sender === undefined || sender === null) {
  758. Janus.warn("Missing sender...");
  759. return;
  760. }
  761. var plugindata = json["plugindata"];
  762. if (plugindata === undefined || plugindata === null) {
  763. Janus.warn("Missing plugindata...");
  764. return;
  765. }
  766. Janus.debug(" -- Event is coming from " + sender + " (" + plugindata["plugin"] + ")");
  767. var data = plugindata["data"];
  768. Janus.debug(data);
  769. var pluginHandle = pluginHandles[sender];
  770. if (pluginHandle === undefined || pluginHandle === null) {
  771. Janus.warn("This handle is not attached to this session");
  772. return;
  773. }
  774. var jsep = json["jsep"];
  775. if (jsep !== undefined && jsep !== null) {
  776. Janus.debug("Handling SDP as well...");
  777. Janus.debug(jsep);
  778. }
  779. var callback = pluginHandle.onmessage;
  780. if (callback !== null && callback !== undefined) {
  781. Janus.debug("Notifying application...");
  782. // Send to callback specified when attaching plugin handle
  783. callback(data, jsep);
  784. } else {
  785. // Send to generic callback (?)
  786. Janus.debug("No provided notification callback");
  787. }
  788. } else if (json["rtcgw"] === "timeout") {
  789. Janus.error("Timeout on session " + sessionId);
  790. Janus.debug(json);
  791. if (websockets) {
  792. ws.close(3504, "Gateway timeout");
  793. }
  794. return;
  795. } else {
  796. Janus.warn("Unknown message/event '" + json["rtcgw"] + "' on session " + sessionId);
  797. Janus.debug(json);
  798. }
  799. }
  800. // Private helper to send keep-alive messages on WebSockets
  801. function keepAlive() {
  802. if (server === null || !websockets || !connected)
  803. return;
  804. wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);
  805. var request = {"rtcgw": "keepalive", "session_id": sessionId, "transaction": Janus.randomString(12)};
  806. if (token !== null && token !== undefined)
  807. request["token"] = token;
  808. if (apisecret !== null && apisecret !== undefined)
  809. request["apisecret"] = apisecret;
  810. ws.send(JSON.stringify(request));
  811. }
  812. // Private method to create a session
  813. function createSession(callbacks) {
  814. var transaction = Janus.randomString(12);
  815. // console.log("jannus create_token",stream);
  816. var request = {
  817. "rtcgw": "create",
  818. "transaction": transaction,
  819. "token": window.EZUITalk.opt.stream,
  820. "device": window.EZUITalk.opt.deviceSerial,
  821. "channel": window.EZUITalk.opt.channelNo,
  822. };
  823. if (callbacks["reconnect"]) {
  824. // We're reconnecting, claim the session
  825. connected = false;
  826. request["rtcgw"] = "claim";
  827. request["session_id"] = sessionId;
  828. // If we were using websockets, ignore the old connection
  829. if (ws) {
  830. ws.onopen = null;
  831. ws.onerror = null;
  832. ws.onclose = null;
  833. if (wsKeepaliveTimeoutId) {
  834. clearTimeout(wsKeepaliveTimeoutId);
  835. wsKeepaliveTimeoutId = null;
  836. }
  837. }
  838. }
  839. if (token !== null && token !== undefined)
  840. request["token"] = token;
  841. if (apisecret !== null && apisecret !== undefined)
  842. request["apisecret"] = apisecret;
  843. if (server === null && Janus.isArray(servers)) {
  844. // We still need to find a working server from the list we were given
  845. server = servers[serversIndex];
  846. if (server.indexOf("ws") === 0) {
  847. websockets = true;
  848. Janus.log("Server #" + (serversIndex + 1) + ": trying WebSockets to contact Janus (" + server + ")");
  849. } else {
  850. websockets = false;
  851. Janus.log("Server #" + (serversIndex + 1) + ": trying REST API to contact Janus (" + server + ")");
  852. }
  853. }
  854. if (websockets) {
  855. ws = Janus.newWebSocket(server, 'rtcgw-protocol');
  856. wsHandlers = {
  857. 'error': function () {
  858. Janus.error("Error connecting to the Janus WebSockets server... " + server);
  859. if (Janus.isArray(servers) && !callbacks["reconnect"]) {
  860. serversIndex++;
  861. if (serversIndex == servers.length) {
  862. // We tried all the servers the user gave us and they all failed
  863. callbacks.error("Error connecting to any of the provided Janus servers: Is the server down?");
  864. return;
  865. }
  866. // Let's try the next server
  867. server = null;
  868. setTimeout(function () {
  869. createSession(callbacks);
  870. }, 200);
  871. return;
  872. }
  873. callbacks.error("Error connecting to the Janus WebSockets server: Is the server down?");
  874. },
  875. 'open': function () {
  876. // We need to be notified about the success
  877. transactions[transaction] = function (json) {
  878. Janus.debug(json);
  879. if (json["rtcgw"] !== "success") {
  880. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  881. callbacks.error(json["error"].reason);
  882. return;
  883. }
  884. wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);
  885. connected = true;
  886. sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
  887. if (callbacks["reconnect"]) {
  888. Janus.log("Claimed session: " + sessionId);
  889. } else {
  890. Janus.log("Created session: " + sessionId);
  891. }
  892. Janus.sessions[sessionId] = that;
  893. callbacks.success();
  894. };
  895. ws.send(JSON.stringify(request));
  896. },
  897. 'message': function (event) {
  898. handleEvent(JSON.parse(event.data));
  899. },
  900. 'close': function () {
  901. if (server === null || !connected) {
  902. return;
  903. }
  904. connected = false;
  905. // FIXME What if this is called when the page is closed?
  906. gatewayCallbacks.error("Lost connection to the server (is it down?)");
  907. }
  908. };
  909. for (var eventName in wsHandlers) {
  910. ws.addEventListener(eventName, wsHandlers[eventName]);
  911. }
  912. return;
  913. }
  914. Janus.httpAPICall(server, {
  915. verb: 'POST',
  916. withCredentials: withCredentials,
  917. body: request,
  918. success: function (json) {
  919. Janus.debug(json);
  920. if (json["rtcgw"] !== "success") {
  921. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  922. callbacks.error(json["error"].reason);
  923. return;
  924. }
  925. connected = true;
  926. sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
  927. if (callbacks["reconnect"]) {
  928. Janus.log("Claimed session: " + sessionId);
  929. } else {
  930. Janus.log("Created session: " + sessionId);
  931. }
  932. Janus.sessions[sessionId] = that;
  933. eventHandler();
  934. callbacks.success();
  935. },
  936. error: function (textStatus, errorThrown) {
  937. Janus.error(textStatus + ":", errorThrown); // FIXME
  938. if (Janus.isArray(servers) && !callbacks["reconnect"]) {
  939. serversIndex++;
  940. if (serversIndex == servers.length) {
  941. // We tried all the servers the user gave us and they all failed
  942. callbacks.error("Error connecting to any of the provided Janus servers: Is the server down?");
  943. return;
  944. }
  945. // Let's try the next server
  946. server = null;
  947. setTimeout(function () {
  948. createSession(callbacks);
  949. }, 200);
  950. return;
  951. }
  952. if (errorThrown === "")
  953. callbacks.error(textStatus + ": Is the server down?");
  954. else
  955. callbacks.error(textStatus + ": " + errorThrown);
  956. }
  957. });
  958. }
  959. // Private method to destroy a session
  960. function destroySession(callbacks) {
  961. callbacks = callbacks || {};
  962. // FIXME This method triggers a success even when we fail
  963. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  964. var asyncRequest = true;
  965. if (callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null)
  966. asyncRequest = (callbacks.asyncRequest === true);
  967. var notifyDestroyed = true;
  968. if (callbacks.notifyDestroyed !== undefined && callbacks.notifyDestroyed !== null)
  969. notifyDestroyed = (callbacks.notifyDestroyed === true);
  970. var cleanupHandles = false;
  971. if (callbacks.cleanupHandles !== undefined && callbacks.cleanupHandles !== null)
  972. cleanupHandles = (callbacks.cleanupHandles === true);
  973. Janus.log("Destroying session " + sessionId + " (async=" + asyncRequest + ")");
  974. if (!connected) {
  975. Janus.warn("Is the server down? (connected=false)");
  976. callbacks.success();
  977. return;
  978. }
  979. if (sessionId === undefined || sessionId === null) {
  980. Janus.warn("No session to destroy");
  981. callbacks.success();
  982. if (notifyDestroyed)
  983. gatewayCallbacks.destroyed();
  984. return;
  985. }
  986. if (cleanupHandles) {
  987. for (var handleId in pluginHandles)
  988. destroyHandle(handleId, {noRequest: true});
  989. }
  990. // No need to destroy all handles first, Janus will do that itself
  991. var request = {"rtcgw": "destroy", "transaction": Janus.randomString(12)};
  992. if (token !== null && token !== undefined)
  993. request["token"] = token;
  994. if (apisecret !== null && apisecret !== undefined)
  995. request["apisecret"] = apisecret;
  996. if (websockets) {
  997. request["session_id"] = sessionId;
  998. var unbindWebSocket = function () {
  999. for (var eventName in wsHandlers) {
  1000. ws.removeEventListener(eventName, wsHandlers[eventName]);
  1001. }
  1002. ws.removeEventListener('message', onUnbindMessage);
  1003. ws.removeEventListener('error', onUnbindError);
  1004. if (wsKeepaliveTimeoutId) {
  1005. clearTimeout(wsKeepaliveTimeoutId);
  1006. }
  1007. ws.close();
  1008. };
  1009. var onUnbindMessage = function (event) {
  1010. var data = JSON.parse(event.data);
  1011. if (data.session_id == request.session_id && data.transaction == request.transaction) {
  1012. unbindWebSocket();
  1013. callbacks.success();
  1014. if (notifyDestroyed)
  1015. gatewayCallbacks.destroyed();
  1016. }
  1017. };
  1018. var onUnbindError = function (event) {
  1019. unbindWebSocket();
  1020. callbacks.error("Failed to destroy the server: Is the server down?");
  1021. if (notifyDestroyed)
  1022. gatewayCallbacks.destroyed();
  1023. };
  1024. ws.addEventListener('message', onUnbindMessage);
  1025. ws.addEventListener('error', onUnbindError);
  1026. ws.send(JSON.stringify(request));
  1027. return;
  1028. }
  1029. Janus.httpAPICall(server + "/" + sessionId, {
  1030. verb: 'POST',
  1031. async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work
  1032. withCredentials: withCredentials,
  1033. body: request,
  1034. success: function (json) {
  1035. Janus.log("Destroyed session:");
  1036. Janus.debug(json);
  1037. sessionId = null;
  1038. connected = false;
  1039. if (json["rtcgw"] !== "success") {
  1040. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1041. }
  1042. callbacks.success();
  1043. if (notifyDestroyed)
  1044. gatewayCallbacks.destroyed();
  1045. },
  1046. error: function (textStatus, errorThrown) {
  1047. Janus.error(textStatus + ":", errorThrown); // FIXME
  1048. // Reset everything anyway
  1049. sessionId = null;
  1050. connected = false;
  1051. callbacks.success();
  1052. if (notifyDestroyed)
  1053. gatewayCallbacks.destroyed();
  1054. }
  1055. });
  1056. }
  1057. // Private method to create a plugin handle
  1058. function createHandle(callbacks) {
  1059. callbacks = callbacks || {};
  1060. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  1061. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  1062. callbacks.consentDialog = (typeof callbacks.consentDialog == "function") ? callbacks.consentDialog : Janus.noop;
  1063. callbacks.iceState = (typeof callbacks.iceState == "function") ? callbacks.iceState : Janus.noop;
  1064. callbacks.mediaState = (typeof callbacks.mediaState == "function") ? callbacks.mediaState : Janus.noop;
  1065. callbacks.webrtcState = (typeof callbacks.webrtcState == "function") ? callbacks.webrtcState : Janus.noop;
  1066. callbacks.slowLink = (typeof callbacks.slowLink == "function") ? callbacks.slowLink : Janus.noop;
  1067. callbacks.onmessage = (typeof callbacks.onmessage == "function") ? callbacks.onmessage : Janus.noop;
  1068. callbacks.onlocalstream = (typeof callbacks.onlocalstream == "function") ? callbacks.onlocalstream : Janus.noop;
  1069. callbacks.onremotestream = (typeof callbacks.onremotestream == "function") ? callbacks.onremotestream : Janus.noop;
  1070. callbacks.ondata = (typeof callbacks.ondata == "function") ? callbacks.ondata : Janus.noop;
  1071. callbacks.ondataopen = (typeof callbacks.ondataopen == "function") ? callbacks.ondataopen : Janus.noop;
  1072. callbacks.oncleanup = (typeof callbacks.oncleanup == "function") ? callbacks.oncleanup : Janus.noop;
  1073. callbacks.ondetached = (typeof callbacks.ondetached == "function") ? callbacks.ondetached : Janus.noop;
  1074. if (!connected) {
  1075. Janus.warn("Is the server down? (connected=false)");
  1076. callbacks.error("Is the server down? (connected=false)");
  1077. return;
  1078. }
  1079. var plugin = callbacks.plugin;
  1080. if (plugin === undefined || plugin === null) {
  1081. Janus.error("Invalid plugin");
  1082. callbacks.error("Invalid plugin");
  1083. return;
  1084. }
  1085. var opaqueId = callbacks.opaqueId;
  1086. var handleToken = callbacks.token ? callbacks.token : token;
  1087. var transaction = Janus.randomString(12);
  1088. var request = {"rtcgw": "attach", "plugin": plugin, "opaque_id": opaqueId, "transaction": transaction};
  1089. if (handleToken !== null && handleToken !== undefined)
  1090. request["token"] = handleToken;
  1091. if (apisecret !== null && apisecret !== undefined)
  1092. request["apisecret"] = apisecret;
  1093. if (websockets) {
  1094. transactions[transaction] = function (json) {
  1095. Janus.debug(json);
  1096. if (json["rtcgw"] !== "success") {
  1097. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1098. callbacks.error("Ooops: " + json["error"].code + " " + json["error"].reason);
  1099. return;
  1100. }
  1101. var handleId = json.data["id"];
  1102. Janus.log("Created handle: " + handleId);
  1103. var pluginHandle =
  1104. {
  1105. session: that,
  1106. plugin: plugin,
  1107. id: handleId,
  1108. token: handleToken,
  1109. detached: false,
  1110. webrtcStuff: {
  1111. started: false,
  1112. myStream: null,
  1113. streamExternal: false,
  1114. remoteStream: null,
  1115. mySdp: null,
  1116. mediaConstraints: null,
  1117. pc: null,
  1118. dataChannel: {},
  1119. dtmfSender: null,
  1120. trickle: true,
  1121. iceDone: false,
  1122. volume: {
  1123. value: null,
  1124. timer: null
  1125. },
  1126. bitrate: {
  1127. value: null,
  1128. bsnow: null,
  1129. bsbefore: null,
  1130. tsnow: null,
  1131. tsbefore: null,
  1132. timer: null
  1133. }
  1134. },
  1135. getId: function () {
  1136. return handleId;
  1137. },
  1138. getPlugin: function () {
  1139. return plugin;
  1140. },
  1141. getVolume: function () {
  1142. return getVolume(handleId, true);
  1143. },
  1144. getRemoteVolume: function () {
  1145. return getVolume(handleId, true);
  1146. },
  1147. getLocalVolume: function () {
  1148. return getVolume(handleId, false);
  1149. },
  1150. isAudioMuted: function () {
  1151. return isMuted(handleId, false);
  1152. },
  1153. muteAudio: function () {
  1154. return mute(handleId, false, true);
  1155. },
  1156. unmuteAudio: function () {
  1157. return mute(handleId, false, false);
  1158. },
  1159. isVideoMuted: function () {
  1160. return isMuted(handleId, true);
  1161. },
  1162. muteVideo: function () {
  1163. return mute(handleId, true, true);
  1164. },
  1165. unmuteVideo: function () {
  1166. return mute(handleId, true, false);
  1167. },
  1168. getBitrate: function () {
  1169. return getBitrate(handleId);
  1170. },
  1171. send: function (callbacks) {
  1172. sendMessage(handleId, callbacks);
  1173. },
  1174. data: function (callbacks) {
  1175. sendData(handleId, callbacks);
  1176. },
  1177. dtmf: function (callbacks) {
  1178. sendDtmf(handleId, callbacks);
  1179. },
  1180. consentDialog: callbacks.consentDialog,
  1181. iceState: callbacks.iceState,
  1182. mediaState: callbacks.mediaState,
  1183. webrtcState: callbacks.webrtcState,
  1184. slowLink: callbacks.slowLink,
  1185. onmessage: callbacks.onmessage,
  1186. createOffer: function (callbacks) {
  1187. prepareWebrtc(handleId, true, callbacks);
  1188. },
  1189. createAnswer: function (callbacks) {
  1190. prepareWebrtc(handleId, false, callbacks);
  1191. },
  1192. handleRemoteJsep: function (callbacks) {
  1193. prepareWebrtcPeer(handleId, callbacks);
  1194. },
  1195. onlocalstream: callbacks.onlocalstream,
  1196. onremotestream: callbacks.onremotestream,
  1197. ondata: callbacks.ondata,
  1198. ondataopen: callbacks.ondataopen,
  1199. oncleanup: callbacks.oncleanup,
  1200. ondetached: callbacks.ondetached,
  1201. hangup: function (sendRequest) {
  1202. cleanupWebrtc(handleId, sendRequest === true);
  1203. },
  1204. detach: function (callbacks) {
  1205. destroyHandle(handleId, callbacks);
  1206. }
  1207. }
  1208. pluginHandles[handleId] = pluginHandle;
  1209. callbacks.success(pluginHandle);
  1210. };
  1211. request["session_id"] = sessionId;
  1212. ws.send(JSON.stringify(request));
  1213. return;
  1214. }
  1215. Janus.httpAPICall(server + "/" + sessionId, {
  1216. verb: 'POST',
  1217. withCredentials: withCredentials,
  1218. body: request,
  1219. success: function (json) {
  1220. Janus.debug(json);
  1221. if (json["rtcgw"] !== "success") {
  1222. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1223. callbacks.error("Ooops: " + json["error"].code + " " + json["error"].reason);
  1224. return;
  1225. }
  1226. var handleId = json.data["id"];
  1227. Janus.log("Created handle: " + handleId);
  1228. var pluginHandle =
  1229. {
  1230. session: that,
  1231. plugin: plugin,
  1232. id: handleId,
  1233. token: handleToken,
  1234. detached: false,
  1235. webrtcStuff: {
  1236. started: false,
  1237. myStream: null,
  1238. streamExternal: false,
  1239. remoteStream: null,
  1240. mySdp: null,
  1241. mediaConstraints: null,
  1242. pc: null,
  1243. dataChannel: {},
  1244. dtmfSender: null,
  1245. trickle: true,
  1246. iceDone: false,
  1247. volume: {
  1248. value: null,
  1249. timer: null
  1250. },
  1251. bitrate: {
  1252. value: null,
  1253. bsnow: null,
  1254. bsbefore: null,
  1255. tsnow: null,
  1256. tsbefore: null,
  1257. timer: null
  1258. }
  1259. },
  1260. getId: function () {
  1261. return handleId;
  1262. },
  1263. getPlugin: function () {
  1264. return plugin;
  1265. },
  1266. getVolume: function () {
  1267. return getVolume(handleId, true);
  1268. },
  1269. getRemoteVolume: function () {
  1270. return getVolume(handleId, true);
  1271. },
  1272. getLocalVolume: function () {
  1273. return getVolume(handleId, false);
  1274. },
  1275. isAudioMuted: function () {
  1276. return isMuted(handleId, false);
  1277. },
  1278. muteAudio: function () {
  1279. return mute(handleId, false, true);
  1280. },
  1281. unmuteAudio: function () {
  1282. return mute(handleId, false, false);
  1283. },
  1284. isVideoMuted: function () {
  1285. return isMuted(handleId, true);
  1286. },
  1287. muteVideo: function () {
  1288. return mute(handleId, true, true);
  1289. },
  1290. unmuteVideo: function () {
  1291. return mute(handleId, true, false);
  1292. },
  1293. getBitrate: function () {
  1294. return getBitrate(handleId);
  1295. },
  1296. send: function (callbacks) {
  1297. sendMessage(handleId, callbacks);
  1298. },
  1299. data: function (callbacks) {
  1300. sendData(handleId, callbacks);
  1301. },
  1302. dtmf: function (callbacks) {
  1303. sendDtmf(handleId, callbacks);
  1304. },
  1305. consentDialog: callbacks.consentDialog,
  1306. iceState: callbacks.iceState,
  1307. mediaState: callbacks.mediaState,
  1308. webrtcState: callbacks.webrtcState,
  1309. slowLink: callbacks.slowLink,
  1310. onmessage: callbacks.onmessage,
  1311. createOffer: function (callbacks) {
  1312. prepareWebrtc(handleId, true, callbacks);
  1313. },
  1314. createAnswer: function (callbacks) {
  1315. prepareWebrtc(handleId, false, callbacks);
  1316. },
  1317. handleRemoteJsep: function (callbacks) {
  1318. prepareWebrtcPeer(handleId, callbacks);
  1319. },
  1320. onlocalstream: callbacks.onlocalstream,
  1321. onremotestream: callbacks.onremotestream,
  1322. ondata: callbacks.ondata,
  1323. ondataopen: callbacks.ondataopen,
  1324. oncleanup: callbacks.oncleanup,
  1325. ondetached: callbacks.ondetached,
  1326. hangup: function (sendRequest) {
  1327. cleanupWebrtc(handleId, sendRequest === true);
  1328. },
  1329. detach: function (callbacks) {
  1330. destroyHandle(handleId, callbacks);
  1331. }
  1332. }
  1333. pluginHandles[handleId] = pluginHandle;
  1334. callbacks.success(pluginHandle);
  1335. },
  1336. error: function (textStatus, errorThrown) {
  1337. Janus.error(textStatus + ":", errorThrown); // FIXME
  1338. }
  1339. });
  1340. }
  1341. // Private method to send a message
  1342. function sendMessage(handleId, callbacks) {
  1343. callbacks = callbacks || {};
  1344. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  1345. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  1346. if (!connected) {
  1347. Janus.warn("Is the server down? (connected=false)");
  1348. callbacks.error("Is the server down? (connected=false)");
  1349. return;
  1350. }
  1351. var pluginHandle = pluginHandles[handleId];
  1352. if (pluginHandle === null || pluginHandle === undefined ||
  1353. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1354. Janus.warn("Invalid handle");
  1355. callbacks.error("Invalid handle");
  1356. return;
  1357. }
  1358. var message = callbacks.message;
  1359. var jsep = callbacks.jsep;
  1360. var transaction = Janus.randomString(12);
  1361. var request = {"rtcgw": "message", "body": message, "transaction": transaction};
  1362. if (pluginHandle.token !== null && pluginHandle.token !== undefined)
  1363. request["token"] = pluginHandle.token;
  1364. if (apisecret !== null && apisecret !== undefined)
  1365. request["apisecret"] = apisecret;
  1366. if (jsep !== null && jsep !== undefined)
  1367. request.jsep = jsep;
  1368. Janus.debug("Sending message to plugin (handle=" + handleId + "):");
  1369. Janus.debug(request);
  1370. if (websockets) {
  1371. request["session_id"] = sessionId;
  1372. request["handle_id"] = handleId;
  1373. transactions[transaction] = function (json) {
  1374. Janus.debug("Message sent!");
  1375. Janus.debug(json);
  1376. if (json["rtcgw"] === "success") {
  1377. // We got a success, must have been a synchronous transaction
  1378. var plugindata = json["plugindata"];
  1379. if (plugindata === undefined || plugindata === null) {
  1380. Janus.warn("Request succeeded, but missing plugindata...");
  1381. callbacks.success();
  1382. return;
  1383. }
  1384. Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")");
  1385. var data = plugindata["data"];
  1386. Janus.debug(data);
  1387. callbacks.success(data);
  1388. return;
  1389. } else if (json["rtcgw"] !== "ack") {
  1390. // Not a success and not an ack, must be an error
  1391. if (json["error"] !== undefined && json["error"] !== null) {
  1392. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1393. callbacks.error(json["error"].code + " " + json["error"].reason);
  1394. } else {
  1395. Janus.error("Unknown error"); // FIXME
  1396. callbacks.error("Unknown error");
  1397. }
  1398. return;
  1399. }
  1400. // If we got here, the plugin decided to handle the request asynchronously
  1401. callbacks.success();
  1402. };
  1403. ws.send(JSON.stringify(request));
  1404. return;
  1405. }
  1406. Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
  1407. verb: 'POST',
  1408. withCredentials: withCredentials,
  1409. body: request,
  1410. success: function (json) {
  1411. Janus.debug("Message sent!");
  1412. Janus.debug(json);
  1413. if (json["rtcgw"] === "success") {
  1414. // We got a success, must have been a synchronous transaction
  1415. var plugindata = json["plugindata"];
  1416. if (plugindata === undefined || plugindata === null) {
  1417. Janus.warn("Request succeeded, but missing plugindata...");
  1418. callbacks.success();
  1419. return;
  1420. }
  1421. Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")");
  1422. var data = plugindata["data"];
  1423. Janus.debug(data);
  1424. callbacks.success(data);
  1425. return;
  1426. } else if (json["rtcgw"] !== "ack") {
  1427. // Not a success and not an ack, must be an error
  1428. if (json["error"] !== undefined && json["error"] !== null) {
  1429. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1430. callbacks.error(json["error"].code + " " + json["error"].reason);
  1431. } else {
  1432. Janus.error("Unknown error"); // FIXME
  1433. callbacks.error("Unknown error");
  1434. }
  1435. return;
  1436. }
  1437. // If we got here, the plugin decided to handle the request asynchronously
  1438. callbacks.success();
  1439. },
  1440. error: function (textStatus, errorThrown) {
  1441. Janus.error(textStatus + ":", errorThrown); // FIXME
  1442. callbacks.error(textStatus + ": " + errorThrown);
  1443. }
  1444. });
  1445. }
  1446. // Private method to send a trickle candidate
  1447. function sendTrickleCandidate(handleId, candidate) {
  1448. if (!connected) {
  1449. Janus.warn("Is the server down? (connected=false)");
  1450. return;
  1451. }
  1452. var pluginHandle = pluginHandles[handleId];
  1453. if (pluginHandle === null || pluginHandle === undefined ||
  1454. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1455. Janus.warn("Invalid handle");
  1456. return;
  1457. }
  1458. var request = {"rtcgw": "trickle", "candidate": candidate, "transaction": Janus.randomString(12)};
  1459. if (pluginHandle.token !== null && pluginHandle.token !== undefined)
  1460. request["token"] = pluginHandle.token;
  1461. if (apisecret !== null && apisecret !== undefined)
  1462. request["apisecret"] = apisecret;
  1463. Janus.vdebug("Sending trickle candidate (handle=" + handleId + "):");
  1464. Janus.vdebug(request);
  1465. if (websockets) {
  1466. request["session_id"] = sessionId;
  1467. request["handle_id"] = handleId;
  1468. ws.send(JSON.stringify(request));
  1469. return;
  1470. }
  1471. Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
  1472. verb: 'POST',
  1473. withCredentials: withCredentials,
  1474. body: request,
  1475. success: function (json) {
  1476. Janus.vdebug("Candidate sent!");
  1477. Janus.vdebug(json);
  1478. if (json["rtcgw"] !== "ack") {
  1479. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1480. return;
  1481. }
  1482. },
  1483. error: function (textStatus, errorThrown) {
  1484. Janus.error(textStatus + ":", errorThrown); // FIXME
  1485. }
  1486. });
  1487. }
  1488. // Private method to create a data channel
  1489. function createDataChannel(handleId, dclabel, incoming, pendingText) {
  1490. var pluginHandle = pluginHandles[handleId];
  1491. if (pluginHandle === null || pluginHandle === undefined ||
  1492. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1493. Janus.warn("Invalid handle");
  1494. return;
  1495. }
  1496. var config = pluginHandle.webrtcStuff;
  1497. var onDataChannelMessage = function (event) {
  1498. Janus.log('Received message on data channel:', event);
  1499. var label = event.target.label;
  1500. pluginHandle.ondata(event.data, label);
  1501. }
  1502. var onDataChannelStateChange = function (event) {
  1503. Janus.log('Received state change on data channel:', event);
  1504. var label = event.target.label;
  1505. var dcState = config.dataChannel[label] ? config.dataChannel[label].readyState : "null";
  1506. Janus.log('State change on <' + label + '> data channel: ' + dcState);
  1507. if (dcState === 'open') {
  1508. // Any pending messages to send?
  1509. if (config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) {
  1510. Janus.log("Sending pending messages on <" + label + ">:", config.dataChannel[label].pending.length);
  1511. for (var i in config.dataChannel[label].pending) {
  1512. var text = config.dataChannel[label].pending[i];
  1513. Janus.log("Sending string on data channel <" + label + ">: " + text);
  1514. config.dataChannel[label].send(text);
  1515. }
  1516. config.dataChannel[label].pending = [];
  1517. }
  1518. // Notify the open data channel
  1519. pluginHandle.ondataopen(label);
  1520. }
  1521. }
  1522. var onDataChannelError = function (error) {
  1523. Janus.error('Got error on data channel:', error);
  1524. // TODO
  1525. }
  1526. if (!incoming) {
  1527. // FIXME Add options (ordered, maxRetransmits, etc.)
  1528. config.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, {ordered: false});
  1529. } else {
  1530. // The channel was created by Janus
  1531. config.dataChannel[dclabel] = incoming;
  1532. }
  1533. config.dataChannel[dclabel].onmessage = onDataChannelMessage;
  1534. config.dataChannel[dclabel].onopen = onDataChannelStateChange;
  1535. config.dataChannel[dclabel].onclose = onDataChannelStateChange;
  1536. config.dataChannel[dclabel].onerror = onDataChannelError;
  1537. config.dataChannel[dclabel].pending = [];
  1538. if (pendingText)
  1539. config.dataChannel[dclabel].pending.push(pendingText);
  1540. }
  1541. // Private method to send a data channel message
  1542. function sendData(handleId, callbacks) {
  1543. callbacks = callbacks || {};
  1544. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  1545. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  1546. var pluginHandle = pluginHandles[handleId];
  1547. if (pluginHandle === null || pluginHandle === undefined ||
  1548. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1549. Janus.warn("Invalid handle");
  1550. callbacks.error("Invalid handle");
  1551. return;
  1552. }
  1553. var config = pluginHandle.webrtcStuff;
  1554. var text = callbacks.text;
  1555. if (text === null || text === undefined) {
  1556. Janus.warn("Invalid text");
  1557. callbacks.error("Invalid text");
  1558. return;
  1559. }
  1560. var label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel;
  1561. if (!config.dataChannel[label]) {
  1562. // Create new data channel and wait for it to open
  1563. createDataChannel(handleId, label, false, text);
  1564. callbacks.success();
  1565. return;
  1566. }
  1567. if (config.dataChannel[label].readyState !== "open") {
  1568. config.dataChannel[label].pending.push(text);
  1569. callbacks.success();
  1570. return;
  1571. }
  1572. Janus.log("Sending string on data channel <" + label + ">: " + text);
  1573. config.dataChannel[label].send(text);
  1574. callbacks.success();
  1575. }
  1576. // Private method to send a DTMF tone
  1577. function sendDtmf(handleId, callbacks) {
  1578. callbacks = callbacks || {};
  1579. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  1580. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  1581. var pluginHandle = pluginHandles[handleId];
  1582. if (pluginHandle === null || pluginHandle === undefined ||
  1583. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1584. Janus.warn("Invalid handle");
  1585. callbacks.error("Invalid handle");
  1586. return;
  1587. }
  1588. var config = pluginHandle.webrtcStuff;
  1589. if (config.dtmfSender === null || config.dtmfSender === undefined) {
  1590. // Create the DTMF sender the proper way, if possible
  1591. if (config.pc !== undefined && config.pc !== null) {
  1592. var senders = config.pc.getSenders();
  1593. var audioSender = senders.find(function (sender) {
  1594. return sender.track && sender.track.kind === 'audio';
  1595. });
  1596. if (!audioSender) {
  1597. Janus.warn("Invalid DTMF configuration (no audio track)");
  1598. callbacks.error("Invalid DTMF configuration (no audio track)");
  1599. return;
  1600. }
  1601. config.dtmfSender = audioSender.dtmf;
  1602. if (config.dtmfSender) {
  1603. Janus.log("Created DTMF Sender");
  1604. config.dtmfSender.ontonechange = function (tone) {
  1605. Janus.debug("Sent DTMF tone: " + tone.tone);
  1606. };
  1607. }
  1608. }
  1609. if (config.dtmfSender === null || config.dtmfSender === undefined) {
  1610. Janus.warn("Invalid DTMF configuration");
  1611. callbacks.error("Invalid DTMF configuration");
  1612. return;
  1613. }
  1614. }
  1615. var dtmf = callbacks.dtmf;
  1616. if (dtmf === null || dtmf === undefined) {
  1617. Janus.warn("Invalid DTMF parameters");
  1618. callbacks.error("Invalid DTMF parameters");
  1619. return;
  1620. }
  1621. var tones = dtmf.tones;
  1622. if (tones === null || tones === undefined) {
  1623. Janus.warn("Invalid DTMF string");
  1624. callbacks.error("Invalid DTMF string");
  1625. return;
  1626. }
  1627. var duration = dtmf.duration;
  1628. if (duration === null || duration === undefined)
  1629. duration = 500; // We choose 500ms as the default duration for a tone
  1630. var gap = dtmf.gap;
  1631. if (gap === null || gap === undefined)
  1632. gap = 50; // We choose 50ms as the default gap between tones
  1633. Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)");
  1634. config.dtmfSender.insertDTMF(tones, duration, gap);
  1635. callbacks.success();
  1636. }
  1637. // Private method to destroy a plugin handle
  1638. function destroyHandle(handleId, callbacks) {
  1639. callbacks = callbacks || {};
  1640. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  1641. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  1642. var asyncRequest = true;
  1643. if (callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null)
  1644. asyncRequest = (callbacks.asyncRequest === true);
  1645. var noRequest = true;
  1646. if (callbacks.noRequest !== undefined && callbacks.noRequest !== null)
  1647. noRequest = (callbacks.noRequest === true);
  1648. Janus.log("Destroying handle " + handleId + " (async=" + asyncRequest + ")");
  1649. cleanupWebrtc(handleId);
  1650. var pluginHandle = pluginHandles[handleId];
  1651. if (pluginHandle === null || pluginHandle === undefined || pluginHandle.detached) {
  1652. // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here
  1653. delete pluginHandles[handleId];
  1654. callbacks.success();
  1655. return;
  1656. }
  1657. if (noRequest) {
  1658. // We're only removing the handle locally
  1659. delete pluginHandles[handleId];
  1660. callbacks.success();
  1661. return;
  1662. }
  1663. if (!connected) {
  1664. Janus.warn("Is the server down? (connected=false)");
  1665. callbacks.error("Is the server down? (connected=false)");
  1666. return;
  1667. }
  1668. var request = {"rtcgw": "detach", "transaction": Janus.randomString(12)};
  1669. if (pluginHandle.token !== null && pluginHandle.token !== undefined)
  1670. request["token"] = pluginHandle.token;
  1671. if (apisecret !== null && apisecret !== undefined)
  1672. request["apisecret"] = apisecret;
  1673. if (websockets) {
  1674. request["session_id"] = sessionId;
  1675. request["handle_id"] = handleId;
  1676. ws.send(JSON.stringify(request));
  1677. delete pluginHandles[handleId];
  1678. callbacks.success();
  1679. return;
  1680. }
  1681. Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
  1682. verb: 'POST',
  1683. async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work
  1684. withCredentials: withCredentials,
  1685. body: request,
  1686. success: function (json) {
  1687. Janus.log("Destroyed handle:");
  1688. Janus.debug(json);
  1689. if (json["rtcgw"] !== "success") {
  1690. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1691. }
  1692. delete pluginHandles[handleId];
  1693. callbacks.success();
  1694. },
  1695. error: function (textStatus, errorThrown) {
  1696. Janus.error(textStatus + ":", errorThrown); // FIXME
  1697. // We cleanup anyway
  1698. delete pluginHandles[handleId];
  1699. callbacks.success();
  1700. }
  1701. });
  1702. }
  1703. // WebRTC stuff
  1704. function streamsDone(handleId, jsep, media, callbacks, stream) {
  1705. var pluginHandle = pluginHandles[handleId];
  1706. if (pluginHandle === null || pluginHandle === undefined ||
  1707. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1708. Janus.warn("Invalid handle");
  1709. callbacks.error("Invalid handle");
  1710. return;
  1711. }
  1712. var config = pluginHandle.webrtcStuff;
  1713. Janus.debug("streamsDone:", stream);
  1714. if (stream) {
  1715. Janus.debug(" -- Audio tracks:", stream.getAudioTracks());
  1716. Janus.debug(" -- Video tracks:", stream.getVideoTracks());
  1717. }
  1718. // We're now capturing the new stream: check if we're updating or if it's a new thing
  1719. var addTracks = false;
  1720. if (!config.myStream || !media.update || config.streamExternal) {
  1721. config.myStream = stream;
  1722. addTracks = true;
  1723. } else {
  1724. // We only need to update the existing stream
  1725. if (((!media.update && isAudioSendEnabled(media)) || (media.update && (media.addAudio || media.replaceAudio))) &&
  1726. stream.getAudioTracks() && stream.getAudioTracks().length) {
  1727. config.myStream.addTrack(stream.getAudioTracks()[0]);
  1728. if (Janus.unifiedPlan) {
  1729. // Use Transceivers
  1730. Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]);
  1731. var audioTransceiver = null;
  1732. var transceivers = config.pc.getTransceivers();
  1733. if (transceivers && transceivers.length > 0) {
  1734. for (var i in transceivers) {
  1735. var t = transceivers[i];
  1736. if ((t.sender && t.sender.track && t.sender.track.kind === "audio") ||
  1737. (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) {
  1738. audioTransceiver = t;
  1739. break;
  1740. }
  1741. }
  1742. }
  1743. if (audioTransceiver && audioTransceiver.sender) {
  1744. audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]);
  1745. } else {
  1746. config.pc.addTrack(stream.getAudioTracks()[0], stream);
  1747. }
  1748. } else {
  1749. Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]);
  1750. config.pc.addTrack(stream.getAudioTracks()[0], stream);
  1751. }
  1752. }
  1753. if (((!media.update && isVideoSendEnabled(media)) || (media.update && (media.addVideo || media.replaceVideo))) &&
  1754. stream.getVideoTracks() && stream.getVideoTracks().length) {
  1755. config.myStream.addTrack(stream.getVideoTracks()[0]);
  1756. if (Janus.unifiedPlan) {
  1757. // Use Transceivers
  1758. Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]);
  1759. var videoTransceiver = null;
  1760. var transceivers = config.pc.getTransceivers();
  1761. if (transceivers && transceivers.length > 0) {
  1762. for (var i in transceivers) {
  1763. var t = transceivers[i];
  1764. if ((t.sender && t.sender.track && t.sender.track.kind === "video") ||
  1765. (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) {
  1766. videoTransceiver = t;
  1767. break;
  1768. }
  1769. }
  1770. }
  1771. if (videoTransceiver && videoTransceiver.sender) {
  1772. videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]);
  1773. } else {
  1774. config.pc.addTrack(stream.getVideoTracks()[0], stream);
  1775. }
  1776. } else {
  1777. Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]);
  1778. config.pc.addTrack(stream.getVideoTracks()[0], stream);
  1779. }
  1780. }
  1781. }
  1782. // If we still need to create a PeerConnection, let's do that
  1783. if (!config.pc) {
  1784. var pc_config = {
  1785. "iceServers": iceServers,
  1786. "iceTransportPolicy": iceTransportPolicy,
  1787. "bundlePolicy": bundlePolicy
  1788. };
  1789. if (Janus.webRTCAdapter.browserDetails.browser === "chrome") {
  1790. // For Chrome versions before 72, we force a plan-b semantic, and unified-plan otherwise
  1791. pc_config["sdpSemantics"] = (Janus.webRTCAdapter.browserDetails.version < 72) ? "plan-b" : "unified-plan";
  1792. }
  1793. var pc_constraints = {
  1794. "optional": [{"DtlsSrtpKeyAgreement": true}]
  1795. };
  1796. if (ipv6Support === true) {
  1797. pc_constraints.optional.push({"googIPv6": true});
  1798. }
  1799. // Any custom constraint to add?
  1800. if (callbacks.rtcConstraints && typeof callbacks.rtcConstraints === 'object') {
  1801. Janus.debug("Adding custom PeerConnection constraints:", callbacks.rtcConstraints);
  1802. for (var i in callbacks.rtcConstraints) {
  1803. pc_constraints.optional.push(callbacks.rtcConstraints[i]);
  1804. }
  1805. }
  1806. if (Janus.webRTCAdapter.browserDetails.browser === "edge") {
  1807. // This is Edge, enable BUNDLE explicitly
  1808. pc_config.bundlePolicy = "max-bundle";
  1809. }
  1810. Janus.log("Creating PeerConnection");
  1811. Janus.debug(pc_constraints);
  1812. config.pc = new RTCPeerConnection(pc_config, pc_constraints);
  1813. Janus.debug(config.pc);
  1814. if (config.pc.getStats) { // FIXME
  1815. config.volume = {};
  1816. config.bitrate.value = "0 kbits/sec";
  1817. }
  1818. Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")");
  1819. config.pc.oniceconnectionstatechange = function (e) {
  1820. if (config.pc)
  1821. pluginHandle.iceState(config.pc.iceConnectionState);
  1822. };
  1823. config.pc.onicecandidate = function (event) {
  1824. if (event.candidate == null ||
  1825. (Janus.webRTCAdapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
  1826. Janus.log("End of candidates.");
  1827. config.iceDone = true;
  1828. if (config.trickle === true) {
  1829. // Notify end of candidates
  1830. sendTrickleCandidate(handleId, {"completed": true});
  1831. } else {
  1832. // No trickle, time to send the complete SDP (including all candidates)
  1833. sendSDP(handleId, callbacks);
  1834. }
  1835. } else {
  1836. // JSON.stringify doesn't work on some WebRTC objects anymore
  1837. // See https://code.google.com/p/chromium/issues/detail?id=467366
  1838. var candidate = {
  1839. "candidate": event.candidate.candidate,
  1840. "sdpMid": event.candidate.sdpMid,
  1841. "sdpMLineIndex": event.candidate.sdpMLineIndex
  1842. };
  1843. if (config.trickle === true) {
  1844. // Send candidate
  1845. sendTrickleCandidate(handleId, candidate);
  1846. }
  1847. }
  1848. };
  1849. config.pc.ontrack = function (event) {
  1850. Janus.log("Handling Remote Track");
  1851. Janus.debug(event);
  1852. if (!event.streams)
  1853. return;
  1854. config.remoteStream = event.streams[0];
  1855. pluginHandle.onremotestream(config.remoteStream);
  1856. if (event.track.onended)
  1857. return;
  1858. Janus.log("Adding onended callback to track:", event.track);
  1859. event.track.onended = function (ev) {
  1860. Janus.log("Remote track muted/removed:", ev);
  1861. if (config.remoteStream) {
  1862. config.remoteStream.removeTrack(ev.target);
  1863. pluginHandle.onremotestream(config.remoteStream);
  1864. }
  1865. };
  1866. event.track.onmute = event.track.onended;
  1867. event.track.onunmute = function (ev) {
  1868. Janus.log("Remote track flowing again:", ev);
  1869. try {
  1870. config.remoteStream.addTrack(ev.target);
  1871. pluginHandle.onremotestream(config.remoteStream);
  1872. } catch (e) {
  1873. Janus.error(e);
  1874. }
  1875. ;
  1876. };
  1877. };
  1878. }
  1879. if (addTracks && stream !== null && stream !== undefined) {
  1880. Janus.log('Adding local stream');
  1881. var simulcast2 = callbacks.simulcast2 === true ? true : false;
  1882. stream.getTracks().forEach(function (track) {
  1883. Janus.log('Adding local track:', track);
  1884. if (!simulcast2) {
  1885. config.pc.addTrack(track, stream);
  1886. } else {
  1887. if (track.kind === "audio") {
  1888. config.pc.addTrack(track, stream);
  1889. } else {
  1890. Janus.log('Enabling rid-based simulcasting:', track);
  1891. const maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates);
  1892. config.pc.addTransceiver(track, {
  1893. direction: "sendrecv",
  1894. streams: [stream],
  1895. sendEncodings: [
  1896. {rid: "h", active: true, maxBitrate: maxBitrates.high},
  1897. {rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2},
  1898. {rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4}
  1899. ]
  1900. });
  1901. }
  1902. }
  1903. });
  1904. }
  1905. // Any data channel to create?
  1906. if (isDataEnabled(media) && !config.dataChannel[Janus.dataChanDefaultLabel]) {
  1907. Janus.log("Creating data channel");
  1908. createDataChannel(handleId, Janus.dataChanDefaultLabel, false);
  1909. config.pc.ondatachannel = function (event) {
  1910. Janus.log("Data channel created by Janus:", event);
  1911. createDataChannel(handleId, event.channel.label, event.channel);
  1912. };
  1913. }
  1914. // If there's a new local stream, let's notify the application
  1915. if (config.myStream)
  1916. pluginHandle.onlocalstream(config.myStream);
  1917. // Create offer/answer now
  1918. if (jsep === null || jsep === undefined) {
  1919. createOffer(handleId, media, callbacks);
  1920. } else {
  1921. config.pc.setRemoteDescription(jsep)
  1922. .then(function () {
  1923. Janus.log("Remote description accepted!");
  1924. config.remoteSdp = jsep.sdp;
  1925. // Any trickle candidate we cached?
  1926. if (config.candidates && config.candidates.length > 0) {
  1927. for (var i = 0; i < config.candidates.length; i++) {
  1928. var candidate = config.candidates[i];
  1929. Janus.debug("Adding remote candidate:", candidate);
  1930. if (!candidate || candidate.completed === true) {
  1931. // end-of-candidates
  1932. config.pc.addIceCandidate(Janus.endOfCandidates);
  1933. } else {
  1934. // New candidate
  1935. config.pc.addIceCandidate(candidate);
  1936. }
  1937. }
  1938. config.candidates = [];
  1939. }
  1940. // Create the answer now
  1941. createAnswer(handleId, media, callbacks);
  1942. }, callbacks.error);
  1943. }
  1944. }
  1945. function prepareWebrtc(handleId, offer, callbacks) {
  1946. callbacks = callbacks || {};
  1947. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  1948. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError;
  1949. var jsep = callbacks.jsep;
  1950. if (offer && jsep) {
  1951. Janus.error("Provided a JSEP to a createOffer");
  1952. callbacks.error("Provided a JSEP to a createOffer");
  1953. return;
  1954. } else if (!offer && (!jsep || !jsep.type || !jsep.sdp)) {
  1955. Janus.error("A valid JSEP is required for createAnswer");
  1956. callbacks.error("A valid JSEP is required for createAnswer");
  1957. return;
  1958. }
  1959. callbacks.media = callbacks.media || {audio: true, video: true};
  1960. var media = callbacks.media;
  1961. var pluginHandle = pluginHandles[handleId];
  1962. if (pluginHandle === null || pluginHandle === undefined ||
  1963. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1964. Janus.warn("Invalid handle");
  1965. callbacks.error("Invalid handle");
  1966. return;
  1967. }
  1968. var config = pluginHandle.webrtcStuff;
  1969. config.trickle = isTrickleEnabled(callbacks.trickle);
  1970. // Are we updating a session?
  1971. if (config.pc === undefined || config.pc === null) {
  1972. // Nope, new PeerConnection
  1973. media.update = false;
  1974. media.keepAudio = false;
  1975. media.keepVideo = false;
  1976. } else if (config.pc !== undefined && config.pc !== null) {
  1977. Janus.log("Updating existing media session");
  1978. media.update = true;
  1979. // Check if there's anything to add/remove/replace, or if we
  1980. // can go directly to preparing the new SDP offer or answer
  1981. if (callbacks.stream !== null && callbacks.stream !== undefined) {
  1982. // External stream: is this the same as the one we were using before?
  1983. if (callbacks.stream !== config.myStream) {
  1984. Janus.log("Renegotiation involves a new external stream");
  1985. }
  1986. } else {
  1987. // Check if there are changes on audio
  1988. if (media.addAudio) {
  1989. media.keepAudio = false;
  1990. media.replaceAudio = false;
  1991. media.removeAudio = false;
  1992. media.audioSend = true;
  1993. if (config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
  1994. Janus.error("Can't add audio stream, there already is one");
  1995. callbacks.error("Can't add audio stream, there already is one");
  1996. return;
  1997. }
  1998. } else if (media.removeAudio) {
  1999. media.keepAudio = false;
  2000. media.replaceAudio = false;
  2001. media.addAudio = false;
  2002. media.audioSend = false;
  2003. } else if (media.replaceAudio) {
  2004. media.keepAudio = false;
  2005. media.addAudio = false;
  2006. media.removeAudio = false;
  2007. media.audioSend = true;
  2008. }
  2009. if (config.myStream === null || config.myStream === undefined) {
  2010. // No media stream: if we were asked to replace, it's actually an "add"
  2011. if (media.replaceAudio) {
  2012. media.keepAudio = false;
  2013. media.replaceAudio = false;
  2014. media.addAudio = true;
  2015. media.audioSend = true;
  2016. }
  2017. if (isAudioSendEnabled(media)) {
  2018. media.keepAudio = false;
  2019. media.addAudio = true;
  2020. }
  2021. } else {
  2022. if (config.myStream.getAudioTracks() === null
  2023. || config.myStream.getAudioTracks() === undefined
  2024. || config.myStream.getAudioTracks().length === 0) {
  2025. // No audio track: if we were asked to replace, it's actually an "add"
  2026. if (media.replaceAudio) {
  2027. media.keepAudio = false;
  2028. media.replaceAudio = false;
  2029. media.addAudio = true;
  2030. media.audioSend = true;
  2031. }
  2032. if (isAudioSendEnabled(media)) {
  2033. media.keepVideo = false;
  2034. media.addAudio = true;
  2035. }
  2036. } else {
  2037. // We have an audio track: should we keep it as it is?
  2038. if (isAudioSendEnabled(media) &&
  2039. !media.removeAudio && !media.replaceAudio) {
  2040. media.keepAudio = true;
  2041. }
  2042. }
  2043. }
  2044. // Check if there are changes on video
  2045. if (media.addVideo) {
  2046. media.keepVideo = false;
  2047. media.replaceVideo = false;
  2048. media.removeVideo = false;
  2049. media.videoSend = true;
  2050. if (config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
  2051. Janus.error("Can't add video stream, there already is one");
  2052. callbacks.error("Can't add video stream, there already is one");
  2053. return;
  2054. }
  2055. } else if (media.removeVideo) {
  2056. media.keepVideo = false;
  2057. media.replaceVideo = false;
  2058. media.addVideo = false;
  2059. media.videoSend = false;
  2060. } else if (media.replaceVideo) {
  2061. media.keepVideo = false;
  2062. media.addVideo = false;
  2063. media.removeVideo = false;
  2064. media.videoSend = true;
  2065. }
  2066. if (config.myStream === null || config.myStream === undefined) {
  2067. // No media stream: if we were asked to replace, it's actually an "add"
  2068. if (media.replaceVideo) {
  2069. media.keepVideo = false;
  2070. media.replaceVideo = false;
  2071. media.addVideo = true;
  2072. media.videoSend = true;
  2073. }
  2074. if (isVideoSendEnabled(media)) {
  2075. media.keepVideo = false;
  2076. media.addVideo = true;
  2077. }
  2078. } else {
  2079. if (config.myStream.getVideoTracks() === null
  2080. || config.myStream.getVideoTracks() === undefined
  2081. || config.myStream.getVideoTracks().length === 0) {
  2082. // No video track: if we were asked to replace, it's actually an "add"
  2083. if (media.replaceVideo) {
  2084. media.keepVideo = false;
  2085. media.replaceVideo = false;
  2086. media.addVideo = true;
  2087. media.videoSend = true;
  2088. }
  2089. if (isVideoSendEnabled(media)) {
  2090. media.keepVideo = false;
  2091. media.addVideo = true;
  2092. }
  2093. } else {
  2094. // We have a video track: should we keep it as it is?
  2095. if (isVideoSendEnabled(media) &&
  2096. !media.removeVideo && !media.replaceVideo) {
  2097. media.keepVideo = true;
  2098. }
  2099. }
  2100. }
  2101. // Data channels can only be added
  2102. if (media.addData)
  2103. media.data = true;
  2104. }
  2105. // If we're updating and keeping all tracks, let's skip the getUserMedia part
  2106. if ((isAudioSendEnabled(media) && media.keepAudio) &&
  2107. (isVideoSendEnabled(media) && media.keepVideo)) {
  2108. pluginHandle.consentDialog(false);
  2109. streamsDone(handleId, jsep, media, callbacks, config.myStream);
  2110. return;
  2111. }
  2112. }
  2113. // If we're updating, check if we need to remove/replace one of the tracks
  2114. if (media.update && !config.streamExternal) {
  2115. if (media.removeAudio || media.replaceAudio) {
  2116. if (config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
  2117. var s = config.myStream.getAudioTracks()[0];
  2118. Janus.log("Removing audio track:", s);
  2119. config.myStream.removeTrack(s);
  2120. try {
  2121. s.stop();
  2122. } catch (e) {
  2123. }
  2124. ;
  2125. }
  2126. if (config.pc.getSenders() && config.pc.getSenders().length) {
  2127. var ra = true;
  2128. if (media.replaceAudio && Janus.unifiedPlan) {
  2129. // We can use replaceTrack
  2130. ra = false;
  2131. }
  2132. if (ra) {
  2133. for (var index in config.pc.getSenders()) {
  2134. var s = config.pc.getSenders()[index];
  2135. if (s && s.track && s.track.kind === "audio") {
  2136. Janus.log("Removing audio sender:", s);
  2137. config.pc.removeTrack(s);
  2138. }
  2139. }
  2140. }
  2141. }
  2142. }
  2143. if (media.removeVideo || media.replaceVideo) {
  2144. if (config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
  2145. var s = config.myStream.getVideoTracks()[0];
  2146. Janus.log("Removing video track:", s);
  2147. config.myStream.removeTrack(s);
  2148. try {
  2149. s.stop();
  2150. } catch (e) {
  2151. }
  2152. ;
  2153. }
  2154. if (config.pc.getSenders() && config.pc.getSenders().length) {
  2155. var rv = true;
  2156. if (media.replaceVideo && Janus.unifiedPlan) {
  2157. // We can use replaceTrack
  2158. rv = false;
  2159. }
  2160. if (rv) {
  2161. for (var index in config.pc.getSenders()) {
  2162. var s = config.pc.getSenders()[index];
  2163. if (s && s.track && s.track.kind === "video") {
  2164. Janus.log("Removing video sender:", s);
  2165. config.pc.removeTrack(s);
  2166. }
  2167. }
  2168. }
  2169. }
  2170. }
  2171. }
  2172. // Was a MediaStream object passed, or do we need to take care of that?
  2173. if (callbacks.stream !== null && callbacks.stream !== undefined) {
  2174. var stream = callbacks.stream;
  2175. Janus.log("MediaStream provided by the application");
  2176. Janus.debug(stream);
  2177. // If this is an update, let's check if we need to release the previous stream
  2178. if (media.update) {
  2179. if (config.myStream && config.myStream !== callbacks.stream && !config.streamExternal) {
  2180. // We're replacing a stream we captured ourselves with an external one
  2181. try {
  2182. // Try a MediaStreamTrack.stop() for each track
  2183. var tracks = config.myStream.getTracks();
  2184. for (var i in tracks) {
  2185. var mst = tracks[i];
  2186. Janus.log(mst);
  2187. if (mst !== null && mst !== undefined)
  2188. mst.stop();
  2189. }
  2190. } catch (e) {
  2191. // Do nothing if this fails
  2192. }
  2193. config.myStream = null;
  2194. }
  2195. }
  2196. // Skip the getUserMedia part
  2197. config.streamExternal = true;
  2198. pluginHandle.consentDialog(false);
  2199. streamsDone(handleId, jsep, media, callbacks, stream);
  2200. return;
  2201. }
  2202. if (isAudioSendEnabled(media) || isVideoSendEnabled(media)) {
  2203. if (!Janus.isGetUserMediaAvailable()) {
  2204. callbacks.error("getUserMedia not available");
  2205. return;
  2206. }
  2207. var constraints = {mandatory: {}, optional: []};
  2208. pluginHandle.consentDialog(true);
  2209. var audioSupport = isAudioSendEnabled(media);
  2210. if (audioSupport === true && media != undefined && media != null) {
  2211. if (typeof media.audio === 'object') {
  2212. audioSupport = media.audio;
  2213. }
  2214. }
  2215. var videoSupport = isVideoSendEnabled(media);
  2216. if (videoSupport === true && media != undefined && media != null) {
  2217. var simulcast = callbacks.simulcast === true ? true : false;
  2218. var simulcast2 = callbacks.simulcast2 === true ? true : false;
  2219. if ((simulcast || simulcast2) && !jsep && (media.video === undefined || media.video === false))
  2220. media.video = "hires";
  2221. if (media.video && media.video != 'screen' && media.video != 'window') {
  2222. if (typeof media.video === 'object') {
  2223. videoSupport = media.video;
  2224. } else {
  2225. var width = 0;
  2226. var height = 0, maxHeight = 0;
  2227. if (media.video === 'lowres') {
  2228. // Small resolution, 4:3
  2229. height = 240;
  2230. maxHeight = 240;
  2231. width = 320;
  2232. } else if (media.video === 'lowres-16:9') {
  2233. // Small resolution, 16:9
  2234. height = 180;
  2235. maxHeight = 180;
  2236. width = 320;
  2237. } else if (media.video === 'hires' || media.video === 'hires-16:9' || media.video === 'hdres') {
  2238. // High(HD) resolution is only 16:9
  2239. height = 720;
  2240. maxHeight = 720;
  2241. width = 1280;
  2242. } else if (media.video === 'fhdres') {
  2243. // Full HD resolution is only 16:9
  2244. height = 1080;
  2245. maxHeight = 1080;
  2246. width = 1920;
  2247. } else if (media.video === '4kres') {
  2248. // 4K resolution is only 16:9
  2249. height = 2160;
  2250. maxHeight = 2160;
  2251. width = 3840;
  2252. } else if (media.video === 'stdres') {
  2253. // Normal resolution, 4:3
  2254. height = 480;
  2255. maxHeight = 480;
  2256. width = 640;
  2257. } else if (media.video === 'stdres-16:9') {
  2258. // Normal resolution, 16:9
  2259. height = 360;
  2260. maxHeight = 360;
  2261. width = 640;
  2262. } else {
  2263. Janus.log("Default video setting is stdres 4:3");
  2264. height = 480;
  2265. maxHeight = 480;
  2266. width = 640;
  2267. }
  2268. Janus.log("Adding media constraint:", media.video);
  2269. videoSupport = {
  2270. 'height': {'ideal': height},
  2271. 'width': {'ideal': width}
  2272. };
  2273. Janus.log("Adding video constraint:", videoSupport);
  2274. }
  2275. } else if (media.video === 'screen' || media.video === 'window') {
  2276. if (!media.screenshareFrameRate) {
  2277. media.screenshareFrameRate = 3;
  2278. }
  2279. if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
  2280. // The new experimental getDisplayMedia API is available, let's use that
  2281. // https://groups.google.com/forum/#!topic/discuss-webrtc/Uf0SrR4uxzk
  2282. // https://webrtchacks.com/chrome-screensharing-getdisplaymedia/
  2283. navigator.mediaDevices.getDisplayMedia({video: true})
  2284. .then(function (stream) {
  2285. pluginHandle.consentDialog(false);
  2286. if (isAudioSendEnabled(media) && !media.keepAudio) {
  2287. navigator.mediaDevices.getUserMedia({audio: true, video: false})
  2288. .then(function (audioStream) {
  2289. stream.addTrack(audioStream.getAudioTracks()[0]);
  2290. streamsDone(handleId, jsep, media, callbacks, stream);
  2291. })
  2292. } else {
  2293. streamsDone(handleId, jsep, media, callbacks, stream);
  2294. }
  2295. }, function (error) {
  2296. pluginHandle.consentDialog(false);
  2297. callbacks.error(error);
  2298. });
  2299. return;
  2300. }
  2301. // We're going to try and use the extension for Chrome 34+, the old approach
  2302. // for older versions of Chrome, or the experimental support in Firefox 33+
  2303. function callbackUserMedia(error, stream) {
  2304. pluginHandle.consentDialog(false);
  2305. if (error) {
  2306. callbacks.error(error);
  2307. } else {
  2308. streamsDone(handleId, jsep, media, callbacks, stream);
  2309. }
  2310. };
  2311. function getScreenMedia(constraints, gsmCallback, useAudio) {
  2312. Janus.log("Adding media constraint (screen capture)");
  2313. Janus.debug(constraints);
  2314. navigator.mediaDevices.getUserMedia(constraints)
  2315. .then(function (stream) {
  2316. if (useAudio) {
  2317. navigator.mediaDevices.getUserMedia({audio: true, video: false})
  2318. .then(function (audioStream) {
  2319. stream.addTrack(audioStream.getAudioTracks()[0]);
  2320. gsmCallback(null, stream);
  2321. })
  2322. } else {
  2323. gsmCallback(null, stream);
  2324. }
  2325. })
  2326. .catch(function (error) {
  2327. pluginHandle.consentDialog(false);
  2328. gsmCallback(error);
  2329. });
  2330. };
  2331. if (Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
  2332. var chromever = Janus.webRTCAdapter.browserDetails.version;
  2333. var maxver = 33;
  2334. if (window.navigator.userAgent.match('Linux'))
  2335. maxver = 35; // "known" crash in chrome 34 and 35 on linux
  2336. if (chromever >= 26 && chromever <= maxver) {
  2337. // Chrome 26->33 requires some awkward chrome://flags manipulation
  2338. constraints = {
  2339. video: {
  2340. mandatory: {
  2341. googLeakyBucket: true,
  2342. maxWidth: window.screen.width,
  2343. maxHeight: window.screen.height,
  2344. minFrameRate: media.screenshareFrameRate,
  2345. maxFrameRate: media.screenshareFrameRate,
  2346. chromeMediaSource: 'screen'
  2347. }
  2348. },
  2349. audio: isAudioSendEnabled(media) && !media.keepAudio
  2350. };
  2351. getScreenMedia(constraints, callbackUserMedia);
  2352. } else {
  2353. // Chrome 34+ requires an extension
  2354. Janus.extension.getScreen(function (error, sourceId) {
  2355. if (error) {
  2356. pluginHandle.consentDialog(false);
  2357. return callbacks.error(error);
  2358. }
  2359. constraints = {
  2360. audio: false,
  2361. video: {
  2362. mandatory: {
  2363. chromeMediaSource: 'desktop',
  2364. maxWidth: window.screen.width,
  2365. maxHeight: window.screen.height,
  2366. minFrameRate: media.screenshareFrameRate,
  2367. maxFrameRate: media.screenshareFrameRate,
  2368. },
  2369. optional: [
  2370. {googLeakyBucket: true},
  2371. {googTemporalLayeredScreencast: true}
  2372. ]
  2373. }
  2374. };
  2375. constraints.video.mandatory.chromeMediaSourceId = sourceId;
  2376. getScreenMedia(constraints, callbackUserMedia,
  2377. isAudioSendEnabled(media) && !media.keepAudio);
  2378. });
  2379. }
  2380. } else if (Janus.webRTCAdapter.browserDetails.browser === 'firefox') {
  2381. if (Janus.webRTCAdapter.browserDetails.version >= 33) {
  2382. // Firefox 33+ has experimental support for screen sharing
  2383. constraints = {
  2384. video: {
  2385. mozMediaSource: media.video,
  2386. mediaSource: media.video
  2387. },
  2388. audio: isAudioSendEnabled(media) && !media.keepAudio
  2389. };
  2390. getScreenMedia(constraints, function (err, stream) {
  2391. callbackUserMedia(err, stream);
  2392. // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810
  2393. if (!err) {
  2394. var lastTime = stream.currentTime;
  2395. var polly = window.setInterval(function () {
  2396. if (!stream)
  2397. window.clearInterval(polly);
  2398. if (stream.currentTime == lastTime) {
  2399. window.clearInterval(polly);
  2400. if (stream.onended) {
  2401. stream.onended();
  2402. }
  2403. }
  2404. lastTime = stream.currentTime;
  2405. }, 500);
  2406. }
  2407. });
  2408. } else {
  2409. var error = new Error('NavigatorUserMediaError');
  2410. error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)';
  2411. pluginHandle.consentDialog(false);
  2412. callbacks.error(error);
  2413. return;
  2414. }
  2415. }
  2416. return;
  2417. }
  2418. }
  2419. // If we got here, we're not screensharing
  2420. if (media === null || media === undefined || media.video !== 'screen') {
  2421. // Check whether all media sources are actually available or not
  2422. navigator.mediaDevices.enumerateDevices().then(function (devices) {
  2423. var audioExist = devices.some(function (device) {
  2424. return device.kind === 'audioinput';
  2425. }),
  2426. videoExist = isScreenSendEnabled(media) || devices.some(function (device) {
  2427. return device.kind === 'videoinput';
  2428. });
  2429. // Check whether a missing device is really a problem
  2430. var audioSend = isAudioSendEnabled(media);
  2431. var videoSend = isVideoSendEnabled(media);
  2432. var needAudioDevice = isAudioSendRequired(media);
  2433. var needVideoDevice = isVideoSendRequired(media);
  2434. if (audioSend || videoSend || needAudioDevice || needVideoDevice) {
  2435. // We need to send either audio or video
  2436. var haveAudioDevice = audioSend ? audioExist : false;
  2437. var haveVideoDevice = videoSend ? videoExist : false;
  2438. if (!haveAudioDevice && !haveVideoDevice) {
  2439. // FIXME Should we really give up, or just assume recvonly for both?
  2440. pluginHandle.consentDialog(false);
  2441. callbacks.error('No capture device found');
  2442. return false;
  2443. } else if (!haveAudioDevice && needAudioDevice) {
  2444. pluginHandle.consentDialog(false);
  2445. callbacks.error('Audio capture is required, but no capture device found');
  2446. return false;
  2447. } else if (!haveVideoDevice && needVideoDevice) {
  2448. pluginHandle.consentDialog(false);
  2449. callbacks.error('Video capture is required, but no capture device found');
  2450. return false;
  2451. }
  2452. }
  2453. var gumConstraints = {
  2454. audio: (audioExist && !media.keepAudio) ? audioSupport : false,
  2455. video: (videoExist && !media.keepVideo) ? videoSupport : false
  2456. };
  2457. Janus.debug("getUserMedia constraints", gumConstraints);
  2458. if (!gumConstraints.audio && !gumConstraints.video) {
  2459. pluginHandle.consentDialog(false);
  2460. streamsDone(handleId, jsep, media, callbacks, stream);
  2461. } else {
  2462. navigator.mediaDevices.getUserMedia(gumConstraints)
  2463. .then(function (stream) {
  2464. pluginHandle.consentDialog(false);
  2465. streamsDone(handleId, jsep, media, callbacks, stream);
  2466. }).catch(function (error) {
  2467. pluginHandle.consentDialog(false);
  2468. callbacks.error({code: error.code, name: error.name, message: error.message});
  2469. });
  2470. }
  2471. })
  2472. .catch(function (error) {
  2473. pluginHandle.consentDialog(false);
  2474. callbacks.error('enumerateDevices error', error);
  2475. });
  2476. }
  2477. } else {
  2478. // No need to do a getUserMedia, create offer/answer right away
  2479. streamsDone(handleId, jsep, media, callbacks);
  2480. }
  2481. }
  2482. function prepareWebrtcPeer(handleId, callbacks) {
  2483. callbacks = callbacks || {};
  2484. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  2485. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError;
  2486. var jsep = callbacks.jsep;
  2487. var pluginHandle = pluginHandles[handleId];
  2488. if (pluginHandle === null || pluginHandle === undefined ||
  2489. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  2490. Janus.warn("Invalid handle");
  2491. callbacks.error("Invalid handle");
  2492. return;
  2493. }
  2494. var config = pluginHandle.webrtcStuff;
  2495. if (jsep !== undefined && jsep !== null) {
  2496. if (config.pc === null) {
  2497. Janus.warn("Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep");
  2498. callbacks.error("No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep");
  2499. return;
  2500. }
  2501. config.pc.setRemoteDescription(jsep)
  2502. .then(function () {
  2503. Janus.log("Remote description accepted!");
  2504. config.remoteSdp = jsep.sdp;
  2505. // Any trickle candidate we cached?
  2506. if (config.candidates && config.candidates.length > 0) {
  2507. for (var i = 0; i < config.candidates.length; i++) {
  2508. var candidate = config.candidates[i];
  2509. Janus.debug("Adding remote candidate:", candidate);
  2510. if (!candidate || candidate.completed === true) {
  2511. // end-of-candidates
  2512. config.pc.addIceCandidate(Janus.endOfCandidates);
  2513. } else {
  2514. // New candidate
  2515. config.pc.addIceCandidate(candidate);
  2516. }
  2517. }
  2518. config.candidates = [];
  2519. }
  2520. // Done
  2521. callbacks.success();
  2522. }, callbacks.error);
  2523. } else {
  2524. callbacks.error("Invalid JSEP");
  2525. }
  2526. }
  2527. function createOffer(handleId, media, callbacks) {
  2528. callbacks = callbacks || {};
  2529. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  2530. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  2531. callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop;
  2532. var pluginHandle = pluginHandles[handleId];
  2533. if (pluginHandle === null || pluginHandle === undefined ||
  2534. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  2535. Janus.warn("Invalid handle");
  2536. callbacks.error("Invalid handle");
  2537. return;
  2538. }
  2539. var config = pluginHandle.webrtcStuff;
  2540. var simulcast = callbacks.simulcast === true ? true : false;
  2541. if (!simulcast) {
  2542. Janus.log("Creating offer (iceDone=" + config.iceDone + ")");
  2543. } else {
  2544. Janus.log("Creating offer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")");
  2545. }
  2546. // https://code.google.com/p/webrtc/issues/detail?id=3508
  2547. var mediaConstraints = {};
  2548. if (Janus.unifiedPlan) {
  2549. // We can use Transceivers
  2550. var audioTransceiver = null, videoTransceiver = null;
  2551. var transceivers = config.pc.getTransceivers();
  2552. if (transceivers && transceivers.length > 0) {
  2553. for (var i in transceivers) {
  2554. var t = transceivers[i];
  2555. if ((t.sender && t.sender.track && t.sender.track.kind === "audio") ||
  2556. (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) {
  2557. if (!audioTransceiver)
  2558. audioTransceiver = t;
  2559. continue;
  2560. }
  2561. if ((t.sender && t.sender.track && t.sender.track.kind === "video") ||
  2562. (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) {
  2563. if (!videoTransceiver)
  2564. videoTransceiver = t;
  2565. continue;
  2566. }
  2567. }
  2568. }
  2569. // Handle audio (and related changes, if any)
  2570. var audioSend = isAudioSendEnabled(media);
  2571. var audioRecv = isAudioRecvEnabled(media);
  2572. if (!audioSend && !audioRecv) {
  2573. // Audio disabled: have we removed it?
  2574. if (media.removeAudio && audioTransceiver) {
  2575. if (audioTransceiver.setDirection) {
  2576. audioTransceiver.setDirection("inactive");
  2577. } else {
  2578. audioTransceiver.direction = "inactive";
  2579. }
  2580. Janus.log("Setting audio transceiver to inactive:", audioTransceiver);
  2581. }
  2582. } else {
  2583. // Take care of audio m-line
  2584. if (audioSend && audioRecv) {
  2585. if (audioTransceiver) {
  2586. if (audioTransceiver.setDirection) {
  2587. audioTransceiver.setDirection("sendrecv");
  2588. } else {
  2589. audioTransceiver.direction = "sendrecv";
  2590. }
  2591. Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver);
  2592. }
  2593. } else if (audioSend && !audioRecv) {
  2594. if (audioTransceiver) {
  2595. if (audioTransceiver.setDirection) {
  2596. audioTransceiver.setDirection("sendonly");
  2597. } else {
  2598. audioTransceiver.direction = "sendonly";
  2599. }
  2600. Janus.log("Setting audio transceiver to sendonly:", audioTransceiver);
  2601. }
  2602. } else if (!audioSend && audioRecv) {
  2603. if (audioTransceiver) {
  2604. if (audioTransceiver.setDirection) {
  2605. audioTransceiver.setDirection("recvonly");
  2606. } else {
  2607. audioTransceiver.direction = "recvonly";
  2608. }
  2609. Janus.log("Setting audio transceiver to recvonly:", audioTransceiver);
  2610. } else {
  2611. // In theory, this is the only case where we might not have a transceiver yet
  2612. audioTransceiver = config.pc.addTransceiver("audio", {direction: "recvonly"});
  2613. Janus.log("Adding recvonly audio transceiver:", audioTransceiver);
  2614. }
  2615. }
  2616. }
  2617. // Handle video (and related changes, if any)
  2618. var videoSend = isVideoSendEnabled(media);
  2619. var videoRecv = isVideoRecvEnabled(media);
  2620. if (!videoSend && !videoRecv) {
  2621. // Video disabled: have we removed it?
  2622. if (media.removeVideo && videoTransceiver) {
  2623. if (videoTransceiver.setDirection) {
  2624. videoTransceiver.setDirection("inactive");
  2625. } else {
  2626. videoTransceiver.direction = "inactive";
  2627. }
  2628. Janus.log("Setting video transceiver to inactive:", videoTransceiver);
  2629. }
  2630. } else {
  2631. // Take care of video m-line
  2632. if (videoSend && videoRecv) {
  2633. if (videoTransceiver) {
  2634. if (videoTransceiver.setDirection) {
  2635. videoTransceiver.setDirection("sendrecv");
  2636. } else {
  2637. videoTransceiver.direction = "sendrecv";
  2638. }
  2639. Janus.log("Setting video transceiver to sendrecv:", videoTransceiver);
  2640. }
  2641. } else if (videoSend && !videoRecv) {
  2642. if (videoTransceiver) {
  2643. if (videoTransceiver.setDirection) {
  2644. videoTransceiver.setDirection("sendonly");
  2645. } else {
  2646. videoTransceiver.direction = "sendonly";
  2647. }
  2648. Janus.log("Setting video transceiver to sendonly:", videoTransceiver);
  2649. }
  2650. } else if (!videoSend && videoRecv) {
  2651. if (videoTransceiver) {
  2652. if (videoTransceiver.setDirection) {
  2653. videoTransceiver.setDirection("recvonly");
  2654. } else {
  2655. videoTransceiver.direction = "recvonly";
  2656. }
  2657. Janus.log("Setting video transceiver to recvonly:", videoTransceiver);
  2658. } else {
  2659. // In theory, this is the only case where we might not have a transceiver yet
  2660. videoTransceiver = config.pc.addTransceiver("video", {direction: "recvonly"});
  2661. Janus.log("Adding recvonly video transceiver:", videoTransceiver);
  2662. }
  2663. }
  2664. }
  2665. } else {
  2666. mediaConstraints["offerToReceiveAudio"] = isAudioRecvEnabled(media);
  2667. mediaConstraints["offerToReceiveVideo"] = isVideoRecvEnabled(media);
  2668. }
  2669. var iceRestart = callbacks.iceRestart === true ? true : false;
  2670. if (iceRestart) {
  2671. mediaConstraints["iceRestart"] = true;
  2672. }
  2673. Janus.debug(mediaConstraints);
  2674. // Check if this is Firefox and we've been asked to do simulcasting
  2675. var sendVideo = isVideoSendEnabled(media);
  2676. if (sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
  2677. // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
  2678. Janus.log("Enabling Simulcasting for Firefox (RID)");
  2679. var sender = config.pc.getSenders().find(function (s) {
  2680. return s.track.kind == "video"
  2681. });
  2682. if (sender) {
  2683. var parameters = sender.getParameters();
  2684. if (!parameters)
  2685. parameters = {};
  2686. const maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates);
  2687. parameters.encodings = [
  2688. {rid: "h", active: true, maxBitrate: maxBitrates.high},
  2689. {rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2},
  2690. {rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4}
  2691. ];
  2692. sender.setParameters(parameters);
  2693. }
  2694. }
  2695. config.pc.createOffer(mediaConstraints)
  2696. .then(function (offer) {
  2697. Janus.debug(offer);
  2698. // JSON.stringify doesn't work on some WebRTC objects anymore
  2699. // See https://code.google.com/p/chromium/issues/detail?id=467366
  2700. var jsep = {
  2701. "type": offer.type,
  2702. "sdp": offer.sdp
  2703. };
  2704. callbacks.customizeSdp(jsep);
  2705. offer.sdp = jsep.sdp;
  2706. Janus.log("Setting local description");
  2707. if (sendVideo && simulcast) {
  2708. // This SDP munging only works with Chrome (Safari STP may support it too)
  2709. if (Janus.webRTCAdapter.browserDetails.browser === "chrome" ||
  2710. Janus.webRTCAdapter.browserDetails.browser === "safari") {
  2711. Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
  2712. offer.sdp = mungeSdpForSimulcasting(offer.sdp);
  2713. } else if (Janus.webRTCAdapter.browserDetails.browser !== "firefox") {
  2714. Janus.warn("simulcast=true, but this is not Chrome nor Firefox, ignoring");
  2715. }
  2716. }
  2717. config.mySdp = offer.sdp;
  2718. config.pc.setLocalDescription(offer)
  2719. .catch(callbacks.error);
  2720. config.mediaConstraints = mediaConstraints;
  2721. if (!config.iceDone && !config.trickle) {
  2722. // Don't do anything until we have all candidates
  2723. Janus.log("Waiting for all candidates...");
  2724. return;
  2725. }
  2726. Janus.log("Offer ready");
  2727. Janus.debug(callbacks);
  2728. callbacks.success(offer);
  2729. }, callbacks.error);
  2730. }
  2731. function createAnswer(handleId, media, callbacks) {
  2732. callbacks = callbacks || {};
  2733. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  2734. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  2735. callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop;
  2736. var pluginHandle = pluginHandles[handleId];
  2737. if (pluginHandle === null || pluginHandle === undefined ||
  2738. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  2739. Janus.warn("Invalid handle");
  2740. callbacks.error("Invalid handle");
  2741. return;
  2742. }
  2743. var config = pluginHandle.webrtcStuff;
  2744. var simulcast = callbacks.simulcast === true ? true : false;
  2745. if (!simulcast) {
  2746. Janus.log("Creating answer (iceDone=" + config.iceDone + ")");
  2747. } else {
  2748. Janus.log("Creating answer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")");
  2749. }
  2750. var mediaConstraints = null;
  2751. if (Janus.unifiedPlan) {
  2752. // We can use Transceivers
  2753. mediaConstraints = {};
  2754. var audioTransceiver = null, videoTransceiver = null;
  2755. var transceivers = config.pc.getTransceivers();
  2756. if (transceivers && transceivers.length > 0) {
  2757. for (var i in transceivers) {
  2758. var t = transceivers[i];
  2759. if ((t.sender && t.sender.track && t.sender.track.kind === "audio") ||
  2760. (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) {
  2761. if (!audioTransceiver)
  2762. audioTransceiver = t;
  2763. continue;
  2764. }
  2765. if ((t.sender && t.sender.track && t.sender.track.kind === "video") ||
  2766. (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) {
  2767. if (!videoTransceiver)
  2768. videoTransceiver = t;
  2769. continue;
  2770. }
  2771. }
  2772. }
  2773. // Handle audio (and related changes, if any)
  2774. var audioSend = isAudioSendEnabled(media);
  2775. var audioRecv = isAudioRecvEnabled(media);
  2776. if (!audioSend && !audioRecv) {
  2777. // Audio disabled: have we removed it?
  2778. if (media.removeAudio && audioTransceiver) {
  2779. try {
  2780. if (audioTransceiver.setDirection) {
  2781. audioTransceiver.setDirection("inactive");
  2782. } else {
  2783. audioTransceiver.direction = "inactive";
  2784. }
  2785. Janus.log("Setting audio transceiver to inactive:", audioTransceiver);
  2786. } catch (e) {
  2787. Janus.error(e);
  2788. }
  2789. }
  2790. } else {
  2791. // Take care of audio m-line
  2792. if (audioSend && audioRecv) {
  2793. if (audioTransceiver) {
  2794. try {
  2795. if (audioTransceiver.setDirection) {
  2796. audioTransceiver.setDirection("sendrecv");
  2797. } else {
  2798. audioTransceiver.direction = "sendrecv";
  2799. }
  2800. Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver);
  2801. } catch (e) {
  2802. Janus.error(e);
  2803. }
  2804. }
  2805. } else if (audioSend && !audioRecv) {
  2806. try {
  2807. if (audioTransceiver) {
  2808. if (audioTransceiver.setDirection) {
  2809. audioTransceiver.setDirection("sendonly");
  2810. } else {
  2811. audioTransceiver.direction = "sendonly";
  2812. }
  2813. Janus.log("Setting audio transceiver to sendonly:", audioTransceiver);
  2814. }
  2815. } catch (e) {
  2816. Janus.error(e);
  2817. }
  2818. } else if (!audioSend && audioRecv) {
  2819. if (audioTransceiver) {
  2820. try {
  2821. if (audioTransceiver.setDirection) {
  2822. audioTransceiver.setDirection("recvonly");
  2823. } else {
  2824. audioTransceiver.direction = "recvonly";
  2825. }
  2826. Janus.log("Setting audio transceiver to recvonly:", audioTransceiver);
  2827. } catch (e) {
  2828. Janus.error(e);
  2829. }
  2830. } else {
  2831. // In theory, this is the only case where we might not have a transceiver yet
  2832. audioTransceiver = config.pc.addTransceiver("audio", {direction: "recvonly"});
  2833. Janus.log("Adding recvonly audio transceiver:", audioTransceiver);
  2834. }
  2835. }
  2836. }
  2837. // Handle video (and related changes, if any)
  2838. var videoSend = isVideoSendEnabled(media);
  2839. var videoRecv = isVideoRecvEnabled(media);
  2840. if (!videoSend && !videoRecv) {
  2841. // Video disabled: have we removed it?
  2842. if (media.removeVideo && videoTransceiver) {
  2843. try {
  2844. if (videoTransceiver.setDirection) {
  2845. videoTransceiver.setDirection("inactive");
  2846. } else {
  2847. videoTransceiver.direction = "inactive";
  2848. }
  2849. Janus.log("Setting video transceiver to inactive:", videoTransceiver);
  2850. } catch (e) {
  2851. Janus.error(e);
  2852. }
  2853. }
  2854. } else {
  2855. // Take care of video m-line
  2856. if (videoSend && videoRecv) {
  2857. if (videoTransceiver) {
  2858. try {
  2859. if (videoTransceiver.setDirection) {
  2860. videoTransceiver.setDirection("sendrecv");
  2861. } else {
  2862. videoTransceiver.direction = "sendrecv";
  2863. }
  2864. Janus.log("Setting video transceiver to sendrecv:", videoTransceiver);
  2865. } catch (e) {
  2866. Janus.error(e);
  2867. }
  2868. }
  2869. } else if (videoSend && !videoRecv) {
  2870. if (videoTransceiver) {
  2871. try {
  2872. if (videoTransceiver.setDirection) {
  2873. videoTransceiver.setDirection("sendonly");
  2874. } else {
  2875. videoTransceiver.direction = "sendonly";
  2876. }
  2877. Janus.log("Setting video transceiver to sendonly:", videoTransceiver);
  2878. } catch (e) {
  2879. Janus.error(e);
  2880. }
  2881. }
  2882. } else if (!videoSend && videoRecv) {
  2883. if (videoTransceiver) {
  2884. try {
  2885. if (videoTransceiver.setDirection) {
  2886. videoTransceiver.setDirection("recvonly");
  2887. } else {
  2888. videoTransceiver.direction = "recvonly";
  2889. }
  2890. Janus.log("Setting video transceiver to recvonly:", videoTransceiver);
  2891. } catch (e) {
  2892. Janus.error(e);
  2893. }
  2894. } else {
  2895. // In theory, this is the only case where we might not have a transceiver yet
  2896. videoTransceiver = config.pc.addTransceiver("video", {direction: "recvonly"});
  2897. Janus.log("Adding recvonly video transceiver:", videoTransceiver);
  2898. }
  2899. }
  2900. }
  2901. } else {
  2902. if (Janus.webRTCAdapter.browserDetails.browser == "firefox" || Janus.webRTCAdapter.browserDetails.browser == "edge") {
  2903. mediaConstraints = {
  2904. offerToReceiveAudio: isAudioRecvEnabled(media),
  2905. offerToReceiveVideo: isVideoRecvEnabled(media)
  2906. };
  2907. } else {
  2908. mediaConstraints = {
  2909. mandatory: {
  2910. OfferToReceiveAudio: isAudioRecvEnabled(media),
  2911. OfferToReceiveVideo: isVideoRecvEnabled(media)
  2912. }
  2913. };
  2914. }
  2915. }
  2916. Janus.debug(mediaConstraints);
  2917. // Check if this is Firefox and we've been asked to do simulcasting
  2918. var sendVideo = isVideoSendEnabled(media);
  2919. if (sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
  2920. // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
  2921. Janus.log("Enabling Simulcasting for Firefox (RID)");
  2922. var sender = config.pc.getSenders()[1];
  2923. Janus.log(sender);
  2924. var parameters = sender.getParameters();
  2925. Janus.log(parameters);
  2926. const maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates);
  2927. sender.setParameters({
  2928. encodings: [
  2929. {rid: "high", active: true, priority: "high", maxBitrate: maxBitrates.high},
  2930. {rid: "medium", active: true, priority: "medium", maxBitrate: maxBitrates.medium},
  2931. {rid: "low", active: true, priority: "low", maxBitrate: maxBitrates.low}
  2932. ]
  2933. });
  2934. }
  2935. config.pc.createAnswer(mediaConstraints)
  2936. .then(function (answer) {
  2937. Janus.debug(answer);
  2938. // JSON.stringify doesn't work on some WebRTC objects anymore
  2939. // See https://code.google.com/p/chromium/issues/detail?id=467366
  2940. var jsep = {
  2941. "type": answer.type,
  2942. "sdp": answer.sdp
  2943. };
  2944. callbacks.customizeSdp(jsep);
  2945. answer.sdp = jsep.sdp;
  2946. Janus.log("Setting local description");
  2947. if (sendVideo && simulcast) {
  2948. // This SDP munging only works with Chrome
  2949. if (Janus.webRTCAdapter.browserDetails.browser === "chrome") {
  2950. // FIXME Apparently trying to simulcast when answering breaks video in Chrome...
  2951. //~ Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
  2952. //~ answer.sdp = mungeSdpForSimulcasting(answer.sdp);
  2953. Janus.warn("simulcast=true, but this is an answer, and video breaks in Chrome if we enable it");
  2954. } else if (Janus.webRTCAdapter.browserDetails.browser !== "firefox") {
  2955. Janus.warn("simulcast=true, but this is not Chrome nor Firefox, ignoring");
  2956. }
  2957. }
  2958. config.mySdp = answer.sdp;
  2959. config.pc.setLocalDescription(answer)
  2960. .catch(callbacks.error);
  2961. config.mediaConstraints = mediaConstraints;
  2962. if (!config.iceDone && !config.trickle) {
  2963. // Don't do anything until we have all candidates
  2964. Janus.log("Waiting for all candidates...");
  2965. return;
  2966. }
  2967. callbacks.success(answer);
  2968. }, callbacks.error);
  2969. }
  2970. function sendSDP(handleId, callbacks) {
  2971. callbacks = callbacks || {};
  2972. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
  2973. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
  2974. var pluginHandle = pluginHandles[handleId];
  2975. if (pluginHandle === null || pluginHandle === undefined ||
  2976. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  2977. Janus.warn("Invalid handle, not sending anything");
  2978. return;
  2979. }
  2980. var config = pluginHandle.webrtcStuff;
  2981. Janus.log("Sending offer/answer SDP...");
  2982. if (config.mySdp === null || config.mySdp === undefined) {
  2983. Janus.warn("Local SDP instance is invalid, not sending anything...");
  2984. return;
  2985. }
  2986. config.mySdp = {
  2987. "type": config.pc.localDescription.type,
  2988. "sdp": config.pc.localDescription.sdp
  2989. };
  2990. if (config.trickle === false)
  2991. config.mySdp["trickle"] = false;
  2992. Janus.debug(callbacks);
  2993. config.sdpSent = true;
  2994. callbacks.success(config.mySdp);
  2995. }
  2996. function getVolume(handleId, remote) {
  2997. var pluginHandle = pluginHandles[handleId];
  2998. if (pluginHandle === null || pluginHandle === undefined ||
  2999. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  3000. Janus.warn("Invalid handle");
  3001. return 0;
  3002. }
  3003. var stream = remote ? "remote" : "local";
  3004. var config = pluginHandle.webrtcStuff;
  3005. if (!config.volume[stream])
  3006. config.volume[stream] = {value: 0};
  3007. // Start getting the volume, if getStats is supported
  3008. if (config.pc.getStats && Janus.webRTCAdapter.browserDetails.browser === "chrome") {
  3009. if (remote && (config.remoteStream === null || config.remoteStream === undefined)) {
  3010. Janus.warn("Remote stream unavailable");
  3011. return 0;
  3012. } else if (!remote && (config.myStream === null || config.myStream === undefined)) {
  3013. Janus.warn("Local stream unavailable");
  3014. return 0;
  3015. }
  3016. if (config.volume[stream].timer === null || config.volume[stream].timer === undefined) {
  3017. Janus.log("Starting " + stream + " volume monitor");
  3018. config.volume[stream].timer = setInterval(function () {
  3019. config.pc.getStats(function (stats) {
  3020. var results = stats.result();
  3021. for (var i = 0; i < results.length; i++) {
  3022. var res = results[i];
  3023. if (res.type == 'ssrc') {
  3024. if (remote && res.stat('audioOutputLevel'))
  3025. config.volume[stream].value = parseInt(res.stat('audioOutputLevel'));
  3026. else if (!remote && res.stat('audioInputLevel'))
  3027. config.volume[stream].value = parseInt(res.stat('audioInputLevel'));
  3028. }
  3029. }
  3030. });
  3031. }, 200);
  3032. return 0; // We don't have a volume to return yet
  3033. }
  3034. return config.volume[stream].value;
  3035. } else {
  3036. // audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel
  3037. // seems to be available on Chrome and Firefox, but they don't seem to work
  3038. Janus.warn("Getting the " + stream + " volume unsupported by browser");
  3039. return 0;
  3040. }
  3041. }
  3042. function isMuted(handleId, video) {
  3043. var pluginHandle = pluginHandles[handleId];
  3044. if (pluginHandle === null || pluginHandle === undefined ||
  3045. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  3046. Janus.warn("Invalid handle");
  3047. return true;
  3048. }
  3049. var config = pluginHandle.webrtcStuff;
  3050. if (config.pc === null || config.pc === undefined) {
  3051. Janus.warn("Invalid PeerConnection");
  3052. return true;
  3053. }
  3054. if (config.myStream === undefined || config.myStream === null) {
  3055. Janus.warn("Invalid local MediaStream");
  3056. return true;
  3057. }
  3058. if (video) {
  3059. // Check video track
  3060. if (config.myStream.getVideoTracks() === null
  3061. || config.myStream.getVideoTracks() === undefined
  3062. || config.myStream.getVideoTracks().length === 0) {
  3063. Janus.warn("No video track");
  3064. return true;
  3065. }
  3066. return !config.myStream.getVideoTracks()[0].enabled;
  3067. } else {
  3068. // Check audio track
  3069. if (config.myStream.getAudioTracks() === null
  3070. || config.myStream.getAudioTracks() === undefined
  3071. || config.myStream.getAudioTracks().length === 0) {
  3072. Janus.warn("No audio track");
  3073. return true;
  3074. }
  3075. return !config.myStream.getAudioTracks()[0].enabled;
  3076. }
  3077. }
  3078. function mute(handleId, video, mute) {
  3079. var pluginHandle = pluginHandles[handleId];
  3080. if (pluginHandle === null || pluginHandle === undefined ||
  3081. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  3082. Janus.warn("Invalid handle");
  3083. return false;
  3084. }
  3085. var config = pluginHandle.webrtcStuff;
  3086. if (config.pc === null || config.pc === undefined) {
  3087. Janus.warn("Invalid PeerConnection");
  3088. return false;
  3089. }
  3090. if (config.myStream === undefined || config.myStream === null) {
  3091. Janus.warn("Invalid local MediaStream");
  3092. return false;
  3093. }
  3094. if (video) {
  3095. // Mute/unmute video track
  3096. if (config.myStream.getVideoTracks() === null
  3097. || config.myStream.getVideoTracks() === undefined
  3098. || config.myStream.getVideoTracks().length === 0) {
  3099. Janus.warn("No video track");
  3100. return false;
  3101. }
  3102. config.myStream.getVideoTracks()[0].enabled = mute ? false : true;
  3103. return true;
  3104. } else {
  3105. // Mute/unmute audio track
  3106. if (config.myStream.getAudioTracks() === null
  3107. || config.myStream.getAudioTracks() === undefined
  3108. || config.myStream.getAudioTracks().length === 0) {
  3109. Janus.warn("No audio track");
  3110. return false;
  3111. }
  3112. config.myStream.getAudioTracks()[0].enabled = mute ? false : true;
  3113. return true;
  3114. }
  3115. }
  3116. function getBitrate(handleId) {
  3117. var pluginHandle = pluginHandles[handleId];
  3118. if (pluginHandle === null || pluginHandle === undefined ||
  3119. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  3120. Janus.warn("Invalid handle");
  3121. return "Invalid handle";
  3122. }
  3123. var config = pluginHandle.webrtcStuff;
  3124. if (config.pc === null || config.pc === undefined)
  3125. return "Invalid PeerConnection";
  3126. // Start getting the bitrate, if getStats is supported
  3127. if (config.pc.getStats) {
  3128. if (config.bitrate.timer === null || config.bitrate.timer === undefined) {
  3129. Janus.log("Starting bitrate timer (via getStats)");
  3130. config.bitrate.timer = setInterval(function () {
  3131. config.pc.getStats()
  3132. .then(function (stats) {
  3133. stats.forEach(function (res) {
  3134. if (!res)
  3135. return;
  3136. var inStats = false;
  3137. // Check if these are statistics on incoming media
  3138. if ((res.mediaType === "video" || res.id.toLowerCase().indexOf("video") > -1) &&
  3139. res.type === "inbound-rtp" && res.id.indexOf("rtcp") < 0) {
  3140. // New stats
  3141. inStats = true;
  3142. } else if (res.type == 'ssrc' && res.bytesReceived &&
  3143. (res.googCodecName === "VP8" || res.googCodecName === "")) {
  3144. // Older Chromer versions
  3145. inStats = true;
  3146. }
  3147. // Parse stats now
  3148. if (inStats) {
  3149. config.bitrate.bsnow = res.bytesReceived;
  3150. config.bitrate.tsnow = res.timestamp;
  3151. if (config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) {
  3152. // Skip this round
  3153. config.bitrate.bsbefore = config.bitrate.bsnow;
  3154. config.bitrate.tsbefore = config.bitrate.tsnow;
  3155. } else {
  3156. // Calculate bitrate
  3157. var timePassed = config.bitrate.tsnow - config.bitrate.tsbefore;
  3158. if (Janus.webRTCAdapter.browserDetails.browser == "safari")
  3159. timePassed = timePassed / 1000; // Apparently the timestamp is in microseconds, in Safari
  3160. var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / timePassed);
  3161. if (Janus.webRTCAdapter.browserDetails.browser === 'safari')
  3162. bitRate = parseInt(bitRate / 1000);
  3163. config.bitrate.value = bitRate + ' kbits/sec';
  3164. //~ Janus.log("Estimated bitrate is " + config.bitrate.value);
  3165. config.bitrate.bsbefore = config.bitrate.bsnow;
  3166. config.bitrate.tsbefore = config.bitrate.tsnow;
  3167. }
  3168. }
  3169. });
  3170. });
  3171. }, 1000);
  3172. return "0 kbits/sec"; // We don't have a bitrate value yet
  3173. }
  3174. return config.bitrate.value;
  3175. } else {
  3176. Janus.warn("Getting the video bitrate unsupported by browser");
  3177. return "Feature unsupported by browser";
  3178. }
  3179. }
  3180. function webrtcError(error) {
  3181. Janus.error("WebRTC error:", error);
  3182. }
  3183. function cleanupWebrtc(handleId, hangupRequest) {
  3184. Janus.log("Cleaning WebRTC stuff");
  3185. var pluginHandle = pluginHandles[handleId];
  3186. if (pluginHandle === null || pluginHandle === undefined) {
  3187. // Nothing to clean
  3188. return;
  3189. }
  3190. var config = pluginHandle.webrtcStuff;
  3191. if (config !== null && config !== undefined) {
  3192. if (hangupRequest === true) {
  3193. // Send a hangup request (we don't really care about the response)
  3194. var request = {"rtcgw": "hangup", "transaction": Janus.randomString(12)};
  3195. if (pluginHandle.token !== null && pluginHandle.token !== undefined)
  3196. request["token"] = pluginHandle.token;
  3197. if (apisecret !== null && apisecret !== undefined)
  3198. request["apisecret"] = apisecret;
  3199. Janus.debug("Sending hangup request (handle=" + handleId + "):");
  3200. Janus.debug(request);
  3201. if (websockets) {
  3202. request["session_id"] = sessionId;
  3203. request["handle_id"] = handleId;
  3204. ws.send(JSON.stringify(request));
  3205. } else {
  3206. Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
  3207. verb: 'POST',
  3208. withCredentials: withCredentials,
  3209. body: request
  3210. });
  3211. }
  3212. }
  3213. // Cleanup stack
  3214. config.remoteStream = null;
  3215. if (config.volume) {
  3216. if (config.volume["local"] && config.volume["local"].timer)
  3217. clearInterval(config.volume["local"].timer);
  3218. if (config.volume["remote"] && config.volume["remote"].timer)
  3219. clearInterval(config.volume["remote"].timer);
  3220. }
  3221. config.volume = {};
  3222. if (config.bitrate.timer)
  3223. clearInterval(config.bitrate.timer);
  3224. config.bitrate.timer = null;
  3225. config.bitrate.bsnow = null;
  3226. config.bitrate.bsbefore = null;
  3227. config.bitrate.tsnow = null;
  3228. config.bitrate.tsbefore = null;
  3229. config.bitrate.value = null;
  3230. try {
  3231. // Try a MediaStreamTrack.stop() for each track
  3232. if (!config.streamExternal && config.myStream !== null && config.myStream !== undefined) {
  3233. Janus.log("Stopping local stream tracks");
  3234. var tracks = config.myStream.getTracks();
  3235. for (var i in tracks) {
  3236. var mst = tracks[i];
  3237. Janus.log(mst);
  3238. if (mst !== null && mst !== undefined)
  3239. mst.stop();
  3240. }
  3241. }
  3242. } catch (e) {
  3243. // Do nothing if this fails
  3244. }
  3245. config.streamExternal = false;
  3246. config.myStream = null;
  3247. // Close PeerConnection
  3248. try {
  3249. config.pc.close();
  3250. } catch (e) {
  3251. // Do nothing
  3252. }
  3253. config.pc = null;
  3254. config.candidates = null;
  3255. config.mySdp = null;
  3256. config.remoteSdp = null;
  3257. config.iceDone = false;
  3258. config.dataChannel = {};
  3259. config.dtmfSender = null;
  3260. }
  3261. pluginHandle.oncleanup();
  3262. }
  3263. // Helper method to munge an SDP to enable simulcasting (Chrome only)
  3264. function mungeSdpForSimulcasting(sdp) {
  3265. // Let's munge the SDP to add the attributes for enabling simulcasting
  3266. // (based on https://gist.github.com/ggarber/a19b4c33510028b9c657)
  3267. var lines = sdp.split("\r\n");
  3268. var video = false;
  3269. var ssrc = [-1], ssrc_fid = [-1];
  3270. var cname = null, msid = null, mslabel = null, label = null;
  3271. var insertAt = -1;
  3272. for (var i = 0; i < lines.length; i++) {
  3273. var mline = lines[i].match(/m=(\w+) */);
  3274. if (mline) {
  3275. var medium = mline[1];
  3276. if (medium === "video") {
  3277. // New video m-line: make sure it's the first one
  3278. if (ssrc[0] < 0) {
  3279. video = true;
  3280. } else {
  3281. // We're done, let's add the new attributes here
  3282. insertAt = i;
  3283. break;
  3284. }
  3285. } else {
  3286. // New non-video m-line: do we have what we were looking for?
  3287. if (ssrc[0] > -1) {
  3288. // We're done, let's add the new attributes here
  3289. insertAt = i;
  3290. break;
  3291. }
  3292. }
  3293. continue;
  3294. }
  3295. if (!video)
  3296. continue;
  3297. var fid = lines[i].match(/a=ssrc-group:FID (\d+) (\d+)/);
  3298. if (fid) {
  3299. ssrc[0] = fid[1];
  3300. ssrc_fid[0] = fid[2];
  3301. lines.splice(i, 1);
  3302. i--;
  3303. continue;
  3304. }
  3305. if (ssrc[0]) {
  3306. var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
  3307. if (match) {
  3308. cname = match[1];
  3309. }
  3310. match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
  3311. if (match) {
  3312. msid = match[1];
  3313. }
  3314. match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
  3315. if (match) {
  3316. mslabel = match[1];
  3317. }
  3318. match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)')
  3319. if (match) {
  3320. label = match[1];
  3321. }
  3322. if (lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) {
  3323. lines.splice(i, 1);
  3324. i--;
  3325. continue;
  3326. }
  3327. if (lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
  3328. lines.splice(i, 1);
  3329. i--;
  3330. continue;
  3331. }
  3332. }
  3333. if (lines[i].length == 0) {
  3334. lines.splice(i, 1);
  3335. i--;
  3336. continue;
  3337. }
  3338. }
  3339. if (ssrc[0] < 0) {
  3340. // Couldn't find a FID attribute, let's just take the first video SSRC we find
  3341. insertAt = -1;
  3342. video = false;
  3343. for (var i = 0; i < lines.length; i++) {
  3344. var mline = lines[i].match(/m=(\w+) */);
  3345. if (mline) {
  3346. var medium = mline[1];
  3347. if (medium === "video") {
  3348. // New video m-line: make sure it's the first one
  3349. if (ssrc[0] < 0) {
  3350. video = true;
  3351. } else {
  3352. // We're done, let's add the new attributes here
  3353. insertAt = i;
  3354. break;
  3355. }
  3356. } else {
  3357. // New non-video m-line: do we have what we were looking for?
  3358. if (ssrc[0] > -1) {
  3359. // We're done, let's add the new attributes here
  3360. insertAt = i;
  3361. break;
  3362. }
  3363. }
  3364. continue;
  3365. }
  3366. if (!video)
  3367. continue;
  3368. if (ssrc[0] < 0) {
  3369. var value = lines[i].match(/a=ssrc:(\d+)/);
  3370. if (value) {
  3371. ssrc[0] = value[1];
  3372. lines.splice(i, 1);
  3373. i--;
  3374. continue;
  3375. }
  3376. } else {
  3377. var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
  3378. if (match) {
  3379. cname = match[1];
  3380. }
  3381. match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
  3382. if (match) {
  3383. msid = match[1];
  3384. }
  3385. match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
  3386. if (match) {
  3387. mslabel = match[1];
  3388. }
  3389. match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)')
  3390. if (match) {
  3391. label = match[1];
  3392. }
  3393. if (lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) {
  3394. lines.splice(i, 1);
  3395. i--;
  3396. continue;
  3397. }
  3398. if (lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
  3399. lines.splice(i, 1);
  3400. i--;
  3401. continue;
  3402. }
  3403. }
  3404. if (lines[i].length == 0) {
  3405. lines.splice(i, 1);
  3406. i--;
  3407. continue;
  3408. }
  3409. }
  3410. }
  3411. if (ssrc[0] < 0) {
  3412. // Still nothing, let's just return the SDP we were asked to munge
  3413. Janus.warn("Couldn't find the video SSRC, simulcasting NOT enabled");
  3414. return sdp;
  3415. }
  3416. if (insertAt < 0) {
  3417. // Append at the end
  3418. insertAt = lines.length;
  3419. }
  3420. // Generate a couple of SSRCs (for retransmissions too)
  3421. // Note: should we check if there are conflicts, here?
  3422. ssrc[1] = Math.floor(Math.random() * 0xFFFFFFFF);
  3423. ssrc[2] = Math.floor(Math.random() * 0xFFFFFFFF);
  3424. ssrc_fid[1] = Math.floor(Math.random() * 0xFFFFFFFF);
  3425. ssrc_fid[2] = Math.floor(Math.random() * 0xFFFFFFFF);
  3426. // Add attributes to the SDP
  3427. for (var i = 0; i < ssrc.length; i++) {
  3428. if (cname) {
  3429. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' cname:' + cname);
  3430. insertAt++;
  3431. }
  3432. if (msid) {
  3433. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' msid:' + msid);
  3434. insertAt++;
  3435. }
  3436. if (mslabel) {
  3437. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' mslabel:' + mslabel);
  3438. insertAt++;
  3439. }
  3440. if (label) {
  3441. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' label:' + label);
  3442. insertAt++;
  3443. }
  3444. // Add the same info for the retransmission SSRC
  3445. if (cname) {
  3446. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' cname:' + cname);
  3447. insertAt++;
  3448. }
  3449. if (msid) {
  3450. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' msid:' + msid);
  3451. insertAt++;
  3452. }
  3453. if (mslabel) {
  3454. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' mslabel:' + mslabel);
  3455. insertAt++;
  3456. }
  3457. if (label) {
  3458. lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' label:' + label);
  3459. insertAt++;
  3460. }
  3461. }
  3462. lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[2] + ' ' + ssrc_fid[2]);
  3463. lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[1] + ' ' + ssrc_fid[1]);
  3464. lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[0] + ' ' + ssrc_fid[0]);
  3465. lines.splice(insertAt, 0, 'a=ssrc-group:SIM ' + ssrc[0] + ' ' + ssrc[1] + ' ' + ssrc[2]);
  3466. sdp = lines.join("\r\n");
  3467. if (!sdp.endsWith("\r\n"))
  3468. sdp += "\r\n";
  3469. return sdp;
  3470. }
  3471. // Helper methods to parse a media object
  3472. function isAudioSendEnabled(media) {
  3473. Janus.debug("isAudioSendEnabled:", media);
  3474. if (media === undefined || media === null)
  3475. return true; // Default
  3476. if (media.audio === false)
  3477. return false; // Generic audio has precedence
  3478. if (media.audioSend === undefined || media.audioSend === null)
  3479. return true; // Default
  3480. return (media.audioSend === true);
  3481. }
  3482. function isAudioSendRequired(media) {
  3483. Janus.debug("isAudioSendRequired:", media);
  3484. if (media === undefined || media === null)
  3485. return false; // Default
  3486. if (media.audio === false || media.audioSend === false)
  3487. return false; // If we're not asking to capture audio, it's not required
  3488. if (media.failIfNoAudio === undefined || media.failIfNoAudio === null)
  3489. return false; // Default
  3490. return (media.failIfNoAudio === true);
  3491. }
  3492. function isAudioRecvEnabled(media) {
  3493. Janus.debug("isAudioRecvEnabled:", media);
  3494. if (media === undefined || media === null)
  3495. return true; // Default
  3496. if (media.audio === false)
  3497. return false; // Generic audio has precedence
  3498. if (media.audioRecv === undefined || media.audioRecv === null)
  3499. return true; // Default
  3500. return (media.audioRecv === true);
  3501. }
  3502. function isVideoSendEnabled(media) {
  3503. Janus.debug("isVideoSendEnabled:", media);
  3504. if (media === undefined || media === null)
  3505. return true; // Default
  3506. if (media.video === false)
  3507. return false; // Generic video has precedence
  3508. if (media.videoSend === undefined || media.videoSend === null)
  3509. return true; // Default
  3510. return (media.videoSend === true);
  3511. }
  3512. function isVideoSendRequired(media) {
  3513. Janus.debug("isVideoSendRequired:", media);
  3514. if (media === undefined || media === null)
  3515. return false; // Default
  3516. if (media.video === false || media.videoSend === false)
  3517. return false; // If we're not asking to capture video, it's not required
  3518. if (media.failIfNoVideo === undefined || media.failIfNoVideo === null)
  3519. return false; // Default
  3520. return (media.failIfNoVideo === true);
  3521. }
  3522. function isVideoRecvEnabled(media) {
  3523. Janus.debug("isVideoRecvEnabled:", media);
  3524. if (media === undefined || media === null)
  3525. return true; // Default
  3526. if (media.video === false)
  3527. return false; // Generic video has precedence
  3528. if (media.videoRecv === undefined || media.videoRecv === null)
  3529. return true; // Default
  3530. return (media.videoRecv === true);
  3531. }
  3532. function isScreenSendEnabled(media) {
  3533. Janus.debug("isScreenSendEnabled:", media);
  3534. if (media === undefined || media === null)
  3535. return false;
  3536. if (typeof media.video !== 'object' || typeof media.video.mandatory !== 'object')
  3537. return false;
  3538. var constraints = media.video.mandatory;
  3539. if (constraints.chromeMediaSource)
  3540. return constraints.chromeMediaSource === 'desktop' || constraints.chromeMediaSource === 'screen';
  3541. else if (constraints.mozMediaSource)
  3542. return constraints.mozMediaSource === 'window' || constraints.mozMediaSource === 'screen';
  3543. else if (constraints.mediaSource)
  3544. return constraints.mediaSource === 'window' || constraints.mediaSource === 'screen';
  3545. return false;
  3546. }
  3547. function isDataEnabled(media) {
  3548. Janus.debug("isDataEnabled:", media);
  3549. if (Janus.webRTCAdapter.browserDetails.browser == "edge") {
  3550. Janus.warn("Edge doesn't support data channels yet");
  3551. return false;
  3552. }
  3553. if (media === undefined || media === null)
  3554. return false; // Default
  3555. return (media.data === true);
  3556. }
  3557. function isTrickleEnabled(trickle) {
  3558. Janus.debug("isTrickleEnabled:", trickle);
  3559. if (trickle === undefined || trickle === null)
  3560. return true; // Default is true
  3561. return (trickle === true);
  3562. }
  3563. };