diff --git a/tab/venue/amd/build/venue_manager.min.js b/tab/venue/amd/build/venue_manager.min.js new file mode 100644 index 00000000..bc0f85e7 --- /dev/null +++ b/tab/venue/amd/build/venue_manager.min.js @@ -0,0 +1,3 @@ +function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(obj){return typeof obj}:function(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}define("videotimetab_venue/venue_manager",["exports","core/ajax","core/fragment","core/str","block_deft/janus-gateway","core/log","core/notification","videotimeplugin_live/socket","core/toast","block_deft/janus_venue"],(function(_exports,_ajax,_fragment,_str,_janusGateway,_log,_notification,_socket,Toast,_janus_venue){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _defineProperties(target,props){for(var i=0;iRoom "+this.roomid+" is not configured."):_notification.default.alert(msg.error_code,msg.error));if(msg.leaving){var leaving=msg.leaving;_janusGateway.default.log("Participant left: "+leaving),document.querySelectorAll('[data-region="venue-participants"] [data-peerid="'+leaving+'"]').forEach((function(peer){peer.remove()}))}}jsep&&(_janusGateway.default.debug("Handling SDP as well...",jsep),this.audioBridge.handleRemoteJsep({jsep:jsep}))}},{key:"processSignal",value:function(){}},{key:"updateParticipants",value:function(list){for(var f in _janusGateway.default.debug("Got a list of participants:",list),list){var id=list[f].id,display=list[f].display,setup=list[f].setup,muted=list[f].muted;_janusGateway.default.debug(" >> ["+id+"] "+display+" (setup="+setup+", muted="+muted+")"),document.querySelector('[data-region="venue-participants"] [data-peerid="'+id+'"]')||Number(this.peerid)==Number(id)||this.peerAudioPlayer(id)}}},{key:"sendSignals",value:function(){var _this4=this;if(!this.throttled&&navigator.onLine){var time=Date.now();if(this.lastUpdate+200>time)return this.throttled=!0,setTimeout((function(){_this4.throttled=!1}),this.lastUpdate+250-time),void this.sendSignals();this.lastUpdate=time,_ajax.default.call([{args:{contextid:this.contextid},contextid:this.contextid,done:function(response){response.settings.forEach((function(peer){if(peer.id==Number(_this4.peerid)){if(peer.status)return clearInterval(_this4.meterId),_this4.audioInput.then((function(audioStream){return audioStream&&audioStream.getAudioTracks().forEach((function(track){track.stop()})),audioStream})).catch(_log.default.debug),_this4.janus.destroy(),document.querySelectorAll('[data-region="deft-venue"] [data-peerid="'+_this4.peerid+'"]').forEach((function(venue){var e=new Event("venueclosed",{bubbles:!0});venue.dispatchEvent(e)})),_this4.socket.disconnect(),void window.close();_this4.mute(peer.mute)}document.querySelectorAll('[data-contextid="'+_this4.contextid+'"] [data-peerid="'+peer.id+'"] [data-action="mute"], [data-contextid="'+_this4.contextid+'"] [data-peerid="'+peer.id+'"] [data-action="unmute"]').forEach((function(button){peer.mute==("mute"==button.getAttribute("data-action"))?button.classList.add("hidden"):button.classList.remove("hidden")}))})),document.querySelectorAll('[data-region="venue-participants"] [data-peerid]').forEach((function(peer){response.peers.includes(Number(peer.getAttribute("data-peerid")))||peer.remove()})),response.peers.includes(Number(_this4.peerid))&&_this4.restart&&((0,_str.get_string)("reconnecting","block_deft").done((function(message){Toast.add(message,{type:"info"})})),_this4.restart=!1,_this4.startConnection())},fail:_notification.default.exception,methodname:"videotimetab_venue_status"}])}}},{key:"sendMessage",value:function(text){if(text&&""!==text&&this.textroom&&document.querySelector('[data-contextid="'+this.contextid+'"] .hidden[data-action="join"]')){var message={textroom:"message",transaction:_janusGateway.default.randomString(12),room:this.roomid,text:text};this.textroom.data({text:JSON.stringify(message),error:_log.default.debug})}}},{key:"subscribeTo",value:function(){}},{key:"handleClose",value:function(){this.janus&&(this.janus.destroy(),this.janus=null),document.querySelector("body").removeEventListener("click",handleClick),this.remoteFeed&&this.remoteFeed.janus&&(this.remoteFeed.janus.destroy(),this.remoteFeed=null)}},{key:"peerAudioPlayer",value:function(peerid){var usernode=document.querySelector('[data-region="venue-participants"] div[data-peerid="'+peerid+'"] audio');if(usernode)return Promise.resolve(usernode);var node=document.createElement("div");return node.setAttribute("data-peerid",peerid),document.querySelector("body#page-blocks-deft-venue")?node.setAttribute("class","col col-12 col-sm-6 col-md-4 col-lg-3 p-2"):node.setAttribute("class","col col-12 col-sm-6 col-md-4 p-2"),window.setTimeout((function(){node.querySelectorAll("img.card-img-top").forEach((function(image){image.setAttribute("height",null),image.setAttribute("width",null)}))})),_fragment.default.loadFragment("videotimetab_venue","venue",this.contextid,{peerid:peerid}).done((function(userinfo){document.querySelector('[data-region="venue-participants"] div[data-peerid="'+peerid+'"] audio')||(document.querySelector('[data-region="venue-participants"]').appendChild(node),node.innerHTML=userinfo)})).then((function(){return document.querySelector('[data-region="venue-participants"] div[data-peerid="'+peerid+'"] audio')})).catch(_notification.default.exception)}},{key:"handleMuteButtons",value:function(e){var _this5=this,button=e.target.closest('a[data-action="mute"], a[data-action="unmute"]');if(button){var action=button.getAttribute("data-action"),peerid=button.closest("[data-peerid]").getAttribute("data-peerid");e.stopPropagation(),e.preventDefault(),button.closest('[data-region="venue-participants"]')?_ajax.default.call([{args:{contextid:this.contextid,mute:!0,peerid:peerid,status:!1},fail:_notification.default.exception,methodname:"videotimetab_venue_settings"}]):this.audioInput.then((function(audioStream){return audioStream?_ajax.default.call([{args:{contextid:_this5.contextid,mute:"mute"==action,status:!1},fail:_notification.default.exception,methodname:"videotimetab_venue_settings"}]):"unmute"==action&&(_this5.audioInput=navigator.mediaDevices.getUserMedia({audio:{autoGainControl:_this5.autogaincontrol,echoCancellation:_this5.echocancellation,noiseSuppression:_this5.noisesuppression,sampleRate:_this5.samplerate},video:!1}).then((function(audioStream){return _ajax.default.call([{args:{mute:!1,status:!1},fail:_notification.default.exception,methodname:"videotimetab_venue_settings"}]),_this5.monitorVolume(audioStream),audioStream})).catch(_log.default.debug)),audioStream})).catch(_notification.default.exception),button.closest("[data-peerid]").querySelectorAll('[data-action="mute"], [data-action="unmute"]').forEach((function(option){option.getAttribute("data-action")==action?option.classList.add("hidden"):option.classList.remove("hidden")}))}}},{key:"handleRaiseHand",value:function(e){var button=e.target.closest('[data-action="raisehand"], [data-action="lowerhand"]');if(button&&!button.closest('[data-region="venue-participants"]')){var action=button.getAttribute("data-action");e.stopPropagation(),e.preventDefault(),"raisehand"==action?document.querySelector("body").classList.add("videotimetab_raisehand"):document.querySelector("body").classList.remove("videotimetab_raisehand"),document.querySelectorAll('a[data-action="raisehand"], a[data-action="lowerhand"]').forEach((function(button){button.getAttribute("data-action")==action?button.classList.add("hidden"):button.classList.remove("hidden")})),_ajax.default.call([{args:{contextid:this.contextid,status:"raisehand"==action},fail:_notification.default.exception,methodname:"videotimetab_venue_raise_hand"}]),this.sendMessage(JSON.stringify({raisehand:"raisehand"==action}))}}}])&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),JanusManager}(_janus_venue.default);_exports.default=JanusManager;var handleClick=function(e){var button=e.target.closest('[data-contextid] a[data-action="join"], [data-contextid] a[data-action="leave"]');if(button){var action=button.getAttribute("data-action"),contextid=button.closest("[data-contextid]").getAttribute("data-contextid"),room=rooms[contextid];if(e.stopPropagation(),e.preventDefault(),document.querySelectorAll('[data-contextid] a[data-action="join"], [data-contextid] a[data-action="leave"]').forEach((function(button){contextid==button.closest("[data-contextid]").getAttribute("data-contextid")&&(action==button.getAttribute("data-action")?button.classList.add("hidden"):button.classList.remove("hidden"))})),"leave"==action){var leave={textroom:"leave",transaction:_janusGateway.default.randomString(12),room:room.manager.roomid};_ajax.default.call([{args:{contextid:room.manager.contextid,mute:!0,status:!0},fail:_notification.default.exception,methodname:"videotimetab_venue_settings"}]),room.manager.audioBridge.send({message:{request:"leave"}}),room.manager.textroom.data({text:JSON.stringify(leave),error:function(reason){_notification.default.alert("Error",reason)}})}else if(room)if(room.manager.audioBridge){var join={textroom:"join",transaction:_janusGateway.default.randomString(12),room:Number(room.manager.roomid),display:"",username:String(room.manager.peerid)};room.manager.textroom.data({text:JSON.stringify(join),error:function(reason){_notification.default.alert("Error",reason)}}),room.manager.register(room.manager.audioBridge).then((function(){var configure={audiobridge:"configure",mute:!1,transaction:_janusGateway.default.randomString(12)};room.manager.audiobridge.send({message:JSON.stringify(configure),error:function(reason){_notification.default.alert("Error",reason)}})}))}else setTimeout((function(){room.manager.startConnection()}))}};return _exports.default})); + +//# sourceMappingURL=venue_manager.min.js.map \ No newline at end of file diff --git a/tab/venue/amd/build/venue_manager.min.js.map b/tab/venue/amd/build/venue_manager.min.js.map new file mode 100644 index 00000000..a1698dac --- /dev/null +++ b/tab/venue/amd/build/venue_manager.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"venue_manager.min.js","sources":["../src/venue_manager.js"],"sourcesContent":["/**\n * Manage venue connections\n *\n * @module videotimetab_venue/venue_manager\n * @copyright 2023 bdecent gmbh \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from \"core/ajax\";\nimport Fragment from \"core/fragment\";\nimport {get_string as getString} from 'core/str';\nimport Janus from 'block_deft/janus-gateway';\nimport Log from \"core/log\";\nimport Notification from \"core/notification\";\nimport Socket from \"videotimeplugin_live/socket\";\nimport * as Toast from 'core/toast';\nimport VenueManagerBase from \"block_deft/janus_venue\";\n\nvar rooms = {},\n stereo = false;\n\nexport default class JanusManager extends VenueManagerBase {\n /**\n * Add event listeners\n */\n addListeners() {\n\n document.querySelector('body').removeEventListener('click', this.handleMuteButtons.bind(this));\n document.querySelector('body').addEventListener('click', this.handleMuteButtons.bind(this));\n\n document.querySelector('body').removeEventListener('click', this.handleRaiseHand.bind(this));\n document.querySelector('body').addEventListener('click', this.handleRaiseHand.bind(this));\n\n document.querySelector('body').removeEventListener('click', this.closeConnections.bind(this));\n document.querySelector('body').addEventListener('click', this.closeConnections.bind(this));\n\n window.onbeforeunload = this.closeConnections.bind(this);\n\n this.audioInput = Promise.resolve(null);\n\n this.socket = new Socket(this.contextid, this.token);\n this.socket.subscribe(() => {\n this.sendSignals();\n });\n }\n\n\n /**\n * Start to establish the peer connections\n */\n startConnection() {\n const contextid = this.contextid,\n peerid = this.peerid,\n room = rooms[contextid];\n if (!this.started) {\n Ajax.call([{\n methodname: 'videotimeplugin_live_get_room',\n args: {contextid: contextid},\n done: (response) => {\n this.iceservers = JSON.parse(response.iceservers);\n this.roomid = Number(response.roomid);\n this.server = response.server;\n\n rooms[String(contextid)] = {\n contextid: contextid,\n manager: this,\n peerid: peerid,\n roomid: response.roomid,\n server: response.server,\n iceServers: JSON.parse(response.iceservers)\n };\n this.startConnection();\n },\n fail: Notification.exception\n }]);\n this.started = true;\n }\n if (!room) {\n return;\n }\n this.transactions = {};\n\n document.querySelector('body').removeEventListener('venueclosed', this.handleClose.bind(this));\n document.querySelector('body').addEventListener('venueclosed', this.handleClose.bind(this));\n\n document.querySelector('body').removeEventListener('click', handleClick);\n document.querySelector('body').addEventListener('click', handleClick);\n\n if (!document.querySelector('[data-contextid=\"' + this.contextid + '\"] .hidden[data-action=\"join\"]')) {\n return;\n }\n\n if (document.querySelector('[data-contextid=\"' + this.contextid + '\"] .hidden[data-action=\"unmute\"]')) {\n this.audioInput = navigator.mediaDevices.getUserMedia({\n audio: {\n autoGainControl: this.autogaincontrol,\n echoCancellation: this.echocancellation,\n noiseSuppression: this.noisesuppression,\n sampleRate: this.samplerate\n },\n video: false\n }).catch(() => {\n Ajax.call([{\n args: {\n mute: true,\n \"status\": false\n },\n fail: Notification.exception,\n methodname: 'videotimetab_venue_settings'\n }]);\n\n return false;\n });\n this.audioInput.then(this.monitorVolume.bind(this)).catch(Log.debug);\n } else {\n this.audioInput = Promise.resolve(null);\n }\n\n // Initialize the library (all console debuggers enabled)\n Janus.init({\n debug: \"none\", callback: () => {\n // Create session.\n this.janus = new Janus(\n {\n server: this.server,\n iceServers: this.iceServers,\n success: () => {\n // Attach audiobridge plugin.\n this.janus.attach(\n {\n plugin: \"janus.plugin.audiobridge\",\n opaqueId: \"audioroom-\" + Janus.randomString(12),\n success: pluginHandle => {\n this.audioBridge = pluginHandle;\n Log.debug(pluginHandle.session.getSessionId());\n this.register(pluginHandle);\n },\n error: function(error) {\n Janus.error(\" -- Error attaching plugin...\", error);\n Notification.alert('', \"Error attaching plugin... \" + error);\n },\n onmessage: this.onMessage.bind(this),\n onremotetrack: (track, mid, on, metadata) => {\n Janus.debug(\n \"Remote track (mid=\" + mid + \") \" +\n (on ? \"added\" : \"removed\") +\n (metadata ? \" (\" + metadata.reason + \") \" : \"\") + \":\", track\n );\n if (this.remoteStream || track.kind !== \"audio\") {\n return;\n }\n if (!on) {\n // Track removed, get rid of the stream and the rendering\n this.remoteStream = null;\n return;\n }\n this.remoteStream = new MediaStream([track]);\n Janus.attachMediaStream(document.getElementById('roomaudio'), this.remoteStream);\n }\n }\n );\n this.janus.attach(\n {\n plugin: \"janus.plugin.textroom\",\n opaqueId: \"textroom-\" + Janus.randomString(12),\n success: pluginHandle => {\n this.textroom = pluginHandle;\n Janus.log(\"Plugin attached! (\" + this.textroom.getPlugin()\n + \", id=\" + this.textroom.getId() + \")\");\n // Setup the DataChannel\n const body = {request: \"setup\"};\n Janus.debug(\"Sending message:\", body);\n this.textroom.send({message: body});\n },\n error: function(error) {\n Notification.alert('', error);\n Janus.error(\" -- Error attaching plugin...\", error);\n },\n onmessage: (msg, jsep) => {\n Janus.debug(\" ::: Got a message :::\", msg);\n if (msg.error) {\n Notification.alert(msg.error_code, msg.error);\n }\n\n if (jsep) {\n // Answer\n this.textroom.createAnswer(\n {\n jsep: jsep,\n // We only use datachannels\n tracks: [\n {type: 'data'}\n ],\n success: (jsep) => {\n Janus.debug(\"Got SDP!\", jsep);\n const body = {request: \"ack\"};\n this.textroom.send({message: body, jsep: jsep});\n },\n error: function(error) {\n Janus.error(\"WebRTC error:\", error);\n }\n }\n );\n }\n },\n // eslint-disable-next-line no-unused-vars\n ondataopen: (label, protocol) => {\n const transaction = Janus.randomString(12),\n register = {\n textroom: \"join\",\n transaction: transaction,\n room: this.roomid,\n username: String(this.peerid),\n display: '',\n };\n this.textroom.data({\n text: JSON.stringify(register),\n error: function(reason) {\n Notification.alert('Error', reason);\n }\n });\n },\n ondata: (data) => {\n Janus.debug(\"We got data from the DataChannel!\", data);\n const message = JSON.parse(data),\n event = message.textroom,\n transaction = message.transaction;\n if (transaction && this.transactions[transaction]) {\n this.transactions[transaction](message);\n delete this.transactions[transaction];\n }\n\n if (event === 'message' && message.from != this.peerid) {\n this.handleMessage(message.from, {data: message.text});\n }\n if (event === 'error') {\n Log.debug(message);\n }\n if (event === 'join') {\n this.sendMessage(JSON.stringify({\n \"raisehand\": !!document.querySelector(\n '[data-peerid=\"' + this.peerid + '\"] a.hidden[data-action=\"raisehand\"]'\n )\n }));\n }\n }\n }\n );\n },\n error: (error) => {\n getString('serverlost', 'block_deft').done((message) => {\n Toast.add(message, {'type': 'info'});\n });\n Log.debug(error);\n this.restart = true;\n },\n destroyed: function() {\n window.close();\n }\n }\n );\n }\n });\n }\n\n /**\n * Register the room\n *\n * @param {object} pluginHandle\n * @return {Promise}\n */\n register(pluginHandle) {\n // Try a registration\n return Ajax.call([{\n args: {\n handle: pluginHandle.getId(),\n id: Number(this.peerid),\n plugin: pluginHandle.plugin,\n room: this.roomid,\n session: pluginHandle.session.getSessionId(),\n },\n contextid: this.contextid,\n fail: Notification.exception,\n methodname: 'videotimeplugin_live_join_room'\n }])[0];\n }\n\n /**\n * Handle plugin message\n *\n * @param {object} msg msg\n * @param {string} jsep\n */\n onMessage(msg, jsep) {\n const event = msg.audiobridge;\n Log.debug(msg);\n if (event) {\n if (event === \"joined\") {\n // Successfully joined, negotiate WebRTC now\n if (msg.id) {\n Janus.log(\"Successfully joined room \" + msg.room + \" with ID \" + this.peerid);\n Log.debug(\"Successfully joined room \" + msg.room + \" with ID \" + this.peerid);\n if (!this.webrtcUp) {\n this.webrtcUp = true;\n this.audioInput.then(audioStream => {\n // Publish our stream.\n const tracks = [];\n if (audioStream) {\n audioStream.getAudioTracks().forEach(track => {\n tracks.push({\n type: 'audio',\n capture: track,\n recv: true\n });\n });\n } else {\n tracks.push({\n type: 'audio',\n capture: true,\n recv: true\n });\n }\n this.audioBridge.createOffer({\n // We only want bidirectional audio\n tracks: tracks,\n customizeSdp: function(jsep) {\n if (stereo && jsep.sdp.indexOf(\"stereo=1\") == -1) {\n // Make sure that our offer contains stereo too\n jsep.sdp = jsep.sdp.replace(\"useinbandfec=1\", \"useinbandfec=1;stereo=1\");\n }\n },\n success: (jsep) => {\n Janus.debug(\"Got SDP!\", jsep);\n const publish = {request: \"configure\", muted: false};\n this.audioBridge.send({message: publish, jsep: jsep});\n },\n error: function(error) {\n Janus.error(\"WebRTC error:\", error);\n Notification.alert(\"WebRTC error... \", error.message);\n }\n });\n\n return audioStream;\n }).catch(Notification.exception);\n }\n }\n // Any room participant?\n if (msg.participants) {\n this.updateParticipants(msg.participants);\n }\n } else if (event === \"left\") {\n document.querySelector('[data-contextid=\"' + this.contextid + '\"] [data-action=\"join\"]').classList.remove('hidden');\n document.querySelector('[data-contextid=\"' + this.contextid + '\"] [data-action=\"leave\"]').classList.add('hidden');\n } else if (event === \"destroyed\") {\n // The room has been destroyed\n Janus.warn(\"The room has been destroyed!\");\n Notification.alert('', \"The room has been destroyed\");\n } else if (event === \"event\") {\n if (msg.participants) {\n this.updateParticipants(msg.participants);\n } else if (msg.error) {\n if (msg.error_code === 485) {\n // This is a \"no such room\" error: give a more meaningful description\n Notification.alert(\n \"

Room \" + this.roomid + \" is not configured.\"\n );\n } else {\n Notification.alert(msg.error_code, msg.error);\n }\n return;\n }\n if (msg.leaving) {\n // One of the participants has gone away?\n const leaving = msg.leaving;\n Janus.log(\n \"Participant left: \" + leaving\n );\n document.querySelectorAll(\n '[data-region=\"venue-participants\"] [data-peerid=\"' + leaving + '\"]'\n ).forEach(peer => {\n peer.remove();\n });\n }\n }\n }\n if (jsep) {\n Janus.debug(\"Handling SDP as well...\", jsep);\n this.audioBridge.handleRemoteJsep({jsep: jsep});\n }\n }\n\n processSignal() {\n return;\n }\n\n /**\n * Update participants display for audio bridge\n *\n * @param {array} list List of participants returned by plugin\n */\n updateParticipants(list) {\n Janus.debug(\"Got a list of participants:\", list);\n for (const f in list) {\n const id = list[f].id,\n display = list[f].display,\n setup = list[f].setup,\n muted = list[f].muted;\n Janus.debug(\" >> [\" + id + \"] \" + display + \" (setup=\" + setup + \", muted=\" + muted + \")\");\n if (\n !document.querySelector('[data-region=\"venue-participants\"] [data-peerid=\"' + id + '\"]')\n && Number(this.peerid) != Number(id)\n ) {\n // Add to the participants list\n this.peerAudioPlayer(id);\n }\n }\n }\n\n /**\n * Transfer signals with signal server\n */\n sendSignals() {\n\n if (this.throttled || !navigator.onLine) {\n return;\n }\n\n const time = Date.now();\n if (this.lastUpdate + 200 > time) {\n this.throttled = true;\n setTimeout(() => {\n this.throttled = false;\n }, this.lastUpdate + 250 - time);\n this.sendSignals();\n return;\n }\n this.lastUpdate = time;\n\n Ajax.call([{\n args: {\n contextid: this.contextid\n },\n contextid: this.contextid,\n done: response => {\n response.settings.forEach(peer => {\n if (peer.id == Number(this.peerid)) {\n if (peer.status) {\n // Release microphone.\n clearInterval(this.meterId);\n this.audioInput.then(audioStream => {\n if (audioStream) {\n audioStream.getAudioTracks().forEach(track => {\n track.stop();\n });\n }\n return audioStream;\n }).catch(Log.debug);\n\n // Close connections.\n this.janus.destroy();\n\n document.querySelectorAll(\n '[data-region=\"deft-venue\"] [data-peerid=\"' + this.peerid\n + '\"]'\n ).forEach(venue => {\n const e = new Event('venueclosed', {bubbles: true});\n venue.dispatchEvent(e);\n });\n\n this.socket.disconnect();\n\n window.close();\n return;\n }\n this.mute(peer.mute);\n }\n document.querySelectorAll(\n '[data-contextid=\"' + this.contextid + '\"] [data-peerid=\"' + peer.id +\n '\"] [data-action=\"mute\"], [data-contextid=\"' + this.contextid + '\"] [data-peerid=\"' + peer.id\n + '\"] [data-action=\"unmute\"]'\n ).forEach(button => {\n if (peer.mute == (button.getAttribute('data-action') == 'mute')) {\n button.classList.add('hidden');\n } else {\n button.classList.remove('hidden');\n }\n });\n });\n document.querySelectorAll('[data-region=\"venue-participants\"] [data-peerid]').forEach(peer => {\n if (!response.peers.includes(Number(peer.getAttribute('data-peerid')))) {\n peer.remove();\n }\n });\n if (!response.peers.includes(Number(this.peerid))) {\n return;\n }\n if (this.restart) {\n getString('reconnecting', 'block_deft').done((message) => {\n Toast.add(message, {'type': 'info'});\n });\n this.restart = false;\n this.startConnection();\n }\n },\n fail: Notification.exception,\n methodname: 'videotimetab_venue_status'\n }]);\n }\n\n /**\n * Send a message through data channel to peers\n *\n * @param {string} text\n */\n sendMessage(text) {\n if (text && text !== \"\" && this.textroom) {\n const message = {\n textroom: \"message\",\n transaction: Janus.randomString(12),\n room: this.roomid,\n text: text\n };\n this.textroom.data({\n text: JSON.stringify(message),\n error: Log.debug,\n });\n }\n }\n\n /**\n * Subscribe to feed\n *\n */\n subscribeTo() {\n return;\n }\n\n /**\n * Close connection when peer removed\n */\n handleClose() {\n if (this.janus) {\n this.janus.destroy();\n this.janus = null;\n }\n\n document.querySelector('body').removeEventListener('click', handleClick);\n\n\n if (this.remoteFeed && this.remoteFeed.janus) {\n this.remoteFeed.janus.destroy();\n this.remoteFeed = null;\n }\n }\n\n /**\n * Return audio player for peer\n *\n * @param {int} peerid Peer id\n * @returns {Promise} Resolve to audio player node\n */\n peerAudioPlayer(peerid) {\n const usernode = document.querySelector('[data-region=\"venue-participants\"] div[data-peerid=\"' + peerid + '\"] audio');\n if (usernode) {\n return Promise.resolve(usernode);\n } else {\n const node = document.createElement('div');\n node.setAttribute('data-peerid', peerid);\n if (document.querySelector('body#page-blocks-deft-venue')) {\n node.setAttribute('class', 'col col-12 col-sm-6 col-md-4 col-lg-3 p-2');\n } else {\n node.setAttribute('class', 'col col-12 col-sm-6 col-md-4 p-2');\n }\n window.setTimeout(() => {\n node.querySelectorAll('img.card-img-top').forEach(image => {\n image.setAttribute('height', null);\n image.setAttribute('width', null);\n });\n });\n return Fragment.loadFragment(\n 'videotimetab_venue',\n 'venue',\n this.contextid,\n {\n peerid: peerid\n }\n ).done((userinfo) => {\n if (!document.querySelector('[data-region=\"venue-participants\"] div[data-peerid=\"' + peerid + '\"] audio')) {\n document.querySelector('[data-region=\"venue-participants\"]').appendChild(node);\n node.innerHTML = userinfo;\n }\n }).then(() => {\n return document.querySelector('[data-region=\"venue-participants\"] div[data-peerid=\"' + peerid + '\"] audio');\n }).catch(Notification.exception);\n }\n }\n\n /**\n * Handle click for mute\n *\n * @param {Event} e Button click\n */\n handleMuteButtons(e) {\n const button = e.target.closest(\n 'a[data-action=\"mute\"], a[data-action=\"unmute\"]'\n );\n if (button) {\n const action = button.getAttribute('data-action'),\n peerid = button.closest('[data-peerid]').getAttribute('data-peerid');\n e.stopPropagation();\n e.preventDefault();\n if (!button.closest('[data-region=\"venue-participants\"]')) {\n this.audioInput.then(audioStream => {\n if (audioStream) {\n Ajax.call([{\n args: {\n contextid: this.contextid,\n mute: action == 'mute',\n \"status\": false\n },\n fail: Notification.exception,\n methodname: 'videotimetab_venue_settings'\n }]);\n } else if (action == 'unmute') {\n this.audioInput = navigator.mediaDevices.getUserMedia({\n audio: {\n autoGainControl: this.autogaincontrol,\n echoCancellation: this.echocancellation,\n noiseSuppression: this.noisesuppression,\n sampleRate: this.samplerate\n },\n video: false\n }).then(audioStream => {\n\n Ajax.call([{\n args: {\n mute: false,\n \"status\": false\n },\n fail: Notification.exception,\n methodname: 'videotimetab_venue_settings'\n }]);\n\n this.monitorVolume(audioStream);\n\n return audioStream;\n }).catch(Log.debug);\n }\n\n return audioStream;\n }).catch(Notification.exception);\n } else {\n Ajax.call([{\n args: {\n contextid: this.contextid,\n mute: true,\n peerid: peerid,\n \"status\": false\n },\n fail: Notification.exception,\n methodname: 'videotimetab_venue_settings'\n }]);\n }\n button.closest('[data-peerid]').querySelectorAll('[data-action=\"mute\"], [data-action=\"unmute\"]').forEach(option => {\n if (option.getAttribute('data-action') == action) {\n option.classList.add('hidden');\n } else {\n option.classList.remove('hidden');\n }\n });\n }\n }\n\n /**\n * Handle hand raise buttons\n *\n * @param {Event} e Click event\n */\n handleRaiseHand(e) {\n const button = e.target.closest(\n '[data-action=\"raisehand\"], [data-action=\"lowerhand\"]'\n );\n if (button && !button.closest('[data-region=\"venue-participants\"]')) {\n const action = button.getAttribute('data-action');\n e.stopPropagation();\n e.preventDefault();\n if (action == 'raisehand') {\n document.querySelector('body').classList.add('videotimetab_raisehand');\n } else {\n document.querySelector('body').classList.remove('videotimetab_raisehand');\n }\n document.querySelectorAll('a[data-action=\"raisehand\"], a[data-action=\"lowerhand\"]').forEach(button => {\n if (button.getAttribute('data-action') == action) {\n button.classList.add('hidden');\n } else {\n button.classList.remove('hidden');\n }\n });\n Ajax.call([{\n args: {\n contextid: this.contextid,\n \"status\": action == 'raisehand'\n },\n fail: Notification.exception,\n methodname: 'videotimetab_venue_raise_hand'\n }]);\n this.sendMessage(JSON.stringify({\"raisehand\": action == 'raisehand'}));\n }\n }\n\n /**\n * Send a message through data channel to peers\n *\n * @param {string} text\n */\n sendMessage(text) {\n if (\n text\n &&\n text !== \"\"\n && this.textroom\n && document.querySelector('[data-contextid=\"' + this.contextid + '\"] .hidden[data-action=\"join\"]')\n ) {\n const message = {\n textroom: \"message\",\n transaction: Janus.randomString(12),\n room: this.roomid,\n text: text\n };\n this.textroom.data({\n text: JSON.stringify(message),\n error: Log.debug,\n });\n }\n }\n}\n\n/**\n * Handle click event\n *\n * @param {Event} e Click event\n */\nconst handleClick = function(e) {\n const button = e.target.closest('[data-contextid] a[data-action=\"join\"], [data-contextid] a[data-action=\"leave\"]');\n if (button) {\n const action = button.getAttribute('data-action'),\n contextid = button.closest('[data-contextid]').getAttribute('data-contextid'),\n room = rooms[contextid];\n e.stopPropagation();\n e.preventDefault();\n\n document.querySelectorAll(\n '[data-contextid] a[data-action=\"join\"], [data-contextid] a[data-action=\"leave\"]'\n ).forEach(button => {\n if (contextid == button.closest('[data-contextid]').getAttribute('data-contextid')) {\n if (action == button.getAttribute('data-action')) {\n button.classList.add('hidden');\n } else {\n button.classList.remove('hidden');\n }\n }\n });\n\n if (action == 'leave') {\n const transaction = Janus.randomString(12),\n leave = {\n textroom: \"leave\",\n transaction: transaction,\n room: room.manager.roomid\n };\n Ajax.call([{\n args: {\n contextid: room.manager.contextid,\n mute: true,\n \"status\": true\n },\n fail: Notification.exception,\n methodname: 'videotimetab_venue_settings'\n }]);\n room.manager.audioBridge.send({\n message: {\n request: 'leave'\n }\n });\n room.manager.textroom.data({\n text: JSON.stringify(leave),\n error: function(reason) {\n Notification.alert('Error', reason);\n }\n });\n } else if (room) {\n if (room.manager.audioBridge) {\n const transaction = Janus.randomString(12),\n join = {\n textroom: \"join\",\n transaction: transaction,\n room: Number(room.manager.roomid),\n display: '',\n username: String(room.manager.peerid)\n };\n room.manager.textroom.data({\n text: JSON.stringify(join),\n error: function(reason) {\n Notification.alert('Error', reason);\n }\n });\n room.manager.register(room.manager.audioBridge).then(() => {\n const transaction = Janus.randomString(12),\n configure = {\n audiobridge: \"configure\",\n mute: false,\n transaction: transaction\n };\n room.manager.audiobridge.send({\n message: JSON.stringify(configure),\n error: function(reason) {\n Notification.alert('Error', reason);\n }\n });\n });\n } else {\n setTimeout(() => {\n room.manager.startConnection();\n });\n }\n }\n }\n};\n"],"names":["rooms","JanusManager","document","querySelector","removeEventListener","this","handleMuteButtons","bind","addEventListener","handleRaiseHand","closeConnections","window","onbeforeunload","audioInput","Promise","resolve","socket","Socket","contextid","token","subscribe","_this","sendSignals","peerid","room","started","call","methodname","args","done","response","_this2","iceservers","JSON","parse","roomid","Number","server","String","manager","iceServers","startConnection","fail","Notification","exception","transactions","handleClose","handleClick","navigator","mediaDevices","getUserMedia","audio","autoGainControl","autogaincontrol","echoCancellation","echocancellation","noiseSuppression","noisesuppression","sampleRate","samplerate","video","catch","mute","then","monitorVolume","Log","debug","init","callback","janus","Janus","success","attach","plugin","opaqueId","randomString","pluginHandle","audioBridge","session","getSessionId","register","error","alert","onmessage","onMessage","onremotetrack","track","mid","on","metadata","reason","remoteStream","kind","MediaStream","attachMediaStream","getElementById","textroom","log","getPlugin","getId","body","request","send","message","msg","jsep","error_code","createAnswer","tracks","type","ondataopen","label","protocol","transaction","username","display","data","text","stringify","ondata","event","from","handleMessage","sendMessage","Toast","add","restart","destroyed","close","Ajax","handle","id","audiobridge","webrtcUp","audioStream","getAudioTracks","forEach","push","capture","recv","_this3","createOffer","customizeSdp","muted","participants","updateParticipants","classList","remove","warn","leaving","querySelectorAll","peer","handleRemoteJsep","list","f","setup","peerAudioPlayer","throttled","onLine","time","Date","now","lastUpdate","setTimeout","_this4","settings","status","clearInterval","meterId","stop","destroy","venue","e","Event","bubbles","dispatchEvent","disconnect","button","getAttribute","peers","includes","remoteFeed","usernode","node","createElement","setAttribute","image","Fragment","loadFragment","userinfo","appendChild","innerHTML","target","closest","action","stopPropagation","preventDefault","_this5","option","VenueManagerBase","leave","join","configure"],"mappings":"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"} \ No newline at end of file diff --git a/tab/venue/amd/src/venue_manager.js b/tab/venue/amd/src/venue_manager.js new file mode 100644 index 00000000..36691f30 --- /dev/null +++ b/tab/venue/amd/src/venue_manager.js @@ -0,0 +1,828 @@ +/** + * Manage venue connections + * + * @module videotimetab_venue/venue_manager + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from "core/ajax"; +import Fragment from "core/fragment"; +import {get_string as getString} from 'core/str'; +import Janus from 'block_deft/janus-gateway'; +import Log from "core/log"; +import Notification from "core/notification"; +import Socket from "videotimeplugin_live/socket"; +import * as Toast from 'core/toast'; +import VenueManagerBase from "block_deft/janus_venue"; + +var rooms = {}, + stereo = false; + +export default class JanusManager extends VenueManagerBase { + /** + * Add event listeners + */ + addListeners() { + + document.querySelector('body').removeEventListener('click', this.handleMuteButtons.bind(this)); + document.querySelector('body').addEventListener('click', this.handleMuteButtons.bind(this)); + + document.querySelector('body').removeEventListener('click', this.handleRaiseHand.bind(this)); + document.querySelector('body').addEventListener('click', this.handleRaiseHand.bind(this)); + + document.querySelector('body').removeEventListener('click', this.closeConnections.bind(this)); + document.querySelector('body').addEventListener('click', this.closeConnections.bind(this)); + + window.onbeforeunload = this.closeConnections.bind(this); + + this.audioInput = Promise.resolve(null); + + this.socket = new Socket(this.contextid, this.token); + this.socket.subscribe(() => { + this.sendSignals(); + }); + } + + + /** + * Start to establish the peer connections + */ + startConnection() { + const contextid = this.contextid, + peerid = this.peerid, + room = rooms[contextid]; + if (!this.started) { + Ajax.call([{ + methodname: 'videotimeplugin_live_get_room', + args: {contextid: contextid}, + done: (response) => { + this.iceservers = JSON.parse(response.iceservers); + this.roomid = Number(response.roomid); + this.server = response.server; + + rooms[String(contextid)] = { + contextid: contextid, + manager: this, + peerid: peerid, + roomid: response.roomid, + server: response.server, + iceServers: JSON.parse(response.iceservers) + }; + this.startConnection(); + }, + fail: Notification.exception + }]); + this.started = true; + } + if (!room) { + return; + } + this.transactions = {}; + + document.querySelector('body').removeEventListener('venueclosed', this.handleClose.bind(this)); + document.querySelector('body').addEventListener('venueclosed', this.handleClose.bind(this)); + + document.querySelector('body').removeEventListener('click', handleClick); + document.querySelector('body').addEventListener('click', handleClick); + + if (!document.querySelector('[data-contextid="' + this.contextid + '"] .hidden[data-action="join"]')) { + return; + } + + if (document.querySelector('[data-contextid="' + this.contextid + '"] .hidden[data-action="unmute"]')) { + this.audioInput = navigator.mediaDevices.getUserMedia({ + audio: { + autoGainControl: this.autogaincontrol, + echoCancellation: this.echocancellation, + noiseSuppression: this.noisesuppression, + sampleRate: this.samplerate + }, + video: false + }).catch(() => { + Ajax.call([{ + args: { + mute: true, + "status": false + }, + fail: Notification.exception, + methodname: 'videotimetab_venue_settings' + }]); + + return false; + }); + this.audioInput.then(this.monitorVolume.bind(this)).catch(Log.debug); + } else { + this.audioInput = Promise.resolve(null); + } + + // Initialize the library (all console debuggers enabled) + Janus.init({ + debug: "none", callback: () => { + // Create session. + this.janus = new Janus( + { + server: this.server, + iceServers: this.iceServers, + success: () => { + // Attach audiobridge plugin. + this.janus.attach( + { + plugin: "janus.plugin.audiobridge", + opaqueId: "audioroom-" + Janus.randomString(12), + success: pluginHandle => { + this.audioBridge = pluginHandle; + Log.debug(pluginHandle.session.getSessionId()); + this.register(pluginHandle); + }, + error: function(error) { + Janus.error(" -- Error attaching plugin...", error); + Notification.alert('', "Error attaching plugin... " + error); + }, + onmessage: this.onMessage.bind(this), + onremotetrack: (track, mid, on, metadata) => { + Janus.debug( + "Remote track (mid=" + mid + ") " + + (on ? "added" : "removed") + + (metadata ? " (" + metadata.reason + ") " : "") + ":", track + ); + if (this.remoteStream || track.kind !== "audio") { + return; + } + if (!on) { + // Track removed, get rid of the stream and the rendering + this.remoteStream = null; + return; + } + this.remoteStream = new MediaStream([track]); + Janus.attachMediaStream(document.getElementById('roomaudio'), this.remoteStream); + } + } + ); + this.janus.attach( + { + plugin: "janus.plugin.textroom", + opaqueId: "textroom-" + Janus.randomString(12), + success: pluginHandle => { + this.textroom = pluginHandle; + Janus.log("Plugin attached! (" + this.textroom.getPlugin() + + ", id=" + this.textroom.getId() + ")"); + // Setup the DataChannel + const body = {request: "setup"}; + Janus.debug("Sending message:", body); + this.textroom.send({message: body}); + }, + error: function(error) { + Notification.alert('', error); + Janus.error(" -- Error attaching plugin...", error); + }, + onmessage: (msg, jsep) => { + Janus.debug(" ::: Got a message :::", msg); + if (msg.error) { + Notification.alert(msg.error_code, msg.error); + } + + if (jsep) { + // Answer + this.textroom.createAnswer( + { + jsep: jsep, + // We only use datachannels + tracks: [ + {type: 'data'} + ], + success: (jsep) => { + Janus.debug("Got SDP!", jsep); + const body = {request: "ack"}; + this.textroom.send({message: body, jsep: jsep}); + }, + error: function(error) { + Janus.error("WebRTC error:", error); + } + } + ); + } + }, + // eslint-disable-next-line no-unused-vars + ondataopen: (label, protocol) => { + const transaction = Janus.randomString(12), + register = { + textroom: "join", + transaction: transaction, + room: this.roomid, + username: String(this.peerid), + display: '', + }; + this.textroom.data({ + text: JSON.stringify(register), + error: function(reason) { + Notification.alert('Error', reason); + } + }); + }, + ondata: (data) => { + Janus.debug("We got data from the DataChannel!", data); + const message = JSON.parse(data), + event = message.textroom, + transaction = message.transaction; + if (transaction && this.transactions[transaction]) { + this.transactions[transaction](message); + delete this.transactions[transaction]; + } + + if (event === 'message' && message.from != this.peerid) { + this.handleMessage(message.from, {data: message.text}); + } + if (event === 'error') { + Log.debug(message); + } + if (event === 'join') { + this.sendMessage(JSON.stringify({ + "raisehand": !!document.querySelector( + '[data-peerid="' + this.peerid + '"] a.hidden[data-action="raisehand"]' + ) + })); + } + } + } + ); + }, + error: (error) => { + getString('serverlost', 'block_deft').done((message) => { + Toast.add(message, {'type': 'info'}); + }); + Log.debug(error); + this.restart = true; + }, + destroyed: function() { + window.close(); + } + } + ); + } + }); + } + + /** + * Register the room + * + * @param {object} pluginHandle + * @return {Promise} + */ + register(pluginHandle) { + // Try a registration + return Ajax.call([{ + args: { + handle: pluginHandle.getId(), + id: Number(this.peerid), + plugin: pluginHandle.plugin, + room: this.roomid, + session: pluginHandle.session.getSessionId(), + }, + contextid: this.contextid, + fail: Notification.exception, + methodname: 'videotimeplugin_live_join_room' + }])[0]; + } + + /** + * Handle plugin message + * + * @param {object} msg msg + * @param {string} jsep + */ + onMessage(msg, jsep) { + const event = msg.audiobridge; + Log.debug(msg); + if (event) { + if (event === "joined") { + // Successfully joined, negotiate WebRTC now + if (msg.id) { + Janus.log("Successfully joined room " + msg.room + " with ID " + this.peerid); + Log.debug("Successfully joined room " + msg.room + " with ID " + this.peerid); + if (!this.webrtcUp) { + this.webrtcUp = true; + this.audioInput.then(audioStream => { + // Publish our stream. + const tracks = []; + if (audioStream) { + audioStream.getAudioTracks().forEach(track => { + tracks.push({ + type: 'audio', + capture: track, + recv: true + }); + }); + } else { + tracks.push({ + type: 'audio', + capture: true, + recv: true + }); + } + this.audioBridge.createOffer({ + // We only want bidirectional audio + tracks: tracks, + customizeSdp: function(jsep) { + if (stereo && jsep.sdp.indexOf("stereo=1") == -1) { + // Make sure that our offer contains stereo too + jsep.sdp = jsep.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1"); + } + }, + success: (jsep) => { + Janus.debug("Got SDP!", jsep); + const publish = {request: "configure", muted: false}; + this.audioBridge.send({message: publish, jsep: jsep}); + }, + error: function(error) { + Janus.error("WebRTC error:", error); + Notification.alert("WebRTC error... ", error.message); + } + }); + + return audioStream; + }).catch(Notification.exception); + } + } + // Any room participant? + if (msg.participants) { + this.updateParticipants(msg.participants); + } + } else if (event === "left") { + document.querySelector('[data-contextid="' + this.contextid + '"] [data-action="join"]').classList.remove('hidden'); + document.querySelector('[data-contextid="' + this.contextid + '"] [data-action="leave"]').classList.add('hidden'); + } else if (event === "destroyed") { + // The room has been destroyed + Janus.warn("The room has been destroyed!"); + Notification.alert('', "The room has been destroyed"); + } else if (event === "event") { + if (msg.participants) { + this.updateParticipants(msg.participants); + } else if (msg.error) { + if (msg.error_code === 485) { + // This is a "no such room" error: give a more meaningful description + Notification.alert( + "

Room " + this.roomid + " is not configured." + ); + } else { + Notification.alert(msg.error_code, msg.error); + } + return; + } + if (msg.leaving) { + // One of the participants has gone away? + const leaving = msg.leaving; + Janus.log( + "Participant left: " + leaving + ); + document.querySelectorAll( + '[data-region="venue-participants"] [data-peerid="' + leaving + '"]' + ).forEach(peer => { + peer.remove(); + }); + } + } + } + if (jsep) { + Janus.debug("Handling SDP as well...", jsep); + this.audioBridge.handleRemoteJsep({jsep: jsep}); + } + } + + processSignal() { + return; + } + + /** + * Update participants display for audio bridge + * + * @param {array} list List of participants returned by plugin + */ + updateParticipants(list) { + Janus.debug("Got a list of participants:", list); + for (const f in list) { + const id = list[f].id, + display = list[f].display, + setup = list[f].setup, + muted = list[f].muted; + Janus.debug(" >> [" + id + "] " + display + " (setup=" + setup + ", muted=" + muted + ")"); + if ( + !document.querySelector('[data-region="venue-participants"] [data-peerid="' + id + '"]') + && Number(this.peerid) != Number(id) + ) { + // Add to the participants list + this.peerAudioPlayer(id); + } + } + } + + /** + * Transfer signals with signal server + */ + sendSignals() { + + if (this.throttled || !navigator.onLine) { + return; + } + + const time = Date.now(); + if (this.lastUpdate + 200 > time) { + this.throttled = true; + setTimeout(() => { + this.throttled = false; + }, this.lastUpdate + 250 - time); + this.sendSignals(); + return; + } + this.lastUpdate = time; + + Ajax.call([{ + args: { + contextid: this.contextid + }, + contextid: this.contextid, + done: response => { + response.settings.forEach(peer => { + if (peer.id == Number(this.peerid)) { + if (peer.status) { + // Release microphone. + clearInterval(this.meterId); + this.audioInput.then(audioStream => { + if (audioStream) { + audioStream.getAudioTracks().forEach(track => { + track.stop(); + }); + } + return audioStream; + }).catch(Log.debug); + + // Close connections. + this.janus.destroy(); + + document.querySelectorAll( + '[data-region="deft-venue"] [data-peerid="' + this.peerid + + '"]' + ).forEach(venue => { + const e = new Event('venueclosed', {bubbles: true}); + venue.dispatchEvent(e); + }); + + this.socket.disconnect(); + + window.close(); + return; + } + this.mute(peer.mute); + } + document.querySelectorAll( + '[data-contextid="' + this.contextid + '"] [data-peerid="' + peer.id + + '"] [data-action="mute"], [data-contextid="' + this.contextid + '"] [data-peerid="' + peer.id + + '"] [data-action="unmute"]' + ).forEach(button => { + if (peer.mute == (button.getAttribute('data-action') == 'mute')) { + button.classList.add('hidden'); + } else { + button.classList.remove('hidden'); + } + }); + }); + document.querySelectorAll('[data-region="venue-participants"] [data-peerid]').forEach(peer => { + if (!response.peers.includes(Number(peer.getAttribute('data-peerid')))) { + peer.remove(); + } + }); + if (!response.peers.includes(Number(this.peerid))) { + return; + } + if (this.restart) { + getString('reconnecting', 'block_deft').done((message) => { + Toast.add(message, {'type': 'info'}); + }); + this.restart = false; + this.startConnection(); + } + }, + fail: Notification.exception, + methodname: 'videotimetab_venue_status' + }]); + } + + /** + * Send a message through data channel to peers + * + * @param {string} text + */ + sendMessage(text) { + if (text && text !== "" && this.textroom) { + const message = { + textroom: "message", + transaction: Janus.randomString(12), + room: this.roomid, + text: text + }; + this.textroom.data({ + text: JSON.stringify(message), + error: Log.debug, + }); + } + } + + /** + * Subscribe to feed + * + */ + subscribeTo() { + return; + } + + /** + * Close connection when peer removed + */ + handleClose() { + if (this.janus) { + this.janus.destroy(); + this.janus = null; + } + + document.querySelector('body').removeEventListener('click', handleClick); + + + if (this.remoteFeed && this.remoteFeed.janus) { + this.remoteFeed.janus.destroy(); + this.remoteFeed = null; + } + } + + /** + * Return audio player for peer + * + * @param {int} peerid Peer id + * @returns {Promise} Resolve to audio player node + */ + peerAudioPlayer(peerid) { + const usernode = document.querySelector('[data-region="venue-participants"] div[data-peerid="' + peerid + '"] audio'); + if (usernode) { + return Promise.resolve(usernode); + } else { + const node = document.createElement('div'); + node.setAttribute('data-peerid', peerid); + if (document.querySelector('body#page-blocks-deft-venue')) { + node.setAttribute('class', 'col col-12 col-sm-6 col-md-4 col-lg-3 p-2'); + } else { + node.setAttribute('class', 'col col-12 col-sm-6 col-md-4 p-2'); + } + window.setTimeout(() => { + node.querySelectorAll('img.card-img-top').forEach(image => { + image.setAttribute('height', null); + image.setAttribute('width', null); + }); + }); + return Fragment.loadFragment( + 'videotimetab_venue', + 'venue', + this.contextid, + { + peerid: peerid + } + ).done((userinfo) => { + if (!document.querySelector('[data-region="venue-participants"] div[data-peerid="' + peerid + '"] audio')) { + document.querySelector('[data-region="venue-participants"]').appendChild(node); + node.innerHTML = userinfo; + } + }).then(() => { + return document.querySelector('[data-region="venue-participants"] div[data-peerid="' + peerid + '"] audio'); + }).catch(Notification.exception); + } + } + + /** + * Handle click for mute + * + * @param {Event} e Button click + */ + handleMuteButtons(e) { + const button = e.target.closest( + 'a[data-action="mute"], a[data-action="unmute"]' + ); + if (button) { + const action = button.getAttribute('data-action'), + peerid = button.closest('[data-peerid]').getAttribute('data-peerid'); + e.stopPropagation(); + e.preventDefault(); + if (!button.closest('[data-region="venue-participants"]')) { + this.audioInput.then(audioStream => { + if (audioStream) { + Ajax.call([{ + args: { + contextid: this.contextid, + mute: action == 'mute', + "status": false + }, + fail: Notification.exception, + methodname: 'videotimetab_venue_settings' + }]); + } else if (action == 'unmute') { + this.audioInput = navigator.mediaDevices.getUserMedia({ + audio: { + autoGainControl: this.autogaincontrol, + echoCancellation: this.echocancellation, + noiseSuppression: this.noisesuppression, + sampleRate: this.samplerate + }, + video: false + }).then(audioStream => { + + Ajax.call([{ + args: { + mute: false, + "status": false + }, + fail: Notification.exception, + methodname: 'videotimetab_venue_settings' + }]); + + this.monitorVolume(audioStream); + + return audioStream; + }).catch(Log.debug); + } + + return audioStream; + }).catch(Notification.exception); + } else { + Ajax.call([{ + args: { + contextid: this.contextid, + mute: true, + peerid: peerid, + "status": false + }, + fail: Notification.exception, + methodname: 'videotimetab_venue_settings' + }]); + } + button.closest('[data-peerid]').querySelectorAll('[data-action="mute"], [data-action="unmute"]').forEach(option => { + if (option.getAttribute('data-action') == action) { + option.classList.add('hidden'); + } else { + option.classList.remove('hidden'); + } + }); + } + } + + /** + * Handle hand raise buttons + * + * @param {Event} e Click event + */ + handleRaiseHand(e) { + const button = e.target.closest( + '[data-action="raisehand"], [data-action="lowerhand"]' + ); + if (button && !button.closest('[data-region="venue-participants"]')) { + const action = button.getAttribute('data-action'); + e.stopPropagation(); + e.preventDefault(); + if (action == 'raisehand') { + document.querySelector('body').classList.add('videotimetab_raisehand'); + } else { + document.querySelector('body').classList.remove('videotimetab_raisehand'); + } + document.querySelectorAll('a[data-action="raisehand"], a[data-action="lowerhand"]').forEach(button => { + if (button.getAttribute('data-action') == action) { + button.classList.add('hidden'); + } else { + button.classList.remove('hidden'); + } + }); + Ajax.call([{ + args: { + contextid: this.contextid, + "status": action == 'raisehand' + }, + fail: Notification.exception, + methodname: 'videotimetab_venue_raise_hand' + }]); + this.sendMessage(JSON.stringify({"raisehand": action == 'raisehand'})); + } + } + + /** + * Send a message through data channel to peers + * + * @param {string} text + */ + sendMessage(text) { + if ( + text + && + text !== "" + && this.textroom + && document.querySelector('[data-contextid="' + this.contextid + '"] .hidden[data-action="join"]') + ) { + const message = { + textroom: "message", + transaction: Janus.randomString(12), + room: this.roomid, + text: text + }; + this.textroom.data({ + text: JSON.stringify(message), + error: Log.debug, + }); + } + } +} + +/** + * Handle click event + * + * @param {Event} e Click event + */ +const handleClick = function(e) { + const button = e.target.closest('[data-contextid] a[data-action="join"], [data-contextid] a[data-action="leave"]'); + if (button) { + const action = button.getAttribute('data-action'), + contextid = button.closest('[data-contextid]').getAttribute('data-contextid'), + room = rooms[contextid]; + e.stopPropagation(); + e.preventDefault(); + + document.querySelectorAll( + '[data-contextid] a[data-action="join"], [data-contextid] a[data-action="leave"]' + ).forEach(button => { + if (contextid == button.closest('[data-contextid]').getAttribute('data-contextid')) { + if (action == button.getAttribute('data-action')) { + button.classList.add('hidden'); + } else { + button.classList.remove('hidden'); + } + } + }); + + if (action == 'leave') { + const transaction = Janus.randomString(12), + leave = { + textroom: "leave", + transaction: transaction, + room: room.manager.roomid + }; + Ajax.call([{ + args: { + contextid: room.manager.contextid, + mute: true, + "status": true + }, + fail: Notification.exception, + methodname: 'videotimetab_venue_settings' + }]); + room.manager.audioBridge.send({ + message: { + request: 'leave' + } + }); + room.manager.textroom.data({ + text: JSON.stringify(leave), + error: function(reason) { + Notification.alert('Error', reason); + } + }); + } else if (room) { + if (room.manager.audioBridge) { + const transaction = Janus.randomString(12), + join = { + textroom: "join", + transaction: transaction, + room: Number(room.manager.roomid), + display: '', + username: String(room.manager.peerid) + }; + room.manager.textroom.data({ + text: JSON.stringify(join), + error: function(reason) { + Notification.alert('Error', reason); + } + }); + room.manager.register(room.manager.audioBridge).then(() => { + const transaction = Janus.randomString(12), + configure = { + audiobridge: "configure", + mute: false, + transaction: transaction + }; + room.manager.audiobridge.send({ + message: JSON.stringify(configure), + error: function(reason) { + Notification.alert('Error', reason); + } + }); + }); + } else { + setTimeout(() => { + room.manager.startConnection(); + }); + } + } + } +}; diff --git a/tab/venue/backup/moodle2/backup_videotimetab_venue_subplugin.class.php b/tab/venue/backup/moodle2/backup_videotimetab_venue_subplugin.class.php new file mode 100644 index 00000000..d67edc0a --- /dev/null +++ b/tab/venue/backup/moodle2/backup_videotimetab_venue_subplugin.class.php @@ -0,0 +1,53 @@ +. + +/** + * Defines backup_videotimetab_venue_subplugin class + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Defines backup_videotimetab_venue_subplugin class + * + * Provides the step to perform back up of sublugin data + */ +class backup_videotimetab_venue_subplugin extends backup_subplugin { + + /** + * Defined suplugin structure step + */ + protected function define_videotime_subplugin_structure() { + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + $subplugintablesettings = new backup_nested_element('videotimetab_venue', + null, array('videotime')); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subplugintablesettings); + + // Set source to populate the data. + $subplugintablesettings->set_source_table('videotimetab_venue', + array('videotime' => backup::VAR_ACTIVITYID)); + + return $subplugin; + } +} diff --git a/tab/venue/backup/moodle2/restore_videotimetab_venue_subplugin.class.php b/tab/venue/backup/moodle2/restore_videotimetab_venue_subplugin.class.php new file mode 100644 index 00000000..63827406 --- /dev/null +++ b/tab/venue/backup/moodle2/restore_videotimetab_venue_subplugin.class.php @@ -0,0 +1,66 @@ +. + +/** + * videotime restore task + * + * provides all the settings and steps to perform one * complete restore of the activity + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/videotime/backup/moodle2/restore_videotime_stepslib.php'); // Because it exists (must). + +/** + * Define restore step for videotime tab plugin + * + * restore subplugin class that provides the data + * needed to restore one videotimetab_venue subplugin. + */ +class restore_videotimetab_venue_subplugin extends restore_subplugin { + + /** + * Define subplugin structure + * + */ + protected function define_videotime_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor(''); + $elepath = $this->get_pathfor('/videotimetab_venue'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes the videotimetab_venue element, if it is in the file. + * @param array $data the data read from the XML file. + */ + public function process_videotimetab_venue($data) { + global $DB; + + $data = (object)$data; + $oldvideotime = $data->videotime; + $data->videotime = $this->get_new_parentid('videotime'); + $DB->insert_record('videotimetab_venue', $data); + } +} diff --git a/tab/venue/classes/event/audiobridge_launched.php b/tab/venue/classes/event/audiobridge_launched.php new file mode 100644 index 00000000..9341a72c --- /dev/null +++ b/tab/venue/classes/event/audiobridge_launched.php @@ -0,0 +1,90 @@ +. + +namespace videotimetab_venue\event; + +use \core\event\base; + +/** + * The audiobridge launched event + * + * @package videotimetab_venue + * @category event + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class audiobridge_launched extends base { + + /** + * Set all required data properties: + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'videotime'; + } + + /** + * Validate their custom data (such as $this->data['other'], contextlevel, etc.). + * + * Throw \coding_exception or debugging() notice in case of any problems. + */ + protected function validate_data() { + // Override if you want to validate event properties when + // creating new events. + } + + /** + * Returns localised general event name. + * + * Override in subclass, we can not make it static and abstract at the same time. + * + * @return string + */ + public static function get_name() { + return get_string('eventaudiobridgelaunched', 'block_deft'); + } + + /** + * Get backup mappinig + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'videotime', 'restore' => 'id']; + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/videotime/view.php', ['v' => $this->objectid]); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' joined audiobridge in venue tab for Video Time activity with id '$this->objectid'."; + } + +} diff --git a/tab/venue/classes/event/hand_lower_sent.php b/tab/venue/classes/event/hand_lower_sent.php new file mode 100644 index 00000000..6708b7a5 --- /dev/null +++ b/tab/venue/classes/event/hand_lower_sent.php @@ -0,0 +1,79 @@ +. + +namespace videotimetab_venue\event; + +use \core\event\base; + +/** + * The hand lower event + * + * @package videotimetab_venue + * @category event + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class hand_lower_sent extends base { + + /** + * Set all required data properties: + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'videotime'; + } + + /** + * Returns localised general event name. + * + * Override in subclass, we can not make it static and abstract at the same time. + * + * @return string + */ + public static function get_name() { + return get_string('eventhandlowersent', 'block_deft'); + } + + /** + * Get backup mappinig + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'videotime', 'restore' => 'id']; + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/videotime/view.php', ['v' => $this->objectid]); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' has lowered hand in venue tab of Video Time activity with id '$this->objectid'."; + } +} diff --git a/tab/venue/classes/event/hand_raise_sent.php b/tab/venue/classes/event/hand_raise_sent.php new file mode 100644 index 00000000..898dc2d3 --- /dev/null +++ b/tab/venue/classes/event/hand_raise_sent.php @@ -0,0 +1,79 @@ +. + +namespace videotimetab_venue\event; + +use \core\event\base; + +/** + * The hand raise event + * + * @package videotimetab_venue + * @category event + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class hand_raise_sent extends base { + + /** + * Set all required data properties: + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'videotime'; + } + + /** + * Returns localised general event name. + * + * Override in subclass, we can not make it static and abstract at the same time. + * + * @return string + */ + public static function get_name() { + return get_string('eventhandraisesent', 'block_deft'); + } + + /** + * Get backup mappinig + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'videotime', 'restore' => 'id']; + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/videotime/view.php', ['v' => $this->objectid]); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' has raised hand in venue tab of Video Time activity with id '$this->objectid'."; + } +} diff --git a/tab/venue/classes/external/get_room.php b/tab/venue/classes/external/get_room.php new file mode 100644 index 00000000..79980f73 --- /dev/null +++ b/tab/venue/classes/external/get_room.php @@ -0,0 +1,95 @@ +. + +namespace videotimetab_venue\external; + +use videotimeplugin_live\socket; +use block_deft\venue_manager; +use cache; +use context; +use context_module; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use videotimeplugin_live\janus_room; + +/** + * External function for joining Janus gateway + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_room extends external_api { + + /** + * Get parameter definition for get room + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'contextid' => new external_value(PARAM_INT, 'Context id for videotime mod'), + ] + ); + } + + /** + * Get room information + * + * @param int $contextid Video Time module context id + * @return array + */ + public static function execute($contextid): array { + global $DB, $SESSION; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + ]); + + $context = context::instance_by_id($contextid); + self::validate_context($context); + $cm = get_coursemodule_from_id('videotime', $context->instanceid); + + require_login(); + require_capability('mod/videotime:view', $context); + + $janusroom = new janus_room($cm->instance); + $socket = new socket($context); + + return [ + 'roomid' => $janusroom->get_roomid(), + 'iceservers' => json_encode($socket->ice_servers()), + 'server' => $janusroom->get_server(), + ]; + } + + /** + * Get return definition for hand_raise + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'iceservers' => new external_value(PARAM_TEXT, 'JSON ICE server information'), + 'roomid' => new external_value(PARAM_TEXT, 'Video room id'), + 'server' => new external_value(PARAM_TEXT, 'Server url for room'), + ]); + } +} diff --git a/tab/venue/classes/external/raise_hand.php b/tab/venue/classes/external/raise_hand.php new file mode 100644 index 00000000..2874faa5 --- /dev/null +++ b/tab/venue/classes/external/raise_hand.php @@ -0,0 +1,110 @@ +. + +namespace videotimetab_venue\external; + +use videotimeplugin_live\socket; +use block_deft\venue_manager; +use cache; +use context; +use context_block; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; + +/** + * External function for logging hand raising events + * + * @package videotimetab_venue + * @copyright 2023 Daniel Thies + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class raise_hand extends external_api { + + /** + * Get parameter definition for raise hand + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'contextid' => new external_value(PARAM_INT, 'Block context id'), + 'status' => new external_value(PARAM_BOOL, 'Whether hand should be raised'), + ] + ); + } + + /** + * Log action + * + * @param int $contextid Context id for module + * @param int $status Whether to raise hand + * @return array Status indicator + */ + public static function execute($contextid, $status): array { + global $DB, $SESSION; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + 'status' => $status, + ]); + + $context = context::instance_by_id($contextid); + self::validate_context($context); + + require_login(); + require_capability('block/deft:joinvenue', $context); + + if ( + $context->contextlevel != CONTEXT_MODULE + || !$cm = get_coursemodule_from_id('videotime', $context->instanceid) + ) { + return [ + 'status' => false, + ]; + } + + $params = [ + 'context' => $context, + 'objectid' => $cm->instance, + ]; + + if ($status) { + $event = \videotimetab_venue\event\hand_raise_sent::create($params); + } else { + $event = \videotimetab_venue\event\hand_lower_sent::create($params); + } + $event->trigger(); + + return [ + 'status' => true, + ]; + } + + /** + * Get return definition for hand_raise + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'status' => new external_value(PARAM_BOOL, 'Whether changed'), + ]); + } +} diff --git a/tab/venue/classes/external/venue_settings.php b/tab/venue/classes/external/venue_settings.php new file mode 100644 index 00000000..80358ed5 --- /dev/null +++ b/tab/venue/classes/external/venue_settings.php @@ -0,0 +1,151 @@ +. + +namespace videotimetab_venue\external; + +use videotimeplugin_live\socket; +use block_deft\venue_manager; +use cache; +use context; +use context_block; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use moodle_exception; + +/** + * External function for storing user venue settings + * + * @package videotimetab_venue + * @copyright 2023 Daniel Thies + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class venue_settings extends external_api { + + /** + * Get parameter definition for send_signal. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'contextid' => new external_value(PARAM_INT, 'Block context id'), + 'mute' => new external_value(PARAM_BOOL, 'Whether audio should be muted'), + 'status' => new external_value(PARAM_BOOL, 'Whether the connection should be closed'), + 'peerid' => new external_value(PARAM_INT, 'Some other peer to change', VALUE_DEFAULT, 0), + ] + ); + } + + /** + * Change settings + * + * @param int $contextid Context id for module + * @param int $mute Whether to mute + * @param int $status Whether to close + * @param int $peerid The id of a user's peer changed by manager + * @return array Status indicator + */ + public static function execute($contextid, $mute, $status, $peerid): array { + global $DB, $SESSION; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + 'mute' => $mute, + 'status' => $status, + 'peerid' => $peerid, + ]); + + $context = context::instance_by_id($contextid); + self::validate_context($context); + + require_login(); + require_capability('block/deft:joinvenue', $context); + + if ( + $context->contextlevel != CONTEXT_MODULE + || !$cm = get_coursemodule_from_id('videotime', $context->instanceid) + ) { + return [ + 'status' => false, + ]; + } + + + if (!empty($peerid) && $peerid != $DB->get_field_select('sessions', 'id', 'sid = ?', [session_id()])) { + require_capability('block/deft:moderate', $context); + } else { + $peerid = $DB->get_field_select('sessions', 'id', 'sid = ?', [session_id()]); + } + + if (!$record = $DB->get_record('videotimetab_venue_peer', [ + 'sessionid' => $peerid, + 'videotime' => $cm->instance, + 'status' => 0, + ])) { + throw new moodle_exception('invalidpeer'); + } + + if (($record->mute == $mute) && ($record->status == $status)) { + // No changes needed. + return [ + 'status' => false, + ]; + } + + $record->mute = $mute; + $record->status = $status; + + $DB->update_record('videotimetab_venue_peer', $record); + + $socket = new socket($context); + $socket->dispatch(); + + $params = [ + 'context' => $context, + 'objectid' => $cm->instance, + ]; + + if ($status) { + $event = \block_deft\event\venue_ended::create($params); + } else { + $params['other'] = ['status' => $mute]; + if (!empty($relateduserid)) { + $params['relateduserid'] = $relateduserid; + } + $event = \block_deft\event\mute_switched::create($params); + } + $event->trigger(); + + return [ + 'status' => true, + ]; + } + + /** + * Get return definition for send_signal + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'status' => new external_value(PARAM_BOOL, 'Whether changed'), + ]); + } +} diff --git a/tab/venue/classes/external/venue_status.php b/tab/venue/classes/external/venue_status.php new file mode 100644 index 00000000..ae43855e --- /dev/null +++ b/tab/venue/classes/external/venue_status.php @@ -0,0 +1,114 @@ +. + +namespace videotimetab_venue\external; + +use videotimeplugin_live\socket; +use block_deft\venue_manager; +use cache; +use context; +use context_block; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use moodle_exception; + +/** + * External function for storing user venue settings + * + * @package videotimetab_venue + * @copyright 2023 Daniel Thies + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class venue_status extends external_api { + + /** + * Get parameter definition for venue_status. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'contextid' => new external_value(PARAM_INT, 'Videotime module context id'), + ] + ); + } + + /** + * Get status + * + * @param int $contextid Context id for module + * @return array Current status + */ + public static function execute($contextid): array { + global $DB, $SESSION; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + ]); + + $context = context::instance_by_id($contextid); + self::validate_context($context); + + require_login(); + require_capability('block/deft:joinvenue', $context); + + if ( + $context->contextlevel != CONTEXT_MODULE + || !$cm = get_coursemodule_from_id('videotime', $context->instanceid) + ) { + throw new moodle_exception('invalidcontext'); + } + + + $peerid = $DB->get_field_select('sessions', 'id', 'sid = ?', [session_id()]); + + $records = $DB->get_records('videotimetab_venue_peer', [ + 'status' => 0, + 'videotime' => $cm->instance, + ], '', 'sessionid AS id, status, mute'); + + $record = $records[$peerid]; + + return [ + 'peers' => array_keys($records), + 'settings' => array_values($records), + ]; + } + + /** + * Get return definition for venue_status + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'peers' => new external_multiple_structure( + new external_value(PARAM_INT, 'Currently available peer ids'), + ), + 'settings' => new external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'Peer id'), + 'mute' => new external_value(PARAM_BOOL, 'Whether audio should be muted'), + 'status' => new external_value(PARAM_BOOL, 'Whether connection should be closed'), + ]), + ), + ]); + } +} diff --git a/tab/venue/classes/output/main.php b/tab/venue/classes/output/main.php new file mode 100644 index 00000000..a05462c9 --- /dev/null +++ b/tab/venue/classes/output/main.php @@ -0,0 +1,101 @@ +. + +/** + * Class to render Venue tab + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace videotimetab_venue\output; + +defined('MOODLE_INTERNAL') || die(); + +use cache; +use moodle_url; +use renderable; +use renderer_base; +use stdClass; +use templatable; +use videotimeplugin_live\socket; +use user_picture; + +/** + * Class to render Venue tab + * + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class main implements renderable, templatable { + + /** + * Constructor. + * + * @param int $context The context of the block. + */ + public function __construct($instance) { + global $DB; + + $this->context = $instance->get_context(); + $this->socket = new socket($this->context); + $this->instance = $instance; + + $this->peerid = $DB->get_field('sessions', 'id', ['sid' => session_id()]); + $this->settings = $DB->get_record('videotimetab_venue_peer', [ + 'videotime' => $instance->id, + 'sessionid' => $this->peerid, + 'status' => 0, + ]); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output) { + global $PAGE, $USER; + + $user = clone ($USER); + $user->fullname = fullname($user); + $userpicture = new user_picture($user); + $user->pictureurl = $userpicture->get_url($PAGE, $output); + $user->avatar = $output->user_picture($user, [ + 'class' => 'card-img-top p-1 m-1', + 'link' => false, + 'size' => 36, + ]); + return [ + 'autogaincontrol' => !empty(get_config('block_deft', 'autogaincontrol')), + 'canuse' => has_capability('block/deft:manage', $this->context), + 'contextid' => $this->context->id, + 'echocancellation' => !empty(get_config('block_deft', 'echocancellation')), + 'iceservers' => json_encode($this->socket->ice_servers()), + 'instanceid' => $this->instance->id, + 'mute' => !empty($this->settings->mute), + 'noisesuppression' => !empty(get_config('block_deft', 'noisesuppression')), + 'peerid' => $this->peerid, + 'samplerate' => get_config('block_deft', 'samplerate'), + 'throttle' => get_config('block_deft', 'throttle'), + 'token' => $this->socket->get_token(), + 'roomid' => 0, + 'uniqid' => uniqid(), + 'user' => $user, + ]; + } +} diff --git a/tab/venue/classes/privacy/provider.php b/tab/venue/classes/privacy/provider.php new file mode 100644 index 00000000..dc5a8f99 --- /dev/null +++ b/tab/venue/classes/privacy/provider.php @@ -0,0 +1,44 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace videotimetab_venue\privacy; + +/** + * The videotimetab_venue module does not store any data. + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/tab/venue/classes/tab.php b/tab/venue/classes/tab.php new file mode 100644 index 00000000..6bd40c9e --- /dev/null +++ b/tab/venue/classes/tab.php @@ -0,0 +1,173 @@ +. + +/** + * Tab. + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace videotimetab_venue; + +defined('MOODLE_INTERNAL') || die(); + +use block_contents; +use context_module; +use core_component; +use stdClass; + +require_once("$CFG->dirroot/mod/videotime/lib.php"); + +/** + * Tab. + * + * @package videotimetab_venue + */ +class tab extends \mod_videotime\local\tabs\tab { + + /** + * Get tab panel content + * + * @return string + */ + public function get_tab_content(): string { + global $DB, $OUTPUT; + + $instance = $this->get_instance(); + if ($record = $DB->get_record('videotimetab_venue', array('videotime' => $instance->id))) { + $main = new output\main($instance); + + return $OUTPUT->render($main); + } + } + + /** + * Defines the additional form fields. + * + * @param moodle_form $mform form to modify + */ + public static function add_form_fields($mform) { + $mform->addElement('advcheckbox', 'enable_venue', get_string('pluginname', 'videotimetab_venue'), + get_string('showtab', 'videotime')); + $mform->setDefault('enable_venue', get_config('videotimetab_venue', 'default')); + $mform->disabledIf('enable_venue', 'enabletabs'); + + $mform->addElement('text', 'venuetab_name', get_string('venuetab_name', 'videotimetab_venue')); + $mform->setType('venuetab_name', PARAM_TEXT); + } + + /** + * Saves a settings in database + * + * @param stdClass $data Form data with values to save + */ + public static function save_settings(stdClass $data) { + global $DB; + + if (empty($data->enable_venue)) { + $DB->delete_records('videotimetab_venue', array( + 'videotime' => $data->id, + )); + } else if ($record = $DB->get_record('videotimetab_venue', array('videotime' => $data->id))) { + $record->name = $data->venuetab_name; + $DB->update_record('videotimetab_venue', $record); + } else { + $DB->insert_record('videotimetab_venue', array( + 'videotime' => $data->id, + 'name' => $data->venuetab_name, + )); + } + } + + /** + * Delete settings in database + * + * @param int $id + */ + public static function delete_settings(int $id) { + global $DB; + + $DB->delete_records('videotimetab_venue', array( + 'videotime' => $id, + )); + } + + /** + * Prepares the form before data are set + * + * @param array $defaultvalues + * @param int $instance + */ + public static function data_preprocessing(array &$defaultvalues, int $instance) { + global $DB; + + if (empty($instance)) { + $defaultvalues['enable_venue'] = get_config('videotimetab_venue', 'default'); + } else { + $defaultvalues['enable_venue'] = $DB->record_exists('videotimetab_venue', array('videotime' => $instance)); + } + if (empty($instance)) { + $defaultvalues['enable_venue'] = get_config('videotimetab_venue', 'default'); + } else if ($record = $DB->get_record('videotimetab_venue', array('videotime' => $instance))) { + $defaultvalues['enable_venue'] = 1; + $defaultvalues['venuetab_name'] = $record->name; + } else { + $defaultvalues['enable_venue'] = 0; + } + } + + /** + * Whether tab is enabled and visible + * + * @return bool + */ + public function is_visible(): bool { + global $DB; + + $record = $this->get_instance()->to_record(); + return $this->is_enabled() && $DB->record_exists('videotimetab_venue', array( + 'videotime' => $record->id + )); + } + + /** + * List of missing dependencies needed for plugin to be enabled + */ + public static function added_dependencies() { + global $OUTPUT; + $manager = \core_plugin_manager::instance(); + $plugin = $manager->get_plugin_info('block_deft'); + if ($plugin && $plugin->versiondb > 2022111400) { + return ''; + } + return $OUTPUT->render_from_template('videotimetab_venue/upgrade', []); + } + + /** + * Get label for tab + * + * @return string + */ + public function get_label(): string { + if ($label = $this->get_record()->name) { + return $label; + } + + return parent::get_label(); + } +} diff --git a/tab/venue/db/events.php b/tab/venue/db/events.php new file mode 100644 index 00000000..f10bf634 --- /dev/null +++ b/tab/venue/db/events.php @@ -0,0 +1,40 @@ +. + +/** + * Plugin event observers are registered here. + * + * @package videotimetab_venue + * @category event + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$observers = [ + + [ + 'eventname' => '\\videotimetab_venue\\event\\comment_created', + 'callback' => 'videotimetab_venue\\comment::observe', + 'internal' => true, + ], + [ + 'eventname' => '\\videotimetab_venue\\event\\comment_deleted', + 'callback' => 'videotimetab_venue\\comment::observe', + 'internal' => true, + ], +]; diff --git a/tab/venue/db/install.php b/tab/venue/db/install.php new file mode 100644 index 00000000..be421b25 --- /dev/null +++ b/tab/venue/db/install.php @@ -0,0 +1,35 @@ +. + +/** + * Enable plugin for new install + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_videotime\plugin_manager; + +/** + * Enable this plugin for new installs + * @return bool + */ +function xmldb_videotimetab_venue_install() { + $manager = new plugin_manager('videotimetab'); + + return true; +} diff --git a/tab/venue/db/install.xml b/tab/venue/db/install.xml new file mode 100644 index 00000000..b004bc2c --- /dev/null +++ b/tab/venue/db/install.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+
+
diff --git a/tab/venue/db/services.php b/tab/venue/db/services.php new file mode 100644 index 00000000..5e0c72c3 --- /dev/null +++ b/tab/venue/db/services.php @@ -0,0 +1,54 @@ +. + +/** + * Core external functions and service definitions. + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'videotimetab_venue_settings' => [ + 'classname' => '\\videotimetab_venue\\external\\venue_settings', + 'methodname' => 'execute', + 'description' => 'Change peer settings in venue', + 'type' => 'write', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], + + 'videotimetab_venue_status' => [ + 'classname' => '\\videotimetab_venue\\external\\venue_status', + 'methodname' => 'execute', + 'description' => 'Return peer status in venue', + 'type' => 'read', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], + + 'videotimetab_venue_raise_hand' => [ + 'classname' => '\\videotimetab_venue\\external\\raise_hand', + 'methodname' => 'execute', + 'description' => 'Change status for raised hand', + 'type' => 'write', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], +]; diff --git a/tab/venue/lang/en/videotimetab_venue.php b/tab/venue/lang/en/videotimetab_venue.php new file mode 100644 index 00000000..7e3b5406 --- /dev/null +++ b/tab/venue/lang/en/videotimetab_venue.php @@ -0,0 +1,33 @@ +. + +/** + * Plugin strings are defined here. + * + * @package videotimetab_venue + * @copyright 2021 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['venuetab_name'] = 'Custom tab name'; +$string['default'] = 'Default'; +$string['default_help'] = 'Whether tab is enabled by default'; +$string['label'] = 'Venue'; +$string['pluginname'] = 'Video Time Venue tab'; +$string['privacy:metadata'] = 'The Video Time Venue tab plugin does not store any personal data.'; +$string['upgradeplugin'] = 'This plugin requires installation of Deft response block to enable.'; diff --git a/tab/venue/lib.php b/tab/venue/lib.php new file mode 100644 index 00000000..ae301032 --- /dev/null +++ b/tab/venue/lib.php @@ -0,0 +1,91 @@ +. + +/** + * Library functions for Deft. + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/lti/locallib.php'); + +use videotimetab_venue\output\main; +use videotimetab_venue\socket; + +/** + * Serve the comments as a fragment. + * + * @param array $args List of named arguments for the fragment loader. + * @return string + */ +function videotimetab_venue_output_fragment_content($args) { + global $CFG, $OUTPUT; + + $context = $args['context']; + + if ($context->contextlevel != CONTEXT_MODULE) { + return null; + } + + $main = new \videotimetab_venue\output\main($context); + $data = $main->export_for_template($OUTPUT); + + return '

' . $data['comments'] . '
'; +} + +/** + * Provide venue user information + * + * @param array $args List of named arguments for the fragment loader. + * @return string + */ +function videotimetab_venue_output_fragment_venue($args) { + global $DB, $OUTPUT, $USER, $PAGE; + + + $context = $args['context']; + $peerid = $args['peerid']; + $userid = $DB->get_field('sessions', 'userid', [ + 'id' => $peerid, + ]); + + if (!$user = core_user::get_user($userid)) { + return ''; + } + $url = new moodle_url('/user/view.php', [ + 'id' => $user->id, + 'course' => $context->get_course_context->instance, + ]); + $user->fullname = fullname($user); + $userpicture = new user_picture($user); + $user->pictureurl = $userpicture->get_url($PAGE, $OUTPUT); + $user->avatar = $OUTPUT->user_picture($user, [ + 'class' => 'card-img-top', + 'link' => false, + 'size' => 32, + ]); + $user->manage = has_capability('block/deft:moderate', $context); + $user->profileurl = $url->out(false); + + return $OUTPUT->render_from_template('block_deft/venue_user', [ + 'peerid' => $peerid, + 'user' => $user, + ]); +} diff --git a/tab/venue/settings.php b/tab/venue/settings.php new file mode 100644 index 00000000..7e97bf2e --- /dev/null +++ b/tab/venue/settings.php @@ -0,0 +1,36 @@ +. + +/** + * Plugin administration pages are defined here. + * + * @package videotimetab_venue + * @category admin + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_videotime\videotime_instance; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/videotime/lib.php'); + +$settings->add(new admin_setting_configcheckbox('videotimetab_venue/default', + new lang_string('default', 'videotimetab_venue'), + new lang_string('default_help', 'videotimetab_venue'), + 0 +)); diff --git a/tab/venue/templates/main.mustache b/tab/venue/templates/main.mustache new file mode 100644 index 00000000..986ce50b --- /dev/null +++ b/tab/venue/templates/main.mustache @@ -0,0 +1,90 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle 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. + + Moodle 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 Moodle. If not, see . +}} +{{! + @template videotimetab_venue/main + + This template renders the main content area for the deft choice block. + + Example context (json): + { + "instanceid": 100, + "autogaincontrol": true, + "contextid": 2, + "echocancellation": true, + "iceservers": "[]", + "noisesuppression": true, + "peerid": 109, + "peers": "[105, 106]", + "roomid": 3, + "samplerate": 11025, + "server": "wss://deftly.us/janus/ws", + "token": "dladfjlakerlaa3j4lr", + "throttle": 100, + "uniqueid": "eworio" + } +}} +
+
+
+ {{^ peerid }} +
+
+ {{# str }} venueclosed, block_deft {{/ str }} +
+
+ {{/ peerid }} + {{# peerid }} + {{# user }} +
+ {{# picture }} + + {{/ picture }} + {{^ picture }} + {{{ avatar }}} + {{/ picture }} +
+
+
+ {{ fullname }} +
+
+ {{> block_deft/volume_indicator }} +
+ +
+ {{/ user }} + {{/ peerid }} +
+
+ +
+
+{{#js}} + require([ + 'videotimetab_venue/venue_manager' + ], function(VenueManager) { + new VenueManager({{ contextid }}, '{{ token }}', [], {{ peerid }}, {{{ iceservers }}}, {{ autogaincontrol }}, {{ echocancellation }}, {{ noisesuppression }}, {{ samplerate }}, {{ roomid }}, '{{ server }}'); +}); +{{/js}} diff --git a/tab/venue/templates/upgrade.mustache b/tab/venue/templates/upgrade.mustache new file mode 100644 index 00000000..5223ab76 --- /dev/null +++ b/tab/venue/templates/upgrade.mustache @@ -0,0 +1,32 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle 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. + + Moodle 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 Moodle. If not, see . +}} +{{! + @template videotimetab_texttrack/upgrade + + This template for text track tab + + Variables optional for this template: + + Example context (json): + { + } + +}} +
+ {{# str }} upgradeplugin, videotimetab_venue {{/ str }} + {{# str }} moreinfo {{/ str }} +
diff --git a/tab/venue/version.php b/tab/venue/version.php new file mode 100644 index 00000000..b4ce645e --- /dev/null +++ b/tab/venue/version.php @@ -0,0 +1,34 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package videotimetab_venue + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'videotimetab_venue'; +$plugin->release = '1.7'; +$plugin->version = 2023011206; +$plugin->requires = 2015111610; +$plugin->maturity = MATURITY_STABLE; +$plugin->dependencies = [ + 'videotime' => 2023011200, +];