diff --git a/classes/videotime_instance.php b/classes/videotime_instance.php index f211c2b0..e1bdbdbb 100644 --- a/classes/videotime_instance.php +++ b/classes/videotime_instance.php @@ -24,6 +24,7 @@ namespace mod_videotime; +use cm_info; use core_component; use external_description; use mod_videotime\local\tabs\tabs; @@ -64,7 +65,7 @@ class videotime_instance implements \renderable, \templatable { /** * Temporary storage for course module. Use $this->get_cm() instead. * - * @var \stdClass|null + * @var cm_info|null */ private $cm = null; @@ -565,6 +566,10 @@ public static function get_external_description(): external_description { * Call plugins hook to setup page */ public function setup_page() { + foreach (array_keys(core_component::get_plugin_list('videotimeplugin')) as $name) { + component_callback("videotimeplugin_$name", 'setup_page', [$this->to_record(), $this->get_cm()]); + } + if ($this->enabletabs) { $this->tabs->setup_page(); } diff --git a/classes/vimeo_embed.php b/classes/vimeo_embed.php index 38e17a68..ad9ef57c 100644 --- a/classes/vimeo_embed.php +++ b/classes/vimeo_embed.php @@ -42,6 +42,8 @@ */ class vimeo_embed implements \renderable, \templatable { + protected $cm = null; + /** * Constructor * @@ -61,7 +63,7 @@ public function __construct(\stdClass $instancerecord) { */ public function get_cm() { if (is_null($this->cm)) { - $this->cm = get_coursemodule_from_instance('videotime', $this->id); + $this->cm = get_coursemodule_from_instance('videotime', $this->record->id); } return $this->cm; } diff --git a/lib.php b/lib.php index 20ff8782..1c077785 100644 --- a/lib.php +++ b/lib.php @@ -264,6 +264,10 @@ function videotime_update_instance($moduleinstance, $mform = null) { $classname::save_settings($moduleinstance); } + if (!empty($mform) && !empty($mform->get_data()->livefeed)) { + $moduleinstance->vimeo_url = ''; + } + return $DB->update_record('videotime', $moduleinstance); } diff --git a/plugin/live/amd/build/socket.min.js b/plugin/live/amd/build/socket.min.js new file mode 100644 index 00000000..fe8f9379 --- /dev/null +++ b/plugin/live/amd/build/socket.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("videotimeplugin_live/socket",["exports","core/ajax","core/log","core/notification","block_deft/socket"],(function(_exports,_ajax,_log,_notification,_socket){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;i\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from \"core/ajax\";\nimport Log from \"core/log\";\nimport Notification from \"core/notification\";\nimport SocketBase from \"block_deft/socket\";\n\nexport default class Socket extends SocketBase {\n /**\n * Renew token\n *\n * @param {int} contextid Context id of block\n */\n renewToken(contextid) {\n Ajax.call([{\n methodname: 'videotimeplugin_live_renew_token',\n args: {contextid: contextid},\n done: (replacement) => {\n Log.debug('Reconnecting');\n this.connect(contextid, replacement.token);\n },\n fail: Notification.exception\n }]);\n }\n}\n"],"names":["Socket","contextid","call","methodname","args","done","replacement","debug","_this","connect","token","fail","Notification","exception"],"mappings":"g2EAcqBA,6oBAMjB,SAAWC,wCACFC,KAAK,CAAC,CACPC,WAAY,mCACZC,KAAM,CAACH,UAAWA,WAClBI,KAAM,SAACC,0BACCC,MAAM,gBACVC,MAAKC,QAAQR,UAAWK,YAAYI,QAExCC,KAAMC,sBAAaC"} \ No newline at end of file diff --git a/plugin/live/amd/build/videotime.min.js b/plugin/live/amd/build/videotime.min.js new file mode 100644 index 00000000..b85fc7bd --- /dev/null +++ b/plugin/live/amd/build/videotime.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("videotimeplugin_live/videotime",["exports","core/ajax","mod_videotime/videotime","block_deft/janus-gateway","core/log","core/notification","block_deft/publish","block_deft/subscribe","videotimeplugin_live/socket"],(function(_exports,_ajax,_videotime,_janusGateway,_log,_notification,_publish,_subscribe,_socket){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;i\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from \"core/ajax\";\nimport VideoTimeBase from \"mod_videotime/videotime\";\nimport Janus from 'block_deft/janus-gateway';\nimport Log from \"core/log\";\nimport Notification from \"core/notification\";\nimport PublishBase from \"block_deft/publish\";\nimport SubscribeBase from \"block_deft/subscribe\";\nimport Socket from \"videotimeplugin_live/socket\";\n\nvar rooms = {};\n\nclass Publish extends PublishBase {\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 ptype: this.ptype == 'publish',\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 publishFeed() {\n if (\n this.videoroom.webrtcStuff.pc\n && this.videoroom.webrtcStuff.pc.iceConnectionState == 'connected'\n ) {\n setTimeout(() => {\n this.videoroom.webrtcStuff.pc.getTransceivers().forEach(transceiver => {\n const sender = transceiver.sender;\n if (\n sender.track\n && this.selectedTrack\n && (sender.track.id == this.selectedTrack.id)\n ) {\n const message = JSON.stringify({\n feed: Number(this.peerid),\n mid: transceiver.mid\n });\n this.videoroom.data({\n text: message,\n error: Log.debug\n });\n }\n });\n return Ajax.call([{\n args: {\n id: Number(this.peerid),\n room: this.roomid,\n },\n contextid: this.contextid,\n fail: Notification.exception,\n methodname: 'videotimeplugin_live_publish_feed'\n }])[0];\n });\n }\n }\n\n unpublish() {\n if (this.videoInput) {\n this.videoInput.then(videoStream => {\n if (videoStream) {\n videoStream.getVideoTracks().forEach(track => {\n track.stop();\n });\n }\n this.videoInput = null;\n\n return videoStream;\n }).catch(Notification.exception);\n this.videoroom.send({\n message: {\n request: 'unpublish'\n }\n });\n }\n document.querySelectorAll(\n '[data-contextid=\"' + this.contextid + '\"][data-action=\"publish\"]'\n ).forEach(button => {\n button.classList.remove('hidden');\n });\n //document.querySelectorAll(\n //'[data-contextid=\"' + this.contextid + '\"][data-action=\"unpublish\"]'\n //).forEach(button => {\n //button.classList.add('hidden');\n //});\n }\n\n onLocalTrack(track, on) {\n const remoteStream = new MediaStream([track]);\n remoteStream.mid = track.mid;\n Log.debug(on);\n Log.debug(remoteStream);\n Janus.attachMediaStream(\n document.getElementById('video-controls-' + this.tracks[track.id]),\n remoteStream\n );\n return;\n }\n\n handleClick(e) {\n const button = e.target.closest(\n '[data-contextid=\"' + this.contextid + '\"][data-action=\"publish\"], [data-contextid=\"'\n + this.contextid + '\"][data-action=\"unpublish\"]'\n );\n if (button) {\n const action = button.getAttribute('data-action'),\n type = button.getAttribute('data-type') || 'camera';\n e.stopPropagation();\n e.preventDefault();\n document.querySelectorAll(\n '[data-region=\"deft-venue\"] [data-action=\"publish\"], [data-region=\"deft-venue\"] [data-action=\"unpublish\"]'\n ).forEach(button => {\n if ((button.getAttribute('data-action') != action) || (button.getAttribute('data-type') != type)) {\n button.classList.remove('hidden');\n }\n });\n switch (action) {\n case 'publish':\n Log.debug(type);\n if (type == 'display') {\n this.shareDisplay();\n } else {\n this.shareCamera();\n }\n\n this.videoInput.then(videoStream => {\n const tracks = [];\n if (videoStream) {\n videoStream.getVideoTracks().forEach(track => {\n tracks.push({\n type: 'video',\n capture: track,\n recv: false\n });\n this.selectedTrack = track;\n this.tracks = this.tracks || {};\n this.tracks[track.id] = type;\n });\n videoStream.getAudioTracks().forEach(track => {\n tracks.push({\n type: 'audio',\n capture: track,\n recv: false\n });\n });\n this.videoroom.createOffer({\n tracks: tracks,\n success: (jsep) => {\n const publish = {\n request: \"configure\",\n video: true,\n audio: true\n };\n this.videoroom.send({\n message: publish,\n jsep: jsep\n });\n },\n error: function(error) {\n Notification.alert(\"WebRTC error... \", error.message);\n }\n });\n }\n\n return videoStream;\n }).catch(Notification.exception);\n break;\n case 'unpublish':\n if (this.videoInput) {\n this.videoInput.then(videoStream => {\n if (videoStream) {\n videoStream.getVideoTracks().forEach(track => {\n track.stop();\n });\n }\n this.videoInput = null;\n\n return videoStream;\n }).catch(Notification.exception);\n }\n this.videoroom.send({\n message: {\n request: 'unpublish'\n }\n });\n return Ajax.call([{\n args: {\n id: Number(this.peerid),\n publish: false,\n room: this.roomid\n },\n contextid: this.contextid,\n fail: Notification.exception,\n methodname: 'videotimeplugin_publish_feed'\n }])[0];\n }\n }\n\n return true;\n }\n}\n\nexport default class VideoTime extends VideoTimeBase {\n initialize(contextid, token, peerid) {\n Log.debug(\"Initializing Video Time \" + this.elementId);\n\n this.contextid = contextid;\n this.peerid = peerid;\n\n Ajax.call([{\n methodname: 'videotimeplugin_live_get_room',\n args: {contextid: contextid},\n done: (response) => {\n const socket = new Socket(contextid, token);\n\n this.iceservers = JSON.parse(response.iceservers);\n this.roomid = response.roomid;\n this.server = response.server;\n\n rooms[String(contextid)] = {\n contextid: contextid,\n peerid: peerid,\n roomid: response.roomid,\n server: response.server,\n iceServers: JSON.parse(response.iceservers)\n };\n this.roomid = response.roomid;\n\n socket.subscribe(() => {\n Ajax.call([{\n methodname: 'videotimeplugin_live_get_feed',\n args: {contextid: contextid},\n done: (response) => {\n const room = rooms[String(contextid)];\n if (room.publish && room.publish.restart) {\n if (response.feed == peerid) {\n this.unpublish();\n }\n room.publish = null;\n }\n this.subscribeTo(Number(response.feed));\n },\n fail: Notification.exception\n }]);\n });\n },\n fail: Notification.exception\n }]);\n\n this.addListeners();\n\n return true;\n }\n\n /**\n * Register player events to respond to user interaction and play progress.\n */\n addListeners() {\n document.querySelector('body').removeEventListener('click', handleClick);\n document.querySelector('body').addEventListener('click', handleClick);\n return;\n }\n\n /**\n * Subscribe to feed\n *\n * @param {int} source Feed to subscribe\n */\n subscribeTo(source) {\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"][data-action=\"publish\"]').forEach(button => {\n if (source == Number(this.peerid)) {\n button.classList.remove('hidden');\n } else {\n button.classList.remove('hidden');\n }\n });\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"][data-action=\"unpublish\"]').forEach(button => {\n if (source == Number(this.peerid)) {\n button.classList.remove('hidden');\n } else {\n button.classList.remove('hidden');\n }\n });\n Log.debug(source);\n Log.debug(this.peerid);\n\n if (this.remoteFeed && !this.remoteFeed.creatingSubscription && !this.remoteFeed.restart) {\n const update = {\n request: 'update',\n subscribe: [{\n feed: Number(source)\n }],\n unsubscribe: [{\n feed: Number(this.remoteFeed.current)\n }]\n };\n\n if (!source && this.remoteFeed.current) {\n delete update.subscribe;\n } else if (source && !this.remoteFeed.current) {\n delete update.unsubscribe;\n }\n\n if (this.remoteFeed.current != source) {\n this.remoteFeed.videoroom.send({message: update});\n if (this.remoteFeed.current == this.peerid) {\n const room = rooms[String(this.contextid)];\n room.publish.unpublish();\n }\n this.remoteFeed.current = source;\n Log.debug('[data-contextid=\"' + this.contextid + '\"] img.poster-img');\n if (source) {\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"] img.poster-img').forEach(img => {\n img.classList.add('hidden');\n });\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"] video').forEach(img => {\n img.classList.remove('hidden');\n });\n } else {\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"] img.poster-img').forEach(img => {\n img.classList.remove('hidden');\n });\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"] video').forEach(img => {\n img.classList.add('hidden');\n });\n }\n }\n } else if (this.remoteFeed && this.remoteFeed.restart) {\n if (this.remoteFeed.current != source) {\n this.remoteFeed = null;\n this.subscribeTo(source);\n }\n } else if (this.remoteFeed) {\n setTimeout(() => {\n this.subscribeTo(source);\n }, 500);\n } else if (source) {\n this.remoteFeed = new Subscribe(this.contextid, this.iceservers, this.roomid, this.server, this.peerid, source);\n this.remoteFeed.remoteVideo = document.getElementById(this.elementId);\n this.remoteFeed.remoteAudio = document.getElementById(this.elementId).parentNode.querySelector('audio');\n this.remoteFeed.startConnection(source);\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"] img.poster-img').forEach(img => {\n img.classList.add('hidden');\n });\n document.querySelectorAll('[data-contextid=\"' + this.contextid + '\"] video').forEach(img => {\n img.classList.remove('hidden');\n });\n }\n }\n}\n\nconst handleClick = function(e) {\n const button = e.target.closest('[data-roomid] [data-action=\"publish\"], [data-roomid] [data-action=\"unpublish\"]');\n if (button) {\n const action = button.getAttribute('data-action'),\n contextid = e.target.closest('[data-contextid]').getAttribute('data-contextid'),\n room = rooms[String(contextid)],\n iceServers = room.iceServers,\n peerid = room.peerid,\n roomid = room.roomid,\n server = room.server,\n type = button.getAttribute('data-type');\n e.stopPropagation();\n e.preventDefault();\n if (action == 'unpublish') {\n Ajax.call([{\n args: {\n id: Number(peerid),\n room: roomid,\n publish: false\n },\n contextid: contextid,\n fail: Notification.exception,\n methodname: 'videotimeplugin_live_publish_feed'\n }]);\n } else if (!room.publish || room.publish.restart) {\n room.publish = new Publish(contextid, iceServers, roomid, server, peerid);\n if (type == 'display') {\n room.publish.shareDisplay();\n } else {\n room.publish.shareCamera();\n }\n room.publish.startConnection();\n } else {\n room.publish.handleClick(e);\n }\n }\n};\n\nclass Subscribe extends SubscribeBase {\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 ptype: false,\n feed: this.feed,\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"],"names":["rooms","Publish","pluginHandle","Ajax","call","args","handle","getId","id","Number","this","peerid","plugin","room","roomid","ptype","session","getSessionId","contextid","fail","Notification","exception","methodname","videoroom","webrtcStuff","pc","iceConnectionState","setTimeout","_this","getTransceivers","forEach","transceiver","sender","track","selectedTrack","message","JSON","stringify","feed","mid","data","text","error","Log","debug","videoInput","then","videoStream","getVideoTracks","stop","_this2","catch","send","request","document","querySelectorAll","button","classList","remove","on","remoteStream","MediaStream","attachMediaStream","getElementById","tracks","e","target","closest","action","getAttribute","type","stopPropagation","preventDefault","shareDisplay","shareCamera","push","capture","recv","_this3","getAudioTracks","createOffer","success","jsep","video","audio","alert","publish","PublishBase","VideoTime","token","elementId","done","response","socket","Socket","_this4","iceservers","parse","server","String","iceServers","subscribe","restart","unpublish","subscribeTo","addListeners","querySelector","removeEventListener","handleClick","addEventListener","source","_this5","remoteFeed","creatingSubscription","current","Subscribe","remoteVideo","remoteAudio","parentNode","startConnection","img","add","update","unsubscribe","VideoTimeBase","SubscribeBase"],"mappings":"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"} \ No newline at end of file diff --git a/plugin/live/amd/src/socket.js b/plugin/live/amd/src/socket.js new file mode 100644 index 00000000..aaaa6564 --- /dev/null +++ b/plugin/live/amd/src/socket.js @@ -0,0 +1,32 @@ +/* + * Video Time Live extension Deft socket + * + * @package videotimeplugin_live + * @module videotimeplugin_live/socket + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from "core/ajax"; +import Log from "core/log"; +import Notification from "core/notification"; +import SocketBase from "block_deft/socket"; + +export default class Socket extends SocketBase { + /** + * Renew token + * + * @param {int} contextid Context id of block + */ + renewToken(contextid) { + Ajax.call([{ + methodname: 'videotimeplugin_live_renew_token', + args: {contextid: contextid}, + done: (replacement) => { + Log.debug('Reconnecting'); + this.connect(contextid, replacement.token); + }, + fail: Notification.exception + }]); + } +} diff --git a/plugin/live/amd/src/videotime.js b/plugin/live/amd/src/videotime.js new file mode 100644 index 00000000..aaa4b1bc --- /dev/null +++ b/plugin/live/amd/src/videotime.js @@ -0,0 +1,437 @@ +/* + * Video time player specific js + * + * @package videotimeplugin_live + * @module videotimeplugin_live/videotime + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from "core/ajax"; +import VideoTimeBase from "mod_videotime/videotime"; +import Janus from 'block_deft/janus-gateway'; +import Log from "core/log"; +import Notification from "core/notification"; +import PublishBase from "block_deft/publish"; +import SubscribeBase from "block_deft/subscribe"; +import Socket from "videotimeplugin_live/socket"; + +var rooms = {}; + +class Publish extends PublishBase { + /** + * 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, + ptype: this.ptype == 'publish', + session: pluginHandle.session.getSessionId() + }, + contextid: this.contextid, + fail: Notification.exception, + methodname: 'videotimeplugin_live_join_room' + }])[0]; + } + + publishFeed() { + if ( + this.videoroom.webrtcStuff.pc + && this.videoroom.webrtcStuff.pc.iceConnectionState == 'connected' + ) { + setTimeout(() => { + this.videoroom.webrtcStuff.pc.getTransceivers().forEach(transceiver => { + const sender = transceiver.sender; + if ( + sender.track + && this.selectedTrack + && (sender.track.id == this.selectedTrack.id) + ) { + const message = JSON.stringify({ + feed: Number(this.peerid), + mid: transceiver.mid + }); + this.videoroom.data({ + text: message, + error: Log.debug + }); + } + }); + return Ajax.call([{ + args: { + id: Number(this.peerid), + room: this.roomid, + }, + contextid: this.contextid, + fail: Notification.exception, + methodname: 'videotimeplugin_live_publish_feed' + }])[0]; + }); + } + } + + unpublish() { + if (this.videoInput) { + this.videoInput.then(videoStream => { + if (videoStream) { + videoStream.getVideoTracks().forEach(track => { + track.stop(); + }); + } + this.videoInput = null; + + return videoStream; + }).catch(Notification.exception); + this.videoroom.send({ + message: { + request: 'unpublish' + } + }); + } + document.querySelectorAll( + '[data-contextid="' + this.contextid + '"][data-action="publish"]' + ).forEach(button => { + button.classList.remove('hidden'); + }); + //document.querySelectorAll( + //'[data-contextid="' + this.contextid + '"][data-action="unpublish"]' + //).forEach(button => { + //button.classList.add('hidden'); + //}); + } + + onLocalTrack(track, on) { + const remoteStream = new MediaStream([track]); + remoteStream.mid = track.mid; + Log.debug(on); + Log.debug(remoteStream); + Janus.attachMediaStream( + document.getElementById('video-controls-' + this.tracks[track.id]), + remoteStream + ); + return; + } + + handleClick(e) { + const button = e.target.closest( + '[data-contextid="' + this.contextid + '"][data-action="publish"], [data-contextid="' + + this.contextid + '"][data-action="unpublish"]' + ); + if (button) { + const action = button.getAttribute('data-action'), + type = button.getAttribute('data-type') || 'camera'; + e.stopPropagation(); + e.preventDefault(); + document.querySelectorAll( + '[data-region="deft-venue"] [data-action="publish"], [data-region="deft-venue"] [data-action="unpublish"]' + ).forEach(button => { + if ((button.getAttribute('data-action') != action) || (button.getAttribute('data-type') != type)) { + button.classList.remove('hidden'); + } + }); + switch (action) { + case 'publish': + Log.debug(type); + if (type == 'display') { + this.shareDisplay(); + } else { + this.shareCamera(); + } + + this.videoInput.then(videoStream => { + const tracks = []; + if (videoStream) { + videoStream.getVideoTracks().forEach(track => { + tracks.push({ + type: 'video', + capture: track, + recv: false + }); + this.selectedTrack = track; + this.tracks = this.tracks || {}; + this.tracks[track.id] = type; + }); + videoStream.getAudioTracks().forEach(track => { + tracks.push({ + type: 'audio', + capture: track, + recv: false + }); + }); + this.videoroom.createOffer({ + tracks: tracks, + success: (jsep) => { + const publish = { + request: "configure", + video: true, + audio: true + }; + this.videoroom.send({ + message: publish, + jsep: jsep + }); + }, + error: function(error) { + Notification.alert("WebRTC error... ", error.message); + } + }); + } + + return videoStream; + }).catch(Notification.exception); + break; + case 'unpublish': + if (this.videoInput) { + this.videoInput.then(videoStream => { + if (videoStream) { + videoStream.getVideoTracks().forEach(track => { + track.stop(); + }); + } + this.videoInput = null; + + return videoStream; + }).catch(Notification.exception); + } + this.videoroom.send({ + message: { + request: 'unpublish' + } + }); + return Ajax.call([{ + args: { + id: Number(this.peerid), + publish: false, + room: this.roomid + }, + contextid: this.contextid, + fail: Notification.exception, + methodname: 'videotimeplugin_publish_feed' + }])[0]; + } + } + + return true; + } +} + +export default class VideoTime extends VideoTimeBase { + initialize(contextid, token, peerid) { + Log.debug("Initializing Video Time " + this.elementId); + + this.contextid = contextid; + this.peerid = peerid; + + Ajax.call([{ + methodname: 'videotimeplugin_live_get_room', + args: {contextid: contextid}, + done: (response) => { + const socket = new Socket(contextid, token); + + this.iceservers = JSON.parse(response.iceservers); + this.roomid = response.roomid; + this.server = response.server; + + rooms[String(contextid)] = { + contextid: contextid, + peerid: peerid, + roomid: response.roomid, + server: response.server, + iceServers: JSON.parse(response.iceservers) + }; + this.roomid = response.roomid; + + socket.subscribe(() => { + Ajax.call([{ + methodname: 'videotimeplugin_live_get_feed', + args: {contextid: contextid}, + done: (response) => { + const room = rooms[String(contextid)]; + if (room.publish && room.publish.restart) { + if (response.feed == peerid) { + this.unpublish(); + } + room.publish = null; + } + this.subscribeTo(Number(response.feed)); + }, + fail: Notification.exception + }]); + }); + }, + fail: Notification.exception + }]); + + this.addListeners(); + + return true; + } + + /** + * Register player events to respond to user interaction and play progress. + */ + addListeners() { + document.querySelector('body').removeEventListener('click', handleClick); + document.querySelector('body').addEventListener('click', handleClick); + return; + } + + /** + * Subscribe to feed + * + * @param {int} source Feed to subscribe + */ + subscribeTo(source) { + document.querySelectorAll('[data-contextid="' + this.contextid + '"][data-action="publish"]').forEach(button => { + if (source == Number(this.peerid)) { + button.classList.remove('hidden'); + } else { + button.classList.remove('hidden'); + } + }); + document.querySelectorAll('[data-contextid="' + this.contextid + '"][data-action="unpublish"]').forEach(button => { + if (source == Number(this.peerid)) { + button.classList.remove('hidden'); + } else { + button.classList.remove('hidden'); + } + }); + Log.debug(source); + Log.debug(this.peerid); + + if (this.remoteFeed && !this.remoteFeed.creatingSubscription && !this.remoteFeed.restart) { + const update = { + request: 'update', + subscribe: [{ + feed: Number(source) + }], + unsubscribe: [{ + feed: Number(this.remoteFeed.current) + }] + }; + + if (!source && this.remoteFeed.current) { + delete update.subscribe; + } else if (source && !this.remoteFeed.current) { + delete update.unsubscribe; + } + + if (this.remoteFeed.current != source) { + this.remoteFeed.videoroom.send({message: update}); + if (this.remoteFeed.current == this.peerid) { + const room = rooms[String(this.contextid)]; + room.publish.unpublish(); + } + this.remoteFeed.current = source; + Log.debug('[data-contextid="' + this.contextid + '"] img.poster-img'); + if (source) { + document.querySelectorAll('[data-contextid="' + this.contextid + '"] img.poster-img').forEach(img => { + img.classList.add('hidden'); + }); + document.querySelectorAll('[data-contextid="' + this.contextid + '"] video').forEach(img => { + img.classList.remove('hidden'); + }); + } else { + document.querySelectorAll('[data-contextid="' + this.contextid + '"] img.poster-img').forEach(img => { + img.classList.remove('hidden'); + }); + document.querySelectorAll('[data-contextid="' + this.contextid + '"] video').forEach(img => { + img.classList.add('hidden'); + }); + } + } + } else if (this.remoteFeed && this.remoteFeed.restart) { + if (this.remoteFeed.current != source) { + this.remoteFeed = null; + this.subscribeTo(source); + } + } else if (this.remoteFeed) { + setTimeout(() => { + this.subscribeTo(source); + }, 500); + } else if (source) { + this.remoteFeed = new Subscribe(this.contextid, this.iceservers, this.roomid, this.server, this.peerid, source); + this.remoteFeed.remoteVideo = document.getElementById(this.elementId); + this.remoteFeed.remoteAudio = document.getElementById(this.elementId).parentNode.querySelector('audio'); + this.remoteFeed.startConnection(source); + document.querySelectorAll('[data-contextid="' + this.contextid + '"] img.poster-img').forEach(img => { + img.classList.add('hidden'); + }); + document.querySelectorAll('[data-contextid="' + this.contextid + '"] video').forEach(img => { + img.classList.remove('hidden'); + }); + } + } +} + +const handleClick = function(e) { + const button = e.target.closest('[data-roomid] [data-action="publish"], [data-roomid] [data-action="unpublish"]'); + if (button) { + const action = button.getAttribute('data-action'), + contextid = e.target.closest('[data-contextid]').getAttribute('data-contextid'), + room = rooms[String(contextid)], + iceServers = room.iceServers, + peerid = room.peerid, + roomid = room.roomid, + server = room.server, + type = button.getAttribute('data-type'); + e.stopPropagation(); + e.preventDefault(); + if (action == 'unpublish') { + Ajax.call([{ + args: { + id: Number(peerid), + room: roomid, + publish: false + }, + contextid: contextid, + fail: Notification.exception, + methodname: 'videotimeplugin_live_publish_feed' + }]); + } else if (!room.publish || room.publish.restart) { + room.publish = new Publish(contextid, iceServers, roomid, server, peerid); + if (type == 'display') { + room.publish.shareDisplay(); + } else { + room.publish.shareCamera(); + } + room.publish.startConnection(); + } else { + room.publish.handleClick(e); + } + } +}; + +class Subscribe extends SubscribeBase { + /** + * 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, + ptype: false, + feed: this.feed, + session: pluginHandle.session.getSessionId() + }, + contextid: this.contextid, + fail: Notification.exception, + methodname: 'videotimeplugin_live_join_room' + }])[0]; + } +} diff --git a/plugin/live/backup/moodle2/backup_videotimeplugin_live_subplugin.class.php b/plugin/live/backup/moodle2/backup_videotimeplugin_live_subplugin.class.php new file mode 100644 index 00000000..bd25f7cb --- /dev/null +++ b/plugin/live/backup/moodle2/backup_videotimeplugin_live_subplugin.class.php @@ -0,0 +1,63 @@ +. + +/** + * Defines backup_videotimeplugin_live_subplugin class + * + * @package videotimeplugin_live + * @copyright 2021 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Defines backup_videotimeplugin_live_subplugin class + * + * Provides the step to perform back up of sublugin data + */ +class backup_videotimeplugin_live_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('live_settings', null, [ + 'livefeed', + 'responsive', + 'autoplay', + 'controls', + 'muted', + 'height', + 'option_loop', + 'playsinline', + 'speed', + 'width', + ]); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subplugintablesettings); + + // Set source to populate the data. + $subplugintablesettings->set_source_table('videotimeplugin_live', + array('videotime' => backup::VAR_ACTIVITYID)); + + return $subplugin; + } +} diff --git a/plugin/live/backup/moodle2/restore_videotimeplugin_live_subplugin.class.php b/plugin/live/backup/moodle2/restore_videotimeplugin_live_subplugin.class.php new file mode 100644 index 00000000..fd233b15 --- /dev/null +++ b/plugin/live/backup/moodle2/restore_videotimeplugin_live_subplugin.class.php @@ -0,0 +1,74 @@ +. + +/** + * videotime restore task + * + * provides all the settings and steps to perform one * complete restore of the activity + * + * @package videotimeplugin_live + * @copyright 2021 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 videotimeplugin_live subplugin. + */ +class restore_videotimeplugin_live_subplugin extends restore_subplugin { + + /** + * Define subplugin structure + * + */ + protected function define_videotime_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor(''); + $elepath = $this->get_pathfor('/live_settings'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes the videotimeplugin_live element, if it is in the file. + * @param array $data the data read from the XML file. + */ + public function process_videotimeplugin_live($data) { + global $DB; + + $data = (array) $data + (array) get_config('videotimeplugin_live'); + $data = (object)$data; + $data->videotime = $this->get_new_parentid('videotime'); + $DB->insert_record('videotimeplugin_live', $data); + } + + /** + * Defines post-execution actions. + */ + protected function after_execute(): void { + // Add related files, no need to match by itemname (just internally handled context). + $this->add_related_files('videotimeplugin_live', 'mediafile', null); + } +} diff --git a/plugin/live/classes/event/video_ended.php b/plugin/live/classes/event/video_ended.php new file mode 100644 index 00000000..4db3b93b --- /dev/null +++ b/plugin/live/classes/event/video_ended.php @@ -0,0 +1,90 @@ +. + +namespace videotimeplugin_live\event; + +use \core\event\base; + +/** + * The video ended event + * + * @package videotimeplugin_live + * @category event + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class video_ended 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('eventvideoended', 'block_deft'); + } + + /** + * Get backup mappinig + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'videotime', 'restore' => 'task']; + } + + /** + * 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' ended live video in videotime with id '$this->objectid'."; + } + +} diff --git a/plugin/live/classes/event/video_started.php b/plugin/live/classes/event/video_started.php new file mode 100644 index 00000000..b8593ce5 --- /dev/null +++ b/plugin/live/classes/event/video_started.php @@ -0,0 +1,90 @@ +. + +namespace videotimeplugin_live\event; + +use \core\event\base; + +/** + * The video started event + * + * @package videotimeplugin_live + * @category event + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class video_started 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('eventvideostarted', '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' started live video in videotime with id '$this->objectid'."; + } + +} diff --git a/plugin/live/classes/external/get_feed.php b/plugin/live/classes/external/get_feed.php new file mode 100644 index 00000000..439bf462 --- /dev/null +++ b/plugin/live/classes/external/get_feed.php @@ -0,0 +1,98 @@ +. + +namespace videotimeplugin_live\external; + +use videotimeplugin_live\socket; +use cache; +use context; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use stdClass; + +/** + * External function for storing user venue settings + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_feed 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, 'Context id for module'), + ] + ); + } + + /** + * Change settings + * + * @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): 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); + + $record = $DB->get_record( + 'block_deft_room', + [ + 'itemid' => $cm->instance, + 'component' => 'videotimeplugin_live', + ] + ); + + $data = json_decode($record->data) ?? new stdClass(); + + return [ + 'feed' => $data->feed ?? 0, + ]; + } + + /** + * Get return definition for send_signal + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'feed' => new external_value(PARAM_INT, 'ID of publisher'), + ]); + } +} diff --git a/plugin/live/classes/external/get_room.php b/plugin/live/classes/external/get_room.php new file mode 100644 index 00000000..b92bcf8d --- /dev/null +++ b/plugin/live/classes/external/get_room.php @@ -0,0 +1,105 @@ +. + +namespace videotimeplugin_live\external; + +use videotimeplugin_live\socket; +use block_deft\janus; +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); + + $janus = new janus(); + $textroom = $janus->attach('janus.plugin.textroom'); + $janus->send($textroom, [ + 'request' => 'kick', + 'room' => $janusroom->get_roomid(), + 'secret' => $janusroom->get_secret(), + 'username' => $DB->get_field_select('sessions', 'id', 'sid = ?', [session_id()]), + ]); + + 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/plugin/live/classes/external/join_room.php b/plugin/live/classes/external/join_room.php new file mode 100644 index 00000000..84f5de31 --- /dev/null +++ b/plugin/live/classes/external/join_room.php @@ -0,0 +1,185 @@ +. + +namespace videotimeplugin_live\external; + +use block_deft\janus; +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 join_room extends \block_deft\external\join_room { + + /** + * Join room + * + * @param int $handle Janus plugin handle + * @param string $id Venue peer id + * @param int $plugin Janus plugin name + * @param bool $ptype Whether video publisher + * @param int $room Room id being joined + * @param int $session Janus session id + * @param int $feed Initial video feed + * @return array + */ + public static function execute($handle, $id, $plugin, $ptype, $room, $session, $feed): array { + global $DB, $SESSION, $USER; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'handle' => $handle, + 'id' => $id, + 'plugin' => $plugin, + 'ptype' => $ptype, + 'room' => $room, + 'session' => $session, + 'feed' => $feed, + ]); + + if (!empty($id) && !$DB->get_record('sessions', [ + 'id' => $id, + 'sid' => session_id(), + ])) { + return [ + 'status' => false, + ]; + } + $record = $DB->get_record( + 'block_deft_room', + [ + 'roomid' => $room, + 'component' => 'videotimeplugin_live', + ] + ); + + $cm = get_coursemodule_from_instance('videotime', $record->itemid); + $context = context_module::instance($cm->id); + self::validate_context($context); + + require_login(); + require_capability('mod/videotime:view', $context); + + $janus = new janus($session); + $janusroom = new janus_room($record->itemid); + + $token = $janusroom->get_token(); + + if ($plugin == 'janus.plugin.videoroom') { + if (empty($id)) { + $id = $janus->transaction_identifier(); + } + if ($ptype) { + $message = [ + 'id' => $id, + 'request' => 'kick', + 'room' => $room, + 'secret' => $janusroom->get_secret(), + ]; + + $janus->send($handle, $message); + } + $message = [ + 'id' => $ptype ? $id : $id . 'subscriber', + 'ptype' => $ptype ? 'publisher' : 'subscriber', + 'request' => 'join', + 'room' => $room, + 'token' => $token, + ]; + if ($feed) { + $message['streams'] = [ + [ + 'feed' => $feed + ] + ]; + } else { + require_capability('videotimeplugin/live:sharevideo', $context); + } + } else { + $textroom = $janus->attach('janus.plugin.videoroom'); + $janus->send($textroom, [ + 'request' => 'kick', + 'room' => $room, + 'secret' => $janusroom->get_secret(), + 'username' => $id, + ]); + + $janus->send($handle, [ + 'id' => $id, + 'request' => 'kick', + 'room' => $room, + 'secret' => $janusroom->get_secret(), + ]); + + $message = [ + 'id' => $id, + 'request' => 'join', + 'room' => $room, + 'token' => $token, + ]; + $params = [ + 'context' => $context, + 'objectid' => $cm->instance, + ]; + + $event = \videotimetab_venue\event\audiobridge_launched::create($params); + $event->trigger(); + + $sessionid = $DB->get_field_select('sessions', 'id', 'sid = :sid', ['sid' => session_id()]); + + $timenow = time(); + + if ($record = $DB->get_record('videotimetab_venue_peer', [ + 'sessionid' => $sessionid, + 'videotime' => $cm->instance, + 'userid'=> $USER->id, + 'status' => false, + ])) { + $record->timemodified = $timenow; + $DB->update_record('videotimetab_venue_peer', $record); + } else { + $DB->insert_record('videotimetab_venue_peer', [ + 'sessionid' => $sessionid, + 'videotime' => $cm->instance, + 'userid'=> $USER->id, + 'mute' => true, + 'status' => false, + 'timecreated' => $timenow, + 'timemodified' => $timenow, + ]); + } + } + + $response = $janus->send($handle, $message); + + return [ + 'status' => true, + ]; + } +} diff --git a/plugin/live/classes/external/publish_feed.php b/plugin/live/classes/external/publish_feed.php new file mode 100644 index 00000000..583afde1 --- /dev/null +++ b/plugin/live/classes/external/publish_feed.php @@ -0,0 +1,131 @@ +. + +namespace videotimeplugin_live\external; + +use block_deft\janus; +use videotimeplugin_live\socket; +use context; +use context_module; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use stdClass; +use videotimeplugin_live\janus_room; + +/** + * External function to offer feed to venue + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publish_feed extends \block_deft\external\publish_feed { + /** + * Publish feed + * + * @param string $id Venue peer id + * @param bool $publish Whether to publish + * @param int $room Room id being joined + * @return array + */ + public static function execute($id, $publish, $room): array { + global $DB, $SESSION; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'id' => $id, + 'publish' => $publish, + 'room' => $room, + ]); + + if (!empty($id) && !$DB->get_record('sessions', [ + 'id' => $id, + 'sid' => session_id(), + ])) { + return [ + 'status' => false, + ]; + } + $record = $DB->get_record( + 'block_deft_room', + [ + 'roomid' => $room, + 'component' => 'videotimeplugin_live', + ] + ); + + $cm = get_coursemodule_from_instance('videotime', $record->itemid); + $context = context_module::instance($cm->id); + self::validate_context($context); + + require_login(); + require_capability('mod/videotime:view', $context); + if ($publish) { + require_capability('videotimeplugin/live:sharevideo', $context); + } + + $janus = new janus($session); + $janusroom = new janus_room($record->itemid); + + $token = $janusroom->get_token(); + + $data = json_decode($record->data) ?? new stdClass(); + if (!$publish && !empty($data->feed) && $data->feed == $id) { + $data->feed = 0; + } else if ($publish) { + if ( + !empty($data->feed) + && ($data->feed != $id) + && $DB->get_record('sessions', [ + 'id' => $data->feed, + 'sid' => session_id(), + ]) + ) { + require_capability('videotimeplugin/live:moderate', $context); + } + $data->feed = $id; + } else { + return [ + 'status' => false, + ]; + } + + $record->timemodified = time(); + $record->data = json_encode($data); + $DB->update_record('block_deft_room', $record); + + $socket = new socket($context); + $socket->dispatch(); + + $params = [ + 'context' => $context, + 'objectid' => $record->itemid, + ]; + + if ($publish) { + $event = \videotimeplugin_live\event\video_started::create($params); + } else { + $event = \videotimeplugin_live\event\video_ended::create($params); + } + $event->trigger(); + + return [ + 'status' => true, + ]; + } +} diff --git a/plugin/live/classes/external/renew_token.php b/plugin/live/classes/external/renew_token.php new file mode 100644 index 00000000..3e23d805 --- /dev/null +++ b/plugin/live/classes/external/renew_token.php @@ -0,0 +1,58 @@ +. + +namespace videotimeplugin_live\external; + +use videotimeplugin_live\socket; +use context; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; + +/** + * External function for getting new token + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renew_token extends \block_deft\external\renew_token { + + /** + * Get new token + * + * @param int $contextid The block context id + * @return array + */ + public static function execute($contextid): array { + $params = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + ]); + $contextid = $params['contextid']; + + $context = context::instance_by_id($contextid); + self::validate_context($context); + + $socket = new socket($context); + $token = $socket->get_token(); + + return [ + 'token' => $token, + ]; + } +} diff --git a/plugin/live/classes/form/options.php b/plugin/live/classes/form/options.php new file mode 100644 index 00000000..d07224e3 --- /dev/null +++ b/plugin/live/classes/form/options.php @@ -0,0 +1,204 @@ +. + +/** + * The Vimeo options form. + * + * @package videotimeplugin_live + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace videotimeplugin_live\form; + +defined('MOODLE_INTERNAL') || die(); + +use core_component; +use html_writer; +use mod_videotime\videotime_instance; +use moodleform; +use moodle_url; + +require_once("$CFG->libdir/formslib.php"); + +/** + * The Vimeo options form. + * + * @package videotimeplugin_live + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class options extends moodleform { + + /** + * @var array Vimeo embed option fields. + */ + private static $optionfields = [ + 'autoplay', + 'byline', + 'height', + 'muted', + 'playsinline', + 'portrait', + 'responsive', + 'speed', + 'title', + 'autopause', + 'background', + 'controls', + 'pip', + 'dnt', + 'width', + ]; + + /** + * Defines forms elements + */ + public function definition() { + global $CFG, $COURSE, $PAGE, $DB; + + $mform = $this->_form; + + if (!videotime_has_pro()) { + $mform->addElement('static', '', '', html_writer::link(new moodle_url('https://link.bdecent.de/videotimepro1'), + html_writer::img('https://link.bdecent.de/videotimepro1/image.jpg', '', + ['width' => '100%', 'class' => 'img-responsive', 'style' => 'max-width:700px']))); + } + + $mform->addElement('header', 'embed_options', get_string('embed_options', 'videotime')); + + // Add hidden 'disable' element used for disabling embed options when they are globally forced. + $mform->addElement('hidden', 'disable'); + $mform->setType('disable', PARAM_INT); + $mform->setDefault('disable', 1); + + $mform->addElement('hidden', 'livefeed'); + $mform->setType('livefeed', PARAM_BOOL); + $mform->setDefault('livefeed', true); + + $mform->addElement('advcheckbox', 'responsive', get_string('option_responsive', 'videotime')); + $mform->setType('responsive', PARAM_BOOL); + $mform->addHelpButton('responsive', 'option_responsive', 'videotime'); + $mform->setDefault('responsive', get_config('videotime', 'responsive')); + self::create_additional_field_form_elements('responsive', $mform); + + $mform->addElement('text', 'height', get_string('option_height', 'videotime')); + $mform->setType('height', PARAM_INT); + $mform->addHelpButton('height', 'option_height', 'videotime'); + $mform->disabledIf('height', 'responsive', 'checked'); + $mform->setDefault('height', get_config('videotime', 'height')); + self::create_additional_field_form_elements('height', $mform); + + $mform->addElement('text', 'width', get_string('option_width', 'videotime')); + $mform->setType('width', PARAM_INT); + $mform->addHelpButton('width', 'option_width', 'videotime'); + $mform->setDefault('width', get_config('videotime', 'width')); + $mform->disabledIf('width', 'responsive', 'checked'); + self::create_additional_field_form_elements('width', $mform); + + $mform->addElement('advcheckbox', 'controls', get_string('option_controls', 'videotime')); + $mform->setType('controls', PARAM_BOOL); + $mform->addHelpButton('controls', 'option_controls', 'videotime'); + $mform->setDefault('controls', get_config('videotime', 'controls')); + self::create_additional_field_form_elements('controls', $mform); + + $mform->addElement('advcheckbox', 'muted', get_string('option_muted', 'videotime')); + $mform->setType('muted', PARAM_BOOL); + $mform->addHelpButton('muted', 'option_muted', 'videotime'); + $mform->setDefault('muted', get_config('videotime', 'muted')); + self::create_additional_field_form_elements('muted', $mform); + + $mform->addElement('advcheckbox', 'playsinline', get_string('option_playsinline', 'videotime')); + $mform->setType('playsinline', PARAM_BOOL); + $mform->addHelpButton('playsinline', 'option_playsinline', 'videotime'); + $mform->setDefault('playsinline', get_config('videotime', 'playsinline')); + self::create_additional_field_form_elements('playsinline', $mform); + + // Add fields from extensions. + foreach (array_keys(core_component::get_plugin_list('videotimeplugin')) as $name) { + component_callback("videotimeplugin_$name", 'add_form_fields', [$mform, get_class($this)]); + } + + // Add standard buttons. + $this->add_action_buttons(); + + if (!videotime_has_pro()) { + $mform->addElement('static', '', '', html_writer::link(new moodle_url('https://link.bdecent.de/videotimepro2'), + html_writer::img('https://link.bdecent.de/videotimepro2/image.jpg', '', + ['width' => '100%', 'class' => 'img-responsive', 'style' => 'max-width:700px']))); + } + } + + /** + * Allow additional form elements to be added for each Video Time field. + * + * @param string $fieldname + * @param \MoodleQuickForm $mform + * @param array $group + * @throws \coding_exception + * @throws \dml_exception + */ + public static function create_additional_field_form_elements(string $fieldname, \MoodleQuickForm $mform, &$group = null) { + $advanced = explode(',', get_config('videotimeplugin_live', 'advanced')); + $forced = explode(',', get_config('videotimeplugin_live', 'forced')); + + if (in_array($fieldname, $advanced)) { + $mform->setAdvanced($fieldname); + } + + if (in_array($fieldname, $forced)) { + if (in_array($fieldname, self::$optionfields)) { + $label = get_string('option_' . $fieldname, 'videotime'); + } else { + $label = get_string($fieldname, 'videotime'); + } + + $value = get_config('videotimeplugin_live', $fieldname); + if ($group) { + $element = null; + foreach ($group as $element) { + if ($element->getName() == $fieldname) { + break; + } + } + } else { + $element = $group ? null : $mform->getElement($fieldname); + } + + if ($element) { + if ($element instanceof \MoodleQuickForm_checkbox || $element instanceof \MoodleQuickForm_advcheckbox) { + $value = $value ? get_string('yes') : get_string('no'); + } + } else if ($element instanceof \MoodleQuickForm_radio) { + if ($element->getValue() == $value) { + $value = $element->getLabel(); + } + } + + $element = $mform->createElement('static', $fieldname . '_forced', '', get_string('option_forced', 'videotime', [ + 'option' => $label, + 'value' => $value + ])); + if ($group) { + $group[] = $element; + } else { + $mform->addElement($element); + $mform->removeElement($fieldname); + } + $mform->disabledIf($fieldname, 'disable', 'eq', 1); + } + } +} diff --git a/plugin/live/classes/janus_room.php b/plugin/live/classes/janus_room.php new file mode 100644 index 00000000..f3d469e7 --- /dev/null +++ b/plugin/live/classes/janus_room.php @@ -0,0 +1,96 @@ +. + +/** + * Janus room handler + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace videotimeplugin_live; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/lti/locallib.php'); + +use cache; +use context; +use block_deft\janus; +use block_deft\janus_room as janus_room_base; +use moodle_exception; +use stdClass; + +/** + * Janus room handler + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class janus_room extends janus_room_base { + + + /** + * @var Plugin component using room + */ + protected string $component = 'videotimeplugin_live'; + + /** + * Constructor + * + * @param int $id Instance id + */ + public function __construct (int $id) { + global $DB, $USER; + + if (!get_config('block_deft', 'enablebridge')) { + return; + } + + $this->session = new janus(); + $this->itemid = $id; + + if (!$record = $DB->get_record('block_deft_room', [ + 'itemid' => $id, + 'component' => $this->component, + ])) { + $records = $DB->get_records('block_deft_room', ['itemid' => null]); + if ($record = reset($records)) { + $record->itemid = $this->itemid; + $record->usermodified = $USER->id; + $record->timemodified = time(); + $record->component = $this->component; + $DB->update_record('block_deft_room', $record); + } else { + $this->create_room(); + } + } + + $this->record = $record; + + $this->roomid = $record->roomid ?? 0; + $this->secret = $record->secret ?? ''; + $this->server = $record->server ?? ''; + $this->session = new janus(); + $this->audiobridge = $this->session->attach('janus.plugin.audiobridge'); + $this->textroom = $this->session->attach('janus.plugin.textroom'); + $this->videoroom = $this->session->attach('janus.plugin.videoroom'); + + $this->init_room(); + } +} diff --git a/plugin/live/classes/privacy/provider.php b/plugin/live/classes/privacy/provider.php new file mode 100644 index 00000000..ee0f4963 --- /dev/null +++ b/plugin/live/classes/privacy/provider.php @@ -0,0 +1,44 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package videotimeplugin_live + * @copyright 2020 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace videotimeplugin_live\privacy; + +/** + * The videotimeplugin_live module does not store any data. + * + * @package videotimeplugin_live + * @copyright 2020 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/plugin/live/classes/socket.php b/plugin/live/classes/socket.php new file mode 100644 index 00000000..e369baae --- /dev/null +++ b/plugin/live/classes/socket.php @@ -0,0 +1,59 @@ +. + +/** + * WebSocket manager + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace videotimeplugin_live; + +use context; +use moodle_exception; +use stdClass; + +/** + * Web socket manager + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class socket extends \block_deft\socket { + /** + * @var Component + */ + protected const COMPONENT = 'videotimeplugin_live'; + + /** + * Validate context and availabilty + */ + public function validate() { + if ( + $this->context->contextlevel != CONTEXT_MODULE + ) { + throw new moodle_exception('invalidcontext'); + } + if ( + !get_coursemodule_from_id('videotime', $this->context->instanceid) + ) { + throw new moodle_exception('invalidcontext'); + } + } +} diff --git a/plugin/live/classes/video_embed.php b/plugin/live/classes/video_embed.php new file mode 100644 index 00000000..cb398e4c --- /dev/null +++ b/plugin/live/classes/video_embed.php @@ -0,0 +1,103 @@ +. + +/** + * Represents a single Video Time activity module. Adds more functionality when working with instances. + * + * @package videotimeplugin_live + * @copyright 2020 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace videotimeplugin_live; + +use core_component; +use context_module; +use context_system; +use mod_videotime\vimeo_embed; +use moodle_url; +use renderer_base; + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/filelib.php"); +require_once("$CFG->dirroot/mod/videotime/lib.php"); +require_once("$CFG->libdir/resourcelib.php"); + +/** + * Represents a single Video Time activity module. Adds more functionality when working with instances. + * + * @package videotimeplugin_live + */ +class video_embed extends vimeo_embed implements \renderable, \templatable { + + /** + * Function to export the renderer data in a format that is suitable for a + * mustache template. This means: + * 1. No complex types - only stdClass, array, int, string, float, bool + * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return \stdClass|array + */ + public function export_for_template(renderer_base $output) { + global $CFG, $DB; + + $cm = $this->get_cm(); + $context = context_module::instance($cm->id); + $socket = new socket($context); + + $fs = get_file_storage(); + $poster = $output->image_url('monologo', 'videotime')->out(); + $files = $fs->get_area_files(context_system::instance()->id, 'videotimeplugin_live', 'poster', 0); + foreach ($files as $file) { + if (!$file->is_directory()) { + $poster = moodle_url::make_pluginfile_url( + $file->get_contextid(), + $file->get_component(), + $file->get_filearea(), + null, + $file->get_filepath(), + $file->get_filename(), + false + ); + } + } + + $data = parent::export_for_template($output) + [ + 'contextid' => $context->id, + 'iceservers' => json_encode($socket->ice_servers()), + 'peerid' => $DB->get_field('sessions', 'id', ['sid' => session_id()]), + 'posterurl' => $poster, + 'sharevideo' => has_capability('videotimeplugin/live:sharevideo', $context), + 'token' => $socket->get_token(), + 'video' => true, + ]; + + return $data; + } + + /** + * Returns the moodle component name. + * + * It might be the plugin name (whole frankenstyle name) or the core subsystem name. + * + * @return string + */ + public function get_component_name() { + return 'videotimeplugin_live'; + } +} diff --git a/plugin/live/db/access.php b/plugin/live/db/access.php new file mode 100644 index 00000000..87dc7e31 --- /dev/null +++ b/plugin/live/db/access.php @@ -0,0 +1,47 @@ +. + +/** + * Plugin capabilities are defined here. + * + * @package videotimeplugin_live + * @category access + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + 'videotimeplugin/live:moderate' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW + ], + ], + + 'videotimeplugin/live:sharevideo' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW + ], + ], +]; diff --git a/plugin/live/db/install.php b/plugin/live/db/install.php new file mode 100644 index 00000000..abeb8123 --- /dev/null +++ b/plugin/live/db/install.php @@ -0,0 +1,36 @@ +. + +/** + * Enable plugin for new install + * + * @package videotimeplugin_live + * @copyright 2022 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_videotimeplugin_live_install() { + $manager = new plugin_manager('videotimeplugin'); + $manager->show_plugin('videojs'); + + return true; +} diff --git a/plugin/live/db/install.xml b/plugin/live/db/install.xml new file mode 100644 index 00000000..3cfd95aa --- /dev/null +++ b/plugin/live/db/install.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + +
+
+
diff --git a/plugin/live/db/services.php b/plugin/live/db/services.php new file mode 100644 index 00000000..655b57a9 --- /dev/null +++ b/plugin/live/db/services.php @@ -0,0 +1,73 @@ +. +/** + * Popup activies format external functions and service definitions. + * + * @package videotimeplugin_live + * @category external + * @copyright 2023 bdecent gmbh + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = [ + + 'videotimeplugin_live_get_feed' => [ + 'classname' => '\\videotimeplugin_live\\external\\get_feed', + 'methodname' => 'execute', + 'description' => 'Get currect video feed', + 'type' => 'read', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], + + 'videotimeplugin_live_get_room' => [ + 'classname' => '\\videotimeplugin_live\\external\\get_room', + 'methodname' => 'execute', + 'description' => 'Get currect room parameters for module', + 'type' => 'read', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], + + 'videotimeplugin_live_join_room' => [ + 'classname' => '\\videotimeplugin_live\\external\\join_room', + 'methodname' => 'execute', + 'description' => 'Join Janus room', + 'type' => 'write', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], + + 'videotimeplugin_live_publish_feed' => [ + 'classname' => '\\videotimeplugin_live\\external\\publish_feed', + 'methodname' => 'execute', + 'description' => 'Publish a video feed', + 'type' => 'write', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], + + 'videotimeplugin_live_renew_token' => [ + 'classname' => '\\videotimeplugin_live\\external\\renew_token', + 'methodname' => 'execute', + 'description' => 'Get new token to access message service', + 'type' => 'read', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ], +]; diff --git a/plugin/live/lang/en/videotimeplugin_live.php b/plugin/live/lang/en/videotimeplugin_live.php new file mode 100644 index 00000000..3470ad43 --- /dev/null +++ b/plugin/live/lang/en/videotimeplugin_live.php @@ -0,0 +1,36 @@ +. + +/** + * Plugin strings are defined here. + * + * @package videotimeplugin_live + * @copyright 2023 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['deft:moderate'] = 'Moderate venue'; +$string['deft:sharevideo'] = 'Share video'; +$string['livefeed'] = 'Live feed'; +$string['livefeed_help'] = 'Live feed can be supplied by a teacher'; +$string['mediafile_help'] = 'Upload an audio or video file to use'; +$string['pluginname'] = 'Video Time Live Player'; +$string['posterimage'] = 'Poster image'; +$string['posterimage_desc'] = 'An image image to display when session video is not available'; +$string['privacy:metadata'] = 'The Video Time Live Player plugin does not store any personal data.'; +$string['sharedvideo'] = 'Shared video'; diff --git a/plugin/live/lib.php b/plugin/live/lib.php new file mode 100644 index 00000000..f3b3e8c2 --- /dev/null +++ b/plugin/live/lib.php @@ -0,0 +1,267 @@ +. + +/** + * Library of interface functions and constants. + * + * @package videotimeplugin_live + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use mod_videotime\videotime_instance; +use mod_videotime\embed_player; +use videotimeplugin_live\video_embed; + +require_once("$CFG->libdir/resourcelib.php"); + +/** + * Updates an instance of the videotimeplugin_live in the database. + * + * Given an object containing all the necessary data (defined in mod_form.php), + * this function will update an existing instance with new data. + * + * @param object $moduleinstance An object from the form in mod_form.php. + * @param mod_videotime_mod_form $mform The form. + * @throws \dml_exception + */ +function videotimeplugin_live_update_instance($moduleinstance, $mform = null) { + global $DB; + + if ( + empty(get_config('videotimeplugin_live', 'enabled')) + || empty(get_config('block_deft', 'enableupdating')) + ) { + return; + } + + if (!empty($moduleinstance->livefeed)) { + if ($record = $DB->get_record('videotimeplugin_live', ['videotime' => $moduleinstance->id])) { + $record = ['id' => $record->id, 'videotime' => $moduleinstance->id] + (array) $moduleinstance + (array) $record; + $DB->update_record('videotimeplugin_live', $record); + } else { + $record = ['id' => null, 'videotime' => $moduleinstance->id] + + (array) $moduleinstance + (array) get_config('videotimeplugin_live'); + $record['id'] = $DB->insert_record('videotimeplugin_live', $record); + } + } else { + $DB->delete_records('videotimeplugin_live', array('videotime' => $moduleinstance->id)); + } +} + +/** + * Removes an instance of the mod_videotime from the database. + * + * @param int $id Id of the module instance. + * @return bool True if successful, false on failure. + */ +function videotimeplugin_live_delete_instance($id) { + global $DB; + + $DB->delete_records('videotimeplugin_live', array('videotime' => $id)); + \block_deft\janus_room::remove('videotimeplugin_live', $id); + + return true; +} + +/** + * Loads plugin settings into module record + * + * @param object $instance the module record. + * @return object + */ +function videotimeplugin_live_load_settings($instance) { + global $DB, $USER; + + $instance = (object) $instance; + if ( + empty(get_config('videotimeplugin_live', 'enabled')) + || mod_videotime_get_vimeo_id_from_link($instance->vimeo_url) + ) { + return $instance; + } + + $instance = (array) $instance; + if ( + $record = $DB->get_record('videotimeplugin_live', array('videotime' => $instance['id'])) + ) { + unset($record->id); + unset($record->videotime); + + return ((array) $record) + ((array) $instance) + ['livefeed' => 1]; + } + + return (array) $instance + (array) get_config('videotimeplugin_live'); +} + +/** + * Loads plugin settings into module record + * + * @param object $instance the module record. + * @param array $forcedsettings current forced settings array + * @return array + */ +function videotimeplugin_live_forced_settings($instance, $forcedsettings) { + global $DB; + + if (empty(get_config('videotimeplugin_live', 'enabled')) || !get_config('videotimeplugin_live', 'forced')) { + return $forcedsettings; + } + + $instance = (array) $instance; + if ( + !mod_videotime_get_vimeo_id_from_link($instance['vimeo_url']) + ) { + return array_fill_keys(explode(',', get_config('videotimeplugin_live', 'forced')), true) + (array) $forcedsettings; + } + + return $forcedsettings; +} + +/** + * Loads plugin player for instance + * + * @param object $instance the module record. + * @return object|null + */ +function videotimeplugin_live_embed_player($instance) { + global $DB; + + if ( + empty(get_config('videotimeplugin_live', 'enabled')) + || empty(get_config('block_deft', 'enableupdating')) + || !$DB->get_record('videotimeplugin_live', ['videotime' => $instance->id]) + ) { + return null; + } + + return new video_embed($instance); +} + +/** + * Add additional fields to form + * + * @param moodleform $mform Setting form to modify + * @param string $formclass Class nam of the form + */ +function videotimeplugin_live_add_form_fields($mform, $formclass) { + global $COURSE, $DB, $OUTPUT, $PAGE; + + if ( + !empty(get_config('videotimeplugin_live', 'enabled')) + && !empty(get_config('block_deft', 'enableupdating')) + && $formclass === 'mod_videotime_mod_form') { + $mform->insertElementBefore( + $mform->createElement('advcheckbox', 'livefeed', get_string('livefeed', 'videotimeplugin_live')), + 'name' + ); + $instance = $mform->getElementValue('instance'); + $mform->addHelpButton('livefeed', 'livefeed', 'videotimeplugin_live'); + $mform->disabledIf('vimeo_url', 'livefeed', 'checked'); + $mform->setDefault('livefeed', 0); + } +} + +/** + * Prepares the form before data are set + * + * @param array $defaultvalues + * @param int $instance + */ +function videotimeplugin_live_data_preprocessing(array &$defaultvalues, int $instance) { + global $DB; + + if (empty($instance)) { + $settings = (array) get_config('videotimeplugin_live'); + } else { + $settings = (array) $DB->get_record('videotimeplugin_live', array('videotime' => $instance)); + $defaultvalues['livefeed'] = !empty($settings['id']); + unset($settings['id']); + unset($settings['videotime']); + } + + foreach ($settings as $key => $value) { + $defaultvalues[$key] = $value; + } +} + +/** + * Serves the poster image file setting. + * + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @param string $filearea file area + * @param array $args extra arguments + * @param bool $forcedownload whether or not force download + * @param array $options additional options affecting the file serving + * @return bool false|void + */ +function videotimeplugin_live_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) { + if ($context->contextlevel != CONTEXT_SYSTEM) { + return false; + } + + if ($filearea !== 'poster' ) { + return false; + } + + // Extract the filename / filepath from the $args array. + $filename = array_pop($args); + if (!$args) { + $filepath = '/'; + } else { + $filepath = '/' . implode('/', $args) . '/'; + } + + // Retrieve the file from the Files API. + $itemid = 0; + $fs = get_file_storage(); + $file = $fs->get_file($context->id, 'videotimeplugin_live', $filearea, $itemid, $filepath, $filename); + if (!$file) { + return false; // The file does not exist. + } + + send_stored_file($file, null, 0, $forcedownload, $options); +} + +/** + * Add the control block to default region + * + * @param stdClass $instance Video Time instance + * @param stdClass $cm The course module + */ +function videotimeplugin_live_setup_page($instance, $cm) { + global $OUTPUT, $PAGE, $USER; + + $context = context_module::instance($cm->id); + if (empty($instance->livefeed) || !has_capability('block/deft:moderate', $context)) { + return; + } + + $bc = new block_contents(); + $bc->title = get_string('sharedvideo', 'videotimeplugin_live'); + $bc->attributes['class'] = 'block block_book_toc'; + $bc->content = $OUTPUT->render_from_template('videotimeplugin_live/controls', [ + 'contextid' => $context->id, + 'instance' => $instance, + ]); + + $defaultregion = $PAGE->blocks->get_default_region(); + $PAGE->blocks->add_fake_block($bc, $defaultregion); +} diff --git a/plugin/live/settings.php b/plugin/live/settings.php new file mode 100644 index 00000000..b7f96482 --- /dev/null +++ b/plugin/live/settings.php @@ -0,0 +1,107 @@ +. + +/** + * Plugin administration pages are defined here. + * + * @package videotimeplugin_live + * @category admin + * @copyright 2022 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'); + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_heading('defaultsettings', new lang_string('default', 'videotime') . ' ' . + new lang_string('settings'), '')); + + $settings->add(new admin_setting_configcheckbox( + 'videotimeplugin_live/autoplay', new lang_string('option_autoplay', 'videotime'), + new lang_string('option_autoplay_help', 'videotime'), '1')); + + $settings->add(new admin_setting_configcheckbox( + 'videotimeplugin_live/responsive', new lang_string('option_responsive', 'videotime'), + new lang_string('option_responsive_help', 'videotime'), '1')); + + $settings->add(new admin_setting_configtext('videotimeplugin_live/height', new lang_string('option_height', 'videotime'), + new lang_string('option_height_help', 'videotime'), '', PARAM_INT)); + + $settings->add(new admin_setting_configtext('videotimeplugin_live/width', new lang_string('option_width', 'videotime'), + new lang_string('option_width_help', 'videotime'), '', PARAM_INT)); + + $settings->add(new admin_setting_configcheckbox('videotimeplugin_live/controls', new lang_string('option_controls', 'videotime'), + new lang_string('option_controls_help', 'videotime'), '1')); + + $settings->add(new admin_setting_configcheckbox('videotimeplugin_live/muted', new lang_string('option_muted', 'videotime'), + new lang_string('option_muted_help', 'videotime'), '0')); + + $settings->add(new admin_setting_configcheckbox( + 'videotimeplugin_live/playsinline', new lang_string('option_playsinline', 'videotime'), + new lang_string('option_playsinline_help', 'videotime'), '1')); + + $options = [ + 'accepted_types' => [ + '.png', '.jpg', '.gif', '.webp', '.tiff', '.svg' + ], + ]; + $settings->add(new admin_setting_configstoredfile( + 'videotimeplugin_live/posterimage', + new lang_string('posterimage', 'videotimeplugin_live'), + new lang_string('posterimage_desc', 'videotimeplugin_live'), + 'poster', + 0, + $options + )); + + $settings->add(new admin_setting_heading('forcedhdr', new lang_string('forcedsettings', 'videotime'), '')); + + $options = [ + 'responsive' => new lang_string('option_responsive', 'videotime'), + 'controls' => new lang_string('option_controls', 'videotime'), + 'height' => new lang_string('option_height', 'videotime'), + 'muted' => new lang_string('option_muted', 'videotime'), + 'playsinline' => new lang_string('option_playsinline', 'videotime'), + 'width' => new lang_string('option_width', 'videotime'), + ]; + + $settings->add(new admin_setting_configmultiselect( + 'videotimeplugin_live/forced', + new lang_string('forcedsettings', 'videotime'), + new lang_string('forcedsettings_help', 'videotime'), + [ ], + $options + )); + + $settings->add(new admin_setting_heading('advancedhdr', new lang_string('advancedsettings', 'videotime'), '')); + + $settings->add(new admin_setting_configmultiselect( + 'videotimeplugin_live/advanced', + new lang_string('advancedsettings', 'videotime'), + new lang_string('advancedsettings_help', 'videotime'), + [ + 'height', + 'muted', + 'playsinline', + 'width', + ], + $options + )); +} diff --git a/plugin/live/styles.css b/plugin/live/styles.css new file mode 100644 index 00000000..552670d7 --- /dev/null +++ b/plugin/live/styles.css @@ -0,0 +1,9 @@ +.videotime-embed .vjs-layout-tiny, +.videotime-embed .vjs-layout-x-small, +.videotime-embed .vjs-layout-large, +.videotime-embed .vjs-layout-small, +.videotime-embed .vjs-layout-medium, +.videotime-embed .vjs-layout-x-large, +.videotime-embed .vjs-layout-huge { + width: 100%; +} diff --git a/plugin/live/templates/controls.mustache b/plugin/live/templates/controls.mustache new file mode 100644 index 00000000..3e34aa6e --- /dev/null +++ b/plugin/live/templates/controls.mustache @@ -0,0 +1,82 @@ +{{! + 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 videotimeplugin_live/controls + + This template will render the publishing controls for Video Time Live + + Variables required for this template: + * uniqueid - Unique id of player on page + * vimeo_url - vimeo url + + Variables optional for this template: + * responsive - markup for next activity button + * video_description - vimeo video descript + + Example context (json): + { + "responsive": true, + "cmid": 3, + "haspro": 0, + "interval": 2.5, + "instance": "{}", + "uniqueid": "60dccff8871f6", + "video_description": "UX design tips", + "video": 1, + "vimeo_url": "https://vimeo.com/323424" + } + +}} +{{# instance }} +
+
+ {{# posterurl }} + + {{/ posterurl }} + +
+
+ {{# posterurl }} + + {{/ posterurl }} + +
+
+ +
+
+{{/ instance }} diff --git a/plugin/live/templates/video_embed.mustache b/plugin/live/templates/video_embed.mustache new file mode 100644 index 00000000..f1fd519e --- /dev/null +++ b/plugin/live/templates/video_embed.mustache @@ -0,0 +1,76 @@ +{{! + 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 videotimeplugin_live/video_embed + + This template will render the video time activity and load player in tab + + Variables required for this template: + * uniqueid - Unique id of player on page + * vimeo_url - vimeo url + + Variables optional for this template: + * responsive - markup for next activity button + * video_description - vimeo video descript + + Example context (json): + { + "responsive": true, + "cmid": 3, + "haspro": 0, + "interval": 2.5, + "instance": "{}", + "uniqueid": "60dccff8871f6", + "video_description": "UX design tips", + "video": 1, + "vimeo_url": "https://vimeo.com/323424" + } + +}} +{{# instance }} +
+
+ {{# posterurl }} + + {{/ posterurl }} + + +
+ {{{video_description}}} +
+
+{{/ instance }} +{{#js}} + require(['videotimeplugin_live/videotime'], function(VideoTime) { + var v = new VideoTime('video-embed-{{uniqueid}}', {{cmid}}, {{haspro}}, {{interval}}, {{{instance}}}); + v.initialize( + {{ contextid }}, + '{{ token }}', + {{ peerid }} + ); + }); +{{/js}} diff --git a/plugin/live/version.php b/plugin/live/version.php new file mode 100644 index 00000000..6a4753e8 --- /dev/null +++ b/plugin/live/version.php @@ -0,0 +1,35 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package videotimeplugin_live + * @copyright 2018 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'videotimeplugin_live'; +$plugin->release = '1.7'; +$plugin->version = 2023011203; +$plugin->requires = 2015111610; +$plugin->maturity = MATURITY_STABLE; +$plugin->dependencies = [ + 'videotime' => 2023011200, + 'media_videojs' => 2015111600, +]; diff --git a/view.php b/view.php index 5f76f905..af93e918 100644 --- a/view.php +++ b/view.php @@ -76,18 +76,6 @@ $renderer = $PAGE->get_renderer('mod_videotime'); -// Allow any subplugin to override video time instance output. -foreach (\core_component::get_component_classes_in_namespace(null, 'videotime\\instance') as $fullclassname => $classpath) { - if (is_subclass_of($fullclassname, videotime_instance::class)) { - if ($override = $fullclassname::get_instance($moduleinstance->id)) { - $moduleinstance = $override; - } - if ($override = $fullclassname::get_renderer($moduleinstance->id)) { - $renderer = $override; - } - } -} - echo $OUTPUT->header(); if (!class_exists('core\\output\\activity_header')) {