From 9ec7d149032d33824738ecb02dfcbe87dd2532ef Mon Sep 17 00:00:00 2001 From: Daniel Thies Date: Tue, 1 Aug 2023 17:03:34 -0500 Subject: [PATCH 1/5] VID-656: Add live player --- classes/videotime_instance.php | 7 +- classes/vimeo_embed.php | 4 +- lib.php | 4 + plugin/live/amd/build/socket.min.js | 3 + plugin/live/amd/build/socket.min.js.map | 1 + plugin/live/amd/build/videotime.min.js | 3 + plugin/live/amd/build/videotime.min.js.map | 1 + plugin/live/amd/src/socket.js | 32 ++ plugin/live/amd/src/videotime.js | 437 ++++++++++++++++++ ...p_videotimeplugin_live_subplugin.class.php | 63 +++ ...e_videotimeplugin_live_subplugin.class.php | 74 +++ plugin/live/classes/event/video_ended.php | 90 ++++ plugin/live/classes/event/video_started.php | 90 ++++ plugin/live/classes/external/get_feed.php | 98 ++++ plugin/live/classes/external/get_room.php | 105 +++++ plugin/live/classes/external/join_room.php | 185 ++++++++ plugin/live/classes/external/publish_feed.php | 131 ++++++ plugin/live/classes/external/renew_token.php | 58 +++ plugin/live/classes/form/options.php | 204 ++++++++ plugin/live/classes/janus_room.php | 96 ++++ plugin/live/classes/privacy/provider.php | 44 ++ plugin/live/classes/socket.php | 59 +++ plugin/live/classes/video_embed.php | 103 +++++ plugin/live/db/access.php | 47 ++ plugin/live/db/install.php | 36 ++ plugin/live/db/install.xml | 23 + plugin/live/db/services.php | 73 +++ plugin/live/lang/en/videotimeplugin_live.php | 36 ++ plugin/live/lib.php | 267 +++++++++++ plugin/live/settings.php | 107 +++++ plugin/live/styles.css | 9 + plugin/live/templates/controls.mustache | 82 ++++ plugin/live/templates/video_embed.mustache | 76 +++ plugin/live/version.php | 35 ++ view.php | 12 - 35 files changed, 2681 insertions(+), 14 deletions(-) create mode 100644 plugin/live/amd/build/socket.min.js create mode 100644 plugin/live/amd/build/socket.min.js.map create mode 100644 plugin/live/amd/build/videotime.min.js create mode 100644 plugin/live/amd/build/videotime.min.js.map create mode 100644 plugin/live/amd/src/socket.js create mode 100644 plugin/live/amd/src/videotime.js create mode 100644 plugin/live/backup/moodle2/backup_videotimeplugin_live_subplugin.class.php create mode 100644 plugin/live/backup/moodle2/restore_videotimeplugin_live_subplugin.class.php create mode 100644 plugin/live/classes/event/video_ended.php create mode 100644 plugin/live/classes/event/video_started.php create mode 100644 plugin/live/classes/external/get_feed.php create mode 100644 plugin/live/classes/external/get_room.php create mode 100644 plugin/live/classes/external/join_room.php create mode 100644 plugin/live/classes/external/publish_feed.php create mode 100644 plugin/live/classes/external/renew_token.php create mode 100644 plugin/live/classes/form/options.php create mode 100644 plugin/live/classes/janus_room.php create mode 100644 plugin/live/classes/privacy/provider.php create mode 100644 plugin/live/classes/socket.php create mode 100644 plugin/live/classes/video_embed.php create mode 100644 plugin/live/db/access.php create mode 100644 plugin/live/db/install.php create mode 100644 plugin/live/db/install.xml create mode 100644 plugin/live/db/services.php create mode 100644 plugin/live/lang/en/videotimeplugin_live.php create mode 100644 plugin/live/lib.php create mode 100644 plugin/live/settings.php create mode 100644 plugin/live/styles.css create mode 100644 plugin/live/templates/controls.mustache create mode 100644 plugin/live/templates/video_embed.mustache create mode 100644 plugin/live/version.php 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')) { From fca85a261b6562fc910d42b5296598b769ce24f8 Mon Sep 17 00:00:00 2001 From: Daniel Thies Date: Tue, 1 Aug 2023 17:07:15 -0500 Subject: [PATCH 2/5] VID-676: Add audio venue tab --- tab/venue/amd/build/venue_manager.min.js | 3 + tab/venue/amd/build/venue_manager.min.js.map | 1 + tab/venue/amd/src/venue_manager.js | 828 ++++++++++++++++++ ...kup_videotimetab_venue_subplugin.class.php | 53 ++ ...ore_videotimetab_venue_subplugin.class.php | 66 ++ .../classes/event/audiobridge_launched.php | 90 ++ tab/venue/classes/event/hand_lower_sent.php | 79 ++ tab/venue/classes/event/hand_raise_sent.php | 79 ++ tab/venue/classes/external/get_room.php | 95 ++ tab/venue/classes/external/raise_hand.php | 110 +++ tab/venue/classes/external/venue_settings.php | 151 ++++ tab/venue/classes/external/venue_status.php | 114 +++ tab/venue/classes/output/main.php | 101 +++ tab/venue/classes/privacy/provider.php | 44 + tab/venue/classes/tab.php | 173 ++++ tab/venue/db/events.php | 40 + tab/venue/db/install.php | 35 + tab/venue/db/install.xml | 37 + tab/venue/db/services.php | 54 ++ tab/venue/lang/en/videotimetab_venue.php | 33 + tab/venue/lib.php | 91 ++ tab/venue/settings.php | 36 + tab/venue/templates/main.mustache | 90 ++ tab/venue/templates/upgrade.mustache | 32 + tab/venue/version.php | 34 + 25 files changed, 2469 insertions(+) create mode 100644 tab/venue/amd/build/venue_manager.min.js create mode 100644 tab/venue/amd/build/venue_manager.min.js.map create mode 100644 tab/venue/amd/src/venue_manager.js create mode 100644 tab/venue/backup/moodle2/backup_videotimetab_venue_subplugin.class.php create mode 100644 tab/venue/backup/moodle2/restore_videotimetab_venue_subplugin.class.php create mode 100644 tab/venue/classes/event/audiobridge_launched.php create mode 100644 tab/venue/classes/event/hand_lower_sent.php create mode 100644 tab/venue/classes/event/hand_raise_sent.php create mode 100644 tab/venue/classes/external/get_room.php create mode 100644 tab/venue/classes/external/raise_hand.php create mode 100644 tab/venue/classes/external/venue_settings.php create mode 100644 tab/venue/classes/external/venue_status.php create mode 100644 tab/venue/classes/output/main.php create mode 100644 tab/venue/classes/privacy/provider.php create mode 100644 tab/venue/classes/tab.php create mode 100644 tab/venue/db/events.php create mode 100644 tab/venue/db/install.php create mode 100644 tab/venue/db/install.xml create mode 100644 tab/venue/db/services.php create mode 100644 tab/venue/lang/en/videotimetab_venue.php create mode 100644 tab/venue/lib.php create mode 100644 tab/venue/settings.php create mode 100644 tab/venue/templates/main.mustache create mode 100644 tab/venue/templates/upgrade.mustache create mode 100644 tab/venue/version.php 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, +]; From b7763686f459c956d158df24ee1a105b070c81b8 Mon Sep 17 00:00:00 2001 From: Daniel Thies Date: Fri, 4 Aug 2023 08:10:26 -0500 Subject: [PATCH 3/5] VID-683: Move session code to pro plugin --- amd/build/player.min.js | 2 +- amd/build/player.min.js.map | 2 +- amd/build/resize_tab_player.min.js.map | 2 +- amd/build/videotime.min.js | 2 +- amd/build/videotime.min.js.map | 2 +- amd/src/videotime.js | 32 ++++++++++++++++--- plugin/videojs/amd/build/videotime.min.js | 2 +- plugin/videojs/amd/build/videotime.min.js.map | 2 +- plugin/videojs/amd/src/videotime.js | 14 ++------ 9 files changed, 37 insertions(+), 23 deletions(-) diff --git a/amd/build/player.min.js b/amd/build/player.min.js index d25dd02c..667e0299 100644 --- a/amd/build/player.min.js +++ b/amd/build/player.min.js @@ -1,3 +1,3 @@ -var e,t;e=window,t=function(){function r(e,t){for(var n=0;n\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Player from \"mod_videotime/player\";\nimport Notification from \"core/notification\";\n\nlet column;\n\n/**\n * Intialize listener\n */\nexport const initialize = () => {\n let observer = new ResizeObserver(resize),\n mutationobserver = new MutationObserver(resize);\n mutationobserver.observe(document.querySelector('#page-content'), {subtree: true, childList: true});\n document.querySelectorAll('.instance-container, div.videotime-tab-instance').forEach((container) => {\n observer.observe(container);\n });\n document.querySelectorAll('.videotime-tab-instance').forEach((instance) => {\n instance.style.position = 'absolute';\n });\n resize();\n\n window.removeEventListener('mousemove', mousemoveHandler);\n window.addEventListener('mousemove', mousemoveHandler);\n\n window.removeEventListener('dragstart', dragstartHandler);\n window.addEventListener('dragstart', dragstartHandler);\n\n window.removeEventListener('mouseup', dragendHandler);\n window.addEventListener('mouseup', dragendHandler);\n\n window.removeEventListener('click', cueVideo);\n window.addEventListener('click', cueVideo);\n};\n\n/**\n * Adjust player position when the page configuration is changed\n */\nconst resize = () => {\n document.querySelectorAll('.instance-container').forEach((container) => {\n if (!container.offsetWidth) {\n // Ignore if it is not visible.\n return;\n }\n container.closest('.videotimetabs').querySelectorAll('.videotime-tab-instance').forEach(() => {\n let instance = container.closest('.videotimetabs').querySelector('.videotime-tab-instance'),\n content = container.closest('.videotimetabs').querySelector('.tab-content');\n Object.assign(instance.style, {\n top: container.offsetTop + 'px',\n left: container.offsetLeft + 'px',\n maxWidth: container.offsetWidth + 'px',\n width: container.offsetWidth + 'px'\n });\n container.style.minHeight = instance.offsetHeight + 5 + 'px';\n container.closest('.videotimetabs').querySelectorAll('.videotime-tab-instance-cover').forEach((cover) => {\n Object.assign(cover.style, {\n height: content.offsetHeight + 'px',\n left: content.offsetLeft + 'px',\n top: content.offsetTop + 'px',\n width: content.offsetWidth + 'px'\n });\n });\n });\n });\n};\n\n/**\n * Reset handle when drag ends\n */\nconst dragendHandler = () => {\n document.querySelectorAll('.videotime-tab-instance-cover').forEach((cover) => {\n cover.style.display = 'none';\n });\n};\n\n/**\n * Prepare to drag divider\n *\n * @param {event} e mouse event\n */\nconst dragstartHandler = (e) => {\n if (e.target.classList.contains('videotimetab-resize-handle')) {\n column = e.target.closest('.tab-pane').querySelector('.videotimetab-resize');\n e.stopPropagation();\n e.preventDefault();\n document.querySelectorAll('.videotime-tab-instance-cover').forEach((cover) => {\n cover.style.display = 'block';\n });\n }\n};\n\n/**\n * Resize the content and player to mouse location\n *\n * @param {event} e mouse event\n */\nconst mousemoveHandler = (e) => {\n document.querySelectorAll('.videotimetab-resize-handle').forEach((h) => {\n if (h.closest('.tab-pane') && document.querySelector('.videotime-tab-instance-cover').style.display == 'block') {\n column.style.width = e.pageX - column.getBoundingClientRect().left + 'px';\n }\n });\n};\n\n/**\n * Move video to new time when link clicked\n *\n * @param {event} e mouse event\n */\nconst cueVideo = (e) => {\n if (e.target.closest('[data-action=\"cue\"]')) {\n let starttime = e.target.closest('a').getAttribute('data-start'),\n time = starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+)?)/),\n iframe = e.target.closest('.videotimetabs').querySelector('.vimeo-embed iframe'),\n player = new Player(iframe);\n e.preventDefault();\n e.stopPropagation();\n if (time) {\n player\n .setCurrentTime(3600 * Number(time[3] || 0) + 60 * Number(time[5] || 0) + Number(time[6]))\n .then(player.play.bind(player))\n .catch(Notification.exception);\n }\n }\n};\n"],"names":["column","observer","ResizeObserver","resize","MutationObserver","observe","document","querySelector","subtree","childList","querySelectorAll","forEach","container","instance","style","position","window","removeEventListener","mousemoveHandler","addEventListener","dragstartHandler","dragendHandler","cueVideo","offsetWidth","closest","content","Object","assign","top","offsetTop","left","offsetLeft","maxWidth","width","minHeight","offsetHeight","cover","height","display","e","target","classList","contains","stopPropagation","preventDefault","h","pageX","getBoundingClientRect","time","getAttribute","match","iframe","player","Player","setCurrentTime","Number","then","play","bind","catch","Notification","exception"],"mappings":";;;;;;;;SAYIA,0MAKsB,SAClBC,SAAW,IAAIC,eAAeC,QACX,IAAIC,iBAAiBD,QAC3BE,QAAQC,SAASC,cAAc,iBAAkB,CAACC,SAAS,EAAMC,WAAW,IAC7FH,SAASI,iBAAiB,mDAAmDC,SAASC,YAClFX,SAASI,QAAQO,cAErBN,SAASI,iBAAiB,2BAA2BC,SAASE,WAC1DA,SAASC,MAAMC,SAAW,cAE9BZ,SAEAa,OAAOC,oBAAoB,YAAaC,kBACxCF,OAAOG,iBAAiB,YAAaD,kBAErCF,OAAOC,oBAAoB,YAAaG,kBACxCJ,OAAOG,iBAAiB,YAAaC,kBAErCJ,OAAOC,oBAAoB,UAAWI,gBACtCL,OAAOG,iBAAiB,UAAWE,gBAEnCL,OAAOC,oBAAoB,QAASK,UACpCN,OAAOG,iBAAiB,QAASG,iBAM/BnB,OAAS,KACXG,SAASI,iBAAiB,uBAAuBC,SAASC,YACjDA,UAAUW,aAIfX,UAAUY,QAAQ,kBAAkBd,iBAAiB,2BAA2BC,SAAQ,SAChFE,SAAWD,UAAUY,QAAQ,kBAAkBjB,cAAc,2BAC7DkB,QAAUb,UAAUY,QAAQ,kBAAkBjB,cAAc,gBAChEmB,OAAOC,OAAOd,SAASC,MAAO,CAC1Bc,IAAKhB,UAAUiB,UAAY,KAC3BC,KAAMlB,UAAUmB,WAAa,KAC7BC,SAAUpB,UAAUW,YAAc,KAClCU,MAAOrB,UAAUW,YAAc,OAEnCX,UAAUE,MAAMoB,UAAYrB,SAASsB,aAAe,EAAI,KACxDvB,UAAUY,QAAQ,kBAAkBd,iBAAiB,iCAAiCC,SAASyB,QAC3FV,OAAOC,OAAOS,MAAMtB,MAAO,CACvBuB,OAAQZ,QAAQU,aAAe,KAC/BL,KAAML,QAAQM,WAAa,KAC3BH,IAAKH,QAAQI,UAAY,KACzBI,MAAOR,QAAQF,YAAc,iBAU3CF,eAAiB,KACnBf,SAASI,iBAAiB,iCAAiCC,SAASyB,QAChEA,MAAMtB,MAAMwB,QAAU,WASxBlB,iBAAoBmB,IAClBA,EAAEC,OAAOC,UAAUC,SAAS,gCAC5B1C,OAASuC,EAAEC,OAAOhB,QAAQ,aAAajB,cAAc,wBACrDgC,EAAEI,kBACFJ,EAAEK,iBACFtC,SAASI,iBAAiB,iCAAiCC,SAASyB,QAChEA,MAAMtB,MAAMwB,QAAU,aAU5BpB,iBAAoBqB,IACtBjC,SAASI,iBAAiB,+BAA+BC,SAASkC,IAC1DA,EAAErB,QAAQ,cAAyF,SAAzElB,SAASC,cAAc,iCAAiCO,MAAMwB,UACxFtC,OAAOc,MAAMmB,MAAQM,EAAEO,MAAQ9C,OAAO+C,wBAAwBjB,KAAO,UAU3ER,SAAYiB,OACVA,EAAEC,OAAOhB,QAAQ,uBAAwB,KAErCwB,KADYT,EAAEC,OAAOhB,QAAQ,KAAKyB,aAAa,cAC9BC,MAAM,iDACvBC,OAASZ,EAAEC,OAAOhB,QAAQ,kBAAkBjB,cAAc,uBAC1D6C,OAAS,IAAIC,gBAAOF,QACxBZ,EAAEK,iBACFL,EAAEI,kBACEK,MACAI,OACKE,eAAe,KAAOC,OAAOP,KAAK,IAAM,GAAK,GAAKO,OAAOP,KAAK,IAAM,GAAKO,OAAOP,KAAK,KACrFQ,KAAKJ,OAAOK,KAAKC,KAAKN,SACtBO,MAAMC,sBAAaC"} \ No newline at end of file +{"version":3,"file":"resize_tab_player.min.js","sources":["../src/resize_tab_player.js"],"sourcesContent":["/*\n * Position the Vimeo player within tab layout\n *\n * @package mod_videotime\n * @module mod_videotime/resize_tab_player\n * @copyright 2021 bdecent gmbh \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Player from \"mod_videotime/player\";\nimport Notification from \"core/notification\";\n\nlet column;\n\n/**\n * Intialize listener\n */\nexport const initialize = () => {\n let observer = new ResizeObserver(resize),\n mutationobserver = new MutationObserver(resize);\n mutationobserver.observe(document.querySelector('#page-content'), {subtree: true, childList: true});\n document.querySelectorAll('.instance-container, div.videotime-tab-instance').forEach((container) => {\n observer.observe(container);\n });\n document.querySelectorAll('.videotime-tab-instance').forEach((instance) => {\n instance.style.position = 'absolute';\n });\n resize();\n\n window.removeEventListener('mousemove', mousemoveHandler);\n window.addEventListener('mousemove', mousemoveHandler);\n\n window.removeEventListener('dragstart', dragstartHandler);\n window.addEventListener('dragstart', dragstartHandler);\n\n window.removeEventListener('mouseup', dragendHandler);\n window.addEventListener('mouseup', dragendHandler);\n\n window.removeEventListener('click', cueVideo);\n window.addEventListener('click', cueVideo);\n};\n\n/**\n * Adjust player position when the page configuration is changed\n */\nconst resize = () => {\n document.querySelectorAll('.instance-container').forEach((container) => {\n if (!container.offsetWidth) {\n // Ignore if it is not visible.\n return;\n }\n container.closest('.videotimetabs').querySelectorAll('.videotime-tab-instance').forEach(() => {\n let instance = container.closest('.videotimetabs').querySelector('.videotime-tab-instance'),\n content = container.closest('.videotimetabs').querySelector('.tab-content');\n Object.assign(instance.style, {\n top: container.offsetTop + 'px',\n left: container.offsetLeft + 'px',\n maxWidth: container.offsetWidth + 'px',\n width: container.offsetWidth + 'px'\n });\n container.style.minHeight = instance.offsetHeight + 5 + 'px';\n container.closest('.videotimetabs').querySelectorAll('.videotime-tab-instance-cover').forEach((cover) => {\n Object.assign(cover.style, {\n height: content.offsetHeight + 'px',\n left: content.offsetLeft + 'px',\n top: content.offsetTop + 'px',\n width: content.offsetWidth + 'px'\n });\n });\n });\n });\n};\n\n/**\n * Reset handle when drag ends\n */\nconst dragendHandler = () => {\n document.querySelectorAll('.videotime-tab-instance-cover').forEach((cover) => {\n cover.style.display = 'none';\n });\n};\n\n/**\n * Prepare to drag divider\n *\n * @param {event} e mouse event\n */\nconst dragstartHandler = (e) => {\n if (e.target.classList.contains('videotimetab-resize-handle')) {\n column = e.target.closest('.tab-pane').querySelector('.videotimetab-resize');\n e.stopPropagation();\n e.preventDefault();\n document.querySelectorAll('.videotime-tab-instance-cover').forEach((cover) => {\n cover.style.display = 'block';\n });\n }\n};\n\n/**\n * Resize the content and player to mouse location\n *\n * @param {event} e mouse event\n */\nconst mousemoveHandler = (e) => {\n document.querySelectorAll('.videotimetab-resize-handle').forEach((h) => {\n if (h.closest('.tab-pane') && document.querySelector('.videotime-tab-instance-cover').style.display == 'block') {\n column.style.width = e.pageX - column.getBoundingClientRect().left + 'px';\n }\n });\n};\n\n/**\n * Move video to new time when link clicked\n *\n * @param {event} e mouse event\n */\nconst cueVideo = (e) => {\n if (e.target.closest('[data-action=\"cue\"]')) {\n let starttime = e.target.closest('a').getAttribute('data-start'),\n time = starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+)?)/),\n iframe = e.target.closest('.videotimetabs').querySelector('.vimeo-embed iframe'),\n player = new Player(iframe);\n e.preventDefault();\n e.stopPropagation();\n if (time) {\n player\n .setCurrentTime(3600 * Number(time[3] || 0) + 60 * Number(time[5] || 0) + Number(time[6]))\n .then(player.play.bind(player))\n .catch(Notification.exception);\n }\n }\n};\n"],"names":["column","observer","ResizeObserver","resize","MutationObserver","observe","document","querySelector","subtree","childList","querySelectorAll","forEach","container","instance","style","position","window","removeEventListener","mousemoveHandler","addEventListener","dragstartHandler","dragendHandler","cueVideo","offsetWidth","closest","content","Object","assign","top","offsetTop","left","offsetLeft","maxWidth","width","minHeight","offsetHeight","cover","height","display","e","target","classList","contains","stopPropagation","preventDefault","h","pageX","getBoundingClientRect","time","getAttribute","match","iframe","player","Player","setCurrentTime","Number","then","play","bind","catch","Notification","exception"],"mappings":";;;;;;;;SAYIA,0MAKsB,SAClBC,SAAW,IAAIC,eAAeC,QACX,IAAIC,iBAAiBD,QAC3BE,QAAQC,SAASC,cAAc,iBAAkB,CAACC,SAAS,EAAMC,WAAW,IAC7FH,SAASI,iBAAiB,mDAAmDC,SAASC,YAClFX,SAASI,QAAQO,cAErBN,SAASI,iBAAiB,2BAA2BC,SAASE,WAC1DA,SAASC,MAAMC,SAAW,UAA1B,IAEJZ,SAEAa,OAAOC,oBAAoB,YAAaC,kBACxCF,OAAOG,iBAAiB,YAAaD,kBAErCF,OAAOC,oBAAoB,YAAaG,kBACxCJ,OAAOG,iBAAiB,YAAaC,kBAErCJ,OAAOC,oBAAoB,UAAWI,gBACtCL,OAAOG,iBAAiB,UAAWE,gBAEnCL,OAAOC,oBAAoB,QAASK,UACpCN,OAAOG,iBAAiB,QAASG,iBAM/BnB,OAAS,KACXG,SAASI,iBAAiB,uBAAuBC,SAASC,YACjDA,UAAUW,aAIfX,UAAUY,QAAQ,kBAAkBd,iBAAiB,2BAA2BC,SAAQ,SAChFE,SAAWD,UAAUY,QAAQ,kBAAkBjB,cAAc,2BAC7DkB,QAAUb,UAAUY,QAAQ,kBAAkBjB,cAAc,gBAChEmB,OAAOC,OAAOd,SAASC,MAAO,CAC1Bc,IAAKhB,UAAUiB,UAAY,KAC3BC,KAAMlB,UAAUmB,WAAa,KAC7BC,SAAUpB,UAAUW,YAAc,KAClCU,MAAOrB,UAAUW,YAAc,OAEnCX,UAAUE,MAAMoB,UAAYrB,SAASsB,aAAe,EAAI,KACxDvB,UAAUY,QAAQ,kBAAkBd,iBAAiB,iCAAiCC,SAASyB,QAC3FV,OAAOC,OAAOS,MAAMtB,MAAO,CACvBuB,OAAQZ,QAAQU,aAAe,KAC/BL,KAAML,QAAQM,WAAa,KAC3BH,IAAKH,QAAQI,UAAY,KACzBI,MAAOR,QAAQF,YAAc,iBAU3CF,eAAiB,KACnBf,SAASI,iBAAiB,iCAAiCC,SAASyB,QAChEA,MAAMtB,MAAMwB,QAAU,MAAtB,KASFlB,iBAAoBmB,IAClBA,EAAEC,OAAOC,UAAUC,SAAS,gCAC5B1C,OAASuC,EAAEC,OAAOhB,QAAQ,aAAajB,cAAc,wBACrDgC,EAAEI,kBACFJ,EAAEK,iBACFtC,SAASI,iBAAiB,iCAAiCC,SAASyB,QAChEA,MAAMtB,MAAMwB,QAAU,OAAtB,MAUNpB,iBAAoBqB,IACtBjC,SAASI,iBAAiB,+BAA+BC,SAASkC,IAC1DA,EAAErB,QAAQ,cAAyF,SAAzElB,SAASC,cAAc,iCAAiCO,MAAMwB,UACxFtC,OAAOc,MAAMmB,MAAQM,EAAEO,MAAQ9C,OAAO+C,wBAAwBjB,KAAO,UAU3ER,SAAYiB,OACVA,EAAEC,OAAOhB,QAAQ,uBAAwB,KAErCwB,KADYT,EAAEC,OAAOhB,QAAQ,KAAKyB,aAAa,cAC9BC,MAAM,iDACvBC,OAASZ,EAAEC,OAAOhB,QAAQ,kBAAkBjB,cAAc,uBAC1D6C,OAAS,IAAIC,gBAAOF,QACxBZ,EAAEK,iBACFL,EAAEI,kBACEK,MACAI,OACKE,eAAe,KAAOC,OAAOP,KAAK,IAAM,GAAK,GAAKO,OAAOP,KAAK,IAAM,GAAKO,OAAOP,KAAK,KACrFQ,KAAKJ,OAAOK,KAAKC,KAAKN,SACtBO,MAAMC,sBAAaC"} \ No newline at end of file diff --git a/amd/build/videotime.min.js b/amd/build/videotime.min.js index 0ea4f2c8..939d618e 100644 --- a/amd/build/videotime.min.js +++ b/amd/build/videotime.min.js @@ -3,6 +3,6 @@ * @copyright 2021 bdecent gmbh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("mod_videotime/videotime",["jquery","mod_videotime/player","core/ajax","core/config","core/log","core/templates","core/notification"],(function($,Vimeo,Ajax,Config,Log,Templates,Notification){let VideoTime=function(elementId,cmId,hasPro,interval,instance){this.elementId=elementId,this.cmId=cmId,this.hasPro=hasPro,this.interval=interval,this.player=null,this.resumeTime=null,this.session=null,this.instance=instance,this.played=!1,this.playing=!1,this.time=0,this.percent=0,this.currentTime=0,this.playbackRate=1,this.plugins=[],hasPro&&$("body").hasClass("path-course-view")&&!$("body").hasClass("vtinit")&&($("body").addClass("vtinit"),$(document).on("focus","body",this.initializeNewInstances.bind(this))),this.modulecount=$("body .activity.videotime").length};return VideoTime.prototype.getCmId=function(){return this.cmId},VideoTime.prototype.registerPlugin=function(plugin){this.plugins.push(plugin)},VideoTime.prototype.initialize=function(){let instance=this.instance;Log.debug("Initializing Video Time "+this.elementId),Log.debug("Initializing Vimeo player with options:"),Log.debug(instance),this.player=new Vimeo(this.elementId,{autopause:Number(instance.autopause),autoplay:Number(instance.autoplay),background:Number(instance.background),byline:Number(instance.byline),color:instance.color,controls:Number(instance.controls),dnt:Number(instance.dnt),height:instance.height,loop:Number(instance.option_loop),maxheight:instance.maxheight,maxwidth:instance.maxwidth,muted:Number(instance.muted),portrait:instance.portrait,pip:Number(instance.pip),playsinline:instance.playsinline,responsive:Number(instance.responsive),speed:instance.speed,title:Number(instance.title),transparent:Number(instance.transparent),url:instance.vimeo_url,width:instance.width});let url=new URL(window.location.href),q=url.searchParams.get("q"),starttime=(url.searchParams.get("time")||"").match(/^([0-9]+:){0,2}([0-9]+)(\.[0-9]+)$/);starttime?this.setStartTime(starttime[0]).then((function(){return q&&window.find&&window.find(q),!0})).catch(Notification.exception):q&&window.find&&window.find(q),this.addListeners();for(let i=0;i(this.hasPro&&this.startWatchInterval(),this.view(),!0))),this.hasPro&&(this.player.on("loaded",(()=>!this.instance.resume_playback||this.instance.resume_time<=0||this.getDuration().then((duration=>{let resumeTime=this.instance.resume_time;return resumeTime+1>=Math.floor(duration)&&(Log.debug("VIDEO_TIME video finished, resuming at start of video."),resumeTime=0),Log.debug("VIDEO_TIME duration is "+duration),Log.debug("VIDEO_TIME resuming at "+resumeTime),this.setCurrentPosition(resumeTime),!0})).fail(Notification.exception))),this.player.on("play",function(){this.playing=!0,Log.debug("VIDEO_TIME play")}.bind(this)),this.player.on("playing",function(){this.playing=!0,Log.debug("VIDEO_TIME playing")}.bind(this)),this.player.on("pause",function(){this.playing=!1,Log.debug("VIDEO_TIME pause")}.bind(this)),this.player.on("stalled",function(){this.playing=!1,Log.debug("VIDEO_TIME stalled")}.bind(this)),this.player.on("suspend",function(){this.playing=!1,Log.debug("VIDEO_TIME suspend")}.bind(this)),this.player.on("abort",function(){this.playing=!1,Log.debug("VIDEO_TIME abort")}.bind(this)),this.player.getPlaybackRate().then(function(playbackRate){this.playbackRate=playbackRate}.bind(this)).catch(Notification.exception),this.player.on("playbackratechange",function(event){this.playbackRate=event.playbackRate}.bind(this)),this.player.on("timeupdate",function(event){this.percent=event.percent,this.currentTime=event.seconds,Log.debug("VIDEO_TIME timeupdate. Percent: "+this.percent+". Current time: "+this.currentTime)}.bind(this)),this.player.on("ended",this.handleEnd.bind(this)))):Log.debug("Player was not properly initialized for course module "+this.cmId)},VideoTime.prototype.handleEnd=function(){this.playing=!1,Log.debug("VIDEO_TIME ended"),this.getSession().then(function(session){this.setSessionState(session.id,1).then((()=>this.setPercent(session.id,1))).then((()=>this.setCurrentTime(session.id,this.currentTime))).then((()=>this.getNextActivityButtonData(session.id).then((response=>{let data=JSON.parse(response.data);if(data.instance&&parseInt(data.instance.next_activity_auto)&&!data.is_restricted&&data.hasnextcm){let link=$('.aalink[href="'+data.nextcm_url+'"] img').first();$(".path-course-view").length&&link?link.click():window.location.href=data.nextcm_url}return Templates.render("videotime/next_activity_button",JSON.parse(response.data)).then((function(html){return $("#next-activity-button").html(html),!0}))})))).catch(Notification.exception)}.bind(this)).catch(Notification.exception)},VideoTime.prototype.startWatchInterval=function(){this.watchInterval||(this.watchInterval=setInterval(function(){this.playing&&(this.time+=this.playbackRate,this.getSession().then(function(session){return this.time%this.interval==0&&(Log.debug("VIDEO_TIME watch_time: "+this.time+". percent: "+this.percent),this.recordWatchTime(session.id,this.time),this.setPercent(session.id,this.percent),this.setCurrentTime(session.id,this.currentTime)),!0}.bind(this)).catch(Notification.exception))}.bind(this),1e3))},VideoTime.prototype.setSessionState=function(sessionId,state){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_set_session_state"),data.set("state",state),data.set("session_id",sessionId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_set_session_state",args:{session_id:sessionId,state:state},fail:Notification.exception}])[0]},VideoTime.prototype.setCurrentTime=function(sessionId,currentTime){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_set_session_current_time"),data.set("current_time",currentTime),data.set("session_id",sessionId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_set_session_current_time",args:{session_id:sessionId,current_time:currentTime},fail:Notification.exception}])[0]},VideoTime.prototype.setPercent=function(sessionId,percent){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_set_percent"),data.set("percent",percent),data.set("session_id",sessionId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_set_percent",args:{session_id:sessionId,percent:percent},fail:Notification.exception}])[0]},VideoTime.prototype.recordWatchTime=function(sessionId,time){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_record_watch_time"),data.set("session_id",sessionId),data.set("time",time),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_record_watch_time",args:{session_id:sessionId,time:time},fail:Notification.exception}])[0]},VideoTime.prototype.getNextActivityButtonData=function(sessionId){return this.instance.token?Promise.resolve({data:"{}"}):Ajax.call([{methodname:"videotimeplugin_pro_get_next_activity_button_data",args:{session_id:sessionId}}])[0]},VideoTime.prototype.getInstance=function(){return this.instance?Promise.resolve(this.instance):Ajax.call([{methodname:"mod_videotime_get_videotime",args:{cmid:this.cmId},done:response=>(this.instance=response,this.instance),fail:Notification.exception}])[0]},VideoTime.prototype.getResumeTime=function(){return this.resumeTime?Promise.resolve(this.resumeTime):Ajax.call([{methodname:"videotimeplugin_pro_get_resume_time",args:{cmid:this.cmId},done:response=>(this.resumeTime=response.seconds,this.resumeTime),fail:Notification.exception}])[0]},VideoTime.prototype.getSession=function(){if(this.instance.token){if(!this.session){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_get_new_session"),data.set("cmid",this.cmId),this.session=fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return this.session}return this.session||(this.session=Ajax.call([{methodname:"videotimeplugin_pro_get_new_session",args:{cmid:this.cmId},fail:Notification.exception}])[0]),this.session},VideoTime.prototype.setStartTime=function(starttime){let time=starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\.[0-9]+))/);return time&&(this.resumeTime=3600*Number(time[3]||0)+60*Number(time[5]||0)+Number(time[6]),this.currentTime(this.resumeTime)),this.player.getCurrentTime()},VideoTime.prototype.view=function(){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","mod_videotime_view_videotime"),data.set("cmid",this.cmId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"mod_videotime_view_videotime",args:{cmid:this.cmId},fail:Notification.exception}])[0]},VideoTime.prototype.initializeNewInstances=function(){this.modulecount!=$("body .activity.videotime").length&&(this.modulecount=$("body .activity.videotime").length,$("body .activity.videotime").each(function(index,module){if(!$(module).find(".instancename").length&&$(module).find(".vimeo-embed").length&&!$(module).find(".vimeo-embed iframe").length){let instance={cmid:Number($(module).attr("id").replace("module-","")),haspro:!0,interval:this.interval,uniqueid:$(module).find(".vimeo-embed").first().attr("id").replace("vimeo-embed-","")};Templates.render("mod_videotime/videotime_instance",{instance:instance}).then((function(html,js){return Templates.runTemplateJS(js),!0})).fail(Notification.exception)}}.bind(this)))},VideoTime.prototype.getPlaybackRate=function(){return this.player.getPlaybackRate()},VideoTime.prototype.getDuration=function(){return this.player.getDuration()},VideoTime.prototype.setCurrentPosition=function(secs){return this.player.setCurrentTime(secs)},VideoTime.prototype.getCurrentPosition=function(){return this.player.getCurrentTime()},VideoTime})); +define("mod_videotime/videotime",["jquery","mod_videotime/player","core/ajax","core/config","core/log","core/templates","core/notification"],(function($,Vimeo,Ajax,Config,Log,Templates,Notification){let VideoTime=function(elementId,cmId,hasPro,interval,instance){this.elementId=elementId,this.cmId=cmId,this.hasPro=hasPro,this.interval=interval,this.player=null,this.resumeTime=null,this.session=null,this.instance=instance,this.played=!1,this.playing=!1,this.time=0,this.percent=0,this.currentTime=0,this.playbackRate=1,this.plugins=[],hasPro&&$("body").hasClass("path-course-view")&&!$("body").hasClass("vtinit")&&($("body").addClass("vtinit"),$(document).on("focus","body",this.initializeNewInstances.bind(this))),this.modulecount=$("body .activity.videotime").length};return VideoTime.prototype.getCmId=function(){return this.cmId},VideoTime.prototype.registerPlugin=function(plugin){this.plugins.push(plugin)},VideoTime.prototype.initialize=function(){let instance=this.instance;Log.debug("Initializing Video Time "+this.elementId),Log.debug("Initializing Vimeo player with options:"),Log.debug(instance),this.player=new Vimeo(this.elementId,{autopause:Number(instance.autopause),autoplay:Number(instance.autoplay),background:Number(instance.background),byline:Number(instance.byline),color:instance.color,controls:Number(instance.controls),dnt:Number(instance.dnt),height:instance.height,loop:Number(instance.option_loop),maxheight:instance.maxheight,maxwidth:instance.maxwidth,muted:Number(instance.muted),portrait:instance.portrait,pip:Number(instance.pip),playsinline:instance.playsinline,responsive:Number(instance.responsive),speed:instance.speed,title:Number(instance.title),transparent:Number(instance.transparent),url:instance.vimeo_url,width:instance.width});let url=new URL(window.location.href),q=url.searchParams.get("q"),starttime=(url.searchParams.get("time")||"").match(/^([0-9]+:){0,2}([0-9]+)(\.[0-9]+)$/);starttime?this.setStartTime(starttime[0]).then((function(){return q&&window.find&&window.find(q),!0})).catch(Notification.exception):q&&window.find&&window.find(q),this.addListeners();for(let i=0;i(this.hasPro&&this.startWatchInterval(),this.view(),!0))),this.hasPro&&(this.player.on("loaded",(()=>!this.instance.resume_playback||!this.instance.resume_time||this.instance.resume_time<=0||this.getDuration().then((duration=>{let resumeTime=this.instance.resume_time;return resumeTime+1>=Math.floor(duration)&&(Log.debug("VIDEO_TIME video finished, resuming at start of video."),resumeTime=0),Log.debug("VIDEO_TIME duration is "+duration),Log.debug("VIDEO_TIME resuming at "+resumeTime),this.setCurrentPosition(resumeTime),!0})).fail(Notification.exception))),this.player.on("play",function(){this.playing=!0,Log.debug("VIDEO_TIME play")}.bind(this)),this.player.on("playing",function(){this.playing=!0,Log.debug("VIDEO_TIME playing")}.bind(this)),this.player.on("pause",function(){this.playing=!1,Log.debug("VIDEO_TIME pause")}.bind(this)),this.player.on("stalled",function(){this.playing=!1,Log.debug("VIDEO_TIME stalled")}.bind(this)),this.player.on("suspend",function(){this.playing=!1,Log.debug("VIDEO_TIME suspend")}.bind(this)),this.player.on("abort",function(){this.playing=!1,Log.debug("VIDEO_TIME abort")}.bind(this)),this.player.getPlaybackRate().then(function(playbackRate){this.playbackRate=playbackRate}.bind(this)).catch(Notification.exception),this.player.on("playbackratechange",function(event){this.playbackRate=event.playbackRate}.bind(this)),this.player.on("timeupdate",function(event){this.percent=event.percent,this.currentTime=event.seconds,event.seconds===event.duration&&this.plugins.forEach((plugin=>{"function"==typeof plugin.setCurrentTime&&plugin.getSessions().then((session=>{plugin.setCurrentTime(session.id,event.seconds)}))}))}.bind(this)),this.player.on("ended",this.handleEnd.bind(this)))):Log.debug("Player was not properly initialized for course module "+this.cmId)},VideoTime.prototype.handleEnd=function(){this.playing=!1,Log.debug("VIDEO_TIME ended"),this.plugins.length>2?this.plugins.forEach((plugin=>{"function"==typeof plugin.handleEnd&&plugin.handleEnd()})):this.getSession().then(function(session){this.setSessionState(session.id,1).then((()=>this.setPercent(session.id,1))).then((()=>this.setCurrentTime(session.id,this.currentTime))).then((()=>this.getNextActivityButtonData(session.id).then((response=>{let data=JSON.parse(response.data);if(data.instance&&parseInt(data.instance.next_activity_auto)&&!data.is_restricted&&data.hasnextcm){let link=$('.aalink[href="'+data.nextcm_url+'"] img').first();$(".path-course-view").length&&link?link.click():window.location.href=data.nextcm_url}return Templates.render("videotime/next_activity_button",JSON.parse(response.data)).then((function(html){return $("#next-activity-button").html(html),!0}))})))).catch(Notification.exception)}.bind(this)).catch(Notification.exception)},VideoTime.prototype.startWatchInterval=function(){this.plugins.forEach((plugin=>{"function"==typeof plugin.startWatchInterval&&(this.watchInterval=!0,plugin.startWatchInterval())})),this.watchInterval||(this.watchInterval=setInterval(function(){this.playing&&(this.time+=this.playbackRate,this.getSession().then(function(session){return this.time%this.interval==0&&(Log.debug("VIDEO_TIME watch_time: "+this.time+". percent: "+this.percent),this.recordWatchTime(session.id,this.time),this.setPercent(session.id,this.percent),this.setCurrentTime(session.id,this.currentTime)),!0}.bind(this)).catch(Notification.exception))}.bind(this),1e3))},VideoTime.prototype.setSessionState=function(sessionId,state){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_set_session_state"),data.set("state",state),data.set("session_id",sessionId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_set_session_state",args:{session_id:sessionId,state:state},fail:Notification.exception}])[0]},VideoTime.prototype.setCurrentTime=function(sessionId,currentTime){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_set_session_current_time"),data.set("current_time",currentTime),data.set("session_id",sessionId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_set_session_current_time",args:{session_id:sessionId,current_time:currentTime},fail:Notification.exception}])[0]},VideoTime.prototype.setPercent=function(sessionId,percent){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_set_percent"),data.set("percent",percent),data.set("session_id",sessionId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_set_percent",args:{session_id:sessionId,percent:percent},fail:Notification.exception}])[0]},VideoTime.prototype.recordWatchTime=function(sessionId,time){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_record_watch_time"),data.set("session_id",sessionId),data.set("time",time),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"videotimeplugin_pro_record_watch_time",args:{session_id:sessionId,time:time},fail:Notification.exception}])[0]},VideoTime.prototype.getNextActivityButtonData=function(sessionId){return this.instance.token?Promise.resolve({data:"{}"}):Ajax.call([{methodname:"videotimeplugin_pro_get_next_activity_button_data",args:{session_id:sessionId}}])[0]},VideoTime.prototype.getInstance=function(){return this.instance?Promise.resolve(this.instance):Ajax.call([{methodname:"mod_videotime_get_videotime",args:{cmid:this.cmId},done:response=>(this.instance=response,this.instance),fail:Notification.exception}])[0]},VideoTime.prototype.getResumeTime=function(){return this.resumeTime?Promise.resolve(this.resumeTime):Ajax.call([{methodname:"videotimeplugin_pro_get_resume_time",args:{cmid:this.cmId},done:response=>(this.resumeTime=response.seconds,this.resumeTime),fail:Notification.exception}])[0]},VideoTime.prototype.getSession=function(){if(this.instance.token){if(!this.session){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","videotimeplugin_pro_get_new_session"),data.set("cmid",this.cmId),this.session=fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return this.session}return this.session||(this.session=Ajax.call([{methodname:"videotimeplugin_pro_get_new_session",args:{cmid:this.cmId},fail:Notification.exception}])[0]),this.session},VideoTime.prototype.setStartTime=function(starttime){let time=starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\.[0-9]+))/);return time&&(this.resumeTime=3600*Number(time[3]||0)+60*Number(time[5]||0)+Number(time[6]),this.currentTime(this.resumeTime)),this.player.getCurrentTime()},VideoTime.prototype.view=function(){if(this.instance.token){const url=new URL(Config.wwwroot+"/webservice/rest/server.php"),data=url.searchParams;return data.set("wstoken",this.instance.token),data.set("moodlewsrestformat","json"),data.set("wsfunction","mod_videotime_view_videotime"),data.set("cmid",this.cmId),fetch(url).then((response=>(response.ok||Notification.exeption("Web service error"),response.json())))}return Ajax.call([{methodname:"mod_videotime_view_videotime",args:{cmid:this.cmId},fail:Notification.exception}])[0]},VideoTime.prototype.initializeNewInstances=function(){this.modulecount!=$("body .activity.videotime").length&&(this.modulecount=$("body .activity.videotime").length,$("body .activity.videotime").each(function(index,module){if(!$(module).find(".instancename").length&&$(module).find(".vimeo-embed").length&&!$(module).find(".vimeo-embed iframe").length){let instance={cmid:Number($(module).attr("id").replace("module-","")),haspro:!0,interval:this.interval,uniqueid:$(module).find(".vimeo-embed").first().attr("id").replace("vimeo-embed-","")};Templates.render("mod_videotime/videotime_instance",{instance:instance}).then((function(html,js){return Templates.runTemplateJS(js),!0})).fail(Notification.exception)}}.bind(this)))},VideoTime.prototype.getPlaybackRate=function(){return this.player.getPlaybackRate()},VideoTime.prototype.getDuration=function(){return this.player.getDuration()},VideoTime.prototype.setCurrentPosition=function(secs){return this.player.setCurrentTime(secs)},VideoTime.prototype.getCurrentPosition=function(){return this.player.getCurrentTime()},VideoTime})); //# sourceMappingURL=videotime.min.js.map \ No newline at end of file diff --git a/amd/build/videotime.min.js.map b/amd/build/videotime.min.js.map index e456bebb..57b8e849 100644 --- a/amd/build/videotime.min.js.map +++ b/amd/build/videotime.min.js.map @@ -1 +1 @@ -{"version":3,"file":"videotime.min.js","sources":["../src/videotime.js"],"sourcesContent":["/*\n * @package mod_videotime\n * @copyright 2021 bdecent gmbh \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @module mod_videotime/videotime\n */\ndefine([\n 'jquery',\n 'mod_videotime/player',\n 'core/ajax',\n 'core/config',\n 'core/log',\n 'core/templates',\n 'core/notification'\n], function($, Vimeo, Ajax, Config, Log, Templates, Notification) {\n let VideoTime = function(elementId, cmId, hasPro, interval, instance) {\n this.elementId = elementId;\n this.cmId = cmId;\n this.hasPro = hasPro;\n this.interval = interval;\n this.player = null;\n this.resumeTime = null;\n this.session = null;\n this.instance = instance;\n\n this.played = false;\n\n this.playing = false;\n this.time = 0;\n this.percent = 0;\n this.currentTime = 0;\n this.playbackRate = 1;\n\n this.plugins = [];\n\n if (hasPro && $('body').hasClass('path-course-view') && !$('body').hasClass('vtinit')) {\n $('body').addClass('vtinit');\n $(document).on('focus', 'body', this.initializeNewInstances.bind(this));\n }\n this.modulecount = $('body .activity.videotime').length;\n };\n\n /**\n * Get course module ID of this VideoTime instance.\n *\n * @return {int}\n */\n VideoTime.prototype.getCmId = function() {\n return this.cmId;\n };\n\n /**\n * Register a plugin to hook into VideoTime functionality.\n *\n * @param {VideoTimePlugin} plugin\n */\n VideoTime.prototype.registerPlugin = function(plugin) {\n this.plugins.push(plugin);\n };\n\n VideoTime.prototype.initialize = function() {\n let instance = this.instance;\n Log.debug('Initializing Video Time ' + this.elementId);\n\n Log.debug('Initializing Vimeo player with options:');\n Log.debug(instance);\n this.player = new Vimeo(this.elementId, {\n autopause: Number(instance.autopause),\n autoplay: Number(instance.autoplay),\n background: Number(instance.background),\n byline: Number(instance.byline),\n color: instance.color,\n controls: Number(instance.controls),\n dnt: Number(instance.dnt),\n height: instance.height,\n loop: Number(instance.option_loop),\n maxheight: instance.maxheight,\n maxwidth: instance.maxwidth,\n muted: Number(instance.muted),\n portrait: instance.portrait,\n pip: Number(instance.pip),\n playsinline: instance.playsinline,\n responsive: Number(instance.responsive),\n speed: instance.speed,\n title: Number(instance.title),\n transparent: Number(instance.transparent),\n url: instance.vimeo_url,\n width: instance.width\n });\n\n let url = new URL(window.location.href),\n q = url.searchParams.get('q'),\n starttime = (url.searchParams.get('time') || '').match(/^([0-9]+:){0,2}([0-9]+)(\\.[0-9]+)$/);\n if (starttime) {\n this.setStartTime(starttime[0]).then(function() {\n if (q && window.find) {\n window.find(q);\n }\n return true;\n }).catch(Notification.exception);\n } else if (q && window.find) {\n window.find(q);\n }\n\n this.addListeners();\n\n for (let i = 0; i < this.plugins.length; i++) {\n const plugin = this.plugins[i];\n plugin.initialize(this, instance);\n }\n\n return true;\n };\n\n /**\n * Get Vimeo player object.\n *\n * @returns {Vimeo}\n */\n VideoTime.prototype.getPlayer = function() {\n return this.player;\n };\n\n /**\n * Register player events to respond to user interaction and play progress.\n */\n VideoTime.prototype.addListeners = function() {\n if (!this.player) {\n Log.debug('Player was not properly initialized for course module ' + this.cmId);\n return;\n }\n\n // If this is a tab play set time cues and listener.\n $($('#' + this.elementId).closest('.videotimetabs')).each(function(i, tabs) {\n $(tabs).find('[data-action=\"cue\"]').each(function(index, anchor) {\n let starttime = anchor.getAttribute('data-start'),\n time = starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/);\n if (time) {\n this.player.addCuePoint(\n 3600 * Number(time[3] || 0) + 60 * Number(time[5] || 0) + Number(time[6]),\n {\n starttime: starttime\n }\n ).catch(Notification.exeception);\n }\n }.bind(this));\n\n this.player.on('cuepoint', function(event) {\n if (event.data.starttime) {\n $(tabs).find('.videotime-highlight').removeClass('videotime-highlight');\n $(tabs).find('[data-action=\"cue\"][data-start=\"' + event.data.starttime + '\"]')\n .closest('.row')\n .addClass('videotime-highlight');\n $('.videotime-highlight').each(function() {\n if (this.offsetTop) {\n this.parentNode.scrollTo({\n top: this.offsetTop - 50,\n left: 0,\n behavior: 'smooth'\n });\n }\n });\n }\n });\n }.bind(this));\n\n // Fire view event in Moodle on first play only.\n this.player.on('play', () => {\n if (this.hasPro) {\n this.startWatchInterval();\n }\n this.view();\n return true;\n });\n\n // Features beyond this point are for pro only.\n if (!this.hasPro) {\n return;\n }\n\n // If resume is present force seek the player to that point.\n this.player.on(\"loaded\", () => {\n if (!this.instance.resume_playback || this.instance.resume_time <= 0) {\n return true;\n }\n\n return this.getDuration().then((duration) => {\n let resumeTime = this.instance.resume_time;\n // Duration is often a little greater than a resume time at the end of the video.\n // A user may have watched 100 seconds when the video ends, but the duration may be\n // 100.56 seconds. BUT, sometimes the duration is rounded depending on when the\n // video loads, so it may be 101 seconds. Hence the +1 and Math.floor usage.\n if (resumeTime + 1 >= Math.floor(duration)) {\n Log.debug(\n \"VIDEO_TIME video finished, resuming at start of video.\"\n );\n resumeTime = 0;\n }\n Log.debug(\"VIDEO_TIME duration is \" + duration);\n Log.debug(\"VIDEO_TIME resuming at \" + resumeTime);\n this.setCurrentPosition(resumeTime);\n return true;\n }).fail(Notification.exception);\n });\n\n // Note: Vimeo player does not support multiple events in a single on() call. Each requires it's own function.\n\n // Catch all events where video plays.\n this.player.on('play', function() {\n this.playing = true;\n Log.debug('VIDEO_TIME play');\n }.bind(this));\n this.player.on('playing', function() {\n this.playing = true;\n Log.debug('VIDEO_TIME playing');\n }.bind(this));\n\n // Catch all events where video stops.\n this.player.on('pause', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME pause');\n }.bind(this));\n this.player.on('stalled', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME stalled');\n }.bind(this));\n this.player.on('suspend', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME suspend');\n }.bind(this));\n this.player.on('abort', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME abort');\n }.bind(this));\n\n this.player.getPlaybackRate().then(function(playbackRate) {\n this.playbackRate = playbackRate;\n }.bind(this)).catch(Notification.exception);\n\n this.player.on('playbackratechange', function(event) {\n this.playbackRate = event.playbackRate;\n }.bind(this));\n\n // Always update internal values for percent and current time watched.\n this.player.on('timeupdate', function(event) {\n this.percent = event.percent;\n this.currentTime = event.seconds;\n Log.debug('VIDEO_TIME timeupdate. Percent: ' + this.percent + '. Current time: ' + this.currentTime);\n }.bind(this));\n\n // Initiate video finish procedure.\n this.player.on('ended', this.handleEnd.bind(this));\n };\n\n /**\n * Start interval that will periodically record user progress via Ajax.\n */\n VideoTime.prototype.handleEnd = function() {\n this.playing = false;\n Log.debug('VIDEO_TIME ended');\n\n this.getSession().then(function(session) {\n this.setSessionState(session.id, 1).then(() => {\n return this.setPercent(session.id, 1);\n }).then(() => {\n return this.setCurrentTime(session.id, this.currentTime);\n }).then(() => {\n return this.getNextActivityButtonData(session.id).then(response => {\n let data = JSON.parse(response.data);\n\n if (data.instance && parseInt(data.instance.next_activity_auto)) {\n if (!data.is_restricted && data.hasnextcm) {\n let link = $('.aalink[href=\"' + data.nextcm_url + '\"] img').first();\n if ($('.path-course-view').length && link) {\n link.click();\n } else {\n window.location.href = data.nextcm_url;\n }\n }\n }\n\n return Templates.render('videotime/next_activity_button', JSON.parse(response.data))\n .then(function(html) {\n $('#next-activity-button').html(html);\n return true;\n });\n });\n }).catch(Notification.exception);\n }.bind(this)).catch(Notification.exception);\n };\n\n /**\n * Start interval that will periodically record user progress via Ajax.\n */\n VideoTime.prototype.startWatchInterval = function() {\n if (this.watchInterval) {\n return;\n }\n\n this.watchInterval = setInterval(function() {\n if (this.playing) {\n this.time += this.playbackRate;\n\n this.getSession().then(function(session) {\n if (this.time % this.interval === 0) {\n Log.debug('VIDEO_TIME watch_time: ' + this.time + '. percent: ' + this.percent);\n this.recordWatchTime(session.id, this.time);\n this.setPercent(session.id, this.percent);\n this.setCurrentTime(session.id, this.currentTime);\n }\n return true;\n }.bind(this)).catch(Notification.exception);\n }\n }.bind(this), 1000);\n };\n\n /**\n * Set state on session.\n *\n * @param {int} sessionId\n * @param {int} state\n * @returns {Promise}\n */\n VideoTime.prototype.setSessionState = function(sessionId, state) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_set_session_state');\n data.set('state', state);\n data.set('session_id', sessionId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_set_session_state',\n args: {\"session_id\": sessionId, state: state},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Set current watch time for video. Used for resuming.\n *\n * @param {int} sessionId\n * @param {float} currentTime\n * @returns {Promise}\n */\n VideoTime.prototype.setCurrentTime = function(sessionId, currentTime) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_set_session_current_time');\n data.set('current_time', currentTime);\n data.set('session_id', sessionId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_set_session_current_time',\n args: {\"session_id\": sessionId, \"current_time\": currentTime},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Set video watch percentage for session.\n *\n * @param {int} sessionId\n * @param {float} percent\n * @returns {Promise}\n */\n VideoTime.prototype.setPercent = function(sessionId, percent) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_set_percent');\n data.set('percent', percent);\n data.set('session_id', sessionId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_set_percent',\n args: {\"session_id\": sessionId, percent: percent},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Record watch time for session.\n *\n * @param {int} sessionId\n * @param {float} time\n * @returns {Promise}\n */\n VideoTime.prototype.recordWatchTime = function(sessionId, time) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_record_watch_time');\n data.set('session_id', sessionId);\n data.set('time', time);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_record_watch_time',\n args: {\"session_id\": sessionId, time: time},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Get data for next activity button.\n *\n * @param {int} sessionId\n * @returns {Promise}\n */\n VideoTime.prototype.getNextActivityButtonData = function(sessionId) {\n if (this.instance.token) {\n // We do not support button in iframe.\n return Promise.resolve({data: '{}'});\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_get_next_activity_button_data',\n args: {\"session_id\": sessionId}\n }])[0];\n };\n\n /**\n * Get VideoTime instance for this course module.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getInstance = function() {\n if (this.instance) {\n return Promise.resolve(this.instance);\n }\n\n return Ajax.call([{\n methodname: 'mod_videotime_get_videotime',\n args: {cmid: this.cmId},\n done: (response) => {\n this.instance = response;\n return this.instance;\n },\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Get time to resume video as seconds.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getResumeTime = function() {\n if (this.resumeTime) {\n return Promise.resolve(this.resumeTime);\n }\n\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_get_resume_time',\n args: {cmid: this.cmId},\n done: (response) => {\n this.resumeTime = response.seconds;\n return this.resumeTime;\n },\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Get new or existing video viewing session.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getSession = function() {\n if (this.instance.token) {\n if (!this.session) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_get_new_session');\n data.set('cmid', this.cmId);\n this.session = fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n\n return this.session;\n }\n if (!this.session) {\n this.session = Ajax.call([{\n methodname: 'videotimeplugin_pro_get_new_session',\n args: {cmid: this.cmId},\n fail: Notification.exception\n }])[0];\n }\n return this.session;\n };\n\n /**\n * Parse start time and set player\n *\n * @param {string} starttime\n * @returns {Promise}\n */\n VideoTime.prototype.setStartTime = function(starttime) {\n let time = starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/);\n if (time) {\n this.resumeTime = 3600 * Number(time[3] || 0) + 60 * Number(time[5] || 0) + Number(time[6]);\n this.currentTime(this.resumeTime);\n }\n return this.player.getCurrentTime();\n };\n\n /**\n * Log the user view of video.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.view = function() {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'mod_videotime_view_videotime');\n data.set('cmid', this.cmId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n\n }\n return Ajax.call([{\n methodname: 'mod_videotime_view_videotime',\n args: {cmid: this.cmId},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Initialize new labels and preview when editing\n */\n VideoTime.prototype.initializeNewInstances = function() {\n if (this.modulecount == $('body .activity.videotime').length) {\n return;\n }\n this.modulecount = $('body .activity.videotime').length;\n $('body .activity.videotime').each(function(index, module) {\n if (\n !$(module).find('.instancename').length\n && $(module).find('.vimeo-embed').length\n && !$(module).find('.vimeo-embed iframe').length\n ) {\n let instance = {\n cmid: Number($(module).attr('id').replace('module-', '')),\n haspro: true,\n interval: this.interval,\n uniqueid: $(module).find('.vimeo-embed').first().attr('id').replace('vimeo-embed-', '')\n };\n Templates.render('mod_videotime/videotime_instance', {\n instance: instance\n }).then(function(html, js) {\n Templates.runTemplateJS(js);\n return true;\n }).fail(Notification.exception);\n }\n }.bind(this));\n };\n\n /**\n * Get play back rate\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getPlaybackRate = function() {\n return this.player.getPlaybackRate();\n };\n\n /**\n * Get duration of video\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getDuration = function() {\n return this.player.getDuration();\n };\n\n /**\n * Set current time of player\n *\n * @param {float} secs time\n * @returns {Promise}\n */\n VideoTime.prototype.setCurrentPosition = function(secs) {\n return this.player.setCurrentTime(secs);\n };\n\n /**\n * Get current time of player\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getCurrentPosition = function() {\n return this.player.getCurrentTime();\n };\n\n return VideoTime;\n});\n"],"names":["define","$","Vimeo","Ajax","Config","Log","Templates","Notification","VideoTime","elementId","cmId","hasPro","interval","instance","player","resumeTime","session","played","playing","time","percent","currentTime","playbackRate","plugins","hasClass","addClass","document","on","this","initializeNewInstances","bind","modulecount","length","prototype","getCmId","registerPlugin","plugin","push","initialize","debug","autopause","Number","autoplay","background","byline","color","controls","dnt","height","loop","option_loop","maxheight","maxwidth","muted","portrait","pip","playsinline","responsive","speed","title","transparent","url","vimeo_url","width","URL","window","location","href","q","searchParams","get","starttime","match","setStartTime","then","find","catch","exception","addListeners","i","getPlayer","closest","each","tabs","index","anchor","getAttribute","addCuePoint","exeception","event","data","removeClass","offsetTop","parentNode","scrollTo","top","left","behavior","startWatchInterval","view","resume_playback","resume_time","getDuration","duration","Math","floor","setCurrentPosition","fail","getPlaybackRate","seconds","handleEnd","getSession","setSessionState","id","setPercent","setCurrentTime","getNextActivityButtonData","response","JSON","parse","parseInt","next_activity_auto","is_restricted","hasnextcm","link","nextcm_url","first","click","render","html","watchInterval","setInterval","recordWatchTime","sessionId","state","token","wwwroot","set","fetch","ok","exeption","json","call","methodname","args","Promise","resolve","getInstance","cmid","done","getResumeTime","getCurrentTime","module","attr","replace","haspro","uniqueid","js","runTemplateJS","secs","getCurrentPosition"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"videotime.min.js","sources":["../src/videotime.js"],"sourcesContent":["/*\n * @package mod_videotime\n * @copyright 2021 bdecent gmbh \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @module mod_videotime/videotime\n */\ndefine([\n 'jquery',\n 'mod_videotime/player',\n 'core/ajax',\n 'core/config',\n 'core/log',\n 'core/templates',\n 'core/notification'\n], function($, Vimeo, Ajax, Config, Log, Templates, Notification) {\n let VideoTime = function(elementId, cmId, hasPro, interval, instance) {\n this.elementId = elementId;\n this.cmId = cmId;\n this.hasPro = hasPro;\n this.interval = interval;\n this.player = null;\n this.resumeTime = null;\n this.session = null;\n this.instance = instance;\n\n this.played = false;\n\n this.playing = false;\n this.time = 0;\n this.percent = 0;\n this.currentTime = 0;\n this.playbackRate = 1;\n\n this.plugins = [];\n\n if (hasPro && $('body').hasClass('path-course-view') && !$('body').hasClass('vtinit')) {\n $('body').addClass('vtinit');\n $(document).on('focus', 'body', this.initializeNewInstances.bind(this));\n }\n this.modulecount = $('body .activity.videotime').length;\n };\n\n /**\n * Get course module ID of this VideoTime instance.\n *\n * @return {int}\n */\n VideoTime.prototype.getCmId = function() {\n return this.cmId;\n };\n\n /**\n * Register a plugin to hook into VideoTime functionality.\n *\n * @param {VideoTimePlugin} plugin\n */\n VideoTime.prototype.registerPlugin = function(plugin) {\n this.plugins.push(plugin);\n };\n\n VideoTime.prototype.initialize = function() {\n let instance = this.instance;\n Log.debug('Initializing Video Time ' + this.elementId);\n\n Log.debug('Initializing Vimeo player with options:');\n Log.debug(instance);\n this.player = new Vimeo(this.elementId, {\n autopause: Number(instance.autopause),\n autoplay: Number(instance.autoplay),\n background: Number(instance.background),\n byline: Number(instance.byline),\n color: instance.color,\n controls: Number(instance.controls),\n dnt: Number(instance.dnt),\n height: instance.height,\n loop: Number(instance.option_loop),\n maxheight: instance.maxheight,\n maxwidth: instance.maxwidth,\n muted: Number(instance.muted),\n portrait: instance.portrait,\n pip: Number(instance.pip),\n playsinline: instance.playsinline,\n responsive: Number(instance.responsive),\n speed: instance.speed,\n title: Number(instance.title),\n transparent: Number(instance.transparent),\n url: instance.vimeo_url,\n width: instance.width\n });\n\n let url = new URL(window.location.href),\n q = url.searchParams.get('q'),\n starttime = (url.searchParams.get('time') || '').match(/^([0-9]+:){0,2}([0-9]+)(\\.[0-9]+)$/);\n if (starttime) {\n this.setStartTime(starttime[0]).then(function() {\n if (q && window.find) {\n window.find(q);\n }\n return true;\n }).catch(Notification.exception);\n } else if (q && window.find) {\n window.find(q);\n }\n\n this.addListeners();\n\n for (let i = 0; i < this.plugins.length; i++) {\n const plugin = this.plugins[i];\n plugin.initialize(this, instance);\n }\n\n return true;\n };\n\n /**\n * Get Vimeo player object.\n *\n * @returns {Vimeo}\n */\n VideoTime.prototype.getPlayer = function() {\n return this.player;\n };\n\n /**\n * Register player events to respond to user interaction and play progress.\n */\n VideoTime.prototype.addListeners = function() {\n if (!this.player) {\n Log.debug('Player was not properly initialized for course module ' + this.cmId);\n return;\n }\n\n // If this is a tab play set time cues and listener.\n $($('#' + this.elementId).closest('.videotimetabs')).each(function(i, tabs) {\n $(tabs).find('[data-action=\"cue\"]').each(function(index, anchor) {\n let starttime = anchor.getAttribute('data-start'),\n time = starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/);\n if (time) {\n this.player.addCuePoint(\n 3600 * Number(time[3] || 0) + 60 * Number(time[5] || 0) + Number(time[6]),\n {\n starttime: starttime\n }\n ).catch(Notification.exeception);\n }\n }.bind(this));\n\n this.player.on('cuepoint', function(event) {\n if (event.data.starttime) {\n $(tabs).find('.videotime-highlight').removeClass('videotime-highlight');\n $(tabs).find('[data-action=\"cue\"][data-start=\"' + event.data.starttime + '\"]')\n .closest('.row')\n .addClass('videotime-highlight');\n $('.videotime-highlight').each(function() {\n if (this.offsetTop) {\n this.parentNode.scrollTo({\n top: this.offsetTop - 50,\n left: 0,\n behavior: 'smooth'\n });\n }\n });\n }\n });\n }.bind(this));\n\n // Fire view event in Moodle on first play only.\n this.player.on('play', () => {\n if (this.hasPro) {\n this.startWatchInterval();\n }\n this.view();\n return true;\n });\n\n // Features beyond this point are for pro only.\n if (!this.hasPro) {\n return;\n }\n\n // If resume is present force seek the player to that point.\n this.player.on(\"loaded\", () => {\n if (!this.instance.resume_playback || !this.instance.resume_time || this.instance.resume_time <= 0) {\n return true;\n }\n\n return this.getDuration().then((duration) => {\n let resumeTime = this.instance.resume_time;\n // Duration is often a little greater than a resume time at the end of the video.\n // A user may have watched 100 seconds when the video ends, but the duration may be\n // 100.56 seconds. BUT, sometimes the duration is rounded depending on when the\n // video loads, so it may be 101 seconds. Hence the +1 and Math.floor usage.\n if (resumeTime + 1 >= Math.floor(duration)) {\n Log.debug(\n \"VIDEO_TIME video finished, resuming at start of video.\"\n );\n resumeTime = 0;\n }\n Log.debug(\"VIDEO_TIME duration is \" + duration);\n Log.debug(\"VIDEO_TIME resuming at \" + resumeTime);\n this.setCurrentPosition(resumeTime);\n return true;\n }).fail(Notification.exception);\n });\n\n // Note: Vimeo player does not support multiple events in a single on() call. Each requires it's own function.\n\n // Catch all events where video plays.\n this.player.on('play', function() {\n this.playing = true;\n Log.debug('VIDEO_TIME play');\n }.bind(this));\n this.player.on('playing', function() {\n this.playing = true;\n Log.debug('VIDEO_TIME playing');\n }.bind(this));\n\n // Catch all events where video stops.\n this.player.on('pause', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME pause');\n }.bind(this));\n this.player.on('stalled', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME stalled');\n }.bind(this));\n this.player.on('suspend', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME suspend');\n }.bind(this));\n this.player.on('abort', function() {\n this.playing = false;\n Log.debug('VIDEO_TIME abort');\n }.bind(this));\n\n this.player.getPlaybackRate().then(function(playbackRate) {\n this.playbackRate = playbackRate;\n }.bind(this)).catch(Notification.exception);\n\n this.player.on('playbackratechange', function(event) {\n this.playbackRate = event.playbackRate;\n }.bind(this));\n\n // Always update internal values for percent and current time watched.\n this.player.on('timeupdate', function(event) {\n this.percent = event.percent;\n this.currentTime = event.seconds;\n if (event.seconds === event.duration) {\n this.plugins.forEach(plugin => {\n if (typeof plugin.setCurrentTime == 'function') {\n plugin.getSessions().then(session => {\n plugin.setCurrentTime(session.id, event.seconds);\n });\n }\n });\n }\n }.bind(this));\n\n // Initiate video finish procedure.\n this.player.on('ended', this.handleEnd.bind(this));\n };\n\n /**\n * Start interval that will periodically record user progress via Ajax.\n */\n VideoTime.prototype.handleEnd = function() {\n this.playing = false;\n Log.debug('VIDEO_TIME ended');\n if (this.plugins.length > 2) {\n this.plugins.forEach(plugin => {\n if (typeof plugin.handleEnd == 'function') {\n plugin.handleEnd();\n }\n });\n } else {\n // This moved to pro plugin, but left for compatibility.\n this.getSession().then(function(session) {\n this.setSessionState(session.id, 1).then(() => {\n return this.setPercent(session.id, 1);\n }).then(() => {\n return this.setCurrentTime(session.id, this.currentTime);\n }).then(() => {\n return this.getNextActivityButtonData(session.id).then(response => {\n let data = JSON.parse(response.data);\n\n if (data.instance && parseInt(data.instance.next_activity_auto)) {\n if (!data.is_restricted && data.hasnextcm) {\n let link = $('.aalink[href=\"' + data.nextcm_url + '\"] img').first();\n if ($('.path-course-view').length && link) {\n link.click();\n } else {\n window.location.href = data.nextcm_url;\n }\n }\n }\n\n return Templates.render('videotime/next_activity_button', JSON.parse(response.data))\n .then(function(html) {\n $('#next-activity-button').html(html);\n return true;\n });\n });\n }).catch(Notification.exception);\n }.bind(this)).catch(Notification.exception);\n }\n };\n\n /**\n * Start interval that will periodically record user progress via Ajax.\n */\n VideoTime.prototype.startWatchInterval = function() {\n this.plugins.forEach(plugin => {\n if (typeof plugin.startWatchInterval == 'function') {\n this.watchInterval = true;\n plugin.startWatchInterval();\n }\n });\n if (this.watchInterval) {\n return;\n }\n\n this.watchInterval = setInterval(function() {\n if (this.playing) {\n this.time += this.playbackRate;\n\n this.getSession().then(function(session) {\n if (this.time % this.interval === 0) {\n Log.debug('VIDEO_TIME watch_time: ' + this.time + '. percent: ' + this.percent);\n this.recordWatchTime(session.id, this.time);\n this.setPercent(session.id, this.percent);\n this.setCurrentTime(session.id, this.currentTime);\n }\n return true;\n }.bind(this)).catch(Notification.exception);\n }\n }.bind(this), 1000);\n };\n\n /**\n * Set state on session.\n *\n * @param {int} sessionId\n * @param {int} state\n * @returns {Promise}\n */\n VideoTime.prototype.setSessionState = function(sessionId, state) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_set_session_state');\n data.set('state', state);\n data.set('session_id', sessionId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_set_session_state',\n args: {\"session_id\": sessionId, state: state},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Set current watch time for video. Used for resuming.\n *\n * @param {int} sessionId\n * @param {float} currentTime\n * @returns {Promise}\n */\n VideoTime.prototype.setCurrentTime = function(sessionId, currentTime) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_set_session_current_time');\n data.set('current_time', currentTime);\n data.set('session_id', sessionId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_set_session_current_time',\n args: {\"session_id\": sessionId, \"current_time\": currentTime},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Set video watch percentage for session.\n *\n * @param {int} sessionId\n * @param {float} percent\n * @returns {Promise}\n */\n VideoTime.prototype.setPercent = function(sessionId, percent) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_set_percent');\n data.set('percent', percent);\n data.set('session_id', sessionId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_set_percent',\n args: {\"session_id\": sessionId, percent: percent},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Record watch time for session.\n *\n * @param {int} sessionId\n * @param {float} time\n * @returns {Promise}\n */\n VideoTime.prototype.recordWatchTime = function(sessionId, time) {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_record_watch_time');\n data.set('session_id', sessionId);\n data.set('time', time);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_record_watch_time',\n args: {\"session_id\": sessionId, time: time},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Get data for next activity button.\n *\n * @param {int} sessionId\n * @returns {Promise}\n */\n VideoTime.prototype.getNextActivityButtonData = function(sessionId) {\n if (this.instance.token) {\n // We do not support button in iframe.\n return Promise.resolve({data: '{}'});\n }\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_get_next_activity_button_data',\n args: {\"session_id\": sessionId}\n }])[0];\n };\n\n /**\n * Get VideoTime instance for this course module.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getInstance = function() {\n if (this.instance) {\n return Promise.resolve(this.instance);\n }\n\n return Ajax.call([{\n methodname: 'mod_videotime_get_videotime',\n args: {cmid: this.cmId},\n done: (response) => {\n this.instance = response;\n return this.instance;\n },\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Get time to resume video as seconds.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getResumeTime = function() {\n if (this.resumeTime) {\n return Promise.resolve(this.resumeTime);\n }\n\n return Ajax.call([{\n methodname: 'videotimeplugin_pro_get_resume_time',\n args: {cmid: this.cmId},\n done: (response) => {\n this.resumeTime = response.seconds;\n return this.resumeTime;\n },\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Get new or existing video viewing session.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getSession = function() {\n if (this.instance.token) {\n if (!this.session) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'videotimeplugin_pro_get_new_session');\n data.set('cmid', this.cmId);\n this.session = fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n }\n\n return this.session;\n }\n if (!this.session) {\n this.session = Ajax.call([{\n methodname: 'videotimeplugin_pro_get_new_session',\n args: {cmid: this.cmId},\n fail: Notification.exception\n }])[0];\n }\n return this.session;\n };\n\n /**\n * Parse start time and set player\n *\n * @param {string} starttime\n * @returns {Promise}\n */\n VideoTime.prototype.setStartTime = function(starttime) {\n let time = starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/);\n if (time) {\n this.resumeTime = 3600 * Number(time[3] || 0) + 60 * Number(time[5] || 0) + Number(time[6]);\n this.currentTime(this.resumeTime);\n }\n return this.player.getCurrentTime();\n };\n\n /**\n * Log the user view of video.\n *\n * @returns {Promise}\n */\n VideoTime.prototype.view = function() {\n if (this.instance.token) {\n const url = new URL(Config.wwwroot + '/webservice/rest/server.php'),\n data = url.searchParams;\n data.set('wstoken', this.instance.token);\n data.set('moodlewsrestformat', 'json');\n data.set('wsfunction', 'mod_videotime_view_videotime');\n data.set('cmid', this.cmId);\n return fetch(url).then((response) => {\n if (!response.ok) {\n Notification.exeption('Web service error');\n }\n return response.json();\n });\n\n }\n return Ajax.call([{\n methodname: 'mod_videotime_view_videotime',\n args: {cmid: this.cmId},\n fail: Notification.exception\n }])[0];\n };\n\n /**\n * Initialize new labels and preview when editing\n */\n VideoTime.prototype.initializeNewInstances = function() {\n if (this.modulecount == $('body .activity.videotime').length) {\n return;\n }\n this.modulecount = $('body .activity.videotime').length;\n $('body .activity.videotime').each(function(index, module) {\n if (\n !$(module).find('.instancename').length\n && $(module).find('.vimeo-embed').length\n && !$(module).find('.vimeo-embed iframe').length\n ) {\n let instance = {\n cmid: Number($(module).attr('id').replace('module-', '')),\n haspro: true,\n interval: this.interval,\n uniqueid: $(module).find('.vimeo-embed').first().attr('id').replace('vimeo-embed-', '')\n };\n Templates.render('mod_videotime/videotime_instance', {\n instance: instance\n }).then(function(html, js) {\n Templates.runTemplateJS(js);\n return true;\n }).fail(Notification.exception);\n }\n }.bind(this));\n };\n\n /**\n * Get play back rate\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getPlaybackRate = function() {\n return this.player.getPlaybackRate();\n };\n\n /**\n * Get duration of video\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getDuration = function() {\n return this.player.getDuration();\n };\n\n /**\n * Set current time of player\n *\n * @param {float} secs time\n * @returns {Promise}\n */\n VideoTime.prototype.setCurrentPosition = function(secs) {\n return this.player.setCurrentTime(secs);\n };\n\n /**\n * Get current time of player\n *\n * @returns {Promise}\n */\n VideoTime.prototype.getCurrentPosition = function() {\n return this.player.getCurrentTime();\n };\n\n return VideoTime;\n});\n"],"names":["define","$","Vimeo","Ajax","Config","Log","Templates","Notification","VideoTime","elementId","cmId","hasPro","interval","instance","player","resumeTime","session","played","playing","time","percent","currentTime","playbackRate","plugins","hasClass","addClass","document","on","this","initializeNewInstances","bind","modulecount","length","prototype","getCmId","registerPlugin","plugin","push","initialize","debug","autopause","Number","autoplay","background","byline","color","controls","dnt","height","loop","option_loop","maxheight","maxwidth","muted","portrait","pip","playsinline","responsive","speed","title","transparent","url","vimeo_url","width","URL","window","location","href","q","searchParams","get","starttime","match","setStartTime","then","find","catch","exception","addListeners","i","getPlayer","closest","each","tabs","index","anchor","getAttribute","addCuePoint","exeception","event","data","removeClass","offsetTop","parentNode","scrollTo","top","left","behavior","startWatchInterval","view","resume_playback","resume_time","getDuration","duration","Math","floor","setCurrentPosition","fail","getPlaybackRate","seconds","forEach","setCurrentTime","getSessions","id","handleEnd","getSession","setSessionState","setPercent","getNextActivityButtonData","response","JSON","parse","parseInt","next_activity_auto","is_restricted","hasnextcm","link","nextcm_url","first","click","render","html","watchInterval","setInterval","recordWatchTime","sessionId","state","token","wwwroot","set","fetch","ok","exeption","json","call","methodname","args","Promise","resolve","getInstance","cmid","done","getResumeTime","getCurrentTime","module","attr","replace","haspro","uniqueid","js","runTemplateJS","secs","getCurrentPosition"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/videotime.js b/amd/src/videotime.js index 69087754..59532555 100644 --- a/amd/src/videotime.js +++ b/amd/src/videotime.js @@ -183,7 +183,7 @@ define([ // If resume is present force seek the player to that point. this.player.on("loaded", () => { - if (!this.instance.resume_playback || this.instance.resume_time <= 0) { + if (!this.instance.resume_playback || !this.instance.resume_time || this.instance.resume_time <= 0) { return true; } @@ -248,7 +248,15 @@ define([ this.player.on('timeupdate', function(event) { this.percent = event.percent; this.currentTime = event.seconds; - Log.debug('VIDEO_TIME timeupdate. Percent: ' + this.percent + '. Current time: ' + this.currentTime); + if (event.seconds === event.duration) { + this.plugins.forEach(plugin => { + if (typeof plugin.setCurrentTime == 'function') { + plugin.getSessions().then(session => { + plugin.setCurrentTime(session.id, event.seconds); + }); + } + }); + } }.bind(this)); // Initiate video finish procedure. @@ -259,9 +267,16 @@ define([ * Start interval that will periodically record user progress via Ajax. */ VideoTime.prototype.handleEnd = function() { - this.playing = false; - Log.debug('VIDEO_TIME ended'); - + this.playing = false; + Log.debug('VIDEO_TIME ended'); + if (this.plugins.length > 2) { + this.plugins.forEach(plugin => { + if (typeof plugin.handleEnd == 'function') { + plugin.handleEnd(); + } + }); + } else { + // This moved to pro plugin, but left for compatibility. this.getSession().then(function(session) { this.setSessionState(session.id, 1).then(() => { return this.setPercent(session.id, 1); @@ -290,12 +305,19 @@ define([ }); }).catch(Notification.exception); }.bind(this)).catch(Notification.exception); + } }; /** * Start interval that will periodically record user progress via Ajax. */ VideoTime.prototype.startWatchInterval = function() { + this.plugins.forEach(plugin => { + if (typeof plugin.startWatchInterval == 'function') { + this.watchInterval = true; + plugin.startWatchInterval(); + } + }); if (this.watchInterval) { return; } diff --git a/plugin/videojs/amd/build/videotime.min.js b/plugin/videojs/amd/build/videotime.min.js index 54845a2d..78eec972 100644 --- a/plugin/videojs/amd/build/videotime.min.js +++ b/plugin/videojs/amd/build/videotime.min.js @@ -6,6 +6,6 @@ define("videotimeplugin_videojs/videotime",["exports","jquery","mod_videotime/vi * @module videotimeplugin_videojs/videotime * @copyright 2022 bdecent gmbh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_videotime=_interopRequireDefault(_videotime),_log=_interopRequireDefault(_log),_notification=_interopRequireDefault(_notification),_videoLazy=_interopRequireDefault(_videoLazy);class VideoTime extends _videotime.default{initialize(){_log.default.debug("Initializing Video Time "+this.elementId);let instance=this.instance,options={autoplay:Number(instance.autoplay),controls:Number(instance.controls),sources:[{type:instance.type,src:instance.vimeo_url}],loop:Number(instance.option_loop),fluid:Number(instance.responsive),playbackRates:instance.speed?[.5,.75,1,1.25,1.5,2]:[1],muted:Number(instance.muted)};"video/youtube"===instance.type&&(options.techOrder=["youtube"]),!Number(instance.responsive)&&Number(instance.height)&&Number(instance.width)&&(options.height=Number(instance.height),options.width=Number(instance.width)),_log.default.debug("Initializing VideoJS player with options:"),_log.default.debug(options),this.player=new _videoLazy.default(this.elementId,options),this.player.on("loadedmetadata",(()=>{if(!instance.resume_playback||instance.resume_time<=0||this.resumed)return!0;let duration=this.getPlayer().duration(),resumeTime=instance.resume_time;return resumeTime+1>=Math.floor(duration)&&(_log.default.debug("VIDEO_TIME video finished, resuming at start of video."),resumeTime=0),_log.default.debug("VIDEO_TIME duration is "+duration),_log.default.debug("VIDEO_TIME resuming at "+resumeTime),resumeTime&&setTimeout((()=>{this.setCurrentPosition(resumeTime)}),10),!0}));let url=new URL(window.location.href),q=url.searchParams.get("q"),starttime=(url.searchParams.get("time")||"").match(/^([0-9]+:){0,2}([0-9]+)(\.[0-9]+)$/);starttime?this.setStartTime(starttime[0]).then((function(){return q&&window.find&&window.find(q),!0})).catch(_notification.default.exception):q&&window.find&&window.find(q),this.addListeners();for(let i=0;i(this.played||(this.hasPro?this.getSession().then((()=>(this.view(),this.startWatchInterval(),!0))).catch(_notification.default.exception):this.view()),!0))),this.hasPro&&(this.player.on("play",function(){this.playing=!0,_log.default.debug("VIDEO_TIME play")}.bind(this)),this.player.on("playing",function(){this.playing=!0,_log.default.debug("VIDEO_TIME playing")}.bind(this)),this.player.on("pause",function(){this.playing=!1,_log.default.debug("VIDEO_TIME pause")}.bind(this)),this.player.on("stalled",function(){this.playing=!1,_log.default.debug("VIDEO_TIME stalled")}.bind(this)),this.player.on("suspend",function(){this.playing=!1,_log.default.debug("VIDEO_TIME suspend")}.bind(this)),this.player.on("abort",function(){this.playing=!1,_log.default.debug("VIDEO_TIME abort")}.bind(this)),this.player.on("playbackrateschange",function(){this.playbackRate=this.player.playbackRate()}.bind(this)),this.player.on("timeupdate",function(){this.currentTime=this.player.currentTime(),this.percent=this.currentTime/this.player.duration(),_log.default.debug("VIDEO_TIME timeupdate. Percent: "+this.percent+". Current time: "+this.currentTime)}.bind(this)),this.player.on("ended",this.handleEnd.bind(this)),this.player.options().responsive)){new ResizeObserver((()=>{this.player.height(this.player.videoHeight()/this.player.videoWidth()*this.player.currentWidth())})).observe(document.querySelector("#"+this.elementId))}}else _log.default.debug("Player was not properly initialized for course module "+this.cmId)}setStartTime(starttime){let time=starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\.[0-9]+))/);return time?(this.resumeTime=3600*Number(time[3]||0)+60*Number(time[5]||0)+Number(time[6]),this.player.currentTime(this.resumeTime)):(_log.default.debug("Set start time:"+starttime),this.player.currentTime())}getDuration(){return new Promise((resolve=>(resolve(this.player.duration()),!0)))}getPlaybackRate(){return new Promise((resolve=>(resolve(this.player.playbackRate()),!0)))}setCurrentPosition(secs){return new Promise((resolve=>(resolve(this.player.currentTime(secs)),!0)))}getCurrentPosition(){return new Promise((resolve=>(resolve(this.player.currentTime()),!0)))}}return _exports.default=VideoTime,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_videotime=_interopRequireDefault(_videotime),_log=_interopRequireDefault(_log),_notification=_interopRequireDefault(_notification),_videoLazy=_interopRequireDefault(_videoLazy);class VideoTime extends _videotime.default{initialize(){_log.default.debug("Initializing Video Time "+this.elementId);let instance=this.instance,options={autoplay:Number(instance.autoplay),controls:Number(instance.controls),sources:[{type:instance.type,src:instance.vimeo_url}],loop:Number(instance.option_loop),fluid:Number(instance.responsive),playbackRates:instance.speed?[.5,.75,1,1.25,1.5,2]:[1],muted:Number(instance.muted)};"video/youtube"===instance.type&&(options.techOrder=["youtube"]),!Number(instance.responsive)&&Number(instance.height)&&Number(instance.width)&&(options.height=Number(instance.height),options.width=Number(instance.width)),_log.default.debug("Initializing VideoJS player with options:"),_log.default.debug(options),this.player=new _videoLazy.default(this.elementId,options),this.player.on("loadedmetadata",(()=>{if(!instance.resume_playback||instance.resume_time<=0||this.resumed)return!0;let duration=this.getPlayer().duration(),resumeTime=instance.resume_time;return resumeTime+1>=Math.floor(duration)&&(_log.default.debug("VIDEO_TIME video finished, resuming at start of video."),resumeTime=0),_log.default.debug("VIDEO_TIME duration is "+duration),_log.default.debug("VIDEO_TIME resuming at "+resumeTime),resumeTime&&setTimeout((()=>{this.setCurrentPosition(resumeTime)}),10),!0}));let url=new URL(window.location.href),q=url.searchParams.get("q"),starttime=(url.searchParams.get("time")||"").match(/^([0-9]+:){0,2}([0-9]+)(\.[0-9]+)$/);starttime?this.setStartTime(starttime[0]).then((function(){return q&&window.find&&window.find(q),!0})).catch(_notification.default.exception):q&&window.find&&window.find(q),this.addListeners();for(let i=0;i(this.played||(this.hasPro&&this.startWatchInterval(),this.view()),!0))),this.hasPro&&(this.player.on("play",function(){this.playing=!0,_log.default.debug("VIDEO_TIME play")}.bind(this)),this.player.on("playing",function(){this.playing=!0,_log.default.debug("VIDEO_TIME playing")}.bind(this)),this.player.on("pause",function(){this.playing=!1,_log.default.debug("VIDEO_TIME pause")}.bind(this)),this.player.on("stalled",function(){this.playing=!1,_log.default.debug("VIDEO_TIME stalled")}.bind(this)),this.player.on("suspend",function(){this.playing=!1,_log.default.debug("VIDEO_TIME suspend")}.bind(this)),this.player.on("abort",function(){this.playing=!1,_log.default.debug("VIDEO_TIME abort")}.bind(this)),this.player.on("playbackrateschange",function(){this.playbackRate=this.player.playbackRate()}.bind(this)),this.player.on("timeupdate",function(){this.currentTime=this.player.currentTime(),this.percent=this.currentTime/this.player.duration(),_log.default.debug("VIDEO_TIME timeupdate. Percent: "+this.percent+". Current time: "+this.currentTime)}.bind(this)),this.player.on("ended",this.handleEnd.bind(this)),this.player.options().responsive)){new ResizeObserver((()=>{this.player.height(this.player.videoHeight()/this.player.videoWidth()*this.player.currentWidth())})).observe(document.querySelector("#"+this.elementId))}}else _log.default.debug("Player was not properly initialized for course module "+this.cmId)}setStartTime(starttime){let time=starttime.match(/((([0-9]+):)?(([0-9]+):))?([0-9]+(\.[0-9]+))/);return time?(this.resumeTime=3600*Number(time[3]||0)+60*Number(time[5]||0)+Number(time[6]),this.player.currentTime(this.resumeTime)):(_log.default.debug("Set start time:"+starttime),this.player.currentTime())}getDuration(){return new Promise((resolve=>(resolve(this.player.duration()),!0)))}getPlaybackRate(){return new Promise((resolve=>(resolve(this.player.playbackRate()),!0)))}setCurrentPosition(secs){return new Promise((resolve=>(resolve(this.player.currentTime(secs)),!0)))}getCurrentPosition(){return new Promise((resolve=>(resolve(this.player.currentTime()),!0)))}}return _exports.default=VideoTime,_exports.default})); //# sourceMappingURL=videotime.min.js.map \ No newline at end of file diff --git a/plugin/videojs/amd/build/videotime.min.js.map b/plugin/videojs/amd/build/videotime.min.js.map index 21884528..adc58873 100644 --- a/plugin/videojs/amd/build/videotime.min.js.map +++ b/plugin/videojs/amd/build/videotime.min.js.map @@ -1 +1 @@ -{"version":3,"file":"videotime.min.js","sources":["../src/videotime.js"],"sourcesContent":["/*\n * Video time player specific js\n *\n * @package videotimeplugin_videojs\n * @module videotimeplugin_videojs/videotime\n * @copyright 2022 bdecent gmbh \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from \"jquery\";\nimport VideoTimeBase from \"mod_videotime/videotime\";\nimport Log from \"core/log\";\nimport Notification from \"core/notification\";\nimport Player from \"media_videojs/video-lazy\";\nimport \"media_videojs/Youtube-lazy\";\n\nexport default class VideoTime extends VideoTimeBase {\n initialize() {\n Log.debug(\"Initializing Video Time \" + this.elementId);\n\n let instance = this.instance,\n options = {\n autoplay: Number(instance.autoplay),\n controls: Number(instance.controls),\n sources: [{type: instance.type, src: instance.vimeo_url}],\n loop: Number(instance.option_loop),\n fluid: Number(instance.responsive),\n playbackRates: instance.speed\n ? [0.5, 0.75, 1, 1.25, 1.5, 2]\n : [1],\n muted: Number(instance.muted)\n };\n if (instance.type === \"video/youtube\") {\n options.techOrder = [\"youtube\"];\n }\n if (!Number(instance.responsive) && Number(instance.height) && Number(instance.width)) {\n options.height = Number(instance.height);\n options.width = Number(instance.width);\n }\n Log.debug(\"Initializing VideoJS player with options:\");\n Log.debug(options);\n this.player = new Player(this.elementId, options);\n\n this.player.on(\"loadedmetadata\", () => {\n if (!instance.resume_playback || instance.resume_time <= 0 || this.resumed) {\n return true;\n }\n\n let duration = this.getPlayer().duration(),\n resumeTime = instance.resume_time;\n // Duration is often a little greater than a resume time at the end of the video.\n // A user may have watched 100 seconds when the video ends, but the duration may be\n // 100.56 seconds. BUT, sometimes the duration is rounded depending on when the\n // video loads, so it may be 101 seconds. Hence the +1 and Math.floor usage.\n if (resumeTime + 1 >= Math.floor(duration)) {\n Log.debug(\n \"VIDEO_TIME video finished, resuming at start of video.\"\n );\n resumeTime = 0;\n }\n Log.debug(\"VIDEO_TIME duration is \" + duration);\n Log.debug(\"VIDEO_TIME resuming at \" + resumeTime);\n if (resumeTime) {\n setTimeout(() => {\n this.setCurrentPosition(resumeTime);\n }, 10);\n }\n return true;\n });\n\n let url = new URL(window.location.href),\n q = url.searchParams.get(\"q\"),\n starttime = (url.searchParams.get(\"time\") || \"\").match(\n /^([0-9]+:){0,2}([0-9]+)(\\.[0-9]+)$/\n );\n if (starttime) {\n this.setStartTime(starttime[0])\n .then(function() {\n if (q && window.find) {\n window.find(q);\n }\n return true;\n })\n .catch(Notification.exception);\n } else if (q && window.find) {\n window.find(q);\n }\n\n this.addListeners();\n\n for (let i = 0; i < this.plugins.length; i++) {\n const plugin = this.plugins[i];\n plugin.initialize(this, instance);\n }\n\n return true;\n }\n\n /**\n * Register player events to respond to user interaction and play progress.\n */\n addListeners() {\n // If this is a tab play set time cues and listener.\n $($(\"#\" + this.elementId).closest(\".videotimetabs\")).each(\n function(i, tabs) {\n $(tabs)\n .find('[data-action=\"cue\"]')\n .each(\n function(index, anchor) {\n let starttime = anchor.getAttribute(\"data-start\"),\n time = starttime.match(\n /((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/\n );\n if (time) {\n this.player\n .addCuePoint(\n 3600 * Number(time[3] || 0) +\n 60 * Number(time[5] || 0) +\n Number(time[6]),\n {\n starttime: starttime\n }\n )\n .catch(Notification.exeception);\n }\n }.bind(this)\n );\n\n this.player.on(\"cuepoint\", function(event) {\n if (event.data.starttime) {\n $(tabs)\n .find(\".videotime-highlight\")\n .removeClass(\"videotime-highlight\");\n $(tabs)\n .find(\n '[data-action=\"cue\"][data-start=\"' +\n event.data.starttime +\n '\"]'\n )\n .closest(\".row\")\n .addClass(\"videotime-highlight\");\n $(\".videotime-highlight\").each(function() {\n if (this.offsetTop) {\n this.parentNode.scrollTo({\n top: this.offsetTop - 50,\n left: 0,\n behavior: \"smooth\"\n });\n }\n });\n }\n });\n }.bind(this)\n );\n\n if (!this.player) {\n Log.debug(\n \"Player was not properly initialized for course module \" +\n this.cmId\n );\n return;\n }\n\n // Fire view event in Moodle on first play only.\n this.player.on(\"play\", () => {\n if (!this.played) {\n if (this.hasPro) {\n // Getting a new session on first play.\n this.getSession()\n .then(() => {\n this.view();\n this.startWatchInterval();\n return true;\n })\n .catch(Notification.exception);\n } else {\n // Free version can still mark completion on video time view.\n this.view();\n }\n }\n return true;\n });\n\n // Features beyond this point are for pro only.\n if (!this.hasPro) {\n return;\n }\n\n // Note: Vimeo player does not support multiple events in a single on() call. Each requires it's own function.\n\n // Catch all events where video plays.\n this.player.on(\n \"play\",\n function() {\n this.playing = true;\n Log.debug(\"VIDEO_TIME play\");\n }.bind(this)\n );\n this.player.on(\n \"playing\",\n function() {\n this.playing = true;\n Log.debug(\"VIDEO_TIME playing\");\n }.bind(this)\n );\n\n // Catch all events where video stops.\n this.player.on(\n \"pause\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME pause\");\n }.bind(this)\n );\n this.player.on(\n \"stalled\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME stalled\");\n }.bind(this)\n );\n this.player.on(\n \"suspend\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME suspend\");\n }.bind(this)\n );\n this.player.on(\n \"abort\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME abort\");\n }.bind(this)\n );\n\n this.player.on(\n \"playbackrateschange\",\n function() {\n this.playbackRate = this.player.playbackRate();\n }.bind(this)\n );\n\n // Always update internal values for percent and current time watched.\n this.player.on(\n \"timeupdate\",\n function() {\n this.currentTime = this.player.currentTime();\n this.percent = this.currentTime / this.player.duration();\n Log.debug(\n \"VIDEO_TIME timeupdate. Percent: \" +\n this.percent +\n \". Current time: \" +\n this.currentTime\n );\n }.bind(this)\n );\n\n // Initiate video finish procedure.\n this.player.on(\"ended\", this.handleEnd.bind(this));\n\n // Readjust height when responsive player is resized.\n if (this.player.options().responsive) {\n let observer = new ResizeObserver(() => {\n this.player.height(\n (this.player.videoHeight() / this.player.videoWidth()) *\n this.player.currentWidth()\n );\n });\n observer.observe(document.querySelector(\"#\" + this.elementId));\n }\n }\n\n /**\n * Parse start time and set player\n *\n * @param {string} starttime\n * @returns {Promise}\n */\n setStartTime(starttime) {\n let time = starttime.match(\n /((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/\n );\n if (time) {\n this.resumeTime =\n 3600 * Number(time[3] || 0) +\n 60 * Number(time[5] || 0) +\n Number(time[6]);\n return this.player.currentTime(this.resumeTime);\n }\n Log.debug(\"Set start time:\" + starttime);\n return this.player.currentTime();\n }\n\n /**\n * Get play back rate\n *\n * @returns {Promise}\n */\n getDuration() {\n return new Promise(resolve => {\n resolve(this.player.duration());\n return true;\n });\n }\n\n /**\n * Get duration of video\n *\n * @returns {Promise}\n */\n getPlaybackRate() {\n return new Promise(resolve => {\n resolve(this.player.playbackRate());\n return true;\n });\n }\n\n /**\n * Set current time of player\n *\n * @param {float} secs time\n * @returns {Promise}\n */\n setCurrentPosition(secs) {\n return new Promise(resolve => {\n resolve(this.player.currentTime(secs));\n return true;\n });\n }\n\n /**\n * Get current time of player\n *\n * @returns {Promise}\n */\n getCurrentPosition() {\n return new Promise(resolve => {\n resolve(this.player.currentTime());\n return true;\n });\n }\n}\n"],"names":["VideoTime","VideoTimeBase","initialize","debug","this","elementId","instance","options","autoplay","Number","controls","sources","type","src","vimeo_url","loop","option_loop","fluid","responsive","playbackRates","speed","muted","techOrder","height","width","player","Player","on","resume_playback","resume_time","resumed","duration","getPlayer","resumeTime","Math","floor","setTimeout","setCurrentPosition","url","URL","window","location","href","q","searchParams","get","starttime","match","setStartTime","then","find","catch","Notification","exception","addListeners","i","plugins","length","closest","each","tabs","index","anchor","getAttribute","time","addCuePoint","exeception","bind","event","data","removeClass","addClass","offsetTop","parentNode","scrollTo","top","left","behavior","played","hasPro","getSession","view","startWatchInterval","playing","playbackRate","currentTime","percent","handleEnd","ResizeObserver","videoHeight","videoWidth","currentWidth","observe","document","querySelector","cmId","getDuration","Promise","resolve","getPlaybackRate","secs","getCurrentPosition"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"videotime.min.js","sources":["../src/videotime.js"],"sourcesContent":["/*\n * Video time player specific js\n *\n * @package videotimeplugin_videojs\n * @module videotimeplugin_videojs/videotime\n * @copyright 2022 bdecent gmbh \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from \"jquery\";\nimport VideoTimeBase from \"mod_videotime/videotime\";\nimport Log from \"core/log\";\nimport Notification from \"core/notification\";\nimport Player from \"media_videojs/video-lazy\";\nimport \"media_videojs/Youtube-lazy\";\n\nexport default class VideoTime extends VideoTimeBase {\n initialize() {\n Log.debug(\"Initializing Video Time \" + this.elementId);\n\n let instance = this.instance,\n options = {\n autoplay: Number(instance.autoplay),\n controls: Number(instance.controls),\n sources: [{type: instance.type, src: instance.vimeo_url}],\n loop: Number(instance.option_loop),\n fluid: Number(instance.responsive),\n playbackRates: instance.speed\n ? [0.5, 0.75, 1, 1.25, 1.5, 2]\n : [1],\n muted: Number(instance.muted)\n };\n if (instance.type === \"video/youtube\") {\n options.techOrder = [\"youtube\"];\n }\n if (!Number(instance.responsive) && Number(instance.height) && Number(instance.width)) {\n options.height = Number(instance.height);\n options.width = Number(instance.width);\n }\n Log.debug(\"Initializing VideoJS player with options:\");\n Log.debug(options);\n this.player = new Player(this.elementId, options);\n\n this.player.on(\"loadedmetadata\", () => {\n if (!instance.resume_playback || instance.resume_time <= 0 || this.resumed) {\n return true;\n }\n\n let duration = this.getPlayer().duration(),\n resumeTime = instance.resume_time;\n // Duration is often a little greater than a resume time at the end of the video.\n // A user may have watched 100 seconds when the video ends, but the duration may be\n // 100.56 seconds. BUT, sometimes the duration is rounded depending on when the\n // video loads, so it may be 101 seconds. Hence the +1 and Math.floor usage.\n if (resumeTime + 1 >= Math.floor(duration)) {\n Log.debug(\n \"VIDEO_TIME video finished, resuming at start of video.\"\n );\n resumeTime = 0;\n }\n Log.debug(\"VIDEO_TIME duration is \" + duration);\n Log.debug(\"VIDEO_TIME resuming at \" + resumeTime);\n if (resumeTime) {\n setTimeout(() => {\n this.setCurrentPosition(resumeTime);\n }, 10);\n }\n return true;\n });\n\n let url = new URL(window.location.href),\n q = url.searchParams.get(\"q\"),\n starttime = (url.searchParams.get(\"time\") || \"\").match(\n /^([0-9]+:){0,2}([0-9]+)(\\.[0-9]+)$/\n );\n if (starttime) {\n this.setStartTime(starttime[0])\n .then(function() {\n if (q && window.find) {\n window.find(q);\n }\n return true;\n })\n .catch(Notification.exception);\n } else if (q && window.find) {\n window.find(q);\n }\n\n this.addListeners();\n\n for (let i = 0; i < this.plugins.length; i++) {\n const plugin = this.plugins[i];\n plugin.initialize(this, instance);\n }\n\n return true;\n }\n\n /**\n * Register player events to respond to user interaction and play progress.\n */\n addListeners() {\n // If this is a tab play set time cues and listener.\n $($(\"#\" + this.elementId).closest(\".videotimetabs\")).each(\n function(i, tabs) {\n $(tabs)\n .find('[data-action=\"cue\"]')\n .each(\n function(index, anchor) {\n let starttime = anchor.getAttribute(\"data-start\"),\n time = starttime.match(\n /((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/\n );\n if (time) {\n this.player\n .addCuePoint(\n 3600 * Number(time[3] || 0) +\n 60 * Number(time[5] || 0) +\n Number(time[6]),\n {\n starttime: starttime\n }\n )\n .catch(Notification.exeception);\n }\n }.bind(this)\n );\n\n this.player.on(\"cuepoint\", function(event) {\n if (event.data.starttime) {\n $(tabs)\n .find(\".videotime-highlight\")\n .removeClass(\"videotime-highlight\");\n $(tabs)\n .find(\n '[data-action=\"cue\"][data-start=\"' +\n event.data.starttime +\n '\"]'\n )\n .closest(\".row\")\n .addClass(\"videotime-highlight\");\n $(\".videotime-highlight\").each(function() {\n if (this.offsetTop) {\n this.parentNode.scrollTo({\n top: this.offsetTop - 50,\n left: 0,\n behavior: \"smooth\"\n });\n }\n });\n }\n });\n }.bind(this)\n );\n\n if (!this.player) {\n Log.debug(\n \"Player was not properly initialized for course module \" +\n this.cmId\n );\n return;\n }\n\n // Fire view event in Moodle on first play only.\n this.player.on(\"play\", () => {\n if (!this.played) {\n if (this.hasPro) {\n this.startWatchInterval();\n }\n // Free version can still mark completion on video time view.\n this.view();\n }\n return true;\n });\n\n // Features beyond this point are for pro only.\n if (!this.hasPro) {\n return;\n }\n\n // Note: Vimeo player does not support multiple events in a single on() call. Each requires it's own function.\n\n // Catch all events where video plays.\n this.player.on(\n \"play\",\n function() {\n this.playing = true;\n Log.debug(\"VIDEO_TIME play\");\n }.bind(this)\n );\n this.player.on(\n \"playing\",\n function() {\n this.playing = true;\n Log.debug(\"VIDEO_TIME playing\");\n }.bind(this)\n );\n\n // Catch all events where video stops.\n this.player.on(\n \"pause\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME pause\");\n }.bind(this)\n );\n this.player.on(\n \"stalled\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME stalled\");\n }.bind(this)\n );\n this.player.on(\n \"suspend\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME suspend\");\n }.bind(this)\n );\n this.player.on(\n \"abort\",\n function() {\n this.playing = false;\n Log.debug(\"VIDEO_TIME abort\");\n }.bind(this)\n );\n\n this.player.on(\n \"playbackrateschange\",\n function() {\n this.playbackRate = this.player.playbackRate();\n }.bind(this)\n );\n\n // Always update internal values for percent and current time watched.\n this.player.on(\n \"timeupdate\",\n function() {\n this.currentTime = this.player.currentTime();\n this.percent = this.currentTime / this.player.duration();\n Log.debug(\n \"VIDEO_TIME timeupdate. Percent: \" +\n this.percent +\n \". Current time: \" +\n this.currentTime\n );\n }.bind(this)\n );\n\n // Initiate video finish procedure.\n this.player.on(\"ended\", this.handleEnd.bind(this));\n\n // Readjust height when responsive player is resized.\n if (this.player.options().responsive) {\n let observer = new ResizeObserver(() => {\n this.player.height(\n (this.player.videoHeight() / this.player.videoWidth()) *\n this.player.currentWidth()\n );\n });\n observer.observe(document.querySelector(\"#\" + this.elementId));\n }\n }\n\n /**\n * Parse start time and set player\n *\n * @param {string} starttime\n * @returns {Promise}\n */\n setStartTime(starttime) {\n let time = starttime.match(\n /((([0-9]+):)?(([0-9]+):))?([0-9]+(\\.[0-9]+))/\n );\n if (time) {\n this.resumeTime =\n 3600 * Number(time[3] || 0) +\n 60 * Number(time[5] || 0) +\n Number(time[6]);\n return this.player.currentTime(this.resumeTime);\n }\n Log.debug(\"Set start time:\" + starttime);\n return this.player.currentTime();\n }\n\n /**\n * Get play back rate\n *\n * @returns {Promise}\n */\n getDuration() {\n return new Promise(resolve => {\n resolve(this.player.duration());\n return true;\n });\n }\n\n /**\n * Get duration of video\n *\n * @returns {Promise}\n */\n getPlaybackRate() {\n return new Promise(resolve => {\n resolve(this.player.playbackRate());\n return true;\n });\n }\n\n /**\n * Set current time of player\n *\n * @param {float} secs time\n * @returns {Promise}\n */\n setCurrentPosition(secs) {\n return new Promise(resolve => {\n resolve(this.player.currentTime(secs));\n return true;\n });\n }\n\n /**\n * Get current time of player\n *\n * @returns {Promise}\n */\n getCurrentPosition() {\n return new Promise(resolve => {\n resolve(this.player.currentTime());\n return true;\n });\n }\n}\n"],"names":["VideoTime","VideoTimeBase","initialize","debug","this","elementId","instance","options","autoplay","Number","controls","sources","type","src","vimeo_url","loop","option_loop","fluid","responsive","playbackRates","speed","muted","techOrder","height","width","player","Player","on","resume_playback","resume_time","resumed","duration","getPlayer","resumeTime","Math","floor","setTimeout","setCurrentPosition","url","URL","window","location","href","q","searchParams","get","starttime","match","setStartTime","then","find","catch","Notification","exception","addListeners","i","plugins","length","closest","each","tabs","index","anchor","getAttribute","time","addCuePoint","exeception","bind","event","data","removeClass","addClass","offsetTop","parentNode","scrollTo","top","left","behavior","played","hasPro","startWatchInterval","view","playing","playbackRate","currentTime","percent","handleEnd","ResizeObserver","videoHeight","videoWidth","currentWidth","observe","document","querySelector","cmId","getDuration","Promise","resolve","getPlaybackRate","secs","getCurrentPosition"],"mappings":"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"} \ No newline at end of file diff --git a/plugin/videojs/amd/src/videotime.js b/plugin/videojs/amd/src/videotime.js index 25241f56..e6b55da1 100644 --- a/plugin/videojs/amd/src/videotime.js +++ b/plugin/videojs/amd/src/videotime.js @@ -165,18 +165,10 @@ export default class VideoTime extends VideoTimeBase { this.player.on("play", () => { if (!this.played) { if (this.hasPro) { - // Getting a new session on first play. - this.getSession() - .then(() => { - this.view(); - this.startWatchInterval(); - return true; - }) - .catch(Notification.exception); - } else { - // Free version can still mark completion on video time view. - this.view(); + this.startWatchInterval(); } + // Free version can still mark completion on video time view. + this.view(); } return true; }); From 6cecee76ac26fbe86abf512040c3781cb0d162a2 Mon Sep 17 00:00:00 2001 From: Daniel Thies Date: Fri, 4 Aug 2023 16:35:14 -0500 Subject: [PATCH 4/5] VID-536: Add open and close dates --- lang/en/videotime.php | 4 ++++ mod_form.php | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lang/en/videotime.php b/lang/en/videotime.php index d88922b8..e3360386 100644 --- a/lang/en/videotime.php +++ b/lang/en/videotime.php @@ -40,6 +40,8 @@ $string['authenticate_vimeo_success'] = 'Vimeo authentication successful. You can now use features that rely on the Vimeo API.'; $string['averageviewtime'] = 'Average view time'; $string['browsevideos'] = 'Browse videos'; +$string['calendarend'] = '{$a} closes'; +$string['calendarstart'] = '{$a} opens'; $string['choose_video'] = 'Choose Video'; $string['choose_video_confirm'] = 'Are you sure you want to choose the video'; $string['cleanupalbumsandtags'] = 'Cleanup albums and tags'; @@ -322,3 +324,5 @@ $string['completiondetail:_on_view_time'] = 'View for time {$a}'; $string['completiondetail:_on_finish'] = 'Finish video'; $string['completiondetail:_on_percent'] = 'Finish watching {$a} percent'; +$string['videoopen'] = 'Allow viewing from'; +$string['videoclose'] = 'Allow viewing until'; diff --git a/mod_form.php b/mod_form.php index a254b91a..2808e958 100644 --- a/mod_form.php +++ b/mod_form.php @@ -141,6 +141,9 @@ public function definition() { } } + // Add availability section. + //$this->availability_elements(); + // Add standard elements. $this->standard_coursemodule_elements(); @@ -190,6 +193,28 @@ protected function standard_intro_elements($customlabel=null) { } } + /** + * Add availability section + * + * @throws coding_exception + */ + protected function availability_elements() { + + $mform = $this->_form; + $mform->addElement('header', 'availabilityhdr', get_string('availability')); + $mform->addElement( + 'date_time_selector', 'timeopen', get_string("videoopen", "videotime"), ['optional' => true] + ); + + $mform->addElement( + 'date_time_selector', 'timeclose', get_string("videoclose", "videotime"), ['optional' => true] + ); + + $mform->addElement('advcheckbox', 'showpreview', get_string('showpreview', 'choice')); + $mform->addHelpButton('showpreview', 'showpreview', 'choice'); + $mform->disabledIf('showpreview', 'timeopen[enabled]'); + } + /** * Add custom completion rules. * From 4a2e11a4dfe6ebfacad7b26dd8ffd84418c34863 Mon Sep 17 00:00:00 2001 From: Daniel Thies Date: Tue, 15 Aug 2023 14:49:57 -0500 Subject: [PATCH 5/5] VID-690: Update Dash session data source --- .../attribute/completion_status_attribute.php | 64 +++++++++++++++++++ .../attribute/first_session_attribute.php | 4 +- .../attribute/last_session_attribute.php | 4 +- .../block_dash/attribute/views_attribute.php | 4 +- .../videotime_sessions_data_source.php | 17 +++-- .../structure/videotime_session_table.php | 27 ++++++-- .../structure/videotime_table.php | 2 +- classes/vimeo_embed.php | 2 - lang/en/videotime.php | 7 ++ 9 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 classes/local/block_dash/attribute/completion_status_attribute.php diff --git a/classes/local/block_dash/attribute/completion_status_attribute.php b/classes/local/block_dash/attribute/completion_status_attribute.php new file mode 100644 index 00000000..1c5acf78 --- /dev/null +++ b/classes/local/block_dash/attribute/completion_status_attribute.php @@ -0,0 +1,64 @@ +. + +/** + * Transforms completion status into human readable form + * + * @package mod_videotime + * @copyright 2020 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_videotime\local\block_dash\attribute; + +use block_dash\local\data_grid\field\attribute\abstract_field_attribute; +use mod_videotime\videotime_instance; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/completionlib.php'); + +/** + * Transforms data to average view time. + * + * @package mod_videotime + * @copyright 2020 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class completion_status_attribute extends abstract_field_attribute { + + /** + * After records are relieved from database each field has a chance to transform the data. + * Example: Convert unix timestamp into a human readable date format + * + * @param int $data + * @param \stdClass $record Entire row + * @return mixed + * @throws \moodle_exception + */ + public function transform_data($data, \stdClass $record) { + global $DB; + if ($data == COMPLETION_COMPLETE) { + return get_string("completed", "mod_videotime"); + } else if ($data == COMPLETION_COMPLETE_PASS) { + return get_string("passed", "mod_videotime"); + } else if ($data == COMPLETION_COMPLETE_FAIL) { + return get_string("failed", "mod_videotime"); + } else { + return get_string("incomplete", "mod_videotime"); + } + } +} diff --git a/classes/local/block_dash/attribute/first_session_attribute.php b/classes/local/block_dash/attribute/first_session_attribute.php index 5237dfd1..7aece919 100644 --- a/classes/local/block_dash/attribute/first_session_attribute.php +++ b/classes/local/block_dash/attribute/first_session_attribute.php @@ -48,8 +48,8 @@ public function transform_data($data, \stdClass $record) { $instance = videotime_instance::instance_by_id($data); - return $DB->get_field_sql('SELECT MIN(vts.timestarted) + return $DB->get_field_sql('SELECT MIN(vts.timecreated) FROM {videotimeplugin_pro_session} vts - WHERE vts.module_id = ? AND vts.timestarted > 0', [$instance->get_cm()->id]); + WHERE vts.module_id = ? AND vts.timecreated > 0', [$instance->get_cm()->id]); } } diff --git a/classes/local/block_dash/attribute/last_session_attribute.php b/classes/local/block_dash/attribute/last_session_attribute.php index a90dd5a1..12065047 100644 --- a/classes/local/block_dash/attribute/last_session_attribute.php +++ b/classes/local/block_dash/attribute/last_session_attribute.php @@ -48,8 +48,8 @@ public function transform_data($data, \stdClass $record) { $instance = videotime_instance::instance_by_id($data); - return $DB->get_field_sql('SELECT MAX(vts.timestarted) + return $DB->get_field_sql('SELECT MAX(vts.timecreated) FROM {videotimeplugin_pro_session} vts - WHERE vts.module_id = ? AND vts.timestarted > 0', [$instance->get_cm()->id]); + WHERE vts.module_id = ? AND vts.timecreated > 0', [$instance->get_cm()->id]); } } diff --git a/classes/local/block_dash/attribute/views_attribute.php b/classes/local/block_dash/attribute/views_attribute.php index ea26383e..862fd59d 100644 --- a/classes/local/block_dash/attribute/views_attribute.php +++ b/classes/local/block_dash/attribute/views_attribute.php @@ -49,7 +49,9 @@ public function transform_data($data, \stdClass $record) { $instance = videotime_instance::instance_by_id($data); return $DB->get_field_sql( - 'SELECT COUNT(*) FROM {videotimeplugin_pro_session} vts WHERE vts.module_id = ?', + 'SELECT COUNT(DISTINCT uuid) + SUM(CASE WHEN uuid IS NULL THEN 1 ELSE 0 END) + FROM {videotimeplugin_pro_session} vts + WHERE vts.module_id = ?', [$instance->get_cm()->id] ); } diff --git a/classes/local/block_dash/videotime_sessions_data_source.php b/classes/local/block_dash/videotime_sessions_data_source.php index 1798facb..47393def 100644 --- a/classes/local/block_dash/videotime_sessions_data_source.php +++ b/classes/local/block_dash/videotime_sessions_data_source.php @@ -26,6 +26,7 @@ use block_dash\local\dash_framework\query_builder\builder; use block_dash\local\dash_framework\query_builder\join; +use block_dash\local\dash_framework\query_builder\where; use block_dash\local\dash_framework\structure\user_table; use block_dash\local\data_grid\filter\bool_filter; use block_dash\local\data_grid\filter\course_condition; @@ -79,14 +80,22 @@ public function get_query_template(): builder { $builder = new builder(); $builder - ->select('vts.id', 'vts_id') + ->select("CONCAT(vt.id, '-', u.id)", 'vtn_id') ->from('videotime', 'vt') + ->join('videotime', 'vts', 'id', 'vt.id') ->join('course_modules', 'cm', 'instance', 'vt.id', join::TYPE_INNER_JOIN, ['cm.module' => $module]) - ->join('videotime_pro_session', 'vts', 'module_id', 'cm.id') - ->join('user', 'u', 'id', 'vts.user_id') + ->join('videotimeplugin_pro_session', 'vtn', 'module_id', 'cm.id') + ->join('user', 'u', 'id', 'vtn.user_id') ->join('course', 'c', 'id', 'vt.course') ->join('course_categories', 'cc', 'id', 'c.category') - ->groupby('vt.id')->groupby('vts.user_id'); + ->join('videotime_vimeo_video', 'vvv', 'link', 'vt.vimeo_url', join::TYPE_LEFT_JOIN) + ->join('course_modules_completion', 'cmc', 'coursemoduleid', 'cm.id', join::TYPE_LEFT_JOIN, ['cmc.userid' => 'u.id']) + ->groupby('vt.id') + ->groupby('vts.id') + ->groupby('u.id') + ->groupby('c.id') + ->groupby('vvv.duration') + ->groupby('vtn.user_id'); $filterpreferences = $this->get_preferences('filters'); diff --git a/classes/local/dash_framework/structure/videotime_session_table.php b/classes/local/dash_framework/structure/videotime_session_table.php index 5ea70c10..53bd2e7b 100644 --- a/classes/local/dash_framework/structure/videotime_session_table.php +++ b/classes/local/dash_framework/structure/videotime_session_table.php @@ -48,6 +48,7 @@ use mod_videotime\local\block_dash\attribute\video_created_attribute; use mod_videotime\local\block_dash\attribute\video_preview_attribute; use mod_videotime\local\block_dash\attribute\views_attribute; +use mod_videotime\local\block_dash\attribute\completion_status_attribute; use moodle_url; defined('MOODLE_INTERNAL') || die(); @@ -65,7 +66,7 @@ class videotime_session_table extends table { * Build a new table. */ public function __construct() { - parent::__construct('videotimeplugin_pro_session', 'vts'); + parent::__construct('videotime', 'vts'); } /** @@ -83,17 +84,17 @@ public function get_title(): string { * @return field_interface[] */ public function get_fields(): array { - return [ + $fields = [ new field('id', new lang_string('pluginname', 'videotime'), $this, null, [ new identifier_attribute() ]), new field('time', new lang_string('watch_time', 'videotime'), $this, 'SUM(time)', [ new time_attribute() ]), - new field('state', new lang_string('state_finished', 'videotime'), $this, 'MAX(state)', [ + new field('state', new lang_string('state_finished', 'videotime'), $this, 'MAX(vtn.state)', [ new bool_attribute() ]), - new field('timestarted', new lang_string('timestarted', 'videotime'), $this, 'MIN(vts.timestarted)', [ + new field('timestarted', new lang_string('timestarted', 'videotime'), $this, 'MIN(vtn.timecreated)', [ new date_attribute() ]), new field('percent_watch', new lang_string('watch_percent', 'videotime'), $this, 'MAX(percent_watch)', [ @@ -101,7 +102,23 @@ public function get_fields(): array { ]), new field('current_watch_time', new lang_string('currentwatchtime', 'videotime'), $this, 'MAX(current_watch_time)', [ new time_attribute() - ]) + ]), ]; + + if (videotime_has_repository()) { + $addfields = [ + new field('watched_time', new lang_string('watchedtime', 'videotime'), $this, 'MAX(current_watch_time)', [ + new time_attribute() + ]), + new field('time_left', new lang_string('timeleft', 'videotime'), $this, 'MIN(vvv.duration - current_watch_time)', [ + new time_attribute() + ]), + new field('status', new lang_string('activitystatus', 'videotime'), $this, 'MAX(cmc.completionstate)', [ + new completion_status_attribute() + ]) + ]; + $fields = array_merge($fields, $addfields); + } + return $fields; } } diff --git a/classes/local/dash_framework/structure/videotime_table.php b/classes/local/dash_framework/structure/videotime_table.php index 37961d5c..3ae41bbf 100644 --- a/classes/local/dash_framework/structure/videotime_table.php +++ b/classes/local/dash_framework/structure/videotime_table.php @@ -87,7 +87,7 @@ public function get_fields(): array { new field('id', new lang_string('pluginname', 'videotime'), $this, null, [ new identifier_attribute() ]), - new field('name', new lang_string('activity_name', 'videotime'), $this), + new field('name', new lang_string('activity_name', 'videotime'), $this, 'vt.name'), new field('url', new lang_string('videotimeurl', 'videotime'), $this, 'vt.id', [ new moodle_url_attribute(['url' => new moodle_url('/mod/videotime/view.php', ['v' => 'vt_id'])]) ]), diff --git a/classes/vimeo_embed.php b/classes/vimeo_embed.php index 4d55056b..c51faf03 100644 --- a/classes/vimeo_embed.php +++ b/classes/vimeo_embed.php @@ -42,8 +42,6 @@ */ class vimeo_embed implements \renderable, \templatable { - protected $cm = null; - /** * @var $cm Course module */ diff --git a/lang/en/videotime.php b/lang/en/videotime.php index 6d00c75e..cb2bb550 100644 --- a/lang/en/videotime.php +++ b/lang/en/videotime.php @@ -73,6 +73,9 @@ $string['confirmation'] = 'Confirmation'; $string['create_vimeo_app'] = 'Create Vimeo App'; $string['currentwatchtime'] = 'Current watch time'; +$string['watchedtime'] = "Time watched"; +$string['timeleft'] = "Time Left"; +$string['activitystatus'] = "Activity completion status"; $string['datasource:videotime_sessions_data_source'] = 'Video Time sessions'; $string['datasource:videotime_stats_data_source'] = 'Video Time stats'; $string['default'] = 'Default'; @@ -326,5 +329,9 @@ $string['completiondetail:_on_view_time'] = 'View for time {$a}'; $string['completiondetail:_on_finish'] = 'Finish video'; $string['completiondetail:_on_percent'] = 'Finish watching {$a} percent'; +$string['completed'] = "Completed"; +$string['passed'] = "Passed"; +$string['failed'] = "Failed"; +$string['incomplete'] = "Incomplete"; $string['videoopen'] = 'Allow viewing from'; $string['videoclose'] = 'Allow viewing until';