/* This file is part of Bolixo. Bolixo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Bolixo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Bolixo. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "bolixo.h" #include "proto/bod_client.protodef" #include "documentd.h" #include "documentd_menu.h" #include "bolixo.m" #include using namespace std; // Information on user connection. This is the connection used to send notifications struct WEBRTC_CON{ int fd; string connectid; time_t started=time(nullptr); WEBRTC_CON(int _fd, PARAM_STRING _connectid): fd(_fd), connectid(_connectid.ptr){} }; // This is a copy of old connections. When add_notification_fd is called, we can re-install // the WEBRTC_CON entry in webrtc_started. // This is needed when we restart documentd. The webrtc connections continue to exist // and this is the way the new documentd instance learn about the old connections. // This is useful because documentd is really on the side while a conference is running. // But if documentd is restarted (after an update) and loose the connections, some // actions in the conference will stop working. New users joining won't show to the already connected ones. struct WEBRTC_OLDCON{ string connectid; time_t started; WEBRTC_OLDCON(PARAM_STRING _connectid, time_t _started): connectid(_connectid.ptr){ started = _started; } }; class VIDCONF: public GAME{ std::string message; std::string define_styles(bool mobile); std::string define_functions(const DOC_CONTEXT &ctx, bool mobile, unsigned board_width, unsigned board_height); std::string draw_board (bool mobile, unsigned docnum, unsigned width, unsigned height, std::string &script); vector webrtc_started; // All connection having sent the "webrtcstart" command; vector old_webrtcs; // Old webrtc connections string showing; // Which user is currently shown struct { // Only one connection may request stats at a time // only used by bofs. At some point, it may be part // of the user interface to help debug issues. string connectid; // Connection ID having done a getstats int nbreq=0; // Number of getstats sent (number of connected users) } stats; int del_notification_fd(int fd); void add_notification_fd(int fd, const char *username, const char *connectid); int findpeer(PARAM_STRING connectid) const; public: VIDCONF(); const char *getclass() const{ return "VIDC"; } void save(DOC_WRITER &w, bool save_session_info); void load(DOC_READER &r, std::string &msg); void exec (const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, std::vector &res , std::vector &unotifies); }; GAME_P make_VIDCONF() { return make_shared(); } #include "proto/documentd_vidconf.protoh" #include "proto/documentd_vidconf.protoch" // Return the file handle associated with a connected peer int VIDCONF::findpeer(PARAM_STRING connectid) const { int ret = -1; for (auto &n:webrtc_started){ if (is_eq(n.connectid,connectid)){ ret = n.fd; break; } } return ret; } VIDCONF::VIDCONF() { resetgame(); } void VIDCONF::save(DOC_WRITER &w, bool save_session_info) { documentd_vidconf_header (&w,revision); vector schat; documentd_copychat (schat,chat); documentd_vidconf_chat(&w,schat); if (save_session_info){ for (auto &o:old_webrtcs) documentd_vidconf_oldcon(&w,o.connectid,o.started); for (auto &o:webrtc_started) documentd_vidconf_oldcon(&w,o.connectid,o.started); } } void VIDCONF::load(DOC_READER &r, std::string &msg) { glocal revision; glocal msg; glocal chat; glocal old_webrtcs; chat.clear(); (&r); glocal.revision = revision; for (auto l:lines) glocal.chat.emplace_back(l.time,l.line); glocal.old_webrtcs.emplace_back(connectid,started); glocal.msg = "Invalid format for vidconf file"; } int VIDCONF::del_notification_fd(int fd) { string deleted_id; for (auto p=webrtc_started.begin(); p != webrtc_started.end(); p++){ if (p->fd == fd){ old_webrtcs.emplace_back(p->connectid,p->started); if (old_webrtcs.size() > 100) old_webrtcs.erase(old_webrtcs.begin()); deleted_id = p->connectid; webrtc_started.erase(p); break; } } int ret = GAME::del_notification_fd(fd); if (deleted_id.size() > 0){ add_notification(string_f("VIDC_delcon('%s');\n",deleted_id.c_str())); } return ret; } void VIDCONF::add_notification_fd(int fd, const char *username, const char *connectid) { GAME::add_notification_fd(fd,username,connectid); for (auto &o:old_webrtcs){ if (is_eq(connectid,o.connectid)){ auto &p = webrtc_started.emplace_back(fd,connectid); p.started = o.started; break; } } } string VIDCONF::define_styles(bool mobile) { string lines; lines = ".grid{\n" "\tdisplay: grid;\n" "\tbackground-color: white;\n" "\tpadding: 0px;\n" "\toverflow: auto;\n" "}\n" ".grid-item {\n" "\tbackground-color: white;\n" "\tborder: 1px solid black;\n" "\tpadding: 1px;\n" "\tfont-size: 30px;\n" "\ttext-align: center;\n" "}\n" ".grid-item-me {\n" "\tposition: absolute;\n" //"\tborder: 1px solid black;\n" "\tz-index: 10;\n" "}\n" ".grid-main {\n" "\tbackground-color: white;\n" "\tborder: 1px solid rgba(0, 0, 0, 0.8);\n" "\tpadding: 2px;\n" "\tfont-size: 30px;\n" "\ttext-align: center;\n" "}\n"; return lines; } static string turn_secret; string VIDCONF::define_functions(const DOC_CONTEXT &ctx, bool mobile, unsigned board_width, unsigned board_height) { // The secret for authentication with the TURN server comes from a config file. It is read once as needed static bool turn_secret_done = false; if (!turn_secret_done){ turn_secret_done = true; ("/etc/bolixo/vidconf.secret",true); if (line[0] != '0' && line[0] != '#' && turn_secret.size()==0) turn_secret = line; return 0; //tlmp_warning ("turn secret = :%s:",turn_secret.c_str()); if (turn_secret.size()==0){ tlmp_error ("Empty secret for video conference"); } } string lines; lines += "function VIDC_updmsg(color,msg){\n"; lines += string_f("\tvar elm = document.getElementById('msg-%s');\n",gameid.c_str()); lines += "\tif (elm != null){\n"; lines += "\t\telm.style.color=color;\n"; lines += "\t\telm.innerHTML=msg;\n"; lines += "\t}\n"; lines += "}\n"; // The temporary user holds a timestamp, good for 600 seconds. string tempuser = string_f("%ld:%s",time(nullptr)+600,ctx.username); string temppass; { unsigned char res[SHA_DIGEST_LENGTH]; unsigned res_len = SHA_DIGEST_LENGTH; temppass = base64_encode((const char *)HMAC(EVP_sha1() ,(const unsigned char*) turn_secret.c_str(),turn_secret.size() ,(const unsigned char*) tempuser.c_str(),tempuser.size() ,res,&res_len),res_len); } // tlmp_warning ("tempuser :%s: temppass :%s:",tempuser.c_str(),temppass.c_str()); lines += "var gridmode=true;\n" "const config = {\n" "\ticeServers: [" "{urls: 'stun:'+location.hostname + ':3478'},"; lines += string_f("{urls: 'turn:'+location.hostname + ':3478', username: '%s',credential: '%s'},",tempuser.c_str(),temppass.c_str()); lines += "]};\n" "const constraints = { audio: true, video: true };\n"; lines += "var connects = [];\n" // List of all peer connections "var logs='';\n" // Message stored and retrieved by VIDC_getlogs() "var showing='';\n" // peer currently shown in the video tag "function findconnect(peerid){\n" "\tvar ret=null;\n" "\tfor (let i=0; i {\n" "\t\tlogs += 'gatheringstatechange ' + connect.peerid + ' state=' + event.target.iceGatheringState + '\\r';\n" "\t};\n" // Closure for ontrack "\tfunction clo_ontrack(con){\n" "\t\treturn function ({ track, streams }){\n" "\t\t\ttrack.onunmute = () => {\n" "\t\t\t\tconsole.log('selfVideo='+selfVideo+' con='+con);\n" "\t\t\t\tif(track.kind==='audio'){\n" "\t\t\t\t\tlet newStream = new MediaStream( [ track ] );\n" "\t\t\t\t\tcon.audio = audioctx.createMediaStreamSource(newStream);\n" "\t\t\t\t\tcon.audio.connect(audioctx.destination);\n" "\t\t\t\t}else{\n" "\t\t\t\t\tcon.stream = streams[0];\n" "\t\t\t\t\tVIDC_update();\n" "\t\t\t\t}\n" "\t\t\t\tlogs += 'onunmute ' + con.peerid + ' kind=' + track.kind + '\\r';\n" "\t\t\t};\n" "\t\t};\n" "\t}\n" "\tconnect.pc.ontrack = clo_ontrack(connect);\n" // Special closure for the object, see onnegotiontionneeded below "\tfunction clo_connect(con){\n" "\t\treturn async function(){\n" "\t\t\ttry {\n" "\t\t\t\tcon.makingOffer = true;\n" "\t\t\t\tawait con.pc.setLocalDescription();\n" "\t\t\t\tconsole.log ('Send localDescription');\n" "\t\t\t\tnotesocket.send('description:'+con.peerid+' '+JSON.stringify(con.pc.localDescription,null,0));\n" "\t\t\t\tcon.sentdesc = true;\n" "\t\t\t\tlogs += 'send description to ' + con.peerid + ' tboffer.length=' + con.tboffer.length + '\\r';\n" "\t\t\t\tif (con.tboffer.length > 0){\n" "\t\t\t\t\tlet off=con.tboffer[0];\n" "\t\t\t\t\treceiveoffer(off.connect,off.description,off.candidate);\n" "\t\t\t\t}\n" "\t\t\t} catch (err) {\n" "\t\t\t\tlogs += 'onnegotiationneeded error ' + err + '\\r';\n" "\t\t\t} finally {\n" "\t\t\t\tcon.makingOffer = false;\n" "\t\t\t}\n" "\t\t};\n" "\t}\n" "\tconnect.pc.onnegotiationneeded = clo_connect(connect);\n" "\tfunction clo_candidate(con){\n" "\t\treturn function ({ candidate }){\n" "\t\t\tif(candidate==null){\n" "\t\t\t\tlogs += 'Send Candidate null to '+con.peerid + '\\r';\n" "\t\t\t}else{\n" "\t\t\t\tlogs += 'Send Candidate to '+con.peerid + '\\r';\n" "\t\t\t}\n" "\t\t\tnotesocket.send ('candidate:'+con.peerid+' '+JSON.stringify(candidate,null,0));\n" "\t\t};\n" "\t}\n" "\tconnect.pc.onicecandidate = clo_candidate(connect);\n" "\tconnects.push(connect);\n" "\tfor (const track of selfStream.getTracks()) {\n" "\t\tlogs += 'addTrack ' + connect.peerid + ' kind=' + track.kind + '\\r';\n" "\t\tconnect.pc.addTrack(track, selfStream);\n" "\t}\n" "}\n" "var start_sent=false;\n" "async function vidc_start(constraints) {\n" "\ttry {\n" "\t\tselfStream = await navigator.mediaDevices.getUserMedia(constraints);\n" "\t\tVIDC_showgrid();\n" "\t\tif (!start_sent){\n" "\t\t\tnotesocket.send('webrtcstart:');\n" "\t\t\tstart_sent=true;\n" "\t\t}\n" "\t\tfor (const connect of connects){\n" "\t\t\tfor (const sender of connect.pc.getSenders()){\n" "\t\t\t\tfor (const track of selfStream.getTracks()) {\n" "\t\t\t\t\tif(sender.track.kind===track.kind){\n" "\t\t\t\t\t\tsender.replaceTrack(track);\n" "\t\t\t\t\t\tlogs += 'replaceTrack ' + connect.peerid + ' sender.track.kind=' + sender.track.kind + ' ' + sender.track.id + ' kind=' + track.kind + ' ' + track.id + '\\r';\n" "\t\t\t\t\t}\n" "\t\t\t\t}\n" "\t\t\t}\n" "\t\t}\n" "\t} catch (err) {\n" "\t\tlogs += 'vidc_start ' + err + '\\r';\n" "\t}\n" "}\n" "async function receiveoffer (connect,description,candidate) {\n" "\ttry {\n" "\t\tif (description) {\n" "\t\t\tconst offerCollision = description.type === 'offer'\n" "\t\t\t\t&& (connect.makingOffer || connect.pc.signalingState !== 'stable');\n" "\t\t\tconnect.ignoreOffer = !connect.polite && offerCollision;\n" "\t\t\tif (connect.ignoreOffer) {\n" "\t\t\t\tlogs += 'ignoreOffer '+connect.peerid+' type='+description.type+'\\r';\n" "\t\t\t\tendoffer(connect);\n" "\t\t\t\treturn;\n" "\t\t\t}\n" "\t\t\tlogs += 'setRemoteDescription '+ connect.peerid + ' type=' + description.type + '\\r';\n" "\t\t\tawait connect.pc.setRemoteDescription(description);\n" "\t\t\tif (description.type === 'offer') {\n" "\t\t\t\tawait connect.pc.setLocalDescription();\n" "\t\t\t\tnotesocket.send ('description:'+connect.peerid+' '+JSON.stringify(connect.pc.localDescription,null,0));\n" "\t\t\t\tlogs += 'offer send description to ' + connect.peerid + ' type=' + connect.pc.localDescription.type + '\\r';\n" "\t\t\t}\n" "\t\t} else {\n" "\t\t\tlogs += 'addIceCandidate from ' + connect.peerid + (candidate==null ? ' null' : '') + '\\r';\n" "\t\t\ttry {\n" "\t\t\t\tawait connect.pc.addIceCandidate(candidate);\n" "\t\t\t} catch (err) {\n" "\t\t\t\tlogs += 'addIceCandidate error ' + err + '\\r';\n" "\t\t\t\tif (!connect.ignoreOffer) {\n" "\t\t\t\t\tthrow err;\n" "\t\t\t\t}\n" "\t\t\t}\n" "\t\t}\n" "\t} catch (err) {\n" "\t\tlogs += 'receiveoffer error ' + err + '\\r';\n" "\t}\n" "\tendoffer(connect);\n" "}\n" "function endoffer(connect){\n" "\tconsole.log('receiveoffer done');\n" "\tconnect.tboffer.shift();\n" "\tif (connect.tboffer.length > 0){\n" "\t\tlet off=connect.tboffer[0];\n" "\t\treceiveoffer(off.connect,off.description,off.candidate);\n" "\t}\n" "}\n" // Make sure only one receive offer is performed at a time // we count null candidate. "function pushoffer(connect,description,candidate){\n" "\tconnect.tboffer.push({connect:connect,description:description,candidate:candidate});\n" "\tif (description != null){\n" "\t\t connect.nbpushdesc++;\n" "\t}else{\n" "\t\tconnect.nbpushcand++;\n" "\t}\n" "\tconsole.log('tboffer.length='+connect.tboffer.length);\n" "\tif(connect.sentdesc && connect.tboffer.length == 1) receiveoffer(connect,description,candidate);\n" "}\n" // Draw the current user video tag if enabled "function VIDC_showme(){\n" "var ret = '';\n" "\tif(VIDC_showme_on){\n"; lines += string_f("\t\tvar vidw=%u;\n",(board_width-10)/4); lines += string_f("\t\tvar vidh=%u;\n",(board_height-10)/3); lines += string_f("\t\tvar left=%u-vidw;\n",board_width-10); lines += string_f("\t\tvar top=%u-vidh;\n",board_height-10); lines += "\t\tret = `
\n" "\t\t
\n`;\n" "\t}\n" "\treturn ret;\n" "}\n" // Assign selfStream to the current user video tag "function VIDC_assignme(){\n" "\tif(VIDC_showme_on || connects.length == 0){\n" "\t\tvar vid = document.getElementById(`guest-me`);\n" "\t\tvid.srcObject = selfStream;\n" "\t}\n" "}\n" // Show only the current user, no one connected yet "function VIDC_show_only_me(){\n" "\tvar grid = document.getElementById('confgrid');\n"; lines += string_f("\tvar cellw=%u;\n",board_width-10); lines += string_f("\tvar cellh=%u;\n",board_height-10); lines += "\tvar txt = `
\n`;\n" "\tgrid.innerHTML = txt;\n" "\tVIDC_assignme();\n" "}\n" // Redraw the grid holding all guests "function VIDC_showgrid(){\n" "\tgridmode=true;\n" "\tif (connects.length == 0){\n" "\t\tVIDC_show_only_me();\n" "\t\treturn;\n" "\t}\n" "\tvar grid = document.getElementById('confgrid');\n"; lines += string_f("\tvar cellw=%u;\n",board_width-10); lines += string_f("\tvar cellh=%u;\n",board_height-10); lines += "\tvar style='auto';\n" "\tvar w=100;\n" "\tif(connects.length == 1){\n" "\t\tw=100;\n" "\t}else if (connects.length > 4){\n" "\t\tstyle='auto auto auto';\n" "\t\tw=33;\n" "\t\tcellw /= 3;\n" "\t\tcellh = 3*cellw/4;\n" "\t}else{\n" "\t\tstyle='auto auto';\n" "\t\tw=50;\n" "\t\tcellw /= 2;\n" "\t\tcellh = 3*cellw/4;\n" "\t}\n" "\tgrid.style.gridTemplateColumns = style;\n" "\tvar txt = '';\n" "\tfor (var i=0; i\n`;\n" "\t}\n" "\ttxt += VIDC_showme();\n" "\tgrid.innerHTML = txt;\n" "\tfor (var i=0; i {\n" "\t\tconsole.log ('tbstats.length='+tbstats.length);\n" "\t\tvar res = '';\n" "\t\tfor (let i=0; i {\n" "\t\t\t\tres += peerid + ' ' + JSON.stringify(rep,null,0)+ '\\r';\n" "\t\t\t});\n" "\t\t}\n" "\t\tnotesocket.send('stats:'+res);\n" "\t});\n" "}\n"; // Manage the popup dialog to configure the sources and output devices of the conference // draw a little X to close the dialog unsigned width = 20; unsigned start = 7; unsigned end = 15; unsigned len = 8; unsigned stroke= 1; if (mobile){ width = 60; start = 10; end = 50; len = 40; stroke= 3; } lines += string_f ( "function vidc_draw_x(){\n" "\treturn \"
" "" "" "" "" "" "" "
\";\n" "}\n" ,width,width ,width-2,width-2 ,stroke,start,start,len,len,start,end,len,len); // Present the popup dialog lines += "\tconst vidc_id = 'vidconf_popup';\n" "function vidc_delform(){\n" "\tvar div = document.getElementById(vidc_id);\n" "\tif (div != null){\n" "\t\tdocument.body.removeChild(div);\n" "\t}\n" "}\n" "function vidc_showform(){\n" "\tvar div = document.getElementById(vidc_id);\n" "\tif (div != null){\n" "\t\tdocument.body.removeChild(div);\n" "\t}else{\n" "\t\tconst txt = '\\n'\n" ,MSG_U(I_VIDEOINPUT,"Video input device"),MSG_U(I_AUDIOINPUT,"Audio input device"),MSG_U(I_AUDIOOUTPUT,"Audio output device")); lines += "\t\tdiv = document.createElement(\'div\');\n" "\t\tdiv.setAttribute(\'id\',vidc_id);\n" "\t\tdiv.setAttribute(\'class\',\'popup\');\n" "\t\tdiv.innerHTML = txt;\n" "\t\tdocument.body.appendChild(div);\n" "\t\tlet x2=document.getElementById('vidc_x2');\n" "\t\tx2.style.top=action_mousey+20;\n" // Position just lower of the button "\t\tx2.style.left=action_mousex;\n" // that triggered this dialog "\t\tx2.classList.toggle('show');\n" //"\t\tconsole.log (txt);\n" "\t\tvar videoinput = document.querySelector('#videoinput');\n" "\t\tvar audioinput = document.querySelector('#audioinput');\n" "\t\tvar audiooutput = document.querySelector('#audiooutput');\n" "\t\taudioinput.onchange = vidc_updatesources;\n" "\t\tvideoinput.onchange = vidc_updatesources;\n" "\t\taudiooutput.onchange = vidc_updateoutput;\n" "\t\tnavigator.mediaDevices.enumerateDevices().then(vidc_gotDevices).catch(vidc_handleError);\n" "\t}\n" "}\n" "function vidc_handleError(error) {\n" "\tconsole.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);\n" "}\n" // Read the setup saved in the browser "var audioSource = localStorage.getItem('vidc_audioSource');\n" "var videoSource = localStorage.getItem('vidc_videoSource');\n" "var audioOutput = localStorage.getItem('vidc_audioOutput');\n" "function vidc_setaudiooutput(){\n" "\tlogs += 'set audio output to ' + audioOutput + '\\r';\n" "\tif (audioctx.setSinkId !== undefined && audioOutput != null && audioOutput !== 'default') audioctx.setSinkId(audioOutput);\n" "}\n" "vidc_setaudiooutput();\n" // Populate the 'select' field in the popup device configuration dialog "function vidc_gotDevices(deviceInfos) {\n" "\tvar videoinput = document.querySelector('#videoinput');\n" "\tvar audioinput = document.querySelector('#audioinput');\n" "\tvar audiooutput = document.querySelector('#audiooutput');\n" "\tconst selectors = [audioinput, audiooutput, videoinput];\n" "\tconst values = [audioSource,audioOutput,videoSource];\n" "\tselectors.forEach(select => {\n" "\t\twhile (select.firstChild) {\n" "\t\t\tselect.removeChild(select.firstChild);\n" "\t\t}\n" "\t});\n" "\tfor (let i = 0; i !== deviceInfos.length; ++i) {\n" "\t\tconst deviceInfo = deviceInfos[i];\n" "\t\tconst option = document.createElement('option');\n" "\t\toption.value = deviceInfo.deviceId;\n" "\t\tif (deviceInfo.kind === 'audioinput') {\n" "\t\t\toption.text = deviceInfo.label || `microphone ${audioInputSelect.length + 1}`;\n" "\t\t\taudioinput.appendChild(option);\n" "\t\t} else if (deviceInfo.kind === 'audiooutput') {\n" // There is no way to set the sinkid of AudioContext back to default. So we hide this option "\t\t\tif(option.value !== 'default'){\n" "\t\t\t\toption.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;\n" "\t\t\t\taudiooutput.appendChild(option);\n" "\t\t\t}\n" "\t\t} else if (deviceInfo.kind === 'videoinput') {\n" "\t\t\toption.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;\n" "\t\t\tvideoinput.appendChild(option);\n" "\t\t} else {\n" "\t\t\tconsole.log('Some other kind of source/device: ', deviceInfo);\n" "\t\t}\n" "\t}\n" "\tselectors.forEach((select, selectorIndex) => {\n" "\t\tif (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {\n" "\t\t\tselect.value = values[selectorIndex];\n" "\t\t}\n" "\t});\n" "}\n" "function vidc_setupsources() {\n" "\tconsole.log ('get audio='+audioSource+' video='+videoSource);\n" "\tconst constraints = {\n" "\t\taudio: {deviceId: audioSource ? {exact: audioSource} : undefined},\n" "\t\tvideo: {deviceId: videoSource ? {exact: videoSource} : undefined}\n" "\t};\n" "\tvidc_start(constraints);\n" "}\n" "function vidc_updatesources() {\n" "\taudioSource = audioinput.value;\n" "\tvideoSource = videoinput.value;\n" "\tlocalStorage.setItem('vidc_audioSource',audioSource);\n" "\tlocalStorage.setItem('vidc_videoSource',videoSource);\n" "\tvidc_setupsources();\n" "}\n" "function vidc_updateoutput() {\n" "\taudioOutput = audiooutput.value;\n" "\tlocalStorage.setItem('vidc_audioOutput',audioOutput);\n" "\tvidc_setaudiooutput();\n" "}\n" // Tell web/index.hcc to call the start() function when notesocket is ready "var notesocket_call=vidc_setupsources;\n"; return lines; }
string VIDCONF::draw_board( bool mobile, unsigned docnum, unsigned width, unsigned height, string &script) { string lines; lines += "
\n"; lines += string_f("
\n",width,height); #if 0 lines += string_f("\n"; #endif lines += "
\n"; lines += "
\n"; #if 0 script += string_f("const videoTag = document.getElementById('vidc-%s');\n",gameid.c_str()); script += "const myMediaSource = new MediaSource();\n" "const url = URL.createObjectURL(myMediaSource);\n" "myMediaSource.addEventListener('sourceopen', sourceOpen);\n" "videoTag.src = url;\n" "function sourceOpen(){\n" //"\tvideoTag.play();\n" "\tconsole.log('sourceopen state='+this.readyState); // open\n" "\tconst mtype = 'video/mp4; codecs=\"avc1.64001e, mp4a.40.2,avc1.4d401e\"';\n" "\tif (MediaSource.isTypeSupported(mtype)){\n" "\t\tconsole.log('mp4 codec supported');\n" "\t}else{\n" "\t\tconsole.log('mp4 codec not supported');\n" "\t}\n" "\tsourcebuffer = this.addSourceBuffer(mtype);\n" "\tconst curMode = sourcebuffer.mode;\n" "\tif (curMode === 'segments') {\n" "\t\tsourcebuffer.mode = 'sequence';\n" "\t}\n" "\tconsole.log ('sourcebuffer.mode='+sourcebuffer.mode);\n" "\tsourcebuffer.addEventListener('update', function() {\n" "\t\tconsole.log ('update');\n" "\t\tif(vidc_chunks.length > 0){\n" "\t\t\tsourcebuffer.appendBuffer(vidc_chunks.shift());\n" "\t\t}\n" "\t\t});\n" "\tsourcebuffer.addEventListener('updateend', function() {\n" "\t\tconsole.log ('updateend');\n" "\t});\n" "\tsourcebuffer.addEventListener('error', function(e) {\n" "\t\tconsole.log ('buffer error');\n" "\t\tconsole.log(e);\n" "\t});\n" "\tvideoTag.addEventListener('play', function() {\n" "\t\tconsole.log ('play');\n" "\t}, false);\n" "\tvideoTag.addEventListener('playing', function() {\n" "\t\tconsole.log ('playing');\n" "\t}, false);\n" "\tvideoTag.addEventListener('waiting', function() {\n" "\t\tconsole.log ('waiting');\n" "\t}, false);\n" "\tvideoTag.addEventListener('stalled', function() {\n" "\t\tconsole.log ('stalled');\n" "\t}, false);\n" "\tvideoTag.addEventListener('progress', function() {\n" "\t\tconsole.log ('progress');\n" "\t}, false);\n" "\tvideoTag.addEventListener('timeupdate', function() {\n" "\t\tconsole.log ('timeupdate');\n" "\t}, false);\n" "\tvideoTag.addEventListener('seeking', function() {\n" "\t\tconsole.log ('seeking');\n" "\t}, false);\n" "\tvideoTag.addEventListener('seeked', function() {\n" "\t\tconsole.log ('seeked');\n" "\t}, false);\n" "\tvideoTag.addEventListener('loadeddata', function() {\n" "\t\tconsole.log ('loadeddata');\n" "\t}, false);\n" "\tvideoTag.addEventListener('loadedmetadata', function() {\n" "\t\tconsole.log ('loadedmetadata');\n" "\t}, false);\n" "\tvideoTag.addEventListener('error', function(e) {\n" "\t\tconsole.log ('videotag error');\n" "\t\tconsole.log (e);\n" "\t}, false);\n" "}\n" ; #endif return lines; } /* Generate the JS code to update the background of a button based on the state of a JS variable */ static void vidconf_update_button (string &script, const char *variable, unsigned button_id) { script += string_f("document.getElementById('button%u').style.background=%s ? 'lightblue' : 'lightgray';\n" ,button_id,variable); } void VIDCONF::exec ( const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, vector &res, vector &unotifies) { string error,status,api_error; bool clear_status_ok = true; // We send a clear status line except when processing webrtc request setactivity(); VARVAL notify_var; notify_var.var = VAR_NOTIFY; notify_var.val = string_f("doc_cur_gameid='%s';\n",gameid.c_str()); size_t notify_init_size = notify_var.val.size(); // Do not send notification if there is only the variable declaration in it // See the end of this function. if (is_eq(var,REQ_PRINT)){ if (is_any_of(val,"","full")){ VARVAL v; glocal VIDCONF *doc = this; (val,this,ctx,sp,"vidconf",false,is_eq(val,"full"),v); return glocal.doc->define_styles(sp.mobile); return glocal.doc->define_functions(ctx,sp.mobile,board_width,board_height); VIDEO_MENU menu(specs); #define BUTTON_CONFIG 0 #define BUTTON_SHOWGRID 1 #define BUTTON_SHOWMAIN 2 #define BUTTON_SHOWME 3 #define BUTTON_MUTEME 4 #define BUTTON_HIDEME 5 documentd_bar_button (lines,BUTTON_CONFIG,menu.svg_config,specs,false,MSG_U(I_DEVICESELECT,"Audio/video devices selection")); // gridmode and camera view are active by default documentd_bar_button (lines,BUTTON_SHOWGRID,menu.svg_showgrid,specs,true,MSG_U(I_SHOWGUESTS,"Show all participants")); documentd_bar_button (lines,BUTTON_SHOWMAIN,menu.svg_showmain,specs,false,MSG_U(_SHOWACTIVEGUES,"Show active participant")); documentd_bar_button (lines,BUTTON_SHOWME,menu.svg_showme,specs,true,MSG_U(_SHOWME,"Display the user camera view")); documentd_bar_button (lines,BUTTON_MUTEME,menu.svg_muteme,specs,false,MSG_U(_MUTEME,"Mute the microphone")); documentd_bar_button (lines,BUTTON_HIDEME,menu.svg_hideme,specs,false,MSG_U(_HIDEME,"Turn off the camera")); return glocal.doc->draw_board(sp.mobile,ctx.docnum,board_width,board_height,script); res.emplace_back(move(v)); } }else if (is_eq(var,REQ_FUNCTIONS)){ VARVAL var; var.var = VAR_DEFSCRIPT; var.val = define_functions (ctx,sp.mobile,1000,1000); res.emplace_back(move(var)); }else if (is_eq(var,REQ_STYLES)){ VARVAL var; var.var = VAR_STYLES; var.val += define_styles(sp.mobile); res.emplace_back(move(var)); }else if (is_eq(var,REQ_REGION)){ // For embedding VARVAL var,var_script; var.var = VAR_CONTENT; var_script.var = VAR_DEFSCRIPT; var.val = draw_board(sp.mobile,ctx.docnum,1000,1000,var_script.val); res.emplace_back(move(var)); res.emplace_back(move(var_script)); }else if (is_eq(var,REQ_CHAT)){ appendchat(val,notify_var.val,res,ctx); }else if (is_eq(var,REQ_GETFIELDS)){ VARVAL var; var.var = VAR_FIELDS; if (is_eq(val,DIALOG_VIDCONF_CONFIG)){ } res.emplace_back(var); }else if (is_eq(var,REQ_FOCUS)){ // Nothing to do }else if (ctx.maywrite){ USERS_NOTIFIES others; // Messages sent to other users USERS_NOTIFIES wstats; // Messages sent to the connection having done the getstats request for (auto &n:notification_fds){ //tlmp_warning ("count=%d/%zu n.second.connectid %s ctx.connectid %s",count,notification_fds.size(),n.second.connectid.c_str(),ctx.connectid); if (!is_eq(n.second.connectid,ctx.connectid)){ others.connections.push_back(n.first); } if (is_eq(n.second.connectid,stats.connectid)){ wstats.connections.push_back(n.first); } } if (wstats.connections.size()==0) stats.connectid.clear(); // The connection stats.connectid is gone if (is_eq(var,"append")){ others.val = string_f("vidc_add('%s');\n",val); }else if (is_eq(var,"webrtcstart")){ /* Here the strategy for multiple users (more than 2) video conference Every connected user has a unique connectid allocated by bo-websocket. We have access to all connections (and connectid) in notification_fds. When we receive a webrtcstart, we setup all the connection between this new connection and all already connected. So when we receive a webrtcstart, we have the connectid associated with webrtcstart (called from_id). the connectids of all other connected users (called peed_ids) So we do one loop. for peer_id in peer_ids{ send to webrtcstart caller: VIDC_setpolite(true,'connectid'); send to the peer: VIDC_setpolite(false,'peer_connectid'); } If there is no peer yet, we send nothing. We add this one to webrtc_started and the next user will trigger the connection. */ tlmp_warning ("webrtcstart user=%s connectid=%s",ctx.username,ctx.connectid); clear_status_ok = false; VARVAL var; var.var = VAR_SCRIPT; var.val = string_f("VIDC_show('%s');\n",showing.c_str()); for (auto &n:webrtc_started){ var.val += string_f("VIDC_setpolite(false,'%s');\n",n.connectid.c_str()); USERS_NOTIFIES peer; // Message sent to connect the peer peer.connections.push_back(n.fd); if (webrtc_started.size()==1) peer.val = string_f("VIDC_show('%s');\n",ctx.connectid); peer.val += string_f("VIDC_setpolite(true,'%s');\n",ctx.connectid); unotifies.emplace_back(move(peer)); } // We have to locate the notification handle for the current request int fd = -1; for (auto &n:notification_fds){ //tlmp_warning ("scan %s <> %s username=%s fd=%d",ctx.connectid,n.second.connectid.c_str(),n.second.username.c_str(),n.first); if (is_eq(n.second.connectid,ctx.connectid)){ fd = n.first; break; } } // tlmp_warning ("webrtcstart fd=%d val=%zu",fd,var.val.size()); webrtc_started.emplace_back(fd,ctx.connectid); if (webrtc_started.size()==1) showing = ctx.connectid; if (var.val.size() > 0) res.emplace_back(move(var)); appendchat(string_f("%s: %s",ctx.username,MSG_U(I_JUSTCONNECTED,"connected")),notify_var.val,res,ctx); }else if (is_eq(var,"description")){ string peerid; const char *pt = str_copyword(peerid,val); int fd = findpeer(peerid); // tlmp_warning ("description %s to %s fd=%d user=%s len=%zu",ctx.connectid,peerid.c_str(),fd,ctx.username,strlen(pt)); if (fd != -1){ USERS_NOTIFIES peer; // Message sent to the peer peer.connections.push_back(fd); peer.val = string_f("VIDC_setdesc('%s','%s');\n",ctx.connectid,documentd_escape(pt).c_str()); unotifies.emplace_back(move(peer)); } }else if (is_eq(var,"candidate")){ string peerid; const char *pt = str_copyword(peerid,val); int fd = findpeer(peerid); // tlmp_warning ("candidate %s to %s fd=%d user=%s len=%zu",ctx.connectid,peerid.c_str(),fd,ctx.username,strlen(pt)); if (fd != -1){ USERS_NOTIFIES peer; // Message sent to the peer peer.connections.push_back(fd); peer.val = string_f("VIDC_setcand('%s','%s');\n",ctx.connectid,documentd_escape(pt).c_str()); unotifies.emplace_back(move(peer)); } }else if (is_eq(var,"getstates")){ // Request connection status from all video conference users stats.nbreq = others.connections.size(); if (stats.nbreq > 0){ stats.connectid = ctx.connectid; others.val = "VIDC_getstates();\n"; }else{ VARVAL var; var.var = VAR_SCRIPT; var.val = string_f("states %s\nend\n",MSG_U(E_NOUSERSINCONF,"No users in conference")); res.emplace_back(move(var)); } clear_status_ok = false; }else if (is_eq(var,"states")){ // tlmp_warning ("states from %s: %s",ctx.username,val); if (wstats.connections.size() > 0){ wstats.val = string_f("states %s %s %s\n",ctx.username,ctx.connectid,val); stats.nbreq--; if (stats.nbreq == 0){ wstats.val += "end\n"; stats.connectid.clear(); } } }else if (is_eq(var,"getlogs")){ // Request connection logs from all video conference users stats.nbreq = others.connections.size(); if (stats.nbreq > 0){ stats.connectid = ctx.connectid; others.val = "VIDC_getlogs();\n"; }else{ VARVAL var; var.var = VAR_SCRIPT; var.val = string_f("logserror %s\nend\n",MSG_R(E_NOUSERSINCONF)); res.emplace_back(move(var)); } clear_status_ok = false; }else if (is_eq(var,"logs")){ // tlmp_warning ("logs from %s: %s",ctx.username,val); if (wstats.connections.size() > 0){ wstats.val = string_f("logs %s %s %s\n",ctx.username,ctx.connectid,val); stats.nbreq--; if (stats.nbreq == 0){ wstats.val += "end\n"; stats.connectid.clear(); } } }else if (is_eq(var,"getstats")){ // Request stats from all video conference users stats.nbreq = others.connections.size(); if (stats.nbreq > 0){ stats.connectid = ctx.connectid; VARVAL var; var.var = VAR_SCRIPT; for (auto &w:webrtc_started){ var.val += string_f("statspeerid %s %lu\n",w.connectid.c_str(),w.started); } res.emplace_back(move(var)); others.val = "VIDC_getstats();\n"; }else{ VARVAL var; var.var = VAR_SCRIPT; var.val = string_f("statserror %s\nend\n",MSG_R(E_NOUSERSINCONF)); res.emplace_back(move(var)); } clear_status_ok = false; }else if (is_eq(var,"stats")){ // tlmp_warning ("stats from %s: %s",ctx.username,val); if (wstats.connections.size() > 0){ wstats.val = string_f("stats %s %s\n",ctx.username,val); stats.nbreq--; if (stats.nbreq == 0){ wstats.val += "end\n"; stats.connectid.clear(); } } }else if (is_eq(var,"listusers")){ VARVAL var; var.var = VAR_SCRIPT; // Go through all connections (to get the username) // and check if it is a webrtc connection. for (auto &n:notification_fds){ auto web = find_if(webrtc_started.begin(),webrtc_started.end() ,[&ncon=n.second.connectid](auto &w){ return ncon==w.connectid; }); if (web != webrtc_started.end()){ var.val += string_f("user %s %s %ld\n",n.second.connectid.c_str(),n.second.username.c_str(),web->started); } } var.val += "end\n"; res.emplace_back(move(var)); clear_status_ok = false; }else if (is_eq(var,"show")){ // We accept a user name or a connection id. // This command may be sent by bofs or by a conference user (javascript in browser). // If it is a user name, it may contains a fully qualified user@server. // If server is this server, we only keep the user string user,node; if (splitline(val,'@',user,node) && is_eq(node,documentd_getnodename())){ val = user.c_str(); } USERS_NOTIFIES peers; for (auto &n:notification_fds){ if (is_eq(n.second.connectid,val) || is_eq(n.second.username,val)){ if (find_if(webrtc_started.begin(),webrtc_started.end() ,[&ncon=n.second.connectid](auto &w){ return ncon==w.connectid; })!=webrtc_started.end()){ showing = n.second.connectid; peers.val = string_f("VIDC_show('%s');\n",showing.c_str()); for (auto &w:webrtc_started){ peers.connections.push_back(w.fd); } break; } } } if (peers.connections.size() > 0){ unotifies.emplace_back(move(peers)); } clear_status_ok = false; }else if (is_eq(var,"getjs")){ clear_status_ok = false; // Get the client javascript code VARVAL var; var.var = VAR_SCRIPT; var.val = define_functions(ctx,false,1000,1000); var.val += "end\n"; res.emplace_back(move(var)); }else if (is_eq(var,"newgame")){ VARVAL var; var.var = VAR_SCRIPT; int uval = atoi(val); if (uval == BUTTON_RELOAD){ documentd_action_reload(res); }else if (uval == BUTTON_CONFIG){ var.val = "vidc_showform();\n"; }else if (uval == BUTTON_SHOWGRID){ var.val = "VIDC_showgrid();\n"; vidconf_update_button(var.val,"gridmode",BUTTON_SHOWGRID); vidconf_update_button(var.val,"!gridmode",BUTTON_SHOWMAIN); }else if (uval == BUTTON_SHOWMAIN){ var.val = "VIDC_showmain();\n"; vidconf_update_button(var.val,"gridmode",BUTTON_SHOWGRID); vidconf_update_button(var.val,"!gridmode",BUTTON_SHOWMAIN); }else if (uval == BUTTON_SHOWME){ var.val = "VIDC_showme_on = !VIDC_showme_on;\n" "VIDC_update();\n"; vidconf_update_button(var.val,"VIDC_showme_on",BUTTON_SHOWME); }else if (uval == BUTTON_MUTEME){ var.val = "for (const track of selfStream.getTracks()) {\n" "\tif (track.kind === 'audio'){\n" "\t\ttrack.enabled = !track.enabled;\n"; vidconf_update_button(var.val,"!track.enabled",BUTTON_MUTEME); var.val += "\t}\n" "}\n"; }else if (uval == BUTTON_HIDEME){ var.val = "for (const track of selfStream.getTracks()) {\n" "\tif (track.kind === 'video'){\n" "\t\ttrack.enabled = !track.enabled;\n"; vidconf_update_button(var.val,"!track.enabled",BUTTON_HIDEME); var.val += "\t}\n" "}\n"; } if (var.val.size() > 0){ res.emplace_back(move(var)); } }else{ api_error = MSG_U(E_IVLAPICOMMAND,"Invalid API command"); } if (others.val.size() > 0) unotifies.emplace_back(move(others)); if (wstats.val.size() > 0) unotifies.emplace_back(move(wstats)); }else{ tlmp_error ("var=%s\n",var); error = MSG_R(E_READONLY); } if (notify_var.val.size() > notify_init_size) res.emplace_back(move(notify_var)); if (api_error.size() > 0){ VARVAL var; var.var = VAR_ERROR; var.val = move(api_error); res.emplace_back(move(var)); } if (error.size() > 0){ update_msg(false,error,"red",res); }else if (status.size() > 0){ update_msg(true,status,"red",res); }else if (clear_status_ok){ update_msg(false," ","white",res); } }